天天看點

通過建構微服務來學習Docker

如果你正在尋找練手機會以便深入學習Docker,那麼本文就是你最好的選擇。在本文中,我将展示Docker是如何工作的,以及應用Docker完成建構一個基本的微服務開發任務。

我們将使用一個簡單的Node.js服務與一個MySQL後端為例,實作從本地運作的代碼遷移到容器化運作的微服務和資料庫。

通過建構微服務來學習Docker

什麼是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:/# lsb_release -a  
No LSB modules are available.  
Distributor ID:    Ubuntu  
Description:    Ubuntu  LTS  
Release:      
Codename:    trusty  
root@719059da250d:/# exit  
           

這看起來好像并沒有什麼,但是實際上背後發生了很多。你們看到的是Ubuntu的一個bash shell,它運作于在你的機器上隔離的容器中。在這裡,你可以安裝任何東西,運作任何軟體,或者其他任何你想要做的。以下是上述動作的流程分解圖(該圖表來自于Docker文檔庫的“了解架構”,非常值得推薦)

通過建構微服務來學習Docker

1.輸入一條Docker指令:

odocker: 運作docker用戶端

orun: 該指令啟動一個新的容器

o-it: 是否啟動互動式終端模式的可選項

oubuntu: 容器啟動所基于的鏡像名

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以及建立一個測試資料庫… …

建立一個本地資料庫并執行腳本是一個簡單的開端,但也可能是一團混亂。很多不可控的事情會發生。它可能會正常工作,我們甚至可以執行一些腳本來檢查我們的版本庫,但是假如已經有其他開發人員在機器上安裝了MySQL呢?并且假設他們所使用的資料庫已經占用了我們想要使用的資料庫名‘users’?

第一步:在Docker上建立測試資料庫伺服器

這是一個非常好的Docker使用者案例。或許我們不會把生産資料庫跑在Docker上,但是可以在任何時間為開發人員快速部署一個基于Docker容器的純淨MySQL資料庫,保持我們幹淨的開發機器環境并且一切都是可控且可重複的。

執行以下指令:

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

該指令啟動一個MySQL資料庫執行個體,并且允許root使用者以及123的密碼通過3306端口通路它.

  1. docker run 這裡我們告訴Docker引擎我們需要加載一個鏡像(這個鏡像名在指令最後:mysql:vlatest)。
  2. –name db 這裡給容器命名db。
  3. -d (or –detach) 分離,即在背景運作的容器。
  4. -e MYSQL_ROOT_PASSWORD=123 (or –env) 環境變量-告訴Docker我們需要提供的環境變量,随後的這個變量就是MySQL鏡像需要檢查配置的root預設密碼。
  5. -p 3306:3306 (或者 –publish 告訴Docker引擎我們需要映射容器内部的3306端口到外部的3306端口。

這個指令的傳回值就是容器的id,它是容器的引用代碼可以用來針對具體容器停止、重新開機、執行指令等等。接下來就讓我們來看看哪些容器目前正在運作:

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

這裡的關鍵資訊就是容器ID,鏡像以及容器名。下面讓我們連接配接上這個鏡像并且看看上面到底有什麼:

$ docker exec -it db /bin/bash

[email protected]:/# mysql -uroot -p123  
mysql> show databases;  
+--------------------+
| Database           |
+--------------------+
| information_schema |
+--------------------+
1 rows in set (0.01 sec)

mysql> exit  
Bye  
[email protected]:/# exit  
           

這裡的實作也是相當的巧妙:

  1. docker exec -it db 這裡告訴Docker,我們要在容器名為db(這裡我們也可以使用容器id,或者是id頭上的部分縮寫)内執行一條指令。
  2. mysql -uroot -p123 這條是真正在容器内運作具體程序的指令,在本例中就是啟動了mysql的用戶端。

至此,我們已經可以建立資料庫、表、使用者以及其他一切所需。

測試資料庫總結

上述文章中介紹了一些在容器中運作MySQL的Docker技巧,不過暫停一下,轉移到服務上。現在,我們将要建立一個test-database 檔案夾以及使用腳本來啟動資料庫,停止資料庫和設定測試資料:

test-database\setup.sql  
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= \
  -e MYSQL_DATABASE=users -e MYSQL_USER=users_service -e MYSQL_PASSWORD= \
  -p : \
  mysql:latest

# Wait for the database service to start up.
echo "Waiting for DB to start up..."  
docker exec db mysqladmin --silent --wait= -uusers_service -p123 ping || exit 

# 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 ('[email protected]', '+1 888 123 1111');  
insert into directory (email, phone_number) values ('[email protected]', '+1 888 123 1112');  
insert into directory (email, phone_number) values ('[email protected]', '+1 888 123 1113');  
insert into directory (email, phone_number) values ('[email protected]', '+1 888 123 1114');  
insert into directory (email, phone_number) values ('[email protected]', '+1 888 123 1115');  
           

stop.sh 腳本将會停止容器并删除它(預設情況下容器并不會被Docker立即删除,進而它們可以在需要時被快速恢複,在本例中我們并不需要這個功能):

#!/bin/sh

# Stop the db and remove the container.
docker stop db && docker rm db  
           

接下來将要讓這些步驟更加平滑和簡潔,具體可以檢視在這個階段的第一步的版本庫分支代碼。

第二步:建立一個Node.js的微服務

由于本文的主要關注點在于學習Docker,是以我将不會在Node.js如何實作微服務上花費太多筆墨,相反,我将重點關注Docker的領域和結論。

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 === ) {
          resolve(undefined);
        } else {
          resolve({
            email: results[].email,
            phone_number: results[].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: 
}).then((repo) => {
  repo.getUsers().then(users) => {
    console.log(users);
  });
  repo.getUserByEmail('[email protected]').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().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: '/[email protected]'.");
    }

    //  Get the user from the repo.
    options.repository.getUserByEmail(email).then((user) => {

      if(!user) { 
        res.status().send('User not found.');
      } else {
        res.status().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 || ,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 
  }
};
           

下面代碼的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();
  });
});
           

我們會有一些小的錯誤需要處理,除此之外我們需要做的僅僅是加載config,建立倉庫以及啟動伺服器。

這就是一個微服務。它允許我們獲得所有的使用者,以及搜尋任一使用者:

HTTP GET /users                              # gets all users  
HTTP GET /[email protected]  # searches by email  
           

假如你checkout代碼,你可以看到這裡有一些可用的指令如下:

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 并看到相應回應。假如你正在使用Docker機器(例如:你跑着Mac或者Windows上),那麼localhost 不會工作,你需要使用Docker機器的IP來替代上述的localhost。你可以使用指令docker-machine ip 來擷取docker IP。

上面我們已經快速簡略的描述了如何建構一個微服務。

第三步: Dockerising(Docker化)我們的微服務

接下來讓我們享受一下docker的樂趣!這裡我們已經有了一個可以跑在開發盒子裡的微服務,隻要有任一相容的Node.js已經安裝。接下來我們要做的就是設定我們的服務進而可以基于它建立Docker鏡像,允許把服務部署到任何支援docker的地方。

這裡的實作方式是通過建立一個Dockerfile。Dockerfile告訴Docker引擎應該如何建立你的鏡像。我們将會在users-service 目錄下建立一個簡單的Dockerfile并且開始探讨如何使之适應我們的需要。

建立Dockerfile

建立一個新的文本檔案名為 Dockerfile 在 users-service/ 目錄下,内容如下:

# Use Node v4 as the base image.
FROM node:

# Run node 
CMD ["node"]
           

然後執行下列指令建立鏡像以及運作基于這個鏡像的一個容器:

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

先來看看build指令。

  1. docker build 這裡告訴docker引擎我們需要建立一個新鏡像
  2. -t node4 命名鏡像标簽為node4。然後我們可以通過該标簽引用這個鏡像。
  3. 使用目前目錄作為Dockerfile檔案目錄

當這些指令台輸出完畢之後,我們就可以看到一個新鏡像建立完畢。你可以通過指令docker images檢視目前系統的所有鏡像。下一步的指令基于之前所做的練習你應該已經相當熟悉了:

  1. docker run 基于一個鏡像運作一個新的容器run a new container from an image。
  2. -it 使用互動式終端模式。
  3. node4 我們所希望在容器中使用的鏡像标簽。

當我們的鏡像運作起來之後,我們就得到了一個Node repl,可以通過如下指令檢查版本号:

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

這裡有潛在的可能性,docker上的node版本和你本地機器的node版本并不一緻。

檢查Dockerfile

縱覽dockerfile我們可以很容易了解它具體在做什麼:

  1. FROM node:4 在dockerfile中指定的第一件就是基本鏡像。通過google的node organisation page on the docker hub可以快速檢索出所有可用的鏡像。這也是已經安裝node.js的ubuntu的基本骨架。
  2. CMD [“node”] CMD 指令告訴docker這個鏡像是支援node可執行的。當node執行終止時,容器也應該被關閉。

通過以下額外的幾條指令,可以更新dockerfile來執行我們的服務:

# Use Node v4 as the base image.
FROM node:

# 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 

# Run our app.
CMD ["node", "/app/index.js"]  
           

這裡唯一增加的内容就是會使用ADD 指令來拷貝目前目錄下的所有檔案到容器中的app/目錄下。然後可以使用RUN 來執行鏡像中的一個指令,進而安裝我們的子產品。最終,我們暴露伺服器端口,告訴docker我們需要在端口8123上支援入向連接配接,之後啟動我們的服務代碼。

以下指令用來檢查确認測試資料庫服務運作,服務的運作,然後建立和再次運作鏡像:

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

這裡假如你在浏覽器中直接通路 localhost:8123/users 将會看到錯誤,檢查控制台終端你将會看到容器報告了一些問題:

--- Customer Service---
Connecting to customer repository...  
Connected. Starting server...  
Server started successfully, running on port   
GET /users   ms -   
Error: An error occured getting the users: Error: connect ECONNREFUSED :  
    at Query._callback (/app/repository/repository.js::)
    at Query.Sequence.end (/app/node_modules/mysql/lib/protocol/sequences/Sequence.js::)
    at /app/node_modules/mysql/lib/protocol/Protocol.js::
    at Array.forEach (native)
    at /app/node_modules/mysql/lib/protocol/Protocol.js::
    at nextTickCallbackWith0Args (node.js::)
    at process._tickCallback (node.js::)
           

是以,可以看出所有從我們的users-service容器到test-database容器的連接配接請求都被拒絕了。我們可以試着運作docker ps看看所有正在運作的容器:

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

很顯然,它們都在,那麼是怎麼回事呢?

連接配接容器

其實我們看到的這個問題是預期的,Docker容器都應該是彼此隔離的,是以建立容器之間的連接配接本來就是不合理的,除非我們有顯式的允許它們這麼做。

是的,我們可以連接配接我們的主機和容器,因為我們已經對此開放了端口(例如通過-p 8123:8123)。假設我們允許容器之間也能同樣交流,那麼在同一個主機上的兩個容器能夠互相溝通,甚至開發者完全沒有計劃這麼做,那這将是一個徹底的災難,特别是當我們在一個叢集的大量機器上運作基于容器的各種不同應用程式時。

假如我們需要連接配接不同的容器時,我們需要連接配接(link)它們,它的目的在于顯式的告訴docker我們允許它們互相通訊。這裡有兩種方式可以實作,第一種是老的模式,但是畢竟簡單,第二種我們将在稍後介紹。

通過’link’ 參數連接配接容器

當我們在運作容器時,可以通過link 參數告訴docker我們計劃連接配接到另一個容器。在我們的例子中,我們可以通過如下指令來正确運作我們的服務:

  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 || ,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 
  }
};
           

當運作容器時,我們設定了環境變量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  
    localhost  
::    localhost ip6-localhost ip6-loopback
fe00::    ip6-localnet  
ff00::    ip6-mcastprefix  
ff02::    ip6-allnodes  
ff02::    ip6-allrouters  
    db f91343db01    # linking magic!!  
    ac9449d3d552  
           

還記得docker exec是工作的嗎?首先選擇容器名稱,然後跟随的指令就是無論任何你想要在容器中執行的,在本例中 cat /etc/hosts 。

很明顯hosts檔案并沒有什麼linking魔法。所有這裡你可以看到docker已經把 db加到了我們的hosts檔案中,這樣我們就可以通過主機名連結到容器。這就是linking的其中一個結果。以下是其他更多詳細内容:

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連接配接容器時,它同樣提供了一系列的環境變量以及一些非常有用的資訊。我們可以知道host,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('http://localhost:8123');

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

    api.get('/[email protected]')
      .expect(, done);
  });

});
           

這将檢查API調用并顯示測試結果。隻要你的users-service和test-database都在運作,測試就會通過。 然而,到了這個階段,服務将會越來越難處理:

  1. 我們必須使用一個shell腳本來啟動和停止資料庫
  2. 我們必須記住針對資料庫的使用者服務啟動指令序列
  3. 我們必須使用node直接運作內建測試

現在我們已經對docker有了更多了解,我們可以用docker解決這些問題。

簡化測試資料庫

目前,對于測試資料庫我們有如下檔案:

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

現在我們對docker有了更多了解,我們可以來着手改善這個。在Docker Hub的mysql image documentation 有一個提示告訴我們所有被添加到鏡像/docker-entrypoint-initdb.d 目錄的.sql 或 .sh 檔案在配置資料庫時都會被自動執行。

這就意味這我們可以用dockerfile來替代start.sh 和 stop.sh腳本。

FROM mysql:

ENV MYSQL_ROOT_PASSWORD   
ENV MYSQL_DATABASE users  
ENV MYSQL_USER users_service  
ENV MYSQL_PASSWORD 

ADD setup.sql /docker-entrypoint-initdb.d  
           

現在運作我們的測試資料庫隻需要如下指令:

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

編排(Composing)

至此,建構和運作每個容器仍然需要消耗一定時間。我們可以采用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. 拷貝一切是個壞主意,因為我們會同時拷貝node_modules目錄。通常,你最好是顯式的列出需要拷貝的檔案和目錄,或者使用一個.dockerignore,就像使用.gitignore一樣。
  2. 假如伺服器并沒有按預期運作,這可能并不是真正麻煩的異常,而可能是supertest的一個bug。具體見github.com/visionmedia/supertest/issues/314

原文連結: http://www.dwmkerr.com/learn-docker-by-building-a-microservice/

翻譯:王旭敏,Nokia開發工程師,關注雲計算、高性能及可用架構、容器等。

責編:魏偉,歡迎加入微服務架構群探讨技術和經驗,微信搜尋“k15751091376”進入。

繼續閱讀