Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Google Cloud KMS Key Ring Terraform Module

Provisions a single Cloud KMS key ring (google_kms_key_ring) and its crypto keys (google_kms_crypto_key, for_each-managed) as one coherent unit — the CMEK source every other GCP module optionally references. Targets hashicorp/google ~> 7.0, Terraform >= 1.12.0.

Terraform Provider Version Type Resources Posture


🧩 Overview

  • 🔐 Provisions one Cloud KMS key ring (google_kms_key_ring.this) — the sole container Cloud KMS uses to scope location and access control for the keys inside it.
  • 🔑 Manages any number of crypto keys (google_kms_crypto_key.crypto_keys) via for_each over var.crypto_keys, keyed by crypto key name.
  • 🔁 Automatic 90-day rotation on symmetric (ENCRYPT_DECRYPT) keys by default — a KMS-specific secure-by-default extension (see 🧱 Design Principles).
  • 🛡️ deletion_policy = "PREVENT" by default on every crypto key, guarding against accidental, genuinely destructive terraform destroy operations against key material still in use.
  • 🧬 Supports symmetric encryption, asymmetric sign, asymmetric decrypt, MAC, and software/HSM/external protection levels via a fully-typed version_template.
  • 🚫 Never defaults crypto_keys to a non-empty map — an empty call is a key ring with no keys, the safe, inert result.

💡 Why it matters: This is our CMEK (customer-managed encryption key) source module. terraform-google-cloud-sql-instance, terraform-google-storage-bucket, terraform-google-gke-cluster, and terraform-google-bigquery-dataset each accept an optional kms_key_name-style variable populated from this module's crypto_key_ids output — but none of them default to a specific key. CMEK is always an explicit, caller-supplied opt-in, per the house Secure-by-default table.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits

This module has no upstream Consumes entry — it is foundational within the GCP catalog, on the same tier as terraform-google-vpc-network. The diagram below renders it as the source node, with outbound edges to every known consuming sibling (per SCOPE.md's Emits table).

flowchart LR
 kms["terraform-google-kms-keyring"]:::self
 sql["terraform-google-cloud-sql-instance"]:::sibling
 gcs["terraform-google-storage-bucket"]:::sibling
 gke["terraform-google-gke-cluster"]:::sibling
 bq["terraform-google-bigquery-dataset"]:::sibling

 kms -->|"crypto_key_ids (optional CMEK)"| sql
 kms -->|"crypto_key_ids (optional CMEK)"| gcs
 kms -->|"crypto_key_ids (optional CMEK)"| gke
 kms -->|"crypto_key_ids (optional CMEK)"| bq

 classDef self fill:#4285F4,color:#FFFFFF,stroke:#174EA6,stroke-width:2px;
 classDef sibling fill:#ECEFF1,color:#263238,stroke:#90A4AE,stroke-width:1px;
Loading

Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.


🧬 What this builds

flowchart TB
 subgraph Module["terraform-google-kms-keyring"]
 direction TB
 ring["google_kms_key_ring.this<br/>(keystone)"]:::keystone
 ringto["timeouts (dynamic)<br/>create/delete only"]:::nested
 keys["google_kms_crypto_key.crypto_keys<br/>for_each var.crypto_keys"]:::child
 vt["version_template (dynamic)<br/>algorithm + protection_level"]:::nested
 keysto["timeouts (dynamic)<br/>create/update/delete"]:::nested
 end

 ring -->|"key_ring = google_kms_key_ring.this.id"| keys
 ring --> ringto
 keys --> vt
 keys --> keysto

 classDef keystone fill:#174EA6,color:#FFFFFF,stroke:#0D47A1,stroke-width:2px;
 classDef child fill:#4285F4,color:#FFFFFF,stroke:#174EA6,stroke-width:2px;
 classDef nested fill:#ECEFF1,color:#263238,stroke:#90A4AE,stroke-width:1px;
Loading

Validated via the Mermaid Chart MCP before embedding.

Resource inventory (2 resources):

Resource Count Role
google_kms_key_ring.this 1 Keystone key ring, scopes location + access control
google_kms_crypto_key.crypto_keys for_each over var.crypto_keys CMEK crypto keys, each with its own purpose, rotation, protection level, and deletion guard

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/google ~> 7.0
Provider block None — the caller's root module configures google (ADC, WIF, or a service-account key per our authentication model)

Schema notes that bite:

  • Neither resource exposes a self_link attribute. Cloud KMS resources are addressed purely by their id-form resource name — confirmed against the live schema for both resources. This module's outputs deliberately omit a self_link row (see 🧾 Outputs).
  • google_kms_key_ring has no labels argument at all, while google_kms_crypto_key does. labels is therefore scoped inside each crypto_keys map entry rather than as a flat module-level variable — see 🧠 Architecture Notes.
  • Docs vs. resolved schema gap on deletion_policy. The terraform-registry MCP's docs (sourced from the provider's main branch) describe a deletion_policy argument on google_kms_key_ring mirroring the one on google_kms_crypto_key. The schema-JSON fallback against the actually-resolved ~> 7.0 install (7.39.0 at authoring time) confirms this argument is not yet present on the key ring resource — only on the crypto key. This module grounds in the resolved schema and does not expose a key-ring-level deletion_policy variable as a result.
  • Neither key ring name/location nor crypto key key_ring/purpose can be changed in-place. Every one of these forces a destroy/recreate — and because neither resource can actually be deleted from GCP (see 🧠 Architecture Notes), the "old" resource is orphaned in the project, not removed.
  • rotation_period is purpose-gated. It is only valid when purpose = "ENCRYPT_DECRYPT"; supplying it alongside ASYMMETRIC_SIGN, ASYMMETRIC_DECRYPT, RAW_ENCRYPT_DECRYPT, or MAC is rejected by the API. This module's own validation {} block catches the mismatch at plan time rather than leaving it to an apply-time API rejection.
  • purpose's enum values were not returned verbatim by the live provider schema/documentation during authoring — the schema/documentation linked out to Google's CryptoKeyPurpose REST reference rather than enumerating it inline. The five values encoded in this module's validation (ENCRYPT_DECRYPT, ASYMMETRIC_SIGN, ASYMMETRIC_DECRYPT, RAW_ENCRYPT_DECRYPT, MAC) come from that long-stable public REST reference rather than a value confirmed verbatim from the provider schema/documentation directly — flagged per this suite's honesty standard.
  • key_access_justifications_policy appears in the provider's docs for google_kms_crypto_key but is flagged Beta-launch-stage by Google and is absent from the actually-resolved ~> 7.0 (7.39.0) schema. Excluded from this module in v1.

🔑 Required IAM Roles

  • roles/cloudkms.admin on the target project — required to create and manage the key ring and its crypto keys (cloudkms.keyRings.create, cloudkms.cryptoKeys.create, cloudkms.cryptoKeys.update, and related permissions bundled in this role).

(Sourced directly from SCOPE.md — not re-derived.)


☁️ GCP Prerequisites

  • cloudkms.googleapis.com API enabled on the target project (via terraform-google-project-services, applied before this module per the house recommended authoring order).
  • Cloud KMS enforces per-project, per-location quotas on the number of key rings and the number of crypto keys per key ring — verify current quota before a var.crypto_keys collection grows large; this library's plan-only posture cannot surface a quota rejection until apply.
  • If the organization has constraints/gcp.resourceLocations set, it restricts which key ring location values are permitted — confirm the intended location is allowed before authoring a composition that hardcodes it; a violation surfaces only at apply, never at plan.
  • Any resource that will consume a crypto key from this module as CMEK (Cloud SQL, GCS, GKE, BigQuery, etc.) requires its own Google-managed service agent to be granted an appropriate Cloud KMS IAM role directly on that crypto key before encryption/decryption succeeds. Granting that binding is out of scope for this module by design — it belongs to the composing root module or a dedicated IAM aggregation module, applied after this module's crypto key exists.

