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

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

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

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 が扱っている医療ドメインは複雑ですが、社会的にもやりがいがある領域だと思っています。複雑な仕組みを実装しているアプリケーションには可観測性は重要な要素です。一緒にシステムの可観測性を向上しつつ、複雑な領域の問題を解決してみませんか?

mablers_JPでドメインエキスパートとQAについて登壇しました

SDET / SREのsumirenです。 2023/12/21に開催されたmablers_JP オフラインMeetUpにヘンリーから登壇しました。その際の登壇内容について、こちらのエントリにサマリを記します。

当日のアーカイブは以下になります。よろしければぜひご覧ください。

youtu.be

イベントを運営・企画いただいた運営の皆様に感謝します。ありがとうございました。

背景

ヘンリーは医療ドメインにディープダイブし、複雑な診療報酬制度に向き合っています。難解なドメインでお客様のペインに正しくアプローチするために、ヘンリーではドメインエキスパートが活躍しています。ヘンリーで活躍しているドメインエキスパートについては、こちらの記事も是非ご覧ください。

ドメインエキスパートの知見をサービスの品質に最大限活かすため、ヘンリーではドメインエキスパートがQAの役割も担っています。これにより、「実際には医療事務さんはこういう使い方をしない」であったり「この診察をしたときには、厳密にはこういう金額計算になる」といった業務への深い洞察をサービスの品質に活かすことを目指しています。

登壇内容

上記のドメインエキスパートの知見を自動テストにおいても活かすため、ヘンリーではローコード自動テストツールであるmablをドメインエキスパート中心で運用しています。

ローコード自動テストツールとはいえ、実際にmablを非エンジニア中心で運用しているケースは少ないのではないかと思います。ヘンリーでは様々な工夫をしながらこうした尖った運用を実現し、自動テストの領域においてもドメインエキスパートの知見を活用することに挑戦しています。

イベントでは、こうしたドメインエキスパート中心のmabl運用について、組織やプロセスの設計における工夫の事例をシェアさせていただきました。

最後に

ヘンリーの組織にはまだまだQAの課題があります。

ドメインへの理解度が高い一方、QA自体の専門性が不足しているため、一般的に検知しやすいバグが摘出できないこともあります。また、QA専任の方はいないため、長期的な品質戦略や計画を立てたり、品質のボトルネックを積極的に特定して課題解決を推進することもできていないと言えます。

制約の中で、ドメインエキスパートやSDET中心で定例や改善活動を回し、今自分たちにできるQAにチームで向き合っているというのが現状です。

そうした背景もあり、ヘンリーでは1人目のQAエンジニアを募集しています。ドメインエキスパートとQAエンジニアの専門性をシナジーさせることでサービスの品質をさらに高めたり、プロアクティブに品質戦略に臨む組織体制を作っていきたいと考えています。

興味を持っていただけましたら、ぜひカジュアル面談でお話させてください。よろしくお願いいたします。

hrmos.co