werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

27 月 2019 日在 DevOpsConf XNUMX 会议主厅举行,作为节日的一部分 RIT++ 2019作为“持续交付”部分的一部分,给出了一份报告“werf - 我们在 Kubernetes 中进行 CI/CD 的工具”。 讲的是那些 每个人在部署到 Kubernetes 时都会面临的问题和挑战,以及可能不会立即注意到的细微差别。 分析可能的解决方案,我们展示了如何在开源工具中实现它 韦尔夫.

自演示以来,我们的实用程序(以前称为 dapp)已经达到了历史性的里程碑 GitHub 上有 1000 颗星 - 我们希望其不断增长的用户社区将使许多 DevOps 工程师的生活变得更轻松。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

所以,我们提出 报告视频 (约 47 分钟,比文章内容丰富得多)以及文本形式的主要摘录。 去!

向 Kubernetes 交付代码

演讲将不再是关于 werf,而是关于 Kubernetes 中的 CI/CD,这意味着我们的软件是打包在 Docker 容器中的 (我在 2016年报告),并且 K8s 将用于在生产中运行它 (有关此内容的更多信息,请参见 2017年).

Kubernetes 中的交付是什么样的?

  • 有一个 Git 存储库,其中包含构建它的代码和说明。 该应用程序内置到 Docker 映像中并发布在 Docker 注册表中。
  • 同一存储库还包含有关如何部署和运行应用程序的说明。 在部署阶段,这些指令被发送到 Kubernetes,后者从注册表接收所需的映像并启动它。
  • 另外,通常还有测试。 其中一些可以在发布图像时完成。 您还可以(按照相同的说明)部署应用程序的副本(在单独的 K8s 命名空间或单独的集群中)并在那里运行测试。
  • 最后,您需要一个 CI 系统来接收来自 Git 的事件(或按钮单击)并调用所有指定的阶段:构建、发布、部署、测试。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

这里有一些重要的注意事项:

  1. 因为我们有一个不可变的基础设施 (不可变的基础设施),在所有阶段(暂存、生产等)使用的应用程序映像, 一定有一个. 我通过示例更详细地讨论了这一点。 这里.
  2. 因为我们遵循基础设施即代码方法 (IAC),应用程序代码,组装和启动它的说明应该是 完全在一个存储库中. 有关这方面的更多信息,请参阅 同一份报告.
  3. 配送链 (送货) 我们通常会看到这样的情况:应用程序被组装、测试、发布 (发布阶段) 就是这样 - 交付已经发生。 但实际上,用户得到的是你推出的东西, 没有 然后当你将其交付生产时,当他能够去那里并且该生产工作成功时。 所以我相信交付链结束了 仅在运营阶段 (跑),或者更准确地说,即使在代码从生产中删除(用新代码替换)的那一刻。

让我们回到上面的 Kubernetes 交付方案:它不仅是我们发明的,而且是每个处理这个问题的人发明的。 事实上,这种模式现在被称为 GitOps (您可以阅读有关该术语及其背后的想法的更多信息 这里)。 让我们看看该计划的各个阶段。

构建阶段

看来你可以在 2019 年谈论构建 Docker 镜像了,那时每个人都知道如何编写 Dockerfile 并运行 docker build?...以下是我想注意的细微差别:

  1. 图像权重 很重要,所以使用 多级仅在图像中留下操作真正需要的应用程序。
  2. 层数 必须通过组合链来最小化 RUN- 根据含义发出命令。
  3. 然而,这增加了问题 调试,因为当程序集崩溃时,你必须从导致问题的链中找到正确的命令。
  4. 组装速度 很重要,因为我们希望快速推出更改并查看结果。 例如,您不希望每次构建应用程序时都重建语言库中的依赖项。
  5. 通常来自您需要的一个 Git 存储库 许多图像,这可以通过一组 Dockerfile(或一个文件中的命名阶段)和一个 Bash 脚本及其顺序组装来解决。

