Skip to content

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 readyAbout 120 minutes

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

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 help desk’s platform team will run the ticket API from now on. Before they accept it, they ask for four things:

  1. 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.
  2. 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.
  3. 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.
  4. A deployment and a clean-up that someone else can repeat. A checklist, an inventory of every resource, and proof that nothing is left.
  • Make a file evidence.md outside 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.

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.

  1. Add these lines to the Dockerfile. Decide where they go. A new commit changes REVISION, and that change must not run pip install again.

    ARG VERSION=dev
    ARG REVISION=unknown
    LABEL org.opencontainers.image.title="ticket-api" \
    org.opencontainers.image.version="${VERSION}" \
    org.opencontainers.image.revision="${REVISION}"
  2. Commit the change. Then copy the project to a new folder with Git, so that no ignored file comes with it:

    Terminal window
    cd ~/projects
    git clone ticket-api ticket-api-fresh
    cd ticket-api-fresh
  3. 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.

  4. Show the labels:

    Terminal window
    docker image inspect ticket-api:1.1.0 --format '{{json .Config.Labels}}'
  5. 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.txt
    diff 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.

  6. Make one more commit (a change in README.md is enough), in your usual project folder. Pull it into the fresh folder with git pull, and build again with the new commit ID. Look at which steps say CACHED.

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.

Uses Ports and service access: Choose who can connect, Persistent data: Your turn: the database stops and Operational configuration: Three probes, two endpoints.

  1. Add a health check to the api service in compose.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: 10s
    timeout: 3s
    retries: 3
    start_period: 5s
  2. Give Compose the build arguments, so that docker compose up --build labels the image too. Replace build: . with:

    build:
    context: .
    args:
    VERSION: 1.1.0
    REVISION: ${REVISION:-unknown}
  3. 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 --build
    docker compose ps

    In PowerShell: $env:REVISION = git rev-parse --short HEAD, then docker compose up -d --build.

  4. Show who can connect: docker compose port api 8000.

  5. Stop the database. Check /ready and /health. Wait 30 seconds and run docker compose ps again.

  6. Start the database again, and check /ready.

  7. 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.

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.

  1. Read the key and the password into variables:

    Terminal window
    KEY=$(cat secrets/api_key.txt)
    PW=$(grep POSTGRES_PASSWORD .env | cut -d= -f2)
  2. 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"
  3. The image’s configuration (environment values, labels, command):

    Terminal window
    docker image inspect ticket-api:1.1.0 | grep -c -e "$KEY" -e "$PW"
  4. 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.tar
    mkdir -p /tmp/ticket-api-layers && tar -xf /tmp/ticket-api.tar -C /tmp/ticket-api-layers
    for 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
  5. Git: git ls-files .env secrets prints nothing, and git check-ignore .env secrets/api_key.txt prints both names.

  6. 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.

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.

  1. 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.
  2. Build for linux/amd64 with the labels from Part 1, and push it with the tag 1.1.0.
  3. 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.
  4. Run the smoke checks: /health, /ready, a classification with the key, the same request without the key, /docs, and http:// instead of https://.
  5. 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}}'. Compare revision with your commit.
  6. Delete the resource group. Wait until az group exists says false. If you made a Key Vault, purge it. Then run the checks from your clean-up list.

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.

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.
  • /health answers “is the process alive?” and /ready answers “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.