Kubernetes Learning Notes
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:
namelabels
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 versionkind→ resource typemetadata→ name and labelsreplicas→ number of Pods to maintaintemplate→ 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 scalechanges 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
targetPortis not specified, it defaults to the same value asport. IfnodePortis 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.