暴露 Kubernetes 应用

KubernetesBeginner
立即练习

简介

你现在已经能够部署和排查应用,但客户端仍然需要一种可靠的方式来访问它们。Pod IP 是临时的:Deployment 随时可能替换 Pod,而新 Pod 通常会获得不同的 IP 地址。

Kubernetes 通过 Service 解决了这个问题。Service 会选择一组不断变化的 Pod,并为客户端提供一个稳定的网络标识。在本实验中,你将沿着完整的连接路径进行探索:从标签到 EndpointSlices,再到集群 DNS,以及两种 Service 类型:用于集群内访问的 ClusterIP 和用于通过节点访问的 NodePort

本实验将重点限定在 Service。Ingress 会增加 HTTP 路由和独立的控制器,等你真正理解 Service 的选择机制和可达性之后,再学习 Ingress 会更容易。

部署 Service 后端

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

在本步骤中,你将创建一个由多个副本组成的应用,后续的 Service 会对外暴露它。在这里,后端指的是能够接收 Service 流量的 Pod。

进入工作目录。cd 用于切换 Shell 的当前目录;执行成功时通常不会输出任何内容:

cd /home/labex/project/service-lab

创建一个包含两个副本的 Deployment。app: course-nginx 这个 Pod 标签非常重要,因为 Service 会使用它作为选择规则。

Shell 语法 cat <<'EOF' > filename 称为 Here 文档。从这里开始,直到结束标记 EOF 之前的所有内容都会写入文件,而 > 会创建或覆盖该文件。第一个 EOF 使用引号包裹,可以避免 Shell 对 YAML 内容进行意外展开。

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

应用配置并等待两个 Pod 就绪。-f 告诉 apply 要读取哪个文件;rollout status 会等待 Deployment 控制器完成部署;--timeout=60s 将等待时间限制为 60 秒。在最后一条命令中,-l 按标签进行筛选,-o wide 会额外显示 Pod IP 和节点列:

kubectl apply -f course-nginx-deployment.yaml
kubectl rollout status deployment/course-nginx --timeout=60s
kubectl get pods -l app=course-nginx -o wide

命名为 http 的端口表示每个容器都提供 TCP 80 端口。两行记录的状态应为 Running,并且显示不同的 Pod IP。这些 IP 当前有效,但不能作为持久的客户端访问地址;接下来的步骤会在这些 Pod 前面添加稳定的 Service 标识。

将标签连接到 Service 选择机制

在本步骤中,你将检查把 Service 与 Pod 连接起来的元数据。Service 不会根据名称选择 Deployment,而是独立查找标签与自身选择器匹配的 Pod。

显示 Pod 标签。-l app=course-nginx 是标签选择器,--show-labels 会在最后一列显示完整的标签集合:

kubectl get pods -l app=course-nginx --show-labels

每个 Pod 都会包含 app=course-nginx 和自动生成的 pod-template-hash。你的 Service 应该只选择稳定的应用标签。

以紧凑格式对比名称、标签和 IP。-o custom-columns 会根据指定的对象字段创建表格。每个 : 前面的标题对应一列,后面的内容是该列使用的字段路径;反斜杠表示命令会在显示时跨行继续:

kubectl get pods -l app=course-nginx \
  -o custom-columns='NAME:.metadata.name,LABEL:.metadata.labels.app,IP:.status.podIP,READY:.status.containerStatuses[0].ready'

自动生成的名称和 IP 用于区分不同的 Pod;共享的标签则描述它们承担的角色。正是这种间接关联,使得某个 Pod 被替换后,Service 仍然可以继续工作。

确认 Deployment 的选择器与 Pod 模板标签一致。-o jsonpath='...' 只提取指定字段。大括号之外的文本会作为标签输出,大括号中的字段路径会返回对应值,而 {"\n"} 会插入换行:

kubectl get deployment course-nginx \
  -o jsonpath='Selector: {.spec.selector.matchLabels.app}{"\n"}Pod label: {.spec.template.metadata.labels.app}{"\n"}'

两个值都应为 course-nginx。如果选择器不匹配,控制器或 Service 就无法连接到预期的 Pod。

创建 ClusterIP Service

