记录我们迁移到 Docker 的挑战和经验教训

几周之前,我们宣布了最新的产品发布,以及由容器技术和 Docker 支持的 Artifakt 平台的全新的任意 App 功能。

在过去几年中,Artifakt 一直专注于 PHP 栈。但 PHP 并不是 Web 应用程序的唯一语言。通过使用 Docker 集成,我们提前完成了宏伟的计划!

基于应用程序打包的事实标准来重新调整我们的 PaaS,对于各种形式和规模的开发团队来说都是一个好消息。

在这个版本中,你会发现许多额外的功能,在代码、更改可跟踪性、运行时性能和客户价值等方面,现在是加入容器友好型 PaaS 基础设施的最佳时机。更不用说,Docker 是 DevOps 核心原则(敏捷性、抗敏捷性和上市时间)的最好朋友。

但所有的旅程,无论结果如何,通常都伴随着奋斗。在本文中,我想要深入讨论我们在迁移到 Docker 的过程中所面临的挑战和我们学到的经验教训。

如果你想要迁移到容器,请读一读本文——我相信你至少会找到一点相关信息,也许我们处理某些问题和挑战的方案可以帮助你避免一些潜在的错误。

问题:软件工程中没有免费的午餐

PaaS 解决方案非常方便。我相信任何从本地部署服务或 IaaS 迁移其应用程序到 PaaS 的人都会同意。

其中很方便的一点是虚拟机(Virtual Machines,VM)的使用。这是一款很好的软件,几十年来至今,都非常安全和成熟。现在没有人做事情能离得开它们,所以它们是非常必要的。尽管如此,这还不够。

虚拟机的阴暗面是,它们倾向于作为私人宠物成长。日常维护、定期支持和偶尔的急救支持需要大量的固定成本以及人为协助。

现在,面对痛苦的现实:虚拟机已经不再适应云基础设施。目前的条件对它们来说是苛刻的,可能采用严格的规章制度。尽管科技领域不断发展,但虚拟机仍然建立在 20 年前相同的原则上。

放大:哪里会出问题

你应该时刻记住所有可能出错的情况并做好准备。让我们看一看容器迁移过程中可能出现的几个问题。

不良后果 #1:速度不够快

我们以前都听过,速度是所有战斗之母。不仅是在战略上,而且从构思到执行都是如此。变化需要快速发生,跟上生态系统的快速进化。正如通用电气前 CEO 杰克·韦尔奇(Jack Welch)所说:“如果外部变化的速度超过内部变化的速度,终结就不远了。”


不是大吃小,而是快吃慢。拥有好主意,但是执行慢,意味着死刑。拥有糟糕的主意,但是执行良好,意味着你仍然可以调整和实验直到适应市场获得成功。

不良后果 #2:缺乏自信

如果你在一个软件项目中听过“但是……它在我的笔记本电脑上可以运行!”,请举手——我怀疑我会看到很多人举手。当然,我们有代码、持续集成和所有现代机器等基础设施。别误会,有一些本地优化确实非常有用。但真正有用的是打破局限,将团队提升到足够高的成熟度水平,以便“谁构建谁运行”。

我们不希望 PaaS 成为新的“运维问题”和“支持问题”。还记得“灾难女孩”咒语吗?黑暗运维更加危险,你不希望开发团队自己运行容器,并在你的防火墙上戳洞

是什么触发了变革的需要以及我们向 Docker 的迁移?

容器是上述所有挑战的答案——一个成熟、甚至枯燥的软件打包标准。但情况并非总是如此。

回想 20 世纪,全球化是通过一个非常具体的物质变化实现的:一个通用的箱子来移动汽车、食品等等。这就是我们今天所知道的“多式联运(inter-modal transportation)”。


图片来源:Docker Inc.

自 2014 年以来,软件容器在 Docker Inc.公司旗下的事实解决方案中迅速成熟,并有一个简单却有效的承诺:

“无论底层执行系统是什么,Docker 都可以使用完全相同的代码逐字节运行它。”


图片来源:Docker Inc.

我们敢说,在 Artifakt,容器已经在后台运行了好几个月,即使是有状态的东西。我们现在可以在几秒中内运行不同的配置更改,而不是需要 10 到 30 分钟的虚拟机配置。

在我们的下一个主要控制台版本中,Artifakt 将容器作为部署单元公开。

