Using Lambda@Edge with CloudFront to configure redirect rules for domains

An AWS CDK project to create CloudFront distributions with Lambda@Edge functions to replace Apache and mod_rewrite rules

I'm a big fan of cool URLs and not breaking the internet and so over the years I've accumulated a few domains for which I've implemented a lot of redirect rules. In recent years I've implemented these rules using mod_rewrite and run them on Apache and on my Linode VPS, e.g. the redirect rules for blog.floehopper.org.

However, in September 2019 I started publishing this website on GitHub Pages and over the intervening period I've removed pretty much everything else I was running on that VPS. And so I'd started thinking it would be nice to shutdown the VPS and stop paying for it! The legacy redirects mentioned above were the only things preventing me from doing this.

In the last 18 months I've used the AWS CDK quite a bit both at work and for personal projects. Unfortunately I've only got round to publishing one article about it (something I hope to remedy in the not too distant future). Anyway, I'd read a bit about Lambda@Edge and noticed that it was indeed supported by CloudFormation and the AWS CDK, so I thought I'd give it a whirl.

My basic idea was to create a CloudFront distribution, associate a custom domain with it, configure an Edge Lambda function to handle all requests to that distribution and implement the appropriate redirects in JavaScript, then point the relevant DNS records at the CloudFront distribution. This turned out to be pretty straightforward, although I did have to use the AWS Certificate Manager to create an SSL certificate for the domain to get everything to work.

You can find the full project source code on GitHub. I'm afraid I haven't yet got round to updating the README from the default version generated by the AWS CDK. If you prefer, here's a slightly cut down version of a couple of key project files illustrating how I did this for the blog.floehopper.org domain:

  
    # file: lib/edge-redirector-stack.ts

    import * as cdk from '@aws-cdk/core';
    import * as lambda from '@aws-cdk/aws-lambda';
    import * as cloudfront from '@aws-cdk/aws-cloudfront';
    import * as origins from '@aws-cdk/aws-cloudfront-origins';
    import * as acm from '@aws-cdk/aws-certificatemanager';

    export class EdgeRedirectorStack extends cdk.Stack {
      constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        this.createDistribution('blog.floehopper.org', 'blogFloehopperOrg', 'arn:aws:acm:us-east-1:687105911108:certificate/aa11ee5a-54db-4a04-8307-f77330f86cb5');
      }

      createDistribution(domain: string, handler: string, certificateArn: string) {
        const certificate = acm.Certificate.fromCertificateArn(this, `${handler}Certificate`, certificateArn);
        new cloudfront.Distribution(this, `${handler}Distribution`, {
          defaultBehavior: {
            origin: new origins.HttpOrigin('example.com'),
            edgeLambdas: [
              {
                eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
                functionVersion: this.redirectVersion(domain, handler)
              }
            ]
          },
          domainNames: [domain],
          certificate: certificate,
          enableLogging: true
        });
      }

      redirectVersion(domain: string, handler: string) : lambda.IVersion {
        const redirectFunction = new cloudfront.experimental.EdgeFunction(this, `${handler}Redirect`, {
          runtime: lambda.Runtime.NODEJS_12_X,
          handler: `${handler}.handler`,
          code: lambda.Code.fromAsset('./lambdaFunctions/redirect')
        });

        return redirectFunction.currentVersion;
      }
    }
  
  
    # file: lambdaFunctions/redirect/blogFloehopperOrg.js

    'use strict';

    exports.handler = function(event, context, callback) {
      const request = event.Records[0].cf.request;

      const mapping = [
        // Legacy Typo-style articles
        ['^/articles/([0-9]{4})/([0-9]{2})/([0-9]{2})/(.+)$', (m) => `http://jamesmead.org/blog/${m[1]}-${m[2]}-${m[3]}-${m[4]}`],

        // Redirect blog.floehopper.org -> jamesmead.org
        ['^/(.*)$', (m) => `http://jamesmead.org/${m[1]}`],
        ['^$', (m) => `http://jamesmead.org`],
    ];

      let redirectUrl;
      for (const [pattern, url] of mapping) {
        const match = request.uri.match(new RegExp(pattern));
        if (match) {
          if (typeof(url) == 'function') {
            redirectUrl = url(match);
          } else {
            redirectUrl = url;
          };
          break;
        };
      };

      let response;

      if (redirectUrl) {
        response = {
          status: '301',
          statusDescription: 'Moved Permanently',
          headers: {
            location: [{
              key: 'Location',
              value: redirectUrl
            }],
          }
        };
      } else {
        response = {
          status: '404',
          statusDescription: 'Not Found'
        };
      };

      callback(null, response);
    };