Skip to main content

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:

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 the docker 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 in dstest.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 to kubernetes-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
This page was last reviewed on 26 January 2023. It needs to be reviewed again on 26 April 2023 .
This page was set to be reviewed before 26 April 2023. This might mean the content is out of date.