转变与见证:我们如何让 Magento 2 更加闪亮

你可以想象 Docker 迁移对我们日常工作的开创性影响。协调虚拟机需要与我们的云提供商在某种专有技术上进行强耦合。对于 AWS 来说,是 CloudFormation 和 OpsWorks。我们花了很多时间消化云的复杂性,这样我们亲爱的客户就不必这么做了。

最终,出色的重写让我们拥有了开源和开放格式:Dockerfile、docker-compose.yaml 和稳定的 Docker API。

在笔记本上运行完全相同的 Magento 2 栈并将其投入生产如何?这在 Artifakt 是可能的。

Magento 2 是自 7 月早些时候发布Stack v5以来我们正式支持的九个运行时的一部分。在许多方面,这个发布版本将所有挑战集中在一个地方:

  • crontab 管理

  • 容器测试

  • 部署过程

  • ISO 生产环境本地堆栈

让我们来看看我们是如何克服这些挑战的,以及这将给我们带来什么。Docker 迁移部分 I: 好的方面先从 Docker 的好处开始。我们已经意识到在 PaaS 环境中容器化的好处。有些方面真的很容易实现。成熟度足够高,生态系统蓬勃发展,云供应商已经为未来几年铺平了道路。

我们不可能一一讲述所有的好处,很多好处在 2021 年讲都会很无聊,所以让我把重点放在最有启发性的事情上。

好处 #1:使用 Argo 的工作流引擎

Argo Workflows是实现响应式 GitOps 管道的一个非常好的工具。要讲的有很多,让我们拆开来一个个讲。GitOps 使团队能够执行 Git 中的更改,而不仅仅是代码更改,比如基础设施、网络、存储等。机器的每一个部分,创建、升级或停用,都可以链接到一个 Git 提交。

感觉兴奋吗?我们确实很兴奋!我们的工作流很好地隐藏在 Argo 中,可以为许多不同的堆栈和语言运行部署任务等基础操作。

我们组织中的每个人都可以访问过去的工作流及其日志,从而轻松解决问题。我们现在受益于一个共享的平台,客户支持和工程团队可以帮助客户测量和优化他们的流程,而不是将日志分散在许多复杂的层次上。

在未来,我们也期待着尝试 Argo CD 以及它为像 Artifakt 这样的 PaaS 产品提供的许多机会。

好处 #2:在容器中格式化和测试

事实证明,Docker 镜像有很多出错的方式。由于自动测试在代码中是一种很好的实践,同样的原则也适用于 Dockerfiles。官方文档已经确保了 Dockerfile 的正确性:所有命令都应该返回 0,否则构建将失败。

我们需要在这里增加两个维度:有效性和内容。有效性确保我们在编写 Dockerfile 和语义内容检查时具有正确的风格。

我们可以只使用本机 Dockerfile 指令完成所有操作吗?几乎可以,但是即使如此,它也会导致包含许多非生产层的膨胀镜像,然后进入多阶段构建,等等。

换句话说,如果构建成功,它必须装载恰当数量的正确软件。

因此,我们选择遵循关注点分离的原则,并在测试、验证和语义检查之间划清界限。

对于简单的语法格式化,我们使用 hadolint,这是针对 Dockerfiles 的一种格式化工具。它功能强大,很容易上手,可以与持续集成/持续部署很好地集成,并得到了积极的维护。当然,它本身就存在于一个 Docker 镜像中!让我们看一下基本选项:

hadolint - Dockerfile Linter written in Haskell




Usage: 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 (e.g. docker.io)]
            	[--require-label LABELSCHEMA (e.g. maintainer:text)]
            	[--strict-labels] [-t|--failure-threshold THRESHOLD]
            	[--file-path-in-report FILEPATHINREPORT]
  Lint Dockerfile for errors and best practices

在默认的帮助屏幕上,我们可以看到一系列不错的选项和示例:检查所需选项、以不同格式输出报告、声明私有的受信任的注册表等。

要在持续集成中包含 hadolint ,我们建议以下三个步骤:

  1. 使用–no-fail 调用 hadolint ,并在不中断当前流的情况下安全地评估结果。

  2. 在使用选项并找到恰当的平衡点后,对测试的 Dockerfile 进行必要的修复。

  3. 删除–no-fail 选项,并将 hadolint 作为强制步骤启用。

为了演示 hadolint 有多好,让我们看一下我们的 Docker 基础镜像中的这条简单命令:

$ docker run –rm -i hadolint/hadolint:v2.6.0 hadolint – < ./akeneo/5-apache/Dockerfile

接着会给出一个附带严重程度(从 style 到 error)的建议列表,附带有具体的行号,非常整洁。例如:

-:16 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
-:17 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
-:17 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.

所有规则,70 多条,都在官方库的wiki中有参考和解释。它们包括非常基础的简单检查(“不要使用 apt-get upgrade”)到高度专业化的用例(“运行 yarn install 之后,没有运行 yarn cache clean”)。

结合在一起,hadolint 选项更加强大。看看这个命令:

docker run –rm -i hadolint/hadolint:v2.6.0 hadolint –require-label author:text –ignore=DL4006 –failure-threshold=warning – ./Dockerfile

在这段代码中,我们告诉 hadolint 去扫描我们的 Dockerfile 文件,通过检查一个强制标签 author ,除了 DL4006 规则,最后,只有在反馈包含警告时才会失败,从而忽略 style 和 info 级别。

对于开发人员来说,hadolint 还支持 Dockerfile 中的 #ignore 内联注解,这使得在一组 Dockerfile 文件上使用相同的命令更容易。

总而言之,这为我们的测试提供了一个良好的开端。稍后,当我们准备好应用更高级别的良好实践时,我们可以降低错误阈值到 notice 。

拥有语法正确且遵循良好实践的 Docker 镜像是非常棒的。现在,我们还需要检查内容的有效性。毕竟,PaaS 产品应该有一套应用的内部规则。

为此,我们使用了另一个很棒的工具——谷歌的container-structure-test。它是 Github 上 GoogleContainerTools 命名空间的一部分,在这个命名空间下还托管了一些你可能已经知道或使用过的著名项目:Skaffold、distroless、Jib 和 Kaniko。

Container-structure-test 是一个开源项目,读取测试套件并对照现有的 Docker 镜像进行检查。它有一个命令:test 。注意,这与 hadolint 不同,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
  volumes: []
  entrypoint: ["/usr/local/bin/docker-entrypoint.sh"]




fileExistenceTests:
- name: 'bash'
  path: '/bin/bash'
  shouldExist: true
  permissions: '-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 三个主键定义。

但是为什么 container-structure-test 需要一个预先存在的构建步骤呢?因为,在后台,它在活跃容器上使用 docker exec 命令来运行每一个测试。最重要的是,由于一个容器只能运行一个测试,所以测试也保证是隔离的。

当你运行测试时使用–save 选项来保持容器,这个行为就很容易查看。让我们在一个官方镜像上运行如下测试:

container-structure-test test –image registry.artifakt.io/sylius:1.10-apache –save –config ./global.yaml

检查保存的容器:

caae8bd3159c    	678b262bc2d6                          	"NOOP_COMMAND_DO_NOT…"   2 minutes ago   	Created                                    	practical_einstein
3b63b4f5fcd1    	registry.artifakt.io/sylius:1.10-apache   "NOOP_COMMAND_DO_NOT…"   2 minutes ago   	Created                                    	admiring_solomon
44497e1d88cf    	d10ef6baba46                          	"which apt-get"      	2 minutes ago   	Exited (0) 2 minutes ago                   	bold_tesla
6e87be191af3    	registry.artifakt.io/sylius:1.10-apache   "NOOP_COMMAND_DO_NOT…"   2 minutes ago   	Created                                    	angry_bouman

当你想使用 docker logs 或 docker diff 检查保存的容器以及它们如何受影响时,这非常有用。使用最后一个命令,我们可以轻松编写一个测试来确保我们的容器是足够无状态的,或者不会泄露意料外的文件,或者对活跃容器进行任何其它评估。

正如我们在第一个示例中已经提到的,container-structure-test 评估三类开箱即用的检查:镜像元数据(labels、volumes 和 entrypoints 等等)、文件是否存在、任意命令结果。

在测试是否存在的基础上,我们还编写测试来检查最终 Docker 镜像中我们不需要的内容。想想开发包、编译器和工具,它们可能到处都是,在生产环境肯定不受欢迎。防止这种情况的一种方法是:

fileExistenceTests:
- name: 'forbid gcc'
  path: '/usr/bin/gcc'
  shouldExist: false

最后,另一个有用的选项是将 setup 关键词与测试命令结合使用。有些测试只在 Docker 入口点运行之后才有意义。我们使用 setup 键来处理这种情况。请看下面的示例:

- name: "check mounted folder private"
	setup: [["/usr/local/bin/docker-entrypoint.sh"]]
	command: "ls"
	args: [ "-la", "/opt/drupal/web/sites/default/private"]
	expectedOutput: [ 'lrwxrwxrwx 1 www-data www-data .+ /opt/drupal/web/sites/default/private -> /data/web/sites/default/private' ]

官方文档和高级案例中还有很多功能,比如测试守护进程,因此我鼓励你们进行深入研究。

Docker 迁移部分 II:坏的方面(和有趣的方面!)

在容器化的道路上,许多挑战是预料不到的,由于我们获取了一些有价值的见解,因此我们必须分享其中一些挑战。

挑战 #1:crontab 集成

我们最完整的运行时是 Magento 2 和 Akeneo,它们是 cron 任务的重度用户:索引、缓存、镜像大小调整、导入/导出等等。

Docker 对于异步间歇进程处理得怎么样?其实并不太好。Docker 101 中众所周知,你不能在与主进程相同的容器中运行 cron。

那么,有效的替代方案是什么?我们考虑了以下几个方案:

  • Swarm cronjob

  • cron job containers

  • Docker exec bridge

首先,Docker 刚刚升级了 Swarm 编排层来运行 cron 作业,就像 Kubernetes 那样。这可能行得通,但是 Swarm 不在我们最初的路线图上。

其次,我们可以为每个 cron 作业运行额外的容器,在节点级别使用一个 cron 守护进程。这个方法有利有弊。由于时间和计划的限制,我们不得不加快步伐。

最后,我们可以声明将 crontab 保持在节点级别,并使用 docker exec 将命令运行到活跃的容器中。这可能起作用,因为我们仍然在每个服务器上运行一个应用程序容器,所以现在这是有意义的。

我们选择了最后一个选项,结果是简单、优化,并且尊重我们热爱的 Linux 精神。下面是将 cron 作业注入到活跃容器的三个简单步骤:

步骤 1

编写一个 docker exec 包装器,其中实际上有 2 行代码足以指向<cID> 容器。

#!/bin/bash
sudo docker exec -t <cID> sh -c "$2"

步骤 2

将包装器保存在 crontab 用户空间的某个地方(我们将其命名为 dockercron.sh)。

用你的脚本添加/替换默认的 contab SHELL env. var :

