Continuous Deployment of an application using Github Actions
This tutorial will walk through setting up continuous deployment (CD) of a GitHub repository to a namespace on the cloud platform, using GitHub Actions.
Pre-requisites
To work through this guide, you will need:
- A cloud platform namespace
- An ECR in your namespace, to store your docker images
- A serviceaccount in your namespace
If you haven’t done so already, please go through the linked steps to create those resources, and then resume this guide.
Something to deploy
To simulate a real project, we need to:
- build our own docker image, and
- deploy it from our ECR
Once we have our CD system set up, we should be able to merge a PR in our GitHub repository and see the change in our deployed application.
We’re going to use a customised nginx image as our application.
Create a new GitHub repository, and add these files:
index.html
<h1>Hello, World!</h1>
Dockerfile
FROM bitnami/nginx:1.19
COPY index.html /app/index.html
We can build and run this locally:
docker build -t foo .
docker run --rm -p 8080:8080 foo
Now visit http://localhost:8080
and you should see “Hello, World!”
We could achieve the same result using the base image and a ConfigMap, but the point is to have a simple project which has the same kind of build requirements as a “real” project.
Manual deploy
Before we setup our CD system, we’re going to go once through the deployment process manually.
Build the project
docker build -t foo .
Push to the ECR
Set your AWS ECR credentials:
export AWS_ACCESS_KEY_ID=xxxxxx
export AWS_SECRET_ACCESS_KEY=xxxxxx
Authenticate to your image repository following these instructions
Tag and push your image:
docker tag foo 754256621582.dkr.ecr.eu-west-2.amazonaws.com/webops/dstest-ecr:1.0
docker push 754256621582.dkr.ecr.eu-west-2.amazonaws.com/webops/dstest-ecr:1.0
You will need to supply the AWS credentials, and change
webops/dstest-ecr
in thedocker tag
URL above, using the values for your own ECR. Instructions for getting the AWS credentials are here
Deploy to kubernetes
Now that you have a docker image in your ECR, you can launch it in the cluster.
Add this kubernetes-deploy.yaml
file to your GitHub repository, making the following changes:
- Replace the
image:
value with the one for your ECR Replace the
dstest
indstest.apps.live.cloud-platform.service.justice.gov.uk
with a hostname that is unique to your application (e.g. use your own name, or the namespace name)kubernetes-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld-nginx
spec:
replicas: 1
selector:
matchLabels:
app: webserver
template:
metadata:
labels:
app: webserver
spec:
containers:
- name: nginx
image: 754256621582.dkr.ecr.eu-west-2.amazonaws.com/webops/dstest-ecr:1.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx-service
spec:
ports:
- port: 8080
name: http
targetPort: 8080
selector:
app: webserver
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <ingress-name>
namespace: <namespace-name>
annotations:
external-dns.alpha.kubernetes.io/set-identifier: <ingress-name>-<namespace-name>-<colour>
external-dns.alpha.kubernetes.io/aws-weight: "100"
spec:
ingressClassName: default
tls:
- hosts:
- dstest.apps.live.cloud-platform.service.justice.gov.uk
rules:
- host: dstest.apps.live.cloud-platform.service.justice.gov.uk
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: nginx-service
port:
number: 8080
(Note - Please change the ingress-name
and namespace-name
values in the above example. The colour
should be green
for ingress in EKS live
cluster)
Deploy to the cluster like this:
kubectl -n <your namespace> apply -f kubernetes-deploy.yaml
After a few seconds, you should be able to visit the ingress hostname and see “Hello, World!”
Deployment summary
To make a change to our project, we need to:
- update the source code (in this case, by editing
index.html
) - rebuild the docker image
- push it to the ECR with a new tag
- update our kubernetes-deploy.yaml file with the new image tag
- repeat the
kubectl apply
command to update the cluster with our changes
This is the process we’re going to automate using GitHub Actions.
If you’re unclear on any of these steps, try going through the process manually before proceeding.
Automating the deployment process
Build the docker image
We need a GitHub Action that will run whenever we merge a PR.
There’s no dedicated “PR merge” event in GitHub Actions (although you can
detect “PR closed” and use an “if” statement to tell whether it was merged or
not). Instead, we’re going to trigger our action whenever there is a push to
the main
branch, which will happen whenever a PR is merged.
Create this file in your repository (using your namespace name instead of
dstest
):
.github/workflows/cd.yaml
name: Continuous Deployment
on:
workflow_dispatch:
push:
branches:
- 'main'
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: docker build -t foo .
The workflow_dispatch:
line lets us trigger the action via the GitHub web
UI, which makes things easier when you’re developing an action.
The KUBE_CLUSTER
and KUBE_NAMESPACE
Github Actions Secrets are created by
the serviceaccount module.
That takes us as far as building our docker image.
Push to ECR
The GitHub Action needs to push to your ECR, so it’s going to need the URL of the ECR, and AWS credentials.
The cloud platform ECR module will create GitHub Actions Secrets in your repository containing these values.
To set this up, go back to your namespace folder in the environments repository, and edit the resources/ecr.tf
file you created.
Find the line:
# github_repositories = ["my-repo"]
Uncomment it, and change my-repo
to the name of your GitHub repository. So, if your GitHub repository is ministryofjustice/my-cd-test
, you should change the line to:
github_repositories = ["my-cd-test"]
Raise a PR as usual. When the PR has been merged, you should see four GitHub Actions Secrets in your repository (click “Settings” and then “Secrets” on the GitHub web page for your repository):
- ECR_NAME
- ECR_URL
- ECR_AWS_ACCESS_KEY_ID
- ECR_AWS_SECRET_ACCESS_KEY
ECR_NAME is used by the GitHub Action which pushes docker images to the ECR. We need ECR_URL for our deployment manifest
Now add this step to the end of your GitHub Action:
- name: Push to ECR
id: ecr
uses: jwalton/gh-ecr-push@v1
with:
access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
region: eu-west-2
local-image: foo
image: ${{ secrets.ECR_NAME }}:${{ github.sha }}
That takes care of building the docker image and pushing it to the ECR - in this case, tagged with the SHA hash of the last commit to the GitHub repository.
Update the manifest
We need to update the kubernetes manifest so that it deploys the appropriate tag version of the docker image.
We can do this by turning our kubernetes-deploy.yaml
file into a template,
and interpolating the image tag at deployment time.
We’ll use
envsubst
for this. Any other templating system will work, but envsubst is easiest to set
up for this tutorial, because it’s already installed on the GitHub actions
ubuntu-latest
VM.
Rename the file from
kubernetes-deploy.yaml
tokubernetes-deploy.tpl
Change the
image:
line to this:
image: ${ECR_URL}:${IMAGE_TAG}
Then add this step to the end of the GitHub Action:
- name: Update image tag
env:
ECR_URL: ${{ secrets.ECR_URL }}
run: export IMAGE_TAG=${{ github.sha }} && cat kubernetes-deploy.tpl | envsubst > kubernetes-deploy.yaml
At this point, the updated docker image has been built and pushed to the ECR, and the kubernetes manifest has been updated with the correct image tag. Now we need to apply the updated manifest to the kubernetes cluster.
Using your serviceaccount
You applied the manifest to the cluster using kubectl
earlier. This worked
because you are already authenticated to the cluster, but the GitHub Action
isn’t, so we need to provide some credentials and get it to authenticate.
The serviceaccount has permissions to deploy to your namespace, so we will use
its ca.crt
and token
in the pipeline.
Similar to the ECR module, the cloud platform serviceaccount module will create these as GitHub Actions Secrets in your repository, along with the cluster and namespace names.
Back in your namespace folder in the environments repository, make the same
change to resources/serviceaccount.tf
that you made to the resources/ecr.tf
file, i.e. uncomment the github_repositories = ...
line, and supply your
repository name as a one-element list.
Once your change has been merged, you should see four more GitHub Actions Secrets in your repository:
- KUBE_CERT
- KUBE_TOKEN
- KUBE_CLUSTER
- KUBE_NAMESPACE
We need the namespace name in multiple workflow steps, so add it in a top-level env
section of the file:
env:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
Now add this step to the end of the GitHub Action to use the credentials to authenticate to the cluster:
- name: Authenticate to the cluster
env:
KUBE_CLUSTER: ${{ secrets.KUBE_CLUSTER }}
run: |
echo "${{ secrets.KUBE_CERT }}" > ca.crt
kubectl config set-cluster ${KUBE_CLUSTER} --certificate-authority=./ca.crt --server=https://${KUBE_CLUSTER}
kubectl config set-credentials deploy-user --token=${{ secrets.KUBE_TOKEN }}
kubectl config set-context ${KUBE_CLUSTER} --cluster=${KUBE_CLUSTER} --user=deploy-user --namespace=${{ secrets.KUBE_NAMESPACE }}
kubectl config use-context ${KUBE_CLUSTER}
Deploying the latest code
The final step in the GitHub Action is to apply the yaml file like this:
- name: Apply the updated manifest
run: |
kubectl -n ${KUBE_NAMESPACE} apply -f kubernetes-deploy.yaml
Now if you raise and merge a PR making a change to index.html
, you should see
your GitHub Action run and deploy your changes to the cluster.
For reference, your complete GitHub Action cd.yaml
should look like below:
name: Continuous Deployment
on:
workflow_dispatch:
push:
branches:
- 'main'
env:
KUBE_NAMESPACE: ${{ secrets.KUBE_NAMESPACE }}
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: docker build -t foo .
- name: Push to ECR
id: ecr
uses: jwalton/gh-ecr-push@v1
with:
access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
region: eu-west-2
local-image: foo
image: ${{ secrets.ECR_NAME }}:${{ github.sha }}
- name: Update image tag
env:
ECR_URL: ${{ secrets.ECR_URL }}
run: export IMAGE_TAG=${{ github.sha }} && cat kubernetes-deploy.tpl | envsubst > kubernetes-deploy.yaml
- name: Authenticate to the cluster
env:
KUBE_CLUSTER: ${{ secrets.KUBE_CLUSTER }}
run: |
echo "${{ secrets.KUBE_CERT }}" > ca.crt
kubectl config set-cluster ${KUBE_CLUSTER} --certificate-authority=./ca.crt --server=https://${KUBE_CLUSTER}
kubectl config set-credentials deploy-user --token=${{ secrets.KUBE_TOKEN }}
kubectl config set-context ${KUBE_CLUSTER} --cluster=${KUBE_CLUSTER} --user=deploy-user --namespace=${{ secrets.KUBE_NAMESPACE }}
kubectl config use-context ${KUBE_CLUSTER}
- name: Apply the updated manifest
run: |
kubectl -n ${KUBE_NAMESPACE} apply -f kubernetes-deploy.yaml