Infra terraform review
Skill ivanshamaev/de-agent-skills/group_skills/infra_dataops_group_skills/infra_terraform_review
Профессиональные Data Engineering Agent Skills для разработки AI Agentic Data Platform
npx -y skills add ivanshamaev/de-agent-skills --skill infra_terraform_reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Terraform code review — module structure (main/variables/outputs/modules layout), variable validation blocks, type constraints, sensitive variables, locals vs variables, resource naming conventions, provider version pinning, data sources vs resources, module composition patterns, state management (remote backend S3/GCS), workspace strategy, dependency management (depends_on vs implicit), terraform fmt/validate/tflint checks, DRY with Terragrunt
SKILL.md
10.1 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
Terraform Code Review
When to Use
- Reviewing a Terraform PR for a data platform component
- Auditing a new module before it goes to production
- Establishing team coding standards for IaC
- Diagnosing state drift or dependency issues
- Evaluating module reusability and composability
Module Structure
Recommended File Layout
modules/
s3-data-lake/
main.tf # primary resource definitions
variables.tf # all input variables with types + validation
outputs.tf # all outputs with descriptions
versions.tf # required_providers with version constraints
README.md # auto-generated with terraform-docs
envs/
dev/
main.tf # module calls + env-specific overrides
terraform.tfvars # variable values (not secrets)
backend.tf # remote state config
prod/
main.tf
terraform.tfvars
backend.tf
versions.tf — Pin Providers
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # allows 5.x but not 6.x
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.20.0, < 3.0.0"
}
}
}
variables.tf Patterns
Variable Validation
variable "environment" {
type = string
description = "Deployment environment. Controls resource sizing and retention."
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging, or prod."
}
}
variable "bucket_name" {
type = string
description = "S3 bucket name for the data lake landing zone."
validation {
condition = can(regex("^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", var.bucket_name))
error_message = "Bucket name must be 3–63 lowercase letters, digits, or hyphens."
}
}
variable "retention_days" {
type = number
description = "S3 lifecycle expiration for raw data in days."
default = 90
validation {
condition = var.retention_days >= 1 && var.retention_days <= 3650
error_message = "retention_days must be between 1 and 3650."
}
}
variable "db_password" {
type = string
description = "RDS master password. Injected from secrets manager at plan time."
sensitive = true # never shown in plan output or logs
}
variable "tags" {
type = map(string)
description = "Resource tags applied to all resources."
default = {}
}
Locals for Derived Values
# Use locals, not variables, for computed/derived values
locals {
name_prefix = "${var.project}-${var.environment}"
# Merge standard tags with caller-supplied tags
common_tags = merge(
{
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
},
var.tags
)
# Conditional sizing
node_count = var.environment == "prod" ? 3 : 1
}
outputs.tf Patterns
output "bucket_arn" {
description = "ARN of the data lake S3 bucket."
value = aws_s3_bucket.data_lake.arn
}
output "bucket_name" {
description = "Name of the data lake S3 bucket."
value = aws_s3_bucket.data_lake.id
}
output "kms_key_id" {
description = "KMS key ID used for bucket encryption."
value = aws_kms_key.data_lake.key_id
sensitive = true
}
Resource Naming Conventions
# Pattern: <project>-<environment>-<component>[-<index>]
resource "aws_s3_bucket" "data_lake" {
bucket = "${local.name_prefix}-data-lake"
tags = local.common_tags
}
resource "aws_iam_role" "airflow_task" {
name = "${local.name_prefix}-airflow-task-role"
tags = local.common_tags
}
# For multi-instance: use for_each over count (preserves keys on deletion)
resource "aws_s3_bucket" "zones" {
for_each = toset(["raw", "processed", "curated"])
bucket = "${local.name_prefix}-${each.key}"
tags = merge(local.common_tags, { Zone = each.key })
}
Remote State Backend
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "data-platform/prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "alias/terraform-state"
dynamodb_table = "terraform-state-lock" # prevents concurrent applies
}
}
State Output Cross-Module Reference
# Read output from another module's state
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-company-terraform-state"
key = "networking/prod/terraform.tfstate"
region = "us-east-1"
}
}
# Use it
resource "aws_db_subnet_group" "main" {
subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
Data Sources vs Resources
# data source: read existing infra (don't manage it)
data "aws_vpc" "main" {
filter {
name = "tag:Environment"
values = [var.environment]
}
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# resource: manage lifecycle (create/update/destroy)
resource "aws_subnet" "private" {
vpc_id = data.aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
depends_on — Use Sparingly
# Implicit dependency (preferred): reference the resource attribute
resource "aws_iam_role_policy" "airflow" {
role = aws_iam_role.airflow.name # implicit dependency on aws_iam_role.airflow
policy = data.aws_iam_policy_document.airflow.json
}
# Explicit depends_on: only when Terraform can't infer the dependency
resource "aws_s3_bucket_policy" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
policy = data.aws_iam_policy_document.data_lake.json
depends_on = [aws_s3_bucket_public_access_block.data_lake]
}
Linting and Validation
# Format check (run in CI)
terraform fmt -check -recursive
# Syntax + validity
terraform validate
# tflint — catches provider-specific errors and deprecations
tflint --init
tflint --recursive
# tflint config
cat .tflint.hcl
# .tflint.hcl
plugin "aws" {
enabled = true
version = "0.27.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "terraform_required_version" { enabled = true }
rule "terraform_required_providers" { enabled = true }
rule "terraform_naming_convention" {
enabled = true
module {
format = "snake_case"
}
}
rule "aws_instance_invalid_type" { enabled = true }
# Generate docs from variables.tf + outputs.tf
terraform-docs markdown table . > README.md
Terragrunt for DRY Configs
# terragrunt.hcl (root)
locals {
account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
account_id = local.account_vars.locals.account_id
environment = local.env_vars.locals.environment
aws_region = local.env_vars.locals.aws_region
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "tfstate-${local.account_id}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = local.aws_region
encrypt = true
dynamodb_table = "terraform-locks"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
}
EOF
}
# envs/prod/s3-data-lake/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/s3-data-lake"
}
inputs = {
environment = "prod"
retention_days = 365
bucket_name = "my-company-prod-data-lake"
}
Review Checklist
[ ] Provider versions pinned (~> minor, not =)
[ ] required_version set for Terraform itself
[ ] All variables have type + description + validation
[ ] Sensitive variables marked sensitive = true
[ ] Derived values in locals{}, not repeated inline
[ ] Resources use for_each instead of count where key stability matters
[ ] Remote state backend configured with encryption + lock
[ ] No hardcoded account IDs, regions, or passwords
[ ] common_tags applied to all resources
[ ] terraform fmt passes cleanly
[ ] terraform validate passes
[ ] tflint passes with AWS plugin
[ ] No overly broad IAM policies (*:* actions or resources)
[ ] outputs.tf covers all values a caller might need
[ ] README generated by terraform-docs
Anti-Patterns
countfor named resources — deleting mid-list shifts all indices; usefor_each = toset(...)instead.- Hardcoded ARNs and account IDs — breaks across accounts; use
data "aws_caller_identity"anddata "aws_region". - No provider version constraints — a major upgrade can silently break all resources; always pin with
~>. - Storing secrets in tfvars — committed to git and logged in state; inject via environment variable or secrets manager.
- Giant
main.tffile — hard to review; split intos3.tf,iam.tf,rds.tfper resource type. terraform applywithout plan review — always runplanfirst, review changes, thenapply; automate this in CI.
References
- Terraform module structure:
developer.hashicorp.com/terraform/language/modules/develop/structure - Variable validation:
developer.hashicorp.com/terraform/language/values/variables - tflint:
github.com/terraform-linters/tflint - terraform-docs:
terraform-docs.io - Terragrunt:
terragrunt.gruntwork.io/docs/ - Related skills:
[[infra-terraform-security-scan]],[[infra-terraform-cost-estimator]],[[github-actions-dataops]]
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.