一种更好的Google Cloud持续部署方法

本文概述

  • 大多数CD设置的问题
  • 我理想的CD设置
  • 实施Google Cloud持续部署
  • 总结
连续部署(CD)是一种将新代码自动部署到生产中的实践。大多数连续部署系统都可以通过运行单元和功能测试来验证要部署的代码是否可行, 如果一切正常, 则可以开始部署。部署本身通常会分阶段进行, 以便在代码运行不正常时能够进行回滚。
关于如何使用各种工具(例如AWS堆栈, Google Cloud堆栈, Bitbucket管道等)实现自己的CD管道的博客文章不乏不足。但是我发现其中大多数都不符合我对良好CD管道的想法。应该看起来像这样:一个首先构建, 然后仅测试和部署单个构建文件的文件。
在本文中, 我将构建一个事件驱动的连续部署管道, 该管道首先构建, 然后在我们的最终部署工件上运行测试。这不仅使我们的测试结果更加可靠, 而且使CD流水线易于扩展。它看起来像这样:
  1. 提交到我们的源存储库。
  2. 这将触发关联图像的构建。
  3. 测试在构建的工件上运行。
  4. 如果一切正常, 则将映像部署到生产环境。
本文假定你对Kubernetes和容器技术至少有一定的了解, 但是如果你不熟悉或可以使用复习知识, 请参见_什么是Kubernetes?容器化和部署指南_。
大多数CD设置的问题 这是大多数CD流水线的问题:它们通常会完成构建文件中的所有操作。我所读过的大多数博客文章在其拥有的任何构建文件中都具有以下顺序的一些变化(cloudbuild.yaml用于Google Cloud Build, bitbucket-pipeline.yaml用于Bitbucket)。
  1. 运行测试
  2. 建立影像
  3. 将图像推送到容器仓库
  4. 用新映像更新环境
你不会在最终工件上运行测试。
通过按此顺序执行操作, 可以运行测试。如果成功, 则你将构建映像并继续执行其余的管道。如果构建过程以无法通过测试的方式更改了你的映像, 会发生什么?我认为, 你应该首先生成一个工件(最终的容器映像), 并且该工件在构建和部署到生产的时间之间不应改变。这样可以确保你拥有的有关上述工件的数据(测试结果, 大小等)始终有效。
你的构建环境具有” 通往王国的钥匙” 。
通过使用构建环境将映像部署到生产堆栈, 可以有效地允许它更改生产环境。我认为这是一件很糟糕的事情, 因为任何对你的源存储库具有写访问权的人现在都可以对你的生产环境进行任何操作。
如果最后一步失败, 则必须重新运行整个管道。
如果最后一步失败(例如, 由于凭据问题), 则必须重新运行整个管道, 从而占用时间和其他资源, 而这些资源可能会花费在做其他事情上。
这引出我的最后一点:
你的步骤不是独立的。
从更一般的意义上讲, 具有独立的步骤可以使你在管道中具有更大的灵活性。假设你要向管道中添加功能测试。通过将步骤放在一个构建文件中, 你需要使构建环境启动一个功能测试环境并在其中运行测试(最有可能按顺序进行)。如果你的步骤是独立的, 则可以通过” 生成图像” 事件来启动单元测试和功能测试。然后, 它们将在自己的环境中并行运行。
我理想的CD设置 我认为, 解决此问题的更好方法是通过事件机制将一系列独立的步骤链接在一起。
与以前的方法相比, 它具有几个优点:
你可以对不同事件采取几种独立的措施。
如上所述, 成功构建新映像只会发布” 成功构建” 事件。反过来, 触发此事件时, 我们可以运行一些操作。就我们而言, 我们将开始单元测试和功能测试。你还可以想到诸如在触发构建失败事件或测试未通过时提醒开发人员的事情。
每个环境都有其自己的权利集。
通过使每个步骤都在自己的环境中进行, 我们消除了对单个环境拥有所有权利的需求。现在, 构建环境只能构建, 测试环境只能测试, 部署环境只能部署。这样一来, 你可以确信一旦建立了图像, 图像就不会改变。产生的工件将最终出现在你的生产堆栈中。它还可以简化审核管道的哪一步正在执行的操作, 因为你可以将一组凭据链接到一个步骤。
有更多的灵活性。
想要在每次成功构建时向某人发送电子邮件吗?只需添加对事件进行反应并发送电子邮件的内容即可。这很容易-你不必更改构建代码, 也不必在源存储库中对某人的电子邮件进行硬编码。
重试比较容易。
具有独立的步骤还意味着, 如果一个步骤失败, 则不必重新启动整个管道。如果失败情况是暂时的或已手动修复, 则可以重试失败的步骤。这允许更有效的管道。当构建步骤需要花费几分钟时, 最好不要仅仅因为你忘记为部署环境授予对群集的写访问权限而重建映像。
实施Google Cloud持续部署 Google Cloud Platform具有在短时间内以很少的代码构建这样的系统所需的所有工具。
我们的测试应用程序是一个简单的Flask应用程序, 仅提供一段静态文本。该应用程序已部署到Kubernetes集群中, 并为更广泛的Internet服务。
我将实现前面介绍的管道的简化版本。我基本上删除了测试步骤, 所以现在看起来像这样:
  • 对源存储库进行新的提交
  • 这将触发图像构建。如果成功, 则将其推送到容器存储库, 并将事件发布到Pub / Sub主题
  • 订阅了一个小脚本, 并检查图像的参数-如果它们符合我们的要求, 它将部署到Kubernetes集群。
