在 Kubernetes 上部署应用

KubernetesBeginner
立即练习

简介

在课程的第一部分中,你探索了 Kubernetes 集群,但没有对其进行修改。你了解到,kubectl 会向 API 服务器发送请求,而 Kubernetes 控制器会持续比较期望状态实际状态

现在,你将首次向集群提交应用请求。你不再需要逐项告诉 Kubernetes 要执行哪些底层操作,而是将期望的结果描述在 YAML 文件中,这类文件称为清单。Kubernetes 会保存这些对象定义,并负责将集群调整到所描述的状态。

你会先创建一个单独的 Pod,这样可以更直观地了解清单的基本结构。随后,你将定义一个管理两个 Pod 的 Deployment。通过比较独立 Pod 与由 Deployment 管理的 Pod,你会看到为什么应用通常更适合使用更高级别的控制器。

本实验将重点放在工作负载的创建上。等你熟悉 Pod、标签和 Deployment 后,后续实验会再介绍如何通过 Service 暴露应用。

理解 Kubernetes 声明式对象

环境启动: 本实验会为你启动一个完整的 Kubernetes 集群。配置控制平面、节点和网络组件通常需要 2~3 分钟。请耐心等待环境加载完成后再开始操作。

在本步骤中,你将把上个实验中学到的期望状态概念与 Kubernetes 清单联系起来,并为首次定义应用准备工作目录。

从命令转向期望状态

Kubernetes 支持两种主要的管理方式:

  • 使用命令式命令时,你会直接请求某个操作,例如「创建一个名为 first-nginx 的 Pod」。
  • 使用声明式清单时,你会将期望的对象配置保存到文件中,然后请求 Kubernetes 让集群与该配置保持一致。

声明式文件的价值在于:你可以在执行变更前阅读它们,重复应用,查看差异,并将其存储在版本控制系统中。本课程将重点采用声明式方式。

API 返回的每个 Kubernetes 对象都有几个重要的顶层字段:

  • apiVersion 选择 Kubernetes API 的组和版本。
  • kind 标识对象类型,例如 PodDeployment
  • metadata 描述对象的身份信息,包括名称和标签。
  • spec 描述该对象的期望状态。
  • status 报告观察到的实际状态,通常由 Kubernetes 在创建对象后填充,而不是写入清单。

这会形成一个重要的闭环:

manifest spec -> API server stores desired state -> controllers act -> object status reports actual state

准备清单目录

进入本实验准备好的目录:

cd /home/labex/project/k8s-manifests

使用 pwd 确认当前位置。pwd 表示打印工作目录

pwd
/home/labex/project/k8s-manifests

创建一个简短的笔记文件,记录你将使用的四个清单字段。printf 命令会将每个带引号的字符串写入单独一行,> 会把输出重定向到文件中;如果文件已存在,则会覆盖它。

printf '%s\n' apiVersion kind metadata spec > manifest-fields.txt

显示文件内容进行确认:

cat manifest-fields.txt
apiVersion
kind
metadata
spec

这个笔记文件是一个小型学习检查点:接下来创建的两个清单中都会出现这四个字段。

编写并验证 Pod 清单

在本步骤中,你将为一个 Pod 编写 YAML 清单,并在将其提交到集群前验证其结构。

认识 Pod

Pod 是 Kubernetes 中最小的可部署对象。一个 Pod 可以为一个或多个关系紧密的容器提供共享的网络身份和存储上下文。入门示例通常会在每个 Pod 中运行一个容器。

本实验中的 Pod 将运行 NGINX,这是一个轻量级 Web 服务器。镜像固定为 nginx:1.27-alpine。与使用会不断变化的 latest 标签相比,固定版本可以让结果更加稳定、可复现。该镜像已经缓存在集群中,因此实验过程不依赖从互联网下载镜像。

创建 YAML 文件

确认你位于清单目录中:

cd /home/labex/project/k8s-manifests

你将使用文档 here-document 创建文件。Shell 会把 <<'EOF' 与结尾的 EOF 之间的每一行重定向到 first-pod.yaml。给开头的 EOF 加引号,可以防止 Shell 展开 YAML 中的特殊字符。

cat <<'EOF' > first-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: first-nginx
  namespace: default
  labels:
    app: first-nginx
spec:
  containers:
    - name: nginx
      image: nginx:1.27-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - name: http
          containerPort: 80
          protocol: TCP
EOF

YAML 使用缩进表示层级关系。请统一使用空格;制表符可能导致 YAML 无效。像 - name: nginx 中的连字符表示列表中的一个项目。

从上到下阅读该对象:

  • apiVersion: v1 选择 Pod 使用的核心 API。
  • kind: Pod 声明资源类型。
  • metadata.name 为 Pod 指定稳定名称 first-nginx
  • metadata.namespace: default 将其放入课程使用的普通应用命名空间,而不是系统命名空间。
  • metadata.labels 添加 app=first-nginx 标签,之后可以通过该标签选择此 Pod。
  • spec.containers 是 Pod 应运行的容器列表。
  • imagePullPolicy: IfNotPresent 表示镜像存在时优先使用缓存镜像。
  • 名为 http 的端口使用 containerPort: 80protocol: TCP。它用于说明 NGINX 在容器内监听的位置,但不会将 Pod 暴露到集群外部。

