Wildcard subdomains point to appropriate S3/CloudFront subdirectories

update: this answer was correct when written, and the techniques described below are still perfectly viable but potentially less desirable since Lambda@Edge can now be used to accomplish this objective, as I explained in my answer to Serving a multitude of static sites from a wildcard domain in AWS.


No, there is no way to do this automatically.

Is there a way to accomplish this without running a server for redirection?

Technically, it isn't redirection that you'd need, to accomplish this. You'd need path rewriting, and that's why the answer to your ultimate question is "no" -- because Route 53 (and DNS in general) can't do anything related to paths.

Route 53 does support wildcard DNS, but that's of limited help without CloudFront and/or S3 supporting a mechanism to put the host header from the HTTP request into the path (which they don't).

Now, this could easily be accomplished in a "zero-touch" mode with a single Route 53 * wildcard entry, a single CloudFront distribution configured for *.example.com, and one or more EC2 instances running HAProxy to do the request path rewriting and proxy the request onward to the S3 bucket. A single line in a basic configuration file would accomplish that request rewrite:

http-request set-path /%[req.hdr(host)]%[path] 

Then you'd need the proxy to send the the actual bucket endpoint hostname to S3, instead of the hostname supplied by the browser:

http-request set-header Host example-bucket.s3.amazonaws.com

The proxy would send the modified request to S3, return S3's response to CloudFront, which would return the response to the browser.

However, if you don't want to take this approach, since a server would be required, then the alternative solution looks like this:

  • Configure a CloudFront distribution for each subdomain, setting the alternate domain name for the distribution to match the specific subdomain.

  • Configure the Origin for each subdomain's distribution to point to the same bucket, setting the origin path to /one-specific-subdomain.example.com. CloudFront will change a request for GET /images/funny-cat.jpg HTTP/1.1 to GET /one-specific-subdomain.example.com/images/funny-cat.jpg HTTP/1.1 before sending the request to S3, resulting in the behavior you described. (This is the same net result as the behavior I described for HAProxy, but it is static, not dynamic, hence one distribution per subdomain; in neither case would this be a "redirect" -- so the address bar would not change).

  • Configure an A-record Alias in Route 53 for each subdomain, pointing to the subdomain's specific CloudFront distribution.

This can all be done programmatically through the APIs, using any one of the the SDKs, or using aws-cli, which is a very simple way to test, prototype, and script such things without writing much code. CloudFront and Route 53 are both fully automation-friendly.

Note that there is no significant disadvantage to each site using its own CloudFront distribution, because your hit ratio will be no different, and distributions do not have a separate charge -- only request and bandwidth charges.

Note also that CloudFront has a default limit of 200 distributions per AWS account but this is a soft limit that can be increased by sending a request to AWS support.


Since Lambda@edge this can be done with a lambda function triggered by the Cloud Front "Viewer Request" event.

Here is an example of such a Lambda function where a request like foo.example.com/index.html will return the file /foo/index.html from your origin.

You will need a CF distribution with the CNAME *.example.com, and an A record "*.example.com" pointing to it

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const subdomain = getSubdomain(request);
  if (subdomain) {
    request.uri = '/' + subdomain + request.uri;
  }
  callback(null, request);
};

function getSubdomain(request) {
  const hostItem = request.headers.host.find(item => item.key === 'Host');
  const reg = /(?:(.*?)\.)[^.]*\.[^.]*$/;
  const [_, subdomain] = hostItem.value.match(reg) || [];
  return subdomain;
}

As for the costs take a look at lambda pricing. At current pricing is 0.913$ per million requests