そこそこ大きめな Web アプリの CI とデプロイの改善日誌
ユビレジという会社などで働いています、今日はユビレジの話をします。これは話なのですが、我々は「サービス産業のための データインフラを整備する」ということを目標にしています。なので、 iPad 用レジアプリを作っている会社なのですが、そのアプリと同様に重要な製品として各種データの分析機能みたいのがあります。
ようするに我々はそれなりに巨大な業務用 Web アプリの開発、運用も行なっています。 Web アプリのは Ruby on Rails で書かれていて、 iOS クライアント向け API とユーザー向け管理画面と内部向け管理画面が一つのアプリになっています。モノリスですがこれをバラそうと思ったことは今のところありません。
もともと私達はテストや CI/CD について積極的に取り組んできました。古くから( Backbone 時代から)いわゆる SPA を開発、保守してきたこともあって、古くから PhantomJS などを使ってブラウザテストを実行してきていました。これにより、結構な量の実際にブラウザを起動して行なうテストが書かれています。結果として実行時間もすごく、 1 並列でながすと 2-3 時間ぐらいかかるようになっています。それではこまるので、並列数を上げて対処しています。
ユビレジでは TravisCI を使っているので、まあだいたい以下のようになっています。
travis.yml
yaml
env:
matrix:
- PARALLEL_TEST_PROCESSORS=3 TEST_SUITE="parallel:features1"
- PARALLEL_TEST_PROCESSORS=3 TEST_SUITE="parallel:features2"
- PARALLEL_TEST_PROCESSORS=4 TEST_SUITE="ci:minitest"
- PARALLEL_TEST_PROCESSORS=1 TEST_SUITE="ci:ie" SAUCE=true
script:
- bundle exec rails $TEST_SUITE
ci:ie という部分に嫌な予感がしますがここでは忘れます。 ci:minitest という名前のタスクが定義されていることから、我々が rspec ではなく minitest を使っていることが分かります。
Rakefile をみていきましょう
features1 features2 という部分は壮絶でこのようになっています
task :features1 do
Dir.glob("#{Rails.root}/features/**/*.feature").sort_by{|x| File::Stat.new(x).size }.select.with_index{|x,i| i % 2 == 0 }.each(&FileUtils.method(:rm))
sh "./bin/rake parallel:features:with_retry"
end
task :features2 do
Dir.glob("#{Rails.root}/features/**/*.feature").sort_by{|x| File::Stat.new(x).size }.select.with_index{|x,i| i % 2 == 1 }.each(&FileUtils.method(:rm))
sh "./bin/rake parallel:features:with_retry"
end
このようにすることで Cucumber の実行を 2 インスタンスに分割することができます。テストの実行を複数インスタンスに分割する方法はいろいろと難しいものが提供されていますが、これでも十分です。
ライブラリやらなんやらのキャッシュは有効です。ここで真面目の物事を考えると、いざというときにデプロイできないみたいな事態を防ぐためにキャッシュないときでもいろいろ動くように週一でキャッシュ飛ばして CI 実行するみたいなことをしておくとかが考えられます。
しかし僕はあえてそれをやっていません。以下の理由からです
リポジトリの clone は以外と CI において時間のかかる作業です。特に歴史ある Rails アプリの場合 assets pipeline に膨大な量の資源がたまっていると思います。わたしたちのアプリケーションの場合、 app/assets 以下に 400MB 以上の画像、動画、 PDF などがたまっていました。
これについて、もはや使われていないものについては削除したり、別のアセット管理用のリポジトリに切り出す(そこに入れたものが Cloudfront 経由で参照できるようにした)などして最終的に 70MB ぐらいにまで削ることができました。
これによりデプロイとかも高速化されていろいろよい効果がありました。
他にも大量のことをやっているのですがもう書くの飽きてきたので終わりにしたい。いろいろやっていった結果 30 分ぐらい待たされていた CI が 15 分とかになったので結構満足してます。
まとめというか、こういうことを取り組むときの心構えのようなものについてなのですが、だいたい以下のようなことを考えています。
iOS アプリの CI が遅すぎるので Web アプリで得られた知見などをもとになんとかしていきたいですね。