IaC GCP

Terraform Module: GCP Assured Workloads — Compliance-Regime Folders with Sovereign Controls as Code

Quick take — A reusable hashicorp/google ~> 5.0 Terraform module for GCP Assured Workloads that provisions a compliance-regime folder, CMEK key settings, sovereign controls, and partner regimes with validated, audit-ready inputs. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "google" {
  project = "my-project"
  region  = "us-central1"
}

module "assured_workloads" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-assured-workloads?ref=v1.0.0"

  organization      = "..."  # Numeric org ID that owns the boundary. Immutable.
  location          = "..."  # Workload location (`us`, `europe-west3`, …). Pins resid…
  display_name      = "..."  # Human-readable workload name (4–256 chars).
  compliance_regime = "..."  # Regime enforced (`FEDRAMP_MODERATE`, `IL4`, `EU_REGIONS…
  billing_account   = "..."  # `billingAccounts/XXXXXX-XXXXXX-XXXXXX`, allow-listed fo…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

Assured Workloads is GCP’s control plane for running regulated workloads inside a compliance-regime boundary. When you create a google_assured_workloads_workload, GCP doesn’t just tag a folder — it provisions a new folder (or project) under your organization and binds it to a named regime such as FEDRAMP_MODERATE, IL4, EU_REGIONS_AND_SUPPORT, HIPAA, or ITAR. From that moment GCP enforces a bundle of org policies, data-residency constraints, personnel-access controls, and CMEK requirements that match the regime, and it continuously evaluates the boundary for violations you can surface in the Assured Workloads dashboard.

The resource looks deceptively small but is dense with one-way decisions. compliance_regime, organization, billing_account, and location are immutable — you cannot move a workload to a different regime or org after creation; you delete and recreate. The kms_settings block (regimes that mandate customer-managed encryption keys) controls the rotation cadence of the Assured Workloads–managed key ring, and resource_settings lets you pre-seed the resource IDs and display names of the folder/project/keyring that GCP will create so they land with predictable, policy-friendly names instead of generated ones. Sovereign regimes (EU/JP/regional support) add enable_sovereign_controls and a partner (e.g. T_SYSTEMS, SIA_MINSAIT, PSN) that hands operational control to a vetted local partner.

Wrapping all of this in a module matters because the failure modes are organizational, not just technical: pick the wrong regime and an auditor rejects the boundary; forget provisioned_resources_parent and the workload folder lands at the org root instead of under your regulated/ folder; hand-set a billing account that isn’t allow-listed for the regime and apply fails halfway through provisioning. This module turns the immutable, validated, regime-specific knobs into one reviewable interface so platform teams stamp out consistent compliant landing zones instead of clicking through the console per region.

When to use it

Skip it for ordinary, unregulated workloads: Assured Workloads adds real constraints (and a subset of services/regions) and is only worth the friction when an auditor or regulation actually requires the boundary.

Module structure

terraform-module-gcp-assured-workloads/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # the workload, KMS settings, resource_settings, sovereign controls
├── variables.tf     # var-driven inputs with regime/location validation
└── outputs.tf       # workload name/id + provisioned resources, compliance status

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

main.tf

locals {
  # Regimes that mandate a customer-managed key ring. For these we require a
  # kms_settings block; for others (e.g. EU_REGIONS_AND_SUPPORT) it is optional.
  cmek_required_regimes = [
    "FEDRAMP_MODERATE",
    "FEDRAMP_HIGH",
    "IL4",
    "CJIS",
    "ITAR",
    "HIPAA",
    "HITRUST",
    "PCI_DSS",
  ]

  needs_kms = contains(local.cmek_required_regimes, var.compliance_regime)

  # Default the workload folder to sit under provisioned_resources_parent so it
  # never lands at the org root. If a parent is supplied we pass it through.
  resources_parent = var.provisioned_resources_parent
}

