2016年6月30日
Node.jsのマイクロサービスの構築を通してDockerを学ぶ – 後編
(2016-04-19)by Dave Kerr
本記事は、原著者の許諾のもとに翻訳・掲載しております。
前編はこちら: 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
それでは、まず、ビルドコマンドを見てみましょう。
docker build
は、新しいイメージを作成したいことをエンジンに伝えます。-t node4
は、このイメージにnode4
というタグを付けます。今後は、このイメージをタグで参照することができます。.
は、Dockerfile
を探す際にカレントディレクトリを検索します。
コンソールに幾つか出力された後、新しいイメージが作成されたことが表示されます。 docker images
を実行するとシステム上の全てのイメージを見ることができます。2番目のコマンド docker run
は、ある程度説明してきたので、だいぶ慣れているはずです。
docker run
は、イメージから新しいコンテナを実行します。-it
はインタラクティブなターミナルを利用します。node4
は、コンテナで使用したいイメージのタグです。
このイメージを実行すると、Node REPLが立ち上がります。現在のバージョンが以下のように表示されるので、確認してください。
> process.version
'v4.4.0'
> process.exit(0)
これは、あなたの現在のマシン上のNodeバージョンと異なる可能性があります。
Dockerfileの中身を調べる
Dockerfileを見れば、何が行われているかを非常に簡単に知ることができます。
-
Dockerfileの中で最初に指定する
FROM node:4
は、ベースイメージです。Googleでさっと検索すると、利用可能な全てのイメージを表示している Docker Hub上のNodeのページ が見つかります。これは、基本的にNodeがインストールされた必要最小限のUbuntuです。 -
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
docker run -it
は、インタラクティブなターミナルを使いコンテナ内でDockerイメージを実行する。-p 8123:8123
はホストのポート8123をコンテナのポート8123へマップする。link db:db
はdb
と名付けられたコンテナにリンクし、これをdb
として参照する。-e DATABASE_HOST=db
は環境変数DATABASE_HOST
をdb
に設定する。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-service
と test-database
が動いている限り、テストは合格するでしょう。しかしこの段階では、サービスは少々扱いづらくなっています。
- データベースを起動や停止するためにシェルスクリプトを使わなければならない
- データベースに対してユーザサービスを起動するための一連のコマンドを覚えておかなければならない
- 統合テストを実行するために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.sh
と stop.sh
を Dockerfile
に置き換えることができることを意味しています。
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 。
注釈
-
何もかもをコピーするのは、実のところ名案ではありません。というのも、node_modulesフォルダもコピーしてしまうからです。一般的にはコピーしたいと思うファイルとフォルダを明示的に指定するのが良い考えです。あるいは.gitignoreファイルと同じような働きをする.dockerignoreファイルを使用してください。 ↩
-
もしサーバが起動しない場合は、実際にはかなり厄介な例外の兆候です。これはSupertestのバグによるものです。詳細は github.com/visionmedia/supertest/issues/314 をご覧ください。 ↩
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa