1+1>2:用Docker和Vagrant构建简洁高效开发环境

Docker和Vagrant经常被认为是两种相互替代的工具,其实它们可以结合使用,构建隔离的、可重复的开发环境。我们将证明该环境可以构建一个Docker容器以便开发Java应用程序,并充分利用Vagrant的强大功能,以解决一些现实当中的实际问题。

这篇博客的第一部分探讨了开发环境的常见缺陷、简单Docker环境的构建以及Vagrant+Docker配置具有的优点。但是如果你想就开始使用Docker和Vagrant,不妨直接跳到本文的这个章节:使用Vagrant,让Docker容器易于移植。

开发环境有什么问题

要花很长的时间来构建

新的开发人员要花多长时间才能构建好当前项目的开发环境答案取决于诸多因素(项目时间、从事该项目的开发人员数量等),但至少需要半天时间并不罕见。

嘿!其实应该比这快得多:查看脚本,执行脚本。就是这样。这两个步骤应该足以构建你的环境,并准备随时开发。

它可能与测试环境和生产环境大不一样

你有没有因构建的环境在机器上未通过而跳过自动化测试或者更糟糕的是,即使更改的内容在机器上顺利编译,但是在持续整合(CI)服务器上老是失败,你有没有查过问题的根源出在哪里

任何稍有不同,就会导致意料不到的行为。有的方法可能很简单,比如试一试框架的上一个版本,或者改用不同的项目。

查明什么导致你的系统出现不同行为是每个开发人员都应该避免的烦人任务。

虚拟机和Docker

因而,开发环境应该具有两个特点:

隔离:你不希望在测试某个新工具或不同项目时弄得一团糟。

可重复:同一个环境应该在每个团队成员的机器、持续整合服务器和生产服务器上都一再可复制。

虚拟机环境确保了这些特性,可是典型的虚拟机很耗费资源。开发人员需要每隔几分钟编码/构建/测试,不会接受虚拟化带来的开销。

这时候,Docker显得大有帮助。相比典型的虚拟机,其轻型容器极其快速,而且在开发人员当中极受欢迎。下面是来自Docker博客的一段摘要,解释了这种成功的原因:

在问世后的头12个月内,Docker在初创企业和早期采用者当中迅速流行起来,他们重视该平台的这一功能,即可以将应用程序开发管理的问题与基础设施提 供、配置和运营的问题分离开来。Docker为这些早期用户提供了一种新颖的、更迅速的方法,可以构建分布式应用程序,另外提供了一种“编写一次到处运 行”的部署选择,部署对象从笔记本电脑、裸机、虚拟机到私有云/公有云,不一而足。

使用Docker来配置的、可重复的开发环境

为了举例说明,我们将构建一个构建并测试Vert.x HTTP服务器的Docker容器。

Vert.x是一种轻型应用程序框架,鼓励小型、独立微服务的架构。微服务“就是一种小巧的独立式可执行程序,可与其他的独立式可执行程序进行沟通”(Uncle Bob)。我们认为,它在Docker容器中再合适不过了,这就是为什么我们在此选择它作为例子。

要是你之前还没有安装过Docker,先安装它。你可以使用get docker script来安装它。我们假设在本章节中,我们在Linux上运行。即使Docker也可以安装到Windows和Mac上(借助boot2docker),我们会在下一章中看到如何使用Vagrant来安装、为什么Vagrant是一种更好的选择。

Docker文件(Dockerfile)

为了描述容器,我们需要一个Docker文件:

FROM ubuntu:14.04

安装开发工具:jdk和git等

  1. RUN apt-get update
    1. RUNapt-getinstall-yopenjdk-7-jdkgitwget

jdk7是默认JDK

  1. RUNln-fs/usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java/etc/alternatives/java

安装vertx

  1. RUN\
    1. mkdir-p/usr/local/vertx&&cd/usr/local/vertx&&\
    1. wgethttp://dl.bintray.com/vertx/downloads/vert.x-2.1.2.tar.gz-qO-|tar-xz

将vertx添加到路径

  1. ENVPATH/usr/local/vertx/vert.x-2.1.2/bin:$PATH
    1. RUNmkdir-p/usr/local/src
    1. WORKDIR/usr/local/src
    1. CMD["bash"]

Docker文件确实简单直观,当你需要更深入地挖掘时。

FROM ubuntu:14.04定义了我们依赖的基本映像。你可以在docker中心(registry.hub.docker.co)找到Docker基本映像的完整列表。就这个例子而言,我们使用了docker团队构建Docker所用的基本映像。

后面几行描述了将应用到基本映像上的改动:

一旦我们拷贝了Docker文件,就可以构建Docker映像:

  1. $sudodockerbuild-t=vertxdev.

获得源代码

我们刚构建的映像已安装了git。我们可以用它从Github获取源代码:

  1. $sudodockerrun-t--rm-v/src/vertx/:/usr/local/srcvertxdevgitclonehttps://github.com/vert-x/vertx-examples.git

请注意:git在容器里面运行,源代码因而在容器里面传送(确切的位置是在文件夹/usr/local/src)。为了让代码具有持续性,即使在容器停止和删除之后,我们也可以使用标志-v /src/vertx/:/usr/local/src,将容器的文件夹/usr/local/src绑定挂载到主机文件夹/src/vertx。一旦git clone命令完成了执行,--rm”就销毁容器。

构建并运行应用程序

由于源代码已获取,我们将启动新的容器,这个容器负责构建并运行vertx示例:HelloWorldServer。要注意:vertx run同时负责vertx应用程序的构建和执行。

  1. $sudodockerrun-d-v/src/vertx/:/usr/local/src-p8080:8080vertxdevvertxrunvertx-examples/src/raw/java/httphelloworld/HelloWorldServer.java

与前一个容器相反,这个容器在停止后不会被销毁,暴露端口8080(-p 8080:8080),在后台运行(-d)。若想看一看vertx run的输出结果:

  1. $sudodockerlogs
    1. Succeededindeployingverticle

不妨使用curl,从主机端测试应用程序:

  1. $curllocalhost:8080
    1. HelloWorld

这个简单例子应该足以体现出运行Docker容器有多快速。在Docker容器里面运行git clone和vertx run的开销可忽略不计。

但构建的这个环境很基本。在现实环境中,纯Docker配置存在一些不足,而Vagrant就有助于克服这些不足。

Docker + Vagrant

Docker(其实是作为Docker模块的libcontainer)仍需要Linux内核3.8或更高版本和x86_64架构。这在大大限定了Docker能够原生运行在其中的环境。

Vagrant是一种开源软件,它为跨众多操作系统构建可重复的开发环境提供了一种方法。Vagrant使用提供者(provider)来启动隔离的虚拟环境。默认的提供者是Virtualbox;自v1.6以来,基于docker的开发环境也得到支持。相比帮助Docker在非Linux平台上运行的其他工具(比如boot2docker),Vagrant具有一些重大优点:

配置一次,即可到处运行:Vagrant就是原生支持Docker的系统上的Docker封装器,同时它可启动“主机虚拟机”,以便在不支持它的系统上运行容器。用户没必要操心Docker是不是得到原生支持:同一个配置可适用于每一个操作系统。

在Linux及其他操作系统上使用Vagrant运行Docker

在下面三个章节中,我们将探讨这每一个要点。

使用vagrant,让Docker容器易于移植

Vagrant可支持Docker,既作为提供者,又作为配置者。但是为了让它可以在Windows和Mac上自动创建Docker主机虚拟机,应该将它用作提供者。

我们将重复使用我们在上面见到的同一个Docker文件。与上面一样,我们将运行两个Docker容器,以便执行git clone和vertx run。不过将改而使用Vagrant命令,而不是Docker命令。

安装Vagrant(https://www.vagrantup.com/downloads)和Virtualbox(https://www.virtualbox.org/wiki/Downloads),以便能够运行示例。

Vagrantfile

Vagrantfile描述了Vagrant设备。我们将使用下列内容:

  1. ENV['VAGRANT_DEFAULT_PROVIDER']='docker'
    1. Vagrant.configure("2")do|config|
    1. config.vm.define"vertxdev"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.build_dir="."
    1. d.build_args=["-t=vertxdev"]
    1. d.ports=["8080:8080"]
    1. d.name="vertxdev"
    1. d.remains_running=true
    1. d.cmd=["vertx","run","vertx-examples/src/raw/java/httphelloworld/HelloWorldServer.java"]
    1. d.volumes=["/src/vertx/:/usr/local/src"]
    1. end
    1. end
    1. end

ENV'VAGRANT_DEFAULT_PROVIDER' = 'docker' 让我们不必在每个Vagrant命令中指定提供者是Docker(默认提供者是Virtualbox)。该文件的其余部分拥有Vagrant构建Docker映像和运行容器所用的选项。

获得源代码

一旦我们将Vagrantfile拷贝到Dockerfile文件夹中,我们可以运行git clone,以获取源代码:

  1. $vagrantdocker-runvertxdev--gitclonehttps://github.com/vert-x/vertx-examples.git

与之前一样,git clone完成执行后,容器将被销毁。请注意:我们还没有构建映像,Vagrant自动构建了映像。减少了手动步骤。

构建并运行应用程序

我们能够构建并运行HTTP Hello World服务器:

  1. $vagrantup

在底层,Vagrant将执行docker run,启动容器的命令由d.cmd选项指定。

想获得vertx run命令的输出结果:

  1. $vagrantdocker-logs
    1. ==>vertxdev:Succeededindeployingverticle

测试

在Linux平台上,只要运行:

  1. $curllocalhost:8080
    1. HelloWorld

在Windows和Mac上,端口8080并不从Docker主机虚拟机被转发到主vagrant主机(不过Docker容器端口被转发到Docker主机)。因而,我们需要通过ssh进入到Docker主机虚拟机,以便连接至HTTP服务器。不妨检索Vagrant默认Docker主机的ID:

  1. $vagrantglobal-status
    1. idnameproviderstatedirectory

    1. c62a174defaultvirtualboxrunning/Users/mariolet/.vagrant.d/data/docker-host

一旦检索到该设备,我们可以测试HTTP服务器了:

  1. $vagrantsshc62a174-c"curllocalhost:8080"
    1. HelloWorld

你有说过一模一样吗如何定制Docker主机

在不支持容器的平台上,默认情况下,Vagrant启动一个Tiny Core Linux(boot2docker)Docker主机。如果我们的持续整合环境、试运行环境或生产环境不运行boot2docker,我们会在这些环境的配置之间有一个缺口。这实际上是生产环境缺陷的根源,不可能在开发环境中发现。不妨试着解决这个问题。

不同环境上的不同Docker主机:实际上违反安全

如上所见,Vagrant的主要便利之一是,它让我们可以指定自定义的Docker主机。换句话说,我们并不被boot2docker和Tiny Core Linux所束缚。

Docker主机虚拟机Vagrantfile

我们将使用一个新的Vagrantfile来定义Docker主机虚拟机。下面这个文件基于Ubuntu Server 14.04 LTS:

  1. Vagrant.configure("2")do|config|
    1. config.vm.provision"docker"
    1. 下面这一行终结所有ssh连接,因此

    1. Vagrant将被迫重新连接。

    1. 那是在PATH中拥有docker命令的替代办法

    1. config.vm.provision"shell",inline:
    1. "psaux|grep'sshd:'|awk'{print$2}'|xargskill"
    1. config.vm.define"dockerhost"
    1. config.vm.box="ubuntu/trusty64"
    1. config.vm.network"forwarded_port",
    1. guest:8080,host:8080
    1. config.vm.provider:virtualboxdo|vb|
    1. vb.name="dockerhost"
    1. end
    1. end

将它保存到原始的Vagrantfile文件夹,名称为DockerHostVagrantfile。

在自定义的Docker主机中运行Docker容器

下一步指定使用这个新的虚拟机作为Docker主机,而不是默认主机,因而将下面新的两行添加到a.vm.provider代码段:

  1. config.vm.define"vertxdev"do|a|
    1. a.vm.provider"docker"do|d|
    1. [...]
    1. d.vagrant_machine="dockerhost"
    1. d.vagrant_vagrantfile="./DockerHostVagrantfile"
    1. end
    1. end

请注意:配置自定义的Docker主机有另一个好处:我们现在可以指定自定义的转发端口:

  1. config.vm.network"forwarded_port",
    1. guest:8080,host:8080

因而,我们能够从主主机操作系统里面访问vertx HTTP服务器:

不同环境上同样的Docker主机和端口转发

当然,主机虚拟机并不局限于Ubuntu。可以在Vagrant云(https://vagrantcloud.com)上发现更多的vagrant设备。值得关注的支持Docker的设备有boot2docker(原始版和改良版)和CoresOS。

使用Vagrant编排Docker容器

就在不久前,我们还每次只能运行一个Docker容器。然而在现实生活中,我们却常常需要同时运行多个容器:数据库、http、Web容器等都在单独的容器中运行。

我们在本章节将探讨使用Vagrant“多机器”环境(https://docs.vagrantup.com/v2/multi-machine/),同时执行多个docker容器。然而,我们不会考虑Docker容器分布在不同的Docker主机这一场景:所有容器都在同一个主机里面运行。

运行多个容器

多个容器

作为第一个例子,我们将使用Vert.x Event Bus Point to Point这个例子。我们利用了在文章开头定义的同一个Docker文件,并且在新的Vagrantfile文件里面配置了两个Docker容器:“vertxreceiver”和“vertxsender”:

  1. ENV['VAGRANT_DEFAULT_PROVIDER']='docker'
    1. DOCKER_HOST_NAME="dockerhost"
    1. DOCKER_HOST_VAGRANTFILE="./DockerHostVagrantfile"
    1. Vagrant.configure("2")do|config|
    1. config.vm.define"vertxreceiver"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.build_dir="."
    1. d.build_args=["-t=vertxreceiver"]
    1. d.name="vertxreceiver"
    1. d.remains_running=true
    1. d.cmd=["vertx","run","vertx-examples/src/raw/java/eventbus_pointtopoint/Receiver.java","-cluster"]
    1. d.volumes=["/src/vertx/:/usr/local/src"]
    1. d.vagrant_machine="#{DOCKER_HOST_NAME}"
    1. d.vagrant_vagrantfile="#{DOCKER_HOST_VAGRANTFILE}"
    1. end
    1. end
    1. config.vm.define"vertxsender"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.build_dir="."
    1. d.build_args=["-t=vertxsender"]
    1. d.name="vertxsender"
    1. d.remains_running=true
    1. d.cmd=["vertx","run","vertx-examples/src/raw/java/eventbus_pointtopoint/Sender.java","-cluster"]
    1. d.volumes=["/src/vertx/:/usr/local/src"]
    1. d.vagrant_machine="#{DOCKER_HOST_NAME}"
    1. d.vagrant_vagrantfile="#{DOCKER_HOST_VAGRANTFILE}"
    1. end
    1. end
    1. end

对这两个docker容器而言,vagrant_mahchine即Docker主机虚拟机的ID是dockerhost。Vagrant要足够智能化,才能重复使用dockerhost的同一个实例来运行两个容器。

想启动vertxsender和vertxreceiver,把Vagrantfile换成这一个文件,并运行vagrant up:

  1. $vagrantup
    1. ...
    1. $vagrantdocker-logs
    1. ==>vertxsender:Startingclustering...
    1. ==>vertxsender:Nocluster-hostspecifiedsousingaddress172.17.0.18
    1. ==>vertxsender:Succeededindeployingverticle
    1. ==>vertxreceiver:Startingclustering...
    1. ==>vertxreceiver:Nocluster-hostspecifiedsousingaddress172.17.0.19
    1. ==>vertxreceiver:Succeededindeployingverticle
    1. ==>vertxreceiver:Receivedmessage:ping!
    1. ==>vertxsender:Receivedreply:pong
    1. ==>vertxreceiver:Receivedmessage:ping!
    1. ==>vertxreceiver:Receivedmessage:ping!
    1. ==>vertxsender:Receivedreply:pong
    1. ==>vertxsender:Receivedreply:pong
    1. ...

即使vertxsender和vertxreceiver根本不知道彼此的主机名和IP地址,vertx eventbus协议却有一项发现功能,以便连接发送方和接收方。对于没有类似功能的应用程序而言,Docker提供了容器连接选项。

连接容器

在这个例子中,我们先运行Docker容器(vertxdev),它启动我们之前看到的那个HelloWorld Web服务器。然后,第二个容器(vertxdev-client)将使用wget执行HTTP请求:

  1. ENV['VAGRANT_DEFAULT_PROVIDER']='docker'
    1. Vagrant.configure("2")do|config|
    1. config.vm.define"vertxdev"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.image="vertxdev:latest"
    1. d.ports=["8080:8080"]
    1. d.name="vertxdev"
    1. d.remains_running=true
    1. d.cmd=["vertx","run","vertx-examples/src/raw/java/httphelloworld/HelloWorldServer.java"]
    1. d.volumes=["/src/vertx/:/usr/local/src"]
    1. d.vagrant_machine="dockerhost"
    1. d.vagrant_vagrantfile="./DockerHostVagrantfile"
    1. end
    1. end
    1. config.vm.define"vertxdev-client"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.image="vertxdev:latest"
    1. d.name="vertxdev-client"
    1. d.link("vertxdev:vertxdev")
    1. d.remains_running=false
    1. d.cmd=["wget","-qO","-","--save-headers","http://vertxdev:8080"]
    1. d.vagrant_machine="dockerhost"
    1. d.vagrant_vagrantfile="./DockerHostVagrantfile"
    1. end
    1. end
    1. end

这个新Vagrantfile文件的重要部分是这一行d.link("vertxdev:vertxdev")。由于这一行,vertxdev-client就能够解析主机名vertxdev,因而使用命令wget -qO - --save-headers http://vertxdev:8080,处理HTTP请求。

想运行容器,把Vagrantfile换成这个新文件,并运行vagrant up.。--no-parallel选项确保vertxdev容器在vertxdev-client之前启动。

  1. $vagrantup--no-parallel

不妨看一下日志,以证实发生的情况:

  1. $vagrantdocker-logs
    1. ==>vertxdev:Succeededindeployingverticle
    1. ==>vertxdev-client:HTTP/1.1200OK
    1. ==>vertxdev-client:Content-Type:text/plain
    1. ==>vertxdev-client:Content-Length:11
    1. ==>vertxdev-client:
    1. ==>vertxdev-client:HelloWorld

伙计,我的IDE在哪里

虽然集成开发环境(IDE)是开发环境的一个重要部分,但我们还没有讨论它。那是由于图形化应用程序并不通常在Docker容器里面运行。Eclipse或IntelliJ等IDE在主主机中通常很适合,源代码在主机和容器之间使用Docker卷来共享。这就是本章节所介绍的内容。

Vagrant随带synced_folder选项,以便在docker容器和主主机之间共享文件夹:

  1. ENV['VAGRANT_DEFAULT_PROVIDER']='docker'
    1. Vagrant.configure("2")do|config|
    1. config.vm.synced_folder".","/usr/local/src"
    1. config.vm.define"vertxdev-src"do|a|
    1. a.vm.provider"docker"do|d|
    1. d.build_dir="."
    1. d.build_args=["-t=vertxdev"]
    1. d.ports=["8080:8080"]
    1. d.name="vertxdev-src"
    1. d.remains_running=true
    1. d.cmd=["vertx","run","vertx-examples/src/raw/java/httphelloworld/HelloWorldServer.java"]
    1. d.vagrant_machine="dockerhost"
    1. d.vagrant_vagrantfile="./DockerHostVagrantfile"
    1. end
    1. end
    1. end

在这个例子中,vertxdev-src文件夹/usr/local/src将与主主机Vagrantfile文件夹(.)同步。请注意:Vagrant负责为我们构建Docker卷。

一旦我们把Vagrantfile换成这一个文件,就可以运行git clone,再次使用vertxdev-src容器:

  1. $vagrantdocker-runvertxdev-src--gitclonehttps://github.com/vert-x/vertx-examples.git

一旦克隆完毕,源代码将同时出现在容器和主主机。因而,我们可以直接访问,还可以编辑文件:

  1. $cdvertx-examples/src/raw/java/httphelloworld/
    1. $sed-i'''s/HelloWorld/IminadockercontainerandIfeelgood/'HelloWorldServer.java

想测试该应用程序,运行vagrant up:

  1. $cd-
    1. $vagrantup
    1. $curllocalhost:8080
    1. IminadockercontainerandIfeelgood

结束语

如果你在处理一系列不同的平台:一些平台支持Docker,另一些不支持,那么使用Vagrant来控制Docker容器很有用。在这种场景下,使用Vagrant可以让构建环境的过程在不同平台上具有一致性。

作为Vagrant的替代方案,Fig无疑值得关注。Docker雇用Fig的主要开发人员,大力支持它,将其视作一种构建基于Docker的开发环境的出色工具。