resource "google_assured_workloads_workload" "this" {
  # Immutable after creation — changing any of these forces replacement.
  organization      = var.organization
  location          = var.location
  display_name      = var.display_name
  compliance_regime = var.compliance_regime
  billing_account   = var.billing_account

  # Where the generated folder/project is created. Omit and it lands at org root.
  provisioned_resources_parent = local.resources_parent

  # Sovereign regimes (EU/JP/regional support) gate access through a partner.
  enable_sovereign_controls = var.enable_sovereign_controls
  partner                   = var.partner

  # Surface boundary violations as notifications for the security team.
  violation_notifications_enabled = var.violation_notifications_enabled

  labels = var.labels

  # CMEK rotation for regimes that require a customer-managed key ring. The
  # Assured Workloads service creates and rotates the key on this cadence.
  dynamic "kms_settings" {
    for_each = local.needs_kms && var.kms_next_rotation_time != null ? [1] : []
    content {
      next_rotation_time = var.kms_next_rotation_time
      rotation_period    = var.kms_rotation_period
    }
  }

  # Pre-seed the IDs/names/types of the resources Assured Workloads provisions
  # (folder, project, key ring) so they land with predictable, policy-friendly
  # names instead of GCP-generated ones.
  dynamic "resource_settings" {
    for_each = var.resource_settings
    content {
      resource_id   = try(resource_settings.value.resource_id, null)
      resource_type = resource_settings.value.resource_type
      display_name  = try(resource_settings.value.display_name, null)
    }
  }

  lifecycle {
    # Display name is mutable; the regime/org/billing/location are not. Guard the
    # billing account against accidental in-place edits that would force replace.
    ignore_changes = []

    precondition {
      condition     = !local.needs_kms || var.kms_next_rotation_time != null
      error_message = "compliance_regime ${var.compliance_regime} requires kms_settings: set kms_next_rotation_time (RFC3339) and kms_rotation_period."
    }

    precondition {
      condition     = !var.enable_sovereign_controls || var.partner != null
      error_message = "enable_sovereign_controls = true requires a partner (e.g. T_SYSTEMS, SIA_MINSAIT, PSN)."
    }
  }
}

variables.tf

variable "organization" {
  description = "Numeric GCP organization ID that owns the Assured Workloads boundary (e.g. \"123456789012\"). Immutable."
  type        = string

  validation {
    condition     = can(regex("^[0-9]+$", var.organization))
    error_message = "organization must be the numeric organization ID, digits only."
  }
}

variable "location" {
  description = "Assured Workloads location for the boundary (e.g. \"us\", \"europe-west3\", \"asia-northeast1\"). Pins data residency and Google support access. Immutable."
  type        = string
}

variable "display_name" {
  description = "Human-readable name of the workload (4–256 chars). Shown in the Assured Workloads dashboard and used as the basis for the generated folder name."
  type        = string

  validation {
    condition     = length(var.display_name) >= 4 && length(var.display_name) <= 256
    error_message = "display_name must be between 4 and 256 characters."
  }
}

variable "compliance_regime" {
  description = "Compliance regime to enforce on the boundary. Immutable — changing it replaces the workload and its provisioned resources."
  type        = string

  validation {
    condition = contains([
      "FEDRAMP_MODERATE", "FEDRAMP_HIGH", "IL4", "CJIS", "ITAR",
      "HIPAA", "HITRUST", "PCI_DSS", "US_REGIONAL_ACCESS",
      "EU_REGIONS_AND_SUPPORT", "JP_REGIONS_AND_SUPPORT",
      "CA_REGIONS_AND_SUPPORT", "CA_PROTECTED_B", "IL5",
      "ASSURED_WORKLOADS_FOR_PARTNERS", "REGIONAL_CONTROLS",
    ], var.compliance_regime)
    error_message = "compliance_regime is not a recognised Assured Workloads regime for hashicorp/google ~> 5.0."
  }
}

variable "billing_account" {
  description = "Billing account for the workload in the form \"billingAccounts/000000-AAAAAA-BBBBBB\". Must be allow-listed for the chosen regime. Immutable."
  type        = string

  validation {
    condition     = can(regex("^billingAccounts/[A-Za-z0-9-]+$", var.billing_account))
    error_message = "billing_account must be in the form billingAccounts/XXXXXX-XXXXXX-XXXXXX."
  }
}

variable "provisioned_resources_parent" {
  description = "Parent under which Assured Workloads creates the generated folder, e.g. \"folders/123456789\". Omit to create at the organization root (not recommended)."
  type        = string
  default     = null

  validation {
    condition     = var.provisioned_resources_parent == null || can(regex("^folders/[0-9]+$", var.provisioned_resources_parent))
    error_message = "provisioned_resources_parent must be null or of the form folders/<numeric-id>."
  }
}

variable "enable_sovereign_controls" {
  description = "Enable sovereign controls (EU/JP/regional-support regimes). When true, a partner must be set and operational access is gated through that partner."
  type        = bool
  default     = false
}

variable "partner" {
  description = "Sovereign-controls partner that operates the boundary. One of T_SYSTEMS, SIA_MINSAIT, PSN, or null for non-sovereign regimes."
  type        = string
  default     = null

  validation {
    condition     = var.partner == null || contains(["T_SYSTEMS", "SIA_MINSAIT", "PSN"], var.partner)
    error_message = "partner must be one of T_SYSTEMS, SIA_MINSAIT, PSN, or null."
  }
}

variable "violation_notifications_enabled" {
  description = "Whether to emit notifications when the boundary drifts out of compliance. Strongly recommended for production regulated workloads."
  type        = bool
  default     = true
}

variable "kms_next_rotation_time" {
  description = "RFC3339 timestamp of the first/next rotation of the Assured Workloads CMEK key ring (e.g. \"2026-09-01T00:00:00Z\"). Required for CMEK-mandating regimes."
  type        = string
  default     = null

  validation {
    condition     = var.kms_next_rotation_time == null || can(regex("^[0-9]{4}-[0-9]{2}-[0-9]{2}T", var.kms_next_rotation_time))
    error_message = "kms_next_rotation_time must be an RFC3339 timestamp such as 2026-09-01T00:00:00Z."
  }
}

variable "kms_rotation_period" {
  description = "Rotation period of the CMEK key ring as a duration in seconds with an 's' suffix (e.g. \"7776000s\" for 90 days). Used only when kms_settings is active."
  type        = string
  default     = "7776000s"

  validation {
    condition     = can(regex("^[0-9]+s$", var.kms_rotation_period))
    error_message = "kms_rotation_period must be a seconds duration like 7776000s."
  }
}

variable "resource_settings" {
  description = <<-EOT
    Pre-seeded settings for the resources Assured Workloads provisions. List of
    objects, each with:
      resource_type - one of CONSUMER_FOLDER, CONSUMER_PROJECT,
                      ENCRYPTION_KEYS_PROJECT, KEYRING
      resource_id   - desired ID for that resource (optional; GCP generates one
                      if omitted). Folder/project IDs must satisfy GCP naming.
      display_name  - desired display name (optional)
  EOT
  type = list(object({
    resource_type = string
    resource_id   = optional(string)
    display_name  = optional(string)
  }))
  default = []

  validation {
    condition = alltrue([
      for r in var.resource_settings :
      contains(["CONSUMER_FOLDER", "CONSUMER_PROJECT", "ENCRYPTION_KEYS_PROJECT", "KEYRING"], r.resource_type)
    ])
    error_message = "Each resource_settings.resource_type must be CONSUMER_FOLDER, CONSUMER_PROJECT, ENCRYPTION_KEYS_PROJECT, or KEYRING."
  }
}

variable "labels" {
  description = "Labels applied to the workload for cost allocation and governance (e.g. data_classification, owner, regime)."
  type        = map(string)
  default     = {}
}

outputs.tf

output "workload_name" {
  description = "Fully-qualified resource name of the workload (organizations/<org>/locations/<location>/workloads/<id>). Use for IAM, dashboard links, and gcloud lookups."
  value       = google_assured_workloads_workload.this.name
}

output "workload_id" {
  description = "Terraform resource ID of the Assured Workloads workload."
  value       = google_assured_workloads_workload.this.id
}

output "compliance_regime" {
  description = "Compliance regime enforced on the boundary (echoes input for downstream assertions and policy wiring)."
  value       = google_assured_workloads_workload.this.compliance_regime
}

output "provisioned_resources" {
  description = "List of resources Assured Workloads created for the boundary, each as { resource_id, resource_type }. The CONSUMER_FOLDER entry is where you deploy regulated workloads."
  value       = google_assured_workloads_workload.this.resources
}

output "consumer_folder_id" {
  description = "Resource ID of the generated CONSUMER_FOLDER (e.g. \"folders/987654321\"), or null if none was provisioned. Use this as the parent for downstream projects."
  value = try(
    [for r in google_assured_workloads_workload.this.resources : "folders/${r.resource_id}" if r.resource_type == "CONSUMER_FOLDER"][0],
    null
  )
}

output "compliance_status" {
  description = "Current compliance status of the boundary, including counts of active and acknowledged violations."
  value       = google_assured_workloads_workload.this.compliance_status
}

output "kaj_enrollment_state" {
  description = "Key Access Justifications enrollment state for the workload (relevant for sovereign / KAJ-enabled regimes)."
  value       = google_assured_workloads_workload.this.kaj_enrollment_state
}

How to use it

# Reusable FedRAMP Moderate landing zone under our regulated/ folder, with a
# 90-day CMEK rotation and predictable folder/key-ring names.
module "assured_workloads" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-assured-workloads?ref=v1.0.0"

  organization      = "123456789012"
  location          = "us"
  display_name      = "kloudvin-fedramp-moderate-prod"
  compliance_regime = "FEDRAMP_MODERATE"
  billing_account   = "billingAccounts/000000-AAAAAA-BBBBBB"

  # Land the generated folder under our regulated/ parent, never at org root.
  provisioned_resources_parent = "folders/210987654321"

  # CMEK is mandatory for FedRAMP — rotate the Assured Workloads key every 90 days.
  kms_next_rotation_time = "2026-09-01T00:00:00Z"
  kms_rotation_period    = "7776000s"

  # Give the provisioned folder and key ring deterministic names.
  resource_settings = [
    {
      resource_type = "CONSUMER_FOLDER"
      display_name  = "fedramp-moderate-prod"
    },
    {
      resource_type = "KEYRING"
      resource_id   = "fedramp-moderate-prod-kr"
    },
  ]

  violation_notifications_enabled = true

  labels = {
    data_classification = "controlled"
    regime              = "fedramp-moderate"
    owner               = "platform-security"
  }
}

# Downstream: deploy a regulated project INTO the generated compliance folder,
# using the module's consumer_folder_id output as the parent so the project
# inherits the boundary's org policies and data-residency controls.
resource "google_project" "regulated_app" {
  name            = "fedramp-app-prod"
  project_id      = "kloudvin-fedramp-app-prod"
  folder_id       = module.assured_workloads.consumer_folder_id
  billing_account = "000000-AAAAAA-BBBBBB"

  labels = {
    regime = "fedramp-moderate"
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "gcs"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...gcs state bucket/container + key per path...
  }
}

2. Module configlive/prod/assured_workloads/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-assured-workloads?ref=v1.0.0"
}

inputs = {
  organization = "..."
  location = "..."
  display_name = "..."
  compliance_regime = "..."
  billing_account = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/assured_workloads && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
organization string Yes Numeric org ID that owns the boundary. Immutable.
location string Yes Workload location (us, europe-west3, …). Pins residency. Immutable.
display_name string Yes Human-readable workload name (4–256 chars).
compliance_regime string Yes Regime enforced (FEDRAMP_MODERATE, IL4, EU_REGIONS_AND_SUPPORT, …). Immutable.
billing_account string Yes billingAccounts/XXXXXX-XXXXXX-XXXXXX, allow-listed for the regime. Immutable.
provisioned_resources_parent string null No Parent folders/<id> for generated resources; omit for org root.
enable_sovereign_controls bool false No Enable sovereign controls (requires partner).
partner string null No T_SYSTEMS / SIA_MINSAIT / PSN for sovereign regimes.
violation_notifications_enabled bool true No Emit notifications on compliance drift.
kms_next_rotation_time string null No RFC3339 first/next CMEK rotation; required for CMEK regimes.
kms_rotation_period string "7776000s" No CMEK rotation period as a seconds duration.
resource_settings list(object) [] No Pre-seeded IDs/names/types for provisioned folder/project/key ring.
labels map(string) {} No Governance/cost labels on the workload.

Outputs

Name Description
workload_name Fully-qualified workload resource name (organizations/<org>/locations/<loc>/workloads/<id>).
workload_id Terraform resource ID of the workload.
compliance_regime Regime enforced on the boundary.
provisioned_resources List of { resource_id, resource_type } GCP created for the boundary.
consumer_folder_id folders/<id> of the generated consumer folder (parent for regulated projects).
compliance_status Active/acknowledged violation counts for the boundary.
kaj_enrollment_state Key Access Justifications enrollment state of the workload.

Enterprise scenario

A US healthcare ISV must host its PHI-processing platform under FedRAMP Moderate to win a federal agency contract. The platform team consumes this module once per environment (dev/stage/prod), each call landing a dedicated FedRAMP-regime folder under their regulated/ parent with a 90-day-rotating CMEK key ring and deterministic names that match the corporate naming standard. They wire consumer_folder_id straight into their existing project-factory module, so every regulated service automatically inherits the boundary’s org policies, in-region data residency, and personnel-access controls — and because compliance_regime, organization, and billing_account are validated and immutable in code, an auditor can read the Terraform state and the module call to confirm, in seconds, that production is pinned to the certified regime.

Best practices

TerraformGCPAssured WorkloadsModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading