65.9K
CodeProject 正在变化。 阅读更多。
Home

学习 Docker:构建微服务

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2016年4月20日

CPOL

16分钟阅读

viewsIcon

39041

如果您想亲自动手学习 Docker 的所有知识,那么就不要错过了!

Learn Docker by building a Microservice

如果您想亲自动手学习 Docker 的所有知识,那么就不要错过了!

在本文中,我将向您展示 Docker 是如何工作的,它究竟有什么吸引力,以及 Docker 如何帮助完成一项基本的开发任务——构建微服务。

我们将以一个简单的 Node.js 服务和一个 MySQL 后端为例,从本地运行代码到运行微服务和数据库的容器。

Learn Docker by building a Microservice

什么是 Docker?

其核心而言,Docker 是一种软件,可以创建镜像(类似于虚拟机的模板),然后在此镜像的容器中运行实例。

Docker 维护着一个庞大的镜像仓库,称为 Docker Hub,您可以将其用作起点,也可以用作自己镜像的免费存储空间。您可以安装 Docker,选择一个您想使用的镜像,然后在容器中运行其一个实例。

在本文中,我们将构建镜像、从镜像创建容器等等。

安装 Docker

要继续阅读并使用本文,您需要 Docker。

请参阅您平台的安装指南,docs.docker.com/engine/installation

如果您使用的是 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  

这看起来不算什么,但已经发生了很多事情!

您现在看到的是一个隔离的容器的 bash shell,它运行着 Ubuntu,就在您的机器上。您可以随意处理它——您可以安装东西、运行软件,随心所欲。

下面是刚才发生的事情的图解和说明

Learn Docker by building a Microservice

  1. 我们发出一个 docker 命令
    • docker:运行 docker 客户端
    • run:运行新容器的命令
    • -it:为容器提供交互式终端的选项
    • ubuntu:用于构建容器的基础镜像
  2. 在主机(我们的机器)上运行的 docker 服务会检查我们本地是否有请求镜像的副本——没有。
  3. docker 服务会检查公共仓库(docker hub)是否有一个名为 ubuntu 的可用镜像——有。
  4. docker 服务会下载镜像并将其存储在本地镜像缓存中(为下次做准备)。
  5. docker 服务会创建一个新的容器,基于 ubuntu 镜像。

尝试以下任何一种

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 -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest  

这会启动一个运行中的 MySQL 实例,允许通过端口 3306 使用 root 密码 123 进行访问。

  1. docker run 告诉引擎我们要运行一个镜像(镜像在最后,mysql:vlatest
  2. --name db 将此容器命名为 db
  3. -d 分离——即,在后台运行容器。
  4. -e MYSQL_ROOT_PASSWORD=123 -e 是一个标志,告诉 docker 我们要提供一个环境变量。它后面的变量是 MySQL 镜像检查以设置默认 root 密码的。
  5. -p 3306:3306 告诉引擎我们想将容器内的端口 3306 映射到外部端口 3306。

最后一部分非常重要——即使这是 MySQL 的默认端口,如果我们不明确告诉 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_database\setup.sh  
test_database\start.sh  
test_database\stop.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  

我们稍后会让它变得更流畅,进一步简化。请查看此时的代码,方法是查看仓库的 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。将数据库访问包装在某种类或抽象中可能很有用,以便进行测试时可以模拟它。

//  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 文件中还有一组单元测试。现在我们有了仓库,就可以创建一个服务器。这是 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/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
  }
};

我们可以根据需要 require 配置。目前,大多数配置都是硬编码的,但正如您从 port 中看到的,很容易添加环境变量作为选项。

最后一步——使用一个 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();
  });
});

我们有一些基本的错误处理,除此之外,我们只是加载配置,创建一个仓库并启动我们的服务器。

这就是微服务。它允许我们获取所有用户,或搜索用户。

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 Inspector 用于调试
  2. Mocha/shoud/supertest 用于单元测试
  3. ESLint 用于代码检查

就是这样!

使用以下命令运行测试数据库

cd test-database/  
./start.sh

然后是服务

cd ../users-service/  
npm start  

您可以在浏览器中访问 localhost:8123/users,看看它的运行情况。

我们很快就完成了服务构建。如果您想在我们继续之前查看此代码,请查看 step2 分支。

第三步:Docker 化我们的微服务

好了,现在开始有趣的部分了!

所以我们有一个微服务,可以在开发机器上运行,只要它安装了兼容版本的 Node.js。我们想要做的是设置我们的服务,以便我们可以从中创建一个Docker 镜像,从而能够在支持 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.js 版本不同。

检查 Dockerfile

看看 Dockerfile,我们可以很容易地看到发生了什么。

  1. FROM node:4 我们在 Dockerfile 中首先指定的是基础镜像。快速搜索一下会找到 Docker Hub 上的 Node.js 组织页面,其中显示了所有可用的镜像。这基本上是一个带有 Node.js 安装的 Ubuntu 基础系统。
  2. RUN ["node"] RUN 命令告诉 docker 这个镜像应该运行 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 命令将当前目录中的所有内容1 复制到容器内的 app/ 文件夹中。然后我们使用 RUN 在镜像中运行命令,该命令安装我们的模块。最后,我们 EXPOSE 服务器端口,告诉 docker 我们打算支持 8123 上的入站连接,然后运行我们的服务器代码。

确保测试数据库服务正在运行,然后再次构建并运行镜像。

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”参数链接容器

当我们运行一个容器时,我们可以使用 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=dbDATABASE_HOST 环境变量设置为 db
  5. users-service 要在我们的容器中运行的镜像的名称。

现在当我们访问 localhost:8123/users 时,一切都正常了。

工作原理

还记得服务config文件吗?它允许我们通过环境变量指定数据库主机。

//  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

好的,hosts 文件没有 # linking magic!! 注释,那是为了让您看到——docker 已经将 db 添加到我们的 hosts 文件中,以便我们可以通过主机名引用链接的容器。这是链接的后果之一。这是另一个。

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 端口和容器名称。

这样第三步就完成了——我们有一个 MySQL 数据库在一个容器中愉快地运行,我们有一个 Node.js 微服务,可以在本地或它自己的容器中运行,并且我们知道如何将它们链接在一起。

您可以通过访问 step3 分支来查看此阶段的代码。

第四步:集成测试环境

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

编写集成测试可以使用任何您想要的语言或平台,在合理范围内,但为了保持简单,我将使用 Node.js,因为我们已经在项目中看到了 Mocha 和 Supertest。

在一个名为 integration-tests 的新文件夹中,我们有一个单独的 index.js 文件。

var supertest = require('supertest');  
var should = require('should');

describe('users-service', () => {

  var api = supertest('https://:8123');

  it('returns a 200 for a known user', (done) => {

    api.get('/search?email=homer@thesimpsons.com')
      .expect(200, done);
  });
});

这将检查 API 调用并显示测试结果2

只要您的 users-servicetest-database 正在运行,测试就会通过。然而,在这一点上,服务开始变得有点难以处理。

  1. 我们必须使用 shell 脚本来启动和停止数据库。
  2. 我们必须记住一系列命令来启动用户服务并连接到数据库。
  3. 我们必须直接使用 node 来运行集成测试。

现在我们对 Docker 有了一些了解,我们可以解决这些问题。

简化测试数据库

目前,我们有以下用于测试数据库的文件。

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

现在我们对 Docker 更熟悉了,我们可以改进这一点。查看 Docker Hub 上的 MySQL 镜像文档,其中有一条说明告诉我们,添加到镜像的 /docker-entrypoint-initdb.d 文件夹中的任何 .sql.sh 文件都将在设置数据库时执行。

这意味着我们可以用一个 Dockerfile 替换我们的 start.shstop.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 Compose 允许您创建一个文件,该文件定义系统中的每个容器、它们之间的关系,并构建或运行所有容器。

首先,安装 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 进行的有趣且有用的事情。

一如既往,欢迎提问和评论!我还要强烈推荐文档 Understanding Docker,以便更深入地了解 Docker 的工作原理。

您可以在 github.com/dwmkerr/node-docker-mircroservice 上查看本文构建的项目的最终源代码。

注释

  1. 复制所有内容实际上是个坏主意,因为我们也会复制 undefined 文件夹。通常,最好明确列出要复制的文件或文件夹,或者使用一个 undefined 文件,它的工作方式类似于 undefined 文件。

  2. 如果服务器未运行,它实际上会显示一个非常烦人的异常,这是由于 supertest 的一个 bug 造成的,请参阅 github.com/visionmedia/supertest/issues/314

© . All rights reserved.