Tag Drift Between Dev, Stage and Prod
Argo Logistics deploys one service to dev, stage and prod from a single OCI DevOps project in Phoenix. A build pipeline builds phx.ocir.io/argolog/api and pushes two tags: latest and $BUILD_ID. All three deployment pipelines reference the OCIR artifact with version latest. An audit found prod running build 812 while stage ran build 815 for two days; a rollback during an incident re-deployed latest, which by then was a newer build that had never passed stage; and rebuilding the same commit produced a different image because the Dockerfile ran apt-get upgrade and pulled from an unpinned node:20 base. Compliance requires every prod deployment to be traceable to a commit SHA, no rebuild between environments, and rollback to a previously deployed version within 10 minutes. Images average 620 MB, builds take roughly 9 minutes, and each repository accumulates about 400 tags.
1. Explain what causes the drift and describe the artifact flow you would put in place.
Read the worked answer
Root cause: latest is a mutable pointer resolved at deploy time. Each environment's pipeline resolves whatever latest means at the moment its stage runs, so the artifact identity is decided by whoever pushed most recently, not by which build passed validation. Dev and stage drifted to 815 because they ran after the 815 build; prod stayed on 812 because it ran earlier. The rollback re-deployed a pointer, not a build.
Target flow:
- Tag every image with immutable identifiers only:
$COMMIT_SHA-$BUILD_ID, optionally plus a semver tag.latestmay exist as a convenience alias but no deployment stage may reference it. - Model the image as a versioned DevOps artifact: each Deliver Artifact stage produces a new artifact version tied to a specific build run, and each deployment stage takes the artifact version as a pipeline parameter (
${ARTIFACT_VERSION}) rather than a hard-coded value. - Promotion is a deployment run that carries the same artifact version from dev to stage to prod, with the approval stage in front of prod. Nothing is rebuilt on the way.
- Emit the commit SHA and build run OCID as build output variables and pass them into the deployment, so the prod record shows the exact source revision.
- Separate repositories per environment are optional - tags and versions are enough unless you need different IAM boundaries - but enforce an OCIR cleanup rule: 400 tags at 620 MB is roughly 250 GB per repository, so keep the last N builds plus everything currently deployed to prod.
2. Design the rollback procedure so it meets the 10-minute objective.
Read the worked answer
Rollback is a re-deploy of a known-good immutable version, not a rebuild. Because images are never rebuilt between environments, the previous prod version already exists in the registry.
Procedure:
- Run the prod deployment pipeline with
ARTIFACT_VERSIONset to the last version that passed prod - typically the one from the previous deployment run, which is recorded in pipeline history. - Pre-authorize the on-call group for rollback runs, or maintain a dedicated rollback pipeline with a single approval, so approval does not consume the 10-minute budget. The forward path can keep its full approval chain; the reverse path should not.
- Retain the last ~20 deployed versions with a registry cleanup exclusion, so a rollback target cannot have been garbage-collected.
- Keep the previous Kubernetes manifest as a versioned artifact tagged with the same
$BUILD_ID, otherwise a rollback restores the image but not the desired state.
The hidden dependency: rollback only completes in 10 minutes if the schema is backward compatible. If the current schema cannot serve the previous image, rollback becomes a data restore and the RTO is fiction - which is why schema changes must be additive and decoupled from the app deployment.
3. What do you do about reproducibility - is rebuilding acceptable?
Read the worked answer
Pinning matters, but it is not what caused this incident. Compliance asks for no rebuild between environments, which immutability satisfies even when the build is not bit-for-bit reproducible. Fix the priority order accordingly.
What to pin anyway:
- Base images by digest (
node@sha256:...), not by floating tag. - Remove
apt-get upgradefrom the Dockerfile; use a locked dependency set and an internal mirror with pinned package versions. - Record the build inputs (base digests, lockfile hash, commit SHA) as artifact metadata for forensics.
Why bit-reproducibility still matters: after a registry loss or a security investigation you may have to rebuild and prove the output matches what ran in production. That is a different requirement from promotion, and it is worth engineering for, but it should not block the drift fix.
Rule to enforce: if a rebuild is ever unavoidable, the result is a new artifact version and it must be re-promoted through dev and stage. Never push a fresh build straight to prod on the argument that the source commit is the same - that reintroduces exactly the drift being removed, and there is no evidence the new binary behaves identically to the one that passed stage.