2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クロスアカウントの閉域 VPC にGenU をデプロイして共有リソース VPC 経由でオンプレミスから接続してみる

2
Posted at

AWS がオープンソースで公開している、生成 AI を安全に業務活用するためのビジネスユースケース集を備えたアプリケーション実装 generative-ai-use-cases (GenU) を自治体のガバメントクラウド AWS 環境で使えないかを個人的に検証しています。

GenU が利用する生成 AI サービスの Amazon Bedrock は、政府情報システムのためのセキュリティ評価制度(Information system Security Management and Assessment Program (ISMAP))の言明範囲 であり、自治体がこれを利用することは問題なさそうです。

また、GenU には閉域モードオプションがあり、閉域ネットワークで利用することも可能なので、自治体のマイナンバー利用事務系と言われる閉域ネットワーク内で使うこともできそうです。

しかし実際に自治体のマイナンバー利用事務系で GenU を使うには、いくつかの条件をクリアする必要があります。

  • 閉域 VPC にデプロイして、閉域ネットワーク経由でオンプレミスから接続できること(名前解決も含む)
  • 東京、大阪リージョンで完結していること(クロスリージョン推論も同様)

これに加えて、自治体のガバメントクラウド AWS 環境のネットワーク構成は、共有リソースアカウント(ネットワークアカウントと呼ばれることが多い)を構築して Direct Connect で接続し、共有リソースアカウントの Transit Gateway に複数アカウントの VPC が接続されていたり、共有リソースアカウント VPC に Route 53 インバウンドエンドポイントやその他インターフェース型 VPC エンドポイントなどの複数 AWS アカウントから共有できるネットワークリソースを置いたりしているケースが多いです。

GenU は AWS CDK でデプロイする仕組みです。CDK で作成するリソースには極力ドリフトが生じないようにし、また既存の環境への影響も最小限としたいです。

そこで、共有リソースアカウントが存在する既存の環境に GenU を、以下の条件でデプロイできるか検証してみました。

  • 共有リソースアカウントとは別のアカウント(GenU アカウントとします)の既存 VPC に GenU をデプロイする
  • GenU アカウント VPC と共有リソースアカウントの Transit Gateway を接続し、オンプレミスから GenU アカウント VPC へルーティングさせる
  • オンプレミスからの名前解決は共有リソース VPC で行うこととし、GenU にアクセスするオンプレミスのクライアントが必要とするインターフェース型 VPC エンドポイントは全て共有リソース VPC に作成する

なお、GenU の CDK のコードも極力変更せず、設定変更はパラメーターでの変更を優先することにします。

GenU 閉域モードのクロスアカウント接続構成の概要

まず GenU がデプロイされる前の既存環境は以下のとおりとします。全て東京リージョンに作成します。

onpremises-to-genu-before.drawio.png

GenU アカウントの既存 VPC とプライベートサブネットは GenU のために新規で事前に作り、Transit Gateway との接続も済ませておきます。

GenU の CDK はデフォルトでは VPC を新規作成するようになっていますが、CDK で作った VPC を Transit Gateway に接続する際、クロスアカウントになるので作業が手間になります。そこで既に Transit Gateway に接続済みの VPC とサブネットを指定してデプロイするよう、GenU のオプションを設定することにしました。

また、共有リソースアカウント VPC には Route 53 インバウンドエンドポイントがあり、GenU にアクセスするオンプレミスのクライアントから名前解決が必要なサービスのインターフェース型 VPC エンドポイントも作成済みとします。

GenU の CDK も同じインターフェース型 VPC エンドポイントを作るので、コストを考えると無駄があります。これを回避する方法はいくつかありますが、GenU の CDK のコードを変更せず、既存の共有リソース VPC での名前解決に影響を与えずにやることが難しいため、多少のコストは諦めることにします。

GenU デプロイ後の構成は以下のようになります。

onpremises-to-genu.drawio.png

GenU の CDK コードに変更が必要だった箇所について

先述のとおり、原則 GenU の CDK のコードには変更を加えないようにしたかったのですが、条件をクリアするためにはいくつか変更しなければならない箇所があったため、以下のような変更を行いました。

API Gateway へ共有リソース VPC のエンドポイントからアクセスできるようにする

GenU の CDK が作る API Gateway は、リソースポリシーで GenU をデプロイした VPC からのアクセスのみが許可されています。しかし今回の条件では、オンプレミスのクライアントは共有リソースアカウント VPC のインターフェース型 VPC エンドポイントから API Gateway へアクセスします。そこで、パラメーターで指定した VPC エンドポイント ID を API Gateway のリソースポリシーで許可するよう変更します。

packages/cdk/lib/construct/api.ts
 export interface BackendApiProps {
   // 省略
   readonly apiGatewayVpcEndpoint?: InterfaceVpcEndpoint;
+  readonly additionalApiGatewayVpcEndpointIds?: string[] | null;
+  // 省略
 }

 export class Api extends Construct {
   constructor(scope: Construct, id: string, props: BackendApiProps) {
   
     const {
       // 省略
       apiGatewayVpcEndpoint,
+      additionalApiGatewayVpcEndpointIds,
     } = props;

     const builtinAgentsJson = props.agents || '[]';
       policy: vpc
         ? new PolicyDocument({
-            statements: [apiGatewayVpcEndpoint!].map(
-              (e: InterfaceVpcEndpoint) => {
-                return new PolicyStatement({
-                  effect: Effect.ALLOW,
-                  principals: [new AnyPrincipal()],
-                  actions: ['execute-api:Invoke'],
-                  resources: ['execute-api:/*'],
-                  conditions: {
-                    StringEquals: {
-                      'aws:SourceVpce': e.vpcEndpointId,
-                    },
+            statements: [
+              apiGatewayVpcEndpoint!.vpcEndpointId,
+              ...(additionalApiGatewayVpcEndpointIds ?? []),
+            ].map((vpceId: string) => {
+              return new PolicyStatement({
+                effect: Effect.ALLOW,
+                principals: [new AnyPrincipal()],
+                actions: ['execute-api:Invoke'],
+                resources: ['execute-api:/*'],
+                conditions: {
+                  StringEquals: {
+                    'aws:SourceVpce': vpceId,
                   },
-                });
-              }
-            ),
+                },
+              });
+            }),
           })
         : undefined,
     });
packages/cdk/lib/generative-ai-use-cases-stack.ts
 export class GenerativeAiUseCasesStack extends Stack {
   
   constructor(
     scope: Construct,
     id: string,
     props: GenerativeAiUseCasesStackProps
   ) {
   
       // API
       const api = new Api(this, 'API', {
         // 省略
         apiGatewayVpcEndpoint: props.apiGatewayVpcEndpoint,
+        // Allow access from other VPC Endpoint (ex. shared VPC)
+        additionalApiGatewayVpcEndpointIds:
+          params.closedNetworkAdditionalApiGatewayVpcEndpointIds,
         // 省略
       });
packages/cdk/lib/stack-input.ts
 const baseStackInputSchema = z.object({
   // 省略
   // Closed network
   closedNetworkMode: z.boolean().default(false),
   closedNetworkVpcIpv4Cidr: z.string().default('10.0.0.0/16'),
   closedNetworkVpcId: z.string().nullish(),
   closedNetworkSubnetIds: z.array(z.string()).nullish(),
   closedNetworkCertificateArn: z.string().nullish(),
   closedNetworkDomainName: z.string().nullish(),
   closedNetworkCreateTestEnvironment: z.boolean().default(true),
   closedNetworkCreateResolverEndpoint: z.boolean().default(true),
+  closedNetworkAdditionalApiGatewayVpcEndpointIds: z
+    .array(z.string())
+    .nullish(),
 });

RAG チャットユースケースの Adbanced Parsing のパース用モデルに推論プロファイルを指定できるよう変更

RAG チャットユースケースで使うモデルについて、パラメーターでは Adbanced Parsing のパース用モデルに基盤モデルしか指定できませんでした。これでは Adbanced Parsing のパース用モデルに Claude を使いたい場合に推論を国内リージョンに閉じられないため、パラメーターで推論プロファイルを指定できるようにします。

packages/cdk/lib/rag-knowledge-base-stack.ts
 export class RagKnowledgeBaseStack extends Stack {
   constructor(scope: Construct, id: string, props: RagKnowledgeBaseStackProps) {

     knowledgeBaseRole.addToPolicy(
       new iam.PolicyStatement({
         effect: iam.Effect.ALLOW,
         resources: ['*'],
-        actions: ['bedrock:InvokeModel'],
+        actions: ['bedrock:InvokeModel', 'bedrock:GetInferenceProfile'],
       })
     );

+    // Cross-region inference profile IDs (e.g. jp.anthropic...) need the inference-profile ARN
+    const parsingModelArn =
+      typeof ragKnowledgeBaseAdvancedParsingModelId === 'string' &&
+      /^(us|us-gov|eu|apac|jp|au|ca|global)\./.test(
+        ragKnowledgeBaseAdvancedParsingModelId
+      )
+        ? `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${ragKnowledgeBaseAdvancedParsingModelId}`
+        : `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`;
+

     new bedrock.CfnDataSource(this, 'DataSource', {
     
       vectorIngestionConfiguration: {
         ...(ragKnowledgeBaseAdvancedParsing
           ? {
               // Enable Advanced Parsing only if it is enabled
               parsingConfiguration: {
                 parsingStrategy: 'BEDROCK_FOUNDATION_MODEL',
                 bedrockFoundationModelConfiguration: {
-                  modelArn: `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`,
+                  modelArn: parsingModelArn,
                   parsingPrompt: {
                     parsingPromptText: PARSING_PROMPT,
                   },
                 },
               },
             }
           : {}),

デプロイオプションの設定

GenU のオプションは packages/cdk/parameter.ts に設定します。今回は dev という環境名でデプロイすることにします。

ポイントとなる設定は以下のとおりです。

パラメーター名 設定値
closedNetworkMode 閉域モードを有効にする
closedNetworkDomainName 設定すると Route 53 プライベートホストゾーンが作成される
closedNetworkCertificateArn ACM のパブリック証明書の ARN なので、closedNetworkDomainName を設定する場合は事前に ACM で作成する必要あり
closedNetworkCreateResolverEndpoint VPC に Route 53 インバウンドエンドポイントを作成するかどうかを指定するものだが、今回は共有リソースアカウント VPC にこれが既にある想定なので false を指定
closedNetworkVpcId GenU アカウントの既存の VPC ID
closedNetworkSubnetIds GenU アカウントの既存 VPC のサブネット ID
selfSignUpEnabled 不特定のユーザーからサインアップできないよう無効にする
closedNetworkAdditionalApiGatewayVpcEndpointIds 今回独自に追加した設定で、API Gateway にクライアントがアクセスする時に使用される execute-api の VPC エンドポイント ID を指定
packages/cdk/parameter.ts
const envs: Record<string, Partial<StackInput>> = {
   // },
   dev: {
     // Parameters for development environment
+    region: 'ap-northeast-1',
+    modelRegion: 'ap-northeast-1',
+    modelIds: ['jp.anthropic.claude-sonnet-4-6'],
+    // Voice Chat は利用不可
+    speechToSpeechModelIds: [],
+    // 任意のオプション
+    ragKnowledgeBaseEnabled: true,
+    ragKnowledgeBaseStorageType: 's3vectors', // 'opensearch' or 's3vectors'
+    // 作成済みの Knowledge Base がある場合、ナレッジベース ID を指定
+    // ragKnowledgeBaseId: 'XXXXXXXXXX',
+    ragKnowledgeBaseStandbyReplicas: false,
+    ragKnowledgeBaseAdvancedParsing: true,
+    ragKnowledgeBaseAdvancedParsingModelId: 'jp.anthropic.claude-sonnet-4-6',
+    ragKnowledgeBaseBinaryVector: false, // s3vectors の場合は非対応
+    embeddingModelId: 'amazon.titan-embed-text-v2:0',
+    rerankingModelId: 'amazon.rerank-v1:0',
+    queryDecompositionEnabled: true,
+    agentEnabled: false, // Bedrock Agent は新規に作成できなくなっている
+    // 以下は閉域のオプション
+    closedNetworkMode: true,
+    // ドメインを設定しない場合は以下の 2 つは不要
+    closedNetworkDomainName: 'web.genai.intra.morori.jp',
+    closedNetworkCertificateArn:
+      '<ACM のパブリック証明書の ARN>',
+    // 検証環境と Route 53 Resolver エンドポイントは作成しない
+    closedNetworkCreateTestEnvironment: false,
+    closedNetworkCreateResolverEndpoint: false,
+    // 既存の VPC を Subnet を指定する場合 (新規作成する場合は以下の 2 つは不要)
+    closedNetworkVpcId: '<GenU アカウント既存 VPC の VPC ID>',
+    closedNetworkSubnetIds: [
+      '<1 つ目のサブネット ID>',
+      '<2 つ目のサブネット ID>',
+    ],
+    // セキュリティ関連設定
+    selfSignUpEnabled: false,
+    allowedSignUpEmailDomains: ['<受信を許可するメールドメイン名>'],
+    // モニタリング用のダッシュボードの有効化
+    dashboard: true,
+    // API Gateway の VPC エンドポイントが共有リソース VPC のように別の VPC にある場合は指定
+    closedNetworkAdditionalApiGatewayVpcEndpointIds: ['<共有リソースアカウント VPC の execute-api の VPC エンドポイント ID>'],
   },

デプロイ前の事前確認

以下の作業は全てプロジェクトのルートディレクトリで実施します。

$ npm ci

もしデプロイしようとしているリージョンで初めて CDK Deploy する場合は事前に bootstrap が必要です。

$ npx -w packages/cdk cdk bootstrap --profile <プロファイル名>

cdk synth を実行してテンプレートに問題がないか確認します。

$ npx -w packages/cdk cdk synth -c env=dev --quiet --profile <プロファイル名>

cdk diff を実行して作成される AWS リソースの差分を確認します。

$ npm run cdk:diff -- -c env=dev --profile <プロファイル名>

cdk:test で差分を確認し、意図した変更だけなことを確認したら、スナップショットを更新します。

$ npm run cdk:test
$ npm run cdk:test:update-snapshot

ローカルの Docker でイメージのビルドが通るか確認します。

$ cd packages/cdk/fargate-s3-server
$ docker build --platform linux/amd64 -t genu-fargate-s3-server .
$ docker run --rm -p 8080:8080 --platform linux/amd64 -e BUCKET_NAME=dummy genu-fargate-s3-server
$ curl http://localhost:8080/healthcheck

curl などで Docker で実行しているサービスにアクセスして HTTP Status Code の 200 が帰ってこれば成功です。

CDK Deploy の実行

デプロイを実行します。

$ npm run cdk:deploy -- -c env=dev --profile <プロファイル名> --require-approval never

デプロイが完了したら、CDK でデプロイされた Route 53 プライベートホストゾーンのホストゾーン ID を確認し、残りの手作業を実施していきます。

Route 53 プライベートホストゾーンの関連付け

CDK で作成した Route 53 プライベートホストゾーンは、そのままでは GenU をデプロイした VPC にしか関連付けられていないので、オンプレミスのクライアントから名前解決ができません。そこでこれを、Route 53 インバウンドエンドポイントのある共有リソースアカウントの VPC へ関連付けるための承認要求を行います。

$ aws route53 create-vpc-association-authorization --hosted-zone-id <ホストゾーン ID> --vpc VPCRegion=ap-northeast-1,VPCId=<共有リソースアカウント VPC の ID> --profile <GenU アカウントのプロファイル名>

共有リソースアカウント側で関連付けを承認します。

$ aws route53 associate-vpc-with-hosted-zone --hosted-zone-id <ホストゾーン ID> --vpc VPCRegion=ap-northeast-1,VPCId=<共有リソースアカウント VPC の ID> --profile <共有リソースアカウントのプロファイル名>

VPC の関連付けが完了したら、GenU アカウント側で関連付けの承認要求を削除しておきます。

$ aws route53 delete-vpc-association-authorization --hosted-zone-id <ホストゾーン ID> --vpc VPCRegion=ap-northeast-1,VPCId=<共有リソースアカウント VPC の ID> --profile <GenU アカウントのプロファイル名>

Cognito ユーザープールに手動でユーザーを追加

セルフサインアップを無効にしている場合、マネジメントコンソール等から Cognito ユーザープールに手動でユーザーを追加する必要があります。

スクリーンショット 2026-10-03 23.24.29.png

実際の運用でもユーザーにメールアドレスを申請してもらい、Cognito ユーザープールから招待メールを送信するようにしようと考えています。

ここまで完了したら準備完了です。

GenU 閉域モードにオンプレミスから接続

オンプレミスのクライアントから closedNetworkDomainName に設定したドメインに Web ブラウザーで HTTPS 接続します。(指定しなかった場合は ALB の DNS 名に HTTP で接続)

サインイン画面が表示されるので、Cognito ユーザープールで事前に作成したユーザー名(メールアドレス)と仮パスワードでサインインします。

スクリーンショット 2026-10-03 232637.png

パスワードの設定を行ったら、GenU の画面が表示されます。チャットなどのユースケースを試して動作確認してみましょう。

スクリーンショット 2026-10-03 232831.png

ガバメントクラウドなど既存環境に GenU 閉域モードをデプロイする注意点

GenU は CDK で全てのリソースをデプロイできるように作られており、閉域モードでも基本的に単体で動作するのに必要なリソースが作られる構成ですが、既存環境にデプロイする場合、名前解決や API Gateway のリソースポリシーなど逆に競合してうまくいかない部分もあります。

自治体のガバメントクラウド AWS 環境のような構成に後から GenU 閉域モードをデプロイする場合は、リソース作成の全てを GenU の CDK に任せるのではなく、なるべく既存の環境と疎結合になるよう工夫した方が良さそうです。

参考 URL

GenU 閉域モードについては、Kubota さん (@infra365) が深掘りしている記事がとても参考になりました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?