← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202618 phút đọc15 min read

Giới thiệu và Kiến trúcIntroduction and Architecture

Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.

Tổng quan

Kubernetes (K8s) là một nền tảng mã nguồn mở để tự động hóa việc triển khai, scale và vận hành các ứng dụng container hóa trên cả một cụm máy. Nó được thiết kế ban đầu tại Google — kế thừa một hệ thống nội bộ tên Borg — được mã nguồn mở năm 2014, và hiện là dự án hàng đầu của Cloud Native Computing Foundation (CNCF). Tên gọi bắt nguồn từ tiếng Hy Lạp nghĩa là “người lái tàu”; “K8s” là cách viết tắt (chữ K, tám chữ cái, rồi s).

Để hiểu vì sao Kubernetes tồn tại, hãy nhìn theo hành trình đóng gói ứng dụng. Bare-metal chạy một app trên một server — lãng phí và cấp phát chậm. Máy ảo (VM) nhồi nhiều OS cô lập lên cùng một host, nhưng mỗi VM mang theo cả một guest OS đầy đủ: nặng, boot chậm, ship lớn. Container (Docker, 2013) dùng chung kernel của host và chỉ đóng gói app cùng các dependency của nó — một container image nhỏ, khởi động trong vài mili-giây, và chạy y hệt nhau trên laptop, trong CI, và trên production. Container đã giải quyết bài toán đóng gói. Nhưng khi bạn có hàng trăm container trải trên hàng chục node, một lớp vấn đề mới xuất hiện: mỗi container nên chạy trên node nào? Chuyện gì xảy ra khi một container crash, hay cả một node chết? Làm sao rollout phiên bản mới không downtime, rollback khi nó lỗi, scale replica theo tải, để các container tìm và nói chuyện với nhau, gắn storage và cấu hình? Đó là orchestration, và đây chính là khoảng trống mà Kubernetes lấp đầy.

Giải pháp thay thế cho orchestrator là một đống script tự chế dễ vỡ: logic đặt container, health check, vòng lặp restart, script rolling-deploy, các mánh service-discovery. Kubernetes biến từng mối lo vận hành đó thành một primitive khai báo (declarative) qua API. Bạn mô tả điểm đến; nền tảng tự tìm đường và giữ bạn ở đó.

Một mô hình tư duy

Kubernetes được hiểu tốt nhất như một cỗ máy control-loop. Mọi thứ đều là object lưu trong một database (etcd); mỗi object có một spec (điều bạn muốn) và một status (điều đang thực sự có). Các controller liên tục theo dõi object và làm cho status khớp spec. kubectl apply không làm gì theo kiểu mệnh lệnh — nó ghi lại ý định của bạn, và controller biến nó thành hiện thực. Thấm được đúng một ý này thì phần lớn hành vi của Kubernetes — self-healing, rollout, autoscaling, GitOps — hết còn là phép màu và trở nên hiển nhiên.

Kiến thức nền tảng

Mô hình khai báo và desired state

Automation truyền thống mang tính mệnh lệnh (imperative): bạn phát ra các lệnh (“khởi động container này”, “thêm một replica”, “restart process kia”). Nếu một lệnh bị bỏ lỡ, nếu một máy reboot, hoặc một bước lỗi giữa chừng, hệ thống trôi lệch (drift) và bạn phải suy luận nó đã đến trạng thái này bằng cách nào để sửa.

Kubernetes mang tính khai báo (declarative): bạn nộp trạng thái cuối mong muốn (“tôi muốn 3 replica của image web:1.4.2 sau một load balancer”), và việc của hệ thống là làm cho thực tế khớp — và giữ cho nó luôn khớp. Đây là khác biệt giữa một danh sách việc cần làm và một bộ điều nhiệt (thermostat). Thermostat không bắn một lệnh “làm nóng” một lần rồi thôi; nó liên tục so sánh nhiệt độ phòng (thực tế) với nhiệt độ mục tiêu (mong muốn) và hành động cho tới khi khớp, rồi tiếp tục theo dõi.

Thiết kế này là level-triggered, không phải edge-triggered. Controller không phản ứng với từng sự kiện riêng lẻ (“một pod vừa bị xóa”); chúng liên tục suy ra lại hành động đúng từ trạng thái hiện tại (“muốn 3, thực tế 2, vậy tạo thêm một”). Đó là lý do Kubernetes phục hồi sạch sẽ sau sự kiện bị bỏ lỡ, controller restart, và lỗi cục bộ: nó không bao giờ phụ thuộc vào việc đã thấy một sự kiện cụ thể — nó luôn đọc lại thế giới và hội tụ.

