我们迁移到容器。还不是一帆风顺

2021-07-30 20:46:44

几周前,我们发布了最新的产品版本以及由容器技术和 Docker 提供支持的 Artifakt 平台的新任意应用功能。在过去的几年里,Artifakt 一直专注于 PHP 堆栈。但是 PHP 并不是 Web 应用程序的唯一语言。借助新的 Docker 集成,我们有远大的计划!根据应用程序打包的事实标准重新构建我们的 PaaS,对于各种形式和规模的开发团队来说都是个好消息。您会在此版本中发现许多好处,现在是加入容器友好型 PaaS 的最佳时机——基础设施即代码、更改可追溯性、运行时性能、客户价值。更不用说 Docker 是敏捷性、反脆弱性和上市时间等核心 DevOps 原则的最佳朋友。但所有的旅程,无论结果如何,都常常伴随着挣扎。在本文中,我想深入探讨我们在 Docker 迁移过程中面临的挑战以及我们吸取的教训。如果您希望迁移到容器,请阅读一本书——我相信您会找到至少一个相关的观点,也许我们处理某些问题和挑战的方式可以帮助您避免潜在的错误。 PaaS 解决方案非常方便。我相信任何从本地或 IaaS 迁移其应用程序的人都会同意。

这种便利水平的一部分是使用虚拟机 (VM)。不错的软件,非常可靠的抽象,几十年来的安全和成熟。现在没有人离不开它们,因此它们是非常必要的。尽管如此,这还不够。虚拟机的阴暗面是它们往往会成长为私人宠物。日常维护、定期支持和偶尔的生命支持需要大量的固定成本以及人力援助。现在,对于痛苦的现实:虚拟机不再适用于云基础架构。当前的条件对它们来说很苛刻,可能会适用严格的法规,虽然技术格局不断发展,但虚拟机仍然建立在与 20 年前相同的原则上。您应该始终牢记所有可能出错的场景并做好准备。让我们来看看在容器迁移中可能会出错的几件事情。我们以前听说过,速度是一切战斗之母。不仅在战略上,而且从构思到执行也是如此。变化需要快速发生,跟上生态系统的快速发展。正如通用电气前首席执行官杰克韦尔奇所说:不是大吃小,而是快吃慢。一个执行缓慢的好主意意味着死刑。具有良好执行力的坏主意意味着您仍然可以调整和试验并关闭成功的市场契合点。如果您从未听说过“但是……它可以在我的笔记本电脑上运行!”,请举手。在一个软件项目中——我怀疑我会看到很多人举手。当然,我们拥有基础设施即代码、持续集成和所有现代机器。不要误会我的意思,筒仓内部创建了局部优化,它们很有用。真正的交易是打破孤岛,让团队达到足够高的成熟度,这样“他们构建它,他们运行它”。

我们不希望 PaaS 成为新的“现在的运营问题”并说“现在的支持问题”。还记得“灾难女孩”模因吗?这是一个筒仓综合症出了问题!而 BlackOps 更危险,你不希望开发团队自己运行容器并在防火墙中戳洞。容器是上述所有挑战的答案——一个成熟的,甚至乏味的软件打包标准。但并非总是如此。早在 20 世纪,全球化就可以通过一个真正具体的物质变化成为可能:一个用于运输汽车、食物等的通用盒子。这只是实现了我们今天所知的“多式联运”。自 2014 年以来,软件容器在 Docker Inc. 旗下的事实上的解决方案中迅速成熟,并带有一个简单而有效的承诺:“无论底层执行系统是什么,Docker 都可以使用完全相同的代码逐字节运行它。”容器已经在 Artifakt 的引擎盖下运行了好几个月——即使是有状态的东西,我们敢说。我们现在可以在几秒钟内运行不同的配置更改,而不是 10 到 30 分钟的 VM 配置。在我们的下一个主要控制台版本中,Artifakt 将容器作为部署单元公开。

您可以想象 Docker 迁移对我们日常工作的开创性影响。编排 VM 需要与我们的云提供商在某些专有技术上进行强耦合。对于 AWS,它是 CloudFormation 和 OpsWorks。我们花了很多时间来了解云的复杂性,这样我们心爱的客户就不必这样做了。因此,宏伟的重写为我们提供了开源和开放格式:Dockerfile、docker-compose.yaml 和稳定的 Docker API。如何在笔记本电脑上运行完全相同的 Magento 2 堆栈并将其投入生产?这现在在 Artifakt 中是可能的。自 7 月初发布 Stack v5 以来,Magento 2 是我们正式支持的九个运行时的一部分。在许多方面,此版本将所有挑战集中在一个地方:让我们看看我们如何克服这些挑战以及这给我们留下了什么。旅程从经典的 Docker 优点开始。我们已经被 PaaS 环境中容器化的好处所吸引。有些方面真的很容易实现。成熟度足够高,生态系统正在蓬勃发展,云提供商已经在铺平道路几年了。我们不能经历所有的快速胜利,许多在 2021 年会直接枯燥乏味,所以让我专注于最有启发性的。

