I’ve been setting up a separate website recently that needs only simple static content, and I thought I’d have an experiment with getting it HTTPS-enabled on AWS.
I had a couple of requirements:
http://...
address should respond with a 301 Moved Permanently
to the corresponding https://...
version.example.com
), and from the www
subdomain (www.example.com
). To prevent oddities in search results, I want any requests to the base domain to redirect to the www
subdomain.Basically, I want this behaviour:
Request | Response |
---|---|
http://example.com |
301 https://www.example.com |
http://www.example.com |
301 https://www.example.com |
https://example.com |
301 https://www.example.com |
https://www.example.com |
< the static site content > |
I don’t much mind if there is more than one redirect to the final site.
Finally, I want this to be managed via CloudFormation.
I’m going to make use of the following AWS functionality:
http
to https
.It took me a little while to realise that from AWS’s perspective, what I actually need to create are two distinct websites:
www.example.com
, will serve the static content.example.com
, will simply serve a redirect.Because of this, much of the CloudFormation script below is effectively duplicated.
I’m going to present the CloudFormation in this blog post as “literate code”, but if you prefer to see it all at once, I’ll link to a gist at the end of this article.
The overall structure looks like this:
AWSTemplateFormatVersion: 2010-09-09
Parameters:
RootDomainName:
Type: String
Mappings:
RegionMap:
<region-map>
Resources:
RootCertificate:
<root-certificate>
SubdomainCertificate:
<subdomain-certificate>
PublicWebsiteRootBucket:
<root-bucket>
PublicWebsiteWwwBucket:
<www-bucket>
PublicRootBucketPolicy:
<root-bucket-policy>
PublicWwwBucketPolicy:
<www-bucket-policy>
PublicWebsiteRootCloudfront:
<root-cloudfront>
PublicWebsiteWwwCloudfront:
<www-cloudfront>
HostedZone:
<hosted-zone>
DNS:
<dns-setup>
This script takes a single input parameter, RootDomainName
, which is the root domain name, for example example.com
.
First up is the <region-map>
. This is just a lookup table of AWS’s own endpoints and hosted zones for S3. I’m probably missing a few regions so if you’re hosting from a newer region you’ll need to update this table. (AWS documentation reference).
us-east-1:
S3HostedZoneID: Z3AQBSTGFYJSTF
S3WebsiteEndpoint: s3-website-us-east-1.amazonaws.com
us-west-1:
S3HostedZoneID: Z2F56UZL2M1ACD
S3WebsiteEndpoint: s3-website-us-west-1.amazonaws.com
us-west-2:
S3HostedZoneID: Z3BJ6K6RIION7M
S3WebsiteEndpoint: s3-website-us-west-2.amazonaws.com
eu-west-1:
S3HostedZoneID: Z1BKCTXD74EZPE
S3WebsiteEndpoint: s3-website-eu-west-1.amazonaws.com
ap-southeast-1:
S3HostedZoneID: Z3O0J2DXBE1FTB
S3WebsiteEndpoint: s3-website-ap-southeast-1.amazonaws.com
ap-southeast-2:
S3HostedZoneID: Z1WCIGYICN2BYD
S3WebsiteEndpoint: s3-website-ap-southeast-2.amazonaws.com
ap-northeast-1:
S3HostedZoneID: Z2M4EHUR26P7ZW
S3WebsiteEndpoint: s3-website-ap-northeast-1.amazonaws.com
sa-east-1:
S3HostedZoneID: Z31GFT0UA1I2HV
S3WebsiteEndpoint: s3-website-sa-east-1.amazonaws.com
The <root-certificate>
and <subdomain-certificate>
parts define ACM certificates that match the root domain (example.com
) and any sub-domains (*.example.com
). I need two certificates here because *.example.com
does not match example.com
. (AWS documentation reference).
RootCertificate:
Type: 'AWS::CertificateManager::Certificate'
Properties:
DomainName: !Ref RootDomainName
SubdomainCertificate:
Type: 'AWS::CertificateManager::Certificate'
Properties:
DomainName: !Sub
- '*.${Domain}'
- Domain: !Ref RootDomainName
IMPORTANT: Before AWS will issue a certificate for your domain, it needs to verify that you own the domain and are happy for the certificate to be issued. (Otherwise, you could ask AWS top issue certificates for any old domain, which would lead to their losing their status as a trusted certificate authority).
There are two ways AWS can verify you own a domain:
@
your domain: referenceNow the CNAME method is the preferred way, but I couldn’t see a way of getting all of that connected up in CloudFormation – the docs suggest that it isn’t directly possible.
Email sounds at first like it would be a lot more hassle – I haven’t yet set up email for my new domain – but as luck would have it, my domain registrar (NameCheap) enables email forwarding, so the AWS verification emails simply popped up in GMail.
What happens if you don’t verify you own the domain? The stack creation or update simply sits in the *_IN_PROGRESS
state indefinitely.
Onwards to buckets…
The bucket that will serve requests to the root (i.e. simply returning redirects) looks like the following:
PublicWebsiteRootBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Ref RootDomainName
AccessControl: PublicRead
WebsiteConfiguration:
RedirectAllRequestsTo:
HostName: !Ref PublicWebsiteWwwBucket
It just points to the www
bucket, which looks like this:
PublicWebsiteWwwBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Sub
- www.${Domain}
- Domain: !Ref RootDomainName
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: 404.html
You’ll need some other process to populate this bucket with the static files you want to serve of course – I don’t believe CloudFormation can handle that. And if you want your index and 404 files named differently, that’s up to you.
Next come the bucket policies, allowing public read on any new items (i.e. your content) that is added:
PublicRootBucketPolicy:
Type: 'AWS::S3::BucketPolicy'
Properties:
PolicyDocument:
Id: PublicWebsitePolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref PublicWebsiteRootBucket
- /*
Bucket: !Ref PublicWebsiteRootBucket
PublicWwwBucketPolicy:
Type: 'AWS::S3::BucketPolicy'
Properties:
PolicyDocument:
Id: PublicWebsitePolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref PublicWebsiteWwwBucket
- /*
Bucket: !Ref PublicWebsiteWwwBucket
Next up are the definitions for the two CloudFront distributions. As far as I can tell, a CloudFront distribution serves one and only one set of content. If you want two sets of content (as we do here), then you need two distributions.
PublicWebsiteRootCloudfront:
Type: AWS::CloudFront::Distribution
DependsOn:
- PublicWebsiteRootBucket
Properties:
DistributionConfig:
Comment: CloudFront to S3 - root
Origins:
- DomainName: !Join
- '.'
- - !Ref 'RootDomainName'
- !FindInMap [RegionMap, !Ref 'AWS::Region', S3WebsiteEndpoint]
Id: S3RootOrigin
CustomOriginConfig:
HTTPPort: '80'
HTTPSPort: '443'
OriginProtocolPolicy: http-only
Enabled: true
HttpVersion: 'http2'
DefaultRootObject: index.html
Aliases:
- !Ref 'RootDomainName'
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
TargetOriginId: S3RootOrigin
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_All
ViewerCertificate:
AcmCertificateArn: !Ref RootCertificate
SslSupportMethod: sni-only
PublicWebsiteWwwCloudfront:
Type: AWS::CloudFront::Distribution
DependsOn:
- PublicWebsiteWwwBucket
Properties:
DistributionConfig:
Comment: CloudFront to S3 - www
Origins:
- DomainName: !Join
- '.'
- - 'www'
- !Ref 'RootDomainName'
- !FindInMap [RegionMap, !Ref 'AWS::Region', S3WebsiteEndpoint]
Id: S3WwwOrigin
CustomOriginConfig:
HTTPPort: '80'
HTTPSPort: '443'
OriginProtocolPolicy: http-only
Enabled: true
HttpVersion: 'http2'
DefaultRootObject: index.html
Aliases:
- !Join
- '.'
- - 'www'
- !Ref 'RootDomainName'
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
DefaultTTL: 3600
TargetOriginId: S3WwwOrigin
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_All
ViewerCertificate:
AcmCertificateArn: !Ref SubdomainCertificate
SslSupportMethod: sni-only
I’ve lowered DefaultTTL
for the www
distribution just for easier debugging. You might well want to change that value, or omit it and leave it at the default.
Finally, we come to DNS. We need a hosted zone (to hold the domain records), plus two actual domain records: one for the root, and one for www
:
HostedZone:
Type: 'AWS::Route53::HostedZone'
Properties:
Name: !Ref RootDomainName
DNS:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Sub
- ${Domain}.
- Domain: !Ref RootDomainName
RecordSets:
- Name: !Ref 'RootDomainName'
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt [PublicWebsiteRootCloudfront, DomainName]
- Name: !Join
- '.'
- - 'www'
- !Ref 'RootDomainName'
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt [PublicWebsiteWwwCloudfront, DomainName]
(That hosted zone ID is the one for CloudFront: reference )
These two records are AWS-specific, and I think they are extremely confusing. Why? The two mostly-used kinds of DNS records are:
A
records, that point to IP addresses, and;CNAME
records, that point to other names.The two records above are ALIAS
records – an AWS-specific bit of functionality. I think it would have been far clearer if in CloudFormation you did something like Type: ALIAS
, but sadly AWS decided that instead, you would define them as Type: A
, but indicate they are alias records using AliasTarget
. To my eyes it looks odd having an A
record that points to a name, but there we have it.
If you’d like the code all in one place, you can find it here: GitHub Gist
So how much does this sort of static hosting cost?
The “big” item is hosted zones: these cost $0.50 per month each: Pricing docs.
(Note: this is a fixed cost and is not applied pro-rata. So for example, I got myself into a bit of a mess, decided to trash everything and re-create, and hence I have been billed for two hosted zones this month, even though I have only ever had one active).
If you plan on hosting multiple sites, this is likely to be your biggest cost.
So far, my other costs have been minimal. You’ll need to pay for the following:
Published: Thursday, December 28, 2017
Hackification.io is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com. I may earn a small commission for my endorsement, recommendation, testimonial, and/or link to any products or services from this website.