曾经我写过两篇文章:一篇是 利用 OpenVZ 容器构建家庭云服务;另一篇则提出 服务器必须能够自动化重建 的新思路。后来 Docker 出现了,它能够保证每次重建环境的一致性,已然成了我的最爱。
这篇文章我将介绍自己使用 Docker 过程中不断重复的那些模式,我并不认为这些模式有多新奇,只希望它们能对各位有用,同时我也非常期待听到各位关于 Docker 使用的各种模式。
Docker 实验的基础在于它能够将容器状态持久化到存储介质中,并且可在任意时候无损还原回来(除非修改容器状态时忘了更新 Dockerfile 文件,定期重建容器可避免这样的杯具)。
Docker 鼓励使用继承特性,善用继承能大大减少构建容器的时间,这是高效使用 Docker 的关键。Docker 在缓存过程容器方面表现过于出色,因此经常错过分享和重用的机会。
在迁移其它容器到 Docker 的过程中,存在太多重复冗余的安装步骤;当项目需要部署到很多地方,或者项目运行周期较长,又或项目需要安装一些特定包时,我都会考虑为该项目创建单独的容器。长此以往,我有了很多的容器,并且数量仍在快速增加;我试图在 Docker 中运行一切(包括某些桌面应用),只为让宿主环境更加纯粹。
以上这些,促使我开始思考如何减少容器的构建时间。我开始从各种项目容器中提取基本安装步骤,构建适用各种场景的可重用容器。下面是 "devbase" 基础容器对应的 Dockerfile 文件:
FROM debian:wheezy RUN apt-get update RUN apt-get -y install ruby ruby-dev build-essential git RUN apt-get install -y libopenssl-ruby libxslt-dev libxml2-dev # For debugging RUN apt-get install -y gdb strace # Set up my user RUN useradd vidarh -u 1000 -s /bin/bash --no-create-home RUN gem install -n /usr/bin bundler RUN gem install -n /usr/bin rake WORKDIR /home/vidarh/ ENV HOME /home/vidarh VOLUME ["/home"] USER vidarh EXPOSE 8080
这段代码简单易懂:随便选择一个发行版,安装一些经常使用的工具,这些工具因人而异;开放容器的 8080
端口;新增一个用户,将其 userid
设置为服务器的用户ID,不创建 /home
目录;将 /home
目录设为共享文件夹,共享文件夹将在下一节介绍。
为了方便开发,我的所有开发容器都至少有一个共享文件夹 /home
。凭借开发模式下的代码重载(code-reloader)特性,容器可以在运行时重载文件系统的改变,这样就不需要为每一次代码提交重启或者重建容器,当然你也可以偶尔重启容器来确认是否有所遗漏。对于测试和生产环境的容器,我不推荐启用共享文件夹功能。取而代之,你应该使用 ADD
命令添加修改。
下面是 "homepage" 开发容器的 Dockerfile 文件,它被我用于开发个人维基。这个容器继承自 "devbase" 容器,这个例子演示了基础容器的重用和共享文件夹的使用。
FROM vidarh/devbase WORKDIR /home/vidarh/src/repos/homepage ENTRYPOINT bin/homepage web
(备注:我本应给 "devbase" 容器加上标签)
下面是我的个人博客开发容器的 Dockerfile 文件:
FROM vidarh/devbase WORKDIR / USER root # For Graphivz integration RUN apt-get update RUN apt-get -y install graphviz xsltproc imagemagick USER vidarh WORKDIR /home/vidarh/src/repos/hokstad-com ENTRYPOINT bundle exec rackup -p 8080
因为这些容器可直接从公共仓库拉取,且基于可重用的基础容器构建,当容器依赖发生变更时,我们可以快速重建容器,这对于避免引入未文档化的依赖(未写入 Dockerfile 中的依赖)也非常重要。
即使我们的基础系统已经非常轻量,却仍有提升的空间:容器中的大部分功能将不会被使用。 Docker 采用写时复制(copy-on-write)层技术,也许不会造成太大资源开销,但这意味着我们没能构建一个最小依赖系统,也无法将被攻击和出错的风险降到最低。
本节内容对于通过 ssh
等工具进行开发的用户(而非 IDE 用户)比较有价值,最吸引我的是这种模式可以将代码编辑和测试执行(test-executing)从应用中完全分离出来。
一直以来,令我非常困扰的问题是:开发、生产环境和开发工具的依赖经常混在一起。这个问题很难真正解决,在开发过程中我们总会不经意的安装一些未文档化的依赖(比如:通过 apt-get
安装 strace / tcpdump / emacs 这些最终不被需要的工具时,不可避免会安装大量的依赖)。过去,我必须花上数周时间 "逆向工程(reverse engineering)" 这些应用的依赖,因为它们已经和我的开发、测试甚至生产环境混在一起。
定期的测试部署(test-deployments)可以帮助我们解决这个问题,但这并不完美。结合以上两种模式,我找到了从根本解决问题的办法。我首先创建一个专用容器,安装 emacs 等必需的开发工具,然后通过笔记本安装的 "autossh" 工具,保持在专用容器的会话里。再借助共享文件夹功能,就可以在专用容器直接编辑开发容器中的代码。考虑到专用容器只会影响调试和开发,有时我会在其中直接试用一些工具(遇到好用的就加入 Dockerfile 中,没用的下次重建容器的时候会自动消失)。下面是开发工具专用容器的 Dockerfile 文件:
FROM vidarh/devbase RUN apt-get update RUN apt-get -y install openssh-server emacs23-nox htop screen # For debugging RUN apt-get -y install sudo wget curl telnet tcpdump # For 32-bit experiments RUN apt-get -y install gcc-multilib # Man pages and "most" viewer: RUN apt-get install -y man most RUN mkdir /var/run/sshd ENTRYPOINT /usr/sbin/sshd -D VOLUME ["/home"] EXPOSE 22 EXPOSE 8080
利用共享文件夹 "/home" ,通过 ssh
可以方便的使用专用容器,我会慢慢加强专用容器的功能,但目前来看,它已经能满足我的需求。
我喜欢 Docker 的另一个理由是可以轻松的在不同环境的容器中测试代码。举个栗子,我升级了 Ruby compiler project 项目,使其支持 Ruby 1.9 ,但对 Ruby 1.8 的支持情况未知。此时,我仅需一个小小的 Dockerfile ,便可在 Ruby 1.8 下对项目进行测试。
FROM vidarh/devbase RUN apt-get update RUN apt-get -y install ruby1.8 git ruby1.8-dev
当然也可以使用 rbenv
这类工具,但当需要大量的系统包时这些工具并不省心(和曾经的 Ruby 一样痛苦)。有了 Docker 容器,当我需要不同的运行环境时,运行 docker run
命令即可,且不局限于 Ruby 这种自带版本切换工具的环境。完整的虚拟机也能胜任这项任务,但相比之下,Docker 消耗的时间少很多。
虽然大多数时候我使用解释性语言编程,但依然有些时候,我需要执行昂贵的构建(build)操作。举个栗子,用于运行 Ruby 应用的 "bundler" 工具在更新 Rubygems 依赖关系时,如果应用很大,会消耗大量时间。并且,我们经常需要安装一些运行时不需要的依赖,比如:安装 gems 时有时会引入一些依赖的系统包,而这些包通常没有文档化(未写入 Dockerfile 中),非常容易引入整个 build-essential 包及其依赖。同时,因为宿主环境和部署容器的环境可能并不兼容,我也不想在宿主环境中构建(build)应用。
解决方案之一是创建构建(build)专用容器,如果专用容器和应用容器的依赖环境不同,就单独创建 Dockerfile 文件;如果专用容器和应用容器的依赖环境一致,则可复用应用容器的 Dockerfile 文件,使其运行所期望的构建(build)命令。下面是构建(build)专用容器的 Dockerfile 文件。
FROM myapp RUN apt-get update RUN apt-get install -y build-essential [assorted dev packages for libraries] VOLUME ["/build"] WORKDIR /build CMD ["bundler", "install","--path","vendor","--standalone"]
运行构建(build)专用容器,并挂载源代码目录到构建(build)专用容器的 "/build" 目录。这样我就可以将构建流程及其依赖封装在两个或多个容器中,最终实现应用构建(build)或者打包(packaging)的解偶。
(我经常检查整条构建流水线,为了能够100%重现最终的打包步骤,需要检查构建的每一个环节,确保流水线能精确的完成整个构建过程)
这个方法虽非本人原创,但却着实值得一提。 nsenter and docker-enter tools 是一款优秀的工具, 它出自流行的 curl [some url you have no control over] | bash
模式,它也提供了一个构建专用容器,但却更进一步。其在 Dockerfile 的最后部分下载并构建了一个合适版本的 nsenter (未对下载文件做完整性检查)。
ADD installer /installer CMD /installer
其中 "installer" 文件如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 | #!/bin/sh if mountpoint -q /target; then echo "Installing nsenter to /target" cp /nsenter /target echo "Installing docker-enter to /target" cp /docker-enter /target else echo "/target is not a mountpoint." echo "You can either:" echo "- re-run this container with -v /usr/local/bin:/target" echo "- extract the nsenter binary (located at /nsenter)" fi |
这种情况下,虽然恶意攻击者可能利用权限问题攻击该容器,但攻击面却非常有限。本节的方法更多时候是为了避免 开发者在安装脚本时不经意执行危险操作 。我喜欢这种方法,希望通过它能够减少大家对 curl [something] | bash
的厌恶(即时杯具发生,也仅会影响专用容器)。
当我认真对待一个 app 时,我会快速准备合适的容器,用于安装数据库等服务。对于项目,这一系列经过优化调整的 "基本" 容器都是无价的。与运行 docker run [some-app-name]
创建容器或者直接从 Docker Hub 拉取容器相比,我更倾向于先审核它们,搞清楚它们如何处理数据,然后优化调整容器,最后保存在自己的容器库中。下面是 Beanstalkd 的 Dockerfile 文件:
FROM debian:wheezy ENV DEBIAN_FRONTEND noninteractive RUN apt-get -q update RUN apt-get -y install build-essential ADD http://github.com/kr/beanstalkd/archive/v1.9.tar.gz /tmp/ RUN cd /tmp && tar zxvf v1.9.tar.gz RUN cd /tmp/beanstalkd-1.9/ && make RUN cp /tmp/beanstalkd-1.9/beanstalkd /usr/local/bin/ EXPOSE 11300 CMD ["/usr/local/bin/beanstalkd","-n"]
(对比这个栗子,我有个更加复杂的 postgres 容器,其中包括持久化的数据和各种配置文件)
我的目标是当需要 memcached ,postgres 和 beanstalk 时,只需运行三条 docker run
命令,三个根据我的宿主环境和个人偏好优化的容器将被创建并运行,这样建立“标准”基础设施组件只需一分钟。
上文介绍了很多用于开发环境的容器(这意味着讨论生产环境需要更多的篇幅),但还是漏了一类容器——胶水容器。胶水容器用于将各种环境粘合成一个整体,目前为止,这一领域还有待研究,这里我先举个栗子:
为了更加方便的访问容器,我构建了一个小小的 haproxy 容器,并在家用DNS服务器上添加一条A记录指向该容器,设置 iptable 防火墙使得 haproxy 容器可以正常访问,下面是对应的 Dockerfile 文件:
FROM debian:wheezy ADD wheezy-backports.list /etc/apt/sources.list.d/ RUN apt-get update RUN apt-get -y install haproxy ADD haproxy.cfg /etc/haproxy/haproxy.cfg CMD ["haproxy", "-db", "-f", "/etc/haproxy/haproxy.cfg"] EXPOSE 80 EXPOSE 443
首先由脚本生成 haproxy.cfg
文件,然后根据 docker ps
的输出生成后端(backend)部分:
backend test acl authok http_auth(adminusers) http-request auth realm Hokstad if !authok server s1 192.168.0.44:8084
前端(frontend)定义中关于 acls 和 use_backend 部分的声明,可将来自 [hostname].mydomain
的请求转发到 backend test
后端。
如果希望优雅一些,可以部署类似 AirBnB's Synapse 这样的服务发现系统,其提供的功能选项远超我的需求。以上这些容器已经能够满足我对家用环境的多数需求,但工作中,我还在不断的推出各种新容器,这些容器使得真实应用的部署轻而易举,就仿佛拥有一套私有云系统。