O problema real
Você tem um docker-compose.yml que roda perfeitamente no laptop: API em Django, banco Postgres, Redis pra cache. 5 serviços, docker-compose up e pronto. Aí chega a hora de colocar em produção com Kubernetes. Você abre a documentação oficial, vê "Pod", "ReplicaSet", "Deployment", "Service", "Ingress", "ConfigMap", "Secret"... e pensa: por que tudo tem nome de ficção científica?
A realidade: cada conceito do Compose mapeia exatamente 1:1 para Kubernetes. Service do Compose = Deployment + Service do K8s. Volumes = PVC. Environment = ConfigMap. O problema é que ninguém faz essa tradução lado a lado. Você fica procurando doc, stackoverflows, e 3 horas depois descobre que depends_on não existe em Kubernetes porque... bem, porque o design é diferente.
Conceitos que você já tem, em nomes novos
Vamos começar com um docker-compose.yml real e converter campo por campo:
# docker-compose.yml que você já entende
version: '3.8'
services:
api:
image: myapp/api:1.2.0
container_name: api
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
- ./logs:/app/logs
db:
image: postgres:15
environment:
POSTGRES_DB: app
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- dbdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
dbdata:
Aqui está a tradução:
ports: → Service + Ingress
Em Compose, ports: - "8000:8000" mapeia porta do host. Em K8s, um Pod nunca é alcançado direto. Você precisa:
- Service (load balancer interno): expõe pods dentro do cluster
- Ingress (roteador HTTP): expõe pra internet
# Kubernetes: Deployment roda os pods
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3 # 3 cópias do container
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
ports:
- containerPort: 8000
---
# Kubernetes: Service redireciona tráfego pros Pods
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ClusterIP # interno ao cluster
selector:
app: api
ports:
- port: 80 # porta que outros pods usam
targetPort: 8000 # porta que o container escuta
---
# Kubernetes: Ingress expõe pra internet (HTTPS, DNS, etc)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
Diferença chave: em Compose você vê localhost:8000 do host. Em K8s, você coloca api.example.com no DNS e o Ingress rota. Nada de localhost.
environment: → ConfigMap
Em Compose, variáveis diretamente no yml. Em K8s, nunca coloque valores direto. Use ConfigMap (pra não-sensível) ou Secret (pra sensível):
# ConfigMap: valores públicos
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: "INFO"
CACHE_TTL: "3600"
---
# Secret: valores sensíveis (encoded, não é criptografia real)
apiVersion: v1
kind: Secret
metadata:
name: api-secret
type: Opaque
stringData: # Kubernetes vai base64 isso automaticamente
DATABASE_URL: "postgres://user:pass@db:5432/app"
REDIS_URL: "redis://cache:6379"
---
# Deployment que usa ConfigMap e Secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
ports:
- containerPort: 8000
env:
# Vem de ConfigMap
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: api-config
key: LOG_LEVEL
# Vem de Secret
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secret
key: DATABASE_URL
volumes: → PVC (PersistentVolumeClaim)
Em Compose, volumes: dbdata: cria um diretório anônimo gerenciado. Em K8s, é explícito:
# PersistentVolumeClaim: pede armazenamento
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dbdata
spec:
accessModes:
- ReadWriteOnce # só 1 pod lê e escreve
storageClassName: standard
resources:
requests:
storage: 10Gi # 10 gigabytes
---
# StatefulSet: como Deployment, mas com identidade estável (pra DB)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db # importante pra pods se descobrirem
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: "app"
- name: POSTGRES_USER
value: "user"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # cada replica pega seu volume
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: standard
resources:
requests:
storage: 10Gi
depends_on: → Health checks explícitos
Aqui é a armadilha: Compose tem depends_on, K8s não. Por quê? Porque em Kubernetes, serviços escalam dinamicamente. API pode ter 1 ou 3 réplicas, db pode estar em outro nó. Você não pode dizer "espera db subir", porque db nunca morre (almejado).
Solução real: probes de saúde. O próprio container avisa quando tá pronto:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
ports:
- containerPort: 8000
# Readiness: "tá pronto pra receber tráfego?"
readinessProbe:
httpGet:
path: /health/ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
# Liveness: "tá vivo ou morreu?"
livenessProbe:
httpGet:
path: /health/live
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3 # se falhar 3x, reinicia
# Startup: "ainda tá iniciando?"
startupProbe:
httpGet:
path: /health/startup
port: 8000
failureThreshold: 30
periodSeconds: 10 # tenta por 5 minutos
O código da API precisa responder essas rotas:
# Flask example
@app.route('/health/startup')
def startup():
# Verificar dependências pesadas (conexão com DB, etc)
try:
db.session.execute('SELECT 1')
return {'status': 'ok'}, 200
except:
return {'status': 'not ready'}, 503
@app.route('/health/ready')
def ready():
# Verificar se pode aceitar requests
return {'status': 'ready'}, 200
@app.route('/health/live')
def live():
# Ping simples
return {'status': 'alive'}, 200
Pod, ReplicaSet, Deployment: por que tantos?
- Pod: unidade mínima, 1+ containers. Raro você criar direto.
- ReplicaSet: "quero 3 cópias do Pod". Raro você criar direto.
- Deployment: "quero 3 cópias do Pod + rolling updates + rollback". Você usa isso 99% das vezes.
# NÃO FAÇA: Pod isolado
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
# FAÇA: Deployment (que gerencia ReplicaSet, que gerencia Pods)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
Se você matarei um Pod, o Deployment percebe ("espera, falha!") e cria outro automaticamente. Em Compose, o container morreu, pronto.
Requests vs. Limits: o que acontece se omitir
Em Compose, nada. Em Kubernetes, é obrigatório (em produção) especificar quanto cada container precisa:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
# Requests: quanto você GARANTE que precisa
resources:
requests:
memory: "256Mi"
cpu: "250m" # 0.25 CPU
# Limits: nunca passar disso
limits:
memory: "512Mi"
cpu: "500m"
Sem requests, o scheduler pode colocar 100 pods num nó com 2 GB de RAM total. Aí tudo fica lento. Com requests, o scheduler sabe "preciso de no mínimo 25 GB pra esses 100 pods" e falha se não houver.
Armadilhas comuns
Imagem sem tag: image: postgres é postgres:latest. Amanhã sai versão nova, sua aplicação quebra. Sempre coloque versão: postgres:15.2.
ConfigMap no lugar de Secret: você coloca senha em ConfigMap (não é criptografado, base64 é legível). Qualquer um com acesso ao cluster lê. Use Secret sempre pra sensível.
Número de replicas fixo: replicas: 3 tá hardcoded. Use HPA (HorizontalPodAutoscaler) pra escalar automaticamente:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-autoscale
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # escala se CPU passar de 70%
Quando NÃO usar Kubernetes
Se sua aplicação roda em 1 container, use Docker simples ou ECS (AWS). K8s é overhead.
Se o time não tem DevOps/SRE dedicado, considera managed K8s (EKS, GKE). Não gerencie cluster você mesmo.
Para prototipagem de 2 semanas, use Compose. K8s só quando chegar em produção.
Próximos passos
Configure alertas e observabilidade com Observabilidade em aplicações com LLM pra monitorar seus pods.
Deploy isso com infra como código: Terraform na prática: sua primeira infra versionada.
Instale kubectl localmente, execute kubectl apply -f api.yaml num cluster gratuito (minikube ou kind), e veja os pods ligarem. A prática é o melhor professor.