Kengo's blog

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

考察:Reactive Workflowが生まれた背景とその狙い

人に説明するのがスムーズにできなさそうなので、理論武装というか順序立てて話すためにこの記事をまとめる。

対象

TL;DR

  1. 状況やベストプラクティスが目まぐるしく変わる現代において、すぐに変化できるソフトウェアを保つこと・ヒトの手をできるだけ空けることが重要。
  2. かつてIaaSがAPIを提供し環境管理の多くを自動化したように、各種サービスがAPIやWebhookを通じてDevelopment Workflowの多くを自動化してきている。
  3. 多くの視点や知見を活かすcross functionalチームによる共同開発を支えるために、コラボレーションを助ける仕組みも必要。

前置き:課題解決手法としての生産性向上

我々はなぜ生産性向上を目指すのか? それは生産性が十分ではないという課題意識や危機感を持っているからだろう。では、なぜ課題意識を持ったのか? 機能の実装が進まないとか、バグが多いとか、手戻りが多いとか、本質的でない業務が多いとか、現場によって多くの理由があると思われる。そしてそれを深堀りする前に他者とざっくり課題意識を共有するための言葉として「生産性を向上しないとね」といった表現が用いられる。 言い換えれば、生産性向上というのは現場の抱える課題に対する抽象的な解決策である。ので「生産性を向上するには」などと議論しても、地に足をつけた議論にはならない。「なぜ生産性が低いのか」を掘り下げて、その解決策を見出す必要がある。

ここで重要なのは「なぜ低いのか」は比較対象を知らなければ説明できないことだ。それが他のチームが実際に回している開発なのか、スケジュールから逆算した「理想生産性」なのかは課題ではない。そうした「あるべき姿」が共有されてはじめて比較ができ、KPI(モノサシ)が生まれ、議論できるようになる。これは開発手法や組織、プロセスなど、どこに課題がある場合でも同じだ。

これはこのブログで繰り返し出てきているproblem solvingの考え方であり、真新しいものではない。単に課題の設定と共有をきちんとしようという話である。課題が設定されてはじめて、課題解決の進捗や施策の妥当性を評価できる。Development Workflowに手を入れる前に、本当に必要なものがそれで手に入るのか確認する必要がある。

Development Workflow

私はDevelopment Workflowは自分の専門というわけではなく、またウォッチャーを自任できるほど時間をかけてトレンドを追ってもない。いちエンジニアの理解であり何かを代表するものではないことを、はじめに改めて強調しておく。

またここでDevelopment WorkflowというのはCI, CD, DevOpsといった文脈で出てくるパイプライン処理、特に確認や承認や手動テストという非自動化処理をも含めた一連の流れを指している。GitHub ActionsCircle CIでは単にWorkflowと呼んでいるが、ここでは業務向けワークフローシステムと区別する目的でDevelopment Workflowと呼ぶ。Development Workflowの「終点」は成果物のデプロイだけでなく、チームへのフィードバック=学習機会の提供であることも多い。

かつてDevelopment Workflowはどのようなものだったか

自分が10年前にどういうDevelopment Workflowを組んでいたかというと、概ね以下の通りだったように記憶している(便宜上HudsonをJenkins*1と表記):

  • trunk, branches, tagsを備えたSubversionによるコードのバージョン管理(ブランチ切り替えコストが高い)
  • 設定画面を利用したJenkinsジョブの作成・設定
  • ブランチを指定して実行するJenkinsジョブを開発者に対する公開
  • プラグインによるJenkinsの機能拡張
  • Mavenの親pomファイルを利用したプロジェクト管理
  • LANに閉じた開発環境
  • 環境構築や開発作業の手順書をWiki等でメンテ(.jarファイルの手動配置、テスト手順、IDE設定方法など)

自動化されているのはジョブと呼ばれる単発作業のみで、いつどのように実行するかは主にヒトに委ねられていた。出荷作業をするにはこのジョブをこういったパラメータで実行すること、という手順書が必要だったのである。VCS上での変更を知るにはpollingを必要としていたし、他にもマスターサーバの責務は多くある程度の性能を要した。また結果をヒトに通知する手法はメールが主体だったので、何かを強制することは難しかった。

当時はまだビルド職人という言葉があったが、それは成果物の作成を特定の環境や個人でしか行えなかったり、ジョブ実行時間が非常に長かったりしたことを反映している。実際ビルド環境を可搬にすることはまだ難しく、また手順書が必要な現場も多かったのではと推測される。

近年Development Workflow周りで見られる変化

逆に、当時に無くここ10年で開発され普及したものを思いつく限り列記する:

  • ブランチを利用しやすいVCS(Git)
  • 画面による設定ではなく、VCSによって管理されたファイルよる設定
  • CIパイプライン
  • インクリメンタルビルド
  • 複数ノードにおけるテストのparallel実行
  • プロジェクトごとに利用するJDKを切り替える仕組み
    • rvm はあったようなので、他言語には実行環境をプロジェクトローカルのファイルで指定する仕組みはあった?
    • 開発環境にはあらかじめ複数のJDKを入れておくことで、Toolchainを使って使うものを指定する仕組みはあった(どの程度普及していたのかは不明)
  • DisposableでReproducibleな、頻繁に壊して作りなおせる環境
    • Vagrant Sahara pluginが出たのが2011年
    • Boxenが出たのが2012年
    • Dockerが出たのが2013年
    • ChefとPuppet, EC2にAMIは10年前既にあったので、自前でコード書いて実現する手はあったと思う(自分はやった記憶がない)
  • CLIから実行できる、設定がVCSで共有可能で環境非依存なコードフォーマッター
    • gofmt, rustfmt, Prettier, Spotlessなど
    • コードフォーマットが個人の環境・設定に依存する必要性をなくし、差分レビューを容易にした
  • チケット中心主義の緩和
    • PRで変更を提案し、その中で議論・修正できるようになった
    • 不具合も再現するテストをPRで送り、同一PR内で修正しマージできる(チケットによる報告も残ってはいる)
    • GitHub Releasesなどでタグに対して情報を残せるのでリリース作業もチケット不要
    • チケットを作って議論を尽くしてからコードを書くケースはもちろん残っているが、関連作業の記録・集約のためにまずチケットを作るというフローはほぼ不要になった
    • チケット管理システムの責務から変更の記録が外れ、要件の記録に注力できるようになった
  • 他システムとの連携を前提としたAPIやWebHook、アクセストーク
  • 運用を肩代わりし本質に注力させてくれるSaaS
  • ベンダ間で統一されたインタフェース(WebDriver, JS, CSSなど)

多様にわたるが、その多くがビルド時間の短縮や環境管理を楽にすること、システムを”繋ぐ”ことに貢献するものである。

10年前と比較して、どのようなニーズの変化があったのか

技術の進展というのはあとから振り返ると「なんで今までなかったんだっけ?」と感じるものが多いが、Development Workflowを構成するサービスの成長にも同じことを感じる。 例えば10年前はヒトに使われることを前提にGUIを通じて提供していた機能の多くが、サービスから使われることを前提にWebAPIやVCSで管理されたファイルによって使えるようになった。しかしGUICUIどちらが技術的難易度が高いかと言うと、一般的にGUIだろう。GUIを提供できるがCUIはできない、というケースはあまりないはずだ。

ではなぜ10年前はGUIを中心としていたのか?それは(特にJenkinsについては)利用者に対するハードルを下げた面もあると思うが、手実行することがあまり問題にならなかった、設定や運用をオープンにし誰でも触れるようにするモチベーションが高くなかったことが要因として挙げられる。もっと言えば、当時は1日に数回のビルドで十分にビジネスを回せたのだ。DevOpsの草分けとなったFlickrの発表がちょうど10年ちょっと前*2だし、おそらく当時はまだ多くの組織で「開発と運用の対立」「出荷前検証の重視」が残っていたはずだ。Flickrを始めとした多くの組織で、インフラのコード化や共有されたVCSリポジトリがいかに生産性へ貢献するか、注目された頃とも考えられる。

つまり10年前と今の大きな違いは、開発における試行錯誤の速度だ。自動テスト、継続的デプロイ、カオスエンジニアリング、コンテナなどの様々な技術が生まれたことで、ソフトウェアがより素早く変化するものになっている。またソフトウェア開発の現場に経験主義の考えが強く根付いていることや、スタートアップとソフトウェアの相互の結びつきから、ソフトウェアが素早く変化できることがビジネスで有利に働くと強く信じられるようにもなっている。DevOpsやSREが生まれた背景もそれを裏付けているように思う。

速度が違うとは、どういうことか。例えば10年前の私に「Gradle Pluginの開発でテストに使うGradleのバージョンを増やしたい、なぜこんなことをJenkins管理者にお願いしなければならないのか」と言ったところで、「え、メール一本だし別にいいじゃん、何気にしてるの?」と不思議がられるに違いない。今はこれもPRひとつで実現可能だが、当時はそうではなかった。
他の例では、Mavenのバグを回避するためにバージョンを上げるとして、Jenkins管理者にお願いしてCI環境のバージョンを上げるだけでなく、各開発者の使っているMavenのバージョンまで上げるにはメールでバージョンを上げるよう依頼しつつmaven-enforcer-pluginで意図しないバージョンでビルドした場合に落ちるようにする必要があるだろう。さらに複数プロジェクトで実施するにはすべてのプロジェクトのpom.xmlを書き換えるか、あらかじめ親pomを作成して各プロジェクトに依存させ、maven-enforcer-pluginの設定を親pomに加えてprivate maven repositoryにdeployしてから各プロジェクトのpom.xmlを書き換える必要がある。文が長くてわかりにくいが、とりあえず面倒なプロセスが必要ということが伝われば良い。これもMaven Wrapperを使うようにすればPRひとつで済む。ミドルウェアについても同様で、Dockerfileなりdocker-compose.ymlなりAnsibleなり、何らかのファイルを少し変えるだけで意図したバージョンが確実に使われるようにできる。

こうして製品ないし開発環境を変えることがPRひとつでできるようになっただけでなく、マージ前のプロセスの簡素化・短縮化も図られるようになった。Tracでチケットを作って手元で対応するパッチを作ってReview Boardでレビューして問題なければパッチを適用するという複数のシステムを行き来する方法から、検証済みマージやPull Requestといったひとつのシステムで完結する方法へと変化した。またPre-merge build自体の実行時間も各種技術によって短縮されたし、ミドルウェアを使った統合テストも使い捨て環境を作りやすくなったので実行したいときに他のテストとの衝突を気にせず実行できるようになった。気にすることが減るというのは、Pre-merge buildでいろんなことを自動的に確認・検証したいというニーズを満たす上で必要だ。統合テストを回して互換性が壊れていないことを確認したいのに、DBが他のテストで使われているので待つ必要がある、なんて事態は避けたいのだ。

10年前と今の「生産性」を比べたときに、この開発プロセスの違いは大きな差を生むに違いない。

自動化、有機的連携、そしてリアクティブワークフロー

別の表現をすると、10年前は省力化のために作業の自動化が推奨され、それで充分に競争力を生んでいた。その後、自動化された作業を有機的に繋ぎ合わせるようになり、CIパイプラインが登場した*3。その後でWebAPIやWebHook等の整備により、PRやcron以外でもパイプラインを自動発火することが増え、リリース作業やデプロイ、ドキュメント整備など多様な使い方をされるようになったと言えるだろう。 近年は脆弱性対応の観点から、ライブラリのリリースや脆弱性の公開をイベントとして使えるようになってきている*4が、これも従来なら開発者が各ベンダからの情報をRSSSNS等を使ってウォッチする必要があった。

さてジョブの並列実行・並行実行ならびに有機的連帯についてはPipelineやWorkflowという用語が存在するが、このヒトの指示による実行ではなくイベントに応じた実行を意識した組み方については特に固有名詞がないように思う。イベントを受け取りステートレスな要素をつなぎ合わせて実行する様がリアクティブシステムの考え方に近いことから、ここではリアクティブワークフロー(Reactive Workflow)と呼ぶ。

なぜリアクティブワークフローが好ましいのか、その説明はReactive Manifestoが流用可能だ。使いたいときにすぐ使え(Responsible)、ジョブがステートレスなため壊れても再実行でほぼ対処でき(Resilient)、ビジネス上の要請あるいは技術的な必要性から生まれる突発的な負荷に対応する(Elastic)。また各ジョブの入出力がコミットハッシュやパラメータなど不変なものがほとんどであることと、ワークフロー基盤がサーバーレスアーキテクチャで構成されることが多いことから、メッセージ駆動のメリットも享受できると思われる。

おそらくユーザ(開発者)としてはResponsibleであることが最も重要だ。JenkinsのマスターやエージェントをLAN内に構築していた(サーバーレスでなかった)ころは、資源をうまく使うように夜間に定例処理を回したり週末にテストを回したりといった工夫が必要だった。これはリアクティブワークフローを組み、資源の管理をSaaSあるいはIaaSに押し付けることで、必要なときに必要なだけ実行し即結果を受け取れるようになった。Elasticであることも合わさって、開発者が本質的でない気遣いを排し(ビルド並列化などによって)開発速度を上げることに貢献できるわけだ。

こうしてみると、コンテナ(ジョブはコンテナ内で走ることがある)やサーバーレスといった新技術によって最近のDevlopment Workflowも支えられているのだなと感じる。

コラボレーションの重要性

近年の開発手法、例えばSREやDevOps, Lean, cross-functional teamについて紐解くと、個人の役割を明確化・細分化すると同時に組織のサイロ化を防ぐことに多大な関心を持っていることが伺える。著名なのがError Budgetで、Reliabilityの実現を目的としたSREと多様なデプロイを多数行う(ことによるプロダクトの改善)を目的とした開発という一見相反する行動原理を持つ役割をうまく「同じ課題を解決するチーム」へと仕立て上げ、サイロ化を防いでいる。詳しい話は書籍に譲り割愛する:

他にもプロダクトマネジメントトライアングルでも、立場が異なるヒトをいかに連帯させ機能させるかという関心ごとについて説いているが、その議論の中心はProductという誰もが共有しているモノにある(と私は解釈した)。デザイナーは開発者の生み出す価値をユーザーに届けるためにどうデザインすべきか、経営は開発資源を効率的に使うためにどうマネジメントすべきか、開発者はユーザに製品の魅力を伝えるためにどうコミュニティを築くべきなのか。そのすべてのコラボレーションが製品というOutputを通じてOutcomeを生み出す。開発、ビジネス、ユーザのどれを取っても独立しておらず、すべてがProductや他のロールと繋がっている。プロダクトマネジメントはヒトのコラボレーションを認めてはじめてできるものだ。

もうひとつ、面白い観点にロックスターエンジニアというやつがある。有能なエンジニアはそうでないエンジニアの10倍、あるいは27倍の生産性を持っているというold termだ。これもたぶん昔は説得力のある概念だったのだろうが、近年では全く使われなくなっている。代わりにtechnology raderでは10x teamという表現を紹介していて、素晴らしいoutputが素晴らしいチームによって生まれるとしている。これはGoogleが研究で明らかにしたチームやマネジメントの重要性によっても肯定されていると言えよう。

つまり近年のソフトウェア開発の現場では、個人を育てること以上にチームを育てることに関心を持っている。個人を育て組織を開発することで、ビジネス上の競争力を効率的に高めていけると信じているわけだ。 チームを育てるには様々な手法があるだろうが、成長が内省によって生まれることを考えればBlameless Postmortemのような事実と向き合い理性的な議論を通じてより良い姿を模索する活動には一定の価値があると思われる。失敗時に限らず日頃から反省の機会を持つこと、Scrumで言うSprint Retrospectiveのような定例イベントも助けになるだろう。

さてこの手のイベントをやってみるとわかるのが、準備のコストが高いことだ。Postmortemのためには事実をリアルタイムに記録しなければならないし、ベロシティを測るには日々チケット消化の状況を記録する必要がある。自分とは違う専門性を持つチームメイトに向けて資料を整理することもあるし、わかりやすくするためにグラフを整理することもあるだろう。定期的なイベントの準備をすべて人力でやっていては、継続的にコストを払うことになる。

Development Workflowはこのコストを下げることができる。人手を使って証跡を残していた運用を、自動的にログを残す運用に変えられる。ChatOpsやGitOpsが最もイメージしやすいだろう。 また自動化やSaaSなどによって、相手の専門性に合わせたビューやファイル形式で情報を提供するコストも下げられる。例えばデザイナの要請を受けてCSSを変更したときに、それが既存ページにどういった影響を及ぼすか、percyのようなGUI regression testingが整っていればスクリーンショットを取ること無くPR一本で説明できる。

まとめ

とりとめのないままに書き下した結果、ひどいことになってしまったが。

結局は「cross-functional teamとして高速度で成果を出す」という時代の要請に自動化やコンテナ、サーバーレスといった技術が応えた形がいまのリアクティブワークフローなのだと思う。機械学習や業務改革の文脈で「機械ができることは機械にやらせて、ヒトはヒトにしかできないことをやろう」と言われるが、開発プロセスにそれを適用したものがこれだ。 事実と向き合い考えることこそが、ヒトにしかできないことだ。事実(ログ)の集約や解析、テストやビルドといった開発作業、ライブラリの更新や脆弱性報告の精査といった定例作業はすべてリアクティブワークフローにやらせる。私達は上がってきたデータをもとに考え、議論し、内省し、立てた仮説をもとに次のPRを作る。Gitにpushすれば機会が変更の妥当性をまず検証してくれるので、ヒトは既知の問題が無いことを前提に議論を尽くすことができる。タブ文字かスペースか、括弧の位置はどこにするかなんて議論をPRでする時代ではもう無いのだ。

もちろんここで議論したのはある種の理想形で、すべてのプロジェクトがこれを満たせるとは限らない。私が見ているFOSSプロジェクトにも、まだ手動で色々とやらなければリリースすらできないものもある。また10年前でも既にリアクティブワークフローを回していたところもあるかもしれない。

*1:このエントリではJenkinsが「古いWorkflowシステム」の代名詞として使われている節があるが、それは単に筆者個人の経験がそうだっただけで、Jenkinsというプロダクトがレガシーというわけではない。Cloudbees Flowなどを参照

*2:この点で、本考察はここ10年ではなく13〜15年とかで切ったほうが多くを学べるのかもしれないが、その頃は私が技術的情報収集をしていなかったのでよくわからない

*3:時期はよくわからないが後発のJenkins 2.0が2016年4月なので2014〜2015くらいか?

*4:GitHubこれとか、試してないけどSonatypeのこれとかjFrogのこれとか

SpotBugs 4.0.0がリリース&貢献者募集のお知らせ

.classファイル向け静的解析ツールであるSpotBugsのバージョン4.0.0がリリースされました。 FindBugsとの互換性を極力保っていた3.1系と異なり、いくつかのAPIが変わったり機能が削られていたりします。 特にプラグイン開発者は、SpotBugs 3.1 から 4.0 への移行ガイドを確認するようお願いします。 一応ユーザに使われていない機能を調べて削ったので、一般ユーザに対する影響は限定的なはずです。

なおJava 9以降にはまだ対応していません。コミュニティで議論された4.0でやりたいことには含まれていたのですが、貢献がありませんでした。 ObjectWeb ASMやApache BCELの更新に追随しているのでコア機能は動作しますが、標準DetectorのうちNullness周りをはじめとしたいくつかのDetectorが動作しないことが知られています。のでJava9移行のプロジェクトでもSpotBugsの利用が必要な場合は、オプションで利用するDetectorを絞ってください。詳細は以下のissueを確認すると良いでしょう:

なお最近はIDEAプラグインの実装に動きがあるので、SpotBugsを使いたいIDEAユーザはwatchすると良いかもしれません。 Gradleプラグインもフルスクラッチで書き直し中で、正式リリースも近いのでGradleユーザは是非試してみてほしいです。

SpotBugsは貢献者を求めています

さて本日、サイボウズさんのブログでOSSに貢献する利点や手法が紹介されています:

そしてSpotBugsは知名度もあり、本家のFindBugsよりも使われているプロダクトとなりましたが、まだ貢献者が少ない状態です。コードが複雑なのは難点ですが、貢献を始めるには良いプロジェクトかもしれません!ぜひ覗いてみてください!!!

チームには私の他に、issueでの返信やコードレビューを担う人、Eclipse PluginやMaven Pluginに特化した人がいますが、間に落ちている問題もJava9対応に限らず多くあります。また直接コードに貢献しなくても、使い方や利用状況をブログやSNSで発信するだけでも他のユーザの参考になります。どういう形でも構わないので、ぜひ検討してみてください。コミュニティの言語は英語ですが、日本語のわかる私を貢献の取っ掛かりに利用していただいても構わないです。Twitterやメールで連絡ください。

なお継続的に貢献いただいた方には、Organization参加要請を出していきます。実績としても、2019年には2名をOrganizationに招待しています。お邪魔でなければ受けていただければ幸いです。一見リリースが少ない不活発なプロダクトではありますが、チーム自体はissueだけでなくGitHub Teamを通じてコミュニケーションを取っていますので、招待や議論は問題なく行えます。よろしくお願いします。

あわせて読みたい

WIP: 2020年やりたいこと50

2020年やりたいこと100 - ややめもを見て良い取り組みだと思ったので書いてみたのですが、100も出てこないのでとりあえず半分でお茶を濁す作戦。

健康(1~10)

  1. 週2回の筋トレを継続
  2. 平日は10,000歩以上歩く
  3. 体重を72kg±2程度に抑える
  4. 24時までに寝る
  5. 平日のコーヒーをやめる
  6. 水やお茶を1日2リットル以上継続して採る
  7. 朝ごはんの菓子パン依存率を減らす、食パン+野菜や肉などで代替する
  8. 皮膚のプロアクティブ治療を継続する
  9. 暗いところでスマホで読書する習慣をなくす、視力低下を防ぐ
  10. 平日の昼寝を継続する

育児(11~20)

  1. 息子氏がひらがなを読めるようにする
  2. 息子氏を新幹線に乗せる
  3. 息子氏の小学校を決める
  4. 息子氏の中学校以降を検討する
  5. 絵本を継続して読みきかせる
  6. 毎朝30秒のハグを息子氏が嫌がらない限り継続する
  7. 幼稚園への登園中に日本語で息子氏にどんどん話しかける
  8. 息子氏を動物園や水族館、スポーツの試合に連れて行く
  9. 息子氏の友人一家と遊ぶ
  10. 日本語を話す息子氏の友人を作る

家庭(21~30)

  1. 家族と中国国内旅行に行く
  2. 家族と日本国内旅行に行く
  3. 音声認識コントローラを導入する
  4. 無駄を省き効率化を進めることで、平日夜の時間を創出する
  5. 平日朝の忙しさを緩和する(≒息子氏をきちんと起こす、息子氏の朝食や着替えをきちんと済ませさせる)
  6. 投資や副業などで収入を増やす
  7. 映画や美術館、新しいショッピングモールなどに行く習慣を作る
  8. そのための日常的な情報収集を行う(WeChatなど)
  9. 実家との継続的な連絡を取る
  10. スケジュールや心持ちに余裕を持つ、息子氏の突発的な風邪などを常に想定に入れる

趣味(31~40)

  1. Skebでらきすけさんにライチュウを描いてもらう
  2. SpotBugs Gradle plugin v2をSpotBugs Organization下でリリース
  3. SpotBugs 4.0.0安定版をリリース
  4. Sphinxに3つ貢献する(docker imageとか)
  5. n月刊ラムダノートに寄稿する
  6. JavaプロジェクトをひとつKotlinで書き直す
  7. VRゴーグルを導入する
  8. 週1冊の本を読む
  9. GitHub sponsorsに登録する
  10. ブログ記事を6つ書く

業務(41〜50)

  1. 部署の売上目標をつくり達成する
  2. 部署の利益目標をつくり達成する
  3. 部署の技術目標をつくり達成する
  4. 個人の技術目標を策定・達成する
  5. 職場のモニタを新調する
  6. 職場のポインティングデバイスを新調する
  7. 日本の技術イベント登壇
  8. 上海の技術イベント登壇
  9. 3回ほど社内技術勉強会で公開セッションをする
  10. M5StickCで職場用ガジェットを作る

2019年のFOSS活動状況まとめ

昨年のに引き続きFOSS活動状況をまとめます。2019年12月30日時点の情報です。

概要:昨年比22%増

GitHubのプロファイルページによると今年のpublic contributionsは1,166で、昨年が950だったので約22%増です。commit 61%のpull requests 21%なので、けっこう手を動かしてコードを書けたと思います。業務でのpublic contributionsはゼロなので、すべて趣味開発です。

f:id:eller:20191230233035p:plain
Contributions at GitHub in 2019

今年新規に作成したのは主に gradle-semantic-release-plugin, open-the-sesamespotbugs-gradle-plugin-v2 です。gradle-semantic-release-pluginについては3月に紹介記事も書いています。

7〜9月の Contributions が減っているのは勤務先における異動による影響です。これがなかったら1,500くらい行けたかもしれません。

SpotBugs周りの開発

2019年もSpotBugsが一番活発な開発でした。コア開発58 commitsと昨年比で減っていますが、SonarQube pluginGradle pluginは横ばいという感じです。

f:id:eller:20191230235144p:plain
Contributions for spotbugs/spotbugs in 2019

特に現Gradle PluginがGradle本体からのforkなため、internal APIに依存していたりAndroid開発へのケアが抜けていたりという課題が多かったので、半年ほどかけて準備をしスクラッチで書き直しています。Gradle Pluginポータルにはデプロイしたので、承認されしだい利用可能となるはずです。

今年の目玉はSpotBugsのダウンロード数がFindBugsのそれを上回ったことですね。あれから苦節3年弱、継続は力なりを体現した感じです。リリースもそうですがマニュアルサイトの作成と管理も結構関われているので、嬉しく思います。

その他

ひとつプロジェクトに関わらせていただいているのですが、まだ公開できる状態ではなさそうです。あまり積極的に貢献できていないとはいえ意義のあるプロジェクトですので、機が熟したらいろいろと紹介していければと思います。

JavaウェブアプリプロジェクトにJavaScript/TypeScriptなどの静的アセットをどう配置するか

以前のJavaウェブアプリ開発では、JavaScriptをはじめとした静的アセットはsrc/main/webappディレクトリに配置するのが普通だった。そこに置くことでmaven-war-pluginのようなビルドシステムが.warファイルの中に突っ込んでくれる。この挙動は今でも変わらないが、src/main/webappディレクトリに静的アセットを直接置くにはいくつかの問題がある:

  1. TypeScriptコンパイラやBabelのような、静的アセットに事前処理を施す手法が普通になった。
    • src/main/webappに処理後のリソースを置くようにもできるが、mvn cleanなどで処理後のリソースが削除されるようにする手間を考えるとtargetディレクトリ直下にあるwebappDirectoryに置くのが無難と思われる。
    • よってsrc/main/webappには事前処理を必要としないアセットのみを置くことになるが、次に挙げる理由からそれも必要なくなってきている。
  2. フロントエンド開発が複雑化し、サーバサイドと同一プロジェクト・モジュールで開発することが難しくなった。
    • ここで言うフロントエンドはサーバからのレスポンスを受け取ってユーザ向けにインタフェースを提供する部分。HTML5アプリだったりモバイルアプリだったりする。
    • フロントエンド開発で使うビルドツールやライブラリ、development workflowは必ずしもJavaのそれと一致しない。例えばブラウザやOSのプレビュー版が出たときにフロントエンドだけテストを回すといったことができればより高速に検証できる。またJavaScriptライブラリはJavaライブラリ以上にこまめなバージョンアップが必要となるケースが多い(脆弱性対応とか、OSアップデート追随とか)ため、フロントエンドを独立に更新していけるプロジェクト体制を整えることが必要になる。
    • frontend-maven-pluginを使ってJavaのビルドツールに統合することは可能だが、そもそも開発者が異なるケースではあまり意味がない。OpenAPIやgRPC、GraphQLなどのインタフェース定義をフロントエンドとサーバサイドの間に入れて独立に開発していく体制を組むほうが良い。
  3. 静的アセットはTomcatのようなサーブレットコンテナではなく、Amazon S3のようなストレージサービスやCDNによって配信することが好ましい場合が多い。
    • 例えばGoogle Cloudのドキュメントでは、アプリによる静的アセットの配信をstraightforwardだが欠点のある手法として紹介している。

この問題を解決するために、大きく分けて3つの手法を紹介する。

1. 同一プロジェクト内で2つのビルドツールを併用する

サーバサイド開発にはMaven/Gradleを、フロントエンド開発にはnpm/Yarnを利用するが、プロジェクトは分割しない手法。

この手法1.を採る場合、アセットは .war.jar に同梱する形が最もやりやすい。例えばSpringだと同梱されているリソースをStatic Resourcesとして配信可能。懸念されるサーブレットコンテナの負荷低減は、HTTPレスポンスのキャッシュやCDNによって実現することになる。

1.1. サーバサイドをMaven/Gradleで、フロントエンドをMaven/Gradleで包んだnpm/Yarnで開発するケース

サーバサイドとフロントエンドを同一のビルドライフサイクルに乗せるため、frontend-maven-plugin/frontend-gradle-pluginを使って、Maven/Gradleからnpm/Yarnを実行することになる。フロントエンド開発の複雑さがあまりない場合、Java開発者がフロントエンド開発も兼ねる場合に重宝する。例えばsrc/main/typescriptにTypeScriptを、src/main/sassにSassを置くような体制。

プロジェクトルートにpom.xmlsettings.gradleを置いてフロントエンド用モジュールをMavenのサブモジュール/Gradleのサブプロジェクトとして扱うこともできるが、そこまでやるならば次に挙げる手法2で充分であろう。

なおフロントエンドの成果物をMaven Repositoryにzipしてデプロイし、他プロジェクトから利用するということを以前試したことがあったのだが、zip/unzipを多用し性能が出ないのでおすすめしない。普通にファイルコピーで済ませるために、利用者(サーバサイド)と同一プロジェクト(Gitリポジトリ)に置くようにしたほうが良い。

1.2. サーバサイドをYarnで包んだMaven/Gradleで、フロントエンドをYarnで開発するケース

逆にYarnを主、Maven/Gradleを従とする手法。サーバサイドが小さいときは使えるかもしれないが、そこまでするなら手法3が素直。

2. サーバサイドとフロントエンドでプロジェクトを分ける

各モジュールの開発技法において、自由度を確保することを意識した手法。それぞれ異なる開発者が請け負う場合にはこういった形を取ることが多いのでは。Gitリポジトリを分けるかmonorepoにするかという点も考慮が必要だが、自分の開発経験(小型中心)だと分割の必要性を感じたケースは無いので、ここではmonorepoという前提をおいて考える。またフロントエンド=ウェブアプリとし、モバイルアプリ開発については触れない。

サーバサイドとフロントエンドのつなぎ目には、OpenAPIなどのAPI定義手法を利用する。サーバサイドとフロントエンドを突合する部分(CD、プロビジョニングなど)が複雑化する可能性はある(例えばspring-boot-vue-exampleではserverとclientのデプロイにスクリプトを使っている)が、多くの場合でさほど問題ないのでは。プロジェクト構成例は以下の通り:

interface/
  openapi.yml

server/
  src/
  pom.xml // 最終成果物は .war, executable .jar あるいはコンテナ

frontend/
  src/
  package.json // 最終成果物は .zip あるいはコンテナ

docker-compose.yml // あるいはTerraformとかCloudFormationとか?
README.md

この場合、フロントエンドの成果物はサーバサイドの成果物に含めない。またフロントエンド開発にサーバサイドの挙動をmockする必要があるが、Open APIならmock serverを利用できるし、その他のAPI定義手法でも似たものがあるはず。

3. サーバサイドをNode.js化するという選択肢

これはタイトルに全く沿わない手法であるが、真面目にありがちな話だと感じている。つまりJavaJavaScriptという異なる言語を同一プロジェクトで使用することが複雑さを生むならば、その原因を取り除けないだろうか?ということである。

そもそもNode.jsが発展しTypeScriptのような型を使った開発も可能な現代において、サーバサイドとフロントエンドで異なる言語を使うモチベーションがどの程度あるだろうか?

サーバサイドをNode.jsにすれば、プロジェクトの複雑さを除くことができる。性能に直結する非同期処理もCompletableFutureRxJava、spring-webfluxで使われるReactorに比べればJavaScriptasync/awaitの方が簡潔になるし、何よりJavaScriptは言語自身が「メインスレッドをブロックしない」ことを前提に設計されているので、うっかりblocking処理をサーバ内に書くことを避けやすい。 もちろんJavaの方がやりやすいこともたくさんある。またJVMの進展は非常に目覚ましいものがある。運用の知見など積み直しになるものもあるため即置き換えとは行かないし、する必要もない。結局道具は適材適所なので、Javaエンジニアだと自分を狭く定義するのではなくて、JavaもNode.jsもTypeScriptも学んで使いこなせばいいし、KotlinやDartのような新しい言語とそのコミュニティにも目を向けていければ良いのだと思う。

まとめ

小型のプロジェクトでは手法1.1、専任のフロントエンド開発者がいる場合は手法2を使うことをまず検討すると良いだろう。

Javaプロジェクトにおけるリリース周りの手法あれこれ

考慮する点

成果物のデプロイ

ビルドの成果物(artifct)をアップロードすること。アップロードと公開は分けて考えることに注意。デプロイ先にはいくつか候補がある:

  1. GitHub Packages (旧GitHub Package Registry)
  2. Maven Central Repository
  3. Docker HubなどのDocker Registry

GitHub Packagesはコンテナも.jarもまとめて置けるが、コミュニティ標準の場所ではないので利用する際にひと手間必要になる。プライベートプロジェクトの場合は積極利用することになりそう。FOSSなら基本的にMaven Centralに置くことになる*1。プロジェクトによっては.jarファイルとしてではなくコンテナとしてデプロイすることもあるだろう。

リリースノートの作成

CHANGELOG.mdsrc/site以下のファイルを手動でメンテナンスする手法だけではなく、コミットコメントやPRからリリースノートを作成する手法もある。

  1. 手動管理
  2. CHANGELOG.md などファイルの自動生成
  3. GitHub Releasesによるリリースノート自動生成

特に開発者向けであれば、CHANGELOG.mdではなくGitHub Releasesを使うことで、変更内容の伝達を自動化できる。例えばDependabotは依存先の更新を提案する際にリリースノートの内容をPRに含めてくれる。ファイルの頒布が必要な場合でも、GitHub Releasesも合わせて使ったほうが良い。

バージョンの管理

利用者の利便性のため、Semantic Versioning 2.0を使うことが前提になる。ただ混乱を避ける意味でもバージョン 0.x.x の利用は避けたほうが良いかもしれない

  1. 手動管理
  2. 自動的に決定する

バージョンを決める作業自体は自動化が進んでいて、Conventional Commitsから生成するものがsemantic-releaseだけでなく様々あるので使ったほうが良い。私はプラグイン機構がしっかりしているのでsemantic-releaseを使っている。

プロジェクトのバージョンをpom.xmlbuild.gradle, gradle.propertiesといったファイルで管理するのが普通だが、最近はGitのTagを見るものもある。Tagを見ることで「ファイルに書いてあるバージョンをインクリメントする」ためのコミットが不要になるのが利点。例えばDraft Releaseを本Releaseに昇格させる(ブランチの最新コミットにタグを打つ)ことで公開処理を回す(tagイベントに紐付いたワークフローを回す)処理がシンプルになる。のだが、SNAPSHOT運用と相性が悪いのと、ファイルに書かれたversionの信頼性がなくなるのとで、あまりJavaプロジェクト向きではないと考えている。

各手法について

Maven (maven-release-plugin)

mvn release:prepare && mvn release:performを実行するだけで必要な処理が完結する。Maven公式の機能で完結し、歴史も長いのでノウハウも蓄積されている。

シンプルだが、Tag作成をリリースの起点にできない(Tag打ちがプラグインによって行われる)ために自動化には向かない。多くの場合、リリース用の(Jenkins)Jobを作って手動で蹴るような運用になるのでは?pom.xmlの更新とGitへのpushがプラグインによって行われることもあり、自動化の際はプラグインによるコミットメッセージに[skip ci]を含めたり設定を変更したりといった工夫が必要になる。

Maven (maven-deploy-plugin)

Mavenでリリースを自動化する場合はmaven-deploy-pluginによるデプロイ手法を採ることになる。特にmaven-versions-pluginのsetゴールと組み合わせることが多いはず。

リリースに使用するバージョンをTag名などを経由してnewVersionプロパティに渡してversions:setを実行、deploy:deployでデプロイした後に再度versions:setでSNAPSHOTバージョンに変えてGitにpushする運用。release-drafterが使えるのでリリースノート管理もしやすい。

この場合でも、Tagが打たれたrevisionのpom.xmlには実際にリリースされたversionとは異なる値が書いてあるはず。versionの信頼性という点で問題が残る。

Maven (maven-semantic-release)

version mismatchが気になる場合は、semantic-releaseの利用を検討する。これはmasterブランチにpushされた変更をすべてリリースするという手法である。

バージョンの決定はsemantic-release本体が、リリースノートの作成は@semantic-release/github@semantic-release/release-notes-generator@semantic-release/changelogが、GitのcommitやTag打ちやpushは@semantic-release/gitが行うことになる。すべてのプロセスが自動化されるので、運用の負担は最も低くなる。またversionの信頼性も確保できる。

Conventional Commits(厳密にはAngularのルール)の利用を全開発者に強制しなければならないこと、Javaプロジェクトなのにpackage.jsonを使わなければならないこと、masterブランチを常にリリース可能にすることが許容できるならばこの手法が最善と思われる。

ライブラリはともかくアプリやサービスだとPR毎リリースが受け入れられないことはあると思うが、こちらのFAQに目を通した上でも必要と判断されるなら、リリースを意図的なタイミングでトリガする手法を取り入れることもできる。

Gradle (gradle-semantic-release-plugin)

semantic-releaseはGradleでも使える。詳細は以前の投稿を参照。

Jib

GitHub repoにBuild container images for your Java applicationsと書いてあるように、Javaアプリをコンテナ化するときに使えるツール。Maven/Gradle両対応。デフォルトでもコンテナのレイヤをうまく分けたり、warプロジェクトならJettyを同梱したりしてくれる。

依存を必要としないのはありがたいが、dockerひとつ追加するだけならあまり苦ではない(GitHub Actionsだとデフォルトで付いてくる)のでそこまでの利点という感じではない。ドキュメントにちょいちょいKubernetesとの組み合わせ方について言及されているので、Kubernetes使いにとってはやりやすいところがあるのかも。

Docker

MavenやGradleを使ってデプロイするのではなく、CIサービスでdockerコマンドを実行する。Maven/Gradleはpackage/assembleするまでが責務で、デプロイはdocker pushで行う運用になる。前述の通りdockerコマンドの導入は多くのCIサービスでfirst classのサポートを受けているので、セットアップは非常に楽。最近はmulti-stage buildもあるので最終成果物も充分に小さくしやすい。

一見やりやすいが、開発作業中にdockerコマンドが実行されない可能性をはらんでいる。これは開発者が触るものがリリースされるものと異なるものになるということで、微妙なリスクになる。開発作業中もmvnw/gradlewではなくdockerコマンドを使うようにする(付随する課題も解決する)か、コンテナにデプロイされたアプリを使うE2Eテストを早期に統合すると良いかもしれない。

なおイメージを小さくするのに jlink を試してみたいのだが、現状ではalpineで動くOpenJDK(Portolaプロジェクト)が公開されてない?ようで、2018年5月時点で有効だった手法が使えなくなっている。のでalpineイメージをベースにするためにはDockerfileがけっこう複雑かつ管理の難しい状態になってしまうのが難点。この辺の情報はおそらくこの2018年11月時点のブログ記事が最新。

docker-compose

コンテナの利用をもう一歩進めて、データストアやHTTPサーバもコンテナ化してアプリとの依存関係やそのバージョンをdocker-compose.ymlで管理する。 ここまで行くとより良いデプロイ手段というよりは、より良いプロビジョニングやポータブルな実行環境の方を実現したい段階だと思われる。

注意することはDockerを単体で利用する場合と同様だが、アプリのコンテナが依存するコンテナ(データストアなど)をアプリをビルドするたびにビルドするような運用にならないように注意する必要がある。具体的には、ADDするファイルを最小限にすることで、スキーマ定義やマイグレーションスクリプトが変更されたときなど、必要なときだけ依存するコンテナがビルドされるようにする。

追記 2019-12-12

buildpacks.ioでHerokuで使えるbuildpackを標準化しようとしているらしい。

speakerdeck.com

speakerdeck.com

*1:Maven Centralも十分に早いしHTTPSもサポートされたので、個人的にはJCenter使うモチベーションがない

draft: より良いチームを作る2019

ということで形にするために書き下しています。

背景にある考え方

  • 高い利益を上げる製品、キャリアを高める成長機会、より良い待遇と給与、良い顧客。こうした良いモノすべての源泉が「良いチーム」である。
  • しかし変化の激しい時代、ひとつの固定した「理想のチーム像」を作ることは不可能である。よって現代のチームは「時や状況に応じて学び変化できるチーム」を目指すべきである。
  • この文書では「最高のチーム」「理想的なマネジメント」ではなく「より良いチーム」を作ることを目標とする。

良いチームの指標

  • 頻繁に質の高い学習ができる。

    • 頻繁に:組織の目的によるが、少なくとも月イチ程度にretrospectiveを行う。開発組織の場合はweekly iterationやbiweekly iterationを目指す。突発性インシデントへの対応が規定され共有されているため、ダメージを抑えpostmortemを行い学習し製品にfeedbackできる。
    • 質の高い:すべての施策に共有された目標がある。このfactがほしいと思ったときに探し出せる、あるいは新規に測定できる。problem solvingについてチームが理解している。他社事例や公開された研究結果などを適宜参照する。自分たちの学習したことも可能な限り公開し、ひろくフィードバックを受け付ける。
    • 学習ができる:人のせいにしない、失敗を成長の糧として捉える、blameless postmortemの文化がある。個人の意識や努力でカバーするのではなく、組織や自動化でカバーする。目指す像が全員に共有されている。目標に対するオープンな議論とdisagree and commitのための責任の所在(後述)が明確である。
  • 個人の個性と意思が尊重され、結果としてモチベーション高くモノづくりができる。

    • 多様性を重視する、オープンな議論を行う、役職を発言と紐付けない。認知的不協和を抑えるためにも、個人が謙遜さとオープンさを身につける必要が、組織が心理的安全性を満たす必要がある。
    • マネジメントが「上司」ではなく「支援者」であることを信じてもらう。servant leadershipを実行する。
    • 各個人が学びたいこと、挑戦したいことの把握に努める。得た情報を現状に照らし、勤務の一環として成長機会を作る努力をする。学びたくないこと、目を背けたいこととの折り合いを共に考える。
    • 健康状態や家庭の事情に気を配る。可能な限り勤務時間や貢献手法に制限を設けない。場所や時間を縛る働き方を排除する。
  • 各自の役割が明確になっている、ボトムアップトップダウンのバランスが良い。

    • ブレークスルーの源泉は常にチームにある。チームのリソースをどこに割くかを検討し優先度をつけるのがマネジメントの役割。
    • ボトムアップの力を信じてempowermentすることと、組織目標やスコープを明確にすることは両立する。
    • マネジメントはまずチームのMissionとVisionを明確にし、チームのベクトルを合わせる必要がある。
    • チームを信じられない、チームに任せられない理由があるならばまず第一に排除する。言語の壁、文化の壁、情報の不対称性、習慣の違いなどが該当する。

従来の「責任」と、これからの「責任」

我々は(失敗したときに)責任を取れという言い方をすることがあるが、それになんの意味があるのだろうか?

失敗によるダメージを抑えるためにマネジメントはリスク管理をするべきで、管理が足りていなかったなら反省し学ぶ必要がある。 また管理できなかったダメージは既に存在するので、再発防止だけでなくケアの方法も考える必要がある。

責任の本質は、情報と権限を明確にして状況に応じて速やかに学び行動できる組織づくりではないか。 責任を取らせることが必要なのではなく、責任を預けるに足る能力(組織づくりやリスク管理)を持つ人材を責任者に据えることと、能力が足りないと判断したときに上長の判断で外せることが必要なのではないか。

問題点を発見する運用手法

One on oneとOKR。MBO-Sでも良いけどOKRではダメな理由がないのと、OKRの方が明確に失敗を織り込んでいる(Key Resultsがすべて達成できてるのは逆に良くないという前提が共有されている)ので良いと思う。あとフォームを利用したマネジメントへの匿名FBも有用。あとで掘り下げる。

コーチングが問題発見の手法になるかもしれない。少なくとも聞き手と話し手の双方に問題発見を促す働きがある。

不確実性を早期に発見するために、目標は小さくし、Scrum手法を適用する。 ダメージを想定し備えるとともに、失敗したときのコンティンジェンシープランを持っておく。

良いチームを支える技術

手を動かすことと議論することにチームを集中させる必要がある。ここで言う議論はコンセンサスを作ることではなく知恵を出し合うことである。

手を動かす時間を作るために、作業は極力自動化する。バグの再現環境を作ったり、管理用の情報を揃えたり、やるべき作業をリマインドしたり、必要なライブラリやミドルウェアを揃えたり、コードレビューをしたり。 CIジョブでできるチェックは極力CIジョブで実施する。理想的には1日あたり4時間以上集中して作業できる状態にする。時間はまとまって確保できるようにし、フローに入れるようにする。

議論はあらかじめ目的を明確にして、結論の出し方を決めておく。ファシリテーターを置く。ここでは思考や知見を積み上げることを目的としているため、アウトプットは文書化し検索可能にする必要がある。

ほか

  • TeachingとCoachingの使い分け
  • 組織パターン
  • 売上、利益、バジェット

参考書