创建前进行验证

使用客户端试运行解析文件,但不创建 Pod。-f 表示文件--dry-run=client 表示仅在本地处理请求:

kubectl apply --dry-run=client -f first-pod.yaml
pod/first-nginx created (dry run)

其中的 dry run 非常重要:这说明语法有效,但集群尚未发生任何变化。

kubectl 以 YAML 格式打印规范化后的对象:

输出选项 -o 表示输出格式。指定 yaml 后,kubectl 会将解析后的对象渲染为 YAML,而不是只打印一行结果:

kubectl apply --dry-run=client -f first-pod.yaml -o yaml

你会看到自己定义的字段,以及客户端自动补充的默认字段。这有助于在真正应用之前发现缩进、字段名和类型方面的问题。

创建并检查你的第一个 Pod

在本步骤中,你将应用已经验证的清单,观察 Kubernetes 如何让 Pod 逐步达到期望状态,并检查生成的对象。

应用清单

如有需要,先进入清单目录:

cd /home/labex/project/k8s-manifests

不使用试运行选项,直接应用文件:

kubectl apply -f first-pod.yaml
pod/first-nginx created

kubectl apply 会将对象发送到 API 服务器。API 服务器保存 Pod 的期望配置,调度器和 kubelet 协同工作,在节点上运行该 Pod。

等待就绪

Pod 的创建是异步的:kubectl apply 返回时,容器可能还没有准备好。使用 kubectl wait 等待 Pod 的 Ready 条件。条件变为真时命令会成功结束;如果 60 秒内仍未满足,则命令失败:

kubectl wait --for=condition=Ready pod/first-nginx --timeout=60s
pod/first-nginx condition met

现在列出该 Pod。本实验会再次使用 -o wide,因为每个实验都应能够独立使用:-o 用于选择输出格式,wide 会额外显示 Pod IP 和节点名称等字段:

kubectl get pod first-nginx -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE
first-nginx   1/1     Running   ...        ...   ...          labex-v135

READY=1/1 表示 Pod 中唯一的容器已就绪,STATUS=Running 表示 Pod 当前所处的阶段。宽格式输出还会显示 Pod IP 和分配到的节点。Pod IP 和创建时间都是动态生成的,因此具体值可能不同。

检查标签和所有权

显示 Pod 的标签:

kubectl get pod first-nginx --show-labels

查找 app=first-nginx。标签会与对象一起保存,后续 Deployment 和 Service 选择 Pod 时会用到它们。

询问 Kubernetes 是否有其他对象控制此 Pod。-o jsonpath='...' 可以提取指定字段,而不是打印整个对象。该表达式会遍历 metadata.ownerReferences{"\n"} 会添加一个换行符,使 Shell 提示符出现在下一行:

kubectl get pod first-nginx -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}{"\n"}'
Owner:

所有者为空,是因为你直接创建的是这个独立 Pod。如果它被删除,没有更高级别的控制器知道应该重新创建它。接下来的步骤中,你将把它与受控制器管理的 Pod 进行比较。

步骤检查点

你已经将本地的期望状态文件转换成了一个正在运行的 Kubernetes 对象。API 接受了 Pod,调度器为其分配了节点,kubelet 也让其中的容器进入就绪状态。Pod 虽然存在,但没有控制器管理其生命周期。

定义 Deployment

在本步骤中,你将定义一个 Deployment,请求 Kubernetes 维持两个 NGINX Pod 副本。

为什么使用 Deployment?

独立 Pod 适合用于学习,但应用通常需要控制器。Deployment 提供期望的副本数量和 Pod 模板。它会创建一个 ReplicaSet,再由 ReplicaSet 维持所请求数量的 Pod。

所有权关系如下:

Deployment -> ReplicaSet -> Pods -> containers

如果一个受管理的 Pod 消失,ReplicaSet 会发现实际副本数低于期望副本数,并创建替代 Pod。后续实验将使用 Deployment 执行扩缩容和滚动更新。

创建 Deployment 清单

返回清单目录:

cd /home/labex/project/k8s-manifests

使用 here-document 创建 course-web-deployment.yaml

cat <<'EOF' > course-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: course-web
  labels:
    app: course-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: course-web
  template:
    metadata:
      labels:
        app: course-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
EOF

Deployment 使用 apps/v1,这是 Deployment 的稳定 API。它的 spec 引入了三个重要字段:

  • replicas: 2 表示期望的 Pod 数量为 2。
  • selector.matchLabels 用于标识 Deployment 管理的 Pod。
  • template 是创建每个 Pod 时使用的模板。

