从Puppet到Docker的服务迁移

  1. 云栖社区>
  2. DockOne.io>
  3. 博客>
  4. 正文

从Puppet到Docker的服务迁移

轩墨 2017-09-14 16:24:00 浏览832
展开阅读全文
本文讲的是从Puppet到Docker的服务迁移【编者的话】Docker究竟带来了什么? 新的工作流? 新的服务编排方式? 还是新的devops模式? 本文作者就自己亲身经历的一次基础设施服务由Puppet转向Docker的实际案例,详细介绍了Docker所带来的革命性变化。

近日,我将之前由Puppet管理的一系列复杂的基础设施组件迁移到了Docker下。这么做的原因很多,其中最主要的原因在于我的Puppet代码过于陈旧,面临两种选择:重写兼容Puppet 4的代码,又或者对整件事进行反思,我倾向于后者。就我的使用情况,从基础设施的某些方面而言,我觉得配置管理(CM)无法正确解决我的问题,也许新的解决方案能给我带来更大的价值。

现在有很多讲述Docker的文章侧重于镜像构建或容器运行方面,这在我看来非常无趣,甚至有些糟糕。对我来说,实际的好处在于工作流、API、来自守护进程的事件以及容器统计数据。人们把镜像和容器两方面单独拿出来看待,得出“这并不是什么新技术”的结论,显然没有抓住问题的关键所在。

主要是工作流的问题

我们来看一个将rbldnsd从Puppet迁移到Docker的例子,以及我从中得到的收获。在这个过程中,我也会展示一些我在我的bind服务器上完成的更为复杂的迁移示例。rbldnsd是一个守护进程,使用类似下面的配置文件来维护基于DNS的实时黑名单(RBL):
$DATASET dnset senderhost
.digitalmarketeer.com   :127.0.0.2:Connection rejected after user complaints.

然后,你可以使用邮件传输代理(MTA)所支持的常规方式来查询它,并以此制定相应策略。

这个服务的生命周期正是我要讲述的典型之一:
  • 自定义的RPM包必须在别的基础设施上构建、维护和提供;
  • 基本的包、配置、服务三位一体。因此代码本身没有什么可看的,就像其他的包、配置、服务代码一样;
  • 要求能对运行时数据进行管理——我时常需要添加或是删除黑名单里的主机。但与上面的配置部分相叠加;
  • 要求能在开发过程中甚至是代码提交前测试DNS查询;
  • 要求能快速更新配置数据。

后面的三项要解释一下。在编辑这些配置文件时,我希望能在shell中测试,而不需要将代码提交到git中。也就是启动一个rbldnsd实例,然后用dig去查询它。使用Puppet工作流来做这件事非常费力,这个话题说来话长,我就不详述了。简单而言,这个方法对我不适用,最终也没有在生产环境这样做。

当我将配置文件更新到运行中的服务上时,有一个守护进程会将它们加载到其运行内存中。我现在需要确保我测试所在的守护进程与生产环境中的是完全一致的。理想情况下是每个字节都相同。显然,这很难做到,因为许多/绝大部分的开发环境往往比生产环境超前。我需要一种方法来获取生产机上运行的版本,将配置交给它们处理,并在5秒内完成端对端测试而不引起任何问题。

我需要一种编排方式,使得配置数据更新发生在我需要的时候,而不是等到Puppet再次运行时,而且这个操作要很快,而不是管理着600个资源的Puppet的那种速度。服务应当告诉我如何更新他们的数据,并提供一个通用的更新器(updater)用以更新所有匹配这个流程的服务。

在之前使用Puppet工作流的项目中,我从未真正的解决好这三项,要正确地解决它们实在是一个异常复杂的问题。大家都是通过Vagrant实例或更复杂的环境来解决。或是通过修改、提交,同时确保被测试覆盖,然后只有在诸如Beaker之类运行后才能得到反馈信息。这种方式对我来说实在是太慢了。我所想做的只是锁定一台主机而已。Vagrant对我尤其不适用,因为我都是远端虚拟机上进行开发,不愿意在台式机或笔记本上运行程序,因此,Vagrant不在考虑范围内。另外,Vagrant环境已经变得太复杂了,基本上算是一个全新的环境。但是又有不同方式来构建,因此要与生产环境保持一致变得很艰难;如果使用Puppet构建他们,速度将异常慢。所以,最终的结果是你还是无法在与远端生产环境一致的环境下测试。

这些就是使用Puppet时我无法解决的问题。我首先将一系列Web站点转移到Docker下,然后是bind,现在是rbldnsd,我想我已经想出了一个工作流和工具链来解决这个问题。

