A modern application pattern is to deploy static web content, say a React or Vue.js single page application, that interacts with an app API. For this example we want to deploy a static web site -- https://www.gofaas.net
-- that talks to our gofaas API running at https://api.gofaas.net
to authenticate a user and display a dashboard.
This pattern offers many advantages. Now the API is only concerned with data, making it easier to write and more cost effective to run. The web content is completely static, making it extremely reliable and cost effective to deliver to our users.
Compare this to a traditional Model View Controller (MVC) approach like Rails or Django. In this architecture the API may spend lots of time rendering HTML, and the HTML may not get served to users if there is an application bug or a database outage.
Static websites are a solved problem on AWS. We simply create an S3 bucket configured for website hosting and upload the content with public-read permissions. Then anyone can access the content from a URL like http://gofaas-webbucket-572007530218.s3-website-us-east-1.amazonaws.com
with some of the highest reliability and lowest storage and bandwidth costs possible.
Serving this from a custom domain is also a solved problem. We add the CloudFront CDN, configured with an SSL cert via the AWS Certificate Manager, in front of the S3 bucket. When we point our custom domain DNS to CloudFront, users can access the content from a URL like https://www.gofaas.net
with some of the fastest delivery times and lowest bandwidth costs possible thanks to the global content caching network.
Let's set this all up for our app...
There are a lot of configuration options for S3 and CloudFront so the template isn't short. But rest assured this template will keep your website running forever.
Note that we add a WebsiteConfiguration
for the S3 bucket, and conditionally create an ACM cert and CloudFront distribution if we specify the WebDomainName
parameter.
---
AWSTemplateFormatVersion: '2010-09-09'
Conditions:
WebDomainNameSpecified: !Not [!Equals [!Ref WebDomainName, ""]]
Mappings:
RegionMap:
ap-northeast-1:
S3HostedZoneId: Z2M4EHUR26P7ZW
S3WebsiteEndpoint: s3-website-ap-northeast-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
eu-west-1:
S3HostedZoneId: Z1BKCTXD74EZPE
S3WebsiteEndpoint: s3-website-eu-west-1.amazonaws.com
sa-east-1:
S3HostedZoneId: Z31GFT0UA1I2HV
S3WebsiteEndpoint: s3-website-sa-east-1.amazonaws.com
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
Outputs:
WebDistributionDomainName:
Condition: WebDomainNameSpecified
Value: !GetAtt WebDistribution.DomainName
WebUrl:
Value:
!If
- WebDomainNameSpecified
- !Sub https://${WebDomainName}
- !Sub
- http://${WebBucket}.${Endpoint}
- {Endpoint: !FindInMap [RegionMap, !Ref "AWS::Region", S3WebsiteEndpoint]}
Parameters:
WebDomainName:
Default: ""
Description: "Domain or subdomain for the static website distribution, e.g. www.gofaas.net"
Type: String
Resources:
WebBucket:
DeletionPolicy: Retain
Properties:
AccessControl: PublicRead
BucketName: !If [WebDomainNameSpecified, !Ref WebDomainName, !Sub "${AWS::StackName}-webbucket-${AWS::AccountId}"]
WebsiteConfiguration:
ErrorDocument: 404.html
IndexDocument: index.html
Type: AWS::S3::Bucket
WebBucketPolicy:
Properties:
Bucket: !Ref WebBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: "*"
Resource: !Sub arn:aws:s3:::${WebBucket}/*
Sid: PublicReadForGetBucketObjects
Type: AWS::S3::BucketPolicy
WebCertificate:
Condition: WebDomainNameSpecified
Properties:
DomainName: !Ref WebDomainName
Type: AWS::CertificateManager::Certificate
WebDistribution:
Condition: WebDomainNameSpecified
Properties:
DistributionConfig:
Aliases:
- !Ref WebDomainName
Comment: !Sub Distribution for ${WebBucket}
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
Compress: true
ForwardedValues:
Cookies:
Forward: none
QueryString: true
TargetOriginId: !Ref WebBucket
ViewerProtocolPolicy: redirect-to-https
DefaultRootObject: index.html
Enabled: true
HttpVersion: http2
Origins:
- CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: http-only
DomainName: !Sub
- ${WebBucket}.${Endpoint}
- {Endpoint: !FindInMap [RegionMap, !Ref "AWS::Region", S3WebsiteEndpoint]}
Id: !Ref WebBucket
PriceClass: PriceClass_All
ViewerCertificate:
AcmCertificateArn: !Ref WebCertificate
SslSupportMethod: sni-only
Type: AWS::CloudFront::Distribution
From template.yml
Now we can deploy the config to create the website bucket:
$ aws cloudformation package \
--output-template-file out.yml --template-file template.yml
$ aws cloudformation deploy --stack-name gofaas \
--capabilities CAPABILITY_NAMED_IAM --template-file out.yml
Waiting for stack create/update to complete
$ aws cloudformation describe-stacks --stack-name gofaas \
--output text --query 'Stacks[*].Outputs'
WebUrl http://gofaas-webbucket-572007530218.s3-website-us-east-1.amazonaws.com
From Makefile
And upload our first content:
$ aws s3 sync public s3://gofaas-webbucket-572007530218/
upload: public/index.html to s3://gofaas-webbucket-572007530218/index.html
...
From Makefile
Sure enough we can access it over HTTP:
$ curl http://gofaas-webbucket-572007530218.s3-website-us-east-1.amazonaws.com/
...
<title>My first gofaas/Vue app</title>
Now we can re-deploy the config with our domain name to create the certificate and CDN:
aws cloudformation deploy --stack-name gofaas \
--parameter-overrides WebDomainName=www.gofaas.net \
--capabilities CAPABILITY_NAMED_IAM --template-file out.yml
$ aws cloudformation describe-stacks --stack-name gofaas \
--output text --query 'Stacks[*].Outputs'
WebDistributionDomainName d2bwnae7bzw1t6.cloudfront.net
WebUrl https://www.gofaas.net
Note that this can take 10 to 20 minutes to set up the global infrastructure for our static site. Also note that ACM will send an email to the domain owner (e.g. [email protected]) who must click through the approval to create the certificate. See the ACM email validation guide for more information.
Once the CDN is in place, we can sync content to the S3 bucket the same way, but we may need to invalidate content cached in the CDN to immediately see the latest content:
$ aws s3 sync public s3://www.gofaas.net/
$ aws cloudfront create-invalidation --distribution-id E2YL0GMGANCGMA --paths '/*'
Sure enough we can access our content via the CDN:
$ curl https://d2bwnae7bzw1t6.cloudfront.net/
...
<title>My first gofaas/Vue app</title>
The final step is to set up a DNS CNAME from our WebDomainName
parameter (e.g. www.gofaas.net
) to the new WebDistributionDomainName
output (e.g. d2bwnae7bzw1t6.cloudfront.net
).
If we are using Route53, this is easy to do through the UI:
In this case we could consider automating DNS setup by adding an conditional AWS::Route53::RecordSet
resource to our template. We could also consider using ACM DNS validation to fully automate the certificate.
After a few minutes we have our custom HTTPS endpoint:
$ curl https://www.gofaas.net
...
<p>Hello world! This is HTML5 Boilerplate.</p>
When hosting a static site an app with S3, CloudFront and ACM we can:
- Store our static web content for low cost
- Access cached web content quickly via a custom domain
- Automate cert creation and renewal
We no longer have to worry about:
- Configuring HTTP servers
- Generating HTML content in our API
Our app is easier to build and more reliable and cost effective to run.