Node.jsのマイクロサービスの構築を通してDockerを学ぶ – 後編

前編はこちら:Node.jsのマイクロサービスの構築を通してDockerを学ぶ – 前編

ステップ3:マイクロサービスを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を実行するとシステム上の全てのイメージを見ることができます。2番目のコマンドdocker runは、ある程度説明してきたので、だいぶ慣れているはずです。

  1. docker runは、イメージから新しいコンテナを実行します。
  2. -itはインタラクティブなターミナルを利用します。
  3. node4は、コンテナで使用したいイメージのタグです。

このイメージを実行すると、Node REPLが立ち上がります。現在のバージョンが以下のように表示されるので、確認してください。

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

これは、あなたの現在のマシン上のNodeバージョンと異なる可能性があります。

Dockerfileの中身を調べる

Dockerfileを見れば、何が行われているかを非常に簡単に知ることができます。

  1. Dockerfileの中で最初に指定するFROM node:4は、ベースイメージです。Googleでさっと検索すると、利用可能な全てのイメージを表示しているDocker Hub上のNodeのページが見つかります。これは、基本的にNodeがインストールされた必要最小限のUbuntuです。

  2. CMD ["node"]CMDコマンドは、このイメージがNodeの実行可能ファイルを実行する必要があることをDockerに知らせます。実行が終了すると、コンテナはシャットダウンします。

次のように、さらにコマンドを幾つか追加すれば、私たちのサービスを実行するように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/というコンテナの中のフォルダにカレントディレクトリにあるものを全て1コピーします。次に、RUNを使って、イメージの中のコマンドを実行します。それによって、私たちのモジュールがインストールされます。最後に、サーバポートをEXPOSEして、8123でインバウンド接続をサポートするつもりであることをDockerに伝えます。その後、私たちのサーバコードを実行します。

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という引数を使います)。もしもコンテナに、お互いに同じ方法で通信することを許せば、開発者が意図していなかったとしても同じコンピュータで実行されている2つのコンテナは、通信できてしまうのです。これは特に、異なるアプリケーションからコンテナを実行するのが仕事のコンピュータがいくつもある場合は大失敗の元になります。

もしもコンテナを他のコンテナと接続させるなら、リンクさせる必要があります。これにより、Dockerに「2つのコンテナの間のコミュニケーションを明白に許可したい」と伝えられます。これを行う方法は2つあります。1つは”昔ながらの”非常にシンプルな方法です。2つ目はもう少し後で見ましょう。

“リンク”パラメータを使ってコンテナをリンクさせる

コンテナを実行する時は、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:dbdbと名付けられたコンテナにリンクし、これをdbとして参照する。
  4. -e DATABASE_HOST=dbは環境変数DATABASE_HOSTdbに設定する。
  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になります。

hostsファイルに、通常は含まれない# linking magic!!というコメントがあることに気付きましたね。Dockerがhostsファイルにdbを加えたので、ホスト名を使ってリンクされたコンテナを参照することができます。これは、リンクすることによる結果の1つです。他の例も挙げておきましょう。

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のマイクロサービスもあり、これらをどのようにリンクさせるかも分かりました。

この段階でコードがどのようになっているのかは、ステップ3のブランチに行くことで確認できます。

ステップ4:環境の統合テスト

これで、実際のサーバを呼び出す統合テストを書くことができるようになりました。そして、Dockerコンテナとして実行し、コンテナ化されたテストデータベースを呼び出します。

統合テストは、常識の範囲内で、あなたが書きたいと思うどの言語、どのプラットホームで書いても構いません。しかし、状況を単純にしておくため、私たちのプロジェクトで既にMochaとSupertestを見てきたように、Node.jsを使っています。

integration-testsと名付けた新しいフォルダの中に、以下のような内容を持つindex.jsファイルが1つだけ入っています。

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呼び出しをチェックして、テストの結果を示します2

users-servicetest-databaseが動いている限り、テストは合格するでしょう。しかしこの段階では、サービスは少々扱いづらくなっています。

  1. データベースを起動や停止するためにシェルスクリプトを使わなければならない
  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という拡張子のファイルはどれも、そのDBをセットアップする際に実行されるという解説が記されています。

これは次のように、スクリプトファイルstart.shstop.shDockerfileに置き換えることができることを意味しています。

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  

Composeする

各コンテナを構築し、実行するのは、いまだに時間がかかる作業です。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の値は、Dockerfileをどこで見つければよいかをDockerに伝えます。docker-compose upと実行すると、Dockerは全サービスを開始します。Dockerfileから、ポートと依存パッケージを特定できることにご注意ください。実のところ、私たちが変更できる設定がこのファイルの中にたくさん含まれています。

別のターミナルで、docker compose downと実行すれば、円滑にコンテナを終了します。

まとめ

この記事の中で、Dockerについて多くのことを見てきました。しかし、まだまだたくさんあります。あなたの業務においてDockerを使えるような興味深く役に立つことをご紹介できていればいいのですが。

いつものように、質問やコメントは大歓迎です! Dockerがどのように機能するかという理解をより深めるために、Dockerを理解するという文書も一読されることを強くお勧めします。

この記事でご紹介したプロジェクトの最終的なソースコードは、以下のWebサイトから入手できますgithub.com/dwmkerr/node-docker-microservice

注釈


  1. 何もかもをコピーするのは、実のところ名案ではありません。というのも、node_modulesフォルダもコピーしてしまうからです。一般的にはコピーしたいと思うファイルとフォルダを明示的に指定するのが良い考えです。あるいは.gitignoreファイルと同じような働きをする.dockerignoreファイルを使用してください。 

  2. もしサーバが起動しない場合は、実際にはかなり厄介な例外の兆候です。これはSupertestのバグによるものです。詳細はgithub.com/visionmedia/supertest/issues/314をご覧ください。