Last reviewed: May 2026
Build the AWS services on the AIP-C01 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, a production-shaped Bedrock Agent — an S3 bucket for any source data, a least-privilege IAM role the agent assumes, a production-strength Bedrock Guardrail (content + PII + denied-topics filters), the Bedrock Agent itself, and an Action Group that lets the agent invoke a Lambda function as a tool. This is the AIP-C01 reference architecture for tool-calling generative AI: a model with safety rails, scoped permissions, and a callable extension surface.
Every resource is plain Terraform. Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.us-east-1 (Bedrock Agents support is broadest there).anthropic.claude-3-haiku-20240307-v1:0 at minimum (the agent in Step 4 references it). Model access is an account-level toggle outside infrastructure-as-code.aws_bedrockagent_* resources require AWS provider ~> 5.60 and the Bedrock Agent service must be GA in your region. Check the AWS console if aws bedrock-agent list-agents works.Idle billing is minimal; the bill scales with usage:
The cost trap AIP-C01 specifically tests: Bedrock Knowledge Bases require an OpenSearch Serverless collection that bills ~$350/month idle minimum. The lab deliberately does NOT provision a Knowledge Base — the Action Group pattern in Step 5 demonstrates the more common tool-calling agent shape without the OpenSearch trap. If you want to study Knowledge Bases, do so conceptually first; provision the OSS collection only when you're ready to actually use it.
Standard opener. us-east-1 for broadest Bedrock + Bedrock Agent feature availability. The provider version pin is non-negotiable: aws_bedrockagent_* resources require ~> 5.60 or later.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
archive = {
source = "hashicorp/archive"
version = "~> 2.4"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "certlabpro-aip-c01"
ManagedBy = "terraform"
}
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}Most production Bedrock Agents end up reading something from S3 — knowledge-base source documents, JSON tool definitions, response templates. We provision a bucket up front so we don't have to plumb permissions in later. Encryption + public-access-block defaults are the AIP-C01 Foundation Model Integration, Data Management, and Compliance domain at work.
The AIP-C01 exam tests one specific compliance pattern around this bucket: client-side encryption with a key the application controls is the right answer when the regulation says "AWS must never have the unencrypted data". For everything else, SSE-S3 (what we do here) is sufficient.
resource "aws_s3_bucket" "agent_data" {
bucket_prefix = "certlabpro-aip-c01-"
}
resource "aws_s3_bucket_public_access_block" "agent_data" {
bucket = aws_s3_bucket.agent_data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "agent_data" {
bucket = aws_s3_bucket.agent_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}AIP-C01's AI Safety, Security, and Governance domain (20% of the exam) goes much deeper than the AIF-C01 version. The exam tests the multi-layered guardrail composition: content filters for harmful categories at high strength, sensitive-information policies for PII (with BLOCK rather than the AIF-style ANONYMIZE — production agents usually need hard blocks), and topic policies for application-specific forbidden topics.
We configure all three. The denied topic "FinancialAdvice" in the policy is a common production guardrail for non-financial chatbots — even Claude 3 will sometimes drift into giving stock tips unless you tell the guardrail to block topical drift. AIP-C01 tests this exact composition pattern in scenario questions where the prompt is benign but the response could go off-policy.
resource "aws_bedrock_guardrail" "production" {
name = "certlabpro-aip-c01-production"
description = "Production-grade safety rail for the AIP-C01 lab agent."
blocked_input_messaging = "I can't help with that request."
blocked_outputs_messaging = "I can't share that response."
content_policy_config {
filters_config {
input_strength = "HIGH"
output_strength = "HIGH"
type = "HATE"
}
filters_config {
input_strength = "HIGH"
output_strength = "HIGH"
type = "VIOLENCE"
}
filters_config {
input_strength = "HIGH"
output_strength = "HIGH"
type = "SEXUAL"
}
filters_config {
input_strength = "HIGH"
output_strength = "HIGH"
type = "INSULTS"
}
filters_config {
input_strength = "HIGH"
output_strength = "HIGH"
type = "MISCONDUCT"
}
}
sensitive_information_policy_config {
pii_entities_config {
action = "BLOCK"
type = "EMAIL"
}
pii_entities_config {
action = "BLOCK"
type = "PHONE"
}
pii_entities_config {
action = "BLOCK"
type = "US_SOCIAL_SECURITY_NUMBER"
}
pii_entities_config {
action = "BLOCK"
type = "CREDIT_DEBIT_CARD_NUMBER"
}
}
topic_policy_config {
topics_config {
name = "FinancialAdvice"
definition = "Specific recommendations to buy, sell, or hold financial instruments."
examples = ["Should I buy NVDA?", "Is now a good time to sell my Bitcoin?"]
type = "DENY"
}
}
}The agent ties together a foundation model, a system prompt, and (in Step 5) one or more action groups that extend its capabilities. The IAM role we attach is the agent's execution role — it's what the agent uses to invoke models, retrieve from knowledge bases, and call action-group Lambdas. AIP-C01's Implementation and Integration domain tests this exact role/trust shape: bedrock.amazonaws.com as the principal, scoped permissions to bedrock:InvokeModel on specific model ARNs, and lambda:InvokeFunction on specific function ARNs.
The agent's instruction field is the system prompt — short, action-oriented, naming the agent's purpose. AIP-C01 questions about prompt engineering at agent level test exactly this: the instruction is the persistent personality; the user message changes per turn.
resource "aws_iam_role" "agent" {
name = "certlabpro-aip-c01-agent"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "bedrock.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}]
})
}
resource "aws_iam_role_policy" "agent_invoke_model" {
name = "invoke-foundation-model"
role = aws_iam_role.agent.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
Resource = "arn:aws:bedrock:${data.aws_region.current.name}::foundation-model/anthropic.claude-3-haiku-20240307-v1:0"
}]
})
}
resource "aws_bedrockagent_agent" "main" {
agent_name = "certlabpro-aip-c01-agent"
agent_resource_role_arn = aws_iam_role.agent.arn
foundation_model = "anthropic.claude-3-haiku-20240307-v1:0"
instruction = "You are a helpful assistant for the certlabpro AIP-C01 lab. Answer concisely. When the user asks about the current time, use the get_current_time tool."
idle_session_ttl_in_seconds = 600
# Attach the guardrail from Step 3. Bedrock evaluates inputs and outputs
# against the guardrail policy on every model call.
guardrail_configuration {
guardrail_identifier = aws_bedrock_guardrail.production.guardrail_id
guardrail_version = aws_bedrock_guardrail.production.version
}
}Action Groups are how Bedrock Agents call out to your code. The pattern: define the API schema (what the tool accepts and returns), point at a Lambda that implements the actions, and Bedrock handles the rest — when the model decides it needs the tool, it calls your Lambda with structured arguments and feeds the response back into the conversation.
We define a minimal get_current_time action with the OpenAPI 3.0 schema Bedrock expects. The Lambda implementation is intentionally tiny — what the exam tests is the shape of the integration (function-calling Lambda + OpenAPI schema + Bedrock invocation permission), not the function's internal logic.
The aws_lambda_permission resource is the canonical AIP-C01 gotcha: without it, the agent's invocation of the Lambda gets AccessDenied even though every other piece is wired correctly. Bedrock Agent → Lambda is an external-service invocation pattern; the function's resource policy needs to allow bedrock.amazonaws.com explicitly.
With Action Groups in place, the full agent shape is complete: model + system prompt + safety guardrail + tool-calling extensibility, all under one scoped IAM identity. Every additional AIP-C01 pattern (Knowledge Base retrieval, multi-action agents, agent-collaboration via supervisor agents) extends this base.
# ── Lambda tool ───────────────────────────────────────────────
data "archive_file" "tool" {
type = "zip"
output_path = "${path.module}/build/tool.zip"
source {
filename = "index.py"
content = <<-EOT
import json
from datetime import datetime, timezone
def handler(event, context):
# Bedrock Agent passes the parsed args under `requestBody` / `parameters`.
# For a simple no-arg tool, just return the current time.
now_iso = datetime.now(timezone.utc).isoformat()
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event.get("actionGroup", ""),
"apiPath": event.get("apiPath", "/"),
"httpMethod": event.get("httpMethod", "GET"),
"httpStatusCode": 200,
"responseBody": {"application/json": {"body": json.dumps({"current_time_utc": now_iso})}},
},
}
EOT
}
}
resource "aws_iam_role" "tool_lambda" {
name = "certlabpro-aip-c01-tool"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "tool_logs" {
role = aws_iam_role.tool_lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "tool" {
function_name = "certlabpro-aip-c01-tool"
role = aws_iam_role.tool_lambda.arn
runtime = "python3.12"
handler = "index.handler"
filename = data.archive_file.tool.output_path
source_code_hash = data.archive_file.tool.output_base64sha256
timeout = 10
}
resource "aws_lambda_permission" "agent_invoke_tool" {
statement_id = "AllowBedrockAgentInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.tool.function_name
principal = "bedrock.amazonaws.com"
source_arn = aws_bedrockagent_agent.main.agent_arn
}
# Also let the agent invoke the lambda from its execution role.
resource "aws_iam_role_policy" "agent_invoke_lambda" {
name = "invoke-tool"
role = aws_iam_role.agent.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = aws_lambda_function.tool.arn
}]
})
}
# ── Action Group ──────────────────────────────────────────────
resource "aws_bedrockagent_agent_action_group" "time" {
action_group_name = "time-tools"
agent_id = aws_bedrockagent_agent.main.agent_id
agent_version = "DRAFT"
description = "Time-related tool calls for the agent."
skip_resource_in_use_check = false
action_group_executor {
lambda = aws_lambda_function.tool.arn
}
api_schema {
payload = jsonencode({
openapi = "3.0.0"
info = { title = "Time tools", version = "1.0.0" }
paths = {
"/get_current_time" = {
get = {
operationId = "get_current_time"
description = "Returns the current UTC time as an ISO-8601 string."
responses = {
"200" = {
description = "Current UTC time"
content = {
"application/json" = {
schema = {
type = "object"
properties = {
current_time_utc = { type = "string", format = "date-time" }
}
}
}
}
}
}
}
}
}
})
}
}terraform destroy tears down everything in this lab, with care for ordering:
force_destroy = false. If you uploaded any reference data, empty it (aws s3 rm s3://<bucket> --recursive) before destroying.AIP-C01 covers production GenAI surfaces this lab can't fit — Bedrock Knowledge Bases (require OpenSearch Serverless = $350+/month idle, deliberately skipped per the cost-note), Bedrock Agent Aliases for traffic shifting and version management, multi-agent collaboration (supervisor agents that delegate to specialist sub-agents), agent memory for long-running conversational state, Bedrock Custom Models (fine-tuning), Bedrock Provisioned Throughput for high-volume production deployments, Amazon Q for Business (no Terraform resources), SageMaker JumpStart for off-platform foundation model hosting, and the Bedrock observability stack (model invocation logging deeper than AIF-C01 covered).
We stick to the Agent + Action Group + Production Guardrail shape because it's the most-tested production-GenAI architecture on the exam and the substrate every more-advanced pattern attaches to. Knowledge Bases attach to this agent. Aliases version this agent. Multi-agent collaboration composes agents like this one.
For coverage of the surfaces above, the Browse, Playbook, and Editorial sections of this cert page have the conceptual material. A natural follow-up lab would add a Knowledge Base on top of this base (the OpenSearch cost is the only reason we don't include it here).