Автоматическая генерация CI/CD пайплайна для развёртывания инфраструктуры

от автора

Подход «Инфраструктура как код» означает, что инфраструктура создаётся, развёртывается и управляется при помощи кода. Это позволяет автоматизировать процессы, делать их более гибкими и масштабируемыми. Код для инфраструктуры фиксирует конфигурацию, обеспечивает воспроизводимость и упрощает управление настройками. Также благодаря этому подходу возрастает эффективность работы команды, поскольку он позволяет вести совместное развитие инфраструктуры и обеспечивает удобство отслеживания изменений.

Именно этот подход мы используем при нашей работе. Однако в процессе его использования мы столкнулись с проблемой написания пайплайнов для инфраструктуры.

Мы были вынуждены сделать процесс выкатки инфраструктуры максимально точечным из-за использования terragrunt. Каждый его модуль должен выкатываться отдельно, иначе будут получены десятки планов, и понять, что делает каждый из них, будет невозможно. Это означает, что каждому модулю terragrunt нужна отдельная джоба в пайплайне на plan и apply, но для каждого модуля они во многом повторяют друг друга. Подобное постоянное написание одинаковых частей CI/CD пайплайна при добавлении новых баз и бакетов навевало тоску.

Меня зовут Татьяна Мигулаева, я DevOps-инженер в «Магните». Поделюсь тем, как мы создали генератор джоб в GitLab CI/CD и навсегда забыли о ручном написании пайплайнов для развёртывания элементов инфраструктуры.

Что мы имеем

Инфраструктура на базе Yandex Cloud, описанная в terragrunt-файлах, код которых хранится в GitLab.

Периодически приходится создавать новые файлы или изменять существующие – например, чтобы поправить название пользователя в managed базе или создать новый топик в Кафке. После внесения этих изменений их необходимо выкатить в облако, предварительно проверив, что при выкатке ничего не сломается. Для этого используются CI/CD пайплайны со следующим флоу:

При создании merge request должен запускаться terragrunt plan, который проверяет допустимость вносимых в коде изменений и показывает, что произойдёт, если их применить. При мерже этих изменений в main должен повторно запускаться terragrunt plan, а после него – terragrunt apply, который применит эти изменения на инфраструктуре.

Есть два примерных варианта, как это реализовать:

1. Большой статичный пайплайн, который включает в себя все сервисы сразу.
Плюсы: не надо ничего менять, для выкатки достаточно изменений в terragrunt-файле. Удобно, когда инфраструктура небольшая и не меняется слишком часто.
Минусы: Такой пайплайн может выполняться довольно долго. При этом результатом его работы будут сотни планов и отчетов в гитлабе. По ним будет очень тяжело понять, какие изменения были внесены в код и как они отразятся на инфраструктуре. Также такой пайплайн будет не полностью управляем: не получится нажать кнопку деплоя отдельно для того элемента инфраструктуры, в который были внесены изменения.

2. Для каждого элемента — отдельный шаг в пайплайне. Автоматически запускаются только те шаги, в которые были внесены изменения. terragrunt apply применяется отдельно для каждого элемента инфраструктуры и запускается только вручную.
Плюсы: Это удобно, пайплайн выполняется относительно быстро. При этом нет риска случайно выкатить не то.
Минусы: Придётся постоянно дописывать новые шаги в CI/CD, высок риск ошибки. Учитывая, что наша инфраструктура динамична и много и часто меняется, делать такие изменения пришлось бы постоянно. И это просто скучно 🙂 

Наше решение

В нашем решении объединены плюсы обоих вариантов. По сути мы изменяем только код инфраструктуры, а наш пайплайн автоматически подстраивается под это.
При создании merge request автоматически генерируется и запускается шаг пайплайна, который запускает terragrunt plan только для тех terragrunt-файлов, в которые были внесены изменения. 

Например, мы решили создать postgres-базу в облаке для сервиса my-lovely-service. Для этого создали в репозитории в нужной папке terragrunt-файл с конфигурацией бакета. Далее создали merge request с этими изменениями. При этом был автоматически создан и запущен следующий шаг пайплайна.

Есть возможности зайти в пайп и посмотреть вывод команды terragrunt plan. Стоит обратить внимание, что для остальных файлов в репозитории пайплайн не запускался, что значительно ускоряет работу.

После слияния изменений в main запускается пайплайн целиком, но при этом триггерятся только те шаги terragrunt plan, которые запускались на merge request. У нас этот пайплайн выглядит немного страшно.

Но это зависит от количества сервисов и размера инфраструктуры.

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

Реализация

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

