アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Docker を使ってどんどんステージング環境を作る方法

こんにちは、tahara です。

Docker を使って git push をトリガーにステージング環境をどんどんたてて開発しています。 いま見たら24面のステージング環境が動いていました。

新しいブランチを push すると Jenkins が Docker のコンテナを作りそこにデプロイしてくれます。 Jenknis では Git Pluign を使っています。 あとは shell script でガシガシとドロくさくやっています。

次がその shell script です。

#!/bin/bash

export SSH_DOCKER="ssh user@docker.example.com"

source ./config/jenkins/functions.sh

# 20 は /etc/init.d/skype の XSERVERNUM=20
source `ls ~/.dbus/session-bus/*-20`
export DBUS_SESSION_BUS_ADDRESS

# bundler
rvm_path=/home/user/.rvm /home/user/.rvm/bin/rvm-shell 'ruby-2.1.0' -c 'bundle install --path vendor/bundler'

# https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin
echo "GIT_COMMIT=$GIT_COMMIT"
echo "GIT_BRANCH=$GIT_BRANCH"
echo "GIT_PREVIOUS_COMMIT=$GIT_PREVIOUS_COMMIT"
echo "GIT_URL=$GIT_URL"
echo "GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL"
echo "GIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL"


# まずいらないコンテナを削除する
delete_unused_containers


BRANCH=`echo $GIT_BRANCH | sed -e 's/^origin\///'`
echo $BRANCH

CONTAINER_NAME="d`echo $BRANCH | sed -e 's/^d\///;s/[-_\/].*//'`"
echo "CONTAINER_NAME=$CONTAINER_NAME"

HOST_NAME="${CONTAINER_NAME}.o.example.com"
echo "HOST_NAME=${HOST_NAME}"


if ${SSH_DOCKER} docker.io ps | grep "$CONTAINER_NAME *$"
then
    echo "container is already exists."
    CONTAINER_ID=`${SSH_DOCKER} docker.io ps | grep "$CONTAINER_NAME *$" | awk '{print $1;}'`
    CONTAINER_IP=$(${SSH_DOCKER} "docker.io inspect --format='{{ .NetworkSettings.IPAddress }}{% end raw %}' ${CONTAINER_ID}")
else
    echo "create container..."
    CONTAINER_ID=$(${SSH_DOCKER} docker.io run -d -t --name "${CONTAINER_NAME}" localhost:5000/outing)
    CONTAINER_IP=$(${SSH_DOCKER} "docker.io inspect --format='{% raw %}{{ .NetworkSettings.IPAddress }}' ${CONTAINER_ID}")

    ${SSH_DOCKER} ssh-keygen -f "/home/user/.ssh/known_hosts" -R ${CONTAINER_IP}

    echo "setup nginx..."
    sed -e "s/_server_name_/${CONTAINER_NAME}.o.example.com/;s/_container_ip_/${CONTAINER_IP}/" config/server/staging/docker/nginx-site.conf | ${SSH_DOCKER} tee /etc/nginx/conf.d/${CONTAINER_NAME}.conf
    ${SSH_DOCKER} sudo service nginx reload
fi

echo "CONTAINER_ID=$CONTAINER_ID"
echo "CONTAINER_IP=$CONTAINER_IP"

# cap
rvm_path=/home/user/.rvm /home/user/.rvm/bin/rvm-shell 'ruby-2.1.0' -c "bundle exec cap docker deploy:migrations -s host_ip=$CONTAINER_IP -s host_name=${HOST_NAME} -s branch=$BRANCH"

if [ $? -ne 0 ] ; then
    sbcl --script /var/lib/jenkins/skype/skype.lisp ";( ごめんなさい、エラーになっちゃいました。 https://ci.example.com/job/outing_docker_gitlab/ から確認してください。 (heidy)"
    exit 1
else
    sbcl --script /var/lib/jenkins/skype/skype.lisp "(ninja) Capistrano: いこーよを 確認(ステージング) 環境 http://${HOST_NAME}/ にデプロイしました (h)`git log --pretty='%n%s%n%b  %an' HEAD...HEAD~ | head -n 10`"
fi

Jenkins と Docker は別のマシンで動いているので ssh 経由でいろいろやっている感じです。

functions.sh

#!/bin/bash

error_exit() {
    ssh deployer@tosa.actindi.net sbcl --script /var/lib/jenkins/skype/skype.lisp ";( ごめんなさい、エラーになっちゃいました。 https://ci.actindi.net/job/outing_master/ から確認してください。 (heidy)"
    exit 1
}

# いらないコンテナを削除する
delete_unused_containers() {
    for container_id in `${SSH_DOCKER} docker.io ps -q`
    do
        name=`${SSH_DOCKER} "docker.io inspect --format '{{ .Name }}' ${container_id}"`
        if echo ${name} | grep -q -E "^/d[0-9]+"
        then
            branch=`echo ${name} | sed -e 's|/d||'`
            if git branch -a --no-merged origin/master | grep -q "d/${branch}"
            then
                echo "${name} is alive."
            else
                echo "kill ${name} ${container_id}"
                ${SSH_DOCKER} docker.io kill ${container_id}
                ${SSH_DOCKER} docker.io rm ${container_id}

                ${SSH_DOCKER} rm -f /etc/nginx/conf.d/d${branch}.conf
                ${SSH_DOCKER} sudo service nginx reload
            fi
        fi
    done
}

master にマージ済みのブランチのコンテナは自動で削除するようにしています。

あと Docker コンテナ内の nginx は外からは見えないので、 次のような nginx の設定ファイルをコンテナごとに自動で作っています。 そして DNS の設定で *.o.example.com を docker.example.com(Docker のホスト) の CNAME にしています。

# HTTP server
server {
    listen 80;
    server_name _server_name_;
    location / {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/conf.d/outing-password;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header Host $host;
        proxy_redirect   off;
        proxy_pass       http://_container_ip_;
    }
}

# HTTPS server
server {
    listen 443;
    server_name _server_name_;

    ssl on;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    ssl_session_timeout 5m;

    ssl_protocols SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;

    location / {

        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/conf.d/outing-password;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect   off;
        proxy_pass       http://_container_ip_;
    }
}

Docker のイメージは日次で本番の最新データをマスクしたもので作り直しているので、ステージングのデータは常に本番に近いものになります。

push するだけでブランチごとのステージングが作られるというのは思っていたより、ずっと快適なことでした。 DB のマイグレーションのあるタスクでも他タスクを気にせず作業できます。 以前は月2回の本番リリースもタスクごとにリリースできるようになりました。 とてもおすすめです。