19 minute read

Cluster Architecture

A Kubernetes cluster is made up of multiple machines called nodes. These nodes work together to run and manage containerized applications.

The cluster is mainly divided into two parts:

Control Plane (Master Node)

The Control Plane manages the Kubernetes cluster. It keeps track of the cluster’s state, decides where workloads should run, and ensures that the desired state is maintained.

Key components include:

  • kube-apiserver — The central communication point for the Kubernetes cluster. All cluster operations interact with the API server.
  • etcd — A distributed key-value store that stores the cluster’s configuration and state.
  • kube-scheduler — Selects the most suitable worker node for newly created Pods based on resources and scheduling rules.
  • kube-controller-manager — Runs controllers that continuously monitor the cluster and ensure the actual state matches the desired state.

Worker Nodes

Worker nodes are responsible for running application workloads.

Key components include:

  • kubelet — An agent running on each node that communicates with the Control Plane and ensures Pods and containers are running correctly.
  • kube-proxy — Manages network rules that allow communication between services and Pods across the cluster.
  • Container Runtime — Runs containers on the node. Common container runtimes include containerd and CRI-O.

Workloads

Kubernetes Pods

A Pod is the smallest deployable unit in Kubernetes.

A Pod represents a single instance of an application and runs on a Kubernetes Node.

Node
 └── Pod
      └── Container
           └── Application

A Pod can contain one or more containers, but typically a Pod runs a single application container.

Pod Characteristics

1. Shared Network

All containers inside the same Pod:

  • Share the same IP address
  • Share the same network namespace
  • Can communicate with each other using localhost

For example, if Container A listens on port 3000, another container in the same Pod can access it using:

http://localhost:3000

2. Shared Storage

Containers inside a Pod can share storage volumes.

Example:

Pod
├── App Container
│     └── writes logs
│
└── Helper Container
      └── processes logs

This allows containers that work together to share data.

3. Shared Lifecycle

Containers within the same Pod are managed together and share the Pod’s lifecycle.

Multi-container Pods are generally used when containers are closely related and need to work together, such as an application container and a helper/sidecar container.

Scaling with Pods

Kubernetes scales applications by creating more Pods, not by adding more copies of the application container inside one Pod.

1 Pod
  ↓
2 Pods
  ↓
3 Pods
  ↓
4 Pods

For example:

Node 1              Node 2
┌─────────┐         ┌─────────┐
│ Pod     │         │ Pod     │
│ App     │         │ App     │
└─────────┘         └─────────┘

This allows Kubernetes to distribute application instances across available nodes.

Creating a Pod

A simple way to create a Pod is:

kubectl run nginx --image=nginx

Check the Pod:

kubectl get pods

Example:

NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          10s

Initially, the pod might be in a ContainerCreating state, followed by a transition to the Running state as the application container becomes active.

Pod IPs are Temporary

Pods are ephemeral. A Pod can be replaced when it:

  • Crashes
  • Is deleted
  • Runs on a failed node

A replacement Pod can receive a different IP address.

Therefore, applications should not depend directly on a Pod’s IP address.

Instead, Kubernetes Services provide a stable way to access Pods.

Application
     │
     ▼
  Service
     │
     ▼
   Pod(s)

Pod vs Container

Pod Container
Smallest deployable Kubernetes unit Runs the application
Can contain one or more containers Runs inside a Pod
Provides shared network Uses Pod’s network
Can provide shared storage through volumes Can access shared volumes

Pods with YAML

Kubernetes uses YAML files to define and configure objects such as:

  • Pods
  • ReplicaSets
  • Deployments
  • Services

A Kubernetes YAML definition generally has four main top-level fields:

apiVersion:
kind:
metadata:
spec:

1. apiVersion

Defines the Kubernetes API version used by the object.

For a Pod:

apiVersion: v1

2. kind

Defines the type of Kubernetes object.

For a Pod:

kind: Pod

Other examples include Deployment, Service, and ReplicaSet.

3. metadata

Contains information that identifies the object, such as:

  • name
  • labels

Example:

metadata:
  name: myapp-pod
  labels:
    app: myapp
    type: front-end

YAML indentation is important. Fields at the same level must have consistent indentation.

4. spec

Defines the desired configuration of the object.

For a Pod, the spec contains the containers to run.

spec:
  containers:
    - name: nginx-container
      image: nginx

The - indicates that containers is a list. A Pod can contain one or more containers.

Complete Pod YAML

apiVersion: v1
kind: Pod

metadata:
  name: myapp-pod
  labels:
    app: myapp
    type: front-end

spec:
  containers:
    - name: nginx-container
      image: nginx

Save the file, for example:

pod-definition.yaml

Create the Pod

Create the Pod from the YAML file:

kubectl create -f pod-definition.yaml

Verify the Pod

Check the Pod status:

kubectl get pods -n <namespace>

Example:

NAME         READY   STATUS    RESTARTS   AGE
myapp-pod    1/1     Running   0          20s

Inspect the Pod

Use kubectl describe to see detailed information:

kubectl describe pod myapp-pod -n <namespace>

This can show:

  • Pod metadata
  • Node assignment
  • Container details
  • Pod status
  • Events
  • Image information
  • Mounts

The Events section is especially useful when troubleshooting Pod creation or startup problems.

YAML Structure to Remember

apiVersion
    ↓
kind
    ↓
metadata
    ↓
spec
    ↓
containers

Key Points

Kubernetes YAML defines the desired state of an object.

For a Pod, remember the four main fields:

apiVersion:
kind:
metadata:
spec:

Then use kubectl create -f to create the resource and kubectl describe to inspect or troubleshoot it.

ReplicationController and ReplicaSet

Kubernetes can run multiple copies of the same Pod to improve availability and handle more traffic.

If one Pod crashes, Kubernetes should automatically create another one. This is where ReplicationController and ReplicaSet come in.

Why Do We Need Them?

Let’s say our application is running in only one Pod, if that Pod crashes, the application becomes unavailable.

With ReplicationController and ReplicaSet, we can create multiple Pods. If one Pod fails, the controller creates a replacement based on the number in desired.

This provides high availability and also helps distribute application load across multiple Pods.

ReplicationController

A ReplicationController (RC) is an older Kubernetes resource used to make sure a specified number of Pod replicas are running.

For example:

replicas: 3

means Kubernetes should maintain 3 Pods.

If one disappears, the ReplicationController creates a replacement Pod.

ReplicationController is the older technology and has largely been replaced by ReplicaSet.

ReplicationController YAML

apiVersion: v1
kind: ReplicationController

metadata:
  name: myapp-rc

spec:
  replicas: 3

  template:
    metadata:
      labels:
        app: myapp
        type: front-end

    spec:
      containers:
        - name: nginx-container
          image: nginx

The important parts are:

  • apiVersion → API version
  • kind → resource type
  • metadata → name and labels
  • replicas → number of Pods to maintain
  • template → blueprint used to create Pods

Create it with:

kubectl create -f rc-definition.yaml

Check it:

kubectl get replicationcontroller
kubectl get pods

ReplicaSet

A ReplicaSet does the same basic job as a ReplicationController, but it is the newer and preferred resource.

The main differences are ReplicaSet is requires a selector to identify which Pods it manages

ReplicaSet YAML

apiVersion: apps/v1
kind: ReplicaSet

metadata:
  name: myapp-replicaset

spec:
  replicas: 3

  selector:
    matchLabels:
      type: front-end

  template:
    metadata:
      labels:
        type: front-end

    spec:
      containers:
        - name: nginx-container
          image: nginx

Create it:

kubectl create -f replicaset-definition.yaml

Check it:

kubectl get replicaset
kubectl get pods

Labels and Selectors

This is one of the most important concepts to understand with ReplicaSets.

A label is attached to a Pod:

labels:
  tier: front-end

The ReplicaSet uses a selector to find Pods with that label:

selector:
  matchLabels:
    tier: front-end

The ReplicaSet uses this relationship to know which Pods belong to it and which Pods it should maintain.

Scaling a ReplicaSet

Suppose we currently have:

replicas: 3

and want 6 Pods.

Method 1: Change the YAML

Change:

replicas: 6

Then:

kubectl replace -f replicaset-definition.yaml

Method 2: Use kubectl scale

kubectl scale --replicas=6 -f replicaset-definition.yaml

Using kubectl scale changes the running ReplicaSet, but does not automatically update the replica count stored in our YAML file.

ReplicationController vs ReplicaSet

Feature ReplicationController ReplicaSet
Status Older Newer
API version v1 apps/v1
Kind ReplicationController ReplicaSet
Maintains Pod replicas Yes Yes
Uses selector Basic matchLabels
Modern Kubernetes usage Legacy Preferred

Common Commands

# Create from YAML
kubectl create -f <filename>

# View ReplicaSets
kubectl get replicaset

# View ReplicationControllers
kubectl get replicationcontroller

# View Pods
kubectl get pods

# Scale
kubectl scale --replicas=6 -f <filename>

# Replace using YAML
kubectl replace -f <filename>

# Delete a ReplicaSet
kubectl delete replicaset <name>

ReplicationController and ReplicaSet make sure the desired number of Pods are running. ReplicaSet is the newer version and uses labels/selectors to identify the Pods it manages.

Deployments

A Deployment is a Kubernetes object used to manage an application running in multiple Pods.

Instead of managing Pods and ReplicaSets directly, a Deployment manages them for us and provides useful features such as:

  • Running multiple copies of an application
  • High availability
  • Scaling
  • Rolling updates
  • Rollbacks
  • Pause and resume of changes

A simple way to think about it:

Deployment
    |
    v
ReplicaSet
    |
    v
  Pods

The Deployment sits above the ReplicaSet and manages it.

Why Do We Need Deployments?

A ReplicaSet can make sure a certain number of Pods are running.

But what happens when we want to update your application?

For example, you want to change:

nginx:old-version
        |
        v
nginx:new-version

A Deployment makes this process easier by providing rolling updates and rollbacks.

Creating a Deployment

A Deployment YAML looks very similar to a ReplicaSet.

The main difference is:

kind: Deployment

Example:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: myapp-deployment
  namespace: dev     
  labels:
    app: myapp
    type: front-end

spec:
  replicas: 3

  selector:
    matchLabels:
      type: front-end

  template:
    metadata:
      labels:
        app: myapp
        type: front-end

    spec:
      containers:
        - name: nginx-container
          image: nginx

The important parts are:

replicas
   ↓
How many Pods?

selector
   ↓
Which Pods does it manage?

template
   ↓
How should those Pods be created?

Create the Deployment:

kubectl create -f deployment-definition.yaml

Check it:

kubectl get deployments -n <namespace>

Example:

NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE
myapp-deployment   3         3         3            3

What Happens Behind the Scenes?

When we create a Deployment, Kubernetes automatically creates a ReplicaSet.

The ReplicaSet then creates and manages the Pods.

        Deployment
             |
             v
        ReplicaSet
             |
       +-----+-----+
       |     |     |
       v     v     v
     Pod 1 Pod 2 Pod 3

So we don’t need to create the ReplicaSet itself.

We we create the deployment with:

kubectl create -f deployment-definition.yaml

We can see the ReplicaSet with:

kubectl get replicasets

And the Pods with:

kubectl get pods

Deployment vs ReplicaSet

The easiest way to understand the relationship is:

Resource Main responsibility
Pod Runs the application container
ReplicaSet Maintains the desired number of Pods
Deployment Manages ReplicaSets and provides rolling updates/rollbacks

A Deployment manages our application at a higher level than a ReplicaSet.

The important features to remember are:

  • Deployment → manages the application rollout
  • ReplicaSet → maintains the desired number of Pods
  • Pods → run the application
  • Rolling update → gradually update the application
  • Rollback → return to a previous version
  • Pause/Resume → coordinate multiple changes

In short: Deployment = ReplicaSet management + application update and rollback capabilities.

Kubernetes Namespaces

A Namespace is a way to organize and isolate resources inside a Kubernetes cluster.

For example:

Kubernetes Cluster
│
├── default
│   ├── Pods
│   ├── Services
│   └── Deployments
│
├── dev
│   ├── Pods
│   ├── Services
│   └── Deployments
│
└── kube-system
    ├── DNS
    ├── API Server
    └── Scheduler

Namespaces are especially useful when different environments, teams, or applications share the same cluster.

When a Kubernetes cluster is created, the default namespace is available automatically.

If we create a resource without specifying a namespace, it is normally created in the default namespace.

Kubernetes also creates system namespaces:

  • kube-system — contains Kubernetes system components such as DNS and networking components.
  • kube-public — intended for resources that need to be publicly accessible across users.

Why Use Namespaces?

Namespaces help separate resources and allow different policies and resource limits to be applied to different environments.

We can apply different resource limits to each namespace, such as:

  • CPU
  • Memory
  • Number of Pods

This helps prevent one environment from consuming all available cluster resources.

Communicating Between Namespaces

Resources inside the same namespace can refer to each other using their simple names.

For example, a web application can connect to a database Service named db-service.

If the Service is in another namespace, we use its fully qualified DNS name.

For example, if db-service is in the dev namespace:

db-service.dev.svc.cluster.local

The structure is:

<service-name>.<namespace>.svc.cluster.local

List Pods in Default Namespace

kubectl get pods

This lists Pods in the current namespace, which is normally default.

List Pods in Another Namespace

For example, to see Pods in kube-system:

kubectl get pods -n kube-system

Creating a Pod in a Specific Namespace

We can define the namespace directly inside the YAML:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  namespace: dev
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx

The important part is:

metadata:
  namespace: dev

Creating a Namespace

We can use kubectl:

kubectl create namespace dev

Setting the Default Namespace

If we frequently work inside the dev namespace, we can configure our current Kubernetes context to use it by default:

kubectl config set-context $(kubectl config current-context) --namespace=dev

After this:

kubectl get pods

will automatically look for Pods in the dev namespace.

To see resources across all namespaces:

kubectl get pods -A

ResourceQuotas

Namespaces can also have resource limits using a ResourceQuota.

For example:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: dev
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: 5Gi
    limits.cpu: "10"
    limits.memory: 10Gi

This limits how much of these resources can be consumed in the dev namespace.

Create it with:

kubectl create -f compute-quota.yaml

A Namespace provides a way to organize and isolate Kubernetes resources.

Namespaces help keep Kubernetes environments organized and prevent different groups of resources from interfering with each other.

Services

Kubernetes Services

A Kubernetes Service provides a stable way for Pods to communicate with each other and allows applications to be accessed from outside the cluster.

Pods are temporary. Their IP addresses can change when Pods are recreated.

A Service solves this problem by providing a stable endpoint for accessing a group of Pods.

             Service
                |
       +--------+--------+
       |        |        |
       v        v        v
     Pod 1    Pod 2    Pod 3

Instead of connecting directly to a Pod, clients connect to the Service.

Why Do We Need Services?

Imagine that we have a web application running inside a Pod:

Laptop
   |
   X
   |
Pod IP: 10.244.0.2

The Pod is on an internal Kubernetes network, so an external device may not be able to access it directly.

Also, if the Pod is deleted and recreated, its IP can change.

A Service provides a stable way to access the application.

Laptop
   |
   v
Service
   |
   +----> Pod 1
   |
   +----> Pod 2
   |
   +----> Pod 3

The Service can continue directing traffic to the available Pods even when Pods are replaced.

Types of Kubernetes Services

The main Service types covered here are:

Service Type Purpose
ClusterIP Communication inside the cluster (Internal access)
NodePort Exposes the application through a port on a node (External access through Node IP + Port)
LoadBalancer Provides an external load balancer, typically in cloud environments (External load balancer)

NodePort Service

A NodePort Service allows external users to access a Pod through the IP address of a Kubernetes node.

For example:

Laptop
   |
   | 192.168.1.2:30008
   v
