This blog post is a tutorial on how to deploy a static web application to AWS S3.

It includes a comprehensive explanation on what we’re doing; hence the length! However, there will be some skipping points which you can click when you don’t want to hear about lengthy explanations.

For you who like to dive into the source code directly, I’ve made a Github repo for this post as well.

I present to you the first skipping point. It will escort you to the Architecture Overview without having to deal with the reasoning behind this post.

Why are we doing this?

First of all, I like to save money; and S3 has a great pricing scheme. There are some other advantages, though.

However, it does not support back-end codes such as Java, Go, Python, etc. It’s a “Simple Storage Service”.

This post will use one of the most popular frameworks: ReactJS.

But you don’t need to use React specifically. If you have any preferred frameworks like VueJS, Angular, or even plain HTML; you can use S3 too. As long as it produces static sites.

Hold on, how do I interact with the database then?

Usually, the website would interact with an API(s) which is hosted somewhere else. This API(s) will interact with the database. One of the most popular options in building serverless API(s) can be achieved by using AWS Lambda. However, I won’t talk about that in this post.

What can I expect from this post?

I will cover how to deploy a simple ReactJS application (“Hello World!”) to an S3 Bucket which can later be accessed through our browser. It includes:

  1. Hosting in S3;
  2. Using CloudFront as a CDN;
  3. Securing our S3 bucket;
  4. Using a custom domain for your CloudFront distribution (optional);
  5. One-step deployment using CodeBuild (optional);
  6. Doing all of them using Serverless Framework;

However, if you’re expecting a ReactJS tutorial, this is not the one.

Now we’re in business!

Architecture Overview

Before we dive in, take a look at the architecture overview below.

It’s ok if you’re not familiar with the names. We’re going to talk about it!

From the image above, we can tell that when a user wants to access our React application, they will go through CloudFront which will access our React application in an S3 Bucket.

AWS CloudFront is a CDN (Content Delivery Network) from AWS. Think of it like your favourite restaurant opening a branch near your house. It brings the resources closer to you! CDN won’t serve food, but it will serve you web contents (images/videos/any static files).

An S3 Bucket is the origin of those resources. Think about the main branch of your favourite restaurants. It may not always be the case, but imagine that the “new” branches require supplies for the food; and those supplies will be provided from the “main” branch. Anyhow, it’s where you have to upload your React application.

Furthermore, CloudFront will protect our wallet by preventing abusive GET requests to our S3 Bucket (we need to pay per GET request!).

You’ve encountered the second skipping point. It will escort you to the end of this post! It’s great if you just need the solution rather than the explanation.

Serverless Framework Initialisation

You can entirely skip this if you’re familiar with Serverless Framework.

The first step of our initialisation is to configure our machine to access AWS resources. You need to inject AWS credentials into your computer. Make sure that you have configured your credentials file.

The second step is to create a serverless.yml file. This is a manual process. The hardest part of this step is to determine where would you put the file. I like to put it within the React project itself. So it’d be something like this:

Where I put my serverless.yml file (you can put it anywhere!)

Let’s write our initial configuration in the serverless.yml.

service:
  name: example-app

provider:
  name: aws
  region: ap-southeast-2

These 2 blocks are something you need to do for every serverless project.

service is your service name. This will be reflected in your CloudFormation Stack.

The provider section tells the framework that you’re using aws and you’re using ap-southeast-2 (Sydney) as your AWS region.

Orchestrating Our Resources

To help you navigate through this post, these are the steps that we’d take:

  1. Creating an S3 Bucket;
  2. Creating a CloudFront Distribution;
  3. Securing our S3 Bucket;
  4. Using a custom domain for our CloudFront Distribution (optional);
  5. Creating a “one-step” deployment with CodeBuild (optional);

And below is the final configuration. Feel free to just read this configuration and ignore the explanations. Note that this configuration does not use a custom domain.

