「Docker ComposeでMySQLを使う」ではDocker Composeを使ってJobSchedulerからMySQLをそれぞれ別のDockerコンテナ上で動作させ連携して使用しました。
テストなどでMySQL公式Dockerコンテナを使う際の問題点として、データ投入があります。事前に用意したデータをMySQLに投入後に、テスト対象のアプリケーションが起動されると理想的なのですが、MySQL公式Dockerイメージではデータ投入する機能は提供されていません。
また、MySQL公式DockerイメージではMySQLの起動完了を待ち合わせる機能を持っていないことも問題です。
これらの問題に対応するためMySQL公式Dockerイメージを元に、マイDockerイメージであるmysql-java-embulk-dockerを作ってみました。
mysql-java-embulk-docker
mysql-java-embulk-dockerはdocker-composeを使ってアプリケーションDockerイメージのテストを行う際に、事前にデータ投入したMySQLデータベースを提供するためのDockerイメージです。
GitHubにソースコードがありますので、詳細はこちらを参照して下さい。
以下では、実装上のポイントと使い方について説明します。
Dockerfile
mysql-java-embulk-dockerのDockerfileは以下になります。
FROM mysql:5.6 MAINTAINER asami RUN apt-get update && apt-get -y install wget curl # Install JDK 1.7 RUN cd /opt; wget --no-cookies --no-check-certificate --header "Cookie: oraclelicense=accept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/7u51-b13/jdk-7u51-linux-x64.tar.gz" -O /opt/jdk-7-linux-x64.tar.gz # Install in /usr/java/jdk1.7.0_51 RUN mkdir /usr/java && (cd /usr/java; tar xzf /opt/jdk-7-linux-x64.tar.gz) RUN rm /opt/jdk-7-linux-x64.tar.gz RUN update-alternatives --install /usr/bin/java java /usr/java/jdk1.7.0_51/jre/bin/java 20000; update-alternatives --install /usr/bin/jar jar /usr/java/jdk1.7.0_51/bin/jar 20000; update-alternatives --install /usr/bin/javac javac /usr/java/jdk1.7.0_51/bin/javac 20000; update-alternatives --install /usr/bin/javaws javaws /usr/java/jdk1.7.0_51/jre/bin/javaws 20000; update-alternatives --set java /usr/java/jdk1.7.0_51/jre/bin/java; update-alternatives --set javaws /usr/java/jdk1.7.0_51/jre/bin/javaws; update-alternatives --set javac /usr/java/jdk1.7.0_51/bin/javac; update-alternatives --set jar /usr/java/jdk1.7.0_51/bin/jar; RUN curl --create-dirs -o /opt/embulk -L "http://dl.embulk.org/embulk-latest.jar" && chmod +x /opt/embulk RUN /opt/embulk gem install embulk-output-mysql RUN apt-get -y install redis-server COPY charset.cnf /etc/mysql/conf.d/charset.cnf COPY entrypoint.sh /opt/entrypoint.sh RUN chmod +x /opt/entrypoint.sh VOLUME ["/var/lib/mysql", "/etc/mysql/conf.d", "/opt/setup.d"] ENTRYPOINT ["/opt/entrypoint.sh"] CMD ["mysqld"]
Embulk
Embulkはビッグデータスケールのデータローダです。
データ投入にEmbulkを利用できると大変便利なので組み込んでみました。
EmbulkはJava VM上で動作するので、Embulk用にJDKをインストールしています。
また、MySQLにデータ投入するので、Embulkにembulk-output-mysqlプラグインを追加しています。
Redis
Docker Compose上でmysql-java-embulk-dockerコンテナの起動時にデータ投入をする際に問題点として、mysql-java-embulk-dockerコンテナの起動とアプリケーションコンテナの起動の同期が行われないというものがあります。
mysql-java-embulk-dockerコンテナの起動とアプリケーションコンテナの起動が同時に行われてしまうために、mysql-java-embulk-dockerコンテナの起動時に行われるデータ投入が完了する前に、アプリケーションコンテナが動き出してしまい、想定したデータがない状態なので誤動作する、という問題です。
現段階ではDocker Composeにはこの問題を解決するための機能は提供されていないようなので、Redisを使って対応することにしました。
この目的でRedisをインストールしています。
本来はRedisのクライアントのみがインストールできればよいのですが、簡単にできるよい方法がみつからなかったのでRedisをまるごとインストールしています。
charset.cnf
MySQLで日本語を使うための設定として、サーバーのコード系をUTF-8に設定します。
この目的で以下のchaset.cnfを用意して、Dockerコンテナの/etc/mysql/conf.d/charset.cnfにCOPYします。
[mysqld] character-set-server = utf8
entrypoint.sh
mysql-java-embulk-dockerのentrypoint.shは以下になります。
#!/bin/bash
# WAIT_DB_TIMER
# WAIT_CONTAINER_KEY
# set -x
set -e
echo Wait contaner key: ${WAIT_CONTAINER_KEY:=mysql-java-embulk-docker}
echo Redis host: ${REDIS_SERVER_HOST:=$REDIS_PORT_6379_TCP_ADDR}
echo Redis port: ${REDIS_SERVER_PORT:=$REDIS_PORT_6379_TCP_PORT}
function check_db {
if [ "$MYSQL_ROOT_PASSWORD" ]; then
mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e "status"
elif [ "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then
mysql -e "status"
else
exit 1
fi
}
function wait_db {
result=1
for i in $(seq 1 ${WAIT_DB_TIMER:-10})
do
sleep 1s
result=0
check_db && break
result=1
done
if [ $result = 1 ]; then
exit 1
fi
}
if [ "${1:0:1}" = '-' ]; then
set -- mysqld "$@"
fi
is_install=false
if [ "$1" = 'mysqld' ]; then
# read DATADIR from the MySQL config
DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"
if [ ! -d "$DATADIR/mysql" ]; then
if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then
echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
exit 1
fi
is_install=true
echo 'Running mysql_install_db ...'
mysql_install_db --datadir="$DATADIR"
echo 'Finished mysql_install_db'
# These statements _must_ be on individual lines, and _must_ end with
# semicolons (no line breaks or comments are permitted).
# TODO proper SQL escaping on ALL the things D:
tempSqlFile='/tmp/mysql-first-time.sql'
cat > "$tempSqlFile" <<-EOSQL
DELETE FROM mysql.user ;
CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
DROP DATABASE IF EXISTS test ;
EOSQL
if [ "$MYSQL_DATABASE" ]; then
echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile"
fi
if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" >> "$tempSqlFile"
if [ "$MYSQL_DATABASE" ]; then
echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" >> "$tempSqlFile"
fi
fi
echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile"
# http://qiita.com/toritori0318/items/242274d4f5794e2f68e5
# setup
echo "use $MYSQL_DATABASE;" >> "$tempSqlFile"
if [ -e "/opt/setup.d/setup.sql"]; then
cat /opt/setup.d/setup.sql >> "$tempSqlFile"
fi
# start mysql
set -- "$@" --init-file="$tempSqlFile"
fi
chown -R mysql:mysql "$DATADIR"
fi
exec "$@" &
wait_db
if [ -e "/opt/setup.d/setup.yml" ]; then
if [ $is_install=true ]; then
echo "embulk run setup.yml"
cd /opt/setup.d && /opt/embulk run setup.yml
fi
fi
if [ -n "$REDIS_SERVER_HOST" ]; then
redis-cli -h $REDIS_SERVER_HOST -p $REDIS_SERVER_PORT SET $WAIT_CONTAINER_KEY up
fi
sleep infinityMySQL公式をベースに、データ投入用SQLおよびEmbulkでデータ投入するように拡張したものです。
setup.sql
/opt/setup.d/setup.sqlとしてデータ投入用SQLが存在する場合は、MySQLの初期起動スクリプトにこの内容を追加することで、起動時にデータ投入されるようになっています。
/opt/setup.dはDockerfileでVolumeなっていて、外部からディレクトリをマウントして使用することを想定しています。
データ投入用SQLは「Docker公式のmysqlイメージを使いつつ初期データも投入する」の記事を参考にしました。
setup.yml
/opt/setup.d/setup.ymlとしてデータ投入用Embulk記述ファイルが存在する場合は、Embulkを使ってデータ投入するようになっています。
ただし、MySQLの起動が完了した後でないとデータ投入ができないのでmysqlコマンドを使って待ち合わせ処理を行っています。
Redisによる同期
mysql-java-embulk-dockerコンテナの起動終了の待ち合わせのためRedisを使用します。
具体的には以下のように、外部コンテナでRedisが起動されている場合に、redis-cliコマンドを使って環境変数WAIT_CONTAINER_KEYで指定されたスロットに「up」という文字列を設定しています。
if [ -n "$REDIS_SERVER_HOST" ]; then
redis-cli -h $REDIS_SERVER_HOST -p $REDIS_SERVER_PORT SET $WAIT_CONTAINER_KEY up
fiアプリケーション側は、Redisのこのスロットがupになるまでポーリングで待ち合わせることで同期を取ることになります。
終了抑止
最後にDockerでサービスを記述する時のお約束としてsleepコマンドで終了抑止を行っています。
Docker Hub
Docker HubはGitHubやBitBucketと連動した自動ビルド機能を提供しています。
mysql-java-embulk-dockerもこの設定を行っているので、以下の場所にDockerイメージが自動ビルドされます。
このイメージは「asami/mysql-java-embulk-docker」という名前で利用することができます。
サンプル
Dockerイメージ「asami/mysql-java-embulk-docker」を利用してテストデータの投入を行うサンプルを作ってみます。
サンプルのコードはGitHubのmysql-java-embulk-dockerのsampleディレクトリにあるので、詳細はこちらを参照して下さい。
docker-compose.yml
サンプルプログラムのdocker-compose.ymlは以下になります。
app:
build: .
links:
- mysql
- redis
environment:
WAIT_CONTAINER_KEY: mysql-java-embulk-docker
MYSQL_SERVER_USER: baseball
MYSQL_SERVER_PASSWORD: baseball
mysql:
image: asami/mysql-java-embulk-docker
links:
- redis
ports:
- ":3306"
volumes:
- setup.d:/opt/setup.d
environment:
MYSQL_USER: baseball
MYSQL_PASSWORD: baseball
MYSQL_ROOT_PASSWORD: baseball
MYSQL_DATABASE: baseball
redis:
image: redis
ports:
- ":6379"setup.dをコンテナの/opt/setup.dにマウントしています。
setup.dには後述のsetup.ymlとデータファイルBatting.csvが格納されています。
Batting.csvは以下のサイトからデータを取得しました。
setup.yml
setup.ymlはembulkで移入するデータの情報を記述したものです。
in:
type: file
path_prefix: Batting.csv
parser:
charset: UTF-8
newline: CRLF
type: csv
delimiter: ','
quote: '"'
escape: ''
skip_header_lines: 1
columns:
- {name: playerID, type: string}
- {name: yearID, type: long}
- {name: stint, type: long}
- {name: teamID, type: string}
- {name: lgID, type: string}
- {name: G, type: long}
- {name: AB, type: long}
- {name: R, type: long}
- {name: H, type: long}
- {name: 2B, type: long}
- {name: 3B, type: long}
- {name: HR, type: long}
- {name: RBI, type: long}
- {name: SB, type: long}
- {name: CS, type: long}
- {name: BB, type: long}
- {name: SO, type: long}
- {name: IBB, type: long}
- {name: HBP, type: long}
- {name: SH, type: long}
- {name: SF, type: long}
- {name: GIDP, type: long}
exec: {}
out:
type: mysql
host: localhost
database: baseball
user: baseball
password: baseball
table: batting
mode: insertCSVファイルから入力したデータをMySQLに投入する際の標準的な指定と思います。
Dockerfile
サンプルアプリケーションのDockerfileは以下になります。
FROM mysql MAINTAINER asami ENV MYSQL_ALLOW_EMPTY_PASSWORD true RUN apt-get update && apt-get -y install redis-server COPY app.sh /opt/app.sh RUN chmod +x /opt/app.sh ADD https://raw.githubusercontent.com/asami/mysql-java-embulk-docker/master/lib/mysql-java-embulk-docker-lib.sh /opt/mysql-java-embulk-docker-lib.sh ENTRYPOINT /opt/app.sh
アプリケーションでmysqlコマンドを使うので、MySQL公式Dockerイメージをベースにしました。
mysql-java-embulk-dockerコンテナとの同期にRedisを使うのでRedisをインストールしています。
また、アプリケーション起動シェルの共通ライブラリmysql-java-embulk-docker-lib.shをGitHubからコンテナ内にコピーしています。
アプリケーション起動の動きは大きく以下の3つの部分に分かれます。
- パラメタの取り込み
- データ投入の待ち合わせ
- アプリケーションロジック
この中の「パラメタの取り込み」と「データ投入の待ち合わせ」をmysql-java-embulk-docker-lib.shが行います。
app.sh
サンプルアプリケーションの実行スクリプトapp.shは以下になります。
#! /bin/bash
# set -x
set -e
DIR="${BASH_SOURCE%/*}"
if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
source $DIR/mysql-java-embulk-docker-lib.sh
mysql -u $MYSQL_SERVER_USER -p$MYSQL_SERVER_PASSWORD --host=$MYSQL_SERVER_HOST --port=$MYSQL_SERVER_PORT -e "select count(*) from baseball.batting"まず、共通ライブラリmysql-java-embulk-docker-lib.shをsourceで取り込んでいます。
この中でRedisを使った同期が行われ、Embulkによるデータ投入が完了した状態でアプリケーションロジックに入ってきます。
今回のアプリケーションロジックは非常に単純で以下の処理を行います。
- baseball.battingテーブルの総レコード数を取得する
この問合せ処理をmysqlコマンドを使って行っています。
実行
docker-composeのbuildコマンドでビルドします。
$ docker-compose build
docker-composeのupコマンドでビルドします。
$ docker-compose up
動作過程がコンソールに出力されますが、最後の方で以下のような出力があります。
app_1 | count(*) app_1 | 99846
無事、Batting.csvをMySQLのbaseball.battingテーブルに投入した後、baseball.battingテーブルの総レコード数を取得することができました。
まとめ
SQLとEmbulkを使ってデータ投入できるMySQL用のDockerコンテナを作ってみました。
アプリケーション開発では、テスト用データベースの準備とデータ投入が大きな手間であり、テスト自動化の障壁にもなっていたので、今回開発したDockerイメージをアプリケーション開発に適用していきたいと思います。
それにしても、Dockerのイメージ開発はシェルスクリプトプログラミングということを実感しました。相当錆び付いていましたが、なんとか動くものができました。
諸元
- Mac OS 10.7.5
- docker 1.6
- docker-compose 1.2.0

0 件のコメント:
コメントを投稿