Docker 技术文章
介绍
持续集成(CI)是指开发人员尽可能频繁地集成代码,并且每次提交都经过自动化构建之前和之后的测试,然后才合并到共享存储库中的一种实践。
CI加速了您的开发过程,并最大程度地减少了生产中的关键问题的风险,但设置起来并不是一件简单的事情;自动化构建在不同的环境中运行,其中运行时依赖项的安装和外部服务的配置可能与您的本地和开发环境不同。
Docker是一个容器化平台,旨在简化环境标准化的问题,以便应用程序的部署也可以标准化(了解更多关于Docker的信息)。对于开发人员来说,Docker允许您通过在本地容器中运行应用程序组件来模拟生产环境。这些容器可以使用Docker Compose轻松自动化,而与应用程序和底层操作系统无关。
本教程使用Docker Compose演示了CI工作流程的自动化。
我们将创建一个基于Docker的“Hello world”类型的Python应用程序和一个Bash测试脚本。Python应用程序将需要两个容器来运行:一个用于应用程序本身,另一个用于存储的Redis容器,这是应用程序所需的一个依赖项。
然后,测试脚本将被Docker化到自己的容器中,并且整个测试环境将移动到一个名为__docker-compose.test.yml__的文件中,以便我们可以确保每次执行测试时都在一个新的统一的应用程序环境中运行。
这种方法展示了如何为您的应用程序构建一个相同的、新鲜的测试环境,包括其依赖项,每次测试它时。
因此,我们可以独立于正在测试的应用程序和底层基础架构自动化CI工作流程。
要求
开始之前,您需要:
一个Ubuntu 14.04服务器
一个具有sudo特权的非root用户。您可以按照本教程进行设置
对Docker和Docker Compose有一些了解;这些链接仅供参考,因为我们将在本教程中安装软件
测试、测试和更多(自动化)测试! 您应该已经对测试的优势以及将CI应用到您的开发工作流程中有所了解
步骤1 — 安装Docker
如果Docker尚未在您的服务器上可用,最简单的方法是下载并执行官方Docker安装脚本,该脚本会提示输入sudo密码(在此处找到更多有关安装Docker的信息):
wget -qO- https://get.docker.com/ | sh
为了更轻松地使用Docker,使用以下命令将您的用户添加到docker组中:
sudo usermod -aG docker $(whoami)
注销然后重新登录到服务器以激活docker组对您的用户的权限。
步骤2 — 安装Docker Compose
Docker Compose是一个使用声明性方法定义和运行多容器应用程序的开源工具。为了安装Docker Compose,执行以下命令:
sudo apt-get update
sudo apt-get -y install python-pip
sudo pip install docker-compose
通过执行以下命令验证docker-compose
是否正确安装:
docker-compose --version
您应该看到类似以下内容:
docker-compose version 1.6.2, build 4d72027
这应该告诉您已安装的docker-compose
版本。
步骤3 — 创建“Hello World” Python应用程序
在这一步中,我们将创建一个简单的Python应用程序,作为您可以使用此设置进行测试的应用程序的示例。
通过执行以下命令为我们的应用程序创建一个新的文件夹:
cd ~
mkdir hello_world
cd hello_world
使用_nano_编辑一个名为app.py
的新文件:
nano app.py
添加以下内容:
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host="redis")
@app.route("/")
def hello():
visits = redis.incr('counter')
html = "<h3>Hello World!</h3>" \
"<b>Visits:</b> {visits}" \
"<br/>"
return html.format(visits=visits)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
app.py
是一个基于Flask的Web应用程序,连接到Redis数据服务。visits = redis.incr('counter')
这一行增加了访问次数,并将该值持久化在Redis中。最后,以HTML形式返回带有访问次数的Hello World
消息。
我们的应用程序有两个依赖项,Flask
和Redis
,您可以在前两行中看到。在我们执行应用程序之前,必须定义这些依赖项。编辑一个新文件:
nano requirements.txt
添加以下内容:
Flask
Redis
步骤 4 —— 为 “Hello World” 应用创建 Docker 镜像
Docker 使用一个名为 Dockerfile
的文件来指示构建给定应用的 Docker 镜像所需的步骤。编辑一个新文件:
nano Dockerfile
添加以下内容:
FROM python:2.7
WORKDIR /app
ADD requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
ADD app.py /app/app.py
EXPOSE 80
CMD ["python", "app.py"]
让我们分析每一行的含义:
FROM python:2.7
:表示我们的 “Hello World” 应用镜像是从官方的python:2.7
Docker 镜像构建的WORKDIR /app
:将 Docker 镜像内的工作目录设置为/app
ADD requirements.txt /app/requirements.txt
:将文件requirements.txt
添加到我们的 Docker 镜像中RUN pip install -r requirements.txt
:安装应用程序的pip
依赖项ADD app.py /app/app.py
:将我们的应用程序源代码添加到 Docker 镜像中EXPOSE 80
:表示我们的应用程序可以通过端口 80(标准的公共 Web 端口)访问CMD ["python", "app.py"]
:启动我们的应用程序的命令
这个 Dockerfile
文件包含了构建我们的 “Hello World” 应用的主要组件所需的所有信息。
依赖关系
现在我们来到示例的更复杂部分。我们的应用程序需要 Redis 作为外部服务。在传统的 Linux 环境中,这种依赖关系可能很难以相同的方式设置,但是使用 Docker Compose,我们可以以可重复的方式每次设置它。
让我们创建一个 docker-compose.yml
文件来开始使用 Docker Compose。
编辑一个新文件:
nano docker-compose.yml
添加以下内容:
web:
build: .
dockerfile: Dockerfile
links:
- redis
ports:
- "80:80"
redis:
image: redis
这个 Docker Compose 文件指示如何在两个 Docker 容器中本地启动 “Hello World” 应用。
它定义了两个容器,web
和 redis
。
web
使用当前文件夹作为build
上下文,并从我们刚刚创建的Dockerfile
文件构建我们的 Python 应用程序。这是我们为 Python 应用程序专门制作的本地 Docker 镜像。它定义了一个到redis
容器的链接,以便访问redis
容器的 IP。它还使用您的 Ubuntu 服务器的公共 IP,使端口 80 可以从互联网公开访问redis
是从一个标准的公共 Docker 镜像redis
中执行的
步骤 5 —— 部署 “Hello World” 应用
在这一步中,我们将部署应用程序,最终它将可以通过互联网访问。对于您的部署工作流程,您可以考虑这要么是一个开发、预发布或生产环境,因为您可以以相同的方式多次部署应用程序。
docker-compose.yml
和 Dockerfile
文件允许您通过执行以下命令自动化本地环境的部署:
docker-compose -f ~/hello_world/docker-compose.yml build
docker-compose -f ~/hello_world/docker-compose.yml up -d
第一行从 Dockerfile
文件构建我们的本地应用程序镜像。第二行以守护模式 (-d
) 运行 web
和 redis
容器,如 docker-compose.yml
文件中指定的那样。
通过执行以下命令检查应用程序容器是否已创建:
docker ps
这应该显示两个正在运行的容器,名称分别为 helloworld_web_1
和 helloworld_redis_1
。
让我们检查一下应用程序是否已启动。我们可以通过执行以下命令获取 helloworld_web_1
容器的 IP:
WEB_APP_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' helloworld_web_1)
echo $WEB_APP_IP
检查 Web 应用程序是否返回正确的消息:
curl http://${WEB_APP_IP}:80
这应该返回类似以下内容:
<h3>Hello World!</h3><b>Visits:</b> 1<br/>
每次访问此端点时,访问次数都会增加。您还可以通过访问您的 Ubuntu 服务器的公共 IP 地址,从浏览器访问 “Hello World” 应用程序。
如何为您自己的应用程序定制
设置您自己的应用程序的关键是将您的应用程序放在自己的 Docker 容器中,并从自己的容器中运行每个依赖项。然后,您可以使用 Docker Compose 定义容器之间的关系,就像示例中演示的那样。Docker Compose 在这篇 Docker Compose 文章中有更详细的介绍。
要了解如何在多个容器中运行应用程序的另一个示例,请阅读这篇关于使用 Docker Compose 运行 WordPress 和 phpMyAdmin 的文章。
步骤 6 — 创建测试脚本
现在我们将为我们的 Python 应用程序创建一个测试脚本。这将是一个简单的脚本,用于检查应用程序的 HTTP 输出。该脚本是您可能希望作为持续集成部署过程的一部分运行的测试类型的示例。
编辑一个新文件:
nano test.sh
添加以下内容:
sleep 5
if curl web | grep -q '<b>Visits:</b> '; then
echo "Tests passed!"
exit 0
else
echo "Tests failed!"
exit 1
fi
test.sh
用于测试我们的“Hello World”应用程序的基本 web 连通性。它使用 cURL 来检索访问次数,并报告测试是否通过。
步骤 7 — 创建测试环境
为了测试我们的应用程序,我们需要部署一个测试环境。而且,我们希望确保它与我们在步骤 5中创建的生产应用程序环境完全相同。
首先,我们需要通过创建一个新的 Dockerfile 文件来将我们的测试脚本 Docker 化。编辑一个新文件:
nano Dockerfile.test
添加以下内容:
FROM ubuntu:trusty
RUN apt-get update && apt-get install -yq curl && apt-get clean
WORKDIR /app
ADD test.sh /app/test.sh
CMD ["bash", "test.sh"]
Dockerfile.test
扩展了官方的 ubuntu:trusty
镜像,以安装 curl
依赖项,将 tests.sh
添加到镜像文件系统,并指示使用 Bash 执行测试脚本的 CMD
命令。
一旦我们的测试被 Docker 化,它们就可以以可复制和不可知的方式执行。
下一步是将我们的测试容器链接到我们的“Hello World”应用程序。这里 Docker Compose 再次发挥了作用。编辑一个新文件:
nano docker-compose.test.yml
添加以下内容:
sut:
build: .
dockerfile: Dockerfile.test
links:
- web
web:
build: .
dockerfile: Dockerfile
links:
- redis
redis:
image: redis
Docker Compose 文件的后半部分以与之前的 docker-compose.yml
文件相同的方式部署主要的 web
应用程序及其 redis
依赖项。这是指定 web
和 redis
容器的文件的部分。唯一的区别是 web
容器不再公开端口 80,因此在测试期间应用程序将无法通过公共互联网访问。因此,您可以看到我们正在以与生产部署相同的方式构建应用程序及其依赖项。
docker-compose.test.yml
文件还定义了一个名为 system under tests(sut)的 sut
容器,负责执行我们的集成测试。sut
容器指定当前目录作为我们的 build
目录,并指定我们的 Dockerfile.test
文件。它链接到 web
容器,因此应用程序容器的 IP 地址对我们的 test.sh
脚本是可访问的。
如何为您自己的应用程序定制
请注意,docker-compose.test.yml
可能包括数十个外部服务和多个测试容器。Docker 将能够在单个主机上运行所有这些依赖项,因为每个容器共享底层操作系统。
如果您有更多的测试要在您的应用程序上运行,您可以为它们创建类似于上面显示的 Dockerfile.test
文件的其他 Dockerfiles。然后,您可以在 docker-compose.test.yml
文件的 sut
容器下面添加其他容器,引用其他 Dockerfiles。
步骤 8 — 测试“Hello World”应用程序
最后,将 Docker 的思想从本地环境扩展到测试环境,我们可以通过执行以下命令以使用 Docker 自动化地测试我们的应用程序:
docker-compose -f ~/hello_world/docker-compose.test.yml -p ci build
此命令构建 docker-compose.test.yml
需要的本地镜像。请注意,我们使用 -f
指向 docker-compose.test.yml
,并使用 -p
指示特定的项目名称。
现在通过执行以下命令启动您的全新测试环境:
docker-compose -f ~/hello_world/docker-compose.test.yml -p ci up -d
通过执行以下命令检查 sut
容器的输出:
docker logs -f ci_sut_1
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 42 100 42 0 0 3902 0 --:--:-- --:--:-- --:--:-- 4200
Tests passed!
最后,通过执行以下命令检查 sut
容器的退出代码,以验证您的测试是否通过:
docker wait ci_sut_1
0
执行此命令后,如果测试通过,则 $?
的值将为 0
。否则,我们的应用程序测试失败。
请注意,其他 CI 工具可以克隆我们的代码存储库并执行这几个命令,以验证最新版本的应用程序是否通过了测试,而无需担心运行时依赖项或外部服务配置。
就是这样!我们已成功在一个与我们的生产环境完全相同的新构建环境中运行了我们的测试。
结论
通过 Docker 和 Docker Compose,我们已经能够自动化构建应用程序(Dockerfile
),部署本地环境(docker-compose.yml
),构建测试镜像(Dockerfile.test
),以及执行(集成)测试(docker-compose.test.yml
)。
特别是,使用 docker-compose.test.yml
文件进行测试的优势在于测试过程是:
- 可自动化的:工具执行
docker-compose.test.yml
的方式与被测试的应用程序无关 - 轻量级的:可以在单个主机上部署数百个外部服务,模拟复杂的(集成)测试环境
- 不可知的:避免 CI 提供商的锁定,并且您的测试可以在任何支持 Docker 的基础设施和任何操作系统上运行
- 不可变的:在本地机器上通过的测试将在 CI 工具上通过
本教程展示了如何测试一个简单的“Hello World”应用程序的示例。
现在是时候使用您自己的应用程序文件,将您自己的应用程序测试脚本 Docker 化,并创建您自己的 docker-compose.test.yml
来在一个新的不可变环境中测试您的应用程序了。