• Simple AWS
  • Posts
  • Geolocation-Based Redirects using Lambda@Edge

Geolocation-Based Redirects using Lambda@Edge

Using CloudFront and Lambda@Edge to show a different page to customers in different countries

In the past few issues, you were running an e-commerce app. Now it's my turn to run the e-commerce! And we're going to solve my problem this time (I mean, I've been solving yours for 30 issues…).

I have a static website hosted in an S3 bucket, with a CloudFront distribution in front of it. Let's focus on that, check past issues for DynamoDB design, transactions, streams, multiple Lambdas and all that stuff.

My problem: I'm from Argentina! (in case you didn't know). I want customers to go to downloads.simpleaws.dev and be automatically redirected based on where they're from: to downloads.simpleaws.dev/en-us.html if they're from the US, and to downloads.simpleaws.dev/index.html (which is in Spanish) if they're from anywhere else. Originally I wanted to do it the other way round: default en-us, special case es-ar, which makes a lot more sense; I changed it to simplify the testing steps.

We're going to use the following AWS services:

  • S3: It just stores and serves the website. It won't solve the problem.

  • CloudFront: Caches content in a lot of servers distributed globally, and serves it from there with much lower latency. It doesn't run any logic by itself, but it can invoke the following service:

  • Lambda@Edge: Lambda functions that run at Edge Locations (the same servers where CloudFront serves its cached content from). They're triggered by CloudFront at certain parts of the request-response process, and can affect what CloudFront returns.

Identify the geographical origin of the request, and use a Lambda@Edge function to pick the correct HTML page to respond to it.

Request flow for users in the US and outside the US

I'm going to assume you already have an S3 bucket and a CloudFront distribution created, and all HTML files created and uploaded. Here's a CloudFormation template to create them, and you can deploy it with one click:

