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. Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- 🔐 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) viafor_eachovervar.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 destructiveterraform destroyoperations 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_keysto 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, andterraform-google-bigquery-dataseteach accept an optionalkms_key_name-style variable populated from this module'scrypto_key_idsoutput — 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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;
Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.
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;
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 |
| 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_linkattribute. Cloud KMS resources are addressed purely by theirid-form resource name — confirmed against the live schema for both resources. This module's outputs deliberately omit aself_linkrow (see 🧾 Outputs). google_kms_key_ringhas nolabelsargument at all, whilegoogle_kms_crypto_keydoes.labelsis therefore scoped inside eachcrypto_keysmap 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'smainbranch) describe adeletion_policyargument ongoogle_kms_key_ringmirroring the one ongoogle_kms_crypto_key. The schema-JSON fallback against the actually-resolved~> 7.0install (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-leveldeletion_policyvariable as a result. - Neither key ring
name/locationnor crypto keykey_ring/purposecan 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_periodis purpose-gated. It is only valid whenpurpose = "ENCRYPT_DECRYPT"; supplying it alongsideASYMMETRIC_SIGN,ASYMMETRIC_DECRYPT,RAW_ENCRYPT_DECRYPT, orMACis rejected by the API. This module's ownvalidation {}block catches the mismatch atplantime rather than leaving it to anapply-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'sCryptoKeyPurposeREST 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_policyappears in the provider's docs forgoogle_kms_crypto_keybut 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.
roles/cloudkms.adminon 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.)
cloudkms.googleapis.comAPI enabled on the target project (viaterraform-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_keyscollection grows large; this library's plan-only posture cannot surface a quota rejection untilapply. - If the organization has
constraints/gcp.resourceLocationsset, it restricts which key ringlocationvalues are permitted — confirm the intendedlocationis allowed before authoring a composition that hardcodes it; a violation surfaces only atapply, never atplan. - 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.)
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
# 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"
}
}
}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_linkrow — neithergoogle_kms_key_ringnorgoogle_kms_crypto_keyexposes aself_linkattribute. This is a deliberate, documented deviation from the library's default "id then self_link" ordering, not an omission.
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_keysdefaults 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_perioddefaults to"7776000s"(90 days) anddeletion_policydefaults to"PREVENT"— both apply even though this entry supplies onlypurpose.
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 resourcename— keep them stable oncecrypto_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_perioddefaults to 90-day automatic rotation (a KMS-specific secure-by-default extension). Setting it tonullexplicitly 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_periodmust be a decimal-seconds string ending insand greater than86400s(1 day) — enforced by this module's ownvalidation {}block atplantime.
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_periodMUST benullfor any non-ENCRYPT_DECRYPTpurpose — 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 atapply.
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_leveldefaults 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"
}
}
}
}ℹ️
MACkeys are used to generate/verify message authentication codes rather than to encrypt/decrypt or sign — a distinct use case from the other fourpurposevalues.
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"
}
}
}
}🔒
labelslives on eachcrypto_keysentry, not as a single module-wide variable — the key ring resource itself has nolabelsargument 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 = truecreates theCryptoKeywith noCryptoKeyVersion— the caller must create one separately viagoogle_kms_crypto_key_versionor agoogle_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_policydefaults 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
timeoutsblock supports onlycreate/delete(noupdate— verified independently against the live schema, since the key ring has no mutable fields besidesdeletion_policy). The crypto key'stimeoutsblock 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_durationis 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"
}
}
}ℹ️
locationaccepts 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 atapply, not atplan. Confirm anyconstraints/gcp.resourceLocationsorg 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 grantedroles/cloudkms.cryptoKeyEncrypterDecrypterdirectly onmodule.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.
| 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
}| 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_linkoutput — see 🔌 Cross-Module Contract and 🧠 Architecture Notes.
- Key rings cannot be deleted via the GCP API under any circumstance.
terraform destroy(or removinggoogle_kms_key_ring.thisfrom 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.0schema (see below). - Crypto key destroy is not an immediate delete either. With the default
deletion_policy = "PREVENT", a destroy attempt is refused outright. Even withdeletion_policy = "DELETE"(explicit opt-out), the crypto key is only disabled and its versions scheduled for destruction, subject todestroy_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
locationis force-new and permanent. There is no in-place "move" between locations. Combined with the non-deletion guarantee above, changinglocationleaves the old key ring orphaned in the project forever, not removed. - No key-ring-level
deletion_policyvariable, despite the crypto key having one. The terraform-registry MCP's docs describe this argument ongoogle_kms_key_ring(sourced from the provider'smainbranch), but the schema-JSON fallback against the actually-resolved~> 7.0install (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. labelslives inside eachcrypto_keysentry, not as a flat module-level variable. The key ring resource has nolabelsargument 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),labelsis scoped per crypto key — mirroring the escape-clause precedent already established interraform-google-vpc-network's SCOPE.md for a resource pair with no label surface at all.timeoutsdiffer by resource, verified independently. The key ring supports onlycreate/delete(no mutable fields besidesdeletion_policyto time an update against); the crypto key supportscreate/update/delete. This module exposes both shapes distinctly rather than assuming they match.- Implicit dependency ordering.
google_kms_crypto_key.crypto_keysreferencesgoogle_kms_key_ring.this.iddirectly — there is no explicitdepends_onbetween the two resources; Terraform's graph ordering follows the reference. for_eachkey stability. Crypto key map keys become the GCP resourcenameand the key used in thecrypto_key_idsoutput. 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 correspondingcrypto_key_idsmap entry.- IAM propagation delay. This module grants no IAM itself, but any composition that grants
roles/cloudkms.adminimmediately 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.
| 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 |
cd C:\GitHubCode\newgooglecloudmodules\terraform-google-kms-keyring
terraform init -backend=false
terraform validate
terraform fmt -checkPin ?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.
terraform init -backend=false,terraform validate, andterraform fmt -checkare the entire offline proof gate for this module — all three pass cleanly as of this authoring session, against the actually-resolvedhashicorp/googleprovider version 7.39.0.validate/fmtconfirm internal type/reference consistency and canonical formatting only. Neither can catch GCP API-level rejections (quota, org policy, an invalidlocationorversion_template.algorithmstring, IAM propagation) — those surface only atapplytime, 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 realterraform planagainst 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.
$ 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"
}
| 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 |
google_kms_key_ringprovider referencegoogle_kms_crypto_keyprovider reference- Sibling modules:
terraform-google-cloud-sql-instance,terraform-google-storage-bucket,terraform-google-gke-cluster,terraform-google-bigquery-dataset - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."