The sample solutions proposed during the workshop were created to demonstrate how to create complex DevOps pipelines. During the workshop, you’ll also create your own, simplified version of this process. We’re focusing on microservices here because working with them is significantly more complex than working with a monolith.

Everyone who is a part of the project development team should find something interesting here do not matter if a main focus of this person is development, qa, or operations.

Agenda

  • Start 10:00

  • Section 1:

    • Architecture Overview

    • Gitlab Overview

    • Frontend Service Pipeline Overview

    • Backend Services Pipelines Overview

    • Gitlab Container Registry Overview

    • Gitlab Pages Overview

    • Gitlab CI Overview

  • Coffee break 11:00 - 11:15

  • Section 2:

    • Your first pipeline (practice + explanation)

    • A complex pipeline (practice + explanation)

  • Coffee break 12:10 - 12:25

  • Section 3:

    • DevOps pipeline for our frontend microservice (demo)

    • DevOps pipeline for our frontend microservice (explanation)

  • Section 4:

    • DevOps pipeline for our backend microservices (demo)

    • DevOps pipeline for our backend microservices (explanation)

  • Lunch 13:45 - 14:15

  • Section 5:

    • Component use training (optional)

  • Coffee break 15:45 - 16:00

  • Section 6:

    • What’s Next

    • Cleanup

    • Q&A

  • Ends between 16:15 and 16:30 - depends on Q&A session

Requirements

For this Workshop you will be working as a DevOps automator this role can be granted to anyone in the team. In most of the cases or a Developer is chosen or a QA automator but in some cases this role can be given to the Cloud Infrastructure Engineer.

Because we will be working on FrontEnd example during this workshop and also focusing on the local development part of it then our requirements are the same as for this example project local development requirements:

  • Languages

    • JavaScript (programming language used for our example project)

    • Ruby (documentation)

    • Python (automation and mocks)

  • Services

    • Docker CLI, Docker Engine / Colima / Podman, Docker Compose / Podman Compose, Docker BuildX (containers)

  • Tools

    • Brew (package management)

    • Ansible (configuration management)

    • docker-compose (container management)

    • kubectl (kubernetes cli)

    • kubectx (kubernetes cli extension)

    • helm (kubernetes package management)

    • k3d (lightweight kubernetes)

    • k9s (kubernetes management tool for cli)

    • gitlab-runner to prepare Gitlab CI executor

You can find how to install this software here: https://local-workshops-requirements-guide-b9c7de.gitlab.io

Windows users should use WSL 2, or install VirtualBox, and install linux system in the VM.

Gitlab CI required runners to work. The current version of this workshop requires two runners working on your laptop/desktop/server - you can decide where yourself. First one which will be a default runner and working for untagged jobs will be a Kubernetes based runner. There is a special guide showing how to configure it here: https://gitlab-runner-k3d-guide-1aca27.gitlab.io. Second runner will be a shell runner and in our case this will be bash runner - this one will be configured for jobs I prefer to use it in our example.

If you like to check overview of the pipeline created by Gitlab itself you can find it here: CI Overview v1, but unfortunately it was created at 2020, and I do not see never version of it. A lot changed, and we will focus on the new additions to the process.

You can also do a short Gitlab training by creating a simple Python project using Gitlab Infrastructure prepared by us Python GitLab CI-CD development - guide prior this one or after this one.

Why those requirements

Like mentioned in this workshop you need all of this software installed but in the real life not.

JavaScript programing language will be used normally only by Developers. Testers and Infrastructure in most of the cases do not need it to be installed.

Ruby programing language in our case also will be used normally only by Developers and in this example they do not need to be Ruby programmers - tool we are using here for the documentation creation requires it. We expect developers to be responsible for documentation creation and maintenance.

Python programing language is used here by a Developers and QA. It will be easy to remove this need for the Developers just by serving doc not with using python but with using ruby. QA will need this language to work with our MOCK examples.

Brew tool is required in our setup only if you will choose this tool to install almost all required software.

Ansible is the configuration management tool will be used by all groups as our flow is based on it.

Docker Engine - For Linux and Windows users (WSL2) Docker Cli is part of the Docker package. Mac users needs to install a replacement like for example Colima. It is used by the Docker CLI to physically manage containers.

Colima - This is low weight Container Engine compatible with Docker Cli. In our case Mac users using it instead of the Docker Engine which currently is available on Mac only through paid Docker Desktop.

Podman - Podman is an open source container, pod, and container image management engine. Podman makes it easy to find, run, build, and share containers.

Docker Compose / Podman Compose - Easy to use simple orchestration tool for managing and configuring multiple containers.

Docker BuildX - This is a Docker CLI plugin for extended build capabilities with BuildKit.

All those tools:

kubectl - Kubernetes CLI will be used to check if our cluster works

kubectx - CLI tool extending kubeclt is used by our local deployment script to be sure that proper cluster and namespace is used.

helm - Package manager for Kubernetes used to deploy applications to our kubernetes cluster.

k3d - CLI wrapper for the K3s clusters which we will use to create out local kubernetes compatible K3s cluster

k9s - terminal based UI to interact with Kubernetes clusters

will be used mostly by the Infrastructure but also in many times by Developers and QA when working with Continuous Deployment.

Overview

Architecture

Our target backend architecture will be looking like this:

Backend Architecture
Figure 1: Backend Architecture

We have three backend microservices prepared which we will be used by our deployed application:

Locally for a simplicity we will be using Mocked version of the Backend infrastructure. In this particular case we have mockup service which we created ourselves but there are automated solution for this in the market.

In our example we have BFF Service which depends on API-1 and API-2. Like we can see below when using mocks our architecture can be simplified a lot:

Mocked Architecture
Figure 2: Mocked Architecture

As you can see this will help our teams to test and work on services which depends on other services. With this design you do not need to recreate full architecture with all dependencies to just work.

Our mockup services are created using Python[1] language. In addition, for maximum simplicity we are using also Flask[2] micro framework.

We will be using those mocked versions of our services also in our E2E tests.

Test doubles: Mocks, Stubs, and Fakes
For simplicity we are naming our Stubs as Mocks as this name is in most of the cases used for Fakes and Stubs all the time.

Fake: Implements a shortcut to the external service, for example in memory database instead real database. This method of mocking requires changes in the code.

Stub: It is a hard coded version of the external service and our examples are in the reality Stubs.

Mock: We use mocks only to verify that access was made. Mocks do not hold data.

In the end our full architecture on the staging and production environment looks like this:

Target Architecture
Figure 3: Target System Architecture

In the upper target system which will be available on the staging and production environments our frontend is talking through the Ingres with our BFF Service which is operating in the fully working environment. We will be deploying our services with the help of the CI/CD Pipeline to those environments.

Locally we will be working with the mocked version of this architecture which will be looking like this:

Mocked Architecture
Figure 4: Local Mocked System Architecture

As you can see, microservices-based architecture poses many challenges in the CI/CD process, which we will attempt to address during this workshop. Some of these challenges haven’t been addressed in my workshops to date, as they require a specialized testing platform due to their complexity.

If possible, before starting a project, try asking your architects WHY they need microservices in the first place.

Gitlab

Each project in the Gitlab system have a Project menu:

Gitlab - project menu
Figure 1: Gitlab - project menu

This menu is giving us access to all Gitlab features which are project related.

Pinned

We have a Pinned section which is configurable and have shortcuts to the feature which normally are the most used. By default it is Issues and Merge requests.

Manage

We have Manage section where we have these features:

  • Activity

  • Members

  • Labels

Plan

We have Plan section where we have these features:

  • Issues

  • Issue boards

  • Milestones

  • Wiki

Code

We have Code section where we have these features:

  • Merge requests

  • Repository

  • Branches

  • Commits

  • Tags

  • Repository graph

  • Compare revisions

  • Snippets

Build

We have Build section which looks like this:

Gitlab - build section
Figure 1: Gitlab - build section

and as you can see have these features:

  • Pipelines

  • Jobs

  • Pipeline editor

  • Pipeline schedules

  • Artifacts

Because we will be strongly focusing on this section during this workshops let’s focus a little on it.

Pipelines

We have here a list of our pipelines which are connected to this project. We can easily jom from it to the particular pipeline and job connected to the pipeline

Jobs

This is a list of jobs which were: passed, manual (waiting for run action), running, pending, and failed. This view is only useful if you do not know why your pipeline is still waiting for something or just investigate by job.

Pipeline editor

You can use build in pipeline editor which will help you with creating your pipeline. Because Gitlab pipelines are created with a use of the YAML files it is not required to use it but it’s features can help you with more complex pipelines like we will see during this workshop.

Pipeline schedules

Where we can schedule our periodic pipeline.

Artifacts

Where we can easily browse artifacts created by our pipelines.

Secure

We have Secure section where we have these features:

  • Security capabilities

  • Audit events

  • Security configuration

Those features are available mostly in the paid plans of the Gitlab system.

Deploy

We have Deploy section which looks like this:

Gitlab - deploy section
Figure 2: Gitlab - deploy section

and as you can see have these features:

  • Releases

  • Feature flags

  • Package Registry

  • Container Registry

  • Pages

We will be focusing on this section later as some of those features we are using in the our Continuous Delivery and Deployment process.

Operate

We have Operate section which looks like this:

Gitlab - operate section
Figure 3: Gitlab - operate section

and as you can see have these features:

  • Environments

  • Kubernetes clusters

  • Terraform states

  • Terraform modules

  • Google Cloud

but we will only look at Environments feature during those workshop.

Monitor

We have Monitor section where we have these features:

  • Error tracking

  • Alerts

  • Incidents

  • Service Desk

Analyze

We have Analyze section where we have these features:

  • Value Stream Analytics

  • Contributor statistics

  • CI/CD Analytics

  • Repository Analytics

  • Model experiments

Settings

We have Settings section which looks like this:

Gitlab - settings section
Figure 4: Gitlab - settings section

and as you can see have these features:

  • General

  • Integrations

  • Webhooks

  • Access Tokens

  • Repository

  • Merge requests

  • Ci/CD

  • Packages and registries settings

  • Monitor

  • Usage Quotas

We will be only focusing on CI/CD configuration feature.

Frontend Service Pipeline Overview

Because we will be focusing on Book Frontend service we will focus on this service pipeline.

To work with pipelines in the Gitlab we first need to open a Build section and then choose a Pipelines option.

In our example link to this option will be: Book Frontend - Pipelines

And we will see our pipelines with a short text and graphical description.

Book Frontend - pipelines
Figure 2: Book Frontend - pipelines

In the upper case we can see that all our stages of the visible pipelines are green and those pipelines passed.

In this page we can also Run pipeline manually and do many various actions we normally need like by example retrying our pipelines and of course check details of them.

Let’s do a fast look on one of those pipelines:

Book Frontend - pipeline
Figure 3: Book Frontend - pipeline

In our example link to this pipeline will be: Book Frontend - Pipeline

In this view we can see stages our pipeline have and all jobs which are a part of these stages.

In our case those stages are:

  • configure

  • pre-build-tests

  • build

  • post-build-tests

  • release

  • deploy

  • .post

Lets focus a little on them now.

Configure

Let us focus first on the configure stage visible below:

Book Frontend - pipeline - configure
Figure 4: Book Frontend - pipeline - configure

In this stage we have fwo jobs:

  • First one is also called configure in our case we are generating configuration for the CI and also for the deployment to the staging and production environments which are Kubernetes based.

  • Second section will download all required packages as we will be reusing them in the other jobs.

Pre Build Tests

In our example we have linters. We are using them to check if code is written properly.

Let us look on the format stage now:

Book Frontend - pipeline - Pre build tests
Figure 5: Book Frontend - pipeline - format

In this stage we are focusing on the two:

  • code format with Prettier

  • static analysis with ESLint

Those tests are normally very fast and can fail before we even think about build our application which will save our time.

Build

Let look how this stage look as it is a little unusual:

Book Frontend - pipeline - build
Figure 6: Book Frontend - pipeline - build

We have three jobs here

  • build for node 26.1.0

  • build for latest node

  • generate

  • test_image

First, and second one is an example which shows how to just build projects which will give us artifacts with a result of the build for a various versions of node with them we can check if our app can be built for a different versions of Node.js.

Third one is building project documentation for a developers.

Forth one is building test docker image which we will be using later. Instead of the standard artifacts in this example we have a docker image which is stored in the Gitlab docker registry.

Post Build Tests

Let us look on the post-build tests stage now:

Book Frontend - pipeline - Post build tests
Figure 7: Book Frontend - pipeline - post build tests

We have two jobs here:

  • coverage-latest

  • scan

First job is running standard testing and will generate coverage report for us.

Second job will do a security scan of the test docker image.

Release

This stage is a part of the Continuous Delivery process

Let us do a first look at it:

Book Frontend - pipeline - release
Figure 8: Book Frontend - pipeline - release

We have three jobs here

  • gitlab_registry_latest

  • pages

  • upload-artifact-registry

This stage is assuming that all our tests passed, and now we are preparing rest of the artifacts required for the deployment to our Kubernetes or Container based environments.

First one is preparing our image to be used by us in the local development process by copying/creating[3] our docker image to the Gitlab Registry.

Second one will start release process of our build documentation to the Gitlab Pages service.

Third one is preparing our image to be used in the Google Kubernetes engine or Google Cloud Run by copying/creating[4] our docker image to the Google Artifact Registry service.

Deploy

This stage is a part of the Continuous Deployment process

Currently, we do not have new version of the GKE or Cloud Run prepared and we will show only pages deployment part. I hope this will be fixed soon

Let us do a first look at it:

Book Frontend - pipeline - deploy
Figure 9: Book Frontend - pipeline - deploy

We have one job here

  • pages:deploy

First one is scheduled and ran automatically by Gitlab CI as the release job pages is scheduling it. This one when will end will create/update documentation using Gitlab Pages service. This documentation for the Book Frontend service will be available here: Book Frontend documentation.

Post

This stage is a special stage which will start when pipeline ends

Book Frontend - pipeline - post
Figure 10: Book Frontend - pipeline - post

In case of our frontend application we have one job here

  • remove_test_image

This job is started when pipeline will end and do not matter id pipeline will end with success or not. This job will try to remove test image and will end with success if test image was existing and remove will end with success or will end with warning if test image was not existing or was not possible to remove.

Backend Services Pipelines Overview

All our Backends are using the same template for the pipeline because are written in the same language and are using the same framework.

This pipeline looks like this:

Backend - pipelines
Figure 1: Backends - pipelines

As you can see this pipeline looks very similar to the pipeline we are using for our frontend service.

The same as for the frontend service we have sections with the same names an many jobs look exactly the same. We will see soon why when we will focus on the concept of the shared templates and jobs.

Gitlab Container Registry

We will be using Gitlab Container Registry feature to store our docker images.

Each of our example projects will be deployed to the Kubernetes cluster, or Container based environment - and also used locally by us, and our team members.

For example, you can check images created by Book Frontend CI located in this project Container Registry which is located in the Deploy menu section.

You will see that in our case we have two subsections there: - book-frontend where we have our standard images and for example our latest image registry.gitlab.com/mobica-workshops/examples/js/vuejs/book-frontend:latest - book-frontend/test where we have test images. This one should be empty when all pipelines ends as those images are deleted in the post action you saw in the previous section.

Gitlab Pages

We will be using Gitlab Pages feature to deploy our project technical documentation.

Each of our example projects have technical documentation created with a use of SSG (Static Site Generator) software. We are using asciidoctor in our projects byt there are better ones to do this like for example Hugo[5] which is used by example to create Kubernetes documentation.

For example, you can check link to the Documentation we are creating using our pipeline for the Book Frontend service in the Pages subsection in the Deploy section.

In our case documentation link looks like this: https://book-frontend-11cecb.gitlab.io. GitLab allows you to configure domain for this page.

Gitlab CI

Before we start our first hand ons let’s talk a little about Gitlab CI.

Gitlab CI is fully integrated with the Gitlab system, the same as other gitlab features. Like was mentioned only external part of the Gitlab CI are Gitlab Runners. In the Requirements section you will find how to create own runners.

To start using GitLab CI/CD in your project you will need to create a .gitlab-ci.yml file at the root of your project. This file follows the YAML format and has its own special syntax which can be found here. We will use a lot of features mentioned there but of course not all.

Gitlab CI can be used for a projects which have multiple repositories multi-repo but also for the monorepo type of projects. We will focus on multi-repo during this workshop.

Gitlab CI have also own subsection in the Settings section.

It is possible to configure runners per server, group and project.

Official Gitlab Trainings

Your first pipeline

A First pipeline tutorial is available here:

If you will any questions please contact Me. This tutorial should take not more than between 5 and 10 min of your time.

Explanation

Please read tips section of the official tutorial first. Most of my explanations here will be just a duplicate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
build-job: (1)
  stage: build (2)
  script: (3)
    - echo "Hello, $GITLAB_USER_LOGIN!"

test-job1:
  stage: test
  script:
    - echo "This job tests something"

test-job2:
  stage: test
  script:
    - echo "This job tests something, but takes more time than test-job1."
    - echo "After the echo commands complete, it runs the sleep command for 20 seconds"
    - echo "which simulates a test that runs 20 seconds longer than test-job1"
    - sleep 20

deploy-prod:
  stage: deploy
  script:
    - echo "This job deploys something from the $CI_COMMIT_BRANCH branch."
  environment: production (4)
1 Name of the job will be build-job
2 We can group our jobs in stages. Jobs in the same stage normally are ran in parallel. There can be other configuration in the Gitlab CI file or on runner which will not made this possible.
3 Script section is a place where we are executing our commands on the runner. Folder we are in is a root folder of our repository.
4 We can inform gitlab which environment was used for a deployment.

This super easy example shows a core of the Gitlab CI system. You will be extending this more and more depends on your needs, and we will show you during those workshops a preview of how much elastic this system is.

A complex pipeline

Before you start working on this tutorial I advise you to add .gitignore file which is not mentioned, and ignore at least node_modules/ folder before adding anything to the git repository.

Now, please do a next a little more complex training which is available on the official Gitlab page here:

If you will any questions please contact Me. This tutorial should take not more than between 10 and 20 min of your time.

Explanation

This pipeline is really nicely explain during the tutorial.

Let’s just focus on what this tutorials show.

Because this example is using deployment of a documentation to the Gitlab Pages service we can see a complete pipeline which have here 3 stages:

  • Build stage where we are building our documentation and creating artifacts which will be used for a deployment.

  • Test stage were we are testing our code.

  • Deploy stage which is deploying our documentation to the Gitlab Pages service.

DevOps pipeline for our microservices

Frontend microservice Gitlab CI pipeline

TODO: Video with demo

Now Let’s start with the .gitlab-ci.yml file which we have in the Book Frontend repository

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Version: 6.0
variables: (1)
  FF_NETWORK_PER_BUILD: "true"
  APP_NAME: book-frontend (2)

stages: (3)
  - configure
  - format
  - build
  - test
  - security
  - release

include: (4)
  - component: gitlab.com/devops-training-info/templates/ci/components/ansible-component/configure@1.0.0
    inputs:
      stage: configure
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/install@1.1.0
    inputs:
      stage: configure
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/format@1.1.0
    inputs:
      stage: format
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/lint@1.1.0
    inputs:
      stage: format
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/build@1.1.0
    inputs:
      stage: build
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/build@1.1.0
    inputs:
      stage: build
      node_version: "26.1.0"
  - component:  gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/generate@1.0.0
    inputs:
      stage: build
      ruby_image: ruby:3.4
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/build@1.0.2
    inputs:
      stage: build
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/coverage@1.1.0
    inputs:
      stage: test
  - component: gitlab.com/devops-training-info/templates/ci/components/security-component/trivy@1.0.0
    inputs:
      stage: test
  - component: gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/pages@1.0.0
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/release@1.0.2
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/clean@1.0.2
1 environment variable which can be used in the pipeline jobs
2 this variable will be used by one of our components and shows that we still have a lot to do as we should give it as an input later on
3 pipeline stages which will exists in our pipeline and cut it into stages
4 we can include in our YAML files - you can find a lot of examples here: https://docs.gitlab.com/ci/yaml/includes/. We plan to use this feature for a Component based flow - you can read more about it here: https://docs.gitlab.com/ci/components/. We will be focusing a lot on this flow during this workshop.

Component based flow we will be using in our workshop uses special component projects stored in the separate repositories. Those component projects should be created in a way which will use CI for testing them. Our examples are using this testing and have own CI pipelines. This flow also allows easy to understand way to version components we are using to mitigate issue when older version of the application no longer have working CI.

Configure stage

In this stage we are including two component templates:

1
2
3
4
5
6
7
8
9
# ...
include:
  - component: gitlab.com/devops-training-info/templates/ci/components/ansible-component/configure@1.0.0 (1)
    inputs: (2)
      stage: configure (3)
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/install@1.1.0
    inputs:
      stage: configure
# ...
1 We are giving a path to the template in a format which this GitLab feature expects. In this example this is component project: gitlab.com/devops-training-info/templates/ci/components/ansible-component, template: configure and tag: 1.0.0
2 We are setting also inputs which will be used by this template
3 Input we are giving is stage and value we are giving is configure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spec: (1)
  inputs:
    stage: (2)
      default: configure
      description: "Defines the build stage"
    ansible_image:
      default: alpine/ansible
      description: "Defines image with ansible to use"
    shell_script:
      default: "./scripts/gitlab-configure.sh"
      description: "Defines location of prepared shell script which should configure and run ansible playbook"
    ansible_artifacts_paths:
      type: array
      default:
        - secrets
      description: "Defines locations of generated artifacts"
---

configure: (3)
  image: $[[ inputs.ansible_image ]]
  stage: $[[ inputs.stage ]]
  before_script:
    - ansible --version
  script:
    - $[[ inputs.shell_script ]] (4)
  artifacts:
    expire_in: 1 week
    paths: $[[ inputs.ansible_artifacts_paths ]] (5)
1 This is a spec configuration which in our case are inputs.
2 We are using this input in our example just to show how to use it. Our example uses exactly the same value as is a default value here.
3 This is a job which this template will give us and will be run in our example.
4 We are also using a shell script which is stored with a repository and is maintained by a team working on the project pipeline currently working at - with this template we can give its name with using input shell_script.
5 In our case we are generating configuration files in the secrets folder. We are storing this as artifacts in the repository. Normally you will not have staging and production secrets store this way but for simplicity we are also storing secrets in those files.
1
2
3
4
5
6
7
8
stages:
  - configure

include:
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/configure@$CI_COMMIT_SHA (1)
    inputs:
      stage: configure
      ansible_image: alpine/ansible
1 each commit of this template is tested
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
spec:
  inputs:
    stage:
      default: 'configure'
      description: 'Defines the format stage'
    node_version:
      default: 'latest'
      description: 'Node image version tag'
---
"install-$[[ inputs.node_version ]]": (1)
  image: registry.gitlab.com/devops-training-info/examples/docker/node-yarn:$[[ inputs.node_version ]] (2)
  stage: $[[ inputs.stage ]]
  script:
    - yarn install (3)
  artifacts: (4)
    expire_in: 1 week
    paths:
      - node_modules
1 Name of the job can be modified with a input. With this we can for example have a different job name for a different node version.
2 Unfortunately yarn is no longer added by default to the node image that is why we are currently using our own modified node image https://gitlab.com/devops-training-info/examples/docker/node-yarn/-/blob/master/Dockerfile?ref_type=heads. Because now we have classic yarn and modern yarn - most probably I should just migrate from yarn to the standard node with my examples ;).
3 We will install all dependencies
4 We will store installed dependencies as artifacts for 1 week and use them in all other jobs which will depend on this one.

Format stage

In this stage we are including two component templates:

# ...
include:
  # ...
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/format@1.1.0
    inputs:
      stage: format
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/lint@1.1.0
    inputs:
      stage: format
# ...

As you can see one component can have many templates.

First template looks like this:

spec:
  inputs:
    stage:
      default: 'pre-build-tests'
      description: 'Defines the format stage'
    node_version:
      default: 'latest'
      description: 'Node image version tag'
---
"format-$[[ inputs.node_version ]]":
  image: registry.gitlab.com/devops-training-info/examples/docker/node-yarn:$[[ inputs.node_version ]]
  stage: $[[ inputs.stage ]]
  script:
    - yarn test:format

Second template looks like this:

spec:
  inputs:
    stage:
      default: 'pre-build-tests'
      description: 'Defines the format stage'
    node_version:
      default: 'latest'
      description: 'Node image version tag'
---
"lint-$[[ inputs.node_version ]]":
  image: registry.gitlab.com/devops-training-info/examples/docker/node-yarn:$[[ inputs.node_version ]]
  stage: $[[ inputs.stage ]]
  script:
    - yarn test:lint

This stage is focusing on code quality

Build stage

In this stage we are including four component templates:

# ...
include:
  # ...
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/build@1.1.0
    inputs:
      stage: build
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/build@1.1.0
    inputs:
      stage: build
      node_version: "26.1.0"
  - component:  gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/generate@1.0.0
    inputs:
      stage: build
      ruby_image: ruby:3.4
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/build@1.0.2
    inputs:
      stage: build
# ...

In this stage we are introducing two new components. One is an asciidoctor component and second one is a container component.

This time just let’s focus only on the container component as the other three are not giving us anything new.

We are using build template of this component which looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spec:
  inputs:
    stage:
      default: 'build'
      description: 'Defines the build stage'
    docker_file:
      default: 'multi-stage.Dockerfile'
      description: 'Name of the Dockerfile to use for Container creation'
    test_image:
      default: ${CI_REGISTRY_IMAGE}/test:${CI_COMMIT_SHA}_${CI_PIPELINE_ID} (1)
      description: 'Name of the target container test image'
---

test_image:
  stage: $[[ inputs.stage ]]
  variables:
    DOCKER_FILE: $[[ inputs.docker_file ]]
    CONTAINER_TEST_IMAGE: $[[ inputs.test_image ]]
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/${DOCKER_FILE}" --destination "${CONTAINER_TEST_IMAGE}"
  after_script: (2)
    # If we try to pull the image immediately it sometimes fails
    - /bin/sleep 5
1 In the spec inputs default values we can use Gitlab variables as is visible here
2 We are using here after_script to give some time to the pipeline to be really finished as the last part of the storing container in the Gitlab Registry is async.

Test stage

In this stage we are including two component templates:

# ...
include:
  # ...
  - component: gitlab.com/devops-training-info/templates/ci/components/js-component/coverage@1.1.0
    inputs:
      stage: test
  - component: gitlab.com/devops-training-info/templates/ci/components/security-component/trivy@1.0.0
    inputs:
      stage: test
# ...

Let’s check our coverage template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
spec:
  inputs:
    stage:
      default: 'pre-build-tests'
      description: 'Defines the format stage'
    node_version:
      default: 'latest'
      description: 'Node image version tag'
---
"coverage-$[[ inputs.node_version ]]":
  image: registry.gitlab.com/devops-training-info/examples/docker/node-yarn:$[[ inputs.node_version ]]
  stage: $[[ inputs.stage ]]
  script:
    - yarn test:coverage
  artifacts:
    reports: (1)
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
1 This one will end with generating coverage report. In gitlab there is visualization of those reports which is better in the paid plans but also exists in the free plan. For the free plan we need to use coverage in the cobertura format.

Now there is also something interesting in the trivy template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
spec:
  inputs:
    stage:
      default: test
      description: "Defines the build stage"
    image_to_scan:
      default: "${CI_REGISTRY_IMAGE}/test:${CI_COMMIT_SHA}_${CI_PIPELINE_ID}"
      description: "Image which we are planning to scan for security"
---

scan:
  stage: $[[ inputs.stage ]]
  image:
    name: docker.io/aquasec/trivy:latest
    entrypoint: [ "" ]
  variables:
    # No need to clone the repo, we exclusively work on artifacts.  See
    # https://docs.gitlab.com/ee/ci/runners/README.html#git-strategy
    GIT_STRATEGY: none
    TRIVY_USERNAME: "$CI_REGISTRY_USER"
    TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD"
    TRIVY_AUTH_URL: "$CI_REGISTRY"
    TRIVY_DB_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-db"
    TRIVY_JAVA_DB_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-java-db"
    CONTAINER_TEST_IMAGE: $[[ inputs.image_to_scan ]]
  script:
    - trivy --version
    # cache cleanup is needed when scanning images with the same tags, it does not remove the database
    - time trivy clean --scan-cache
    # update vulnerabilities db
    - time trivy image --download-db-only --no-progress --cache-dir .trivycache/
    # Builds report and puts it in the default workdir $CI_PROJECT_DIR, so `artifacts:` can take it from there
    - time trivy image --exit-code 0 --cache-dir .trivycache/ --no-progress --format template --template "@/contrib/gitlab.tpl"
        --output "$CI_PROJECT_DIR/gl-container-scanning-report.json" "$CONTAINER_TEST_IMAGE"
    # Prints full report
    - time trivy image --exit-code 0 --cache-dir .trivycache/ --severity CRITICAL,HIGH,MEDIUM --ignore-unfixed --no-progress "$CONTAINER_TEST_IMAGE"
    # Fail on critical vulnerabilities which are fixed
    - time trivy image --exit-code 1 --cache-dir .trivycache/ --severity CRITICAL --ignore-unfixed --no-progress "$CONTAINER_TEST_IMAGE"
  allow_failure: true (1)
  cache:
    paths:
      - .trivycache/
  # Enables https://docs.gitlab.com/ee/user/application_security/container_scanning/ (Container Scanning report is available on GitLab EE Ultimate or GitLab.com Gold)
  artifacts: (2)
    when:                          always
    reports:
      container_scanning:          gl-container-scanning-report.json
1 we are allowing this one to fail because there is high risk of false positive result, and also someone needs to decide what is better at the moment - older version with more security issues or newer version with less.
2 unfortunately this report we are saving as artifact is only available in the paid plan.

Release stage

In this stage we are including three component templates:

# ...
include:
  # ...
  - component: gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/pages@1.0.0
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/release@1.0.2
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/clean@1.0.2
# ...

First let’s look at the pages template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spec:
  inputs:
    stage:
      default: release
      description: "Defines the build stage"
---

pages: (1)
  stage: $[[ inputs.stage ]]
  image: alpine:latest
  script: (2)
    - mkdir -p public
    - mkdir -p doc/images
    - cp -R doc/images public/
    - cp doc/$APP_NAME.html public/index.html
    - cp doc/$APP_NAME.pdf public/
  artifacts: (3)
    paths:
      - public
  only:
    - master
  dependencies:
    - generate
1 In case of Gitlab Pages we just need a job named pages, and we need to prepare artifacts in a way explained in the Gitlab Pages documentation.
2 asciidoctor will generate our documentation in the doc folder. We are storing files important to the Gitlab pages as the artifacts.
3 We must set this public folder as artifact to make possible Gitlab Pages service to copy it to the valid place.

Next let’s check our release template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
spec:
  inputs:
    stage:
      default: 'release'
      description: 'Defines the build stage'
    docker_file:
      default: 'multi-stage.Dockerfile'
      description: 'Name of the Dockerfile to use for Container creation'
    source_image:
      default: ${CI_REGISTRY_IMAGE}/test:${CI_COMMIT_SHA}_${CI_PIPELINE_ID}
      description: 'Name of the target container test image'
---

# final image for the feature branch -
gitlab_registry_branch:
  stage: $[[ inputs.stage ]]
  variables:
    DOCKER_FILE: $[[ inputs.docker_file ]]
    CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG (1)
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/${DOCKER_FILE}" --destination "${CONTAINER_IMAGE}"
  except: (2)
    - master
    - tags
  when: manual

# final image for the master branch
gitlab_registry_latest:
  stage: $[[ inputs.stage ]]
  image:
    name: gcr.io/go-containerregistry/crane:debug
    entrypoint: [""]
  variables:
    GIT_STRATEGY: none
    CONTAINER_SOURCE_IMAGE: $[[ inputs.source_image ]]
    CONTAINER_TARGET_IMAGE: $CI_REGISTRY_IMAGE:latest
  before_script:
    - crane auth login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - crane cp $CONTAINER_SOURCE_IMAGE $CONTAINER_TARGET_IMAGE
  dependencies:
    - test_image
  only:
    - master

# final image for the production ready deployment - tags
gitlab_registry_tag:
  stage: $[[ inputs.stage ]]
  image:
    name: gcr.io/go-containerregistry/crane:debug
    entrypoint: [""]
  variables:
    GIT_STRATEGY: none
    CONTAINER_SOURCE_IMAGE: $[[ inputs.source_image ]]
    CONTAINER_TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
  before_script:
    - crane auth login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - crane cp $CONTAINER_SOURCE_IMAGE $CONTAINER_TARGET_IMAGE
  dependencies:
    - test_image
  only:
    - tags

include: (3)
  - component: gitlab.com/google-gitlab-components/artifact-registry/upload-artifact-registry@0.1.0
    inputs:
      stage: $[[ inputs.stage ]]
      source: ${CI_REGISTRY_IMAGE}/test:${CI_COMMIT_SHA}_${CI_PIPELINE_ID}
      target: europe-central2-docker.pkg.dev/devops-training-workshop-sbx/docker/$CI_PROJECT_PATH:latest
    only:
      - master
  - component: gitlab.com/google-gitlab-components/artifact-registry/upload-artifact-registry@0.1.0
    inputs:
      stage: $[[ inputs.stage ]]
      source: ${CI_REGISTRY_IMAGE}/test:${CI_COMMIT_SHA}_${CI_PIPELINE_ID}
      target: europe-central2-docker.pkg.dev/devops-training-workshop-sbx/docker/$CI_PROJECT_PATH:$CI_COMMIT_REF_NAME
    only:
      - tags
1 We can use this job when we like to create normal (not test) image which tag will be by default a slug name of the branch (for tags and latest we are overwriting it). For example if a branch is called feature/user-api-change-1 our tag will be in the end: feature-user-api-change-1. It is important as docker tags have character restrictions. We also needs to be cautious as tags also have length limitations.
2 We are informing pipeline that this jobs can be used only when we are NOT at the master branch, and we are NOT tagging.
3 We can also include other component template - in our case we are using already prepared component for uploading to the Google Artifact Registry.

Backend microservice Gitlab CI pipeline

TODO: Video with demo

Now Let’s start with the .gitlab-ci.yml file which we have in the Book List repository

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Version: 6.0
variables: (1)
  FF_NETWORK_PER_BUILD: "true"
  APP_NAME: book-list
  POSTGRES_PASSWORD: 2vQU45haQCnDS8sO
  POSTGRES_USER: book-list
  POSTGRES_DB: book-list

stages:
  - configure
  - format
  - build
  - test
  - security
  - release
  - deploy
  - destroy

include:
  - component: gitlab.com/devops-training-info/templates/ci/components/ansible-component/configure@1.0.0
    inputs:
      stage: configure
  - component: gitlab.com/devops-training-info/templates/ci/components/go-component/format@1.0.0
    inputs:
      stage: format
  - component: gitlab.com/devops-training-info/templates/ci/components/go-component/build@1.0.0
    inputs:
      stage: build
  - component: gitlab.com/devops-training-info/templates/ci/components/go-component/build@1.0.0
    inputs:
      stage: build
      golang_version: "1.24"
  - component:  gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/generate@1.0.0
    inputs:
      stage: build
      ruby_image: ruby:3.4
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/build@1.0.2
    inputs:
      stage: build
  - component: gitlab.com/devops-training-info/templates/ci/components/go-component/test@1.0.0 (2)
    inputs:
      stage: test
      services:
        - name: postgres:14-alpine
          alias: book-list-db
        - name: redis:7-alpine
          alias: redis
  - component: gitlab.com/devops-training-info/templates/ci/components/go-component/coverage@1.0.0
    inputs:
      stage: test
      services:
        - name: postgres:14-alpine
          alias: book-list-db
        - name: redis:7-alpine
          alias: redis
  - component: gitlab.com/devops-training-info/templates/ci/components/security-component/trivy@1.0.0
    inputs:
      stage: test
  - component: gitlab.com/devops-training-info/templates/ci/components/asciidoctor-component/pages@1.0.0
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/release@1.0.2
    inputs:
      stage: release
  - component: gitlab.com/devops-training-info/templates/ci/components/container-component/clean@1.0.2
  - component: gitlab.com/devops-training-info/templates/ci/components/terraform-component/deploy@1.0.4
    inputs:
      stage: deploy
      folder: terraform/environments/staging/europe-central2/run
  - component: gitlab.com/devops-training-info/templates/ci/components/terraform-component/destroy@1.0.4
    inputs:
      stage: destroy
      folder: terraform/environments/staging/europe-central2/run
1 The same as in a case of our frontend CI we need to rethink our use of variables here
2 We will focus in a moment on this template as it is bringing something new for us

Like mentioned upper let’s focus on our go component test template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
spec:
  inputs:
    stage:
      default: 'test'
      description: 'Defines the format stage'
    golang_version:
      default: 'latest'
      description: 'Golang image version tag'
    services:
      type: array
      default: []
      description: "Defines services which needs to exists for testing"

---

"test-$[[ inputs.golang_version ]]":
  image: golang:$[[ inputs.golang_version ]]
  stage: $[[ inputs.stage ]]
  script:
    - ./scripts/ci-test.sh
  services: $[[ inputs.services ]] (1)
1 Our unit tests requires additional services to work and we are giving this part to the template as an input.

Summary

Just to do a Quick Summary of presented solution.

As you can see our pipelines can be created in a reusable way and be complex in the nature. Normally this kind of complex pipelines can be found only in the medium and big projects which have more than 10 or 50 microservices.

Basically more repositories you have then more urgent is to create templates like we have shown here.

Component use Training

If you are Python developer we have a good training for this available here: https://python-gitlab-ci-cd-development-guide-be8161.gitlab.io If you haven’t done it yet, do it now.

We are planning to extend this section for a Go programmers and potentially other languages to give you a choice here.

What Next

Learning paths

After this workshop you can to take those learning paths:

  • A Cloud Guru Containers and Kubernetes related trainings - where you can learn more about Docker, Kubernetes, and Helm

  • A Cloud Guru DevOps automation related trainings - where you can learn more about Configuration Management tools like by example Ansible and many type of Pipelines

Next workshops in the microservices series

Backend

For a Backend developers we have this workshop in the series:

  • Backend local development with Docker and K3s for project using microservices architecture

Current agenda for the third edition looks like this:

  • Start 10:00

  • Section 1:

    • Architecture

    • Automation

    • Configuration

    • Service Skeleton

    • Readiness Checklist

    • Documentation

    • Services

    • Mockups

  • Coffee break 11:00 - 11:15

  • Section 2:

    • Book List API (demo)

  • Coffee break 12:30 - 12:45

  • Section 3:

    • Book Admin API (practice)

  • Lunch 13:30 - 14:00

  • Section 4:

    • Backend For Frontend (demo + practice)

  • Coffee break 15:15 - 15:30

  • Section 5:

    • Cleanup

    • What’s Next

    • Q&A

  • Ends between 16:00 and 17:00 - depends on Q&A session

Frontend

For a Frontend developers we have this workshop in the series:

  • Frontend local development with Docker and K3s for project using microservices architecture

Current agenda for the first edition looks like this:

  • Start 10:00

  • Section 1:

    • Architecture

    • Automation

    • Configuration

    • Service Skeleton

    • Readiness Checklist

    • Documentation

    • Services

  • Coffee break 11:00 - 11:15

  • Section 2:

    • Book Frontend (demo + practice)

  • Coffee break 12:30 - 12:45

  • Section 3:

    • What’s Next

    • Cleanup

    • Q&A

  • Ends between 13:00 and 13:30 - depends on Q&A session

Infrastructure

For Cloud Infrastructure Engineers and SysOps:

  • Preparing and using Infrastructure required for the projects using microservices architecture with the help of the Kubernetes, Clouds, Ansible, and Terraform.

Like in the title we will be creating mainly Cloud Infrastructure with a help of Terraform and other mentioned tools. It is possible that also bare metal will be mentioned and example prepared.

We have also a special OpenSource project MOB175:

  • Preparing examples used in the workshops series connected to the microservices architecture.

Anyone who is interested can join in a free time and people who are currently without project can let us know if wanted to join this project. Please contact me to gain more details.

Cleanup

TODO: new version based on Components flow

Let’s start with removing our deployment from the cluster. Please add new job called cleanup

cleanup:
  stage: deploy
  variables:
    PROJ_ENV: staging
  image: alpine/k8s:1.27.4
  before_script:
    - if [ $PROJ_ENV == "production" ]; then export KUBECONFIG=$KUBE_CONFIG_PROD ; else export KUBECONFIG=$KUBE_CONFIG_STAG ; fi
  script:
    - helm delete --namespace="workshops-${PROJ_ENV}" "${PROJ_ENV}-${CI_PROJECT_NAME}-${CI_PROJECT_ID}"
  only:
    - master
  when: manual

Commit changes wait till it will go to the manual jobs and run them one after another, and insterad of the staging please choose cleanup.

In the cleanup job in our current pipeline we should see this:

1
2
3
4
5
6
7
8
Running with gitlab-runner 16.3.0 (8ec04662)
  on gitlab-runner-bf79f759d-9xgcm k9s1JGXC, system ID: r_TNb3aMOHeJHC
....
$ helm delete --namespace="workshops-${PROJ_ENV}" "${PROJ_ENV}-${CI_PROJECT_NAME}-${CI_PROJECT_ID}"
...
release "staging-book-frontend-50493637" uninstalled (1)
Cleaning up project directory and file based variables 00:00
Job succeeded
1 You should see that your release was uninstalled.

QnA

Waiting for Questions :)


1. Python Language https://www.python.org/
2. Flask micro framework https://flask.palletsprojects.com/
3. we not always can just copy test image to the Gitlab Registry because when job is manual we will lose access to the test image
4. we not always can just copy test image to the Google Artifact Registry because when job is manual we will lose access to the test image
5. Hugo - The world’s fastest framework for building websites https://gohugo.io/