Skip to content

Images and scans

2 min read · main part only; folded sections are optional

After this you can answer: Why build an image once and deploy the same bytes, and what does the scan block?

flowchart TB
C["commit 1a2b3c4"] --> CI["CI: build x86 + Trivy scan<br/>(nothing pushed)"]
C --> P["CD publish: build ARM64<br/>push :1a2b3c4"]
P --> E[("ECR, immutable tags")]
E -->|"same bytes"| D["deploy: run :1a2b3c4<br/>tag deployed-1a2b3c4"]
  1. CI proves the Dockerfiles: it builds all three images for x86 and scans them. Nothing is pushed.
  2. CD publish builds the real images for ARM64 on an ARM runner (Fargate and Lambda run ARM64) and pushes them as :<sha>. An existing tag is skipped, so a rerun succeeds.
  3. ECR scans on push too (scanOnPush, a basic scan).
  4. deploy runs those bytes. It never builds. In phase B it adds a second tag, deployed-<sha>, to the same image (same digest).
  5. Lifecycle rules clean up without deleting rollback targets.
Lifecycle rule (per repository)Keeps
1. tag deployed-*the newest 10 deployed images: the rollback targets
2. any other tagthe newest 30 published images
3. untagged1 day

Rule 1 runs first, so a deployed image is safe even when 30 newer, never-deployed images exist. Before this rule, the running image could be expired after enough pushes, and ECS could not start a replacement task.

The Trivy scan (severity: CRITICAL, ignore-unfixed: true, exit-code: 1) fails CI on critical findings that have a fix. .trivyignore holds one dated exception, CVE-2023-45853 in Debian’s zlib, which expires on 2026-11-14 and must then be reviewed again.

The api and web images run as the unprivileged node user. The worker image starts from AWS’s Lambda base image.

See the repositories on the map →

ECR → Repositories → minitickets-dev-api → Images (each SHA, and deployed- tags) → Lifecycle policy.

SymptomCauseWhere to look
CannotPullContainerErrorThe tag was never pushed, or the image is not ARM64ECR → the repository
A push is refused: tag already existsTags are immutable; publish should have skipped itThe publish job’s check step
CI fails at a scan stepA fixable critical CVE in a base image or a dependencyTrivy’s table in the log
Why not rebuild the image in the deploy job?

A rebuild can pull newer base images or packages, so it would not be what was tested and scanned. Promoting one build removes that difference.

Why ignore unfixed findings?

A finding with no fix cannot be acted on, and failing on it would block every build. The trade-off: those findings stay invisible unless someone reads the full report.

How do you add a deployed tag without a new image?

Read the image’s manifest and put it again under the new tag. Same manifest bytes give the same digest, and CD checks that the digest did not change.

Advanced: Details: the publish build

Source difference: docs/20-deploy-day.md says unfixed Trivy findings are included in the failure gate. The book follows .github/workflows/ci.yml: ignore-unfixed: true (the --ignore-unfixed option) excludes findings without a fix. The old step notes stay frozen.

provenance: false keeps each tag one plain ARM64 image; Lambda refuses image indexes. Layer caches are per image and per platform (api-arm64 in CD, api in CI), so they do not overwrite each other (.github/workflows/cd.yml → publish).

Answer these without scrolling up. If one feels hard, reread that section.

  1. What does “immutable tag” guarantee?
  2. Why does CI build images it never pushes?
  3. Which lifecycle rule protects rollback targets?
  4. What does the Trivy scan fail on?
  5. Why must the images be ARM64?

Redraw it from memory: commit → CI (x86 build + Trivy, no push) and CD publish (ARM64 → ECR :sha) → deploy runs the same digest + deployed-sha tag

Try this: on the map, select the ECR repositories and see which jobs and services connect to them

Read this book online: https://book.theversehub.in/