Динамические тестовые окружения на ArgoCD и Crossplane

от автора

Принципиальная схема
Принципиальная схема

Всем привет! Хочу поделиться вариантом организации динамических окружений для разработки и тестирования с помощью ArgoCD и применением GitOps подхода на реальном примере.

Статья рассчитана на DevOps инженеров и разработчиков уже хорошо знакомых с такими инструментами как:

  • Kubernetes

  • Helm

  • Crossplane

  • ArgoCD

  • GitLab CI

Краткая логика работы пайплайна.

  1. Разработчик пушит новую ветку c постфиксом ‑dyn в названии в репозиторий с project-backend

  2. Стартует GitLabCI пайплайн:

    • билдит docker image, пушит его в image registry;

    • «идет» в репозиторий manifests, отрезает новую ветку — имя которой идентичное имени ветки в исходном репозитории — project-backend;

    • обновляет image tag контейнера в Helm values для окружения и пушит изменения.

  3. ApplicationSet в ArgoCD отслеживает изменение в репозитории manifests, «видит» новую ветку с постфиксом и создает новый Application вместе с базой.

  4. При удалении ветки из репозитория — удаляется App и БД из облака.

Из официальной документации ArgoCD с переводом:

Argo CD следует схеме GitOps, использующей репозитории Git в качестве источника достоверной информации для определения желаемого состояния приложения.

Argo CD реализован как контроллер kubernetes, который постоянно отслеживает запущенные приложения и сравнивает текущее рабочее состояние с желаемым целевым состоянием (как указано в репозитории Git). Развернутое приложение, текущее состояние которого отличается от целевого состояния, считается OutOfSync. Argo CD сообщает и визуализирует различия, предоставляя средства для автоматической или ручной синхронизации реального состояния с желаемым целевым состоянием. Любые изменения, внесенные в желаемое целевое состояние в репозитории Git, могут автоматически применяться и отражаться в указанных целевых средах.

Crossplane позволяет описывать облачные ресурсы в формате k8s yaml манифестов, «применять» их в кластер и создавать ресурсы в облаке согласно описанному состоянию.

Вводные:

  • используем ресурсы в Yandex Cloud;

  • классический проект -2 репозитория — project-frontend, project-backend;

  • артефакты (docker image) проекта «собираются» в GitLab CI;

  • собранные артефакты проекта — 2 docker image — project-frontend, project-backend;

  • приложение деплоится в Kubernetes cluster через Helm (имеет Helm chart);

  • Git репозиторий manifests с Helm charts приложения и crossplane ресурсов (БД, DataTransfer и т. д.);

  • для работы project-backend необходима база данных — PostgreSQL — для создания БД используем Yandex Cloud DataTransfer — копируем данные из исходной — «эталонной» БД в новую.

Структура репозитория manifests

argocd:   argo-application-sets:     - project-dyn-envs-manifest-repo-scm.yaml # AppSet for project   argo-projects:     - project-dyn.yaml # ArgoCD project for dyn envs  charts: # project Helm Charts   project-backend:   project-dyn-infra   project-frontend   project-stack: # Main project chart - in deps has back, front and infra chart     ci:       dyn:         - dyn-values.yaml  custom-manifests:   crossplane-dyn-envs     external:        # external resources created not from crossplane        # used for dyn envs (DB clusters, VPS, etc) - imported to crossplane       folders       postgres       vpc   crossplane-provider-config-yc # YC provider config

