Módulo 7: Alternative Platforms (Render, Railway, Fly.io)
6. CI/CD Integration con Plataformas Alternativas
Descripción
En esta cápsula vas a integrar GitHub Actions con Render, Railway y Fly.io para automatizar deployments. Las tres plataformas ofrecen auto-deploy desde Git, pero en producción necesitas más control: tests antes del deploy, environment promotion (staging → production), rollback automatizado, y notificaciones. GitHub Actions (que ya conoces del prerequisite #16) es la pieza que conecta tu pipeline de CI con el deployment en la plataforma elegida.
Contexto: Las cápsulas anteriores (02-04) desplegaron manualmente — dashboard o CLI. Eso está bien para la primera vez, pero en un equipo y en producción necesitas que el proceso sea reproducible, auditable y automático. Un push a main debe ejecutar tests, construir la imagen, desplegar a staging, verificar health, y promover a producción. Esta cápsula construye ese pipeline para cada plataforma.
El Pipeline de Deployment
Estructura del pipeline
Push a main
↓
GitHub Actions
├── 1. Checkout código
├── 2. Setup Python
├── 3. Instalar dependencias
├── 4. Ejecutar tests
├── 5. Build Docker image (opcional, depende de plataforma)
├── 6. Deploy a staging
├── 7. Health check en staging
├── 8. Deploy a production (si staging pasa)
└── 9. Notificación (Slack, Discord, etc.)
Prerequisitos
# Necesitas:
# 1. Repositorio en GitHub
# 2. App desplegada en al menos una plataforma (M7 caps 02-04)
# 3. Tests básicos para tu app
# 4. GitHub Actions habilitado en tu repo
# Verificar estructura
ls .github/workflows/
# Si no existe, créalo:
mkdir -p .github/workflows
Tests básicos para tu app AI
Antes de automatizar el deploy, necesitas tests que verifiquen que tu app funciona:
# tests/test_app.py
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
from main import app
client = TestClient(app)
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert "version" in data
def test_ask_requires_api_key():
with patch.dict("os.environ", {}, clear=True):
response = client.post(
"/ask",
json={"question": "test", "max_tokens": 50},
)
assert response.status_code == 500
@patch("main.openai.OpenAI")
def test_ask_returns_answer(mock_openai):
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Test answer"
mock_response.usage.total_tokens = 42
mock_openai.return_value.chat.completions.create.return_value = mock_response
with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test"}):
response = client.post(
"/ask",
json={"question": "¿Qué es Python?", "max_tokens": 50},
)
assert response.status_code == 200
data = response.json()
assert "answer" in data
# tests/requirements-test.txt
pytest==8.3.0
httpx==0.27.0
GitHub Actions + Render
Opción 1: Auto-deploy nativo (sin GitHub Actions)
Render despliega automáticamente cuando pusheas a main. No necesitas GitHub Actions para deploy básico. Pero si quieres tests antes del deploy:
Opción 2: Deploy controlado con GitHub Actions
# .github/workflows/deploy-render.yml
name: Deploy to Render
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
RENDER_SERVICE_ID: ${{ secrets.RENDER_SERVICE_ID }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -r app/requirements.txt
pip install -r tests/requirements-test.txt
- name: Run tests
run: pytest tests/ -v
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Trigger Render Deploy
run: |
curl -X POST \
"https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys" \
-H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"clearCache": "do_not_clear"}'
- name: Wait for deploy
run: sleep 120
- name: Health check
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
${{ secrets.RENDER_URL }}/health)
if [ "$STATUS" != "200" ]; then
echo "Health check failed with status $STATUS"
exit 1
fi
echo "Health check passed"
Configurar secrets en GitHub
GitHub → tu repo → Settings → Secrets and variables → Actions
Agregar:
- RENDER_API_KEY: (Dashboard → Account Settings → API Keys)
- RENDER_SERVICE_ID: (Dashboard → tu servicio → la URL contiene el ID: srv-xxx)
- RENDER_URL: https://tu-servicio.onrender.com
Deshabilitar auto-deploy de Render
Si usas GitHub Actions para controlar el deploy, desactiva el auto-deploy de Render:
Dashboard → tu servicio → Settings → Build & Deploy
Auto-Deploy: OFF
Ahora solo GitHub Actions trigger deploys, después de pasar tests.
GitHub Actions + Railway
Deploy con Railway CLI en GitHub Actions
# .github/workflows/deploy-railway.yml
name: Deploy to Railway
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -r app/requirements.txt
pip install -r tests/requirements-test.txt
- name: Run tests
run: pytest tests/ -v
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Install Railway CLI
run: npm install -g @railway/cli
- name: Deploy to staging
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: railway up --environment staging --detach
- name: Wait for staging deploy
run: sleep 60
- name: Health check staging
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
${{ secrets.RAILWAY_STAGING_URL }}/health)
if [ "$STATUS" != "200" ]; then
echo "Staging health check failed"
exit 1
fi
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Railway CLI
run: npm install -g @railway/cli
- name: Deploy to production
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: railway up --environment production --detach
- name: Wait for production deploy
run: sleep 60
- name: Health check production
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
${{ secrets.RAILWAY_PRODUCTION_URL }}/health)
if [ "$STATUS" != "200" ]; then
echo "Production health check failed!"
exit 1
fi
echo "Production deploy successful"
Obtener Railway Token
# Generar token de servicio (no usa tu sesión personal)
# Dashboard → tu proyecto → Settings → Tokens → Create Token
# O desde CLI:
railway tokens create
# Token: rlwy_xxx
# Agregar a GitHub Secrets:
# RAILWAY_TOKEN = rlwy_xxx
# RAILWAY_STAGING_URL = https://...staging.up.railway.app
# RAILWAY_PRODUCTION_URL = https://...production.up.railway.app
Configurar entornos en Railway
# Railway soporta múltiples entornos nativamente
# Dashboard → Project → Settings → Environments
# Crear entorno staging:
# + New Environment → staging
# Cada entorno tiene:
# - Sus propias variables
# - Su propia URL
# - Su propia instancia de base de datos (opcional)
GitHub Actions + Fly.io
Deploy con flyctl en GitHub Actions
# .github/workflows/deploy-flyio.yml
name: Deploy to Fly.io
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -r app/requirements.txt
pip install -r tests/requirements-test.txt
- name: Run tests
run: pytest tests/ -v
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Setup Fly.io CLI
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to staging
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: flyctl deploy --app docusearch-ai-staging --remote-only
- name: Health check staging
run: |
sleep 30
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://docusearch-ai-staging.fly.dev/health)
if [ "$STATUS" != "200" ]; then
echo "Staging health check failed"
exit 1
fi
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Fly.io CLI
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to production
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: flyctl deploy --app docusearch-ai --remote-only
- name: Health check production
run: |
sleep 30
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://docusearch-ai.fly.dev/health)
if [ "$STATUS" != "200" ]; then
echo "Production health check failed!"
flyctl releases --app docusearch-ai
exit 1
fi
echo "Production deploy successful"
- name: Show deployment info
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
flyctl status --app docusearch-ai
flyctl releases --app docusearch-ai --image
Obtener Fly.io API Token
# Crear token
flyctl tokens create deploy -x 999999h
# Token: FlyV1 xxx
# Agregar a GitHub Secrets:
# FLY_API_TOKEN = FlyV1 xxx
Fly.io: Apps de staging y producción
# Crear app de staging (separada de producción)
flyctl launch --name docusearch-ai-staging --region iad --no-deploy
# Copiar secrets a staging
flyctl secrets set OPENAI_API_KEY=sk-staging-xxx --app docusearch-ai-staging
# Ahora tienes:
# Production: docusearch-ai.fly.dev
# Staging: docusearch-ai-staging.fly.dev
Environment Promotion: Staging → Production
El patrón de promotion
Feature branch → PR → Tests → Merge to main
↓
Deploy a STAGING (automático en push a main)
↓
Health check + smoke tests en staging
↓
Deploy a PRODUCTION (automático si staging pasa)
↓
Health check en production
↓
Notificación de éxito/fallo
Workflow de promotion
El patrón es: test → staging → smoke tests → production → health check. Cada job depende del anterior. Si staging falla, production no se ejecuta. El workflow completo se implementa en los ejercicios de esta cápsula combinando los bloques de cada plataforma mostrados arriba.
Comparativa: CI/CD por Plataforma
| Aspecto | Render | Railway | Fly.io |
|---|---|---|---|
| Auto-deploy nativo | ✅ Git push | ✅ Git push | ❌ (necesita Actions) |
| GitHub Actions setup | API REST (curl) | CLI (railway up) | CLI (flyctl deploy) |
| Action oficial | ❌ | ❌ | ✅ superfly/flyctl-actions |
| Multi-environment | Manual (servicios separados) | ✅ Nativo (environments) | Manual (apps separadas) |
| Rollback | Dashboard (redeploy anterior) | Dashboard + CLI | flyctl releases rollback |
| Deploy hooks | ✅ Deploy hooks URL | ✅ Webhooks | ❌ (usar Actions) |
| Secret management | Dashboard/API | CLI + Dashboard | CLI + Dashboard |
Troubleshooting
Problema 1: "GitHub Actions falla — railway/flyctl command not found"
Solución: Necesitas instalar la CLI en el runner de GitHub Actions:
# Railway
- name: Install Railway
run: npm install -g @railway/cli
# Fly.io (usar action oficial)
- uses: superfly/flyctl-actions/setup-flyctl@master
Problema 2: "Deploy funciona local pero falla en CI — authentication error"
Solución: En CI no puedes usar login interactivo. Necesitas tokens:
# Railway — usa RAILWAY_TOKEN
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
# Fly.io — usa FLY_API_TOKEN
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
# Render — usa API key en headers
-H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}"
Problema 3: "Health check pasa en staging pero falla en production"
Solución: Variables de entorno diferentes entre staging y production. Verifica:
- name: Debug environment
run: |
echo "Checking staging..."
curl -s ${{ secrets.STAGING_URL }}/health | jq .
echo "Checking production..."
curl -s ${{ secrets.PRODUCTION_URL }}/health | jq .
Causas comunes:
- API key de producción expirada o incorrecta
- Base de datos de producción no migrada
- Límites de plan diferentes (Free vs Starter)
Problema 4: "El pipeline tarda mucho — 10+ minutos"
Solución: Optimiza el pipeline:
# Cachear dependencias Python
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
# Reducir wait times (usar polling en vez de sleep fijo)
- name: Health check with polling
run: |
for i in $(seq 1 20); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $URL/health 2>/dev/null)
if [ "$STATUS" = "200" ]; then exit 0; fi
sleep 10
done
exit 1
Problema 5: "Quiero rollback automático si production falla"
Solución: Cada plataforma tiene su mecanismo:
# Fly.io — rollback al release anterior
flyctl releases rollback --app docusearch-ai
# Railway — redeploy desde un commit anterior
railway up --commit abc123
# Render — desde dashboard, click "Manual Deploy" en un deploy anterior
# O via API:
curl -X POST "https://api.render.com/v1/services/$SVC_ID/deploys" \
-H "Authorization: Bearer $API_KEY" \
-d '{"commitId": "abc123"}'
Ejercicios Prácticos
Ejercicio 1: Pipeline básico de CI/CD
Crea un GitHub Actions workflow que ejecute tests y despliegue a tu plataforma elegida cuando haces push a main.
Ver solución
# .github/workflows/deploy.yml
name: Test and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
working-directory: app
run: |
pip install -r requirements.txt
pip install pytest httpx
- name: Run tests
working-directory: app
run: pytest tests/ -v
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
# === Elegir UNO según tu plataforma ===
# Render:
- name: Deploy to Render
if: false # Cambiar a true si usas Render
run: |
curl -X POST \
"https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys" \
-H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" \
-H "Content-Type: application/json"
# Railway:
- name: Deploy to Railway
if: false # Cambiar a true si usas Railway
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: |
npm install -g @railway/cli
railway up --detach
# Fly.io:
- name: Deploy to Fly.io
if: false # Cambiar a true si usas Fly.io
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
curl -L https://fly.io/install.sh | sh
flyctl deploy --remote-only
- name: Health check
run: |
sleep 60
curl -f ${{ secrets.APP_URL }}/health
# Configurar secrets en GitHub
# Settings → Secrets → Actions → New repository secret
# Para Render:
# RENDER_API_KEY, RENDER_SERVICE_ID, APP_URL
# Para Railway:
# RAILWAY_TOKEN, APP_URL
# Para Fly.io:
# FLY_API_TOKEN, APP_URL
# Push y verifica
git add .github/workflows/deploy.yml
git commit -m "Add CI/CD pipeline"
git push origin main
# Ve a GitHub → Actions para monitorear el pipeline
Ejercicio 2: Pipeline con staging → production
Extiende el pipeline anterior para que despliegue primero a staging, ejecute smoke tests, y luego promueva a production. Usa GitHub Environments para separar staging/production con sus propias variables.
Ver solución
# .github/workflows/deploy-staged.yml
name: Staged Deployment
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- run: pip install -r app/requirements.txt pytest httpx
- run: pytest tests/ -v
staging:
needs: test
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: |
npm install -g @railway/cli
railway up --environment staging --detach
- name: Verify staging
run: |
sleep 60
curl -f ${{ vars.STAGING_URL }}/health
production:
needs: staging
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: |
npm install -g @railway/cli
railway up --environment production --detach
- name: Verify production
run: |
sleep 60
curl -f ${{ vars.PRODUCTION_URL }}/health
Ejercicio 3: Configurar rollback automático
Agrega un step que haga rollback automático si el health check de producción falla después del deploy.
Ver solución
# Agregar después del deploy de producción:
production:
needs: smoke-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Get current release
id: pre_deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
CURRENT=$(flyctl releases --app docusearch-ai --json | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Version'])")
echo "version=$CURRENT" >> $GITHUB_OUTPUT
echo "Current release: v$CURRENT"
- name: Deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: flyctl deploy --app docusearch-ai --remote-only
- name: Health check with rollback
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
sleep 30
HEALTHY=false
for i in $(seq 1 10); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://docusearch-ai.fly.dev/health 2>/dev/null)
if [ "$STATUS" = "200" ]; then
HEALTHY=true
break
fi
echo "Health check attempt $i failed (status: $STATUS)"
sleep 10
done
if [ "$HEALTHY" = "false" ]; then
echo "::error::Production unhealthy — rolling back to v${{ steps.pre_deploy.outputs.version }}"
flyctl releases rollback --app docusearch-ai \
${{ steps.pre_deploy.outputs.version }}
sleep 30
ROLLBACK_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://docusearch-ai.fly.dev/health 2>/dev/null)
echo "Rollback health check: $ROLLBACK_STATUS"
exit 1
fi
echo "Production deploy successful"
Ejercicio 4: Notificación de deploy por webhook
Agrega notificaciones de deploy exitoso/fallido usando un webhook (Discord, Slack, o cualquier URL).
Ver solución
# Job de notificación al final del pipeline
notify:
needs: [test, staging, production]
runs-on: ubuntu-latest
if: always()
steps:
- name: Build notification payload
id: payload
run: |
if [ "${{ needs.production.result }}" = "success" ]; then
STATUS="success"
COLOR="3066993"
MSG="Deploy exitoso en producción"
elif [ "${{ needs.staging.result }}" = "failure" ]; then
STATUS="failed"
COLOR="15158332"
MSG="Deploy falló en staging"
elif [ "${{ needs.test.result }}" = "failure" ]; then
STATUS="failed"
COLOR="15158332"
MSG="Tests fallaron — deploy abortado"
else
STATUS="failed"
COLOR="15158332"
MSG="Deploy falló en producción"
fi
echo "status=$STATUS" >> $GITHUB_OUTPUT
echo "color=$COLOR" >> $GITHUB_OUTPUT
echo "msg=$MSG" >> $GITHUB_OUTPUT
- name: Send Discord notification
if: secrets.DISCORD_WEBHOOK != ''
run: |
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "Deploy: ${{ steps.payload.outputs.status }}",
"description": "${{ steps.payload.outputs.msg }}",
"color": ${{ steps.payload.outputs.color }},
"fields": [
{"name": "Repo", "value": "${{ github.repository }}", "inline": true},
{"name": "Branch", "value": "${{ github.ref_name }}", "inline": true},
{"name": "Commit", "value": "${{ github.sha }}", "inline": false}
]
}]
}'
- name: Send Slack notification
if: secrets.SLACK_WEBHOOK != ''
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d '{
"text": "${{ steps.payload.outputs.msg }} — ${{ github.repository }}@${{ github.ref_name }}"
}'
Resumen
- GitHub Actions conecta tu pipeline de CI con el deployment en cualquier plataforma.
- Render no tiene CLI — el deploy se trigger via API REST con curl.
- Railway se integra con su CLI (
railway up --environment xxx) usandoRAILWAY_TOKEN. - Fly.io tiene action oficial (
superfly/flyctl-actions) y CLI potente. - Environment promotion (staging → production) es el patrón profesional: nunca despliegues a producción sin verificar en staging.
- Health checks después del deploy son obligatorios — no asumas que el deploy fue exitoso porque no hubo error.
- Rollback automático es posible en Fly.io (
releases rollback), Railway (redeploy commit), y Render (API deploy con commitId). - Tests antes del deploy son la primera línea de defensa — si los tests fallan, el deploy no se ejecuta.
Recursos Adicionales
- GitHub Actions Documentation — Documentación oficial
- Render Deploy Hooks — Trigger deploys desde CI
- Railway CI/CD — Guía de integración CI/CD
- Fly.io GitHub Actions — Guía oficial de CI/CD
- GitHub Environments — Staging/production environments
- Deployment Best Practices — Martin Fowler — Principios de deployment pipeline