Translate OrderFlow into Kubernetes

Run OrderFlow locally on Kubernetes with services, storage, probes, and Caddy.

Introduction

30 Second Summary

An online shop can look healthy while one hidden component is unavailable or loses its data. Running every moving part reliably becomes the real challenge as the system grows.

In this project, you will move the existing OrderFlow checkout system from Docker Compose to a local Kubernetes cluster in Docker Desktop. You will end with a working checkout path plus proof that PostgreSQL data survives Pod replacement while Redis rebuilds its cache.

What You'll Build

You will submit an order through a local Caddy gateway, receive an HTTP 202 response, then watch the matching reference appear in Inventory's processed results.

By the end of this project, you'll have:

  • A live Catalog slice that serves K8S-TEE-001 through a Kubernetes Service. An immediate second request proves the cache is working with X-Cache: HIT.
  • A complete asynchronous checkout that enters through Caddy, returns HTTP 202, and appears in Inventory's durable processed results.
  • A storage and health proof showing ten available workloads plus four bound PersistentVolumeClaims. You will also prove that Catalog data survives replacement of its PostgreSQL Pod.
  • Secret Mission: Recreate the disposable Redis Pod, then prove PostgreSQL repopulates the cache with a MISS followed immediately by a HIT.

Are there any prerequisites?

You need the completed OrderFlow Projects 1 through 9 workspace in persistent-checkout.

This lab uses Linux with more than 8 GB of host memory. Step 1 checks the required local tools before you create the cluster.

Before We Start

This is your moment to lock in what OrderFlow needs from Kubernetes before you touch the environment. A clear goal keeps the migration focused on changing the operating model while preserving application behavior.

Set Up Docker Desktop Kubernetes

OrderFlow already runs as ten workloads through Docker Compose. That inherited stack can compete with a local cluster for memory or ports.

This step establishes an isolated Kubernetes control plane in Docker Desktop. A pinned toolchain gives every later manifest the same local target.

In this step, get ready to:
  • Verify the Linux tools required for the migration.
  • Install the pinned kubectl client.
  • Create a single-node local Kubernetes cluster.
Verify the Linux toolchain

Docker Desktop provides the local container engine that powers this lab. Visual Studio Code provides the editor for the manifests you create later.

The initial checks reveal which tools are already available. They also prevent an installation from overwriting a working setup unnecessarily.

  • Check the Docker contexts plus the editor and HTTP client by running these commands:
docker context ls
code --version
curl --version

What do these checks confirm?

  • The Docker context list shows whether the desktop-linux context exists.
  • The code check confirms that Visual Studio Code is available from the terminal.
  • The curl check confirms that the HTTP client is ready for later API tests.
  • Compare your installed Docker Desktop release with 4.90.0 in your Linux package manager.

✔️ Docker Desktop is ready

Your installed release is 4.90.0. The context list also includes desktop-linux.

Your Docker installation is ready for the common startup commands below.

ⓧ I have an older version

The cluster workflow uses Docker Desktop 4.90.0. Upgrade before creating the cluster.

Check the Linux requirements

Docker Desktop for Linux requires KVM plus a supported desktop environment. It also depends on QEMU and systemd.

ⓧ Docker Desktop is missing

The missing command or context means Docker Desktop needs to be installed for this lab.

Check the Linux requirements

Docker Desktop for Linux requires KVM plus a supported desktop environment. It also depends on QEMU and systemd.

This lab creates no cloud charges. Larger-enterprise commercial use of Docker Desktop can require a paid subscription.

✔️ I see both version outputs

Both supporting tools are available. Their version output confirms that the terminal can reach each executable.

ⓧ One or both commands are missing

Install only the supporting tool that failed its check.

  • Install Visual Studio Code by following its official Linux setup guide if the code check failed.
  • Install curl through the package option for your distribution from the official curl download page if the curl check failed.
  • Rerun the failed version check from above after installation.

Still missing a command?

Restart your terminal so it reloads its executable search path. Confirm that you installed the package for your Linux distribution.

If the command remains unavailable, help me troubleshoot the missing Linux command.

Docker Desktop is now installed. The next command starts its user service before selecting the local image store.

  • Start Docker Desktop and select its Linux context by running these commands:
systemctl --user start docker-desktop
docker context use desktop-linux
docker version

What do these commands do?

  • The first command starts Docker Desktop as a service for your Linux user.
  • The second command selects the desktop-linux Docker context.
  • The final command asks the selected engine for its version details.

You should see separate Client and Server sections. That output proves the Docker CLI can reach the running Docker Desktop engine.

Docker Desktop did not start?

  • Check that your Linux installation uses a supported systemd-based setup.
  • Confirm that Docker Desktop finished installing before starting the user service.
  • Review the Docker Desktop window for a startup issue if the Server section is missing.

If the service still fails, help me diagnose Docker Desktop on Linux.

The local engine is active. Stopping the inherited stack now releases its memory and ports while preserving its existing volumes.

  • Return to the existing persistent-checkout workspace in your terminal.
  • Stop the inherited Compose stack by running this command:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml stop

What does this command preserve?

The command stops the containers defined across the three inherited Compose files. Their volumes remain intact for the existing workspace.

That is the first environment boundary secured. The old stack is no longer competing with Kubernetes for local capacity.

Compose stack did not stop?

  • Confirm that the terminal is inside the existing persistent-checkout workspace.
  • Confirm that all three Compose files are present in that workspace.
  • Check that docker version still returns both Client and Server details.

If the command still fails, help me troubleshoot the inherited Compose stack.

Install the pinned kubectl client

The kubectl client sends commands to the Kubernetes control plane. Its version must stay within one minor release of the cluster.

  • Check the installed kubectl client by running this command:
kubectl version --client

What does this check confirm?

The command prints the local client version without requiring a running cluster. This project pins the client to v1.37.0.

✔️ I see the required version

Your client output includes v1.37.0. The Kubernetes command-line client is ready.

ⓧ I see an older version

Install the pinned binary over the older client. Use the architecture that matches your Linux machine.

Linux x86-64

  • Download the x86-64 binary plus its checksum file by running these commands:
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256)  kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
How is the client installed?
  • The first two commands download the pinned binary plus its matching checksum.
  • The checksum command verifies that the downloaded binary matches the published file.
  • The install command places the executable in /usr/local/bin/kubectl with executable permissions.
  • The final command prints the installed client version.

The checksum line should report kubectl: OK. The final output should identify v1.37.0.

Checksum or installation failed?
  • Download the binary and checksum again if the integrity check fails.
  • Confirm that both files came from the v1.37.0 x86-64 path.
  • Enter your Linux password if sudo requests permission.

