CI/CD & Deployment

The Pipeline as Infrastructure

Git push → CI → test → build → artifact → deploy → production. The part people skip: the pipeline holds production credentials, so it is a production system with a production identity, and it is the most attractive target in the estate.

The question this answers

Infrastructure question

What actually happens between a commit and running code, and what does the machinery in the middle have permission to do?

Application requirement

A merged change must reach production in under an hour, repeatably, with tests run, an immutable artifact produced, and a record of who approved it — without a human ever holding a long-lived production credential.

What it provides

An automated, logged, reproducible path from a commit to running code, executed by a scoped machine identity rather than by whoever is around.

Application RequirementInfrastructure RequirementComputeNetworkStorageIdentityDeploymentScalingReliabilityObservabilitySecurityCostTrade-offs

Every stage produces an artifact and demands an identity

The pipeline is usually drawn as a row of boxes, which hides the two things that matter at each hop: what concrete thing now exists, and which identity was allowed to make it. A commit becomes a test result, becomes a build output, becomes an image with a content digest, becomes a running revision behind a load balancer. Each transition is performed by an identity with a specific permission, and the permissions get more dangerous as you move right.

The last hop is the one worth staring at. Something has to be able to change production. If that something is a CI runner, then the CI system can change production — and so can anything that can run a job on it, which includes any pull request from a fork if the configuration is careless, and any dependency that executes code at install time. A build system with deploy credentials is a production system whose access control is a YAML file in a repository.

The mitigations are structural. Separate the identity that builds from the identity that deploys; the build identity needs to push to a registry and nothing else. Use short-lived, workload-identity-federated credentials rather than long-lived keys stored as CI secrets. Scope the deploy identity to the specific resources it updates, not to the account. And gate the deploy stage on the protected branch, so a job triggered by an arbitrary pull request cannot reach it. See Roles vs Static Keys and Human vs Workload Identity.

Commit to traffic, with the artifact and the identity named at each hop.PROVIDER-NEUTRAL
  1. 1Git push

    A commit on a branch. The artifact is a source revision; the identity is a human with repository write access.

    Direct pushes to the protected branch, bypassing review — the gate the rest of the pipeline assumes exists.

  2. 2CI: test2–10 minutes — ILLUSTRATIVE

    The runner checks out the revision and runs unit and integration tests. Artifact: a test result. Identity: a runner with read-only repository scope.

    Flaky tests trained the team to re-run rather than investigate, and the gate stops meaning anything.

  3. 3Build

    A container image or package is produced from that exact revision. Artifact: an image with a content digest. Identity: a build role that can push to the registry and nothing else.

    A non-reproducible build pulling :latest base images, so the same commit produces different bytes on different days.

  4. 4Publish + sign

    The image is pushed to the registry, and a signature and provenance attestation are recorded against the digest.

    Mutable tags. v1.4 pointing at a different digest tomorrow makes every later guarantee meaningless.

  5. 5Deploy

    The deploy identity updates the workload to reference the new digest and waits for the rollout. Identity: a scoped, short-lived production role.

    The dangerous hop. This identity can change production, so anything that can trigger this job can change production.

  6. 6Health check and traffic shift

    Readiness probes pass, the load balancer adds the new instances, old ones drain.

    A readiness probe that returns healthy before dependencies are reachable, so traffic arrives at an instance that cannot serve it. See Liveness vs Readiness.

  7. 7Observe

    Error rate, latency and saturation are compared to the pre-deploy baseline; rollback is one action away.

    Nobody watching. A deploy that is not observed for its first ten minutes is a deploy whose rollback happens by customer report.

The pipeline identity is the crown jewel

Ask "what is the most privileged identity in this system?" and the honest answer in most organisations is the deploy role, not any human. It can replace the running code of every service. A supply-chain attacker does not need to break your application; they need one dependency that runs at build time on a runner that holds that role.

This is what makes build-time execution such a sharp edge. A package install script, a test helper, a linter plugin — all of them execute with whatever the runner has. If the runner has production deploy rights, then so does every transitive dependency in the build. Splitting build and deploy into separate jobs with separate identities is not bureaucracy; it is the control that makes that sentence false.

