はじめに
Amazon Bedrockを使って、S3のドキュメントに基づくAIの応答システムを構築してみました。本記事では、以下の構成でWeb UIから質問を送信し、Claude Instantに回答させる仕組みを紹介します。
システム構成
[ User (ウェブブラウザ) ]
│ ▲
▼ │
[ CloudFront (CDN: index.html, JS) ]
│ ▲
▼ │
[ S3 (静的ホスティング + インプットファイル格納) ]
│ ▲
▼ │
[ API Gateway (POSTリクエスト受信) ]
│ ▲
▼ │
[ Lambda Function ]
├─ S3からインプットファイルを取得
├─ 内容をBedrockのモデルに渡す
└─ Bedrockの応答を受け取り、ユーザーに返す
│ ▲
▼ │
[ Amazon Bedrock (Claude Instant) ]
構築順序
1. API Gateway と Lambda の作成
ユーザーからのリクエストを処理するために、Lambda 関数を作成し、API Gateway(HTTP API)と統合します。
Lambda では、S3 からファイルを取得し、Bedrock に問い合わせて結果を返す処理を実装します。
2. CloudFront と S3 の構築
フロントエンドを配信するために、S3 に静的ホスティング用バケットを作成し、CloudFront を通じて公開します。
オリジンとして S3 を指定し、キャッシュ設定やオブジェクトへのアクセス権限も調整します。
3. S3 に index.html をアップロード
作成したフロントエンドファイル(例:index.html)を S3 にアップロードします。
正しい Content-Type を指定し、CloudFront のキャッシュが有効な場合は、必要に応じて Invalidation を行います。
4. S3 に import.txt をアップロード
ユーザーからの入力として処理する import.txt ファイルを S3 にアップロードします。
Lambda 関数はこのファイルを読み取り、Bedrock モデルに渡すことで応答を生成します。
5. Amazon Bedrock モデルの有効化
使用する Bedrock モデル(例:Claude Instant)をコンソールから有効化します。
併せて、Lambda から Bedrock にアクセスするための適切な IAM 権限を設定します。
1. API Gateway と Lambda の作成
以下テンプレートを元にCloudFormationのスタックを作成していきます。
CloudFormationで新規スタックを作成する方法の詳細は次の記事をご参照下さい。
CloudFormationで新規スタックを作成する方法
今回はYAMLで作成しておりますので、JSONに変換したい方は次の記事をご参照下さい。
YAMLからJSONに変換する方法
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda + API Gateway with CORS for Bedrock Q&A
Parameters:
S3BucketName:
Type: String
Description: Lambdaが読み込むS3バケット名
Default: "auto-webui-web-bucket"
S3ObjectKey:
Type: String
Description: Lambdaが読み込むS3のオブジェクトキー
Default: "documents/import.txt"
Resources:
QALambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: QALambdaFunction
Runtime: python3.12
Handler: index.lambda_handler
Timeout: 300
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
BUCKET_NAME: !Ref S3BucketName
OBJECT_KEY: !Ref S3ObjectKey
Code:
ZipFile: |
import json
import boto3
import os
s3 = boto3.client('s3')
bedrock = boto3.client('bedrock-runtime')
def lambda_handler(event, context):
# CORS対応
headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST"
}
if event.get("httpMethod") == "OPTIONS":
return {
"statusCode": 200,
"headers": headers,
"body": json.dumps("CORS preflight OK")
}
try:
# クライアント(Web UI)からの質問を取得
body = json.loads(event.get("body", "{}"))
question = body.get("question", "")
# 環境変数で設定したバケットとキーを取得
bucket = os.environ["BUCKET_NAME"]
key = os.environ["OBJECT_KEY"]
# S3から文書を読み込む
s3_obj = s3.get_object(Bucket=bucket, Key=key)
s3_text = s3_obj['Body'].read().decode('utf-8')
# Claude用のプロンプトフォーマット
prompt = f"""\n\nHuman:
以下のドキュメントを参考に、ユーザーの質問に日本語で答えてください。
--- ドキュメント ---
{s3_text}
--- 質問 ---
{question}
\n\nAssistant:"""
# Bedrock に送信(Claude Instant を想定)
response = bedrock.invoke_model(
modelId="anthropic.claude-instant-v1",
body=json.dumps({
"prompt": prompt,
"max_tokens_to_sample": 500,
"temperature": 0.7
}),
accept="application/json",
contentType="application/json"
)
result = json.loads(response['body'].read())
return {
"statusCode": 200,
"headers": headers,
"body": json.dumps({
"response": result.get("completion", "回答が得られませんでした")
})
}
except Exception as e:
return {
"statusCode": 500,
"headers": headers,
"body": json.dumps({
"error": str(e)
})
}
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: QALambdaExecutionRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: BedrockS3Policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- Effect: Allow
Action: s3:GetObject
Resource: !Sub arn:aws:s3:::${S3BucketName}/${S3ObjectKey}
- Effect: Allow
Action: bedrock:InvokeModel
Resource: "*"
APIGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: QAApi
APIResourceAsk:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref APIGateway
ParentId: !GetAtt APIGateway.RootResourceId
PathPart: ask
APIAskPostMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref APIGateway
ResourceId: !Ref APIResourceAsk
HttpMethod: POST
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Sub
- arn:aws:apigateway:${Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
- Region: !Ref "AWS::Region"
LambdaArn: !GetAtt QALambdaFunction.Arn
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
APIAskOptionsMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref APIGateway
ResourceId: !Ref APIResourceAsk
HttpMethod: OPTIONS
AuthorizationType: NONE
Integration:
Type: MOCK
RequestTemplates:
application/json: '{"statusCode": 200}'
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Origin: "'*'"
method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Methods: true
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref QALambdaFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${APIGateway}/*/*/ask
Deployment:
Type: AWS::ApiGateway::Deployment
DependsOn:
- APIAskPostMethod
- APIAskOptionsMethod
Properties:
RestApiId: !Ref APIGateway
StageName: prod
Outputs:
APIEndpoint:
Description: "API Gateway Endpoint"
Value: !Sub "https://${APIGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/ask"
パラメータの詳細
パラメータ名 | 用途 | 備考 |
---|---|---|
S3BucketName | Bedrockに受け渡すファイルが格納されるS3バケット名 | 2.で作成するスタック名+-web-bucketがS3バケット名となります。 |
S3ObjectKey | Bedrockに受け渡すファイルが格納されるオブジェクトパス | 例:documents/import.txt |
2. CloudFront と S3 の構築
以下テンプレートを元にCloudFormationのスタックを作成していきます。
AWSTemplateFormatVersion: '2010-09-09'
Description: S3 + CloudFront
Parameters:
ApiGatewayDomain:
Type: String
Description: APIGateway
Resources:
WebSiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${AWS::StackName}-web-bucket'
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebSiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebSiteBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontAccess
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: 's3:GetObject'
Resource: !Sub '${WebSiteBucket.Arn}/*'
Condition:
StringEquals:
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'
MyCustomOriginRequestPolicy:
Type: AWS::CloudFront::OriginRequestPolicy
Properties:
OriginRequestPolicyConfig:
Name: !Sub "${AWS::StackName}-CORSRequestPolicy"
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
- Content-Type
QueryStringsConfig:
QueryStringBehavior: all
## 2. CloudFront用 OAC(Origin Access Control)
CloudFrontOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub '${AWS::StackName}-OAC'
Description: OAC for S3 access
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
## 3. CloudFront ディストリビューション
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
DefaultRootObject: index.html
Origins:
- Id: S3Origin
DomainName: !GetAtt WebSiteBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: ''
OriginAccessControlId: !Ref CloudFrontOAC
- Id: APIGatewayOrigin
DomainName: !Ref ApiGatewayDomain
OriginPath: "/prod"
CustomOriginConfig:
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # AWS-Managed CachingOptimized
CacheBehaviors:
- PathPattern: "ask"
TargetOriginId: APIGatewayOrigin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
- OPTIONS
- PUT
- POST
- PATCH
- DELETE
CachedMethods:
- GET
- HEAD
- OPTIONS
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # AWS-Managed CachingDisabled
OriginRequestPolicyId: !Ref MyCustomOriginRequestPolicy # AWS-Managed CORS-With-Preflight
Compress: true
ViewerCertificate:
CloudFrontDefaultCertificate: true
Outputs:
WebUIURL:
Description: Web UI URL (CloudFront)
Value: !Sub 'https://${CloudFrontDistribution.DomainName}'
パラメータの詳細
パラメータ名 | 用途 | 備考 |
---|---|---|
ApiGatewayDomain | Postリクエストを受信するAPI Gatewayのドメイン | 1.で作成したスタックの出力の値を記載する。 |
3. S3 に index.html をアップロード
作成したS3に以下、index.htmlをアップロードする。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>質問フォーム</title>
<style>
body {
font-family: sans-serif;
padding: 2em;
background-color: #f4f4f4;
}
input, button {
padding: 0.5em;
font-size: 1em;
margin: 0.5em 0;
}
#responseArea {
margin-top: 1em;
padding: 1em;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>質問を入力してください</h1>
<input type="text" id="questionInput" placeholder="質問を入力" size="50" />
<br />
<button onclick="submitQuestion()">送信</button>
<div id="responseArea">AIの回答がここに表示されます</div>
<script>
async function submitQuestion() {
const question = document.getElementById("questionInput").value;
const responseArea = document.getElementById("responseArea");
if (!question.trim()) {
responseArea.innerText = "質問を入力してください。";
return;
}
responseArea.innerText = "送信中...";
try {
const response = await fetch("https://<API Gatewayのエンドポイント記載する>.execute-api.ap-northeast-1.amazonaws.com/prod/ask", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ question: question })
});
if (!response.ok) {
throw new Error("APIからの応答が不正です");
}
const result = await response.json();
responseArea.innerText = result.response || "返答がありませんでした。";
} catch (err) {
responseArea.innerText = "エラーが発生しました: " + err.message;
}
}
</script>
</body>
</html>
4. S3 に import.txt をアップロード
index.htmlを配置したS3にフォルダ「document」を作成して、import.txtをアップロードします。
ここはAIが回答する際に考慮させたいテキストファイルをアップロードします。
記載例
CloudFormation記載ルール(cf_rules.txt)
1. テンプレート構成
----------------------
- AWSTemplateFormatVersion は常に先頭に記載
- Description を明確に記載(プロジェクト名、目的、作成者など)
2. Resourcesセクション
----------------------
- リソース名は 頭文字に z99project を付けて
- 複数の同種リソースがある場合は番号付きで区別(例: AppServer01, AppServer02)
- Type と Properties のインデントはスペース2つ
3. パラメータとマッピング
----------------------
- Parameters は Usage に応じて明確な説明を付与(Description 必須)
- Mappings は必要最低限にとどめ、コメントで用途を記載
4. Outputs
----------------------
- 他スタックで参照する値は Outputs に記載し Export 名も明示
- Description を必ず付与
5. 条件・依存関係
----------------------
- DependsOn を使用する場合は理由をコメントで記載
- Conditions は true/false のロジックが直感的にわかる命名を使用(例: IsProduction)
6. コメントの書き方
----------------------
- `#` を使用し、処理目的や背景を記載する
7. その他
----------------------
- YAML形式を推奨(可読性重視)
- セキュリティ関連リソース(IAM, KMS, SecurityGroup等)は別ファイルに分離可能
- スタック名、リソース名に環境名(dev, stg, prodなど)を含める
動作確認
CloudFrontのドメインにアクセスします。
入力フォームから好きな質問をしてみてください。
まとめ
今回は、簡単なAI応答システムを作ってみました。
社内ルールをあらかじめまとめておけば、必要な情報をすぐに検索できるので、業務の効率アップに役立ちそうです。
また、社内メンバーの情報も入れておけば、AIを使った人事異動のサポートなんかもできるかもしれません。
気になるのはセキュリティ面ですが、対策としてはIAM認証やCognito、WAFなどでAPIへのアクセスを制限したり、必要に応じてIP制限やVPC連携を検討するのが良さそうです。