Skip to content

Chapter 8: Kubernetes Configuration

Reyas Khan M edited this page Jun 5, 2025 · 1 revision

Welcome to Chapter 8! In our journey so far, we've built our three microservices: the Shopfront, Product Catalogue, and Stock Manager. We learned how they talk to each other using REST APIs and exchange DTOs. We also mastered packaging each service into portable Docker Images using Dockerfiles, and how to run and manage this multi-container application easily on a single machine using Docker Compose.

Docker Compose is fantastic for local development and testing. But what about running our application in a real-world production environment? This usually involves:

  • Running our services on multiple servers, not just one.
  • Automatically scaling up or down the number of copies (instances) of each service based on demand.
  • Automatically restarting services if they crash or become unhealthy.
  • Rolling out updates to services without downtime.
  • Ensuring services can still find each other reliably even if they are running on different machines and new copies are constantly starting or stopping.

Doing all of this manually across a cluster of machines is incredibly complex. This is where Kubernetes comes in.

The Challenge: Orchestrating Containers in a Cluster

Imagine managing a vast warehouse filled with thousands of those standardized Docker boxes (containers) we talked about. You need to:

  • Decide which boxes go onto which shelf (server).
  • Make sure there are always enough copies of the popular boxes.
  • Replace any box that breaks immediately.
  • Connect boxes that need to talk to each other, even if they are on different shelves.
  • Receive new versions of boxes and swap them out without stopping the whole operation.

This task quickly becomes impossible for a human to do efficiently or reliably at scale.

The Solution: Kubernetes as an Automated Orchestrator

Kubernetes (often shortened to K8s because there are 8 letters between the 'K' and the 's') is a powerful platform for automating the deployment, scaling, and management of containerized applications across a cluster of machines.

Think of Kubernetes as the sophisticated automated warehouse management system. You tell the system what you want the state of your warehouse to be (e.g., "I need 10 copies of the 'Shopfront' box running," "the 'Shopfront' box needs to talk to the 'Product Catalogue' box"), and Kubernetes figures out how to make that happen and continuously works to maintain that desired state.

Instead of giving Kubernetes step-by-step instructions ("start container A, then start container B, then connect them"), you give it a declarative configuration describing the end result you want. This configuration is typically written in YAML files.

These YAML files are like the instruction manuals for the automated warehouse system. They tell K8s about the different parts of your application and how they should behave in the cluster.

Key Concepts in Kubernetes Configuration (YAML)

Kubernetes uses several core building blocks, configured via YAML, to manage your applications:

  1. Pods: The smallest deployable unit in Kubernetes. A Pod is a group of one or more containers that are deployed together on the same machine and share resources like network and storage. For most simple microservices, a Pod contains just one container (your application). Think of a Pod as the minimal unit that must stick together – like putting the main application box and any tiny required accessories box together on a single hand truck within the warehouse.

    # This is NOT a full file, just showing a Pod definition structure
    apiVersion: v1
    kind: Pod # We are defining a Pod
    metadata:
      name: my-app-pod
    spec:
      containers:
      - name: my-app-container
        image: my-docker-image:latest # Which Docker image to run
        ports:
        - containerPort: 8080 # Which port the container listens on
  2. Deployments: This is how you manage stateless applications (like our microservices). A Deployment tells Kubernetes how to create and update Pods. You specify which Docker image to use, how many replicas (copies) of the Pod you want running, and update strategies. Deployments ensure that the desired number of Pods are always running and handle replacing Pods if they fail. Think of a Deployment as the instruction for the warehouse manager: "Always keep 5 'Shopfront' hand trucks (Pods) ready to go using this specific blueprint (Image). If one breaks, replace it automatically."

    # This is NOT a full file, just showing a Deployment structure
    apiVersion: apps/v1
    kind: Deployment # We are defining a Deployment
    metadata:
      name: my-app-deployment
    spec:
      replicas: 3 # I want 3 copies (Pods) of my application running
      selector:
        matchLabels:
          app: my-app # How this Deployment finds the Pods it manages
      template: # This is the blueprint for the Pods this Deployment creates
        metadata:
          labels:
            app: my-app # Labels applied to the Pods
        spec:
          containers:
          - name: my-app-container
            image: my-docker-image:latest
            ports:
            - containerPort: 8080

    Notice the template section looks like a Pod definition nested inside the Deployment. The selector matches the labels in the template to connect the Deployment to its Pods.

  3. Services: Pods are ephemeral – they can be created, killed, and recreated with new IP addresses. How do other Pods (or external users) find them reliably? Kubernetes Services solve this. A Service provides a stable way to access a set of Pods (usually defined by matching labels). It gives them a consistent IP address and DNS name within the cluster, even as the underlying Pods change. Think of a Service as the stable internal phone number or public loading dock for a specific department (a group of Pods). You call the number/go to the dock, and the Service routes you to one of the available team members/hand trucks (Pods).

    # This is NOT a full file, just showing a Service structure
    apiVersion: v1
    kind: Service # We are defining a Service
    metadata:
      name: my-app-service # The stable name for this service (used for discovery)
    spec:
      selector:
        app: my-app # This Service targets Pods with the label 'app: my-app'
      ports:
      - protocol: TCP
        port: 80 # The port this Service exposes
        targetPort: 8080 # The port the Pods are listening on

    The selector connects the Service to the relevant Pods (those managed by the Deployment, because their labels match). The port is the port the Service itself listens on, and targetPort is the port inside the Pods that the Service should forward traffic to.