在本步骤中,你将创建默认的 Service 类型 ClusterIP。它会提供一个虚拟 IP 和一个 DNS 名称,供集群内的工作负载访问。

使用本实验前面介绍过的 Here 文档方式创建配置文件:

cat <<'EOF' > course-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: course-nginx
spec:
  type: ClusterIP
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
EOF

仔细查看端口映射关系:

  • port: 80 是客户端访问 Service 时使用的端口。
  • targetPort: http 指向每个被选中 Pod 上命名为 http 的容器端口。
  • 选择器负责选择后端 Pod,它本身不是网络地址。

验证、应用并检查 Service。--dry-run=client 只在本地解析配置,不会创建任何对象;移除该参数后才会真正应用配置;随后使用 get service 读取线上对象:

kubectl apply --dry-run=client -f course-nginx-service.yaml
kubectl apply -f course-nginx-service.yaml
kubectl get service course-nginx

CLUSTER-IP 的值由 Kubernetes 分配。在该 Service 的整个生命周期内,即使后端 Pod 发生变化,这个地址也会保持稳定。

跟踪 Service 到 EndpointSlices 的映射

在本步骤中,你将沿着 Service 的选择器,找到实际的后端地址。Kubernetes 会将这些地址记录在 EndpointSlice 对象中。

查看 Service 的详细信息。describe 会展开指定对象的配置、状态以及相关端点信息,相比简短的 get 表格,它更适合进行进一步排查:

kubectl describe service course-nginx

查找 Selector: app=course-nginx,以及包含两个 Pod IP 和端口 80 的 Endpoints 行。

通过 Service 名称标签列出被选中的 EndpointSlice。这里的 -l 选择器使用的是 Kubernetes 自动添加的标签 kubernetes.io/service-name=course-nginx

kubectl get endpointslices -l kubernetes.io/service-name=course-nginx

检查其中的地址和就绪状态。这个 JSONPath 使用 range,为每个端点重复执行大括号内的模板。它会依次输出第一个地址、固定文本 ready=、就绪状态值以及换行:

kubectl get endpointslices -l kubernetes.io/service-name=course-nginx \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

你应该会看到两个地址,并且它们的状态都是 ready=true。此时,整条链路已经清晰可见:

Service selector -> matching Pod labels -> EndpointSlice addresses -> ready Pods

如果 Service 已存在但没有端点,首先检查它的选择器是否与 Pod 标签和就绪状态匹配。

通过集群 DNS 访问 Service

在本步骤中,你将模拟一个集群内客户端。Kubernetes DNS 允许同一命名空间中的 Pod 使用 Service 名称 course-nginx,而不必记住它的虚拟 IP。

启动一个临时的 BusyBox Pod,请求 NGINX 页面,并在命令退出后自动删除客户端 Pod。按从上到下的顺序理解这些选项:

  • --image 选择容器镜像,--image-pull-policy=IfNotPresent 会优先复用已缓存的镜像。
  • --restart=Never 创建一个独立 Pod,而不是由控制器管理的工作负载。
  • --rm 会在命令退出后删除 Pod,-i 会保持命令的输入输出连接。
  • -- 分隔符表示 kubectl 选项到此结束;其后的内容会作为容器内执行的命令。
  • wget -qO- 会静默请求 URL,并将响应正文输出到终端。
kubectl run service-client \
  --image=busybox:1.36 \
  --image-pull-policy=IfNotPresent \
  --restart=Never \
  --rm -i \
  -- wget -qO- http://course-nginx

响应内容中应包含 NGINX 欢迎页面。流量经过了 Service,而不是直接访问某个指定的 Pod IP。

执行一个更简洁的成功检查。>/dev/null 会丢弃 HTML 正文,&& 只有在请求命令成功时才会输出提示信息:

kubectl run service-client-check \
  --image=busybox:1.36 \
  --image-pull-policy=IfNotPresent \
  --restart=Never \
  --rm -i \
  -- wget -qO- http://course-nginx >/dev/null && echo "ClusterIP Service responded"

成功提示证明 DNS 解析和 HTTP 访问都正常。客户端 Pod 是临时的,而 Service 及其两个后端 Pod 仍会保留。

添加 NodePort Service

