k8s/roles/flux
2026-07-15 11:14:21 +03:00
..
defaults initial commit from gigacoms 2026-07-15 11:14:21 +03:00
handlers initial commit from gigacoms 2026-07-15 11:14:21 +03:00
meta initial commit from gigacoms 2026-07-15 11:14:21 +03:00
tasks initial commit from gigacoms 2026-07-15 11:14:21 +03:00
README.md initial commit from gigacoms 2026-07-15 11:14:21 +03:00

Flux CD — GitOps-эталон для команды разработчиков

Этот документ описывает стандарт организации GitOps-деплоя приложений в Kubernetes через Flux CD.
Является эталоном для всех команд, разворачивающих сервисы в кластере gitlab.gigacoms.info / k8s.


Содержание

  1. Концепция и термины
  2. Двухрепозиторная модель: app-repo и fleet-repo
  3. Структура fleet-репозитория
  4. Многоветочный деплой: main → production, dev → staging
  5. Пример 1: Одно приложение, один Pod
  6. Пример 2: Одно приложение, несколько Pod (Deployment + HPA)
  7. Пример 3: Несколько компонентов одного приложения (frontend + backend + БД)
  8. Пример 4: Взаимодействующие независимые приложения (микросервисы)
  9. CI/CD в app-repo: автоматическое обновление образа
  10. Secrets: безопасная передача секретов через Flux
  11. Диагностика и типичные ошибки
  12. Чеклист перед деплоем нового приложения

1. Концепция и термины

GitOps — практика, при которой Git-репозиторий является единственным источником истины о состоянии кластера. Flux CD периодически (по умолчанию каждые 15 минут) сравнивает желаемое состояние (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

# 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/

# 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 (нетипично, но возможно):

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

apiVersion: v1
kind: Namespace
metadata:
  name: production          # для base используем production; staging переопределит через patch

apps/base/simple-api/deployment.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

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

# Это 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

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

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

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

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)

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

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

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

# 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

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 не нужны — экономим ресурсы:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-service
  namespace: staging
spec:
  replicas: 1              # staging всегда 1 реплика

apps/staging/web-service/kustomization.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):

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

apiVersion: v1
kind: Namespace
metadata:
  name: shop-production
  labels:
    environment: production
    app.kubernetes.io/part-of: shop

apps/base/shop/backend/configmap.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

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

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

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

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

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

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

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

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

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

---
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:

# 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:

<service-name>.<namespace>.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

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

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 тоже меняется

# 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, нужно явно разрешить:

# 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)

# 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 (стандартный шаблон)

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<SHA-7> или semver v1.5.2, vabc1234f
staging dev dev-<SHA-7> 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: ручное создание (текущий стандарт)

# Создать секрет вручную — один раз, не через 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 (без значений!):

# Требуемые секреты (создавать вручную перед первым деплоем)

## shop-production
kubectl create secret generic shop-postgres-secret \
  --from-literal=password='<ASK_OPS>' \
  -n shop-production

Подход 2: Sealed Secrets (рекомендуется при масштабировании)

# Установить 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. Диагностика и типичные ошибки

Основные команды

# Состояние всех 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 <name> --with-source
Deployment завис на 0/2 imagePullError Проверить тег образа и доступность registry

Проверка до деплоя (dry-run)

# Локальная проверка 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/<app-name>/ с namespace, deployment, service, kustomization.yaml
  • Создана директория apps/production/<app-name>/ с patch-image.yaml и kustomization.yaml
  • Создана директория apps/staging/<app-name>/ с 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-name> выполняется без ошибок локально

В 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 <name> показывает Ready=True
  • Pod в состоянии Running, все контейнеры Ready
  • Сервис доступен внутри кластера: kubectl exec -it <pod> -- curl http://<service>
  • (если нужен внешний доступ) IngressRoute добавлен в inventory/prod/group_vars/traefik.yml

Приложение: соглашения по именованию

Сущность Формат Пример
Namespace production <app>-production shop-production
Namespace staging <app>-staging shop-staging
Flux Kustomization <app>-<env> shop-production
Docker-образ production registry/team/<app>:v<sha> registry.gigacoms.info/myteam/shop-backend:vabc1234f
Docker-образ staging registry/team/<app>:dev-<sha> registry.gigacoms.info/myteam/shop-backend:dev-abc1234f
Secret <app>-<purpose>-secret shop-postgres-secret
ConfigMap <component>-config backend-config
Service (внутр.) <component> backend, postgres

Приложение: минимальный ресурсный профиль

Использовать как отправную точку, корректировать под реальную нагрузку:

resources:
  requests:
    cpu: 50m       # гарантированное CPU
    memory: 64Mi   # гарантированная память
  limits:
    cpu: 200m      # максимальное CPU
    memory: 256Mi  # максимальная память (OOMKill при превышении)

При выборе limits.memory: установить в 24x от requests.memory. Слишком маленький лимит приведёт к постоянным OOMKill. Слишком большой — к неэффективному использованию узла.