Przejdź do głównej zawartości

Wzorce orkiestracji Kubernetes

Twój rollout właśnie położył API. Sonda readiness zrobiła się zielona, zanim pula połączeń do bazy danych skończyła się rozgrzewać, więc Kubernetes przekierował ruch produkcyjny na pody, które natychmiast zaczęły zwracać 500-tki. Dashboard jest czerwony, PM pyta o ETA, a manifest, który to spowodował, wygenerowało narzędzie AI, które nie wiedziało, że Twoja pula potrzebuje ośmiu sekund na zapełnienie.

To jest prawdziwe ryzyko związane z konfiguracją Kubernetes generowaną przez AI: YAML aplikuje się bez błędów, a mimo to wdraża awarię. Rozwiązaniem nie jest rezygnacja z AI — jest sterowanie nim za pomocą promptów, które kodują Twoje rzeczywiste ograniczenia (timing sond, zapas zasobów, budżety zakłóceń), oraz przeglądanie wyniku pod kątem trybów awarii, które gryzą na produkcji.

  • Prompt do skopiowania, który generuje Deployment zero-downtime z sondami dostrojonymi do prawdziwego okna rozgrzewki — a nie do domyślnego dla AI initialDelaySeconds: 0.
  • Prompt do debugowania, który zamienia wynik kubectl describe + kubectl logs w uszeregowaną listę przyczyn źródłowych dla CrashLoopBackOff, OOMKilled i ImagePullBackOff.
  • Prompt, który przekształca luźne manifesty w sparametryzowany wykres Helm z walidacją wartości — wykorzystując utrzymywane zależności wykresów, a nie wycofany katalog Bitnami.
  • Playbook „Gdy to się zepsuje” dla pięciu awarii, które manifesty generowane przez AI powodują najczęściej: wyścigi sond, OOMKille, PDB blokujące drenowanie węzłów, błędy pobierania obrazów i zacięte synchronizacje ArgoCD.

Wzorzec jest taki sam we wszystkich trzech narzędziach: opisz workload i jego ograniczenia operacyjne, pozwól narzędziu wygenerować kod, a następnie porównaj wynik z tym, co naprawdę by zawiodło. Różni się to, jak wywołujesz każde z narzędzi — a ta różnica ma znaczenie w pracy z Kubernetes, gdzie często iterujesz nad katalogiem manifestów.

Otwórz czat agenta (Cmd/Ctrl+I), wskaż katalog przez @k8s/ i pozwól agentowi pisać pliki bezpośrednio. Checkpointy Cursora pozwalają zaakceptować Deployment, a potem cofnąć samo HPA, jeśli wygenerował błędny cel metryki. Trzymaj otwarty @k8s/values.yaml, aby edycje inline pozostały ograniczone do jednego pliku.

Mocną stroną jest tu przegląd wizualny: Cursor pokazuje diff dla każdego manifestu, którego dotyka, więc wyłapiesz maxUnavailable: 0, które zablokowałoby Deployment z jedną repliką, zanim go zaaplikujesz.

Konfiguracja MCP jest identyczna we wszystkich trzech narzędziach — wszystkie mówią protokołem Model Context Protocol. Serwer MCP dla Kubernetes pozwala narzędziu odpytywać stan klastra na żywo (co faktycznie działa, ostatnie zdarzenia) zamiast zgadywać:

Okno terminala
# Claude Code
claude mcp add k8s -- npx -y kubernetes-mcp-server
# Cursor / Codex: add the same server to .mcp.json
# (Cursor reads .cursor/mcp.json; Codex reads ~/.codex/config.toml or .mcp.json)
{
"mcpServers": {
"k8s": {
"command": "npx",
"args": ["-y", "kubernetes-mcp-server"]
}
}
}

Z podłączonym klastrem „dlaczego pod orders się restartuje?” dostaje odpowiedź na podstawie prawdziwego wyniku kubectl get events zamiast ogólnej listy kontrolnej.

Krok 1 — Wygeneruj Deployment, który nie ściga się z własnymi sondami

Dział zatytułowany „Krok 1 — Wygeneruj Deployment, który nie ściga się z własnymi sondami”

Najczęstszą wadą manifestów generowanych przez AI jest sonda readiness, która przechodzi, zanim aplikacja jest naprawdę gotowa. Zakoduj okno rozgrzewki w prompcie, a wygenerowany startupProbe poprawnie zablokuje pozostałe sondy.

Oto kształt, jakiego powinieneś się spodziewać — zwróć uwagę, jak startupProbe chroni powolny rozruch, a maxUnavailable: 0 gwarantuje brak spadków pojemności podczas rolloutu:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: production
labels:
app: orders-api
tier: api
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: orders-api
topologyKey: kubernetes.io/hostname
containers:
- name: orders-api
image: myregistry/orders-api:v1.4.0
ports:
- name: http
containerPort: 8080
- name: metrics
containerPort: 9090
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
startupProbe: # gates liveness/readiness until the pool warms
httpGet:
path: /health/startup
port: http
periodSeconds: 2
failureThreshold: 30 # up to 60s before the app is declared ready
readinessProbe:
httpGet:
path: /health/ready
port: http
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 10
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]

Co sprawdzić przed zaaplikowaniem: jeśli AI wypisze initialDelaySeconds na sondzie readiness zamiast startupProbe, odrzuć to — initialDelaySeconds to sztywne zgadywanie, podczas gdy startupProbe dostosowuje się do powolnego rozruchu i powstrzymuje liveness przed zabiciem poda w trakcie rozgrzewki.

Sam Deployment jest nieosiągalny. Wygeneruj Service, Ingress i domyślnie odrzucającą (default-deny) NetworkPolicy razem, aby workload był jednocześnie udostępniony i zablokowany. Poniższe manifesty mają kształt produkcyjny — TLS przez cert-manager, ograniczanie rate na Ingressie i egress ograniczony do Postgresa, Redisa i DNS:

service.yaml
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: production
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
selector:
app: orders-api
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: orders-api
namespace: production
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts: [api.example.com]
secretName: api-tls-secret
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: orders-api
port:
number: 80
---
# networkpolicy.yaml — default-deny ingress, scoped egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: orders-api
namespace: production
spec:
podSelector:
matchLabels:
app: orders-api
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
name: production
ports:
- protocol: TCP
port: 5432 # PostgreSQL
- protocol: TCP
port: 6379 # Redis
- to: # DNS must be allowed explicitly
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53

Gdy te same manifesty muszą trafić na dev, staging i prod, poproś narzędzie o wyodrębnienie ich do wykresu z walidacją wartości. Ważna korekta w 2026: nie pozwól, by AI domyślnie sięgnęło po zależności wykresów Bitnami. Od 28 sierpnia 2025 publiczny katalog Bitnami został okrojony do podzbioru społecznościowego dostępnego tylko jako „latest”, a starsze tagi obrazów przeniesiono do bitnamilegacy, więc piny wykresów postgresql/redis, których narzędzia AI nauczyły się z danych treningowych, prowadzą teraz do usuniętych lub zepsutych obrazów. Zamiast tego przypnij utrzymywane alternatywy.

Chart.yaml i helper walidacji powinny wrócić wyglądając tak — zwróć uwagę, że repozytoria zależności to CloudNativePG i wykresy prometheus-community, oba aktywnie utrzymywane:

Chart.yaml
apiVersion: v2
name: orders-api
description: Helm chart for the orders-api service
type: application
version: 1.0.0
appVersion: "1.4.0"
dependencies:
- name: cloudnative-pg
version: "0.x.x" # CloudNativePG operator — maintained
repository: https://cloudnative-pg.github.io/charts
condition: postgresql.enabled
- name: kube-prometheus-stack
version: "70.x.x"
repository: https://prometheus-community.github.io/helm-charts
condition: monitoring.enabled
# templates/_helpers.tpl (validation excerpt)
{{- define "orders-api.validateValues" -}}
{{- if not .Values.image.repository }}
{{- fail "image.repository is required" }}
{{- end }}
{{- if and .Values.ingress.enabled (not .Values.ingress.hosts) }}
{{- fail "ingress.hosts must be set when ingress is enabled" }}
{{- end }}
{{- end }}

Dla ciągłego dostarczania wygeneruj Application ArgoCD, który wskazuje na nakładkę wykresu i sam się naprawia. Dla kształtowania ruchu wygeneruj konfigurację Istio — i upewnij się, że apiVersion to networking.istio.io/v1 (awansowane do stabilnego w Istio 1.22 i rekomendowane dla nowych konfiguracji), a nie starsze v1beta1, które narzędzia AI nadal domyślnie wypisują.

argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: orders-api-production
namespace: argocd
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-configs
targetRevision: main
path: applications/orders-api/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
---
# virtualservice.yaml — note apiVersion v1 (Istio 1.22+), not v1beta1
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: orders-api
namespace: production
spec:
hosts: [orders-api]
http:
- route: # 90/10 canary split
- destination:
host: orders-api
subset: v1
weight: 90
- destination:
host: orders-api
subset: v2
weight: 10
retries:
attempts: 3
perTryTimeout: 10s
retryOn: 5xx
timeout: 30s

Dla autoskalowania wygeneruj HPA oraz — co kluczowe — PodDisruptionBudget. PDB ma ostrą krawędź, którą narzędzia AI mylą: możesz ustawić minAvailable ALBO maxUnavailable, nigdy oba. Manifest z obydwoma jest odrzucany przez kubectl apply. Wybierz jedno (maxUnavailable jest zwykle lepsze, bo śledzi liczbę replik):

hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orders-api
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orders-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # avoid flapping
---
# pdb.yaml — exactly one of minAvailable / maxUnavailable
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: orders-api
namespace: production
spec:
maxUnavailable: 1
selector:
matchLabels:
app: orders-api

Wygeneruj workflow GitHub Actions do budowania, pushowania i podbijania tagu obrazu. Narzędzia AI mają tendencję do wypisywania przestarzałych majorów akcji ze swoich danych treningowych — przypnij aktualne (actions/checkout@v6, docker/build-push-action@v7, peter-evans/create-pull-request@v8):

# .github/workflows/deploy.yml (excerpt)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
bump-prod:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
repository: myorg/k8s-configs
token: ${{ secrets.CONFIG_REPO_TOKEN }}
- run: kustomize edit set image orders-api=ghcr.io/${{ github.repository }}:${{ github.sha }}
working-directory: applications/orders-api/overlays/production
- uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.CONFIG_REPO_TOKEN }}
commit-message: "Deploy ${{ github.sha }} to production"
title: "Deploy ${{ github.sha }} to production"
branch: deploy-${{ github.sha }}
  1. CrashLoopBackOff tuż po rollout. Zwykle sonda liveness zabija poda, zanim ten skończy się uruchamiać. Sprawdź kubectl logs <pod> --previous pod kątem ostatnich słów przed zabiciem. Naprawa: przenieś zabezpieczenie rozruchu do startupProbe (zobacz Krok 1), aby liveness ruszało dopiero, gdy aplikacja zgłosi gotowość.

  2. OOMKilled pod obciążeniem. kubectl describe pod pokazuje Last State: Terminated, Reason: OOMKilled. Limit pamięci jest zbyt ciasny dla szczytowego zużycia. Pamięć jest nieściśliwa — jądro zabija kontener, zamiast go dławić. Naprawa: podnieś limit pamięci powyżej zaobserwowanego P99 (użyj promptu „zaciśnij limity zasobów” z prawdziwymi danymi kubectl top), a nie na podstawie zgadywania.

  3. Drenowanie węzła wisi w nieskończoność. kubectl drain zacina się, bo PodDisruptionBudget nie pozwala na kolejną eksmisję. Częsta przyczyna: minAvailable równe liczbie replik (np. minAvailable: 3 przy 3 replikach nie zostawia żadnego budżetu zakłóceń) albo manifest nielegalnie ustawił jednocześnie minAvailable i maxUnavailable. Naprawa: ustaw dokładnie jedno pole i zostaw realny zapas (maxUnavailable: 1).

  4. ImagePullBackOff. kubectl describe pod pokazuje błąd pobierania. Albo tag nie istnieje (podbicie w CI wypchnęło inny SHA niż ten, do którego odwołuje się manifest), albo rejestr potrzebuje imagePullSecrets, albo — coraz częściej w 2026 — wykres wskazuje na usunięty obraz Bitnami. Naprawa: potwierdź tag przez crane ls / UI rejestru, dołącz pull secret i zastąp zależności Bitnami utrzymywanymi wykresami.

  5. ArgoCD zacięte na OutOfSync lub Progressing w nieskończoność. Często pole, które klaster mutuje (jak replicas, zarządzane przez HPA), walczy z Gitem. Dodaj wpis ignoreDifferences dla /spec/replicas, aby ArgoCD przestało próbować cofać HPA. Jeśli to Progressing, leżący u podstaw Deployment nigdy nie staje się Ready — wróć do trybu awarii 1 lub 2.