![Принципиальная схема Принципиальная схема](https://habrastorage.org/getpro/habr/upload_files/941/916/29c/94191629c75c11379faba92f59710d10.png)
Всем привет! Хочу поделиться вариантом организации динамических окружений для разработки и тестирования с помощью 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
.
k3NGuru
Мы в своем проекте слегка по иному сделали (средствами Gitlab).
В Dev кластере создается namespace с логином разработчика (берем просто переменную GITLAB_USER)
после создается также персонализированная БД из ранее заготовленного дампа (данные чисто для отладки)
Берем значения image из тестового контура (идет как эталон для отладки), и складываем его файлом для Helm чарта.
Далее благодаря helmwave раскатываем стенд в namespace разработчика.
Последним этапом идет шаг с отображением инфы, сколько действует стенд (использовали environments), хост до БД и ссылка на дебаг порты (Java Spring)
Из этого же pipeline после можно удалить БД и Namespace.
В репозитории каждого сервиса для feature ветки есть шаг Deploy Personal, который деплоит на этот стенд.