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 默认把镜像、容器全放在
Docker 改/var/lib/docker/。如果系统盘空间不够,在/etc/docker/daemon.json里加"data-root"改到其他分区,注意改完后旧数据全部丢失:data-root后等于换了个新家,旧目录/var/lib/docker/成了孤儿数据——Docker 不会再碰它。确认新路径工作正常后可以删掉腾空间:Debian Testing / Trixie 等新版系统:官方源可能还没适配,用一键脚本:# 看看占了多大 du -sh /var/lib/docker # Docker 确认跑在新路径 docker info | grep "Docker Root Dir" # 应该输出 /data/docker # 没问题就删 rm -rf /var/lib/dockercurl -fsSL https://get.docker.com | sh
国内网络加速(一次性配置)¶
Docker Hub 在国内直连大概率超时。如果配了镜像加速仍然 i/o timeout,先关 IPv6——很多内网环境 IPv6 路由不通,而 Docker 和 containerd 默认优先走 IPv6:
然后配两处镜像加速——宿主机 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)¶
kind 和 kubectl 不需要 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 |
资源的「期望状态」——你想要它长什么样 | 体检表上的各项指标目标值 |
specvsstatus:K8s 声明式 API 的核心。spec是你写的(我想要 3 个副本),status是 K8s 写回的(当前实际运行了 3 个)。你对资源的任何修改都写入spec,K8s 的控制循环持续比对spec和status,不一致就自动修复。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 的 apiVersion 是 apps/v1。
版本号会变吗? K8s API 的版本遵循固定的演进路径:
v1alpha1→v1beta1→v1(稳定版)。一个资源一旦达到v1就不再改变字段结构。上表中你看到的v1都是稳定版,放心沿用。会出现 v2 吗? 极少。K8s 的设计哲学是「宁换组、不升版」:需要不兼容变更时创建新 API 组重新从
v1开始。比如 Ingress 从extensions/v1beta1→networking.k8s.io/v1。极少数的真正 v2(如autoscaling/v2)存在但罕见。这套机制的最终效果:只要写v1,就可以假设它不会变。如果查阅旧资料遇到v1beta1或extensions/v1beta1,那是已废弃的老版本,不要再用。⚠️ 上表只是常用的,不代表全部。K8s 还有很多内置 API 组,且 CRD 可以引入全新的 API 组——例如 VerticaDB Operator 注册的
vertica.com/v1beta1。kubectl 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.yamlfluent-bit、busybox等镜像同理。镜像加载到的是节点存储而非 Pod,删 Pod 重建仍会命中缓存,无需重复加载。使用kind delete cluster --name k8s-playground才会清掉缓存。
2.3 多容器 Pod 模式¶
2.3.1 Sidecar 模式¶
在主容器旁运行辅助容器,扩展主容器的功能。
怎么区分主次? K8s 不区分——
containers列表里所有容器地位平等。主次由你的业务逻辑决定:删掉它核心功能没了 → 主容器;删掉它核心功能还在、只是少了增强 → Sidecar。下面的 YAML 中,删掉log-collector,nginx 照样对外服务;删掉nginx,log-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 加密、认证、连接池、服务发现这些脏活。
拿 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 为例,写法通用。
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 不直接操作 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 scale、set image、rollout 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.253. 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)、网页、证书 |
环境变量方式(适合键值对):
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命名空间。如果开发团队和生产团队共用同一个集群,张三建了个nginxPod,李四也建了个nginxPod,名字冲突了怎么办?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是命令行快捷方式,和下面 YAMLkubectl apply -f效果一样(跟 Pod 那节的kubectl runvskubectl 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 devNamespace 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 pods的STATUS列(如CrashLoopBackOff、Terminating、Init: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 的存储模型,从临时卷到持久化存储。