project-dyn-envs-manifest-repo-scm.yaml

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata:   name: project-stack-dyn   namespace: argocd spec:   generators:     - scmProvider:         cloneProtocol: ssh         filters:           - branchMatch: .*PROJECT.*-dyn             repositoryMatch: manifests         gitlab:           group: "12345678"            allBranches: true           includeSubgroups: true           tokenRef:             secretName: gitlab-token             key: token   template:     metadata:       name: "{{branchNormalized}}"     spec:       source:         path: charts/project-stack         repoURL: https://gitlab.com/manifests.git         targetRevision: "{{branch}}"         helm:           valueFiles:             - ci/dyn/dyn-values.yaml           values: |             project-backend:               extraVolumes:                 - name: dynenv                   configMap:                      name: {{branchNormalized}}-project-backend                 - name: dynentrypointinitdta                   configMap:                      name: {{branchNormalized}}-project-backend-entrypointinitdta                     defaultMode: 0755               envConfig:                 dynenv: |                   BRANCH_NAME="{{branchNormalized}}"                   TARGET_DB="{{branchNormalized}}-t-db"                 FRONT_BASE_URL: {{branchNormalized}}.dyn.project.ru                 APPLICATION_REDIS_KEY_PREFIX: "project:{{branchNormalized}}:"                 DATASOURCE_URL: "jdbc:postgresql://db-dyn-project.ru:6432/{{branchNormalized}}-t-db"                 DATASOURCE_USERNAME: "{{branchNormalized}}-t-db-user"                 DATASOURCE_PASSWORD: "12345678"             project-ingress:               hosts:                 - host: {{branchNormalized}}.dyn.project.ru               tls:                 - secretName: {{branchNormalized}}-dyn-project-ru                   hosts:                     - {{branchNormalized}}.dyn.project.ru             project-dyn-infra:               enabled: true               fullnameOverride: {{branchNormalized}}               namespace: argo-app-project-dyn               dataTransfer:                 enabled: "true"                 endpoints:                   target:                     dbName: db-project-dyn-{{branchNormalized}}                     dbUser: db-user-project-dyn-{{branchNormalized}}       project: project-dyn       destination:         namespace: 'argo-app-project-dyn'         server: https://XXX.XXX.XXX.XXX       syncPolicy:         automated:           selfHeal: true           prune: true           allowEmpty: true         syncOptions:           - CreateNamespace=true 

ApplicationSet использует SCM provider geneator — позволяет по тригеру (появлению в репозитории manifestsновой ветки, имя которой попадает по Regexp создавать новое приложения в ArgoCD) AppSet генерирует параметры (helm values) для нового приложения на основе имени ветки. Можно использовать в параметрах sha комита или например имя репозитория, полный список параметров можно посмотреть в документации генератора.

project-stack Helm Chart

apiVersion: v2 description: project stack (project) Helm chart type: application maintainers:   - name: xxx     email: xxx version: 0.1.0 appVersion: 1.0.0 kubeVersion: ">=1.23.0-0" keywords: - project annotations:   "finalizers": "resources-finalizer.argocd.argoproj.io"   "gitlab.com/links": |     - name: Chart Source       url: https://gitlab.com/project     - name: Upstream Projects       url: https://gitlab.com/project dependencies:   # Project   - name: project-frontend     condition: project-frontend.enabled     version: "0.1.*"     repository: "file://../project-frontend"   - name: project-backend     condition: project-backend.enabled     version: "0.1.*"     repository: "file://../project-backend"   - name: project-ingress     condition: project-ingress.enabled     version: "0.1.*"     repository: "file://../project-ingress"   - name: project-dyn-infra     condition: project-dyn-infra.enabled     version: "0.1.*"     repository: "file://../project-dyn-infra"

Crossplane

Допустим crossplane уже установлен и сконфигурирован в кластере.

Crossplane используем для создания новой БД для project-bakcned и DataTranser. При создании окружения БД через DataTransfer копируется из исходной — эталонной БД в новую созданную для окружения.

Манифесты Crossplane «упакованы» в Helm chart, приведу пример templates и valuesдля Yandex Cloud.

Новая БД для project-backend db-target.yaml.

Тут важный момент — мы используем отдельно созданный кластер PostgreSQL для всех БД в динамических окружениях, который под управлением Terraform — он импортирован в Crossplane как внешний ресурс. Но никто не мешает создавать новые кластера Postgres из Crosplane.

{{- if (eq .Values.dataTransfer.enabled "true") }} apiVersion: mdb.yandex-cloud.jet.crossplane.io/v1alpha1 kind: PostgresqlDatabase metadata:   name: {{ template "project-dyn-infra.fullname" . }}-t-db spec:   #  deletionPolicy: Orphan   providerConfigRef:     name: {{ toYaml .Values.providerConfigRef.name }}   forProvider:     name: {{ template "project-dyn-infra.fullname" . }}-t-db     clusterId: {{ toYaml .Values.targetDb.clusterId }}     owner: {{ template "project-dyn-infra.fullname" . }}-t-db-user     lcCollate: en_US.UTF-8     lcType: en_US.UTF-8     extension:       - name: uuid-ossp {{- end }} 

БД пользователь — в Yandex Cloud пользователей можно создавать только через API:

db-user-target.yaml

{{- if (eq .Values.dataTransfer.enabled "true") }} apiVersion: mdb.yandex-cloud.jet.crossplane.io/v1alpha1 kind: PostgresqlUser metadata:   name: {{ template "project-dyn-infra.fullname" . }}-t-db-user spec:   #  deletionPolicy: Orphan   providerConfigRef:     name: {{ toYaml .Values.providerConfigRef.name }}   forProvider:     name: {{ template "project-dyn-infra.fullname" . }}-t-db-user     passwordSecretRef:       name: {{ template "project-dyn-infra.fullname" . }}-t-db-password       namespace: {{ template "project-dyn-infra.namespace" . }}       key: password     clusterId: {{ toYaml .Values.targetDb.clusterId }}     connLimit: 35     login: true     grants:       - mdb_admin       - mdb_replication {{- end }} 

datatransfer.yaml

# Secret с паролем от новой БД {{- if (eq .Values.dataTransfer.enabled "true") }} apiVersion: v1 kind: Secret metadata:   name: {{ template "project-dyn-infra.fullname" . }}-t-db-password type: Opaque data:   password: {{ toYaml .Values.targetDb.password }}  --- # Secret с паролем от БД источника apiVersion: v1 kind: Secret metadata:   name: {{ template "project-dyn-infra.fullname" . }}-s-db-password type: Opaque data:   password: {{ toYaml .Values.dataTransfer.endpoints.source.password }}  --- apiVersion: datatransfer.yandex-cloud.jet.crossplane.io/v1alpha1 kind: Endpoint metadata:   name: {{ template "project-dyn-infra.fullname" . }}-source spec:   #  deletionPolicy: Orphan   forProvider:     folderId: {{ toYaml .Values.dataTransfer.endpoints.source.folderId }}     name: {{ template "project-dyn-infra.fullname" . }}-source     settings:       - postgresSource:         - database: {{ toYaml .Values.dataTransfer.endpoints.source.dbName }}           user: {{ toYaml .Values.dataTransfer.endpoints.source.dbUser }}           password:             - rawSecretRef:                 name: {{ template "project-dyn-infra.fullname" . }}-s-db-password                 namespace: {{ template "project-dyn-infra.namespace" . }}                 key: password           objectTransferSettings:             - function: AFTER_DATA           connection:           - mdbClusterIdRef:               name: {{ toYaml .Values.dataTransfer.endpoints.source.mdbClusterIdRef }}   providerConfigRef:     name: {{ toYaml .Values.providerConfigRef.name }}  --- apiVersion: datatransfer.yandex-cloud.jet.crossplane.io/v1alpha1 kind: Endpoint metadata:   name: {{ template "project-dyn-infra.fullname" . }}-target spec:   #  deletionPolicy: Orphan   forProvider:     folderId: {{ toYaml .Values.dataTransfer.endpoints.target.folderId }}     name: {{ template "project-dyn-infra.fullname" . }}-target     settings:       - postgresTarget:         - database: {{ template "project-dyn-infra.fullname" . }}-t-db           user: {{ template "project-dyn-infra.fullname" . }}-t-db-user           password:             - rawSecretRef:                 name: {{ template "project-dyn-infra.fullname" . }}-t-db-password                 namespace: {{ template "project-dyn-infra.namespace" . }}                 key: password           connection:           - mdbClusterIdRef:               name: {{ toYaml .Values.dataTransfer.endpoints.target.mdbClusterIdRef }}   providerConfigRef:     name: {{ toYaml .Values.providerConfigRef.name }}  --- apiVersion: datatransfer.yandex-cloud.jet.crossplane.io/v1alpha1 kind: Transfer metadata:   name: {{ template "project-dyn-infra.fullname" . }} spec: #  deletionPolicy: Orphan   forProvider:     folderId: {{ toYaml .Values.dataTransfer.config.folderId }}     name: {{ template "project-dyn-infra.fullname" . }}     sourceIdRef:       name: {{ template "project-dyn-infra.fullname" . }}-source     targetIdRef:       name: {{ template "project-dyn-infra.fullname" . }}-target     type: {{ toYaml .Values.dataTransfer.config.type }}   providerConfigRef:     name: {{ toYaml .Values.providerConfigRef.name }} {{- end }} 

