開発環境のDocker化

kanazawa|2015年12月10日
AWS

インフラエンジニアの金澤です。

今回、ランサーズの開発環境をDocker化しましたので、その内容を紹介いたします。

Docker移行の決断

Dockerについては、ここ1年で技術的な調査はしていましたが、決定的なメリットを見出していたわけではなく、採用に踏み切ってはいませんでした。

その一方で、開発規模は拡大し続けており、ランサーズ本体に加え、関連サービスのプロジェクトも並行して進むようになり、開発環境のリソース不足に悩まされるようになってきていました。

ランサーズの開発環境は、主に以下の構成で運用されています。

  • 個人PC
    • Virtualbox + Vagrant でCentOS環境を構築
    • Ansibleでプロビジョニング
  • 社内VMサーバー
    • VMWare 上にCentOS環境を構築
  • AWS検証用環境
    • EC2(CentOS、Amazon Linux)上に構築
    • Ansibleでプロビジョニング

個人PCの開発環境は、VirtualBoxにメモリ2GB、HDD 50GBのVMが稼働しています。
このVM上にランサーズが稼働しているわけですが、関連サービス(例えば、チャットサービス等)のプロジェクトが立ち上がると、その開発環境も必要になります。

関連サービスはランサーズと連動して動作しますが、ミドルウェア構成が異なるため、1つのVMに共存させるにはいろいろ不都合があります。
そのため、別VMを構築することになるのですが、個人PCに2台以上のVMを稼働させると、HDD、メモリ等のリソースが不足し始めてきます。

今回の開発環境のDocker移行は、差し迫った問題である、個人PCのリソース解決手段として導入しました。

Docker移行の方針

開発環境のDocker化は、以下の方針で行ないました。

  • 個人PC上にはDockerMachine(DockerToolbox)、VMWareにはCoreOSで構築する
  • 本番AWS環境のDocker化はしない
  • 極力、本番AWS環境と同じ構成にする
  • 個人でプロビジョニングはしない(コンテナを持ってくるだけ)
  • 開発環境、本番環境共に(ほぼ)同じコードでプロビジョニングできるようにする
  • Vagrant時代の利便性を維持する(共有ディレクトリ、XDebugによるステップ実行など)

「個人でプロビジョニングはしない」方針にしたのは、構築に時間がかかるためです。

Vagrant + Ansibleでプロビジョニングしていたときは、構築(特にMySQLデータのインポート)に時間がかかり、完了まで数時間ほどかかっていました。

そのため、積極的に再構築する人が減り、Ansible Playbookの更新が滞った結果、プロビジョニングエラーが増え、それを補うための手作業が増える悪循環に陥っていました。

また、Windowsの場合、Vagrant、Ansibleのバージョンなどによる相性がシビアで、構築がなかなか成功しないという悩みもありました。

そのため、Docker移行後は、コンテナのプロビジョニングは特定の人が行い、Dockerを使う人は、完成したコンテナをpullすれば構築できる手順にしました。

これらの方針で進めたため、一部、Dockerのベストプラクティスに従っていない方法も採用しています。

例えば、以下のような方法です。

  • Dockerfileだけでなく、Ansibleでもプロビジョニング
  • 1コンテナに2つ以上のサービスを稼働
  • sshdの稼働
  • root以外の一般ユーザーの作成
  • 固定IPの付与
  • MySQLにデータをインポートしたコンテナをそのままRegistryに登録

Dockerのベストプラクティスを実現させるには、リリース方法も踏まえた上で本番環境の構成にも手を入れる必要があります。
今回の目的は開発環境のリソース不足の解決がメインのため、その目的の達成に注力し、上記のような方法も採用しています。

開発エンジニアには、事前に以下のようなイメージを共有してから着手しました。


docker02

Dockerを使った開発環境構成2

 

移行時に直面した問題とその解決方法

コンテナのプロビジョニング

コンテナのプロビジョニングは、主にDockerfileで行いますが、本番環境のEC2で稼働するサービスのコンテナには、共通のAnsibleコードを利用してプロビジョニングします。

AWSのいわゆるマネージド系のサービス(ELB、RDS、Erasticache等)にあたるコンテナはDockerfileのみで構築、EC2で構築するサービスは、Dockerfileでベースを作成した上で、SSH経由でAnsibleのPlaybookを実行しています。

※docker-machineは再起動すると個人で作業したファイルが消えてしまうため、Playbookの実行用にAnsibleをインストールしたコンテナを別途用意しています。

Docker Registory

コンテナを格納するRegistryには、DockerHubなどのサービスがありますが、現状はどのサービスも有料で、サーバーも海外にあるものがほとんどです。

データをインポートしたMySQLのコンテナは数GBになるため、転送速度が遅いと運用に大きな影響を与えます。

そのため、プライベートレジストリを構築し、コンテナはAWS東京リージョンのS3に格納することにしました。

プライベートレジストリ用にサーバーを用意する選択肢もありましたが、Docker Registoryのコンテナが公開されていますので、それを各Docker Machine上で起動しています。

※コンテナをpull、pushするときのみ起動すればよいので、リソース的にはほとんど問題になりません。

Docker Registoryのコンテナ起動時に、以下のオプションを指定することで、格納先をS3に設定しています。

docker run -d \
    --name registry \
    -p 5000:5000 \
    -e REGISTRY_STORAGE_S3_ACCESSKEY=XXXXXXXX \
    -e REGISTRY_STORAGE_S3_SECRETKEY=xxxxxxxx \
    -e REGISTRY_STORAGE_S3_BUCKET=docker-registory-lancers \
    -e REGISTRY_STORAGE_S3_REGION=ap-northeast-1 \
    -e REGISTRY_STORAGE_S3_ROOTDIRECTORY=/ \
    -e REGISTRY_STORAGE=s3 \
registry:2.2

複数サービスの起動

Dockerは、1コンテナ1サービス、サービスの起動はフォアグラウンドが原則ですが、本番環境と同様の構成にするため、sshdを含め、サービスを複数起動しています。

複数サービスを起動するための手法については、様々な方法が紹介されていますが、以下のようなスクリプトで稼働させるのが最もわかりやすく、この方法を採用させていただきました。

service.sh
#!/bin/sh
/sbin/service sshd start
/sbin/service httpd start
 
while true
do
 sleep 10
done

Dockerfileに、起動時に上記のスクリプトを実行するように記述します。

Dockerfile
# service.shをコンテナに配置
COPY ./service.sh /root/
RUN chmod 744 /root/service.sh
 
# 起動時にservice.shを実行
CMD ["/root/service.sh"]

固定IPアドレスの付与

CentOS6の場合は、ip addr add コマンドで固定IPアドレスを付与できます。
docker run、docker start後に、以下のようにdocker execコマンドで付与しています。

DOCKER_ID=$(docker inspect --format="{{.Id}}" $DOCKER_NAME)
IP_STATIC=172.17.51.11
NIC=eth0
docker exec $DOCKER_ID ip addr add $IP_STATIC/16 dev $NIC

※つまり、コンテナ起動時に付与されるIPアドレスと、固定IPアドレスが2つ付与されている状態になります。

既存のIPアドレスの削除も試みましたが、iptablesの設定も必要で手順が煩雑になるためやめました。IPアドレスが衝突する可能性が僅かながらあるのですが、今は割り切って利用しています。

ELBコンテナ

AWSのELBに相当するコンテナを用意しています。

本番AWS環境では、ELB経由でAppサーバーにアクセスしています。
SSL Termination機能を利用しているため、Appサーバー内ではhttpしか処理していません。

その状態をシミュレートするため、ELBコンテナでhttps→http変換を行い、環境変数X-Fowarded-Protoをセットした上で、Appサーバーに処理を渡すように設定しています。

具体的には、以下のようなVirturlhostの設定を行っています。

ProxyAddHeaders Off

<VirtualHost *:80>
    ServerName dev.lancers.jp
    ServerAlias *.lancers.jp
    RequestHeader set X-Forwarded-Proto "http"
    RewriteEngine On
    RewriteCond %{SERVER_NAME} ^(.*).lancers.jp$
    RewriteRule ^/(.*) http://%1.lancers.jp/$1 [P,L,QSA]
</VirtualHost>
<VirtualHost *:443>
    ServerName dev.lancers.jp
    ServerAlias *.lancers.jp
    ErrorLog logs/ssl_error_log
    TransferLog logs/ssl_access_log
    LogLevel warn
    SSLEngine on
    SSLProtocol all -SSLv2
    SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW
    SSLCertificateFile /etc/pki/tls/certs/lancers.jp.cert
    SSLCertificateKeyFile /etc/pki/tls/private/lancers.jp.key
    RequestHeader set X-Forwarded-Proto "https"
    RewriteEngine On
    RewriteCond %{SERVER_NAME} ^(.*).lancers.jp$
    RewriteRule ^/(.*) http://%1.lancers.jp/$1 [P,L,QSA]
</VirtualHost>

また、このELBコンテナは、lancers.jpドメイン以外のサービスも起動した場合に、同じIPアドレスのdocker-machineを利用するための、リバースプロキシの役目も果たしています。

共有ディレクトリ

docker-machineのVMを作成後、VirtualBoxの設定で以下のように共有フォルダーを設定することで、docker-machineのディレクトリとPC上のディレクトリを共有できます。

vb_setting_shared_folderさらに、docker run時に-vオプションを指定することでコンテナとdocker-machineのディレクトリを共有します。

これで、PC上で直接ソースコードの修正が可能となります。

以下は、コンテナの/var/wwwを共有した場合のコンテナ起動例です。

docker run \
--name $DOCKER_NAME \
-v /Users/www:/var/www \
--hostname $DOCKER_NAME \
--privileged \
-d $IMAGE_NAME

※ディレクトリの所有権の関係で、/var/www以下での書き込みは、root権限が必要になる場合があります。

hostsの設定

Dockerfileで/etc/hostsを編集してもコンテナには反映されません。
docker run時に–add-hostオプションを指定することで、hostsを設定できます。

docker run \
--name $DOCKER_NAME \
--hostname $DOCKER_NAME \
--add-host=dev.lancers.jp:172.17.50.11 \
--add-host=dev-img.lancers.jp:172.17.50.11 \
--privileged \
-d $IMAGE_NAME

ポートマッピング

ポートマッピングは、コンテナで起動したhttpのサービスをPC上で確認するために必ず設定することになりますが、MySQLコンテナにも設定しています。

以下のように設定することで、PC上からdocker-macineのIPアドレスで、直接MySQLコンテナのMySQLにアクセスできます。

Dockerfile
EXPOSE 3306

起動時に、-pオプションでポートをマッピング

IMAGE_NAME=mysql:latest
DOCKER_NAME=mysql-51-11
IP_STATIC=172.17.51.11
PORT=3306
 
docker run \
--name $DOCKER_NAME \
--hostname $DOCKER_NAME \
-p $PORT:$PORT \
--privileged \
-d $IMAGE_NAME

Xdebugによるステップ実行

Xdebugをインストールし、php.iniに以下の設定をすることで、PHP Storm等でステップ実行を可能にしています。
(※ステップ実行時に、xdebug.remote_autostart = Onにしてhttpdを再起動します)

php.ini

[xdebug]
zend_extension="/usr/lib64/php/modules/xdebug.so"
xdebug.remote_enable = On
xdebug.remote_port = 9000
;xdebug.remote_autostart = On
xdebug.remote_host = 192.168.99.1
xdebug.profiler_output_dir = "/tmp"
xdebug.max_nesting_level= 1000
xdebug.idekey = "PHPSTORM"

Docker化の効果

開発環境をDockerに移行したことで、複数のVMを起動する必要がなくなり、PCリソースの削減になったことはもちろんですが、加えて、短時間でかつ簡単に構築できるようにもなりました。

エンジニア以外の方も積極的に開発環境を構築するようになり、その結果、元エンジニアのディレクターが開発に復帰したり、子供が産まれたばかりのデザイナーも、リモートワークで子供の面倒を見ながら仕事を進めることができるようになりました。

また、データ変更や、インフラの構成変更時は、コンテナを破棄、更新するだけで良くなりました。

今後、ランサーへの開発依頼がよりスムーズに進むことも期待しています。

今後の展望

一定の効果はありましたが、いわゆるDockerのベストプラクティスにはまだ程遠い状態です。

今回のDocker移行で、エンジニアのDocker意識が高まりましたので、今後、マイクロサービス化も見据えながら、Docker導入をさらに進めていきたいと思います。