Final assignment
- After Module 5
- about 2 hr
What you will be able to do
Hand the ticket API to another team the way a professional does. Anyone must be able to build the same image from your files, and to tell which commit it came from. It must start with the correct port and health checks, carry no secret, and be deployed and removed with a checked record. You make one small change to the project, then you prove each of these with evidence.
Before you startWhat to know and to have ready
What you need to know
- You completed Modules 1 to 5. Each part below names the lessons that it uses.
What you need on your computer
- Your
ticket-apiproject withDEPLOY.md, as at the end of Verify and remove: Module practice. deploy/containerapp.yaml, from Operational configuration: The settings in one file.deploy/local-cloud.yaml, from Registry and deployment: The simulated route: the same steps on your computer.- The project in a Git repository, as in Build and Deploy Your First API. If it is not, run
git init, then commit all files. Check first that.gitignorehas.envandsecrets/. - Docker, from Install and verify: Install Docker.
- For the live route of Part 4: an Azure subscription and the Azure CLI. You set them up in Registry and deployment: Before step 1: sign in and prepare the subscription. Without them, use the simulated route. Both routes can complete the course.
- If your project is missing or broken, copy the snapshot
end-of-m05. Use a snapshot has the steps. A snapshot has no.env, nosecrets/and no cloud resources: make them again.
Tested with Docker Desktop 4.81.0 (Docker Engine 29.6.1) on macOS, Apple silicon. The Azure steps were run on the same day with Azure CLI 2.88.0 and the containerapp extension 1.3.0b5, in region canadacentral. The Windows and Linux steps are not tested.
The situation
Section titled “The situation”The help desk’s platform team will run the ticket API from now on. Before they accept it, they ask for four things:
- A reproducible, traceable image. They build the image from your files and get the same packages. Every image says which version and which Git commit it came from.
- Correct ports and health checks. The API is reachable only where it must be. A health check shows if the process works. A database outage makes the API “not ready”, but it does not restart the API.
- No secret in the image. No key and no password in any layer, in the image’s settings, in Git or in the Compose file.
- A deployment and a clean-up that someone else can repeat. A checklist, an inventory of every resource, and proof that nothing is left.
Rules for every part
Section titled “Rules for every part”- Make a file
evidence.mdoutside the project folder, for example~/projects/c18-evidence.md. After each part, copy the commands and outputs that the part asks for into it. - Never copy a key or a password into the evidence. Where a check uses a secret, show only the result (for example a count of
0). - Use a fresh folder for the build in Part 1. A build that works only in your usual folder is not reproducible.
Part 1: a reproducible, traceable image
Section titled “Part 1: a reproducible, traceable image”Uses Dockerfile essentials: Make requirements-run.txt in a container and Build efficiently: The layer cache.
The platform team wants each image to carry labels: key–value metadata in the image’s configuration. The Open Container Initiative defines standard names for them. You set the values when you build, with build arguments (ARG): values that exist only while the image is built.
-
Add these lines to the
Dockerfile. Decide where they go. A new commit changesREVISION, and that change must not runpip installagain.ARG VERSION=devARG REVISION=unknownLABEL org.opencontainers.image.title="ticket-api" \org.opencontainers.image.version="${VERSION}" \org.opencontainers.image.revision="${REVISION}" -
Commit the change. Then copy the project to a new folder with Git, so that no ignored file comes with it:
Terminal window cd ~/projectsgit clone ticket-api ticket-api-freshcd ticket-api-fresh -
Build with the version and the short commit ID:
Terminal window docker build --build-arg VERSION=1.1.0 --build-arg REVISION=$(git rev-parse --short HEAD) -t ticket-api:1.1.0 .In PowerShell, use
--build-arg REVISION=$(git rev-parse --short HEAD)the same way. It works in PowerShell too. -
Show the labels:
Terminal window docker image inspect ticket-api:1.1.0 --format '{{json .Config.Labels}}' -
Prove that the image has exactly the packages of
requirements-run.txt:Terminal window docker run --rm ticket-api:1.1.0 pip freeze > installed.txtdiff installed.txt requirements-run.txt && echo "same packages"On Windows, use
Compare-Object (Get-Content installed.txt) (Get-Content requirements-run.txt). No output means no difference. -
Make one more commit (a change in
README.mdis enough), in your usual project folder. Pull it into the fresh folder withgit pull, and build again with the new commit ID. Look at which steps sayCACHED.
Evidence for part 1: the place of the new lines in the Dockerfile, with one sentence of reason; the labels output; the diff result; the list of steps that were cached in step 6.
What you should see
The labels output has your values, for example:
{"org.opencontainers.image.revision":"1b1b958","org.opencontainers.image.title":"ticket-api","org.opencontainers.image.version":"1.1.0"}diff prints nothing and then same packages. When the ARG and LABEL lines are after USER app, all seven numbered steps ([1/7] to [7/7]) are CACHED in step 6: only the labels change. If you put ARG REVISION near the top, before RUN pip install, a new commit runs pip install again. A build argument that a step can see is part of that step’s cache key.
Part 2: ports and health
Section titled “Part 2: ports and health”Uses Ports and service access: Choose who can connect, Persistent data: Your turn: the database stops and Operational configuration: Three probes, two endpoints.
-
Add a health check to the
apiservice incompose.yaml. The slim image has no curl, so use Python:healthcheck:test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]interval: 10stimeout: 3sretries: 3start_period: 5s -
Give Compose the build arguments, so that
docker compose up --buildlabels the image too. Replacebuild: .with:build:context: .args:VERSION: 1.1.0REVISION: ${REVISION:-unknown} -
Start the API and the database. Wait about 15 seconds, then show the status:
Terminal window REVISION=$(git rev-parse --short HEAD) docker compose up -d --builddocker compose psIn PowerShell:
$env:REVISION = git rev-parse --short HEAD, thendocker compose up -d --build. -
Show who can connect:
docker compose port api 8000. -
Stop the database. Check
/readyand/health. Wait 30 seconds and rundocker compose psagain. -
Start the database again, and check
/ready. -
Open
deploy/containerapp.yaml. Check that each probe uses the right path and port, and write down why each probe uses its path.
Evidence for part 2: the docker compose ps outputs before and after step 5; the status codes of /ready and /health in step 5; the output of step 4 with one sentence: who can connect, and why; your notes from step 7.
What you should see
Both services are Up … (healthy). docker compose port api 8000 gives 127.0.0.1:8000: only programs on your computer can connect. With the database stopped, /ready gives 503 and /health gives 200. After 30 seconds, api is still (healthy): the health check uses /health, so a database outage does not mark the API as broken or restart it. That is correct: a restart cannot repair a database. /ready tells the platform to send no traffic to this replica. In deploy/containerapp.yaml, the startup and liveness probes use /health and the readiness probe uses /ready, all on port 8000, the port that the app listens on in the container.
Part 3: no secret anywhere
Section titled “Part 3: no secret anywhere”Uses Runtime safety: Keep credentials out of the image, Runtime configuration: Secret files and Identity and secrets: Where the secret lives.
Check in five places. For each check, record the command and its result. Never record the secret itself. The macOS and Linux commands read the key into a variable, so that it does not appear on the screen.
-
Read the key and the password into variables:
Terminal window KEY=$(cat secrets/api_key.txt)PW=$(grep POSTGRES_PASSWORD .env | cut -d= -f2) -
The image’s history (every instruction, in full):
Terminal window docker history --no-trunc ticket-api:1.1.0 | grep -c -e "$KEY" -e "$PW" -
The image’s configuration (environment values, labels, command):
Terminal window docker image inspect ticket-api:1.1.0 | grep -c -e "$KEY" -e "$PW" -
Every file in every layer. Save the image as a file, then search it:
Terminal window docker save ticket-api:1.1.0 -o /tmp/ticket-api.tarmkdir -p /tmp/ticket-api-layers && tar -xf /tmp/ticket-api.tar -C /tmp/ticket-api-layersfor f in $(find /tmp/ticket-api-layers -type f); do tar -xOf "$f" 2>/dev/null || cat "$f"; done | grep -a -c -e "$KEY" -e "$PW"rm -rf /tmp/ticket-api.tar /tmp/ticket-api-layers -
Git:
git ls-files .env secretsprints nothing, andgit check-ignore .env secrets/api_key.txtprints both names. -
The Compose file:
grep -c -e "$KEY" -e "$PW" compose.yaml.
The commands in steps 2 to 4 and 6 work in a Bash or zsh terminal. On Windows, run them in WSL, or do the same checks with Select-String in PowerShell. The Windows steps are not tested.
Evidence for part 3: the five results, and one sentence for each place that says why a secret could be there in a careless project.
What you should see
Every count is 0. git ls-files prints nothing. git check-ignore prints .env and secrets/api_key.txt. Why each place matters: docker history shows every RUN, ENV and ARG value; docker image inspect shows ENV values and labels; a file that was copied in and later deleted is still in a lower layer; a committed .env stays in the Git history even after you delete it; and a password typed into compose.yaml goes to Git with it. If a count is not 0, do not only remove the secret: it is exposed. Make a new key or password, and change it everywhere it is used.
Part 4: deploy, verify and remove
Section titled “Part 4: deploy, verify and remove”Uses Registry and deployment: Build for the cloud’s CPU and Operational configuration: The settings in one file. It also uses Verify and remove: Smoke checks: what this deployment must prove, Clean up from an explicit inventory and Cost awareness: A resource inventory.
Choose one route. Label your evidence live or simulated.
- Before you create anything, write the inventory in
DEPLOY.md: every resource that you will make, what it can cost, and how you will delete it. - Build for
linux/amd64with the labels from Part 1, and push it with the tag1.1.0. - Create or update the container app with
deploy/containerapp.yaml: the key as a secret,REQUIRE_API_KEY=true,SHOW_DOCS=false, the probes, and at most 2 replicas. - Run the smoke checks:
/health,/ready, a classification with the key, the same request without the key,/docs, andhttp://instead ofhttps://. - Show the labels of the image in the registry:
docker buildx imagetools inspect <registry>.azurecr.io/ticket-api:1.1.0 --format '{{json .Image.Config.Labels}}'. Comparerevisionwith your commit. - Delete the resource group. Wait until
az group existssaysfalse. If you made a Key Vault, purge it. Then run the checks from your clean-up list.
- Write the inventory in
DEPLOY.mdfor the live design, as if you were going to deploy it: every resource, what it can cost, and how you would delete it. - Build the image with the labels from Part 1. Also build it for
linux/amd64withdocker buildx build --platform linux/amd64, to prove that the cloud’s platform works. You do not need to push it. - Start the simulation with
docker compose -f deploy/local-cloud.yaml up -d. - Run the smoke checks against
http://127.0.0.1:8080:/health,/ready, a classification with the key, the same request without the key, and/docs. - Show the labels of the running container’s image with
docker inspect. - Stop and remove everything that you made for this part. Check with
docker ps -a,docker image lsanddocker volume ls.
Evidence for part 4: the inventory; the smoke-check results (status codes and bodies, without the key); the revision label from the deployed or running image; and the clean-up record with the commands and outputs that prove nothing is left.
What you should see
Smoke checks: /health → {"status":"ok"}, /ready → {"status":"ready"}, classification with the key → 200, without the key → 401, /docs → 404. Live only: http:// → 301 to https://, and the first request can take about 18 seconds if the app was at zero replicas. The inventory has the resource group and the five resources in it (registry, identity, Log Analytics workspace, Container Apps environment, container app), plus the Key Vault if you made one. Live clean-up ends with az group exists --name rg-ticket-api-practice → false, and az keyvault list-deleted without your vault. Simulated clean-up ends with no ticket-api-cloud-sim container in docker ps -a.
Check your work
Section titled “Check your work”There is no reviewer for this course yet. Review your evidence with this table, or ask a colleague to use it. Mark each item as demonstrated, partially demonstrated or not yet demonstrated, with a reason. The course is complete when every item is demonstrated. You can improve an item and review it again as often as you need.
| Required evidence | Demonstrated when |
|---|---|
| Reproducible container build | A build from a fresh clone works; pip freeze in the image equals requirements-run.txt; the labels show the version and the commit; a new commit reuses the pip install layer. |
| Correct port and health configuration | The host port is bound to 127.0.0.1 locally; the health check is healthy; a stopped database gives /ready 503 while /health stays 200 and the container is not restarted; the probes in deploy/containerapp.yaml use the right paths and port. |
| No embedded secrets | All five checks give 0 or nothing; .env and secrets/ are ignored by Git; the key reaches the app only at run time (a secret file locally, a platform secret in the cloud). |
| Deployment and clean-up checklist | DEPLOY.md has the inventory with costs, the deployment steps and the clean-up steps; the smoke checks pass, including the failure paths (401, 404); the clean-up is verified with commands, not assumed. |
A working happy path is not enough. Maybe the API answers 200 without a key, a secret shows up in a layer, or a resource is still there after the clean-up. Then the item is not yet demonstrated, even if everything else works.
Common mistake
The labels say unknown
Symptom: docker image inspect shows "org.opencontainers.image.revision":"unknown" after docker compose up --build.
Cause: Compose reads ${REVISION:-unknown} from your shell or from .env. You did not set REVISION before the command, so Compose used the default.
Fix: set it in the same command: REVISION=$(git rev-parse --short HEAD) docker compose up -d --build. For a real release, a build pipeline sets it every time, so no person can forget it. The course CI/CD, Testing and Safe Releases does this.
Recap
- A label and a build argument make an image traceable to a version and a commit. Put them late in the Dockerfile, so the cache still works.
/healthanswers “is the process alive?” and/readyanswers “can it serve now?”. A database outage changes only the second.- Check five places for secrets: the history, the configuration, the layers, Git and the Compose file.
- A deployment is complete only when its clean-up is verified.
Next: CI/CD, Testing and Safe Releases builds, tests and promotes this image automatically, with every check from this assignment in a pipeline.