diff --git a/README.md b/README.md
index f04cf43..fa35dc9 100644
--- a/README.md
+++ b/README.md
@@ -22,24 +22,33 @@ No modules.
| Name | Type |
| ---- | ---- |
| [aws_iam_access_key.admin_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
+| [aws_iam_access_key.bedrock_opencode](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
| [aws_iam_access_key.sops_secrets_operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
| [aws_iam_openid_connect_provider.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource |
+| [aws_iam_role.bedrock_batch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy.bedrock_batch_invoke](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
+| [aws_iam_role_policy.bedrock_batch_s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_user.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
+| [aws_iam_user.bedrock_opencode](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
| [aws_iam_user.sops_secrets_operator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
+| [aws_iam_user_policy.bedrock_opencode](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy) | resource |
| [aws_iam_user_policy.sops_secrets_operator_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy) | resource |
| [aws_iam_user_policy_attachment.admin_attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource |
| [aws_kms_alias.sops](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
| [aws_kms_key.sops](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
+| [aws_s3_bucket.bedrock_batch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket_policy.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
| [aws_s3_bucket_policy.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
+| [aws_s3_bucket_public_access_block.bedrock_batch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_public_access_block.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_public_access_block.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_website_configuration.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_website_configuration) | resource |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [sops_file.secret_vars](https://registry.terraform.io/providers/carlpett/sops/latest/docs/data-sources/file) | data source |
## Inputs
@@ -51,6 +60,10 @@ No inputs.
| Name | Description |
| ---- | ----------- |
| [admin\_access\_keys](#output\_admin\_access\_keys) | Admin IAM user access keys |
+| [bedrock\_batch\_bucket\_name](#output\_bedrock\_batch\_bucket\_name) | S3 bucket for Bedrock batch inference input and output data |
+| [bedrock\_batch\_service\_role\_arn](#output\_bedrock\_batch\_service\_role\_arn) | Service role ARN to pass as roleArn when creating Bedrock batch inference jobs |
+| [bedrock\_opencode\_access\_key](#output\_bedrock\_opencode\_access\_key) | Access key for OpenCode to invoke Anthropic models via AWS Bedrock |
+| [bedrock\_opencode\_iam\_user\_arn](#output\_bedrock\_opencode\_iam\_user\_arn) | IAM user ARN for OpenCode Bedrock access |
| [github\_actions\_sops\_kms\_role\_arn](#output\_github\_actions\_sops\_kms\_role\_arn) | IAM role ARN for GitHub Actions SOPS KMS access |
| [sops\_kms\_key\_arn](#output\_sops\_kms\_key\_arn) | KMS key ARN for future SOPS AWS KMS recipients |
| [sops\_secrets\_operator\_access\_key](#output\_sops\_secrets\_operator\_access\_key) | Access key for the k3s sops-secrets-operator to decrypt SOPS AWS KMS secrets |
diff --git a/aws-bedrock.tf b/aws-bedrock.tf
new file mode 100644
index 0000000..3887250
--- /dev/null
+++ b/aws-bedrock.tf
@@ -0,0 +1,196 @@
+data "aws_caller_identity" "current" {}
+
+# Batch inference (50% discount vs on-demand) requires a dedicated service
+# role: CreateModelInvocationJob takes a roleArn that Bedrock assumes for S3
+# input/output access. The opencode user below gets iam:PassRole on it.
+resource "aws_s3_bucket" "bedrock_batch" {
+ bucket = "mitw-bedrock-batch"
+
+ tags = {
+ ManagedBy = "Terraform"
+ Purpose = "bedrock-batch-inference"
+ }
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+
+resource "aws_s3_bucket_public_access_block" "bedrock_batch" {
+ bucket = aws_s3_bucket.bedrock_batch.bucket
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
+
+resource "aws_iam_role" "bedrock_batch" {
+ name = "bedrock-batch-inference"
+
+ 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
+ }
+ ArnEquals = {
+ "aws:SourceArn" = "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:model-invocation-job/*"
+ }
+ }
+ }
+ ]
+ })
+
+ tags = {
+ ManagedBy = "Terraform"
+ Purpose = "bedrock-batch-inference"
+ }
+}
+
+resource "aws_iam_role_policy" "bedrock_batch_s3" {
+ name = "bedrock-batch-s3"
+ role = aws_iam_role.bedrock_batch.name
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "S3Access"
+ Effect = "Allow"
+ Action = [
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:ListBucket"
+ ]
+ Resource = [
+ aws_s3_bucket.bedrock_batch.arn,
+ "${aws_s3_bucket.bedrock_batch.arn}/*"
+ ]
+ Condition = {
+ StringEquals = {
+ "aws:ResourceAccount" = data.aws_caller_identity.current.account_id
+ }
+ }
+ }
+ ]
+ })
+}
+
+# Newer Anthropic models (Opus 4.x) reject base model IDs and must be called
+# through inference profiles, so both resource types are granted everywhere.
+resource "aws_iam_role_policy" "bedrock_batch_invoke" {
+ name = "bedrock-batch-invoke"
+ role = aws_iam_role.bedrock_batch.name
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "InvokeAnthropicModels"
+ Effect = "Allow"
+ Action = [
+ "bedrock:InvokeModel"
+ ]
+ Resource = [
+ "arn:aws:bedrock:*::foundation-model/anthropic.*",
+ "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:inference-profile/*.anthropic.*"
+ ]
+ }
+ ]
+ })
+}
+
+resource "aws_iam_user" "bedrock_opencode" {
+ name = "opencode-bedrock"
+ force_destroy = false
+
+ tags = {
+ ManagedBy = "Terraform"
+ Purpose = "opencode-bedrock"
+ }
+}
+
+resource "aws_iam_user_policy" "bedrock_opencode" {
+ name = "bedrock-invoke"
+ user = aws_iam_user.bedrock_opencode.name
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "InvokeAnthropicModels"
+ Effect = "Allow"
+ Action = [
+ "bedrock:InvokeModel",
+ "bedrock:InvokeModelWithResponseStream"
+ ]
+ Resource = [
+ "arn:aws:bedrock:*::foundation-model/anthropic.*",
+ "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:inference-profile/*.anthropic.*"
+ ]
+ },
+ {
+ Sid = "DiscoverInferenceProfiles"
+ Effect = "Allow"
+ Action = ["bedrock:ListInferenceProfiles"]
+ Resource = "*"
+ },
+ {
+ Sid = "ManageBatchJobs"
+ Effect = "Allow"
+ Action = [
+ "bedrock:CreateModelInvocationJob",
+ "bedrock:GetModelInvocationJob",
+ "bedrock:StopModelInvocationJob",
+ "bedrock:TagResource"
+ ]
+ Resource = [
+ "arn:aws:bedrock:*::foundation-model/anthropic.*",
+ "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:inference-profile/*.anthropic.*",
+ "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:model-invocation-job/*"
+ ]
+ },
+ {
+ Sid = "ListBatchJobs"
+ Effect = "Allow"
+ Action = ["bedrock:ListModelInvocationJobs"]
+ Resource = "*"
+ },
+ {
+ Sid = "BatchDataS3Access"
+ Effect = "Allow"
+ Action = [
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:ListBucket"
+ ]
+ Resource = [
+ aws_s3_bucket.bedrock_batch.arn,
+ "${aws_s3_bucket.bedrock_batch.arn}/*"
+ ]
+ },
+ {
+ Sid = "PassBatchServiceRole"
+ Effect = "Allow"
+ Action = ["iam:PassRole"]
+ Resource = aws_iam_role.bedrock_batch.arn
+ Condition = {
+ StringEquals = {
+ "iam:PassedToService" = "bedrock.amazonaws.com"
+ }
+ }
+ }
+ ]
+ })
+}
+
+resource "aws_iam_access_key" "bedrock_opencode" {
+ user = aws_iam_user.bedrock_opencode.name
+}
diff --git a/outputs.tf b/outputs.tf
index 017f229..f8a15fa 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -38,3 +38,27 @@ output "sops_secrets_operator_iam_user_arn" {
description = "IAM user ARN for the k3s sops-secrets-operator"
value = aws_iam_user.sops_secrets_operator.arn
}
+
+output "bedrock_opencode_access_key" {
+ description = "Access key for OpenCode to invoke Anthropic models via AWS Bedrock"
+ value = {
+ access_key_id = aws_iam_access_key.bedrock_opencode.id
+ secret_access_key = aws_iam_access_key.bedrock_opencode.secret
+ }
+ sensitive = true
+}
+
+output "bedrock_opencode_iam_user_arn" {
+ description = "IAM user ARN for OpenCode Bedrock access"
+ value = aws_iam_user.bedrock_opencode.arn
+}
+
+output "bedrock_batch_service_role_arn" {
+ description = "Service role ARN to pass as roleArn when creating Bedrock batch inference jobs"
+ value = aws_iam_role.bedrock_batch.arn
+}
+
+output "bedrock_batch_bucket_name" {
+ description = "S3 bucket for Bedrock batch inference input and output data"
+ value = aws_s3_bucket.bedrock_batch.bucket
+}