6

I have been searching for a way to do the following if someone can enlighten me would be greatly appreciated.

e.g. would the following mappings be possible in a single CloudFront instance?

feature-a.domain.com => dev-bucket/feature-a
feature-b.domain.com => dev-bucket/feature-b
staging.domain.com   => dev-bucket/staging

and so forth..

The use case is I want to be able to deploy as many environments for each git branch which maps to a bucket on S3 that exists. Would this all be possible?

Van Nguyen
  • 568
  • 3
  • 6
  • 15

1 Answers1

4

Yes, it's possible, but you'll need to use the Lambda@Edge enhancement to CloudFront in order to manipulate the request parameters before sending the request to the bucket.

I described one possible solution in this official forum post. That same solution is included, below.

Lambda@Edge allows programmatic access to the HTTP requests or responses as CloudFront is processing them -- providing trigger hooks, essentially. The HTTP transaction is presented as a javascript object that you can observe and modify and then return control to CloudFront.

For this application, you need to use the web site hosting endpoint of the bucket as your origin domain name in CloudFront config (i.e. don't select the bucket from the dropdown -- type it in, using the appropriate "s3-website" hostname) and you need to whitelist the Host header for forwarding to the origin, even though we won't actually be forwarding it (we'll be reading it and then manipulating it, along with the path) and even though, normally, forwarding this to an S3 origin would not work as intended... but we need to tell CloudFront to whitelist it so that it can be read and manipulated.

Configure the following Lambda function as Origin Request trigger. This trigger fires after the CloudFront cache is checked and a cache miss occurs, and before the request is sent to the origin server (S3).

'use strict';

// https://serverfault.com/a/930191/153161
// if the end of incoming Host header matches this string, 
// strip this part and prepend the remaining characters onto the request path,
// along with a new leading slash (otherwise, the request will be handled
// with an unmodified path, at the root of the bucket)

// set this to what we should remove from the incoming hostname, including the leading dot.

const remove_suffix = '.example.com';

// provide the correct origin hostname here so that we send the correct 
// Host header to the S3 website endpoint
// this is the same value as "origin domain name" in the cloudfront distribution configuration

const origin_hostname = 'example-bucket.s3-website-us-east-1.amazonaws.com';

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;
  const host_header = headers.host[0].value;

  if(host_header.endsWith(remove_suffix))
  {
    // prepend '/' + the subdomain onto the existing request path ("uri")
    request.uri = '/' + host_header.substring(0,host_header.length - remove_suffix.length) + request.uri;
  }

  // fix the host header so that S3 understands the request
  // we have to do this even if the above if() didn't match
  headers.host[0].value = origin_hostname;

  // return control to CloudFront with the modified request
  return callback(null,request);
};

Remember also to set your error caching minimum TTL to 0 in order to avoid having CloudFront cache error responses. This is a separare setting from min/default/max TTL in the Cache Behavior settings. The default is 5 minutes, which makes sense, but complicates troubleshooting if you don't anticipate this behavior.


Note that Lambda@Edge functions can now use either the v6.10 or v8.10 Node.js runtime environment. The above example was originally written for v6.10, but is compatible with either runtime because in v8.10 the handler has more options: it can use async/await, it can return a promise directly, or it can be written as shown above to use the callback interface.

Michael - sqlbot
  • 21,988
  • 1
  • 57
  • 81