Canonical source of reusable, opinionated Terraform modules for the NHS Screening programme on AWS. Modules are consumed by downstream repositories (primarily NHSDigital/bcss) via Git source with pinned release tags.
- Screening Terraform Modules (AWS)
Clone the repository and install tooling:
git clone https://github.com/NHSDigital/screening-terraform-modules-aws.git
cd screening-terraform-modules-awsTool versions are managed via mise. See .tool-versions for the pinned versions (and mise.toml for the TOML configuration). The key dependencies are:
| Tool | Version | Purpose |
|---|---|---|
| Terraform | >= 1.13.2 | Infrastructure as code |
| tflint | 0.59.1 | Terraform linter |
| terraform-docs | 0.24.0 | Auto-generate module documentation |
| terraform-config-inspect | latest | Generate aliased providers for validation |
| pre-commit | 4.6.0 | Git hook framework |
| Vale | 3.6.0 | English prose linter |
| Gitleaks | 8.30.1 | Secret scanning |
| jq | 1.7.1 | JSON processor |
| shellcheck | — | Shell script linter |
| GNU make | >= 3.82 | Task runner |
Install all tool versions:
mise installTool versions are maintained in two complementary formats for compatibility:
.tool-versions(asdf format) — Legacy format, used by CI/CD workflows and some toolingmise.toml(TOML format) — Modern mise configuration, withmise.lockfor reproducible cross-platform builds
Both files must be kept in sync. Update .tool-versions first, then ensure mise.toml is updated accordingly. Run mise lock to regenerate the lock file.
Local development and CI both resolve pinned versions from these files through mise.
make configThis installs Git hooks, configures the local development environment, and prepares the toolchain.
This branch includes comprehensive test coverage for new features:
- Conventional commit checks — native bash-based validation script replacing an external dependency
- Workflow security — GitHub Actions and pre-commit hooks pinned to immutable commit SHAs
- Tool version sync —
.tool-versionsandmise.tomlconsistency checks
Run validation tests:
make test-validations # Run all validations
make test-commit-validator # Test conventional commit checks
make test-workflow-pinning # Test action/hook pinning
bash tests/run-all-tests.sh # Run all tests directlyFor more details, see tests/README.md.
Reference a module from a downstream Terraform stack using a pinned Git ref:
module "my_bucket" {
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/s3-bucket?ref=v3.0.0"
context = module.this.context
name = "audit-data"
}All modules accept a context input for naming and tagging. See Context and tagging below.
Validate modules locally:
# Format
terraform fmt -recursive infrastructure/modules/
# Validate a specific module
terraform -chdir=infrastructure/modules/s3-bucket init -backend=false
terraform -chdir=infrastructure/modules/s3-bucket validate
# Run all pre-commit checks
pre-commit run --all-filesUse the upgrade helper to refresh a single module after dependency changes:
./scripts/terraform/upgrade-module.sh infrastructure/modules/vpcTo refresh every module in the repository, use:
./scripts/terraform/upgrade-module.sh update-allrepo-wide mode warns before it starts because it iterates every module under infrastructure/modules.
screening-terraform-modules-aws/
├── infrastructure/
│ └── modules/ # All reusable Terraform modules
│ ├── tags/ # Foundation module (naming + tagging context)
│ │ └── exports/
│ │ └── context.tf # File copied into every other module
│ ├── s3-bucket/ # Exemplar: S3 wrapper
│ ├── iam/ # Exemplar: iam policies & roles
│ ├── secrets-manager/
│ ├── kms/
│ └── ... # Additional modules
├── scripts/ # Helper scripts (linting, hooks, Docker)
├── docs/ # ADRs, developer guides, diagrams
├── .pre-commit-config.yaml # Pre-commit hook definitions
├── scripts/githooks/generate-terraform-providers.sh # Aliased provider generation for validate
├── .tool-versions # Tool versions (asdf format, legacy)
├── mise.toml # Tool configuration (TOML format)
├── mise.lock # Locked versions for reproducible builds
├── .github/
│ └── workflows/
│ ├── stage-1-pre-commit.yml # Main CI quality gate
│ ├── cicd-1-pull-request.yaml # PR checks
│ ├── stage-1-coding-standards.yaml # Legacy (kept for rollback)
│ └── stage-1-commit.yaml # Legacy (kept for rollback)
├── tests/ # Validation tests
│ ├── test-conventional-commit.sh # Validator unit tests
│ ├── test-workflow-security.sh # Action pinning verification
│ ├── run-all-tests.sh # Test runner
│ └── README.md # Testing documentation
├── Makefile
└── VERSION
Every module must contain the following files:
infrastructure/modules/<module-name>/
├── main.tf # Resource definitions with header comment block
├── variables.tf # Inputs: types, descriptions, defaults, validation blocks
├── outputs.tf # Outputs with descriptions and stable names
├── versions.tf # required_version and provider constraints for the module
├── context.tf # Copied from tags/exports/context.tf (never edited directly)
├── locals.tf # Derived/computed values, naming logic
└── README.md # Usage docs with enforcement table and examples
Modules are thin, opinionated wrappers around community Terraform modules that enforce the NHS Screening security baseline:
################################################################
# S3 bucket
#
# Enforces:
# * Ownership: BucketOwnerEnforced
# * Encryption: SSE enabled
# * Transport: TLS-only
# * Public access: blocked at all toggles
################################################################
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "5.13.0"
create_bucket = module.this.enabled
bucket = module.this.id
# Security baseline (fixed and enforced)
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
tags = module.this.tags
}Security baseline — every module must enforce:
| Control | Requirement |
|---|---|
| Encryption at rest | KMS or service-managed; no unencrypted storage |
| Encryption in transit | TLS required where applicable |
| No public access | Blocked by default at all available toggles |
| iam least-privilege | No * actions in policies |
| Logging | Enabled where the service supports it |
| Tagging | All resources via module.this.tags |
Every module includes context.tf (copied from infrastructure/modules/tags/exports/context.tf). This instantiates module "this" which provides:
module.this.id— generated resource name (e.g.,bcss-test-account-default-my-resource)module.this.tags— standard tag map with all NHS-required labelsmodule.this.context— full context object passable to child modulesmodule.this.enabled— boolean creation gate
Rules:
- Never edit
context.tfdirectly — it is a copy fromtags/exports/context.tf. - Use
source = "../tags"(relative) within this repository. - Consumer stacks use the Git source with a pinned ref.
| Module | Wraps | Description |
|---|---|---|
api-gateway |
— | API Gateway configuration |
aws-backup-destination |
— | AWS Backup destination vault |
aws-backup-source |
— | AWS Backup source configuration |
aws-scheduler |
— | EventBridge Scheduler |
cognito |
— | Cognito user/identity pools |
cw-firehose-splunk |
— | CloudWatch to Splunk via Firehose |
ecr |
— | ECR repository |
ecs-cluster |
— | ECS Fargate cluster |
elasticache |
— | ElastiCache cluster |
github-config |
— | GitHub OIDC and runner configuration |
guardduty |
— | GuardDuty threat detection |
iam |
terraform-aws-modules/iam/aws |
iam policies and roles |
inspector |
— | Inspector vulnerability scanning |
kms |
terraform-aws-modules/kms/aws |
KMS key with policy enforcement |
lambda |
— | Lambda function |
lambda-layer |
— | Lambda layer |
license-manager |
— | License Manager configuration |
parameter_store |
— | SSM Parameter Store |
r53-healthcheck |
— | Route 53 health checks |
rds-database |
— | RDS database (logical) |
rds-gateway-ecs-task |
— | RDS gateway ECS task definition |
rds-instance |
— | RDS instance |
rds-users |
— | RDS user management |
s3 |
— | S3 (legacy) |
s3-bucket |
terraform-aws-modules/s3-bucket/aws |
S3 bucket with full security |
secrets-manager |
terraform-aws-modules/secrets-manager/aws |
Secrets Manager |
security-hub |
— | Security Hub |
sns |
Native resources | SNS topic with encryption |
sqs |
— | SQS queue |
tags |
— | Foundation: naming and tagging context |
vpc |
— | VPC |
vpce |
— | VPC endpoint (single) |
vpces |
— | VPC endpoints (multiple) |
waf |
— | WAF web ACL |
This repository uses pre-commit to run quality checks before code is committed locally, and in CI via the stage-1-pre-commit.yml GitHub Actions workflow.
The reusable workflows stage-1-coding-standards.yaml and stage-1-commit.yaml now call stage-1-pre-commit.yml for coding checks. Their legacy per-check jobs are kept disabled for fast rollback.
The PR workflow cicd-1-pull-request.yaml also includes:
- a non-blocking Conventional Commit advisory check for all commit messages in the PR
- a final
all-checks-completeaggregation job suitable for branch protection
CI tooling versions are resolved from .tool-versions via mise. Both .tool-versions and mise.toml are maintained in sync.
For Terraform-related matrix shards, CI enables TF_PLUGIN_CACHE_DIR and caches ~/.terraform.d/plugin-cache to reduce repeated provider downloads for hooks that initialise Terraform (for example terraform_validate and terraform_tflint).
# Install hooks (run once after cloning)
pre-commit install --install-hooks
pre-commit install --hook-type commit-msg
# Run all hooks against the full repo
pre-commit run --all-filesThis repository enforces 26 hooks across six categories:
| Category | Hooks | Purpose |
|---|---|---|
| Terraform (5) | terraform_fmt, terraform_providers_lock, terraform_validate, terraform_tflint, terraform_docs |
Format, lock, validate, lint, and document Terraform modules |
| File Hygiene (8) | check-added-large-files, check-merge-conflict, no-commit-to-branch, end-of-file-fixer, trailing-whitespace, check-yaml, check-case-conflict, mixed-line-ending |
Prevent commits of large files, merge conflicts, direct commits to main, and enforce line ending consistency |
| Shell Scripts (1) | shellcheck |
Lint Bash/shell scripts for errors and bad practices |
| File Formatting (4) | check-file-format, check-markdown-format, check-english-usage, check-terraform-format |
Enforce consistent formatting and British English in documentation |
| Security (3) | detect-aws-credentials, detect-private-key, scan-secrets |
CRITICAL: Prevent credentials and secrets from being committed |
| Commit Messages (1) | conventional-commit |
Enforce conventional commit format |
| Utilities (4) | generate-terraform-providers, check-executables-have-shebangs, custom githooks |
Support functions for Terraform validation and general checks |
For a comprehensive reference covering each hook, common failure scenarios, and how to fix them, see:
→ Pre-Commit Hooks Reference Guide — Detailed documentation with examples and troubleshooting
Common quick fixes:
| Issue | Fix |
|---|---|
| Terraform format mismatch | terraform fmt -recursive infrastructure/modules/ |
| Module docs out of sync | pre-commit run terraform_docs --all-files |
| Shell script errors | Review and fix; re-run pre-commit run shellcheck |
| English/spelling | Update text per Vale rules or adjust .vale.ini |
| Trailing whitespace | Auto-fixed; re-stage and commit |
| Commit message format | Use feat(scope): description per Conventional Commits |
detect-aws-credentials— prevents leaked AWS credentialsdetect-private-key— prevents leaked private keysscan-secrets— scans entire git history for secretsterraform_validate— ensures Terraform modules are syntactically validno-commit-to-branch— enforces PR workflow (no direct commits to main)
Use git commit --no-verify only in genuine emergencies, and report the issue immediately.
Commit messages must follow Conventional Commits format:
<type>(<scope>): <description>
optional body
optional footer
Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
Examples:
✅ feat(s3-bucket): add KMS encryption support
✅ fix(vpc): correct CIDR validation logic
❌ Updated stuff (too vague)
❌ fix bug (missing scope)
For more details, see Pre-Commit Hooks Reference.
| trailing-whitespace | Remove trailing whitespace |
| check-yaml | Validate YAML syntax |
| mixed-line-ending | Enforce LF line endings |
| detect-aws-credentials | Catch accidentally committed credentials |
| detect-private-key | Catch committed private keys |
| gitleaks | Scan for secrets |
| shellcheck | Lint shell scripts |
| editorconfig-checker | Enforce .editorconfig rules |
| markdownlint | Lint Markdown files |
| vale | Check English prose style |
| conventional-commit | Native bash validation script for conventional commit messages |
Commit messages must follow the Conventional Commits format:
type(scope): description
[optional body]
Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
The conventional-commit hook is implemented as a native bash script (scripts/githooks/validate-conventional-commit.sh) rather than using an external dependency. This provides:
- Supply chain security — Eliminates dependency on external pre-commit packages
- No Docker overhead — Pure bash, no container orchestration
- Fast validation — Minimal overhead compared to external tools
- Can be audited — Full source visible, easy to customise
To make writing conventional commit messages easier, install one of the following interactive helpers. These provide a guided prompt when you run git commit so you don't have to remember the format manually.
Commitizen provides an interactive CLI and can also bump versions and generate changelogs.
# Install via pip (or pipx for isolation)
pipx install commitizen
# Use instead of `git commit`
cz commitPair with commitlint for CI-level validation:
npm install -g @commitlint/cli @commitlint/config-conventional
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.jsgit-cz is a lightweight, zero-config interactive commit prompt:
# Install globally
npm install -g git-cz
# Use instead of `git commit`
git czTip
Whichever tool you choose, the conventional-commit hook in .pre-commit-config.yaml will still validate the final message at commit time, so these tools complement rather than replace the hook.
All GitHub Actions in CI/CD workflows are pinned to immutable commit SHAs rather than version tags, with version comments for human readability:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: jdx/mise-action@dba19683ed58901619b14f395a24841710cb4925 # v4.1.0This prevents tag relinking attacks and supply chain compromises. Version comments are maintained for readability when reviewing workflows.
All external pre-commit repositories are pinned to commit SHAs:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: d61ded22bf9aa0f757303ebcbb0d6d71c4b54015 # v1.106.0Custom hooks are implemented as local scripts where practical:
scripts/githooks/validate-conventional-commit.sh— Native bash conventional commit validation scriptscripts/githooks/generate-terraform-providers.sh— Local provider alias generator
This reduces external dependencies and improves supply chain security.
To verify pinning compliance:
make test-workflow-pinning
bash tests/test-workflow-security.sh verbose- Create a feature branch from
main. - Run
pre-commit run --all-filesbefore pushing. - Ensure commit messages follow the Conventional Commits format.
- Open a pull request — the
pre-commit.ymlworkflow will validate all hooks pass. - All modules must include the required files listed in Module layout and meet the security baseline.
For detailed module authoring guidance, see infrastructure/AGENTS.md.
Raise an issue or open a GitHub discussion on this repository.
Unless stated otherwise, the codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation.
Any HTML or Markdown documentation is © Crown Copyright and available under the terms of the Open Government Licence v3.0.