通过做一个基于 Node 的微服务器来学习 Docker

3,566 阅读20分钟
原文链接: blog.jobbole.com

如果你正准备着手学习 Docker,别再观望,动起手来吧!

在这篇文章中,我将告诉你 Docker 是如何工作的?使用中会遇到什么问题?如何通过 Docker 完成一个基本的开发任务——构建一个微服务器。

我们将以一台配有 Node.js 服务和 MySQL 后台的服务器为例,从在本地运行代码开始,完成一个运行着微服务和数据库的容器。

查看图片

什么是 Docker ?

从本质上来说,Docker 是一种软件,让用户创建镜像文件(就像虚拟机中的模板),然后在容器中运行这个镜像的实例。

Docker 维护着有着大量镜像的存储库,名字叫 Docker Hub ,你可以将它作为尝试镜像的起始点,或者用来免费存储你的镜像。你可以安装 Docker ,选择你喜欢的镜像,然后在容器中运行它的实例。

本文我们将介绍创建镜像、从镜像创建容器等一系列内容。

安装 Docker

如果你想跟上本文的节奏,那么你需要安装 Docker 。

点击 docs.docker.com/engine/inst… 这个链接,在上面查看适合你的系统的安装向导。

如果你是 Mac 或者 Windows 操作系统,那么你需要使用虚拟机。我在 Mac OS X 上使用 Parallels 安装 Ubuntu 虚拟机来应付大多数的开发任务。因为它支持快照功能,当你做实验的时候,他可以方便的将破坏了的环境恢复回去。

试试看

输入以下命令:

docker run -it ubuntu

一段时间后,你将会看到如下提示:

root@719059da250d:/#

试试如下的命令,然后退出容器:


root@719059da250d:/# lsb_release -a  
No LSB modules are available.  
Distributor ID:    Ubuntu  
Description:    Ubuntu 14.04.4 LTS  
Release:    14.04  
Codename:    trusty  
root@719059da250d:/# exit

这看起来没什么,但是其实在后台发生了很多事情。

你看到的是在你的机器上运行着的 Ubuntu 的隔离容器环境里的 bash shell。这个环境完全归你所有——可以在上面安装软件,运行软件,可以做任何你想做的事情。

下图表明了刚刚发生了什么(图来自于《 理解 Docker 架构 》一文):

查看图片

1. 列出如下的 Docker 指令:

  • docker : 运行 docker 客户端
  • run : 运行一个新的容器
  • -it :让容器带有“交互终端”的一个参数
  • ubuntu : 容器所依赖的基础镜像

2. 在主机(我们的机器)上运行的 docker 服务检查本地是否有所请求的镜像拷贝——这里发现没有。

3. docker 服务检查公有存储库(the docker hub),看是否有可用的名为 ubuntu 的镜像——这里发现有。

4. docker 服务下载镜像,将其存储到本地缓存里(为了下一次直接使用)。

5. docker 服务基于 ubuntu 镜像创建新的容器。

Try any of these:

试试下面这些命令:

docker run -it haskell  
docker run -it java  
docker run -it python

我们没准备使用 Haskell ,但是你可以看到,搭建一个环境是多么容易。

构建自己的镜像也很轻松,可以在这上面安装应用程序或者服务,可以是数据库,或者是其他你需要的。随后就可以在任意安装了 Docker 的机器上运行它们——要保证镜像是相同的、可预测的方式在每台机器上运行。我们可以将软件及其运行所需的环境整体构建成代码,并且轻松部署。

让我们以一个简单微服务器为例。

概述

我们将要用 Node.js 和 MySQL 创建一个让我们管理邮件地址到电话号码目录的微服务。

开始

要完成本地开发,需要安装MySQL,并且创建一个测试数据库…

…摇头。

创建本地数据库,并且上面运行脚本,这很容易,但是可能会带来一些问题。很多不受控制的事情开始了。它可能工作,我们甚至可以通过提交进代码库的 shell 脚本来控制这些步骤,但是如果其他开发人员已经安装了 MySQL 了呢?如果他们的数据库已经使用了我们想要创建的名称  ‘users’  了呢?

第一步:在 Docker 中创建一个数据库测试服务器

这是很好的 Docker 应用场景。我们可能不想在 Docker 里运行生产环境数据库(比如可能会使用 Amazon RDS),但是可以使用 Docker 容器创建一个干净的 MySQL 数据库做开发——让我们的开发及其保持干净,并且保证所有东西都在控制中,并且可重复使用。

运行下面的命令:

docker run --name db -d -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest

该命令启动一个运行着的 MySQL 实例,通过 3306 端口访问,root 密码为 123 。

  1.  docker run 告诉引擎,用户想要运行一个镜像(在最后传入的是镜像,mysql:latest
  2.  –name db 将整个容器命名为 db 。
  3.  -d detach,在后台运行容器。
  4.  -e MYSQL_ROOT_PASSWORD=123(或者是 –env)环境变量 – 参数告诉 docker 所提供的环境变量。这之后跟着的变量正是 MySQL 镜像检查且用来设置的默认 root 密码。
  5.  -p 3306:3306(或者 --publish) 告诉引擎用户想要将容器内的3306端口映射到外部的3306端口上。

最后一部分很重要——即使这是 MySQL 的默认端口,如果用户不显式告诉 docker 想要映射的端口,docker 就会阻塞该端口的访问(因为容器默认是隔离的,直到用户告诉 docker 想要访问它们)。

该命令返回值是容器 id,这是容器的指针,用户可以用它来停止容器,向容器发送命令等等。让我们看看正在运行的是哪些容器:

$ docker ps
CONTAINER ID  IMAGE         ...  NAMES  
36e68b966fd0  mysql:latest  ...  db

关键的信息是容器 ID,镜像和名称。连接到这个镜像看看里面有什么:

$ docker exec -it db /bin/bash
 
root@36e68b966fd0:/# mysql -uroot -p123  
mysql> show databases;  
+--------------------+
| Database           |
+--------------------+
| information_schema |
+--------------------+
1 rows in set (0.01 sec)
 
mysql> exit  
Bye  
root@36e68b966fd0:/# exit

下面这么做也很有意思:

1. docker exec -it db :告诉 docker 用户想要在名为 db 的容器里执行一个命令(我们也可以使用 id,或者 id 的前几个字母)。 -it 确保用户有交互型终端。

2. mysql -uroot -p123 :我们实际在容器里作为进程运行的命令,这里是 mysql 客户端。

我们可以创建数据库,表,用户,其他你需要的等等。

打包测试数据库

在容器内运行 MySQL 需要一些 Docker 技巧,但是让我们先打住,看看服务。现在,使用脚本创建一个 test-database 目录来启动数据库,停止数据库以及搭建测试数据:

test-databasesetup.sql  
test-databasestart.sh  
test-databasestop.sh

启动脚本很简单:

#!/bin/sh
 
# Run the MySQL container, with a database named 'users' and credentials
# for a users-service user which can access it.
echo "Starting DB..."  
docker run --name db -d   
  -e MYSQL_ROOT_PASSWORD=123 
  -e MYSQL_DATABASE=users -e MYSQL_USER=users_service -e MYSQL_PASSWORD=123 
  -p 3306:3306 
  mysql:latest
 
# Wait for the database service to start up.
echo "Waiting for DB to start up..."  
docker exec db mysqladmin --silent --wait=30 -uusers_service -p123 ping || exit 1
 
# Run the setup script.
echo "Setting up initial data..."  
docker exec -i db mysql -uusers_service -p123 users < setup.sql

该脚本在一个分离容器钟运行数据库镜像(比如,在后台运行),创建了一个用户来访问 users 数据库,然后等待数据库服务器启动,随后运行 setup.sql 脚本来设置初始数据。

setup.sql 的内容是:

create table directory (user_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, email TEXT, phone_number TEXT);  
insert into directory (email, phone_number) values ('homer@thesimpsons.com', '+1 888 123 1111');  
insert into directory (email, phone_number) values ('marge@thesimpsons.com', '+1 888 123 1112');  
insert into directory (email, phone_number) values ('maggie@thesimpsons.com', '+1 888 123 1113');  
insert into directory (email, phone_number) values ('lisa@thesimpsons.com', '+1 888 123 1114');  
insert into directory (email, phone_number) values ('bart@thesimpsons.com', '+1 888 123 1115');

stop.sh 脚本会停止容器并且删除容器(docker 默认会保留容器,这样能够快速重启,本示例中并不需要这样):

#!/bin/sh
 
# Stop the db and remove the container.
docker stop db && docker rm db

之后会进一步简化这个过程,让它更加顺畅。在 repo 里的 step1 分支里查看这一阶段的代码。

第二步:用 Node.js 创建一个微服务

本文的主题是 Docker 的学习,因此并不会花太多篇幅讲解 Node.js 的微服务。只是强调一些重点。

test-database/          # contains the code seen in Step 1  
users-service/          # root of our node.js microservice  
- package.json          # dependencies, metadata
- index.js              # main entrypoint of the app
- api/                  # our apis and api tests
- config/               # config for the app
- repository/           # abstraction over our db
- server/               # server setup code

让我们仔细看看这一部分。首先看看这个代码库。最好将你的数据库访问封装和抽象成一些类,允许模拟它来实现测试目的:

//  repository.js
//
//  Exposes a single function - 'connect', which returns
//  a connected repository. Call 'disconnect' on this object when you're done.
'use strict';
 
var mysql = require('mysql');
 
//  Class which holds an open connection to a repository
//  and exposes some simple functions for accessing data.
class Repository {  
  constructor(connection) {
    this.connection = connection;
  }
 
  getUsers() {
    return new Promise((resolve, reject) => {
 
      this.connection.query('SELECT email, phone_number FROM directory', (err, results) => {
        if(err) {
          return reject(new Error("An error occured getting the users: " + err));
        }
 
        resolve((results || []).map((user) => {
          return {
            email: user.email,
            phone_number: user.phone_number
          };
        }));
      });
 
    });
  }
 
  getUserByEmail(email) {
 
    return new Promise((resolve, reject) => {
 
      //  Fetch the customer.
      this.connection.query('SELECT email, phone_number FROM directory WHERE email = ?', [email], (err, results) => {
 
        if(err) {
          return reject(new Error("An error occured getting the user: " + err));
        }
 
        if(results.length === 0) {
          resolve(undefined);
        } else {
          resolve({
            email: results[0].email,
            phone_number: results[0].phone_number
          });
        }
 
      });
 
    });
  }
 
  disconnect() {
    this.connection.end();
  }
}
 
//  One and only exported function, returns a connected repo.
module.exports.connect = (connectionSettings) => {  
  return new Promise((resolve, reject) => {
    if(!connectionSettings.host) throw new Error("A host must be specified.");
    if(!connectionSettings.user) throw new Error("A user must be specified.");
    if(!connectionSettings.password) throw new Error("A password must be specified.");
    if(!connectionSettings.port) throw new Error("A port must be specified.");
 
    resolve(new Repository(mysql.createConnection(connectionSettings)));
  });
};

其实有很多种其他实现方式!但是我们可以像下面这样创建 Repository 对象:

repository.connect({  
  host: "127.0.0.1",
  database: "users",
  user: "users_service",
  password: "123",
  port: 3306
}).then((repo) => {
  repo.getUsers().then(users) => {
    console.log(users);
  });
  repo.getUserByEmail('homer@thesimpsons.com').then((user) => {
    console.log(user);
  })
  //  ...when you are done...
  repo.disconnect();
});

在 repository/repository.spec.js 文件里也有一系列的单元测试。得到 repo 后,就可以创建服务器了。server/server.js 如下:

//  server.js
 
var express = require('express');  
var morgan = require('morgan');
 
module.exports.start = (options) => {
 
  return new Promise((resolve, reject) => {
 
    //  Make sure we have a repository and port provided.
    if(!options.repository) throw new Error("A server must be started with a connected repository.");
    if(!options.port) throw new Error("A server must be started with a port.");
 
    //  Create the app, add some logging.
    var app = express();
    app.use(morgan('dev'));
 
    //  Add the APIs to the app.
    require('../api/users')(app, options);
 
    //  Start the app, creating a running server which we return.
    var server = app.listen(options.port, () => {
      resolve(server);
    });
 
  });
};

该模块暴露了一个 start 函数,可以像下面这样使用:

var server = require('./server/server);  
server.start({port: 8080, repo: repository}).then((svr) => {  
  // we've got a running http server :)
});

注意到 server.js 中使用了 api/users/js 吧?代码如下:

//  users.js
//
//  Defines the users api. Add to a server by calling:
//  require('./users')
'use strict';
 
//  Only export - adds the API to the app with the given options.
module.exports = (app, options) => {
 
  app.get('/users', (req, res, next) => {
    options.repository.getUsers().then((users) => {
      res.status(200).send(users.map((user) => { return {
          email: user.email,
          phoneNumber: user.phone_number
        };
      }));
    })
    .catch(next);
  });
 
  app.get('/search', (req, res) => {
 
    //  Get the email.
    var email = req.query.email;
    if (!email) {
      throw new Error("When searching for a user, the email must be specified, e.g: '/search?email=homer@thesimpsons.com'.");
    }
 
    //  Get the user from the repo.
    options.repository.getUserByEmail(email).then((user) => {
 
      if(!user) { 
        res.status(404).send('User not found.');
      } else {
        res.status(200).send({
          email: user.email,
          phoneNumber: user.phone_number
        });
      }
    })
    .catch(next);
 
  });
};

这些文件都有和源码匹配的单元测试。

我们还需要配置。与其使用特定的库函数,不如使用一个简单的文件 – config/config.js :

//  config.js
//
//  Simple application configuration. Extend as needed.
module.exports = {  
    port: process.env.PORT || 8123,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 3306
  }
};

我们可以按需进行配置。目前,大部分配置是硬编码的,但是从端口的配置中可以看出,我们可以很容易的通过添加环境变量的方式来改变它。
最后一步 – 将它和包含所有东西的 index.js 文件连接到一起:

//    index.js
//
//  Entrypoint to the application. Opens a repository to the MySQL
//  server and starts the server.
var server = require('./server/server');  
var repository = require('./repository/repository');  
var config = require('./config/config');
 
//  Lots of verbose logging when we're starting up...
console.log("--- Customer Service---");  
console.log("Connecting to customer repository...");
 
//  Log unhandled exceptions.
process.on('uncaughtException', function(err) {  
  console.error('Unhandled Exception', err);
});
process.on('unhandledRejection', function(err, promise){  
  console.error('Unhandled Rejection', err);
});
 
repository.connect({  
  host: config.db.host,
  database: config.db.database,
  user: config.db.user,
  password: config.db.password,
  port: config.db.port
}).then((repo) => {
  console.log("Connected. Starting server...");
 
  return server.start({
    port: config.port,
    repository: repo
  });
 
}).then((app) => {
  console.log("Server started successfully, running on port " + config.port + ".");
  app.on('close', () => {
    repository.disconnect();
  });
});

我们做了一点错误处理,在此之上仅仅加载了配置,创建了 repo 并且启动了服务器。
这就是微服务,它让用户能够得到所有用户,或者搜索某个用户:

HTTP GET /users                              # gets all users  
HTTP GET /search?email=homer@thesimpons.com  # searches by email

如果下载了相关代码,可以发现有一些可用的命令:

cd ./users-service  
npm install         # setup everything  
npm test            # unit test - no need for a test database running  
npm start           # run the server - you must have a test database running  
npm run debug       # run the server in debug mode, opens a browser with the inspector  
npm run lint        # check to see if the code is beautiful

除了代码之外,我们完成了:
1. 用于调试的 Node 面板
2. 用于单元测试的 Mocha/shoud/supertest
3. 用于 linting 的 ESlint

大功告成!
使用如下命令运行数据库测试:

cd test-database/  
./start.sh

然后启动服务:

cd ../users-service/  
npm start

可以用浏览器打开 localhost:8123/users,就可以看到数据库已经可以使用了。如果使用的是 Docker Machine(假设,在 Mac 或者 Windows 上),那么 localhost 无法访问,你需要使用 docker 的 IP。可以通过 docker-machine ip 得到 IP 地址。
可以看到,我们很快完成了服务的创建。继续下一步之前如果想查看代码,见 step2 分支。

第三步:微服务 Docker 化

现在开始变得有趣啦!

我们已经有了一个可以运行在开发环境里的微服务,只要它和安装的 Node.js 版本兼容即可。这一步想做的是搭建起我们的服务,这样可以从其中创建一个 Docker Image,从而可以将服务部署到任何支持 docker 的地方。

要达到这一目的,需要创建一个 Dockerfile。Dockerfile 告诉 Docker 引擎如何构建镜像。我们会在 users-service 目录下创建一个简单的 Dockerfile,并且研究如何通过修改它来适应需求。

创建 Dockerfile

在 users-service/ 目录下创建名为 Dockerfile 的文本文件,内容如下:

# Use Node v4 as the base image.
FROM node:4
 
# Run node 
CMD ["node"]

运行如下命令行构建镜像,并在镜像上运行容器:

docker build -t node4 .    # Builds a new image  
docker run -it node4       # Run a container with this image, interactive

首先看看构建命令。
1. docker build 告诉引擎用户需要创建一个新的镜像
2. -t node4 使用标签 node4 标记该镜像。之后就可以使用这个标签来指代该镜像。
3. 在当前目录里查找 Dockerfile.

控制台打出一些输出之后,就可以看到新的镜像创建好了。使用 docker images 命令可以在系统里看到所有镜像。下面的命令和之前的很类似:
1. docker run 从某个镜像里运行新容器
2. -it 使用交互式终端
3. node4 是想要在容器里使用的镜像的标签。

当运行该镜像时,会得到 node repl,运行如下命令检查当前版本:

> process.version
'v4.4.0'  
> process.exit(0)

这很可能和你当前机器上的 node 版本不同。

检查 Dockerfile

从 Dockerfile 里可以很容易看出发生了什么:
1. FROM node:4 在 Dockerfile 里指定的第一件事就是基础镜像。docker hub上的 Node 官方页面可以搜索列出所有可用镜像。这里用的是安装了 node 的 ubuntu。
2. CMD ["node"] 里的  CMD 告诉 docker 该镜像需要运行 node 程序。当 node 程序终止时,容器会关闭。

使用额外的几个命令,可以更新 Dockerfile,从而运行服务:

# Use Node v4 as the base image.
FROM node:4
 
# Add everything in the current directory to our image, in the 'app' folder.
ADD . /app
 
# Install dependencies
RUN cd /app;   
    npm install --production
 
# Expose our server port.
EXPOSE 8123
 
# Run our app.
CMD ["node", "/app/index.js"]

唯一的改变是使用了 ADD 命令将当前目录下的所有东西拷贝到名为 app/ 的容器目录里。随后使用 RUN 在镜像里运行命令,该命令安装了模块。最后,EXPOSE 了服务器端口,告诉 docker 想要支持 8123 端口的连接,然后运行服务器代码。
确保 test-database 服务已经运行着,然后再次构建并且运行镜像:

docker build -t users-service .  
docker run -it -p 8123:8123 users-service

如果在浏览器里查看 localhost:8123/users,会看到一个错误,检查控制台,提示容器报告了一些问题:

--- Customer Service---
Connecting to customer repository...  
Connected. Starting server...  
Server started successfully, running on port 8123.  
GET /users 500 23.958 ms - 582  
Error: An error occured getting the users: Error: connect ECONNREFUSED 127.0.0.1:3306  
    at Query._callback (/app/repository/repository.js:21:25)
    at Query.Sequence.end (/app/node_modules/mysql/lib/protocol/sequences/Sequence.js:96:24)
    at /app/node_modules/mysql/lib/protocol/Protocol.js:399:18
    at Array.forEach (native)
    at /app/node_modules/mysql/lib/protocol/Protocol.js:398:13
    at nextTickCallbackWith0Args (node.js:420:9)
    at process._tickCallback (node.js:349:13)

我勒个去!从 users-service 容器到 test-database 容器的连接被拒绝了。运行 docker ps 查看所有运行着的容器:

CONTAINER ID  IMAGE          PORTS                   NAMES  
a97958850c66  users-service  0.0.0.0:8123->8123/tcp  kickass_perlman  
47f91343db01  mysql:latest   0.0.0.0:3306->3306/tcp  db

这两个容器都运行着呢,到底怎么回事呢?

我们看到的问题实际上是可以预期的。Docker 容器应该是互相隔离的,因此如果不显式地允许容器间连接的话就容器间就无法互联。

是的,用户可以从自己的机器(宿主机)连接到容器里,因为我们为这样的连接开启了端口(比如,使用了-p 8123:8123)。如果以同样的方式允许容器间互联,那么运行在同一台机器上的两个容器之间就应该能够通信,即使开发人员不想这么做。并且这是灾难性的,尤其是我们在集群的机器上利用容器运行不同的应用程序的时候。

如果想要从某个容器连接到另一个容器,需要连接这两个容器,告诉 docker,用户显式想要允许这两个容器间通信。有两种方式可以完成这一目标,第一种是“不流行的旧方式”但是非常简单,第二种之后会介绍。

当运行容器时,可以使用 link 参数告诉 docker 我们想要连接到另外的容器上。本文示例中,可以通过如下命令正确运行服务:

docker run -it -p 8123:8123 --link db:db -e DATABASE_HOST=DB users-service

1. docker run -it 在容器里运行 docker 镜像,使用交互式终端。
2. -p 8123:8123 将宿主机的 8123 端口映射到容器的 8123 端口上
3. link db:db 连接到名为 db的 容器上,并且用 db 指代该容器
4. -e DATABASE_HOST=db 将环境变量 DATABASE_HOST 设置为 db
5. users-service 是在容器内运行的镜像名称

现在当我们访问 localhost:8123/users 时一切工作正常。

它是如何工作的呢?

还记得服务的配置文件么?它让用户能够使用环境变量指定数据库的主机名:

//  config.js
//
//  Simple application configuration. Extend as needed.
module.exports = {  
    port: process.env.PORT || 8123,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 3306
  }
};

运行容器时,将环境变量设置为 DB,这意味着要连接到一个名字为 DB 的主机上。当连接到容器上时,docker 引擎会自动为我们设置好一切。

尝试运行 docker ps 列出所有运行着的容器。查询运行 users-service 的容器名称,这是个随机名称,例如 trusting_jang:

docker ps  
CONTAINER ID  IMAGE          ...   NAMES  
ac9449d3d552  users-service  ...   trusting_jang  
47f91343db01  mysql:latest   ...   db

现在可以看到容器可用的主机:

docker exec trusting_jang cat /etc/hosts  
127.0.0.1    localhost  
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet  
ff00::0    ip6-mcastprefix  
ff02::1    ip6-allnodes  
ff02::2    ip6-allrouters  
172.17.0.2    db 47f91343db01    # linking magic!!  
172.17.0.3    ac9449d3d552

还记得 docker exec 是怎么工作的吗?选择一个容器名,之后跟着想在容器上执行的命令,在本例中是 cat /etc/hosts

好了,主机文件之前可没有 # linking magic!! 注释,可以看到 -docker 将 db 添加到了主机文件里,因此可以通过主机名连接到容器上。这是连接信息:

docker exec trusting_jang printenv | grep DB  
DB_PORT=tcp://172.17.0.2:3306  
DB_PORT_3306_TCP=tcp://172.17.0.2:3306  
DB_PORT_3306_TCP_ADDR=172.17.0.2  
DB_PORT_3306_TCP_PORT=3306  
DB_PORT_3306_TCP_PROTO=tcp  
DB_NAME=/trusting_jang/db

从该命令还可以看到当 docker 连接容器时,它还提供了一系列包含有用信息的环境变量,比如,主机名,tcp 端口和容器名。

第3步完成了 —— MySQL 数据库正常运行在容器里,还可以在本地或者在容器里运行 node.js 微服务,并且已经知道了如何连接这两者。

如果你想了解更多,可以在 step3 的分支里查看这一阶段的代码。

第4步:环境的集成测试

现在可以编写集成测试,调用实际服务器,作为 docker 容器运行,调用容器化的测试数据库。

可以用任何语言,或者在任何平台上完成集成测试,但是为了保持简洁,这里使用的是 Node.js,因为项目里已经使用了Mocha 和 Supertest。

在名为 integration-tests 的新目录下,创建一个 index.js:

var supertest = require('supertest');  
var should = require('should');
 
describe('users-service', () => {
 
  var api = supertest('http://localhost:8123');
 
  it('returns a 200 for a known user', (done) => {
 
    api.get('/search?email=homer@thesimpsons.com')
      .expect(200, done);
  });
 
});

它会检查 API 调用,并且显示测试结果。
只要 users-services 和 test-database 正在运行,测试就能够通过。但是,这时候服务开始变得有点难处理:
1. 需要使用 shell 脚本来启动和停止数据库
2. 需要记住一系列命令来基于数据库启动用户服务
3. 需要使用 node 直接运行集成测试
既然我们已经很熟悉 Docker 了,应该能够解决这些问题。

简化 Test 数据库

目前测试数据库有如下文件:

/test-database/start.sh
/test-database/stop.sh
/test-database/setup.sql

既然已经很熟悉 Docker 了,让我们尝试改进它们。

在 Docker Hub 上查看 mysql 镜像文档,有一处注释告诉用户任何添加到镜像的 /docker-entrypoint-initdb.d 目录的 .sql 或者 .sh 文件会在搭建 DB 的时候执行。

这意味着可以使用 Dockerfile 代替 start.sh 和 stop.sh 

FROM mysql:5
 
ENV MYSQL_ROOT_PASSWORD 123  
ENV MYSQL_DATABASE users  
ENV MYSQL_USER users_service  
ENV MYSQL_PASSWORD 123
 
ADD setup.sql /docker-entrypoint-initdb.d

现在运行测试数据库只需要:

docker build -t test-database .  
docker run --name db test-database

组合

构建并且运行每个容器仍然有些费时。可以使用 Docker Compose 工具进一步简化。

Docker Composer 允许用户创建一个文件,在其中定义系统里的每个容器,容器间的关系,并且构建或者运行它们。
首先,安装 Docker Compose。在项目根目录下创建一个新文件,称为 docker-compose.yml:

version: '2'  
services:  
  users-service:
    build: ./users-service
    ports:
     - "8123:8123"
    depends_on:
     - db
    environment:
     - DATABASE_HOST=db
  db:
    build: ./test-database

现在就可以试一下啦:

docker-compose build  
docker-compose up

Docker Compose 会构建出应用程序所需要的所有镜像,从其上创建出容器,并且以正确顺序运行容器,从而启动整个应用程序!
docker-compose build 命令构建 docker-compose.yml 文件里列出的每个镜像:

version: '2'  
services:  
  users-service:
    build: ./users-service
    ports:
     - "8123:8123"
    depends_on:
     - db
    environment:
     - DATABASE_HOST=db
  db:
    build: ./test-database

每个服务的 build 值告诉 docker 到哪里找到 Dockerfile 。当用户运行 docker-compose up 时,docker 会启动所有服务。注意在 Dockerfile 里,用户可以指定端口和依赖关系。实际上,在这个文件里,用户可以更改所有配置。
在另一个终端里,运行 docker compose down,可以正常关闭容器。

总结

本文里介绍了很多 docker 知识,不过这里还有一些。我希望你能够从本文找到一些有趣有用的东西,能够帮助你在工作中使用 docker。

和平常一样,欢迎提问和建议!同时强烈推荐文档《理解 Docker 》,可以帮助大家更深入地理解 docker 的工作机制。

github.com/dwmkerr/nod… 处可以看到本文所构建的项目的最终源码。

注意

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式