Содержание
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
Удобен тем, что содержит всё необходимое, не нужно использовать кучу различных сервисов:
- Собственно, сам git-репозиторий
- wiki
- Issue board с привязкой к событиями внутри Гитлаба, этакая канбан-доска. Для каждого этапа можно создать список задач, а также метки для удобной фильтрации. Для каждого тикета можно назначить ответственного и этап.
- Container registry - хранилище образов контейнеров
- Package registry - хранилище ПО
Ещё один комбайн типа Гитлаба - Jetbrains Space.
- Pipeline - описывается в
.gitlab-ci.yml
, кладётся в корень репозитория. - Job artifacts — результаты выполнения задач.
- Cache dependencies — кэш зависимостей можно сохранять для ускорения сборки.
- CI/CD variables — могут использоваться в
.gitlab-ci.yml
, но лучше не задавать переменные в самом файле, а определять их в настройках CI/CD.
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 |
Поток
- Issues - всё начинается с этого. Обсуждение и комментирование нововведения и его реализации. Проблема связана только с конкретным проектом, но если в группе несколько проектов, то можно видеть проблемы всех проектов на уровне группы.
- Merge Request - создаётся после создания проблемы. Мерж-реквесты позволяют визуализировать и совместно работать над предлагаемыми изменениями в исходном коде, которые существуют в виде коммитов в данной ветке Git. Иногда называется pull request.
- Когда мерж-реквест принят, его можно закоммитить, что запустит пайплайн.
- Выполнение пайплайна - сборка, тесты и деплой на тестовое окружение. Если пайплайн завершился неуспешно, можно прочесть логи для исправления проблемы.
- Review Apps - проверка приложения. Живой экземпляр новой версии приложения для ознакомления дизайнерам/менеджерам и т. п.
- Peer Review and Discussion - проверка и обсуждение с коллегами на предмет отсутствия каких-то конфликтов и т. п.
- Approve changes - одобрение изменений тем, у кого есть на это права.
- Merge; Issue Closed; CD Pipeline runs - после одобрения изменений проблема закрывается и приложение выкатывается в прод.
- Мониторинг - контроль приложения на предмет того, что изменения имеют желаемый эффект. В Гитлабе, если что, изменения можно откатить обратно.
Code Review Workflow
Дополнительные инструменты
- Snippets - хранение небольших кусков кода или текста и общий доступ к ним.
- Wiki - встроена в каждый проект Гитлаба. Используется, если не хочется хранить документацию к проекту в самом репозитории, но она должна быть близко. Её можно править как через веб-интерфейс, так и локально через git. Wiki является отдельным репозиторием.
- Web IDE - удобный встроенный редактор кода, можно работать прямо в браузере. Обзор новых функций.
Реестры
- Container registry - хранение образов контейнеров
- Package registry - хранение пакетов разных пакетных менеджеров и зависимостей.
Дополнительно: 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-тэг для пометки точки релиза в исходном коде.
Релиз включает:
- Снапшот исходного кода репозитория.
- Пакеты (generic packages), созданные из артефактов.
- Метаданные, ассоциированные с релиз-версией кода.
- Примечания к релизу.
Когда релиз создаётся, то
- Гитлаб автоматически архивирует исходный код и ассоциирует его с релизом.
- Гитлаб автоматически создаёт json-файл (release evidence), где перечислено всё, что входит в релиз, что нужно для сравнения релизов и их аудита. Можно использовать API для генерации release evidence, их может быть несколько.
После релиза можно
- Добавить примечания к релизу.
- Добавить сообщение к гит-тэгу, ассоциированному с релизом.
- Ассоциировать вехи (milestones) с релизом.
- Добавить прочие ресурсы (assets) типа инструкций по запуску и пакетов.
Creating a release
Release CI/CD examples
Release CLI tool
В Гитлабе есть несколько функций для удобства развертывания и теста релизов.
- Review apps - предпросмотр изменений, сделанных в ветке, с помощью развёртывания динамического окружения для мердж-реквеста. Youtube
- Feature flags - переключатели, позволяющие включить и выключать функции для тех или иных групп пользователей.
- Gitlab Pages - встроенный хостинг статических веб-страниц прямо из репозитория. Таким образом можно хостить портфолио, документацию, презентации и т. п. Youtube
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-.*$/
Задачи (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
- На сервере, куда будет ставиться приложение, сгенерить ключи SSH:
ssh-keygen -t ed25519 -C "dev-server"
- Засунуть закрытый ключ в переменную File на Гитлабе (в конце должна быть пустая строка!)
- Открытый ключ добавить на dev-server в
~/.ssh/authorized_keys
пользователя, под которым будет деплой. - Добавить сертификат git.ca.crt в доверенные, так же как на раннере:
nano /etc/ssl/certs/git.ca.crt # вставить содержимое сертификата с сервера
- В пайплайне примерно следующее
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
- Package registry - для пакетов поддерживаемых менеджеров пакетов (Maven, npm, NuGet,PyPI, Rube gems, etc)
- Container registry - для хранения контейнеров
- Terraform modules (Infrastructure registry) - пакеты IaC
Здесь:
- Gitlab был установлен из пакета (Omnibus GitLab installations)
- Тот же домен, порт 5050
- Сертификат самоподписанный
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
На раннере:
# Добавить сертификат 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 подхода к хранению кода
- Один репозиторий (monorepo) - для каждого микросервиса используется свой каталог. Минусы - опасность написания связанного кода между микросервисами, большой объём кода, сложность управления пайплайнами, одним коммитом можно поломать всё сразу. Подходит для маленьких проектов и одной команды.
- Несколько (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
Polyrepo
Подготовка:
- Создаётся группа, куда добавляются все нужные проекты. Удобство группы в том, что можно задавать переменные, раннеры и т. п. для всей группы, а не для каждого проекта.
- Раннер для группы регистрируется отдельно в настройках группы. Потом нужно в группе настроить переменную для закрытого ключа 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
указывает на ветку.
Т. к. репозиторий в той же группе, указывается группа/репозиторий
. Если бы он был вне группы, надо было бы указывать имя пользователя/репозиторий
.
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
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
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
Гитлаб предлагает сканеры безопасности:
- Static Application Security Testing (SAST) - обнаружение проблем в самом исходном коде, например, пользовательский ввод позволяет выполнить инъекцию команды. Демо
- Secret Detection - обнаружение вбитых паролей в исходном коде. Демо
- Dynamic Application Security Testing (DAST) - сканирование на уязвимости через вызов приложения или API. Демо
- Infrastructure-as-Code (IaC) Scanning - сканирование IaC-файлов (Ansible, Terraform) на уязвимости.
- Dependency Scanning - отчёт по уязвимостям зависимостей проекта по их версиям. Демо
- Container Scanning - сканирование докер-образов на уязвимости. Демо
- Fuzz Testing - шлёт на вход функций всякую ерунду на предмет неожиданных глюков. Демо
Отчёты / управление:
- Security Reports - сводный отчёт по уязвимостям. Демо
Прочее
Тестовый проект Gitlab from zero to hero
Example CI/CD Pipelines
Заметки о реальных проектах
Сервис личного кабинета в 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