# Flux CD — GitOps-эталон для команды разработчиков Этот документ описывает стандарт организации GitOps-деплоя приложений в Kubernetes через Flux CD. Является эталоном для всех команд, разворачивающих сервисы в кластере `gitlab.gigacoms.info / k8s`. --- ## Содержание 1. [Концепция и термины](#1-концепция-и-термины) 2. [Двухрепозиторная модель: app-repo и fleet-repo](#2-двухрепозиторная-модель-app-repo-и-fleet-repo) 3. [Структура fleet-репозитория](#3-структура-fleet-репозитория) 4. [Многоветочный деплой: main → production, dev → staging](#4-многоветочный-деплой-main--production-dev--staging) 5. [Пример 1: Одно приложение, один Pod](#5-пример-1-одно-приложение-один-pod) 6. [Пример 2: Одно приложение, несколько Pod (Deployment + HPA)](#6-пример-2-одно-приложение-несколько-pod-deployment--hpa) 7. [Пример 3: Несколько компонентов одного приложения (frontend + backend + БД)](#7-пример-3-несколько-компонентов-одного-приложения-frontend--backend--бд) 8. [Пример 4: Взаимодействующие независимые приложения (микросервисы)](#8-пример-4-взаимодействующие-независимые-приложения-микросервисы) 9. [CI/CD в app-repo: автоматическое обновление образа](#9-cicd-в-app-repo-автоматическое-обновление-образа) 10. [Secrets: безопасная передача секретов через Flux](#10-secrets-безопасная-передача-секретов-через-flux) 11. [Диагностика и типичные ошибки](#11-диагностика-и-типичные-ошибки) 12. [Чеклист перед деплоем нового приложения](#12-чеклист-перед-деплоем-нового-приложения) --- ## 1. Концепция и термины **GitOps** — практика, при которой Git-репозиторий является единственным источником истины о состоянии кластера. Flux CD периодически (по умолчанию каждые 1–5 минут) сравнивает желаемое состояние (Git) с фактическим (кластер) и приводит их в соответствие. | Термин | Описание | |---|---| | **fleet-repo** | Git-репозиторий с манифестами Kubernetes. Flux следит за ним и применяет изменения в кластер. В нашем случае: `gitlab.gigacoms.info/k8s/k8s-fleet` | | **app-repo** | Репозиторий с исходным кодом приложения. Содержит `Dockerfile` и CI/CD пайплайн, который собирает образ и обновляет тег в fleet-repo | | **Kustomization** | CRD Flux, описывающий: откуда брать манифесты, в каком namespace применять, от чего зависеть | | **GitRepository** | CRD Flux, описывающий: какой Git-репозиторий наблюдать, какую ветку/тег/коммит | | **HelmRelease** | CRD Flux для деплоя Helm-чарта с управлением версией и values | | **ImageRepository** | CRD Flux для наблюдения за образами в Container Registry | | **ImageUpdateAutomation** | CRD Flux для автоматического обновления тега образа в fleet-repo | | **Overlay** | Kustomize-слой поверх базовой конфигурации — позволяет менять значения для разных окружений без дублирования | ### Принцип работы ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Developer │ │ │ │ │ ├─ git push → app-repo (main или dev) │ │ │ │ │ │ CI/CD в app-repo: │ │ │ 1. docker build + push → registry (тег: sha/version) │ │ │ 2. git commit в fleet-repo → обновить тег образа │ │ │ │ │ └─ fleet-repo (k8s-fleet) │ │ │ │ │ │ Flux (в кластере) каждые N минут: │ │ │ 1. git pull fleet-repo │ │ │ 2. сравнить с кластером │ │ │ 3. kubectl apply изменений │ │ ▼ │ │ Kubernetes Cluster │ │ ├── namespace: production ← из ветки main fleet-repo │ │ └── namespace: staging ← из ветки main fleet-repo │ │ (другой path в том же repo) │ └─────────────────────────────────────────────────────────────────────┘ ``` > **Важно:** Flux читает **один fleet-repo**, но может следить за разными **путями** внутри него для разных окружений. Ветки `main` и `dev` app-repo влияют на разные директории fleet-repo — staging и production. --- ## 2. Двухрепозиторная модель: app-repo и fleet-repo ### app-repo (репозиторий приложения) Содержит: - Исходный код приложения - `Dockerfile` - `.gitlab-ci.yml` — сборка образа и **обновление fleet-repo** - (опционально) базовые Kustomize-манифесты, которые копируются в fleet-repo при инициализации **Не содержит:** - Финальных Kubernetes-манифестов с тегами образов - Секретов ### fleet-repo (репозиторий конфигурации кластера) Единственный источник истины для Flux. Содержит: - Манифесты всех приложений и инфраструктурных компонентов - Kustomize-оверлеи для каждого окружения - HelmRelease-объекты - Ссылки на секреты (но не сами секреты в открытом виде) **Не содержит:** - Исходного кода приложений - `Dockerfile` - Секретов в открытом виде --- ## 3. Структура fleet-репозитория Стандартная структура fleet-repo для нашего кластера: ``` k8s-fleet/ ├── clusters/ │ └── production/ # flux bootstrap --path=clusters/production │ ├── flux-system/ # авто-генерируется flux bootstrap, не трогать вручную │ │ ├── gotk-components.yaml │ │ ├── gotk-sync.yaml │ │ └── kustomization.yaml │ ├── apps.yaml # Flux Kustomization → apps/production/ │ └── infrastructure.yaml # Flux Kustomization → infrastructure/production/ │ ├── apps/ │ ├── base/ # базовые манифесты (без env-специфики) │ │ ├── myapp/ │ │ │ ├── namespace.yaml │ │ │ ├── deployment.yaml │ │ │ ├── service.yaml │ │ │ └── kustomization.yaml # Kustomize kustomization.yaml (не Flux CRD) │ │ └── another-app/ │ │ └── ... │ ├── production/ # production оверлей │ │ ├── kustomization.yaml # Flux Kustomization CRD │ │ └── myapp/ │ │ ├── kustomization.yaml # Kustomize: patch поверх base │ │ └── patch-image.yaml # тег образа для production │ └── staging/ # staging оверлей (из dev ветки app-repo) │ ├── kustomization.yaml # Flux Kustomization CRD │ └── myapp/ │ ├── kustomization.yaml │ └── patch-image.yaml # тег образа для staging │ └── infrastructure/ ├── base/ │ ├── traefik/ │ └── longhorn/ ├── production/ │ └── kustomization.yaml └── staging/ └── kustomization.yaml ``` ### Точка входа Flux: clusters/production/apps.yaml ```yaml # clusters/production/apps.yaml --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: apps namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system # fleet-repo, созданный flux bootstrap path: ./apps/production # Flux следит за этой директорией prune: true # удалять из кластера то, чего нет в Git wait: true # ждать готовности перед следующим шагом timeout: 5m ``` > `prune: true` — критически важный параметр. Без него удалённые из Git ресурсы останутся в кластере. --- ## 4. Многоветочный деплой: main → production, dev → staging ### Концепция Оба окружения (production и staging) существуют в **одном кластере** в **разных namespace**. Flux следит за **одним fleet-repo** (ветка `main`), но за **разными путями** внутри него: - `apps/production/` → namespace `production` (образы из ветки `main` app-repo) - `apps/staging/` → namespace `staging` (образы из ветки `dev` app-repo) CI/CD в app-repo при пуше в `main` обновляет тег в `apps/production/myapp/patch-image.yaml`. CI/CD в app-repo при пуше в `dev` обновляет тег в `apps/staging/myapp/patch-image.yaml`. ### Настройка двух окружений в clusters/production/ ```yaml # clusters/production/apps.yaml --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: apps-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production prune: true wait: true timeout: 5m --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: apps-staging namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/staging prune: true wait: true timeout: 5m ``` ### Зависимости между окружениями (dependsOn) Если staging должен деплоиться только после успешного production (нетипично, но возможно): ```yaml spec: dependsOn: - name: apps-production ``` ### Схема взаимодействия веток и окружений ``` app-repo fleet-repo (k8s-fleet, ветка main) ─────────────────────────────── ────────────────────────────────────── branch: main apps/production/myapp/patch-image.yaml │ image: registry/myapp:v1.5.0 │ CI push → обновляет тег ──────► │ branch: dev apps/staging/myapp/patch-image.yaml │ image: registry/myapp:dev-abc1234 │ CI push → обновляет тег ──────► │ Flux (каждые 5 минут): git pull fleet-repo apply apps/production/ → ns: production apply apps/staging/ → ns: staging ``` --- ## 5. Пример 1: Одно приложение, один Pod **Сценарий:** простой HTTP-сервис (например, API на Go), одна реплика. ### Структура в fleet-repo ``` apps/ ├── base/ │ └── simple-api/ │ ├── namespace.yaml │ ├── deployment.yaml │ ├── service.yaml │ └── kustomization.yaml ├── production/ │ ├── kustomization.yaml │ └── simple-api/ │ ├── kustomization.yaml │ └── patch-image.yaml └── staging/ ├── kustomization.yaml └── simple-api/ ├── kustomization.yaml └── patch-image.yaml ``` ### apps/base/simple-api/namespace.yaml ```yaml apiVersion: v1 kind: Namespace metadata: name: production # для base используем production; staging переопределит через patch ``` ### apps/base/simple-api/deployment.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: simple-api namespace: production spec: replicas: 1 selector: matchLabels: app: simple-api template: metadata: labels: app: simple-api version: "1.0.0" spec: containers: - name: simple-api image: registry.gigacoms.info/myteam/simple-api:latest # тег заменяется патчем ports: - containerPort: 8080 env: - name: APP_ENV value: production resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 128Mi readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 20 ``` ### apps/base/simple-api/service.yaml ```yaml apiVersion: v1 kind: Service metadata: name: simple-api namespace: production spec: selector: app: simple-api ports: - port: 80 targetPort: 8080 type: ClusterIP ``` ### apps/base/simple-api/kustomization.yaml ```yaml # Это Kustomize kustomization.yaml (не Flux CRD) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - deployment.yaml - service.yaml ``` ### apps/production/simple-api/patch-image.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: simple-api namespace: production spec: template: spec: containers: - name: simple-api image: registry.gigacoms.info/myteam/simple-api:v1.5.2 # обновляет CI/CD ``` ### apps/production/simple-api/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: production resources: - ../../base/simple-api patches: - path: patch-image.yaml ``` ### apps/staging/simple-api/patch-image.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: simple-api namespace: staging spec: template: spec: containers: - name: simple-api image: registry.gigacoms.info/myteam/simple-api:dev-abc1234f env: - name: APP_ENV value: staging ``` ### apps/staging/simple-api/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: staging # переопределяет namespace для всех ресурсов из base resources: - ../../base/simple-api patches: - path: patch-image.yaml ``` ### apps/production/kustomization.yaml (Flux Kustomization CRD) ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: simple-api-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/simple-api prune: true targetNamespace: production ``` --- ## 6. Пример 2: Одно приложение, несколько Pod (Deployment + HPA) **Сценарий:** нагруженный HTTP-сервис с горизонтальным масштабированием. ### apps/base/web-service/deployment.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: web-service namespace: production spec: replicas: 2 # минимальное количество; HPA управляет масштабом selector: matchLabels: app: web-service strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 # zero-downtime deploy template: metadata: labels: app: web-service spec: affinity: podAntiAffinity: # разносить Pod по разным нодам preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: web-service topologyKey: kubernetes.io/hostname containers: - name: web-service image: registry.gigacoms.info/myteam/web-service:latest ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 3 ``` ### apps/base/web-service/hpa.yaml ```yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-service namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web-service minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 # не уменьшать быстрее чем раз в 5 минут policies: - type: Pods value: 1 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 30 policies: - type: Pods value: 2 periodSeconds: 60 ``` ### apps/base/web-service/pdb.yaml ```yaml # PodDisruptionBudget — не допускать недоступности при обновлении нод apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: web-service namespace: production spec: minAvailable: 1 selector: matchLabels: app: web-service ``` ### apps/base/web-service/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - deployment.yaml - service.yaml - hpa.yaml - pdb.yaml ``` ### apps/staging/web-service/patch-replicas.yaml В staging HPA и PDB не нужны — экономим ресурсы: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: web-service namespace: staging spec: replicas: 1 # staging всегда 1 реплика ``` ### apps/staging/web-service/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: staging resources: - ../../base/web-service patches: - path: patch-image.yaml - path: patch-replicas.yaml # исключаем HPA и PDB из staging components: [] ``` Или через `kustomization.yaml` с явным списком resources (без hpa.yaml и pdb.yaml): ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: staging resources: - ../../base/web-service/namespace.yaml - ../../base/web-service/deployment.yaml - ../../base/web-service/service.yaml patches: - path: patch-image.yaml - path: patch-replicas.yaml ``` --- ## 7. Пример 3: Несколько компонентов одного приложения (frontend + backend + БД) **Сценарий:** веб-приложение из трёх компонентов в одном namespace. Все три деплоятся вместе, версионируются независимо. ### Структура ``` apps/ ├── base/ │ └── shop/ # одно логическое приложение "shop" │ ├── namespace.yaml │ ├── frontend/ │ │ ├── deployment.yaml │ │ └── service.yaml │ ├── backend/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── configmap.yaml │ ├── postgres/ │ │ ├── statefulset.yaml │ │ ├── service.yaml │ │ └── pvc.yaml │ └── kustomization.yaml ├── production/ │ └── shop/ │ ├── kustomization.yaml │ ├── patch-frontend.yaml │ ├── patch-backend.yaml │ └── patch-postgres.yaml └── staging/ └── shop/ ├── kustomization.yaml ├── patch-frontend.yaml ├── patch-backend.yaml └── patch-postgres.yaml ``` ### apps/base/shop/namespace.yaml ```yaml apiVersion: v1 kind: Namespace metadata: name: shop-production labels: environment: production app.kubernetes.io/part-of: shop ``` ### apps/base/shop/backend/configmap.yaml ```yaml apiVersion: v1 kind: ConfigMap metadata: name: backend-config namespace: shop-production data: DB_HOST: postgres # имя Service внутри namespace DB_PORT: "5432" DB_NAME: shopdb FRONTEND_URL: http://frontend LOG_LEVEL: info ``` ### apps/base/shop/backend/deployment.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: backend namespace: shop-production spec: replicas: 2 selector: matchLabels: app: shop component: backend template: metadata: labels: app: shop component: backend spec: initContainers: - name: wait-for-postgres # ждём БД перед стартом image: busybox:1.36 command: - sh - -c - | until nc -z postgres 5432; do echo "Waiting for postgres..." sleep 2 done containers: - name: backend image: registry.gigacoms.info/myteam/shop-backend:latest ports: - containerPort: 3000 envFrom: - configMapRef: name: backend-config env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: shop-postgres-secret key: password resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi ``` ### apps/base/shop/backend/service.yaml ```yaml apiVersion: v1 kind: Service metadata: name: backend namespace: shop-production spec: selector: app: shop component: backend ports: - port: 80 targetPort: 3000 type: ClusterIP ``` ### apps/base/shop/frontend/deployment.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: shop-production spec: replicas: 2 selector: matchLabels: app: shop component: frontend template: metadata: labels: app: shop component: frontend spec: containers: - name: frontend image: registry.gigacoms.info/myteam/shop-frontend:latest ports: - containerPort: 80 env: - name: BACKEND_URL value: http://backend # DNS-имя Service в том же namespace resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 128Mi ``` ### apps/base/shop/postgres/statefulset.yaml ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres namespace: shop-production spec: serviceName: postgres replicas: 1 selector: matchLabels: app: shop component: postgres template: metadata: labels: app: shop component: postgres spec: containers: - name: postgres image: postgres:16-alpine ports: - containerPort: 5432 env: - name: POSTGRES_DB value: shopdb - name: POSTGRES_USER value: shopuser - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: shop-postgres-secret key: password volumeMounts: - name: data mountPath: /var/lib/postgresql/data resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 1Gi volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: longhorn resources: requests: storage: 10Gi ``` ### apps/base/shop/postgres/service.yaml ```yaml apiVersion: v1 kind: Service metadata: name: postgres namespace: shop-production spec: selector: app: shop component: postgres ports: - port: 5432 targetPort: 5432 type: ClusterIP clusterIP: None # Headless service для StatefulSet ``` ### apps/base/shop/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - backend/configmap.yaml - backend/deployment.yaml - backend/service.yaml - frontend/deployment.yaml - frontend/service.yaml - postgres/statefulset.yaml - postgres/service.yaml ``` ### apps/production/shop/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: shop-production resources: - ../../base/shop patches: - path: patch-frontend.yaml - path: patch-backend.yaml - path: patch-postgres.yaml ``` ### apps/production/shop/patch-frontend.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: shop-production spec: template: spec: containers: - name: frontend image: registry.gigacoms.info/myteam/shop-frontend:v2.1.0 ``` ### apps/staging/shop/kustomization.yaml ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: shop-staging # staging в отдельном namespace resources: - ../../base/shop namePrefix: "" # не добавлять префикс patches: - path: patch-frontend.yaml - path: patch-backend.yaml - path: patch-postgres.yaml - path: patch-staging-replicas.yaml # все компоненты = 1 реплика ``` ### apps/staging/shop/patch-staging-replicas.yaml ```yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: shop-staging spec: replicas: 1 --- apiVersion: apps/v1 kind: Deployment metadata: name: backend namespace: shop-staging spec: replicas: 1 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres namespace: shop-staging spec: replicas: 1 ``` ### Порядок деплоя (dependsOn в Flux Kustomization) Когда postgres должен быть готов до backend: ```yaml # clusters/production/apps.yaml --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: shop-postgres-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/shop/postgres prune: true healthChecks: - apiVersion: apps/v1 kind: StatefulSet name: postgres namespace: shop-production --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: shop-backend-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/shop/backend prune: true dependsOn: - name: shop-postgres-production # backend стартует только после healthy postgres --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: shop-frontend-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/shop/frontend prune: true dependsOn: - name: shop-backend-production ``` --- ## 8. Пример 4: Взаимодействующие независимые приложения (микросервисы) **Сценарий:** несколько независимых сервисов в разных namespace, которые вызывают друг друга по HTTP. ### Топология ``` namespace: auth-production └── auth-service (порт 8080) namespace: orders-production └── orders-service (порт 8080) → вызывает auth-service через cross-namespace DNS namespace: notifications-production └── notifications-service (порт 8080) → вызывает orders-service ``` ### DNS-имена для cross-namespace обращения Внутри Kubernetes полное DNS-имя Service: ``` ..svc.cluster.local ``` Примеры: - `auth-service.auth-production.svc.cluster.local:8080` - `orders-service.orders-production.svc.cluster.local:8080` ### Структура fleet-repo для микросервисов ``` apps/ ├── base/ │ ├── auth-service/ │ │ ├── namespace.yaml │ │ ├── deployment.yaml │ │ └── service.yaml │ ├── orders-service/ │ │ ├── namespace.yaml │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── configmap.yaml # URL других сервисов │ └── notifications-service/ │ ├── namespace.yaml │ ├── deployment.yaml │ ├── service.yaml │ └── configmap.yaml ├── production/ │ ├── kustomization.yaml # включает все три сервиса │ ├── auth-service/ │ │ ├── kustomization.yaml │ │ └── patch-image.yaml │ ├── orders-service/ │ │ ├── kustomization.yaml │ │ └── patch-image.yaml │ └── notifications-service/ │ ├── kustomization.yaml │ └── patch-image.yaml └── staging/ └── ... # аналогично, namespace: *-staging ``` ### apps/base/orders-service/configmap.yaml ```yaml apiVersion: v1 kind: ConfigMap metadata: name: orders-config namespace: orders-production data: # cross-namespace DNS — полный FQDN обязателен AUTH_SERVICE_URL: http://auth-service.auth-production.svc.cluster.local:80 NOTIFICATIONS_SERVICE_URL: http://notifications-service.notifications-production.svc.cluster.local:80 LOG_LEVEL: info ``` ### apps/base/orders-service/deployment.yaml ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: orders-service namespace: orders-production spec: replicas: 2 selector: matchLabels: app: orders-service template: metadata: labels: app: orders-service spec: containers: - name: orders-service image: registry.gigacoms.info/myteam/orders-service:latest ports: - containerPort: 8080 envFrom: - configMapRef: name: orders-config resources: requests: cpu: 100m memory: 128Mi limits: cpu: 300m memory: 256Mi readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 5 ``` ### Staging: cross-namespace URL тоже меняется ```yaml # apps/staging/orders-service/patch-config.yaml apiVersion: v1 kind: ConfigMap metadata: name: orders-config namespace: orders-staging data: AUTH_SERVICE_URL: http://auth-service.auth-staging.svc.cluster.local:80 NOTIFICATIONS_SERVICE_URL: http://notifications-service.notifications-staging.svc.cluster.local:80 LOG_LEVEL: debug # staging = verbose logging ``` ### NetworkPolicy: разрешить inter-namespace трафик явно По умолчанию в Kubernetes трафик между namespace разрешён. Если в кластере включён запретительный NetworkPolicy, нужно явно разрешить: ```yaml # apps/base/auth-service/networkpolicy.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-from-orders namespace: auth-production spec: podSelector: matchLabels: app: auth-service ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: orders-production podSelector: matchLabels: app: orders-service ports: - port: 8080 ``` ### Порядок деплоя микросервисов (dependsOn) ```yaml # clusters/production/apps.yaml --- # auth деплоится первым (ни от чего не зависит) apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: auth-service-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/auth-service prune: true healthChecks: - apiVersion: apps/v1 kind: Deployment name: auth-service namespace: auth-production --- # orders ждёт auth apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: orders-service-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/orders-service prune: true dependsOn: - name: auth-service-production healthChecks: - apiVersion: apps/v1 kind: Deployment name: orders-service namespace: orders-production --- # notifications ждёт orders apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: notifications-service-production namespace: flux-system spec: interval: 5m sourceRef: kind: GitRepository name: flux-system path: ./apps/production/notifications-service prune: true dependsOn: - name: orders-service-production ``` --- ## 9. CI/CD в app-repo: автоматическое обновление образа ### Принцип работы 1. Разработчик делает `git push` в ветку `main` (или `dev`) 2. CI собирает Docker-образ, тегирует его (`v1.5.2` или `dev-abc1234f`) 3. CI пушит образ в Container Registry 4. CI клонирует fleet-repo, обновляет тег образа в нужном оверлее, делает commit + push 5. Flux обнаруживает изменение в fleet-repo и применяет его в кластер ### .gitlab-ci.yml в app-repo (стандартный шаблон) ```yaml variables: REGISTRY: registry.gigacoms.info IMAGE_NAME: $REGISTRY/myteam/simple-api FLEET_REPO: https://oauth2:${GITLAB_FLEET_TOKEN}@gitlab.gigacoms.info/k8s/k8s-fleet.git stages: - build - deploy build: stage: build image: docker:27 services: - docker:27-dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $REGISTRY - | if [ "$CI_COMMIT_BRANCH" = "main" ]; then TAG="v${CI_COMMIT_SHORT_SHA}" OVERLAY="production" else TAG="dev-${CI_COMMIT_SHORT_SHA}" OVERLAY="staging" fi - docker build -t $IMAGE_NAME:$TAG . - docker push $IMAGE_NAME:$TAG - echo "TAG=$TAG" >> build.env - echo "OVERLAY=$OVERLAY" >> build.env artifacts: reports: dotenv: build.env rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "dev" deploy-to-fleet: stage: deploy image: alpine/git:latest needs: - job: build artifacts: true script: - git config --global user.email "ci@gitlab.gigacoms.info" - git config --global user.name "GitLab CI" - git clone --depth=1 $FLEET_REPO /tmp/fleet - | PATCH_FILE="/tmp/fleet/apps/${OVERLAY}/simple-api/patch-image.yaml" cat > $PATCH_FILE << EOF apiVersion: apps/v1 kind: Deployment metadata: name: simple-api namespace: simple-api-${OVERLAY} spec: template: spec: containers: - name: simple-api image: ${IMAGE_NAME}:${TAG} EOF - cd /tmp/fleet - git add apps/${OVERLAY}/simple-api/patch-image.yaml - git diff --cached --quiet || git commit -m "ci: update simple-api ${OVERLAY} to ${TAG}" - git push origin main rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "dev" ``` > **Важно:** `GITLAB_FLEET_TOKEN` — отдельный токен с правами `write_repository` на fleet-repo. Хранить в CI/CD Variables проекта app-repo. Не использовать персональные токены. ### Соглашения по тегам образов | Окружение | Ветка app-repo | Формат тега | Пример | |---|---|---|---| | production | `main` | `v` или semver | `v1.5.2`, `vabc1234f` | | staging | `dev` | `dev-` | `dev-abc1234f` | Никогда не использовать тег `latest` в fleet-repo — он неиммутабелен и Flux не сможет отследить изменение. --- ## 10. Secrets: безопасная передача секретов через Flux ### Принцип Секреты **никогда не хранятся в Git в открытом виде**. Допустимые подходы: 1. **Ручное создание** (используем сейчас) — `kubectl create secret` вручную один раз 2. **Sealed Secrets** — шифрование секрета публичным ключом кластера, зашифрованный yaml коммитится в Git 3. **External Secrets Operator** — секрет хранится во внешнем хранилище (Vault, AWS SM), ESO синхронизирует в кластер ### Подход 1: ручное создание (текущий стандарт) ```bash # Создать секрет вручную — один раз, не через Ansible и не через Git kubectl create secret generic shop-postgres-secret \ --from-literal=password='StrongPassword123!' \ -n shop-production # Для staging отдельно kubectl create secret generic shop-postgres-secret \ --from-literal=password='StagingPassword456!' \ -n shop-staging ``` В манифесте деплоя ссылаться на секрет через `secretKeyRef` (как в примерах выше). Если Flux удалит namespace (через `prune: true`), секрет исчезнет вместе с ним. Документировать секреты в `secrets/README.md` внутри fleet-repo (без значений!): ```markdown # Требуемые секреты (создавать вручную перед первым деплоем) ## shop-production kubectl create secret generic shop-postgres-secret \ --from-literal=password='' \ -n shop-production ``` ### Подход 2: Sealed Secrets (рекомендуется при масштабировании) ```bash # Установить kubeseal brew install kubeseal # или скачать бинарь # Зашифровать секрет публичным ключом кластера kubectl create secret generic shop-postgres-secret \ --from-literal=password='StrongPassword123!' \ --dry-run=client -o yaml | \ kubeseal --controller-namespace flux-system \ --controller-name sealed-secrets \ --format yaml > apps/base/shop/postgres/sealed-secret.yaml # Полученный sealed-secret.yaml безопасно коммитить в Git git add apps/base/shop/postgres/sealed-secret.yaml git commit -m "feat: add shop postgres sealed secret" ``` --- ## 11. Диагностика и типичные ошибки ### Основные команды ```bash # Состояние всех Flux-объектов flux get all -A # Состояние конкретного Kustomization flux get kustomization apps-production -n flux-system # Принудительная синхронизация (не ждать interval) flux reconcile kustomization apps-production --with-source # Логи flux-контроллеров kubectl logs -n flux-system -l app=kustomize-controller --tail=50 kubectl logs -n flux-system -l app=source-controller --tail=50 # Просмотр событий в namespace kubectl get events -n production --sort-by='.lastTimestamp' # Детали конкретного Kustomization kubectl describe kustomization apps-production -n flux-system ``` ### Типичные ошибки и решения | Ошибка | Причина | Решение | |---|---|---| | `Health check failed` | Pod не стал Ready в течение timeout | Проверить `kubectl describe pod`, `kubectl logs` | | `kustomize build failed` | Синтаксическая ошибка в yaml | `kustomize build apps/production/myapp` локально | | `object not found` | Ресурс удалён из Git но остался в кластере без `prune: true` | Добавить `prune: true` или удалить вручную | | `no such file or directory` | Неверный path в Kustomization | Проверить path в CRD, путь относительный от корня repo | | `resource conflict` | Два Kustomization управляют одним ресурсом | Разнести по разным namespace или убрать дублирование | | Flux не видит изменений | Интервал не истёк | `flux reconcile kustomization --with-source` | | Deployment завис на 0/2 | imagePullError | Проверить тег образа и доступность registry | ### Проверка до деплоя (dry-run) ```bash # Локальная проверка Kustomize-сборки kustomize build apps/production/myapp # Проверить что Flux увидит после изменения flux diff kustomization apps-production --path ./apps/production # Синтаксическая проверка всех yaml find apps/ -name '*.yaml' | xargs kubectl apply --dry-run=client -f ``` --- ## 12. Чеклист перед деплоем нового приложения ### В fleet-repo - [ ] Создана директория `apps/base//` с namespace, deployment, service, kustomization.yaml - [ ] Создана директория `apps/production//` с patch-image.yaml и kustomization.yaml - [ ] Создана директория `apps/staging//` с patch-image.yaml и kustomization.yaml - [ ] Добавлен Flux Kustomization CRD в `clusters/production/apps.yaml` - [ ] Настроены `dependsOn` если приложение зависит от других компонентов - [ ] Настроены `healthChecks` если другие компоненты зависят от этого приложения - [ ] `prune: true` установлен во всех Kustomization CRD - [ ] Ресурсы `requests`/`limits` заданы для всех контейнеров - [ ] `readinessProbe` настроен для всех контейнеров - [ ] `kustomize build apps/production/` выполняется без ошибок локально ### В app-repo - [ ] `.gitlab-ci.yml` содержит шаг `deploy-to-fleet` - [ ] Используются иммутабельные теги образов (не `latest`) - [ ] `GITLAB_FLEET_TOKEN` добавлен в CI/CD Variables проекта - [ ] Образ успешно собирается и пушится в registry ### В кластере - [ ] Secrets созданы вручную в нужных namespace перед первым деплоем (documented в secrets/README.md) - [ ] `flux get kustomization ` показывает `Ready=True` - [ ] Pod в состоянии `Running`, все контейнеры `Ready` - [ ] Сервис доступен внутри кластера: `kubectl exec -it -- curl http://` - [ ] (если нужен внешний доступ) IngressRoute добавлен в `inventory/prod/group_vars/traefik.yml` --- ## Приложение: соглашения по именованию | Сущность | Формат | Пример | |---|---|---| | Namespace production | `-production` | `shop-production` | | Namespace staging | `-staging` | `shop-staging` | | Flux Kustomization | `-` | `shop-production` | | Docker-образ production | `registry/team/:v` | `registry.gigacoms.info/myteam/shop-backend:vabc1234f` | | Docker-образ staging | `registry/team/:dev-` | `registry.gigacoms.info/myteam/shop-backend:dev-abc1234f` | | Secret | `--secret` | `shop-postgres-secret` | | ConfigMap | `-config` | `backend-config` | | Service (внутр.) | `` | `backend`, `postgres` | ## Приложение: минимальный ресурсный профиль Использовать как отправную точку, корректировать под реальную нагрузку: ```yaml resources: requests: cpu: 50m # гарантированное CPU memory: 64Mi # гарантированная память limits: cpu: 200m # максимальное CPU memory: 256Mi # максимальная память (OOMKill при превышении) ``` > При выборе `limits.memory`: установить в 2–4x от `requests.memory`. Слишком маленький лимит приведёт к постоянным OOMKill. Слишком большой — к неэффективному использованию узла.