Local Kubernetes Dev — Part 14: Preparing for deployment and CI
Одна база манифестов плюс наложения на окружения, immutable-теги вместо latest, простой пайплайн build → push → apply и осознанное разделение «локалка против прода» — мост от локальной разработки к настоящему деплою.
К этому моменту у вас на k3d-кластере dev крутится myapp: образ собирается, манифесты применяются, Tilt пересобирает всё на лету (см. главу про Tilt). Локально всё хорошо. Но рано или поздно сервис нужно отдать наружу — в staging, а потом и в прод. И вот тут начинается интересное: как сделать так, чтобы прод-деплой не превратился в ручное копирование YAML с правкой пары строк «на глазок».
Эта глава — про мост между вашей локалкой и настоящим деплоем. Мы посмотрим, как описать несколько окружений без копипасты, почему latest в проде — это боль, как выглядит простейший CI-пайплайн и куда расти дальше (спойлер: GitOps).
Одни манифесты — три окружения: dev, staging, prod
Соблазн очевиден: скопировать папку k8s/ в k8s-prod/, поменять количество реплик, домен и тег образа. Один раз — терпимо. Через месяц у вас три почти одинаковые копии, в одной из которых забыли поправить лимит памяти, и поведение прода тихо разъезжается с тем, что вы тестировали. Это называется дрейф конфигурации (configuration drift), и борются с ним инструментами, которые держат одну базу и накладывают сверху только различия.
Отличаются окружения, как правило, в предсказуемых местах:
- число реплик (
replicas); - тег образа;
- ресурсы (requests/limits);
- адрес базы данных и прочие настройки;
- домен в Ingress.
Есть два популярных подхода вынести эти различия. Базовый разбор configMapGenerator (Kustomize) и values-файлов (Helm) был в главе 10 — здесь смотрим на них именно под углом «несколько окружений и CI», не повторяя основ.
Helm: шаблоны + values
Helm — это шаблонизатор для Kubernetes. Манифесты превращаются в шаблоны (Go-templates), а конкретные значения подставляются из values.yaml. На каждое окружение — свой values-файл, который перекрывает дефолтные значения.
1# chart/values.yaml — дефолты (как для dev)
2replicaCount: 1
3image:
4 repository: k3d-registry.localhost:5000/myapp
5 tag: dev
6resources:
7 requests: { cpu: 50m, memory: 128Mi }
8ingress:
9 host: myapp.localhost1# values-prod.yaml — только то, что отличается в проде
2replicaCount: 3
3image:
4 repository: ghcr.io/acme/myapp
5resources:
6 requests: { cpu: 250m, memory: 256Mi }
7 limits: { cpu: 500m, memory: 512Mi }
8ingress:
9 host: api.myapp.comЗдесь хорошо виден переход от сквозного примера к проду: локально образ лежит в k3d-registry.localhost:5000/myapp:dev, а в проде — в боевом реестре ghcr.io/acme/myapp с immutable-тегом. Подробнее о том, что именно меняется при переезде, — в разделе про локалку и прод ниже.
Деплой прода — наложение одного файла поверх другого:
1helm upgrade --install myapp ./chart \
2 -f chart/values.yaml \
3 -f values-prod.yaml \
4 --set image.tag=sha-abc123 # последний источник перекрывает предыдущиеВажный момент про приоритеты. Документация Helm описывает порядок так: values.yaml (дефолт) → values родительского чарта → пользовательский файл через -f → --set. Каждый следующий перекрывает предыдущий, и при нескольких -f важен порядок — последний файл главнее. Это частые грабли: переставили -f местами и получили неожиданное значение реплик в проде.
Kustomize: патчи поверх base
Kustomize идёт от другого конца: никаких шаблонов, только чистый YAML и патчи поверх него. Он встроен в kubectl, отдельно ставить ничего не нужно. Структура — общий base/ и накладки overlays/:
1k8s/
2 base/
3 deployment.yaml
4 service.yaml
5 kustomization.yaml
6 overlays/
7 dev/
8 kustomization.yaml
9 prod/
10 kustomization.yaml
11 replicas-patch.yaml1# overlays/prod/kustomization.yaml
2resources:
3 - ../../base
4namePrefix: prod-
5commonLabels:
6 env: prod
7images:
8 - name: myapp # подменяет образ без правки Deployment
9 newName: ghcr.io/acme/myapp
10 newTag: sha-abc123
11patches:
12 - path: replicas-patch.yamlПрименяется так:
1kubectl apply -k overlays/prod/
2# или собрать и посмотреть, что получится, до применения:
3kustomize build overlays/prod | kubectl apply -f -Обратите внимание на поле images — это аккуратный способ подменить тег образа, не трогая сам Deployment. Пригодится в CI (см. разделы про теги образов и CI-пайплайн). Кроме него в kustomization.yaml живут resources, patches, namePrefix, commonLabels, configMapGenerator и другие трансформеры.
Что выбрать? Helm удобнее, когда нужна параметризация и переиспользование (особенно для сторонних приложений — БД, кэши из главы 9 часто ставят именно Helm-чартами). Kustomize проще и прозрачнее для своих сервисов: видно ровно тот YAML, что уйдёт в кластер. Распространён и гибрид: чужое ставим Helm, своё описываем Kustomize-overlays. Оба подхода понимают и Argo CD, и Flux — о них в разделе про GitOps.
Теги образов: почему latest — боль
Самая частая ошибка новичка — тег latest. Кажется удобным: всегда «свежий» образ. На деле в проде это источник трудноуловимых проблем, и документация Kubernetes прямо рекомендует его избегать: с :latest «сложнее отследить, какая версия запущена, и труднее откатиться».
Разберём механику боли:
latestмутабельный. Содержимое тега меняется незаметно — сегодня подlatestодин код, завтра другой. Воспроизводимости ноль.- Повторный
applyне вызывает rollout. Kubernetes триггерит перевыкатку, только если изменился pod template. Строкаimage: myapp:latestне поменялась — значит, с точки зрения кластера деплоить нечего, хотя образ в registry уже другой. - Рестарт пода может стянуть другой образ. Если под упал и поднимается заново, он может подтянуть новую (возможно, сломанную) версию
latest— без всякого деплоя с вашей стороны. Прод «сам по себе» меняет версию.
Сюда же — imagePullPolicy. Его дефолт зависит от тега: для latest и образов без тега это Always, для конкретного тега — IfNotPresent, для digest — тоже IfNotPresent. С локальными образами IfNotPresent может, наоборот, «залипнуть» на старой версии — об этом подробнее в главе про частые проблемы.
Как правильно. Immutable-теги, которые однозначно указывают на конкретную сборку:
- по короткому commit SHA:
sha-abc1234; - по семверу:
v1.2.3; - максимальная воспроизводимость — пин по digest:
1image: ghcr.io/acme/myapp@sha256:45b23dee...Digest гарантирует, что «каждый раз запускается ровно тот же код». В CI всё это не нужно генерировать руками — есть docker/metadata-action, которая делает теги из git-контекста (type=sha, type=ref, type=semver) и заодно пишет OCI-лейблы вроде org.opencontainers.image.revision (commit SHA), .source, .version. Это связывает образ с конкретным коммитом — потом легко понять, что именно запущено в проде.
Базовый CI-пайплайн: build → push → apply
Простейшая модель деплоя называется push-based: коммит попадает в репозиторий → CI собирает образ → пушит в registry → CI же обновляет манифест и применяет его в кластер. Никакой магии, всё линейно.
На GitHub Actions это собирается из официальных docker-экшенов. Вот рабочий каркас для myapp:
1# .github/workflows/deploy.yml
2name: build-and-deploy
3on:
4 push:
5 branches: [main]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10 permissions:
11 contents: read
12 packages: write
13 steps:
14 - uses: actions/checkout@v4
15
16 - uses: docker/setup-buildx-action@v3 # BuildKit: кэш, multi-arch
17
18 - uses: docker/login-action@v3 # логин в registry
19 with:
20 registry: ghcr.io
21 username: ${{ github.actor }}
22 password: ${{ secrets.GITHUB_TOKEN }}
23
24 - id: meta
25 uses: docker/metadata-action@v5 # immutable-теги из git
26 with:
27 images: ghcr.io/acme/myapp
28 tags: |
29 type=sha
30 type=ref,event=branch
31 type=semver,pattern={{version}}
32
33 - uses: docker/build-push-action@v6 # build + push
34 with:
35 push: true
36 tags: ${{ steps.meta.outputs.tags }}
37 labels: ${{ steps.meta.outputs.labels }}
38
39 - name: Deploy
40 run: |
41 echo "${{ secrets.KUBECONFIG }}" > kubeconfig
42 export KUBECONFIG=kubeconfig
43 kubectl set image deployment/myapp \
44 myapp=ghcr.io/acme/myapp:sha-${GITHUB_SHA::7}Шаг деплоя на практике делают по-разному:
1# вариант с Kustomize
2kustomize build overlays/prod | kubectl apply -f -
3
4# вариант с Helm
5helm upgrade --install myapp ./chart -f values-prod.yaml \
6 --set image.tag=sha-abc123
7
8# точечно обновить только образ
9kubectl set image deployment/myapp myapp=ghcr.io/acme/myapp:sha-$GIT_SHAГлавное — деплой обновляет образ на immutable-тег, а не на latest. Тогда новый pod template отличается от старого, и rollout честно происходит.
У push-модели есть встроенные слабости, которые стоит знать заранее:
- CI хранит ключи от кластера (kubeconfig или облачные credentials). Это расширяет поверхность атаки: кто угодно с доступом к секретам CI получает доступ к проду.
- Возможен дрейф между Git и кластером. Кто-то сделал
kubectl editруками — и состояние кластера разошлось с тем, что записано в репозитории. CI об этом не узнает. - Нет автоматического отката. Если деплой выкатил сломанную версию, возвращать всё придётся вручную.
Именно эти три пункта подталкивают к GitOps (см. следующий раздел).
Что переезжает из локалки, а что — только для прода
Хорошая новость: большая часть вашей работы из предыдущих глав переиспользуется. В этом и смысл production-like локалки (глава 2) — паритет окружений.
Переиспользуется как есть:
- тот же Dockerfile и тот же образ (глава 6) — собранный артефакт один на все окружения;
- те же Helm-чарты или Kustomize-base — меняются только values/overlays;
- liveness/readiness-пробы (глава 13) — нужны и локально, и в проде;
- если используете Skaffold — один skaffold.yaml с профилями. Профили Skaffold накладываются поверх базовой секции (заменой полей или JSON-патчем) и активируются через
-p, переменную окружения или kube-context. Идея авторов прямая: «используйте локально те же команды, что и для удалённого деплоя».
Добавляется только для прода:
- реальный registry и immutable-теги вместо
k3d-registry.localhost:5000иdev; - продакшен-ресурсы: настоящие requests/limits, HPA (горизонтальное автомасштабирование), PodDisruptionBudget, anti-affinity, несколько реплик;
- настоящие Secrets, TLS (например, через cert-manager), боевой Ingress и DNS (глава 11);
- мониторинг, NetworkPolicy, RBAC;
- продакшен-тайминги проб (локальные значения обычно агрессивнее).
И отдельно — что в прод тащить НЕЛЬЗЯ. Все dev-ускорители живут только на локалке:
- Live Update и file-sync из Tilt (глава 8);
- hot-reload (
uvicorn --reload) — в проде uvicorn запускается без него; - открытые debug-порты и dev-тулинг.
Эти вещи удобны в inner dev loop, но в проде они — дыра в безопасности и источник нестабильности. Профили Skaffold, отдельный values-prod или prod-overlay как раз и нужны, чтобы dev-only механизмы физически не попали в прод-манифесты.
GitOps как следующий шаг
Push-модель из предыдущего раздела работает, но её слабости (ключи в CI, дрейф, ручной откат) никуда не делись. Следующая ступень эволюции — GitOps.
Идея простая: Git становится единственным источником истины. Вы не пушите изменения в кластер из CI — вместо этого внутри кластера живёт агент (Argo CD или Flux), который сам тянет (pull) изменения из Git и непрерывно приводит кластер к описанному в репозитории состоянию.
Argo CD описывает себя как «декларативный GitOps-инструмент continuous delivery для Kubernetes». Он работает как контроллер: «непрерывно отслеживает приложения и сравнивает текущее живое состояние с желаемым целевым». Если кто-то поправил кластер руками — агент увидит расхождение (drift) и вернёт всё к тому, что записано в Git. Понимает он и Helm, и Kustomize, и обычный YAML.
Push (CI) против pull (GitOps):
| Push (CI) | Pull (GitOps) | |
|---|---|---|
| Кто применяет | CI извне | агент внутри кластера |
| Где живут ключи к кластеру | в CI | внутри кластера |
| Дрейф | не отслеживается | детектируется и устраняется |
| Откат | вручную | откатить коммит в Git |
Связка получается такая: CI собирает образ с immutable-тегом и пушит в registry → бот (или тот же CI) обновляет тег в Git-манифесте (image.tag в Helm values или newTag в Kustomize images) → GitOps-агент замечает коммит и применяет изменение в кластер. CI больше не держит kubeconfig — его задача заканчивается на пуше образа и коммите в манифесты.
Argo CD и Flux — оба проекта CNCF выпускного уровня (graduated). Argo CD даёт богатый web-UI и app-центричную модель; Flux — более k8s-native, через набор CRD-контроллеров. Для первого знакомства Argo CD обычно проще за счёт наглядного интерфейса.
Разворачивать GitOps в этой статье мы не будем — это тема для отдельного материала. Но знать про него полезно уже сейчас: когда ваш push-пайплайн начнёт упираться в дрейф и ручные откаты, вы будете понимать, куда двигаться.
Итого: одна база манифестов + наложения на окружения (Helm values или Kustomize overlays), immutable-теги вместо latest, простой пайплайн build → push → apply, и осознанное разделение «что из локалки переезжает, а что остаётся только в проде». А дальше — GitOps, когда дорастёте. В следующей главе разберём частые проблемы, на которые натыкаются по дороге.