大型サービスのデプロイに組み込むgulpプラグインの話

この記事はカヤックアドベントカレンダー20日目の記事です。

おはようございます。Lobiチームで主にフロントエンドの実装を担当しています、森本(@moshisora990)です。ISUCONで悔しい思いをして以来Goにはまりつつありますが、今日はLobiのフロントエンド開発事情と、そのために(本当にそのために)書いたgulpプラグインの話にしたいと思います。

もくじ

Lobiのフロントエンド開発事情

Lobiでは、AngularJSを採用しています。また、開発はCoffeeScript、ES2015+、Sassなどの言語を取り入れたり、browserifyを導入したり、他にも多々周辺技術を採用して、gulp/grunt等のタスクランナーは欠かせないものになっています(最近は npm scripts で全部済ませる派もいるとかいないとか…)。

タスクランナーで compile concat minfy 等のタスクを実行して、最終的に配布する形のファイルを作成することは、もはやフロントエンド界隈ではほぼ必須といってもよい状況かと思いますが、複数人で開発していると、最終的に出力するminfyされたファイルがconflictする等困ったことも起こります。そこで、Lobiチームでは出力されたファイルはcommitせず、デプロイ用のサーバ上でタスクを実行する形をとっています。

さて、Lobiも稼働が長くなってきて、使用している開発環境もずいぶん古いものになってきていて、チーム内からぽつぽつと声が上がるようになってきてしまいました。

  • gruntじゃなくてgulp使いたい!(個人的な見解です)
  • node古いよ…時代はES2015+でしょう…

上げればいいじゃん、と思うところですが、稼働も長いと依存も多いものなのです…。

ということで、最近奮闘しているこの辺りのお話をします。

デプロイ時に考慮したかったこと

コード内の依存はもりもり取っ払えばいい(これも大変)として、Lobi内の仕組み(ここではデプロイ)に起因する改善点もありました。

デプロイについてはこちらの記事で詳しく紹介しています。

techblog.kayac.com

レビューを終えたブランチをmasterとなるブランチへマージし、デプロイサーバはgit pull、Perlのモジュールの更新、Go製アプリのビルド、npmパッケージの更新、およびタスクランナーの実行等を全自動で行っています。その後、新たに配布の必要なファイル(以前のデプロイ時から更新のあったファイル)をtarballにしてS3にアップロード、consul eventを発火して、各アプリケーションサーバがそれをPullしてきて反映する、という流れです。

ここでいくつか問題が…

  • 巨大になったコードをデプロイの度にビルドしているとデプロイの所要時間がどんどん伸びる
  • タスクランナーを実行すると、ソースとなるファイルに変更が無くても出力されるファイルは「更新」される(つまりデプロイ対象になってしまう)
  • 大型のフレームワークを導入しているので当然出力されるファイルも巨大。これを関係のないファイルの変更のデプロイの度に再配布していては、ブラウザのキャッシュも捨ててしまってもったいない...

というわけで、これをなんとかしたかったのです。

今までどうしていたのか

基本的にほぼ全てのビルドタスクを grunt で実行しているので、grunt-diff を使ってビルドを実行しています。

www.npmjs.com

このプラグインは、監視対象となるファイル群を指定すると、それらの内容に変更があった場合にだけ特定のタスクを実行させることができます。変更がなければタスクも実行されないので出力ファイルが更新されることはありません。

gulpプラグインを書いた

gulpに移行するに際して、上記の悩みを解消すべく、とりあえずgulpプラグインを書いてみました。gulp-diff-buildです。基本的な着想は grunt-diff と同じで、それを gulp 向けに実装して拡張したものです。

www.npmjs.com

どういうものかというと、タスクの実行元となるファイル群の内容を監視して、変更があった場合のみ、Streamにファイルを流します。

gulp-changedgulp-cachedといった差分ビルド用のプラグインも存在しますが、gulp-changed gulp-cached は出力ファイルと入力ファイルを比較して変更が必要なファイルだけを以後のタスクに渡すのに対して、gulp-diff-build は、srcのファイル群に変更がなければ、以後のタスクは実行されませんが、いずれかのファイルに変更があった場合はsrcの全ファイルをStreamに流す、これらとは少し違うアプローチのプラグインです。

const diff = require('gulp-diff-build');

gulp.task('default', () => {
    gulp.src('src/**/*.*')
        .pipe(diff())
        .pipe(gulp.dest('dist'));
});

とすると、1度目は全ファイルをコピーして2度目は何もコピーしない、いずれかのファイルを加えると再度全ファイルをコピーする挙動をします。

また、Sassやbrowserifyのように、importする形で依存関係を解決するタスクに指定するファイルは、import元の数ファイルですが、import対象のファイルが更新された場合にもタスクを実行する必要があります。このようなケースでは、次のように記述すれば、監視するファイルからStreamに流すファイルをフィルタリングすることができます。

const diff = require('gulp-diff-build');

gulp.task('default', () => {
    gulp.src('src/sass/**/*.sass')
        .pipe(diff({
            dest: [
                'src/sass/main.sass'
            ]
        }))
        .pipe(sass())
        .pipe(gulp.dest('dist'));
});

これで sass ディレクトリ以下のファイルに変更があった場合のみ、main.sassを以後のタスクに渡せます。

ファイルの内容をhashにしてファイルに保存するので、キャッシュをタスクごとに保持しておくこともできます。

const diff = require('gulp-diff-build');

gulp.task('build-css', () => {
    gulp.src('src/sass/**/*.sass')
        .pipe(diff({
           hash: 'css'
        }))
        .pipe(sass())
        .pipe(gulp.dest('dist'));
});

gulp.task('build-js', () => {
    gulp.src('src/js/**/*.js')
        .pipe(diff({
           hash: 'js'
        }))
        .pipe(browserify())
        .pipe(gulp.dest('dist'));
});

これでひとまず

  • src ファイル群に変更がないときはタスクは実行されない(正確にはされてるけど入力が空)
  • src ファイル群に変更がないときは出力ファイルも変更されない
  • src ファイルに変更を加えると更新の必要なファイルの出力タスクのみ実行する

を実現できました。書き出されるファイル毎にキャッシュ管理もできるので、更新時にブラウザに破棄させるキャッシュも最小限にできます。

Lobiでのデプロイのようなケース以外にあまり利用ケースは思いつきません…が、最終的な出力ファイルをcommitしていない環境で、かつ gulp-rev gulp-rev-replace 、あるいはサーバサイドでファイルのmtimeをhashにして加える等、何らかのキャッシュ破棄機構を入れている場合には使っていただけるとお役に立てるかもしれません。

おわりに

  • gulp のプラグインは多々公開されていて大抵既存のもので事足りますが(大変ありがたいです🙏)、ニッチそうな当記事が少しでも参考になることがあれば幸いです。
  • 大抵事足りるので書く発想が無かったですが、案外簡単に書けたので困ったら書くの精神で!
  • 実は知らないだけでこれを実現できるプラグインが存在するのでは…という気がしてならない…
  • 何かよい方法をご存知の方、是非教えてください🙏

明日はブログのヘッダを飾っている…?!とも噂される @mackee 先生のお話です!お楽しみに!!

おまけ

カヤックでは大規模サービスのフロントエンド環境をいい感じにしたいエンジニアも切に募集しています!

カヤックUnityアドベントカレンダー2016 もアツいです!