# the previous `service` and `provider` block
resources:
  Resources:
    FrontPageWebsiteBucket:
      Type: AWS::S3::Bucket
    FrontPageWebsiteBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref FrontPageWebsiteBucket
        PolicyDocument:
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource: 
                Fn::Join:
                  - /
                  - - Fn::GetAtt:
                        - FrontPageWebsiteBucket
                        - Arn
                    - '*'
              Principal:
                CanonicalUser: 
                  Fn::GetAtt: 
                    - FrontPageWebsiteOriginAccessIdentity
                    - S3CanonicalUserId
    FrontPageWebsiteOriginAccessIdentity:
      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
      Properties:
        CloudFrontOriginAccessIdentityConfig:
          Comment: Origin Access Identity to Access Website Bucket
    FrontPageCloudFront:
      Type: AWS::CloudFront::Distribution
      DependsOn:
        - FrontPageWebsiteBucket
      Properties:
        DistributionConfig: 
          Origins:
            - DomainName: 
                Fn::GetAtt: 
                  - FrontPageWebsiteBucket
                  - DomainName
              Id: S3Origin
              S3OriginConfig:
                OriginAccessIdentity: 
                  Fn::Join: 
                    - /
                    - - origin-access-identity
                      - cloudfront
                      - !Ref FrontPageWebsiteOriginAccessIdentity
          CustomErrorResponses:
            - ErrorCachingMinTTL: 0
              ErrorCode: 403
              ResponseCode: 200
              ResponsePagePath: /index.html
          DefaultCacheBehavior:
            AllowedMethods:
              - GET
              - HEAD
            Compress: true
            ForwardedValues:
              QueryString: true
              Cookies:
                Forward: none
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
          Comment: my example website in s3
          DefaultRootObject: index.html
          Enabled: true
          HttpVersion: http2
          PriceClass: PriceClass_All
          ViewerCertificate:
            CloudFrontDefaultCertificate: true

This is the third skipping point. It will escort you to the end of this post! Or you can stop here if you like. The rest of this post will explain the content of our serverless.yml.

1. Creating an S3 Bucket

This is very straightforward. Take a look at the configuration for creating an S3 bucket below:

FrontPageWebsiteBucket:
   Type: AWS::S3::Bucket

That’s it!

2. Creating a CloudFront Distribution

This is a bit complicated. But I’ll try to explain it.

FrontPageCloudFront:
  Type: AWS::CloudFront::Distribution
  DependsOn:
    - FrontPageWebsiteBucket
  Properties:
    DistributionConfig: 
      Origins:
        - DomainName: 
            Fn::GetAtt: 
              - FrontPageWebsiteBucket
              - DomainName
          Id: S3Origin
          S3OriginConfig:
            OriginAccessIdentity: 
              Fn::Join: 
                - /
                - - origin-access-identity
                  - cloudfront
                  - !Ref FrontPageWebsiteOriginAccessIdentity
      CustomErrorResponses:
        - ErrorCachingMinTTL: 0
          ErrorCode: 403
          ResponseCode: 200
          ResponsePagePath: /index.html
      DefaultCacheBehavior:
        AllowedMethods:
          - GET
          - HEAD
        Compress: true
        ForwardedValues:
          QueryString: true
          Cookies:
            Forward: none
        TargetOriginId: S3Origin
        ViewerProtocolPolicy: redirect-to-https
      Comment: my example website in s3
      DefaultRootObject: index.html
      Enabled: true
      ViewerCertificate:
        CloudFrontDefaultCertificate: true

Ugh.. It’s really long isn’t it.

Let’s break it down.

Origins

This is essentially telling our CloudFront distribution to retrieve files from the S3 Bucket that we’ve created before.

Origins:
  - DomainName: 
      Fn::GetAtt: 
        - FrontPageWebsiteBucket
        - DomainName
    Id: S3Origin
    S3OriginConfig:
      OriginAccessIdentity: 
        Fn::Join: 
          - /
          - - origin-access-identity
            - cloudfront
            - !Ref FrontPageWebsiteOriginAccessIdentity

