Docker Composeを使ってマイクロサービスを作ってみた

Adways Advent Calendar 12日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar/archive


こんにちは。本間です。
最近、業務でDocker Compoesを扱うようになったで勉強も兼ねて個人的に使うツールを開発する際に導入してみました。
作成したツールはSlackにコマンドを打つとコマンドに応じて返答を返してくれるBOTです。
最近良く耳にするマイクロサービスを意識して作成しました。 完成したBOTは、以下のような感じです。

f:id:AdwaysEngineerBlog:20161216154932p:plain

構成イメージ

f:id:AdwaysEngineerBlog:20161216150030p:plain

※ コマンド処理サービスを以降ではuiと呼びます。
※ api群の1つをスケジュール管理サービスとし以降ではapiと呼びます。
※ ID紐付けサービスを以降ではcronと呼びます。
※ IDキャッシュサービスを以降ではキャッシュと呼びます。

処理の流れ

予定 @homma-masahiro とコマンドを打つと本間 将紘の予定を取得してSlackで返答します。
処理の流れは下記の通りになります。

  1. Slackにコマンドを発言します。
  2. uiがコマンドを解析します。
  3. uiがキャッシュに問い合わせてSlackのIDとスケジュール管理サービス上のIDを解決します。
    ※ cronが定期的にキャッシュを更新します。
  4. uiが3の結果を利用してapiに本間 将紘の予定を問い合わせます。
  5. uiが4の結果をSlackに投稿します。

ディレクトリ構成

├── Dockerfile_ui
├── Dockerfile_api
├── Dockerfile_cron
├── docker-compose.yml
├── conf
│    └── ntp.conf
├── ui
│    ├── config.js
│    ├── index.js
│    ├── lib
│    ├── node_modules
│    ├── npm-debug.log
│    ├── package.json
│    └── run
├── api
│    ├── app.js
│    ├── bin
│    ├── lib
│    ├── node_modules
│    ├── package.json
│    ├── routes
│    ├── ssh
│    └── views
└── cron
      ├── cache_email_id.js
      ├── cache_id_email.js
      ├── config.js
      ├── lib
      ├── node_modules
      └── package.json

※ Dockerfile_*はそれぞれのコンテナのイメージを作成するためのDockerfileです。
※ docker-compose.ymlは各コンテナを管理するためのファイルです。
※ ui, api, cron はそれぞれ独立して動くサービスのソースコードです。

Dockerの環境

  • Docker version 1.12.0-rc2, build 906eacd, experimental
  • docker-compose version 1.8.0-rc1, build 9bf6bc6

Docker

DockerはDockerfileをもとに簡単にコンテナのイメージを作成することができます。
今回開発したサービスの各コンテナのDockerfileは下記になります。

ui

FROM node:4.1.2

ENV TZ=JST-9

RUN apt-get update
RUN apt-get -y install ntp

COPY conf/ntp.conf /etc

RUN npm update -g npm

RUN mkdir /ui
WORKDIR /ui
COPY ui/package.json .
RUN npm install

api

FROM node:4.1.2

ENV TZ=JST-9

RUN npm update -g npm

RUN mkdir /api
WORKDIR /api
COPY api/package.json .
RUN npm install

cron

FROM node:4.1.2

ENV TZ=JST-9

RUN npm update -g npm

RUN mkdir /cron
WORKDIR /cron
COPY cron/package.json .
RUN npm install

設定オプションには下記の表の意味があります。

オプション名 意味
FROM ベースとなるイメージを指定できます。
ENV 環境変数を設定できます。
RUN コマンドを実行できます。
WORKDIR 作業ディレクトリを指定できます。
COPY ホストからコンテナにファイルをコピーできます。

他にもオプションがありますので詳しくは下記を参照してください。日本語で助かりました@w@
http://docs.docker.jp/engine/reference/builder.html

ui, api, cronはいずれもNode.jsで開発しました。 共通するイメージの作成手順は下記になります。

  1. Node.jsがインストールされたイメージを用意します。
  2. npmをアップデートします。
  3. サービス用のディレクトリを作成します。
  4. サービスのpackage.jsonをコンテナにコピーします。
  5. package.jsonをもとにモジュールをインストールします。

以上でNode.jsで作成したサービスが動作するイメージを作成できます。簡単ですね!
uiには時刻同期ができるようにntpをインストールしています。

Docker Compose

Docker Composeは複数のコンテナからなるサービスの構成をymlファイルで容易に管理することができます。
今回開発したサービスのymlファイルは下記になります。

今回は、Docker Composeはversion1 を使用しています

ui:
  restart: always
  build: .
  dockerfile: Dockerfile_ui
  cap_add:
    - SYS_TIME
  extra_hosts:
    - "ntp.nict.jp:192.168.2.3"
  links:
    - api
    - cache
  environment:
    NODE_TLS_REJECT_UNAUTHORIZED: 0
  volumes:
    - ./ui:/ui
    - /ui/node_modules
  command: ./run

api:
  restart: always
  build: .
  dockerfile: Dockerfile_api
  volumes:
    - ./api:/api
    - /api/node_modules
  command: npm start

cron:
  build: .
  dockerfile: Dockerfile_cron
  links:
    - api
    - cache
  environment:
    NODE_TLS_REJECT_UNAUTHORIZED: 0
  volumes:
    - ./cron:/cron
    - /cron/node_modules

cache:
  restart: always
  image: "redis:2.8.21"

キャッシュに使っているredisはDocker Hubにイメージがあるので自分で用意する必要がありません。簡単!

設定オプションには下記の表の意味があります。

オプション名 意味
restart restart policyを設定できます。 always にすると明示的に stop しない限り、常に再起動されます。
build Dockerfileがあるディレクトリを指定できます。
dockerfile Dockerfileを指定できます。
cap_add 制限されている機能を使用できるようにします。 SYS_TIME を追加すると時刻の変更ができるようになります。
extra_hosts ホスト名を割り当てます。 /etc/hostsに追記されます。
links 他のコンテナと連携設定ができます。
environment 環境変数を設定できます。
volumes ディレクトリのマウントを設定できます。
command デフォルトの実行コマンドを設定できます。

他にもオプションがありますので詳しくは下記を参照してください。

http://docs.docker.jp/compose/compose-file.html

ちょっとはまったところ

apiをhttps対応にする

apiはNode.jsのモジュールexpressを使ってwebサーバーを構築しています。
https対応するためには公開鍵と証明書を作成し、サーバ起動時のオプションに設定する必要があります。
個人的に使用するツールなので自己署名の証明書を使うことにしました。

apiのディレクトリ構成

└── api
      ├── app.js
      ├── bin
      │    └── www
      ├── lib
      ├── node_modules
      ├── package.json
      ├── routes
      ├── ssh
      │    ├── server.crt # 証明書
      │    └── server.key # 公開鍵
      └── views

api/bin/www の対応箇所

var options = {
  key:  fs.readFileSync('./ssh/server.key'),
  cert: fs.readFileSync('./ssh/server.crt')
};

var server = https.createServer(options, app);

このapiを使うuiとcronもNode.jsで作成しました。
uiやcronからrequestモジュールを使って自己署名の証明書を使ったhttpsサーバにアクセスすると
下記のエラーがでて通信をすることが出来ません。

Got error: UNABLE_TO_VERIFY_LEAF_SIGNATURE

このエラーは環境変数に下記を設定することで無視出来るようになります。
※ docker-compose.ymlでapiとuiのコンテナを起動する時に環境変数を設定することで解消しました。

NODE_TLS_REJECT_UNAUTHORIZED: 0

crontabでdocker-compose runを定期的に実行させる

cronを定期的に実行させるためにcrontabに下記を設定しました。

0 5 * * * docker-compose run cron npm run cache_id_email
0 5 * * * docker-compose run cron npm run cache_email_id

1つ目のタスクは実行されましたが、同じ名前のコンテナは作成出来ないので2つ目のタスクはエラーで実行できませんでした。
オプションで名前を指定することで2つのタスクを実行できるようになります。
しかし、2回目に実行する時に既に同じ名前のコンテナが存在するためどちらのタスクもエラーで実行できませんでした。
定期的に実行するためには実行後にコンテナを削除するオプションが必要です。
最終的に2つのタスクを定期的に実行できるcrontabの設定は下記になります。

0 5 * * * docker-compose run --rm --name cache_id_email cron npm run cache_id_email
0 5 * * * docker-compose run --rm --name cache_email_id cron npm run cache_email_id

消えないVolume

イメージをbuildすると下記のエラーで失敗することがありました。

write error: No space left on device

ディスク容量が不足していると発生するエラーなので必要ないコンテナとイメージを削除すると解決することがあります。
しかし、必要のないコンテナとイメージを削除してみましたが解決しませんでした。
原因はコンテナを削除してもマウントされていたVolumeが消えずに残り続けるからでした。

整理すると私の環境下で write error: No space left on device が発生した原因は
crontabで実行後にコンテナを削除するようにしてタスクを実行することで、マウントされていないVolumeがどんどん増えていったためディスク容量が足りなくなったと考えられます。

この問題を解決するために、定期的にマウントされていないVolumeを削除するタスクをcrontabに設定しました。

docker volume rm $(docker volume ls -qf dangling=true)

docker volume ls -qf dangling=true マウントされていないVolumeの一覧を取得します。
docker volume rm Volumeを削除します。

消えるnode_modules

各サービスのnode_modulesはgitで管理していないため、イメージをビルドする時にサービスのディレクトリにnpm installします。

以降、uiを例にとります。

※ Dockerfileの該当箇所

RUN mkdir /ui
WORKDIR /ui
COPY ui/package.json .
RUN npm install

各サービスのソースコードはgitで管理しているので、コンテナを起動する時にサービスのディレクトリにマウントします。

※ docker-compose.ymlの該当箇所

volumes:
  - ./ui:/ui

この方法だとコンテナが起動するときのuiディレクトリの状態は下記のように遷移します。

  1. イメージにuiディレクトリ作成
    イメージに空のuiディレクトリが作成されます。
  2. イメージのuiディレクトリにホストマシンのpackage.jsonのコピー
    イメージのuiディレクトリの直下にpackage.jsonがコピーされます。
  3. npm install
    イメージのuiディレクトリの直下にnode_modulesが作成され、モジュールがインストールされます。
  4. コンテナのuiディレクトリにホストマシンのuiディレクトリをマウント
    ホストマシンのソースコードがあり、node_modulesがないuiディレクトリがコンテナのuiディレクトリにマウントされます。

この状態でuiを起動すると下記エラーで失敗します。

Error: Cannot find module ◯◯

3の段階では確実にインストールされているのに実行時には4の手順でnode_modulesがなくなってしまいます。
4の手順でイメージのnode_modulesをコンテナにマウントすることで解決できます。

※ 解決後のdocker-compose.yml

volumes:
  - ./ui:/ui
  - /ui/node_modules

所感

  • Dockerを使うと仮想環境を簡単に構築出来て良かったです。
    ※ 今回はNode.jsとRedisのベースイメージしか利用していませんが、
    Docker HubにはNode.jsやRedisの他にも様々なベースイメージがあります。
    https://hub.docker.com/explore/

  • Docker Composeを使う事でコンテナ同士の連携を簡単に設定出来て良かったです。
    ※ 低レイヤーの設定に時間をかけなくてい良いのでサービス構築に集中できます。


次は奥村さんの記事です。

http://blog.engineer.adways.net/entry/advent_calendar/13