こんにちは。RubyMine大好き!@vexus2です。
先月末くらいにnanapi.jpの本番環境をDockerに乗せました。
Dockerは、Heroku Button的にOSSプロダクトの環境をサクッと作ったり気軽に検証したりする分には簡単にできて便利です。
が、それらを本番運用に乗せるには色々ケアしなきゃいけないことがが多くて、例えば安定しないソフトウェアがあったり、デプロイ周りの速度や安定感向上とか、開発環境適用から本番環境運用までに非常に分厚い壁があるなーと感じたので、自分たちのハマったところやノウハウを共有できればと思います。
サーバ構成
元々nginxは各アプリケーションサーバ内に配置していましたが、アプリケーションサーバから切り出してELB -> FrontProxy -> Application Serverという多段プロキシ構成にしました。
各アプリケーションサーバ、Adminサーバ、バッチサーバはCoreOSを乗せて、同一コンテナ環境で動いています。
コンテナ構成
上記四角枠の中のApplicationServer、BatchServer、AdminServerは全てCoreOSで動かしていて、各CoreOSホストはこのようなコンテナ構成で積んでいます。
RailsコンテナはUnicorn、Fluentdコンテナはtd-agentをそれぞれForegroundプロセスとして動かしています。
DatastoreはいわゆるData Volume Containerで、Rails側のログなどのデータをFluentdコンテナ側で利用出来るようにコンテナ間共有と永続化を行っています。
Data Volume Containerについてはこちらが参考になります。
Data Volume と Data Volume Container
コンテナ内でのテータ管理 – Docker User Guide
その他にDatadogというコンテナ監視用のサービスのエージェントコンテナと、Docker Registryも各ホストで上げています。
Docker Registry Serviceについて
Docker Registryサービスの選定についてです。
DockerHubやQuay.ioなどといったSaaS型と、docker-registryやCoreOS Enterprise Registryのインストール型の、大きく分けて2パターンがあります。
詳細については以下に書きましたのでご参考ください。
で、機能的にはQuay.io一択なんですが、如何せん日本国内からのPull/Pushに時間が掛かってしまい、デプロイごとに20分弱の時間が取られてしまうという状態でした。
二転三転した結果、WantedlyさんがDocker at Wantedlyで紹介されていた「docker-registryを全ホストに配備」という方法を取りました。
全ホストで上げることで多少メモリは食いますが、docker-registryが単一障害点になることを防げるのと、docker-registryのバックエンドにS3などを使うことで任意のリージョンからPull/Push出来るので高速になります。
docker-registryを自前運用するためにDocker Imageのビルドサーバも自前で持つ必要が出てきますが、デプロイ高速化のためにはしょうがない、と一旦許容しています。
Blue-Green Deploymentについて
nanapiではデプロイ時に Container based Blue-Green Deployment の手法を取っています。
Blue-Green Deploymentでは新規にインスタンス郡を立ち上げてそちら側にデプロイして切り替えるという方式が一般的ですが、Container basedはデプロイ時に静的インスタンスに対してコンテナ郡を新しく立ちあげて、nginxからのコンテナの向き先を切り替える、という仕組みです。
この形によるメリットとしては
- デプロイのたびにインスタンスを倍にする必要がない
- デザイン修正や文言修正などの軽微なリリースも気軽に行える
- リリース前に切り替え前のコンテナ群で動作検証が容易に可能
- デプロイごとのインスタンス郡初期化処理が不要なため高速
というのが上げられます。逆にデメリットとしては
- nginx側は現在サービスインしているコンテナをポート番号で振り分け管理する必要があるので、若干煩雑になる
- デプロイの際に一時的に各ホストにコンテナが倍立ち上がる必要があるので、マシンスペックがそれなりに必要
ということがあったりします。
どちらも一長一短ではあるので一概にはオススメしにくいですが、軽微な修正の類を頻繁かつ高速にデプロイしたいような場合には向いているので、サービス特性に合わせて設計に合わせて使い分けると良いかなと思います。
Container-based Blue-Green Deploymentの実装周りについては別エントリで紹介します。
Rails側でやったこと
Bundle install周り
Dockerfile内でRailsアプリのコードをADDしてからbundle installすると、コードに1行でも変更があったらbundle installがキャッシュに乗らなくなります。(厳密にはADD以降のコマンドが全てbuildキャッシュに乗らない)
そのため、RailsアプリをADDする前にGemfileとGemfile.lockをADDすることで、Gemfileが更新されたときのみbundle installが実行されるようになります。
ENV APP_HOME /app
ADD ./Gemfile $APP_HOME/Gemfile
ADD ./Gemfile.lock $APP_HOME/Gemfile.lock
RUN bundle install -j4 --path vendor/bundle --without development test staging
ADD . $APP_HOME
バッチ処理
今まではwheneverを使ってシステムのcronで動かしていたんですが、CoreOSではそれができない 1 のでclockworkを使うことにしました。
バッチサーバでclockworkコンテナを上げておき、その中でcron処理を実行させる、という仕組みです。
ログ管理周りの設定
Railsコンテナ内でのログをFluentdコンテナでハンドルさせたいので2つのコンテナ間をそれぞれLinkで繋いでいます。
Fluentd -> Railsコンテナのログはdata volumesが共有されるのでそのままtailが出来るので特に設定は不要ですが、
Rails -> Fluentdにログを吐き出す場合は、当たり前ですがコンテナ間はlocalhostでは接続できないのでIPアドレスをEnvから取得して指定してやる必要があります。
Fluent::Logger::FluentLogger.open(nil, host: ENV['FLUENTD_PORT_24224_TCP_ADDR'], port: ENV['FLUENTD_PORT_24224_TCP_PORT'])
最後に
nanapiでは新しい技術を使って技術基盤やインフラ基盤を一緒に考えてくれるエンジニアを募集しています。
株式会社nanapi │ エンジニア採用サイト
Docker化にあたってはWantedlyの@spesnovaさんにお話を伺ったりアドバイスを頂いたりしました。大変感謝しています。ありがとうございました!!
- systemdのtimerユニットを使えば不可能ではないが、CoreOS側にcronの役割を持たせたくなかったので ↩