If installation still fails, help me install kubectl on Linux x86-64.

Linux ARM64

  • Download the ARM64 binary plus its checksum file by running these commands:
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/arm64/kubectl
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/arm64/kubectl.sha256
echo "$(cat kubectl.sha256)  kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
How is the client installed?
  • The first two commands download the pinned binary plus its matching checksum.
  • The checksum command verifies that the downloaded binary matches the published file.
  • The install command places the executable in /usr/local/bin/kubectl with executable permissions.
  • The final command prints the installed client version.

The checksum line should report kubectl: OK. The final output should identify v1.37.0.

Checksum or installation failed?
  • Download the binary and checksum again if the integrity check fails.
  • Confirm that both files came from the v1.37.0 ARM64 path.
  • Enter your Linux password if sudo requests permission.

If installation still fails, help me install kubectl on Linux ARM64.

ⓧ The command is missing

Install the pinned client from the official Kubernetes download server. Use the architecture that matches your Linux machine.

Linux x86-64

  • Download and install the x86-64 client by running these commands:
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256)  kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
How is the client installed?
  • The first two commands download the pinned binary plus its matching checksum.
  • The checksum command verifies that the downloaded binary matches the published file.
  • The install command places the executable in /usr/local/bin/kubectl with executable permissions.
  • The final command confirms that the client is available.

The checksum line should report kubectl: OK. The final output should identify v1.37.0.

kubectl is still unavailable?
  • Confirm that the checksum succeeded before installing the binary.
  • Confirm that /usr/local/bin is included in your executable search path.
  • Restart your terminal after installation if the command remains unavailable.

If the command is still missing, help me find the installed kubectl client.

Linux ARM64

  • Download and install the ARM64 client by running these commands:
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/arm64/kubectl
curl -LO https://dl.k8s.io/release/v1.37.0/bin/linux/arm64/kubectl.sha256
echo "$(cat kubectl.sha256)  kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
How is the client installed?
  • The first two commands download the pinned binary plus its matching checksum.
  • The checksum command verifies that the downloaded binary matches the published file.
  • The install command places the executable in /usr/local/bin/kubectl with executable permissions.
  • The final command confirms that the client is available.

The checksum line should report kubectl: OK. The final output should identify v1.37.0.

kubectl is still unavailable?
  • Confirm that the checksum succeeded before installing the binary.
  • Confirm that /usr/local/bin is included in your executable search path.
  • Restart your terminal after installation if the command remains unavailable.

If the command is still missing, help me find the installed kubectl client.

Create the local Kubernetes cluster

Docker Desktop can create the local control plane that runs OrderFlow. A single node is enough for this local ten-workload lab.

Cluster creation can take several minutes while Docker Desktop prepares the control-plane components. A short wait during this process is expected.

  • Switch back to the running Docker Desktop application.
  • Open the Docker Desktop resource settings.
  • Set the memory allocation to approximately 8 GB when that leaves enough memory for Linux.
  • Open Kubernetes.
  • Select Create cluster.
  • Choose Kubeadm as the provisioner.
  • Keep the node count at one.
  • Select Create.

Why use Kubeadm?

Kubeadm supports Docker Desktop's Docker image store. It also supports the containerd image store.

The kind provisioner does not support the Docker image store. Kubeadm keeps the local images you build later available to this cluster.

Before you check the cluster, do you expect the new node to be ready immediately after creation?

  • Select the Docker Desktop Kubernetes context and inspect its node by running these commands:
kubectl config use-context docker-desktop
kubectl get nodes

What does this verification prove?

  • The context command points kubectl at the cluster managed by Docker Desktop.
  • The node command asks that cluster for its available nodes.

You should see one node named docker-desktop with a Ready status. That result proves the local control plane is accepting requests.

Node is missing or not ready?

  • Wait for Docker Desktop to finish creating the cluster.
  • Confirm that docker-desktop remains the selected kubectl context.
  • Confirm that Docker Desktop still has enough memory allocated for the cluster.

If the node does not become ready, help me diagnose the Docker Desktop Kubernetes cluster.

That is the environment ready. Docker Desktop now provides an isolated Kubernetes target for the OrderFlow migration.

Your local control plane is ready for workloads. Next up, you will launch a small Catalog slice before moving the entire OrderFlow system.

Launch the Catalog Slice

Your local Kubernetes control plane is ready. The inherited Compose stack is stopped so both environments do not compete for memory or ports.

Migrating all ten workloads at once would make every failure harder to trace. You will build a visible Catalog path first so each infrastructure layer has a clear success signal.

In this step, get ready to:
  • Build three stable local application images from the inherited hardened Dockerfiles.
  • Apply the Kubernetes foundation and Catalog manifests.
  • Seed a product and prove the Catalog cache-aside flow.
Build stable local application images

The single-node cluster needs access to each application image before it can start a Pod. The kubeadm cluster can use images from the selected Docker image store.

  • Return to the terminal in persistent-checkout from the previous step.
  • Open the current persistent-checkout folder in Visual Studio Code by running this command:
code .

What does this command do?

The dot tells Visual Studio Code to use the current terminal directory as its workspace. You should see persistent-checkout at the top of the file sidebar.

  • Select the folder creation control in the Visual Studio Code file sidebar.
  • Name the new folder k8s.

You should now see an empty k8s directory inside persistent-checkout.

These builds can take a few minutes while Docker processes each inherited service. Steady build output means the work is progressing.

  • Build the three application images with stable local tags by running these commands:
docker build -t orderflow-order-service:v1 ./order-service
docker build -t orderflow-inventory-service:v1 ./inventory-service
docker build -t orderflow-catalog-service:v1 ./catalog-service

What do these builds create?

  • Each command builds from one inherited hardened service directory.
  • Each :v1 tag gives the corresponding Deployment a stable local image reference.
  • The Catalog manifest uses imagePullPolicy: Never so Kubernetes uses the local image instead of contacting an image registry.
  • Verify that all three local image references exist by running:
docker image inspect orderflow-order-service:v1 orderflow-inventory-service:v1 orderflow-catalog-service:v1

What should you see?

Docker prints inspection data for all three images without reporting a missing reference. The output confirms that the kubeadm node can use the exact tags referenced by the manifests.

That is the image pipeline ready. All three inherited applications now have stable local identities for Kubernetes.

Image build or inspection failed?

  • Confirm the terminal is inside persistent-checkout so each service directory resolves correctly.
  • Confirm Docker Desktop is still using the desktop-linux context.
  • Check the failed build output for the first Dockerfile stage that did not complete.

Need help isolating the failure?

Apply the foundation and Catalog manifests