Kiến trúc cụm (cluster)

Một cụm Kubernetes chia thành hai mặt phẳng: control plane (bộ não — ra quyết định toàn cục và lưu trạng thái) và các worker node (cơ bắp — chạy container của bạn). Trong production chúng chạy trên các máy tách biệt; các dịch vụ managed (EKS, GKE, AKS) chạy control plane hộ bạn.

Kiến trúc cluster Kubernetes
  1. Control Plane
    kube-apiserver
    etcdkho trạng thái
    kube-scheduler
    kube-controller-manager
    (cloud-controller-manager)
  2. (mọi traffic đi qua API)
    Nodes
    Nodekubelet, kube-proxy, containerd
    Nodekubelet, kube-proxy, containerd
    Nodekubelet, kube-proxy, containerd

Các thành phần control plane:

Các thành phần node (trên mọi worker):

Mô hình object/resource

Mọi thứ bạn quản lý trong Kubernetes là một object — một bản ghi bền vững về ý định. Mọi object đều chia sẻ cùng bốn field cấp cao nhất:

FieldÝ nghĩa
apiVersionNhóm API và version nào định nghĩa kind này — ví dụ v1 (core), apps/v1 (Deployment), batch/v1 (Job), networking.k8s.io/v1 (Ingress)
kindLoại object — Pod, Deployment, Service, ConfigMap, …
metadataDanh tính và tổ chức — name, namespace, labels, annotations, uid
specTrạng thái mong muốn bạn khai báo — hình dạng tùy theo kind
statusTrạng thái quan sát được, do hệ thống điền và cập nhật liên tục — bạn không viết nó

API được nhóm và đánh version. Nhóm (group) đặt namespace cho các kind liên quan (apps, batch, networking.k8s.io); version tốt nghiệp qua alpha → beta → stable (v1). Các version API bị deprecated cuối cùng sẽ bị gỡ, đây là mối lo lặp lại khi upgrade — một manifest dùng apiVersion đã bị gỡ sẽ bị từ chối sau khi upgrade.

Label và selector là chất keo loose-coupling của toàn hệ thống. Label là một cặp key: value tùy ý trên metadata của object (app: web, tier: backend). Selector khớp các object theo label của chúng. Một Service tìm Pod của nó bằng selector; một Deployment sở hữu các Pod có label khớp selector của nó. Không có gì được nối bằng tham chiếu cứng — tất cả khớp qua label, và đó là lý do hệ thống dễ ghép nối đến vậy. Annotation trông tương tự (key: value) nhưng dùng cho metadata không định danh mà công cụ đọc (thông tin build, config-hash, cấu hình ingress controller); bạn không bao giờ select theo annotation.

kubectl và kubeconfig

kubectl là CLI chính — một client mỏng phủ lên API server. Nó đọc thông tin kết nối từ file kubeconfig (mặc định ~/.kube/config), gói ba danh sách cùng một lựa chọn hiện tại:

Chuyển context là cách bạn di chuyển giữa các cluster hay namespace mặc định mà không phải gõ lại flag:

kubectl config get-contexts                 # liệt kê context, * là hiện tại
kubectl config use-context prod-eks         # đổi cluster
kubectl config set-context --current --namespace=payments   # đổi ns mặc định

Các công cụ như kubectx/kubens làm việc này còn nhanh hơn. Hãy coi kubeconfig như một credential — nó cấp quyền truy cập cluster.

Namespace

Một namespace là một cluster ảo bên trong một cluster vật lý — một phạm vi cho tên và một ranh giới cho policy. Tên object phải duy nhất trong một namespace, không phải trên toàn cluster, nên webdevwebprod cùng tồn tại. Namespace là nơi bạn gắn RBAC (ai được làm gì ở đây), ResourceQuota (namespace này được dùng bao nhiêu CPU/memory/object), và LimitRange (request/limit mặc định và tối đa cho mỗi Pod). Mỗi cluster có sẵn default, kube-system (add-on của control plane), kube-public, và kube-node-lease. Lưu ý không phải mọi thứ đều có namespace: Node, PersistentVolume, bản thân Namespace, và ClusterRole là cluster-scoped.

