Kustomize и Argo CD: Dev и Prod без дублирования
Опубликовано:
Используемые термины:
В инструкции разберем, как хранить манифесты для двух окружений в одном GitOps-репозитории без копирования Deployment, Service и Ingress под каждое окружение отдельно. Используем Kustomize для наложения различий поверх общей базы и Argo CD для автоматической синхронизации из Git в кластер.
Каталоги GitOps-репозитория
Каталог Base: общие манифесты
Каталоги Overlay
Проверка рендера Kustomize
Application для Dev и Prod
Проверка sync
Типовое изменение без копипаста base
Возможные проблемы
Каталоги GitOps-репозитория
Создадим структуру репозитория: общий base, overlay для dev, overlay для prod и отдельный каталог под манифесты Argo CD Application.
mkdir -p /opt/gitops-demo/argo/base /opt/gitops-demo/argo/overlays/dev /opt/gitops-demo/argo/overlays/prod /opt/gitops-demo/argo/applications
Перейдем в каталог репозитория, чтобы дальше работать с относительными путями:
cd /opt/gitops-demo
* где:
- /opt/gitops-demo — фиксированный корень репозитория; все пути ниже относительно него.
- argo/base — общие манифесты приложения.
- argo/overlays/dev и argo/overlays/prod — отличия окружений (namespace, реплики, тег образа, Ingress host).
- argo/applications — манифесты Argo CD Application; их не включают в sync path overlays, иначе Application начнет синхронизировать сам себя.
Каталог Base: общие манифесты
Начнем с Deployment. В base описываем только то, что одинаково для dev и prod, а окружение-специфичные значения вынесем в overlays.
vi argo/base/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-web
labels:
app.kubernetes.io/name: demo-web
spec:
replicas: 1
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app.kubernetes.io/name: demo-web
template:
metadata:
labels:
app.kubernetes.io/name: demo-web
spec:
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: demo-web
image: docker.dmosk.ru/demo/web:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
* где:
- maxUnavailable: 0 и maxSurge: 1 — сначала поднимается новый pod, старый гасится после Ready.
- preStop + sleep 10 — окно на обновление Endpoints/Ingress до SIGTERM; terminationGracePeriodSeconds: 45 дает приложению доработать запросы.
- memory requests = limits — Guaranteed QoS; для CPU задан только request.
- readOnlyRootFilesystem — корень только на чтение; временные файлы в /tmp через emptyDir.
- image — базовое имя образа; тег для окружений меняется в overlays через images.
Добавим Service, чтобы обращаться к подам по стабильному адресу внутри кластера:
vi argo/base/service.yml
apiVersion: v1
kind: Service
metadata:
name: demo-web
labels:
app.kubernetes.io/name: demo-web
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: demo-web
ports:
- name: http
port: 80
targetPort: http
Добавим Ingress, чтобы пропускать внешний трафик к сервису. Host в base оставим заглушкой — реальные адреса для dev и prod зададим позже отдельными патчами:
vi argo/base/ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-web
labels:
app.kubernetes.io/name: demo-web
spec:
ingressClassName: nginx
rules:
- host: demo-web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-web
port:
name: http
* ingressClassName и host в base — заглушка; реальные хосты задаются патчами в overlays.
Свяжем три манифеста в один base через kustomization.yml — именно на этот каталог будут ссылаться оба overlay:
vi argo/base/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yml
- service.yml
- ingress.yml
Каталоги Overlay
Отдельно рассмотрим примеры манифестов для dev и prod окружений
Dev окружение
Для dev нужен свой host в Ingress. Опишем его отдельным патчем, который наложится поверх base:
vi argo/overlays/dev/ingress-patch.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-web
spec:
rules:
- host: demo-web-dev.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-web
port:
name: http
Теперь соберем overlay: укажем namespace для dev, число реплик, тег образа и подключим патч Ingress:
vi argo/overlays/dev/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-web-dev
resources:
- ../../base
replicas:
- name: demo-web
count: 1
images:
- name: docker.dmosk.ru/demo/web
newTag: dev
patches:
- path: ingress-patch.yml
* где:
- namespace — все ресурсы overlay уходят в demo-web-dev.
- newTag: dev — тег образа для стенда; имя образа должно совпасть с полем image в base без тега.
- replicas: 1 — для одной реплики anti-affinity не обязателен.
Манифесты Prod
Для prod также зададим свой host в Ingress — уже основной домен, а не поддомен для тестов:
vi argo/overlays/prod/ingress-patch.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-web
spec:
rules:
- host: demo-web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-web
port:
name: http
В prod держим несколько реплик, поэтому добавим patch с anti-affinity — он не даст планировщику разместить все поды на одной ноде:
vi argo/overlays/prod/affinity-patch.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-web
spec:
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: demo-web
topologyKey: kubernetes.io/hostname
Соберем overlay prod: namespace, две реплики, прод-тег образа и оба патча — Ingress и affinity:
vi argo/overlays/prod/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-web-prod
resources:
- ../../base
replicas:
- name: demo-web
count: 2
images:
- name: docker.dmosk.ru/demo/web
newTag: latest
patches:
- path: ingress-patch.yml
- path: affinity-patch.yml
* где:
- newTag: latest — прод-тег для примера; на реальном проде вместо latest стоит указывать конкретный тег релиза или git SHA — так проще откатиться и понятно, какая сборка сейчас в кластере.
- count: 2 — несколько реплик; requiredDuringSchedulingIgnoredDuringExecution разводит их по разным нодам.
- affinity-patch.yml — strategic merge patch по имени Deployment из base.
Проверка рендера Kustomize
Прежде чем публиковать репозиторий, проверим локально, что Kustomize собирает оба overlay без ошибок и подставляет нужные значения:
kubectl kustomize argo/overlays/dev
kubectl kustomize argo/overlays/prod
* в выводе должны отличаться namespace, host Ingress, тег образа и число реплик; общие Deployment/Service/Ingress берутся из одного base. Если kubectl без поддержки Kustomize, ту же проверку сделает автономный бинарник: kustomize build argo/overlays/dev.
Application для Dev и Prod
Опишем Application для dev. Он указывает Argo CD, откуда брать манифесты (repoURL, targetRevision, path) и куда их применять (destination):
vi argo/applications/demo-web-dev.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-web-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://git.example.com/infra/gitops-demo.git
targetRevision: main
path: argo/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: demo-web-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Аналогично опишем Application для prod — отличается только path до overlay и целевой namespace:
vi argo/applications/demo-web-prod.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-web-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://git.example.com/infra/gitops-demo.git
targetRevision: main
path: argo/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: demo-web-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
* где:
- path — каталог с kustomization.yml нужного overlay; Argo CD сам вызывает Kustomize.
- CreateNamespace=true — namespace из overlay создается при первом sync.
- prune / selfHeal — удаление лишнего из кластера и откат ручных правок к состоянию git.
- argo/applications не указан в path — Application-манифесты применяются отдельно через kubectl и не синкаются как часть приложения.
* если Application-ов становится много, их удобнее не применять руками, а собрать в паттерн App of Apps — один корневой Application, который сам синкает каталог argo/applications из Git.
Применим оба Application в кластер — после этого Argo CD начнет следить за репозиторием и синхронизировать оба окружения:
kubectl apply -f argo/applications/demo-web-dev.yml
kubectl apply -f argo/applications/demo-web-prod.yml
После правок Application-файлов закоммитьте их в git для истории, но деплой Application в кластер делайте через kubectl apply (или отдельный bootstrap-Application на каталог applications), а не через path overlay самого demo-web.
Проверка sync
Проверим, что оба Application подтянулись, поды поднялись в своих namespace, а Ingress получил нужный host:
kubectl get applications -n argocd
kubectl get pods -n demo-web-dev
kubectl get pods -n demo-web-prod
kubectl get ingress -n demo-web-dev
kubectl get ingress -n demo-web-prod
Если статус Application вызывает вопросы, посмотрим детали через describe — там видны события и условия синхронизации:
kubectl describe application demo-web-dev -n argocd
* у Application статусы Synced и Healthy; если Unknown/Degraded — смотрите Events и Conditions в describe.
Типовое изменение без копипаста base
Дальнейшая работа с окружениями сводится к правке overlay, а не base. Например, обновим тег образа в dev:
vi argo/overlays/dev/kustomization.yml
* меняете только newTag / replicas / патч Ingress в нужном overlay, коммитите и пушите; automated sync подтянет изменение в соответствующий namespace.
git add argo/overlays/dev/kustomization.yml
git commit -m "Bump demo-web image tag in dev"
git push
Понаблюдаем за применением изменения в реальном времени:
kubectl get application demo-web-dev -n argocd -w
Возможные проблемы
Рассмотрим некоторые тимичные проблемы при работе с кастомайзером Argo CD.
Application не переходит в Synced
В kubectl get applications -n argocd статус висит на OutOfSync или Unknown, automated sync не срабатывает сам по себе.
Причина: path в манифесте Application не совпадает с реальным расположением kustomization.yml, либо в целевом namespace есть ресурс, измененный вручную и заблокировавший selfHeal.
Решение: сверьте path в Application с фактическим путем до overlay в репозитории. Откройте kubectl describe application demo-web-dev -n argocd и проверьте Events и Conditions — там указана точная причина рассинхронизации.
Второе окружение не разворачивается
Application для dev применен и синкается, а для prod поды в namespace не появляются, либо наоборот.
Причина: Application-манифесты не входят в sync path overlays и не применяются автоматически — каждый Application нужно применить в кластер отдельно.
Решение: убедитесь, что оба манифеста применены через kubectl apply -f (demo-web-dev.yml и demo-web-prod.yml). Для большого числа Application удобнее не применять их по одному вручную, а собрать в паттерн App of Apps — один корневой Application, который сам синкает каталог argo/applications из Git.
Изменение в base появилось не сразу в обоих окружениях
После правки base поды в dev обновились, а в prod — еще нет (или наоборот).
Причина: у каждого Application свой independent automated sync с собственным интервалом поллинга Git — окружения не обновляются синхронно друг с другом.
Решение: ждать не нужно дольше обычного интервала синхронизации Argo CD. Если требуется обновление немедленно, синхронизацию для конкретного Application можно запустить вручную из UI или через kubectl.
Ресурсы неожиданно исчезли из namespace
Под, Service или Ingress пропали из кластера без явного удаления вручную.
Причина: при prune: true Argo CD удаляет из кластера все, чего больше нет в Git — например, если overlay случайно стерли, переименовали или сломали путь в path.
Решение: перед изменениями в overlays для prod проверяйте diff в UI Argo CD или через kubectl describe application, прежде чем давать automated sync применить его. Так удаление можно заметить до того, как оно затронет прод.
Так base остается единственным источником общей конфигурации, а Kustomize и Argo CD берут на себя различия между dev и prod и их доставку в кластер.