Ir al contenido

Runbooks

Procedimientos operativos. Para cada uno: qué hace, cuándo se ejecuta, comandos exactos, qué verificar al final.

  1. Deploy de tcgcards-web (frontend)
  2. Deploy de tcgcards-api (backend)
  3. Rollback de un deploy
  4. Restaurar backup de Mongo
  5. Rotar un secret
  6. Agregar/quitar admin a un usuario
  7. Forzar la ejecución de un cron job
  8. Actualizar/agregar credenciales Mercado Pago
  9. Investigar un error en producción
  10. Cambiar dominio

Cuándo: cada vez que quieres que un cambio del repo tcgcards-web llegue a tcgcards.cl.

Procedimiento (resumen):

  1. Push a la rama staging → Vercel publica en staging.tcgcards.cl (automático). Pruebas ahí.
  2. Fusiona la rama del featuremain (no despliega nada · nunca stagingmain).
  3. GitHub → tcgcards-webActions“Deploy Web → PROD (manual)” → Run workflow → main.

Fallback sin Actions: cd /Users/yoel/Documents/Repos/tcgcards-web && vercel --prod.

Cómo verificar:

  • El run de “Deploy Web → PROD” en verde; en su log aparece Aliased https://tcgcards.cl.
  • Abrir https://tcgcards.cl en incognito → verificar el cambio en producción.

Tiempo total: ~6-7 min (CI + build en Vercel).


Cuándo: cada vez que quieres que un cambio del repo tcgcards-api llegue a producción. Esto no es automático.

Procedimiento (resumen):

  1. Push a la rama staging → GitHub Actions despliega tcgcards-api-staging (automático). Pruebas ahí.
  2. Fusiona la rama del featuremain (no despliega nada · nunca stagingmain).
  3. GitHub → tcgcards-apiActions“Deploy API → PROD (manual)” → Run workflow → main. (Ejecuta CI y despliega por --image.)

Fallback sin Actions (build remoto en Cloud Build, igual destino):

Ventana de terminal
cd /Users/yoel/Documents/Repos/tcgcards-api
bash scripts/deploy-prod.sh
# equivale a: gcloud run deploy tcgcards-api --source . --region us-central1 --project api-cards-prod --quiet

Cómo verificar:

Ventana de terminal
# Status del servicio
gcloud run services describe tcgcards-api \
--region us-central1 \
--project api-cards-prod \
--format="value(status.conditions[0].type,status.conditions[0].status,status.latestReadyRevisionName)"
# Esperado: "Ready True tcgcards-api-XXXXX-xxx"
# Sanity check endpoint público
curl -s -o /dev/null -w "%{http_code}\n" \
"https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/health"
# Esperado: 200

Tiempo total: 5-8 min (build + container push + revision routing).


  1. https://vercel.comtcgcards-web → Deployments
  2. Encuentra el deploy anterior bueno.
  3. Click en los ... → “Promote to Production”.

Toma ~30 segundos. No requiere CLI.

Ventana de terminal
# 1. Listar revisiones recientes
gcloud run revisions list \
--service tcgcards-api \
--region us-central1 \
--project api-cards-prod \
--limit 5
# 2. Apuntar 100% del tráfico a una revisión específica
gcloud run services update-traffic tcgcards-api \
--to-revisions=tcgcards-api-00128-gpd=100 \
--region us-central1 \
--project api-cards-prod

(Reemplaza tcgcards-api-00128-gpd por la revisión deseada.)

Cómo verificar: comando describe igual que en deploy.


Cuándo: se borró o corrompió data, o quieres ver/recuperar algo de un backup.

Conceptos en 30 segundos (si nunca hiciste esto)

Sección titulada «Conceptos en 30 segundos (si nunca hiciste esto)»
  • Backup / dump: una copia completa de la base de datos en un momento dado. Es un solo archivo: dump-AÑOMESDÍA-HORAMINSEG.archive.gz. La hora es en UTC (~4 h adelante de Chile).
  • Dónde viven: en Google Cloud Storage, en gs://tcgcards-mongo-backups/.
  • Restaurar: tomar ese archivo y cargarlo en una base MongoDB.
  • Local vs producción:
    • Local = una MongoDB “de juguete” en tu computador. No puede romper nada. Empieza siempre por acá.
    • Producción = la base real que usan los usuarios. Tocarla es peligroso.
  • Situación A — “Solo quiero MIRAR un backup o recuperar algún dato perdido”. → Seguro, no toca producción. Hazlo siempre primero.
  • Situación B — “Se borró/corrompió data en PRODUCCIÓN y hay que devolverla”. → Peligroso. Más abajo, con calma.

