跳转至

Kubernetes Pod 与 Deployment 实战

作者:JiangChong | 撰写时间:2026年07月

学完本文你将能够: ✅ 使用 kubectl 创建和管理 Pod ✅ 理解 Pod 的多容器模式(Sidecar、Init、Ambassador) ✅ 使用 Deployment 进行声明式应用部署和滚动更新 ✅ 通过 ConfigMap、Secret 和健康检查提升应用可靠性

1. 本文概览

上一篇文章讲了 K8s 集群的「有哪些组件、它们怎么协作」,这一篇开始动手操作。学习路径如下:

定义单个容器 → Pod
管理多个副本 + 滚动更新 → Deployment
注入配置 + 敏感信息 → ConfigMap & Secret
健康检查 + 自动恢复 → Probe
多团队资源隔离 → Namespace & Labels

简单说:从「跑起一个容器」到「生产级应用部署」,每个环节解决一个实际问题。

1.1 准备实验环境

如果你还没有可用的 K8s 集群,最快的方式是用 Kind(Kubernetes in Docker)在本机起一个单节点集群。Kind 把 K8s 的控制平面和工作节点全部跑在 Docker 容器里,不依赖虚拟机——创建完成后 docker ps 就能看到 K8s 节点容器。

没有 Docker 也不想装? 直接用 Killercoda 免费在线环境,浏览器操作,零安装,跳过下面所有步骤。

Step 1:安装 Docker(前置条件)

以下需要 root,仅此一次。装好后全部用普通用户操作。

# Debian / Ubuntu
apt update && apt install -y ca-certificates curl       # 确保有 https 下载能力
install -m 0755 -d /etc/apt/keyrings                    # 创建 GPG 密钥存放目录
curl -fsSL https://download.docker.com/linux/debian/gpg \
  -o /etc/apt/keyrings/docker.asc                       # 下载 Docker 官方 GPG 密钥
echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | tee /etc/apt/sources.list.d/docker.list             # 添加 Docker 官方 apt 源
apt update && apt install -y docker-ce docker-ce-cli containerd.io
systemctl enable --now docker

# CentOS / RHEL 8+
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io
systemctl enable --now docker

# macOS:直接安装 Docker Desktop
# https://docs.docker.com/desktop/setup/install/mac-install/

(可选)修改 Docker 存储目录: Docker 默认把镜像、容器全放在 /var/lib/docker/。如果系统盘空间不够,在 /etc/docker/daemon.json 里加 "data-root" 改到其他分区,注意改完后旧数据全部丢失

{
  "registry-mirrors": ["https://docker.m.daocloud.io"],
  "data-root": "/data/docker"
}
systemctl restart docker  # 重启生效
Docker 改 data-root 后等于换了个新家,旧目录 /var/lib/docker/ 成了孤儿数据——Docker 不会再碰它。确认新路径工作正常后可以删掉腾空间:
# 看看占了多大
du -sh /var/lib/docker
# Docker 确认跑在新路径
docker info | grep "Docker Root Dir"   # 应该输出 /data/docker
# 没问题就删
rm -rf /var/lib/docker
Debian Testing / Trixie 等新版系统:官方源可能还没适配,用一键脚本:curl -fsSL https://get.docker.com | sh

国内网络加速(一次性配置)

Docker Hub 在国内直连大概率超时。如果配了镜像加速仍然 i/o timeout先关 IPv6——很多内网环境 IPv6 路由不通,而 Docker 和 containerd 默认优先走 IPv6:

sysctl -w net.ipv6.conf.all.disable_ipv6=1

然后配两处镜像加速——宿主机 Docker 和 Kind 集群内部都需要:

# 1. 宿主机 Docker 镜像加速(影响 docker pull / kind 下载节点镜像)
mkdir -p /etc/docker
echo '{ "registry-mirrors": ["https://docker.m.daocloud.io"] }' > /etc/docker/daemon.json
systemctl restart docker

# 2. Kind 集群内部镜像加速(影响 Pod 拉 nginx 等应用镜像)
#    创建配置文件,Step 4 创建集群时带上 --config 参数即可
cat > ~/kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
    endpoint = ["https://docker.m.daocloud.io"]
EOF

Step 2:创建专用用户(Linux)

kindkubectl 不需要 root,只需要 Docker 权限。创建一个 k8s 专用用户并加入 docker 组:

useradd -m -s /bin/bash k8s    # -s 指定 shell,否则 Debian 默认 /bin/sh
usermod -aG docker k8s
echo 'export PATH=$PATH:/usr/local/bin' >> /home/k8s/.bashrc
echo 'alias k=kubectl' >> /home/k8s/.bashrc
su - k8s

macOS 不需要此步骤,brew 安装后当前用户直接可用。

Step 3:安装 Kind 和 kubectl

# Kind(二选一)
# x86_64
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-amd64
# ARM64(鲲鹏、飞腾、Apple Silicon 虚拟机)
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-arm64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind

# kubectl(二选一)
# x86_64
curl -Lo ./kubectl "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# ARM64
curl -Lo ./kubectl "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl"
chmod +x ./kubectl && sudo mv ./kubectl /usr/local/bin/kubectl

# macOS
brew install kind kubectl

Step 4:创建集群

# 海外网络
kind create cluster --name k8s-playground

# 国内网络(使用 Step 1 已创建好的镜像加速配置)
kind create cluster --name k8s-playground --config ~/kind-config.yaml

输出过程(仅首次需要下载 ~500MB 节点镜像):

  • Ensuring node image — 下载预装 K8s 的节点镜像
  • Preparing nodes — 从镜像启动容器
  • Starting control-plane — 启动 kubelet、API Server 等核心组件
  • Installing CNI — 安装容器网络插件
  • Set kubectl context — 自动配置 kubectl 连接到新集群

删除集群:kind delete cluster --name k8s-playground

Step 5:验证

kubectl get nodes
# 输出:
# NAME                             STATUS   ROLES           AGE   VERSION
# k8s-playground-control-plane     Ready    control-plane   30s   v1.36.1

没有 Docker 也不想装? 使用 Killercoda 免费在线环境,浏览器直接操作,无需安装任何东西。

完成后 kubectl 自动指向新集群,下面所有命令都可以直接执行。

前置阅读:请先读 Kubernetes 核心架构,了解 K8s 的基本组件和工作原理。

2. Pod — 最小部署单元

Pod 是 Kubernetes 中最小的可部署计算单元,可以包含一个或多个容器。一个节点上可以同时运行多个 Pod(默认上限 110 个),由 Scheduler 根据资源余量分配。同 Pod 内的容器:

  • 共享网络命名空间(同一 IP 和端口空间)
  • 共享存储卷(Volume)
  • 通过 localhost 互相通信
  • 生命周期绑定(同时创建、同时销毁)

验证一下多 Pod 共存:

kubectl run nginx-1 --image=nginx:1.25
kubectl run nginx-2 --image=nginx:1.25
kubectl get pods -o wide

# NAME      READY   STATUS    RESTARTS   AGE   IP           NODE                           NOMINATED NODE   READINESS GATES
# nginx-1   1/1     Running   0          14m   10.244.0.8   k8s-playground-control-plane   <none>           <none>
# nginx-2   1/1     Running   0          14m   10.244.0.9   k8s-playground-control-plane   <none>           <none>

# 两个 Pod 都在同一节点 Running,各分配独立 IP

# 清理
kubectl delete pod nginx-1 nginx-2
# pod "nginx-1" deleted from default namespace
# pod "nginx-2" deleted from default namespace

清理:kubectl delete pod nginx-1 nginx-2

2.1 理解 K8s YAML 结构

K8s 中不管是 Pod、Deployment 还是后面会遇到的 Service、ConfigMap,都用同一种 YAML 格式定义。这一节把结构讲清楚,后面遇到任何新资源都能自己读懂。

YAML 语法三要素:

缩进表示层级      列表用 - 开头      键值对用 key: value
parent:           items:             name: nginx
  child:          - item1            image: nginx:1.25
    grandchild:   - item2            replicas: 3
  • 缩进只能用空格(Tab 会报错),每层 2 个空格。
  • --- 用于在一个文件中分隔多个资源(一个 YAML 文件可以定义 Role + RoleBinding 等多个对象)。

一个 K8s 资源 YAML 的骨架:

apiVersion: v1           # 这个资源属于哪个 API 组+版本
kind: Pod                # 资源类型(Pod、Deployment、Service...)
metadata:                # 元数据:名字、标签、namespace
  name: nginx
  labels:
    app: web
spec:                    # 规格:你想要这个资源「长什么样」
  containers:
  - name: nginx
    image: nginx:1.25

四个必填字段各自的职责:

字段 作用 类比
apiVersion 告诉 K8s「我这份 YAML 用的是哪套 API」 身份证上的「签发机关」
kind 告诉 K8s「我要创建什么类型的资源」 身份证上的「证件类型」
metadata 资源的身份证信息——名字、标签、所属 namespace 姓名、住址
spec 资源的「期望状态」——你想要它长什么样 体检表上的各项指标目标值

spec vs status:K8s 声明式 API 的核心。 spec 是你写的(我想要 3 个副本),status 是 K8s 写回的(当前实际运行了 3 个)。你对资源的任何修改都写入 spec,K8s 的控制循环持续比对 specstatus,不一致就自动修复。kubectl describe 输出里可以看到这两个字段。这也是 kubectl apply -f 远胜 kubectl run 的原因——kubectl run 只能设镜像和端口,YAML 能定义 spec 的全部能力(资源限制、健康检查、存储卷、安全上下文、亲和性规则……)。

apiVersion 详解:

K8s 把不同类型的资源划分到不同的 API 组(apiGroup) 中,每个组独立演进自己的版本号。apiVersion 就是告诉 K8s:「这份 YAML 定义的是哪个组、哪个版本的资源」。常见值:

apiVersion 所属 API 组 适用资源
v1 核心组(core,组名为空) Pod、Service、ConfigMap、Secret、ServiceAccount、Namespace、Node、PVC
apps/v1 apps 组 Deployment、StatefulSet、DaemonSet
batch/v1 batch 组 Job、CronJob
rbac.authorization.k8s.io/v1 RBAC 组 Role、ClusterRole、RoleBinding、ClusterRoleBinding
networking.k8s.io/v1 networking 组 NetworkPolicy、Ingress
storage.k8s.io/v1 storage 组 StorageClass、CSI Driver

格式为 <apiGroup>/<version>——核心组比较特殊,组名是空字符串,所以只写 v1。这个值与后续 Kubernetes RBAC 权限与安全 中 RBAC rules 里的 apiGroups 字段是对应的:如果你要在 Role 里授权别人管理 Deployment,就写 apiGroups: ["apps"],因为 Deployment 的 apiVersionapps/v1

版本号会变吗? K8s API 的版本遵循固定的演进路径:v1alpha1v1beta1v1(稳定版)。一个资源一旦达到 v1 就不再改变字段结构。上表中你看到的 v1 都是稳定版,放心沿用。