Argo Workflows 是实现反应式 GitOps 管道的绝佳工具。这是一口,所以让我们先分解它。 GitOps 使团队能够从 Git 进行和执行更改,而不仅仅是代码更改。我们谈论的是基础设施、网络、存储等。曾经创建、升级或退役的机器的每一部分都可以链接回 Git 提交。感觉非常激动?我们知道我们是!我们的工作流很好地隐藏在 Argo 中,并为许多不同的堆栈和语言运行我们的基本操作,例如部署作业。我们组织内的每个人都可以访问过去的工作流程及其日志,从而轻松进行故障排除。日志不再被分成多层复杂的碎片,我们现在受益于一个共享的立场,客户支持和工程团队可以在其中帮助客户衡量和优化他们的流程。未来,我们也期待着尝试 Argo CD 以及它为像 Artifakt 这样的 PaaS 产品提供的许多机会。事实证明,Docker 镜像有很多出错的地方。由于自动化测试是一种很好的代码实践,同样的原则也适用于 Dockerfiles。官方文档已经保证了 Dockerfiles 的正确性:所有命令都应该返回 0 否则构建将失败。我们需要在这里添加两个维度:有效性和内容。有效性确保我们在编写 Dockerfile 和语义内容检查时有正确的风格。我们可以仅使用本机 Dockerfile 指令来完成所有操作吗?差不多,但即便如此,它也会导致具有许多非生产层的膨胀图像,然后进入多阶段构建等。

换句话说,如果构建成功,它现在必须以正确的数量和数量加载正确的软件。因此,我们选择遵循关注点分离的原则,在测试、验证和语义检查之间划清界限。对于简单的语法 linting,我们使用了 hadolint,它是 Dockerfiles 的一个特定的 linter——它足够强大但易于使用,与 CI/CD 很好地集成,并得到积极维护。当然,它本身就存在于 Docker 镜像中!让我们看一下基本选项: hadolint - 用 HaskellUsage 编写的 Dockerfile Linter: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME] [- V|--verbose] [-f|--format ARG] [DOCKERFILE...] [--error RULECODE] [--warning RULECODE] [--info RULECODE] [--style RULECODE] [--ignore RULECODE ] [--trusted-registry REGISTRY(例如docker.io)] [--require-label LABELSCHEMA(例如维护者:文本)] [--strict-labels] [-t|--failure-threshold THRESHOLD] [-- file-path-in-report FILEPATHINREPORT] 用于错误和最佳实践的 Lint Dockerfile 从默认的帮助屏幕开始,我们受到了一系列不错的选项和示例的欢迎:检查所需的选项,以不同格式输出报告,声明一个私人可信注册表等。在使用选项并找到正确的平衡之后,对经过测试的 Dockerfile 进行必要的修复。为了演示 hadolint 有多好,让我们看一下来自 Docker 基础镜像的这个简单命令:

Hadolint 然后通过引用确切的行,继续为您提供具有严重程度(从样式到错误)的建议列表,这有多整洁?例如:-:16 DL4006 警告:在带有管道的 RUN 之前设置 SHELL 选项 -o pipefail。如果您在 alpine 映像中使用 /bin/sh,或者如果您的 shell 符号链接到 busybox,则考虑将您的 SHELL 显式设置为 /bin/ash,或禁用此检查:17 DL3008 警告:apt get install 中的固定版本。代替 `apt-get install <package>` 使用 `apt-get install <package>=<version>`-:17 DL3059 信息:多个连续的 `RUN` 指令。考虑合并。官方 repo wiki 中引用和解释了所有规则,70 和计数。它们包括非常基本的检查(“不要使用 apt-get 升级”)到高度专业化的用例(“纱线安装运行后纱线缓存清理丢失”)。在这个片段中,我们告诉 hadolint 扫描我们的 Dockerfile,通过检查强制性标签作者,除了规则 DL4006,最后,只有在反馈包含警告时才会失败,从而忽略样式和信息级别。对于开发人员来说,最后一点,hadolint 还支持 Dockerfile 内的内联 #ignore 注释,从而可以更轻松地在一组 Dockerfile 上使用相同的命令。总而言之,得出一个结论,这为我们的测试提供了一个良好的开端。稍后,当我们准备好应用更高级别的良好实践时,我们可以降低要注意的错误阈值。拥有同时在语法上正确且遵循良好实践的 Docker 镜像是很棒的。现在,我们还需要检查内容的有效性。毕竟,PaaS 产品应该有一套应用的内部规则。