Ignore the OriginAccessIdentity for now. It’ll be used to secure our S3 Bucket.

CustomErrorResponses

This is applicable mostly for SPA (e.g. React/Angular) as we want to handle routes in our application. However, if you’re using HTML, you don’t need to do this.

CustomErrorResponses:
  - ErrorCachingMinTTL: 0
    ErrorCode: 403
    ResponseCode: 200
    ResponsePagePath: /index.html

Any 403 (Forbidden) Errors will be redirected to our index.html. This is essential as when we access routes (e.g. /home or /about), most SPA doesn’t have the corresponding folder for the routes; hence the 403.

DefaultCacheBehavior

This is where we can optimise our delivery by compressing (gzipping) our static files, forwarding query strings to our app, redirecting HTTP to HTTPS, and only allowing certain HTTP methods.

DefaultCacheBehavior:
  AllowedMethods:
    - GET
    - HEAD
  Compress: true
  ForwardedValues:
    QueryString: true
    Cookies:
      Forward: none
  TargetOriginId: S3Origin
  ViewerProtocolPolicy: redirect-to-https

Note the TargetOriginId must match with Id in Origins.

ViewerCertificate

This is used for specifying the SSL certificate for our distribution. As we’re using CloudFront’s domain name, we can just use the default certificate from CloudFront.

ViewerCertificate:
  CloudFrontDefaultCertificate: true

Other Configs

Comment: my example website in s3
DefaultRootObject: index.html
Enabled: true

Comment is optional. This is used for making it easy to identify your distribution in AWS console.

DefaultRootObject is the default file you want to access. In our case, it’s index.html.

Enabled enables the distribution (duh!). Although seems unnecessary, this is required.

3. Securing S3 Bucket

I know it’s a vague heading as security is a really broad topic. This post will only focus on securing your S3 bucket from abusive requests.

Because you’ve created your CloudFront distribution, you don’t want people to access your S3 bucket directly. Imagine a person issuing thousands of GET requests to your S3 bucket per second. Your wallet will cry.

The concept is pretty simple. You only want your CloudFront distribution to access your S3 bucket. No one else (except authorised users like you).

The first step is to create an identity for your CloudFront distribution. Remember OriginAccessIdentity in the previous section?

These are 2 pieces that you need to secure your bucket.

FrontPageWebsiteBucketPolicy:
  Type: AWS::S3::BucketPolicy
  Properties:
    Bucket: !Ref FrontPageWebsiteBucket
    PolicyDocument:
      Statement:
        - Effect: Allow
          Action:
            - s3:GetObject
          Resource: 
            Fn::Join:
              - /
              - - Fn::GetAtt:
                    - FrontPageWebsiteBucket
                    - Arn
                - '*'
          Principal:
            CanonicalUser: 
              Fn::GetAtt: 
                - FrontPageWebsiteOriginAccessIdentity
                - S3CanonicalUserId
FrontPageWebsiteOriginAccessIdentity:
  Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
  Properties:
    CloudFrontOriginAccessIdentityConfig:
      Comment: Origin Access Identity to Access Website Bucket

The first block is a policy for our S3 bucket. Essentially, it tells our bucket that it should whitelist our CloudFront.

The second block is the identity of our CloudFront. Nothing much in there; but you need to reference that block in your CloudFront origin.

Below is how you attach it. (this is just a copy-paste from the previous section).

Origins:
  - DomainName: 
      Fn::GetAtt: 
        - FrontPageWebsiteBucket
        - DomainName
    Id: S3Origin
    S3OriginConfig:
      OriginAccessIdentity: 
        Fn::Join: 
          - /
          - - origin-access-identity
            - cloudfront
            - !Ref FrontPageWebsiteOriginAccessIdentity

4. Using a custom domain for CloudFront distribution (optional)

This step is optional and requires you to have a custom domain.

