Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions anthropic_workloads/.terraform-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
content: |-
{{ .Header }}

{{ .Inputs }}

{{ .Outputs }}
21 changes: 21 additions & 0 deletions anthropic_workloads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!-- BEGIN_TF_DOCS -->
# anthropic\_workloads

Service accounts and federation rules that let workloads call the Anthropic API with their own OIDC token instead of an API key.

Issuers and workspaces are created elsewhere and passed in by ID. The provider must authenticate with an `org:admin` token.

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_application"></a> [application](#input\_application) | The name of the application (tenant app\_code). Every workload name must start with it. | `string` | n/a | yes |
| <a name="input_audience"></a> [audience](#input\_audience) | Audience the identity token must carry. | `string` | `"https://api.anthropic.com"` | no |
| <a name="input_workloads"></a> [workloads](#input\_workloads) | Workloads that call the Anthropic API with their own OIDC token. The key is the name of the<br/>service account and federation rule, in the form <app\_code>-<env\_code>-<component\_code>.<br/>claims are the exact claims the identity token must carry. | <pre>map(object({<br/> issuer_id = string<br/> workspace_id = string<br/> claims = map(string)<br/> }))</pre> | `{}` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_workloads"></a> [workloads](#output\_workloads) | Federation rule and service account IDs that each workload exchanges its OIDC token against. |
<!-- END_TF_DOCS -->
39 changes: 39 additions & 0 deletions anthropic_workloads/examples/example1.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module "anthropic_workloads" {
source = "github.com/mozilla/terraform-modules//anthropic_workloads?ref=anthropic_workloads-0.1.0"
application = "example"

workloads = {
# GitHub Actions job on the main branch.
"example-prod-api" = {
issuer_id = "fdis_example_github"
workspace_id = "wrkspc_example"
claims = {
repository = "example-org/example-repo"
ref = "refs/heads/main"
}
}

# Google service account. sub is the service account's numeric unique ID.
"example-prod-worker" = {
issuer_id = "fdis_example_google"
workspace_id = "wrkspc_example"
claims = {
sub = "111111111111111111111"
email = "worker@example-project.iam.gserviceaccount.com"
}
}

# Workload with a SPIFFE identity.
"example-prod-agent" = {
issuer_id = "fdis_example_spiffe"
workspace_id = "wrkspc_example"
claims = {
sub = "spiffe://example.org/ns/example/sa/agent"
}
}
}
}

output "workloads" {
value = module.anthropic_workloads.workloads
}
33 changes: 33 additions & 0 deletions anthropic_workloads/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* # anthropic_workloads
*
* Service accounts and federation rules that let workloads call the Anthropic API with their own OIDC token instead of an API key.
*
* Issuers and workspaces are created elsewhere and passed in by ID. The provider must authenticate with an `org:admin` token.
*/

resource "anthropic_service_account" "workloads" {
for_each = var.workloads
name = each.key
organization_role = "developer"
}

resource "anthropic_service_account_workspace" "workloads" {
for_each = var.workloads
service_account_id = anthropic_service_account.workloads[each.key].id
workspace_id = each.value.workspace_id
workspace_role = "workspace_user"
}

resource "anthropic_federation_rule" "workloads" {
for_each = var.workloads
name = each.key
issuer_id = each.value.issuer_id
service_account_id = anthropic_service_account.workloads[each.key].id
oauth_scope = "workspace:inference"
workspace_ids = [each.value.workspace_id]
match = {
audience = var.audience
claims = each.value.claims
}
}
10 changes: 10 additions & 0 deletions anthropic_workloads/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
output "workloads" {
description = "Federation rule and service account IDs that each workload exchanges its OIDC token against."
value = {
for name, w in var.workloads : name => {
federation_rule_id = anthropic_federation_rule.workloads[name].id
service_account_id = anthropic_service_account.workloads[name].id
workspace_id = w.workspace_id
}
}
}
92 changes: 92 additions & 0 deletions anthropic_workloads/tests/anthropic_workloads.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
mock_provider "anthropic" {}

variables {
application = "app"
}

run "creates_account_rule_and_link_per_workload" {
command = plan

variables {
workloads = {
"app-dev-api" = {
issuer_id = "fdis_example"
workspace_id = "wrkspc_example"
claims = { sub = "spiffe://example/api" }
}
}
}

assert {
condition = anthropic_service_account.workloads["app-dev-api"].organization_role == "developer"
error_message = "Service accounts must have the developer organization role."
}

assert {
condition = anthropic_federation_rule.workloads["app-dev-api"].oauth_scope == "workspace:inference"
error_message = "Rules must grant workspace:inference only."
}

assert {
condition = anthropic_federation_rule.workloads["app-dev-api"].match.audience == "https://api.anthropic.com"
error_message = "Rules must match the default audience."
}

assert {
condition = anthropic_federation_rule.workloads["app-dev-api"].match.claims == tomap({ sub = "spiffe://example/api" })
error_message = "Rules must match the given claims exactly."
}

assert {
condition = anthropic_service_account_workspace.workloads["app-dev-api"].workspace_id == "wrkspc_example"
error_message = "Service account must be a member of the workload's workspace."
}
}

run "name_must_start_with_application" {
command = plan

variables {
workloads = {
"other-dev-api" = {
issuer_id = "fdis_example"
workspace_id = "wrkspc_example"
claims = { sub = "x" }
}
}
}

expect_failures = [var.workloads]
}

run "name_must_be_lowercase" {
command = plan

variables {
workloads = {
"app-Dev-api" = {
issuer_id = "fdis_example"
workspace_id = "wrkspc_example"
claims = { sub = "x" }
}
}
}

expect_failures = [var.workloads]
}

run "claims_are_required" {
command = plan

variables {
workloads = {
"app-dev-api" = {
issuer_id = "fdis_example"
workspace_id = "wrkspc_example"
claims = {}
}
}
}

expect_failures = [var.workloads]
}
39 changes: 39 additions & 0 deletions anthropic_workloads/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
variable "application" {
description = "The name of the application (tenant app_code). Every workload name must start with it."
type = string
}

variable "workloads" {
description = <<-EOT
Workloads that call the Anthropic API with their own OIDC token. The key is the name of the
service account and federation rule, in the form <app_code>-<env_code>-<component_code>.
claims are the exact claims the identity token must carry.
EOT
type = map(object({
issuer_id = string
workspace_id = string
claims = map(string)
}))
default = {}

validation {
condition = alltrue([for name in keys(var.workloads) : can(regex("^[a-z0-9-]{1,255}$", name))])
error_message = "Workload names use lowercase letters, numbers, and hyphens only."
}

validation {
condition = alltrue([for name in keys(var.workloads) : startswith(name, "${var.application}-")])
error_message = "Workload names must start with the application name followed by a hyphen."
}

validation {
condition = alltrue([for w in values(var.workloads) : length(w.claims) > 0])
error_message = "Each workload needs at least one claim to match."
}
}

variable "audience" {
description = "Audience the identity token must carry."
type = string
default = "https://api.anthropic.com"
}
10 changes: 10 additions & 0 deletions anthropic_workloads/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9"

required_providers {
anthropic = {
source = "mozilla/anthropic"
version = ">= 0.1.0, < 1.0.0"
}
}
}
Loading