Setting up SSL-enabled S3 redirection with CloudFormation

I show how to create an S3 bucket for redirecting web requests, put it behind a CloudFront distribution, and configured this with an SSL certificate—all via CloudFormation.

This is the third post in an ongoing series in which I move my blog to HTTPS. In the previous post I learnt how to set up DNS in Route 53 using CloudFormation and Sceptre. In this post I will learn how to set up a redirection from the Apex domain (i.e. superloopy.io) to www.superloopy.io.

Table of Contents

Outputs

In addition to Parameters and Resources this stack will have an Outputs section. This will contain the domain name of the CloudFront distribution created. We need this because later we will amend our DNS template to set up an alias for that, rather than hard-code GitHub's IP addresses in the A record for superloopy.io. The Outputs section is at the top level of the YAML template, and looks like this:

Outputs:
  ApexCloudFrontDomain:
    Description: >-
      The CloudFront domain name for the Apex
      domain that we can use to add a CNAME to
      later.
    Value: !GetAtt CloudFront.DomainName

Certificate

Next we create a Certificate. It is fairly easy: the only required configuration is the domain name. However, we will also add a SubjectAlternativeName for the WWW sub-domain so we can use the same cert for both. This goes in the Resources section:

Resources:
  SSL:
    Type: 'AWS::CertificateManager::Certificate'
    Properties:
      DomainName: !Ref DomainName
      SubjectAlternativeNames:
        - !Join ['.', ['www', !Ref DomainName]]

S3 Redirection bucket

Next we create ApexBucket—the bucket used for redirection from the Apex domain to the WWW site proper. We'll use the AWS::S3::Bucket resource for that. BucketName is—surprisingly, at least to me—not required, but I would like to specify it so I can find it in the S3 console later.

I think we just need the RedirectAllRequestsTo property, with its HostName attribute. Furthermore the point of all this was to add SSL so let's force HTTPS as the protocol, despite the fact that my site doesn't support that yet. (It will before I make this configuration active.) Here's how this section looks:

Resources:
  [...]
  ApexBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref DomainName
      WebsiteConfiguration:
        RedirectAllRequestsTo:
          HostName: !Join ['.', ['www', !Ref DomainName]]
          Protocol: https

CloudFront Distribution

Finally we need to set up the CloudFront distribution, using its AWS::CloudFront::Distribution resource. It has only one property, the DistributionConfig, however, its attributes are numerous. Luckily all are not required. Let's start with Aliases, which is our CNAME. This is the name we want to assign later. It has to match the SSL certificate we'll assign later.

We also have to set Origins, line 10. Rather than building the DomainName for by !Join-ing strings I use the WebsiteURL attribute. It wasn't as successful as I had hoped, since I have to split it to separate the protocol from the domain name, as CloudFront wants only the latter in its Origins section. One step forward, two steps back?

Moving swiftly on, the CustomOriginConfig1, DefaultCacheBehaviour and ViewerCertificate sections I cribbed from a template my colleague Kristian Glass provided as a reference point. I don't have much to say about them, other than that they seem to do the job.

 1: Resources:
 2:   [...]
 3:   ApexCloudFront:
 4:     Type: 'AWS::CloudFront::Distribution'
 5:     Properties:
 6:       DistributionConfig:
 7:         Aliases:
 8:           - !Ref DomainName
 9:         Enabled: True
10:         Origins:
11:           - DomainName: !Select
12:               - 1
13:               - !Split ["//", !GetAtt ApexBucket.WebsiteURL]
14:             Id: origin
15:             CustomOriginConfig:
16:               OriginProtocolPolicy: http-only
17:         DefaultCacheBehavior:
18:           TargetOriginId: origin
19:           DefaultTTL: 5
20:           MaxTTL: 30
21:           ForwardedValues:
22:             QueryString: false
23:           ViewerProtocolPolicy: redirect-to-https
24:         ViewerCertificate:
25:           AcmCertificateArn: !Ref SSL
26:           SslSupportMethod: sni-only

