Harden your Gitlab CI pipeline with DevOps Principals

Lately Docker introduces a rate limit for the pulls from the public repository. If you hit them, you will have to wait for 6h until you can fetch an image again.
The rate limits of 100 container image requests per six hours for anonymous usage, and 200 container image requests per six hours for free Docker accounts are now in effect. Image requests exceeding these limits will be denied until the six hour window elapses.
There might be different strategies to stop fetching images from the official Docker repository. One is to cache the fetched layers. This works quite good and will be done behind the scene. But if you are using dind
in your pipeline this will not work for you. Using dind
will prevent layer from leaking from one build to another.
Photo by Jason Dent on Unsplash
Use your own images
Using your own images from your own registry during the CI run will give you control over the image itself and adds another layer of certainty.
Keep in mind that the docker tag mechanism uses a convention and there is no strict use. Tags can be overridden and therefor might be updated. There are some image maintainer out there, who uses some tags to catch up versions. Something like this:
...:latest
...:3
...:3.8
...:3.8.76
All of those versions have the same sha256
.
|
|
Now if you use a version which might not be too specific, you might get updates automatically but you d not have any control over it. Maybe a faulty version was released and now your pipeline fails with you have changed anything.
So you might use the most specific version possible. But keep in mind the tags are not immutable. If the maintainer decides to update a version you will get it without notice.
To harden your CI you need to keep the image and it’s updates under your control. Only use images from your registry during the build.
Use images from own local repo
To create a clone we will need to:
- download the image from the official docker hub
- upload the image to your own registry
- use your own images on the pipeline
All those steps should be part of a pipeline itself. So you only kick off the pipeline and then a clone is stored on you registry.
For gitlab a pipeline could be like this:
|
|
Here we have three stages in place. First a lint to check if the Dockerfile
is in a good shape, then we test if an image can be created from the information inside the Dockerfile
. The last step deploys the result to our registry.
The steps themselves are in a shared place so that they can be reused in other projects. We will take a look at them next. But before that we need some specific information what makes this pipeline specific for e.g. node
.
REPO_NAME
: will hold the information where the copied image will be stored in our registry.
VERSIONS
: will hold all versions we are interested in.
BASE_IMAGE
: the image from docker hub we are interested in.
Common Information
First we will need to import some common information, which will be used during the steps.
|
|
Here we define some parameter for the pipeline itself. This includes driver information, TLS, Docker Client Version and where the hosted docker hub storage is. The hosted storage in my case is a nexus instance.
Next is something specific when running jobs as DinD
.
|
|
Lint
The linting step might be of little use, but in future we might use the image clone and add/change something to better fit for our needs.
|
|
I’m using the Hadolint image to check my Docekrfile
.
Test
|
|
The script command might look a little bit strange, so let’s break it down in multiple parts.
Here we use a for loop to iterate over all the different versions we would like to create. As a source we take the content of $VERSIONS
and split it by ;
. Remember in the general .gitlab-ci.yaml
we put all version we would like to copy divided by a semicolon, e.g. VERSIONS=14.6;13.6
. Then this information is set to an environment variable and injected into to out Dockerfile
.
We run the test step only during development time and finally on the Merge/Pull-Request.
With this step all we do is checking if the versions are present on docker hub and they can be reached. If we would have some additional commands inside the Dockerfile
, those would also be checked to work correctly.
And when the request is accepted the changes get merged into master.
Deploy to Registry
The next step deploying the images to our local registry.
|
|
In this step we do the same tasks like inside the test step, but we also login into our registry, tag our images and upload those.
The Final Version
Here is the complete file. I normally put each of those steps in a single file and then import the file into the build pipeline. And also some common configurations. By doing this, those steps can easily be reused for other pipelines.
|
|
And here is the final version of the .gitlab-ci.yaml
:
|
|
And here is the Dockerfile
:
|
|
Using Copied Images
Now we can use our copied images inside the build pipeline. This avoids reaching out to the official Docker Hub repo to download the build image. This will the pipeline prevent from failing, when images can not be downloaded from Docker Hub. Also the time to download the image can be reduced, when the hosted registry and the runner are close together. Maybe connected via a switch and not over the internet.
Wrap-Up
You have learned how a build pipeline can benefit from keeping a copy of the image on a self controlled hub. By using those images, the pipeline became protected against unknown changes to the images and/or unreachable resources. Also the download time for he images can be reduced if the connection is moved from an internet based one to a direct connection.