Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

個人サービスやOSS開発の保守運用から何を学んだか

というようなことを私はよく主に採用の文脈で口走るのですが、ちゃんと内容をまとめておこうと思ったのでメモ。特に保守運用を経験しているというのが強いと思っているので、そこに注力して書いてみます。

障害対応の経験が積める

10年ほど前にTwistoire (ついすとわーる)というサービスを運営した際に、利用想定の甘さから半日程度のサービス停止を招いたことがありました。

blog.kengo-toda.jp

個人が無償で提供していたサービスとはいえ、使ってくれているユーザに対してそれなりの責任を感じた記憶があります。またひとりで使っていたころには発生しなかった障害なので、単純に課題の分析と解決が面白そうに映るのも事実です。こうした体験から一次対応の必要性や根本解決ないしポストモーテムのあり方について実体験を持って考えられるようになります。

これ、言葉で「実体験を持って考えられる」と書くと軽いんですが、本人としては結構深刻に落ち込んでいて当時のツイート↓を見てもかなり悔しかったのだと思われます。

業務でメンテしていたシステムでも近い経験はできたので、ホビープログラマ固有の特徴かというとそんなことはないと思います。 ただ最近読んだシステム障害対応の教科書でも指摘されていましたが、障害対応は暗黙知と習熟とが必要になりがちです。そのため、むしろすべて自分でコントロールできる=SECIモデルのプロセスを自分の中で完結できる趣味開発というのは、学習効率の面からも悪くないと思います。

「運用しやすさ」という見えない品質に気づける

「いいログを書くためには何を勉強すればいいですか?」という質問をシニアなプログラマに聞くと、だいたいは「運用経験を積む」という回答が返ってきます(n=10くらい)。逆に言うと、運用を経験していないと悪いログを書きがちということです。思いつく「悪いログ」の例はこんな感じです:

  • 単純に量が多い。ログローテーションや保存、実行時性能に悪影響を及ぼす。
  • ひとつの情報が複数行に渡って表示する、パースしにくい書式を使うなど、機械による利用を想定していない。grepやログ解析サービスで追いにくい。
  • 何が起こったかだけ表示し、何が問題で何をすべきかを教えてくれない。次のアクションに必要な情報が入っていない。
  • 情報が足りず、ソースコードと照らし合わせることで初めて意味が理解できる。
  • 出力してはいけない情報が出力されている。

こういう問題は世に出ているガイドラインなどを参照すれば回避可能ですし、アーキテクトやテックリードがちゃんと統制しろという話もあるんですが、やはりプログラマ個々人が運用を意識してコードを書けた方が良いです。経験は最悪を回避するだけでなく、よりよいログを見出すのにも役立ってくれます。思いつく「良いログ」の例はこんな感じです:

  • 発生しやすい問題や運用時の関心事を考慮したもの
    • TRACEログにバッチ処理の進捗度を出力するとか(処理が進んでいないのか遅いだけなのかの切り分け、完了時間の見積の役に立てる)
    • 処理失敗の原因になりやすい「外部から受け取ったパラメータ」を残してくれるとか
    • ガイドライン化しにくい気がしている
  • 読み手が取るべきアクションがわかるもの
    • 「予期せぬのエラーが発生しました」の真逆
    • エラーコード、参照すべきURL、検索を助けるキーワードなど
    • ガイドラインによってエラーレベル(FATAL, ERROR, WARNINGなど)の使い分け方針を定めておくだけでもだいぶ違うが、最終的には想定力がモノを言う印象

このへんは、コーナーケース想定やテストシナリオ設計に通じるところがありますね。サービス運用やアプリリリースの経験があれば、こうした想定の幅が広がり、様々な面からソフトウェア品質の改善に役立てることができます。

品質を落とさないためのリグレッションテストや統合テストを大切にできるようになる

