自学内容网 自学内容网

K8s 之 Pod 高级用法(Advanced Usage of Pods in Kubernetes)

K8s 中之Pod 的高级用法

作为 Kubernetes 的核心编排对象之一,Pod 承载了丰富的信息。其中,资源定义(如 CPU 和内存)和调度相关的字段将在我们讨论调度器时详细介绍。本文中,我们将首先探讨一种特殊的 Volume 类型,以帮助你理解 Pod 对象中各种重要字段的意义。

这种特殊的 Volume 类型称为 Projected Volume。它是什么意思呢?

在 Kubernetes 中,有几种特殊的 Volume 并不是用于存储容器数据或促进容器与主机之间的数据交换。相反,这些特殊 Volume 的目的是为容器提供预定义的数据。从容器角度来看,这些 Volume 中的信息本质上是由 Kubernetes “投影”到容器中的。这就是 Projected Volume 的本质。

目前,Kubernetes 支持四种 Projected Volume 类型:

  1. Secret

  2. ConfigMap

  3. Downward API

  4. ServiceAccountToken

在本文中,我将首先讨论Secret。Secret 的目的是存储 Pod 需要访问的加密数据,并保存在 Etcd 中。然后,你可以将此 Volume 挂载到 Pod 的容器中,以访问存储在这些 Secret 中的信息。

Secret 的一个典型用例是存储数据库凭证。例如:

apiVersion: v1
kind: Pod
metadata:
  name: test-projected-volume
spec:
  containers:
  - name: test-secret-volume
    image: busybox
    args:
    - sleep
    - "86400"
    volumeMounts:
    - name: mysql-cred
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: mysql-cred
    projected:
      sources:
      - secret:
          name: user
      - secret:
          name: pass

在这个 Pod 中,我定义了一个简单的容器。它声明挂载的 Volume 是projected 类型,而不是更常见的emptyDir 或hostPath 类型。此 Volume 的数据源是名为user 和pass 的Secret 对象,分别对应数据库用户名和密码。

数据库用户名和密码作为Secret 对象存储在 Kubernetes 中。创建这些Secret 对象的命令如下:

$ cat ./username.txt
admin
$ cat ./password.txt
c1oudc0w!

$ kubectl create secret generic user --from-file=./username.txt
$ kubectl create secret generic pass --from-file=./password.txt

在此设置中,username.txt 和password.txt 文件分别包含用户名和密码。user 和pass 是分配给Secret 对象的名称。要查看这些Secret 对象,可以使用以下kubectl 命令:

$ kubectl get secrets
NAME           TYPE                                DATA      AGE
user          Opaque                                1         51s
pass          Opaque                                1         51s

当然,除了使用kubectl create secret 命令外,我还可以通过编写 YAML 文件直接创建Secret 对象,例如:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  user: YWRtaW4=
  pass: MWYyZDFlMmU2N2Rm

可以看到,通过 YAML 文件创建的Secret 对象只包含一个Secret 对象。但其data 字段以 Key-Value 格式存储了两条Secret 数据。键 "user" 对应第一条数据,键 "pass" 对应第二条。

需要注意的是,Secret 对象要求这些数据必须进行 Base64 编码,以防止明文密码带来的安全风险。这种编码操作也非常简单,例如:

$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n '1f2d1e2e67df' | base64
MWYyZDFlMmU2N2Rm

需要注意的是,通过这种方式创建的Secret 对象内容仅进行了 Base64 编码,并未加密。在实际生产环境中,应启用 Kubernetes 中的 Secret 加密以增强数据安全性。我将在后续更深入讨论 Secret 时提供更多关于启用 Secret 加密的详细信息。

接下来,让我们尝试创建这个 Pod:

$ kubectl create -f test-projected-volume.yaml

Pod 进入 Running 状态后,让我们验证这些 Secret 对象是否在容器中可用。

$ kubectl exec -it test-projected-volume -- /bin/sh
$ ls /projected-volume/
user
pass
$ cat /projected-volume/user
root
$ cat /projected-volume/pass
1f2d1e2e67df

从结果中可以看到,存储在 Etcd 中的用户名和密码信息已作为文件出现在容器的 Volume 目录中。文件名对应于kubectl create secret 命令中指定的键或Secret 对象data 字段中定义的键。