Khái niệm chính

Một request kubectl apply chảy qua hệ thống như thế nào

Theo dõi một lệnh từ đầu đến cuối sẽ nối mọi thành phần lại với nhau. Giả sử bạn chạy kubectl apply -f deployment.yaml cho một Deployment 3 replica:

  1. kubectl đọc YAML, lấy endpoint API và credential từ kubeconfig, và gửi một HTTP PATCH/POST tới kube-apiserver.
  2. API server xác thực người gọi, chạy authorization (RBAC: user này có được tạo Deployment ở đây không?), rồi admission control — mutating webhook có thể chèn giá trị mặc định (ví dụ một sidecar), validating webhook có thể từ chối object (ví dụ “không cho image :latest”). Object được kiểm tra theo schema của nó.
  3. API server lưu object Deployment vào etcd. Tới đây phần mệnh lệnh đã xong — chưa có gì được “chạy” cả. Object chỉ đơn giản tồn tại với một spec và một status rỗng.
  4. Deployment controller (trong kube-controller-manager) đang theo dõi các Deployment. Nó thấy một cái mới với 0 ReplicaSet và tạo một object ReplicaSet (một lần ghi nữa vào API/etcd).
  5. ReplicaSet controller thấy một ReplicaSet muốn 3 replica và hiện có 0, nên nó tạo 3 object Pod — vẫn chỉ là bản ghi trong etcd, mỗi cái chưa gán node (nodeName rỗng).
  6. scheduler đang theo dõi các Pod chưa được schedule. Với mỗi Pod, nó lọc và chấm điểm các node rồi ghi nodeName được chọn lại lên Pod (một binding).
  7. Trên mỗi node được chọn, kubelet thấy một Pod giờ đã gán vào mình, gọi container runtime (qua CRI) pull image và khởi động container, thiết lập mạng (qua CNI), mount volume, và bắt đầu chạy probe.
  8. Khi container lên, kubelet cập nhật status của từng Pod qua API server vào etcd. ReplicaSet và Deployment controller quan sát các status và cập nhật status của chính mình — status.readyReplicas leo lên 3, và Deployment báo Available.

Để ý: không thành phần nào ra lệnh xuống theo một chuỗi. Mỗi cái theo dõi API, thấy khoảng cách giữa mong muốn và thực tế, và hành động. Đó là mô hình reconciliation đang vận hành.

Ví dụ: một manifest tối thiểu

Object hữu ích nhỏ nhất là một Pod đơn lẻ, nhưng trong thực tế bạn gần như luôn khai báo một Deployment (nó quản lý Pod hộ bạn). Đây là một Deployment tối thiểu, dễ đọc, cùng Service để expose nó:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3                      # desired state: luôn giữ 3 Pod chạy
  selector:
    matchLabels:
      app: web                     # Deployment này sở hữu Pod có app=web
  template:                        # Pod template được dập ra cho mỗi replica
    metadata:
      labels:
        app: web                   # PHẢI khớp selector ở trên
    spec:
      containers:
        - name: web
          image: nginx:1.27        # tag bất biến — không dùng :latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web                       # route tới Pod có label app=web
  ports:
    - port: 80
      targetPort: 80

template.metadata.labels của Deployment phải được spec.selector của nó khớp — chính liên kết đó khiến Deployment sở hữu các Pod ấy. selector của Service khớp cùng label một cách độc lập để tìm endpoint. Đổi image thành nginx:1.28 và apply lại: Deployment controller tạo một ReplicaSet mới và thực hiện rolling update, tất cả từ một chỉnh sửa khai báo duy nhất đó.

Các lệnh kubectl cốt lõi

# Apply / kiểm tra
kubectl apply -f app.yaml                 # tạo hoặc cập nhật từ manifest
kubectl get pods -o wide                  # liệt kê Pod, kèm node và IP
kubectl get deploy,svc                    # nhiều kind cùng lúc
kubectl describe pod web-6d4c...          # events + config — vì sao Pending/CrashLooping?
kubectl get pod web-6d4c... -o yaml       # object đầy đủ, gồm cả status

