Git, CI/CD & GitOpsXLIV · ArtifactsImagesAndManifests
Images and manifests — OCI image artifacts, manifest artifacts, the production pattern
What you'll learn
- Distinguish an OCI image artifact (pushed to a registry) from a manifest artifact (uploaded with upload-artifact)
- Identify the image digest as the audit anchor for the deployed binary
- Upload a rendered Kubernetes manifest as an artifact and apply it from the artifact in the deploy job
- Recognise the production pattern: image in registry, manifest in artifact store, deploy job binds them
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A container pipeline produces two distinct artifacts: the image (a binary that runs in the cluster) and the manifest (a YAML bundle that tells the cluster how to run the image). The image is an OCI artifact that lives in a registry; the manifest is a YAML bundle that lives in the artifact store. The deploy job binds them: it downloads the manifest from the artifact store, reads the image digest from the manifest, and applies the manifest to the cluster. The image digest is the audit anchor; the manifest is the contract.
OCI images as artifacts
An OCI image is an artifact, but it does not live in the
artifact store. It lives in an OCI registry (Docker Hub, GitHub
Container Registry, Amazon ECR, Google Artifact Registry, Azure
Container Registry). The registry is a content-addressed store
keyed by the image digest (sha256:...). The digest is the
audit anchor: pinning the image by digest in the manifest
guarantees that the cluster runs the exact bytes that the build
produced.
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/acme/api:${ github.sha }
- name: Get image digest
id: digest
run: |
DIGEST=$(docker inspect ghcr.io/acme/api:${ github.sha } --format='{index .RepoDigests 0}' | cut -d'@' -f2)
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
The image is pushed by digest; the digest is exposed as a
step output. The digest is the immutable reference: the tag
(ghcr.io/acme/api:abc123) is mutable, the digest
(ghcr.io/acme/api@sha256:deadbeef...) is not.
Manifests as artifacts
The manifest is a YAML bundle that the deploy job will apply to the cluster. The manifest references the image by digest; the deploy job reads the manifest, sees the digest, and applies the manifest. The artifact is the bundle; the digest is the binding:
- name: Render manifest
run: |
envsubst < k8s/deployment.yaml.template > deployment.yaml
- name: Upload manifest
uses: actions/upload-artifact@v4
with:
name: k8s-manifest
path: deployment.yaml
retention-days: 90
The rendered deployment.yaml references the image by digest:
image: ghcr.io/acme/api@sha256:deadbeef.... The upload
preserves the rendered bytes; the deploy job downloads them.
The deploy job downloads the manifest
The deploy job downloads the manifest from the artifact store and applies it to the cluster. The artifact is the contract: the deploy job applies the bytes the build produced, not a re-rendered template:
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: Download manifest
uses: actions/download-artifact@v4
with:
name: k8s-manifest
path: ./manifest
- name: Verify image digest
run: |
grep -q "ghcr.io/acme/api@sha256:" ./manifest/deployment.yaml
- name: Apply manifest
run: kubectl apply -f ./manifest/deployment.yaml
The verify step asserts the manifest references the image by digest; the apply step ships the bytes the build produced. The audit trail is: the PR was reviewed against the manifest; the deploy applied the manifest; the image digest in the manifest is the audit anchor.
flowchart LR
A["build-push-action"] --> B["Image in registry\nghcr.io/acme/api@sha256:abc"]
A --> C["Rendered manifest"]
C --> D["upload-artifact k8s-manifest"]
D --> E["Artifact store"]
E --> F["Deploy job downloads manifest"]
F --> G["kubectl apply -f manifest"]
G --> H["Cluster pulls image by digest"]
B --> H
F --> I["PR reviewer reads manifest in PR UI"]
The production pattern
The production container pipeline is:
- Build and push the image by digest. The digest is the audit anchor.
- Render the manifest with the image digest. The manifest is the contract between the image and the cluster.
- Upload the manifest as an artifact. The artifact is the durable record.
- Download the manifest in the deploy job. The deploy applies the review-grade bytes.
- Verify the image digest in the manifest. The verification is the audit invariant.
- Apply the manifest. The cluster pulls the image by digest.
The image and the manifest are two artifacts of the same build, bound by the digest. The artifact store holds the manifest; the registry holds the image. The deploy job reads the manifest, finds the digest, and applies.
Production discipline
- Pin the image by digest in the manifest. The digest is the audit anchor; the tag is not.
- Upload the rendered manifest, not the template. The artifact is the rendered YAML.
- Download the manifest in the deploy job. Never re- render the template.
- Verify the digest in the deploy step. The verification is the audit invariant.
- Set retention on the manifest to the audit window. A typical value is 90 days.
Cross-course references
- Git, CI/CD & GitOps — Part XLIV-01 (What an artifact is) covers the artifact model.
- Git, CI/CD & GitOps — Part XLIV-03 (Terraform plans as artifacts) applies the same pattern to plan files.
- Kubernetes for Production Sysadmins — Parts XIV-XVI (Manifests) cover the manifest model and the digest-pinned reference.
Quiz
Knowledge check · 4 questions
Q1. A Kubernetes manifest in the deploy job references the image by tag (`image: myapp:v1.2.3`). What is the failure mode?
Q2. A deploy job that re-renders the manifest template from source has the same anti-pattern as a Terraform apply job that re-runs `terraform plan`.
Q3. Explain why the OCI image and the Kubernetes manifest are two distinct artifacts in two distinct stores.
Q4. Diagnose why a Kubernetes deploy ran an image that the reviewer did not approve, and prescribe the fix.
Team T's pipeline builds a container image, pushes it to ghcr.io/acme/api:${ github.sha } (a tag, not a digest), and renders a manifest that references the image by tag. The deploy job downloads the manifest from the artifact store and applies it. A colleague pushes a hotfix image to the same tag two minutes later. The deploy applies the manifest; the cluster pulls the hotfix image, not the image the build produced. The hotfix has a different bug fix than the reviewer's PR.
Passing score: 75%. Answers are checked in this browser.