Skip to content
yisusvii
Go back

AWS Bedrock from Zero to Ninja: First Model Call to Production Infrastructure

Suggest Changes

Table of Contents

Open Table of Contents

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:

  1. Call your first model correctly.
  2. Provision everything with Terraform.
  3. Lock down IAM with least-privilege policies.
  4. Add governance with Guardrails, logging, and CloudTrail.
  5. Wire in FinOps before costs scale.
  6. Study the reference repos that encode real-world lessons.
  7. 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:


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:

{
  "Sid": "DenyBedrockOutsideApprovedRegions",
  "Effect": "Deny",
  "Action": "bedrock:*",
  "Resource": "*",
  "Condition": {
    "StringNotEquals": { "aws:RequestedRegion": ["us-east-1"] }
  }
}

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


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

  1. Model routing — route simple tasks to Nova Lite / Claude Haiku; reserve Sonnet/Opus for hard tasks. A routing layer typically cuts spend 40–70%.
  2. Prompt caching — cache system prompts and long contexts; cached input tokens are billed at a steep discount.
  3. Batch inferenceCreateModelInvocationJob for offline workloads at ~50% of on-demand price.
  4. Budgets + anomaly detection — AWS Budgets per tag (Team=payments) with alerts at 80/100/120%; Cost Anomaly Detection scoped to the Bedrock service.
  5. Token quotas in the app layer — enforce per-user/per-day token ceilings before the call reaches Bedrock.
  6. 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:

RepoWhy it matters
aws-samples/amazon-bedrock-samplesOfficial samples — every feature, every SDK, well maintained
aws-samples/bedrock-claude-chatFull-stack chat app with RAG, auth, and CDK deployment — a real reference architecture
aws-samples/generative-ai-use-cases-jpProduction-grade GenAI use-case collection with CDK infra (English docs included)
aws-samples/amazon-bedrock-guardrailsGuardrail patterns and policy examples
aws-samples/sample-connector-for-bedrockOpenAI-compatible gateway in front of Bedrock — useful for FinOps metering and app migration
terraform-aws-modulesNot Bedrock-specific, but the standard for the surrounding VPC/IAM/KMS modules you will compose
langchain-ai/langchain-awsLangChain 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


Best Practices Recap — The Ninja Rules

  1. Converse API by default — provider portability for free.
  2. Everything in Terraform — console is read-only.
  3. IAM per model ARN — allowlists, not wildcards.
  4. Guardrails are platform policy — versioned, enforced at call time.
  5. Inference profiles = FinOps unit — tag first, scale second.
  6. Log every invocation — S3, encrypted, lifecycle-managed.
  7. Route models by task difficulty — cost and latency both improve.
  8. Cache prompts, batch offline jobs — the two biggest price levers.
  9. SCPs for blast-radius control — regions, provisioned throughput, marketplace subscriptions.
  10. Read the reference repos — aws-samples encodes years of hard lessons.

Further Reading


Suggest Changes
Share this post on:


Previous Post
DevOps & SRE Weekly Digest — 2026-09-09
Next Post
HyperFrames: Rendering Deterministic MP4 Video From Plain HTML