And use these HTML files (I know, I'm the best web designer in the world). The CloudFormation template creates an S3 bucket, upload these there with the names index.html and en-us.html respectively.

<!DOCTYPE html>
<html>
    <head>
        <title>Página para otros clientes</title>
    </head>
    <body>
        <h1>Página para otros clientes</h1>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <title>Page for US customers</title>
    </head>
    <body>
        <h1>Page for US customers</h1>
    </body>
</html>

Test this before applying the solution: Go to the CloudFront distribution and open the URL (it looks like https://d12345qwertyu6.cloudfront.net).

Step by step

Step 1: Create a CloudFront Cache policy to forward country header

  1. Go to the CloudFront console and on the menu on the left click on Policies

  2. Under "Custom Policies" click on "Create cache policy"

  3. Fill the required fields:

    1. For name, enter something like "simple-aws-redirect-policy".

    2. For "Cache key settings" click on the "Headers" dropdown and select "Include the following headers"

    3. Click "Add header", search for "CloudFront-Viewer-Country" and select it

  4. Leave the rest as default and click "Create"

Step 2: Create the Lambda@Edge function

If you haven't deployed the initial-setup cfn template, create an IAM Role for your Lambda@Edge function, with permissions for CloudWatch Logs and a trust policy for lambda.amazonaws.com and edgelambda.amazonaws.com.

After that, create the Lambda@Edge function:

  1. Go to the Lambda Console and click "Create Function"

  2. Fill the required fields:

    1. For name, enter something like "simple-aws-redirect-function"

    2. For runtime, choose "Node.js 18.x"

  3. Click "Change default execution role"

  4. Select "Use an existing role" and under Existing role choose the role "simple-aws-lambda-role" created by the initial-setup template, or the one you just created

  5. Click "Create Function"

  6. In the function page (after it was created), under "Code source", paste the code below and click on "Deploy"

  7. Scroll up, click "Actions" and click "Publish new version". Add a version description, like "simple-aws-redirect-function" and click and click "Publish"

const AWS = require('aws-sdk');

exports.handler = async (event, context) => {
    // Get the Country Code from the Request
    const viewerCountryHeader = event.Records[0].cf.request.headers['cloudfront-viewer-country'];
    const viewerCountry = getCountryViewerHeader[0].value;

    // Respond according to the country
    if (viewerCountry === 'US') {
        // If the country is US, redirect to /en-us.html
        const response = {
            status: '301',
            statusDescription: 'Permanent Redirect',
            headers: {
                location: [{
                    key: 'Location',
                    value: '/en-us.html'
                }]
            }
        };
        return response;
    } else {
        // If the country is something other than the US, let the request continue its course
        let request = event.Records[0].cf.request;
        return request;
    }
};

Step 3: Create a CloudFront behavior

  1. Go to the CloudFront console and click on your existing distribution

  2. Click on the "Behaviors" tab and then on "Create behavior"

  3. Enter the following data:

    1. For "Path Pattern" enter "/index.html"

    2. Under "Origin and origin groups", expand the drop down and choose your S3 bucket

    3. For "Cache key and origin requests", select "Cache policy and origin request policy (recommended)" and under "Cache policy" select the policy you created in Step 1

    4. Leave the rest of the settings as default

  4. Click "Create Behavior"

Step 4: Associate the Lambda function with the CloudFront behavior

  1. Go back to the Lambda console and open your function created in Step 2

  2. Click on "+ Add trigger" and choose CloudFront

  3. Click on "Deploy to Lambda@Edge"

  4. In the new window that opens:

    1. Under "Distribution", select your existing CloudFront distribution

    2. Under "Cache behavior", select "/index.html"

    3. Under "CloudFront event", select "Origin request"

    4. Mark the "Confirm deploy to Lambda@Edge" check box and click "Deploy"

Step 5: Test it!

  1. Go to the CloudFront console, click on your distribution and copy the domain name (it looks like https://d12345qwertyu6.cloudfront.net)

  2. Select a region that's in the US (regardless of where you deployed this)

  3. Open the CloudShell Console

  4. Run the following command, replacing the URL with your CloudFront distribution's domain name (that you just copied): curl -v -o /dev/null https://d12345qwertyu6.cloudfront.net

  5. Check that the page says "Page for US customers"

  6. Open the AWS Console in a new tab

  7. Select a region that's not in the US (regardless of where you deployed this)

  8. Open the CloudShell Console in the new tab

  9. Run the same command as above, once again replacing the URL with your CloudFront distribution's domain name (exactly like you ran it earlier): curl -v -o /dev/null https://d12345qwertyu6.cloudfront.net

  10. Check that the page says "Página para otros clientes" (it means Page for other customers, in Spanish)

Explanation

Step 1: Create a CloudFront Cache policy to forward country header

This is the non-intuitive step, if you're not familiar with CloudFront. Our Lambda needs to know the Country header (obviously), but it's not passed to the Origin by default, basically because the default behavior of CloudFront doesn't use it to decide whether to cache the content or not. Everything that it forwards is part of the key in the key-value store that's the cache, and everything that isn't part of the key (and of the decision of whether this request is identical to the one for which it has a cached response) isn't passed to the Origin (whatever sits behind CloudFront, S3 in this case) or to the Lambda@Edge functions.

The Cache policy tells CloudFront what parameters to use to determine whether two requests are identical and should receive the same response (which it may pull from the cache). We're creating a new policy to tell CloudFront to include the Country header in that decision. We need this, otherwise a user in the US would get the cached response that another user from Argentina got, because CloudFront wouldn't be able to tell the two requests apart. We also need this so the Country header is passed to our Lambda@Edge function.

Step 2: Create the Lambda@Edge function

This one's pretty straightforward: Create a regular Lambda function with code that digs out the value for Viewer Country from the request, and if it has a specific value ("US" in this case), it returns an HTTP code that tells the browser to GET a different page, which CloudFront sends back to the browser. If the value is not that specific value, it just returns the request, which CloudFront interprets as "keep going", so it forwards the request to the Origin (S3), which returns the default page.

Step 3: Create a CloudFront behavior

The core resource in CloudFront is a Distribution. It doesn't do much by itself, mainly it has a URL. A Distribution also has one or more Origins, which are whatever ends up responding to the requests for which CloudFront doesn't have a cached response. In our case, we only have one Origin: an S3 bucket. And a Distribution has one or more Behaviors, which are the rules that CloudFront will use to process requests. A Behavior includes a few interesting things like the pattern for the requested path (for example "/", if it matches for a request then the distribution uses this behavior) the Cache policy (which we created in Step 1), which Origin to use, the Lambda@Edge functions, and basically most settings that you'd typically associate with CloudFront.

We're creating a new Behavior because we want to use the Cache policy we created in Step 1. For Path Pattern we're setting /index.html because we want this to only apply to that page.

Step 4: Associate the Lambda function with the CloudFront behavior

Before this step, our Lambda function is just a regular Lambda function in our region. When we add CloudFront as a trigger, we get the action to deploy it to Lambda@Edge, which will deploy it to the Edge locations that CloudFront uses. We associate it with a specific Distribution, a Behavior in that Distribution, and an Event. The Event in this case is Origin Request, which means when CloudFront didn't find the response in the cache and sent a request to the origin. These are the other 3 options.

Step 5: Test it!

To test this, you need to send a request from the country that you're adding this custom behavior for (US in this case) and a request from somewhere other than that country (Argentina in my narrative, but it could be any country). We're doing this really easily with CloudShell, which is a shell running in an instance in the region, with access to the internet, to the AWS CLI using your current role, and a couple of utilities. CloudShell in a region in the US (such as us-east-1) will send a request that CloudFront views as coming from the US, and CloudShell in a region outside the US (such as eu-central-1) will send a request from outside the US.

This is why I switched my original example, where the default was the website in English and the special case was the website in Spanish (/es-ar.html) for customers in Argentina. That made a lot more sense in the narrative, but we don't have an AWS region in Argentina, so testing this would have been a lot more cumbersome, and would have deviated our attention. Third world problems.

Discussion

There's other ways to achieve the same result of customers viewing different pages depending on where they're from:

  • Localized text: If all you want to change is the language of the text, you can just follow the regular internationalization practices: Use placeholders everywhere instead of hardcoded text, and replace those placeholders with the actual values when the page is rendered (either client-side or server-side). If the page is rendered client-side, you might still want to use this to pull the right language file, but language files aren't that large anyways.

  • Server-side rendering: If you're using server-side rendering that depends on the Country header, you don't need the Lambda@Edge since your Origin (a backend capable of rendering the website) will include that same if (ViewerCountry === "US") code. You will need the rest of this solution, so the Country header is passed by CloudFront to the Origin as part of the Origin Request. Note that in this case you can change a lot more than just the texts.

  • Modular Frontend: Most of us are used to viewing a page as an HTML file that has all the elements, a CSS file that makes it pretty, and a JavaScript file that controls what happens when the user clicks buttons. A modular frontend is an HTML file with all the <div></div>s and structure, but the contents of each <div></div> are determined by different backend modules (possibly even microservices!). Consider a social network, you'd have one section for the feed, one for the ads on the side, one for the nav bar with notifications, and so on. In that case you don't need to pick a different HTML file for customers in a different country, but you will need to include the Country header in the request to that module. It can use client-side or server-side rendering.

Language isn't the only reason you'd want to change the content based on the Country. Another example could be country-specific offers, taxes calculations, etc. And Country isn't the only available header, here's a complete list.

Best Practices

Operational Excellence

  • Monitoring and Alerting: Use CloudWatch to monitor Lambda@Edge functions and CloudFront distributions. Set up alarms for things like error rates, latency, etc to detect and respond to issues.

Security

  • Use WAF: AWS WAF (Web Application Firewall) helps protect a website against common web exploits like SQL injection and XSS. You can set it up in the CloudFront Behavior.

  • Secure the S3 bucket: Ensure that the S3 bucket that serves the website is not publicly accessible and that the access is only via CloudFront. Here's how to do it.

Reliability

  • Handle Lambda@Edge Errors: Handle errors appropriately, for example if the CloudFront-Viewer-Country header is missing or has an unexpected value. An unhandled error could cause the function to fail and impact the website's availability.

Performance Efficiency

  • Optimize Lambda@Edge Execution Time: The execution time for the Lambda@Edge function is added to the time the request takes to travel to CloudFront, to the Origin (if necessary), for the Origin to process the request, etc. Optimizing the functions that you deploy to Lambda@Edge is critical for a good user experience. This is especially important if they're executed on Viewer Request or Viewer Response, which are events that happen every time CloudFront receives a request or sends a response to the user. Origin Request and Origin Response only happen when CloudFront doesn't have a cached response and forwards the request to the Origin, in those cases the response time is already longer and the Origin (your backend) comes into play.

Cost Optimization

  • Use CloudFront Price Classes: If your users are concentrated in specific geographic regions, consider choosing a lower price class in CloudFront to serve the content from a subset of AWS edge locations at a lower price. Users that don't have an Edge Location nearby can still access your content, they'll just experience higher latency (but never worse than if you didn't have CloudFront at all).

Richard Donovan is a coach for software engineers. That means no generic BS, just real advice from someone who's an expert at software (like us) and an expert at coaching. He has a newsletter, and he's offering his subscribers a few free 30-minute coaching sessions every week. If you want in:

If you're a dev, you want this book. Lucky for you, it's free!

And if you like coding challenges that are actually good (not leetcode stuff about counting parentheses) and where you actually learn stuff, check out John Crickett's latest one:

I've already shared with you the book Code Your Future: A Guide to Career Change and Success in Software Engineering. I know it's not for you, but how often does a friend ask you how to get into software engineering? Twice a week for me. So, I just got all of our not-yet-software-engineers friends a 20% discount code! 37YFNYA. Link with code included:

Did you like this issue?

Login or Subscribe to participate in polls.

Join the conversation

or to participate.