Last reviewed: May 2026
Build the AWS services on the AIF-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 working Amazon Bedrock setup that demonstrates four of the five AIF-C01 exam domains: an S3 store for AI inputs and outputs, a least-privilege IAM role that Bedrock can assume to invoke foundation models, a guardrail that filters prompts and responses for safety and PII, and account-and-region-level invocation logging.
Every resource is plain Terraform — the same code works without modification on OpenTofu. There are no variables, no modules, no remote state. Drop the snippets that follow into a single main.tf, run terraform init once, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.us-east-1 because Bedrock and its newest foundation models roll out there first.Most resources in this lab cost nothing while idle: IAM roles, the guardrail configuration, the logging configuration, an empty S3 bucket, and a CloudWatch log group with no events are all free.
Line items you might see on a bill if you actually use the lab:
What we intentionally skipped: the lab does not create a Bedrock Knowledge Base. Knowledge Bases require an OpenSearch Serverless collection that bills a minimum ~$350/month while idle. That's deliberately out of scope here — see the AIF-C01 cert page if you want to study Knowledge Bases conceptually without provisioning one.
Before we create any resources we need to tell Terraform which version of itself we expect and which AWS provider we'll be using. We pin the AWS provider to ~> 5.60 because every Bedrock-related resource we touch in this lab landed in that line, and we deploy everything to us-east-1 — AWS rolls foundation models out there first, and most AIF-C01 questions assume us-east-1 availability when they reference a "newest" service.
Drop this into a fresh main.tf to start. Everything that follows in the lab sits in the same file.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
provider "aws" {
region = "us-east-1"
}Every AWS AI service we'll use in this lab — Bedrock today, and the audit logging we'll wire up in Step 5 — reads inputs and writes outputs through Amazon S3, so the first piece of real infrastructure is a bucket. We'll come back to it in almost every later step.
We turn on versioning so a corrupt upload can be rolled back, lock down public access (these are training inputs and model invocation logs, not website assets), and enable AES256 encryption at rest. That last point matters for AIF-C01 — the Security, Compliance, and Governance for AI Solutions domain explicitly tests that you reach for encryption-at-rest by default on any AI data store.
resource "aws_s3_bucket" "ai_data" {
bucket_prefix = "certlabpro-aif-c01-"
}
resource "aws_s3_bucket_public_access_block" "ai_data" {
bucket = aws_s3_bucket.ai_data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "ai_data" {
bucket = aws_s3_bucket.ai_data.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "ai_data" {
bucket = aws_s3_bucket.ai_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}Bedrock can't invoke a model on its own — it needs an IAM role it can assume. We create one with a trust policy that names bedrock.amazonaws.com as the only allowed principal, then attach a minimal permission policy: invoke any foundation model, and read/write only the bucket we created in Step 2.
Least-privilege patterns like this one are a recurring AIF-C01 theme. Notice we don't grant s3:* — only the two object actions we actually need, scoped to objects inside our bucket. The exam rewards spotting that same shape in multiple-choice scenarios: when two answer choices both "work," the narrower one is almost always correct.
resource "aws_iam_role" "bedrock_caller" {
name = "certlabpro-aif-c01-bedrock-caller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "bedrock.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "bedrock_caller" {
name = "bedrock-invoke"
role = aws_iam_role.bedrock_caller.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
]
Resource = "arn:aws:bedrock:*::foundation-model/*"
},
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
]
Resource = "${aws_s3_bucket.ai_data.arn}/*"
},
]
})
}A Bedrock model on its own will happily answer anything its training data lets it answer. Guardrails sit in front of every model call and apply content + PII policies independent of which foundation model you use — meaning a single guardrail covers Claude, Llama, Titan, and any future model.
Here we configure two layers. A content filter blocks high-severity hate / violence / sexual content in both prompts and responses, and a sensitive-information policy blocks emails, phone numbers, and US Social Security numbers from leaking through. These map directly to AIF-C01's Guidelines for Responsible AI domain — content filtering and PII handling are the two named pillars there.
With the guardrail in place, the role we built in Step 3 can invoke a model and the result is filtered before it reaches the caller.
resource "aws_bedrock_guardrail" "safety" {
name = "certlabpro-aif-c01-safety"
description = "Default safety rails for the lab."
blocked_input_messaging = "I can't help with that."
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"
}
}
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"
}
}
}Bedrock invocation logging captures the full prompt, response, and embedding payload of every model call in the account and region. It writes to CloudWatch Logs (good for ad-hoc grep) and to S3 (good for long-term retention) — we wire both. The role we attach grants exactly the actions the Bedrock service needs and nothing more.
One AIF-C01-relevant catch worth knowing: invocation logging is a singleton — one configuration per account per region. If you run this lab a second time in us-east-1 Terraform will report no change but the existing config gets re-bound. Destroy before re-running if you ever change the log destinations.
With logging in place, every model call we make against the role from Step 3, through the guardrail from Step 4, ends up archived in the bucket from Step 2. That's the full audit chain — exactly the kind of "show me the evidence" story the exam expects you to be able to describe.
resource "aws_iam_role" "bedrock_logging" {
name = "certlabpro-aif-c01-bedrock-logging"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "bedrock.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_cloudwatch_log_group" "bedrock_invocations" {
name = "/aws/bedrock/invocations"
retention_in_days = 30
}
resource "aws_iam_role_policy" "bedrock_logging" {
name = "write-logs-and-s3"
role = aws_iam_role.bedrock_logging.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
]
Resource = aws_cloudwatch_log_group.bedrock_invocations.arn
},
{
Effect = "Allow"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.ai_data.arn}/bedrock-invocations/*"
},
]
})
}
resource "aws_bedrock_model_invocation_logging_configuration" "this" {
logging_config {
embedding_data_delivery_enabled = true
image_data_delivery_enabled = true
text_data_delivery_enabled = true
cloudwatch_config {
log_group_name = aws_cloudwatch_log_group.bedrock_invocations.name
role_arn = aws_iam_role.bedrock_logging.arn
}
s3_config {
bucket_name = aws_s3_bucket.ai_data.bucket
key_prefix = "bedrock-invocations/"
}
}
}A standard terraform destroy tears down everything in this lab. Two notes:
aws_bedrock_model_invocation_logging_configuration is account-and-region scoped — destroying it disables logging globally in us-east-1. If another workload also relies on that configuration, you'll want to skip this step or coordinate.force_destroy = false means destroy will fail if any objects (including delete markers) remain. Either empty the bucket via the console before destroying, or set force_destroy = true on the bucket resource and re-apply before tearing down.The AIF-C01 exam covers a wide AWS AI portfolio — Rekognition, Comprehend, Textract, Polly, Translate, Transcribe, Lex, SageMaker, Kendra, Personalize, and Q. We deliberately don't provision them in this lab.
The reason isn't that they're unimportant — it's that meaningful hands-on practice for those services either needs no infrastructure at all (Textract, Comprehend, Rekognition are pure API calls against existing S3 data), runs into resources that don't exist in the Terraform AWS provider (Polly lexicons, Translate terminologies, Amazon Q), or burns money while idle (Bedrock Knowledge Bases, Kendra, SageMaker domains).
For those services, the Browse, Playbook, and Editorial sections of this cert page have the conceptual coverage you'll need for the exam. The hands-on value lives where the exam itself is hardest to internalize without doing: the Bedrock IAM + guardrail + logging control plane.