「良いテストを書けるようになるには何を勉強をすればいいですか?」という質問をシニアなプログラマに聞くと、だいたいは「ソフトウェアプロジェクトを継続して運用する」という回答が返ってきます。「テストを書くクセをつけるには?」という質問も同様です。

ここでソフトウェアプロジェクトの運用というのは、コードを書いて成果物を届けるという動きを継続して行うことを指しています。

個人プロダクトを始めた理由は「やりたいことがあるから」のはずです。それはお手玉をしながら綱渡りで火の輪くぐりをするような「あっちこっちを意識しながら壊れないように祈りつつコードを書く」体験とは大きく異なります。しかしながら、テスト設計も自動テストも無いプロジェクトでは容易にこうした体験が醸成されてしまいます。機能ひとつ手を加えるごとに他の機能や非機能要件に対する影響を確認しなければならない状況が生まれ、インクリメンタルに成果を積み上げ「やりたいこと」に近づくことすら難しくなります。

ので品質を落とさないための、というか「新しいことをやっているときに他のことを心配しなくて済むための」リグレッションテストや統合テストを大切にできるようになります。私の場合、同じ理由でシステム監視や運用の自動化、開発環境構築の自動化も関心領域になりました。

大富豪プログラミングに気づける

クラウドが生まれAPIやボタンひとつでデプロイが行えるありがたい時代になりました。私が初めて運用したサービスは有償レンタルサーバー上で動いていましたが、前出のTwistoireはGAE/Jで動いていましたし、最近はHerokuやさくらインターネットのHacobuneのような無償あるいは安価に使えるものも多くなっています。

しかし完全に無料でサービスを続けられるかというとそんなことはなく、ある程度はコストをかけていくことになります。そしてある日、想定していたよりもコストが高くなるという事態が生じます。Twistoireの場合はGAE/Jの料金プラン変更がその契機でしたが、アクセス集中や不具合によって気づくこともあるでしょう:

blog.kengo-toda.jp

趣味開発プログラマはこうした機会をコードが計算機資源を効率よく扱えていないというシグナルとして受け止め、プロジェクトとスキルの改善に繋げることができます。「性能足りなかったらサーバースペック上げればいい」ときもあるかもしれませんが、小型の趣味開発システムだとリスクを取った変更がしやすいということもありコスト効率を追求しやすいはずです。リスクや資源を自分でコントロールできるので、勉強会資料や個人Webサービスシステム構成事典↓のような情報を試しやすいというのもあります:

techbookfest.org

以上、まとめるとホビープログラマは勝手に試行錯誤していろんなことの重要性を理解しているかもしれません。落ち込んでいるときも計画していない出費ができてしまったときもあるでしょうから、元気なさそうだったら話を聞いてあげたら良いんじゃないでしょうか。

SonarQube解析をGitHub Actionsのpull_request_targetイベントで回す

SQ解析が現時点ではあまりpull_request_targetイベントをサポートしておらず、ちょいちょい手間が必要だったのでまとめます。 完全なYAMLのサンプルは↓にあります。

github.com

checkout する場合はrefを指定する

pull_request_target イベントでcheckoutを実行すると標準ではデフォルトブランチをチェックアウトしてしまいます。 よって fetch-depth: 0 に加えて ref の指定も必要です。以下のようにHEADをチェックアウトするか:

      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.ref || github.ref }}

以下のようにマージコミットをチェックアウトすることで解決できます*1

      - name: Decide the ref to check out
        uses: haya14busa/action-cond@v1
        id: condval
        with:
          cond: ${{ github.event_name == 'pull_request_target' }}
          if_true: refs/pull/${{ github.event.pull_request.number }}/merge
          if_false: ${{ github.ref }}
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
          ref: ${{ steps.condval.outputs.value }}

2021年11月17日更新:前者の方法だとforkからのPRでは動作しないので、後者がおすすめです。

PR解析用のオプションを加える

