Kubernetes-GCP-入门指南-二-

107 阅读48分钟

Kubernetes GCP 入门指南(二)

原文:Beginning Kubernetes on the Google Cloud Platform

协议:CC BY-NC-SA 4.0

三、部署和扩展

一个部署是一组统一管理的 Pod 实例,它们都基于相同的 Docker 映像。一个 Pod 实例被称为副本。部署控制器使用多个副本来实现高可伸缩性,通过提供比单个单体单元更多的计算能力,以及集群内高可用性,通过将流量从不健康的单元分流(在服务控制器的帮助下,我们将在第四章中看到)并在它们出现故障或阻塞时重新启动或重新创建它们。

根据这里给出的定义,部署可能只是“Pod 集群”的一个花哨名称,但“部署”实际上并不是用词不当;部署控制器的真正力量在于其实际的发布能力—向其消费者部署几乎零停机时间的新 Pod 版本(例如,使用蓝/绿或滚动更新)以及不同扩展配置之间的无缝过渡,同时保留计算资源。

本章一开始,我们将概述部署控制器、复制集控制器和 pod 之间的关系。然后,我们将学习如何启动、监视和控制部署。我们还将看到定位和引用 Kubernetes 作为指示部署的结果而创建的对象的各种方法。

一旦涵盖了部署的要点,我们将重点关注可用的部署策略,包括滚动和蓝/绿部署,以及允许在资源利用率和服务消费者影响之间实现最佳折衷的参数。

最后,我们将讨论在 Pod 级别使用 Kubernetes 的开箱即用水平 Pod 自动缩放器(HPA)和在节点级别使用 GKE 在集群创建时的自动缩放标志进行自动缩放的主题。

复制集

由于历史原因,运行副本的过程是通过一个名为 ReplicaSet 的独立组件来处理的。这个组件又替换了一个更老的组件,叫做复制控制器

为了避免任何混淆,让我们花一些时间来理解部署控制器和复制集控制器之间的关系。部署是更高级别的控制器,它管理部署转换(例如滚动更新)以及通过复制集的复制条件。并不是部署替换或者嵌入replica set 对象(Camel 大小写拼写用于指代对象名),他们只是简单地控制它;尽管鼓励用户通过部署与复制集进行交互,但复制集仍然作为离散的 Kubernetes 对象可见。

总之,在 Kubernetes 中,ReplicaSet 是一个独立的、完全合格的对象,但是不鼓励在部署之外运行 ReplicaSet,而且,只要 ReplicaSet 在部署的控制之下,所有的交互都应该由部署对象进行。

我们的第一次部署

部署是 Kubernetes 中的一个基本特性,创建一个部署比创建一个单一的单体 Pod 更容易。如果我们看一下上一章的例子,我们会注意到每一个kubectl run实例都必须带有一个--restart=Never标志。嗯,创建部署的一种“廉价”方式是简单地丢弃这个标志:

$ kubectl run nginx --image=nginx
deployment.apps/nginx created

省略--restart=Never标志具有戏剧性的效果。我们现在已经创建了两个额外的对象,而不是创建一个单元:一个部署控制器,它控制一个复制集,然后复制集反过来控制一个单元!

$ kubectl get deployment,replicaset,pod
NAME               DESIRED CURRENT UP-TO-DATE AVAIL.
deployment.*/nginx 1       1       1          1

NAME                        DESIRED   CURRENT   READY
replicaset.*/nginx-8586cf59 1         1         1

NAME                       READY  STATUS    RESTARTS
pod/nginx-8586cf59-b72sn   1/1    Running   0

虽然以这种方式创建部署很方便,但是 Kubernetes 将来会反对使用kubectl run命令创建部署。新的首选方法是通过kubectl create deployment <NAME>命令,如下所示:

$ kubectl create deployment nginx --image=nginx
deployment.apps/nginx created

目前这两个版本是等价的,但是“老方法”,通过kubectl run,仍然是大多数教科书和官方 http://kubernetes.io 网站上的例子所使用的方法;因此,建议读者暂时记住这两种方法。

注意

从 Kubernetes v1.15 开始,kubectl create <RESOURCE-TYPE>命令仍然不能很好地替代传统的kubectl run方法。例如,当 Kubernetes 团队反对通过传统的基于运行的形式创建 CronJobs(在第七章中有所涉及)命令时,他们没有包括--schedule标志。根据 GitHub 上的功能请求,这个问题在随后的版本中得到了解决。

kubectl create deployment的情况下,--replicas标志缺失。这并不意味着命令被“破坏”,但是它迫使用户采取更多的步骤来实现一个曾经只需要一个命令的目标。部署的副本数量仍然可以通过kubectl scale命令(将在下一节中介绍)或通过声明一个 JSON 片段来强制设置。

将我们的注意力转回到作为创建部署的结果而创建的对象上,给定的名称nginx现在应用于部署控制器实例,而不是 Pod。Pod 有一个随机的名字:nginx-8586cf59-b72sn。为什么 POD 现在有随机的名字是因为它们是短暂的。它们的数量可能不同;一些可能被杀死,一些新的可能被创造,等等。事实上,控制单个 Pod 的部署不是很有用。让我们通过使用--replicas=<N>标志来指定除 1(默认值)之外的副本数量:

# Kill the running Deployment first
$ kubectl delete deployment/nginx

# Specify three replicas
$ kubectl run nginx --image=nginx --replicas=3
deployment.apps/nginx created

我们现在将看到三个而不是一个 pod 在运行:

$ kubectl get pods
NAME                    READY STATUS   RESTARTS  AGE
nginx-64f497f8fd-8grlr  1/1   Running  0         39s
nginx-64f497f8fd-8svqz  1/1   Running  0         39s
nginx-64f497f8fd-b5hxn  1/1   Running  0         39s

副本的数量是动态的,可以在运行时使用kubectl scale deploy/<NAME> --replicas=<NUMBER>命令指定。例如:

$ kubectl scale deploy/nginx --replicas=5
deployment.extensions/nginx scaled

$ kubectl get pods
NAME                    READY STATUS   RESTARTS  AGE
nginx-64f497f8fd-8grlr  1/1   Running  0         5m
nginx-64f497f8fd-8svqz  1/1   Running  0         5m
nginx-64f497f8fd-b5hx   1/1   Running  0         5m
nginx-64f497f8fd-w8p6k  1/1   Running  0         1m
nginx-64f497f8fd-x7vdv  1/1   Running  0         1m7

同样,指定的 Pod 映像也是动态的,可以使用kubectl set image deploy/<NAME> <CONTAINER-NAME>=<URI>命令更改。例如:

$ kubectl set image deploy/nginx nginx=nginx:1.9.1
deployment.extensions/nginx image updated

关于列出部署的更多信息

kubectl get deployments命令显示许多列:

$ kubectl get deployments
NAME    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
nginx   5        5        5           5          10m

显示的列是指部署中的 Pod 副本的数量:

  • DESIRED:在deployment.spec.replicas中指定的目标状态

  • CURRENT:运行但不一定可用的副本数量:在deployment.status.replicas中指定

  • UP-TO-DATE:已经被更新以达到当前状态的 Pod 副本的数量:在deployment.status.updatedReplicas中指定

  • AVAILABLE:用户实际可用的副本数量:在deployment.status.availableReplicas中指定

  • AGE:部署控制器自首次创建以来已经运行了多长时间

部署清单

一个最小但完整的部署清单的例子可能会令人生畏。因此,更容易将部署清单视为一个两步过程。

第一步是定义一个 Pod 模板。Pod 模板几乎与独立 Pod 的定义相同,只是我们只填充了metadataspec部分:

# Pod Template
...
spec:
  template:
    metadata:
      labels:
        app: nginx-app # Pod label
    spec:
      containers:
      - name: nginx-container
        image: nginx:1.7.1

还需要声明一个显式的 Pod 标签键/对,因为我们需要引用 Pod。前面我们已经使用了app: nginx-app,,然后在部署规范中使用它来将控制器对象绑定到 Pod 模板:

# Deployment Spec
...
spec:
  replicas: 3        # Specify number of replicas
  selector:
    matchLabels:     # Select Pod using label
      app: nginx-app

spec:下,我们还指定了副本的数量,这相当于命令形式中使用的--replicas=<N>标志。

最后,我们将这两个定义组合成一个完整的部署清单:

# simpleDeployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-declarative
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx-container
        image: nginx:1.7.1

首先使用kubectl apply -f <FILE>命令创建这个部署:

$ kubectl apply -f simpleDeployment.yaml
deployment.apps/nginx-declarative created

效果将类似于它的命令性对应物的效果;将创建三个单元:

$ kubectl get pods
NAME                      READY STATUS  RESTARTS AGE
nginx-declarative-*-bj4wn 1/1   Running 0        3m
nginx-declarative-*-brhvw 1/1   Running 0        3m
nginx-declarative-*-tc6hv 1/1   Running 0        3m

监视和控制部署

kubectl rollout status deployment/<NAME>命令用于监控正在进行的部署。例如,假设我们为 Nginx 创建了一个新的强制性部署,并且我们想要跟踪它的进度:

$ kubectl run nginx --image=nginx --replicas=3 \
    ; kubectl rollout status deployment/nginx
deployment.apps/nginx created
Waiting for deployment "nginx" rollout to finish:
0 of 3 updated replicas are available...
Waiting for deployment "nginx" rollout to finish:
1 of 3 updated replicas are available...
Waiting for deployment "nginx" rollout to finish:
2 of 3 updated replicas are available...
deployment "nginx" successfully rolled out

简单的部署——尤其是那些不涉及对现有部署进行更新的部署——通常在几秒钟内即可执行;然而,更复杂的部署可能需要几分钟。在这种情况下,我们可能希望暂停正在进行的部署,以清理资源或执行额外的监视。

分别使用kubectl rollout pause deploy/<NAME>kubectl rollout resume deploy/<NAME>命令暂停和恢复部署。例如:

$ kubectl rollout pause deploy/nginx
deployment "nginx" paused

$ kubectl rollout resume deploy/nginx
deployment "nginx" resumed

找出部署的副本集

副本(Pod 实例)不是由部署控制器直接控制的,而是由中间媒介 ReplicaSet 控制器控制的。因此,确定哪些复制集控制器从属于给定的部署控制器通常是有用的。

这可以通过标签选择器,使用kubectl describe命令或者简单地依靠视觉匹配来实现。

让我们从标签选择器方法开始。在这种情况下,我们简单地使用kubectl get rs命令列出复制集,但是添加了--selector=<SELECTOR-EXPRESSION>标志以匹配部署清单中 Pod 的标签和选择器表达式。例如:

$ kubectl get rs --selector="run=nginx"
NAME               DESIRED   CURRENT   READY     AGE
nginx-64f497f8fd   3         3         3         2m

请注意标签run=nginx是由kubectl run命令自动添加的;在simpleDeployement.yaml,我们使用了一个自定义标签:app=nginx-app

现在,让我们考虑一下kubectl describe方法。在这里,我们只需键入kubectl describe deploy/<NAME>命令并定位OldReplicaSetsNewReplicaSet字段。例如:

$ kubectl describe deploy/nginx
...
OldReplicaSets: <none>
NewReplicaSet:  nginx-declarative-381369836
                (3/3 replicas created)
...

最后一种更简单的方法是简单地输入kubectl get rs并识别前缀是部署名称的复制集。

有时,我们可能想要找出给定复制集的父部署控制器。为此,我们可以使用kubectl describe rs/<NAME>命令并搜索Controlled By字段的值。例如:

$ kubectl describe rs/nginx-381369836
...
Controlled By:  Deployment/nginx
...

或者,如果需要更程序化的方法,我们可以使用 JSONPath:

$ kubectl get pod/nginx-381369836-g4z5r \
    -o jsonpath \
    --template="{.metadata.ownerReferences[*].name}"
nginx-381369836

找出复制体的 POD

在上一节中,我们已经看到了如何识别部署的副本集。反过来,复制集控制 POD;因此,下一个自然的问题是如何找出哪些是在给定复制集控制下的荚果。幸运的是,我们使用了之前见过的三种技术:标签选择器、kubectl describe命令和视觉匹配。

让我们从标签选择器开始。这与之前完全相同,使用了--selector=<SELECTOR-EXPRESSION>标志,除了我们向kubectl get请求类型为pod (Pod)的对象,而不是rs(复制集)。例如:

$ kubectl get pod --selector="run=nginx"
NAME                   READY  STATUS   RESTARTS  AGE
nginx-64f497f8fd-72vfm 1/1    Running  0         18m
nginx-64f497f8fd-8zdhf 1/1    Running  0         18m
nginx-64f497f8fd-skrdw 1/1    Running  0         18m

类似地,kubectl describe命令的使用包括简单地指定复制集的对象类型(rs)和名称:

$ kubectl describe rs/nginx-381369836
...
Events:
  FirstSeen LastSeen Count Message
  --------- -------- ----- -------
  55m       55m      1     Created pod: nginx-*-cv2xj
  55m       55m      1     Created pod: nginx-*-8b5z9
  55m       55m      1     Created pod: nginx-*-npkn8
...

视觉匹配技术是最简单的。我们可以通过考虑 Pod 的两个前缀字符串来计算出副本集的名称。例如,对于 Pod nginx-64f497f8fd-72vfm,它的控制复制集将是nginx-64f497f8fd:

nginx-64f497f8fd-8zdhf  1/1  Running   0     18m

最后但同样重要的是,如果我们从一个 Pod 的对象开始,并想找出它的控制对象,我们可以使用kubectl describe pod/<NAME>命令并定位控制器对象,后跟Controlled By:属性:

$ kubectl describe pod/nginx-381369836-g4z5r
...
Controlled By:  ReplicaSet/nginx-381369836
...

如果需要更程序化的方法,可以使用 JSONPath 获得相同的结果,如下所示:

$ kubectl get pod/nginx-381369836-g4z5r \
    -o jsonpath \
    --template="{.metadata.ownerReferences[*].name}"
nginx-declarative-381369836

删除部署

使用触发级联删除的kubectl delete deploy/<NAME>命令删除部署;所有子副本集和相关的 Pod 对象都将被删除:

$ kubectl delete deploy/nginx
deployment.extensions "nginx" deleted

kubectl delete命令也可以直接从清单文件中选取部署的名称,如下所示:

$ kubectl delete -f simpleDeployment.yaml
deployment.apps "nginx-declarative" deleted

可以通过添加--cascade=false标志来防止级联默认删除行为(导致所有副本集和单元被删除)。例如:

$ kubectl delete -f simpleDeployment.yaml \
    --cascade=false
deployment.apps "nginx-declarative" deleted

版本跟踪与仅扩展部署

部署可以分为两种类型:版本跟踪仅伸缩

修订跟踪部署是更改 Pod 规范的某个方面,很可能是其中声明的容器映像的数量和/或版本。仅改变副本数量(强制和声明)的部署不会触发修订。

例如,发出kubectl scale命令不会创建一个可用于撤销比例变化的修订点。返回到先前数量的副本需要再次设置先前的数量。

通过使用kubectl scale命令或者通过设置deployment.spec.replicas属性并使用kubectl apply -f <DEPLOYMENT-MANIFEST>命令应用相应的文件来强制实现扩展部署。

由于仅扩展部署完全由复制集控制器管理,因此主部署控制器不提供修订跟踪(例如,回滚功能)。客观地说,尽管这种行为相当不一致,但人们可以认为改变副本的数量不如改变映像那样重要。

部署策略

到目前为止,我们只是将部署视为一种部署多个 Pod 副本的机制,但我们没有描述如何控制这一过程:Kubernetes 应该删除所有现有的 Pod(导致停机)还是应该以更优雅的方式进行?这就是部署策略的全部内容。Kubernetes 提供了两大类部署策略:

  • Recreate: Oppenheimer 升级部署的方法:首先销毁所有东西,然后创建由新部署清单声明的副本。

  • RollingUpdate: 微调升级流程,从一次更新一个 Pod 这样的“细致”工作,一直到完全成熟的蓝绿色部署,在部署过程中,在丢弃旧的 Pod 副本之前,会建立一整套新的 Pod 副本。

使用可设置为RecreateRollingUpdatedeployment.spec.strategy.type属性配置策略。后者是默认的。在接下来的两节中,我们将详细讨论每个选项。

重新创建部署

重新创建部署实际上是终止所有现有的 pod,并根据指定的目标状态创建新的集。从这个意义上说,重新创建部署会导致停机,因为一旦 pod 的数量达到零,在创建新的 pod 之前会有一些延迟。

重新创建部署对于非生产场景非常有用,在这些场景中,我们希望尽快看到预期的更改,而不必等待滚动更新程序完成。

通常,部署类型会在部署清单中以声明方式指定:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-declarative
spec:
  replicas: 5
  strategy:
    type: Recreate # Recreate setting
...

当使用kubectl apply -f <MANIFEST>命令应用时,实际的重新创建部署将会发生。

滚动更新部署

滚动更新通常是一次更新一个 Pod(负载均衡器在后台进行相应的管理),这样用户就不会经历停机时间,并且资源(例如,节点数量)得到合理利用。

在实践中,传统的“一次一个”滚动更新部署蓝/绿部署(前面几节将详细介绍蓝/绿部署)都可以通过设置deployment.spec.rolling.Update.maxSurgedeployment.spec.rolling.Update.maxUnavailable变量使用相同的滚动更新机制来实现。我们将进一步了解如何实现这一目标。

现在,让我们考虑下面的说明:

...
spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
...

这里显示的maxSurgemaxUnavailable值允许我们调整滚动更新的性质:

  • maxSurge:该属性指定在基线(旧副本集)上的终止过程开始之前,必须为目标(新副本集)创建的单元数量。如果该值为 1,这意味着副本的数量将在部署期间保持不变,代价是 Kubernetes 在任何给定时间为一个额外的单元分配资源。

  • maxUnavailable:该属性指定在任何给定时间可能不可用的节点的最大数量。如果该数量为零,如在前面的例子中,则至少需要值为 1 的maxSurge,因为需要备用资源来保持期望副本的数量恒定。

请注意,maxUnavailablemaxSurge变量接受百分比值。例如,在这种情况下,Kubernetes 25%的额外资源将用于保证运行的副本数量不会减少:

...
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0%
...

这个例子相当于最初讨论的例子,因为四个副本中的 25%恰好是一个副本。

更高的 MaxSurge 值的利弊

在从基线(旧)发生转换之前,较高的maxSurge设置会创建目标的更多 Pod 实例,将成为(新)复制集。因此,maxSurge号越高,用户达到基线版本的过渡期就越短——只要部署与第四章中控制的服务结合使用。

缺点是集群需要额外的资源来创建新的 pod,同时保持旧的 pod 运行。例如,假设基准复制集计算利用率为 100%,50%的maxSurge设置在迁移高峰时将正好需要 150%的计算资源。

高 MaxUnavailable 值的利弊

在理想情况下,maxUnavailable属性应该简单地设置为 0,以确保副本的数量保持不变。然而,这并不总是可能的,或者必需的

如果 Kubernetes 集群中的计算资源不足,那么maxSurge必须为 0。在这种情况下,从基线状态的转换将涉及取下一个或多个吊舱。

例如,以下设置可确保不分配额外的资源,但在部署期间的任何给定时间,至少有 75%的节点可用:

...
maxSurge: 0%
maxUnavailable: 25%
...

更高的maxUnavailable值的缺点是会减少集群的容量——包括与部署相关的副本,而不是整个 Kubernetes 集群。然而,这未必是一件坏事。部署可以在需求较低的时候执行,减少副本的数量可能不会产生明显的效果。此外,即使该值越高,可用的副本数量越少,整个过程也会更快,因为可以同时更新多个 pod。

蓝色/绿色部署

蓝/绿部署是指我们提前将整个新的部署为 Pod 集群,当准备就绪时,我们一次性将流量从基线 Pod 集群中切换出来。在服务控制器的帮助下,整个过程被透明地编排(参见第四章)。

在 Kubernetes,这并不是一个全新的策略类型;我们仍然使用RollingUpdate部署类型,但是我们将maxSurge属性设置为 100%,将maxUnavailability属性设置为 0,如下所示:

...
maxSurge: 100%
maxUnavailable: 0%
...

假设部署构成了修订变更—底层映像类型或映像版本发生了变化—Kubernetes 将分三大步骤执行蓝/绿部署:

  1. 为要成为新副本集的创建新单元,直到达到maxSurge限制;在这种情况下是 100%。

  2. 将流量重定向到新的副本集——这需要服务控制器的帮助,我们将在第四章中介绍。

  3. 终止旧副本集中的节点。

最大浪涌和最大不可用设置摘要

正如我们在前面几节中看到的,大多数部署策略包括将maxSurgemaxUnavailability属性设置为不同的值。表 3-1 总结了最典型的用例以及相关的权衡和样本值。

表 3-1

不同部署策略的适当值

|

方案

|

权衡取舍

|

maxSurge

|

maxUnavailbility

| | --- | --- | --- | --- | | 销毁和部署* | 容量和效用。 | Zero | 100% | | 一次一个滚动更新 | 资源 | one | Zero | | 一次一个滚动更新 | 容量 | Zero | one | | 更快的滚动更新 | 资源 | 25% | Zero | | 更快的滚动更新 | 容量 | Zero | 25% | | 蓝色/绿色 | 资源 | 100% | Zero |

*与重新创建的部署相同

受控部署

受控展开是一种允许操作员(或等效的自动化系统)对潜在的失败展开做出反应的展开。部署可能会以多种方式失败,但在两种情况下,Kubernetes 为运营商提供了更多的控制。

第一种情况是最常见的:Kubernetes 无法更新请求的副本数量,因为没有足够的集群资源,或者因为 pod 本身无法启动—例如,当指定了不存在的容器映像时。新的计算资源或容器映像不太可能突然出现并纠正这种情况。相反,我们想要的是 Kubernetes 在设定的时间后认为部署失败,而不是让部署永远进行下去。这是通过将deployment.spec.progressDeadlineSeconds属性设置为适当的值来实现的。

例如,让我们考虑指定大量副本(30 个)的示例,假设一个小型的三节点 Kubernetes 集群计算资源不足:

# nginxDeployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  progressDeadlineSeconds: 60
  replicas: 30 # excessive number for a tiny cluster
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.1
        ports:
        - containerPort: 80

如果 60 秒后所有副本都不可用,属性将强制 Kubernetes 使部署失败:

$ kubectl apply -f nginxDeployment.yaml ; \
    kubectl rollout status deploy/nginx
deployment.apps/nginx created
Waiting for deployment "nginx" rollout to finish:
0 of 30 updated replicas are available...
Waiting for deployment "nginx" rollout to finish:
7 of 30 updated replicas are available...
Waiting for deployment "nginx" rollout to finish:
11 of 30 updated replicas are available...
error: deployment "nginx" exceeded its
progress deadline

第二个场景是部署本身成功,但是容器由于一些内部问题(比如内存泄漏、引导代码中的空指针异常等等)而失败。在受控部署中,我们可能希望部署单个副本,等待几秒钟以确保它是稳定的,然后才继续下一个副本。Kubernetes 的默认行为是并行更新所有副本,这可能不是我们想要的。

属性允许我们定义 Kubernetes 在一个 Pod 准备好之后,在处理下一个副本之前必须等待多长时间。该属性的值是出现最明显问题所需的时间和部署所需时间之间的权衡。

结合progressDeadlineSeconds属性,minReadySeconds属性有助于防止灾难性的部署,尤其是在更新现有的健康部署时。

例如,假设我们有一个名为myapp(在名为myApp1.yaml的清单中声明)的健康部署,它由三个副本组成:

# myApp1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  progressDeadlineSeconds: 60
  minReadySeconds: 10
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: busybox:1.27 # 1.27
        command: ["bin/sh"]
        args: ["-c", "sleep 999999;"]

现在让我们部署myApp1.yaml并通过确保三个 pod 的状态都是Running来检查它是否启动并运行:

$ kubectl apply -f myApp1.yaml
deployment.apps/myapp created

$ kubectl get pods -w
NAME                   READY  STATUS  RESTARTS   AGE
myapp-54785c6ddc-5zmvp 1/1    Running 0          17s
myapp-54785c6ddc-rbcv8 1/1    Running 0          17s
myapp-54785c6ddc-wlf8r 1/1    Running 0          17s

假设我们想用一个新的“有问题的”版本来更新部署,这个版本存储在一个名为myApp2.yaml的清单中:更多关于为什么有问题的信息在源代码片段之后:

# myApp2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  progressDeadlineSeconds: 60
  minReadySeconds: 10
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: busybox:1.27 # 1.27
        command: ["bin/sh"]
        args: ["-c", "sleep 5; exit 1"] # bug!

我们在前面的清单中故意引入了一个错误;应用在五秒钟后退出,并出现错误。我们现在应用新的,有问题的部署清单:

$ kubectl apply -f myApp2.yaml
deployment.apps/myapp configured

如果我们观察 pod 的状态,我们将看到一个新的副本控制器5d79979bc9将被生成,并且它将只创建一个名为nm7pv的 pod。请注意,健康的副本控制器54785c6ddc保持不变:

$ kubectl get pods -w
NAME                   STATUS            RESTARTS
myapp-54785c6ddc-5zmvp Running           0
myapp-54785c6ddc-rbcv8 Running           0
myapp-54785c6ddc-wlf8r Running           0
myapp-5d79979bc9-sgqsn Pending           0
myapp-5d79979bc9-sgqsn Pending           0
myapp-5d79979bc9-sgqsn ContainerCreating 0s
myapp-5d79979bc9-sgqsn Running           0
myapp-5d79979bc9-sgqsn Error             0
myapp-5d79979bc9-sgqsn Running           1
myapp-5d79979bc9-sgqsn Error             1
myapp-5d79979bc9-sgqsn CrashLoopBackOff  1
...

一秒钟后,pod sgqsn显示为Running,,但是因为我们已经将minReadySeconds设置为十秒钟,Kubernetes 还没有开始旋转新的 pod。不出所料,五秒钟后,Pod 出现错误,Kubernetes 继续重新启动 Pod。

由于我们也将deadlineProgressSeconds设置为 60,新的错误部署将在一段时间后过期:

$ kubectl rollout status deploy/myapp
Waiting for deployment "myapp" rollout to finish:
1 out of 3 new replicas have been updated...
Waiting for deployment "myapp" rollout to finish:
1 out of 3 new replicas have been updated...
error: deployment "myapp" exceeded its
progress deadline

最终结果将是现有的健康复制集myapp-54785c6ddc保持不变;这种行为允许我们修复失败的部署,并在不中断健康 pod 用户的情况下重试:

$ kubectl get pods
NAME                   STATUS           RESTARTS
myapp-54785c6ddc-5zmvp Running          0
myapp-54785c6ddc-rbcv8 Running          0
myapp-54785c6ddc-wlf8r Running          0
myapp-5d79979bc9-sgqsn CrashLoopBackOff 5

请注意,使用minReadySeconds属性时,deadlineProgressSeconds中指定的时间可能会稍微延长。

首次展示历史

给定部署的首次展示历史由针对其执行的修订跟踪更新组成。正如我们之前所解释的,仅缩放更新不会创建修订版,也不是部署历史的一部分。修订是允许执行回滚的机制。卷展栏的历史是一个按升序排列的修订列表,可以使用kubectl rollout history deploy/<NAME>命令进行检索。例如:

$ kubectl rollout history deploy/nginx
deployments "nginx"
REVISION        CHANGE-CAUSE
1               <none>
2               <none>

注意,CHANGE-CAUSE字段的值是<none>,因为默认情况下不记录变更命令,. Kubernetes 为每个修订跟踪部署更新分配一个递增的修订号。此外,它还可以记录命令(如kubectl applykubectl set image等)。)用于创建每个修订。要实现这种行为,只需在每个命令后添加--record标志。这将填充CHANGE-CAUSE列—依次从deployment.metadata.annotations.kubernetes.io/change-cause获得。例如:

$ kubectl run nginx --image=nginx:1.7.0 --record
deployment.apps/nginx created

$ kubectl set image deploy/nginx nginx=nginx:1.9.0 \
    --record
deployment.extensions/nginx image updated

$ kubectl rollout history deploy/nginx
deployments "nginx"
REVISION CHANGE-CAUSE
1        kubectl run nginx --image=nginx:1.7.0 ...
2        kubectl set image deploy/nginx ...

了解用于创建修订版的命令可能不足以区分修订版和其他修订版,尤其是在部署的详细信息是通过文件(声明性方法)捕获的情况下,这一点也很有用。为了获得修订版的映像和其他元数据的细节,我们应该使用kubectl rollout history --revision=<N>命令。例如:

$ kubectl rollout history deploy/nginx --revision=1
deployments "nginx" with revision #1
Pod Template:
  Labels:       pod-template-hash=4217019353
        run=nginx
  Annotations:  kubernetes.io/change-cause=kubectl
                run nginx --image=nginx --record=true
  Containers:
   nginx:
    Image:      nginx
    Port:       <none>
    Environment:        <none>
    Mounts:     <none>
  Volumes:      <none>
...

为了使修订历史中的条目数量易于管理,deployment.spec.revisionHistoryLimit有助于建立最大限制。

正在回滚部署

在 Kubernetes 中,回滚部署被称为撤销过程,它是使用kubectl rollout undo deploy/<NAME>命令执行的。撤消过程实际上创建了一个新的修订,其参数与前一个相同。通过这种方式,进一步的撤销将导致回到之前的版本。在以下示例中,我们创建了两个部署,检查了展开历史记录,执行了撤消操作,然后再次检查了展开历史记录:

$ kubectl run nginx --image=nginx:1.7.0 --record
deployment.apps/nginx created

$ kubectl set image deploy/nginx nginx=nginx:1.9.0 \
    --record
deployment.extensions/nginx image updated

$ kubectl rollout history deploy/nginx
deployments "nginx"
REVISION CHANGE-CAUSE
1        kubectl run nginx --image=nginx:1.7.0 ...
2        kubectl set image deploy/nginx ...

$ kubectl rollout undo deploy/nginx
deployment.extensions/nginx

$ kubectl rollout history deploy/nginx
deployments "nginx"
REVISION CHANGE-CAUSE
2       kubectl set image deploy/nginx ...
3       kubectl run nginx --image=nginx:1.7.0 ...

也可以回滚到特定的修订,而不是以前的修订。这是通过使用常规的撤销命令并添加--to-revision=<N>标志来实现的。例如:

$ kubectl rollout undo deploy/nginx --to-revision=2

使用0作为修订号会导致 Kubernetes 恢复到之前的版本——这相当于省略了标志。

水平吊舱自动缩放器

自动扩展是指运行时系统根据 CPU 负载等可观察指标,以无人值守的方式分配额外计算和存储资源的能力。特别是在 Kubernetes 中,当前事实上的自动缩放功能是由一种称为水平 Pod 自动缩放器(HPA)的服务提供的。水平 Pod 自动缩放器(HPA)是一个常规的 Kubernetes API 资源和控制器,它根据观察到的资源利用率以无人值守的方式管理 Pod 的副本数量。它可以被认为是一个机器人,它根据 Pod 的扩展标准(通常是平均 CPU 负载)代表人类管理员发出kubectl scale命令。

为了避免混淆,有必要理解“水平缩放”是指跨多个节点创建或删除 Pod 副本,这是在编写水平 Pod 自动缩放(HPA)服务时 Kubernetes 中正式实现的唯一一种自动缩放类型。不应将水平缩放误认为垂直缩放。垂直扩展涉及增加特定节点的计算资源(例如,RAM 和 CPU)。除了水平扩展和垂直扩展之外,实际的“其他”扩展类型是集群扩展,它调整节点(虚拟机或物理机)的数量,而不是固定节点数量内的单元数量。在本章的最后,我们提供了集群扩展的概述。

设置自动缩放

使用kubectl autoscale命令可以强制设置自动缩放。我们首先应该有一个正在运行的部署(或复制集),它将由水平 Pod 自动缩放器(HPA)控制。我们还必须指定实例的最小和最大数量,最后,还要指定作为扩展基础的 CPU 百分比。完整的命令语法如下:kubectl autoscale deploy/<NAME> --min=<N> --max=<N> --cpu-percent=<N>

例如,要在自动缩放模式下运行 Nginx,我们需要遵循以下两个步骤:

$ kubectl run nginx --image=nginx
deployment.apps/nginx created

$ kubectl autoscale deployment nginx \
    --min=1 --max=3 --cpu-percent=5
horizontalpodautoscaler.autoscaling/nginx autoscaled

CPU 百分比为 5 是故意的,以便在部署负载最小时,很容易观察到 HPA 的行为。

以声明方式设置自动扩展需要创建新的清单文件,该文件指定目标部署(或副本集)副本的最小和最大数量,以及 CPU 阈值:

# hpa.yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: nginx
spec:
  maxReplicas: 3
  minReplicas: 1
  scaleTargetRef:
    kind: Deployment
    name: nginx
  targetCPUUtilizationPercentage: 5

为了运行清单,使用了kubectl apply -f <FILE>命令,但是我们必须确保在应用自动缩放清单之前已经启动并运行了部署。例如:

$ kubectl run nginx --image=nginx --replicas=1
deployment.apps/nginx created

$ kubectl apply -f hpa.yaml
horizontalpodautoscaler.autoscaling/nginx created

观察自动缩放的作用

观察自动伸缩的运行只需设置一个较低的 CPU 阈值(比如 5%),然后在运行的副本上创建一些 CPU 负载。让我们来看看这个过程的实际操作。

我们首先创建一个复制副本,并将 HPA 连接到它,如前面所示:

$ kubectl run nginx --image=nginx
deployment.apps/nginx created

$ kubectl autoscale deployment nginx \
    --min=1 --max=3 --cpu-percent=5
horizontalpodautoscaler.autoscaling/nginx autoscaled

然后,我们观察自动缩放器的行为:

$ kubectl get hpa -w
NAME  REFERENCE    TARGETS MINPODS MAXPODS REPLICAS
nginx Deployment/* 0% / 5% 1       3       1

然后,我们打开一个单独的 shell,访问部署的 Pod,并生成一个无限循环来引起一些 CPU 负载:

$ kubectl get pods
NAME                   READY STATUS    RESTARTS   AGE
nginx-4217019353-2fb1j 1/1   Running   0          27m

$ kubectl exec -it nginx-4217019353-sn1px \
    -- sh -c 'while true; do true; done'

如果我们回到观察 HPA 控制器的 shell,我们会看到它是如何增加副本数量的:

$ kubectl get hpa -w
NAME  REFERENCE    TARGETS   MINPODS MAXPODS REPLICAS
nginx Deployment/*  0% / 5%  1       3       1
nginx Deployment/* 20% / 5%  1       3       1
nginx Deployment/* 65% / 5%  1       3       2
nginx Deployment/* 80% / 5%  1       3       3
nginx Deployment/* 90% / 5%  1       3       3

如果我们中断无限循环,我们会看到副本的数量会在一段时间后减少到一个。

另一种方法是在 Nginx HTTP 服务器上生成负载。这将是一个更现实的场景,但需要一些额外的步骤来设置。首先,我们需要一个生成负载的工具,比如 ApacheBench:

$ sudo apt-get update
$ sudo apt-get install apache2-utils

然后,我们需要公开外部负载均衡器上的部署。这使用服务控制器命令—将在第四章中进一步解释:

$ kubectl expose deployment nginx \
    --type="LoadBalancer" --port=80 --target-port=80
service/nginx exposed

我们一直等到获得公共 IP 地址:

$ kubectl get service -w
NAME       TYPE         CLUSTER-IP    EXTERNAL-IP
kubernetes ClusterIP    10.59.240.1   <none>
nginx      LoadBalancer 10.59.245.138 <pending>
nginx      LoadBalancer 10.59.245.138 35.197.222.105

然后,我们向它“抛出”过多的流量,在本例中,使用 100 个单独的线程向外部 IP/端口发出 1,000,000 个请求:

$ ab -n 1000000 -c 100 http://35.197.222.105:80/
This is ApacheBench, Version 2.3
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 35.197.222.105 (be patient)

我们可以通过运行kubectl get hpa -w来观察结果。请注意,nginx 是高效的,显著的负载和快速连接同样需要将 pod 的 CPU 提升到 5%以上。

另一个需要考虑的方面是,当观察 HPA 的动作时,它不会立即做出反应。原因是 HPA 通常每 30 秒查询一次资源利用率,除非默认值已经更改,然后根据所有可用单元的最后一分钟平均值做出反应。这意味着,例如,在 30 秒的窗口中,单个 Pod 上短暂的 99% CPU 峰值可能不足以使聚合平均值超过定义的阈值。

此外,HPA 算法的实现方式避免了不稳定的缩放行为。HPA 在纵向扩展时比在横向扩展时相对更快,因为使服务可用优先于节省计算资源。

更多信息请参考 https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

最后但同样重要的是,当不再需要 HPA 对象时,可以使用kubectl delete hpa/<NAME>命令将其处理掉。例如:

$ kubectl delete hpa/nginx
horizontalpodautoscaler.autoscaling "nginx" deleted

扩展 Kubernetes 集群本身

HPA 控制器受限于固定 Kubernetes 集群中的可用资源。它产生了新的 pod,而不是全新的虚拟机来托管 Kubernetes 节点。

GKE 支持集群式扩展;然而,它的原理与颐康保障户口不同。缩放触发不是 CPU 负载或类似的度量;当 Pods 请求当前集群节点中不可用的计算资源时,会分配新节点。同样,“向下”扩展包括将 pod 整合到更少的节点中;这可能会对那些无法正常管理意外关机的 pod 上运行的工作负载产生影响。

在 GCP 中启用集群式自动缩放的最实用的方法是在创建集群时使用gcloud container clusters create <CLUSTER-NAME> --num-nodes <NUM>(参见第一章了解出错时可能需要的其他附加标志)命令,但添加--enable-autoscaling --min-nodes <MIN> --max-nodes <MAX>作为附加参数。在下面的示例中,我们将<MIN>设置为 3,但将<MAX>设置为 6,以便在需要时可以提供两倍的标准集群容量:

$ gcloud container clusters create my-cluster \
    --num-nodes=3 --enable-autoscaling \
    --min-nodes 3 --max-nodes 6

Creating cluster my-cluster...
Cluster is being health-checked (master is healthy)
done.
Creating node pool my-pool...done.
NAME        LOCATION        MASTER_VERSION  NUM_NODES
my-cluster  europe-west2-a  1.12.8-gke.10   3

查看集群式自动扩展运行情况的最简单方法是启动具有大量副本的部署,并查看部署前后可用节点的数量:

$ kubectl get nodes
NAME                                        STATUS
gke-my-cluster-default-pool-3d996410-7307   Ready     gke-my-cluster-default-pool-3d996410-d2wz   Ready
gke-my-cluster-default-pool-3d996410-gw59   Ready

$ kubectl run nginx --image=nginx:1.9.1 --replicas=25
deployment.apps/nginx created

# After 2 minutes
$ kubectl get nodes
NAME                                        STATUS
gke-my-cluster-default-pool-3d996410-7307   Ready
gke-my-cluster-default-pool-3d996410-d2wz   Ready
gke-my-cluster-default-pool-3d996410-gw59   Ready
gke-my-cluster-default-pool-3d996410-rhnp   Ready
gke-my-cluster-default-pool-3d996410-rjnc   Ready

相反,删除部署会提示自动缩放器减少节点数量:

$ kubectl delete deploy/nginx
deployment.extensions "nginx" deleted

# After some minutes
$ kubectl get nodes
NAME                                        STATUS
gke-my-cluster-default-pool-3d996410-7307   Ready
gke-my-cluster-default-pool-3d996410-d2wz   Ready
gke-my-cluster-default-pool-3d996410-gw59   Ready

摘要

在本章中,我们学习了如何使用部署控制器使用不同的策略(例如滚动和蓝/绿部署)来扩展 pod 和发布新版本。我们还看到了如何监视和控制正在进行的部署,例如,通过暂停、恢复部署,甚至回滚到以前的版本。最后,我们学习了如何在 Pod 和节点级别设置自动伸缩机制,以实现更好的集群资源利用率。

四、服务发现

服务发现是从其他 pod 以及外部世界(互联网)中定位一个或多个 pod 的地址的能力。Kubernetes 提供了一个服务控制器来满足服务发现和连接用例,如 Pod 到 Pod、LAN 到 Pod 和 Internet 到 Pod。

服务发现是必要的,因为 pod 是易变的;在它们的生命周期中,它们可能会被创建和销毁多次,每次都会获得不同的 IP 地址。Kubernetes 的自修复和扩展特性也意味着我们通常需要虚拟 IP 地址和循环负载均衡机制,而不是特定的、类似宠物的 pod 的离散地址。

在本章中,我们将首先探讨前面提到的三种连接用例(Pod 到 Pod、LAN 到 Pod 和 Internet 到 Pod)。然后,我们将研究跨不同空间的发布服务的特性以及多个端口的公开。最后,我们将思考服务控制器如何帮助实现平稳的启动和关闭,以及零停机部署。

连接使用案例概述

服务控制器执行各种功能,但它的主要目的是跟踪 Pods 地址和端口,并将这些信息发布给感兴趣的服务消费者。服务控制器还在群集场景中提供了单一入口点—多个 Pod 副本。为了实现其目的,它使用其他 Kubernetes 服务,如kube-dnskube-proxy,这些服务反过来利用底层内核和来自操作系统的网络资源,如 iptables。

服务控制器适合各种用例,但这些是最典型的用例:

  • Pod-to-Pod : 这个场景涉及一个 Pod 连接到同一个 Kubernetes 集群中的其他 Pod。集群 IP 服务类型用于此目的;它由虚拟 IP 地址和 DNS 条目组成,可由同一集群内的所有 pod 寻址,并将在副本准备就绪和“未准备就绪”时分别从集群中添加和删除副本。

  • LAN-to-Pod : 在这种情况下,服务消费者通常位于 Kubernetes 集群之外,但位于同一个局域网(LAN)内。节点端口服务类型通常适用于满足此用例;服务控制器在每个工作节点中发布一个离散端口,该端口映射到公开的部署。

  • Internet-to-Pod : 在大多数情况下,我们会希望将至少一个部署暴露在互联网上。Kubernetes 将与 GCP 的负载均衡器进行交互,以便创建一个外部公共 IP 地址并将其路由到节点端口。这是一个负载均衡器服务类型。

还有一种特殊情况是无头服务主要与 StatefulSets 结合使用,以提供对每个单独 Pod 的 DNS 访问。这种特殊情况将在第九章中单独介绍。

值得理解的是,服务控制器提供了一个间接层——以 DNS 条目、额外的 IP 地址或端口号的形式——到拥有自己的离散 IP 地址并可以直接访问的 pod。如果我们需要的只是找出一个 Pods 的 IP 地址,我们可以使用kubectl get pods -o wide -l <LABEL>命令,其中<LABEL>将我们正在寻找的 Pods 与其他 Pods 区分开来。只有在大量 pod 运行时才需要-l标志。否则,我们可以通过它们的命名约定来区分这些 POD。

在下一个示例中,我们首先创建三个 Nginx 副本,然后找出它们的 IP 地址:

$ kubectl run nginx --image=nginx --replicas=3
deployment.apps/nginx created

$ kubectl get pods -o wide -l run=nginx
NAME            READY STATUS    RESTARTS   IP
nginx-*-5s7fb   1/1   Running   0          10.0.1.13
nginx-*-fkjx4   1/1   Running   0          10.0.0.7
nginx-*-sg9bv   1/1   Running   0          10.0.1.14

相反,如果我们想要 IP 地址本身——比方说,通过流水线将它们传送到某个程序中——我们可以通过指定 JSON 路径查询来使用编程方法:

$ kubectl get pods -o jsonpath \
   --template="{.items[*].status.podIP}" -l run=nginx
10.36.1.7 10.36.0.6 10.36.2.8

单元到单元连接用例

从外部 Pod 寻址 Pod 涉及创建一个服务对象,该服务对象将观察 Pod,以便在它们分别准备就绪和未准备就绪时,从虚拟 IP 地址(称为集群 IP )添加或删除它们。如第二章所述,容器“准备就绪”的检测可通过自定义探头实现。服务控制器可以针对各种对象,如裸 pod 和复制集,但我们将只关注部署。

第一步是创建服务控制器。创建观察现有部署的服务的命令是kubectl expose deploy/<NAME>。可选标志--name=<NAME>将赋予服务一个不同于其目标部署的名称,只有在我们没有在目标对象上指定端口的情况下,--port=<NUMBER>才是必需的。例如:

$ kubectl run nginx --image=nginx --replicas=3
deployment.apps/nginx created

$ kubectl expose deploy/nginx --port=80
service/nginx exposed

声明性方法类似,但是它需要使用标签选择器(第二章)来标识目标部署:

# service.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 80

可以在创建部署后立即应用此清单,如下所示:

$ kubectl run nginx --image=nginx --replicas=3
deployment "nginx" created

$ kubectl apply -f service.yaml
service "nginx" created

到目前为止,我们已经解决了“暴露”一个部署的问题,但是结果如何呢?我们现在能做什么以前不可能的事情?首先,让我们键入kubectl get services来查看我们刚刚创建的对象的详细信息:

$ kubectl get services
NAME       TYPE      CLUSTER-IP    EXT-IP PORT(S)
kubernetes ClusterIP 10.39.240.1   <none> 443/TCP
nginx      ClusterIP 10.39.243.143 <none> 80/TCP

IP 地址10.39.243.143现在准备好从端口80上的任何 Pod 接收进入流量。我们可以通过运行连接到此端点的虚拟一次性 Pod 来检查这一点:

$ kubectl run test --rm -i --image=alpine \
    --restart=Never \
    -- wget -O - http://10.39.243.143 | grep title
<title>Welcome to nginx!</title>

我们有一个虚拟 IP 来访问我们部署的 Pod 副本,但是 Pod 如何首先找到 IP 地址呢?好消息是 Kubernetes 为每个服务控制器创建了一个 DNS 条目。在一个简单的、单一集群、单一名称空间的场景中——就像本文中的所有例子一样——我们可以简单地使用服务名本身。例如,假设我们在另一个 Pod 中(例如,Alpine 实例),我们可以如下访问 Nginx web 服务器:

$ kubectl run test --rm -i --image=alpine \
    --restart=Never \
    -- wget -O - http://nginx | grep title
<title>Welcome to nginx!</title>

注意,我们现在用http://nginx代替http://10.39.243.143。对 Nginx 的每个 HTTP 请求将以循环方式命中三个 Pod 副本中的一个。如果我们想让自己相信事实确实如此,我们可以改变每个 Nginx Pod 的index.html内容,使其显示 Pod 的名称,而不是相同的默认欢迎页面。假设我们的三副本 Nginx 部署仍然在运行,我们可以应用建议的更改,首先提取部署的 Pod 名称,然后用$HOSTNAME的值覆盖每个 Pod 中的index.html的内容:

# Extract Pod names
$ pods=$(kubectl get pods -l run=nginx -o jsonpath \
       --template="{.items[*].metadata.name})"

# Change the contents of index.html for every Pod
$ for pod in $pods; \
    do kubectl exec -ti $pod \
           -- bash -c "echo \$HOSTNAME > \
           /usr/share/nginx/html/index.html"; \
    done

现在,我们可以再次启动临时 Pod 但是这一次,我们将循环运行对http://nginx的请求,直到我们按下 Ctrl+C:

$ kubectl run test --rm -i --image=alpine \
    --restart=Never -- \
    sh -c "while true; do wget -q -O \
    - http://nginx ; sleep 1 ; done"
nginx-dbddb74b8-t728t
nginx-dbddb74b8-h87s4
nginx-dbddb74b8-mwcg4
nginx-dbddb74b8-h87s4
nginx-dbddb74b8-h87s4
nginx-dbddb74b8-t728t
nginx-dbddb74b8-h87s4
nginx-dbddb74b8-h87s4
...

正如我们在结果输出中看到的,循环机制正在起作用,因为每个请求都落在一个随机的 Pod 上。

LAN-to-Pod 连接用例

从外部主机访问 Kubernetes 集群的 Pods 涉及到使用NodePort服务类型公开服务(默认服务类型是ClusterIP)。这只是将--type=NodePort标志添加到kubectl expose命令中的问题。例如:

$ kubectl run nginx --image=nginx --replicas=3
deployment.apps/nginx created

$ kubectl expose deploy/nginx --type="NodePort" \
    --port=80
service/nginx exposed

结果是,现在可以通过任何节点 IP 地址上的离散端口访问 Nginx HTTP 服务器。首先,让我们看看分配的端口是什么:

$ kubectl describe service/nginx | grep NodePort
NodePort:                 <unset>  30091/TCP

我们可以看到自动分配的端口是30091。我们现在可以使用外部 IP 地址,从位于同一本地区域的 Kubernetes 集群之外的一台机器*,通过该端口上的任何 Kubernetes 工作节点向 Nginx 的 web 服务器发出请求:*

$ kubectl get nodes -o wide
NAME                  STATUS  AGE EXTERNAL-IP
gke-*-9777d23b-9103   Ready   7h  35.189.64.73
gke-*-9777d23b-m6hk   Ready   7h  35.197.208.108
gke-*-9777d23b-r4s9   Ready   7h  35.197.192.9

$ curl -s http://35.189.64.73:30091 | grep title
<title>Welcome to nginx!</title>
$ curl -s http://35.197.208.108:30091 | grep title
<title>Welcome to nginx!</title>
$ curl -s http://35.197.192.9:30091 | grep title
<title>Welcome to nginx!</title>

注意

除非应用进一步的安全/网络设置,否则 Lan-to-Pod 示例可能无法直接在 Google Cloud Shell 中工作。这种设置超出了本书的范围。

作为本节的总结,这里提供了kubectl expose deploy/nginx --type="NodePort" --port=80命令的声明版本:

# serviceNodePort.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 80
  type: NodePort

用于 Pod 到 Pod 访问的 ClusterIP 清单之间的唯一区别是添加了属性type并将其设置为NodePort。如果未声明,该属性的值默认为ClusterIP

互联网到 Pod 连接用例

从互联网访问 pod 包括创建LoadBalancer服务类型。LoadBalancer服务类型类似于NodePort服务类型,因为它将在 Kubernetes 集群的每个节点的离散端口上发布公开的对象。不同的是,除此之外,它还会与谷歌云平台的负载均衡器进行交互,并分配一个可以将流量导向这些端口的公共 IP 地址。

以下示例创建了一个包含三个 Nginx 副本的集群,并将部署公开到 Internet。请注意,最后一个命令使用了-w标志,以便等待分配外部公共 IP 地址:

$ kubectl run nginx --image=nginx --replicas=3
deployment.apps/nginx created

$ kubectl expose deploy/nginx --type=LoadBalancer \
    --port=80
service/nginx exposed

$ kubectl get services -w
NAME  TYPE         CLUSTER-IP    EXT-IP          nginx LoadBalancer 10.39.249.178 <pending>
nginx LoadBalancer 10.39.249.178 35.189.65.215

每当负载均衡器被分配一个公共 IP 地址时,就会填充service.status.loadBalancer.ingress.ip属性——或者其他云供应商(如 AWS)的.hostname。如果我们想以编程方式捕获公共 IP 地址,我们必须做的就是等待,直到设置了这个属性。我们可以通过 Bash 中的一个 while 循环来检测这个解决方案,例如:

while [ -z $PUBLIC_IP ]; \
 do PUBLIC_IP=$(kubectl get service/nginx \
 -o jsonpath \
 --template="{.status.loadBalancer.ingress[*].ip}");\
 sleep 1; \
 done; \
 echo $PUBLIC_IP
35.189.65.215

kubectl expose deploy/nginx --type=LoadBalancer --port=80的声明性版本出现在下一个代码清单中。用于 LAN-to-Pod 用例的清单之间的唯一区别是type属性被设置为LoadBalancer。使用kubectl apply -f serviceLoadBalancer.yaml命令应用清单。在应用这个命令之前,我们可能想通过首先发出kubectl delete service/nginx命令来处理任何正在运行的冲突服务。

# serviceLoadBalancer.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 80
  type: LoadBalancer

访问不同名称空间中的服务

到目前为止,我们看到的所有例子都存在于默认的名称空间中。这就好像默认情况下每个kubectl命令都添加了-n default标志。因此,我们从来不需要关心完整的 DNS 名称。每当我们通过键入kubectl expose deploy/nginx来公开 Nginx 部署时,我们都可以从 Pods 访问结果服务,而不需要任何额外的域组件,例如,通过键入wget http://nginx

但是,如果使用了更多的名称空间,事情可能会变得棘手,了解与每个服务相关的完整 DNS 记录的形状可能会很有用。让我们假设在default名称空间中有一个nginx服务——在这种情况下,不需要指明特定的名称空间,因为这是默认名称空间——在production名称空间中有另一个名称相同的服务,如下所示:

# nginx in the default namespace

$ kubectl run nginx --image=nginx --port=80
deployment.apps/nginx created

$ kubectl expose deploy/nginx
service/nginx exposed

# nginx in the production namespace

$ kubectl create namespace production
namespace/production created

$ kubectl run nginx --image=nginx --port=80 \
    -n production
deployment.apps/nginx created

$ kubectl expose deploy/nginx -n production
service/nginx exposed

结果是两个名为nginx的服务存在于不同的名称空间中:

$ kubectl get services --all-namespaces | grep nginx
NAMESPACE  NAME  TYPE      CLUSTER-IP      PORT(S)
default    nginx ClusterIP 10.39.243.143   80/TCP
production nginx ClusterIP 10.39.244.112   80/TCP

我们是在10.39.243.143还是10.39.244.112发布 Nginx 服务将取决于请求 Pod 运行的名称空间:

$ kubectl run test --rm -ti --image=alpine \
    --restart=Never \
    -- getent hosts nginx | awk '{ print $1 }'
10.39.243.143

$ kubectl run test --rm -ti --image=alpine \
    --restart=Never \
    -n production \
    -- getent hosts nginx | awk '{ print $1 }'
10.39.244.112

当使用nginx作为主机时,default空间中的单元将连接到10.39.243.143,而production名称空间中的单元将连接到10.39.244.112。从production到达default ClusterIP 的方法是使用完整的域名,反之亦然。

默认配置使用service-name.namespace.svc.cluster.local约定,其中service-namenginx,在我们的示例中namespacedefaultproduction:

$ kubectl run test --rm -ti --image=alpine \
    --restart=Never \
    -- sh -c \
    "getent hosts nginx.default.svc.cluster.local; \
    getent hosts nginx.production.svc.cluster.local"
10.39.243.143     nginx.default.svc.cluster.local
10.39.244.112     nginx.production.svc.cluster.local

在不同的端口上公开服务

命令及其等价的声明形式将自省目标对象,并在其声明的端口上公开它。如果没有可用的端口信息,那么我们使用--port标志或service.spec.ports.port属性来指定端口。在我们前面的例子中,暴露的端口总是与实际的 Pod 的端口一致;每当公开的端口不同于发布的端口时,必须使用服务清单中的--target-port标志或service.spec.ports.targetPort属性来指定。

在下一个例子中,我们像往常一样在端口 80 上创建一个 Nginx 部署,但是在公共负载均衡器的端口8000上公开它。请注意,鉴于暴露端口和发布端口不同,我们必须使用--target-port标志指定暴露端口:

$ kubectl run nginx --image=nginx
deployment.apps/nginx created

$ kubectl expose deploy/nginx --port=8000 \
    --target-port=80 \
    --type=LoadBalancer
service/nginx exposed

$ kubectl get services -w
NAME  TYPE         EXTERNAL-IP   PORT(S)
nginx LoadBalancer <pending>     8000:31937/TCP
nginx LoadBalancer 35.189.65.99  8000:31937/TCP

结果是 Nginx 现在可以通过端口 8000 在公共互联网上访问,即使它是在 Pod 级别的端口 80 上公开的:

$ curl -s -i http://35.189.65.99:8000 | grep title
<title>Welcome to nginx!</title>

为了完整起见,这里我们给出了所呈现的kubectl expose命令的声明性等价物;使用kubectl apply -f serviceLoadBalancerMapped.yaml命令应用。我们可能需要首先通过运行kubectl delete service/nginx来删除使用命令式方法创建的服务:

# serviceLoadBalancerMapped.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 8000
    targetPort: 80
  type: LoadBalancer

暴露多个端口

一个 Pod 可以公开多个端口,因为它包含多个容器,或者因为一个容器监听多个端口。例如,web 服务器通常在端口 80 上监听常规的未加密流量,在端口 443 上监听 TLS 流量。服务清单中的spec.ports属性需要一个端口声明数组,所以我们所要做的就是将更多的元素添加到这个数组中,记住无论何时定义了两个或更多的端口,都必须给每个端口一个惟一的名称,这样它们才能被区分开来:

# serviceMultiplePorts.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  selector:
    run: nginx
  ports:
  - name: http  # user-defined name
    protocol: TCP
    port: 80
    targetPort: 80

  - name: https # user-defined name
    protocol: TCP
    port: 443
    targetPort: 443
  type: LoadBalancer

金丝雀释放

canary 发布背后的想法是,我们只向一部分用户公开服务的新版本,然后再向整个用户群推广,这样我们可以观察新服务的行为一段时间,直到我们确信它不存在运行时缺陷。

实现该策略的一个简单方法是创建一个服务对象,在 canary 发布期间,该服务对象在其负载均衡集群中包含一个新的 Pod“金丝雀”。例如,假设生产群集在其当前版本中包括 Pod 版本 1.0 的三个副本,我们可以包括 Pod 版本 2.0 的一个实例,以便 1/4 的流量(平均)到达新的 Pod。

这个策略的关键成分是标签和选择器,我们已经在第二章中介绍过了。我们所要做的就是为将要投入生产的 pod 添加一个标签,并在服务对象中添加一个匹配的选择器。这样,我们可以预先创建服务对象,并让 pod 声明一个标签,使它们被服务对象自动选择。这在行动中比在言语中更容易看到;让我们一步一步地遵循这个过程。

我们首先创建一个服务清单,其选择器将寻找标签prod等于true的 pod:

# myservice.yaml
kind: Service
apiVersion: v1
metadata:
  name: myservice
spec:
  selector:
    prod: "true"
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer

应用清单后,我们可以保持第二个窗口打开,在该窗口中,我们将交互地查看哪些端点加入和离开集群:

$ kubectl apply -f myservice.yaml
service/myservice created


$ kubectl get endpoints/myservice -w
NAME        ENDPOINTS   AGE
myservice   <none>      29m

假设我们还没有创建任何 Pods,集群中没有端点,这可以从ENDPOINTS列下的值<none>得到证明。让我们创建由三个副本组成的“现任”v1 生产部署:

$ kubectl run v1 --image=nginx --port=80 \
    --replicas=3 --labels="prod=true"
deployment.apps/v1 created

如果我们检查让kubectl get endpoints/myservice -w运行的终端窗口,我们会注意到将添加三个新的端点。例如:

$ kubectl get endpoints/myservice -w
NAME      ENDPOINTS
myservice 10.36.2.10:80
myservice <none>
myservice 10.36.2.11:80
myservice 10.36.0.6:80,10.36.2.11:80
myservice 10.36.0.6:80,10.36.1.8:80,10.36.2.11:80

由于我们已经请求了一个外部 IP,我们可以使用curl来检查我们的 v1 服务是否可操作:

$ kubectl get service/myservice
NAME      TYPE         EXTERNAL-IP   PORT(S)
myservice LoadBalancer 35.197.192.45 80:30385/TCP

$ curl -I -s http://35.197.192.45 | grep Server
Server: nginx/1.13.8

现在是时候介绍金丝雀 POD 了。让我们创建一个 v2 部署。不同之处在于标签prod被设置为false,并且我们将使用 Apache 服务器而不是 Nginx 作为新版本的容器映像:

$ kubectl run v2 --image=httpd --port=80 \
    --replicas=3 --labels="prod=false"
deployment.apps/v2 created

截至目前,我们可以看到总共有六个 Pod 副本。-L <LABEL>显示标签的值:

$ kubectl get pods -L prod
NAME                  READY  STATUS   RESTARTS  PROD
v1-3781799777-219m3   1/1    Running  0         true
v1-3781799777-qc29z   1/1    Running  0         true
v1-3781799777-tbj4f   1/1    Running  0         true
v2-3597628489-2kl05   1/1    Running  0         false
v2-3597628489-p8jcv   1/1    Running  0         false
v2-3597628489-zc95w   1/1    Running  0         false

为了让 v2 Pods 之一进入myservice集群,我们所要做的就是相应地设置标签。这里我们选择名为v2-3597628489-2kl05的 Pod,并将其prod标签设置为true:

$ kubectl label pod/v2-3597628489-2kl05 \
    prod=true --overwrite
pod "v2-3597628489-2kl05" labeled

在标签操作之后,如果我们检查运行命令kubectl get endpoints/myservice -w的窗口,我们将看到一个额外的端点被添加。此时,如果我们反复点击公共 IP 地址,我们会注意到一些请求到达了 Apache web 服务器:

$ while true ; do curl -I -s http://35.197.192.45 \
    | grep Server ; done
Server: nginx/1.13.8
Server: nginx/1.13.8
Server: nginx/1.13.8
Server: nginx/1.13.8
Server: Apache/2.4.29 (Unix)
Server: nginx/1.13.8
Server: nginx/1.13.8
...

一旦我们对 v2 的行为感到满意,我们就可以将 v2 的其余部分投入生产。如前所示,这可以通过贴标签逐步实现;然而,此时,最好创建一个正式的部署清单,这样一旦应用,Kubernetes 就会以无缝的方式引入 v2 Pods 并淘汰 v2 Pods 请参考第三章了解有关滚动和蓝/绿部署的更多详细信息。

总结这一节,标签和选择器的组合为我们提供了向服务消费者公开哪些 pod 的灵活性。一个实际应用是金丝雀释放的仪器。

Canary 版本和不一致的版本

canary 版本可能包括内部代码增强或错误修复,但也可能向用户引入新功能。这样的新特征可能涉及视觉用户界面本身(例如,HTML 和 CSS)以及支持这样的界面的数据 API。每当是后者的情况时,每个请求可能落在任何随机 Pod 上的事实可能是有问题的。例如,第一个请求可以检索依赖于 v2 REST API 的新的 v2 AngularJS 代码,但是当第二个请求命中负载均衡器时,所选择的 Pod 可以是 v1,并且提供这种所述 API 的不正确版本。本质上,当 canary 版本引入外部变化时——无论是 UI 还是数据方面——我们通常希望用户保持相同的版本,无论是当前版本还是 canary 版本。

用户登陆同一个服务实例的技术术语是粘性会话会话相似性——后者是 Kubernetes 使用的。根据有多少数据可用于识别单个用户,有无数种实现会话相似性的方法。例如,附加到 URL 上的 cookies 或会话标识符可以用在 web 应用的场景中,但是如果接口是协议缓冲区或 Thrift 而不是 HTTP 呢?唯一能够从另一个用户中识别出一个给定用户的细节是他们的客户端 IP 地址,这正是服务对象可以用来实现这个行为的。

默认情况下,会话关联性是禁用的。在命令式上下文中实现会话亲缘关系只是简单地将--session-affinity=ClientIP标志添加到kubectl expose命令中。例如:

# Assume there is a Nginx Deployment running
$ kubectl expose deploy/nginx --type=LoadBalancer \
    --session-affinity=ClientIP
service "nginx" exposed

声明性版本包括设置service.spec.sessionAffinity属性和应用运行kubectl apply -f serviceSessionAffinity.yaml命令的清单:

# serviceSessionAffinity.yaml
kind: Service
apiVersion: v1
metadata:
  name: nginx
spec:
  sessionAffinity: ClientIP
  selector:
    run: nginx
  ports:
  - protocol: TCP
    port: 80
  type: LoadBalancer

基于 IP 的会话相似性的限制是多个用户可能共享同一个 IP 地址,这在公司和学校中是典型的情况。同样,同一个逻辑用户可能看起来来自多个 IP 地址,就像用户在家中使用他们的 Wi-Fi 宽带路由器并通过他们的智能手机使用 LTE 或类似技术观看网飞的情况一样。对于这样的场景,服务的能力是不够的;因此,最好使用具有第 7 层自检能力的服务,如入口控制器。更多信息请参考 https://kubernetes.io/docs/concepts/services-networking/ingress/

正常启动和关闭

受益于宽限启动属性的应用仅在其自举过程完成后才准备好接受用户请求,而宽限关闭意味着应用不应突然停止,从而破坏其用户或其依赖关系的完整性。换句话说,应用应该能够告诉“我已经喝了咖啡,洗了澡,我准备好工作了”以及“我今天到此为止了”。我现在要去刷牙睡觉了。”

服务控制器使用两种主要机制来决定给定的 Pod 是否应该成为其集群的一部分:Pod 的标签和 Pod 的就绪状态,这是使用就绪探测器实现的(参见第二章)。例如,在基于 Spring 的 Java 应用中,从应用启动到 Spring Boot 框架完全初始化并准备好接受 http 请求之间有几秒钟的延迟。

零停机部署

Kubernetes 的部署控制器通过向服务控制器注册新的 Pod 并以协调的方式删除旧的 Pod 来实现零停机部署,以便最终用户始终获得最少数量的 Pod 副本。如第三章所述,零停机部署可以实施为滚动部署或蓝/绿部署。

只需几个步骤就能看到零停机部署的实际效果。首先,我们创建一个常规的 Nginx 部署对象,并通过公共负载均衡器公开它。我们设置了--session-affinity=ClientIP,以便消费客户端在准备就绪后可以无缝过渡到新的升级 Pod:

$ kubectl run site --image=nginx --replicas=3
deployment.apps/site created

$ kubectl expose deploy/site \
    --port=80 --session-affinity=ClientIP \
    --type=LoadBalancer
service/site exposed

# Confirm public IP address
$ kubectl get services -w
NAME    TYPE          EXTERNAL-IP      PORT(S)
site    LoadBalancer  35.197.210.194   80:30534/TCP

然后,我们打开一个单独的终端窗口,让一个简单的 http 客户端在无限循环中运行:

$ while true ; do curl -s -I http://35.197.210.194/ \
    | grep Server; sleep 1 ; done
Server: nginx/1.17.1
Server: nginx/1.17.1
Server: nginx/1.17.1
...

现在我们所要做的就是将deploy/site转换到一个新的部署,这可以通过简单地改变它的底层容器映像来实现。让我们使用来自 Docker Hub 的 Apache HTTPD 映像:

$ kubectl set image deploy/site site=httpd
deployment.extensions/site image updated

如果我们返回到运行示例客户端的窗口,我们将看到很快迎接我们的将是 Apache HTTPD 服务器,而不是 Nginx:

Server: nginx/1.17.1
Server: nginx/1.17.1
Server: nginx/1.17.1
Server: Apache/2.4.39 (Unix)
Server: Apache/2.4.39 (Unix)
Server: Apache/2.4.39 (Unix)
...

使用kubectl get pod -w命令打开另一个平行窗口来监视 Pod 活动也很有趣,这样我们就可以观察新的 Pod 是如何启动的,旧的 Pod 是如何被终止的。

pod 的端点

在大多数情况下,服务控制器的角色是为两个或更多 pod 提供单个端点。但是,在某些情况下,我们可能希望确定服务控制器选择的那些 pod 的特定端点。

kubectl get endpoints/<SERVICE-NAME>命令直接在屏幕上显示多达三个端点,用于即时调试。例如:

$ kubectl get endpoints/nginx -o wide
NAME      ENDPOINTS
nginx     10.4.0.6:80,10.4.1.6:80,10.4.2.6:80

和我们之前看到的一样,可以使用kubectl describe service/<SERVICE-NAME>命令检索相同的信息。如果我们想要一种更程序化的方法,允许我们对端点的数量和值的变化做出反应,我们可以使用 JSONPath 查询。例如:

$ kubectl get endpoints/nginx -o jsonpath \
    --template= "{.subsets[*].addresses[*].ip}"
10.4.0.6 10.4.1.6 10.4.2.6

管理摘要

正如我们已经看到的,服务可以使用kubectl expose命令强制创建,也可以使用清单文件声明创建。使用kubectl get services命令列出服务,使用kubectl delete service/<SERVICE-NAME>命令删除服务。例如:

$ kubectl get services
NAME        TYPE         CLUSTER-IP   EXTERNAL-IP
kubernetes  ClusterIP    10.7.240.1   <none>
nginx       LoadBalancer 10.7.241.102 35.186.156.253

$ kubectl delete services/nginx
service "nginx" deleted

请注意,当服务被删除时,底层部署仍将继续运行,因为它们都有独立的生命周期。

摘要

在本章中,我们了解到服务控制器有助于在服务消费者和 pod 之间创建一个间接层,以便于工具化属性,如自修复、金丝雀释放、负载均衡、正常启动和关闭以及零停机部署。

我们讨论了具体的连接用例,如 Pod 到 Pod、LAN 到 Pod 和 Internet 到 Pod,并看到后者特别有用,因为它允许使用公共 IP 地址访问我们的应用。

我们还解释了如何使用完整的 DNS 记录来消除跨不同名称空间的冲突服务,以及在声明多个端口时命名端口的需要。

五、配置图和机密

云原生应用的一个关键原则是外部化配置。在十二因素应用方法中,这种架构属性最好由因素 III 来描述。这一因素中的一段相关文字,在 https://12factor.net/config 中写道:

十二因素应用将配置存储在环境变量中(通常简称为 env vars 或 env)。Env 变量很容易在部署之间改变,而不需要改变任何代码;与配置文件不同,它们被意外签入代码仓库的可能性很小;与定制配置文件或其他配置机制(如 Java 系统属性)不同,它们是与语言和操作系统无关的标准。

Pods 中的容器通常运行常规的 Linux 发行版——比如 Alpine——这意味着 Kubernetes 可以假设 shell 和环境变量的存在,这与对操作系统不可知的低级虚拟化平台不同。

正如 factor III 的 12-Factor App 段落所建议的,几乎所有的编程语言都可以访问环境变量,因此这无疑是一种将配置细节传递给应用的通用和可移植的方法。Kubernetes 并不局限于简单地填充环境变量;它还可以通过虚拟文件系统提供配置。它还有其他一些技巧,比如从文件中解析键/值对和混淆敏感数据的能力。

这一章分为两大部分。第一部分包括以手动方式设置环境变量,然后通过各种方法使用 ConfigMap 对象自动填充它们:文字值、清单中的硬编码值以及从文件加载的数据。还特别关注以纯文本和二进制形式存储复杂的配置数据,以及使用虚拟文件系统公开配置变量,这有助于实时配置更新。

第二部分重点介绍 Secrets,这是 ConfigMap 的姐妹功能:它支持几乎所有与 ConfigMap 相同的功能,只是它更适合于密码和其他类型的敏感数据。在 Secrets 中,还处理了 Docker 注册中心凭证的特殊情况(从需要身份验证的 Docker 注册中心提取映像时需要),以及 TLS 证书和密钥的存储。

手动设置环境变量

在命令式场景中,可以通过将--env=<NAME>=<VALUE>标志添加到kubectl run命令来定义环境变量。比如说我们有一个变量叫做mysql_host,另一个叫做ldap_host,它们的值分别是mysql1.company.comldap1.company.com;我们将以这种方式应用这些属性:

$ kubectl run my-pod --rm -i --image=alpine \
    --restart=Never \
    --env=mysql_host=mysql1.company.com \
    --env=ldap_host=ldap1.company.com \
    -- printenv | grep host
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

注意,在最后一个参数中,我们运行了printenv命令,并对包含关键字host的变量进行了 grep 处理;结果包括我们使用--env=<NAME>=<VALUE>标志传递的两个变量。

声明性版本要求我们在 Pod 清单中设置pod.spec.containers.env属性:

# podHardCodedEnv.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","printenv | grep host"]
      env:
        - name: mysql_host
          value: mysql1.company.com
        - name: ldap_host
          value: ldap1.company.com

运行命令kubectl apply -f podHardCodedEnv.yaml时,不会有任何终端输出。这是因为在清单的args属性中声明的printenvgrep命令将成功退出,而apply命令不会打印出 Pod 的标准输出;因此,我们需要查阅它的日志:

$ kubectl logs pod/my-pod
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

即使我们已经成功地在给出的例子中具体化了mysql_hostldap_host,我们仍然有一个相对不灵活的系统,其中需要为每个新的环境配置产生新版本的 Pod 清单。只有当环境变量是适用于所有环境的常量时,才适合在 Pod 清单中声明环境变量。在下一节中,我们将学习如何使用 ConfigMap 对象将配置从 Pod 清单中分离出来。

在 Kubernetes 中存储配置属性

Kubernetes 提供了 ConfigMap 对象,用于存储全局配置属性,这些属性与单个工作负载(如 monolithic Pods、部署、作业等)的详细信息(即配置清单)无关。基本配置映射对象由顶级名称(配置映射“名称”本身)和一组键/值对组成。通常,新的 ConfigMap 名称用于每组密切相关的属性。同样,给定的配置图可以适用于多个应用;例如,Java 和. NET 容器化应用可以使用相同的 MySQL 凭证。这种方法有助于集中管理通用配置设置。

通过使用kubectl create configmap <NAME>命令并添加与我们拥有的属性数量一样多的--from-literal=<KEY>=<VALUE>标志,以命令的方式创建一个配置映射。例如,以下命令创建了一个名为data-sources的配置映射,具有mysql_host=mysql1.company.comldap_host=ldap1.company.com属性:

$ kubectl create configmap data-sources \
    --from-literal=mysql_host=mysql1.company.com \
    --from-literal=ldap_host=ldap1.company.com
configmap/data-sources created

这个命令相当于下面显示的声明性版本。使用kubectl apply -f simpleconfigmap.yaml命令应用:

# simpleconfigmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: data-sources
data:
  mysql_host: mysql1.company.com
  ldap_host: ldap1.company.com

现在我们已经创建了一个 ConfigMap 对象,我们可以使用kubectl describe命令快速检查它的状态:

$ kubectl describe configmap/data-sources
...
Data
====
ldap_host:
----
ldap1.company.com
mysql_host:
----
mysql1.company.com

至此,我们已经成功地使用 ConfigMap 对象存储了表示为键/值对的配置数据,但是我们如何将值提取回来呢?编程方法包括使用 JSONPath 查询并将结果存储在环境变量中:

$ mysql_host=$(kubectl get configmap/data-sources \
    -o jsonpath --template="{.data.mysql_host}")
$ ldap_host=$(kubectl get configmap/data-sources \
    -o jsonpath --template="{.data.ldap_host}")

然后,我们可以使用分配的变量将配置值传递给 Pod,如下所示:

$ kubectl run my-pod --rm -i --image=alpine \
    --restart=Never \
    --env=mysql_host=$mysql_host \
    --env=ldap_host=$ldap_host \
    -- printenv | grep host
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

尽管这种方法是可行的——因为它将配置从 Pod 的清单中分离出来——但它要求我们每次运行接受配置设置的 Kubernetes 对象时都要查询 ConfigMap 相关对象。在下一节中,我们将看到如何使这个过程更有效。

自动应用配置

可以让 Pods 的容器知道它们的环境变量(和值)可以在现有的配置图中找到,这样就不需要逐个手动指定环境变量。通过使用podWithConfigMapReference.yaml清单中所示的pod.spec.containers.envFrom.configMapRef.name属性,可以在 Pod 清单中指定所需的配置映射:

# podWithConfigMapReference.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","printenv | grep host"]
      envFrom:
      - configMapRef:
          name: data-sources

要运行这个 Pod 清单,从零开始——假设我们事先运行了kubectl delete all --all—我们将首先通过键入kubectl apply -f simpleconfigmap.yaml来设置一个配置映射,然后使用kubectl apply -f podWithConfigMapReference.yaml简单地运行 Pod,而不需要直接与配置映射交互:

$ kubectl apply -f simpleconfigmap.yaml
configmap/data-sources created

$ kubectl apply -f podWithConfigMapReference.yaml
pod/my-pod created

可以通过检查my-pod的日志来观察结果:

$ kubectl logs pod/my-pod
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

这可能是让 Pod 的容器从 ConfigMap 中自动检索配置数据的最简单的方法,但是它有副作用,即它是不加选择的;它将设置 ConfigMap 中声明的所有环境键和值,不管它们是否相关。

让我们假设有一个名为secret_host的特殊键,其值为hushhush.company.com:

# selectiveProperties.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: data-sources
data:
  mysql_host: mysql1.company.com
  ldap_host: ldap1.company.com
  secret_host: hushhush.company.com

现在我们想以这样一种方式再次定义my-pod,它只从data-sources配置图中检索mysql_hostldap_host,而不检索 secret_host。在这种情况下,我们使用类似于适用于硬编码值的语法;我们在pod.spec.containers.env下创建一个项目数组,并使用name命名键,而不是使用value硬编码值,我们通过创建一个valueFrom对象来引用 ConfigMap 的适用键,如下所示:

...
spec:
  containers:
    - name: ...
      env:
        - name: mysql_host
          valueFrom:
            configMapKeyRef:
              name: data-sources
              key: mysql_host

再次注意,在这个代码片段中,我们使用了valueFrom而不是value,并且我们在下面设置了一个由两个属性组成的configMapKeyRef对象:name引用配置图,key引用所需的键。

名为podManifest.yaml的完整的最终 Pod 清单如下所示:

# podManifest.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","printenv | grep host"]
      env:
        - name: mysql_host
          valueFrom:
            configMapKeyRef:
              name: data-sources
              key: mysql_host
        - name: ldap_host
          valueFrom:
            configMapKeyRef:
              name: data-sources
              key: ldap_host

没有对secret_host,的显式引用,因此将只设置mysql_hostldap_host的值。假设我们已经定义了新版本的data-sources配置图,我们将首先通过键入kubectl delete all --all来清理环境,应用新的配置图,然后再次运行my-pod:

$ kubectl apply -f selectiveProperties.yaml
configmap/data-sources configured

$ kubectl apply -f podManifest.yaml
pod/my-pod created

不出所料,在检查my-pod的日志时,没有从配置图中提取出secret_host键:

$ kubectl logs pod/my-pod
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

将配置图的值传递给 Pod 的启动参数

有时,我们希望直接使用 ConfigMap 数据作为命令参数,而不是让容器化的应用显式地检查环境变量,就像我们在前面几节中使用printenv所做的那样。要实现这一点,首先我们必须使用pod.spec.containers.envpod.spec.containers.envFrom属性将所需的配置映射数据分配给环境变量,如前几节所述。一旦设置完成,我们可以使用$(ENV_VARIABLE_KEY)语法在 Pod 清单中的任何地方引用这些变量。

例如,假设我们想要创建一个 Pod,它的唯一目的是使用echo命令问候mysql_host。为了实现这个需求,我们通过引用$(mysql_host)变量查询语句来引用命令参数中的mysql_host变量:

# podManifestWithArgVariables.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args:
      - /bin/sh
      - -c
      - echo Hello $(mysql_host)
      envFrom:
      - configMapRef:
          name: data-sources

在应用此 Pod 清单之前,我们必须确保删除任何其他正在运行的 Pod——通过运行kubectl delete pod --all,但保留名为data-sources的配置图:

$ kubectl apply -f podManifestWithArgVariables.yaml
pod/my-pod created

$ kubectl logs my-pod
Hello mysql1.company.com

正如所料,变量$(mysql_host)被解析为它的值mysql1.company.com

从文件加载配置图的属性

到目前为止,我们已经看到了如何将键/值对直接定义为kubectl create configmap命令的标志或者在 ConfigMap 的清单中定义。通常情况下,配置文件存储在外部系统中,如 Git 如果是这种情况,我们不希望依赖 shell 脚本来解析和转换这样的文件。幸运的是,我们可以将文件直接导入到 ConfigMap 中,额外的好处是能够使用简单的<KEY>=<VALUE>语法来表达键/值对,类似于 Java 的.properties和微软/Python 的.ini文件类型。让我们考虑名为data-sources.properties的示例文件:

# data-sources.properties
mysql_host=mysql1.company.com
ldap_host=ldap1.company.com

一旦这个文件被保存为data-sources.properties,当运行kubectl create configmap命令时,我们可以通过添加--from-env=<FILE-NAME>标志来引用它。例如:

$ kubectl create configmap data-sources \
    --from-env-file=data-sources.properties
configmap/data-sources created

请注意,由于这是一个create命令,而不是一个apply命令,我们可能首先需要通过发出kubectl delete configmap/data-sources命令删除任何先前声明的同名配置图对象。

在配置图中存储大文件

是否有些应用只需要一组简单的键/值对进行配置,有些应用可能会使用 XML、YAML 或 JSON 格式的大型文档。一个很好的例子是基于 XML 的配置文件,Spring 框架用它来定义 Java 应用中的“bean”——在 Spring Boot 出现之前,它主要依赖于注释而不是庞大的外部配置文件。

ConfigMap 服务不仅限于存储简单的键/值对;键值可以是包含换行符的长文本文档。长文本文档可以在常规 ConfigMap 本身中定义,也可以作为外部文件引用。

例如,假设我们需要在数据源配置旁边包含一个 XML 格式的地址记录。这样的记录将包含在名为configMapLongText.yaml的配置映射清单中,如下所示:

# configMapLongText.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: data-sources
data:
  mysql_host: mysql1.company.com
  ldap_host: ldap1.company.com
  address: |
    <address>
       <number>88</number>
       <street>Wood Street</street>
       <city>London</city>
       <postcode>EC2V 7RS</postcode>
    </address>

让我们应用清单:

$ kubectl apply -f configMapLongText.yaml
configmap/data-sources created

现在我们可以使用kubectl describe configmap/data-sources来确认基于 XML 的address值已经被有效地存储——除了mysql_hostldap_host:

$ kubectl describe configmap/data-sources
...
Data
====
address:
----
<address>
   <number>88</number>
   <street>Wood Street</street>
   <city>London</city>
   <postcode>EC2V 7RS</postcode>
</address>
...

另一种更灵活的方法是引用外部文件。让我们假设地址存储在名为address.xml的文件中:

<address>
  <number>88</number>
  <street>Wood Street</street>
  <city>London</city>
  <postcode>EC2V 7RS</postcode>
</address>

要引用这个文件,我们只需在使用kubectl create configmap命令时添加--from-file=<FILE>标志。例如:

$ kubectl create configmap data-sources \
    --from-file=address.xml
configmap "data-sources" created

这相当于之前看到的方法,除了这样一个事实,即address值的键现在已经变成了文件名本身(address.xml),正如我们在用kubectl describe命令检查结果时看到的:

$ kubectl describe configmap/data-sources
...
Data
====
address.xml:
----
<address>
  <number>88</number>
  <street>Wood Street</street>
  <city>London</city>
  <postcode>EC2V 7RS</postcode>
</address>

值得考虑的是,文件路径将被转换成不包括父文件夹的键。例如,/tmp/address.xml/home/ernie/address.xml都将被转换成一个名为address.xml的键。如果两者都通过单独的--from-file指令被引用,将会报告一个键冲突。

还要注意,为了简洁起见,我们没有为mysql_hostldap_host应用文字值。如果我们想要一个与声明形式完全等价的命令,我们应该添加几个--from-literal标志来包含那些属性。

到目前为止,我们已经学习了如何存储长文本文件,但是如何检索内容以便 Pods 可以使用呢?环境变量不方便,而且最初也不打算存储多行换行的长文本块。然而,我们仍然可以通过使用-e标志让echo解析换行符来检索多行文本。例如,假设我们已经应用了上一个示例中的地址 XML 文件,我们可以按如下方式检索它:

# Assuming we are inside a Pod's container
$ echo -e $address > /tmp/address.xml
$ cat /tmp/address.xml
<address>
   <number>88</number>
   <street>Wood Street</street>
   <city>London</city>
   <postcode>EC2V 7RS</postcode>
</address>

尽管这个技巧允许我们检索相对简单的 XML 文档,但是用多行文本污染 Pod 的环境变量集是不可取的,而且它还有一个主要的限制:数据是不可变的,一旦创建了 Pod,就不能再更改。在下一节中,我们将探索一种更方便的方法来将长文本文档放入 Pod 的容器中。

实时配置图更新

配置映射通常以声明的形式定义。kubectl createkubectl apply的区别在于,后者刷新(覆盖)现有匹配实例的状态——如果有的话。每当我们使用kubectl apply -f <FILE>,应用新的配置图时,我们有效地更新任何匹配的配置图实例及其配置键/值对,但是这并不意味着绑定到刷新的配置图的 pod 得到更新。这是因为到目前为止,我们看到的传播配置数据的方法是通过只设置一次的环境变量——在创建 Pod 的容器时。

通过环境变量(和/或命令参数)使配置图数据对 pod 可用有两个限制。首先,对于多行的长文本来说,这很不方便。第二个,也是最基本的一个,就是环境变量一旦被设置,就不能再被更新,除非为了重启底层的 Linux 进程而重启 Pod。Kubernetes 有一个解决方案,一举解决了这两个问题;它可以使 ConfigMap 属性作为文件在 Pod 的容器内可用,以便键作为文件名出现,值作为它们的内容出现,此外,每当更新底层 ConfigMap 时,它将刷新所述文件。ConfigMap 对象确实允许我们鱼和熊掌兼得。

让我们看看这个“配置为文件”的解决方案是如何工作的。首先,让我们再次考虑我们的data-sources ConfigMap 的清单,它包括一个名为 address 的多行属性:

# configMapLongText.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: data-sources
data:
  mysql_host: mysql1.company.com
  ldap_host: ldap1.company.com
  address: |
    <address>
       <number>88</number>
       <street>Wood Street</street>
       <city>London</city>
       <postcode>EC2V 7RS</postcode>
    </address>

data-sources配置图作为文件系统提供给 Pod 包括两个步骤。首先,我们必须在 Pod 清单中的pod.spec.volumes下定义一个卷名。在此属性下,我们指定卷的名称,在我们的示例中为my-volume,以及所述卷将链接到的配置图的名称data-sources:

...
volumes:
  - name: my-volume
    configMap:
      name: data-sources

第二步是使用我们在pod.spec.containers.volumeMounts下选择的名称(my-volume)来引用卷,从而挂载它。我们还必须指定希望卷挂载到的路径:

...
volumeMounts:
  - name: my-volume
    mountPath: /var/config
...

在我们将卷定义和卷挂载合并到最终的 Pod 清单中之前,我们还想包含一个脚本,通过发出一个ls -l /var/config命令来检查结果目录和文件结构。我们还想通过发出cat /var/config/address命令来查看特定键address的内容。

我们还说过,每当底层配置图更新时,文件都会自动刷新;我们可以通过使用inotifywait命令监视/var/config/address的变化来观察这种行为。结果脚本如下所示:

apk update;
apk add inotify-tools;
ls -l /var/config;
while true;
do cat /var/config/address;
   inotifywait -q -e modify /var/config/address;
done

该脚本以如下方式工作:前两个apk命令安装inotifywait,它是inotify-tools包的一部分,然后它显示在/var/config中找到的文件,最后,它进入一个无限循环,当文件被修改时,它显示/var/config/address的内容。

生成的 Pod 清单称为podManifestVolume.yaml,包括提供的卷和卷装载声明以及前面看到的脚本,如下所示:

# podManifestVolume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args:
        - sh
        - -c
        - >
          apk update;
          apk add inotify-tools;
          ls -l /var/config;
          while true;
          do cat /var/config/address;
          inotifywait -q
          -e modify /var/config/address;
          done
      volumeMounts:
        - name: my-volume
          mountPath: /var/config
  volumes:
    - name: my-volume
      configMap:
        name: data-sources

我们现在将应用configMapLongText.yaml(配置映射)和podManifestVolume.yaml(之前定义的清单):

$ kubectl apply -f configMapLongText.yaml -f podManifestVolume.yaml
configmap/data-sources created
pod/my-pod created

通过查看my-pod的日志显示ls -la /var/configcat /var/config/address的结果;

$ kubectl logs -f pod/my-pod
...
lrwxrwxrwx 1 14 Jul 8 address -> ..data/address
lrwxrwxrwx 1 16 Jul 8 ldap_host -> ..data/ldap_host
lrwxrwxrwx 1 17 Jul 8 mysql_host -> ..data/mysql_host
<address>
   <number>88</number>
   <street>Wood Street</street>
   <city>London</city>
   <postcode>EC2V 7RS</postcode>
</address>

让我们检查结果输出。ls -l /var/config命令显示每个 ConfigMap 键(addressldap_hostmysql_host)被表示为一个文件。第二个命令,cat /var/config/address,显示每个键的值现在已经成为文件的内容;在本例中,address包含一个 XML 文件。

我们现在可以观察到“配置为文件”特性对于传播配置更改是如何有用的。首先,我们将定义一个名为configMapLongText_changed.yamldata-sources的新版本,它包含一个address键的更改值:

# configMapLongText_changed.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: data-sources
data:
  mysql_host: mysql1.company.com
  ldap_host: ldap1.company.com
  address: |
    <address>
       <number>33</number>
       <street>Canada Square</street>
       <city>London</city>
       <postcode>E14 5LB</postcode>
    </address>

在应用该清单之前,我们必须确保我们离开了启动并行运行的kubectl logs -f pod/my-pod的窗口,并在一个新窗口中写入以下命令:

$ kubectl apply -f configMapLongText_changed.yaml
configmap/data-sources configured

几秒钟后,我们会注意到运行kubectl logs -f pod/my-pod的窗口显示了在configMapLongText_changed.yaml中声明的新地址:

...
<address>
   <number>33</number>
   <street>Canada Square</street>
   <city>London</city>
   <postcode>E14 5LB</postcode>
</address>

正如我们在本节中看到的,将配置作为文件提供的好处是允许包含长文本文件,并使正在运行的应用能够检测到新的配置更改。这并不意味着使用环境变量是一个低劣的解决方案。即使在配置细节不稳定的情况下,使用环境变量结合金丝雀测试方法仍然是一个好主意,在金丝雀测试方法中,只有一部分 pod(新的 pod)随着旧的 pod 逐渐退役而获得新的更改。

存储二进制数据

ConfigMap 对象被设计用来存储文本数据,因为我们通常使用一个文本友好的接口来检索它的内容,比如在 Linux shell 中的环境变量,以及分别使用kubectl get configmap/<NAME>命令和-o json-o yaml标志时的 JSON 和 YAML 输出。

因为在使用 volume 时,键值可以作为文件的内容出现,所以看起来也是一种存储 BLOB(二进制大对象)数据的实用方法,但是由于前面给出的原因,这里的直觉是误导性的。这个问题的解决方案是使用类似 base64 的 ASCII 编码机制来编码和解码我们感兴趣的任何二进制文件。例如,假设我们想要在名为binary的配置映射上存储名为logo.png的映像的内容,我们将发出以下两个命令:

$ base64 logo.png > /tmp/logo.base64
$ kubectl create configmap binary \
    --from-file=/tmp/logo.base64

然后,在 Pod 中,假设binary配置图安装在/var/config下,我们将获得原始映像,如下所示:

$ base64 -d /var/config/logo.base64 > /tmp/logo.png

自然,在 Python 或 Java 这样的编程语言中,我们更愿意使用本地库,而不是如本例所示的 shell 命令。还要注意,尽管 base64 提供了某种程度的模糊处理,但它不是一种加密形式。我们将在下一节进一步讨论这个话题。

秘密

ConfigMap 对象用于通常来自集中式 SCM 的明文、非敏感数据。对于密码和其他敏感数据,应该使用 Secret 对象。在大多数情况下,对于所有命令性和声明性用例,Secret 对象是 ConfigMap 的“插入式”替换(当以通用模式运行时),除了明文数据应该以 base64 编码并在通过环境变量和卷变得可用时自动解码这一事实。

与秘密对象相关联的安全能力正在不断改进。在撰写本文时,支持对静止秘密进行加密( https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/ ),并采取设计措施防止 pod 访问不打算与其共享的秘密。尽管如此,Secret 对象提供的安全级别不应该被认为适合于保护 Kubernetes 集群管理员的凭证,因为他们具有根访问权限。

我们现在将看看如何使用 Secret 对象存储敏感信息与使用 ConfigMap 对象存储敏感信息之间的主要区别。

配置映射和机密对象之间的差异

命令kubectl create secret generic <NAME>类似于kubectl create configmap <NAME>命令。就像它的 ConfigMap 对应物一样,它有三个标志:--from-literal用于就地值,--from-env-file用于包含多个键/值对的文件,--from-file用于大型数据文件。

我们现在将考虑上述每个用例,目的是存储mysql_user=erniemysql_pass=HushHush凭证。请注意,所有这三个版本都是等价的,并且使用相同的名称,所以如果我们在一个接一个地运行所有示例时出现类似于Error from server (AlreadyExists)的错误,我们必须运行kubectl delete secrets/my-secrets:

用例 1: --from-literal取值:

$ kubectl create secret generic my-secrets \
    --from-literal=mysql_user=ernie \
    --from-literal=mysql_pass=HushHush
secret/my-secrets created

用例 2: --from-env和一个名为mysql.properties的文件:

# secrets/mysql.properties
mysql_user=ernie
mysql_pass=HushHush
$ kubectl create secret generic my-secrets \
    --from-env-file=secrets/mysql.properties
secret/my-secrets created

用例三: --from-file:

$ echo -n ernie > mysql_user

$ echo -n HushHush > mysql_pass

$ kubectl create secret generic my-secrets \

    --from-file=mysql_user --from-file=mysql_pass
secret/my-secrets created

使用声明性清单,我们首先需要将值手动编码为 base64,如下所示:

$ echo -n ernie | base64
ZXJuaWU=

$ echo -n HushHush | base64
SHVzaEh1c2g=

然后,我们可以在清单中使用这些 base64 编码的值:

# secrets/secretManifest.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-secrets
data:
  mysql_user: ZXJuaWU=
  mysql_pass: SHVzaEh1c2g=

通常,当使用声明性清单而不是命令性形式时,我们只需使用kubectl apply -f <FILE>命令来应用清单文件:

$ kubectl apply -f secrets/secretManifest.yaml
secret/my-secrets unchanged

总的来说,我们已经看到了定义同一组凭证的四种不同方式;前三个使用适用于kubectl create secret generic命令的--from-literal--from-env-file--from-file标志,最后一个使用清单文件。在所有情况下,名为my-secrets的结果对象都是相同的——除了元数据信息和一些其他小细节:

$ kubectl get secret/my-secrets -o yaml
apiVersion: v1
data:
  mysql_pass: SHVzaEh1c2g=
  mysql_user: ZXJuaWU=
kind: Secret
...

kubectl describe命令也很有帮助,但是它不会显示 base64 值;只有它们的长度:

$ kubectl describe secret/my-secrets
...
Data
====
mysql_user:  5 bytes
mysql_pass:  8 bytes

从机密中读取属性

秘密属性在 Pod 清单中以环境变量或卷装载的形式提供,方式与 ConfigMap 类似。在大多数情况下,整体语法保持不变,除了我们使用关键字secretconfigMap本应适用的事实。

在下面的例子中,我们假设已经创建了my-secrets秘密,并且它包含了mysql_usermysql_pass密钥和值。

让我们从envForm方法开始,这是提取秘密的最简单的方法,因为它简单地使用pod.spec.containers.envFrom声明将秘密对象中声明的所有键/值对作为环境变量进行投影。这与我们在配置图的情况下所做的完全一样,除了我们必须用secretRef替换configMapRef:

# secrets/podManifestFromEnv.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","printenv | grep mysql"]
      envFrom:
      - secretRef:
          name: my-secrets

应用清单,然后检查 Pod 的日志揭示了秘密的价值。这表明 Kubernetes 在容器运行时内设置环境变量之前将值从 base64 解码回纯文本:

$ kubectl apply -f secrets/podManifestFromEnv.yaml
pod/my-pod created

$ kubectl logs pod/my-pod
mysql_user=ernie
mysql_pass=HushHush

另一种稍微更热情但更安全的方法是逐个指定环境变量并选择特定的属性。除了我们用secretKeyRef替换configMapKeyRef之外,这种方法与 ConfigMap 完全相同:

# secrets/podManifesSelectedEnvs.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","printenv | grep mysql"]
      env:
        - name: mysql_user
          valueFrom:
            secretKeyRef:
              name: my-secrets
              key: mysql_user
        - name: mysql_pass
          valueFrom:
            secretKeyRef:
              name: my-secrets
              key: mysql_pass

应用此清单的结果与前面的示例完全相同:

$ kubectl apply -f \
    secrets/podManifestSelectedEnvs.yaml
pod/my-pod created

$ kubectl logs pod/my-pod
mysql_user=ernie
mysql_pass=HushHush

现在让我们把注意力转向卷。同样,工作流程与配置映射对象的情况相同。我们首先必须在pod.spec.volumes下声明一个卷,然后在pod.spec.containers.volumeMounts将它挂载到一个给定的容器下。只有第一部分(卷的定义)与 ConfigMap 对象不同。这里有两处改动:configMap必须换成secretname必须换成secretName:

# secrets/podManifestVolume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: alpine
      image: alpine
      args: ["sh","-c","ls -l /var/config"]
      volumeMounts:
        - name: my-volume
          mountPath: /var/config
  volumes:
    - name: my-volume
      secret: # rather than configMap
        secretName: my-secrets # rather than name

一旦应用了清单,就可以在/var/config下找到秘密属性文件。我们保留了与上一个 ConfigMap 示例中相同的挂载点,因为我们想突出相似之处。

这次我们选择了一个简单的脚本,它在运行 Pod 时简单地列出了/var/config目录的内容:

$ kubectl apply -f secrets/podManifestVolume.yaml
pod/my-pod created

$ kubectl logs pod/my-pod
total 0
lrwxrwxrwx 1 17 Jul 8 mysql_pass -> ..data/mysql_pass
lrwxrwxrwx 1 17 Jul 8 mysql_user -> ..data/mysql_user

变更传播的属性仍然存在于 Secrets 中,并且以与 ConfigMaps 中相同的方式工作。为了简洁起见,我们在这里不重复这个例子。

Docker 注册表凭据

到目前为止,我们已经看到了 Secret 对象使用所谓的通用模式。Docker 注册表凭证的处理是其特意设计的扩展之一,有助于提取 Docker 映像,而不需要在 Pod 清单中显式指定凭证,并且缺乏安全性。

在本文的例子中,比如那些涉及 Alpine 或 Nginx 映像的例子,我们一直在处理公共 Docker 注册中心(比如 Docker Hub),所以不需要关心凭证。然而,每当需要私有 Docker 存储库时,我们需要提供正确的用户名、密码和电子邮件地址,然后才能提取映像。

这个过程相当简单,因为它只涉及创建一个保存 Docker 注册中心凭证的秘密对象,我们只需要在适用的 Pod 清单中引用该对象。

使用kubectl create docker-registry <NAME>命令以及每个凭证组件的特定标志创建 Docker 注册表机密:

  • --docker-server=<HOST>:服务器的主机

  • --docker-username=<USER_ID>:用户名

  • --docker-password=<USER_PASS>:密码,未加密

  • --docker-email=<USER_EMAIL>:用户的电子邮件地址

以下是为 Docker Hub 的名为docker-hub-secret的私有存储库创建秘密的示例:

$ kubectl create secret docker-registry docker-hub-secret \
    --docker-server=docker.io \
    --docker-username=egarbarino \
    --docker-password=HushHush \
    --docker-email=antispam@garba.org
secret/docker-hub-secret created

现在,我们可以在pod.spec.imagePullSecrets.name下的 Pod 清单中引用docker-hub-secret。在下一个例子中,我们引用存储在位于docker.io/egarbarino/hello-image的 Docker Hub 中的映像:

# secrets-docker/podFromPrivate.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-app
spec:
  containers:
    - name: hello-app
      image: docker.io/egarbarino/hello-image
  imagePullSecrets:
    - name: docker-hub-secret
$ kubectl apply -f secrets-docker/podFromPrivate.yaml
pod/hello-app created

Docker 镜像是一个监听端口 80 的 Flask (Python)应用。如果 Docker 凭证成功,我们应该能够通过在本地主机和hello-app Pod 之间建立隧道来连接到它:

$ kubectl port-forward pod/hello-app 8888:80 \
    > /dev/null &
[1] 5052

$ curl http://localhost:8888
Hello World | Host: hello-app | Hits: 1

读者应该提供他们自己的 Docker Hub 凭证——作者的密码显然不是HushHush。同样值得注意的是,Docker 注册中心证书是使用简单的 base64 编码存储的,这不是一种加密形式,仅仅是混淆。可以通过查询秘密对象并解码结果来检索凭证:

$ kubectl get secret/docker-hub-secret \
    -o jsonpath \
    --template="{.data.\.dockerconfigjson}" \
    | base64 -d
{
   "auths":{
      "docker.io":{
         "username":"egarbarino",
         "password":"HushHush",
         "email":"antispam@garba.org",
         "auth":"ZWdhcmJhcmlubzpUZXN0aW5nJDEyMw=="
      }
   }
}

TLS 公钥对

除了一般的(用户定义的)秘密和 Docker 注册表凭证,秘密对象还具有存储 TLS 公共/密钥对的特殊规定,以便它们可以被诸如第 7 层(http/https)代理入口 ( https://kubernetes.io/docs/concepts/services-networking/ingress/ )之类的对象引用。请注意,在撰写本文时,入口控制器仍处于测试阶段,本文并未涉及。

使用kubectl create secret tls <NAME>命令和以下两个标志存储公钥/私钥对:

  • --cert=<FILE> : PEM 编码的公钥证书。它通常有一个.crt扩展名。

  • --key=<FILE>:私钥。它通常有一个.key扩展名。

假设我们在secrets-tls目录中有文件tls.crttls.key,下面的命令将把它们存储在秘密对象中:

$ kubectl create secret tls my-tls-secret \
    --cert=secrets-tls/tls.crt \
    --key=secrets-tls/tls.key
secret/my-tls-secret created

得到的对象与一般的或 docker-registry 机密没有什么不同。这些文件使用 base64 编码,可以通过查询产生的 Secret 对象轻松地检索和解码。在下一个例子中,我们检索内容,解码它们,并与原始内容进行比较;diff命令没有输出,这意味着两个文件是相同的:

$ kubectl get secret/my-tls-secret \
    --output="jsonpath={.data.tls\.crt}" \
    | base64 -d > /tmp/recovered.crt
$ kubectl get secret/my-tls-secret \
    --output="jsonpath={.data.tls\.key}" \
    | base64 -d > /tmp/recovered.key

$ diff secrets-tls/tls.crt /tmp/recovered.crt
$ diff secrets-tls/tls.key /tmp/recovered.key

管理摘要

常规的 ConfigMap 和 Secret 对象都响应典型的kubectl get <RESOURCE-TYPE>/<OBJECT-IDENTIFIER>kubectl delete <RESOURCE-TYPE>/<OBJECT-IDENTIFIER>命令,分别用于列出和删除。

这是一个列出现有 ConfigMap 对象并删除名为data-sources的对象的示例:

$ kubectl get configmap
NAME                    DATA      AGE
data-sources            1         58s
language-translations   1         34s

$ kubectl delete configmap/data-sources
configmap "data-sources" deleted

同样,这是一个列出现有秘密对象并删除名为my-secrets的对象的示例:

$ kubectl get secret
NAME              TYPE                            DATA
*-token-c4bdr     kubernetes.io/service-*-token   3
docker-hub-secret kubernetes.io/dockerconfigjson  1
my-secrets        Opaque                          2
my-tls-secret     kubernetes.io/tls               2

$ kubectl delete secret/my-secrets
secret "my-secrets" deleted

摘要

本章展示了如何通过使用环境变量标志手动设置配置,以及通过 ConfigMap 和 Secret 对象自动设置配置,从而将配置外部化,使其不被硬编码到应用中。

我们了解到配置图和秘密对象是相似的。它们都有助于填充配置属性(使用标志、清单文件和包含键/值对的外部文件)以及将所述属性注入到 Pods 的容器中(将所有数据作为环境变量投影,选择特定的变量,使变量作为虚拟文件系统可用)。我们还探讨了如何处理文本和二进制形式的长文件,以及如何生成实时配置更新。

最后,我们看到 Secrets 对象还具有存储 Docker 注册表凭证的特殊能力,这些凭证对于从私有存储库中提取 Docker 映像和存储 TLS 键非常有用,TLS 键可以被支持 TLS 的对象(如入口控制器)获取。