There are a lot of ways of hosting a static website. I chose to use AWS as a hosting platform for my website primarily for learning purposes, since I’ve been working on several AWS certifications. I created the resources for my static website using an AWS Serverless Application Model (SAM) template. AWS SAM templates are an extension to CloudFormation templates that provide a simpler syntax for defining serverless resources, such as Lambda functions. Every resource for this solution is included in the SAM template, with the exception of the Route 53 Hosted Zone. The SAM template and associated Lambda function for this solution can be found here.
The simplest way of hosting a static website in AWS is to create an S3 bucket, allow public access to it, and enable the static website hosting feature. One issue with this approach is that S3 static website hosting does not support SSL. Additionally, each request will have to fetch data from the S3 bucket directly. For large files (ex. images and videos), this can present issues.
The solution I present uses CloudFront with an S3 bucket as its origin. CloudFront is a CDN that caches data at AWS edge locations, which increases performance for frequently accessed files. Additionally, CloudFront supports SSL certificates, although we’ll have to create our own when using a custom domain name.
Let’s start by declaring some inputs to the SAM template:
Parameters:
DomainName:
Type: String
Description: Domain name
HostedZoneId:
Type: String
Description: Hosted zone ID
CFDistributionPriceClass:
Type: String
Default: PriceClass_All
AllowedValues:
- PriceClass_All
- PriceClass_100
- PriceClass_200
Description: Price class for CloudFront distribution
This declares three inputs: a domain name, a Route 53 Hosted Zone ID, and a CloudFront distribution price class. As I mentioned earlier, the Route 53 Hosted Zone is intentionally kept outside of the template. Next, we’ll need two S3 buckets: one for the website content, and another for the CloudFront distribution logs:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref DomainName
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
AccessControl: Private
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true
LogBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'logs.${DomainName}'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
AccessControl: Private
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true
These buckets are configured with some security features: server-side encryption is enabled, and public access is blocked. We’re able to block public access to the website bucket since it will sit behind a CloudFront distribution. Next, let’s create a certificate for the CloudFront distribution:
WebCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref DomainName
SubjectAlternativeNames:
- !Sub 'www.${DomainName}'
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: !Ref DomainName
HostedZoneId: !Ref HostedZoneId
- DomainName: !Sub 'www.${DomainName}'
HostedZoneId: !Ref HostedZoneId
For domains hosted in Route 53, this approach will work fine: CloudFormation will automatically perform domain validation for the certificate. For externally hosted domains, manual verification is required. In that case, it would probably be best to leave the certificate outside of the SAM template. We’re now ready to create the CloudFront distribution, along with a CloudFront origin access identity (OAI) for the distribution to access the S3 website bucket:
CFOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub '${DomainName} CloudFront OAI'
CFDistribution:
Type: AWS::CloudFront::Distribution
DependsOn:
- WebsiteBucket
- LogBucket
Properties:
DistributionConfig:
Enabled: 'true'
Comment: !Sub 'Distribution for ${DomainName}'
HttpVersion: http2
PriceClass: !Ref CFDistributionPriceClass
DefaultRootObject: index.html
Aliases:
- !Ref DomainName
- !Sub 'www.${DomainName}'
Origins:
- Id: !Sub 'S3-${DomainName}'
DomainName: !Sub '${DomainName}.s3.amazonaws.com'
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CFOriginAccessIdentity}'
DefaultCacheBehavior:
TargetOriginId: !Sub 'S3-${DomainName}'
Compress: true
ForwardedValues:
QueryString: false
Headers:
- Origin
Cookies:
Forward: none
ViewerProtocolPolicy: allow-all
CustomErrorResponses:
- ErrorCode: '403'
ResponsePagePath: /404.html
ResponseCode: '404'
ErrorCachingMinTTL: '30'
ViewerCertificate:
AcmCertificateArn: !Ref WebCertificate
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2019
Logging:
IncludeCookies: false
Bucket: !Sub 'logs.${DomainName}.s3.amazonaws.com'
Prefix: cf-logs
The CloudFront distribution references all of the other resources we’ve created so far. Since we’re not explicitly referencing the outputs of the WebsiteBucket
and LogBucket
resources, we need to add a DependsOn
statement to ensure that they’re created before the CloudFront distribution comes up. With the CloudFront distribution and OAI created, we’ll need an S3 bucket policy to allow the OAI to access the website bucket:
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebsiteBucket
PolicyDocument:
Statement:
- Effect: Allow
Action: s3:GetObject
Principal:
CanonicalUser: !GetAtt CFOriginAccessIdentity.S3CanonicalUserId
Resource: !Sub 'arn:aws:s3:::${WebsiteBucket}/*'
Finally, let’s create the DNS records for the website:
DNSRecordSet:
Type: AWS::Route53::RecordSetGroup
Properties:
Comment: !Sub 'DNS records for ${DomainName}'
HostedZoneId: !Ref HostedZoneId
RecordSets:
- Name: !Ref DomainName
Type: A
AliasTarget:
DNSName: !GetAtt CFDistribution.DomainName
EvaluateTargetHealth: false
HostedZoneId: Z2FDTNDATAQYW2
- Name: !Sub 'www.${DomainName}'
Type: A
AliasTarget:
DNSName: !GetAtt CFDistribution.DomainName
EvaluateTargetHealth: false
HostedZoneId: Z2FDTNDATAQYW2
We have two records here: one for the root domain, and one for the www subdomain. We can also create other records here, such as AAAA records for IPv6. The HostedZoneId
parameter for the AliasTarget
resource has to be set to Z2FDTNDATAQYW2
for CloudFront distributions (see Amazon’s documentation for this).
In part 2 of this series, we’ll deploy this template and write some code to implement URL redirection.