Hosting a static website on AWS S3 with CI/CD
Tags: webdev, aws, s3, tutorial
Published: 2024-03-11 18:26:44

## Getting Started

You are expected to have a AWS account to continue with this tutorial, If you don't have one, create one! It is ideal to create a AdminRole and create projects within that instead of your ROOT account. This playlist from [**Tiny Technical Tutorials**](https://www.youtube.com/watch?v=W6jQmVi31Xk&list=PLwyXYwu8kL0wg9R_VMeXy0JiK5_c70IrV&index=5) will help you in that.

I believe you have basic understanding git, web technologies!

Now Grab your popcorns and let's start.

## Let's Create a Github Repo

If you already have a github repository where you store your website files, you can go ahead with that, else take a spoon and start feeding 😅

* run `npm create vite@latest aws-app -- --template vanilla-ts` on your terminal
    
* now type to terminal `cd aws-app` and go to the project directory ,now install dependencies `npm install` , start dev server `npm run dev`
    
* You web-app should be available at [`http://localhost:5173/`](http://localhost:5173/)
    
* Now let's push this to Github, Now we are ready to move on to next steps
    

## Uploading files to S3

If you are unfamilar with s3 please go through [tutorial series](https://www.youtube.com/watch?v=iF9fs8Rw4Uo&list=PLwyXYwu8kL0wg9R_VMeXy0JiK5_c70IrV&index=5) and develop some basic understanding. Simple Storage Service (Amazon S3) is an object storage service that can store almost all types of files. we will be using Amazon S3 to host our static website.

* Navigate to the project folder and run `npm run build` on your terminal.
    
* You should see `dist` folder getting generated. we will be using these build files for our website.
    
* Navigate to S3 and create the bucket with the domain of your website, we will be using webapp.hstart.in as our domain so we will create a bucket named `webapp.hstart.in` to differentiate it better, you are free to name it the way you want, go with default options and create bucket
    
* now navigate to S3 bucket and upload your files
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710061018034/92488f7b-ebe5-4506-b639-3e48afb8b168.png align="center")
    
* Now navigate to `properties` tab and scroll to bottom to find static website hosting, click on edit, and let's enable it, and save changes
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710061302102/79c21520-a747-4ff7-a49f-8f18b83a413b.png align="center")
    
    Although we have enabled, static website hosting, because we have blocked all public access during creating a bucket (default option), it cannot be accessed from outside, let's enable all public access.
    
* click on `Permissions` tab and click on edit to the right of **Block public access (bucket settings),** uncheck the box, and save changes, confirm the dialog.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710061577059/0eba0007-818c-4062-8f16-16a5f482b6c1.png align="center")
    
* Although we have enabled public access, if try to access the object from a browser using objectURL because we haven't attached a public access policy, website will show access denied error,Don't worry we will fix that soon.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710061840140/ad6c4fb3-0cc1-47cb-be58-a15846904afb.png align="center")
    
* Navigate to `permissions` tab again and click on **edit** beside the bucket policy and add the following policy, make sure to change the bucket name on your policy, this will allow all the public to get the object/file from s3 bucket you defined.
    
    ```json
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "PublicReadGetObject",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::webapp.hstart.in/*"
            }
        ]
    }
    ```
    
* Now you shouldn't be getting **Access Denied** error that show above, you should be able to access the objects/files from the objectURL of each file, this is what showed up when i tried the objectURL of js file within the dist/assets folder, we can see all the contents inside the js file.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710062492950/eea6cde8-95be-4890-ae8a-b7f5c7a36ae5.png align="center")
    

Everything seems to be configured correctly, still, why isn't it showing up as a website, this seems to be of no use. Don't worry, it will get fixed as we add cloudfront, SSL certificate to the mix.

## Setting Up the Cloudfront

Cloudfront is the CDN that we are going to distribute out website content across the globe, It will reduce the latency for our users by storing a replica of our website content closer to the users who frequently access our website. Let's navigate to cloudfront, start configuring it.

* click on **create distribution**
    
