Привет, Хабр! С вами не опять, а снова Виктор Ашакин, DevOps-инженер компании «Флант», и мы заканчиваем приготовление экосистемы управления кодом и его развёртывания в Deckhouse Kubernetes Platform на основе Gitea. В прошлой части статьи мы сделали всё необходимое для создания тестового репозитория и написания Gitea Actions-пайплайнов и можем двигаться дальше.
В этом — завершающем — материале создадим репозиторий с кодом приложения, подготовим простенький Helm-чарт и Gitea Actions-пайплайн, в котором опишем автоматический процесс сборки и деплоя приложения в кластер Kubernetes.

Ключевым компонентом автоматизации CI/CD будет werf, Open Source-утилита, созданная «Флантом» и пополнившая ряды Sandbox-проектов CNCF. Она организует полный цикл доставки приложения в Kubernetes и использует Git как единый источник истины для состояния приложения, развёрнутого в кластере. Утилита werf позволяет собирать и упаковывать код приложения в контейнеры, эффективно кэшировать стадии и изменения при сборке, что значительно ускоряет пайплайн.
С выходом версии 2.0 и переходом на новый движок werf ещё качественнее развёртывает приложения в кластер Kubernetes. В процессе активной работы над приложениями в container registry и на раннере накапливается значительное количество ненужных образов контейнеров — werf позволяет эффективно чистить весь лишний мусор.
Все эти полезные функции мы применим в нашем пайплайне.
Подготовка тестового репозитория
Для начала создадим в Gitea тестовый репозиторий hello-world внутри организации: в правом верхнем углу кнопка «+» → Owner: your_organization → Create Repository.

