mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
165 lines
7.5 KiB
YAML
165 lines
7.5 KiB
YAML
# EP-00-T04 (walking skeleton) + EP-16-T01 (grows into the full stack).
|
|
#
|
|
# `core` profile = the minimum that actually runs: gateway + sales-domain +
|
|
# finance-domain + OPA. OPA stopped being optional as of M1 (EP-03/EP-05-T06):
|
|
# the Gateway's `lifespan()` fails closed at startup if it cannot sync
|
|
# entitlements to a PDP, so "gateway without OPA" is not a smaller working
|
|
# stack, it's a Gateway that refuses to start (README.md §38) -- by design,
|
|
# not an oversight. `identity` profile (EP-01-T01) adds a real Keycloak with
|
|
# the realm this RI's client registrations expect -- opt-in via `.env` (see
|
|
# `.env.example`), since the Gateway's "no OIDC provider configured" mode
|
|
# (deny-by-default via handler-level checks) is just as valid a deployment
|
|
# choice as configuring one (see gateway/server.py's module docstring).
|
|
# `observability` profile (EP-08-T04) adds a real OTel Collector + Jaeger.
|
|
#
|
|
# EP-05-T07 (anti-bypass): `opa`, `sales-domain` and `finance-domain`
|
|
# deliberately publish no host port below -- on a real Docker host, that
|
|
# already means nothing outside the compose network can reach them at all
|
|
# (Docker only exposes a container port past the host via `ports:`); only
|
|
# `gateway` does. The domain servers add a second, in-process layer on top
|
|
# (`emcp_bus.audit.bypass_detection.BackendCredentialGate`, applied when
|
|
# `main()` runs, i.e. exactly this deployment): a request lacking the
|
|
# Gateway's own outbound service credential is rejected even if network
|
|
# segmentation were ever misconfigured. See RI/README.md, "O que foi
|
|
# validado com Docker", for exactly what was run and the two real bugs this
|
|
# caught (Dockerfile missing `COPY config`; `opa`'s healthcheck relying on a
|
|
# `wget` the official image doesn't ship).
|
|
|
|
services:
|
|
opa:
|
|
image: openpolicyagent/opa:1.20.2
|
|
command: ["run", "--server", "--addr", "0.0.0.0:8181", "/policies"]
|
|
volumes:
|
|
- ./config/policies:/policies:ro
|
|
profiles: ["core"]
|
|
# No host port published (EP-05-T07) -- only `gateway`, on the same
|
|
# compose network, ever reaches this service.
|
|
# No HEALTHCHECK either: the official image is a scratch/distroless-style
|
|
# build with only the `opa` binary -- no shell, wget, curl or nc to probe
|
|
# itself with (confirmed by `docker exec`: "sh": executable file not
|
|
# found). `gateway` below compensates with `service_started` +
|
|
# `restart: on-failure`, which is also just the fail-closed behavior
|
|
# (README.md §38) working as intended: a Gateway that starts before OPA
|
|
# is actually accepting connections refuses to come up and gets retried,
|
|
# rather than silently caching an empty entitlement set.
|
|
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.7.3
|
|
command: ["start-dev", "--import-realm"]
|
|
environment:
|
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
|
# Fixed so the `iss` claim on every issued token is always
|
|
# `http://keycloak:8080/realms/emcp`, regardless of whether the caller
|
|
# reached Keycloak via the container network (`keycloak:8080`, what the
|
|
# Gateway uses to validate) or the host-mapped port (`127.0.0.1:8080`,
|
|
# e.g. to fetch a token for manual testing) -- without this, Keycloak
|
|
# derives `iss` from the request's own Host header, so a token fetched
|
|
# from the host would fail the Gateway's issuer check.
|
|
KC_HOSTNAME: keycloak
|
|
KC_HOSTNAME_STRICT: "false"
|
|
volumes:
|
|
- ./deploy/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
|
|
ports:
|
|
- "8080:8080"
|
|
profiles: ["identity"]
|
|
# Same reasoning as `opa`: `start-dev` is a JVM/Quarkus dev-mode image
|
|
# with no lightweight in-container probe available out of the box, and
|
|
# ~15-20s cold start (schema init + realm import) makes a short
|
|
# healthcheck interval noisy. `gateway`'s own `restart: on-failure`
|
|
# covers the startup race the same way it does for `opa`.
|
|
|
|
sales-domain:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/Dockerfile
|
|
command: ["python3", "-m", "example_mcp_servers.sales_domain.server"]
|
|
environment:
|
|
SALES_DOMAIN_HOST: "0.0.0.0"
|
|
SALES_DOMAIN_PORT: "8100"
|
|
SALES_DOMAIN_SERVICE_TOKEN: "dev-sales-domain-service-token"
|
|
EMCP_AUDIT_LOG_PATH: "/tmp/emcp-audit.jsonl"
|
|
profiles: ["core"]
|
|
# No host port published (EP-05-T07) -- see module-level comment above.
|
|
healthcheck:
|
|
# A bare TCP connect, not an HTTP/MCP-protocol check: streamable-http
|
|
# rejects a plain GET, so this only confirms the process is listening.
|
|
test: ["CMD", "python3", "-c", "import socket; socket.create_connection(('127.0.0.1', 8100), timeout=2)"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
finance-domain:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/Dockerfile
|
|
command: ["python3", "-m", "example_mcp_servers.finance_domain.server"]
|
|
environment:
|
|
FINANCE_DOMAIN_HOST: "0.0.0.0"
|
|
FINANCE_DOMAIN_PORT: "8101"
|
|
FINANCE_DOMAIN_SERVICE_TOKEN: "dev-finance-domain-service-token"
|
|
EMCP_AUDIT_LOG_PATH: "/tmp/emcp-audit.jsonl"
|
|
profiles: ["core"]
|
|
# No host port published (EP-05-T07) -- see module-level comment above.
|
|
healthcheck:
|
|
test: ["CMD", "python3", "-c", "import socket; socket.create_connection(('127.0.0.1', 8101), timeout=2)"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
gateway:
|
|
build:
|
|
context: .
|
|
dockerfile: deploy/docker/Dockerfile
|
|
command: ["python3", "-m", "emcp_bus.gateway.server"]
|
|
environment:
|
|
GATEWAY_HOST: "0.0.0.0"
|
|
GATEWAY_PORT: "8000"
|
|
SALES_DOMAIN_URL: "http://sales-domain:8100/mcp"
|
|
FINANCE_DOMAIN_URL: "http://finance-domain:8101/mcp"
|
|
OPA_URL: "http://opa:8181"
|
|
SALES_DOMAIN_SERVICE_TOKEN: "dev-sales-domain-service-token"
|
|
FINANCE_DOMAIN_SERVICE_TOKEN: "dev-finance-domain-service-token"
|
|
EMCP_AUDIT_LOG_PATH: "/tmp/emcp-audit.jsonl"
|
|
# Empty (default) unless set in `.env`/the shell environment before
|
|
# `docker compose --profile core --profile identity up`: the Gateway
|
|
# then runs in its "no OIDC provider configured" mode -- deny-by-default
|
|
# via handler-level checks instead of the SDK's RequireAuthMiddleware
|
|
# (see gateway/server.py's module docstring and `build_auth`). Set all
|
|
# three (see `.env.example`) to point at the `keycloak` service above.
|
|
OIDC_JWKS_URL: "${OIDC_JWKS_URL:-}"
|
|
OIDC_ISSUER: "${OIDC_ISSUER:-}"
|
|
OIDC_AUDIENCE: "${OIDC_AUDIENCE:-emcp-gateway}"
|
|
# EP-08-T04: empty (default) exports traces to stdout via
|
|
# ConsoleSpanExporter; set to "http://otel-collector:4318" (with
|
|
# `--profile observability`) to export real OTLP traces instead.
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: "${OTEL_EXPORTER_OTLP_ENDPOINT:-}"
|
|
ports:
|
|
- "8000:8000"
|
|
profiles: ["core"]
|
|
restart: on-failure:5
|
|
depends_on:
|
|
sales-domain:
|
|
condition: service_healthy
|
|
finance-domain:
|
|
condition: service_healthy
|
|
opa:
|
|
condition: service_started
|
|
|
|
otel-collector:
|
|
image: otel/opentelemetry-collector-contrib:0.114.0
|
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
|
volumes:
|
|
- ./deploy/otel/collector-config.yaml:/etc/otel-collector-config.yaml:ro
|
|
profiles: ["observability"]
|
|
# No host port published -- only the RI services (pointed at it via
|
|
# OTEL_EXPORTER_OTLP_ENDPOINT above) and `jaeger` need to reach it.
|
|
|
|
jaeger:
|
|
image: jaegertracing/all-in-one:1.62.0
|
|
environment:
|
|
COLLECTOR_OTLP_ENABLED: "true"
|
|
ports:
|
|
- "16686:16686" # Jaeger UI
|
|
profiles: ["observability"]
|