* In the *origin domain* input select your s3 Bucket, once you select the bucket it will give the below warning, as we are intend to use it for static website hosting, click on **use website endpoint ,** otherwise hosting will not work properly.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710065892528/78dd37f2-8ac2-4007-a548-3a759dfea04e.png align="center")
    
* select redirect http to https
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710066119399/c649c4b7-381b-41e1-ab87-8be804a9655b.png align="center")
    
* Disable **Website Application firewall** for now
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710066199236/769e8bfc-9249-4f7c-acb9-b7bac1eda909.png align="center")
    
* As I intend to use it with a subdomain of my own, I will be adding an SSL certificate, As we do not have a certificate for the subdomain , we will have to create it. Under *Settings section,* click on `Request Certificate` link , just below the *Custom SSL Certificate* Input
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710066503312/55ef4ef0-1922-4bcf-9e3d-94211251be49.png align="center")
    
* It will open AWS CERTIFICATE MANAGER on another window
    
    Make sure the location were you create certificate is US EAST (N. Virginia) Region (us-east-1), The Location will get automatically selected when you naviagte to AWS certificate manager, for some reason it doesn't, select us-east-1 region from the navbar and create the certificate
    
    select `request a public certificate` and click on next
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710066743448/85a92b71-db7f-4c93-a389-2c83f82d6694.png align="center")
    
* Add both www and non-www version of your subdomain, leave everything else on default, and click on `request certificate`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710066929674/7aae2626-f562-4500-9a38-d89f8dff6bcc.png align="center")
    
* now click on the `certificate` that is being issued, then click on `create records in route 53` , then confirm it, if you do not have a hosted zone in route53, you may need to create it first. Hosted zone is a paid service, it will cost around 0.6$ including tax per month to have one. this is where you set up the domain you have purchased. If you need a tutorial on how to set it up, let me know in the comments.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710067108345/83e567fe-e4e2-4abb-86f7-1114d5669310.png align="center")
    
    * Now comeback to our cloudfront tab (remember new tab got opened when we clicked on request certificate), click on refresh button beside the input, then select the certificate you just created.
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710067546148/016e42ba-dfce-41c7-9e99-2399be085a04.png align="center")
        
* Additionally you can add **Alternate domain name** , which is optional, settings section should look like this after you have done with all, keep everything else default, and click on `create distribution`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710067650729/abc49c8b-74c4-4d30-bba7-e1320457eb5c.png align="center")
    
* Now you'll be able to see your new created distribution
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710067913093/8aa1bf08-e0ca-4c73-873f-97b7e15f6591.png align="center")
    
* Setup invalidations, we need to invalidate cloudfront cache once changes happen inside our S3 bucket, Navigate to your distribution and click on `invalidations` tab. click on create invalidation, then add `/**/*` to the input field, this will trigger invalidations for changes inside the subfolders too. then click on `create invalidation.`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710069025219/ba3e4151-700c-4016-9a3a-ecd9d7901b7f.png align="center")
    
* Now copy the **domain name** from the distribution and try to access the address from your browser, Viola! your app is live on cloudfront
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710068131507/23e1d56a-7f38-4cca-a73a-46358b2a204a.png align="center")
    

## Setup subdomain

Although we have created certificate for our subdomain an all, route53 still doesn't know where to route the DNS queries coming to [http://webapp.hstart.in/](http://webapp.hstart.in/) so we need to set it up.

* Go to route 53 then navigate to your hosted zone.then click on `create record`
    
* Add your subdomain, select Record type A as shown in the image, toggle on *Alias ,* Select `Alias to cloudfront distribution,` then select newly created distribution from the dropdown, then `create records`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710068491332/8087e7ff-90b2-42af-a7e0-a2073b2c7e77.png align="center")
    
