Building a Compliant GCP Storage Module with Terraform (CGE-P Lab 2.4)

Summary

Continuing through the Certified GRC Engineer - Practitioner (CGE-P) course. Lab 2.3 built a single compliant S3 bucket on AWS. Lab 2.4 moves to GCP and shifts from writing one bucket to writing a module that deploys buckets, with compliance sitting inside the module where a consumer can't reach it.

Code for this lab is in my CGE-P Capstone repo.

Lab 2.4: Terraform Modules for Compliance (GCP)

The module is called compliant-gcs-bucket. It is a reusable building block that creates a GCS bucket pre-wired with NIST SP 800-53 controls, plus its own Cloud KMS keyring and key for encryption. Two consumers, dev and prod, call the same module with different environment and retention values. Both get the same security posture either way.

terraform/
├── modules/
│   └── compliant-gcs-bucket/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── README.md
└── primitives/
    └── compliant-gcs/
        ├── main.tf
        └── variables.tf

The module has three files:

  • main.tf hardcodes anything compliance-relevant. A consumer cannot turn it off.
  • variables.tf exposes only what business actually changes: project, environment, retention, naming.
  • outputs.tf returns evidence, including a computed compliance_attestation map.

The shift from Lab 2.3 is that compliance stops being something you remember to add per-resource, and becomes something the module forces on every consumer by construction.

Code blockNIST ControlWhat it does
google_kms_key_ring + google_kms_crypto_keySC-12The module owns its own encryption key instead of relying on Google-managed encryption
encryption { default_kms_key_name = ... } on the bucketSC-13 / SC-28Customer-managed encryption key (CMEK) applied at rest
versioning + retention_policyAU-11Object versions preserved, retention period enforced per environment
uniform_bucket_level_access + public_access_prevention = "enforced"AC-3IAM-only access, no ACLs, no accidental public exposure
locals.required_labels merged into labelsCM-6Compliance labels forced onto every bucket; a consumer can add labels but not remove the required four

Gotchas I Hit

Unlike Lab 2.3, the issue I ran into wasn't a compliance gap in the design, it was drift in the documentation. If a compliance module's own supporting docs can drift from its code, what does that imply about trusting a vendor module's documentation without reading the .tf files directly? A vendor module's README makes compliance claims too ("this enforces encryption at rest"). The lesson here is: don't treat a README as proof of what a module does. Read the resource blocks yourself before you rely on the claim.