第 2 周 · D8 · 标准

Kubernetes
把集装箱交给码头调度

上一讲你自己造了一个集装箱。今天,把它交给集群,让它自己跑起来、自己活下去、自己换版本 —— 并且挂上属于你自己的域名。

声明式与控制器 核心对象字典 GitOps 上机实操
承上

上一讲结尾留了五处痛

单机 Docker 的痛今天的答案
机器跑满,手工换机器Scheduler 自动挑节点调度
进程挂了,半夜爬起来重启Deployment 副本掉了自动补
升级要停服滚动更新 新起旧摘,全程可用
端口冲突、对外暴露难Service + Ingress 稳定入口 + 域名
配置/密码写死在镜像里ConfigMap / Secret 配置与镜像分离

这些答案不是"五个功能",而是同一套思想推出来的结果 —— 先看这个思想。

目标

今天要做出的一件可验证的事

浏览器打开 <你的用户名>.dev.oaiai.ai
看到的是 你自己构建的那个镜像

顺带学会

看集群状态、读清单、滚一次新版本

顺带学会

亲手把一次故障定位出来

顺带理解

为什么公司要求新人"只看不改"

地图

本讲四段

  1. 思想 —— 声明式与控制器循环
  2. 对象 —— 集群的"字典"与 GitOps
  3. 上机 —— 注册 STARGATE,拿到自己的实验田
  1. 实战 —— 部署、滚动更新、修一次故障
  2. 结尾:卡点速查与作业
思想 · 1

你说"要什么",不说"怎么干"

命令式(docker 的做法)
docker run -d -p 8000:8000 hello:v1
docker stop hello
docker rm hello
docker run -d -p 8000:8000 hello:v2

每一步都要你亲自敲。**机器挂了、进程死了,没人管**。

声明式(K8s 的做法)
spec:
  replicas: 3
  image: registry.akria.net/hello:v2

你只写"我要 3 个、用这个镜像"。**剩下的它自己盯着**。

声明式的价值不在"少敲几条命令",而在集群会持续把现实拉回你声明的那份期望

思想 · 2

控制器循环:不断对齐现实与期望

现场演示:杀掉一个副本会怎样
$ kubectl -n st-<你> get podshello-6d9f-aaa   1/1   Runninghello-6d9f-bbb   1/1   Running$ kubectl -n st-<你> delete pod hello-6d9f-aaa$ kubectl -n st-<你> get podshello-6d9f-bbb   1/1   Runninghello-6d9f-ccc   1/1   Running   ← 新的自己补上来了
期望(你写的)

replicas: 2

现实(集群看到的)

running: 1

不一致 → 动手补齐
这就是"自愈",不是魔法

选型

K8s 有很多发行版,公司用 k3s

k8s(上游)
  • 组件多、配置重,适合大规模平台团队
  • 装一套要几个小时,还要单独维护 etcd
k3s(轻量发行版)
  • 单二进制、内置 etcd 替代(SQLite),一条命令装好
  • API 与对象完全一致 —— 你学的 kubectl 一样用
  • 公司规模够用,运维成本低
集群用途你能做什么
prod(生产)线上业务只读:get / describe / logs,仅此而已
learn(教学,你今天用的)你们的练习田自己 ns 里可以随意增删改
词典 · 1

最常打交道的两个对象

P
Pod

最小调度单元,装 1 个(有时多个)容器。
Pod 是"耗材":坏了就删,不要去修它。

D
Deployment

声明"要几个副本、用哪个镜像"。
它负责建 Pod、滚动更新、坏了自己补 —— 你平时只跟它打交道

新人最常见的错误:手动去删 CrashLoopBackOff 的 Pod"想重来"。 删掉它,Deployment 立刻又建一个,问题照旧 —— 要查日志找根因,不是删 Pod(公司安全规则也明令禁止)。

词典 · 2

让外面的人找得到它

S
Service

给一组 Pod 一个稳定入口
Pod 换来换去 IP 一直在变,Service 的名字不变。

I
Ingress

域名 → Service 的路由规则,外加 TLS 证书。
公司用 Traefik 当这层门卫。

一条请求的完整路径

浏览器 域名 Traefik(Ingress) Service Pod 容器里的进程

排障时按这条链逐段查:域名对不对 → Ingress 规则对不对 → Service 有没有后端 → Pod 活着没。

词典 · 3

划分地盘,分离配置

Namespace

逻辑隔离的"地盘"。
你的地盘就是 st-<你的用户名>,配额也按它算。

ConfigMap

普通配置:配置文件、index.html、环境变量。

Secret

敏感配置:密码、密钥、拉私有镜像的凭据

为什么要把配置拿出来?因为这样同一个镜像可以跑在不同环境(测试/生产),不用重新构建 —— 上一讲"配置写死在镜像里"的痛,到这里解掉了。

实践 · 读

看清单只看四个地方

image

跑哪个镜像

replicas

要几个副本

labels

谁认谁(Service 靠它找 Pod)

ports

容器监听哪个端口

Deployment 精简后
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
  namespace: st-<你的用户名>
spec:
  replicas: 1                     # 几个副本
  selector:
    matchLabels: {app: hello}     # 认哪些 Pod
  template:
    metadata:
      labels: {app: hello}        # 给它贴的标签
    spec:
      containers:
        - name: web
          image: registry.akria.net/hello:v1   # 哪个镜像
          ports: [{containerPort: 8000}]        # 监听端口
GitOps · 1

集群的期望状态,住在 Git 里

传统做法
  • 谁登上去 kubectl edit 改了点什么,没人知道
  • 改坏了查不出是谁、什么时候、为什么
  • 机器重建后,集群长什么样全凭记忆
GitOps 做法
  • 清单进 Git,改了什么都留下记录
  • 变更走 PR,可评审、可回滚(git revert 就是回滚集群)
  • 任何时刻都能重建出一模一样的集群

你在公司仓库看到的 manifests/ 目录,就是集群期望状态的"原件" —— 它不是备份,它是事实源

GitOps · 2