会出现 v2 吗? 极少。K8s 的设计哲学是「宁换组、不升版」:需要不兼容变更时创建新 API 组重新从 v1 开始。比如 Ingress 从 extensions/v1beta1networking.k8s.io/v1。极少数的真正 v2(如 autoscaling/v2)存在但罕见。这套机制的最终效果:只要写 v1,就可以假设它不会变。如果查阅旧资料遇到 v1beta1extensions/v1beta1,那是已废弃的老版本,不要再用。

⚠️ 上表只是常用的,不代表全部。K8s 还有很多内置 API 组,且 CRD 可以引入全新的 API 组——例如 VerticaDB Operator 注册的 vertica.com/v1beta1kubectl api-resources 可以看到你集群中当前所有可用的 API 组和资源类型。

2.2 创建一个简单的 Pod(单容器)

前面用 kubectl run 快速起了 Pod,现在用 YAML 方式重做一遍。kubectl 背后每个命令都对应一次对 API Server 的 REST API 调用,先记住三个最常用的:kubectl get(查看资源)、kubectl describe(查看详情)、kubectl logs(查看日志)。

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.25
# 创建 Pod
k8s@debian:~$ kubectl apply -f pod.yaml
# pod/nginx created

# 查看 Pod
k8s@debian:~$ kubectl get pods -o wide
# NAME      READY   STATUS    RESTARTS   AGE   IP           NODE                           NOMINATED NODE   READINESS GATES
# nginx     1/1     Running   0          53m   10.244.0.7   k8s-playground-control-plane   <none>           <none>
# nginx-1   1/1     Running   0          14m   10.244.0.8   k8s-playground-control-plane   <none>           <none>
# nginx-2   1/1     Running   0          14m   10.244.0.9   k8s-playground-control-plane   <none>           <none>

# 查看详细状态
k8s@debian:~$ kubectl describe pod nginx
# Name:             nginx
# Namespace:        default
# Priority:         0
# Service Account:  default
# Node:             k8s-playground-control-plane/172.18.0.2
# Start Time:       Wed, 29 Jul 2026 20:24:56 +0800
# Labels:           <none>
# Annotations:      <none>
# Status:           Running
# IP:               10.244.0.5
# IPs:
#   IP:  10.244.0.5
# Containers:
#   nginx:
#     Container ID:   containerd://a47685dee2002b9c9474b8a5e5b2daaab07417b888ba6f4ebff517991d7a78a3
#     Image:          nginx:1.25
#     Image ID:       docker.io/library/import-2026-07-29@sha256:58b1db9d0fe7d6150ea397a28bc3bc0c152283da61cc0d52524dc747689ae595
#     Port:           <none>
#     Host Port:      <none>
#     State:          Running
#       Started:      Wed, 29 Jul 2026 20:24:57 +0800
#     Ready:          True
#     Restart Count:  0
#     Environment:    <none>
#     Mounts:
#       /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-sksfq (ro)
# Conditions:
#   Type                        Status
#   PodReadyToStartContainers   True
#   Initialized                 True
#   Ready                       True
#   ContainersReady             True
#   PodScheduled                True
# Volumes:
#   kube-api-access-sksfq:
#     Type:                    Projected (a volume that contains injected data from multiple sources)
#     TokenExpirationSeconds:  3607
#     ConfigMapName:           kube-root-ca.crt
#     Optional:                false
#     DownwardAPI:             true
# QoS Class:                   BestEffort
# Node-Selectors:              <none>
# Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
#                              node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
# Events:
#   Type    Reason     Age   From               Message
#   ----    ------     ----  ----               -------
#   Normal  Scheduled  69s   default-scheduler  Successfully assigned default/nginx to k8s-playground-control-plane
#   Normal  Pulled     68s   kubelet            spec.containers{nginx}: Container image "nginx:1.25" already present on machine and can be accessed by the pod
#   Normal  Created    68s   kubelet            spec.containers{nginx}: Container created
#   Normal  Started    68s   kubelet            spec.containers{nginx}: Container started


# 查看日志
k8s@debian:~$ kubectl logs nginx
# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up
# 2026/07/29 12:24:57 [notice] 1#1: using the "epoll" event method
# 2026/07/29 12:24:57 [notice] 1#1: nginx/1.25.5
# 2026/07/29 12:24:57 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
# 2026/07/29 12:24:57 [notice] 1#1: OS: Linux 6.12.86+deb13-amd64
# 2026/07/29 12:24:57 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1073741816:1073741816
# 2026/07/29 12:24:57 [notice] 1#1: start worker processes
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 36
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 37
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 38
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 39
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 40
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 41
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 42
# 2026/07/29 12:24:57 [notice] 1#1: start worker process 43

# 进入 Pod 执行命令
k8s@debian:~$ kubectl exec -it nginx -- /bin/sh
#
#
#

# 删除 Pod
k8s@debian:~$ kubectl delete pod nginx

Pod 一直 ImagePullBackOff 拉不下来镜像? 如果 Step 1 的镜像加速和 IPv6 都搞了还是超时,用能上网的电脑下载镜像传过来:

# macOS(能上网的电脑上)
brew install crane
crane pull docker.m.daocloud.io/library/nginx:1.25 nginx-1.25.tar
scp nginx-1.25.tar k8s@<服务器IP>:~/

