system-design45 min read

Design a Kubernetes-Style Container Orchestration Platform: The Complete Guide — A Senior+ Guide | Ayodhyya

Design a Kubernetes-Style Container Orchestration Platform: The Complete Guide

A Senior+ Guide to Building, Operating, and Scaling Production-Grade Orchestration

Published: July 14, 2024 Reading Time: 45 min Author: Ayodhyya

1. Introduction

Container orchestration is the backbone of modern cloud-native infrastructure. Every major technology company — from Netflix and Spotify to Stripe and Shopify — runs thousands of containers across fleets of machines, and they all rely on orchestration platforms to schedule, network, secure, scale, and heal those workloads automatically. Kubernetes has emerged as the de facto standard, but understanding the internals of how such a system is designed from scratch is what separates a senior engineer from a staff engineer.

In this guide, we will dissect every major subsystem of a Kubernetes-style container orchestration platform. We will begin with the high-level landscape, move into capacity estimation and data modeling, then walk through the control plane and data plane in exhaustive detail. We will cover pod lifecycle management, deployment strategies, service discovery, ingress routing, configuration management, persistent storage, role-based access control, package management via Helm, horizontal and vertical auto-scaling, network policies, observability through Prometheus and Grafana, centralized logging with the EFK stack, multi-cluster federation, cost modeling, testing strategies, and finally interview preparation.

This is not a tutorial on running kubectl apply. This is a deep architectural guide for engineers who want to understand the system at the level required to design, build, debug, and optimize orchestration platforms for production workloads serving millions of requests per second.

Who is this guide for? Senior software engineers, platform engineers, SREs, and architects who want a ground-up understanding of container orchestration internals. You should be familiar with Linux fundamentals, networking, and basic container concepts before proceeding.

2. Container Orchestration Landscape

Before diving into the design, it is important to understand the competitive landscape and why Kubernetes won. The orchestration space has evolved through several generations of tooling, each solving progressively harder problems.

2.1 Historical Context

In the early days of containerization, Docker provided the runtime but had no native multi-host scheduling. Teams wrote custom shell scripts to deploy containers across VMs, which worked until the fleet grew beyond a dozen machines. This pain led to a wave of orchestration tools in the 2014-2016 timeframe.

2.2 Comparison of Orchestration Platforms

PlatformOriginLanguageKey DifferentiatorStatus (2026)
Docker SwarmDocker Inc.GoSimplicity, built into DockerDeprecated by Docker
Apache Mesos / MarathonTwitter / MesosphereScala / JavaMulti-framework schedulingEnd of life
KubernetesGoogleGoDeclarative API, extensibilityDominant standard
NomadHashiCorpGoSimple binary, multi-workloadNiche adoption
Docker Desktop / ECSAmazonVariousAWS integrationActive but proprietary

2.3 Why Kubernetes Won

Kubernetes succeeded because of three strategic advantages. First, the declarative API model — you describe desired state and the system converges toward it, rather than issuing imperative commands. Second, the extensibility model — Custom Resource Definitions (CRDs) and admission webhooks allow the platform to be extended without forking. Third, the ecosystem — Helm, Istio, Prometheus, ArgoCD, and hundreds of other projects created a gravitational pull that no competitor could match.

Understanding these design principles is critical because when we build our own orchestration system, we will adopt the same declarative, extensible approach that made Kubernetes successful.

graph TD A[Application Developer] -->|Defines YAML| B[API Server] B -->|Validates & Stores| C[etcd] B -->|Notifies| D[Controller Manager] D -->|Reconciles State| E[Scheduler] E -->|Assigns Nodes| F[kubelet on Node] F -->|Runs Containers| G[Container Runtime] G -->|Manages| H[Pods] H -->|Exposes Services| I[Service Mesh]

3. Requirements

Any production-grade orchestration platform must satisfy a comprehensive set of functional and non-functional requirements. Let us enumerate them systematically.

3.1 Functional Requirements

  • Container Scheduling: Automatically place containers onto appropriate nodes based on resource requirements, affinity rules, and constraints.
  • Self-Healing: Automatically restart failed containers, reschedule them on healthy nodes, and replace unresponsive instances.
  • Service Discovery & Load Balancing: Provide DNS-based service discovery and distribute traffic across container replicas.
  • Horizontal Auto-Scaling: Automatically adjust replica counts based on CPU, memory, or custom metrics.
  • Rolling Updates & Rollbacks: Deploy new versions incrementally with zero downtime and automatic rollback on failure.
  • Storage Orchestration: Automatically mount persistent volumes from various storage backends (NFS, cloud block storage, distributed filesystems).
  • Secrets Management: Securely store and inject sensitive configuration (passwords, certificates, API keys) into containers.
  • Network Policies: Enforce firewall rules between pods to implement zero-trust networking.
  • Multi-Tenancy: Support namespace isolation, resource quotas, and RBAC to serve multiple teams from a single cluster.
  • Batch & Cron Workloads: Support one-off batch jobs and scheduled recurring tasks alongside long-running services.

3.2 Non-Functional Requirements

RequirementTargetRationale
Availability99.95% (control plane)Control plane downtime blocks all cluster operations
Scheduling Latency< 1 second (p99)Fast placement enables rapid scaling
Cluster SizeUp to 5,000 nodesMatches largest production deployments
API Throughput10,000+ operations/secHandles burst scaling events
Data Durability3+ replicas of etcdPrevents cluster state loss
Network Pod-to-Pod Latency< 1ms within AZMicroservice communication
Boot Time (Node)< 30 secondsEnables rapid cluster expansion
Failure Detection< 10 secondsFast healing for HA workloads

3.3 Capacity Estimation

For a production cluster serving a mid-to-large organization, let us estimate the capacity requirements across all subsystems.

ComponentData per NodeTotal (5,000 nodes)Growth Rate
Pod Metadata (etcd)~50 KB (avg 20 pods)250 MB10% monthly
Container Runtime State~10 KB per container1 GB (100K containers)Variable
Network State (CNI)~5 KB per pod500 MBScales with pods
Metrics (Prometheus)~2 KB/s per pod200 MB/s scrape15% monthly
Logs (per node)~500 MB/day2.5 TB/day20% monthly
etcd Storage TotalN/A8 GB (recommended max 8GB)Compaction required
Key Insight: etcd is the single most critical component in the entire system. It stores the entire cluster state and must be backed by fast SSDs with low-latency networking. A corrupted etcd cluster means a completely inoperable orchestration platform. Always run etcd on dedicated machines with NVMe storage.

4. Data Model (etcd)

etcd is a distributed, consistent key-value store based on the Raft consensus algorithm. In our orchestration platform, etcd serves as the single source of truth for all cluster state. Every resource — nodes, pods, services, deployments, secrets, configmaps, and more — is stored as a key-value pair in etcd.

4.1 Key Schema Design

The key hierarchy follows a logical namespace structure that enables efficient prefix-based lookups and watches.

/registry/
├── /pods/
│   ├── /default/
│   │   ├── nginx-deployment-7fb96c846b-xk2jz
│   │   └── nginx-deployment-7fb96c846b-abc12
│   └── /kube-system/
│       ├── coredns-5d78c9869d-pxyzq
│       └── etcd-master-0
├── /services/
│   ├── /default/
│   │   └── kubernetes
│   └── /production/
│       └── api-gateway
├── /deployments/
│   └── /default/
│       └── nginx-deployment
├── /nodes/
│   ├── node-01
│   ├── node-02
│   └── node-03
├── /secrets/
│   └── /default/
│       └── db-credentials
├── /configmaps/
│   └── /default/
│       └── app-config
├── /replicasets/
├── /daemonsets/
├── /statefulsets/
├── /serviceaccounts/
├── /roles/
├── /rolebindings/
├── /clusterroles/
└── /clusterrolebindings/

4.2 Resource Object Model

Every resource in the system follows a consistent object model with metadata, specification, and status fields.

// C# representation of a Kubernetes-style resource object
public class KubernetesResource
{
    public string ApiVersion { get; set; }    // e.g., "v1", "apps/v1"
    public string Kind { get; set; }          // e.g., "Pod", "Deployment"
    public ObjectMetadata Metadata { get; set; }
    public object Spec { get; set; }           // Desired state
    public object Status { get; set; }         // Observed state
}

public class ObjectMetadata
{
    public string Uid { get; set; }            // Unique identifier (UUID)
    public string Name { get; set; }           // Human-readable name
    public string Namespace { get; set; }      // Logical isolation
    public Dictionary<string, string> Labels { get; set; }
    public Dictionary<string, string> Annotations { get; set; }
    public DateTime CreationTimestamp { get; set; }
    public long ResourceVersion { get; set; }  // Optimistic concurrency
    public int Generation { get; set; }        // Incremented on spec change
}

4.3 Pod Object Example

{
    "apiVersion": "v1",
    "kind": "Pod",
    "metadata": {
        "name": "nginx-deployment-7fb96c846b-xk2jz",
        "namespace": "production",
        "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "labels": {
            "app": "nginx",
            "version": "v2.1.0",
            "environment": "production",
            "team": "platform"
        },
        "resourceVersion": "12345678",
        "generation": 3
    },
    "spec": {
        "nodeName": "node-01",
        "containers": [{
            "name": "nginx",
            "image": "nginx:1.25.3",
            "ports": [{"containerPort": 8080}],
            "resources": {
                "requests": { "cpu": "250m", "memory": "256Mi" },
                "limits": { "cpu": "500m", "memory": "512Mi" }
            },
            "livenessProbe": {
                "httpGet": { "path": "/healthz", "port": 8080 },
                "initialDelaySeconds": 10,
                "periodSeconds": 5
            }
        }],
        "restartPolicy": "Always"
    },
    "status": {
        "phase": "Running",
        "podIP": "10.244.1.42",
        "hostIP": "192.168.1.101",
        "conditions": [...],
        "containerStatuses": [...]
    }
}

4.4 etcd Performance Tuning

For production deployments, etcd requires careful tuning to maintain consistent low-latency performance under heavy write loads.