Здесь важно обратить внимание на то, как работает include в GitLab CI/CD. Если кратко — он объединяет имеющийся CI/CD файл и те, которые указаны в include. При этом шаги, которые называются одинаково в нескольких файлах, сливаются между собой. Подробнее можно изучить в документации Gitlab: https://docs.gitlab.com/ee/ci/yaml/#include

Предположим, нам надо создать s3 бакет для сервиса my-service в dev-окружении. Для этого в папке dev создаём папку my-service и создаём в ней terragrunt.hcl файл, из которого будет создаваться бакет.

Делаем коммит с этими изменениями в отдельную ветку. В этот момент pre-commit hook запускает следующий скрипт.

generate-mr-jobs.sh
#!/bin/sh echo '' > .gitlab-ci-mr-jobs.yml for env in "dev" "uat" "mm-dev" "mm-uat" do  isMmProject=false if [[ "$env" =~ ^mm-.*$ ]] then   isMmProject=true fi  for root in $(git ls-files -s |grep -oE "\s${env}/.*terragrunt.hcl$" | xargs dirname) do prefix=$(echo "$root" | tr / -) cat >>.gitlab-ci-mr-jobs.yml <<EOF ${prefix}:plan:   variables:     IS_MM_PROJECT: "${isMmProject}"     TF_ROOT: "${root}"   extends:      - .terragrunt:plan:${env}     - .rules:mr:${env}     - .variables:unprotected  EOF done done 

В данном скрипте сначала очищается имеющийся файл с джобами, после чего прописывается ci/cd шаг для каждого terragrunt файла в репозитории.
Таким образом, мы получаем файл .gitlab-ci-mr-jobs.yml со списком шагов следующего вида:

dev-s3-my-service:plan: variables: TF_ROOT: "dev/s3/my-service" extends: - .terragrunt:plan:dev - .rules:mr:dev

После создания merge request с нашими изменениями запускается ci/cd пайплайн. Сам gitlab-ci.yml файл включает в себя три других:

include:   - '.gitlab-ci-terragrunt.yml'   - '.gitlab-ci-templates.yml'   - '.gitlab-ci-mr-jobs.yml'

А также следующие шаги:

stages:   - build   - deploy   - setup   - triggers

Файл gitlab-ci-mr-jobs.yml был рассмотрен ранее.

В gitlab-ci-templates.yml задаются такие элементы, как теги раннеров, правила создания шагов и так далее. Выглядит он следующим образом. 

gitlab-ci-templates.yml
.runners:dev: tags: - devops - dev   .rules:mr:dev: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" changes: - terragrunt.hcl - provider.tmpl - dev/env.hcl - ${TF_ROOT}/* - ${TF_ROOT}/**/*   .rules:default:dev: rules: - if: $CI_COMMIT_REF_PROTECTED == "true" changes: - terragrunt.hcl - provider.tmpl - dev/env.hcl - ${TF_ROOT}/* - ${TF_ROOT}/**/* - when: manual   .terragrunt:plan:dev: extends: - .terragrunt:plan - .runners:dev variables: TF_TARGET_ENV: "dev"   .terragrunt:apply:dev: extends: - .terragrunt:apply - .runners:dev variables: TF_TARGET_ENV: "dev" 

В gitlab-ci-terragrunt.yml описаны шаги подключения к облаку и работы с ним. В нашем случае это Yandex Cloud, но может быть и любое другое. В качестве хранилища секретов используется Hashicorp Vault.

gitlab-ci-terragrunt.yml
variables:   TF_PLAN_CACHE: plan.cache  .terragrunt:common:   image:     name: my_name   resource_group: ${TF_ROOT}   id_tokens:     VAULT_ID_TOKEN:       aud: ${VAULT_ADDR}   variables:     VAULT_AUTH_PATH: "id_jwt"     YC_KEY: /tmp/yc_key.json   before_script:     - export VAULT_TOKEN="$(vault write -field=token auth/${VAULT_AUTH_PATH}/login role=${VAULT_AUTH_ROLE} jwt=${VAULT_ID_TOKEN})"     - vault kv get -field=${TF_TARGET_ENV} infra/${TF_SECRET_PATH}/terraform > ${YC_KEY}     - yc config profile create my-robot-profile     - yc config set service-account-key ${YC_KEY}     - export YC_TOKEN=$(yc iam create-token)     - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf ssh://git@gitlab.com/     - export TF_HTTP_PASSWORD=${CI_JOB_TOKEN}     - cd ${TF_ROOT}  .terragrunt:validate:   extends:      - .terragrunt:common   stage: validate   script:     - terragrunt validate  .terragrunt:plan:   variables:     TF_PLAN_JSON: plan.json     JQ_PLAN: |       (         [.resource_changes[]?.change.actions?] | flatten       ) | {         "create":(map(select(.=="create")) | length),         "update":(map(select(.=="update")) | length),         "delete":(map(select(.=="delete")) | length)       }   extends:      - .terragrunt:common   stage: build   script:     - terragrunt plan -input=false -out="$(pwd)/${TF_PLAN_CACHE}"     - terragrunt show -json "$(pwd)/${TF_PLAN_CACHE}" |       jq -r "${JQ_PLAN}" > "${TF_PLAN_JSON}"   artifacts:     expire_in: 1 week     paths:       - ${TF_ROOT}/${TF_PLAN_CACHE}     reports:       terraform: ${TF_ROOT}/${TF_PLAN_JSON}  .terragrunt:apply:   extends:      - .terragrunt:common   stage: deploy   script:     - eval $(ssh-agent -s)     - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -     - terragrunt apply -input=false "$(pwd)/${TF_PLAN_CACHE}"   when: manual

Вернёмся к нашему бакету для my-service. После создания merge request из всех описанных ранее файлов будут объединены следующие шаги.

.rules:mr:dev: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" changes: - terragrunt.hcl - provider.tmpl - dev/env.hcl - ${TF_ROOT}/* - ${TF_ROOT}/**/*  .terragrunt:plan:dev: extends: - .terragrunt:plan - .runners:dev variables: TF_TARGET_ENV: "dev"  .runners:dev: tags: - devops - dev  - .terragrunt:plan - настройка terragrunt с Yandex Cloud

Таким образом, этот шаг будет запускаться только на те ресурсы, в которых произошло изменение, то есть на созданный terragrunt файл s3 бакета сервиса my-service. Далее на раннеры ставятся нужные теги, а terragrunt настраивается на использование нужного нам dev окружения в облаке.

Далее, когда всё это прошло успешно, merge request вливается в main ветку. В этот момент запускается скрипт generate-default-jobs.sh

generate-default-jobs.sh
#!/bin/sh  # directory is in the first argument directory=$1 # Environment is in the second argument env=$2 # Prefix is the directory name with slashes replaced by dashes parent_prefix=$(echo "$directory" | tr / -)  cat >.gitlab-ci-${parent_prefix}.yml <<EOF include:   - '.gitlab-ci-terragrunt.yml'   - '.gitlab-ci-templates.yml'  stages:   - build   - deploy   - setup   - triggers  EOF  # filter subdirectories with terragrunt.hcl has_terragrunt="" no_terragrunt=""  # Loop through all subdirectories of the given directory for dir in "$directory"/*/; do   # Check if the directory contains a terragrunt.hcl file   if [ -f "${dir}terragrunt.hcl" ]; then     has_terragrunt="${has_terragrunt} ${dir%/}"   else     no_terragrunt="${no_terragrunt} ${dir%/}"   fi done  echo ${has_terragrunt} echo ${no_terragrunt}  for root in ${has_terragrunt} do prefix=$(echo "$root" | tr / -) basename=$(basename "$root") cat >>.gitlab-ci-${parent_prefix}.yml <<EOF ${basename}:plan:   stage: build   variables:     TF_ROOT: "${root}"   extends:      - .terragrunt:plan:${env}     - .rules:default:${env}     - .variables:protected  ${basename}:apply:   stage: deploy   variables:     TF_ROOT: "${root}"   extends:      - .terragrunt:apply:${env}     - .variables:protected   needs:     - ${basename}:plan  EOF done  for root in ${no_terragrunt} do prefix=$(echo "$root" | tr / -) basename=$(basename "$root")  cat >>.gitlab-ci-${parent_prefix}.yml <<EOF ${basename}:generate:   stage: setup   extends:     - .runners:${env}   script:     - ./generate-default-jobs.sh ${root} ${env}   artifacts:     paths:     - .gitlab-ci-${prefix}.yml     expire_in: 1 week  ${basename}:trigger:   stage: triggers   trigger:     include:       - artifact: .gitlab-ci-${prefix}.yml         job: ${basename}:generate  EOF done

Суть этого скрипта, что он генерирует gitlab-ci файл аналогично предыдущему, с той разницей, что появится шаг развёртывания файлов terraform apply. Результат выглядит следующим образом:

Первые шаги пайплайна

Первые шаги пайплайна
Шаг деплоя S3-бакетов

Шаг деплоя S3-бакетов

Таким образом, мы создали механизм автоматической генерации пайплайнов, который значительно упрощает работу инженера. При его использовании достаточно писать вручную исключительно terragrunt-файлы, а пайплайны в репозитории будут появляться и запускаться автоматически.


ссылка на оригинал статьи https://habr.com/ru/articles/776214/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *