Using AWS to Host a Static Website - Part 3
- Jamie Tyler
- Aws
- December 23, 2024
Table of Contents
In part 2 I discussed the sequence that is to be used to build out a static website on AWS and covered how to register a domain using Amazon Route 53. In this post, I am going to cover using CloudFormation to create an Amazon S3 bucket for the static website content.
Before We Start
There is an assumption that the awscli has been installed and credentials configured. Personally I do not like having aws credentials stored locally in the AWS credentials and config files. I prefer to use a password manager to store the access_key_id and the secret_access_key if single sign on (SSO) is not configured. If you would like to know how to do this then refer to How to use a Password Manager to Store AWS Credentials post.
For illustration purposes I am not creating an IaC pipeline that would automatically deploy changes when I commit to the repo. I recommend you do so that both the IaC and content for a static website is automatically updated when changes are needed to be made.
I have made the CloudFormation template for this post available in public repository on GitHub although I would recommend trying to create it yourself to get used to doing it.
The Sequence
This template does the following:
- Creates an S3 bucket with the name you specify with a random string appended to ensure it is globally unique
- Ensures that deletion and update stack operations to do not affect data persistence
- Configure access logging on S3 bucket and encrypt both S3 buckets
- Enables static website hosting on the bucket
- Sets the index document to index.html and the error document to error.html
- Enables versioning on the bucket for better content management
- Tags the S3 bucket resource (best practice)
- Attaches a bucket policy that allows public read access to the objects in the bucket
- Outputs the website URL and the secure S3 bucket URL
The CloudFormation Template
Note
Using DeletionPolicy and UpdateReplacePolicy will mean that the Amazon S3 bucket will remain even if the CloudFormation stack is deleted. Therefore, the bucket will need to be manually deleted.
This CloudFormation template is available here.
AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFormation template to create an S3 bucket for static website hosting"
Resources:
LoggingBucket:
Metadata:
cfn_nag:
rules_to_suppress:
- id: W35
reason: "access logs bucket has no other use case"
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub
- ${BaseName}-logs-${RandomString}
- BaseName: !Ref "BucketName"
RandomString:
!Select [
2,
!Split ["-", !Select [2, !Split ["/", !Ref "AWS::StackId"]]],
]
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketKeyEnabled: true
Tags:
- Key: workload
Value: !Ref "TagValue"
LoggingBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref "LoggingBucket"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action: "s3:PutObject"
Condition:
ArnLike:
"aws:SourceArn":
- !GetAtt
- "StaticWebsiteBucket"
- "Arn"
Effect: "Allow"
Principal:
Service: "logging.s3.amazonaws.com"
Resource:
- !Sub "arn:${AWS::Partition}:s3:::${LoggingBucket}/*"
StaticWebsiteBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub
- ${BaseName}-${RandomString}
- BaseName: !Ref "BucketName"
RandomString:
!Select [
2,
!Split ["-", !Select [2, !Split ["/", !Ref "AWS::StackId"]]],
]
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: false # Set to false to allow public bucket policies
IgnorePublicAcls: true
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketKeyEnabled: true
LoggingConfiguration:
DestinationBucketName: !Ref "LoggingBucket"
LogFilePrefix: "access-logs/"
Tags:
- Key: workload
Value: !Ref "TagValue"
BucketPolicy:
Type: AWS::S3::BucketPolicy
Metadata:
cfn_nag:
rules_to_suppress:
- id: F16
reason: "will be replaced as CloudFront OAI will be used"
Properties:
PolicyDocument:
Id: MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: "*"
Action: "s3:GetObject"
Resource: !Sub "arn:${AWS::Partition}:s3:::${StaticWebsiteBucket}/*"
Bucket: !Ref "StaticWebsiteBucket"
Parameters:
BucketName:
Type: String
Description: "Name for the S3 bucket (must be globally unique)"
TagValue:
Type: String
Description: "Value to tag the bucket with"
Outputs:
WebsiteURL:
Value: !GetAtt
- StaticWebsiteBucket
- WebsiteURL
Description: URL for website hosted on S3
S3BucketSecureURL:
Value: !Sub "https://${StaticWebsiteBucket}.s3-website.${AWS::Region}.amazonaws.com"
Description: Name of S3 bucket to hold website content
Some points to note.
- Encryption uses AWS managed keys as there is no sensitive information. If there was I would used Customer Managed Keys via AWS KMS
- I use cfn_nag for CloudFormation linting and have suppressed several warnings and 1 failure. The failure will be replaced in subsequent post as Amazon CloudFront will be used
- Index.html and error.html are missing from the S3 bucket so it will not render at the moment. This will be remedied in a later post
Deploying the CloudFormation Template
For ease I am using the AWS CLI. As mentioned earlier, a pipeline would be a better mechanism but for ease I am keeping it simple. This assumes the AWS CLI is configured to authenticate in whatever the preferred mechanism is.
aws cloudformation create-stack --stack-name MY-STATIC-WEBSITE-STACKNAME --template-body file://s3-static-website.yaml \
--parameters ParameterKey=BucketName,ParameterValue=MY-STATIC-WEBSITE \
ParamterKey=TagValue,ParameterValue=MY-STATIC-WEBSITE --region us-east-1
For MY-STATIC-WEBSITE-STACKNAME I used nostrom0cloud and for MY-STATIC-WEBSITE I used nostrom0.cloud as the domain I registered in a previous post was nostrom0.cloud.
Note
If Amazon CloudFront is not going to be used the bucket name must match the DNS entry. This CloudFormation template does not conform to this.
Monitoring the Deployment
this can be done natively within the AWS CLI by using the wait command. The wait command will automatically poll and wait until the specified condition is met or until it times out (typically around 2 hours). This is the simplest way to monitor stack status without writing a script or using watch.
aws cloudformation wait stack-create-complete --stack-name MY-STATIC-WEBSITE-STACKNAME && \
aws cloudformation describe-stacks --stack-name MY-STATIC-WEBSITE-STACKNAME --query 'Stacks[0].StackStatus' --output text
The conditions that wait will monitor.
- stack-create-complete
- stack-delete-complete
- stack-exists
- stack-import-complete
- stack-rollback-complete
- stack-update-complete
- change-set-create-complete
- type-registration-complete
Retrieving the URL
After the stack creation is complete, you can retrieve the website URL from the stack outputs.
aws cloudformation describe-stacks --stack-name MY-STATIC-WEBSITE-STACKNAME \
--query 'Stacks[0].Outputs[?OutputKey==`WebsiteURL`].OutputValue' --output text --region us-east-1
Conclusion
With one, relatively, straight forward CloudFormation template there is a repeatable way of setting up an Amazon S3 bucket for static website hosting. In the next post I will walk through setting up a public zone in Amazon Route 53.