先来看一个经常会发生的例子:
假如你被指派为一个用以监测运动健身的移动应用建立REST API,同时要求在笔记本上的开发环境完成第一个端点的编码。在运行了所有单元测试并成功通过后,你将代码放入Git,并且通知QA工程师程序已经等待检测,而且一切运行良好。然而,当QA工程师认真地将最新版本代码部署到测试环境后却发现,这个新开发的REST端点往往连前几分钟的测试都通不过。
为什么会发生这样的情况?你明明已经完整的运行了单元测试,而代码传递给QA工程师之前又没发生任何问题。在与QA工程师一起奋斗数个小时后,你发现测试环境使用了一个过时版本的第三方库,而正是这个原因导致了你的REST端点崩溃。
在软件开发过程中,这个问题并不稀奇,轻视开发、测试、演示、生产环境的区别很容易会造成应用程序崩溃,而传统的类似改变管理过程的处理方法已经适应不了当下应用程序快速的建立和部署周期。取而代之,我们需要的是将开发环境与测试环境无缝对接,消除人工干涉和容易出错的资源供应及配置。
长期以来,AWS为开发者提供了自动化建立可靠及有效的开发环境。类似Amazon EC2和AWS CloudFormation等服务都允许开发者们像代码一样的管理基础设施。通过CloudFormation服务,AWS资源可以使用JSON做预分配。CloudFormation模板可以在应用程序代码中正确的进行描述,通过EC2的自动化能力,用户可以快速和可靠地建立及结束某个环境。正是基于这个原因,AWS非常适合开发和测试工作。
类似Docker等容器技术,将资源配置声明理念进行的更深一步。类似CloudFormation提供给EC2实例的功能,Docker为容器建立提供了一个非常实用的声明语法。同时,Docker容器并不依赖任何虚拟化平台,或者一个专用的操作系统。容器的运行仅仅需要一个Linux内核,这就意味着它几乎可以运行在任何环境之下——不管是笔记本或者是EC2实例。
Docker容器的架构如下图所示:
图1
Docker容器使用了一个被称为libcontainer的执行环境,它为不同的Linux内核隔离特性提供了一个接口,类似命名空间及控制组。这种架构允许多个容器在共享同一个Linux内核的情况下完全隔离地运行。鉴于Docker容器并不需要一个专用的操作系统,因此它比虚拟机更加的便捷和轻量。
Docker平台架构由下图一系列组件组成:
图2
Docker客户端并不与运行的容器直接通信,取而代之,它通过TCP sockets或者REST与Docker守护进程通信,而守护进程将与主机上的容器直接通信。同时,Docker客户端并不需要与守护进程安装在同一台主机上。
在使用Docker时有3个理念必须理解:镜像(image)、注册表(registry)和容器(container)。
镜像,用于建立容器组件,它是个只读模板,使用它可以发布一个以上的容器实例。理论上说,它非常类似于AMI。
注册表用于储存镜像,既可以在本地,也可以在远程。当我们发布一个容器时,Docker首先会在本地注册表上搜索镜像。如果在本地注册表上没有发现,它随后会搜索远程公用的注册表,也就是DockerHub。如果在DockerHub发现所需镜像,Docker会将它下载到本地注册表,并使用它来发布所需容器。DockerHub非常类似于GitHub,我们可以使用它来建立公用或私有镜像资源。鉴于这个属性,有效及安全的镜像发布将非常便捷。
可以这么说,容器运行在一个镜像的实例上,Docker使用容器来执行和运行被打包在镜像中的软件。
你也可以为一个正在运行的容器建立一个Docker镜像,类似为一个EC2实例建立AMI。举个例子,用户可以发布一个容器,并使用类似APT或者YUM的包管理器安装大量的软件,然后将更新提交到一个新的Docker镜像。
但是这里还存在更有效和灵活的途径来建立镜像,那就是使用DockerFile,它允许声明式的镜像定义。DockerFile语法由一系列的命令组成,我们可以用之安装和配置镜像中包括的各种组件。写一个DockerFile就像茶余饭后使用UserData配置一个EC2实例那么简单。类似一个CloudFormation模板,DockerFile可以使用一个版系统进行跟踪和发布,你可以将DockerFile比作一个镜像的建立文件。
那么在运动健身移动应用的打造中,Docker又会起到什么样的作用?应用程序架构由下图中的组件构成:
图3
首先,我们需要建立一个Docker镜像用于发布运行中REST端点的容器。我们可以使用它在笔记本上测试我们的代码,而QA工程师则可以使用它在EC2实例上对应用程序进行测试。REST端点使用Ruby和Sinatra框架建立,因此它们需要被封装到容器中。我们将使用Amazon DynamoDB作为后端,因此,为了保证应用程序在AWS内外都可以使用,Docker镜像同样需要封装DynamoDB数据库。这样一来,DockerFile的代码可能如下所示:
FROM ubuntu:14.04
MAINTAINER Nate Slater <slatern@amazon.com>
RUN apt-get update && apt-get install -y curl wget default-jre git
RUN adduser --home /home/sinatra --disabled-password --gecos '' sinatra
RUN adduser sinatra sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER sinatra
RUN curl -sSL https://get.rvm.io | bash-s stable
RUN /bin/bash -l -c "source /home/sinatra/.rvm/scripts/rvm"
RUN /bin/bash -l -c "rvm install 2.1.2"
RUN /bin/bash -l -c "gem install sinatra"
RUN /bin/bash -l -c "gem install thin"
RUN /bin/bash -l -c "gem install aws-sdk"
RUN wget -O /home/sinatra/dynamodb_local.tar.gz https://s3-us-west
-2.amazonaws.com/dynamodb-local/dynamodb_local_2013-12-12.tar.gz
RUN tar -C /home/sinatra -xvzf /home/sinatra/dynamodb_local.tar.gz
DockerFile的内容必须要是自声明的,RUN关键字用以执行命令。默认情况下,命令执行在超级用户权限下。鉴于需要使用RVM来安装Ruby,我们需要使用USER关键字来转换到Sinatra用户权限,因此Ruby相关文件会安装到用户目录下。从USER命令生效起,随后的RUN命令都是使用Sinatra用户权限来执行。这同样意味着,当容器发布后,它也是以Sinatra用户权限来执行命令的。
Docker守护进程负责管理镜像与运行容器,而Docker客户端通常被用以将命令发送到守护进程。因此在使用上文DockerFile建立镜像时,我们需要执行这个客户端命令:
$ docker build --tag=”aws_activate/sinatra:v1" .
在docker.io网站上,我们可以发现完整的Docker客户端命令说明文档。下面,我们着重看一下我们建立镜像所使用的命令。Tag选项用于在镜像上建立识别符,其典型值是owner/repository:version。这样一来,我们可以轻易的识别镜像中所包含的内容,并且可以从注册表中轻易的发现这个镜像的所有权。
在执行build命令后,我们可以在DockerFile中使用声明来拥有一个配置好的镜像。DockerFile如下:
$ docker imagesREPOSITORY TAG IMAGE ID CREATED
VIRTUAL SIZE
aws_activate/sinatra v1 84b6d4a5a22b
36 hours ago 942.2 MB
ubuntu 14.04 96864a7d2df3
6 days ago 205.1 MB
毫无疑问,我们可以看到Docker建立好了我们所需的镜像,并给它分配tag中指定的所有权,同时还会拥有一个唯一的镜像ID。现在,我们就可以通过新建立好的镜像来发布容器:
$ docker run -it aws_activate/sinatra:v1 /bin/bash
运行这个命令后,容器将成功发布,同时我们将进入Bash shell。在Bash shell中,我们可以像与Linux服务器一样与容器交互。鉴于我们建立的是一个Web应用程序,我们会从Git repository中克隆最新版本到容器,用以运行我们的单元测试,并做好给QA传送的准备。当代码被克隆到容器之后,并且做好了被测试的准备,我们会将运行容器中所做的更新克隆到一个新的镜像。为了完成这个步骤,我们需要确定容器的ID:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b9d03d60ba89 aws_activate/sinatra:v1 "/bin/bash" 11 minutes ago Up
11 minutes nostalgic_davinci
下一步,我们运行提交命令:
$ docker commit -m “ready for testing” b9d03d60ba89
aws_activate/sinatra:v1.1
现在我们在本地注册表中会拥有一个新的容器:
$ docker imagesREPOSITORY TAG IMAGE ID CREATED
VIRTUAL SIZE
aws_activate/sinatra v1.1 40355be9eb8f
21 hours ago 947.5 MB
aws_activate/sinatra v1 84b6d4a5a22b
3 days ago 942.2 MB
ubuntu 14.04 96864a7d2df3
8 days ago 205.1 MB
Version 1.1版本镜像拥有服务我们REST端点所需的Sinatra应用程序。我们可以使用以下命令来运行Web应用程序:
$ docker run -d -w /home/sinatra -p 10001:4567
aws_activate/sinatra:v1.1 ./run_app.sh
上面命令告诉Docker需要做以下的工作:
1. 从镜像aws_activate/sinatra:v1.1建立一个容器
2. (-d)表示以分离的形式运行容器
3. 将工作路径设置为/home/sinatra (-w)
4. 映射容器端口到主机端口4567——10001
5. 在容器中执行一个叫做run_app.sh的shell script
这个shell script会在容器中启动DynamoDB,并且在4567下使用Thin网络服务器的模式发布Sinatra应用程序。现在,如果我们在运行这个Docker容器的笔记本浏览器中指向http://localhost:10001/activity/1,我们将看到以下结果:
{"activity_id":"1",
"user_id":" db430d35-92a0-49d6-ba79-0f37ea1b35f7",
"type":"meal",
"calories":100,
"date":"2014-09-26 15:33:58 +0000"}
我们的端点看起来运行良好——活动记录从本地DynamoDB中取出,并从Sinatra应用程序代码中以JSON的格式返回。
如果想让这个容器可以给QA工程师做进一步测试,我们可以将之推送给DockerHub这个公用的注册表。类似GitHub,DockerHub提供了公用和私有两个选项,可以满足这个容器不面向所有人的需求。
QA工程师将在EC2中运行这个实例,这就意味着我们将需要一个配置了Docker守护进程和客户端软件的EC2实例。假设需要使用CloudFormation启动一个EC2实例和CloudFormation表,我们可以借助CloudFormation AWS::EC2::Instance类型的UserData属性,使用Docker软件安装程序中的引导程序。CloudFormation中规定EC2实例的JSON文件可能拥有类似如下代码:
"DockerInstance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"InstanceType": "t2.micro",
"ImageId": {"Fn::FindInMap" : ["RegionMap",{"Ref" :
"AWS::Region"}, "64"]},
"KeyName": {"Ref": "KeyName"},
"SubnetId": {"Ref": "SubnetId"},
"SecurityGroupIds": [{"Ref": "SecurityGroupId"}],
"Tags": [{"Key": "Name", "Value": "DockerHost"}],
"UserData": {"Fn::Base64":
"#include https://get.docker.io" }
}}
这样一来,如果QA工程师登入CloudFormation堆栈建立的EC2实例,镜像可以使用如下命令从远程的DockerHub注册表中取出:
$ docker pull aws_activate/sinatra:v1.1
这里从镜像中启动容器的命令和上文没太大的区别,有一个区别是环境变量会使用“-e”选项来设置,而Sinatra应用程序则会被配置为“test”环境。这个配置将使用区域端点(regional endpoint)来连接DynamoDB,而不是本地端点:
$ docker run -d -w /home/sinatra –e “RACK_ENV=test” -p 10001:4567
aws_activate/sinatra:v1.1 ./run_app.sh
到这里,QA工程师就可以通过HTTP在公共DNS(名称是EC2实例,端口号是10001)下访问REST端点。当然,前提你还需要设置一个安全组规则,并允许10001端口访问。如果发现任何bug,运行的容器可以提交到一个新的镜像,指定一个合适的版本号,并将之提交到注册表。容器的状态会被完整的保存,因此软件工程师可以便捷的复制QA中发现的问题,检查日志文件并且做常规的排错。
我们希望通过本文让用户对Docker有一个很好的认识,同时也认识到AWS和Docker的完美兼容。Docker的可移植性让它非常适合开发和测试,因为我们可以在多个团队中非常便捷的共享容器。EC2和CloudFormation完美的支撑了容器在AWS中的运行,但是AWS的便利绝不止于此。AWS ElasticBeanstalk,允许开发者将整个应用程序堆栈部署到Docker容器。经常关注本网站,你将看到更多关于AWS中运行Docker的博客。