ParameterRecommended ValueRationale
--quota-backend-bytes8589934592 (8 GB)Prevents unbounded growth
--auto-compaction-retention8 hoursKeeps DB size manageable
--snapshot-count10000Triggers compaction before DB grows too large
--heartbeat-interval100 msRaft leader election tuning
--election-timeout1000 msMust be 5-10x heartbeat interval
Disk TypeNVMe SSDWrite latency directly impacts API server
Disk IOPSMinimum 10,000Handles burst write loads

5. Architecture: The Control Plane

The control plane is the brain of the orchestration platform. It consists of several components that together manage the desired state of the cluster, schedule workloads, and respond to changes. A well-designed control plane must be highly available, fault-tolerant, and capable of handling thousands of concurrent API requests.

graph TB subgraph Control Plane API[API Server] ETCD[(etcd Cluster)] SCHED[Scheduler] CM[Controller Manager] CCM[Cloud Controller Manager] end subgraph Worker Node 1 KL1[kubelet] KP1[kube-proxy] RT1[Container Runtime] POD1A[Pod A] POD1B[Pod B] end subgraph Worker Node 2 KL2[kubelet] KP2[kube-proxy] RT2[Container Runtime] POD2A[Pod C] POD2B[Pod D] end API <--> ETCD SCHED --> API CM --> API KL1 --> API KL2 --> API API --> KL1 API --> KL2 KP1 -.-> POD1A KP1 -.-> POD1B KP2 -.-> POD2A KP2 -.-> POD2B RT1 --> POD1A RT1 --> POD1B RT2 --> POD2A RT2 --> POD2B

5.1 High Availability Design

Production control planes run multiple instances of each component across availability zones. The API server sits behind a load balancer, etcd runs as a 3 or 5-node cluster for Raft consensus, and the scheduler and controller manager use leader election so only one instance is active at a time.

// C# representation of the HA control plane configuration
public class ControlPlaneConfig
{
    public ApiServerConfig ApiServer { get; set; }
    public EtcdConfig Etcd { get; set; }
    public SchedulerConfig Scheduler { get; set; }
    public ControllerManagerConfig ControllerManager { get; set; }
}

public class ApiServerConfig
{
    public List<string> Endpoints { get; set; } = new()
    {
        "https://api-server-1.internal:6443",
        "https://api-server-2.internal:6443",
        "https://api-server-3.internal:6443"
    };
    public string AuditLogFile { get; set; } = "/var/log/k8s/audit.log";
    public int MaxRequestsInFlight { get; set; } = 400;
    public int MaxMutatingRequestsInFlight { get; set; } = 200;
    public int WatchCacheSize { get; set; } = 1000;
}

public class EtcdConfig
{
    public List<string> Endpoints { get; set; } = new()
    {
        "https://etcd-1:2379",
        "https://etcd-2:2379",
        "https://etcd-3:2379"
    };
    public string CaFile { get; set; } = "/etc/etcd/ca.crt";
    public string CertFile { get; set; } = "/etc/etcd/server.crt";
    public string KeyFile { get; set; } = "/etc/etcd/server.key";
    public int QuotaBackendBytes { get; set; } = 8_589_934_592;
    public string AutoCompactionRetention { get; set; } = "8h";
}

5.2 Component Interaction Flow

When a user submits a deployment, the following sequence occurs:

  1. The API server validates the request against admission webhooks and schema validation.
  2. The validated object is written to etcd with a new resource version.
  3. The controller manager detects the new deployment and creates a ReplicaSet.
  4. The scheduler watches for unscheduled pods and assigns them to nodes.
  5. kubelets on assigned nodes watch for pods assigned to them and start containers.
  6. The controller manager continuously monitors actual vs. desired state and takes corrective action.
sequenceDiagram participant User participant API as API Server participant ETCD as etcd participant CM as Controller Manager participant SCHED as Scheduler participant KL as kubelet User->>API: POST /apis/apps/v1/namespaces/default/deployments API->>API: Validate & Run Admission Webhooks API->>ETCD: Store Deployment Object API-->>User: 201 Created ETCD-->>CM: Watch Event: Deployment Created CM->>API: Create ReplicaSet API->>ETCD: Store ReplicaSet ETCD-->>CM: Watch Event: ReplicaSet Created CM->>API: Create Pods (unscheduled) API->>ETCD: Store Pods ETCD-->>SCHED: Watch Event: Unscheduled Pods SCHED->>API: Bind Pod to Node API->>ETCD: Update Pod with nodeName ETCD-->>KL: Watch Event: Pod assigned to this node KL->>KL: Pull image & Start container KL->>API: Update Pod status to Running API->>ETCD: Store updated status

6. API Server & Authentication

The API server is the central hub and the only component that communicates directly with etcd. Every operation in the cluster — whether from kubectl, the dashboard, or internal controllers — flows through the API server. It is the enforcement point for authentication, authorization, and admission control.

6.1 Authentication Pipeline

The API server processes every incoming request through a multi-stage pipeline: Authentication, Authorization, Admission Control, and finally persistence to etcd.

graph LR A[Client Request] --> B[Authentication] B --> C[Authorization - RBAC] C --> D[Admission Control] D --> E[Persistence to etcd] B -->|Fail| F[401 Unauthorized] C -->|Fail| G[403 Forbidden] D -->|Fail| H[422/400 Error]

6.2 Authentication Methods

MethodUse CaseSecurity Level
Client Certificates (mTLS)Node-to-API communication, CI/CDHigh
Bearer Tokens (Static)Service accounts, legacy systemsMedium
OIDC (OpenID Connect)Human users via identity providersHigh
Webhook Token AuthCustom authentication backendsHigh
ServiceAccount TokensIn-cluster pod authenticationHigh (bound tokens)

6.3 C# API Server Implementation

// Simplified API Server request processing pipeline
public class ApiServerPipeline
{
    private readonly IAuthenticationHandler _authHandler;
    private readonly IAuthorizationHandler _rbacHandler;
    private readonly IEnumerable<IAdmissionWebhook> _admissionWebhooks;
    private readonly IEtcdStore _etcdStore;

    public async Task<ApiResponse> ProcessRequestAsync(
        ApiRequest request)
    {
        // Stage 1: Authentication
        var authResult = await _authHandler.AuthenticateAsync(request);
        if (!authResult.IsAuthenticated)
        {
            return ApiResponse.Unauthorized(
                "Unable to authenticate the request");
        }

        // Stage 2: Authorization (RBAC)
        var authzResult = await _rbacHandler.AuthorizeAsync(
            authResult.User,
            request.Resource,
            request.Verb);
        if (!authzResult.IsAuthorized)
        {
            return ApiResponse.Forbidden(
                $"User {authResult.User.Name} is not allowed " +
                $"to {request.Verb} {request.Resource}");
        }

        // Stage 3: Admission Control
        var admissionCtx = new AdmissionContext
        {
            User = authResult.User,
            Resource = request.Resource,
            Operation = request.Verb,
            Object = request.Body
        };

        foreach (var webhook in _admissionWebhooks)
        {
            var result = await webhook.AdmitAsync(admissionCtx);
            if (!result.Allowed)
            {
                return ApiResponse.BadRequest(result.Reason);
            }
            // Mutating webhooks can modify the object
            if (result.ModifiedObject != null)
            {
                admissionCtx.Object = result.ModifiedObject;
            }
        }

        // Stage 4: Persist to etcd
        var resourceVersion = await _etcdStore.UpsertAsync(
            request.Key,
            admissionCtx.Object);

        return ApiResponse.Created(new
        {
            metadata = new { resourceVersion }
        });
    }
}

6.4 Watch Mechanism

The API server implements a watch mechanism that allows clients to receive real-time notifications when resources change. This is the foundation of the controller pattern — every controller watches for specific resource types and reacts to changes.

// Watch implementation - controllers subscribe to resource changes
public class ResourceWatcher<T> where T : KubernetesResource
{
    private readonly HttpClient _apiClient;
    private readonly string _resourcePath;
    private long _lastResourceVersion;

    public event EventHandler<WatchEvent<T>> OnResourceChanged;

    public async Task StartWatchingAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var url = $"{_resourcePath}?watch=true" +
                    $"&resourceVersion={_lastResourceVersion}";

                using var stream = await _apiClient
                    .GetStreamAsync(url, ct);

                using var reader = new StreamReader(stream);
                while (!reader.EndOfStream)
                {
                    var line = await reader.ReadLineAsync();
                    var watchEvent = JsonSerializer
                        .Deserialize<WatchEvent<T>>(line);

                    _lastResourceVersion =
                        watchEvent.Object.Metadata.ResourceVersion;

                    OnResourceChanged?.Invoke(this, watchEvent);
                }
            }
            catch (Exception ex)
            {
                // Reconnect with exponential backoff
                await Task.Delay(
                    TimeSpan.FromSeconds(Math.Min(30,
                        Math.Pow(2, _retryCount))), ct);
                _retryCount++;
            }
        }
    }
}
Watch Cache: The API server maintains an in-memory watch cache for each resource type. When a watch client reconnects after a brief disconnection, it can resume from the cache rather than re-listing all resources from etcd. This reduces etcd load dramatically. The default cache size is 100 events per resource type but should be increased for large clusters.

7. Scheduler & Controller Manager

7.1 The Scheduler

The scheduler is responsible for assigning pods to nodes. It watches for pods with no nodeName set and runs them through a multi-stage scheduling pipeline to find the best node. This is one of the most performance-critical components because scheduling latency directly impacts scaling speed.

7.2 Scheduling Pipeline

graph TD A[Pod enters scheduling queue] --> B[Filtering Phase] B -->|Predicate checks| C[Score Phase] C -->|Priority scoring| D[Select highest score] D --> E[Bind pod to node] B -->|Node fails filter| F[Skip node] F --> B C -->|Node fails scoring| G[Score = 0] G --> C
PhaseWhat HappensExample Filters
FilteringEliminate nodes that cannot run the podSufficient CPU/memory, node selectors, taints/tolerations, affinity rules, PVC binding
ScoringRank remaining nodes by fitnessLeast requested resources, balanced allocation, pod affinity spread, image locality
BindingWrite the selected node to the pod specAtomic update via API server

7.3 Scheduler Implementation

public class Scheduler
{
    private readonly IApiClient _apiClient;
    private readonly List<IFilterPlugin> _filters;
    private readonly List<IScorePlugin> _scorers;
    private readonly PriorityQueue<SchedulablePod, int> _queue;

    public async Task SchedulePodAsync(Pod pod)
    {
        // Step 1: Get all available nodes
        var nodes = await _apiClient.ListNodesAsync();

        // Step 2: Filtering - remove ineligible nodes
        var feasibleNodes = new List<Node>();
        foreach (var node in nodes)
        {
            bool passes = true;
            foreach (var filter in _filters)
            {
                if (!await filter.ShouldScheduleAsync(pod, node))
                {
                    passes = false;
                    break;
                }
            }
            if (passes) feasibleNodes.Add(node);
        }

        if (!feasibleNodes.Any())
        {
            // Pod is unschedulable - add to backoff queue
            _queue.Enqueue(new SchedulablePod(pod), priority: 0);
            return;
        }

        // Step 3: Scoring - rank feasible nodes
        var scoredNodes = new List<(Node Node, int Score)>();
        foreach (var node in feasibleNodes)
        {
            int totalScore = 0;
            foreach (var scorer in _scorers)
            {
                totalScore += await scorer.ScoreAsync(pod, node);
            }
            scoredNodes.Add((node, totalScore));
        }

        // Step 4: Select the highest-scoring node
        var selected = scoredNodes
            .OrderByDescending(x => x.Score)
            .First();

        // Step 5: Bind the pod to the selected node
        pod.Spec.NodeName = selected.Node.Metadata.Name;
        await _apiClient.BindPodAsync(pod);
    }
}

// Example filter: Check if node has enough resources
public class ResourceFilter : IFilterPlugin
{
    public async Task<bool> ShouldScheduleAsync(Pod pod, Node node)
    {
        var requested = pod.Spec.Containers
            .Sum(c => c.Resources.Requests.CpuMillicores);
        var available = node.Status.Allocatable.CpuMillicores
            - node.Status.Used.CpuMillicores;

        return available >= requested;
    }
}

// Example scorer: Prefer nodes with least requested resources
public class LeastRequestedScorer : IScorePlugin
{
    public async Task<int> ScoreAsync(Pod pod, Node node)
    {
        var totalCpu = node.Status.Allocatable.CpuMillicores;
        var usedCpu = node.Status.Used.CpuMillicores;
        var requestedCpu = pod.Spec.Containers
            .Sum(c => c.Resources.Requests.CpuMillicores);

        // Score 0-100, higher = less utilized = preferred
        var score = (int)((1.0 - (double)(usedCpu + requestedCpu)
            / totalCpu) * 100);
        return Math.Max(0, Math.Min(100, score));
    }
}

7.4 Controller Manager

The controller manager runs multiple controllers, each responsible for a specific resource type. Every controller follows the same reconciliation loop pattern: watch for resource changes, compare actual state to desired state, and take action to converge.

// Base controller reconciliation loop pattern
public abstract class ReconciliationController<T> where T : KubernetesResource
{
    private readonly IApiClient _apiClient;
    private readonly TimeSpan _resyncPeriod = TimeSpan.FromMinutes(5);

    protected abstract string ResourcePath { get; }
    protected abstract Task<bool> ReconcileAsync(T resource);

    public async Task RunAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                // List all resources and watch for changes
                var resources = await _apiClient
                    .ListAsync<T>(ResourcePath);

                foreach (var resource in resources)
                {
                    await ReconcileAsync(resource);
                }

                // Watch for future changes
                await WatchAsync(ct);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    "Reconciliation loop failed, retrying");
                await Task.Delay(_resyncPeriod, ct);
            }
        }
    }

    protected async Task<bool> ReconcileAsync(T resource)
    {
        // Compare desired state with actual state
        var desiredState = GetDesiredState(resource);
        var actualState = await GetActualStateAsync(resource);

        if (StatesMatch(desiredState, actualState))
        {
            return true; // Already in desired state
        }

        // Take corrective action
        return await TakeActionAsync(resource, desiredState, actualState);
    }
}

7.5 Key Controllers

ControllerWatchesCreates/Manages
Deployment ControllerDeploymentsReplicaSets
ReplicaSet ControllerReplicaSetsPods
DaemonSet ControllerDaemonSetsPods (one per node)
Job ControllerJobsPods (batch)
Node ControllerNodesPod eviction on failure
Service Account ControllerNamespacesDefault service accounts
Endpoint ControllerServices + PodsEndpoints / EndpointSlices

8. kubelet & kube-proxy

While the control plane manages the cluster-level state, kubelet and kube-proxy run on every worker node and manage the node-level operations. Together they form the data plane — the layer that actually runs containers and routes network traffic.

8.1 kubelet

kubelet is the node agent that ensures containers described in pod specs assigned to its node are running and healthy. It communicates with the container runtime (containerd, CRI-O) via the Container Runtime Interface (CRI).

graph TD A[kubelet] -->|CRI gRPC| B[Container Runtime - containerd] B -->|OCI runtime| C[runc / kata / gVisor] C -->|Creates| D[Container] A -->|Volume plugin| E[CSI Driver] A -->|Network plugin| F[CNI Plugin] A -->|Liveness Probe| D A -->|Readiness Probe| D A -->|Metrics| G[Node Exporter]
// C# representation of kubelet's core loop
public class KubeletNodeAgent
{
    private readonly IContainerRuntime _runtime;
    private readonly IApiClient _apiClient;
    private readonly IMetricsCollector _metrics;
    private readonly Dictionary<string, ManagedPod> _managedPods;

    public async Task RunAsync(CancellationToken ct)
    {
        // Register this node with the API server
        await RegisterNodeAsync();

        // Main sync loop
        while (!ct.IsCancellationRequested)
        {
            try
            {
                // Get pods assigned to this node
                var desiredPods = await _apiClient
                    .ListPodsForNodeAsync(NodeName);

                // Sync desired state
                await SyncPodsAsync(desiredPods);

                // Report node status
                await ReportNodeStatusAsync();

                // Collect and report metrics
                await ReportMetricsAsync();

                // Run garbage collection
                await GarbageCollectAsync();

                await Task.Delay(TimeSpan.FromSeconds(5), ct);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Kubelet sync loop error");
                await Task.Delay(TimeSpan.FromSeconds(5), ct);
            }
        }
    }

    private async Task SyncPodsAsync(List<Pod> desiredPods)
    {
        var desiredUids = desiredPods
            .Select(p => p.Metadata.Uid).ToHashSet();

        // Kill pods that are no longer desired
        foreach (var (uid, managed) in _managedPods)
        {
            if (!desiredUids.Contains(uid))
            {
                await _runtime.StopContainerAsync(
                    managed.ContainerId);
                _managedPods.Remove(uid);
            }
        }

        // Start pods that are desired but not running
        foreach (var pod in desiredPods)
        {
            if (!_managedPods.ContainsKey(pod.Metadata.Uid))
            {
                await StartPodAsync(pod);
            }
        }
    }

    private async Task StartPodAsync(Pod pod)
    {
        foreach (var container in pod.Spec.Containers)
        {
            // Pull image if not present
            await _runtime.PullImageAsync(container.Image);

            // Create and start container
            var containerId = await _runtime.CreateContainerAsync(
                new ContainerConfig
                {
                    Image = container.Image,
                    Command = container.Command,
                    Args = container.Args,
                    EnvVars = container.Env,
                    Ports = container.Ports,
                    ResourceLimits = container.Resources.Limits,
                    ResourceRequests = container.Resources.Requests,
                    Mounts = GetMounts(pod, container)
                });

            await _runtime.StartContainerAsync(containerId);

            _managedPods[pod.Metadata.Uid] = new ManagedPod
            {
                Pod = pod,
                ContainerId = containerId
            };
        }
    }
}

8.2 kube-proxy

kube-proxy maintains network rules on each node to implement the Service abstraction. When a pod sends traffic to a Service ClusterIP, kube-proxy rules intercept and redirect that traffic to one of the backing pods. There are several proxy modes.

ModeMechanismPerformanceUse Case
iptablesLinux netfilter rulesGood (O(n) rule matching)Default for most clusters
IPVSLinux IP Virtual ServerExcellent (O(1) hashing)Large clusters (10K+ services)
eBPF (Cilium)BPF programs in kernelBest (no iptables overhead)Performance-critical workloads
nftablesLinux netfilter (new API)GoodReplacing iptables long-term
iptables vs IPVS: In iptables mode, kube-proxy creates a chain of rules that are evaluated sequentially — with 5,000 services, each connection requires walking through thousands of rules. IPVS uses consistent hashing for O(1) lookups, making it essential for large clusters. For clusters with fewer than 1,000 services, iptables is simpler and sufficient.

9. Pod Lifecycle & Management

Understanding the complete pod lifecycle is essential for building a reliable orchestration platform. Pods progress through a well-defined set of phases, and each phase transition must be handled correctly by the system.

9.1 Pod Phases

stateDiagram-v2 [*] --> Pending Pending --> Running: All containers started Pending --> Failed: Scheduling or image pull error Running --> Succeeded: All containers exited 0 Running --> Failed: Container exited non-zero Running --> Unknown: Node unreachable Failed --> [*] Succeeded --> [*] Unknown --> Running: Node becomes reachable Unknown --> Failed: Node confirmed dead

9.2 Pod Conditions

ConditionValuesMeaning
PodScheduledTrue/FalsePod has been assigned to a node
PodReadyToStartContainersTrue/FalseSandbox created (pod sandbox runtime)
InitializedTrue/FalseAll init containers completed successfully
ContainersReadyTrue/FalseAll containers in the pod are ready
ReadyTrue/FalsePod can accept and serve traffic