# Linux 服务器上加载
docker load -i ~/nginx-1.25.tar
docker tag docker.m.daocloud.io/library/nginx:1.25 nginx:1.25
kind load docker-image nginx:1.25 --name k8s-playground
kubectl delete pod nginx
kubectl apply -f pod.yaml
后续用到的 fluent-bitbusybox 等镜像同理。镜像加载到的是节点存储而非 Pod,删 Pod 重建仍会命中缓存,无需重复加载。使用 kind delete cluster --name k8s-playground 才会清掉缓存。

2.3 多容器 Pod 模式

2.3.1 Sidecar 模式

在主容器旁运行辅助容器,扩展主容器的功能。

怎么区分主次? K8s 不区分——containers 列表里所有容器地位平等。主次由你的业务逻辑决定:删掉它核心功能没了 → 主容器;删掉它核心功能还在、只是少了增强 → Sidecar。下面的 YAML 中,删掉 log-collector,nginx 照样对外服务;删掉 nginxlog-collector 空转。所以 nginx 是主,fluent-bit 是辅。

回忆一下 2.1 节 讲的骨架:同样是 apiVersion + kind + metadata + spec 四个字段。

apiVersion: v1
kind: Pod
metadata:
  name: web-with-sidecar
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80
  - name: log-collector
    image: fluent-bit:2.1
    volumeMounts:
    - name: logs
      mountPath: /var/log/nginx
  volumes:
  - name: logs
    emptyDir: {}

概念速览: volumes 定义了一个共享存储空间——这里是 emptyDir(Pod 创建时自动生成的空目录,Pod 删除时数据也删除)。volumeMounts 把这个空间挂载到容器的指定路径。nginx 往 /var/log/nginx 写日志,log-collector 从同一个路径读日志——两个容器通过共享 Volume 协作,这就是 Sidecar 模式的核心。

2.3.2 Init 容器

在主容器之前运行,执行初始化任务(数据库迁移、权限设置、数据预热)。Init 容器按顺序执行,全部成功后才启动主容器。

spec:
  initContainers:
  - name: init-db
    image: busybox
    command: ['sh', '-c', 'until nc -z db-service 5432; do sleep 1; done;']
  containers:
  - name: main-app
    image: my-app:latest

2.3.3 Ambassador 模式

在 Pod 内部署一个代理容器,替主容器封装对外部服务的连接。主容器只需要连 localhost,Ambassador 负责处理 TLS 加密、认证、连接池、服务发现这些脏活。

主容器 ──→ localhost:5433 ──→ Ambassador ──→ 外部 Vertica 集群
          (不感知外部地址)    (处理连接逻辑)    (实际目标)

拿 Vertica 举例:你写了一个数据分析程序,它只需要连 localhost:5433。旁边跑一个 Ambassador 容器(比如 Envoy 或自定义代理),它把你的请求转发到真正的 Vertica 集群 vertica-cluster.prod.svc:5433,顺带处理 TLS 证书和连接池。哪天 Vertica 集群地址变了,改 Ambassador 配置就行,主程序不用动。

与 Sidecar 的区别: Sidecar 增强(主容器不知道它的存在也能工作);Ambassador 代理(主容器通过它访问外部,没它就断网)。


3. Labels 与 Selectors — K8s 的胶水

为什么需要 Label? 假设你部署了 10 个 Pod:3 个前端、5 个后端、2 个数据库。当你要「把所有前端 Pod 升级到新版本」或「只看数据库 Pod 的日志」时,你怎么区分它们?手动记 IP 地址?Label 解决的就是这个问题——给资源打上 app: frontend 这样的标签,然后通过 Selector 按标签筛选。

在 K8s 中,所有资源之间的关联都是通过 Label + Selector 建立的:Service 靠它找到后端 Pod,Deployment 靠它管理 ReplicaSet。不理解 Label,就无法理解 K8s 组件之间如何协作。

Label 是附加在 K8s 对象上的键值对,用于标识和分组。所有 K8s 资源都能打 Label——Pod、Deployment、Service、Node、Namespace、ConfigMap 都一样。下面以 Pod 为例,写法通用。

metadata:
  labels:
    app: myapp
    tier: frontend
    environment: production
    release: v2.0

Label Selector 类型:

# 等式选择器
kubectl get pods -l environment=production
kubectl get pods -l 'environment!=staging'

# 集合选择器
kubectl get pods -l 'environment in (production, staging)'
kubectl get pods -l 'app notin (legacy)'

# 多条件(AND 逻辑)
kubectl get pods -l 'app=myapp,environment=production'

Selector 的应用场景:

  • Service 通过 selector 选择后端 Pod
  • Deployment 通过 selector 管理 ReplicaSet
  • NetworkPolicy 通过 selector 定义网络规则的访问范围
  • kubectl 通过 -l 参数操作一组资源

4. Deployment — 声明式应用管理

Deployment 是 K8s 中最常用的工作负载资源,它管理 ReplicaSet 并提供了声明式更新能力。

4.1 Deployment 与 ReplicaSet 的关系

Deployment
    └── 管理 ReplicaSet (v1, v2, v3, ...)
            └── 管理 Pod (副本)

Deployment 不直接操作 Pod,而是通过 ReplicaSet 来确保指定数量的 Pod 副本始终运行。

4.2 动手实操

边做边理解 Deployment → ReplicaSet → Pod 三层关系。

# nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
# 1. 创建
kubectl apply -f nginx-deploy.yaml

