本稿は、Christffer Noring さん (@chris_noring) の Learn Docker, from the beginning, part V を翻訳し、分かりやすいように少しだけ追記、サンプルコードの実行上の補足等を行ったものです。シリーズ翻訳の意図については 超基礎からの 速習 Docker (1) の冒頭に掲載しています。併せてご参照くださいませ。
Twitter をフォローして、トピックへのあなたの問い合わせやご質問、提案を頂けるとハッピーです。
#超基礎からの 速習 Docker シリーズ一覧
この文章はシリーズの一つです:
-
超基礎からの 速習 Docker (1)
- なぜ Docker なのか、コンテナーやイメージ、Dockerfile の基本コンセプトの解説、もちろん、それらを管理するのに必要なコマンド群もカバーしています。
-
超基礎からの 速習 Docker (2)
- Volume を用いたデータの永続化や、開発環境の Volume 化を通じて、開発をより手軽なものにします。
-
超基礎からの 速習 Docker (3)
- データベースをコンテナ化し、レガシーなやり方及び新しいやり方である Network を用いて他のコンテナから連絡可能にします。
-
超基礎からの 速習 Docker (4)
- Docker Compose を用いた複数サービスの管理方法を解説します(その1)
-
超基礎からの 速習 Docker (5)
- 僕らはいまここ
パート IV で紹介したプロジェクトを引き続き用いて、Docker Compose の機能と、必要になるだろう全てをカバーする本質にせまったプロジェクトのビルドについてのショーケースをお見せします。
このパートでカバーするのは:
- 環境変数 前回のパートでカバーしていますので、多くはどのようにそれらを Docker Compose にセットするかについてです。
- Volume 以前の記事でカバーしていますが、その利用法と Docker Compose でどう使うかについて説明します。
- Network と データベース ついにデータベースと Network をカバーします。このパートは少しトリッキーですが、多分、全てを説明することに成功したと思います。
もし、どこかのポイントで混乱を感じたなら、この記事の元になっているリポジトリーは以下にあります。
注釈
混乱も何も、パート IV 以降、Docker Compose 解説に用いられていたプログラムは、この GitHub にあるコードが初公開です(お待たせしました)。以降、こちらのコードを用いて確認していきましょう。パート IV のお浚いにも使えるかと思います。
#リソース
Docker を使う事、コンテナー化は、一枚岩をマイクロサービスに分解していくことです。このシリーズのいたるところで僕らは Docker やそのコマンド体系をマスターするために学ぶことになります。そうすれば、きっと君は自作のコンテナーをプロダクション環境で使いたくなるでしょう。その環境は大抵クラウド上にあります。十分な Docker 経験を積んだと思ったなら、次のリンクで Docker をクラウドでどのように活用できるか、ご確認してみると良いと思います。
- Azure 無料アカウントのサインアップ プライベート レジストリのようなクラウドのコンテナーを使うには、無料 Azure アカウントが必要でしょう。
- クラウドのコンテナー クラウドのコンテナーについて他に知っておくべきことについて網羅する概要ページです。
- 自作コンテナーをクラウドにデプロイ 今の Docker スキルをレバレッジしてクラウド上でサービスを動かすことがいかに簡単かを示すチュートリアル。
- コンテナー レジストリの作成 自作 Docker イメージを Docker Hub に入れられますが、クラウドのコンテナー レジストリも可能です。自作イメージをどこかにストアして、一瞬でレジストリから実際のサービスをできるようにすることは凄くないですか?
#環境変数
以前の記事で示したものの一つに、環境変数の指定方法があります。変数は Dockerfile
に設定できますが、コマンドラインで設定でき、よって、Docker Compose の中でも可能であり、つまり、docker-compose.yaml
内では:
version: '3'
services:
product-service:
build:
context: ./product-service
ports:
- "8000:3000"
environment:
- test=testvalue
inventory-service:
build:
context: ./inventory-service
ports:
- "8001:3000"
上記では、-test=testvalue
で環境変数の定義、変数 test の値 testvalue
を行っています。
product-service ディレクトリにある app.js ファイル の中で、process.env.test から読み込むことで、簡単にテストできます。
他のテスト方法は、Docker Compose を実行して、どんな環境変数が有効か、以下のように query することです:
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
72a2c0c98dda docker-compose-experiments_product-service "docker-entrypoint.s…" 6 minutes ago Up 56 seconds 0.0.0.0:8000->3000/tcp docker-compose-experiments_product-service_1
211d3dc8584a docker-compose-experiments_inventory-service "npm start" 6 minutes ago Up 56 seconds 0.0.0.0:8001->3000/tcp docker-compose-experiments_inventory-service_1
fb095493518d docker-compose-experiments_db "docker-entrypoint.s…" 6 minutes ago Up 56 seconds 0.0.0.0:8002->3306/tcp docker-compose-experiments_db_1
% docker exec docker-compose-experiments_product-service_1 env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=72a2c0c98dda
test=testvalue
DATABASE_PASSWORD=complexpassword
DATABASE_HOST=db
NODE_VERSION=14.1.0
YARN_VERSION=1.22.4
PORT=3000
HOME=/root
まず最初に、Docker Compose セッションのコンテナーを docker-compose ps
とすることで取得します。ついで、docker exec [container name] env
として、環境変数をリストします。他のやり方としては docker exec -it [container name] bash
を実行してコンテナーに入り、環境変数を表示させます。Docker Compose で環境変数を管理するやり方はわずかしかありません。他に何ができるかについては、official doc を参照ください。
訳注
もし、docker ps
で何も出てこなければ、まだコンテナーを起動していません。以下の手順でコンテナーを起動します。
- GitHub から clone します。Git をインストールして、こんな感じで clone します。
- 取得した
docker-compose-experiments/product-service
フォルダに移動。 -
npm install --save-dev nodemon
として nodemon をインストールする。 - (Windows のみ)wait-for-it.sh ファイルを CRLF から LF に変換する。これにはいろんなやり方があります。適当なエディタで変換しても OK です。CRLF ってなんじゃいって言う方はこちら。
-
docker-compose up -d
などとしてコンテナーを起動します。
#Volume
以前のパートで Volume についてカバーして分かった凄いやり方は:
- 永続的スペースを作る ログファイルや データベースと言った、コンテナーの再起動後も残しておきたいものに最適です。
- 開発環境を Volume 上に展開する このようにするメリットは、コンテナー開始後のコードの変更を、リビルドやコンテナーの再起動なしに反映する、言わばリアルタイム サーバーにすることができる点です。
「永続的スペース」は、persistent space を翻訳したものですが、要はそこに書いてある通り、コンテナーの終了後も残せるということをイメージして「永続的」と言っています。
##永続的スペースを作る
Docker Compose で Volume をどのように扱うか見て行きましょう。
version: '3'
services:
product-service:
build:
context: ./product-service
ports:
- "8000:3000"
environment:
- test=testvalue
inventory-service:
build:
context: ./inventory-service
ports:
- "8001:3000"
volumes:
- my-volume:/var/lib/data
volumes:
my-volume:
ファイル最後の volumes コマンドで volume を作成しています。2つ目の行では、my-volume
と言う名前を与えています。さらに、inventory-service の個所で、作成された volume の参照と、終了しても永続化したい Volume のディレクリ、var/lib/data
へのマッピングを行っています。正しくマップされている状態を見てみましょう。
% docker exec -it 211 bash
root@211d3dc8584a:/app# ls
Dockerfile README.md app.js node_modules package-lock.json package.json
root@211d3dc8584a:/app# cd ..
root@211d3dc8584a:/# ls
app bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@211d3dc8584a:/# cd var/lib/
root@211d3dc8584a:/var/lib# ls
apt data dpkg git misc pam python systemd ucf
root@211d3dc8584a:/var/lib# cd data
root@211d3dc8584a:/var/lib/data# ls
ご覧になっている通り、上記コマンド、docker exec
でコンテナーに入り、次にマッピングされたディレクトリに移動しています。ありました。すごいね
Volume マッピングがちゃんと動いているかどうか確認するために、ディレクトリにファイルを作りましょう:
echo persist > persist.log
上記コマンドは、persist と言う内容で persist.log
を作っています。派手なものではありませんが、コンテナーの再起動後でも探すことのできるファイルを作成します。
さあ、コンテナーを終了できます。次に、便利な Volume コマンドをお浚いしましょう:
docker volume ls
% docker volume ls
DRIVER VOLUME NAME
local ce29ece31b9a806b4f8f5527ca914c6b695fc28ebea294f61b1fa916b2988bd1
local docker-compose-experiments_my-volume
上記は 現在マウントされている Volume すべてがリストされます。作成された Volume docker-compose-experiments_my-volume も見えます。
更なる詳細へダイブしましょう:
docker volume inspect docker-compose-experiments_my-volume
% docker volume inspect docker-compose-experiments_my-volume
[
{
"CreatedAt": "2020-06-02T16:49:32Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "docker-compose-experiments",
"com.docker.compose.version": "1.25.5",
"com.docker.compose.volume": "my-volume"
},
"Mountpoint": "/var/lib/docker/volumes/docker-compose-experiments_my-volume/_data",
"Name": "docker-compose-experiments_my-volume",
"Options": null,
"Scope": "local"
}
]
OK、これは Moutpoint
と言った Volume の詳細を教えてくれます。Mountpoint
は、Volume マッピング ディレクトリに書き込んだ時、どこにあるファイル群が永続化されるのかが分かります。
それではコンテナーを終了させましょう。
docker-compose down
Volume はまだあるはず。それでは再度コンテナーを開始しましょう:
docker-compose up-d
コンテナーに入って、persist.log ファイルがあるかどうか見てみましょう。
% docker exec -it 648 bash
root@648f3b1ac8f1:/app# ls
Dockerfile README.md app.js node_modules package-lock.json package.json
root@648f3b1ac8f1:/app# cd ..
root@648f3b1ac8f1:/# ls
app bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@648f3b1ac8f1:/# cd var/
root@648f3b1ac8f1:/var# ls
backups cache lib local lock log mail opt run spool tmp
root@648f3b1ac8f1:/var# cd lib
root@648f3b1ac8f1:/var/lib# ls
apt data dpkg git misc pam python systemd ucf
root@648f3b1ac8f1:/var/lib# cd data
root@648f3b1ac8f1:/var/lib/data# ls
persist.log
root@648f3b1ac8f1:/var/lib/data# more persist.log
persist
root@648f3b1ac8f1:/var/lib/data#
イェーイ、動いていますね。
##カレント ディレクトリを Volume にする
OK、新しい Volume を追加して、僕らのコンピュータのディレクトリと、それと同期するべきコンテナー内の場所を指定する必要があります。docker-compose.yaml
は次のようになっています:
version: '3'
services:
product-service:
build:
context: ./product-service
ports:
- "8000:3000"
environment:
- test=testvalue
volumes:
- type: bind
source: ./product-service
target: /app
inventory-service:
build:
context: ./inventory-service
ports:
- "8001:3000"
volumes:
- my-volume:/var/lib/data
volumes:
my-volume:
新しく追加になったのは、product-service
です。Volume コマンドを一つのエントリーで設定できるようになったのが分かります。では、エントリーをブレイク・ダウンしてみましょう:
- type: bind bind mount と呼ばれる、ローカル ディレクトリとコンテナー間のファイル同期によりフィットした Volume を作ります。
-
source 単純に君のファイル群がどこにあるか。ここでは
./product-service
が指定されています。このディレクトリにあるファイルに変更があれば、Docker は直ちにそれを拾い上げます。 - target コンテナーのディレクトリです。source と target は同期状態となります。source が変われば、同じ変更が target にも起きます。
##Network とデータベース
OK、それではこれがこの記事でカバーしようとしていたことのラスト パートです。データベースを開始しましょう。多くの主要なベンダーは SQL Server、Postgres、MuSQL などなどのような Docker Image を持っています。このことは、データベースを起動、実行するのに、ビルド ステップは必要ないことを意味します。しかし、環境変数ややりとりするための Port のオープンといった設定は必要です。僕らのソリューションに MySQL データベースを追加しましょう。dockder-compose.yml ファイルで行います。
##データベースの追加
データベースを docker-compose.yml に追加することは、凡そ、既成イメージを追加することです。幸運なことに、MySQL は Ready-made イメージが提供されています。追加するには、ただ サービス以下に別のエントリーを追加するだけです。こんな具合に:
product-db:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=complexpassword
ports:
- 8002:3306
ブレイクダウンしていきましょう:
-
product-db
は僕らが選んだ新しいサービス エントリーの名前です。 -
image
はビルドの代わりの新しいコマンドです。イメージがすでにビルドされている時に使います。ほとんどのデータベースについて可能です。 -
enviroment
多くのデータベースはユーザー名、パスワード、もしかするとデータベースの名前(データベースの種類で変わります)といった、接続のためのいくつかの変数を設定する必要があるでしょう。今回のケースでは、MYSQL_ROOT_PASSWORD が設定され、root ユーザーのパスワードが何か、MySQL インスタンスに伝えています。アクセス レベルを変えたユーザーをいくつか作ることを考慮するべきでしょう。 -
ports
オープンするポートを列挙します。これがデータベースと話す入り口になります。8002:3306
とすれば、コンテナーの Port は 3306 が外部ポート 8002 にマッピングされます。
データベースと残りのサービスを起動、実行しましょう:
% docker-compose up -d
Creating network "docker-compose-experiments_products" with the default driver
Creating network "docker-compose-experiments_default" with the default driver
Creating docker-compose-experiments_inventory-service_1 ... done
Creating docker-compose-experiments_db_1 ... done
Creating docker-compose-experiments_product-service_1 ... done
確認してみましょう:
docker-compose ps
または docker ps
% docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------------------------------------
docker-compose-experiments_db_1 docker-entrypoint.sh mysqld Up 0.0.0.0:8002->3306/tcp
docker-compose-experiments_inventory-service_1 npm start Up 0.0.0.0:8001->3000/tcp
docker-compose-experiments_product-service_1 docker-entrypoint.sh /wait ... Up 0.0.0.0:8000->3000/tcp
良さそうですね。データベース docker-compose-experiments_db_1
は Port 8002 で起動、実行しているように見えます。次にデータベースに接続してみましょう。以下のコマンドはデータベースに接続します。指を交差させて
mysql -uroot -pcomplexpassword -h 0.0.0.0 -P 8002
訳注
パート3で述べた通り、Windows では 0.0.0.0 にアクセスすることはできないため、docker exec -it docker-compose-experiments_db_1 bash
などとして、コンテナー内にログインし、コンテナー内でmysql -uroot -pcomplexpassword
すれば良いかな、と思います。
勝者は・・:
% mysql -uroot -pcomplexpassword -h 0.0.0.0 -P 8002
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.48 MySQL Community Server (GPL)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
素晴らしい!次はデータベースに接続するように更新しましょう。
##データベースに接続する
データベースに接続するには、主に 3つの方法があります。
- すでに試した通り、
mysql -uroot -pcomplexpassword -h 0.0.0.0 -P 8002
とする -
docker exec -it [name of container] bash
を用いてコンテナーに入り、コンテナーの中でmysql
をタイプする。 - 次章でお見せする通り、
npm library mysql
を用いて、アプリから接続する
3つめの選択にフォーカスして、アプリからデータベースに接続します。データベースとアプリは異なるコンテナーに存在します。どのように接続するのでしょう?その答えは:
- 同じ Network にいる必要がある 二つのコンテナーがお互い話せるようにするには、同じ Network にいる必要があります。
- データベースが Ready 状態である必要がある データベースの起動には時間がかかるし、アプリがデータベースと話せるようになるためには、データベースを正常に起動する必要があります。私が方法を見つけるまで、これは楽しく/興味深く/ツライことでした。だから安心して。僕らは成功します
-
connection object を作る
product-service
のapp.js
で coonection object をセットアップします。
最初のアイテムから始めましょう。どのようにデータベースとコンテナーを同じネットワークに入れるのでしょう。簡単です。Network を作って、それぞれのコンテナーを同じネットワークに配置します。docker-compose.yaml
の中を見てみましょう:
# excerpt from docker-compose.yaml
networks:
products:
各サービスにネットワークをアサインします。こんな風に:
# excerpt from docker-compose.yaml
services:
some-service:
networks:
- products
今や第二の弾丸ポイントです。データベースが初期化処理を終わっているとどうやって分かるのでしょう?ええっと、depends_on
というプロパティがあります。あるコンテナーが他のコンテナーが初期化処理を終えるまで待つことを設定できます。こんな風に設定します:
# excerpt from docker-compose.yaml
services:
some-service:
depends_on: db
db:
image: mysql
すばらしい、解決した?いやいやいや、落ち着いてくれ。
Docker Compose ver.2 では、サービス ヘルスを確認する代替案が使われます。もし良いヘルスなら、コンテナーを起動します。こんな感じです:
depends_on:
db:
condition: service_healthy
これは、データベースが完全に初期化するまで待つことを意味します。しかし、これが最後ではありません。ver.3 では、このオプションはなくなりました。ここに、それがなぜなのかについて書かれた doc ページがあります → control startup and shutdown order。要点は、データベースが実行中で接続準備可能であることを確認することは我々の責任だということです。Docker はこの件について、いくつかスクリプトを提案しています。
これら全てのスクリプトが、特定の host
と port
を監視し、返信があり次第アプリを起動する、という動作をします。そのように動作するためには何をする必要があるでしょう?wait-for-it
というスクリプトを選択し、やるべきことをリストしましょう:
- コピー スクリプトをサービス コンテナーに配置します。
- 実行権限 スクリプトに実行権限を付与する。
- 設定 docker ファイルに、スクリプトが実行できえるよう、データベース ホストとポートを設定し、スクリプトが OK のとき一度サービスが実行するように設定する。
GitHub からスクリプトをコピーして、product-service
ディレクトリに入れるところから始めましょう。こんな感じに見えるでしょう:
/product-service
wait-for-it.sh
Dockerfile
app.js
package.json
Dockerfile を開いて、以下を追加しましょう。
FROM node:latest
WORKDIR /app
ENV PORT=3000
COPY . .
RUN npm install
EXPOSE $PORT
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
上記は、wait-for-it.sh
ファイルをコンテナーにコピーして、下の行で実行権限を与えています。注目するべき点は、Dockerfile から ENTRYPOINT も削除されていることです。代わりに、コンテナーに docker-compose.yaml
ファイルからスタートを指示します。
# excerpt from docker-compose.yaml
services:
product-service:
command: ["/wait-for-it.sh", "db:8002", "--", "npm", "start"]
db:
# definition of db service below
上記で、wait-for-it.sh ファイルを実行するようにし、db:8002
をパラメータに与え、期待するレスポンスがあった後に、サービスがスタートアップするよう、npm start
が実行されています。素晴らしいものに見えますね、上手く動くでしょうか?
全部を公開するために、フルバージョンの docker-compose.yaml
を見ましょう:
version: '3.3'
services:
product-service:
depends_on:
- "db"
build:
context: ./product-service
command: ["/wait-for-it.sh", "db:8002", "--", "npm", "start"]
ports:
- "8000:3000"
environment:
- test=testvalue
- DATABASE_PASSWORD=complexpassword
- DATABASE_HOST=db
volumes:
- type: bind
source: ./product-service
target: /app
networks:
- products
db:
build: ./product-db
restart: always
environment:
- "MYSQL_ROOT_PASSWORD=complexpassword"
- "MYSQL_DATABASE=Products"
ports:
- "8002:3306"
networks:
- products
inventory-service:
build:
context: ./inventory-service
ports:
- "8001:3000"
volumes:
- my-volume:/var/lib/data
volumes:
my-volume:
networks:
products:
OK、復習しましょう。product-service
と db
が Network products
に配置されていて、wait-for-it.sh
スクリプトがダウンロードされ、アプリが起動する前に実行され、データベースの実行準備が整い次第、反応するようにデータベースのホストとポートを監視します。やることが一つ残っています。product-service
の app.js
ファイルを調整します。ファイルを開きましょう:
const express = require('express')
const mysql = require('mysql');
const app = express()
const port = process.env.PORT || 3000;
const test = process.env.test;
let attempts = 0;
const seconds = 1000;
function connect() {
attempts++;
console.log('password', process.env.DATABASE_PASSWORD);
console.log('host', process.env.DATABASE_HOST);
console.log(`attempting to connect to DB time: ${attempts}`);
const con = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: "root",
password: process.env.DATABASE_PASSWORD,
database: 'Products'
});
con.connect(function (err) {
if (err) {
console.log("Error", err);
setTimeout(connect, 30 * seconds);
} else {
console.log('CONNECTED!');
}
});
conn.on('error', function(err) {
if(err) {
console.log('shit happened :)');
connect()
}
});
}
connect();
app.get('/', (req, res) => res.send(`Hello product service, changed ${test}`))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
引数に object を持つ createConnection()
を呼び出して接続を作る connect()
メソッドが定義されていることが分かると思います。引数には host
、user
、password
、database
が必要です。これは完全に合理的に見えます。setTimeout()
を呼び出して、次の接続まで 30秒間隔をあけるロジックが僅かに connect()
メソッドにあります。今回は wait-for-it.sh
を使うため、この機能は必要ないのですが、接続を得るためにアプリケーション単体でも頼れるようにします。しかし、conn.on('error')
も呼び出す理由は、接続を失う可能性があるからです。お行儀のよいコードにするべきであり、その接続を取り戻すことができるようにする必要があります。
ところで、僕らのパワーですべてを終えたけど、Dockerfile
が変わったことを教えてあげなければいけません。docker-compose build
で全てをリビルドし、その後すべてを実行しましょう:
docker-compose up
すると・・・
ありました。ヒューストン、接続があります。または、私の友人バーニーがこうするみたいに:
訳注
すみません、なんのジョークかさっぱり分かりませんでした・・
##データベースの設定 ―― 構造とデータを与える
OK、もしかすると、service db をどう作るのか考えているかも?docker-compose.yaml
の一部はこんな感じになっています:
db:
build: ./product-db
restart: always
environment:
- "MYSQL_ROOT_PASSWORD=complexpassword"
- "MYSQL_DATABASE=Products"
ports:
- "8002:3306"
networks:
- products
特に build を見て欲しい。この記事の最初で、データベースの Ready-made イメージを引っ張ってこれると説明しました。このステートメントは正しいですが、この Dockerfile を作ることによって、データベースを指定するだけでなく、データベースの構造とシード データを作るといったコマンドを実行することもできるのです。product-db
ディレクトリを見てみましょう:
/product-db
Dockerfile
init.sql
OK、Dockerfile
があります。最初にそれをみてみましょう:
FROM mysql:5.6
ADD init.sql /docker-entrypoint-initdb.d
init.sql
がコピーされ、docker-entroypoint-initdb.d
にリネームすることを設定しています。これは最初に実行されます。素晴らしい、init.sql
の中身はどうでしょうか:
CREATE DATABASE IF NOT EXISTS Products;
# create tables here
# add seed data inserts here
今回、多くを持っていませんが、絶対拡張できることが分かると思います。それが重要です。
#サマリー
このシリーズについて一周まわって、最初からすべてを説明しました。基本的な概念、主要コマンド、Volume とデータベースの扱い方、そして、Docker Compose のより効果的に使う方法などです。このシリーズは今後も継続して Docker の世界へ深く深く進んでいきますが、少しでもお役に立てれば幸いです。ここまで読んでくれてありがとうございました。