GitLab CI ч.3
Раздельный CI и CD
Разделение Continuous Integration (CI) и Continuous Delivery (CD) на разные репозитории может иметь как плюсы, так и минусы, в зависимости от контекста и требований проекта. Вот некоторые преимущества разделения CI и CD репозиториев для развертывания:
Чистота и независимость: Раздельные репозитории для CI и CD могут помочь держать кодовую базу чистой и не загромождать ее скриптами и конфигурациями для развертывания.
Управление правами доступа: Разделение позволяет более точно настроить права доступа к репозиториям. Например, команда для CI может иметь более широкий доступ, чем команда для CD, что повышает безопасность.
Упрощение развертывания на сторонних платформах: Часто CI процессы могут быть настроены для запуска на сторонних CI-серверах, в то время как CD процессы могут выполняться напрямую на серверах развертывания, таким образом проще настроить CI/CD процессы для совместного использования.
Улучшение шаблонирования и переиспользования: Разделенные репозитории позволяют создавать шаблоны или библиотеки, которые могут быть повторно использованы между различными проектами для CI и CD, что облегчает поддержку и управление процессами.
Улучшение управления изменениями: Разделение CI и CD репозиториев помогает лучше контролировать изменения в процессах CI/CD и позволяет более гибко обновлять и адаптировать их без влияния на другие компоненты процесса.
Однако стоит отметить, что разделение CI и CD может привести к увеличению сложности и настройки в зависимости от требований проекта и командных процессов. Важно тщательно продумать и оценить плюсы и минусы разделения CI и CD репозиториев для конкретного проекта перед принятием данного решения.
Пайплайны развертывания
До этого мы заканчивали пайплайны публикацией Docker-образа, т. е. мы находились только в области CI: проверяли, что код работает, делали интеграции для упрощения процесса разработки, собирали и упаковывали наш код. После публикации образов (или других артефактов) в репозиторий нам остается лишь установить эти артефакты на целевые ВМ (или другие платформы запуска приложений), т. е. развернуть их в целевые окружения.
Для начала определимся с методом развертывания: очевидно что наши проекты counter-frontend и counter-backend сильно связаны, т.к. они реализуют клиентскую и серверную часть одного приложения, а их разделение на два отдельных репозитория — чисто организационное решение, т.к. они используют разный стек технологий и пишут их разные люди.
Если бы counter было нативным приложением, то обе эти части располагались бы в одном репозитории, но в разных модулях. Из этих соображений логично развертывать их вместе (в одном пайплайне), чтобы контролировать версии обоих проектов при развертывании.
Для начала создадим проект, содержащий конфигурацию развертывания, в своей группе. Назовите его counter-deploy и инициализируйте README.md. Мы вернемся к нему позднее.
Чтобы обезопасить себя от случайных или зловредных изменений, инфраструктуру, на которой работают промышленные версии (те к которым имеют доступ пользователи), отделяют от остальной инфраструктуры при помощи политик доступа, межсетевых экранов и т. д.
У нас в компании это реализуется при помощи размещения приложений в изолированных корпоративным межсетевым экраном (в простонародье — файрволлом) участках сети (так называемых DMZ сегментах) с предоставлением доступа минимально необходимому набору сотрудников.
Сейчас для сборки наших проектов мы пользуемся общим на весь сервер GitLab раннером, очевидно, если мы решим развертывать приложение тем же раннером, то мы лишимся как минимум одного слоя защиты — сетевого (забудьте на секунду, что все курсовые ВМ расположены в одной подсети), также при взломе соседнего проекта (получении push прав в репозиторий стороннего проекта) злоумышленник сможет получить доступ к раннеру целиком и, следовательно, к развертываниям всех проектов, которые пользовались этим раннером.
Из этих соображений будем пользоваться для развертывания отдельными раннерами, размещенными в том же окружении, что и приложение. Представим в нашем примере, что у нас одно окружение — промышленное, и оно расположено на ВМ2. Для самого развертывания воспользуемся Shell движком GitLab раннера (запускает задачи в обычном Shell на хосте, без контейнеров) и Docker Compose.
Настройка раннера
Сервис gitlab runner
Для начала установим раннер. Зайдем на ВМ2 и выполним следующие команды:
$ curl -L "https://packages.GitLab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
$ sudo apt install --yes gitlab-runner
Так как мы будем использовать раннер для работы с Docker, то добавим пользователя gitlab-runner в группу docker:
# usermod -aG docker gitlab-runner
Символ # перед командой означает, что ее нужно выполнить с правами суперпользователя.
GitLab раннер использует в качестве конфигов TOML файлы; по структуре они совпадают с JSON или YAML файлами. TOML файлы используют, т.к. они внешне имеют более плоскую структуру, что упрощает чтение конфигов с небольшим уровнем вложенности.
Напишем следующий шаблон конфига и сохраним его в /etc/gitlab-runner/template.toml:
concurrent = 1
[[runners]]
name = "Deploy runner"
url = "https://git.devops-teta.ru"
executor = "shell"
shell = "bash"
limit = 1
builds_dir = "/data/gitlab-runner/builds"
cache_dir = "/data/gitlab-runner/cache"
[runners.custom_build_dir]
enabled = true
Такой файл в YAML формате выглядит так:
concurrent: 1
runners:
- name: Deploy runner
url: https://git.devops-teta.ru
executor: shell
shell: bash
limit: 1
builds_dir: /data/gitlab-runner/builds
cache_dir: /data/gitlab-runner/cache
custom_build_dir:
enabled: true
Как видите, секции [name] — это ключи словарей, [[name]] — ключи коллекций (списков, элементами которых являются словари).
Смысл большинства опций понятен исходя из нашего опыта работы с GitLab CI, поясним некоторые:
limit — количество задач, которые одновременно будет выполнять этот раннер;
builds_dir, cache_dir — директории на хосте, где будут храниться склонированные проекты и кеши;
runners.custom_build_dir.enabled — разрешить ли использование опции GIT_CLONE_PATH, переопределяющей путь клонирования проекта.
Остальные опции смотри в документации.
Создадим указанные в шаблоне директории:
# mkdir -p /data/gitlab-runner/builds /data/gitlab-runner/cache
# chown -R gitlab-runner:gitlab-runner /data/gitlab-runner
# chmod -R 0700 /data/gitlab-runner
Теперь нужно зарегистрировать раннер на нашем сервере GitLab. Для этого откройте проект counter-deploy в GitLab, перейдите в Settings -> CI/CD -> Runners и скопируйте регистрационный токен (registration token), затем выполните команду на ВМ2 (подставьте вместо «$(cat token.txt)» ваш токен или сохраните токен в файл token.txt):
# gitlab-runner register \
--template-config /etc/gitlab-runner/template.toml \
--registration-token "$(cat token.txt)" \
--access-level ref_protected \
--tag-list 'deploy,prom' \
--run-untagged false
Здесь мы указали некоторые параметры раннера, хранящиеся на сервере GitLab:
--access_level ref_protected — использовать раннер только для задач, запускаемых в защищенных ветках или тегах (значение по умолчанию — not_protected);
--tag-list 'deploy, prom' — теги раннера, разделенные запятой;
--run-untagged false — не выполнять этим раннером задачи без тегов.
До этого в пайплайнах мы не сталкивались с понятием тэгов. Этот механизм служит для распределения задач между раннерами: в задаче GitLab СI (или в секции default) можно указать директиву tags, которая содержит список тегов.
Эта задача будет отправлена на раннер, когда он содержит все теги указанные в задаче. Если задача не содержит тегов, то она будет запущена на одном из раннеров, согласных запускать нетегированные задачи. Если же раннеров с нужным набором тегов нет, то задача не будет запущена и зависнет.
Здесь мы используем теги, чтобы задачи развертывания не запускались на общем сборочном раннере.
Снова откройте меню Settings -> CI/CD -> Runners в проекте counter-deploy и проверьте, что раннер зарегистрирован. Указанные нами при регистрации настройки можно изменить в этом же интерфейсе.
Про безопасность пайплайнов
Внимательный читатель заметит противоречие: мы разворачиваем отдельный Shell раннер из соображений безопасности, когда документация GitLab CI и другие авторитетные источники говорят, что Shell — самый небезопасный движок раннера и рекомендуют избегать его. Это правда и описанные здесь условия, когда раннер используется отдельным проектом для развертывания приложения на выделенную ВМ — одни из немногих, когда использование Shell раннера оправдано и относительно безопасно. Если вы планируете повторить описанный здесь подход (не стоит, это учебный пример), то дополнительно ограничьте доступ к репозиторию развертывания и его пайплайнам:
- предоставьте доступ к репозиторию развертывания минимально необходимому числу людей;
- переопределите источник пайплайна в настройках проекта, вместо включения переиспользуемого пайплайна в. gitlab-ci.yml (см. документацию);
- ссылайтесь на фиксированную версию пайплайна из защищенного тега; удостоверьтесь, что права на изменение защищенных тегов в репозитории с пайплайнами есть только у тех людей, у которых есть права на изменение репозитория развертывания;
- если изменения в вашем репозитории развертывания выполняются ботами (например после сборки новой версии приложения бот загружает в репозиторий развертывания ссылку на новый образ), то распространите доступ к пайплайнам на все связанные проекты.
Пайплайн развертывания
Теперь напишем сам пайплайн развертывания. Склонируйте counter-deploy и создайте файл .gitlab-ci.yml, затем скопируйте в него скелет пайплайна:
stages:
- deploy
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_PROJECT_PATH_SLUG
COMPOSE_PROJECT_NAME: $CI_PROJECT_PATH_SLUG
default:
before_script:
- set -eu
Deploy - Prom:
tags:
- deploy
- prom
script:
- echo 1
Во-первых, мы здесь переопределили GIT_CLONE_PATH, чтобы гарантировать постоянный путь для склонированного проекта (для монтирования локальных конфигов в контейнеры).
Во-вторых, мы явно задали имя проекта Docker Compose переменной COMPOSE_PROJECT_NAME. В-третьих, как уже было сказано выше, мы используем теги, чтобы явно указать раннер, использующийся для выполнения этой задачи.
Ключевое отличие задач развертывания от сборочных в том, что они изменяют состояние внешних систем, составляющих окружение развертывания. GitLab CI позволяет отслеживать число и состояние таких окружений при помощи механизма с одноименным названием:
Deploy - Prom:
# ...
environment:
name: prom
deployment_tier: production
Здесь директива environment указывает, что задача выполняет развертывание в окружение с именем prom (промышленное) и это окружение имеет класс (deployment_dier) — промышленное (production). При указании в задаче окружения GitLab начнет отслеживать все развертывания в это окружение и будет отображать в интерфейсе коммит, содержащий последнее состояние этого окружения.
Также GitLab позволяет определять переменные CI для конкретного окружения при настройке проектных/групповых переменных. Таким образом мы можем задать разные параметры запуска для одного и того же пайплайна в разных окружениях (аналогично заданию разных параметров для роли Ansible в двух инвентарях).
Наполним задачу Deploy — Prom:
Deploy - Prom:
tags:
- deploy
- prom
environment:
name: prom
deployment_tier: production
script:
- docker-compose up --remove-orphans --detach
Создадим конфиги развертывания:
Конфигурация Docker Compose docker-compose.yml:
services:
frontend:
image: counter-frontend:latest
backend:
image: counter-backend:latest
nginx:
image: nginx:1.20
volumes:
- ./files/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- 8080:8080/tcp
Конфигурация Nginx files/default.conf:
server {
listen 8080 _ default_server;
location / {
proxy_pass http://frontend:8080;
}
location /api {
proxy_pass http://backend:8080;
}
}
Такая конфигурация работает, но она не расширяема. Добавим в репозиторий поддержку dotenv файлов для различных окружений:
Deploy - Prom:
stage: deploy
tags:
- deploy
- prom
environment:
name: prom
deployment_tier: production
script:
- |-
for script in $(find 'env' "env/$CI_ENVIRONMENT_NAME" -maxdepth 1 -type f -name '*.env'); do
printf >&2 'reading environment variables from %s\n' "$script"
eval "$(sed -e 's/^/export /' <"$script")"
done
- docker-compose up --remove-orphans --detach
И перенесем имена образов в отдельные dotenv файлы:
env/prom/frontend.image.env:
FRONTEND_IMAGE="counter-frontend:latest"
env/prom/backend.image.env:
BACKEND_IMAGE="counter-backend:latest"
Изменим docker-compose.yml так, чтобы имена образов подставлялись из переменных окружения:
services:
frontend:
image: $FRONTEND_IMAGE
backend:
image: $BACKEND_IMAGE
nginx:
image: nginx:1.20
volumes:
- ./files/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- 8080:8080/tcp
Задача развертывания должна запускаться только при коммите в ветку по-умолчанию, добавим соответствующие правила:
Deploy - Prom:
# ...
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
И приведем пайплайн целиком:
stages:
- deploy
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_PROJECT_PATH_SLUG
COMPOSE_PROJECT_NAME: $CI_PROJECT_PATH_SLUG
HARBOR_HOST: harbor.devops-teta.ru
HARBOR_USER: robot_demo+gitlab
HARBOR_PASSWORD: $__HARBOR_PASSWORD
default:
before_script:
- set -eu
Deploy - Prom:
stage: deploy
tags:
- deploy
- prom
environment:
name: prom
deployment_tier: production
script:
- docker login -u $HARBOR_USER -p $HARBOR_PASSWORD $HARBOR_HOST
- |-
for script in $(find 'env' "env/$CI_ENVIRONMENT_NAME" -maxdepth 1 -type f -name '*.env'); do
printf >&2 'reading environment variables from %s\n' "$script"
eval "$(sed -e 's/^/export /' <"$script")"
done
- docker-compose up --remove-orphans --detach
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
При успешном выполнении пайплайна, у нас запустятся 3 контейнера с нашим приложением на нашем сервере, где установлен docker runner.
Интеграция репозитория кода и репозитория деплоя
Несложно заметить, что наш репозиторий деплоя никоим образом не интегрирован с репозиториями counter-frontend и counter-backend. Нам предстоит это исправить. Наша задача состоит в том, что бы обновлять переменные в репозитории деплоя.
Для этого нам необходимо организовать дополнительный job в пайплайне приложений, который будет обновлять наш репозиторий деплоя. Приступим.
Доработка скриптов развертывания
Изменим название переменных в шаблоне docker-compose.yml. При написании шаблонов очень удобно опираться на неизменяемые и легко определяемые сущности. В нашем примере это название репозиториев counter-frontend и counter-backend.
services:
frontend:
image: $counter_frontend_IMAGE
backend:
image: $counter_backend_IMAGE
nginx:
image: nginx:1.20
volumes:
- ./files/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- 8080:8080/tcp
Создаем ключ для доступа к репозиторию counter-deploy
В репозиториях counter-backend и counter-frontend мы будем использовать скрипт для автоматизации нашего репозитория деплоя. Для доступа к репозиторию в гитлаб, отличных от источника, запустившего задачу на runner, необходим токен для доступа. Создадим его в репозитории counter-deploy:
Сохраним полученный токен в переменных группы нашего проекта.
Создаем задачу с git push в репозиториях с кодом
Воспользуемся нашим репозиторием шаблонов templates. Создадим новый скрипт-шаблон, который будем использовать в counter-frontend и counter-backend.
Сначала доработаем текущую последнюю джобу Publish Package. Ведь именно там есть нужная нам информация о текущем собранном имени образа.
jobs/kaniko.yml
include:
- local: jobs/git_strategy.yml
variables:
IMAGE_TAG: ""
IMAGE_OUTPUT_DOTENV_KEY: $CI_PROJECT_NAME
IMAGE_OUTPUT_DOTENV_FILE: $CI_PROJECT_NAME
.job__kaniko_publish_image:
extends:
- .job__shallow_clone
stage: publish
needs:
- job: Build Package
artifacts: true
interruptible: false
image:
name: $KANIKO_IMAGE
entrypoint: [""]
script:
- b64_auth=$(printf '%s:%s' "$HARBOR_USER" "$HARBOR_PASSWORD" | base64 | tr -d '\n')
- >-
printf '{"auths": {"%s": {"auth": "%s"}}}' "$HARBOR_HOST" "$b64_auth"
>/kaniko/.docker/config.json
- >-
/kaniko/executor
--cache
--use-new-run
--skip-unused-stages
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/Dockerfile"
--destination "$HARBOR_IMAGE:$IMAGE_TAG"
--cache-repo "$HARBOR_IMAGE/cache"
# добавленный скрипт
- >-
if [ -n "${IMAGE_OUTPUT_DOTENV_KEY:-}" ]
; then printf '%s=%s' "${IMAGE_OUTPUT_DOTENV_KEY//-/_}_IMAGE" "$HARBOR_IMAGE:$IMAGE_TAG" >"$IMAGE_OUTPUT_DOTENV_FILE"
; fi
artifacts:
paths:
- $IMAGE_OUTPUT_DOTENV_FILE
Записываем в файл имя образа и тег. И упаковываем этот файл в артефакт для передачи в другую джобу. Его то мы и должны передать в наш counter-deploy.
${IMAGE_OUTPUT_DOTENV_KEY//-/_}_IMAGE — имя переменной указанной в docker-compose
$IMAGE_OUTPUT_DOTENV_FILE — имя файла, который мы передаем в следующую джобу, может быть произвольным.
jobs/dotenv_push.yml:
include:
- local: jobs/git_strategy.yml
- local: variables/dotenv_vars.yml
.job__dotenv_push:
stage: publish
image:
name: $KANIKO_IMAGE
entrypoint: [""]
script:
- export PUSH_DOTENV_TOKEN=$__DEPLOY_TOKEN # Используем токен созданный для counter-deploy
- cat $CI_PROJECT_NAME # Проверяем, что файл передался и содержит нужные нам данные
- git clone --depth 1 --branch "$PUSH_DOTENV_PROJECT_BRANCH" "https://ci-bot:$PUSH_DOTENV_TOKEN@$CI_SERVER_HOST/${PUSH_DOTENV_PROJECT_PATH}.git" $PUSH_DOTENV_TARGET_DIR # Клонируем репозиторий counter-deploy
- git -C $PUSH_DOTENV_TARGET_DIR config user.name ci-bot # Настраиваем обязательные парамметры для git commit
- git -C $PUSH_DOTENV_TARGET_DIR config user.email ci-bot@git.devops-teta.ru # Настраиваем обязательные парамметры для git commit
- ls -l $PUSH_DOTENV_TARGET_DIR/$PUSH_DOTENV_VARS_PATH
- PUSH_DOTENV_TARGET_PATH="$PUSH_DOTENV_TARGET_DIR/$PUSH_DOTENV_VARS_PATH/${CI_PROJECT_NAME//-/_}.env" # Определяем путь до файла с переменной, которую нам необходимо изменить.
- cp $CI_PROJECT_NAME $PUSH_DOTENV_TARGET_PATH
- IFCHANGED=$(git -C $PUSH_DOTENV_TARGET_DIR status --porcelain) # Проверяем, было ли изменение или запущен текущий коммит/тег.
- if [ ! -n "${IFCHANGED:-}" ]; then echo "Not changed"; exit 0; fi
- git -C $PUSH_DOTENV_TARGET_DIR add .
- git -C $PUSH_DOTENV_TARGET_DIR commit -m "$CI_COMMIT_MESSAGE"
- git -C $PUSH_DOTENV_TARGET_DIR push origin "HEAD:$PUSH_DOTENV_PROJECT_BRANCH" #делаем коммит и пуш
extends:
- .job__no_clone # используем no_clone, т.к. никакие исходные коды приложения на данном этапе нам не нужны. Экономит время выполнения.
Разберем некоторые моменты нашей новой задачи.
PUSH_DOTENV_TARGET_PATH — Определяем путь до файла с переменной, которую нам необходимо изменить. Именно тут мы ссылаемся на имя репозитория в котором запущена джоба. Таким образом обеспечивается и уникальность файлов внутри counter-deploy и постоянность при сборке одного и того же проекта.
Также в задаче есть условие, если файл не изменился — значит изменения образа приложения не было. Чтобы попусту не запускать задачу деплоя и не выполнять ненужную итерацию скриптов на «боевом» сервере, просто выходим и не продолжаем работу.
Мы объявили много переменных, порах их заполнить.
variables/dotenv_vars.yml:
variables:
PUSH_DOTENV_PROJECT_BRANCH: main
PUSH_DOTENV_PROJECT_PATH: $CI_PROJECT_NAMESPACE/counter-deploy
PUSH_DOTENV_TARGET_DIR: cloned_repo
PUSH_DOTENV_VARS_PATH: env/prom
Добавим нашу новую джобу в пайплайн для frontend.
jobs/nextjs_standalone_docker.yml:
include:
- local: jobs/dotenv_push.yml
{...}
Deploy Job:
stage: deploy
extends:
- .job__dotenv_push
needs:
- job: Publish Package
artifacts: true
rules:
- !reference [.rule__on_default_branch]
- !reference [.rule__on_release_tag]
Если все сработало как мы ожидали, теперь у нас работает цепочка пайплайнов. Коммит в код после сборки приложения и публикации докер образа в harbor, обновляет файл с переменной в counter-deploy. После чего counter-deploy запускает задачу на обновление стенда.
Основные способы ускорения выполнения задач CI/CD в пайплайнах
Ранее в курсе мы делали фокус на построение пайплайна оптимизируя его. Пайплайн запускается часто, и время его работы существенно. Хотелось бы ещё раз проговорить основные оптимизации, которые влияют на ускорение выполнения задач нашего пайплайна.
Оптимизация зависимостей устанавливаемых в базовые Docker образы
Slim образы — это образы, в которых присутствует минимальное количество пакетов и, в первую очередь, такие образы предназначены для запуска написанных программ. В качестве примера можно привести образ node: slim, в котором отсутствует компилятор.
Alpine образы — это образы, которые содержат в себе одноименную операционную систему, разработанную специально для запуска внутри контейнера. Легковесность Alpine объясняется тем, что в данном дистрибутиве не используются привычные функции, которые доступны в других дистрибутивах Linux, такие как пакетные менеджеры apt/yum/dnf, система инициализации systemd, а также существенно сокращён список используемых стандартных утилит.
У многих разработчиков велик соблазн использовать стандартный slim образ, в который по мере надобности устанавливаются нужные инструменты во время выполнения пайплайнов. Чаще всего это ведет к тому, что одни и те же компоненты скачиваются и устанавливаются несколько раз. Сам по себе процесс выкачивания и установки зависимостей занимает большевремени, чем скачивание и подгрузка подготовленного образа. Таким образом необходимо использовать заранее подготовленный образ, заточенный для конкретной задачи и содержащий в себе минимальный набор требуемых зависимостей и библиотек. Если такого базового образа нет, мы можем его создать сами.
Оптимизация образов Docker
Это обратная оптимизация. Не нужно в один и тот же образ добавлять все возможные инструменты, которые могут потребоваться для выполнения задач (как на практике, так и в теории). Это в свою очередь приводит к разрастанию образа до гигантских размеров. Хотя это упрощает процесс написания пайплайнов (автору не требуется выбирать и подготавливать образ), но ведет к снижению эффективности их выполнения. Чем меньше наш образ, тем быстрее CI задача.
- Используем базовые slim образы (но смотрим на зависимости выше).
- Избегаем установки пользовательских инструментов (vim, curl, и т. д.).
- Отключаем установку man-страниц и прочей документации.
- Минимизируем количество слоев RUN (комбинируя команды в один слой)
- Используйте мультисборки
- Если используем apt, то отключаем установку ненужных зависимостей при помощи ключа --no-install-recommends
- Не забываем почистить кэш (например, rm -rf /var/lib/apt/lists/* для Debian)
- Такие инструменты как dive или DockerSlim могут помочь с дальнейшей оптимизацией
Docker кэш при сборке образов
При выполнении команды docker build, сборка слоев производятся с нуля. Использование ключа --cache-from с указанием образов, которые послужат источником кэша, может значительно ускорить сборку. Мы также можем передать процессу несколько аргументов --cache-from, задействовав таким образом несколько образов.
#.gitlab-ci.yml
---
build:
stage: build
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
Локальное кэширование Docker образов
Где хранятся базовые образы и как добиться минимизации задержки сети при работе с ними? GitLab содержит в себе Container Registry Dependency Proxy, который умеет проксировать и кэшировать образы. Таким образом GitLab может использоваться как pull-through сервис. В зависимости от нашей сети и того, где располагаются GitLab раннеры, подобное кэширование может значительно ускорить процесс запуска CI задач. Использование Dependency Proxy так же позволяет обойти ограничения количества запросов на Docker Hub в «100 скачиваний».
Для использование этой возможности нам необходимо:
- Включить функционал на уровне группы (Settings > Packages & Registries > Dependency Proxy > Enable Proxy)
- Добавить префикс ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX} к имени образа в. gitlab-ci.yml
# .gitlab-ci.yml
---
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:latest
Ну или мы можем использовать специально подготовленные для этого корпоративные зеркала.
Политики скачивания Docker образов
При настройке собственных раннеров мы можем указать политику скачивания при помощи параметра pull_policy (делается это в конфигурационном файле config. toml). Этот параметр определяет то, как раннер будет подгружать требуемые образы из регистра.
Возможные значения:
- always (дефолтное значение): образы каждый раз подгружаются из удаленного регистра.
- never: образы вообще не выгружаются из удаленного регистра, а должны быть вручную закэшированы на Docker хосте.
- if-not-present: раннер сначала проверит локальный кэш и лишь при отсутствии искомого образа скачает его из внешнего регистра.
Значения if-not-present может сократить задержки на скачивание и анализ слоев за счет использования локального кэша, а, значит, ускорить запуск и выполнение задач. Однако если образ часто меняется, то появляется быстрорастущий кэш, который необходимо регулярно чистить, что может свести все выигрыши по времени на нет.
# config.toml
---
[runners.docker]
pull_policy = "if-not-present"
Файл .dockerignore
В данном файле можно указывать, какие файлы и директории не нужно включать в итоговую сборку образа, а следовательно и копировать. Файл. dockerignore (имя файла начинается с символа точки, так как этот файл является скрытым) помещается в корневую папку с проектом и располагается там же, где и файл Dockerfile.
Кэширование CI/CD
Кэш в GitLab CI — мощный и гибкий инструмент для оптимизации работы пайплайнов. Наверно, одним из самых часто встречаемых и популярных примеров его использования является кэширования зависимостей (.npm/, node_modules, .cache/pip, .go/pkg/mod/, и т. д.).
Ранее мы уже пользовались им в курсе:
Build Package:
# ...Остальная часть
cache:
- key: npm-packages
paths:
- .cache
unprotect: true
- key:
prefix: npm-node-modules
files:
- package-lock.json
paths:
- node_modules
unprotect: true
- key: npm-next-cache
paths:
- .next/cache
---
Важной особенностью является то, что CI/CD кэш может быть как локальным (файлы остаются на хосте, на котором запушен раннер), так и распределенным (кэш в виде архива сохраняется в S3 хранилище). Это позволяет оптимизировать работу пайплайнов даже в том случае, если у вас нет выделенных раннеров или если они создаются динамически, а значит является эффективным решением как для собственных, так и для публичных (shared) раннеров
Политика кэширования CI/CD
Использование механизмов кэширования, описанных выше — это хорошо. Однако многие забывают о дополнительных возможностях оптимизации за счёт использования правильной политики. Дело в том, что при стандартной конфигурации кэш скачивается в начале выполнения CI задачи и загружается обратно в конце. При большом размере кэша и медленных сетях это может стать проблемой. В курсе мы уже конфигурировали этот процесс можно за счет параметра cache: policy.
- push-pull (стандартное поведение): кэш скачивается в начале и загружается обратно в конце выполнения задачи
- pull: кэш скачивается в начале выполнения задачи, но не загружается в конце
- push: кэш не скачивается, но загружается в конце выполнения задачи.
Таким образом мы можем оптимизировать продолжительность выполнения задачи за счет использования политики pull для некоторых задач
Lint
# ...Остальная часть
cache:
- key: npm-packages
paths:
- .cache
unprotect: true
policy: pull
---
Изменение уровня компрессии
Все артефакты и кэш, требуемые для выполнения задач, передаются в сжатом виде. Это означает, что архивы должны разжиматься в начале выполнения задачи и сжиматься в её конце. GitLab Позволяет выбирать желаемый уровень компрессии для этого процесса (fastest, fast, default, slow, slowest). Конфигурация производится за счёт использования переменных окружения и может выполняться как для всего пайплайна, так и для индивидуальных задач. Необходимо также включить feature flag FF_USE_FASTZIP.
# .gitlab-ci.yml
variables:
# Enable feature flag
FF_USE_FASTZIP: "true"
# применимо для задачи или пайплайна
ARTIFACT_COMPRESSION_LEVEL: "fast"
CACHE_COMPRESSION_LEVEL: "fast"
Стратегии git клонирования
В начале выполнения любой задачи происходит клонирование git репозитория. Влиять на это можно за счет конфигурирования стратегии git клонирования при помощи переменной окружения (GIT_STRATEGY).
Доступные значение GIT_STRATEGY:
- none: репозиторий не клонируется вообще
- fetch: с использованием локальной рабочей копии (обычно быстрее, особенно на выделенных раннерах)
- clone: без использования локальной рабочей копии
Ранее в курсе мы уже использовали fetch и clone
# jobs/git_strategy.yml
.job__shallow_clone:
variables:
GIT_STRATEGY: fetch
GIT_DEPTH: 20
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_PROJECT_PATH_SLUG
.job__no_clone:
variables:
GIT_STRATEGY: none
Дополнительные возможности GitLab CI
В курсе мы так же использовали некоторые оптимизации задач, вспомним о них:
rules: директива помогает запускать задачи только тогда, когда они нужны, а так же модифицировать их поведение.
needs: директива позволяет построить пайплайны таким образом, что задачи не будут ожидать завершения предыдущей стадии, а запустятся в тот момент, когда все зависимости будут удовлетворены (оптимизируя таким образом общее время выполнения)
interruptible: задачи, помеченные как прерываемые, могут быть автоматически отменены при запуске нового пайплайна на той же ветке кода. Позволяет свести на нет выполнение ненужных задач
Использование собственных раннеров
Использование собственных раннеров бесспорно влияет на ускорение CI/CD задач в пайплайнах.
Домашнее задание
- Доработайте шаблон counter-backend для передачи имени образа в counter-deploy.
- Перепишите пайплайн в counter-deploy на использование шаблона.
Для проверки предоставьте:
- ссылки на успешно отработанные пайплайны в counter-backend и counter-deploy.
- ссылки на шаблон для counter-deploy.