9.3 Probe Types

Probes are health-check mechanisms that kubelet uses to determine container health. Each probe type serves a distinct purpose in the pod lifecycle.

// C# implementation of probe evaluation logic
public class ProbeEvaluator
{
    public async Task<ProbeResult> EvaluateLivenessAsync(
        Container container, IContainerRuntime runtime)
    {
        var probe = container.LivenessProbe;
        if (probe == null) return ProbeResult.Success;

        try
        {
            var result = probe.Type switch
            {
                ProbeType.HttpGet => await EvaluateHttpProbeAsync(
                    probe.HttpGet),
                ProbeType.TcpSocket => await EvaluateTcpProbeAsync(
                    probe.TcpSocket),
                ProbeType.Exec => await EvaluateExecProbeAsync(
                    probe.Exec, runtime),
                ProbeType.Grpc => await EvaluateGrpcProbeAsync(
                    probe.Grpc),
                _ => ProbeResult.Success
            };

            // Update consecutive failure/success counts
            if (result == ProbeResult.Failure)
            {
                container.ConsecutiveFailures++;
                if (container.ConsecutiveFailures >= probe.FailureThreshold)
                {
                    return ProbeResult.Failure; // Trigger restart
                }
                return ProbeResult.Success; // Still within threshold
            }
            else
            {
                container.ConsecutiveFailures = 0;
                return ProbeResult.Success;
            }
        }
        catch (Exception)
        {
            container.ConsecutiveFailures++;
            if (container.ConsecutiveFailures >= probe.FailureThreshold)
                return ProbeResult.Failure;
            return ProbeResult.Success;
        }
    }

    private async Task<ProbeResult> EvaluateHttpProbeAsync(
        HttpGetAction action)
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
        var url = $"http://{action.Host}:{action.Port}{action.Path}";

        try
        {
            var response = await client.GetAsync(url);
            return response.StatusCode >= 200 &&
                   response.StatusCode < 400
                ? ProbeResult.Success
                : ProbeResult.Failure;
        }
        catch
        {
            return ProbeResult.Failure;
        }
    }
}

9.4 Init Containers

Init containers run sequentially before the main application containers start. They are useful for setup tasks such as database migrations, waiting for external services, or downloading configuration files. The pod will not proceed to the Running phase until all init containers complete successfully.

9.5 Pod Disruption Budgets (PDB)

PDBs protect against voluntary disruptions such as node drains, cluster upgrades, and autoscaler events. They specify the minimum number or percentage of pods that must remain available.

{
    "apiVersion": "policy/v1",
    "kind": "PodDisruptionBudget",
    "metadata": {
        "name": "api-server-pdb",
        "namespace": "production"
    },
    "spec": {
        "minAvailable": "60%",
        "selector": {
            "matchLabels": {
                "app": "api-server"
            }
        }
    }
}

10. Deployment & ReplicaSet

Deployments are the most common workload type in Kubernetes. They provide declarative updates for pods and ReplicaSets, enabling zero-downtime rolling updates and automatic rollbacks.

10.1 Deployment Strategy: Rolling Update

During a rolling update, the Deployment controller gradually replaces old pods with new ones, ensuring that the desired number of replicas is always maintained. The two key parameters are maxSurge (how many extra pods can be created) and maxUnavailable (how many pods can be down during the update).

graph LR subgraph "Before Update" A1[Pod v1] --- A2[Pod v1] --- A3[Pod v1] --- A4[Pod v1] --- A5[Pod v1] end subgraph "Step 1: maxSurge=1" B1[Pod v1] --- B2[Pod v1] --- B3[Pod v1] --- B4[Pod v1] --- B5[Pod v2] end subgraph "Step 2: maxUnavailable=1" C1[Pod v1] --- C2[Pod v1] --- C3[Pod v1] --- C5[Pod v2] --- C6[Pod v2] end subgraph "Step 3: Continue" D1[Pod v1] --- D2[Pod v1] --- D5[Pod v2] --- D6[Pod v2] --- D7[Pod v2] end subgraph "Complete" E1[Pod v2] --- E2[Pod v2] --- E3[Pod v2] --- E4[Pod v2] --- E5[Pod v2] end
// C# implementation of the Deployment controller's rolling update logic
public class DeploymentController
{
    public async Task<bool> ReconcileDeploymentAsync(
        Deployment deployment)
    {
        // Find the ReplicaSet managed by this deployment
        var replicaSets = await _apiClient
            .ListReplicaSetsAsync(deployment.Metadata.Namespace,
                deployment.Metadata.Labels);

        var currentRs = replicaSets
            .Where(rs => MatchesSelector(deployment, rs))
            .OrderByDescending(rs => rs.Metadata.CreationTimestamp)
            .FirstOrDefault();

        var desiredReplicas = deployment.Spec.Replicas;
        var maxSurge = deployment.Spec.Strategy.RollingUpdate
            ?.MaxSurge ?? 1;
        var maxUnavailable = deployment.Spec.Strategy.RollingUpdate
            ?.MaxUnavailable ?? 0;

        // Check if update is in progress
        if (deployment.Spec.Template.Spec.Image !=
            currentRs?.Spec.Template.Spec.Containers[0].Image)
        {
            // Create new ReplicaSet with updated template
            var newRs = await CreateReplicaSetAsync(deployment,
                deployment.Spec.Template);

            // Scale down old, scale up new
            var totalPods = currentRs.Status.Replicas + newRs.Status.Replicas;
            var desiredTotal = desiredReplicas +
                Math.Min(maxSurge, desiredReplicas);

            // Calculate scaling for both ReplicaSets
            var (oldScale, newScale) = CalculateRollingScales(
                currentRs.Status.Replicas,
                newRs.Status.Replicas,
                desiredReplicas,
                maxSurge,
                maxUnavailable);

            await _apiClient.ScaleReplicaSetAsync(currentRs, oldScale);
            await _apiClient.ScaleReplicaSetAsync(newRs, newScale);

            return false; // Not yet complete
        }

        // Normal scaling (no update in progress)
        await ScaleReplicaSetAsync(currentRs, desiredReplicas);
        return true;
    }

    private (int oldScale, int newScale) CalculateRollingScales(
        int currentReplicas, int newReplicas,
        int desired, int maxSurge, int maxUnavailable)
    {
        int available = currentReplicas + newReplicas;
        int targetAvailable = desired - maxUnavailable;

        int newTarget = Math.Min(
            desired,
            Math.Max(newReplicas, targetAvailable - currentReplicas));

        int oldTarget = Math.Min(
            desired,
            Math.Max(currentReplicas,
                desired + maxSurge - newReplicas));

        return (oldTarget, newTarget);
    }
}

10.2 Rollback Mechanism

When a deployment update fails (detected by pod readiness failures or exceeding the progress deadline), the system automatically reverts to the previous ReplicaSet. The rollback process is the same as a forward update — it is simply a rolling update from the failed template to the previous template.

Rolling Update ParameterDefaultDescription
maxSurge25%Maximum number of pods above desired count
maxUnavailable25%Maximum number of pods below desired count
minReadySeconds0Time a pod must be ready before considered available
progressDeadlineSeconds600Time after which a stalled deployment is considered failed
revisionHistoryLimit10Number of old ReplicaSets to retain for rollback

11. Service & Ingress

Services provide stable network endpoints for a dynamic set of pods. Since pods are ephemeral and can be rescheduled at any time, Services provide a fixed IP address and DNS name that routes traffic to healthy backing pods.

11.1 Service Types

TypeDescriptionUse Case
ClusterIPInternal-only virtual IP within the clusterInternal microservice communication
NodePortExposes service on each node's IP at a static portDevelopment, simple external access
LoadBalancerProvisions external load balancer (cloud provider)Production external-facing services
ExternalNameMaps service to a DNS name (CNAME)Accessing external services

11.2 Service Discovery

// Service DNS resolution within the cluster
// Format: <service-name>.<namespace>.svc.cluster.local
// Example: api-gateway.production.svc.cluster.local

// C# service discovery client
public class ServiceDiscoveryClient
{
    private readonly IDnsResolver _dns;

    public async Task<List<Endpoint>> ResolveServiceAsync(
        string serviceName, string ns)
    {
        // Kubernetes DNS resolves to cluster IP
        var clusterIp = await _dns.ResolveAsync(
            $"{serviceName}.{ns}.svc.cluster.local");

        // kube-proxy rules redirect ClusterIP traffic to pod IPs
        var endpoints = await _apiClient.GetEndpointsAsync(
            serviceName, ns);

        return endpoints.Subsets
            .SelectMany(s => s.Addresses
                .Select(a => new Endpoint
                {
                    Ip = a.Ip,
                    Port = s.Ports.First().Port,
                    Ready = a.Conditions.Ready == true
                }))
            .Where(e => e.Ready)
            .ToList();
    }
}

11.3 Ingress Controllers

Ingress provides HTTP/HTTPS routing at the cluster edge. An Ingress controller (typically NGINX, Traefik, or Envoy) reads Ingress resources and configures routing rules.

{
    "apiVersion": "networking.k8s.io/v1",
    "kind": "Ingress",
    "metadata": {
        "name": "api-ingress",
        "annotations": {
            "nginx.ingress.kubernetes.io/ssl-redirect": "true",
            "nginx.ingress.kubernetes.io/rate-limit": "1000",
            "cert-manager.io/cluster-issuer": "letsencrypt-prod"
        }
    },
    "spec": {
        "tls": [{
            "hosts": ["api.example.com"],
            "secretName": "api-tls-cert"
        }],
        "rules": [{
            "host": "api.example.com",
            "http": {
                "paths": [
                    {
                        "path": "/v1",
                        "pathType": "Prefix",
                        "backend": {
                            "service": {
                                "name": "api-v1",
                                "port": { "number": 8080 }
                            }
                        }
                    },
                    {
                        "path": "/v2",
                        "pathType": "Prefix",
                        "backend": {
                            "service": {
                                "name": "api-v2",
                                "port": { "number": 8080 }
                            }
                        }
                    }
                ]
            }
        }]
    }
}

