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

Всем привет! Хочу поделиться вариантом организации динамических окружений для разработки и тестирования с помощью 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.

Комментарии (1)


  1. k3NGuru
    00.00.0000 00:00

    Мы в своем проекте слегка по иному сделали (средствами Gitlab).
    В Dev кластере создается namespace с логином разработчика (берем просто переменную GITLAB_USER)

    после создается также персонализированная БД из ранее заготовленного дампа (данные чисто для отладки)

    Берем значения image из тестового контура (идет как эталон для отладки), и складываем его файлом для Helm чарта.
    Далее благодаря helmwave раскатываем стенд в namespace разработчика.

    Последним этапом идет шаг с отображением инфы, сколько действует стенд (использовали environments), хост до БД и ссылка на дебаг порты (Java Spring)
    Из этого же pipeline после можно удалить БД и Namespace.

    В репозитории каждого сервиса для feature ветки есть шаг Deploy Personal, который деплоит на этот стенд.