# 2. 看三层结构(Deployment → ReplicaSet → Pod)
kubectl get deploy,rs,pods -l app=nginx
# NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/nginx-deploy   3/3     3            3           7s
#
# NAME                                      DESIRED   CURRENT   READY   AGE
# replicaset.apps/nginx-deploy-7cd5cff77d   3         3         3       7s
#
# NAME                                READY   STATUS    RESTARTS   AGE
# pod/nginx-deploy-7cd5cff77d-667zh   1/1     Running   0          7s
# pod/nginx-deploy-7cd5cff77d-gdg6t   1/1     Running   0          7s
# pod/nginx-deploy-7cd5cff77d-hntnr   1/1     Running   0          7s

# 3. 删一个 Pod,看自动恢复
kubectl delete pod nginx-deploy-7cd5cff77d-667zh
# pod "nginx-deploy-7cd5cff77d-667zh" deleted from default namespace
kubectl get pods   # Pod 立刻重建——Deployment 发现副本数不够,马上补了一个
# NAME                            READY   STATUS    RESTARTS   AGE
# nginx                           1/1     Running   0          136m
# nginx-deploy-7cd5cff77d-gdg6t   1/1     Running   0          2m9s
# nginx-deploy-7cd5cff77d-hntnr   1/1     Running   0          2m9s
# nginx-deploy-7cd5cff77d-vggmr   1/1     Running   0          14s     <-- 新补的pod

# 4. 扩缩容
kubectl scale deployment nginx-deploy --replicas=5
# deployment.apps/nginx-deploy scaled
kubectl get pods   # 5 个了
# NAME                            READY   STATUS    RESTARTS   AGE
# nginx                           1/1     Running   0          138m
# nginx-deploy-7cd5cff77d-dv9jf   1/1     Running   0          3s
# nginx-deploy-7cd5cff77d-gdg6t   1/1     Running   0          3m52s
# nginx-deploy-7cd5cff77d-hntnr   1/1     Running   0          3m52s
# nginx-deploy-7cd5cff77d-vggmr   1/1     Running   0          117s
# nginx-deploy-7cd5cff77d-zpmzx   1/1     Running   0          3s

kubectl scale deployment nginx-deploy --replicas=2
# deployment.apps/nginx-deploy scaled
kubectl get pods   # 2 个了
# NAME                            READY   STATUS    RESTARTS   AGE
# nginx                           1/1     Running   0          139m
# nginx-deploy-7cd5cff77d-gdg6t   1/1     Running   0          4m45s
# nginx-deploy-7cd5cff77d-hntnr   1/1     Running   0          4m45s

# 5. 滚动更新(需提前把 nginx:1.27 镜像传入 Kind,方法同 1.1 节的 crane 方案)
    # # macOS 上
    # crane pull docker.m.daocloud.io/library/nginx:1.27 nginx-1.27.tar
    # scp nginx-1.27.tar k8s@<服务器IP>:~/

    # # Debian 上
    # docker load -i ~/nginx-1.27.tar
    # docker tag docker.m.daocloud.io/library/nginx:1.27 nginx:1.27
    # kind load docker-image nginx:1.27 --name k8s-playground

kubectl set image deployment/nginx-deploy nginx=nginx:1.27
# deployment.apps/nginx-deploy image updated
kubectl annotate deployment nginx-deploy kubernetes.io/change-cause="更新到 1.27" --overwrite # 可选:记录变更原因

kubectl rollout status deployment/nginx-deploy  # 逐个替换,服务不中断
# deployment "nginx-deploy" successfully rolled out

# 验证更新结果
kubectl describe deployment nginx-deploy | grep Image  # 看 Deployment 当前镜像版本
#     Image:         nginx:1.27
kubectl rollout history deployment/nginx-deploy        # 看变更记录,每次更新修订号 +1
# REVISION  CHANGE-CAUSE
# 3         <none>       ← 上一个版本
# 4         <none>       ← 当前版本(nginx:1.27)
# CHANGE-CAUSE 默认就是 <none>,手动写注解才会有内容

# 6. 回滚
kubectl rollout undo deployment/nginx-deploy
kubectl get pods   # 全部变回 nginx:1.25

清理:kubectl delete deployment nginx-deploy

注意: kubectl scaleset imagerollout undo 都是直接操作集群中的资源,不会更新你本地的 nginx-deploy.yaml 文件。实操几轮后,文件里写的还是 replicas: 3, image: nginx:1.25,但集群里可能已经是 replicas: 2, image: nginx:1.25 了。这就是为什么生产环境推崇「改 YAML → kubectl apply -f」而非命令行直接操作——让文件和实际状态始终保持一致。

4.3 YAML 定义详解

上面实操用的就是下面这个 YAML,现在逐字段拆解:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 3              # 始终保持 3 个 Pod 副本运行,少一个自动补,多一个自动删
  strategy:
    type: RollingUpdate    # 更新策略:逐个替换旧 Pod,另一个选项 Recreate:先删光再创建,服务会中断
    rollingUpdate:
      maxSurge: 1          # 更新期间最多比 replicas 多出几个 Pod,值越大更新越快
      maxUnavailable: 0    # 更新期间最少保持几个 Pod 可用,设为 0 = 绝对不中断
  selector:
    matchLabels:           # Deployment 靠这个 Label 找到它管理的 Pod,必须与 template.labels 完全一致
      app: nginx
  template:                # Pod 模板,定义每个副本「长什么样」,里面的内容就是一个完整的 Pod 定义
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:        # 每个 Pod 最少需要多少 CPU/内存,Scheduler 用这个选节点
            cpu: 500m
            memory: 512Mi
          limits:          # 每个 Pod 最多能用多少,CPU 超限降速,内存超限 OOM Kill
            cpu: 1000m
            memory: 1Gi

字段逐行解读:

字段 含义 一句话
spec.replicas: 3 始终保持 3 个 Pod 副本运行 少一个自动补,多一个自动删
spec.selector.matchLabels Deployment 靠这个 Label 找到它管理的 Pod 必须与 template.labels 完全一致
spec.template Pod 模板,定义每个副本长什么样 里面的内容就是一个完整的 Pod 定义
resources.requests 每个 Pod 最少需要多少 CPU/内存 Scheduler 用它选节点:不够就不调度
resources.limits 每个 Pod 最多能用多少 CPU/内存 CPU 超限降速,内存超限 OOM Kill
strategy.type: RollingUpdate 更新策略:逐个替换旧 Pod 另一个选项 Recreate:先删光再创建
maxSurge: 1 更新期间最多比 replicas 多出几个 Pod 值越大更新越快,临时资源消耗越多
maxUnavailable: 0 更新期间最少保持几个 Pod 可用 设为 0 = 绝对不中断,但更新最慢

4.4 滚动更新(Rolling Update)

滚动更新是默认更新策略——不写 strategy 字段也按 RollingUpdate 处理,只是 maxSurge/maxUnavailable 用默认值 25%。Deployment 还支持另一个策略 Recreate:删除所有旧 Pod 后再创建新 Pod,期间服务短暂中断。适用于不能同时运行新旧版本的场景(如数据库大版本升级、使用 ReadWriteOnce 卷的单副本应用)。

滚动更新不是热重载——旧 Pod 销毁、新 Pod 创建,Pod 名中间那串 hash 会变(因为模板变了,生成新 ReplicaSet → 新 Pod)。但回滚不一样:旧的 ReplicaSet 一直没删,回滚只是把它重新 scale up,所以不创建新 ReplicaSet,Pod 名 hash 前缀和旧版相同。

# 触发滚动更新(更新镜像)
kubectl set image deployment/nginx-deploy nginx=nginx:1.27

# 可选:记录变更原因
kubectl annotate deployment nginx-deploy kubernetes.io/change-cause="更新到 1.27" --overwrite

# 或修改 YAML 后 apply
kubectl apply -f nginx-deploy.yaml

# 查看更新状态
kubectl rollout status deployment/nginx-deploy

# 查看更新历史
kubectl rollout history deployment/nginx-deploy

核心参数:

  • maxSurge:滚动更新期间最多可以超出期望副本数的比例或绝对数。maxSurge=25% 表示最多比 replicas=4 多 1(25% × 4 = 1)
  • maxUnavailable:最多允许不可用的 Pod 数量。maxUnavailable=0 确保服务始终有全部副本可用,但更新速度较慢
  • minReadySeconds:Pod 就绪后等待多少秒才算可用(默认 0),用于捕获启动后的短期故障

4.5 回滚(Rollback)

# 回滚到上一版本
kubectl rollout undo deployment/nginx-deploy

# 回滚到指定版本
kubectl rollout undo deployment/nginx-deploy --to-revision=2

# 查看历史版本详情
kubectl rollout history deployment/nginx-deploy --revision=2

回滚的底层机制: rollout undo 不是把镜像从 1.27「变回」1.25,而是: 1. 旧版本的 ReplicaSet 一直没删(只是之前被 scale down 到 0) 2. 旧 ReplicaSet 的 Pod 模板里写的是 image: nginx:1.25 3. 1.25 的镜像还在节点 containerd 缓存里 4. 回滚 = 把旧 ReplicaSet scale up 到 3,当前 ReplicaSet scale down 到 0

验证:kubectl get rs 能看到新旧两个 ReplicaSet,回滚前后各自主次对调。ReplicaSet 默认保留 10 个(revisionHistoryLimit),这就是回滚能秒切的原因。

关于回滚时的警告: rollout undo 会提示 "previously managed with kubectl apply"——意思是回滚不更新 last-applied-configuration 注解,以后再用 kubectl apply 同一个 YAML 可能产生意外。学习阶段忽略即可,生产环境建议用 kubectl apply -f 历史版本的 YAML 来「回滚」。


5. ConfigMap 与 Secret — 配置注入

问题: 前面 nginx Pod 的配置都写死在镜像里。如果你想让 nginx 输出自定义首页,或者给应用传数据库密码,难道每次改配置都重新打镜像?ConfigMap 和 Secret 解决的就是「配置与镜像分离」。

核心思路:把配置存成 K8s 资源 → 注入到 Pod 里 → 改配置只需要更新资源,镜像不用动。

5.1 动手实操:用 ConfigMap 配置 nginx

# 1. 创建一个 ConfigMap,存一条自定义 HTML
kubectl create configmap nginx-html --from-literal=index.html='<h1>Hello from ConfigMap!</h1>'

# 2. 查看内容
kubectl get configmap nginx-html -o yaml
# apiVersion: v1
# data:
#   index.html: <h1>Hello from ConfigMap!</h1>
# kind: ConfigMap
# metadata:
#   creationTimestamp: "2026-07-29T16:08:44Z"
#   name: nginx-html
#   namespace: default
#   resourceVersion: "18066"
#   uid: 36645034-fa40-4d0b-bef7-261e0039843f

# 3. 创建 Pod,把这个 ConfigMap 挂载到 nginx 的网页目录
cat > nginx-cm.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nginx-cm
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html   # nginx 默认网页目录
  volumes:
  - name: html
    configMap:
      name: nginx-html
EOF

kubectl apply -f nginx-cm.yaml
# pod/nginx-cm created

# 4. 验证:访问 Pod,看是不是 ConfigMap 里的内容
kubectl exec nginx-cm -- curl -s localhost
# <h1>Hello from ConfigMap!</h1>

