k8s/roles/flux/README.md
2026-07-15 11:14:21 +03:00

1502 lines
48 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 периодически (по умолчанию каждые 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
```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:
```
<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
```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<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: ручное создание (текущий стандарт)
```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='<ASK_OPS>' \
-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 <name> --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/<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` |
## Приложение: минимальный ресурсный профиль
Использовать как отправную точку, корректировать под реальную нагрузку:
```yaml
resources:
requests:
cpu: 50m # гарантированное CPU
memory: 64Mi # гарантированная память
limits:
cpu: 200m # максимальное CPU
memory: 256Mi # максимальная память (OOMKill при превышении)
```
> При выборе `limits.memory`: установить в 24x от `requests.memory`. Слишком маленький лимит приведёт к постоянным OOMKill. Слишком большой — к неэффективному использованию узла.