+---------+
|  Node   |
+---------+
     |
     v
  Service
     |
     v
    Pod
   :80

The user connects to:

curl http://192.168.1.2:30008

The request is then forwarded to the application running inside the Pod.

The Three Important Ports

NodePort can be confusing because there are three different ports.

External User
     |
     | NodePort
     | 30008
     v
+-----------+
|   Node    |
+-----------+
     |
     | Service port
     | 80
     v
+-----------+
|  Service  |
+-----------+
     |
     | targetPort
     | 80
     v
+-----------+
|    Pod    |
|   :80     |
+-----------+

1. targetPort

The port where the application is actually running inside the Pod.

targetPort: 80

2. port

The port exposed by the Service inside the cluster.

port: 80

3. nodePort

The port exposed on the Kubernetes node for external access.

nodePort: 30008

By default, NodePort values are in the range 30000 - 32767

If targetPort is not specified, it defaults to the same value as port. If nodePort is not specified, Kubernetes automatically assigns one.

Creating a NodePort Service

A basic NodePort Service looks like this:

apiVersion: v1
kind: Service

metadata:
  name: myapp-service

spec:
  type: NodePort

  ports:
    - targetPort: 80
      port: 80
      nodePort: 30008

However, this Service doesn’t yet know which Pods it should send traffic to.

For that, we use a selector.

Connecting a Service to Pods

Suppose our Pod has these labels:

metadata:
  labels:
    app: myapp
    type: front-end

The Service can use the same labels in its selector:

selector:
  app: myapp
  type: front-end

Complete Service:

apiVersion: v1
kind: Service

metadata:
  name: myapp-service

spec:
  type: NodePort

  ports:
    - targetPort: 80
      port: 80
      nodePort: 30008

  selector:
    app: myapp
    type: front-end

The relationship is:

Service
   |
   | selector:
   | app=myapp
   | type=front-end
   |
   +------> Pod 1
   |
   +------> Pod 2
   |
   +------> Pod 3

The Service finds Pods based on their labels.

Creating and Checking the Service

Create the Service:

kubectl create -f service-definition.yaml

Check Services:

kubectl get services -n <namespace>

Example:

NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)
myapp-service    NodePort   10.106.127.123  <none>        80:30008/TCP

The 80:30008/TCP means:

Service Port : NodePort
      80     :   30008

We can then access the application using:

curl http://192.168.1.2:30008

A Service can point to multiple Pods.

For example:

                 Service
                    |
          +---------+---------+
          |         |         |
          v         v         v
        Pod 1     Pod 2     Pod 3

When multiple Pods have matching labels, the Service can send traffic to those Pods.

This is useful for:

  • High availability
  • Distributing traffic
  • Scaling applications

If a Pod is removed or a new matching Pod is created, Kubernetes updates the Service’s available endpoints accordingly.

Services Across Multiple Nodes

Pods don’t have to be on the same node.

For example:

Node 1                  Node 2

+---------+             +---------+
|  Pod 1  |             |  Pod 2  |
+---------+             +---------+
      \                    /
       \                  /
        +----------------+
        |    Service     |
        +----------------+
                |
                v
             Clients

The Service can route traffic to the matching Pods even when they are running on different nodes.

For a NodePort Service, the NodePort can be used through a node’s IP to reach the Service.

Important Concept: Service + Selector

One of the most important things to remember is that a Service uses a selector to find its Pods.

Pod labels
    |
    | app=myapp
    v
Service selector
    |
    v
Matching Pods

If the labels and selector don’t match, the Service won’t find the intended Pods.

Service ClusterIP

A ClusterIP Service provides a stable way for Pods to communicate with other Pods inside the Kubernetes cluster.

This is especially useful in a microservices application where different parts of the application run in different Pods.

For example:

Frontend Pods
     |
     v
Backend Service
     |
     v
Backend Pods
     |
     +----> Redis
     |
     +----> MySQL

Each part of the application can have its own Pods and Service.

Why Do We Need ClusterIP?

Pod IP addresses are not permanent.

