Redirect users based on country with Lambda@Edge
What is Lambda@Edge?
Edge computing is the idea of moving compute resources closer to your users. Instead of having centralised locations where your servers are running, functions are run at edge locations distributed around the world. Using edge computing for routing is one of the most common and beneficial applications – having redirections and routing rules handled closer to the user can result in a significant reduction in latency. In a traditional setup, these rules would have been handled by a service like Apache or NGINX and require managing deployments, instances, and more.
Lambda@Edge is AWS’ solution for edge computing. It’s designed specifically for routing and works with CloudFront.
Example Situation
We’re running an ecommerce store, dogtreats.com, with a dedicated website and subdomain for each region we sell in, for example us.dogtreats.com and fr.dogtreats.com. When advertising our store, we want to tell people to head to our root-level domain (dogtreats.com) and have them redirected to their local store.
We want this redirect to be fast and scalable, with protection against DDoS attacks, minimal maintenance, and low costs. This is the perfect use-case for using CloudFront with Lambda@Edge.
Solution
There are two main steps to creating a CloudFront distribution using Lambda@Edge. First, we need to create a Lambda function, then we need to associate it with the distribution.
Lambda function
Head to the AWS console and click Create Function within the Lambda console. Lambda@Edge functions must be created in N. Virginia (us-east-1), otherwise you won’t be able to use them.
Select “Author from scratch”, then type in the name of your function. Then, we need to select the runtime. We’re going to write our Lambda using Node.js, so we’d naturally select the latest version (Node.js 12.x). Lambda@Edge doesn’t support Node.js 12 yet, so we’re forced to use Node.js 10.x instead.
We also need to create an execution role so our Lambda function has the AWS permissions required to run. Luckily, AWS has a policy template for this, so select “Create a new role from AWS policy templates”, give your role any name you’d like, then select the “Basic Lambda@Edge permissions” policy template.
Once you click “Create function”, you’ll be redirected to the function details page. We’re going to want to edit the function code to perform our redirection logic.
Now, we need to write the logic to redirect users to the correct regional store. CloudFront adds a header called CloudFront-Viewer-Country to requests, which contains the ISO-3166-1 alpha-2 country code of the user.
Click “Save” as soon as you’ve finished writing the Lambda logic. Now, we need to check that our function is working correctly. Click on “Test” and then “Create new test event”.
Let’s see how our function behaves when we give it a request from France…
Great! The user will be redirected to fr.dogtreats.com. Now we’re ready to create the CloudFront distribution.
CloudFront distribution
First, head to the CloudFront console and click Create Distribution, then click “Get Started” on the standard Web distribution. The first part of the configuration is very simple – the origin configuration doesn’t matter as all requests will be redirected by the Lambda. It’s a good idea to select “Redirect HTTP to HTTPS” to ensure all requests are made via HTTPS.
Next, we can configure our caching behaviour. Our redirections shouldn’t need to change very often, so here I’ve set them to be cached for 1 day (86400 seconds). To make the caching work, we need to cache based on the CloudFront-Viewer-Country header.
Finish up by setting up any custom SSL certificates or Alternate Domain Names(CNAMEs), in this case we’d need a certificate for dogtreats.com and we’d set our CNAMEs to dogtreats.com. Do NOT create any Lambda Function Associations at this stage. Make sure you set a comment on the distribution so it’s easy to find later.
Now we can head back to the Lambda console to associate the Lambda with our distribution. Click on the “Add trigger” button in the function designer, then select CloudFront from the triggers dropdown. Choose your newly created distribution and leave the cache behaviour as the default – *. Ensure the CloudFront event is set to “Origin request” and body is not included, then hit “Deploy”. AWS will start deploying your Lambda@Edge function.
It can take up to 20 minutes for the CloudFront distribution to update, but once it’s complete we can test our implementation using curl, or just head to the distribution in your browser.
Run curl -v https://YOUR-DISTRIBUTION.cloudfront.net/ in your terminal, then look for the location header in the response, for example location: us.dogtreats.com.
How can ipdata.co help?
ipdata.co offers an ultra-fast, highly-available API for looking up the physical location of any IP address. The results returned from ipdata will be more accurate and detailed – allowing redirections to take place based on state, continent, or based on whether the user is in an EU country.
We can easily update our Lambda function to call the ipdata API and redirect based on the response.
const https = require("https");
// Configuration to map country codes to regional stores
const regionalStores = new Map([
["US", "us.dogtreats.com"],
["FR", "fr.dogtreats.com"],
// We've added a new store to cover the rest of the European Union
["EU", "eu.dogtreats.com"]
]);
// Get an ipdata API Key from here: https://ipdata.co/sign-up.html
const IPDATA_API_KEY = "test";
// Enabling keepAlive on the https agent will speed requests up
const httpsAgent = new https.Agent({ keepAlive: true });
const getIPData = ip => {
const url = `https://api.ipdata.co/${ip}?api-key=${IPDATA_API_KEY}`;
return new Promise((resolve, reject) => {
https
.get(url, { agent: httpsAgent }, res => {
let result = "";
res.on("data", chunk => {
result += chunk;
});
res.on("end", () => {
resolve(JSON.parse(result));
});
})
.on("error", err => {
reject(err);
});
});
};
const handler = async (event, context) => {
// Extract the CloudFront request details
const request = event.Records[0].cf.request;
// Get detailed information about this IP from ipdata.co
const ipdata = await getIPData(request.clientIp);
// This IP is flagged as a threat! Let's block them out
if (ipdata.threat.is_threat) {
return {
status: "403",
statusDescription: "Forbidden"
};
}
const ipdataCountry = ipdata.country_code.toUpperCase();
// Set our default regional store
// This is used if there's no matching store configuration
let regionalStore = regionalStores.get("US");
// Set the new regional store, if it's set in the regionalStores map
if (regionalStores.has(ipdataCountry)) {
regionalStore = regionalStores.get(ipdataCountry);
} else if (ipdata.is_eu) {
// If there's no dedicated regional store, and the user is EU-based
// let's take them to our EU store
regionalStore = regionalStores.get("EU");
}
// Create a redirection response for CloudFront
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [
{
key: "Location",
value: "https://" + regionalStore
}
]
}
};
return response;
};
module.exports = { handler };
We’ve also added some extra logic to block IP addresses which have been marked as a threat. To use this code, we need to disable caching on our CloudFront distribution as otherwise it’ll cache the result for a single IP as the result for an entire country.
Now, we can update our Lambda function with this new logic. Edit the function code, save it, and test it using different clientIp values.
Once you’re happy with the behaviour, we need to update CloudFront to use the new function version. Just the same as before, click on “Add trigger”, “CloudFront”, then “Deploy to Lambda@Edge”. In the pop-up, select “Use existing CloudFront trigger on this function” and deploy. Your CloudFront distribution will now update. Now, requesting the distribution from the UK will take users to eu.dogtreats.com instead of us.dogtreats.com. Perfect.
Conclusions
Using Lambda@Edge for redirecting users is a perfect use-case. It’s cheap and easy to maintain, and, if you use the ipdata API, can perform advanced functionality such as accurately redirecting based on state, continent and more. In our example, we were also able to block IP addresses which could be a threat to our business.
Once you have Lambda@Edge running, you can also perform many more advanced actions, such as redirecting users based on short links (for example, dogtreats.com/offer to us.dogtreats.com/products/1111) or A/B testing new versions of your website.