Zum Hauptinhalt springen
••
Sichere ProgrammierungCryptotremor: finding out when your encryption actually stops working(03.10.2026 um 08:10 Uhr)
•
Sichere ProgrammierungAI Viva Partner: A Private, Offline Exam Study Assistant for My Friend(03.10.2026 um 08:11 Uhr)
••
Windows Tipps & SecurityMintline: prove which Solana token came first(03.10.2026 um 08:15 Uhr)
•••
Sichere ProgrammierungI've never run an AI model on my own machine. This month I will.(03.10.2026 um 08:17 Uhr)
•
Sichere ProgrammierungI removed my Chinese web font. Every page lost 1 MB(03.10.2026 um 08:18 Uhr)
•••
Sichere ProgrammierungCryptotremor: finding out when your encryption actually stops working(03.10.2026 um 08:10 Uhr)
•
Sichere ProgrammierungAI Viva Partner: A Private, Offline Exam Study Assistant for My Friend(03.10.2026 um 08:11 Uhr)
••
Windows Tipps & SecurityMintline: prove which Solana token came first(03.10.2026 um 08:15 Uhr)
•••
Sichere ProgrammierungI've never run an AI model on my own machine. This month I will.(03.10.2026 um 08:17 Uhr)
•
Sichere ProgrammierungI removed my Chinese web font. Every page lost 1 MB(03.10.2026 um 08:18 Uhr)
•
Intelligence View
⚡ tsecurity.de Intelligence

De Cero a Kubernetes Parte 4: Backend FastAPI + PostgreSQL en Producción

Serie: De Cero a Kubernetes — Parte 1 · Parte 2 · Parte 3 · Parte 4 · Parte 5 Ya tenemos un cluster con ingress, TLS y GitOps. Ahora le ponemos carga de traba…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

K8s Terraform ArgoCD — Header



Serie: De Cero a Kubernetes — Parte 1 · Parte 2 · Parte 3 · Parte 4 · Parte 5






Ya tenemos un cluster con ingress, TLS y GitOps. Ahora le ponemos carga de trabajo real: una base de datos PostgreSQL gestionada por un operador y un backend en FastAPI que se conecta a ella, ejecuta migraciones al arrancar, y expone una API REST con health checks.



Esta es la parte donde el tutorial deja de ser "miren qué bonito el cluster vacío" y empieza a ser "esto ya funciona en producción".









Componentes de esta parte






┌─────────────────────────────────────────┐
│ api.tudominio.com │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Ingress │ │
│ │ (nginx + TLS) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Backend Service │ │
│ │ (ClusterIP) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Backend Deployment │ │
│ │ 2 réplicas │ │
│ │ FastAPI + Uvicorn │ │
│ │ Init container: │ │
│ │ migraciones │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ PostgreSQL │ │
│ │ (CloudNativePG) │ │
│ │ 1 instancia │ │
│ │ PVC 10Gi │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────┘












Paso 1: CloudNativePG — PostgreSQL como recurso nativo de Kubernetes



CloudNativePG (CNPG) es un operador que gestiona PostgreSQL en Kubernetes sin StatefulSets manuales. Lo instalas y creas un recurso Cluster. El operador maneja replicación, backups, failover y actualizaciones. Es la forma moderna de correr PostgreSQL en Kubernetes.



Instalamos el operador con Terraform (Parte 2, pero lo documentamos aquí):




resource "helm_release" "cnpg" {
name = "cloudnative-pg"
namespace = kubernetes_namespace.infrastructure.metadata[0].name
repository = "https://cloudnative-pg.github.io/charts"
chart = "cloudnative-pg"
version = "0.22.0"
}






Luego definimos la base de datos como código en nuestro repo GitOps:



gitops/apps/database/postgresql-cluster.yaml:




apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: guayoyo-db
namespace: production
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16.4

bootstrap:
initdb:
database: guayoyo
owner: app
secret:
name: db-credentials

storage:
size: 10Gi
storageClass: local-path

resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"

monitoring:
enablePodMonitor: false






gitops/apps/database/kustomization.yaml:




apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- postgresql-cluster.yaml
- db-credentials-secret.yaml












Paso 2: Secretos — sin texto plano en git



gitops/apps/database/db-credentials-secret.yaml:




apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: kubernetes.io/basic-auth
stringData:
username: app
password: "CambiaEstoEnProduccionConSealedSecrets"






⚠️ En producción real: Nunca pongas passwords en texto plano en git. Usa una de estas opciones:





  1. SealedSecrets — encripta el Secret completo; solo el controlador en el cluster sabe descifrarlo. El YAML encriptado sí puede committearse.


  2. ExternalSecrets — sincroniza secretos desde Vault, AWS Secrets Manager o 1Password.


  3. SOPS + Age — encripta archivos completos con llaves age.



Para este tutorial, el Secret va en texto plano PORQUE es un cluster de laboratorio. Pero quiero que quede claro: en tu empresa, esto jamás va así.









Paso 3: Código del backend (FastAPI)



Creamos el backend en un repo separado (o subdirectorio del monorepo). Estructura:




backend/
├── Dockerfile
├── requirements.txt
├── main.py
├── models.py
├── database.py
└── migrations/
└── 001_init.sql






main.py:




from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import asyncpg
import os
import time
from contextlib import asynccontextmanager

DB_HOST = os.getenv("DB_HOST", "guayoyo-db-rw")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "guayoyo")
DB_USER = os.getenv("DB_USER", "app")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")

pool = None

@asynccontextmanager
async def lifespan(app: FastAPI):
global pool
max_retries = 10
for attempt in range(max_retries):
try:
pool = await asyncpg.create_pool(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
min_size=2,
max_size=10
)
# Inicializar tablas
async with pool.acquire() as conn:
await conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")
break
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(3)
yield
await pool.close()

