ADR 0010: One app, many targets — deploy specs are the master
- Status: accepted
- Date: 2026-09-28
- Supersedes: nothing (complements ADR 0007, which covers the mosaic-spec
deployblock; this ADR covers the tesseradeploy { name … }specs)
Context
A product is not deployed to one place. The same mosaic app ships to our
customers' own Kubernetes (bare k8s), to managed clusters on the clouds
their workloads already live on (aws-eks, aws-ecs, gcp-gke,
azure-aks, alibaba-ack), to plain docker and local for dev. And the
datastores are the same story in miniature: sometimes a managed service
(RDS, Cloud SQL, ApsaraDB RDS, Azure Database for PostgreSQL), sometimes a
self-hosted engine in the cluster (postgres, mysql, redis, clickhouse,
qdrant, weaviate, opensearch, milvus).
The old shape — one deploy block, one target, terraform that also rendered
the k8s workload inline — could not express "same app, prod on GCP with
Cloud SQL, staging on our own k3s with an in-cluster postgres".
Decision
An app declares any number of deployment specs:
deploys:
- name: prod-gcp # -> deploy/prod-gcp/
target: gcp-gke # local | docker | k8s | aws-eks | aws-ecs |
region: europe-west1 # gcp-gke | azure-aks | alibaba-ack (closed vocab)
replicas: 2
db:
name: shop
engine: postgres # postgres|mysql|mariadb|redis|dynamodb|clickhouse
managed: true # default: cloud targets managed, own-infra not
vector:
name: shop-vectors
engine: qdrant # opensearch|qdrant|weaviate|milvus|pinecone
- name: staging-k8s
target: k8s # own infra: no terraform, chart only
db: { name: shop, engine: postgres } # in-cluster via the chart
Rules, all fail-closed in the plan:
-
nameis required and unique (it is thedeploy/<name>/directory). -
targetis a closed vocabulary; unknown targets are an error. -
Each spec resolves a
cloud(aws|gcp|azure|alibaba|none) and a per-cloud default region. -
Stores are closed vocabularies too.
manageddefaults totrueon cloud targets andfalseon own-infra targets.dynamodbandpineconeexist only as managed services; self-hosted stores require a k8s target. -
A managed store must have a stable first-party Terraform resource with a connection URL on that cloud (
deploy-managed-unavailable, fail-closed) — otherwise the chart would reference a DSN secret Terraform cannot create:cloud managed db engines managed vector engines aws postgres, mysql, mariadb, redis, clickhouse, dynamodb¹ opensearch gcp postgres, mysql, mariadb, redis, dynamodb¹ — (use managed: false)azure postgres, mysql, redis all (map to AI Search) alibaba postgres, mysql, mariadb, redis, clickhouse, dynamodb¹ — (use managed: false)¹ managed document store, no DSN: the app is wired through the provider SDK, and the chart installs nothing for it (
enabled: falsein values).The matrix lives in the plan (
managed_store_available) and must stay in sync withtf_db_url/tf_vector_urlin mosaic-render.
Each spec derives its artifacts; the app, the DSL, and the codegen are identical across specs:
local→deploy/<name>/run.sh;docker→deploy/<name>/Dockerfile.- cloud targets →
deploy/<name>/infra/Terraform: the managed cluster (EKS/GKE/AKS/ACK module), the image registry, the managed store resources (RDS/DynamoDB/ElastiCache/OpenSearch, Cloud SQL/Memorystore, Azure PostgreSQL/MySQL/Redis/AI Search, ApsaraDB RDS/Redis/ClickHouse), and — for k8s targets — the namespace + DSN secrets. Terraform never renders the workload. - k8s targets →
deploy/<name>/Dockerfile+deploy/<name>/helm/chart: the image is the bare binary (the chart passesserve --bindas container args); the chart is the app Deployment/Service plus the stores.mode: clusterrenders the store in-cluster (postgres/mysql/ mariadb/redis/clickhouse statefulsets or deployments; qdrant/weaviate/ opensearch/milvus workloads) and materializes the DSN into a chart-owned secret;mode: externalreads the DSN secret Terraform created (<app>-db/<app>-vector) or that the operator provides.
The app reads exactly two store env vars in every mode: MOSAIC_DB_URL and
MOSAIC_VECTOR_URL.
Consequences
- "Deploy to another cloud" is a spec, not a fork: same image, same DSL,
same generated app; only
deploy/<name>/differs. - The chart is the single k8s footprint for any cluster, managed or not —
what
ee-helmdoes for the enterprise engine, generalized over engines. - Cloud testing can be per-cloud: render the spec,
terraform apply(or reuse the managed resources),helm installwith the chart values, and the app is exercised against the real managed services. - Terraform output is codegen, reviewed like any generated file; per-cloud drift (which engines exist where) is a renderer concern, invisible to the DSL.
aws-ecshas no cluster: terraform owns the whole footprint (Fargate service + ALB), store DSNs wired into the task definition.- In-cluster store passwords live in the chart values (test/dev-grade);
production footprints should prefer
managed: trueormode: externalso no credential is in the values file.
Proof
cargo test -p mosaic-core→tessera_plan::deploy_spec_tests::*(defaults, managed inference, cloud-only engines, managed-availability guard, name rules, alibaba/azure targets).cargo test -p mosaic-render→app::terraform_generation::*(per-spec infra + helm for aws-eks and gcp-gke).examples/full-stack/tessera.yamlcarries three specs (prod-aws, prod-gcp, staging-k8s);cargo run -p mosaic-cli -- conformancepins the rendereddeploy/<name>/trees in the golden.scripts/e2e/— the k8s e2e harness + peer fixtures;.github/workflows/e2e-*.ymlrun it on a GH-hosted k3d cluster, on the self-hosted on-prem runner, and (secrets-gated) against the clouds.