# Debug
kubectl logs -f deploy/web                # stream log
kubectl logs web-6d4c... --previous       # log của container crash lần trước
kubectl exec -it web-6d4c... -- sh        # shell vào một container đang chạy
kubectl get events --sort-by=.lastTimestamp

# Quản lý
kubectl rollout status deploy/web
kubectl rollout undo deploy/web           # rollback về ReplicaSet trước
kubectl scale deploy/web --replicas=5
kubectl delete -f app.yaml

# Context / namespace
kubectl config use-context prod
kubectl get pods -n kube-system
kubectl api-resources                     # mọi kind, apiVersion, và tên viết tắt

kubectl describekubectl get events là điểm dừng đầu tiên để tìm vì sao thứ gì đó không hoạt động — câu trả lời hầu như luôn nằm trong events (lỗi pull image, schedule thất bại, probe thất bại, OOM kill).

Dùng imperative so với declarative

kubectl hỗ trợ cả lệnh imperative (kubectl run, kubectl create deployment, kubectl scale, kubectl edit) lẫn declarative (kubectl apply -f). Imperative tiện cho khám phá và debug, nhưng apply declarative trên các file lưu trong Git mới là cách tiếp cận cấp production: manifest là nguồn sự thật, thay đổi review được, và cluster có thể dựng lại từ chúng. Đây là nền tảng mà GitOps xây dựng trên đó.