### Crontab Managed By Artifakt ###
SHELL=/home/ec2-user/dockercron.sh
5/* * * * * uptime > /tmp/uptime.txt

步骤 3

运行 docker events 并检查 crontab 是否正在调用应用程序容器:

2021-07-28T13:55:02.338665047Z container exec_create: sh -c uptime > /tmp/uptime.txt a360afb6e (artifakt.io/image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius, execID=8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e, image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius:1.10, vendor=Artifakt)
2021-07-28T13:55:02.339417333Z container exec_start: sh -c uptime > /tmp/uptime.txt a360afb6e (artifakt.io/image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius, execID=8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e, image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius:1.10, vendor=Artifakt)
2021-07-28T13:55:02.822814763Z container exec_die a360afb6e  (artifakt.io/image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius, execID=8b14a4e96eb5175c74b689260e1b692af34f22260e9572fbd2bb2c2b663a3c1e, exitCode=0, image=616787838396.dkr.ecr.eu-west-1.amazonaws.com/sylius:1.10, vendor=Artifakt)

这个方案保持了完全相同的执行环境,同时将资源使用保持在一个容器中,保证了总体稳定性。

挑战 #2:取消 OpsWorks

取消一个工作多年的遗留层几乎是不可能的。因此,说实话,我们并没有完全成功。然而,我们尽了最大的努力,设法使这一层尽可能无关紧要。

从 OpsWorks 到 Docker 的转变,需要将移除过去的平台规则,并转移到编写 docker-compose YAML 和 bash 脚本。结果是我们所有的部署都是一个独特的分支。

经过快速的多次提交,并经过许多尝试和错误后,OpsWorks 现在所做的就是安装 Docker Engine 以及屈指可数的一些容器依赖项。

其它一切都是通过 Docker API 操作,由普通的过去的 OpsWorks 作业触发,比以往任何时候都更可预测。

这是简化和技术债务平抑的一个伟大里程碑!

挑战 #3:本地运行 HTTPS

当我们接近发布时间时,我们的 CEO 看了看我们的 demo,说:“嘿,如果开发者可以用 HTTPS 本地运行他们的应用程序,那就太好了”。作为开发者,团队中的每个人都马上理解了这个机遇。

工作站越接近生产环境,我们送出的 bug 就越少。这是表达12因素应用程序第10章——“dev/prod 对等”的另一种方式。

下面是我们如何做到的。只需要 2 个附加组件(容器!):Nginx-proxyCert companion。我们使用它们修改了原始的 docker-compose.yaml ,除此之外没有修改其它代码:

version: '3'
services:
  proxy:
	image: jwilder/nginx-proxy
	container_name: base-wordpress-proxy
	restart: always
	ports:
    	  - "8000:80"
    	  - "8443:443"
	volumes:
    	  - /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"
	volumes:
    	  - /var/run/docker.sock:/var/run/docker.sock:ro
    	  - ./certs:/etc/nginx/certs

最后,为了使这个设置生效,我们的应用程序容器还有两个 env.变量是很重要的,因此我们按如下方式添加了它们:

 app:
	image: base-wordpress:5-apache
	volumes:
  	  - ".:/var/www/html"
	environment:
    	  VIRTUAL_HOST: "localhost"
  	  SELF_SIGNED_HOST: "localhost"

在第一次运行时,nginx 寻找一个 ca.cert ,如果不存在就在线为你生成一个。然后,我们必须告诉浏览器要像信任其它 CA 一样信任这个 ca.cert 。遗憾的是,这仍然需要一次手工设置。

我们尝试了 Let’s Encrypt 等各种方案,但没有开箱即用的解决方案。你不能使用Let’s Encrypt作为一个CA来提供本地主机证书。

最后,还需要一些额外的步骤,通常会弄乱本地的根证书。下面是我们为开发人员找到的一条最短路径,即一次性安装本地证书颁发机构并在所有本地开发堆栈上使用它。请注意,以下步骤仅适用于 Google Chrome。

  1. 打开 Chrome 设置并搜索“certificates”。

  2. 打开 Manage Certificates 菜单,它会弹出密钥链入口。

  3. 在 System 密钥链,打开 Certificate 选项卡,将 ca.cert 从 Nginx-Proxy Companion 中删除。

  4. 双击 Nginx-proxy Companion 证书为 Always Trust。


Docker 迁移部分 III:

前方我们还有很多方法可以让一个好的平台更好。以下是我们正在考虑的下一步行动。

持久化数据

我们严重依赖 AWS 持久化数据。这已经在弹性和可伸缩性方面发挥了神奇的作用。我们的客户需要他们的数据安全且封闭。另一方面,开发者更喜欢便利性和易用性。

网络带宽

由于速度是一种竞争优势,我们正朝着更快的部署迈进。Docker 镜像可能会变大,构建任意代码的速度会变得非常慢。我们希望在未来实施的一些最佳实践包括:

  • 代理依赖管理器(例如 composer、npm、maven)

  • Docker 层缓存,而不是原始构建环境

  • 从一个构建到另一个构建的共享数据卷

  • 构建并推动镜像更接近生产环境,而不是通过区域移动

这些步骤可以显著减少延迟,我强烈建议你试一试。

Docker 构建中的高级用例

一些高级 Docker 镜像需要一个实时数据库来完成构建阶段。这听起来有点儿奇怪,但我们经常看到这样的情况。我们正考虑以下几条实例:

  • 为构建单独绑定一个虚拟数据库

  • 当兼容的时候,使用 SQLite 作为卷——不需要服务器

随你发挥,容器化的方案很多样化,并不枯燥!

以上就是我们迁移到 Docker 的经历。如果你当前正在迁移到容器或者希望迁移到容器,我希望你能够在本文中找到一些有用的点子。

关于如何让开发人员的工作更轻松,如果你有什么想法或建议可以在此与我们交流。

作者介绍:

Djalal Elbe

原文链接:

Documenting our migration to Docker—challenges and lessons learned

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注