Очистка GitLab CI/CD runner с помощью сценариев предварительной сборки

Введение

GitLab CI/CD имеет мощную, но несколько недокументированную функцию сценария предварительной сборки, которая позволяет нам выполнять пользовательскую логику перед запуском сборки на GitLab runner. В этой статье мы рассмотрим, как использовать сценарий предварительной сборки для автоматизации очистки системы Docker на бегунах GitLab.

Бегуны GitLab и проблема автоматизированной очистки

Представьте, что мы работаем над проектом с использованием GitLab CI/CD с набором сложных критериев, таких как

  • высокая степень автоматизации,
  • необходимость высокого времени работы бегунов,
  • быстрое выполнение сборки и
  • частое использование Docker.

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

К сожалению, найти общее решение для очистки бегущей строки GitLab не так просто, о чем свидетельствует существование этой, до сих пор не решенной проблемы на трекере проблем бегущей строки GitLab. Например, если мы просто очистим все ресурсы Docker после каждой сборки, мы, скорее всего, не исчерпаем дисковое пространство. Однако время сборки будет намного больше, поскольку Docker не сможет использовать свой механизм кэширования сборок.

Между тем, документация GitLab рекомендует запускать скрипт clear-docker-cache раз в неделю через cron в качестве обходного пути. Использование подхода cron также довольно просто и, кроме того, позволит реже замедлять сборку. Однако, с другой стороны, теперь нам придется обеспечивать наших исполнителей достаточным дисковым пространством на целую неделю (или на любой интервал, на который настроено задание cron), что может оказаться чрезмерным и, кроме того, трудно угадать правильно.

Как отмечено в нерешенном вопросе, о котором я говорил ранее, предложенный GitLab способ управления дисковым пространством также имеет еще как минимум две проблемы:

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

Это проблематично по нескольким причинам. Во-первых, другие кэшированные ресурсы Docker — тома и контейнеры — не попадают под действие сценария очистки. Кроме того, это может привести к замедлению сборки, поскольку часто используемые образы очищаются и должны быть созданы заново. Наконец, поскольку скрипт запускается с помощью cron-, возникает сложная проблема условий гонки между скриптом очистки и заданиями сборки: поскольку cron работает асинхронно с выполнением заданий сборки, наше задание очистки может непреднамеренно нарушить конвейеры, поскольку оно очищает образы, от которых зависят некоторые задания. Например, если мы собираем наши образы и отправляем их в реестр в отдельных заданиях]. Конечно, эту проблему можно решить, запуская сценарий очистки только раз в неделю, но разработчикам придется помнить, что конвейеры могут время от времени давать сбой из-за отсутствия образов.

Определение использования диска Docker и очистка кэша Docker

Итак, что же делать? Прежде всего, давайте рассмотрим, какие инструменты доступны для определения использования диска Docker, а также для запуска очистки ресурсов, занимающих дисковое пространство.

Чтобы получить представление о текущем использовании диска Docker, мы можем запустить docker system df. Вот пример вывода с моей локальной машины:

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          2         2         216.6MB   0B (0%)
Containers      2         2         84.83kB   0B (0%)
Local Volumes   5         5         506.7MB   0B (0%)
Build Cache     0         0         0B        0B
Вход в полноэкранный режим Выход из полноэкранного режима

Как мы видим, Docker помогает перечислить дисковое пространство, занимаемое каждым из классов ресурсов.

С помощью легкой акробатики BASH мы можем использовать это в тесте, который сообщает нам, что использование дискового пространства Docker ниже заданного предела (см. is_docker_disk_space_usage_above_limit.sh).