Best Practices

  1. Tư duy declarative, không phải imperative. Mô tả desired state trong manifest và để controller hội tụ; tránh các thao tác imperative một lần trong production. Nếu không tái tạo được cluster từ file, bạn không thực sự biết trạng thái của nó.
  2. Lưu mọi manifest trong Git và apply chúng, không edit trực tiếp. kubectl apply/GitOps thay vì kubectl edit. Repository là nguồn sự thật; cluster nên phản chiếu một trạng thái đã được review và commit.
  3. Hiểu hình dạng object API (apiVersion/kind/metadata/spec/status) trước khi với tới công cụ. Helm và Kustomize chỉ sinh ra các object này — bạn không debug được thứ chúng tạo nếu các primitive vẫn là bí ẩn.
  4. Ghim apiVersion có chủ đích và theo dõi deprecation. API bị gỡ qua các bản phát hành; dùng công cụ như pluto hoặc để ý cảnh báo deprecation của kubectl để một lần upgrade không từ chối manifest của bạn.
  5. Không bao giờ dùng tag image latest. Triển khai tag bất biến, có version (hoặc digest) để rollout, rollback, và audit tái lập được và có ý nghĩa.
  6. Dùng namespace để phân vùng team và environment, và gắn RBAC, ResourceQuota, LimitRange cho mỗi namespace để một sai sót ở một namespace không thể ngốn cả cluster.
  7. Áp dụng RBAC tối thiểu quyền ngay từ ngày đầu. Cấp các Role hẹp gắn với ServiceAccount/user cụ thể; đừng phát cluster-admin một cách bừa bãi.
  8. Coi kubeconfig như một secret và dùng context tường minh. Đặt tên context rõ ràng (prod-eks, staging), xác nhận context hiện tại trước các lệnh phá hủy, và tách credential prod khỏi dev.
  9. Đặt label nhất quán khắp nơi. Áp dụng bộ label khuyến nghị app.kubernetes.io/*; selector, dashboard, và phân bổ chi phí đều phụ thuộc vào một sơ đồ label sạch, dễ đoán.
  10. Dùng annotation cho metadata, label cho selection. Đừng nhồi label với dữ liệu bạn không bao giờ select (build SHA, URL) — chúng thuộc về annotation.
  11. Bảo vệ và backup etcd. Nó là toàn bộ trạng thái cluster; chạy quorum số lẻ, mã hóa secret at-rest, và chụp (và test-restore) snapshot — trên cluster managed đây là việc của provider, nhưng hãy biết ranh giới ở đâu.
  12. Ưu tiên control plane managed (EKS/GKE/AKS) trừ khi có lý do không. Tự chạy và vá API server cùng etcd là công việc thật, liên tục; để provider lo và tập trung vào workload.
  13. Học cách đọc events trước tiên. Khi thứ gì đó kẹt, kubectl describekubectl get events hầu như luôn chứa lý do (ImagePullBackOff, FailedScheduling, OOMKilled) — với tới chúng trước khi đoán mò.
  14. Giữ tách bạch trong đầu control plane và workload. Scheduler quyết định ở đâu, kubelet chạy, API server là cửa trước duy nhất — biết thành phần nào sở hữu một hành vi giúp troubleshoot nhanh hơn nhiều.

Tài liệu tham khảo

Part of the Kubernetes Roadmap knowledge base.

Overview

Kubernetes (K8s) is an open-source platform for automating the deployment, scaling, and operation of containerized applications across a fleet of machines. It was originally designed at Google — descended from an internal system called Borg — open-sourced in 2014, and is now the flagship project of the Cloud Native Computing Foundation (CNCF). The name comes from the Greek word for “helmsman”; “K8s” is a numeronym (K, eight letters, s).

To see why Kubernetes exists, follow the arc of application packaging. Bare-metal ran one app per server — wasteful and slow to provision. Virtual machines packed several isolated OSes onto one host, but each VM carries a full guest OS: heavy, slow to boot, big to ship. Containers (Docker, 2013) share the host kernel and package only the app plus its dependencies — a container image is small, starts in milliseconds, and runs identically on a laptop, in CI, and in production. Containers solved packaging. But once you have hundreds of containers spread across dozens of nodes, a new class of problems appears: which node should each container run on? What happens when a container crashes, or a whole node dies? How do you roll out a new version without downtime, roll it back if it breaks, scale replicas with load, let containers find and talk to each other, and attach storage and configuration? This is orchestration, and it is exactly the gap Kubernetes fills.

The alternative to an orchestrator is a pile of brittle bespoke scripts: placement logic, health checks, restart loops, rolling-deploy shell scripts, service-discovery hacks. Kubernetes turns every one of those operational concerns into a declarative API primitive. You describe the destination; the platform figures out the route and keeps you there.

A mental model

Kubernetes is best understood as a control-loop machine. Everything is an object stored in a database (etcd); every object has a spec (what you want) and a status (what actually is). Controllers continuously watch objects and work to make status match spec. kubectl apply does not do anything imperatively — it records your intent, and controllers make it real. Internalize this one idea and most of Kubernetes’ behavior — self-healing, rollouts, autoscaling, GitOps — stops being magic and becomes obvious.

Fundamentals

The declarative model and desired state

Traditional automation is imperative: you issue commands (“start this container”, “add a replica”, “restart that process”). If a command is missed, if a machine reboots, or if a step half-fails, the system drifts and you must reason about how it got here to fix it.

Kubernetes is declarative: you submit the desired end state (“I want 3 replicas of image web:1.4.2 behind a load balancer”), and the system’s job is to make reality match — and to keep it matching. This is the difference between a to-do list and a thermostat. A thermostat doesn’t fire a one-shot “heat up” command; it continuously compares the room temperature (actual) to the target (desired) and acts until they agree, then keeps watching.

This design is level-triggered, not edge-triggered. Controllers don’t react to individual events (“a pod was deleted”); they repeatedly re-derive the correct action from current state (“desired 3, actual 2, therefore create one”). That is why Kubernetes recovers cleanly from missed events, controller restarts, and partial failures: it never depends on having seen a particular event — it always re-reads the world and converges.

Cluster architecture

A Kubernetes cluster is split into two planes: the control plane (the brain — makes global decisions and stores state) and the worker nodes (the muscle — run your containers). In production these run on separate machines; managed offerings (EKS, GKE, AKS) run the control plane for you.

Kubernetes cluster architecture
  1. Control Plane
    kube-apiserver
    etcdstate store
    kube-scheduler
    kube-controller-manager
    (cloud-controller-manager)
  2. (all traffic via the API)
    Nodes
    Nodekubelet, kube-proxy, containerd
    Nodekubelet, kube-proxy, containerd
    Nodekubelet, kube-proxy, containerd

Control plane components:

Node components (on every worker):

The objects/resources model

Everything you manage in Kubernetes is an object — a persisted record of intent. Every object shares the same four top-level fields:

FieldMeaning
apiVersionWhich API group and version defines this kind — e.g. v1 (core), apps/v1 (Deployment), batch/v1 (Job), networking.k8s.io/v1 (Ingress)
kindThe type of object — Pod, Deployment, Service, ConfigMap, …
metadataIdentity and organization — name, namespace, labels, annotations, uid
specThe desired state you declare — the shape depends on the kind
statusThe observed state, filled in and continuously updated by the system — you don’t write it

The API is grouped and versioned. Groups namespace related kinds (apps, batch, networking.k8s.io); versions graduate through alpha → beta → stable (v1). Deprecated API versions are eventually removed, which is a recurring upgrade concern — a manifest using a removed apiVersion will be rejected after an upgrade.

Labels and selectors are the loose-coupling glue of the whole system. A label is an arbitrary key: value on an object’s metadata (app: web, tier: backend). A selector matches objects by their labels. A Service finds its Pods by selector; a Deployment owns the Pods whose labels match its selector. Nothing is wired by hard reference — it’s all matched by labels, which is what makes the system so composable. Annotations look similar (key: value) but are for non-identifying metadata that tools read (build info, config-hash, ingress controller settings); you never select on annotations.

kubectl and kubeconfig

kubectl is the primary CLI — a thin client over the API server. It reads its connection details from a kubeconfig file (default ~/.kube/config), which bundles three lists plus a current selection:

Switching context is how you move between clusters or default namespaces without retyping flags:

kubectl config get-contexts                 # list contexts, * marks current
kubectl config use-context prod-eks         # switch clusters
kubectl config set-context --current --namespace=payments   # change default ns

Tools like kubectx/kubens make this even faster. Treat kubeconfig as a credential — it grants cluster access.

Namespaces

A namespace is a virtual cluster inside a physical cluster — a scope for names and a boundary for policy. Object names must be unique within a namespace, not across the whole cluster, so web in dev and web in prod coexist. Namespaces are where you attach RBAC (who can do what here), ResourceQuota (how much CPU/memory/objects this namespace may use), and LimitRange (default and max requests/limits per Pod). Every cluster ships with default, kube-system (control-plane add-ons), kube-public, and kube-node-lease. Note that not everything is namespaced: Nodes, PersistentVolumes, Namespaces themselves, and ClusterRoles are cluster-scoped.

Key Concepts

How a kubectl apply request flows through the system

Following one command end-to-end ties every component together. Suppose you run kubectl apply -f deployment.yaml for a 3-replica Deployment:

  1. kubectl reads the YAML, resolves the API endpoint and credentials from kubeconfig, and sends an HTTP PATCH/POST to the kube-apiserver.
  2. The API server authenticates the caller, runs authorization (RBAC: may this user create Deployments here?), then admission control — mutating webhooks may inject defaults (e.g. a sidecar), validating webhooks may reject the object (e.g. “no :latest images”). The object is validated against its schema.
  3. The API server persists the Deployment object to etcd. At this point the imperative part is over — nothing has been “run” yet. The object simply exists with a spec and an empty status.
  4. The Deployment controller (in kube-controller-manager) is watching Deployments. It sees a new one with 0 ReplicaSets and creates a ReplicaSet object (another write to the API/etcd).
  5. The ReplicaSet controller sees a ReplicaSet wanting 3 replicas and 0 existing, so it creates 3 Pod objects — still just records in etcd, each with no assigned node (nodeName empty).
  6. The scheduler is watching for unscheduled Pods. For each, it filters and scores nodes and writes the chosen nodeName back onto the Pod (a binding).
  7. On each chosen node, the kubelet sees a Pod now bound to it, calls the container runtime (via CRI) to pull images and start containers, sets up networking (via CNI), mounts volumes, and starts running probes.
  8. As containers come up, the kubelet updates each Pod’s status back through the API server into etcd. The ReplicaSet and Deployment controllers observe the statuses and update their own — status.readyReplicas climbs to 3, and the Deployment reports Available.

Notice: no component gave orders down a chain. Each watched the API, saw a gap between desired and actual, and acted. That is the reconciliation model in motion.

Example: a minimal manifest

The smallest useful object is a single Pod, but in practice you almost always declare a Deployment (which manages Pods for you). Here is a minimal, readable Deployment plus the Service that exposes it:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3                      # desired state: keep 3 Pods running
  selector:
    matchLabels:
      app: web                     # this Deployment owns Pods with app=web
  template:                        # the Pod template stamped out for each replica
    metadata:
      labels:
        app: web                   # MUST match the selector above
    spec:
      containers:
        - name: web
          image: nginx:1.27        # immutable tag — never :latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web                       # route to Pods labelled app=web
  ports:
    - port: 80
      targetPort: 80

The Deployment’s template.metadata.labels must be matched by its spec.selector — that link is what makes the Deployment own those Pods. The Service’s selector independently matches the same label to find endpoints. Change the image to nginx:1.28 and re-apply: the Deployment controller creates a new ReplicaSet and performs a rolling update, all from that one declarative edit.

Core kubectl commands

# Apply / inspect
kubectl apply -f app.yaml                 # create or update from a manifest
kubectl get pods -o wide                  # list Pods, with node and IP
kubectl get deploy,svc                    # multiple kinds at once
kubectl describe pod web-6d4c...          # events + config — why is it Pending/CrashLooping?
kubectl get pod web-6d4c... -o yaml       # full object incl. status

# Debug
kubectl logs -f deploy/web                # stream logs
kubectl logs web-6d4c... --previous       # logs from the last crashed container
kubectl exec -it web-6d4c... -- sh        # shell into a running container
kubectl get events --sort-by=.lastTimestamp

# Manage
kubectl rollout status deploy/web
kubectl rollout undo deploy/web           # roll back to previous ReplicaSet
kubectl scale deploy/web --replicas=5
kubectl delete -f app.yaml

# Context / namespace
kubectl config use-context prod
kubectl get pods -n kube-system
kubectl api-resources                     # every kind, its apiVersion, and short name

kubectl describe and kubectl get events are your first stop for why something isn’t working — the answer is almost always in the events (image pull failures, failed scheduling, probe failures, OOM kills).

Imperative vs declarative usage

kubectl supports imperative commands (kubectl run, kubectl create deployment, kubectl scale, kubectl edit) and declarative ones (kubectl apply -f). Imperative is convenient for exploration and debugging, but declarative apply against files stored in Git is the production-grade approach: the manifests are your source of truth, changes are reviewable, and the cluster can be rebuilt from them. This is the foundation GitOps builds on.

Best Practices

  1. Think declaratively, not imperatively. Describe desired state in manifests and let controllers converge; avoid one-off imperative mutations in production. If you can’t reproduce your cluster from files, you don’t really know its state.
  2. Store all manifests in Git and apply them, don’t edit live. kubectl apply/GitOps over kubectl edit. The repository is the source of truth; the cluster should reflect a reviewed, committed state.
  3. Understand the API object shape (apiVersion/kind/metadata/spec/status) before reaching for tools. Helm and Kustomize just generate these objects — you can’t debug what they produce if the primitives are a mystery.
  4. Pin apiVersion deliberately and track deprecations. APIs get removed across releases; run tools like pluto or heed kubectl deprecation warnings so an upgrade doesn’t reject your manifests.
  5. Never use the latest image tag. Deploy immutable, versioned tags (or digests) so rollouts, rollbacks, and audits are reproducible and meaningful.
  6. Use namespaces to partition teams and environments, and attach RBAC, ResourceQuota, and LimitRange to each so a mistake in one namespace can’t consume the cluster.
  7. Apply least-privilege RBAC from day one. Grant narrow Roles bound to specific ServiceAccounts/users; never hand out cluster-admin casually.
  8. Treat kubeconfig as a secret and use explicit contexts. Name contexts clearly (prod-eks, staging), confirm the current context before destructive commands, and keep prod credentials separate from dev.
  9. Label everything consistently. Adopt the recommended app.kubernetes.io/* labels; selectors, dashboards, and cost allocation all depend on a clean, predictable labeling scheme.
  10. Use annotations for metadata, labels for selection. Don’t overload labels with data you’ll never select on (build SHAs, URLs) — that belongs in annotations.
  11. Protect and back up etcd. It is the entire cluster state; run an odd-sized quorum, encrypt secrets at rest, and take (and test-restore) snapshots — on managed clusters this is the provider’s job, but know where the line is.
  12. Prefer managed control planes (EKS/GKE/AKS) unless you have a reason not to. Running and patching the API server and etcd yourself is real, ongoing work; let the provider own it and focus on workloads.
  13. Learn to read events first. When something is stuck, kubectl describe and kubectl get events almost always contain the reason (ImagePullBackOff, FailedScheduling, OOMKilled) — reach for them before guessing.
  14. Keep the control plane and workloads mentally separate. The scheduler decides where, the kubelet runs, the API server is the only front door — knowing which component owns a behavior makes troubleshooting far faster.

References