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

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 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」という文字列を設定しています。

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: insert

CSVファイルから入力したデータを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 件のコメント:

コメントを投稿