八种最常见Docker开发模式

  categories:linux资料  tags:  author:
Docker已迅速成为本人最喜欢的基础工具之一,以便构建可重复软件产品,从而带来尽可能静 态的服务器环 境。我在本文中将概述我在使用Docker的过程中开始反复出现的几种模式。我不指望它们会带来多少新奇或惊喜,但希望其中一些有用,我也很想听听各位在 使用Docker过程中遇到的模式。

八种最常见Docker开发模式 别说你还不知道

Docker已迅速成为本人最喜欢的基础工具之一,以便构建可重复软件产品,从而带来尽可能静态的服务器环境。

我在本文中将概述我在使用Docker的过程中开始反复出现的几种模式。我不指望它们会带来多少新奇或惊喜,但希望其中一些有用,我也很想听听各位在使用Docker过程中遇到的模式。

我试用Docker的基础是保持在卷中持续的状态,那样Docker容器本身可以随意重建,而不会丢失数据(除非我改动容器状态,而不更新Docker文件(Dockerfile)的状态,而经常重建容器有助于改掉这个坏习惯)。

下面的示例Docker文件都专注于此:构建容器――在这种环境下,容器本身可以随时更换,没必要考虑它。

1. 共享基础容器

Docker 鼓励“继承”,所以这应该并不奇怪――继承是高效使用Docker的一个基本方面,尤其是由于它有助于减少构建新容器所需的时间,因为 没必要那么频繁地重新执行步骤。Docker会试图将中间步骤放入到缓存,它在这方面做得很好――有时太好了,不过要是没有明确注明,也很容易错过共享的 机会。

将我的各种容器迁移到Docker上时明显出现的事情之一是,存在太多的冗余设置。

我为预计部署到任何地方的大多数项目运行单独的容器,至少它需要任何长时间运行的进程,或者需要“标准”程序包集之外的任何特定程序包时,是这样,因而我有好多容器,而程序包迅速变得越来越多。

等到我考虑迁移时,就试图在Docker中运行“一切”(包括我依赖的少数几个桌面应用程序),以便让我的mybase环境完全可以随意使用。

于是我很快开始将我的基本设置提取到基础容器,用于众多用途。下面是我当前的“devbase”Docker文件:

  1. FROM debian:wheezy
  2. RUN apt-get update
  3. RUN apt-get -y install ruby ruby-dev build-essential git
  4. RUN apt-get install -y libopenssl-ruby libxslt-dev libxml2-dev
  5. # 用于调试
  6. RUN apt-get install -y gdb strace
  7. # 设置我的用户
  8. RUN useradd vidarh -u 1000 -s /bin/bash –no-create-home
  9. RUN gem install -n /usr/bin bundler
  10. RUN gem install -n /usr/bin rake
  11. WORKDIR /home/vidarh/
  12. ENV HOME /home/vidarh
  13. VOLUME [“/home”]
  14. USER vidarh
  15. EXPOSE 8080

这里没有什么需要特别说明的――它安装了我往往喜欢随时可用的一些特定工具。这些工具对大多数人来说恐怕不一样。选择什么样的发行版很随意。值得考虑的是,如果/当你重建容器时,就要指定一个特定的标记以避免意外。

它在默认情况下暴露了端口8080,因为那是我通常暴露Web应用程序的端口,我通常将这些容器用于这些Web应用程序。

它为我添加了一个用户,将userid设置为服务器上的用户ID,并不创建/home目录。之所以不创建/home目录,是由于我从主机绑定挂载共享/ home,这就引出了下一种模式。

2. 共享卷开发容器

我 的所有开发容器与主机至少共享一个卷:/ home,这么做是为了便于开发。就许多应用程序而言,它让我可以让与合适的基于文件-系统-变更的代码重载器一起运行的应用程序处于开发模式,那样容器 就可以封装操作系统/发行版层面的依赖项,并且帮助证实捆绑的应用程序在原始环境中运行,我用不着针对每处代码变更,需要完全重启/重建虚拟机。与此同 时,我可以相当频繁地重启虚拟机,确保没有什么错失。

