一种更好的 Google Cloud 持续部署方法
已发表: 2022-03-11持续部署 (CD) 是将新代码自动部署到生产环境的做法。 大多数持续部署系统通过运行单元和功能测试来验证要部署的代码是否可行,如果一切看起来都不错,部署就会展开。 推出本身通常分阶段进行,以便在代码未按预期运行时能够回滚。
不乏关于如何使用各种工具(如 AWS 堆栈、Google Cloud 堆栈、Bitbucket 管道等)实现您自己的 CD 管道的博客文章。但我发现其中大多数不符合我对好的 CD 管道的想法应该看起来像:一个首先构建,并且仅测试和部署该单个构建文件。
在本文中,我将构建一个事件驱动的持续部署管道,它首先构建,然后在我们的部署最终工件上运行测试。 这不仅使我们的测试结果更加可靠,而且使 CD 流水线易于扩展。 它看起来像这样:
- 对我们的源存储库进行了提交。
- 这会触发相关图像的构建。
- 测试在构建的工件上运行。
- 如果一切看起来都不错,则将映像部署到生产环境。
本文假设您至少熟悉 Kubernetes 和容器技术,但如果您不熟悉或需要复习,请参阅什么是 Kubernetes? 容器化和部署指南。
大多数 CD 设置的问题
这是我对大多数 CD 管道的问题:它们通常在构建文件中执行所有操作。 我读过的大多数博客文章在他们拥有的任何构建文件中都会有以下顺序的一些变化( cloudbuild.yaml
用于 Google Cloud Build, bitbucket-pipeline.yaml
用于 Bitbucket)。
- 运行测试
- 构建镜像
- 将图像推送到容器 repo
- 使用新图像更新环境
您没有在最终工件上运行测试。
通过按此顺序执行操作,您可以运行测试。 如果他们成功了,您将构建映像并继续进行其余的管道。 如果构建过程以某种方式更改了您的图像,导致测试不再通过,会发生什么? 在我看来,您应该首先生成一个工件(最终的容器映像),并且该工件不应该在构建和部署到生产之间发生变化。 这可确保您拥有的关于所述工件的数据(测试结果、大小等)始终有效。
您的构建环境拥有“通往王国的钥匙”。
通过使用您的构建环境将您的映像部署到您的生产堆栈,您实际上是允许它更改您的生产环境。 我认为这是一件非常糟糕的事情,因为任何对您的源存储库具有写入权限的人现在都可以对您的生产环境做任何他们想做的事情。
如果最后一步失败,您必须重新运行整个管道。
如果最后一步失败(例如,由于凭证问题),您必须重新运行整个管道,占用时间和其他本可以更好地用于其他事情的资源。
这引出了我的最后一点:
你的步骤不是独立的。
在更一般的意义上,拥有独立的步骤可以让您在管道中拥有更大的灵活性。 假设您想向管道添加功能测试。 通过将步骤放在一个构建文件中,您需要让构建环境启动一个功能测试环境并在其中运行测试(很可能是按顺序运行)。 如果您的步骤是独立的,您可以通过“图像构建”事件启动单元测试和功能测试。 然后它们将在自己的环境中并行运行。
我理想的 CD 设置
在我看来,解决这个问题的更好方法是让一系列独立的步骤通过事件机制链接在一起。
与以前的方法相比,这有几个优点:
您可以对不同的事件采取几个独立的操作。
如上所述,成功构建新映像只会发布“成功构建”事件。 反过来,当这个事件被触发时,我们可以运行几件事情。 在我们的例子中,我们将开始单元和功能测试。 您还可以考虑在触发构建失败事件或测试未通过时提醒开发人员等事情。
每个环境都有自己的一组权限。
通过让每一步都发生在自己的环境中,我们不再需要一个单一的环境来拥有所有的权利。 现在构建环境只能构建,测试环境只能测试,部署环境只能部署。 这使您可以确信,一旦您的图像构建完成,它就不会改变。 生成的工件最终会出现在您的生产堆栈中。 它还允许更轻松地审核管道的哪个步骤正在做什么,因为您可以将一组凭据链接到一个步骤。
有更多的灵活性。
想要在每次成功构建时向某人发送电子邮件? 只需添加一些对该事件作出反应并发送电子邮件的内容。 这很容易——您不必更改构建代码,也不必在源存储库中硬编码某人的电子邮件。
重试更容易。
拥有独立的步骤也意味着如果一个步骤失败,您不必重新启动整个管道。 如果失败条件是暂时的或已手动修复,您可以重试失败的步骤。 这允许更有效的管道。 如果构建步骤需要几分钟时间,最好不要因为忘记授予部署环境对集群的写入权限而重新构建映像。
实施 Google Cloud 持续部署
谷歌云平台拥有在短时间内构建这样一个系统所需的所有工具,并且只需要很少的代码。
我们的测试应用程序是一个简单的 Flask 应用程序,它只提供一段静态文本。 此应用程序部署到 Kubernetes 集群,该集群为更广泛的互联网提供服务。
我将实现我之前介绍的管道的简化版本。 我基本上删除了测试步骤,所以现在看起来像这样:
- 对源存储库进行新的提交
- 这会触发映像构建。 如果成功,则将其推送到容器存储库并将事件发布到 Pub/Sub 主题
- 订阅该主题的小脚本并检查图像的参数 - 如果它们与我们要求的匹配,则将其部署到 Kubernetes 集群。
这是我们管道的图形表示。
流程如下:
- 有人提交到我们的存储库。
- 这会触发基于源存储库构建 Docker 映像的云构建。
- 云构建将镜像推送到容器存储库,并将消息发布到云 pub/sub。
- 这会触发一个云函数,该函数会检查已发布消息的参数(构建状态、构建的映像名称等)。
- 如果参数正确,云功能会使用新映像更新 Kubernetes 部署。
- Kubernetes 使用新镜像部署新容器。
源代码
我们的源代码是一个非常简单的 Flask 应用程序,它只提供一些静态文本。 这是我们项目的结构:
├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Docker 目录包含构建 Docker 映像所需的所有内容。 该映像基于 uWSGI 和 Nginx 映像,仅安装依赖项并将应用程序复制到正确的路径。
k8s 目录包含 Kubernetes 配置。 它由一项服务和一项部署组成。 部署基于从Dockerfile构建的映像启动一个容器。 然后该服务启动具有公共 IP 地址的负载均衡器并重定向到应用程序容器。
云构建
云构建配置本身可以通过云控制台或谷歌云命令行来完成。 我选择使用云控制台。
在这里,我们为任何分支上的任何提交构建一个映像,但例如,您可以为开发和生产创建不同的映像。

如果构建成功,云构建会自行将镜像发布到容器注册中心。 然后它将向 cloud-builds pub/sub 主题发布一条消息。
云构建还会在构建进行中和构建失败时发布消息,因此您也可以让事物对这些消息做出反应。
云构建的发布/订阅通知的文档在这里,消息的格式可以在这里找到
云发布/订阅
如果您查看云控制台中的云发布/订阅选项卡,您会看到云构建创建了一个名为云构建的主题。 这是云构建发布其状态更新的地方。
云功能
我们现在要做的是创建一个云函数,该函数会在发布到 cloud-builds 主题的任何消息上触发。 同样,您可以使用云控制台或 Google Cloud 命令行实用程序。 在我的案例中,我所做的是,每次发生更改时,我都会使用云构建来部署云功能。
云功能的源代码在这里。
我们先来看一下部署这个云功能的代码:
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
在这里,我们使用 Google Cloud Docker 映像。 这允许轻松运行 GCcloud 命令。 我们正在执行的操作相当于直接从终端运行以下命令:
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
我们要求 Google Cloud 部署一个新的云函数(或者如果该区域中已经存在该名称的函数,则替换该函数),它将使用 Python 3.7 运行时并由 cloud-builds 主题中的新消息触发。 我们还告诉 Google 在哪里可以找到该函数的源代码(这里 PROJECT_ID 和 REPO_NAME 是由构建过程设置的环境变量)。 我们还告诉它调用哪个函数作为入口点。
附带说明一下,为了使其工作,您需要为您的 cloudbuild 服务帐户提供“云功能开发人员”和“服务帐户用户”,以便它可以部署云功能。
以下是云函数代码的一些注释片段
入口点数据将包含在 pub/sub 主题上收到的消息。
def onNewImage(data, context):
第一步是从环境中获取特定部署的变量(我们通过修改云控制台中的云函数来定义这些变量。
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
我们将跳过检查消息结构是否符合我们预期的部分,并验证构建是否成功并生成了一个图像工件。
下一步是确保构建的映像是我们要部署的映像。
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
现在,我们得到一个 Kubernetes 客户端并检索我们想要修改的部署
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
最后,我们使用新映像修补部署; Kubernetes 将负责将其推出。
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
结论
这是一个非常基本的示例,说明我喜欢如何在 CD 管道中构建事物。 您可以通过更改发布/订阅事件触发的内容来执行更多步骤。
例如,您可以运行一个容器,该容器在映像中运行测试,并在成功时发布一个事件,在失败时发布另一个事件,并通过更新部署或根据结果发出警报来对这些事件做出反应。
我们构建的管道非常简单,但您可以为其他部分编写其他云函数(例如,向提交破坏单元测试的代码的开发人员发送电子邮件的云函数)。
如您所见,我们的构建环境无法更改 Kubernetes 集群中的任何内容,我们的部署代码(云功能)无法修改已构建的映像。 我们的权限分离看起来不错,而且我们知道流氓开发人员不会让我们的生产集群崩溃,我们可以睡个安稳觉。 此外,我们可以让更多面向运维的开发人员访问云功能代码,以便他们修复或改进它。
如果您有任何问题、意见或改进,请随时在下面的评论中联系。