11.4 Service Mesh Integration

For advanced traffic management, service meshes like Istio or Linkerd inject sidecar proxies alongside each pod. These proxies handle mTLS, load balancing, circuit breaking, retries, and observability without application code changes. The sidecar intercepts all inbound and outbound traffic, creating a transparent proxy layer.

12. ConfigMap & Secrets

Configuration management in containerized environments requires separating configuration from code. ConfigMaps handle non-sensitive configuration while Secrets handle sensitive data such as passwords, certificates, and API keys.

12.1 ConfigMap Structure

{
    "apiVersion": "v1",
    "kind": "ConfigMap",
    "metadata": {
        "name": "app-config",
        "namespace": "production"
    },
    "data": {
        "DATABASE_HOST": "postgres-cluster.internal",
        "DATABASE_PORT": "5432",
        "LOG_LEVEL": "info",
        "FEATURE_FLAG_NEW_UI": "true",
        "nginx.conf": "server { listen 80; ... }"
    }
}

12.2 Secrets Management

Secrets are base64-encoded by default but should be encrypted at rest for production. The encryption at rest feature uses AES-CBC or AES-GCM to encrypt Secret objects before writing them to etcd.

// Secret encryption configuration
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}  # Fallback for reading unencrypted secrets

12.3 Secret Injection Methods

MethodSecurityUse Case
Environment VariablesMedium (visible in procfs)Simple configuration
Volume MountHigh (file-based)Certificate files, config files
CSI Secrets StoreVery High (external provider)Vault, AWS Secrets Manager, Azure KV
Projected VolumeHighServiceAccount token + secrets
Security Warning: Never commit Secret manifests to version control. Use sealed-secrets, external-secrets-operator, or vault-sidecar to manage secrets securely. Enable encryption at rest for etcd, and always use RBAC to restrict which service accounts can read secrets in each namespace.

13. Persistent Volumes & Storage

Stateful workloads such as databases, message queues, and file servers require persistent storage that survives pod restarts and rescheduling. The Container Storage Interface (CSI) provides a standard API for storage providers to integrate with the orchestration platform.

13.1 Storage Architecture

graph TD A[Pod] -->|Volume Mount| B[PersistentVolumeClaim] B -->|Bound to| C[PersistentVolume] C -->|Provisioned by| D[StorageClass] D -->|Implements| E[CSI Driver] E -->|Manages| F[Physical Storage] F --> G[NFS] F --> H[Cloud Block Storage] F --> I[Distributed FS - Ceph] F --> J[Local NVMe]

13.2 StatefulSet for Stateful Workloads

// C# representation of StatefulSet management
public class StatefulSetController
{
    public async Task<bool> ReconcileStatefulSetAsync(
        StatefulSet sts)
    {
        var pods = await _apiClient.ListPodsAsync(
            sts.Metadata.Namespace,
            sts.Metadata.Labels);

        var healthyPods = pods
            .Where(p => p.Status.Phase == PodPhase.Running)
            .OrderBy(p => p.Metadata.Name)
            .ToList();

        // StatefulSets manage pods sequentially
        var desiredReplicas = sts.Spec.Replicas;

        if (healthyPods.Count < desiredReplicas)
        {
            // Create next pod in order (ordinal index)
            var nextOrdinal = healthyPods.Count;
            var podName = $"{sts.Metadata.Name}-{nextOrdinal}";

            // Ensure PVC exists before creating pod
            var pvcName = $"{sts.Spec.VolumeClaimTemplates[0]
                .Metadata.Name}-{podName}";
            await EnsurePvcExistsAsync(pvcName, sts);

            // Create pod with stable network identity
            await CreateStatefulPodAsync(sts, podName, nextOrdinal);

            return false;
        }

        // Verify ordered readiness
        for (int i = 0; i < healthyPods.Count; i++)
        {
            if (healthyPods[i].Metadata.Name !=
                $"{sts.Metadata.Name}-{i}")
            {
                return false; // Out of order
            }
        }

        return true;
    }
}

13.3 Storage Classes

StorageClassProvisionerPerformanceUse Case
fast-nvmelocal.csi1M+ IOPS, <0.1ms latencyDatabases, caching
standard-ssddisk.csi.cloudprovider10K IOPS, 1ms latencyGeneral stateful workloads
slow-hdddisk.csi.cloudprovider500 IOPS, 10ms latencyLog storage, archival
networked-nfsnfs.csi1K IOPS, 5ms latencyShared file systems
replicated-cephrook-ceph.csi50K IOPS, 2ms latencyDistributed storage

14. RBAC & Security

Role-Based Access Control (RBAC) is the primary authorization mechanism in Kubernetes. It determines which users, groups, and service accounts can perform which actions on which resources. A well-designed RBAC policy follows the principle of least privilege.

14.1 RBAC Components

graph LR A[User / ServiceAccount] -->|Has| B[RoleBinding / ClusterRoleBinding] B -->|References| C[Role / ClusterRole] C -->|Grants| D[Verbs on Resources]

14.2 RBAC Example

// Namespace-scoped Role: allows read-only access to pods
{
    "apiVersion": "rbac.authorization.k8s.io/v1",
    "kind": "Role",
    "metadata": {
        "name": "pod-reader",
        "namespace": "production"
    },
    "rules": [
        {
            "apiGroups": [""],
            "resources": ["pods", "pods/log"],
            "verbs": ["get", "list", "watch"]
        },
        {
            "apiGroups": ["apps"],
            "resources": ["deployments", "replicasets"],
            "verbs": ["get", "list", "watch"]
        }
    ]
}

// RoleBinding: assigns the role to a service account
{
    "apiVersion": "rbac.authorization.k8s.io/v1",
    "kind": "RoleBinding",
    "metadata": {
        "name": "read-pods-binding",
        "namespace": "production"
    },
    "subjects": [
        {
            "kind": "ServiceAccount",
            "name": "monitoring-agent",
            "namespace": "production"
        }
    ],
    "roleRef": {
        "kind": "Role",
        "name": "pod-reader",
        "apiGroup": "rbac.authorization.k8s.io"
    }
}

14.3 C# RBAC Evaluator

public class RbacAuthorizer
{
    public async Task<AuthorizationResult> AuthorizeAsync(
        UserInfo user, string resource, string verb,
        string namespaceName)
    {
        // Get all ClusterRoles bound to this user
        var clusterRoles = await GetBoundClusterRolesAsync(user);
        foreach (var role in clusterRoles)
        {
            if (role.Matches(resource, verb, namespaceName))
            {
                return AuthorizationResult.Allowed(
                    $"Granted by ClusterRole {role.Name}");
            }
        }

        // Get all namespace-scoped Roles bound to this user
        if (!string.IsNullOrEmpty(namespaceName))
        {
            var nsRoles = await GetBoundRolesAsync(
                user, namespaceName);
            foreach (var role in nsRoles)
            {
                if (role.Matches(resource, verb))
                {
                    return AuthorizationResult.Allowed(
                        $"Granted by Role {role.Name} " +
                        $"in {namespaceName}");
                }
            }
        }

        return AuthorizationResult.Denied(
            $"No RBAC rules allow {verb} on {resource}");
    }
}

public class ClusterRole
{
    public string Name { get; set; }
    public List<PolicyRule> Rules { get; set; }

    public bool Matches(string resource, string verb,
        string requestedNamespace = null)
    {
        return Rules.Any(rule =>
            rule.Verbs.Contains(verb) &&
            rule.Resources.Contains(resource) &&
            (string.IsNullOrEmpty(requestedNamespace) ||
             rule.Namespaces.Contains(requestedNamespace) ||
             rule.Namespaces.Contains("*")));
    }
}

14.4 Security Best Practices

  • Pod Security Standards: Use restricted, baseline, or privileged profiles to control what pod configurations are allowed.
  • Network Policies: Implement default-deny ingress and egress, then whitelist specific pod-to-pod communication.
  • Service Accounts: Disable auto-mounting of service account tokens when pods do not need cluster API access.
  • Image Security: Use image scanning (Trivy, Snyk), sign images with Sigstore/Cosign, and enforce image pull policies.
  • Runtime Security: Use Falco or Tetragon for runtime threat detection, seccomp profiles for syscall filtering, and AppArmor/SELinux for mandatory access control.
  • Supply Chain Security: Implement SLSA framework compliance, use SBOM generation, and verify image provenance.

15. Helm Charts & Package Management

Helm is the package manager for Kubernetes. It provides a templating engine, release management, and a repository system for distributing reusable application definitions. Understanding Helm internals is critical for managing complex deployments at scale.

15.1 Helm Chart Structure

my-chart/
├── Chart.yaml          # Chart metadata
├── Chart.lock          # Dependency lock file
├── values.yaml         # Default configuration values
├── templates/
│   ├── _helpers.tpl    # Template helper functions
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   └── tests/
│       └── test-connection.yaml
└── charts/             # Sub-chart dependencies

15.2 Helm Template Example

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.targetPort }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- if .Values.livenessProbe.enabled }}
          livenessProbe:
            httpGet:
              path: {{ .Values.livenessProbe.path }}
              port: {{ .Values.service.targetPort }}
            initialDelaySeconds: {{ .Values.livenessProbe.initialDelay }}
            periodSeconds: {{ .Values.livenessProbe.period }}
          {{- end }}
          {{- if .Values.readinessProbe.enabled }}
          readinessProbe:
            httpGet:
              path: {{ .Values.readinessProbe.path }}
              port: {{ .Values.service.targetPort }}
            initialDelaySeconds: {{ .Values.readinessProbe.initialDelay }}
            periodSeconds: {{ .Values.readinessProbe.period }}
          {{- end }}

15.3 Helm Values

# values.yaml - Default configuration
replicaCount: 3

image:
  repository: myregistry.io/api-server
  tag: "v2.1.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

livenessProbe:
  enabled: true
  path: /healthz
  initialDelay: 10
  period: 5

