Static Website into AWS S3 with Github Actions

If you use AWS and GitHub for your project, one option to deploy your frontend is to host the static files of your built application in S3 and make them public. This article is covering this specific case. It is meant for developers who have already made the decision of the technologies mentioned above and therefore know more or less what they are.
This article assumes you already have an AWS account and know the basics about navigating in the AWS console. If this is not the case, I would recommend you follow the official AWS tutorial to get started.
It also assumes you have a GitHub project containing the code (Angular/React/…) for the static web page to deploy to AWS.
Create the GitHub Action
Let’s assume you have an Angular application ready that you want to make available online over S3. You have picked AWS S3, you use GitHub, and therefore want to use GitHub Actions as your CI/CD tool. How do you start?
You can create a GitHub Action directly in the GitHub web interface by clicking on “Actions” and create a new one. GitHub will offer you some common template, but you can start from scratch by clicking “setup a workflow yourself”. This will lead you to a text editor. Saving the file will create a new file in your repository, under .github/workflows/. All your actions will be there and vice versa, all YAML files created in this folder will be understood as action templates by GitHub.

That means that you can actually get started in your usual code editor and just create the folders .github/workflows and add your action template there.

Build Job
You should now have an empty action template, whether in your code editor or in GitHub. In this template, we will declare to GitHub the list of actions to perform to deploy our Angular application to S3. There will be two main steps in this case: first build the app, then deploy it.
Let’s start by building the application:
name: Action Template Name
on:
push:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node 20.x
uses: actions/setup-node@v1
with:
node-version: '20.x'
- name: Install dependencies
run: npm i
- name: Build
run: npm run build
- name: Archive build
uses: actions/upload-artifact@v1
with:
name: dist
path: dist/testapp
We first need to give some global information: the template name and when the action should be performed. In the example above, the action is triggered when something is pushed onto the main branch.
What exactly should be done when the action is triggered is described right after. We can define many jobs (we will eventually have two: build and deploy). The job is running on ubuntu-latest, it uses
- the code (“checkout”)
- node 20.x
and consists in:
- installing npm dependencies (“npm i”)
- run the “build” npm command (which should be described in your package.json)
- and keeps it in the artifacts for future use (in future jobs)
Paste the template above into your empty template file. Adapt the different values (in particular the name, the name of the build destination folder and the branch) and commit/push your changes. After a few second the action should appear (maybe after a refresh) in your GitHub Actions and you should see it running. If all goes well, after a few minutes your action should be successful:

Deployment Job
The next step is to deploy the built application in S3. For this we will need to have an S3 bucket correctly set up for static website hosting, and need to provide access to it to your GitHub action.
The final template will look like this:
name: Action Template Name
on:
push:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node 20.x
uses: actions/setup-node@v1
with:
node-version: '20.x'
- name: Install dependencies
run: npm i
- name: Build
run: npm run build
- name: Archive build
uses: actions/upload-artifact@v1
with:
name: dist
path: dist/testapp
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Download build
uses: actions/download-artifact@v1
with:
name: dist
- name: Set AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_PROD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_PROD_SECRET_ACCESS_KEY }}
aws-region: eu-central-1
- name: Sync files to S3 bucket
run: aws s3 cp dist s3://testapp --recursive
The deploy blocks first gets the artificats from the previous build step. It then connects to AWS using some access key id / secret combination. For security reasons, the key is not directly given in the template, but is fetched at runtime from GitHub secrets. We will need to create this key and save it in the GitHub secrets for the template to work.
Once the action is “logged in “ into AWS, it pushes the static files into the S3 bucket. This can only work if the AWS user used for login has the right permission. We will see how to create the user properly in order for it to work.
Once the files are pushed, the website should be online. This will only be the case if the S3 bucket has been created and is configured correctly. This is what we will start with.
S3 Bucket
If you don’t already have an S3 bucket, navigate to S3 in your AWS console and create a new bucket:

Give it a name and scroll down. You will need to allow public access to your bucket files in order for it to be accessible by everyone online. Uncheck the box Block all public access.
Once your bucket is ready, navigate to Properties and scroll down until you find the block Static website hosting. Edit it to enable it:

Finally, navigate to Permissions and edit the bucket policy. You need to allow anyone to fetch the content of the bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1405592139000",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::testapp/*",
"arn:aws:s3:::testapp"
]
}
]
}
IAM User
Policy
The bucket is ready and we should now be able to push files into it. For this to work, our action should be acting as an AWS user. We need to create this user and give it enough permissions (just enough) to perform this task.
First we need to create the policy. Navigate to IAM in your AWS console, then Policies and create a new one:

Our user will need to be able to see the bucket and its content but also to add and delete objects. You can use the AWS visual editor, or simply click on JSON and paste the following policy (don’t forget to adapt the bucket name):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GitHubTestAppBucketPolicy",
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:PutBucketWebsite",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::testapp.com",
"arn:aws:s3:::testapp.com/*"
]
}
]
}
User
Once your policy is saved, go to Users and create a new one. Give it a name and in the next step chose Attach policies directly and add the policy you just created.


Access key in GitHub
The credentials of the user we just created need to be passed onto the GitHub Action template. We first need to create an access key in AWS. In the user details, go to Security credentials and scroll down to Access keys.

Create a new one for an application running outside AWS.

This will create an access key with an id and a secret.

Copy both. We need to pass these values to our action. We could directly enter them, but they would then be public in our repo. Instead, for security reasons, we will create a GitHub secret. The action will then be able to retrieve the secrets at runtime.
In GitHub, navigate to Settings, then to the secrets for actions.

Create two new secrets, one for the id, one for the key.

Run and test
The final template, as already showed above, looks like this:
name: Action Template Name
on:
push:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node 20.x
uses: actions/setup-node@v1
with:
node-version: '20.x'
- name: Install dependencies
run: npm i
- name: Build
run: npm run build
- name: Archive build
uses: actions/upload-artifact@v1
with:
name: dist
path: dist/testapp
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Download build
uses: actions/download-artifact@v1
with:
name: dist
- name: Set AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_PROD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_PROD_SECRET_ACCESS_KEY }}
aws-region: eu-central-1
- name: Sync files to S3 bucket
run: aws s3 cp dist s3://testapp --recursive
In the second block, still using ubuntu, we first download the artifacts created in the build phase. We then login into AWS user the credentials we saved as GitHub secrets. Finally, we copy the static files into the bucket.
After adapting the values for your application, push the template. Once the action is done running, you should find your static files into your bucket:

If you have correctly setup the bucket and its permissions, you should find a URL under the Static website hosting block:

This is the URL of your website. Click on it to be redirected to it:

That’s it! Of course the URL might not be the one you want to use and it is not currently using HTTPS. Checkout CloudFront and Route 53 for the next steps.