AWS CloudFront Lambda@Edge semi-static content hosting

Learn how to use AWS CloudFront, Lambda@Edge, localized S3 buckets, and a global Redis database to host semi-static content with in-house tracking globally. Optimize your website's performance and reliability with these AWS services.

Clock Icon
6 minutes
Calendar Icon
8/7/24
Person Icon
Antoine Français
Summary
Summary
Down Arrow
Share this article
Looking for more leads?
Link Icon

In this guide, we will discuss how to use AWS services to host semi-static content generated by Gatsby, a popular static site generator, and implement in-house tracking across the globe. This involves using AWS CloudFront, Lambda@Edge, localized S3 buckets, and a global Redis database. We will cover how to set up the architecture, route requests using Geo DNS entries, and implement tracking via Lambda@Edge.

Using Gatsby for Static Website Generation

Architecture Overview

Step 1: Build Gatsby Site

Gatsby is a powerful static site generator that uses React and GraphQL to build fast, modern websites. We will configure Gatsby to generate landing pages from a JSON configuration file that describes the content of each page.

Gatsby will use this JSON configuration to build the static HTML, CSS, and JavaScript files.

{
  "title": "Sample Landing Page",
  "content": [
    {
      "type": "header",
      "value": "Welcome to our website!"
    },
    {
      "type": "paragraph",
      "value": "This is a sample paragraph with some introductory text."
    }
  ]
}

Step 2: Host Gatsby Generated Files on S3

Once the Gatsby site is generated, the static files will be hosted on Amazon S3. We will create localized S3 buckets to store copies of the site in different geographic regions to ensure low-latency access for users worldwide.

Diagram illustrating the setup of localized S3 buckets across different AWS regions

Configuring AWS CloudFront with Geo DNS and Lambda@Edge

Step 1: Configure Geo DNS in Route 53

AWS Route 53 will be used to route users to the nearest S3 bucket based on their geographic location. This is achieved using a Geo DNS entry.

aws route53 change-resource-record-sets --hosted-zone-id Z3M3LMPEXAMPLE --change-batch file://geodns.json

geodns.json:

{
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "nearestS3.test.com",
        "Type": "CNAME",
        "GeoLocation": {
          "ContinentCode": "NA"
        },
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "us-west-2.s3.amazonaws.com"
          }
        ]
      }
    },
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "nearestS3.test.com",
        "Type": "CNAME",
        "GeoLocation": {
          "ContinentCode": "EU"
        },
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "eu-central-1.s3.amazonaws.com"
          }
        ]
      }
    }
  ]
}

Step 2: Implement DNS Resolution with Lambda@Edge

A Lambda@Edge function will be used to resolve the DNS entry and direct traffic to the appropriate S3 bucket. This function will be triggered by CloudFront requests.

'use strict';

const https = require('https');

exports.handler = async (event) => {
    const request = event.Records[0].cf.request;

    // Resolve the nearest S3 bucket address
    const s3Address = await resolveNearestS3();

    // Modify request to point to the resolved S3 bucket
    request.origin = {
        s3: {
            domainName: s3Address,
            region: '',
            authMethod: 'none',
            path: '',
        }
    };
    request.headers['host'] = [{ key: 'host', value: s3Address }];

    return request;
};

async function resolveNearestS3() {
    return new Promise((resolve, reject) => {
        https.get('https://nearestS3.test.com', (res) => {
            let data = '';
            res.on('data', (chunk) => { data += chunk; });
            res.on('end', () => { resolve(data.trim()); });
        }).on('error', (err) => { reject(err); });
    });
}

Implementing Tracking with Lambda@Edge and Redis

Step 1: Create Unique IDs and Set Cookies

A Lambda@Edge function will generate a unique ID for each user at runtime and store it in a cookie. This cookie will be used for tracking purposes and will be stored in a global Redis database.

const Redis = require('ioredis');
const redis = new Redis({ host: 'global.redis.endpoint', port: 6379 });

exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    let uniqueId;

    // Check if the user already has a unique ID cookie
    if (headers.cookie) {
        const cookies = headers.cookie[0].value.split(';');
        for (let cookie of cookies) {
            if (cookie.trim().startsWith('uid=')) {
                uniqueId = cookie.split('=')[1];
                break;
            }
        }
    }

    // If no unique ID is found, generate a new one
    if (!uniqueId) {
        uniqueId = generateUniqueId();
        const setCookieHeader = `uid=${uniqueId}; Path=/; HttpOnly`;
        headers['set-cookie'] = [{ key: 'Set-Cookie', value: setCookieHeader }];
        
        // Store the unique ID in Redis
        await redis.set(uniqueId, JSON.stringify({ createdAt: new Date().toISOString() }));
    }

    return request;
};

function generateUniqueId() {
    return Math.random().toString(36).substring(2) + Date.now().toString(36);
}

Step 2: Store and Retrieve Tracking Data in Redis

The Redis database will store tracking data, such as user visits, actions, and other relevant metrics.

const Redis = require('ioredis');
const redis = new Redis({ host: 'global.redis.endpoint', port: 6379 });

exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    const uniqueId = getUniqueIdFromHeaders(headers);

    // Retrieve tracking data from Redis
    let trackingData = await redis.get(uniqueId);
    if (trackingData) {
        trackingData = JSON.parse(trackingData);
        trackingData.lastVisit = new Date().toISOString();
    } else {
        trackingData = { createdAt: new Date().toISOString(), lastVisit: new Date().toISOString() };
    }

    // Update tracking data in Redis
    await redis.set(uniqueId, JSON.stringify(trackingData));

    return request;
};

function getUniqueIdFromHeaders(headers) {
    if (headers.cookie) {
        const cookies = headers.cookie[0].value.split(';');
        for (let cookie of cookies) {
            if (cookie.trim().startsWith('uid=')) {
                return cookie.split('=')[1];
            }
        }
    }
    return null;
}

Quote Icon