project-dyn-infra — values

enabled: false  providerConfigRef:   name: yc  targetDb:   clusterId: XXX   name: db-project-dyn-{{branchNormalized}}   owner: db-user-project-dyn-{{branchNormalized}}   user: db-user-project-dyn-{{branchNormalized}}   password: XXX # base64  dataTransfer:   enabled: "false"   config:     folderId: XXX     type: "SNAPSHOT_ONLY"   endpoints:     source:       folderId: XXX       dbName: propject_source_db       dbUser: project_source_db_user       password: XXX       mdbClusterIdRef: XXX     target:       folderId: XXX       mdbClusterIdRef: project-dyn-envs 

Через Crossplane невозможно изменять параметры endpoints (политика копирования, порядок переноса данных) в DataTransfer, так же нет возможности активировать DataTransfer с параметром SNAPSHOT_ONLY — это возможно сделать только через API.

Для обхода ограничений в values (project-backend) init container который изменяет параметры DataTransfer после его создания, активирует трансфер и проверяет его статус. После успешного завершения — стартует контейнер с беком.

В чарт с беком (project-backend) добавлен ConfigMap в котором описан скрипт — entrypoint для init container (dynInitEntrypointDta) — скрипт монтируется как extraVolumes в init container.

Для определения — с каким именно трансфером должен работать скрипт «проброшено» имя ветки через ConfigMap — dynenv в init container

Транспорт 

Что бы не генерить новые DNS записи для окружений заведена запись указывающая на балансир кластера — *.dyn.project.ru

Ограничение и недостатки 

  • имя ветки — id окружения;

  • постфикс dyn и имя проекта в имени ветки (PROJECT-7777-my-branch-dyn);

  • одинаковое имя ветки в репозитории фронта и бека;

  • длина имени ветки не более 63 символов;

  • при создании окружения берется из эталонной БД на момент деплоя окружения;

  • при обновлении версии бека — БД не пересоздается — если нужно пересоздать — дропаем App в ArgoCD;

  • трансфер БД требует времени;

  • нет актуализации версии бека и фронта с продом (деплоим фронт — фронт соберется, версия бека будет из dev ветки manifests (project-stack/dyn/values.yaml);

  • при копировании БД не переносятся math views — ограничение DataTransfer;

  • список созданных окружений можно посмотреть только в ArgoCD;

  • удаление окружения и ресурсов = удаление ветки в manifests.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Используете ArgoCD?
0% Да, в проде 0
33.33% Да, для тестовых окружений 1
0% Нет, но хотим 0
66.67% Нет 2
Проголосовали 3 пользователя. Воздержавшихся нет.

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


Комментарии

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

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