Using AWS to Host a Static Website - Part 3

Using AWS to Host a Static Website - Part 3

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:

  1. Creates an S3 bucket with the name you specify with a random string appended to ensure it is globally unique
  2. Ensures that deletion and update stack operations to do not affect data persistence
  3. Configure access logging on S3 bucket and encrypt both S3 buckets
  4. Enables static website hosting on the bucket
  5. Sets the index document to index.html and the error document to error.html
  6. Enables versioning on the bucket for better content management
  7. Tags the S3 bucket resource (best practice)
  8. Attaches a bucket policy that allows public read access to the objects in the bucket
  9. 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.


Using AWS to Host a Static Website - Part 4

Related Posts

Using AWS to Host a Static Website - Part 6

Using AWS to Host a Static Website - Part 6

In part 5 I created the SSL certificates to use with the static website. In this post, I am going to deploy Amazon CloudFront for the CDN portion. Before I do that, I am going to upload the static HTML files that will be rendered.

Read More
Using AWS to Host a Static Website - Part 4

Using AWS to Host a Static Website - Part 4

In part 3 I walked through the creation of the Amazon S3 bucket where the static website content will reside. In this post I will walk through using CloudFormation to create an Amazon Route 53 public hosted zone. This is automatically created when a domain is registered using Amazon Route 53 so this is for completeness and is not required for this series of posts.

Read More
How to use a Password Manager to Store AWS Credentials

How to use a Password Manager to Store AWS Credentials

I like to have a way of avoiding having to have access_key_ids and secret_access_keys locally configured on my Mac.

Read More