Lab — from source code to AKS operations
You already know how code is promoted through configured dev, test, UAT, and production environments. This lab starts with code-complete software and covers nine phases: container contract → image → registry → Kubernetes → AKS → operations. Each activity has a pre-check (baseline what you know), the steps to run, a validation gate, and a post-check (validate the result). Every phase also has a scored knowledge check — a baseline quiz before and a measured quiz after to record changes in understanding. The guide records activity and assessment scores. Switch to Instructor-led to display trainer talk tracks and demo cues.
scripts/, and runnable Skybridge
starter applications under apps/. Foundation labs F1–F5 also use your own workshop-notes folder.
Later phases additionally require environments, Compose and Kubernetes manifests, and a CI workflow to be placed alongside these files.
Confirm the starter repository and those later-phase inputs are open in VS Code before Phase 0.
Commands in this guide use a Bash-compatible VS Code terminal (Git Bash, WSL, macOS, or Linux); do not run them unchanged in PowerShell.
./scripts/verify-prerequisites.sh from the starter-repository root.
Continue only when it confirms the required source, scripts, environment profiles, Dockerfiles, Compose template, Kubernetes templates,
Git, curl, Docker Engine with the Compose and Scout plugins, Azure CLI, kubectl, and kind at the supported versions.
Restore any missing repository files or obtain the required environment, manifest, and workflow inputs before continuing.
How points work
Activity +10 · baseline +5 · correct follow-up answer +10 · passed phase +25Foundation labs — turn the reading into evidence
Five guided exercises connect the reading, architecture diagrams, and Skybridge scenario to the implementation phases.Create your workshop notebook
Open the workshop folder in VS Code. Create a folder named workshop-notes. Each foundation lab adds one Markdown file to that folder; later phases refine those files with measured evidence.
F1 Reading lab — recognize cloud design patterns ~25 min
◆ Read and predict
- Open the Microsoft Learn Cloud Design Patterns catalog.
- Before reading the three patterns, write one sentence predicting where each might apply to a messaging gateway.
▶ Do in VS Code
- Create
workshop-notes/PATTERN-DECISIONS.mdwith the table below. - Read Retry, Health Endpoint Monitoring, and Competing Consumers. For each, record the problem, trigger, benefit, and one risk.
- For one pattern, follow the official Microsoft Learn example or guidance linked from its pattern page and cite that page in your notes.
- Mark each pattern use now, defer to replatform, or not applicable. Defend the decision in one sentence.
| Pattern | Problem in Skybridge | Trigger/evidence | Decision | Risk |
| --- | --- | --- | --- | --- |
| Retry | | | | |
| Health Endpoint Monitoring | | | | |
| Competing Consumers | | | | |
✓ Check your work
The file has three completed rows, one Microsoft Learn citation, and decisions that distinguish application resilience from platform orchestration. You can explain why retries require limits and backoff.
Need help?
If a pattern feels abstract, write the failure first: “the parser is temporarily unavailable.” Then ask whether the caller retries, a health probe removes traffic, or multiple consumers share queued work.
◇ Reflect
- Which behavior belongs in application code, and which can Kubernetes provide?
- What evidence would prove the pattern is helping rather than amplifying a failure?
F2 Reading lab — inspect an Azure landing-zone boundary ~25 min
◆ Read and predict
- Read What is an Azure landing zone?.
- Predict which controls belong to the platform team and which belong to the Skybridge workload team.
▶ Do in the VS Code terminal
- Sign in and confirm the subscription displayed by Azure CLI. Stop if it is not your intended training subscription.
- List available regions, then choose the instructor-approved region.
- Create a tagged resource group as the workload lifecycle boundary and inspect its tags.
- Create
workshop-notes/AZURE-CONTEXT.md. Record subscription name only, tenant ID suffix only, region, resource group, required tags, and who owns identity, policy, networking, cost, and workload configuration.
az login
az account show --query "{subscription:name, tenant:tenantId}" -o table
az account list-locations --query "[?metadata.regionType=='Physical'].{name:name,display:displayName}" -o table
az group create --name rg-aks-workshop --location <approved-region> --tags purpose=training owner=<your-alias> expires=<YYYY-MM-DD>
az group show --name rg-aks-workshop --query "{name:name,location:location,tags:tags}"
✓ Check your work
The active subscription is intentional, the resource group has all three tags, and AZURE-CONTEXT.md distinguishes landing-zone responsibilities from workload responsibilities.
Need help?
If az login cannot open a browser, use az login --use-device-code. If resource-group creation is denied, record the policy or RBAC error and use an instructor-provided group; do not try to bypass the control.
◇ Clean up and reflect
Delete the empty group with az group delete --name rg-aks-workshop --yes --no-wait, then verify completion with az group wait --name rg-aks-workshop --deleted and az group exists --name rg-aks-workshop (expect false). Which landing-zone controls must exist before an AKS workload is production-ready?
F3 Reading lab — draw cohesive application boundaries ~25 min
◆ Read and predict
- Review the recommended feature-slice and modular-monolith readings from the overview.
- Write down whether “one module” must mean “one container.” Keep the answer for comparison after the exercise.
▶ Do in VS Code
- Create
workshop-notes/BOUNDARY-MAP.mdand add the Mermaid diagram below. - For gateway, parser, operations console, and PostgreSQL, record responsibility, inbound/outbound protocol, owned data, scaling trigger, and failure impact.
- Draw a solid boundary around components that must deploy together today. Use a dashed boundary for a possible future separation.
- Use VS Code Markdown Preview to confirm the diagram renders. Record one reason to keep the current boundary and one evidence-based reason that might justify splitting it later.
```mermaid
flowchart LR
Client -- TCP --> Gateway
Gateway --> Parser
Gateway --> Database[(PostgreSQL)]
Console -- HTTPS --> Gateway
```
✓ Check your work
The diagram has four components, labeled protocols, data ownership, failure impact, and at least one explicit deployment boundary. No boundary is justified only by “microservices are better.”
Need help?
If Mermaid preview is unavailable, use a Markdown table instead. The evidence matters more than the drawing tool.
◇ Reflect
- Which components can scale independently?
- Which boundary changes the container contract, and which changes only deployment topology?
F4 Concept lab — compare classic, container, and Kubernetes workflows ~30 min
◆ Observe
Study both diagrams in The unit of promotion changes. Without looking back, name the promoted unit and runtime owner in each workflow.
▶ Do in the VS Code terminal
- Run nginx as a conventional local process if available, or inspect an existing host process. Record who supplies its runtime and configuration.
- Run nginx as a container. Inspect the image ID, environment, ports, process, and read-only image layers.
- Stop and replace the container from the same image. Confirm the image ID remains constant while the container ID changes.
- Create
workshop-notes/WORKFLOW-COMPARISON.mdwith columns for promoted unit, runtime owner, configuration source, health/restart owner, scale mechanism, and rollback mechanism.
docker pull nginx:alpine
docker image inspect nginx:alpine --format "image={{.Id}}"
docker run --name workflow-demo -d -p 8080:80 nginx:alpine
docker inspect workflow-demo --format "container={{.Id}} image={{.Image}}"
docker rm -f workflow-demo
docker run --name workflow-demo-2 -d -p 8080:80 nginx:alpine
docker inspect workflow-demo-2 --format "container={{.Id}} image={{.Image}}"
docker rm -f workflow-demo-2
✓ Check your work
The two container IDs differ, both report the same image ID, and the comparison file correctly states that containers package runtime dependencies while Kubernetes adds desired-state reconciliation.
Need help?
If port 8080 is busy, replace both occurrences of 8080:80 with 8081:80. If the container exits, run docker logs workflow-demo before removing it.
◇ Reflect
- What did containerization solve in this test?
- What remains unsolved until a scheduler continuously maintains desired state?
F5 Scenario lab — establish the Skybridge promotion baseline ~35 min
◆ Frame the scenario
Re-read Code-complete release promotion. Treat the listed Java gateway, C/C++ parser, operations console, PostgreSQL integration, and RHEL hosts as discovery inputs, not yet as container designs.
▶ Do in VS Code
- Create
workshop-notes/PROMOTION-BASELINE.mdfrom the template below. - For each component, classify every dependency as travels with release, supplied by host, or supplied by environment.
- Define dev, test, UAT, and production rows. Record what must remain identical and what may vary.
- Add one likely drift failure and the command or evidence that would detect it.
- Write the target invariant in your own words: one immutable image reference plus external stage configuration produces each deployment.
## Component inventory
| Component | Artifact | Runtime | Port/protocol | Config | Secret | Writable data |
| --- | --- | --- | --- | --- | --- | --- |
| Gateway | JAR | Java 8 / Netty | TCP 4561 | | | |
| Parser | Native binary | RHEL-compatible libs | Internal | | | |
| Console | Static files | Web server | HTTP 80 | | | |
## Promotion stages
| Stage | Immutable input | Allowed variation | Validation gate | Rollback |
| --- | --- | --- | --- | --- |
| Dev | | | | |
| Test | | | | |
| UAT | | | | |
| Production | | | | |
✓ Check your work
Every scenario component has an artifact, runtime, interface, configuration source, secret boundary, and writable-data decision. Every stage names an immutable input and validation gate.
Need help?
Unknown values are valid discovery results. Write UNKNOWN — owner: <role> — resolve before Phase 1 instead of guessing.
◇ Reflect
- Which host responsibility should move into an image?
- Which values must stay outside every image?
- What would prevent “build once, promote many” today?
Where every workshop concept becomes hands-on
Use this map when a concept feels abstract or when you want to repeat its practical exercise.
Phase 0 — Orient from the model you know
Verify the code-complete release, then contrast configured environments with immutable-image promotion. 0/20.1 Verify the release and container toolchain ~15 min
◆ Pre-check — baseline: Confident / Fuzzy / No idea
- What evidence tells you a code-complete release is ready to enter promotion?
- Which runtime dependencies are currently supplied by the target environment?
- What is the difference between a container image and a running container?
▶ Do
- Run the supplied unit and integration tests for the code-complete Skybridge release.
- Confirm Git and the supported container engine are installed and the daemon is available.
- Pull and run a known image, then build a minimal local image to verify the complete tool path.
./scripts/test-release.sh
docker version
docker run --rm hello-world
docker run --name workshop-nginx -d -p 8080:80 nginx:alpine
curl --fail http://localhost:8080/
docker rm -f workshop-nginx
echo 'FROM alpine' > Dockerfile.test
docker build -t aks-workshop-check:1 -f Dockerfile.test .
rm Dockerfile.test
✓ Validation
The supplied release tests pass, hello-world prints its greeting, nginx serves on :8080, and aks-workshop-check:1 appears in docker images.
◇ Post-check — if not "Confident" on every line, flag it before moving on
- Which release artifacts are code complete, and which environment assumptions remain external?
- You ran two containers from the same nginx image — how many images were pulled?
- What must be captured before "build once, promote many" can be true?
0.2 Contrast environment promotion with image promotion ~10 min
◆ Pre-check
- Between dev, test, UAT, and production, what changes besides application code?
- Which environment differences have caused a release to behave differently after promotion?
- What would it mean to promote exactly the same runtime and dependency set?
▶ Do
- Review the supplied release manifest and the dev, test, UAT, and production environment profiles.
- In
PROMOTION-BASELINE.md, classify what currently travels with the release and what each environment supplies. - Write the proposed invariant: one immutable image reference plus injected environment configuration produces each stage deployment.
./scripts/show-release-manifest.sh
diff environments/dev.env environments/test.env
git rev-parse HEAD
✓ Validation
PROMOTION-BASELINE.md identifies the release artifact, runtime dependencies, configuration ownership, secret boundary, and exact values that vary by stage.
◇ Post-check
- Which current environment responsibility should move into the image?
- Which values must remain outside the image, and why?
- How will an immutable image reduce uncertainty without eliminating environment-specific configuration?
Phase 1 — Derive the container contract
Make the release's runtime assumptions explicit before writing a Dockerfile. 0/31.1 Inventory runtime dependencies and process behavior ~20 min
◆ Pre-check
- What does the application need from an operating system before its main process can start?
- Which Java, native-library, certificate, user, filesystem, and network assumptions are implicit today?
- How should the process react to startup failure and an operating-system termination signal?
▶ Do
- Inspect the release manifest, launch scripts, linked libraries, and existing environment documentation.
- Record build-time dependencies separately from runtime dependencies for the Java gateway and C++ parser.
- Define the entrypoint, working directory, user, writable paths, ports, and termination behavior in
CONTAINER-CONTRACT.md.
./scripts/inspect-runtime.sh gateway-java
./scripts/inspect-runtime.sh parser-cpp
ldd build/parser-cpp/skybridge-parser
✓ Validation
CONTAINER-CONTRACT.md names every build-time and runtime dependency and defines how each component starts, runs, writes files, exposes ports, and terminates.
◇ Post-check
- Which dependencies belong in a build stage but not in the runtime image?
- Which host assumptions must become explicit image instructions?
- What state would be lost when the container is replaced?
1.2 Define configuration, secrets, health, and observability ~12 min
◆ Pre-check
- Which values vary by deployment stage and therefore cannot be baked into the image?
- What is the difference between process liveness and readiness to receive traffic?
- What must be emitted to standard output so a platform can collect useful logs?
▶ Do
- Classify every setting as invariant, stage configuration, or secret; define its name, format, default policy, and missing-value behavior.
- Define startup, readiness, and liveness signals plus the structured fields required in logs.
- Add configuration, secret, health, and telemetry sections to
CONTAINER-CONTRACT.md.
./scripts/inventory-config.sh
./scripts/validate-contract.sh CONTAINER-CONTRACT.md
✓ Validation
The contract accounts for every stage-specific setting and secret, defines fail-fast behavior for required values, and names machine-testable health and telemetry signals.
◇ Post-check
- Which configuration values may safely have defaults, and which must fail startup when absent?
- How will a platform distinguish "process exists" from "safe to route traffic"?
- Which log fields let you connect a request, image revision, Pod, and failure?
1.3 Set resource, security, and promotion acceptance criteria ~13 min
◆ Pre-check
- Why does a scheduler need CPU and memory expectations before placing a workload?
- What privileges and Linux capabilities does this application actually require?
- What evidence should permit the same image digest to advance to the next stage?
▶ Do
- Record initial CPU and memory requests, limits, expected concurrency, and shutdown grace period.
- Require a non-root user, read-only runtime where possible, minimal capabilities, and no embedded credentials.
- Define test, scan, health, performance, provenance, and rollback gates for promotion of one immutable digest.
✓ Validation
The completed contract is specific enough to implement as a Dockerfile, Compose service, and Kubernetes container specification without rediscovering runtime requirements.
◇ Post-check
- Which contract fields belong to the image, and which belong to the deployment platform?
- Which digest will verify that the image tested is the image deployed to UAT?
- What evidence will trigger rollback rather than further tuning?
Phase 2 — Separate image from environment
Externalize stage configuration so one immutable build can move through dev, test, UAT, and production. 0/32.1 Inventory every setting and where it lives ~15 min
◆ Pre-check
- What makes a value deployment configuration rather than invariant application code?
- What is the difference between config that varies by environment and config that is the same everywhere?
- Why should a hard-coded default in source count as configuration too?
▶ Do
- Grep the three components and the runbook for every tunable: ports, hostnames, DB DSN, timeouts, thread pools, credentials.
- Sort each into per-environment vs constant, and flag any secret.
- Capture the result as a config table in
CONFIG.md.
git grep -nEI 'password|secret|host|port|timeout|url|dsn' -- apps legacy
✓ Validation
CONFIG.md lists every setting with a column for source-of-truth today and a column marking secrets. Nothing is "we'll remember it".
◇ Post-check
- How many secrets are currently in plaintext on a host or in the repo?
- Which settings differ between dev and prod, and how do you know today?
- Which current constant is likely to vary by environment?
2.2 Run one build with multiple stage configurations ~20 min
◆ Pre-check
- Why does reading config from environment variables decouple build from deploy?
- What is a sensible precedence order: env var, config file, built-in default?
- What belongs in a committed
.env.example, and what must never be committed?
▶ Do
- Refactor each component to read every per-environment setting from an environment variable, with a safe default only where it's truly universal.
- Add a committed
.env.exampledocumenting each variable; keep the real.envgit-ignored. - Run the app from the same build with the supplied dev, test, and UAT profiles and observe only the intended configuration differences.
./scripts/show-release-manifest.sh > workshop-notes/release-before.txt
./scripts/run-release.sh --env environments/dev.env
./scripts/run-release.sh --env environments/test.env
./scripts/run-release.sh --env environments/uat.env
./scripts/show-release-manifest.sh > workshop-notes/release-after.txt
diff workshop-notes/release-before.txt workshop-notes/release-after.txt
git diff --exit-code -- apps # no source edit
✓ Validation
One build runs under dev, test, and UAT profiles with the intended behavior in each stage. No source edit or rebuild is required to promote it.
◇ Post-check
- Which setting was hardest to externalize, and why?
- If a value is absent, does the app fail loudly at startup or silently use a wrong default? Which do you want?
- Why is "config in the environment" a precondition for one image promoting across environments?
2.3 Get secrets out of source and out of the image ~15 min
◆ Pre-check
- Why is a secret baked into an image effectively public to anyone who can pull it?
- What is the difference between a secret at build time and at run time?
- Name two ways to inject a secret at run time without writing it to the image layers.
▶ Do
- Remove every credential from source and config files; replace with an env var reference.
- Provide secrets at run time only (an injected env var or a mounted file), never in the build.
- Add a pre-commit secret scan so a credential can't slip back in.
git grep -nEI 'password=|secret=|BEGIN [A-Z ]*PRIVATE KEY' -- . \
&& echo 'Review every match above' \
|| echo 'No matching tracked content found'
✓ Validation
The secret scan reports clean, the app still runs when a secret is injected at run time, and it fails clearly when the secret is missing.
◇ Post-check
- If someone pulls your image, can they recover any credential from its layers?
- What is your rotation story now that secrets are injected rather than baked?
- What would you hand the AKS team as the "secret contract" for each component?
Phase 3 — Build and operate the containers
Implement the container contract, then inspect and constrain each running component. 0/43.1 Containerize the JVM gateway with a multi-stage build ~18 min
◆ Pre-check
- What is a multi-stage build and what problem does it solve for compiled languages?
- Why does layer order matter for build-cache hit rate?
- Why run as a non-root user inside the container?
▶ Do
- Write a multi-stage Dockerfile: a build stage that compiles the gateway, a slim runtime stage with only the JRE and the artifact.
- Copy dependencies before source so dependency layers cache across code changes.
- Set a non-root user and an explicit entrypoint that reads config from the environment.
docker build -t gateway-java:0.1.0 apps/gateway-java/
docker run --rm -p 4561:4561 \
--add-host host.docker.internal:host-gateway \
-e GATEWAY_PORT=4561 -e PARSER_URL=http://host.docker.internal:8080 \
gateway-java:0.1.0
host.docker.internal addresses a parser running on the host. The explicit host-gateway mapping makes this work with Docker Engine on Linux as well as Docker Desktop. A dependency-unavailable log is expected until the parser is started in 3.2; the gateway itself must remain healthy and listening.✓ Validation
The gateway image builds, runs as non-root, listens on :4561, and honours the same env vars from Phase 2. Editing source and rebuilding reuses the dependency layer.
◇ Post-check
- How much smaller is the runtime image than the single-stage build?
- Which layer rebuilt when you changed one line of code — and which stayed cached?
- What did the container carry that the "VM" made you install by hand in 1.1?
3.2 Containerize the C/C++ parser ~16 min
◆ Pre-check
- Why is a compiled native binary especially well-suited to a tiny runtime image?
- What is the risk of dynamically linked libraries when you swap the runtime base?
- What does a minimal or distroless base buy you, and what does it cost?
▶ Do
- Write a multi-stage Dockerfile: a build stage with the C++ toolchain, a minimal runtime stage with just the binary and its required libs.
- Verify the binary runs on the slim base (link errors appear here, not in the build stage).
- Expose the parser's HTTP port and read its config from the environment.
docker build -t parser-cpp:0.1.0 apps/parser-cpp/
docker run --name parser-local -d -p 8080:8080 parser-cpp:0.1.0
curl --fail http://localhost:8080/healthz
docker rm -f parser-local
✓ Validation
The parser image builds and responds on its health endpoint from the slim runtime base, with no missing-library errors at start.
◇ Post-check
- What broke first when you slimmed the base, and how did you diagnose it?
- Compare the parser and gateway images — which is smaller and why?
- Why is the C/C++ maintainer-retirement risk a little smaller once the build is captured in a Dockerfile?
3.3 Containerize the ops-console static site ~12 min
◆ Pre-check
- Why containerize a static site at all, when a web server could just serve the files?
- What is the build-then-serve pattern for a front-end (build assets, serve with a tiny server)?
- How would this container get its backend URL without a rebuild?
▶ Do
- Multi-stage: build the console assets, then serve them from a minimal static server image.
- Use the supplied startup script to render a browser-readable
config.jsfromAPI_BASEbefore the web server starts. A static nginx image does not substitute environment variables into already-built JavaScript by itself. - Run it and load the console in a browser.
docker build -t ops-console:0.1.0 apps/ops-console/
docker run --name ops-console-local -d -p 3000:80 \
-e API_BASE=http://localhost:8080 ops-console:0.1.0
curl --fail http://localhost:3000/config.js
docker rm -f ops-console-local
✓ Validation
The console loads from the container, and pointing it at a different API_BASE needs only a new env value, not a new build.
◇ Post-check
- Give two reasons containerizing the static site is worthwhile even though a plain web server could serve it.
- Which parts of the front-end config are baked at build time vs injected at run time, and why?
- All three components now share one interface — what is it?
3.4 Harden, inspect, and constrain the running containers ~14 min
◆ Pre-check
- What kinds of files should a
.dockerignorekeep out of the build context, and why? - What does an image vulnerability scan inspect?
- What happens when a process exceeds a container memory limit?
▶ Do
- Add a
.dockerignoreto each component so build context excludes VCS, local env, and build junk. - Scan each image for known CVEs and trim obvious bloat from the runtime stages.
- Run the gateway under controlled CPU and memory limits; inspect its logs, process list, network, mounts, and live resource use.
docker scout cves gateway-java:0.1.0
docker run -d --name gateway-limited --cpus 1 --memory 512m \
-e GATEWAY_PORT=4561 -e PARSER_URL=http://parser.invalid:8080 \
gateway-java:0.1.0
docker logs gateway-limited
docker stats --no-stream gateway-limited
docker inspect gateway-limited
docker rm -f gateway-limited
✓ Validation
Each image has a .dockerignore and recorded scan result, contains no secret or VCS data, and runs under explicit CPU and memory limits while exposing useful logs and resource evidence.
◇ Post-check
- A High CVE sits in a base image. What should you do, and what should you not do?
- Which image had the largest attack surface, and what drove it?
- Which signal would distinguish CPU saturation, memory pressure, and an application failure?
Phase 4 — Run the system with Compose
Run, observe, and troubleshoot the complete code-complete release as cooperating containers. 0/34.1 Compose gateway + parser + Postgres locally ~20 min
◆ Pre-check
- How do containers on the same Compose network find each other?
- Why keep the database as a container in local dev but treat it as external in production?
- What is a health check in Compose and why gate
depends_onon it?
▶ Do
- Write
compose.yamlwith services for gateway, parser, ops-console, and a Postgres instance. - Wire them by service name (e.g.
PARSER_URL=http://parser:8080,DB_DSN=…@db:5432/…) using the env vars from Phase 2. - Add a named Postgres volume, database initialization, health checks, and
depends_onconditions for dependencies that must be healthy; then render and start the stack.
docker compose config --quiet
docker compose up -d --wait
docker compose ps
./scripts/smoke.sh 127.0.0.1 4561 # end-to-end through the composed stack
✓ Validation
All services report healthy, and a socket message travels client → gateway → parser → Postgres and back, entirely from images.
◇ Post-check
- Trace the message path across the composed services out loud.
- Compare "stand this up" now vs the runbook in Phase 1 — what changed about reproducibility?
- Why does the database being external in prod not change your app's config contract?
4.2 Observe and diagnose the composed system ~15 min
◆ Pre-check
- Where do you look when one healthy container cannot reach another?
- How do container logs, health state, DNS, and resource usage narrow different failure classes?
- Why should one controlled change be made between diagnostic runs?
▶ Do
- Generate controlled traffic and record baseline latency, throughput, CPU, memory, health, and log error counts.
- Inject one broken service hostname, use Compose state, logs, and network inspection to diagnose it, then restore the configuration.
- Apply a CPU or memory constraint to one component and explain the resulting runtime signal.
./scripts/load.sh --duration 60s
docker compose ps
docker compose logs --since 5m gateway parser
docker compose config --networks
docker inspect "$(docker compose ps -q gateway)" --format '{{json .NetworkSettings.Networks}}'
docker stats --no-stream
✓ Validation
COMPOSE-BASELINE.md records measured behavior, the injected symptom, evidence used to isolate it, the corrective change, and the passing retest.
◇ Post-check
- Which evidence showed whether the failure was image, configuration, network, or dependency related?
- How did the constrained component affect end-to-end behavior?
- Which measurements must be preserved for comparison on Kubernetes and AKS?
4.3 Verify the same images across stage profiles ~18 min
◆ Pre-check
- Which Compose fields describe the application, and which describe a deployment stage?
- Why must stage promotion change configuration but not rebuild an image?
- How can you verify that two stages use the same image content?
▶ Do
- Parameterize Compose with the supplied dev, test, and UAT environment profiles.
- Run the end-to-end test in each profile without invoking an image build.
- Record each container's image ID and verify it remains unchanged across the three runs.
for STAGE in dev test uat; do
docker compose --env-file "environments/$STAGE.env" up -d --no-build --wait
./scripts/smoke.sh 127.0.0.1 4561
docker compose images --quiet | sort > "workshop-notes/images-$STAGE.txt"
docker compose down
done
diff workshop-notes/images-dev.txt workshop-notes/images-test.txt
diff workshop-notes/images-dev.txt workshop-notes/images-uat.txt
✓ Validation
Dev, test, and UAT pass with the expected stage configuration while every application container retains the same image ID.
◇ Post-check
- Which profile values changed application behavior?
- Which invariant would be violated if CI rebuilt for UAT?
- What identifier should the registry provide so this proof survives beyond one local engine?
Phase 5 — Register and automate
Turn tested local images into traceable, immutable promotion artifacts an orchestrator can pull. 0/35.1 CI: build, test, scan, and push on every commit ~20 min
◆ Pre-check
- What are the minimum stages of a container CI pipeline?
- Why tag the image with the commit SHA inside CI rather than a human tag?
- What should a failing CVE gate do to a build, and what should it not do automatically?
▶ Do
- Complete the supplied CI workflow template so a push builds each image, runs unit tests, scans for CVEs, and pushes
sha-<short>only on success. Do not start from a non-executable sketch. - Open a trivial PR that changes one line, and watch the pipeline run.
- Confirm the new SHA tag lands in the registry only when checks are green.
./scripts/validate-ci-workflow.sh .github/workflows/build.yml
git commit -am "chore: trivial change" && git push
✓ Validation
A green pipeline run publishes a new sha-<short> image per component; a deliberately failing test or scan blocks the push.
◇ Post-check
- Why is the SHA tag the natural handle a later deployment system would consume?
- Where would you cache layers to cut CI time meaningfully?
- Why does provenance still matter even when you trust the developer?
5.2 Label, scan, attest, and publish immutable images ~15 min
◆ Pre-check
- What does an image tag commit you to operationally, versus an image digest?
- Which metadata connects an image to source revision, build, and dependency inventory?
- Why must promotion consume an existing digest rather than invoke another build?
▶ Do
- Add OCI source/revision/version labels, tag each CI-built image by semantic version and commit SHA, and generate an SBOM.
- Apply the vulnerability policy, push passing images, and record the registry digest returned for each component.
- Pull by digest into a clean environment and run the end-to-end smoke test without rebuilding.
SHA=$(git rev-parse --short HEAD)
REGISTRY=<registry-login-server>
docker scout cves gateway-java:sha-$SHA
docker scout sbom gateway-java:sha-$SHA
docker tag gateway-java:sha-$SHA "$REGISTRY/gateway-java:sha-$SHA"
docker push "$REGISTRY/gateway-java:sha-$SHA"
DIGEST=$(docker image inspect "$REGISTRY/gateway-java:sha-$SHA" \
--format '{{index .RepoDigests 0}}' | cut -d@ -f2)
test -n "$DIGEST"
docker pull "$REGISTRY/gateway-java@$DIGEST"
✓ Validation
Every component has source and revision labels, a stored scan and SBOM, immutable registry digest, and recorded results from a clean pull and smoke test.
◇ Post-check
- Why is deploying by digest more deterministic than deploying by tag?
- Which evidence lets an operator map a running container back to source and dependencies?
- What should happen when the vulnerability policy fails after an image has been built?
5.3 Bind the container contract to promotion evidence ~15 min
◆ Pre-check
- What does any orchestrator need to know to run a container: image ref, ports, env, health, resources — what else?
- Why is a container's run contract independent of whether Compose or Kubernetes runs it?
- What will replace Compose's service-name networking once a scheduler is in charge?
▶ Do
- Update
CONTAINER-CONTRACT.mdwith each component's registry digest, ports, required env vars, secret inputs, health endpoints, resource baseline, and shutdown behavior. - Record test, scan, SBOM, provenance, and promotion-gate evidence in
RELEASE-EVIDENCE.md. - Map each runtime field to the Kubernetes object or container-spec field that will satisfy it next.
✓ Validation
The contract names immutable registry digests and everything a scheduler must supply; RELEASE-EVIDENCE.md records the promotion-gate results for those artifacts.
◇ Post-check
- Which run-contract fields will become a container spec, a Service, and a ConfigMap?
- What does Compose give you that a single host never did — and what does it still not give you?
- If a container crashed at 3 a.m. under Compose, who restarts it? Hold that thought for Phase 6.
Phase 6 — Kubernetes: Pods, Deployments, and Services
Express the container contract on a local cluster, then practice reconciliation, rollout, and diagnosis. 0/46.1 Stand up a local cluster and meet kubectl ~15 min
◆ Pre-check
- What are the two halves of a Kubernetes cluster — control plane vs nodes — and what does each do?
- What is
kubectl, and where does it read the cluster address and credentials from? - What is a "context" in a kubeconfig?
▶ Do
- Create a throwaway local cluster with
kind. - Load the exact SHA-tagged local application images into the cluster; a private registry is not automatically authenticated to a local kind cluster.
- Confirm the node is Ready and inspect your current context.
kind create cluster --name z2h
SHA=$(git rev-parse --short HEAD)
kind load docker-image gateway-java:sha-$SHA parser-cpp:sha-$SHA ops-console:sha-$SHA --name z2h
kubectl config current-context
kubectl get nodes -o wide
✓ Validation
kubectl get nodes shows at least one Ready node, and your context points at the local cluster.
◇ Post-check
- What did
kindcreate on your machine to run the cluster? - Which file did your cluster credentials get written into?
- What is the supported client/server version-skew rule you should respect?
6.2 Translate the container contract into Kubernetes workloads ~20 min
◆ Pre-check
- What is the difference between a Pod, a Deployment, and a Service?
- How do two Pods find each other without hard-coding IPs?
- What does a Deployment give you that a bare Pod does not?
▶ Do
- Write a Deployment + Service for the gateway and parser, plus a dev-grade StatefulSet + Service + persistent volume claim for Postgres.
- For kind, reference the exact loaded
sha-<short>tags withimagePullPolicy: IfNotPresent. Implement startup/readiness/liveness probes, CPU/memory requests and limits, graceful termination, and a non-root security context. Phase 7 replaces these local references with ACR digests. - Apply everything, watch the Pods become Ready, and smoke-test through in-cluster Service DNS.
./scripts/validate-k8s-manifests.sh k8s/
kubectl apply -f k8s/
kubectl get pods,svc
kubectl port-forward svc/gateway 4561:4561
./scripts/smoke.sh 127.0.0.1 4561 in a second terminal, and then press Ctrl+C in the first terminal.✓ Validation
All Pods are Ready, run as non-root with declared resources and probes, and carry a message gateway → parser → Postgres through in-cluster Service DNS.
◇ Post-check
- Which Service type did you use, and how would traffic reach it from outside the cluster?
- What replaced Compose's service-name networking?
- Why is a Deployment the right controller for the gateway and parser at this stage?
6.3 Externalize config with ConfigMaps and Secrets ~15 min
◆ Pre-check
- How does your 12-factor config from Phase 2 map onto a ConfigMap and a Secret?
- Why is a Kubernetes Secret not the same thing as "encrypted"?
- Where should these values ultimately come from in production?
▶ Do
- Move per-environment settings into a ConfigMap and credentials into a Secret.
- Reference them from the Deployments via
envFrom/valueFrom. - Change one value and roll the Deployment — no image rebuild.
kubectl create configmap gateway-config --from-env-file=environments/dev.env \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic skybridge-secrets --from-env-file=.env.local \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deploy/gateway deploy/parser
kubectl rollout status deploy/gateway
.env.local must be git-ignored and contain only disposable local-development credentials. Commands can be retained in shell history; do not pass production secrets as command-line literals.✓ Validation
Pods read every setting from the ConfigMap/Secret, and changing a value plus rolling the Deployment takes effect with no rebuild.
◇ Post-check
- Run
base64 -don a Secret value — what does that tell you about its protection? - Which settings stayed constant across environments, and which varied?
- What would inject these from a managed secret store instead of a base64 blob?
6.4 Replace failed Pods, diagnose, scale, roll out, and roll back ~18 min
◆ Pre-check
- What does "reconcile to desired state" mean in practice?
- What does a rolling update do to Pods, one at a time?
- What is the difference between a readiness probe and a liveness probe?
▶ Do
- Delete a parser Pod and watch the Deployment replace it, then scale the parser to 3 replicas.
- Inject a broken image reference or readiness path and diagnose it using workload state, events, description, logs, and metrics.
- Restore the current image, verify readiness, then load and roll out another tested SHA-tagged image and undo the rollout. Registry digests replace these local tags in Phase 7.
kubectl delete pod -l app=parser # watch the Deployment replace it
kubectl scale deploy/parser --replicas=3
./scripts/inject-k8s-failure.sh bad-readiness
kubectl get pods
kubectl describe pod -l app=parser
kubectl get events --sort-by=.lastTimestamp
kubectl logs -l app=parser --all-containers --tail=100
./scripts/reset-k8s-failure.sh
NEXT_SHA=<another-tested-short-sha>
kind load docker-image parser-cpp:sha-$NEXT_SHA --name z2h
kubectl set image deploy/parser parser=parser-cpp:sha-$NEXT_SHA
kubectl rollout status deploy/parser
kubectl rollout undo deploy/parser
✓ Validation
The killed Pod is replaced, the injected failure is diagnosed from recorded evidence, the replica count changes on demand, and a rollout can be restored and undone.
◇ Post-check
- Compare this rollback to your hand-run rollback in 1.3 — what changed in the steps and elapsed time?
- What did the controller do that no runbook could?
- Why do probes have to be right before you trust a rolling update?
Phase 7 — Deploy the workload to AKS
Deploy the same immutable images and workload contract to an Azure-managed cluster. 0/47.1 Provision a basic AKS cluster and connect kubectl ~20 min
◆ Pre-check
- What does AKS operate on your behalf that you ran yourself with
kind? - What is a node pool?
- What does
az aks get-credentialswrite, and where?
▶ Do
- Set the instructor-approved region, verify Azure sign-in, active subscription, regional compute usage, and required permissions; then create a resource group and disposable training AKS cluster.
- Merge its credentials into your kubeconfig.
- Confirm the nodes are Ready and your context now points at AKS.
LOCATION=<approved-region>
az account show --query '{subscription:name,tenant:tenantId}' -o table
az provider show --namespace Microsoft.ContainerService --query registrationState -o tsv
az vm list-usage --location "$LOCATION" -o table
az group create --name rg-z2h --location "$LOCATION" \
--tags purpose=training expires=<YYYY-MM-DD>
az aks create --resource-group rg-z2h --name aks-z2h --location "$LOCATION" \
--node-count 1 --tier free --generate-ssh-keys
az aks get-credentials --resource-group rg-z2h --name aks-z2h --overwrite-existing
kubectl config current-context
kubectl get nodes -o wide
Registered, run az provider register --namespace Microsoft.ContainerService and wait for registration before creating the cluster. This lab intentionally uses one system-pool node to limit evaluation cost. Production workloads should use a properly sized, resilient system pool and separate user node pools.az group delete -n rg-z2h when you're done (see 7.4).✓ Validation
kubectl get nodes against AKS returns Ready nodes, and your current context is the AKS cluster.
◇ Post-check
- Which cluster components does Azure operate?
- What is the supported version-skew rule between your
kubectland the AKS server? - Name one Day-0 default you accepted here that the replatform workshop will make you defend.
7.2 Push images to ACR and let AKS pull them ~18 min
◆ Pre-check
- Why put images in Azure Container Registry rather than a public registry?
- Why does the kubelet need
AcrPullpermission on the registry? - What has to change in your manifests when the registry changes?
▶ Do
- Create an ACR using a globally unique name.
- Tag and push the already-tested local SHA images. Do not rebuild: a new
az acr buildwould create different artifacts and break the promotion proof. - Attach the ACR to the cluster so nodes can pull without a pull secret. This operation creates a role assignment and therefore requires permission such as Owner or User Access Administrator at the applicable scope.
ACR_NAME="acrz2h${RANDOM}${RANDOM}"
SHA=$(git rev-parse --short HEAD)
az acr create --resource-group rg-z2h --name "$ACR_NAME" --sku Basic \
--role-assignment-mode rbac
LOGIN_SERVER=$(az acr show --name "$ACR_NAME" --query loginServer -o tsv)
az acr login --name "$ACR_NAME"
for IMAGE in gateway-java parser-cpp ops-console; do
docker image inspect "$IMAGE:sha-$SHA" >/dev/null
docker tag "$IMAGE:sha-$SHA" "$LOGIN_SERVER/$IMAGE:sha-$SHA"
docker push "$LOGIN_SERVER/$IMAGE:sha-$SHA"
done
az aks update --resource-group rg-z2h --name aks-z2h --attach-acr "$ACR_NAME"
./scripts/set-aks-images.sh "$ACR_NAME" "$SHA" k8s/aks/
--attach-acr is not supported for registries configured for “RBAC Registry + ABAC Repository Permissions.” This lab explicitly creates an RBAC-mode training registry. Production designs should use scoped repository permissions and managed identities.✓ Validation
All three local image IDs were pushed without rebuilding, k8s/aks/ contains each ACR digest reference, and the cluster can pull them without an image pull secret or ImagePullBackOff.
◇ Post-check
- A Pod is stuck
ImagePullBackOff— what are the two most likely AKS-side causes? - How does pulling by digest differ from pulling by tag?
- Where should CVE scanning sit relative to this push — before, after, or both?
7.3 Deploy the workload to AKS and expose it ~20 min
◆ Pre-check
- What does a Service of type
LoadBalancerprovision on AKS? - How does an external client's traffic reach a Pod?
- What is still missing between "reachable" and "production"?
▶ Do
- Inspect the generated AKS overlay and confirm all three images use fully qualified ACR references pinned by
@sha256:digest. - Apply them to AKS and expose the gateway with a
LoadBalancerService. - Get the external IP and smoke-test the cluster.
grep -R '@sha256:' k8s/aks/
kubectl apply -f k8s/aks/
kubectl rollout status deploy/gateway
kubectl get svc gateway -w # press Ctrl+C after EXTERNAL-IP appears
IP=$(kubectl get svc gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
test -n "$IP"
./scripts/smoke.sh "$IP" 4561
✓ Validation
The app answers on the AKS load-balancer IP, and a message travels end to end on the managed cluster.
◇ Post-check
- How many hops did traffic take from client to Pod?
- What did AKS provision in Azure when you asked for a
LoadBalancerService? - Name three things about this deployment that are explicitly not production-ready yet.
7.4 Compare local Kubernetes with managed AKS ~15 min
◆ Pre-check
- Which Kubernetes concepts and workload artifacts remained unchanged between kind and AKS?
- Which capabilities appeared because AKS integrates Kubernetes with Azure?
- Where does responsibility divide between Azure's managed control plane and your workload team?
▶ Do
- Capture cluster, node-pool, ACR, image-digest, Service, and identity facts in
AKS-BASELINE.md. - Map the managed control plane, node pool, Azure Load Balancer, ACR authorization, and workload objects to their owners.
- Keep the cluster running for Phase 8; use the Azure Pricing Calculator to record a dated estimate and its assumptions. Also record the cleanup command.
✓ Validation
AKS-BASELINE.md records the workload contract running on AKS and separates Azure-managed platform responsibilities from application-team responsibilities.
◇ Post-check
- What did AKS remove from your operational responsibility, and what remained yours?
- Which failure would you investigate in Kubernetes, and which in Azure resource state?
- Which Day-0 question in the replatform workshop can you now evaluate with experience?
Phase 8 — Observe, diagnose, and optimize on AKS
Use evidence under load to find a fault, improve one constraint, and verify the result. 0/38.1 Establish an AKS workload baseline under load ~25 min
◆ Pre-check
- Which signals describe demand, saturation, errors, and latency for this workload?
- What can Kubernetes metrics show that application logs cannot, and vice versa?
- Why must an optimization begin with a repeatable baseline?
▶ Do
- Run the supplied load profile against the AKS gateway and record request rate, latency percentiles, errors, CPU, memory, restarts, and replica readiness.
- Correlate application logs with Pod identity and immutable image revision; inspect workload state, events, and node placement.
- Save the load parameters and observations in
AKS-OPERATIONS-REPORT.mdso the exact test can be repeated.
./scripts/load.sh --target "$IP:4561" --profile baseline --duration 5m
kubectl get pods -o wide
kubectl top pods
kubectl get events --sort-by=.lastTimestamp
kubectl logs -l app=gateway --all-containers --since=10m --prefix
✓ Validation
The report contains a repeatable load profile and timestamped baseline for throughput, p50/p95/p99 latency, errors, CPU, memory, restarts, and Ready replicas.
◇ Post-check
- Which component saturated first, and what evidence supports that conclusion?
- Can every log line be tied to a Pod and image revision?
- Which baseline metric is closest to violating the container contract?
8.2 Diagnose controlled AKS workload failures ~25 min
◆ Pre-check
- How do
CrashLoopBackOff, failed readiness, andOOMKilleddiffer? - When should you inspect a Kubernetes event before an application log?
- Which evidence distinguishes a workload defect from an AKS platform-health issue?
▶ Do
- Inject the assigned failure: missing configuration, failed readiness, or memory pressure.
- Follow the diagnostic sequence without skipping steps: get state → describe → events → previous/current logs → metrics → Azure resource health.
- State one falsifiable hypothesis, restore the workload, rerun the load profile, and record the evidence that confirmed or rejected the hypothesis.
./scripts/inject-aks-failure.sh assigned
kubectl get pods
kubectl describe pod -l app=gateway
kubectl get events --sort-by=.lastTimestamp
kubectl logs deploy/gateway --all-pods --previous
kubectl top pods
az aks show -g rg-z2h -n aks-z2h --query provisioningState -o tsv
az aks nodepool show -g rg-z2h --cluster-name aks-z2h -n nodepool1 \
--query '{provisioningState:provisioningState,powerState:powerState.code}' -o table
./scripts/reset-aks-failure.sh
✓ Validation
The report records symptom, evidence, hypothesis, root cause, corrective action, and the results of a passing post-recovery load run for the assigned failure.
◇ Post-check
- Which single piece of evidence most strongly identified the root cause?
- Which signals were symptoms rather than causes?
- Would the same diagnostic sequence work on local Kubernetes? What AKS evidence was additional?
8.3 Change one constraint and compare the result ~25 min
◆ Pre-check
- Why should tuning change one controlled variable at a time?
- How can increasing a resource limit hide rather than solve a defect?
- What result would cause you to reject a proposed optimization?
▶ Do
- Choose one evidence-backed change: resource request/limit, JVM container setting, parser concurrency, probe threshold, or replica count.
- Apply only that change, wait for a healthy rollout, and repeat the exact baseline load profile.
- Compare before/after latency, throughput, errors, CPU, memory, and cost implication; update the container contract and clean up the Azure resource group.
kubectl apply -f k8s/
kubectl rollout status deploy/gateway
./scripts/load.sh --target "$IP:4561" --profile baseline --duration 5m
./scripts/compare-runs.sh baseline optimized
az group delete -n rg-z2h --yes --no-wait
az group wait --name rg-z2h --deleted
az group exists --name rg-z2h # must return false
✓ Validation
AKS-OPERATIONS-REPORT.md contains comparable before/after evidence, accepts or rejects the change, updates the contract, and records Azure cleanup.
◇ Post-check
- Did the change improve the target without degrading another contract measure?
- What did the change do to expected cost and capacity?
- What new evidence should feed the technical design and the production replatform workshop?
Phase 8 complete
You promoted code-complete software as immutable images, expressed the container contract as Kubernetes workloads, deployed those workloads to AKS, diagnosed a failure from evidence, and compared measurements before and after a configuration change. A production implementation must additionally address Day-0 decisions, private networking, zones, workload identity, policy, observability, autoscaling, upgrade strategy, and disaster recovery.
Finished the lab?
Check activity, knowledge, and operational-evidence completion.Complete the assessment
Complete at least 80% of activities, including the Phase 8 operational report, and pass each phase's knowledge check (an increase of at least 10 points, or a qualifying baseline score). The completion page reports whether the activity and knowledge thresholds are met and links to the AKS replatform workshop.