diff --git a/terraform/environments/aws/README.md b/terraform/environments/aws/README.md new file mode 100644 index 00000000..3096e90b --- /dev/null +++ b/terraform/environments/aws/README.md @@ -0,0 +1,81 @@ +# 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 + 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 + 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_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/archera.tf b/terraform/environments/aws/archera.tf new file mode 100644 index 00000000..8884949a --- /dev/null +++ b/terraform/environments/aws/archera.tf @@ -0,0 +1,197 @@ +# ============================================== +# Archera Integration — AWS +# ============================================== +# +# When enable_archera = true, this block creates a cross-account IAM role +# 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. +# +# 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, 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 (optionally) 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" + # 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 } + } + }, + ] + }) + + tags = merge(local.common_tags, { + Integration = "archera" + Purpose = "commitment-optimisation" + }) +} + +# 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_read" { + count = var.enable_archera ? 1 : 0 + + name = "cudly-archera-read" + description = "Provisional Archera read-only policy — cost Explorer + RI/SP telemetry (no purchase actions)" + + 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 = "*" + }, + ] + }) + + 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 ──────────────────────────── + { + Sid = "ReservedInstancesPurchase" + Effect = "Allow" + Action = [ + "ec2:PurchaseReservedInstancesOffering", + "ec2:ModifyReservedInstances", + ] + Resource = "*" + }, + # ── Purchase-execution: Savings Plans ──────────────────────────────── + { + Sid = "SavingsPlansPurchase" + Effect = "Allow" + Action = [ + "savingsplans:CreateSavingsPlan", + "savingsplans:DeleteQueuedSavingsPlan", + ] + Resource = "*" + }, + ] + }) + + tags = merge(local.common_tags, { + Integration = "archera" + }) +} + +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_purchase[0].arn +} diff --git a/terraform/environments/aws/dev.tfvars.example b/terraform/environments/aws/dev.tfvars.example index b7b2e0c6..e21b39ea 100644 --- a/terraform/environments/aws/dev.tfvars.example +++ b/terraform/environments/aws/dev.tfvars.example @@ -139,3 +139,16 @@ 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 +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 6415e79f..f3b0ceec 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -479,3 +479,79 @@ 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 = "" + + 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" { + 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/README.md b/terraform/environments/azure/README.md index 74b49790..b1640939 100644 --- a/terraform/environments/azure/README.md +++ b/terraform/environments/azure/README.md @@ -33,3 +33,40 @@ 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" + # 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 + 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 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 | diff --git a/terraform/environments/azure/archera.tf b/terraform/environments/azure/archera.tf new file mode 100644 index 00000000..315f3866 --- /dev/null +++ b/terraform/environments/azure/archera.tf @@ -0,0 +1,85 @@ +# ============================================== +# 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 +# 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. +# 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, 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. +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, optionally purchase RIs (confirm scope before enabling)" + + permissions { + 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). + var.enable_archera_purchase_actions ? [ + "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..d8328c4f 100644 --- a/terraform/environments/azure/dev.tfvars.example +++ b/terraform/environments/azure/dev.tfvars.example @@ -128,3 +128,15 @@ 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 +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 dc26f857..7bfa0d01 100644 --- a/terraform/environments/azure/variables.tf +++ b/terraform/environments/azure/variables.tf @@ -521,3 +521,60 @@ 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 = "" + + 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/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..18f09529 --- /dev/null +++ b/terraform/environments/gcp/archera.tf @@ -0,0 +1,90 @@ +# ============================================== +# 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 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. +# 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, 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. +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, optionally purchase CUDs (confirm scope before enabling)" + stage = "BETA" + + 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. + # 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 ───────────────────── + # 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). + var.enable_archera_purchase_actions ? [ + "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..20d38967 100644 --- a/terraform/environments/gcp/dev.tfvars.example +++ b/terraform/environments/gcp/dev.tfvars.example @@ -105,3 +105,15 @@ 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 +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 a8021df2..7648c7bf 100644 --- a/terraform/environments/gcp/variables.tf +++ b/terraform/environments/gcp/variables.tf @@ -469,3 +469,61 @@ 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 = "" + + 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 +}