readinessProbe:
  enabled: true
  path: /ready
  initialDelay: 5
  period: 3

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
  targetCPU: 70
  targetMemory: 80

15.4 C# Helm Chart Installer

public class HelmChartInstaller
{
    public async Task<HelmRelease> InstallChartAsync(
        HelmInstallRequest request)
    {
        // Step 1: Template the chart
        var renderedManifests = await RenderChartAsync(
            request.ChartPath, request.Values);

        // Step 2: Apply resource ordering
        var orderedManifests = ApplyResourceOrdering(renderedManifests);

        // Step 3: Apply manifests with server-side apply
        foreach (var manifest in orderedManifests)
        {
            await _apiClient.ServerSideApplyAsync(manifest);
        }

        // Step 4: Record release in Helm storage
        var release = new HelmRelease
        {
            Name = request.ReleaseName,
            Chart = request.ChartPath,
            Revision = 1,
            Status = HelmReleaseStatus.Deployed,
            Values = request.Values,
            Timestamp = DateTime.UtcNow
        };

        await _storage.SaveReleaseAsync(release);
        return release;
    }

    private List<string> ApplyResourceOrdering(
        List<string> manifests)
    {
        var order = new Dictionary<string, int>
        {
            ["Namespace"] = 1,
            ["ServiceAccount"] = 2,
            ["ConfigMap"] = 3,
            ["Secret"] = 4,
            ["PersistentVolumeClaim"] = 5,
            ["Service"] = 6,
            ["Deployment"] = 7,
            ["DaemonSet"] = 7,
            ["StatefulSet"] = 7,
            ["Ingress"] = 8,
            ["PodDisruptionBudget"] = 9,
            ["HorizontalPodAutoscaler"] = 10
        };

        return manifests
            .OrderBy(m =>
            {
                var kind = ExtractKind(m);
                return order.TryGetValue(kind, out var o) ? o : 50;
            })
            .ToList();
    }
}

16. Auto-Scaling (HPA / VPA)

Auto-scaling is essential for optimizing resource utilization and maintaining performance during traffic spikes. The platform supports two primary auto-scaling mechanisms: Horizontal Pod Autoscaler (HPA) for scaling replica counts, and Vertical Pod Autoscaler (VPA) for adjusting resource requests.

16.1 HPA Algorithm

The HPA uses a proportional control algorithm to calculate the desired replica count based on current vs. target metrics.

desiredReplicas = ceil(
    currentReplicas × (
        currentMetricValue / targetMetricValue
    )
)
Current CPUTarget CPUCurrent ReplicasDesired Replicas
80%50%5ceil(5 × 80/50) = 8
40%50%5ceil(5 × 40/50) = 4
95%70%3ceil(3 × 95/70) = 5
30%70%10ceil(10 × 30/70) = 5

16.2 HPA Manifest

{
    "apiVersion": "autoscaling/v2",
    "kind": "HorizontalPodAutoscaler",
    "metadata": {
        "name": "api-hpa",
        "namespace": "production"
    },
    "spec": {
        "scaleTargetRef": {
            "apiVersion": "apps/v1",
            "kind": "Deployment",
            "name": "api-server"
        },
        "minReplicas": 3,
        "maxReplicas": 50,
        "metrics": [
            {
                "type": "Resource",
                "resource": {
                    "name": "cpu",
                    "target": {
                        "type": "Utilization",
                        "averageUtilization": 70
                    }
                }
            },
            {
                "type": "Resource",
                "resource": {
                    "name": "memory",
                    "target": {
                        "type": "Utilization",
                        "averageUtilization": 80
                    }
                }
            },
            {
                "type": "Pods",
                "pods": {
                    "metric": {
                        "name": "http_requests_per_second"
                    },
                    "target": {
                        "type": "AverageValue",
                        "averageValue": "1000"
                    }
                }
            }
        ],
        "behavior": {
            "scaleUp": {
                "stabilizationWindowSeconds": 60,
                "policies": [
                    {
                        "type": "Percent",
                        "value": 100,
                        "periodSeconds": 60
                    }
                ]
            },
            "scaleDown": {
                "stabilizationWindowSeconds": 300,
                "policies": [
                    {
                        "type": "Percent",
                        "value": 10,
                        "periodSeconds": 60
                    }
                ]
            }
        }
    }
}

16.3 VPA (Vertical Pod Autoscaler)

VPA adjusts the CPU and memory requests of pods based on historical usage. It recommends or automatically applies resource changes, helping right-size containers without manual tuning.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Auto"
  resourcePolicy:
    containerPolicies:
      - containerName: api
        minAllowed:
          cpu: 100m
          memory: 128Mi
        maxAllowed:
          cpu: 4
          memory: 8Gi
        controlledResources: ["cpu", "memory"]
HPA vs VPA: You cannot use both HPA and VPA on the same resource for the same metric (e.g., CPU). HPA adjusts replica count while VPA adjusts resource requests per pod. The recommended pattern is to use HPA for CPU/memory-based scaling and VPA in recommendation mode to guide resource request tuning.

17. Network Policies

Network Policies are the Kubernetes mechanism for implementing micro-segmentation and zero-trust networking. By default, all pods in a cluster can communicate with all other pods. Network Policies restrict this by specifying which pod-to-pod communication is allowed.

17.1 Default Deny Pattern

# Deny all ingress traffic in a namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress

17.2 Allow Specific Communication

# Allow frontend to communicate with backend only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

17.3 C# Network Policy Evaluator

public class NetworkPolicyEvaluator
{
    private readonly List<NetworkPolicy> _policies;

    public bool IsConnectionAllowed(
        Pod source, Pod destination, int port)
    {
        // Find all policies targeting the destination pod
        var applicablePolicies = _policies
            .Where(p =>
                p.Namespace == destination.Metadata.Namespace &&
                MatchesSelector(p.Spec.PodSelector,
                    destination) &&
                p.Spec.PolicyTypes.Contains("Ingress"))
            .ToList();

        if (!applicablePolicies.Any())
        {
            // No policies = allow all (if no default deny)
            return !_hasDefaultDeny[destination.Metadata.Namespace];
        }

        // Check if any policy allows this connection
        return applicablePolicies.Any(policy =>
            policy.Spec.Ingress.Any(rule =>
                rule.From.Any(sourceRule =>
                    MatchesPodSelector(sourceRule.PodSelector,
                        source)) &&
                rule.Ports.Any(p =>
                    p.Port == port)));
    }
}

18. Monitoring: Prometheus & Grafana

Observability is not optional in production. The monitoring stack provides visibility into cluster health, application performance, and resource utilization. Prometheus collects metrics, Alertmanager handles alerting, and Grafana provides visualization.

18.1 Prometheus Architecture

graph LR A[Application Pods] -->|/metrics endpoint| B[Prometheus Server] B -->|Scrapes every 15s| C[Time Series DB] B -->|Fires alerts| D[Alertmanager] D -->|Routes| E[Slack / PagerDuty / Email] F[Grafana] -->|Queries| B B -->|Long-term| G[Thanos / Cortex]

18.2 Key Metrics to Monitor

CategoryMetricAlert Threshold
API Serverapiserver_request_duration_secondsp99 > 1s
API Serverapiserver_request_total (5xx)> 1% of requests
etcdetcd_disk_wal_fsync_duration_secondsp99 > 100ms
etcdetcd_server_leader_changes_seen_total> 3 in 1 hour
Nodenode_cpu_seconds_total> 85% sustained
Nodenode_memory_MemAvailable_bytes< 15% free
Nodenode_filesystem_avail_bytes< 20% free
Podcontainer_cpu_usage_seconds_total> 90% of limit
Podcontainer_memory_working_set_bytes> 85% of limit
Podkube_pod_container_status_restarts_total> 5 in 1 hour
Schedulerscheduler_scheduling_duration_secondsp99 > 2s
Networkcontainer_network_receive_bytes_totalAnomaly detection

18.3 PrometheusRule Example

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cluster-alerts
spec:
  groups:
    - name: cluster-health
      rules:
        - alert: HighAPIServerLatency
          expr: |
            histogram_quantile(0.99,
              rate(apiserver_request_duration_seconds_bucket[5m])
            ) > 1
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "API server p99 latency exceeds 1s"
            description: "API server latency has been above 1s
              for 5 minutes. Current value: {{ $value }}s"

        - alert: PodCrashLooping
          expr: |
            increase(kube_pod_container_status_restarts_total[1h]) > 5
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Pod {{ $labels.pod }} is crash looping"

        - alert: NodeDiskSpaceLow
          expr: |
            (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}
            / node_filesystem_size_bytes) < 0.2
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Node {{ $labels.instance }} disk space
              below 20%"

18.4 Custom Metrics Adapter

To enable HPA scaling on custom application metrics (not just CPU/memory), the Custom Metrics API adapter translates application-specific Prometheus queries into the metrics API that HPA consumes.

// Custom metrics configuration for HPA
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-metrics-config
data:
  config.yaml: |
    rules:
      - seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
        resources:
          overrides:
            namespace: {resource: "namespace"}
            pod: {resource: "pod"}
        name:
          matches: "^(.*)_total$"
          as: "${1}_per_second"
        metricsQuery: 'sum(rate(<>[1m])) by (<.GroupBy>)'

19. Logging: The EFK Stack

Centralized logging aggregates logs from all containers and nodes into a searchable, analyzable system. The EFK stack (Elasticsearch, Fluentd, Kibana) is the most common logging solution, though Loki has gained significant traction as a lighter alternative.

19.1 Logging Architecture

graph TD A[Application Pods] -->|Write to stdout/stderr| B[Container Runtime] B -->|Log files| C[Fluentd DaemonSet] C -->|Parse & Transform| D[Elasticsearch] D -->|Index & Store| E[Hot/Warm/Cold Storage] F[Kibana] -->|Query & Visualize| D G[Alert Manager] -->|Log-based alerts| D

19.2 Fluentd Configuration