Paso 0 — Instalar herramientas (una sola vez)

Sección titulada «Paso 0 — Instalar herramientas (una sola vez)»

En una terminal (Mac):

Ventana de terminal
brew tap mongodb/brew
brew install mongodb-database-tools mongodb-community mongosh
brew install --cask google-cloud-sdk # solo si aún no tienes gcloud

Verifica que quedaron instaladas (cada comando debe imprimir una versión, no un error):

Ventana de terminal
mongorestore --version
mongosh --version
gcloud --version

Paso 0b — Conectarte a Google Cloud (una sola vez)

Sección titulada «Paso 0b — Conectarte a Google Cloud (una sola vez)»
Ventana de terminal
gcloud auth login # abre el navegador; inicia sesión con yoeldts2@gmail.com
gcloud config set project api-cards-prod
gcloud auth list # debe mostrar tu correo con un "*" al lado

Si gcloud auth list no muestra tu correo como activo, repite gcloud auth login.


Situación A — Restaurar en un Mongo local (no puedes romper nada)

Sección titulada «Situación A — Restaurar en un Mongo local (no puedes romper nada)»

A1. Ver los backups disponibles y elegir uno

Ventana de terminal
gcloud storage ls gs://tcgcards-mongo-backups/ | tail -20

Verás líneas como gs://tcgcards-mongo-backups/dump-20260529-211811.archive.gz. El 20260529-211811 es la fecha/hora UTC (29-may-2026, 21:18:11 UTC). Elige el más reciente de ANTES de que ocurriera el problema. Para usar el último de todos:

Ventana de terminal
ULTIMO=$(gcloud storage ls gs://tcgcards-mongo-backups/ | sort | tail -1)
echo "Voy a usar: $ULTIMO"

A2. Descargarlo a tu computador

Ventana de terminal
mkdir -p /tmp/restore
gcloud storage cp "$ULTIMO" /tmp/restore/dump.archive.gz

(Si elegiste uno específico, reemplaza "$ULTIMO" por la ruta completa gs://tcgcards-mongo-backups/dump-FECHA.archive.gz.)

A3. Confirmar que el archivo no está corrupto

Ventana de terminal
gzip -t /tmp/restore/dump.archive.gz && echo "OK: archivo íntegro"

Debe imprimir OK: archivo íntegro. Si da error, se dañó al bajar — bórralo y vuelve a descargar.

A4. Levantar una MongoDB de juguete (Terminal 1)

Abre una terminal y deja este comando corriendo. Va a quedar imprimiendo logs — es normal, NO cierres esta terminal:

Ventana de terminal
mkdir -p /tmp/mongo-juguete
mongod --dbpath /tmp/mongo-juguete --port 27018

A5. Cargar el backup en la base de juguete (Terminal 2)

Abre otra terminal (deja la anterior corriendo) y ejecuta:

Ventana de terminal
mongorestore --uri="mongodb://localhost:27018" \
--archive=/tmp/restore/dump.archive.gz --gzip

Qué deberías ver al final: una línea como 207445 document(s) restored successfully. 0 document(s) failed to restore. Lo importante es 0 document(s) failed.

A6. Mirar los datos

Ventana de terminal
mongosh "mongodb://localhost:27018/api-cards"

Ya dentro (verás un prompt >), prueba:

show collections // lista las colecciones
db.cards.countDocuments() // ~205000
db.orders.countDocuments()
// Buscar un documento específico que se haya perdido, por ejemplo una orden:
db.orders.findOne({ _id: "order-XXXXXXX" })
// O un usuario por correo:
db.users.findOne({ email: "persona@ejemplo.cl" })

Para salir de mongosh escribe exit.

A7. Limpiar al terminar

  1. Vuelve a la Terminal 1 y aprieta Ctrl+C (apaga la base de juguete).
  2. Borra los temporales:
Ventana de terminal
rm -rf /tmp/mongo-juguete /tmp/restore

Listo. No tocaste producción en ningún momento.


Situación B — Restaurar EN producción (PELIGROSO)

Sección titulada «Situación B — Restaurar EN producción (PELIGROSO)»

B1. Antes de tocar nada: backup del estado ACTUAL

Esto te da un “punto de retorno” por si la restauración empeora las cosas:

Ventana de terminal
gcloud run jobs execute mongo-backup \
--region=us-central1 --project=api-cards-prod --wait

Espera a que termine (exit 0). El estado actual quedó guardado en GCS.

B2. Decide el alcance (clave)

  • ¿Solo una colección quedó mal (ej. listings)? → restaura solo esa (Opción B-mín). Es lo más seguro: no pierdes el resto.
  • ¿Está toda la base corrupta/borrada? → restauración completa (Opción B-total). Último recurso.

B3. Obtener la conexión a producción y el dump

Ventana de terminal
URI=$(gcloud secrets versions access latest --secret=mongodb-uri --project=api-cards-prod)

Asegúrate de tener el dump elegido descargado en /tmp/restore/dump.archive.gz (Pasos A1–A3). Si no, hazlo ahora.

Opción B-mín — Restaurar UNA sola colección (recomendado)

Reemplaza listings por la colección afectada:

Ventana de terminal
mongorestore --uri="$URI" --gzip \
--archive=/tmp/restore/dump.archive.gz \
--nsInclude="api-cards.listings" --drop
  • --nsInclude="api-cards.listings" = restaura solo esa colección.
  • --drop = borra esa colección antes de restaurarla (la deja igual al backup).

Opción B-total — Restaurar TODA la base (último recurso)

Ventana de terminal
mongorestore --uri="$URI" --gzip \
--archive=/tmp/restore/dump.archive.gz --drop

B4. Verificar

Ventana de terminal
mongosh "$URI" --quiet --eval '
const d = db.getSiblingDB("api-cards");
print("cards: " + d.cards.countDocuments());
print("orders: " + d.orders.countDocuments());
print("wallet_entries: " + d.wallet_entries.countDocuments());
const sys = d.users.findOne({_id:"user-system-tcgcards"});
print("system wallet balance: " + (sys ? sys.walletBalance : "NO ENCONTRADO"));
'

Los números deberían tener sentido (cards ~205000) y el system wallet debe aparecer.

B5. Si quedó peor que antes

Tienes el backup del Paso B1: repite la restauración usando ese archivo (el que generaste en B1) para volver al estado previo.


RPO efectivo: ~1 hora (los backups son cada hora). Drill de restore verificado el 2026-05-29 (restauración completa, 0 documentos fallidos). El usuario de la app no puede dropear bases de datos (least-privilege), un freno extra contra borrados accidentales.


Ventana de terminal
# 1. Generar el nuevo valor (ejemplo para secret aleatorio de 32 bytes hex)
NEW_VALUE=$(openssl rand -hex 32)
# 2. Agregar como nueva versión
echo -n "$NEW_VALUE" | gcloud secrets versions add INTERNAL_JWT_SECRET \
--data-file=- \
--project=api-cards-prod
# 3. Verificar
gcloud secrets versions access latest \
--secret=INTERNAL_JWT_SECRET \
--project=api-cards-prod
# 4. Redeploy del Cloud Run para que aplique
cd /Users/yoel/Documents/Repos/tcgcards-api
gcloud run deploy tcgcards-api \
--source . \
--region us-central1 \
--project api-cards-prod \
--quiet
Ventana de terminal
# Listar
vercel env ls
# Quitar el viejo
vercel env rm INTERNAL_JWT_SECRET production
# Agregar el nuevo (interactive, pide valor)
vercel env add INTERNAL_JWT_SECRET production
# Redeploy para que aplique
vercel --prod

Cambiar valor sensible y mantener compatibilidad

Sección titulada «Cambiar valor sensible y mantener compatibilidad»

Para minimizar downtime al rotar secrets compartidos:

  1. Generar nuevo valor.
  2. Setear en backend + redeploy.
  3. Setear en frontend + redeploy.
  4. (Entre paso 2 y 3 hay ~2 min de incompatibilidad. Hacer en horario de menor tráfico.)

No hay UI para esto. Se hace por Mongo directo.

Ventana de terminal
# Conectar
mongosh "<MONGODB_URI>"
> use api-cards
# Hacer admin
> db.users.updateOne({ username: "ejemplo" }, { $set: { isAdmin: true } })
# Quitar admin
> db.users.updateOne({ username: "ejemplo" }, { $set: { isAdmin: false } })
# Verificar
> db.users.findOne({ username: "ejemplo" }, { isAdmin: 1, username: 1 })

El cambio aplica inmediatamente — el siguiente request del user con sesión activa lo verá como admin. No hace falta reiniciar nada.


Ventana de terminal
gcloud scheduler jobs run orders-tick \
--project=api-cards-prod \
--location=us-central1

Reemplaza orders-tick por el job que quieras. Lista completa:

  • mongo-backup-hourly
  • orders-tick
  • orders-awaiting-payment-cleanup
  • wallet-reconcile-daily
  • refunds-reconcile-daily

Cómo verificar:

Ventana de terminal
# Ver logs del Cloud Run del último minuto
gcloud run services logs read tcgcards-api \
--region=us-central1 \
--project=api-cards-prod \
--limit=50

Buscar líneas con prefijo cron. (e.g., cron.canceled, cron.completed).


Actualizar/agregar credenciales Mercado Pago

Sección titulada «Actualizar/agregar credenciales Mercado Pago»

Cuándo: se necesita rotar MP_ACCESS_TOKEN, el webhook secret, o cuando MP cambia algo del lado de ellos.

Ventana de terminal
# 1. Obtener token nuevo desde el panel MP → Credenciales productivas (o test si testing)
# El token es de la forma APP_USR-XXXXXX-XXXXXX-XXXXXX-XXXXXX
# 2. Actualizar el secret
echo -n "APP_USR-..." | gcloud secrets versions add MP_ACCESS_TOKEN \
--data-file=- \
--project=api-cards-prod
# 3. Redeploy
cd /Users/yoel/Documents/Repos/tcgcards-api
gcloud run deploy tcgcards-api --source . --region us-central1 --project api-cards-prod --quiet

Para el MP_WEBHOOK_SECRET:

  1. En el panel MP, configurar el webhook con el endpoint: https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/webhooks/mp
  2. MP genera un secret — copiarlo
  3. echo -n "SECRET" | gcloud secrets versions add MP_WEBHOOK_SECRET --data-file=- --project=api-cards-prod
  4. Redeploy.

https://sentry.io → proyecto correspondiente (web o api) → buscar el error por mensaje o usuario.

Ventana de terminal
gcloud run services logs read tcgcards-api \
--region=us-central1 \
--project=api-cards-prod \
--limit=100 \
--format=json | jq '.[] | select(.severity == "ERROR")'

3. Mongo directo (datos del user/orden afectada)

Sección titulada «3. Mongo directo (datos del user/orden afectada)»
Ventana de terminal
mongosh "<MONGODB_URI>"
> use api-cards
> db.orders.findOne({ _id: "order-xxxxx" })
> db.users.findOne({ email: "afectado@example.com" }, { walletBalance: 1, status: 1 })

https://app.posthog.com → buscar por user properties (email/username).


Si alguna vez se cambia el dominio tcgcards.cl a otro:

  1. DNS en Cloudflare: apuntar el nuevo dominio a Vercel + Cloud Run.
  2. Vercel: agregar el nuevo dominio en el proyecto tcgcards-web → Settings → Domains.
  3. Env vars del web: actualizar NEXT_PUBLIC_SITE_URL → redeploy Vercel.
  4. Env vars del api: actualizar WEB_PUBLIC_URL (afecta los emails que mandamos) → redeploy Cloud Run.
  5. OAuth Google: agregar https://nuevo-dominio.cl/api/auth/callback/google a los Authorized Redirect URIs en Google Cloud Console.
  6. MP webhooks: registrar el nuevo URL en el panel MP.
  7. Resend: verificar el nuevo dominio (SPF + DKIM en Cloudflare DNS) para poder mandar emails como notificaciones@nuevo-dominio.
  8. Email Routing en Cloudflare: repetir setup soporte@ y disputas@ para el nuevo dominio.
  9. Sentry: agregar el nuevo dominio a CORS allowlist en cada proyecto.
  10. Cloudflare Access (docs): actualizar la aplicación de Zero Trust si el dominio de docs también cambia.

Es un cambio grande — hacer en ventana de mantenimiento, con OK explícito.