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.
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.
Hopefully in the future CloudFront allows certs made anywhere, and then I can re-create my stack in a region closer to home.