Gyazo 開発環境の Docker 化

業務委託として現在 Nota 社の Gyazo のサーバサイドの開発をお手伝いさせてもらっているのですが、その中でやっていることについて幾つか紹介したいと思い、今回は開発環境で全面的に Docker を使うようにしたという話について書こうと思います。

Gyazo

Gyazo について知らない人もいると思うので、念のため説明しておきます。Nota 社では、主要な製品のひとつとして Gyazo というサービスを提供しています。Gyazo は、スクリーンショットや GIF 動画を簡単に保存、共有、検索できるサービスです。

Gyazo - Welcome to Gyazo: The Easiest Way to Capture Your Screen

単純にスクリーンショットを保存するだけなら OS の機能だけでも十分ですが、GIF 動画を保存できたり、いつどこでどんなアプリケーションを利用しているときに撮影したのか、あるいは画面にどんな文字が写っているかといった情報を元に検索できたり、保存した画像をコレクションという単位でまとめて共有できたりと、Gyazo を使って保存しておくと意外と便利なことが多く、個人的にも重宝しているサービスの1つです。

我々が開発環境で Docker を使うメリット

Gyazo のサーバサイドの実装には、プログラミング言語の観点で見ると Ruby、Go、JavaScript などが利用されています。ミドルウェアの観点で見ると、Datastore、Elasticsearch、MongoDB、Redis、Nginx、Puma、Sidekiq などが利用されています。

Gyazo の開発チームでは、以前までは一部ストレージ系のミドルウェアについては Docker を利用しつつ、その他の部分を MacOS 上で動かして開発を行っていました。Nota 社が提供している別のアプリケーション Scrapbox では、既に全体的に Docker を利用して開発を行うような環境になっているということで、今回は Gyazo の方も Docker を利用して開発を行えるようにしました。

以前までの開発環境構築では、MacOS に Homebrew を入れ、各種パッケージや Ruby や Node.js を入れ、bundle install し、Nokogiri や OpenSSL 周りでたまに失敗し、Google で調べて適当に回避するようなことが多かったのですが、開発時に必要な環境すべてを Docker に管理させるようにすることで、Git リポジトリを git clone して docker-compose up というコマンドを叩くだけで開発を始められるようになり、新しく (あるいは久しぶりに) 開発に参加するメンバーの負担が減りました。

また、環境構築を行った時期によってミドルウェアのバージョンや環境構築方法が微妙に異なり、これが原因で稀に発生していた問題が解消されました。他にも、実行環境に関する情報をコードとして表現する (ことが半ば強制される) ことで、開発環境の見通しが全体的に良くなったなと実感しています。

gems や node modules の管理

開発用の Docker イメージは、共有の Docker イメージリポジトリを利用して共有するのではなく、それぞれの開発者のマシンでビルドするようにしています。今回のアプリケーションでは、gems や node modules を利用しており、Bundler や Yarn で依存パッケージを管理していますが、Docker イメージ内に含めた依存パッケージを開発中に直接利用するような設計にしてしまうと、依存パッケージが更新されたとき (Gemfile.lock や yarn.lock が更新されたとき) に、各開発者のマシンで Docker イメージの再ビルドが必要になってしまいます。

これは、依存パッケージを配置するディレクトリに local-driver を利用した data volume をマウントすることで回避できます。docker-compose を使う場合、以下のような設定でマウントできます。

docker-compose.yml から一部抜粋したもの

Dockerfile に依存パッケージをインストールする処理が含まれていれば、初めて docker-compose up を実行したときに、自動的にマウントされた data volume に依存パッケージがインストールされていきます。

Ruby 用の Dockerfile の例

ファイルの変更検知

これまで macOS 上で開発していたこともあり、Docker を動かす場合は必然的に Docker for Mac を使うことになる訳ですが、Docker for Mac を利用する場合、OS 依存のファイル変更検知の仕組みが使えなくなる場合があります。

今回のような構成のアプリケーションの場合は、Webpack や Watchify、Rails の listen gem を利用した ActiveSupport::EventedFileUpdateChecker などがそういった仕組みを利用しています。大抵のツールでは代わりに Polling でファイルの変更を検知するような選択肢が用意されているので、必要な場合は適宜変更しましょう。

config/development.rb で polling を強制する例 webpack.config.js で polling を強制する例

※ファイル変更検知の話題は、いま現在 Docker や Docker for Mac のファイルシステム周りが活発に更新されているということもあり、この記事の執筆時点からまた状況が変わるかもしれません。

段階的移行のすすめ

Docker 移行時に気を付けたいところとして、並行期間を設けながら段階的に移行していくべきという点があります。これまでと同じような環境でも動かせるように保ちつつ、Docker を利用した環境に緩やかに移行していく方が良い、ということです。

慣れた環境から移行するのに腰が重いとか、個々人のタスクの優先度がまちまちであるということもそうですが、まだ踏み固められていない状態で一気に Docker を使う環境に切り替えてしまうと、自分が利用していない予想外の箇所で問題が立て続けに起きるということが、往々にして有り得ます。

こういった場合にも「問題が起きた場合は、とりあえずこれまでと同じ方法で開発してください」と言えるようになっていると、開発者をあまり煩わせずに済み、安心感もあります。自分のようにフルタイムで面倒を見られないような立場で担当している場合には、なおさら重要になります。旧来の環境と新しい環境、両方で動作するようにコードを整えるのはたいへんな場合もありますが、開発のしやすさがそれ以上に大事なことが多いので、ここにコストを掛けておく価値はあると思います。

Circle CI で docker-compose を使う

開発環境の Docker を完了させた後、継続的なテストのために利用している Circle CI でも、開発環境と同じく Docker を利用するように変更を加えました。

2017年に正式リリースされた Circle 2.0 では、既に Docker を利用してビルドを実行できるような環境が用意されており、Circle CI 2.0 自体は既に利用している状態だったので、開発環境で利用している docker-compose.yml を利用してテストを行えるようにする、という作業だけで済みました。

執筆時点の Circle CI では docker-compose 2.0 が動作するので、machine executor を利用すれば、ローカル環境同様に docker-compose を利用するだけでテストが可能です。次の段階として、毎度 Docker イメージをビルドせずに済ませるためにキャッシュを利用したくなりますが、課金されているリポジトリの場合、docker layer caching というオプションを指定するだけで勝手にキャッシュを行ってくれるため、キャッシュの設定で困ることはほとんどありません。

.circleci/config.yml の設定例

課金されていないリポジトリの場合、自分で Docker のキャッシュを生成 & 復元する処理を用意する必要がありますが、自分でキャッシュを生成するための処理も Circle CI によってある程度の機能がサポートされています。docker layer caching を使わずにキャッシュを利用する方法については、以下の記事でも説明しています。

docker-compose を利用して開発しているアプリを Circle CI 2.0 でテストする

嬉しいことに、Docker のキャッシュを効率的に利用できる構成に変更したおかげで、ビルドの所要時間も少し短くなりました。それまでの1ヶ月間におけるビルドに要する時間の中央値は 450 秒程度でしたが、この変更により 330 秒程度になりました。

おわり

以上、Gyazo のサーバサイドの開発環境で Docker を使うようにした話について書きました。これから Docker を使おうとしている方、あるいは既に Docker を使っている方々の参考になればと嬉しいです。

次回は、Gyazo の Web API を再設計した話について書こうと思います。