选择器和 template.metadata.labels 都使用 app: course-web。两者必须匹配,否则 Deployment 将无法识别由自身模板创建的 Pod。

验证 Deployment

解析清单,但不修改集群:

kubectl apply --dry-run=client -f course-web-deployment.yaml
deployment.apps/course-web created (dry run)

使用 kubectl diff 将清单与集群中的实际状态进行比较。非零的差异退出状态只表示对象将发生变化;|| true 可以避免 Shell 将这种预期差异当作失败:

kubectl diff -f course-web-deployment.yaml || true

由于 course-web 尚不存在,输出会显示完整对象将作为新增内容出现,相关行以 + 开头。与 apply 不同,diff 不会修改集群。

部署并检查受管理的应用

在本步骤中,你将应用 Deployment,等待两个副本就绪,并通过共享标签检查相关资源。

应用 Deployment 并等待完成

先进入包含清单的目录,然后应用保存的期望状态;-f 告诉 kubectl 从指定文件中读取配置:

cd /home/labex/project/k8s-manifests
kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web created

等待 Deployment 的发布完成。发布是指让 Deployment 的 Pod 达到期望模板和副本数量的过程:

kubectl rollout status deployment/course-web --timeout=60s
deployment "course-web" successfully rolled out

列出 Deployment:

kubectl get deployment course-web
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
course-web   2/2     2            2           ...

READY=2/2 表示期望的两个副本都已就绪。UP-TO-DATE=2 表示两个副本都使用当前 Pod 模板,AVAILABLE=2 表示两个副本都可用。

检查相关资源

使用标签选择器 -l app=course-web 列出相关资源:

kubectl get deployment,replicaset,pods -l app=course-web

输出中会包含一个 Deployment、一个 ReplicaSet 和两个 Pod。ReplicaSet 和 Pod 自动生成的后缀会有所不同:

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/course-web   2/2     2            2           ...

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/course-web-...          2         2         2       ...

NAME                              READY   STATUS    RESTARTS   AGE
pod/course-web-...-...            1/1     Running   ...        ...
pod/course-web-...-...            1/1     Running   ...        ...

这个视图证明,Deployment 期望的两个副本已经变成两个就绪的 Pod。下一步骤中,你将继续追踪这些资源之间的所有权关系。

步骤检查点

你已经应用了 Deployment 清单,并等待其达到期望状态。Kubernetes 创建了一个 ReplicaSet 和两个 Pod,而共享的 app=course-web 标签让你能够将它们作为一个应用组进行查看。

追踪控制器所有权

在本步骤中,你将沿着 Kubernetes 的所有者引用,从受管理的 Pod 追踪到 ReplicaSet,再追踪到 Deployment。同时,你还会重新应用清单,观察声明式操作的幂等性。

检查 Pod 的所有者

将一个自动生成的 Pod 名称保存到 Shell 变量中。NAME=$(command) 语法称为命令替换:Shell 会运行命令,并将其输出保存到 NAME 中。这里,-l app=course-web 会选择匹配的 Pod,JSONPath 则提取第一个 Pod 的自动生成名称:

POD_NAME=$(kubectl get pods -l app=course-web -o jsonpath='{.items[0].metadata.name}')

打印该名称,以便你知道选中了哪个 Pod:

echo "$POD_NAME"

现在检查它的直接所有者。使用引号包裹 "$POD_NAME",可以将保存的名称作为一个安全的命令参数传递:

kubectl get pod "$POD_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: ReplicaSet/course-web-...

与独立的 first-nginx Pod 不同,由 Deployment 管理的 Pod 的所有者是 ReplicaSet,而 ReplicaSet 本身又由 Deployment 所有。

显示 ReplicaSet 的所有者。第一个命令再次使用相同的命令替换模式,这次将 ReplicaSet 的名称保存到 RS_NAME 中:

RS_NAME=$(kubectl get replicaset -l app=course-web -o jsonpath='{.items[0].metadata.name}')
kubectl get replicaset "$RS_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: Deployment/course-web

重新应用期望状态

再次应用相同的清单:

kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web unchanged

unchanged 展示了声明式管理的一个重要特性:重复应用相同的期望状态是安全的。只有当期望配置与实际配置不一致时,Kubernetes 才需要执行操作。

步骤检查点

现在,你同时拥有一个独立 Pod 和一个由 Deployment 管理的应用。两者都运行容器,但 Deployment 增加了控制器层级,可以维持两个副本,并为后续的扩缩容和滚动更新奠定基础。

总结

你已经从只读式的集群探索,转向了声明式的应用管理。你学习了 apiVersionkindmetadataspec 的作用;使用客户端试运行验证了清单;创建并检查了一个独立 Pod;还通过 Deployment 部署了两个受管理的副本。

更重要的是,你观察到了从 Deployment 到 ReplicaSet,再到 Pod 的控制器所有权链。这一基于期望状态的基础,将帮助你在后续课程中诊断应用、通过 Service 暴露应用、扩展副本数量并执行滚动更新。