A Namespace isolates the OrderFlow resources. A Secret holds the local training credentials.

PersistentVolumeClaims reserve durable storage. Services provide stable in-cluster names for the Deployments behind them.

  • Select the file creation control beside the k8s folder in the Visual Studio Code file sidebar.
  • Name the new file 00-foundation.yaml.
  • Add the OrderFlow namespace and local-development Secret to k8s/00-foundation.yaml by copying this first section:
apiVersion: v1
kind: Namespace
metadata:
  name: orderflow
---
apiVersion: v1
kind: Secret
metadata:
  name: orderflow-secrets
  namespace: orderflow
type: Opaque
stringData:
  ORDER_DB_USER: order_user
  ORDER_DB_PASSWORD: local_order_password
  INVENTORY_DB_USER: inventory_user
  INVENTORY_DB_PASSWORD: local_inventory_password
  CATALOG_DB_USER: catalog
  CATALOG_DB_PASSWORD: catalog
  RABBITMQ_USER: orderflow
  RABBITMQ_PASSWORD: orderflow-local

What does this foundation section do?

  • The orderflow namespace gives every lab resource a dedicated boundary.
  • The orderflow-secrets Secret groups the database and RabbitMQ credentials expected by the inherited applications.
  • These values are local training credentials. Never reuse them outside this lab.
  • Save k8s/00-foundation.yaml.
  • Create the namespace and Secret by running:
kubectl apply -f k8s/00-foundation.yaml

What did Kubernetes create?

The apply output reports the orderflow namespace and orderflow-secrets Secret as created. A later apply updates or preserves these resources from the same manifest.

  • Append the Order and Inventory claims to k8s/00-foundation.yaml with this section:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: order-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: inventory-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi

How are these claims scoped?

  • Each claim requests 1Gi from the cluster's default StorageClass.
  • ReadWriteOnce allows read-write mounting on one node.
  • The two claims reserve future storage for the Order and Inventory databases.
  • Save k8s/00-foundation.yaml.
  • Create the first two claims by applying the expanded manifest:
kubectl apply -f k8s/00-foundation.yaml

What changed in this apply?

Kubernetes keeps the namespace and Secret from the first apply. The output now adds order-data plus inventory-data.

  • Append the Catalog and RabbitMQ claims to k8s/00-foundation.yaml with this final section:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: catalog-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rabbitmq-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi

Why create all four claims now?

The foundation declares the complete storage boundary before every workload exists. catalog-data gains its first consumer in this step.

The remaining claims can stay Pending until their workloads arrive. A default StorageClass can wait for a first consumer before provisioning storage.

  • Save k8s/00-foundation.yaml.
  • Create the final two claims by applying the completed foundation manifest:
kubectl apply -f k8s/00-foundation.yaml

What completes the foundation?

The apply output now includes catalog-data plus rabbitmq-data. The complete foundation has one namespace, one Secret, and four claims.

Foundation manifest rejected?

  • Confirm each resource begins after a line containing exactly three hyphens.
  • Check that every claim uses the orderflow namespace.
  • Compare the indentation under resources with the full file below.

Help me compare my foundation manifest.

✔️ Awesome, I've got everything!

Your foundation file now defines the complete namespace, credential, and storage layer.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Namespace
metadata:
  name: orderflow
---
apiVersion: v1
kind: Secret
metadata:
  name: orderflow-secrets
  namespace: orderflow
type: Opaque
stringData:
  ORDER_DB_USER: order_user
  ORDER_DB_PASSWORD: local_order_password
  INVENTORY_DB_USER: inventory_user
  INVENTORY_DB_PASSWORD: local_inventory_password
  CATALOG_DB_USER: catalog
  CATALOG_DB_PASSWORD: catalog
  RABBITMQ_USER: orderflow
  RABBITMQ_PASSWORD: orderflow-local
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: order-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: inventory-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: catalog-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rabbitmq-data
  namespace: orderflow
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi

The Catalog slice combines PostgreSQL for durable product data with Redis for short-lived cached reads. The Catalog API reaches both dependencies through same-namespace Service names.

  • Select the file creation control beside the k8s folder.
  • Name the new file 10-catalog.yaml.
  • Add the Catalog database Service to k8s/10-catalog.yaml by copying:
apiVersion: v1
kind: Service
metadata:
  name: catalog-db
  namespace: orderflow
spec:
  selector:
    app: catalog-db
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres

What does this Service provide?

The catalog-db Service selects Pods with the matching application label. Catalog can use the short Service name because both resources live in orderflow.

The Service exposes PostgreSQL on port 5432. Its target points to the named container port.

  • Save k8s/10-catalog.yaml.
  • Create the Catalog database Service by running:
kubectl apply -f k8s/10-catalog.yaml

What should the apply report?

The output reports the catalog-db Service as created. It has no selected Pod until you add its Deployment.

  • Append the first half of the Catalog database Deployment to k8s/10-catalog.yaml.
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-db
  namespace: orderflow
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: catalog-db
  template:
    metadata:
      labels:
        app: catalog-db
    spec:
      containers:
        - name: postgres
          image: postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
          env:
            - name: POSTGRES_DB
              value: catalog
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: CATALOG_DB_USER

How is the database identified?

  • The Deployment creates one Pod with the app: catalog-db label used by the Service.
  • Recreate prevents overlapping database Pods during a Deployment replacement.
  • The image digest fixes the exact PostgreSQL image content.
  • The database name is ordinary configuration. The username comes from the Secret.
  • Complete the Catalog database Deployment by appending this section:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: CATALOG_DB_PASSWORD
          ports:
            - name: postgres
              containerPort: 5432
          readinessProbe:
            exec:
              command: [pg_isready, -h, 127.0.0.1, -p, "5432"]
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: catalog-data

How does PostgreSQL become ready and durable?

  • The password comes from CATALOG_DB_PASSWORD in the local Secret.
  • The readiness probe checks the local PostgreSQL listener with pg_isready.
  • The catalog-data claim mounts at /var/lib/postgresql so database files outlive the Pod.
  • Save k8s/10-catalog.yaml.
  • Create the Catalog database Deployment by applying the expanded manifest:
kubectl apply -f k8s/10-catalog.yaml

What starts after this apply?

Kubernetes schedules the PostgreSQL Pod and attaches catalog-data. The readiness probe controls when the Pod becomes ready for Service traffic.

  • Append the Redis Service to k8s/10-catalog.yaml.
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-cache
  namespace: orderflow
spec:
  selector:
    app: catalog-cache
  ports:
    - name: redis
      port: 6379
      targetPort: redis

What does the cache Service provide?