When a Pod is deleted and recreated, it can receive a different IP address.

This is why we don’t want an applications to depend directly on individual Pod IPs.

ClusterIP Solves This Problem

Instead of connecting directly to a Pod, we create a Service for the backend Pods.

              Backend Service
              ClusterIP
                  |
        +---------+---------+
        |         |         |
        v         v         v
      Pod 1     Pod 2     Pod 3

The frontend connects to the Service, not directly to a Pod.

The Service provides a stable:

  • Cluster IP
  • DNS name

So even if the backend Pods are recreated or scaled, the frontend can continue using the same Service.

A Service uses a selector to find the Pods it should send traffic to.

For example:

selector:
  app: myapp
  type: back-end

The backend Pods must have matching labels:

labels:
  app: myapp
  type: back-end

The Service then distributes incoming requests among the available matching Pods.

Creating a ClusterIP Service

A ClusterIP Service can be defined like this:

apiVersion: v1
kind: Service

metadata:
  name: back-end

spec:
  type: ClusterIP

  ports:
    - port: 80
      targetPort: 80

  selector:
    app: myapp
    type: back-end

So the flow will be:

type
  ↓
ClusterIP → internal cluster communication

port
  ↓
Port exposed by the Service

targetPort
  ↓
Port where the application is running in the Pod

selector
  ↓
Identifies the backend Pods

We can create the Service with:

kubectl create -f service-definition.yaml

Check it with:

kubectl get services -A

Example:

NAME         TYPE        CLUSTER-IP       PORT(S)
kubernetes   ClusterIP   10.96.0.1        443/TCP
back-end     ClusterIP   10.106.127.123   80/TCP

The CLUSTER-IP is the stable IP assigned to the Service.

Other Pods inside the cluster can communicate with the Service using its ClusterIP or its DNS name.

Kubernetes Service LoadBalancer

A LoadBalancer Service is used to expose an application to users outside the Kubernetes cluster through an external load balancer.

It is especially useful for front-end applications where users should access the application through a simple URL instead of remembering node IP addresses and high port numbers.

One solution is to place a load balancer in front of the Kubernetes nodes.

                  Users
                    |
                    v
            +---------------+
            | Load Balancer |
            +---------------+
              /     |     \
             /      |      \
            v       v       v
         Node 1   Node 2   Node 3
            |       |       |
            +-------+-------+
                    |
                   Pods

The load balancer receives the external traffic and distributes it to the Kubernetes nodes.

We could manually create a virtual machine running software such as HAProxy or Nginx for this purpose, but that adds additional infrastructure to manage.

Cloud providers can provide this load-balancing infrastructure for us.

Examples include:

  • Google Cloud Platform (GCP)
  • AWS
  • Azure

Kubernetes can then request a cloud-native load balancer for the Service.

The LoadBalancer Service type is most useful when Kubernetes is running on a supported cloud platform that can provision an external load balancer.

In environments that don’t provide an external load balancer, such as the VirtualBox example from the course, the LoadBalancer Service does not provide the same cloud load-balancing functionality.

Instead, it behaves similarly to a NodePort and exposes the application through a high port.

Kubernetes Service with type: LoadBalancer on AWS

A Kubernetes Service with type: LoadBalancer can be used to expose an application to the internet when the Kubernetes cluster is running on a cloud platform such as AWS.

The AWS load balancer becomes the external entry point for the application.

Creating a LoadBalancer Service

Suppose we already have Pods with the label:

app: myapp

We can create a Service like this:

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: myapp

Save the YAML as:

service-definition.yaml

Then create it:

kubectl create -f service-definition.yaml

Check the Service:

kubectl get services -A

We may see something similar to:

NAME             TYPE           CLUSTER-IP      EXTERNAL-IP
myapp-service    LoadBalancer   10.96.10.20     <pending>

On AWS, after the external load balancer is provisioned, the EXTERNAL-IP field will contain the load balancer’s hostname/address.

We can also inspect more details:

kubectl describe service myapp-service

The Service uses its selector to identify the Pods that should receive traffic.