| .. | ||
| defaults | ||
| handlers | ||
| meta | ||
| tasks | ||
| README.md | ||
Flux CD — GitOps-эталон для команды разработчиков
Этот документ описывает стандарт организации GitOps-деплоя приложений в Kubernetes через Flux CD.
Является эталоном для всех команд, разворачивающих сервисы в кластере gitlab.gigacoms.info / k8s.
Содержание
- Концепция и термины
- Двухрепозиторная модель: app-repo и fleet-repo
- Структура fleet-репозитория
- Многоветочный деплой: main → production, dev → staging
- Пример 1: Одно приложение, один Pod
- Пример 2: Одно приложение, несколько Pod (Deployment + HPA)
- Пример 3: Несколько компонентов одного приложения (frontend + backend + БД)
- Пример 4: Взаимодействующие независимые приложения (микросервисы)
- CI/CD в app-repo: автоматическое обновление образа
- Secrets: безопасная передача секретов через Flux
- Диагностика и типичные ошибки
- Чеклист перед деплоем нового приложения
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иdevapp-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/→ namespaceproduction(образы из веткиmainapp-repo)apps/staging/→ namespacestaging(образы из веткиdevapp-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:8080orders-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: автоматическое обновление образа
Принцип работы
- Разработчик делает
git pushв веткуmain(илиdev) - CI собирает Docker-образ, тегирует его (
v1.5.2илиdev-abc1234f) - CI пушит образ в Container Registry
- CI клонирует fleet-repo, обновляет тег образа в нужном оверлее, делает commit + push
- 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 в открытом виде. Допустимые подходы:
- Ручное создание (используем сейчас) —
kubectl create secretвручную один раз - Sealed Secrets — шифрование секрета публичным ключом кластера, зашифрованный yaml коммитится в Git
- 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: установить в 2–4x отrequests.memory. Слишком маленький лимит приведёт к постоянным OOMKill. Слишком большой — к неэффективному использованию узла.