株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

Test Talkで紹介されていた探索的テストの会をヘンリーでもやってみたら、早速、品質向上の効果が出た!

LEADING QUALITYの輪読会後の雑談中様子

株式会社ヘンリーCEOの逆瀬川です。

4月から製品企画の責任者として開発ロードマップや要件開発を行っています。

特に今後、電子カルテを開発するチームでは人数も増え、これから大きな機能開発も増えていくため、これまで以上に品質向上への取り組みを強化しています。

具体的には、品質と信頼性を上げるための施策会議(品質と信頼性の会)や、不具合分析の定例会を開始しました。

本日は、品質と信頼性の会で出てきた施策のひとつである探索的テストを学び、早速実行したところ、大きな効果が出たので共有します。

弊社の品質の守護神 Aさん依存体質からの脱却!!

探索的テストに取り組むことになった背景としては、QAのAさんへの依存です。

元々医療事務出身のAさんがQAやテストを学び、チームの全てのテストを担っていました。ドメインエキスパートでもある彼女はスクリプトテストの他に、暗黙的に探索的テストを行っている状態です。

現状でもAさんのおかげで一定の品質は担保されていますが、以下のような懸念がありました。

  • 開発とQAが分離しており、品質への投資がチームとして行えず、品質が上がる速度が遅いのではないか?
  • テストの視点が単一であり、複数の視点で行われておらず、特に開発者やデザイナー視点で気づく内容が漏れているのではないか?
  • 品質担保が属人的になっており、彼女が体調不良になってしまった場合やチームから離れたりした時のダメージが大きいではないか?

チームとしても品質への関心が高かったため、何かチームで取り組めるきっかけがあれば改善速度が上がると考え、気軽に始められる探索的テストを取り組むことにしました。

LayerX様のET会良い!Henryでも始められそう!

奇跡的に探索的テストを取組むことを決めた翌週に、Nihonbashi Test Talk #2があり発表の一つが探索的テストだったので、早速申込みます。

LayerX様のmatsuさん発表で早速探索的テストを学び、ヘンリーでも試せそうな感覚を持ったので早速企画することにしました。

学んだことの一部を下記に紹介します。

探索的テストの定義

  • アプリケーションから得た情報を元に仮説を構築し、その検証を反復する、柔軟かつ自由度の高いソフトウェアテストの方法
  • 上手な探索的テストの言語化
    • バグを多く見つけることが出来る人ではなく、より多くの良質な仮説を立てられる人
      • 広範囲のドメイン知識を使って、より多くの仮説を立てられる
      • 有限な時間の中でより優先度の高い仮説から検証出来ること
      • テスト対象の性質によって様々な視点から有効なアプローチの方法が取れる
  • 良質的な仮説を立てられる探索的テストの条件
    • 適切な自由度を設定したチャーター
    • テスト対象に効果的なツアーが設定されている

詳細は、matsuさんの登壇資料「上手な探索的テストとその上達方法について」をご参照ください。

探索的テストの準備

準備としてやったことは下記の通りです。

  1. 探索的テストの説明をNotionにまとめる
    • matsuさんのスライドとAgile Testingの2章を参考にしました
  2. 要求仕様の説明をNotionにまとめる
  3. 今回テストで用いるチャーターとツアーを準備する
  4. 当日の探索的テスト会のファシリテーションプランを作成する

チャーターの準備として、テスト範囲・テスト方法・テストの狙いを定めました。

初回ということもあり、完璧を求めずに自分で理解できるわかりやすいフォーマットを選んで作成しています。

参考 : 「やってみよう!探索的テスト」

今回使った「血液検査などの検査結果を確認する画面のUI改善」に対する探索的テストのチャーター例

テスト当日

当日のテストの様子

当日、オンラインでテストを実施しました。

休みだったメンバー以外は全員参加し、10名でワイワイとテストを実施しました。

実際参加したメンバーからの声

Aさん(開発者)「結論楽しい!」

Bさん(UX リサーチャー)「(機能が)良く出来てるなぁ以上出てこない」

Cさん(PdM)「成果もシェアできるしよさそうですね!!毎週のリリースごとに1時間くらいとってやってもよさそう!!」

他にも、昨日のEpic Ownerやフロント・バックエンドの開発者などが参加し、多様なメンバーが各々の観点で仮説出しとテスト実施を行いました。
結構盛り上がり、最終的に30分予定だったところを1時間に延長しています。 実際20以上の仮説が出てきて、初回にしては大成功でした。

その後、その場で出てきた不具合に対してもemojiを付けて、どれを直すかをEpic Ownerが決めていきます。今回は仕様の考慮漏れで修正したほうが良いものが多く、仕様段階での要求開発やレビューに改善の余地がありそうです。 そして、最終的には、なんと、17個の🐲がリリース前に倒されました。

探索的テスト出でてきた不具合の分類

リリース当日!

今回修正した画面は、「血液検査などの検査結果を確認する画面のUI改善」。

機能を公開した当初から検査結果を瞬時に把握しづらいとフィードバックをもらっていた画面で、ユーザーインタビューを重ねてリニューアルした画面になります。UIの完成度がとても重要な画面であり、探索的テストを経てブラッシュアップした結果を祈るのみ。。。


そして、社内外からポジティブな反響が!!!!

めちゃくちゃいい感じを伝えるPdMと、品質の高さにビビる私

お客様からのお喜びの声

学びとこれから

良かったこと

  • リリース前にチームで同期的に機能を触ることで、チームで新しい機能を出すという一体感が生まれた
  • リリース前に様々な観点より仮説が出たため、スクリプトテストでは見つけられない不具合が発見された。(特にエンジニアの技術観点が追加されたことが良かった)
  • 思ったより、改善出来る内容が多くリリース前にだいたいの不具合を潰すことが出来た
  • 通常ならリリース後にフィードバックを受けて潰していたことが内部で潰すことが出来、効率的な開発となった!
  • 探索的テスト、とても楽しい!!

改善していきたいこと

  • 仕様理解に時間がかかるため、想定より時間がかかった!ので時間が足りない
  • ツアーをうまく活用できなかった
    • ランドマークツアーやガイドブックツアー、FedEXツアーを活用していきたい
    • 専門性を生かしたツアーを提案できればよかったかもしれない
  • 開発内に閉じず、実際運用提案をしている導入 / CSのメンバーにも参加してもらいたい
  • 仮説に対して、不具合を見つける。仮説と不具合が紐づくレポートフォーマットに変えたい
  • 当日の仕切りの改善
    • 今回は各々もくもくとテストをする感じでしたが、モブテストやチャーターごとに時間を仕切るなど、ファシリテーションを改善していきたい

探索的テストはとても楽しいし、効果テキメン!!!なので、今後も継続的に行っていきたいと思います!(次回は今週実施予定!)

一方で、品質担保が属人手的である点については、今回の取組では解消されません。ただQA以外のメンバーがテストに参加したことで、品質への取り組みに実際貢献できるという実感を感じたことはとてもポジティブでした。チーム全体での品質への優先度が上がり、開発者のE2Eテストの実装や、リグレッションテストのシナリオ作成等の取組みが行われてきております。今後もこのような活動を継続的に取り組むことで属人化から解放されることが期待できるではないかと考えております。

最後に、採用中です!

ヘンリーではQA含め、各種エンジニア職を積極的に採用しています。Henry が扱っている医療ドメインは複雑ですが、社会的にもやりがいがある領域だと思っています。また、基幹システムであることと医療の業務の特異性から、求められるQAの基準は大変高く、プロダクトだけではなくプロダクトを利用したワークフローやオペレーションレベルで高い品質の実現が求められます。ぜひ、品質の高いプロダクトを提供して、社会的に価値を出していきたいという方、カジュアル面談で弊社のVPoEとお話しましょう!!

henry.jp

開発者イベントやコミュニティ参加についての期待や効果

VP of Engineeringの id:Songmu です。冒頭に、大事なお知らせですが、今週土曜日(6/22)に開催される、Kotlin Fest 2024にヘンリーはスポンサーをしています。スポンサーブースも出展しますので、是非お立ち寄りください。私もいます。

また、Henryの開発者の一人でもあり「Kotlin サーバーサイドプラグラミング実践開発」の著者でもある、 @n_takehata が、2024年版 Kotlin サーバーサイドプログラミング実践開発というタイトルで登壇します。是非こちらも聞きに来てください。

ヘンリーも社員数が増えてきたこともあり、このスポンサーを機に、イベントやコミュニティ参加に関する制度づくりを始めました。また、それらに参加する社員も増えて欲しいと思っています。そのために、改めて、社員がイベントやコミュニティに参加する意義を考え直して整理した内容が本エントリです。

前提として、頻繁に技術勉強会に参加していたり、技術コミュニティの運営に関わっているような、社交的でトレンドに敏感な開発者が社内に一定割合必要だと考えます。そういう開発者ばかりになるのが良いとも思いませんし、そういう活動に興味がない人もいても構いません。とは言え、開発者コミュニティとつながることは、組織と個人、両面にメリットがあるため、促進したいと考えます。

イベント参加に対する期待

一般的に、社員の開発者イベント参加に対する、わかりやすい期待は以下の3点です。

  • 開発者の技術力向上
    • 専門領域の技術力を高め、社の生産性の向上に繋げる
  • 技術進化と開発戦略の一致
    • トレンドのキャッチアップとエコシステムとの協調
  • ファンを増やす
    • 会社の認知率や好感度を向上させ、人材採用や自社プロダクト購買に繋げる

それぞれの観点で、まずは受動的に関わるところからで構いませんが、能動的に関わることでより効果を高められます。

開発者の技術力向上への期待

開発者の技術力向上への期待は一番わかりやすい観点でしょう。

まずは、情報や技術を学んで社内に持ち帰るというところから始め、ゆくゆくは登壇等で自ら情報発信をして広くフィードバックをもらって成長や改善につなげられると良いでしょう。

技術進化と開発戦略の一致への期待

1点目と似ていますが、技術トレンドのキャッチアップとエコシステムとの協調という観点も重要です。

すべてのソフトウェアを自前で作ることは実質的に不可能であり、自前主義で頑張りすぎるとスピードで負けてしまいます。また、技術の流行り廃りも激しくなりました。正しい(と自分たちが思う)技術が生き残るとは限らず、使われている技術が生き残るという現実もあります。利用技術が自分たちにとって好ましい方向に進化するとも限りません。

そのため、トレンドのキャッチアップやエコシステムとの協調は重要です。開発者イベントやコミュニティは、それらの雰囲気を感じ取るのに最適な場所であり、影響を与えるチャンスでもあります。

まずは、技術動向やトレンドをキャッチアップすること。それにより、技術選定の精度が高まり、ガラパゴスな技術スタックで自社開発が先細ってしまうリスクを低減できます。

そして、ゆくゆくはトレンドやエコシステムに影響を与えられるようになると良いでしょう。自分たちの開発が困らないよう、コミュニティと協調して技術進化の方向性をマネージできることが理想です。

それは、OSSや技術トレンドを作ったりコントリビュートしたり、ディスカッションしたりすること。それらについて、発信することです。実際、開発者イベントの「廊下」でOSS開発者が議論して開発方針が決まることもよくあります。

開発者コミュニティに自分たちの技術を還元することは、コミュニティからも好意的に受け止められます。これは次の「ファン」の項目にも関わってきます。

ファンを増やすことへの期待

開発者イベントでは、登壇やスポンサーをうまくやることで社の認知や好感度を大きく高められます。それらをやらずとも、参加している自社の社員が他社の社員と交流するだけでもそれらを高める効果が期待できます。近い職種同士だと話も弾みやすく、リアルな情報交換も行われやすい。そこで、会社の雰囲気や働いている人の人となりを知ってもらい、良い会社だと感じてもらうことは、非常に効果的です。

言ってしまえば、イベントの参加者は、潜在顧客だったり潜在的な採用候補者になりうる人たちです。その人達の好感度を上げることは、製品の購買や、将来的な人材採用に思ってる以上に効いてきます。潜在顧客であるかどうかは製品の特性にも寄りますが、特に、採用市場としては参加者は近い位置にいることは間違いないです。

実際、開発者はコミュニティづてで転職先を決める人も多いです。社員と直接のつながりが無くとも「コミュニティで良く名前を聞く評判の良い会社だ」と認知してもらえるだけで、その人が転職活動を始めた時に、転職先候補に挙げてもらえる確率が高まります。

閉ざされている会社より、開かれている会社の方が魅力を感じてもらいやすいのは当然のことです。

社外のコミュニティとつながること自体のメリット

前項で、わかりやすい期待を3点を挙げましたが、実は、社員が社外のコミュニティとつながる事自体、越境学習の観点から、本人と会社双方にメリットがあります。コミュニティとの触れ合いそのものは楽しいですが、それだけではない価値があります。詳しくは、以下のブログ記事が参考になります。

社員各々が、多様なコミュニティに接続されていることが大事です。それぞれの興味範囲のコミュニティに属し、知的好奇心を満たすことを会社が認めることが、モチベーション高く仕事をしてもらうことにも繋がります。

開発技術領域が細分化と同時に、コミュニティの細分化が進んでいる現状において、それぞれの社員が各方面に多様なコネクションを持つことは重要です。いわゆる「弱い紐帯の強み」における紐帯が各所に張り巡らされているイメージです。

ですので、自社の社員が、現在の社内の技術スタックとは直接つながりがない技術コミュニティに属することにも意味があります。その技術やコミュニティが長期的に役立つかもしれないし、役に立たなくても良い。実際に、コミュニティで仲良くなった人がリファラル採用につながるケースは頻繁にあります。

発信力を上げるメリット

イベントに参加する場合、能動的な発信、つまり、登壇ができると尚良いです。これは、会社と個人双方に大きなメリットがあります。個人側のメリットについては、以前私が書いた以下のエントリーに説明を譲ります。

会社側のメリットについて補足すると、社員の登壇は社の認知や好感度向上に何より効果的です。スポンサーと違って少ないお金で済みますし、参加者からの第一印象もスポンサーに比べて良い傾向にあります。登壇発表がイベントのメインコンテンツだからです。

登壇発表内容には「現場の生の声感」や「技術の面白さや楽しさ」などが盛り込まれていると魅力的になります。参加者もそれが一番有益だと感じているからです。オープンに率直にノウハウを出すことはコミュニティからポジティブに受け取られますし、その人や会社のスタイルや音楽性の共有にもなります。参加者に、そういう生の声に触れてもらい「自分とマッチしていそうだな」「この人と働きたいな」などの共感を得られば、転職先候補としても見てくれるようになるでしょう。

発表資料は、オンラインで公開すると良いでしょう。発表だけではせいぜい数百人の聴衆にしかリーチしませんが、オンラインに公開すれば、聴衆が拡散を促してくれて1万人以上にリーチすることもあります。会社説明や採用情報にも軽く触れられると効果的な広報活動になります。

ただ、企業色が出過ぎることは、特にコミュニティベースの開発者イベントでは、ポジティブに受け取られません。宣伝色が強すぎたり、メリットばかり強調するような過度なポジショントークをしてしまうのは逆効果で、折角の登壇機会を台無しにします。

イベントスポンサーを上手くやる難しさもこのあたりに起因します。以前ほど潔癖な雰囲気はなくなり、コミュニティに企業からスポンサーしてもらうことの重要性を多くの開発者が理解するようになりました。それでも、スポンサー登壇枠やブース出展では、商業色を出しすぎず、その場のコンテキストにあった発信をすることが好まれます。

この開発者コミュニティの雰囲気やコンテキストを理解することは、他職種からするとかなり難しいのではないかと感じています。これは、参加してもらって実際に体験してもらうのが効果的です。開発者に限らず、経営者、人事や広報、マーケの人などにも参加してもらえると良いでしょう。

コミュニティ作りや運営に関わる

余力があれば、会社としてコミュニティを作りを支援したり、運営に関わったりする事も考えたいところです。CSR的な側面もありますが、これもまた、受動的にコミュニティに参加するだけではなく、能動的にコミュニティづくりに関わるほうが効果を大きくできる、という打算的な考えも裏側にあります。

ただ、このあたりを真面目にやろうとすると、金銭だけじゃなくて人的コストも結構掛かるので、小さい会社がそこにどれくらい踏み込むかは悩ましいのが実情です。ただ、コミュニティや場を作るのが好きな人・得意な人がいて、そういう人が社内にいる場合、動きを妨げないことは大事です。

私個人としては、コミュニティやイベントに参加して、発表させてもらうことが好きだし得意領域です。コミュニティ作りや勉強会運営などもやったことがありますが、個人的な志向としては、そこまで情熱や優先度が高いわけではありません。

なので、コミュニティ作りや運営をして場を提供してくださっている方々には本当に感謝しかありません。このあたりはお互いの得意領域を活かして持ちつ持たれつ、という話でもあるとも思っていますが。

両刃の剣になりうるコミュニティ戦略

ここまで書いたコミュニティ戦略は、コミュニティでうまく立ち回っている他社も同様に考えており、対称性があります。つまり、他社の参加者を我々が潜在採用候補だと見ているのと同様に、自社の参加社員が潜在採用候補だと見られているということです。

とはいえ、これはそういうオープンな場での競争なので、場に出ていかないことには、会社の魅力付けで差をつけられ、ジリ貧になってしまいます。それに、コミュニティ戦略をうまくやっている会社はごく一部ですし、そこをそれほど重要視していない会社もあるでしょう。なのでちょっと丁寧に立ち回るくらいで悪くないポジションが取れます。

ここの競争に負けないために、まずは、社員の自社へのエンゲージメントが高いことが前提になります。各人が自然と、適切な自社をアピールできるようになってもらうのが理想です。

なので、そういう場に快く送り出すことが大事です。例えば「休みを取って勉強会に参加しています」などの会話がなされると「えっ、あの会社ってそんなイケてない会社なのか」と受け取られてしまうリスクもあります。愚痴や不平不満を撒き散らされるともっと困ります。

会社と社員双方のメリット

社員のコミュニティ参加に対する会社からの支援においては期待値調整が大事です。

会社としては、個人が期待する「当たり前水準」を認識しつつ、スタンスを示すことが必要です。例えば、前項で書いたように、業務扱いでコミュニティの勉強会に参加できることが「当たり前水準」になってきていることを会社側が受け止め、どういうスタンスを取って説明するか、という話などです。「海外カンファレンスの渡航費を支援して欲しい」という期待値がある時に「今はまだそこに投資はできない」という説明をするといったことです。

社員個人側も、会社と社員がWin-Winであるかどうかを意識できると良いでしょう。業務に関係ない技術領域のコミュニティに出ていくことも無駄ではない、という話はしました。ただ、それを濫用し、業務に直接関係ない領域のコミュニティに、業務に支障が出るレベルで頻繁に参加するのは困りものです。

社員が、与えられた状況を享受するだけではなく会社側のメリットも理解できるようになれば、例えば「会社にこういうメリットがあるからこのカンファレンス渡航費を支援して欲しい」といった交渉もできるようになります。

コミュニティ参加は自発的にされてほしいし、過度に縛りは設けたくありません。縛りを設ければ設けるほど、ここまで説明してきたような効果が薄れてしまうからです。性善説前提で、それぞれが節度を持って自治することが望ましいです。もちろん何が濫用なのかは、それぞれ考えがあるので、期待値調整が必要です。

何にせよ、お互いのメリットになるように期待値調整していかないと、持続性に欠けます。ただ、短期的な結果を求めない自発的な活動であることが前提なので、コミュニティ活動を通して、短期的に会社へのリターンを持ち帰られないことに対して重く受け止める必要はありません。

とはいえ、その辺のしがらみなく、気楽に参加したいこともあるでしょう。なので、敢えて個人の予定としてコミュニティに参加することもあっても良いと思いますし、私もたまにやります。ただ、それらもあまり変に考えすぎずに、業務の一環として参加して良いとは思っています。

まとめ

開発者イベントやコミュニティ参加に関する意義を私なりにまとめてみました。これらの前提を踏まえ、社内で制度設計をしているところです。この内容自体は、汎用的なものなので他の方の参考に慣れば幸いです。

また、ヘンリーは、開発者やその他職種を絶賛募集中です。カジュアル面談も実施していますので、興味のある方は是非連絡してください。お待ちしています。

Server-Side Kotlinで書かれたCloud Runサービスのコールドスタートレイテンシを短縮する

株式会社ヘンリーでSREなどをしている戸田(id:eller)です。先日弊社のエンジニアが登壇したサーバーサイドKotlin LT大会 vol.11でSansan社の柳浦様がServer-Side KotlinアプリのCloud Run コールドスタートレイテンシを改善した話をされていました。

Server-Side Kotlin アプリのCloud Run コールドスタート レイテンシを改善した話 - Speaker Deck

本件はCloud Runを使ってServer-side Kotlinを運用している弊社でも関心が高い内容です。そこで弊社の事例も紹介させていただければと思います。

動機:コールドスタートレイテンシを改善すると何が嬉しいか

Cloud Runはスケールアウト時に10秒までリクエストを滞留させます。そして10秒でインスタンスが用意できなかった場合は429エラーが発生する可能性があります。エラーの発生はユーザ体験を悪化させるため、常に多めにインスタンスを起動しておくか、コンテナを10秒以内に起動させる必要があるわけです。

常に多めにインスタンスを起動させる方法は容易ではありますが、ランニングコストを増大させます。コールドスタートレイテンシを改善できればスケールアウトに頼った運用も採用しやすくなり、ランニングコスト圧縮効果を期待できます。

採用した施策

DB接続確立の遅延

弊社ではJDBC接続のプールにHikariCPを採用しています。HikariCPはデフォルトの設定では、DataSource作成時に接続の確立まで処理をブロックします。

設定を変更することでDataSource作成をすぐに終了して後続処理を進めることができますが、この場合はDataSourceからConnectionを取得する際に例外が出る可能性があります。このためstartup probeが呼び出されたときにデータベースに接続できることを確認する処理を入れておくことが良いでしょう。

logbackの設定を動的に組み立てるのをやめる

弊社ではそこまでlogbackの設定を複雑にしていませんが、それでも起動時に ch.qos.logback.classic.util.DefaultJoranConfigurator が1秒弱の処理時間を持っていっていました。これはXMLの解析や各種インスタンスの作成・設定に時間がかかっているからと思われます。

この問題を解決するためのパッケージが logback-tyler として公開されています。まだ安定バージョンに到達していませんが、Javaコード生成ツールなので生成されたコードに自分で責任を持てば良いだけです。

また生成されたコード自身は logback-tyler に依存しないため、ランタイムの依存は追加する必要ありません。むしろjaninoへの依存を削れるので、1MiB程度ですがコンテナも小さくできます。設定時間は弊社事例では100msと、10倍高速化まで持っていくことができました。

利用しているConfiguratorは以下のようなものです。生成されたコードは変数名などに課題が多く、またLogstashEncoderなどに対応していなかったため、けっこう手を加えています。

import ch.qos.logback.classic.AsyncAppender
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.spi.Configurator
import ch.qos.logback.classic.spi.Configurator.ExecutionStatus
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.classic.tyler.TylerConfiguratorBase
import ch.qos.logback.core.Appender
import ch.qos.logback.core.ConsoleAppender
import net.logstash.logback.encoder.LogstashEncoder
import net.logstash.logback.stacktrace.ShortenedThrowableConverter
import kotlin.system.measureTimeMillis

class TylerConfigurator : TylerConfiguratorBase(), Configurator {
    override fun configure(loggerContext: LoggerContext): ExecutionStatus {
        context = loggerContext
        val elapsed = measureTimeMillis {
            val asyncAppender = setupAsyncAppender()
            val loggerRoot = setupLogger("ROOT", "INFO", null)
            loggerRoot.addAppender(asyncAppender)
        }
        println("TylerConfigurator.configure() call lasted $elapsed milliseconds.")
        return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY
    }

    private fun setupAsyncAppender() = AsyncAppender().apply {
        context = this@TylerConfigurator.context
        name = "ASYNC"
        queueSize = 1024

        addAppender(setupStdoutAppender())
    }.also {
        it.start()
    }

    private fun setupStdoutAppender() = ConsoleAppender<ILoggingEvent>().apply {
        context = this@TylerConfigurator.context
        name = "STDOUT"
        encoder = setupLogstashEncoder()

        addFilter(HealthCheckLogFilter())
    }.also {
        it.start()
    }

    private fun setupLogstashEncoder() = LogstashEncoder().apply {
        context = this@TylerConfigurator.context
        throwableConverter = ShortenedThrowableConverter().apply {
            context = this@TylerConfigurator.context
            maxDepthPerThrowable = 50
            shortenedClassNameLength = 20
        }
        fieldNames.apply {
            level = "severity"
            logger = "loggerName"
            thread = "threadName"
            levelValue = "[ignore]"
        }
    }
}

JITコンパイラ最適化

Sansan様の資料にも記載されているものです。Cloud Runのドキュメントでも紹介されていますので、すでに試された方も多いかもしれません。

k6による負荷テストを行ったところ、弊社サービスではパフォーマンスへの影響も限定的と判断できました。これにより1秒近い高速化効果が得られました。

CPU boost

Sansan様の資料にも記載されているとおり、起動時にCPUを追加するCPU boostがとても強力です。JavaやKotlinのサーバはDB接続の確立やDIコンテナの初期化を行ってはじめて接続を受け付けることになりますが、DIコンテナの初期化すなわち依存関係の算出やインスタンス生成はCPUを多く使う処理です。

とはいえ普通にコードを書いていてはCPUコア数増大のメリットは受けにくいようにも感じます。JITやGCでの活用はできているはずですが…。本当はDIコンテナによるインスタンス生成をcoroutineを使って並列化したいのですが、弊社で使っているKoinにはまだこうした機能がまだありません。 lazyModule を使った遅延初期化がexperimentalな機能として実装中とのことで期待しています。

また弊社事例では該当しませんでしたが、CPUコア数が動的に変化するということは Runtime.availableProcessors() を参照して動的になにかを決定するような実装がある場合には注意が必要かもしれません。

適用を見送った手法

コンテナを小さくする

ベースイメージにalpineを使ったり、jlinkで不要な機能を削ぎ落としたJVMを作ったりしてコンテナを小さくできます。場合によっては大きな高速化効果を得られることもありますが、今回のケースでは0.5秒程度の短縮にとどまったため、ビルド工程をシンプルにするために採用しませんでした。

OpenTelemetryの自動計装を諦める

Sansan様の資料にはSplunkのagentの設定変更で充分に回避できると記載されていましたが、弊社はopentelemetry-javaagentを使っている関係からか、必要な自動計装だけを有効にしても10秒の壁を超えられませんでした。-Dotel.javaagent.debug=true オプションを有効にして調査したところ、bytecode manipulationの準備のために編集対象となるClassを探しているようで、ここがかなり遅そうでした。TypeInstrumentationのJavadocによるとクラス名以外の実装や親クラス・インタフェースによって挙動を変えるinstrumentationの場合にここが遅くなるようです。

公式GitHub Issuesにも似たような指摘と議論が複数存在します。こちらのIssueからリンクされているので、気になる方はご覧ください。昔は40秒とかかかってたんですね、さすがに今はそういったレベルではないですがパフォーマンスに課題があるのは変わらないようです。

一応extensionを用意して特定のクラスを対象から外すことはできるようですが、有効にしたい計装があまり多くないことからjavaagentによる自動計装を諦めることも充分に検討できそうです。

AppCDS

javaagentの利用を諦めるなら、AppCDSも検討できます。今回はAppCDS無しでも目標を達成できたので、ビルド工程をシンプルにするために採用しませんでした。

CRaC

JVMアプリケーションコンテナの起動高速化と言えばCRaC、という雰囲気はありますが、弊社で使っているgRPCサーバなどのフレームワークでは公式の対応を謳っているものがなく、検証工数が大きくなると判断して採用していません。Cloud RunでCRaCを使うこと自体は可能なようなので、こちらの記事などを参考に挑戦しても良いかもしれません。

まとめ

Cloud Runのコールドスタートレイテンシを改善すると何が嬉しいかについて述べ、弊社で採用した施策をいくつか紹介いたしました。よく紹介されている手法でもあまり効果がなかったり、新しく開発された技法が有効だったりと、検証のしがいがある技術領域だと感じます。特に logback-tyler はまだ情報が少ないので、この記事が皆さんの参考になれば幸いです。

弊社ではこれらの施策により、18秒近くかかっていたコールドスタートレイテンシが10秒程度に短縮されています。もっと短縮したいとは思っていますが、OpenTelemetry agentによる自動計装、CRaCなどの基盤技術に手を入れないと難しそうです。JVMアプリケーションやServer-side Kotlinの高速化なら任せろという方、ぜひ弊社の採用サイトにも足を伸ばしていただけると幸いです。

jobs.henry-app.jp

ヘンリー版 エンジニア社外登壇 How-To

ヘンリーで SRE をやっている id:nabeop です。

昨年からオフラインイベントも活況になってきています。登壇の準備段階や登壇時に気をつけておきたいポイントについてまとめて、技術勉強会 (ギベン)1でエンジニアが社外登壇するときに気をつけたいことの Tips 集みたいな感じで発表してみました。このときの発表の評判がよくて社外向け版もつくってみない?という声があったので書いてみました。


  1. ギベンについては過去にエントリがあります。
続きを読む

Apollo Serverをv4にアップグレードしました

株式会社ヘンリーでSREなどをやってる戸田(id:eller)です。弊社サービスはBackend for FrontendとしてApollo Serverを採用しています。

先月まではApollo Server v3を利用していましたがEOLが今年の10月に迫っていたため、v4へのアップグレードを実施しました。この記事では移行時に実施したことをご紹介いたします。

なお公式ドキュメントに主な変更点がまとまっていますので、類似の作業を予定されている方はそちらをまずご確認ください。

GraphQL関係の依存を更新する

Apollo Server v4は graphql 16.7.0以降の利用を推奨しています。もしいま依存しているバージョンが古いなら、その更新から始めることをおすすめします。

弊社では graphql に加えて @graphql-tools/load@graphql-tools/schema も使っており、更新が必要でした。これらの変更はApollo Serverの更新とは独立して事前に行ない、動作確認をしています。幸いにも性能への悪影響は観測されませんでした。

@apollo/server を導入する

v3のときは以下のように数多くの依存を package.json に記述する必要がありました:

  • apollo-server
  • apollo-server-core
  • apollo-server-errors
  • apollo-server-express
  • apollo-server-plugin-base
  • apollo-server-types

しかしv4からは @apollo/server ひとつで足りるようになっています。コンテナの大きさには変化はなかったものの、依存管理コストが下がってとても嬉しい変更です。

なお @apollo/server-plugin-landing-page-graphql-playground に含まれる ApolloServerPluginLandingPageGraphQLPlayground はマイグレーション用に提供されているだけだそうです。弊社ではドキュメントに従って ApolloServerPluginLandingPageLocalDefault に置き換えました。

エラーの扱いを1本化する

v3では ApolloErrorGraphQLError の2つのエラーが存在しましたが、v4では GraphQLError に1本化されています。独自のエラー型を提供しなくなったわけですね。

弊社では ApolloError をcatchしていたり AuthenticationError を投げていたりしている箇所のコード書き換えを行っています。

ヘルスチェックを変更する

v3では/.well-known/apollo/server-health を使ったHTTPレベルのヘルスチェックが提供されていましたが、v4ではこれがなくなりました。GraphQLレベルのヘルスチェックを使ったうえで apollo-require-preflight: true HTTPヘッダを設定する必要があります。

弊社ではCloud Runのstartup probeないしliveness probe、ならびにUptime Checkで利用するURLを変更し、GraphQLレベルに加えてDBサーバへの接続性なども確認しています。

まとめ

Apollo Serverをv3からv4にアップグレードした際に実施したことを紹介しました。マイグレーションガイドが親切で、おおむね書いてある手順に従うだけで完了できました。性能上の問題も特に確認できておらず、更新後2週間近く経ちましたが安定して稼働できています。

この記事がApollo Server v3をご利用中の方の背中を押すきっかけになれれば幸いです。

GitHub ActionsでNextJSアプリのビルドとCloud Runへのデプロイを組む

株式会社ヘンリーでSREなどをやってる戸田(id:eller)です。最近の仕事のテーマはリスクコミュニケーションとサイト信頼性です。

弊社のビルドとデプロイは長らくCircle CIを使ってきました。一方でGitHub Actionsも強力なRunnerを使うハードルが下がったり、Circle CIのcontextsよりも使いやすいvariablesやsecretsの管理ができるようになってきたりしています。特にNodeJS開発界隈はGitHub ActionsがメジャーなCI/CD環境になってきている感触もあります。

今回は既存デプロイパイプライン整理のため、NextJSプロジェクトのデプロイパイプラインをGitHub Actionsで組み直しました。要点をご紹介いたしますので、どなたかの参考になれば幸いです。

要件

  • ビルドとデプロイを分離すること。コンテナイメージとアセットをビルドのタイミングで作成しておき、デプロイでは gcloud run deploygcloud storage cp を実行するだけにして、変更のリードタイムを短縮する一環とします。
  • 動作確認環境や本番環境など、複数の環境へのデプロイを統一的に扱えること。
  • ランニングコストがCircle CIと大きく乖離しないこと。将来的にCircleCIの利用を止められればユーザごと料金を大きく削れるため、多少のランニングコスト増は許容範囲とします。

課題

ビルドとデプロイを分離するために、以下のようなワークフローが必要になります。弊社の組み方ですと対象環境ごとにコンテナイメージを作る必要があったため、ビルドマトリックスを利用しています。

graph LR
  テスト
  subgraph ビルド
    development
    staging
    production
  end
  subgraph デプロイ
    d-development[development]
    d-staging[staging]
    d-production[production]
  end
  development --> d-development
  staging --> d-staging
  production --> d-production

また弊社はアセットをCloud Storageにアップロードして、Cloud Load Balancingを経由してエンドユーザーに配布しています。Cloud Storageへのアップロードが即エンドユーザーに対する公開となるため、コンテナイメージはビルドフェーズにpushし、アセットはデプロイフェーズにアップロードさせたいところです。しかしどちらも next build コマンドによって作成するものであるため、アセットのアップロードをビルドフェーズではなくデプロイフェーズまで遅延させる必要がありました。

graph LR
  subgraph ビルド
    direction TB
    build[job] --コンテナイメージ--> ar[(Artifact Registry)]
  end
  ビルド --アセットの受け渡し--> デプロイ
  subgraph デプロイ
    direction TB
    deploy[job] --アセット--> gcs[(Cloud Storage)]
  end

最後に、Pull Request(PR)作成時のワークフローではテストやビルドは行いたいもののコンテナイメージのアップロードはしたくない、またデプロイフェーズは実行する必要がないという特徴があります。こうした制御によってランニングコストを下げるとともに、エンジニアの開発体験を改善することができます。

ビルドマトリックス間の依存関係を表現する

今回の要件はほぼGitHub Actionsの基本機能で実現可能ですが、ビルドフェーズとデプロイフェーズの双方でビルドマトリックスを使っているところだけ注意が必要です。ジョブ同士の依存には jobs.*.needs を使いますが、ここでは matrix を参照できないからです。

jobs:
  build:
    strategy:
      matrix:
        env:
          - development
          - staging
          - production
  # ...
  deploy:
    strategy:
      matrix:
        env:
          - development
          - staging
          - production
    needs:
      - build[matrix.env] # このようには書けない

この問題に対応するため、今回は cloudposse/github-action-matrix-outputs-writeを採用しました。ビルドフェーズに含まれる各々のジョブからひとつずつアーティファクトをアップロードし、これを統合するジョブをデプロイフェーズの前に挟むことで、ビルド用ジョブとデプロイ用ジョブの間に依存関係を持たせつつ、不必要なデプロイ用ジョブを実行しない仕組みを実現しています:

jobs:
  build:
    # ...
      - uses: cloudposse/github-action-matrix-outputs-write@928e2a2d3d6ae4eb94010827489805c17c81181f # v0.4.2
        if: steps.trigger-release.outputs.result == 'true' # リリースが必要な場合。後述
        with:
          matrix-step-name: ${{ github.job }}
          matrix-key: ${{ matrix.env }}
          outputs: |-
            include: true

  # デプロイが必要な環境をリストアップしてmatrix用のJSONを出力する
  prepare:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: cloudposse/github-action-matrix-outputs-read@ea1c28d66c34b8400391ed74d510f66abc392d5e # v0.1.1
        id: read
        with:
          matrix-step-name: build
      - uses: actions/github-script@v7
        id: set-result
        with:
          script: |
            const input = JSON.parse(${{ toJSON(steps.read.outputs.result) }});
            return input.include ? Object.keys(input.include) : [];
    outputs:
      result: "${{ steps.set-result.outputs.result }}"

  deploy:
    needs: prepare
    runs-on: ubuntu-latest
    if: join(fromJSON(needs.prepare.outputs.result), '') != ''
    strategy:
      matrix:
        env: ${{ fromJson(needs.prepare.outputs.result) }}
    environment: ${{ matrix.env }}

ジョブの依存関係は以下のようになります。

graph LR
  テスト
  subgraph ビルド
    development
    staging
    production
  end
  subgraph デプロイ
    d-development[development]
    d-staging[staging]
    d-production[production]
  end
  development & staging & production --> prepare --> d-development & d-staging & d-production

ビルドマトリックス間のファイル受け渡し

依存関係が表現できれば、ファイルの受け渡しは難しくありません。actions/upload-artifactactions/download-artifact を利用して、 ${{ matrix.env }} をnameに含むアーティファクトをアップロード&ダウンロードするようにします。アーティファクトは一定時間で削除されますが、デプロイが充分に頻繁であれば問題にならないでしょう。

jobs:
  build:
    steps:
      # ...
      - uses: actions/upload-artifact@v4
        with:
          name: next-static-${{ matrix.env }}
          path: .next/static
  # ...
  deploy:
    steps:
      # ...
      - uses: actions/download-artifact@v4
        with:
          name: next-static-${{ matrix.env }}
          path: .next/static
          merge-multiple: true

コンテナイメージのpushやデプロイフェーズの必要性を判断する

弊社はGitflowを使って開発をしています。コンテナイメージのpushやデプロイフェーズの必要性を整理すると、以下のようになります:

  • developブランチに変更をpushしたら、開発環境にデプロイ
  • releaseブランチからmasterブランチに向けたPRを更新したら、動作確認環境にデプロイ
  • masterブランチに変更をpushしたら、本番環境にデプロイ

これ以外のケース、例えばtopicブランチへの変更のpushやdevelopブランチに向けたPRの更新では、ビルドやテストは必要でもコンテナイメージのpushやデプロイの実行は不要です。この判断をGitHub Actions Workflowのフォーマットで表現することは可能ですが、単体テストを書きたいと考えたためJavaScriptファイルに切り出してactions/github-script で実行することとしました:

      - run: |
          echo "head=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
        id: extract-branch
      - uses: actions/github-script@v7
        id: trigger-release
        with:
          result-encoding: string
          script: |
            const triggerRelease = require(".github/workflows/trigger-release");
            const {HEAD_BRANCH, BASE_BRANCH, APP_ENV} = process.env;
            return triggerRelease(HEAD_BRANCH, BASE_BRANCH, APP_ENV);
        env:
          HEAD_BRANCH: ${{ steps.extract-branch.outputs.head }}
          BASE_BRANCH: ${{ github.base_ref }}
          APP_ENV: ${{ matrix.env }}
// .github/workflows/trigger-release.js
/**
 * @param {string} head PRのHEAD、あるいはPUSHされたブランチの名前
 * @param {string|undefined} base PRのBASEブランチの名前、あるいはundefined
 * @param {string} env development, staging, productionのいずれか
 */
module.exports = (head, base, env) => {
  const isPush = base === undefined || base.length === 0;

  if (env === "production") {
    return isPush && head === "master";
  } else if (env === "staging") {
    return !isPush && head.startsWith("release/");
  } else {
    return isPush && head === "develop";
  }
};

まとめ

弊社のユースケースでは next build はコンテナイメージとアセットの双方を作成するのに必要なコマンドでありビルドフェーズに実行したいものでしたが、アセットのアップロードタイミングはデプロイ時にまで遅延させる必要がありました。またビルドマトリックスを利用するために、ビルドジョブとデプロイジョブの依存関係管理が複雑化していました。アーティファクトを利用することでこの2つの問題が解消できました。

またコンテナイメージのpushやデプロイフェーズの必要性を判断する条件は複雑化しがちですが、JavaScriptに切り出すことでVitestなどによる単体テストを書けるようになります。必要ならTypeScriptで書くこともできるでしょう。複雑化しやすいワークフローを制御するテクニックとして覚えておいて損はないと思います。

OpenTelemetry Collector 自身のモニタリングについて考える

ヘンリーで SRE をやっている id:nabeop です。最近の仕事のテーマはサービスの可観測性の向上と信頼性の計測です。

最近では可観測性の文脈では OpenTelemetry が話題に上がると思いますが、ヘンリーでも OpenTelemetry を導入してテレメトリデータを収集して、各種バックエンドに転送しています。分散トレース周りの話題については、以下のエントリがあります。

ヘンリーではマイクロサービスからのテレメトリデータは Cloud Run で構築した OpenTelemetry Collector で集約し、otelcol のパイプライン中で必要な処理を実施し、バックエンドに転送するアプローチを採用しています。

OpenTelemetry Collector でテレメトリデータを収集している様子

現在は監視基盤の移行期なので、メトリクスが Google Cloud と Datadog の両方に転送されていますが、将来的には Datadog に一本化される見込みです。

今回のエントリでは OpenTelemetry Collector 自体の可観測性をどのように確保しているかについて紹介します。

OpenTemetry Collector の内部メトリクスを Prometheus 形式でエクスポートする

OpenTelemetry Collector のモニタリングについては以下のドキュメントが参考になります。

また、OpenTelemetry Collector 自体の可観測性の考え方についてはこのようなドキュメントがあります。このドキュメントでは実験的なアプローチとして OTLP でテレメトリデータを外部に転送するアプローチが紹介されています。今回は以下の理由から OTLP によるエクスポートを選択せず、Prometheus 方式で OpenTelemetry Collector の内部情報をエクスポートするアプローチを採用しました。

  • OTLP でのエクスポートは実験的という扱いである
  • 前述のモニタリング方法のメトリクスが Prometheus 形式で記述されている

したがって、OpenTelemetry Collector の内部のメトリクスを Cloud Monitoring と Datadog の双方に転送する OpenTelemetry Collector の設定は以下のようになりました。

receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: otel-collector
          scrape_interval: 30s
          static_configs:
            - targets: ['0.0.0.0:8888']

processors:
  batch:
    send_batch_size: 8192
    timeout: 15s
  transform/gcp:
    metric_statements:
    - context: datapoint
      statements:
      - set(attributes["exported_service_name"], attributes["service_name"])
      - delete_key(attributes, "service_name")
      - set(attributes["exported_service_namespace"], attributes["service_namespace"])
      - delete_key(attributes, "service_namespace")
      - set(attributes["exported_service_instance_id"], attributes["service_instance_id"])
      - delete_key(attributes, "service_instance_id")
      - set(attributes["exported_instrumentation_source"], attributes["instrumentation_source"])
      - delete_key(attributes, "instrumentation_source")
      - set(attributes["exported_instrumentation_version"], attributes["instrumentation_version"])
      - delete_key(attributes, "instrumentation_version")

exporters:
  googlecloud:
  datadog:
    api:
      site: datadoghq.com
      key: ${env:DD_API_KEY}

service:
  telemetry:
    metrics:
      address: ":8888"

  pipelines:
    metrics/promethus-for-datadog:
      receivers: [prometheus]
      processors: [batch]
      exporters: [datadog]
    metrics/promethus-for-gcp:
      receivers: [prometheus]
      processors: [batch, transform/gcp]
      exporters: [googlecloud]

service の telemetry.metrics によって OpenTelemetry Collector の内部メトリクスを Prometheus 形式で 0.0.0.0:8888/tcp でエクスポートして、prometheus レシーバーで Prometheus 形式のテレメトリデータを収集しています。

また、Cloud Monitoring にメトリクスを転送しようとした際に「Duplicate label Key eccountered」というエラーが発生し、メトリクスデータの転送に失敗していたので、google exporter の README.md の記述を参考に transform プロセッサーで transform/gcp としてメトリクスの属性を exported_ プレフィックスをつけた属性名に置き換えています。

このようなパイプライによって、Datadog と Google Cloud の Cloud Monitoring の両方で OpenTemetry Collector の内部メトリクスが他のマイクロサービスと同様に観測できるようになりました。

今後の課題

今回のアプローチでは Prometheus 形式でエクスポートしていますが、前述の OpenTelemetry Collector の Observability のドキュメントでは実験的という立ち位置ですが、将来的に OTLP 形式に置き換わることが示唆されています。将来的に OTLP 形式が推奨となり、Prometheus 形式でのエクスポートが非推奨となった場合、メトリクス名もドット区切りの OTLP 形式に置き換わることが予想されるので、メトリクスデータの連続性が失われることが課題になりそうと思っています。

また、OpenTelemetry Collector のモニタリングのドキュメントでは転送時にエラーになった場合は otelcol_processor_dropped_spansotelcol_processor_dropped_metric_points がカウントアップされるとありましたが、我々の環境ではこれらのメトリクスは生成されていませんでした。今は代替として otelcol_exporter_send_failed_spansotelcol_exporter_send_failed_metric_points を監視するようにしています。

We are hiring!!

ヘンリーでは各種エンジニア職を積極的に採用しています。Henry が扱っている医療ドメインは複雑ですが、社会的にもやりがいがある領域だと思っています。複雑な仕組みを実装しているアプリケーションには可観測性は重要な要素です。一緒にシステムの可観測性を向上しつつ、複雑な領域の問題を解決してみませんか?