Клонируем репозиторий к себе:
git clone git@your_gitea.com:team-romeo/hello-world.git cd hello-worl touch README.md git add README.md git commit -m "first commit" git push
Теперь добавим код нашего приложения. В моём случае это простая HTML-страница, которая лежит в каталоге app/index.html:
<!DOCTYPE html> <html> <body> <header> <h1> Hi! I'm another one typical nginx! </h1> <h2> Kubernetes, Kubernetes everywhere! </h2> </header> </body> </html>
Теперь нужно подготовить файл werf.yaml для сборки нашего приложения. В моём примере происходит простое копирование файла в контейнер nginx. В реальной ситуации может происходить многоуровневая сборка с переиспользованием стадий сборки нескольких контейнеров.
werf нативно работает с файлами Dockerfile, но при этом мы теряем преимущество кэширования стадий сборки. Для каждого изменения состояния репозитория
будет запускаться повторный сборочный процесс, что зачастую избыточно. Зачем пересобирать приложение, если мы добавили в репозиторий Helm-чарт,
поправили README.md или файл, который в коде не участвует? Также избыточно постоянно переустанавливать в контейнере одни и те же пакеты.
Я покажу, как оптимизировать сборку с помощью werf stapel. В корне создаём файл werf.yaml — это аналог Dockerfile, сценарий сборки:
# Название приложения, оно будет использоваться в чарте как .Chart.Name project: hello-world configVersion: 1 --- image: nginx from: nginx:1.24.0-bullseye # Директива указывает, что и куда кладём, # можно задать несколько директив списком git: - add: /app to: /app # Директива указывает, что исключать при добавлении в контейнер, # также изменения в этих файлах не будут отслеживаться excludePaths: - .helm - werf.yaml - .gitea - README.md # Указываем, что на стадии install отслеживаем все файлы в каталоге /app. # Можно фильтровать файлы по маскам, например */*.html stageDependencies: install: - "*/**" # В этой директиве управляем процессом сборки, # стадии удобно комбинировать со стадиями добавления кода # Разные стадии кэшируются отдельно shell: install: - sed -i 's/Kubernetes/Deckhouse/g' app/index.html setup: - rm /etc/nginx/conf.d/default.conf
Процесс сборки у меня достаточно прост: в Docker-образ nginx я помещаю каталог /app по пути /app, после выполняю команду редактирования index.html, а затем удаляю дефолтный конфиг nginx. Последний шаг нужен потому, что мы подложим наш конфиг в виде configmap для удобства редактирования и выката. Но об этом чуть позже.
В моём werf.yaml я оставил комментарии, из которых понятны структура файла и директива управления сборкой. Пример выше расширен для наглядности, но его можно максимально упростить:
project: hello-world configVersion: 1 --- image: nginx from: nginx:1.24.0-bullseye git: - add: /app to: /app stageDependencies: install: - "*/**" shell: setup: - sed -i 's/Kubernetes/Deckhouse/g' app/index.html - rm /etc/nginx/conf.d/default.conf
Helm-чарт
Теперь подготовим Helm-чарт приложения. По умолчанию werf ищет каталог с чартом в корне репозитория в каталоге .helm. Структура нашего чарта будет выглядеть так:
├── .helm ├── templates │ ├── deployment.yaml │ ├── ingress.yaml │ ├── nginx-config-cm.yaml │ └── service.yaml └── values.yaml
Создаём ресурсы чарта
# deployment.yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Chart.Name }} labels: app: {{ .Chart.Name }} annotations: # Аннотация включает автоматическое отслеживание CM и secrets # Перезапускает под в случае изменения "pod-reloader.deckhouse.io/auto": "true" spec: revisionHistoryLimit: 1 selector: matchLabels: app: {{ .Chart.Name }} replicas: 1 template: metadata: labels: app: {{ .Chart.Name }} spec: volumes: - name: configs configMap: name: {{ .Chart.Name }} imagePullSecrets: # Название секрета для подключения к container registry - name: gitea-regsecret containers: - name: nginx image: {{ .Values.werf.image.nginx }} ports: - containerPort: 80 volumeMounts: - name: configs mountPath: /etc/nginx/nginx.conf subPath: nginx.conf --- # ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ .Chart.Name }} labels: app: {{ .Chart.Name }} spec: rules: # С помощью переменной $WERF_ENV, которая передаётся при развёртывании, определяем, какой url подставить в окружение - host: {{ pluck .Values.werf.env .Values.app.url | first | default .Values.app.url._default }} http: paths: - path: "/" pathType: ImplementationSpecific backend: service: name: {{ .Chart.Name }} port: number: 80 --- # service.yaml apiVersion: v1 kind: Service metadata: name: {{ .Chart.Name }} labels: app: {{ .Chart.Name }} spec: selector: app: {{ .Chart.Name }} ports: - name: http port: 80 --- # nginx-config-cm.yaml apiVersion: v1 kind: ConfigMap metadata: name: {{ .Chart.Name }} labels: app: {{ .Chart.Name }} data: nginx.conf: | error_log /dev/stderr; events { worker_connections 100000; multi_accept on; } http { charset utf-8; server { listen 80; index index.html; root /app; error_log /dev/stderr; location / { try_files $uri /index.html$is_args$args; } } } --- # values.yaml app: url: _default: 'hello-dev.example-domain.com' stage: 'hello-flant.example-domain.com' prod: 'hello.example-domain.com'
Внимательный читатель заметит, что в Helm-чарте активно используется переменная .Chart.Name, но при этом сам файл chart.yaml, из которого обычно берётся эта переменная, отсутствует. Ошибки нет, .Chart.Name подставляется из переменной project: hello-world, указанной в файле werf.yaml
В манифесте deployment.yaml в директиве image: используется переменная {{ .Values.werf.image.nginx }}. При деплое werf подставит туда image:tag из стадии build. Эту переменную нужно подставлять везде, где используются собранные образы из werf.yaml.
В ресурсе ingress.yaml мы подставляем доменное имя приложения согласно окружению, в которое выкатываем приложение. Название окружения задаётся на стадии выката через ключ команды werf converge --env $ENV или через переменную окружения WERF_ENV. Подобным методом — через Helm-функцию pluck — удобно шаблонизировать переменные.
В ресурсе deployment.yaml есть аннотация "pod-reloader.deckhouse.io/auto": "true", которая показывает модулю DKP pod-reloader, что нужно следить за данным ресурсом и автоматически перезагружать контейнеры, если в связанных конфигурационных файлах или секретах произошли изменения.
Наш чарт готов, добавляем в репозиторий всё, что создали в каталоге:
git add . git commit -am 'add chart' git push
Gitea Actions CI/CD-пайплайн
Gitea Actions копирует подход GitHub Actions, их синтаксисы практически идентичны. Большинство пайплайнов, написанных для GitHub Actions, скорее всего, будут работать и в Gitea Actions.
Gitea Actions поддерживает контекстные переменные от GitHub Actions. На момент написания статьи не поддерживался только метод on: workflow_dispatch (в релизе v1.22), что исключает ручной запуск пайплайна. Разработчики обещают добавить данный функционал в релизе 1.23.
Пайплайн будет состоять из двух стадий — сборки и развёртывания. Триггер для запуска пайплайна — коммит в Gitea.
Сборка (build-and-publish) — на этой стадии в раннер загружается код приложения, затем утилита werf на основании сценария сборки (werf.yaml) собирает и упаковывает код в образ Docker-контейнера. Сборка происходит на основании состояния кода, зафиксированного в коммите, который запустил пайплайн. В финале werf отправляет собранный Docker-образ в container registry.
Развёртывание (deploy) — на этой стадии werf формирует Helm-чарт (производит рендер YAML-манифестов), определяет образ, собранный на предыдущем шаге и подставляет его в Helm-чарт. Далее werf развёртывает Helm-чарт в кластере Kubernetes как Helm-релиз.
В контекстных переменных мы можем определить, из какой ветки был запущен пайплайн, на основе какого события, кто делал коммит и так далее. Эти данные позволяют задавать условия для выполнения тех или иных стадий пайплайна.
В моём примере сборка происходит из любого коммита, то есть Docker-образ будет одинаков для тестового и prod-окружения и займёт меньше места в хранилище образов. Разделение между продуктовым кодом и тестовым желательно проводить за счёт переменных окружения, которые передаются в Helm-релиз на этапе развёртывания.
Для наглядности приложение будет развёртываться в три разных окружения в зависимости от следующих условий:
-
Выкат в dev-окружение из любых веток и тегов, кроме веток master, stage и тегов с маской
release-*. -
Выкат в prod-окружение из ветки мастер или тега с маской
release-*. -
Выкат в stage-окружение из ветки stage.
Пайплайны должны находиться в каталоге .gitea/workflows и иметь формат .yaml. Создадим каталог и пайплайн с произвольным названием:
mkdir -vp .gitea/workflows vim ci-cd.yaml
А теперь рассмотрим готовый пайплайн. Все пояснения будут после.
Готовый пайплайн
name: build and deploy run-name: ${{ gitea.actor }} is testing out Gitea Actions # Условие для запуска пайплайна — запушили коммит в репозиторий on: [push] # Переменные, заданные на глобальном уровне, будут доступны на глобальном уровне во всех стадиях пайплайна env: # Обязательные переменные APP_NAME: hello-world WERF_VERSION: 2 stable WERF_REPO: ${{ vars.WERF_REPO }}/${{ gitea.repository }} WERF_IMAGE_REPO_USER: ${{ vars.WERF_IMAGE_REPO_USER }} WERF_IMAGES_REPO_TOKEN: ${{ secrets.WERF_IMAGES_REPO_TOKEN }} # Устанавливаем аннотации, нужные при развёртывании (необязательные, но полезные переменные) WERF_ADD_ANNOTATION_WERF_RELEASE_CHANNEL: 'werf.io/release-channel=${{ env.WERF_VERSION }}' WERF_ADD_ANNOTATION_PROJECT_GIT: 'project.werf.io/git=${{ gitea.event.repository.html_url }}' WERF_ADD_ANNOTATION_CI_COMMIT: 'ci.werf.io/commit=${{ gitea.event.head_commit.url }}' WERF_ADD_ANNOTATION_GITEA_CI_PIPELINE_URL: 'gitea.ci.werf.io/pipeline-url=${{ gitea.event.repository.html_url }}/actions/runs/${{ gitea.run_id }}' WERF_ADD_ANNOTATION_GITEA_CI_JOB_URL: 'gitea.ci.werf.io/job-url=${{ gitea.event.repository.html_url }}/actions/runs/${{ gitea.run_id }}/jobs/${{ gitea.action }}' # Стадии пайплайна jobs: # Стадия сборки build-and-publish: name: Build and Publish # Указываем тег, определяющий раннер runs-on: werf # Обязательный шаг всех стадий, подгружаем репозиторий и его состояние steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 # Необязательный шаг, добавлен для возможности просмотра массива переменных контекста пайплайна # Используя контекстные переменные, строим условия для запуска стадий и шагов пайплайна - name: Dump Gitea context env: JOB_CONTEXT: ${{ toJson(gitea) }} run: echo "$JOB_CONTEXT" # werf логинится в репозиторий и производит сборку - name: Build run: | source "$(~/bin/trdl use werf 2 stable)" werf cr login -u $WERF_IMAGE_REPO_USER -p ${{ secrets.WERF_IMAGES_REPO_TOKEN }} $WERF_REPO werf build # Развёртывание в dev deploy-dev: name: Deploy dev needs: build-and-publish runs-on: werf # Условие выполнения текущей стадии deploy-dev # Пайплайн запускается не из ветки master, не из ветки stage, не из тега, который начинается с release- if: gitea.ref != 'refs/heads/master' && gitea.ref != 'refs/heads/stage' && ! startsWith(gitea.ref, 'refs/tags/release-') steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 # Шаг развёртывания приложения в кластер # Активируем werf с указанием версии, в данном случае # переменная WERF_ENV участвует в формировании Helm-чарта - name: Deploy env: WERF_ENV: dev run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" werf converge -Z # Развёртывание в prod deploy-prod: name: Deploy prod needs: build-and-publish runs-on: werf if: gitea.ref == 'refs/heads/master' || startsWith(gitea.ref, 'refs/tags/release-') steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 - name: Deploy env: WERF_ENV: prod run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" werf converge -Z # Развёртывание в stage deploy-stage: name: Deploy stage needs: build-and-publish runs-on: werf if: gitea.ref == 'refs/heads/stage' steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 # Добавлены ключи для converge # Меняют поведение по умолчанию # set -x добавлен для демонстрации отладки - name: Deploy env: WERF_ENV: stage run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" set -x werf converge -Z \ --env ${WERF_ENV} \ --namespace "${APP_NAME}-${WERF_ENV}" \ --release "${APP_NAME}-${WERF_ENV}"
Анатомия пайплайна
Краткая структура пайплайна:
name: build and deploy # Произвольное название пайплайна on: [push] # Условие для запуска пайплайна — запушили коммит в репозиторий env: {} # Список переменных окружения, используемых в стадиях и шагах jobs: # Секция стадий build: # Стадия runs-on: werf # Лейбл раннера, на котором запустим код if: some conditions # Условия запуска стадии steps: [] # Шаги стадии deploy: needs: build-and-publish # Зависимость стадии от выполнения других стадий steps: []
На верхнем уровне Gitea Actions-пайплайна задаются глобальные директивы, такие как название, триггеры запуска on: [] и переменные env: []. Триггами запуска пайплайна могут быть несколько событий, например создание issue, форк или удаление ветки. В моём случае это push изменений в репозиторий.
Директиву env: {} можно задать на разных уровнях, и переменные в разных стадиях могут переопределять друг друга. В данном случае задаются глобальные переменные, которые будут использованы на шагах сборки и развёртывания. Важно отметить, что при определении переменных доступа к container registry используются контексты vars и secrets.
env: WERF_REPO: ${{ vars.WERF_REPO }}/${{ gitea.repository }} WERF_IMAGE_REPO_USER: ${{ vars.WERF_IMAGE_REPO_USER }} WERF_IMAGES_REPO_TOKEN: ${{ secrets.WERF_IMAGES_REPO_TOKEN }}
vars и secrets — это массив переменных, которые задаются в Gitea-репозитории, группе или организации. Переменные WERF_REPO, WERF_IMAGE_REPO_USER и WERF_IMAGES_REPO_TOKEN мы задавали в разделе настройки CI/CD.
Переменные вида WERF_ADD_ANNOTATION_* добавляются всем YAML-манифестам во время деплоя Helm-релиза в качестве аннотаций. По ним удобно определять
репозиторий, из которого был развёрнут ресурс, сам пайплайн и его номер. Можно перейти по ссылке и перевыкатить ресурс или отследить изменение в репозитории, которое привело к поломке.
Рассмотрим основные моменты стадий пайплайна.
Директива runs-on: werf с помощью лейбла задаёт раннер, на котором будет запущена стадия.
С помощью директивы if: определяется условие выполнения стадии. В пайплайне в директиве if: используется контекст массива переменных Gitea. if: gitea.ref != 'refs/heads/master' && gitea.ref != 'refs/heads/stage' && ! startsWith(gitea.ref, 'refs/tags/release-'). Данный контекст аналогичен контексту GitHub из GitHub Actions, он тоже поддерживается в Gitea Actions.
needs: — директива, которая задаёт зависимость одной стадии от другой, в данном случае прямая зависимость стадии выката от сборки.
Шаги steps — отдельные процессы внутри стадии. Шагами могут быть shell-команды, например run: echo "$JOB_CONTEXT" или werf build. Также можно использовать уже готовые шаги, встроенные из внешних репозиториев.
Уже готовый функционал используется в шаге Checkout code с помощью директивы uses:
- name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0
На шаге Checkout code в стадию подгружается репозиторий, это нужно делать в каждой стадии пайплайна.
Рассмотрим подробнее шаги Build и Deploy.
- name: Build run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" werf cr login -u $WERF_IMAGE_REPO_USER -p ${{ secrets.WERF_IMAGES_REPO_TOKEN }} $WERF_REPO werf build
В шаге Build с помощью | используется многострочный shell. Команда source активирует werf нужной версии, а с помощью переменной $WERF_VERSION мы можем управлять версиями. Это может быть удобно при обновлении версии или использовании нового функционала. Можно создать эту переменную на глобальном уровне организации, группы или репозитория.
werf cr login — логинимся к container registry, используя переменные и секреты. Можно обращаться к массивам vars или secrets при описании shell-команд.
werf build — запускается сборка Docker-образа на основе сценария werf.yaml.
Шаг Deploy stage:
deploy-stage: steps: - name: Deploy env: WERF_ENV: stage run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" set -x werf converge -Z \ --env ${WERF_ENV} \ --namespace "${APP_NAME}-${WERF_ENV}" \ --release "${APP_NAME}-${WERF_ENV}"
На данном примере наглядно показано, как можно управлять релизами и деплоем. Что и куда будет развёртываться, определяется переменными окружения или ключами командной строки.
WERF_ENV — окружение.
WERF_NAMESPACE — пространство имён, в которое будет развёрнуто приложение, по умолчанию берётся значение project из werf.yaml + WERF_ENV (project_$WERF_ENV).
WERF_RELEASE — имя, которое будет присвоено Helm-release после развёртывания, по умолчанию аналогично WERF_NAMESPACE.
С помощью данных переменных или ключей --env, --namespace, --release мы управляем развёртыванием.
Развёртывания в dev- и prod-окружения производятся командой werf converge -Z без дополнительных ключей, так как отрабатывает поведение по умолчанию, передаём лишь WERF_ENV. Данная переменная в Helm-чарте будет доступна при обращении к встроенному массиву переменных werf {{ .Values.werf.env }}.
Коммитим изменения и отправляем их в репозиторий:
git add . git commit -am 'first deploy' git push
Проверяем, включен ли Gitea Actions: страница репозитория → Settings → Enable Repository Actions.
Теперь можно посмотреть на список пайплайнов и их стадии: страница репозитория → Actions:

Названием пайплайна будет message коммита, на котором пайплайн сработал. Давайте взглянем на стадии выполнения: здесь видно, что сборка и выкат прошли успешно.
Листинг выката:

werf выдаёт подробную информацию по своим процессам, что удобно при отладке.
В листинге выката видны название Helm-релиза и пространство имён, в которое развернулось приложение: Succeeded release "hello-world-dev" (namespace: "hello-world-dev"). В моём случае выкат был не из master-ветки, поэтому приложение было развёрнуто в dev-окружение. Соответственно, при выкате в prod-окружение Helm-release и пространство имён будут называться hello-world-prod.
Бонусом в стадию deploy можно добавить шаг werf plan — эта команда покажет план развёртывания и укажет, какие ресурсы будут изменены при следующем развертывании. А если что-то не так с Helm-чартом, то она выдаст ошибку и расскажет, в чём именно проблема.
- name: Deploy env: WERF_ENV: dev run: | source "$(~/bin/trdl use werf ${{ env.WERF_VERSION }})" werf plan
Очистка container registry
Со временем в container registry накапливаются Docker-образы, которые уже не нужны. Для их эффективной очистки создадим в репозитории пайплайн, который будет выполняться по расписанию и удалять устаревшие Docker-образы проекта.
Пайплайн cleanup.yaml желательно иметь в каждом репозитории, в котором для сборки и развёртывания используется werf.
Файл создаём в директории .gitea/workflows/:
# .gitea/workflows/cleanup.yaml --- name: Cleanup container registry run-name: Cleanup container registry on: schedule: - cron: '05 00 * * *' env: WERF_REPO: ${{ vars.WERF_REPO }}/${{ gitea.repository }} WERF_VERSION: '2 stable' jobs: cleanup: name: Cleanup runs-on: werf steps: - name: Checkout code uses: actions/checkout@v3 - name: Fetch all history for all tags and branches run: git fetch --prune --unshallow - name: Cleanup env: WERF_ENV: stage run: | set -x source "$(~/bin/trdl use werf ${WERF_VERSION:-'2 stable'} )" werf cleanup $WERF_REPO
Особенность пайплайна cleanup.yaml заключается в его триггере активации. Он срабатывает по расписанию и имеет cron-синтаксис:
on: schedule: - cron: '05 00 * * *'
Запуск будет происходить каждый день в 00:05 согласно часовому поясу Gitea.
В финале наш репозиторий будет выглядеть так:
hello-world ├── app │ └── index.html ├── .gitea │ └── workflows │ ├── ci-cd.yaml │ └── cleanup.yaml ├── .helm │ ├── templates │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── nginx-config-cm.yaml │ │ └── service.yaml │ └── values.yaml ├── README.md └── werf.yaml
Заключение
Мы прошли долгий путь и проделали большую работу: настроили Gitea, провели его интеграцию с Deckhouse Kubernetes Platform, настроили автоматизацию CI/CD, научились писать пайплайны Gitea Actions и, наконец, познакомились с отличным лучшим инструментом CI/CD — werf.
Поздравляю, вы великолепны!
P. S.
Читайте также в нашем блоге:
ссылка на оригинал статьи https://habr.com/ru/articles/857100/
Добавить комментарий