おそらくScanner側の問題だと思うのですが、Developer Editionの機能のためかコードは見つけられていません。現時点では sonar.pullrequest.keysonar.pullrequest.branch の双方を手動で設定する必要があります*2

        run: |
          mvn org.jacoco:jacoco-maven-plugin:prepare-agent verify sonar:sonar -B -e -V \
            ${PR_NUMBER:+ -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} }
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}

2021年8月30日更新:

上記実装では脆弱性が残ることをazu さんにご指摘いただきました。ありがとうございました! ブランチ名はPR作成者が決定可能なので、一度環境変数で受けてから使用するべきとのことです。

        run: |
          mvn org.jacoco:jacoco-maven-plugin:prepare-agent verify sonar:sonar -B -e -V \
            ${PR_NUMBER:+ -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=${PR_BRANCH} }
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PR_BRANCH: ${{ github.event.pull_request.head.ref }}

Sonar Scannerの実行にはJava 11が必要

pull_request_targetと直接関係はありませんが、最近更新が入ったようなので念のため。

Sonar Scannerは実行にJava 11以上が必要なので、Java 8のコードを書いている場合でもJDK 11以上でビルドする必要があります。MavenもGradle*3--release オプションをサポートしているので、それを使うと良いでしょう。Java 8に無いAPIを見つけてコンパイルエラーにしてくれるので、 --target よりも安心です。

勝手を知らないプロジェクトにPull Requestを送るときに気にしていること

初心者向けのPull Request(PR)作成方法はopensource.guideをはじめとして数多く見るのですが、もうちょっと突っ込んだというか中級的な内容の記事を読みたかったので自分で書きます。題材として主に直近で書いた大きめの機能追加用PRを使っています:

github.com

修正や新機能を入れる利点を明確に伝える

何かを提案する場合、その背景には必ず動機となる利点があるはずです。これはとても単純に伝えられることもあれば、テストやグラフを作成しないと伝わりにくいものもあります。

今回の変更では、GitHub Actions workflowの定義ファイル簡素化とパフォーマンス改善が利点でした。定義ファイルが簡素化されることはドキュメントで簡単に伝えられますが、パフォーマンスは測定しなければわかりません。よってパフォーマンス検証用のリポジトリを作成し測定を行うことで、ビルド時間が約28%高速化されることを示しました。

github.com

このリポジトリにあるActionsを実行すると、グラフを描くために必要なCSVが出力されます。それをGoogle Sheetsに突っ込んで以下のようなグラフを作成し、PRページに添付しています。結果として、本当に高速化に繋がるのか?といった疑問や質問は来ず、純粋に実装手法についての議論から入ることができました。

f:id:eller:20210826152424p:plain
パフォーマンスがどの程度具合を説明するためのグラフ

こうして根拠となる事実を自動的に出せるようにしておくと、レビューを受けて実装が変わったときに再確認を行えるという利点もあります。今回の例で言うとキャッシュの解決とJDKのインストールとを並行することでの高速化を意図していたのですが、セキュリティ上の兼ね合いから順次実行するようコードを変更せざるを得ませんでした。このような大きな変更があっても、既に自動化されたパフォーマンス比較手段があったため、パフォーマンス改善が維持できていることを示せました。

考慮すべきことも伝える

変更には利点だけではなく、互換性や運用など考慮しなければならないこともあります。これらをどこまで認識していて、どのような検証を行ったのかを事前に共有することで、多角的な議論の下地とすることができます。

今回の変更では upload-chunk-size が設定できなくなりユーザの自由度が失われること、IvyやGrapeといったより多くのパッケージマネージャをサポートすべきかもしれないことを俎上に載せ、その上でそれらが現時点で問題にならない理由を述べています。

新機能をメンテナンスし続けることのコストを最小化する

新機能追加のためにPRを送る場合、その機能のメンテナンスを行うのが自分ではなく相手だという点を意識する必要があります。言い換えれば、メンテナンスの工数が高いと判断された場合、PRをマージしてもらえない可能性があるということです。

