TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

417

はじめに 今回は AgentCore CLI を使ったエージェント開発を本番運用できるかを検討した際に、複数環境のデプロイについて詰まったポイントがあったので、ご紹介させていただきます。 AgentCore CLIは2026年4月17日現在では、GA前段階のため、本記事で紹介する内容が今後変更される可能性があります。 検証に使用したエージェント構成 今回検証のために使用したエージェントの構成を簡単に紹介します。 今回はAgentCore CLIの使い方の説明が主題ではないため、使い方についての詳細は省かせていただきます。 AgentCore CLIの agentcore create コマンドで以下のようなエージェントを作成したという前提で話を進めさせていただきます。 - Project name : MyProject - Agent name : analysis - Type : Create new agent - Language : Python - Build : Direct Code Deploy - Protocol : HTTP - Framework : OpenAI Agents - Advanced : defaults コマンドを実行すると以下のような構成でファイルが生成されます。 主要なものに絞って記載していますが、実際には CDK の設定ファイルや LLM コンテキストファイルなども生成されます。 MyProject/ # プロジェクトルート ├── AGENTS.md # エージェントの概要・設計ドキュメント ├── README.md # プロジェクトのREADME ├── agentcore/ │ ├── agentcore.json # エージェント定義(ランタイム、Gateway、Credential等) │ ├── aws-targets.json # デプロイ先のAWSアカウント・リージョン │ ├── tool-schema.json # Gatewayターゲットのツール定義 │ ├── .env.local # APIキー等のシークレット │ ├── .cli/ │ │ └── deployed-state.json # デプロイ済みリソースの状態 │ └── cdk/ │ ├── bin/cdk.ts # CDKエントリポイント │ └── lib/cdk-stack.ts # CDKスタック定義 └── app/ # エージェントのアプリケーションコード └── analysis/ ├── main.py └── pyproject.toml また、上記構成に含まれていませんが、今回の構成では、AgentCore CLIで作成したエージェントが Gateway 経由で、 lambroll でデプロイした Lambda 関数をツールとして呼び出します。 デプロイの仕組み agentcore deploy を実行すると、内部では以下が行われます: デプロイターゲット( aws-targets.json )の読み込み agentcore.json のバリデーション CDKプロジェクトのビルド Credential(APIキー等)のセットアップ CloudFormationテンプレートの合成(synth) CloudFormationスタックのデプロイ CloudFormationスタックには、ランタイム、Gateway、IAMロール等のリソースがまとめて含まれます。 詰まったポイント 1. --target オプションでデプロイ先が絞り込めない 問題 AgentCore CLIでは、デプロイ先のAWSアカウント・リージョンを aws-targets.json に定義します。 dev/prodを分離するために、以下のように2つのターゲットを定義しました。 // aws-targets.json [ { " name ": " dev ", " account ": " 111111111111 ", " region ": " ap-northeast-1 " } , { " name ": " prod ", " account ": " 999999999999 ", " region ": " ap-northeast-1 " } ] agentcore deploy コマンドには --target オプションがあり、デプロイ先を指定できます。 --target dev を指定すればdev環境のみにデプロイされると期待しましたが、実際には以下のようにprodのCloudFormationスタックもdevアカウントに作成されてしまいました。 # targetをdevに指定してデプロイ AWS_PROFILE=dev-profile agentcore deploy --target dev 実際にはdevアカウントに以下の2つのスタックが作成される - AgentCore-MyProject-dev (意図通り) - AgentCore-MyProject-prod (意図しない) 原因 この問題は、CLIとCDKの間でターゲット情報が連携されていないことが原因のようです。 CLIの --target オプションは、 aws-targets.json からターゲット情報(account, region)を取得してIdentityのセットアップやデプロイ後の状態記録に使われますが、CDKのsynth(CloudFormationテンプレートの合成)やdeploy処理にはターゲット名が伝わりません。 agentcore create で生成される cdk.ts のデフォルトコードでは、 aws-targets.json に定義された 全ターゲット に対してスタックを生成するforループになっています。 // cdk.ts(デフォルト生成コード) for ( const target of targets) { // --target の指定に関係なく、全ターゲット分のスタックが生成される new AgentCoreStack(app, stackName, { ... } ); } CDKのsynthはローカルで実行されるため、アカウントIDが異なっていてもテンプレート生成自体は成功します。その結果、devアカウントのクレデンシャルで実行しているにもかかわらず、prod用のスタック定義もdevアカウントにデプロイされてしまいます。 対策 対策1: cdk.ts を修正して環境変数でフィルタ cdk.ts に環境変数 AGENTCORE_TARGET でターゲットをフィルタするコードを追加しました。 // cdk.ts const app = new App (); // 環境変数でターゲットをフィルタ const targetFilter = process .env.AGENTCORE_TARGET; const filteredTargets = targetFilter ? targets. filter ( t => t. name === targetFilter) : targets; for ( const target of filteredTargets) { // フィルタされたターゲットのみスタックを生成 new AgentCoreStack(app, stackName, { ... } ); } デプロイ時に環境変数を指定して実行します: # dev環境のみデプロイ AGENTCORE_TARGET=dev AWS_PROFILE=dev-profile agentcore deploy --target dev # prod環境のみデプロイ AGENTCORE_TARGET=prod AWS_PROFILE=prod-profile agentcore deploy --target prod 注意点 : agentcore create で新規プロジェクトを作成するたびに cdk.ts が初期状態で生成されるため、毎回この修正を適用する必要があります。 CLIの --target オプションの値はCDKプロセスに自動的に引き渡されないため、環境変数 AGENTCORE_TARGET として別途指定する必要があります。 --target はCLI内部でのターゲット情報取得に、 AGENTCORE_TARGET はCDKのsynthでのスタック絞り込みに使われるため、両方に同じ値を指定する必要があり、冗長になってしまいます。将来のCLIバージョンで改善される可能性はありますが、現時点(v0.8.0)ではこの対応が必要です。 対策2: aws-targets.json を毎回リセットしてプロファイルから自動検出 aws-targets.json を空( [] )にしてからデプロイすると、CLIが AWS_PROFILE からアカウントIDとリージョンを自動検出し、 "default" という名前のターゲットを自動生成します。 # dev環境(aws-targets.json が空の状態で実行) AWS_PROFILE=dev-profile agentcore deploy # prod環境(aws-targets.json をリセットしてから実行) echo '[]' > agentcore/aws-targets.json AWS_PROFILE=prod-profile agentcore deploy 一見シンプルですが、実用上は問題があります。devデプロイ後に aws-targets.json にはdevターゲットが追加された状態になっています。この状態でリセットせずにprodをデプロイすると、 aws-targets.json に2つのターゲットが登録され、対策1で述べたのと同じ問題(全ターゲット分のスタックがsynthされる)が発生してしまいます。 そのため、デプロイのたびに aws-targets.json をリセットする運用が必要になりますが、CI/CDを使い、 echo '[]' > agentcore/aws-targets.json を実行してからデプロイする形にすれば、毎回クリーンなワークスペースから始まるためリセット忘れは防げると思います。 対策1は agentcore create で自動生成される cdk.ts を書き換える必要があり、CLIのバージョンアップで生成内容が変わった際に手動マージが必要になったり、修正漏れで予期せぬ挙動を起こすリスクがあります。そのため、基本的には自動生成ファイルには手を入れず、対策2をCI/CDで運用するのが望ましいと考えています。 2. Lambda ARNのハードコーディング 問題 GatewayにLambda関数をターゲットとして追加するには、以下のようにコマンドを実行します。 agentcore add gateway-target \ --gateway Gateway \ --name DataFetcher \ --type lambda-function-arn \ --lambda-arn arn:aws:lambda:ap-northeast-1:111111111111:function:get-data \ --tool-schema-file ./agentcore/tool-schema.json ./agentcore/tool-schema.json にはLambda関数が提供するツールの定義を記述したJSONファイルを指定します。 Lambda ARNターゲットの場合、Gatewayがどのツールを公開しているか知る手段がないため、このファイルを自分で用意する必要があります。 // tool-schema.json の例 { " tools ": { " get-data ": { " name ": " get-data ", " description ": " 分析用のデータを取得する ", " inputSchema ": { " type ": " object ", " properties ": { " date ": { " type ": " string ", " description ": " 取得対象の日付 " } } , " required ": [ " date " ] } } } } このコマンドを実行すると、 agentcore.json に以下のようなGatewayターゲットが追加されます。 // agentcore.json " agentCoreGateways ": [ { " name ": " Gateway ", " targets ": [ { " name ": " DataFetcher ", " targetType ": " lambdaFunctionArn ", " lambdaFunctionArn ": { " lambdaArn ": " arn:aws:lambda:ap-northeast-1:111111111111:function:get-data ", " toolSchemaFile ": " ./agentcore/tool-schema.json " } } ] } ] ここで問題になるのが lambdaArn の値です。Lambda ARNにはAWSアカウントIDが含まれるため、dev/prodでアカウントが異なる場合、デプロイ前に毎回この値を対象環境のARNに書き換える必要があります。 devにデプロイする場合: arn:aws:lambda:ap-northeast-1:111111111111:function:get-data prodにデプロイする場合: arn:aws:lambda:ap-northeast-1:999999999999:function:get-data agentcore.json はgit管理されるファイルのため、デプロイのたびにARNを書き換えてコミットするのは手間がかかりますし、書き換え忘れにより誤った環境のARNでデプロイしてしまうリスクもあります。 対策 agentcore.json にはdev用のARNを登録しておき、 cdk.ts 側で関数名だけを取り出して、ターゲットのアカウント・リージョンからARNを動的に再構築するようにしました。 // agentcore.json(dev用のARNで登録しておく) { " lambdaArn ": " arn:aws:lambda:ap-northeast-1:111111111111:function:get-data ", " toolSchemaFile ": " ./agentcore/tool-schema.json " } // cdk.ts const resolvedMcpSpec = mcpSpec ? JSON . parse ( JSON . stringify (mcpSpec)) : undefined ; if (resolvedMcpSpec?.agentCoreGateways) { for ( const gw of resolvedMcpSpec.agentCoreGateways) { for ( const t of gw.targets ?? [] ) { if (t.lambdaFunctionArn?.lambdaArn) { // 元のARNから関数名を抽出し、ターゲットのアカウント・リージョンで再構築 const functionName = t.lambdaFunctionArn.lambdaArn. split ( ':' ). pop (); t.lambdaFunctionArn.lambdaArn = `arn:aws:lambda: ${ target. region} : ${ target.account } :function: ${ functionName } ` ; } } } } 注意点 : cdk.ts は agentcore create で新規プロジェクトを作成するたびに初期状態で生成されるため、毎回この修正を適用する必要があります。 また、前述の通り自動生成ファイルを書き換えるのはCLIのバージョンアップ等でバグを生みやすいので、CI/CDのデプロイジョブ内で agentcore.json のARNを対象環境のアカウントIDに置換してからデプロイする方が安全かなと思います。 3. APIキーをdev/prodで分けたい 問題 エージェントが外部API(OpenAI等)を利用する場合、APIキーをCredentialとして登録します。登録されたAPIキーはAgentCore Identityサービスの アウトバウンド認証 (エージェントから外部サービスへの認証情報)として管理されます。 agentcore add credential --name OpenAIApiKey --api-key sk-xxxxx このコマンドを実行すると、以下の2箇所に情報が書き込まれます。 agentcore/agentcore.json — Credentialのメタ情報(名前・タイプ) agentcore/.env.local — APIキーの実際の値 // agentcore.json " credentials ": [ { " authorizerType ": " ApiKeyCredentialProvider ", " name ": " OpenAIApiKey " } ] agentcore/.env.local AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-xxxxx 環境変数名は Credential名から AGENTCORE_CREDENTIAL_{NAME} の形式で自動生成されます。デプロイ時にこの値が読み取られ、AWS側の Token Vault (AgentCore Identityサービスのシークレットストア)に登録されます。 dev/prodで同じAPIキーを使う場合は、 .env.local の値をそのまま使えるので問題ありません。しかし、セキュリティや課金管理の観点からdev/prodでAPIキーを分けたい場合、 .env.local は1ファイルしかないため、デプロイのたびに値を書き換える必要があります。 対策 デプロイ時に環境変数でAPIキーを上書きします。環境変数が設定されていれば .env.local の値より優先されます。 # dev環境 AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-dev-xxxxx \ AGENTCORE_TARGET=dev \ AWS_PROFILE=dev-profile \ agentcore deploy --target dev # prod環境 AGENTCORE_CREDENTIAL_OPENAIAPIKEY=sk-prod-xxxxx \ AGENTCORE_TARGET=prod \ AWS_PROFILE=prod-profile \ agentcore deploy --target prod ただし、毎回デプロイコマンドにAPIキーを環境変数として渡すのであれば、 .env.local を直接書き換える運用と手間は変わりません。今回はCI/CDを使わずローカルからデプロイする運用のため、デプロイ前に .env.local の値を対象環境のAPIキーに書き換える方法を採用しました。 別のアプローチ:dev/prodでプロジェクトを分ける ここまで紹介した課題は、いずれも 1つのプロジェクトでdev/prodを共有する ことに起因しています。 これらをすべて解消するシンプルなアプローチとして、dev用とprod用でそれぞれ別のAgentCoreプロジェクトを作成する方法があります。 MyAgent-dev/ ├── agentcore/ │ ├── agentcore.json ← dev用のLambda ARN、dev用のAPIキー │ ├── aws-targets.json ← devアカウントのみ │ └── cdk/ └── app/ └── analysis/ MyAgent-prod/ ├── agentcore/ │ ├── agentcore.json ← prod用のLambda ARN、prod用のAPIキー │ ├── aws-targets.json ← prodアカウントのみ │ └── cdk/ └── app/ └── analysis/ この方法なら cdk.ts のカスタマイズは不要で、 aws-targets.json にはターゲットが1つだけなのでsynthの問題も発生せず、 .env.local も環境ごとに独立しています。 ただし、 app/ 配下のエージェントコードが2つのプロジェクトで重複するため、ロジックを変更するたびに両方を更新する必要があります。コードの同期忘れによる環境差異が生まれるリスクもあるため、この方法を積極的に採用することはできないなと思いました。 まとめ AgentCore CLIを使用してみて、実際に本番運用できるのかを検討しました。 CLIで必要なリソースを簡単に素早く作成できるというメリットはありますが、環境を分離するには課題が多いという検証結果になりました。 最後まで読んでいただきありがとうございました!
はじめに 株式会社エブリーでデリッシュキッチンのiOSアプリの開発をしている成田です。 iOS 26から、Appleの新しいデザイン言語である「Liquid Glass」が導入されました。 2026年4月の現時点では設定のフラグによって適用を回避できますが、次のXcodeのメジャーアップデートではこのフラグの廃止が見込まれています。 また、2027年春頃には新しいメジャーバージョンのXcodeでのビルドが必須になると考えられ、対応は避けられない状況です。 こうした背景から、すでにLiquid Glassへの対応を進めているiOSアプリ開発者の方も多いのではないでしょうか。 デリッシュキッチンでも現在ユーザーへのリリースを目指して対応を進めています。 本記事では、以下のような流れでデリッシュキッチンにおけるLiquid Glass対応への取り組みについて紹介したいと思います。Liquid Glassの概要については他の記事でも多く紹介されているので本記事ではできるだけ割愛します。 Liquid Glass対応の進め方 大まかな対応箇所 デリッシュキッチンにおける課題 AppleのLiquid Glassワークショップへの参加 Liquid Glass対応の進め方 キックオフと開発の流れ 今年の1月にPdMとデザイナー、エンジニアが集まりキックオフを行ってプロジェクトがスタートしました。 まず最初に、アプリのプロジェクト設定のオプトアウトフラグ UIDesignRequiresCompatibility を外した状態のアプリを社内に配布し、Liquid Glassがそのまま適用された状態で各画面をデザイナーに確認してもらいました。Appleの標準アプリや他のメジャーなアプリのUIも参考にしながら、対応が必要な箇所の洗い出しと優先度付け、そして大まかな工数見積もりを行いました。 また、対応方針については単にデザイン観点だけで決めるのではなく、技術的な実現可否や実装コストも踏まえながら、エンジニアとデザイナーで議論を重ねて整理していきました。デザインと実装の両面から検討することで、現実的かつ一貫性のある方針を定められていると感じます。 さらに、初期段階では一定期間を設けて集中的に実装を進めることで、実際の対応にどの程度の工数がかかるのかを把握することもでき、おおよそのベロシティ感を掴むことができました。 なお、参考事例としてAppleが紹介している デザイン事例集 も、実際にどのようにLiquid Glassがプロダクトに取り入れられているかを把握するうえで非常に参考になりました。 専任を置かず全員で対応する このプロジェクトでは、iOSチーム内に専任を置かず、各プロジェクトごとに分担して対応を進めています。 専任を設けると知見が特定のメンバーに偏り、今後の機能開発においてプロジェクトごとに実装のばらつきが生じる可能性があるためです。Liquid Glassのようなデザイン言語の変化は一部の対応にとどまらず、プロダクト全体に継続的に影響していくものだと考えています。 また、UIはデザイナーだけで完結するものではなく、エンジニアと連携しながら作り上げていくものです。こうした背景もあり、プロダクトに関わるiOSエンジニア全員で取り組む形で進めています。 独自フラグでコードを先行リリース 現在対応を進めているコードは、まだユーザー向けには公開せず、以下のような独自のフィーチャーフラグを設けることで、コード自体は順次リリースしつつ、ユーザーにはLiquid Glassが適用されない状態を保ったままにしています。 public enum LiquidGlassAvailability { /// Liquid Glass デザインが有効かどうかを返す。 /// iOS 26 以降かつ UIDesignRequiresCompatibility が設定されていない(または false)場合に true。 public static let isEnabled : Bool = { guard #available(iOS 26.0 , * ) else { return false } // UIDesignRequiresCompatibility が true の場合は互換モードなので Liquid Glass 無効 if let requiresCompatibility = Bundle.main.object(forInfoDictionaryKey : "UIDesignRequiresCompatibility" ) as? Bool , requiresCompatibility { return false } return true }() } このような進め方にしているのは、変更をため込むことでGitHub上のPRが滞留し、コンフリクトが発生しやすくなるのを防ぐためです。対応が完了した箇所から順次マージしていくことで、開発の流れをスムーズに保っています。 ユーザー向けの初回リリース時にはプロジェクト設定のオプトアウトフラグを取り除き、Liquid Glassが適用された状態で提供する予定です。また、リリース後も優先度に応じて段階的に適用範囲を広げていく方針です。 初回リリースに向けた大まかな対応箇所 ユーザーへの初回のリリースに向けて、優先度が高いのは以下の内容です。 ナビゲーションバー・タブバー周りの対応 最も優先度が高く、Liquid Glassの効果が大きい箇所がナビゲーションバーとタブバー周りです。Liquid Glassではこれらのバーが透過されることでコンテンツへの没入感が高まりますが、デリッシュキッチンでは元々これらのバーに対して背景色やボタンのスタイルなどを独自にカスタマイズしていました。Liquid Glassに対応するにあたり、これらの独自設定を取り除いていく作業が必要になりました。 レイアウトの修正 独自設定を削除していくと、画面によってコンテンツのレイアウトが崩れるケースが発生しました。Liquid Glassではナビゲーションバーやタブバーの背面にまでコンテンツが広がるレイアウトが前提となりますが、一部の画面ではそのような構造になっていなかったためです。各画面ごとにレイアウトを見直し、コンテンツがバーの裏側まで自然に潜り込むよう修正する作業も対応範囲に含まれています。 その他の表示崩れの修正 ここでは書ききれないので紹介を省きますが、上記の対応に加え、Liquid Glassの適用によって生じる細かな表示崩れについても最低限の修正を行ったうえでユーザーに向けた初回のリリースを行う予定です。 デリッシュキッチンにおける課題 ここでは、Liquid Glass対応を進める上でのデリッシュキッチンにおける課題をいくつかピックアップして紹介します。 ナビゲーションバー直下のカスタムViewの扱い デリッシュキッチンには、ナビゲーションバーの直下にタブやカスタムViewが配置されている画面がいくつかあります。単純にナビゲーションバーを透過にするだけでは、その下に続くカスタムViewとの境界が不自然になってしまい、コンテンツの表示領域も狭まってしまいます。これはLiquid Glassが目指すコンテンツへの没入感という思想に反してしまいます。 これらのカスタムViewをコンテンツ領域の中にどう自然に溶け込ませるか、デザインと実装の両面から検討する必要があり、現在取り組んでいる課題の一つです。 幅広い環境での検証体制 デリッシュキッチンはユーザー数も多く、現在は最新から3つのメジャーバージョンのiOSをサポートしています。Liquid GlassはiOS 26以降でのみ適用されますが、それ以前のOSでもレイアウト崩れが発生しないよう、すべてのサポートバージョンで表示を確認する必要があります。そのため、単一の環境での検証にとどまらず、複数バージョンをまたいだ確認が求められる点が大きな負担となっています。 また、弊社には専任のQAチームがないため、動作検証はPdM・デザイナー・エンジニアが協力して行っています。Liquid Glass対応のように影響範囲が広い変更では、確認すべき画面やパターンも多岐にわたるため、検証の抜け漏れを防ぎつつ、いかに効率的に進めていくかが課題となっています。 並行開発による手戻りリスク また、もう一つの課題として、通常の機能開発との並行進行があります。 現在のプロダクトでは複数のプロジェクトが並行して開発を進めており、Liquid Glass対応と並行して進行しています。そのため、新規機能の開発時にLiquid Glassの考慮が十分に行われないケースも発生しがちです。 その結果、後からデザインの調整や実装の修正が必要になり、手戻りが発生してしまう可能性があります。こうした手戻りをいかに防ぎ、現状の開発の中にLiquid Glass対応を組み込んでいくかも重要な課題となっています。 AppleのLiquid Glassワークショップへの参加 Liquid Glass対応の一環として、Appleが時折に開催しているワークショップに会社で参加する機会をいただき、3月にエンジニアとデザイナー数名で参加してきました。 ワークショップは、まずLiquid Glassの概要や設計思想、もたらす効果について一通り説明いただくところから始まり、その後はAppleのデザインのエバンジェリストの方と直接やり取りできる時間が設けられており、デリッシュキッチンにおける対応方針について質問やディスカッションを行いました。 自社アプリの課題を持ち込み、その場でフィードバックをもらえる形式だったため、抽象的なガイドラインだけではイメージしづらかった部分についても、具体的な方向性を確認することができました。 せっかくなので、ワークショップに参加して特に印象に残っている学びをいくつか紹介します。 ナビゲーションバーやタブバーで特色を出さない ナビゲーションバーやタブバーといった操作周りのUIで個性を出すのではなく、コンテンツでプロダクトの特色を表現することが重要であるという考え方が印象に残りました。 透過させることが目的ではない Liquid Glassは単に透過やブラーを適用すること自体が目的ではなく、コンテンツへのフォーカスを高めるための手段であるという話がありました。見た目だけをなぞるのではなく、どういう意図で使うかが重要だと感じました。 システムとの一貫性を保つ OS全体の表現と調和することが重要で、過度に独自のスタイルを持ち込むと違和感につながるという点も印象的でした。標準の振る舞いを尊重することが結果的に良い体験につながると感じました。 おわりに 本記事では、デリッシュキッチンにおけるLiquid Glass対応の取り組み状況についてご紹介しました。 同じようにLiquid Glassへの対応を進めている方にとって、少しでも参考になれば幸いです。 デリッシュキッチンのLiquid Glass対応のリリースもぜひ楽しみにしていてください!
はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 日頃の業務でClaude Codeを活用しているのですが、AWSからリリースされたAIツール群(IAM Policy Autopilot、Agent Plugins for AWS)がClaude Codeと連携できることを知り、社内勉強会を機に実際に試してみました。 本記事では、これらのツールの概要と、素のLLMに指示した場合と専用ツールを使った場合でどのような違いが出るのかを4つのシナリオで比較した結果をまとめます。 IAM Policy Autopilot 概要 IAM Policy Autopilotは、AWS re:Invent 2025で発表されたオープンソース(Apache 2.0)のツールです。ソースコードを静的解析し、最小権限のIAMポリシーを自動生成します。 対応言語はPython / TypeScript / Goで、CLI / MCPサーバーの両方で利用できます。 仕組み 特筆すべきは LLMを使用しない決定論的な静的解析 である点です。Rust製のAST解析エンジン(ast-grep)がSDK呼び出しを検出し、IAMアクションにマッピングします。同じコードからは常に同じポリシーが生成されるため、再現性があります。 ソースコード ↓ AST解析(ast-grep) SDK呼び出しを検出 ↓ IAMアクションにマッピング IAMポリシーJSON生成 主要機能 コマンド 用途 generate-policies ソースコード解析からIAMポリシー生成 fix-access-denied AccessDeniedエラーメッセージから修正ポリシー生成 Agent Plugins for AWS 概要 Agent Plugins for AWSは、2026年2月にAWS Labsからリリースされたプラグイン群です。AIエージェントにAWSの設計・構築・運用スキルを付与します。 利用可能なプラグイン プラグイン 用途 aws-serverless Lambda / API Gateway / EventBridge deploy-on-aws アーキテクチャ推奨 / コスト見積もり / IaC生成 databases-on-aws Aurora DSQL含むDB設計ガイダンス aws-amplify Amplify Gen 2 フルスタックアプリ構築 amazon-location-service マップ / ジオコーディング / ルーティング migration-to-aws GCPからAWSへの移行支援 deploy-on-aws の5段階ワークフロー deploy-on-awsプラグインは、以下の5段階のワークフローでプロジェクトのデプロイを支援します。 1. Analyze → 2. Recommend → 3. Estimate → 4. Generate → 5. Deploy (解析) (推奨) (試算) (生成) (デプロイ) 各フェーズでは、ワークフローを主導するSkillと、外部データを参照するMCPサーバー(awsknowledge, awspricing, aws-iac-mcp)、さらにIaC検証を自動実行するHooksが組み合わさって動作します。これにより、最新のAWSドキュメント・料金情報・IaCベストプラクティスを参照しながら一貫したプロセスで進行します。 素のLLMに指示する場合の課題 これらのツールを使わず、素のLLMに直接指示した場合には以下のような課題があります。 学習データの鮮度 : 知識カットオフ以降のAPI変更・新サービスに非対応 ハルシネーション : 存在しないAPIパラメータやサービス名を生成するリスク 一貫性の欠如 : 毎回異なるアプローチ・構成を提案する可能性 検証手段がない : 生成されたポリシーやIaCの正しさを確認できない 一方、ツールを利用すると以下の改善が得られます。 最新情報の参照 : MCPサーバー経由でリアルタイムにAWSドキュメント・料金を参照 構造化プロセス : 明確なワークフローにより一貫した品質を実現 最小権限の原則 : 自動的に最小権限を適用、ベストプラクティスに基づく設計 比較シナリオ ツールを使うと実際どれくらい差分が出るのかが気になったので、AWS開発でよく遭遇しそうな場面をAIに挙げてもらい、以下の4つのシナリオを用意して比較しました。各シナリオで素のLLMと専用ツール付きに対して同じプロンプトを渡し、出力を見比べています。 シナリオ1: Lambda関数のIAMポリシー作成(IAM Policy Autopilot) シナリオ2: サーバーレスREST APIの構築(aws-serverless Plugin) シナリオ3: AccessDeniedエラーの解決(IAM Policy Autopilot) シナリオ4: AWSへのデプロイ設計(deploy-on-aws Plugin) シナリオ1:Lambda関数のIAMポリシー作成 S3からファイルを読み取り、DynamoDBに書き込むLambda関数に必要な最小権限ポリシーを作成するシナリオです。 対象コード import boto3 s3 = boto3.client( 's3' ) dynamodb = boto3.resource( 'dynamodb' ) table = dynamodb.Table( 'my-data-table' ) def handler (event, context): bucket = event[ 'bucket' ] key = event[ 'key' ] response = s3.get_object(Bucket=bucket, Key=key) data = response[ 'Body' ].read().decode( 'utf-8' ) table.put_item(Item={ 'id' : key, 'content' : data, 'source_bucket' : bucket }) return { 'statusCode' : 200 , 'body' : 'Success' } ツール利用時は「IAM Policy Autopilotの generate_application_policies ツールを使って」と追加で指示しました。 結果比較 項目 素のLLM IAM Policy Autopilot S3アクション GetObject のみ GetObject + LegalHold + Retention + Tagging + Version + ObjectLambda DynamoDBアクション PutItem のみ PutItem + WriteDataForReplication KMS暗号化 なし S3・DynamoDB向け kms:Decrypt (条件付き) CloudWatch Logs 含む(推測で追加) 含まない(サービスロールに委任) IAM Policy Autopilotは暗号化・バージョニング・Access Point等、本番運用で必要になる権限を網羅的にカバーしています。素のLLMが推測ベースで生成したのに対し、IAM Policy AutopilotはAST解析により get_object() と put_item() の呼び出しを検出し、関連する権限を自動的に追加しました。 一方で、IAM Policy Autopilotの出力はKMS暗号化やAccess Pointなど実際に使っていない権限まで含まれるため、過剰な権限にならないよう利用するリソースに合わせてレビューすることは必要そうです。 シナリオ2:サーバーレスREST APIの構築 ユーザー情報のCRUD APIをLambda + API Gateway + DynamoDBで構築するシナリオです。 ツール利用時はaws-serverlessプラグインのMCPサーバーを利用しました。 結果比較(template.yaml) 項目 素のLLM aws-serverless Plugin Lambda関数数 1(ルーターパターン) 5(操作ごとに分離) IAMポリシー 全操作に DynamoDBCrudPolicy Read → ReadPolicy / Write → CrudPolicy (粒度分離) CPUアーキテクチャ x86_64(デフォルト) arm64(コスト最適化) トレーシング なし Tracing: Active(X-Ray) ツール利用時は5回のMCP呼び出しが行われました。最初の get_serverless_templates では条件が具体的すぎて失敗しましたが、エージェントが自動で条件を緩めて再試行する適応的な動作が見られました。最後に validate_cloudformation_template でテンプレートの妥当性検証も実施されています。 興味深かったのは、aws-serverless Pluginが単一のLambda関数ではなく、CRUD操作ごとに5つに分割した関数を生成した点です。これは最小権限の原則を徹底するためで、Read系の関数には DynamoDBReadPolicy 、Write系の関数には DynamoDBCrudPolicy と、操作ごとに必要最低限のIAMポリシーだけを付与できるようにするための構成だと考えられます。単一関数にするとどうしてもCRUD全ての権限を付けざるを得ないため、関数を分割することで権限分離をしっかり行うベストプラクティスが反映されているようでした。 シナリオ3:AccessDeniedエラーの解決 Policies: [] でDynamoDB権限を付け忘れたLambdaのAccessDeniedエラーを解決するシナリオです。 ツール利用時はIAM Policy Autopilotの generate_policy_for_access_denied ツールを利用しました。 注:実際にAWS上へリソースを作成して再現したわけではなく、あらかじめ用意したエラーメッセージ(JSON)とLambdaコード・SAMテンプレートを入力として渡し、修正ポリシーがどのように生成されるかを確認しています。 エラーメッセージ { " statusCode ": 500 , " body ": " { \" error \" : \" AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/scenario3-data-writer-role/scenario3-data-writer is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/scenario3-data-table \" } " } 結果比較 項目 素のLLM IAM Policy Autopilot 原因特定 正しく特定 正しく特定 ポリシーJSON 一般的な記述(アカウントIDなし) 完全修飾ARN(リージョン+アカウントID) 検証手順 なし sam build → sam deploy → テストの手順を提示 どちらも根本原因( Policies: [] )は正しく特定できました。差が出たのはポリシーの精度で、IAM Policy AutopilotはエラーメッセージからアクションとリソースARNをパースし、ピンポイントの修正ポリシーを生成しました。 シナリオ4:AWSへのデプロイ設計 Express.js(PostgreSQL / Redis / WebSocket / 画像アップロード)アプリケーションのAWS構成設計とコスト見積もりを行うシナリオです。 ツール利用時はdeploy-on-awsプラグインのMCPサーバー(awsiac + awspricing)を利用しました。 アーキテクチャ比較 項目 素のLLM deploy-on-aws Plugin NAT Gateway あり($36/月) なし(Public Subnet + Public IP) RDS構成 Multi-AZ(高可用性) Single-AZ(コスト重視) Fargate 512 CPU / 1024 MB × 2タスク 256 CPU / 512 MB × 1タスク(ARM64) セキュリティ 標準的 enforceSSL , allowAllOutbound: false 設計方針 可用性・冗長性重視 コスト効率重視(必要十分) コスト比較 サービス 素のLLM deploy-on-aws Plugin 差 ECS Fargate $29.55 $8.99 -70% ALB $22.40 $20.66 -8% RDS PostgreSQL $27.36 $20.55 -25% ElastiCache Redis $11.68 $18.25 +56% NAT Gateway $36.14 $0 -100% 合計 ≈$134/月 ≈$71/月 -47% ElastiCache Redisのようにdeploy-on-aws側の方が高くなる項目もありますが、NAT Gatewayの削除やARM64採用などのコスト最適化により全体では約半額に収まっています。 ツール利用時は14回のMCP呼び出しが行われましたが、その中で試行錯誤も発生しました。たとえばECS Fargateの料金取得でフィルタが不正だったり、ALBのサービスコード名がAPI正式名称( AWSELB )と異なるためにエラーになったりと、エージェントが get_pricing_service_codes で正しいコードを探索する過程が見られました。 /deploy Skill による実行 シナリオ4を /deploy スラッシュコマンドでも実行してみました。Skillが5段階のワークフローを主導し、各フェーズで選定理由をテーブルで明示するなど、よりプロセスの透明性が高い出力が得られました。 注: /deploy は最後にAWSへ実際にデプロイするステップまで含むワークフローですが、今回はAnalyze → Recommend → Estimate → GenerateまでのIaCコード生成フェーズで停止させ、実際のデプロイは行っていません。 3方比較 観点 素のLLM MCP直接 /deploy Skill 正確性 一般知識に基づく推測 静的解析・API参照で裏付けあり 同左 + 構造化ワークフローで漏れを防止 コスト 冗長性重視で高コスト(≈$134/月) リアルタイム料金でコスト最適化(≈$71/月) コスト最適化 + 代替案の差額も提示(≈$87/月) プロセス Read/Writeのみ MCP呼び出し多数 Skill + MCP + cdk synth 検証ループ まとめ 今回はIAM Policy AutopilotとAgent Plugins for AWSを実際に使い、素のLLMとの出力の違いを4つのシナリオで比較してみました。 全体を通して感じた共通する価値は以下の点です。 最新のAWS情報に基づいた提案 : MCPサーバー経由でリアルタイムにドキュメント・料金を参照するため、知識カットオフの影響を受けない 実コード解析による根拠ベースの出力 : 推測ではなく、AST解析やAPI参照に基づくため信頼性が高い(IAM Policy Autopilotの特徴) 構造化ワークフローによる一貫した品質 : 毎回同じプロセスで進行するため、出力のばらつきが少ない 最小権限・ベストプラクティスの自動適用 : ARM64、関数分離、権限の粒度分離などが自動で適用される 一方、ツールがあればすべて完璧というわけではなく、料金取得の試行錯誤やテンプレート検索の条件調整など、エージェントが適応的に動作する場面も多く見られました。また、静的解析ベースのIAMポリシー生成では実際に使わないリソースへの権限まで含まれる場合があるため、生成されたコード・ポリシーは必ず人間がレビューしてからデプロイすることが重要です。 今回のように素のLLMとの出力差分を実際に確認してみると、ツールがどのような前提・ベストプラクティスに基づいて出力を生成しているかを把握することの重要性も感じました。便利だからと漫然と使うのではなく、ツールを導入することで何が変わるのか・どこまで任せられるのかをきちんと理解した上で、日々の開発に取り入れていきたいと思います。 参考文献 IAM Policy Autopilot - GitHub Simplify IAM policy creation with IAM Policy Autopilot, a new open source MCP server for builders | AWS News Blog Agent Plugins for AWS - GitHub Introducing Agent Plugins for AWS | AWS Developer Tools Blog
はじめに こんにちは、リテールハブ開発部の杉森です。 Vercel Labs が開発しているローカル API エミュレータ「emulate」が面白そうだったので、実際に触りながら AWS SDK (S3) との互換性、GitHub / Google の OAuth フロー、本番 API への切り替えまでを試してみました。 emulate とは emulate は Vercel Labs が開発しているオープンソースのローカル API エミュレータです。GitHub、Google、Slack、Stripe、AWS など 12 のサービスをローカルで再現でき、単なるモック(固定レスポンスを返すだけ)ではなく、ステートフルなデータストアと OAuth フローを備えています。 npx emulate の1コマンドで 12 サービスがポート 4000〜4010 で起動します。設定ファイルは不要で、デフォルトのユーザーとトークンが自動で作成されます。 emulate v0.4.1 vercel http://localhost:4000 github http://localhost:4001 google http://localhost:4002 slack http://localhost:4003 apple http://localhost:4004 microsoft http://localhost:4005 okta http://localhost:4006 aws http://localhost:4007 resend http://localhost:4008 stripe http://localhost:4009 mongoatlas http://localhost:4010 Tokens test_token_admin -> admin 起動直後から Authorization: Bearer test_token_admin で全サービスの API を呼び出せます。 # ユーザー情報を取得 curl http://localhost:4001/user -H "Authorization: Bearer test_token_admin" # リポジトリを作成 curl -X POST http://localhost:4001/user/repos \ -H "Authorization: Bearer test_token_admin" \ -H "Content-Type: application/json" \ -d '{"name":"hello-world"}' レスポンスは本物の GitHub API と同じ JSON 構造です。ステートフルなので、上で作成したリポジトリに対して Issue や PR を追加するといった操作もできます。 実際に試してみた emulate v0.4.1 / Node.js v22 の環境で、以下の3つの観点から検証しました。 AWS SDK (S3) との互換性: SDK 経由でそのまま使えるか GitHub / Google OAuth フローの実装: OAuth を含むアプリをローカルで開発できるか 本番 API への切り替え: 2 で作ったアプリのコードを変えずに本番で動かせるか AWS SDK (S3) との互換性を検証する emulate の AWS エミュレータが、AWS SDK v3 からそのまま使えるかを検証しました。検証には @aws-sdk/client-s3 3.1028.0 を使用しています。 emulate の認証の仕組み emulate ではすべてのサービスが Bearer トークン認証に統一されています。実際のサービスではそれぞれ認証方式が異なりますが、emulate 上ではどれも同じ Authorization: Bearer でアクセスできるようになっています。 また、登録されていないトークンでリクエストしても、フォールバック機構によりデフォルトユーザー(admin)として認証が通ります。テストコードでトークンの値を気にせず書けるのは便利でした。 この仕組みが AWS SDK との互換性に直接関わってきます。 困った点と対応内容 AWS SDK v3 から emulate の S3 エミュレータを使うには、以下の2つの対応が必要でした。 1. endpoint のパスが合わない emulate のルートは /s3/:bucket/:key 形式ですが、AWS SDK は /bucket/key にリクエストを送るため、パスが一致しません。endpoint を localhost:4007/s3 にすることでパスを合わせます。 2. 末尾スラッシュでルートが一致しない AWS SDK は PUT /s3/bucket-name/ のように末尾スラッシュ付きでリクエストを送りますが、emulate のルート定義にスラッシュがないためマッチしません。SDK ミドルウェアで末尾スラッシュを除去することで対応しました。 対応後の動作結果 上記2つの対応を入れることで、CreateBucket、PutObject、GetObject、ListObjectsV2、ListBuckets、DeleteObject といった主要な S3 操作はすべて動作しました。 なお、AWS SDK は SigV4 署名を送りますが、emulate は SigV4 を解釈しません。前述のフォールバック認証によりデフォルトユーザーとして通るため、credentials の値は { accessKeyId: "dummy", secretAccessKey: "dummy" } で動きます。 ただし、Presigned URL、S3 イベント通知、SQS との連携などは未対応です。AWS SDK のより広い互換性が必要な場合は別のライブラリ等を検討した方が良さそうです。emulate の AWS エミュレータは「REST API の形状をテストする」用途向けという印象です。 補足: 本記事で触れたルートのパス不一致や Presigned URL 未対応については、修正の PR がすでに存在しております。マージされれば上記の回避策は不要になりそうです。 OAuth フローを組み込む emulate を使ってみて特に便利だと思ったのは OAuth フローのエミュレーションです。GitHub OAuth でサインインして PR 一覧を表示するアプリを作って検証しました。 シード設定で初期データを定義する emulate は YAML で初期データ(シード)を定義できます。起動時にユーザー、リポジトリ、OAuth App が自動で作成されます。 github : users : - login : admin name : Admin User email : admin@example.com repos : - owner : admin name : test-repo auto_init : true oauth_apps : - client_id : emu_github_client_id client_secret : emu_github_client_secret name : PR Viewer App redirect_uris : - http://localhost:3000/auth/callback 以下は、今回作ったアプリの OAuth フローです。 emulate にアクセスすると、以下のような認可画面が表示されます。 シードで定義したユーザーをクリックするだけで認可が完了します。トークン交換や API コールは本物の GitHub API と同じエンドポイントで動作するため、アプリ側のコードは本番と同じ実装がそのまま使えます。 なお、シード設定は宣言的なデータ定義のみに対応しており、PR のようなリソースはシードで作成できません。API 経由で投入する必要があります。 Node.js から直接起動して開発環境を自動化する emulate は CLI( npx emulate )だけでなく、Node.js のコードから直接起動する API( createEmulator )も提供しています。これを使って、emulate の起動、テストデータの投入、Web サーバーの起動を1コマンドにまとめました。 import { createEmulator } from "emulate" ; const github = await createEmulator ({ service : "github" , port : 4001 , seed : config }) ; const google = await createEmulator ({ service : "google" , port : 4002 , seed : config }) ; npm run dev だけで全部起動する体験は快適でした。 本番 API への切り替えを検証する server.js はすべてのエンドポイント URL を process.env から読み取る設計にしました。emulate と本番の切り替えは .env.local と .env の読み分けだけで行えます。 # .env.local(emulate 用) GITHUB_URL=http://localhost:4001 GITHUB_API_URL=http://localhost:4001 GITHUB_CLIENT_ID=emu_github_client_id GITHUB_CLIENT_SECRET=emu_github_client_secret GITHUB_OWNER=admin GITHUB_REPO=test-repo # .env(本番用) GITHUB_URL=https://github.com GITHUB_API_URL=https://api.github.com GITHUB_CLIENT_ID=<実際の Client ID> GITHUB_CLIENT_SECRET=<実際の Client Secret> GITHUB_OWNER=<実際のオーナー名> GITHUB_REPO=<実際のリポジトリ名> { " scripts ": { " dev ": " node --env-file=.env.local dev.js ", " start:prod ": " node --env-file=.env server.js " } } 本番の GitHub OAuth App を作成し .env に設定して npm run start:prod で起動したところ、コード変更なしで本物の GitHub 認可画面が表示され、実際の PR 一覧が取得できました。 観点 emulate 本番 GitHub 認可画面 emulate のユーザー選択画面 GitHub の実際の認可画面 認可の操作 ユーザーをクリック 「Authorize」ボタン データ テストデータ リポジトリの実際の PR API キー 不要 実際の Client ID / Secret Google OAuth + Gmail API も同じパターンで追加しました。emulate 用の環境変数は以下の通りです。 # .env.local(emulate 用) GOOGLE_URL=http://localhost:4002 GOOGLE_TOKEN_URL=http://localhost:4002/oauth2/token GOOGLE_API_URL=http://localhost:4002 GOOGLE_CLIENT_ID=emu_google_client_id GOOGLE_CLIENT_SECRET=emu_google_client_secret # .env(本番用) GOOGLE_URL=https://accounts.google.com GOOGLE_TOKEN_URL=https://oauth2.googleapis.com GOOGLE_API_URL=https://www.googleapis.com GOOGLE_CLIENT_ID=<実際の Client ID> GOOGLE_CLIENT_SECRET=<実際の Client Secret> まとめ 3つの検証観点ごとに結論を整理します。 AWS SDK (S3) との互換性: endpoint のプレフィックス追加と末尾スラッシュ除去の2つの回避策を入れれば、主要な S3 操作は動作する。ただし Presigned URL 等は未対応で、より広い互換性が必要なら 別のライブラリを検討した方が良い。 GitHub / Google OAuth フローの実装: シード設定と認可画面の自動生成により、OAuth App の登録やテストユーザーの作成なしで OAuth フローを含むアプリの開発を始められる。OAuth フローをローカルで手軽にテストできるのは便利だった。 本番 API への切り替え: エンドポイント URL を環境変数に切り出しておけば、コード変更なしで本番に切り替えられる。 興味がある方はぜひ試してみてください。 参考リンク emulate GitHub リポジトリ emulate 公式ドキュメント
目次 はじめに ECR イメージスキャンとは 構成の全体像 検知の網羅性 通知のノイズ低減 認知のスピード コスト 試算の考え方 試算例 Terraform による構築 1. ECR スキャン設定 2. EventBridge ルール 3. SNS トピック 4. AWS Chatbot(Slack 通知) 実際の通知と運用 導入してみて まとめ はじめに こんにちは、開発本部開発1部トモニテグループのエンジニアの パンダム/rymiyamoto です。 2025年末に Next.js の React Server Components に DoS(サービス拒否)とソースコード露出の脆弱性が公開 され、App Router を使用するサービスでのアップグレード対応が求められました。 このように、利用しているフレームワークやライブラリに深刻な脆弱性が見つかることは珍しくありません。 こうした脆弱性が公開中のサービスに影響していないかを素早く把握できる体制を整えるべく、弊社でも ECR のイメージスキャンを導入しました。 本記事では、その取り組みの一つとして ECR のイメージスキャンを導入した際の設計・構築・運用について紹介します。 同じように ECR のイメージスキャンをこれから導入しようとしている方の参考になれば幸いです。 ECR イメージスキャンとは Amazon ECR のイメージスキャンは、コンテナイメージに含まれるソフトウェアの脆弱性(CVE)を検出する機能です。 スキャンには Basic Scanning と Enhanced Scanning の2種類があります。 項目 Basic Scanning Enhanced Scanning スキャンエンジン Clair(オープンソース) Amazon Inspector2 検出対象 OS パッケージの脆弱性 OS パッケージ + プログラミング言語パッケージ(npm, pip, Maven 等) スキャンタイミング プッシュ時 / 手動 プッシュ時 / 継続スキャン 料金 無料 有料(スキャンしたイメージ数に応じた従量課金) 構成の全体像 導入した構成は以下の通りです。 ECR Enhanced Scanning (Inspector2) ↓ 脆弱性検知 EventBridge Rule (CRITICAL のみフィルタ) ↓ SNS Topic ↓ AWS Chatbot → Slack チャンネルに通知 設計にあたって意識したのは以下です。 検知の網羅性 OS パッケージだけでなく言語パッケージもカバーしたかったため、Enhanced Scanning を採用しました。対応言語の詳細は公式ドキュメントを参照してください。 docs.aws.amazon.com 一方で、OS パッケージの脆弱性検知だけで十分なケースや、まずは無料で始めたいケースでは Basic Scanning も有力な選択肢です。自社の要件に合わせて検討してみてください。 通知のノイズ低減 すべての severity を通知すると対応が追いつかなくなるため、まずは CRITICAL に絞って運用を開始しました。実際に HIGH まで含めて試してみたところ、本当に対応すべき通知が埋もれかねないと感じたので、まずは CRITICAL で運用を開始し、必要に応じてフィルタを広げる方針としています。 認知のスピード 脆弱性の存在に気づかないことが一番のリスクなので、Slack への即時通知を組み込みました。Slack への通知方法としては EventBridge → Lambda で通知内容をカスタマイズする方法もありますが、今回はまず検知できる状態を素早く作ることを優先し、コードを書かずに構築できる AWS Chatbot を採用しました。 コスト Enhanced Scanning は Amazon Inspector2 の料金体系に基づきます。料金は以下の2つで構成されます(2026年4月時点)。 最新の料金は公式ドキュメントをご確認ください。 aws.amazon.com 初回スキャン: イメージがプッシュされた時のスキャン、$0.09 / イメージ 再スキャン: 継続スキャンにより新しい CVE が公開された際の自動再スキャン、$0.01 / イメージ 試算の考え方 スキャン頻度によってコストの構造が異なります。 スキャン頻度 発生するコスト 計算式 プッシュ時 初回スキャンのみ 月間プッシュ数 × $0.09 継続スキャン 初回スキャン + 再スキャン 上記 + 保持イメージ数 × 再スキャン回数/月 × $0.01 弊社では本番環境は継続スキャン、開発環境はプッシュ時スキャンで運用しています。本番環境では新しい CVE が公開されたタイミングでも即座に検知したいため継続スキャン、開発環境では脆弱性を含む実装が入った時点で素早く検知しつつコストも抑えたいためプッシュ時スキャンが適しています。 試算例 例えば、5つのリポジトリに対して月間100回プッシュし、本番では各リポジトリに2イメージを保持(計10イメージ)するケースで試算します。再スキャン回数は月にどれくらいの頻度で対象の CVE が新たに公開されるかに依存しますが、ここでは月15回程度を見込みました。 項目 計算式 コスト 初回スキャン 100 push × $0.09 $9.00 再スキャン 10 images × 15回 × $0.01 $1.50 月額合計 $10.50 実際のコストはリポジトリ数・プッシュ頻度・保持イメージ数によって変わるので、自社の運用に合わせて試算してみてください。 Basic Scanning(無料)と比較するとコストはかかりますが、言語パッケージの脆弱性検知や新規 CVE の自動再スキャンが得られることを考えると、検討する価値はあると思います。 Terraform による構築 1. ECR スキャン設定 まず ECR レジストリに対して Enhanced Scanning を有効化します。 resource "aws_ecr_registry_scanning_configuration" "this" { scan_type = "ENHANCED" rule { scan_frequency = "CONTINUOUS_SCAN" repository_filter { filter = "*" filter_type = "WILDCARD" } } } filter = "*" でレジストリ内のすべてのリポジトリをスキャン対象にしています。リポジトリを個別に指定する方法もありますが、新しいリポジトリを追加した際にスキャン対象への追加を忘れるリスクがあるため、ワイルドカードで全体を対象にしています。 scan_frequency は環境によって使い分けています。本番環境では CONTINUOUS_SCAN 、開発環境では SCAN_ON_PUSH を設定しています。 2. EventBridge ルール resource "aws_cloudwatch_event_rule" "ecr_scan_finding" { name = "ecr-scan-finding-notification" event_pattern = jsonencode ( { "source" : [ "aws.inspector2" ] , "detail-type" : [ "Inspector2 Finding" ] , "detail" : { "status" : [ "ACTIVE" ] , "severity" : [ "CRITICAL" ] , "resources" : { "type" : [ "AWS_ECR_CONTAINER_IMAGE" ] } } } ) state = "ENABLED" } resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" { rule = aws_cloudwatch_event_rule.ecr_scan_finding.name arn = var.ecr_scan_finding_sns_topic_arn } Enhanced Scanning では Inspector2 がスキャンエンジンとなるため、イベントソースは aws.inspector2 になります。 Basic Scanning の場合は aws.ecr になるので注意が必要です。 3. SNS トピック EventBridge から受け取ったイベントを AWS Chatbot に渡すための SNS トピックを作成します。 resource "aws_sns_topic" "ecr_scan_finding_topic" { name = "ecr-scan-finding-topic" } resource "aws_sns_topic_policy" "ecr_scan_finding_topic_policy" { arn = aws_sns_topic.ecr_scan_finding_topic.arn policy = data.aws_iam_policy_document.sns_ecr_scan_finding_topic_policy.json } data "aws_iam_policy_document" "sns_ecr_scan_finding_topic_policy" { # EventBridge からの Publish を許可 statement { sid = "AllowEventBridgeToPublishSNS" effect = "Allow" actions = [ "sns:Publish" ] principals { type = "Service" identifiers = [ "events.amazonaws.com" ] } resources = [ aws_sns_topic.ecr_scan_finding_topic.arn ] condition { test = "StringEquals" variable = "AWS:SourceAccount" values = [ data.aws_caller_identity.current.account_id ] } condition { test = "ArnEquals" variable = "aws:SourceArn" values = [ "arn:aws:events:$ { data.aws_region.current.name } :$ { data.aws_caller_identity.current.account_id } :rule/ecr-scan-finding-notification" ] } } # Chatbot からの Subscribe を許可 statement { sid = "AllowChatbotToSubscribe" effect = "Allow" actions = [ "sns:Subscribe" ] principals { type = "Service" identifiers = [ "chatbot.amazonaws.com" ] } resources = [ aws_sns_topic.ecr_scan_finding_topic.arn ] condition { test = "StringEquals" variable = "AWS:SourceAccount" values = [ data.aws_caller_identity.current.account_id ] } condition { test = "ArnEquals" variable = "aws:SourceArn" values = [ "arn:aws:chatbot::$ { data.aws_caller_identity.current.account_id } :chat-configuration/slack-channel/alert-to-slack" ] } } } SNS トピックポリシーでは、EventBridge からの Publish と Chatbot からの Subscribe のみを許可しています。 condition で発信元を絞ることで、意図しないリソースからの操作を防いでいます。 4. AWS Chatbot(Slack 通知) 最後に、SNS トピックのメッセージを Slack に転送する Chatbot の設定です。 resource "aws_chatbot_slack_channel_configuration" "chatbot_alert_to_slack" { configuration_name = "alert-to-slack" slack_channel_id = "XXXXXXXXX" # 通知先の Slack チャンネル ID slack_team_id = "XXXXXXXXX" # Slack ワークスペース ID iam_role_arn = var.chatbot_role_arn sns_topic_arns = [ var.ecr_scan_finding_topic_arn, # 他の通知用 SNS トピックもここに追加できる ] guardrail_policy_arns = [ "arn:aws:iam::aws:policy/ReadOnlyAccess" ] logging_level = "ERROR" } これで CRITICAL な脆弱性が検知された際に、Slack チャンネルに通知が届くようになります。 なお、AWS Chatbot では同じ Slack チャンネルに対して複数の configuration を作成できません。そのため configuration_name は alert-to-slack のように汎用的な名前にしています。こうしておけば、今後 WAF のアラートなど別の通知を追加したくなっても sns_topic_arns にトピックを足すだけで済みます。 実際の通知と運用 実際に届く通知は以下のような形式です。 最初は CVE の詳細まで Slack で確認できるものだと思っていたのですが、実際に届く通知には Inspector2 Finding というイベント名と対象の ECR イメージの ARN が表示されるだけで、CVE 名もパッケージ名も表示されませんでした。 そのため、EventBridge の input_transformer を使い、Chatbot のカスタム通知で通知内容を改善しました。 resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" { rule = aws_cloudwatch_event_rule.ecr_scan_finding.name target_id = "SendToSNS" arn = var.ecr_scan_finding_sns_topic_arn input_transformer { input_paths = { "severity" = "$.detail.severity" "title" = "$.detail.title" "description" = "$.detail.description" "repository" = "$.detail.resources[0].details.awsEcrContainerImage.repositoryName" } input_template = <<TEMPLATE { "version": "1.0", "source": "custom", "content": { "textType": "client-markdown", "title": ":rotating_light: ECR <severity> 脆弱性検出 [環境名 (AWSアカウントID)]", "description": "*重要度*: <severity>\n*リポジトリ*: <repository>\n*脆弱性*: <title>\n*詳細*: <description>" } } TEMPLATE } } ポイントは input_paths でイベントから必要な項目を抽出し、カスタム通知フォーマットで整形している点です。改善後の通知は以下のような形式です。 CVE-ID やパッケージ名、リポジトリ名が表示されるようになり、Slack 上で脆弱性の概要を把握できるようになりました。詳細な対応判断が必要な場合は Inspector2 のダッシュボードを確認する運用ですが、通知を見ただけで対応要否がわかることが増えました。 さらに通知内容を自由にカスタマイズしたい場合は、EventBridge → SNS → Chatbot の経路ではなく、EventBridge → Lambda で整形する方法もあります。 導入してみて CRITICAL に絞った判断はうまくいきました。最初の通知が来たときも「これは本当に対応が必要なものだ」と落ち着いて対処できたので、狙い通りでした。 一方で、Chatbot のデフォルトの通知では CVE の詳細が出ず、正直もう少し情報が出ると思っていました。実際に使ってみて初めて気づいた部分で、 input_transformer を使ってカスタマイズできることも後から知りました。 Terraform での複数環境展開やスキャン頻度の使い分けはすんなりいきました。 まとめ 今回は、フレームワークやライブラリの脆弱性に素早く対応できる体制づくりの一環として、ECR の Enhanced Scanning を導入した事例を紹介しました。 構成としては ECR Enhanced Scanning → EventBridge → SNS → Chatbot → Slack というシンプルなパイプラインですが、Terraform でコード化することで再現性のある形で複数環境に展開できました。 まず検知できる状態を作ることが第一歩、そこさえ超えれば運用しながら精度を上げていけます。本記事がその一歩を踏み出すきっかけになれば嬉しいです。 最後まで読んでいただきありがとうございました!
はじめに こんにちは。株式会社エブリーの開発1部の村上です。 弊社ではClaudeを非エンジニアも含めた全社に展開しており、業務のあらゆる場面で生成AIの活用を推進しています。 弊社のデータ基盤は、昨年TreasureDataとDatabricksを併用していた構成からDatabricksに統一しました。(この移行の話は今週の 「第3回 Youは何しにDatabricksへ!?」 で「データ基盤をTreasureData + DatabricksからDatabricksへ統一する話」として弊社のデータエンジニアの吉田がお話しする予定なので、ぜひご参加ください。)基盤が統一されたことで、次のステップとして見据えているのが「AIを活用したデータの民主化」です。 AIの進歩によって、ずっと掲げてきたこのテーマがいよいよ現実味を帯びてきました。MCP経由で社内のデータを取得し、AIと対話しながら分析を進め、今までにないインサイトを得る。そんな世界がすぐそこまで来ています。 一方で、 「AIを使えばデータが簡単に出せる」ことと「現場で信頼して意思決定に使えるレベルの分析ができる」 こととの間には、まだまだ大きな壁があります。 AIはとても賢いですが、私たちの会社のこと、事業のこと、プロダクトのことを詳細には知りません。そのため、聞きたいことをそのまま質問してもその意図を正確に理解できず、全く違うデータを返してしまったり、生成するクエリが微妙に間違っていて正しいデータが出せなかったりします。結果として「使い物にならない」となってしまうわけです。 今回は、そんな状態からどのように進めていくことで「現場で使えるAI分析基盤」を作れるのか、Databricks環境で試行錯誤している話をします。 Databricks Genieとは こうした課題を解決するためにDatabricksが提供しているのがGenieです。Genieは自然言語でデータに対して質問すると、SQLを自動生成して結果を返してくれるインターフェースです。 docs.databricks.com ただし、これは単にDatabricks上でLLMを呼び出してSQLを書かせるだけのものではありません。 LLMの限界を理解した上で、自分たちの組織専用にチューニングできるように設計されている のがGenieの本質です。 Genie Space — 目的特化の分析空間 Genieでは「Genie Space」という単位でデータの分析空間を作ります。会社にはさまざまな部門があり、それぞれが見たいデータや使う用語は異なります。Spaceではそのそれぞれに適したコンテキストを設定できるようになっており、登録するテーブルを指定することで必要なデータだけにアクセスさせることができます。 たとえば営業チーム向けのSpaceにはSalesforceのデータを、ECチーム向けのSpaceには注文・顧客データを登録するといった具合です。1つのSpaceに登録できるテーブルは最大30件で、むやみに広げるのではなく、そのSpaceが答えるべき質問の範囲に絞ることが推奨されています。 Knowledge Store — AIのためのコンテキストを整える仕組み 各Genie Spaceには「Knowledge Store」と呼ばれるコンテキストをチューニングする機能が備わっています。これがGenieを組織専用に育てていくための中核です。Knowledge Storeには以下の要素があります。 Metadata: テーブルやカラムの説明文、同義語、不要カラムの非表示。GenieがSQLを組み立てるための基礎知識 Prompt Matching: カラムの実際のデータ値をGenieに事前認識させ、ユーザーの言葉とデータ値のマッチング精度を上げる Joins: テーブル間の結合条件を定義。Genieが複数テーブルをまたぐクエリを正しく書けるようにする SQL Expressions: Filter(条件定義)、Measure(指標の計算式)、Dimension(グルーピング定義)をSQLで直接登録 Example SQL: よくある質問に対する正解SQLをテンプレートとして登録 General Instructions: テキストでの補足指示 ここからは、実際にSpaceを作ってKnowledge Storeを育てていく過程を、実際に行なった試行錯誤とともに解説していきます。 まずは何もチューニングせずに聞いてみる まずやったのは、最小限の設定だけでGenie Spaceを作って、いきなり質問してみることです。テーブルにはカラムコメント(日本語の説明文)を付与済みで、General Instructions(テキストの指示文)にはビジネスコンテキストを3行だけ書きました。 これはECサービスのデータが格納されているスペースです。 ECカートからのトランザクションデータを元に、事業KPIを分析します。 日本語で回答してください。 この状態でいくつかの質問を投げてみた結果がこちらです。 質問 Genieの挙動 正誤 先月のGMVは? キャンセル・返品済みの注文も含めて集計 ❌ 先月の割引額は? 割引関連の3カラムのうち1つだけで集計 ❌ 先月の定期購入のGMVは? データ値を英語で推測し、0件ヒット ❌ 先月の1人あたり月間購入金額は? 分子に使うべき指標を別の指標と混同 ❌ 先月のキャリア決済のGMVは? 一部の決済方法を集計から漏らした ❌ 5問中、正解はゼロ。しかし、間違い方には共通するパターンがあります。 ビジネスルールを知らない 「KPI集計時にはキャンセル・返品を除外する」というルールをGenieは知りません。そのためフィルタなしで集計してしまいます。 言葉の定義が曖昧 「割引」と聞かれたとき、 discount というカラム名だけを見てそれだけで完結したと判断しました。実際には複数のカラムを合算する必要があるのですが、ビジネスの定義を知らなければわかりません。 データの中身を知らない 受注種別カラムには「定期受注」「通常受注」という日本語の値が入っているのに、Genieは英語の 'subscription' で推測して何もヒットしませんでした。 似た指標を区別できない 税込の総額と税抜の売上高を混同したり、決済方法のグルーピングが期待と一致しなかったり。似た概念が複数存在する領域で間違いが起きやすいことがわかりました。 AIは知らないことを推測で埋めようとします。それ自体は賢い振る舞いですが、ビジネスでは「もっともらしい間違い」が一番危険です。ここから「AIに正しく教えていく」プロセスが始まります。 AIにデータを活用できるようにするためのステップ 先述のKnowledge Storeの機能を使い、実際に設定を追加してはテストし、間違えたらまた設定を足すという繰り返しで精度を上げていった過程を紹介します。 1. メタデータ整備 — まずAIにデータの地図を渡す Genieがテーブル構造やカラムの意味を理解できなければ、そもそもSQLを正しく組み立てることさえできません。個人でClaudeを使うなら自分専用のテーブル定義書を作ってコンテキストに含めればいいですが、組織で複数人が使う場合にはスケールしません。 そこで重要になるのが、Databricksの Unity Catalog でのメタデータ管理です。 テーブル・カラムの説明文 Unity Catalogではテーブルやカラムに対してCOMMENTを付与できます。 COMMENT ON COLUMN orders.subtotal IS ' 小計(税抜商品売上)。定期割引適用済み ' ; COMMENT ON COLUMN orders.total IS ' 注文合計(税込)。GMV計算に使用 ' ; COMMENT ON COLUMN orders.revenue IS ' 売上高(subtotal + deliv_fee + charge)。税抜合計 ' ; カラムの説明は「何が入っているか」だけでなく「何に使うか」「何と違うか」まで書くと、Genieの精度が大きく変わります。特に似た概念のカラムが複数ある場合(GMV / 売上高 / 商品売上など)は、区別を明示することが重要です。 Genie Space上の同義語 ユーザーはいつも同じ言い方で質問するとは限りません。「UU」「ユニーク顧客数」「月間ユーザー数」はすべて同じ指標を指しています。Genie SpaceのMetadata設定でカラムに同義語を登録しておくことで、こうした表記揺れを吸収できます。 Prompt Matching Genieにはカラムの実際のデータ値を事前に認識させる機能があります。 Format Assistance: カラムからサンプルデータを取得して、どんな値が入っているかをGenieに学習させる Entity Matching: カテゴリカラムのユニーク値をリスト化して保存し、ユーザーの言葉と実際のデータ値をマッチさせる たとえば先ほどの「定期購入のGMV」問題。これは受注種別カラムの値が日本語であることをGenieが知らず、英語で推測してしまったことが原因でした。Prompt Matchingを有効にすることで、Genieは実際のデータ値を事前に把握した状態で質問に答えられるようになります。 ただし、Prompt Matchingは値を「見せる」機能であり、「使わせる」保証はありません。あくまで補助的な役割です。確実にビジネスロジックを定義するには、次のステップが必要です。 2. SQL Expressionでビジネスロジックを定義する 自然言語での質問には、データ上の定義とのギャップが必ず存在します。ユーザーが「売上」と言ったとき、それがGMV(税込総額)なのか売上高(税抜)なのか商品売上(商品のみ)なのかは、ビジネスの文脈を知らなければ判断できません。 Genie SpaceのKnowledge Storeでは、 SQL Expression としてこのビジネスロジックをSQLで直接定義できます。SQL Expressionには3つの種類があります。 Filter — 条件の定義 「有効注文のみで集計する」というビジネスルールをFilterとして定義します。 名前 SQL 同義語 Instructions 有効注文 orders.state NOT IN ('canceled', 'returned') 有効注文, KPI対象 GMV・売上・注文数など金額や数量を集計するクエリでは必ず適用すること。キャンセル分析時のみ適用しない Filterを設定する前は集計に不要なデータが含まれていましたが、設定後は正しい値が返るようになりました。 Measure — 指標の定義 ビジネスで使うKPIの計算式をMeasureとして定義します。 名前 SQL 同義語 Instructions 割引額 SUM(orders.subscription_discount + orders.discount + orders.point) 割引額, 割引合計, 値引き 定期割引 + クーポン割引 + ポイント利用の合計 設定前は割引に関連するカラムのうち1つだけが使われていましたが、設定後は3カラムの合算で正しい値を返すようになりました。 同様に、「1人あたり月間購入金額」もMeasureで定義することで、分子と分母に使う指標が正しく固定され、安定して正確な結果が得られるようになりました。 Dimension — グルーピングの定義 データ上は複数種類ある決済方法を、ビジネスで見たいグループにまとめるDimensionを定義します。 CASE WHEN orders.payment_method IN ( ' ドコモ払い ' , ' au決済 ' , ' ソフトバンク払い ' ) THEN ' キャリア決済 ' WHEN orders.payment_method = ' クレジットカード ' THEN ' クレジットカード ' WHEN orders.payment_method LIKE ' 後払い% ' THEN ' 後払い ' ELSE orders.payment_method END Dimensionを定義する前は、Genieが毎回自力でCASE WHENを書いていたため、聞き方によってグルーピングが変わるリスクがありました。定義後は「決済グループ別のGMVは?」と聞くだけで毎回同じロジックが適用されます。 3. Example SQLで信頼性を引き上げる SQL Expressionが「部品」だとすると、Example SQLは「完成品の見本」です。よくある質問パターンに対する正解SQLを丸ごと登録しておくことで、Genieはそのテンプレートを参考にSQLを生成します。 Example SQLの設定で重要なポイントが3つあります。 1. タイトルはユーザーが実際に聞く質問文にする Genieはタイトルとユーザーの質問をマッチングしています。「定期購入GMVクエリ」ではなく「先月の定期購入のGMVは?」と書くことで、マッチング精度が上がります。 2. Usage Guidanceでいつ使うかを明示する 「定期購入のGMV」「定期のGMV」「サブスクのGMV」と聞かれたとき、のように具体的な発動条件を書きます。 3. 全Example SQLに共通のフィルタパターンを含める これが最も効果的でした。すべてのExample SQLに有効注文フィルタを含めておいたところ、Example SQLに直接マッチしない新しい質問に対しても、Genieが同じフィルタパターンを自然に適用するようになりました。Example SQLはGenieにとって「スタイルテンプレート」としても機能するのです。 -- タイトル: 定期購入のGMVは? SELECT SUM (orders.total) AS gmv FROM orders WHERE orders.kind = ' 定期受注 ' AND orders.state NOT IN ( ' canceled ' , ' returned ' ) AND fct_orders.order_date >= :start_date AND fct_orders.order_date < :end_date Example SQLを パラメータ化 すると、そのクエリがそのまま使われた場合に応答に「Trusted」ラベルが付きます。これはGenieが検証済みのクエリをそのまま実行したことを示すもので、結果の信頼性をユーザーに保証する仕組みです。 究極的には、レビュー済みのクエリが使われるのが一番精度が高く、出力が安心できます。Trustedラベルがどんどんつくようになれば、ユーザーがデータを疑う回数は極端に減っていきます。 General Instructionsは最後の手段 ここまでの3ステップで、大半の課題は解決します。General Instructionsには何を書いたかというと、最終的にこれだけです。 これはECサービスのデータが格納されているスペースです。 ECカートからのトランザクションデータを元に、事業KPIを分析します。 日本語で回答してください。 たった3行。なぜこれだけでいいのかというと、 テキストの自然言語指示はGenieの行動を強制する力が最も弱い からです。 Genieは複合AIシステムであり、単一のLLMではありません。テーブルのメタデータ、SQL Expression、Example SQL、サンプル値、チャット履歴など、周辺のあらゆる情報を総合的に参照してSQLを生成します。多くの場合、General Instructionsに書きたいことは、SQL ExpressionやExample SQLでより堅牢に定義できます。 実際、当初はGeneral Instructionsに「KPI集計時はキャンセル・返品を除外すること」と書いていましたが、それだけでは適用されないケースがありました。SQL ExpressionのFilterとして定義し、さらにExample SQLのパターンで学習させることで、ようやく安定して適用されるようになりました。 Databricksの公式ドキュメントでも「instructionsは他の方法で対応できない場合の最終手段」と 明記 されています。 Knowledge Storeを育てた結果 ここまでの設定を積み重ねた結果、冒頭で全問不正解だった質問に対して、すべて正しい値を返せるようになりました。 対策したのは以下のようなシンプルな設定の積み重ねです。 SQL Expression: Filter、Measure、Dimensionの定義 Example SQL: よくある質問パターンの正解SQLを登録 Prompt Matching: カテゴリカラムの値を認識させる 一つひとつは小さな設定ですが、それぞれが特定の間違いパターンに対応しており、積み重なることでGenieの応答精度は着実に向上していきます。 育てたGenie Spaceを組織で活用する Genie Spaceをある程度チューニングしたら、次はそれを組織で活用して育てるフェーズです。 Genie MCP — ClaudeからGenieを直接使う DatabricksのManaged MCP Serverを使えば、Genie SpaceごとにMCPエンドポイントを作成できます。 https://<workspace>/api/2.0/mcp/genie/<genie_space_id> これをClaude.aiのConnectorに登録すると、普段使いのClaudeから直接Genieに質問できるようになります。ユーザーはDatabricksの操作を覚える必要がなく、いつも使っているClaudeで自然言語で質問するだけです。裏側でGenieがKnowledge Storeを参照しながら正確なSQLを生成し、結果を返します。 弊社ではClaudeを全社展開しているため、各部門のGenie Spaceを作ってそれぞれのMCPをClaudeに登録すれば、非エンジニアでも自分の部門のデータに自然言語でアクセスできる環境が作れます。 Monitoringで改善サイクルを回す Genie SpaceのMonitoringタブでは、ユーザーが実際に投げた質問と応答を確認できます。うまく答えられなかった質問は、Benchmarkに追加し、Knowledge Storeの設定を改善し、再度評価する。このループをチームで地道に回していくことが、Genie Spaceの精度を継続的に向上させる鍵です。 おわりに AIが自社データを"わかる"ようになるには、一度の設定では終わりません。使って、間違いを見つけて、設定を足して、テストする。その繰り返しです。 改善は地道ですが、 これをやり切った組織とそうでない組織では、プロダクト改善のスピードや事業成長のスピードに取り戻せないほどの差が生まれてくる と考えています。 AIの進化によって、データ分析の主役はSQLを書けるエンジニアから、事業を深く理解しているビジネスメンバーへとシフトしていきます。そのとき、AIが正しく答えてくれるための「土台」を整えておくことが、データ基盤をみるエンジニアの役割の一つだと思っています。私たちの組織ではこれを全社を巻き込んで主導していきたいと思っています。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!
はじめに こんにちは。 開発本部 開発1部 デリッシュリサーチでデータエンジニアをしている吉田です。 今回は、DatabricksのUnity Catalog管理下のテーブルを、自然言語で検索できるClaude Codeスキルを構築した話を紹介します。 背景 以前の記事 では、Databricks Managed MCP Serverを通してUnity Catalog Functionを実行することでテーブルのスキーマ情報を取得する方法を紹介しました。 この仕組みは便利でしたが、テーブルのパスを把握していることが前提でした。 しかし実際の運用では「あのデータはどのテーブルだっけ?」というケースが多く、テーブルがわからない状態から対象のテーブルを探したいというニーズがありました。 そこで今回は、社内で活用が進んでいるClaude Codeのスキルとして、自然言語でテーブルを検索するスキルを作成します。 スキルの概要 スキルの動作フローは以下のようになります。 ユーザが質問 : 「アプリの動画視聴ログはどこ?」 Claude CodeがSkillを起動 質問文からLLMが文脈に応じてキーワードを抽出 : アプリ/動画/視聴/app/search など scriptを利用して検索SQLを作成 Databricks DBSQL MCPを使ってsystemテーブルに対してクエリを発行 結果を受け取り解釈、イマイチな場合、キーワードを変えて再検索 Claude Codeが回答 : 「最有力候補は以下です。その他候補は ~ です」 アプローチとしてテーブル情報をVector Searchすることも考えましたが、今回は簡単な手法を選択しました。 Databricks Managed MCPの活用 今回のスキルでは、Databricksが提供するManaged MCP ServerのDBSQLを利用しています。 https://docs.databricks.com/gcp/ja/generative-ai/mcp/managed-mcp 以前の記事ではUnity Catalog FunctionsのMCP Serverを使い、事前に定義した関数を呼び出すアプローチでした。 今回は任意のクエリを実行できるDBSQLのMCP Serverを使い、SQLを直接実行するアプローチを取っています。 DBSQL MCPでは以下のツールを提供しています。 execute_sql 任意クエリの実行ツール execute_sql_read_only Select,Showなど読み取りクエリの実行ツール poll_sql_result 長時間実行されるクエリの結果を取得するツール execute_sql ツールは、 Delete , Drop などの危険なクエリを発行できるため、必要に応じて利用制限を行うのが良いです。 // settings.json { " permissions ": { " deny ": [ " mcp__<mcp_name>__execute_sql " ] } } DBSQL MCPは標準で提供されており、すぐに利用する事ができます。 systemテーブルによるメタデータ検索 Databricksの system.information_schema にはカタログ配下のオブジェクトに関するメタデータが保存されています。 https://docs.databricks.com/aws/ja/sql/language-manual/sql-ref-information-schema 主に system.information_schema.tables を利用してテーブル名やコメント名に対してLIKE検索を行うことでテーブルを探しています。 SELECT table_catalog, table_schema, table_name FROM system.information_schema.tables WHERE LOWER (table_name) LIKE ' %<keyword1>% ' OR LOWER ( comment ) LIKE ' %<keyword1>% ' Claude Codeスキルとして実装 スキルの構成 Claude Codeのスキルは SKILL.md というファイルで定義します。 今回のスキルのディレクトリ構成は以下のとおりです。 ~/.claude/skills/<skill-name>/ ├── SKILL.md # スキル定義 └── scripts/ ├── _common.py # 共通ユーティリティ ├── gen_search_table_query.py # テーブル検索(基本) ├── gen_get_columns_query.py # カラム情報取得 └── gen_get_sample_data_query.py # サンプルデータ取得 SKILL.mdのfrontmatterで、スキルが使用できるツールを制限しています。 --- name : <skill-name> description : Databricks Unity Catalogのテーブル検索スキル。 context : fork allowed-tools : - Bash - mcp__databricks-sql-mcp__execute_sql_read_only - mcp__databricks-sql-mcp__poll_sql_result --- context: fork スキルを独立したサブプロセスで実行し、メインの会話コンテキストを汚さない allowed-tools Bashに加え、読み取り専用のMCPツールのみを許可 SKILL.md の本文にはワークフロー(検索の手順)や出力ルール(最有力候補1件+その他最大4件に絞り込む等)を記述しており、Claude Codeはこの指示に従ってSQLを生成・実行します。 sqlglotによるSQL生成 SQLは sqlglot ライブラリを利用したpythonスクリプトで生成しています。 https://sqlglot.com/sqlglot.html プロンプトでSQLの生成をAgentに任せる方法と比べて、確実に目的のクエリを作成することができます。 SKILL.md中に以下のようなコマンドを指示することでSQLを作成しています。 uv run --no-project --with sqlglot scripts/<スクリプト名>.py <パラメータ> sqlglotでのクエリ生成は以下のようにして行っています。 gen_search_table_query.py,_common.pyから抜粋 # gen_search_table_query.py from sqlglot import exp, select from _common import like_or def build_query (keywords: list [ str ]) -> exp.Expression: return ( select( "table_catalog" , "table_schema" , "table_name" , "comment" ) .from_( "system.information_schema.tables" ) .where(like_or([( "table_name" , None ), ( "comment" , None )], keywords)) ) # _common.py def like_or ( columns: list [ tuple [ str , str | None ]], keywords: list [ str ] ) -> exp.Expression: """Build OR chain: LOWER(col) LIKE '%kw%' for each (column, keyword) pair.""" conditions = [ exp.Like( this=exp.Lower( this=exp.Column( this=exp.to_identifier(col), table=exp.to_identifier(tbl) if tbl else None , ) ), expression=exp.Literal.string(f "%{kw}%" ), ) for kw in keywords for col, tbl in columns ] return reduce ( lambda a, b: exp.Or(this=a, expression=b), conditions) 検索精度とデータカタログの重要性 今回のスキルの検索精度は、テーブルやスキーマに付与されたコメントの充実度に強く依存します。 テーブル名とコメントをLIKEで検索するため、コメントが空のテーブルはテーブル名のみでしかマッチしません。 十分な結果が得られなかった場合はサンプルデータの取得や別キーワードでの再検索など探索的にテーブルを探しますが、判断材料としてコメントは非常に重要です。 データカタログの育成こそが、データ活用全体の効率化につながると考えています。 まとめ Databricks Managed MCP ServerのDBSQLとsystemテーブルを組み合わせることで、Unity Catalogのテーブルを自然言語で探索できるClaude Codeスキルを構築しました。
目次 はじめに AIの発展と開発スピードの変化 PRレビューの負荷 レビューに要する時間の増加 レビューの何が負荷なのか レビュー負荷への対処 仕組みでの対処 Claude Code plugin を活用したレビューの効率化 エージェント構成の概要 レビュー結果の出力イメージ 仕組み 課題 おわりに はじめに こんにちは。 開発本部開発1部トモニテ開発部所属の庄司( @ktanonymous )です。 最近では、AI の性能向上や開発フレームワークの進化による開発スピードの向上に伴い、 これまで以上に大量のコードをレビューしていく中で、負荷が大きくなっていると感じています。 本記事では、どういった点で負担を感じているのか、それに対してどのようなアプローチができるのか、 自分なりに考えてみたことをまとめていきたいと思います。 AIの発展と開発スピードの変化 AIの性能向上という面では、CursorやClaude Codeをはじめとするコーディングエージェントやモデル自体の進化により、 AIにコンテキストを渡して実装を依頼するだけで、実装計画の策定からコーディング、テストの追加までを自律的に行わせることが現実的になっています。 さらに、こうしたAIの性能向上に加えて、ドキュメント駆動開発/仕様駆動開発のような実装以外のフェーズにもAIを組み込むフレームワークの発展も目覚ましいです。 以前の記事ではAI駆動開発を見据えたドキュメント運用について整理してみましたが、 弊社の別チームでもエンジニアが仕様書を主導して書くことでAIによる設計・実装を加速させる取り組みが紹介されています。 tech.every.tv tech.every.tv 実装フェーズのスピードアップに加え、ドキュメント運用や仕様策定といった上流工程にもAIが入ることで、開発フロー全体が大きく加速しています。 こうした変化自体は非常にポジティブですが、一方で開発フローの中で人間が直接対応している部分がボトルネックとしてこれまで以上に顕在化しているように感じています。 PRレビューの負荷 レビューに要する時間の増加 レビュー対象には大きく分けると、以下の2種類に分類できます。 自分のアウトプット 他のメンバーのアウトプット また、個人的に、AIの出力を100%信頼できる状況にはまだなっていないと思っていて、 その他にもレビュー活動は以下のような役割を担ってくれているのが現状なのではないかと考えています。 コードの品質担保 : バグや設計上の問題を早期に発見する チーム内の知識共有 : 他のメンバーの変更を把握し、コードベースへの理解を深める機会になる コードベースの一貫性維持 : チームとしてのコーディング方針や設計思想を保つ ビジネスロジックの妥当性判断 : 仕様やドメイン知識に基づく判断は、プロジェクトの文脈を深く理解している人間でないと精度が出しにくい 実装速度の向上に伴い、PRの作成頻度は高まっていますし、適切にAIをコントロールしないと1回の変更量や粒度なども大きくなってしまいがちです。 実際に自分の業務の中でも、コーディングとレビューの比率が以前までは 7:3 程度だったのが、最近は 3:7 くらいになっているように感じています。 特に、自分のアウトプットに関しては、自分は指示役でAIがアウトプットするようになって他の人のアウトプットと近い感覚でレビューをするようになりました。 その結果、ここにかかる労力や時間も少なからず増えています。 そのような背景もあり、レビューがボトルネックになっているという感覚やレビューを通じての疲労感を以前にも増して感じています。 理想的にはPRレビューを全てAIに委譲できれば、レビュー待ちによるブロッキングはなくなります。 しかし、先ほども述べたように、現時点で人間のレビューを完全になくすのは難しいと感じています。 一方で、レビューの全てが人間にしかできないわけではないとも思っています。 「レビューをなくす」のではなく「レビューの負荷を分解して、適切に分担する」という考え方で、 機械的にチェックできる部分についてはツールやAIに委譲し、本質的なレビューに人間が集中できるようにする体制を多くの人が整備しているのではないかと思います。 レビューの何が負荷なのか そもそもレビューのどういった部分に負荷を感じているのかを考えた時、 個人的には、最近レビューをしている中で、以下のような辛みポイントがあるかなと感じています。 レビューするべきものが多い コンテキストの把握(PRの背景や仕様の確認) (以前より)レビューの優先度を考えないといけない 1つ目はシンプルで、レビューするべきものが多いほど負担が増えます。 2つ目に関しては、1つ目とも相まって、時間に対して把握する必要のあるコンテキストが多くなっているので、 これまで以上に負荷のある作業となっています。 3つ目に関しては、レビューするべきものが複数ある状況において、どれからレビューしていくのか、 その中でもどこからレビューしていくのか、優先度や効率を以前よりも意識しないといけないように感じています。 レビュー負荷への対処 ここで、仕組みの面での対処の例を挙げてみます。 仕組みでの対処 ルールやCIを整備することでレビュー負荷の軽減が見込めます。 コーディング規約・スタイル linterやformatterの導入・徹底はやはり重要で、CIでの強制など、 レビュー以前の段階で静的検査が完了していれば、レビュアーはその部分を気にする必要がなくなります。 テストの充実化 テストが要件を適切に表現できていれば、レビュアーとしても 実装が正しく動くか、要件を満たしているのかを判断しやすくなります。 AIレビューツールの活用 GitHub ActionsなどでAIによる自動レビューを組み込むことで、 人間のレビュアーが確認すべきポイントをある程度絞ることができます。 このように、ルールやCIの整備によって「本質的な判断」に集中できる状態に近づけられます。 (なお、レビュー対象の粒度や背景となる情報など、レビュアーに与えるコンテキストの調整によっても負荷の軽減は可能であり、 人間がAIをコントロールして、これらの仕組みがより効果的に働くように意識することも重要だと思います。) Claude Code plugin を活用したレビューの効率化 個人的には先に挙げたような点がレビューをしていて辛いなと感じていたので、 仕組みでの対処とは別に、Claude CodeのカスタムPluginを利用してレビューの効率化を図っています。 Claude Codeには公式のcode-reviewプラグイン 1 が提供されています。 これの構成を踏襲しつつ、個人的に以前から利用しているレビューフォーマットやドキュメントなどの外部コンテキストの取り込みを含めたプラグインを作成して利用しています。 1次レビューをAIに任せるようにしたことで、自分の作業をブロックせずにレビューの基盤を整理しておくことができるようになり、 全体感や内容の把握、優先度の判断に要する負担がある程度減らせました。 エージェント構成の概要 プラグインの処理は、情報収集・外部コンテキスト取得から、専門エージェントによる並列レビュー、信頼度の再評価を経て、レポート生成に至る流れになっています。 エージェント構成の概要 レビュー結果の出力イメージ プラグインは、レビュー結果をマークダウン形式のレポートとしてローカルに出力します。 GitHubにコメントを直接投稿しない設計にしており、レビュー結果を自分で確認・判断してからフィードバックを行うことを想定しています。 実際のレポートは以下のような構成で出力されます。 # PR Review: everytv/repo#123 — PRタイトル ## 変更の概要 ### 背景 / 変更内容 / 実現できる根拠 / メリット・デメリット ## 指摘事項 ### Critical — 信頼度 90-100 ### Important — 信頼度 80-89 ### Suggestion — 信頼度 60-79 ## 良い点 ## レビューガイド ### 推奨レビュー順序 以下では、このレポートの主要な部分がどのように生成されるかを簡単に紹介します。 仕組み コンテキストの自動収集 レビュー時の「コンテキストの把握」の負荷を軽減するために、PRの基本情報(概要・コミット履歴・変更ファイル一覧)に加え、 PR本文に含まれるissueやConfluenceなどの関連リンクを解析して外部コンテキストを取得します。 指摘事項 指摘事項は、構成図に示した3つの専門エージェント(code-reviewer、test-analyzer、design-reviewer)が並列にレビューを行い、その結果を信頼度に基づいてフィルタリングすることで生成されます。 code-reviewerはバグ検出やセキュリティ、パフォーマンスの観点で、test-analyzerはテストカバレッジや設計品質の観点で、design-reviewerは設計原則や既存コードとの整合性の観点で、それぞれレビューを行います。 公式プラグインと同様に0-100の信頼度スコアを導入し、confidence-verifierというエージェントが各指摘を独立した視点で再検証します。 レビュー結果のレポートには、信頼度スコアおよびその判定理由を記述させています。 レビューガイド レポートの「レビューガイド」では、推奨レビュー順序と各ファイルの注目ポイントを示します。 どこからレビューすればよいかの判断をツールに任せることで、優先順位付けの負荷の軽減を狙っています。 複数PRの並列レビュー レビューするべきものが多い場合に対応するため、batch-reviewスキルも用意しています。 複数のPR番号を指定すると、各PRを独立したエージェントが並列にレビューしてリポジトリごとのディレクトリに個別出力します。 まとまった数のPRをレビューする際に、事前にポイントを把握するための情報源として活用しています。 課題 プラグインはあくまでレビューの「補助」であり、完璧なレビューではありません。 そのため、ツールの出力をそのまま信頼してレビュー完了とはせず、内容を確認する必要があります。 おわりに 今回の記事では、PRレビューの負荷について自分が感じていることや、それに対して取り組んでいることを紹介しました。 AIの活用が進む中で、レビューの量やコンテキストの把握、優先順位付けといった部分に辛さを感じるようになりましたが、 仕組みの整備やプラグインの活用で少しずつ改善できている実感があります。 まだまだ試行錯誤の段階ではありますが、同じような課題感を持っている方の参考になれば幸いです。 最後まで読んでいただき、ありがとうございました。 Claude Code 公式 code-review plugin (2026年3月23日閲覧) ↩
はじめに こんにちは!トモニテで開発をしている吉田です。 2026/3/20〜2026/3/22に開催されたPHPerKaigi 2026にスタッフとして参加してきました! PHPerKaigi(ペチパーカイギ)とは以下のようなイベントです! PHPerKaigi(ペチパーカイギ)は、PHPer、つまり、 現在PHPを使用している方、過去にPHPを使用していた方、 これからPHPを使いたいと思っている方、そしてPHPが大好きな方たちが、 技術的なノウハウとPHP愛を共有するためのイベントです。 phperkaigi.jp しかしながら私自身、普段の業務でPHPは書いていません。どうして私が今回参加したのか、PHPを書いていない私が参加してどうだったのか書いていきます。 参加経緯 一番初めのきっかけは昨年開催されたiOSDC 2025で当日スタッフをしたことです。普段はGo言語やTypeScriptを書いていてiOSとは無縁でしたが、スタッフとして参加できました。 iOSDCとPHPerKaigiは実行委員長が同じです。その関係でiOSDC 2025終了後にスタッフの方からPHPerKaigiのスタッフについても話を聞いて、ぜひやってみたいと思い参加させてもらうことにしました! そんな私がどうして普段触る技術領域以外のカンファレンスでスタッフをしているのか。それは 仕事をしてるだけだと出会わない人たちと出会ってみたい!新しい世界を知りたい! と思ったからです。 ここからは実際に参加してみての感想を書いていきます。 参加してみて スタッフについて スタッフにはコアスタッフと当日スタッフの2種類があります。当日スタッフは会期前に事前の顔合わせを行い、当日の運営を担当します。コアスタッフは開催に向けて早い段階から事前準備を進めていてます。 私はコアスタッフとして参加しました。事前準備では名札を首から下げるためのストラップ制作を担当しました。 会期中はTrack Aを担当し、セッションごとに司会や演出を担当したりしていました。演出は場面に応じてスクリーンに映す内容を切り替えるといった役割です。 Track Aではオープニングや通常セッションに加えて、PHPer コードバトルやルーキーズLT、LT大会といった多様なコンテンツが行われていました。 もちろんメインは担当としての仕事ですが、シフトの合間にはスポンサーブースを回ったり、セッションも聞いていました。PHPのカンファレンスではありますが、スピーカーが話す内容はPHPを書いていないと分からないということはなく、それぞれが課題に対してどういうアプローチを取ったのかという手法の部分は、普段の技術領域にも活かせることがあるのではと感じました。 聞いたもの全て興味深かったのですが、特に面白かったものを紹介します。 PHPer コードバトル PHPer コードバトルは、指示された動作をする PHP コードをより短く書けた方が勝ちという 1 対 1 の対戦コンテンツです。予選を勝ち上がったプレイヤー6名がトーナメント形式で対決します。 スコアはコードの空白を除去したバイト数になります。 普段のサービスを動かすためのコードとはまた違うテクニック的な要素も必要になります。 ルールは分かるのですが、正直プレイヤーが具体的にどんなテクニックを使っているのかは分かりません(笑)。 ただ、会場のスクリーンにはリアルタイムにプレイヤーが書いているコードやその瞬間のプログラムサイズが表示され、解説者による解説があります! 個人的にはさながらスポーツ観戦をしているような臨場感で、プレイヤーが大きくプログラムサイズを減らすと会場がどよめくような瞬間もありました。 何より解説があるのでプレイヤーがどういう工夫をしているのか観戦者も知ることができます。 私がコードバトルで学んだのは && と and は優先順位が違うということです。いつかどこかで役立てたいと思います。 参考: PHP: 論理演算子 - Manual コードバトルはシフトが当たっていなくても会場に見に行っていたくらい面白かったです。Track Aの担当にならなければ見ることはなかったと思うので、スタッフをやったからこそ知れた面白さでした! ルーキーズLT/LT大会 ルーキーズLT大会はPHPerKaigiで初めてトークする「ルーキー」たちによる5分のショートトーク、LT大会はスピーカーを限定しないLT大会です。LT大会では参加者がペンライトを振る場面があるのですが、これが会場にとても綺麗な彩りを添えていました! 特に印象に残ったトークを2つ紹介します。まずルーキーズLTから: AI時代の脳疲れと向き合う「言語学としてのPHP」 - プロポーザル / 登壇資料 AI疲れは私自身実感していましたが、それを言語学の観点から考察しているのが新鮮で勉強になりました。ハイコンテキストな日本語話者がローコンテキストな指示を出そうとしていて、これが疲れの原因らしいです...。 「なんでこんな疲れるんだろう...」の原因を知ることができたので、これからは対策が取れそうです。 LT大会からは以下のトークです。 よし、PHPで円でも描いてみるか - プロポーザル / 登壇資料 PHPerKaigi 2024の登壇でもらった質問から「PHPで円を書いてみよう」ということになったそうです! Webやコンソールで描いてみたり、途中では電子工作をされていたり、最終的にはアニメーションする円を実現されていたりと、多種多様な円をPHPで描かれていました。実現過程も面白かったですが話術もすごくてたくさん笑わせてもらいながら聞いていました。 最後に PHPを書いていない私でも参加してみてどうだったかというと、十分に楽しめたし学びもありました。セッションで語られる課題へのアプローチは言語を問わず通じるものが多く、普段の開発にも持ち帰れる気づきがありました。また、コードバトルのようにスタッフとしてTrack Aを担当したからこそ出会えたコンテンツもあり、「仕事をしてるだけだと出会わない人たちと出会ってみたい、新しい世界を知りたい」という動機は十分に満たされました。 これからもカンファレンスのスタッフ活動を続けていきたいと思います!
はじめに こんにちは、デリッシュキッチンのバックエンドエンジニアの鈴木です。 先日、プロダクトのGoのバージョンを 1.25.4 から 1.26.0 へ アップデートしたところ、CI上の自動テストが一部落ちる(失敗する)問題に直面しました。 原因を調べてみると、テストデータの初期化で使っている TRUNCATE 処理において、これまで発生していなかった外部キー制約(Foreign Key Constraint)のエラーが頻発していることがわかりました。 コード自体はいじっていないにもかかわらず、なぜGoのバージョンを上げただけでデータベース操作が失敗するようになったのか。本記事では、このエラーの調査を通して改めて気付かされた、Goの database/sql パッケージにおけるコネクションプールの仕様と、安全なコネクション管理について共有します。 概要 Goの database/sql は内部でコネクションプーリングを行っており、 db.Exec() などのクエリ実行ごとに、プールからアイドル状態のコネクションを動的に取得・返却する仕組みになっている。 一方、MySQLの SET foreign_key_checks = 0 のような設定は、同一セッション(コネクション)内でのみ有効。 そのため、 db.Exec() を連続して呼んでも、同じコネクションで実行される保証はなく、別のコネクションが割り当てられると設定が反映されずにエラーになる。 解決策として、 db.Conn() を使ってコネクションを明示的に取得(占有)し、一連の処理が終わるまで同じコネクションを使い回す必要がある。 Go 1.26.0 へのアップデートと TRUNCATE の失敗 Goを 1.26.0 に上げたタイミングで、テストのクリーンアップ処理(テーブルのTRUNCATE)において、MySQLから以下のエラーが返るようになりました。 Error 1701: Cannot truncate a table referenced in a foreign key constraint ... MySQLで外部キー制約が張られているテーブルを TRUNCATE する場合、一時的に SET foreign_key_checks = 0 を実行して制約を無効化するのが一般的です。私たちのコードでもこの処理を入れていたはずですが、なぜか制約違反のエラーが発生していました。 問題となった実装 エラーが発生していた箇所のコードです。標準の *sql.DB を使って、3つのクエリを順番に実行していました。 // 外部キー制約を一時的に無効化してTRUNCATEを実行する実装 func (e *DBEngine) TruncateTable(tableName string ) error { // 1. 制約チェックの無効化 if _, err := e.db.Exec( "SET foreign_key_checks = 0" ); err != nil { return err } // 2. TRUNCATEの実行(ここで Error 1701 が発生) if _, err := e.db.Exec( "TRUNCATE TABLE " + tableName); err != nil { return err } // 3. 制約チェックの有効化 if _, err := e.db.Exec( "SET foreign_key_checks = 1" ); err != nil { return err } return nil } 一見すると上から順番に実行されるため問題なさそうに見えますが、この実装は各クエリが別々のコネクションで実行される可能性を考慮できていませんでした。 原因:コネクションプールとセッション変数の仕様の違い 今回の問題は、MySQLのセッション変数の仕様と、Goのコネクションプールの挙動のミスマッチが原因でした。 MySQLのセッション変数 MySQLの SET foreign_key_checks はセッション変数であり、その設定は 現在のセッション(コネクション)内でのみ 有効です。別のコネクションから繋ぎ直した場合、デフォルトの設定(通常は有効)に戻ってしまいます。 Goの database/sql の挙動 Goの db.Exec() は、実行されるたびにコネクションプールから空いているコネクションを1つ取得し、クエリを実行し終えるとすぐにプールへ返却します。 つまり、 コード上で連続して db.Exec() を書いても、同じコネクションで実行される保証はどこにもありません。 内部では以下のように、コネクションのすれ違いが発生していました。 fig.1: コネクションが切り替わることで設定が引き継がれずエラーになるフロー なぜ今までエラーにならなかったのか? これまでの環境(Go 1.25.4)では、このコードでも特にエラーは起きていませんでした。しかしこれは仕様として保証されていたわけではなく、単なる 実行タイミングの偶然 でした。 これまでは、以下の流れがたまたま成立していました。 SET foreign_key_checks = 0 を実行。 使い終わったコネクションが即座にプールへ返却される。 直後の TRUNCATE でプールからコネクションを取得する際、たった今返却されたばかりのコネクション(設定変更済み)がそのまま使い回される。 このように、他に並行して走っているクエリがない限り、実質的に同じコネクションが連続して割り当てられやすい状態になっていたに過ぎません。 なぜ Go 1.26.0 で顕在化したのか? Go 1.26.0 では、ランタイムのパフォーマンスが大きく向上しています。特に、デフォルトで有効化された新しいガベージコレクタ Green Tea GC によるスキャン待ち時間の削減や、メモリアロケーションの高速化などが含まれています。 こうしたランタイムの最適化によって、プログラム全体の実行速度やゴルーチンの切り替わりといった、マイクロ秒単位のスケジュールタイミングが微妙に変化しました。 その結果、 SET クエリを実行してコネクションが完全にプールへ戻る前に、次の TRUNCATE の処理が走り出し、プール側がいま空いている別のコネクション(設定変更されていないもの)を割り当ててしまうケースが増加したと考えられます。 つまり、Goのバージョンアップによるバグではなく、 Goのランタイムが高速化・効率化されたことで、アプリケーション側に潜んでいた実装上の不備が表面化した というのが真相です。 解決策:sql.Conn を使ってコネクションを固定する 同じコネクションを使って一連のクエリを確実に実行するには、Go 1.9から導入された db.Conn(ctx) を使用します。これを使うことで、プールから特定のコネクションを明示的に取得(占有)できます。 修正後の実装 func (e *DBEngine) TruncateTable(ctx context.Context, tableName string ) error { // 1. コネクションを明示的に取得(チェックアウト)する conn, err := e.db.Conn(ctx) if err != nil { return err } // 使い終わったら必ずプールへ返却する defer conn.Close() // 2. 確保した同一のコネクション(conn)に対してクエリを実行する if _, err := conn.ExecContext(ctx, "SET foreign_key_checks = 0" ); err != nil { return err } // 同じコネクションなので、設定が反映された状態で実行できる if _, err := conn.ExecContext(ctx, "TRUNCATE TABLE " + tableName); err != nil { return err } if _, err := conn.ExecContext(ctx, "SET foreign_key_checks = 1" ); err != nil { return err } return nil } sql.Conn オブジェクトに対してメソッドを呼び出し、処理が終わった後に Close() を呼ぶことで、セッション変数の設定を維持したまま安全にクエリを実行できるようになります。 まとめ MySQLの SET 構文のようなセッション依存の設定を行う場合、単純な db.Exec() の連続呼び出し(コネクションプール任せ)にしてはいけません。必ず sql.Conn などを使い、明示的にコネクションを占有して処理を行う必要があります。 今回のケースのように、言語やランタイムのパフォーマンスが向上した結果、これまでたまたま動いていたコードの潜在的なバグが顕在化することがあるため、仕様を正しく理解して実装することの重要性を再認識しました。
目次 はじめに 2つの課題と、目指すアーキテクチャ 手法1 — UIKit の中に SwiftUI を埋め込む 手法2 — ViewModel の Protocol と実装の分離 手法3 — UIKit 依存の画面遷移を列挙型で集約する 手法4 — SwiftUI から UIKit の画面を呼ぶ 手法5 — 本体プロジェクトに依存する View コンポーネントを外から差し込む まとめ — 制約の中で前に進む はじめに デリッシュキッチンで iOS エンジニアをしている谷口恭一です。 デリッシュキッチンは今年で10年目を迎えるアプリです。この約1年間、2つの取り組みを並行して進めています。 SwiftUI 化 — UIKit で書かれた既存画面を SwiftUI に置き換える マルチパッケージ化 — 本体プロジェクトからコードを SPM パッケージに切り出す どちらも一括でやれるものではなく、通常の機能開発と並行しながら少しずつ進めるしかありません。本記事では、この2つの取り組みを同時に進めるために実践している 5つの手法 を紹介します。 2つの課題と、目指すアーキテクチャ 具体的な手法の話に入る前に、それぞれの課題と目指す方向を整理します。 SwiftUI 化の課題 多くの既存画面は UIKit と RxSwift で構成されています。新規の画面は SwiftUI で作っていますが、既存画面の SwiftUI 化はまだ道半ばです。 SwiftUI 化を進めるうえで最大の障壁は、ナビゲーションです。アプリ全体の画面遷移は UINavigationController の push/pop で成り立っています。SwiftUI には NavigationStack がありますが、アプリ全体のナビゲーションを一気に置き換えるのは現実的ではありません。画面数が多く、各画面の遷移ロジックが UINavigationController に深く依存しているためです。 また、各画面の ViewModel は API クライアント、永続化層、広告 SDK など、本体プロジェクトの様々なサービスに依存しています。UIKit の画面を単純に SwiftUI に書き換えるだけでは済まず、こうした依存関係をどう扱うかという設計上の判断が必要になります。 マルチパッケージ化の課題 一部のパッケージ化は進んでいるものの、まだ多くのソースコードが本体プロジェクト( .xcodeproj )のメインターゲットに含まれている状態です。依存管理は CocoaPods と SPM を併用しています。 本体プロジェクトのメインターゲットにコードが集中している構成には、チーム開発で厄介な問題があります。 ファイルを追加・削除するたびに .xcodeproj 内の project.pbxproj に差分が出て、ブランチ間のコンフリクトの原因になる ことです。 Xcode 16 で導入された フォルダベースのグループ管理 を使えば、ファイルシステムとプロジェクト構造が自動同期されるため、この問題は解消できます。しかし、CocoaPods で管理されている古い依存がフォルダベースのグループに対応しておらず、現時点ではフォルダへの移行がまだ行えません。 方針:SwiftUI 化のタイミングでパッケージにも切り出す この2つの課題は別々のものですが、同時に取り組むことで互いを補い合えます。 具体的には、 UIKit の画面を SwiftUI に書き換えるタイミングで、書き換えた SwiftUI のコードを本体プロジェクトに残すのではなく、 SPM(Swift Package Manager) の Feature パッケージに切り出す という方針を取っています。 こうすることで、SwiftUI 化によってコードが新しくなると同時に、パッケージへの移動によって本体プロジェクトのメインターゲットからコードが減っていきます。SPM パッケージ内のファイル操作は .xcodeproj に影響しないため、コンフリクト問題も根本的に回避できます。 本記事ではこの2つの場所を以下の用語で呼び分けます。 本体プロジェクト ( .xcodeproj のメインアプリターゲット) - UIKit の ViewController - ViewModel の実装クラス(各種サービスに依存) - Networking / Services / Repository Feature パッケージ (SPM で管理する独立したパッケージ) - SwiftUI の View - ViewModel の Protocol - Action の列挙型 - UI コンポーネント 依存の方向は当然 本体プロジェクト → Feature パッケージ の一方向です。 理想像:本体プロジェクトをエントリーポイントにする 最終的には、UI 層だけでなく、責務ごとに適切なパッケージへコードを隠蔽し、本体プロジェクトはそれらを組み合わせるエントリーポイントとしての役割に留めるのがゴールです。今回の Feature パッケージへの UI 層切り出しは、その最初の一歩にあたります。 前提:開発リソースの制約 なお、SwiftUI 化やパッケージ化だけに専念できる時期はありません。通常の機能開発・改善と並行して、できるところから少しずつ進めるしかないのが現実です。だからこそ、1画面ずつ、1コンポーネントずつ着実に移行していける手法が必要になります。 以下、5つの考え方とそれに対応する手法を順に紹介します。 手法1 — UIKit の中に SwiftUI を埋め込む 最も基本的な手法です。アプリ全体の画面遷移を一気に NavigationStack に置き換えるのは現実的ではありません。そこで、 画面遷移は UIKit のまま諦めて、1画面ずつ中身だけを SwiftUI に置き換えていく という考え方を取ります。 Feature パッケージ側では、純粋な SwiftUI View を定義するだけです。 public struct FeatureView < VM : FeatureViewModel >: View { @ObservedObject var viewModel : VM var body : some View { ... } } 本体プロジェクト側では、この SwiftUI View を UIHostingController 経由で既存の UIKit 画面に埋め込みます。 let hosting = UIHostingController(rootView : FeatureView (viewModel : viewModel )) addChild(hosting) view.addSubview(hosting.view) hosting.didMove(toParent : self ) ナビゲーション階層には一切手を加えないため、 影響範囲がその画面だけ に限定されます。既存のナビゲーションバーの設定や画面遷移ロジックをそのまま活用できます。 なお、シートやフルスクリーンカバーによるモーダル表示は、ナビゲーションの push/pop 階層とは独立しています。したがって、ホスティングされた SwiftUI View の中で .sheet() ) や .fullScreenCover() ) を使ったモーダル遷移は、完全に SwiftUI 内で完結できます。 手法2 — ViewModel の Protocol と実装の分離 SwiftUI View を Feature パッケージに移すとき、最初にぶつかるのが ViewModel の依存関係です。ViewModel の実装クラスは API クライアントや永続化層など、本体プロジェクトの様々なサービスに依存しています。これをそのままパッケージに持っていくことはできません。 解決策は、ViewModel を Protocol(インターフェース)と実装に分離 することです。 Feature パッケージには Protocol だけを置きます。 @MainActor public protocol FeatureViewModel : ObservableObject { var items : [ Item ] { get } var isLoading : Bool { get } func fetch () async } public struct FeatureView < VM : FeatureViewModel >: View { @ObservedObject var viewModel : VM } SwiftUI View はジェネリクスで ViewModel Protocol に依存し、具象型を知りません。 実装は本体プロジェクトに残します。 final class FeatureViewModelImpl : FeatureViewModel { @Published private ( set ) var items : [ Item ] = [] @Published private ( set ) var isLoading = false private let service : ItemService func fetch () async { isLoading = true items = ( try ? await service.fetchItems()) ?? [] isLoading = false } } サービス層への依存は本体プロジェクトの実装クラスに閉じ込められ、Feature パッケージは Protocol が定義するインターフェースだけを相手にします。 そして、この2つを繋ぐのが本体プロジェクトの UIKit ViewController です。手法1と組み合わせて、実装クラスを生成し SwiftUI View に渡します。 final class FeatureViewController : UIViewController { override func viewDidLoad () { super .viewDidLoad() let viewModel = FeatureViewModelImpl(service : .shared) let hosting = UIHostingController(rootView : FeatureView (viewModel : viewModel )) addChild(hosting) view.addSubview(hosting.view) hosting.didMove(toParent : self ) } } Feature パッケージの FeatureView は FeatureViewModel Protocol しか知りませんが、本体プロジェクトが具象型 FeatureViewModelImpl を生成して渡すことで、依存関係が解決されます。 Protocol の定義はパッケージに、実装の生成は本体プロジェクトに — この役割分担がパッケージ境界を成立させます。 この分離のもう一つの利点は、 将来の拡張性 です。Service 層や Repository 層のパッケージ化が進めば、ViewModel の実装クラスもいずれ Feature パッケージに移すことができます。今の時点では Protocol と実装を分けておくことで、将来その選択肢を確保しておけるということです。 手法3 — UIKit 依存の画面遷移を列挙型で集約する UI 層が Feature パッケージに移ると、次の問題が浮上します。SwiftUI の画面からユーザーが「詳細を見る」「検索画面を開く」といった操作をしたとき、その遷移先がまだ本体プロジェクトに UIKit で実装されたままのケースです。依存の方向は本体プロジェクト → Feature パッケージの一方向なので、Feature パッケージから本体プロジェクトの画面を直接呼ぶことはできません。 このギャップを埋めるために、Feature パッケージで Action 列挙型 を定義し、本体プロジェクトのクロージャで処理します。 public enum FeatureAction { case showDetail(Item) case showSearch case showSettings } public protocol FeatureViewModel : ObservableObject { var actionHandler : (( FeatureAction ) -> Void ) ? { get set } } Feature パッケージが知っているのは「こういうアクションが起きうる」という列挙型の定義だけです。それをどう処理するかは、本体プロジェクトに委ねます。 viewModel.actionHandler = { [ weak self ] action in switch action { case .showDetail( let item ) : self? .navigationController?.pushViewController(DetailVC(item : item ), animated : true ) case .showSearch : SearchVC.present (from : self ) case .showSettings : SettingsVC.push (from : self ) } } UIKit に依存する処理はこの switch 文の中に集約されます。新しい遷移が増えたら enum にケースを追加し、 switch にハンドリングを書くだけです。遷移先が SwiftUI 化されたら、対応する case の処理を SwiftUI 内の .navigationDestination ) 等に移せばいい。enum の associated values によって遷移に必要なパラメータが型安全に保証されるため、実行時エラーのリスクも低くなります。 なお、このアクションハンドリングの処理を ViewController に直接書くのではなく、Coordinator に切り出して責務を閉じ込めるという選択肢もあります。画面遷移のパターンが多い画面では、そちらのほうが見通しが良くなるかもしれません。 手法4 — SwiftUI から UIKit の画面を呼ぶ 手法3は、主にナビゲーション(push)ベースの画面遷移で機能します。一方で、SwiftUI の .fullScreenCover() や .sheet() によるモーダル遷移は事情が異なります。 シートやフルスクリーンカバーによるモーダル遷移は、ナビゲーションの push/pop 階層から独立しています。つまり、理想的にはモーダルで表示する画面とその先をまるごと SwiftUI 化できる領域です。しかし現実には、 .fullScreenCover() の遷移先にまだ UIKit の画面が残っていることがあります。 ここでの考え方は、 遷移先が UIKit のままでも、呼び出し元の SwiftUI 化を止めない ということです。UIKit の画面を UIViewControllerRepresentable でラップして SwiftUI から呼べるようにし、UIKit → SwiftUI → UIKit という「サンドイッチ」構造を移行の過渡期として許容します。 具体的な手法は3つのステップで構成されます。 UIKit 画面を Representable でラップする まだ SwiftUI 化されていない UIKit の画面を、 UIViewControllerRepresentable で薄くラップします。 extension LegacyDetailViewController { struct Representable : UIViewControllerRepresentable { let item : Item func makeUIViewController (context : Context ) -> LegacyDetailViewController { . init (item : item ) } func updateUIViewController (_ vc : LegacyDetailViewController , context : Context ) {} } } このラッパーは本体プロジェクトに置きます。 Feature パッケージは遷移先を外から受け取る ここが最も重要なポイントです。Feature パッケージの SwiftUI View は、遷移先の具体的な画面を自分では持たず、 ジェネリクスの @ViewBuilder クロージャ として外部から受け取ります。 public struct FeatureRootView < Destination : View >: View { @ViewBuilder let detailDestination : ( Item ) -> Destination var body : some View { content .fullScreenCover(isPresented : $viewModel .showDetail) { if let item = viewModel.selectedItem { NavigationStack { detailDestination(item) } } } } } Destination: View というジェネリクス制約だけがあり、具体的にどんな View(あるいは Representable)が来るかは知りません。Feature パッケージは本体プロジェクトのレガシー画面に一切依存していません。 本体プロジェクトで Representable を注入する 組み立ては本体プロジェクトが担当します。 let rootView = FeatureRootView( viewModel : viewModel , detailDestination : { item in LegacyDetailViewController.Representable(item : item ) } ) present(UIHostingController(rootView : rootView ), animated : true ) レガシーな UIKit 画面への依存があるのは、この注入の一箇所だけです。 この構造の大きな利点は、 遷移先が SwiftUI 化されたときの変更が最小限 で済むことです。注入するクロージャの中身を差し替えるだけで、Feature パッケージのコードには一切触れる必要がありません。 detailDestination : { item in NewDetailView(item : item ) } サンドイッチ構造はあくまで移行の過渡期のものであり、最終的には UIKit の層が消えて自然な SwiftUI のコードになります。 いつこの手法を使うか この手法を使わず「遷移先もすべて SwiftUI 化してからでないと手を付けられない」と考えてしまうと、SwiftUI 化できる範囲がなかなか広がりません。たとえば画面 A から .fullScreenCover() で画面 B を表示していて、画面 B がまだ UIKit だとします。画面 A の SwiftUI 化は画面 B の完了を待つことになり、画面 B にも UIKit の遷移先があれば、さらにその先を待つ…と連鎖してしまいます。 ただし、すべてのモーダル遷移にこの手法を適用すべきというわけではありません。遷移先の UIKit 画面が少数であればラップして先に進めるメリットがありますが、遷移先の大半が UIKit であれば、ラップのコストが見合わないのでその画面の SwiftUI 化自体を後回しにする判断もありえます。ラップにかかるコストと、SwiftUI 化を先に進められるメリットを天秤にかけて判断することが重要です。 手法5 — 本体プロジェクトに依存する View コンポーネントを外から差し込む 手法1〜4で多くの UI を Feature パッケージに移せますが、もう一つ厄介なケースがあります。 画面の一部に、本体プロジェクトの依存がないと成立しないコンポーネント が含まれている場合です。 典型的な例がインフィード広告です。広告の表示には広告 SDK への依存が必要ですが、これは本体プロジェクトにしかありません。だからといって、広告を含む画面全体を Feature パッケージに移せないとなると、移行が大きく停滞してしまいます。 考え方は手法4と同じです。Feature パッケージ側では「ここに何かの View が入る」というジェネリクスの枠だけを定義し、具体的な実装は本体プロジェクトから差し込みます。 Feature パッケージの View は、広告コンポーネントの表示位置をジェネリクスの @ViewBuilder パラメータとして受け取ります。 public struct FeatureTabView < VM : FeatureTabViewModel , AdContent : View >: View { @ObservedObject var viewModel : VM private let adContent : () -> AdContent public init ( viewModel : VM , @ViewBuilder adContent : @escaping () -> AdContent ) { self .viewModel = viewModel self .adContent = adContent } public var body : some View { ScrollView { LazyVStack(spacing : 8 ) { SomeSection(viewModel : viewModel ) adContent() AnotherSection(viewModel : viewModel ) } } } } Feature パッケージはこの AdContent が何であるかを一切知りません。広告でもプレースホルダーでも EmptyView でも構わないという設計です。 本体プロジェクト側では、広告 SDK に依存する具体的な View を差し込みます。 let view = FeatureTabView(viewModel : viewModel ) { InFeedAdSectionView(adType : .infeed) } let hosting = UIHostingController(rootView : view ) InFeedAdSectionView は本体プロジェクトにあり、内部で広告 SDK を使って広告をロード・表示します。Feature パッケージにはこの View の存在も広告 SDK の存在も見えていません。 この手法のポイントは、 1つのコンポーネントが Feature パッケージに移せないからといって、画面全体のパッケージ移行を諦めない ということです。移せない部分だけを抽象化して外から差し込めば、画面の大部分は Feature パッケージに移すことができます。 まとめ — 制約の中で前に進む 本記事で紹介した5つの手法を整理します。 手法 主に解決する課題 いつ不要になるか 手法1: UIKit に SwiftUI を埋め込む SwiftUI 化 NavigationStack 全面移行時 手法2: ViewModel の Protocol / 実装分離 マルチパッケージ化 Service 層のパッケージ化完了時 手法3: UIKit 依存処理の列挙型集約 マルチパッケージ化 UIKit 画面の SwiftUI 化完了時 手法4: SwiftUI から UIKit を呼ぶ SwiftUI 化 遷移先の SwiftUI 化完了時 手法5: 依存のあるコンポーネントを外から差し込む マルチパッケージ化 依存のパッケージ化完了時 重要なのは、 すべての手法に「不要になる日」がある ということです。これらは最終的なアーキテクチャではなく、移行期を乗り越えるための手法です。 これらの手法を組み合わせることで、通常の機能開発と並行しながら少しずつ SwiftUI 化とマルチパッケージ化を進められています。一気に大きな時間を確保しなくても、1画面ずつ、1コンポーネントずつ着実に移行を進めていける実感があります。 大規模アプリの SwiftUI 移行は、短距離走ではなくマラソンです。今日の制約の中で最善の一歩を選び、長い戦略スパンで着実に前に進めていく。本記事がその一助になれば幸いです。
Go 1.26で追加されたnew(expr)はなぜこの形なのか こんにちは、開発1部の @uho-wq です。 本記事ではGo 1.26で追加された new(expr) がどのような議論の末にこの形に落ち着いたのかを説明しようと思います。 go.dev new(expr) Go 1.26で、組み込み関数 new が式(expression)を受け取れるようになりました。 p := new ( 42 ) // *int, 値は42 s := new ( "hello" ) // *string, 値は"hello" b := new ( true ) // *bool, 値はtrue とてもシンプルな構文追加に思えますが、実はこの結論に至るまで2014年から2025年までの 11年 もかかりました。 この記事では、以下の2つのissueをもとに議論の流れを追っていきます。 github.com github.com ※ この記事を作成するにあたり、これらのissueに付いたコメントすべてに目を通しました。11年分の議論は非常に膨大なため本記事では要点を絞って紹介しており、解釈の違いや抜け漏れがある可能性がありますがご了承ください。 そもそも何が問題だったのか Goではcomposite literalは直接ポインタを取得できますが、プリミティブ型は宣言時にポインタを得ることができません。 p := &Point{X: 1 , Y: 2 } // OK: composite literalは&を取れる p := & 42 // コンパイルエラー: cannot take address of 42 よって従来では以下のように一度変数に代入してポインタを得る書き方をするか、ヘルパー関数を定義するしかありませんでした。 v := 42 p := &v // ヘルパー関数 func IntPtr(v int ) * int { return &v } 例えば、AWS SDK for Goでは aws.String() 、 aws.Int64() といった ヘルパー関数 が大量に定義されています。構造体の値をaws.String()で囲むといった作業はAWS SDK for Goを使ったことがある方は経験済みなのかなと思います。 Go 1.18でGenericsが導入されたことによって、ヘルパー関数を汎用的に記述することができるようになりました。 func Ptr[T any](v T) *T { return &v } しかし、これもcomposite literalのみ直接ポインタを取れるという問題の回避策にはなりましたが、根本解決には至りませんでした。 こうした背景から、言語レベルでの解決策が長年にわたって議論されてきました。以降では、その議論がなぜ最終的に new(expr) という形に落ち着いたのかを時系列で追っていきます。 proposal: spec: add &T(v) to allocate variable of type T, set to v, and return address #9097 2014年11月にchai2010氏により最初の提案が行われました。 提案は、以下の2つの構文を追加する、というものでした。 new 関数の拡張: func new(Type, value ...Type) *Type &Type(value) 構文の追加 例: px := new ( int , 9527 ) px := & int ( 9527 ) 当初は大きな反響もなくissueは放置されていましたが、2018年にIan Lance Taylor氏が 提案に再度言及 しました。 &int(5) を許すなら new(int, 5) は不要であり、 new を完全に削除することすら検討すべき だと述べました。そして任意の式に & を適用する際の問題点を2つ指摘しています。 1つ目は任意の式 v に対して &v を取れるとした場合、論理的にはアドレスのアドレス &&v を取れるべきだが、 && は異なる意味を持つ演算子なので動作しない 2つ目は &var はループ内で呼び出しても毎回同じ値に解決されるが、 &expr は毎回新しいインスタンスを確保するので異なる値に解決される また2020年には、Ian Lance Taylor氏自身が「ジェネリクスが入れば新しい言語機能を必要としないので、ジェネリクスを得るまで待って、そのようなアプローチが十分かどうかを見たいと思う」とも 述べています 。 結局#9097は2023年8月に#45624を優先する形でクローズされました。9年間で40件のコメントが寄せられ、Ian Lance Taylor氏が提示した論点は#45624でも継続して議論されます。 spec: expression to create pointer to simple types #45624 2021年4月にRob Pike氏によってissueが立てられました。 Pike氏はissueを再オープンする代わりに、新たに2つの選択肢を提示しました。 Option 1: new に第2引数を追加する p1 := new ( int , 3 ) p2 := new ( rune , 10 ) p3 := new (Weekday, Tuesday) Option 2: 型変換の結果をアドレス可能にする p1 := & int ( 3 ) p2 := & rune ( 10 ) p3 := &Weekday(Tuesday) Pike氏は「両方入れてもいいかもしれない」とも述べています。 注目すべきは、この時点では最終形となる new(expr) はまだ提案されていなかったということです。Pike氏の提案はあくまで new(T, v) (型と値の2引数)と &T(v) の2択でした。 new(1) の提案 (2021年4月) Pike氏の提案から数日後、Go Teamの Russ Cox氏のコメント が多くの賛同を得ました。 The overloading of & for "take address of existing value" and "allocate copy of composite literal" has always been unfortunate. An alternative to expanding the overloading of & would be to overload new instead, so that it is the generic ptrTo function as well as the original new(T), as in new(1). Then &T{...} can be explained retroactively as mere syntactic sugar for new(T{...}). & 演算子は既に「既存の値のアドレスを取得する &v 」と「composite literalのコピーを割り当てる &T{...} 」という2つの異なる意味を持っています。ここにさらに意味を追加するのではなく、 new を拡張して new(1) のように書けるようにすべきではないか。そうすれば &T{...} は new(T{...}) の糖衣構文として説明できる、という主張です。 これが最終形 new(expr) の原型でした。 ジェネリクスの提案 (2021年4月) 一方でRoger Peppe氏は 言語変更そのものに異を唱えました 。 Given this possibility, I don't see that there's any need to change new or the language syntax itself to accommodate this functionality. Goのジェネリクスを使えば以下のように書けるのだから、newや言語仕様自体を変える必要はないのでは、というものです。 // ref returns a pointer to the value of t. func ref[T any](t T) *T { return &t } このジェネリクス案は、その後4年にわたって繰り返される反論の原型となりました。 膠着状態 (2021年9月) 2021年9月、Ben Hoyt氏が 議論の停滞を指摘 し、再検討を求めました。 Looks like this was last discussed in the proposal review meeting on May 5. While there's no clear consensus here, there are a number of good options. It seems like there's a fair bit of enthusiasm for Russ's simple new(1) form, and a decent amount of support for a new builtin generic function like Roger Peppe's ptr(1) suggestion. My vote would be for ptr(1) as it just uses "ordinary" generics, but I like new(1) too. Could this be discussed at the review meetings again? この時点で支持が集まっていたのはnewの拡張である new(1) とジェネリクスを使用した ptr(1) の2案でしたが、コンセンサスには至りませんでした。ジェネリクスの正式リリース(Go 1.18、2022年3月)を待つ形で、議論は一時休止に入ります。 PtrTo[T any] vs &T(v) vs new の拡張 (2023年6月) 2023年6月、Go TeamのIan Lance Taylor氏がissueに戻り、 選択肢を3つに絞りました 。 PtrTo[T any] のような標準ライブラリ関数 &T(v) 構文 new(v) / new(T, v) の拡張 そしてGo Teamの立場を明確にしました。 @griesemer, @bradfitz, and @ianlancetaylor prefer permitting both new(v) and new(T, v) . この時点では、Go Teamの主要メンバー3人が new 拡張を支持していました。 ただし new(v) と new(T, v) の 両方 を許可する案であり、 new(v) 単独ではありませんでした。 また、依然として &T(v) を支持する声はあったものの、批判的な意見も支持されるようになってきました。Ben Hoyt氏の 主張 が端的に示しています。 I slightly prefer new(v) over &T(v) because it eliminates stuttering in cases like new(time.Now()) -- that would be &time.Time(time.Now()) with the other syntax. If new(T, v) is supported in addition for clarity in certain cases, that's fine. new() is also a bit clearer that it always creates a "new" thing. new(time.Now()) のようなケースだと冗長な繰り返しがなくなりますが、&T(v)の構文だと &time.Time(time.Now()) になってしまいます。明確さが必要な場合に new(T, v) が追加でサポートされるのは問題にはならず、new() は常に「新しい」ものを作成することがより明確である、と主張しています。 さらにHoyt氏も &演算子が概念的に同等でないこと も指摘しています。 When you do &Struct{} Go creates a new value every time and returns its address, but when you do &s Go returns the address of that same variable each time. &Struct{}を行うと、Goは毎回新しい値を作成してそのアドレスを返しますが、&sを行うとGoは毎回その同じ変数のアドレスを返します。 この後も &T(v) 案は依然として支持されるものの、議論の焦点はnewの拡張方法とジェネリクスの活用に移っていきます。 new(T, v) vs new(v) (2023年7月) Goチームが支持しているnewの拡張方法は new(T, v) と new(v) の2パターンありました。 2023年7月、Ian Lance Taylor氏が 方針転換を報告 しました。Rob Pike氏とRoger Peppe氏などから「 new(v) と new(T, v) の両方ではなく、 new(T, v) のみにすべき」という意見が出ました。 また型名が長くなるケースの大半は構造体であり、構造体には既に &S{} 表記があります。単純な値 v に対して複雑な型 T を書く new(T, v) のケースはそもそもほとんど発生しないと考え、 new(T, v) でも混乱を招くことは少ないだろう、という見解を示しています。 これに対してMerovius氏が 具体例 で切り返しました。 new(int64(42)) isn't any more to type or read than new(int64, 42), but new(time.Second) is significantly better than new(time.Duration, time.Second). I don't think having the type in there really adds anything. We are already kind of used to inferring the type from a constant literal. new(int64(42)) は new(int64, 42) と比べてタイプ量も読む量も変わりませんが、 new(time.Second) は new(time.Duration, time.Second) よりもはるかに良いです、と述べています。 このコメントが賛同を集めた一方で、 new(v) を見たときに v が型なのか値なのかを読者が把握している必要があるのでnew(v)を好まない、という意見も複数ありました。 new(T, v) は書き方として冗長である一方で明確に記述でき、 new(v) は書き方として簡潔である一方で表現として曖昧であるとし、この時点ではコンセンサスには至りませんでした。 ジェネリクスの限界 (2023年 - 2024年) 「ジェネリクスで Ptr[T] が書ける」という反論は依然として主張されていました。 しかし2023年12月、Rob Pike氏が 改めてこの問題の本質を言い直しています 。 it's easier to build a pointer to a complex thing than to a simple one. 「複雑な構造体へのポインタは &T{...} で簡単に作れるのに、単純な int へのポインタは面倒」 ジェネリクスを用いたヘルパー関数を書くことは、この非対称性の問題の根本的な解決にはなっていないと言及しています。 ジェネリクスが根本の解決になっていないとするエピソードとして、perj氏の 体験談 が象徴的でした。 I appear to be writing this function about once every second month, when I need it in a new package. It's not very annoying, but does feel a bit like I'm littering my packages with this function, so not having to write it would be welcome. I do realise I can put it in a package I import, but that also seems overkill for a one-liner. 2ヶ月に1度、新しいパッケージでこのヘルパー関数を書いている パッケージをこの関数で散らかしているような感じがするので、書かなくて済むなら歓迎 importするパッケージに入れることもできるが、たった1行のコードのためにそれをするのはやりすぎな気がする このコメントは20ものGood評価を集めており、ジェネリクス案の限界を端的に示しているといえます。 new(T, v) は解決策にならない (2025年3月-8月) 2025年3月、かつて「ジェネリクスで十分」と主張していたRoger Peppe氏が、 new(T, v) 案に対して 批判 を投じました。 Replacing, for example, ref(someMap[x]) with new(SomeType, someMap[x]) would be a net loss because it makes the code more verbose and a little bit more fragile, requiring update should the type of the map's values change. ref(someMap[x]) を new(SomeType, someMap[x]) に書き換えるのはコードが冗長になるだけでなく、mapの値の型が変わるたびに修正が必要になる。 型を2回書く new(T, v) では、ジェネリクスのヘルパー関数からの移行メリットがない、という指摘です。 その後、2025年8月にGo TeamのAlan Donovan氏が 決めてとなるコメント を投じました。 it is important not to have to redundantly state the type and the value, making new(T, v) a non-solution. 型と値を冗長に並べる必要がないことが重要であり、 new(T, v) は解決策にならない、と主張し、Donovan氏は3月のPeppe氏のコメントに納得して new(value) を支持する立場を表明しました。 デフォルトの型が合わない場合は new(T(v)) とキャストを組み合わせればよく、 new(T, v) のような複雑なルールは不要だ、としています。 The proposal committeeの承認 (2025年8月) 2025年8月15日、The proposal committeeを代表してAustin Clements氏が 宣言 しました。 The proposal committee is happy with new(expr) . new(T) (型を渡す)と new(expr) (式を渡す)は動作が異なり、構文的な曖昧さを欠点として持つものの、どちらも「新しいストレージを確保して返す」点で一貫しています。 そしてDonovan氏が 収集したデータ が決め手となりました。 the data @adonovan collected indicates that, while this can be written as a generic function, there are so many instances that it seems well-worth a standardized built-in. ジェネリクスを用いた関数として記述することも可能ですが、その利用箇所が非常に多いため、標準化された組み込み関数として実装する価値は十分にある、としています。 Accepted (2025年9月) 2025年9月17日、Austin Clements氏が 正式に採択を宣言 しました。 そして2025年10月27日、実装を完了したAlan Donovan氏が issueを締めくくりました 。 All done, in only eleven years since #9097. ;-) Go 1.26での仕様 The Go Programming Language Specification では、 new は以下のように定義されています。 The built-in function new creates a new, initialized variable and returns a pointer to it. It accepts a single argument, which may be either an expression or a type. 引数が型 T の式(または、デフォルト型が T のuntyped定数式)である場合、 new(expr) は型 T の変数を確保し、 expr の値で初期化し、そのアドレス(型 *T の値)を返します。 type Config struct { Timeout *time.Duration Retries * int Verbose * bool } cfg := Config{ Timeout: new ( 30 * time.Second), Retries: new ( 3 ), Verbose: new ( true ), } 関数の戻り値も渡せます。 p := new (time.Now()) // *time.Time q := new (strconv.Itoa( 42 )) // *string 注意点: untyped constantの挙動 ただし1つ注意点があります。 new() に定数を渡した場合、default typeが使われます。 var ui uint = 10 // OK: untyped constant 10はuintに暗黙変換される // しかし... uip := new ( 10 ) // *int(10のdefault typeがint) var ui2 uint = *uip // コンパイルエラー: cannot use *uip (type int) as type uint 定数 10 がそのまま変数宣言で使われる場合はuntyped constantとして柔軟に型推論されますが、 new(10) の時点で *int に確定してしまいます。明示的な型が必要な場合は型変換を組み合わせましょう。 uip := new ( uint ( 10 )) // *uint まとめ 最後に、11年の議論で登場した各提案の結論についてまとめます。 提案 結論 &T(v) & 演算子の意味の不連続性。 &2 は毎回新しいアドレスを返すが &v は同じアドレスを返す。混乱を招く ref(v) / ptr(v) ジェネリクスで1行で書ける。だが逆に「全員が書いている」。組み込みとして標準化する方が合理的 new(T, v) 冗長。 new(time.Duration, time.Second) はジェネリクスの ref(time.Second) より後退する new(expr) 採用。 & のセマンティクスを変えず、既存の new 関数の自然な拡張 個人的には、議論全体を通して new(expr) という結論に至ったことがとても腑に落ちました。ジェネリクスの導入を見越して一度議論を止め、導入後も便利さに飛びつかず実運用の課題を吸い上げた上で、本質的な解決策に辿り着いています。 最終形の new(expr) は、2021年にRuss Cox氏が投じた new(1) の発想そのものでした。4年の間に &T(v) や new(T, v) が検討され、結局最もシンプルな案に戻ってきたのが面白いなと思いました。
1. はじめに:Liquid Glass で変わる「検索」の体験 WWDC25 で発表された Liquid Glass は、iOS 26 の目玉となるデザインシステムです。ナビゲーションバーやタブバーがガラスのような半透明素材になり、コンテンツがその裏側に透過して見えるようになります。 見た目の変化も大きいですが、Liquid Glass がアプリの体験として特に大きく変えたのは 検索 です。iOS 26 では、タブバーの右端に検索アイコンが配置され、タップするとタブバー自体が検索フィールドに変わります。設定アプリや App Store など Apple 純正アプリではこの新しい検索 UI が標準になっており、ユーザーはどのアプリでも同じ操作で検索にアクセスできるようになりました。 これはサードパーティアプリにとっても重要な変更です。この新しい検索パターンを採用することで、iOS の標準的な検索体験と統一感のある UI を提供できます。 Liquid Glass の対応ポイントは多岐にわたりますが、この検索タブの変更は特にユーザー体験への影響が大きいと感じたため、既存の UIKit アプリでどう実現するかを調べて実装してみました。 SwiftUI であれば、検索タブの Liquid Glass 対応は驚くほど簡単です。 TabView { Tab( "ホーム" , systemImage : "house" ) { HomeView() } Tab(role : .search) { SearchView() } } Tab(role: .search) の 1 行で、タブバーの右端に検索アイコンが分離配置され、タップするとタブバー内に検索フィールドが展開する動きを実現できます。 この記事では、UIKit の UITabBarController + UINavigationController を基盤とした既存アプリに Liquid Glass の検索タブを適用する方法を紹介します。 2. ゴール:タブバー内に検索フィールドが展開する UI 実装するのは以下の動きです。 タブバーの右端に検索アイコンが配置される 検索アイコンをタップすると、タブバー内に検索フィールドがスライドして展開する テキストを入力して検索できる キャンセルすると元のタブバー表示に戻る これは iOS 26 の設定アプリや App Store で見られる標準的な動きで、 UISearchTab というクラスを使って実現します。 3. UISearchTab — UIKit 版の Tab(role: .search) iOS 18 で UITab API が導入されました。従来の viewControllers 配列ベースのタブ管理に代わり、 UITab オブジェクトを使ってタブを構成する新しい方式です。 UISearchTab は UITab のサブクラスで、検索専用のタブを表します。SwiftUI の Tab(role: .search) に対応する UIKit のクラスです。 @available ( iOS 18.0 , * ) private func setupWithUITabAPI () { // 検索以外のタブを UITab として生成 var uiTabs : [ UITab ] = nonSearchItems.map { item in UITab( title : item.title , image : item.tabImage , identifier : item.identifier ) { _ in item.controller } } // 検索タブは UISearchTab を使う let searchTab = UISearchTab { [ weak self ] _ in guard let self else { return UIViewController() } return TabBarItemType.search.controller } uiTabs.append(searchTab) tabs = uiTabs } UISearchTab のポイントは以下の通りです。 タイトルや画像の指定が不要 です。システムが検索アイコンを自動で提供します タブバーの右端(trailing 端)に自動で分離配置 されます。他のタブとは異なる位置に置かれます tabs プロパティに設定するだけで、タブバー内の検索フィールド展開がシステム標準で動作します 4. 検索 ViewController の対応 UISearchTab を設定すると、タブバーに検索アイコンが表示されます。しかし、タップしたときに検索フィールドをタブバー内に展開させるには、ViewController 側の対応も必要です。 UISearchTab がタブバー内で検索フィールドを展開するには、検索コントローラーが navigationItem.searchController に設定されている必要があります。 navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false navigationItem.titleView にカスタム配置した検索バーでは、 UISearchTab のタブバー展開とは連携しません。既存のアプリで titleView ベースの検索バーを使っている場合は、 navigationItem.searchController への移行が必要です。 5. iOS バージョンだけでなく Info.plist も見て分岐する サンプルコード中で使用している LiquidGlassAvailability.isEnabled について説明します。 iOS 26 以降でも UIDesignRequiresCompatibility を true に設定している場合 、アプリは Liquid Glass ではなく従来のデザインで表示されます。この場合、 UISearchTab を使ったタブ構成にすると見た目と挙動が噛み合わなくなります。 そこで、iOS バージョンと Info.plist のフラグを両方チェックするヘルパーを用意しました。 enum LiquidGlassAvailability { static var isEnabled : Bool { guard #available(iOS 26.0 , * ) else { return false } // UIDesignRequiresCompatibility が true なら互換モード → Liquid Glass 無効 if let requiresCompatibility = Bundle.main.object( forInfoDictionaryKey : "UIDesignRequiresCompatibility" ) as? Bool , requiresCompatibility { return false } return true } } 判定ロジックは以下の通りです。 条件 isEnabled iOS 25 以前 false iOS 26+ / UIDesignRequiresCompatibility = true false iOS 26+ / UIDesignRequiresCompatibility 未設定 or false true なぜこれが必要か UIDesignRequiresCompatibility は、Liquid Glass への移行を段階的に進めるための Apple 公式の仕組みです。Info.plist にこのキーを true で設定すると、iOS 26 でもアプリは従来のデザインで表示されます。つまり 「iOS 26 以降 = Liquid Glass」ではない のです。 開発中は Liquid Glass を有効にして動作確認し、問題があればこのフラグを true にして一時的に互換モードに戻す、という使い方ができます。コード側もこのフラグに連動して分岐しておけば、フラグひとつで Liquid Glass の ON/OFF を切り替えられます。 Liquid Glass 対応は多岐にわたるため、ある程度長い開発期間が必要になると考えられます。他の施策の開発と並行して進められるように、このような仕組みを用意しました。 6. まとめ UIKit アプリで Liquid Glass の検索タブを実装するための要点を整理します。 UISearchTab を使う — UITab のサブクラスで、SwiftUI の Tab(role: .search) に対応する UIKit のクラスです。タイトルや画像は不要で、タブバー右端への分離配置と検索フィールドの展開がシステム標準で動作します。 検索バーは navigationItem.searchController に設定する — navigationItem.titleView にカスタム配置した検索バーでは、 UISearchTab のタブバー展開と連携しません。 iOS バージョンだけでなく UIDesignRequiresCompatibility も見る — #available だけでは不十分です。Info.plist のフラグと組み合わせた LiquidGlassAvailability.isEnabled を用意し、Liquid Glass の ON/OFF にコードが追従するようにしました。 #available で後方互換を維持する — UITab API は iOS 18+、 automaticallyActivatesSearch は iOS 26+ です。既存の viewControllers ベースのコードと UITab API ベースのコードを分岐で共存させます。 SwiftUI への全面移行を待たなくても、UIKit アプリに段階的に Liquid Glass を取り入れることは可能です。 UISearchTab はその第一歩として取り組みやすい対応だと思います。
はじめに こんにちは、開発1部で食事管理アプリ「ヘルシカ」の開発をしている新谷です。 ヘルシカ - ダイエット・食事管理のための簡単カロリー計算 every, Inc. ヘルスケア/フィットネス 無料 apps.apple.com 社内でAIツールを使って開発を進める中で、個々のタスクは確実に速くなっているものの、開発フロー全体としてはまだ思ったほど生産性が上がっていないと感じています。この記事では、その原因を分析し、「エンジニアが仕様書を主導して書く」という開発フローの改善に取り組んだ話を紹介します。 現状の開発フローと課題 これまでの開発フロー ヘルシカチームでは、以下のような流れでプロダクト開発を行っています。 施策立案 → 認識合わせ → デザイン作成 → 設計 → 実装 → コードレビュー → QA → リリース 認識合わせのために、PdMがPRD(Product Requirements Document)を作成しています。PRDの構成は大まかに以下のような形です。 ## 背景・目的・仮説 なぜこの施策をやるのか(Why) ## 分析・検証 確認したい指標(定量面) ## 要件・仕様 ユーザーは〇〇にアップロードした画像を保存できる(What) このPRDには、WhyとWhatが書かれています。これをもとにチーム全員で認識を合わせ、担当のエンジニアがシステム設計(How)を作成して実装に入ります。 タスク規模によるボトルネックの違い AIツールの導入によって、小さなタスクでは設計・実装がボトルネックになりにくくなりました。むしろ認識合わせやコードレビューの方が相対的にボトルネックになってきています。 しかし、中規模〜大規模なタスクでは、依然として設計・実装がボトルネックです。具体的には以下の2つの課題があります。 設計の課題 PRDの仕様からシステム設計に落とすのに時間がかかる 仕様の考慮漏れや認識齟齬が設計段階で発覚し、手戻りが発生する 実装の課題 AIが生成したコードの確認・修正に時間がかかる 課題の深掘り 実装の課題について掘り下げると、AIが期待通りのコードを出せない原因の多くは、設計書(= AIへのプロンプト)の不備に行き着きます。 もちろん、AIが力を発揮するための環境整備(リンターや自動テストの整備、ルールファイルの充実、既存コードの品質改善など)も大切です。しかし、これらを整えたとしても、不備のある設計書を渡せば不備のある実装が出てきます。 つまり、設計フェーズで短時間かつ考慮漏れのない設計を作れるかが、AI時代の開発生産性を左右するポイントだと考えました。 では、設計の課題をもう少し細かく見てみます。 設計に時間がかかる:技術的知識やドメイン知識に基づく設計力・スピードに依存する 手戻りが発生する:PRDの仕様に考慮漏れがあり、設計や実装の段階で初めて問題が発覚する この2つの課題を解決するために、「エンジニアがPRDの仕様(What)を主導して作成する」というアプローチを考えました。 エンジニアが仕様を主導する なぜエンジニアが仕様を書くのか エンジニアは実際のコードベースを理解しています。そのため、仕様を考える段階で「この機能を実現するには、既存の〇〇の処理にも影響がある」「この条件分岐は仕様として明確にしておく必要がある」といった技術的な観点を織り込めます。 これにより、仕様段階での考慮漏れが減り、後続のシステム設計で大きな手戻りが発生しにくくなります。結果として、設計にかかる時間も短縮されます。 また、エンジニアが仕様を引き受けることで、PdMはマーケティングや数値分析など、本来注力すべき領域により多くの時間を使えるようになるのではないかと考えています。 AI時代ならではのメリット さらに、AI時代ならではのメリットもあります。エンジニアが仕様書を作ることで、「どういう仕様書を書けば、後段のシステム設計書を楽に作れるか」「どういう粒度で書けば、AIで設計書の生成を自動化できるか」といったPDCAを回しやすくなります。 仕様書のフォーマット自体を改善していくことで、仕様書→システム設計書→実装の一連の流れを最適化できる可能性があります。 AI-DLCとの共通点 この考え方は、AWSが提唱している AI-DLC(AI-Driven Development Life Cycle) のINCEPTION PHASE(AIと要件を深掘りして決めていくフェーズ)に通じるものがあります。AI-DLCでは、最初のフェーズでPdM・デザイナー・エンジニアが一緒に要件を詰めていくことを推奨しており、今回の取り組みと方向性が近いと感じています。弊社でも別チームがAI-DLCを試した事例があるので、興味のある方は こちらの記事 もご覧ください。 難しい点 一方で、エンジニアが仕様作成を主導するには、PdMと同じくらいの解像度でビジネスや数値を理解する必要があります。これは簡単ではありません。 しかし、AIによって職種間のオーバーラップが進む中、エンジニアにもビジネス理解が強く求められるようになっていくと考えています。仕様を主導することは、エンジニアのスキルセットを広げる機会にもなるのではないかと思っています。 新しい開発フロー 最終的に、以下のフローに変更しました。 PdMがPRDのWhy(背景・目的・仮説)を作成 PdM・デザイナー・エンジニアで認識合わせ → エンジニアが主導してWhat(仕様)を詰める エンジニアが仕様書を作成 仕様書のレビュー エンジニアがシステム設計書(How)を作成 実装 これまでシステム設計書はドキュメントとして残すことを必須としていませんでしたが、仕様書→システム設計書の変換をAIで自動化するPDCAを回すために、今回から残すことを必須としました。 実際の成果物の例 具体例として、「プレミアム機能無料開放」施策での成果物の一部を紹介します。 まず、PdMが作成したPRDの一部です。施策の背景・目的・仮説が記載されています。 ## 概要 新規ユーザーに対し、プレミアム機能を一定期間無料開放することで、 アプリの価値を早期に体験してもらう。 ## 仮説 新規ユーザーはプレミアム機能がロックされているため、 アプリの価値を体験できず、継続利用につながっていない。 次に、エンジニアが作成した仕様書の一部です。PdMとの認識合わせを経て、具体的な仕様に落とし込んでいます。 ## 無料開放期間 - 期間: 初回ホーム到達からx日間 ## 機能要件 ### プレミアム機能の無料開放 - 無料開放期間中、全ユーザーがプレミアム機能を利用できるようにする ### バナー表示(ホーム画面) - 対象: 非プレミアムユーザーのみ - 無料開放期間中: 無料開放中であることを示すバナーを表示 - 無料開放期間終了後: 終了のアナウンスに切り替え そして、この仕様書をもとに作成したシステム設計書の一部です。 ## 設計方針 ### 意味の分離 フリーミアム導入により、「課金しているか」と「プレミアム機能を使えるか」の 意味が同じではなくなるので、コード内でこれらを分離する。 | プロパティ | 意味 | 用途 | | isPremium | 実際に課金しているか | ログ送信パラメータ | | freemiumLastDateTime | フリーミアム期間終了日時 | バナー表示 | | hasFullAccess | プレミアム機能を使えるか | UI表示・遷移制御 | 試してみての所感 この取り組みはまだ実験段階で、試し始めて2週間ほどです。 よかった点 エンジニアがWhatの段階から積極的に介入できる構造になった 仕様書→システム設計書→実装の流れで、エンジニア同士の議論が増えた システム設計書に着手する段階で、大きな手戻りは発生していない 課題 タスクの並列度が上がり、頭の切り替えコストが高い これまでは「実装」と「認識合わせMTG・コードレビュー」の並行だったが、そこに「次のタスクの仕様書作成」が加わる 実装中のタスクと仕様を書くタスクはまったく別の内容なので、コンテキストスイッチが頻繁に発生する 仕様書にどこまで書くべきかの基準がまだ定まっていない どの粒度で書けば後段のシステム設計書や実装がAIで精度よく出てくるのか、引き続き検証が必要 正直なところ、現時点では開発速度が上がったという実感はまだない ただし、仕様書・設計書をドキュメントとして残す運用にしたことで、「何を書けばAIの出力精度が上がるか」を振り返れる状態にはなった。この改善サイクルを回していくことに意味があると考えている まとめ AIツールによって個々のタスクの開発速度は向上していますが、中規模以上のタスクでは設計・実装がボトルネックになっています。この課題に対して、エンジニアがPRDの仕様(What)を主導して書くというアプローチを導入しました。 まだ試し始めたばかりで、タスク並列度の増加や仕様書の粒度の最適化など、新たな課題も見えてきています。 今後は仕様書のフォーマットを改善しながら、仕様書→システム設計書のAI自動生成についてもPDCAを回していきたいと思います。
目次 はじめに UserMatching(UM)とは QAの課題 Agent Skillsというアプローチ QA手順をSkillsに落とし込む 設計した5つのSkills Skill設計のポイント LPのQA: 表示条件のパターンを自動で網羅する シナリオの自動生成 Claude in Chromeによる自動確認を断念した経緯 ETLのQA: テストデータ設計から検証まで一気通貫 テストデータの自動設計 検証フロー 導入効果と課題 効果 課題 おわりに はじめに こんにちは、デリッシュキッチンのUserMatching事業でエンジニアをしている惟高です。 今回は、Anthropicが提供する Agent Skills を活用して、UserMatching(UM)のQA業務を効率化した取り組みを紹介します。 QAに必要な手順や判断基準をSkillとして定義し、「△△のLPをQAして」「△△のETLをQAして」といった一言で複雑なQAフローを進められる仕組みを構築しました。 最終的には、LPのQAでは設定分析とテストシナリオ生成までを、ETLのQAではテストデータ設計から実行結果の照合までをSkill化しました。 UserMatching(UM)とは デリッシュキッチンやトモニテでは複数のプレゼントキャンペーンがあり、ユーザー様はLP(ランディングページ)経由でいくつかの質問に回答することで応募できるようになっています。クライアント様の要望に応じたLPを個別で作成しているため、案件ごとにフォームの構成や表示条件が異なります。 案件ごとの基本的な流れは、LPで回答を収集し、その回答データがDBに保存され、最後にETLでクライアント向けの出力形式へ変換する、というものです。 UMでの主なQA対象は以下の2つです。 LP(ランディングページ) :案件ごとにフォームの設問・表示条件・応募完了条件が異なり、ユーザーの回答によって後続の設問が動的に変わるなど、複雑な条件分岐が組まれています。 ETL(データ変換処理) :応募データをクライアント企業が求める形式に変換する処理です。変換ルールやフィルタリング条件が案件ごとに設定されます。 参考: DELISH KITCHEN×パルシステム神奈川 プレゼントキャンペーン|DELISH KITCHEN QAの課題 LP・ETLともにビジネス側が管理画面から設定とQAを担当していますが、案件ごとの設定が複雑で、初見では確認すべきポイントをつかみにくい状況でした。 たとえばLPでは、「神奈川県在住」「26歳以上」「現在サービス未利用」といった応募条件に加え、ある設問への回答によって次の設問の表示有無が変わることがあります。そのため、単に入力項目を埋めるだけではなく、「どの回答パターンでどの設問が表示されるか」「どの条件で応募対象外になるか」まで含めて確認する必要があります。 またETLでも、応募データをクライアントごとの指定フォーマットに合わせて変換するため、日付形式の変換、項目名のマッピング、特定条件のデータ除外などを案件ごとに確認する必要があります。1つの設定ミスでも、出力結果全体が意図とずれてしまいます。 こうした背景から、主に以下の3つが課題でした。 パターンの多さ : LPは表示条件の組み合わせが、ETLは変換ルールとフィルタリング条件の組み合わせが多く、網羅すべきテストパターンが膨大になります。 人依存の網羅性 : 確認すべきパターンの優先度や網羅範囲の判断が担当者の経験に委ねられており、案件ごとに確認観点がばらつく状況でした。 作業コスト : テストパターンの設計・実施から結果照合まで手動工程が多く、案件数が増えるほどQAがボトルネックとなっていました。 Agent Skillsというアプローチ Agent Skills は、Anthropicが提供する再利用可能なプロンプトテンプレートの仕組みです。本記事でいうSkillは、Claudeへの指示手順と補助スクリプトをまとめて再利用できる単位、と捉えてもらうとイメージしやすいと思います。 業務手順や判断基準をSkillとして定義しておくことで、自然言語の指示を起点に複雑なタスクを進めやすくなります。Claude CodeとClaude Desktopの両方で利用できますが、今回はビジネス側のメンバーも使えるよう、GUIで操作できるClaude Desktop上で運用しています。 QA支援の仕組みとしては管理画面にQA機能を組み込む選択肢もありましたが、以下の理由でClaude Desktop上のAgent Skillsを選択しました。 開発コストの低さと柔軟性 : 管理画面への実装と比べてSkillを作成するだけで開発コストが低く、QA観点の変更もSkillの修正だけで対応できます。 一定の手順で進められる : Skillとして手順と判断基準を定義することで、担当者による観点のばらつきを抑えつつ、一定の流れでQAを進められるようになりました。 ビジネス側のメンバーが直接使える : 弊社ではビジネス側にもClaude Desktopを配布しており、Skillsは自然言語で呼び出せるため、QA担当者が「△△のLPをQAして」と指示するだけでQAフローを開始できます。 QA手順をSkillsに落とし込む 設計した5つのSkills まず完成形のイメージを紹介します。たとえばLPのQAの場合、以下のような対話になります。 QA担当者 : 「△△のLPをQAして」(△△は案件名) Claude : 案件の設定を読み込み、フォームの表示条件を分析します。 Claude : 設定ミスが1件見つかりました。テストシナリオを8パターン生成しました。確認してください。 QA担当者 : シナリオ5を修正して、あとはOK Claude : 修正しました。チェックリストを出力します。 このように、 設定の読み込み→シナリオ生成→人間のレビュー→チェックリスト出力 を一連の対話で進められます。これを実現するために、以下の5つのSkillを設計しました。 # 分類 スキル名 役割 1 設定取得 campaign-lp-lookup 案件の設定情報を取得する(LP/ETL共通) 2 導出 lp-column-resolver フォーム定義からDBの保存形式を導出する 3 設定取得 etl-config-lookup ETLの変換設定とフィルタリング条件を取得・整形する 4 分析 lp-form-checker 設定の整合性チェック・テストシナリオ生成・チェックリスト出力を行う 5 実行 etl-qa-runner テストデータの設計フェーズと、投入・実行・検証フェーズを持つ LPのQAでは主に campaign-lp-lookup lp-column-resolver lp-form-checker を使い、ETLのQAではそれに etl-config-lookup etl-qa-runner を組み合わせて進めます。 これらのスキルが、LPのQAとETLのQAでどのように連携するかを図にすると、次のようになります。 Agent SkillsによるQAフロー全体像 Skill設計のポイント 再利用性 : 「案件の設定を取得する」スキルはLPのQAとETLのQAの両方で使う共通部品にしました。 段階的な実行 : AIが生成したテストシナリオをそのまま実行せず、人間の確認を挟むことで信頼性を担保しています。 確定的な処理はスクリプトに追い出す : Agent Skillsでは、Skill本体のMarkdownファイルとは別にスクリプトをバンドルできます。「APIから設定データを取得する」「フォーム定義からDBのカラム構造を導出する」といった、入力が決まれば出力も一意に定まる処理はPythonスクリプトとして実装し、Skillから呼び出す形にしています。判断や要約のような曖昧さを含む部分はLLMに任せつつ、データ取得や構造変換のような確定処理はスクリプトで実行することで、再現性を高めています。 ドメイン知識の局所化 : 「フォーム項目がDBにどう保存されるか」の変換ルールはデータ構造導出のスキルに、「ETLの変換タイプごとの仕様」は変換設定取得のスキルに閉じ込めています。各スキルが担当する知識のスコープを明確にすることで、メンテナンスしやすくなります。 LPのQA: 表示条件のパターンを自動で網羅する シナリオの自動生成 LPでは、ユーザーの回答内容によって後続の設問が表示されたり、応募対象外になったりします。こうした条件分岐を人手だけで漏れなく追うのは難しく、QAでは「どの回答パターンで何が表示されるべきか」を整理する必要があります。 たとえば、同じLPでも回答内容によってフォームの表示内容や応募可否が変わります。 応募対象外になるパターン 応募対象外になるパターン 後続の設問が表示されるパターン 後続の設問が表示されるパターン このような分岐が複数の設問にまたがって存在するため、確認すべき表示パターンを体系的に洗い出すことがLPのQAでは重要になります。 まず案件の設定データを取得し、以下の2つの分析を行います。 設定ミスの自動検出 : フォーム設定とフロントエンドの実装仕様を突合し、矛盾や不整合がないかをチェックします。 表示条件テストシナリオの生成 : 応募条件と表示条件を分析し、テストすべき表示パターンを整理します。各シナリオには「どの選択肢を選ぶか」「何が表示されるべきか」「応募不可になるか」といった期待結果を付与します。最終的な過不足の確認は担当者がレビューして補います。 生成されたシナリオはユーザーに提示され、追加・修正・削除を経て承認後、チェックリストが出力されます。 LPのQAチェックリスト Claude in Chromeによる自動確認を断念した経緯 当初はClaude in Chrome(Claude Desktopのブラウザ操作機能)を使い、承認済みシナリオの実機確認まで自動化する設計で、実際に動作する仕組みも構築しました。しかし、1シナリオあたり3〜5分ほどかかり、途中で処理が止まってしまうこともありました。特に各操作でスクリーンショット取得を挟む点が律速となり、実運用には向きませんでした。 結果としてこのSkillの運用は取りやめ、 パターンの洗い出しとチェックリスト出力までをlp-form-checkerに集約し、実機確認は担当者が行う フローに落ち着きました。パターンの網羅的な洗い出しこそがQAの課題の本質だったため、この分担でも十分に価値がありました。 ETLのQA: テストデータ設計から検証まで一気通貫 ETLのQAでは、テストデータの設計から投入、ETL実行、結果検証までを一連の流れで実行します。 ETLのQAフロー テストデータの自動設計 ETLのQAで最も手間がかかるのがテストデータの準備です。LPのフォームから毎回手入力するのは非効率なため、必要なテストデータを開発環境のDBに直接投入する方針を取りました。投入先は本番とは分離された開発環境の検証用データに限定しており、本番データや本番環境には触れない前提で運用しています。ここでの目的はETLの変換処理とフィルタリング条件の検証に絞っており、UI経由の入力体験やLP側の表示制御の確認はLPのQAで担保しています。 直接投入にはデータの「形」を正確に知る必要があり、LP側で応募不可になるパターンの除外判断にもLPの表示条件の知識が必要です。Skillはこうした複雑さを吸収し、以下のテストパターンを自動生成します。 変換テスト : 正常値・境界値・異常値で各変換タイプが正しく動作するか フィルタリングテスト : 全条件クリア(有効)、各条件で無効になるパターン 共通バリデーション : 電話番号・メール・郵便番号・日時の形式チェック 検証フロー 承認後は、テストデータの投入、ETLの再実行、結果取得、期待値との照合までを一連の流れで進めます。不一致が見つかった場合は、ETL設定の問題かテストデータの問題かを切り分けて報告します。 導入効果と課題 効果 最も大きな効果は、QA担当者が毎回ゼロから確認観点を考えなくてよくなったことです。従来は案件ごとに設定画面を読み解き、表示条件や変換条件を見ながらテストパターンを組み立てていましたが、Skill導入後はその整理とたたき台作成を自動化できるようになりました。 自動化の範囲と人間の介在ポイントを整理すると、以下のようになります。 工程 従来(手動) Skill導入後 設定内容の読み解き 人間が管理画面を目視確認 Skillが自動取得・整形 テストパターンの設計 人間が経験ベースで洗い出し Skillがたたき台を自動生成 パターンの妥当性確認 — 人間がレビュー・承認 テスト実行 人間が手動で1パターンずつ LP: チェックリストに沿って人間が確認 / ETL: Skillが自動実行 結果の判定 人間が目視で照合 LP: 人間が目視で判定 / ETL: Skillが期待値と自動照合 LP・ETLに共通してSkillに任せているのは「設定の読み解き」と「パターンのたたき台作成」で、ETLではさらに「テスト実行」と「結果照合」も自動化しています。一方、 パターンの妥当性を判断する工程は担当者に残しています 。完全自動化ではなく、担当者のレビューを挟むことで安全性を高めています。 実際に複数案件で試したところ、特に負荷軽減につながったのは「設定の整合性確認」「パターンの洗い出し」「テストパターンの設計」でした。たとえばLPのQAでは、Skillが最初に複数の表示パターンを整理した状態から確認を始められるため、担当者は不足分の追加や期待結果の見直しに集中できます。 また、LPのQAでは表示条件の整合性を確認する過程で、実際に設定ミスを事前に検出できたケースもありました。見落としやすい条件分岐を人手だけで追う負担を減らしつつ、確認精度の底上げにもつながっています。 ETLの設定でも、変換対象のカラムが想定とは異なるカラムを参照している設定ミスに事前に気づけたケースがありました。設定画面では見落としやすい差分でも、Skillが設定を整理して確認観点を提示することで発見しやすくなりました。 ETLでも、従来はLPから実際に応募して用意していた検証データを、開発環境に直接投入する形へ置き換えられたため、テストデータ準備の手間を減らせました。フォーム入力を何度も繰り返さずに必要な検証ケースを作れるようになり、変換条件やフィルタリング条件の確認に集中しやすくなっています。 課題 今回の取り組みを通じて、以下の2点を課題として感じました。 Claude in Chromeの安定性 : 実機確認の自動化を試みましたが、1シナリオあたり3〜5分ほどかかり、途中で処理が止まってしまうこともあったため断念しました。 Skillの保守コスト : 管理画面の仕様変更や新しい変換タイプの追加があった場合、Skillファイルの更新が必要です。各利用者のローカル環境に配置されるため変更の配布・反映を自動化しにくく、現状は手動更新を前提に運用しています。 おわりに QAに必要な手順と判断基準をAgent Skillsとして定義することで、「LPの表示パターンの整理と確認観点の洗い出し」「ETLのテストデータ設計から検証まで」という複雑なQA業務を効率化できました。 今後はQA以外の業務にもSkillを活用していきたいと考えています。案件のセットアップ作業など、手順や判断基準をSkillとして定義できる場面は多く、幅広い業務への展開が期待できます。 最後までお読みいただきありがとうございました。
はじめに Gson について Gson の課題 1. Null 安全が破壊されるリスク 2. デフォルト引数が無視される Kotlin Serialization について 具体的な修正内容 1. Data Class の書き換え 2. Retrofit の Converter の置き換え まとめと今後の課題 はじめに こんにちは、株式会社エブリーで Android アプリ開発を担当している岡田です。 弊社が提供する デリッシュキッチン の Android アプリでは、アプリの堅牢性向上とモダンな開発体験のための選択として、JSON パーサーを従来の Gson から Kotlin Serialization への移行を検討しています。 今回は弊社で行なっているイベント「挑戦WEEK」にて、Gson から Kotlin Serialization への移行を、Android のコードベース変更に限定して挑戦してみました。こちらについて、少しお話しさせていただければと思います。 弊社の挑戦WEEKの取り組みについては以下の記事をご覧ください! tech.every.tv Gson について Android アプリの開発において、API との通信で受け取った JSON をデータクラスに変換する「JSON パース」は避けては通れない実装です。 デリッシュキッチン の Android アプリでは、長らく JSON パーサーとして Google 製の「Gson」を利用してきました。Gson は非常に歴史が長く、Android アプリ開発の黎明期からデファクトスタンダードとして広く使われており、Retrofit などのネットワークライブラリとも標準で連携しやすいという特徴があります。 長年アプリの通信基盤を支えてくれた Gson ですが、プロジェクトのフル Kotlin 化が進み、よりモダンな言語仕様を活用していく中で、実は Android アプリを開発する上でいくつかの大きな課題を抱えるようになっていました。 Gson の課題 Java 時代には非常に優秀だった Gson ですが、Kotlin で構成された現代のアプリにおいては、Kotlin の強みである言語仕様とコンフリクトを起こすケースが目立つようになってきました。 1. Null 安全が破壊されるリスク Gson は内部でリフレクション( sun.misc.Unsafe など)を用いてインスタンスを生成します。そのため、Kotlin のデータクラスでプロパティを「非 Null( String など)」で定義していても、サーバーから返ってくる JSON 側にそのキーが存在しない場合、Gson は強制的に null を代入してしまいます。 これにより、Kotlin コンパイラが保証しているはずの「Null 安全」がランタイムで破壊され、アプリの思わぬところで NullPointerException を引き起こす原因となっていました。 2. デフォルト引数が無視される Kotlin のデータクラスでは val isPremium: Boolean = false のようにデフォルト引数を設定できます。しかし、Gson はコンストラクタを経由せずにインスタンスを生成することがあるため、JSON に該当のキーが含まれていない場合、このデフォルト値が適用されません。結果として、意図しない型の初期値( Int なら 0 、参照型なら null )が入ってしまうという問題がありました。 これらの挙動は、開発者が意図しない「不正な状態を持ったインスタンス」がアプリ内を回遊することを意味しており、結果として予期せぬクラッシュの温床になり得ます。 Kotlin Serialization について 最終的に、これらの課題を根本から解決するために、Kotlin 公式が提供しているシリアライズライブラリ「Kotlin Serialization( kotlinx.serialization )」へ移行を検討しています。 Kotlin Serialization は、コンパイル時にシリアライズ・デシリアライズのためのコードを自動生成する仕組みを持っています。実行時に重いリフレクションを行わないため、非常にモダンで Kotlin ライクな設計となっています。 このライブラリへ切り替えることで、以下のような大きな恩恵を受けることができます。 厳格な Null 安全の保証 非 Null として定義したプロパティに対して JSON に値が存在しない場合、強制的に Null を入れるのではなく、パース時に明確に例外( SerializationException )を投げてくれます。これにより、不正なデータによる後続処理でのクラッシュを防ぐことができます。 デフォルト値の完全なサポート JSON にキーが存在しない場合、Kotlin 側で定義したデフォルト引数が正しく適用されます。 パフォーマンス向上とアプリサイズ削減 リフレクションに依存しないため、パース速度が向上します。また、ProGuard/R8 による最適化とも相性が良く、アプリのバイナリサイズの削減にも繋がります。 具体的な修正内容 実際に Gson から Kotlin Serialization へ移行するにあたり、行った具体的な修正内容をご紹介します。 1. Data Class の書き換え Gson の @SerializedName アノテーションを、Kotlin Serialization の @SerialName に変更し、クラスに @Serializable アノテーションを付与します。 【従来の Gson での実装】 data class UserResponse( @SerializedName ( "id" ) val id: Long , @SerializedName ( "user_name" ) val userName: String , @SerializedName ( "profile_image_url" ) val profileImageUrl: String ? ) 【新しい Kotlin Serialization での実装】 @Serializable data class UserResponse( @SerialName ( "id" ) val id: Long , @SerialName ( "user_name" ) val userName: String , // サーバーからキーが送られてこない可能性がある場合はデフォルト値を設定 @SerialName ( "profile_image_url" ) val profileImageUrl: String ? = null , @SerialName ( "is_premium" ) val isPremium: Boolean = false ) 2. Retrofit の Converter の置き換え API 通信に Retrofit を使用しているため、Gson の ConverterFactory を Kotlin Serialization 用のものへ差し替えました。 この際、サーバーからのレスポンスにおいて、アプリ側で定義していない未知のキーが含まれていてもパースエラーにならないよう、 ignoreUnknownKeys = true を設定しています。 // Json パーサーの設定 val json = Json { ignoreUnknownKeys = true // 未知のキーを無視する coerceInputValues = true // null が来た場合にデフォルト値があればフォールバックする } val contentType = "application/json" .toMediaType() val retrofit = Retrofit.Builder() .baseUrl( "https://api.example.com/" ) // GsonConverterFactory.create() からの置き換え .addConverterFactory(json.asConverterFactory(contentType)) .build() 主にこれらの修正を、API レスポンスを受け取る全てのデータクラスと Retrofit クライアントに対して適用し、段階的に移行を進めました。 また他にも com.google.gson.internal.bind.util.ISO8601Utils を利用している箇所や、 JsonUtil という Android アプリ側で Json を扱う際に使用するクラスの修正など、細かい修正も行いました。 総差分ファイル数はおよそ 500 ファイルと、大規模な改修になりました。 まとめと今後の課題 今回の改修で JSON パーサーを Kotlin Serialization に移行したことにより、Kotlin の言語仕様に沿った厳格な型安全性が担保されます。Android のコードベース上での堅牢性は大きく向上しました。 しかし、ライブラリが「厳格」になったからこそ直面する新たな課題もあります。 それは、 サーバーからのレスポンス仕様(スキーマ)の正確な把握 です。 Gson の時代は「JSON にキーがなくても、とりあえず Null を入れてクラッシュさせない」という緩さがありました。しかしこれからは、非 Null プロパティのキーが JSON に存在しなければ、即座にパース失敗となってしまいます。 これを防ぐためには、以下のような対応をサービス全体で意識していく必要があります。 サーバーレスポンスで Null が返る、またはキーが省略される可能性のあるフィールドには、適切な Nullable 定義やデフォルト値を設定する クラッシュログを監視し、パースエラーが発生した場合は迅速にデータクラスの定義をチューニングする サーバーサイドのエンジニアと密に連携し、API 仕様書とクライアント実装の乖離をなくす デリッシュキッチンは歴史のあるサービスですから、型安全に API レスポンスをパースするには、この辺りの見直しは避けて通れません。 時間と根気がいる作業にはなりますが、徐々にでも整備できればと思います。 もしまだ Gson を利用している方で、「データクラスの Null 安全が担保できずに困っている」「原因不明の NullPointerException に悩まされている」と感じているなら、一度 JSON パーサーの移行を検討してみてはいかがでしょうか。 今後も、Kotlin の厳格な型安全性を武器に、より品質が高く安定した デリッシュキッチン をユーザーの皆様にお届けできるよう、改善を続けていきます。
はじめに こんにちは。開発本部 開発1部 デリッシュリサーチチームの 江﨑 です。 本記事では、これまでHive Metastore上のDeltaテーブルで管理していたデリッシュリサーチ用データ(約40テーブル)をUnity Catalogへ移行したプロジェクトの全体像を、インフラ整備からAthena連携・Databricks Managed MCP活用まで紹介します。 はじめに 背景:なぜ Unity Catalog に移行したか 課題 1:テーブルスキーマが「コードを読まないと分からない」 課題 2:データリネージを Mermaid で管理していた Unity Catalog とは マネージドテーブル vs 外部テーブル 移行手順の全体像 Step 1:インフラ整備 IAMロールの設定 Catalogの作成 Step 2:Unity Catalog テーブルの作成 移行スクリプトの流れ Step 3:既存 ETL コードの変更 主な変更:テーブル参照パスの書き換え Step 4:Athena から Unity Catalog のデータを参照する 実際に行った変更 Step 5:Quick Suite のデータセット移行 移行結果 Before / After Before(Hive Metastore 時代) After(Unity Catalog 移行後) Databricks Managed MCP Databricks Managed MCP とは DBSQL MCP:ETL 開発が変わる 具体的な開発体験 まとめ 背景:なぜ Unity Catalog に移行したか 移行前、デリッシュリサーチではDatabricksのHive Metastore上で約40個のDeltaテーブルを運用していました。その中で、運用上の課題が積み重なってきていました。 課題 1:テーブルスキーマが「コードを読まないと分からない」 「このテーブルに user_id カラムってあったっけ?」という確認をするたびに、Notebookを開いて display(spark.table("schema.table")) を実行するか、ETLコードを読み返す必要がありました。テーブルが増えるほど、この手間もかさんでいきます。 課題 2:データリネージを Mermaid で管理していた テーブル間の依存関係(「このテーブルはどのテーブルから作られているか」)をMermaidのコードで手作業管理していましたが、40テーブルを超えると複雑すぎてメンテナンスが限界になり放置されていました。テーブルを追加するたびにMermaidを手で更新する運用は、明らかにスケールしません。 これらの課題を解消するためにUnity Catalogへの移行を決めました。 Unity Catalog とは Unity CatalogはDatabricksの統合データガバナンスソリューションです。Hive Metastoreとの最大の違いは、 三層の名前空間(Catalog > Schema > Table) を持つ点です。 Catalog(例: marketing_research) └── Schema(例: search) └── Table(例: search_count) Hive Metastoreでは schema.table の二層構造でしたが、Unity Catalogでは catalog.schema.table の三層になります。この変更によって、チームやプロジェクトを跨いだデータの整理がしやすくなります。 主な機能は以下の3点です: データカタログ :テーブルのスキーマ・カラムの説明文をUI上で管理・参照できる データリネージ :データの流れ(どのテーブルがどのテーブルを参照しているか)を自動追跡・可視化 アクセス制御 :行・列レベルの細粒度なセキュリティ設定 Unity Catalogのオブジェクト階層。今回の移行ではCatalog > Schema > Tableの三層構造を利用。(出典: Databricks公式ドキュメント ) Unity CatalogのUI。テーブルを選択するとカラム名・データ型・コメントを一覧で確認できる。 マネージドテーブル vs 外部テーブル Unity Catalogへの移行を検討するとき、決めなければならないのが テーブルタイプ です。 観点 マネージドテーブル 外部テーブル データ保管場所 Unity Catalogが管理するパス 任意のストレージパス(S3など) S3パスの形式 自動生成されたID形式になる 任意のS3パスを指定できる テーブル削除時 データも削除される データは残る Databricksの推奨 ほとんどのユースケース 既存ストレージとの互換性が必要な場合 Databricksはマネージドテーブルを推奨していますが、本プロジェクトでは 外部テーブル を選択しました。 その理由は、データ参照構成にあります。デリッシュリサーチではダッシュボードを Amazon Quick Suite(以下Quick Suite) → Athena → Glue Crawler → S3 という構成で構築しています。Glue Crawlerはクロール先のS3パスを読み取ってテーブル名を付与します。 ここで問題になるのが、マネージドテーブルのS3パス形式です。マネージドテーブルに移行すると、S3パスは s3://unity-catalog-metastore/__unitystorage/... のような __unitystorage 配下のシステム管理ディレクトリ(ランダム生成IDを含むパス) に配置されます。Glue CrawlerはS3プレフィックス/フォルダ名ベースでテーブル名を付けるため、Athena側のテーブル名が人間可読でない名前になり、現実的な運用が難しくなります。 外部テーブルであればS3パスを s3://<バケット名>/table_name のようにテーブル名ベースで保持できるため、Athena上のテーブル名が人間にとって分かりやすい名前のままになり、移行コストを抑えつつ今後の管理も楽になります。 移行手順の全体像 移行は以下の5ステップで実施しました。 Step 1 :インフラ整備(IAM・Catalogの作成) Step 2 :Unity Catalogテーブルの作成 Step 3 :既存ETLコードの変更(テーブル参照パスの更新) Step 4 :AthenaからUnity Catalogのデータを参照できるようにする Step 5 :Quick Suiteのデータセット移行 Step 1:インフラ整備 Unity Catalogを有効化するにあたって、AWS・Databricks側でいくつかの設定が必要でした。 IAMロールの設定 DatabricksがS3バケットにアクセスするためのIAMロールと、それをDatabricks側に登録するストレージクレデンシャルを新規作成しました。インフラ変更はTerraformで管理しています。概念的には以下のような構成です。 resource "aws_iam_role" "databricks_unity_catalog" { name = "<ロール名>" assume_role_policy = jsonencode ( { Statement = [{ Effect = "Allow" Principal = { AWS = "arn:aws:iam::<Databricks の AWS アカウント ID>:role/unity-catalog-prod-role" } Action = "sts:AssumeRole" Condition = { StringEquals = { "sts:ExternalId" = <databricks_sts_external_id> } } }] } ) } Catalogの作成 Unity Catalogを利用するには、Databricks上にCatalogを作成する必要があります。Catalog作成時のmanaged storage location指定は任意ですが、今回は運用上の理由から指定のS3バケットをmanaged storage locationとして指定しました。 参照: Unity Catalog の Catalog を作成する(Databricks 公式ドキュメント) Step 2:Unity Catalog テーブルの作成 インフラが整ったら、既存のHive MetastoreテーブルのデータをUnity Catalogの外部テーブルとして再作成します。旧パスから schema/table というパス構成に移行するため、CTAS(CREATE TABLE AS SELECT)を使いました。 移行スクリプトの流れ 旧パスのDeltaテーブルを SELECT * で読み取る schema/table 形式のパスにDelta形式で書き込みながらUnity Catalogの外部テーブルを作成 # 旧 Delta からデータをコピーしながら UC 外部テーブルを作成 spark.sql(f """ CREATE TABLE IF NOT EXISTS {catalog}.{schema}.{table_name} USING DELTA LOCATION '{new_s3_path}' AS SELECT * FROM delta.`{old_s3_path}` """ ) Step 3:既存 ETL コードの変更 主な変更:テーブル参照パスの書き換え Hive MetastoreではS3パスを直接指定してデータを読み書きしていましたが、Unity Catalogでは catalog.schema.table の三層構造で参照するよう書き換えました。 # Before(Hive Metastore):S3 パスを直接指定 df = spark.read.format( "delta" ).load( "s3://path/to/delta" ) df.write.format( "delta" ).save( "s3://path/to/delta" ) # After(Unity Catalog):カタログ名で参照 df = spark.table( "catalog.schema.table" ) df.write.saveAsTable( "catalog.schema.table" ) Step 4:Athena から Unity Catalog のデータを参照する 前述の通り、Quick Suiteのデータソースは Athena → Glue Crawler → S3 という構成です。Unity Catalogに移行しても、この構成を維持する必要があります。 実際に行った変更 Glue Crawlerのクロール先を、Unity Catalog外部テーブルのS3パスに変更しました。 Before: 旧 S3 バケット(Hive Metastore 用)→ Glue Crawler → Athena After: Unity Catalog 外部テーブルの S3 バケット → Glue Crawler → Athena 具体的な変更内容: Glue Crawlerのクロール先S3パスをUnity Catalog外部テーブルのパスに変更 GlueのIAMポリシーにS3バケットへのアクセス権限を追加 Terraformで管理しているIAMポリシーとGlueリソースを更新し、 terraform apply 後にGlueコンソールからクローラーを手動実行してテーブルが正しく作成されることを確認しました。 Step 5:Quick Suite のデータセット移行 Step 4でGlue → Athena側の変更が完了したら、Quick Suiteのデータセット参照先を新しいAthenaテーブルに切り替えます。 変更自体はQuick SuiteのUIから接続設定を変更するだけで完結しますが、切り替え前に テーブルスキーマの互換性確認 が重要です。カラム名・データ型が一致していないと、ダッシュボードの集計が壊れます。 移行結果 Before / After 移行の成果をBefore / Afterでまとめます。 Before(Hive Metastore 時代) 項目 状態 テーブルスキーマ確認 Notebookを実行するかコードを読む必要がある データリネージ Mermaidで手作業管理 After(Unity Catalog 移行後) 項目 状態 テーブルスキーマ確認 Unity CatalogのUIでカラム一覧・データ型・説明文を即時参照 データリネージ UI上で自動生成・可視化(Mermaidでの手作業管理が不要に) 特にデータリネージの自動可視化は、Mermaidの維持コストをゼロにしてくれる大きな恩恵でした。Unity CatalogにETLジョブが書き込むと、どのテーブルがどのテーブルから作られているかが自動でグラフとして記録されていきます。 Unity Catalogのデータリネージ画面。どのテーブルがどのテーブルから作られているかが自動でグラフ表示される。 Databricks Managed MCP Unity Catalogへの移行をきっかけに、Databricks Managed MCPも使えるようになったので紹介します。 Databricks Managed MCP とは Databricks Managed MCPとは、DatabricksがホストするMCP(Model Context Protocol)サーバーです。Claude CodeなどのAIエージェントからDatabricksのリソースをツールとして呼び出せるようにする仕組みです。 Databricks Managed MCPはUnity Catalogとの統合を前提に設計 されています。今回の移行後、実際に使えるようになりました。 DBSQL MCP:ETL 開発が変わる Databricks Managed MCPの中でも、ETL開発が中心のデリッシュリサーチにとって特に相性が良さそうだと感じたのが DBSQL MCP です。これはClaude CodeやCursorのMCPとして設定することで、ETL開発中にSQLをその場で実行・確認できるようになるツールです。 提供されるツール: execute_sql_read_only :テーブルの内容・カラム定義・データ分布をその場で確認 execute_sql :SQLの実行 poll_sql_result :長時間クエリの結果をポーリング ※ ツール名や提供機能はアップデートで変更される可能性があります。上記は執筆時点(2026年3月6日)で使用できるツールです。 具体的な開発体験 ETLコードを書きながら、Claude Codeに対して次のような依頼ができるようになります: 「 catalog.schema.table のカラム構成を確認して」 「 product_id カラムのユニーク数を調べて」 「このテーブルとJOINするテーブルのスキーマを見せて」 DBSQL MCPを使うことでCursorやVS CodeなどのエディタからDatabricksのテーブルの中身・スキーマをリアルタイムで確認しながらコーディングができます。 なお、DBSQL MCP以外にもUnity Catalog functionsやGenie spaceなども使えるようになっています。 参照: Databricks Managed MCP 公式ドキュメント MCPの詳細な設定方法や活用例については、 こちらの記事 も参照してください。 まとめ 約40テーブルのHive Metastore → Unity Catalog移行を通じて得た主な成果を3点でまとめます。 データカタログとデータリネージの整備 :Unity CatalogのUIでスキーマ情報を参照でき、データリネージも自動管理されるため、「コードを読まないと分からない」問題が解消されました 外部テーブルで構成互換性を維持 :Athena連携(Glue Crawler)がある場合は外部テーブルを選ぶことで、既存のBI構成に影響を与えずに移行できました Databricks Managed MCPが使えるようになった :ETL開発中にエディタから出ずにテーブル情報を確認できるようになり、開発体験が向上しました 今後は、Unity Catalogへの移行によって使えるようになった機能の検証・活用をしていきたいと考えています。 Unity Catalogへの移行を検討している方の参考になれば幸いです。
こんにちは @kyo です! 2026年2月21日に開催された Go Conference mini in Sendai 2026 にて、「GoとWasmでつくる軽量ブラウザUI」というタイトルで登壇させていただきました。この記事では、発表中にいただいたフィードバックについて深掘りをして得られた知見をご共有できたらと思います。 フィードバック: 「 (*js.Value).Call は遅いので、 bind したうえで Invoke するといいですよ」 from Hajime Hoshi さん、Go製ゲームエンジン Ebitengine の作者 発表スライド speakerdeck.com 背景 Go の syscall/js パッケージでは、JS のメソッドを呼び出す方法が2つあります。 方法 Go コード 特徴 Call document.Call("getElementById", "myDiv") シンプルだが毎回オーバーヘッドあり bind + Invoke getElementById.Invoke("myDiv") 初期化が必要だが高速 Call が遅い理由 前提知識: Go Wasm の仕組み Go で書いた Wasm コードがブラウザの JS を呼び出すとき、直接呼べるわけではありません。 間に Wasm メモリ と wasm_exec.js (Go 公式提供の橋渡しスクリプト)を挟んでやりとりします。 Wasm メモリ(Linear Memory)とは? Wasm メモリは WebAssembly の仕様で定義された WebAssembly.Memory オブジェクトで、 実体は Go(Wasm)と JavaScript の 両方からアクセスできる巨大なバイト配列 ( ArrayBuffer )です。 「リニアメモリ(Linear Memory)」とも呼ばれます。 developer.mozilla.org wasmbyexample.dev 普通、Go と JS はお互いの変数を直接見ることができませんが、 この共有のバイト配列を「伝言板」のように使うことで、データをやりとりできます。 例: document.Call("getElementById", "myDiv") の場合 Go 側が "getElementById" という文字列をバイト列に変換して Wasm メモリに書き込む JS 側( wasm_exec.js )が Wasm メモリからそのバイト列を読み出す TextDecoder で JS の文字列に変換する(= loadString() ) その文字列を使って document["getElementById"] を探す(= Reflect.get() ) 見つけた関数を実行する Invoke が速い理由は、このステップ 1〜4 を丸ごとスキップできるからです。 事前に関数への参照を取得しておけば、Wasm メモリを経由した文字列のやりとりが不要になります。 Call の処理の流れ Go 側で document.Call("getElementById", "myDiv") を呼ぶと、 wasm_exec.js の以下のコードが実行されます: // wasm_exec.js "syscall/js.valueCall" : ( sp ) => { sp >>>= 0 ; try { const v = loadValue ( sp + 8 ) ; // ① オブジェクトを取得(例: document) const m = Reflect . get ( v , loadString ( sp + 16 )) ; // ② ここが遅い(後述) const args = loadSliceOfValues ( sp + 32 ) ; // ③ 引数を取得(例: "myDiv") const result = Reflect . apply ( m , v , args ) ; // ④ 関数を実行 sp = this. _inst . exports . getsp () >>> 0 ; storeValue ( sp + 56 , result ) ; // ⑤ 結果をメモリに書き戻す this. mem . setUint8 ( sp + 64 , 1 ) ; // ⑥ 成功フラグ } catch ( err ) { // エラー処理... } } , ② が遅い理由には二つの原因があります const m = Reflect . get ( v , loadString ( sp + 16 )) ; // ^^^^^^^^^^^^^^^^^^ ← (A) 文字列デコード // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^← (B) プロパティ検索 (A) loadString() — 文字列デコード const loadString = ( addr ) => { const saddr = getInt64 ( addr + 0 ) ; // Wasmメモリ上の文字列の開始位置 const len = getInt64 ( addr + 8 ) ; // 文字列の長さ(バイト数) return decoder . decode ( // TextDecoder で バイト列 → JS文字列に変換 new DataView ( this. _inst . exports . mem . buffer , saddr , len ) , ) ; } ; Go が Wasm メモリに書き込んだバイト列を、 TextDecoder を使って JavaScript の文字列 "getElementById" に変換しています。 この処理では毎回 new DataView の生成と decoder.decode() が走っています。 (B) Reflect.get() — プロパティ検索 補足: プロパティとプロパティ検索とは? JavaScript のオブジェクトは、名前(キー)と値のペアの集まり です。 この「名前と値のペア」1つ1つを プロパティ と呼びます。 // document オブジェクトのイメージ(実際はもっと多い) document = { "getElementById" : function ( ... ) { ... } , // ← プロパティ "createElement" : function ( ... ) { ... } , // ← プロパティ "querySelector" : function ( ... ) { ... } , // ← プロパティ "title" : "My Page" , // ← プロパティ // ... 他にも数百のプロパティがある } ; プロパティ検索 とは、この中から名前を指定して値を探す処理です。 Go でいえば `map[string]any` から `map["getElementById"]` でキーを探すのに近いイメージです。 // プロパティ検索の例(どれも同じ意味) document .getElementById ; // ドット記法 document [ "getElementById" ] ; // ブラケット記法 Reflect . get ( document , "getElementById" ) ; // Reflect API(wasm_exec.js が使う方法) Reflect . get ( v , "getElementById" ) ; // これは実質的に v["getElementById"] と同じ // = document オブジェクトから "getElementById" という名前の関数を探す JavaScript のオブジェクトからプロパティ名で関数を検索します。 ここの処理でも毎回この探索処理が走ります。 Invoke の処理の流れ 一方、 getElementById.Invoke("myDiv") を呼ぶと // wasm_exec.js "syscall/js.valueInvoke" : ( sp ) => { sp >>>= 0 ; try { const v = loadValue ( sp + 8 ) ; // ① 関数そのものを取得(文字列ではない) const args = loadSliceOfValues ( sp + 16 ) ; // ② 引数を取得 const result = Reflect . apply ( v , undefined , args ) ; // ③ 関数を直接実行 sp = this. _inst . exports . getsp () >>> 0 ; storeValue ( sp + 40 , result ) ; // ④ 結果をメモリに書き戻す this. mem . setUint8 ( sp + 48 , 1 ) ; // ⑤ 成功フラグ } catch ( err ) { // エラー処理... } } , Call との違い loadString() がない → 文字列デコードが不要 Reflect.get() がない → プロパティ検索が不要 v はすでに関数への参照なので、 Reflect.apply() で直接呼ぶだけ 処理の違いまとめ Call の処理: Go → [メソッド名をメモリに書く] → JS: loadString() → Reflect.get() → Reflect.apply() ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~~~~ 毎回発生するオーバーヘッド 文字列デコード プロパティ検索 Invoke の処理: Go → JS: Reflect.apply() 図解 1. Call パターン(毎回のオーバーヘッド) 2. bind + Invoke パターン(初回のみオーバーヘッド) 3. 処理ステップの比較 4. bind が必要な理由 JS ではメソッドをオブジェクトから切り離すと this コンテキストが失われます。 bind で this を固定しないと Invoke 時にエラーになります。 コード例 遅いパターン(毎回 Call ) document := js.Global().Get( "document" ) for i := 0 ; i < 1000 ; i++ { // 毎回: 文字列書き込み → デコード → プロパティ検索 → 実行 element := document.Call( "getElementById" , "myElement" ) element.Call( "setAttribute" , "data-index" , i) } 速いパターン( bind + Invoke ) document := js.Global().Get( "document" ) // 初期化: bind で this を固定 getElementById := document.Get( "getElementById" ).Call( "bind" , document) for i := 0 ; i < 1000 ; i++ { // 毎回: 関数実行のみ(文字列処理・プロパティ検索なし) element := getElementById.Invoke( "myElement" ) // ... } 実用的なパターン: よく使うメソッドをまとめて事前バインド var ( document = js.Global().Get( "document" ) getElementById = document.Get( "getElementById" ).Call( "bind" , document) createElement = document.Get( "createElement" ).Call( "bind" , document) querySelector = document.Get( "querySelector" ).Call( "bind" , document) consoleLog = js.Global().Get( "console" ).Get( "log" ).Call( "bind" , js.Global().Get( "console" )) ) func getElement(id string ) js.Value { return getElementById.Invoke(id) } func newElement(tag string ) js.Value { return createElement.Invoke(tag) } オーバーヘッド比較表 処理 Call bind + Invoke 文字列の Wasm メモリ書き込み 毎回 初回のみ TextDecoder によるデコード 毎回 初回のみ Reflect.get (プロパティ検索) 毎回 初回のみ Reflect.apply (関数呼び出し) 毎回 毎回 makeArgSlices + storeArgs 毎回 毎回 ベンチマーク結果(10,000回呼び出し) 各メソッドを計測した実測結果 DOM操作(実際のJS API) JS API自体の実行コストが含まれるため、相対的な差は小さい。 // Call パターン document.Call( "getElementById" , "myElement" ) // bind+Invoke パターン getElementById := document.Get( "getElementById" ).Call( "bind" , document) getElementById.Invoke( "myElement" ) 対象メソッド Call (ms) bind+Invoke (ms) 差分 速度比 document.getElementById 48.7 46.6 +2.1 ms 1.05倍 console.log 68.3 59.3 +9.0 ms 1.15倍 element.setAttribute 26.8 25.8 +1.0 ms 1.04倍 DOM操作自体のコストが大きいため、Call と bind+Invoke の差は 3〜15% 程度に留まる。 純粋なオーバーヘッド検証 JS側の処理コストを排除し、 Call 固有のオーバーヘッドを可視化。 空の関数(何もしない関数) JS側に何もしない関数を用意し、呼び出しオーバーヘッドだけを測定。 // Call パターン: 毎回「文字列デコード → プロパティ検索 → 関数実行」 noopObj.Call( "noop" ) // bind+Invoke パターン: 事前バインド済みなので「関数実行」のみ noop := noopObj.Get( "noop" ).Call( "bind" , noopObj) noop.Invoke() 対象 Call (ms) bind+Invoke (ms) 差分 速度比 noop 1.90 0.40 +1.50 ms 4.76倍 JS側の処理コストがないため、 Call 固有のオーバーヘッド(文字列デコード + プロパティ検索)が約4〜5倍の差としてはっきり現れる。 メソッド名の長さによる影響 Call は毎回メソッド名を文字列デコードするため、名前が長いほどコストが増えるか検証。 // 短いメソッド名(1文字) obj.Call( "a" ) // 長いメソッド名(30文字) obj.Call( "abcdefghijklmnopqrstuvwxyz1234" ) // bind+Invoke はどちらも同じ(事前バインド済み) fn := obj.Get( "a" ).Call( "bind" , obj) fn.Invoke() 対象 Call (ms) bind+Invoke (ms) 差分 速度比 メソッド名 "a"(1文字) 1.80 0.50 +1.30 ms 3.61倍 メソッド名 "abcdefghij...1234"(30文字) 2.10 0.60 +1.50 ms 3.50倍 メソッド名の長さはほぼ影響しない。 TextDecoder のコストは小さく、Go↔JS間の境界越え自体( valueCall のスタック操作 + Reflect.get )の方がはるかに大きい。(メソッドをたくさん増やしたらもっと差が出るかも) いつ使い分けるか シナリオ 推奨 理由 高頻度呼び出し(60fps 描画、大量 DOM 操作) bind + Invoke オーバーヘッド削減の効果が大きい 低頻度呼び出し(ボタンクリック等) Call でOK 可読性を優先、パフォーマンス差は体感できない 同じメソッドをループで繰り返し呼ぶ bind + Invoke 最もメリットが出るケース まとめ Call は毎回「文字列の Wasm メモリ書き込み → TextDecoder によるデコード → Reflect.get によるプロパティ検索」という3つのオーバーヘッドが発生する bind + Invoke は事前に関数参照を取得・固定しておくことで、これらのオーバーヘッドをすべてスキップし、 Reflect.apply で直接関数を実行できる 純粋なオーバーヘッド比較では約4〜5倍の差があり、高頻度呼び出し(描画ループや大量DOM操作)では効果が大きい 一方、DOM操作自体のコストが大きい場面では差は数%程度に留まるため、低頻度の呼び出しでは Call のシンプルさを優先して良さそう よく使うメソッドを var でまとめて事前バインドしておくのが実用的なパターン 最後に Go Conference mini in Sendai で Hajime Hoshi さんからいただいた「 Call は遅いので bind + Invoke がいいですよ」というフィードバックは、最初は「そういうテクニックがあるんだな」程度の理解でした。しかし実際に wasm_exec.js のソースコードを読んでみると、 Call が遅い理由は単なる「関数呼び出しの方法の違い」ではなく、Go と JavaScript という2つの異なるランタイムが Wasm メモリという共有バイト配列を介してやりとりする仕組みそのものに起因していることがわかりました。 普段 Go を書いているだけでは意識しない「文字列がバイト列として Wasm メモリに書き込まれ、JS 側で TextDecoder によってデコードされる」という一連の流れを知ったことで、Go Wasm が裏側でどれだけの処理をしているのかを実感できました。と同時に、 wasm_exec.js がたった1つのファイルで Go と JS の橋渡しをすべて担っていることに、改めてすごさを感じました。 カンファレンスでのたった一言のフィードバックが、ここまで深い学びにつながるとは思っていませんでした。発表して、フィードバックをもらって、それを深掘りする——このサイクルの価値を改めて実感しています。 参考 golang/go#32591 — syscall/js: performance considerations golang/go#39740 — syscall/js: increase performance of Call, Invoke, and New golang/go#44006 — syscall/js: remove Wrapper type to avoid extreme allocations Go syscall/js ソースコード Go wasm_exec.js ソースコード
はじめに こんにちは。リテールハブ開発部小売アプリチームの池です。 エンジニアチームのマネージャーになってから、気づけば1年半が経ちました。 この1年半を振り返ると、悩みながら行動を続けてきた時間でした。マネージャーとしてどう行動すべきか日々悩みながら試行錯誤し、周りの支援を借りつつ、自分なりにこれだと思うことを試しては失敗を重ね、走り続けてきました。その中で意識していたのは、ただ失敗を繰り返すだけではなく、そこから得られる学びを積み重ねて次に活かすことです。 この記事では、マネージャーが日々何を考え、どんな判断をしているのかを共有したいと思います。また、失敗談が中心にはなりますが、同時にマネージャーという仕事は「人やチームと向き合う仕事」であり、多くの魅力とやりがいがあることも伝えられたらと思っています。マネージャーに興味があるエンジニアの方、同じ立場で悩んでいるマネージャーの方、あるいはマネージャーが何を考えているか知りたいチームメンバーの方にとって、少しでも参考になれば幸いです。 今回は、自身の振り返りも兼ねて、その中でも特に強く心に残っているマネージャーとしての意思決定に関する学びをピックアップして振り返ります。 背景 私がマネージャーになった当時のチーム状況を説明します。 1年半前の当時、私のチームでは以下の業務を並行して進めていました。 自社でゼロから新規開発している小売アプリの開発 事業譲渡で引き継いだ5つの小売アプリの運用保守 エンジニアは私を含めて3名、デザイナー1名、PdM1名の計5名を中心に、上長の支援を得ながらそれらの業務をこなす必要がありました。 引き継いだアプリそれぞれの全容は十分に把握できておらず、わからないことだらけです。コードから仕組みを読み解きながら運用保守、顧客からの問い合わせ・要望に対応する日々でした。チームも発足して間もなく体制が整っておらず、PdMも入れ替わりで着任したばかり。そこに新規開発も並行して進めていて、カオスな状態でした。 そんな中で、私は初めてマネージャーを担うことになりました。 失敗①:チームの方針を明確に示さなかった 一つ目の失敗は、マネージャーとして方針を明確に示すことの重要性と、それがチームに与える影響の大きさを理解していなかったことです。 その当時、少ない人数のチームで新規開発と既存運用が同時に走り、毎日やることが尽きません。私自身もプレイヤーとして動かないと回りませんでした。「まずは足元の開発を回すこと」が最優先で、開発や調整、障害対応と、自身も一人のプレイヤーとして目の前のタスクを一つずつ処理し、なんとか回すことに必死でした。 一方で、私はマネージャーになったばかりです。方針を示すことの大事さを理解していませんでした。どこを目指すのか、何を優先するのか。そういったことを言語化するという発想自体が薄かったのだと思います。 そして、方針を明確に示さずに全てをうまく回そうという意識のまま、大きな対策を打たずに走り続けていました。その結果、チームはどこに向かっていいかわからない状態になっていきました。 重要でないことに時間を使ってしまう 自律的な判断が難しくなる 方針がないと、迷いながら働くことになります。技術負債をどこまで許容するのか、属人化をどこまで受け入れるのか、ドメイン理解にどれだけ時間をかけるのか、作り込みすぎないラインはどこか、各々の判断のズレが積み重なりチームはさらに忙しくなっていきました。 結果的に「全部をそのままやる」ということが暗黙の方針となり、当然ながら、すべてが中途半端になり目標達成も遠のきます。メンバーの不満も溜まり、私自身も時間で解決しようと夜遅くまで働くことが増えました。疲弊するばかりで状況は良くなりません。 方針が全てを解決するわけではないですが、方針を示さなかったからこそ、余計な忙しさを生んでいたのだと思います。 学び 忙しくても、方針を示すことだけは省いてはいけません。 何を最優先にするのか 何を後回しにするのか どこまでやれば十分か このような方針があるだけで、チームは「何に時間を使うべきか」を考えられるようになります。 今は、「やること/やらないこと」を明確にすることを意識し、忙しい時ほど立ち止まるようにしています。 失敗②:チームを見ずに手法を当てはめた 2つ目の失敗は、解決策から入ってしまったことです。 マネージャーに役割が変わると求められるスキルは変わり、人やチームを動かすスキルが必要になります。しかし当時の私は、その変化を十分に受け止めきれていませんでした。マネージャーとしての理解も引き出しもなく、何をすればいいのかわからない。そんな状態です。 わからないなら学ぶしかないと思い、本や記事を読み、過去の自身の成功体験や他社の成功事例に答えを求め、「これが正解だろう」と思ったものをチームに適用しました。 その一つがスクラム開発の導入です。自分の過去の経験からスクラムをやることでチームが良い方向に進むと、どこかで信じていました。マネジメントに自信が持てない中で、実績のある手法を頼ろうとしていたのだと思います。 スクラム自体は良い手法ですが、そのときのチームのフェーズや状況には合っていませんでした。本来私がやるべきだったのはプロセス改善ではなく、チームの課題を見つけてどう解決するかを考えることです。 スクラムをうまく運用できなかったことにも問題はありますが、チームの課題を見ずに形式的に導入しても効果は限定的になります。その結果、重要ではない会議や作業、議論が増えていきました。 たまたまチームの問題とスクラムの手法がマッチしていた箇所では効果が出たものの、全体としては納得感も高まらず、次第に形骸化して空回りしていき、最終的にはスクラムをやめる判断をしました。 失敗の原因は手法そのものではなく、チームを見ていなかったことでした。 学び まずやるべきことは、手法を探すことではなく、チームの状態を観察して明らかにすることでした。 何が一番のボトルネックなのか どこにエネルギーを割くべきなのか メンバーは何に困っているのか それらを言語化した上で解決策を考えるべきでした。 チームはそれぞれ、プロダクトのフェーズや事業状況も、メンバーの性格・スキルも異なります。当然、課題やボトルネックもチームごとに違います。同じ状況のチームは存在しません。だからこそ、マネジメントにどのチームにも当てはまる画一的な手法はありません。 特に、ベンチャー企業の新規事業で限られたリソースと期間で目標を達成する必要がある環境を踏まえると、何を優先し何を捨てるかの判断は大きく変わってきます。そのためにも、まずはチームを観察し、置かれた状況を把握した上で、行動を考える必要がありました。 一方で、考え過ぎて動けなくなるのもまた問題です。すべてを理解してから動くことはできません。実際には、軽く試し、軽く失敗し、そこから学ぶことも多くあります。行動してみて初めて見えてくる課題もあれば、後から納得感がついてくるケースもあります。 今は、観察しながら動き、チームの反応を見て調整することを意識しています。 失敗③:一度決めた方針を続け過ぎてしまった 3つ目の失敗は方針を見直さなかったことです。 失敗①②を経て、私はチームの状況を見て方針や行動の意思決定を意識するようになりました。 たとえば、初期フェーズで作るものがある程度決まっている状況では、ドメイン理解を一定に留めることや属人化を許容するという判断をしました。その時点では合理的な判断だったと思います。 しかし問題は、その方針を見直すべきタイミングで見直さなかったことでした。 プロダクトの状況は変化し、チームの構成も変わり、メンバーも成長しているのにもかかわらず、その判断だけが更新されないままになっていました。 その結果、以下のような影響が徐々に現れてきました。 技術的な判断の拠り所が持てない場面が増えていく 「自分たちが作っているものは本当に価値があるのか」という空気がチームに漂い始める 属人化が固定して急な休みが取りづらくなる アラート対応の担当者が偏る 変化の兆しには気づいていましたが、対策を打てていない自分もいました。方針を決めた後の運用ができていなかったのです。 学び 方針にもメンテナンスが必要です。定期的に見直さないと現実との乖離が大きくなっていきます。 チームが不健全な状態になっていないか チームの熱量や納得感は下がっていないか このような、出ていたはずのシグナルをしっかりと見逃さず、短いサイクルで方針が実状に合っているかを問い続ける必要があります。 方針は一度決めて終わりではなく、状況の変化に合わせて更新し続けるものだと学びました。 おわりに 本記事では、マネージャーとしての意思決定に関する3つの失敗を振り返ってみました。実際にはもっと多くの失敗をしています。 大事なことは、失敗しないことではないと思っています。 マネージャーの仕事に正解はないと、この1年半で実感しました。完璧な判断を下し続けることはできません。それでも、打席に立ち続けることはできます。迷いながらでも決める。うまくいかなければ振り返って次に活かす。その繰り返しで前に進めると思っています。 一方で、失敗ばかりを書いてきましたが、マネージャーの仕事にはそれ以上のやりがいがあると感じています。一人では到底成し遂げられないことをチームで実現できたときの達成感。エンジニアとは異なる視点やスキルが求められる中で、自分自身が成長していく実感。そして何より、メンバーの成長や変化に向き合いながら、チームが前に進んでいく過程を間近で見られることの面白さ。マネージャーの醍醐味だと思っています。 まだまだ未熟ですが、これからも打席に立ち続け、学び続けていければと思います。
はじめに こんにちは。リテールハブ開発部の清水です。 先日 SRE Kaigi 2026 に参加してきまして、私の中でSRE熱がかなり高まっています。 私たちはDatadogをオブザーバビリティ基盤として使用しているのですが、私自身はDatadogをまともに触った経験がありませんでした。 Datadogの画面を開くと左のメニューだけでも大量の項目があって、何ができるのか把握すること自体に大きなハードルを感じていました。 そのような中で、Datadog Learning Centerというものを知りました。無料でハンズオン形式の学習ができるとのことだったので、実際にやってみることにしました。 Datadog Learning Centerとは Datadog Learning Center は、Datadogが公式に提供している無料のオンライン学習プラットフォームです。 ブラウザ上でハンズオン形式でDatadogの各機能を実際に操作して学ぶことができます。 コースはGetting Started、APM、Logs、Kubernetes、Securityなどのカテゴリに分類されていて、初心者向けの入門コースから特定のケースにフォーカスしたコースまで幅広く用意されています。 学習環境について ラボという学習環境が提供されるので、ブラウザさえあれば学習できる形になっていました。 ラボの画面では左半分にターミナル、右半分に学習教材が表示されます。 学習用のDatadogアカウントが自動作成されて、IDとパスワードがターミナル上に表示される仕組みです。 これをDatadogのログイン画面に入力すれば、学習用のアカウントで実際のDatadog環境を自由に触ることができます。 自分の本番環境を壊す心配がないので、気軽に色々試せるのが良いところです。 なお、コンテンツはすべて英語です。英語が苦手な方にとってはややハードルが高いかもしれません。 最初に取り組んだコースについて Datadog Learning Centerの コース一覧画面 を開くと多数のコースがずらりと並んでおり、圧倒されます。 画面上部に LEARNING PATHS というリンクがあるので、ここを開くと目的別に整理された学習順序が紹介されています。 私はこの画面で紹介されている Core Skills Learning Path から始めました。 以下の6コースで構成されており、Datadogの基本的な操作スキルを身につけるための入門パスです。 Datadog Quick Start Tagging Best Practices Getting Started with Metrics Getting Started with Monitors Introduction to Dashboards Getting Started with Notebooks 所要時間 じっくり内容を確認しながら進めて、各コースで30分~1時間程度、合計4時間で終わりました。 学習した感想 Datadogに対する恐怖感が薄れた 一番大きな収穫は、Datadogでざっくりどんなことができるのかわかったことです。 学習前はDatadogの画面を開いても、左メニューに並ぶ項目が何を意味しているのかわからず、触ること自体に抵抗がありました。 学習後は、メトリクス、ダッシュボード、モニター、ログ、APM、トレース、Software Catalog、ノートブックといった用語がそれぞれ何を指しているのか、なんとなく掴めるようになりました。 正直なところ、すべてを完璧に理解できたわけではありません。ただ、「何がわからないかがわからない」状態から「各機能の役割はわかった上で、詳細はこれから深掘りしていけばいい」という状態になれたのは大きな進歩でした。 今回学習したCore Skills Learning Pathはよく使う部分をざっと紹介してもらうような内容となっており、他のコースで実践的な内容を学んでいけるのではないかと思います。 学習用Datadogアカウントの中身を見るだけで勉強になった 学習用Datadogアカウントには、架空のWebサービスを題材として本番さながらの設定やデータが最初から用意されています。 AIに「これはどんな機能ですか?」と会話しながら各画面を見ていくと各種機能について素早く理解が進むと感じました。 特にダッシュボードの作り方がすぐ真似できる要素があって参考になりました。 元々、私たちはテナントごとに別々のダッシュボードを用意していて、それぞれのダッシュボードの中でも各種リソース状況がフラットに並んでいるだけの状態でした。 学習用Datadogアカウントのダッシュボードから以下の内容を真似することにしました: ダッシュボードを全テナント共通にして、テンプレート変数で切り替える 画面上部にはリンク集とMonitor Summaryを設置する 画面要素をGroupウィジェットでグループ分けして表示する 改善前と見比べると、情報の整理や視認性がだいぶ向上したのではないかと思います。同じようにダッシュボードの構成に悩んでいる方の参考になれば幸いです。 おわりに Core Skills Learning Pathは、Datadogを初めて触る人にとって良いスタート地点でした。4時間程度の投資で、Datadogの主要機能の全体像と基本操作を一通り学ぶことができます。 Learning Centerにはこのパス以外にも、さまざまな機能に特化したコースが多数用意されています。今後も引き続き他のコースに取り組み、Datadogの活用の幅を広げていきたいと考えています。 Datadogを使っているけれどちゃんと学んだことがないという方は、ぜひCore Skills Learning Pathから始めてみてはいかがでしょうか。