#!/bin/bash
# --
# Test if Docker daemon disk space usage is above a given limit in bytes.
#
# Example 1: Docker disk space usage is above limit 
#
#    $ source is_docker_disk_space_usage_above_limit.sh
#    $ is_docker_disk_space_usage_above_limit 1
#    Docker disk space usage is above limit (actual: 1050000005B, limit: 1B)
#    $ printf $?
#    0
#
# Example 2: Docker disk space usage is below or equal to limit
#
#    $ source is_docker_disk_space_usage_above_limit.sh
#    $ is_docker_disk_space_usage_above_limit 1000000000000
#    Docker disk space usage is below or equal to limit (actual: 1050000005B, limit: 1000000000000B)
#    $ printf $?
#    1
# 
# --
iec_string_to_bytes() {
  local iec_string=$1
      iec_format_pattern='([0-9.]+)s*([kMGTP]?B)'

  if ! [[ "${iec_string}" =~ ${iec_format_pattern} ]]; then
    printf "Input string has invalid format (received: "%s", expected: "%s")." "$1" "${iec_format_pattern}"
    return 1
  fi 

  local number_value="${BASH_REMATCH[1]}"
    iec_unit="${BASH_REMATCH[2]}"
    factor=""

  case "${iec_unit}" in 
    B) factor=1;;
    kB) factor=1000;;
    MB) factor=1000000;;
    GB) factor=1000000000;;
    TB) factor=1000000000000;;
    PB) factor=1000000000000000;;
  esac

  # We use scale=0 here to drop the (redundant) decimal points.
  # This only works with division so we divide by one.
  printf "scale=0;%s * %s/1n" "${number_value}" "${factor}" 
    | bc 
}

calculate_docker_total_disk_space_usage() {
  local bc_expression="0"
    disk_space_used="$(docker system df --format='{{.Size}}' | tr 'n' ' ')"

  for disk_space_used_by_resource in ${disk_space_used}; do 
    # shellcheck disable=SC2086
    disk_space_used_by_resource_bytes="$(iec_string_to_bytes "${disk_space_used_by_resource}")"
    bc_expression="${bc_expression} + ${disk_space_used_by_resource_bytes}"
  done 

  printf "%sn" "${bc_expression}" 
    | bc
}

is_docker_disk_space_usage_above_limit() {
  local disk_space_limit=$1
    docker_disk_space_used="$(calculate_docker_total_disk_space_usage)"
    docker_disk_space_usage_is_above_limit="$(printf '%s > %sn' ${docker_disk_space_used} ${disk_space_limit} | bc -l)"

  # Note that bc returns 1 if the comparison is true and 0 otherwise.
  if [ "${docker_disk_space_usage_is_above_limit}" -eq 1 ]; then 
    printf "Docker disk space usage is above limit (actual: %sB, limit: %sB)" "${docker_disk_space_used}" "${disk_space_limit}"
    return 0
  fi 

  printf "Docker disk space usage is below or equal to limit (actual: %sB, limit: %sB)" "${docker_disk_space_used}" "${disk_space_limit}"
  return 1
}

Войти в полноэкранный режим Выход из полноэкранного режима

Схематично мы можем запустить этот скрипт в BASH следующим образом, чтобы запустить логику очистки:

# Check if docker disk space usage is above a given limit and run clean-up logic if it is
if is_docker_disk_space_usage_above_limit "${docker_disk_space_usage_limit}"; then
  # run clean up
fi
Войти в полноэкранный режим Выйти из полноэкранного режима

Если Docker использует слишком много дискового пространства, мы можем удалить неиспользуемые ресурсы через Docker CLI. Самый простой способ сделать это — запустить docker system prune -af --volumes. Обрезка системы с этими параметрами просто очистит все висящие и неиспользуемые образы, контейнеры, сети, а также тома.

Если нам нужна более сложная логика очистки, в Docker CLI также есть отдельные команды для освобождения кэша от неиспользуемых образов, контейнеров, сетей и томов, которые поддерживают фильтры. Например, команду docker image prune можно использовать для очистки только тех образов, которые старше 24 часов.

docker image prune -a --force --filter "until=24h"
Войти в полноэкранный режим Выйти из полноэкранного режима

Хуки бегунка на помощь

Теперь, когда у нас есть способ, как узнать, что у Docker заканчивается дисковое пространство, и как запустить очистку, мы можем подумать о том, когда запускать нашу логику. Согласно требованиям нашей CI/CD-настройки GitLab — как было сказано ранее — мы хотим запустить

  • пользовательскую логику очистки,
  • с упреждением (чтобы избежать сбоев в работе бегунов из-за нехватки дискового пространства), и
  • синхронно с выполнением конвейера (для предотвращения случайных сбоев конвейера).

