From e4ce8b51a46e12811a1236e7dce10e0b71e573cd Mon Sep 17 00:00:00 2001 From: xnoto Date: Tue, 25 Aug 2026 13:13:54 -0600 Subject: [PATCH] feat: add AWS Bedrock access for OpenCode with batch inference support Add opencode-bedrock IAM user scoped to Anthropic models via foundation model and inference profile ARNs (Opus 4.x requires inference profiles). Batch inference (50% discount vs on-demand) requires CreateModelInvocationJob with a mandatory service role ARN and S3 input/output, so this also adds the bedrock-batch-inference service role, mitw-bedrock-batch bucket, and PassRole/S3 permissions for the user. --- README.md | 13 ++++ aws-bedrock.tf | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ outputs.tf | 24 ++++++ 3 files changed, 233 insertions(+) create mode 100644 aws-bedrock.tf 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 +}