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.