vlambda博客
学习文章列表

读书笔记《hands-on-docker-for-microservices-with-python》处理系统中的更改、依赖项和机密

Handling Change, Dependencies, and Secrets in the System

在本章中,我们将描述与多个微服务交互的不同元素。

我们将研究如何使服务描述其版本的策略,以便依赖的微服务可以发现它们并确保它们已经部署了适当的依赖项。这将允许我们在依赖服务中定义部署顺序,如果不是所有依赖项都准备好,将停止服务部署。

本章介绍如何定义集群范围的配置参数,以便它们可以在多个微服务之间共享并使用 Kubernetes ConfigMap 在一个地方进行管理。我们还将学习如何处理属于机密的配置参数(例如加密密钥),团队中的大多数人都无法访问这些参数。

本章将涵盖以下主题:

  • Understanding shared configuration across microservices
  • Handling Kubernetes secrets
  • Defining a new feature affecting multiple services
  • Dealing with service dependencies

在本章结束时,您将了解如何准备相关服务以进行安全部署,以及如何在微服务中包含在它们预期的部署之外无法访问的秘密。

Technical requirements

代码可在 GitHub 上的以下 URL 获得:https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11。请注意,该代码是 Chapter10 中代码的扩展,具有本章所述的额外元素。结构是一样的,一个子目录叫 microservices 和代码,另一个叫 kubernetes 和Kubernetes配置文件。

要安装集群,您需要使用以下命令构建每个单独的微服务:

$ cd Chapter11/microservices/
$ cd rsyslog
$ docker-compose build
...
$ cd frontend
$ ./build-test.sh
...
$ cd thoughts_backend
$./build-test.sh
...
$ cd users_backend
$ ./build-test.sh
...

这将构建所需的服务。

请注意,我们使用 build-test.sh 脚本。我们将在本章中解释它是如何工作的。

然后,创建 namespace 示例并使用 Chapter11/kubernetes 子目录中的配置启动 Kubernetes 集群:

$ cd Chapter11/kubernetes
$ kubectl create namespace example
$ kubectl apply --recursive -f .
...

这会将微服务部署到集群。

代码包含在 Chapter11 有一些问题和 在修复之前不会正确部署。这是预期的行为。在本章中,我们将解释两个问题:secrets 没有被配置,以及 Frontend 的依赖没有得到满足,导致它无法启动。

继续阅读本章以找到所描述的问题。该解决方案是作为评估提出的。

为了能够访问不同的服务,您需要更新您的 /etc/hosts 文件以包含以下行:

127.0.0.1 thoughts.example.local
127.0.0.1 users.example.local
127.0.0.1 frontend.example.local

这样,您将能够访问本章的服务。

Understanding shared configurations across microservices

某些配置可能对多个微服务是通用的。在我们的示例中,我们为数据库连接复制相同的值。我们可以使用 ConfigMap 并在不同的部署中共享它,而不是在每个部署文件上重复这些值。

We've seen how to add ConfigMap to include files in Chapter 10, Monitoring Logs and Metrics, under the Setting up metrics section. It was used for a single service, though.

ConfigMap 是一组键/值元素。它们可以作为环境变量或文件添加。在下一节中,我们将添加一个包含集群中所有共享变量的通用配置文件。

Adding the ConfigMap file

configuration.yaml 文件包含系统的常用配置。它位于 Chapter11/kubernetes 子目录中:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: shared-config
  namespace: example
data:
  DATABASE_ENGINE: POSTGRES
  POSTGRES_USER: postgres
  POSTGRES_HOST: "127.0.0.1"
  POSTGRES_PORT: "5432"
  THOUGHTS_BACKEND_URL: http://thoughts-service
  USER_BACKEND_URL: http://users-service

与数据库相关的变量 DATABASE_ENGINEPOSTGRES_USERPOSTGRES_HOSTPOSTGRES_PORT 在 Thoughts Backend 之间共享和用户后端。

The POSTGRES_PASSWORD variable is a secret. We will describe this later in this chapter in the Handling Kubernetes secrets section.

THOUGHTS_BACKEND_URLUSER_BACKEND_URL 变量用于前端服务。不过,它们在整个集群中很常见。任何想要连接到 Thoughts 后端的服务都应该使用 THOUGHTS_BACKEND_URL 中描述的相同 URL。

尽管到目前为止它仅用于单个服务 Frontend,但它符合系统范围的变量的描述,并且应该包含在一般配置中。

One of the advantages of having a shared repository for variables is to consolidate them.

While creating multiple services and developing them independently, it is quite common to end up using the same information, but in two slightly different ways. Teams developing independently won't be able to share information perfectly, and this kind of mismatch will happen.

For example, one service can describe an endpoint as URL=http://service/api, and another service using the same endpoint will describe it as HOST=service PATH=/api. The code of each service handles the configuration differently, though they connect to the same endpoint. This makes it more difficult to change the endpoint in a unified way, as it needs to be changed in two or more places, in two ways.

A shared place is a good way to first detect these problems, as they normally go undetected if each service keeps its own independent configuration, and then to adapt the services to use the same variable, reducing the complexity of the configuration.

