Yesterday, I was asked about the setup behind hello-world.sh and as I caught myself drifting off into details I thought to myself: Why not explain it in depth right here?
So this is what this post is about: Setting up a highly-scalable site, obtaining your own cool domain name, have all content cached at the edge for millisecond latency, have your own Email domain and mailboxes, encrypt all content in transit with validated TLS certificates and don’t spend tons of money, because it’s all serverless.
First of all we need a domain. Route 53 comes with super neat functionality to register domains. To be honest, I actually spent quite a lot of time trying loads and loads of possible domain names. You can come up with quite a lot of unique ideas given the recent additions in TLDs. I was barely able to believe that something like hello-world.sh wasn’t taken yet. I was able to directly use my newly purchased domain as a hosted zone in Route 53 in a matter of minutes.
It is probably a wise idea to spend quite a lot of time choosing your domain name. In the end you will spend the most money on domain registration. The rest of the architecture is comparatively inexpensive. Bringing an existing domain along and importing it into Route 53 is also possible.
Okay, let’s look at what I wanted for hello-world.sh and how I actually built it. A quick glance at my requirements for the site reveals that there is no black magic involved. It’s fairly simple and straight forward in its functional requirements. However, non-functionally speaking, it absolutely needs to be state-of-the-art.
I wanted hello-world.sh to:
- be globally retrievable with sub-second latency through a CDN
- have its traffic completely encrypted with 256-bit AES over TLS
- have a proper trust validation with recognised root CAs
- be as highly available as possible
- be completely automated and
- cost very little money
Now, this might seem like a pretty tall order, but thankfully, AWS has made this incredibly easy.
Okay, lets weld some building blocks together and visualize them:
Storage for hosting the content of the site is handled by an S3 bucket with static website hosting functionality enabled (revolutionary… who would have thought?!). CloudFront is used to do edge-caching of the content. Now, AWS has builtin functionality to limit access to the underlying bucket to the CloudFront distribution. By using this functionality, we don’t need to make our bucket publicly accessible on the open internet. The only way of accessing its contents is through CloudFront. To make this work, we also need to provision a so-called Origin Access Identity (OAI) during deployment. OAIs come with a unique, canonical user identity to access S3. IAM and (more importantly) S3 Bucket Policies are aware of this identity and they can evaluate permissions just as with any other prinicipal. This means we can limit access to our bucket to just a very specific requester: CloudFront.
We are also setting up a DNS record in Route 53 to point the apex of our domain at the unique FQDN of the CloudFront distribution. To do this, we are making use of the AWS-internal ALIAS functionality to resolve our CloudFront IPs. Interestingly, we need to specify the HostedZoneId of the CloudFront service, which is ‘Z2FDTNDATAQYW2’. This is the HostedZoneId for the hosted zone ‘cloudfront.net.’, which holds all the record sets to the various CloudFront distribution in AWS.
It is important to understand that the static web-site content needs to be tuned to properly do redirecting to content. It is a good idea to try and always use relative URLs. Another important limitation is that CloudFront can only default to an index.html in the root of the distribution. Don’t use links to sub-paths of the content directory. Use links to the full object path instead. Most static web-site generators, like Hugo, can be configured to do so automatically.
Here is the CloudFormation template to deploy all the infrastructure I need for hello-world.sh:
Description: > This template sets up a global page. Parameters: DomainName: Type: String Description: Name of the domain to point to the page content HostedZoneName: Type: String Description: Name of the target hosted zone Certificate: Type: String Description: ARN of the ACM sertificate to use Resources: Bucket: Type: AWS::S3::Bucket Properties: Tags: - Key: Url Value: !Join [ "", [ "https://", !Ref DomainName ] ] WebsiteConfiguration: IndexDocument: index.html BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref Bucket PolicyDocument: Statement: - Action: - s3:GetObject Resource: !Join [ "", [ "arn:aws:s3:::", !Ref Bucket, "/*"] ] Effect: Allow Principal: CanonicalUser: !GetAtt OAI.S3CanonicalUserId OAI: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: OAI for CloudFront to access page bucket CDN: Type: AWS::CloudFront::Distribution Properties: Tags: - Key: Url Value: !Join [ "", [ "https://", !Ref DomainName ] ] DistributionConfig: Origins: - DomainName: !GetAtt Bucket.DomainName Id: landingpages-bucket S3OriginConfig: OriginAccessIdentity: !Join [ "/", [ "origin-access-identity", "cloudfront", !Ref OAI ] ] Enabled: true Comment: CDN DefaultRootObject: index.html Aliases: - !Ref DomainName DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS TargetOriginId: landingpages-bucket ForwardedValues: QueryString: true Cookies: Forward: none ViewerProtocolPolicy: redirect-to-https ViewerCertificate: AcmCertificateArn: !Ref Certificate SslSupportMethod: sni-only DNS: Type: AWS::Route53::RecordSet Properties: Type: A Name: !Ref DomainName Comment: Alias for Page HostedZoneName: !Ref HostedZoneName AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 # Constant HostedZoneId of CloudFront DNSName: !GetAtt CDN.DomainName Outputs: Bucket: Value: !Ref Bucket
It is not that much! We are just feeding in the name of our domain and a previously created certificate in us-east-1. It has to be in us-east-1 to be used as a global custom certificate for CloudFront.
Oh yeah, let’s also do mail
In order to have mail infrastructure for @hello-world.sh, I quickly setup AWS WorkMail to do the job. WorkMail lets you create a highly-scalable Personal Information Management (PIM) suite with mail, calendar and contacts functionality. It even comes with it’s own web apps to easily access your accounts. After setting up WorkMail I chose to configure the service to also be able to use my newly-purchased domain. This is done by simply setting up some record sets in my hosted zone in Route 53.
That’s it! The process of setting up mail was less than 15 minutes!
Code (for you to reuse)
Initial requirements don’t seem like that much of a tall order anymore. AWS has made it incredibly easy and accessible for everyone to setup a secure, state-of-the-art static web-site, that is ready to scale to millions of viewers.
The complete code to this page (content & infrastructure) is here.
And yes: it has a hello-world.sh file in it.