This is the first part of a series for Code Pipeline: Deploying a React App in AWS. There's a lot of blogs already around this topic but there's a particular use-case that I want to document in an easy-to-follow guide for DevOps Engineers.
Below are some of the technologies that I will briefly touch on in the entirety of this series (some at a later point): #containers #docker #dockerfile #react #GitHubActions #ecr #ecs #fargate #iam
What am I Doing?
The image above depicts the sequence of steps that we're trying to do here. Taking from a React UI code, we will build a simple pipeline in Github Actions to package it into a container using a Dockerfile and store the package in Amazon ECR (Elastic Container Registry) for future deployment which we'll tackle in the next parts of the series.
Create a React Application
React has a Quick Start guide here to do the first step. In my case, I want to create a React App based on a TypeScript template so I will append --template typescript
to the creation command.
npx create-react-app react-ui --template typescript
This will create a directory called react-ui
in the current folder with an initial project structure of the application. Navigate to the folder and from here, run the app locally on your machine.
cd react-ui
npm start
Now, let's create a Dockerfile
in the same directory and put the following code:
FROM node:20-alpine as build
WORKDIR /app
ENV PATH=/app/node_modules/.bin:$PATH
COPY ./package*.json /app/
RUN npm install
COPY . /app
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
npm install
ensures that all dependencies are installed in the container runtime. As best practice, we can add a .dockerignore
file in the react-ui directory so that the script will explicitly ignore files or directories that are not needed when moving files around. For example, I can add node_modules
in .dockerignore so that it will not be copied in the container being built when I run the copy command.
Build and Tag the Image: From the root directory, run the following command to build and tag the image:
docker build -f Dockerfile -t react-ui:latest .
Then, check the details:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
react-ui latest 63ac15re6a37 1 minute ago 135MB
Test Run the Application: Run the container using the following command:
docker run -it -p 80:3000 --rm react-ui:latest
You can use any port when running the container to route to the exposed port 3000. You can then use this same port when testing the application in a web browser http://localhost:80
Think like a DevOps Engineer: Build a Pipeline for Reusability
Now, everytime a software developer updates the react app code, it doesn't make sense for someone to manually run these commands on their machine to deploy the changes to a production server.
It is recommended to store the code in a repository like GitHub. Then we can make use of a workflow to automatically run scripts ensuring accuracy in deployments.
In my GitHub Workflow, I will trigger a pipeline that has the following steps:
- Checkout the react code from GitHub to the GitHub runner
- Generate a random tag to the package
- Build the image, then push the image to ECR
Checkout the Code from GitHub
The action below is one of the commonly used actions in building a GitHub Actions pipeline. This will simply download the code being built to the GitHub runner that spins up.
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
Generate a Random Tag to the Package
There are several ways on how to uniquely tag a package in ECR. For my use-case, I'd like a simple logic to add a date in the tag so I will add an action to pull the date.
- name: "Get current date"
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d')"
Using this, I can create an environment variable that can be used on other actions with this string, along with another random number such as the github.run_number
.
IMAGE_TAG: ${{ steps.date.outputs.date }}-${{ github.run_number }}
Now, let's slightly modify the Dockerfile
with a few more commands. Assume that the react application now includes packages that needs to be downloaded from 3rd party package repositories. We would need to include an .npmrc
file temporarily to the Dockerfile to allow the image runtime to pull the package with authorization to the package repository.
FROM node:20-alpine AS build
ARG AUTH
ARG EMAIL
WORKDIR /app
ENV PATH=/app/node_modules/.bin:$PATH
COPY ./package*.json /app/
COPY ./.npmrc /app/
RUN echo "//registry.npmjs.org/:_auth=$AUTH" >> .npmrc && \
echo "email = $EMAIL" >> .npmrc && \
echo "always-auth = true" >> .npmrc && \
npm install --force && \
rm -f .npmrc
COPY . /app
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app /app
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Notice here that I added two arguments to pass the authentication at runtime. I will then remove the .npmrc file from the package after the npm install
. I also added another stage to also remove the auth from the docker history (basically doing multi-stage builds as explained here). There are other more modern ways to solve this like using Docker build secrets, details are also explained on the linked blog. Wow, I like saying "also"! 😅
Build the Image and Push it to Amazon ECR
Now that everything is ready let's continue with the pipeline. To further interact with other AWS services from the runner, we will need to authenticate in AWS and also (also?!) login to Amazon ECR.
- name: "Configure AWS credentials"
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE }}
aws-region: us-east-1
role-session-name: ReactDeployment
role-duration-seconds: 3600
role-skip-session-tagging: true
- name: "Login to Amazon ECR"
id: ecr-config
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: true
Now my docker commands will look something like this:
- name: "Build & tag, then push image to Amazon ECR"
env:
ECR_REGISTRY: ${{ steps.ecr-config.outputs.registry }}
ECR_REPOSITORY: ${{ env.IMAGE_NAME }}
IMAGE_TAG: ${{ steps.date.outputs.date }}-${{ github.run_number }}
run: |
docker build -f Dockerfile --build-arg AUTH=${{ secrets.AUTH }} --build-arg EMAIL=${{ secrets.EMAIL }} -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
As a bonus, I added some logic below on how I intend to identify my "latest" image in ECR, which is based on the date that it was pushed:
- name: Get latest image tag
id: get-latest-tag
run: |
image=$(aws ecr describe-images \
--repository-name react-ui \
--output text \
--query 'sort_by(imageDetails,&imagePushedAt)[-1].imageTags[0]')
echo "latest=$image" >> $GITHUB_ENV
That's it! Stay tuned on the next part of this series where I intend to explain one of the options to deploy this image in AWS. See you then!