Configuring Our Microservices for Kubernetes

Now let's see how these concepts apply to our specific project. We need YAML files to define a Deployment and a Service for each of our three microservices. These files are typically stored in a kubernetes directory.

Stock Manager Configuration

Let's look at the kubernetes/stockmanager-service.yaml file. It actually defines two things: a Service and a Deployment for the Stock Manager. Kubernetes YAML files can often contain definitions for multiple resources separated by ---.

# kubernetes/stockmanager-service.yaml
--- # This separates the Service definition from the Deployment definition
apiVersion: v1
kind: Service # First, define the Service for Stock Manager
metadata:
  name: stockmanager # The stable name: 'stockmanager'
  labels:
    app: stockmanager # Labels for organizing and selecting
spec:
  type: NodePort # How the Service is exposed (NodePort for external access)
  selector:
    app: stockmanager # This Service targets Pods with label 'app: stockmanager'
  ports:
  - protocol: TCP
    port: 8030 # The Service port (what other services/users call)
    name: http
    # targetPort defaults to port if not specified, so it will be 8030 too
    # nodePort is automatically assigned if type is NodePort and nodePort isn't set

---
apiVersion: apps/v1 # Version for Deployment kind
kind: Deployment # Second, define the Deployment for Stock Manager
metadata:
  name: stockmanager # The name of the Deployment
spec:
  selector:
    matchLabels:
      app: stockmanager # This Deployment manages Pods with label 'app: stockmanager'
  replicas: 1 # Start with 1 copy (Pod) of the Stock Manager
  template: # The blueprint for the Pods
    metadata:
      labels:
        app: stockmanager # Apply the label 'app: stockmanager' to Pods created
    spec:
      containers:
      - name: stockmanager # Name of the container inside the Pod
        image: danielbryantuk/djstockmanager:latest # Use this Docker image
        ports:
        - containerPort: 8030 # The container listens on port 8030 (internal to Pod)
        livenessProbe: # Configuration for health checks
          httpGet: # How to check health: make an HTTP GET request
            path: /health # The health check endpoint path
            port: 8030 # The port to check on
          initialDelaySeconds: 30 # Wait 30 seconds before the first check
          timeoutSeconds: 1 # Consider the check failed if no response after 1 second
  • Service: Creates a stable name (stockmanager) and IP address for the Stock Manager. It uses the app: stockmanager label selector to find the correct Pods. It exposes port 8030. type: NodePort means K8s will also expose this service on a high, static port across all nodes in the cluster, potentially allowing external access (though typically an Ingress is used for production).
  • Deployment: Ensures that one Pod (replicas: 1) with the label app: stockmanager is running. The Pod template specifies to use the danielbryantuk/djstockmanager:latest Docker image and that the container inside listens on port 8030. The livenessProbe tells K8s how to check if a running Stock Manager Pod is still healthy; if the /health endpoint doesn't respond on port 8030, K8s will consider the Pod unhealthy and restart it.

Product Catalogue Configuration

The kubernetes/productcatalogue-service.yaml is very similar:

# kubernetes/productcatalogue-service.yaml
---
apiVersion: v1
kind: Service # Define the Service for Product Catalogue
metadata:
  name: productcatalogue # Stable name: 'productcatalogue'
  labels:
    app: productcatalogue
