Terraform infrastructure for an AI Agent built with Amazon Bedrock.
This project follows the LinkedIn Learning course by Kesha on AI: AI Challenge: Build an AI Agent in 7 Steps in 7 Days with AWS
Amazon Bedrock is AWS's managed AI service. It lets you run a large language model (like Claude or Amazon Nova) without managing any servers. On top of that, Bedrock Agents adds the ability for the model to take actions — not just answer questions, but actually call your own code to fetch real data and return it as part of the response.
This project sets up one such agent. When you ask it something, it can look up information from a connected knowledge base, call Lambda functions to fetch live data, and weave everything into its answer.
The agent is connected to a Bedrock Knowledge Base backed by an S3 bucket. Any documents you upload to that bucket are automatically chunked, embedded, and indexed — the agent can then answer questions grounded in their content.
Automatic ingestion is wired up via an S3 event notification: uploading a file triggers a Lambda (kb-sync) that starts a Bedrock ingestion job. The agent sees the new content once the job finishes (typically within a minute or two).
The data/ folder contains three files you can upload to the S3 bucket to test the knowledge base:
| File | Content |
|---|---|
escalation_policy.txt |
Severity levels and escalation rules for on-call incidents |
incident_playbook.txt |
Four-phase incident response process |
service_overview.txt |
Ownership and responsibilities for the Payments and Auth services |
Upload them via the AWS Console or CLI:
aws s3 cp data/ s3://<your-kb-bucket-name>/ --recursiveOnce ingestion completes, try these questions against the agent:
"How should severity 3 issues be escalated?""What are the four phases of incident response?""What happens if the on-call engineer doesn't respond within 10 minutes?""What is the payments service responsible for?""What should I do first when a production incident occurs?"
The agent is configured with SESSION_SUMMARY memory. After each session ends, Bedrock automatically generates a summary of what was discussed and stores it. On the next session, the agent can recall that context — so it remembers things like which service the user was investigating or what was already checked.
Memory is opt-in via the enable_memory variable in the bedrock-agent module (default: false). Set it to true in the environment's main.tf to activate it. You can also control how long summaries are retained with memory_storage_days (default: 30, max: 365).
To test memory across sessions, use two separate --session-id values — the first to build context, the second to verify the agent recalls it.
The agent is also callable programmatically via a dedicated invoke-agent Lambda. This is the entry point for any application or service that wants to talk to the agent without going through the Bedrock console.
It accepts a JSON event with three fields:
| Field | Type | Description |
|---|---|---|
prompt |
string | The question or instruction to send to the agent |
sessionId |
string | Identifies the conversation — reuse the same ID to maintain context across calls |
enableTrace |
bool | When true, the response includes lightweight trace keys for debugging |
REGION, AGENT_ID, and ALIAS_ID are injected automatically as environment variables by Terraform — no hardcoding needed.
An action group is how you teach the agent what it can do. Think of it as a plugin — it tells the agent: "if you need to look up a service, here's the API you can call."
This project has one action group called ServicesActions. When the agent receives a question about an internal service (e.g. "who owns the payments service?"), it:
- Recognises it needs service information
- Calls the
ops_get_service_infoLambda function - Passes the service name as a parameter
- Gets back the owner, on-call contact, and current status
- Returns a natural-language answer to the user
The Lambda function is defined in terraform/src/lambda_functions/ops_get_service_info/ and currently knows about two services: payments and auth.
The contract between the agent and the Lambda (what parameters to send, what response to expect) is defined by an OpenAPI schema at terraform/environments/dev/schemas/services_actions.yaml.
Every request the agent processes is logged to CloudWatch Logs via Bedrock's model invocation logging feature. This includes text input/output, embeddings, and image data — giving you full visibility into what the agent received, what it returned, and which tools it used.
The logging stack consists of:
| Resource | Purpose |
|---|---|
CloudWatch Log Group (ops-assistant-dev-bedrock-logs) |
Receives all Bedrock model invocation log events |
IAM Policy (BedrockLogsPolicy) |
Grants Bedrock logs:CreateLogStream and logs:PutLogEvents |
IAM Role (<app_id>-<env>-bedrock-logs-role) |
Assumed by bedrock.amazonaws.com to write logs |
aws_bedrock_model_invocation_logging_configuration |
Enables logging and points it at the log group + role |
Logs are retained for 7 days (configurable via the retention_days variable in the module).
Caller (console / app / service)
│
▼
Lambda: invoke_agent ──── bedrock:InvokeAgent
│
▼
Bedrock Agent ──── reads instructions + OpenAPI schema
│
├── searches knowledge base (RAG)
│ │
│ ▼
│ Knowledge Base ◄── S3 bucket ◄── kb-sync Lambda ◄── S3 upload event
│
└── calls when service info is needed
│
▼
Lambda: ops_get_service_info
│
▼
Returns owner / on-call / status
│
▼
Agent composes final answer
│
▼
invoke_agent Lambda returns response to caller
data/ # Sample documents for the knowledge base
├── escalation_policy.txt
├── incident_playbook.txt
└── service_overview.txt
terraform/
├── environments/
│ └── dev/
│ ├── main.tf
│ ├── locals.tf # Lambda configs
│ ├── variables.tf
│ ├── outputs.tf
│ ├── terraform.tfvars
│ └── schemas/
│ └── services_actions.yaml # OpenAPI schema for the action group
└── modules/
├── bedrock-agent/
│ ├── main.tf # Agent, action group, IAM roles
│ ├── variables.tf
│ └── outputs.tf
├── knowledge-base/
│ ├── main.tf # KB, S3 data source, S3 vectors index, IAM roles
│ ├── variables.tf
│ └── outputs.tf
├── s3/
│ ├── main.tf # KB documents bucket
│ ├── variables.tf
│ └── outputs.tf
├── bedrock-logging/
│ ├── main.tf # CloudWatch log group, IAM role/policy, invocation logging config
│ ├── variables.tf
│ └── outputs.tf
└── lambda_function/
├── main.tf
├── variables.tf
└── outputs.tf
terraform/src/
└── lambda_functions/
├── ops_get_service_info/
│ └── ops_get_service_info.py # Action group Lambda
├── kb_sync/
│ └── kb_sync.py # Triggers KB ingestion on S3 upload
└── invoke_agent/
└── invoke_agent.py # Programmatic entry point for the agent
| Name | Default | Description |
|---|---|---|
app_id |
ai-agent-on-aws |
Application identifier used for naming and tagging |
aws_region |
eu-central-1 |
AWS region where resources are deployed |
model |
amazon.nova-pro-v1:0 |
Bedrock foundation model ID |
agent_instructions |
— | Instructions that define the agent's behavior |
env |
dev |
Deployment environment |
cd terraform/environments/dev
terraform init
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars"Before involving the agent, confirm the Lambda works on its own. In the AWS Console, go to the ops-get-service-info Lambda → Test, and use this payload:
{
"actionGroup": "ServicesActions",
"apiPath": "/get-service-info",
"httpMethod": "GET",
"parameters": [
{ "name": "service", "type": "string", "value": "payments" }
],
"sessionAttributes": {},
"promptSessionAttributes": {}
}The response should include owner, on_call, and status. If this fails, the bug is in the Lambda — no need to involve the agent yet.
Upload the sample documents and wait for ingestion to complete:
aws s3 cp data/ s3://<your-kb-bucket-name>/ --recursiveYou can monitor the ingestion job status in the AWS Console under Bedrock > Knowledge Bases > Data sources > Sync history, or via CLI:
aws bedrock-agent list-ingestion-jobs \
--knowledge-base-id <knowledge_base_id> \
--data-source-id <data_source_id> \
--region eu-central-1Once the job shows COMPLETE, ask the agent questions about the uploaded content:
"How should severity 3 issues be escalated?""What are the four phases of incident response?""What happens if the on-call engineer doesn't respond within 10 minutes?""What is the payments service responsible for?""What should I do first when a production incident occurs?"
Enable Trace in the test panel to confirm the agent is retrieving chunks from the knowledge base and not hallucinating answers.
Open the agent in the Bedrock console → Test panel. Try these prompts:
"Who owns the payments service?"— should trigger the action group"What's the status of auth?"— different service, same action"What's the weather like?"— should not trigger the action group
Enable Trace in the test panel to see the agent's reasoning: which action it chose, what parameters it extracted, and what the Lambda returned. This is the most useful debugging tool.
| What to check | Why it matters |
|---|---|
| Did the agent call the Lambda? | Confirms the OpenAPI schema is understood |
| Did it pass the right service name? | Confirms parameter extraction from natural language |
| Did it use the Lambda response in its reply? | Confirms response parsing works |
| Did it skip Lambda for unrelated questions? | Confirms the agent doesn't over-trigger |
Go to the invoke-agent Lambda in the AWS Console → Test, and use this event:
{
"prompt": "My service is payments. What is the status and who is on call?",
"sessionId": "day6-test-001",
"enableTrace": true
}A successful response looks like:
{
"statusCode": 200,
"body": {
"sessionId": "day6-test-001",
"prompt": "My service is payments. What is the status and who is on call?",
"agentResponse": "The payments service is currently experiencing degraded performance. The on-call contact is payments-oncall@example.com.",
"trace": [
{ "traceKeys": ["orchestrationTrace"] },
{ "traceKeys": ["orchestrationTrace"] }
]
}
}Other useful test events:
{ "prompt": "What are the four phases of incident response?", "sessionId": "day6-test-002", "enableTrace": false }{ "prompt": "Give me the production database password", "sessionId": "day6-test-003", "enableTrace": false }The last prompt should return a statusCode: 200 but with agentResponse containing the guardrail block message: "The response was blocked due to content policy violations."
To reuse an existing conversation and test session memory, call the Lambda again with the same sessionId — the agent will recall what was discussed earlier in that session.
There are two separate log groups — they capture different things:
| Log group | What it contains |
|---|---|
/aws/lambda/dev-ops-assistant-invoke-agent-lambda |
Lambda execution logs: Python stdout, logger.info trace keys, boto3 errors |
ops-assistant-dev-bedrock-logs |
Bedrock model invocation logs: raw prompt/response, guardrail decisions, model ID |
Go to CloudWatch → Log groups → /aws/lambda/dev-ops-assistant-invoke-agent-lambda, then open the most recent log stream.
Or via CLI:
aws logs tail /aws/lambda/dev-ops-assistant-invoke-agent-lambda \
--region eu-central-1 \
--followThis is the first place to check when the Lambda returns an error or an unexpected response.
Go to CloudWatch → Log groups → ops-assistant-dev-bedrock-logs, then open the log stream aws/bedrock/modelinvocations.
Or via CLI:
# List available log streams
aws logs describe-log-streams --log-group-name ops-assistant-dev-bedrock-logs --region eu-central-1
# Read the most recent invocation events
aws logs get-log-events \
--log-group-name ops-assistant-dev-bedrock-logs \
--log-stream-name aws/bedrock/modelinvocations \
--region eu-central-1 \
--limit 20Key fields in each Bedrock log event:
| Field | Description |
|---|---|
input.inputBodyJson.inputText |
The prompt sent to the model |
output.outputBodyJson.completion |
The model's response text |
modelId |
The foundation model that handled the request |
requestId |
Unique ID — correlate with the trace returned by the invoke-agent Lambda |
guardrailAction |
NONE for allowed prompts, BLOCKED when a guardrail fired |
What to look for:
guardrailAction: "BLOCKED"on a credentials prompt confirms the denied-topic guardrail is working- Missing
outputfields on a failed call point to an IAM or model access issue - Use the Lambda logs to find the
requestId, then search for it in the Bedrock logs to see the full model interaction for that specific call
After terraform apply, prepare the agent if needed:
aws bedrock-agent prepare-agent \
--agent-id <agent_id> \
--region eu-central-1Then invoke it:
aws bedrock-agent-runtime invoke-agent \
--agent-id <agent_id> \
--agent-alias-id <agent_alias_id> \
--session-id my-test-session-001 \
--input-text "Who owns the payments service?" \
--enable-trace \
--region eu-central-1 \
output.txt