Automating Deployments with GitHub Actions

3/24/20224 min read

When writing my last blog post, I really wanted to share a draft of it (and not just the source code) before publishing. To do that, deployed my standard helm template to its own namespace and, after showing it, removed the namespace. However, I had to hand-craft those scripts, which meant the results weren't reproducible. I already wasn't really satisfied with how my publishing script required a manual run, and didn't ensure it matched main. I wanted a better solution.

The first step was to set up a deployment for main. Since this blog is hosted on GitHub, GitHub Actions seem to be the best option. This action, triggered by any push to main, would need to build a docker image, publish it to the container registry, and update my helm template.

First, the trigger:

on:
  push:
    branches:
      - main

Logging in to the container registry was a bit tricky, but following the guide by Microsoft for Azure GitHub Actions I was able to create the secrets. You can see my full deploy script on GitHub, but the login scripts included:

- name: 'Log in to docker registry'
	uses: azure/docker-login@v1
	with:
	login-server: ${{ env.registryLoginServer }}
	username: ${{ secrets.REGISTRY_USERNAME }}
	password: ${{ secrets.REGISTRY_PASSWORD }}
- uses: azure/login@v1
	with:
	creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/aks-set-context@v2.0
	with:
	cluster-name: ${{ env.azClusterName }}
	resource-group: ${{ env.azClusterResourceGroup }}

The docker build is standard and a bit boring, as is the helm deploy command for the main build. However... I wasn't done yet. I really wanted to be able to preview branches as they show on the site itself. (I'd already run into one bug with how Next.js loads new URLs when using next export with a standard static files server...) I needed a couple more pipleines.

This time, since I wanted to preview a PR, I needed a PR trigger:

on:
  pull_request:
    types: [opened, reopened, synchronize, ready_for_review]

We can reuse most of the yaml from our original main build, but the helm deployment becomes a bit different. Since I wanted a per-PR site to preview, I needed to create a new helm release, and I didn't want it to collide with anything else. Setting up a new namespace in my Kubernetes cluster seems like the way to go!

- name: 'Deploy'
  run: |
    helm upgrade --install \
      -n ${{ env.k8sNamespacePrefix }}${{ github.event.pull_request.number }} $releaseName --create-namespace \
      --repo https://mdekrey.github.io/helm-charts single-container \
      --set-string "image.repository=$registryLoginServer/$imageName" \
      --set-string "image.tag=${{ github.sha }}" \
      --set-string "ingress.hosts[0].host=pr-${{ github.event.pull_request.number }}.dekrey.net" \
      --values ./deployment/values.yaml

This looked pretty good, though I really wanted to notify when the site was available. I found a ton of published actions that allow you to easily add a comment to the PR, but I'd rather use something official and definitely maintained. Eventually I found the official github-script repository, and they had a pretty basic example that almost did what I needed. The final result looks like this:

- name: Report PR url
  uses: actions/github-script@v6
  with:
    script: |
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: 'Branch ready for preview at https://pr-${{ github.event.pull_request.number }}.dekrey.net'
      })

With that, we're almost done! But, when creating new resources, we want to always clean up after ourselves. On the PR closed (whether merged or not), we want to be sure to delete both the helm resource and the kubernetes namespace.

on:
  pull_request:
    types: [closed]

Again, we'll use the same login scripts as before, but this time, we'll make sure to issue the delete commands.

- name: 'Clean up Kubernetes'
  run: |
    helm delete -n ${{ env.k8sNamespacePrefix }}${{ github.event.pull_request.number }} ${{ env.releaseName }} --wait
    kubectl delete ns ${{ env.k8sNamespacePrefix }}${{ github.event.pull_request.number }}

And that's it! Now I have a site that not only builds my PRs, but deploys a testing branch so I can preview it and really make sure it works on the server before hitting that merge button.