Skip to content

19 | K8s 应用部署

前置:建立 K3s 练习环境 ​

什么时候需要集群 ​

前面的课程已经完成单机 Compose 发布。只有需要学习调度、声明式应用管理或贴近生产 Kubernetes 环境时,才进入本课;单机 Compose 仍可作为小项目的合理选择。

本课使用独立的 Ubuntu 24.04 LTS 实验机,不在已有业务服务器直接安装。K3s server 的官方基础要求为 2 核 CPU、2 GB 内存,业务和高级课程的 Argo CD 还需额外容量。单节点适合练习,但不具备节点故障容错能力;轻量发行版也不消除网络、存储和权限知识的学习成本。

安装与版本记录 ​

准备 sudo 权限、可用 DNS、足够磁盘和镜像仓库出口。按实际访问路径设置防火墙:API 6443 仅对管理来源开放,HTTP 80 仅在入口练习需要时开放,不把集群端口全部开放到公网。

bash
curl -fL https://get.k3s.io -o /tmp/install-k3s.sh
less /tmp/install-k3s.sh
# 检查脚本后安装 stable 通道;团队实验应另行固定审核过的 INSTALL_K3S_VERSION。
sudo env INSTALL_K3S_CHANNEL=stable sh /tmp/install-k3s.sh
sudo k3s --version
sudo systemctl status k3s --no-pager
sudo k3s kubectl get nodes
sudo k3s kubectl get pods -A

记录安装日期与 k3s --version。使用 stable 通道会随时间变化,不能把一次实验结果视为所有版本都兼容。后续命令使用 kubectl,首次设置当前练习用户的客户端:

bash
mkdir -p "$HOME/.kube"
sudo install -m 600 -o "$(id -u)" -g "$(id -g)" /etc/rancher/k3s/k3s.yaml "$HOME/.kube/config"
export KUBECONFIG="$HOME/.kube/config"
kubectl get nodes

上述配置具有集群管理权限,只用于当前实验机,不上传 Git、不交给 CI。配置中地址为本机回环地址;远程访问还涉及证书与网络,不能只复制文件就假定可用。

安装成功的证据 ​

节点应为 Ready;长期运行的系统 Pod 应就绪;安装 Job 的 Completed 属于正常终态,持续 Pending 或 ImagePullBackOff 不属于成功。

bash
kubectl get events -A --sort-by=.metadata.creationTimestamp
kubectl -n kube-system get svc
kubectl -n kube-system get pods -o wide
sudo journalctl -u k3s -n 100 --no-pager
现象下一步
Pending对具体 Pod 执行 kubectl describe,检查资源、端口和调度事件
ImagePullBackOff检查完整镜像名、网络和仓库认证
NotReady查看 kubelet / K3s 日志、磁盘和网络插件状态
permission denied检查 kubeconfig 所有者,不将配置改为全员可读

K3s 的镜像由 d 管理,宿主机 docker login 不会自动给它授权。仓库镜像配置必须采用实际可用的服务地址,不把普通镜像仓库直接当作通用镜像代理。需要写配置时使用 sudo tee /etc/rancher/k3s/registries.yaml,普通 shell 的 sudo cat > 文件 无法给重定向提权。

默认入口与端口冲突 ​

K3s 默认安装 Traefik,其 LoadBalancer 通过 ServiceLB 使用节点的 80/443 端口。因此本系列不再创建第二个占用 80 的 Nginx LoadBalancer,而是在下面使用 ClusterIP Service + Ingress 共享入口。

ServiceLB 并不意味着自动购买云负载均衡器,也不保证分配新的公网 IP。应检查 Service 状态、节点地址与云防火墙后再测试访问。


把镜像放进集群 ​

前置条件:本节前置的 K3s 节点 Ready、Traefik 就绪,且已取得 17 篇 CI 流水线 的 image.txt。本课先用占位镜像生成配置,再用脚本替换为自己的 digest;不要直接 apply 占位值。

在练习仓库创建 k8s/base。后续课程沿用这些文件,并在 21 篇 多环境隔离与制品晋级 通过 Kustomize 组织环境。

Deployment 与 Service ​

