Use an AWS CloudFormation script to create and host an SPA on S3 with SSL and apex/subdomain redirection using CloudFront
2025-01-06
Previous article

Context
This post shows a way to:
- Create a single page app hosted on AWS S3
- Redirect all requests to the www subdomain
- Use CloudFront to handle SSL distributions
By way of a CloudFormation script so you can do it over and over and over again with confidennce that it works.
Inputs you need to supply
- The FQDN for Your domain (this post assumes you host it in an AWS Route 53 hosted zone)
- example: hellopaella.com
- The hosted zone ID for the domain (you can can get it from Route 53 ‘hosted zone details’)
CloudFormation script, unannotated.
See annotations following the code block.
AWSTemplateFormatVersion: '2010-09-09'
Description: Website using an S3 bucket with SSL redirect using CloudFront distribution for both Apex and subdomains
Parameters:
DomainName:
Type: String
DomainHostedZone:
Type: String
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Description: WWW Content Bucket
Properties:
BucketName: !Sub "www.${DomainName}"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
BucketEncryption:
ServerSideEncryptionConfiguration:
- BucketKeyEnabled: true
ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Tags:
- Key: !Ref DomainName
Value: WebsiteBucket
ApexBucket:
Type: AWS::S3::Bucket
Description: Apex bucket redirection
Properties:
BucketName: !Ref DomainName
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebsiteConfiguration:
RedirectAllRequestsTo:
HostName: !Sub "www.${DomainName}"
Protocol: https
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
BucketEncryption:
ServerSideEncryptionConfiguration:
- BucketKeyEnabled: true
ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Tags:
- Key: !Ref DomainName
Value: WebsiteBucket
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Description: WebsiteBucketPolicy Description
Properties:
Bucket: !Ref WebsiteBucket
PolicyDocument:
Id: SiteBucketPolicy
Version: 2012-10-17
Statement:
- Sid: PolicyForCloudFrontPrivateContent
Effect: Allow
Principal:
Service: "cloudfront.amazonaws.com"
Action:
- s3:GetObject
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref WebsiteBucket
- /*
SiteCertificate:
Type: AWS::CertificateManager::Certificate
Description: Certificate Description
Properties:
DomainName: !Ref DomainName
DomainValidationOptions:
- DomainName: !Ref DomainName
HostedZoneId: !Ref DomainHostedZone
ValidationMethod: DNS
SubjectAlternativeNames:
- !Sub "*.${DomainName}"
Tags:
- Key: !Ref DomainName
Value: SiteCertificate
ApexCFCachePolicy:
Type: AWS::CloudFront::CachePolicy
Description: ApexCFCachePolicy
Properties:
CachePolicyConfig:
Comment: "Apex Policy with caching enabled. Supports Gzip and Brotli compression."
MinTTL: 1
MaxTTL: 3.1536E7
ParametersInCacheKeyAndForwardedToOrigin:
QueryStringsConfig:
QueryStringBehavior: none
EnableAcceptEncodingBrotli: true
HeadersConfig:
HeaderBehavior: none
CookiesConfig:
CookieBehavior: none
EnableAcceptEncodingGzip: true
DefaultTTL: 86400
Name: !Sub "${DomainHostedZone}CachingOptimized"
ApexCFDistribution:
Type: AWS::CloudFront::Distribution
Description: CFDistribution for Apex
Properties:
DistributionConfig:
Logging:
IncludeCookies: false
Bucket: ""
Prefix: ""
Comment: !Ref DomainName
Origins:
- ConnectionTimeout: 10
OriginAccessControlId: ""
ConnectionAttempts: 3
OriginCustomHeaders: []
DomainName: !Sub "${DomainName}.s3-website-${AWS::Region}.amazonaws.com"
OriginShield:
Enabled: false
OriginPath: ""
Id: !Ref DomainName
CustomOriginConfig:
OriginKeepaliveTimeout: 5
OriginReadTimeout: 30
OriginSSLProtocols:
- "SSLv3"
- "TLSv1"
- "TLSv1.1"
- "TLSv1.2"
HTTPSPort: 443
HTTPPort: 80
OriginProtocolPolicy: "http-only"
Aliases:
- !Ref DomainName
DefaultRootObject: ""
DefaultCacheBehavior:
AllowedMethods:
- "HEAD"
- "DELETE"
- "POST"
- "GET"
- "OPTIONS"
- "PUT"
- "PATCH"
CachedMethods:
- "HEAD"
- "GET"
SmoothStreaming: false
CachePolicyId: !Ref ApexCFCachePolicy
Compress: true
FunctionAssociations: []
LambdaFunctionAssociations: []
TargetOriginId: !Ref DomainName
ViewerProtocolPolicy: "redirect-to-https"
ResponseHeadersPolicyId: "60669652-455b-4ae9-85a4-c4c02393f86c"
GrpcConfig:
Enabled: false
TrustedSigners: []
FieldLevelEncryptionId: ""
TrustedKeyGroups: []
ForwardedValues:
QueryString: false
Cookies:
Forward: none
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
PriceClass: PriceClass_100
ViewerCertificate:
MinimumProtocolVersion: "TLSv1.2_2021"
SslSupportMethod: "sni-only"
AcmCertificateArn: !Ref SiteCertificate
Staging: false
CustomErrorResponses: []
ContinuousDeploymentPolicyId: ""
OriginGroups:
Quantity: 0
Items: []
Enabled: true
IPV6Enabled: true
WebACLId: ""
HttpVersion: "http2"
Restrictions:
GeoRestriction:
Locations: []
RestrictionType: "none"
CacheBehaviors: []
Tags:
- Key: !Ref DomainName
Value: ApexCFDistribution
CFOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Description: CFOriginAccessControl
Properties:
OriginAccessControlConfig:
SigningBehavior: "always"
Description: !Sub "www.${DomainName}"
SigningProtocol: "sigv4"
OriginAccessControlOriginType: "s3"
Name: !Sub "www.${DomainName}"
WWWCFDistribution:
Type: AWS::CloudFront::Distribution
Description: WWWCFDistribution
Properties:
DistributionConfig:
Logging:
IncludeCookies: false
Bucket: ""
Prefix: ""
Comment: !Sub "www.${DomainName}"
DefaultRootObject: index.html
Origins:
- ConnectionTimeout: 10
OriginAccessControlId: !Ref CFOriginAccessControl
ConnectionAttempts: 3
DomainName: !GetAtt WebsiteBucket.RegionalDomainName
OriginShield:
Enabled: false
S3OriginConfig:
OriginAccessIdentity: ""
OriginPath: ""
Id: !Sub "www.${DomainName}"
ViewerCertificate:
MinimumProtocolVersion: "TLSv1.2_2021"
SslSupportMethod: "sni-only"
AcmCertificateArn: !Ref SiteCertificate
PriceClass: "PriceClass_100"
DefaultCacheBehavior:
Compress: true
FunctionAssociations: []
LambdaFunctionAssociations: []
TargetOriginId: !Sub "www.${DomainName}"
ViewerProtocolPolicy: "redirect-to-https"
ResponseHeadersPolicyId: "60669652-455b-4ae9-85a4-c4c02393f86c"
GrpcConfig:
Enabled: false
TrustedSigners: []
FieldLevelEncryptionId: ""
TrustedKeyGroups: []
AllowedMethods:
- "HEAD"
- "DELETE"
- "POST"
- "GET"
- "OPTIONS"
- "PUT"
- "PATCH"
CachedMethods:
- "HEAD"
- "GET"
SmoothStreaming: false
CachePolicyId: !Ref ApexCFCachePolicy
Staging: false
CustomErrorResponses: []
ContinuousDeploymentPolicyId: ""
OriginGroups:
Quantity: 0
Items: []
Enabled: true
Aliases:
- !Sub "www.${DomainName}"
IPV6Enabled: true
WebACLId: ""
HttpVersion: "http2"
Restrictions:
GeoRestriction:
Locations: []
RestrictionType: "none"
CacheBehaviors: []
Tags:
- Key: !Ref DomainName
Value: WWWCFDistribution
WWWDNSRecord:
Type: AWS::Route53::RecordSet
Description: WWWDNSRecord
Properties:
HostedZoneName: !Sub "${DomainName}."
Name: !Sub "www.${DomainName}"
Type: A
AliasTarget:
DNSName: !GetAtt WWWCFDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2
ApexDNSRecord:
Type: AWS::Route53::RecordSet
Description: ApexDNSRecord
Properties:
HostedZoneName: !Sub "${DomainName}."
Name: !Ref DomainName
Type: A
AliasTarget:
DNSName: !GetAtt WWWCFDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2
Outputs:
ApexCFDistributionURL:
Value: !GetAtt ApexCFDistribution.DomainName
Description: ApexCFDistribution URL
WWWCFDistributionURL:
Value: !GetAtt WWWCFDistribution.DomainName
Description: WWWCFDistribution URL
WWWDNSRecord:
Value: !Sub "https://www.${DomainName}"
Description: WWWDNSRecord
ApexDNSRecord:
Value: !Sub "https://${DomainName}"
Description: ApexDNSRecord
Annotations
ApexBucket
Notice how it redirects all requests to www, asking for SSL, supported by CloudFront
SiteCertificate
Provisions a cert to cover all domain names under your apex domain (example.com and *.example.com)
The trick: ApexCFDistribution
DomainName: !Sub "${DomainName}.s3-website-${AWS::Region}.amazonaws.com"
The domain name is not a regular S3 designation, but contains -website-, without which, all goes to hell.
In contrast: WWWCFDistribution
DomainName: !GetAtt WebsiteBucket.RegionalDomainName
is plain.
The rest of the script is standard.
What this will do
It will create:
- The subdomain bucket to which you would upload your content to be served as an SPA/Static site
- Apex S3 bucket that will redirect to the www subdomain
- A certificate to cover the apex and all subdomains
- Two CloudFront distributions to handle the redirects (http to https)
- Assorted needed peripherial resources.
What this will allow you to do on behalf of your site’s visitors
- They visit http://example.com and will be redirected to https://example.com and then to https://www.example.com
- They visit http://www.example.com and will be redirected https://www.example.com
at which point they’ll see your website’s content
Conclusion
I hope this helps! I looked around quite a bit and did not see a way to do this with CloudFormation; all the examples were using the AWS Console, but I hate clicking around… Let me know how to improve or optimise the script!
Happy hacking!
Previous article
Filed under
AWS
- Programming ESP32 using MQTT with AWS and FreeRTOS
- Quick AWS IoT Setup and test
- Set up AWS API GW with a Typescript authorizer and logging
- Use AWS CodePipline to execute CloudFormation templates
- Use GitHub Actions to deploy your SPA hosted on Amazon S3
- Writing an Alexa skill using Ruby and AWS Lambda (Part 0)
CloudFormation
CloudFront
Route 53
S3
SPA
Other Tags
API GW
AWS
- Programming ESP32 using MQTT with AWS and FreeRTOS
- Quick AWS IoT Setup and test
- Set up AWS API GW with a Typescript authorizer and logging
- Use AWS CodePipline to execute CloudFormation templates
- Use GitHub Actions to deploy your SPA hosted on Amazon S3
- Use an AWS CloudFormation script to create and host an SPA on S3 with SSL and apex/subdomain redirection using CloudFront
- Writing an Alexa skill using Ruby and AWS Lambda (Part 0)
ActiveRecord
Agile
- A review of software development metrics
- Agile programme management brief
- An alternative to current product development metrics
- An alternative to the current product development governance model
- Command & Control Management - The Party Killer
- Document Driven Development
- Inceptions revisited
- Managing multiple stakeholders
- Returns Driven Development
- The tip of the (good) iceberg
Alexa
Analysis
Ansible
BDD
BLE
C
CAB
CloudFormation
- Set up AWS API GW with a Typescript authorizer and logging
- Use AWS CodePipline to execute CloudFormation templates
- Use GitHub Actions to deploy your SPA hosted on Amazon S3
- Use an AWS CloudFormation script to create and host an SPA on S3 with SSL and apex/subdomain redirection using CloudFront
- Writing an Alexa skill using Ruby and AWS Lambda (Part 0)
CloudFront
CloudWatch
Cross-compile
Cucumber
DevOps
Devops
DotNet
Embedded
Fitbit
GNU
GitHub Actions
Governance
How-to
Inception
IoT
Javascript
Jest
Lambda
Mac OS X
- Bluetooth Low Energy (BLE) Implementing a peripheral on Mac OS X
- Cross-compiling for Raspberry Pi on a Mac and debugging using NetBeans
- Drobo will not mount in Finder
- Quickie - ssh dynamic port forwarding to avoid unsecured public networks
- Remote compilation, execution and debugging Raspberry Pi from a Mac using NetBeans
- Weekend warrior - MacRuby and rSpec, Mac OS X Lion, Xcode V4.3.2
MacRuby
Metrics
MySQL
NetBeans
Objective-C
PMO
Product Management
- A path to accelerating value realization
- A review of software development metrics
- Agile programme management brief
- An alternative to current product development metrics
- An alternative to the current product development governance model
- Express initiative kickoff formula
- Inceptions revisited
- Managing multiple stakeholders
- Plan for value delivery
- Pre-prod activity - Futurespective
- Value Stream Mapping
- When planning, it's not only about relative complexity
Programme management
Project Management
- A path to accelerating value realization
- A review of software development metrics
- Agile programme management brief
- An alternative to current product development metrics
- An alternative to the current product development governance model
- Command & Control Management - The Party Killer
- Express initiative kickoff formula
- Inceptions revisited
- Managing multiple stakeholders
- Plan for value delivery
- Pre-prod activity - Futurespective
- Value Stream Mapping
- When planning, it's not only about relative complexity
Quality Assurance
Rails
Raspberry Pi
Remote compilation
Remote debugging
Remote execution
Risk Assessment
Route 53
Ruby
- Alexa on Rails - how to develop and test Alexa skills using Rails
- Arduino programming using Ruby, Cucumber & rSpec
- How to reconnect to a database when its connection was lost
- Oh, the places you'll go...
- Quick AWS IoT Setup and test
- Weekend warrior - MacRuby and rSpec, Mac OS X Lion, Xcode V4.3.2
- Writing an Alexa skill using Ruby and AWS Lambda (Part 0)