every Tech Blog

株式会社エブリーのTech Blogです。

Go WASMをJavaScriptの代わりに使うべきケースとは?

開発2部の内原です。

Goは静的型付けで事前コンパイルされる言語なので、WebAssembly(WASM)にコンパイルしておけば、JavaScriptのJust-In-Time(JIT)コンパイルより速度的に有利であるように思えます。

なんとなくGoをWASMにすればJSより速くなるくらいのふわっとした認識でいましたが、果たしてどのような実装でも速くなるのかそうでないのか、速くなるとしたらどれくらいの差が出るのか、という疑問を持ったので調べてみました。

そこで、いくつかのアルゴリズムで実際にベンチマークを取って検証してみましたが、アルゴリズムの特性によって結果が様々であることがわかりました。

事前準備

実行環境

  • MacOS 26.2
  • Go 1.25
  • Node.js v25
  • Chrome (144)

Go WASM のビルドと関数公開

Go側の関数公開は以下のように js.FuncOf でラップしてグローバルに登録します。

import "syscall/js"

func main() {
    js.Global().Set("goAdd", js.FuncOf(func(this js.Value, args []js.Value) any {
        n1 := args[0].Int()
        n2 := args[1].Int()
        return add(n)
    }))

    select {}
}

Go側では syscall/js パッケージを使って関数をグローバルに公開し、以下のコマンドでWASMバイナリをビルドします。

$ GOOS=js GOARCH=wasm go build -o main.wasm main.go
$ ls -lh main.wasm
-rwxr-xr-x@ 1 uchihara  staff   2.1M Feb 11 16:00 main.wasm

生成されるWASMバイナリのサイズは約2MBです。Goランタイムが含まれるため、それなりのサイズになります。

JS側では WebAssembly.instantiateStreaming でWASMをロードし、go.run(instance) を呼ぶと、上記で登録した関数がグローバルから呼び出せるようになります。

const go = new Go();
const { instance } = await WebAssembly.instantiateStreaming(
  fetch("main.wasm"), go.importObject
);
go.run(instance);

const r = goAdd(1, 2);

計測方法

  • ブラウザ版とCLI版(Node.js)の両方で計測(ただし一部を除いて性能差はさほど出なかった)
  • 各テストは複数回計測の平均を採用

ベンチマーク関数は以下です。

function bench(fn, args, iters) {
  const times = [];
  for (let i = 0; i < iters; i++) {
    const start = performance.now();
    fn(...args);
    const end = performance.now();
    times.push(end - start);
  }
  return times.reduce((a, b) => a + b, 0) / times.length;
}

フィボナッチ関数

まずはシンプルなフィボナッチ数列の計算で比較しました。その際、関数呼び出しのオーバーヘッドが性能に影響を与える可能性があると考えたため、再帰版とループ版の2パターンで確認します。

Go側とJS側でほぼ同一のロジックを実装しています。

func goFibRecursive(n int) int {
    if n <= 1 {
        return n
    }
    return goFibRecursive(n-1) + goFibRecursive(n-2)
}

func goFibIterative(n int) int {
    if n <= 1 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}
function jsFibRecursive(n) {
  if (n <= 1) return n;
  return jsFibRecursive(n - 1) + jsFibRecursive(n - 2);
}

function jsFibIterative(n) {
  if (n <= 1) return n;
  let a = 0, b = 1;
  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b];
  }
  return b;
}

再帰版 fibRecursive(40)

実装 実行時間 倍率
JavaScript 660ms 1.0x
Go WASM 1,560ms 2.4x

ループ版 fibIterative(10000000)

実装 実行時間 倍率
JavaScript 9ms 1.0x
Go WASM 15ms 1.7x

CLI版だとどちらのパターンでもJSのほうが高速という結果になりました。 ただブラウザ版だとGo WASMのほうが3倍ほど速くなっていました。JSエンジンの最適化による差分かもしれません。

原因分析

フィボナッチ計算は計算自体が軽量で、関数呼び出しのオーバーヘッドが支配的になります。JITコンパイラはこの種のシンプルな数値計算を最適化している可能性が考えられます。

どうやら「WASMにすれば速くなる」という単純な話でもなさそうです。

行列乗算

計算量をもう増やせば差が出るかもと考えたので、比較的計算量の大きい512x512の行列乗算で試してみました。

Go/JS双方でikjループ順を使い、同一の決定的データを生成して計算しています。 Go側は[]float64 スライスを使い、JS側では Float64Array を使っています。

func goMatMul() {
    n := 512
    a := make([]float64, n*n)
    b := make([]float64, n*n)
    for i := 0; i < n*n; i++ {
        a[i] = float64(i%97) * 0.01
        b[i] = float64(i%89) * 0.01
    }
    c := make([]float64, n*n)
    for i := 0; i < n; i++ {
        for k := 0; k < n; k++ {
            aik := a[i*n+k]
            for j := 0; j < n; j++ {
                c[i*n+j] += aik * b[k*n+j]
            }
        }
    }
    sum := 0.0
    for _, v := range c {
        sum += v
    }
    return sum
}
function jsMatMul() {
  const n = 512;
  const a = new Float64Array(n * n);
  const b = new Float64Array(n * n);
  for (let i = 0; i < n * n; i++) {
    a[i] = (i % 97) * 0.01;
    b[i] = (i % 89) * 0.01;
  }
  const c = new Float64Array(n * n);
  for (let i = 0; i < n; i++) {
    for (let k = 0; k < n; k++) {
      const aik = a[i * n + k];
      for (let j = 0; j < n; j++) {
        c[i * n + j] += aik * b[k * n + j];
      }
    }
  }
  let sum = 0;
  for (let i = 0; i < n * n; i++) sum += c[i];
  return sum;
}
実装 実行時間 倍率
JavaScript 190ms 1.0x
Go WASM 208ms 1.1x

差は縮まりましたが、まだ若干JSが優勢です。

原因分析

JS側ではTypedArrayに対して最適化が行われている可能性があります。 またGo WASM側では以下箇所がオーバーヘッドになっている可能性があります。

  • WASMではSIMD命令を十分に活用できない?
  • Goのスライスにおけるbounds checkのコストがある?

行列乗算は計算量が大きいためJS-WASM境界のオーバーヘッドは相対的に小さくなりますが、依然としてJSが有利でした。

SHA-256

計算量をもっと増やすと変化が出てくるかもと考えたので、SHA-256関数を利用することにします。

その際、JSによる純粋な実装よりネイティブAPIによる実装のほうが効率的である可能性が高いと考えたため、SubtleCrypto:digest()も比較対象に含めました。

ただ、SubtleCrypto:digest()は非同期関数であり、ベンチ時に同期的に呼び出しを行う必要がある点に注意が必要でした。

チェインハッシュ

小さなデータのハッシュ結果を次のハッシュの入力にする、という処理を10万回繰り返しました。

実装 実行時間 倍率
Go WASM 41ms 1.0x
JS 純粋実装 114ms 2.8x
SubtleCrypto 200ms 4.9x

Go WASMがJS純粋実装の約2.8倍速く、最速という結果になりました。また、SubtleCryptoはさらに遅いという結果になりました。

Go WASMが速い理由

SHA-256はビット演算・整数演算が中心のアルゴリズムで、Goのコンパイル済みWASMコードが有利な立場だったと言えそうです。また、10万回のハッシュ計算を1回の関数呼び出しでWASM内で完結させている点で、呼び出しオーバーヘッドの影響がほぼなく、効率的だったと考えられます。

js.Global().Set("goSHA256", js.FuncOf(func(this js.Value, args []js.Value) any {
    data := []byte(args[0].String())
    iterations := args[1].Int()
    h := sha256.Sum256(data)
    for i := 1; i < iterations; i++ {
        h = sha256.Sum256(h[:])
    }
    return hex.EncodeToString(h[:])
}))

JS-WASM境界を跨ぐのは最初の呼び出しと結果の返却の1往復だけで、ループ全体がWASM内で実行されます。これにより crypto/sha256 標準ライブラリの実装がそのまま適用されます。

フィボナッチではJS側から関数を1回呼ぶという意味では同じでしたが、計算自体が軽いためランタイムオーバーヘッドが目立ちました。SHA-256チェインでは1回の呼び出しの中で重い計算を行うため、オーバーヘッドが相対的に無視できるようになります。

SubtleCryptoが遅い理由

ネイティブAPIの crypto.subtle.digest() が最も遅い結果となりました。

async function jsSHA256Subtle(str, iterations) {
  const encoder = new TextEncoder();
  let data = encoder.encode(str);
  data = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
  for (let i = 1; i < iterations; i++) {
    data = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
  }
  return Array.from(data, b => b.toString(16).padStart(2, "0")).join("");
}

SubtleCrypto はasync APIのみを提供しているため、10万回のチェインハッシュでは毎回Promise生成 → microtask enqueue → await復帰を繰り返します。ハッシュ計算自体よりも非同期ディスパッチのコストが支配的になっているようです。

巨大バッファハッシュ

SubtleCryptoは大きなデータを一括処理するケースで優位であることが予想されます。64MBのバッファを1回だけハッシュする形式に変更して計測しました。

async function jsSHA256BulkSubtle(size) {
  const data = new Uint8Array(size);
  for (let i = 0; i < size; i++) data[i] = i % 251;
  const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
  return Array.from(hash, b => b.toString(16).padStart(2, "0")).join("");
}
実装 実行時間 倍率
SubtleCrypto 350ms 1.0x
Go WASM 430ms 1.2x
JS 純粋実装 980ms 2.8x

非同期呼び出しは1回だけなのでネイティブの速度がそのまま活かされ、SubtleCryptoが最速という結果になりました。

対して、GO WASM版にはなんらかのオーバーヘッドが存在しているのか、もしくはダイジェスト関数実装における性能差があるのかもしれません。

SubtleCryptoは大きなデータを一括処理する用途向きであり、小さなデータを繰り返しハッシュするような用途には向いていなさそう、ということがわかりました。

おまけ

計測中に興味深い現象を発見しました。Mac Chrome(144.0.7559.110)で、DevToolsを開いた状態では閉じた状態に比べてGo WASMの性能が低下するというものです。

テスト Go WASM(DevTools閉) Go WASM(DevTools開) 劣化率
再帰 fib(40) 1,550ms 3,150ms 2.0x
行列乗算 512x512 203ms 453ms 2.2x
SHA-256 チェイン 41ms 136ms 3.3x
SHA-256 64MB 428ms 1,461ms 3.4x

一方、JS側にはほとんど影響がありませんでした。

原因

DevToolsを開くと、Chromeが内部的にChrome DevTools Protocol(CDP)の Debugger.enable() を発行するようです。これによりWASMバイトコードにデバッグ用コード(ブレークポイント判定等)を挿入するため、WASMの実行速度が大幅に低下しますが、一方JSのJITコードには同等の影響があまり発生しないようです。

WASMのベンチマーク時はDevToolsを閉じた状態で行う、またはDevToolsを開いている場合にはDebuggerを無効化した状態で行う必要があることが分かりました。

まとめ

  • Go WASMがJSより優位なのは、1回の関数呼び出しで大量の計算をWASM内で完結させるパターン(SHA-256チェインなど)
  • 逆に、関数呼び出しが頻繁・計算が軽い場合は、JS JITが有利(フィボナッチなど)
  • SubtleCryptoなどのasync APIは呼び出し回数に注意が必要。大バッファの一括処理なら効果的
  • WASMのベンチマーク時はDevToolsを閉じるorDebugger無効化。そうしないと2〜3倍の劣化が発生する

「GoをWASMにすればJSより速くなる」、というのは条件次第で真偽いずれもあり得ることが分かりました。

JS-WASM境界を跨ぐ回数を最小化し、WASM内でまとまった計算を完結させる設計にすることが重要そうです。

WASMの導入を検討する際は、対象のアルゴリズムがこのパターンに合致するかを事前に見極める必要があります。

CloudFront構成でWAFをどこに適用するか?ALB適用も視野に入れた技術的検討

こんにちは、エブリーでデリッシュキッチンの開発を主に担当している塚田です。

WebやAPIを運用する中で、セキュリティ強化は継続的な課題の一つです。

今回は、AWS WAF (Web Application Firewall) を導入する場合のアーキテクチャ選定と、そこで直面した技術的な検討事項について紹介します。

特に、「CloudFront -> ALB -> ECS」という標準的な構成において、「WAFをどこに適用するか(Edge vs Regional)」という議論にフォーカスします。

はじめに

今回は、静的コンテンツの配信効率化と負荷分散のために、以下のような構成をとっている前提で検討します。

また、セキュリティグループやネットワークACLによる防御や、アプリケーション側で行える基本的な攻撃への対処(SQLインジェクションやXSSなど)は実現できているものとします。

User -> CloudFront -> ALB (Application Load Balancer) -> ECS (Amazon ECS)

そこで最初の岐路となったのが、「CloudFront(Edge)にWAFを適用するか、ALB(Regional)に適用するか」という問題です。

CloudFront vs ALB どちらにWAFを置くか?

AWS WAFは、CloudFrontとALBのどちらにもアタッチ可能です。それぞれのメリット・デメリットを整理し、検討を行いました。

比較項目 CloudFront (Edge) に適用 ALB (Regional) に適用
防御範囲 全リクエストをエッジでブロック キャッシュミスし、オリジンに到達したリクエストのみブロック
コスト 高い (全リクエストが課金対象) 安い (オリジン到達分のみ課金対象)
オリジン負荷 攻撃リクエストがオリジンに届かないため負荷減 攻撃リクエストもALBまでは到達する
直接アクセス対策 ALBへ直接アクセスされるとWAFを回避される ALB自体を守るため、直接アクセスでもWAFが機能する
IP制限 クライアントIPで直接制限可能 X-Forwarded-For ヘッダーの参照が必要

単純に以下の観点で検討した場合ALBの方が有利であると感じました。

圧倒的なコストメリット

メディアサービスの特性上、画像や動画などの静的リクエスト数が膨大です。

CloudFront側でWAFを有効にすると、静的リソースへの正常なアクセスも含めた「全リクエスト」に対してWAFの料金(Web ACL使用料 + リクエスト数課金)が発生してしまいます。 ALB側であれば、CloudFrontでキャッシュアウトした「動的処理が必要なリクエスト」のみが検査対象となるため、コストを大幅に最適化できます。

防御対象の絞り込み

本当に守りたいのは、データベース接続やビジネスロジックを持つECS上のアプリケーションです。静的ファイルへのリクエストを除外した、純粋なアプリケーションリクエストのみを検査対象とすることで、運用時のログ分析ノイズも減らせると判断しました。

技術的検討事項と実装のポイント

有利であると感じたALB側にWAFを適用する場合ですが、いくつか特有の技術的課題が発生します。

1.クライアントIPの識別(IP制限)

ALBにWAFを適用する場合、WAFが見る「送信元IPアドレス」はCloudFrontのIPレンジになってしまいます。攻撃者のIPや、社内からのアクセス許可(ホワイトリスト)をIPベースで行う場合、そのままでは機能しません。

対策: AWS WAFのルール設定において、IP判定に X-Forwarded-For ヘッダーを使用するように構成することで、IP制限を実現。

方法: AWS WAFの設定(コンソールやTerraform等)で、IPセットの一致条件を「IP address in header」とし、ヘッダー名に X-Forwarded-For を指定します。 ※ X-Forwarded-For は改ざん可能なヘッダーであることに注意が必要です。

2.AWSマネージドルール(IPレピュテーション等)の制約

ここが大きな注意点ですが、AWSが提供する AWSManagedRulesAmazonIpReputationList などのIPアドレスベースのマネージドルールは、基本的に「送信元IPアドレス」を検査対象とします。

ALB上のWAFから見ると送信元はすべてCloudFrontのIPとなるため、これらのルールはクライアントのIP(攻撃者)に対して正しく機能しません(CloudFrontのIPを評価してしまいます)。

SQLインジェクションやXSSなど「アプリケーション脆弱性への攻撃(リクエストの中身)」への防御やレートリミットでの制限を最優先とする場合は、この制約を許容できるかもしれません。 しかし、AWS提供のIPレピュテーションリストによる防御が必須要件である場合は、コスト増を許容してでもCloudFront側にWAFを適用する必要があります。

3.CloudFront以外からのアクセス遮断(WAF回避の防止)

ALBにWAFがあるため、攻撃者がCloudFrontを経由せずにALBのDNS名に直接アクセスしてきた場合でも、WAF自体は機能します。しかし、キャッシュを介さない不必要な負荷を避けるため、ALBへのアクセスはCloudFront経由のみに限定すべきです。

対策: ALBのセキュリティグループ(SG)で、インバウンドルールをCloudFrontのManaged Prefix ListからのHTTPSのみ許可」に設定しました。

これにより、攻撃者がALBのIPやDNSを特定して直接攻撃を仕掛けてきても、ネットワーク層で遮断されます。WAF以前の段階で不正アクセスを防ぐ重要な設定となります。

4.静的・動的コンテンツの分離によるコスト最適化

ALBレイヤーでの適用と同様の効果(コスト削減)を得るための別のアプローチとして、CloudFrontディストリビューションを静的コンテンツ用と動的コンテンツ用に分割する構成も有効です。

  • 静的コンテンツ用CloudFront: WAFを適用しない(または安価なルールのみ)。常にキャッシュさせる

  • 動的コンテンツ用CloudFront: WAFを適用する

このように構成することで、エッジ(CloudFront)での防御メリットを享受しつつ、検査対象を動的リクエスト(ALBと同等のアクセス量)に絞り込んでコストを最適化できます。ただし、ドメイン設計やフロントエンドの実装変更が必要になるため、対応に必要なコストと防御したい内容を天秤にかけて検討すると良いでしょう。

実際に導入する際の運用考慮事項

実際にWAFを本番環境へ導入する場合には、技術的な設定だけでなく、誤検知(False Positive)への運用フローなどの考慮も必要です。

この点については、過去の記事でも詳しく紹介していますので、ぜひ参考にしてください。

tech.every.tv

tech.every.tv

まとめ

今回のWAF導入では、「どこで守るか」と「コスト最適化」という観点で検討を行いました。

  • CloudFront (Edge): 全リクエスト防御、DDoS対策重視、設定がGlobal

  • ALB (Regional): コスト最適化(キャッシュ済みリクエスト除外)、アプリ保護重視

構成図だけ見れば「前段(Edge)で止めるのがベストプラクティス」とされることが多いですが、実際のリクエスト量や守るべき対象のリスク許容度を計算すると、他の方法も検討の余地があります。

今後も、サービスの成長に合わせて、セキュリティとパフォーマンス、そしてコストの最適なバランスを追求していきたいと思います。

Flutter3.38アップグレードにおけるiOSとAndroidの影響範囲

Flutter3.38アップグレードにおけるiOSとAndroidの影響範囲
Flutter3.38アップグレードにおけるiOSとAndroidの影響範囲

 こんにちは、開発本部 開発2部 RetailHUB NetSuperグループに所属するホーク🦅アイ👁️です。

背景

 現在提供しているネットスーパーアプリはFlutter+Dartで実装しております。一方で、昨年2025年11月1日までに対応が必要であったGoogleからのメモリの16KBページサイズをサポートせよという通知を延長申請して2026年5月31日まで対応を保留にしていました。2026年2月現在、この延長期限も近づいてきていることを理由にFlutterのバージョンを3.38系にアップグレードすることになりました(こちらの公式ブログにも3.38から標準対応したとあります)。

Flutter3.38アップグレードの手順

 通常、Flutterのバージョンをアップグレードする際の流れとしては、以下の2点を気にする必要があり実際にそのプロセスを踏まないといけないことも少なからずあります。

  • パッケージ依存関係の解消
  • 既存ソースコードで改修

 今回、対象のソースコードで利用しているFlutterバージョンは3.24.1と古めのバージョンであるためバージョン差分が大きく、2点とも対応が発生しました。以下にその2点の対応について詳細を記していきます。

パッケージのバージョン依存関係調整

 目的はあくまでAndroidアプリの16KBページサイズに対応することなのでAndroidアプリのビルドをまず意識した以下のバージョンを調整しました。

  • Flutter,Dartのバージョンを上げる
    • Flutter3.38.9、Dart3.10.8にしました
    • DevTools 2.51.1
  • Gradleのバージョンを上げる
    • 8.7にしました
  • AGPのバージョンを上げる
    • 8.5.1にしました
  • Kotlinのバージョンを上げる
    • 2.0.21にしました
  • NDKバージョンを明示指定
    • 29.0.14206865にしました
  • その他利用ライブラリの対応バージョンをtrial and errorで上げる
    • 現状のバージョンのままでビルドしてみてエラーが出たら上げるを繰り返しました

依存関係の解消によって副次的問題が発生

 今回、Androidアプリのためにアップグレードをするので必要なライブラリパッケージも必要最低限のものだけを最小限でアップグレードすることを心がけたのですが、意図せずiPhoneアプリ側の方にも影響を及ぼしてしまうことが判明しました。具体的には以下のことが発生しました。

  • iOSの最小メジャーバージョン番号が12から13以上に引き上げ

 直接的な引き上げ条件は、Flutter公式によれば、swiftコードを利用する場合とありますがFlutter3.38自体がそれに該当するということ(参考ページ)でした。というわけで、必然的にFlutter3.38にしないといけない場合はiOS最小対応バージョンも13以上になるということでした。  最終的には以下に挙げるバージョン対応で一旦、すべての依存関係の競合を解決し、pub get、コード生成、iOS CocoaPodsインストールがすべて成功しました。

1. Dart SDK バージョン

  • 変更: >=3.5.0 <4.0.0>=3.10.0 <4.0.0
  • 理由: json_serializablefreezedの最新版が要求

2. Ferryエコシステム(GraphQLクライアント)

パッケージ 最終バージョン 理由
ferry ^0.16.1 ferry_generator 0.12.0+3との互換性
ferry_generator ^0.12.0+3 build 4.0対応
build_runner ^2.10.3 build 4.0対応
gql_code_builder_serializers ^0.1.0 ferry_generator 0.12.0+で必須
gql_exec ^1.0.0 ferry_generator依存関係
gql_http_link ^1.0.0 gql_execとの互換性
gql_transform_link ^1.0.0 gql_execとの互換性

3. Freezed(コード生成)

パッケージ 最終バージョン 理由
freezed ^3.2.5 build 4.0対応
freezed_annotation ^3.0.0 freezed 3.x対応

4. Firebase(iOS 13対応のため2.x系を使用)

パッケージ 最終バージョン 理由
firebase_core ^2.32.0 iOS 13サポート(3.x+はiOS 13必須)
firebase_analytics ^10.10.7 firebase_core 2.x互換
firebase_crashlytics ^3.5.7 firebase_core 2.x互換
firebase_messaging ^14.7.10 firebase_core 2.x互換 & webパッケージ互換
firebase_remote_config ^4.4.7 firebase_core 2.x互換
firebase_app_installations ^0.2.5+7 firebase_core 2.x互換

Firebase iOS SDK: 10.25.0(iOS 13+をサポート)

5. その他の重要な更新

パッケージ 最終バージョン 理由
intl ^0.20.2 flutter_localizations要求
retrofit_generator ^10.2.1 source_gen 4.0対応
json_annotation ^4.9.0 json_serializable要求(dependenciesに追加)

6. Isar Plus(データベース)モデル修正

isar_plus v4のAPI変更に対応:

変更後(isar_plus v4スタイル):

@collection
class EventLog {
  EventLog({required this.id});

  final int id;  // Auto-increment id (isar_plus v4)
  late String data;
}

主な変更点:

  • @Collection()(大文字)→ @collection(小文字)
  • Id id = Isar.autoIncrementfinal int idとコンストラクタで受け取る
  • 実際のauto-increment IDは isar.collection.autoIncrement()で生成

解決した依存関係の競合

競合1: isar_db_generator

  • 問題: isar_db_generatorパッケージが存在しない
  • 解決: isar_dbは元の isar_generatorを使用することを確認

競合2: intl バージョン

  • 問題: flutter_localizationsintl 0.20.2を要求
  • 解決: intl^0.20.2に更新

競合3: source_gen バージョン

  • 問題: isar_plussource_gen ^4.0.2を要求、retrofit_generator ^8.1.0source_gen ^1.3.0を要求
  • 解決: retrofit_generator^10.2.1に更新(source_gen 4.0対応版)

競合4: build バージョン

  • 問題: build_runner >=2.9.0build ^4.0.0を要求、freezed ^2.xbuild ^2.3.1を要求
  • 解決: freezed^3.2.5に更新(build 4.0対応版)

競合5: gql_exec バージョン

  • 問題: gql_transform_linkが古い gql_execを要求、ferry_generator 0.12.0+3gql_exec ^1.0.0を要求
  • 解決: gql_execgql_http_linkgql_transform_linkをすべて1.x系に更新

競合6: ferry バージョン

  • 問題: ferry ^0.14.2+1ferry_exec ^0.3.1を要求、ferry_generator 0.12.0+3ferry_exec ^0.7.0を要求
  • 解決: ferry^0.16.1に更新

競合7: web パッケージ(iOS 13対応のための調整)

  • 問題: isar_plusweb ^1.1.0を要求、Firebase 4.x系が web ^0.5.1を要求
  • 解決: Firebaseパッケージを2.x系にダウングレード(iOS 13サポートのため)

競合8: Firebase iOS 15要件

  • 問題: Firebase 4.x系(firebase_core 4.0+)はiOS 15を最小要件とする
  • 解決: Firebase 2.x系(firebase_core 2.32.0)を使用してiOS 13をサポート

参考リンク

既存コードの改修作業

 前述にある依存パッケージライブラリをアップグレードすることで破壊的変更が発生してしまった全箇所をエラーがなくなるまで対応していくという作業も相当数発生しました。

1. isar_plus 移行に伴うimport文の変更

  package:isar/isar.dartpackage:isar_plus/isar_plus.dart に変更

2. isar_plus 移行に伴うPodfileの変更

ios/Podfileisar_flutter_libsisar_plus_flutter_libs に変更

3. NDKバージョンの明示的指定

  • 追加: ndkVersion "29.0.14206865"(安定版最新。r28+ で 16KB アライメント対応のため r29 を使用)
  • ファイル: app/build.gradle
  • 理由: NDK r28以上で16KBアライメントがデフォルト対応。固定していた 28.0.12674087 ではなく、安定版最新 29.0.14206865 を推奨

4. Android app の namespace(AGP 8.x 必須)

  • 追加: namespace "tv.every.fresh"
  • ファイル: app/build.gradle
  • 理由: AGP 8.x ではモジュールに namespace の指定が必須。未指定だと「Namespace not specified」でビルド失敗する。

5. サブプロジェクトへの namespace 注入

  • 追加: Android library プラグインで namespace 未指定のサブプロジェクトに、AndroidManifest.xmlpackage を namespace として設定
  • ファイル: build.gradle(root)
  • 理由: AGP 8.x では全モジュールに namespace 必須。古いパッケージは namespace 未指定のため「Namespace not specified」でビルド失敗する。pub cache は編集しないため、root の subprojects.plugins.withId("com.android.library") で manifest の package を注入する。

6. BuildConfig の有効化

  • 追加: 全 Android サブプロジェクトで buildFeatures.buildConfig true
  • ファイル: build.gradle(root)
  • 理由: AGP 8.x では BuildConfig がデフォルト無効。custom BuildConfig を使うパッケージがあったため「defaultConfig contains custom BuildConfig fields, but the feature is disabled」でビルド失敗する。root の subprojects.afterEvaluate で全モジュールに有効化する。

7. Freezed 3.x マイグレーション(全モデルに abstract 修飾子)

  • 対象: @freezed を付けた全てのモデルクラス
  • 対応: Freezed 3.0 マイグレーションガイドに従い、全てのクラス定義に abstract 修飾子を追加
  • 変更例:
  // 変更前
  @freezed
  class Shop with _$Shop {...}

  // 変更後
  @freezed
  abstract class Shop with _$Shop {...}
  • 理由: Dart 3.10 コンパイラ下で non_abstract_class_inherits_abstract_member エラーを解消するため。Freezed 3.x の生成コード(mixin の抽象メンバー)と互換させるには、公開クラスを abstract class にすることが必要。
  • 実施方法: 上記のとおり、該当する全モデルファイルで classabstract class に手動で置換。
  • 検証: 本対応後に iOSビルド成功し、iOS Simulator(26.2)でアプリ起動を確認済。

このマイグレーションでLintエラー問題が発生したので以下に経緯と対応を追記しておきます。

最初の実装でLintエラーが発生していたので「@JsonSerializable(...) を factory constructor からクラスレベルに移動」しましたが、この変更により build_runnerが失敗したため、この修正では不十分であることが判明しました。@JsonSerializable をクラスレベル(abstract class)に置くと、json_serializable ジェネレーターは、以下の動きをします。

  1. abstract class のコンストラクターを探す
  2. const Xxx._() (private constructor)しか見つからない、またはコンストラクターなしと判断
  3. Freezed が生成した factory constructor(= _Xxx)との対応を解決できない
  4. 「required constructor argument を populate できない」エラーになる

Freezed + json_serializable の正しいパターンでは、@JsonSerializable は factory constructor に置く必要がありました。Freezed はこのパターンを前提に .g.dart ファイルを生成する必要がありました。

7.1. 7のLintエラー対応

@JsonSerializablefactory constructor に残したまま// ignore: invalid_annotation_target@JsonSerializable直前の行に追加しました。

ポイント: // ignore: <rule>次の1行に適用されます。旧コードでは ignore コメントがクラス宣言行にあったため、実際に警告が出る @JsonSerializable 行をカバーできていませんでした。

8. retrofit / ParseErrorLogger 対応

  • 対象: retrofit パッケージをimportしているREST APIを実装したdartファイル(ex. rest_api.dart)
  • 現象: 旧 retrofit 4.1.0 + retrofit_generator 10.2.1 で生成したコードが型 ParseErrorLogger を参照するが、package:retrofit/http.dart のみの import では参照できずコンパイルエラーになる
  • 対応:
    • retrofit: ^4.1.0^4.9.2 に更新(Dart 3.8 対応バージョン、ParseErrorLogger は package:retrofit/retrofit.dart で提供)
    • rest_api.dart: import 'package:retrofit/retrofit.dart'; を追加し、生成コード(part ファイル)から ParseErrorLogger を参照可能にする。package:retrofit/http.dart は retrofit.dart に含まれるため削除可
  • 結果: build_runner 再生成後も手動修正不要でビルド可能

9. Android 旧埋め込み (PluginRegistry.Registrar) 削除対応

Flutter 3.38 では v1 Android 埋め込み API(PluginRegistry.Registrar / registerWith)が削除されている。以下のパッケージを更新済み。

  • path_provider: ^2.1.3^2.1.5(path_provider_android 2.2.5+ を要求し、v1 削除済み)
  • shared_preferences: ^2.2.3^2.3.4(shared_preferences_android 2.2.3+ で v1 削除済み)
  • url_launcher_android: ^6.0.38>=6.3.3 <6.3.27(6.3.3 で v1 削除。6.3.27+ は androidx.browser 1.9.0 が AGP 8.9.1 を要求するため 6.3.26 以下に制限)
  • compileSdkVersion: 34 → 36(path_provider_android 等が SDK 36 を要求。app/build.gradle

10. iOS CocoaPods更新

iOS最小デプロイメントターゲットをiOS 13.0にFixしました。

platform :ios, '13.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'

11. dart formatの修正

Dart SDK が 3.5→3.10 に上がったことで、同梱の dart_style が 2.3.x→3.x に major version upgrade されました。dart_style 3.x では "tall style" と呼ばれる新しいフォーマット戦略が導入され、trailing comma の扱いや multi-line expression のレイアウトルールが根本的に変わったため、コードに問題がなくても 100 ファイル以上が再フォーマット対象となり、大量のフォーマット修正を余儀なくされました。

Flutter バージョン別 カート追加時パフォーマンス比較レポート

 新しいバージョンになったのでそれだけでどれだけ既存アプリのパフォーマンスにも影響を及ぼしたか気になったのでFlutter DevToolsでプロファイリングしてTimeline Eventsを3.24.1と3.38.9でベンチマーク比較してみました。

 以下は、同じカート追加アクションで取得したDevToolsの計測データを用い、Flutter 3.24.1Flutter 3.38.9 のパフォーマンスを比較したレポートです。

項目 Flutter 3.24.1 Flutter 3.38.9
総フレーム数 96 107

比較サマリー

指標 Flutter 3.24.1 Flutter 3.38.9 差分 傾向
平均FPS 19.8 fps 48.3 fps +28.5 fps ✅ 大幅改善
平均フレーム時間 50.47 ms 20.71 ms -29.76 ms ✅ 約59%短縮
平均ビルド時間 7.23 ms 1.25 ms -5.98 ms ✅ 約83%短縮
平均ラスター時間 26.86 ms 15.32 ms -11.54 ms ✅ 約43%短縮
平均Vsyncオーバーヘッド 5.96 ms 1.46 ms -4.50 ms ✅ 約75%短縮
最大フレーム時間 274.89 ms 85.51 ms -189.38 ms ✅ 約69%短縮
最大ビルド時間 115.16 ms 18.66 ms -96.50 ms ✅ 約84%短縮
最大ラスター時間 145.56 ms 41.46 ms -104.10 ms ✅ 約72%短縮
Janky率 100.0% 88.8% -11.2pt ✅ 改善
重度Jank率 (>33ms) 38.5% 4.7% -33.8pt ✅ 大幅改善

結論

  • Flutter 3.38.9 は 3.24.1 と比較して、カート追加時のパフォーマンスが全体的に大きく改善しています。
  • 平均FPSが 19.8 → 48.3 と約2.4倍になり、体感の滑らかさが向上しています。
  • ビルド時間・ラスター時間・Vsyncオーバーヘッドのいずれも短縮。最大フレーム時間は 274.89ms → 85.51ms(約69%短縮) と、改善が確認できます。
  • 重度Jank率は 38.5% → 4.7% と約1/8に減少。

主な改善要因(推測)

  • Flutterエンジン・Skiaの最適化
  • ビルドパイプラインの効率化(ビルド時間の大幅短縮)
  • Vsyncまわりのオーバーヘッド低減

今後の検討

  • 3.38.9 時点でも Janky率 88.8%、平均FPS 48.3 であり、60fps目標にはまだ余裕があります。
  • ラスターが主なボトルネックのため、画像最適化・RepaintBoundary・Clip削減などの施策を続けると、さらに改善の余地があります。

まとめ

 本ブログでは、Android15以降でサポートされている16KBページサイズに対応するためFlutterのバージョンを3.38系にアップグレードすると既存アプリにどのような影響を及ぼすことになるかについてお話ししました。

 一旦は、両OSともビルドが成功してアプリ起動までは確認が取れたのでこれから5月31日まであまり日がないですが以下のようなThe next stepsに基づいて進めていく予定であることをお知らせして結びとさせていただきます。

1. テスト

  • 単体テストの実行
  • 統合テストの実行
  • 手動テスト(特にデータベース操作とGraphQL操作)

2. iOS 11-12ユーザーへの対応検討

  • アプリストアで古いバージョンを継続提供
  • ユーザーへの事前通知
  • 段階的な移行計画

最後に

エブリーでは、ともに働く仲間を募集しています。

テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください!

corp.every.tv

OpenTelemetry JS はページ遷移やタブクローズで失われる計測データを永続化なしでどのように減らしているのか

はじめに

デリッシュキッチンの鈴木です。

UX 体験向上のために Web フロントエンドのパフォーマンスを計測することもあるでしょう。その際に、計測結果をその都度サーバーへ送信すると、ネットワーク通信やシリアライズ処理が増え、画面描画やユーザー操作の体感に影響しやすくなります。これは避けなければなりません。 そこで実運用では、計測データをいったんメモリ上のバッファに溜め、一定間隔または一定件数でまとめて送信するバッチ送信が一般的です。しかしこの方式では、ページ遷移やタブクローズが起きた時点でバッファに未送信データが残っていると、送信開始前に失われたり、送信中の通信が中断されたりして欠損が起きる可能性があります。

さて、この問題にどう対処するべきなのでしょうか?今回は、ページ終了時に未送信データをできるだけ取りこぼさないために、パフォーマンス計測で使用される OpenTelemetry JS がどのように設計・実装して問題に対処しているかを、コードを手がかりに整理していきます。

課題: ページ遷移・クローズ時にデータが欠損する

送信完了前に通信が中断される問題

たとえばユーザーがボタンをクリックしてから画面遷移が完了するまでの時間(E2E レイテンシ)を計測する場合、まず正常系では次の流れになります。

  1. ユーザー操作(計測開始)
  2. 処理完了(計測終了 → データ確定)
  3. 計測データをメモリ上のバッファに保存する
  4. 一定間隔または一定件数で、バッファの内容をまとめて送信する

ここで問題になるのは、3 と 4 のあいだ、または 4 の途中にページ遷移やタブクローズが割り込むケースです。この時以下の問題が起こり得ます。

  • バッファに未送信データが残っていると欠損する
  • 送信中の通信がページ終了により中断されることがある

一般的な非同期通信(fetch / XMLHttpRequest)は、ページ終了に伴ってブラウザ側で中断されることがあります。その結果、バッファ内に残っていたデータや送信途中のデータが Collector に届かず、データ欠損が起きます。 以下は、欠損が起きる典型的な流れを、シーケンス図として表したものです。

Fig 1: バッチ送信における正常系と、ページ終了割り込みによる欠損パターン(シーケンス図)

※ 図中の Normal は正常系、残り 2 つはページ終了が割り込むことで欠損が起こり得るケースです。

OpenTelemetry の対策

この問題に対して OpenTelemetry JS は、Web 標準 API を活用した 2 つのアプローチを組み合わせ、タブクローズ時の送信成功率を高めています。

  • 検知とトリガー: ページ終了の直前に発火するイベントを検知し、バッファに残っているデータの送信をただちに開始する
  • 送信継続: ページ終了後も送信が完了しやすい Web API に委譲する

全体像をレイヤに分けると次のようになります。

Fig 2: OpenTelemetry JS による二段構えの送信設計(検知とトリガー/送信継続)

それでは、この 2 つの対策が具体的にどのコードで実現されているかを追っていきたいと思います。

実装詳細をコードで追う

フェーズ1: 【検知】ページライフサイクルイベントの監視

通常、スパン(計測データ)はパフォーマンスへの影響を抑えるため、一定数をバッファに溜めてからまとめて送信(バッチ処理)します。しかし、ページ終了時に通常の周期的な送信タイミングを待っていると、その前にページが破棄されてしまった場合にスパンが失われる可能性があります。 そこで BatchSpanProcessor は、ページが終了する兆候を示すイベントを監視し、発火したら forceFlush() を呼んで今ある分を即座に送る方針を取ります。

該当コード(BatchSpanProcessor.ts

private onInit(config?: BatchSpanProcessorBrowserConfig): void {
  if (
    config?.disableAutoFlushOnDocumentHide !== true &&
    typeof document !== 'undefined'
  ) {
    this._visibilityChangeListener = () => {
      if (document.visibilityState === 'hidden') {
        this.forceFlush().catch(error => {
          globalErrorHandler(error);
        });
      }
    };
    this._pageHideListener = () => {
      this.forceFlush().catch(error => {
        globalErrorHandler(error);
      });
    };
    document.addEventListener('visibilitychange', this._visibilityChangeListener);

    // use 'pagehide' event as a fallback for Safari; see
    // https://bugs.webkit.org/show_bug.cgi?id=116769
    document.addEventListener('pagehide', this._pageHideListener);
  }
}
  • visibilitychange: document.visibilityState === 'hidden' になった瞬間を検知
  • pagehide: Safari 向けのフォールバック(コメントにもある通り)

これらのイベントが発火すると forceFlush() が呼ばれ、バッファ内のスパンがエクスポート処理へ回されます。

該当コード(BatchSpanProcessorBase.ts

forceFlush(): Promise<void> {
  if (this._shutdownOnce.isCalled) {
    return this._shutdownOnce.promise;
  }
  return this._flushAll();
}

ここで重要なのは、forceFlush() 自体は非同期であり、送信完了までページの終了を止められるわけではない点です。JavaScript には送信が完了するまでページ遷移を確実に止めるための一般的な仕組みがありません。したがって、検知してただちに送信を開始しても、なお送信中にページが閉じてしまう可能性は残ります。その穴を埋めるのが次のフェーズです。

フェーズ2: 【送信継続】ページ終了後も通信を継続しやすい API の活用

ページ終了後も送信を完遂するには、ページの寿命とネットワークリクエストの寿命を切り離せる API が必要です。OpenTelemetry JS の Transport 層は、状況に応じて次の 2 つを使い分けます。

  • navigator.sendBeacon()(ヘッダ不要の場合)
  • fetch(..., { keepalive: true })(認証などでヘッダが必要な場合)

sendBeacon はページ終了時の送信継続に適した API ですが、リクエストにカスタム HTTP ヘッダー(例: Authorization)を付与できないという制約があります。そのため、認証等でヘッダーが必要なケースでは fetch を使う必要があり、ページ終了後も送信継続が期待できるよう keepalive: true を併用する設計になります。

選択肢A: navigator.sendBeacon()

sendBeacon は、ページアンロード時の送信を想定して設計された API です。ノンブロッキングで送信を開始でき、ページが閉じた後もブラウザが送信継続を試みます。ただし、sendBeacon の戻り値は送信完了を保証するものではなく、あくまでブラウザが送信処理の開始(キュー投入)を受け付けたかどうかの成否に近い点には注意が必要です。つまり、sendBeacon を使っても確実に届くわけではなく、ページ終了時の到達率を上げるためのベストエフォートな手段だと捉えるのが正確です。

該当コード(send-beacon-transport.ts

async send(data: Uint8Array): Promise<ExportResponse> {
  const blobType = (await this._params.headers())['Content-Type'];
  return new Promise<ExportResponse>(resolve => {
    if (
      navigator.sendBeacon(
        this._params.url,
        new Blob([data], { type: blobType })
      )
    ) {
      // no way to signal retry, treat everything as success
      diag.debug('SendBeacon success');
      resolve({ status: 'success' });
    } else {
      resolve({
        status: 'failure',
        error: new Error('SendBeacon failed'),
      });
    }
  });
}

選択肢B: fetchkeepalive: true

認証ヘッダーが必要な場合は fetch を使いますが、ポイントは keepalive: true を付けることです。これにより、ページの破棄後もリクエストが一定の条件で継続されることが期待できます。

該当コード(fetch-transport.ts

const isBrowserEnvironment = !!globalThis.location;
const url = new URL(this._parameters.url);
const response = await fetch(url.href, {
  method: 'POST',
  headers: await this._parameters.headers(),
  body: data,
  signal: abortController.signal,
  keepalive: isBrowserEnvironment,
  mode: isBrowserEnvironment
    ? globalThis.location?.origin === url.origin
      ? 'same-origin'
      : 'cors'
    : 'no-cors',
});

OpenTelemetry JS は、ブラウザ環境であることを検知した場合に自動で keepalive: true を付与するため、利用者側で特別な設定をしなくてもページ終了時に強い送信経路を取りやすい設計になっています。

なお keepalive(および sendBeacon)は、実装上おおむね 数十KB(典型的には約 64KiB 前後)の送信サイズ上限に当たりやすく、バッチが肥大化すると送信に失敗する可能性があります。したがって実運用では、ページ終了時のフラッシュ対象を未送信すべてにするのではなく、イベントを小さく保つ、分割する、重要度で間引くといった設計上の工夫も合わせて検討すると安全です。

まとめ: タブクローズ時のデータ損失はどこまで回避できるか

OpenTelemetry JS は、ブラウザ仕様の範囲内で ベストエフォートにデータ欠損を減らす設計を取っています。

  • 検知(BatchSpanProcessor): ページが非表示・終了に向かうイベントを検知し、即座に forceFlush() を起動する
  • 継続(Transport): sendBeacon または fetch(keepalive) を用い、ページ破棄後の通信継続をブラウザに委譲する

この二段構えにより、タブクローズ時のデータ到達率は現実的に大きく改善します。一方で、開発者が理解しておくべき限界もあります。

開発者が知っておくべきポイント

  • 設定不要で動く: 既定設定の範囲で、この仕組みは動作する
  • 完全な保証ではない: ブラウザのクラッシュ、ネットワーク断、OS 側の強制終了などでは失敗し得る
  • サイズ上限の影響がある: keepalive には送信サイズの上限があり、上限を超えると送信に失敗する可能性がある
  • 認証環境でも破綻しにくい: 認証ヘッダーが必要な場合は fetch(keepalive) が選択されるため、現代のブラウザでは一定の実用性が期待できる

以上が、OpenTelemetry JS がページ終了時に欠損しやすい計測データを守るために採用している設計の要点です。

おわりに

今回 Opentelemetry JS のコードを追ってみましたが、かなり SDK レベルで頑張ってくれている印象がありますね。 ただ、SDK だけで完全に永続性を担保することは難しいので、どうしても失いたくないデータがある場合は、ブラウザのストレージ(IndexedDB など)にいったん永続化し、Service Worker などを用いてバックグラウンドで再送する、といった設計も選択肢になります。OpenTelemetry JS の仕組みはあくまでベストエフォートであるため、要件に応じて永続化を組み合わせるとより堅牢になるでしょう。

SRE Kaigi 2026 参加レポート

タイトル

目次

はじめに

こんにちは。2025年4月にソフトウェアエンジニアとして新卒入社した黒髙です。普段はデリッシュキッチンの開発に携わっています。

2026年1月31日(土)に中野セントラルパーク カンファレンスで開催された SRE Kaigi 2026 に参加してきました。本記事では、特に印象に残ったセッションをご紹介します。

SRE Kaigi 2026 とは?

2026.srekaigi.net

SRE Kaigi は、Site Reliability Engineering(SRE)コミュニティの活性化と技術的な交流を促進することを目的としたカンファレンスです。第2回となる今回は「Challenge SRE!」をテーマに掲げ、SREを前に進めるための挑戦を応援することを目指して開催されました。

弊社にはSREエンジニアや専任のSREチームは存在せず、プロダクトごとの開発チームがそれに近い役割を担っています。私自身もその組織の一員として知見を高めたく、「SREの考え方を開発チームとしてどう取り入れるか」という視点で参加しました。

入場時には様々なノベルティをいただきましたが、『わかばちゃんと学ぶSRE』という冊子は、初めてSREに触れる私にとって理解の助けになりました。

ノベルティ

会場には3つのセッションルームに加え、多様なスポンサーブースや書籍販売コーナーがありました。さらにマッサージブースや屋台での軽食提供、コーヒー提供などバラエティ溢れる企画があり、一日を通してとても賑わっていました。

参加レポート

生成AI時代にこそ求められるSRE

発表者: 山口能迪さん

speakerdeck.com

AWSの山口能迪さんによる、AI時代におけるSREの価値を再定義するセッションでした。「AIがコードや設定を書く時代に、SREは不要になるのか?」という問いに対して、「SREの重要性は、かつてないほど高まっている」と明確に答える内容でした。

セッションでは「AIは組織の能力を増幅するアンプである」という表現が使われていました。優れた組織はより強化され、課題のある組織は弱点を増幅させる。開発速度が上がる一方で、システムの不安定さや変更失敗率も増大しうるということです。その上で、SREの責務を「AIの爆発的な生産性を、カオスではなく、持続可能なユーザー価値へと変換する」と定義していたのが印象的でした。

また、SREがAIにもたらす価値は「コンテキスト」と「ガードレール」という2つの軸で整理されていました。

コンテキストとは、AIがより良く動作するための下地のことです。LLMは学習時点の一般的な情報しか知らないため、システム固有の情報はコンテキストとして与える必要があります。具体的にはオブザーバビリティによるテレメトリーの収集や、Infrastructure as Codeによる設定のコード化、ポストモーテムの整備などが挙げられていました。

一方、ガードレールはAIの失敗を予防・回復するための保険です。AIが生成したコードに存在しないライブラリが含まれるリスクへの対策としてのサプライチェーンセキュリティや、組織ポリシー違反を防ぐPolicy as Code、SLOに基づいた自動ロールバックなどが紹介されていました。

SRE自体がAI開発の文脈の中心にいる印象はあまりありませんが、AIが当たり前のように受け入れられ開発プロセスが成熟した今だからこそ、次のフェーズとしてSREを一連のデリバリーパイプラインに適切に組み込めるかどうかが今後の鍵になると実感しました。

SRE とプロダクトエンジニアは何故分断されてしまうのか

発表者: 渡邉美希パウラさん

speakerdeck.com

ワンキャリアの渡邉美希パウラさんによる、SREチームとプロダクトチームの間に生じる「分断」の構造と解消アプローチについてのセッションでした。渡邉さん自身がSREチームからフロントエンドエンジニアへ異動した経験を持ち、両方の立場を知る当事者として語られていたのが印象的でした。

セッションでは、分断を引き起こす構造的要因として「受発注関係の固定化」「目指すベクトルのズレ」「1対多が生む情報の非対称性」の3つが挙げられていました。SREが横断的に複数プロダクトを担当する体制では、「パフォーマンス改善はSREの仕事」という意識が生まれやすく、受発注関係が固定化してしまう問題があります。また、プロダクトチームは「価値提供・スピード」、SREは「信頼性・安定」を重視するため、本来は同じユーザー起点であるはずなのに対立構造を生みやすいとも言えます。こうした分断は仲の良し悪しではなく、役割分担が生む必然的な帰結として整理されていました。私たちの組織にはSREこそいないものの、「過度な役割分担が心理的な壁になる」「受発注関係が固定化する」という現象はありありと想像できました。

解決アプローチとしては「バウンダリー・スパニング」- 境界を意図的になくし、人や情報を繋ぐことで価値を創造するというリーダーシップ理論を参考に、Reflecting(反射)、Mobilizing(結集)、Transforming(変形)という3つのステップで実践されていました。具体的には、インフラ変更のリポジトリをプロダクトチーム側に統合して境界線を排除したり、SLOを両チーム共通の評価指標として定例で議論したり、チーム間で人材を異動させたりといった施策が紹介されていました。

当事者として経験しながら、同時に観察者として構造を見抜いているような視点の鋭さに感銘を受けました。「足並みが揃いづらい」という課題感を、再現性のあるフレームワークで整理し、フェーズに分けてアクションを起こしていることも学び深い点でした。最後に「結局は全員が視座高くオーナーシップを持てば分断は問題にならない」と締められていましたが、シンプルながら本質を捉えた言葉であると感じ、自分自身も心がけていきたいと思いました。

開発チームが信頼性向上のためにできること: 医療SaaS企業を支える共通基盤の挑戦

発表者: kosuiさん

talks.kosui.me

カケハシのkosuiさんによる、医療SaaS企業の認証権限基盤チームが信頼性向上に取り組んだ事例紹介でした。キーメッセージは「Embedded SRE不在でも、開発チームが設計を"自分ごと"として運用し続けることで信頼性は向上できる」というもので、自分たちの状況にも通じる内容でした。

医療SaaSは患者情報という極めて機密性の高いデータを扱うため、コンプライアンス、高可用性、トレーサビリティ、テナント分離といった厳しい品質要求があります。セッションでは、小規模チームがこれらの要求を満たすために採用した具体的な設計として、DBレベルでテナントを強制分離する行レベルセキュリティ(RLS)、過去データへの即座のアクセスを可能にするDelta Lakeのタイムトラベル機能、「何が起きたか」を完全に記録するドメインイベント、強い整合性と独立デプロイを両立するサービスベースアーキテクチャなど、詳細な設計内容が紹介されていました。これらの導入により、障害時の原因特定が2〜3時間から30分以内に短縮されるなど、具体的な成果も示されていました。

SREの役割や組織についてのセッションが多かった今回ですが、その中での具体的なアーキテクチャ設計の話は新鮮で聞き応えのあるものでした。ドメインごとに異なる品質要求を、限られた人的・時間的リソースの中でどう満たすか。その試行錯誤やトレードオフの判断は興味深く、同時に設計一つがプロダクトの今後を大きく左右する責任の重さも感じました。自分が設計に携わる際にも、SRE的な観点と「設計意図の浸透」を意識していきたいと思います。

おわりに

AI時代におけるSREの再定義、チーム間の分断を防ぐ組織設計、開発チーム自身が信頼性を担うアーキテクチャ設計と、切り口はそれぞれ異なりますが、共通して感じたのは「信頼性は誰かに任せるものではなく、自分ごととして向き合うもの」というメッセージでした。

SRE Kaigi 2026全体を通して、SREという領域の幅広さと奥深さを知ることができました。また、「受発注関係の固定化」「目指すベクトルのズレ」「情報の非対称性」といった構造的な課題感は他のセッションでも度々取り上げられており、多くの組織で共通していることを実感しました。

個人的には、山口さんのセッションで語られた「AIの爆発的な生産性を、カオスではなく、持続可能なユーザー価値へと変換する」という言葉が印象に残っています。開発速度の向上も、最終的にはユーザーへの安定した価値提供があってこそ意味を持つ。その視点を忘れずに、これからは開発ライフサイクル全体の改善にも寄与していきたいと感じました。