这是管道的图形表示。
一种更好的Google Cloud持续部署方法

文章图片
流程如下:
  1. 有人提交到我们的存储库。
  2. 这将触发云构建, 该云构建基于源存储库构建Docker映像。
  3. 云构建将映像推送到容器存储库, 并将消息发布到云发布/订阅。
  4. 这将触发一个云功能, 该功能会检查已发布消息的参数(构建状态, 构建映像的名称等)。
  5. 如果参数正确, 则云功能会使用新映像更新Kubernetes部署。
  6. 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地址的负载平衡器, 并重定向到应用容器。
云构建
可以通过云控制台或Google Cloud命令行完成云构建配置本身。我选择使用云控制台。
一种更好的Google Cloud持续部署方法

文章图片
在这里, 我们为任何分支上的任何提交构建映像, 但是例如, 对于开发和生产, 你可能具有不同的映像。
如果构建成功, 则云构建将自行将映像发布到容器注册表。然后它将发布消息到cloud-builds pub / sub主题。
云构建还会在构建进行中以及构建失败时发布消息, 因此你还可以对这些消息做出反应。
云构建的发布/订阅通知的文档在此处, 消息的格式可以在此处找到
云发布/订阅
如果你在云控制台中的” 云发布/订阅” 标签中查看, 则会看到该云构建已创建了一个名为” 云构建” 的主题。云构建在此处发布其状态更新。
一种更好的Google Cloud持续部署方法

文章图片
云功能
我们现在要做的是创建一个云功能, 该功能将在发布到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管道中构建事物。你可以通过更改什么pub / sub事件触发什么来采取更多步骤。
例如, 你可以运行一个容器, 该容器在映像中运行测试, 并在成功时发布一个事件, 在失败时发布另一个事件, 并根据结果通过更新部署或警报来对事件做出反应。
我们构建的管道非常简单, 但是你可以为其他部分编写其他云功能(例如, 一个云功能会向开发人员发送一封电子邮件, 该开发人员提交了破坏你的单元测试的代码)。
如你所见, 我们的构建环境无法更改Kubernetes集群中的任何内容, 并且我们的部署代码(云功能)无法修改所构建的映像。我们的权限分离看起来不错, 而且我们知道一个流氓开发人员不会破坏我们的生产集群, 所以我们可以睡一觉。此外, 我们还可以使更多面向操作的开发人员可以访问云功能代码, 以便他们可以对其进行修复或改进。
【一种更好的Google Cloud持续部署方法】如果你有任何疑问, 评论或改进, 请随时与以下评论联系。

    推荐阅读