Your Deployment Pipeline Has a Boundary Problem
If you’ve ever inherited a deploy pipeline you know the parts that quietly worry you: the awk line pulling a database endpoint out of CLI…
Your Deployment Pipeline Has a Boundary Problem

Production infrastructure, photographed honestly.
If you’ve ever inherited a deploy pipeline you know the parts that quietly worry you: the awk line pulling a database endpoint out of CLI output, the regex matching a column in some tool’s status table, the script that strips ANSI codes before piping into the next job. Every one of those is a boundary between stages, and every one is what breaks first when an upstream tool ships a release.
Azure CLI’s deployment command broke for users when two new print statements got added to stdout. Downstream pipelines parsing the JSON output failed because diagnostic text — “Bicep CLI is already installed at…” — was now interleaved with the response. AWS CLI broke kubectl entirely when users had output = text set in their config; kubectl couldn’t parse the credentials it received and EKS access fell over with a JSON parse error.
Same pattern in each case: one tool’s output format shifts in a way that’s invisible to humans but catastrophic to the robots downstream trying to parse it.
The Azure team didn’t think two diagnostic lines were a breaking change. They were right, for humans. They were wrong for the thousands of pipelines silently treating their stdout as a contract.
This is Hyrum’s Law in shell form. With enough downstream consumers, every observable byte of your stdout becomes part of your API — whether you intended it or not. The Azure team didn’t think two diagnostic lines were a breaking change. They were right, for humans. They were wrong for the thousands of pipelines silently treating their stdout as a contract.
It’s also the inverse of Postel’s Law. Be conservative in what you send, liberal in what you accept built the web, but it assumed a human was on the receiving end. Machines need the opposite contract: a strict, versioned schema on both ends, with no room for “helpful” diagnostic noise. CLIs were designed in the human-output era.
This isn’t just a CI/CD problem. It’s the same pattern that cracks bridges at the expansion joint, that makes API versioning the hardest part of building a platform, that turns every protocol negotiation into a security surface. Wherever two systems meet, the meeting point is the fragile part. Pipelines are just a place where the fragility is cheap to demonstrate and expensive to ignore.
What declarative tools quietly assume
The OpenGitOps principles are short: declarative state in Git, continuous reconciliation, software agents enforcing convergence. Argo CD’s documentation builds on this with the pull loop: an agent inside the cluster that watches Git and reconciles continuously, rather than CI/CD pushing changes outward.
What’s missing from those definitions is anything about how a multi-stage pipeline carries data forward between jobs. The original framing was “get declared state into Kubernetes,” and a single apply was assumed to be sufficient.
Real CI/CD pipelines aren’t that simple. They have provisioning, capture, deploy, verify. Each stage usually needs at least one value the previous stage produced — a database endpoint, an ARN, a generated DNS name. Get that handoff wrong and the whole pipeline fails, usually loudly.
The pattern that holds
The pattern that actually survives is structured: emit JSON, parse with jq, write to the CI/CD’s native stage-output mechanism. A --output-consumer=machine flag forces structured JSON instead of the human-readable default — telling the tool explicitly that no human will ever read this output, so don't be helpful about it.
DB_HOST=$(formae inventory resources \
--query='label:pg-server' \
--output-consumer=machine \
| jq -r '.Resources[0].ReadOnlyProperties.fullyQualifiedDomainName')
echo "db_host=$DB_HOST" >> $GITHUB_OUTPUT
Call it the stdout contract: the moment a second program parses your output, that output is an API.
GitHub Actions reads ${{ needs.provision.outputs.db_host }} in the next job. GitLab CI does the same with dotenv artifacts. Argo Workflows uses output parameters. The CI’s native primitive does the carrying; what matters is that the data being carried came out of the previous stage as structured, schema-stable JSON.
Call it the stdout contract: the moment a second program parses your output, that output is an API, and you owe it the same versioning discipline you’d give any other interface. Few CLIs make this the default, which is why Azure, AWS, and kubectl have all shipped variations of the same failure.
The same pattern, slightly extended
The same pattern works for resources your pipeline doesn’t own — same flag, same parse, different filter.
ORDERS_DB=$(formae inventory resources \
--query='managed:false label:legacy-orders-db' \
--output-consumer=machine \
| jq -r '.Resources[0].ReadOnlyProperties.endpoint.address')
Same flag, same parse, different filter. No coupling to the writing pipeline, just a structured read.
That turns a CI/CD pipeline into a consumer of typed state instead of a script that scrapes other tools’ output.
Why this pattern matters more than it looks
Stable schemas between stages is the kind of win that doesn’t fit on a slide. It doesn’t sell anyone on a tool, and it’s hard to demo. But the cost of getting it wrong is going up. AI is pushing more code through the same fragile bridges — Harness found 92% of developers think it’s widening the blast radius from bad deployments. And the bridges themselves are more expensive when they snap: EMA now puts unplanned downtime at $23,750 a minute for large enterprises. A typed boundary between stages is the version that holds. jq is load-bearing infrastructure for the entire industry, and we should probably admit that out loud. The unglamorous handoff is most of the work.
Every engineered system fails first at its boundaries. Pipelines are no exception — the boundary is just a shell pipe, and the failure mode is a parser choking on someone else’s diagnostic noise. A typed handoff is the version that holds. It’s unglamorous. It’s also most of the work.
I work on formae, an open-source infrastructure tool that emits structured JSON natively from apply, inventory, and destroy. The pattern above uses that output directly. Code is on GitHub and the docs can be found here.
메타데이터
- post_id
- ae94dde7ebf3
- slug
- your-deployment-pipeline-has-a-boundary-problem-ae94dde7ebf3
- url
- https://blog.platform.engineering/your-deployment-pipeline-has-a-boundary-problem-ae94dde7ebf3
- canonical_url
- https://blog.platform.engineering/your-deployment-pipeline-has-a-boundary-problem-ae94dde7ebf3
- author_url
- https://medium.com/@nicholas.browdues
- status
- ok
- fetched_at
- 2026-06-15 20:49:13