Geo-fencing Users With Lambda @ Edge and CloudFront
Image: MPD01605,flickr. CC BY-SA 2.0
Let’s say you’ve got an AWS CloudFront distribution serving your website. Most static S3 websites, for example, sit behind a CloudFront distribution, as do many ECS apps set up with Elastic Load Balancers.
Let’s also say your website needs to redirect users to a specific page or subdomain, based on the country they are in.
As it turns out, CloudFront does IP geolocation by default, and it’s straightforward to leverage.
With the looming onset of the EU’s General Data Protection Regulation, redirection based on whether or not the user’s IP is in the EU might be a good canonical example of such an implementation.
I wrote this article to teach you, brave engineer, how you can perform this detection and redirection with zero changes to your application code, using AWS Lambda @ Edge and CloudFront.
Write your Lambdas
Let’s write ourselves some Lambda functions. We’ll use Serverless to scaffold this whole thing, so make sure you’ve installed Serverless and set up your AWS credentials before going any further.
Once that’s done, you’re ready to rock.
Create a Serverless project:
serverless create --template aws-nodejs --path serverless-geolocation
cd serverless-geolocation
Open up serverless.yml
and replace it with the following contents:
service: geolocation
custom:
stage: ${opt:stage, self:provider.stage}
provider:
name: aws
runtime: nodejs6.10 # Node 8 is not yet supported by Lambda@Edge, only in normal Lambda
stage: dev
memorySize: 256
timeout: 30
region: us-east-1
apiKeys:
- ${self:custom.stage}-geolocation-apikey
environment:
DOMAIN: mydomain.com # replace this with your domain name
iamRoleStatements:
- Effect: "Allow"
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
functions:
edgeRedirect:
handler: handler.edgeRedirect
countryLookup:
handler: handler.countryLookup
events:
- http:
path: /
method: get
private: true
cors: true
This creates two functions, edgeRedirect
and countryLookup
.
edgeRedirect
will be the Edge Lambda handling Cloudfront origin requests, and leveraging CloudFront’s built-in geolocation feature.
countryLookup
is a general-purpose geolocation Lambda using the normal lambda-proxy/API Gateway integration and MaxMind GeoLite2 open-source data to create an endpoint that will detect the user’s country. This allows you to geolocate user IPs in a non CloudFront-dependent way (for example, in a mobile app or any other context). This method would require changes to application code, and custom logic to handle the results.
Open up handler.js
and replace its contents with the following:
"use strict";
const util = require("util");
const Cookie = require("cookie");
const Reader = require("mmdb-reader");
// edgeRedirect solves the Cloudfront website use-case
/*
Typical Cloudfront event structure to consume:
{
"Records": [
{
"cf": {
"request": {
"uri": "/test",
"headers": {
"cloudfront-viewer-country": [
{
key: "Cloudfromt-Viewer-Country",
value: "FR"
}
]
}
}
}
}
]
}
*/
module.exports.edgeRedirect = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const uri = request.uri === "/index.html" ? "/" : request.uri;
const countryCode = headers["cloudfront-viewer-country"][0].value;
console.log(`Country detected: '${countryCode}'`);
if (countryCode !== "US") {
// Non-US IP was detected by CloudFront
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [
{
key: "Location",
value: `https://${countryCode.toLowerCase()}.${process.env.DOMAIN}/${uri}`
}
],
["cache-control"]: [
{
key: "Cache-Control",
value: "no-cache, no-store, private"
}
]
}
};
callback(null, response);
} else {
// Allow request to continue as intended
callback(null, request);
}
};
// countryLookup solves all non-CloudFront use-cases, such as mobile
module.exports.countryLookup = (
{ requestContext },
ctx,
callback
) => {
let ip =
requestContext &&
requestContext.identity &&
requestContext.identity.sourceIp
? requestContext.identity.sourceIp
: callback(null, { statusCode: 422, body: "Unprocessable Request" });
Reader.open(__dirname + "/data/GeoLite2-Country.mmdb", (error, reader) => {
if (error) callback(error);
else {
let { iso_code, is_in_european_union } = reader.lookup(ip).country;
callback(null, {
statusCode: 200,
body: JSON.stringify({
iso_code,
is_in_european_union: is_in_european_union || false
})
});
}
});
};
The gist (pun intended) here is that edgeRedirect
will pull the ISO country code from the “Cloudfront-Viewer-Country” header, and redirect to the lowercase version of that code as a subdomain on your DOMAIN
environment variable.
Now download the MaxMind GeoLite2 data.
Create the folder "data" in your service. Unzip the .tar.gz file and extract the contents into it.
Deploy your functions
Deploy with serverless deploy --stage dev
.
After a few minutes, you should see
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (1.66 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
....................
Serverless: Stack update finished...
Service Information
service: geolocation
stage: dev
region: us-east-1
stack: geolocation-dev
api keys:
dev-geolocation-apikey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
endpoints:
GET - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/
functions:
edgeRedirect: geolocation-dev-edgeRedirect
countryLookup: geolocation-dev-countryLookup
Save the apikey and the GET URL somewhere. You’ll need it to access your lookup endpoint.
You should be able to curl your new endpoint now:
curl -H "X-Api-Key:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/
You should see
{"iso_code":"US","is_in_european_union":false}
Great, it’s half-done. Now for the tricky part: Lambda @ Edge.
Set up Lambda @ Edge
Lambda@Edge is not like normal Lambda. There are significantly more setup steps involved, which I’ll enumerate here.
First, go to the AWS CloudFront console. Select the distribution you want to add your Lambda to, and click the “Behaviors” tab.
Select “Default (*)” behavior and click “Edit.”
In the Cache Based on Selected Request Headers section, click the dropdown and choose “Whitelist.”
Now under Whitelist Headers section, choose “CloudFront-Viewer-Country”. Your section should look like this:
In another tab, go to the AWS Lambda console. Search for “geolocation” and select the edgeRedirect lambda you wish to use.
Under the “Qualifiers” drop-down (located just underneath the ARN, at the top-right of the page), select “Versions.”
Here’s an important thing to note: You cannot use the $LATEST version in an Edge Lambda. Don’t ask me why — it’s an AWS thing. So you must choose the latest version that isn’t $LATEST. Here’s a visual example.
In this case, I’d choose 2 (5/13/2018) because it’s the latest non-$LATEST version. Yeah it’s a mouthful.
In a third tab, open the AWS IAM console.
Go to “Roles” and search for “geolocation.” Find the role that Serverless created for you when you deployed your function. It should be something like “geolocation-dev-us-east-1-lambdaRole.”
Click the role and click the “Trust Relationships” tab. Click “Edit Trust Relationship."
Replace its contents with:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
Click “Update Trust Policy” and close the tab.
Now go back to your Lambda console tab and copy the ARN from the top-right. It should include the version number (in this case, 2) at the end.
Go back to your CloudFront console tab and paste the ARN in the Lambda Function Associations section. Select “Origin Request” in the dropdown. It should end up looking like this:
Click “Yes, Edit” and you’re all set. Changes may take a few minutes to propagate.
Test it out by accessing your site from a non-US location or using a VPN set to a non-US location. If you’re in Amsterdam, for example, you’ll be redirected to nl.yourdomain.com. A user in Shanghai will be redirected to cn.yourdonaim.com, and so on.
Don’t forget to clap if this article helped you!