K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:`kubectl apply` 主链路实验

上一轮已经完成: 这一轮的目标,是把 Kubernetes 最核心的一条链路做成真实实验: 为了做到这一点,我不是只写解释,而是实际做了以下几件事: 1. 在仓库里新增一组教学用 manifests 2. 在集群里创建独立实验命名空间和资源 3. 抓取 Deployment / ReplicaSet / Pod / Endpoints / Events 的真

Markdown02-操作记录-kubectl-apply-主链路实验.md2026年4月9日 17:42

本轮操作记录:kubectl apply 主链路实验

本轮目标

上一轮已经完成:

  • SSH 验证
  • kubectl 验证
  • 集群基线盘点
  • 故障样本采集

这一轮的目标,是把 Kubernetes 最核心的一条链路做成真实实验:

提交一个 Deployment,到底是如何一步步变成真实运行中的 Pod 和可访问的 Service 的?

为了做到这一点,我不是只写解释,而是实际做了以下几件事:

  1. 在仓库里新增一组教学用 manifests
  2. 在集群里创建独立实验命名空间和资源
  3. 抓取 Deployment / ReplicaSet / Pod / Endpoints / Events 的真实状态
  4. 进入 Master 看 Static Pod 清单和 kubelet 配置
  5. 把所有结果写成第二课教学文档

Step 1: 把实验 YAML 落到仓库里

为什么要先写 YAML 文件,而不是直接终端临时 apply

因为教学场景里,长期可复用比“一次跑通”更重要。

如果只是终端里临时 cat <<EOF | kubectl apply -f -

  • 当下可以跑
  • 但后续复习、对照、复现都不方便

所以我先把这次实验对象写到了:

这些文件分别承担什么角色

namespace.yaml

作用:

  • 创建一个独立实验命名空间 learn-k8s

为什么重要:

  • 避免和已有业务或平台组件混在一起
  • 方便后续筛选对象和事件

deployment.yaml

作用:

  • 创建一个 2 副本的 nginx Deployment

为什么选它:

  • nginx 简单稳定,适合观察主链路
  • 加了 readiness/liveness probe,便于讲 Ready vs Running

service.yaml

作用:

  • 创建一个 ClusterIP Service

为什么重要:

  • 可以把对象链完整延伸到 Endpoints
  • 否则只能讲到 Pod,少了服务发现这半边

Step 2: 首次 apply 整个目录

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/02-apply-chain

为什么用目录 apply

因为这和日常 GitOps / 多文件资源管理的习惯更接近。

一个目录里通常会同时放:

  • Namespace
  • Deployment
  • Service
  • ConfigMap
  • RBAC

所以这类操作非常贴近真实工程。

实际结果

第一次执行返回:

namespace/learn-k8s created
service/hello-svc created
Error from server (NotFound): error when creating "...deployment.yaml": namespaces "learn-k8s" not found

这个现象为什么值得记录

因为它恰好体现了声明式系统里很重要的一点:

  • 一次 apply 不一定所有对象都一次成功
  • 但重复 apply 应该是安全的

也就是幂等性。

我对这个现象的工程判断

我没有在这里直接做复杂猜测,而是先做状态确认:

  • Namespace 是否真的已经存在
  • Service 是否已经存在

这是比较稳妥的工程习惯。


Step 3: 验证 namespace 和 service 的实际状态

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get ns learn-k8s
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get svc hello-svc

为什么这两条是下一步

因为当一次 apply 部分成功、部分失败时,下一步不是“拍脑袋重跑”,而是先确认:

  • 哪些对象已经创建
  • 哪些对象没创建

结果

确认:

  • learn-k8s namespace 已存在
  • hello-svc 已存在

我从这里得到的结论

只需要重新 apply 即可,不需要先做删除或清理。


Step 4: 第二次 apply,验证声明式幂等

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/02-apply-chain

实际结果

deployment.apps/hello-deploy created
namespace/learn-k8s unchanged
service/hello-svc unchanged

这说明什么

这说明:

  • Namespace 和 Service 已存在,第二次 apply 不会重复创建,而是判定 unchanged
  • Deployment 这次创建成功

这就是声明式操作最关键的一个性质:

你可以安全地重新提交同样的期望状态,让系统继续收敛。

这也是 GitOps 能长期反复 sync 的基础。


Step 5: 等待 Deployment 真正 rollout 完成

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s rollout status deployment/hello-deploy --timeout=120s

为什么不用 kubectl get pods 代替

因为 get pods 只能看到当前快照。

rollout status 会站在 Deployment 视角告诉你:

  • 期望更新是否完成
  • 更新副本是否都 ready
  • 发布过程是否真正结束

参数解释

rollout status

用来查看控制器发布进度。

--timeout=120s

避免无限等待。

