diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d89693ff..748ce246 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,58 +1,43 @@ -name: Deploy to EC2 - -# Auto-deploy on every push to main (or run manually). Keyless: assumes an AWS -# IAM role via GitHub OIDC and triggers the deploy on the instance through SSM -# (no SSH, no AWS keys stored in GitHub). +name: Deploy API (srv07) +# Autodeploy de la API en srv07 al hacer push a main. +# +# La clave `SRV07_DEPLOY_KEY` entra como root pero está atada a un *forced +# command* en /root/.ssh/authorized_keys: sshd ejecuta siempre +# /usr/local/sbin/responsegrid-deploy e ignora lo que mande el cliente. Es decir, +# esta clave no puede hacer nada más que desplegar, y el script de deploy vive en +# el servidor (con git reset --hard, build, migraciones y health check). +# El `script:` de abajo existe solo porque la action lo exige. on: push: branches: [main] + paths: + - 'apps/api/**' + - 'packages/**' + - 'deploy/**' + - 'package.json' + - 'pnpm-workspace.yaml' + - 'pnpm-lock.yaml' + - '.github/workflows/deploy.yml' workflow_dispatch: concurrency: - group: responsegrid-deploy + group: deploy-api-srv07 cancel-in-progress: false -permissions: - id-token: write - contents: read +# El job solo usa secretos: sin permisos para GITHUB_TOKEN. +permissions: {} jobs: deploy: runs-on: ubuntu-latest steps: - - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 + - name: SSH deploy to srv07 + uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0 with: - role-to-assume: arn:aws:iam::386707340306:role/responsegrid-gha-deploy - aws-region: us-east-1 - - - name: Deploy on EC2 via SSM - run: | - set -euo pipefail - CMD='cd /opt/responsegrid && git fetch origin && git reset --hard origin/main && printf "DD_VERSION=%s\n" "$(git rev-parse --short HEAD)" > deploy/.env.version && install -m 644 deploy/responsegrid.service /etc/systemd/system/responsegrid.service && systemctl daemon-reload && systemctl enable responsegrid.service && docker compose -f deploy/docker-compose.prod.yml up -d --build && docker compose -f deploy/docker-compose.prod.yml ps' - PARAMS=$(jq -n --arg c "$CMD" '{commands: [$c]}') - CID=$(aws ssm send-command \ - --instance-ids i-0f5979080ad5da6e5 \ - --document-name AWS-RunShellScript \ - --comment "GHA ${GITHUB_SHA::7}" \ - --timeout-seconds 2400 \ - --parameters "$PARAMS" \ - --query Command.CommandId --output text) - echo "SSM CommandId: $CID" - for i in $(seq 1 220); do - sleep 9 - ST=$(aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query Status --output text 2>/dev/null || echo Pending) - echo "status: $ST" - if [ "$ST" = "Success" ]; then - aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query StandardOutputContent --output text | tail -25 - exit 0 - fi - case "$ST" in - Failed|Cancelled|TimedOut) - echo "::error::deploy $ST" - aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query StandardErrorContent --output text | tail -40 - exit 1 ;; - esac - done - echo "::error::timed out waiting for SSM deploy"; exit 1 + host: ${{ secrets.SRV07_HOST }} + username: ${{ secrets.SRV07_USER }} + key: ${{ secrets.SRV07_DEPLOY_KEY }} + command_timeout: 20m + script: | + true diff --git a/AGENTS.md b/AGENTS.md index 6ee6e4a1..717cc320 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Canonical instructions for any AI agent or contributor working in this repo. Rea ## What ResponseGrid is -Multi-emergency **material aid coordination + logistics** platform (org: **Global Emergency**). Live: web `https://responsegrid.app` (Vercel), API `https://api.responsegrid.app` (EC2). Activated per emergency; data isolated by `emergency_id`/slug. Connects citizens, organizations and coordinators during a disaster. +Multi-emergency **material aid coordination + logistics** platform (org: **Global Emergency**). Live: web `https://responsegrid.app` (Vercel), API `https://responsegrid-api.globalemergency.online` (srv07, Plesk + Docker; the legacy `https://api.responsegrid.app` is a Cloudflare 308 redirect to it). Activated per emergency; data isolated by `emergency_id`/slug. Connects citizens, organizations and coordinators during a disaster. **In scope:** collection/logistic points (puntos de acopio) **with declared material inventory per place**, validated needs (with 48h freshness), material offers + matching to needs, **a single shared catalogue of supplies + categories** (insumos), **transport capacity + shipments** (logistics), volunteers + tasks, field reports (incident/stock/status), real-time Leaflet map, **authorization** (roles/grants/groups/API keys), public read-only API + developer `/docs`. @@ -92,7 +92,7 @@ Verify `dynamic(ssr:false)` components (the Leaflet map) only in a **production* ## Deploy -A merged PR → push to `main` triggers: **GitHub Action** deploys the API to EC2 via SSM (builds the Docker image, applies migrations with `migrate.sh`) **and** Vercel auto-deploys the web. The CI gate having passed is what keeps prod healthy. +A merged PR → push to `main` triggers: **GitHub Action** deploys the API to srv07 over SSH (builds the Docker image, applies migrations with `migrate.sh`; see `docs/deploy/srv07.md`) **and** Vercel auto-deploys the web. The CI gate having passed is what keeps prod healthy. ## Public emergency for testing diff --git a/README.md b/README.md index 22f3aabd..8be7be73 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ Flujo de trabajo: ## 🚀 Despliegue -- [`docs/deploy/aws-free-tier.md`](docs/deploy/aws-free-tier.md) — guía paso a paso: **web en Vercel + API/Postgres/Redis en una EC2 + S3** (la guía usa t3.micro free-tier; producción corre en **t3.small** por el agente de observabilidad). Artefactos en [`deploy/`](deploy) (Dockerfile, `docker-compose.prod.yml`, migraciones, Caddy). +- [`docs/deploy/srv07.md`](docs/deploy/srv07.md) — **despliegue actual de producción**: web en Vercel + API/Postgres/Redis en srv07 (Plesk + Docker) en `responsegrid-api.globalemergency.online`, detrás de nginx (`api.responsegrid.app` redirige ahí). Artefactos en [`deploy/`](deploy) (`docker-compose.srv07.yml`, unit de systemd, migraciones). +- [`docs/deploy/aws-free-tier.md`](docs/deploy/aws-free-tier.md) — alternativa en AWS, guía paso a paso: **web en Vercel + API/Postgres/Redis en una EC2 + S3** (la guía usa t3.micro free-tier; producción corre en **t3.small** por el agente de observabilidad). Artefactos en [`deploy/`](deploy) (Dockerfile, `docker-compose.prod.yml`, migraciones, Caddy). - [`deploy/datadog.md`](deploy/datadog.md) — observabilidad: agente **Datadog** (host, contenedores, Postgres+DBM, Redis, logs y **APM/trazas**), sitio EU. ## 📚 Documentación diff --git a/deploy/docker-compose.srv07.yml b/deploy/docker-compose.srv07.yml new file mode 100644 index 00000000..149a075f --- /dev/null +++ b/deploy/docker-compose.srv07.yml @@ -0,0 +1,98 @@ +# ResponseGrid — stack de producción para srv07 (Plesk + Docker compartido). +# +# Diferencias con docker-compose.prod.yml (EC2): +# - Sin Caddy: Plesk/nginx ya ocupa 80/443 y termina TLS para +# responsegrid-api.globalemergency.online, y hace proxy_pass a 127.0.0.1:3100. +# - La API publica un puerto SOLO en loopback (127.0.0.1:3100 -> 3000). +# - Sin agente Datadog propio: srv07 ya tiene uno. Las trazas APM se envían +# al agente del host por la gateway de la red bridge (DD_AGENT_HOST). +# - Sin rol de instancia IAM: las credenciales de S3 son de un usuario IAM +# dedicado y viven en deploy/.env. +# +# Uso (en srv07, como root): +# cd /opt/responsegrid +# docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d --build + +name: responsegrid + +services: + postgres: + image: postgres:16 + restart: unless-stopped + env_file: .env + command: + - postgres + - -c + - shared_preload_libraries=pg_stat_statements + - -c + - track_activity_query_size=4096 + - -c + - pg_stat_statements.track=all + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] + interval: 5s + timeout: 5s + retries: 10 + # Autodiscovery del agente Datadog del host (lee las labels vía docker.sock). + labels: + com.datadoghq.ad.checks: | + {"postgres":{"init_config":{},"instances":[{"host":"%%host%%","port":5432,"username":"datadog","password":"%%env_DD_POSTGRES_PASSWORD%%","dbname":"%%env_POSTGRES_DB%%","dbm":true}]}} + + redis: + image: redis:7 + restart: unless-stopped + command: ['redis-server', '--save', '60', '1'] + volumes: + - redisdata:/data + labels: + com.datadoghq.ad.checks: | + {"redisdb":{"init_config":{},"instances":[{"host":"%%host%%","port":6379}]}} + + migrate: + image: postgres:16 + env_file: .env + depends_on: + postgres: + condition: service_healthy + volumes: + - ../apps/api/drizzle:/migrations:ro + - ./migrate.sh:/migrate.sh:ro + entrypoint: ['sh', '/migrate.sh'] + restart: 'no' + + api: + build: + context: .. + dockerfile: apps/api/Dockerfile + restart: unless-stopped + env_file: + - path: .env + required: true + - path: .env.version + required: false + environment: + - DD_TRACE_ENABLED=${DD_TRACE_ENABLED:-false} + # Agente Datadog del host. 172.17.0.1 es la gateway de la bridge por + # defecto; verificar con: ip -4 addr show docker0 + # El agente debe tener DD_APM_NON_LOCAL_TRAFFIC=true y escuchar en 0.0.0.0:8126. + - DD_AGENT_HOST=${DD_AGENT_HOST:-172.17.0.1} + - DD_TRACE_AGENT_PORT=8126 + - DD_ENV=prod + - DD_SERVICE=responsegrid-api + - DD_LOGS_INJECTION=true + # El X-Forwarded-For lo pone nginx de Plesk. + - DD_TRACE_CLIENT_IP_ENABLED=true + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_started + # SOLO loopback: nginx de Plesk es quien expone el servicio al exterior. + ports: + - '127.0.0.1:3100:3000' + +volumes: + pgdata: + redisdata: diff --git a/deploy/responsegrid.srv07.service b/deploy/responsegrid.srv07.service new file mode 100644 index 00000000..119b3539 --- /dev/null +++ b/deploy/responsegrid.srv07.service @@ -0,0 +1,20 @@ +# Unit que levanta el stack en cada arranque de srv07. +# La instala el deploy (/usr/local/sbin/responsegrid-deploy) como +# /etc/systemd/system/responsegrid.service. +# Hace falta porque el contenedor `api` depende del one-shot `migrate`, y las +# políticas de restart de Docker por sí solas no reconcilian el stack al reiniciar. +[Unit] +Description=ResponseGrid stack (docker compose up on boot) +Requires=docker.service +After=docker.service network-online.target +Wants=network-online.target +ConditionPathExists=/opt/responsegrid/deploy/docker-compose.srv07.yml + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/opt/responsegrid +ExecStart=/usr/bin/docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d + +[Install] +WantedBy=multi-user.target diff --git a/docs/deploy/srv07.md b/docs/deploy/srv07.md new file mode 100644 index 00000000..d670e0d2 --- /dev/null +++ b/docs/deploy/srv07.md @@ -0,0 +1,60 @@ +# Producción actual: srv07 (Plesk + Docker) + +La API corre en **srv07** (`srv07.ingenierosweb.co`), no en AWS. La guía de +[`aws-free-tier.md`](aws-free-tier.md) sigue siendo válida como alternativa +autocontenida, pero no describe el despliegue vivo. + +La web (`API_URL` y `NEXT_PUBLIC_API_URL` en Vercel) apunta directamente al +subsitio. El dominio antiguo `api.responsegrid.app` es solo una *Redirect Rule* +308 de Cloudflare hacia él (conserva ruta y query), para consumidores externos y +para el callback de Google OAuth (`OAUTH_CALLBACK_BASE`), que sigue registrado con +ese dominio. + +``` +responsegrid.app (Vercel) ──► responsegrid-api.globalemergency.online + │ nginx de Plesk (TLS wildcard) + ▼ + 127.0.0.1:3100 → contenedor api + postgres · redis +``` + +## Diferencias con el stack de la EC2 + +| | EC2 (`docker-compose.prod.yml`) | srv07 (`docker-compose.srv07.yml`) | +|---|---|---| +| TLS y reverse proxy | Caddy en 80/443 | nginx de Plesk, la API solo escucha en `127.0.0.1:3100` | +| Datadog | contenedor `datadog/agent:7` propio | agente del host, trazas a `DD_AGENT_HOST` (gateway de docker0) | +| Credenciales de S3 | rol de instancia EC2 | usuario IAM con claves en `deploy/.env` | +| Arranque | `responsegrid.service` | igual, desde `deploy/responsegrid.srv07.service` | + +El proyecto de compose se llama `responsegrid` (`-p responsegrid`) para no +colisionar con los demás stacks del servidor. + +## Despliegue + +Push a `main` → [`deploy.yml`](../../.github/workflows/deploy.yml) entra por SSH +a srv07. La clave `SRV07_DEPLOY_KEY` está atada a un *forced command* en +`/root/.ssh/authorized_keys`, así que solo puede ejecutar +`/usr/local/sbin/responsegrid-deploy`, que hace: + +1. `git fetch` + `git reset --hard origin/main` +2. escribe `deploy/.env.version` con el sha corto (etiqueta `DD_VERSION`) +3. instala y habilita la unit de systemd +4. `docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d --build` + (el servicio `migrate` aplica las migraciones pendientes antes de arrancar la API) +5. health check contra `http://127.0.0.1:3100/emergencies`; si no responde 200 + en 2 minutos, falla el deploy y vuelca los logs de la API + +Secretos del repo: `SRV07_HOST`, `SRV07_USER`, `SRV07_DEPLOY_KEY`. + +## Operación manual + +```bash +cd /opt/responsegrid +docker compose -p responsegrid -f deploy/docker-compose.srv07.yml ps +docker compose -p responsegrid -f deploy/docker-compose.srv07.yml logs api --tail 50 +/usr/local/sbin/responsegrid-deploy # el mismo deploy, a mano +``` + +Backup diario de Postgres en `/etc/cron.daily/responsegrid-backup` → +`/var/backups/responsegrid`, retención 14 días.