app = FastAPI(title="Guayoyo API", lifespan=lifespan)

app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://admin.tudominio.com",
"https://app.tudominio.com",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

class ItemCreate(BaseModel):
name: str
description: str = ""

class Item(ItemCreate):
id: int
created_at: str

@app.get("/health")
async def health():
try:
async with pool.acquire() as conn:
await conn.fetchval("SELECT 1")
return {"status": "ok", "db": "connected", "timestamp": time.time()}
except Exception as e:
return {"status": "degraded", "db": str(e)}

@app.post("/items", response_model=Item)
async def create_item(item: ItemCreate):
async with pool.acquire() as conn:
row = await conn.fetchrow(
"INSERT INTO items (name, description) VALUES ($1, $2) RETURNING *",
item.name, item.description
)
return Item(id=row["id"], name=row["name"],
description=row["description"] or "",
created_at=str(row["created_at"]))

@app.get("/items")
async def list_items():
async with pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM items ORDER BY created_at DESC")
return [{"id": r["id"], "name": r["name"],
"description": r["description"] or "",
"created_at": str(r["created_at"])} for r in rows]

@app.get("/items/{item_id}")
async def get_item(item_id: int):
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM items WHERE id = $1", item_id)
if not row:
raise HTTPException(status_code=404, detail="Item not found")
return {"id": row["id"], "name": row["name"],
"description": row["description"] or "",
"created_at": str(row["created_at"])}

@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
async with pool.acquire() as conn:
result = await conn.execute("DELETE FROM items WHERE id = $1", item_id)
if result == "DELETE 0":
raise HTTPException(status_code=404, detail="Item not found")
return {"status": "deleted"}






Dockerfile:




FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false && poetry install --no-dev

FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]






Build y push:




docker build -t ghcr.io/tu-org/backend:v1.0.0 .
docker push ghcr.io/tu-org/backend:v1.0.0












Paso 4: Desplegar en Kubernetes vía ArgoCD



gitops/apps/backend/deployment.yaml:




apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
serviceAccountName: apps-sa
containers:
- name: backend
image: ghcr.io/tu-org/backend:v1.0.0
ports:
- containerPort: 8000
env:
- name: DB_HOST
value: guayoyo-db-rw.production.svc.cluster.local
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: guayoyo
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10






Puntos importantes aquí:





  • guayoyo-db-rw.production.svc.cluster.local — este es el DNS interno que CloudNativePG crea automáticamente. -rw = read-write (la primaria). Si tuvieras réplicas de lectura, estarían en -ro.


  • secretKeyRef inyecta username/password del Secret db-credentials sin exponerlos en el Deployment


  • livenessProbe y readinessProbe — Kubernetes usa estos para saber si el pod está vivo y si debe recibir tráfico. Si /health falla, Kubernetes reinicia el pod (liveness) o lo saca del balanceador (readiness)



gitops/apps/backend/service.yaml:




apiVersion: v1
kind: Service
metadata:
name: backend
namespace: production
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 8000
protocol: TCP
type: ClusterIP






gitops/apps/backend/ingress.yaml:




apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backend
namespace: production
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.tudominio.com
secretName: api-tls
rules:
- host: api.tudominio.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend
port:
number: 80






gitops/apps/backend/kustomization.yaml:




apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- deployment.yaml
- service.yaml
- ingress.yaml












Paso 5: Verificación end-to-end






# 1. Verificar que ArgoCD sincronizó todo
argocd app list
# backend Healthy Synced
# database Healthy Synced

# 2. Probar la API
curl https://api.tudominio.com:30443/health
# {"status":"ok","db":"connected","timestamp":1716163200.123}

# 3. Crear un item
curl -X POST https://api.tudominio.com:30443/items \
-H "Content-Type: application/json" \
-d '{"name":"Test Item","description":"Desde curl"}'
# {"id":1,"name":"Test Item","description":"Desde curl","created_at":"..."}

# 4. Listar items
curl https://api.tudominio.com:30443/items
# [{"id":1,"name":"Test Item",...}]

# 5. Borrar un item
curl -X DELETE https://api.tudominio.com:30443/items/1
# {"status":"deleted"}

# 6. Verificar que las 2 réplicas están corriendo
kubectl get pods -n production -l app=backend
# NAME READY STATUS RESTARTS AGE
# backend-xxxxx-xxxxx 1/1 Running 0 1m
# backend-xxxxx-yyyyy 1/1 Running 0 1m












Qué aprendiste en esta parte




  • CloudNativePG como operador de PostgreSQL en Kubernetes

  • DNS interno de Kubernetes: guayoyo-db-rw.production.svc.cluster.local

  • Secretos para credenciales (y la nota seria sobre SealedSecrets/ExternalSecrets)

  • FastAPI con connection pooling a PostgreSQL usando asyncpg

  • Init container pattern para migraciones al arranque

  • Liveness y readiness probes — la diferencia y por qué ambas importan

  • CORS configurado para aceptar tráfico de los frontends

  • Construcción de imagen Docker multi-stage optimizada



En la Parte 5, desplegaremos los dos frontends (React + Vue), los conectaremos al backend, y haremos una prueba de integración completa donde una app crea datos y la otra los consume en tiempo real.






En Guayoyo Tech diseñamos backends que no se caen cuando hay tráfico real. FastAPI, Go, Node.js — lo que tu proyecto necesite, con PostgreSQL, health checks, métricas y deployment automatizado en Kubernetes. Hablemos gratis 15 minutos y te mostramos cómo.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten De Cero a Kubernetes Parte 4: Backend FastAPI + PostgreSQL en Producción

Thematisch verwandte Begriffe: Cero, Kubernetes, Parte, Backend · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
Nächster Beitrag