保存 k8s/base/deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: delivery-demo
spec:
  replicas: 2
  revisionHistoryLimit: 3
  progressDeadlineSeconds: 120
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: delivery-demo
  template:
    metadata:
      labels:
        app: delivery-demo
    spec:
      containers:
        - name: web
          image: ghcr.io/replace-me/delivery-demo:replace-me
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /healthz
              port: 80
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /healthz
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 250m
              memory: 128Mi

readiness 决定是否接收 Service 流量,liveness 失败可能触发重启。真实应用的 liveness 不宜直接依赖外部数据库,否则数据库波动可能造成重启风暴。滚动更新需要额外一个 Pod 的容量,不能仅配置副本数就承诺零中断。

保存 k8s/base/service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: delivery-demo
spec:
  selector:
    app: delivery-demo
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

Service 用 label 找 Pod。selector 拼错时,Service 仍存在,但没有可用后端。

通过 Ingress 访问 ​

保存 k8s/base/ingress.yaml:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: delivery-demo
spec:
  ingressClassName: traefik
  rules:
    - host: demo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: delivery-demo
                port:
                  number: 80

在练习终端执行。临时渲染目录避免覆盖 Git 中的占位模板:

bash
kubectl create namespace demo
mkdir -p /tmp/delivery-manifests
cp k8s/base/*.yaml /tmp/delivery-manifests/
export IMAGE_REF="$(cat image.txt)"
python3 - <<'CODE'
import os, pathlib, re
ref = os.environ['IMAGE_REF']
assert re.fullmatch(r'ghcr.io/[a-z0-9._/-]+@sha256:[a-f0-9]{64}', ref)
p = pathlib.Path('/tmp/delivery-manifests/deployment.yaml')
p.write_text(p.read_text().replace('ghcr.io/replace-me/delivery-demo:replace-me', ref))
CODE
kubectl -n demo apply -f /tmp/delivery-manifests/
kubectl -n demo rollout status deployment/delivery-demo --timeout=120s
curl --fail -H 'Host: demo.local' http://127.0.0.1/healthz
curl --fail -H 'Host: demo.local' http://127.0.0.1/

curl 在 K3s 节点运行;外部访问改为可达的节点 IP,并放通对应来源的 80 端口。这里是 HTTP 实验,生产入口需配置域名和 TLS。

私有镜像需要在 demo namespace 预先创建 kubernetes.io/dockerconfigjson 类型的 ghcr-read Secret,然后在 Deployment 的 spec.template.spec 下添加:

yaml
imagePullSecrets:
  - name: ghcr-read

可在临时 DOCKER_CONFIG 目录用只读账号登录,再通过 kubectl -n demo create secret generic ghcr-read --from-file=.dockerconfigjson=配置文件路径 --type=kubernetes.io/dockerconfigjson 导入。勿输出或提交认证文件;后续每个 namespace 都需配置自己的凭据。

更新与回退 ​

用 17 篇构建 v2,保存其引用为 image-v2.txt。记录旧版后更新:

bash
kubectl -n demo get deployment delivery-demo -o jsonpath='{.spec.template.spec.containers[0].image}' > old-image.txt
kubectl -n demo set image deployment/delivery-demo web="$(cat image-v2.txt)"
kubectl -n demo rollout status deployment/delivery-demo --timeout=120s
curl --fail -H 'Host: demo.local' http://127.0.0.1/

在实验环境故意指定不存在的标签,观察事件;确认旧副本仍提供服务,再执行:

bash
kubectl -n demo rollout undo deployment/delivery-demo
kubectl -n demo rollout status deployment/delivery-demo --timeout=120s

rollout undo 恢复 Deployment 的历史 Pod 模板,不恢复数据库、外部配置或文件。进入 GitOps 后,应在 Git 回退期望状态,避免控制器把手工回退再次覆盖。

最小验收与排查 ​

  • 两个 Pod Ready,首页显示期望版本,健康端点返回 ok。
  • 查看 kubectl -n demo get endpointslices,确认 Service 有后端地址。
  • 镜像错误时查看 kubectl -n demo describe pod POD名称;运行错误查看 kubectl -n demo logs deployment/delivery-demo。
  • 入口 404 检查 Host 和 Ingress class;503 检查 selector 和 readiness。
  • 失败版本未就绪时不能宣告发布成功,恢复后再次检查实际镜像。

参考:Deployment、探针、K3s 要求。