公司现在怎么跑这套

  1. 代码改完 → 打 tag 推上去(git tag hello-v1.0.1
  2. CI 自动构建镜像 → 推公司仓库 registry.akria.net
  3. CI 更新 Deployment 的镜像版本 → 集群滚动更新
  4. 清单同步改好、提交进 Git(这一步是"记账",不能少)
由此推出那条红线

既然 Git 是事实源,那手工登集群改东西 = 制造"漂移":集群和 Git 说的不一样了,下次发版就会互相打架、出了事没人说得清。所以公司规定新人只读生产集群 —— 这不是不信任你,是流程本身的要求。

(自动化同步工具如 ArgoCD / Flux 能把这套做得更极致,行业里很常见;公司目前是"清单进 Git + CI 触发更新"这一步,还没上这类工具。)

串起来

滚动更新:客户无感的换版本

  1. 新版本的 Pod 先起来
  2. 健康检查通过(readinessProbe)
  3. 才把旧的摘掉
  4. 直到全部换完,Service 入口一直可用
更新前
v1
v1
更新中(新起 → 旧的还在)
v2
v1
v1
更新完
v2
v2

想亲眼看到过程:另开一个终端 kubectl -n st-<你> get pods -w,然后改镜像版本。

上机 · 0

STARGATE —— 集群接入网关

它替你做了三件事
  • 发你一把 SSH 私钥 —— 不用自己生成、不用找运维
  • 每次登录,自动签发一份 8 小时的集群凭证写进你的 ~/.kube/config
  • 给你一块专属地盘 st-<你的用户名>,并给你管理员权限
入口
网址
  brac.oaiai.ai

SSH
  ssh -i ~/.ssh/<你的私钥> -p 30022 \
      <你的用户名>@brac.oaiai.ai

登录后 = 一个 Linux 终端
(kubectl 已装好)

详细操作步骤在 STARGATE 站内手册(登录后左侧「手册」):3 分钟从邀请码到 kubectl get nodes。课件不重复贴,以手册为准。

任务 · 1

注册 → 下载私钥 → 登录

步骤要点(照站内手册做
① 拿到邀请码课上发放,一次性使用、有有效期,丢了找讲师
② 注册用户名小写字母开头(3–32 位),这个用户名会变成你的命名空间和域名前缀
③ 下载私钥私钥只显示一次;浏览器可能给它加 .txt 后缀,照实际文件名用
④ 放进 ~/.sshchmod 600 收紧权限,权限太松 SSH 会直接拒绝
⑤ 登录ssh -i 后面要写完整路径,只写文件名会 Permission denied

登录成功后第一条命令:kubectl get nodes —— 看到两个 Ready 就成功了。

上机 · 安全

你的权限和你的红线

三层权限
  1. 全集群只读 —— 任何 ns 都能看
  2. st-<你的用户名>管理员 —— 可以随意增删改
  3. 临时提权到 cluster-admin —— 按人申请、有到期时间
红线一

生产集群 只读,写操作一律禁止

红线二

只在 st-<你的用户名> 里写,别碰别人的 ns

红线三

域名只能是 xxx.dev.oaiai.ai(准入策略强制,绕不过去)

配额:约 20 个 Pod、2C/2Gi 请求(上限 4C/4Gi)、5 个存储卷。实验做完记得删,别人还要用。

任务 · 2a

先给集群一份"拉取凭据"

在你自己的命名空间里建凭据
$ kubectl -n st-<你的用户名> create secret docker-registry regcred \    --docker-server=registry.dev.oaiai.ai \    --docker-username=<账号> \    --docker-password=<密码> # 账号密码课上发放(共用的教学仓库账号)
为什么必须做这一步

教学仓库是私有的,不做这一步,Pod 一定起不来,报 ImagePullBackOff —— 这不是你写错了清单。

回顾上一讲

"凭据不只在 push 时需要,pull 也需要"—— 就是这里。

任务 · 2b

写清单:让它跑起来

hello.yaml(namespace 换成你自己的)
apiVersion: apps/v1
kind: Deployment
metadata: {name: hello, namespace: st-<你的用户名>}
spec:
  replicas: 1
  selector: {matchLabels: {app: hello}}
  template:
    metadata: {labels: {app: hello}}
    spec:
      imagePullSecrets: [{name: regcred}]
      containers:
        - name: web
          image: registry.dev.oaiai.ai/<你的用户名>/hello:v1
          ports: [{containerPort: 8000}]
逐行读

namespace 你的地盘
replicas 几个副本
labels 贴标签(Service 靠它找你)
imagePullSecrets 拉取凭据
ports 容器监听端口

保底路径:镜像先换成 nginx:alpine、端口换成 80,把流程跑通再说。

任务 · 2b

再给它一个稳定入口

接在同一个 hello.yaml 后面(--- 分隔)
apiVersion: v1
kind: Service
metadata: {name: hello, namespace: st-<你的用户名>}
spec:
  selector: {app: hello}       # 靠标签找到上面的 Pod
  ports:
    - port: 80                 # Service 对外端口
      targetPort: 8000         # 容器实际监听端口
最容易错的一对

Service 的 selector 必须等于 Pod 的 labels。写错一个字母,Service 就找不到后端 → 页面 503

怎么验证

kubectl -n st-<你> get endpoints hello
有 IP 就是找到了,空的就是没对上。

port 与 targetPort

前者是集群内部访问用,后者是容器真正监听的。

任务 · 2c

挂上你的域名

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: hello, namespace: st-<你的用户名>}
spec:
  ingressClassName: traefik
  rules:
    - host: <你的用户名>.dev.oaiai.ai
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello
                port: {number: 80}
应用与验收
$ kubectl -n st-<你> apply -f hello.yaml$ kubectl -n st-<你> get pods,svc,ingresspod/hello-6d9f-aaa   1/1   Running$ curl http://<你>.dev.oaiai.ai/<!doctype html> …

浏览器打开 http://<你的用户名>.dev.oaiai.ai —— 看到你自己的内容,今天的核心目标达成。

用 http,不用 https:证书要单独申请,Let's Encrypt 有每周限额,课上别批量试。

任务 · 3

滚一次版本,看它怎么不停服

两个终端:一个看,一个改
# 终端 A:盯着看(-w = watch)$ kubectl -n st-<你> get pods -w # 终端 B:改副本数 / 换镜像版本$ kubectl -n st-<你> scale deploy/hello --replicas=3$ kubectl -n st-<你> set image deploy/hello web=registry.dev.oaiai.ai/<你>/hello:v2$ kubectl -n st-<你> rollout status deploy/hello
你会看到

新 Pod ContainerCreating → Running,然后旧 Pod 才 Terminating

顺手试自愈

delete pod 一个副本,看 Deployment 秒级补回来。

回滚也很简单:rollout undo deploy/hello

任务 · 4

自己造坑,自己填

故意把镜像名写错
$ kubectl -n st-<你> set image deploy/hello \    web=registry.dev.oaiai.ai/<你>/hello:v999
$ kubectl -n st-<你> get podshello-7f8c-xyz   0/1   ImagePullBackOff$ kubectl -n st-<你> describe pod hello-7f8c-xyz | tail -20Failed to pull image "…/hello:v999"manifest unknown$ kubectl -n st-<你> rollout undo deploy/hello
排障四件套
  1. get pods —— 看状态
  2. describe pod —— 看 Events(最有用)
  3. logs —— 看应用自己说了什么
  4. get endpoints —— 看 Service 有没有后端

反复强调不要靠删 Pod 解决问题。删了它,Deployment 立刻重建一个,错误照旧 —— 先看 Events 与 logs。

随手查

看到这些状态,先想这些原因

状态大概率原因先试
ImagePullBackOff镜像名/标签写错,或没配拉取凭据 —— 事件里出现 no basic auth credentials 就是后者describe 看 Failed to pull
CrashLoopBackOff容器起来就退出:配置错、依赖连不上、命令不对logs 看应用报错
Pending调度不出去:资源不够、配额用满describe 看 Insufficient
ContainerCreating 卡住正在拉镜像 / 挂载卷等一会儿,一直卡就 describe

记住一条:Events 里的那句话,通常就是答案,看不懂就把原文丢给 Claude Code 问。

随手查

页面打不开,按这条链逐段查

现象断在哪一段
404 page not found(Traefik 页面)Ingress 的 host 拼错,或域名不符合 xxx.dev.oaiai.ai 规则
503 Service Temporarily UnavailableService 的 selector 和 Pod 的 labels 对不上(get endpoints 为空就是它)
502 Bad GatewayPod 没起来,或没监听你声明的那个端口
域名解析不了域名规则写错(只能 xxx.dev.oaiai.ai

三连命令定位:get pods,svc,ingressget endpoints hellodescribe ingress hello

随手查

今天最容易卡住的六个地方

现象先试这个
Permission denied (publickey)-i 要写完整路径;私钥权限 chmod 600;文件名带不带 .txt 要照实际
建 Ingress 被拒(ValidatingAdmissionPolicy)域名必须形如 xxx.dev.oaiai.ai,且每条 rule 都要写 host
忘了自己在哪个 ns所有命令加 -n st-<你的用户名>,别依赖默认
apply 报配额不足实验做完了没删(配额 20 Pod / 5Gi 存储),先清理旧作业
改了镜像没生效set image 后要等 rollout status;确认改的是 Deployment 不是 Pod
凭据拷错 / 密码带空格regcred 删掉重建,密码复制时别带换行
作业

今天要交的四件事

  1. 注册 STARGATE,kubectl get nodes 截图
  2. 把你的镜像部署成功,截图你自己域名的页面
  3. 滚动更新一次(改副本数或换版本),记录 pods -w 观察到的变化
提高题

给 Deployment 加一条 readinessProbe,解释它为什么能让滚动更新更安全

附加(原只读排查)

用只读命令看一遍 prod 集群:get nodes / get pods -A / 挑一个服务 describe —— 守住"只看不改"

提交

docs/training/学员/<姓名>/D8/,按 D6 的 Git 规范提 PR,中文提交信息

小结

你现在能独立上线一个服务

写 Dockerfile → 推镜像 → 写清单 → 部署 → 挂域名 → 滚版本 → 排故障。 这条链路,就是公司每个服务上线的全流程 —— 你只是把规模缩小到了一个人的地盘。

下一讲

D9 Python 基础 —— 开始写服务端代码

后面会回来

D22 运维与 CI/CD 实操:把你的项目真正发一次版

随手可玩

站内手册「小实验」:给 nginx 配域名、多副本、HTTPS

learn.docker.oaiai.ai D8 · Kubernetes 首页 g
← → 翻页 · f 全屏
/ 1
用 ← → 翻页,按 o 看目录

目录