Therefore, I present you with the fourth skipping point. It will take you to the end of this post. You can go to Step 5: One-step deployment using CodeBuild too if you like.

Let’s be honest. Even though CloudFront’s domain name is free, it’s still ugly.

Prerequisite

To be able to use your domain name in your CloudFront distribution, you need to point the domain to CloudFront’s ugly domain.

You can use another company to manage your DNS record and use your custom SSL certificate. However, I’m going to use Route53 (DNS) and an SSL certificate issued from AWS Certificate Manager.

You don’t have to use them, but I’ll be using them in this post.

Generating SSL Certificate in AWS Certificate Manager

I think the tutorial from AWS is clear enough.

The certificate is free when you use them with CloudFront.

Note that you need to create the certificate in us-east-1 region. Otherwise, CloudFront won’t pick up the certificate.

Registering your domain name in Route53

If you haven’t bought the domain, AWS has provided a straightforward tutorial to register domain names with them.

If you already have a domain name and want to move the records to Route53, you can follow yet another AWS tutorial.

Now all you need to do is to create a Hosted Zone in Route53 for your domain. AWS should automatically create one for you when you register your domain with them. However, if for some reason you’re not seeing your domain in the hosted zone list, you can always make a new one.

Now we’re ready.

First of all, this is the final serverless.yml file:

# the previous `service` and `provider` block
resources:
  Resources:
    FrontPageWebsiteBucket:
      Type: AWS::S3::Bucket
    FrontPageWebsiteBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref FrontPageWebsiteBucket
        PolicyDocument:
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource: 
                Fn::Join:
                  - /
                  - - Fn::GetAtt:
                        - FrontPageWebsiteBucket
                        - Arn
                    - '*'
              Principal:
                CanonicalUser: 
                  Fn::GetAtt: 
                    - FrontPageWebsiteOriginAccessIdentity
                    - S3CanonicalUserId
    FrontPageWebsiteOriginAccessIdentity:
      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
      Properties:
        CloudFrontOriginAccessIdentityConfig:
          Comment: Origin Access Identity to Access Website Bucket
    FrontPageCloudFront:
      Type: AWS::CloudFront::Distribution
      DependsOn:
        - FrontPageWebsiteBucket
      Properties:
        DistributionConfig: 
          Origins:
            - DomainName: 
                Fn::GetAtt: 
                  - FrontPageWebsiteBucket
                  - DomainName
              Id: S3Origin
              S3OriginConfig:
                OriginAccessIdentity: 
                  Fn::Join: 
                    - /
                    - - origin-access-identity
                      - cloudfront
                      - !Ref FrontPageWebsiteOriginAccessIdentity
          CustomErrorResponses:
            - ErrorCachingMinTTL: 0
              ErrorCode: 403
              ResponseCode: 200
              ResponsePagePath: /index.html
          DefaultCacheBehavior:
            AllowedMethods:
              - GET
              - HEAD
            Compress: true
            ForwardedValues:
              QueryString: true
              Cookies:
                Forward: none
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
          Comment: my example website in s3
          DefaultRootObject: index.html
          Enabled: true
          HttpVersion: http2
          PriceClass: PriceClass_All
          ViewerCertificate:
            AcmCertificateArn: arn:aws:acm:us-east-1:....
            MinimumProtocolVersion: TLSv1.1_2016
            SslSupportMethod: sni-only
          Aliases:
            - example.com
    FrontPageDNSName:
      Type: AWS::Route53::RecordSetGroup
      Properties:
        HostedZoneName: example.com.
        RecordSets:
          - Name: example.com
            Type: A
            AliasTarget:
              HostedZoneId: Z2FDTNDATAQYW2 #cloudfront hostedzone id
              DNSName: 
                Fn::GetAtt:
                  - FrontPageCloudFront
                  - DomainName

This is a modified version from our previous serverless.yml.