如果 120 秒内没完成,命令会失败,这对自动化和教学都更清晰。

实际结果

先看到:

Waiting for deployment "hello-deploy" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "hello-deploy" rollout to finish: 1 of 2 updated replicas are available...

最后:

deployment "hello-deploy" successfully rolled out

这说明什么

这正好说明:

  • Deployment 创建成功 ≠ Pod 立刻就绪
  • 中间还经历了拉镜像、起容器、Readiness 探针通过等步骤

Step 6: 抓 Deployment 的完整 YAML

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get deployment hello-deploy -o yaml

为什么这里一定要看 YAML

因为 YAML 能一次看出三类关键信息:

  1. 你提交的 spec
  2. API Server / kubectl 自动加的元数据
  3. Controller 回写的 status

从输出里读到了什么

kubectl.kubernetes.io/last-applied-configuration

说明 kubectl apply 会把“上次提交的完整声明”作为注解保存下来。

这对后续 patch 和 diff 很重要。

deployment.kubernetes.io/revision: "1"

说明这是 Deployment 的第一个修订版本。

generation: 1status.observedGeneration: 1

这是个非常关键的信号。

它说明:

  • 用户期望版本号是 1
  • 控制器已经观察并处理到了这个版本

这就是控制器“已经追上用户最新期望”的证据之一。

status.availableReplicas: 2

说明有 2 个副本已经真正可用。


Step 7: 查看 ReplicaSet 和 Pod 的 ownership 链

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get rs -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get rs -o jsonpath='...'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -o jsonpath='...'

为什么同时看 -o widejsonpath

-o wide

适合快速看全貌:

  • 名字
  • READY
  • STATUS
  • IP
  • NODE

jsonpath

适合精准抓想要的字段,例如:

  • ownerReferences
  • .spec.nodeName
  • .status.podIP

我抓到了什么

ReplicaSet:

hello-deploy-79646d9bdc owner=Deployment/hello-deploy replicas=2 ready=2

Pods:

hello-deploy-79646d9bdc-fx6s9 owner=ReplicaSet/hello-deploy-79646d9bdc node=us590068728056 phase=Running podIP=10.244.119.226
hello-deploy-79646d9bdc-x5ldg owner=ReplicaSet/hello-deploy-79646d9bdc node=wk-1 phase=Running podIP=10.244.147.106

这一步的教学价值

它把“Deployment -> ReplicaSet -> Pod”这个概念链条,变成了真实对象链。

以后你讲这个机制,不要只说概念,要像这样拿 ownership 链说话。


Step 8: 查看 Service 和 Endpoints

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get svc,endpoints

为什么这条命令重要

因为它一下把:

  • 稳定入口
  • 动态后端

同时展示出来。

实际结果

service/hello-svc   ClusterIP   10.98.52.186
endpoints/hello-svc 10.244.119.226:80,10.244.147.106:80

这说明了什么

这说明:

  • Service 本身只有一个虚拟 IP
  • 真正的后端目标是两个 Ready Pod
  • Service 和 Pod 中间不是直接连死,而是通过 Endpoints 解耦

这对理解 Service 原理至关重要。


Step 9: 为什么我一定要抓 Events

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get events --sort-by=.metadata.creationTimestamp

参数解释

get events

查看事件流。

--sort-by=.metadata.creationTimestamp

按创建时间排序。

为什么重要:

  • 事件如果不排序,阅读起来容易混乱
  • 排序后,你能接近“流水账”地看到对象生命周期

我从事件里读到了什么

关键顺序大致是:

  1. ScalingReplicaSet on Deployment
  2. SuccessfulCreate on ReplicaSet
  3. Scheduled on Pod
  4. Pulling image
  5. Pulled
  6. Created
  7. Started

为什么这一步这么关键

因为它把:

  • Deployment Controller
  • ReplicaSet Controller
  • Scheduler
  • kubelet
  • containerd

这些分散的概念串成了有时间顺序的一条链。

这也是我把它作为第二课核心证据的原因。


Step 10: 去 Master 上看 Static Pod 清单

实际命令

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@107.148.176.193 'ls -1 /etc/kubernetes/manifests && ...'

还读取了:

  • /etc/kubernetes/manifests/kube-apiserver.yaml
  • /etc/kubernetes/manifests/etcd.yaml

为什么这一步不能省

因为如果只看书,你会知道:

  • kubeadm 控制面是 static pod

但你不会真正“看见”。

而一旦你看到:

etcd.yaml
kube-apiserver.yaml
kube-controller-manager.yaml
kube-scheduler.yaml

你才会真正建立连接:

  • 控制面不是抽象服务
  • 它们也是 Pod
  • kubelet 直接拉起它们

kube-apiserver.yaml 里看到的关键信息

我重点关注了:

  • --advertise-address=10.10.0.1
  • --authorization-mode=Node,RBAC
  • --etcd-servers=https://127.0.0.1:2379
  • --service-cluster-ip-range=10.96.0.0/12
  • hostNetwork: true

这些直接对应后面第二课里的原理解释。

etcd.yaml 里看到的关键信息

我重点关注了:

  • --data-dir=/var/lib/etcd
  • --listen-client-urls=https://127.0.0.1:2379,https://10.10.0.1:2379
  • --initial-cluster=us480851516617a=https://10.10.0.1:2380
  • hostNetwork: true

这帮助我把 etcd 的单节点控制面形态解释清楚。


Step 11: 为什么还要看 kubelet 配置里的 staticPodPath

命令

ssh ... 'grep -n "staticPodPath\|containerRuntimeEndpoint\|clusterDNS" /var/lib/kubelet/config.yaml'

为什么不是只看 manifests 目录就够了

因为你还需要知道:

  • kubelet 是不是确实被配置为监控这个目录

实际结果

看到:

staticPodPath: /etc/kubernetes/manifests

这说明什么

这说明:

  • 不是碰巧这里有文件
  • 而是 kubelet 明确被配置为监控这个路径

也就是说:

Static Pod 是 kubelet 配置层面的正式机制,不是某种临时技巧。


Step 12: 对照集群里实际运行的控制面 Pod

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n kube-system get pods -o wide | rg 'kube-apiserver|kube-scheduler|kube-controller-manager|etcd-us480851516617a'

为什么这里用 rg

因为 kubectl get pods -o wide 输出很多,用 rg 可以快速筛出目标控制面组件。

实际结果

我确认集群里正在运行:

  • etcd-us480851516617a
  • kube-apiserver-us480851516617a
  • kube-controller-manager-us480851516617a
  • kube-scheduler-us480851516617a

这一步的意义

它把“宿主机上的 static pod 文件”和“集群里能看到的真实 Pod”对上了。

也就是说:

  • 文件存在
  • kubelet 配置指向该目录
  • 集群里对应 Pod 真的跑着

证据链闭环了。


Step 13: 把这轮实验提炼成教学文档

基于上述所有数据,我写入了:

这份文档里我把:

  • kubectl 的角色
  • API Server 的职责
  • etcd 的作用
  • Deployment/ReplicaSet/Pod 链
  • Scheduler vs kubelet
  • Running vs Ready
  • Service vs Endpoints
  • Static Pod

全部串成了一条完整主线。

而且不是抽象解释,而是直接引用你这次实验里的真实对象名、节点名和 IP。


本轮命令清单

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/02-apply-chain
KUBECONFIG=~/.kube/config-k8s-lab kubectl get ns learn-k8s
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get svc hello-svc
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/02-apply-chain
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s rollout status deployment/hello-deploy --timeout=120s

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get deployment hello-deploy -o yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get rs -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get svc,endpoints
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get events --sort-by=.metadata.creationTimestamp
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get rs -o jsonpath='...'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -o jsonpath='...'

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@107.148.176.193 'ls -1 /etc/kubernetes/manifests && sed -n ... /etc/kubernetes/manifests/kube-apiserver.yaml && sed -n ... /etc/kubernetes/manifests/etcd.yaml'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@107.148.176.193 'grep -n "staticPodPath\|containerRuntimeEndpoint\|clusterDNS" /var/lib/kubelet/config.yaml'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n kube-system get pods -o wide | rg 'kube-apiserver|kube-scheduler|kube-controller-manager|etcd-us480851516617a'

本轮最重要的结论

结论一:Kubernetes 的核心不是执行命令,而是提交状态

kubectl apply 本质上是:

  • 向 API Server 提交期望状态

不是:

  • 在本机启动容器

结论二:Deployment 到 Pod 之间至少隔了两层控制

不是:

  • Deployment 直接变成容器

而是:

  • Deployment -> ReplicaSet -> Pod -> Node -> Container

结论三:Events 是理解主链路最好的证据

因为它把:

  • 控制器动作
  • 调度动作
  • 节点执行动作

串成了时间线。

结论四:控制面本身也是 Pod

而且在你的 kubeadm 集群里,它们是通过 kubelet 监控 /etc/kubernetes/manifests 目录拉起的 Static Pod。


下一步建议

继续推进时,最自然的下一个主题不是再换新工具,而是顺着本轮主链路往下钻:

  • 为什么 Pod 被调度到这两个节点?
  • 如果资源不够、标签不匹配、taint 不兼容,会发生什么?
  • 调度器是怎么做过滤和打分的?

也就是下一课应该进入:

  • 调度原理
  • Pending 排障
  • requests / limits
  • Affinity / Taint / Toleration

这样整个知识链会非常顺。