这只是每个人面临的冰山一角。 但还存在其他问题,特别是:

  1. 通常在组装阶段我们需要一些东西 (例如,将 apt 等命令的结果缓存在第三方目录中)。
  2. 我们想要 Ansible 而不是在 shell 中编写。
  3. 我们想要 不使用 Docker 构建 (当我们已经有一个可以运行容器的 Kubernetes 集群时,为什么我们还需要一个额外的虚拟机来配置所有内容?)。
  4. 并行装配,可以用不同的方式来理解:来自 Dockerfile 的不同命令(如果使用多阶段)、同一存储库的多次提交、多个 Dockerfile。
  5. 分布式组装:我们希望在 pod 中收集“短暂”的东西,因为它们的缓存消失了,这意味着它需要单独存储在某个地方。
  6. 最后,我命名了欲望的顶峰 自动魔法:理想的做法是访问存储库,输入一些命令并获取现成的映像,并了解如何正确执行操作和执行哪些操作。 然而,我个人不确定是否可以通过这种方式预见所有细微差别。

以下是这些项目:

  • 莫比/buildkit — Docker Inc 的一个构建器(已经集成到当前版本的 Docker 中),正在尝试解决所有这些问题;
  • iko子 — 来自 Google 的构建器,允许您在没有 Docker 的情况下进行构建;
  • Buildpacks.io — CNCF 尝试创造自动魔法,特别是一个有趣的层变基解决方案;
  • 以及许多其他实用程序,例如 积木, 正版工具/img...

...看看他们在 GitHub 上有多少颗星。 也就是说,一方面, docker build 存在并且可以做某事,但实际上 问题还没有完全解决 - 证明这一点的是替代收集器的并行开发,每个收集器都解决了部分问题。

在码头组装

所以我们必须 韦尔夫 (原 已知 像 dapp) — 来自 Flant 公司的开源实用程序,我们已经开发了很多年。 这一切都始于 5 年前,Bash 脚本优化了 Dockerfile 的组装,并且在过去 3 年里,在一个拥有自己的 Git 存储库的项目框架内进行了全面的开发 (首先在 Ruby 中,然后 改写 转到Go,同时重命名)。 werf 解决了哪些装配问题?

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

蓝色阴影的问题已经实施,并行构建是在同一主机内完成的,黄色突出显示的问题计划在夏末完成。

在注册表中发布的阶段(发布)

我们拨打了 docker push... - 将图像上传到注册表可能会遇到什么困难? 然后问题出现了:“我应该在图像上放置什么标签?” 它的出现是因为我们有 gitflow (或其他 Git 策略)和 Kubernetes,业界正在努力确保 Kubernetes 中发生的事情遵循 Git 中发生的事情。 毕竟,Git 是我们唯一的真理来源。

这有什么难的? 确保重现性:来自 Git 中的提交,本质上是不可变的 (不可变),到 Docker 镜像,该镜像应保持不变。

这对我们也很重要 确定原产地,因为我们想了解 Kubernetes 中运行的应用程序是从哪个提交构建的(然后我们可以进行差异和类似的操作)。

标记策略

第一个很简单 git标签。 我们有一个注册表,其中的图像标记为 1.0。 Kubernetes 有阶段和生产,上传此图像的地方。 在 Git 中,我们进行提交,并在某个时刻进行标记 2.0。 我们根据存储库的说明收集它,并将其放置在带有标签的注册表中 2.0。 我们将其推向舞台,如果一切顺利,然后投入生产。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

这种方法的问题在于我们首先放置标签,然后才进行测试并推出它。 为什么? 首先,这根本不合逻辑:我们正在发布一个我们甚至还没有测试过的软件版本(我们不能这样做,因为为了检查,我们需要放置一个标签)。 其次,这条路径与Gitflow不兼容。

第二个方案 - git提交+标签。 master分支有一个标签 1.0; 在注册表中 - 部署到生产环境的映像。 此外,Kubernetes 集群还具有预览和暂存轮廓。 接下来我们按照Gitflow:在主分支中进行开发(develop)我们创建新功能,导致使用标识符进行提交 #c1。 我们收集它并使用此标识符将其发布在注册表中(#c1)。 使用相同的标识符,我们推出预览。 我们对提交做同样的事情 #c2 и #c3.

当我们意识到有足够的功能时,我们开始稳定一切。 在 Git 中创建分支 release_1.1 (在底座上 #c3 из develop)。 没有必要收集这个版本,因为...... 这是在上一步中完成的。 因此,我们可以简单地将其推出到暂存阶段。 我们修复了以下错误 #c4 并类似地推广到分期。 与此同时,开发工作正在进行中 develop,其中定期进行更改 release_1.1。 在某个时候,我们得到了编译并上传到暂存的提交,我们对此感到满意(#c25).

然后我们合并(快进)发布分支(release_1.1)在主控中。 我们在此提交上放置了带有新版本的标签(1.1)。 但是这个镜像已经在注册表中收集了,所以为了不再收集它,我们只需在现有镜像上添加第二个标签(现在它在注册表中有标签 #c25 и 1.1)。 之后,我们将其投入生产。

有一个缺点,即只有一张图像上传到暂存(#c25),而在生产中则有点不同(1.1),但我们知道“物理上”这些是注册表中的相同图像。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

真正的缺点是不支持合并提交,你必须快进。

我们可以更进一步,做一个技巧......让我们看一个简单的 Dockerfile 的示例:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

让我们根据以下原则从中构建一个文件:

  • 来自所用图像标识符的 SHA256 (ruby:2.3 и nginx:alpine),这是其内容的校验和;
  • 所有球队(RUN, CMD 等等。);
  • 来自添加的文件的 SHA256。

...并从这样的文件中获取校验和(同样是 SHA256)。 这 签名 定义 Docker 镜像内容的所有内容。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

让我们回到图表 我们将使用这样的签名来代替提交, IE。 用签名标记图像。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

现在,例如,当有必要将更改从发布版本合并到主版本时,我们可以进行真正的合并提交:它将具有不同的标识符,但具有相同的签名。 使用相同的标识符,我们将把镜像投入生产。

缺点是现在无法确定哪种提交被推送到生产环境 - 校验和仅在一个方向起作用。 这个问题可以通过使用元数据的附加层来解决 - 我稍后会告诉你更多信息。

在 werf 中标记

在 werf 中,我们走得更远,准备使用不存储在一台机器上的缓存进行分布式构建......因此,我们正在构建两种类型的 Docker 镜像,我们称之为 阶段 и 图片.

werf Git 存储库存储特定于构建的指令,这些指令描述了构建的不同阶段(安装前, 安装, 安装前, 格局)。 我们收集第一阶段图像,其签名定义为第一步的校验和。 然后我们添加源代码,对于新的阶段图像,我们计算其校验和...对所有阶段重复这些操作,结果我们得到一组阶段图像。 然后我们制作最终图像,其中还包含有关其来源的元数据。 我们用不同的方式标记这个图像(稍后详细介绍)。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

假设此后出现一个新的提交,其中仅更改了应用程序代码。 会发生什么? 对于代码更改,将创建补丁并准备新的阶段映像。 其签名将被确定为旧阶段图像和新补丁的校验和。 将从该图像形成新的最终图像。 其他阶段的变化也会发生类似的行为。

因此,阶段镜像是一个可以分布式存储的缓存,已经创建的镜像会上传到 Docker 注册表。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

清理注册表

我们不是在谈论删除删除标签后仍然挂起的层 - 这是 Docker 注册表本身的标准功能。 我们讨论的是这样一种情况:大量 Docker 标签积累起来,我们知道我们不再需要其中一些标签,但它们占用了空间(和/或我们为此付费)。

清洁策略有哪些?

  1. 你可以什么也不做 不清洁。 有时,花一点钱购买额外的空间确实比解开一大堆标签更容易。 但这仅在一定程度上有效。
  2. 完全重置。 如果删除所有映像并仅重建 CI 系统中的当前映像,可能会出现问题。 如果容器在生产中重新启动,则会为其加载一个新映像 - 尚未经过任何人测试的映像。 这扼杀了不可变基础设施的想法。
  3. 蓝绿。 一个注册表开始溢出 - 我们将图像上传到另一个注册表。 与上一种方法相同的问题:什么时候可以清除已经开始溢出的注册表?
  4. 按时间。 删除所有超过 1 个月的图像? 但肯定会有一个月没有更新的服务……
  5. 手动 确定哪些内容已经可以删除。

有两个真正可行的选择:不清洗或者蓝绿+手动的组合。 在后一种情况下,我们讨论的是:当您知道需要清理注册表时,您可以创建一个新注册表,并在例如一个月的时间内向其中添加所有新映像。 一个月后,查看 Kubernetes 中哪些 Pod 仍在使用旧注册表,并将它们也转移到新注册表。

我们到了什么程度 韦尔夫? 我们收集:

  1. Git head:所有标签,所有分支 - 假设我们需要图像中 Git 中标记的所有内容(如果没有,那么我们需要在 Git 本身中删除它);
  2. 当前输出到 Kubernetes 的所有 Pod;
  3. 旧的 ReplicaSet(最近发布的),我们还计划扫描 Helm 版本并选择其中的最新映像。

...并从该集合中创建一个白名单 - 我们不会删除的图像列表。 我们清理其他所有内容,然后找到孤立的舞台图像并将其删除。

部署阶段

可靠的声明性

我想在部署中引起注意的第一点是更新的资源配置的推出,以声明方式声明。 描述 Kubernetes 资源的原始 YAML 文档总是与集群中实际运行的结果有很大差异。 因为 Kubernetes 添加了配置:

  1. 身份标识;
  2. 服务信息;
  3. 许多默认值;
  4. 当前状态的部分;
  5. 作为准入 Webhook 的一部分进行的更改;
  6. 各种控制器(和调度程序)的工作结果。

因此,当出现新的资源配置时(),我们不能只用它来覆盖当前的“实时”配置(生活)。 为此,我们必须比较 与最后应用的配置(最后申请的)并滚到 生活 收到补丁。

这种方法称为 三路合并。 例如,它被用在 Helm 中。

也有 三路合并,其不同之处在于:

  • 比较 最后申请的 и ,我们看看删除了什么;
  • 比较 и 生活,我们查看添加或更改的内容;
  • 求和补丁应用于 生活.

我们使用 Helm 部署了 1000 多个应用程序,因此我们实际上采用的是 2 路合并。 不过,它有一些问题,我们已经通过补丁解决了,这有助于 Helm 正常工作。

真实推出状态

我们的 CI 系统根据下一个事件为 Kubernetes 生成新的配置后,将其传输以供使用 (申请) 到集群 - 使用 Helm 或 kubectl apply。 接下来,发生已经描述的 N 路合并,Kubernetes API 向 CI 系统及其用户做出满意的响应。

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

然而,有一个很大的问题:毕竟 申请成功并不代表上线成功。 如果 Kubernetes 了解需要应用哪些更改并应用它,我们仍然不知道结果会是什么。 例如,在前端更新和重新启动 Pod 可能会成功,但在后端则不会成功,并且我们将获得正在运行的应用程序映像的不同版本。

为了正确地完成所有工作,该方案需要一个额外的链接 - 一个特殊的跟踪器,它将从 Kubernetes API 接收状态信息并将其传输以进一步分析事物的真实状态。 我们用 Go 创建了一个开源库 - 立方体狗 (见其公告 这里),它解决了这个问题并内置于 werf 中。

该跟踪器在 werf 级别的行为是使用放置在 Deployments 或 StatefulSet 上的注释进行配置的。 主要注释- fail-mode - 理解以下含义:

  • IgnoreAndContinueDeployProcess — 我们忽略推出该组件的问题并继续部署;
  • FailWholeDeployProcessImmediately — 该组件中的错误会停止部署过程;
  • HopeUntilEndOfDeployProcess - 我们希望该组件能够在部署结束时正常工作。

例如,资源和注释值的这种组合 fail-mode:

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

当我们第一次部署时,数据库(MongoDB)可能还没有准备好——部署将会失败。 但您可以等待它启动,部署仍然会进行。

werf 中对 kubedog 的注释还有两个:

  • failures-allowed-per-replica — 每个副本允许的跌落次数;
  • show-logs-until — 调节 werf 显示(在标准输出中)所有已推出 pod 的日志的时间。 默认为 PodIsReady (当流量开始进入 Pod 时忽略我们可能不想要的消息),但值也是有效的: ControllerIsReady и EndOfDeploy.

我们还想从部署中得到什么?

除了已经描述的两点之外,我们还希望:

  • 去看看 日志 - 只有必要的,而不是连续的所有;
  • 追踪 进度,因为如果作业“无声地”挂起几分钟,了解那里发生的情况很重要;
  • 自动回滚 以防出现问题(因此了解部署的真实状态至关重要)。 推出必须是原子的:要么进行到最后,要么一切都返回到之前的状态。

结果

对于我们公司来说,要在交付的不同阶段(构建、发布、部署)实现所有描述的细微差别,CI 系统和实用程序就足够了 韦尔夫.

而不是结论:

werf - 我们在 Kubernetes 中的 CI / CD 工具(概述和视频报告)

在 werf 的帮助下,我们在为 DevOps 工程师解决大量问题方面取得了良好进展,如果更广泛的社区至少尝试一下这个实用程序,我们将很高兴。 在一起的话会更容易取得好的结果。

视频和幻灯片

表演视频(约 47 分钟):

报告介绍:

PS

我们博客上有关 Kubernetes 的其他报道:

来源: habr.com

添加评论