TECH PLAY

電通総研

電通総研 の技術ブログ

828

こんにちは、事業開発室の里中裕輔です。 本記事では、Engineers GUILDのご紹介と、その5回目のイベントであった Engineers GUILD Vol5 「実装から考えるAIエージェント設計勉強会 ~ メモリ設計とエージェント連携プロトコルの実践 ~」 についてレポートします。 Engineers GUILDとは Engineers GUILD(エンジニアズ・ギルド)は「エンジニアの、エンジニアによる、エンジニアのためのギルド」を掲げる、分野横断型の技術コミュニティです。現場で活躍するエンジニア同士が、副業や越境活動を通じて学び合い、つながり、共創できる場を目指します。 https://engineersguild.peatix.com/ その第5回目として、2026年02月18日(水)にLayerX株式会社と合同で、AIエージェントに関する勉強会を開催しました。 生成AIの実務活用が一気に進む中、「プロダクトにAIエージェントをどう組み込むか」に悩む声が増えています。Deep Researchやコーディングエージェントなど、業務でAIエージェントを使うことは身近になりましたが、自社プロダクトの機能としてエージェントを組み込み、ユーザー価値につなげるとなると、一気に難易度が上がります。 今回の勉強会は、LayerXおよび電通総研のエンジニアが、AIエージェントを実務・プロダクトに組み込む際の設計・実装の観点を話し、参加者の方々と活発に議論できた会になりました。 勉強会の内容としては以下のとおりです。 ・LayerX『澁井雄介』さん: 「Agent Memoryについて」 ・電通総研『袴田時生』さん: 「Agent Payments Protocolで実装するAIエージェント間の取引」 ・懇親会 Agent Memoryについて 澁井さんからは、AIエージェントのメモリに関する講演があり、 プロダクトにエージェントを組み込むにあたって、ステートフルな機能を提供するためのお話がありました。 皆さんは、生成AIやLLM、AIエージェントと聞くと、どういったサービスを思い浮かべるでしょうか。 おそらく、多くの方は ChatGPT や Gemini など、ブラウザのチャットUIで会話をするサービスや、Claude CodeやCodexのように、コーディングを進めるサービスを想像する方がほとんどではないでしょうか。 2026年2月時点において、これらのサービスの多くにはメモリ機能が搭載されています。 そのため、会話においては過去のやり取りを参照しながら、文脈に沿った文章を提供できていますし、コーディングにおいても、プロジェクト全体のコンテキストを理解したうえで、より適切なコードを出力できています。 一方、自社のプロダクトに生成AIやLLM、AIエージェントを組み込む場合、大手のクラウドが提供しているマネージドなAPIを活用することがほとんどですが、 これらのAPIはステートレスな設計になっていることが多く、うまくコンテキストを与えないと、良い出力結果をユーザに提供することが難しくなります。 この課題に対応するため、プロダクト開発者は自分たちでメモリ機能を実装する必要があります。 澁井さんから、 「MAGMA」「SimpleMem」「TAME」という3つのアプローチが紹介されました。 発表スライドはこちらで公開されているので、興味のある方はご覧ください。 https://speakerdeck.com/shibuiwilliam/aiezientonomemorinituite Agent Payments Protocol 袴田さんからは、 Agent Payments Protocolなどを活用したAIエージェント間の取引 について、講演がありました。 昨今、Agentic Commerceという言葉が注目されており、AIエージェントを介したショッピングが今後さらに普及していくと予想されています。一般的には下記のようなシナリオが語られます。 「新しく園芸を趣味として始めたAさん、ベランダでトマトを育て始めましたが、途中で生育不良になってしまいました。困ったAさんはスマートフォンでトマトの様子の写真を撮り、AIエージェントに助けを求めます。AIエージェントからは、『土のpHの値がトマトにあっていないかもしれません。この肥料を与えてみませんか?』との助言が。Aさんはその後もAIエージェントを会話進め、AIエージェントが提示した商品の購入ボタンを押し、肥料を購入しました。」 このように、検索エンジンで検索したり、ECサイトで商品を探したりする手間をかけることなく、 チャットUI上でショッピングが完結するケースが増えていく ことが予想されています。 これらの実現に向けて、技術面では Agent Payments Protocol などの策定が進んでいます。 袴田さんからは、Agent Payments Protocolの解説があり、実際にエージェント同士でウェブショッピングをするデモなどの紹介がありました。 発表スライドはこちらで公開されているので、興味のある方はご覧ください。 https://speakerdeck.com/tokio007/agent-payments-protocoldeshi-zhuang-suruaiezientojian-qu-yin 懇親会 懇親会も多くの方に残っていただき、発表内容に関する質疑や広くAIについてのディスカッションができました。 意外だったのは、参加者の方が多様だったことで、日ごろAIエージェントの開発を行っている方はもちろん、AIの導入を検討しているマネジメント層の方や、デザイナーの方、決済領域で企画を行っている方など、幅広い職種や立場の方々に集まっていただけました。 私自身としても、多様な視点で会話することができ、有意義な懇親会だったと感じております。 まとめ 今回は、Engineers GUILDのご紹介と、その5回目のイベントであった 「実装から考えるAIエージェント設計勉強会 ~ メモリ設計とエージェント連携プロトコルの実践 ~」 についてレポートしました。 私が勉強会を通じて感じたのは、2026年も、生成AIやLLM、AIエージェントに関する技術的な変化は続きそうだという点です。そんな状況の中で、自社プロダクトの機能に生成AIやLLM、AIエージェントを組み込み、継続的にユーザーへ価値を届けることができるように先端技術の情報収集を続けていこうと強く思えました。 LayerXと電通総研は、引き続きこのような勉強会を開催する予定ですので、興味ある方はぜひご参加ください。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 執筆: @satonaka.yusuke レビュー: @handa.kenta ( Shodo で執筆されました )
アバター
金融IT本部(兼XI本部)の上野です。 現在は主にアプリケーションエンジニアの文脈におけるアーキテクトとして日々アーキテクチャ設計/コーディングに勤しんでおります。 今回は、Claude CodeでIaCコードを書いた際の備忘を残しておきたく、ブログに起こすものです。 読み進めるうえで、最初に私のインフラ関連のスキルセットを記載しておきます。 AWSで頻出のサービスと役割くらいは理解している。資格は一個もない。 Dockerについては理解しており、k8sレベルになると怪しい。なんとなくデプロイはできる。 ネスペ二回受けて二回落ちるくらいの低レイヤ勉強量。 コンソールポチポチでデプロイもできる。 IaCは初めて書くがTypeScriptは書けるしアプリ開発でClaude Codeも使っている。 モチベーション 検証 環境 (1)何もガードレールを引かないシンプルなプロンプトで実行 (2)課題を抽出 (3)ガードレールとして設計とプロンプトをチューニング、どの程度解消されたかを確認 出力されたコード (4)構成の比較(As-Is → To-Be) 総評 最初に吐いたコードはどのレベルか? ガードレール後に出てきたコードはどのレベルか? どのレベルであれば使いこなせるか? 工夫するとしたら? 最後に モチベーション 一人のアプリケーションエンジニアとして、2025年11月にOpus 4.5が出たときは衝撃でした。 これまではClaude Code等によりTUIによる操作ができるようになったことで、コーディング速度は爆速になったがプロダクションで使うにはもう一歩という感触がありました。しかしOpus 4.5の登場により、そのアウトプットや計画能力は飛躍したと感じています。実際にアプリケーションの文脈ではプロダクションレベルのコーディングにClaude Codeを採用し、大幅な開発効率の向上を実現しています。 さてそうなってくると、これまでインフラ経験がほぼ0の私が、どの程度IaCコードによりインフラ構築が可能になるのか気になってきました。 Claude Codeが登場した5月頃には、IaCにおいては以下のような課題がありました。これらはすべてIaCは「状態機械」に過ぎないことから生じた課題であり、その結果出てきたコードがそのまま動かないことも頻発しています。 (1)フィードバックループが極端に遅い。 アプリの場合はUnit Test→ログ出力の即時フィードバックの流れですが、IaCではsynth, deployの流れをとおして数分~数10分かかり、エラー表現も曖昧なことがある (2)ログに出ない暗黙知が多すぎる。 AZ制約→Claudeが生成したコードでAZが無指定、AWSがランダム割当、接続不備 ルーティング地獄→private subnetから外に出られない、NAT Gatewayがない IAMの評価順トラブル→AllowよりもDenyが優先されるがDenyの定義を見逃す (3)精度としての問題。 サービス(Stack)定義の依存関係、パラメータの組み合わせが複雑化しやすいため、正しくコードを生成できない (4)最新パラメータへの追従 最新のクラウド機能に追従しようとすると、モデル側が新サービス・パラメータに追従できていないケースが多い。 Opus 4.5では、知識のカットオフ日が伸びたことで最新の知見が増え、かつコンテキストウィンドウも200kトークンとなり、単体のStackを見てインフラ構成を見るのではなく全体の状態を見ながら差分検知できるようになっています。これにより、少ないログからも高い精度が期待できるようにな\ りま、、\ ったはずです。 (参考)Claudeのトレーニングデータはどの程度最新ですか? 検証 さて、いくつか課題を書きましたが、(4)については触れず、既存の学習モデルでどこまで(1)~(3)の課題に対応できるかを検証します。 まずは、 最低限の指示でどの程度やってくれるのか そこからどの程度ガードレールを引くことで使い物になるのか/ならないのか ガードレールを引くためにどの程度のスキルが必要なのか を検証します。これにより、「今のモデルであれば、どの程度インフラ知識をつけておけば(どの程度設計能力があれば)実務で活用できるのか」という指標を明らかにしていきたいと考えています。方針としては、 (1)何もガードレールを引かないシンプルなプロンプトで実行 (2)課題を抽出 (3)ガードレールとして設計とプロンプトをチューニング、どの程度解消されたかを確認 (4)構成の比較(As-Is → To-Be) 今回は、AWS上に構築。ECS、DBはAuroraの超簡単な構成で検証します。 環境 検証自体は2026年1月であり、以下のツール・モデルを利用します。 Claude Code v2.1.21 モデル Opus 4.5 (1)何もガードレールを引かないシンプルなプロンプトで実行 最初のプロンプトは超簡単に以下です。 ECS、Auroraの構成をCDKで作ってください。 いくつかの質問が返ってきます。 CDKのプログラミング言語は何を使いますか? → TypeScript (Recommended) ECSのコンピューティングタイプはどちらですか? → Fargate (Recommended) Auroraのエンジンはどちらを使いますか? → PostgreSQL (Recommended) Auroraのデプロイ方式はどちらですか? → Serverless v2 (Recommended) 出てきたコードを見てみましょう。 import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'; import * as rds from 'aws-cdk-lib/aws-rds'; import { Construct } from 'constructs'; export class CdkTestStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // VPC const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 1, }); // Aurora PostgreSQL Serverless v2 const dbCluster = new rds.DatabaseCluster(this, 'AuroraCluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_4, }), serverlessV2MinCapacity: 0.5, serverlessV2MaxCapacity: 4, writer: rds.ClusterInstance.serverlessV2('Writer'), vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, defaultDatabaseName: 'appdb', removalPolicy: cdk.RemovalPolicy.DESTROY, }); // ECS Cluster const cluster = new ecs.Cluster(this, 'EcsCluster', { vpc, }); // ECS Fargate Service with ALB const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService( this, 'FargateService', { cluster, taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, cpu: 256, memoryLimitMiB: 512, desiredCount: 1, taskImageOptions: { image: ecs.ContainerImage.fromRegistry('nginx:latest'), containerPort: 80, environment: { DB_HOST: dbCluster.clusterEndpoint.hostname, DB_PORT: dbCluster.clusterEndpoint.port.toString(), DB_NAME: 'appdb', }, secrets: { DB_SECRET: ecs.Secret.fromSecretsManager(dbCluster.secret!), }, }, publicLoadBalancer: true, }, ); // Allow ECS tasks to connect to Aurora dbCluster.connections.allowDefaultPortFrom(fargateService.service); // Outputs new cdk.CfnOutput(this, 'AlbDnsName', { value: fargateService.loadBalancer.loadBalancerDnsName, description: 'ALB DNS Name', }); new cdk.CfnOutput(this, 'DbClusterEndpoint', { value: dbCluster.clusterEndpoint.hostname, description: 'Aurora Cluster Endpoint', }); } } 作成されるリソースは以下の通りです。 リソース 詳細 VPC 2 AZ, Public/Private サブネット、NAT Gateway×1 Aurora PostgreSQL Serverless v2, Privateに配置 ECS Fargate ALB付き Security Group ECS→Aurora(PostgreSQL 5432のみ許可) Secrets Manager Aurora認証情報を自動生成、ECSタスクにDB_SECRETとして付与 (2)課題を抽出 上記コードによる課題は以下です。 こちらは自分で洗いだしたものを含め、最終的にChatGPT5.2(Codex)にレビューしてもらいながら全量を記載しています。普段からコーディングエージェントが出力したコードのレビューにはCodexを利用しており、生成元とは別ベンダーのモデルに専門家の立場でレビューさせることで、同一モデルのバイアスを避ける狙いがあります。 No 課題 課題分類 優先度 詳細 1 Aurora が RemovalPolicy.DESTROY 運用 / セキュリティ must スタック削除や置換で DBが削除 され得ます。本番は RETAIN を基本に、削除保護も有効化すべきです。 2 Aurora の削除保護(DeletionProtection)が未設定 運用 / セキュリティ must オペミス( cdk destroy 等)や誤置換で消えるリスクが残ります。 3 バックアップ保持(backup retention)等のデータ保護が薄い 運用 must Aurora 側のバックアップ保持日数・復旧設計を明示していません。最低でも保持期間を要件化し、復旧手順を想定すべきです。 4 NAT Gateway が 1 台(2AZでも単一NAT) 可用性 / コスト must 2AZ 構成でも NAT が単一だと NAT配置AZ障害時に private 側の外向き通信が破綻 しやすいです。加えてクロスAZ経由コストが増えがち。 5 ALB が publicLoadBalancer: true 固定 セキュリティ / 運用 must 無条件でインターネット公開前提になります。用途要件(社内向け/internal か公開か)を決めて選ぶべきです。 6 HTTPS 設定がない(証明書/HTTP→HTTPSリダイレクト) セキュリティ must 現状は HTTP のまま公開になりがち。ACM 証明書の設定と 443 終端、80→443 リダイレクトを入れるのが基本です。 7 Secrets の渡し方が「secret全体」を1変数に入れている 保守性 / セキュリティ recommend DB_SECRET に JSON 全体が入る形になり、アプリ側が解釈依存で壊れやすい。 fromSecretsManager(secret, 'password') のようにキー指定して渡す方が堅牢です。 8 dbCluster.secret! の non-null assertion 保守性 recommend 将来の変更や条件分岐で secret がない構成に寄ると実行時に破綻します。存在前提をコードで担保するか、生成条件を明示すべきです。 9 DB への接続情報を env に直入れ(host/port/name) セキュリティ / 保守性 recommend 機微度は低いが、環境差分や変更に弱い。アプリ設定として一元管理(SSM/Secrets/Config)や接続文字列化などを検討。 10 監視/ログ設計がほぼない(ALBアクセスログ、ECSログ、アラーム等) 運用 recommend デプロイ後に障害対応できない構成になりがち。最低限 CloudWatch Logs、主要メトリクス(CPU/Memory/ALB 5xx/TargetResponseTime)アラームを用意。 11 スタック分割がない(Network/App/Db が単一Stack) 保守性 / 運用 recommend 差分デプロイ・権限分離・変更管理が難しくなります。環境が育つほど辛いです。 12 VPC Endpoint を使わず NAT 依存(ECR/Logs/Secrets 等) コスト / 可用性 recommend NAT はコスト増・単一点化になりやすい。ECR/CloudWatch Logs/Secrets Manager などは VPC Endpoint 併用でコストと可用性を改善できます。 13 SG 設計が最小限(ECS→DBの許可のみで、方針がコード化されていない) セキュリティ nits 今は動くが、将来拡張でルールが散逸しがち。インバウンド/アウトバウンド方針やポート設計をパターン化したい。 14 DB ユーザー/ローテーション方針がコード外 セキュリティ / 運用 nits どの認証方式で運用するか(Secrets rotation、IAM auth、踏み台/SSM 経由など)が未定義。要件として別途決める領域。 特に問題なのは以下ですね。 No5: ALB が publicLoadBalancer: true 固定 No6: HTTPS 設定がない(証明書/HTTP→HTTPSリダイレクト) もともとアプリケーションエンジニアとしては、以下も気になるところです。 No10: 監視/ログ設計がほぼない(ALBアクセスログ、ECSログ、アラーム等) No11: スタック分割がない(Network/App/Db が単一Stack) (3)ガードレールとして設計とプロンプトをチューニング、どの程度解消されたかを確認 出てきた課題点をガードレールとして敷けるよう、プロンプトをチューニングします。 あなたはAWS CDK(TypeScript)を用いて本番運用前提の ECS(Fargate) + Aurora PostgreSQL(Serverless v2) 構成を実装するクラウドアーキテクトです。 doc/require-infra.mdに従って、ドキュメントの内容を漏らさず、実運用に耐える構成で作ってください。 与えるドキュメントは以下のとおりです。 # 共通前提 - CDK: aws-cdk-lib v2 - 言語: TypeScript - AZ: 2AZ - 環境: prod のみ - 出力構成: - lib/network-stack.ts - lib/db-stack.ts - lib/app-stack.ts - bin/app.ts - README.md(デプロイ方法と必須パラメータ) # スタック分割 以下の 3 Stack に分離すること: ## NetworkStack - VPC (2AZ) - public / private subnet - NAT Gateway: AZごとに1台(natGateways: 2) - VPC Endpoint: - ECR(api, dkr) - CloudWatch Logs - Secrets Manager - SSM / SSMMessages / EC2Messages - 共通タグ(Name / Env=prod) ## DbStack - Aurora PostgreSQL Serverless v2 - private subnet のみ - Secrets Manager(DB認証情報) - DB用 SecurityGroup ## AppStack - ECS Cluster - Fargate Service + ALB - ECS用 SecurityGroup - HTTPS Listener - CloudWatch Logs / Alarm # Aurora 要件 - removalPolicy = RETAIN - deletionProtection = true - backup retention = 7 days - DB は public subnet に置かない - Secrets Manager を必ず作成 - dbCluster.secret! の non-null assertion は使用禁止 - CloudWatch Logs export(postgresql)を有効化 # ネットワーク - ECS / Aurora は private subnet - NAT Gateway は 2 台 - ECS が以下に到達できること: - ECR - CloudWatch Logs - Secrets Manager - SSM # ALB / HTTPS - ALB は public(internet-facing) - ACM Certificate ARN は props で受け取る - Listener: - 443 HTTPS - 80 → 443 redirect - SecurityGroup: - ALB: inbound 443 のみ(IP制御はWAFで行う) - ECS: ALB SG から app port のみ - DB: ECS SG から 5432 のみ # WAF - AWS WAFv2 を ALB に関連付ける - 許可する送信元 IP は allowlist 方式とする(それ以外はブロック) - IP set(IPv4)を作成し、CIDR のリストを props で受け取れるようにする - 例:["203.0.113.10/32","198.51.100.0/24"] - WebACL のルール優先度: 1) Allow: 指定 IP set に一致 2) Default action: Block - visibilityConfig を有効化し、WAF のメトリクス/サンプリングログを有効化する # Secrets / 接続情報 - Secrets は JSONキー単位で ECS に渡す: - DB_USER - DB_PASSWORD - DB_HOST / DB_PORT / DB_NAME は Parameter 化 - Secret 全体を1変数に渡さない # Secrets / 接続情報 - Secrets は JSONキー単位で ECS に渡す: - DB_USER - DB_PASSWORD - DB_HOST / DB_PORT / DB_NAME は Parameter 化 - Secret 全体を1変数に渡さない # 監視 / ログ - ECS Container Logs → CloudWatch Logs - Log retention = 30 days - CloudWatch Alarm: - ALB 5xx - ECS CPU / Memory # 保守性 - Network → Db → App の依存方向を守る - VPC は NetworkStack から export - DB endpoint / secret は DbStack から export # SG設計 - SGは明示的に作成 - allowDefaultPortFrom のみは禁止 - どのSG→どのSGか分かるようルールを書く # コンテナ - nginx:latest のような latest タグは禁止 - 固定タグ or digest を使用 # 出力期待 - 各 Stack の責務が明確 - prod 固定構成 - README に以下を記載: - デプロイ手順 - 必須パラメータ(ACM ARN 等) 出力されたコード lib/network-stack.ts import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; export class NetworkStack extends cdk.Stack { public readonly vpc: ec2.IVpc; constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ---------------------------------------------------------------- // VPC – 2AZ / public + private / NAT Gateway x2 // ---------------------------------------------------------------- const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 2, subnetConfiguration: [ { cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC, }, { cidrMask: 24, name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, ], }); // ---------------------------------------------------------------- // VPC Endpoints – Interface // ---------------------------------------------------------------- // ECR API vpc.addInterfaceEndpoint('EcrApiEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.ECR, }); // ECR Docker (dkr) vpc.addInterfaceEndpoint('EcrDkrEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, }); // CloudWatch Logs vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, }); // Secrets Manager vpc.addInterfaceEndpoint('SecretsManagerEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, }); // SSM vpc.addInterfaceEndpoint('SsmEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SSM, }); // SSM Messages vpc.addInterfaceEndpoint('SsmMessagesEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES, }); // EC2 Messages vpc.addInterfaceEndpoint('Ec2MessagesEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES, }); // S3 Gateway (ECR イメージレイヤー取得に必要) vpc.addGatewayEndpoint('S3Endpoint', { service: ec2.GatewayVpcEndpointAwsService.S3, }); // ---------------------------------------------------------------- // 共通タグ // ---------------------------------------------------------------- cdk.Tags.of(this).add('Env', 'prod'); this.vpc = vpc; } } lib/app-stack.ts import * as cdk from 'aws-cdk-lib'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as rds from 'aws-cdk-lib/aws-rds'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as wafv2 from 'aws-cdk-lib/aws-wafv2'; import { Construct } from 'constructs'; export interface AppStackProps extends cdk.StackProps { readonly vpc: ec2.IVpc; readonly dbCluster: rds.DatabaseCluster; readonly dbSecurityGroup: ec2.ISecurityGroup; readonly dbSecret: secretsmanager.ISecret; readonly certificateArn: string; readonly allowedCidrs: string[]; } export class AppStack extends cdk.Stack { constructor(scope: Construct, id: string, props: AppStackProps) { super(scope, id, props); const { vpc, dbCluster, dbSecurityGroup, dbSecret, certificateArn, allowedCidrs } = props; const appPort = 8080; // ---------------------------------------------------------------- // ACM Certificate (ARN から参照) // ---------------------------------------------------------------- const certificate = acm.Certificate.fromCertificateArn( this, 'Certificate', certificateArn, ); // ---------------------------------------------------------------- // Security Groups // ---------------------------------------------------------------- // ALB SG – inbound 443 のみ const albSg = new ec2.SecurityGroup(this, 'AlbSecurityGroup', { vpc, description: 'Security group for Application Load Balancer', allowAllOutbound: true, }); albSg.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS inbound', ); // ECS SG – ALB SG から app port のみ const ecsSg = new ec2.SecurityGroup(this, 'EcsSecurityGroup', { vpc, description: 'Security group for ECS Fargate tasks', allowAllOutbound: true, }); ecsSg.addIngressRule( albSg, ec2.Port.tcp(appPort), 'Allow traffic from ALB on app port', ); // DB SG – ECS SG から 5432 のみ // 循環依存を回避するため CfnSecurityGroupIngress を直接作成 new ec2.CfnSecurityGroupIngress(this, 'DbFromEcsIngress', { ipProtocol: 'tcp', fromPort: 5432, toPort: 5432, groupId: dbSecurityGroup.securityGroupId, sourceSecurityGroupId: ecsSg.securityGroupId, description: 'Allow PostgreSQL access from ECS tasks', }); // ---------------------------------------------------------------- // ECS Cluster // ---------------------------------------------------------------- const cluster = new ecs.Cluster(this, 'EcsCluster', { vpc, containerInsightsV2: ecs.ContainerInsights.ENABLED, }); // ---------------------------------------------------------------- // Task Definition // ---------------------------------------------------------------- const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', { cpu: 512, memoryLimitMiB: 1024, }); const logGroup = new logs.LogGroup(this, 'AppLogGroup', { retention: logs.RetentionDays.ONE_MONTH, removalPolicy: cdk.RemovalPolicy.DESTROY, }); const container = taskDef.addContainer('app', { image: ecs.ContainerImage.fromRegistry('nginx:1.27-alpine'), logging: ecs.LogDrivers.awsLogs({ logGroup, streamPrefix: 'app', }), environment: { DB_HOST: dbCluster.clusterEndpoint.hostname, DB_PORT: dbCluster.clusterEndpoint.port.toString(), DB_NAME: 'appdb', }, secrets: { DB_USER: ecs.Secret.fromSecretsManager(dbSecret, 'username'), DB_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, 'password'), }, healthCheck: { command: ['CMD-SHELL', `curl -f http://localhost:${appPort}/ || exit 1`], interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), retries: 3, startPeriod: cdk.Duration.seconds(60), }, }); container.addPortMappings({ containerPort: appPort, protocol: ecs.Protocol.TCP, }); // ---------------------------------------------------------------- // Fargate Service // ---------------------------------------------------------------- const service = new ecs.FargateService(this, 'FargateService', { cluster, taskDefinition: taskDef, desiredCount: 2, securityGroups: [ecsSg], vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, minHealthyPercent: 100, maxHealthyPercent: 200, enableExecuteCommand: true, }); // ---------------------------------------------------------------- // ALB – public (IP 制御は WAF で実施) // ---------------------------------------------------------------- const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', { vpc, internetFacing: true, securityGroup: albSg, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); // HTTPS Listener (443) const httpsListener = alb.addListener('HttpsListener', { port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, certificates: [certificate], }); httpsListener.addTargets('EcsTarget', { port: appPort, protocol: elbv2.ApplicationProtocol.HTTP, targets: [service], healthCheck: { path: '/', healthyHttpCodes: '200', interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), healthyThresholdCount: 2, unhealthyThresholdCount: 3, }, }); // HTTP → HTTPS redirect (80 → 443) alb.addListener('HttpRedirectListener', { port: 80, protocol: elbv2.ApplicationProtocol.HTTP, defaultAction: elbv2.ListenerAction.redirect({ protocol: 'HTTPS', port: '443', permanent: true, }), }); // ---------------------------------------------------------------- // WAFv2 – IP allowlist // ---------------------------------------------------------------- const ipSet = new wafv2.CfnIPSet(this, 'AllowedIpSet', { scope: 'REGIONAL', ipAddressVersion: 'IPV4', addresses: allowedCidrs, description: 'Allowed source IP addresses', }); const webAcl = new wafv2.CfnWebACL(this, 'WebAcl', { scope: 'REGIONAL', defaultAction: { block: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: 'ProdWebAcl', sampledRequestsEnabled: true, }, rules: [ { name: 'AllowFromIpSet', priority: 1, action: { allow: {} }, statement: { ipSetReferenceStatement: { arn: ipSet.attrArn, }, }, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: 'AllowFromIpSet', sampledRequestsEnabled: true, }, }, ], }); new wafv2.CfnWebACLAssociation(this, 'WebAclAssociation', { resourceArn: alb.loadBalancerArn, webAclArn: webAcl.attrArn, }); // ---------------------------------------------------------------- // CloudWatch Alarms // ---------------------------------------------------------------- // ALB 5xx new cloudwatch.Alarm(this, 'Alb5xxAlarm', { metric: alb.metrics.httpCodeElb(elbv2.HttpCodeElb.ELB_5XX_COUNT, { period: cdk.Duration.minutes(5), statistic: 'Sum', }), threshold: 10, evaluationPeriods: 2, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, alarmDescription: 'ALB 5xx errors exceeded threshold', treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, }); // ECS CPU Utilization new cloudwatch.Alarm(this, 'EcsCpuAlarm', { metric: service.metricCpuUtilization({ period: cdk.Duration.minutes(5), statistic: 'Average', }), threshold: 80, evaluationPeriods: 3, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, alarmDescription: 'ECS CPU utilization exceeded 80%', treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, }); // ECS Memory Utilization new cloudwatch.Alarm(this, 'EcsMemoryAlarm', { metric: service.metricMemoryUtilization({ period: cdk.Duration.minutes(5), statistic: 'Average', }), threshold: 80, evaluationPeriods: 3, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, alarmDescription: 'ECS memory utilization exceeded 80%', treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, }); // ---------------------------------------------------------------- // 共通タグ // ---------------------------------------------------------------- cdk.Tags.of(this).add('Env', 'prod'); // ---------------------------------------------------------------- // Outputs // ---------------------------------------------------------------- new cdk.CfnOutput(this, 'AlbDnsName', { value: alb.loadBalancerDnsName, description: 'ALB DNS Name', }); new cdk.CfnOutput(this, 'AlbArn', { value: alb.loadBalancerArn, description: 'ALB ARN', }); new cdk.CfnOutput(this, 'EcsClusterName', { value: cluster.clusterName, description: 'ECS Cluster Name', }); new cdk.CfnOutput(this, 'EcsServiceName', { value: service.serviceName, description: 'ECS Service Name', }); } } lib/db-stack.ts import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as rds from 'aws-cdk-lib/aws-rds'; import { Construct } from 'constructs'; export interface DbStackProps extends cdk.StackProps { readonly vpc: ec2.IVpc; } export class DbStack extends cdk.Stack { public readonly dbCluster: rds.DatabaseCluster; public readonly dbSecurityGroup: ec2.ISecurityGroup; public readonly dbSecret: cdk.aws_secretsmanager.ISecret; constructor(scope: Construct, id: string, props: DbStackProps) { super(scope, id, props); const { vpc } = props; // ---------------------------------------------------------------- // Security Group – DB // ---------------------------------------------------------------- const dbSg = new ec2.SecurityGroup(this, 'DbSecurityGroup', { vpc, description: 'Security group for Aurora PostgreSQL cluster', allowAllOutbound: false, }); // ---------------------------------------------------------------- // Aurora PostgreSQL Serverless v2 // ---------------------------------------------------------------- const cluster = new rds.DatabaseCluster(this, 'AuroraCluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_4, }), serverlessV2MinCapacity: 0.5, serverlessV2MaxCapacity: 4, writer: rds.ClusterInstance.serverlessV2('Writer'), readers: [ rds.ClusterInstance.serverlessV2('Reader', { scaleWithWriter: true, }), ], vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, securityGroups: [dbSg], defaultDatabaseName: 'appdb', credentials: rds.Credentials.fromGeneratedSecret('dbadmin'), backup: { retention: cdk.Duration.days(7), }, deletionProtection: true, removalPolicy: cdk.RemovalPolicy.RETAIN, cloudwatchLogsExports: ['postgresql'], cloudwatchLogsRetention: logs.RetentionDays.ONE_MONTH, storageEncrypted: true, }); // Secret の安全な取得(non-null assertion 禁止) const secret = cluster.secret; if (!secret) { throw new Error('Aurora cluster secret was not created'); } // ---------------------------------------------------------------- // 共通タグ // ---------------------------------------------------------------- cdk.Tags.of(this).add('Env', 'prod'); // ---------------------------------------------------------------- // Export // ---------------------------------------------------------------- this.dbCluster = cluster; this.dbSecurityGroup = dbSg; this.dbSecret = secret; } } (4)構成の比較(As-Is → To-Be) ガードレール前後でコード構成がどう変わったかを比較します。 As-Is:ガードレールなし(単一 Stack) CdkTestStack(1ファイルに全部入り) ├─ VPC(NAT×1, Endpoint なし) ├─ Aurora(DESTROY, 削除保護なし, バックアップ未設定) ├─ ECS Fargate │ ├─ ALB(HTTP のみ, publicLoadBalancer: true) │ ├─ Secret 全体を 1 変数で渡す │ └─ nginx:latest ├─ SG: allowDefaultPortFrom のみ └─ 監視なし, WAF なし To-Be:ガードレールあり(3 Stack 分離) NetworkStack └─ VPC(2AZ, NAT×2, VPC Endpoints 8種) ↓ vpc を props で渡す DbStack ├─ Aurora Serverless v2(RETAIN, 削除保護, backup 7日, 暗号化) ├─ DB SecurityGroup(outbound も制限) └─ Secret(fromGeneratedSecret → 存在チェック付きで取得) ↓ dbCluster / dbSecurityGroup / dbSecret を props で渡す AppStack ├─ ECS (Fargate) + ALB (HTTPS 443 + 80→443 redirect) ├─ WAFv2(IP allowlist, デフォルト Block) ├─ SG チェーン: ALB(:443) → ECS(:8080) → DB(:5432) ├─ Secrets は JSONキー単位で渡す(DB_USER / DB_PASSWORD) ├─ CloudWatch Logs(30日保持) └─ CloudWatch Alarms(ALB 5xx / ECS CPU / ECS Memory) 主な差分をまとめると以下です。 観点 As-Is To-Be Stack 分割 1 Stack に全部入り Network / Db / App の 3 分割 NAT Gateway 1 台(AZ 障害で Private 通信断) 2 台(AZ ごと) VPC Endpoint なし(全通信が NAT 経由) ECR / Logs / Secrets / SSM 等 8 種 Aurora 削除保護 DESTROY + 保護なし RETAIN + deletionProtection + backup 7日 HTTPS なし(HTTP 公開) ACM 証明書 + 443 終端 + 80→443 redirect WAF なし IP allowlist(デフォルト Block) SG 設計 allowDefaultPortFrom のみ 明示的に 3 SG を作成しチェーン接続 Secrets の渡し方 Secret 全体を 1 変数 username / password をキー単位で分離 コンテナタグ nginx:latest nginx:1.27-alpine (固定タグ) 監視 なし CloudWatch Logs + Alarms(5xx / CPU / Memory) 外部パラメータ ハードコード certificateArn / allowedCidrs をコンテキスト変数で注入 No 残っている課題 課題分類 優先度 詳細 1 コンテナ(nginx)と appPort=8080 、ヘルスチェックが不整合 可用性 / 運用 must nginx デフォルトは 80。現状の curl http://localhost:8080/ と ALB ターゲット(8080)が成立せず、 タスクがunhealthyになり続ける 。 appPort=80 にそろえるか、nginx の listen を 8080 に変更する。 2 LogGroup が RemovalPolicy.DESTROY 運用 / セキュリティ recommend prod 固定構成でログを DESTROY は事故時の調査・監査に弱い。 RETAIN 推奨 (retention 30日設定は良い)。 3 ALB SG の inbound が anyIpv4(WAF前提でも “ネットワーク境界” として緩い) セキュリティ recommend WAF allowlist で制御する方針はOKだが、SG が 0.0.0.0/0 だと WAF無効化・誤設定・関連付け漏れの際に即全開放になりやすい。 4 NAT×2 と Endpoint 多数が併存(コスト最適化方針が未決) コスト / 運用 recommend 目的に対して二重投資になりがち。 「NATを減らしてEndpointで寄せる」or「Endpointを絞ってNATで寄せる」 の方針を決めたい(ECR/Logs/Secrets/SSMは残す、など)。 5 CfnSecurityGroupIngress 採用理由(循環依存回避)が不透明 保守性 nits 必要性が明確でないと保守時に混乱しやすい。通常の dbSg.addIngressRule(ecsSg, ...) で成立するなら統一、成立しないなら なぜ循環するか をコメントで残す。 6 DB接続情報の Parameter 化が未実装(要件はあるがコード反映が薄い) 保守性 / 運用 nits 現状 DB_HOST/PORT/NAME が直書き寄り。要件通りなら SSM Parameter Store などで管理 し、環境差分・変更容易性を上げる。 7 ECS Exec の運用前提(IAM/監査/利用制御)が未定義 セキュリティ / 運用 nits enableExecuteCommand: true は良いが、 誰が・いつ・どう許可 するか(IAM条件、CloudTrail監査、手順)を設計に落とすと本番運用で揉めにくい。 8 オートスケール戦略が未定義(固定 desiredCount=2) 可用性 / コスト nits 最小構成としては可だが、本番前提なら CPU/Memory/ALB 指標で AutoScaling を検討したい(スパイク耐性・コスト最適化)。 9 ALB アクセスログ(S3)など監査ログが未実装 運用 / セキュリティ nits 監査や障害解析の観点で、要件次第では ALB access log を有効化したい(個人情報や保管ポリシー含めて要検討)。 10 ACM 証明書を参照のみで作成しておらず、更新ライフサイクルが IaC 管理外 運用 / セキュリティ recommend fromCertificateArn で既存証明書を参照しているだけで、証明書の発行・更新が CDK 管理外。外部 CA からのインポート証明書だった場合、ACM は自動更新しないため有効期限切れの運用事故リスクがある。 11 ドメイン・DNS(Route 53)が構成に含まれておらず、IaC 管理範囲が未定義 運用 / 設計 recommend Route 53 Hosted Zone、ALB への Alias レコード、ACM DNS 検証用 CNAME が未定義。ドメインが IaC の外にあるため証明書も CDK で作成できていない。どこまでを IaC で管理するかの方針決定が必要。 12 Aurora のバージョン管理方針が未定義、かつ本番で Serverless v2(minCapacity 0.5)の妥当性が未検討 可用性 / コスト recommend メジャーバージョンのアップグレード戦略(16→17等)が未定義。また Serverless v2 の minCapacity 0.5 ACU は低トラフィック時のコールドスタートやコスト予測の不安定さがあり、本番では Provisioned or minCapacity 引き上げの検討が要る。 (当然ですが)must, recommendが減りました。デプロイするアプリの特性に依存するものを除くと、プロダクションに持っていくには残課題として以下の修正が必要となりそうです。 ALB/App/DBそれぞれをどのサブネットに配置するかを明確に。 PRIVATE_WITH_EGRESS ではなく少なくともALBは PRIVATE_ISOLATED を利用する。 ALBがNAT付きサブネットに置く理由がない。 LogGroupは RemovalPolicy.DESTROY ではなく RETAIN としたい。 SGでもallowlistのCIDRに絞ってWAFと二重防御。 ACMをpropで受け取ることは明記していたものの、ACMを発行するスタックを明示的に指定していなかったことで証明書が正しく機能しないため、明示的にACMを作成するスタックを作成。 くらいでしょうか。 総評 さて、今回出てきたコードの評価と、どういったスキルの人が使いこなせるか、といった観点でまとめます。 まずはインフラエンジニアについて以下のようにレベルを定義します。 ジュニアレベル “動く構成” を素早く組める(VPC/ECS/RDSをつなぐ、疎通させる) ただし 本番の安全要件(削除耐性・HTTPS・監査/運用・境界設計)をデフォルトで落としがち 生成物は「PoC/デモ品質」になりやすい ミドルレベル 本番のガードレール(RETAIN、DeletionProtection、HTTPS、SG分離、ログ/監視、VPC Endpoint 等) を意識して設計に落とせる ただし細部(ポート/ヘルスチェック整合、サブネットの置き方、WAF/SGの多層防御、運用フロー)で穴が残りやすい 「レビュー前提で本番候補」まで持っていける シニアレベル 設計意図・運用・変更耐性(将来の要件変更/誤変更/監査対応)まで含めて、壊れにくいCDKにできる トレードオフ(可用性/コスト/セキュリティ)を前提から言語化し、CDKへ反映できる “動く”だけでなく 事故らない/継続運用できる をデフォルトにできる 最初に吐いたコードはどのレベルか? ジュニアレベルです。PoCの品質としても危ういレベル。象徴的な理由としては、 DBが RemovalPolicy.DESTROY HTTPS通信がないかつパブリック全開放 Secretsの使い方が雑 NAT、可用性、ログ監視等が未定義 ガードレール後に出てきたコードはどのレベルか? ミドルレベルであり、レビュー前提で本番候補。 良い点としては、 Aurora:RETAIN + deletionProtection + backup + logs export 3スタック分割(Network/Db/App)で保守性が上がった HTTPS終端、80→443 redirect VPC Endpoints/SSM系も入り、運用導線(ECS Exec)が成立しやすい WAF allowlist を実装(入口制御をコード化) レビューでシニアレベルの人に弾いていただく必要がある点としては、まだ課題があります。 nginxなのに appPort=8080、ヘルスチェックの不整合 LogGroupがDESTROY(本番環境としては弱い) public ALB + SG anyIpv4 で “WAF依存が強い”(多層防御が薄い) NAT×2 + Endpoint大量の“方針”が曖昧(コスト設計が残る) どのレベルであれば使いこなせるか? 結論として、CDKを読めるシニアまたはミドルでも上位クラスの人が設計者兼レビュアとして使うと、本番環境に持っていけるかなという感触です。 アプリケーション同様に、細部を理解できる人であればIaCコードをClaude Codeで構築することは可能になってきたなと感じました。 工夫するとしたら? Security HubやAWS Config、またprowlerなどのセキュリティ・コンプライアンスチェックの仕組みを利用することでフィードバックループを回す AWS公式のMCP経由で最新ドキュメントを参照させる といったことを加味することで、もう少し実用的なシステムに仕上げることができるでしょう。 最後に 私のようなインフラ経験がない人間でも、このレベルの構成に持っていけるのは素直に感動しました。 もちろん日常から常にAWS環境を触っている人からしたら「当然じゃん」もしくは「穴だらけ」と思われるかもしれませんが、一番感動したのは「勉強速度が上がった」点です。今回の構成に限った話であれば、ものの数時間でインフラジュニアエンジニアと名乗れるくらいにはなったかなと。 これはAIが出始めてアプリケーション開発にも言われていることですが、「理解負債」を完済し続けることでAIと共存しながら成長し続けられるのはインフラについても同じだと実感しました。今後は積極的にIaCにもClaude Codeを利用していきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kamino.shunichiro レビュー: @ozaki.hisanori ( Shodo で執筆されました )
アバター
こんにちは! HCM本部 HCMソリューション企画開発部の佐藤です。 本日は、先月5日に Opus 4.6 と同時に公開された新機能 Agent Teams を取り上げます。 公開と同日に、 16エージェントが協働してCコンパイラを一から実装したというコンセプト実証のブログ記事 も公開され、大きな反響を呼びました。 本稿では、そんな Agent Teams の要点を改めて押さえつつ、ドキュメントには明記されていないチームメンバー間のやり取りの実態を観察してみたいと思います。 Agent Teams の要点 Subagents との違い 適したユースケース 高品質なテストが鍵 コミュニケーションの実態 実際の Agent Teams セッションを観察する 役割を伝達するメッセージ 自律性の先に見える"人間"らしさ 2種類のメッセージ まとめ 参考サイト Agent Teams の要点 Agent Teams について解説した記事はすでに数多く公開されており、 公式ドキュメント も非常に分かりやすいため、本稿では筆者が重要だと考えるポイントのみを説明します。 Subagents との違い Claude Code には、以前から Subagents という似た機能が存在しています。 独立したコンテキストで作業を並列に進められる点は共通していますが、Subagents はあくまで結果をメインに返すだけで、エージェント同士が直接やり取りすることはできません。 一方、Agent Teams では、メンバーが互いに 直接コミュニケーションをとり、議論し、協働できる 点が大きく異なります。 この違いの重要性は、私たち人間に当てはめて考えてみると分かりやすいでしょう。 こんな経験はないでしょうか。抽象度の高い複雑なタスクがアサインされ、一人だけで考え続けた結果、気付かぬうちに思考が一辺倒になり袋小路に迷い込んでいた... 悩んだ末、チームメンバーに相談したところ、「ああ、たしかに」と腑に落ちるような気づきが、対話を通して嘘のようにあっさりと降りてきた。 Agent Teams は、まさにこうした多角的な視点による相互補正の力をAIに与える機能だと言えそうです。 適したユースケース SubAgent との違いから、Agent Teams の適したユースケースが導かれます。 公式ドキュメントには 並列コードレビュー 競合する仮説による調査 の二例が掲載されていますが、筆者としては後者がより適しているように考えます。 公式ドキュメントにおける後者のプロンプト例では Have them talk to each other to try to disprove each other's theories, like a scientific debate. [3] というように、互いの意見を反証することを強制しています。 これはまさに、メンバー間の直接通信という Agent Teams ならではの強みを活かした指示です。 反証に始まる議論を通じて盲点があぶり出され、そのプロセスを経て最終的に導かれる結論は、単独のエージェントによるものよりも信頼性が高いと考えられます。 Agent Teams の真価が発揮されるのは、このようなメンバー同士の対話そのものが成果の質を左右するケースだと言えるでしょう。 高品質なテストが鍵 Agent Teams の振る舞いは SubAgent を使うケースと比べてより有機的であり、より Agentic ― すなわち自律的なものになります。だからこそ、エージェントたちが正しい方向に進んでいることを担保する仕組みが欠かせません。コンセプト実証のブログ記事でも、著者はこの点を強調しています。 Claude will work autonomously to solve whatever problem I give it. So it’s important that the task verifier is nearly perfect, otherwise Claude will solve the wrong problem. [...] For example, near the end of the project, Claude started to frequently break existing functionality each time it implemented a new feature. To address this, I built a continuous integration pipeline and implemented stricter enforcement that allowed Claude to better test its work so that new commits can’t break existing code. [2] 高品質なテストを設計し、それをワークフローの決定的なステップとして組み込んだテストハーネスの構築が、Agent Teams を活用するうえでの鍵となります。 コミュニケーションの実態 Agent Teams の各メンバーの稼働状況は、ターミナル上でもリアルタイムに確認できます。しかし、表示されるのはあくまで作業の概要であり、メンバー間で交わされるやり取りのすべてが見えるわけではありません。 では、メンバー間の相互コミュニケーションは、実際にはどこで、どのように行われているのでしょうか。 その実体は ~/.claude/teams/{team-id}/inboxes/ ディレクトリにあります。 ここには、以下のような各メンバー名に対応する JSON ファイルが並んでいます。 ~/.claude/teams/{team-id}/inboxes/ ├── team-lead.json ├── {member-1}.json ├── {member-2}.json └── {member-3}.json これらはいわば、各メンバーの「メール受信箱」です。ファイルの中身は、以下のようなオブジェクト配列になっています。 [ { " from ": " {送信元メンバー名} ", " text ": " {メッセージ本文} ", " summary ": " {メッセージの要約} ", " timestamp ": " {ISO 8601 形式のタイムスタンプ} ", " color ": " {メンバーに割り当てられた色} ", " read ": " {既読フラグ(true/false)} " } ] つまり、この JSON をリアルタイムで監視すれば、メンバー間の正確なコミュニケーションの流れが見えてきます。 そこで本稿では、この JSON を読み取る簡易チャットインターフェースを用意し、実際にチームを動かしながらやり取りを観察してみたいと思います。 実際の Agent Teams セッションを観察する それでは、さっそく Agent Teams を招集し、メンバー間のメッセージを観察してみましょう。 今回は以下のプロンプトを使い、「エンジニアにとって最も幸福な瞬間」というテーマについて議論してもらいます。 なお、Agent Teams はタスク完了後に自動的に解散され、 inboxes/ 配下のファイルも削除されてしまいます。今回はタスク完了後もメッセージを観察するため、プロンプトの末尾で明示的に待機を指示しています。 tmux セッション上で実行すると、以下のように各メンバーのペインが起動します。それぞれが独立した Claude Code セッションとして動作していることが分かります。 さて、このセッションで交わされたメッセージを実況中継したいところですが、画面キャプチャですべてをお見せするのは現実的ではないため、ここでは特筆すべきメッセージを抜粋して掲載します。 なお、セッション全体のログは GitHub Pages にて公開していますので、あわせてご参照ください。 役割を伝達するメッセージ 各メンバーの責務などの詳細情報は、 teams/{team-id}/ 配下の config.json に記載されます。 { " name ": " engineer-happiness ", " description ": " 「エンジニアにとって最も幸福な瞬間」について議論し、全員が妥協なく賛同できる結論を導くチーム ", " leadAgentId ": " team-lead@engineer-happiness ", " members ": [ { " agentId ": " team-lead@engineer-happiness ", " name ": " team-lead ", " agentType ": " team-lead ", " model ": " claude-sonnet-4-6 " } , { " agentId ": " alex-chen@engineer-happiness ", " name ": " alex-chen ", " agentType ": " general-purpose ", " model ": " claude-opus-4-6 ", " prompt ": " あなたはAlexandra \" Alex \" Chenです。GoogleのDistributed Systems部門を10年率い...(中略)...最終的に結論が出たら、SendMessageツールでteam-leadに結論を報告してください。 ", " color ": " blue " } , ... ] } しかし、メッセージを観察していると、リーダー役のメインエージェント team-lead から各メンバー宛に責務や行動指針が伝達されていることが分かりました。 Marcus アカウントとしてログインし、 team-lead からのメッセージを確認してみましょう。 このように、各メンバーの Claude Code セッションは、 config.json および team-lead からのメッセージを通して自身の責務を把握し、そのうえで議論に臨みます。 ※ 各チームメンバーの経歴は AI による創作であり、実在の人物・団体とは関係ありません ※ チャットバブル内の本文は、 inboxes/ 配下 の JSON における text の値を表示しています 自律性の先に見える"人間"らしさ 実際にチームを動かしてみると、まったく予想外の出来事が起こりました。 Alex が Marcus にメッセージを送る際、宛先を marcus-okonkwo とすべきところ marcus としてしまい、メッセージが届かないまま議論が停滞してしまったのです。 ※ チャットバブル上のオレンジ色の見出しは inboxes/ 配下 の JSON における summary の値を表示しています しばらく様子を見ても進展がないため、筆者が「議論が停滞しているようです。あらためて状況を確認してください。」と介入する事態となりました。 これを受けてリーダーは Alex に正しい宛先への再送を指示しますが、その直後に「直接通信に問題があるので私が仲介する」と方針を切り替えてしまいます。 しかし実際には再送は成功しており、Marcus はメッセージを直接受け取って返信まで済ませていました。 「実はもう受信済みです」という Marcus の報告の後もリーダーは中継方針を変えず、最後まで仲介役を務め上げます。(本稿ではメンバー間の議論を紹介するつもりだったのですが... しかし、これはこれで興味深いため良しとしましょう) 再送を指示しておきながら結果を待てないせっかちさ、すでに解消された問題に気付かないすれ違い ——この一連のやり取りには、(今回の行き違いはアーキテクチャ上の問題に起因すると思われますが)どこか人間らしさを感じざるを得ません。 各メンバーが自律的に判断し行動するからこそ生まれるこうした「ちぐはぐさ」に、Agent Teams の有機的な振る舞いの一端が垣間見えました。 2種類のメッセージ 上述のキャプチャからも見て取れるとおり、メンバー間のメッセージは JSON や XML のような構造化されたデータの送受信ではなく、自然言語によるテキストで行われています。 互いに正面から反論し合い、論点を整理し、相手の意見を取り入れて自説を修正していく。チャットインターフェースで眺めていると、思わず筆者も割って入りそうになるほど自然な「対話」がそこにはありました。 ただし、すべてのやり取りが自然言語で行われるわけではありません。タスクのアサインやアイドル通知といったシステムイベントについては、 text フィールドに JSON 形式のメタデータが設定されています。たとえば、メンバーの手が空いた際には以下のような idle_notification が送信されていました。 { " type ": " idle_notification ", " from ": " alex-chen ", " timestamp ": " 2026-03-01T13:36:08.403Z ", " idleReason ": " available " } このように、自然言語による「対話」とシステムレベルの「メタメッセージ」 ——この2種類のメッセージが Agent Teams のコミュニケーションを支えていることが分かりました。 まとめ 本稿では、Agent Teams の要点を SubAgent との違いを軸に整理したうえで、メンバー間メッセージの実体が ~/.claude/teams/{team-id}/inboxes/ 配下の JSON ファイルであることを確認し、実際のチームセッションを通じてそのやり取りを観察しました。 ちなみに、彼らが導いた「エンジニアにとって最も幸福な瞬間」は、以下のとおりです。 **【最終結論】エンジニアにとって最も幸福な瞬間とは** Alexandra ChenとMarcus Okonkwoの議論を経て、以下の結論に全員が心から合意しました。 > エンジニアにとって最も幸福な**状態**は、「発見と影響の螺旋」の中に身を置き続けること——内なる構造の発見が世界に影響を与え、世界からの予測不可能な反響が新たな発見を触発する、その循環の中にいること。 > > そして最も幸福な**瞬間**は、その螺旋が回転していることを実感する時。ホワイトボードの前で問題の本質構造が見えた夜も、見知らぬ開発者が想像を超えた使い方をしているのを発見した日も、螺旋の異なる位相における「自覚の瞬間」である。 > > この螺旋を回し続けるために必要なのは、自らの発見を世界に委ねる勇気と、世界からの反響に自分を開く謙虚さである。 なかなかにエモい結論でした。 エージェント間のコミュニケーションを観察し、理解することは Agent Teams をより効果的に活用するうえでのヒントになるはずです。たとえば、メンバー間のメッセージが冗長になっていることに気付けば、プロンプトで簡潔なやり取りを指示することで、コンテキストの消費を抑えるといった改善につなげられるでしょう。 本稿で使用したチャットインターフェースは GitHub リポジトリ で公開しています。サーバーの起動は不要で、ブラウザで HTML ファイルを開くだけで利用可能です。皆さんもぜひ、Agent Teams のメンバー間のやり取りを観察してみてください。 本記事が、Agent Teams の理解や活用の一助となれば幸いです。 参考サイト [1] Introducing Claude Opus 4.6 [2] Building a C compiler with a team of parallel Claudes [3] Claude Code セッションのチームを調整する 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @satorin レビュー: @handa.kenta ( Shodo で執筆されました )
アバター
こんにちは、クロスイノベーション本部リーディングエッジテクノロジーセンターの小澤です。 「Claude Codeのスキルを多数運用しているとコンテキストを圧迫する」といった意見がGitHub Issuesやブログに複数あり、独自のワークアラウンドも生まれているようです。 本ブログでは、スキルの読み込みの仕様を整理し関連する議論を概観したうえで、執筆時の最新バージョンでのコンテキスト消費を計測します。 検証バージョン Claude Codeのスキル読み込みの仕様 関連する議論 GitHub Issues Zenn記事 議論の整理 計測 第1段階(起動時):分割による変化なし 第2段階(呼び出し時):分割版のほうがコストが高い まとめ 参考リンク 検証バージョン Claude Code v2.1.50(2026年2月23日時点の最新) / Claude Opus 4.6 Claude Codeのスキル読み込みの仕様 Claude Codeのスキル読み込みは Progressive Disclosure という段階的に読み込む仕組みで設計されています。 これはClaude Code固有の仕様ではなく、Anthropicが公開した Agent Skillsオープンスタンダード に基づくもので、Codex、Cursor、Gemini CLI、GitHub Copilot、opencodeなど、多くのプラットフォームが採用しています。 段階 タイミング 読み込まれる内容 第1段階 起動時 YAMLフロントマター( name と description )のみをシステムプロンプトに読み込む 第2段階 スキル呼び出し時 SKILL.md本文をコンテキストに注入する 第3段階 必要に応じて スキルディレクトリ内の追加ファイルをReadツール等で読み込む 公式ドキュメントのスキルの例 つまり、第1段階の起動時にはYAMLフロントマターだけが読み込まれ、SKILL.md本文は第2段階のスキルが呼び出されるまでコンテキストを消費しません。スキルが多数あっても起動時のコストはフロントマターの個数分だけであり、全スキルの本文が常時コンテキストに載るわけではない、というのがProgressive Disclosureの設計意図です。 第3段階は、スキルが複雑になり単一のSKILL.mdに収まりきらない場合や、特定のシナリオでしか使わないコンテキストがある場合に、追加ファイルをスキルディレクトリにバンドルしてSKILL.mdから参照する設計です。 公式ドキュメントで例示されているPDFスキルでは、フォーム入力の手順をforms.mdに分離しています。PDF処理全般がスキルの責務ですが、フォーム入力はそのうちの一部のシナリオでしか発生しません。SKILL.mdに全手順を書くとフォーム入力しないときもコンテキストを消費するため、forms.mdに分離してフォーム入力タスクのときだけClaudeが Read ツールで参照します。 また、 ベストプラクティス には Keep SKILL.md body under 500 lines for optimal performance とあり、500行を超える場合に別ファイルへの分割を推奨しています。 関連する議論 問題は上記仕様のProgressive Disclosureが期待通りに動いていないのではないか、という指摘がGitHub Issuesやブログ記事で複数見られることです。 GitHub Issues 2025年12月 ~ 2026年1月にかけて、スキルのトークン消費に関するIssueが報告されています。 Issue 起票日 ステータス 主張 Claude Code バージョン #14834 2025/12/20 Open /context でスキル全量のトークン数を表示しているが、実際はフロントマターだけロードされている v2.0.74 #14882 2025/12/21 Open /context でスキル全量のトークン数を表示。Progressive Disclosureが動いていない v2.0.74 #15530 2025/12/28 Closed as duplicate 6スキルで17.8kトークン。フルファイルがロード v2.0.76 #16616 2026/01/07 Closed as not planned スキルが全量ロード(5.9k〜1.7k)。Pluginのフォーマット .claude-plugin/plugin.json に変換して解消 v2.0.76 これらのIssueで注目したい点が2つあります。 #14834 の報告者が「実際にはフロントマターだけロードされている。 /context の表示が間違っているだけ」と主張していることです。この報告者は #16616 にも「表示バグであり、v2.1.1で修正された」とコメントしています。ただし、報告者はAnthropic社員でないため公式回答ではありません。 #16616 でPluginのフォーマットへの変換で解消したことです。表示バグだけなら両方とも同様に大きく表示されるはずで違和感があります。 なお、v2.1.1(2026/01/07)は CHANGELOG に記載がなく、修正内容の公式の記録はありません。 Zenn記事 Zenn記事 「スキルを87個運用したら898KBになった。SKILL.md分割で27KBに戻した実測記録」(2026/02/21公開) では、以下の主張があります。 スキルが増えるほどClaude Codeが遅くなる。原因はSKILL.mdがコンテキストを圧迫しているからだった。SKILL.md(フロントマター+参照1行)とINSTRUCTIONS.md(詳細手順)に分割したところ、87スキル全体で898KB→27KBへ97%削減できた。 スキルを増やすほど問題が起きる。スキルが呼ばれるたびに、SKILL.mdの全内容がシステムプロンプトに注入されるのだ。 ただし、Claude Codeのバージョン明記がなく、計測もファイルサイズの比較のみでコンテキスト消費量の比較がないため、検証の余地があります。 議論の整理 これらの報告をまとめると、以下の状況が見えてきます。 v2.0.x時代(2025年12月頃)に、 /context コマンドの表示バグ、あるいは実際のロードバグがあった可能性がある 表示バグなのか実際のロードバグなのかは、Claude Codeのソースコードが非公開のため確定できない v2.1.1以降で修正されたという報告があるが、公式の記録はない 重要なのは最新バージョンでの挙動なので、次のセクションでコンテキスト消費を計測して確認します。 計測 関連する議論で紹介したZenn記事のワークアラウンド(SKILL.md + INSTRUCTIONS.mdへの分割)が実際に効果があるのかを確認するため、同一スキルを「SKILL.md単体」と「SKILL.md + INSTRUCTIONS.mdに分割」の2パターンで比較しました。起動時と呼び出し時のトークン消費を /context コマンドで計測しています。 /context コマンドはトークン消費の内訳を表示します。本計測ではSkills(システムプロンプト内のスキル情報)とMessages(会話のやり取り)の2項目に注目します。 第1段階(起動時):分割による変化なし パターン Skillsのトークン数 スキル分 スキルなし 393 0(ベースライン、スキルなしでもトークン消費あり) SKILL.md単体 441 48(YAMLフロントマターのみ) SKILL.md + INSTRUCTIONS.mdに分割 441 48(YAMLフロントマターのみ) 「SKILL.md単体」と「SKILL.md + INSTRUCTIONS.mdに分割」の起動時のSkillsのトークン数は同一です。 SKILL.md本文のサイズは起動時のコストに影響がなく、Progressive Disclosureが仕様通りに動作していることが確認できます。 第2段階(呼び出し時):分割版のほうがコストが高い パターン Messages(呼び出し前) Messages(呼び出し後) Skills(呼び出し前) Skills(呼び出し後) SKILL.md単体 8 7.1k 441 441 SKILL.md + INSTRUCTIONS.mdに分割 8 9.4k 441 441 分割版ではMessagesのトークン数が2.3k多くなっています。 単体ではSKILL.md本文が1回で注入されるのに対し、分割版ではSKILL.md本文にINSTRUCTIONS.mdへの参照のみが書かれているため、Claudeが必ずReadツールで読みに行きます。その結果、ツール呼び出しのオーバーヘッドが加算されます。 また、どちらのパターンでもSkillsのトークン数の441はスキル呼び出し後も変化していません。SKILL.md本文はシステムプロンプトではなくMessagesのトークンとして計上されていました。 なお、v2.0.x時代に /context コマンドの表示バグが報告されていましたが、本計測ではSkillsとMessagesのトークン数が仕様通りに変動しており、表示バグの影響はないと判断しています。 まとめ 計測結果は以下の通りです。 観点 結果 起動時のSkillsのトークン数 SKILL.md単体・分割版ともに同一(YAMLフロントマターのみ) スキル呼び出し時のMessagesのトークン数 分割版のほうが2.3kトークン多い(Readツールのオーバーヘッド) SKILL.md分割の効果 v2.1.50では不要。むしろ逆効果 v2.0.x時代に表示バグ、あるいはロードバグがあった可能性はありますが、少なくともv2.1.50ではProgressive Disclosureが正しく動作しており、全スキルの本文をINSTRUCTIONS.mdに分離するような対策は不要です。 スキルの運用で意識したい点は2つです。 YAMLフロントマターの description を適切に書く description を見てスキルを呼び出しを判断するので、曖昧だと不要なスキルが呼ばれてMessagesのトークンを浪費します。 SKILL.md本文が大きくなったら関心ごとにファイルを分割する Progressive Disclosureの第3段階にあたり、特定のシナリオでしか使わない手順を別ファイルに分離することで、呼び出し時のトークン消費を抑えられます。ただし、全スキルの本文を一律に別ファイルへ逃がすのではなく、1つのスキル内でシナリオに応じて読み込みを分岐させる設計です。 参考リンク Agent Skills 公式ドキュメント Skill authoring best practices Equipping agents for the real world with Agent Skills Issue #14834: /context command shows full skill tokens instead of loaded metadata tokens Issue #14882: Skills consume full token count at startup instead of progressive disclosure Issue #15530: Project skills loading full files instead of frontmatter during discovery phase Issue #16616: User skills fully loaded into context instead of frontmatter-only Claude Code CHANGELOG スキルを87個運用したら898KBになった。SKILL.md分割で27KBに戻した実測記録 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @ozawa.hideyasu レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
金融IT本部 入社1年目の河岸歩希です。 会社の同期と個人開発に取り組んでいます。 その過程で「LINEのような個別チャット機能」を実装するにあたり、AWSのサーバーレス構成(Lambda + DynamoDB)の採用を検討することになりました。 今回は実際に調査と設計を行う中で得られた気づきについて共有させていただきます。 はじめに 想定読者 本記事の目的 チャット機能の要件 DynamoDBの設計を理解する 基本的な仕組み プライマリキーとパーティション ホットパーティションに注意する データアクセスパターン RDBとの違い アクセスパターンを洗い出す チャット機能へのアクセスパターン テーブル設計 最初に考えた設計 問題「自分の参加ルーム一覧」が取れない GSIとは GSIで何が解決できるのか GSIの特徴 射影(Projection)という概念 LSI(ローカルセカンダリインデックス)との違い テーブル設計(完成) Messagesテーブル GSI(グローバルセカンダリインデックス) まとめ はじめに 想定読者 RDBは触ったことがあるけど、DynamoDBは初めての方 「DynamoDBって何が嬉しいの?」という疑問をお持ちの方 サーバーレス構成でチャット機能を作りたい方 本記事の目的 DynamoDBとRDBの設計思想の違いを理解する チャット機能の要件 今回調査したのは、LINEのような1対1の個別チャット機能です。 個別チャット機能はアプリケーション全体のコア機能ではありませんが、初めて実装する機能だったため、先行して調査を行いました。 チャット機能を実装するにあたり、以下のような要件を整理しました。 要件 理由 リアルタイム性 メッセージは送信後すぐに相手に届いてほしい 高頻度の書き込み チャットはメッセージの追加が頻繁に発生する スケーラビリティ 現状は30人規模だが、将来的な拡大も想定したい 高可用性 業務時間中はいつでも利用できる状態を維持したい DynamoDBの設計を理解する DynamoDBはNoSQL(Not Only SQL)データベースの一種で、キーバリュー型に分類されます。 キーバリュー型の特徴は、キーを指定して値を取得するシンプルな構造です。 DynamoDBでは、データは以下のような構造で格納されます。 テーブル └── 項目(Item)← 1件のデータ ├── パーティションキー: "UserA" ← キー(必須) ├── ソートキー: "2026-01-17" ← キー(任意) ├── Name: "鈴木一郎" ← 属性 ├── Email: "suzuki@example.com" ← 属性 └── Department: "営業部" ← 属性 項目(Item) : 1件のデータのまとまり(RDBでいう「行」) キー(プライマリキー) : 項目を一意に特定するもの 属性(Attribute) : 項目が持つ各フィールドのこと 基本的な仕組み DynamoDBのようなキーバリュー型のデータベースを理解するうえで、最も重要なのは「キー」の設計です。 はじめに「プライマリキー」の構成について説明します。 プライマリキーとパーティション プライマリキーは「パーティションキー」と「ソートキー」で構成されており、これらを適切に設計することで、効率的なデータアクセスが可能になります。パーティションキーは必須、ソートキーは任意です。 パーティションキーの役割はその名の通り「パーティション(区画)を決定する」ことです。 DynamoDBでのパーティションは、SSDによってバックアップされ、AWSリージョン内の複数のアベイラビリティゾーン間で自動的にレプリケートされる、テーブル用のストレージの割り当てのことを指します。 公式ドキュメントには以下のように記載されています。 DynamoDBは、パーティションキーの値を内部ハッシュ関数への入力として使用します。 ハッシュ関数からの出力により、項目が保存されるパーティション (DynamoDB内部の物理ストレージ) が決まります。 出典: パーティションとデータ分散 - Amazon DynamoDB デベロッパーガイド つまり、同じパーティションキーを持つデータは物理的に同じパーティションに格納され、異なるパーティションキーを持つデータは別のパーティションに格納されます。 以下の図は、同ドキュメントから引用したものです。 この図では、パーティションキー「AnimalType: Dog」を持つ項目が、ハッシュ関数を通過し、その出力値に基づいて特定のパーティションに格納される様子を示しています。 同様に「Fish」「Lizard」「Bird」「Cat」「Turtle」といった異なるパーティションキーを持つ項目は、それぞれ異なるパーティションに分散して格納されます。 これがパーティションキーのみを用いた場合の、DynamoDBにデータが格納される仕組みです。 この方法に加えて、パーティションキーとソートキーを組み合わせた 複合プライマリキー と呼ばれるタイプも存在します。 この図では、パーティションキー「AnimalType」とソートキー「Name」を組み合わせています。同じパーティションキー「Dog」を持つ項目(Bowser、Fido、Rover)は、同じパーティションに格納されます。 その中でソートキー「Name」によって項目が一意に識別され、ソートされた状態で格納されます。複合プライマリキーを使用することで、「同じパーティションキーを持つ複数の項目」を1つのパーティションにまとめて格納し、ソートキーを使って範囲検索やソートを行うことが可能になります。 なので効率的なデータ探索をするためにも、シンプルなプライマリキーにするのか、複合プライマリキーを利用するべきなのかは、アプリケーションのユースケースによって異なるため、事前にどのようなアクセスパターンでデータ操作するのかを検討することが重要です。公式のベストプラクティスでも、アクセスパターンを事前に把握したうえでのキー設計が推奨されています。 NoSQL の設計の違い 対照的に、DynamoDB の場合は答えが必要な質問が分かるまで、スキーマの設計を開始すべきではありません。 ビジネス上の問題とアプリケーションのユースケースを理解することが不可欠です。 出典: NoSQL 設計のベストプラクティス - Amazon DynamoDB デベロッパーガイド 「答えが必要な質問が分かるまで」という表現は少々わかりづらいですが、言い換えると 「どのようなクエリパターンでデータを取得するのかが明確になるまで、スキーマ設計を始めるべきではない」 ということだと解釈しています。 ホットパーティションに注意する ここまでのパーティションキーの説明を踏まえて、パーティションがキーごとに分散されるのなら、仮にユーザー数が増えたときにパーティションの上限が来るのではないかという疑問が浮かびました。 公式ドキュメントに以下のような記載がありました。 DynamoDB テーブルには、パーティションキーバリューごとに個別のソートキーバリューの数に上限はありません。何十億もの Dog 項目を Pets テーブルに保存する必要がある場合、DynamoDB はこの要件を自動的に処理するのに十分なストレージを割り当てます。 出典: パーティションとデータ分散 - Amazon DynamoDB デベロッパーガイド つまり、パーティションが増えること自体は問題ではないということです。 むしろ注意すべきは、特定のパーティションにアクセスが集中することです。これを「 ホットパーティション 」と呼びます。 例えば、パーティションキーを「日付」にした場合を考えます。今日のデータへのアクセスが集中し、過去の日付のパーティションはほとんど使われません。せっかくパーティションが分かれていても、1つに集中してしまっては意味がありません。 今回のチャット機能では、パーティションキーを RoomId にしています。1対1の個別チャットなので、全員が同じルームに集中することはなく、ルーム数が増えるにつれてアクセスは自然と分散されます。もしグループチャットや全社連絡用のルームを作る場合は、特定のルームにアクセスが集中する可能性があるため、別の設計を検討する必要があるかもしれません。 なので、ユーザーアクセスが特定のパーティションに集中することがないようにパーティションキーを設定する必要があります。 データアクセスパターン DynamoDBのデータを取得する方法は主に2パターンあります。 方法 説明 速度・コスト Query パーティションキーを指定して取得。ソートキーで範囲指定やソートも可能 高速・低コスト Scan テーブル全体を走査して条件に合うものを取得 低速・高コスト 基本的にはQueryを使い、Scanは避けるべきとされています。Queryが高速なのは、たった今説明したように、パーティションキーやソートキーによってパーティションが明確に分けられているからです。この分散したデータ管理によって、DynamoDBは大規模なデータに対しても高速なアクセスを実現することができます。 具体的には、一つのテーブルに対して同時にアクセスする際にも、パーティションが分かれていることによって、同時実行性が向上し、スループットを効率的にスケールさせることが可能となります。 またScanはテーブル全体をスキャンするオペレーションであるため、テーブルが大きくなるにつれて、処理速度は遅くなります。そのため応答時間短縮のためにも、なるべくQueryを使用できるように、テーブル設計段階で適切なパーティションキーとソートキーを検討することが大切です。 RDBとの違い ここまで理解したところで、ある疑問が浮かびました。 「RDBでもWHERE句を使えば、目的のデータを効率的に探索できるよね?DynamoDBのパーティションキーを指定するのと何が違うの?」 この時点では突出したDynamoDBの良さを感じることはできていませんでした。(初心者目線です...) ただ、調べていくうちに、 「1回のクエリの速度」ではなく「同時に大量のクエリが来たとき」 に違いが出ることがわかりました。 RDBの場合、1000人が同時にアクセスすると、すべてのリクエストが1台のDBサーバーに集中します。(リードレプリカやマルチAZ構成にした場合を除く) その結果、CPUやコネクションが逼迫し、全体的にレスポンスが遅くなります。 一方、先ほども説明したとおり、DynamoDBの場合、各リクエストはパーティションキーに基づいて別々のパーティションに分散されます。 そのため、同時接続数が増えても負荷が1箇所に集中せず、レスポンス速度を維持できます。 公式ドキュメントにも、以下のような記載があります。 RDBMSでは、データは柔軟にクエリできますが、クエリは比較的コストが高く、トラフィックが多い状況では スケールがうまくいかない場合があります。 出典: リレーショナルデータ設計と NoSQL の相違点 - Amazon DynamoDB デベロッパーガイド DynamoDBは、パーティションキーによるハッシュ分散があるからこそ、データ量やアクセス数が増えても性能を維持できます。これがRDBとの大きな違いの一つです。 なので大規模アプリケーションや人気イベント等などの、ユーザーからの大量なアクセスが予想される際は、こういったDynamoDBの分散管理の仕組みが輝くということがわかりました。 アクセスパターンを洗い出す 実際にここからテーブル設計の流れを共有します。 前述したNoSQLの設計のベストプラクティスの引用文の中でもあったように、DynamoDBでは「どのようなクエリパターンでデータを取得するのか」を明確にしてから設計を始める必要があります。 そこで、まずはチャット機能のアクセスパターンを洗い出しました。 チャット機能へのアクセスパターン ケース パターン 使用例 1 特定ルームのメッセージ一覧を取得 チャットルームを開いたとき 2 最新N件のメッセージを取得 初期表示・スクロール時 3 自分が参加しているルーム一覧を取得 チャット画面を開いたとき 4 新しいメッセージを追加 メッセージ送信時 5 新しいチャットルームを作成 初めてのDMを送るとき 6 チャットルームを削除 ユーザーがルームを消したとき このような形でアクセスパターンを洗い出しました。その後に、「何を起点にデータを探すか」を考えてみます。 ケース1, 2, 4: RoomId を起点にメッセージを操作 ケース3: UserId を起点にルームを検索 ケース5, 6:ルームの作成・削除 ここでケース3だけ他のケースとは起点となるデータが違ってしまうことに気づきました。 DynamoDBでは、パーティションキーとソートキーを指定することで効率的にクエリを実行できますが、パーティションキー以外の属性を検索条件にすることはできません。 テーブル設計 最初に考えた設計 メッセージを格納するテーブルとして、以下のような設計を考えました。 パーティションキー :RoomId(チャットルームのID) ソートキー :Timestamp#UserId(送信時刻と送信者ID) 属性 :Message アクセスパターンを踏まえて、一番起点となるデータをパーティションキーに持ってきました。また、 ソートキーにはチャットの履歴の取得などを行いたいので、タイムスタンプとして、同じ時間に送信した場合にどちらのユーザーが送ったのかを判別するために UserId と結合させて ソートキーに格納します。 パーティションキーに RoomId を指定してQueryを実行すれば、そのルームのメッセージがソートキー(時刻順)でソートされて取得できるイメージです。 問題「自分の参加ルーム一覧」が取れない しかし、「自分が参加しているルーム一覧を取得(ケース3)」を実現しようとした際、壁にぶつかりました。 現在の設計では、パーティションキーは RoomId です。 DynamoDBのQueryは パーティションキーの完全一致 が必須なので、「 UserId を起点にルームを検索する」ということはできません。 これを解決するのが GSI(グローバルセカンダリインデックス) という仕組みです。 GSIとは GSIを一言で説明すると、 「元のテーブルとは別のプライマリキーでQueryできるようにするコピーテーブル」 です。 公式ドキュメントには以下のように記載されています。 グローバルセカンダリインデックスには、ベーステーブルとは異なるパーティションキーとソートキーがあります。 出典: グローバルセカンダリインデックス - Amazon DynamoDB デベロッパーガイド GSIで何が解決できるのか 公式ドキュメントのシナリオがとてもわかりやすかったので、具体的な使用例はそちらを参考にしていただければと思います。(上記のGSIの出典先と同じリンク) GSIを定義することで、元のテーブルに加えて、別の切り口で検索できるようになります。 【元のテーブル】 パーティションキー: RoomId ソートキー: Timestamp#UserId → 「特定ルームのメッセージ」を取得できる 【GSI】 GSI-パーティションキー: UserId GSI-ソートキー: RoomId → 「特定ユーザーの参加ルーム一覧」を取得できる GSIで新たに選ばれたパーティションキー UserId ごとに、異なるパーティションにデータが元のテーブルからコピーされます。 これによって、ケース3の「自分が参加しているルーム一覧を取得」も実現できそうです。 GSIの特徴 GSIについて調べる中で、以下のような特徴があることがわかりました。 特徴 説明 元テーブルとは別のプライマリキーを設定可能 柔軟な検索が可能になる データは自動で同期される 元テーブルに書き込むとGSIにも反映(結果整合性) 後から追加可能 設計を間違えても後からリカバリできる 追加コストがかかる 書き込み・ストレージ両方で課金 注意点として、結果整合性であるため、書き込み直後にGSIをクエリすると、最新のデータが反映されていない場合があります。 そのため、リアルタイム性が重要な場面では考慮が必要になります。今回はGSIを使うのが、自分が参加しているルーム一覧取得の場面のみなので、多少のデータ遅延は許容できると判断しました。(実際ルーム作成直後にそのままルームでチャットを始めると思うので...) これが金融の残高照会等のシビアな要件が絡むと、この結果整合性に伴う遅延が大きな影響をもたらすので、要件と照らし合わせて適切なアーキテクチャを選択する必要があります。 射影(Projection)という概念 GSIを学ぶ中で、 射影(Projection) という概念にも出会いました。 射影とは、「元テーブルのどの属性をGSIにコピーするか」を指定するものです。 今回は KEYS_ONLY (キーのみコピー)を選びました。 理由は、GSIで取得したいのは「どのルームに参加しているか」という RoomId の一覧だけで、メッセージ本文などの属性は不要だからです。 GSIにコピーされていない属性が必要な場合、元テーブルに対して追加のクエリが必要になります。 そのため、アクセス頻度が高い属性は射影に含めておくことで、クエリ回数を削減できます。 公式ドキュメントにも、以下のような記載がありました。 一部のアプリケーションでは、テーブルに対して多くのクエリを発行する必要があり、その結果、プロビジョニングされたスループットの多くを消費することがあります。これを軽減するために、すべてまたは一部のテーブル属性をグローバルセカンダリインデックスに射影できます。 出典: グローバルセカンダリインデックス - Amazon DynamoDB デベロッパーガイド ストレージ管理コストは増えますが、頻繁にアクセスする場合はクエリコストの削減で相殺されるという考え方です。 LSI(ローカルセカンダリインデックス)との違い GSIを調べる中で、 LSI(ローカルセカンダリインデックス) という似た概念があることも知りました。 LSIの「ローカル」とは、 同じパーティション内に存在する という意味です。 GSIは元テーブルとは別のパーティションにデータがコピーされますが、LSIは同じパーティション内でソートキーだけを変えたインデックスです。 今回の設計では GSI を使うのがよさそうだと判断しました。 理由は、LSIでは要件を満たせないからです。 今回のアクセスパターンの中で「ユーザーIDで、自分の参加ルーム一覧を検索する」部分があります。 メインテーブルのパーティションキーは RoomId なので、 UserId で検索するにはパーティションキーを変更する必要があります。 LSIではパーティションキーを変更できないため、そもそも今回の要件を満たせません。 そのため、GSIを採用しました。 さて、これらのプロセスを経て、実際にテーブル設計が完了しました。 今回設計したテーブルは以下のとおりです。 テーブル設計(完成) Messagesテーブル 項目 値 説明 パーティションキー RoomId チャットルームのID(例: suzuki_yamada ) ソートキー Timestamp#UserId 送信時刻とユーザーID(例: 2026-01-18T10:30:00Z#suzuki ) 属性 Message メッセージ本文 GSI(グローバルセカンダリインデックス) 項目 値 説明 GSI名 UserRoomsIndex GSI-パーティションキー UserId ユーザーID GSI-ソートキー RoomId チャットルームID 射影 KEYS_ONLY プライマリキーのみコピー これにより、以下の検索が可能になります。 アクセスパターン 使用するインデックス クエリ方法 特定ルームのメッセージ一覧 メインテーブル パーティションキー=RoomIdでQuery 自分の参加ルーム一覧 GSI(UserRoomsIndex) GSI-パーティションキー=UserIdでQuery ここまででテーブル設計が完了しました。 まとめ 今回、チャット機能のためのDynamoDBテーブル設計を行いました。 設計を通じて学んだポイントは以下の3つです。 アクセスパターンを先に考える DynamoDBでは「どんなクエリが必要か」を明確にしてからキー設計を行う。RDBのように後からインデックスを追加して柔軟に対応する発想ではうまくいかない。 パーティションキーの設計が最重要 パーティションキーによってパーティションが決まり、検索効率とスケーラビリティが大きく変わる。ホットパーティションを避け、アクセスが分散されるようなPKを選ぶ。 GSIで検索の柔軟性を確保 メインテーブルのパーティションキーでは対応できないアクセスパターンがある場合、GSIを活用する。ただし結果整合性やコストには注意が必要。 今回はここまでで、実装はこれからです。 あくまでも机上の設計なので、実装段階で想定外のエラーや設計の見直しが発生するかもしれません。そのときはまた調べて共有できればと思います。 初心者なりにまとめてみましたが、同じようにDynamoDBを学び始めた方の参考になれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kawagishi.ibuki ( Shodo で執筆されました )
アバター
はじめまして! バリューチェーン本部の花ヶ崎雄太と申します。 新卒1年目の12月に一般社団法人コンピュータ教育振興協会(ACSP)の主催する3次元CAD利用者技術試験の準1級を受験し、合格いたしました。 今回はそれについて、 ・3次元CAD試験とは ・受験の理由と感想 ・出題される問題の概要と傾向 ・実際の学習方法 ・合格のコツ の5つに分けてご紹介します。 本記事の想定読者 ・3次元CAD利用者技術試験に興味がある/取得の予定があるが、CADの操作については未習得な人 ・仕事や学業で製造系の業界や学問に携わっており、これからCADを習得予定の人 ※なお本記事では「CADそのもの」やそれに関連した個別の用語についての説明は行いませんが、想定読者向けに難しいと考えられる単語には一部注釈をつけています。 1. 3次元CAD試験とは 試験概要 ここは公式から引用します。 『「3次元CAD利用技術者試験」は、3次元CAD を利用するエンジニアや学生が身につけておくべき知識と技能が証明できる、3次元CAD試験制度です。』 『3次元CADシステムを利用した機械系・製造系のモデリング・設計・製図などの業務に従事することを目指す方、もしくは従事して間もない方を想定して試験を行います。3次元CADを学び、知識と操作の基礎的な部分を習得し、設計の補助業務やオペレーターを目指す方が対象です。』 2025年度 3次元CAD利用技術者試験 概要 より引用。 つまり3次元CADをこれから積極的に利用していこう、という方に向けての試験ということですね。 ちなみに本試験は製造業でCADを使用する方向けであり、建築業向けではないようです。 2級について ~準1級受験には事前に2級の取得が必要~ 準1級を受験するためには事前に2級に合格している必要があります。 具体的には準1級の申し込み期限の前日(準1級試験日の1か月ほど前)までには2級を取得している必要があるので、準1級以上の取得を目指す方はまずは2級を取得しましょう。 2級は比較的難易度の低い選択式の試験で、CBT形式でテストセンターで実施されます。 難易度は高くはありませんので、公式の販売している書籍( https://www.acsp.jp/ACSP_books.html )を学習して過去問演習を重ねれば十分合格可能です。 私の累計の学習時間は 15時間 ほどです。 2級を学習することで、その後のモデリングでも用いる基本的な用語や手法を知ることができます。 2級にはモデリングの実技はありませんが、できれば2級もCADを触りながら学習したほうが学習効率が高いと思います。 準1級について 準1級は年に2回、例年7月と12月に開催されます。 いつでも受けられる2級とは違い年2回のみの開催ですので、機会を逃さないように注意しましょう。 準1級では実際に会場にPCを持ち込み、モデリングの実技を行います。 詳しくは後述しますが、問題は2次元図面を読み取って パーツモデル*1 を作成する形式です。 そうして作成したモデルの体積、表面積などを測定し、解答群の中から最も近い値をマークシートに記入する形式で試験が行われています。 試験時間は120分と長めです。 ーーーーー *1 パーツモデル :単一の部品を3DCADで作成したモデルのこと。 ーーーーー 2. 受験の理由と感想 なぜ受験したのか? 率直に言えば、業務上必要だったからです。 私の所属する部署はCADをメインの商材として扱っており、CADの基礎知識/技術を身に着ける必要があります。 そのため業務上の要求として最低限「2級」の資格を取得することとなっていました。 しかしOJTの方からのアドバイスや、より高みを目指したいという思いから、私は準1級に挑戦することにしました。結果的に、私の所属するバリューチェーン本部の新人14人のうち、準1級を取得したのは私含め2人でした。 受験してよかったか? 結論としては、よかったです。そう思う観点は3つあります。 1.CADの基礎知識および基本的なコマンドを習得することができた。 準1級では、要求されるコマンドは基本的なものがほとんどです。試験問題がスムーズに解けるようになる頃には、基本的な形状(例えばテレビリモコンなど)なら難なくモデリングできるくらいのスキルが十分につきます。 2.扱うCADに関する理解が大きく深まった。 学習・受験に利用するのはそれぞれの受験者が利用しているCADですから、そのCADについての設定や効率的な使用方法、得意や苦手などの理解が大きく深まります。 今回私は「NX」というCADを使って学習と受験を行いましたが、学習の過程でNXならではの強みや特異な点を複数感じ、理解することができました。 3.単純に楽しかった。 個人的には重要な観点だと思います。 CADで形状を作るのが、なんだか砂山で城を作っているような感覚で楽しかったです。 またモデリングの時間が早まることや、それまで気づかなかったモデリング方法に気づくことなどで自分の成長をひしひしと感じられます。 今回の学習と受験経験を通して、仕事を行うモチベーションという点で「楽しい」という感覚が重要だと実感できました。 3. 出題される問題の概要と傾向 ここでは準1級における具体的な問題の概要と傾向、主な使用コマンドについて紹介します。 問題の概要 準1級では、 準1級と1級で共通の問題が2問 と、 準1級のみの問題が1問 の、 合計3問 出題されます。 どの問題にも共通なのが、最終的な問題の解答は「モデルの完成」ではなく、「 完成したモデルからのパラメータの測定値 」であることです。 具体的には各問題につき3~5問の設問があり、「点Aから点Bの長さを測定し、選択肢から最も近いものを選べ」「立体Cの体積を測定し、選択肢から最も近いものを選べ」といった出題がなされます。 指示や図面に従ってモデリングをし、その最中や完成後にパラメータの測定を行い、マークシートに記入する、までが、基本的な解答の流れです。 (1) 共通問題-1 座標や形状、モデリング方法についての指示があり、指示通りにモデリングを行う形式です。 基本は指示に従えばよいので、練習を繰り返すことでコマンドを理解し、モデリング速度を上げることが重要です。 主な要求コマンド スケッチ、押し出し(和・差・積)、回転、直方体、円筒、球、エッジブレンド、面取り、シェル、データム平面(無限平面)、トリムボディ、ミラージオメトリ、パターンジオメトリ、線 ※コマンドの名称はNX準拠のため、CADによって異なる可能性があります。 (2) 共通問題-2 3面図*2 を読み取って、その図面の通りにモデリングを行います。 無限平面*3 を使った複雑な押し出しが求められることが多く、効率よく解くためには、経験に基づく「閃き」が必要です。個人的には準1級範囲で最も癖が強いと感じています。 また実際の現場で作ることは決して多くない形状だと思いますので、問題に慣れるまで時間がかかります。 主な要求コマンド スケッチ、押し出し(和・差)、直方体、エッジブレンド、面取り、データム平面(無限平面)、トリムボディ、円錐、点セット ※コマンドの名称はNX準拠のため、CADによって異なる可能性があります。 ーーーーー *2 3面図 :正面図、側面図、上面図の3種の図面。3次元形状を3方向から投影した2次元図面であり、この3つを見ることで3次元形状の作成が可能。 *3 無限平面 :CADではXYZ平面以外にも任意の平面が作成可能。この平面で立体を切断する手順は超頻出。 ーーーーー (3) 準1級問題 共通問題-2と同じく、3面図を読み取ってその通りにモデリングを行います。しかしこちらの方が”現実にありそうな”形状が出題されるので、比較的イメージはつきやすいです。 ただ複雑なスケッチやコマンドの使用をせざるを得ない場合もあり、要求されるモデリング力は高いです。 主な要求コマンド スケッチ、押し出し(和・差)、回転、直方体、円筒、エッジブレンド、面取り、シェル、ドラフト、データム平面(無限平面)、トリムボディ、点セット ※コマンドの名称はNX準拠のため、CADによって異なる可能性があります。 4. 実際の学習方法 コマンドを知る 私はまず、コマンドを知ることから学習を開始しました。 最初はそもそも必要なコマンドがバーのどこにあるのか、似たようなコマンド(例えば、パターンフィーチャとパターンジオメトリ)のうちどれを使えばいいのかなど、全くわかりません。 そこで最初は、準1級の共通問題-1をじっっっくり時間をかけて解き、「どのコマンドがどこにあるのか」「どのコマンドをどの順番で使えば目的の形状を実現できるのか」を学習しましょう。 共通問題-1は問題文の中で使うコマンドや座標を指示してくれているので、「コマンドの所在の把握」や「そのコマンドで何が起こるのか」の学習に向いています。 ちなみに私は最初、共通問題-1を1問解くのに4時間かかりました(笑)。 とにかく演習あるのみ コマンドについてある程度把握出来たら、あとはとにかく問題を解きまくります。 問題は基本的には過去問です。 公式の書籍には過去問が1年分(前期と後期の2回分)掲載されているので、演習に使用できます。 また 試験の公式サイト ( https://www.acsp.jp/cad/3d_past_web3d.html )では、過去問において出題された形状のモデリング結果が掲載されています。図面から正解の形状が想像もつかないときや、行き詰まった際に参照するととても役立ちます。 さらに、 日経クロステック ( https://xtech.nikkei.com/dm/article/COLUMN/20120605/221476/ )では当試験の例題が複数掲載されているので、そちらを利用して演習するのもよいでしょう。 問題に取り組む順番は 共通問題-1:~15分で解けるようになるまで 準1級問題:~60分で解けるようになるまで 共通問題-2:40~60分で解けるようになるまで をおすすめします。 共通問題-2と準1級問題は図面の読み取り力も必要になりますが、共通問題-2では図面で隠された部分(点線部分)に三角形の入れ子構造を作ることが要求されます。しかしコレには非常に癖があり、図面に慣れていない状態でいきなりやるのは難しいです。 そのため、手順が多い代わりに比較的オーソドックスな準1級問題で実力をつけてから取り組むのがおすすめです。 合格までの累計学習時間は? 私の場合は合計 70時間 ほどです。 業務時間での学習が認められていたため、平均1日2~3時間の学習を約1.5か月継続しました。 ただCAD経験ゼロの状態から開始し、初期はコマンドの学習に丸1日費やすこともありましたから、CAD使用経験が少しでもあるならこの時間は短くなると思います。 5. 合格のコツ 最後に私の経験から役立ったと感じた具体的な合格へのコツを、いくつかかいつまんでご紹介します。 準1級受験用のコマンドバーを作る CADによって異なるとは思いますが、私の利用するNXではコマンドのバーをカスタムで作成することができたので、試験で使うコマンドを集めてカスタムバーを作成して使っていました。 コレがあるだけでいちいちコマンドを探す手間が省け、また慣れも早かったので解答の高速化が実現しました。 また副次的に「カスタムバーを作成する」という設定面での学習ができたのも良かったと感じました。 同一形状を作るうえでも、複数の実現方法があると知る 単なる円筒を作成するうえでも、 ・円をスケッチして押し出し ・長方形をスケッチして回転 ・円筒コマンドの利用 など 複数の実現方法 があります。 これが試験問題ともなると、各形状を実現する方法が非常に多岐にわたります。 その中でどの方法が自分に合っているのか、どの方法が効率がよいか、といった部分は、問題を解くうちに経験的に身についてきます。 例えば上の例であれば基本的には「円筒コマンドの利用」が最速でしょう。 たくさん演習問題を解いて、自分に合ったモデリング方法を学びましょう。 またその方法が実際の業務や研究で使用するものならなお良いですね。 設問の順番通りにモデリングを行う 上の事項に関連して、同一形状を作るうえでも色々な順番で実現することが可能です。 しかし「試験の合格」を意識する場合は、設問の順番通りにモデリングを行うことをおすすめします。 指示がある共通問題-1は当然として、共通問題-2、準1級問題にも効率の良いモデリング順が存在します。これは設問の順番に従えば実現できるようになっています。 例えば 設問(1):点Aと点Bの距離を測定 設問(2):面Cの面積を測定 の場合、面Cを作る過程で、点Aと点Bも完成していることが多いです。 そしてここで先に、 設問(2)の面Cのところまで作った後に設問(1)を解いて間違っていた場合、まず間違いなく設問(2)も間違っているので、面Cのところまで作り直しになってしまいます。 しかし先に設問(1)の点Aと点Bを作成し解答しておけば、 間違っていても早く気づくことができる*4 のです。 ーーーーー *4 間違っていても早く気づくことができる :モデリング結果が違うときには「測定結果」が「解答の選択肢」と大幅に異なることが多いため、モデリングのミスには比較的気づきやすいです。 ーーーーー まとめ 今回は、3次元CAD利用者技術試験の準1級の受験や学習について、新卒1年目の目線で解説しました。 学習コストは大きいですが、準1級に合格する程度の知識/技術があれば、基本的なモデリング能力を身に着けたと十分言っていいくらいのスキルは得られます。 また単に学習するだけでなく、その中で実際の設計に活かせる経験も手に入ったと思います。 今後はさらにモデリング力を高めるために1級の取得を目指します。1級はアセンブリの知識やさらなるモデリングの高速化が求められるため、引き続き経験を積んでスキルアップを志向します。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @hanagasaki.yuta レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
はじめに 最近、 AWS の AI Practitioner を取得した金融IT本部 2年目の坂江 克斗です。 今回は、XI 本部の佐藤悠さんに協力していただきながら執筆した記事となります。 最近、「AI エージェント」という言葉をよく耳にするようになりましたが、正直なところ、私自身はエージェントが何をしているのか全く分かっていませんでした。 そこで今回、AI初心者の自分なりに理解するため、 MCP ベースのエージェントを 専用のライブラリを使わずに自作 し、その仕組みを理解してみることにしました。 本記事では、AI 初心者の方向けに、以下のイメージを掴んでもらえればと思います。 AIエージェントが「どのような仕組み」なのか MCP ホスト / MCP クライアント / MCP サーバの役割分担 はじめに 学習フロー エージェントとは ざっくりAIを考える AIの弱み RAG(Retrieval-Augmented Generation) Function calling Model Context Protocol(MCP) AIエージェント エージェントの作成(MCPベース) 前提 実装方針 通信内容の整理 JSON-RPC MCP 今回想定するMCPリクエスト・レスポンス initialize notifications/initialized tools/list tools/call 検証 まとめ おわりに 学習フロー エージェントの概要を理解し、実装するにあたり大まかに以下の流れで進めていきました。 佐藤さんから MCP やエージェントに関する概要を聞き、ソースと併せて理解を深める(1日目) MCP の公式ドキュメントを確認し、機能を絞ってエージェント( MCP ホスト / MCP クライアント / MCP サーバ / LLMとの連携)を実装する(1-2日目) 佐藤さんが執筆した MCP に関する記事も、あわせてご覧ください。 【初心者向け】2年目エンジニアが実践したMCPサーバー構築ガイド2025 【初心者向け】2年目エンジニアが解説するMCPクライアント構築ガイド エージェントとは ざっくりAIを考える 私自身、モデル構造や学習 アルゴリズム を理解しているわけではないので、ここでは最低限持っておきたい概念レベルに絞って整理します。 AI とは、人間の知能的な振る舞いを模倣することを目的とした概念であり、現在一般ユーザーが日常的に触れている AI の多くは、LLM(Large Language Model)を中心としたシステムになっています。 LLM の内部構造を詳細に追い始めると一気に難しくなりますが、本質的には非常に多くのパラメータを持つ巨大な関数、つまり、 「入力データを与えると、出力データが返ってくる」 ただそれだけの存在です。( ニューラルネットワーク では「層」という概念を用いてパラメータを調整していますが、外から見ると最終的には数式で表現できる関数として扱えます) 重要なのは、 LLM 自身が自動的に外部 API を呼び出したり、何かを実行しているわけではない という点です。 ただし、その関数が持つパラメータ量と複雑さが人間の想像をはるかに超えているため、結果として「考えているように見える」出力が得られている、というわけです。 AIの弱み AI が関数である以上、当然ながら限界も存在します。 それは、学習時(関数の各パラメータの調整時)に与えられていない情報については、単体の LLM だけでは原則として扱うことができません。 そのため開発者としては、「AI が知らない情報を、外部のデータを取得して回答に含めてほしい」と考えるようになります。 この課題を解決するために登場した仕組みが、RAG(Retrieval-Augmented Generation:検索拡張生成)です。 RAG(Retrieval-Augmented Generation) RAG(Retrieval-Augmented Generation) は、「AIが、質問に関連する情報を参照しながら、回答を生成しているように見せる」仕組みです。 この仕組みでは、従来の Web 検索や 検索エンジン の裏側でも使われてきた ベクトル検索 が利用されています。 ベクトル検索の細かい仕組みや実装は難しいですが、ざっくりとWeb検索をした際に関連サイトが取得できるイメージを持っておけば問題ありません。 大まかな実装イメージは、次のようなシンプルな流れになります。 まず、ユーザがプロンプト(質問文など)を入力すると、その内容をもとにベクトル検索を行い、 あらかじめ用意しておいたデータベースの中から 、プロンプトに関連する情報を取得します。(検索に適したプロンプトに変換するために、事前に LLM を利用する方式も存在します) 次に、その取得した情報をプロンプトとあわせて LLM に渡すことで、より文脈に沿った、精度の高い回答を生成できるようになります。 一方で、RAG はあくまで 「検索を拡張する仕組み」 であり、関連する情報を用いて回答を生成することはできますが、 API を呼び出したり何らかの処理やタスクを実行したりすることはできません。 そこで登場したのが Function Calling です。 Function calling Function calling は、OpenAI が発表した仕組みで、「AI が外部システムと連携し、学習範囲外のデータ取得や API 実行などの処理を行えるように見せる」ための方法です。 ただし、最初に述べたとおり、AI 自体は API を呼び出すことはできません。そのため、 API を実際に呼び出す処理はアプリケーション側で実装する必要があります。 イメージとしては以下の図に示す、クライアント(アプリ)・サーバ(LLM)間での処理フローとなります。 初めのLLMへのリクエストにおいて、LLMに使用すべきツール(機能)を選択させ、その機能をクライアント側で実行したのちに、結果とともに再度LLMに回答をもらう流れとなります。仕組み自体は、「 API 呼び出しをうまく抽象化しているだけ」とも言えますが、この発想を形にした点が非常に面白いと感じました。 一方で、Function callingには弱点もあります。それは、ツール呼び出しの実装がクライアント側に閉じてしまうという点です。つまり、実装が各アプリごとに依存する課題を抱えています。 この課題を解決するために登場したのが MCP (Model Context Protocol) です。 Model Context Protocol( MCP ) Model Context Protocol(MCP) は Claude を開発している Anthropic 社が定義した プロトコル です。 Function calling と同様に、「AI が外部システムと連携して、処理・回答しているように見せる」という点は同じですが、ツール呼び出しの方法そのものを標準化したことが大きな特徴です。 アーキテクチャ は以下のようになります。先ほどのクライアントとなるアプリが、 MCP ホストと MCP サーバに分割された形になります。 Function Callingに比べて少しリソースが増えました。 全体の処理フローは以下のようになり、 MCP サーバがツールの実行を抽象化していることが分かります。 この構成の強みは、 MCP ホスト / MCP クライアント / MCP サーバという役割を標準化したことにあります。 MCP ホスト:1つ以上の MCP クライアントを管理し、LLMと対話しながら処理をする アプリケーション全体 。 MCP クライアント: MCP ホスト内部に存在し、 MCP プロトコル に基づいて MCP サーバと通信を行う コンポーネント 。( MCP ホストから MCP サーバへの呼び出しを抽象化) MCP サーバ:実際に 外部サービスとの連携を行う コンポーネント で、 MCP プロトコル に基づいて MCP クライアントと通信する 。 例えば、サービス提供者が MCP サーバを用意しておけば、利用者はその実装の詳細を意識せず、他の MCP サーバと同じ感覚で利用できます。 HTTP が存在しなかった世界では、サービスごとに独自の プロトコル で API を実装する必要がありました。実際には、HTTP という共通 プロトコル があるからこそ、私たちは意識せずに様々な Web サービスを利用できています。 MCP も、それと同じ文脈でサービスの提供者・使用者の目線で考えると分かりやすいかと思います。 AIエージェント Function Calling や MCP を用いることで、LLM が本来単体では実行できないタスクについても、外部サービス(ツール)による処理の実行や、外部サービスにより取得したデータを文脈として取り込みながら回答を生成できるようになります。 このように、 外部のサービスやデータと連携した処理を基に振る舞う仕組み全体を、一般に AI エージェント と呼びます。 ただし、実態としてはWeb アプリケーションとしてのクライアント・サーバ構成で動いているという点を理解しておくことが重要です。 エージェントの作成( MCP ベース) 前提 ということで、実際に MCP をベースにしたエージェントを実装してみます。 本記事の目的は、 MCP の推奨構成をそのまま再現することではなく、各 コンポーネント がどんな順序で、どんなデータをやり取りしているかを理解することです。 そのため、 公式ドキュメント で扱われているいくつかの要素を省略し、以下の方針で実装を行いました。 目標 エージェントに「( アメリ カの都市名)の直近の天気」を必要とする質問を尋ねると、実際の天気予報 API を基にした回答を返してくれること 実装方針 MCP のバージョン 2025-11-25( Model Context Protocol 、 GitHub ) 機能面 MCP サーバ Prompts / Resources は省略して、Tools のみを実装し、外部 API (天気予報 API )の呼び出しを主に実装 ローカルでHTTPサーバとして起動 MCP クライアント Roots / Sampling / Elicitation は省略し、 MCP サーバに対する Tools の呼び出しを主に実装 MCP ホストにクラスとして内包される MCPホスト UIとして CLI でのstdin / stdoutを使用(ユーザとの入出力は コマンドライン ベース) ローカルで起動 通信面 HTTP ベースでの実装 SSEやセッション管理(stateful)を省略し、ステートレスに実装 認証は実装しない 使用技術 LLM:Claude MCP ホスト / MCP クライアント / MCP サーバー: JavaScript (TypeScript)での実装 実装コードは GitHub で公開しています。詳細が気になる方は mcp-sample を参照してください( mcp 系のライブラリを使用せず自作したこともあり、設計・実装ともに粗い点はご容赦ください)。 実装方針 MCP の公式ドキュメントをただ読むだけで、そのままコードに落とし込むのは難しく感じたため、今回は以下の手順で実装をします。 先に発生するすべてのHTTP通信を書き出し 各通信それぞれの HTTPヘッダ と ボディ( JSON -RPC) を整理 通信内容から逆算して実装。 通信内容の整理 JSON -RPC MCP では JSON-RPC 2.0 がメッセージ形式として採用されています。 以下が JSON -RPC の基本的な形式です。 リクエスト { " jsonrpc ": " 2.0 ", " id ": " number ", " method ": " string ", " params ": { object } } レスポンス { " jsonrpc ": " 2.0 ", " id ": " number ", " result ": { object } } レスポンス(エラー) { " jsonrpc ": " 2.0 ", " id ": " number ", " error ": { " code ": " number ", // -32768 ~ -32000 are reserved, " message ": " string ", " data ": " primitive or structed value " } } 今回はこの JSON -RPC を HTTP リクエストのボディに乗せて送受信する形で実装します。 つまり、HTTP はあくまで通信経路として利用し、実際の処理内容やメソッドの表現は JSON -RPC によって行う構成になります。 TCP の上でHTTPが成り立つように、HTTPのボディの上で JSON -RPC形式によるやり取りを行うイメージです MCP 本来の MCP の通信では、 公式ドキュメントのシーケンス図 に記載されているとおり、Server-Sent Events(SSE)やセッション管理を含む構成が想定されています。 しかし、今回の構成では理解を優先するため、SSE やセッション管理は割愛し、公式ドキュメントの構成を簡略化した以下の形で MCP によるやり取りを行います。 今回はセッションを持たないステートレスな HTTP 通信として実装しているため、通信の終了フェーズを明示的に実装する必要はありません。 今回想定する MCP リクエスト・レスポンス 通信の全体像が把握できたところで、公式ドキュメントに記載の ライフサイクル章 や トランスポート章 を参考にし、各フェーズにおいてどのようなデータが送受信されるのかを整理します。 initialize リクエスト ヘッダ Content-Type: application/json Accept: application/json - ボディ { " jsonrpc ": " 2.0 ", " id ": 1 , " method ": " initialize ", " params ": { " protocolVersion ": " 2025-11-25 ", " capabilities ": {} , " clientInfo ": { " name ": " ExampleClient ", " version ": " 1.0.0 ", " description ": " An example MCP client application " } } } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 1 , " result ": { " protocolVersion ": " 2025-11-25 ", " capabilities ": { " tools ": { " listChanged ": false } } , " serverInfo ": { " name ": " ExampleServer ", " version ": " 1.0.0 ", " description ": " An example MCP server providing tools and resources " } } } notifications/initialized リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " method ": " notifications/initialized " } レスポンス ステータス: 202 ボディ:空 tools/list リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " id ": 2 , " method ": " tools/list ", " params ": {} } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 2 , " result ": { " tools ": [ { " name ": " get_weather ", " description ": " Get current weather information for a location ", " inputSchema ": { " type ": " object ", " properties ": { " latitude ": { " type ": " number " } , " longitude ": { " type ": " number " } } , " required ": [ " latitude ", " longitude " ] } } ] } } tools/call リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " id ": 3 , " method ": " tools/call ", " params ": { " name ": " get_weather ", " arguments ": { " longitude ": " -94.0 ", " latitude ": " 40.71 " } } } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 3 , " result ": { " content ": [ { " type ": " text ", " text ": " Current weather in New York: \n Temperature: 72°F \n Conditions: Partly cloudy " } ] } } このようにデータの流れをイメージできたため、あとはこの流れ通りに動作するよう MCP ホスト / MCP クライアント / MCP サーバ を実装していきました。 その結果が、先ほど紹介した リポジトリ の状態となります。 実装を進める中で、特に詰まった点は以下の2点です。 LLMが JSON を返却する際、どうしてもコードブロックを含めてしまう点 システムプロ ンプトによる制御では完全に回避できなかったため、文字列を整形する処理を実装し対処。 「直接天気を調べて」とは指示せず、「天気をもとにアド バイス してほしい」といった依頼をした場合に、LLMがツールを選択せず、「その情報を調べてもよいですか?」と確認してくるケースがあった点 「回答に必要な処理が存在する場合は、必ずツール呼び出しのみを返すこと」を明示したプロンプトで対処。 検証 本章では、前章で整理した通信設計が、実際の実装でもその通りに動作しているかを確認します。 初めにローカル環境で MCP サーバーを起動します。 PS C:\Users\katsu\Documents\github\mcp-sample\server> npm run start > mcp-server@1.0.0 start > node dist/index.js MCP server listening on http://127.0.0.1:8080/mcp 次に、同じくローカル環境で MCP ホストを起動します( MCP ホストはプロセス内部で MCP クライアントクラスの インスタンス を生成・管理しています)。 起動後はユーザーの入力を促すメッセージが出るので、依頼文(ユーザプロンプト)を入力します。 PS C:\Users\katsu\Documents\github\mcp-sample\host> npm run start > mcp-host@1.0.0 start > node --env-file=.env dist/index.js ################################################################## ------------ initialize request ------------ Content-Type: application/json Accept: application/json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "weather", "version": "0.1.0", "description": "Weather MCP Client with Stateless and Streamable HTTP and non SSE" } } } ------------ initialize response ------------ status: 200 { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "weather", "version": "0.1.0", "description": "Sample MCP Server with Stateless and Streamable HTTP and non SSE" } } } ################################################################## ------------ notifications/initialized request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "method": "notifications/initialized" } ------------ notifications/initialized response (no body) ------------ status: 202 ################################################################## ------------ tools/list request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ------------ tools/list response ------------ status: 200 { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_weather", "description": "Get weather forecast for US coordinates using NWS API.", "inputSchema": { "type": "object", "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" } }, "required": [ "latitude", "longitude" ] } } ] } } ################################################################## # 質問してみましょう 明日夜のニューヨークの天気を基に、服装のアドバイスをしてください。今は半袖短パンを想定しています。 ################################################################## ------------ LLM request ------------ Content-Type: application/json x-api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX anthropic-version: 2023-06-01 { "model": "claude-sonnet-4-5", "max_tokens": 1000, "system": "\nYou are a tool-routing assistant.\n\nABSOLUTE RULES:\n- You MUST respond in Japanese only.\n- Output MUST be a single valid JSON object.\n- Do NOT use Markdown.\n- Do NOT use code fences or triple backticks.\n- The first character MUST be \"{\" and the last character MUST be \"}\".\n- Do NOT include any text outside the JSON object.\n", "messages": [ { "role": "user", "content": "\nYou will be given:\n1) A list of available tools (JSON)\n2) A user request\n\nDecide whether a tool is needed.\n\nRules:\n- Use a tool only if it materially improves correctness or usefulness.\n- If multiple tools could work, pick exactly ONE.\n- Never invent tools or parameters.\n- If required parameters are missing, ask ONE concise follow-up question.\n- If answering accurately to user request requires external or real-world data, you MUST select the appropriate tool and make a tool_call.\n\nOutput format (JSON only):\n\nTool call:\n{\n \"type\": \"tool_call\",\n \"tool\": \"<tool_name>\",\n \"arguments\": { },\n \"reason\": \"<short reason>\"\n}\n\nDirect answer:\n{\n \"type\": \"direct_answer\",\n \"answer\": \"<answer>\",\n \"reason\": \"<short reason>\"\n}\n\nTools (JSON):\n[\n {\n \"name\": \"get_weather\",\n \"description\": \"Get weather forecast for US coordinates using NWS API.\",\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"latitude\": {\n \"type\": \"number\"\n },\n \"longitude\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"latitude\",\n \"longitude\"\n ]\n }\n }\n]\n\nUser request:\n明日夜のニューヨークの天気を基に、服装のアドバイスをしてください。今は半袖短パンを想定しています。\n" } ] } ------------ LLM response ------------ status : 200 { "model": "claude-sonnet-4-5-20250929", "id": "msg_0144BrLAY9HUWMcHmgopBxpp", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "```json\n{\n \"type\": \"tool_call\",\n \"tool\": \"get_weather\",\n \"arguments\": {\n \"latitude\": 40.7128,\n \"longitude\": -74.0060\n },\n \"reason\": \"ニューヨークの明日夜の天気予報を取得して、適切な服装アドバイスを提供するため\"\n}\n```" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 481, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "output_tokens": 104, "service_tier": "standard" } } ################################################################## ------------ tools/call request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "latitude": 40.7128, "longitude": -74.006 } } } ------------ tools/call response ------------ status: 200 { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Forecast for 40.7128, -74.006:\n\nTonight:\nTemperature: 13°F\nWind: 14 to 22 mph N\nMostly Cloudy\n---\nSunday:\nTemperature: 25°F\nWind: 17 to 22 mph N\nMostly Cloudy\n---\nSunday Night:\nTemperature: 16°F\nWind: 14 to 17 mph NW\nMostly Cloudy\n---\nMonday:\nTemperature: 32°F\nWind: 14 mph NW\nSunny\n---\nMonday Night:\nTemperature: 16°F\nWind: 8 to 12 mph NW\nMostly Clear\n---\nTuesday:\nTemperature: 31°F\nWind: 9 mph W\nSunny\n---\nTuesday Night:\nTemperature: 22°F\nWind: 6 to 10 mph W\nPartly Cloudy\n---\nWednesday:\nTemperature: 30°F\nWind: 9 mph NW\nMostly Sunny\n---\nWednesday Night:\nTemperature: 17°F\nWind: 10 mph NW\nMostly Clear\n---\nThursday:\nTemperature: 29°F\nWind: 10 mph NW\nSunny\n---\nThursday Night:\nTemperature: 15°F\nWind: 10 mph NW\nPartly Cloudy\n---\nFriday:\nTemperature: 30°F\nWind: 8 to 12 mph W\nPartly Sunny then Slight Chance Light Snow\n---\nFriday Night:\nTemperature: 18°F\nWind: 12 to 15 mph W\nChance Light Snow\n---\nSaturday:\nTemperature: 22°F\nWind: 16 to 21 mph NW\nChance Light Snow then Mostly Sunny\n---" } ] } } ################################################################## ------------ LLM request ------------ Content-Type: application/json x-api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX anthropic-version: 2023-06-01 { "model": "claude-sonnet-4-5", "max_tokens": 1000, "system": "\nYou are a Japanese assistant.\n\nABSOLUTE RULES:\n- You MUST respond in Japanese only.\n", "messages": [ { "role": "user", "content": "\nYou will be given:\n1) A list of API result\n2) A user request\n\nAnswer user request with API result.\nDo not mention that you are referring to the API results.\n\nAPI Result (JSON):\n{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Forecast for 40.7128, -74.006:\\n\\nTonight:\\nTemperature: 13°F\\nWind: 14 to 22 mph N\\nMostly Cloudy\\n---\\nSunday:\\nTemperature: 25°F\\nWind: 17 to 22 mph N\\nMostly Cloudy\\n---\\nSunday Night:\\nTemperature: 16°F\\nWind: 14 to 17 mph NW\\nMostly Cloudy\\n---\\nMonday:\\nTemperature: 32°F\\nWind: 14 mph NW\\nSunny\\n---\\nMonday Night:\\nTemperature: 16°F\\nWind: 8 to 12 mph NW\\nMostly Clear\\n---\\nTuesday:\\nTemperature: 31°F\\nWind: 9 mph W\\nSunny\\n---\\nTuesday Night:\\nTemperature: 22°F\\nWind: 6 to 10 mph W\\nPartly Cloudy\\n---\\nWednesday:\\nTemperature: 30°F\\nWind: 9 mph NW\\nMostly Sunny\\n---\\nWednesday Night:\\nTemperature: 17°F\\nWind: 10 mph NW\\nMostly Clear\\n---\\nThursday:\\nTemperature: 29°F\\nWind: 10 mph NW\\nSunny\\n---\\nThursday Night:\\nTemperature: 15°F\\nWind: 10 mph NW\\nPartly Cloudy\\n---\\nFriday:\\nTemperature: 30°F\\nWind: 8 to 12 mph W\\nPartly Sunny then Slight Chance Light Snow\\n---\\nFriday Night:\\nTemperature: 18°F\\nWind: 12 to 15 mph W\\nChance Light Snow\\n---\\nSaturday:\\nTemperature: 22°F\\nWind: 16 to 21 mph NW\\nChance Light Snow then Mostly Sunny\\n---\"\n }\n ]\n}\n\nUser request:\n明日夜のニューヨークの天気を基に、服装のアドバイスをして ください。今は半袖短パンを想定しています。\n" } ] } ------------ LLM response ------------ status : 200 { "model": "claude-sonnet-4-5-20250929", "id": "msg_011ykwGSo5qRzEHTQpD9UV9G", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "明日夜のニューヨークの天気は以下の通りです:\n\n**日曜日の夜**\n- 気温:16°F(約-9°C)\n- 風:14〜17 mph 北西 の風\n- 天候:ほぼ曇り\n\n**服装のアドバイス:**\n\n半袖短パンは**絶対に避けてください**。この気温では命に関わる危険性があり ます。\n\n以下の装備が必要です:\n\n1. **厚手の冬用コート**(ダウンジャケットやウールコートなど)\n2. **重ね着**(長袖のイン ナー、セーター、フリースなど複数枚)\n3. **厚手の長ズボン**(ジーンズやウールパンツに加えて、可能であればレギンスなどの下着も)\n4. **防寒アクセサリー**:\n - 帽子(耳まで覆えるもの)\n - 手袋(厚手のもの)\n - マフラー\n - 厚手の靴下\n5. ** 防風性のある上着**(風が強いため)\n\n氷点下の極寒の天候ですので、肌の露出は最小限に抑え、しっかりと防寒対策をしてお出かけく ださい。" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 713, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "output_tokens": 382, "service_tier": "standard" } } ################################################################## 明日夜のニューヨークの天気は以下の通りです: **日曜日の夜** - 気温:16°F(約-9°C) - 風:14〜17 mph 北西の風 - 天候:ほぼ曇り **服装のアドバイス:** 半袖短パンは**絶対に避けてください**。この気温では命に関わる危険性があります。 以下の装備が必要です: 1. **厚手の冬用コート**(ダウンジャケットやウールコートなど) 2. **重ね着**(長袖のインナー、セーター、フリースなど複数枚) 3. **厚手の長ズボン**(ジーンズやウールパンツに加えて、可能であればレギンスなどの下着も) 4. **防寒アクセサリー**: - 帽子(耳まで覆えるもの) - 手袋(厚手のもの) - マフラー - 厚手の靴下 5. **防風性のある上着**(風が強いため) 氷点下の極寒の天候ですので、肌の露出は最小限に抑え、しっかりと防寒対策をしてお出かけください。 上記の結果から、想定通りの通信が発生し、回答も外部サービスを使用した結果が含まれていることが確認できました。 まとめ 外部のサービスやデータを用いて、あたかもLLM自身が処理を行ってタスクを実施し、回答しているように見せる仕組み全体を、AIエージェントと呼びます。 その中で、外部サービス(ツール)呼び出しの形式を標準化したものがFunction Callingや MCP です。 特に MCP で定義された コンポーネント の役割は以下のように分類できます。 MCP ホスト:1つ以上の MCP クライアントを管理し、LLMと対話しながら処理をする アプリケーション全体 。 MCP クライアント: MCP ホスト内部に存在し、 MCP プロトコル に基づいて MCP サーバと通信を行う コンポーネント 。 MCP サーバ:実際に 外部サービスとの連携を行う コンポーネント で、 MCP プロトコル に基づいて MCP クライアントと通信する 。 実体がWebアプリと同じようなクライアント・サーバ構成である以上、 MCP サーバの配置場所や実行させる処理内容によっては、セキュリティや監査の扱いが非常に重要になります。例えば、ローカルで実行した MCP サーバが意図せず内部ファイルに干渉してしまうケースや、外部に公開した MCP サーバがセキュリティ攻撃にさらされる可能性などが考えられます。 これは、これまでの Web サービスと本質的には同じであるため、適切な アーキテクチャ 設計と セキュリティポリシー の策定が、今後ますます定式化されていく領域だと感じています。 おわりに 佐藤さんの助けもあり、これまで実態がつかめていなかった「AI エージェント」という概念を掴みつつ、実装しながら理解を深められたのはとても面白い体験でした。 このエージェントをさらに抽象化した仕組みとして Strands が登場しているので、次はそちらの実装を検証してみたいと思います。 今後も抽象化のレイヤーはどんどん高まっていくはずなので、流れに取り残されないためにも、AI 関連技術の基礎知識を学びつつ、使用者側としても継続的に研鑽していきます。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
こんにちは、 電通 総研 XI本部 サイバーセキュリティテクノロ ジー センターの櫻井です。 本記事では 代表的な クラウド サービスプロバイダ(以降、CSPと略)である AWS ( Amazon Web Services )、 Microsoft Azure、 Google Cloudの最上位アーキテクト資格に関する比較を紹介します。 なお、本記事でご紹介する資格の情報は2026年1月時点のものとなります。 アーキテクト資格の種類 筆者の経歴 チャレンジのおススメ順 各CSP最上位資格で共通で問われる内容 各CSP最上位資格の特色 まとめ 最後に アーキテクト資格の種類 各CSP資格群はFoundational、Associate、Professiona(Expert)lの3段階に分かれており、受験者の対象と試験の大枠としては以下を想定しています。 Foundational 対象: クラウド 初心者やIT未経験者。また、基礎知識を理解したい営業・ マーケティング ・プロジェクトマネジメントサイドの担当者 試験の大枠: クラウド 自体の理解、大枠のサービス概要、 クラウド 利用料計算の理解 Associate 対象:ソリューション設計に1年以上の実務経験のあるITエンジニア 試験の大枠:設計に用いるソリューションの基本理解 Professional(Expert) 対象:ソリューション設計に理解のある、実務経験のあるITエンジニア 試験の大枠:設計時のソリューションベストプラクティスの理解、適切なサービス組み合わせの理解 それぞれの資格群の名称(略称)に関しては以下の表のとおりです。 Grade AWS Azure Google Foundational Cloud Practitioner(CLF) Azure Fundamentals(AZ-900) Cloud Digital Leader(CDL) Associate Solutions Architect Associate(SAA) Azure Administrator(AZ-104) Associate CloudEngineer(ACE) Professional(Expert) Solutions Architect Professional(SAP) Azure SolutionsArchitect Expert(AZ-305) Professional Cloud Architect (PCA) 以降は、Professional、Expert資格にフォーカスして説明を行います。 筆者の経歴 (テンプレにはなりますが、)オンプレ& クラウド 、ネットワーク&セキュリティのごちゃまぜエンジニアです。担務領域に関しては特にこだわりなくなんとなく興味を持った資格は取得してます。(※ My credly ) 電通 総研ではサイバーセキュリティテクノロ ジー センターで主にセキュリティサービス設計、運用やマルチ クラウド のセキュリティ設計レビューに従事しています。 また、筆者の各CSPの経験歴としては以下となります。 AWS 7年程度、メインは クラウド インフラ設計ガッツリです。オンプレ接続のために用いる クラウド リソース管理や運用/監視、リソースデプロイの自動化がメインタスクでした。 電通 総研に入社後はアプリまで含めた AWS 全般のセキュリティ設計レビューを行っています。 Microsoft Azure 5年程度、メインは クラウド マイグレーション に関わるインフラ設計、ID( Microsoft Entra)の設計・運用でした。 電通 総研に入社後は Microsoft defender for cloudをはじめとしたセキュリティサービスの調査や社内向けサービス設計に従事しています。 Google Cloud 3年程度、以前はBigQueryやApp Engineを触っていた程度です。 電通 総研に入社後は Google Cloudログサービスの調査・検証の実施、Firebaseを利用したWebサイト構築のセキュリティ設計レビューにも参加しています。 チャレンジのおススメ順 最初に、3種すべてを取得するならばの前提で筆者の考える難易度を記載します。各試験で問われる範囲と出題形式、傾向、学習コンテンツを踏まえて、以下の順が望ましいと考えています。 Google Cloud(Professional Cloud Architect) AWS (Solutions Architect Professional) Microsoft Azure(Azure Solutions Architect Expert) はい、見事に筆者のチャレンジ順とは逆です。 単一CSPでガッツリ設計をやっている方は専門の分野を突き詰めるのが望ましいのであまり難易度比較に意味はありませんが、基本は下に行くほど難しい試験である(事前準備が必要である)と筆者は考えています。 各CSP最上位資格で共通で問われる内容 最上位資格の試験について、共通で必要とされる内容を簡単に整理します。 対象CSPのサービス全般に対する理解① 対象のサービス種類(試験に含まれるサービスの範囲) CSPの試験ガイドで具体的に対象と記載されているサービスについてすべてが対象となります。 例として、 AWS (Solutions Architect Professional)であれば 試験ガイド の「範囲内の AWS のサービスと機能」となっているサービスはすべて対象です。 各試験によってどこまでを対象とするかは異なりますが、ITサービス構築のために クラウド インフラを利用するだけではなじみのないAIやデータ分析のサービスも含まれますので、普段利用していないサービスでも用途等は理解しておく必要があります。 対象CSPのサービス全般に対する理解② サービスの単位 「 AWS における VPC はリージョナルサービスである」、「 Google Cloudにおける VPC はグロー バルサ ービスである」という比較に代表されるように クラウド 設計におけるサービスがどこまでの範囲で展開可能かを明確に覚えておきましょう。 対象としているCSPにフォーカスしてきちんと学習しましょう。実務目線でも設計や提案の際に理解できていないと致命的です。 例えば、複数リージョンに渡る負荷分散の設計方法は各CSP毎に大きく異なる等ですね。 (この場合、グローバルロードバランシングの種類をちゃんと理解しているか?になります) 設計の理解① 設定パラメータ 利用ケースが多いサービス(コンピュート、コンテナ、アプリケーション統合)については細かい設定が問われる問題が出題されます。この点に関しては明確に実務経験があった方が望ましいといえます。 対象サービスのパラメータを設定するのに、どういった方法があるのかも含めて押さえておくとよいです。 1.「環境を作りなおす必要があるのか?」もしくは「既存の環境のままで設定変更が可能か?」 2.「マネジメントコンソールから設定できるか?」もしくは「 API や CLI からのみ設定できるのか?」 設計の理解② ソリューションの選定、ソリューションの組み合わせ比較 出題内容としては、具体的なソリューションを一意に特定するケースに加えて、必要な要件に対して複数のソリューションの組み合わせを比較した上で最も適したものを選択するケースも存在します。 いわゆるベストプラクティスを選択する必要がありますので、 AWS の場合は Black Belt の様な公式のソリューションガイドを参考に学習するとよいです。 筆者の経験でも、顧客の特殊要件を踏まえ意図的にベストプラクティスを選択しなかった経験もありますし、当時と比較して新しいサービスがリリースされている可能性もありますので、最低限は確認しましょう。 各CSP最上位資格の特色 それぞれの試験の概要と筆者の試験自体に関する印象、筆者の主観的分析を紹介します。 AWS Solutions Architect Professional(SAP) 問題数:75問、時間:180分 試験後の印象 とにかく問題数が多い。そして問題文、選択肢の文章が比較的長いので持続的な集中力を求められる。 AWS のProfessional、Security分野の試験では共通している点かと思います。 主観的分析 AWS に関してはCSPがリリースしているサービスの種類が他の2社と比べて多いです。(200個以上) 時間をかけて理解する必要があり、初学者は数に圧倒されると思います。 問題そのものは取り組みやすいですが、文章量の理解に時間を要するため練習問題等で慣れておく必要があります。 SAPを含め、 AWS 資格のコンテンツは数多く存在するためこの点は学習者にプラスです。 Azure Solutions Architect Expert(AZ-305) 問題数: 不定 (概ね50問前後)、時間:120分 試験後の印象 設計の組み合わせやサービスの詳細など細かい部分まで回答を求められる。(理由は後述) 試験への慣れが必要。3パートに分かれており、2パートは選択を終了したら戻ることができない。 本試験はダイレクトに「 クラウド インフラ設計」の理解を求められる。 Microsoft Webページ の記載の通り、複数系統に分かれているためです。 主観的分析 CSP3種試験の中では難易度は最も高いと思います。 設計にフォーカスしているため、サービス単体を理解しているだけではNGなケースが多いです。 Microsoft AzureのAssociate以上の試験に関しては Microsoft Learn でコンテンツを検索しながら回答が可能です。 その点に起因するかはわかりませんが、事前の学習では手が回らないような調べないとほぼ回答できないような細かいサービス詳細に関する問題も一部出題されます。(この要件を満たすライセンスの種類は?というような問題) 購入した問題集の設問で構わないのでこんな感じで検索したら出てきそうというのを Microsoft Learnで事前に確認しておくとよいです。関係のないコンテンツも検索結果に含まれてしまい、思いのほか抽出が難しいです。 40問程度の大問パート以外にシナリオ問題とケース問題で合計3パート以上で構成され、パート間で戻ることはできないため、時間配分を間違わないようにしましょう。(筆者はAZ-305以外の試験で勘違いし、ドボンしています) 市販の学習用コンテンツは最低限は存在します。 Microsoft Azureは Microsoft Learn以外にも公式の トレーニングコース が充実しているため、集中的に学習したいものは登録しましょう。 開催日程が決まっていますが、 Microsoft Virtual Training Days で無料で受講できるハンズオンや講習も存在するので確認しましょう。 Google Cloud Professional Cloud Architect(PCA) 問題数: 不定 (50問以上、60問未満)、時間:120分 試験後の印象 一定以上の知識は必要だが、複雑な問題はあまり出題されない。 サービスの種類と設計をバランスよく理解しておくことが求められる。 数問で構成されるケース問題が複数出題される。全く関係ない導入文も含まれるので選択をミスらないようにしましょう。 試験中に問題言語を英語に切り替えられない。筆者だけかもしれませんが、細かいオプション説明を英字で確認しようと切り替えても日本語のままでした。 主観的分析 個人的な忖度ではないですが、試験自体は一番取り組みやすいと感じました。試験時間と問題文、文章量がマッチしていると思います。 ACEとは別にProfessionalとなっているので簡単ではないですが、適度な難易度の問題が適量出題されるイメージです。 難点は学習コンテンツが少ないことかと思います。PCAに限っては市販の書籍は見当たりませんでした。 Udemyのコンテンツで日本語対応のもの(※1、※2)がありますのでこちらをお勧めします。 筆者の感覚では既にACEを取得済みであれば、Udemyのコンテンツを繰り返すだけでも合格することは十分可能だと思います。(自信がない方はラボ演習をしましょう) 所属する会社によって変わりますが、 Google Cloudに関する招待制ト レーニン グプログラムも存在します。無料でラボコンテンツが一定期間利用できるプログラムです。 ※1 https://www.udemy.com/course/google-cloud-professional-cloud-architectpca/ ※2 https://www.udemy.com/course/google-cloud-professional-cloud-architect-i/ まとめ 以上を踏まえて、それぞれの試験を試験自体の難易度(筆者の感覚)、試験への慣れの必要性、学習コンテンツの豊富さで整理したいと思います。(勉強時間は各個人によるので含めません) 評価観点 AWS (SAP) Azure(AZ-305) Google (PCA) 試験自体の難易度(筆者感覚) 普通 (サービスの数は多いが基本的なサービス組み合わせ、設計等を理解できていれば問題なし。) 難しい (ひとえに難しい。細かいところまで出題される。) 比較的取り組みやすい (サービスの種類を覚え、組み合わせや設計が理解できていれば問題なし。) 試験への慣れの必要性 ある程度必要 (問題数、文章量への慣れが必要。) 慣れが必要 (最終的にExpertとなるためにはAZ-104の合格も併せて必要なので先に受験し訓練しましょう。) ほぼ不要 (初チャレンジでもケース問題の切り替えに注意すれば問題はないと思います。) 学習コンテンツの豊富さ 豊富 (ググっても、 Amazon で検索しても、Udemyでも大量にあります。むしろ精査が難しいレベル。) 普通 (発売されているものは少ないですが、良書が多いので一度は目を通しましょう。) 少ない (市販書籍がないのでUdemy等を活用しましょう。) 表からチャレンジ順の理由がご理解いただけたかと思いますが、AZ-305に関しては、事前に準備しないといけないもの(知識も当然だが、問題形式や回答方法の予習)が多く、3種横並びで見た場合で最初にチャレンジするのは難易度が高いという評価です。ただ、 クラウド インフラ設計という点からは逸脱している内容ではないので、学んでおくことでプラスになる内容であると思います。 最後に 読了いただきありがとうございました。 筆者は事前の積み重ねもあって3種のArchitect最上位資格を約半年間で取得できました。しかし、エンジニア(もしくは コンサルタント )としてのゴールは資格を取得することではありません。 基本は クラウド への理解を深めるためにCSPの提供するラボ等を活用し、じっくり学習することをお勧めします。 今後も クラウド 、セキュリティに関する投稿を行っていきますのでよろしくお願いします。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakurai.ryo レビュー: @kobayashi.hinami ( Shodo で執筆されました )
アバター
はじめに 金融IT本部 2年目の坂江 克斗です。 業務にてアウトバウンドセキュリティを考えるタイミングがあったため、本記事を書きました。 初学者の視点で疑問に感じる部分も含め、アウトバウンドセキュリティ全体の基本的な概念を解説できればと思います。 はじめに アウトバウンドセキュリティの概要 AWSのアウトバウンドセキュリティサービス Route53 Resolver DNS Firewall Network Firewall Network Firewall Proxy アウトバウンドセキュリティの設計方針 Deny List (拒否リスト) Strict Allow List (厳格な許可リスト) Generous Allow List (寛容な許可リスト) まとめ おわりに アウトバウンドセキュリティの概要 Webアプリケーション開発においては、 SQLインジェクション や XSS 、DDoS など、インバウンド通信に対するセキュリティを意識する機会が多い一方で、アウトバウンド通信のセキュリティについては、あまり意識されないことが多いように感じます。 しかし、 AWS re:Inforce 2025 - Outbound network controls made easy でも述べられているとおり、アウトバウンドセキュリティは 防御(Prevention) と 可視性(Visibility) の両面において、近年ますます重要になっています。 防御の観点では、以下のケースが挙げられます。 マルウェア に感染したサーバから外部の悪意あるエンドポイントへの通信を防止。 ソフトウェア サプライチェーン において、意図しない参照先や汚染された OSS へのアクセスを制御。 直接的な通信遮断に至らない場合であっても、 不正アクセス や異常な通信パターンを検知する機会の増加。 可視性の観点では、以下のケースが挙げられます。 NAT Gateway と VPC エンドポイントの使い分けのような設定ミスの検知 SOC(Security Operations Center)における分析・調査のためのセキュリティログや分析材料の充実。 よって、防御と可視性の両面において、アウトバウンドセキュリティが重要であることが分かります。 アウトバウンドセキュリティは大枠として、 DNS ベース、パケットヘッダ(3層-7層)ベース、ボディベースでのフィルタリング が中心となり、組み合わせることで多層防御を形成します。(ただし、ボディの解析は処理負荷があるため、セキュリティと性能のバランスを考慮した設定が必要となると思われます) AWS では以下の表に示すアウトバウンドセキュリティ対策が挙げられ、中でもマネージドに提供されているものとして Route 53 Resolver DNS Firewall ( DNS ベース) および Network Firewall (L3 - L7パケットヘッダベース) 、 AWS Network Firewall Proxy (L3 - L7パケットヘッダベース) が存在します。 レイヤ 制御観点 AWS サービス 料金 特徴 DNS (名前解決) ドメイン 名 Route53 Resolver DNS Firewall 低( DNS クエリ) DNS クエリを制御。ローカルでの名前解決やIP直打ち、独自 DNS 使用の通信は防御不可。 L3–4 IP / Port SG / NACL 無料 ファイアウォール 。ネットワークセキュリティのベースとなるフィルタリング。 L3–7 IP / Port, TCP ヘッダ / HTTP ヘッダ / TLS ヘッダ Network Firewall , GatewayLoadBalancer + サードパーティ ソフト 高(AZ毎の常時稼働エンドポイント+処理量) 柔軟な制御が可能で、マネージドにも運用可能。 DNS + L3–7 IP / Port, HTTP ヘッダ / TLS ヘッダ AWS Network Firewall Proxy 調査中 マネージドな フォワ ードプロキシサービス。現状はパブリックプレビュー中で無料使用可能。 AWS のアウトバウンドセキュリティサービス Route53 Resolver DNS Firewall Route 53 Resolver は、 VPC のCIDRブロックにおける +2 の 予約済み IPアドレス で提供されるマネージドリゾルバ です。例えば、10.0.0.0/16の VPC では、10.0.0.2がRoute 53 Resolver の IP アドレスとして割り当てられます。この IPアドレス 自体は VPC 内で共通ですが、実際に DNS クエリを処理するリゾルバのエンドポイントはAZごとに配置されており、高可用性を考慮した構成となっています VPC 内のアプリケーションからこの Route 53 Resolver を経由して送信される アウトバウンド DNS クエリに対して VPC 単位でフィルタリング を行うサービスが、Route 53 Resolver Firewall となります。 ドメイン リストを基に ALLOW / BLOCK / ALERT を設定することで、 特定の ドメイン 群に対する通信を許可・拒否、またはアラートとして検知 することができます。 また、BLOCK の場合にはカスタム DNS レスポンスを設定することで、CNAME を用いて特定の ドメイン への名前解決に誘導することも可能です。 さらに、CNAME による変換後の ドメイン についても検証対象とするかを設定できるため、Allow List / Deny List のいずれにおいても柔軟な構成が可能です。 CNAME による変換後の ドメイン 検証 Deny List 構成では、CNAME 変換後の ドメイン までを検証対象とすることで、悪質な ドメイン への名前解決を防止できます。 Allow List 構成において CNAME 変換後の ドメイン まで全て許可対象とする場合、管理すべき ドメイン 数が増加し、運用負荷が高くなる傾向があります。そのため、運用方針によっては CNAME 変換後の検証を行わない設定とし、管理コストを抑える選択肢も考えられます。 Network Firewall AWS Network Firewall は、 AWS マネージドの ファイアウォール および IDS/IPS サービスであり、インバウンド / アウトバウンドに対して サブネット単位で設定したルール(ステートレスルール / ステートフルルール)に基づいて、パケットを ALLOW / DROP / ALERT に分類し制御 します。 ( 1つのNetwork Firewallを複数のVPCで共有できる機能 も公開されています) ステートレスルールは、全てのパケットを個別に評価して判定を行います。一方、ステートフルルール(Suricata互換)では、 TCP や TLS 、HTTP などの一連の通信(コネクション)を単位として制御することが可能です。 例えば、ステートレスルールでは TCP の 3 ウェイハンドシェイクを構成する各パケットをそれぞれ独立してフィルタリングしますが、ステートフルルールでは TCP の通信全体をひとまとまりとして扱い、コネクションの状態(established か否か)を追跡しながらフィルタリングを行います。 Network Firewall では、上図に示すようにルートテーブルの設定によって、 検査対象となる通信の往路・復路の両方を同一の Firewall Endpoint に通過させる ことで、ステートフルな 通信制 御を実現しています。( 非対称ルーティングの回避 ) これは、各 Firewall Endpoint が通信状態を個別に管理しており、 TCP や HTTP などのハンドシェイクにおいてリクエストとレスポンスの両方を確認することで、通信が正しく確立されたものであるかを判断する必要があるためと考えられます。 今回は比較的シンプルな構成を例に紹介しましたが、 Network Firewallのデプロイモデル には複数のパターンが存在します。 実際の設計においては、通信要件や可用性、運用負荷を考慮したうえで、適切なモデルを選択することが重要です。 Network Firewall と Gateway Load Balancer(GWLB) Network Firewall は、内部的に Gateway Load Balancer と連携して動作するマネージドサービスです。 Gateway Load Balancer は、GENEVE プロトコル を用いて サードパーティ 製のインスペクションサービスへ トラフィック をトンネリングする仕組みを提供するもので、インバウンド / アウトバウンド のセキュリティ対策に利用できます。 一方で、Network Firewall を利用することで、こうしたインスペクション機能そのものを AWS マネージドサービスとして構成できるため、構築・運用負荷を大きく下げることができます。 Network Firewall Proxy 2025年11月末にマネージドな フォワ ードプロキシサービスである AWS Network Firewall Proxy が発表されました。現在はパブリックプレビュー中のため、 オハイオ (us-west-2)リージョン限定で利用可能となっています。 厳密にはNetwork Firewall の機能の一部に当たりますが、役割の明確化のために分けて説明いたします。 AWS Network Firewall Proxy はいわゆるWebプロキシのように、 HTTP / HTTPS 通信専用のマネージドな フォワ ードプロキシ であり、Route 53 Resolver DNS Firewall や Network Firewall と同様にアウトバウンドセキュリティに適した AWS サービスとなります。 以下の構成( Architecture overview より引用)により使用可能であり、NAT ゲートウェイ と統合して動作するものとなっています。 本サービスでは、以下の図( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように 名前解決前(Pre- DNS )、名前解決後の接続先サーバへのHTTPリクエスト(Pre-request)、HTTPレスポンス(Post-response)の3段階 で評価を行い、名前解決に使用する ドメイン 名からL3-L7(IP / Port, HTTP / TLS ヘッダ) までの情報を評価に使用可能となります。 詳細な挙動や設計上の注意点については、 こちらの記事 で紹介しておりますので、是非ご参照ください。 アウトバウンドセキュリティの設計方針 セキュリティ全体としては、セキュリティ効果と運用負荷のバランスを踏まえ、以下の 3 つの方針が重要となります。 多層防御を活用する 導入コストやリスクが低く、効果の高い対策から着手する 最初から完璧を目指さず、運用を通じて継続的に改善する アウトバウンドセキュリティにおける具体的な設計方針として、 AWS re:Inforce 2025 - Outbound network controls made easy で紹介されている考え方に基づき、 Deny List / Strict Allow List / Generous Allow List の 3 つの方針を紹介します。 Deny List (拒否リスト) 明示的に拒否する対象のみを定義する方式であり、以下のようなメリットがあります。 導入が容易 AWS ではマネージドな拒否リストを利用可能 拒否された通信はログとして記録されるため、通信内容の分析や調査が可能(なお、ALERT を挟んだ Allow List 構成においても、同様に情報を取得可能) 一方で、悪質な宛先は無数に存在するため、全てを網羅的に拒否リストへ登録することは現実的ではありません。 そのため、未知の通信先が許可されてしまう可能性が残り、 Allow List と比較するとリスク低減効果は限定的 である点がデメリットとなります。 2025年に公開された AWS Network Firewall with Active Threat Defense では、 AWS がグローバルに展開する独自の ハニーポット から収集した脅威情報を活用し、既知の悪意あるインフラに対する拒否ルールを動的に更新することが可能となっています。 これにより、従来の静的な Deny List が抱えていた運用負荷や追従性の課題を一定程度緩和できます。 また、 AWS Network Firewall の 地理的IPフィルタリング も、ルール管理を簡素化する観点から、同様に運用負荷の軽減に寄与する機能と言えます。 Strict Allow List (厳格な許可リスト) 明示的に許可する対象のみを定義する方式であり、以下のようなメリットがあります。 リスク低減効果が最も高い 通信先の追加・変更・削除時に、設計レビューや承認といった定められた運用手順を強制しやすい 一方、デメリットとしては以下の内容が挙げられます。 導入・運用コストが高い (アプリが依存する OSS 、 OSS 自体のアクセス先等、管理対象が多数) 運用コストの高さから、正当な通信の許可が抜けてしまい拒否するリスクがある 許可すべき通信が限定的なサービスにおいては非常に有効な手段となりますが、多様な外部サービスや API と連携する必要があるシステムに対しては導入コストも高く相性が悪い方式となります。 Generous Allow List (寛容な許可リスト) Allow List のリスク低減と Deny List の運用コストの低さを組み合わせた 、最も推奨される方針となります。 AWS Network Firewall においては、主に以下の方針で Deny List および Allow List を構成します。 Deny List AWS マネージドルール 既知の悪意のある宛先を拒否 高リスクな地理的 IP 高リスクな TLD 高リスクなポート IP 直指定による通信 ポートと プロトコル が一致しない通信 Generous Allow-List 信頼できる ドメイン リスト、以下例 *.gov (日本向けなら *.go.jp )、 *.edu 、us-*.amazonaws.com .<自社 ドメイン >、 .<信頼する他社 ドメイン > Generous Allow List では、初期段階では厳格に絞り込みすぎず、 比較的寛容な範囲で ドメイン リストを設定 します。 その上で、運用開始後に取得したログを基に継続的な評価と見直しを行い、Allow List / Deny List の精度を段階的に高めていくことが重要です。 このような運用を支援する手段として、 Reporting on network traffic in Network Firewall の機能を利用することで、 過去の通信ログから ドメイン 単位のアクセス状況を分析・可視化 し、ルール設計や改善に活用することができます。 まとめ アウトバウンド対策は DNS / L3–7(IP / Port / TCP / HTTP / TLS ヘッダ)/ ボディ のレイヤで整理すると理解しやすい。 AWS のマネージドな主要選択肢として、 Route 53 Resolver DNS Firewall / Network Firewall / Network Firewall Proxy が挙げられる。 次回以降の記事で詳細検証しますが、要件に応じた AWS サービスの主軸の使い分けは以下のイメージを持っています。 ドメイン フィルタ:Route 53 Resolver DNS Firewall + Network Firewall or Network Firewall Proxy UDP / TCP を含む評価:Network Firewall HTTP / HTTPS のみの評価:Network Firewall Proxy ただし、あくまでプロキシであるため、ベースのネットワークセキュリティの担保が必要。 Generous Allow List を基に、まずは低コストで効果の高い対策から導入し、ログを活用しながら継続的に改善していくことが現実的である。 おわりに 本記事では、 AWS におけるアウトバウンドセキュリティ全体の概要を整理しました。 今回は基礎的な内容を中心に取り上げましたが、今後は実際の設計・検証・運用を通じて、より実践的な理解を深めていきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @nagamatsu.yuji ( Shodo で執筆されました )
アバター
金融IT本部入社一年目の河岸歩希です。 この度、社内で実施されている 伴走型研修 に参加し、 AWS Certified Solutions Architect – Professional (以下SAP) を受験いたしました。本記事では、その学習過程で得た知見や経験を、僭越ながら受験体験記としてまとめさせていただきます。 これからSAP認定の取得を目指される方や、 AWS 業務未経験だけどSAPに興味をお持ちの方にとって、本記事が少しでも参考になれば幸いです。 前提 想定読者 著者情報 試験概要と伴走型研修について 試験概要(SAP-C02) 伴走型研修とは 伴走型研修の感想 結果と勉強時間 学習リソース AWS Solution Architect Professional (AWS-SAP-C02) 演習テスト Google AI Studio 問題を解いていて思ったこと 当日の立ち回り おわりに 前提 想定読者 これからSAPを受ける予定のある方 AWS 業務未経験だけど AWS の資格取得に興味がある方 資格勉強が好きな方 著者情報 学部卒の新卒1年目 大学では情報系の学部に所属 ( クラウド 技術には触れたことがないためほぼ未経験者) 会社に入ってから(4月) AWS を学び始める CLF(5月)とSAA(7月)をそれぞれ取得済み 業務では AWS を触っていない(触りたい) 試験概要と伴走型研修について 試験概要(SAP-C02) 試験の公式ガイドにも記載がありますが、この AWS Certified Solutions Architect – Professional認定は、 AWS を用いたソリューションの設計における2年以上の実務経験を持つ方を主な対象として設計されています。 私のような社会人1年目がこの資格を取得できたなら、それは 2年分の業務経験に相当する知識 や設計能力が身についた一つの証となり、自身のキャリアにおける大きな自信へと繋がるのではないかと考えております。 SAP認定の学習を始めるにあたり、皆様に最初におすすめしたいのは、まず公式の「試験ガイド」に必ず目を通し、それから学習計画を立てることです。 その理由は、試験範囲が他の認定と比較しても 非常に広範かつ多岐にわたる ためです。やみくもに学習を始めてしまうと、重要な分野に時間を割けなかったり、逆に試験の比重が低い部分に時間を使いすぎてしまったりする可能性があります。 以下は、その試験ガイドから引用した、出題範囲の主要な分野です。これらの分野に、それぞれどの程度の比重が置かれているかを把握することが、効率的な学習の第一歩となります。 本試験のコンテンツ分野と重み設定は、以下のとおりです。 コンテンツ分野 1: 複雑な組織に対応するソリューションの設計 (採点対象コンテンツの 26%) コンテンツ分野 2: 新しいソリューションのための設計 (採点対象コンテンツの 29%) コンテンツ分野 3: 既存のソリューションの継続的な改善 (採点対象コンテンツの 25%) コンテンツ分野 4: ワークロードの移行とモダナイゼーションの加速 (採点対象コンテンツの 20%) どの分野もまんべんなく出題されるため、偏りなく学習することが重要です。 試験時間と設問数、合格点に関しても、SAAより高く設定されています。 試験時間:180分 設問数:75 (うち10問が採点対象外) 合格点:750点 詳細は 試験ガイド を参照していただければと思います。 伴走型研修とは 当社では半期に一度、 AWS やAzureなどの資格取得に興味のある方を対象に伴走型研修が行われております。(要申し込み) 内容としては、外部講師との1on1(スキルや状況に応じた学習計画のアド バイス )、毎週30分の勉強会(試験問題の解説や情報共有)、専用Teamsでの相談や質疑応答などを通して、資格取得まで包括的なサポートを受けることができます。 怠惰な私にとってはピッタリの研修だと思い応募しました。 伴走型研修の感想 特によかったと思うのは、外部講師との1on1です。 9月からスタートして、11月末までの3か月間のうち、ほぼ隔週で1on1の時間を設けていただきました。 (スケジュールに関しても、柔軟に対応していただけました) この定期的な1on1を通じて、学習の進捗と次回の面談までの目標を設定するのですが、この目標設定が継続的な学習のモチベーション維持に大きく寄与したと思います。 また、勉強に際してのアド バイス もいただけたため、効率的に学習を進めることができました。 結果と勉強時間 結果↓ 無事SAPに合格することができました。 意外かもしれませんが、 試験で最も壁を感じたのは、SAPよりもアソシエイト(SAA)でした 。 もちろん、出題される問題の運にも左右されるため、一概には言えないことですが。 ご参考までに、私が各試験を初めて受けた際の 「難易度感」 を表すと、以下のようになります。 SAA >>> SAP >>>>> CLF = 基本情報技術者試験 こうして振り返ってみると、CLFからSAAへの難易度の上昇がいかに大きかったかを改めて感じます。 SAPの学習では、SAAで得た個々のサービスの知識を、より深く、実践的なシナリオに当てはめていく意識が重要だと感じました。 そのようなアプローチで学習を続けることが、合格への確かな一歩になるのではないかと思います。 学習リソース 主にUdemyとGoogleAIStudioを用いて学習を行いました。 AWS Solution Architect Professional ( AWS -SAP-C02) 演習テスト https://www.udemy.com/share/10bBe7/ 75問構成の模擬試験が5セット収録されているUdemyの問題集です。 この教材を選んだ理由は、問題の最終更新日が他の教材と比較して最も新しかったからです。 AWS のサービスは仕様変更が頻繁に行われるため、その最新の変更に対応して問題内容を随時更新してくれるコースを選ぶ ことが、非常に重要だと考えています。 難易度はかなり高めでした。 私自身、学習時間の都合で2周目には手が回らず、結果的に1周しかできませんでしたが、平均正答率は6割程度でした。 若干解説が薄い部分もありますが、この問題集だけで試験範囲は網羅できるので安心です。 Google AI Studio https://aistudio.google.com/prompts/new_chat もちろん、ChatGPTなど他のAIサービスでも全く問題ありません。 私が Google AI Studioをおすすめする理由は、無料で最新モデル(※2025年12月時点)が利用でき、一度に扱える情報量( トーク ン数)が多い点です。これにより、模擬試験の疑問点や不明点を余すことなく質問できるため、非常に効率的でした。 模擬試験を解き、解説を読んでも腹落ちしない部分があれば、すぐにこの Google AI Studioを開き、納得できるまで対話を繰り返す、というスタイルで理解を深めていきました。 問題を解いていて思ったこと 問題を解いていてふと思ったことがあります。それは 要件によって正解が変わる ということです。 当たり前のことのように聞こえるかもしれませんが、180分という時間の中で75問もの長文問題と向き合い続けると、どれだけ準備をしていても、どうしても集中力が途切れてしまう瞬間が訪れます。 そして、そういうときに限って、問題文の最も重要な 「制約条件」や「優先順位」 の部分をつい読み飛ばしてしまい、「選択肢のAもBも、技術的にはどちらも可能に見える…」という、判断に迷う状況に陥りがちです。 例えば、「モノリシックなWebアプリケーションを、サーバー管理から解放されるマネージドサービスへ移行したい」という典型的なシナリオを考えてみましょう。 この場合、頭の中にはいくつかの有力な候補が浮かびます。(実際の試験では、与えられた選択肢の中から判断します) 候補1: AWS Fargate ( Amazon ECS on Fargate) 候補2: AWS Lambda どちらもサーバーレスのマネージドサービスですが、どちらが「正解」になるかは、問題文に添えられた 「要件としては~」 といった一文にかかっています。 ケース1:要件が「コードの変更を最小限に抑え、迅速に移行したい」場合 この場合、正解は AWS Fargate になるでしょう。 モノリシックな既存アプリケーションをLambdaが要求するような小さな関数に分割するのは、大規模な再設計を伴うため、迅速な移行とは言えません。一方、コンテナ化してFargateに乗せる場合であれば、比較的少ないコード変更で移行を完了させることが可能です。 ケース2:要件が「長期的な運用コストを最適化し、スケーラビリティを最大化したい」場合 この場合、正解は AWS Lambda になる可能性が高まります。 アプリケーションを関数にまで分解する初期の移行コストは高くとも、完全な従量課金やイベント駆動のスケーラビリティといった、 クラウド ネイティブのメリットを最大限に活かせる長期的な視点を、この要件は重視しているからです。 2択までに絞れたが、何がまちがっているのかわからないようなケースに陥ったときは、一度冷静になって問題文を読み返し、 「コスト」 「スピード」 「運用負荷」 といったキーワードと、各選択肢を照らし合わせてみてください。 ユーザーが最も重視している要件に応えている方が正解です。 当日の立ち回り お手洗いと身分証は忘れずに。 私は身分証を忘れて家に取りに帰りました。 皆様にはぜひ落ち着いて持ち物を確認してから出発していただきたいです。 また、試験は長丁場になるので、合間に休憩を入れることをおすすめします。 おわりに 今回、伴走型研修に参加させていただいたことで、 AWS 未経験だった私でも、最後まで諦めることなく資格学習と向き合うことができたと実感しています。 一人では難しいモチベーションの維持に悩んでいる方にとって、このような学習環境は一度検討する価値が大いにあるかと思います。 SAPは確かに難関資格ですが、今回の経験を通じて、たとえ実務経験がなくても、コツコツと演習を重ねれば必ず合格に手が届く試験だと思いました。 本記事が、これから挑戦される一人でも多くの方の参考となり、合格への一助となれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kawagishi.ibuki レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
はい、こんにちはー!クロス イノベーション 本部 サイバーセキュリティテクノロ ジー センターの福山です。 2025年もReact2Shellなどを筆頭に多くの 脆弱性 情報が公表されました。 公開された 脆弱性 情報の傾向を1年単位で振り返ってみたいと思い、各種サイトの API を用いて収集し、分析してみました。 収集対象のデータ 2025年 分析結果 深刻度レベル別(2024/2025年) 深刻度レベル別(2025年月別) NVDステータス別 攻撃コード公開済み脆弱性の割合(PoC/MSF) 悪用確認済み脆弱性の割合(CISA KEV)/製品内訳(上位5製品) RCE可能な脆弱性の割合/悪用確認済みネットワーク機器内訳 CWE別件数(上位10位) CWE別リスク分布 CVE別/2025年最も危険な脆弱性4選 CVE別/今後悪用が広まる可能性が高い脆弱性4選 まとめ 収集対象のデータ 2025年に公表された約5万件のCVEを含む各種データを、 スクリプト で収集しました。 取得した情報の内容や抽出条件については以下を参照してください。 取得情報の一覧を表示 取得情報 概要 取得方法 参照元 CVE-ID 脆弱性 の一意の識別子。NIST(米国国立標準技術研究所)が運営するNVDに登録されたCVE番号を使用 2025年に公表されたCVE-IDを全件取得 NVD( https://nvd.nist.gov/vuln ) CVSS Base Score 基本評価基準に基づいて、 脆弱性 そのものの深刻度を数値(0.0〜10.0)で評価したスコア 対象CVE-IDにおける、CVSS v3.1(※1)のスコアを取得。取得優先度はNIST評価(Primary)>CNA(※2)評価(Secondary)で、両方未評価の場合は「N/A」とする。 NVD( https://nvd.nist.gov/vuln ) CVSS 深刻度レベル CVSS Base Scoreを基に分類した深刻度レベル(Low / Medium / High / Critical) 対象CVE-IDにおける、CVSS Base Scoreに基づく深刻度レベルを取得。取得優先度はNIST評価(Primary)>CNA評価(Secondary)とする。 NVD( https://nvd.nist.gov/vuln ) NVDステータス NVDにおける当該 脆弱性 の分析状況(例:Undergoing Analysis、Analyzed など) 対象CVE-IDにおける、NVDステータスを取得 NVD( https://nvd.nist.gov/vuln ) CWE-ID 共通 脆弱性 タイプ(Common Weakness Enumeration)の識別子 対象CVE-IDにおける、CWE-IDを取得。取得優先度はNIST評価(Primary)>CNA評価(Secondary)とする。 NVD( https://nvd.nist.gov/vuln ) RCE リモートから任意のコード実行が可能かどうか 筆者独自のフィルター。対象CVE-IDにおいて、Descriptionに大文字・小文字に関わらず「remote code execution」「execute arbitrary code」「arbitrary code execution」のいずれかのキーワードが含まれ、かつAttack Vector が「NETWORK」であるか判定 NVD( https://nvd.nist.gov/vuln ) CISA KEV 実際に悪用されているかどうか CISA のKEV(Known Exploited Vulnerabilities Catalog)への登録有無、ベンダー名、製品名を収集 CISA KEV( https://www.cisa.gov/known-exploited-vulnerabilities-catalog ) PoC PoC(概念実証コード)が公開されているかどうか 筆者独自のフィルター。PoC-in- GitHub のデータを基に判定(※ GitHub に存在するPoCのみ参照)。具体的にはZIPファイルを取得したうえで、全ファイルパスを走査し、CVE-IDが含まれる場合は抽出。 PoC-in- GitHub ( https://github.com/nomi-sec/PoC-in-GitHub ) MSF Metasploit Frameworkに当該 脆弱性 のモジュールが組み込まれているかどうか 筆者独自のフィルター。Metasploit Frameworkに対象CVE-IDを示すファイルが存在するかチェック。具体的にはZIPファイルを取得したうえでmodules/配下のファイルパスを走査し、CVE-ID形式の文字列があれば抽出。 Metasploit Framework( https://github.com/rapid7/metasploit-framework ) EPSS Score FIRSTが提供するEPSS(Exploit Prediction Scoring System)。今後30日以内に悪用される確率の予測値(0〜1) 対象CVE-IDにおける、EPSS Scoreを取得 FIRST EPSS( https://www.first.org/epss/ ) (※1)CVSS v3.1の上位バージョンであるCVSS v4.0が2023年11月に公開されたが、2025年に公表されたCVE全体の約25%しか評価されておらずNVDによる評価が進まないため、広く普及していない状況である。本記事ではCVSS v3.1をベースに解説を進める。 (※2)CNA=CVE Numbering Authorityの略称で、MITRE社から認定を受けて採番を行う組織のこと。 2025年 分析結果 ⚠️本集計は2026年1月中旬時点のデータに基づきます。 2025年に公表されたCVEの総数は 49,972 件でした。 2024年は40,704件であったため、 前年比で約23%増 、 9,000件近くの増加 となっています。 以降、分析結果の詳細です。 深刻度レベル別(2024/2025年) 2025年に公表された全CVEにおける、深刻度レベル別の件数です。 2024年と比較して 全体的に増加傾向 となっています。 2025年分は直近での集計ということもあり2024年分と比較すると差が出ていますが、それでも 未評価のCVEが前年比約4倍 と目立ちます。 NVDとしては、CNAによる評価の迅速化であったり、CWEの紐づけをLLMで自動化するなどの効率化(※)を図っているようですが、それでも対応が追いついていないことがわかります。 (※)参考: Vulnerability Root Cause Mapping with CWE 深刻度レベル別(2025年月別) 2025年に公表された全CVEにおける、月別/深刻度レベル別の件数です。 直近で公表されたCVEに未評価が多いのは当然ですが、12月は 未評価が1,500件 を超えています。 NVDではKEVに登録があるものを優先して評価(※)しているようですが、直近の 脆弱性 には対応が追いついていない状況が見て取れます。 今は未評価でも、分析が進むにつれてCriticalレベルと判明する 脆弱性 が出てくる可能性はありそうです。 (※)参考: The National Vulnerability Database (NVD) – Where It Is and Where It’s Going NVDステータス別 左が2024年、右が2025年に公表された全CVEにおける、NVD分析状況の割合を100%積み上げ棒グラフで示したものです。 (各ステータスの意味は下表を参照してください) 2024年分はAnalyzed(分析完了)と Modified(更新)を合わせて約80%の分析が完了しています。 一方、2025年分は分析完了の割合は前年並みを維持しているものの、 Awaiting Analysis(分析待ち)が約40% と大きく目立ちます。 ここについては今後分析が進んでいくと思われますが、現状では情報の確定に時間を要していることが伺えます。 また、 Rejected(却下)されたCVEが前年比で2倍以上 に増加している点も注目すべき変化です。 背景には、特定のライブラリに起因する 脆弱性 が製品ごとに乱立した際のCVEの統合や、昨今話題となっているAIツールを用いた大量かつ低品質な 脆弱性 報告の増加(※)といった、NVDを取り巻く環境の変化が影響していると考えられます。 (※)参考: NVD's HUGE Backlog: Vulnerability Crisis Explained ステータス名 意味 解説 Received 受理 NVD登録初期状態 Awaiting Analysis 分析待ち CVEの情報は公開されているが、NVDによるCVSSスコアやCWEの紐付けなどの詳細分析を待っている状態 Undergoing Analysis 分析進行中 CPE (情報システムを構成する、ハードウェア・ソフトウェアなどの識別子)やCWEの付与を行っている最中の状態 Analyzed 分析完了 NVDによる詳細分析がすべて完了した状態 Modified 修正 分析完了後に、新しい情報が追加・更新された状態 Deferred 延期 分析の優先順位が下げられた状態 Rejected 却下 脆弱性 ではないと判明した、あるいは既存のCVEと重複していた等の理由で、CVE-ID自体が取り消された状態 攻撃コード公開済み 脆弱性 の割合(PoC/MSF) 2025年に公表された全CVEにおける、PoCが公開されているCVEの割合は 2.47% で、 1,234件 ありました。 GitHub 以外やプライベー トリポジ トリのPoCも含めると、実際にはこの数値より膨らむものと推測されます。 また、上記のうちMetasploit Frameworkでモジュール化されているCVEの割合は 4.04% で、 50件 でした。 攻撃コードがモジュール化されている 脆弱性 は、高度な技術を持たない攻撃者でも簡単に悪用できる可能性があるため、特に注意が必要です。 悪用確認済み 脆弱性 の割合( CISA KEV)/製品内訳(上位5製品) 左の円グラフは2025年に公表された全CVEのうち、 CISA KEVに登録された割合を示しており、全体の 0.33% でした。 件数にするとわずか167件ですが、これらは理論上のリスクではなく、実際に悪用されていることが確認されているため、優先的に対応すべき重大な 脆弱性 として取り扱う必要があります。 右の円グラフは、その中でも悪用報告が多かった製品の内訳です。 Windows が17.37% と最多なのは、多くの企業で共通基盤として使われており、攻撃者にとって最も効率よく広範囲に影響を及ぼせるためと考えられます。 また、上位にはFortinetやIvantiといったネットワーク機器も並んでいます。 VPN などの境界を守る機器は、一度突破されると組織内への侵入や権限奪取に直結するため、OSと同等に執拗な標的となっていると考えられます。 RCE可能な 脆弱性 の割合/悪用確認済みネットワーク機器内訳 2025年に公表された全CVEにおいて、リモートコード実行(RCE)可能と判断されるものは、全体の 3.25% でした。 リモートから悪意のあるコードを実行された場合、情報窃取や攻撃の起点となるリスクが高く、特に注意が必要です。 さらにその中でも悪用確認済みの 脆弱性 に絞ると、 ネットワーク機器の 脆弱性 が3割 を超えています。 なお、これらの 脆弱性 のうち CWE-787(境界外書き込み) に分類される 脆弱性 は約4割と最も多く占めていることもわかりました。 これらの境界機器のRCEは、認証を介さず外部から直接システム権限を奪取できるものが多いため、昨今の ランサムウェア 攻撃において最も警戒すべき侵入経路とされています。 CWE別件数(上位10位) 2025年のCWE別件数の上位10タイプを、2025年(オレンジ)と2024年(グレー)で比較しました。 CWE-79( クロスサイトスクリプティング )が不動の1位となっています。 また、最も件数が増加したのは CWE-74(インジェクション)で1,169件増加 、一方で最も件数が減少したのは CWE-787(境界外書き込み)で843件減少 となりました。 CWE-74については、2025年に暫定策として取り入れられたGap Filling(CNAから提出されたCVSSおよびCWEデータを再検証せずに、一時的に受け入れるもの)による登録促進(※)の影響で、CNA側で付与したCWE-74が精査を待たずに大量に登録されたことが、増加の主な要因ではないかと推測しています。 (※)参考: The National Vulnerability Database (NVD) – Where It Is and Where It’s Going CWE別リスク分布 前項では件数に着目しましたが、ここでは筆者独自の観点で、リスクベースで分析してみます。 下図のバブルチャートでは、バブルの大きさが2025年に公表されたCVE総数を表し、X軸にRCE可能なCVE件数、Y軸に CISA KEVに登録されたCVE件数でプロットしたものです。 その中でもRCE可能な件数が50件以上、KEV登録数が10件以上のCWEに注目し、右上の領域にプロットされたCWEを下表にリストアップしました。 これらの 脆弱性 タイプは、 段階を踏まずインターネット越しから任意のコマンドを実行できる割合が高く、悪用実績も多数報告されている ため、特に注意が必要なカテゴリと言えるでしょう。 一方で最もサイズが大きいバブルはCWE-79ですが、単体でのRCEやKEV登録数は相対的に低めです。 CWE-ID 名称 解説 CWE-787 境界外書き込み メモリ操作時の不備により、本来の範囲を超えてデータを書き込んでしまう 脆弱性 のタイプ CWE-502 信頼できないデータのデシ リアラ イゼーション 外部から受け取ったデータをオブジェクトに復元する際、悪意あるコードを実行可能にする 脆弱性 のタイプ CWE-78 OSコマンドインジェクション OSコマンドの生成時に外部入力のバリデーションを不適切に行うことで、任意のコマンドを実行可能にする 脆弱性 のタイプ CVE別/2025年最も危険な 脆弱性 4選 2025年を通して最も危険な 脆弱性 について、個人の見解に基づいて選抜しました。 選抜の観点としては以下です。 CVSSスコア10(最大値)かつRCE、PoC、MSF、KEVをすべて網羅している 脆弱性 もし自社の環境や稼働しているシステムに下記に該当する 脆弱性 が存在する場合は、一刻も早く対処することをおすすめします。 CVE Published Vendor Product CVSS CWE RCE PoC MSF KEV EPSS CVE-2025-32433 2025-04-16 Erlang Erlang /OTP 10 CWE-306 TRUE TRUE TRUE TRUE 0.43921 CVE-2025-47812 2025-07-10 Wing FTP Server Wing FTP Server 10 CWE-158 TRUE TRUE TRUE TRUE 0.924 CVE-2025-55182 2025-12-03 Meta React Server Components 10 CWE-502 TRUE TRUE TRUE TRUE 0.5512 CVE-2025-37164 2025-12-16 Hewlett Packard Enterprise (HPE) OneView 10 CWE-94 TRUE TRUE TRUE TRUE 0.8131 CVE別/今後悪用が広まる可能性が高い 脆弱性 4選 最後に、2025年に公表されたCVEの中で、今後悪用が広まる可能性が高い 脆弱性 について、個人の見解に基づいて選抜しました。 選抜の観点としては以下です。 CWE別リスク分布で特定したCWEのうち、EPSSスコア0.9以上(直近30日間で悪用される可能性が非常に高い)かつPoC、MSFが公開されている 脆弱性 これらの 脆弱性 も早期に特定し、優先的に対処することをおすすめします。 CVE Published Vendor Product CVSS CWE RCE PoC MSF KEV EPSS CVE-2025-0282 2025-01 Ivanti Connect Secure, Policy Secure, and ZTA Gateways 9 CWE-787 TRUE TRUE TRUE TRUE 0.94105 CVE-2025-24016 2025-02 Wazuh Wazuh Server 9.9 CWE-502 TRUE TRUE TRUE TRUE 0.93801 CVE-2025-24813 2025-03 Apache Tomcat 9.8 CWE-502 TRUE TRUE TRUE TRUE 0.94183 CVE-2025-53770 2025-07 Microsoft SharePoint 9.8 CWE-502 FALSE TRUE TRUE TRUE 0.91188 まとめ 2025年はこれまで以上に 脆弱性 情報の量が増えたと感じていましたが、実際に分析することで日々のニュースを追うだけでは見えてこないその年の傾向や、NVDを取り巻く環境の変化や運用方針の転換が、データに影響を及ぼしていることが見えてきました。 CWEの総数で上位10位に入っていないものの、ひとたび悪用されればRCEに直結しやすく、かつ実際にKEVとして登録されるケースも目立ちました。 特に、ネットワーク機器の深刻な 脆弱性 として分類される傾向が見えたCWE-787と、冒頭でも触れたReact2Shellを含む CWE-502の動向には引き続き注視する必要がありそうです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @fukuyama.kenta レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
クロス イノベーション 本部、新卒1年目の大岡叡です。 今回は、1月29日にプレビュー公開された AWS Deployment SOPsを使ってみたので、その内容を報告します。 AWS Deployment SOPsを使ってみた結果、簡単なプロンプトを一度与えるだけで静的Webサイトをデプロイでき、CodePipelineのCI/CDについても簡単に構築することができました。この検証を通じて、非エンジニアがインフラ構築をして簡単なアプリケーションのデプロイを行う日も遠くないのではないかと思ったと同時に、 フルスタ ックアプリケーションも今回のように簡単なプロンプトを一度与えるだけでデプロイできる時代が来るのではないかと、期待が膨らみました。 AWS MCP Serverとは AWS Knowledge Tools AWS API Tools Agent SOP Tools AWS Deployment SOPsとは 検証の前提 検証1:Next.jsアプリケーションのデプロイ 検証内容 実装してくれたCDKのコード Deployment SOPsなしの場合 検証2:CodePipelineのセットアップ 検証内容 実装してくれたCDKのコード まとめ 参考文献 AWS MCP Serverとは AWS Deployment SOPsの説明をするうえで前提となる AWS MCP Server について説明します。 AWS MCP Serverは8つのツールを提供しており、 AWS Knowledge Tools 、 AWS API Tools 、 Agent SOP Tools の3つのカテゴリに分類されています。 AWS Knowledge Tools aws___search_documentation : API リファレンス・ベストプラクティス・サービスガイドを含む、すべての AWS ドキュメントを横断的に検索して関連情報を見つける。 aws___read_documentation : AWS ドキュメントページを取得し、 AIアシスタント が利用しやすいように Markdown 形式に変換する。 aws___recommend : AWS ドキュメントページに関連するトピックや、他のユーザーがよく一緒に閲覧しているコンテンツに基づいて、おすすめのドキュメントページを取得する。 aws___list_regions :リージョンID(例:ap-northeast-1)と名称(例:Asia Pacific (Tokyo))のペアの一覧を取得する。 aws___get_regional_availability :サービス、機能、 SDK API 、CloudFormationリソースについて、 AWS リージョンごとの利用可否情報を確認する。 AWS API Tools aws___call_aws : AWS API の呼び出しを実行する。 aws___suggest_aws_commands :関連する AWS API の説明と構文ヘルプを取得する。AIモデルの学習データに含まれていない可能性がある、新しくリリースされた API にも対応できる。 Agent SOP Tools aws___retrieve_agent_sop :Agent SOPsの検索または特定のSOPsの詳細情報の取得をする。 💡 Agent SOPsとは Agent SOPsとは、Claude Codeのような AIアシスタント が AWS 関連のタスクを行う際の標準作業手順書(Standard Operating Procedures)のことです。例えば、以下のようなSOPが用意されています。 SOP名 説明 create-secrets-using-best-practices ローテーション機能とKMSを備えたSecrets Managerによるシークレット作成 create-budget アラート機能付きの AWS Budgets作成 setup_cloudwatch_alarm_notifications SNS 経由でのCloudWatchアラーム通知セットアップ application-failure-troubleshooting CloudWatchログを分析してアプリケーション障害を デバッグ AWS Deployment SOPsとは AWS Deployment SOPsは、 aws___retrieve_agent_sop のツールが取得するデプロイ用のSOPのことです。 以下の4つのSOPがDeployment SOPsです。 SOP名 説明 deploy-webapp アプリケーションがDeployment SOPsでサポートされているものか確認し、適切なDeployment SOPを選択する。 deploy-frontend-app AWS CDKのコードを生成し、アプリケーションをデプロイした後、デプロイしたWebアプリのURLを提供する。 setup-pipeline AWS CodePipelineを使用してパイプラインを作成し、 GitHub に変更がプッシュされると自動的にアプリケーションの検証とデプロイを行う。 document-deployment デプロイに関するドキュメントを生成し、進捗を管理する。 Deployment SOPsがサポートしているアプリケーションタイプは以下のとおりです。 SPA:React, Vue, Angular, SvelteKit SSG:Next.js (static export), Nuxt, Gatsby , Hugo, Jekyll, Docusaurus, Astro, Eleventy Static websites そして、Deployment SOPsは2つの ユースケース 「 AWS へのアプリケーションのデプロイ」と「CodePipelineのセットアップ」をサポートしています。今回はこの2つの ユースケース を実際に試してみました。 検証の前提 今回はコーディングエージェントとしてClaude Codeを使用しました。 Skills等のカスタマイズ系のものは使用しないようにしました。 AWS CLI (認証情報設定済み)やGit CLI などの必要なツールはインストール済みです。 プロジェクトのルートで、以下のような .mcp.json を配置しました。 { " mcpServers ": { " aws-mcp ": { " command ": " uvx ", " args ": [ " mcp-proxy-for-aws@latest ", " https://aws-mcp.us-east-1.api.aws/mcp " ] } } } あらかじめ以下のようなTODOアプリケーションをNext.jsの静的エクスポートで実装しました。これはプロジェクトのルート ディレクト リにtodo-appという ディレクト リ名で配置しました。 検証1:Next.jsアプリケーションのデプロイ Claude Codeの挙動ベースで検証内容を報告します。結果としては、簡単なプロンプトを一度与えるだけでCloudFront + S3の構成を構築してデプロイを行い、CloudFrontのURLを共有してくれました。 検証内容 最初に「 AWS MCP の AWS Deployment SOPsを用いて、todo-appを AWS にデプロイしてください。」というプロンプトをClaude Codeに投げました(ちなみに、この検証で投げたプロンプトはこれだけです)。すると、 AWS MCP Serverの aws___retrieve_agent_sop を用いてdeploy-webappのSOPを取得してきて、アプリケーションがDeployment SOPsでサポートされているタイプか分析してくれました。 アプリケーションのファイルの読み込みが完了した後、 AWS MCP Serverの aws___retrieve_agent_sop を用いてdeploy-frontend-appのSOPを取得してきてくれました。そして、Next.jsの静的エクスポートはデプロイ可能ということで、デプロイ開始宣言をしてくれました。 続いて、 DEPLOYMENT_PLAN.md を生成してくれました。これ以降はこのプランに従って自律的に実装を進めてくれました。 --- sop_name: deploy-frontend-app, setup-pipeline repo_name: aws-deployment-sops-test app_name: TodoApp app_type: Frontend Application (Next.js Static Export) branch: main created: 2026-02-01 last_updated: 2026-02-01 --- # Deployment Plan: TodoApp Coding Agents should follow this Deployment Plan, and validate previous progress if picking up the Deployment in a new coding session. **IMPORTANT**: Update this plan after EACH step completes. Mark the step `[x]` and update `last_updated` timestamp. ## Phase 1: Gather Context and Configure - [x] Step 0: Inform User of Execution Flow - [x] Step 1: Create Deployment Plan - [] Step 2: Create Deploy Branch - [] Step 3: Detect Build Configuration - [] Step 4: Validate Prerequisites - [] Step 5: Revisit Deployment Plan ## Phase 2: Build CDK Infrastructure - [] Step 6: Initialize CDK Foundation - [] Step 7: Generate CDK Stack - [] Step 8: Create Deployment Script - [] Step 9: Validate CDK Synth ## Phase 3: Deploy and Validate - [] Step 10: Execute CDK Deployment - [] Step 11: Validate CloudFormation Stack ## Phase 4: Update Documentation - [] Step 12: Finalize Deployment Plan - [] Step 13: Update README.md そして、最終的にCloudFrontのURLを提供してくれました。このURLをクリックしたら実際にデプロイされたアプリケーションが正常に動作していることを確認できました。 実装してくれたCDKのコード 実際にClaude Codeが生成してくれたCDKのコードを以下に示します。CloudFront(Distribution, CloudFront Function)、S3(Webサイト用)、S3(CloudFront アクセスログ 用)、S3(S3 アクセスログ 用)などがセキュリティのベストプラクティスに基づいて実装されています。 import * as cdk from "aws-cdk-lib" ; import * as cloudfront from "aws-cdk-lib/aws-cloudfront" ; import * as s3deploy from "aws-cdk-lib/aws-s3-deployment" ; import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3" ; import { Construct } from "constructs" ; export interface FrontendStackProps extends cdk.StackProps { environment : string ; buildOutputPath : string ; } export class FrontendStack extends cdk.Stack { public readonly distributionDomainName : string ; public readonly bucketName : string ; constructor ( scope : Construct , id : string , props : FrontendStackProps ) { super (scope, id, props); const { environment , buildOutputPath } = props; const isProd = environment === "prod" ; const removalPolicy = isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY; // CSP via CloudFront Function - permissive policy for 3rd-party CDNs/APIs const cspFunction = new cloudfront.Function( this , "CspFunction" , { runtime : cloudfront.FunctionRuntime.JS_2_0, code : cloudfront.FunctionCode.fromInline( ` function handler(event) { var response = event.response; response.headers['content-security-policy'] = { value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: blob:; style-src 'self' 'unsafe-inline' https:; font-src 'self' https: data:; img-src 'self' https: data: blob:; connect-src 'self' https: wss:; frame-src 'self' https:; media-src 'self' https: blob:; worker-src 'self' https: blob:; object-src 'self' https:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" }; return response; } ` ), comment : "Adds Content-Security-Policy header" , } ); // Extension rewrite function for Next.js static export (trailingSlash: false) // Rewrites /path to /path.html const extensionRewriteFunction = new cloudfront.Function( this , "ExtensionRewriteFunction" , { runtime : cloudfront.FunctionRuntime.JS_2_0, code : cloudfront.FunctionCode.fromInline( ` function handler(event) { var request = event.request; var uri = request.uri; if (!uri.includes('.') && uri !== '/') { request.uri = uri + '.html'; } return request; } ` ), comment : "Rewrites /path to /path.html for Next.js static export" , } ); const cloudfrontToS3 = new CloudFrontToS3( this , "CFToS3" , { bucketProps : { removalPolicy , autoDeleteObjects : !isProd, versioned : false , enforceSSL : true , } , loggingBucketProps : { removalPolicy , autoDeleteObjects : !isProd, lifecycleRules : [{ id : "DeleteOldLogs" , enabled : true , expiration : isProd ? cdk.Duration.days( 3650 ) : cdk.Duration.days( 7 ) }] , enforceSSL : true , } , cloudFrontLoggingBucketProps : { removalPolicy , autoDeleteObjects : !isProd, lifecycleRules : [{ id : "DeleteOldLogs" , enabled : true , expiration : isProd ? cdk.Duration.days( 3650 ) : cdk.Duration.days( 7 ) }] , enforceSSL : true , } , insertHttpSecurityHeaders : false , cloudFrontDistributionProps : { comment : ` ${ id } - ${ environment } ` , defaultRootObject : "index.html" , defaultBehavior : { responseHeadersPolicy : cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS, functionAssociations : [ { function : cspFunction, eventType : cloudfront.FunctionEventType.VIEWER_RESPONSE, } , { function : extensionRewriteFunction, eventType : cloudfront.FunctionEventType.VIEWER_REQUEST, } , ] , } , priceClass : cloudfront.PriceClass.PRICE_CLASS_100, enableIpv6 : true , httpVersion : cloudfront.HttpVersion.HTTP2_AND_3, minimumProtocolVersion : cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, } , } ); const websiteBucket = cloudfrontToS3.s3Bucket!; const distribution = cloudfrontToS3.cloudFrontWebDistribution; new s3deploy.BucketDeployment( this , "DeployWebsite" , { sources : [ s3deploy.Source.asset(buildOutputPath) ] , destinationBucket : websiteBucket, distribution , distributionPaths : [ "/*" ] , prune : true , memoryLimit : 512 , } ); this .distributionDomainName = distribution.distributionDomainName; this .bucketName = websiteBucket.bucketName; // Outputs new cdk.CfnOutput( this , "WebsiteURL" , { value : `https:// ${ distribution.distributionDomainName } ` , description : "CloudFront distribution URL" , exportName : ` ${ id } -WebsiteURL` , } ); new cdk.CfnOutput( this , "BucketName" , { value : websiteBucket.bucketName, description : "S3 bucket name" , exportName : ` ${ id } -BucketName` , } ); new cdk.CfnOutput( this , "DistributionId" , { value : distribution.distributionId, description : "CloudFront distribution ID" , exportName : ` ${ id } -DistributionId` , } ); new cdk.CfnOutput( this , "DistributionDomainName" , { value : distribution.distributionDomainName, description : "CloudFront domain name" , exportName : ` ${ id } -DistributionDomain` , } ); if (cloudfrontToS3.s3LoggingBucket) { new cdk.CfnOutput( this , "S3LogBucketName" , { value : cloudfrontToS3.s3LoggingBucket.bucketName, description : "Bucket for S3 access logs" , exportName : ` ${ id } -S3LogBucket` , } ); } if (cloudfrontToS3.cloudFrontLoggingBucket) { new cdk.CfnOutput( this , "CloudFrontLogBucketName" , { value : cloudfrontToS3.cloudFrontLoggingBucket.bucketName, description : "Bucket for CloudFront access logs" , exportName : ` ${ id } -CloudFrontLogBucket` , } ); } cdk.Tags.of( this ). add ( "Stack" , "Frontend" ); cdk.Tags.of( this ). add ( "aws-mcp:deploy:sop" , "deploy-frontend-app" ); cdk.Tags.of(cloudfrontToS3). add ( "Stack" , "Frontend" ); cdk.Tags.of(cloudfrontToS3). add ( "aws-mcp:deploy:sop" , "deploy-frontend-app" ); } } 以上より、「 AWS MCP の AWS Deployment SOPsを用いて、todo-appを AWS にデプロイしてください。」と一言お願いをすれば、それなりの構成でインフラを構築してくれて、アプリケーションをデプロイできることを確認できました。これなら非エンジニアの方でも簡単かつ安全にアプリケーションをデプロイできるのではないでしょうか。 Deployment SOPsなしの場合 Deployment SOPsを使用せずにClaude Codeに「todo-appを AWS にデプロイしてください。」とお願いしたらどうなるか試してみました。ここでは非エンジニアがデプロイする想定で、条件などを一切与えずにお願いしました。こちらも検証1と同様、Claudeの指示に基本従う方針で進めました。 最初にdeploy方法の選択を迫られました。Recommendedの AWS Amplifyを選択しました。 デプロイを進めてくれて、最終的にAmplifyのURLを表示してくれました。 しかし、このURLにアクセスしたところ、以下のようなWebページでした。 Amplifyへのデプロイ手順はCloudFront + S3構成と比べて複雑ではないものの、単に「デプロイして」と依頼するだけでは、現状のClaude Codeでは自律的にタスクを完遂することが難しいようです。 検証2:CodePipelineのセットアップ Claude Codeの挙動ベースに検証内容を報告します。CodePipelineが構築され、 GitHub にプッシュするだけで変更が自動的にデプロイされる仕組みが整いました。 検証内容 最初に「 AWS MCP の AWS Deployment SOPsを用いて、CodePipelineをセットアップしてください。」というプロンプトをClaude Codeに投げました。すると、 AWS MCP Serverの aws___retrieve_agent_sop を用いてsetup-pipelineのSOPを取得してきて、CodePipelineのセットアップを開始してくれました。 次に以下のような確認をされました。lintエラーの修正だけお願いしておきました。 検証1と同様にDEPLOYMENT_PLAN.mdを更新しながらCDKのコード実装など色々やってくれた後に、以下の認証をお願いされました。これは手順通り対応しました。 その後、パイプラインのデプロイなど色々やってくれてタスクが完了しました。 CodePipelineの動作を確認するために以下のようにh1の中身に変更を加えて、deploy-to- aws ブランチにプッシュしたところ、CodePipelineがトリガーされてビルド→デプロイが実行され、Webページに変更内容が反映されました。 実装してくれたCDKのコード 実際にClaude Codeが生成してくれたCDKのコードを以下に示します。CodePipeline、CodeBuildプロジェクト、S3 バケット ( アーティファクト 用)が構築されます。 import * as cdk from "aws-cdk-lib" ; import * as codebuild from "aws-cdk-lib/aws-codebuild" ; import * as codepipeline from "aws-cdk-lib/aws-codepipeline" ; import * as pipelines from "aws-cdk-lib/pipelines" ; import { Construct } from "constructs" ; import { FrontendStack } from "./frontend-stack" ; export interface PipelineStackProps extends cdk.StackProps { codeConnectionArn : string ; repositoryName : string ; branchName : string ; } export class PipelineStack extends cdk.Stack { public readonly pipeline : pipelines.CodePipeline ; constructor ( scope : Construct , id : string , props : PipelineStackProps ) { super (scope, id, props); const source = pipelines.CodePipelineSource. connection ( props.repositoryName, props.branchName, { connectionArn : props.codeConnectionArn, triggerOnPush : true , } ); const synth = new pipelines.ShellStep( "Synth" , { input : source, commands : [ // Install dependencies "(cd todo-app && npm install)" , "(cd infra && npm install)" , // Quality checks "(cd todo-app && npm run lint)" , // Secret scanning "npx -y @secretlint/quick-start '**/*'" , // Build frontend "(cd todo-app && npm run build)" , // CDK synth "cd infra" , "npx tsc" , `npx -y cdk synth --context codeConnectionArn=" ${ props.codeConnectionArn } " --context repositoryName=" ${ props.repositoryName } " --context branchName=" ${ props.branchName } "` , ] , primaryOutputDirectory : "infra/cdk.out" } ); this .pipeline = new pipelines.CodePipeline( this , "Pipeline" , { pipelineName : "TodoAppPipeline" , selfMutation : true , pipelineType : codepipeline.PipelineType.V2, synth , synthCodeBuildDefaults : { buildEnvironment : { computeType : codebuild.ComputeType.MEDIUM, buildImage : codebuild.LinuxBuildImage.STANDARD_7_0 } , partialBuildSpec : codebuild.BuildSpec.fromObject( { version : "0.2" , phases : { install : { "runtime-versions" : { nodejs : "latest" , } , } , } , } ), } , } ); // Deploy stage for production const deployStage = new cdk.Stage( this , "Deploy" , { env : { account : this .account, region : this . region } , } ); new FrontendStack(deployStage, "TodoAppFrontend-prod" , { stackName : "TodoAppFrontend-prod" , environment : "prod" , buildOutputPath : "../todo-app/out" , } ); this .pipeline.addStage(deployStage); // Build pipeline to enable property access this .pipeline.buildPipeline(); cdk.Tags.of( this ). add ( "Stack" , "Pipeline" ); cdk.Tags.of( this ). add ( "aws-mcp:deploy:sop" , "setup-pipeline" ); new cdk.CfnOutput( this , "PipelineName" , { value : "TodoAppPipeline" , description : "CodePipeline Name" , } ); new cdk.CfnOutput( this , "PipelineArn" , { value : this .pipeline.pipeline.pipelineArn, description : "CodePipeline ARN" , } ); } } まとめ AWS Deployment SOPsを用いて、静的エクスポートしたNext.jsアプリケーションをデプロイしたり、CodePipelineのセットアップを行いました。特に問題なくAIがすべての作業を完了してくれたことは驚きでした。近い将来、非エンジニアでもデプロイを行える日は遠くないと感じました。さらに、 フルスタ ックアプリケーションも簡単なプロンプトを一度与えるだけで構築できる時代が来るのではないかと、期待が膨らみました。 最後までお読みいただきありがとうございました! 参考文献 https://docs.aws.amazon.com/aws-mcp/latest/userguide/what-is-mcp-server.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/getting-started-aws-mcp-server.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/understanding-mcp-server-tools.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/agent-sops.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/agent-sops-deployment.html https://aws.amazon.com/jp/about-aws/whats-new/2025/01/aws-announces-deployment-agent-sops-in-aws-mcp-server-preview/ 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @ooka.toru レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
はじめに 金融IT本部 2年目の坂江 克斗です。 今回はECR リポジトリ 作成テンプレート機能について、2025年12月に プッシュ時の自動リポジトリ作成 がサポート開始となりましたので、新機能を含めたテンプレート機能全体を検証し紹介します。 ベースの概念から含めて丁寧に解説できればと思います。 はじめに コンテナの概要 コンテナとは コンテナに関する基本用語 Amazon ECR の概要 リポジトリ作成テンプレートの概要 プルスルーキャッシュの概要 レプリケーションの概要 リポジトリ作成テンプレートの検証 前提 実装 検証 プルスルーキャッシュ レプリケーション プッシュ時の自動作成(Create on push) まとめ おわりに (補足)Dive deep into Amazon ECR より内部実装の紹介 内部アーキテクチャ 動作イメージ紹介 コンテナの概要 コンテナの概要に関して、ざっくりと説明します。 コンテナとは コンテナ技術とは、 Linux カーネル が持つ機能(namespaceやcgroup)を利用して、1つのOS上に論理的に隔離された実行環境を作成する技術 を指します。この隔離された実行環境そのものをコンテナと呼びます。 コンテナ内で動作しているのは 仮想マシン とは異なり、 ホストOS上で動作する通常のプロセス(プログラム単位) です。 プロセスが属する namespace によって、プロセス・ ファイルシステム ・ネットワーク・ Linux ユーザやグループの権限等を分離・隔離し、cgroupによってコンテナのプロセスが使用するリソース(CPU/メモリ)を制限することで、 1つのOS上に存在しながら、独立した OS のように振る舞う環境が実現されています。 コンテナに関するリソースの管理や操作を行う OSS を、 コンテナランタイム と呼びます。 このうち、 Linux カーネル の機能を直接利用してコンテナの作成・起動・削除といった処理を実行するものを 低レベルのコンテナランタイム (runc、gVisor、kata-containers)と呼びます。 一方で、イメージの管理やコンテナのライフサイクル管理を担い、コンテナの作成・削除といったリクエストを低レベルランタイムに委譲するものを 高レベルのコンテナランタイム (containerdやCRI-O)と呼びます。 コンテナに関する基本用語 コンテナを扱ううえで、以下の用語が頻出します。 用語 説明 イメージ(Image) コンテナを実行するために必要な ファイルシステム と メタデータ をまとめたもの。 レジストリ (Registry) コンテナイメージを保管・配布するためのサービス。 API による認証・認可やイメージ管理、保管・配布基盤などを含めた サービス全体 を指す。 リポジトリ (Repository) レジストリ 内に作成される イメージの論理的な格納単位 。 通常はサービスやアプリケーション単位で作成し使用される。 タグ(Tag) イメージに付与されるバージョンや識別子として使用。 現在、コンテナに関する形式は Open Container Initiative(OCI)規格 により定義されています。 OCI では、コンテナを実行するための 低レベルなコンテナランタイム に加え、 イメージ および レジストリ の仕様について標準化が行われています。 この標準化により、Docker に限らずさまざまなコンテナランタイムや レジストリ (Docker Hub、ECR、 GitHub )で互換性が保たれており、私たちは内部実装の違いを意識することなくコンテナイメージを利用することができます。 Amazon ECR の概要 Amazon Elastic Container Registry(ECR)は、 AWS が提供する マネージドなコンテナ レジストリ サービス です。 ECR には、 AWS アカウント毎に作成され、IAM によるアクセス制御が可能な プライベート レジストリ と、イメージの取得(pull)を誰でも行うことができる パブリック レジストリ の2 種類が存在します。 実際にアプリケーションをデプロイする際には、プライベート レジストリ を使用してアプリケーションイメージをセキュアに管理・運用するケースが一般的です。一方で、パブリック レジストリ はベースイメージの取得(踏み台サーバ用の Amazon Linux イメージなど)といった、公開されているイメージを参照する用途で利用されることが多いです。 ECR には、 レジストリ としての基本的機能だけでなく、 イメージの 脆弱性 スキャン 、 ライフサイクル管理 、 プルスルーキャッシュ 、 レプリケーション そして リポジトリ 作成テンプレート など、コンテナイメージ管理を効率化するためのさまざまな機能が用意されています。 本記事では、ECR が提供する機能のうち リポジトリ 作成テンプレート機能 に焦点を当て、その概要と活用方法について紹介します。 ECRに関してもっと詳しく知りたい方向けに、本記事の最後にECRの内部 アーキテクチャ や技術的な仕組みを解説した章を追加しましたので、ぜひご参照ください。 リポジトリ 作成テンプレートの概要 名前の通り、本機能は ユーザの代わりに Amazon ECR 自身が リポジトリ を作成する際に、その リポジトリ に適用される設定をテンプレートとして定義 できる機能です。 従来、ECR において リポジトリ が自動的に作成されるタイミングは、 プルスルーキャッシュ や レプリケーション 機能を有効化した場合に限られていました。 しかし、2025年12月のアップデートにより、 リポジトリ 作成テンプレート機能の拡張として push 時に リポジトリ を自動作成する機能 が追加されました。 具体的にテンプレートに設定する項目は、以下となります。 適用対象 Create on push 、 Pull through cache 、 Replication のいずれか、または複数を指定。 リポジトリ 名のプレフィクス。 プレフィクスを指定せず、 他の リポジトリ 作成テンプレートに該当しない全 リポジトリ を対象とすることも可能。 イメージタグ設定 Mutable (タグの上書き可能)もしくは Immutable (タグの上書き不可)を指定。 上記設定から除外するタグを指定するための、フィルター用プレフィクス。 暗号化設定 AES-256 もしくはKMSを指定。 KMSの場合は、 AWS マネージドキーもしくはカスタ マーマネ ージドキー(CMK)を指定。 リポジトリ 権限 リポジトリ 単位で適用するIAMリソースポリシーを設定。 ライフサイクルポリシー リポジトリ AWS タグ イメージタグではなく、ECR リポジトリ 自体に付与したい AWS リソース用のタグを指定。 リポジトリ 作成ロール 未定義の場合、 サービスにリンクされたロール を自動的に使用。 ただし、上記のロールは ecr:CreateRepository 権限しかないため、ケースごとに以下の権限を追加付与したカスタムロールの作成が必要。( AWSが提案するポリシー ) KMS キーを使用する場合:対象の KMS キーに対する権限 リソースタグを付与する場合: ecr:TagResource 権限 プルスルーキャッシュの概要 プルスルーキャッシュ は、特定の プライベートECR リポジトリ をキャッシュとして 、オリジンとなるパブリック レジストリ や別のプライベート ECRからECR自身がコンテナイメージを取得し、必要に応じてプライベート ECR リポジトリ を自動作成したうえで、そのイメージを保存する機能です。 pull元のECSタスク等は、 プライベートECRに対してイメージをpullするだけ で、シームレスに利用できるようになります。 この仕組みにより、インターネットへの直接通信ができない ECS タスクであっても、追加で NAT Gateway をデプロイするなどネットワーク要件に影響を与える対応を行うことなく、ECR を経由した 外部のパブリックイメージのセキュアな取得 が可能となります。 注意点としては、イメージをキャッシュしてから再度イメージの最新をアップストリーム レジストリ に確認する周期が、 少なくとも24時間に1回 となっています。そのため、頻繁に更新されるイメージの最新を同期して使用したい場合には少し不便かもしれません。 プルスルーキャッシュの設定において特に重要な項目は、以下の 2 点です。 アップストリーム レジストリ キャッシュとして利用するプライベートECRに対する、オリジンとして使用する(上位の) レジストリ を指します。 パブリックECR、プライベートECR、Docker Hub、Quay.io、 Kubernetes 、 GitHub Container Registry、 Microsoft Azure Container Registry、GitLab Container Registryのいずれかを指定。 リポジトリ 名プレフィクス ECRへのイメージpullリクエストが発生した際に、キャッシュ リポジトリ 作成をトリガーする リポジトリ 名のプレフィクス。 ECR自身が、他アカウントのプライベートECRにアクセス、もしくはキャッシュ用の リポジトリ を作成することから、IAM権限が必要となりますが、 サービスにリンクされたロール が自動で付与されるため、ユーザ側での管理は必要ありません。 しかし、 pull元のECSタスク等に対しては ecr:CreateRepository 、 ecr:BatchImportUpstreamImage 権限を明示的に付与する必要があります。 直感的には、ECR側に付与されたロールがこれらの権限を持つイメージですが、実際にはイメージを利用する側(ECS タスク等)のロール単位でプルスルーキャッシュの利用可否を制御できる設計となっています。 レプリケーション の概要 レプリケーション は、 特定のプライベートECR リポジトリ に pushされたイメージ を、別のリージョンや AWS アカウントに存在する プライベートECR リポジトリ へ自動的に複製(レプリケート) する機能です。 レプリケーション 先に リポジトリ が存在しない場合は、自動的に リポジトリ が作成されます。 レプリケーション の設定において特に重要な項目は、以下の 2 点です。 送信先 タイプ クロスアカウント、 クロスリー ジョンのいずれか、または両方を指定。 レプリケーション 先のアカウントIDやリージョンを指定。 リポジトリ 名プレフィクス レプリケーション の元となるECR リポジトリ 名のプレフィクス。 プレフィクスに一致するECR リポジトリ にイメージが追加された際に、 レプリケーション がトリガーされます。 プルスルーキャッシュ同様に、ECR自身に付与するIAM権限が必要となりますが、 サービスにリンクされたロール が自動で付与されるため、ユーザ側での管理は必要ありません。 ただし、クロスアカウントの場合には、 レプリケーション 元のアカウントが レプリケーション 先にイメージを複製するために、 レプリケーション先のレジストリ側に適切な権限設定 が必要となります。 リポジトリ 作成テンプレートの検証 前提 検証のためローカルで実装します。 以下の構成を使用します。 プライベートサブネットのみデプロイし、ECSタスクからは VPC エンドポイントを経由してECRにアクセス。 クライアントは AWS CLI およびdocker CLI を使用して、ECRにアクセス。 以下の機能を設定します。 プルスルーキャッシュ example-cache/ というプレフィクスのECR リポジトリ に対して、パブリックECR( public.ecr.aws )をアップストリーム レジストリ として設定。 ECSタスクはこの リポジトリ にpullをリクエストします。 レプリケーション example-replication/ というプレフィクスのECR リポジトリ に対して、 ap-northeast-1 から us-east-1 への レプリケーション を設定。 リポジトリ 作成テンプレート プッシュ時 example-create-on-push/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 14日後 に削除するライフサイクルを設定。 プルスルーキャッシュ example-cache/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 140日後 に削除するライフサイクルを設定。 レプリケーション example-replication/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 365日後 に削除するライフサイクルを設定。 ※ ただし、 レプリケーション 先の リポジトリ は us-east-1 リージョンに作成されるため、本テンプレートのみ us-east-1 リージョンに作成。 実装 基本設定および VPC などのネットワークに関する定義をします。 ECSから VPC エンドポイント経由でECRにアクセスするため、最低限3つの VPC エンドポイント(または到達可能なエンドポイント)が必要となります。構成図に示したとおり、その内訳は以下となります。 s3 イメージレイヤーの実体の保存場所。 dkr.ecr docker pull 等、OCI準拠の レジストリ アクセスを行うためのエンドポイント。 レジストリ のホスト名が <アカウントID>.dkr.ecr.<リージョン名>.amazonaws.com という形式であることからも分かるように、このエンドポイントを 名前解決可能な状態 で利用する必要があります。 api .ecr ECR用の AWS API を使用する際のエンドポイント。 terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.28.0" source = "hashicorp/aws" } } } provider "aws" { region = local.regions.primary } locals { account_id = "<アカウントID>" regions = { primary = "ap-northeast-1" secondary = "us-east-1" } availability_zones = { primary = [ "$ { local.regions.primary } a" , "$ { local.regions.primary } c" , "$ { local.regions.primary } d" ] } } data "aws_caller_identity" "current" {} ########################################################################################### # VPC ########################################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } ## Subnet resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = local.availability_zones.primary [ 0 ] map_public_ip_on_launch = false tags = { Project = "example" } } ## Route Table resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.id tags = { Project = "example" } } resource "aws_route_table_association" "private_a" { subnet_id = aws_subnet.private_a.id route_table_id = aws_route_table.private_a.id } ## VPC Endopoint ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint.html resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .s3" vpc_endpoint_type = "Gateway" route_table_ids = [ aws_route_table.private_a.id, ] tags = { Project = "example" } } resource "aws_vpc_endpoint" "ecr_dkr" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = [ aws_subnet.private_a.id ] security_group_ids = [ aws_security_group.vpce.id, ] private_dns_enabled = true tags = { Project = "example" } } resource "aws_vpc_endpoint" "ecr_api" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .ecr.api" vpc_endpoint_type = "Interface" subnet_ids = [ aws_subnet.private_a.id ] security_group_ids = [ aws_security_group.vpce.id, ] private_dns_enabled = true tags = { Project = "example" } } ## Security Group resource "aws_security_group" "vpce" { name = "vpce-sg" vpc_id = aws_vpc.main.id tags = { Project = "example" } } resource "aws_vpc_security_group_ingress_rule" "ingress_from_ecs" { security_group_id = aws_security_group.vpce.id from_port = 443 to_port = 443 ip_protocol = "tcp" referenced_security_group_id = aws_security_group.ecs.id } ECSの定義を行います。以下の項目に注意してください。 変数の定義は後程記載しますが、 public.ecr.aws/nginx/nginx:tag のイメージをプルスルーキャッシュにより取得。 イメージ URI としては <プライベートECRのホスト名>/<プルスルーキャッシュ条件のプレフィクス名>/nginx/nginx:tag を指定。 ECSの実行ロールに、プルスルーキャッシュ利用に必要な権限を付与。 ecr:CreateRepository 、 ecr:BatchImportUpstreamImage ECSからS3へ通信できるように、ECSのセキュリティグループのルールを設定。 ゲートウェイ エンドポイントを利用して AWS サービスへアクセスする場合、名前解決の結果は VPC のCIDRではなく、 AWS サービスごとに割り当てられた専用の IPアドレス となります。 そのため、セキュリティグループではS3用の IPアドレス レンジ( マネージドプレフィックスリスト )に対して明示的に通信を許可する必要があります。 ## Cluster resource "aws_ecs_cluster" "example" { name = "example-cluster" tags = { Project = "example" } } ## Service resource "aws_ecs_service" "example" { name = "example-service" cluster = aws_ecs_cluster.example.name launch_type = "FARGATE" desired_count = 1 task_definition = aws_ecs_task_definition.nginx.arn network_configuration { subnets = [ aws_subnet.private_a.id ] security_groups = [ aws_security_group.ecs.id ] } tags = { Project = "example" } } ## Task resource "aws_ecs_task_definition" "nginx" { family = "example-nginx" requires_compatibilities = [ "FARGATE" ] cpu = 256 memory = 512 network_mode = "awsvpc" execution_role_arn = aws_iam_role.ecs_exec.arn task_role_arn = aws_iam_role.ecs.arn container_definitions = <<EOL [ { "name": "nginx", "image": "$ { local.cached_image_uri } ", "memory": 512, "cpu": 256, "essential": true, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ] } ] EOL tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ecs" { name = "ecs-sg" vpc_id = aws_vpc.main.id tags = { Project = "example" } } resource "aws_vpc_security_group_egress_rule" "egress_to_vpce" { security_group_id = aws_security_group.ecs.id from_port = 443 to_port = 443 ip_protocol = "tcp" referenced_security_group_id = aws_security_group.vpce.id } data "aws_ec2_managed_prefix_list" "s3" { name = "com.amazonaws.$ { local.regions.primary } .s3" } resource "aws_vpc_security_group_egress_rule" "egress_to_s3_ip" { security_group_id = aws_security_group.ecs.id from_port = 443 to_port = 443 ip_protocol = "tcp" prefix_list_id = data.aws_ec2_managed_prefix_list.s3.id } ## IAM resource "aws_iam_role" "ecs" { name = "ecs-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_role" "ecs_exec" { name = "ecs-exec-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_policy" "ecr_cache" { name = "ecr-cache" policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Sid = "AllowEcrPullThroughCache" Effect = "Allow" Action = [ "ecr:CreateRepository" , "ecr:BatchImportUpstreamImage" ] Resource = "*" } ] } ) } resource "aws_iam_role_policy_attachment" "ecr_cache" { role = aws_iam_role.ecs_exec.name policy_arn = aws_iam_policy.ecr_cache.arn } resource "aws_iam_role_policy_attachment" "ecs_exec" { role = aws_iam_role.ecs_exec.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } プルスルーキャッシュおよび レプリケーション を定義します。 プルスルーキャッシュ用の リポジトリ 名プレフィクス: example-cache レプリケーション 用の リポジトリ 名プレフィクス: example-replication また、イメージ URI などについては、可能な限り共通で再利用可能な式として変数定義していますが、値としては以下に示すようなシンプルなものになっています。 ECSが本来取得したいアップストリーム レジストリ のイメージ URI origin_image_uri : public.ecr.aws/nginx/nginx:1.29-alpine アップストリーム レジストリ のホスト名 origin_registry_host : public.ecr.aws アップストリーム レジストリ のイメージパス origin_image_path : nginx/nginx:1.29-alpine プライベートECRのホスト名 cached_registry_host : <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com ECSタスクが参照するプライベートECR リポジトリ (キャッシュイメージの保存先) cached_image_uri : <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-cache/nginx/nginx:1.29-alpine locals { ## pull_through_cache origin_image_uri = "public.ecr.aws/nginx/nginx:1.29-alpine" origin_registry_host = split ( "/" , local.origin_image_uri) [ 0 ] origin_image_path = replace (local.origin_image_uri, "$ { local.origin_registry_host } /" , "" ) pull_through_cache_prefix = "example-cache" cached_registry_host = "$ { local.account_id } .dkr.ecr.$ { local.regions.primary } .amazonaws.com" cached_image_uri = "$ { local.cached_registry_host } /$ { local.pull_through_cache_prefix } /$ { local.origin_image_path } " ## Replication replication_filter_prefix = "example-replication" ## create on push create_on_push_prefix = "example-create-on-push" } ## pull through cache ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_pull_through_cache_rule resource "aws_ecr_pull_through_cache_rule" "example" { ecr_repository_prefix = local.pull_through_cache_prefix upstream_registry_url = local.origin_registry_host } ## replication ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration resource "aws_ecr_repository" "example_replication" { name = "example-replication/nginx/nginx" image_tag_mutability = "MUTABLE" tags = { Project = "example" } } resource "aws_ecr_replication_configuration" "example" { replication_configuration { rule { destination { region = local.regions.secondary registry_id = local.account_id } repository_filter { filter = local.replication_filter_prefix filter_type = "PREFIX_MATCH" } } } } リポジトリ 作成テンプレートを以下の設定で定義します。 プッシュ時 リポジトリ 名プレフィクス: example-create-on-push/ タグなしイメージを 14日後 に削除するライフサイクルを設定。 プルスルーキャッシュ リポジトリ 名プレフィクス: example-cache/ タグなしイメージを 140日後 に削除するライフサイクルを設定。 レプリケーション リポジトリ 名プレフィクス: example-replication/ タグなしイメージを 365日後 に削除するライフサイクルを設定。 ※ ただし、 レプリケーション 先の リポジトリ は us-east-1 リージョンに作成されるため、本テンプレートのみ us-east-1 リージョンに作成。 また、今回はリソースタグの付与を行うことから、 サービスにリンクされたロール の権限では不足となります。そのため、カスタムロールを作成し必要な権限を設定します。 ## repository creation template ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_creation_template resource "aws_iam_role" "ecr_creation_template" { name = "ecr-creation-template" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecr.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_policy" "ecr_creation_template" { name = "ecr-creation-template" policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Sid = "AllowEcrCreateRepo" Effect = "Allow" Action = [ "ecr:CreateRepository" , "ecr:ReplicateImage" , "ecr:TagResource" ] Resource = "*" } ] } ) } resource "aws_iam_role_policy_attachment" "ecr_creation_template" { role = aws_iam_role.ecr_creation_template.name policy_arn = aws_iam_policy.ecr_creation_template.arn } resource "aws_ecr_repository_creation_template" "example_create_on_push" { prefix = local.create_on_push_prefix description = "An example-create-on-push template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "CREATE_ON_PUSH" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 14 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } resource "aws_ecr_repository_creation_template" "example_pull_through_cache" { prefix = local.pull_through_cache_prefix description = "An example-pull-through-cache template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "PULL_THROUGH_CACHE" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 140 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 140 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } resource "aws_ecr_repository_creation_template" "example_replication" { region = local.regions.secondary prefix = local.replication_filter_prefix description = "An example-replication template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "REPLICATION" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 365 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 365 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } 検証 terraform apply によりデプロイした後に、テンプレート毎の挙動を確認します。 プルスルーキャッシュ ECSタスクが参照するECR リポジトリ はTerraformで未定義でしたが、ECSが正常にデプロイされました。 プルスルーキャッシュにより、ECSタスクが参照する リポジトリ が自動作成され、中にはnginxのイメージが追加されていることが確認できました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 レプリケーション ローカルから <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx リポジトリ にイメージをプッシュします。 katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com Login Succeeded katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx:1.29-test The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx] 25f453064fd3: Pushed 0abf9e567266: Pushed e096540205d5: Pushed 567f84da6fbd: Pushed 1074353eec0d: Pushed 33f95a0f3229: Pushed da7c973d8b92: Pushed 085c5e5aaa8e: Pushed 1.29-test: digest: sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e size: 1989 i Info → Not all multiplatform-content is present and only the available single-platform image was pushed sha256:648c87cd4f9a37a104d194db573d1afe8fa6b2632e618257c0f09e26a80d5110 -> sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e プッシュした結果、 us-east-1 リージョンに想定通り リポジトリ が作成され、イメージもレプリケートされていることが確認できました。 また、他のECR リポジトリ は作成されておらず、指定のプレフィクスを持つ リポジトリ のイメージのみレプリケートされていました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 プッシュ時の自動作成(Create on push) ローカルから、以下2パターンの存在しないECR リポジトリ にイメージをプッシュします。 テンプレート対象外: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx テンプレート対象内: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx:1.29-alpine The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx] 0abf9e567266: Unavailable 567f84da6fbd: Unavailable 1074353eec0d: Unavailable 25f453064fd3: Unavailable e096540205d5: Unavailable da7c973d8b92: Unavailable 33f95a0f3229: Unavailable 085c5e5aaa8e: Unavailable unexpected status from POST request to https://<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/v2/example/nginx/nginx/blobs/uploads/?mount=sha256:25f453064fd3e8a9754b6e51b86c637e13203cbfc748fcf73f3c8b2d10816ae3&from=example/nginx: 404 Not Found katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx:1.29-test The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx] 1074353eec0d: Pushed 085c5e5aaa8e: Pushed da7c973d8b92: Pushed 567f84da6fbd: Pushed 0abf9e567266: Pushed e096540205d5: Pushed 25f453064fd3: Pushed 33f95a0f3229: Pushed 1.29-test: digest: sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e size: 1989 i Info → Not all multiplatform-content is present and only the available single-platform image was pushed sha256:648c87cd4f9a37a104d194db573d1afe8fa6b2632e618257c0f09e26a80d5110 -> sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e プッシュした結果、想定通りテンプレートが対象とするプレフィクスを持つ リポジトリ のみpush処理が成功しました。 また、想定通り リポジトリ が作成されていることも確認できました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 まとめ リポジトリ 作成テンプレートは、 特定の自動作成 ユースケース に限定 されるものの、共通のポリシーや設定を強制できるという点で、運用上のメリットが大きい機能です。 一方で、今回新たに追加された リポジトリ の自動作成機能については、指定したプレフィクスさえ満たせば リポジトリ が自動的に作成されてしまうため、入力ミスなどによって意図しない リポジトリ が作成される可能性があります。 また、自動作成された リポジトリ は、明示的にimportしない限りTerraformなどのIaC管理下には置かれないため、実リソースとコードの乖離が発生しやすい点も、運用面でのデメリットと感じました。 そのため、本機能を利用する際は、 リポジトリ 名の 命名規則 や運用ルールを事前に明確化したうえで導入することが重要だと考えます。 おわりに 本記事では、コンテナおよびECRの概要を整理したうえで、 リポジトリ 作成テンプレートの概要と挙動について、実際に検証を行いました。 特定の ユースケース に向けた機能であるため、私自身はしばらく使用する予定はありませんが、本記事がこれから利用を検討されている方の参考になれば幸いです。 個人的な感覚ではありますが、普段私たちが利用している IT技術 の多くは、数多くの抽象化や技術の積み重ねによって、 シンプルで使いやすいインターフェース として提供されています。そのため、実際に何が行われているのかを具体的にイメージしづらい部分も少なくありません。 こうした背景にある実装や仕組みを把握することは、日々の業務に直接活かされない場面も多いかもしれませんが、理解の実感が結果として技術に対する自信や判断力につながっていくと考えています。 そのため、今後の記事においても使用方法だけでなく、その背後にある技術や仕組みについても共有していきたいと考えています。 (補足)Dive deep into Amazon ECR より内部実装の紹介 Deep Dive 系の記事は AWS サービスごとに別途作成する予定ですが、今回はせっかくなので AWS re:Invent 2023 - Dive deep into Amazon ECR (CON405) を参考にしつつ、 Amazon ECR に限定してその内部仕様について少しだけ見ていきたいと思います。 内部 アーキテクチャ AWS の各サービスも実体としてはアプリケーションであり、 AWS API の仕様に基づいたリクエストを処理するFrontend Service (EC2)が存在します。その背後では、ストレージや名前解決などに関わる複数の AWS サービスが連携して動作しています。 ECRも例外ではなく、以下に示す構成となっております。背後では、S3やDynamoDBなどのストレージサービスも使用しています。 (これらの S3 や DynamoDB 自体も同様に、Frontend Service を起点とした構成になっていると考えられます) ただし、 レジストリ サービスとして利用されるという特性上、コンテナイメージの操作に関するアクセス時には AWS API ではなく、OCI 規格に基づく push / pull リクエストが使用されます。 そのため ECR では、これらの OCI リクエストを受け付けるための専用の Proxy Service がデプロイされている点が、他の AWS サービスとは少し異なる特徴となっています。 あくまで推測にはなりますが、Lambda や S3 などの AWS サービスも、 AWS API とは形式の異なるURLベースのアクセスが可能であることから、ECRと同様に中継層(Proxyに相当する コンポーネント )が存在している可能性があると考えています。 動作イメージ紹介 すべてを説明すると分量が多くなってしまうため、本記事では イメージの pull 時の挙動 に絞って説明します。 push / pull 時の仕様に関しても OCI で標準化されており、 The OpenContainers Distribution Spec で説明されています。 pullの仕様としては、以下2つのリクエストに分類されます。 <レジストリエンドポイント>/v2/<リポジトリ名>/manifests/<タグまたはイメージダイジェスト> への メタデータ (manifest)の取得 manifestは JSON 形式のデータであり、イメージレイヤごとのダイジェスト( ハッシュ値 )が記載されています。 <レジストリエンドポイント>/v2/<リポジトリ名>/blobs/<ダイジェスト> への blob(ダイジェストで管理される論理的なコンテンツ)の取得 ただし、2 つ目の blob の取得については、少なくとも Amazon ECR ではイメージレイヤそのものではなく 保存先の URL を返却しつつリダイレクトさせる方式 が採用されています。 blob はあくまでダイジェストで管理される論理的なコンテンツとして、 The OpenContainers Distribution Spec ではイメージレイヤなのか保存先URLなのかを明示的に指定しているわけではありません。 そのため、具体的に blob が返すものとして何が存在するかは 実装依存 となっており、 レジストリ の仕様やバージョンによって異なる可能性があります。 実際に ECR で pull が発生した際の、 AWS サービス内部での処理フローは以下に示すような流れとなります。 OCI 準拠の pull リクエストは Proxy Serviceによって中継され、 AWS API のエンドポイントとして動作する Frontend Service が、イメージ管理に関する主要な処理を担っています。 また、manifest やイメージレイヤの実体は S3 に保存されており、DynamoDBはあくまで参照情報のみ保存していることがわかります。 ここまでの内容から、ECR等の AWS サービスが抽象的なサービスではなく、複数の AWS サービスが連携して動作する 実体のあるアプリケーションとして運用されている という点がイメージできたのではないでしょうか。 個人的に、サービスにリンクされたロールなど、 AWS サービス自体が主体となって IAM 権限を基に操作するという感覚が、これまであまり掴めていませんでしたが、内部 アーキテクチャ におけるFrontend Service の実体や役割を知ったことで、その挙動がよりしっくりと理解できるようになりました。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @kobayashi.hinami ( Shodo で執筆されました )
アバター
こんにちは。スマート ソサエティ センター 行政デジタル部所属の宇佐美です。 今回は、Vertex AIのGeminiバッチ予測機能を使って、大量のテキストデータを一括で処理する方法をご紹介します。 バッチ予測とは Vertex AIのバッチ予測は、大量のプロンプトを非同期で一括処理できる機能です。リアルタイム推論と比較して以下のメリットがあります。 コスト効率 : リアルタイム推論と比較して安価な料金で利用可能。特にGemini 2.5 Flash はリアルタイム推論のおよそ半額で利用することが出来ます。 高 スループット : 1回のジョブで数十万件のリクエストを処理可能 シンプルなワークフロー : 個別リクエストの管理が不要 データの入出力先としてBigQueryまたはCloud Storageを使用できます。今回はBigQueryを使用します。 BigQueryについて BigQueryは Google Cloudが提供するフルマネージドのデータウェアハウスです。大規模データの分析に特化しており、 ペタバイト 規模のデータに対しても SQL で高速にクエリを実行できます。 BigQueryのデータ構造は以下の階層になっています。 デー タセット : テーブルやビューをまとめるコンテナ。リージョンを指定して作成する テーブル : 実際のデータを格納する場所。 スキーマ (カラム定義)を持つ 今回使用するデー タセット 今回は JaGovFaqs-22k という日本の官公庁のFAQデー タセット を使用します。各レコードには質問と回答が含まれており、今回はそれらを要約対象とします。 前提条件 Google Cloudプロジェクトが作成済みであること 必要な API が有効化されていること(Vertex AI API 、BigQuery API ) 適切なIAMロールが付与されていること 実装手順 1. 必要なライブラリのインストール pip install google-cloud-bigquery google-genai datasets 2. BigQuery スキーマ の作成 まず、BigQueryのデー タセット と、入力用データを管理するためのテーブル( faq_input )を定義します。 from google.cloud import bigquery PROJECT_ID = "your-project-id" LOCATION = "us-central1" client = bigquery.Client(project=PROJECT_ID) # データセット作成 dataset = bigquery.Dataset(f "{PROJECT_ID}.batch_demo" ) dataset.location = LOCATION client.create_dataset(dataset, exists_ok= True ) # テーブル作成 schema = [ bigquery.SchemaField( "Id" , "INTEGER" , mode= "REQUIRED" ), bigquery.SchemaField( "Question" , "STRING" ), bigquery.SchemaField( "Answer" , "STRING" ), bigquery.SchemaField( "copyright" , "STRING" ), bigquery.SchemaField( "url" , "STRING" ), bigquery.SchemaField( "request" , "STRING" , mode= "REQUIRED" ), ] table = bigquery.Table(f "{PROJECT_ID}.batch_demo.faq_input" , schema=schema) client.create_table(table, exists_ok= True ) ポイント : request カラムは、 GenerateContentRequest の構造に準拠する必要があります。 コードを実行すると、以下のとおりデー タセット とテーブルが作成されます。 3. データ登録 Hugging Faceからデー タセット を取得し、バッチ予測用のフォーマットに変換してBigQueryにロードします。 せっかくなので、 構造化出力 でレスポンスのフォーマットを指定してみましょう。 import json import pandas as pd from datasets import load_dataset response_schema = { "type" : "OBJECT" , "properties" : { "summary" : { "type" : "STRING" , "description" : "質問と回答の要約" }, }, } hf_dataset = load_dataset( "matsuxr/JaGovFaqs-22k" ) rows = [{ "Id" : i, "Question" : item[ "Question" ], "Answer" : item[ "Answer" ], "copyright" : item[ "copyright" ], "url" : item[ "url" ], "request" : json.dumps({ "contents" : [{ "role" : "user" , "parts" : [{ "text" : f "以下の質問と回答を140文字以内で要約してください。 \n\n ## 質問 \n {item['Question']} \n\n ## 回答 \n {item['Answer']}" }]}], "generationConfig" : { "responseMimeType" : "application/json" , "responseSchema" : response_schema}, }, ensure_ascii= False ) } for i, item in enumerate (hf_dataset[ "train" ])] df = pd.DataFrame(rows) job = client.load_table_from_dataframe(df, f "{PROJECT_ID}.batch_demo.faq_input" ) job.result() 余談ですが、日本語のような非ASCIIコードの文字は出力時にエスケープされてしまうため、 ensure_ascii=False でエスケープしないように設定しています。 4. バッチ予測タスクの作成 次に、バッチ予測ジョブを作成して実行します。 モデルは速度と品質、利用料金のバランスに優れているGemini 2.5 Flash を利用します。 出力先には別のテーブル(faq_output)を指定します。 from google import genai from google.genai.types import CreateBatchJobConfig genai_client = genai.Client( vertexai= True , project=PROJECT_ID, location=LOCATION ) job = genai_client.batches.create( model= "gemini-2.5-flash" , src=f "bq://{PROJECT_ID}.batch_demo.faq_input" , config=CreateBatchJobConfig(dest=f "bq://{PROJECT_ID}.batch_demo.faq_output" ), ) 5. 結果の確認 Vertex AI → バッチ推論から結果を確認してみましょう。正常に終了していれば、ステータスが「完了」になっています。 22794件のデータに対して15分弱で処理が完了しています。 先ほど作成したfaq_outputテーブルも確認してみます。 response.candidates[0].content.parts[0].text の中身が実際に出力された回答です。 指定されたフォーマット通りに出力されています。 { " text ":" { \n \" summary \" : \" 会計検査院は、国や地方公共団体の公共工事に対し、会計経理面だけでなく設計・積算・施工まで実地で検査します。現場では資料確認や担当者への聴取に加え、非破壊検査装置でコンクリート強度や鉄筋を確認し、必要に応じて破壊検査も実施。不適切な事態の是正を図っています。 \"\n } " } まとめ ここまでの処理をまとめて一つのサンプルコードにします。 import json import pandas as pd from datasets import load_dataset from google import genai from google.cloud import bigquery from google.genai.types import CreateBatchJobConfig, HttpOptions PROJECT_ID = "your-project-id" LOCATION = "us-central1" # BigQueryセットアップ client = bigquery.Client(project=PROJECT_ID) # データセット作成 dataset = bigquery.Dataset(f "{PROJECT_ID}.batch_demo" ) dataset.location = LOCATION client.create_dataset(dataset, exists_ok= True ) # テーブル作成 schema = [ bigquery.SchemaField( "Id" , "INTEGER" , mode= "REQUIRED" ), bigquery.SchemaField( "Question" , "STRING" ), bigquery.SchemaField( "Answer" , "STRING" ), bigquery.SchemaField( "copyright" , "STRING" ), bigquery.SchemaField( "url" , "STRING" ), bigquery.SchemaField( "request" , "STRING" , mode= "REQUIRED" ), ] table = bigquery.Table(f "{PROJECT_ID}.batch_demo.faq_input" , schema=schema) client.create_table(table, exists_ok= True ) # データ準備 response_schema = { "type" : "OBJECT" , "properties" : { "summary" : { "type" : "STRING" , "description" : "質問と回答の要約" }, }, } hf_dataset = load_dataset( "matsuxr/JaGovFaqs-22k" ) rows = [{ "Id" : i, "Question" : item[ "Question" ], "Answer" : item[ "Answer" ], "copyright" : item[ "copyright" ], "url" : item[ "url" ], "request" : json.dumps({ "contents" : [{ "role" : "user" , "parts" : [{ "text" : f "以下の質問と回答を140文字以内で要約してください。 \n\n ## 質問 \n {item['Question']} \n\n ## 回答 \n {item['Answer']}" }]}], "generationConfig" : { "responseMimeType" : "application/json" , "responseSchema" : response_schema}, }, ensure_ascii= False ) } for i, item in enumerate (hf_dataset[ "train" ])] df = pd.DataFrame(rows) job = client.load_table_from_dataframe(df, f "{PROJECT_ID}.batch_demo.faq_input" ) job.result() # バッチ予測実行 genai_client = genai.Client( vertexai= True , project=PROJECT_ID, location=LOCATION, http_options=HttpOptions(api_version= "v1" ) ) job = genai_client.batches.create( model= "gemini-2.5-flash" , src=f "bq://{PROJECT_ID}.batch_demo.faq_input" , config=CreateBatchJobConfig(dest=f "bq://{PROJECT_ID}.batch_demo.faq_output" ), ) 注意点 リージョンの制約 バッチ予測ジョブとBigQueryデー タセット は同じリージョンに配置する必要があります。マルチリージョンデー タセット (US、 EU など)はサポートされていないため、 us-central1 のような特定のリージョンを指定してください。 最後に Vertex AIのGeminiバッチ予測を使用することで、大量のデータを効率的に処理できます。 データ分析、コンテンツ生成、文書分類など、即時性を必要としない大規模なLLM処理タスクには、バッチ予測の活用をぜひご検討ください。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @usami.tsubasa レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
1.自己紹介 2.はじめに 3.VLAとは? 元となるChatGPTなどのAI VLAはアクションを出力 4.今回の検証の狙い 5. 検証環境 環境: ハードウェア: 6.検証内容:SmolVLAによる複数タスクの実行 SmolVLAとは 目指したこと 設定したタスク 7.検証結果 できたこと 検証から見えた課題 8.まとめ 1.自己紹介 HS本部Open Innovationラボ(通称イノラボ)の奥野です。 もともとは製造業で精密機器や家電のメカ設計・機能/制御設計・企画などに携わっていました。 2018年に 電通 総研に入社し、現在は様々な先端技術のR&Dに取り組むイノラボで、ロボット等の研究開発に取り組んでいます。 2.はじめに 最近ニュースで ヒューマノイド 、フィジカルAIといった言葉を目にすることが増えました。年明けに開催されたCES2026でも Hyundai Motor Groupのグループ企業であるBoston Dynamicsが研究開発を続けていた Atlas の商用化に向けた戦略が発表され盛り上がっていました。 イノラボでは長くロボットに関する研究開発に取り組んでおり、特に 最近はフィジカルAIの技術検証を進めております。 今回、その取り組みとして、フィジカルAIの根幹技術であるVLA( Vision Language Action)モデルでアームロボットを動かす検証を行った内容を執筆します。 細かい実行コードなどは記載せず、本記事ではVLAを試してみて何ができるのか・どんな課題がありそうかといったことをお伝えしたいと思います。 なお今回の検証では Hugging FaceのLeRobotとSO-ARM101を組み合わせた環境 で、VLAモデルの事後学習からロボット制御までを行っています。 3.VLAとは? 元となるChatGPTなどのAI 多くの人が使うようになったChatGPTはLLM(Large Language Model)でテキスト入力からテキストを出力したり、 Vision を組み込んだVLM( Vision -Language Model)で画像内容を理解し説明するなど視覚と言語を統合したモデルが組み込まれています。 VLAはアクションを出力 VLAは画像とテキストの入力から、Action(ロボットの制御)を出力するモデルとなります。 LLMやVLMで発展してきたTransformerやDiffusion modelといった技術が拡張され、ロボット制御である アクションを出力するよう発展したものがVLA です。 これまでシステムに閉じていたAIが、現実世界に直接関与できるようになったという点で インパク トのある技術進化だと言えます。 以下は Google が2023年に発表し注目を集めたVLAモデル RT-2 から引用した図です。 モデル内部に視覚と言語を統合したVLMを内包し、その推論結果から直接ロボットを制御するActionを生成することで汎化性を実現しています。 4.今回の検証の狙い 今回の検証目的は大きく3つです。本記事では狙い1と2についての結果を述べます。 狙い3についてはSmolVLAと Physical Intelligenceのπ0.5 を比較しましたので、別の記事で改めて書きたいと思います。 狙い1 : VLAの実行環境を構築して理解する 狙い2 : VLAでアームロボットを動かして精度感や課題を確認する 狙い3 : モデルによる差異を確認する 5. 検証環境 環境: マシンスペック Windows 11 Pro AMD Ryzen 9 9950X3D NVIDIA RTX5090 ソフトウェア : Lerobot v0.41 最初はv0.33の環境で検証していましたが、途中でv0.41がリリースされ環境を変更しました。 このバージョンアップでLerobot Datasetがv3.0となり、 Dataset Tools が使えるようになりました。 ただしv0.33で記録済みのデー タセット をv0.41環境(Lerobot Dataset v3.0)で使うためには、データ変換が必要となる点に注意です。 ハードウェア: ロボット : SO-ARM101 カメラ : USBカメラ3台(空間に2台、ロボットリストに1台) 以下は構築した実環境の様子です。 以下は3つの各カメラの様子です。 6.検証内容:SmolVLAによる複数タスクの実行 SmolVLAとは 今回の検証で使用した SmolVLA はHugging Faceが開発した軽量なVLAモデルです。他のVLAがパラメータ数Bなどと巨大であるのに対して、 SmolVLAはパラメータ数が450Mと小型化され、軽量な環境でも扱いやすいモデル です。 今回の環境でも問題なくファインチューニングから実行までできました。 目指したこと 今回の検証の前に ACT(Action Chunking with Transformer) による模倣学習でアームロボットにタスクを実行させる検証を実施していました。 ACTでも単独タスクであればある程度できるようになりますが、複数のタスクを連続して行うような複雑なものになると上手く学習させることができない結果でした。そこで今回はもう少し難しいことを目指してトライしました。 単独タスクが高い精度で実行できる 1つのモデルで複数の単独タスクが実行できる 複数の単独タスクを連続実行できる 設定したタスク 以下4つのアイテムをそれぞれ黄色のカゴに入れる、ピック&プレースのタスクを4種類設定しました。 それぞれの初期位置は固定として、目印をテーブル上につけた環境としました。 英文がSmolVLAの入力指示文です。 紫色のボール:Pick up only purple ball and put in yellow basket 黄色のボール:Pick up only yellow ball and put in yellow basket 赤色のキューブ:Pick up only red cube and put in yellow basket 青色のキューブ:Pick up only blue cube and put it in the basket 7.検証結果 できたこと 以下は今回の検証でチューニングしたSmolVLAモデルで、4つのタスクを連続実行している様子です。当初目指していた3つの目標は概ね達成することができました。 この動画では 4つのタスクを連続実行 していますが、 1つのモデルでそれぞれの単独タスクのみを実行することも実現 できています。 またそれぞれの単独タスクは 8~9割と高い成功率でタスクを実行することができました 。 検証から見えた課題 データ取得の大変さ 最終的な今回のモデルでは合計550回、1時間以上の学習データでファインチューニングしています。データ取得は人間による繰り返し作業となるため、 精神的にも肉体的にも非常にハード 。 SmolVLAにおいてはタスクを成功させるためには起きうる 条件全ての学習データが必要な傾向 で、条件に対する汎化性を持たせようとすると指数的に学習コストが高まります。例えば初期状態が常に4つ全アイテムがある状態だけのデータで学習すると、1つでもアイテムがない状態で実行した場合にタスクは失敗(動かないなど)となります。 モデル学習時間・コスト 軽量なSmolVLAでも今回の環境では1つのモデルの学習に10時間以上かかりました。今回は学習データの変化に対する結果を試行錯誤しながら検証を進めたため、この 学習時間が大きな ボトルネック でした。 一方で クラウド 環境でH100を使った場合6時間程度まで削減できることが確認できましたが、時間削減のために ハイスペックな GPU を使うと相応の利用コスト がかかります。やはりフィジカルAIには潤沢な GPU インフラが必要で、それだけコストもかかることが分かりました。 モデルの限界感 位置変動への汎化性は低いと感じました。学習データを増やすことである程度精度を高められそうではありますが、かなりのデータ量が必要になると想定されます。 紫と黄色のボール位置を交換するといった条件を混同しそうな変化に対しては、それぞれの条件データをどれだけ入れても成功できませんでした。 AIではそのままロボットを制御してしまうため、アーム先端がずっと振動する、急激な軌道変動が頻繁に起きる、先端が机に激突するといったことが多発しました。結果として筆者の環境では2ヶ月程度でモータ故障が発生しています。ロボットの最終的な制御部分にはAIをそのまま適応させるのではなく、ロボット制御を考慮したフィルタリングなどの工夫が必要となりそうです。 8.まとめ 検証を通して軽量なSmolVLAでも 一定のタスク実行精度や複数タスクの適応性があることを確認 できました。一方で多くの課題や限界感も見えた結果となりました。 VLAはフィジカルAIの重要な技術で急速に発展しています。日々新しいロボット・モデルが発表されており、期待が高まっていますが、それぞれ できることもあれば出来ないこともあり、落ち着いて見極めることが重要 と考えます。 実業務で使っていくためには解決すべき課題や、ソフト・ハード両面でシステム的な落とし所を設計する必要がありそうです。 しかし今回見えた課題については、様々な解決策の研究が進んでいたり米中を中心に巨額な投資がされており、ChatGPTの精度が急激に高まったようにフィジカルAIも周辺技術含め急激に進化する日が近いかもしれません。 新しい動向をキャッチしながら、引き続き検証を続けていきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @okuno_takahiro レビュー: @azeta.takuya ( Shodo で執筆されました )
アバター
検証環境 題材としたアプリ 主な機能 技術スタック SDDとは 1. cc-sddでSDDを試す 良かった点 課題に感じた点 2. spec-workflow-mcpを試してみる 実際に使ってみた流れ 良かった点 課題に感じた点 3. SDDで実装品質をどこまで引き上げられるか 3.1 バックエンドのリファクタリング Steeringの改善 生成されたタスク リファクタリングの結果 3.2 UI/UX課題の改善を要件定義からAIに任せる 発見した課題 AIに要件定義から任せる 改善結果 おわりに こんにちは、クロス イノベーション 本部の 北涼 太です。 最近、コーディングエージェントと協力して開発する手法が主流になりつつあると思います。 ただ、ざっくりした指示だと思ったような実装にならないことも多いですよね。 そんな中で気になっていたのが、SDD(Spec-Driven Development)という開発スタイルです。 「仕様を先に固めてから実装する」アプローチで、AIとの協業と相性が良いとされています。 今回はこのSDDを使って、実際にWebアプリを開発してみました。 今回の検証で分かったのは、 SDDは強力だが、仕様レビューの負荷が想像以上に高い ということでした。 そこで、レビュー体験を改善するSDDツールへの切り替えも試しています。 この記事では、以下の3点についてお伝えします。 CLI ベースのSDDツール(cc-sdd)を試して感じた課題 GUI でレビューできるツール(spec-workflow- mcp )への切り替え SDDで実装品質をどこまで引き上げられるか 検証環境 項目 内容 SDDツール cc-sdd → spec-workflow- mcp AIエージェント Codex(gpt-5.2-codex) 題材としたアプリ 今回は「 RSS ニュースをユーザーの趣味嗜好、フィードバックに合わせてスコアリングするWebアプリ」を題材にしました。 アプリ自体が目的ではないので、シンプル(かつギリギリ役に立ちそう)なアプリとしています。 主な機能 RSS URLとユーザーの趣味嗜好を入力 記事ごとにGood/Bad評価をつけられる 各ニュース記事についてAIエージェントに質問可能 ニュース取得ボタンで収集とスコアリングを実行 成果物のイメージは以下です。 ・メイン画面 ・AIエージェントとのチャット画面 技術スタック Frontend : TypeScript + React / Vite + TailwindCSS Backend : Python + FastAPI Infra : AWS CDK + API Gateway + Lambda SDDとは 本題に入る前に、SDDについて簡単に説明します。 SDDとは、要求・設計・タスクを先に固めてから実装に進むことで、曖昧さを減らす開発スタイルです。 今回は、以下のような手順を採用して開発を進めました。 フェーズ 内容 Steering プロダクト/技術/構成の前提をそろえる(product.md / tech.md / structure.md) Requirements ユーザーストーリーと受け入れ基準を定義する Design UI・ API ・データモデルなどの設計を詰める Tasks 実装タスクに分解し、順序と依存を整理する Implementation タスクを順番に実装する(必要に応じて設計へ戻る) コーディングエージェントに開発を任せられる粒度まで仕様を先に固める分、 レビュー対象となるドキュメントの量が多くなりやすい のが特徴です。 1. cc-sddでSDDを試す まずは CLI ベースのSDDツールであるcc-sdd( リポジトリ : https://github.com/gotalab/cc-sdd )を試してみました。 cc-sddは、Kiro 1 の設計思想( AWS では、 AI-DLC と呼んでいます)を他のAIエージェント上で再現するための CLI ツールです。成果物は Markdown で出力されます。今回はCodex CLI と組み合わせて運用しました。 ツールの仕組みについては詳しい記事がたくさん出ていますので、ここでは実際に使ってみた所感を中心に書きます。 良かった点 設計漏れが起きにくい : 不完全な設計書を渡しても、LLMが不足分を指摘してくれる 仕様を充実させるほど安定する : 前提条件を詳しく書くほど、出力のブレが減る 課題に感じた点 「仕様として十分か」の判断が難しい : どこまで設計すればAIに渡す前提として足りているのか、慣れが必要 AIの柔軟さが損なわれる可能性 : スクラッチで書く場合、最初から仕様を詰め切れないことも多い。中途半端に詳しい仕様を渡すと、かえってAIの判断を縛ってしまう Markdown のレビューが大変 : 自動生成された詳細な仕様を読み通すのに時間がかかる 特に負担だったのが、 Markdown ベースのレビュー でした。 仕様として十分な設計をAIが生成してくれるため、ドキュメント量が多くなるのは仕方ないかもしれません。しかし、指摘箇所を日本語で記述するコストが想像以上にかかりました。 たとえば「2.4章の〇〇について、××と書いているが、△△と書いてほしい」のような書き方をしなければならず、レビューのたびにこの作業が発生します。 そこで、SDDの流れは維持しつつ、仕様レビューを GUI で完結できる「spec-workflow- mcp 」に切り替えてみました。 2. spec-workflow- mcp を試してみる spec-workflow- mcp ( リポジトリ : https://github.com/Pimzino/spec-workflow-mcp )は、SDDワークフローを MCP (Model Context Protocol)経由で進めるための仕組みです。成果物は Markdown で管理しつつ、 レビューや承認はWebの GUI 上で完結 できます。 実際に使ってみた流れ 基本的な流れ(requirements → design → tasks → implementation)はcc-sddと同じです。 また、コーディングエージェントとのやり取りも、他のSDDツールと同様、 CLI から行います。 他の CLI ツールと異なるのは、 MCP サーバーとしてローカルにWebフロントエンドが立ち上がることです。 Webからはプロジェクトの様々な情報が確認できるのですが、特に注目すべきなのは、コーディングエージェントの作業を GUI 上でレビューできる機能です。 まず、requirementsなどの各ワークフローが完了すると、Web上に承認依頼が届きます。 承認依頼を開くと、 Markdown が レンダリング された状態で仕様を確認できます。 気になる箇所があれば、範囲選択をしてそのままコメントを残せます。 修正依頼を出すと、AIが仕様を直してくれます。変更点はDiffで確認できます。 requirements → design → tasks と順番に承認していき、最後にタスク開始を依頼すると、カンバンボードで進捗が自動更新されていきます。 良かった点 行範囲に対してコメントを残せる : 指摘箇所を日本語で説明する必要がなく、直接該当部分にコメントできる 通知が分かりやすい : 視覚化されるべき情報が適切に視覚化されている 課題に感じた点 Webと CLI の行き来が若干面倒 : 純粋な CLI ツールと比較して、どうしても操作回数は多くなる 3. SDDで実装品質をどこまで引き上げられるか ここからは、SDDを使って実装品質をどこまでコン トロール できるかを検証します。 3.1 バックエンドの リファクタリング 今回の検証では、要件定義に重きを置き、実装方針はほとんどAIに任せていました。 Steeringでクリーン アーキテクチャ に準じることをルールとして明記していたのですが、実際に生成された実装は以下のような構造でした。 backend/ ├── rss_news_agent/ │ ├── __init__.py │ ├── adapters.py │ ├── api.py │ ├── errors.py │ ├── llm.py │ ├── logging_service.py │ ├── models.py │ ├── repositories.py │ ├── services.py │ └── settings.py ├── tests/ ├── app.py ├── lambda_handler.py ├── pyproject.toml ├── requirements.txt └── uv.lock たとえば repositories.py を見ると、本来Provider層に書くべき実装が混在していました。 class DynamoDbClient (Protocol): def put_item ( self, TableName: str , Item: dict [ str , dict [ str , str ]], **kwargs: object , ) -> dict [ str , object ]: ... def delete_item ( self, TableName: str , Key: dict [ str , dict [ str , str ]], ConditionExpression: str , ) -> dict [ str , object ]: ... また llm.py を見ると、LLMに関連する様々なデータ型やクラスが一つのファイルに詰め込まれていました。 class LlmFeedbackSummaryDto (BaseModel): good_ids: list [ str ] = Field(default_factory= list ) bad_ids: list [ str ] = Field(default_factory= list ) class BedrockRuntimeClientAdapter : def __init__ (self, model_id: str , region: str | None = None ) -> None : self._client = boto3.client( "bedrock-runtime" , region_name=region) self._model_id = model_id これは良い実装とは言いにくいと思います。そこで、SDDを使って リファクタリング を依頼してみました。 Steeringの改善 方針として、Steering(今回は structure.md )のコード規約を詳しく追記し、具体的な実装はAIに任せます。 structure.mdの変更(抜粋) ## Directory Organization project-root/ ├── backend/ # FastAPIバックエンド -│ ├── rss_news_agent/ # ドメインロジック(API/サービス/リポジトリ) +│ ├── rss_news_agent/ # ドメインロジック(Clean Architecture) +│ │ ├── handler/ # ルーティング/入力変換 +│ │ ├── model/ # ドメイン/DTOモデル +│ │ ├── repository/ # 永続化インターフェイスと実装 +│ │ ├── service/ # ビジネスロジック +│ │ ├── provider/ # 外部依存(DB/HTTP等)の生成 +│ │ └── usecase/ # エンドポイント単位のユースケース │ ├── tests/ # pytestテスト │ ├── app.py # 本番/デプロイ用エントリ │ ├── local_app.py # ローカル開発用エントリ │ └── lambda_handler.py # Lambdaハンドラ ## Code Organization Principles 5. **クリーンアーキテクチャ指向**: バックエンドは層分離を意識し、依存性注入(DI)を徹底する +6. **責務の抽象化**: 外部依存はproviderに閉じ、ビジネスロジックはservice/usecaseに集約する +7. **境界の明確化**: 抽象(Port/Protocol等)は内側のレイヤーに置き、実装は外側に限定する ## Module Boundaries * **Repositories vs Services**: 永続化ロジックとビジネスロジックを分離 +* **Endpoint vs Usecase**: 原則として1エンドポイントに対して1usecaseを用意する +* **Provider vs Service**: providerは外部アクセスに限定し、ビジネスルールはserviceに置く +* **Port vs Implementation**: 抽象は内側、実装は外側に配置する 生成されたタスク この変更を反映すると、以下のようなタスクが生成されました。 - [-] 1. 現状のAPI/サービス/リポジトリの依存関係を整理して移行方針を確定する - [-] 2. model層を新設し、ドメインモデル/DTO/入力出力スキーマを整理する - [-] 3. repository層を新設し、永続化インターフェイスと実装を整理する - [-] 4. service層を新設し、ドメインロジックを整理する - [-] 5. usecase層を新設し、1エンドポイント=1usecaseの実装を追加する - [-] 6. handler層を新設し、ルーティングと入出力変換を移行する - [-] 7. provider層を新設し、外部依存の生成を集約する - [-] 8. 既存エントリポイントの接続を更新する リファクタリング の結果 実装を進めてもらった結果、各レイヤーがしっかり整理された構造になりました。 backend/ ├── rss_news_agent/ │ ├── handler/ │ ├── model/ │ ├── provider/ │ ├── repository/ │ │ ├── __init__.py │ │ ├── article_feedback_repository.py │ │ ├── rss_source_repository.py │ │ └── user_profile_repository.py │ ├── service/ │ ├── usecase/ │ │ ├── __init__.py │ │ ├── add_rss_source.py │ │ ├── delete_rss_source.py │ │ ├── fetch_articles.py │ │ ├── list_rss_sources.py │ │ ├── save_article_feedback.py │ │ ├── save_user_profile.py │ │ ├── send_article_chat.py │ │ └── update_rss_source.py │ └── __init__.py ├── tests/ ├── app.py ├── lambda_handler.py ├── pyproject.toml ├── requirements.txt └── uv.lock 実装内容も改善されました。たとえば user_profile_service.py では、純粋な ビジネスロジック だけが実装されています。 class UserProfileService : def __init__ ( self, repository: UserProfileRepository, clock: Callable[[], datetime] | None = None , ) -> None : self._repository = repository self._clock = clock or datetime.utcnow def save_personal_text (self, personal_text: str , user_id: str = "default" ) -> UserProfile: profile = UserProfile( user_id=user_id, personal_text=personal_text, updated_at=self._clock(), ) return self._repository.save(profile) def get_personal_text (self, user_id: str = "default" ) -> str | None : profile = self._repository.get(user_id) if profile is None : return None return profile.personal_text この検証から、 Steeringの質がコーディングエージェントの出力品質に直結する ことが確認できました。 3.2 UI/UX課題の改善を要件定義からAIに任せる 基本的なアプリケーションは構築できましたが、実際に触ってみると使いにくい点がいくつかありました。 発見した課題 1. RSS ソースの編集ボタンが効かない 「Edit」ボタンを押しても何も起きません。これは使いにくい点というより実装漏れですね。 2. パーソナル情報の保存状態が分からない ユーザーの趣味嗜好を入力する部分で、「SAVE」を押すと入力した文字が消えます。保存できたのか分かりません。 「SAVE」を押すと、バックエンドには保存処理が飛んでいるものの、画面上では文字が消えただけに見えます。 3. チャットの応答待ちが分かりづらい 各記事に対してAIエージェントと会話できる機能がありますが、返答を待っている間のインジケータが分かりにくいです。 AIに要件定義から任せる これらの課題を具体的には指摘せず、「UI/UX上の課題を自分で見つけて改善方針を仕様化せよ」と指示を出してみました。 以下は、生成された requirements.md の抜粋です。 ## Requirements ### Requirement 1: RSSソース管理の編集体験強化 (中略) ### Requirement 2: パーソナルシグナルの保存状態の可視化 (中略) ### Requirement 3: 記事一覧の可読性とスキャン性向上 (中略) ### Requirement 4: 収集アクションと進行状況の明確化 (中略) ### Requirement 5: チャット導線の発見性と文脈維持 (中略) WHEN チャット送信中 THEN システム SHALL 送信中の状態を明確に示し、二重送信を防ぐ。 こちらが指摘しなくても、明らかに使いにくい部分は要件として言及されていました。それ以外の項目も、改善案として納得できる内容でした。 改善結果 このまま機能設計 → タスク設計 → 実装と進めました。実装段階では期待どおりでない部分もあったため、都度フィードバックを与えています。 1. RSS ソースの編集 UIにはまだ改善の余地がありますが、編集できるようになりました。 2. パーソナル情報の保存状態 保存されたことが分かりやすくなりました。 3. チャットの応答待ち インジケータが表示されるようになり、分かりやすくなりました。 この検証から、 明らかに問題がある挙動については、AIに要件定義から任せても一定の改善が可能 なことが分かりました。 ただし、今回の検証アプリは コンポーネント 数が少なく、比較的シンプルな構成でした。より複雑なアプリケーションでは、同じようにうまくいくとは限りません。 おわりに 今回SDDを試してみて、個人的に印象に残ったことを書いておきます。 レビューの負荷は想像以上だった Markdown で生成された仕様を読んで、日本語で指摘を書いて…という作業は、耐えがたいものがありました。spec-workflow- mcp に切り替えてからは、行を選択してコメントするだけで済むようになり、だいぶ楽になったと思います。SDDを続けるなら、レビュー体験は重要だと感じました。 Steeringは可能な限り詳細に書いたほうが良い 当たり前かもしれませんがSteeringはプロジェクトの根本となる部分であり、品質を担保する意味で非常に重要です。 「クリーン アーキテクチャ で」と書くだけでは不十分で、 ディレクト リ構成や各レイヤーの責務まで書いてあげると、期待に近い実装が出てきました。ここは試行錯誤しながら調整していく部分かなと思います。 AIに要件定義から任せるのは、ものによる 明らかな問題点は拾ってくれましたが、細かいUI/UXの調整は結局人間がフィードバックを重ねる必要がありました。 まだ手探りな部分も多いですが、SDDとAIエージェントの組み合わせには可能性を感じています。 同じようにSDDを試している方の参考になれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kita.ryota レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました ) Kiroとは、 AWS が開発したAIエージェント型 IDE で、SDDのアプローチを採用しています。 ↩
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 別ブログにて Amazon ECR と FutureVulsを用いたコンテナイメージ運用について紹介させていただきましたが、実際に使っていく中で、 Amazon ECR には想像以上に多くの便利な機能が備わっていることに気づきました。本記事では、その中でも 日々の運用を効率化したり、セキュリティや管理性を高めたりするうえで役立つ ECR の機能について、実体験を交えながらいくつかご紹介します。 これから Amazon ECR に触れていく方や、すでに利用しているものの「なんとなく使っている」という方にとっても、本記事が少しでも参考になれば幸いです。 まずは復習 Amazon ECR の主な機能 タグフィルタ アーカイブストレージクラス プルタイム更新の除外 まとめ 参考 まずは復習 以前のブログでは、代表的なコンテナ オーケストレーション サービスである Amazon Elastic Kubernetes Service( Amazon EKS) と Amazon Elastic Container Service( Amazon ECS) の 2 つのサービスについてご紹介しました。これらのサービスは、コンテナのデプロイやスケーリング、可用性の確保など、アプリケーション実 行基 盤として重要な役割を担っています。 また、それらのサービスへアプリケーションをデプロイするための コンテナイメージの管理先 として、 Amazon Elastic Container Registry( Amazon ECR) についても取り上げました。 Amazon ECR は、主に Docker コンテナイメージを管理するためのフルマネージドな レジストリ サービスであり、 Amazon ECS、 Amazon EKS、 AWS Lambda(コンテナイメージ形式での実行)など、 AWS の主要なコンテナ関連サービスと高い互換性を持っています。そのため、 AWS 上でコンテナを利用する際の標準的なイメージ管理基盤として利用されるケースも多く、セキュリティ、可用性、運用性の観点からも安心して利用できるサービスとなっています。 Amazon ECR の主な機能 Amazon ECR には、単なるコンテナイメージの保管場所にとどまらず、セキュリティや運用効率を高めるためのさまざまな機能が備わっています。例えば、イメージのスキャンによる 脆弱性 検出、ライフサイクルポリシーによる不要なイメージの自動削除、IAM と連携した細かなアクセス制御など、実運用において役立つ機能が充実しています。 私自身の理解を整理する意味も込めて、本記事では面白いと感じた機能について、実際の利用シーンも想定しながらご紹介します。 タグフィルタ Amazon ECRでコンテナイメージを管理する際、イメージタグの上書き可否を制御できることをご存知でしょうか。ECRのイメージタグミュータビリティは、同じタグ名で異なるイメージを上書きできるかどうかを制御する機能です。 MUTABLE(ミュータブル) - デフォルトの設定 同じタグで何度でも上書き可能 例えば latestタグ を更新し続けるような運用に適している IMMUTABLE(イミュータブル) 一度プッシュしたタグは二度と上書きできない セキュリティとトレーサビリティを重視する運用に最適 IMMUTABLE_WITH_EXCLUSION(除外設定付きイミュータブル) - 比較的新しい機能 基本的にイミュータブルだが、特定のタグパターンだけミュータブルにできる 柔軟性とセキュリティのバランスが取れた設定 例えば、latest タグを使ってコンテナイメージを運用しているプロジェクトでは、latest を継続的に更新するために イメージをミュータブル(上書き可能) に設定しているケースが多く見られます(いわゆるlatest運用)。確かにこの設定にしておくと、同じタグ名であれば常に最新のイメージを参照できるという利便性はありますが、意図せず古いバージョンが上書きされてしまったり、本番環境と開発環境でイメージ内容が一致しなくなるといったリスクが高まるという危険性も伴います。 このような運用による問題を避けるために、 Amazon ECR では タグフィルタ機能 が登場しました。タグフィルタを利用することで、特定のタグ名や形式に対してのみ更新を許可したり、逆に更新を制限することができます。例えば、latest タグはミュータブルのままにしておきつつ、その他の重要なタグはイミュータブルに固定する、といった運用が可能になります。 タグフィルタ機能は コンソール上から簡単に設定できるだけでなく、Terraform の AWS プロバイダーでもすでにサポートされています。インフラをコードとして管理している場合でも、Terraform で柔軟にルールを定義・再利用できるため、安定したタグ運用を実現しやすくなっています。 resource "aws_ecr_repository" "this" { name = "blog-app" image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION" image_tag_mutability_exclusion_filter { filter = "latest" filter_type = "WILDCARD" } } アーカイブ ストレージクラス Amazon ECRを使い続けていると、過去のバージョンのコンテナイメージが蓄積され、ストレー ジコス トが気になってきます。削除するには コンプライアンス 上の保持が必要だったり、いざというときの ロールバック 用に残しておきたかったりと、なかなか削除できないイメージも多いはずです。 そんな課題を解決するために、 AWS は2024年に アーカイブ ストレージクラス(Archive Storage Class)機能をリリースしました。この機能を使うと、アクセス頻度の低いコンテナイメージを通常ストレージの約3割削減されたコストで保管できます。厳密には、最初の150TBまでは標準ストレージと同じコスト($0.10)なのですが、それ以上の アーカイブ がある場合はディスカウントされるようです(以下リファレンスですが、日本語版では情報が掲載されておりませんでしたのでご注意ください)。 https://aws.amazon.com/jp/ecr/pricing/ 項目 標準ストレージ アーカイブ ストレージ 月額コスト(ap-northeast-1) $0.10/GB $0.07/GB(約3割減) アクセス時間 即座 復元が必要(数時間) 最小保存期間 なし 90日間 使用シーン 頻繁にpull/deploy 長期保管・ コンプライアンス 復元コスト なし pull時に復元料金が発生 過去バージョンの長期保管 ロールバック 用の古いバージョン メンテナンス終了した古いバージョン などで用意しておくべきイメージに対して、本機能を駆使することが多いようです。 アーカイブ は手動で実施することもできますし、もちろん CLI やライフサイクルポリシーを使ってイメージを遷移させることもできます。以下はそれぞれの設定例です。 コンソール CLI aws ecr put-image-lifecycle-policy \ --repository-name blog-app \ --image-ids imageTag=v1. 0 . 0 ライフサイクルポリシー resource "aws_ecr_lifecycle_policy" "this" { repository = aws_ecr_repository.this.name policy = jsonencode ( { rules = [ { rulePriority = 1 description = "90日以上前のイメージをアーカイブ" selection = { tagStatus = "any" countType = "sinceImagePushed" countUnit = "days" countNumber = 90 } action = { type = "archive" } } , ] } ) } 簡単に設定できそうですね。 アーカイブ するとプルすることはできなくなり、エラーが返ってきます。 $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>@tag Error response from daemon: unknown: The requested image is in an inaccessible state. Please restore if needed プルする(使用する)ことはできませんが、 describe-images や list-images などをつかった詳細表示は可能です。 こうなると、めったに使わなさそうなイメージはすべて アーカイブ してしまった方が良い気もしますが、復元には一定のコストがかかります。また復元には20分ほど時間がかかるようです(とはいえ自分が試してみたときは数分で復元されてました)。何時間もかかるようなものではないようなので、障害復旧とかでもプロジェクトの要件次第では利用できるかもしれません。 # 復元リクエスト aws ecr restore-archived-images \ --repository-name blog-app \ --image-ids imageTag=v1.0.0 アーカイブ 後は削除も可能ですが、 アーカイブ されたイメージの最小ストレージ期間は 90 日間なようで、ライフサイクルポリシーで削除するには最低でも90日を設定する必要があるようです。もし90日未満のイメージを削除したい場合は、 batch-delete-image API を使った手動削除 になるので、もし短期間のみの アーカイブ を望むのであれば アーカイブ 設定は不適切になります。 ちなみにですが、2025年8月よりECRの リポジトリ あたり 100,000 のイメージまでがサポートされているようです。 https://aws.amazon.com/jp/about-aws/whats-new/2025/08/amazon-ecr-supports-100000-images/ このサポートが保証するイメージ数に、 アーカイブ したイメージが含まれるのかはわかりませんでした...。 コストだけでなく、キャパシティも最適化されると良いですね。 プルタイム更新の除外 Amazon ECRでは、コンテナイメージがプルされるたびに「最終プル時刻(LastRecordedPullTime)」が更新されます。この情報は、sinceImagePulledを使ったライフサイクルポリシーで「実際に使われていないイメージを自動削除する」という賢い運用に活用できます。 そんな中で、この時刻の更新を実施しないようにする方法も存在します。それが プルタイム更新の除外機能(Pull Time Update Exclusion) です。特定のIAMロールからのプルをLastRecordedPullTimeの更新対象から除外することで、より正確なライフサイクル管理が可能になります。 最近のアップデート機能のため、現時点ではまだ詳細な情報は多くありませんが、例えばセキュリティスキャナー(Snyk、Trivy、CrowdStrike など)や CI/CD パイプライン上のテスト処理が頻繁にコンテナイメージを pull すると、実際には本番環境で利用されていないイメージであっても「最近使用された」と判定されてしまうといった課題が想定されます。 その結果、本来であれば削除対象となるはずの古いイメージが、ECR のライフサイクルポリシーによって削除されず、不要なイメージが溜まり続けてしまう可能性があります。 こうした課題に対して、今回制御した制御機能を活用することで、スキャンやテスト用途の pull を「実運用の利用」とは区別して扱えるようになり、ライフサイクルポリシーをより正確に機能させることができます。 まとめ 今回は、 Amazon ECR における運用をより安全かつ効率的にするための機能として、 ミュータブル・イミュータブルのタグ制御(タグフィルタ)、 アーカイブ 機能、 プルタイムの更新除外機能 の 3 つを紹介しました。 これらの機能を活用することで、 タグの誤更新によるリスクを抑えられる 利用頻度の低いイメージを適切に保管・整理できる ライフサイクルポリシーをより意図通りに運用できる といったようなメリットが得られます。 ECR は単なるイメージ保管庫ではなく、工夫次第で運用負荷やコスト、セキュリティリスクの低減にも大きく貢献してくれるサービスだと感じました。 紹介できなかった機能もあるので第二弾としてまた掲載できればと思っております。 本記事が、これから ECR を本格的に活用していく方の参考になれば幸いです。 参考 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/archive-image.html https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/pull-time-update-exclusions.html https://aws.amazon.com/jp/ecr/pricing/ https://aws.amazon.com/jp/ecr/ 執筆: @yoneda.kosuke レビュー: @taguchi.kazutoshi ( Shodo で執筆されました )
アバター
こんにちは! エンタープライズ 第三本部 マーケティング IT部の母壁です。 この度Lookerが MCP に対応し、外部のLLM(生成AIエージェント)からのLookerのデータ探索および活用が可能となりました。この記事ではLooker MCP の導入による利便性と、データ分析・ダッシュボード作成自動化における効果について検証した内容をご紹介します。 まず初めに、 MCP およびLookerの基礎的な知識をご説明します。 MCPとは MCP Toolbox Lookerについて 【検証】Gemini CLIからLooker MCPを使ってみる 1. MCP Toolboxのインストール 2. Gemini CLIのインストール 3. MCPクライアントの構成 4. ダッシュボードの自動生成 5. プロンプトの調整 おわりに:実用性はあるのか? MCP とは MCP (Model Context Protocol)は、 生成AIと外部データソースとの連携を標準化するためのオープンスタンダードな 通信プロトコル です 。 簡単にいうと今まで各社がそれぞれの方法で開発していたLLMと外部ツールの接続方式を統一したら色々と便利だよね、ということです。 MCP を公開したAnthropic社は「AIのためのUSB-Cのような存在」と表現しています。(今や iPhone や Android が同じType-Cコネクタになったように、LLMと外部ツール間の統一された接続規格として機能するということですね!) MCP が オープンソース 化されてから続々と対応するツールやサービスが登場しており、今後さらなる広がりが予想されます。 MCP Toolbox MCP Toolbox とは、AIアプリケーションとデータベースなどの連携を効率化するための Google が提供する オープンソース ツール群です 。 AIエージェントが企業データに安全にアクセスし、 自然言語 でのデータ検索や分析といったタスクを効率的に実行するための基盤を提供します。このToolboxの一部として、2025年7月に「Looker MCP Server」が提供開始されました 。 Lookerについて Looker は Google が提供するビジネスインテリジェンス(BI)ツールです。単なるデータの可視化にとどまらず、ガバナンスの効いた統合的なデータプラットフォームとしての側面も持ち合わせていますが、今回はデータソースから収集、加工、分析したデータをダッシュボード上で可視化できるツールとしてご紹介します。 【検証】Gemini CLI からLooker MCP を使ってみる ここから本題となりますが、 Looker MCP を使用した生成AIによるダッシュボードの自動生成 についてご紹介します。 本来Lookerでダッシュボードを作成するには GUI 上でデータを探索し、可視化したタイル(グラフなど)を一つずつ作成・配置をする必要があります。これを生成AIによって自動化できたら便利そうですよね、、? そこでLooker MCP の登場によりダッシュボード自動生成の機能がどこまで実用的なものなのか検証してみました。 以下、こちらの 公式ドキュメント を参考に作業を行いました。 1. MCP Toolboxのインストール まず MCP Toolboxの最新バージョンをダウンロードします。 curl -O https://storage.googleapis.com/genai-toolbox/v0.14.0/darwin/arm64/toolbox バイナリを実行可能にします。 chmod +x toolbox 最後にバージョンを確認しておきましょう。(検証時はV0.12で実施しています) ./toolbox --version 2. Gemini CLI のインストール 今回はローカル環境からGemini CLI を使用して MCP サーバーを構成しました。 Gemini CLI の利用は、 npx コマンドを使って一時的に実行することも可能ですが、 settings.json の設定など繰り返し利用する想定なので、今回は npm でグローバルにインストールしています。 npm install -g @google/gemini-cli 3. MCP クライアントの構成 続いて MCP クライアントを構成します。 以下のように settings.json ファイルを定義することで、Geminiが MCP クライアントとして機能し、起動時に MCP サーバーを立ち上げてくれます。この時ファイルは .gemini フォルダ直下に配置する必要がある点に注意してください。 各要素を少し解説します。 looker-toolbox :Looker MCP Serverを使用する場合は指定します。 command :インストールした MCP Toolboxのパスを指定します。 env :Looker インスタンス に接続する MCP クライアントを構成するための 環境変数 です。 LOOKER_BASE_URL :接続するLookerのURLを指定します。 LOOKER_CLIENT_ID :LookerにログインするユーザーのIDです。 LOOKER_CLIENT_SECRET :Lookerにログインするユーザーのシークレットです。 設定は以上です!ローカル環境という点もありますが、想定よりも簡単に設定が完了しました。 ではいよいよプロンプトを投げてダッシュボードを作ってもらいます。 4. ダッシュボードの自動生成 Gemini CLI を起動してみます。 MCP クライアントが正常に構成されていれば「Using: 1 MCP server」と表記されます。 ダッシュボードを表示する元データとして、BigQueryのパブリックデー タセット に用意されている ECサイト の売上データを使用しました。 事前準備としてBigQueryからLookerにデータを取り込み、LookMLで エクスプローラ ー等の定義を行ったうえで作業しています。 またLookMLでは以下のようにメジャーを記述し、日本語のラベルを追加することでLLMが探索をしやすくなるように メタデータ を補填する工夫を行いました。メジャー名に略称を使用している場合や、専門的な単語がある場合に、日本語のラベルを定義することで日本語のプロンプトに対して探索の精度が上がることは事前に確認しています。 この状態でLLMにダッシュボード生成を依頼してみましょう。 まずは事前に用意した エクスプローラ ー「hahakabe02」を指定して、ダッシュボードの中身については特に指定せずに生成を依頼してみます。 hahakabe02を使用して、2023年のセールスダッシュボードを作成してください。 依頼をするとまずはログインするユーザーの参照できる範囲でどのデータを探索するか逐一確認されます。参照するデータを選択することでダッシュボードに不要なデータを弾くことができます。 LLMが使用できるツールには例えば以下のようなものがあります。 get_models :LookML モデルの一覧を取得 get_explores : エクスプローラ ーの一覧を取得 get_dimensions :指定された Explore のディメンションの一覧を取得 get_measures :指定された Explore のメジャーの一覧を取得 query :クエリを実行してデータを返却 make_dashboard :ダッシュボードを作成しURLを返却 add_dashboard_element :ダッシュボードにタイルを追加 一連の探索が終わるとクエリを実行してダッシュボードに表示するタイルを作成してくれます。できあがったダッシュボードを確認してみましょう。 月ごとの売上の推移や 都道 府県別の売上など、お題に合うように指標を決めてタイルを作成してくれています。 一方で各タイルの集計条件を細かく確認してみると、例えば総売上の指標については発注情報のうち、「購入日時」のディメンションではなく「製造日時」のディメンションでフィルタリングしているようでした。このような集計条件の指定はプロンプトで指定してあげるほうが良さそうです。 5. プロンプトの調整 改めてプロンプトを調整してダッシュボード生成を依頼してみます。タイルの種類、参照するメジャー、フィルターの条件など各タイルに指定してみます。 Explore「hahakabe02」を使用して、新しいダッシュボード「2024年セールスダッシュボード demo」を作成してください。 以下のビジュアライゼーションをダッシュボードに追加してください: 総売上 タイプ:単一値 メジャー:[総売上] 日付フィルタ:[購入日時]が2024年である 値の形式:$#,##0 平均注文金額 タイプ:単一値 メジャー:[平均注文金額] 日付フィルタ:[購入日時]が2024年である 値の形式:$#,##0 ... できあがったダッシュボードを見てみるとフィルターが正常に適用され、2024年の購入データのみが表示されていることが確認できました!またグラフの形式や使用するメジャーを指定することである程度望んでいた表示になっていました。 ただし「値の形式」で表示桁数を整数値にするようにプロンプトで指示しましたが、LLMがそこを設定することは現状できないようでした。やはり作成されたグラフが正しいデータを参照しているのかの確認や、最終的な見た目の仕上げにはまだ人の手が必要です。 おわりに:実用性はあるのか? MCP はデータ統合・分析基盤やLLMの外部システム連携において、急速的に浸透しつつあります。Lookerが MCP に対応したことで実務で使える場面も増えてくると大いに期待できます。 1. ダッシュボード作成の初動を効率化 先述したように最終的なダッシュボードの正確性の確認や表示スタイルのチューニングなどには人力のコストがある程度必要なものの、作業の初動を早められることは確実だと感じました。また対象のデータに精通していない人でもAIとの会話でデータの探索、グラフの作成ができるため、人員調達のハードルも下がることが期待されます。 2. より高度な インサイト の導出 従来のLookerの組み込み会話分析機能では、通常1つの エクスプローラ ーの範囲内でデータをクエリしますが、複数の エクスプローラ ーを探索しデータを統合することでより柔軟で高度なデータ分析やレポート作成ができるようになると思います。また MCP サーバーを複数立てることでLookerだけでなく CRM やGoogleDriveなど外部サービスと連携することも可能です。例えば今回のように顧客データや売上だけでなく、営業活動のデータやウェブサイト行動のデータなどを集約し、横断的な分析と インサイト の提示を得ることも可能だと思われます。 このようなメリットが期待できる一方で、改めてLookMLにおけるデータの定義を充実させることが会話分析の精度向上に不可欠であることを実感しました。 最後までお読みいただきありがとうございました。 本記事ではLooker MCP を中心にご紹介をしましたが、弊社ではLookerの導入実績も豊富にありますので、Lookerの導入を検討されている、あるいはLookerの次の活用フェーズをお考えの際にはぜひご相談ください! 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @hahakabe.kiichi レビュー: @kinjo.ryuki ( Shodo で執筆されました )
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 この度、2026 Japan All AWS Certifications Engineers クライテリア の応募基準を満たしました。約1年間(厳密には1年1か月)という比較的短期間で全冠でき、資格取得のための学習でかなり クラウド への理解が深まったと感じております。 今回は、 クラウド 初心者(ひいてはIT初心者)が、どのようにして AWS 資格を全冠得を進めたかなどを紹介できればと思います! これから AWS 資格に挑戦する人や、「何から取ればいいかわからない」「モチベーションが続かない」という人の参考になれば嬉しいです。 (同様に新卒1年目で条件を満たした大岡さんのブログも合わせてご覧ください!1年目で全冠を達成されたのはすごいですね、、!全冠を目指されたきっかけや思いが綴られておりますのでぜひ。) https://tech.dentsusoken.com/entry/2025/12/23/%E3%80%90AWS%E8%B3%87%E6%A0%BC%E3%80%91%E6%96%B0%E5%8D%921%E5%B9%B4%E7%9B%AE%E3%81%8CAWS%E8%B3%87%E6%A0%BC%E3%82%92%E5%85%A8%E5%86%A0%E3%81%97%E3%81%9F%E3%81%AE%E3%81%A7%E6%8C%AF%E3%82%8A%E8%BF%94 はじめに 他の方のブログでも紹介していただいていますが、そもそも 「2026 Japan All AWS Certifications Engineers」 とは、 AWS Partner Network(APN)に参加している会社に所属し、2026年度のクライテリアで定義された AWS 認定資格をすべて保持している AWS エンジニアを対象とした 表彰プログラム です。 2026年度のクライテリアでは、以下の AWS 認定資格を すべて有効な状態で保持していること が条件です。 資格は申し込み時点で有効であり、2026年4月30日までに有効期限が切れないことが応募要件となっています(うっかり失効しそう)。 レベル 必須資格 Foundational AWS Certified Cloud Practitioner Foundational AWS Certified AI Practitioner Associate AWS Certified Solutions Architect – Associate Associate AWS Certified SysOps Administrator – Associate または AWS Certified CloudOps Engineer – Associate Associate AWS Certified Developer – Associate Associate AWS Certified Data Engineer – Associate Associate AWS Certified Machine Learning Engineer – Associate Professional AWS Certified Solutions Architect – Professional Professional AWS Certified DevOps Engineer – Professional Specialty AWS Certified Security – Specialty Specialty AWS Certified Machine Learning – Specialty または AWS Certified Generative AI Developer – Professional Specialty AWS Certified Advanced Networking – Specialty なお「SysOps Administrator – Associate」か「CloudOps Engineer – Associate」のどちらか1つの取得でもOKです。 AWS Certified Machine Learning – Specialty は 2026年3月31日をもって受験終了予定 ですが、取得していない場合は AWS Certified Generative AI Developer – Professional を取得することでクライテリアを満たすことができます。 https://aws.amazon.com/jp/blogs/psa/2026-japan-all-aws-certifications-engineers-criteria/ 受験してみた感想ですが、SysOps Administrator – Associate と CloudOps Engineer – Associate にそこまで大きな差は感じられませんでした。最近アップデートがあった資格なだけあって身構えていましたが、これまで対象範囲でなかったECSやEKS、そしてCDKについて簡単に対策していけば、十分に合格できるかと思います。 ロードマップ 気になる取得までのロードマップですが、私は以下のような間隔と順番で資格取得いたしました。 資格 レベル 取得時期 AWS Certified Cloud Practitioner Foundational 2024年12月 AWS Certified Solutions Architect – Associate Associate 2025年1月 AWS Certified Solutions Architect – Professional Professional 2025年3月 AWS Certified Security – Specialty Specialty 2025年5月 AWS Certified Advanced Networking – Specialty Specialty 2025年7月 AWS Certified AI Practitioner Foundational 2025年8月 AWS Certified Machine Learning – Specialty Specialty 2025年9月 AWS Certified Machine Learning Engineer – Associate Associate 2025年10月 AWS Certified Data Engineer – Associate Associate 2025年11月 AWS Certified Developer – Associate Associate 2025年12月 AWS Certified DevOps Engineer – Professional Professional 2026年1月 AWS Certified CloudOps Engineer – Associate Associate 2026年1月 こちらも感想になりますが、やはり AWS に慣れてない最初は「受かりそう!」と思えるまで時間がかかりました。(特にSAP合格までは聞き慣れないサービスばかりで本当に苦労しました……)。 ただ、学習を続けていくうちに少しずつ理解が積み重なり、実際に受験してみると 「意外と受かるな」という感覚も持てるようになりました。 特に後半は、 AWS のサービス全体像や設計思想への理解が深まってきたことで、問題文を読んだ瞬間に「何を問われているのか」が掴める場面が増え、試験への 心理的 なハードルもかなり下がったように思います。 ただ落ちると 2週間は再受験できない ので、適当に受けるのではなく、しっかりと準備をしてから挑むことも大事ですね。 進め方 資格取得までの進め方についてですが、基本的には多くの方のブログで紹介されているとおり、オンライン問題集や教材を中心に学習していました。 これはあくまで個人的な学習スタイルですが、Udemyなどの動画講座を「視聴する」よりも、自分で実際に手を動かして調べたり、検証したりする方が圧倒的に理解が深まると感じました。実際に AWS 環境を触りながらサービスの挙動を確認し、設定を試して失敗し、なぜそうなるのかを調べる、みたいなプロセスを繰り返すことで、知識が単なる暗記ではなく、実感を伴った理解として身についていったと思います。 ただ一方で、自分の知らないサービスを知るきっかけを得る、 AWS のベストプラクティスを体系的に学ぶ、という意味では、動画講座や解説コンテンツをチェックする方法も非常に有効だと感じています。インプット(講座)とアウトプット(実践)をバランスよく組み合わせることが、結果的に一番の近道かと。 AIの活用 分からない内容が出てきた際にネット検索で調査するのも良いですが、個人的には近年発展したAIに頼ってみるのも有効だと思います。 2025年になっていろいろ出てきた AWS の MCP サーバについてご存じでしょうか。 MCP (Model Context Protocol)は、LLMアプリケーションと AWS サービスやリソースを接続するための標準化された プロトコル です。こちらは ユースケース に応じて AWS 環境の構築・運用・ トラブルシューティング など、様々なシーンで活用できる強力なツールになります。普段は開発などで使用しますが、たまに資格勉強でも利用していました。 https://awslabs.github.io/mcp/servers/ 利用方法についてはいろいろブログで記載されているので割愛しますが、例えば以下のような MCP サーバは使ってみても良いかなと思います。 AWS Documentation MCP Server AWS 公式ドキュメントをリアルタイムで検索・参照してくれる MCP サーバーです。 常に最新の情報をもとに回答してくれるため、特に最近アップデートされた資格や新サービス周りを調べる際に非常に有効だと感じました。 AWS IaC MCP Server CloudFormation や AWS CDK、Terraform などの Infrastructure as Code(IaC) に関する設計・記述を支援してくれる MCP サーバーです。 リソース定義の書き方を調べたり、テンプレートのレビューをしたりする用途に向いており、「この構成をコードでどう書けばよいか?」といった疑問を解消するのに役立ちます。実務でIaCを触っている人はもちろん、SAP や DOP など設計系試験の理解を深める用途でも使えると感じました。 AWS Pricing MCP Server リアルタイムの AWS 料金情報にアクセスし、コスト分析を行ってくれる MCP サーバーです。 個人的には使用頻度はそこまで高くありませんでしたが、 AWS 資格の試験では「コスト最適化」の観点でベストプラクティスを問われることが多いため、ケースによっては十分活用できるツールだと思い、紹介しています。 AWS の公式サイトやブログなどを調べて理解を深めてもらっても良いですが、一旦概要を知りたい際や、調べ方がわからない場合には上記 MCP サーバなどを活用して一旦AIに投げてしまっても良いですね! まとめ 改めて、 AWS 資格を全冠し、「2026 Japan All AWS Certifications Engineers」の応募クライテリアを満たすことができました。 もちろん資格取得そのものも目標のひとつでしたが、それ以上に、学習を通して クラウド 全体への理解が深まり、自信を持って AWS に向き合えるようになったことが何より大きな成果だったと感じています。 なお、All Certs を達成すると、 AWS Summit Japan で表彰されるようで、年によっては副賞として景品がもらえることもあるようです。 クラウド に少しでも興味がある方であれば、ぜひ一度、 AWS 資格へのチャレンジ、そして全冠を目標にしてみてほしいと思います。 (来年も全冠維持します...) 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @yoneda.kosuke レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 今回は、 AWS サービス上でのコンテナイメージの運用と、それをより効率的に実現するFutureVulsについて、基礎的な部分を中心に共有させていただきます。 本ブログは、 10月下旬にあった社内向けの勉強会 にて発表させていただいた内容を含んだものになっており、少し振り返りながら改めてまとめさせていただきました。 勉強会当日は100名以上の参加者となり、そのような場での発表は私自身とても良い経験でした。勉強会にご参加いただいた方、コメントいただいた方、取りまとめていただいた方、そしてフューチャー社の皆様、改めてありがとうございました。 はじめに コンテナ制御 コンテナイメージ管理 リポジトリの種別 自動継続的スキャン 脆弱性管理 自動トリアージ FutureVuls AI 未然防止 まとめ 参考 はじめに 普段、皆さんはどのような方法でアプリケーションを開発・実行されていますでしょうか。 クラウド 上でアプリケーションを起動する方法にはさまざまな選択肢があります。 仮想マシン 上で直接実行する方法、PaaS を利用する方法、サーバレス アーキテクチャ など、用途や規模に応じて複数のアプローチが存在します。 その中でも、近年特に多く利用されているのが コンテナサービス です。 コンテナは、アプリケーションとその実行に必要なライブラリや設定をまとめて扱えるため、開発環境と本番環境の差異が生まれにくく、デプロイやスケーリングを容易に行えるという特徴があります。また、移行性の高さも大きなメリットかと思います。別環境への移行や、新しい環境の構築時などで効率的に作業を進めることができ、個人的にはこのあたりは非常に有用だと感じています。 AWS ではコンテナを扱うのに特化したさまざまなサービスが用意されており、それぞれの特性を理解して適切に使い分けることが重要です。ここではその代表的なものを取り上げます。 コンテナ制御 AWS でコンテナ オーケストレーション を検討する際、代表的な選択肢として Amazon EKS と Amazon ECS の 2 つのサービスが挙げられます。 Amazon EKS は、 Kubernetes をマネージドサービスとして提供するもので、 Kubernetes のエコシステムや標準的な運用手法をそのまま利用できる点が特徴です。一方、 Amazon ECS は AWS 独自のコンテナ オーケストレーション サービスであり、 Kubernetes を直接意識することなく、 AWS に最適化された形でコンテナを管理・実行することができます。そのため、ECS は比較的シンプルな構成でコンテナを扱えるという利点があります。 また、これらのコンテナサービスでは、コンテナをどこで起動するか という点も重要な要素になります。 AWS では主に、 Amazon EC2 や AWS Fargate といった起動方式が利用されます。 Amazon EC2 を利用する場合は、 仮想マシン 上でコンテナを実行するため インスタンス のスペックや台数を自分で管理する必要がありますが、その分柔軟な構成が可能です。一方、 AWS Fargate はサーバレスな起動方式であり、 インスタンス 管理を意識することなく、必要なリソースを指定するだけでコンテナを実行できます。 以下で、EC2 と AWS Fargate について簡単に整理してみました。 項目 Amazon EC2 AWS Fargate 起動方式 仮想マシン 上でコンテナを実行 サーバレスでコンテナを実行 インスタンス 管理 必要 不要 OS管理 必要 不要 スケーリング 自前設定 自動 課金方式 インスタンス 単位 vCPU / メモリ使用量 自由度 高い ある程度制約あり また、ご存じの方も多いかもしれませんが、 AWS Lambda でもコンテナイメージを利用してアプリケーションを実行することができます。(コンテナLambdaと呼ぶこととします) AWS Lambda は FaaS(Function as a Service)に分類されるサービスで、従来は ZIP ファイル形式でアプリケーションコードをデプロイする方法が一般的でした。この方式は手軽である一方、デプロイ可能なファイルサイズに制限(最大250MB)があり、ライブラリや依存関係が多いアプリケーションでは制約になることがあります。 一方で、Lambda では コンテナイメージを用いたデプロイ にも対応しており、この場合は10 GB までのサイズのイメージをデプロイすることができます。さらにLambda web adapterの登場により、従来は ECS や EC2 上で動かすことが一般的だった Web アプリケーションを、ほぼそのままの構成で Lambda 上に載せることが可能になりました。さらにコンテナサービスの幅が広がりました。 コンテナイメージ管理 上記のように、 AWS にはコンテナを制御・起動するためのさまざまなサービスが存在しますが、それらを支える重要な コンポーネント として Amazon Elastic Container Registry(ECR) が用意されています。 Amazon ECR は、主にDocker コンテナイメージを管理するためのマネージド レジストリ サービスであり、 Amazon ECS、 Amazon EKS、 AWS Lambda(コンテナイメージ) など、 AWS の主要なコンテナ関連サービスと高い互換性を持っています。これにより、同じコンテナイメージを複数のサービス間で再利用しながら、安全かつ一元的に管理することが可能です。 そんな Amazon ECR には、いろいろ面白い機能がありそうですので、また別の記事で詳しく紹介させていただきます。今回は一部分のみ抜粋して紹介します。 リポジトリ の種別 Amazon Elastic Container Registry(ECR)には、プライベー トリポジ トリ と パブリック リポジトリ の 2 種類が用意されています。どちらもコンテナイメージを管理するためのサービスですが、用途や設計思想には明確な違いがあります。 以下自分の理解です。 プライベー トリポジ トリ AWS アカウント内でのみ利用できるコンテナイメージの保管場所で、社内向けアプリケーションや、外部に公開したくない独自のアプリケーションイメージを扱う場合には、プライベー トリポジ トリが適しています。 パブリック リポジトリ インターネット上に公開されたコンテナイメージを配布するための仕組み。 AWS アカウントを持っていないユーザーでもイメージを取得できるため、 OSS の配布やサンプルアプリケーションの公開などに向いています。 パブリック リポジトリ であれば基本的に認証なしでイメージ取得が可能ですが、プライベー トリポジ トリでは認証が必要になります。実際に認証なしでプライベー トリポジ トリへpullを試みると、以下のように拒否されてしまいます。 $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>:tag Error response from daemon: pull access denied for <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>, repository does not exist or may require 'docker login': denied: Your authorization token has expired. Reauthenticate and try again. 認証してトライしてみます。 # 認証 aws ecr get-login-password --region ap-northeast-1 \ | docker login \ --username AWS \ --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>:tag tag: Pulling from <対象リポジトリ> 11b72ab0c7e9: Pull complete 940530de9b36: Pull complete 711f628928ef: Pull complete 75131756b8ac: Pull complete ... 今度はうまく接続できてそうです。 CI/CDで認証ステージを組み込んであげれば、自動的にECRへログイン→ECRへのイメージプッシュが行えます。 自動継続的スキャン コンテナは軽量で再利用しやすいという利点がある一方で、ベースイメージやライブラリをそのまま使い回すことが多く、知らないうちに 脆弱性 を含んだ状態で運用してしまうケースも少なくありません。そのため、コンテナイメージに含まれる 脆弱性 をどのように管理するかは、運用において避けて通れない課題となっています。 特に、複数のサービスで同じコンテナイメージを利用している場合、1 つの 脆弱性 が複数のシステムに影響を及ぼす可能性があります。こうしたリスクに対応するためには、単発のチェックに留まらず、継続的に 脆弱性 を把握し、適切に対応していく仕組みを整えることが重要です。 そこで AWS ではコンテナイメージの 脆弱性 を管理するための仕組みとして、プライベート レジストリ を対象に 基本スキャン と 拡張スキャン の 2 種類の 脆弱性 スキャン方式が用意されています。 基本スキャンでは、イメージのプッシュ時またはオンデマンドでのスキャンが可能であり、OSに含まれるパッケージの既知の 脆弱性 を検出することができます。こちらは基本的に無料で使用することができ、スキャン結果についてはECRのコンソール上から確認することができます。 スキャンには共通 脆弱性 識別子 (CVE) データベースを使用する2種類の設定があったようですが、Clairと呼ばれる オープンソース の 脆弱性 データベースを用いたスキャンは廃止され、現在は AWS ネイティブスキャンという方式がGAとなっている状態でした。利用するためにはオプトインが必要なようですね。 もう一つのスキャン方法は、拡張スキャンと呼ばれる方式です。こちらは有料になりますが、 Amazon Inspector と統合され、より詳細な 脆弱性 検出が可能になります。また、スキャン対象もOSだけでなく、 プログラミング言語 のパッケージやライブラリにも対応するため、より網羅的に 脆弱性 管理が可能となります。オンデマンドではなく継続的なスキャンがサポートされる点も良いですね。 Inspector統合なこともあり、検出結果は以下画像のようにInspectorのダッシュボードから確認できます。大まかに 脆弱性 が検知されたかも確認できますし、画像にはありませんがスキャン結果詳細を確認することももちろん確認可能です。 拡張スキャンによる料金は、東京リージョンで以下のようになっているようです。 スキャンタイミング コスト プッシュ時 対象イメージにつき $0.11 継続スキャン スキャンごとに $0.01 こうしてみると、そこまで高額なわけでもないので性能を考えると拡張スキャンを採用しておくのが良いのかなと思います。 脆弱性 管理 Amazon Inspector のみを利用した 脆弱性 管理も有効な選択肢ですが、コンテナイメージの数が増えてきたり、複数チーム・複数サービスで同時に運用している場合、「検知したあと、どう管理・運用するか」 という点も重要になってくると思います。 こうした中、 脆弱性 管理ツールの「FutureVuls」を用いることでECR上のイメージの 脆弱性 管理をより効率化することができます。 詳細な設定方法については公式ドキュメントを参照いただければと思いますが、おおまかな流れは以下になります。 FutureVuls で AWS 連携の設定を実施 FutureVulsで 脆弱性 管理対象とする ECR リポジトリ を選択 以降、 AWS 側で実行されるスキャン結果を定期的に FutureVuls に取り込み、 脆弱性 ・タスクとして管理 (公式) help.vuls.biz 手順としては非常に簡単で、個人的にはUIも直感的でわかりやすいなと思っています。 FutureVuls にはECRイメージの 脆弱性 管理を支えるいくつかの機能があり、深ぼっていきたいところですが、ここでは重要な機能と、大変便利な機能を一つずつピックアップし紹介します。 自動 トリアージ FutureVuls の大きな特徴の一つが、SSVC(Stakeholder-Specific Vulnerability Categorization) という フレームワーク に基づいた 脆弱性 管理を実現できる点です。FutureVuls ではこの フレームワーク を用いてリスクを評価し、優先順位付けを行う、自動 トリアージ 機能を搭載しており、「out of cycle(重大な検出)」や「Immediate(緊急性の高い検出)」などと自動分類することで、セキュリティチームが比較的重要な脅威に迅速に対応できるようになります。 さらに 脆弱性 対応のタスク管理もFutureVulsにて実施可能で、対応ステータス等の把握が容易になります。 SSVCについて詳しく知りたい方はFutureVuls様のこちらの記事をご参照ください: www.vuls.biz FutureVuls AI FutureVuls には、 脆弱性 管理をさらに効率化するための機能としてFutureVuls AI が提供されています。 FutureVuls AI を簡単に紹介すると、 検出された 脆弱性 に対して分かりやすいサマリを提示し、対応判断を支援してくれる AI アシスタント です。単に 脆弱性 を検知するだけでなく、「この 脆弱性 は何が問題で、どこに影響があるのか」を把握するまでの時間を大きく短縮してくれます。 脆弱性 対応を行う際には、CVE 情報や各種アドバイザリを確認することが一般的ですが、これらの情報は 英語で記載されており、数ページに及ぶことも少なくありません。そのため、内容を正しく理解するまでに想像以上の時間がかかるケースもあります。 FutureVuls AI は、こうした 大量かつ専門的な情報を要約し、ポイントを整理して提示してくれるため、 脆弱性 の種類 影響範囲の有無 対応の必要性や優先度の判断材料 といった情報を、短時間で把握することが可能になります。 未然防止 AWS Inspector や FutureVuls を活用することで、 脆弱性 を効率的に検知・管理することは可能ですが、理想を言えば 「 脆弱性 が検知されない状態」を目指したい ところです。つまり、問題が発生してから対応するのではなく、そもそも 脆弱性 を含まないイメージを作るための仕組みづくりが重要になります。 その取り組みの一例として、ECR にコンテナイメージを push する前の段階で、依存関係のアップデートを自動で検知・提案してくれるツールを導入する方法があります。具体的には、 GitHub 上で利用できる Renovate や Dependabot などの自動化ツールが代表的です。 これらのツールを導入すると、アプリケーションや Dockerfile で使用しているライブラリ、ベースイメージの新しいバージョンが公開された際に、変更内容を反映した Pull Request を自動で作成してくれます。開発者はその PR をレビューしてマージするだけでよく、手作業でバージョンチェックを行う必要がありません。 さらに、利用するコンテナイメージのベースとして Distroless イメージを採用することで、セキュリティリスクをより一層抑えることができます。Distroless イメージは、OS やシェルといった不要な コンポーネント を含まず、アプリケーションの実行に必要な最小限のライブラリのみで構成されたイメージです。そのため、攻撃対象となり得るパッケージの数を減らすことができ、結果として 脆弱性 が検知される可能性そのものを低減できます。 このように、 依存関係の自動アップデートによる「事前対策」 Distroless イメージの採用による「攻撃対象の最小化」 といった取り組みを組み合わせることで、コンテナイメージに含まれる 脆弱性 を大幅に削減し、より安全で持続可能なコンテナ運用を実現することが可能になります。 まとめ 今回は、コンテナイメージの管理方法として AWS が提供する Amazon Elastic Container Registry(ECR) を中心にご紹介しました。 ECS や EKS、Lambda など複数のコンテナ関連サービスと親和性が高く、イメージを一元的に管理できる点は、ECR の大きな強みと言えます。 また、実運用における 脆弱性 管理の観点では、FutureVuls を活用することで、検知から対応までをより効率的に実施できることをご紹介しました。 単にスキャン結果を確認するだけでなく、優先度に基づいた対応判断や、チケット管理・共有まで含めた運用を行うことで、継続的な 脆弱性 管理体制を構築しやすくなります。 勉強会の中でもコメントをいただきましたが、自動化できる部分は積極的に自動化し、可能な限り手動での設定や対応を減らしていくことは、今後ますます重要になっていくと感じています。今後は外部サービスとのさらなる連携も検討されているとのことで、より実践的な運用が実現できる点に期待が高まります。 参考 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-scanning.html https://aws.amazon.com/jp/inspector/pricing/ https://help.vuls.biz/manual/scan/image/ https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/amazon-elastic-kubernetes-service.html https://aws.amazon.com/jp/ecr/ 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @yoneda.kosuke レビュー: @taguchi.kazutoshi ( Shodo で執筆されました )
アバター