spec:
  type: NodePort # Exposed via NodePort
  selector:
    app: productcatalogue # Targets Pods with label 'app: productcatalogue'
  ports:
  - protocol: TCP
    port: 8020 # Service port
    name: http

---
apiVersion: apps/v1
kind: Deployment # Define the Deployment for Product Catalogue
metadata:
  name: productcatalogue
spec:
  selector:
    matchLabels:
      app: productcatalogue # Manages Pods with label 'app: productcatalogue'
  replicas: 1 # Start with 1 copy
  template:
    metadata:
      labels:
        app: productcatalogue # Apply label to Pods
    spec:
      containers:
      - name: productcatalogue # Container name
        image: danielbryantuk/djproductcatalogue:latest # Docker image
        ports:
        - containerPort: 8020 # Container listens on 8020
        livenessProbe: # Health check config
          httpGet:
            path: /healthcheck # Note: Product Catalogue health check is on /healthcheck
            port: 8025 # Note: Product Catalogue health check is on admin port 8025
          initialDelaySeconds: 30
          timeoutSeconds: 1
  • Service: Provides the stable name productcatalogue, targets Pods with the matching label, and exposes port 8020.
  • Deployment: Ensures one Pod with the correct label is running, using the specified Docker image and exposing container port 8020. Note the livenessProbe path (/healthcheck) and port (8025) match the Dropwizard admin port setup we saw in Chapter 2.

Shopfront Configuration

Finally, the kubernetes/shopfront-service.yaml for the user-facing service:

# kubernetes/shopfront-service.yaml
---
apiVersion: v1
kind: Service # Define the Service for Shopfront
metadata:
  name: shopfront # Stable name: 'shopfront'
  labels:
    app: shopfront
spec:
  type: NodePort # Exposed via NodePort (allows external access)
  selector:
    app: shopfront # Targets Pods with label 'app: shopfront'
  ports:
  - protocol: TCP
    port: 8010 # Service port (what users call, and other services *could* call)
    name: http
    # nodePort will be assigned automatically for external access

---
apiVersion: apps/v1
kind: Deployment # Define the Deployment for Shopfront
metadata:
  name: shopfront
spec:
  selector:
    matchLabels:
      app: shopfront # Manages Pods with label 'app: shopfront'
  replicas: 1 # Start with 1 copy
  template:
    metadata:
      labels:
        app: shopfront # Apply label to Pods
    spec:
      containers:
      - name: djshopfront # Container name
        image: danielbryantuk/djshopfront:latest # Docker image
        ports:
        - containerPort: 8010 # Container listens on 8010
        livenessProbe: # Health check config
          httpGet:
            path: /health # Health check endpoint
            port: 8010 # Health check port
          initialDelaySeconds: 30
          timeoutSeconds: 1
  • Service: Provides the stable name shopfront, targets Pods with the matching label, and exposes port 8010. Since this is the user-facing service, type: NodePort is important here as it makes the Service accessible from outside the Kubernetes cluster on a specific port of the cluster nodes.
  • Deployment: Ensures one Pod with the correct label is running, using the specified Docker image and exposing container port 8010. Includes a health check on /health at port 8010.

How Kubernetes Enables Service Discovery

Remember the Shopfront's configuration from Chapter 1 and Chapter 4:

# shopfront/src/main/resources/application.properties
productCatalogueUri = http://productcatalogue:8020
stockManagerUri = http://stockmanager:8030

Just like with Docker Compose, the Shopfront needs to find the other services using these names. Kubernetes provides DNS-based Service Discovery automatically.

When you create a Kubernetes Service with metadata.name: productcatalogue, Kubernetes automatically creates a DNS entry within the cluster for productcatalogue. Any Pod in the same namespace (a way to organize resources in K8s) can resolve the hostname productcatalogue to the IP address of the productcatalogue Service. The Service then routes the traffic to a healthy Pod managed by the productcatalogue Deployment.

sequenceDiagram
    participant ShopfrontPod as Shopfront Pod
    participant K8sDNS as Kubernetes DNS
    participant K8sServicePC as Product Catalogue Service
    participant ProductCataloguePod as Product Catalogue Pod(s)

    ShopfrontPod->>K8sDNS: Lookup hostname "productcatalogue"
    K8sDNS-->>ShopfrontPod: Return IP address of Product Catalogue Service
    ShopfrontPod->>K8sServicePC: Send HTTP request to Service IP on port 8020
    K8sServicePC->>ProductCataloguePod: Forward request to one of the Product Catalogue Pods on port 8020
    ProductCataloguePod-->>K8sServicePC: Send HTTP response
    K8sServicePC-->>ShopfrontPod: Forward HTTP response
