Codementor Events

Geo-fencing Users With Lambda @ Edge and CloudFront

Published May 25, 2018Last updated Nov 20, 2018
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!

Discover and read more posts from Alfonso Gober
get started
post commentsBe the first to share your opinion
Show more replies