
Всем привет! Хочу поделиться вариантом организации динамических окружений для разработки и тестирования с помощью ArgoCD и применением GitOps подхода на реальном примере.
Статья рассчитана на DevOps инженеров и разработчиков уже хорошо знакомых с такими инструментами как:
Kubernetes
Helm
Crossplane
ArgoCD
GitLab CI
Краткая логика работы пайплайна.
Разработчик пушит новую ветку c постфиксом ‑dyn в названии в репозиторий с
project-backendСтартует GitLabCI пайплайн:
билдит docker image, пушит его в image registry;
«идет» в репозиторий
manifests, отрезает новую ветку — имя которой идентичное имени ветки в исходном репозитории —project-backend;обновляет image tag контейнера в Helm values для окружения и пушит изменения.
ApplicationSet в ArgoCD отслеживает изменение в репозитории manifests, «видит» новую ветку с постфиксом и создает новый Application вместе с базой.
При удалении ветки из репозитория — удаляется 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.