(Sourced directly from SCOPE.md — not re-derived.)


📁 Module Structure

terraform-google-kms-keyring/
├── providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version — no provider {} block
├── variables.tf # key_ring_name, location, crypto_keys, timeouts
├── main.tf # google_kms_key_ring.this + google_kms_crypto_key.crypto_keys (for_each)
├── outputs.tf # id, name, crypto_key_ids — no self_link (see Architecture Notes)
├── README.md # this file
├── SCOPE.md # cross-module contract
└── examples/ # runnable example(s) matching the Quick Start below

⚙️ Quick Start

# Caller's root module configures the google provider (ADC, WIF, or a service-account key) —
# this module never declares project/region/zone/credentials variables.

module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-prod-cmek"
  location      = "us-east1"

  crypto_keys = {
    "app-data-key" = {
      purpose = "ENCRYPT_DECRYPT"
    }
  }
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
(none — this is a foundational module)

Emits

Output Description Consumed by
id Key ring resource id (projects/{{project}}/locations/{{location}}/keyRings/{{name}}) Compositions referencing the ring itself (e.g. a ring-level IAM aggregation module)
name Key ring name, as supplied Callers composing a display name or a data "google_kms_key_ring" lookup elsewhere
crypto_key_ids Map of crypto key name → google_kms_crypto_key.id (fully-qualified resource name) terraform-google-cloud-sql-instance, terraform-google-storage-bucket, terraform-google-gke-cluster, terraform-google-bigquery-dataset — this is the argument each of those modules' kms_key_name-style variable expects

ℹ️ No self_link row — neither google_kms_key_ring nor google_kms_crypto_key exposes a self_link attribute. This is a deliberate, documented deviation from the library's default "id then self_link" ordering, not an omission.


📚 Example Library

1 · Minimal key ring, no crypto keys
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-shared-keyring"
  location      = "us-east1"
}

ℹ️ crypto_keys defaults to {} — a key ring with no keys is a valid, safe starting point for a composition that adds keys in a later apply.

2 · Single symmetric encryption key with default rotation
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-app-keyring"
  location      = "us-east1"

  crypto_keys = {
    "app-data-key" = {
      purpose = "ENCRYPT_DECRYPT"
    }
  }
}

💡 rotation_period defaults to "7776000s" (90 days) and deletion_policy defaults to "PREVENT" — both apply even though this entry supplies only purpose.

3 · Multiple crypto keys in one ring
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-multikey-ring"
  location      = "us-east1"

  crypto_keys = {
    "app-data-key"  = { purpose = "ENCRYPT_DECRYPT" }
    "logs-data-key" = { purpose = "ENCRYPT_DECRYPT" }
  }
}

⚠️ Crypto key map keys become the GCP resource name — keep them stable once crypto_key_ids["<name>"] is referenced by a consuming module.

4 · Disabling automatic rotation for a specific key
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-norotation-ring"
  location      = "us-east1"

  crypto_keys = {
    "static-reference-key" = {
      purpose         = "ENCRYPT_DECRYPT"
      rotation_period = null
    }
  }
}

⚠️ rotation_period defaults to 90-day automatic rotation (a KMS-specific secure-by-default extension). Setting it to null explicitly opts out — document the compliance rationale in the calling composition if used outside a genuine "must reference one immutable key version" scenario.

5 · Custom rotation period
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-fastrotation-ring"
  location      = "us-east1"

  crypto_keys = {
    "high-sensitivity-key" = {
      purpose         = "ENCRYPT_DECRYPT"
      rotation_period = "2592000s" # 30 days
    }
  }
}

ℹ️ rotation_period must be a decimal-seconds string ending in s and greater than 86400s (1 day) — enforced by this module's own validation {} block at plan time.

