Módulo 6: Deployment Pipelines
2. CD Concepts y Estrategias
Descripción
En la cápsula anterior viste la diferencia entre CI y CD a nivel general. Ahora profundizas en los conceptos fundamentales de Continuous Delivery: qué es, cómo se diferencia de Continuous Deployment, cuándo usar cada approach, y cómo se estructura un deployment pipeline. Esta distinción no es académica — define cómo diseñas tu pipeline y qué nivel de automatización aplicas.
Para sistemas AI, la elección entre Delivery y Deployment tiene implicaciones directas. Un modelo que genera respuestas incorrectas en producción no solo causa bugs — puede generar costos significativos en API calls, producir outputs que confunden a los usuarios, o violar constraints de negocio. Por eso, la mayoría de equipos que trabajan con AI eligen Continuous Delivery (con aprobación humana) sobre Continuous Deployment (totalmente automático). El humano en el loop no es debilidad — es prudencia.
Esta cápsula te da el marco conceptual para entender el deployment pipeline que construyes en el resto del módulo.
Continuous Delivery vs Continuous Deployment
Continuous Delivery
Definición: Cada cambio que pasa los tests está listo para ir a producción, pero requiere una acción manual (un click, una aprobación) para desplegarse.
Push → CI Pipeline → Imagen lista → ⏸️ Aprobación humana → Deploy producción
↑
"¿Está bien?"
El pipeline llega hasta "la imagen está en el registry y el staging funciona." Pero no deploya a producción automáticamente. Un humano revisa, verifica staging, y aprueba.
Características:
- 📋 Siempre deployable — Cada commit que pasa CI puede ir a producción
- 📋 Control humano — Alguien decide cuándo se despliega
- 📋 Frecuencia variable — Puedes deployear una vez al día o una vez por semana
- 📋 Safety net — Si staging muestra algo inesperado, no deployeas
Continuous Deployment
Definición: Cada cambio que pasa los tests se despliega a producción automáticamente. No hay paso manual.
Push → CI Pipeline → Imagen lista → Deploy producción (automático)
No hay pausa. Si los tests pasan, el código va a producción. El pipeline es completamente autónomo.
Características:
- 📋 Totalmente automatizado — De push a producción sin intervención
- 📋 Velocidad máxima — Cambios llegan a usuarios en minutos
- 📋 Requiere confianza total en tests — Si los tests no atrapan un bug, producción lo sufre
- 📋 Feedback rápido — Descubres problemas en producción inmediatamente
La comparación directa
| Aspecto | Continuous Delivery | Continuous Deployment |
|---|---|---|
| Último paso | Manual (approval) | Automático |
| Velocidad a producción | Minutos a horas | Minutos |
| Control | Humano decide cuándo | Pipeline decide siempre |
| Riesgo | Menor (humano verifica) | Mayor (depende de tests) |
| Requiere | Tests buenos + reviewer | Tests excelentes + monitoring |
| Frecuencia | Cuando el reviewer aprueba | Cada push |
| Rollback | Menos frecuente (prevenido por review) | Más frecuente (automatizado) |
Visualmente
Continuous Integration (CI):
Code → Build → Test → ✅ "El código es válido"
Continuous Delivery (CD - Delivery):
Code → Build → Test → Deploy Staging → ⏸️ Approval → Deploy Prod
↑
Humano revisa
Continuous Deployment (CD - Deployment):
Code → Build → Test → Deploy Staging → Deploy Prod (automático)
↑
Sin intervención
Por qué Continuous Delivery es mejor para sistemas AI
El argumento principal
Los sistemas AI tienen un problema que las aplicaciones tradicionales no tienen: comportamiento no determinista. Un CRUD siempre devuelve los mismos datos para la misma query. Un endpoint que llama a GPT-4 puede devolver respuestas diferentes cada vez, y no todos los fallos son capturables por tests automatizados.
Escenario: Cambias el system prompt de tu chatbot
Tests automatizados:
✅ El endpoint responde 200
✅ La respuesta es un string no vacío
✅ El formato JSON es correcto
✅ Los tokens usados están dentro del límite
Pero los tests NO verifican:
❓ ¿La respuesta es útil para el usuario?
❓ ¿El tono es apropiado para el contexto?
❓ ¿El modelo no está alucinando datos?
❓ ¿La latencia es aceptable bajo carga?
Un humano que revisa staging puede detectar estos problemas. Los tests automatizados no pueden (al menos no completamente, no todavía).
Cuándo cada approach es apropiado
Usa Continuous Delivery (con approval) cuando:
- 📋 Tu aplicación llama a LLMs (GPT, Claude, etc.) — outputs no deterministas
- 📋 Cambias prompts o configuración de modelos — impacto difícil de testear automáticamente
- 📋 Tu equipo es pequeño (1-5 personas) — el overhead de approval es mínimo
- 📋 Los costos por error son altos — API calls innecesarias, datos incorrectos
- 📋 Estás en una etapa temprana — tu test suite no cubre todos los edge cases
Usa Continuous Deployment (automático) cuando:
- 📋 Tu test suite es muy madura — >90% coverage, integration tests, load tests
- 📋 Tienes monitoring robusto — alertas inmediatas cuando algo falla en producción
- 📋 Los cambios son de bajo riesgo — typos, UI tweaks, config changes
- 📋 Tu equipo deployea muchas veces al día — el overhead de approval es cuello de botella
- 📋 Tienes rollback automático confiable — puedes revertir en segundos
La recomendación para esta guía
Para tu deployment pipeline:
✅ Continuous Delivery con approval gate
Push → Test → Build → Deploy Staging → ⏸️ Reviewer aprueba → Deploy Producción
¿Por qué?
1. Tu test suite está en construcción (no es exhaustiva todavía)
2. Tus prompts cambian frecuentemente (impacto no determinista)
3. API keys de producción cuestan dinero real
4. El overhead de un reviewer aprobando es < 5 minutos
5. La paz mental de saber que alguien verificó vale más que la velocidad
Cuando tu test suite sea madura y tengas monitoring robusto (después de la guía #18), puedes evolucionar a Continuous Deployment. Pero empezar con Delivery es la decisión prudente.
El Deploy Pipeline Pattern
Anatomía de un deployment pipeline
Todo deployment pipeline sigue un patrón de stages progresivos. Cada stage agrega confianza:
Stage 1: Build
→ El código compila/construye sin errores
→ Confianza: "El código es sintácticamente válido"
Stage 2: Test
→ Los tests unitarios e integración pasan
→ Confianza: "El código hace lo que esperamos"
Stage 3: Package
→ La imagen Docker se construye y pushea
→ Confianza: "El código corre en un contenedor"
Stage 4: Deploy Staging
→ La imagen se despliega a un ambiente real
→ Confianza: "El código funciona en un servidor real"
Stage 5: Validate
→ Smoke tests verifican funcionalidad básica
→ Confianza: "La aplicación responde correctamente"
Stage 6: Approve
→ Un humano verifica y aprueba
→ Confianza: "Un experto revisó y está de acuerdo"
Stage 7: Deploy Production
→ La imagen se despliega a producción
→ Confianza: "Los usuarios tienen la nueva versión"
Stage 8: Verify
→ Smoke tests en producción + monitoring
→ Confianza: "Producción está estable"
El principio de confianza progresiva
Cada stage es más caro de ejecutar y más cercano al usuario. Si un cambio va a fallar, quieres que falle lo antes posible — en el stage más barato:
Costo del failure
Stage 1: Build │█ Bajo (solo CI minutes)
Stage 2: Test │██ Bajo (solo CI minutes)
Stage 3: Package │███ Medio (build + push)
Stage 4: Deploy Staging │█████ Medio (servidor staging)
Stage 5: Validate │██████ Medio (smoke tests)
Stage 6: Approve │███████ Alto (tiempo humano)
Stage 7: Deploy Production │█████████████ Muy alto (usuarios afectados)
Stage 8: Verify │████████████████ Máximo (producción inestable)
Si tus tests son buenos, la mayoría de los fallos se detectan en stages 1-3. Staging detecta problemas de ambiente. Approval detecta problemas de negocio. Producción debería recibir solo cambios validados.
Deploy Pipeline en GitHub Actions
Estructura básica del pipeline
name: Deploy Pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements-dev.txt
- run: pytest tests/ -v
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: echo "Deploying to staging..."
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Deploy to production
run: echo "Deploying to production..."
La cadena de needs
test → build → deploy-staging → deploy-production
Cada job depende del anterior. Si test falla, nada más se ejecuta. Si build falla, no hay deploy. Si deploy-staging falla, producción no se toca.
La clave está en environment: — cuando un job declara un environment con protection rules, GitHub pausa el workflow hasta que las reglas se cumplan (aprobación del reviewer, wait timer, etc.).
Diferencia entre el CI pipeline y el Deploy pipeline
CI Pipeline (Módulos 1-5):
Trigger: push + PR
Jobs: test → build → scan → push
Output: Imagen en GHCR
Deploy Pipeline (este módulo):
Trigger: solo push a main (no PRs)
Jobs: test → build → deploy-staging → approve → deploy-production
Output: Aplicación corriendo en producción
Puedes tenerlos como workflows separados o como un solo workflow combinado. En esta guía los mantenemos separados por claridad, pero en el Módulo 8 (proyecto integrador) los combinamos.
Environments como stages del pipeline
Mapping de environments a stages
GitHub Environments mapean directamente a los stages de tu deployment pipeline:
GitHub Environment: staging
→ Sin protection rules (deploy automático)
→ Secrets: OPENAI_API_KEY (budget limitado), SERVER_SSH_KEY (staging)
→ URL: https://staging.tu-app.com
GitHub Environment: production
→ Protection rules: required reviewer, wait timer
→ Secrets: OPENAI_API_KEY (producción), SERVER_SSH_KEY (producción)
→ URL: https://tu-app.com
Cada environment tiene sus propios secrets. Staging usa una API key con budget limitado ($10/mes). Producción usa la API key real. Si staging tiene un bug que hace retry loops, los costos están acotados.
El flujo de secrets por environment
deploy-staging:
environment: staging
steps:
- run: echo "Using staging secrets"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Valor: sk-staging-abc123 (budget: $10/mes)
deploy-production:
environment: production
steps:
- run: echo "Using production secrets"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Valor: sk-prod-xyz789 (sin límite)
El nombre del secret es el mismo (OPENAI_API_KEY), pero el valor es diferente por environment. El workflow no necesita saber la diferencia — GitHub inyecta el valor correcto según el environment declarado.
Pipeline triggers: cuándo deployear
Solo main, nunca PRs
El deploy pipeline se triggerea solo en push a main, no en PRs:
on:
push:
branches: [main]
Por qué no en PRs: Un PR es código propuesto, no aceptado. Deployear código que no ha sido mergeado a main es peligroso — podría sobreescribir staging con código experimental.
Push a main + tags para releases
on:
push:
branches: [main]
tags: ["v*"]
Esto triggerea el pipeline tanto en push a main (deploy automático a staging) como en tags de versión (deploy de release).
Workflow dispatch para deploys manuales
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
image_tag:
description: "Image tag to deploy"
required: true
type: string
workflow_dispatch permite deployear manualmente desde la UI de Actions. Útil para rollbacks (deployear un tag anterior) o re-deploys.
Comparaciones
Deployment pipeline completo vs deploy simple
| Aspecto | Deploy simple | Pipeline completo |
|---|---|---|
| Stages | Build → deploy | Build → test → staging → approve → production |
| Tiempo | 2-3 min | 5-10 min |
| Riesgo | Alto (sin validación) | Bajo (staging + approval + rollback) |
| Rollback | Manual | Automatizado |
| Confianza | "Debería funcionar" | "Staging funciona, reviewer aprobó" |
Un workflow vs workflows separados
| Aspecto | Todo en un workflow | Workflows separados |
|---|---|---|
| Legibilidad | Un archivo grande | Archivos enfocados |
| Trigger | Mismo trigger para todo | Triggers independientes |
| Reusabilidad | Difícil de reusar | Cada workflow es independiente |
| Debugging | Un workflow largo | Fácil de aislar problemas |
| Recomendación | Para el proyecto final (Módulo 8) | Para aprender (este módulo) |
Self-hosted runner vs GitHub-hosted runner
| Aspecto | GitHub-hosted | Self-hosted |
|---|---|---|
| Setup | Cero (listo de fábrica) | Necesitas configurar el runner |
| Costo | Incluido (2000 min/mes free) | Tu infraestructura |
| Red | Internet público | Acceso a red privada |
| Deploy a servidor | SSH o API externa | Acceso directo al servidor |
| Para esta guía | Suficiente | Para cuando necesites red privada |
Troubleshooting
1. "Environment 'staging' not found"
Síntoma:
Error: Environment 'staging' was not found.
Causa: El environment no está creado en GitHub, o el nombre tiene un typo.
Solución:
GitHub → Settings → Environments → New environment
Name: staging (exactamente como en el workflow YAML)
→ Configure environment
Verifica que el nombre en el YAML coincida exactamente con el nombre del environment en GitHub (case-sensitive).
2. Deploy job se queda en "Waiting"
Síntoma: El job muestra "Waiting for review" pero no hay reviewer configurado.
Causa: El environment tiene protection rules con required reviewers, pero nadie puede aprobar.
Solución:
GitHub → Settings → Environments → production → Protection rules
Required reviewers: Agrega tu username o un team
Si estás practicando solo, agrégarte a ti mismo como reviewer — puedes aprobar tus propios deploys.
3. El workflow no se triggerea en push a main
Síntoma: Push a main, pero el deploy workflow no aparece en Actions.
Causa: El archivo YAML no está en .github/workflows/, tiene un error de syntax, o el branch filter no coincide.
Solución:
# Verifica que el archivo existe
ls .github/workflows/deploy.yml
# Valida el YAML
python -c "import yaml; yaml.safe_load(open('.github/workflows/deploy.yml'))"
# Verifica el trigger
# El YAML debe tener:
# on:
# push:
# branches: [main]
4. Secrets de environment no se inyectan
Síntoma: El step usa ${{ secrets.MY_SECRET }} pero el valor está vacío.
Causa: El job no declara environment: o el secret está en un environment diferente.
Solución:
jobs:
deploy:
runs-on: ubuntu-latest
environment: staging # NECESARIO para acceder a secrets de staging
steps:
- run: echo "Key length: ${#KEY}"
env:
KEY: ${{ secrets.OPENAI_API_KEY }}
Sin la línea environment: staging, el job no puede acceder a los secrets del environment.
Ejercicios
Ejercicio 1: Identifica CI vs CD
Para cada escenario, indica si es CI, Continuous Delivery, o Continuous Deployment:
- Push triggerea tests y lint automáticamente
- Si tests pasan, la imagen se pushea a GHCR
- La imagen se despliega a staging automáticamente
- Un reviewer revisa staging y aprueba
- La imagen se despliega a producción después de la aprobación
- Cada push a main despliega a producción sin intervención humana
Ver solución
- CI — Validación automática del código
- CI — Packaging automático (parte del build pipeline)
- CD (Delivery o Deployment) — Deployment automático a staging (común en ambos)
- CD (Delivery) — Approval humano antes de producción
- CD (Delivery) — Deploy post-approval
- CD (Deployment) — Totalmente automático, sin intervención
El flujo 1-5 es Continuous Delivery (approval gate antes de producción). El flujo 1-3 + 6 es Continuous Deployment (sin approval).
Ejercicio 2: Diseña la cadena de jobs
Tienes estos jobs: lint, test, build-docker, deploy-staging, smoke-test, deploy-production. Define la cadena de needs: para que: (1) lint y test corran en paralelo, (2) docker dependa de ambos, (3) staging dependa de docker, (4) smoke-test dependa de staging, (5) production dependa de smoke-test.
Ver solución
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ruff check src/
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/ -v
build-docker:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
deploy-staging:
needs: build-docker
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploy to staging"
smoke-test:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- run: curl -sf https://staging.app.com/health
deploy-production:
needs: smoke-test
runs-on: ubuntu-latest
environment:
name: production
steps:
- run: echo "Deploy to production"
La cadena es:
lint ──┐
├──→ build-docker → deploy-staging → smoke-test → deploy-production
test ──┘
lint y test corren en paralelo porque no tienen needs:. build-docker espera a que ambos terminen (needs: [lint, test]). El resto es secuencial.
Ejercicio 3: Configura triggers para un deploy pipeline
Crea la sección on: de un workflow que: (1) se triggerea en push a main, (2) se triggerea en tags que empiezan con v, (3) se puede triggerear manualmente con un input para elegir el environment (staging o production) y otro para el image tag.
Ver solución
name: Deploy Pipeline
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
image_tag:
description: "Image tag to deploy (e.g., sha-abc1234 or v1.2.3)"
required: true
type: string
Puntos clave:
branches: [main]filtra push solo a main (no a feature branches)tags: ["v*"]matchea tags comov1.0.0,v2.1.3, etc.workflow_dispatchhabilita el botón "Run workflow" en la UI de Actionstype: choicecrea un dropdown con opciones fijastype: stringes un input de texto libre para el tag
Ejercicio 4: Elige la estrategia para cada escenario
Para cada escenario, elige Continuous Delivery o Continuous Deployment y justifica:
- Startup de 3 personas con un chatbot AI que cambia prompts diariamente
- Empresa con test suite madura, monitoring con PagerDuty, y 50 deploys/día
- Proyecto personal de un portfolio que solo tú usas
- Equipo de ML que despliega modelos que clasifican transacciones bancarias
Ver solución
-
Continuous Delivery. Prompts que cambian diariamente generan outputs no deterministas. Con 3 personas, el overhead de approval es bajo (< 5 min). Un humano verificando staging previene que un prompt roto llegue a usuarios.
-
Continuous Deployment. Test suite madura + monitoring robusto + alta frecuencia de deploys. El overhead de approval 50 veces al día es insostenible. Confianza en tests + rollback automático + alertas inmediatas permiten deploy automático.
-
Continuous Deployment. Es tu proyecto, solo tú lo usas. El riesgo es mínimo y el overhead de aprobarte a ti mismo no agrega valor. Si algo se rompe, lo ves inmediatamente.
-
Continuous Delivery (con approval estricto). Clasificar transacciones bancarias es de alto riesgo. Un modelo incorrecto puede aprobar transacciones fraudulentas o bloquear transacciones legítimas. Requiere aprobación de múltiples reviewers, posiblemente con wait timer de 24 horas para observación en staging.
Resumen
- ✅ Continuous Delivery = listo para deploy + aprobación humana antes de producción
- ✅ Continuous Deployment = deploy automático a producción sin intervención
- ✅ Para AI, Delivery es la recomendación — outputs no deterministas necesitan ojo humano
- ✅ El deploy pipeline pattern: build → test → staging → approve → production
- ✅ Confianza progresiva: cada stage agrega confianza, cada fallo es más barato temprano
- ✅ Environments mapean a stages: staging (automático), production (con approval)
- ✅ Triggers: push a main para deploys automáticos, workflow_dispatch para manuales
- ✅ CI produce artefactos; CD los lleva a producción — son complementarios, no sustitutos
Recursos adicionales
- Continuous Delivery vs Continuous Deployment — La diferencia explicada por Atlassian
- GitHub Actions — Using environments — Environments como stages
- Martin Fowler — Continuous Delivery — El concepto original por el co-autor del libro
- The Twelve-Factor App — Build, Release, Run — Separación de stages en apps modernas
- GitHub Actions — Workflow syntax — Referencia de
needs:,environment:, triggers - Accelerate (book) — Investigación sobre deployment frequency y estabilidad