Skip to main content

なぜ buildflow を作ったのか

· 11 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では なぜ buildflow を作ったのかについて説明します。 開発者である自分の好みや置かれた環境などが所々に反映された内容になっています。

解決したい課題

自分は CI/CD の DX の改善に業務として取り組んでいます。 リポジトリはたくさんあり、横断的にメンテナンスしています。 幾つかのリポジトリはモノレポになっており、 CI の複雑さが増していたり、 CI の実行時間が長かったりします。

現在の CI/CD には以下のような問題があると感じています(他にもあるんですが、 buildflow と関係ないので割愛)。

  • 実行時間が長い
    • PR とは関係ない処理(test, build, etc) が実行されている
  • 金銭的に高い
    • 実行時間が長いので無駄にお金がかかっている
    • CI サービスによっては並列度を上げることで実行時間が縮む場合があるが、それでもその分お金がかかる
  • PR とは直接関係ないところで失敗する
    • PR とは関係ない処理(test, build, etc) が実行されていて、それらが flaky で失敗する
  • メンテナンス性が悪い
    • 属人化気味
    • 何をやっているのか分かりにくい
  • 同じような機能を複数のリポジトリで実装・メンテしたくない

これらの問題を解決するために buildflow を開発しました。

buildflow で必要な処理だけを実行する

buildflow では PR の情報を自動で取得し、それらに応じて実行する処理を変更できます。 変更されたファイルに応じてだけでなく、 label や PR の author などでも変更できます。 Tengo script を用いて柔軟なロジックを実装できます。 JSON や YAML の読み込みもサポートしているので、依存関係などの設定を別ファイルで管理することも出来ます。

一部の CI サービスはこれを解決するための機能を提供しています。 CodeBuild は Webhook の Filter で特定のファイルが変更された場合のみ build を実行できますし、 GitHub Actions でも似たようなことが出来ます。

それらで事足りるならそれでも良いでしょう。 それらだけだと難しい場合、 buildflow を使うとより柔軟に対応できるかもしれません。

並列処理による高速化

シェルスクリプトで for loop などで処理していて時間がかかっている場合、 buildflow で並列処理すると高速化するかもしれません。

メンテナンス性

buildflow を使わなくても「必要な処理だけを実行」したり「並列処理で高速化」したりはできるでしょう。 それでも buildflow を開発したのは、楽をするため、メンテナンス性を高めるためです。

PR の情報はよく必要になるので自動で取得するようにしています。

シェルスクリプトで複雑な CI を実装していると、メンテナンス性が悪くなります。 チームメンバーのシェルスクリプトへの習熟度に依存しますが、 シェルスクリプトはエンジニアなら誰でも書ける分全員が習熟しているとは限りませんし、 容易にバグが生まれます。 チームによりますが、Python や Ruby, Go といった他の言語と比べ、 lint や test がされてないことが多いせいもあるとは思います。 アプリケーションのコードは当然 CI で test, lint するのに、 CI とかのシェルスクリプトはしないというのも珍しくないと思います。

あとはサポートされているデータ構造が貧弱だったり、関数の I/F がわかりにくかったり、ググりにくい機能が多かったり、 コマンドのオプションを逐一調べないとわからなかったりします。

余談ですが、 shellcheck や shfmt を使うことをオススメします。 shellcheck を始めて使うと、シェルスクリプトにはこんなに色々罠があるのかと気付かされると思います。

Google の Shell Style Guide では次のように書かれています。

If you are writing a script that is more than 100 lines long, or that uses non-straightforward control flow logic, you should rewrite it in a more structured language now

では Ruby や Python といったスクリプト言語で書いたらどうでしょうか? シェルスクリプトで挙げた問題は解決すると思いますし、非常に自然で合理的な選択だと思います。

それでも buildflow を実装したのには、幾つか課題感があったからです。 まずは処理系、サードパーティのライブラリ、 OS パッケージに依存することです。 サードパーティのライブラリは使わなければいい話ですが、 Ruby や Python を使っていれば使いたいという声も出てくることはあるでしょう。 アプリケーションで同じ言語を使っていればそれとの共存も気にしないといけないかもしれません。 buildflow に限らず、自分は Go の「ワンバイナリで動く」という世界観が非常に好きです。

自分はこれまでシェルスクリプトを Go で書き直すということをやってきました。 その場合以下の2つがありますが、どちらにせよ課題感があります。

  • ビルド済みのバイナリを使う
    • 配布方法を考えないといけない
  • スクリプト言語のように go run で実行する
    • 他のスクリプト言語と同様の問題がある

ロジックとコマンドの分離

シェルスクリプトを何かしらの言語で書き直す場合、 全てをそれらで書きたいわけではありません。 だからこそ、規模が小さいうちはシェルスクリプトで書くのでしょう。 シェルスクリプトで書いたほうが楽な部分もあるのです。

buildflow ではコードを以下の3つに分離します。

  • 設定ファイル(YAML)
  • Tengo script
  • シェルスクリプト(細かいこと言うと、シェルスクリプト以外も実は使えるけど)

こうしてシェルスクリプトで書きにくい部分を分離し、適切な粒度で管理することでメンテナンス性を高めるというのが一つの狙いです。 Tengo script は基本的にデータの整形などに役割を限定し、外部ファイルに切り出せるようにすることで テストしやすいようになっています。

buildflow にはフレームワークとしての側面があり、 buildflow に乗っかることで共通の機能の実装を省いたり、コードを適切に分割してメンテナンス性を維持することができると期待しています。

尤もここはトレードオフがあるでしょう。 上記の 3 つを行き来しないといけなくて辛いというフィードバックをもらったこともあります。 コード分割は必須ではないので YAML にインラインで書くことも出来ますが、あまりおすすめしないのと、 そもそも上記のフィードバックはファイルの行き来だけでなく

  • buildflow の設定
  • Tengo Script
  • シェルスクリプト

という 3 つの異なる言語を行き来するという意味もあるのでしょう。

それはそういう側面もあるでしょう。 これは buildflow の根本的な部分なので変更されることはないと思います。 もし変更するようなら多分別のツールとして作っているでしょう。

まとめ

buildflow は自分が直面している CI/CD の課題

  • 必要な処理だけ実行したい
  • 共通の処理を逐一実装したくない
  • メンテナンス性を高めたい

を解決するために作りました。

buildflow はワンバイナリで動きます。 コードを適切に分離し、シェルスクリプトから複雑なロジックを除去することで、メンテナンス性を高めることを目指しています。