在我们的示例中,ConfigMap 的名称是元数据中定义的 shared-config,并且与任何其他 Kubernetes 对象一样,它可以通过 kubectl 命令进行管理。

Using kubectl commands

可以使用通常的 kubectl 命令集检查 ConfigMap 信息。这使我们能够发现集群中已定义的 ConfigMap 实例:

$ kubectl get configmap -n example shared-config
NAME               DATA AGE
shared-config      6    46m

注意 ConfigMap 包含 的键或变量的数量是如何显示的;这里是 6。要查看 ConfigMap 的内容,请使用 describe

$ kubectl describe configmap -n example shared-config
Name: shared-config
Namespace: example
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","data":{"DATABASE_ENGINE":"POSTGRES","POSTGRES_HOST":"127.0.0.1","POSTGRES_PORT":"5432","POSTGRES_USER":"postgres","THO...

Data
====
POSTGRES_HOST:
----
127.0.0.1
POSTGRES_PORT:
----
5432
POSTGRES_USER:
----
postgres
THOUGHTS_BACKEND_URL:
----
http://thoughts-service
USER_BACKEND_URL:
----
http://users-service
DATABASE_ENGINE:
----
POSTGRES

如果您需要更改 ConfigMap,可以使用 kubectl edit 命令,或者更好的是,更改 configuration.yaml 文件并使用以下命令重新应用它:

$ kubectl apply -f kubernetes/configuration.yaml

这将覆盖所有值。

The configuration won't be applied automatically to the Kubernetes cluster. You'll need to redeploy the pods affected by the changes. The easiest way is to delete the affected pods and allow the deployment to recreate them.

On the other hand, if Flux is configured, it will redeploy the dependent pods automatically. Keep in mind that a change in ConfigMap (referenced in all pods) will trigger a redeploy on all pods in that situation.

我们现在将看到如何将 ConfigMap 添加到部署中。

Adding ConfigMap to the deployment

一旦 ConfigMap 到位,它就可以用于与不同的部署共享其变量,维护一个中心位置来更改变量并避免重复。

让我们看看微服务(Thoughts 后端、用户后端和前端)的每个部署如何使用 shared-config ConfigMap。

Thoughts Backend ConfigMap configuration

Thoughts 后端部署定义如下:

spec:
    containers:
        - name: thoughts-backend-service
          image: thoughts_server:v1.5
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config
          env:
              - name: POSTGRES_DB
                value: thoughts
          ...

完整的 shared-config ConfigMap 将被注入 pod。请注意,这包括以前在 pod 中不可用的 THOUGHTS_BACKEND_URL USER_BACKEND_URL 环境变量。可以添加更多环境变量。在这里,我们留下 POSTGRES_DB 而不是将其添加到 ConfigMap。

我们可以在 pod 中使用 exec 来确认。

Note that to be able to connect the secret, it should be properly configured. Refer to the Handling Kubernetes secrets section.

要检查容器内部,请检索 pod 名称并在其中使用 exec,如以下命令所示:

$ kubectl get pods -n example
NAME                              READY STATUS  RESTARTS AGE
thoughts-backend-5c8484d74d-ql8hv 2/2   Running 0        17m
...
$ kubectl exec -it thoughts-backend-5c8484d74d-ql8hv -n example /bin/sh
Defaulting container name to thoughts-backend-service.
/opt/code $ env | grep POSTGRES
DATABASE_ENGINE=POSTGRESQL
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=postgres
POSTGRES_PORT=5432
POSTGRES_DB=thoughts
/opt/code $ env | grep URL
THOUGHTS_BACKEND_URL=http://thoughts-service
USER_BACKEND_URL=http://users-service

env 命令返回所有的环境变量,但其中有很多是 Kubernetes 自动添加的。

Users Backend ConfigMap configuration

用户后端配置类似于我们刚刚看到的上一个类型的配置:

spec:
    containers:
        - name: users-backend-service
          image: users_server:v2.3
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config
          env:
              - name: POSTGRES_DB
                value: thoughts
          ...

POSTGRES_DB 的值与 Thoughts Backend 中的值相同,但我们将其留在这里以展示如何添加更多环境变量。

Frontend ConfigMap configuration

前端配置仅使用 ConfigMap,因为不需要额外的环境变量:

spec:
    containers:
        - name: frontend-service
          image: thoughts_frontend:v3.7
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config

现在,前端 pod 还将包含有关与数据库的连接的信息,这是它不需要的。这对于大多数配置参数都很好。

You can also use multiple ConfigMaps to describe different groups of configurations, if necessary. It is simpler to handle them in a big bucket with all the configuration parameters, though. This will help to catch duplicated parameters and ensure that you have all the required parameters in all microservices.

但是,必须更加小心地处理一些配置参数,因为它们很敏感。例如,我们从 shared-config ConfigMap 中省略了 POSTGRES_PASSWORD 变量。这样我们就可以登录到数据库中了,不应该存储在任何带有其他参数的文件中,以免意外暴露。

为了处理这类信息,我们可以使用 Kubernetes Secrets。