重要的是,当 Secret 挂载到容器中时,如果 Etcd 中的数据更新,Volume 中的文件也会更新。这是由 kubelet 组件管理的,它会定期维护这些 Volume。

但需要注意的是,这些更新可能会有一些延迟。因此,在应用程序代码中建立数据库连接时,最好实现重试和超时逻辑。

与 Secret 类似,ConfigMap 用于存储不需要加密的配置数据。ConfigMap 的使用几乎与 Secret 相同:你可以使用kubectl create configmap 从文件或目录创建 ConfigMap,或者直接编写 ConfigMap 对象的 YAML 文件。

例如,Java 应用程序所需的配置文件(如.properties 文件)可以存储在 ConfigMap 中,如下所示:

# .properties 文件内容
$ cat example/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

# 从 .properties 文件创建 ConfigMap
$ kubectl create configmap ui-config --from-file=example/ui.properties

# 查看 ConfigMap 中保存的信息(data)
$ kubectl get configmaps ui-config -o yaml
apiVersion: v1
data:
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  name: ui-config
  ...

接下来,我们将讨论 Downward API,它允许 Pod 中的容器直接访问有关 Pod API 对象本身的信息。

例如:

apiVersion: v1
kind: Pod
metadata:
  name: test-downwardapi-volume
  labels:
    zone: us-est-coast
    cluster: test-cluster1
    rack: rack-22
spec:
  containers:
    - name: client-container
      image: k8s.gcr.io/busybox
      command: ["sh", "-c"]
      args:
      - while true; do
          if [[ -e /etc/podinfo/labels ]]; then
            echo -en '\n\n'; cat /etc/podinfo/labels; fi;
          sleep 5;
        done;
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
          readOnly: false
  volumes:
    - name: podinfo
      projected:
        sources:
        - downwardAPI:
            items:
              - path: "labels"
                fieldRef:
                  fieldPath: metadata.labels

在这个 Pod 的 YAML 文件中,我定义了一个简单的容器并声明了一个 projected 类型的 Volume。这次,Volume 的数据源是 Downward API。此 Downward API Volume 配置为将 Pod 的metadata.labels 信息暴露给容器。

通过此配置,当前 Pod 的 Labels 字段值将由 Kubernetes 自动挂载到容器中,作为/etc/podinfo/labels 文件。

容器的启动命令会不断打印/etc/podinfo/labels 的内容。因此,创建此 Pod 后,可以使用kubectl logs 命令查看打印的 Labels 字段,如下所示:

$ kubectl create -f dapi-volume.yaml
$ kubectl logs test-downwardapi-volume
cluster="test-cluster1"
rack="rack-22"
zone="us-est-coast"

目前,Downward API 支持丰富的字段,包括:

  1. 使用fieldRef,可以声明访问:

  • spec.nodeName - 节点的主机名

  • status.hostIP - 主机的 IP 地址

  • metadata.name - Pod 的名称

  • metadata.namespace - Pod 的命名空间

  • status.podIP - Pod 的 IP 地址

  • spec.serviceAccountName - Pod 的服务账户名称

  • metadata.uid - Pod 的 UID

  • metadata.labels['<KEY>'] - 指定<KEY> 标签的值

  • metadata.annotations['<KEY>'] - 指定<KEY> 注解的值

  • metadata.labels - Pod 的所有标签

  • metadata.annotations - Pod 的所有注解

2. 使用resourceFieldRef,可以声明访问:

  • 容器的 CPU 限制

  • 容器的 CPU 请求

  • 容器的内存限制

  • 容器的内存请求

随着 Kubernetes 的发展,Downward API 支持的字段列表可能会扩展。因此,此处列出的信息仅供参考,使用 Downward API 时应查阅官方文档以获取最新信息。

但需要注意的是,Downward API 只能访问在容器进程启动前可以确定的信息。如果需要容器运行后才可获得的信息,如容器的 PID,应考虑使用 sidecar 容器。

在实践中,Secret、ConfigMap 和 Downward API 提供的信息也可以通过环境变量访问。但环境变量不支持自动更新。因此,我通常建议使用 Volume 文件来访问这些信息。

了解了 Secret 后,让我们转向与它们密切相关的概念:Service Account。

你可能会想,是否可以在 Pod 中安装 Kubernetes 客户端,以便直接从容器内部访问和操作 Kubernetes API。

是的,这是可能的。但首先需要解决 API Server 的授权问题。

Service Account 对象是 Kubernetes 内置的“服务账户”,用于权限分配。例如,Service Account A 可能仅限于对 Kubernetes API 的 GET 操作,而 Service Account B 可能拥有对所有 Kubernetes API 操作的完全访问权限。

此类 Service Account 的授权信息和凭证存储在与它们关联的特殊 Secret 对象中,称为 ServiceAccountToken。任何在 Kubernetes 集群中运行的应用程序都必须使用存储在此 ServiceAccountToken 中的授权信息(即 Token)合法访问 API Server。

因此,Kubernetes 中的 Projected Volume 本质上由三种类型组成:Secret、ConfigMap 和 Downward API。第四种类型 ServiceAccountToken 本质上是一种特殊的 Secret。

此外,Kubernetes 提供了一个默认的“服务账户”(default Service Account)以方便使用。任何在 Kubernetes 中运行的 Pod 都可以使用此默认 Service Account,而无需显式声明。

这是如何实现的?

通过 Projected Volume 机制实现。如果你检查 Kubernetes 集群中运行的任何 Pod,你会发现每个 Pod 自动声明了一个名为default-token-xxxx 的 Secret 类型 Volume,并将其挂载到每个容器的固定目录中。例如:

$ kubectl describe pod nginx-deployment-5c678cfb6d-lg9lw
Containers:
...
  Mounts:
    /var/run/secrets/kubernetes.io/serviceaccount from default-token-s8rbq (ro)
Volumes:
  default-token-s8rbq:
  Type:       Secret (a volume populated by a Secret)
  SecretName:  default-token-s8rbq
  Optional:    false

上述 Secret 类型 Volume 实际上是默认 Service Account 的 ServiceAccountToken。Kubernetes 自动为每个创建的 Pod 的spec.volumes 部分添加默认 ServiceAccountToken 的定义,并自动为每个容器包含相应的volumeMounts 字段。此过程对用户完全透明。

Pod 创建后,容器内的应用程序可以直接从挂载目录访问默认 ServiceAccountToken 的授权信息和文件。此路径在 Kubernetes 中是固定的:/var/run/secrets/kubernetes.io/serviceaccount。此 Secret 类型 Volume 的内容如下:

$ ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt namespace  token

因此,你的应用程序可以直接加载这些授权文件以访问和操作 Kubernetes API。此外,如果你使用官方 Kubernetes 客户端库(k8s.io/client-go),它可以自动从该目录加载文件,你无需进行任何配置或编码。

在集群内运行 Kubernetes 客户端并使用默认 Service Account 进行自动授权的方法称为“InClusterConfig”,这是处理 Kubernetes API 编程授权的最推荐方式。

当然,考虑到自动挂载默认 ServiceAccountToken 的潜在风险,Kubernetes 允许你配置默认行为以不自动挂载此 Volume 到 Pod 中的容器。

除了默认 Service Account 外,我们通常需要创建自定义 Service Account 以处理不同的权限设置。这样,Pod 中的容器可以使用这些自定义 Service Account 的 ServiceAccountTokens 进行授权。我们将在讨论 Kubernetes 插件开发时实践此操作。

接下来,让我们看看 Pod 的另一个重要配置:容器健康检查和恢复机制。

在 Kubernetes 中,你可以为 Pod 中的容器定义健康检查“探针”。这样,kubelet 将根据探针的返回值确定容器的状态,而不是仅依赖容器是否运行(来自 Docker 的信息)。此机制是确保生产环境中应用程序健康和正常运行的关键方法。

让我们看看 Kubernetes 文档中的一个示例。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: test-liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

在这个 Pod 中,我们定义了一个有趣的容器。它启动后首先在/tmp 目录中创建一个名为healthy 的文件,表示其正常运行。30 秒后,它将删除此文件。

同时,我们定义了一个类型为exec 的 livenessProbe(健康检查)。这意味着容器启动后,将在容器内执行指定的命令,例如:cat /tmp/healthy。如果此文件存在,该命令的返回值为 0,Pod 将认为此容器不仅在运行,而且健康。此健康检查在容器启动后 5 秒开始执行(initialDelaySeconds: 5),并每 5 秒执行一次(periodSeconds: 5)。

现在,让我们将此过程付诸实践。

首先,创建此 Pod:

$ kubectl create -f test-liveness-exec.yaml

然后,检查 Pod 的状态:

$ kubectl get pod
NAME                READY     STATUS    RESTARTS   AGE
test-liveness-exec   1/1       Running   0          10s

可以看到,由于通过了健康检查,Pod 进入了 Running 状态。

30 秒后,让我们再次检查 Pod 的 Events:

$ kubectl describe pod test-liveness-exec

你会注意到 Pod 在 Events 中报告了一个异常:

FirstSeen LastSeen    Count   From            SubobjectPath           Type        Reason      Message
--------- --------    -----   ----            -------------           --------    ------      -------
2s        2s      1   {kubelet worker0}   spec.containers{liveness}   Warning     Unhealthy   Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory

显然,健康检查探针检测到/tmp/healthy 不再存在,因此报告容器不健康。接下来会发生什么?

让我们再次检查此 Pod 的状态:

$ kubectl get pod test-liveness-exec
NAME           READY     STATUS    RESTARTS   AGE
liveness-exec   1/1       Running   1          1m

你可能会注意到,Pod 并未进入 Failed 状态,而是保持在 Running 状态。为什么呢?

实际上,如果你观察到 RESTARTS 字段从 0 变为 1,你就会明白原因:出现异常的容器已被 Kubernetes 重启。在此过程中,Pod 保持 Running 状态。

需要注意的是,Kubernetes 没有与 Docker 相同的 Stop 语义。因此,尽管是重启,实际上意味着容器被重新创建。

此功能称为 Kubernetes 中的 Pod 恢复机制,也称为restartPolicy。它是 Pod 的 spec(pod.spec.restartPolicy)中的一个标准字段,默认值为 Always,表示:任何时候容器出现问题,都会被重新创建。

但需要强调的是,Pod 恢复过程始终发生在当前节点上,不会移动到其他节点。事实上,一旦 Pod 绑定到某个节点,它将一直留在该节点上,除非绑定发生变化(即pod.spec.node 字段被修改)。这意味着如果主机发生故障,Pod 不会自动迁移到其他节点。

如果你希望 Pod 出现在其他可用节点上,你需要使用 Deployment 等控制器来管理 Pod,即使你只需要一个副本。

你还可以调整restartPolicy 以更改 Pod 的恢复策略。除了 Always 外,还有两个选项:

  • Always:容器不运行时自动重启。

  • OnFailure:仅在容器失败时自动重启。

  • Never:永不重启容器。

在实践中,你需要根据应用程序的特性设置这些恢复策略。

例如,如果一个 Pod 只是计算 1+1=2,退出并变为 Succeeded,使用restartPolicy=Always 强制重启此 Pod 的容器没有实际意义。

如果你需要在容器退出后保留上下文信息,如日志、文件和目录,应将restartPolicy 设置为 Never。否则,这些内容可能会在容器自动重新创建时丢失(被垃圾回收)。

值得一提的是,Kubernetes 官方文档总结了restartPolicy 与容器和 Pod 状态之间的复杂关系。实际上,你不需要记住所有这些细节。只需记住以下两个基本设计原则:

  • 只要restartPolicy 允许在失败时重启容器(如 Always),Pod 将保持在 Running 状态并尝试重启容器。否则,Pod 将进入 Failed 状态。

  • 对于包含多个容器的 Pod,只有当所有容器都处于异常状态时,Pod 才会进入 Failed 状态。在此之前,Pod 保持 Running 状态。此时,Pod 的 READY 字段将显示正常容器的数量。

$ kubectl get pod test-liveness-exec
NAME           READY     STATUS    RESTARTS   AGE
liveness-exec   0/1       Running   1          1m

因此,如果一个 Pod 只包含一个容器且该容器异常退出,Pod 只有在restartPolicy=Never 时才会进入 Failed 状态。在其他情况下,由于 Kubernetes 可以重启容器,Pod 的状态保持 Running。

如果 Pod 有多个容器且只有一个容器异常退出,Pod 仍将保持 Running 状态,即使restartPolicy=Never。只有当所有容器都异常退出时,Pod 才会进入 Failed 状态。

其他情况可以类似推导。

现在,让我们回到之前提到的 livenessProbe。

除了在容器内执行命令外,livenessProbe 还可以定义为发起 HTTP 或 TCP 请求。格式如下:

...
livenessProbe:
     httpGet:
       path: /healthz
       port: 8080
       httpHeaders:
       - name: X-Custom-Header
         value: Awesome
       initialDelaySeconds: 3
       periodSeconds: 3
...
    livenessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 20

因此,你的 Pod 可以暴露一个健康检查 URL(如/healthz)或配置健康检查以监控应用程序的监听端口。这两种方法在 Web 服务应用程序中非常常见。

在 Kubernetes Pod 中,还有一个名为readinessProbe 的字段。虽然其用法与livenessProbe 类似,但其目的却大不相同。readinessProbe 的成功或失败决定了 Pod 是否可以通过 Service 访问,但它不影响 Pod 的生命周期。这部分将在讨论 Services 时详细介绍。

讨论了这么多字段后,你现在应该对 Pod 的语义和描述能力有了初步的了解。

此时,你可能会想:Pod 中有这么多字段,而且不可能记住所有字段,Kubernetes 能否自动填充其中一些字段?

这个需求实际上非常实用。例如,开发者只需提交一个非常简单的 Pod YAML,Kubernetes 就可以自动为相应的 Pod 对象添加其他必要信息,如标签、注解、volumes 等。这些信息可以由运维团队预定义。

此功能称为 PodPreset,在 Kubernetes 版本 v1.11 中引入。

例如,假设开发者编写了以下pod.yaml 文件:

apiVersion: v1
kind: Pod
metadata:
  name: website
  labels:
    app: website
    role: frontend
spec:
  containers:
    - name: website
      image: nginx
      ports:
        - containerPort: 80

作为一个 Kubernetes 初学者,你可能会觉得这很有趣:这不就是你最习惯编写的最简单的 Pod YAML 文件吗?确实,你可能闭着眼睛都能写出这个 YAML 文件中的字段。

然而,如果运维人员看到这个 Pod,他们可能会摇头:这个 Pod 根本不适合生产环境!

在这种情况下,运维人员可以定义一个 PodPreset 对象。在此对象中,他们可以预定义任何希望添加到开发者编写的 Pod 中的字段。例如,考虑这个preset.yaml

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  selector:
    matchLabels:
      role: frontend
  env:
    - name: DB_PORT
      value: "6379"
  volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
    - name: cache-volume
      emptyDir: {}

在这个PodPreset 定义中,第一部分是一个selector。这意味着后续定义将仅适用于具有selector 定义的标签 "role: frontend" 的 Pod,以防止“附带损害”。

然后,定义了一组 Pod 的 Spec 中的标准字段及其对应值。例如,env 字段定义了DB_PORT 环境变量,volumeMounts 指定了容器的 Volume 挂载路径,volumes 定义了一个emptyDir Volume。

接下来,假设运维人员首先创建了这个PodPreset,然后开发者创建了 Pod:

$ kubectl create -f preset.yaml
$ kubectl create -f pod.yaml

Pod 启动运行后,让我们检查此 Pod 的 API 对象:

$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
  name: website
  labels:
    app: website
    role: frontend
  annotations:
    podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
  containers:
    - name: website
      image: nginx
      volumeMounts:
        - mountPath: /cache
          name: cache-volume
      ports:
        - containerPort: 80
      env:
        - name: DB_PORT
          value: "6379"
  volumes:
    - name: cache-volume
      emptyDir: {}

此时,你可以清楚地看到 Pod 已添加了额外的标签、环境变量(env)、volumes 和 volume mounts,这些都与PodPreset 中指定的配置一致。此外,Pod 已自动注解,表明它已被PodPreset 修改。

需要注意的是,PodPreset 中定义的内容仅在 Pod 对象创建之前附加到 Pod API 对象中,并且不会影响任何 Pod 控制器的定义。

例如,如果你提交了一个nginx-deployment,Deployment 对象本身永远不会被PodPreset 修改;只有此 Deployment 创建的 Pod 会被修改。这个区别至关重要。

现在,让我们回答一个问题:如果为单个 Pod 定义了多个PodPreset 对象,会发生什么?

在实践中,Kubernetes 将合并来自不同PodPreset 对象的修改。但是,如果PodPreset 对象中指定的修改存在冲突,这些冲突字段将不会被更改。


原文地址:https://blog.csdn.net/J56793/article/details/144951260

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!