Table of Contents
Open Table of Contents
- Why Bedrock and Why Now
- Level 0: Your First Model Call
- Level 1: Terraform Everything
- Level 2: IAM Policies — Least Privilege or Nothing
- Level 3: Governance — Guardrails, Logging, Audit
- Level 4: FinOps — Know Your Bill Before Finance Does
- Level 5: Reference GitHub Repos Worth Studying
- Level 6: Complete Production Infrastructure Example
- Best Practices Recap — The Ninja Rules
- Further Reading
Why Bedrock and Why Now
Amazon Bedrock is the managed path to foundation models on AWS: Anthropic Claude, Meta Llama, Mistral, Cohere, Amazon Nova and Titan, Stability — all behind one API, one IAM model, one billing surface, and one CloudTrail audit trail. No GPU provisioning, no model hosting, no patching inference servers. You call an endpoint, you pay per token.
That simplicity is also the trap. Teams enable a model in the console, paste an access key into a notebook, and three months later security asks who invoked what, finance asks why the bill tripled, and platform engineering inherits an unmappable mess. This guide walks the full path — zero to ninja — in the order that avoids that outcome:
- Call your first model correctly.
- Provision everything with Terraform.
- Lock down IAM with least-privilege policies.
- Add governance with Guardrails, logging, and CloudTrail.
- Wire in FinOps before costs scale.
- Study the reference repos that encode real-world lessons.
- Assemble a complete production infrastructure example.
Level 0: Your First Model Call
Enable model access
Bedrock requires explicit per-model opt-in per region. In the console: Bedrock → Model access → Modify and request the models you need. Access to Anthropic models typically requires a short use-case form; Amazon models are instant. In production you will not click the console — Terraform handles this, shown in the next section.
The InvokeModel call
The Bedrock Runtime API has two core operations: InvokeModel (single response) and InvokeModelWithResponseStream (streaming). Newer code should prefer the unified Converse API, which normalizes the request shape across model providers.
Python with boto3:
import boto3
import json
client = boto3.client("bedrock-runtime", region_name="us-east-1")
# Converse API — provider-agnostic
response = client.converse(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[
{"role": "user", "content": [{"text": "Explain idempotency in one sentence."}]}
],
inferenceConfig={"maxTokens": 256, "temperature": 0.2},
)
print(response["output"]["message"]["content"][0]["text"])
print(response["usage"]) # inputTokens / outputTokens — your FinOps primitive
Raw InvokeModel when you need provider-specific features:
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello, Bedrock."}],
}
resp = client.invoke_model(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
body=json.dumps(body),
contentType="application/json",
accept="application/json",
)
print(json.loads(resp["body"].read())["content"][0]["text"])
Use IAM credentials, never keys
Locally, use aws sso login or an assumed role. In deployed workloads, use the execution role of the Lambda/ECS task/EKS pod (IRSA). Static AWS_ACCESS_KEY_ID secrets in env vars are the number-one finding in Bedrock audits. Every call in this guide assumes role-based credentials.
Level 1: Terraform Everything
Console clicks do not scale and cannot be reviewed. The AWS provider supports Bedrock resources natively; model access itself is requested through the aws_bedrock_model_invocation_logging_configuration and related resources, while marketplace model subscriptions use the aws_bedrock_foundation_model data source plus an enablement flow. Core building blocks:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "bedrock-platform"
Environment = "prod"
ManagedBy = "terraform"
CostCenter = "ai-platform"
}
}
}
# Invocation logging — the governance backbone
resource "aws_bedrock_model_invocation_logging_configuration" "main" {
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.bedrock_logs.id
key_prefix = "invocations/"
}
}
depends_on = [aws_iam_role_policy.bedrock_logging]
}
Key resources you will manage in Terraform:
aws_bedrock_model_invocation_logging_configuration— log every invocation to S3 + CloudWatch.aws_bedrock_guardrail+aws_bedrock_guardrail_version— content filters, denied topics, PII handling.aws_bedrock_provisioned_model_throughput— reserved capacity for latency-critical models.aws_bedrockagent_agent,aws_bedrockagent_knowledge_base— if you build agents/RAG.- IAM roles and policies for every workload identity.
- S3 buckets, CloudWatch log groups, KMS keys for the telemetry layer.
Level 2: IAM Policies — Least Privilege or Nothing
Bedrock IAM is granular: actions are scoped to specific models via ARN. Deny-by-default and grant only what each workload needs.
Workload invocation policy
data "aws_iam_policy_document" "bedrock_invoke" {
statement {
sid = "InvokeApprovedModels"
effect = "Allow"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:Converse",
"bedrock:ConverseStream",
]
resources = [
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
"arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0",
]
}
statement {
sid = "EnforceGuardrail"
effect = "Allow"
actions = ["bedrock:ApplyGuardrail"]
resources = [aws_bedrock_guardrail_version.main.guardrail_arn]
}
}
resource "aws_iam_policy" "bedrock_invoke" {
name = "bedrock-invoke-approved-models"
policy = data.aws_iam_policy_document.bedrock_invoke.json
}
Governance guardrails in policy form
Patterns that separate mature platforms from demos:
- Model allowlist — enumerate foundation-model ARNs; never
Resource: "*". - Region restriction — a Service Control Policy (SCP) at the AWS Organizations level denying
bedrock:*outside approved regions:
{
"Sid": "DenyBedrockOutsideApprovedRegions",
"Effect": "Deny",
"Action": "bedrock:*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": ["us-east-1"] }
}
}
- Deny provisioned throughput for dev accounts — an SCP blocking
bedrock:CreateProvisionedModelThroughputoutside the production org path prevents accidental $$$ commitments. - Cross-account inference profiles — use
aws:SourceAccountconditions and inference-profile ARNs so usage is attributable per account.
Level 3: Governance — Guardrails, Logging, Audit
Bedrock Guardrails in Terraform
resource "aws_bedrock_guardrail" "main" {
name = "platform-standard-guardrail"
blocked_input_messaging = "This request violates platform content policy."
blocked_outputs_messaging = "The response was blocked by platform policy."
content_policy_config {
filters_config {
type = "HATE"
input_strength = "HIGH"
output_strength = "HIGH"
}
filters_config {
type = "PROMPT_ATTACK"
input_strength = "HIGH"
output_strength = "NONE"
}
}
sensitive_information_policy_config {
pii_entities_config {
type = "EMAIL"
action = "ANONYMIZE"
}
pii_entities_config {
type = "CREDIT_DEBIT_CARD_NUMBER"
action = "BLOCK"
}
}
topic_policy_config {
topics_config {
name = "financial-advice"
definition = "Personalized investment, tax, or financial advice."
examples = ["Should I buy Tesla stock?"]
type = "DENY"
}
}
word_policy_config {
managed_word_lists_config {
type = "PROFANITY"
}
}
}
resource "aws_bedrock_guardrail_version" "main" {
guardrail_arn = aws_bedrock_guardrail.main.guardrail_arn
description = "v1 — initial platform policy"
}
Applications reference the guardrail by ID + version in every Converse/Invoke call — policy enforcement becomes a platform property, not an app-team choice.
Audit trail
- CloudTrail records every
InvokeModelmanagement-plane event. Enable a multi-region, organization-wide trail. - Invocation logs (the Terraform resource from Level 1) capture full request/response payloads to S3. Partition by date, apply lifecycle rules (hot → Glacier → expire), encrypt with KMS.
- Bedrock Application Inference Profiles — tag traffic per application/team; the single most important governance primitive for cost allocation.
Level 4: FinOps — Know Your Bill Before Finance Does
Bedrock pricing is token-based per model plus provisioned throughput. The controls that matter:
Application Inference Profiles for cost allocation
resource "aws_bedrock_application_inference_profile" "checkout_service" {
name = "checkout-service-sonnet"
description = "Sonnet usage attributed to checkout-service team"
model_source {
copy_from = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0"
}
tags = {
Application = "checkout-service"
Team = "payments"
Environment = "prod"
}
}
Tags on inference profiles flow into Cost Explorer and the Cost and Usage Report (CUR) — you get per-team, per-model, per-environment token costs without a sidecar gateway.
Practical cost levers
- Model routing — route simple tasks to Nova Lite / Claude Haiku; reserve Sonnet/Opus for hard tasks. A routing layer typically cuts spend 40–70%.
- Prompt caching — cache system prompts and long contexts; cached input tokens are billed at a steep discount.
- Batch inference —
CreateModelInvocationJobfor offline workloads at ~50% of on-demand price. - Budgets + anomaly detection — AWS Budgets per tag (
Team=payments) with alerts at 80/100/120%; Cost Anomaly Detection scoped to the Bedrock service. - Token quotas in the app layer — enforce per-user/per-day token ceilings before the call reaches Bedrock.
- Provisioned throughput only when justified — commit when sustained utilization is predictable; on-demand otherwise. Track utilization in CloudWatch (
Invocations,InputTokenCount,OutputTokenCount).
For a deeper FinOps architecture, see the companion post FinOps for Centralized AWS Bedrock AI Cost Allocation.
Level 5: Reference GitHub Repos Worth Studying
Before building, read the repos that encode AWS and community best practices:
| Repo | Why it matters |
|---|---|
| aws-samples/amazon-bedrock-samples | Official samples — every feature, every SDK, well maintained |
| aws-samples/bedrock-claude-chat | Full-stack chat app with RAG, auth, and CDK deployment — a real reference architecture |
| aws-samples/generative-ai-use-cases-jp | Production-grade GenAI use-case collection with CDK infra (English docs included) |
| aws-samples/amazon-bedrock-guardrails | Guardrail patterns and policy examples |
| aws-samples/sample-connector-for-bedrock | OpenAI-compatible gateway in front of Bedrock — useful for FinOps metering and app migration |
| terraform-aws-modules | Not Bedrock-specific, but the standard for the surrounding VPC/IAM/KMS modules you will compose |
| langchain-ai/langchain-aws | LangChain Bedrock integrations if your app layer uses it |
Search pattern that surfaces strong community work: bedrock terraform stars:>50 and bedrock cdk stars:>100 on GitHub.
Level 6: Complete Production Infrastructure Example
Everything assembled: a modular Terraform stack with model invocation logging, guardrails, least-privilege IAM for a Lambda workload, an application inference profile for cost attribution, and FinOps budgets.
Layout
infra/
├── main.tf # provider, backend, module wiring
├── variables.tf
├── outputs.tf
├── kms.tf # encryption keys
├── logging.tf # S3 + CloudWatch + invocation logging
├── guardrails.tf # guardrail + version
├── iam.tf # workload role + policies
├── inference_profiles.tf
└── finops.tf # budgets, anomaly detection
iam.tf — Lambda workload
resource "aws_iam_role" "ai_backend_lambda" {
name = "ai-backend-lambda-role"
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" "invoke" {
role = aws_iam_role.ai_backend_lambda.name
policy_arn = aws_iam_policy.bedrock_invoke.arn
}
finops.tf
resource "aws_budgets_budget" "bedrock_team" {
name = "bedrock-checkout-service-monthly"
budget_type = "COST"
limit_amount = "500"
limit_unit = "USD"
time_unit = "MONTHLY"
cost_filter {
name = "TagKeyValue"
values = ["user:Application$checkout-service"]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["finops@example.com"]
}
}
resource "aws_ce_anomaly_monitor" "bedrock" {
name = "bedrock-service-monitor"
monitor_type = "DIMENSIONAL"
monitor_dimension = "SERVICE"
}
resource "aws_ce_anomaly_subscription" "bedrock" {
name = "bedrock-anomaly-alerts"
frequency = "DAILY"
monitor_arn_list = [aws_ce_anomaly_monitor.bedrock.arn]
subscriber {
type = "EMAIL"
address = "finops@example.com"
}
threshold_expression {
dimension {
key = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
values = ["100"]
match_options = ["GREATER_THAN_OR_EQUAL"]
}
}
}
logging.tf — telemetry backbone
resource "aws_s3_bucket" "bedrock_logs" {
bucket = "acme-bedrock-invocation-logs-prod"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "bedrock_logs" {
bucket = aws_s3_bucket.bedrock_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.bedrock.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "bedrock_logs" {
bucket = aws_s3_bucket.bedrock_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "bedrock_logs" {
bucket = aws_s3_bucket.bedrock_logs.id
rule {
id = "archive-invocations"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
resource "aws_cloudwatch_log_group" "bedrock_invocations" {
name = "/aws/bedrock/invocations"
retention_in_days = 90
kms_key_id = aws_kms_key.bedrock.arn
}
The Lambda invocation with guardrails + inference profile
response = client.converse(
# Inference profile ARN → cost attribution + cross-region routing
modelId="arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/checkout-service-sonnet",
messages=[{"role": "user", "content": [{"text": prompt}]}],
guardrailConfig={
"guardrailIdentifier": "platform-standard-guardrail",
"guardrailVersion": "1",
"trace": "enabled",
},
inferenceConfig={"maxTokens": 1024, "temperature": 0.2},
)
Definition of done checklist
- Model access requested per region, documented in code comments
- Invocation logging on (S3 + CloudWatch, KMS-encrypted, lifecycle rules)
- Guardrail created, versioned, referenced in every call
- Workload roles least-privilege; no
Resource: "*"on bedrock actions - SCP: region allowlist + deny provisioned throughput outside prod
- Application inference profile per app/team with FinOps tags
- Budgets + anomaly detection alerts routed to owners
- Prompt caching and model routing implemented in the app layer
- CloudTrail org trail covers all Bedrock regions
- Zero long-lived access keys anywhere
Best Practices Recap — The Ninja Rules
- Converse API by default — provider portability for free.
- Everything in Terraform — console is read-only.
- IAM per model ARN — allowlists, not wildcards.
- Guardrails are platform policy — versioned, enforced at call time.
- Inference profiles = FinOps unit — tag first, scale second.
- Log every invocation — S3, encrypted, lifecycle-managed.
- Route models by task difficulty — cost and latency both improve.
- Cache prompts, batch offline jobs — the two biggest price levers.
- SCPs for blast-radius control — regions, provisioned throughput, marketplace subscriptions.
- Read the reference repos — aws-samples encodes years of hard lessons.