The catalog-cache Service gives the Catalog API a stable Redis hostname. It exposes the named Redis port at 6379.

  • Save k8s/10-catalog.yaml.
  • Create the cache Service by applying the manifest:
kubectl apply -f k8s/10-catalog.yaml

What changed in the cluster?

The apply output now includes the catalog-cache Service. Its backing Redis Pod comes from the next Deployment.

  • Append the Redis Deployment identity to k8s/10-catalog.yaml.
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-cache
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog-cache
  template:
    metadata:
      labels:
        app: catalog-cache
    spec:
      containers:

How does the cache join its Service?

The Deployment selector matches the Pod template label exactly. That same label lets the catalog-cache Service route traffic to the Redis Pod.

  • Complete the Redis Deployment by appending its container and readiness probe:
        - name: redis
          image: redis:8.10.1@sha256:298e5b3bc566bade82f46ad5511777a4a07a294097ce16ada2f6a42be5239df5
          ports:
            - name: redis
              containerPort: 6379
          readinessProbe:
            exec:
              command: [redis-cli, ping]
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 5

How does Kubernetes check Redis?

The readiness probe sends a Redis ping from inside the container. A successful response marks the Pod ready for cache traffic.

This Deployment has no persistent volume. Redis remains a disposable acceleration layer.

  • Save k8s/10-catalog.yaml.
  • Create the Redis Deployment by applying the expanded manifest:
kubectl apply -f k8s/10-catalog.yaml

What starts after this apply?

Kubernetes creates the Redis Pod. The Pod joins the cache Service after its readiness probe succeeds.

  • Append the Catalog API Service to k8s/10-catalog.yaml.
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-service
  namespace: orderflow
spec:
  selector:
    app: catalog-service
  ports:
    - name: http
      port: 8000
      targetPort: http

How will clients reach Catalog?

The catalog-service Service exposes the Catalog API inside the cluster on port 8000. A local port-forward will connect your workstation to this Service.

  • Save k8s/10-catalog.yaml.
  • Create the Catalog API Service by applying the manifest:
kubectl apply -f k8s/10-catalog.yaml

What changed in this apply?

The apply output adds the catalog-service Service. It starts routing only after the Catalog API Pod passes readiness.

  • Append the Catalog API Deployment identity and security context to k8s/10-catalog.yaml.
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-service
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog-service
  template:
    metadata:
      labels:
        app: catalog-service
    spec:
      containers:
        - name: catalog-service
          image: orderflow-catalog-service:v1
          imagePullPolicy: Never
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10001
            runAsGroup: 10001

How does the API use the local image safely?

  • The image tag matches the local Catalog image you built earlier.
  • Never prevents Kubernetes from pulling another image with the same tag.
  • The security context runs the inherited image as UID and GID 10001 without privilege escalation.
  • Complete the Catalog API Deployment by appending its port and health probes:
          ports:
            - name: http
              containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          livenessProbe:
            tcpSocket:
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3

Why use two different probes?

The readiness probe calls the inherited database-backed health route. A failed check removes the Pod from Service endpoints.

The liveness probe checks whether the API listener still accepts TCP connections. A failed check restarts the container.

  • Save k8s/10-catalog.yaml.
  • Create the Catalog API Deployment by applying the completed manifest:
kubectl apply -f k8s/10-catalog.yaml

What completes the Catalog slice?

The apply output adds the catalog-service Deployment. Kubernetes can now start the database, cache, and API as one reachable slice.

The first startup can take a few minutes while Kubernetes creates storage and starts the pinned container images. Pod status changes during this period are expected.

  • Wait for all three Catalog Deployments to become available by running:
kubectl -n orderflow wait --for=condition=available deployment/catalog-db deployment/catalog-cache deployment/catalog-service --timeout=180s

What does the wait confirm?

The command succeeds only after the database, cache, and API Deployments report availability. This confirms that their Pods passed the checks required to serve traffic.

  • Inspect the Catalog Pods, Services, and claims by running:
kubectl -n orderflow get pods,services,pvc

What should you see?

  • The three Catalog Pods show ready containers.
  • The three Catalog Services appear with their cluster addresses and ports.
  • The catalog-data claim reports Bound because PostgreSQL consumes it.
  • The other claims can remain Pending until their first consumers arrive.

Catalog workloads did not become available?

  • Confirm the local image name is exactly orderflow-catalog-service:v1.
  • Check that every Service selector matches the corresponding Deployment label.
  • Compare the PostgreSQL and Redis probe indentation with the full file below.

Help me diagnose the unavailable Catalog slice.

✔️ Awesome, I've got everything!

Your Catalog manifest now defines three Services and three available Deployments.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Service
metadata:
  name: catalog-db
  namespace: orderflow
spec:
  selector:
    app: catalog-db
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-db
  namespace: orderflow
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: catalog-db
  template:
    metadata:
      labels:
        app: catalog-db
    spec:
      containers:
        - name: postgres
          image: postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
          env:
            - name: POSTGRES_DB
              value: catalog
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: CATALOG_DB_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: CATALOG_DB_PASSWORD
          ports:
            - name: postgres
              containerPort: 5432
          readinessProbe:
            exec:
              command: [pg_isready, -h, 127.0.0.1, -p, "5432"]
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: catalog-data
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-cache
  namespace: orderflow
spec:
  selector:
    app: catalog-cache
  ports:
    - name: redis
      port: 6379
      targetPort: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-cache
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog-cache
  template:
    metadata:
      labels:
        app: catalog-cache
    spec:
      containers:
        - name: redis
          image: redis:8.10.1@sha256:298e5b3bc566bade82f46ad5511777a4a07a294097ce16ada2f6a42be5239df5
          ports:
            - name: redis
              containerPort: 6379
          readinessProbe:
            exec:
              command: [redis-cli, ping]
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 5
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-service
  namespace: orderflow
spec:
  selector:
    app: catalog-service
  ports:
    - name: http
      port: 8000
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-service
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog-service
  template:
    metadata:
      labels:
        app: catalog-service
    spec:
      containers:
        - name: catalog-service
          image: orderflow-catalog-service:v1
          imagePullPolicy: Never
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10001
            runAsGroup: 10001
          ports:
            - name: http
              containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          livenessProbe:
            tcpSocket:
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
Seed the product and prove caching

Port forwarding creates a temporary localhost path to the private Catalog Service. This lets you test the API without exposing it outside the cluster.

The inherited Catalog uses a cache-aside pattern. A cold read loads the product from PostgreSQL before Redis accelerates the next read.

  • Start the Catalog port-forward in the Visual Studio Code integrated terminal by running:
kubectl -n orderflow port-forward service/catalog-service 8002:8000

What does the port-forward do?

The command maps local port 8002 to port 8000 on the Catalog Service. Keep this terminal running while you send requests.

Press Ctrl+C when you need to stop the forwarding process.

  • Create a second integrated terminal in Visual Studio Code for the Catalog requests.
  • Check whether K8S-TEE-001 already exists by running:
curl --include http://127.0.0.1:8002/products/K8S-TEE-001

How should you use this response?

An HTTP 404 means the product still needs to be seeded. An HTTP 200 means the persistent Catalog database already contains it from an earlier run.

  • Create the product with this request only when the first request returns HTTP 404:
curl --include --header "Content-Type: application/json" --data '{"sku":"K8S-TEE-001","name":"Kubernetes Tee","price_cents":3200}' http://127.0.0.1:8002/products

What does this request store?

The request creates product K8S-TEE-001 with the name Kubernetes Tee. Its price is stored as 3200 cents.

Skipping this request after HTTP 200 keeps the step safe to repeat. It avoids creating a duplicate product.

  • Skip the product creation request when the first response returns HTTP 200.
  • Wait for the inherited 15-second cache entry to expire before the final test.

Before you run the next two requests, which cache header do you expect on the cold read? Which header should replace it on the immediate second read?

  • Request the product twice in immediate succession by running:
curl --include http://127.0.0.1:8002/products/K8S-TEE-001
curl --include http://127.0.0.1:8002/products/K8S-TEE-001

What proves cache-aside behavior?

The first response returns HTTP 200 with X-Cache: MISS. Catalog reads the durable product from PostgreSQL before filling Redis.

The immediate second response returns HTTP 200 with X-Cache: HIT. Redis served the cached product within its 15-second lifetime.

You have your first live Kubernetes application path. Catalog now returns durable product data through a Service and proves that Redis accelerates repeated reads.

Missing the expected MISS and HIT?

  • Confirm the port-forward terminal is still running.
  • Allow the existing 15-second cache entry to expire before starting the two-request test again.
  • Send the second request immediately after the first so the cached entry remains valid.

Help me diagnose my Catalog cache headers.

The Catalog slice gives you a working reference path before the migration expands. Next, you will deploy the remaining OrderFlow workloads and trace the configuration boundary that Compose previously handled.

Deploy the Remaining Workloads

The Catalog slice now serves K8S-TEE-001 through your local Kubernetes cluster. That working path gives you a reliable control while you migrate the rest of OrderFlow.

Docker Compose previously supplied runtime values to the remaining applications. This step translates their resources before every required value is available so you can diagnose the resulting configuration boundary.

In this step, get ready to:
  • Create the manifest for Order, Inventory, RabbitMQ, and their databases.
  • Apply the incomplete runtime model to expose the intended configuration failure.
  • Trace the failure through Pod events while confirming Catalog remains available.
Create the ordering manifest

A Service gives a workload a stable network identity inside the namespace. A Deployment keeps its requested Pod replica running.

  • Select the k8s directory in the VS Code file sidebar.
  • Use the file creation control in the sidebar.
  • Enter 20-ordering.yaml as the file name.

The new file appears beside 00-foundation.yaml and 10-catalog.yaml. It will hold the remaining five Services and six Deployments.

  • Use the tabs below to add the complete ordering manifest to k8s/20-ordering.yaml.

✔️ Awesome, I've got everything!

Great. Confirm that k8s/20-ordering.yaml contains the database resources, RabbitMQ resources, API resources, and Inventory worker.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Service
metadata:
  name: order-db
  namespace: orderflow
spec:
  selector:
    app: order-db
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-db
  namespace: orderflow
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: order-db
  template:
    metadata:
      labels:
        app: order-db
    spec:
      containers:
        - name: postgres
          image: postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
          env:
            - name: POSTGRES_DB
              value: orders
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: ORDER_DB_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: ORDER_DB_PASSWORD
          ports:
            - name: postgres
              containerPort: 5432
          readinessProbe:
            exec:
              command: [pg_isready, -h, 127.0.0.1, -p, "5432"]
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: order-data
---
apiVersion: v1
kind: Service
metadata:
  name: inventory-db
  namespace: orderflow
spec:
  selector:
    app: inventory-db
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inventory-db
  namespace: orderflow
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: inventory-db
  template:
    metadata:
      labels:
        app: inventory-db
    spec:
      containers:
        - name: postgres
          image: postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
          env:
            - name: POSTGRES_DB
              value: inventory
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_PASSWORD
          ports:
            - name: postgres
              containerPort: 5432
          readinessProbe:
            exec:
              command: [pg_isready, -h, 127.0.0.1, -p, "5432"]
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: inventory-data
---
apiVersion: v1
kind: Service
metadata:
  name: rabbitmq
  namespace: orderflow
spec:
  selector:
    app: rabbitmq
  ports:
    - name: amqp
      port: 5672
      targetPort: amqp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  namespace: orderflow
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: rabbitmq
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      hostname: rabbitmq
      containers:
        - name: rabbitmq
          image: rabbitmq:4.3.5@sha256:1773ab33af6af1bbae4a3b1070472c863e669806a9ac63a627c02a44b3a59dcf
          env:
            - name: RABBITMQ_DEFAULT_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_USER
            - name: RABBITMQ_DEFAULT_PASS
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_PASSWORD
          ports:
            - name: amqp
              containerPort: 5672
          readinessProbe:
            tcpSocket:
              port: amqp
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          volumeMounts:
            - name: data
              mountPath: /var/lib/rabbitmq
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: rabbitmq-data
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: orderflow
spec:
  selector:
    app: order-service
  ports:
    - name: http
      port: 8000
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: orderflow-order-service:v1
          imagePullPolicy: Never
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10001
            runAsGroup: 10001
          env:
            - name: PGHOST
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: ORDER_DB_HOST
            - name: PGPORT
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: POSTGRES_PORT
            - name: PGDATABASE
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: ORDER_DB_NAME
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: ORDER_DB_USER
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: ORDER_DB_PASSWORD
            - name: RABBITMQ_DEFAULT_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_USER
            - name: RABBITMQ_DEFAULT_PASS
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_PASSWORD
          ports:
            - name: http
              containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          livenessProbe:
            tcpSocket:
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: inventory-service
  namespace: orderflow
