В этой статье рассматриваются различные варианты настройки CI/CD с использованием GitLab CI/CD и werf.

Конечный pipeline состоит из следующего набора стадий:

  • build — стадия сборки и публикации образов приложения;
  • deploy — стадия деплоя приложения для одного из контуров кластера;
  • dismiss — стадия удаления приложения для review окружения;
  • cleanup — стадия очистки container registry.

Набор контуров (а равно — окружений GitLab) в кластере Kubernetes может варьироваться в зависимости от многих факторов. В статье будут приведены различные варианты организации окружений для следующих:

  • Production;
  • Staging;
  • Review.

Далее последовательно рассматриваются стадии pipeline и различные варианты их организации. Изложение построено от общего к частному. В конце статьи приведены окончательные версии .gitlab-ci.yml для готовых workflow.

Независимо от workflow, все версии конфигурации подчиняются следующим правилам:

  • Сборка и публикация выполняется при каждом push в репозитории.
  • Развёртывание/удаление review окружений:
    • Развёртывание на review окружение возможен только в рамках Merge Request (MR).
    • Review окружения удаляются с помощью инструментария GitLab (по кнопке в разделе Environment), автоматически при удалении ветки или отсутствии активности в MR в течение суток.
  • Очистка запускается один раз в день по расписанию на master.

Для развёртывания review окружения и staging и production окружений предложены самые популярные варианты по организации. Каждый вариант для staging и production окружений сопровождается всевозможными способами отката релиза в production.

Требования

  • Кластер Kubernetes и настроенный для работы с ним kubectl.
  • GitLab-сервер версии выше 10.x либо учетная запись на gitlab.com.
  • Container registry, встроенный в GitLab или выделенный.
  • Приложение, которое успешно собирается и деплоится с werf.
  • Понимание основных концептов GitLab CI/CD.

Инфраструктура

scheme

  • Кластер Kubernetes.
  • GitLab со встроенным container registry.
  • Узел или группа узлов, с предустановленным werf и зависимостями.

Процесс деплоя требует наличия доступа к кластеру через kubectl, поэтому необходимо установить и настроить kubectl на узле, с которого будет запускаться werf. Если не указывать конкретный контекст опцией --kube-context или переменной окружения WERF_KUBE_CONTEXT, то werf будет использовать контекст kubectl по умолчанию.

В конечном счете werf требует наличия доступа на используемых узлах:

  • к Git-репозиторию кода приложения;
  • к container registry;
  • к кластеру Kubernetes.

Настройка runner

На узле, где предполагается запуск werf, установим и настроим GitLab-runner:

  1. Создадим проект в GitLab и добавим push кода приложения.
  2. Получим токен регистрации GitLab-runner:
    • заходим в проекте в GitLab Settings —> CI/CD;
    • во вкладке Runners необходимый токен находится в секции Setup a specific Runner manually.
  3. Установим GitLab-runner по инструкции.
  4. Зарегистрируем gitlab-runner, выполнив шаги за исключением следующих моментов:
    • используем werf в качестве тега runner;
    • используем shell в качестве executor для runner.
  5. Добавим пользователя gitlab-runner в группу docker.

    sudo usermod -aG docker gitlab-runner
    
  6. Установим werf.
  7. Установим kubectl и скопируем файл конфигурации kubectl в домашнюю папку пользователя `gitlab-runner.
    mkdir -p /home/gitlab-runner/.kube &&
    sudo cp -i /etc/kubernetes/admin.conf /home/gitlab-runner/.kube/config &&
    sudo chown -R gitlab-runner:gitlab-runner /home/gitlab-runner/.kube
    

После того, как GitLab-runner настроен, можно переходить к настройке pipeline.

Активация werf

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

before_script:
  - type trdl && . $(trdl use werf 2 stable)
  - type werf && source $(werf ci-env gitlab --as-file)

Сборка и публикация образов приложения

Build and Publish:
  stage: build
  script:
    - werf build
  except: [schedules]
  tags: [werf]

Забегая вперед, очистка хранилища стадий и container registry предполагает запуск соответствующего задания по расписанию. Так как при очистке не требуется выполнять сборку образов, то указываем except: [schedules], чтобы стадия сборки не запускалась в случае работы pipeline по расписанию.

Конфигурация задания достаточно проста, поэтому хочется сделать акцент на том, чего в ней нет — явной авторизации в container registry, вызова werf cr login.

В простейшем случае, при использовании встроенного container registry, авторизация выполняется автоматически при вызове команды werf ci-env. В качестве необходимых аргументов используются переменные окружения GitLab CI_JOB_TOKEN (подробнее про модель разграничения доступа при выполнении заданий в GitLab можно прочитать здесь) и CI_REGISTRY_IMAGE.

При вызове werf ci-env создаётся временный docker config, который используется всеми командами в shell-сессии (в том числе docker). Таким образов, параллельные задания никак не пересекаются при использовании docker и временный токен в конфигурации не перетирается.

Если необходимо выполнить авторизацию с произвольными учётными данными, werf cr login должен выполняться после вызова werf ci-env.

Развёртывание приложения

Прежде всего необходимо описать шаблон, общую часть для деплоя в любой контур, что позволит уменьшить размер файла .gitlab-ci.yml, улучшит его читаемость, а также позволит далее сосредоточиться на workflow.

.base_deploy:
  stage: deploy
  script:
    - werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
  except: [schedules]
  tags: [werf]

При использовании шаблона base_deploy для каждого контура будет определяться своё окружение GitLab:

Example:
  extends: .base_deploy
  environment:
    name: <environment name>
    url: <url>
    # ...
  # ...

При выполнении задания, werf ci-env устанавливает переменную WERF_ENV в соответствии с именем окружения GitLab (CI_ENVIRONMENT_SLUG).

Для того, чтобы по-разному конфигурировать приложение для используемых контуров кластера в helm-шаблонах можно использовать Go-шаблоны и переменную .Values.werf.env, что соответствует значению опции --env или переменной окружения WERF_ENV.

Также в шаблоне используется адрес окружения, URL для доступа к разворачиваемому в контуре приложению, который передаётся параметром envUrl. Это значение может использоваться в helm-шаблонах, например, для конфигурации Ingress-ресурсов.

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

Варианты организации review окружения

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

Для корректной работы приведённого примера необходимо добавить файл .werf-deploy-report.json в .gitignore.

Рассмотрим базовые конфигурации Review и Stop Review заданий, которые лягут в основу всех предложенных вариантов.

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

В задание Review описывается развёртывание review-релиза в динамическое окружение, в основу имени которого закладывается уникальный идентификатор MR. Параметр auto_stop_in позволяет указать период отсутствия активности в MR, после которого окружение GitLab будет автоматически остановлено. Остановка окружения GitLab сама по себе никак не влияет на ресурсы в кластере, review-релиз, поэтому в дополнении необходимо определить задание, которое вызывается при остановке (on_stop). В нашем случае, это задание Stop Review.

Задание Stop Review выполняет удаление review-релиза, а также остановку окружения GitLab (action: stop): werf удаляет helm-релиз и namespace в Kubernetes со всем его содержимым (werf dismiss). Задание Stop Review может быть запущено вручную после деплоя на review контур, а также автоматически GitLab-сервером, например, при удалении соответствующей ветки в результате слияния ветки с master и указания соответствующей опции в интерфейсе GitLab.

Для выполнения werf dismiss требуется werf.yaml, так как в нём содержаться шаблоны имени релиза и namespace. При удалении ветки нет возможности использовать исходники из git, поэтому в задании Stop Review используется werf.yaml, сохранённый при выполнении задания Review, и отключено подтягивание изменений из git (GIT_STRATEGY: none).

Таким образом, по умолчанию закладываем следующие варианты удаления review окружения:

  • вручную;
  • автоматически при отсутствии активности MR в течение суток и при удалении ветки.

Далее разберём основные стратегии при организации развёртывания review окружения.

Мы не ограничиваем вас предложенными вариантами, даже напротив — рекомендуем комбинировать их и создавать конфигурацию workflow под нужды вашей команды

№1 Вручную

Данный вариант реализует подход описанный в разделе «Развёртывание на review из pull request по кнопке»

При таком подходе пользователь разворачивает и удаляет окружение по кнопке в pipeline.

Он самый простой и может быть удобен в случае, когда развёртывания происходят редко и review окружение не используется при разработке. По сути, для проверки перед принятием MR.

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]
  when: manual

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

№2 Автоматически по имени ветки

Данный вариант реализует подход описанный в разделе «Развёртывание на review из ветки по шаблону автоматически»

В предложенном ниже варианте автоматический релиз выполняется для каждого коммита в MR, в случае, если имя git-ветки имеет префикс review-.

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  rules:
    - if: $CI_MERGE_REQUEST_ID && $CI_COMMIT_REF_NAME =~ /^review-/

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

№3 Полуавтоматический режим с лейблом (рекомендованный)

Данный вариант реализует подход описанный в разделе «Развёртывание на review из pull request автоматически после ручной активации»

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

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

Review:
  stage: deploy
  script:

    - >
      # do optional deploy/dismiss

      if echo $CI_MERGE_REQUEST_LABELS | tr ',' '\n' | grep -q -P '^review$'; then
        werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
      else
        if werf helm get $(werf helm get-release) 2>/dev/null; then
          werf dismiss --with-namespace
        fi
      fi
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]
  tags: [werf]

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

Для проверки наличия лейбла у MR используется переменная среды CI_MERGE_REQUEST_LABELS.

Варианты организации staging и production окружений

Предложенные далее варианты являются наиболее эффективными комбинациями правил развёртывания staging и production окружений.

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

№1 Fast and Furious (рекомендованный)

Данный вариант реализует подходы описанные в разделах «Развёртывание на production из master автоматически» и «Развёртывание на production-like из pull request по кнопке»

Развёртывание в production происходит автоматически при любых изменениях в master. Выполнить развёртывание в staging можно по кнопке в MR.

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [merge_requests]
  when: manual

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [master]

Варианты отката изменений в production:

№2 Push the Button

Данный вариант реализует подходы описанные в разделах «Развёртывание на production из master по кнопке» и «Развёртывание на staging из master автоматически»

Развёртывание production осуществляется по кнопке у коммита в master, а развёртывание в staging происходит автоматически при любых изменениях в master.

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [master]
  when: manual

Варианты отката изменений в production:

  • по кнопке у стабильного коммита или воспользовавшись кнопкой Rollback environment (рекомендованный);
  • развёртывание стабильного MR и нажатии кнопки.

№3 Tag everything (рекомендованный)

Данный вариант реализует подходы описанные в разделах «Развёртывание на production из тега автоматически» и «Развёртывание на staging из master по кнопке»

Развёртывание в production выполняется при проставлении тега, а в staging по кнопке у коммита в master.

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]
  when: manual

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only:
    - tags

Варианты отката изменений в production:

  • нажатие кнопки на другом теге (рекомендованный);
  • создание нового тега на старый коммит (так делать не надо).

№4 Branch, branch, branch!

Данный вариант реализует подходы описанные в разделах «Развёртывание на production из ветки автоматически» и «Развёртывание на production-like из ветки автоматически»

Развёртывание в production происходит автоматически при любых изменениях в ветке production, а в staging при любых изменениях в ветке master.

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [production]

Варианты отката изменений в production:

Очистка образов

Cleanup:
  stage: cleanup
  script:
    - werf cr login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_REPO}
    - werf cleanup
  only: [schedules]
  tags: [werf]

В werf встроен эффективный механизм очистки, который позволяет избежать переполнения container registry и диска сборочного узла от устаревших и неиспользуемых образов. Более подробно ознакомиться с возможностями очистки, встроенными в werf, можно здесь.

Чтобы использовать очистку, необходимо создать Personal Access Token в GitLab с необходимыми правами. С помощью данного токена будет осуществляться авторизация в container registry перед очисткой.

Для вашего тестового проекта вы можете просто создать Personal Access Token а вашей учетной записи GitLab. Для этого откройте страницу Settings в GitLab (настройки вашего профиля), затем откройте раздел Access Token. Укажите имя токена, в разделе Scope отметьте api и нажмите Create personal access token — вы получите Personal Access Token.

Чтобы передать Personal Access Token в переменную окружения GitLab откройте ваш проект, затем откройте Settings —> CI/CD и разверните Variables. Создайте новую переменную окружения WERF_IMAGES_CLEANUP_PASSWORD и в качестве ее значения укажите содержимое Personal Access Token. Для безопасности отметьте созданную переменную как protected.

Стадия очистки запускается только по расписанию, которое вы можете определить открыв раздел CI/CD —> Schedules настроек проекта в GitLab. Нажмите кнопку New schedule, заполните описание задания и определите шаблон запуска в обычном cron-формате. В качестве ветки оставьте master (название ветки не влияет на процесс очистки), отметьте Active и сохраните pipeline.

Полный .gitlab-ci.yml для готовых workflow

Детали workflow

Подробнее про workflow можно почитать в отдельной статье

.gitlab-ci.yml

stages:
  - build
  - deploy
  - dismiss
  - cleanup

before_script:
  - type trdl && . $(trdl use werf 2 stable)
  - type werf && source $(werf ci-env gitlab --as-file)

Build and Publish:
  stage: build
  script:
    - werf build
  except: [schedules]
  tags: [werf]

.base_deploy:
  stage: deploy
  script:
    - werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
  tags: [werf]

Review:
  stage: deploy
  script:
    - >
      # do optional deploy/dismiss

      if echo $CI_MERGE_REQUEST_LABELS | tr ',' '\n' | grep -q -P '^review$'; then
        werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
      else
        if werf helm get $(werf helm get-release) 2>/dev/null; then
          werf dismiss --with-namespace
        fi
      fi
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]
  tags: [werf]

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [merge_requests]
  when: manual

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [master]

Cleanup:
  stage: cleanup
  script:
    - werf cr login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_REPO}
    - werf cleanup
  only: [schedules]
  tags: [werf]

Детали workflow

Подробнее про workflow можно почитать в отдельной статье

.gitlab-ci.yml

stages:
  - build
  - deploy
  - dismiss
  - cleanup

before_script:
  - type trdl && . $(trdl use werf 2 stable)
  - type werf && source $(werf ci-env gitlab --as-file)

Build and Publish:
  stage: build
  script:
    - werf build
  except: [schedules]
  tags: [werf]

.base_deploy:
  stage: deploy
  script:
    - werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
  except: [schedules]
  tags: [werf]

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}-${CI_MERGE_REQUEST_ID}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]
  when: manual

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [master]
  when: manual

Cleanup:
  stage: cleanup
  script:
    - werf cr login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_REPO}
    - werf cleanup
  only: [schedules]
  tags: [werf]

Детали workflow

Подробнее про workflow можно почитать в отдельной статье

.gitlab-ci.yml

stages:
  - build
  - deploy
  - dismiss
  - cleanup

before_script:
  - type trdl && . $(trdl use werf 2 stable)
  - type werf && source $(werf ci-env gitlab --as-file)

Build and Publish:
  stage: build
  script:
    - werf build
  except: [schedules]
  tags: [werf]

.base_deploy:
  stage: deploy
  script:
    - werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
  except: [schedules]
  tags: [werf]

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  only: [merge_requests]
  when: manual

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]
  when: manual

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [tags]

Cleanup:
  stage: cleanup
  script:
    - werf cr login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_REPO}
    - werf cleanup
  only: [schedules]
  tags: [werf]

Детали workflow

Подробнее про workflow можно почитать в отдельной статье

.gitlab-ci.yml

stages:
  - build
  - deploy
  - dismiss
  - cleanup

before_script:
  - type trdl && . $(trdl use werf 2 stable)
  - type werf && source $(werf ci-env gitlab --as-file)

Build and Publish:
  stage: build
  script:
    - werf build
  except: [schedules]
  tags: [werf]

.base_deploy:
  stage: deploy
  script:
    - werf converge -Z --set "env_url=$(echo ${CI_ENVIRONMENT_URL} | cut -d / -f 3)"
  except: [schedules]
  tags: [werf]

Review:
  extends: .base_deploy
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
    on_stop: Stop Review
    auto_stop_in: 1 day
  artifacts:
    paths:
      - .werf-deploy-report.json
  rules:
    - if: $CI_MERGE_REQUEST_ID && $CI_COMMIT_REF_NAME =~ /^review-/

Stop Review:
  stage: dismiss
  script:
    - werf dismiss --with-namespace
  environment:
    name: review-${CI_MERGE_REQUEST_ID}
    action: stop
  variables:
    GIT_STRATEGY: none
  only: [merge_requests]
  when: manual
  tags: [werf]

Deploy to Staging:
  extends: .base_deploy
  environment:
    name: staging
    url: http://${CI_PROJECT_NAME}.kube.DOMAIN
  only: [master]

Deploy to Production:
  extends: .base_deploy
  environment:
    name: production
    url: https://www.company.org
  only: [production]

Cleanup:
  stage: cleanup
  script:
    - werf cr login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_REPO}
    - werf cleanup
  only: [schedules]
  tags: [werf]
назад
далее