К счастью, GitLab CI/CD предоставляет пару скриптовых крючков, которые позволяют нам выполнять код на различных этапах выполнения конвейера. В частности, pre-clone, post-clone, pre-build и post-build (см. раздел [[runners]] в Advanced Configuration).

Как предварительная, так и последующая сборка имеют смысл в нашем сценарии, так как они оба

  • выполняются синхронно с заданиями конвейера (либо до, либо после задания),
  • упреждающе позволяют нам очищать ресурсы (либо перед текущим заданием, либо перед следующим заданием), и
  • предоставляют механизм для определения пользовательской логики очистки.

Здесь мы выбрали крючок предварительной сборки.

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

# /etc/gitlab-runner/config.toml
# ...
[[runners]]
  # ...
  pre_build_script = '''
    # execute clean-up script
  '''
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

Быстрый тест-драйв

Установка Docker и gitlab-runner

Для проверки нашей установки мы установим и настроим GitLab runner на экземпляре AWS EC2. (Очевидно, что подойдет и любое другое аналогичное облачное решение «инфраструктура как сервис»). Двоичные файлы GitLab runner доступны для нескольких платформ. Мы выбрали машину Ubuntu 20.4, чтобы использовать двоичные файлы Linux.

После входа в наш экземпляр EC2

ssh -i "${key-pair-pem-file}" "${ec2-user}@${ec2-instance-address}"
Войдите в полноэкранный режим Выйти из полноэкранного режима

мы сначала хотим установить Docker. Например, используя официальный удобный скрипт установки

curl -fsSL https://get.docker.com -o /home/ubuntu/get-docker.sh
sudo sh /home/ubuntu/get-docker.sh
Войти в полноэкранный режим Выйти из полноэкранного режима

⚠️ Обратите внимание, что использование удобного скрипта не рекомендуется для производственных сборок. Также не рекомендуется выполнять загруженный файл скрипта с правами sudo. Но поскольку здесь мы доверяем источнику и запускаем только тест, это не имеет большого значения.

Проверьте успешность установки Docker, выполнив, например, docker --version.

$ docker --version
Docker version 20.10.16, build aa7e414
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Теперь выполним скрипт, показанный ниже, для установки gitlab-runner. ^[Сценарий установки GitLab runner также доступен в Settings > CI/CD > Runners > Specific runners of a GitLab project for reference].

sh <<EOF
  # Download the binary for your system
  sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

  # Give it permission to execute
  sudo chmod +x /usr/local/bin/gitlab-runner

  # Create a GitLab Runner user
  sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

  # Install and run as a service
  sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
  sudo gitlab-runner start
EOF
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем давайте зарегистрируем наш бегунок в GitLab, выполнив команду

sudo gitlab-runner register 
Войти в полноэкранный режим Выйти из полноэкранного режима