The rest is conventional least privilege applied to a place people forget to apply it: no wildcard resources, no long-lived static keys in CI secret stores, an approval gate on the production deploy job, and audit logging that ties every production change to a pipeline run and through it to a commit and a reviewer.

The deploy identity, scoped. Note what is deliberately absent.
CI deploy role: deploy-checkout-prodcileast privilege
on The checkout service's compute group, its load balancer target group, and its image repository (read)
Allowed
  • compute:UpdateServiceRevision on service/checkout
  • compute:DescribeService on service/checkout
  • registry:PullImage on repo/checkout
  • lb:DescribeTargetHealth on tg/checkout
Actually needed
  • compute:UpdateServiceRevision on service/checkout
  • compute:DescribeService on service/checkout
  • registry:PullImage on repo/checkout
Explicitly denied
  • iam:* — a deploy role that can edit policies can grant itself anything
  • compute:UpdateServiceRevision on any other service
  • db:* — deployment does not touch the data tier
  • state:* — the IaC state backend is a different job with a different role

Blast radius: Compromise replaces the running code of one service with an attacker image — serious, and contained. The version people usually have instead, a single admin-scoped CI role shared by every pipeline, turns the same compromise into full control of the estate, obtained through any build-time dependency in any repository.

Where pipelines actually break

Pipeline failures cluster into three groups and they want different responses. Correctness failures — a test caught something — are the pipeline working. Infrastructure failures — runner out of disk, registry throttling, an expired credential — are yours to operate, and they present as flakiness that erodes trust in the first group. Path failures are the interesting ones: the deploy succeeded, the pipeline is green, and production is broken, because the pipeline verified the deploy mechanism and nothing verified the workload.

The runner fleet is real infrastructure with real capacity limits. A pipeline whose queue depth grows on Monday morning is a capacity problem, and the symptom — "CI is slow" — gets treated as a tooling complaint rather than as saturation. Runners also need egress to pull dependencies, which puts them behind the same NAT and the same rate limits as everything else. A build failing with connection timeouts is often a network story, not a build story.

The single most valuable pipeline metric is not build duration. It is the time from a merged commit to that change serving traffic, tracked as a distribution — because the long tail is where the manual steps are hiding.

ClassSymptomWrong responseRight response
Test failureRed build on a real defectRe-run until greenFix the defect; this is the pipeline working.
Flaky testPasses on re-runRe-run; add a retryQuarantine and fix. Flakiness spends the credibility the gate runs on.
Runner saturation"CI is slow" every MondayAsk people to be patientTreat it as capacity: measure queue depth, scale the fleet, cache dependencies.
Egress failureIntermittent dependency download timeoutsAdd retries around the installLook at the NAT and the registry rate limit. See NAT Gateway.
Expired credentialDeploy fails at the last stageReissue the static keyMove to short-lived federated credentials so there is nothing to expire.
Path failureGreen pipeline, broken productionTrust the greenGate on workload health after the deploy, not on the deploy call returning.
Failure class, symptom, and the response that actually helps.

Key points

  • Each pipeline stage produces a concrete artifact and is performed by an identity; the identities get more dangerous toward the right.
  • The deploy identity can replace production code, which makes CI a production system and a high-value target.
  • Split build and deploy into separate jobs with separate identities, so build-time dependency execution cannot reach production.
  • Prefer short-lived federated credentials over long-lived keys stored as CI secrets — there is then nothing to steal or expire.
  • A green pipeline proves the deploy mechanism worked, not that the workload is serving. Gate on workload health separately.

The loop, answered

Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.

How it works
  • A push or merge triggers a job on a runner, which checks out the exact revision.
  • Tests run against that revision; failure stops the pipeline before an artifact exists.
  • The build produces a container image or package whose content digest identifies it uniquely.
  • The build identity pushes the artifact to a registry; a signature and provenance attestation are recorded against the digest.
  • The deploy job assumes a scoped, short-lived production role and updates the workload to reference that digest.
  • The platform performs the rollout — see Four Ways to Replace Running Code — and health checks decide whether traffic shifts.
What you still own
  • You own the runner fleet: capacity, images, disk, dependency caches and its network path.
  • You own the pipeline identities and their scoping, which is the security posture of your entire delivery path.
  • You own the branch protections and approval gates that decide which jobs may reach production.
  • You own pipeline observability: queue depth, duration, failure classification, and lead time from merge to traffic.
  • You own the break-glass path for deploying when the pipeline itself is broken — and the reconciliation afterwards.
How it fails
  • A shared admin-scoped CI role: one compromised build-time dependency in any repository yields the whole estate.
  • A long-lived deploy key in a CI secret store, readable by any job in the project, valid until someone rotates it.
  • A pull request from a fork triggering a workflow that has access to deploy secrets.
  • Runner saturation turning a ten-minute pipeline into ninety minutes, so people batch changes — which makes each deploy riskier.
  • A non-reproducible build: the same commit produces different bytes because base images and dependency ranges are unpinned.
  • Deploy succeeds, workload is unhealthy, and nothing rolls back because the pipeline's definition of success was an API call returning 200.
How it scales
  • Runner concurrency is the first thing to run out, and it presents as a queue rather than as an error.
  • Registry bandwidth and rate limits bind next, particularly when many services pull large base images at the same time. See Why Image Size Is an Infrastructure Problem.
  • Pipeline count scales with services; the shared configuration between them becomes an internal platform whether or not anyone decides to build one.
  • Lead time from merge to production is the metric that reveals the real bottleneck, and it is usually a manual approval rather than compute.
Security
  • The deploy identity is the most privileged identity in the system. Scope it per service, keep it short-lived, and gate it on the protected branch.
  • Build-time code execution is an unavoidable trust boundary: every dependency runs with the runner's permissions.
  • Sign artifacts and verify signatures at deploy time so the thing deployed is provably the thing built. See The Infrastructure Supply Chain.
  • Pipeline logs regularly leak secrets through debug output; mask at the log sink, not only at the source.
  • Audit-log every production deploy back to a pipeline run, a commit and a reviewer. This is usually the only complete change record you have.
Cost shape
  • Runner compute is usage-shaped and mostly invisible: it is charged per minute across many small jobs and never appears as one large line item.
  • The classic surprises are cache misses causing full dependency downloads on every build, and egress charges on image pulls.
  • Self-hosted runners trade a usage-shaped bill for a fixed one plus operational work, which is worth it only at sustained high volume.
  • The dominant cost is human: a slow pipeline taxes every engineer every day, and it does not appear on any invoice.
What to watch
  • Lead time from merge to serving traffic, as a distribution — the tail is where the manual steps live.
  • Queue depth and runner utilisation, treated as a saturation signal.
  • Failure rate split by class: test, flake, infrastructure, deploy. The mix tells you what to fix.
  • Deploy frequency and change failure rate, which together say more about delivery health than any duration metric.
  • The signal that lies: the green checkmark. It reports that the deploy call succeeded. Whether the workload is healthy is a different question with a different probe.
Simpler alternatives
  • For a single service and a small team, a deploy script run by an engineer with a short-lived credential is honest and sufficient. The pipeline is worth building when the deploy is frequent or the reviewers are many.
  • A platform-as-a-service that builds and deploys on push, when you want the path without owning the machinery — you trade control and portability for not running runners.
  • A pull-based deployment agent (GitOps) that watches a repository and reconciles, so no external system needs production write credentials at all. This removes the crown-jewel identity entirely, at the cost of an agent to run and a less direct feedback loop.
  • Fewer stages. A pipeline with eleven stages and four approval gates is often a process problem being solved with YAML.
What adopting this costs
  • Buys a repeatable, logged, reviewable path to production; costs a production-privileged system that must be secured like production.
  • Buys speed; costs the temptation to trust green as a proxy for healthy.
  • Split identities buy containment; cost more roles, more federation setup, and more places for a permission to be missing at the worst moment.

What people believe, and what is true

Claim

CI is developer tooling, not production infrastructure.

Reality

It holds credentials that can replace production code. It is production infrastructure with a lower review bar, which is what makes it attractive.

Claim

Secrets in the CI secret store are safe.

Reality

They are readable by any job that can run in that scope, including one added by a pull request if the configuration allows it. Short-lived federated credentials remove the stored secret entirely.

Claim

A green pipeline means the deploy worked.

Reality

It means the deploy API call succeeded. Whether the new revision is healthy and serving is answered by health checks and error rate, not by the pipeline.

Apply it