Repository Reading Site
本轮操作记录:`kubectl apply` 主链路实验
上一轮已经完成: 这一轮的目标,是把 Kubernetes 最核心的一条链路做成真实实验: 为了做到这一点,我不是只写解释,而是实际做了以下几件事: 1. 在仓库里新增一组教学用 manifests 2. 在集群里创建独立实验命名空间和资源 3. 抓取 Deployment / ReplicaSet / Pod / Endpoints / Events 的真
本轮操作记录:kubectl apply 主链路实验
本轮目标
上一轮已经完成:
- SSH 验证
kubectl验证- 集群基线盘点
- 故障样本采集
这一轮的目标,是把 Kubernetes 最核心的一条链路做成真实实验:
提交一个 Deployment,到底是如何一步步变成真实运行中的 Pod 和可访问的 Service 的?
为了做到这一点,我不是只写解释,而是实际做了以下几件事:
- 在仓库里新增一组教学用 manifests
- 在集群里创建独立实验命名空间和资源
- 抓取 Deployment / ReplicaSet / Pod / Endpoints / Events 的真实状态
- 进入 Master 看 Static Pod 清单和 kubelet 配置
- 把所有结果写成第二课教学文档
Step 1: 把实验 YAML 落到仓库里
为什么要先写 YAML 文件,而不是直接终端临时 apply
因为教学场景里,长期可复用比“一次跑通”更重要。
如果只是终端里临时 cat <<EOF | kubectl apply -f -:
- 当下可以跑
- 但后续复习、对照、复现都不方便
所以我先把这次实验对象写到了:
这些文件分别承担什么角色
namespace.yaml
作用:
- 创建一个独立实验命名空间
learn-k8s
为什么重要:
- 避免和已有业务或平台组件混在一起
- 方便后续筛选对象和事件
deployment.yaml
作用:
- 创建一个 2 副本的
nginxDeployment
为什么选它:
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-k8snamespace 已存在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 能一次看出三类关键信息:
- 你提交的
spec - API Server / kubectl 自动加的元数据
- Controller 回写的
status
从输出里读到了什么
kubectl.kubernetes.io/last-applied-configuration
说明 kubectl apply 会把“上次提交的完整声明”作为注解保存下来。
这对后续 patch 和 diff 很重要。
deployment.kubernetes.io/revision: "1"
说明这是 Deployment 的第一个修订版本。
generation: 1 与 status.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 wide 和 jsonpath
-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
按创建时间排序。
为什么重要:
- 事件如果不排序,阅读起来容易混乱
- 排序后,你能接近“流水账”地看到对象生命周期
我从事件里读到了什么
关键顺序大致是:
ScalingReplicaSeton DeploymentSuccessfulCreateon ReplicaSetScheduledon PodPullingimagePulledCreatedStarted
为什么这一步这么关键
因为它把:
- 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/12hostNetwork: 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:2380hostNetwork: 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-us480851516617akube-apiserver-us480851516617akube-controller-manager-us480851516617akube-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
这样整个知识链会非常顺。