6 · Asymmetric signing key
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-signing-ring"
  location      = "us-east1"

  crypto_keys = {
    "document-signing-key" = {
      purpose         = "ASYMMETRIC_SIGN"
      rotation_period = null # required — ASYMMETRIC_SIGN does not support automatic rotation

      version_template = {
        algorithm = "EC_SIGN_P384_SHA384"
      }
    }
  }
}

🔒 rotation_period MUST be null for any non-ENCRYPT_DECRYPT purpose — this module's validation rejects the plan otherwise, forcing the caller to be deliberate rather than silently inheriting a rotation default that Cloud KMS would reject at apply.

7 · HSM-backed protection level
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-hsm-ring"
  location      = "us-east1"

  crypto_keys = {
    "hsm-backed-key" = {
      purpose = "ENCRYPT_DECRYPT"

      version_template = {
        algorithm        = "GOOGLE_SYMMETRIC_ENCRYPTION"
        protection_level = "HSM"
      }
    }
  }
}

💡 protection_level defaults to "SOFTWARE"; "HSM" requests hardware-backed key material, subject to its own regional availability and pricing considerations — confirm both before committing a production workload to it.

8 · MAC (message authentication code) key
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-mac-ring"
  location      = "us-east1"

  crypto_keys = {
    "integrity-check-key" = {
      purpose         = "MAC"
      rotation_period = null

      version_template = {
        algorithm = "HMAC_SHA256"
      }
    }
  }
}

ℹ️ MAC keys are used to generate/verify message authentication codes rather than to encrypt/decrypt or sign — a distinct use case from the other four purpose values.

9 · Per-key labels
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-labeled-ring"
  location      = "us-east1"

  crypto_keys = {
    "finance-data-key" = {
      purpose = "ENCRYPT_DECRYPT"
      labels = {
        team           = "finance"
        classification = "restricted"
      }
    }
    "risk-data-key" = {
      purpose = "ENCRYPT_DECRYPT"
      labels = {
        team = "risk"
      }
    }
  }
}

🔒 labels lives on each crypto_keys entry, not as a single module-wide variable — the key ring resource itself has no labels argument in the live schema, and different keys commonly need different labels. See 🧠 Architecture Notes.

10 · Import-only key (external key material)
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-import-ring"
  location      = "us-east1"

  crypto_keys = {
    "imported-material-key" = {
      purpose                       = "ENCRYPT_DECRYPT"
      rotation_period               = null
      import_only                   = true
      skip_initial_version_creation = true
    }
  }
}

⚠️ skip_initial_version_creation = true creates the CryptoKey with no CryptoKeyVersion — the caller must create one separately via google_kms_crypto_key_version or a google_kms_key_ring_import_job, both outside this module's scope. Only applicable at initial creation.

11 · Opting out of the deletion guard for a genuinely disposable dev key
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-dev-keyring"
  location      = "us-east1"

  crypto_keys = {
    "scratch-key" = {
      purpose         = "ENCRYPT_DECRYPT"
      deletion_policy = "DELETE"
    }
  }
}

⚠️ deletion_policy defaults to "PREVENT". Setting it to "DELETE" restores the provider's own default destroy semantics (disable + schedule version destruction) — only appropriate for keys that never protect data anyone needs to recover, e.g. a disposable sandbox/dev key.

12 · Custom create/delete timeouts (key ring) and create/update/delete timeouts (crypto key)
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-slow-provisioning-ring"
  location      = "us-east1"

  timeouts = {
    create = "30m"
    delete = "30m"
  }

  crypto_keys = {
    "app-data-key" = {
      purpose = "ENCRYPT_DECRYPT"
      timeouts = {
        create = "30m"
        update = "30m"
        delete = "30m"
      }
    }
  }
}

ℹ️ The key ring's timeouts block supports only create/delete (no update — verified independently against the live schema, since the key ring has no mutable fields besides deletion_policy). The crypto key's timeouts block supports all three.

