Determining the best way to automate your CI/CD workflows can be a daunting task. There are so many tools out there that can help you with this, narrowing down the best one for your use case can be difficult. In this post, I will walkthrough how I like to set up my CI/CD workflows using Github Actions. In this post we'll cover a simple implementation of a CI/CD workflow for a Go application using golangs standard testing library and focusing on the Continuous Delivery aspecet of CD. Deployment can vary widely depending on your use case, so we will only covert the testing and delivery of a Go application to a docker registry.
For me, Github Actions provide a lot of flexibility and there are tons of pre-built actions that you can use to help you get started. The best part to me, is the proximity to the code. You can define your workflows in the same repository as your code, which makes it easy to maintain and understand. Being so close to the code allows you to really catch any issues that may arise from changes in the codebase and catch them before they make it into your deployment branch. Among other benefits, Github Actions allow me to maintain controll of everything that goes on in my CI/CD pipeline and not have to worry about things like build containers being public. Github Actions are not without their drawbacks, there are some limitations to the free tier and from time to time the service does go down. However, having a solid break glass plan in place can help mitigate these issues.
To get started with Github Actions, you will need to create a new file in your repository called .github/workflows/ci.yml
. This file will
contain the definition of our CI workflow. The first thing you will need to do is define the name of your workflow and the events that will trigger it. I like for the file name to
match the name of the job so it's eaiser to debug any issues that may arise. In this example, we will use the push event to trigger our workflow. This means that every time we
push to our repository, our workflow will run. Here is an example of what that might look like:
---
name: ci
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: run go tests
run: |
go test -v ./...
In this example, we are defining a workflow called ci
that will run every time we push to our repository. This setp can be further configured to run on specific triggers.
A trigger can be a push, pull request, or a schedule. This workflow contains a single job called build
that will run on the latest version of ubuntu. The job contains a
single step that checks out our code and then runs our go tests. This is a very simple example, but it should give you a good idea of how to get started with Github Actions. You can add as many jobs
and steps as you need to get your workflow up and running.
Continuous Delivery is the practice of automating the delivery of your software to a specific destination, not to be confused with it's cousin continuous deployment. This can be a docker registry, a package
manager, or even a server. In this example, we will focus on delivering our Go application to a docker registry. To do this, we will need to create a new file in our repository called
.github/workflows/cd.yml
. This file will contain the definition of our CD workflow. The first thing you will need to do is define the name of your workflow and the events that
will trigger it. Since we won't want this action to run until the code has at least been tested. We will need to add some kind of logic to In this example to only run when our ci workflow has completed.
We can do this by using the workflow_run
declaration on our workflow. Under workflow_run
we'll set the workflow we want to trigger our CD workflow
and the type of event that will trigger it. In the event of a successful run of the ci
workflow, we will trigger our cd
workflow. We will dig
into our the rest of the job after we see what the file will look like.
---
name: cd
on:
workflow_run:
workflows: ["ci"]
types:
- completed
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/arm/v7
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/go-site-gin:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Now let's focus on the docker-build
job. Here we are setting up our Docker build job and we will use the runs_on
field to tell our action
that we want to run the following steps the latest available version of an ubuntu container. Our first step is to checkout our code. For this step we will use a pre-built action called actions/checkout@v3.
You can kind of think of these pre-built actions as the FROM
section of a Dockerfile. They are pre-built and ready to use. This step requires no configuration, and it's pretty
self-explanitory, it checkout out or code. Next we will login to our Docker Hub account. The docker/login-action@v2
action is another pre-built solution provided by the Docker team.
You could, if you were so incliend to, use the run
field to run the docker login command, but this action is a little more legible this way. Hear,
we will supply our service accounts username and password. The username and token DOCKERHUB_USERNAME
DOCKERHUB_TOKEN
will be stored as a key
value pair in our repository's secrets. This will allow multiple users to reuse the same configuration without having to worry about sharing sensitive information. It is good to note that with things like Amazon's
ECR, you can configure trusted IAM roles to allow your actions to push to your registry without having to use a username and password.
Now that we have logged into our Docker Hub account, let's start builing our container image. First we will need to set up docker/setup-buildx-action@v2 to take some
of the heavy lifting off of us when building and pushing the container. Once we have our buildx environment set up, we will be using it for the remainder of the steps in our continuous delivery workflow. On to
actual delivery part of this workflow. Using docker/build-push-action@v4 the final image. There are a lot of interesting things the
docker/build-push-action
can do for us. For instance, in our Build and push step, we are telling Docker where our Dockerfile is located with teh context declaration. What platforms
we want to build for, in this case this image is for my k3s cluster so I'll need an image based on the arm arichitecture. Confgured in the tags declaration, we are telling Docker where our registry is and what
the final image should be tagged as. In the past, I've run into many issue where the latest tag is not always honored, so I like to use the commit hash as the tag. This has the added benefit of forcing kubernets
to pull the latest image when the deployment is updated, but it also provides a way to tie a specific deployment to a point in time in the codebase. When we get into continuous deployment, we'll see that constantly
updating a semantically versioned tag can be a bit of a pain. As always, it's a bit of a decision you'll have to make for yourself. The rest of the configuration is for caching the build and push steps. This is
comes in handy when you really optimize you container builds and you don't want to have to rebuild the entire container every time you make a change (A topic for later).
This is a very simple example of how to get started with Github Actions. There are many more things you can do with Github Actions, and I encourage you to check out the documentation
and explore some of the configis other developers are using. As discussed above, you can find their exampels in the .github/workflows
directory of their repositories. I hope this post
has been helpful and cleared up somethings about GitHub Actions. Good luck automating your workflows!
Argocd app of apps definition.
Argo Workflows up and running
Automating workflows with github actions.
Simple Api using gin-gonic.
Building K8s manifests with Helm3
A collection of kubectl commands that have helped me a ton.
Configuring Role Based Access Control in Kubernetes.
Making images smaller with multi stage builds.
Keeping secrets out of your container images even while using private modules