Инструмент gitlab-runner проведет нас через весь процесс и попросит ввести некоторые детали конфигурации. Мы должны ввести URL нашего экземпляра GitLab (например, https://gitlab.com для публичного GitLab) и регистрационный токен (который можно скопировать из Settings > CI/CD > Runners > Specific runners). Кроме того, мы выбираем Docker executor (docker), поскольку он, по крайней мере, по моему опыту, является наиболее распространенным, а также образ docker:20.10.16, чтобы иметь возможность запускать сборки Docker в рамках наших конвейерных заданий. ^[Полное раскрытие информации, я попробовал использовать SSH-исполнитель для простоты, но не смог заставить его работать из-за проблем с соединением]. Также при запросе тегов мы вводим gl-cl, чтобы иметь возможность запускать наши конвейерные задания именно на этой машине.

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

Runner зарегистрирован успешно. Не стесняйтесь запускать его, но если он уже запущен, конфигурация должна быть автоматически перезагружена!

Как описано в разделе Расширенная конфигурация, файл конфигурации хранится в /etc/gitlab-runner/config.toml на Unix системах. Мы добавим свойства runners.pre_build_script и runners.docker.volumes, показанные ниже.

# /etc/gitlab-runner/config.toml
# ...
[[runners]]
  # ...
  pre_build_script = '''
    sh $CLEAN_UP_SCRIPT     
  '''
  # ...
  [runners.docker]
  # ...
  volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
Вход в полноэкранный режим Выход из полноэкранного режима

Свойство pre_build_script использует небольшой трюк и просто выполняет ссылку на путь к файлу скрипта CLEAN_UP_SCRIPT, который мы позже добавим как переменную GitLab CI/CD типа file. Таким образом, мы сможем применять изменения и тестировать наш сценарий очистки без необходимости подключения к нашему runner. Свойство volumes монтирует сокет Docker хост-машины в нашем контейнере, поэтому мы очищаем ресурсы на хост-машине, а не только в контейнере (Docker в Docker через привязку сокета Docker).

Настройка нашего конвейера GitLab CI/CD

Чтобы это работало, файловая переменная CLEAN_UP_SCRIPT должна быть определена в разделе Settings > CI/CD > Variables нашего проекта. Как показано в следующем разделе. Давайте пока добавим следующий сценарий очистки.

set -eo pipefail
apk update 
apk upgrade 
apk add bash curl
curl https://gist.githubusercontent.com/fkurz/d84e5117d31c2b37a69a2951561b846e/raw/a39d6adb1aaede5df2fc54c1882618bcea9f01e0/is_docker_disk_space_usage_above_limit.sh > /tmp/is_docker_disk_space_above_limit.sh
bash <<EOF || printf "nClean-up failed."
  source /tmp/is_docker_disk_space_above_limit.sh
  if is_docker_disk_space_usage_above_limit 2000000000; then
    printf "nRunning clean up...n"
    docker system prune -af --volumes
  else 
    printf "nSkipping clean up...n"
  fi 
EOF
Вход в полноэкранный режим Выйти из полноэкранного режима

Убедитесь, что при определении переменной в качестве типа выбран файл.

Обратите внимание, что для простоты мы устанавливаем bash и curl в сценарии предварительной сборки. Это подразумевает, что оба инструмента будут установлены перед каждым заданием, которое обрабатывается на этом runner. В реальном сценарии мы, естественно, захотим предоставить пользовательский образ, в котором будут установлены все необходимые инструменты, чтобы ускорить выполнение сценария предварительной сборки.

Теперь добавим в наш проект образец gitlab-ci.yaml, который создает большой образ размером в один гигабайт и, следовательно, в конечном итоге вызовет очистку. (Код доступен на GitLab.)

stages:
  - build

build-job:
  stage: build
  tags:
    - gl-cl
  script:
    - echo "Generating random nonsense..."
    - ./scripts/generate-random-nonsense.sh
    - echo "Building random nonsense image..."
    - ./scripts/build-random-nonsense-image.sh
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы ограничить наши конвейеры нашим новым бегуном, мы используем селекторы тегов и выбираем наш ранее зарегистрированный бегун с помощью тега gl-cl. Теперь мы можем, наконец, запустить наш конвейер несколько раз, чтобы увидеть эффект от нашего сценария предварительной сборки. В зависимости от размера диска бегуна, мы должны увидеть пару заданий без очистки, после чего запуск будет содержать журнал, подобный этому:

Docker disk space usage is above limit (actual: 2361821460B, limit: 2000000000B)
Запускаем очистку…
Удаленные контейнеры:
9a126aead174a15a4f76f2cb5744e36aff30741cc6ab0ac5044837aaee946496
2fa51dc3e7f5b1e3bec63a635c266552f9b02eb74015a9683f7cbf13418a12eb

Удаленные изображения:
без метки: registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-febb2a09
не отмечено: registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper@sha256:edc1bf6ab9e1c7048d054b270f79919eabcbb9cf052b3e5d6f29c886c842bfed
deleted: sha256:c20c992e5d83348903a6f8d18b4005ed1db893c4f97a61e1cd7a8a06c2989c40
deleted: sha256:873201b44549097dfa61fa4ee55e5efe6e8a41bbc3db9c6c6a9bfad4cb18b4ea
без метки: random-nonsense-image-1653227274:latest
deleted: sha256:67fde47d8b24ee105be2ea3d5f04d6cd0982d9db2f1c934b3f5b3675eb7a626f
deleted: sha256:1a310f85590c46c1e885278d1cab269f07033fefdab8f581f06046787cd6156e
без метки: alpine:latest
untagged: alpine@sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454
без метки: random-nonsense-image-1653226909:latest
deleted: sha256:b5923f3fb6dd2446d18d75d5fbdb4d35e5fca888bd88aef8174821c0edfcb87f
deleted: sha256:59150b0202d2d5f75ec54634b4d8b208572cbeec9c5519a9566d2e2e6f2c13f3
deleted: sha256:0ac33e5f5afa79e084075e8698a22d574816eea8d7b7d480586835657c3e1c8b

Общее освобожденное пространство: 2,059 ГБ

Этот результат показывает, что наш сценарий предварительной сборки был выполнен, когда использование дискового пространства Docker достигло значения выше 2 ГБ, и очистка была запущена успешно (освободив в данном случае около 2 ГБ дискового пространства).

Предпосылки и ограничения подхода, основанного на сценарии предварительной сборки

Вероятно, легко понять, что наш подход, основанный на сценарии предварительной сборки, отвечает требованиям, которые мы к нему предъявляли. Т.е. он

  • выполняется синхронно перед заданиями конвейера, он
  • упреждающая очистка неиспользуемых ресурсов, и он
  • позволяет нам предоставить пользовательскую логику очистки.

Тем не менее, все еще остается несколько ограничений.

Во-первых, bash должен быть доступен во время выполнения сценария предварительной сборки, если мы хотим использовать сценарий is_docker_disk_space_usage_above_limit.sh, поскольку он использует некоторые BASH-измы. Более того, поскольку мы используем исполнитель Docker, нам необходимо использовать какой-то образ runner, на котором установлен Docker CLI (например, официальный базовый образ Docker, который мы использовали в нашем тесте ранее). Написание собственного образа для использования в качестве базового образа для запуска наших конвейеров устраняет и уменьшает серьезность этой проблемы, но ее все равно нужно решать.

Следует также помнить, что использование дискового пространства Docker является лишь приблизительным (т.е. меньшим, чем) использованием дискового пространства системы. Следовательно, мы должны найти хорошее значение для предела, который запустит нашу логику очистки, чтобы не исчерпать дисковое пространство на машине в любом случае.

Кроме того, может оказаться сложным выбрать правильную логику очистки. Например, если мы просто запустим docker system prune -af --volumes, как в нашем тесте, мы можем удалить изображения, которые необходимы последующим заданиям в более сложных конвейерах. Исключение определенных образов из очистки — например, созданных за последние 24 часа — может решить эту проблему. Однако в более сложных конвейерах нам, скорее всего, потребуется более сложная логика очистки.

И наконец, даже при использовании сценария предварительной очистки перед сборкой все еще существуют граничные случаи, когда у наших бегунов закончится место на диске. Навскидку могу сказать, что если лимит слишком высок, бегунок все равно может исчерпать дисковое пространство, потому что при выполнении задания производится больше ресурсов, чем доступно свободного места.

Резюме

Как мы уже видели, мы можем использовать крючок сценария предварительной сборки GitLab CI/CD для очистки бегунов GitLab синхронно с выполнением заданий, упреждающе, чтобы избежать нарушения конвейеров, а также используя пользовательскую логику очистки. Учитывая это, подход к очистке сценариев предварительной сборки не идеален, поскольку он не может избежать всех ситуаций, когда у бегуна закончится место на диске. Тем не менее, я считаю, что это более элегантный способ очистки бегунов GitLab, чем простои в обслуживании или подход с использованием заданий cron.

Оцените статью
Procodings.ru
Добавить комментарий