Handling Kubernetes secrets

秘密是一种特殊的配置。需要保护它们不被使用它们的其他微服务读取。它们通常是敏感数据,例如私钥、加密密钥和密码。

请记住,读取密钥是有效的操作。毕竟,它们需要被使用。秘密与其他配置参数的区别在于它们需要受到保护,因此只有授权的来源才能读取它们。

秘密应该由环境注入。这要求代码能够检索配置机密并为当前环境使用正确的机密。它还避免了将秘密存储在代码中。

Remember never to commit production secrets in your Git repositories. The Git tree means that, even if it's deleted, the secret is retrievable. This includes the GitOps environment.

Also, use different secrets for different environments. The production secrets require more care than the ones in test environments.

在我们的 Kubernetes 配置中,授权来源是使用它们的微服务,以及系统管理员,通过 kubectl 访问。

让我们看看如何管理这些秘密。

Storing secrets in Kubernetes

Kubernetes 将机密作为一种特定类型的 ConfigMap 值来处理。它们可以在系统中定义,然后以同样的方式应用到 ConfigMap 中。 与一般 ConfigMap 的不同之处在于,信息在内部受到保护。虽然可以通过 kubectl 访问它们,但它们受到保护以防意外暴露。

可以通过 kubectl 命令在集群中创建秘密。它们应该通过文件和 GitOps 或 Flux 创建,而是手动创建。这避免了将机密存储在 GitOps 存储库下。

需要密码才能运行的 pod 将在其部署文件中指出这一点。这可以安全地存储在 GitOps 源代码控制下,因为它不存储秘密,而只存储对秘密的引用。当 pod 被部署时,它将使用正确的引用和解码密钥。