13 · Extended destroy-scheduled duration for a compliance-driven recovery window
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-compliance-ring"
  location      = "us-east1"

  crypto_keys = {
    "regulated-data-key" = {
      purpose                    = "ENCRYPT_DECRYPT"
      destroy_scheduled_duration = "7776000s" # 90 days, vs. the 30-day GCP default
    }
  }
}

💡 destroy_scheduled_duration is not defaulted by this module — GCP applies its own 30-day default when unset. A longer window widens the recovery opportunity after an accidental "DELETE"-policy destroy, at the cost of a longer window during which destroyed-but-not-yet-gone key material technically still exists.

14 · Multi-region location for data-residency requirements
module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-us-multiregion-ring"
  location      = "us"

  crypto_keys = {
    "residency-scoped-key" = {
      purpose = "ENCRYPT_DECRYPT"
    }
  }
}

ℹ️ location accepts global, regional, and multi-region values and is not validated against a hardcoded allow-list (GCP adds locations over time) — an invalid value is caught by the API at apply, not at plan. Confirm any constraints/gcp.resourceLocations org policy allows the chosen value first.

15 · 🏗️ End-to-end composition

Wires this module's crypto_key_ids output into terraform-google-storage-bucket's encryption.default_kms_key_name input, reflecting the Emits/Consumed-by relationship documented in both modules' SCOPE.md files.

module "kms_keyring" {
  source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"

  key_ring_name = "casey-prod-cmek"
  location      = "us-east1"

  crypto_keys = {
    "gcs-bucket-key" = {
      purpose = "ENCRYPT_DECRYPT"
      labels = {
        team = "platform"
      }
    }
  }
}

module "storage_bucket" {
  source = "git::https://github.com/microsoftexpert/terraform-google-storage-bucket.git?ref=v1.0.0"

  bucket_name = "casey-prod-app-artifacts"
  location    = "US-EAST1"

  # Consumes this module's crypto_key_ids output (Emits table row: `crypto_key_ids`) — never the
  # key ring `id` directly.
  encryption = {
    default_kms_key_name = module.kms_keyring.crypto_key_ids["gcs-bucket-key"]
  }
}

⚠️ Before this composition can succeed, the GCS service agent for the target project must be granted roles/cloudkms.cryptoKeyEncrypterDecrypter directly on module.kms_keyring.crypto_key_ids["gcs-bucket-key"] — a binding out of scope for both modules by design (see ☁️ GCP Prerequisites). Grant it via a dedicated IAM aggregation module or the composing root module, applied after this module's crypto key exists and before the bucket is created; allow for IAM propagation lag (up to ~60 seconds) between the grant and the bucket apply.


📥 Inputs

Variable Type Default Notes
key_ring_name string — (required) Force-new; [a-zA-Z0-9_-]{1,63}
location string — (required) Force-new; not validated against a hardcoded list (see Schema notes)
crypto_keys map(object({...})) {} Keyed by crypto key name — see full schema below
timeouts object({create, delete}) null Applies to the key ring only — no update field
Full crypto_keys object schema
variable "crypto_keys" {
  type = map(object({
    purpose         = optional(string, "ENCRYPT_DECRYPT")
    rotation_period = optional(string, "7776000s")
    version_template = optional(object({
      algorithm        = string
      protection_level = optional(string, "SOFTWARE")
    }))
    destroy_scheduled_duration    = optional(string)
    import_only                   = optional(bool, false)
    skip_initial_version_creation = optional(bool, false)
    crypto_key_backend            = optional(string)
    deletion_policy               = optional(string, "PREVENT")
    labels                        = optional(map(string), {})
    timeouts = optional(object({
      create = optional(string)
      update = optional(string)
      delete = optional(string)
    }))
  }))
  default = {}
}
Full timeouts object schema (key ring)
variable "timeouts" {
  type = object({
    create = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Sensitive
id Key ring Terraform-internal resource id No
name Key ring name No
crypto_key_ids Map of crypto key name → crypto key id (the load-bearing CMEK output) No

ℹ️ No self_link output — see 🔌 Cross-Module Contract and 🧠 Architecture Notes.


🧠 Architecture Notes

  • Key rings cannot be deleted via the GCP API under any circumstance. terraform destroy (or removing google_kms_key_ring.this from configuration) only ever removes it from Terraform state — the key ring persists in the project indefinitely. There is no configurable guard on this resource to change that behavior in the actually-resolved ~> 7.0 schema (see below).
  • Crypto key destroy is not an immediate delete either. With the default deletion_policy = "PREVENT", a destroy attempt is refused outright. Even with deletion_policy = "DELETE" (explicit opt-out), the crypto key is only disabled and its versions scheduled for destruction, subject to destroy_scheduled_duration (30 days by default) — not an instant, irreversible delete, but genuinely destructive once the window elapses: previously-encrypted data becomes irrecoverable.
  • Key ring location is force-new and permanent. There is no in-place "move" between locations. Combined with the non-deletion guarantee above, changing location leaves the old key ring orphaned in the project forever, not removed.
  • No key-ring-level deletion_policy variable, despite the crypto key having one. The terraform-registry MCP's docs describe this argument on google_kms_key_ring (sourced from the provider's main branch), but the schema-JSON fallback against the actually-resolved ~> 7.0 install (7.39.0) confirms it is not yet part of the released schema. This module grounds in the resolved schema rather than the aspirational docs and omits the variable; the protection it would have offered is moot in practice regardless, since key rings cannot be deleted either way.
  • labels lives inside each crypto_keys entry, not as a flat module-level variable. The key ring resource has no labels argument at all in the live schema; the crypto key does. Rather than invent a flat variable with no home on the keystone (or force every key in the ring to share one label set), labels is scoped per crypto key — mirroring the escape-clause precedent already established in terraform-google-vpc-network's SCOPE.md for a resource pair with no label surface at all.
  • timeouts differ by resource, verified independently. The key ring supports only create/delete (no mutable fields besides deletion_policy to time an update against); the crypto key supports create/update/delete. This module exposes both shapes distinctly rather than assuming they match.
  • Implicit dependency ordering. google_kms_crypto_key.crypto_keys references google_kms_key_ring.this.id directly — there is no explicit depends_on between the two resources; Terraform's graph ordering follows the reference.
  • for_each key stability. Crypto key map keys become the GCP resource name and the key used in the crypto_key_ids output. Renaming a key is a destroy/recreate (Terraform sees a removed + an added resource), not an in-place rename — keep keys stable once a consuming module references the corresponding crypto_key_ids map entry.
  • IAM propagation delay. This module grants no IAM itself, but any composition that grants roles/cloudkms.admin immediately before this module's apply, or grants a crypto-key-level role to a consuming resource's service agent immediately before that resource's apply, may see a transient permission-denied error for up to ~60 seconds after the grant.

🧱 Design Principles

Concern Secure default Opt-out (explicit)
SQL / GKE / GCS / BigQuery encryption (CMEK) This module's crypto key id is accepted as an optional variable by consuming modules — never defaulted to a specific key by any of them Caller supplies crypto_key_ids["<name>"] explicitly per this suite's house secure-by-default table
Automatic key rotation (KMS-specific extension) rotation_period = "7776000s" (90 days) for ENCRYPT_DECRYPT keys — the provider's own default is no automatic rotation Caller sets rotation_period = null explicitly per key
Accidental destroy of key material (KMS-specific extension) deletion_policy = "PREVENT" on every crypto key Caller sets "DELETE" (provider default semantics) or "ABANDON" per key
Import-only / bring-your-own key material import_only = false, skip_initial_version_creation = false — GCP-generated key material by default Caller sets both true for a bring-your-own-material key
Key protection level protection_level = "SOFTWARE" (the provider's own default) when version_template is supplied Caller sets "HSM", "EXTERNAL", or "EXTERNAL_VPC" explicitly
Empty call crypto_keys defaults to {} — a key ring with no keys, the safe, inert result Caller supplies crypto_keys entries explicitly

🚀 Runbook

cd C:\GitHubCode\newgooglecloudmodules\terraform-google-kms-keyring
terraform init -backend=false
terraform validate
terraform fmt -check

Pin ?ref=v1.0.0 when consuming this module — never a branch. This library is plan-only; a human applies from CI with valid Workload Identity Federation or ADC credentials.


🧪 Testing

  • terraform init -backend=false, terraform validate, and terraform fmt -check are the entire offline proof gate for this module — all three pass cleanly as of this authoring session, against the actually-resolved hashicorp/google provider version 7.39.0.
  • validate/fmt confirm internal type/reference consistency and canonical formatting only. Neither can catch GCP API-level rejections (quota, org policy, an invalid location or version_template.algorithm string, IAM propagation) — those surface only at apply time, against a real project, from a human-run CI pipeline with valid credentials.
  • The examples/ directory exists so a consuming GitHub Actions workflow can run a real terraform plan against a real project as part of that pipeline's own review gate; this library only guarantees the example is syntactically and structurally sound in isolation.

💬 Example Output

$ terraform output

id = "projects/casey-prod-networking/locations/us-east1/keyRings/casey-prod-cmek"
name = "casey-prod-cmek"
crypto_key_ids = {
 "app-data-key" = "projects/casey-prod-networking/locations/us-east1/keyRings/casey-prod-cmek/cryptoKeys/app-data-key"
}

🔍 Troubleshooting

Symptom Cause Fix
Error: rotation_period is only valid when purpose = "ENCRYPT_DECRYPT" at plan time A crypto_keys entry sets a non-ENCRYPT_DECRYPT purpose without also setting rotation_period = null Add rotation_period = null to that entry — the 90-day default does not apply to asymmetric/MAC purposes
Error: purpose must be one of ENCRYPT_DECRYPT, ASYMMETRIC_SIGN, ASYMMETRIC_DECRYPT, RAW_ENCRYPT_DECRYPT, or MAC Typo'd or unsupported purpose value Use one of the five listed enum values (case-sensitive)
terraform destroy fails with a deletion_policy error deletion_policy = "PREVENT" (the module default) on the key ring or a crypto key Expected behavior — the guard is doing its job. Change deletion_policy to "ABANDON" or "DELETE" explicitly, re-apply, then destroy
terraform apply succeeds but data encrypted under a "removed" crypto key later becomes unreadable The crypto key had deletion_policy = "DELETE" and its destroy_scheduled_duration window elapsed after removal from configuration Expected, irreversible Cloud KMS behavior — restore the key from a backup key ring/version if the window has not yet elapsed, otherwise the data is unrecoverable by design
A downstream module (Cloud SQL, GCS, GKE, BigQuery) fails to encrypt with a permission-denied error right after this module's crypto key is created The consuming resource's Google-managed service agent has not yet been granted a Cloud KMS role on the crypto key, or the grant has not yet propagated (up to ~60 seconds) Confirm the IAM binding exists on the specific crypto key (not just the key ring or project) and retry after the propagation window
terraform plan shows no error but the real apply fails with an invalid location or version_template.algorithm This library is plan-only — these enums are deliberately not hardcoded and are validated by the API, not validate/plan Confirm the value against the current Cloud KMS locations/algorithms reference before apply
A key ring you thought was destroyed still shows up in the GCP console after terraform destroy Expected — Cloud KMS key rings cannot be deleted under any circumstance, regardless of deletion_policy semantics elsewhere in this module This is normal; the ring is simply no longer tracked in Terraform state once removed

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."

Releases

Packages

Contributors

Languages