ADR-005: Modular Deployment Profiles

On this page

Status

Accepted

Context

Canopy is designed to eventually serve multiple jurisdictions beyond Georgia DHS. A state may wish to deploy only SNAP, or only SNAP and TANF, without standing up Medicaid/CHIP, CAPS, or WIC infrastructure. A tribal nation operating a tribal TANF program may need only TANF and child care. An agency undergoing phased modernization may need to introduce programs incrementally alongside a legacy system.

The existing service topology includes 19 independent services plus 2 BFF services. All can be deployed on a single machine or distributed across a cluster. However, there is currently no formal declaration of:

  1. Which services are required for a given program to function

  2. Which services are optional and what happens when they are absent

  3. How Docker Compose or Kubernetes manifests should be constructed for partial deployments

  4. How dependent services should behave when an optional peer is unreachable

This creates accidental coupling: a jurisdiction deploying SNAP should not need to explain why it has no canopy-medicaid container.

The OpenStack project was identified by the project owner as the appropriate architectural model. OpenStack’s "big tent" governance — independent service projects, a shared identity layer, and optional composition — is the reference point for this decision.

Decision

1. Deployment profiles

Every program has a defined minimum required service set: the smallest set of services needed for that program to accept applications, determine eligibility, issue benefits, and meet federal certification requirements.

Deployment profile Required services Notes

snap-only

canopy-auth (Keycloak), canopy-persons, canopy-applications, canopy-rules, canopy-eligibility, canopy-snap, canopy-verification, canopy-notices, canopy-appeals, canopy-security, canopy-enrollment, canopy-renewals, canopy-reporting, canopy-web

canopy-portal (applicant-facing) is optional for UAT; add for go-live

tanf-only

Same as snap-only but replace canopy-snap with canopy-tanf

TANF has no EBT issuance — canopy-enrollment is lighter

snap-tanf

Union of snap-only and tanf-only; canopy-enrollment handles both

Categorical eligibility cross-reference requires both services reachable

medicaid-chip

canopy-auth, canopy-persons, canopy-applications, canopy-rules, canopy-eligibility, canopy-medicaid, canopy-verification, canopy-notices, canopy-appeals, canopy-security, canopy-renewals, canopy-reporting, canopy-exchange, canopy-web

canopy-exchange required for FFE account transfers (ACA §1413)

caps-only

canopy-auth, canopy-persons, canopy-applications, canopy-rules, canopy-caps, canopy-eligibility, canopy-notices, canopy-appeals, canopy-security, canopy-enrollment, canopy-renewals, canopy-reporting, canopy-web

No IEVS or FTI; simpler compliance posture

wic-only

canopy-auth, canopy-persons, canopy-applications, canopy-rules, canopy-wic, canopy-eligibility, canopy-notices, canopy-appeals, canopy-security, canopy-enrollment, canopy-reporting, canopy-web

Adjunctive eligibility from SNAP/Medicaid is optional (can fall back to income test)

full

All 21 services

Georgia DHS production target

2. Docker Compose profiles

Docker Compose v2 profiles are used to implement deployment selections. Every service has a profiles: key in docker-compose.yml. The profiles correspond to the table above.

Example usage:
# SNAP-only deployment
COMPOSE_PROFILES=snap-only docker compose up -d

# Full deployment (default in devstack)
COMPOSE_PROFILES=full docker compose up -d

# SNAP + TANF
COMPOSE_PROFILES=snap-only,tanf-only docker compose up -d

Services that are in every profile (universal dependencies) declare all profiles:

services:
  keycloak:
    profiles: [snap-only, tanf-only, snap-tanf, medicaid-chip, caps-only, wic-only, full]
  canopy-persons:
    profiles: [snap-only, tanf-only, snap-tanf, medicaid-chip, caps-only, wic-only, full]

Program-specific services declare only their relevant profiles:

  canopy-snap:
    profiles: [snap-only, snap-tanf, full]
  canopy-medicaid:
    profiles: [medicaid-chip, full]

3. Graceful degradation for optional services

Infrastructure services that are optional in some profiles (canopy-exchange, canopy-portal) must not crash other services when absent. Required services must not have hard startup dependencies on optional services.

Rules for inter-service calls:

  • Required-to-required calls (e.g., canopy-eligibility → canopy-snap): Circuit breaker with retry. If the required peer is unreachable, fail fast with a 503 and log an alert. This is a misconfigured deployment.

  • Required-to-optional calls (e.g., canopy-enrollment → canopy-exchange for FFE notification): The calling service uses a capability flag. If the optional service is not configured (empty CANOPY_EXCHANGE_URL env var), the call is silently skipped and a tracing::debug!() message is emitted. No error is returned.

  • Optional-to-required calls (e.g., canopy-portal calling canopy-persons): The optional service simply is not deployed; no graceful degradation needed.

Capability flags are set via environment variables:

# In snap-only deployments, these are unset or empty:
CANOPY_EXCHANGE_URL=
CANOPY_PORTAL_URL=
CANOPY_TANF_URL=
CANOPY_MEDICAID_URL=

canopy-api’s `bootstrap() function reads these flags and excludes unconfigured service clients from the AppState. Program service router slots that require an unconfigured peer return 501 Not Implemented with a Problem Details body explaining the missing service.

4. Health and metrics reflect profile

The /healthz endpoint of each service reports only the dependencies that are configured for the active profile. A SNAP-only canopy-eligibility instance does not report canopy-medicaid as a health dependency.

5. Kubernetes and production deployments

Docker Compose profiles are for development and single-node deployments. For Kubernetes, each deployment profile corresponds to a Helm values file:

helm install canopy ./charts/canopy \
  --values charts/canopy/profiles/snap-only.yaml

Helm chart design is out of scope for the current phase but must be compatible with this profile model. Profile names are the authoritative mapping between Docker Compose and Kubernetes deployments.

6. cargo xtask profile support

cargo xtask dev start --profile snap-only
cargo xtask dev start --profile full        # default
cargo xtask dev start --profile snap-tanf

The dev start command passes the profile to docker compose --profile.

Rationale

Why Docker Compose profiles instead of separate compose files?

A single docker-compose.yml with profiles is easier to maintain than multiple compose files that diverge over time. Profile definitions are co-located with service definitions, so adding a new service to a profile is one line. Separate compose files would require constant synchronization.

Why environment-variable capability flags instead of build-time features?

Rust feature flags would require different binary builds per profile — expensive and error-prone. Environment variables allow the same binary to adapt at runtime. This matches the 12-factor app model already used throughout Canopy.

Why define minimum required sets at the ADR level?

Without a formal declaration, every team member makes a different judgment about what is "really required." This leads to integration tests that pass in one profile but fail in another, and UAT environments that differ from production. Formalizing the minimum required sets here makes profile testing a first-class concern.

Consequences

Benefits

  • Any jurisdiction can deploy any program subset on day one

  • SNAP UAT does not require standing up Medicaid or TANF infrastructure

  • Federal auditors reviewing a SNAP-only deployment see only SNAP-relevant services

  • Incremental adoption path: agencies start with one program, add others without rebuilding

  • Smaller attack surface in single-program deployments

Costs and risks

  • Graceful degradation code paths must be tested — they are easy to introduce and hard to notice when broken

  • docker-compose.yml becomes larger (all services + all profile tags)

  • Services must be careful not to assume the presence of optional peers — this requires discipline in pull requests

  • Capability flag logic adds conditional paths to AppState and bootstrap that must be kept current

What does not change

  • ADR-001 (program service isolation) is unchanged and complementary

  • Database isolation is unchanged — profiles change which containers are running, not the isolation model

  • Security model is unchanged — each service still requires auth; no services are "open" in any profile

Edit this page · default