裸奔的容器管理总是让人心里没底,进程挂了没人管、流量来了没法扩、发新版还要停机。所以当我把 Nginx 塞进 Kubernetes 集群的那一刻,等于正式告别“手动 docker run + 半夜爬起来重启”的原始生活。这个项目表面上是“部署一个 Nginx 并做增删改查”,实际上是把 Kubernetes 里最核心的工作负载模型——Deployment 的完整生命周期摸了一遍。
这里要澄清一下标题:原文写的是“Kubernets”,实际对应的是 Kubernetes。本文所有操作我都基于 kubectl 命令行完成,适合刚接触集群、想弄明白 Deployment 怎么玩的人。所谓增删改查,对应到 Kubernetes 里就是:创建 Deployment、查询运行状态、更新镜像和副本数、删除资源对象。这套动作练熟之后,换任何一个无状态应用都只是改一下镜像名和端口的问题。
1. 项目概述:Kubernetes 部署 Nginx 到底在解决什么问题?
1.1 标题里的“增删改查”对应哪些操作
很多教程喜欢一张图讲完 Deployment 的 API 对象关系,但我在实操里更愿意把它理解成一个“声明式管理接口”。你说“我要 3 个 Nginx 副本”,Kubernetes 就会持续保证集群里有 3 个符合条件的 Pod 存在。Pod 挂了就重建,节点挂了就调度到别处,这个过程叫自愈。
“增”就是创建 Deployment 对象,一般写 YAML 然后kubectl apply;“删”是kubectl delete deployment;“改”则分成两派,改资源清单文件后重新 apply,或者用kubectl set image、kubectl scale这种命令直接改运行时对象;“查”是日常用得最频繁的,kubectl get、describe、logs全部归在里面。
这个项目把五类操作串起来做一遍,比单看概念有用得多。因为每个操作背后都牵扯到控制器、标签选择器、滚动更新策略这些底层逻辑,不实际跑一遍很难记住。
1.2 为什么选择 Nginx 和 Deployment 搭配
选 Nginx 不是因为它花哨,而是因为它够单纯。官方镜像小、启动快、无状态、不需要持久化存储,天然适合做 Deployment 演示。部署一个 Nginx,你只需要关心镜像、端口、副本数,不需要像数据库那样额外处理存储卷和 StatefulSet。
Deployment 是绝大多数无状态服务的默认选择。POD IP 会变、副本数会变、镜像版本会变,但 Deployment 这个控制层对象稳定存在。它帮你管理 ReplicaSet,再由 ReplicaSet 管理 Pod。平时kubectl get pods看到的 Pod 只是最底层干活的人,你在增删改查时真正打交道的是 Deployment。
注意:Deployment 不负责服务发现,也就是说它只管把 Pod 拉起、更新、扩容,不管流量怎么进来。Nginx 部署完成后,如果需要对外访问,还得配合 Service 或 Ingress。本文聚焦 Deployment 本身,但最后我会提到访问排查时容易踩的坑。
2. 动手前的环境准备
2.1 kubectl 和集群的连接
我假设你已经有一个可用的集群,比如单节点测试集群,或者云厂商托管集群都行。本机只需要装一个 kubectl,它是你与集群交互的唯一入口。
先确认版本和连通性:
kubectl version --client kubectl cluster-info如果cluster-info能正常输出版本信息和集群地址,说明 kubeconfig 配置没问题。这里最常见的坑是:本机同时配了多个集群上下文,结果操作到了错误的环境。我习惯每动一个集群前先执行:
kubectl config get-contexts kubectl config current-context看到当前上下文确认无误,再往下走。
2.2 命名空间规划
资源对象建议放在独立命名空间里,不要直接裸奔到 default。用命名空间隔离的好处是测试完可以整个空间一键删掉,避免留下垃圾。
我创建了一个专门用于演示的命名空间:
kubectl create namespace nginx-lab后续所有命令我都会带上-n nginx-lab。你也可以通过以下方式设置默认命名空间,省得每次都敲:
kubectl config set-context --current --namespace=nginx-lab不过为了下面每条命令的清晰性,我统一写成显式传入-n nginx-lab的形式。
3. 增:创建第一个 Nginx Deployment
3.1 YAML 核心字段逐行拆解
创建 Deployment 最标准的方式是写 YAML。我用的文件是nginx-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: nginx-lab labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25.3 ports: - containerPort: 80逐个说下关键点:
apps/v1是 Deployment 的稳定 API 版本,旧版extensions/v1beta1早就不该用了。metadata.name是 Deployment 的唯一标识,后续增删改查都用它。spec.replicas是期望副本数,这个值决定集群里同时跑几个 Pod。spec.selector.matchLabels是控制器的标签选择器,Deployment 通过它找到自己管理的 Pod。这个字段一经创建就不可变。spec.template.metadata.labels必须包含 selector 里的标签,否则创建会报错。spec.template.spec.containers定义 Pod 里跑的容器,这里用 Nginx 官方镜像nginx:1.25.3。containerPort: 80不是必须的,但它能给集群一个提示,也让后续 Service 选择端口更方便。
这里有个新手的常见误区:selector里的标签和template里的标签必须匹配,但不能只写 selector 不写 template 标签。两个标签不匹配时,kubectl 会直接拒绝提交。
3.2 提交 Deployment 与验证
执行提交命令:
kubectl apply -f nginx-deployment.yaml预期输出:
deployment.apps/nginx-deploy created看到 created 并不代表容器已经跑起来。Pod 的创建和调度是异步的,所以要继续查状态。这时候“查”的基本功就来了:
kubectl get deployments -n nginx-lab kubectl get pods -n nginx-lab -o wide第一次跑完会看到类似下面的输出:
NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 3/3 3 3 20s NAME READY STATUS RESTARTS AGE IP NODE nginx-deploy-7b6f7c8d9f-abc12 1/1 Running 0 20s 10.244.1.5 node-1 nginx-deploy-7b6f7c8d9f-abc34 1/1 Running 0 20s 10.244.1.6 node-1 nginx-deploy-7b6f7c8d9f-abc56 1/1 Running 0 20s 10.244.2.7 node-2Pod 名称中间的7b6f7c8d9f其实是 ReplicaSet 的哈希值。Deployment 创建时会先生成一个 ReplicaSet,ReplicaSet 再根据副本数创建 Pod。Pod 名 = Deployment 名 + ReplicaSet 哈希 + 随机后缀。
4. 查:从 Deployment 到 Pod 的完整观测链路
4.1 常用查询命令组合
查询是整个增删改查里最常用的能力。我在这个项目里反复使用几个命令,它们各有侧重。
第一组是状态概览:
kubectl get deployment nginx-deploy -n nginx-lab kubectl get rs -n nginx-lab kubectl get pods -n nginx-lab第二组是细节查看:
kubectl describe deployment nginx-deploy -n nginx-lab kubectl describe pod <pod名称> -n nginx-lab kubectl get events -n nginx-lab --sort-by=.lastTimestamp第三组是容器内查看:
kubectl logs -n nginx-lab -l app=nginx kubectl exec -it -n nginx-lab deploy/nginx-deploy -- nginx -v注意kubectl logs -l app=nginx的标签选择器用法,它能一次性查看所有带该标签的 Pod 日志。kubectl exec后面接deploy/nginx-deploy会自动选择第一个可用的 Pod,比手动拼 Pod 名方便。
4.2 状态字段和事件怎么看
kubectl get deployment里的 READY 列是可用副本数/期望副本数。 AVAILABLE 表示 Ready 超过一定时间且处于 Running 的 Pod 数量。如果你看到1/3,说明只有 1 个 Pod 真正就绪,另外两个要么在拉镜像,要么崩溃重启。
Pod 的 STATUS 常见值有Pending、ContainerCreating、Running、CrashLoopBackOff等。Pending 是还没被调度成功,ContainerCreating 是镜像在拉取或存储卷在挂载,CrashLoopBackOff 是容器启动后马上崩了,kubelet 在反复重启。
describe命令永远值得先跑一遍。它会输出事件列表,像Scheduled、Pulled、Created、Started,这些事件按时间排序能帮你快速定位卡在哪个环节。我在一次练习中遇到 Pod 一直 ContainerCreating,describe 里看到Failed to pull image的 Evict 记录,马上就知道是镜像仓库访问问题。
5. 改:扩容缩容、镜像更新与回滚
5.1 实时调整副本数与镜像
改是 Deployment 最出彩的地方。它支持两种修改思路,一种直接命令操作,一种改 YAML 重新 apply。
扩容缩容最简单:
kubectl scale deployment nginx-deploy --replicas=5 -n nginx-lab缩容同理,把数字调小即可。执行后kubectl get pods会看到新增或减少的 Pod。这里特别说一句:缩容时控制器会优先删除启动时间较晚、调度分布较集中的 Pod,这个行为你不需要关心,反正最终数量合法即可。
更新镜像版常用命令:
kubectl set image deployment/nginx-deploy nginx=nginx:1.25.4 -n nginx-lab命令格式是kubectl set image deployment/<名称> <容器名>=<新镜像>。容器名是 YAML 里containers.name,不是 Deployment 名。
我想重点强调第二种方式:改 YAML 后重新 apply。把所有变化沉淀到 YAML 文件,这符合 GitOps 的实践方针。我修改nginx-deployment.yaml里的镜像标签以及副本数,再执行:
kubectl apply -f nginx-deployment.yaml输出会变成deployment.apps/nginx-deploy configured。这个操作和kubectl edit效果类似,区别在于:edit 是直接改集群里的实时对象,不会同步到你本地的 YAML。
提示:如果你倾向用
kubectl apply,不要半路又用kubectl create去创建同一 Deployment。create 和 apply 是两套语义,混用容易在 annotations 里留下奇怪记录,后面删除时也会踩“资源不干净”的坑。我统一用 apply。
5.2 滚动更新的原理与参数控制
更新镜像时默认触发滚动更新。也就是说新老版本的 Pod 不是一次性全换,而是分批替换。这样服务不会中断,滚动过程里始终有副本在对外提供服务。
这里有两个关键参数:
spec: strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1maxUnavailable:更新过程中最多允许几个 Pod 不可用,默认是 25%。maxSurge:更新过程中最多允许超出期望副本数几个 Pod,默认是 25%。
假设副本数是 3,maxSurge=1 那么集群里最多有 4 个 Pod 同时存在;maxUnavailable=1 那么滚动过程中最少要有 2 个 Pod 可用。控制器的节奏是:先创建一个新版 Pod,等它 Ready,再删一个旧版 Pod,如此循环,直到全部替换。
在实际操练时,我建议把 Nginx 镜像从 1.25.3 改成 1.25.4,然后看滚动过程:
kubectl set image deployment/nginx-deploy nginx=nginx:1.25.4 -n nginx-lab kubectl rollout status deployment/nginx-deploy -n nginx-labrollout status会实时输出进度,直到最后显示deployment "nginx-deploy" successfully rolled out。
5.3 版本回滚实操
改着改着难免想回退。Deployment 的每次更新都会留下一次 revision 记录。先看历史:
kubectl rollout history deployment/nginx-deploy -n nginx-lab输出示例:
deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 1 <none> 2 <none>CHANGE-CAUSE 是空的,因为我在 apply 时没有加注解。想让记录带说明,可在 YAML 的metadata.annotations里加kubernetes.io/change-cause。回滚到上一版:
kubectl rollout undo deployment/nginx-deploy -n nginx-lab指定回滚到某个历史版本:
kubectl rollout undo deployment/nginx-deploy -n nginx-lab --to-revision=1undo 之后控制器会再一次滚动更新,把 Pod 恢复到目标版本的镜像。这个过程同样可以用rollout status观察。
我踩过一次挺有意思的坑:某个版本改错了,但错版已经跑了一阵子,回滚命令执行后立刻生效,只是终端没有明显的“回滚成功”提示。实际上只要kubectl rollout status最终成功,就说明回滚完成了。回滚后记得再看一眼kubectl get deployment确认镜像确实变了。
6. 删:清理资源时的边界把握
6.1 一条命令删什么
删除 Deployment 同样有几种姿势。最直接的是:
kubectl delete deployment nginx-deploy -n nginx-lab执行后,Deployment 被删除,ReplicaSet 也会被级联删除,附带所有由它创建的 Pod 都会终止。但这里有个很多人忽略的问题:如果你之前为这个 Deployment 单独创建了 Service、ConfigMap、Ingress 等资源,删除 Deployment 不会动它们。所以清理时得挨个处理:
kubectl delete deployment nginx-deploy -n nginx-lab kubectl delete service nginx-svc -n nginx-lab如果整个命名空间都是测试用,直接删命名空间更省事:
kubectl delete namespace nginx-lab删命名空间会把空间内所有资源一次性清掉,包括 Deployment、ReplicaSet、Pod、Service、ConfigMap。适合练完收尾。
还有一种是只删 Pod,不删 Deployment。这个操作看起来像“删除”,但很快会被控制器拉一个新的 Pod 出来,因为期望副本数没变。想模拟删 Pod 测试自愈能力时,可以这么干:
kubectl delete pod -n nginx-lab -l app=nginx随后你会看到旧 Pod 被终止,新 Pod 被自动创建。
6.2 删不掉的资源怎么办
如果你遇到删除命令一直卡住,比如状态变为 Terminating 却不消失,通常是以下原因之一:
- Pod 里还有进程没正常退出,或者容器有 finalizer 在等待。
- 节点失联,kubelet 无法响应删除请求。
- 负载均衡或存储卷仍在占用。
常见的处理顺序是先看 describe 里的警告:
kubectl describe pod <卡住的Pod> -n nginx-lab确认是 finalizer 问题时,可以通过直接删除对应资源并清除 finalizer 解决,不过这个操作需要比较谨慎。生产环境一定不要直接--force --grace-period=0乱删,会让容器状态变得不可控。
7. 常见问题与排查心得
7.1 镜像拉取失败
Status 显示ImagePullBackOff时,通常有三种原因:
- 镜像名写错,比如把
nginx写成了nginx:laster。 - 镜像仓库需要登录凭证,Pod 没有配置 imagePullSecrets。
- 集群节点访问不到镜像仓库。
排查命令:
kubectl describe pod <pod名称> -n nginx-lab事件里会明确告诉你Failed to pull image ...和对应的错误码。我练这个项目时把镜像误写成nginx:1.25.3-alpine-fat,结果仓库里没有这个 tag,事件马上显示manifest unknown。修正 tag 后重新 apply 就好了。
7.2 Pod 一直 Pending
Pod 停在 Pending 最常见的是资源不够。比如节点 CPU 或内存不足,调度器找不到合适的节点。查看描述后会看到0/3 nodes are available: insufficient cpu之类的提示。
如果是单节点测试集群,解决方式要么调低资源请求,要么加节点。YAML 里可以不加resources字段,这样默认不请求任何资源,调度相对宽松。但生产环境建议总是显式声明 requests 和 limits,否则调度是随机的,运行期也可能出问题。
7.3 服务无法访问
Deployment 部署好后,想访问 Nginx 却打不开,这个坑我在初期踩过很多次。首先明确:只创建 Deployment,Pod 是能被集群内部访问的,但无法从宿主机或浏览器直接访问。
要给外部提供访问,需要一个 Service,类型为 NodePort 或 LoadBalancer。我测试时常用的最小 Service 配置是:
apiVersion: v1 kind: Service metadata: name: nginx-svc namespace: nginx-lab spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80 nodePort: 30080保存为nginx-service.yaml后执行:
kubectl apply -f nginx-service.yaml然后通过节点IP:30080访问。如果发现访问不了,按顺序查三样:
- Service 是否选到了 Pod:
kubectl get endpoints nginx-svc -n nginx-lab,如果 ENDPOINTS 列是空的,说明 selector 没匹配到 Pod。 - 是否在正确的命名空间:Service 和 Deployment 不在同一命名空间时,selector 即使写对也匹配不上资源。
- 节点防火墙或安全组是否放行 30080 端口。
还有一个容易忽略的细节:在云厂商托管集群里,NodePort 可能需要配合安全组开放端口才能外网访问,本地单节点集群则直接能访问。
8. 一点个人体会
这个项目做完之后,我对 Deployment 的认知比之前更踏实了。所谓增删改查不是四个孤立动作,而是一条循环链路:创建后要查询确认,更新后要观察滚动,出错了要回滚,测试完了要清理。每次操作多问一句“控制器此刻在干什么”,能少踩很多坑。
我个人在实际操作中的体会是:凡是能用kubectl apply解决的,就少用直接修改类命令。把 YAML 文件当成唯一事实来源,更新、回滚、重建都从文件出发,集群状态会比较稳定。尤其是多人协作时,一个版本化的 YAML 仓库比一个人脑记忆的随机命令可靠太多。
以前我会习惯性地用docker run起一个 Nginx 就算部署完事,切换到 Kubernetes 之后最大的变化是心态。你不再盯着某一个容器的生死,而是关心声明文件写得好不好。Pod 挂了?没关系,控制器会拉起来。镜像更新了?滚动更新会自己控制节奏。做管理的层级一旦上升,运维姿势就自然不一样了。
最后再分享一个小技巧:在测试环境里把kubectl get pods -w开在旁边,然后执行更新、扩容、删除操作,你能直观看到 Pod 的创建和消亡顺序,这个动态过程比任何原理文章都解释得更清楚。