# Fluentd configuration for Kubernetes log collection
<source>
  @type tail
  @id in_tail_container_logs
  path /var/log/containers/*.log
  pos_file /var/log/fluentd-containers.log.pos
  tag kubernetes.*
  exclude_path ["/var/log/containers/fluentd*"]
  read_from_head true
  <parse>
    @type multi_format
    <pattern>
      format json
      time_key time
      time_format %Y-%m-%dT%H:%M:%S.%NZ
      keep_time_key true
    </pattern>
    <pattern>
      format regexp
      expression /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
      time_format %Y-%m-%dT%H:%M:%S.%N%:z
    </pattern>
  </parse>
</source>

# Enrich with Kubernetes metadata
<filter kubernetes.**>
  @type kubernetes_metadata
  @id filter_kube_metadata
  kubernetes_url "https://#{ENV['KUBERNETES_SERVICE_HOST']}:#{ENV['KUBERNETES_SERVICE_PORT']}"
  verify_ssl true
  ca_file /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  bearer_token_file /var/run/secrets/kubernetes.io/serviceaccount/token
  skip_labels false
  skip_container_metadata false
  skip_master_url true
  skip_namespace_metadata true
</filter>

# Output to Elasticsearch
<match kubernetes.**>
  @type elasticsearch
  @id out_es
  @log_level info
  host elasticsearch.logging.svc.cluster.local
  port 9200
  index_name kubernetes-logs
  type_name _doc
  logstash_format true
  logstash_prefix k8s-logs
  logstash_dateformat %Y.%m.%d
  include_tag_key true
  flush_interval 5s
  <buffer>
    @type file
    path /var/log/fluentd-buffers/kubernetes.buffer
    flush_mode interval
    retry_type exponential_backoff
    flush_interval 5s
    chunk_limit_size 2M
    queue_limit_length 8
    overflow_action block
  </buffer>
</match>

19.3 Log Retention Strategy

TierDurationStorageCost
Hot7 daysSSD-backed ElasticsearchHigh
Warm30 daysHDD-backed ElasticsearchMedium
Cold90 daysS3 / Object Storage (snapshot)Low
Archive1 year+Compressed S3 GlacierVery Low
Structured Logging: Always use structured JSON logging in application code. Unstructured text logs are difficult to parse, search, and alert on. Libraries like Serilog (C#), log4j2 (Java), and zap (Go) support structured logging natively. Include correlation IDs (trace ID, span ID) in every log entry for distributed tracing.

20. Multi-Cluster Federation

As organizations scale, a single cluster becomes insufficient for global reach, regulatory compliance, blast radius containment, or cloud provider diversity. Multi-cluster federation distributes workloads across multiple independent clusters.

20.1 Federation Patterns

PatternDescriptionUse Case
Multi-PrimaryMultiple clusters handle writes independentlyMulti-region active-active
Primary-SecondaryOne cluster is primary, others are read replicasDisaster recovery
Hub-SpokeCentral hub manages spoke clustersEnterprise platform teams
Mesh FederationService mesh connects clusters transparentlyMulti-cloud, hybrid cloud

20.2 C# Cluster Registry

public class MultiClusterManager
{
    private readonly Dictionary<string, IApiClient> _clusterClients;

    public async Task<ClusterDeploymentResult> DeployMultiClusterAsync(
        ApplicationManifest manifest,
        List<ClusterTarget> targets)
    {
        var results = new List<ClusterDeploymentResult>();

        foreach (var target in targets)
        {
            var client = _clusterClients[target.ClusterName];

            try
            {
                // Apply namespace and RBAC if needed
                await EnsureNamespaceAsync(client, target);

                // Apply manifests with cluster-specific overrides
                var adaptedManifest = AdaptManifestForCluster(
                    manifest, target);

                await client.ApplyAsync(adaptedManifest);

                // Verify deployment health
                var health = await WaitForHealthyAsync(
                    client, adaptedManifest);

                results.Add(new ClusterDeploymentResult
                {
                    Cluster = target.ClusterName,
                    Status = DeploymentStatus.Success,
                    Health = health
                });
            }
            catch (Exception ex)
            {
                results.Add(new ClusterDeploymentResult
                {
                    Cluster = target.ClusterName,
                    Status = DeploymentStatus.Failed,
                    Error = ex.Message
                });
            }
        }

        return AggregateResults(results);
    }

    private ApplicationManifest AdaptManifestForCluster(
        ApplicationManifest manifest, ClusterTarget target)
    {
        // Apply cluster-specific resource overrides
        var adapted = manifest.Clone();

        if (target.ResourceProfile == ResourceProfile.HighMemory)
        {
            adapted.SetResourceLimits("memory", "16Gi");
            adapted.SetReplicas(Math.Max(manifest.Replicas, 5));
        }

        // Apply cluster-specific config
        adapted.SetConfigValue("CLUSTER_NAME", target.ClusterName);
        adapted.SetConfigValue("REGION", target.Region);

        return adapted;
    }
}

20.3 Global Load Balancing

Global load balancers distribute traffic across clusters in different regions. They use DNS-based routing (latency-based, weighted, or geolocation) to direct users to the nearest healthy cluster.

graph TD A[User Request] --> B[Global DNS - Route53/CloudFlare] B -->|US-East| C[Cluster US-East] B -->|EU-West| D[Cluster EU-West] B -->|AP-South| E[Cluster AP-South] C --> F[API Pods] D --> G[API Pods] E --> H[API Pods] C -.->|Replication| D D -.->|Replication| E

21. Cost Estimation

Running a production Kubernetes cluster involves significant infrastructure costs. Understanding these costs is essential for capacity planning and budget management.

21.1 Infrastructure Cost Breakdown

ComponentSpecificationCountMonthly Cost (Cloud)
Master Nodes8 vCPU, 32 GB RAM, 100 GB NVMe3$1,200
Worker Nodes (General)16 vCPU, 64 GB RAM, 500 GB SSD20$6,000
Worker Nodes (Compute)32 vCPU, 64 GB RAM, 200 GB SSD10$4,500
Worker Nodes (Memory)8 vCPU, 128 GB RAM, 200 GB SSD5$2,250
etcd Storage (NVMe)500 GB NVMe SSD3$450
Load BalancersNetwork LB5$500
Persistent Storage10 TB totalN/A$1,000
Network Egress5 TB/monthN/A$400
Monitoring (Prometheus/Grafana)Dedicated nodes3$900
Logging (EFK)Dedicated nodes + storage3 + 2 TB$1,200
Container RegistryPrivate registry1$100
Total Monthly Estimate$18,500

21.2 Cost Optimization Strategies

  • Right-Sizing: Use VPA recommendations to right-size pod resource requests. Over-provisioned requests waste cluster capacity.
  • Spot/Preemptible Instances: Use spot instances for batch workloads and non-critical services. Savings of 60-80% compared to on-demand.
  • Cluster Autoscaler: Automatically remove underutilized nodes during low-traffic periods.
  • Bin Packing: The scheduler's LeastRequestedPriority scoring helps pack pods densely onto nodes, reducing the number of active nodes.
  • Reserved Instances: Commit to 1-3 year reserved instances for baseline workloads. Savings of 30-50%.

21.3 Cost Monitoring

// C# cost estimation model
public class ClusterCostEstimator
{
    public ClusterCostReport EstimateMonthlyCost(
        ClusterSpec spec)
    {
        var report = new ClusterCostReport();

        // Master node costs
        report.MasterNodes = spec.MasterNodes *
            GetNodeCost(spec.MasterNodeSpec);

        // Worker node costs (separate by pool)
        foreach (var pool in spec.NodePools)
        {
            report.WorkerNodes += pool.Count *
                GetNodeCost(pool.Spec);
        }

        // Storage costs
        report.Storage = spec.PersistentStorageGB *
            0.10m; // $0.10/GB/month for SSD

        // Network costs
        report.NetworkEgress = spec.MonthlyEgressTB *
            85m; // $85/TB egress

        // Monitoring & logging overhead
        report.Overhead = report.Total * 0.12m; // ~12% overhead

        report.Total = report.MasterNodes +
            report.WorkerNodes + report.Storage +
            report.NetworkEgress + report.Overhead;

        return report;
    }
}

22. Testing

Testing an orchestration platform requires a multi-layered strategy that validates correctness at every level — from individual controller logic to end-to-end cluster behavior.

22.1 Testing Pyramid

graph TD A[End-to-End Tests] -->|Slowest, Most Expensive| B[Integration Tests] B -->|Medium Speed| C[Component Tests] C -->|Fastest, Cheapest| D[Unit Tests]

22.2 Test Categories

LevelWhat We TestFrameworkTarget Time
UnitRBAC evaluator, probe logic, scheduling scoringxUnit / NUnit< 5 seconds total
ComponentController reconciliation in isolationxUnit + Mock API< 30 seconds
IntegrationController + real API server + etcdTestContainers< 5 minutes
E2EFull cluster lifecycle: deploy, scale, upgrade, deleteGinkgo / Playwright< 30 minutes
ChaosNode failures, network partitions, etcd leader lossChaos Mesh / LitmusOngoing

22.3 C# Controller Unit Test

public class DeploymentControllerTests
{
    [Fact]
    public async Task RollingUpdate_CreatesNewReplicaSet()
    {
        // Arrange
        var mockApi = new Mock<IApiClient>();
        mockApi.Setup(x => x.ListReplicaSetsAsync(
            It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
            .ReturnsAsync(new List<ReplicaSet>
            {
                new ReplicaSet
                {
                    Metadata = new ObjectMetadata
                    {
                        Name = "app-v1",
                        Labels = new Dictionary<string, string>
                        {
                            ["app"] = "web"
                        }
                    },
                    Spec = new ReplicaSetSpec
                    {
                        Replicas = 5,
                        Template = new PodTemplate
                        {
                            Containers = new List<Container>
                            {
                                new() { Image = "app:v1" }
                            }
                        }
                    },
                    Status = new ReplicaSetStatus { Replicas = 5 }
                }
            });

        var deployment = new Deployment
        {
            Metadata = new ObjectMetadata
            {
                Name = "app",
                Namespace = "default"
            },
            Spec = new DeploymentSpec
            {
                Replicas = 5,
                Strategy = new DeploymentStrategy
                {
                    Type = RollingUpdate,
                    RollingUpdate = new RollingUpdateStrategy
                    {
                        MaxSurge = 1,
                        MaxUnavailable = 0
                    }
                },
                Template = new PodTemplate
                {
                    Containers = new List<Container>
                    {
                        new() { Image = "app:v2" }
                    }
                }
            }
        };

        var controller = new DeploymentController(
            mockApi.Object, Mock.Of<ILogger>());

        // Act
        var result = await controller.ReconcileDeploymentAsync(
            deployment);

        // Assert
        Assert.False(result); // Not yet complete
        mockApi.Verify(x => x.CreateReplicaSetAsync(
            It.Is<ReplicaSet>(rs =>
                rs.Spec.Template.Containers[0].Image == "app:v2")),
            Times.Once);
    }
}

22.4 Chaos Engineering Tests

  • Node Failure: Kill a worker node and verify pods are rescheduled within 30 seconds.
  • etcd Leader Loss: Kill the etcd leader and verify the cluster recovers within 10 seconds with no data loss.
  • Network Partition: Simulate network isolation between two nodes and verify pod-to-pod communication degrades gracefully.
  • Disk Pressure: Fill the disk on a worker node and verify kubelet evicts pods before the node becomes unresponsive.
  • API Server Overload: Send 10,000 concurrent API requests and verify the server handles them with acceptable latency.

23. Interview Q&A

Note: These questions are designed for senior+ level interviews at top technology companies. Each answer should demonstrate deep architectural understanding, not just surface-level knowledge.

Q1: How does the Kubernetes scheduler decide which node to place a pod on?

The scheduler uses a two-phase approach: filtering and scoring. In the filtering phase, it eliminates nodes that cannot run the pod due to insufficient resources, unmet node selectors, taint/toleration mismatches, or volume binding constraints. In the scoring phase, it evaluates the remaining feasible nodes using a set of scoring plugins — LeastRequestedPriority, BalancedResourceAllocation, NodeAffinityPriority, InterPodAffinityPriority, and others. Each plugin assigns a score (0-100), and the node with the highest aggregate score is selected. If there is a tie, the node is chosen arbitrarily.

Q2: What happens when an etcd node fails in a 3-node cluster?

Raft consensus requires a majority (quorum) of nodes to be available. With 3 nodes, quorum is 2. If 1 node fails, the remaining 2 nodes maintain quorum and continue operating normally. The failed node's data will be replicated when it recovers. If 2 nodes fail simultaneously, the cluster loses quorum and becomes read-only — no new writes can be accepted. This is why production clusters run 3 or 5 etcd nodes. With 5 nodes, quorum is 3, so the cluster can tolerate 2 simultaneous failures.

Q3: Explain the difference between a Deployment, ReplicaSet, DaemonSet, and StatefulSet.

A Deployment manages ReplicaSets and provides declarative rolling updates. A ReplicaSet ensures a specified number of pod replicas are running at any time. A DaemonSet ensures exactly one pod runs on every node (or a subset of nodes matching a selector) — used for node-level agents like log collectors and monitoring exporters. A StatefulSet provides ordered, sequential pod management with stable network identities and persistent storage — designed for stateful workloads like databases, where each pod needs a unique, persistent identity and ordered startup/shutdown.

Q4: How would you handle a scenario where pods keep getting OOMKilled?

First, check if the container's memory limit is too low for its actual usage by examining container_memory_working_set_bytes vs. kube_pod_container_resource_limits. Second, profile the application for memory leaks using heap dumps. Third, consider increasing the memory limit in the pod spec. Fourth, check if VPA is recommending higher memory requests. Fifth, review if there are many sidecar containers sharing the same memory limit. Finally, check if the node itself is under memory pressure and kubelet is evicting pods proactively. The key insight is distinguishing between container-level OOMKill (memory limit exceeded) and node-level eviction (node runs out of memory).

Q5: Design a zero-downtime deployment strategy for a stateful application (e.g., a database).

Stateful applications cannot use standard rolling updates because data must be migrated and replicas must be synchronized before old pods are terminated. The approach is: (1) Use a StatefulSet with ordered deployment strategy. (2) Implement readiness probes that verify the new replica has caught up with the primary. (3) Use a custom controller or operator that performs a "scale up, sync, verify, scale down" sequence. (4) Ensure PersistentVolumeClaims are not deleted during the update. (5) Use a Service with a headless configuration for stable DNS. (6) Implement backup before the upgrade and verify restore capability. (7) Use PodDisruptionBudgets to prevent simultaneous termination of multiple replicas.

Q6: How does Kubernetes handle network connectivity between pods on different nodes?

When a pod on Node A sends traffic to a pod on Node B, the packet first hits the CNI plugin's virtual network interface on Node A. The CNI plugin has configured routing rules that know Node B's pod CIDR is reachable via Node B's IP address. The packet is encapsulated (in overlay networks like Flannel/VXLAN) or routed directly (in routed networks like Calico) to Node B. On Node B, the CNI plugin de-encapsulates (if applicable) and delivers the packet to the target pod's network namespace. The kube-proxy component handles Service-level routing by setting up iptables/IPVS rules that intercept traffic to Service ClusterIPs and redirect to backing pod IPs.

Q7: What is the control plane failure domain, and how do you minimize blast radius?

The control plane failure domain is the scope of impact when a control plane component fails. If the API server goes down, no new workloads can be deployed and no existing workloads can be modified — but already-running pods continue operating. If etcd loses quorum, the entire cluster becomes unmanageable. To minimize blast radius: (1) Run 3+ API server replicas across availability zones. (2) Run 3 or 5 etcd nodes with NVMe storage. (3) Use PodDisruptionBudgets to ensure workload availability during control plane maintenance. (4) Implement cluster federation so a single cluster failure does not affect the entire platform. (5) Use multiple clusters for different failure domains (region, cloud provider). (6) Implement circuit breakers in client applications so API server failures do not cascade.

Q8: How would you debug a pod stuck in "ContainerCreating" state?

The debugging steps are: (1) Run kubectl describe pod to see the Events section, which typically reveals the root cause. (2) Common causes include: image pull failures (check image name, tag, registry authentication), volume mount failures (PVC not bound, NFS server unreachable), network plugin errors (CNI configuration issues), and resource constraints (insufficient CPU/memory on the node). (3) Check node conditions with kubectl describe node for MemoryPressure, DiskPressure, or PIDPressure. (4) Check kubelet logs on the assigned node: journalctl -u kubelet. (5) Check container runtime logs: crictl logs or docker logs. (6) Verify the service account token and image pull secrets are valid.

Q9: Explain the reconciliation pattern and why Kubernetes uses it instead of imperative commands.

The reconciliation pattern is the heart of Kubernetes' declarative model. Instead of issuing imperative commands ("create 5 pods"), you declare desired state ("there should be 5 pods with this spec"), and controllers continuously reconcile actual state with desired state. This provides several critical properties: (1) Self-healing — if a pod crashes, the controller detects the discrepancy and recreates it. (2) Convergence — multiple concurrent changes are handled gracefully because the system always converges to the declared state. (3) Idempotency — applying the same manifest twice produces the same result. (4) Auditability — the desired state is explicitly declared and version-controlled. (5) Simplified operations — operators declare intent, not steps, making the system easier to reason about at scale.

Q10: How do you implement multi-tenancy in a Kubernetes cluster?

Multi-tenancy requires isolation at multiple layers: (1) Namespace isolation: Each tenant gets a dedicated namespace. (2) RBAC: Tenants can only manage resources in their namespace via RoleBindings. (3) Resource Quotas: Limit CPU, memory, storage, and object counts per namespace. (4) Limit Ranges: Set default and maximum resource requests per container. (5) Network Policies: Default-deny ingress/egress per namespace, whitelist only necessary cross-namespace communication. (6) Admission Controllers: Validate that tenants cannot create resources outside their allowed configurations. (7) Pod Security Standards: Enforce baseline or restricted profiles per namespace. (8) Separate logging and monitoring: Each tenant sees only their own metrics and logs via RBAC-scoped dashboards.

24. Conclusion

Building a Kubernetes-style container orchestration platform is one of the most complex and rewarding engineering challenges in modern infrastructure. The system touches every domain of distributed systems — consensus algorithms, scheduling theory, network programming, storage systems, security, and observability.

The key takeaways from this guide are: (1) The declarative, reconciliation-based architecture is the foundation that makes everything else work. (2) etcd is the single point of criticality — treat it with the highest level of care. (3) The API server is the enforcement point for all security policies — invest heavily in its hardening. (4) The scheduler is a performance-critical path — benchmark and tune it for your workload patterns. (5) Observability is not optional — you cannot operate what you cannot see.

Whether you are building your own orchestration platform, contributing to Kubernetes, or simply operating clusters at scale, understanding these internals will make you a significantly more effective platform engineer. The questions in the interview section represent the depth of understanding expected at staff-level positions at major technology companies.

The journey from a single container on a laptop to a multi-cluster orchestration platform spanning hundreds of nodes across multiple regions is one of increasing abstraction and automation. Each layer of the system — from the container runtime to the scheduler to the API server — exists because manual operations cannot scale. The tools and patterns we have explored provide the foundation for building infrastructure that serves millions of users without requiring a correspondingly large operations team.

As the cloud-native ecosystem continues to evolve, with WebAssembly runtimes, AI-driven scheduling, and edge computing pushing the boundaries of what orchestration platforms can do, the fundamental principles covered here — declarative state, reconciliation loops, and extensible APIs — will remain the bedrock of container orchestration for years to come. Investing in a deep understanding of these principles will pay dividends throughout your career in platform engineering and distributed systems architecture.

© 2026 Ayodhyya. All rights reserved.

Designed for senior+ engineers building production-grade infrastructure.