From e869eba7cb8ae8246f56144ace893b569dfe5459 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 5 May 2026 22:05:47 +0200 Subject: [PATCH 1/3] feat(iac): scaffolding for Archera integration perms across AWS/Azure/GCP (closes #305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds enable_archera = false tfvars flag to each cloud environment. When flipped to true, each cloud conditionally creates: - AWS: cross-account IAM role (cudly-archera-integration) with least- privilege Cost Explorer read + RI/SP purchase policy - Azure: custom RBAC role (CUDly Archera Integration) with cost read + reservation purchase actions, assigned to Archera's service principal - GCP: custom project IAM role (cudlyArcheraIntegration) with billing read + CUD purchase permissions, granted to Archera's service account All permission lists are clearly marked PROVISIONAL — they must be confirmed against Archera's integration docs before any environment sets enable_archera = true. TODO(@cristim) comments in archera.tf per cloud call this out explicitly. Per the bootstrap-vs-runtime IAM split: Archera is a runtime integration (cost telemetry reads + commitment purchases during normal operation), so archera.tf lives in the main environment alongside compute.tf, NOT in ci-cd-permissions/. README addendums added to all three cloud envs. terraform fmt and terraform validate pass for all three environments with the flag both on and off. --- terraform/environments/aws/README.md | 76 ++++++++ terraform/environments/aws/archera.tf | 169 ++++++++++++++++++ terraform/environments/aws/dev.tfvars.example | 11 ++ terraform/environments/aws/variables.tf | 34 ++++ terraform/environments/azure/README.md | 35 ++++ terraform/environments/azure/archera.tf | 78 ++++++++ .../environments/azure/dev.tfvars.example | 11 ++ terraform/environments/azure/variables.tf | 33 ++++ terraform/environments/gcp/README.md | 35 ++++ terraform/environments/gcp/archera.tf | 83 +++++++++ terraform/environments/gcp/dev.tfvars.example | 11 ++ terraform/environments/gcp/variables.tf | 35 ++++ 12 files changed, 611 insertions(+) create mode 100644 terraform/environments/aws/README.md create mode 100644 terraform/environments/aws/archera.tf create mode 100644 terraform/environments/azure/archera.tf create mode 100644 terraform/environments/gcp/archera.tf diff --git a/terraform/environments/aws/README.md b/terraform/environments/aws/README.md new file mode 100644 index 00000000..75389717 --- /dev/null +++ b/terraform/environments/aws/README.md @@ -0,0 +1,76 @@ +# AWS Environment Configuration + +This directory contains the Terraform configuration for CUDly on AWS with environment-specific values extracted into `.tfvars` files. + +## Structure + +```text +aws/ +├── main.tf # Provider + locals +├── variables.tf # Variable declarations +├── outputs.tf # Output definitions +├── backend.tf # Backend configuration (S3) +├── networking.tf # VPC, subnets, security groups +├── compute.tf # Lambda / Fargate resources +├── database.tf # RDS PostgreSQL +├── secrets.tf # Secrets Manager +├── build.tf # Docker build (optional) +├── frontend.tf # CloudFront + S3 (optional) +├── archera.tf # Archera integration (gated on enable_archera) +├── dev.tfvars.example # Example dev values (copy to dev.tfvars) +└── ci-cd-permissions/ # Bootstrap-only: deploy IAM role (apply once) + └── README.md +``` + +## Usage + +```bash +# Copy example config +cp dev.tfvars.example dev.tfvars +# Edit dev.tfvars with your values + +# Initialize with dev backend +terraform init -backend-config=backends/dev.tfbackend + +# Plan +terraform plan -var-file=dev.tfvars + +# Apply +terraform apply -var-file=dev.tfvars +``` + +## Archera Integration + +The Archera commitment-optimisation integration is gated behind the +`enable_archera` tfvars flag (default: `false`) so non-Archera customers +see no drift. + +### Enabling Archera + +> **IMPORTANT — scope confirmation required before enabling.** +> The permission list in `archera.tf` is provisional. Confirm the exact +> AWS IAM actions required against [Archera's integration docs](https://archera.ai/docs) +> and validate with `@cristim` before setting `enable_archera = true` in +> any environment. See `TODO(@cristim)` comments in `archera.tf`. + +1. Obtain the Archera **AWS account ID** from Archera during onboarding + (the account whose IAM principal will assume the cross-account role). +2. Set the following in your `*.tfvars`: + + ```hcl + enable_archera = true + archera_aws_account_id = "123456789012" # Archera's account ID + ``` + +3. Run `terraform plan` to review the cross-account IAM role and policy + that will be created, then apply. + +### What gets created + +When `enable_archera = true`: + +| Resource | Purpose | +| --- | --- | +| `aws_iam_role.archera_integration[0]` | Cross-account role trusted by Archera's AWS account | +| `aws_iam_policy.archera_integration[0]` | Provisional least-privilege policy (read cost data + purchase RIs/SPs) | +| `aws_iam_role_policy_attachment.archera_integration[0]` | Attaches the policy to the role | diff --git a/terraform/environments/aws/archera.tf b/terraform/environments/aws/archera.tf new file mode 100644 index 00000000..b282bcb8 --- /dev/null +++ b/terraform/environments/aws/archera.tf @@ -0,0 +1,169 @@ +# ============================================== +# Archera Integration — AWS +# ============================================== +# +# When enable_archera = true, this block creates a cross-account IAM role +# that Archera assumes to read commitment and cost data and to execute +# Reserved Instance / Savings Plan purchases on behalf of the customer. +# +# PROVISONAL SCOPE — must be confirmed against Archera integration docs +# before flipping enable_archera = true in any tfvars. +# TODO(@cristim): confirm Archera scope list against integration docs +# before enabling. Reference: https://archera.ai/docs (integration guide). +# +# Placement rationale (bootstrap vs runtime split): +# Archera is a RUNTIME integration — it reads cost telemetry and submits +# purchases during normal operation, not during Terraform deploys. This +# file therefore lives in the main environment alongside compute.tf / +# database.tf, NOT in ci-cd-permissions/ (which is applied once by a +# privileged human and grants deploy-SA capabilities only). + +locals { + archera_role_name = "cudly-archera-integration" +} + +# IAM role that Archera's AWS account assumes to access this account. +resource "aws_iam_role" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + name = local.archera_role_name + description = "Assumed by Archera SaaS to read cost data and execute RI/SP purchases (provisional — confirm scope before enabling)" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "ArcheraAssumeRole" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${var.archera_aws_account_id}:root" + } + Action = "sts:AssumeRole" + # Optional: require ExternalId supplied by Archera during onboarding. + # Uncomment and set archera_external_id once confirmed with Archera. + # Condition = { + # StringEquals = { "sts:ExternalId" = var.archera_external_id } + # } + }, + ] + }) + + tags = merge(local.common_tags, { + Integration = "archera" + Purpose = "commitment-optimisation" + }) +} + +# Least-privilege policy for Archera — PROVISIONAL. +# Actions are split into read-only cost/commitment data (safe to enable +# immediately) and purchase-execution (requires explicit confirmation with +# Archera before enabling). +# +# TODO(@cristim): narrow to the exact action list from Archera's onboarding +# docs before setting enable_archera = true in any environment. +resource "aws_iam_policy" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + name = "cudly-archera-integration" + description = "Provisional Archera integration policy — read commitment + cost data, execute RI/SP purchases" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # ── Read-only: Cost Explorer ────────────────────────────────────────── + # Archera needs to read historical usage and costs to size commitments. + # TODO(@cristim): confirm whether Archera also needs ce:GetReservation* + # and ce:GetSavingsPlan* actions — add them if so. + { + Sid = "CostExplorerReadOnly" + Effect = "Allow" + Action = [ + "ce:GetCostAndUsage", + "ce:GetCostAndUsageWithResources", + "ce:GetCostForecast", + "ce:GetDimensionValues", + "ce:GetReservationCoverage", + "ce:GetReservationPurchaseRecommendation", + "ce:GetReservationUtilization", + "ce:GetRightsizingRecommendation", + "ce:GetSavingsPlansCoverage", + "ce:GetSavingsPlansUtilization", + "ce:GetSavingsPlansUtilizationDetails", + "ce:ListCostCategoryDefinitions", + ] + Resource = "*" + }, + # ── Read-only: CUR / Cost and Usage Report ──────────────────────────── + # Describes which CUR reports are configured; Archera may consume CUR + # exports directly for more granular data. + { + Sid = "CURDescribe" + Effect = "Allow" + Action = [ + "cur:DescribeReportDefinitions", + ] + Resource = "*" + }, + # ── Read-only: Reserved Instances ───────────────────────────────────── + # Archera needs to see existing RIs to avoid over-purchasing. + { + Sid = "ReservedInstancesRead" + Effect = "Allow" + Action = [ + "ec2:DescribeReservedInstances", + "ec2:DescribeReservedInstancesListings", + "ec2:DescribeReservedInstancesModifications", + "ec2:DescribeReservedInstancesOfferings", + "ec2:DescribeInstanceTypeOfferings", + ] + Resource = "*" + }, + # ── Read-only: Savings Plans ───────────────────────────────────────── + { + Sid = "SavingsPlansRead" + Effect = "Allow" + Action = [ + "savingsplans:DescribeSavingsPlans", + "savingsplans:DescribeSavingsPlansOfferings", + "savingsplans:DescribeSavingsPlanRates", + "savingsplans:ListTagsForResource", + ] + Resource = "*" + }, + # ── Purchase-execution: Reserved Instances ──────────────────────────── + # TODO(@cristim): enable only after confirming approval workflow with + # Archera (i.e. Archera requires customer approval before purchases). + { + Sid = "ReservedInstancesPurchase" + Effect = "Allow" + Action = [ + "ec2:PurchaseReservedInstancesOffering", + "ec2:ModifyReservedInstances", + ] + Resource = "*" + }, + # ── Purchase-execution: Savings Plans ──────────────────────────────── + # TODO(@cristim): same confirmation requirement as RI purchase above. + { + Sid = "SavingsPlansPurchase" + Effect = "Allow" + Action = [ + "savingsplans:CreateSavingsPlan", + "savingsplans:DeleteQueuedSavingsPlan", + ] + Resource = "*" + }, + ] + }) + + tags = merge(local.common_tags, { + Integration = "archera" + }) +} + +resource "aws_iam_role_policy_attachment" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + role = aws_iam_role.archera_integration[0].name + policy_arn = aws_iam_policy.archera_integration[0].arn +} diff --git a/terraform/environments/aws/dev.tfvars.example b/terraform/environments/aws/dev.tfvars.example index b7b2e0c6..1288adf7 100644 --- a/terraform/environments/aws/dev.tfvars.example +++ b/terraform/environments/aws/dev.tfvars.example @@ -139,3 +139,14 @@ admin_email = "admin@example.com" # ============================================== additional_env_vars = {} + +# ============================================== +# Archera Integration (default: disabled) +# ============================================== +# Set enable_archera = true ONLY after confirming the scope list in +# archera.tf against Archera's integration docs (see TODO(@cristim) there). +# When enabling, also set archera_aws_account_id to the value provided by +# Archera during onboarding. + +enable_archera = false +# archera_aws_account_id = "123456789012" # replace with Archera's account ID diff --git a/terraform/environments/aws/variables.tf b/terraform/environments/aws/variables.tf index 6415e79f..91c88dc1 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -479,3 +479,37 @@ variable "enable_docker_build" { type = bool default = true } + +# ============================================== +# Archera Integration +# ============================================== + +variable "enable_archera" { + description = <<-EOT + Enable the Archera commitment-optimisation integration. When true, creates + a cross-account IAM role (cudly-archera-integration) that Archera's AWS + account can assume to read cost data and execute RI/SP purchases. + + Defaults to false — non-Archera customers see no drift. + + IMPORTANT: the provisional scope list in archera.tf must be confirmed + against Archera's integration docs before setting this to true in any + environment. See TODO(@cristim) comments in archera.tf. + EOT + type = bool + default = false +} + +variable "archera_aws_account_id" { + description = <<-EOT + AWS account ID of the Archera SaaS platform that will assume the + cudly-archera-integration role. Obtained from Archera during onboarding. + Only evaluated when enable_archera = true. + + Example: "926226587429" + TODO(@cristim): confirm the correct Archera AWS account ID from their + integration docs before enabling. + EOT + type = string + default = "" +} diff --git a/terraform/environments/azure/README.md b/terraform/environments/azure/README.md index 74b49790..e17ba335 100644 --- a/terraform/environments/azure/README.md +++ b/terraform/environments/azure/README.md @@ -33,3 +33,38 @@ terraform apply -var-file=dev.tfvars ``` See AWS README for detailed usage patterns. + +## Archera Integration + +The Archera commitment-optimisation integration is gated behind the +`enable_archera` tfvars flag (default: `false`) so non-Archera customers +see no drift. + +### Enabling Archera + +> **IMPORTANT — scope confirmation required before enabling.** +> The permission list in `archera.tf` is provisional. Confirm the exact +> actions required against [Archera's integration docs](https://archera.ai/docs) +> and validate with `@cristim` before setting `enable_archera = true` in +> any environment. See `TODO(@cristim)` comments in `archera.tf`. + +1. Obtain the Archera service principal **Object ID** (not the Application + Client ID) from Archera during onboarding. +2. Set the following in your `*.tfvars`: + + ```hcl + enable_archera = true + archera_azure_sp_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ``` + +3. Run `terraform plan` to review the custom role and role assignment that + will be created, then apply. + +### What gets created + +When `enable_archera = true`: + +| Resource | Purpose | +| --- | --- | +| `azurerm_role_definition.archera_integration[0]` | Custom RBAC role with read-only cost management + RI purchase actions | +| `azurerm_role_assignment.archera_integration[0]` | Assigns the custom role to Archera's service principal at subscription scope | diff --git a/terraform/environments/azure/archera.tf b/terraform/environments/azure/archera.tf new file mode 100644 index 00000000..5ef03409 --- /dev/null +++ b/terraform/environments/azure/archera.tf @@ -0,0 +1,78 @@ +# ============================================== +# Archera Integration — Azure +# ============================================== +# +# When enable_archera = true, this block creates a custom RBAC role and +# assigns it to Archera's service principal, granting least-privilege +# access to read commitment and cost data and to purchase Reserved +# Instances / Savings Plans on behalf of the customer. +# +# PROVISIONAL SCOPE — must be confirmed against Archera integration docs +# before flipping enable_archera = true in any tfvars. +# TODO(@cristim): confirm Archera scope list against integration docs +# before enabling. Reference: https://archera.ai/docs (integration guide). +# +# Placement rationale (bootstrap vs runtime split): +# Archera is a RUNTIME integration — it reads cost telemetry and submits +# purchases during normal operation. This file lives in the main +# environment (alongside compute.tf / database.tf), NOT in +# ci-cd-permissions/ (which is applied once by a privileged human and +# grants deploy-SA capabilities only). + +locals { + archera_role_name_azure = "CUDly Archera Integration" +} + +# Custom RBAC role for Archera — PROVISIONAL. +# Actions are scoped to read-only cost management and RI/SP purchase +# execution. Do NOT grant broad Contributor or Cost Management Contributor +# predefined roles — they include write permissions on resource deployments. +# +# TODO(@cristim): narrow to the exact action list from Archera's Azure +# onboarding docs before setting enable_archera = true. +resource "azurerm_role_definition" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + name = local.archera_role_name_azure + scope = "/subscriptions/${var.subscription_id}" + description = "Provisional Archera integration role — read cost data, read/purchase RIs and Savings Plans (confirm scope before enabling)" + + permissions { + actions = [ + # ── Read-only: Cost Management ────────────────────────────────────── + # Archera needs to read historical usage and costs to size commitments. + # TODO(@cristim): confirm whether Archera also needs + # Microsoft.Consumption/*/read — add if required by their docs. + "Microsoft.CostManagement/*/read", + "Microsoft.Consumption/*/read", + "Microsoft.Billing/*/read", + + # ── Read-only: Reserved Instances ───────────────────────────────── + # Archera needs to see existing reservations to avoid over-purchasing. + "Microsoft.Capacity/reservations/read", + "Microsoft.Capacity/reservationOrders/read", + + # ── Purchase-execution: Reserved Instances ──────────────────────── + # TODO(@cristim): enable only after confirming approval workflow with + # Archera (i.e. Archera requires customer approval before purchases). + "Microsoft.Capacity/reservationOrders/write", + ] + not_actions = [] + } + + assignable_scopes = [ + "/subscriptions/${var.subscription_id}", + ] +} + +# Assign the custom role to Archera's service principal. +# archera_azure_sp_object_id must be the Object ID of the service principal +# that Archera provides during onboarding (NOT the Application/Client ID). +resource "azurerm_role_assignment" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + scope = "/subscriptions/${var.subscription_id}" + role_definition_id = azurerm_role_definition.archera_integration[0].role_definition_resource_id + principal_id = var.archera_azure_sp_object_id + principal_type = "ServicePrincipal" +} diff --git a/terraform/environments/azure/dev.tfvars.example b/terraform/environments/azure/dev.tfvars.example index 1734e03f..7eb2fffe 100644 --- a/terraform/environments/azure/dev.tfvars.example +++ b/terraform/environments/azure/dev.tfvars.example @@ -128,3 +128,14 @@ admin_email = "admin@example.com" # --vault-name --name admin-password \ # --query value --output tsv # admin_password = "YourSecurePassword123!" + +# ============================================== +# Archera Integration (default: disabled) +# ============================================== +# Set enable_archera = true ONLY after confirming the scope list in +# archera.tf against Archera's integration docs (see TODO(@cristim) there). +# When enabling, also set archera_azure_sp_object_id to the Object ID +# (not Client ID) of the service principal Archera provides. + +enable_archera = false +# archera_azure_sp_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" diff --git a/terraform/environments/azure/variables.tf b/terraform/environments/azure/variables.tf index dc26f857..2ee857b5 100644 --- a/terraform/environments/azure/variables.tf +++ b/terraform/environments/azure/variables.tf @@ -521,3 +521,36 @@ variable "max_account_parallelism" { type = number default = 10 } + +# ============================================== +# Archera Integration +# ============================================== + +variable "enable_archera" { + description = <<-EOT + Enable the Archera commitment-optimisation integration. When true, creates + a custom RBAC role (CUDly Archera Integration) and assigns it to + Archera's service principal at subscription scope. + + Defaults to false — non-Archera customers see no drift. + + IMPORTANT: the provisional scope list in archera.tf must be confirmed + against Archera's integration docs before setting this to true in any + environment. See TODO(@cristim) comments in archera.tf. + EOT + type = bool + default = false +} + +variable "archera_azure_sp_object_id" { + description = <<-EOT + Object ID (NOT the Application/Client ID) of the Archera service principal + in your Azure AD tenant. Provided by Archera during onboarding. + Only evaluated when enable_archera = true. + + TODO(@cristim): obtain the correct Object ID from Archera's Azure + integration docs before enabling. + EOT + type = string + default = "" +} diff --git a/terraform/environments/gcp/README.md b/terraform/environments/gcp/README.md index 99d7822c..2ea8ca16 100644 --- a/terraform/environments/gcp/README.md +++ b/terraform/environments/gcp/README.md @@ -219,3 +219,38 @@ Cloud Run reserves certain environment variable names (`PORT`, `K_SERVICE`, `K_R ### Docker build on Apple Silicon Cross-compilation from ARM (M1/M2/M3) to linux/amd64 is slower than native builds. The build module uses `--platform linux/amd64` and `--load` to build, load into local daemon, then push to GCR. + +## Archera Integration + +The Archera commitment-optimisation integration is gated behind the +`enable_archera` tfvars flag (default: `false`) so non-Archera customers +see no drift. + +### Enabling Archera + +> **IMPORTANT — scope confirmation required before enabling.** +> The permission list in `archera.tf` is provisional. Confirm the exact +> GCP IAM permissions required against [Archera's integration docs](https://archera.ai/docs) +> and validate with `@cristim` before setting `enable_archera = true` in +> any environment. See `TODO(@cristim)` comments in `archera.tf`. + +1. Obtain the Archera **GCP service account email** from Archera during + onboarding (e.g. `archera-integration@archera-prod.iam.gserviceaccount.com`). +2. Set the following in your `*.tfvars`: + + ```hcl + enable_archera = true + archera_gcp_service_account = "archera-integration@archera-prod.iam.gserviceaccount.com" + ``` + +3. Run `terraform plan` to review the custom IAM role and project IAM + member binding that will be created, then apply. + +### What gets created + +When `enable_archera = true`: + +| Resource | Purpose | +| --- | --- | +| `google_project_iam_custom_role.archera_integration[0]` | Custom role with read-only billing/cost + CUD purchase permissions | +| `google_project_iam_member.archera_integration[0]` | Grants the custom role to Archera's service account at project scope | diff --git a/terraform/environments/gcp/archera.tf b/terraform/environments/gcp/archera.tf new file mode 100644 index 00000000..b0f0aeee --- /dev/null +++ b/terraform/environments/gcp/archera.tf @@ -0,0 +1,83 @@ +# ============================================== +# Archera Integration — GCP +# ============================================== +# +# When enable_archera = true, this block creates a custom IAM role and +# grants it to Archera's service account, providing least-privilege access +# to read commitment and cost data and to purchase Committed Use Discounts +# (CUDs) on behalf of the customer. +# +# PROVISIONAL SCOPE — must be confirmed against Archera integration docs +# before flipping enable_archera = true in any tfvars. +# TODO(@cristim): confirm Archera scope list against integration docs +# before enabling. Reference: https://archera.ai/docs (integration guide). +# +# Placement rationale (bootstrap vs runtime split): +# Archera is a RUNTIME integration — it reads cost telemetry and submits +# purchases during normal operation. This file lives in the main +# environment (alongside compute.tf / database.tf), NOT in +# ci-cd-permissions/ (which is applied once by a privileged human and +# grants deploy-SA capabilities only). +# +# Prefer a custom role over predefined roles like roles/billing.admin or +# roles/recommender.commitmentsPlanAdmin — those are too broad. + +locals { + archera_custom_role_id = "cudlyArcheraIntegration" +} + +# Custom IAM role for Archera — PROVISIONAL. +# Permissions are scoped to read-only billing/cost data and CUD purchase +# execution. Do NOT grant roles/billing.admin or roles/editor — they +# include write permissions across all project resources. +# +# TODO(@cristim): narrow to the exact permission list from Archera's GCP +# onboarding docs before setting enable_archera = true. +resource "google_project_iam_custom_role" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + project = var.project_id + role_id = local.archera_custom_role_id + title = "CUDly Archera Integration" + description = "Provisional Archera integration role — read cost data, read/purchase CUDs (confirm scope before enabling)" + stage = "BETA" + + permissions = [ + # ── Read-only: Billing / Cost data ─────────────────────────────────── + # Archera needs to read historical usage and costs to size commitments. + # TODO(@cristim): confirm whether Archera also needs + # billing.accounts.getSpendingInformation — add if required. + "billing.accounts.get", + "billing.accounts.list", + "billing.budgets.get", + "billing.budgets.list", + + # ── Read-only: Recommender / Committed Use Discounts ───────────────── + # Archera reads CUD recommendations and existing commitments. + "recommender.commitmentUtilizationInsights.get", + "recommender.commitmentUtilizationInsights.list", + "recommender.commitmentUtilizationInsights.update", + "recommender.commitments.get", + "recommender.commitments.list", + + # ── Read-only: Resource Manager (project metadata) ─────────────────── + "resourcemanager.projects.get", + + # ── Purchase-execution: Committed Use Discounts ─────────────────────── + # TODO(@cristim): enable only after confirming approval workflow with + # Archera (i.e. Archera requires customer approval before purchases). + "recommender.commitments.create", + ] +} + +# Bind the custom role to Archera's GCP service account. +# archera_gcp_service_account must be the full service account email that +# Archera provides during onboarding, e.g.: +# "archera-integration@archera-prod.iam.gserviceaccount.com" +resource "google_project_iam_member" "archera_integration" { + count = var.enable_archera ? 1 : 0 + + project = var.project_id + role = google_project_iam_custom_role.archera_integration[0].name + member = "serviceAccount:${var.archera_gcp_service_account}" +} diff --git a/terraform/environments/gcp/dev.tfvars.example b/terraform/environments/gcp/dev.tfvars.example index 508a0b89..0503c538 100644 --- a/terraform/environments/gcp/dev.tfvars.example +++ b/terraform/environments/gcp/dev.tfvars.example @@ -105,3 +105,14 @@ admin_email = "admin@example.com" # gcloud secrets versions access latest \ # --secret=-admin-password --project= # admin_password = "YourSecurePassword123!" + +# ============================================== +# Archera Integration (default: disabled) +# ============================================== +# Set enable_archera = true ONLY after confirming the scope list in +# archera.tf against Archera's integration docs (see TODO(@cristim) there). +# When enabling, also set archera_gcp_service_account to the full service +# account email that Archera provides during onboarding. + +enable_archera = false +# archera_gcp_service_account = "archera-integration@archera-prod.iam.gserviceaccount.com" diff --git a/terraform/environments/gcp/variables.tf b/terraform/environments/gcp/variables.tf index a8021df2..177b0aa0 100644 --- a/terraform/environments/gcp/variables.tf +++ b/terraform/environments/gcp/variables.tf @@ -469,3 +469,38 @@ variable "max_account_parallelism" { type = number default = 10 } + +# ============================================== +# Archera Integration +# ============================================== + +variable "enable_archera" { + description = <<-EOT + Enable the Archera commitment-optimisation integration. When true, creates + a custom IAM role (cudlyArcheraIntegration) and grants it to Archera's + GCP service account at project scope. + + Defaults to false — non-Archera customers see no drift. + + IMPORTANT: the provisional scope list in archera.tf must be confirmed + against Archera's integration docs before setting this to true in any + environment. See TODO(@cristim) comments in archera.tf. + EOT + type = bool + default = false +} + +variable "archera_gcp_service_account" { + description = <<-EOT + Full service account email of the Archera GCP service account that will + be granted the cudlyArcheraIntegration custom role at project scope. + Provided by Archera during onboarding. + Only evaluated when enable_archera = true. + + Example: "archera-integration@archera-prod.iam.gserviceaccount.com" + TODO(@cristim): confirm the correct service account email from Archera's + GCP integration docs before enabling. + EOT + type = string + default = "" +} From f3ba36a4ae68da3d27b0cd98b36b1f47eb67805d Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 5 May 2026 22:21:58 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(iac/archera):=20address=20CodeRabbit=20?= =?UTF-8?q?review=20=E2=80=94=20ExternalId,=20purchase=20split,=20validati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS: - enforce ExternalId in assume-role trust policy (confused-deputy protection) - split archera_purchase into a separate IAM policy gated by enable_archera_purchase_actions (default false) so read-only rollouts never accidentally include financial writes - add archera_external_id variable (sensitive, required when enable_archera=true) - add enable_archera_purchase_actions variable (bool, default false) Azure: - add UUID validation on archera_azure_sp_object_id when enable_archera=true - add enable_archera_purchase_actions variable; gate reservationOrders/write behind it via concat() so the initial rollout is read-only GCP: - remove recommender.commitmentUtilizationInsights.update (write action incorrectly placed in read-only block) - add email format validation on archera_gcp_service_account when enable_archera=true - add enable_archera_purchase_actions variable; gate recommender.commitments.create behind it via concat() All environments: dev.tfvars.example updated to include enable_archera_purchase_actions = false terraform fmt + validate pass for all three clouds. --- terraform/environments/aws/archera.tf | 72 +++++++++++++------ terraform/environments/aws/dev.tfvars.example | 6 +- terraform/environments/aws/variables.tf | 34 +++++++++ terraform/environments/azure/archera.tf | 49 +++++++------ .../environments/azure/dev.tfvars.example | 5 +- terraform/environments/azure/variables.tf | 24 +++++++ terraform/environments/gcp/archera.tf | 65 +++++++++-------- terraform/environments/gcp/dev.tfvars.example | 5 +- terraform/environments/gcp/variables.tf | 23 ++++++ 9 files changed, 205 insertions(+), 78 deletions(-) diff --git a/terraform/environments/aws/archera.tf b/terraform/environments/aws/archera.tf index b282bcb8..8884949a 100644 --- a/terraform/environments/aws/archera.tf +++ b/terraform/environments/aws/archera.tf @@ -3,10 +3,11 @@ # ============================================== # # When enable_archera = true, this block creates a cross-account IAM role -# that Archera assumes to read commitment and cost data and to execute -# Reserved Instance / Savings Plan purchases on behalf of the customer. +# that Archera assumes to read commitment and cost data. Purchase-execution +# permissions are separately gated behind enable_archera_purchase_actions +# so telemetry-only rollouts never accidentally include financial writes. # -# PROVISONAL SCOPE — must be confirmed against Archera integration docs +# PROVISIONAL SCOPE — must be confirmed against Archera integration docs # before flipping enable_archera = true in any tfvars. # TODO(@cristim): confirm Archera scope list against integration docs # before enabling. Reference: https://archera.ai/docs (integration guide). @@ -27,7 +28,7 @@ resource "aws_iam_role" "archera_integration" { count = var.enable_archera ? 1 : 0 name = local.archera_role_name - description = "Assumed by Archera SaaS to read cost data and execute RI/SP purchases (provisional — confirm scope before enabling)" + description = "Assumed by Archera SaaS to read cost data and (optionally) execute RI/SP purchases (provisional — confirm scope before enabling)" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -39,11 +40,12 @@ resource "aws_iam_role" "archera_integration" { AWS = "arn:aws:iam::${var.archera_aws_account_id}:root" } Action = "sts:AssumeRole" - # Optional: require ExternalId supplied by Archera during onboarding. - # Uncomment and set archera_external_id once confirmed with Archera. - # Condition = { - # StringEquals = { "sts:ExternalId" = var.archera_external_id } - # } + # ExternalId prevents confused-deputy attacks — always required when + # trusting a third-party SaaS account. Set archera_external_id to + # the value Archera provides during onboarding. + Condition = { + StringEquals = { "sts:ExternalId" = var.archera_external_id } + } }, ] }) @@ -54,18 +56,16 @@ resource "aws_iam_role" "archera_integration" { }) } -# Least-privilege policy for Archera — PROVISIONAL. -# Actions are split into read-only cost/commitment data (safe to enable -# immediately) and purchase-execution (requires explicit confirmation with -# Archera before enabling). +# Read-only policy for Archera — PROVISIONAL. +# Safe to attach at initial rollout (no financial writes). # # TODO(@cristim): narrow to the exact action list from Archera's onboarding # docs before setting enable_archera = true in any environment. -resource "aws_iam_policy" "archera_integration" { +resource "aws_iam_policy" "archera_read" { count = var.enable_archera ? 1 : 0 - name = "cudly-archera-integration" - description = "Provisional Archera integration policy — read commitment + cost data, execute RI/SP purchases" + name = "cudly-archera-read" + description = "Provisional Archera read-only policy — cost Explorer + RI/SP telemetry (no purchase actions)" policy = jsonencode({ Version = "2012-10-17" @@ -130,9 +130,38 @@ resource "aws_iam_policy" "archera_integration" { ] Resource = "*" }, + ] + }) + + tags = merge(local.common_tags, { + Integration = "archera" + }) +} + +resource "aws_iam_role_policy_attachment" "archera_read" { + count = var.enable_archera ? 1 : 0 + + role = aws_iam_role.archera_integration[0].name + policy_arn = aws_iam_policy.archera_read[0].arn +} + +# Purchase-execution policy for Archera — PROVISIONAL. +# Gated behind enable_archera_purchase_actions (default false) so financial +# writes are never included by accident at initial rollout. +# +# TODO(@cristim): enable only after confirming approval workflow with +# Archera (i.e. Archera requires customer approval before purchases) and +# only after enable_archera_purchase_actions is explicitly set to true. +resource "aws_iam_policy" "archera_purchase" { + count = (var.enable_archera && var.enable_archera_purchase_actions) ? 1 : 0 + + name = "cudly-archera-purchase" + description = "Provisional Archera purchase policy — RI/SP write actions (confirm with Archera before enabling)" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ # ── Purchase-execution: Reserved Instances ──────────────────────────── - # TODO(@cristim): enable only after confirming approval workflow with - # Archera (i.e. Archera requires customer approval before purchases). { Sid = "ReservedInstancesPurchase" Effect = "Allow" @@ -143,7 +172,6 @@ resource "aws_iam_policy" "archera_integration" { Resource = "*" }, # ── Purchase-execution: Savings Plans ──────────────────────────────── - # TODO(@cristim): same confirmation requirement as RI purchase above. { Sid = "SavingsPlansPurchase" Effect = "Allow" @@ -161,9 +189,9 @@ resource "aws_iam_policy" "archera_integration" { }) } -resource "aws_iam_role_policy_attachment" "archera_integration" { - count = var.enable_archera ? 1 : 0 +resource "aws_iam_role_policy_attachment" "archera_purchase" { + count = (var.enable_archera && var.enable_archera_purchase_actions) ? 1 : 0 role = aws_iam_role.archera_integration[0].name - policy_arn = aws_iam_policy.archera_integration[0].arn + policy_arn = aws_iam_policy.archera_purchase[0].arn } diff --git a/terraform/environments/aws/dev.tfvars.example b/terraform/environments/aws/dev.tfvars.example index 1288adf7..e21b39ea 100644 --- a/terraform/environments/aws/dev.tfvars.example +++ b/terraform/environments/aws/dev.tfvars.example @@ -148,5 +148,7 @@ additional_env_vars = {} # When enabling, also set archera_aws_account_id to the value provided by # Archera during onboarding. -enable_archera = false -# archera_aws_account_id = "123456789012" # replace with Archera's account ID +enable_archera = false +enable_archera_purchase_actions = false # keep false until purchase approval workflow is confirmed +# archera_aws_account_id = "123456789012" # replace with Archera's AWS account ID +# archera_external_id = "replace-with-archera-extid" # required when enable_archera = true diff --git a/terraform/environments/aws/variables.tf b/terraform/environments/aws/variables.tf index 91c88dc1..cae45554 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -513,3 +513,37 @@ variable "archera_aws_account_id" { type = string default = "" } + +variable "archera_external_id" { + description = <<-EOT + ExternalId value that Archera's AWS principal must supply when assuming + the cudly-archera-integration role. This prevents confused-deputy attacks + and is required for all third-party cross-account integrations. + Provided by Archera during onboarding. + Only evaluated when enable_archera = true. + + TODO(@cristim): obtain the ExternalId from Archera's onboarding docs + before enabling — do not leave this empty in production. + EOT + type = string + default = "" + sensitive = true + + validation { + condition = !var.enable_archera || var.archera_external_id != "" + error_message = "archera_external_id must be set (non-empty) when enable_archera = true." + } +} + +variable "enable_archera_purchase_actions" { + description = <<-EOT + When true (AND enable_archera = true), attaches a second IAM policy + (cudly-archera-purchase) that grants RI/SP purchase-execution permissions. + Keep false until the approval workflow with Archera is confirmed and the + scope list has been validated against Archera's integration docs. + + Defaults to false so initial rollout is read-only. + EOT + type = bool + default = false +} diff --git a/terraform/environments/azure/archera.tf b/terraform/environments/azure/archera.tf index 5ef03409..315f3866 100644 --- a/terraform/environments/azure/archera.tf +++ b/terraform/environments/azure/archera.tf @@ -4,8 +4,9 @@ # # When enable_archera = true, this block creates a custom RBAC role and # assigns it to Archera's service principal, granting least-privilege -# access to read commitment and cost data and to purchase Reserved -# Instances / Savings Plans on behalf of the customer. +# read access to cost and commitment data. Purchase-execution permissions +# are separately gated behind enable_archera_purchase_actions so +# telemetry-only rollouts never accidentally include financial writes. # # PROVISIONAL SCOPE — must be confirmed against Archera integration docs # before flipping enable_archera = true in any tfvars. @@ -24,9 +25,10 @@ locals { } # Custom RBAC role for Archera — PROVISIONAL. -# Actions are scoped to read-only cost management and RI/SP purchase -# execution. Do NOT grant broad Contributor or Cost Management Contributor -# predefined roles — they include write permissions on resource deployments. +# Actions are scoped to read-only cost management and, optionally, +# reservation purchase execution via enable_archera_purchase_actions. +# Do NOT grant broad Contributor or Cost Management Contributor predefined +# roles — they include write permissions on resource deployments. # # TODO(@cristim): narrow to the exact action list from Archera's Azure # onboarding docs before setting enable_archera = true. @@ -35,28 +37,33 @@ resource "azurerm_role_definition" "archera_integration" { name = local.archera_role_name_azure scope = "/subscriptions/${var.subscription_id}" - description = "Provisional Archera integration role — read cost data, read/purchase RIs and Savings Plans (confirm scope before enabling)" + description = "Provisional Archera integration role — read cost data, optionally purchase RIs (confirm scope before enabling)" permissions { - actions = [ - # ── Read-only: Cost Management ────────────────────────────────────── - # Archera needs to read historical usage and costs to size commitments. - # TODO(@cristim): confirm whether Archera also needs - # Microsoft.Consumption/*/read — add if required by their docs. - "Microsoft.CostManagement/*/read", - "Microsoft.Consumption/*/read", - "Microsoft.Billing/*/read", - - # ── Read-only: Reserved Instances ───────────────────────────────── - # Archera needs to see existing reservations to avoid over-purchasing. - "Microsoft.Capacity/reservations/read", - "Microsoft.Capacity/reservationOrders/read", + actions = concat( + [ + # ── Read-only: Cost Management ────────────────────────────────────── + # Archera needs to read historical usage and costs to size commitments. + # TODO(@cristim): confirm whether Archera also needs + # Microsoft.Consumption/*/read — add if required by their docs. + "Microsoft.CostManagement/*/read", + "Microsoft.Consumption/*/read", + "Microsoft.Billing/*/read", + # ── Read-only: Reserved Instances ───────────────────────────────── + # Archera needs to see existing reservations to avoid over-purchasing. + "Microsoft.Capacity/reservations/read", + "Microsoft.Capacity/reservationOrders/read", + ], # ── Purchase-execution: Reserved Instances ──────────────────────── + # Gated behind enable_archera_purchase_actions so financial writes + # are never included by accident at initial rollout. # TODO(@cristim): enable only after confirming approval workflow with # Archera (i.e. Archera requires customer approval before purchases). - "Microsoft.Capacity/reservationOrders/write", - ] + var.enable_archera_purchase_actions ? [ + "Microsoft.Capacity/reservationOrders/write", + ] : [] + ) not_actions = [] } diff --git a/terraform/environments/azure/dev.tfvars.example b/terraform/environments/azure/dev.tfvars.example index 7eb2fffe..d8328c4f 100644 --- a/terraform/environments/azure/dev.tfvars.example +++ b/terraform/environments/azure/dev.tfvars.example @@ -137,5 +137,6 @@ admin_email = "admin@example.com" # When enabling, also set archera_azure_sp_object_id to the Object ID # (not Client ID) of the service principal Archera provides. -enable_archera = false -# archera_azure_sp_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +enable_archera = false +enable_archera_purchase_actions = false # keep false until purchase approval workflow is confirmed +# archera_azure_sp_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # required when enable_archera = true diff --git a/terraform/environments/azure/variables.tf b/terraform/environments/azure/variables.tf index 2ee857b5..7bfa0d01 100644 --- a/terraform/environments/azure/variables.tf +++ b/terraform/environments/azure/variables.tf @@ -553,4 +553,28 @@ variable "archera_azure_sp_object_id" { EOT type = string default = "" + + validation { + condition = ( + !var.enable_archera || + (var.archera_azure_sp_object_id != "" && + can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + var.archera_azure_sp_object_id)) + )) + error_message = "archera_azure_sp_object_id must be a valid UUID when enable_archera = true." + } +} + +variable "enable_archera_purchase_actions" { + description = <<-EOT + When true (AND enable_archera = true), adds the + "Microsoft.Capacity/reservationOrders/write" action to the Archera custom + RBAC role, enabling reservation purchase execution. + Keep false until the approval workflow with Archera is confirmed and the + scope list has been validated against Archera's integration docs. + + Defaults to false so initial rollout is read-only. + EOT + type = bool + default = false } diff --git a/terraform/environments/gcp/archera.tf b/terraform/environments/gcp/archera.tf index b0f0aeee..18f09529 100644 --- a/terraform/environments/gcp/archera.tf +++ b/terraform/environments/gcp/archera.tf @@ -3,9 +3,10 @@ # ============================================== # # When enable_archera = true, this block creates a custom IAM role and -# grants it to Archera's service account, providing least-privilege access -# to read commitment and cost data and to purchase Committed Use Discounts -# (CUDs) on behalf of the customer. +# grants it to Archera's service account, providing least-privilege read +# access to cost and commitment data. Purchase-execution permissions are +# separately gated behind enable_archera_purchase_actions so telemetry-only +# rollouts never accidentally include financial writes. # # PROVISIONAL SCOPE — must be confirmed against Archera integration docs # before flipping enable_archera = true in any tfvars. @@ -27,9 +28,10 @@ locals { } # Custom IAM role for Archera — PROVISIONAL. -# Permissions are scoped to read-only billing/cost data and CUD purchase -# execution. Do NOT grant roles/billing.admin or roles/editor — they -# include write permissions across all project resources. +# Permissions are scoped to read-only billing/cost data and, optionally, +# CUD purchase execution (gated by enable_archera_purchase_actions). +# Do NOT grant roles/billing.admin or roles/editor — they include write +# permissions across all project resources. # # TODO(@cristim): narrow to the exact permission list from Archera's GCP # onboarding docs before setting enable_archera = true. @@ -39,35 +41,40 @@ resource "google_project_iam_custom_role" "archera_integration" { project = var.project_id role_id = local.archera_custom_role_id title = "CUDly Archera Integration" - description = "Provisional Archera integration role — read cost data, read/purchase CUDs (confirm scope before enabling)" + description = "Provisional Archera integration role — read cost data, optionally purchase CUDs (confirm scope before enabling)" stage = "BETA" - permissions = [ - # ── Read-only: Billing / Cost data ─────────────────────────────────── - # Archera needs to read historical usage and costs to size commitments. - # TODO(@cristim): confirm whether Archera also needs - # billing.accounts.getSpendingInformation — add if required. - "billing.accounts.get", - "billing.accounts.list", - "billing.budgets.get", - "billing.budgets.list", + permissions = concat( + [ + # ── Read-only: Billing / Cost data ───────────────────────────────── + # Archera needs to read historical usage and costs to size commitments. + # TODO(@cristim): confirm whether Archera also needs + # billing.accounts.getSpendingInformation — add if required. + "billing.accounts.get", + "billing.accounts.list", + "billing.budgets.get", + "billing.budgets.list", - # ── Read-only: Recommender / Committed Use Discounts ───────────────── - # Archera reads CUD recommendations and existing commitments. - "recommender.commitmentUtilizationInsights.get", - "recommender.commitmentUtilizationInsights.list", - "recommender.commitmentUtilizationInsights.update", - "recommender.commitments.get", - "recommender.commitments.list", + # ── Read-only: Recommender / Committed Use Discounts ─────────────── + # Archera reads CUD recommendations and existing commitments. + # Note: .update is a write action and intentionally excluded here. + "recommender.commitmentUtilizationInsights.get", + "recommender.commitmentUtilizationInsights.list", + "recommender.commitments.get", + "recommender.commitments.list", - # ── Read-only: Resource Manager (project metadata) ─────────────────── - "resourcemanager.projects.get", - - # ── Purchase-execution: Committed Use Discounts ─────────────────────── + # ── Read-only: Resource Manager (project metadata) ───────────────── + "resourcemanager.projects.get", + ], + # ── Purchase-execution: Committed Use Discounts ───────────────────── + # Gated behind enable_archera_purchase_actions so financial writes are + # never included by accident at initial rollout. # TODO(@cristim): enable only after confirming approval workflow with # Archera (i.e. Archera requires customer approval before purchases). - "recommender.commitments.create", - ] + var.enable_archera_purchase_actions ? [ + "recommender.commitments.create", + ] : [] + ) } # Bind the custom role to Archera's GCP service account. diff --git a/terraform/environments/gcp/dev.tfvars.example b/terraform/environments/gcp/dev.tfvars.example index 0503c538..20d38967 100644 --- a/terraform/environments/gcp/dev.tfvars.example +++ b/terraform/environments/gcp/dev.tfvars.example @@ -114,5 +114,6 @@ admin_email = "admin@example.com" # When enabling, also set archera_gcp_service_account to the full service # account email that Archera provides during onboarding. -enable_archera = false -# archera_gcp_service_account = "archera-integration@archera-prod.iam.gserviceaccount.com" +enable_archera = false +enable_archera_purchase_actions = false # keep false until purchase approval workflow is confirmed +# archera_gcp_service_account = "archera-integration@archera-prod.iam.gserviceaccount.com" # required when enable_archera = true diff --git a/terraform/environments/gcp/variables.tf b/terraform/environments/gcp/variables.tf index 177b0aa0..7648c7bf 100644 --- a/terraform/environments/gcp/variables.tf +++ b/terraform/environments/gcp/variables.tf @@ -503,4 +503,27 @@ variable "archera_gcp_service_account" { EOT type = string default = "" + + validation { + condition = ( + !var.enable_archera || + (var.archera_gcp_service_account != "" && + can(regex("^[^@]+@[^.]+\\.iam\\.gserviceaccount\\.com$", var.archera_gcp_service_account))) + ) + error_message = "archera_gcp_service_account must be a valid service account email (user@project.iam.gserviceaccount.com) when enable_archera = true." + } +} + +variable "enable_archera_purchase_actions" { + description = <<-EOT + When true (AND enable_archera = true), adds "recommender.commitments.create" + to the cudlyArcheraIntegration custom IAM role, enabling CUD purchase + execution. + Keep false until the approval workflow with Archera is confirmed and the + scope list has been validated against Archera's integration docs. + + Defaults to false so initial rollout is read-only. + EOT + type = bool + default = false } From f8daec665646f0aff29d35affd31fbdadb486f18 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 5 May 2026 23:33:21 +0200 Subject: [PATCH 3/3] fix(iac/archera): docs accuracy + variable validation per CodeRabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address 3 actionable findings from CodeRabbit's review of f3ba36a: 1. AWS README — sync with read/purchase split: add `archera_external_id` to enablement example, mention optional `enable_archera_purchase_actions` flag, replace stale `archera_integration` policy/attachment table entries with `archera_read` + `archera_purchase`. 2. AWS variables.tf — add validation block to `archera_aws_account_id` that fails fast if the value isn't a 12-digit account ID when `enable_archera = true` (avoids building an invalid trust principal ARN at apply time). 3. Azure README — clarify that reservation purchase write permission is gated on `enable_archera_purchase_actions = true`, not `enable_archera` alone; adjust the role-definition row to match. `terraform validate` passes for AWS and Azure with the new validation block. --- terraform/environments/aws/README.md | 11 ++++++++--- terraform/environments/aws/variables.tf | 8 ++++++++ terraform/environments/azure/README.md | 4 +++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/terraform/environments/aws/README.md b/terraform/environments/aws/README.md index 75389717..3096e90b 100644 --- a/terraform/environments/aws/README.md +++ b/terraform/environments/aws/README.md @@ -59,7 +59,10 @@ see no drift. ```hcl enable_archera = true - archera_aws_account_id = "123456789012" # Archera's account ID + archera_aws_account_id = "123456789012" # Archera's account ID + archera_external_id = "replace-with-archera-extid" + # Optional: only after the purchase approval workflow is confirmed + # enable_archera_purchase_actions = true ``` 3. Run `terraform plan` to review the cross-account IAM role and policy @@ -72,5 +75,7 @@ When `enable_archera = true`: | Resource | Purpose | | --- | --- | | `aws_iam_role.archera_integration[0]` | Cross-account role trusted by Archera's AWS account | -| `aws_iam_policy.archera_integration[0]` | Provisional least-privilege policy (read cost data + purchase RIs/SPs) | -| `aws_iam_role_policy_attachment.archera_integration[0]` | Attaches the policy to the role | +| `aws_iam_policy.archera_read[0]` | Provisional read-only policy for cost / commitment telemetry | +| `aws_iam_role_policy_attachment.archera_read[0]` | Attaches the read-only policy to the role | +| `aws_iam_policy.archera_purchase[0]` | Optional RI/SP purchase policy, created only when `enable_archera_purchase_actions = true` | +| `aws_iam_role_policy_attachment.archera_purchase[0]` | Attaches the optional purchase policy to the role | diff --git a/terraform/environments/aws/variables.tf b/terraform/environments/aws/variables.tf index cae45554..f3b0ceec 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -512,6 +512,14 @@ variable "archera_aws_account_id" { EOT type = string default = "" + + validation { + condition = ( + !var.enable_archera || + can(regex("^\\d{12}$", trimspace(var.archera_aws_account_id))) + ) + error_message = "archera_aws_account_id must be a 12-digit AWS account ID when enable_archera = true." + } } variable "archera_external_id" { diff --git a/terraform/environments/azure/README.md b/terraform/environments/azure/README.md index e17ba335..b1640939 100644 --- a/terraform/environments/azure/README.md +++ b/terraform/environments/azure/README.md @@ -55,6 +55,8 @@ see no drift. ```hcl enable_archera = true archera_azure_sp_object_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + # Optional: only after the purchase approval workflow is confirmed + # enable_archera_purchase_actions = true ``` 3. Run `terraform plan` to review the custom role and role assignment that @@ -66,5 +68,5 @@ When `enable_archera = true`: | Resource | Purpose | | --- | --- | -| `azurerm_role_definition.archera_integration[0]` | Custom RBAC role with read-only cost management + RI purchase actions | +| `azurerm_role_definition.archera_integration[0]` | Custom RBAC role with read-only cost access by default; adds RI purchase writes only when `enable_archera_purchase_actions = true` | | `azurerm_role_assignment.archera_integration[0]` | Assigns the custom role to Archera's service principal at subscription scope |