A little dip into CI with Github Actions [Part 2]
A quick recap:⌗
Continuous Integration, or the CI in CI/CD, is the first part of a work style to get constant feedback that what you’re doing is working. Integration is the process of adding new code - patches, upgrades, a new feature - to an existing instance of the product. In a git branching situation, that’s merging (or integrating) new code to the main branch. With automated builds and tests, this is a continuous feedback loop about the state of that code.
The other half of CI/CD is Continuous Delivery (or Continuous Deployment, more on this later!), or the act of deploying the code from the repository; that way you know it works, right? This method is about regularly and frequently putting that code out where it gets seen, used, and tested. Instead of a release cadence where you might see a massive release quarterly or even less frequently, small releases happen weekly or daily or even multiple times per day. This reduces the stakes involved in rolling back a release that doesn’t work as anticipated.
Build Artifacts⌗
Where we left off in the previous post, here, was that we’d made a container image tagged with dev
somewhere in the name. But what happens next?
In brief, here’s what the pipeline does:
- the
dev
image is built - tests are run
- if the tests pass, the image is retagged as
stable
- if not, the pipeline reports failure
This means I can set up some tests that are specific to the deployment on the premise that if they pass, it means the container as a whole is probably working fine.
Functionality Tests⌗
Now, on a simple static website like this, using Hugo to generate a tiny set of files and serve them out of an nginx container, there’s not really a lot to get wrong - mostly if the Hugo build process works, the rest of it will also. But I can prove it by making web requests to the container as if it’s serving traffic, and if they work: happy days.
Security Tests⌗
The other kind of test I can run against my dev
artifact is security testing; because it’s running against the container artifact, this not only tests the application that I’m running in my container, but the base OS for the container itself.
There are many options out there, but I chose to use Trivy, by AquaSec.
While there’s some argument to be had about whether scanning is useful as it only picks up known Common Vulnerabilities and Exposures (CVE), the overwhelming agreement is that scanning for some beats scanning for none.
Here’s the relevant part of the code block and we’ll go through it in more detail.
trivy:
...
the only thing that happens outside of this is logging
into the image repository and downloading the -dev image
...
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/myGithubUser/${{ github.event.repository.name }}:${{ github.sha }}
format: "table"
exit-code: "1"
ignore-unfixed: true
vuln-type: "os,library"
severity: "CRITICAL,HIGH"
And here’s the output of such a job step:
2024-12-16T08:08:48Z INFO Detected OS family="alpine" version="3.20.3"
2024-12-16T08:08:48Z INFO [alpine] Detecting vulnerabilities... os_version="3.20" repository="3.20" pkg_num=18
2024-12-16T08:08:48Z INFO Number of language-specific files num=0
ghcr.io/myGHCRRepository/myBlog:<commitSHA> (alpine 3.20.3)
================================================================================
Total: 0 (HIGH: 0, CRITICAL: 0)
Now, looking at the options I have chosen, it might look like it’s a bit of a light touch - but realistically, I’m not about to not deploy my blog because the nginx-alpine image has a warning in the INFO category that I can’t change, and there’s no point not deploying something that isn’t fixable… What’s deployed already is probably also not fixed.
So, it’s a medium-to-low bar to get over, but if the test pass I can be quite at ease that there are no glaring errors.
Amusingly this did previously pick up a high level vulnerability for the alpine image, which was mostly annoying, but meant that I ultimately figuring out some Dockerfile tweaks to make the image update at build time, rather than waiting until the public alpine image was updated to include the fix.
Retagging⌗
The final piece is linking together these jobs, assuming successful outputs of these previous steps, and making a stable
or production
image.
push:
name: Push stable version
runs-on: ubuntu-latest
needs:
- build
- vulnerability_scan
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: USERNAME
password: PASSWORD
- name: Get stable tag
id: tag
run: |
echo "tag=stable-${{ github.sha }}-$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Pull dev image
run: docker pull ghcr.io/myGithubUser/${{ github.event.repository.name }}:${{ github.sha }}
- name: Docker retag
run: docker tag ghcr.io/myGithubUser/${{ github.event.repository.name }}:${{ github.sha }} ghcr.io/myGithubUser/${{ github.event.repository.name }}:${{ steps.tag.outputs.tag }}
- name: Docker push
run: docker push ghcr.io/myGithubUser/${{ github.event.repository.name }}:${{ steps.tag.outputs.tag }}
A quick note: for users of AWS’s container registries, ECR, there’s actually a way to update an existing artifact tag. Everyone else has to pull, tag, and push, the artifact each time.
This is all fairly simple and self-explanatory. We:
- require that the previous jobs have completed successfully with the
needs
imperative - log into the container registry
- work out the new tag that we’ll apply to the artifact e.g.
stable
- pull the testing, or
dev
, artifact - tag the artifact with the new tag,
stable
- push the artifact to the repository
Ta-da, we now have automated artifact promotion with testing.
This artifact can now be used for deployment - however that looks to you.
Next time:⌗
I’ll outline how FluxCD is the Magic Sauce that makes so much of my automation blissfully simple.