基本的には単体テストと統合テストの双方を実装し提供することで対応します。単体テストがあることで実装自体のテスト可能性を担保し、統合テストがあることでリグレッションに気づける状態を作ります。またユーザに対する説明が必要な機能なら、READMEのような書類もあわせて準備します。

また自己流のやり方を持ち込むのではなく、既存のコードベースに溶け込むコードを書くことも重要です。今回は既にある定数クラスを再利用したり、統合テストのための仕組みに乗ったりという工夫をしました。特に統合テストのやり方はリポジトリによって様々なので、既存のビルド設定やコードに目を通す必要があります。

早めに返信するが、相手には求めない

質問や指摘があった場合、24時間以内には必ず何らかのリアクションを返すようにします。どうせタイムゾーンの問題があるので”即レス”は難しいのですが、できるだけ早めに返すことで議論の沈静化を防ぎます。

逆に、向こうから連絡がなくても気長に待ったほうが良いことも多いです。今回は企業の中の人が業務としてレビューしてくれましたが、それでも2週間ほど返信がなかった期間がありました。自分が相手の立場でもそういう事はあるだろうと気長に待つのが得策です。

変更の理由をオーバー気味に説明する

PRを提案する際、依存先の更新やリファクタリングのような、一見不要に見える変更を入れることがあります。例えばこちらのPRでは、ネットワークエラー発生時でもHTTPリクエストのリトライを行うために typed-rest-client のバージョンを上げています。何も説明しないと、依存を上げるのは他のPRでやればいいのでは?という疑問が湧くかもしれません。

github.com

こうした変更を入れる際は、コミットコメントかPR内コメントでその意図を伝えるようにしています。やはりコードでWHYを伝えるのは難しいので、少しオーバーかなと思うくらいの勢いで伝えたほうが良さそうです。

その他、細かいこと

  • README.md はもちろん、 CONTRIBUTING.mdCODE_OF_CONDUCT.md といったドキュメントに目を通しましょう。
  • Pull Requestテンプレートがある場合は利用しましょう。
  • わからないことはわからないと言い、素直に尋ねる方が良いです。相手もこちらを助けやすくなります。
  • 新しいライブラリの導入など相手に判断を求めるケースでも、自分の考えを述べた上で素直に尋ねるようにしましょう。

以上です。どなたかの参考になれば幸いです。 Happy hacking!

GitPodでJavaプロジェクトを開発する

GitHub Codespacesがなかなか個人向けに来ないので、changelog.comで宣伝していたGitPodを試しています。 どうも公式のJava向けの説明が古いようで、既にDeprecatedになっているtheiaを前提としているため、調べたことをメモしておきます。

最新のJavaを使う

普通に gitpod/workspace-full イメージ内でJavaを起動すると、Zuluの11が使われていることがわかります:

$ java --version
openjdk 11.0.12 2021-07-20 LTS
OpenJDK Runtime Environment Zulu11.50+19-CA (build 11.0.12+7-LTS)
OpenJDK 64-Bit Server VM Zulu11.50+19-CA (build 11.0.12+7-LTS, mixed mode)

SDKMANやhomebrewが入っているので好きなバージョンを入れてもいいですが、ワークスペースを立ち上げる度に実行するのは面倒なので、 azul/zulu-openjdk のようなイメージを使ってしまうのが楽でいいと思います。

# .gitpod.yml
image: azul/zulu-openjdk:16

Mavenの依存をダウンロードしておく

Mavenの場合、dependency:go-offlineプラグインや依存をすべてダウンロードできます。 これをイメージ作成時に実行しておくのが良さそうです。

# .gitpod.yml
image: azul/zulu-openjdk:16

tasks:
  - init: ./mvnw -B dependency:go-offline

Gradleの依存をダウンロードしておく

Gradleには標準的な手法がないので、単にビルドを回しておきます。

# .gitpod.yml
image: azul/zulu-openjdk:16

tasks:
  - init: ./gradlew build

Extensionを導入する

Java向けExtensionを3つ入れて様子を見ています。

# .gitpod.yml
vscode:
  extensions:
    - redhat.java
    - vscjava.vscode-java-dependency
    - vscjava.vscode-java-debug

ポートを開けておく

Spring Framework標準の8080ポートを開けておく場合は ports の設定 で足ります。 が、URLの取得にgpコマンドが必要なので azul/zulu-openjdk ではなく gitpod/workspace-full をベースとしたイメージを用意する必要があります。URLの決定ロジックがいまのところ非常に単純なのでなくてもなんとかなりそうではありますが、一応。

# .gitpod.yml
image:
  file: .gitpod.Dockerfile
ports:
  - port: 8080
# .gitpod.Dockerfile
FROM gitpod/workspace-full
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && sdk install java 16.0.2-zulu"

バッジを付ける

バッジはDiscourseに落ちています。 README.md とかに貼っておくと、Contributorの敷居が下がって良いんじゃないでしょうか。

community.gitpod.io

The 2021 State of DevOps Reportが出た

ということでスキマ時間に読み進めていたので、感想と面白いと感じた点をまとめときます。網羅性が高く公平なまとめが必要な方は、3〜4ページに掲載のExective Summaryをおすすめします。

テーマは「中間層からの脱出」

6ページ目を見てもらえれば一目瞭然、今回のテーマは生産性が高くできず中間層で留まっているチームに対する処方箋の発見にあるようです。

近年のレポートは「今更それは無いでしょ」って感じのLow levelが5%強、「リアルチートじゃん」って感じのHighが10%強で、ほとんどのチームがMiddleに属していました。ごく少数の上澄みが高いパフォーマンスを出していて、その鍵となるKPIもベストプラクティスもわかっているのに、ほとんどの中間層は現状から脱出できず指を咥えて見ていた……と言ったら言いすぎでしょうか。この8割を占めるMiddle levelをさらに細分化し、中間層を脱するための働き方を見つけようということです。

f:id:eller:20210726175415p:plain
DevOps evolutionary levels (State of DevOps Report 2021 p.6 より引用)

DevOpsのやり方がだいぶ浸透してきて「何を計測し何をやるか」から「どうやるか」に関心がシフトしてきたとも言えます。同じ6ページにはDevOpsについて語ることよりも働き方について語るチームが多いことが述べられています:

In fact, many of the teams that are “doing DevOps” well don’t even talk about DevOps anymore—it’s simply how they work.

面白いと思ったところ

  • トップダウンによりボトムアップ型の改革を可能にする (p.11)
    • 経営の協力は当然必要というか「DevOpsによる生産性の向上=経営の関心事」という大前提がこのレポートには感じられる
    • 生産性が低い組織はリスクを避けるためにリスクを増大する手段を選択する傾向にある(p.31)が、これも部下を信じられない・Continuous Deliveryの適用をためらう経営なのではと思った
    • executive summaryによれば何でも話して疑問を解消し論点を明確にできる組織が高い生産性を持つわけで、マネジメントやリーダーの日々のコミュニケーションの賜物なのだろうと思った
  • DevOpsチームの存在は組織の進化を助けず、その曖昧な責務が組織に混乱をもたらす(p.13)
    • 開発とオペレーションの間に独立したDevOpsチームを配置するのはアンチパターンとして知られる(p.13)
  • 高度に進化した組織にとって文化は障害にはならず、これこそが高度に進化した理由である(p.14)
  • Team Topologiesという概念の導入(p.17)
    • Fast flow(p.16)実現にとっての障害を取り除くことに注力するものである(p.29)
    • 役割が多く、小規模のチームに当てはめるのは難しそうだと感じた
    • これを実現するための教育はどうやるのだろう?各チームの役割について専門家を探すのも難しそうな気がする
  • internal platformsについて
    • 2020でも触れられたが更に掘り下げて説明している(p.36)
    • 社内向けシステムを構築する際に利用できる認証・認可・デプロイ・保守・監視に使えるPlatformという理解をした
    • データレイクにおけるデータカタログとかも入ってくるんだろうなという理解をした
    • 高度に進化した組織はこうした基盤をinternal customerのビジネスを理解してエンジニアに構築提供している(p.30)
    • こうしたplatform teamがあっても生産性が向上するとは限らないが、DevOps transformationを加速できる(p.30)
    • 標準やルールではなくサービスを提供するということ、すなわち人をチームを信じることが肝要なのだと感じた
  • DevOpsの進化とinternal platformsの利用状況には関連がありそう(p.18)
    • 知識やベストプラクティスを他のチームと活発に共有することが生産性と関連しており、(p.19) これこそがMid levelを脱出するためのとっかかりになる(p.29)
    • だからこそ「でも保守(conservatism)も必要だよね」というCTOコメントが面白い(p.31)
    • 他チームがデプロイの課題を解決したことについて話す事例(p.28)があり、チームの壁を超えて課題解決や挑戦について共有する仕組みや文化が必要なのだと感じた、成果発表会のような季節性の仕組みでは動作しなさそう
    • Chat、金曜のメール、月次学習セッションといった共有手法の一例が共有されている(p.29)
  • 文化を嘆くのをやめて手を動かせ(p.28)
    • これ個人的にはとても気に入った、文化は個人の持ち物ではなくmutabilityを確信できないので
    • 表層の変えやすく失われやすい変化ではなく、5 Whyで組織文化の根っこを変えよう (p.32)
    • 日々正しい方向に組織の振る舞いをnudge(誘導?)していこう(p.32)

過去ログ

2019と2020の感想はTwitterを漁れば出てきます

Gradleのbaseプラグインに書くべきでないconventionとは何か

ひとつ前の記事では「Gradleの設定やコードをどこに書くべきか」のうち、答えが明確な「build.gradleファイルとbuildSrcディレクトリの使い分け」について書きました。この記事ではまだ自分の中でもよくわかっていない「Gradleでbaseプラグインに書くべきでないconventionとは何か」について書きます。

baseプラグインとは

ここで言うbaseプラグインは、plugins { id "base" } で使えるThe base pluginのことではありません。Gradleプラグインの設計手法のひとつとして推奨されている「設定より規約」の文脈で登場するものです。「設定より規約」を実現しつつ、(大部分のケースをカバーするであろう)規約が適用できないケースでもプラグインの機能を使えるようにするため、プラグインの規約(conventions)と基本的な機能(capabilities)とを別のプラグインに定義する設計手法です。

例えば java プラグインjava-base プラグインの場合、 java プラグインが規約を提供し、 java-base プラグインが基本的な機能を提供します。 java プラグインjava-base プラグインに依存することで、java-base プラグインが提供するtaskやextensionを利用します。

なお公式にはbaseプラグイン「ではない」方のプラグインには固有名詞がないのですが、ここでは記載を簡略化するために「規約プラグイン」と呼ぶことにします。

GradleはConventionという用語を多義的に使っている

ではbaseプラグインに書くべきconventionとは何か。この話題に触れるにあたり、GradleがConventionという用語をいつどのように使っているか確認する必要があります。私の知る限り、大きく分けて以下の3つが存在します:

  1. ”設定より規約”の文脈でのconventionのこと。一般的な用法。
  2. プロパティを指定しなかった場合に暗黙的に採用される値。default valueのこと。
  3. プラグイン設定用のconvention
    • 例えばJavaPluginConventionなど。
    • 現時点では非推奨なため、Extensionを利用する必要がある。

こうなるとよくわからないのが「baseプラグインに書くべきでないconventionはどれ?」です。

1のconventionはプラグインが提供する規約そのものでありbaseプラグインに書くべきでないのは明らかですが、JavaBasePluginCargoBasePluginは2と3のconventionを含んでいます。

