We implement CI

CI/CD is a DevOps practice focused on automating application deployment. It allows developers to focus on solving problems without wasting time on routine actions related to deploying new functionality or edits. This is possible due to the provision of an automated system for delivering code to the required environment, most often to the production platform.

CI/CD attempts to eliminate the human factor and remove the need for developers to publish changes manually.

Why GitLab?

In our work we use the self-hosted version of Gitlab, and since this platform provides powerful capabilities for organizing CI/CD out of the box, we use it. Moreover, Gitlab CI is very easy to understand and easy to configure.

This way, adding CI/CD to a new project takes no more than an hour, and the labor costs for publishing and the chances that something will break are reduced many times over.

What does it look like?

To start using GitLab CI, you need to place a special file in the root of the project — .gitlab-ci.yml. This file specifies stages, jobs, and scripts that will be executed during the job execution.

Gitlab CI also allows you to specify conditions for triggering tasks, such as deferred execution (scheduled jobs) or pushing to a specified branch.

A set of stages is called a pipeline. Pipelines can be anything, but most often it is a standard set: build, tests, deployment to the platform. It is the pipelines that you will describe in .gitlab-ci.yml if you want to use Gitlab CI.

GitLab Runner

Pipelines are executed by special demons – runners.

The runner can be hosted on either a physical server or a virtual machine. Typically, the runner downloads code or an artifact and runs tasks, either locally or in a container.

If you are also using a self-hosted solution, you can register a new runner on the server or use an already registered one. The public version of Gitlab should have runners available for Linux, Windows, and Mac.

We implement CI/CD

Roughly .gitlab-ci.yml might look like this:

stages:
  - deploy
  - error

.branch_stay_actual:
  stage: deploy
  rules:
    - if: ($CI_COMMIT_BRANCH == $BRANCH_NAME)
      when: on_success
  script:
    - make check_vars --makefile=MakefilePipeline -j1
    - make notify_start --makefile=MakefilePipeline -j1
    - make build --makefile=MakefilePipeline -j1
    - make shift_build_versions --makefile=MakefilePipeline -j1
    - make notify_success --makefile=MakefilePipeline -j1
    - make clear_tmp --makefile=MakefilePipeline -j1
  variables:
    GIT_STRATEGY: clone

dev-stay-actual:
  rules:
    - if: ($CI_PIPELINE_SOURCE == "schedule")
      when: never
    - if: ($CI_COMMIT_BRANCH == $BRANCH_NAME)
      when: on_success
  extends: .branch_stay_actual
  variables:
    COMPOSE_NAME: docker-compose-dev.yml
    FPM_CONTAINER_NAME: backend-dev-fpm
    CICD_DIR: /var/www/html/project/path/
  environment:
    name: "backend-dev"
  tags:
    - development

dev-react_error:
  stage: error
  tags:
    - development
  environment:
    name: "backend-dev"
  script:
    - sh $TELEGRAM_SCRIPT "❌ <b>FAILURE</b>"
  rules:
    - if: ($CI_PIPELINE_SOURCE == "schedule")
      when: never
    - if: ($CI_COMMIT_BRANCH != $BRANCH_NAME)
      when: never
    - when: on_failure

prod-stay-actual:
  rules:
    - if: ($CI_PIPELINE_SOURCE == "schedule")
      when: never
    - if: ($CI_COMMIT_BRANCH == $BRANCH_NAME)
      when: on_success
  extends: .branch_stay_actual
  variables:
    COMPOSE_NAME: docker-compose-prod.yml
    FPM_CONTAINER_NAME: backend-prod-fpm
    CICD_DIR:  /var/www/html/project/path/
  environment:
    name: "backend-prod"
  tags:
    - production

prod-error:
  stage: error
  tags:
    - production
  environment:
    name: "backend-prod"
  script:
    - sh $TELEGRAM_SCRIPT "❌ <b>FAILURE</b>"
  rules:
    - if: ($CI_PIPELINE_SOURCE == "schedule")
      when: never
    - if: ($CI_COMMIT_BRANCH != $BRANCH_NAME)
      when: never
    - when: on_failure

Let's analyze the file in order.

The stages block describes the pipeline stages. In this case, we have only two stages: deploy, in which the necessary actions for deploying the project on the server are performed, and error, which is performed in case of an error.

Next come the tasks.

The .branch_stay_actual task is a kind of prototype that is not executed directly and is inherited by the actual tasks below (in dev-stay-actual and prod-stay-actual for deployment to the dev site and prod site, respectively).

Each task has rules under which the task will be executed. For example, the dev-stay-actual task is not executed during deferred execution, but is launched only when the value of a special predefined Gitlab variable CI_COMMIT_BRANCH is equal to the value of the BRANCH_NAME variable, which is specified manually. The CI_COMMIT_BRANCH variable is initialized automatically by Gitlab itself when pushing to a branch.

Next comes a special environment block, roughly speaking, responsible for which variables should be passed to the pipeline. Environments are created in the Operate settings of your project in Gitlab, after which you can specify variables for the selected environment in the CI/CD project settings.

The script is responsible for what commands to execute. The contents of the script depend on your goals.

We use Makefile to reduce the size of .gitlab-ci.yml and maintain readability. Inside this Makefile, as well as inside the script as a whole, as said, there can be anything – the main thing is that the commands are available on the machine with the runner.

An example Makefile task that checks that all required variables are set:

check_vars:
    ifeq ($(BRANCH_NAME),)
        $(error BRANCH_NAME var is required!)
    endif
    ifeq ($(CICD_DIR),)
        $(error CICD_DIR var is required!)
    endif
    ifeq ($(TELEGRAM_SCRIPT),)
        $(error TELEGRAM_SCRIPT var is required!)
    endif
    ifeq ($(DOTENV), )
    	$(error DOTENV var is required)
    endif

Thus, the variables specified in the variables block of the task will be passed to the environment of the executed script. The list of predefined Gitlab CI/CD variables can be found Here.

An important detail of the pipeline are task tags. They are necessary for the runner to understand whether a given task needs to be completed or not.

Tags are configured for each runner individually and are specified for tasks. Tasks and runners can have zero or more tags.

For example, you can describe tasks that are picked up only by the runner who works with tasks marked as testing for periodic testing of the system. Or mark tasks that are performed only on dev, test, or prod platforms.

You can also inherit entire pipelines. It looks something like this:

reusable-pipeline.yml:

variables:
  DB_USER: user
  DB_PASSWORD: password

production:
  stage: production
  script:
    - build
    - deploy
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

.gitlab-ci.yml:

include:
   - project: "project-name"
        ref: 0.1.0
        file: reusable-pipeline.yml

variables:
  DB_USER: root
  DB_PASSWORD: real_password

stages:
  - build
  - test
  - production

production:
  environment:
    name: production

In this case, .gitlab-ci.yml is called reusable-pipeline.yml and is found in another repository – project-name. This way, you can describe one common pipeline for all similar projects and inherit it if necessary.

Conclusion

CI/CD helps developers reduce the cost of deploying and configuring projects, allowing them to focus on solving business problems. Gitlab is an extremely powerful platform, and we recommend looking into using the CI/CD tools it provides.

In this article, we looked at one of the simplest scenarios for using Gitlab CI, but its capabilities are much broader.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *