今までDockerを使う時はdockerコマンド単体で扱っていたのですが、最近少し複数コンテナを扱いそうな機会が増えてきまして。これを機に、Docker Composeを試してみようかと思います。
Docker Composeを使うと、複数のコンテナをまとめて管理できるのだとか。
早速使ってみます。
参考)
docker-composeを使うと複数コンテナの管理が便利に - Qiita
Docker Compose チュートリアル - ようへいの日々精進XP
インストール
ドキュメントに沿って、まずはインストール。Docker自体は、あらかじめインストールが済んでいるものとします。
なお、当方の環境はUbuntu Linux 14.04 LTSです。
で、ドキュメントのインストールコマンドだと
curl -L https://github.com/docker/compose/releases/download/VERSION_NUM/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
と書いているのですが、「VERSION_NUM」のところは自分で置き換えてあげる必要があります。
どのバージョンを使うかは、こちらを見て選ぶことになるのでは。
https://github.com/docker/compose/releases
今回は、1.4.1を使用します。
また、このコマンドは
/usr/local/bin/docker-compose
というファイルに書き込めと言っているのですが、rootで実行しているわけでもないのでsudo越しになります。
というわけで、結果このようなコマンドになりました。
$ sudo sh -c 'curl -L https://github.com/docker/compose/releases/download/1.4.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose'
権限変更。
$ sudo chmod +x /usr/local/bin/docker-compose
オプションな手順ではありますが、コマンド補完ができるようにbashの設定も追加しておきます。こちらも案内されている手順は
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
ですが、やはりこのままだとroot権限がないので、以下のようにsudo越しになりました。
$ sudo sh -c "curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose"
バージョン確認。
$ docker-compose --version docker-compose version: 1.4.1
以上で、インストールはおしまいです。
アップグレードする時は、削除して再度インストールするみたいです。
動かしてみる
では、Docker Composeを動かしてみます。Docker Composeを使うには、「docker-compose.yml」というYAMLファイルを作成することになるみたいです。
適当なディレクトリを作成して、そちらに移動。
$ mkdir docker-compose-getting-started $ cd docker-compose-getting-started
ドキュメントに載っているDjango、Rails、Wordpressにチャレンジするのもちょっと微妙な気がしたので、ここは自分で書いてみました。
コンテナは、MySQL
https://hub.docker.com/r/library/mysql/
それから簡単なWebアプリケーションとしてGroovyでスクリプトを書いて試すことにします。
https://hub.docker.com/r/webratio/groovy/
今回は、自分でDockerイメージは作りません。docker-compose.ymlを書いて、その中に「build:」があれば以下のコマンドでDockerイメージのビルドが
$ docker-compose build
またDockerイメージのpullは以下のコマンドで行うそうなのですが、今回は使いません。
$ docker-compose pull
で、用意したdocker-compose.ymlはこちら。
docker-compose.yml
web: image: webratio/groovy:2.4.4 ports: - "18080:8080" volumes: - ./web/script:/source - ./web/graperoot:/graperoot links: - mysql command: "rest-server.groovy" mysql: image: mysql:5.6.26 ports: - "3306" environment: MYSQL_ROOT_PASSWORD: my-secret-pw
webと付けた方のコンテナは、後述しますがgroovyコマンドで実行するスクリプト(ここでは「rest-server.groovy」)でサーバーが立ち、ポート8080でリッスンします。これをホスト側の18080ポートにあてています。また、いくつかVOLUMEを設定し(webratio/groovyイメージ参照)、mysqlとリンク設定を入れています。
最後のcommandは、DockerfileのENTRYPOINTの引数となります。
mysqlは、ポート3306を定義していますが、ホスト側の定義を書いていないので、ホスト側の適当なポートにマッピングされるようになります。パスワードは、環境変数で設定。
で、用意したGroovyスクリプトはこちら。
web/script/rest-server.groovy
@Grab('mysql:mysql-connector-java:5.1.36') @GrabConfig(systemClassLoader = true) import groovy.sql.Sql @Grab('org.jboss.resteasy:resteasy-jackson2-provider:3.0.13.Final') @Grab('org.jboss.resteasy:resteasy-jdk-http:3.0.13.Final') import javax.ws.rs.* import javax.ws.rs.core.* import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder import com.sun.net.httpserver.HttpServer class Entry { int id String name } @Path("mysql") class MySqlResource { @GET @Path("select") @Produces(MediaType.APPLICATION_JSON) def select() { def sql = Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver') def result = sql.rows("SELECT * FROM entry ORDER BY id").collect { row -> new Entry([id: row.id, name: row.name]) } sql.close() result } @PUT @Path("insert") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) def insert(Entry entry) { def sql = Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver') sql.execute("INSERT INTO entry(id, name) VALUES(${entry.id}, ${entry.name})") sql.close() } } def server = HttpServer.create(new InetSocketAddress(8080), 10) def contextBuilder = new HttpContextBuilder() [MySqlResource].each { contextBuilder.deployment.actualResourceClasses.add(it) } def context = contextBuilder.bind(server) server.start() println("[${new Date()}] RestEasyJdkHttpd startup[${server.address}].")
作りはかなり適当ですが、ご愛嬌…。
JDBCの接続URLでのホスト名は、Dockerでリンクを貼った物にしています。
Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver')
このスクリプトを、先ほどVOLUMEで定義した「./web/script」配下に置いています。
では、docker-compose.ymlファイルをカレントディレクトリに置いた状態で、「up」。
$ docker-compose up dockercomposegettingstarted_mysql_1 is up-to-date dockercomposegettingstarted_web_1 is up-to-date Attaching to
起動したまま、待ち状態になります。「docker run -d」のようにするには、同じく「-d」オプションを付与します。
いったんCtrl-Cで終了して、コンテナ削除。
$ docker-compose rm
「-d」付きで起動。
$ docker-compose up -d
Creating dockercomposegettingstarted_mysql_1...
Creating dockercomposegettingstarted_web_1...
今度は、端末に制御が戻ってきます。
「docker-compose ps」で、コンテナの一覧が見れます。
$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------ dockercomposegettingstarted_mysql_1 /entrypoint.sh mysqld Up 0.0.0.0:32795->3306/tcp dockercomposegettingstarted_web_1 groovy -Dgrape.root=/grape ... Up 0.0.0.0:18080->8080/tcp
ログを確認するには、「docker-compose logs」。
$ docker-compose logs
停止。
$ docker-compose stop Stopping dockercomposegettingstarted_web_1... done Stopping dockercomposegettingstarted_mysql_1... done
先ほど1度出しましたが、「docker-compose rm」でコンテナ削除。
$ docker-compose rm Going to remove dockercomposegettingstarted_web_1, dockercomposegettingstarted_mysql_1 Are you sure? [yN] y Removing dockercomposegettingstarted_web_1... done Removing dockercomposegettingstarted_mysql_1... done
また、各コマンドは後ろにコンテナ名の指定ができるので、以下のように特定のコンテナのみを起動することも可能です。
$ docker-compose up -d mysql
Creating dockercomposegettingstarted_mysql_1...
複数指定も可。
コンテナ単位に起動していなくても、ログを個別に見れます。
$ docker-compose logs mysql
おお、便利だ!!
動作確認
で、コンテナ操作をしただけで、上に乗っているものを何も動かしていないので、ちょっと動かしてみます。
まずは起動。
$ docker-compose up -d
Starting dockercomposegettingstarted_mysql_1...
Creating dockercomposegettingstarted_web_1...
MySQLにテーブルなどが何もないので、コンテナに入って作成。
$ docker exec -it dockercomposegettingstarted_mysql_1 /bin/bash
データベース、テーブル作成。
# mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 2 Server version: 5.6.26 MySQL Community Server (GPL) Copyright (c) 2000, 2015, 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> CREATE DATABASE test; Query OK, 1 row affected (0.05 sec) mysql> USE test; Database changed mysql> CREATE TABLE entry(id INT, name VARCHAR(255), PRIMARY KEY(id)); Query OK, 0 rows affected (0.06 sec)
ホスト側から確認してみます。
$ curl http://localhost:18080/mysql/select
{}
とりあえず、エラーにはなりません、と。
データ登録。
$ curl -XPUT -H 'Content-Type: application/json' http://localhost:18080/mysql/insert -d '{ "id": 1, "name": "Taro" }' $ curl -XPUT -H 'Content-Type: application/json' http://localhost:18080/mysql/insert -d '{ "id": 2, "name": "Jiro" }'
確認。
$ curl http://localhost:18080/mysql/select [{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]
OKそうです。
インスタンス数を増やしてみる
Docker Composeでは、scaleコマンドで実行中のインスタンスを増やすことができるようです。
webを3つにしています。
$ docker-compose scale web=3 The "web" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash. Creating and starting 2... error Creating and starting 3... error ERROR: for 2 Cannot start container bb19e6f7acab86eaad22eafe51c3ba39e25f1d525c796f276a07a1053ee98afa: Bind for 0.0.0.0:18080 failed: port is already allocated ERROR: for 3 Cannot start container 6bab879c70bcb455e5d48329d5d3ba00141b9df456b7358a1a584cbf05d35d80: Bind for 0.0.0.0:18080 failed: port is already allocated Removing dockercomposegettingstarted_web_2... done Removing dockercomposegettingstarted_web_3... done
まあ、ホスト側に18080でバインドしようとするので、失敗しますよね…。
いったん停止。
$ docker-compose stop Stopping dockercomposegettingstarted_web_1... done Stopping dockercomposegettingstarted_mysql_1... done
仕方がないので、web側のポートをいったんホスト側で固定するのをやめます。
web: image: webratio/groovy:2.4.4 ports: - "8080"
再度起動。
$ docker-compose up -d
Starting dockercomposegettingstarted_mysql_1...
Recreating dockercomposegettingstarted_web_1...
web側も適当なポートに割り当てられました。
$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------ dockercomposegettingstarted_mysql_1 /entrypoint.sh mysqld Up 0.0.0.0:32800->3306/tcp dockercomposegettingstarted_web_1 groovy -Dgrape.root=/grape ... Up 0.0.0.0:32801->8080/tcp
webを3つにしてみます。
$ docker-compose scale web=3 Creating and starting 2... done Creating and starting 3... done
確認。
## ポート確認 $ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------ dockercomposegettingstarted_mysql_1 /entrypoint.sh mysqld Up 0.0.0.0:32800->3306/tcp dockercomposegettingstarted_web_1 groovy -Dgrape.root=/grape ... Up 0.0.0.0:32801->8080/tcp dockercomposegettingstarted_web_2 groovy -Dgrape.root=/grape ... Up 0.0.0.0:32802->8080/tcp dockercomposegettingstarted_web_3 groovy -Dgrape.root=/grape ... Up 0.0.0.0:32803->8080/tcp ## web#1 $ curl http://localhost:32801/mysql/select [{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}] ## web#2 $ curl http://localhost:32802/mysql/select [{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}] ## web#3 $ curl http://localhost:32803/mysql/select [{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]
できてそうです。
なるほど、確かにこれは便利そうですね。覚えておきましょう。