至于其他,它让我可以只要重启(而不是重建)容器,即可接受代码变更。

对于测试/试运行容器和生产容器,我在大多数情况下会避免通过卷共享代码,而是使用“ADD”命令,将相应代码添加到Docker容器本身中。

比如说,下面是我“homepage”开发容器的Docker文件,它含有我自主开发的个人维基,可利用来自“devbase”容器的已经共享的/home卷,并展示了共享基础容器和我如何使用共享/home卷:

  1. FROM vidarh/devbase
  2. WORKDIR /home/vidarh/src/repos/homepage
  3. ENTRYPOINT bin/homepage web

(注意:我确实应该对我的devbase容器加上版本标记)

至于我博客的开发版本:

  1. FROM vidarh/devbase
  2. WORKDIR /
  3. USER root
  4. # 针对Graphivz整合
  5. RUN apt-get update
  6. RUN apt-get -y install graphviz xsltproc imagemagick
  7. USER vidarh
  8. WORKDIR /home/vidarh/src/repos/hokstad-com
  9. ENTRYPOINT bundle exec rackup -p 8080

因为它们从共享软件库获取代码,而且基于共享的基础容器,当我添加/修改/删除依赖项时,这些容器通常可以极其迅速地重建,我觉得这很重要,以便确保我没有忍不住采用疏忽未记录依赖项的变通方法。

即 便如此,肯定有些方面是我想改进的。尽管上述基础容器是轻量级,但它们肯定不止这样:这些容器中的大多数内容仍然未使用。由于Docker采用写 时拷贝(copy-on-write)覆盖,这不会导致庞大开销,但确实仍意味着我并没有真正体现最基本需求,也没有尽可能减少攻击或出错风险(我倒不是 很担心这些特定情况的攻击风险,因为我的博客并不在“实时”版本中含有任何重要状态。)。

3. 开发工具容器

这对像我们这些喜欢依靠通过ssh连接至屏幕会话来编写代码的人来说可能最有吸引力,而对IDE人群来说不太有吸引力;但对我来,上述方案的一个好处就是,它让我可以将编辑和测试执行部分代码与运行开发中的应用程序分离开来。

过去开发系统方面很烦人的问题之一是,开发及生产依赖项与开发工具依赖项很容易混在一起。你可以试着将它们分开来,但除非这些设置真正做到了分离开来,否则很容易建立未记录依赖项。

在过去,我花了几周对应用程序的依赖项进行“反向工程”后,总算搞清楚了这个问题。由于开发环境、测试和初始原型部署环境混在一起,这个应用程序积累了各种各样的未记录依赖项。

虽然有很多方法可以解决这个问题:只要确保你进行定期的测试部署,结合上述模式,但我还是有一种个人很喜欢的解决方案,因为它可以从根本上防止问题出现:

我 有一个单独的容器含有Emacs安装环境,还有我喜欢随时可用的其他各种工具。我仍试图保持精简,但问题是,我的屏幕会话可以驻留在这个容器中, 结合我那台笔记本电脑上设置的“autossh”,几乎总是有一条连接与容器相连,那样我就可以编辑与我的其他开发容器“实时”共享的代码。

在这个容器,我还允许偶尔出错:直接安装程序包,因为它只影响调试和开发。

目前,它看起来如下:

  1. FROM vidarh/devbase
  2. RUN apt-get update
  3. RUN apt-get -y install openssh-server emacs23-nox htop screen
  4. #用于调试
  5. RUN apt-get -y install sudo wget curl telnet tcpdump
  6. # 针对32位试验
  7. RUN apt-get -y install gcc-multilib
  8. # 参考手册页和“most”viewer:
  9. RUN apt-get install -y man most
  10. RUN mkdir /var/run/sshd
  11. ENTRYPOINT /usr/sbin/sshd -D
  12. VOLUME [“/home”]
  13. EXPOSE 22
  14. EXPOSE 8080

结合共享“/ home“,这给了我一个足够实用的小地方可以通过ssh连入。我确信,用它用得越多,我会补充它,但眼下它证明完全能满足我的需要。

八种最常见Docker开发模式 别说你还不知道

4. 不同环境下测试容器

我特别喜欢Docker的一个方面是,可以在不同环境下轻松测试代码。比如说,我升级Ruby编译项目以便处理Ruby 1.9(早就该有了)后,创建了这个小小的Docker文件,好让我在将主开发环境迁移到1.9之后,在Ruby 1.8环境中生成一个外壳。

  1. FROM vidarh/devbase
  2. RUN apt-get update
  3. RUN apt-get -y install ruby1.8 git ruby1.8-dev

当然,你可以用rbenv等获得类似的效果。但我总是觉得这些工具很烦人,因为我更喜欢尽量使用发行版程序包来部署,尤其是由于,如果我确保这顺利开展,它让其他人更容易使用我的代码。

拥有这样一个Docker容器:当我暂时需要不同的环境时,只要运行“docker run”,圆满地解决了这个问题,而且还有这个好处:它并不受制于像Ruby这种有预包装自定义工具来处理版本的编程语言。

我还可以使用标准虚拟机来达到目的,但是可以在短得多的时间内启动上述docker容器。

5. 构建容器

如今我编写的代码大多是用解释语言编写的,但即使那样,还是常常会有实用的“构建”(build)步骤需要花很大的开销,我宁可不是一直执行它们。

一个例子是为Ruby应用程序运行“捆绑工具”(bundler)。捆绑工具可为Rubygem更新缓存的依赖项(还可视情况更新全部的gem文件,甚至更新未打包的内容),针对较大的应用程序运行捆绑工具要花一段时间。

它 还常常需要应用程序运行时并不需要的依赖项。比如说,安装依赖原生扩展的gem常常依赖众多的程序包――常常没有记录到底是哪些程序包,通过获取 所有的build-essential程序包及其依赖项,就更容易启动。与此同时,虽然你可以事先让捆绑工具做所有的工作,但我真的不想在主机环境中运行 它,主机环境可能与容器兼容,也可能不兼容。

这方面的解决办法就是创建构建容器。如果依赖项不同的话,你可以创建单独的Docker文件,也可以重复使用主应用程序Docker文件,只要覆盖命令来运行你所需要的构建命令。比如说,Docker文件看起来如下:

  1. FROM myapp
  2. RUN apt-get update
  3. RUN apt-get install -y build-essential [assorted dev packages for libraries]
  4. VOLUME [“/build”]
  5. WORKDIR /build
  6. CMD [“bundler”“install”,“–path”,“vendor”,“–standalone”]

然后,只要你更新了依赖项,将你的build/source目录挂载到容器的”build”目录下,就可以运行这段命令。只要更换合适的项就行。

关键在于,你可以将应用程序的构建或者其一部分与最后的包装分开来,同时仍封装Docker容器中的进程和依赖项,只要将进程细分到两个或多个容器中。

6.安装容器

这 种方法虽非我原创,但确实值得一提。出色的nsenter和docker-enter工具随带一个安装选项,这与流行的,但又令人畏惧的 “curl [你无法控制的某个URL] | bash”模式相比是个很大的进步。它通过提供实现上述“构建容器”模式的Docker容器来做到这一点,不过更进了一步。它值得关注。

这是Docker文件的最后部分,之后下载并构建了一个合适的nsenter版本(我要提醒的一点是,对下载文档没有进行完整性检查):

  1. ADD installer /installer
  2. CMD /installer
  3. “installer”看起来如下:
  4. #!/bin/sh
  5. if mountpoint -q /target; then
  6. echo “Installing nsenter to /target”
  7. cp /nsenter /target
  8. echo “Installing docker-enter to /target”
  9. cp /docker-enter /target
  10. else
  11. echo “/target is not a mountpoint.”
  12. echo “You can either:”
  13. echo “- re-run this container with -v /usr/local/bin:/target”
  14. echo “- extract the nsenter binary (located at /nsenter)”
  15. fi

虽然恶意攻击者仍有可能企图利用容器可能存在的特权提升问题大做手脚,但是攻击风险至少要小得多。

但这种模式最可能立即吸引我们大多数人的地方在于,避免了这一风险:本意良好的开发人员偶尔在安装脚本方面犯下很危险的错误。

我确实很喜欢这种方法。但愿它会有助于减少大家对“curl [something] | bash”的厌恶感(但即使没有起到这个作用,至少我们可以轻松控制容器里面的东西)。

7. 盒子中默认服务容器

如果我“认真对待”某个应用程序,会比较迅速地准备好合适的容器,为开发项目处理数据库等服务,但我觉得拥有一系列“基本”的基础设施容器非常重要,我可以进行合适的调整/改动,就能启动所选择的数据库,或者所选择的有合适默认设置的队列系统。

当 然你也可以“基本上如愿以偿”,只要试一试“docker run [某个应用程序名称]”,祈祷Docker索引中有一个出色的替代者,而且这个替代者常常就在索引中。但我喜欢先审查,比如弄清楚它们如何处理数据,然后 我更有可能将自己的修改后版本添加到自己的“库”中。

比如说,我有一个Beanstalkd的Docker文件:

  1. FROM debian:wheezy
  2. ENV DEBIAN_FRONTEND noninteractive
  3. RUN apt-get -q update
  4. RUN apt-get -y install build-essential
  5. ADD http://github.com/kr/beanstalkd/archive/v1.9.tar.gz /tmp/
  6. RUN cd /tmp && tar zxvf v1.9.tar.gz
  7. RUN cd /tmp/beanstalkd-1.9/ && make
  8. RUN cp /tmp/beanstalkd-1.9/beanstalkd /usr/local/bin/
  9. EXPOSE 11300
  10. CMD [“/usr/local/bin/beanstalkd”,“-n”]

我 的目标是能够“OK,我将需要memcached、postgres和beanstalk”,运行三个快速“docker run”命令后,会启动并运行三个容器,它们都针对我的环境和默认环境个人偏好已经过了改动,随时可以使用。设置“标准”基础设施部件应该只需要1分钟, 而不是占用开发本身的时间。

8. 基础设施/粘合剂容器

许多这种模式专注于开发环境(这意味着生产环境方面还有更多需要讨论的),但缺少了一大类别:粘合剂容器。

其目的是把你的环境组合成统一整体的容器。这是到目前为止有待我进一步研究的领域,不过我会提到一个特别的例子:

为了能够轻松访问我的容器,我有一个小小的haproxy容器。我有一个指向主服务器的通配符DNS项,一个iptable项允许访问我的haproxy容器。Docker文件没什么特别之处:

  1. FROM debian:wheezy
  2. ADD wheezy-backports.list /etc/apt/sources.list.d/
  3. RUN apt-get update
  4. RUN apt-get -y install haproxy
  5. ADD haproxy.cfg /etc/haproxy/haproxy.cfg
  6. CMD [“haproxy”“-db”“-f”“/etc/haproxy/haproxy.cfg”]
  7. EXPOSE 80
  8. EXPOSE 443

这 里有趣的部分是haproxy.cfg,它由一段从“docker ps”的输出结果生成后端部分(就像这里)的脚本和前端定义中的一批acls和use_backend语句共同生成,前端定义将 [hostname].mydomain发送到右边的backend.backend测试。

  1. backend test
  2. acl authok http_auth(adminusers)
  3. http-request auth realm Hokstad if !authok
  4. server s1 192.168.0.44:8084

如果我想玩点花样,可以部署像AirBnB的Synapse这样的产品,但它拥有的选项数量之多超出了我对开发使用的要求。

就我的家用环境而言,这满足了我的大多数基础设施要求。我仍在不断推出了一系列基础设施容器,其目的是让实际应用程序部署起来轻而易举,就像我将一个完整的私有云系统向Docker迁移那样。



快乐成长 每天进步一点点