* It might take few minutes, our website is now live on [webapp.hstart.in](https://webapp.hstart.in/) , Yay 🎉.
    

## Re-routing www domain request to non www version.

People tend to add www before a web address, if we haven't set up this up, they might get DNS error, as we already have non-www version of our website up, now we will try to route www requests to non-www version of our website.

* As we already have a bucket with non-www version we will create a bucket with www version and route the requests to non-www, It is also possible to do it vice versa, it is your choice how to set it up.
    
* let's create a bucket with [`www.webapp.hstart.in`](http://www.webapp.hstart.in) name , as we did the last time
    
* As we did last time navigate to newly created bucket and go to permissions and deselect **Block public access (bucket settings)** and save changes
    
* we will use this bucket to redirect queries to our main bucket, so we don't need to add any public access policy, nor we need to add any files to this bucket.
    
* Go to properties tab, navigate to `static web hosting` and click on edit, click on enable, then select `Redirect requests for an object,` add host name (the domain where we want to redirect requests) then choose `https` , save the changes
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710071874711/fe773691-5210-48b7-9b06-f866f3337434.png align="center")
    
* Now go to cloudfront and create a new distribution that points to our newly created S3 Bucket, you can use the SSL certificate that we have created early, as we have added both www and non-www domain name versions of our website to the certificate
    
* once the coludfront distribution is created, go to route 53 and point www.webapp.hstart.in requests to the newly created distribution. same as we did before.
    

now [https://www.webapp.hstart.in/](https://www.webapp.hstart.in/) as well as [https://webapp.hstart.in/](https://www.webapp.hstart.in/) will resolve to [https://webapp.hstart.in/](https://www.webapp.hstart.in/) , Great! Our website is live and well

## CI / CD

How did we start? uploading files to S3, this is definitely not the ideal way to do things, manually building project, uploading files manually to S3 is not a sustainable solution. now we will introduce codebuild, code pipeline to make our deployment process fluid.

Our goal is to use codebuild to build our app and replace the files inside s3 with newly built files whenever we push new changes to our main/master branch. Let's integrate it.

* Go to codebuild and click on create project
    
* choose project name of your choice, i chose "webapp"
    
* Select your source provider, I use GitHub to host my code so i select Github, if you haven't connected to github before you may need to connect and authenticate your Github using a personal access token, once you have connected to github you'll be able to select your project repository from the dropdown
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710078320674/46fbf035-912b-4b84-9cff-6e8fe6e01851.png align="center")
    
* add the name of your main/master branch to `source version` input
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710073866032/ad16aa60-8568-4b5c-b7ec-a29fabba4854.png align="center")
    
* when we create this codebuild, a new role will get created for this project. we will attach necessary permissions/policies this particular role. policies gives resources permissions to make changes on other resources. As i already mentioned, codebuild need to modify objects(files) inside our s3 bucket each time a new build happens so that whenever code changes, it will get reflected on our website.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710074227759/cf0fa7bb-3baa-4ae8-8ca4-43b94586853d.png align="center")
    
* we will be using a buildspec.yml file to handle the build process, so select `use a buildspec file`. we will add buildspec.yml file to the root of our project
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710074347176/3100d885-2b27-4f69-8226-8b5d5a118606.png align="center")
    
    This is how out buildspec.yml file looks like, let's add this to root of our project and push it to github, make sure you change s3 bucket name to your s3 bucket name.
    
    ```yaml
    version: 0.2
    
    phases:
      install:
        runtime-versions:
          nodejs: 18 # Change this to match the version of Node.js you're using
      pre_build:
        commands:
          - npm install # Or any other command to install dependencies
      build:
        commands:
          - npm run build # Or any other command to build your static site assets
          - aws s3 sync ./dist/ s3://webapp.hstart.in --delete # Sync static site files to S3 bucket
      post_build:
        commands:
          - echo "Build completed successfully"
    
    artifacts:
      files:
        - '**/*'
    ```
    
    keep everything to default and create project
    
* Now go to your codebuild project and create a build. The build will get failed as codebuild don't have necessary permissions to work with. You can read the issues within the build log, we need to proceed accordingly. Don't worry, we will fix it soon.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710075099456/2c2d9b45-8cd8-4fa4-a043-74545e27d0bb.png align="center")
    