The fifth skipping point. It will take you to the end of this post. You can go to Step 5: One-step deployment using CodeBuild too if you like. The rest of this section is just explaining what we’ve changed in the serverless.yml.

Creating a DNS record in your HostedZone

We will create a record in our hosted zone.

If you’re not using Route53, skip this bit.

FrontPageDNSName:
  Type: AWS::Route53::RecordSetGroup
  Properties:
    HostedZoneName: example.com.
    RecordSets:
      - Name: example.com
        Type: A
        AliasTarget:
          HostedZoneId: Z2FDTNDATAQYW2 #cloudfront hostedzone id
          DNSName: 
            Fn::GetAtt:
              - FrontPageCloudFront
              - DomainName

That configuration will create a DNS record (example.com) and point it to our CloudFront domain. You might notice that there’s a hard-coded value in the HostedZoneId. It’s a static value which you can use as well. But you need proof.

Note that the HostedZoneName is not the same as Name in RecordSets. HostedZoneName is the”root” domain. The Name in RecordSets is the domain name you want for your CloudFront distribution.

Example: You want to point website.example.com to your CloudFront distribution. HostedZoneName would be example.com. and the Name in RecordSets would be website.example.com.

Registering your domain name in CloudFront

The next thing you need to do is to let CloudFront know that you will be using your custom domain name. This applies to whether you’re using Route53 or not.

ViewerCertificate:
  AcmCertificateArn: arn:aws:acm:us-east-1:....
  MinimumProtocolVersion: TLSv1.1_2016
  SslSupportMethod: sni-only
Aliases:
  - example.com

We have to change the certificate to a custom one as CloudFront’s default certificate only supports CloudFront’s domain name. In this case, I’m using my previously created certificate from AWS Certificate Manager.

We also have to add our domain name to the Aliases block. This is not the HostedZoneName. If you want to point a subdomain (website.example.com), use the subdomain instead of the “root” domain name.

5. One-step deployment using CodeBuild (optional)

To be honest, I hesitated to include this in this post. There are so many ways to deploy your static application that you may find this section useless.

However, for the sake of completeness, I’m going to provide you a simple deployment method that can be useful in certain situations.

The goals are:

  1. Easy & fast deployment;
  2. Execute certain commands before/after deploying (testing, notification, etc);

However, the method in this post will have some limitations such as:

  1. The “one-step” mentioned is a manual process. It’s not automated through git;
  2. Does not support complex pipeline (e.g. manual approval);

Time for another skipping point. It will guide you to the end of this post.

Let’s start.

First: if you haven’t done this, you need to put your project to Github / Bitbucket / CodeCommit. CodeBuild needs to retrieve your project from somewhere.

Second: if you’re using private repositories on Github / Bitbucket, you need to allow CodeBuild to access your repository. You need to connect your AWS account to your Github / Bitbucket account. You can do this by attempting to create a build project in AWS Console > CodeBuild > Create Project. In the Source section, pick Github / Bitbucket, then there will be an option to connect your Github / Bitbucket account to CodeBuild. After connecting, you can just close the tab. You don’t need to create the project for now.

Third: Create a CodeBuild project (we’ll focus on this from now on).

Creating the project

We’re going to use Serverless Framework to do this as well. This will be a different project from our previous attempt on creating S3 + CloudFront.

We need to create another serverless.yml file in a different folder. I like to create a __deploy folder in the React project and put the file there. However, it’s only a matter of preference.

Where I put serverless.yml for CodeBuild (you can put it anywhere!)

Now, let’s see what’s inside our serverless.yml.

service:
  name: example-app-deploy

provider:
  name: aws
  region: ap-southeast-2