spec:
  selector:
    app: inventory-service
  ports:
    - name: http
      port: 8000
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inventory-service
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: inventory-service
  template:
    metadata:
      labels:
        app: inventory-service
    spec:
      containers:
        - name: inventory-service
          image: orderflow-inventory-service:v1
          imagePullPolicy: Never
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10001
            runAsGroup: 10001
          env:
            - name: PGHOST
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: INVENTORY_DB_HOST
            - name: PGPORT
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: POSTGRES_PORT
            - name: PGDATABASE
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: INVENTORY_DB_NAME
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_USER
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_PASSWORD
          ports:
            - name: http
              containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          livenessProbe:
            tcpSocket:
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inventory-worker
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: inventory-worker
  template:
    metadata:
      labels:
        app: inventory-worker
    spec:
      containers:
        - name: inventory-worker
          image: orderflow-inventory-service:v1
          imagePullPolicy: Never
          command: [python, worker.py]
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10001
            runAsGroup: 10001
          env:
            - name: PGHOST
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: INVENTORY_DB_HOST
            - name: PGPORT
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: POSTGRES_PORT
            - name: PGDATABASE
              valueFrom:
                configMapKeyRef:
                  name: orderflow-app-config
                  key: INVENTORY_DB_NAME
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_USER
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: INVENTORY_DB_PASSWORD
            - name: RABBITMQ_DEFAULT_USER
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_USER
            - name: RABBITMQ_DEFAULT_PASS
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: RABBITMQ_PASSWORD

How is this manifest organized?

  • The database Services preserve the hostnames used by the inherited applications.
  • The database Deployments mount order-data or inventory-data at /var/lib/postgresql.
  • The RabbitMQ Deployment mounts rabbitmq-data at /var/lib/rabbitmq.
  • The API Deployments use the local application images with non-root security settings.
  • The Inventory worker uses the Inventory image with command: [python, worker.py].
  • The application Deployments read ordinary runtime settings through configMapKeyRef. Credentials continue to come from orderflow-secrets.
  • Save k8s/20-ordering.yaml.
Apply the incomplete runtime model

The manifest now describes the remaining network endpoints and workloads. Applying it lets the control plane evaluate their images, storage, credentials, and runtime configuration.

Before you run this, do you think all six new Deployments have everything they need to start?

  • Apply the ordering manifest and inspect the initial resource states by running:
kubectl apply -f k8s/20-ordering.yaml
kubectl -n orderflow get pods
kubectl -n orderflow get pvc

What do these commands prove?

  • The first command creates or updates every resource declared in k8s/20-ordering.yaml.
  • The Pod listing shows which workloads can complete their startup configuration.
  • The claim listing shows whether the database and RabbitMQ workloads caused their storage claims to bind.

The Order and Inventory databases can initialize from the existing Secret. RabbitMQ can also initialize with its existing credentials.

The configuration gap is intentional

The three application Pods remain blocked because their non-optional references require the missing orderflow-app-config ConfigMap. Kubernetes stops those containers before application startup.

The database and broker Pods already have the configuration they need. Their PersistentVolumeClaims can become bound as the workloads consume them.

  • Confirm that the Pod listing shows blocked Order, Inventory, and worker Pods.
  • Confirm that the database and RabbitMQ Pods can progress toward readiness.
Trace the failure and verify isolation

Pod events record how the control plane interpreted a workload definition. They distinguish a missing configuration dependency from an image or networking problem.

Before you inspect the Order Pod, which configuration object do you expect its events to name?

  • Inspect the Order Pod events by running:
kubectl -n orderflow describe pod -l app=order-service

What does this command inspect?

  • The selector app=order-service targets the Pod created by the Order Deployment.
  • The description includes the Pod specification and its recorded startup events.
  • An event naming orderflow-app-config connects the blocked container to its missing runtime configuration.

You should see an event identifying orderflow-app-config as the missing dependency. This proves the intended failure comes from configuration instead of the local image or cluster network.

Does the event identify a different problem?

  • Confirm that your current Kubernetes context is docker-desktop.
  • Check that the Order Deployment in k8s/20-ordering.yaml contains the configMapKeyRef entries from the full-file reference.
  • Save the manifest before applying it again.
  • Ask for help with the event details: Help me diagnose why my Order Pod events do not identify the missing orderflow-app-config ConfigMap..

Only Pods that reference the missing object are affected. The Catalog slice provides a control because its dependencies already exist.

  • Return to the Catalog port-forward terminal from earlier.
  • Leave the existing port-forward running if it still shows connection logs.
  • Restart the Catalog connection with this command if the shell prompt has returned:
kubectl -n orderflow port-forward service/catalog-service 8002:8000

What does this connection do?

The port-forward maps local port 8002 to port 8000 on service/catalog-service. Keep this process running while you test Catalog from a separate terminal.

Before you send the final request, do you expect the unrelated Catalog slice to remain available?

  • Request the existing product through the Catalog port-forward by running:
curl --include http://127.0.0.1:8002/products/K8S-TEE-001

What does this request verify?

The request enters through the Catalog Service while the new application Pods remain blocked. A successful response proves the existing Catalog path is isolated from the incomplete ordering configuration.

You should receive HTTP 200 with the original K8S-TEE-001 product body. The response can include X-Cache: MISS or X-Cache: HIT according to the state of the 15-second cache entry.

Catalog no longer responds?

  • Confirm that the Catalog port-forward terminal still shows an active process.
  • Check that the Catalog Pods remain ready in the orderflow namespace.
  • Confirm that the request uses local port 8002.
  • Ask for help with the isolated Catalog path: Help me diagnose why the existing Catalog slice stopped responding after I applied k8s/20-ordering.yaml..

You have translated the remaining workloads and isolated their startup failure to one missing configuration object. Next, you will supply that configuration and open the complete system through Caddy.

Configure Services and Open the Gateway

Your Kubernetes Catalog slice still works after the deliberate configuration failure. The remaining applications are blocked because the required ConfigMap does not exist.

This step supplies the missing service addresses and database settings. You will then add Caddy as the single gateway for a complete OrderFlow checkout.

In this step, get ready to:
  • Supply the configuration that unblocks the Order and Inventory applications.
  • Deploy Caddy with routes to the three private APIs.
  • Submit an order through the gateway and find its durable Inventory result.
Repair the missing configuration

ConfigMaps hold ordinary runtime settings such as service addresses and database names. The existing Secret continues to supply the local credentials.

  • In the Visual Studio Code window from earlier, create 30-app-config.yaml inside the existing k8s directory using the file sidebar.

You should see 30-app-config.yaml beside the three existing manifest files.

  • Populate k8s/30-app-config.yaml by copying this manifest:
apiVersion: v1
kind: ConfigMap
metadata:
  name: orderflow-app-config
  namespace: orderflow
data:
  ORDER_DB_HOST: order-db
  ORDER_DB_NAME: orders
  INVENTORY_DB_HOST: inventory-db
  INVENTORY_DB_NAME: inventory
  POSTGRES_PORT: "5432"

