こんにちはますのです。
Amazon Bedrockのインフラ構成に携わる機会が増えてきた今日このごろです。
そのなかで、出来る限りセキュアにAmazon Bedrockで生成AIを利用したいケースもちらほら見かけるようになりました。
今回はサーバレス開発のインフラ構成として、出来る限りプライベート通信となるようにAWS環境で構築をしてみます。
目指す構成
今回は以下のようなPrivateLinkを利用したエンドポイント経由での通信を目指します。
設定の前提
- 各AWSサービスの通信は同一リージョン内(今回は東京)で完結する
- VPCピアリング設定が完了している
- セキュリティグループのインバウンド / アウトバウンドの設計は既に完了している
- Amazon Bedrockの利用するモデルが対象のリージョンで有効化されている
- API Gatewayのエンドポイントは「プライベートDNS:無効」とする
- APIGateway → Lambda間の通信はAWS網内のパブリックアクセスとなる。
- From通信のIP制御が不可のため、Lambdaのセキュリティグループでは「https:0.0.0.0/0」を許容する
- API Gateway →NLB → ECS などの構成にすれば完全プライベート可能な想定だが今回は対象外とする
これによりLambdaから開始される通信 (Lambda→EC2など) が行えます。(Lambdaからのアウトバウンド通信のみがVPCに接続されている状態です)
なので 「API Gateway → Lambda」 の部分については通常のLambdaと変わらない通信になっていると思います。(ここら辺はAWS内部の使用なので公開情報はおそらく無いです)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/foundation-networking.html
VPCエンドポイントの作成
- VPC1
- com.amazonaws.ap-northeast-1.execute-api
- VPC2
- com.amazonaws.ap-northeast-1.s3
- com.amazonaws.ap-northeast-1.dynamodb
- com.amazonaws.ap-northeast-1.bedrock-runtime
各AWSサービスを利用している際の通信影響が懸念される際は検証のうえ実施を推奨します。
VPC1:APIGateway エンドポイント作成
APIGatewayはFromの環境で作成する必要があります。
今回の例ではVPC1のリソースがBedrockへアクセスする構成となるため「VPC1」に作成を行います。
- 名前タグ:vpc1-apigateway-ep
- タイプ:AWSのサービス
- サービス:
- サービス名:com.amazonaws.ap-northeast-1.execute-api
- タイプ:Interface
- ネットワーク設定
- VPC:[VPC1のVPC ID]
- DNS名:無効(チェックを外す)
- DNSレコードのIPタイプ:IPv4
- サブネット:[Instancesが所属しているサブネット]
- セキュリティグループ:(Instancesがインバウンドアクセス可能 / httpsに0.0.0.0/0でアウトバウンドアクセス可能なものを設定)
- ポリシー:フルアクセス
該当のVPCは全てプライベートAPI Gatewayに接続されるようになるため、要件に応じて変更しましょう。
HTTP 403 Forbidden エラーは、VPC に関連付けられている API Gateway のインターフェイス VPC エンドポイントで DNS を有効にした場合に発生します。この場合、VPC から API Gateway API へのすべてのリクエストは、そのインターフェイス VPC エンドポイントで解決されます。ただし、VPC エンドポイントを使用してパブリック API に接続することはできません。
引用:VPC から API ゲートウェイ API への接続時に HTTP 403 Forbidden エラーが発生する理由を知りたいです。
VPC2:S3 VPCエンドポイント作成
続いてVPC2の作業です。
S3宛の通信をInterface型エンドポイントへ切り替えます。
- 名前タグ:vpc2-s3-ep
- タイプ:AWSのサービス
- サービス
- サービス名:com.amazonaws.ap-northeast-1.s3
- タイプ:Interface
- ネットワーク設定
- VPC:[VPC2のVPC ID]
- DNS名:有効
- インバウンドエンドポイントのためにのみプライベート DNS を有効にする:有効
- DNSレコードのIPタイプ:IPv4
- サブネット:[VPC Lambdaが所属しているサブネット]
- セキュリティグループ:(VPC Lambdaがインバウンドアクセス可能なものを設定)
- ポリシー:フルアクセス
VPC2:DynamoDB VPCエンドポイント作成
S3と同じくVPC2の作業です。
DynamoDB宛の通信をInterface型エンドポイントへ切り替えます。
プライベート通信で接続する際は、DynamoDB接続時に明示的にプライベートのエンドポイントURLを指定する必要があります。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/privatelink-interface-endpoints.html
- 名前タグ:vpc2-dynamodb-ep
- タイプ:AWSのサービス
- サービス
- サービス名:com.amazonaws.ap-northeast-1.dynamodb
- タイプ:Interface
- ネットワーク設定
- VPC:[VPC2のVPC ID]
- DNS名:無効(チェックを外す)
- DNSレコードのIPタイプ:IPv4
- サブネット:[VPC Lambdaが所属しているサブネット]
- セキュリティグループ:(VPC Lambdaがインバウンドアクセス可能なものを設定)
- ポリシー:フルアクセス
> VPC エンドポイントの作成中にエラーが発生しました
> Private DNS can’t be enabled because the service com.amazonaws.ap-northeast-1.dynamodb does not provide a private DNS name.

VPC2:Bedrock VPCエンドポイント作成
引き続きVPC2の作業です。
Bedrock宛の通信をInterface型エンドポイントへ切り替えます。
BedrockのVPCエンドポイントは4種類あります。
カテゴリ | エンドポイントプレフィックス | 概要 |
Amazon Bedrock コントロールプレーン API アクション | bedrock | Bedrockの設定に関する操作 |
Amazon Bedrock ランタイム API アクション | bedrock-runtime | Bedrockのモデル利用の操作 |
Amazon Bedrock エージェント Build-time API アクション | bedrock-agent | Bedrockエージェントの設定に関する操作 |
Amazon Bedrock エージェントランタイム API アクション | bedrock-agent-runtime | Bedrockエージェントの呼び出し操作 |
引用:Amazon Bedrock VPC エンドポイントに関する考慮事項
今回はBedrockのモデルをAPI経由で利用したいため「bedrock-runtime」を設定します。
- 名前タグ:vpc2-bedrock-runtime-ep
- タイプ:AWSのサービス
- サービス
- サービス名:com.amazonaws.ap-northeast-1.bedrock-runtime
- タイプ:Interface
- ネットワーク設定
- VPC:[VPC2のVPC ID]
- DNS名:有効
- DNSレコードのIPタイプ:IPv4
- サブネット:[VPC Lambdaが所属しているサブネット]
- セキュリティグループ:(VPC Lambdaがインバウンドアクセス可能なものを設定)
- ポリシー:フルアクセス
以上で各AWSサービスのVPCエンドポイント作成は完了です。
続いて各種AWSサービスを構築していきます。
DynamoDB の作成
Bedrockとの会話履歴を保存するためのDynamoDBを作成します。
作成するリージョンはLambdaと同じとします。今回の例では東京リージョンを利用します。
テーブルの作成
- マネジメントコンソール > DynamoDB > [テーブルの作成] をクリック
- テーブルの詳細
- テーブル名:ChatMessageHistory
- パーティションキー:SessionId (String : 文字列)
- ソートキー:(空欄)
他はデフォルトの設定で [テーブルの作成] をクリックします。
VPC Lambdaの作成
プライベートAPI Gatewayから呼び出されるLambdaを構築します。
今回、Lambda → Bedrock(DynamoDB、S3含む)間の通信をプライベート化するため、VPC Lambdaの構築を行います。
Lambda 関数の作成
- マネジメントコンソール > Lambda > [関数を作成] をクリック
- 下記の設定を行い [関数の作成] をクリック
- 関数の作成:[一から作成]
- 関数名:(任意の名前)
- ランタイム:Python3.13
- アーキテクチャ:x86_64
- 実行ロール:基本的な Lambda アクセス権限で新しいロールを作成
- その他の構成
- VPC を有効化:有効
- VPC:[VPC2を選択]
- サブネット:[プライベートサブネットを選択]
- セキュリティグループ
- インバウンド:[https 0.0.0.0/0]が許可されていること
- アウトバウンド:[Bedrock VPCEとhttps通信可能]であること
コードをデプロイする
今回のコードはLangChainを利用してBedrockとの会話内容をDynamoDBに保存するものです。
「DynamoDB」「Bedrock」へプライベート接続可能かをテストします。
(S3をテストに含んでいないのはご容赦ください)
コードに関しては以下の記事を参考にさせていただきました。
[Amazon Bedrock][LangChain] チャット会話履歴をセッション毎に記憶・保存する方法
コード内の「endpoint_url」にはDynamoDBのプライベートエンドポイントを明示的に指定します。
VPC > エンドポイント > vpc2-dynamodb-ep : DNS名をコピー
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
import json
import uuid
# 使用するLLMモデルを宣言
chat = BedrockChat(
model_id="anthropic.claude-3-haiku-20240307-v1:0",
model_kwargs={
"max_tokens": 1000,
"temperature": 0.5,
},
)
# Lambdaハンドラー
def lambda_handler(event, context):
# for debug
print(f"Received event: {json.dumps(event, ensure_ascii=False)}")
# イベントJSONから入力パラメーターを取得
session_id = event.get("session_id")
input_text = event.get("input_text")
# セッションIDが未設定の場合、新規にセッションIDをセットする (UUIDを使ったランダムな文字列)
if session_id is None:
session_id = str(uuid.uuid4())
# メインの処理を呼び出す
output_text = chat_conversation(session_id, input_text)
# Lambda関数のレスポンスを返す
return {
"session_id": session_id,
"output_text": output_text,
}
# メイン処理
def chat_conversation(session_id: str, input_text: str) -> str:
# VPCエンドポイントのURLを指定
endpoint_url = "https://vpce-012345678-abcde.dynamodb.ap-northeast-1.vpce.amazonaws.com"
# Historyモジュール (Memoryの内容を外部記憶を使って永続化する)
history = DynamoDBChatMessageHistory(
table_name="ChatMessageHistory",
session_id=session_id,
endpoint_url=endpoint_url,
)
# Memoryモジュール (会話履歴を記憶する)
memory = ConversationBufferMemory(
chat_memory=history,
return_messages=True,
)
# Chainモジュール (複数のモジュールを組み合わせて処理を行わせる)
chain = ConversationChain(
llm=chat,
memory=memory,
verbose=True,
)
# LLMに与えるプロンプトを設定
messages = [
HumanMessage(
content=input_text
),
]
# LLMにプロンプトを与えて、応答を含む結果を得る
result = chain.invoke(messages)
# for debug
print(f"Result: {result}")
# 得られた結果から、LLMが返した最新の応答テキストを抽出する
output_text = result.get("response")
return output_text
カスタムレイヤーの作成
LangChainを利用するためにパッケージを用意します。
バージョンを指定しなかった場合はLambda実行時にエラーとなりました。
今回は「Amazon Bedrock 生成AIアプリ開発入門 [AWS深掘りガイド]」からインストールするバージョン等について拝借します。
pip install -t python langchain==0.2.0 langchain-aws==0.1.4 langchain-community==0.2.0 python-dateutil==2.8.2 "pydantic>=1.10.9,<2.0.0"
rm -r python/boto*
zip -r9 langchain-layer.zip python
Lambdaのレイヤー作成はAWS公式手順を参照して作成します。
レイヤー作成後、先程作成したLambda関数画面の下部から「レイヤーの追加」をクリックしてレイヤーを設定します。
関数の設定変更:実行時間、IAMロール権限
続いて作成したLambda関数の設定を変更します。
設定 > 一般設定 > タイムアウト
デフォルトの3秒の場合、Bedrockからの応答時間を待機している間にTimeoutとなってしまいます。
今回は検証をしやすく「15秒」に設定しますが処理によっては、1分以上にするなど各自調整します。
設定 > アクセス権限 > ロール名 > 許可を追加
今回のLambda関数処理ではBedrockとDynamoDBへの権限が必要になります。
Lambda関数に設定されているIAMロールに下記権限を追加します。
- AmazonBedrockFullAccess
- AmazonDynamoDBFullAccess
また、S3など他の処理も入れる場合はあわせて状況に応じて必要な権限を追加しましょう。
Lambda関数のテスト
以下のように「input_text」の情報を与えるとBedrockとDynamoDBへの疎通テストが可能です。
テスト > テストイベント > イベントJSON
{
"input_text": "カレーの作り方を教えて下さい"
}
実行結果
「session_id」と「output_text」へ返答内容が保存されていれば成功です。
ここまでで「Lambda⇔DynamoDB / Bedrock」間のプライベート接続は完了となります。
{
"session_id": "d7c2055e-0e02-436b-b8e7-58205f8a0fb6",
"output_text": "はい、カレーの作り方をお教えします。カレーは日本の代表的な料理の1つで、さまざまな具材を使って煮込むのが特徴です。基本的な作り方は以下のようになります。\n\n材料:\n- 牛肉や豚肉、鶏肉など好きな肉 300g\n- じゃがいも 2~3個\n- たまねぎ 1個\n- にんじん 1本\n- カレールー 1箱\n- 水 500ml\n- 塩・こしょう 適量\n\n作り方:\n1. 肉は一口大に切り、じゃがいもは1cm角に、たまねぎとにんじんはみじん切りにする。\n2. 鍋にオリーブオイルを熱し、肉を炒める。\n3. 肉に火が通ったら、たまねぎ、にんじん、じゃがいもを加えて炒める。\n4. 水を加え、塩こしょうで味付けする。\n5. カレールーを溶かしながら煮込む。20~30分程度煮れば完成です。\n\nこのほかにも、好みで玉子やナンなどを添えたり、香味野菜を加えるのもおいしいですよ。ぜひお試しください。わからないことがあれば遠慮なく質問してくださいね。"
}
プライベートAPI Gatewayの作成
プライベート通信に対応するAPI Gatewayを構築します。
REST API プライベート の作成
- マネジメントコンソール > API Gateway > [APIを作成] をクリック
- [REST API プライベート] > [構築] をクリック
- REST APIを作成:下記設定を行い [APIを作成] をクリック
- API名:(任意の名前)
- APIエンドポイントタイプ:プライベート
- VPCエンドポイントID:[VPC1で作成したVPCE ID]
リソースポリシーの作成
プライベートAPIの場合、リソースポリシーを設定する必要があります。
設定をしていないとデプロイ時に下記のエラーが発生します。
Private REST API doesn't have a resource policy attached to it
詳しい作成方法は「チュートリアル: プライベート REST API を作成する」を参照ください。
今回わたしが設定したサンプルは以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:ap-northeast-1:[AWSアカウントID]:[API Gateway リソースID]/*/*/*",
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-0123456789" //★作成したAPI GatewayのVPCエンドポイントIDを設定
}
}
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:ap-northeast-1:[AWSアカウントID]:[API Gateway リソースID]/*/*/*"
}
]
}
API GatewayのVPCエンドポイントIDからの通信のみ許可としています。
リソース作成 > APIをデプロイ
今回テスト用として、作成したLambdaを紐づけたPOSTメソッドを作成しました。
プライベート API Gatewayの動作テスト
VPC1のプライベートサブネットに、EC2でWindowsServerを構築して検証しました。
- インターネットへのアクセスは不可
- VPCエンドポイントへの接続は可能
- 検証時はcurlコマンドでプライベート DNS名でのみ解決出来ること
今回作成したAPI GatewayはVPCエンドポイントを経由しない限り実行されません。
DynamoDBと同様、プライベートDNS名を指定してアクセスする必要があります。
プライベートDNS名の確認(引用:プライベート API の呼び出し)
https://{restapi-id}.execute-api.{region}.amazonaws.com/{stage}
PowerShellを起動して以下のコマンドを実行します。
curl.exe -X POST https://[リソース ID].execute-api.ap-northeast-1.amazonaws.com/apitest/bedrock-api `
-d '{\"input_text\": \"カレーの作り方を教えて下さい\"}' `
-H "Content-Type: application/json"
上記コマンドでBedrockからの回答が届いたら完了です。
API Gatewayを介してサーバレス構成でプライベート通信を作成しましたが、プライベートDNS名の解決方法や呼び出し方について考慮が必要であることがわかりました。今後サーバレス開発をする際の参考になれば幸いです。
参考サイト
- VPC内プライベートサブネットに配置したLambdaとAPI Gatewayの関係性について
- VPC から API ゲートウェイ API への接続時に HTTP 403 Forbidden エラーが発生する理由を知りたいです。
- Amazon Bedrock VPC エンドポイントに関する考慮事項
- [Amazon Bedrock][LangChain] チャット会話履歴をセッション毎に記憶・保存する方法
- Amazon Bedrock 生成AIアプリ開発入門 [AWS深掘りガイド]
- Lambda でのレイヤーの作成と削除
- チュートリアル: プライベート REST API を作成する
- プライベート API の呼び出し