CloudFront really likes the us-east-1 region

Attempting to create the above stack unfortunately fails with the following error:

ApexCloudFront AWS::CloudFront::Distribution CREATE_FAILED The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain.

I would prefer to have the S3 bucket here in the UK where I am because I believe it would make syncing my files to S3 faster. However, CloudFormation cannot take input from stack output in other regions and I don't want to have to manage half my setup in CloudFormation and half outside, so I'll relent and move my stack to us-east-1 to satisfy CloudFront for now2.

Testing that redirection works

Now that I'm using us-east-1 as the region creating the stack succeeds. But does it work? Since I haven't delegated DNS yet I use the S3 website URL directly to test the redirection on its own.

curl -v http://superloopy.io.s3-website-us-east-1.amazonaws.com/articles/2017/adding-ssl.html
[...]
< HTTP/1.1 301 Moved Permanently
[...]
< Location: https://www.superloopy.io/articles/2017/adding-ssl.html

So, yes! The S3 redirection setup looks like it's working! But… Does the CloudFront setup work? This is where the Outputs section comes in—I can ask Sceptre to display my stack's outputs:

sceptre describe-stack-outputs superloopy www
- Description: The CloudFront domain name that we can add a CNAME to later.
  OutputKey: Hostname
  OutputValue: d117yhymq9s8zd.cloudfront.net

Plugging that domain name into my query results in:

curl -v https://d117yhymq9s8zd.cloudfront.net/articles/2017/adding-ssl.html
[...]
< HTTP/1.1 301 Moved Permanently
[...]
< Location: https://www.superloopy.io/articles/2017/adding-ssl.html

Result!

The completed template

For the sake of completeness, here's my completed template:

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  DomainName:
    Type: String
    Default: example.net
Outputs:
  ApexCloudFrontDomain:
    Description: >-
      The CloudFront domain name for the Apex
      domain that we can use to add a CNAME to
      later.
    Value: !GetAtt ApexCloudFront.DomainName
Resources:
  SSL:
    Type: 'AWS::CertificateManager::Certificate'
    Properties:
      DomainName: !Ref DomainName
      SubjectAlternativeNames:
        - !Join ['.', ['www', !Ref DomainName]]
  ApexBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref DomainName
      WebsiteConfiguration:
        RedirectAllRequestsTo:
          HostName: !Join ['.', ['www', !Ref DomainName]]
          Protocol: https
  ApexCloudFront:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref DomainName
        Enabled: True
        Origins:
          - DomainName: !Select
              - 1
              - !Split ["//", !GetAtt ApexBucket.WebsiteURL]
            Id: origin
            CustomOriginConfig:
              OriginProtocolPolicy: http-only
        DefaultCacheBehavior:
          TargetOriginId: origin
          DefaultTTL: 5
          MaxTTL: 30
          ForwardedValues:
            QueryString: false
          ViewerProtocolPolicy: redirect-to-https
        ViewerCertificate:
          AcmCertificateArn: !Ref SSL
          SslSupportMethod: sni-only

Conclusion

I am pretty happy with this setup. No doubt it will be useful when moving all my sites to HTTPS over the next months.

1

For a while I felt that I should be using an S3OriginConfig instead, but I didn't feel this was very well documented and I couldn't manage to get that to work. A bit of reading implied that it requires a Origin Access Identity that cannot be created / added using CloudFormation so I decided to just stick with the CustomOriginConfig instead. It's not like my S3 bucket's content is secret. I also got the impression that going that route means you don't get to benefit from the "website hosting bucket" properties, which means no RedirectAllRequestsTo and no CustomErrorDocument and no IndexDocument properties.

2
Hopefully in the future CloudFront allows certs made anywhere, and then I can re-create my stack in a region closer to home.

Date: 21 July 2017

Author: Stig Brautaset

Validate