为此,我们使用了另一个漂亮的工具,Google 自己的 container-structure-test。它是 Github 中 GoogleContainerTools 命名空间的一部分,它还托管一些您可能已经知道和使用的著名项目:Skaffold、distroless、Jib 和 Kaniko。 Container-structure-test 是一个开源项目,它读取测试套件并对照它们检查现有的 Docker 镜像。它有一个命令:test(你期望什么?)。请注意,这与 hadolint 不同,其中纯文本 Dockerfile 就足够了。 Container-structure-test 需要一个二进制工件:Docker 镜像,或导出为 .tar 文件。像 hadolint 一样,测试是用纯 YAML 编写的,这似乎是云生态系统中第二有用的技能(仅次于 Git,对吧?)。这是一个有趣的 YAML 声明性测试示例: schemaVersion: '2.0.0'metadataTest: labels: - key: 'vendor' value: 'Artifakt' - key: 'author' value: "^\\w+([-+ .']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$" isRegex: true 卷: [] 入口点: [ "/usr/local/bin/docker-entrypoint.sh"]fileExistenceTests:- name: 'bash' path: '/bin/bash' shouldExist: true 权限: '-rwxr-xr-x' uid: 0 gid: 0 isExecutableBy: 'any'commandTests: - name: "debian based server" command: "which" args: [ "apt-get"] expectedOutput: ['/usr/bin/apt-get'] 在这个块中,我们测试了一个元数据、文件和命令结果的图像,分别由三个主要键 metadataTest、fileExistenceTests 和 commandTests 定义。但是为什么容器结构测试需要预先存在的构建步骤?因为,在幕后,它在实时容器上使用 docker exec 命令来运行每个测试。最重要的是,测试也可以保证是隔离的,因为一个容器只运行一个测试。当您使用 --save 选项运行测试以保留容器时,此行为很容易看到。让我们在我们的官方镜像之一上运行以下测试: caae8bd3159c 678b262bc2d6 "NOOP_COMMAND_DO_NOT..." 2 分钟前创建了实用_einstein3b63b4f5fcd1 registry.artifakt.io/sylius:1.10-apache"NOOP_262bc2d6 "NOOP_COMMAND_DO_NOT..." 2 分钟前退出 (0) 2 分钟前 bold_tesla6e87be191af3 registry.artifakt.io/sylius:1.10-apache "NOOP_COMMAND_DO_NOT..." 2 分钟前创建了angered_bouman

当您想使用 docker 日志或 docker diff 检查保存的容器以及它们如何受到影响时,这非常有用。使用最后一个命令,我们可以轻松编写测试以确保我们的容器足够无状态或不会泄漏意外文件,或对活动容器进行任何其他评估。正如我们在第一个示例中已经提到的,container-structure-test 评估了 3 类开箱即用的检查:图像元数据(标签、卷、入口点等)、文件存在,然后是任意命令结果。除了测试存在性之外,我们还编写测试来检查最终 Docker 映像中我们不想要的内容。想想可能存在且在生产中绝对不受欢迎的开发包、编译器、工具。防止这种情况的一种方法是这样的:最后,另一个有用的选项是将 setup 关键字与测试命令结合使用。有些测试只有在 Docker 入口点运行后才有意义。我们为此使用设置密钥。请参阅下面的示例: - 名称:“检查已安装的文件夹私有”设置:[[“/usr/local/bin/docker-entrypoint.sh”]] 命令:“ls”参数:[“-la”,“/opt” /drupal/web/sites/default/private"] 预期输出:[ 'lrwxrwxrwx 1 www-data www-data .+ /opt/drupal/web/sites/default/private -> /data/web/sites/default/private ' ] 官方文档和高级案例中还有更多功能,例如测试守护程序(哎呀!),所以我鼓励您深入研究它。在容器化的道路上没有预料到许多挑战,因此我们必须分享其中的一些,因为我们获得了宝贵的见解。

我们最完整的运行时 Magento 2 和 Akeneo 是 cron 作业的重度用户:索引、缓存、图像大小调整、导入/导出,应有尽有。 Docker 如何处理异步间歇性进程?其实不好在 Docker 101 中众所周知,不应在与主进程相同的容器中运行 cron。首先,Docker 只是升级了他们的 Swarm 编排层来运行 cron 作业,就像 Kubernetes 一样。这可行,但 Swarm 不在我们最初的路线图上。其次,我们可以在节点级别使用 cron 守护程序为每个 cron 作业运行额外的容器。这种方法有其优点和缺点。受时间和日程的限制,我们不得不加快步伐。最后,我们可以声明将 crontab 保留在节点级别,并使用 docker exec 将命令运行到实时容器中。这是可行的,因为我们仍然在每台服务器上运行一个应用程序容器,所以现在是有意义的。我们选择了最后一个选项,结果简单、优雅,并且尊重我们热爱的 Linux 精神。以下是将 cron 作业注入实时容器的三个简单步骤: 编写 docker exec 包装器,其中 2 行代码足以定位 <cID> 容器。

2021-07-28T13:55:02.338665047Z 容器 exec_create: sh -c uptime > /tmp/uptime.txt a360afb6e (artifakt.io/image=616787838396.dkr.ecr.eu-west.com.com/ execID = 8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e,图像= 616787838396.dkr.ecr.eu-west-1.amazonaws.com / sylius:1.10,卖主= Artifakt)2021-07-28T13:55:02.339417333Z容器exec_start:SH -c正常运行时间>的/ tmp /uptime.txt a360afb6e(artifakt.io/image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius,execID = 8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e,图像= 616787838396.dkr.ecr.eu-西1.amazonaws。 com/sylius:1.10, vendor=Artifakt)2021-07-28T13:55:02.822814763Z 容器 exec_die a360afb6e (artifakt.io/image=616787838396.dkr.ecr.eu-awyls.com=exec_die a360afb6e 8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e,exitCode=0,image=616787838396.dkr.ecr.eu-west.com=这个完全相同的执行环境w.ifs1awyts.com.ifsamazon-1。将资源使用情况保持在一个容器内,确保整体稳定性。好吧,移除一个工作多年的遗留层几乎是不可能的。所以,老实说,我们并没有完全成功。然而,我们尽了最大努力,设法使这一层尽可能无聊和无关紧要。从 OpsWorks 到 Docker 的转变需要将平台规则从说明书中移到 docker-compose YAML 和 bash 脚本中。结果是我们所有部署的唯一分支。快进到数十次提交,经过多次试验和错误之后,OpsWorks 现在所做的就是安装 Docker 引擎和一些容器依赖项。其他一切都由 Docker API 操作,由普通的旧 OpsWorks 作业触发,比以往任何时候都更可预测。当我们接近发布时间时,我们的 CEO 看了我们的演示并说:“嘿,如果开发人员可以使用 HTTPS 在本地运行他们的应用程序,那就太好了”。作为开发人员,团队中的每个人都立即意识到了这个机会。

工作站离生产越近,我们发送的错误就越少。这是表达 12 要素应用程序第 10 章的另一种方式:“dev/prod parity” 这是我们如何做到的。只需要两个额外的组件(容器!):Nginx-proxy 和 Cert 伴侣。我们用它们修补了原始的 docker-compose.yaml,除此之外没有任何代码修改:版本:'3'服务:代理:图像:jwilder/nginx-proxy container_name:base-wordpress-proxy restart:always ports:-“ 8000:80" - "8443:443" 卷: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs proxy-companion: image: nginx-proxy -companion:latest restart: always environment: - "NGINX_PROXY_CONTAINER=base-wordpress-proxy" 卷: - /var/run/docker.sock:/var/run/docker.sock:ro - ./certs:/etc/nginx /certs 最后,重要的是我们的应用程序容器还有两个环境。变量使这个设置工作,所以我们像这样添加它们:在第一次运行时,nginx 伴侣会查找 ca.cert 并在运行时为您生成它,如果它不存在。然后我们必须告诉浏览器像任何其他 CA 一样信任这个 ca.cert。遗憾的是,这仍然需要手动设置。我们尝试了,环顾四周,并摆弄着 Let's Encrypt — 没有任何解决方案是开箱即用的。您不能使用 Let's Encrypt 作为 CA 来提供本地主机证书。最后,需要一些额外的步骤,通常会弄乱本地根证书。这是我们找到的开发人员安装本地证书颁发机构一次并在所有本地开发堆栈上使用的最短路径。请注意,以下步骤仅适用于 Google Chrome。

我们还有很多方法可以让一个好的平台变得更好。以下是我们正在考虑的后续步骤。数据很难,我们严重依赖 AWS 持久数据。这已经为弹性和可扩展性创造了奇迹。我们的客户需要他们的数据安全和关闭。另一方面,开发人员更喜欢便利性和易用性。由于速度是一种竞争优势,我们正在朝着更快的部署迈进。 Docker 镜像 c ......