resources:
  Resources:
    CodeBuildRole:
      Type: AWS::IAM::Role
      Properties: 
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement: 
            - Effect: Allow
              Principal: 
                Service: 
                  - codebuild.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: ${self:service.name}-build-policy
            PolicyDocument: 
              Version: '2012-10-17'
              Statement: 
                - Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    Fn::Join:
                      - ':'
                      - - arn:aws:logs
                        - !Ref AWS::Region
                        - !Ref AWS::AccountId
                        - log-group
                        - /aws/codebuild/example-app-deploy
                        - '*'
                        - '*'
                - Effect: Allow
                  Action: s3:PutObject
                  Resource: 
                    Fn::Join:
                      - ':'
                      - - arn:aws:s3
                        - ''
                        - ''
                        - example-app-dev-frontpagewebsitebucket-19jr14sozhbv6/*
                - Effect: Allow
                  Action: cloudfront:CreateInvalidation
                  Resource: 
                    Fn::Join:
                      - ':'
                      - - arn:aws:cloudfront
                        - ''
                        - !Ref AWS::AccountId
                        - distribution/E12MWEG8DY8FNG
    CodeBuildProject:
      Type: AWS::CodeBuild::Project
      Properties:
        Name: ${self:service.name}
        Description: My website builder
        ServiceRole: !Ref CodeBuildRole
        Artifacts:
          Type: NO_ARTIFACTS
        Environment:
          ComputeType: BUILD_GENERAL1_SMALL
          Image: aws/codebuild/standard:2.0
          Type: LINUX_CONTAINER
        Source:
          Location: https://github.com/kkesley/react-s3-blog-example.git
          Type: GITHUB

There are 2 main resources created here:

  1. The IAM role for our CodeBuild project
  2. The CodeBuild project itself

The role configuration is long but straightforward. When you read it, you’ll notice that we’re giving the role 3 permissions: Writing build logs to a specific CloudWatch group, uploading files to an S3 bucket, and creating invalidations to our CloudFront distribution. You need to change the Resource value as yours won’t be the same as mine.

Next is the build project itself. The only thing you need to change is the git URL in Location and possibly the Type if you’re not using Github.

Note that this template is far from ideal. Ideally, you’d want those hardcoded resources to be dynamic. You can use the output block from CloudFormation to fill those hardcoded resources. However, this post won’t cover that.

Creating the build step

We need to create a series of commands for CodeBuild to execute. This can be anything depending on your situation. These steps are expressed in a file called buildspec.yml. We can change the name of the file. But I’ll use buildspec.yml as it’s the default name.

Now, where should I put this buildspec.yml? Anywhere in your repository. You can instruct CodeBuild where to look for your buildspec.yml later (I’m going to put it in the root of my React project).

Before we dive into the details, here is my buildspec.yml.

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 10
    commands:
      - npm install
  build:
    commands:
      - npm run test
      - npm run build
      - aws s3 cp ./build s3://$WEBSITE_BUCKET --recursive
      - aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths /*

From the steps above, you can see we have several commands:

  1. we’re using NodeJS environment;
  2. install dependencies;
  3. test our application;
  4. build our application;
  5. copy our build folder to our S3 bucket;
  6. invalidate CloudFront cache (so CloudFront will pick up our updates);

And that’s it!

But…. Where do we define $WEBSITE_BUCKET and $CLOUDFRONT_DISTRIBUTION_ID?

We’re going to use environment variables!

You can use .json file to specify that. I’m going to call my file build-dev.json (for development). Here’s the content of build-dev.json.

{
    "projectName": "example-app-deploy",
    "sourceVersion": "master",
    "environmentVariablesOverride": [
      { "name": "STAGE", "value": "dev", "type": "PLAINTEXT" },
      { "name": "WEBSITE_BUCKET", "value": "example-app-dev-frontpagewebsitebucket-19jr14sozhbv6", "type": "PLAINTEXT" },
      { "name": "CLOUDFRONT_DISTRIBUTION_ID", "value": "E12MWEG8DY8FNG", "type": "PLAINTEXT" }
    ],
    "buildspecOverride": "./buildspec.yml"
}

Now we can see the value of those environment variables. $WEBSITE_BUCKET will be our bucket name and $CLOUDFRONT_DISTRIBUTION_ID will be our CloudFront distribution ID. Obviously, you need to change the value to your resources.

projectName is the name of your CloudBuild project. In this case I’m naming it example-app-deploy.

buildspecOverride can be used to point CodeBuild to your buildspec.yml. I’m storing the buildspec.yml in the project’s root folder.

sourceVersion will specify which branch are you building from (although it’s not just branches).

Where I put my buildspec.yml and build-dev.json (again, you can put it anywhere!)

Later, you can run the build by using this command:

aws codebuild start-build --cli-input-json file://build-dev.json

I like to register the command to npm scripts.

I’m going to call it deploy:dev. So, in our package.json, I’d have something like this:

{
  .... some other blocks ....
  "scripts": {
    .... some other scripts ....
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "deploy:dev": "aws codebuild start-build --cli-input-json file://build-dev.json"
  },
  .... some other blocks ....
}

Now everything’s connected!

We will issue a build command to our CodeBuild project with parameters from build-dev.json. Then, CodeBuild will execute the commands you specify in buildspec.yml.

The End

Phew! Finally!

The only thing you need to do now is to deploy it! Run this command in the folder that contains serverless.yml (S3 + CloudFront):

sls deploy

It will take some time to finish..

Let’s try it! Upload the content of React build folder / Angular dist folder / a sample index.html to your bucket.

Then, try to access it via your CloudFront’s domain. You can find it in AWS Console > CloudFront > Distributions > Look at the Domain Name column. It should work.

Then, try to access it via your S3 URL (e.g. http://{website-bucket}.s3-website-ap-southeast-2.amazonaws.com/index.html). It shouldn’t work.

Yay!

If you’re using CodeBuild, run sls deploy in your folder which contains CodeBuild resources. It will create a CodeBuild project for you.

To test the one-step deployment thing, run yarn deploy:dev in your React project. You can watch the build process in AWS Console > CodeBuild > Build History > Pick the latest (top) one.

Now for the copy-pasting job. Here’s the complete serverless.yml.

Without custom domain name:

service:
  name: example-app

provider:
  name: aws
  region: ap-southeast-2

resources:
  Resources:
    FrontPageWebsiteBucket:
      Type: AWS::S3::Bucket
    FrontPageWebsiteBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref FrontPageWebsiteBucket
        PolicyDocument:
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource: 
                Fn::Join:
                  - /
                  - - Fn::GetAtt:
                        - FrontPageWebsiteBucket
                        - Arn
                    - '*'
              Principal:
                CanonicalUser: 
                  Fn::GetAtt: 
                    - FrontPageWebsiteOriginAccessIdentity
                    - S3CanonicalUserId
    FrontPageWebsiteOriginAccessIdentity:
      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
      Properties:
        CloudFrontOriginAccessIdentityConfig:
          Comment: Origin Access Identity to Access Website Bucket
    FrontPageCloudFront:
      Type: AWS::CloudFront::Distribution
      DependsOn:
        - FrontPageWebsiteBucket
      Properties:
        DistributionConfig: 
          Origins:
            - DomainName: 
                Fn::GetAtt: 
                  - FrontPageWebsiteBucket
                  - DomainName
              Id: S3Origin
              S3OriginConfig:
                OriginAccessIdentity: 
                  Fn::Join: 
                    - /
                    - - origin-access-identity
                      - cloudfront
                      - !Ref FrontPageWebsiteOriginAccessIdentity
          CustomErrorResponses:
            - ErrorCachingMinTTL: 0
              ErrorCode: 403
              ResponseCode: 200
              ResponsePagePath: /index.html
          DefaultCacheBehavior:
            AllowedMethods:
              - GET
              - HEAD
            Compress: true
            ForwardedValues:
              QueryString: true
              Cookies:
                Forward: none
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
          Comment: my example website in s3
          DefaultRootObject: index.html
          Enabled: true
          HttpVersion: http2
          PriceClass: PriceClass_All
          ViewerCertificate:
            CloudFrontDefaultCertificate: true

With custom domain name:

service:
  name: example-app

provider:
  name: aws
  region: ap-southeast-2

resources:
  Resources:
    FrontPageWebsiteBucket:
      Type: AWS::S3::Bucket
    FrontPageWebsiteBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref FrontPageWebsiteBucket
        PolicyDocument:
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource: 
                Fn::Join:
                  - /
                  - - Fn::GetAtt:
                        - FrontPageWebsiteBucket
                        - Arn
                    - '*'
              Principal:
                CanonicalUser: 
                  Fn::GetAtt: 
                    - FrontPageWebsiteOriginAccessIdentity
                    - S3CanonicalUserId
    FrontPageWebsiteOriginAccessIdentity:
      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
      Properties:
        CloudFrontOriginAccessIdentityConfig:
          Comment: Origin Access Identity to Access Website Bucket
    FrontPageCloudFront:
      Type: AWS::CloudFront::Distribution
      DependsOn:
        - FrontPageWebsiteBucket
      Properties:
        DistributionConfig: 
          Origins:
            - DomainName: 
                Fn::GetAtt: 
                  - FrontPageWebsiteBucket
                  - DomainName
              Id: S3Origin
              S3OriginConfig:
                OriginAccessIdentity: 
                  Fn::Join: 
                    - /
                    - - origin-access-identity
                      - cloudfront
                      - !Ref FrontPageWebsiteOriginAccessIdentity
          CustomErrorResponses:
            - ErrorCachingMinTTL: 0
              ErrorCode: 403
              ResponseCode: 200
              ResponsePagePath: /index.html
          DefaultCacheBehavior:
            AllowedMethods:
              - GET
              - HEAD
            Compress: true
            ForwardedValues:
              QueryString: true
              Cookies:
                Forward: none
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
          Comment: my example website in s3
          DefaultRootObject: index.html
          Enabled: true
          HttpVersion: http2
          PriceClass: PriceClass_All
          ViewerCertificate:
            AcmCertificateArn: arn:aws:acm:us-east-1:....
            MinimumProtocolVersion: TLSv1.1_2016
            SslSupportMethod: sni-only
          Aliases:
            - example.com
    FrontPageDNSName:
      Type: AWS::Route53::RecordSetGroup
      Properties:
        HostedZoneName: example.com.
        RecordSets:
          - Name: example.com
            Type: A
            AliasTarget:
              HostedZoneId: Z2FDTNDATAQYW2 #cloudfront hostedzone id
              DNSName: 
                Fn::GetAtt:
                  - FrontPageCloudFront
                  - DomainName

It’s not working!

While creating this post, I experienced an unusual error. Once I accessed my CloudFront URL (e.g. xxxxxx.cloudfront.net), I got redirected (HTTP 307) to an S3 URL (e.g. my-bucket.s3-ap-southeast-2.amazonaws.com/index.html) and it’s throwing me HTTP 403 error.

After a moment of googling, I arrived at a thread that explains the error. The DNS record for your newly created bucket is not propagated yet.

The solution? Patience. It says S3 could take an hour to be ready.

However, I couldn’t confirm this as I ended up redeploying the stack and left it for hours before I upload any files to the S3 Bucket and access it via CloudFront’s domain. But hey, at least it’s working now!

Another alternative is to use S3’s RegionalDomainName instead of DomainName. Change that in the Origins section:

Origins:
  - DomainName: 
      Fn::GetAtt: 
        - FrontPageWebsiteBucket
        - RegionalDomainName
    Id: S3Origin
    S3OriginConfig:
      OriginAccessIdentity: 
        Fn::Join: 
          - /
          - - origin-access-identity
            - cloudfront
            - !Ref FrontPageWebsiteOriginAccessIdentity

If you have any tricks regarding this please let me know!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: