All modules in this track (11)
In this module, you'll learn to:
- Take a module through the five gate steps, from development in isolation to its first apply in
dev - Build the justification package (ADR, scoped skip, README entry) that lets a finding be accepted
- Explain why the scan runs twice, and how CI makes skipping a step impossible rather than just discouraged
Before you start: read CI/CD Guardrails for how Checkov runs, and ADRs & Changelogs for the records an accepted finding needs.
Scan the module before anything depends on it
Module 06 runs Checkov on every pull request. That catches problems, but late: by the time a finding shows up in an environment's plan, the module is already wired in, other work is stacked on it, and the pressure is to add a skip and move on.
The promotion gate moves the scan earlier. No module, base or composition, may be called from environments/dev or any other environment until it has passed the gate. The module is scanned on its own, while it's still cheap to change, and the decision about each finding is made and reviewed before anything depends on it.
Develop the module in isolation
Build or change the module under modules/base/<name> or modules/composition/<name>, with its tests (Module 09). Don't add a module block to any environment yet. Until the gate is passed, nothing calls this code.
Scan only that module
checkov -d terraform/modules/base/<name> \
--config-file terraform/.checkov.yaml
- No findings: go straight to Step 4. No justification is needed.
- Findings: go to Step 3. The module can't move on with any finding unexplained.
Fix it, or justify it
Every finding ends one of two ways:
| Outcome | What's required |
|---|---|
| Fixed | A short note in the module's README that it was fixed. Fixing is always the preferred outcome. |
| Accepted risk | All three parts of the justification package: an ADR, a scoped inline skip, and a README entry |
The three parts of an accepted risk:
- An ADR explaining why the risk is accepted, with real alternatives in its Options Considered section (Module 10).
- A scoped inline skip on the exact resource, giving the reason and the ADR number:
resource "aws_s3_bucket" "this" {
# checkov:skip=CKV_AWS_18:Nonprod synthetic data only (ADR-0003)
bucket = var.bucket_name
}- A README entry under a
## Security Exceptionsheading, so anyone reading the module sees its accepted risks without reading every resource:
Security Exceptions
| Check | Resource | Reason | ADR |
|---|---|---|---|
| CKV_AWS_18 | aws_s3_bucket.this | Nonprod synthetic data only | ADR-0003 |
**No blanket skips to get a module through.** A global `skip-check` in `.checkov.yaml` hides the finding from every module, including ones where it really matters. It's only for a platform-wide decision that already has its own ADR, like the cross-region replication example in [Module 06](06-ci-cd-security-linting-testing.html). Everything else is scoped to one resource and justified one at a time.
A second person approves
Someone other than the module's author reviews, as part of the module's pull request:
- the Checkov output: clean, or every finding justified;
- the ADR for each accepted risk, and whether its rejected options are real;
- that the README's Security Exceptions table matches the skips actually in the code.
The module code doesn't merge without this sign-off. Branch protection makes it a rule, not a habit: the module scan is a required status check, and the reviewer is a required approval.
Wire it in, and scan again
Only now may the module be referenced:
- Add the
moduleblock to the composition, or call the composition fromenvironments/dev/main.tf. - Scan again, at the composition and environment level:
checkov -d terraform/modules/composition/<composition> \
--config-file terraform/.checkov.yaml
checkov -d terraform/environments/dev \
--config-file terraform/.checkov.yaml- Put any new findings through Steps 2 to 4 again.
- Only after this second clean or justified scan does
terraform planandapplyrun againstdev. - Log the promotion in
environments/dev/docs/CHANGELOG.md.
The second scan isn't a formality. Base modules can be clean on their own and still produce findings once joined: the IAM policy that grants the Glue job access to the bucket, or the security group that lets it reach a database, is written in the composition. That joining code is exactly where over-broad permissions tend to appear.
Make skipping a step impossible
A process people are trusted to follow erodes under deadline pressure. The pipeline enforces the gate instead:
| Pull request touches | CI does |
|---|---|
modules/** | Runs terraform test, then the module-scoped Checkov scan. Both are required checks, so the pull request can't merge until they pass and the reviewer approves |
environments/<env>/** | Re-scans the whole environments/<env> tree, not just the diff, and blocks plan and apply if it fails |
In a single repository, "the module passed the gate" has a simple meaning: its code is on main, and main only accepts module code that passed the required checks. Re-scanning the whole environment tree on every environment change is the backstop. A module that somehow slipped in without its own scan is still caught before apply.
# .github/workflows/terraform-modules.yml (excerpt)
on:
pull_request:
paths: ["terraform/modules/**"]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Module tests
run: |
for dir in terraform/modules/*/*/; do
if [ -d "$dir/tests" ]; then
(cd "$dir" && terraform init -backend=false && terraform test)
fi
done
- name: Checkov
run: |
pip install checkov
checkov -d terraform/modules \
--config-file terraform/.checkov.yaml \
-o cli -o junitxml --output-file-path console,checkovThe whole delivery, end to end
Part 2 of this track describes one process. Here it is in a single picture, with where each step is covered:
There are two ways into this process:
- Changing or adding one module. Start at Design, and follow it down. The environments and their layers are already in place.
- Building a platform from scratch. First split it into the numbered state layers from Module 01, such as
01-networking,03-data, and04-compute, and bootstrap the state backend (Module 02). Then build each layer's code through this same process.
Copy into the pull request description
- Module scanned with Checkov on its own (Step 2), output attached.
- Every finding fixed, or justified with an ADR, a scoped inline skip, and a README entry (Step 3).
- A reviewer other than the author approved the scan and the justifications (Step 4).
- Composition and environment re-scan run after wiring (Step 5), output attached.
environments/dev/docs/CHANGELOG.mdupdated with the promotion.
Key terms
- Promotion gate: the rule that a module is scanned, justified, and approved before any environment may call it.
- Justification package: the ADR, scoped inline skip, and README Security Exceptions entry an accepted finding needs.
- Scoped skip: a
checkov:skipcomment on one resource, never a global skip. - Second scan: the composition- and environment-level scan after wiring, which catches findings in the joining code.
- Required status check: a CI check that branch protection won't let a pull request merge without.
Pop quiz
Five questions: three on the ideas in this module, and two scenarios where you apply them. The order changes every time you take it, and 4 out of 5 passes.
5 questions. Decide on your answer to each, then check it.
-
Concept
When is a module first allowed to be called from
environments/dev?Show answer
After it has been scanned alone, every finding fixed or justified, and a second person has approved it
The gate comes before any wiring. That's the whole point: decisions about findings are made while the module is still cheap to change.
-
Concept
What three things make up the justification for an accepted finding?
Show answer
An ADR, a scoped inline skip naming the ADR, and an entry in the module README's Security Exceptions
Each part has a job: the ADR shows the reasoning, the skip scopes it to one resource, and the README makes it visible to anyone using the module.
-
Concept
Why scan again after the module is wired into the composition?
Show answer
The joining code, such as IAM policies and security groups, is written in the composition and wasn't in the first scan
Clean parts can still be joined unsafely. The permissions between components are where new findings usually appear.
-
Scenario
A deadline is close, and an engineer proposes adding
CKV_AWS_18toskip-checkin.checkov.yamlso the pipeline goes green. What's wrong with that?Show answer
A global skip hides that check for every module, including ones where it matters, with no reasoning recorded
Exceptions are scoped to one resource and justified one at a time. A global skip turns one accepted risk into an invisible one everywhere.
-
Scenario
Your team is starting a brand-new data platform with networking, a database, and compute. Where do you begin?
Show answer
Split it into numbered state layers and bootstrap the backend, then build each layer's modules through the gate process
From scratch, first decide how state is split (Module 01). Then the module process runs inside each layer, one module at a time.