What is Kubernetes? #
When we first start deploying container-based applications, everything feels very easy. We just write a Dockerfile, build a container locally, then deploy it to production servers using simple Docker commands or Docker modules in Ansible. However, as our systems and application scale grow, the real operational challenges start to emerge. Docker alone is no longer enough when we have to manage dozens to hundreds of containers running dynamically across a cluster of physical or virtual servers.
What if one of our physical servers experiences hardware failure in the middle of the night? How do we distribute network traffic evenly to dozens of application replicas without having to manually configure load balancers every time a new container goes live? How do we perform application updates without causing downtime for users? It’s to answer these large-scale container management challenges that Kubernetes (often abbreviated as K8s) was created.
In this article, we’ll explore Kubernetes’ fundamental concepts, the internal architecture that drives it, how it treats infrastructure as a set of APIs, and where Ansible’s crucial role lies in simplifying the cluster and workload management lifecycle in Kubernetes.
Infrastructure Evolution: From Bare-Metal to Orchestration #
To understand why Kubernetes has become today’s industry standard, we must look back at how our way of managing applications has evolved over time. Each era has its own operational characteristics, limitations, and solutions.
flowchart LR
BM["Bare-Metal (One OS, One Hardware)"] --> VM["Virtual Machine (Hypervisor, Guest OS)"]
VM --> CO["Container (Shared OS Kernel, Lightweight)"]
CO --> K8S["Kubernetes (Automatic Cluster Orchestration)"]The Traditional Era (Bare-Metal) #
Initially, we ran applications directly on top of physical servers (bare-metal). The biggest limitation of this era was inefficient resource allocation. If we run one web application on a large physical server, that application might only use 5% of the CPU and memory capacity. However, if we run several applications on the same server, we face library dependency conflict problems and security risks where one problematic application can affect other applications on the same operating system.
The Virtualization Era (Virtual Machines) #
To solve the isolation problem on bare-metal, the industry moved to Virtual Machine (VM) technology. With the help of a Hypervisor, we can run several fully isolated guest operating systems (guest OS) on top of a single physical hardware. VMs provide strong security boundaries and clear resource division. However, VMs are very resource-hungry because each VM must run a complete copy of its own operating system, requiring gigabytes of storage space and relatively long boot times (several minutes).
The Container Era (Containerization) #
Containers solve the resource waste problem of VMs by sharing the host operating system kernel. Instead of virtualizing the entire hardware and OS, containers only isolate the user space. This makes containers very lightweight (only megabytes in size), efficient in CPU and memory consumption, and have almost instant boot times (within seconds). We can package an application along with all its dependencies into one portable container image to run anywhere.
The Orchestration Era (Kubernetes) #
Although containers are great for portability, managing hundreds of containers manually across different servers is a nightmare for operations teams. If a container dies, who notices and restarts it? This is where container orchestration comes in. Kubernetes acts as an “orchestra conductor” that manages the entire container lifecycle automatically, ensuring our applications always run according to the state we declared.
Why Do We Need Container Orchestration? #
Let’s imagine a real scenario in a production environment without an orchestration system. We have a microservices application deployed into 30 containers spread across 5 different servers. Without an orchestration system like Kubernetes, we must face the following operational challenges manually:
Challenges Without Container Orchestration:
- Failure Detection: A container on server-3 dies suddenly → We must monitor manually and restart.
- Capacity Allocation: Server-2 runs out of memory → We must figure out which server still has room to deploy new containers.
- Scalability: Traffic spikes during holidays → We must log into every server and create container replicas manually.
- Application Updates: Releasing a new version → We must stop the old version and start the new one one by one, causing downtime.
- Networking: Connecting between services → We must track container IP addresses that always change every time they're restarted.
Kubernetes solves all the challenges above automatically by abstracting a set of physical or virtual servers into one large resource pool. We no longer need to care which server our containers run on. We just tell Kubernetes: “Please run 5 replicas of my application, make sure it’s always available, and expose it on port 80.” Kubernetes continuously works in the background to keep the cluster’s actual state always equal to the desired state.
Automatic Solutions with Kubernetes:
✓ Self-Healing: If a container crashes, Kubernetes immediately detects it and creates a new container.
✓ Auto-Scaling: Kubernetes can automatically increase or decrease the number of container replicas based on CPU utilization.
✓ Service Discovery & Load Balancing: Kubernetes gives a single IP address and DNS name for a group of containers.
✓ Automated Rollouts & Rollbacks: We can do application updates gradually without any downtime.
✓ Resource Bin-Packing: Kubernetes automatically places containers based on resource needs without wasting server capacity.
When Should We Use Kubernetes? #
It’s important for us to understand that Kubernetes isn’t a silver bullet suitable for every scenario. Kubernetes carries fairly high operational complexity, so we must be wise in deciding when to adopt it.
WE NEED KUBERNETES IF:
✓ Our application is built with a complex, interdependent microservices architecture.
✓ We need very dynamic scalability to handle extreme traffic fluctuations.
✓ We want to maximize cloud infrastructure cost efficiency by doing container bin-packing.
✓ Our developer team needs the ability to deploy applications multiple times a day safely.
✓ We need high availability with server-level fault tolerance.
WE DON'T NEED KUBERNETES IF:
✗ We only run one or two simple monolithic applications that are rarely updated.
✗ Our operations team doesn't yet have basic expertise in containers and Linux networking.
✗ We don't need automatic scalability and our application can accept brief downtime during maintenance.
✗ Our infrastructure budget is very limited (running a Kubernetes control plane requires its own resource overhead).
Fundamental Kubernetes Cluster Architecture #
A Kubernetes cluster consists of two main components: the Control Plane (the cluster’s brain making strategic decisions) and Worker Nodes (the worker machines where our application containers actually run). Understanding the interaction between these components is very important before we try to automate the cluster lifecycle using Ansible.
flowchart TD
subgraph ControlPlane["Control Plane (Cluster Brain)"]
API["kube-apiserver (API Gateway)"]
ETCD["("etcd (Cluster State Database)")"]
SCHED["kube-scheduler (Pod Scheduling)"]
CTRL["kube-controller-manager (Reconciliation Loops)"]
API <--> ETCD
API <--> SCHED
API <--> CTRL
end
subgraph WorkerNode["Worker Node (Worker Machine)"]
KLET["kubelet (Node Agent)"]
PROXY["kube-proxy (Networking & Load Balancing)"]
CRI["Container Runtime (containerd)"]
KLET --> CRI
end
API <-->|"API Communication"| KLET
API <-->|"Network Communication"| PROXY1. Control Plane Components #
The Control Plane is responsible for making global decisions about the cluster (like application scheduling), as well as detecting and responding to cluster events. These components are usually run on dedicated servers that don’t run user application containers to maintain security and performance.
- kube-apiserver: This is the main gateway into the Control Plane. All communication inside the cluster, whether from internal components or external commands (like
kubectlor Ansible playbooks), must go through the API Server. It acts as a validator and traffic regulator for Kubernetes objects. - etcd: A highly consistent and reliable key-value data store. etcd is the only place where Kubernetes cluster state data is stored. All configuration, object status, and metadata are stored here. If our etcd is destroyed without a backup, our entire cluster is also lost. Therefore, etcd replication and backup become a top priority.
- kube-scheduler: This component is tasked with watching newly created Pods that don’t yet have an assigned node, and choosing the best node where those Pods should run. This scheduling decision is made based on resource needs (CPU/memory), affinity policies (affinity/anti-affinity), hardware constraints, and the node’s current workload.
- kube-controller-manager: Runs controller processes in the background. Conceptually, each controller is a separate control loop that watches the cluster state through the API Server and tries to bring the current state toward the desired state. Examples include the Node Controller (detecting if a node is dead) and the Job Controller (running batch tasks).
2. Worker Node Components #
Worker Nodes are tasked with running our application Pods and providing the runtime environment needed for containers to communicate with each other safely.
- kubelet: An agent running on every Worker Node in the cluster. Its main task is ensuring the containers defined in the Pod spec run healthily and as targeted. Kubelet receives instructions from the API Server and translates them into local container runtime commands.
- kube-proxy: A network agent running on every node to maintain network rules on that node. These network rules enable network communication to Pods from inside or outside the cluster by leveraging iptables or IPVS in the Linux kernel.
- Container Runtime: The software responsible for running containers. Kubernetes supports various container runtimes through the Container Runtime Interface (CRI), like
containerd,CRI-O, or other Docker variants.
Basic Object Concepts (Resources) in Kubernetes #
Before we start writing Kubernetes manifests, we must understand the basic objects that become the building blocks of applications inside the cluster. Kubernetes defines these objects declaratively using YAML format.
1. Pod #
A Pod is the smallest and most fundamental unit we can create and manage in Kubernetes. A Pod represents one process running in our cluster. A Pod can contain one container (the most common pattern) or several containers that share storage and network (IP), along with instructions on how the containers should run.
# Example of a simple Pod manifest (pod-myapp.yaml)
apiVersion: v1
kind: Pod
metadata:
name: webapp-pod
namespace: production
labels:
app: webapp
spec:
containers:
- name: web-container
image: nginx:1.25-alpine
ports:
- containerPort: 80
2. Deployment #
We almost never create Pods directly in production environments because Pods don’t have built-in self-healing capabilities if the node where they run dies. Instead, we use a Deployment. Deployments abstract Pod and ReplicaSet creation, manage Pod replication lifecycles, perform rolling application version updates without downtime, and allow us to roll back if there’s an error in the new version.
# Example Deployment manifest (deployment-myapp.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
namespace: production
spec:
replicas: 3 # Guarantees there are always 3 running Pod replicas
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: web-container
image: nginx:1.25-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
3. Service #
Pods in Kubernetes are ephemeral. When a Pod dies or is rescheduled to another node, it gets a new, different IP address. To bridge communication between Pods without losing track of IP addresses, we use a Service. A Service acts as an abstraction to expose a group of Pods as one stable network endpoint and does automatic internal load balancing.
# Example Service manifest (service-myapp.yaml)
apiVersion: v1
kind: Service
metadata:
name: webapp-service
namespace: production
spec:
selector:
app: webapp # Directs traffic to Pods with the label app=webapp
ports:
- protocol: TCP
port: 80 # Port exposed by the Service
targetPort: 80 # Destination container port
type: ClusterIP # Default, only accessible from inside the cluster
4. ConfigMap and Secret #
Kubernetes separates application code from configuration and sensitive data using ConfigMap and Secret. This allows us to use the same container image in Development, Staging, and Production environments without changing the code inside it.
- ConfigMap: Used to store non-sensitive configuration data (like environment variables or application configuration files).
- Secret: Used to store sensitive data like passwords, API tokens, and SSH private keys with base64 encoding.
Kubernetes’ API-Driven Architecture #
One of Kubernetes’ main strengths that makes it highly suitable for combining with Ansible is the API-Driven Architecture philosophy. In Kubernetes, everything is treated as an API Object. Kubernetes clusters are fully controlled through the RESTful API exposed by kube-apiserver.
When we run the kubectl apply -f manifest.yaml command, what actually happens is that kubectl reads the YAML file, converts it to JSON format, then sends an HTTP POST/PUT request to the Kubernetes API Server. The API Server then validates the schema, stores it in the etcd database, and triggers the reconciliation loop.
flowchart TD
DS["desired state (YAML)"] --> APIS["kube-apiserver"]
APIS --> ETCD["etcd (State Storage)"]
APIS --> RL["reconciliation loop (Control Manager)"]
RL --> KUBELET["kubelet & CRI"]
KUBELET --> AS["actual state (Worker)"]This reconciliation loop is the core of Kubernetes’ declarative nature. Controller components continuously compare the real state on the ground (actual state) with the state declared in etcd (desired state). If there’s a difference (for example, a Pod dies), the controller immediately takes corrective action (deploying a new Pod on another node) without manual intervention from us. This API-driven nature is what Ansible modules leverage to manage cluster infrastructure structurally and idempotently.
Ansible’s Role in the Kubernetes Ecosystem #
When first learning Kubernetes, many of us think Ansible and Kubernetes are two competing tools. This is a big misunderstanding. In reality, Ansible and Kubernetes complement each other very well.
Ansible excels at managing traditional host-based configuration (operating systems, packages, configuration files, host networking), while Kubernetes excels at managing large-scale containers at the application level. We can divide Ansible’s role in the Kubernetes ecosystem into two big phases:
1. Day 0 & Day 1: Cluster Provisioning and Bootstrapping #
Before we can deploy applications to Kubernetes, we must create the cluster first. The cluster initialization process from scratch is very complex and tedious if done manually. Ansible acts as the main automation tool for preparing the base operating system on control and worker nodes, installing system dependencies like the container runtime (containerd), preparing Linux kernel modules, and performing the initial cluster initialization using kubeadm.
2. Day 2: Workload Management and Hybrid Integration #
After the cluster is active, we can use Ansible to manage objects inside Kubernetes. Using Ansible to deploy application manifests allows us to combine Kubernetes deployments with external infrastructure workflows. For example, our Ansible playbook can create a managed cloud database (RDS) in AWS, register DNS records in Cloudflare, then deploy our application manifest to Kubernetes fetching those database credentials from Ansible Vault.
| Operational Category | Ansible Tasks | Kubernetes Tasks |
|---|---|---|
| Physical Server / VM Management | Installs the OS, configures SSH, partitions disks, tunes the kernel. | Not dominant (Kubernetes runs on top of a prepared OS). |
| Container Runtime | Installs and configures the containerd or CRI-O daemon. | Interacts with the runtime via CRI to create containers. |
| Cluster Bootstrapping | Runs kubeadm init and kubeadm join on all nodes. | Manages internal control plane component scheduling after activation. |
| External Integration | Manages public IPs, external firewalls, global DNS, separate databases. | Only manages the cluster’s internal network (Pod CIDR/Service ClusterIP). |
| Workload Deployment | Sends configuration files and YAML manifests to the API Server. | Runs, monitors, and keeps application pods active. |
Management Tool Comparison: kubectl vs kubernetes.core Module #
When we want to deploy manifests to Kubernetes, we face a choice: using the pure kubectl CLI command, or using the kubernetes.core.k8s module provided by Ansible. Let’s compare both in depth to see why Ansible provides far greater advantages in production environments.
Approach Using kubectl (Anti-Pattern):
✗ Hard to manage centrally if deploying to several different clusters.
✗ No built-in idempotency validation for complex logic flows.
✗ Secret management often leaks because we have to create Secret manifest files locally.
✗ Hard to integrate with non-Kubernetes steps (like external database migrations).
✗ Shell wrapper script syntax for kubectl quickly becomes complex and hard to debug.
Approach Using kubernetes.core (Correct Solution):
✓ Fully integrated with the Ansible Inventory for multi-environment deployment.
✓ Idempotent by default — Ansible only sends changes if there's a state difference.
✓ Tight integration with Ansible Vault for secure secret encryption without leaving plaintext traces.
✓ Hybrid workflow: Ansible can manage external cloud resources and Kubernetes sequentially.
✓ Jinja2 template support for generating dynamic, modular Kubernetes manifests.
Code Comparison: Manual Deployment vs Declarative Playbook #
Let’s visually compare the difference between deploying an application using a kubectl wrapper shell script (which is error-prone) and an Ansible playbook where failure-prone methods are replaced with a clean declarative approach.
# ANTI-PATTERN: Using the shell module to run kubectl commands manually
# This is not idempotent, hard to handle errors, and prone to sensitive variable leaks in terminal logs
- name: Deploy the application using kubectl shell (Highly Not Recommended)
shell: |
kubectl apply -f /opt/app/namespace.yaml
kubectl apply -f /opt/app/secret.yaml
kubectl apply -f /opt/app/deployment.yaml
environment:
KUBECONFIG: /home/admin/.kube/config
# CORRECT: Using the kubernetes.core.k8s module declaratively
# Ansible handles the API connection directly, idempotently, and we can use Ansible Vault for Secrets
- name: Deploy the namespace and resources declaratively (Recommended System)
kubernetes.core.k8s:
kubeconfig: "{{ playbook_dir }}/kubeconfig/admin.conf"
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ app_namespace }}"
labels:
managed-by: ansible
environment: "{{ env_name }}"
- name: Deploy the database secret using encrypted data from Ansible Vault
kubernetes.core.k8s:
kubeconfig: "{{ playbook_dir }}/kubeconfig/admin.conf"
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: "{{ app_namespace }}"
type: Opaque
stringData:
database-url: "postgresql://{{ db_user }}:{{ vault_db_password }}@{{ db_host }}/{{ db_name }}"
no_log: true # Prevents the password from leaking to Ansible stdout logs
By switching to the kubernetes.core.k8s module, we no longer need to think about whether manifest files already exist on the target server or whether we must run replace or apply commands. Ansible analyzes the object’s current state directly from the Kubernetes API Server and takes the minimal action needed to align the real state with our desired configuration.
Decision Tree: Choosing the Right Orchestration Flow #
To make it easier to determine when to use plain Docker, pure Kubernetes, or an Ansible combination on top, we can follow this decision diagram:
flowchart TD
A{"Need Dynamic Scalability?"} -->|"Yes"| B{"Number of Containers > 10?"}
A -->|"No"| C["Docker Compose Is Enough"]
B -->|"Yes"| D["Kubernetes + Ansible"]
B -->|"No"| E["Ansible + Docker Module"]If our application is still small-scale and doesn’t require high availability across multiple physical servers, using plain Docker modules in Ansible is the more cost-effective and efficient choice. However, when complexity demands multi-path cluster orchestration, using Kubernetes controlled by Ansible is the gold standard we must apply.
Summary #
- Kubernetes (K8s) is an open-source container orchestration platform tasked with automating container deployment, scaling, management, and self-healing across server clusters.
- Kubernetes architecture is divided into two parts: the Control Plane (the decision-making brain consisting of the API Server, etcd, Scheduler, and Controller Manager) and Worker Nodes (where containers run, managed by kubelet, kube-proxy, and the container runtime).
- Kubernetes’ API-Driven nature allows all resources to be represented as RESTful objects that can be manipulated declaratively using YAML format.
- Fundamental Kubernetes objects include Pods (the smallest unit), Deployments (managing pod replicas and updates), Services (maintaining IP address stability and load balancing), and ConfigMap & Secret (separating configuration data from container images).
- Ansible doesn’t compete with Kubernetes, but complements it. Ansible excels at initial cluster bootstrapping (Day 0/1) and managing hybrid external integration (Day 2).
- Using the
kubernetes.core.k8smodule is far better than running kubectl in an Ansible shell because it provides idempotency guarantees, dynamic Jinja2 variable integration, and secret security using Ansible Vault.