Loading

The same flow happens when the Shopfront Pod calls stockmanager:8030. This seamless internal routing means the Shopfront code doesn't need to change whether it's running locally via Docker Compose or in a Kubernetes cluster!

Deploying to Kubernetes

With these YAML files ready, deploying the application to a Kubernetes cluster is straightforward using the kubectl command-line tool (the main way to interact with K8s clusters).

Assuming you have kubectl configured to point to your cluster (like a local Minikube or a cloud cluster), you would simply apply the configuration files:

kubectl apply -f kubernetes/stockmanager-service.yaml
kubectl apply -f kubernetes/productcatalogue-service.yaml
kubectl apply -f kubernetes/shopfront-service.yaml

Output:

service/stockmanager created
deployment.apps/stockmanager created
service/productcatalogue created
deployment.apps/productcatalogue created
service/shopfront created
deployment.apps/shopfront created

When you run these commands, you tell Kubernetes:

  1. "Create a Service and Deployment for Stock Manager as described in this file."
  2. "Create a Service and Deployment for Product Catalogue as described in this file."
  3. "Create a Service and Deployment for Shopfront as described in this file."

Kubernetes then takes over:

  • It creates the Service objects, setting up internal DNS entries.
  • It creates the Deployment objects.
  • Each Deployment sees it needs replicas: 1 and starts creating a Pod using the specified Docker image.
  • It pulls the Docker images (e.g., danielbryantuk/djshopfront:latest) from a registry if they aren't already on the cluster node.
  • It schedules the Pods onto available nodes in the cluster.
  • Once the Pods start, the Deployments monitor them. If a Pod becomes unhealthy (fails the livenessProbe), the Deployment automatically kills it and starts a replacement.
  • The Services automatically detect the healthy Pods matching their label selectors and start routing traffic to them.

You can then check the status using commands like kubectl get pods, kubectl get deployments, and kubectl get services.

To access the Shopfront from outside the cluster (because we used type: NodePort), you would typically find the external IP of a node and the specific nodePort assigned to the shopfront Service (using kubectl get services shopfront).

Benefits of Kubernetes Configuration

Using YAML files to configure Kubernetes provides major advantages for managing applications in a cluster:

  • Declarative: You describe the desired state, and K8s makes it happen.
  • Automated Management: K8s handles deployment, scaling, self-healing, and updates.
  • Service Discovery: Services provide stable internal access via DNS names.
  • Abstraction: You deploy applications without needing to know the specifics of the underlying machines.
  • Scalability: Easily change the replicas count in the Deployment YAML or via commands to scale the number of Pods.

While setting up the initial YAML files can seem complex, they provide a robust and repeatable way to define and manage your application's presence in a distributed cluster environment, far exceeding the capabilities of Docker Compose for production use cases.

Conclusion

In this chapter, we transitioned from managing containers on a single machine with Docker Compose to orchestrating them across a cluster using Kubernetes. We learned that Kubernetes uses declarative YAML configuration to define the desired state of our application. We were introduced to key K8s concepts: Pods (the smallest unit, containing one or more containers), Deployments (managing stateless applications, ensuring desired replicas, and handling updates/self-healing), and Services (providing stable network access and service discovery via DNS).

We examined the YAML files used in the project to define a Service and a Deployment for each microservice (Shopfront, Product Catalogue, Stock Manager), highlighting how labels and selectors connect Services to Deployments/Pods, how container images and ports are specified, and how health checks (livenessProbe) are configured. Crucially, we saw how Kubernetes' built-in DNS service discovery allows the Shopfront to find the other services using their stable Service names (productcatalogue, stockmanager), just like in our Docker Compose setup. Finally, we touched upon how using kubectl apply brings this configuration to life in a Kubernetes cluster.

We now have our application built, containerized, and configured for deployment in a powerful, distributed environment. The final step in our tutorial is to verify that our entire application works correctly from end-to-end when deployed in this manner.

Next Chapter: End-to-End Tests


Doc by Reyas Khan. References: [1], [2], [3]