What does this configuration do?

  • The database host values use the short names of Services in the same namespace. Kubernetes resolves those names through service DNS.
  • The database name values match the databases initialized by the PostgreSQL Deployments.
  • The shared port tells both applications to connect to PostgreSQL on 5432.
  • Save k8s/30-app-config.yaml.

✔️ Awesome, I've got everything!

Your saved file now defines the five settings required by the blocked application Pods.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: ConfigMap
metadata:
  name: orderflow-app-config
  namespace: orderflow
data:
  ORDER_DB_HOST: order-db
  ORDER_DB_NAME: orders
  INVENTORY_DB_HOST: inventory-db
  INVENTORY_DB_NAME: inventory
  POSTGRES_PORT: "5432"

What should match?

This complete file defines one ConfigMap in the orderflow namespace. Its keys match the references already present in k8s/20-ordering.yaml.

  • Apply the ConfigMap and wait for the three blocked Deployments by running:
kubectl apply -f k8s/30-app-config.yaml
kubectl -n orderflow wait --for=condition=available deployment/order-service deployment/inventory-service deployment/inventory-worker --timeout=180s

What do these commands prove?

The first command creates orderflow-app-config from the manifest. The second command waits until all three application Deployments report Available.

Their Pods can now read ordinary settings from the ConfigMap and credentials from orderflow-secrets.

That configuration gap is closed. Order, Inventory, and the worker can now start successfully.

Applications still blocked?

  • Check that the ConfigMap name is exactly orderflow-app-config.
  • Compare every key in k8s/30-app-config.yaml with the full-file reference above.
  • Confirm the manifest places the ConfigMap in the orderflow namespace.

Still stuck? Help me diagnose why my OrderFlow Deployments cannot read orderflow-app-config.

Add Caddy as the gateway

Caddy gives clients one local entry point. Its configuration forwards each path to the matching Kubernetes Service name.

  • Create 40-gateway.yaml inside the existing k8s directory using the file sidebar.

You should see 40-gateway.yaml beside 30-app-config.yaml in the file sidebar.

  • Use the full-file reference below to populate k8s/40-gateway.yaml.

✔️ Awesome, I've got everything!

  • Save k8s/40-gateway.yaml after confirming it contains the Caddy ConfigMap, Service, and Deployment.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: ConfigMap
metadata:
  name: caddy-config
  namespace: orderflow
