Setting up CI+CD using GitLab
As demonstrated in the previous tutorial, all tools for creating, running, and
distributing a self-contained environment for the prime checker are available.
However, the manual steps with docker save
and docker load
may perhaps not
be the most convenient, as they do not include a formalized distribution channel
suitable for cyber-physical systems. Luckily, GitLab contains tools that can be
used, both to distribute the Docker images using their Docker registry, but
more importantly to automatically create the Docker image whenever a new version
of the code repository is present. There are other online services to handle
Docker images as well, for example Docker's own Docker hub, but it is, as will
now be demonstrated, quite convenient to combine a code repository with an image
registry.
Use the GitLab container registry
GitLab call their Docker registry feature GitLab container registry even though it is used for distributing Docker images (rather than containers). The first thing to do is to instruct Docker how to log in to the registry by using the following command:
docker login registry.gitlab.com
You will now be prompted for a user name and password, and these are the ones set for GitLab. When logged in, the credentials will be stored with Docker so that you should not need to log in again. Now, the name of any Docker images that should be pushed to GitLab needs to follow a naming scheme including your username and code repository name. Therefore, continue by recreating the Docker image for the prime checker with a new name by running the following from the source folder:
docker build -t registry.gitlab.com/USERNAME/opendlv-logic-primechecker .
Make sure to change USERNAME
into the username you picked at GitLab. Since
this was already built before the process will be very quick, as Docker only
needs to adjust the name of the resulting image. If you are curious, please run
the command docker images
to list all images present in your system. Now it is
time to push the image to the GitLab registry, and this is done with the
following command:
docker push registry.gitlab.com/USERNAME/opendlv-logic-primechecker
Now open the GitLab web page and navigate to the code repository. To the left
there is an option in the menu that says Packages and registries and under
that an option for Container registry. Open this page to see an entry for the
newly pushed image, with the version tag latest
. Note that this is now online
and it can be pushed from any computer by using the following command:
docker pull registry.gitlab.com/USERNAME/opendlv-logic-primechecker
When doing this, you will get a message stating that the image is up to date, since it is already present on the system as you just created it. To fully test the system, first remove the image from your local system with the following command:
docker rmi -f registry.gitlab.com/USERNAME/opendlv-logic-primechecker
This will completely remove the image from your system, where the -f
flag
means force used to remove the image even if it is currently used to spawn
active containers. Now run the above docker pull
command again. This will
automatically save the image into Docker, and there is no more need for
docker save
and docker load
as presented earlier. Also note that since you
created a private repository, the image can only be downloaded if first logging
in using docker login
. If the repository is changed to public, then anyone
could download and use your prime checker. Next, run the pulled image using the
following command:
docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker
Tip! If the image referred to in thedocker run
command does not exist on the system, then Docker will automatically rundocker pull
on the image first. Therefore, it is rare to manually rundocker pull
first.
Automate the full CI+CD chain
It is now time to finalize and automate the whole build and deployment process
by adding a final piece to the puzzle. Currently there is only once manual step
left, and that is to generate the Docker images using docker build
and then
push them to the image registry using docker push
. This can be automated
further by letting GitLab run these commands based on triggers from commits to
the code repository. In particular, two things can be added: (1) to
automatically compile and test the program for every commit that was made to the
repository, and (2) to automatically compiled, test, and push a Docker image to
the registry for every version tag that is assigned to the code repository.
This allows very fine grained control of the build process, where any failed
build or test case would automatically alert all software developers when a bad
code commit was made, and also at the same time allow for a controlled way of
triggering a new Docker image version together with a tag put in the code tree.
GitLab allows activation of these features using a special file called
.gitlab-ci.yml
put in the root of the code repository. Go ahead and open a
terminal and go to the folder opendlv-logic-primechecker
and create the new
file and fill it with the following (gedit .gitlab-ci.yml
):
image: docker
services:
- name: docker:dind
stages:
- build
- deploy
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build-amd64:
tags:
- gitlab-org-docker
stage: build
script:
- docker build .
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
release:
tags:
- gitlab-org-docker
stage: deploy
script:
- docker build -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push .
rules:
- if: $CI_COMMIT_TAG =~ /^[0-9.]+$/
The first lines are not so relevant to explain in detail, but in the section
stages
one can see that the automated build process is divided into two steps,
build
and deploy
. Each stage has a dedicated later section, the first
build-amd64
and the second release
. The section before_scripts
is
activated for both stages and has the sole purpose of login into the Docker
registry using the internally stored username and password (the variables
starting with CI_
are internal GitLab variables used to hide sensitive
information). In the rules
sections the triggers are given, where the first
triggers on a code commit to the default branch (normally main
), and the
second triggers on a commited tag starting with a number (the strange code to
the left is called regular expression and is used to classify text). In the
first section, when a commit is made to the default branch, then the command
docker build .
is executed, and in the second section, when a tag is created,
the command docker build -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push .
is
run that builds and pushes a Docker image to the registry. The tags
section
are not very important now, but they indicate what so called Runners to look
for inside GitLab's internal computational cluster. Now, save and close the
file. Then go ahead and push it to the code repository by using Git as normal:
git status
git add .gitlab-ci.yml
git commit -m "Enabled CI+CD"
git push
When pushed, this commit will directly trigger a build and test of the project as discussed above. Navigate to the GitLab web page and look under CI/CD and then Pipelines to see the running job. You can click on it to get a terminal output from the build, and when everything is done the job will be listed as Passed with green color.
Now, continue by testing the release feature. This is done by creating and pushing a Git tag by using the following commands:
git tag -a 1.0 -m "Version 1.0"
git push origin 1.0
This will create the tag 1.0
locally first, and then the second command pushes
it to the remote code repository where the release sequence is triggered. Again
check under Pipelines to follow the progress of the job, and when it is
completed navigate to the Container registry section to see that there is a
new Docker image created and published. The new image can now be started with
the following command (try in two different terminals at once):
docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker:1.0
It is possible to get an error if running this command, if you happen to use
a computer with a different type of CPU than what is used when building the
Docker image inside GitLab (amd64
). If for example using a computer with an
ARM-based CPU, the program will not work (as the compiled machine code always is
for a specific target). There is however a solution for this, as presented in
the next section.
Adding cross compilation
The CI+CD method as described in the previous section also give great
opportunities for building images targeting different types of computers. This
is referred to as cross compilation as the GitLab server (using an amd64
type
of CPU) builds images that would work on fundamentally different computers.
Docker offers a standardized way of building Docker images for different
computers using its docker buildx
feature (x as in cross compilation). In
fact, the docker buildx
feature does not build a single image that works
with several machines, but instead a series of images sharing the same name but
where only the right sub-variant of the image is sent when the image is
requested. The cross compilation feature can easily be integrated into the
automated build process, so that every time a tag is added all image variants
are prepared. To enable the feature, open the .gitlab-ci.yml
file and change
it to the following (gedit .gitlab-ci.yml
):
image: docker
variables:
PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"
services:
- name: docker:dind
stages:
- build
- deploy
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build-amd64:
tags:
- gitlab-org-docker
stage: build
script:
- docker build .
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
release:
tags:
- gitlab-org-docker
stage: deploy
script:
- docker run --privileged --rm tonistiigi/binfmt --install all
- docker buildx create --name multiplatformbuilder --use
- docker buildx build --platform "$PLATFORMS" -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push .
rules:
- if: $CI_COMMIT_TAG =~ /^[0-9.]+$/
The only difference is the script that is run under the release
section, and
the related variable named $PLATFORMS
. The variable is for convenience and
simply contains all platforms docker buildx
should consider when preparing the
image. In the updated script
section, the first command enables cross
compilation from a tool called QEMU, and the following two lines enables
docker buildx
with the selected platforms. Save and close the file. Now,
create a new release in order to test the cross compilation. Run the following
commands:
git status
git add .gitlab-ci.yml
git commit -m "Enabled cross compilation"
git push
git tag -a 1.1 -m "Version 1.1"
git push origin 1.1
When pushed, this commit will directly trigger a new build and test of the
project, and then when the tag is pushed a new release for version 1.1
is
prepared. Navigate to the GitLab web page and check under Pipelines that
the release is successfully prepared, and then try the new version as (in at
least two terminals):
docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker:1.1