* Navigate to `IAM` --&gt; `roles` and find the role that just created by codebuild, you'll see a policy under the role in the permissions tab, click on it.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710075288933/ea99ae3c-fd85-45b4-bccf-17eba839628d.png align="center")
    
* You'll see code build already created some policies automatically for s3, but some permissions are missing, we also need to specify the s3 object where we stored our website files. It is always be better to specify buckets considering security, give permissions that are needed, nothing more, nothing less
    
* click on s3, then click on **Edit** , I added `Delete Object Put object`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710075715807/e9a6f175-8780-4525-9466-da978b41d344.png align="center")
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710076078127/1f5bdb32-78a3-4c43-ab24-00c01deca3c9.png align="center")
    
* `Delete Object` , `List Bucket` permissions additionally to the policy, you can use visual editor to make those changes.
    

add `Delete Object` , `List Bucket` permissions they can be found under Write, List sections respectively,

Also under Resources section you need to add the arn of your S3 bucket, also same arn with /\* to select all the objects inside. it is better to select JSON editor and add these lines inside the resources object

```yaml
arn:aws:s3:::webapp.hstart.in
arn:aws:s3:::webapp.hstart.in/*
```

The final policy should look similar to this, we only added 2 extra permissions in the Action array , 2 lines under resources array, everything else is created by codebuild.

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketAcl",
                "logs:CreateLogGroup",
                "logs:PutLogEvents",
                "codebuild:CreateReportGroup",
                "codebuild:CreateReport",
                "s3:PutObject",
                "s3:GetObject",
                "logs:CreateLogStream",
                "codebuild:UpdateReport",
                "codebuild:BatchPutCodeCoverages",
                "s3:DeleteObject",
                "codebuild:BatchPutTestCases",
                "s3:GetBucketLocation",
                "s3:GetObjectVersion"
            ],
            "Resource": [
                "arn:aws:codebuild:ap-south-1:343391345253:report-group/webapp-*",
                "arn:aws:logs:ap-south-1:343391345253:log-group:/aws/codebuild/webapp",
                "arn:aws:logs:ap-south-1:343391345253:log-group:/aws/codebuild/webapp:*",
                "arn:aws:s3:::codepipeline-ap-south-1-*",
                "arn:aws:s3:::webapp.hstart.in",
                "arn:aws:s3:::webapp.hstart.in/*"
            ]
        }
    ]
}
```

Once we are satisfied with policy, click on `next` , then click on `save changes` , make sure that the policies are saved properly.

Now navigate to codebuild and retry build, it should be successful now

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710077133411/f861986c-9774-4ccc-8171-bd7a1c141fe9.png align="center")

Now we can create build, with a click on a button, we still need to login to AWS, navigate to codebuild, and click on start build to release a new version of our website, we are too lazy for that. Let integrate codepipeline, and automate the codebuild process whenever we push to our main/master branch.

### Setting up code pipeline

* Navigate to code pipeline, click on `create pipeline` , we will choose v2 as we can create as many pipeline as we need without incuring additional cost. Also it will automatically create a new role so that we can add permissions later if needed. Once everything good click on next
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710077554536/d18ab01d-7638-4c91-8fbd-1e091419f4b9.png align="center")

* Here you will need to connect to github, click on connect to github, give it a name, click on install github app, allow permissions from github, it is pretty straight forward, you should be able to do it. If you are stuck at this, do let me know. once the connection is successfully established you should see following screen
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710078496018/f66787b6-c014-4566-adeb-e9e0bf68ace1.png align="center")
    
* Now specify a trigger to start a build pipeline, we will set it to on push to our main branch, now click on next
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710078602445/c75959cf-7537-4232-b7f0-25d743dde976.png align="center")
    
* Now we need to connect our code pipeline to the codebuild project we set up earlier, select AWS codebuild as our build provider, then select the project we created. If you have any Environment variables, that are being used in the project, you can add them at this step, you can add add them later too as you need. Once everything is good, click on next.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710078882762/9f8ceebd-aafa-4e56-974a-0aa31fb65fe7.png align="center")
    
* We can skip the deploy stage by clicking on the `skip deploy stage` button as we don't need it for our project.
    
* Then click on `create pipeline` to finish creating the pipeline
    
* If everything went well, you should see succeeded message, you can click on `release change` to release a new change.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710079141171/f7c5c01b-b764-4107-bd7e-88c01266c9f5.png align="center")
    
    Now our CI/CD pipeline is finally ready, if you push changes to your main branch and the changes will get reflected on the website. Try making changes.
    

## Extras

While I was working on this, I noticed the cloudfront cache is not getting invalidated somehow, So I decided to force the invalidation in the post build process, new buildspec.yml file is as follows

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 18 # Change this to match the version of Node.js you're using
  pre_build:
    commands:
      - npm install # Or any other command to install dependencies
  build:
    commands:
      - npm run build # Or any other command to build your static site assets
      - aws s3 sync ./dist/ s3://webapp.hstart.in --delete # Sync static site files to S3 bucket
  post_build:
    commands:
      - echo "Build completed successfully"
      - aws cloudfront create-invalidation --distribution-id ET5XFZER5THY --paths '/*' #invalidate the given cloudfront distribution

artifacts:
  files:
    - "**/*"
```