data:
  Caddyfile: |
    :8080 {
      log {
        output stdout
        format json
      }

      @health path /gateway-health
      handle @health {
        respond 200
      }

      @catalog path /products /products/*
      handle @catalog {
        reverse_proxy catalog-service:8000
      }

      @orders path /orders /orders/*
      handle @orders {
        reverse_proxy order-service:8000
      }

      @inventory path /inventory /inventory/* /processed-orders /processed-orders/*
      handle @inventory {
        reverse_proxy inventory-service:8000
      }

      handle {
        respond "route not found" 404
      }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: caddy
  namespace: orderflow
spec:
  selector:
    app: caddy
  ports:
    - name: http
      port: 8080
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: caddy
  namespace: orderflow
spec:
  replicas: 1
  selector:
    matchLabels:
      app: caddy
  template:
    metadata:
      labels:
        app: caddy
    spec:
      containers:
        - name: caddy
          image: caddy:2.11.4@sha256:df7f1c2fb114453b951de51a98efc010db1655a92c2e86be6706714e2417a78d
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /gateway-health
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            tcpSocket:
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          volumeMounts:
            - name: caddyfile
              mountPath: /etc/caddy
              readOnly: true
            - name: caddy-data
              mountPath: /data
            - name: caddy-config-data
              mountPath: /config
      volumes:
        - name: caddyfile
          configMap:
            name: caddy-config
            items:
              - key: Caddyfile
                path: Caddyfile
        - name: caddy-data
          emptyDir: {}
        - name: caddy-config-data
          emptyDir: {}

How does the gateway fit together?

  • The Caddyfile returns a direct health response at /gateway-health.
  • The route handlers use service DNS names to forward Catalog, Order, and Inventory requests.
  • The Service exposes Caddy on port 8080 inside the namespace.
  • The Deployment mounts the Caddyfile from caddy-config. Its temporary runtime files use Pod-scoped storage at /data and /config.
  • Copy the complete manifest into k8s/40-gateway.yaml.
  • Save k8s/40-gateway.yaml.
  • Apply the gateway manifest and wait for Caddy by running:
kubectl apply -f k8s/40-gateway.yaml
kubectl -n orderflow wait --for=condition=available deployment/caddy --timeout=180s

What do these commands create?

The manifest creates caddy-config and the caddy Service. It also creates the final Deployment in the ten-workload system.

The wait finishes when Caddy passes its gateway health readiness check. Service traffic can then reach the gateway Pod.

Caddy is now available inside the cluster. A local port-forward makes its Service reachable from your Linux host.

  • Return to the terminal running the Catalog port-forward from earlier.
  • Stop that Catalog port-forward with Ctrl+C.

The next command remains active while you test the gateway. Keep this terminal running during the checkout.

  • Start the Caddy port-forward by running:
kubectl -n orderflow port-forward service/caddy 8080:8080

What does this port-forward do?

The command maps local port 8080 to port 8080 on the Caddy Service. Requests to 127.0.0.1 now enter OrderFlow through its gateway.

Port-forward did not start?

  • Confirm the Caddy wait command completed before starting the port-forward.
  • Check that no other local process already uses port 8080.
  • Confirm your current Kubernetes context still targets docker-desktop.

Need another pair of eyes? Help me diagnose why the Caddy Service port-forward cannot start.

Prove the complete checkout

The active gateway now connects your host to every private API route. A health request and a Catalog request provide a quick routing check before you submit an order.

  • Test the gateway health route and the Catalog route by running these requests in a separate terminal:
curl --include http://127.0.0.1:8080/gateway-health
curl --include http://127.0.0.1:8080/products/K8S-TEE-001

What do these requests check?

The first request checks Caddy directly. The second request proves Caddy can resolve catalog-service and forward the product request.

You should receive an HTTP 200 response from both routes. The product response should contain K8S-TEE-001.

Before you submit the order, predict whether the request can travel through Caddy and reach the asynchronous OrderFlow pipeline.

  • Submit the checkout through Caddy by running:
curl --include --header "Content-Type: application/json" --data '{"client_reference":"k8s-flow-001","sku":"SCALE-001","quantity":1}' http://127.0.0.1:8080/orders

What path does this request take?

Caddy forwards /orders to order-service:8000. The Order API stores the order and publishes its work for Inventory.

The client reference gives you a stable value to find in the processed results.

You should receive HTTP 202. That response confirms the Order API accepted k8s-flow-001 for asynchronous processing.

Before you check Inventory, predict whether the worker has already processed the queued order.

  • Check the durable Inventory results through Caddy by running:
curl --include http://127.0.0.1:8080/processed-orders

What does this final request prove?

Caddy forwards /processed-orders to inventory-service:8000. A result containing k8s-flow-001 proves the request travelled through the gateway, Order API, RabbitMQ, Inventory worker, and Inventory database.

  • Repeat the processed-orders request until the response contains k8s-flow-001.

You have the complete checkout working. OrderFlow now accepts a request through Caddy and exposes its durable Inventory result.

Processed result not visible?

  • Confirm the order request returned HTTP 202 before polling Inventory.
  • Leave the Caddy port-forward running while you repeat the processed-orders request.
  • Check that inventory-worker completed the earlier availability wait.

Still waiting for the result? Help me trace k8s-flow-001 through Caddy, Order, RabbitMQ, and Inventory.

Your ten-workload OrderFlow system now runs behind one gateway. Next, you will test its storage and health boundaries by recreating a database Pod.

Prove Storage and Health Boundaries

Your checkout now enters through Caddy. The Inventory result proves the asynchronous path works.

A working request can still hide weak operational boundaries. This step audits how Kubernetes exposes healthy workloads.

A warm Redis entry could also hide lost database data. The persistence proof therefore expires the cache before trusting the PostgreSQL result.

In this step, get ready to:
  • Audit workload counts, network endpoints, and health probes.
  • Recreate the Catalog database Pod with race-safe waits.
  • Force a database-backed read after the Redis cache expires.
Audit the Kubernetes model

A healthy cluster needs more than running Pods. This audit checks whether the declared workloads have stable networking, bound storage, and probe definitions.

  • Collect the workload, networking, storage, and probe details by running these commands:
kubectl -n orderflow get deployments,services,pods,pvc
kubectl -n orderflow get endpointslices
kubectl -n orderflow describe deployment order-service

What does this audit show?

  • The first command shows the workloads, stable Services, current Pods, and storage claims in the orderflow namespace.
  • The EndpointSlices listing shows the network backends that Kubernetes tracks for the Services.
  • The order-service description exposes the readiness and liveness checks configured on its Deployment.
  • Confirm the output lists ten Deployments.
  • Confirm the output lists nine Services.
  • Confirm four PersistentVolumeClaims report Bound with 1Gi of capacity.
  • Confirm order-service lists an HTTP readiness probe.
  • Confirm order-service lists a TCP liveness probe.

That audit gives you a concrete baseline. All ten workloads have the networking, storage, and health boundaries expected by the manifests.

Recreate PostgreSQL with race-safe waits

The catalog-data PersistentVolumeClaim sits outside the Catalog database Pod. Its stored row survives when the Deployment replaces that Pod.

Deleting a database Pod can feel risky. The next commands target only that Pod while the claim remains in place.

  • Replace the Catalog database Pod and verify its recovery by running these commands:
kubectl -n orderflow delete pod -l app=catalog-db
kubectl -n orderflow wait --for=create --for=condition=Ready pod -l app=catalog-db --timeout=180s
kubectl -n orderflow wait --for=condition=Ready pod -l app=catalog-service --timeout=180s
kubectl -n orderflow get pvc catalog-data

How do the waits prevent a race?

  • The delete command removes only the Pod selected by app=catalog-db.
  • The first wait requires the replacement Pod to exist through --for=create.
  • The same wait requires the replacement Pod to become ready through --for=condition=Ready.
  • The second wait confirms the Catalog API has recovered its database-backed readiness.
  • The final command checks that catalog-data stayed bound during the replacement.
  • Confirm the replacement catalog-db Pod reports Ready.
  • Confirm the catalog-service Pod reports Ready.
  • Confirm catalog-data still reports Bound.

That replacement worked cleanly. The database returned while catalog-data stayed bound through the Pod change.

Replacement Pod not becoming ready?

  • Confirm the label selector uses app=catalog-db.
  • Check that the catalog-db Deployment still has one desired replica.
  • Review the Pod events for a storage or image issue.

Need help tracing the replacement? Help me diagnose why the replacement Catalog database Pod is not becoming ready.

Force a database-backed persistence check

Redis can satisfy a product request without consulting PostgreSQL. The inherited cache entry lasts for 15 seconds.

Before you run the final check, which cache header do you expect after a 16-second wait?

  • Expire the cached product and request it through Caddy by running these commands:
sleep 16
curl --include http://127.0.0.1:8080/products/K8S-TEE-001

Why does this prove persistence?

  • The 16-second pause moves the request beyond the inherited 15-second Redis TTL.
  • The request reaches Catalog through the active Caddy port-forward.
  • A successful response with X-Cache: MISS shows that Catalog had to fetch the product after the cached copy expired.
  • The original product body proves the recreated PostgreSQL Pod read the row from persistent storage.

You'll see HTTP 200 with X-Cache: MISS. The body still contains K8S-TEE-001, Kubernetes Tee, and 3200.

Still seeing a cache hit?

  • Confirm the Caddy port-forward from the previous step is still running at 127.0.0.1:8080.
  • Wait another 16 seconds if the response reports X-Cache: HIT.
  • Repeat the product request immediately after the wait.

Still missing the expected response? Help me diagnose why the Catalog persistence check does not return HTTP 200 with X-Cache: MISS.

That's the hard proof. The product survived the PostgreSQL Pod replacement because its row remained on catalog-data.

Secret mission

Prove Redis Is Disposable

Replace the Catalog Redis Pod without touching PostgreSQL or its persistent claim. Then prove the empty cache repopulates from durable data with a MISS followed immediately by a HIT.

Clean Up Your Resources

Leaving the workloads running consumes local memory and disk space. Larger-enterprise commercial use of Docker Desktop can require a paid subscription.

Nice Work!

Nice Work!

You did it! OrderFlow now runs as a ten-workload system on Docker Desktop Kubernetes.

You've learned how to:

  • Translated the inherited ten-service architecture into ten Deployments. Nine Services now preserve the application's same-namespace hostnames.
  • Recovered application Pods from a deliberately missing ConfigMap. That diagnosis exposed how explicit runtime configuration controls workload startup.
  • Sent an asynchronous checkout through Caddy to a durable Inventory result. Proved PostgreSQL persistence by recreating the Catalog database Pod after cached state expired.
  • Secret Mission: Recreated the disposable Redis Pod. The first product request returned X-Cache: MISS from PostgreSQL. The immediate second request returned X-Cache: HIT from Redis.

Ready to quiz yourself?