用Dokcer创建开发环境

作者在此处使用了Frankenstein一词。Frankenstein,《弗兰肯斯坦》原是英国诗人雪莱的妻子玛丽·雪莱在1818年创作的小说,被认为是世界第一部真正意义上的科幻小说。弗兰肯斯坦来自于此小说,可以理解为怪人;毁灭创造者自己之物

用Dokcer创建开发环境

知道一个事物和实现这个事物是完全不同的事情。从Docker诞生那天开始,我们就梦想着诸如“15秒部署一个项目”,“版本可控开发环境”,以及时髦的运维用语,如“滚动开发”,“软件定义架构”。处于浪尖的行业人士都在以前所未有的热情参与到将很多名词和工具,例如“编排”,“服务发现”等,定义,重新定义以及商品化大潮中。

我认为这股大潮的催化剂来自于Docker在应用和基础架构之间带来的美妙接口和抽象。开发者可以在不必知道底层架构情况下谈论基础架构,操作人员也不必花大量时间研究如何安装和管理软件。肯定有什么力量隐藏在看似简单的外表下使得大家生活简化,更加高效。

现实世界时残酷的,不要想当然认为采用一项新技术只会带来享受。过去几年经过一些项目的磨练,经历过奇怪的环境,我认为Docker也不例外。但是某一个经验一般可以直接应用到项目的下一阶段。要想从Docker获得功力,必须浸淫到实际项目中去磨练。

过去一年中,我全身心投入去教授我的关于Dokcer基础的书,Docker in Action。

我注意到几乎所有人开始学习Docker技术时都会纠结于如何创建开发环境,然后才能了解生态系统之内大家的关系。每个人开始都会认为使用Docker会使环境搭建变的简单,也不是完全不对,有很多“容器化”教程都涵盖了创建一个image和如何将某个工具打包到容器(Container)内,但是如何将开发环境Docker化是一个完全不同的事情。

作为一个踏坑先驱者,我可以分享一下我的经验。

我曾经是一个资深Java使用者,但这个分享的经验不是关于Java的,而是围绕着我使用Go和Node开发应用发生的。我有一定的Go开发经验,主动提高在这一领域的能力。进入一个不熟悉领域迅速上手碰到的主要问题就是如何获得正确的工作流,而且我还比较厌恶在笔记本上不断安装软件,这些都驱使我尝试用Docker做这些工作,或者有时候采用Vagrant。

用Dokcer创建开发环境

我所参与的项目是用Go写一个标准的REST服务,基于gin,依赖Redis和NSQ的某些库和服务。也就是说需要import一些本地运行着的Redis和NSQ实例的库,更有趣的是我还使用了一些服务于NGINX的静态资源。

对门外汉来说,Go是一种编程语言,实际上还有一种命令行工具也叫“go”。从依赖型管理、编译、测试用例到其它各种任务都使用它。对Go项目来说,除了Git和一个好用的编辑器,剩下就是跟它打交道了。然而还是有一个问题,我不想在笔记本上安装Go,笔记本上我只想安装Git和Docker。这些问题限制了其他环境下的兼容性,并且对新手来说降低了门槛。

这个项目有运行时依赖,意味着此工具集需要为简单环境定义和编排而包括Docker Compose。 很多人会为此感到不适应,那么我们怎么办?开始创建一个Dockerfile或者docker-compose.yml?好吧,先让我告诉大家我是怎么办的,然后解释为什么这么做。

用Dokcer创建开发环境

此案中,我希望我的本地包是完全自动的。我不喜欢手动逐条执行步骤,而且我的vim配置文件也很简单。我只想从“是否运行”层次控制运行环境。本地化开发环境目标被快速复制,不仅用于提高生产效率,而且用于共享Docker images。 我最终完成了Dockerfile,用来产生包含Go,Node,和我最经常使用的打包工具Gulp的images。 此Dockerfile没有嵌入代码,image也没有嵌入Gulpfile。相反的,在一个建立了的GOPATH(Go workspace的根路径)上定义了一个卷。

最终,我为此images设置了给gulp提供服务的entrypoint,设置默认命令来监控。输出images肯定不是我称为build artifact的东西,从这个意义上来讲,此环境唯一做的就是提供了一个运行实例,帮助我们判断是否代码运行。对我的场景来说,运行的非常棒。而我将“artifacts”用于称呼另外一个build。

下一步我用Compose定义本地开发环境。首先定义了在images中用到的所有Docker Hub 中定义的依赖服务,将他们连接到某一个“目标”服务。此服务引用了新Dockerfile从哪里生成,将本地源目录绑定到新image期望输出的挂载点,暴露一些可以测试的端口。然后,添加了一个服务,可以不断地向目标服务循环发起一系列集成测试。最终,我添加了NGINX服务,挂载了有很多配置文件和静态assets的卷。使用卷的好处在于重复使用配置文件和assets而不用重建image。


  1. $ cat ./service/local.df 
  2.  
  3. FROM golang:alpine 
  4.  
  5. RUN apk --update add --no-cache git nodejs 
  6.  
  7. RUN npm install --global gulp 
  8.  
  9. ENV GOPATH=/go PATH=$PATH:/go/bin 
  10.  
  11. VOLUME ["/go/src/github.com/.../myproj""/go/pkg","/go/bin"
  12.  
  13. WORKDIR /go/src/github.com/.../myproj 
  14.  
  15. Bring in dependencies in the imageRUN go get github.com/bitly/go-nsq && \ 
  16.  
  17. go get github.com/codegangsta/cli && \ 
  18.  
  19. go get github.com/gin-gonic/gin 
  20.  
  21. CMD ["gulp"
  22.  
  23. $ cat ./service/gulpfile.js 
  24.  
  25. var gulp = require('gulp'); 
  26.  
  27. var child = require('child_process'); 
  28.  
  29. var server = null
  30.  
  31. gulp.task('default', ['watch']); 
  32.  
  33. gulp.task('watch', function() { 
  34.  
  35. gulp.watch('./**/*.go', ['fmt','build','spawn']); 
  36.  
  37. }); 
  38.  
  39. gulp.task('fmt', function() { 
  40.  
  41. return child.spawnSync('go', ['fmt']); 
  42.  
  43. }); 
  44.  
  45. gulp.task('build', function() { 
  46.  
  47. return child.spawnSync('go', ['install']); 
  48.  
  49. }); 
  50.  
  51. gulp.task('spawn', function() { 
  52.  
  53. if (server) 
  54.  
  55. server.kill(); 
  56.  
  57. server = child.spawn('myproj'); 
  58.  
  59. server.stderr.on('data', function(data) { 
  60.  
  61. process.stdout.write(data.toString()); 
  62.  
  63. }); 
  64.  
  65. server.stdout.on('data', function(data) { 
  66.  
  67. process.stdout.write(data.toString()); 
  68.  
  69. }); 
  70.  
  71. }); 
  72.  
  73. $ cat docker-compose.yml 
  74.  
  75. web: 
  76.  
  77. image: nginx 
  78.  
  79. volumes: 
  80.  
  81. - ./web/assets:/var/www 
  82.  
  83. - ./web/config:/etc/nginx/conf.d 
  84.  
  85. integtest: 
  86.  
  87. build: ./integ 
  88.  
  89. links: 
  90.  
  91. - service 
  92.  
  93. service: 
  94.  
  95. build: ./service 
  96.  
  97. dockerfile: local.df 
  98.  
  99. volumes: 
  100.  
  101. - ./service/src/:/go/src/github.com/.../myproj 
  102.  
  103. links: 
  104.  
  105. - nsqd 
  106.  
  107. - redis 
  108.  
  109. nsqd: 
  110.  
  111. image: nsqio/nsq 
  112.  
  113. ... 
  114.  
  115. redis: 
  116.  
  117. image: redis 
  118.  
  119. ... 

所有代码最终会在电脑上生成本地开发环境,当使用:


  1. docker-compose up –d 

时,会启动git clone,然后循环运行;不需要重建image或者重启容器。每当.go文件发生变化,Gulp就会重建,并且在运行的容器中重启我的服务。就这么简单。

创建此环境很简单吗?不尽然,但是确实实现了。难道不用容器,而在本地直接安装Go,Node,Gulp不是更简单吗?也许在这个场景是,但也只限于用Docker运行此依赖服务。我不喜欢这样。

我曾经要管理这些工具的不同版本,而产生了复杂的环境变量,到处生成artifacts。我不得不提醒同事们注意这些容易发生冲突的环境变量,他们太缺乏集中版本控制了。

也许你并不喜欢上面描述的环境,或者对项目有不同的需求。很好,确实是这样,本文并不是让所有工具都运行在Docker中,如果这样就说明并没考虑过要解决什么问题。

当我设计这个环境时,考虑过下面几个问题,顾虑,以及某些潜在答案。当开始Docker工作环境时,就会发现实际情况可能比自己的回答更糟糕。

当你考虑打包和环境时,最先考虑的因素是什么?

这个确实是最重要的问题。在此场景中,有几个选项。我可以使用go直接在容器内编程,看起来如下:


  1. get dependencies$ docker run --rm -v "$(pwd)"/go/src/app golang:1.5 go get -d -v 
  2.  
  3. start the other servicesbuild and link$ docker run --rm -v "$(pwd)":/go/src/app golang:1.5 go install \ 
  4.  
  5. github.com/allingeek/myproj 
  6.  
  7. run the program stand alone$ docker run --rm -v "$(pwd)"/bin/myproj:/bin/myproj alpine myproj 
  8.  
  9. to iterate, make changes and repeat the last two steps 

其实这个示例中大部分bolierplate可以通过shell别名或者函数隐藏,感觉Go是安装在自己的设备中似的,还可以跟Go工作流联系,创建artifacts。这些特性对非服务项目有益处,但是对库和软件项目就不一定了。

假设你已经在使用Gulp,make,ant或者其他脚本,那么可以继续,并且使用Dokcer作为这些工具的目标。

另外一种方法,我可以通过使用Dockerbuild来定义和控制我的build,获得更多面向Docker的经验。代码如下:


  1. $ cat Dockerfile 
  2.  
  3. FROM golang:1.5-onbuild 
  4.  
  5. start the other servicesinstall dependencies, build, and link$ docker build -t local/myproj . 
  6.  
  7. run the program$ docker run --rm local/myprog 
  8.  
  9. to iterate, make changes and repeat the last two steps 

使用Dokcer来控制build有若干好处。可以使用以前编译好的image,Dockerfilebuilds使用缓存方法,使得编译工作只重复最小的步骤(假设有一个很棒的Dockerfile)。最后,这些builds生成的images也可以跟其他开发者共享。

这个案例中,我使用golang资源库中的onbuildimage作为基础。其中包括一些很棒的下载依赖包逻辑。这个方法会生成可以方便用于其他非生产环境的Dockerimage。这个方法对于生产级别的image的问题在于,必须有步骤避免大image并且包括某些初始化脚本,用于启动和监控服务前验证状态。

有意思的是,Docker使用一系列脚本,Makefiles和Dockerfiles。build系统相对很健壮了,负责各种测试,linting等,以及各种操作系统和架构的artifacts。本场景中,容器是用来产生二进制的工具,然而是从一个本地build image中实现的。

扩充Docker build的选项,可以使用Compose来定义一整套开发环境。


  1. $ cat Dockerfile 
  2.  
  3. FROM golang:1.5-onbuild 
  4.  
  5. $ cat docker-compose.yml 
  6.  
  7. service: 
  8.  
  9. build: . 
  10.  
  11. links: 
  12.  
  13. - redis 
  14.  
  15. - nsq 
  16.  
  17. redis: 
  18.  
  19. image: redis 
  20.  
  21. nsq: 
  22.  
  23. image: nsqio/nsq 
  24.  
  25. install dependencies, build, link, launch dependency services, run$ docker-compose up -d 
  26.  
  27. to iterate, make changes and then$ docker-compose build && docker-compose up -d 

Compose负责环境管理。如果觉得系统非常干净并不奇怪,Compose把所有事情都联系起来,优化卷管理,当images缺失时自动build,汇总日志输出。我之所以选这些开关是为了简化服务依赖,也因为它能生成我需要的artifacts。

这个示例是一个运行时容器,Compose或者Docker都有合适的工具做到这点。此场景中,也可能更需要一个分布式image,或者可能希望build可以为本机产生一个二进制文件。

如果期望获得想要的image,必须确保源码或者预编译库在build时候嵌入image中。build时候没有挂载卷,也即需要每次重复时都要重建image。

如果希望在容器内部产生某些artifacts,则需要引入挂载卷。使用Docker命令行或者Compose环境可以很容易实现。但是要注意,除非容器在运行,否则build并不工作,也就意味着不能只用dockerbuild。

汇总

目前没有Docker方式创建开发环境。Docker是一个可编排工具,不只是圣书。与其使用别人已有的dockerbuild系统,不如花一定时间学习此工具,明确自己的需求,然后创建适合自己的Docker环境。


本文作者:佚名

来源:51CTO

上一篇:vmware安装系统注意点


下一篇:python 特效之一行代码能做哪些丧心病狂的事情