← All skills
terraform-patterns
Use when designing Terraform modules, managing state backends, reviewing Terraform security, planning multi-region deployments, or following IaC best practices.
About this skill
Use when designing Terraform modules, managing state backends, reviewing Terraform security, planning multi-region deployments, or following IaC best practices.
SKILL.md
terraform-patterns.SKILL.mdPlace it in your Syntic skills folder. For Syntic Code, install in your local skills directory, review the contents and run it in a controlled environment first.
---
name: terraform-patterns
description: Use when designing Terraform modules, managing state backends, reviewing Terraform security, planning multi-region deployments, or following IaC best practices.
category: Engineering
version: 1.0.0
tools: []
---
# Terraform Patterns
Predictable infrastructure. Secure state. Modules that compose. No drift.
An opinionated Terraform methodology that turns sprawling HCL into well-structured, secure, production-grade infrastructure code: module design, state management, provider patterns, security hardening, and CI/CD integration. Not a Terraform tutorial — a set of concrete decisions about how to write infrastructure code that doesn't break at 3 AM.
Applies whenever `.tf` files, HCL, Terraform modules, state management, or provider configuration come up, or the user wants to provision infrastructure with Terraform.
## Code Review Checklist
**Module structure:** variables have descriptions and type constraints; outputs expose only what consumers need; resources use consistent naming (`{provider}_{type}_{purpose}`); locals are used for computed values and DRY expressions; nothing is hardcoded — everything is parameterized or in locals.
**State and backend:** a remote backend is configured (S3, GCS, Azure Blob, Terraform Cloud); state locking is enabled (DynamoDB for S3, native for others); state is encrypted at rest; no secrets are stored in state, or state access is restricted; workspaces or directory isolation separate environments.
**Providers:** version constraints use the pessimistic operator (`~> 5.0`); a `required_providers` block exists inside `terraform {}`; provider aliases handle multi-region or multi-account; child modules never configure providers themselves.
**Security:** no hardcoded secrets, keys, or passwords; IAM follows least privilege; encryption is enabled for storage, databases, and secrets; security groups aren't overly permissive (no `0.0.0.0/0` ingress on sensitive ports); sensitive variables are marked `sensitive = true`.
## Module Design Checklist
**Structure:** `main.tf` (primary resources), `variables.tf` (all inputs with descriptions and types), `outputs.tf` (all outputs with descriptions), `versions.tf` (the `terraform {}` block with `required_providers`), `locals.tf` (computed values and naming conventions), `data.tf` (data sources), `README.md` (usage examples and variable documentation).
**Variables:** every variable has a description, type, and validation where applicable; sensitive values are marked `sensitive = true`; defaults are provided for optional settings; related settings use object types (`variable "config" { type = object({...}) }`); constraints use `validation { condition = ... }` blocks.
**Outputs:** expose IDs, ARNs, and endpoints that consumers actually need; every output has a description; sensitive outputs are marked `sensitive = true`; never output an entire resource, only the specific attributes needed.
**Composition:** the root module calls child modules; child modules never call other child modules; values are passed explicitly, no hidden data-source lookups inside child modules; provider configuration lives only in the root module; reference children with `module "name" { source = "./modules/name" }`.
## Security Audit Reference
**Code-level, by severity:** Critical — hardcoded secrets in `.tf` files (fix: variables with `sensitive = true` or a vault), IAM policy with `*` actions (fix: scope to specific actions/resources), a security group open to `0.0.0.0/0` on port 22/3389 (fix: restrict to known CIDR blocks or use SSM/bastion). High — an S3 bucket without encryption (`server_side_encryption_configuration`), an S3 bucket with public access (`aws_s3_bucket_public_access_block`), RDS without encryption (`storage_encrypted = true`), RDS publicly accessible (`publicly_accessible = false`). Medium — CloudTrail not enabled, missing `prevent_destroy` on stateful resources, secret-holding variables missing `sensitive = true`.
**State-level, by severity:** Critical — a local state file (migrate to a remote backend with encryption). High — remote state without encryption (enable SSE-S3 or KMS), no state locking (DynamoDB for S3, native for TF Cloud). Medium — state accessible to all team members (restrict via IAM policies or TF Cloud teams).
## Module Design Patterns
**Flat module** (small/medium projects, <20 resources, one team owns everything): a single directory with `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`, `terraform.tfvars` (not committed), and `backend.tf`.
**Nested modules** (medium/large projects, multiple environments, team collaboration): an `environments/{dev,staging,prod}/` tree where each environment calls into a shared `modules/{networking,compute,database}/` tree with environment-specific parameters and state backends.
**Mono-repo with Terragrunt** (large-scale, many environments, DRY configuration, team-level isolation): a root `terragrunt.hcl`, reusable `modules/{vpc,eks,rds}/`, and per-environment directories where each module invocation gets its own `terragrunt.hcl` with environment overrides.
## Provider Configuration Patterns
**Version pinning:** pin `required_version` on the Terraform binary itself, and pin each provider with the pessimistic operator, e.g. `version = "~> 5.0"` allows 5.x but blocks 6.0.
**Multi-region with aliases:** declare a default provider block for the primary region and an aliased block (`alias = "west"`) for a secondary region; resources that need the secondary region reference `provider = aws.west` explicitly.
**Multi-account with assume role:** an aliased provider block can assume a cross-account IAM role via `assume_role { role_arn = "..." }`, letting one root module provision into another AWS account.
**Multi-cloud:** a single root module can declare `required_providers` for AWS, Azure, and GCP simultaneously and configure each (`provider "aws"`, `provider "azurerm"` with a `features {}` block, `provider "google"`) when a deployment genuinely spans clouds.
## State Management Decision Tree
Single developer, small project -> local state, but migrate to remote as soon as possible. Otherwise: on Terraform Cloud/Enterprise -> use its native backend (built-in locking, encryption, RBAC). On AWS -> S3 + DynamoDB (encryption, locking, versioning). On GCP -> a GCS bucket (native locking, encryption). On Azure -> Azure Blob Storage (native locking, encryption). Otherwise -> Consul or a PostgreSQL backend.
For environment isolation, prefer separate state files per environment — either separate directories (`dev/`, `staging/`, `prod/`) or Terraform workspaces (simpler but less isolation). Never use a single state file for all environments.
## CI/CD Integration Patterns
**Plan/apply split:** run `terraform init`, `terraform validate`, and `terraform plan -out=tfplan` on pull requests (post the plan as a PR comment for review); run `terraform apply -auto-approve` only on pushes to the main branch, gated behind a protected `production` environment.
**Drift detection:** on a schedule (e.g. weekday mornings), run `terraform plan -detailed-exitcode`; exit code 2 means drift was detected between the configured state and real infrastructure — alert the team (Slack, PagerDuty) rather than silently ignoring it.
## Proactive Triggers
Flag these without being asked: no remote backend configured (migrate to S3/GCS/Azure Blob with locking and encryption); a provider without a version constraint (add `version = "~> X.0"` to prevent breaking upgrades); hardcoded secrets in `.tf` files (use `sensitive = true` variables, or integrate Vault/SSM); an IAM policy with `"Action": "*"` (scope to specific actions — no wildcard actions in production); a security group open to `0.0.0.0/0` on SSH/RDP (restrict to a bastion CIDR or use SSM Session Manager); no state locking (enable a DynamoDB table for S3 backends, or use TF Cloud); resources without tags (add `default_tags` in the provider block — tags are mandatory for cost tracking); missing `prevent_destroy` on databases/storage (add a `lifecycle` block to prevent accidental deletion).
Install
Syntic App
- 1Create a dedicated folder for this skill in your local skills library.
- 2Place SKILL.md into that folder.
- 3Restart Syntic and invoke this skill on matching tasks.
Syntic Code (CLI)
- 1Save SKILL.md in your local Syntic Code skills directory.
- 2Keep related files in the same skill folder.
- 3Run in a safe environment and validate outputs.