規約プラグインであるJavaPluginCargoPluginはconventionを持っていることを期待され、実際1や2のconventionを含んでいます。

非推奨となった3のconventionは脇に置くとしても、2の置き場所やそれを決定する判断基準は明瞭に持ちたいところです。

メソッド名に惑わされないのが吉か?

私も2021年7月現在では明瞭な判断基準は持っていないのですが、ポイントはProperty#convention(Provider)というメソッド名に惑わされないことだと思われます。言い換えれば、2のconventionはどこにでも出てくるものだと割り切り、1のconventionを規約プラグインに限定して書くことのみに注力します。そして機能を書く上で必要なプロパティがある場合、baseプラグインでも気にせずにデフォルト値をProperty#convention(Provider)メソッドで設定してしまいましょう。

公式フォーラムに問い合わせてみた

自分的にはわかった気になれましたが、なんかしっくりこないので公式フォーラムに問い合わせてみました。

discuss.gradle.org

返信もらえたらこちらのブログ記事も更新します。

2021年9月29日更新: 公式Slackの方で「JavaBasePluginが異色なだけ、普通は1と2と3は全く同じものでbaseプラグインには出てこないべき」というコメントを貰いました。確かに一番シンプルなまとめ方かもしれません。

GradleのbuildSrcとどう付き合うべきか

Gradleで複数サブプロジェクトをもつプロジェクトを作成する - kdnakt blog を見て、buildSrcディレクトリ周りで混乱した記憶が蘇ってきました。

ということで「Gradleの設定やコードをどこに書くべきか」のうち、答えが明確なbuild.gradleファイルとbuildSrcディレクトリの使い分けについて現時点での見解をまとめておきます。

宣言的か命令的か

Gradleのビルドスクリプトを分割統治する手法として、昔はscript pluginと呼ばれる手法が使われていましたが、Gradle 7.1.1時点ではbuildSrcプロジェクトを使ってimperative logicを抽象化することが推奨されています

imperative logicというのはつまり”命令口調”なビルドスクリプトのことですね。ビルドスクリプト自身が極力宣言的(declarative)になるように、命令的なスクリプトをビルドスクリプトとは別のところに書いておこうということです。ビルドスクリプトを宣言的に保つことは、Gradleベストプラクティス堂々の一位に位置づけられており、その重要性が伺えます。

そしてimperative logicを隠しビルドスクリプトを宣言的に保つことは、プラグインの説明に書かれているように、プラグインの設計意図そのままです:

What plugins do (omit) Encapsulates imperative logic and allows build scripts to be as declarative as possible

自分が最近書いた例でいうと、チェックサムをGitHub Releasesに掲載するためのタスク定義はTHE・命令的でした。この定義、あるいは抽象化した定義をプラグインとして書いておいてビルドスクリプトに適用することで、ビルドスクリプトそのものを宣言的に保てるということです。

命令的なコードを置くためのbuildSrcディレクト

通常プラグインはGradle Plugin Portalにpublishして使いますが、プロジェクトごとにpublishしていたらキリがありません。そこでプロジェクト固有のローカルな"命令的"スクリプトを置くための場所として buildSrc プロジェクトが存在します。プロジェクトルートに buildSrc ディレクトリを置くと、中のコードをコンパイルしたものをビルドスクリプトのCLASSPATHに置いてくれるのです。

以上のように、全体像がわかってしまえばbuild.gradleファイルとbuildSrcディレクトリどちらを使うべきかは見えてきます。 宣言的なコードはbuild.gradleファイルに、命令的なコードはbuildSrcディレクトリに置きましょう

buildSrcプロジェクトの書き方はmike-neckさんのブログに詳しいのでオススメです。プラグインテスト手法も確立しているので、ビルド性能だけでなくビルドスクリプト管理の生産性も上がるといいですね。

mike-neck.hatenadiary.com