Skip to content

Composition & Wiring

A component’s deploy is a composition over capabilities, expressed in the same grammar as Ops: phases run in order, steps in a phase run sequentially unless parallel, gate pauses for a signal, and onFailure compensates. Steps wire output to input so lint can check the graph before anything runs.

deploy: [
phase("Publish", [publishImage({ from: "archive", to: "$env.registry" })]), // → @Publish.digest
phase("Apply", [cfnDeploy({ template: "archive:t.json", imageRef: "@Publish.digest" })]),
phase("Verify", [waitSteadyState({ service: "api" })]),
]

Fan-out is composition, not orchestrator knowledge

Section titled “Fan-out is composition, not orchestrator knowledge”

A single component can expand to many units — one CloudFormation stack per instance in a cluster, one host in a fleet. The fan-out is authored in the composition, never known by the driver.

A fan-out unit is itself a small composition. One EC2 instance is provision (cfn-deploy) + code deploy (code-deploy) + health. A component that fans out over N instances composes N such mini-compositions, sequenced seed-first then rolling with a health gate between:

deploy: [
phase("Seed", [cfnDeploy({ stack: "neo4j-az0-0", template: "archive:neo4j.template.json" }), codeDeploy({ instance: 0, revision: "$env.appBundle" }), waitClusterHealthy({ size: 1 })]),
...followers.map((n) => // Array.map over instance configs
phase(`Node ${n}`, [cfnDeploy({ stack: `neo4j-${n}`, template: "archive:neo4j.template.json" }), codeDeploy({ instance: n, revision: "$env.appBundle" }), waitClusterHealthy({ quorum: true })]),
),
]
One component expanding into N per-instance mini-compositions — provision, code deploy, health — sequenced seed-first then rolling, with a health gate between each node. One component expanding into N per-instance mini-compositions — provision, code deploy, health — sequenced seed-first then rolling, with a health gate between each node.
Fan-out: one component composes N per-instance mini-compositions. Single-host is the N=1 case; a fleet is the same shape at N>1.

The driver never learns that a Neo4j cluster seeds first. If a composition ever needs an if component.name === … in the driver, the contract or a capability is missing expressiveness — fix it there, never in the driver.

When one component’s stack exports a value another needs, the consumer imports it. The graph resolves it, replacing the describe-stacks | jq glue a pipeline would otherwise carry.

export const searchService: Component = {
name: "search-service",
dependsOn: ["shared-alb"],
deploy: [
phase("Apply", [cfnDeploy({ template: "archive:search.template.json", inputs: { ...sharedAlb.outputs } })]), // listener ARN, cluster ARN, subnets, …
],
};

chant graph --stacks orders shared-alb before the service and wires its outputs. See Multi-Stack Projects for the underlying stackOutput mechanism.

examples/adopt-alb-services is the runnable version of this exact pattern: a bespoke two-service ALB pipeline — describe-stacks | jq glue and all, kept as before/.gitlab-ci.yml — adopted as three build.json components a single driver runs.

Components also wire by artifact, not just by stack output. A producer publishes an artifact; a consumer references it as @<component>.publish.uri (or .digest / .key). dependsOn orders the producer first; the graph resolves the reference into the consumer’s step.

The example illustrates the wiring — its jvm-build and publish-artifact verbs are not yet implemented (the cross-component reference resolution is what ships today):

// producer — a JAR published to S3, publish-only deploy
export const jarLib: Component = {
name: "jar-lib",
build: jvmBuild({ tool: "maven", path: ".", into: "archive" }),
deploy: [phase("Publish", [publishArtifact({ from: "archive", to: "$env.s3" })])], // → @jar-lib.publish.uri
};
// consumer — an EMR job that reads the JAR
export const emrJob: Component = {
name: "emr-job",
dependsOn: ["jar-lib"],
deploy: [
phase("Submit", [emrStartJobRun({ jar: "@jar-lib.publish.uri", args: [/* … */] })]),
phase("Verify", [waitJob({ runId: "@Submit.runId" })]),
],
};
  • examples/adopt-alb-services — a bespoke multi-service ALB pipeline adopted onto the component model, before and after, runnable end to end.
  • Build Archive — where a producer’s artifact lives before it is published.
  • Orchestration — how the graph order becomes a run.