```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VisualEditor0",
			"Effect": "Allow",
			"Action": [
				"s3:ListBucket",
				"s3:GetBucketAcl",
				"logs:CreateLogGroup",
				"logs:PutLogEvents",
				"cloudfront:CreateInvalidation",
				"codebuild:CreateReportGroup",
				"codebuild:CreateReport",
				"s3:PutObject",
				"s3:GetObject",
				"logs:CreateLogStream",
				"codebuild:UpdateReport",
				"codebuild:BatchPutCodeCoverages",
				"s3:DeleteObject",
				"codebuild:BatchPutTestCases",
				"s3:GetBucketLocation",
				"s3:GetObjectVersion"
			],
			"Resource": [
				"arn:aws:codebuild:ap-south-1:343391345253:report-group/webapp-*",
				"arn:aws:logs:ap-south-1:343391345253:log-group:/aws/codebuild/webapp",
				"arn:aws:logs:ap-south-1:343391345253:log-group:/aws/codebuild/webapp:*",
				"arn:aws:cloudfront::343391345253:distribution/ET5XFZER5THY",
				"arn:aws:s3:::webapp.hstart.in/*",
				"arn:aws:s3:::codepipeline-ap-south-1-*",
				"arn:aws:s3:::webapp.hstart.in"
			]
		}
	]
}
```

I just added "cloudfront:CreateInvalidation" permission to Action array, also added "arn:aws:cloudfront::343391345253:distribution/ET5XFZER5THY" to the resources object. Which is nothing but the resource arn for our cloudfront distribution.

Now every-time we release a change, codebuild will invalidate the cloudfront cache automatically, and we will be able to see latest website!

hardcoding your bucket name and distribution id isn't a good practice we can move those in to environment variables, new buildspec.yml file will look like this

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 18 # Change this to match the version of Node.js you're using
  pre_build:
    commands:
      - npm install # Or any other command to install dependencies
  build:
    commands:
      - npm run build # Or any other command to build your static site assets
      - aws s3 sync ./dist/ s3://$S3_BUCKET --delete # Sync static site files to S3 bucket
  post_build:
    commands:
      - echo "Build completed successfully"
      - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*' #invalidate the given cloudfront distribution

artifacts:
  files:
    - "**/*"
```

we have two variables S3\_BUCKET and DISTRIBUTION\_ID let's define those in code build.

Navigate to codebuild, select our project "webapp" and click on edit, go to the section **Environment** and click on **Additional configuration** add your environment variables

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710089665740/1e6b90aa-9385-41f1-b226-a0200c296ea1.png align="center")

click on update project, we are good to go!

## End Notes

I hope this article has been helpful for you guys.

There are more things to add like building and testing the code when a pull request is raised. To reduce the amount bugs gets into production, that's for another day.

If you guys have any doubts, feedbacks feel free to drop in the comments. Happy building 😊