在本步骤中,你将为同一组 Pod 创建第二个 Service,类型为 NodePort。NodePort 会在每个节点上开放默认范围 30000–32767 内的一个端口,并将流量转发到 Service 的后端。

Kubernetes 可以从一个 多文档 YAML 文件中读取多个对象。每个对象都有自己的 apiVersionkindmetadataspec;包含 --- 的行用于分隔不同的 YAML 文档。

创建一个可重复使用的文件,其中包含现有的 ClusterIP Service 和新的 NodePort Service。重复定义 ClusterIP Service 是安全的:再次应用相同的目标状态不会改变现有对象。

cat <<'EOF' > course-nginx-services.yaml
apiVersion: v1
kind: Service
metadata:
  name: course-nginx
spec:
  type: ClusterIP
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: course-nginx-nodeport
spec:
  type: NodePort
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080
      protocol: TCP
EOF

在修改集群之前,先同时验证这两个 YAML 文档。输出中应包含 service/course-nginxservice/course-nginx-nodeport,并在后面显示 (dry run)

kubectl apply --dry-run=client -f course-nginx-services.yaml

应用并检查这两个 Service。第一条命令会从一个文件中读取两个文档;现有的 ClusterIP Service 应显示为 unchanged,而 NodePort Service 会被创建。第二条命令则读取新创建的线上 Service:

kubectl apply -f course-nginx-services.yaml
kubectl get service course-nginx-nodeport

PORT(S) 列会显示 80:30080/TCP:80 是 Service 端口,30080 是面向节点的端口。两个 Service 选择的是同一组 Pod,因此它们可以拥有相同的后端地址。

对比两种访问边界

在本步骤中,你将测试 NodePort,并总结每种 Service 类型适用的场景。

获取 Minikube 节点 IP。$(...) 是命令替换语法:Shell 会执行 minikube ip,并将其输出保存到变量 NODE_IP 中。-p labex-v135 用于选择预先准备好的配置文件,echo 则用于查看保存的值:

NODE_IP=$(minikube ip -p labex-v135)
echo "$NODE_IP"

通过面向节点的端口请求应用。Service 创建成功后,Kubernetes 可能需要几秒钟来配置新的节点级网络规则。重试选项会让 curl 等待这段短暂的收敛时间,而不是因第一次连接被拒绝就立即失败:

curl -s --retry 5 --retry-connrefused --retry-delay 2 "http://${NODE_IP}:30080" | grep 'Welcome to nginx'

这里,-s 会隐藏进度条,--retry 5 最多允许重试 5 次,--retry-connrefused 会将连接过早被拒绝视为可重试错误,--retry-delay 2 会在每次尝试之间等待 2 秒。双引号允许 ${NODE_IP} 在 URL 中展开。管道符 | 会将返回的 HTML 传给 grep,由它输出匹配到的欢迎文本作为结果证明。

匹配到的 HTML 标题证明请求已经抵达某个后端 Pod。对比你刚刚构建的两条路径:

in-cluster Pod -> course-nginx:80 -> ready backend Pod
VM/node client -> NODE_IP:30080 -> course-nginx-nodeport:80 -> ready backend Pod

同时检查两个 Service:

kubectl get services course-nginx course-nginx-nodeport

在集群内部进行稳定通信时,应使用 ClusterIP;它是默认类型,也是其他暴露机制通常建立的基础。NodePort 增加了一个节点级入口,适合学习、开发,或与外部负载均衡器集成。两者都依赖正确的选择器和处于就绪状态的 EndpointSlices。

在接下来的挑战中,你将独立运用这些知识,为多个 Web 工作负载提供访问入口。

总结

你已经在可替换的 Pod 前面建立了稳定的网络标识,将 Service 选择器与 Pod 标签连接起来,通过 EndpointSlices 跟踪被选中的后端,通过集群 DNS 访问 ClusterIP Service,并通过节点添加了 NodePort 入口。

需要牢记的核心模型是:Service 不是应用本身,也不包含 Pod。它会持续表示一组符合选择条件且处于就绪状态的后端。当连接失败时,应按顺序检查整条链路:Service 端口、选择器、Pod 标签、EndpointSlices、后端就绪状态,以及客户端所在的访问边界。