Docker Workflow(一):一个可用于生产环境的Docker工作流


【编者的话】作者工作于墨西哥IIIEPE研究院,他将通过一系列文章,为我们逐一讲述他们在Docker实际应用过程中的经验与教训,给后来者提供一些参考。本文主要介绍了他基于Docker的开发工作流,包括GitLab、Jenkins、Registry、Nginx。

Docker现在已经两岁了(译者注:Docker于2013年3月13日首次发布),IIIEPE已经在生产环境中使用Docker3个来月。在此,我分享一些我们的经验和设计的工作流。

我们运行着多个使用Drupal、PHP和Node.js的网站,我们的目标是使用Docker运行所有的应用,因此我们设计了以下工作流:
  • 所有开发人员使用Docker来创建应用。
  • 我们的GitLab实例配置了Webhook,当检测到一个新的推送时,它将命令Jenkins运行一个任务。
  • 每个Jenkins任务都包含相同的布置:从GitLab中克隆最新代码,运行测试,登录到我们的私有Docker Registry,使用最新代码构建一个新的镜像,然后将镜像推送上去。
  • 最后,我们的编排软件Maestro-NG将部署新版本的镜像。
  • 我们的负载均衡器将检测这些变化,并重载新的配置。


每一个步骤都需要几天的规划、测试和工作来设计基本准则。

我们做的第一件事是构建满足自己要求的基础镜像。镜像发布到Docker Hub中,它们包含了除了应用本身之外所有用于运行应用的东西。每次修改其中一个基础镜像,我们就会运行一个Jenkins任务来拉取新镜像,并触发后续任务来重新构建依赖此基础镜像的所有镜像。

创建完镜像之后,我们需要为所有应用定义一个标准结构。我们所有的应用都使用以下结构来组织:
/application
/logs
/files
Dockerfile
fig.yml.example
docker-compose.yml.example
Makefile

/application目录是应用的根目录。

/logs/files目录用于开发,以便应用可以写入日志和文件。这两个目录会被Git忽略,并在生产环境中完全排除。

Dockerfile是Jenkins用于构建镜像的文件,开发人员几乎不需要接触这个文件,后面详述。

fig.yml.exampledocker-compose-yml.example是开发人员用于启动应用的文件。这二者均不用于生产环境,当开发人员克隆一个项目时,他需要复制这个example文件并填入他/她的值。

Makefile是拼图的最后一块,通过它我们可以拥有一个标准的命令集用于所有应用,并对开发人员隐藏各种各样的复杂性。

Dockerfile

每个应用的Dockerfile与其它应用非常类似,这个文件的最重要工作就是构建包含所有待部署代码的最终镜像。我们来看一个例子:
FROM iiiepe/nginx-drupal6

ENV MYSQL_ENV_MYSQL_DATABASE somedb  
ENV MYSQL_ENV_MYSQL_USER root  
ENV MYSQL_ENV_MYSQL_PASSWORD 123  
ENV MYSQL_PORT_3306_TCP_ADDR localhost  
ENV MYSQL_PORT_3306_TCP_PORT 3306  
ENV BASE_URL http://example.com  
ENV DRUPAL_ENVIRONMENT production

EXPOSE 80

RUN usermod -u 1000 www-data  
RUN usermod -a -G users www-data

ADD ./application /var/www  
RUN chown -R www-data:www-data /var/www  

Dockerfile依赖于我们构建的基础镜像,在此之上,它只是设置了一些环境变量默认值、声明要暴露的端口,并将应用代码添加到/var/www

正因为我们构建镜像的这种方式,在Jenkins和开发人员之间唯一的差别是,Jenkins将添加整个应用目录到/var/www中,而开发人员只是映射一下目录。

接下来这个是Docker Compose,他非常酷。
mysql:  
image: mysql:latest
expose:
- "3306"
ports:
- "3307:3306"
environment:
MYSQL_DATABASE: database
MYSQL_USER: root
MYSQL_PASSWORD: admin123
MYSQL_ROOT_PASSWORD: admin123
web:  
image: iiiepe/nginx-drupal6
volumes:
- application:/var/www
- logs:/var/log/supervisor
- files:/var/www/sites/default/files
ports:
- "80:80"
links:
- mysql:mysql
environment:
BASE_URL: http://local.iiiepe.net
DRUPAL_ENVIRONMENT: development

Docker Compose用此文件来初始化。在本例中,我们定义了一个应用,它包括两个容器:一个MySQL容器和一个Web容器。

MySQL容器定义了mysql镜像要使用的环境变量。同时将宿主的3307端口映射到窗口的3306端口上。这允许我们使用任何客户端访问MySQL服务器。

web容器使用了与Jenkins构建最终镜像时使用的相同镜像(见上述Dockerfile),但它同时共享了一些数据卷。在宿主和容器间共享的卷有应用、文件和日志。这实际是开发环境和生产环境间最大的改变:在生产环境中,容器的代码是在镜像中的,这允许我们在任何服务器上启动容器;而在开发环境中,目录只是共享的,因此在应用目录里,任何新的文件或对文件的修改都将即时地反映到容器里。

BASE_URL变量指向了http://local.iiiepe.net,这不是个真实的地址,只是用于标准化应用访问的一个方式。因为我们有些人使用Mac和Boot2Docker,我们需要一个标准的地址以便所有人可以将其写入到/etc/hosts文件中。

我的Mac上的/etc/hosts是这样的:
127.0.0.1    localhost
192.168.59.103    local.iiiepe.net

在一台Linux机器上,看起来是这样的:
127.0.0.1    localhost local.iiiepe.net

最后,我们定义了两个环境变量来决定应用的配置文件的一些设置。

自定义设置

Drupal需要一个settings.php来存储数据库信息,包括密码。该文件将被Git忽略,以免将你的密码提交上去,我们决定修改这个文件,让它使用环境变量并将其提交。

以下是一个Drupal 6网站的settings.php里的重要部分:
$username = getenv("MYSQL_ENV_MYSQL_USER");
$password = getenv("MYSQL_ENV_MYSQL_PASSWORD");
$host = getenv("MYSQL_PORT_3306_TCP_ADDR");
$port = getenv("MYSQL_PORT_3306_TCP_PORT");
$database = getenv("MYSQL_ENV_MYSQL_DATABASE");

$db_url = 'mysql://' . $username . ':' . $password . '@' . $host . '/' . $database;

正如你所看到的,没有密码会被提交。密码和其它敏感值将通过ENV变量注入。

有些网站使用Apache Solr作为搜索引擎,但在开发时,我们不希望能写入到Apache Solr,因此需要一个类似DRUPAL_ENVIRONMENT的ENV变量,完成类似下面的settings.php文件的事情:
$conf = array();
if(getenv("DRUPAL_ENVIRONMENT") === "development") {  
// Disable apache solr writting
$conf["apachesolr_read_only"] = 1;
}

Makefile

由于命令很长,使用Docker非常不便,因此Docker Compose(fig)对此很有帮助。我们更进一步尝试让事情变得更简单一些。

这是我们使用在一个Drupal网站上的Makefile:
CURRENT_DIRECTORY := $(shell pwd)

start:  
@fig up -d

clean:  
@fig rm --force

stop:  
@fig stop

status:  
@fig ps

cli:  
@fig run --rm web bash

log:  
@tail -f logs/nginx-error.log

cc:  
@fig run --rm web drush cc all

restart:  
@fig stop web
@fig start web
@tail -f logs/nginx-error.log

.PHONY: clean start stop status cli log cc restart

使用Makefile比使用Docker Compose或Fig简单得多,因为我们可以创建类似make cc的快捷方式来运行类似drush cc all这样频繁使用的命令。

有关Makefile的最后一点说明是:我们依然使用Fig。因为在我们设计这个工作流时,Docker Compose还不可用,而且我们团队里的一些开发人员还在使用它,我们决定为Docker Compose建立名为Fig的符号连接,名称更短且更实用。安装Docker Compose后,你可以删除fig并创建符号连接:
sudo rm /usr/local/bin/fig  
sudo ln -s /usr/local/bin/docker-compose /usr/local/bin/fig  

走的弯路

我们走过一些弯路,我将在别的文章中做介绍,不过有一个我想特别说一下。使用Docker最大的好处是,开发人员可以在与生产环境相同的环境上运行应用,且只损失一点点性能。

我看过一些文章,说他们在Docker之外做开发,然后在需要部署时构建镜像并发送到生产环境。如果你这么做,那你就错了,因为你开发所用的环境与生产机上运行的不一致。不要每次都构建镜像,相反的,在宿主和容器间共享卷,让别人在你每次推送时构建镜像。

未完待续……

文章还远未结束,不过它已经太长了。我们依然需要说明我们是如何整合Maestro-NG、配置Jenkins以及负载均衡器是如何工作的。咱们回见!

原文链接:A production ready Docker workflow(翻译:梁晓勇 校对:李颖杰)

1 个评论

fig已经被docker给收购了,现在这个fig和compose是一个东西。文章棒棒哒

要回复文章请先登录注册