清理:kubectl delete pod nginx-cm && kubectl delete configmap nginx-html

5.2 两种注入方式

方式 YAML 字段 何时用
环境变量 envFrom.configMapRef / env.valueFrom 传数据库地址、日志级别等键值配置
Volume 挂载 volumes.configMap + volumeMounts 传配置文件(nginx.conf)、网页、证书

环境变量方式(适合键值对):

env:
- name: DB_HOST
  valueFrom:
    configMapKeyRef:
      name: app-config
      key: DB_HOST

Volume 挂载方式(适合整文件,上面实操用的就是这个),好处是 ConfigMap 更新后 POD 内的文件会自动同步。

5.3 Secret — 敏感信息的 ConfigMap

Secret 和 ConfigMap 用法几乎一样,区别是专为密码/Token/证书设计,内容 Base64 编码(注意不是加密)。

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:                  # stringData:直接写明文,K8s 自动转 Base64
  username: dbadmin
  password: Vertica@2024
---
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    env:
    - name: DB_USER
      valueFrom:
        secretKeyRef:        # 和 configMapKeyRef 写法一样,只换了资源类型
          name: db-credentials
          key: username

生产建议: K8s Secret 默认只做 Base64 编码(echo "cGFzc3dvcmQ=" | base64 -d 秒解),生产环境应配合外部工具(HashiCorp Vault、External Secrets Operator)实现真加密。


6. Namespace — 逻辑隔离

问题: 到目前为止所有操作都在 default 命名空间。如果开发团队和生产团队共用同一个集群,张三建了个 nginx Pod,李四也建了个 nginx Pod,名字冲突了怎么办?Namespace 把集群划分成多个虚拟隔间,同名的资源放在不同隔间里互不干扰。

6.1 动手实操

# 1. 创建两个 Namespace
kubectl create namespace dev
# namespace/dev created
kubectl create namespace prod
# namespace/prod created

# 2. 各部署一个同名 Deployment
kubectl create deployment nginx --image=nginx:1.25 --replicas=2 -n dev
# deployment.apps/nginx created
kubectl create deployment nginx --image=nginx:1.25 --replicas=1 -n prod
# deployment.apps/nginx created

# 3. 看不同 Namespace 的 Pod——完全隔离,名字可以相同
kubectl get pods -n dev
# NAME                     READY   STATUS    RESTARTS   AGE
# nginx-7cd5cff77d-9klxj   1/1     Running   0          22s
# nginx-7cd5cff77d-s8g2t   1/1     Running   0          22s

kubectl get pods -n prod
# NAME                    READY   STATUS    RESTARTS   AGE
# nginx-7cd5cff77d-69jpz   1/1     Running   0          19s

# 4. 默认只看 default Namespace,不加 -n 啥也看不到
kubectl get pods
# dev 和 prod 里的 Pod 都不会出现
# NAME                            READY   STATUS    RESTARTS   AGE
# nginx                           1/1     Running   0          3h28m
# nginx-cm                        1/1     Running   0          9m12s
# nginx-deploy-7cd5cff77d-4whj7   1/1     Running   0          21m
# nginx-deploy-7cd5cff77d-8rvlr   1/1     Running   0          21m
# nginx-deploy-7cd5cff77d-ph5vr   1/1     Running   0          21m

清理:kubectl delete namespace dev prod

6.2 操作 Namespace 的三种方式

kubectl get pods -n dev                               # 1. 每次加 -n
kubectl get pods --namespace=dev                      # 2. 等价写法
kubectl config set-context --current --namespace=dev  # 3. 切换默认 NS,后续不加 -n 也看 dev

6.3 ResourceQuota — 资源配额

光隔离不够,还得限制。ResourceQuota 给每个 Namespace 设资源上限,防止某个团队把集群吃光。

# 1. 创建 Namespace + 配额(配额设为极小值,方便测试)
kubectl create namespace dev
kubectl create quota dev-quota --namespace=dev --hard=pods=2

# 2. 验证配额生效
kubectl describe quota dev-quota -n dev
# Resource   Used  Hard
# pods       0     2

# 3. 在 dev 创建 3 个 Pod,第三个会被拒绝
kubectl run test-1 --image=nginx:1.25 -n dev   # ✅
kubectl run test-2 --image=nginx:1.25 -n dev   # ✅
kubectl run test-3 --image=nginx:1.25 -n dev   # ❌ Error from server (Forbidden): pods "test-3" is forbidden: exceeded quota: dev-quota, requested: pods=1, used: pods=2, limited: pods=2

配额 VS 没有配额: 没有配额时 Namespace 只是逻辑隔间(看不到对方),但资源随便用;加了 ResourceQuota 后,连能用多少都被限制住了。

上面用 kubectl create quota 是命令行快捷方式,和下面 YAML kubectl apply -f 效果一样(跟 Pod 那节的 kubectl run vs kubectl apply -f pod.yaml 一个道理)。

# 保存为 quota.yaml,kubectl apply -f 即可(和上面命令行的 pods=2 完全等价)
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    pods: "2"               # 测试用,改大就能用于生产
    requests.cpu: "4"
    limits.memory: 16Gi

清理:kubectl delete namespace dev

Namespace vs Label: Namespace 是硬隔离(A 看不到 B 的资源),Label 是软分组(在同一 Namespace 内打标签筛选)。两者配合:用 Namespace 隔团队,用 Label 在团队内部筛选。


7. 健康检查(Probe)

