Я не отношу себя к борцам с несуществующими проблемами, а больше нацелен на доработку того, что может быть улучшено. Одно из таких направлений — релиз-менеджмент.
По своим проектам я заметил, что часть рутинных задач по конфигурации пайплайна делается методом Сtrl+C/Сtrl+V. Получившиеся манускрипты своей массивностью пугают неискушенные умы программистов, что может приводить к неприятным последствиям.
Языки разметки — невероятно универсальный инструмент, овладев которым можно избавить себя от ежедневной рутины. Не стоит забывать, что сами по себе, без дополнительного слоя компиляции, они ничего не могут, но в связке с другими программами превращаются в мощный инструмент.
Думаю, что моя статья может быть интересна разработчикам и всем, кому интересно, как живет код-поле коммита; возможно, и DevOps почерпнут что-то в свой арсенал.
Языки разметки
Современную разработку невозможно представить без использования языков разметки. Форматы XML, YAML и JSON появились приблизительно в одно и то же время и вполне способны друг друга заменить. Разнообразие вариантов применения в описании и конфигурации различных систем выделяет их из числа языков разметки.
У каждого из них своя специфика. Мы не будем рассматривать семантику и изначальную документ-ориентированность форматов, а сконцентрируемся на сценариях использования.
XML остается языком конфигурации в различных платформах и в различных своих спецификациях позволяет генерировать XHTML из XSLT и XML. YAML и JSON же занимают совершенно другое место в повседневной жизни каждого разработчика.
Чаще всего с этими языками мы сталкиваемся при использовании CI/CD-инструментов. Рассмотреть и сравнить различные решения можно по ссылке.
Я на своих проектах последние пару лет использую GitLab. Приложения деплоятся в Kubernetes, а инфраструктура может конфигурироваться с помощью CloudFormation. Эти продукты объединяет то, что для их конфигурации используется YAML/JSON. И если конфигурации Kubernetes и CloudFormation описывают систему и ничем не примечательны, то GilLab позволяет играть в некое подобие программирования.
Знакомство с GitLab CI/CD
GitLab CI/CD позволяет управлять деливери-процессом через файл конфигурации. На первый взгляд файл не представляет собой ничего интересного, позволяя описать интересующие нас шаги и задать необходимые переменные, не забыв о исполняемых скриптах.
Для переменных применимо наследование, помимо файла конфигурации их можно указать на уровне проекта или группы. Список переменных расширяется новыми на каждом уровне. Переменные группы переопределяются проектными, а те в свою очередь — указанными непосредственно в конфигурации. Помимо пользовательских переменных, доступен внушительный список предопределенных на все случаи жизни.
Базовый пайплайн может выглядеть следующим образом:
--- main.yml stages: - test - build - deploy variables: foo: bar hello: world app-test: stage: test script: - application test app-build: stage: build script: - application build - container push app-deploy: stage: deploy variables: foo: baz script: - container deploy
GitLab/YAML-программирование
Наследованием переменных возможности конфигурации не ограничиваются. YAML предоставляет возможности наследования как блоков, так и списков, что позволяет переиспользовать определенные конструкции внутри одного файла:
nested-block: &some-ref foo: bar hello: world nested-list: &list-ref - command - command param app-block: stage: test <<: *some-ref script: - shell command - *list-ref
GitLab расширяет возможности наследования конструкциями include
и extends
. Include
позволяет импортировать интересующий файл как с локального, так и с удаленного репозитория.
Наследование по умолчанию позволяет дополнять и переопределять определенные блоки. Extends
, в свою очередь, позволяет наследовать уже существующие блоки, создавая новую логику на их основании, и работает с импортированными сущностями. Блоки с точкой в начале имени скрыты от системы и не отображаются на графическом конвейере. В то же время их можно использовать как основу или дополнение к существующей конфигурации.
--- example.yml include: - local: main.yml .app-build: variables: foo: bar app-build: variables: fiz: baz app-redeploy: extends: - .app-deploy - app-deploy when: manual
Каждая из этих техник позволяет навести порядок в наших проектах, но наибольший эффект достигается, если использовать их вместе.
Чтобы не мусорить в своих проектах и максимально упростить процедуру апгрейда, я использую отдельный проект с пайплайнами, которые стандартизуют процедуру. Самая простая структура такого проекта может разниться, но в общих чертах выглядит так:
/ci-project /services java.yml node.yml basic.yml
Для использования в проекте нам будет достаточно сослаться на нужный файл:
include: - project: '/ci-project' ref: 'master' file: 'services/java.yml'
В таком случае каждый сервис расширяет базовый документ, который включает в себя общие определения. Такого подхода вполне достаточно, когда систем немного и все, что мы хотим контролировать, — это однообразие процесса.
Но что будет, если мы захотим регламентировать не только языки, но и различные реализации build-test-deploy? Для этого пригодится ключевое слово rules
. Rules
позволяет настраивать различные условия для наших стейджей, тем самым изменив порядок наследования:
.node-apps: rules: - if: $LANGUAGE == "node"
В результате мы получаем более гибкую структуру с единой точкой входа main.yml:
/ci-project /services java.yml node.yml /deploy k8s.yml ec2.yml /build java.yml node.yml main.yml
Использование версионирования добавит стабильности пайплайнов. А файл с проектными особенностями позволит отделить логику от импортов:
include: - project: 'ci-project' ref: 'tag-version' file: 'main.yml' - local: '/.local-ci.yml'
Шаблонизация деплоя
В то время как тестирование и сборка артефактов регулируются командами, процесс деплоя для распределенных систем описывается документом, в котором от сервиса к сервису изменяется лишь пара строк.
В качестве примера рассмотрим деплой сервиса на Kubernetes. В зависимости от типа сервиса количество различных компонентов будет отличаться, но значащими параметрами будут: образ приложения в реестре, переменные окружения, иногда сертификаты, публичные адреса, параметры авторизации…
Чтобы все работало, стоит унифицировать подходы, использовать одинаковые имена для секретов и переменных окружения, а также пути к сертификатам, что позволит упростить настройку окружения.
Реализовать шаблонизацию можно по-разному. Я выделю два варианта:
- использование «настоящего» программирования (Python + Jinja), что требует дополнительных манипуляций с образом сборщика;
- использование консольных команд (jq), требующее установки нескольких пакетов.
Полноценное приложение, упакованное в образ по сборке контекста, предпочтительнее в большинстве ситуаций, так как повышает гибкость используемых решений.
Вариант с jq наименее требователен к кастомизации исходного образа и способен решать главную задачу: он упрощает работу разработчика. По сути, он представляет собой не что иное, как JSON-программирование.
Вот вариант с кастомизацией CronJob:
--- main.yml variables: TEMPLATE: > #сворачивает текст в строку { "apiVersion": "batch/v1beta1", "kind": "CronJob", "metadata": { "name": .name }, "spec": { "schedule": .schedule, "concurrencyPolicy": "Forbid", "startingDeadlineSeconds": 1800, "successfulJobsHistoryLimit": 1, "failedJobsHistoryLimit": 1, "jobTemplate": { "spec": { "template": { "spec": { "containers": [ { "name": "container-name", } ], "restartPolicy": "Never" } } } } } } .template: &template-ref - SECRETS_VAL=$(kubectl get secret $SECRET -o json | jq -c --arg secret $SECRET '.data | keys | map({"name":.,"valueFrom":{"secretKeyRef":{"name":$secret,"key":.}}})') - JOB=$(echo $JOBS | jq -c "[.[] | map_values(.) | $TEMPLATE]") - JOB=$(echo $JOB | jq -c --arg namespace $NAMESPACE --arg app $PROJECT_NAME '.items[].metadata += {"namespace":$namespace,"labels":{"group":$app}} | .items[].spec += {successfulJobsHistoryLimit:1}') - JOB=$(echo $JOB | jq -c --arg image $IMAGE --argjson env $SECRETS_VAL '.items[].spec.jobTemplate.spec.template.spec.containers[] += {"image":$image,"env":$env}') - echo $JOB | kubectl apply -f - deploy: script: - *template-ref --- .local-ci.yml deploy: variables: NAMESPACE: space IMAGE: image.registry/image JOBS: > [ {"name":"a", "schedule":"* * * * *"}, {"name":"b", "schedule":"0 0 * * *"} ]
Все, что требуется от разработчика в этом примере, — определить переменные JOB, которые он хочет создать.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: