02 May 2018, 23:14

Docker Compose上でのRailsアプリケーションの開発フロー

早いものGWも終わり一週間が経ってしまいました。
長期の連休になるとだいたい自分は趣味のアプリケーション開発をよくすすめています(ふだんからやりたいけどなかなかできない)。今年のGWも趣味アプリケーションの開発をしてました。

いま作っているアプリケーションは自分の英語学習用のサービスなのですが(サービスについては別途どこかでご紹介したい)、新しい技術を実際に試す場としても利用しています。自分はいつも趣味アプリケーションで新しいことを試してみて、よかったらそれを職場にもっていくことが多いです。

アプリケーションはRailsで書いているのですが、Railsの他にMySQLやNginx、ElasticSearch、Kibanaなど関連するコンポーネントが多いです。 そのため、Docker Composeを利用して動かすことにしています。Dockerは以前から使っていたのですが、実際にアプリケーションのせてとして開発・運用してみると、いろいろ問題があり試行錯誤することになりました。

GWはその部分と主に格闘していて、最終的にそこそこ良い開発フローを整えることができました。今日はそのご紹介です。
※もちろん、現時点でのやり方であり、改善しているので現状と異なることも多いと思います。

課題だったこと

すでに上で書きましたが、アプリを動かすコンポーネントも多かったということでDocker Composeを使うことにしました。 本番環境をDocker Composeで動かすとなると、開発環境もcomposeで動かしたほうがいいと考えました。 理由はいくつかありますが、コンポーネントが多いので開発環境を構築するのに手間がかかるのでそこでもcomposeを使いたいというのと、運用になれるためにも可能な限りは開発環境でも本番と同じフローを行うべきと考えました。

しかし、実際にはじめてみるとといろいろな問題がでてきました。たとえば、

  • その1:開発環境はどうするべきか
  • その2:アプリケーションコードをどうやってコンテナに配置するか
  • その3:本番の環境にどうやってデプロイするか

まずはじめに課題になったことが、開発環境としてアプリケーション(Rails)のコードをどこでどうやって書くかという問題でした。 アプリケーションをDockerコンテナで動かす場合には、いくつかのソースコードの配置方法があります。

1つ目は、コンテナ内部にアプリケーションのソースコードを配置する方法です。
アプリケーションのソースコードごとコンテナイメージにするため、シンプルで本番環境へのデプロイも簡単です。 一方、アプリケーション開発時にそのままの方法で行うと、コンテナ内部にあるソースコードを直接編集する必要がでてきます。もちろんですが辛い話です。
理由は書くまでもないですが、そもそもコンテナ内にSSHアクセスすること自体がバッドプラクティスであり、またコンテナのプロセスがもし落ちた場合にはソースコードごと消え去ります。

2つ目は、Dockerのボリュームの機能を利用して、ソースコードをコンテナと共有する方法です。
ホスト側にソースコードを置けるので、直接編集ができますし、それをコンテナ側と共有できます。一番理にかなってそうですが、こちらも実際にやってみると、すべて解決!というわけにはいきませんでした。

例えばRailsの場合、ソースコードはボリュームマウントでローカルのPCからは見えていますが、それを実行するRubyの環境はコンテナ内ということになります。
つまり、RailsのRakeタスクなどを実行する場合も必ずコンテナ側からの実行を行う必要があります。これは、いずれにせよdocker-composeで運用する以上、本番環境では発生するのですが、スピーディに行いたい開発の場面ではすこし辛かったです。 これはそれほど問題ではないですが、それ以上に、アプリケーションのデプロイがdocker-composeの他のコンポーネントの運用フローと別になってしまうことが一番大変なことでした。正直コンテナ化のうまみが少なくなるなと感じました。

このような問題があり、試行錯誤した結果、後述するようなフローとしました。
いまのところはこの流れが一番ストレスなくうまくいっています。

Docker Composeを使ったアプリケーションのデプロイフロー

結果的にどうなったか、まず図で示します。
development_flow

本構成のポイントはまず、DevelopmentとStagingとProductionの3ステージに分けたことです。
趣味アプリケーションにしてはやりすぎでは?と自分でも思っていたのですが、結果的にこれで落ち着きました。 DevelopmentとStagingはステージは別れているが同一マシーン上の話です。

Developmentの特徴は、Railsのアプリケーションはマシーンのローカル上で直接に動かしている点です。 そして、関連するコンポーネントであるMySQLとElasticSearchはDocker Compose上で動かしています。 Railsアプリケーションはローカルですが、そこにDocker Composeで動いているMySQLやElasticSearchなどのDB群に接続する構成です。(実際にはNginxもこの時点で動作していますが、使わないので無視)
なんだかんだいって、開発環境はローカルの操作感が最高っていうことですね。一方で、関連するコンポーネントも多いので、Docker-Composeのいいところも使いたいという発想です。

Railsアプリケーションの方で、変更をGitレポジトリへPushすると、CI runnerが動き、アプリケーションのDockerイメージを作ります。 その際には、本番環境へのデプロイの容易さを考えて、アプリケーションのコードはまるごとコンテナイメージの中に組み込みました。

Dockerイメージが出来上がってしまえば、もうやることは単純です。
デプロイしたい環境で、docker-compose pullして、docker-compose upのみです。

はじめは、Stagingはなく、Dockerイメージがビルドでき次第、そのままProductionへ投入していました。
ですが、コンテナ化することによって発生する不具合なども多く困りました。また、docker-composeの操作を行うのが、ほぼ本番環境のみになるため、不慣れでいろいろとやらかしたりしました。
というわけで結果的にStagingが設けられました。

大まかな流れは上のとおりですが、さらにそれをスムーズに行うために行った工夫点などを書いていきます。

Dockerイメージのビルドを速くする

アプリケーションをDockerイメージの中にいれることにしたわけですが、Dockerイメージをビルドするたびに、依存パッケージのインストールが走り非常に遅くイライラしました。
これについては有名な話ですが、Dockerfileに少し工夫するだけで解消できます。

アプリケーションを配置する前に依存パッケージのインストールを実行しておくことことで効率化しました。 これは、Dockerはイメージをビルドするときにレイヤーごとにキャッシュを行うのですが、それを利用したものです。 頻繁に変更が行われるのはアプリケーションのソースコード部分であり、依存パッケージは頻繁には変更されないのでキャッシュをうまく使いましょうということです。
(Ruby2.3.1なのはきにしないで…)

# Dockerfile
FROM ruby:2.3.1
RUN apt-get update && apt-get upgrade -y
RUN gem install bundler -v "1.15.4"
RUN mkdir /usr/local/src/app

# ----- ここから注目 -----
## ソースコードを入れる前にGemfileをコピーしbundle installしておく
ADD Gemfile /usr/local/src/app/Gemfile
ADD Gemfile.lock /usr/local/src/app/Gemfile.lock
WORKDIR /usr/local/src/app
RUN bundle install
# ----- ここまで注目 -----

# アプリケーション本体コードの配置
ADD . /usr/local/src/app/
RUN bundle exec rake assets:precompile
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "3001"]

Gitlab-ciでDockerイメージを自動ビルドする

Dockerイメージの作成を高速化したと言いましたが、とはいってもそれなりの時間もかかりますし、手作業で行うのがしんどくなってきたので、イメージ作成を自動化させました。
今回はレポジトリにGitlabを使っていたので、Gitlab-ciを使って行いました。
正直な話、GitlabはCI/CDのインテグレーションあたりが気に入っていて、今回もそれが理由で採用しました。

以下のgitlab-ci.ymlは、ほぼGitlabのドキュメントに載っているとおりです。
Masterブランチにコミットがあった場合のみビルドするようにだけ変更しました。

# gitlab-ci.yml
image: docker:stable

services:
  - docker:dind

stages:
  - release

variables:
  CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
  DOCKER_DRIVER: overlay2

before_script:
  - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com

release-image:
  stage: release
  script:
    - docker pull $CONTAINER_IMAGE:latest || true
    - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_BUILD_REF --tag $CONTAINER_IMAGE:latest .
    - docker push $CONTAINER_IMAGE:$CI_BUILD_REF
    - docker push $CONTAINER_IMAGE:latest
  only: 
    - master

Dockerイメージに適切にタグを付ける

Gitlab-ciを使った自動ビルドで重要な点は、自動化だけではありません。
DockerイメージにGitのコミットのハッシュ値をイメージのタグとしてつけているのですが、これがとても重要です。
それは、本番環境にデプロイしたあとに不具合があった場合のロールバック対応に非常に重要なためです。 不具合があった場合に、Gitのコミットの履歴からさかのぼりたいIDをゲットして、そのIDのついたタグのイメージに戻すことができます。これがDockerイメージとソースコードとが紐付いているとてもいい理由かと思っています。

今後について

Dockerはサービスオーケストレーションとして便利ではありますが、ローカルでの開発では必ずしもベストでないこともあり、そのいいところをどるかというところでの試行錯誤でした。
結果的に、この状態になり、インフラの運用はかなり楽になり、アプリケーションの開発にもかなり集中できるようになってきました。
次のステージはいったんアプリケーション側の開発に集中しサービス側を磨いていくことになるかなと思います。

Docker関連の調査は、仕事でも行っているのでまた知見がたまれば報告します。

このエントリーをはてなブックマークに追加