期望的结果

要展示我的成果,也许我应该先说明一下我期望的结果是什么样。这有个rbldnsd的开发会话。我想阻止 *.mailingliststart.com,特别是当我在日志里看到sh8.mailingliststart.com时。我想要在推送到生产环境甚至是提交到git之前测试主机是否会被正确阻止,毕竟为一些蠢事做修复提交是件很尴尬的事。

因此,我将以下内容添加到zone/bl文件:
.mailingliststart.com :127.0.0.2:Excessive spam from this host

并且操作如下:
$ vi zones/bl
$ rake test:host
Host name to test: sh8.mailingliststart.com
Testing sh8.mailingliststart.com

Starting the rbldnsd container...
>>> Testing black list
docker exec rbldnsd dig -p 5301 +noall +answer any sh8.mailingliststart.com.senderhost.bl.rbl @localhost
sh8.mailingliststart.com.senderhost.bl.rbl. 2100 IN A 127.0.0.2
sh8.mailingliststart.com.senderhost.bl.rbl. 2100 IN TXT "Excessive spam from this host"

>>> Testing white list
.
.
.

Removing the rbldnsd container...
$ git commit zones -m 'block mailingliststart.com'
$ git push origin master

这里我追加了一些内容到配置文件,并确认在日志/请求头里看到的主机名被真正的阻止了:
  • 它默认准备了最新的容器,并使用-v ${PWD}:/service将我的工作目录挂载到容器中;
  • 容器的启动过程和它将来在生产环境时所做的差不多,不过读取的是未提交的配置内容;
  • 它使用dig查询运行中的rbldnsd,并运行服务所拥有的任意(any)内建验证步骤(这个容器还没有);
  • 清理所有的环境。

这一切运行在一台2009年Mac的VirtualBox虚拟机上,大概花费了4秒钟。可以见到了主机被列入黑名单,并且未出现在白名单中,看起来不错,然后提交并推送。

一旦完成推送,一个webhook就会触发我的更新编排流程,运行中的容器只会获取最新的配置文件。整个编辑、测试和部署流程用时不超过1分钟。由于数据是推送到git的,也就意味着今天晚上当所有容器开始从最新的源上重新构建时,他们会融入这一次的变更,作为新的实例开始运行。

这边我还想追加一个早期反馈的印象更不错的案例。我的bind域以前是通过Puppet来定义的:
bind::zone{"foo.com": owner => "Bob", masterip => "1.2.3.4", type => $server_type
}
阅读这行代码时,我并不知道它实际上做了什么。我觉得只能够靠猜测了。但是,只有当你在生产环境中运行Puppet你才有确认,因为不管宣传说得多好,只有放到使用Puppet的生产机上时,你才看得出它与实际文件的差异。这着实有些糟糕。在这个过程中你也学不到任何东西,让我厌烦的是Puppet最终变成了像计算器一样的工具,对所有东西进行抽象,使用这项定义的初学者可能永远不知道它做了什么,也不知道bind是如何运转的。这对某些情况是合适的,但不是我想要的。

在我的bind容器中有这样的一个YAML文件:
zones:
  Bob:
    options:
      masterip: 1.2.3.4
    domains:
     - foo.com

这与Puppet的清单(manifest)内容一致,只是结构有所差异。同样的,看着它我也不知道它做了什么。在Docker的世界里,你需要把这个YAML文件转换成bind的配置。而这必须在开发过程中完成,这样执行docker build时才能获得最终的结果。因此,我添加了一个新域bar.com:
$ vi zones.yaml
$ rake construct:files
Reading specification file buildsettings.yaml
Reading scope file zones.yaml
Rendering conf/named_slave_zones with mode 644 using template templates/slave_zones.erb
Rendering conf/named_master_zones with mode 644 using template templates/master_zones.erb

 conf/named_master_zones | 10 ++++++++++
 conf/named_slave_zones  |  9 +++++++++
 2 files changed, 19 insertions(+)
$ git diff
+// Bob
+zone "bar.com" {
+  type slave;
+  file "/srv/named/zones/slave/bar.com";
+  masters {
+    1.2.3.4;
+  };
+};

rake construct:file指令会在域hash之上运行一系列的ERB模板,这些模板和在Puppet中所用的基本上相同,只是一些变量名称做了修改,以及稍有差异的循环语句,在本质上没有什么区别。

这就是会应用到生产环境的实际变更。没有“如果”或“但是”,生产机就会做如上变更。无须提交变更,运行rake test时,就会使用生产机实际命名程序为测试这些实际的生产环境变更。
$ time rake test
docker run -ti --rm -v /home/rip/work/docker_bind:/srv/named -e TEST=1 ripienaar/bind
>> Checking named.conf syntax in master mode
>> Checking named.conf syntax in slave mode
>> Checking zones..
rake test  0.18s user 0.33s system 7% cpu 3.858 total

同样,我的工作目录挂载在与当前生产环境相同版本的容器内,未提交的变更使用了与生产机相同的bind程序进行测试。这非常振奋人心,而且反馈周期小于5秒,我可以一整天都在做这样的事,甚至在每次保存文件时都在一个tmux pane里使用类似guard的程序来运行,速度非常快,而且因为它和生产环境密切相关,所以它的反馈内容具有实际的价值。

实现细节

Dockerfile平淡无奇,所以我不会一一详细介绍。镜像构建和容器运行也没什么可激动的。服务的结构像如下所示:
/service/bin/start.sh
/service/bin/update.sh
/service/bin/validate.sh
/service/zones/{bl,gl,wl}
/opt/rbldnsd-0.997a/rbldnsd

真正令人激动的是,我可以探知一个运行中的容器的内部细节。Dockerfile有这么几行:
ENV UPDATE_METHOD /service/bin/update.sh
ENV VALIDATE_METHOD /service/bin/validate.sh

使用一个外部的工具能够发现这个容器是如何被更新、验证,以及稍后被监控:
$ docker inspect rbldnsd
.
.
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "UPDATE_METHOD=/service/bin/update.sh",
            "VALIDATE_METHOD=/service/bin/validate.sh",
            "GIT_REF=fa9dd19d93e6d6cb7d5b2ebdc57f99cd2906df6f"
        ],


我的webhook差不多就做这么一件事情:
mco rpc docker runtime_update container=rbldnsd -S container("rbldnsd").present=1 --batch 1

我使用mcollective来对所有运行rbldnsd容器的机器发起一个更新操作,每次只更新一台。mcollective代理程序使用docker inspect来检查容器的状态。一旦它了解了容器需要如何被更新地,就会使用docker exec调用上述命令。

结果总结

对我而言,这真是一个巨大的成功。我需要在镜像构建的这块做很多的事情,像编排流程、发布部署等,当然使用Puppet一样也得做这些事。但是,正如我在这篇文章开头所讲的那样,所有的事情都被打包好了,而且收益更大:
  • 一个合理的包、配置、服务三位一体副本,激发幂等构建。
  • 一个舒适的方式进行本地开发和测试,并可快速得到反馈,无论是针对普通代码的单元测试,还是针对使用和生产环境类似的基础组件的集成测试。
  • 在“这实际改变了什么”的问题上有了更棒的直观效果,尤其是在诸如配置文件是使用模板构建的复杂案例情况下。
  • 提供了一个途径,使得我所维护的服务可以相对独立的运行,并且他们都必须重新思考他们的运行方式,更新和验证的逻辑。而这些都是可以被外界追溯及调用的。
  • 我所维护的服务都是一个个相对独立的对象并且每个服务作为一个整体被有效的版本控制。它并不是零散的分布在机器上的软件源、数据、以及尝试把它打包到一起的配置管理代码,它就是一件事物,从一个Git源上获取,以一个独立的版本保存在一个位置。
  • 通过内建到容器的验证逻辑,我在推送任何变化到实际生产之前的持续集成期间构建了一个可运行容器的虚拟环境,就像我之前用命令行所做的那样。而它往往和实际正在使用或者打算即将使用到生产的环境一样。
  • 另外,比之以前使用Puppet的工作流而言,毕竟我现在在生产环境做变更变得更有信心了。
  • 变更能够快速的推送到正在运行的容器 —— 一般是在10秒以内,而不像以前Puppet那样糟糕的慢。
  • 我的开发环境变得异常的简单而且更加的灵活,因为我可以运行现在,过去甚至将来的任一版本配置,操作复杂度被大大的降低。
  • 在服务器需要的稳定性和更新内容的实时性需求之间构建了一个非常好的中间地带。容器在每天晚上的计划任务中保证被重新构建和部署而它们始终是一次性的,这样也就不会带来每天更新所附加的额外成本。

现在我已经在一定规模的容器里应用了这个流程,这些容器提供着
一些对外服务甚至是一些Web站点,例如我日常编辑markdown文件的wiki,而他们可以在推送之后立马使得正在运行中的容器生效。

我也已经有一些办法来解决监控相关的问题,而且这些服务都是相对独立的,并不像一些复杂的多组件应用那样,不过我觉得这个方案对那些复杂的案例应该也是没太大问题的。

我仍然无法解决的一个问题是,与我开发所在的机器相对隔离的生产环境的发起和清理过程,如果没有相应的途径快速的完成的话,那么现有的这些收益也许就荡然无存了,尤其是在终端服务是由许多不同源的松耦合组合的情况下。 我非常乐于接受一些新的不借助Docker实现的或者说相近的解决方案,然后他们最终证明我是错的,但是,就目前而言,这个方案对我来说确实是一个巨大的改进。

所以是否Puppet这样的配置工具已经过时了呢?

回过头来讨论下这篇文章的Puppet部分。我也想在这提出一些混合Puppet的方案。 关于Docker的生命周期有一些其它的有意思的地方值得讨论,我也许会在之后的博客里详细介绍这些,关于如何把这两个工具进行有机的结合的一些想法。

特别是我认为人们今天觉得他们应该使用Puppet去构建或是配置容器的想法有一些不够确定和明朗,我当然希望他们能够在这方面持续的耕耘不过我也希望他们同时关注一些更有意思的事物,比如说利用棒极了的Dockerfiles, 不过我并不认为当下去尝试这件事情一定是有价值的。

当我们最终承认使用Puppet去完成应用的部署不是一个好主意而肯定其在基础设施方面的价值的时候,也许我们又是回到了旧有的思维方式。我开始重新思考“什么是基础架构”以及“什么是应用程序”的问题。
因此我选择了重新出发去思考事物的本质 —— 如果我把一个名称解析服务看做是一个应用程序而非基础设施,那么这个应用的开发生命周期应当如何看待呢?

这对于我来说并不是一个新的认知,我经常表达出对于“Puppet工作室应该更多的关注一下工作流以及开发周期并且致力于提供一些工具和hooks来使得事情变得更美好一些”的期盼,不过我并不认为这会真实发生。所以,针对我个人而言,我得出了这样的结论,即在这样的应用和服务开发、部署的生命周期中选用Puppet是不大合适的。我同样意识到长期来说我同样不会是它们的目标受众。

我也从来没有说过“Puppet或者其他的配置管理工具在Docker面前已经表现的过时而陈旧”这样的胡话。我想这应该是CM和Docker两个世界的一个交汇处而我已然开始意识到我之前所以为是基础设施组件的实际上都应该算作应用程序,而他们拥有着一些与众不同的开发与部署需求,这些都是配置管理以及Puppet未曾解决的问题。

不久之后在我的Puppet代码里便不会再有单独提及DNS相关的基础设施组件了。容器和相关的文件与Puppet里的一行行代码是等同复杂度的,最终的表现结果也是大致相同的,并且是可配置的环境。比较之下,工作流被大大的改善了,作为基础服务的一部分,新的工作流对于应用开发者而言提供了一些显著的优势。
最终一个更大规模的伴随着更少代码的基础设施就这样建立起来了,而它与我用Puppet代码所实现的感觉实在没什么两样,它有相同的反馈周期并且生成单一的可复查的配置结果。而这实在是一个巨大的进步。基础设施仍然在由配置管理所管辖 —— 我只是单纯的希望留下一个更快、更精简的基础设施的足迹。

这里的“巨大的飞跃”并不是说Docker是一工具利器而革掉现有的大部分应用场景或是完全的取代类似配置管理这样的工具集。它实际上是一次带来了全新的工作流并且极大改善用户体验的非常独特而有价值的探索历程。尤其是考虑到了一些原本配置管理工具不怎么关注的部分。最大的胜利可能是独特的思维模式的革新,因为这是我这里所有提及的一切事物的启发点。

它也同样带来了大量daemon端的改变 —— API、events、统计数据等等,我在这里并没有过多的提及,而这都是一些极为关键的要点而关乎未来工作的具体进展。不过那是后面文章所要讨论的事情了。

从技术上来说,我想我发现了很多Docker方方面面的缺陷但是那些问题在这样一个新的敏捷反馈与提振信心的适应快速变化的模型下都显得太过微不足道了。

原文链接:Moving a Service from Puppet to Docker(翻译:吴佳兴 校对:梁晓勇)

================

译者介绍:Colstuwjx,现就职于一家在线OTA,有点文艺,有点技术控,有点偏执。

原文发布时间为:2015-03-09
本文作者:colstuwjx
本文来自云栖社区合作伙伴DockerOne,了解相关信息可以关注DockerOne。
原文标题:从Puppet到Docker的服务迁移

网友评论

登录后评论
0/500
评论
轩墨
+ 关注
所属云栖号: DockOne.io