Готовим Gitea со вкусом werf CI/CD и Dex-авторизации в кластере Deckhouse Kubernetes Platform. Часть 3

от автора

Привет, Хабр! С вами не опять, а снова Виктор Ашакин, 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-релиз на этапе развёртывания.

Для наглядности приложение будет развёртываться в три разных окружения в зависимости от следующих условий:

  1. Выкат в dev-окружение из любых веток и тегов, кроме веток master, stage и тегов с маской release-*.

  2. Выкат в prod-окружение из ветки мастер или тега с маской release-*.

  3. Выкат в 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/


Комментарии

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

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