Processing math: 25%

2015年5月7日木曜日

[docker] Docker Composeでデータ投入

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は以下になります。

  1. FROM mysql:5.6  
  2. MAINTAINER asami  
  3.   
  4. RUN apt-get update && apt-get -y install wget curl  
  5.   
  6. # Install JDK 1.7  
  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  
  8.   
  9. # Install in /usr/java/jdk1.7.0_51   
  10. RUN mkdir /usr/java && (cd /usr/java; tar xzf /opt/jdk-7-linux-x64.tar.gz)  
  11. RUN rm /opt/jdk-7-linux-x64.tar.gz  
  12. 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;  
  13.   
  14. RUN curl --create-dirs -o /opt/embulk -L "http://dl.embulk.org/embulk-latest.jar" && chmod +x /opt/embulk  
  15.   
  16. RUN /opt/embulk gem install embulk-output-mysql  
  17.   
  18. RUN apt-get -y install redis-server  
  19.   
  20. COPY charset.cnf /etc/mysql/conf.d/charset.cnf  
  21. COPY entrypoint.sh /opt/entrypoint.sh  
  22. RUN chmod +x /opt/entrypoint.sh  
  23.   
  24. VOLUME ["/var/lib/mysql""/etc/mysql/conf.d""/opt/setup.d"]  
  25.   
  26. ENTRYPOINT ["/opt/entrypoint.sh"]  
  27.   
  28. 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します。

  1. [mysqld]  
  2. character-set-server = utf8  
entrypoint.sh

mysql-java-embulk-dockerのentrypoint.shは以下になります。

  1. #!/bin/bash  
  2.   
  3. # WAIT_DB_TIMER  
  4. # WAIT_CONTAINER_KEY  
  5.   
  6. # set -x  
  7.   
  8. set -e  
  9.   
  10. echo Wait contaner key: ${WAIT_CONTAINER_KEY:=mysql-java-embulk-docker}  
  11. echo Redis host: {REDIS_SERVER_HOST:=REDIS_PORT_6379_TCP_ADDR}  
  12. echo Redis port: {REDIS_SERVER_PORT:=REDIS_PORT_6379_TCP_PORT}  
  13.   
  14. function check_db {  
  15.     if [ "$MYSQL_ROOT_PASSWORD" ]; then  
  16.  mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e "status"  
  17.     elif [ "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then  
  18.  mysql -e "status"  
  19.     else  
  20.  exit 1  
  21.     fi  
  22. }  
  23.   
  24. function wait_db {  
  25.     result=1  
  26.     for i in $(seq 1 ${WAIT_DB_TIMER:-10})  
  27.     do  
  28.  sleep 1s  
  29.  result=0  
  30.  check_db && break  
  31.  result=1  
  32.     done  
  33.     if [ $result = 1 ]; then  
  34.  exit 1  
  35.     fi  
  36. }  
  37.   
  38. if [ "${1:0:1}" = '-' ]; then  
  39.     set -- mysqld "$@"  
  40. fi  
  41.   
  42. is_install=false  
  43.   
  44. if [ "$1" = 'mysqld' ]; then  
  45.     # read DATADIR from the MySQL config  
  46.     DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"  
  47.   
  48.     if [ ! -d "$DATADIR/mysql" ]; then  
  49.         if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then  
  50.             echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'  
  51.             echo >&2 '  Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'  
  52.             exit 1  
  53.         fi  
  54.   
  55.  is_install=true  
  56.   
  57.         echo 'Running mysql_install_db ...'  
  58.         mysql_install_db --datadir="$DATADIR"  
  59.         echo 'Finished mysql_install_db'  
  60.   
  61.         # These statements _must_ be on individual lines, and _must_ end with  
  62.         # semicolons (no line breaks or comments are permitted).  
  63.         # TODO proper SQL escaping on ALL the things D:  
  64.   
  65.         tempSqlFile='/tmp/mysql-first-time.sql'  
  66.         cat > "$tempSqlFile" <<-EOSQL  
  67.             DELETE FROM mysql.user ;  
  68.             CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;  
  69.             GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;  
  70.             DROP DATABASE IF EXISTS test ;  
  71. EOSQL  
  72.   
  73.         if [ "$MYSQL_DATABASE" ]; then  
  74.             echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile"  
  75.         fi  
  76.   
  77.         if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then  
  78.             echo "CREATE USER 'MYSQL_USER'@'%' IDENTIFIED BY 'MYSQL_PASSWORD' ;" >> "$tempSqlFile"  
  79.   
  80.             if [ "$MYSQL_DATABASE" ]; then  
  81.                 echo "GRANT ALL ON \`MYSQL_DATABASE\`.* TO 'MYSQL_USER'@'%' ;" >> "$tempSqlFile"  
  82.             fi  
  83.         fi  
  84.   
  85.         echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile"  
  86.   
  87.  # http://qiita.com/toritori0318/items/242274d4f5794e2f68e5  
  88.         # setup  
  89.         echo "use $MYSQL_DATABASE;" >> "$tempSqlFile"  
  90.  if [ -e "/opt/setup.d/setup.sql"]; then  
  91.             cat /opt/setup.d/setup.sql >> "$tempSqlFile"  
  92.  fi  
  93.         # start mysql  
  94.         set -- "$@" --init-file="$tempSqlFile"  
  95.     fi  
  96.   
  97.     chown -R mysql:mysql "$DATADIR"  
  98. fi  
  99.   
  100. exec "$@" &  
  101.   
  102. wait_db  
  103.   
  104. if [ -e "/opt/setup.d/setup.yml" ]; then  
  105.     if [ $is_install=true ]; then  
  106.  echo "embulk run setup.yml"  
  107.  cd /opt/setup.d && /opt/embulk run setup.yml  
  108.     fi  
  109. fi  
  110.   
  111. if [ -n "$REDIS_SERVER_HOST" ]; then  
  112.     redis-cli -h REDIS_SERVER_HOST -p REDIS_SERVER_PORT SET $WAIT_CONTAINER_KEY up  
  113. fi  
  114.   
  115. sleep infinity  

MySQL公式をベースに、データ投入用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」という文字列を設定しています。

  1. if [ -n "$REDIS_SERVER_HOST" ]; then  
  2.     redis-cli -h REDIS_SERVER_HOST -p REDIS_SERVER_PORT SET $WAIT_CONTAINER_KEY up  
  3. 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は以下になります。

  1. app:  
  2.   build: .  
  3.   links:  
  4.     - mysql  
  5.     - redis  
  6.   environment:  
  7.     WAIT_CONTAINER_KEY: mysql-java-embulk-docker  
  8.     MYSQL_SERVER_USER: baseball  
  9.     MYSQL_SERVER_PASSWORD: baseball  
  10. mysql:  
  11.   image: asami/mysql-java-embulk-docker  
  12.   links:  
  13.     - redis  
  14.   ports:  
  15.     - ":3306"  
  16.   volumes:  
  17.     - setup.d:/opt/setup.d  
  18.   environment:  
  19.     MYSQL_USER: baseball  
  20.     MYSQL_PASSWORD: baseball  
  21.     MYSQL_ROOT_PASSWORD: baseball  
  22.     MYSQL_DATABASE: baseball  
  23. redis:  
  24.   image: redis  
  25.   ports:  
  26.     - ":6379"  

setup.dをコンテナの/opt/setup.dにマウントしています。

setup.dには後述のsetup.ymlとデータファイルBatting.csvが格納されています。

Batting.csvは以下のサイトからデータを取得しました。

setup.yml

setup.ymlはembulkで移入するデータの情報を記述したものです。

  1. in:  
  2.   type: file  
  3.   path_prefix: Batting.csv  
  4.   parser:  
  5.     charset: UTF-8  
  6.     newline: CRLF  
  7.     type: csv  
  8.     delimiter: ','  
  9.     quote: '"'  
  10.     escape: ''  
  11.     skip_header_lines: 1  
  12.     columns:  
  13.     - {name: playerID, type: string}  
  14.     - {name: yearID, type: long}  
  15.     - {name: stint, type: long}  
  16.     - {name: teamID, type: string}  
  17.     - {name: lgID, type: string}  
  18.     - {name: G, type: long}  
  19.     - {name: AB, type: long}  
  20.     - {name: R, type: long}  
  21.     - {name: H, type: long}  
  22.     - {name: 2B, type: long}  
  23.     - {name: 3B, type: long}  
  24.     - {name: HR, type: long}  
  25.     - {name: RBI, type: long}  
  26.     - {name: SB, type: long}  
  27.     - {name: CS, type: long}  
  28.     - {name: BB, type: long}  
  29.     - {name: SO, type: long}  
  30.     - {name: IBB, type: long}  
  31.     - {name: HBP, type: long}  
  32.     - {name: SH, type: long}  
  33.     - {name: SF, type: long}  
  34.     - {name: GIDP, type: long}  
  35. exec: {}  
  36. out:  
  37.   type: mysql  
  38.   host: localhost  
  39.   database: baseball  
  40.   user: baseball  
  41.   password: baseball  
  42.   table: batting  
  43.   mode: insert  

CSVファイルから入力したデータをMySQLに投入する際の標準的な指定と思います。

Dockerfile

サンプルアプリケーションのDockerfileは以下になります。

  1. FROM mysql  
  2. MAINTAINER asami  
  3.   
  4. ENV MYSQL_ALLOW_EMPTY_PASSWORD true  
  5.   
  6. RUN apt-get update && apt-get -y install redis-server  
  7.   
  8. COPY app.sh /opt/app.sh  
  9. RUN chmod +x /opt/app.sh  
  10. 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  
  11.   
  12. 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は以下になります。

  1. #! /bin/bash  
  2.   
  3. # set -x  
  4.   
  5. set -e  
  6.   
  7. DIR="${BASH_SOURCE%/*}"  
  8. if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi  
  9. source $DIR/mysql-java-embulk-docker-lib.sh  
  10.   
  11. mysql -u MYSQL_SERVER_USER -pMYSQL_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 件のコメント:

コメントを投稿