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
- You must run workloads under a named compliance framework — FedRAMP Moderate/High, CJIS, IL4/IL5, HITRUST/HIPAA, ITAR, or PCI-DSS — and need the boundary, org policies, and data-residency controls applied automatically rather than assembled by hand.
- You operate in the EU, UK, Japan, or other sovereign regions and need
EU_REGIONS_AND_SUPPORT/JP_REGIONS_AND_SUPPORTwith sovereign controls and a designated partner (T-Systems, SIA/Minsait, PSN, etc.) owning operational access. - You run a landing-zone / factory pattern where each regulated tenant gets its own compliance-regime folder, CMEK key ring, and predictable resource naming, all version-pinned and code-reviewed.
- You need data-residency guarantees — pinning the workload
locationtoeurope-west3orasia-northeast1so provisioned resources and Google support access stay in-region. - You want violation visibility as code — enabling
violation_notifications_enabledso the security team is alerted the moment the boundary drifts out of compliance.
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 config — live/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 config — live/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
- Treat regime, org, billing, and location as one-way doors. All four are immutable; the module validates their formats so a typo fails at
planrather than after GCP has half-provisioned a folder. Decide the regime with your compliance team before the firstapply, because changing it later destroys and recreates the boundary and everything beneath it. - Always set
provisioned_resources_parent. Without it the generated folder lands at the organization root, outside yourregulated/hierarchy and its guardrail policies. Pin it to a dedicated parent folder so the boundary inherits your landing-zone controls and is easy to find and bill. - Rotate CMEK on a defined cadence and never longer than the regime allows. For FedRAMP/IL4/HIPAA the module enforces a
kms_settingsblock; keepkms_rotation_periodat 90 days (7776000s) or tighter, and setkms_next_rotation_timeto a real future date so the first rotation is scheduled, not skipped. - Enable violation notifications in every regulated environment. Leave
violation_notifications_enabled = trueso the security team learns about boundary drift immediately; a silent violation is an audit finding waiting to happen. - Pin the partner for sovereign regimes and audit it. For EU/JP/regional-support workloads,
enable_sovereign_controlsplus an explicitpartner(T-Systems, SIA/Minsait, PSN) is what actually delivers the sovereignty guarantee — the module’s precondition blocks the silent mistake of enabling sovereign controls with no partner attached. - Name provisioned resources deterministically and label for cost. Use
resource_settingsto give the folder and key ring policy-friendly IDs that match your standard, and applylabelslikedata_classificationandregimeso regulated spend is attributable and the boundary is discoverable in the resource hierarchy.