From 6123953165cda89dc6a96b5fe140e8bd7843c74f Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 8 May 2026 18:54:22 +0200 Subject: [PATCH] feat(infra): add local-dev Docker Compose stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring up Postgres + Redis + OTel Collector in one command so contributors can run the BFF end-to-end without each setting up the runtime services manually. Replaces the throwaway `docker run postgres:17-alpine` one-liner that was in §3 of docs/development.md. What lands: - `infra/local/dev.compose.yml` — three core services (postgres 17.2-alpine, redis 7.4-alpine, otel/opentelemetry-collector-contrib 0.115.0) plus two viewers gated behind Compose profiles (pgweb under `--profile dbtools`, Jaeger 1.62 under `--profile observability`). All ports overridable via .env; state in named volumes; healthchecks on the data services. - `infra/local/.env.example` — credentials + ports template. POSTGRES_PASSWORD and REDIS_PASSWORD are mandatory (compose refuses to boot without them); other keys default sensibly. - `infra/local/init/postgres/01-init.sql` — bootstrap SQL per ADR-0013: `audit_owner` / `audit_writer` / `audit_reader` / `audit_archiver` roles plus `audit` schema, with default privileges encoding the append-only contract (INSERT to writer, SELECT to reader, DELETE to archiver, no UPDATE / TRUNCATE to anyone). Applied on first Postgres boot only; documented re-run procedure in infra/README.md. - `infra/local/otel-collector.yaml` — pipeline: OTLP gRPC/HTTP → batch → debug exporter (always) + forward to `jaeger:4317` (no-op when the observability profile is off; logs warn-level retry but doesn't block other exporters). Surrounding docs updated: - `infra/README.md` — new "Local-dev stack" section with service inventory, port table, first-time setup, operational tips. The `local/` row replaces the previous placeholder. - `docs/development.md` §3 — rewritten to walk through the compose-based setup; cross-links to `infra/README.md` for the full reference. Roadmap entry for "Local infra recipe" removed from §8 (now implemented); "Observability dev-loop" entry adjusted to point at the new Jaeger profile. Production parity is intentionally left for the on-prem infrastructure ADR (phase 3b) — this is the dev-only stack. --- docs/development.md | 56 +++++----- infra/README.md | 85 ++++++++++++--- infra/local/.env.example | 28 +++++ infra/local/dev.compose.yml | 145 ++++++++++++++++++++++++++ infra/local/init/postgres/01-init.sql | 48 +++++++++ infra/local/otel-collector.yaml | 65 ++++++++++++ 6 files changed, 390 insertions(+), 37 deletions(-) create mode 100644 infra/local/.env.example create mode 100644 infra/local/dev.compose.yml create mode 100644 infra/local/init/postgres/01-init.sql create mode 100644 infra/local/otel-collector.yaml diff --git a/docs/development.md b/docs/development.md index 30997f1..873503f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -97,19 +97,28 @@ cd apps/portal-bff && pnpm exec prisma generate && cd ../.. pnpm nx run-many -t lint test build ``` -For the BFF to actually run end-to-end, you'll also need: +For the BFF to actually run end-to-end, you'll also need the local infrastructure stack — Postgres, Redis, OpenTelemetry Collector — provisioned via Docker Compose: ```bash -# .env from .env.example, then fill in DATABASE_URL with your local Postgres -cp apps/portal-bff/.env.example apps/portal-bff/.env +# 1. Configure infra secrets (copy template, edit, do not commit). +cp infra/local/.env.example infra/local/.env +$EDITOR infra/local/.env +# Set strong dev values for POSTGRES_PASSWORD and REDIS_PASSWORD. -# Local Postgres via Docker (one-liner; will be replaced by a Docker Compose file in a later doc) -docker run -d --name apf-postgres -p 5432:5432 \ - -e POSTGRES_USER=portal -e POSTGRES_PASSWORD=portal -e POSTGRES_DB=portal_dev \ - postgres:17-alpine +# 2. Bring up the core stack (postgres + redis + otel-collector). +docker compose -f infra/local/dev.compose.yml up -d + +# 3. (Optional) Activate viewers when debugging: +docker compose -f infra/local/dev.compose.yml --profile dbtools up -d # pgweb +docker compose -f infra/local/dev.compose.yml --profile observability up -d # Jaeger UI + +# 4. App-side env from .env.example, then fill in DATABASE_URL pointing +# to the compose-managed Postgres (matches the values you set in +# infra/local/.env). +cp apps/portal-bff/.env.example apps/portal-bff/.env ``` -A proper local-dev infra spec (Postgres HA-like, Redis, OTel collector) will land with the on-prem infrastructure ADR; in the meantime the one-liner above is sufficient to run the BFF. +Full reference for the local stack — service inventory, port table, persistence, bootstrap re-run procedure — lives in [`infra/README.md`](../infra/README.md) → "Local-dev stack". --- @@ -350,19 +359,18 @@ This doc starts as a phase-1 + cross-cutting reference. As features for later ph When a section grows beyond a short subsection, it is extracted to its own file under `docs/development/`. Per the documentation convention (see [README.md](README.md)), we group into a folder once we have at least three related files; this doc is then re-organised into an index pointing at the extracted files. Until then, all sections live here. -| Future section | Phase | Triggered by | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Local infra recipe** — Docker Compose for Postgres, Redis, OTel Collector, and a Postgres-friendly viewer. | 2 / 3b | First feature that needs Redis (sessions, ADR-0010). Earlier if the BFF gains a meaningful amount of business code. | -| **Auth dev-loop** — Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. | 2 | Auth flow code lands ([ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md)) once the dev tenant is provisioned by IT. | -| **Session inspection** — reading the Redis session store in dev, decrypting the AES-GCM `tokens` blob with the dev key, force-logout patterns. | 2 | Sessions module lands ([ADR-0010](decisions/0010-session-management-redis.md)). | -| **MFA step-up debugging** — triggering claims-challenge flows, verifying `mfaVerifiedAt` freshness, testing the SPA HTTP interceptor that handles 401 + claims challenge. | 2 | First `@RequireMfa()` route lands ([ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md)). | -| **Observability dev-loop** — running an OTel collector container locally, viewing traces (Tempo / Jaeger UI), reading Pino logs with `pino-pretty`, correlating front spans to BFF spans via `traceparent`. | 2 | OTel SDK setup lands ([ADR-0012](decisions/0012-observability-pino-opentelemetry.md)); needs the local collector from "Local infra recipe". | -| **Audit-log inspection workflow** — querying `audit.events` as `audit_reader`, joining with app logs by `trace_id`, validating the append-only role grants in dev. | 2 | Audit module lands ([ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md)). | -| **Downstream API integration recipe** — adding a new `DownstreamApiConfig`, choosing the auth strategy (OBO vs service+assertion), wiring resilience policies, testing with a mocked downstream. | 2 | First downstream client lands ([ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md)). | -| **Component patterns library** — the in-house, spartan-style components (Angular CDK + Tailwind) as they ship, with a11y notes per component (keyboard model, ARIA, screen-reader expectations). | 5b suite | First non-placeholder component in `libs/shared/ui/`. | -| **a11y testing workflow** — running axe-core via Playwright locally, screen-reader testing notes (NVDA / VoiceOver / TalkBack), the APF user-panel cadence and how to triage findings. | 3a | First Playwright e2e suite touching real screens ([ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)). | -| **Performance debugging** — running Lighthouse CI locally with full config, reading the HTML reports, using `source-map-explorer` to investigate bundle bloat, interpreting BFF p95/p99 from OTel. | 3a | Lighthouse already wired in CI ([ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md)); section grows when first real route is added to the critical-routes list. | -| **Debugging tips** — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | cross | Accumulates organically as the team encounters them. | -| **Release workflow** — tag-driven release, what `release.yml` does, version bumping, changelog generation from Conventional Commits. | 3b | On-prem infrastructure ADR + populated `release.yml`. | -| **GitLab migration runbook** — when the org migrates Gitea → GitLab, how the workflows are ported, which level-2 sections of [ADR-0015](decisions/0015-cicd-gitea-actions.md) get superseded. | future | GitLab migration ADR (6–18 months horizon). | -| **Architecture overview diagrams** — high-level component diagrams, data-flow diagrams, trust boundaries (for security review). | cross | First major architecture review or onboarding cohort ≥ 3 contributors. | +| Future section | Phase | Triggered by | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Auth dev-loop** — Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. | 2 | Auth flow code lands ([ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md)) once the dev tenant is provisioned by IT. | +| **Session inspection** — reading the Redis session store in dev, decrypting the AES-GCM `tokens` blob with the dev key, force-logout patterns. | 2 | Sessions module lands ([ADR-0010](decisions/0010-session-management-redis.md)). | +| **MFA step-up debugging** — triggering claims-challenge flows, verifying `mfaVerifiedAt` freshness, testing the SPA HTTP interceptor that handles 401 + claims challenge. | 2 | First `@RequireMfa()` route lands ([ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md)). | +| **Observability dev-loop** — viewing traces in the Jaeger UI provisioned by `infra/local/dev.compose.yml --profile observability`, reading Pino logs with `pino-pretty`, correlating front spans to BFF spans via `traceparent`. | 2 | OTel SDK setup lands ([ADR-0012](decisions/0012-observability-pino-opentelemetry.md)). | +| **Audit-log inspection workflow** — querying `audit.events` as `audit_reader`, joining with app logs by `trace_id`, validating the append-only role grants in dev. | 2 | Audit module lands ([ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md)). | +| **Downstream API integration recipe** — adding a new `DownstreamApiConfig`, choosing the auth strategy (OBO vs service+assertion), wiring resilience policies, testing with a mocked downstream. | 2 | First downstream client lands ([ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md)). | +| **Component patterns library** — the in-house, spartan-style components (Angular CDK + Tailwind) as they ship, with a11y notes per component (keyboard model, ARIA, screen-reader expectations). | 5b suite | First non-placeholder component in `libs/shared/ui/`. | +| **a11y testing workflow** — running axe-core via Playwright locally, screen-reader testing notes (NVDA / VoiceOver / TalkBack), the APF user-panel cadence and how to triage findings. | 3a | First Playwright e2e suite touching real screens ([ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)). | +| **Performance debugging** — running Lighthouse CI locally with full config, reading the HTML reports, using `source-map-explorer` to investigate bundle bloat, interpreting BFF p95/p99 from OTel. | 3a | Lighthouse already wired in CI ([ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md)); section grows when first real route is added to the critical-routes list. | +| **Debugging tips** — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | cross | Accumulates organically as the team encounters them. | +| **Release workflow** — tag-driven release, what `release.yml` does, version bumping, changelog generation from Conventional Commits. | 3b | On-prem infrastructure ADR + populated `release.yml`. | +| **GitLab migration runbook** — when the org migrates Gitea → GitLab, how the workflows are ported, which level-2 sections of [ADR-0015](decisions/0015-cicd-gitea-actions.md) get superseded. | future | GitLab migration ADR (6–18 months horizon). | +| **Architecture overview diagrams** — high-level component diagrams, data-flow diagrams, trust boundaries (for security review). | cross | First major architecture review or onboarding cohort ≥ 3 contributors. | diff --git a/infra/README.md b/infra/README.md index 4f4d7ac..26d4e91 100644 --- a/infra/README.md +++ b/infra/README.md @@ -2,16 +2,16 @@ Infrastructure-as-code artefacts for the project. Separate from application code and from documentation: this folder contains the recipes and configs that the team and ops use to stand up running infrastructure (CI runners, future local-dev databases, future on-prem deploy assets). -| Subject | File / Folder | ADR / Reference | -| -------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------- | -| Self-hosted CI runners (Gitea Actions) | [`ci-runners.compose.yml`](ci-runners.compose.yml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) | -| Shared `act_runner` configuration | [`runner-config.yaml`](runner-config.yaml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) | -| Runtime state of the runners | `data/` (git-ignored after `.gitignore`) | — | -| Env-vars template for the runners | `.env.example` (`.env` is git-ignored) | — | +| Subject | File / Folder | ADR / Reference | +| -------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Self-hosted CI runners (Gitea Actions) | [`ci-runners.compose.yml`](ci-runners.compose.yml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) | +| Shared `act_runner` configuration | [`runner-config.yaml`](runner-config.yaml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) | +| Runtime state of the runners | `data/` (git-ignored after `.gitignore`) | — | +| Env-vars template for the runners | `.env.example` (`.env` is git-ignored) | — | +| Local-dev runtime stack | [`local/`](local/) | [ADR-0006](../docs/decisions/0006-persistence-postgresql-prisma.md), [ADR-0010](../docs/decisions/0010-session-management-redis.md), [ADR-0012](../docs/decisions/0012-observability-pino-opentelemetry.md), [ADR-0013](../docs/decisions/0013-audit-trail-separated-postgres-append-only.md) | Future folders / files that will land here as the corresponding ADRs ship: -- **`local/`** — Docker Compose for the developer's machine (Postgres + Redis + OTel collector). Triggered by the first feature that needs Redis (sessions, ADR-0010). - **`prod/`** — On-prem deploy manifests (HA Postgres, Redis Sentinel, OTel collector + backend, secret manager). Triggered by the on-prem infrastructure ADR (phase 3b). --- @@ -121,13 +121,72 @@ Old, no-longer-referenced images can be reaped during the periodic `docker syste --- +## Local-dev stack — `local/` + +A Docker Compose recipe spinning up the runtime services the BFF and ADRs assume — Postgres, Redis, OpenTelemetry Collector — plus optional viewers (pgweb, Jaeger UI) gated behind Compose profiles. Designed to start in a single command on a contributor's WSL2 / Linux / macOS host. + +| File | Role | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [`local/dev.compose.yml`](local/dev.compose.yml) | Service definitions: postgres, redis, otel-collector, plus pgweb and jaeger behind profiles | +| [`local/.env.example`](local/.env.example) | Credentials + ports template (copy to `.env`, which is git-ignored) | +| [`local/init/postgres/01-init.sql`](local/init/postgres/01-init.sql) | Bootstrap SQL for ADR-0013: audit roles + schema, applied on first boot only | +| [`local/otel-collector.yaml`](local/otel-collector.yaml) | Collector pipeline: OTLP receivers → batch → debug exporter (always) + forward to Jaeger when active | + +### First-time setup + +```bash +cd infra/local + +# 1. Configure local secrets (copy template, edit, do not commit). +cp .env.example .env +$EDITOR .env +# Set strong dev values for POSTGRES_PASSWORD and REDIS_PASSWORD +# (defaults in the template are placeholders that the compose +# rejects with `must be set in infra/local/.env` if left as-is). + +# 2. Bring up the core stack (postgres + redis + otel-collector). +docker compose -f dev.compose.yml up -d + +# 3. (Optional) Activate viewers when needed: +docker compose -f dev.compose.yml --profile dbtools up -d # pgweb +docker compose -f dev.compose.yml --profile observability up -d # Jaeger UI +docker compose -f dev.compose.yml --profile dbtools --profile observability up -d # both + +# 4. Verify health. +docker compose -f dev.compose.yml ps +``` + +### Service endpoints (defaults) + +| Service | Host port | Purpose | +| ------------------- | --------- | ------------------------------------------------------------------- | +| Postgres | 5432 | DB connection — `postgres://portal:@localhost:5432/portal_dev` | +| Redis | 6379 | Sessions, OBO cache (per ADR-0010 / ADR-0014) | +| OTel Collector gRPC | 4317 | `OTEL_EXPORTER_OTLP_ENDPOINT` for the BFF and the SPA | +| OTel Collector HTTP | 4318 | OTLP/HTTP variant | +| pgweb (profile) | 8081 | http://localhost:8081 — Postgres GUI | +| Jaeger UI (profile) | 16686 | http://localhost:16686 — trace explorer | + +All ports are overridable via `.env` if the host machine has conflicts. + +### Operational tips + +- **Persistence** — state lives in named Docker volumes (`apf-portal-postgres-data`, `apf-portal-redis-data`). Survives `docker compose down`. Use `docker compose -f dev.compose.yml down -v` to wipe (also wipes the audit-roles bootstrap, which re-runs on the next fresh boot). +- **Bootstrap re-run** — the SQL in `local/init/postgres/` only runs on a **fresh** Postgres data volume. To replay after editing the file, `down -v` (loses all dev data) or run the SQL manually with `docker compose exec postgres psql -U portal -d portal_dev -f /docker-entrypoint-initdb.d/01-init.sql`. +- **Logs** — `docker compose -f dev.compose.yml logs -f ` to follow a single service. `otel-collector` is the loudest — its `debug` exporter prints every span / metric / log it receives. +- **Image upgrades** — same policy as the runner image (deliberate, not via `:latest`). Renovate's docker-compose manager will surface bumps automatically once the dashboard rule allows them. + +### Production parity + +This stack is **dev-only**. The corresponding production layout (HA Postgres, Redis Sentinel cluster, OTel Collector with a real backend, secret manager) lives in the future on-prem-infrastructure ADR — see `prod/` placeholder below. + +--- + ## Future infra concerns — placeholders These are listed here so a contributor knows where to expect related files; they don't exist yet. -| File | Purpose | Triggered by | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| `local/dev.compose.yml` | Postgres 17 + Redis (Sentinel-flavoured single-node) + OTel collector for local dev | First feature that needs Redis or end-to-end OTel traces | -| `local/init/postgres/*.sql` | Bootstrap SQL to create the Postgres roles `audit_owner` / `audit_writer` / `audit_reader` / `audit_archiver` (per ADR-0013) and provision the dev DB | Same as above | -| `prod/*` | On-prem deployment manifests (k8s, Compose, or whatever the on-prem infra ADR settles on) | The on-prem infrastructure ADR (phase 3b) | -| `runbooks/*.md` | Operational runbooks (incident response, secret rotation, runner upgrade procedure, …) | First incident, or when ops cadence justifies them | +| File | Purpose | Triggered by | +| --------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------- | +| `prod/*` | On-prem deployment manifests (k8s, Compose, or whatever the on-prem infra ADR settles on) | The on-prem infrastructure ADR (phase 3b) | +| `runbooks/*.md` | Operational runbooks (incident response, secret rotation, runner upgrade procedure, …) | First incident, or when ops cadence justifies them | diff --git a/infra/local/.env.example b/infra/local/.env.example new file mode 100644 index 0000000..c4a8454 --- /dev/null +++ b/infra/local/.env.example @@ -0,0 +1,28 @@ +# Local-dev secrets and ports for `infra/local/dev.compose.yml`. +# Copy to `.env` (which is git-ignored) and adjust as needed. +# +# cp .env.example .env +# $EDITOR .env + +# ---------------------------------------------------------------- Postgres +# `POSTGRES_PASSWORD` is mandatory — the compose refuses to boot without it. +POSTGRES_USER=portal +POSTGRES_PASSWORD=portal_dev_change_me +POSTGRES_DB=portal_dev +POSTGRES_PORT=5432 + +# ---------------------------------------------------------------- Redis +# Same — mandatory. +REDIS_PASSWORD=redis_dev_change_me +REDIS_PORT=6379 + +# ---------------------------------------------------------------- OTel +OTEL_GRPC_PORT=4317 +OTEL_HTTP_PORT=4318 + +# ---------------------------------------------------------------- Optional viewers +# Only consumed when the matching Compose profile is activated: +# --profile dbtools → pgweb +# --profile observability → Jaeger UI +PGWEB_PORT=8081 +JAEGER_UI_PORT=16686 diff --git a/infra/local/dev.compose.yml b/infra/local/dev.compose.yml new file mode 100644 index 0000000..921fcfa --- /dev/null +++ b/infra/local/dev.compose.yml @@ -0,0 +1,145 @@ +# Local-dev infrastructure stack for `apf_portal`. +# +# Spins up the runtime dependencies the BFF and ADRs assume: +# - PostgreSQL 17 (per ADR-0006) +# - Redis 7 (per ADR-0010 — single node in dev, +# Sentinel HA in prod) +# - OpenTelemetry Collector (per ADR-0012 — receives OTLP from the +# BFF and the SPA, debug-logs traces in +# v1, forwards to Jaeger when activated) +# +# Optional viewers behind Compose profiles: +# - --profile dbtools → pgweb (Postgres GUI) +# - --profile observability → Jaeger UI (trace explorer) +# +# Usage from infra/local/: +# cp .env.example .env +# $EDITOR .env # set strong dev passwords +# docker compose -f dev.compose.yml up -d +# +# To bring up viewers too: +# docker compose -f dev.compose.yml --profile dbtools --profile observability up -d +# +# State persists in named volumes (`apf-portal-postgres-data`, +# `apf-portal-redis-data`). To wipe and start fresh: +# docker compose -f dev.compose.yml down -v +# +# Bootstrap SQL (audit roles + schema, per ADR-0013) is applied on +# first boot from init/postgres/. To replay after editing it, wipe +# the volume with `down -v` above. + +name: apf-portal-dev + +services: + postgres: + image: postgres:17.2-alpine + container_name: apf-portal-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-portal} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in infra/local/.env} + POSTGRES_DB: ${POSTGRES_DB:-portal_dev} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - postgres-data:/var/lib/postgresql/data + # Bootstrap SQL runs only on a fresh data volume. + - ./init/postgres:/docker-entrypoint-initdb.d:ro + networks: + - apf-portal-dev + healthcheck: + test: + ['CMD-SHELL', 'pg_isready -U "${POSTGRES_USER:-portal}" -d "${POSTGRES_DB:-portal_dev}"'] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7.4-alpine + container_name: apf-portal-redis + restart: unless-stopped + # AOF (`--appendonly yes`) keeps sessions across container restarts — + # the dev equivalent of the persistence we want from the prod + # Sentinel cluster (per ADR-0010 §"Persistence"). + command: + - redis-server + - --requirepass + - ${REDIS_PASSWORD:?REDIS_PASSWORD must be set in infra/local/.env} + - --appendonly + - 'yes' + ports: + - '${REDIS_PORT:-6379}:6379' + volumes: + - redis-data:/data + networks: + - apf-portal-dev + healthcheck: + test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'PING'] + interval: 5s + timeout: 3s + retries: 5 + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.115.0 + container_name: apf-portal-otel-collector + restart: unless-stopped + command: ['--config=/etc/otel-collector.yaml'] + volumes: + - ./otel-collector.yaml:/etc/otel-collector.yaml:ro + ports: + # OTLP receivers — the BFF and the SPA send here. + - '${OTEL_GRPC_PORT:-4317}:4317' + - '${OTEL_HTTP_PORT:-4318}:4318' + networks: + - apf-portal-dev + + # ------------------------------------------------------------------ + # Optional: Postgres GUI (`--profile dbtools`). + # pgweb is a lightweight, no-config alternative to pgAdmin. + # ------------------------------------------------------------------ + pgweb: + image: sosedoff/pgweb:0.16.2 + container_name: apf-portal-pgweb + restart: unless-stopped + profiles: [dbtools] + environment: + DATABASE_URL: postgres://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-portal_dev}?sslmode=disable + ports: + - '${PGWEB_PORT:-8081}:8081' + depends_on: + postgres: + condition: service_healthy + networks: + - apf-portal-dev + + # ------------------------------------------------------------------ + # Optional: Jaeger UI for trace exploration (`--profile observability`). + # The collector pipeline above always tries to forward to `jaeger:4317`; + # when this profile is off, those exports fail silently (logged at + # warn level by the collector — no blocking effect on the debug + # exporter that prints traces to stdout). + # OTLP ports (4317/4318) are NOT published from this service to + # avoid colliding with the collector — the collector talks to + # Jaeger on the internal Compose network. + # ------------------------------------------------------------------ + jaeger: + image: jaegertracing/all-in-one:1.62 + container_name: apf-portal-jaeger + restart: unless-stopped + profiles: [observability] + environment: + COLLECTOR_OTLP_ENABLED: 'true' + ports: + - '${JAEGER_UI_PORT:-16686}:16686' + networks: + - apf-portal-dev + +volumes: + postgres-data: + name: apf-portal-postgres-data + redis-data: + name: apf-portal-redis-data + +networks: + apf-portal-dev: + name: apf-portal-dev diff --git a/infra/local/init/postgres/01-init.sql b/infra/local/init/postgres/01-init.sql new file mode 100644 index 0000000..ebccf5a --- /dev/null +++ b/infra/local/init/postgres/01-init.sql @@ -0,0 +1,48 @@ +-- Bootstrap SQL for local-dev Postgres, applied on FIRST boot only +-- (Postgres image runs files in /docker-entrypoint-initdb.d once, +-- when the data volume is empty). +-- +-- Implements the role + schema layout from ADR-0013 (Audit trail, +-- separated Postgres schema, append-only by Postgres role grants). +-- Production deployment manifests (future infrastructure ADR) will +-- replicate the same layout with real service accounts; in dev the +-- default `portal` user (created automatically from POSTGRES_USER) +-- is the schema owner, and the audit roles are granted to it for +-- testing role-based access patterns locally. + +-- ---------------------------------------------------------------- Audit roles +-- NOLOGIN: these are permission containers, not login users. The BFF +-- connects as a login user that has been GRANTed the role. +CREATE ROLE audit_owner NOLOGIN; +CREATE ROLE audit_writer NOLOGIN; +CREATE ROLE audit_reader NOLOGIN; +CREATE ROLE audit_archiver NOLOGIN; + +-- ---------------------------------------------------------------- Audit schema +-- Owned by audit_owner so DEFAULT PRIVILEGES below take effect for +-- every future table created under the schema (Prisma migration etc.). +CREATE SCHEMA audit AUTHORIZATION audit_owner; + +GRANT USAGE ON SCHEMA audit TO audit_writer, audit_reader, audit_archiver; + +-- ---------------------------------------------------------------- Append-only contract +-- ADR-0013 §"Append-only by role grants": +-- audit_writer → INSERT only +-- audit_reader → SELECT only +-- audit_archiver → DELETE only (used to prune past retention) +-- nobody → UPDATE, TRUNCATE +ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit + GRANT INSERT ON TABLES TO audit_writer; + +ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit + GRANT SELECT ON TABLES TO audit_reader; + +ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit + GRANT DELETE ON TABLES TO audit_archiver; + +-- ---------------------------------------------------------------- Dev convenience +-- The default `portal` superuser bypasses these grants anyway, but +-- granting the audit roles explicitly lets us test role-based access +-- with `SET ROLE audit_writer;` etc. from a psql session against the +-- dev DB. Production never grants all four to one user. +GRANT audit_owner, audit_writer, audit_reader, audit_archiver TO portal; diff --git a/infra/local/otel-collector.yaml b/infra/local/otel-collector.yaml new file mode 100644 index 0000000..8e8c6fb --- /dev/null +++ b/infra/local/otel-collector.yaml @@ -0,0 +1,65 @@ +# OpenTelemetry Collector config for the local-dev stack. +# +# v1 scope (per ADR-0012): +# - Accept OTLP from the BFF and the SPA on gRPC (4317) and HTTP +# (4318). The default OTEL_EXPORTER_OTLP_ENDPOINT in dev points +# here. +# - Batch + log everything to stdout via the `debug` exporter — the +# dev sees what their app emits without standing up a backend. +# - When the `observability` Compose profile is active, additionally +# forward to Jaeger (`jaeger:4317` on the internal Compose +# network). When the profile is inactive, the OTLP→Jaeger export +# fails at warn level but does not impact the debug pipeline. +# +# Production replaces the debug exporter with the proper backend +# (chosen in the future on-prem infrastructure ADR — likely Tempo + +# Loki + Mimir, or an OpenTelemetry-friendly all-in-one). + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + # Small batches in dev so the developer sees output quickly. + timeout: 5s + send_batch_size: 100 + +exporters: + debug: + verbosity: detailed + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + sending_queue: + enabled: true + # When jaeger isn't running (profile off), retries fall back + # to a small queue rather than blocking the pipeline. + queue_size: 100 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 30s + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug, otlp/jaeger] + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [debug] + telemetry: + logs: + level: info -- 2.30.2