问题: 前面 nginx 的进程在跑,Pod 状态就是 Running。但如果 nginx 内部死锁了、端口不响应了呢?K8s 看进程还在,就觉得一切正常。Probe 让 K8s 通过真正的「体检」来判断容器是否健康,而非只看进程死活。

7.1 动手实操:liveness 探针

nginx 自带一个 / 路径返回 200。先验证健康检查正常,再故意破坏它。

# nginx-liveness.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-probe
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    livenessProbe:
      httpGet:
        path: /            # nginx 默认首页,返回 200
        port: 80
      initialDelaySeconds: 3
      periodSeconds: 5
kubectl apply -f nginx-liveness.yaml
# pod/nginx-probe created

kubectl get pods   # Running,一切正常
# NAME                            READY   STATUS    RESTARTS   AGE
# nginx                           1/1     Running   0          3h44m
# nginx-cm                        1/1     Running   0          25m
# nginx-deploy-7cd5cff77d-4whj7   1/1     Running   0          37m
# nginx-deploy-7cd5cff77d-8rvlr   1/1     Running   0          37m
# nginx-deploy-7cd5cff77d-ph5vr   1/1     Running   0          37m
# nginx-probe                     1/1     Running   0          3s      <-- 新建的pod

# 现在进容器,故意杀掉 nginx 进程
kubectl exec -it nginx-probe -- nginx -s stop
# nginx 停了,但 livenessProbe 还在每 5 秒检查 http://pod-ip:80/
    # 使用 kubectl exec nginx-probe -- curl -s localhost 检查,会报错
# → 返回 200?不可能了 → 连续失败 3 次(默认 failureThreshold)→ kubelet 重启容器
    # 重启过后使用 kubectl exec nginx-probe -- curl -s localhost 验证正常
kubectl get pods
# RESTARTS 列从 0 变成 1——K8s 自动重启了 nginx!

清理:kubectl delete pod nginx-probe

7.2 三种探针

探针 失败后 典型用途
livenessProbe 重启容器 检测死锁、进程假死
readinessProbe 从 Service 移除流量 应用启动中、依赖未就绪
startupProbe 容器启动失败 Java 等慢启动应用,保护 liveness 不误杀

startupProbe 只在启动阶段运行,成功后 livenessProbe 接管。如果没配 startupProbe,慢启动的应用可能在 livenessProbe 超时后被反复误杀。

7.3 四种检测机制

机制 怎么做 成功标准
httpGet 对 Pod 发 HTTP GET 状态码 200-399
tcpSocket TCP 连接目标端口 端口开放
exec 在容器内执行命令 退出码 0
gRPC gRPC 健康检查 响应 SERVING

7.4 关键参数

参数 默认值 作用
initialDelaySeconds 0 容器启动后等多久开始检查
periodSeconds 10 检查间隔
failureThreshold 3 连续失败几次才算失败
timeoutSeconds 1 单次检查超时

7.5 Pod 生命周期阶段(Phase)

Pod 的 status.phase 字段反映 Pod 在生命周期中的位置,在 kubectl describe 输出里就是 Status: 那一行,在 kubectl get pod nginx-probe -o yaml 里是 status.phase。共有五种状态:

Phase 含义
Pending API Server 已接受 Pod,但容器尚未创建(含调度等待、镜像拉取)
Running Pod 已绑定到节点,至少有一个容器在运行中
Succeeded 所有容器成功终止,不会再重启
Failed 所有容器均已终止,至少有一个容器以失败状态退出
Unknown 无法获取 Pod 状态(通常是与节点通信故障)

注意区分: kubectl get podsSTATUS 列(如 CrashLoopBackOffTerminatingInit:0/1)是容器状态的原因描述不是 Pod 的 Phase。Phase 是 K8s API 模型中的严格定义字段,数量固定不变。


8. 实用 kubectl 命令

# 创建资源
kubectl create deployment nginx --image=nginx:1.25 --replicas=3
kubectl expose deployment nginx --port=80 --type=ClusterIP

# 查看资源
kubectl get pods -o wide                # 基本信息和节点 IP
kubectl get pods --show-labels          # 显示所有标签
kubectl get pods -l 'app=nginx'         # 按标签筛选

# 资源操作
kubectl scale deployment nginx --replicas=5
kubectl autoscale deployment nginx --min=3 --max=10 --cpu-percent=80

# 问题排查
kubectl describe pod nginx-xxxxx
kubectl logs --tail=50 -f nginx-xxxxx
kubectl logs --previous nginx-xxxxx     # 查看上一次崩溃的日志
kubectl exec -it nginx-xxxxx -- bash
kubectl port-forward nginx-xxxxx 8080:80 # 端口转发到本地

# 端口转发示例:访问 Vertica on K8s
kubectl port-forward svc/verticadb-sc 5433:5433 -n my-verticadb-operator

更多生产实践请参考 Vertica Eon on K8s 生产部署实战


9. 总结

概念 解决的问题 一句话
Pod 容器如何打包部署? 最小部署单元,共享网络和存储
Label & Selector 如何在海量资源中找到目标? 打标签 + 按标签筛选,K8s 资源的「胶水」
Deployment 如何保证副本数 + 无缝更新? 声明副本数,自动滚动更新和回滚
ConfigMap / Secret 配置和密码放哪里? 配置与镜像解耦,环境变量或文件注入
Namespace 多团队如何共享集群? 逻辑隔离 + ResourceQuota 资源配额
Probe 应用挂了怎么自动恢复? 存活/就绪/启动三种探针自动检测和重启

下一篇文章: Kubernetes 存储体系 — 深入理解 K8s 的存储模型,从临时卷到持久化存储。