登录 pod 将授予您访问该密钥的权限。这是正常的,因为在 pod 内部,应用程序需要读取它的值。授予在 pod 中执行命令的权限将授予他们访问内部机密的权限,因此请记住这一点。您可以阅读 Kubernetes 文档,了解有关密钥的最佳实践,以根据您的要求进行理解和调整( https://kubernetes.io/docs/concepts/configuration/secret/#best - 实践)。

现在我们知道如何处理它们,让我们看看如何创建这些秘密。

Creating the secrets

让我们在 Kubernetes 中创建秘密。我们将存储以下秘密:

  • The PostgreSQL password
  • The public and private keys to sign and validate requests

我们将它们存储在可以有多个密钥的同一个 Kubernetes 密钥中。以下命令显示如何生成一对密钥:

$ openssl genrsa -out private_key.pem 2048
Generating RSA private key, 2048 bit long modulus
........+++
.................+++
e is 65537 (0x10001)
$ openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pub
writing RSA key
$ ls 
private_key.pem public_key.pub

这些密钥对您来说是独一无二的。我们将使用它们来替换前面章节中存储的示例键。

Storing the secrets in the cluster

将机密存储在集群中的 thoughts-secrets 机密下。请记住将其存储在 example 命名空间中:

$ kubectl create secret generic thoughts-secrets --from-literal=postgres-password=somepassword --from-file=private_key.pem --from-file=public_key.pub -n example

您可以列出命名空间中的秘密:

$ kubectl get secrets -n example
NAME             TYPE   DATA AGE
thoughts-secrets Opaque 3    41s

您可以描述这些秘密以获取更多信息:

$ kubectl describe secret thoughts-secrets -n example
Name: thoughts-secrets
Namespace: default
Labels: <none>
Annotations: <none>

Type: Opaque

Data
====
postgres-password: 12 bytes
private_key.pem: 1831 bytes
public_key.pub: 408 bytes

您可以获取密钥的内容,但检索的数据是以 Base64 编码的。

Base64 是一种编码方案,允许您将二进制数据转换为文本,反之亦然。它被广泛使用。这允许您存储任何二进制机密,而不仅仅是文本。这也意味着在检索时不会以纯文本形式显示秘密,从而在屏幕上无意显示等情况下增加了一层保护。

要获取密钥,请使用通常的 kubectl get 命令,如此处所示。我们使用 base64 命令对其进行解码:

$ kubectl get secret thoughts-secrets -o yaml -n example
apiVersion: v1
data:
  postgres-password: c29tZXBhc3N3b3Jk
  private_key.pem: ...
  public_key.pub: ...
$ echo c29tZXBhc3N3b3Jk | base64 --decode
somepassword

同样,如果你编辑一个秘密来更新它,输入应该用 Base64 编码。

Secret deployment configuration

我们需要在部署配置中配置 secret 的使用,所以 secret 在所需的 pod 中可用。例如,在用户后端 deployment.yaml 配置文件中,我们有以下代码:

spec:
    containers:
    - name: users-backend-service
      ...
      env:
      ...
      - name: POSTGRES_PASSWORD
        valueFrom:
          secretKeyRef:
            name: thoughts-secrets
            key: postgres-password
        volumeMounts:
        - name: sign-keys
          mountPath: "/opt/keys/"

    volumes:
    - name: sign-keys
      secret:
        secretName: thoughts-secrets
        items:
        - key: public_key.pub
          path: public_key.pub
        - key: private_key.pem
          path: private_key.pem

我们创建直接来自密钥的 POSTGRES_PASSWORD 环境变量。我们还创建了一个名为 sign-keys 的卷,其中包含两个作为文件的密钥,public_key.pubprivate_key.pem。它安装在 /opt/keys/ 路径中。

以类似的方式,Thoughts 后端的 deployment.yaml 文件包含秘密,但只有 PostgreSQL 密码和 public_key.pub。请注意,未添加私钥,因为 Thoughts Backend 不需要它,并且它不可用。

对于前端,只需要公钥。现在,让我们确定如何检索秘密。

Retrieving the secrets by the applications

对于 POSTGRES_PASSWORD 环境变量, 我们不需要更改任何内容。它已经是一个环境变量,代码正在从那里提取它。

但是对于存储为文件的秘密,我们需要从正确的位置检索它们。存储为文件的秘密是签署身份验证标头的关键。所有微服务都需要公共文件,只有用户后端需要私钥。

现在,让我们看一下用户后端的 config.py 文件:

import os
PRIVATE_KEY = ...
PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'
PRIVATE_KEY_PATH = '/opt/keys/private_key.pem'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        PUBLIC_KEY = fp.read()

if os.path.isfile(PRIVATE_KEY_PATH):
    with open(PRIVATE_KEY_PATH) as fp:
        PRIVATE_KEY = fp.read()

当前键仍然作为默认值存在。当秘密文件未挂载时,它们将用于单元测试。

值得再说一遍,但是请 不要 使用这些键中的任何一个 <跨度>。这些仅用于运行测试,可供有权访问本书的任何人使用。

如果 /opt/keys/ 路径中的文件存在,它们将被读取,并且内容将存储在适当的常量中。用户后端需要公钥和私钥。

在 Thoughts Backend config.py 文件中,我们只检索公钥,如以下代码所示:

import os
PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        PUBLIC_KEY = fp.read()

前端服务在 settings.py 文件中添加公钥:

TOKENS_PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        TOKENS_PUBLIC_KEY = fp.read()

此配置使秘密可用于应用程序并关闭秘密值的循环。现在,微服务集群使用来自秘密值的签名密钥,这是一种存储敏感数据的安全方式。

Defining a new feature affecting multiple services

我们讨论了单个微服务领域内的变更请求。但是,如果我们需要部署一个在两个或多个微服务中工作的功能怎么办?

与单体方法相比,这些类型的功能应该相对较少,并且是微服务开销的主要原因之一。在单体应用中,这种情况根本不可能发生,因为所有东西都包含在单体应用程序的墙内。

与此同时,在微服务架构中,这是一个复杂的变化。这涉及驻留在两个不同存储库中的每个涉及的微服务上的至少两个独立功能。存储库很可能由两个不同的团队开发,或者至少不同的人将负责每个功能。

Deploying one change at a time

为了确保功能可以一次一个地顺利部署,它们需要保持向后兼容性。这意味着您需要能够处于服务 A 已部署但服务 B 已部署的中间阶段。微服务中的每次更改都需要尽可能小以最大程度地降低风险,并且应该在部署时引入一个更改一次。

为什么我们不同时部署它们呢?因为同时发布两个微服务是危险的。首先,部署不是即时的,因此有时过时的服务会发送或接收系统未准备好处理的调用。这将产生可能影响您的客户的错误。

但是有可能发生其中一个微服务不正确并且需要回滚的情况。然后,系统处于不一致的状态。依赖的微服务也需要回滚。这本身就是有问题的,但是当在调试这个问题的过程中,两个微服务都被卡住并且在问题得到解决之前无法更新时,它会使事情变得更糟。

在健康的微服务环境中,会经常发生部署。因为另一个服务需要工作而不得不停止微服务的管道是一个不好的位置,它只会增加压力和紧迫感。

Remember that we talked about the speed of deployment and change. Deploying small increments often is the best way to ensure that each deployment will be of high quality. The constant flow of incremental work is very important.

Interrupting this flow due to an error is bad, but the effect multiplies quickly if the inability to deploy affects the pace of multiple microservices.

同时部署的多个服务也可能会造成死锁,这两个服务都需要工作来解决问题。这会使开发和解决问题的时间变得复杂。

需要进行分析以确定哪个微服务依赖于另一个,而不是同时部署。大多数时候,这是显而易见的。在我们的示例中,前端依赖于 Thoughts Backend,因此涉及它们的任何更改都需要从 Thoughts Backend 开始,然后移至 Frontend。

实际上,Users Backend 是两者的依赖关系,所以假设有一个更改会影响它们三个,您需要先更改 Users Backend,然后是 Thoughts Backend,最后是 Frontend。

请记住,有时,部署可能需要多次跨服务移动。例如,假设我们对身份验证标头的签名机制进行了更改。那么这个过程应该如下:

  1. Implement the new authentication system in the Users Backend, but keep producing tokens with the old system through a config change. The old authentication process is still used in the cluster so far.
  2. Change the Thoughts Backend to allow working with both the old and the new system of authenticating. Note that it is not activated yet.
  3. Change the Frontend to work with both authentication systems. Still, at this point, the new system is not yet used.
  4. Change configuration in the Users Backend to produce new authentication tokens. Now is when the new system starts to be used. While the deployment is underway, some old system tokens may be generated.
  5. The Users Backend and Frontend will work with any token in the system, either new or old. Old tokens will disappear over time, as they expire. New tokens are the only ones being created.
  6. As an optional stage, the old authentication system can be deleted from the systems. The three systems can delete them without any dependency as the system is not used at this point.

在过程的任何步骤,服务都不会中断。每个单独的更改都是安全的。这个过程正在慢慢地使整个系统进化,但是如果出现问题,每个单独的步骤都是可逆的,并且服务不会中断。

系统倾向于通过添加新功能来发展,很少有清理阶段。通常,系统会在很长一段时间内使用已弃用的功能,即使该功能在任何地方都没有使用。

We will talk a bit more about clean-up in Chapter 12, Collaborating and Communicating across Teams.

配置更改也可能需要此过程。在示例中,更改签署身份验证标头所需的私钥将需要以下步骤:

  1. Make the Thoughts Backend and Frontend able to handle more than one public key. This is a prerequisite and a new feature.
  2. Change the handled keys in the Thoughts Backend to have both the old and the new public keys. So far, no headers signed with the new key are flowing in the system.
  3. Change the handled keys in the Frontend to have both the old and the new. Still, no headers signed with the new key are flowing in the system.
  4. Change the configuration of the Users Backend to use the new private key. From now on, there are headers signed with the new private key in the system. Other microservices are able to handle them.
  5. The system still accepts headers signed with the old key. Wait for a safe period to ensure all old headers are expired.
  6. Remove the configuration for the old key in the Users Backend.

可以每隔几个月重复步骤 2 到 6 以使用新密钥。

此过程称为 密钥轮换, 它被认为是一种良好的安全实践,因为它会在密钥有效时缩短寿命,从而减少窗口系统很容易受到泄露的密钥的影响。为简单起见,我们没有在示例系统中实现它,但作为推荐练习保留这样做。尝试更改示例代码以实现此密钥轮换示例!

完整的系统功能可能涉及多个服务和团队。为了帮助协调系统的依赖关系,我们需要知道服务的某个依赖关系何时部署并准备就绪。我们将在 Chapter 12 中讨论团队间的沟通,Collaborating和跨团队通信,但我们可以通过使服务 API 明确描述部署的服务版本来以编程方式提供帮助,正如我们将在处理服务依赖项中讨论的那样部分。

如果刚刚部署的新版本出现问题,可以通过回滚快速恢复部署。

Rolling back the microservices

回滚是将微服务之一快速退回到先前版本的过程。

当刚发布的新版本出现灾难性错误时,可以触发此过程,因此可以快速解决。鉴于该版本目前已经兼容,因此可以在非常短的反应时间内充满信心地完成此操作。通过 GitOps 原则,可以进行一次 revert commit 以带回旧版本。

这 <跨度> git 还原 命令允许你创建一个撤销另一个的提交,反向应用相同的更改。

这是撤消特定更改并允许稍后 revert the revert 并重新引入更改。您可以查看 Git 文档以获取更多详细信息( https://git-scm.com/docs/git-revert )。

考虑到继续前进的战略方法,回滚是一种临时措施,当它到位时,将停止微服务中的新部署。应尽快创建解决导致灾难性部署的错误的新版本,以保持正常的发布流程。

随着您越来越频繁地部署,并获得更好的检查,回滚将越来越少。

Dealing with service dependencies

为了允许服务检查其依赖项是否具有正确的版本,我们将使服务通过 RESTful 端点公开其版本。

我们将遵循 GitHub 中的 Thoughts Backend 中的示例,网址为:https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/thoughts_backend.

检查前端版本是否可用(https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend)。

该过程的第一步是正确定义每个服务的版本。

Versioning the services

为了清楚地了解我们软件的进展,我们需要命名要部署的不同版本。当我们使用 git 来跟踪更改时,系统中的每个提交都有一个单独的提交 ID,但它不遵循任何特定的图案。

为了赋予它含义并对其进行排序,我们需要开发一个版本模式。有多种方法可以创建版本模式,包括按发布日期(Ubuntu 使用这个)或按 major.minor.patch

在任何地方都拥有相同的版本控制方案有助于在团队之间建立共同的语言和理解。它还有助于管理层了解变化——无论是在什么时候发布,以及它们变化的速度。与您的团队商定一个对您的组织有意义的版本控制方案,并在所有服务中遵循它。

对于此示例,我们将使用 vMajor.Minor 模式和用户后端的版本为 v2.3

软件版本控制中最常见的模式是语义版本控制。 这种版本控制模式对包和面向客户的 API 很有用,但对内部微服务 API 用处不大。 让我们看看它的特点是什么。

Semantic versioning

语义版本控制对每个不同版本号的更改赋予意义。这样可以轻松了解版本之间的更改范围以及在依赖系统上进行更新是否有风险。

语义版本控制为每个版本定义了三个数字:主要、次要和补丁,通常描述为 major.minor.patch

增加这些数字中的任何一个都具有特殊的含义,如下所示:

  • Increasing the major number produces backward-incompatible changes.
  • Increasing the minor number adds new features, but keeps backward-compatibility.
  • Increasing the patch number fixes bugs, but doesn't add any new features.

例如,Python 在此模式下工作,如下所示:

  • Python 3 included compatibility changes with Python 2.
  • Python version 3.7 introduced new features compared with Python 3.6.
  • And Python 3.7.4 added security and bug fixes compared with Python 3.7.3.

此版本控制方案在与外部合作伙伴沟通时很有用,并且非常适合大版本和标准包。但是对于微服务中的小幅增量更改,它并不是很有用。

正如我们在前几章中所讨论的,交付持续集成的关键是进行非常小的更改。它们不应破坏向后兼容性,但随着时间的推移,旧功能将被丢弃。每个微服务以受控方式与其他服务协同工作。与外包装相比,没有必要拥有如此强大的功能标签。服务的消费者是其他微服务,在集群中受到严格控制。

由于操作的这种变化,一些项目正在放弃语义版本控制。例如,Linux 内核停止使用语义版本控制来生成没有任何特定含义的新版本( http://lkml.iu.edu/hypermail/linux/kernel/1804.1 /06654.html),因为从一个版本到下一个版本的变化相对较小。

Python 也将 4.0 版视为 3.9 之后的版本,没有像 Python 3 那样的重大变化( http://www.curiousefficiency.org/posts/2014/08/python -4000.html)。

这就是为什么在内部 推荐语义版本控制。保持类似的版本控制方案可能很有用,但不强制它进行兼容性更改,只是不断增加数字,对何时更改次要版本或主要版本没有具体要求。

但是,在外部,版本号可能仍然具有营销意义。对于 外部可访问的端点,使用语义版本控制可能会很有趣。

一旦确定了服务的版本,我们就可以在一个公开这些信息的端点上工作。

Adding a version endpoint

要部署的版本可以从 Kubernetes 部署或 GitOps 配置中读取。但有一个问题。某些配置可能会产生误导或不唯一地指向单个图像。例如, latest 标签可能在不同的时间代表不同的容器,因为它会被覆盖。

此外,还存在访问 Kubernetes 配置或 GitOps 存储库的问题。对于开发人员来说,也许这种配置是可用的,但它们不适用于微服务(也不应该)。

要让集群中的其余微服务发现服务的版本,最好的方法是在 RESTful API 中显式创建版本端点。服务版本的发现被授予,因为它使用在任何其他请求中使用的相同接口。让我们看看如何实现它。

Obtaining the version

要服务版本,我们首先需要将其记录到服务中。

正如我们之前所讨论的,版本存储为 Git 标签。这将是我们在版本中的经典。我们将添加提交以及的 Git SHA-1 以避免任何差异。

SHA-1 是标识每个提交的唯一 ID。它是通过对 Git 树进行散列生成的,因此它能够捕获任何更改——无论是内容还是树历史。我们将使用 40 个字符的完整 SHA-1,即使有时缩写为 8 个或更少。

可以使用以下命令获取提交 SHA-1:

$ git log --format=format:%H -n 1

这将打印最后一次提交信息,并且仅打印带有 %H 描述符的 SHA。

要获取此提交所引用的标签,我们将使用 git-describe 命令:

$ git describe --tags

基本上,git-describe 会找到最接近当前提交的标签。如果此提交由标记标记,对于我们的部署来说应该是这样,它会返回标记本身。如果不是,它会在标签后面加上有关提交的额外信息,直到它到达当前标签。以下代码显示了如何使用 git describe,具体取决于代码的提交版本。请注意与标签无关的代码如何返回最接近的标签和额外的数字:

$ # in master branch, 17 commits from the tag v2.3
$ git describe
v2.3-17-g2257f9c
$ # go to the tag
$ git checkout v2.3
$ git describe
v2.3

这总是返回一个版本,让我们一眼就能看出当前提交中的代码是否被标记在 git 中。

Anything that gets deployed to an environment should be tagged. Local development is a different matter, as it consists of code that is not ready yet.

我们可以通过编程方式存储这两个值,让我们能够自动完成并 将它们包含在 Docker 映像中。

Storing the version in the image

我们希望在图像中提供可用的版本。因为图像是不可变的,所以在构建过程中这样做是目标。这里我们需要克服的限制是 Dockerfile 进程不允许我们在主机上执行命令,只能在容器内执行。我们需要在构建时将这些值注入 Docker 映像中。

A possible alternative is to install Git inside the container, copy the whole Git tree, and obtain the values. This is usually discouraged because installing Git and the full source tree adds a lot of space to the container, something that is worse. During the build process, we already have Git available, so we just need to be sure to inject it externally, which is easy to do with a build script.

传递值的最简单方法是通过 ARG 参数。作为构建过程的一部分,我们会将它们转换为环境变量,因此它们将与配置的任何其他部分一样容易使用。下面我们看一下Dockerfile中的代码:

# Prepare the version
ARG VERSION_SHA="BAD VERSION"
ARG VERSION_NAME="BAD VERSION"
ENV VERSION_SHA $VERSION_SHA
ENV VERSION_NAME $VERSION_NAME

我们接受一个 ARG 参数,然后通过 ENV 参数将其转换为环境变量。为简单起见,两者具有相同的名称。 ARG 参数有一个极端情况的默认值。

这使得版本在我们使用 build.sh 脚本构建后可用(在容器内),该脚本获取值并调用 docker-compose 以版本为参数进行构建,使用以下步骤:

# Obtain the SHA and VERSION
VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=`git describe --tags`
# Build using docker-compose with arguments
docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
# Tag the resulting image with the version
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}

在构建过程之后,该版本可用作容器内的标准环境变量。

我们包括了一个脚本( build-test.sh )在本章的每个微服务中(f或例如, https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/build-test.sh)。这会模拟 SHA-1 和版本名称以创建用于测试的合成版本。它设置了 <跨度> v2.3 版本用于用户后端和 <跨度> v1.5 用于 Thoughts 后端。这些将用于我们代码中的示例。

检查 Kubernetes 部署是否包含这些版本(例如, https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/docker-compose.yaml#L21 镜像是这 <跨度> v1.5 版本)。

此外,VERSION_NAME 也可以来自 CI 管道作为脚本的参数。为此,您需要替换脚本以在外部接受它,如 build-ci.sh 脚本所示:

#!/bin/bash
if [ -z "$1" ]
  then
    # Error, not version name
    echo "No VERSION_NAME supplied"
    exit -1
fi

VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=$1

docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}

这些脚本的所有版本都包含以 VERSION_NAME 作为标签的图像标签。

我们可以在 Python 代码中使用容器内的版本检索环境变量,并在端点中返回它们,从而使版本可以通过外部 API 轻松访问。

Implementing the version endpoint

admin_namespace.py 文件中,我们将创建一个新的 Version 端点以下代码:

import os

@admin_namespace.route('/version/')
class Version(Resource):

    @admin_namespace.doc('get_version')
    def get(self):
        '''
        Return the version of the application
        '''
        data = {
            'commit': os.environ['VERSION_SHA'],
            'version': os.environ['VERSION_NAME'],
        }

        return data

好的,现在这段代码非常简单。它使用 os.environ 来检索在构建期间注入的环境变量作为配置参数,并返回带有提交 SHA-1 和标签(描述为版本)的字典。

该服务可以使用 docker-compose 在本地构建和运行。要测试对 /admin/version 中端点的访问并进行检查,请执行以下步骤:

$ cd Chapter11/microservices/thoughts_backend
$ ./build.sh
...
Successfully tagged thoughts_server:latest
$ docker-compose up -d server
Creating network "thoughts_backend_default" with the default driver
Creating thoughts_backend_db_1 ... done
Creating thoughts_backend_server_1 ... done
$ curl http://localhost:8000/admin/version/
{"commit": "2257f9c5a5a3d877f5f22e5416c27e486f507946", "version": "tag-17-g2257f9c"}

由于版本可用,我们可以更新自动生成的文档以显示正确的值,如 app.py 所示:

import os
...
VERSION = os.environ['VERSION_NAME']
...

def create_app(script=False):
    ...
    api = Api(application, version=VERSION, 
              title='Thoughts Backend API',
              description='A Simple CRUD API')

因此版本会正确显示在自动 Swagger 文档中。一旦可以通过 API 中的端点访问微服务的版本,其他外部服务就可以访问它以发现版本并使用它。

Checking the version

能够通过 API 检查版本允许我们以编程方式轻松访问版本。这可以用于多种目的,例如生成显示在不同环境中部署的不同版本的仪表板。但我们将探索引入服务依赖项的可能性。

微服务在启动时,可以检查它所依赖的服务,并检查它们是否高于预期的版本。如果他们不是,它将不会启动。这避免了在更新依赖关系之前部署一个依赖服务时的配置问题。这可能发生在部署中没有很好的协调的复杂系统中。

为了检查版本,在 start_server.sh中启动服务器时,我们将 首先调用一个小脚本检查依赖关系。如果它不可用,它将产生错误并停止。我们将检查前端是否有可用版本的思想后端或更高版本。

我们将在示例中调用的脚本称为 check_dependencies_services.py,它在前端的 start_server.sh 中调用。

check_dependencies_services 脚本可以分为三部分:需要的依赖列表;检查一个依赖项;以及检查每个依赖项的主要部分。让我们来看看这三个部分。

Required version

第一部分描述了每个依赖项和 required 的最低版本。在我们的例子中,我们规定 thoughts_backend 需要是version v1.6 或以上:

import os

VERSIONS = {
    'thoughts_backend': 
        (f'{os.environ["THOUGHTS_BACKEND_URL"]}/admin/version',
         'v1.6'),
}

这将重用环境变量 THOUGHTS_BACKEND_URL,并使用特定版本路径完成 URL。

主要部分通过描述的所有依赖项来检查它们。

The main function

main 函数遍历 VERSIONS 字典,并且对每个字典执行以下操作:

  • Calls the endpoint
  • Parses the result and gets the version
  • Calls check_version to see whether it's correct

如果失败,则以 -1 状态结束,因此脚本报告为失败。这些步骤通过以下代码执行:

import requests

def main():
    for service, (url, min_version) in VERSIONS.items():
        print(f'Checking minimum version for {service}')
        resp = requests.get(url)
        if resp.status_code != 200:
            print(f'Error connecting to {url}: {resp}')
            exit(-1)

        result = resp.json()
        version = result['version']
        print(f'Minimum {min_version}, found {version}')
        if not check_version(min_version, version):
            msg = (f'Version {version} is '
                    'incorrect (min {min_version})')
            print(msg)
            exit(-1)

if __name__ == '__main__':
    main()

主要功能还打印一些消息以帮助理解不同的阶段。要调用版本端点,它使用 requests 包并期望 200 状态码和可解析的 JSON 结果。

Note that this code iterates through the VERSION dictionary. So far, we only added one dependency, but the User Backend is another dependency and can be added. It's left as an exercise to do.

版本字段将在 check_version 函数中检查,我们将在下一节中看到。

Checking the version

check_version 函数检查当前返回的版本是否高于或等于最低版本。为了简化,我们将使用 natsort 对版本进行排序,然后检查最低的。

You can check out the natsort full documentation ( https://github.com/SethMMorton/natsort). It can sort a lot of natural strings and can be used in a lot of situations.

基本上,natsort 支持排序常见的版本控制模式,其中包括我们之前描述的标准版本控制模式(v1.6 高于 v1.5)。以下代码使用该库对两个版本进行排序并验证最低版本是否较低:

from natsort import natsorted

def check_version(min_version, version):
    versions = natsorted([min_version, version])
    # Return the lower is the minimum version
    return versions[0] == min_version

有了这个脚本,我们现在可以启动服务,它会检查 Thoughts Backend 是否有正确的版本。如果您按照技术要求部分所述启动服务,您将看到前端未正常启动,并产生 CrashLoopBackOff 状态,如下图:

$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
frontend-54fdfd565b-gcgtt 0/1 CrashLoopBackOff 1 12s
frontend-7489cccfcc-v2cz7 0/1 CrashLoopBackOff 3 72s
grafana-546f55d48c-wgwt5 1/1 Running 2 80s
prometheus-6dd4d5c74f-g9d47 1/1 Running 2 81s
syslog-76fcd6bdcc-zrx65 2/2 Running 4 80s
thoughts-backend-6dc47f5cd8-2xxdp 2/2 Running 0 80s
users-backend-7c64564765-dkfww 2/2 Running 0 81s

使用 kubectl logs 命令检查其中一个前端 pod 的日志以查看原因,如下所示:

$ kubectl logs frontend-54fdfd565b-kzn99 -n example
Checking minimum version for thoughts_backend
Minimum v1.6, found v1.5
Version v1.5 is incorrect (min v1.6)

要解决此问题,您需要构建更高版本的 Thoughts Backend 版本或降低依赖要求。这将作为本章末尾的评估。

Summary

在本章中,我们学习了如何处理同时与多个微服务一起工作的元素。

首先,我们讨论了当新功能需要更改多个微服务时要遵循的策略,包括如何以有序的方式部署小增量以及在出现灾难性问题时能够回滚。

然后我们讨论了定义一个清晰的版本控制模式,并将一个版本端点添加到 RESTful 接口,以允许自我发现微服务的版本。这种自我发现可用于确保如果依赖项不存在,则不部署依赖于另一个的微服务,这有助于协调发布。

The code in GitHub for the Frontend in this chapter ( https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend) includes a dependency to the Thoughts Backend that will stop deploying it. Note that the code, as is, won't work. Fixing it is left as an exercise.

我们还学习了如何使用 ConfigMap 来描述在 Kubernetes 集群中的不同服务之间共享的配置信息。我们稍后介绍了如何使用 Kubernetes 机密来处理敏感且需要格外小心的配置。

在下一章中,我们将看到以高效方式协调不同团队使用不同微服务的各种技术。

Questions

  1. What are the differences between releasing changes in a microservice architecture system and a monolith?
  2. Why should the released changes be small in a microservice architecture?
  3. How does semantic versioning work?
  4. What are the problems associated with semantic versioning for internal interfaces in a microservice architecture system?
  5. What are the advantages of adding a version endpoint?
  6. How can we fix the dependency problem in this chapter's code?
  7. Which configuration variables should we store in a shared ConfigMap?
  8. Can you describe the advantages and disadvantages of getting all the configuration variables in a single shared ConfigMap?
  9. What's the difference between a Kubernetes ConfigMap and a Kubernetes secret?
  10. How can we change a Kubernetes secret?
  11. Imagine that, based on the configuration, we decided to change the public_key.pub file from a secret to a ConfigMap. What changes do we have to implement?

Further reading