Содержание

Gitlab

Документация: https://docs.gitlab.com/ee/ci/
Get started with GitLab CI/CD: https://docs.gitlab.com/ee/ci/
Learn GitLab with tutorials: https://docs.gitlab.com/ee/tutorials/
GitLab with Git Essentials - Hands-On Lab: Use GitLab To Merge Code

Удобен тем, что содержит всё необходимое, не нужно использовать кучу различных сервисов:

Ещё один комбайн типа Гитлаба - Jetbrains Space.

Workflow

GitLab Component Function Also Known As…
Project The core building block where work is organized, managed, tracked and delivered to help the team to collaborate and plan work in the form of issues. Repository
Group A collection of projects and/or other groups. They are like folders. Project
Issue An issue is part of a project. It is the fundamental planning object where the team documents the use case in the description, discusses the approach, estimates the size/effort (issue weight), tracks actual time/effort, assigns work, and tracks progress. Story, Narrative, Ticket
Epic A collection of related issues across different groups and projects to help organize by theme Initiatives, Themes
Merge Request The linkage between the issue and the actual code changes. Captures the design, implementation details (code changes), discussions (code reviews), approvals, testing (CI Pipeline), and security scans. Pull Request
Label Used to tag and track work for a project or group and associate issues with different initiatives Tag
Board A visual listing of projects and issues useful for teams to manage their backlog of work, prioritize items, and move issues to the team or specific stage in the project. Kanban
Milestone A sprint or deliverable(s), helping you organize code, issues, and merge requests into a cohesive group Release
Roadmap A visual representation of the various epics for the group

Поток

  1. Issues - всё начинается с этого. Обсуждение и комментирование нововведения и его реализации. Проблема связана только с конкретным проектом, но если в группе несколько проектов, то можно видеть проблемы всех проектов на уровне группы.
  2. Merge Request - создаётся после создания проблемы. Мерж-реквесты позволяют визуализировать и совместно работать над предлагаемыми изменениями в исходном коде, которые существуют в виде коммитов в данной ветке Git. Иногда называется pull request.
  3. Когда мерж-реквест принят, его можно закоммитить, что запустит пайплайн.
  4. Выполнение пайплайна - сборка, тесты и деплой на тестовое окружение. Если пайплайн завершился неуспешно, можно прочесть логи для исправления проблемы.
  5. Review Apps - проверка приложения. Живой экземпляр новой версии приложения для ознакомления дизайнерам/менеджерам и т. п.
  6. Peer Review and Discussion - проверка и обсуждение с коллегами на предмет отсутствия каких-то конфликтов и т. п.
  7. Approve changes - одобрение изменений тем, у кого есть на это права.
  8. Merge; Issue Closed; CD Pipeline runs - после одобрения изменений проблема закрывается и приложение выкатывается в прод.
  9. Мониторинг - контроль приложения на предмет того, что изменения имеют желаемый эффект. В Гитлабе, если что, изменения можно откатить обратно.

Code Review Workflow

Дополнительные инструменты

Реестры

Дополнительно: Terraform Module Registry, Dependency Proxy (local proxy for frequently-used upstream images and packages. The Dependency Proxy caches both the manifest and blobs for a given image, so when you request it again, Docker Hub does not have to be contacted.)

Релизы

Это снапшот проекта для конечных пользователей, включая инсталляционные пакеты и примечания к релизу. Релизы могут создаваться в любой ветке. Создание релиза также создаёт git-тэг для пометки точки релиза в исходном коде.

Релиз включает:

Когда релиз создаётся, то

После релиза можно

Creating a release
Release CI/CD examples
Release CLI tool

В Гитлабе есть несколько функций для удобства развертывания и теста релизов.

Pipeline

Пайплайн - это файл .gitlab-ci.yml в корне git-репозитория. Состоит из последовательных этапов (stages), в каждом этапе есть задачи (jobs). По умолчанию, если не заданы условия, задачи выполняются параллельно. Если какая-то задача выполнилась с ошибкой, то весь пайплайн будет провален.

Вид простейшего пайплайна:

variables:
  ART_TOKEN: "37tui2v3pd8238PGd83g2"

stages:
  - build
  - release
  - notify

build_a:
  stage: build

build_b:
  stage: build
# https://docs.gitlab.com/ee/ci/yaml/index.html#only--except
  only: # срабатывать только, если:
    - master # ветка master
    - tags # может быть инициировано тэгом
    changes:
      - backend/* # при изменении файлов в каталоге backend
      - frontend/* # при изменении файлов в каталоге frontend
  except: # не срабатывать, если:
    refs: # refs: - значение по умолчанию, можно не писать.
      - schedules # этап вызван по расписанию
      - triggers # или по триггеру (через вызов API)
    variables:
      - $CI_COMMIT_MESSAGE =~ /skip tests/ # или сообщение коммита содержит текст
release_a:  
  stage: release
  needs: # зависимость задач друг от друга, в веб-интерфейсе GitLab есть их визуальное представление
    - build_a

release_b:  
  stage: release
  
slack_notify_build_b:
  stage: notify
  only:
    changes:
    - backend/*
    - frontend/*
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
      --data "{\"text\":\":building_construction: *$CI_PROJECT_NAME* ($CI_COMMIT_REF_NAME) backend - <$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/artifacts/$CI_COMMIT_REF_NAME/download?job=build-backend-code-job&private_token=$ART_TOKEN|:package:>\"}" \
      https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXX/w8ydugdiug2p938
  needs:
    - build_b

Релиз - это скомпилированный и упакованный исходный код + файл json, содержащий информацию о выпуске. Дополнительно можно добавить описание и прочее, например, ссылку на объект в хранилище артефактов. Можно использовать встроенное хранилище (Package Registry), а можно другое, например, Nexus.

Использование спецсимволов в строке скрипта: https://docs.gitlab.com/ee/ci/yaml/script.html (как экранировать символы, брать строку в кавычки и т. д.)
Переменные: https://docs.gitlab.com/ee/ci/variables/

Средство для проверки синтаксиса внутри Гитлаба - CI Lint.

Токен можно сгенерировать как для пользователя в целом (Edit profile → Access tokens), так и для отдельного репозитория (Settings → Access tokens).

Heredoc и перенос строк

- |- сохраняет переносы строк. Удобно для heredoc, if/else и т. п.
- > превращает переносы строк в пробелы. Удобно для простого переноса длинной строки.

release:
  image: node:12-stretch-slim
  stage: release
  before_script:
    - apt-get update && apt-get install -y curl git jq
  script:
    - |-
      PAYLOAD=$(cat << JSON
      {
        "branch": "master",
        "commit_message": "some commit message",
        "actions": [
          {
            "action": "create",
            "file_path": "foo/bar",
            "content": "some content"
          }
        ]
      }
      JSON
      )
    - >
      curl -X POST https://requestbin.io/1f84by61
      --header 'Content-Type: application/json; charset=utf-8'
      --data-binary "$PAYLOAD"
  when: manual
  only:
    - /^release-.*$/

https://forum.gitlab.com/t/is-it-possible-to-use-a-here-document-from-within-a-multiline-command-in-gitlab-ci-yml/39329/2

Задачи (jobs)

Могут являться частью этапа (stage). В рамках одного этапа задачи выполняются одновременно. Этап не начинается, пока не закончится предыдущий. Зависимость задач от выполнения этапов:

stages:
    - test
    - build
    - push
    - deploy

lint-test:
    stage: test
    before_script:
        - echo "prepare lint test"
    script:
        - echo "lint test"
    after_script:
        - echo "cleaing up lint test data"

unit-test:
    stage: test
    before_script:
        - echo "prepare unit test"
    script:
        - echo "unit test"
    after_script:
        - echo "cleaing up unit test data"

build-windows:
    stage: build
    script:
    # задача закончится ошибкой, и задача push-windows и deploy будут пропущены
        - wrong-echo "build windows image"
        - echo "tag windows image"

build-linux:
    stage: build
    script:
        - echo "build linux image"
        - echo "tag linux image"

push-windows:
    stage: push
    needs:
        - build-windows
    script:
        - echo "log in to repository"
        - echo "push windows image"

push-linux:
    stage: push
    needs:
        - build-linux
    script:
        - echo "log in to repository"
        - echo "push linux image"

deploy:
    stage: deploy
    script:
        - echo "deploy"

Можно вызвать скрипт, но сначала его нужно сделать исполняемым:

    script:
        - chmod +x ./scriptfile.sh
        - ./scriptfile.sh

Если скрипт что-то создал в каталоге, то можно это не удалять, т. к. пайплайн запускается каждый раз по новой и не использует старого окружения.

only/except

Определяют, при каких условиях задача выполняется.

# выполнять только в ветке main
job:
  only:
    - main

Workflow rules

workflow определяет глобальные настройки пайплайна. Положим, чтобы не добавлять в каждую задачу only:, можно задать это глобально:

workflow:
  rules:
    # если ветка не main и вызывается не с помощью merge/pull request, то
    - if: $CI_COMMIT_BRANCH != "main" && $CI_PIPELINE_SOURCE != "merge_request_event"
      when: never # никогда не выполнять
    - when: always # в остальных случаях - выполнять

$CI_COMMIT_BRANCH и $CI_PIPELINE_SOURCE - встроенные переменные.

Переменные

Помимо встроенных переменных, есть произвольные.
Настройка: Settings (внутри проекта) → CI/CD → Variables.

Тип переменной может быть file. Например, можно занести туда конфигурационный файл для какого-либо сервиса. При вызове переменной в ней содержится полный путь к этому файлу, например,

# так выводится путь
echo "$CONF_FILE"
/builds/user/projectname.tmp/CONF_FILE
# а так - содержимое
cat $CONF_FILE

Можно задать переменные и внутри пайплайна

variables:
  image_repo: docker.io/id/app
  image_tag: v2.0

Если блок variables: находится внутри задачи, то они действуют только внутри этой задачи. Чтобы переменные действовали во всех задачах пайплайна, блок variables: нужно перенести на верхний уровень (вровень с самими задачами).

Использование в Dockerfile образов из локального реестра образов

В пайплайне нужно добавить –build-arg CI_REGISTRY_IMAGE=${CI_REGISTRY_IMAGE} в команду сборки.

build:
  stage: build
  image: docker:20.10.12-dind-rootless
  before_script:
    - until docker info; do sleep 1; done
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - cd backend
    - >
      docker build
      --build-arg VERSION=$VERSION
      --build-arg CI_REGISTRY_IMAGE=${CI_REGISTRY_IMAGE}
      --tag $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
      .
    - docker push $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA

Dockerfile:

ARG CI_REGISTRY_IMAGE=${CI_REGISTRY_IMAGE}

FROM ${CI_REGISTRY_IMAGE}/node:16.18-alpine AS builder

Кэш

Хранение файлов, которые могут быть использованы в соседней задаче на раннере повторно вместо того, чтобы каждый раз качать их из интернета или с сервера, например, npm/ruby/python/go-пакеты.

Артефакты помещаются на сервер, кэш хранится на раннере. Если раннеров много, нужно настраивать распределённый кэш (distributed caching), хранящийся в S3. Хоть это всё равно удалённый сервер, это более эффективно, чем качать из интернета, и качается один zip-файл вместо множества мелких.

Настраивается соответствующим разделом в задаче. Если не задать ключ (имя кэша), то он будет называться default. Все задачи, где совпадает ключ, будут использовать один и тот же кэш.

run_unit_tests:
...
  cache:
    key: "npmdeps_$CI_COMMIT_REF_NAME"
    paths:
      - app/node_modules
    policy: pull-push

«Pull-push» - значение по умолчанию, его можно не указывать, т. е., кэш используется и потом обновляется при необходимости. Если есть какая-то параллельно выполняющаяся задача, использующая тот же кэш, то политику нужно отрегулировать, чтобы обе задачи не пытались писать одни и те же файлы в кэш. В одной из задач ставится policy: pull, чтобы она только использовала кэш без его обновления. Иногда, в больших пайплайнах, бывает выделенная задача для создания кэша, там тогда нужно ставить политику в push.

Команду npm install всё равно нужно оставить, чтобы отсутствие кэша или какие-то проблемы с ним не влияли на общую работу пайплайна.

Если кэш создаётся с помощью Docker executor, то нужно настраивать volume на раннере, чтобы кэш не уничтожился вместе с контейнером. Путь в cache_dir и в volumes должен быть одним и тем же.

/etc/gitlab-runner/config.toml
[[runners]]
...
  executor = "docker"
  cache_dir = "/cache"
  ...
  [runners.docker]
    volumes = ["/cache"]

Разница между первым и вторым запуском пайплайна. Первый раз кэш формировался, второй - использовался.

Почистить кэш можно кнопкой «Clear runner caches» в CI/CD → Pipelines. «Почистить» в этом случае значит просто не использовать кэшв следующих запусках пайплайна, физически кэш не удаляется. Где хранится кэш?

Справка по кэшу

Шаблоны задач

Идут в комплекте с Гитлабом и могут быть включены в пайплайн в любом проекте, например, SAST (тест кода на безопасность).

Шаблоны: https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates

Включить SAST:

sast:
  stage: test
  tags:
    - remote
    - docker

include:
  template: Jobs/SAST.gitlab-ci.yml

Можно включить до 100 шаблонов. Местоположение раздела include: в пайплайне неважно.

Если в проекте ещё нет пайплайна, то в разделе CI/CD → Pipelines можно выбрать шаблон для быстрого его создания.

Deploy

  1. На сервере, куда будет ставиться приложение, сгенерить ключи SSH: ssh-keygen -t ed25519 -C "dev-server"
  2. Засунуть закрытый ключ в переменную File на Гитлабе (в конце должна быть пустая строка!)
  3. Открытый ключ добавить на dev-server в ~/.ssh/authorized_keys пользователя, под которым будет деплой.
  4. Добавить сертификат git.ca.crt в доверенные, так же как на раннере: nano /etc/ssl/certs/git.ca.crt # вставить содержимое сертификата с сервера
  5. В пайплайне примерно следующее
deploy_to_dev:
  stage: deploy
  tags:
    - remote
    - shell
  before_script:  
    - chmod 400 $SSH_PRIVATE_KEY
  script:
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY ssu@$DEV_SERVER_HOST "
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
        docker run -d -p 3000:3000 $IMAGE_NAME:$IMAGE_TAG
        "

Вышеприведённый вариант с Докером неудобен, т. к. контейнер не заменяется, и при повторном запуске пайпдайна будет ошибка, т. к. порт занят. Эту проблему решает docker-compose.

deploy_to_dev:
  stage: deploy
  tags:
    - remote
    - shell
  before_script:  
    - chmod 400 $SSH_PRIVATE_KEY
    - scp -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY ./docker-compose.yaml ssu@$DEV_SERVER_HOST:~
  script:
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY ssu@$DEV_SERVER_HOST "
        export IMAGE_NAME=$IMAGE_NAME &&
        export IMAGE_TAG=$IMAGE_TAG &&
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
        docker-compose down &&
        docker-compose up -d
        "
  environment:
    name: development
    url: $DEV_ENDPOINT

Так как сервер, где запускается контейнер, ничего не знает об окружении, надо пробрасывать значения переменных через export и копировать docker-compose.yaml по SSH. CСоответственно, в docker-compose.yaml можно сослаться на переменные

version: "3.3"
services:
  app:
    image: $IMAGE_NAME:$IMAGE_TAG
    ports:
      - 3000:3000

SSH keys, документация: https://docs.gitlab.com/ee/ci/ssh_keys/

Автоверсии

Канон - major.minor.patch. Здесь в примере npm и версия находится в файле app/version.json, откуда она будет браться:

{
  "name": "bootcamp-node-project",
  "version": "1.0",
  ..
},

Недостающая часть добавляется из ID пайплайна. jd надо поставить на воркере, если его там нет. Полученный файл передаётся через артефакт.

build_image:
  ...
  before_script:
    - export PACKAGE_JSON_VERSION=$(cat app/package.json | jq -r .version)
    - export VERSION=$PACKAGE_JSON_VERSION.$CI_PIPELINE_IID
    - echo $VERSION > version-file.txt
  artifacts:
    paths:
      - version-file.txt

Артефакты автоматически доступны в последующих этапах, но не между задачами внутри одного этапа. Чтобы сделать артефакт доступным для соседней задачи, нужно настроить dependencies/needs.
Needs ждёт выполнения задач, указанных там, а dependencies берёт артефакты из указанных задач. Если указаны и needs, и dependencies одновременно, то needs должен содержать задачи, перечисленные в dependencies, иначе работать не будет. В то же время, если указать только needs, артефакты будут скачаны из задач, перечисленных там, поэтому нет необходимости указывать dependencies, если они совпадают с needs.

push_image:
  ...
  needs:
    - build_image
  before_script:
    - export VERSION=$(cat version-file.txt)
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker push $IMAGE_NAME:$VERSION

Чтобы заблокировать скачивание артефактов, например, на этапе последующего тестирования, нужно указать пустой массив:

test_dev:
  stage: deploy
  dependencies: []
  script:
    - echo "testing dev"

В специфическом случае с npm можно провернуть вариант с .env вместо выгрузки артефактов. Это файл «ключ-значение». В блоке артефактов указывается reports → dotenv.

build_image:
  ...
  before_script:
    - export PACKAGE_JSON_VERSION=$(cat app/package.json | jq -r .version)
    - export VERSION=$PACKAGE_JSON_VERSION.$CI_PIPELINE_IID
    - echo "VERSION=$VERSION" > build.env
    - echo "DEVELOPER=Vasya" >> build.env
  artifacts:
    reports:
      dotenv: build.env

После этого последующие соседние задачи (c соответствующими dependencies/needs) будут уже знать про эти переменные, ничего указывать не надо.

Развёртывание на несколько окружений

dev → staging → prod
Functional tests
Integration tests
SAST tests
Performance tests
DAST tests

Для примера развёртывание будет на одну и ту же машину с использованием разных портов. Для начала нужно задать переменную для порта в докер-композе, чтобы управлять ей из пайплайна.

ports:
  - ${APP_PORT}:3000

Задать переменную в пайплайне в задаче deploy_to_dev: export APP_PORT=3000, а также export COMPOSE_PROJECT_NAME=dev, чтобы задать имя проекта, иначе docker-compose down в другом деплое будет останавливать один и тот же контейнер (файл Докер-композ используется ведь тот же самый, и имя по умолчанию генерируется одинаковое).

Ну а затем копируется задача deploy_to_dev в deploy_to_staging и там меняется всё, что нужно. Создаются этапы deploy_dev и deploy_staging, меняются/дописываются переменные. Перед deploy_to_staging должна стоять задача каких-нибудь functional tests, чтобы, если тесты не прошли, развётрывание на тестовое окружение уже не шло. Чтобы не плодить этапов, эти тесты могут входить в этап deploy_dev с needs: - deploy_to_dev.

Чтобы не копировать практически одинаковые задачи деплоя много раз, нужно сделать шаблон задачи.

Положим, взяли задачу deploy_to_dev, поменяли название (точка спереди - чтобы эта «задача» не выполнялась), удалён stage: и все параметры, которые меняются от одного окружения к другому ($SSH_PRIVATE_KEY), указываются в блоке variables: как задаваемые извне.

.deploy:
  tags:
    - remote
    - shell
  variables:
    SSH_PRIVATE_KEY: ""
    SERVER_HOST: ""
    DEPLOY_ENV: ""
    APP_PORT: ""
    ENDPOINT: ""
  before_script:  
    - chmod 400 $SSH_PRIVATE_KEY
    - export VERSION=$(cat version-file.txt)
    - scp -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY ./docker-compose.yaml ubuntu@$SERVER_HOST:~
  script:
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY ubuntu@$SERVER_HOST "
        export IMAGE_NAME=$IMAGE_NAME &&
        export IMAGE_TAG=$VERSION &&
        export APP_PORT=$APP_PORT &&
        export COMPOSE_PROJECT_NAME=$DEPLOY_ENV &&
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
        docker-compose down &&
        docker-compose up -d
        "
  environment:
    name: $DEPLOY_ENV
    url: $ENDPOINT

А потом можно ссылаться на этот шаблон, задавая недостающие параметры и значения переменных. Имена переменных лучше делать разными, например, так работать не будет: SSH_PRIVATE_KEY: $SSH_PRIVATE_KEY, если $SSH_PRIVATE_KEY имеет тип File. В этом случае при присвоении вместо пути к файлу в переменную будет пихаться его содержимое.

deploy_to_dev:
  extends: .deploy
  stage: deploy_dev
  variables:
    SSH_KEY: $DEV_SSH_PRIVATE_KEY
    SERVER_HOST: $DEV_SERVER_HOST
    DEPLOY_ENV: development
    APP_PORT: 3000
    ENDPOINT: $DEV_ENDPOINT
    
deploy_to_staging:
  extends: .deploy
  stage: deploy_staging
  variables:
    SSH_KEY: $STAGING_SSH_PRIVATE_KEY
    SERVER_HOST: $STAGING_SERVER_HOST
    DEPLOY_ENV: staging
    APP_PORT: 4000
    ENDPOINT: $STAGING_ENDPOINT

Install

:!: По состоянию на 2023 год репозитории для установки закрыты для России, качать и ставить надо пакетом.
https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=amd64&dist=jammy (для Ubuntu 22.04)

wget --content-disposition https://packages.gitlab.com/gitlab/gitlab-ce/packages/ubuntu/jammy/gitlab-ce_16.0.5-ce.0_amd64.deb/download.deb
dpkg -i gitlab-ce_16.0.5-ce.0_amd64.deb

DockerHub: https://hub.docker.com/u/gitlab
Install on Docker: https://docs.gitlab.com/ee/install/docker.html
Private CI/CD using Docker: https://oramind.com/private-cicd-using-gitlab-docker/

https://packages.gitlab.com/gitlab/gitlab-ce
https://docs.gitlab.com/ee/update/package/index.html#upgrade-using-a-manually-downloaded-package

Runner

Скачать: https://gitlab.com/gitlab-org/gitlab-runner/-/releases
Установить:

curl -LJO "https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_amd64.deb"
dpkg -i gitlab-runner_amd64.deb

Регистрация: gitlab-runner register

# Регистрировать можно несколько раз, если нужно несколько видов раннера на одной и той же машине.
# URL и token берутся из Settings -> CI/CD -> Runners
URL='http://git.example.com/'
REG_TOKEN='GD1358941-xY4DE8k7Mr3dffgLLun'
 
gitlab-runner register \
  --url $URL \
  --registration-token $REG_TOKEN \
  --executor shell \
  --tag-list "shell" \
  --description t-docker2
gitlab-runner register \
  --url $URL \
  --registration-token $REG_TOKEN \
  --executor docker \
  --tag-list "docker" \
  --docker-image "alpine:3.18" \
  --description t-docker2
 
# Перезапустить сервис, если раннер не готов сразу
systemctl restart gitlab-runner
 
# Для докера нужно добавить пользователя gitlab-runner в группу docker
sudo usermod -aG docker gitlab-runner
reboot

:!: Если задачи застревают (stack), надо поставить галку Run untagged jobs в Admin → Settings → CI/CD → Runners → Свойства раннера. Но лучше прописывать тэги раннера в задачах пайплайна.

https://docs.gitlab.com/runner/install/linux-manually.html
https://docs.gitlab.com/runner/register/

Container registry

Здесь:

  1. Gitlab был установлен из пакета (Omnibus GitLab installations)
  2. Тот же домен, порт 5050
  3. Сертификат самоподписанный
CRT_DIR=/etc/gitlab/ssl
CN=git.example.com
PORT=5050
 
mkdir -p -m 700 $CRT_DIR
cd $CRT_DIR
 
# Сгенерить сертификаты
openssl genrsa -out $CN.ca.key 2048
openssl req -new -x509 -days 36500 -key $CN.ca.key -subj "/C=CN/ST=GD/L=SZ/O=$CN/CN=$CN Root CA" -out $CN.ca.crt
openssl req -newkey rsa:2048 -nodes -keyout $CN.key -subj "/C=CN/ST=GD/L=SZ/O=$CN/CN=$CN" -out $CN.csr
openssl x509 -req -extfile <(printf "subjectAltName=DNS:$CN") -days 36500 -in $CN.csr -CA $CN.ca.crt -CAkey $CN.ca.key -CAcreateserial -out $CN.crt
 
chmod 600 $CRT_DIR/$CN.*
 
###################################
# В конфиге /etc/gitlab/gitlab.rb
###################################
# Включить SSL для самого Гитлаба
sed -i "/[^_]external_url /c external_url 'https://$CN'
/nginx\['ssl_certificate'\] /c nginx['ssl_certificate'] = \"/etc/gitlab/ssl/$CN.crt\"
/nginx\['ssl_certificate_key'\] /c nginx['ssl_certificate_key'] = \"/etc/gitlab/ssl/$CN.key\"
" /etc/gitlab/gitlab.rb
# Включить реестр контейнеров
sed -i "/registry_external_url /c registry_external_url 'https://$CN:$PORT'
/registry_nginx\['enable'\] /c registry_nginx['enable'] = true
/registry_nginx\['listen_port'\] /c registry_nginx['listen_port'] = $PORT
" /etc/gitlab/gitlab.rb
 
# Перечитать настройки
sudo gitlab-ctl reconfigure

https://docs.gitlab.com/ee/administration/packages/container_registry.html#configure-container-registry-under-an-existing-gitlab-domain

На раннере:

# Добавить сертификат git.ca.crt в доверенные
nano /etc/ssl/certs/git.ca.crt # вставить содержимое сертификата с сервера
update-ca-certificates
# После этого можно регистрировать раннер

Если не добавить в доверенные на уровне самой системы, надо будет париться с регистрацией раннера на сервере и с докером:

### Let Docker accept your self-signed certificate
# Per default, Docker will not accept your self-signed certificate. You need to create a folder with your CA in order to make Docker aware that
# your certificate is valid. For that reason, you create a folder of your trusted Docker registry and copy your CA into the folder.
# If you will not copy the CA in the folder. You will receive the following error:
# Registry fails with x509 certificate signed by unknown authority 
sudo mkdir -p /etc/docker/certs.d/git.example.com:5050
sudo cp /etc/gitlab/ssl/git.example.com.ca.crt /etc/docker/certs.d/git.example.com:5050/
 
### Let GitLab runners accept your self-signed certificate
# The same error will occur when you want to register your GitLab runner:
# Post “https://git.example.com/api/v4/runners”: x509: certificate signed by unknown authority
# Please use the following command in order to register your GitLab runner successfully:
sudo gitlab-runner register --tls-ca-file="/etc/gitlab/ssl/git.example.com.ca.crt"

Справка: <YOUR_GITLAB_URL>/help/administration/packages/container_registry.md
x509: certificate relies on legacy Common Name field, use SANs instead
Installing a root CA certificate in the trust store
GitLab server with a self-signed certificate and embedded docker registry

Подход к работе с микросервисами

Если микросервисов в приложении больше чем один, возникает 2 подхода к хранению кода

  1. Один репозиторий (monorepo) - для каждого микросервиса используется свой каталог. Минусы - опасность написания связанного кода между микросервисами, большой объём кода, сложность управления пайплайнами, одним коммитом можно поломать всё сразу. Подходит для маленьких проектов и одной команды.
  2. Несколько (polyrepo) - каждый микросервис находится в своём репозитории. В Гитлабе связанные проекты можно объединить в группу.

Сеть

Чтобы контейнеры могли взаимодействовать, их нужно поместить в одну сеть. Для этого в докер-композ-файле указывается определённая сеть, которая должна уже присутствовать (параметр external)

version: "3.3"
services:
  app:
    image: ${DC_IMAGE_NAME}:${DC_IMAGE_TAG}
    ports:
      - ${DC_APP_PORT}:${DC_APP_PORT}
    networks:
      - micro_service

networks:
  micro_service:
    external:
      name: micro_service

:!: Параметр name: в опциях сети предписывает не прибавлять к её названию имя проекта, т. е. будет именно micro_service, а не project_micro_service.

А в пайплайне в шаблоне задачи деплоя прописать создание этой сети и указание, чтобы задача не обламывалась, если сеть уже есть:

docker network create micro_service || true &&
docker-compose down &&
docker-compose up -d

Полный листинг .gitlab-ci.yml (monorepo)

Polyrepo

Подготовка:

  1. Создаётся группа, куда добавляются все нужные проекты. Удобство группы в том, что можно задавать переменные, раннеры и т. п. для всей группы, а не для каждого проекта.
  2. Раннер для группы регистрируется отдельно в настройках группы. Потом нужно в группе настроить переменную для закрытого ключа SSH сервера деплоя.

Дальше в каждую репу копируются docker-compose.yml и .gitlab-ci.yml и редактируются соответственно. Шаблоны задач можно переделать в реальные задачи, т. к. сервис в репе только один, отредактировав их и убрав задачи, вызывающие эти шаблоны, удалить из них блоки переменных, ожидаемых извне. Убрать детекты срабатывания в подкаталогах и заходы в подкаталоги из задач.

Полный листинг .gitlab-ci.yml (polyrepo) сервиса frontend

Проблема множества репозиториев в том, что одна и та же конфигурация повторяется из раза в раз, а если работают разные команды разрабов, то эти конфигурации начинают разниться в подходах, появляются разные тесты и т. п. и управлять этим барахлом становится тяжело. Для решения этой проблемы есть шаблоны задач, но не в том же пайплайне, а вынесенные в отдельный проект, на которые можно ссылаться как на шаблон SAST, к примеру.

Шаблоны задач

Надо вынести повторяющиеся задачи в отдельные .yml-файлы, например

.build-template.yml
# Можно также вписать блок с переменными, которые нужно передать извне для большей наглядности, но это необязательно
variables:
  MICRO_SERVICE: ""
  SERVICE_VERSION: ""

build:
  stage: build
  before_script:
    - export IMAGE_NAME=$CI_REGISTRY_IMAGE/microservice/$MICRO_SERVICE
    - export IMAGE_TAG=$SERVICE_VERSION
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker push $IMAGE_NAME:$IMAGE_TAG

В файле шаблона всё должно быть параметризовано по-максимуму. В основном пайплайне нужно сослаться на файлы шаблона, а в самой задае добавить только те разделы, которых нет в шаблоне.
Местоположение раздела include: в пайплайне не имеет значения.
:!: Если нужно что-то добавить в раздел, который уже есть в шаблоне (например, before_script), то придётся писать его полностью, т. к. при наличии раздела в основном пайплайне раздел шаблона полностью заменяется.

.gitlab-ci.yml
include:
  - local: '.build-template.yml'
  - local: '.deploy-template.yml'
...

build:
  tags:
    - group
    - shell
  before_script:
    - echo "Добавленные действия, а дальше то, что уже есть в шаблоне, иначе не заработает"
    - export IMAGE_NAME=$CI_REGISTRY_IMAGE/microservice/$MICRO_SERVICE
    - export IMAGE_TAG=$SERVICE_VERSION

Файлы шаблонов можно вынести в отдельный репозиторий, например, ci-templates. В этом случае ссылаться на эти шаблоны нужно так:

.gitlab-ci.yml
include:
  - project: mymicroservice-cicd/ci-templates
    ref: main
    file:
      - build.yml
      - deploy.yml

ref: main указывает на ветку.
Т. к. репозиторий в той же группе, указывается группа/репозиторий. Если бы он был вне группы, надо было бы указывать имя пользователя/репозиторий.

Mail

If you would rather send application email via an SMTP server instead of via Sendmail or Postfix, add the following configuration information to /etc/gitlab/gitlab.rb and run gitlab-ctl reconfigure.
https://docs.gitlab.com/omnibus/settings/smtp.html

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "mail.example.com"
gitlab_rails['smtp_port'] = 25
gitlab_rails['smtp_domain'] = "example.com"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_openssl_verify_mode'] = 'none'
 
### Email Settings
gitlab_rails['gitlab_email_from'] = 'git@example.com'
gitlab_rails['gitlab_email_display_name'] = 'Git'

Проверка отправки писем из консоли

gitlab-rails console -e production
Notify.test_email('user@example.com', 'Hello World', 'This is a test message').deliver_now

Решение проблем

502 - Whoops, GitLab is taking too much time to respond is normal during GitLab startup and goes away after couple minutes

После перезапуска возникает ошибка 502. Нужно подождать, т. к. Gitlab запускается небыстро. См. статус: gitlab-ctl

OpenSSL::SSL::SSLError (hostname does not match the server certificate)

В конфигурацию /etc/gitlab/gitlab.rb добавить
gitlab_rails['smtp_openssl_verify_mode'] = 'none'
# Затем
gitlab-ctl reconfigure
gitlab-rake cache:clear RAILS_ENV=production

https://gitlab.com/gitlab-org/gitlab-foss/-/issues/446

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

Возникает при сборке образа на раннере (shell executor). Решение:

usermod -aG docker gitlab-runner
service docker restart

https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3492

CI_REGISTRY error during connect status 255: Permission denied, please try again

Раннер не может подключиться к реестру контейнеров:

$ echo "$CI_REGISTRY_PASSWORD" |docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
error during connect: Post "http://docker.example.com/v1.24/auth": command [ssh -l cicd -- 10.1.0.138 docker system dial-stdio] has exited with exit status 255,
please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=Permission denied, please try again.
Permission denied, please try again.
cicd@10.1.0.138: Permission denied (publickey,password).

Решение: не раннере удалить всё в каталоге /home/gitlab-runner.

Безопасность

Shifting Security Left - GitLab DevSecOps Overview: https://www.youtube.com/watch?v=XnYstHObqlA

Гитлаб предлагает сканеры безопасности:

Отчёты / управление:

Прочее

Тестовый проект Gitlab from zero to hero

https://gitlab.com/nanuchi/mynodeapp-cicd-project

Monorepo: https://gitlab.com/nanuchi/mymicroservice-cicd

Example CI/CD Pipelines

Ruby Auto Deploy
Simple Maven App
AutoDevOps
More Project Examples

Заметки о реальных проектах

Сервис личного кабинета в Docker Swarm

# SSH / Context
ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY $DEPLOY_USER@$DEPLOY_HOST
 
docker context create remote --docker "host=ssh://cicd@host"
docker context use remote
 
# Вход в репозиторий
docker login -u mail@example.com -p P@ssw0rd git.example.com:5050
# Пулл для локального тестирования
docker pull git.example.com:5050/personal-account/backend-laravel/lk/backend_app:1.0
docker pull git.example.com:5050/personal-account/backend-laravel/lk/backend_nginx:1.0
 
# Сети надо создавать overlay и swarm, иначе не будет работать внутренний DNS
docker network create --driver=overlay --scope=swarm lk_rabbitmq
docker network create --driver=overlay --scope=swarm lk_backend
# Запуск
docker stack deploy -c ./docker-compose.yml lk
 
#docker stack rm lk
 
docker exec $(docker ps -qf name=lk_app) mkdir -p /var/www/html/storage/framework/sessions
 
mkdir -p /var/www/html/storage/framework/sessions
chown -R 82:82 /var/www/html/storage
version: "3.9"
services:
  app: &app
    image: git.example.com:5050/personal-account/backend-laravel/lk/backend_app:1.0
    healthcheck:
      test: ["CMD", "netstat", "-an", "|fgrep", ":9000"]
    volumes:
      - /docker/lk/www/storage:/var/www/html/storage
    networks:
      - lk_backend
      - lk_rabbitmq

  nginx:
    image: git.example.com:5050/personal-account/backend-laravel/lk/backend_nginx:1.0
    environment:
      NGINX_ROOT: /var/www/html/public
      NGINX_FASTCGI_PASS: app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80"]
    ports:
      - 8080:80
    networks:
      - lk_backend

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: personal-area
      MYSQL_ROOT_PASSWORD: password
      MYSQL_PASSWORD: password
      MYSQL_USER: personal-area-user
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$$MYSQL_ROOT_PASSWORD"]
    volumes:
      - /docker/lk/db:/docker-entrypoint-initdb.d
    networks:
      - lk_backend

  rabbitmq:
    image: rabbitmq:management-alpine
    hostname: rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
    ports:
      - 5672:5672
      - 15672:15672
    volumes:
      - /docker/lk/rabbitmq:/var/lib/rabbitmq
    networks:
      - lk_rabbitmq

  queue-lk-job-broadcast: &queue
    <<: *app
    command: php artisan queue:work --queue=lk.job.broadcast
    healthcheck:
      disable: true

  queue-lk-job-catalog_order:
    <<: *queue
    command: php artisan queue:work --queue=lk.job.catalog_order

  queue-lk-job-notification:
    <<: *queue
    command: php artisan queue:work --queue=lk.job.notification

  queue-lk-job-schedule:
    <<: *queue
    command: php artisan queue:work --queue=lk.job.schedule
 
# И ещё куча таких же, слушающих очереди

  websockets:
    <<: *queue
    command: php artisan websockets:serve
 

networks:
  lk_backend:
    external: true
    name: lk_backend
  lk_rabbitmq:
    external: true
    name: lk_rabbitmq