SOC 2 controls as Terraform modules
The traditional SOC 2 audit cycle is a project: months of evidence-gathering, screenshots, control narratives, attestation letters. There’s a different shape where each control is a Terraform module the engineering team uses; the audit reduces to checking which teams used which module. Here is what it looks like in practice.
The traditional shape
The auditor sends a request for evidence. The team scrambles to produce screenshots of CloudTrail configuration, IAM policies, KMS key rotation settings, etc. Each piece of evidence is a one-time gather; six months later the same auditor needs the same screenshots for the next cycle.
This shape doesn’t scale, and it’s brittle. The screenshot you sent the auditor in March doesn’t tell you anything about what your configuration looks like in September.
The module-based shape
Each control gets a module. The module:
- Implements the control (e.g. enables CloudTrail with the right options).
- Carries metadata about which control it satisfies.
- Is itself reviewed once by the security team.
When a team needs the control, they module "logging" { source =
"git::.../bloom-logging?ref=v2.1.0" }. They don’t decide the
configuration; the module does.
The audit becomes: which teams use which module versions? That’s
a one-line terraform state list query across all stacks.
A concrete example
The SOC 2 logging control (broadly: “events are logged, retained, and reviewable”) becomes a module:
# bloom-logging/main.tf
resource "aws_cloudtrail" "main" {
name = "${var.stack}-trail"
s3_bucket_name = aws_s3_bucket.logs.id
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true
kms_key_id = aws_kms_key.logs.arn
}
resource "aws_s3_bucket" "logs" {
bucket = "${var.stack}-trail-logs"
}
resource "aws_s3_bucket_object_lock_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
object_lock_configuration {
object_lock_enabled = "Enabled"
rule {
default_retention {
mode = "GOVERNANCE"
days = 2557 # 7 years
}
}
}
}
# ... encryption, IAM, alarms ...
The module’s README documents:
# bloom-logging
Satisfies SOC 2 CC7.2 — "monitoring of system components for
anomalies".
Provides:
- Multi-region CloudTrail with log file validation.
- S3 bucket with object lock + 7-year retention.
- KMS-encrypted log storage.
- CloudWatch alarms on log delivery failure.
Audit evidence:
- Trail configuration: aws_cloudtrail.main
- Retention: aws_s3_bucket_object_lock_configuration.logs
- Encryption: aws_kms_key.logs
Last security review: 2026-02-14 (review.md)
The auditor reads the README, reviews the module once, and signs off. Every team that uses the module inherits the sign-off.
What this changes about audit cycles
The audit conversation shifts from “show me your CloudTrail
configuration” to “show me which version of bloom-logging you’re
using, and when you last upgraded.” That’s two terraform state
queries.
Across our engagements, audit prep time dropped from ~4 weeks per
cycle to ~1 week. The reduction came almost entirely from this
shift; the actual evidence gathering went from one-off Slack
threads to a make audit-report command.
The hidden requirement: module governance
This pattern works only if the modules themselves are tightly governed. Two rules:
- Modules are reviewed by security before publish. Any change to a module that affects a control requires sign-off from the security team. The review is small (it’s one module) and happens once.
- Teams can’t fork modules. If a team forks
bloom-logginginto their own repo to customise it, the audit inheritance breaks. The platform should make it easy to extend modules without forking — variables, optional features, well-defined extension points.
If governance is loose, modules drift, audit inheritance breaks, and you’re back to per-team evidence gathering.
Which controls map well
Some SOC 2 controls map cleanly to Terraform modules:
| Control area | Module |
|---|---|
| Logical access (IAM boundaries) | bloom-iam-boundary |
| System monitoring (CloudTrail) | bloom-logging |
| Encryption at rest | bloom-encryption |
| Encryption in transit (ACM, ELB) | bloom-tls |
| Backup and recovery | bloom-backup |
| Change management (CI/CD pipeline) | bloom-pipeline |
Some don’t:
- People-process controls (background checks, security training).
- Business continuity / DR planning (you can’t Terraform a plan).
- Vendor management.
The split is roughly 60/40: 60% of SOC 2 maps to code; 40% stays narrative. The narrative portion still needs the traditional evidence gathering, but it’s a fraction of the volume.
When you should not use this pattern
Two cases where the module approach is wrong:
- The control varies wildly per stack. If every team needs a different IAM configuration, there’s no module to share. A per-team configuration with a checklist is the better fit.
- The audit cycle is too infrequent to amortise the module work. If you’re audited every two years and have three teams, the cost of building the modules outweighs the savings.
For multi-team, frequent-audit shops (most regulated finance and healthcare), the module approach pays back inside one cycle.
Where I’d start
Pick the loudest control in your last audit — the one that took the most evidence-gathering time. Build a Terraform module for it. Convince one team to adopt it. Get the security team to sign off on the module.
Next cycle, that control will take you 15 minutes. Pick the next loudest. Repeat.
A year in, you’ll have 10-15 modules covering the bulk of your SOC 2. The cycle that used to take 4 weeks will take 1.