
Terraform Validator
- 399 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
terraform-validator is a DevOps skill that reviews Terraform plans and HCL for policy violations, unsafe defaults, missing backends, and provider misconfigurations before merge or apply.
About
terraform-validator is a Claude Code skill for infrastructure engineers who need automated Terraform review before merge or terraform apply in CI/CD pipelines. The skill inspects Terraform plan output and HCL source for policy violations, unsafe defaults, missing remote backends, and provider misconfigurations that commonly slip past manual review. Developers reach for terraform-validator when pull requests touch modules, state backends, or provider blocks and they want a consistent gate before production applies. It fits teams running Terraform in GitHub Actions, GitLab CI, or local pre-commit hooks who need repeatable infra policy checks without writing custom OPA or Sentinel rules from scratch.
- Flags invalid HCL and provider constraints
- Surfaces risky IAM and networking patterns
- Supports CI pre-apply policy checks
- Reduces state drift and module breakage
- Complements generator for IaC quality loops
Terraform Validator by the numbers
- 399 all-time installs (skills.sh)
- Ranked #391 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill terraform-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 399 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you catch Terraform policy violations before apply?
Review Terraform plans and HCL for policy violations, unsafe defaults, missing backends, and provider misconfigurations before merge or terraform apply in CI/CD.
Who is it for?
DevOps and platform engineers who run Terraform in CI/CD and need consistent pre-merge infrastructure policy review.
Skip if: Teams not using Terraform or those who already enforce equivalent checks exclusively through OPA, Sentinel, or Checkov with no gap to fill.
When should I use this skill?
A pull request modifies Terraform HCL, generates a terraform plan, or is about to run terraform apply in CI.
What you get
Annotated review of Terraform plans and HCL files listing policy violations, unsafe defaults, backend gaps, and provider misconfigurations flagged for fix.
- Terraform review findings
- Policy violation report
Files
Terraform Validator
Comprehensive toolkit for validating, linting, and testing Terraform configurations with automated workflows for syntax validation, security scanning, and intelligent documentation lookup.
⚠️ Critical Requirements Checklist
STOP: You MUST complete these steps in order. Do NOT skip any REQUIRED step.
| Step | Action | Required |
|---|---|---|
| 1 | Run bash scripts/extract_tf_info_wrapper.sh <path> | ✅ REQUIRED |
| 2 | Context7 lookup for ALL providers (explicit AND implicit); WebSearch fallback if not found | ✅ REQUIRED |
| 3 | READ references/security_checklist.md | ✅ REQUIRED |
| 4 | READ references/best_practices.md | ✅ REQUIRED |
| 5 | Run terraform fmt | ✅ REQUIRED |
| 6 | Run tflint (or note as skipped if unavailable) | Recommended |
| 7 | Run terraform init (if not initialized) | ✅ REQUIRED |
| 8 | Run terraform validate | ✅ REQUIRED |
| 9 | Run bash scripts/run_checkov.sh <path> | ✅ REQUIRED |
| 10 | Cross-reference findings with security_checklist.md sections | ✅ REQUIRED |
| 11 | Generate report citing reference files | ✅ REQUIRED |
| 12 | Run regression tests (bash tests/test_regression.sh) | ✅ REQUIRED |
| 13 | Run lightweight CI checks (bash -n, py_compile, smoke) | ✅ REQUIRED |
IMPORTANT: Steps 3-4 (reading reference files) must be completed BEFORE running security scans. The reference files contain remediation patterns that MUST be cited in your report.
Context7 Fallback: If Context7 does not have a provider (common for: random, null, local, time, tls), use WebSearch: "terraform-provider-{name} hashicorp documentation"When to Use This Skill
- Working with Terraform files (
.tf,.tfvars,.tfstate) - Validating Terraform configuration syntax and structure
- Linting and formatting HCL code
- Performing dry-run testing with
terraform plan - Debugging Terraform errors or misconfigurations
- Understanding custom Terraform providers or modules
- Security validation of Terraform configurations
External Documentation
| Tool | Documentation |
|---|---|
| Terraform | developer.hashicorp.com/terraform |
| TFLint | github.com/terraform-linters/tflint |
| Checkov | checkov.io |
| Trivy | aquasecurity.github.io/trivy |
Validation Workflow
IMPORTANT: Follow this workflow in order. Each step is REQUIRED unless explicitly marked optional.
1. Identify Terraform files in scope
├─> Single file, directory, or multi-environment
2. Extract Provider/Module Info (REQUIRED)
├─> MUST run: bash scripts/extract_tf_info_wrapper.sh <path>
├─> Parse output for providers and modules
└─> Use for Context7 documentation lookup
3. Lookup Provider Documentation (REQUIRED)
├─> For EACH provider detected:
│ ├─> mcp__context7__resolve-library-id with "terraform-provider-{name}"
│ ├─> mcp__context7__query-docs for version-specific guidance
│ └─> If NOT found in Context7: WebSearch fallback (see below)
└─> Note any custom/private providers for WebSearch
4. Read Reference Files (REQUIRED before validation)
├─> MUST READ: references/security_checklist.md (before security scan)
├─> MUST READ: references/best_practices.md (for structure validation)
└─> Reference common_errors.md if errors occur
5. Format and Lint (REQUIRED)
├─> MUST run: terraform fmt -recursive (auto-fix formatting)
├─> MUST run: terraform fmt -check -recursive (verify no drift)
├─> RUN: tflint (or note as skipped if unavailable)
└─> Report formatting issues
6. Syntax Validation (REQUIRED)
├─> MUST run: terraform init (if not initialized)
├─> MUST run: terraform validate
└─> Report syntax errors (consult common_errors.md)
7. Security Scanning (REQUIRED)
├─> MUST run: bash scripts/run_checkov.sh <path>
├─> Analyze policy violations against security_checklist.md
└─> Suggest remediations from reference
8. Dry-Run Testing (if credentials available)
├─> terraform plan
├─> Analyze planned changes
└─> Report potential issues
9. Regression and Wrapper Determinism Checks (REQUIRED)
├─> MUST run: bash tests/test_regression.sh
├─> Confirms parser error handling returns non-zero
├─> Confirms implicit provider detection for docs lookup
├─> Confirms wrapper argument handling is deterministic
└─> Confirms checkov wrapper preserves scanner exit code
10. Lightweight CI Checks (REQUIRED)
├─> MUST run: bash -n scripts/*.sh
├─> MUST run: python3 -m py_compile scripts/*.py
├─> MUST run: smoke check for extract wrapper on sample fixture
└─> Record command outputs and exit codes
11. Generate Comprehensive Report
├─> Include all findings with severity
├─> Reference best_practices.md for recommendations
└─> Offer to fix issues if appropriateRequired Reference File Reading
You MUST read these reference files during validation:
| When | Reference File | Action |
|---|---|---|
| Before security scan | references/security_checklist.md | Read to understand security checks and remediation patterns |
| During validation | references/best_practices.md | Read to validate project structure, naming, and patterns |
| If errors occur | references/common_errors.md | Read to find solutions for specific error messages |
| If using Terraform 1.10+ | references/advanced_features.md | Read to understand ephemeral values, actions, list resources |
Required Script Usage
You MUST use these wrapper scripts instead of calling tools directly:
| Task | Script | Command |
|---|---|---|
| Extract provider/module info | extract_tf_info_wrapper.sh | bash scripts/extract_tf_info_wrapper.sh <path> |
| Run security scan | run_checkov.sh | bash scripts/run_checkov.sh <path> |
| Install checkov (if missing) | install_checkov.sh | bash scripts/install_checkov.sh install |
Note:extract_tf_info_wrapper.shautomatically handles the python-hcl2 dependency. If system Python lackspython-hcl2, it creates/reuses a cached virtual environment under~/.cache/terraform-validator/by default.
Script Run Context (REQUIRED)
- Default working directory:
devops-skills-plugin/skills/terraform-validator - If running from elsewhere, use absolute script paths:
bash /absolute/path/to/terraform-validator/scripts/extract_tf_info_wrapper.sh <path>bash /absolute/path/to/terraform-validator/scripts/run_checkov.sh <path>bash /absolute/path/to/terraform-validator/scripts/install_checkov.sh install
Context7 Provider Documentation Lookup (REQUIRED)
For EVERY provider detected, you MUST lookup documentation via Context7:
1. Run extract_tf_info_wrapper.sh to get provider list
2. For each provider (e.g., "aws", "google", "azurerm"):
a. Call: mcp__context7__resolve-library-id with "terraform-provider-{name}"
b. Call: mcp__context7__query-docs with the resolved ID
c. Note version-specific features and constraints
3. Include relevant provider guidance in validation reportExample for AWS provider:
mcp__context7__resolve-library-id("terraform-provider-aws")
mcp__context7__query-docs(context7CompatibleLibraryID, "best practices")Context7 Fallback to WebSearch (REQUIRED)
If Context7 does not find a provider, you MUST fall back to WebSearch:
1. If mcp__context7__resolve-library-id returns no results or provider not found:
a. Use WebSearch with query: "terraform-provider-{name} hashicorp documentation"
b. For specific version: "terraform-provider-{name} {version} documentation site:registry.terraform.io"
2. Common providers NOT in Context7 (use WebSearch directly):
- random (hashicorp/random)
- null (hashicorp/null)
- local (hashicorp/local)
- time (hashicorp/time)
- tls (hashicorp/tls)
3. Document in report: "Provider docs via WebSearch (not in Context7)"WebSearch Fallback Example:
# If Context7 fails for random provider:
WebSearch("terraform-provider-random hashicorp documentation site:registry.terraform.io")Note: HashiCorp utility providers (random, null, local, time, tls, archive, external, http) may not be indexed in Context7. Always fall back to WebSearch for these.
Detecting Implicit Providers (REQUIRED)
IMPORTANT: Providers can be used without being declared in required_providers. You MUST detect ALL providers:
Detection Methods
1. Explicit Providers: Listed in required_providers block (from extract_tf_info_wrapper.sh output) 2. Implicit Providers: Inferred from resource type prefixes
Common Implicit Provider Patterns
| Resource Type Prefix | Provider Name | Context7 Lookup |
|---|---|---|
random_* | random | terraform-provider-random |
null_* | null | terraform-provider-null |
local_* | local | terraform-provider-local |
tls_* | tls | terraform-provider-tls |
time_* | time | terraform-provider-time |
archive_* | archive | terraform-provider-archive |
http (data source) | http | terraform-provider-http |
external (data source) | external | terraform-provider-external |
Workflow for Complete Provider Detection
1. Parse extract_tf_info_wrapper.sh output
2. Get providers from "providers" array (explicit)
3. Get resources from "resources" array
4. For EACH resource type:
a. Extract prefix (e.g., "random" from "random_id")
b. Check if already in providers list
c. If NOT in providers: add as implicit provider
5. Perform Context7 lookup for ALL providers (explicit + implicit)Example
If extract_tf_info_wrapper.sh returns:
{
"providers": [{"name": "aws", ...}],
"resources": [
{"type": "aws_instance", ...},
{"type": "random_id", ...}
]
}You MUST lookup BOTH:
terraform-provider-aws(explicit)terraform-provider-random(implicit - detected fromrandom_idresource)
Quick Reference Commands
Format and Lint
# Check formatting (dry-run)
terraform fmt -check -recursive .
# Apply formatting
terraform fmt -recursive .
# Run tflint (requires .tflint.hcl config)
tflint --init # Install plugins
tflint --recursive # Lint all modules
tflint --format compact # Compact outputTFLint Configuration: See TFLint Ruleset documentation for plugin setup.
Validate Configuration
# Initialize (downloads providers and modules)
terraform init
# Validate syntax
terraform validate
# Validate with JSON output
terraform validate -jsonSecurity Scanning
MUST use wrapper script:
# Use the wrapper script (REQUIRED)
bash scripts/run_checkov.sh ./terraform
# With specific options
bash scripts/run_checkov.sh -f json ./terraform
bash scripts/run_checkov.sh --compact ./terraformDetailed Security Scanning: You MUST read references/security_checklist.md before running security scans to understand the checks and remediation patterns.Security Finding Cross-Reference (REQUIRED)
When reporting security findings, you MUST cite specific sections from `security_checklist.md`:
Cross-Reference Mapping
| Checkov Check Pattern | security_checklist.md Section |
|---|---|
CKV_AWS_24 (SSH open) | "Overly Permissive Security Groups" |
CKV_AWS_260 (HTTP open) | "Overly Permissive Security Groups" |
CKV_AWS_16 (RDS encryption) | "Encryption at Rest" |
CKV_AWS_17 (RDS public) | "RDS Databases" |
CKV_AWS_130 (public subnet) | "Network Security" |
CKV_AWS_53-56 (S3 public access) | "Public S3 Buckets" |
CKV_AWS_* (IAM) | "IAM Security" |
CKV_AWS_79 (IMDSv1) | "ECS/EKS" |
| Hardcoded passwords | "Hardcoded Credentials" |
| Sensitive outputs | "Sensitive Output Exposure" |
Report Template for Security Findings
### Security Issue: [Check ID]
**Finding:** [Description from checkov]
**Resource:** [Resource name and file:line]
**Severity:** [HIGH/MEDIUM/LOW]
**Reference:** security_checklist.md - "[Section Name]"
**Remediation Pattern:**
[Copy relevant code example from security_checklist.md]
**Recommended Fix:**
[Specific fix for this configuration]Example Cross-Referenced Report
````markdown
Security Issue: CKV_AWS_24
Finding: Security group allows SSH from 0.0.0.0/0 Resource: aws_security_group.web (main.tf:47-79) Severity: HIGH
Reference: security_checklist.md - "Overly Permissive Security Groups"
Remediation Pattern (from reference):
variable "admin_cidr" {
description = "CIDR block for admin access"
type = string
}
resource "aws_security_group" "app" {
ingress {
description = "SSH from admin network only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_cidr]
}
}Recommended Fix: Replace cidr_blocks = ["0.0.0.0/0"] with a variable or specific CIDR range. ````
Dry-Run Testing
# Generate execution plan
terraform plan
# Save plan to file
terraform plan -out=tfplan
# Plan with specific var file
terraform plan -var-file="production.tfvars"
# Plan with target resource
terraform plan -target=aws_instance.examplePlan Output Symbols:
+Resources to be created-Resources to be destroyed~Resources to be modified-/+Resources to be replaced
Handling Missing Tools
When validation tools are not installed, follow this recovery workflow:
Recovery Workflow (REQUIRED)
1. Detect missing tool
2. Inform user what is missing and why it's needed
3. Provide installation command
4. ASK user: "Would you like me to install [tool] and continue?"
5. If yes: Run installation and RERUN the validation step
6. If no: Note as skipped in report, continue with available toolsTool-Specific Recovery
If checkov is missing:
1. Inform: "Checkov is not installed. It's required for security scanning."
2. Ask: "Would you like me to install it? I'll use: bash scripts/install_checkov.sh install"
3. If yes: Run install script, then rerun security scanIf tflint is missing:
1. Inform: "TFLint is not installed. It provides advanced linting beyond terraform validate."
2. Ask: "Would you like me to install it?"
3. Provide: brew install tflint (macOS) or installation script (Linux)If python-hcl2 is missing:
The extract_tf_info_wrapper.sh script handles this automatically by creating
or reusing a cached venv. No user action required.Required tools: terraform fmt, terraform init, terraform validate Required for full security validation: checkov Optional but recommended: tflint
Scripts
| Script | Purpose | Usage |
|---|---|---|
extract_tf_info_wrapper.sh | Parse Terraform files for providers/modules (auto-handles dependencies) | bash scripts/extract_tf_info_wrapper.sh <path> |
extract_tf_info.py | Core parser (requires python-hcl2) | Use wrapper instead |
run_checkov.sh | Wrapper for Checkov scans with enhanced output | bash scripts/run_checkov.sh <path> |
install_checkov.sh | Install Checkov in isolated venv | bash scripts/install_checkov.sh install |
Reference Documentation
MUST READ during validation workflow:
| Reference | When to Read | Content |
|---|---|---|
references/security_checklist.md | Before security scan | Security validation, Checkov/Trivy usage, common policies, remediation patterns |
references/best_practices.md | During validation | Project structure, naming conventions, module design, state management |
references/common_errors.md | When errors occur | Error database with causes and solutions |
references/advanced_features.md | If Terraform >= 1.10 | Ephemeral values (1.10+), Actions (1.14+), List Resources (1.14+) |
Workflow Examples
Example 1: Validate Single File
1. MUST: bash scripts/extract_tf_info_wrapper.sh main.tf
2. MUST: Context7 lookup for each provider detected
3. MUST: Read references/security_checklist.md
4. MUST: Read references/best_practices.md
5. RUN: terraform fmt -check main.tf
6. RUN: terraform init (if needed) && terraform validate
7. MUST: bash scripts/run_checkov.sh -f json main.tf
8. Report issues with remediation from references
9. If custom providers: WebSearch for documentationExample 2: Full Module Validation
1. Identify all .tf files in directory
2. MUST: bash scripts/extract_tf_info_wrapper.sh ./modules/vpc/
3. MUST: Context7 lookup for ALL providers
4. MUST: Read references/security_checklist.md
5. MUST: Read references/best_practices.md
6. RUN: terraform fmt -recursive
7. RUN: tflint --recursive (or note as skipped if unavailable)
8. RUN: terraform init && terraform validate
9. MUST: bash scripts/run_checkov.sh ./modules/vpc/
10. Analyze findings against security_checklist.md
11. Validate structure against best_practices.md
12. Provide comprehensive report with referencesExample 3: Production Dry-Run
1. Verify terraform initialized
2. MUST: Read references/security_checklist.md (production focus)
3. RUN: terraform plan -var-file="production.tfvars"
4. Analyze for unexpected changes
5. Highlight create/modify/destroy operations
6. Flag security concerns (compare with security_checklist.md)
7. Recommend whether safe to applyAdvanced Features
Terraform 1.10+ introduces ephemeral values for secure secrets management. Terraform 1.14+ adds Actions for imperative operations and List Resources for querying infrastructure.
MUST READ: references/advanced_features.md when:
- Terraform version >= 1.10 is detected
- Configuration uses
ephemeralblocks - Configuration uses
actionblocks - Configuration uses
.tfquery.hclfiles
Integration with Other Skills
- k8s-yaml-validator - For Terraform Kubernetes provider validation
- helm-validator - When Terraform manages Helm releases
- k8s-debug - For debugging infrastructure provisioned by Terraform
Notes
- Always run validation in order: extract info → lookup docs → read refs → format → lint → validate → security → plan
- MUST use wrapper scripts for extract_tf_info and checkov
- MUST run
bash tests/test_regression.shafter script changes - MUST run lightweight CI checks:
bash -n scripts/*.shandpython3 -m py_compile scripts/*.py - MUST read reference files before relevant validation steps
- MUST lookup provider docs via Context7 for ALL providers
- MUST offer recovery/rerun when tools are missing
- Never commit without running terraform fmt
- Always review plan output before applying
- Use version constraints for all providers and modules
- Use remote state for team collaboration
- Enable state locking to prevent concurrent modifications
Done Criteria
- Validation instructions are executable end-to-end with one deterministic command path.
- Wrapper scripts behave predictably in both success and failure paths (including propagated non-zero exits).
- Regression tests cover parser error handling, implicit provider detection, wrapper argument handling, and checkov exit-code behavior.
- Lightweight CI checks (
bash -n,py_compile, smoke checks) pass before final reporting.
Terraform Advanced Features
Modern Terraform features for enhanced infrastructure management. This reference covers features introduced in Terraform 1.10+.
Official Documentation: developer.hashicorp.com/terraform
Ephemeral Values and Write-Only Arguments (1.10+)
Purpose: Securely manage sensitive data like passwords and tokens without storing them in Terraform state or plan files.
Overview
Ephemeral values are temporary values that exist only during a Terraform operation. They are never persisted to state, plan files, or logs. This is a major security improvement for secrets management.
Ephemeral Resources
Ephemeral resources generate temporary values that don't persist:
# Generate a temporary password - NOT stored in state
ephemeral "random_password" "db_password" {
length = 16
override_special = "!#$%&*()-_=+[]{}<>:?"
}
# Use with AWS Secrets Manager
resource "aws_secretsmanager_secret" "db_password" {
name = "db_password"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string_wo = ephemeral.random_password.db_password.result
secret_string_wo_version = 1
}Write-Only Arguments (1.11+)
Write-only arguments accept values but never persist them:
# Use ephemeral password with write-only argument
ephemeral "random_password" "db_password" {
length = 16
}
resource "aws_db_instance" "example" {
instance_class = "db.t3.micro"
allocated_storage = "5"
engine = "postgres"
username = "admin"
skip_final_snapshot = true
# Write-only argument - password is NOT stored in state
password_wo = ephemeral.random_password.db_password.result
password_wo_version = 1 # Increment to trigger password update
}Key Concepts
| Concept | Version | Description |
|---|---|---|
ephemeral block | 1.10+ | Defines resources that are never stored in state |
| Ephemeral variables | 1.10+ | Variables marked ephemeral = true |
| Ephemeral outputs | 1.10+ | Outputs marked ephemeral = true |
| Write-only arguments | 1.11+ | Resource arguments ending in _wo that accept ephemeral values |
_wo_version arguments | 1.11+ | Version tracking to prevent updates on every run |
ephemeralasnull function | 1.10+ | Convert ephemeral to null for conditional logic |
Ephemeral Input Variables
variable "api_token" {
type = string
sensitive = true
ephemeral = true # Value is not stored in state
}Ephemeral Outputs
output "generated_password" {
value = ephemeral.random_password.main.result
ephemeral = true # Value is not stored in state
}Provider Support
Ephemeral resources are available in:
- AWS Provider (secrets, passwords)
- Azure Provider
- Kubernetes Provider
- Random Provider (
random_password) - Google Cloud Provider
Security Best Practices
1. Always use ephemeral for secrets - passwords, API keys, tokens 2. Use write-only arguments - for database passwords, secret values 3. Increment version - when you need to update write-only values 4. Combine with Secrets Manager - store ephemeral values in vault 5. Never log ephemeral values - they won't appear in plan output
Validation Considerations
When validating Terraform configurations with ephemeral values:
- Ephemeral resources don't appear in state
- Write-only arguments show as
(sensitive value)in plans terraform planwill show ephemeral resource creation each run- Checkov may not detect issues in ephemeral resources (no state)
---
Actions Blocks (1.14+)
Purpose: Execute provider-defined imperative operations outside the normal CRUD model.
Overview
Actions are a concept in Terraform 1.14 (GA - November 2025) that allow providers to define operations that don't fit the standard create/read/update/delete lifecycle. This is useful for one-time operations like invoking Lambda functions or invalidating CDN caches.
Basic Example
# Define an action to invoke a Lambda function
action "aws_lambda_invoke" "process_data" {
config {
function_name = aws_lambda_function.processor.function_name
payload = jsonencode({ action = "process" })
}
}
# CloudFront cache invalidation action
action "aws_cloudfront_create_invalidation" "invalidate_cache" {
config {
distribution_id = aws_cloudfront_distribution.cdn.id
paths = ["/*"]
}
}Advanced Example with Dependencies
# Resource with action trigger on lifecycle events
resource "aws_s3_object" "data_file" {
bucket = aws_s3_bucket.data.id
key = "data/input.json"
source = "local/input.json"
content_type = "application/json"
# Trigger action when S3 object is updated
lifecycle {
action_trigger {
events = [after_update]
actions = [action.aws_lambda_invoke.process_data]
}
}
}
# Lambda invocation action - triggered by resource lifecycle
action "aws_lambda_invoke" "process_data" {
config {
function_name = aws_lambda_function.processor.function_name
payload = jsonencode({
bucket = aws_s3_bucket.data.id
key = aws_s3_object.data_file.key
action = "process"
})
}
}
# CloudFront cache invalidation - triggered after S3 update
resource "aws_s3_object" "index_html" {
bucket = aws_s3_bucket.website.id
key = "index.html"
content_type = "text/html"
source = "html/index.html"
lifecycle {
action_trigger {
events = [after_update]
actions = [action.aws_cloudfront_create_invalidation.invalidate_cache]
}
}
}
action "aws_cloudfront_create_invalidation" "invalidate_cache" {
config {
distribution_id = aws_cloudfront_distribution.cdn.id
paths = ["/*"]
}
}Key Features
1. Imperative Operations - Actions perform side effects, not resource management 2. Lifecycle Integration - Can trigger on resource create/update/destroy 3. CLI Invocation - Run with terraform apply -invoke to trigger actions directly 4. Provider-Defined - Actions are defined by providers (AWS, Azure, etc.) 5. Chainable - Actions can depend on other actions
CLI Commands
# Plan with specific action invocation
terraform plan -invoke=action.aws_lambda_invoke.process_data
# Apply with specific action invocation
terraform apply -invoke=action.aws_lambda_invoke.process_data
# Apply with auto-approve and action invocation
terraform apply -auto-approve -invoke=action.aws_cloudfront_create_invalidation.invalidate_cache
# Normal apply (actions triggered by lifecycle events still run)
terraform applyWhen to Use Actions
- Invoking Lambda/Cloud Functions
- Cache invalidation (CloudFront, CDN)
- Stopping/starting EC2 instances
- Database migrations
- API calls that don't create resources
- Post-deployment scripts
- Integration testing triggers
Provider Support (as of November 2025)
| Provider | Available Actions |
|---|---|
| AWS | aws_lambda_invoke, aws_cloudfront_create_invalidation, aws_ec2_stop_instance |
| Azure | Coming soon |
| GCP | Coming soon |
Validation Considerations
- Actions don't create resources in state
terraform planshows action effects separately- Actions run in dependency order
- Failed actions don't roll back completed actions
---
List Resources and Query Command (1.14+)
Purpose: Query and filter existing infrastructure resources directly from Terraform, with optional configuration generation for importing.
Overview
Terraform 1.14 introduces List Resources, defined in *.tfquery.hcl files, that allow you to query existing infrastructure and optionally generate Terraform configuration for discovered resources.
Basic Query File
# my_query.tfquery.hcl
# List all S3 buckets with specific tags
list "aws_s3_bucket" "production_buckets" {
filter {
tags = {
Environment = "production"
}
}
}
# List EC2 instances by type
list "aws_instance" "large_instances" {
filter {
instance_type = "t3.large"
}
}
# List all VPCs
list "aws_vpc" "all_vpcs" {}CLI Commands
# Execute query and display results
terraform query
# Execute query with specific query file
terraform query -query=my_query.tfquery.hcl
# Generate configuration for discovered resources
terraform query -generate-config-out=discovered.tf
# Validate query files offline
terraform validate -queryAdvanced Query Example
# infrastructure_audit.tfquery.hcl
# Find untagged resources
list "aws_s3_bucket" "untagged_buckets" {
filter {
tags = null
}
}
# Find publicly accessible resources
list "aws_security_group" "public_ingress" {
filter {
ingress {
cidr_blocks = ["0.0.0.0/0"]
}
}
}
# Find resources by name pattern
list "aws_instance" "web_servers" {
filter {
tags = {
Name = "web-*"
}
}
}Use Cases
1. Infrastructure Auditing - Discover resources not managed by Terraform 2. Compliance Checking - Find resources missing required tags 3. Cost Optimization - Identify oversized or unused resources 4. Import Generation - Generate configuration for manual imports 5. Drift Detection - Compare query results with state
Output Example
$ terraform query
List: aws_s3_bucket.production_buckets
Found 3 resources:
- arn:aws:s3:::prod-logs-bucket
tags.Environment = "production"
tags.Team = "ops"
- arn:aws:s3:::prod-assets-bucket
tags.Environment = "production"
tags.Team = "web"
- arn:aws:s3:::prod-backups-bucket
tags.Environment = "production"
tags.Team = "dba"Validation Considerations
- Query files are validated with
terraform validate -query - Queries require valid provider authentication
- Results depend on IAM permissions
- Large queries may be rate-limited by cloud providers
---
Feature Version Matrix
| Feature | Terraform Version | Status |
|---|---|---|
| Ephemeral resources | 1.10+ | GA |
| Ephemeral variables/outputs | 1.10+ | GA |
| Write-only arguments | 1.11+ | GA |
| S3 native state locking | 1.11+ | GA |
| Actions blocks | 1.14+ | GA (Nov 2025) |
| List resources / Query | 1.14+ | GA (Nov 2025) |
Related Documentation
Terraform Best Practices
Coding standards and best practices for writing maintainable, scalable, and reliable Terraform configurations.
Project Structure
Recommended Directory Layout
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ └── production/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ └── database/
├── global/
│ ├── iam/
│ └── route53/
└── README.mdFile Organization
Standard Files:
main.tf- Primary resource definitionsvariables.tf- Input variable declarationsoutputs.tf- Output value declarationsversions.tf- Terraform and provider version constraintsbackend.tf- Backend configurationlocals.tf- Local value definitions (if many)data.tf- Data source definitions (if many)terraform.tfvars- Variable values (not committed for secrets)
When to Split Files:
- More than 200 lines in a single file
- Logical grouping of resources (e.g.,
networking.tf,compute.tf) - Complex modules with many resource types
Naming Conventions
Resources
Pattern: <resource-type>_<descriptive-name>
# Good
resource "aws_instance" "web_server" {}
resource "aws_s3_bucket" "application_logs" {}
resource "aws_security_group" "database_access" {}
# Avoid
resource "aws_instance" "instance1" {}
resource "aws_s3_bucket" "bucket" {}Variables
Pattern: snake_case with descriptive names
# Good
variable "vpc_cidr_block" {}
variable "instance_type" {}
variable "environment_name" {}
# Avoid
variable "VPCCIDR" {}
variable "type" {}
variable "env" {}Modules
Pattern: kebab-case for directories, snake_case for module calls
# Directory: modules/vpc-networking/
module "vpc_networking" {
source = "./modules/vpc-networking"
}Tags
Consistent Tagging Strategy:
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
Owner = var.owner_email
CostCenter = var.cost_center
}
}
resource "aws_instance" "web" {
# ... other config ...
tags = merge(local.common_tags, {
Name = "${var.environment}-web-server"
Role = "webserver"
})
}Variable Management
Variable Declarations
Always Include:
- Type constraints
- Descriptions
- Validation rules (when applicable)
- Default values (for non-sensitive, non-environment-specific values)
variable "instance_type" {
description = "EC2 instance type for web servers"
type = string
default = "t3.micro"
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
}
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "VPC CIDR must be a valid IPv4 CIDR block."
}
}
variable "db_password" {
description = "Database master password"
type = string
sensitive = true # Prevents display in logs
}Variable Types
Use Specific Types:
# Primitive types
variable "instance_count" {
type = number
}
variable "enable_monitoring" {
type = bool
}
# Collection types
variable "availability_zones" {
type = list(string)
}
variable "tags" {
type = map(string)
}
# Object types
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
})
}Environment-Specific Variables
Use .tfvars Files:
# environments/dev/terraform.tfvars
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
enable_backup = false
# environments/production/terraform.tfvars
environment = "production"
instance_type = "t3.large"
instance_count = 3
enable_backup = trueModule Design
Module Best Practices
Single Responsibility: Each module should have one clear purpose.
# Good: Focused module
module "vpc" {
source = "./modules/vpc"
# VPC-specific config
}
# Avoid: Kitchen-sink module
module "infrastructure" {
source = "./modules/everything"
# VPC, databases, compute, monitoring, etc.
}Required vs Optional Variables:
# modules/database/variables.tf
# Required - no default
variable "database_name" {
description = "Name of the database"
type = string
}
# Optional - has sensible default
variable "backup_retention_days" {
description = "Number of days to retain backups"
type = number
default = 7
}Output Everything Useful:
# modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}Module Documentation
README.md Template:
# VPC Module
Creates a VPC with public and private subnets across multiple availability zones.
## Usage
module "vpc" { source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16" availability_zones = ["us-east-1a", "us-east-1b"] environment = "production" }
## Requirements
| Name | Version |
|------|---------|
| terraform | >= 1.0 |
| aws | >= 5.0 |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| vpc_cidr | CIDR block for VPC | `string` | n/a | yes |
| availability_zones | List of AZs | `list(string)` | n/a | yes |
## Outputs
| Name | Description |
|------|-------------|
| vpc_id | ID of the VPC |
| private_subnet_ids | List of private subnet IDs |State Management
Remote State
Always Use Remote State for Teams:
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
# Workspace-specific state
workspace_key_prefix = "workspaces"
}
}State Locking
DynamoDB Table for S3 Backend:
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform State Locks"
ManagedBy = "Terraform"
}
}State Isolation
Separate State Files by Environment and Component:
s3://terraform-state/
├── production/
│ ├── vpc/terraform.tfstate
│ ├── database/terraform.tfstate
│ └── compute/terraform.tfstate
├── staging/
│ ├── vpc/terraform.tfstate
│ └── compute/terraform.tfstate
└── dev/
└── all/terraform.tfstateResource Management
Use Data Sources for Existing Resources
# Instead of hardcoding
resource "aws_instance" "web" {
subnet_id = "subnet-12345" # Avoid
}
# Use data sources
data "aws_subnet" "private" {
filter {
name = "tag:Name"
values = ["${var.environment}-private-subnet"]
}
}
resource "aws_instance" "web" {
subnet_id = data.aws_subnet.private.id
}Resource Dependencies
Implicit Dependencies (Preferred):
resource "aws_instance" "web" {
subnet_id = aws_subnet.private.id # Implicit dependency
security_groups = [aws_security_group.web.id]
}Explicit Dependencies (When Needed):
resource "aws_iam_role_policy" "example" {
# ... config ...
# Ensure role exists before attaching policy
depends_on = [aws_iam_role.example]
}Count vs For_Each
Use for_each for Map-Like Resources:
# Good: for_each with maps
locals {
subnets = {
public_a = { cidr = "10.0.1.0/24", az = "us-east-1a" }
public_b = { cidr = "10.0.2.0/24", az = "us-east-1b" }
private_a = { cidr = "10.0.3.0/24", az = "us-east-1a" }
private_b = { cidr = "10.0.4.0/24", az = "us-east-1b" }
}
}
resource "aws_subnet" "main" {
for_each = local.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
Name = each.key
}
}Use count for Simple Conditionals:
resource "aws_cloudwatch_log_group" "app" {
count = var.enable_logging ? 1 : 0
name = "/aws/app/logs"
}Version Constraints
Terraform Version
terraform {
required_version = ">= 1.0, < 2.0"
}Provider Versions
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Allow patch updates, lock minor version
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}Version Constraint Operators:
=- Exact version!=- Exclude version>,>=,<,<=- Comparison~>- Pessimistic constraint (allow rightmost version component to increment)
State Management Blocks
Terraform 1.1+ introduced declarative blocks for managing state without manual terraform state commands.
Import Block (Terraform 1.5+)
The import block allows config-driven import of existing resources into Terraform state.
Basic Usage:
# Import an existing VPC
import {
to = aws_vpc.main
id = "vpc-0123456789abcdef0"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}Dynamic Import (Terraform 1.6+):
# Import with expressions
variable "vpc_id" {
type = string
}
import {
to = aws_vpc.main
id = var.vpc_id
}
# Import with string interpolation
import {
to = aws_s3_bucket.logs
id = "${var.environment}-logs-bucket"
}Generate Configuration:
# Generate config for imported resources
terraform plan -generate-config-out=generated.tfWorkflow: 1. Add import block with target resource address and ID 2. Run terraform plan to see what will be imported 3. Add or generate the corresponding resource block 4. Run terraform apply to import 5. Remove the import block after successful import
Moved Block (Terraform 1.1+)
The moved block enables refactoring without manual state manipulation.
Rename a Resource:
# Old: aws_instance.web
# New: aws_instance.web_server
moved {
from = aws_instance.web
to = aws_instance.web_server
}
resource "aws_instance" "web_server" {
ami = "ami-12345678"
instance_type = "t3.micro"
}Move to a Module:
# Move resource into a module
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.main
}
module "networking" {
source = "./modules/networking"
}Move from count to for_each:
# Old: aws_instance.web[0], aws_instance.web[1]
# New: aws_instance.web["web-1"], aws_instance.web["web-2"]
moved {
from = aws_instance.web[0]
to = aws_instance.web["web-1"]
}
moved {
from = aws_instance.web[1]
to = aws_instance.web["web-2"]
}
resource "aws_instance" "web" {
for_each = toset(["web-1", "web-2"])
ami = "ami-12345678"
instance_type = "t3.micro"
tags = {
Name = each.key
}
}Rename a Module:
moved {
from = module.old_name
to = module.new_name
}
module "new_name" {
source = "./modules/compute"
}Best Practices for moved:
- Keep
movedblocks until all team members have applied the changes - Remove
movedblocks after state migration is complete across all environments - Use descriptive commit messages explaining the refactoring
Removed Block (Terraform 1.7+)
The removed block allows declarative removal of resources from Terraform management.
Remove Without Destroying:
# Stop managing resource but keep it in cloud
removed {
from = aws_instance.legacy_server
lifecycle {
destroy = false
}
}Remove and Destroy:
# Remove from state and destroy the resource
removed {
from = aws_s3_bucket.old_logs
lifecycle {
destroy = true
}
}Remove Module:
# Remove entire module from management
removed {
from = module.deprecated_service
lifecycle {
destroy = false
}
}Use Cases:
- Migrating resource ownership to another team/state
- Removing resources that should persist but not be managed
- Cleaning up after manual resource creation
- Deprecating modules without destroying infrastructure
State Block Comparison
| Block | Version | Purpose | Use Case |
|---|---|---|---|
import | 1.5+ | Bring existing resources into Terraform | Adopting existing infrastructure |
moved | 1.1+ | Refactor without state surgery | Renaming, restructuring modules |
removed | 1.7+ | Stop managing resources declaratively | Ownership transfer, cleanup |
Migration from CLI Commands
Old Way (CLI):
# Import
terraform import aws_vpc.main vpc-12345
# Move
terraform state mv aws_instance.web aws_instance.web_server
# Remove
terraform state rm aws_instance.legacyNew Way (Config-Driven):
# All operations are declarative and version-controlled
import {
to = aws_vpc.main
id = "vpc-12345"
}
moved {
from = aws_instance.web
to = aws_instance.web_server
}
removed {
from = aws_instance.legacy
lifecycle {
destroy = false
}
}Benefits of Config-Driven Approach:
- Changes are code-reviewed and version-controlled
- Operations are repeatable and documented
- Team collaboration without state file conflicts
- Rollback capability through git history
Code Quality
Use Locals for Computed Values
locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
# Computed values
is_production = var.environment == "production"
instance_type = local.is_production ? "t3.large" : "t3.micro"
}Dynamic Blocks
Use Sparingly and Only When Necessary:
resource "aws_security_group" "example" {
name = "example"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}Conditional Resources
# Use count for conditional creation
resource "aws_kms_key" "encryption" {
count = var.enable_encryption ? 1 : 0
description = "Encryption key"
}
# Reference with [0] and handle with try()
resource "aws_s3_bucket" "example" {
# ...
kms_master_key_id = try(aws_kms_key.encryption[0].arn, null)
}Testing
Validation
# Format check
terraform fmt -check -recursive
# Validation
terraform validate
# Plan review
terraform plan
# Compliance testing
terraform-compliance -p terraform.plan -f compliance/Pre-Commit Hooks
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
- id: terraform_tflintPerformance
Reduce Plan Time
- Use targeted plans for large infrastructures:
terraform plan -target=module.vpc - Split large configurations into smaller state files
- Use
-parallelismflag:terraform apply -parallelism=20
Optimize Resource Queries
# Cache data source results in locals
data "aws_ami" "ubuntu" {
most_recent = true
# ... filters ...
}
locals {
ami_id = data.aws_ami.ubuntu.id
}
# Reuse local value
resource "aws_instance" "web" {
count = 10
ami = local.ami_id # Don't repeat data source
instance_type = var.instance_type
}Documentation
Inline Comments
# Create VPC with DNS support enabled for private hosted zones
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true # Required for Route53 private zones
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${var.environment}-vpc"
})
}Module Documentation
Use terraform-docs to auto-generate documentation:
terraform-docs markdown table . > README.mdSecurity Best Practices
- Never commit
.tfstatefiles - Never commit
.tfvarsfiles with secrets - Use
.gitignore:
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
.terraform.lock.hcl- Use
sensitive = truefor sensitive variables and outputs - Encrypt remote state
- Use least-privilege IAM policies
- Enable MFA for state bucket access
Workflow
Recommended Git Workflow
1. Create feature branch 2. Make changes 3. Run terraform fmt 4. Run terraform validate 5. Run terraform plan and review 6. Commit changes 7. Create pull request 8. Peer review 9. Merge to main 10. Apply in environment
CI/CD Integration
# .github/workflows/terraform.yml
name: Terraform
on: [pull_request]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform planCommon Terraform Errors
Database of frequently encountered Terraform errors with detailed solutions and prevention strategies.
Initialization Errors
Error: Failed to query available provider packages
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/aws: no available releases match the given constraintsCauses:
- Invalid version constraint in
required_providers - Network connectivity issues
- Provider source incorrect or doesn't exist
Solutions:
# Check provider configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws" # Verify source is correct
version = "~> 5.0" # Check version exists
}
}
}# Clear cache and reinitialize
rm -rf .terraform .terraform.lock.hcl
terraform initError: Module not found
Error: Module not installed
This configuration requires module "vpc" but it is not installed.Causes:
- Forgot to run
terraform init - Module source path incorrect
- Network issues downloading remote modules
Solutions:
# Initialize to download modules
terraform init
# Update modules
terraform init -upgrade
# Check module source
module "vpc" {
source = "./modules/vpc" # Verify path exists
# or
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2"
}Validation Errors
Error: Unsupported argument
Error: Unsupported argument
An argument named "instance_class" is not expected here.Causes:
- Typo in argument name
- Argument not supported in this resource type
- Wrong provider version
Solutions: 1. Check official documentation for correct argument names 2. Verify provider version supports the argument 3. Use terraform console to explore resource schema
# Check resource schema
terraform console
> provider::aws::schema::aws_instanceError: Missing required argument
Error: Missing required argument
The argument "ami" is required, but no definition was found.Solutions:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # Add missing argument
instance_type = var.instance_type
}Error: Incorrect attribute value type
Error: Incorrect attribute value type
Inappropriate value for attribute "instance_count": a number is required.Solutions:
# Ensure variable has correct type
variable "instance_count" {
type = number
default = 1 # Not "1"
}
# Convert if needed
resource "aws_instance" "web" {
count = tonumber(var.instance_count)
}Resource Errors
Error: Error creating resource: already exists
Error: Error creating VPC: VpcLimitExceeded: The maximum number of VPCs has been reached.Causes:
- Resource already exists in AWS
- Service quota exceeded
- Import needed for existing resource
Solutions:
# Import existing resource
terraform import aws_vpc.main vpc-12345678
# Request quota increase
aws service-quotas request-service-quota-increase \
--service-code vpc \
--quota-code L-F678F1CE \
--desired-value 10Error: Resource not found
Error: Error reading VPC: VPCNotFound: The vpc ID 'vpc-12345' does not existCauses:
- Resource was manually deleted
- Wrong AWS region
- State file out of sync
Solutions:
# Refresh state
terraform refresh
# Remove from state if truly deleted
terraform state rm aws_vpc.main
# Check AWS region configuration
provider "aws" {
region = "us-east-1" # Verify correct region
}Error: Resource dependency violation
Error: Error deleting VPC: DependencyViolation: The vpc 'vpc-12345' has dependencies and cannot be deleted.Causes:
- Resources still attached to VPC
- Manual deletion required first
- Incorrect destroy order
Solutions:
# Use targeted destroy
terraform destroy -target=aws_subnet.private
terraform destroy -target=aws_vpc.main
# Or recreate dependencies
terraform apply
terraform destroy # Destroy in correct orderState Management Errors
Error: State lock acquisition failed
Error: Error acquiring the state lock
Lock Info:
ID: abc123
Path: terraform.tfstate
Operation: OperationTypeApplyCauses:
- Another terraform process running
- Previous operation crashed without releasing lock
- DynamoDB table issues (S3 backend)
Solutions:
# Wait for other process to complete, or force unlock (use carefully)
terraform force-unlock abc123
# Verify no other terraform processes
ps aux | grep terraform
# Check DynamoDB lock table
aws dynamodb scan --table-name terraform-state-locksError: State file version mismatch
Error: state snapshot was created by Terraform v1.5.0, which is newer than current v1.4.0Solutions:
# Upgrade Terraform to required version
brew upgrade terraform
# Or use tfenv for version management
tfenv install 1.5.0
tfenv use 1.5.0Error: Backend configuration changed
Error: Backend configuration changed
A change in the backend configuration has been detected.Solutions:
# Reconfigure backend
terraform init -reconfigure
# Migrate state to new backend
terraform init -migrate-statePlan/Apply Errors
Error: Provider authentication failed
Error: error configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.Causes:
- AWS credentials not configured
- Expired credentials
- Wrong profile or role
Solutions:
# Set environment variables
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-east-1"
# Or use AWS CLI profile
export AWS_PROFILE="your-profile"
# Or configure in provider
provider "aws" {
profile = "your-profile"
region = "us-east-1"
}
# Verify credentials
aws sts get-caller-identityError: Cycle dependency
Error: Cycle: aws_security_group.web, aws_security_group.dbCauses:
- Security groups reference each other
- Circular module dependencies
Solutions:
# Break cycle with security group rules
resource "aws_security_group" "web" {
name = "web-sg"
# Remove inline rules causing cycle
}
resource "aws_security_group" "db" {
name = "db-sg"
}
# Create rules separately
resource "aws_security_group_rule" "web_to_db" {
type = "egress"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_group_id = aws_security_group.web.id
source_security_group_id = aws_security_group.db.id
}Error: Invalid count argument
Error: Invalid count argument
The "count" value depends on resource attributes that cannot be determined until apply.Solutions:
# Use two-step apply or redesign
# Bad
resource "aws_instance" "web" {
count = length(aws_subnet.private) # Unknown until apply
}
# Good - use for_each instead
resource "aws_instance" "web" {
for_each = toset(var.subnet_ids) # Known at plan time
subnet_id = each.value
}Error: Invalid for_each argument
Error: Invalid for_each argument
The "for_each" value depends on resource attributes that cannot be determined until apply.Solutions:
# Use data sources or variables instead of resource attributes
# Bad
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private # Unknown until apply
}
# Good
locals {
subnets = {
private_a = { cidr = "10.0.1.0/24" }
private_b = { cidr = "10.0.2.0/24" }
}
}
resource "aws_subnet" "private" {
for_each = local.subnets
cidr_block = each.value.cidr
}Variable Errors
Error: No value for required variable
Error: No value for required variable
The root module input variable "db_password" is not set.Solutions:
# Set via command line
terraform apply -var="db_password=secretpass"
# Set via tfvars file
echo 'db_password = "secretpass"' > terraform.tfvars
# Set via environment variable
export TF_VAR_db_password="secretpass"Error: Invalid variable type
Error: Invalid value for input variable
The given value is not suitable for var.instance_count: number required.Solutions:
# In terraform.tfvars, use correct type
instance_count = 3 # Not "3"
# Or convert in code
variable "instance_count" {
type = string
}
resource "aws_instance" "web" {
count = tonumber(var.instance_count)
}Module Errors
Error: Unsuitable value for module variable
Error: Unsuitable value for var.vpc_cidr
This value does not have any of the required types: string.Solutions:
# Check module call
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16" # Ensure string, not object
}Error: Unsupported attribute in module output
Error: Unsupported attribute
This object does not have an attribute named "vpc_id".Causes:
- Output not defined in module
- Typo in output name
- Module version mismatch
Solutions:
# Check module outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
# Reference correctly
resource "aws_instance" "web" {
subnet_id = module.vpc.vpc_id # Use exact output name
}Provider-Specific Errors
AWS: Error creating Security Group: InvalidGroup.Duplicate
Error: Error creating Security Group: InvalidGroup.Duplicate: The security group 'web-sg' already existsSolutions:
# Import existing security group
terraform import aws_security_group.web sg-12345678
# Or use data source
data "aws_security_group" "existing" {
name = "web-sg"
}AWS: Error: Timeout while waiting for state
Error: timeout while waiting for resource to be createdCauses:
- Resource taking longer than expected
- Resource creation actually failed
- API throttling
Solutions:
# Increase timeout
resource "aws_db_instance" "main" {
# ... config ...
timeouts {
create = "60m"
update = "60m"
delete = "60m"
}
}AWS: Error: UnauthorizedOperation
Error: UnauthorizedOperation: You are not authorized to perform this operation.Solutions:
# Check IAM permissions
aws iam get-user-policy --user-name your-user --policy-name your-policy
# Verify required permissions for resource
# Example: EC2 instance requires:
# - ec2:RunInstances
# - ec2:DescribeInstances
# - ec2:DescribeImages
# etc.Workspace Errors
Error: Workspace already exists
Error: Workspace "production" already existsSolutions:
# Select existing workspace
terraform workspace select production
# List workspaces
terraform workspace list
# Delete workspace (if empty)
terraform workspace delete productionFormatting Errors
Error: Terraform fmt found issues
main.tf
- Line 5: Incorrect indentationSolutions:
# Auto-fix formatting
terraform fmt
# Check formatting (CI/CD)
terraform fmt -check
# Recursive formatting
terraform fmt -recursiveImport Errors
Error: Import resource does not exist
Error: Cannot import non-existent remote objectSolutions:
# Verify resource ID
aws ec2 describe-instances --instance-ids i-12345
# Use correct resource address
terraform import aws_instance.web i-1234567890abcdef0
# Check provider configuration matches resource regionPrevention Strategies
Pre-Commit Checks
# Run these before every commit
terraform fmt -check -recursive
terraform validate
terraform planUse Validation Rules
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}Enable Detailed Logging
# Debug mode
export TF_LOG=DEBUG
terraform apply
# Log to file
export TF_LOG_PATH="./terraform.log"Version Pinning
terraform {
required_version = "~> 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}Terraform Security Checklist
Comprehensive security validation checklist for Terraform configurations. Use this reference when performing security reviews or auditing infrastructure-as-code.
Secrets Management
Hardcoded Credentials
Risk: Secrets committed to version control can be exposed.
Detection:
# Search for common secret patterns
grep -rE "(password|secret|api_key|access_key)\s*=\s*\"[^$]" *.tf
grep -rE "private_key\s*=\s*\"" *.tf
grep -rE "token\s*=\s*\"[^$]" *.tfRemediation:
- Use Terraform variables with
sensitive = true - Use environment variables (TF_VAR_*)
- Use HashiCorp Vault or AWS Secrets Manager
- Use AWS Systems Manager Parameter Store
- Never commit
.tfvarsfiles with secrets
Example - Insecure:
resource "aws_db_instance" "example" {
username = "admin"
password = "hardcoded_password123" # SECURITY ISSUE
}Example - Secure:
variable "db_password" {
type = string
sensitive = true
}
resource "aws_db_instance" "example" {
username = "admin"
password = var.db_password
}Sensitive Output Exposure
Risk: Sensitive data exposed in terraform state or plan output.
Detection:
- Review output blocks for sensitive data
- Check state files for plaintext secrets
Remediation:
output "db_password" {
value = aws_db_instance.example.password
sensitive = true # Prevents display in console
}Network Security
Overly Permissive Security Groups
Risk: Unrestricted access to resources from the internet.
Detection Patterns:
# SECURITY ISSUE: SSH open to world
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# SECURITY ISSUE: All ports open
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}Best Practices:
- Restrict SSH/RDP to specific IP ranges or VPN
- Use security group references instead of CIDR blocks
- Implement least-privilege access
- Document exceptions with comments
Example - Secure:
variable "admin_cidr" {
description = "CIDR block for admin access"
type = string
}
resource "aws_security_group" "app" {
ingress {
description = "SSH from admin network only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_cidr]
}
}Public S3 Buckets
Risk: Data exposure through public S3 access.
Detection:
# SECURITY ISSUE: Public bucket
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = false # Should be true
block_public_policy = false # Should be true
ignore_public_acls = false # Should be true
restrict_public_buckets = false # Should be true
}Best Practices:
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Encryption
Encryption at Rest
Resources to Check:
- RDS databases
- S3 buckets
- EBS volumes
- DynamoDB tables
- Elasticsearch domains
- Kinesis streams
- SQS queues
Example - RDS Encryption:
resource "aws_db_instance" "example" {
storage_encrypted = true # Required
kms_key_id = aws_kms_key.db.arn # Use customer-managed keys
}Example - S3 Encryption:
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.example.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
}
}Encryption in Transit
Risk: Data intercepted during transmission.
Best Practices:
- Enforce HTTPS/TLS for all endpoints
- Use SSL/TLS for database connections
- Enable encryption for load balancers
Example - ALB HTTPS:
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.example.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.example.arn
}
}
# Redirect HTTP to HTTPS
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.example.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}IAM Security
Overly Permissive Policies
Risk: Privilege escalation and unauthorized access.
Detection Patterns:
# SECURITY ISSUE: Admin access
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
# SECURITY ISSUE: Too broad
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}Best Practices:
- Follow least-privilege principle
- Use specific actions instead of wildcards
- Scope resources narrowly
- Use conditions to restrict access
Example - Least Privilege:
data "aws_iam_policy_document" "s3_read_only" {
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:ListBucket"
]
resources = [
aws_s3_bucket.app_data.arn,
"${aws_s3_bucket.app_data.arn}/*"
]
}
}Missing MFA Requirements
Best Practice:
data "aws_iam_policy_document" "require_mfa" {
statement {
effect = "Deny"
actions = ["*"]
resources = ["*"]
condition {
test = "BoolIfExists"
variable = "aws:MultiFactorAuthPresent"
values = ["false"]
}
}
}Cross-Account Access
Risk: Unauthorized access from other AWS accounts.
Best Practices:
- Explicitly specify trusted accounts
- Require external ID for third-party access
- Use conditions to restrict access
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::123456789012:root"]
}
actions = ["sts:AssumeRole"]
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}Logging and Monitoring
Missing CloudTrail
Risk: No audit trail for API calls.
Best Practice:
resource "aws_cloudtrail" "main" {
name = "main-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
include_global_service_events = true
is_multi_region_trail = true
enable_logging = true
event_selector {
read_write_type = "All"
include_management_events = true
}
}Missing VPC Flow Logs
Best Practice:
resource "aws_flow_log" "vpc" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.flow_logs.arn
}Unencrypted Logs
Best Practice:
resource "aws_cloudwatch_log_group" "app" {
name = "/aws/app/logs"
retention_in_days = 90
kms_key_id = aws_kms_key.logs.arn # Encrypt logs
}Resource-Specific Checks
RDS Databases
- [ ]
storage_encrypted = true - [ ]
publicly_accessible = false - [ ] Backup retention enabled
- [ ] Multi-AZ for production
- [ ] IAM authentication enabled
- [ ] Enhanced monitoring enabled
- [ ] SSL/TLS required for connections
ElastiCache
- [ ]
at_rest_encryption_enabled = true - [ ]
transit_encryption_enabled = true - [ ] Auth token enabled for Redis
- [ ] Subnet group in private subnets
Lambda Functions
- [ ] Environment variables encrypted with KMS
- [ ] VPC configuration if accessing private resources
- [ ] IAM role with least-privilege
- [ ] Dead letter queue configured
- [ ] Reserved concurrency to prevent cost overruns
ECS/EKS
- [ ] Secrets managed via Secrets Manager
- [ ] Container images scanned
- [ ] Network policy enforcement
- [ ] Pod security policies
- [ ] RBAC configured
State File Security
Remote State
Risk: State files contain sensitive data in plaintext.
Best Practices:
Terraform 1.11+ (S3 Native Locking - Recommended):
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # Required
kms_key_id = "arn:aws:kms:..."
use_lockfile = true # S3 native locking (1.11+)
}
}Note: Terraform 1.11 introduced S3 native state locking via theuse_lockfileargument. This uses S3's conditional writes to implement locking without requiring DynamoDB. The DynamoDB-based locking (dynamodb_table) is now deprecated but still supported for backward compatibility.
Legacy (Terraform < 1.11 or backward compatibility):
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # Required
kms_key_id = "arn:aws:kms:..."
dynamodb_table = "terraform-locks" # State locking (deprecated in 1.11+)
}
}Checklist:
- [ ] Encryption enabled for state storage
- [ ] State locking configured (
use_lockfile = truefor 1.11+ or DynamoDB for older versions) - [ ] Versioning enabled on state bucket
- [ ] Access restricted via IAM policies
- [ ] MFA delete enabled on state bucket
- [ ] State files never committed to version control
Compliance Checks
Tagging
Best Practice:
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Owner = var.owner
CostCenter = var.cost_center
Compliance = "HIPAA" # If applicable
}
}
resource "aws_instance" "example" {
# ... other config ...
tags = merge(local.common_tags, {
Name = "app-server"
})
}Data Residency
- Ensure resources in correct regions
- Check for cross-region replication
- Verify data sovereignty requirements
Terraform-Specific Security
Provider Version Pinning
Risk: Unexpected behavior from provider updates.
Best Practice:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Pin major version
}
}
}Module Sources
Risk: Malicious code from untrusted modules.
Best Practices:
- Use verified modules from Terraform Registry
- Pin module versions
- Review module code before use
- Use private module registry for internal modules
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2" # Pin specific version
}Automated Security Scanning
Tools to integrate:
- trivy - Unified security scanner (successor to tfsec, includes IaC scanning)
- checkov - Policy-as-code security scanner (3000+ built-in policies)
- terraform-compliance - BDD-style testing
Note: Terrascan was archived by Tenable on November 20, 2025 and is no longer maintained. Use Checkov or Trivy instead for OPA/Rego-style policy enforcement.
Trivy (Recommended)
Trivy is Aqua Security's unified scanner that absorbed tfsec. It scans Terraform, CloudFormation, Kubernetes, Helm, and more.
Version Note:
Warning: Trivy v0.60.0 has known regression issues that can cause panics when scanning Terraform configurations. If you experience crashes or unexpected behavior, downgrade to v0.59.x until v0.61.0+ is released with fixes.
>
To install a specific version:
```bash
# macOS
brew install trivy@0.59.1
>
# Linux - specify version in install script
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.59.1
```
Installation:
# macOS
brew install trivy
# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Docker
docker pull aquasec/trivyUsage:
# Scan Terraform directory
trivy config ./terraform
# Scan with specific severity
trivy config --severity HIGH,CRITICAL ./terraform
# Scan with JSON output
trivy config -f json -o results.json ./terraform
# Scan specific file
trivy config main.tf
# Skip specific checks
trivy config --skip-dirs .terraform ./terraform
# Scan Terraform plan JSON (more accurate)
terraform show -json tfplan > tfplan.json
trivy config tfplan.json
# Use tfvars files for accurate variable resolution
trivy config --tf-vars prod.terraform.tfvars ./terraform
# Exclude downloaded modules from scanning
trivy config --tf-exclude-downloaded-modules ./terraformCommon Trivy Checks for Terraform:
AVD-AWS-0086- S3 bucket encryptionAVD-AWS-0089- S3 bucket versioningAVD-AWS-0132- Security group unrestricted ingressAVD-AWS-0107- RDS encryption at restAVD-AWS-0078- EBS encryption
Output Formats:
table- Human-readable table (default)json- JSON format for CI/CD integrationsarif- SARIF format for IDE integrationtemplate- Custom template output
Ignore Findings:
# trivy:ignore:AVD-AWS-0086
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
}Advanced Trivy Configuration (trivy.yaml):
# trivy.yaml
exit-code: 1
severity:
- HIGH
- CRITICAL
scan:
scanners:
- vuln
- secret
- misconfig
misconfiguration:
terraform:
tfvars-files:
- prod.tfvarsCheckov 3.0
Checkov 3.0 introduces major improvements for Terraform scanning with enhanced graph policies and deeper analysis.
Key 3.0 Features:
1. Deep Analysis Mode: Fully resolve for_each, dynamic blocks, and complex configurations:
# Enable deep analysis with plan file
checkov -f tfplan.json --deep-analysis --repo-root-for-plan-enrichment .2. Baseline Feature: Track only new misconfigurations (ignore existing):
# Create baseline from current state
checkov -d . --create-baseline
# Run subsequent scans against baseline
checkov -d . --baseline .checkov.baseline3. Enhanced Policy Language: 36 new operators including:
SUBSET- Check if values are subset of allowed valuesjsonpath_*operators - Deep JSON path queries- Enhanced graph traversal for complex dependencies
4. Improved Dynamic Block Support:
# Scan with full dynamic block resolution
checkov -d . --download-external-modules trueCheckov 3.0 Commands:
# Basic scan
checkov -d .
# Deep analysis with Terraform plan
terraform plan -out=tf.plan
terraform show -json tf.plan > tfplan.json
checkov -f tfplan.json --deep-analysis
# Create and use baseline
checkov -d . --create-baseline
checkov -d . --baseline .checkov.baseline
# Compact output (failures only)
checkov -d . --compact
# Skip specific checks
checkov -d . --skip-check CKV_AWS_20,CKV_AWS_21
# Run only specific frameworks
checkov -d . --framework terraformTool Comparison
| Tool | Focus | Policy Language | Built-in Policies | Best For |
|---|---|---|---|---|
| trivy | Security | Rego | 1000+ | All-in-one scanning, container + IaC |
| checkov | Security/Compliance | Python/YAML | 3000+ | Multi-framework, compliance, deep analysis |
Note: tfsec has been deprecated and merged into Trivy. Terrascan was archived in November 2025. New users should use Trivy or Checkov.
Quick Security Audit Commands
# Check for hardcoded secrets
grep -r "password\s*=\s*\"" . --include="*.tf"
grep -r "secret\s*=\s*\"" . --include="*.tf"
# Find public security groups
grep -r "0.0.0.0/0" . --include="*.tf"
# Find unencrypted resources
grep -r "encrypted\s*=\s*false" . --include="*.tf"
# Check for missing backup configurations
grep -r "backup_retention_period\s*=\s*0" . --include="*.tf"#!/bin/bash
# Wrapper script for extract_tf_info.py that handles python-hcl2 dependency.
# Reuses a cached virtual environment for repeat runs to avoid reinstall overhead.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_SCRIPT="$SCRIPT_DIR/extract_tf_info.py"
DEFAULT_CACHE_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/terraform-validator"
HCL2_VENV="${TF_VALIDATOR_HCL2_VENV:-$DEFAULT_CACHE_ROOT/hcl2-venv}"
usage() {
echo "Usage: extract_tf_info_wrapper.sh <terraform-file-or-directory>" >&2
echo "" >&2
echo "Extracts provider, module, and resource information from Terraform files." >&2
echo "Outputs JSON structure for validation and documentation lookup." >&2
}
if [ $# -lt 1 ]; then
usage
exit 1
fi
TARGET_PATH="$1"
if [ ! -e "$TARGET_PATH" ]; then
echo "Error: Path does not exist: $TARGET_PATH" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required but not installed." >&2
exit 1
fi
run_parser() {
local py_bin="$1"
"$py_bin" "$PYTHON_SCRIPT" "$TARGET_PATH"
}
# Fast path: system python already has python-hcl2.
if python3 -c "import hcl2" >/dev/null 2>&1; then
run_parser python3
exit $?
fi
mkdir -p "$(dirname "$HCL2_VENV")"
# Build or repair cached virtualenv if needed.
if [ ! -x "$HCL2_VENV/bin/python3" ]; then
echo "python-hcl2 not found. Creating cached environment at: $HCL2_VENV" >&2
python3 -m venv "$HCL2_VENV" >&2
fi
if ! "$HCL2_VENV/bin/python3" -c "import hcl2" >/dev/null 2>&1; then
echo "Installing python-hcl2 into cached environment..." >&2
"$HCL2_VENV/bin/pip" install --quiet --disable-pip-version-check python-hcl2 >&2
fi
run_parser "$HCL2_VENV/bin/python3"
#!/usr/bin/env python3
"""
Terraform Configuration Parser
Extracts provider, module, and resource information from Terraform files (.tf)
to facilitate version-aware documentation lookup and validation.
This script uses python-hcl2 for proper HCL parsing instead of regex,
which handles nested blocks, heredocs, and complex types correctly.
Usage:
python extract_tf_info.py <path-to-tf-file-or-directory>
python extract_tf_info.py main.tf
python extract_tf_info.py ./terraform/modules/
Output:
JSON structure containing:
- providers: List of required providers with versions
- modules: List of module sources with versions
- resources: List of resources by type
- data_sources: List of data sources
- variables: List of input variables
- outputs: List of outputs
- locals: List of local value names
Requirements:
pip install python-hcl2
"""
import json
import os
import sys
from pathlib import Path
from typing import Any
# Check for python-hcl2 and provide helpful error message if missing
try:
import hcl2
from lark.exceptions import UnexpectedCharacters, UnexpectedToken
HCL2_AVAILABLE = True
except ImportError:
HCL2_AVAILABLE = False
UnexpectedCharacters = Exception
UnexpectedToken = Exception
class TerraformParser:
"""Parse Terraform HCL files to extract configuration metadata."""
def __init__(self):
# Keep `providers` as required_providers for backward compatibility.
self.providers: list[dict[str, Any]] = []
self.required_providers: list[dict[str, Any]] = []
self.provider_configs: list[dict[str, Any]] = []
self.modules: list[dict[str, Any]] = []
self.resources: list[dict[str, Any]] = []
self.data_sources: list[dict[str, Any]] = []
self.variables: list[dict[str, Any]] = []
self.outputs: list[dict[str, Any]] = []
self.locals: list[dict[str, Any]] = []
self.ephemeral_resources: list[dict[str, Any]] = []
self.terraform_settings: dict[str, Any] = {}
self.implicit_providers: list[dict[str, Any]] = []
self.all_providers_for_docs: list[dict[str, Any]] = []
self.parse_errors: list[dict[str, str]] = []
self._seen_required_providers: set[tuple] = set()
self._seen_provider_configs: set[tuple] = set()
def parse_file(self, filepath: str) -> None:
"""Parse a single Terraform file using python-hcl2."""
if not HCL2_AVAILABLE:
print("Error: python-hcl2 is required but not installed.", file=sys.stderr)
print("Install it with: pip install python-hcl2", file=sys.stderr)
sys.exit(1)
try:
with open(filepath, 'r', encoding='utf-8') as f:
parsed = hcl2.load(f)
self._extract_terraform_block(parsed, filepath)
self._extract_providers(parsed, filepath)
self._extract_modules(parsed, filepath)
self._extract_resources(parsed, filepath)
self._extract_data_sources(parsed, filepath)
self._extract_variables(parsed, filepath)
self._extract_outputs(parsed, filepath)
self._extract_locals(parsed, filepath)
self._extract_ephemeral_resources(parsed, filepath)
except (UnexpectedToken, UnexpectedCharacters) as e:
error = {
'file': filepath,
'error': 'hcl_syntax_error',
'message': str(e)
}
self.parse_errors.append(error)
print(f"HCL syntax error in {filepath}", file=sys.stderr)
except Exception as e:
error = {
'file': filepath,
'error': 'parse_error',
'message': str(e)
}
self.parse_errors.append(error)
print(f"Error parsing {filepath}: {e}", file=sys.stderr)
def parse_directory(self, dirpath: str) -> None:
"""Parse all .tf files in a directory recursively."""
path = Path(dirpath)
if not path.exists():
print(f"Error: Path {dirpath} does not exist", file=sys.stderr)
return
# Find all .tf files
tf_files = sorted(path.rglob("*.tf"))
if not tf_files:
print(f"Warning: No .tf files found in {dirpath}", file=sys.stderr)
return
for tf_file in tf_files:
# Skip .terraform directory
if '.terraform' in tf_file.parts:
continue
self.parse_file(str(tf_file))
def _extract_terraform_block(self, parsed: dict, filepath: str) -> None:
"""Extract terraform settings including required_providers."""
terraform_blocks = parsed.get('terraform', [])
for block in terraform_blocks:
# Extract required_version
if 'required_version' in block:
self.terraform_settings['required_version'] = block['required_version']
# Extract required_providers
required_providers = block.get('required_providers', [])
for provider_block in required_providers:
if isinstance(provider_block, dict):
for name, config in provider_block.items():
if isinstance(config, dict):
source = config.get('source')
version = config.get('version')
else:
source = None
version = config if isinstance(config, str) else None
provider_key = (name, source)
if provider_key not in self._seen_required_providers:
self._seen_required_providers.add(provider_key)
provider_entry = {
'name': name,
'source': source,
'version': version,
'file': filepath,
'type': 'required_provider'
}
self.required_providers.append(provider_entry)
self.providers.append(provider_entry)
# Extract backend configuration
backend = block.get('backend', [])
if backend:
for backend_config in backend:
if isinstance(backend_config, dict):
for backend_type, config in backend_config.items():
self.terraform_settings['backend'] = {
'type': backend_type,
'config': config
}
def _extract_providers(self, parsed: dict, filepath: str) -> None:
"""Extract provider configuration blocks."""
provider_blocks = parsed.get('provider', [])
for block in provider_blocks:
if isinstance(block, dict):
for name, config in block.items():
if isinstance(config, dict):
alias = config.get('alias')
region = config.get('region')
provider_key = (name, alias, filepath)
if provider_key in self._seen_provider_configs:
continue
self._seen_provider_configs.add(provider_key)
self.provider_configs.append({
'name': name,
'alias': alias,
'region': region,
'file': filepath,
'type': 'provider_config'
})
def _extract_modules(self, parsed: dict, filepath: str) -> None:
"""Extract module blocks."""
module_blocks = parsed.get('module', [])
for block in module_blocks:
if isinstance(block, dict):
for name, config in block.items():
if isinstance(config, dict):
source = config.get('source')
version = config.get('version')
# Extract providers passed to module
providers = config.get('providers')
# Determine module type based on source
module_type = self._determine_module_type(source) if source else 'unknown'
self.modules.append({
'name': name,
'source': source,
'version': version,
'type': module_type,
'providers': providers,
'file': filepath
})
def _determine_module_type(self, source: str) -> str:
"""Determine module type from source string.
Terraform module source types:
local - ./path or ../path
git - git:: prefix, git@ SSH, github.com/* shorthand,
bitbucket.org/* shorthand, any domain/org/repo pattern
mercurial - hg:: prefix
cloud_storage - s3:: or gcs:: prefix
http - https:// or http:// (zip archives)
registry - namespace/module/provider (no dots in first segment)
unknown - anything else
"""
source = source.strip()
if source.startswith('./') or source.startswith('../') or source.startswith('/'):
return 'local'
if source.startswith('git::') or source.startswith('git@'):
return 'git'
if source.startswith('hg::'):
return 'mercurial'
if source.startswith('s3::') or source.startswith('gcs::'):
return 'cloud_storage'
if source.startswith('https://') or source.startswith('http://'):
return 'http'
# Strip query and submodule selectors for source shape analysis.
base_source = source.split('?', 1)[0].split('//', 1)[0]
if not base_source:
return 'unknown'
if '.git' in base_source:
return 'git'
segments = [segment for segment in base_source.split('/') if segment]
if len(segments) == 3:
first_segment = segments[0].lower()
if first_segment in {'github.com', 'bitbucket.org', 'gitlab.com'}:
return 'git'
return 'registry'
if len(segments) == 4 and '.' in segments[0]:
# Private/remote registry source format:
# <hostname>/<namespace>/<name>/<provider>
return 'registry'
if len(segments) >= 3 and '.' in segments[0]:
# Domain-based shorthand VCS source (e.g. github.com/org/repo).
return 'git'
return 'unknown'
def _extract_resources(self, parsed: dict, filepath: str) -> None:
"""Extract resource blocks."""
resource_blocks = parsed.get('resource', [])
for block in resource_blocks:
if isinstance(block, dict):
for resource_type, instances in block.items():
if isinstance(instances, dict):
for resource_name, config in instances.items():
# Extract key attributes for analysis
count = config.get('count') if isinstance(config, dict) else None
for_each = config.get('for_each') if isinstance(config, dict) else None
depends_on = config.get('depends_on') if isinstance(config, dict) else None
lifecycle = config.get('lifecycle') if isinstance(config, dict) else None
self.resources.append({
'type': resource_type,
'name': resource_name,
'has_count': count is not None,
'has_for_each': for_each is not None,
'has_depends_on': depends_on is not None,
'has_lifecycle': lifecycle is not None,
'file': filepath
})
def _extract_data_sources(self, parsed: dict, filepath: str) -> None:
"""Extract data source blocks."""
data_blocks = parsed.get('data', [])
for block in data_blocks:
if isinstance(block, dict):
for data_type, instances in block.items():
if isinstance(instances, dict):
for data_name, config in instances.items():
self.data_sources.append({
'type': data_type,
'name': data_name,
'file': filepath
})
def _extract_variables(self, parsed: dict, filepath: str) -> None:
"""Extract variable declarations."""
variable_blocks = parsed.get('variable', [])
for block in variable_blocks:
if isinstance(block, dict):
for name, config in block.items():
if isinstance(config, dict):
var_type = config.get('type')
description = config.get('description')
default = config.get('default')
sensitive = config.get('sensitive', False)
nullable = config.get('nullable')
validation = config.get('validation')
# Convert type to string representation if it's a complex type
type_str = self._type_to_string(var_type)
self.variables.append({
'name': name,
'type': type_str,
'description': description,
'has_default': default is not None,
'sensitive': sensitive,
'nullable': nullable,
'has_validation': validation is not None,
'file': filepath
})
def _type_to_string(self, type_value: Any) -> str | None:
"""Convert type expression to string representation."""
if type_value is None:
return None
if isinstance(type_value, str):
return type_value
if isinstance(type_value, dict):
# Handle complex types like object({...}) or map(string)
return str(type_value)
if isinstance(type_value, list):
# Handle type expressions returned as lists
return ''.join(str(t) for t in type_value)
return str(type_value)
def _extract_outputs(self, parsed: dict, filepath: str) -> None:
"""Extract output declarations."""
output_blocks = parsed.get('output', [])
for block in output_blocks:
if isinstance(block, dict):
for name, config in block.items():
if isinstance(config, dict):
description = config.get('description')
sensitive = config.get('sensitive', False)
depends_on = config.get('depends_on')
self.outputs.append({
'name': name,
'description': description,
'sensitive': sensitive,
'has_depends_on': depends_on is not None,
'file': filepath
})
def _extract_locals(self, parsed: dict, filepath: str) -> None:
"""Extract local value definitions."""
locals_blocks = parsed.get('locals', [])
for block in locals_blocks:
if isinstance(block, dict):
for name in block.keys():
self.locals.append({
'name': name,
'file': filepath
})
def _extract_ephemeral_resources(self, parsed: dict, filepath: str) -> None:
"""Extract ephemeral resource blocks (Terraform 1.10+).
Ephemeral resources hold temporary values that are never stored in state
(e.g. passwords, API tokens). Their type prefix identifies the provider
exactly as regular resource types do, so they must be included in
implicit provider detection.
HCL structure mirrors resource blocks:
ephemeral "<type>" "<name>" { ... }
which python-hcl2 yields as:
{'ephemeral': [{'<type>': {'<name>': {...}}}]}
"""
ephemeral_blocks = parsed.get('ephemeral', [])
for block in ephemeral_blocks:
if isinstance(block, dict):
for ephemeral_type, instances in block.items():
if isinstance(instances, dict):
for ephemeral_name in instances:
self.ephemeral_resources.append({
'type': ephemeral_type,
'name': ephemeral_name,
'file': filepath
})
def _infer_provider_from_type(self, block_type: str, tf_type: str) -> str | None:
"""Infer provider name from Terraform resource/data type."""
if not tf_type:
return None
# Built-in terraform data source is not a provider plugin.
if block_type == 'data_source' and tf_type == 'terraform_remote_state':
return None
if '_' in tf_type:
provider_name = tf_type.split('_', 1)[0]
else:
provider_name = tf_type
if provider_name == 'terraform':
return None
return provider_name
def _collect_provider_analysis(self) -> None:
"""Collect explicit, implicit, and combined provider sets for docs lookup."""
explicit_provider_names = {
p['name'] for p in self.required_providers
if p.get('name')
}
explicit_provider_names.update(
p['name'] for p in self.provider_configs
if p.get('name')
)
seen_implicit_names: set[str] = set()
implicit: list[dict[str, str]] = []
for resource in self.resources:
resource_type = resource.get('type', '')
name = self._infer_provider_from_type('resource', resource_type)
if not name or name in explicit_provider_names or name in seen_implicit_names:
continue
seen_implicit_names.add(name)
implicit.append({
'name': name,
'detected_from': 'resource',
'type': resource_type,
'file': str(resource.get('file', ''))
})
for data_source in self.data_sources:
data_type = data_source.get('type', '')
name = self._infer_provider_from_type('data_source', data_type)
if not name or name in explicit_provider_names or name in seen_implicit_names:
continue
seen_implicit_names.add(name)
implicit.append({
'name': name,
'detected_from': 'data_source',
'type': data_type,
'file': str(data_source.get('file', ''))
})
for ephemeral in self.ephemeral_resources:
ephemeral_type = ephemeral.get('type', '')
name = self._infer_provider_from_type('ephemeral', ephemeral_type)
if not name or name in explicit_provider_names or name in seen_implicit_names:
continue
seen_implicit_names.add(name)
implicit.append({
'name': name,
'detected_from': 'ephemeral',
'type': ephemeral_type,
'file': str(ephemeral.get('file', ''))
})
self.implicit_providers = implicit
all_provider_names = sorted(explicit_provider_names | seen_implicit_names)
self.all_providers_for_docs = [
{
'name': provider_name,
'source': 'explicit' if provider_name in explicit_provider_names else 'implicit'
}
for provider_name in all_provider_names
]
def to_dict(self) -> dict[str, Any]:
"""Convert parsed data to dictionary."""
self._collect_provider_analysis()
explicit_provider_names = sorted({
p['name'] for p in self.required_providers + self.provider_configs
if p.get('name')
})
implicit_provider_names = sorted({
p['name'] for p in self.implicit_providers
if p.get('name')
})
return {
'terraform_settings': self.terraform_settings,
'parse_errors': self.parse_errors,
'providers': self.providers,
'required_providers': self.required_providers,
'provider_configs': self.provider_configs,
'implicit_providers': self.implicit_providers,
'all_providers_for_docs': self.all_providers_for_docs,
'modules': self.modules,
'resources': self.resources,
'data_sources': self.data_sources,
'ephemeral_resources': self.ephemeral_resources,
'variables': self.variables,
'outputs': self.outputs,
'locals': self.locals,
'provider_analysis': {
'explicit_provider_names': explicit_provider_names,
'implicit_provider_names': implicit_provider_names,
'all_provider_names_for_docs': [p['name'] for p in self.all_providers_for_docs]
},
'summary': {
'provider_count': len(self.providers),
'required_provider_count': len(self.required_providers),
'provider_config_count': len(self.provider_configs),
'implicit_provider_count': len(self.implicit_providers),
'providers_for_docs_count': len(self.all_providers_for_docs),
'module_count': len(self.modules),
'resource_count': len(self.resources),
'data_source_count': len(self.data_sources),
'ephemeral_resource_count': len(self.ephemeral_resources),
'variable_count': len(self.variables),
'output_count': len(self.outputs),
'local_count': len(self.locals),
'parse_error_count': len(self.parse_errors)
}
}
def to_json(self, indent: int = 2) -> str:
"""Convert parsed data to JSON string."""
return json.dumps(self.to_dict(), indent=indent, default=str)
def check_dependencies() -> bool:
"""Check if required dependencies are installed."""
if not HCL2_AVAILABLE:
print("Error: python-hcl2 is required but not installed.", file=sys.stderr)
print("", file=sys.stderr)
print("Install it with:", file=sys.stderr)
print(" pip install python-hcl2", file=sys.stderr)
print("", file=sys.stderr)
print("Or in a virtual environment:", file=sys.stderr)
print(" python -m venv venv", file=sys.stderr)
print(" source venv/bin/activate", file=sys.stderr)
print(" pip install python-hcl2", file=sys.stderr)
return False
return True
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Terraform Configuration Parser")
print("")
print("Usage: python extract_tf_info.py <path-to-tf-file-or-directory>")
print("")
print("Examples:")
print(" python extract_tf_info.py main.tf")
print(" python extract_tf_info.py ./terraform/")
print("")
print("Output: JSON structure with providers, modules, resources, and more")
sys.exit(1)
if not check_dependencies():
sys.exit(1)
target_path = sys.argv[1]
parser = TerraformParser()
if os.path.isfile(target_path):
if not target_path.endswith('.tf'):
print(f"Error: {target_path} is not a .tf file", file=sys.stderr)
sys.exit(1)
parser.parse_file(target_path)
elif os.path.isdir(target_path):
parser.parse_directory(target_path)
else:
print(f"Error: {target_path} is not a valid file or directory", file=sys.stderr)
sys.exit(1)
# Output JSON
print(parser.to_json())
if parser.parse_errors:
sys.exit(2)
if __name__ == "__main__":
main()
#!/bin/bash
# Checkov Installation Script with Virtual Environment
# This script installs Checkov in an isolated virtual environment and provides
# a wrapper script for easy execution, with automatic cleanup capabilities.
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default installation directory
DEFAULT_INSTALL_DIR="${HOME}/.local/checkov-venv"
INSTALL_DIR="${CHECKOV_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
WRAPPER_LINK="${HOME}/.local/bin/checkov"
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
AUTO_YES="false"
FORCE_RECREATE="false"
# Help message
show_help() {
cat << EOF
Checkov Installation Script with Virtual Environment
Usage: $(basename "$0") <command> [--yes] [--force]
This script installs Checkov in an isolated Python virtual environment,
creating a wrapper script for easy execution.
COMMANDS:
install Install Checkov in a virtual environment
uninstall Remove Checkov virtual environment and wrapper
upgrade Upgrade Checkov to the latest version
status Check installation status
-h, --help Show this help message
FLAGS:
-y, --yes Non-interactive mode; accept confirmation prompts
--force Recreate install dir during install if it already exists
ENVIRONMENT VARIABLES:
CHECKOV_INSTALL_DIR Custom installation directory (default: ~/.local/checkov-venv)
EXAMPLES:
# Install Checkov
$(basename "$0") install --yes
# Check installation status
$(basename "$0") status
# Upgrade Checkov
$(basename "$0") upgrade
# Uninstall Checkov
$(basename "$0") uninstall --yes
NOTES:
- Requires Python 3.9 or higher
- Creates a wrapper script at ~/.local/bin/checkov
- Isolated installation prevents dependency conflicts
EOF
}
# Check Python version
check_python() {
if ! command -v python3 &> /dev/null; then
echo -e "${RED}ERROR: python3 is not installed${NC}" >&2
echo "Install Python 3.9 or higher and try again" >&2
exit 1
fi
local python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
local major=$(echo "$python_version" | cut -d. -f1)
local minor=$(echo "$python_version" | cut -d. -f2)
if [ "$major" -lt 3 ] || ([ "$major" -eq 3 ] && [ "$minor" -lt 9 ]); then
echo -e "${RED}ERROR: Python 3.9 or higher is required${NC}" >&2
echo "Current version: $python_version" >&2
echo "Please upgrade Python and try again" >&2
exit 1
fi
echo -e "${GREEN}✓${NC} Python version: $python_version"
}
# Create virtual environment
create_venv() {
echo -e "${BLUE}Creating virtual environment at: ${INSTALL_DIR}${NC}"
if [ -d "$INSTALL_DIR" ]; then
echo -e "${YELLOW}Virtual environment already exists${NC}"
if [ "$FORCE_RECREATE" = "true" ] || [ "$AUTO_YES" = "true" ]; then
rm -rf "$INSTALL_DIR"
else
read -p "Remove and recreate? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$INSTALL_DIR"
else
echo "Installation cancelled"
exit 0
fi
fi
fi
python3 -m venv "$INSTALL_DIR"
echo -e "${GREEN}✓${NC} Virtual environment created"
}
# Install Checkov
install_checkov() {
echo -e "${BLUE}Installing Checkov...${NC}"
# Activate virtual environment and install
source "$INSTALL_DIR/bin/activate"
# Upgrade pip and setuptools
echo "Upgrading pip and setuptools..."
pip install --upgrade pip setuptools wheel --quiet
# Install checkov
echo "Installing checkov..."
pip install checkov --quiet
deactivate
# Get installed version
local version=$("$INSTALL_DIR/bin/checkov" --version 2>&1 | head -n 1)
echo -e "${GREEN}✓${NC} Checkov installed: $version"
}
# Create wrapper script
create_wrapper() {
echo -e "${BLUE}Creating wrapper script...${NC}"
# Ensure ~/.local/bin exists
mkdir -p "$(dirname "$WRAPPER_LINK")"
# Create wrapper script
cat > "$WRAPPER_LINK" << WRAPPER_EOF
#!/bin/bash
# Checkov wrapper script - executes checkov from virtual environment
VENV_DIR="\${CHECKOV_INSTALL_DIR:-\$HOME/.local/checkov-venv}"
INSTALL_SCRIPT_PATH="$SCRIPT_PATH"
if [ ! -d "\$VENV_DIR" ]; then
echo "ERROR: Checkov virtual environment not found at: \$VENV_DIR" >&2
if [ -f "\$INSTALL_SCRIPT_PATH" ]; then
echo "Run: bash \"\$INSTALL_SCRIPT_PATH\" install" >&2
else
echo "Run install_checkov.sh install from terraform-validator/scripts" >&2
fi
exit 1
fi
exec "\$VENV_DIR/bin/checkov" "\$@"
WRAPPER_EOF
chmod +x "$WRAPPER_LINK"
echo -e "${GREEN}✓${NC} Wrapper created at: $WRAPPER_LINK"
}
# Check if wrapper is in PATH
check_path() {
local bin_dir=$(dirname "$WRAPPER_LINK")
if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
echo ""
echo -e "${YELLOW}WARNING: $bin_dir is not in your PATH${NC}"
echo ""
echo "Add it to your PATH by adding this line to your shell profile:"
echo ""
echo -e "${BLUE}export PATH=\"$bin_dir:\$PATH\"${NC}"
echo ""
echo "Shell profiles: ~/.bashrc, ~/.zshrc, ~/.bash_profile"
fi
}
# Install command
do_install() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Checkov Installation${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
check_python
create_venv
install_checkov
create_wrapper
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}Installation Complete!${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Checkov is installed at: $INSTALL_DIR"
echo "Wrapper script: $WRAPPER_LINK"
echo ""
check_path
echo ""
echo "Test the installation:"
echo -e "${BLUE}checkov --version${NC}"
echo ""
}
# Uninstall command
do_uninstall() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Checkov Uninstallation${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
if [ ! -d "$INSTALL_DIR" ] && [ ! -f "$WRAPPER_LINK" ]; then
echo "Checkov is not installed"
exit 0
fi
echo "This will remove:"
[ -d "$INSTALL_DIR" ] && echo " - Virtual environment: $INSTALL_DIR"
[ -f "$WRAPPER_LINK" ] && echo " - Wrapper script: $WRAPPER_LINK"
echo ""
if [ "$AUTO_YES" != "true" ]; then
read -p "Continue with uninstallation? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Uninstallation cancelled"
exit 0
fi
fi
# Remove virtual environment
if [ -d "$INSTALL_DIR" ]; then
echo "Removing virtual environment..."
rm -rf "$INSTALL_DIR"
echo -e "${GREEN}✓${NC} Virtual environment removed"
fi
# Remove wrapper
if [ -f "$WRAPPER_LINK" ]; then
echo "Removing wrapper script..."
rm -f "$WRAPPER_LINK"
echo -e "${GREEN}✓${NC} Wrapper script removed"
fi
echo ""
echo -e "${GREEN}Uninstallation complete${NC}"
}
# Upgrade command
do_upgrade() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Checkov Upgrade${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
if [ ! -d "$INSTALL_DIR" ]; then
echo -e "${RED}ERROR: Checkov is not installed${NC}" >&2
echo "Run: $(basename "$0") install" >&2
exit 1
fi
# Get current version
local current_version=$("$INSTALL_DIR/bin/checkov" --version 2>&1 | head -n 1)
echo "Current version: $current_version"
echo ""
echo "Upgrading checkov..."
# Activate and upgrade
source "$INSTALL_DIR/bin/activate"
pip install --upgrade checkov --quiet
deactivate
# Get new version
local new_version=$("$INSTALL_DIR/bin/checkov" --version 2>&1 | head -n 1)
echo ""
echo -e "${GREEN}✓${NC} Upgrade complete"
echo "New version: $new_version"
}
# Status command
do_status() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Checkov Installation Status${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Check Python
if command -v python3 &> /dev/null; then
local python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
echo -e "Python: ${GREEN}✓${NC} $python_version"
else
echo -e "Python: ${RED}✗${NC} Not installed"
fi
# Check virtual environment
if [ -d "$INSTALL_DIR" ]; then
echo -e "Virtual Environment: ${GREEN}✓${NC} $INSTALL_DIR"
else
echo -e "Virtual Environment: ${RED}✗${NC} Not found"
fi
# Check wrapper
if [ -f "$WRAPPER_LINK" ]; then
echo -e "Wrapper Script: ${GREEN}✓${NC} $WRAPPER_LINK"
else
echo -e "Wrapper Script: ${RED}✗${NC} Not found"
fi
# Check if checkov is accessible
if command -v checkov &> /dev/null; then
local version=$(checkov --version 2>&1 | head -n 1)
echo -e "Checkov Command: ${GREEN}✓${NC} $version"
else
echo -e "Checkov Command: ${RED}✗${NC} Not in PATH"
fi
echo ""
# Installation status summary
if [ -d "$INSTALL_DIR" ] && [ -f "$WRAPPER_LINK" ]; then
echo -e "${GREEN}Status: Installed${NC}"
check_path
else
echo -e "${YELLOW}Status: Not installed or incomplete${NC}"
echo ""
echo "To install, run:"
echo -e "${BLUE}$(basename "$0") install${NC}"
fi
}
# Main execution
main() {
local command=""
while [[ $# -gt 0 ]]; do
case "$1" in
install|uninstall|upgrade|status|-h|--help|help)
if [ -n "$command" ]; then
echo "ERROR: Multiple commands specified: $command and $1" >&2
exit 1
fi
command="$1"
shift
;;
-y|--yes)
AUTO_YES="true"
shift
;;
--force)
FORCE_RECREATE="true"
shift
;;
*)
echo "ERROR: Unknown argument: $1" >&2
echo ""
show_help
exit 1
;;
esac
done
case "$command" in
install)
do_install
;;
uninstall)
do_uninstall
;;
upgrade)
do_upgrade
;;
status)
do_status
;;
-h|--help|help)
show_help
;;
"")
echo "ERROR: No command specified" >&2
echo ""
show_help
exit 1
;;
esac
}
# Run main function only when executed directly.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
#!/bin/bash
# Checkov Terraform Security Scanner Wrapper Script
# Provides stable CLI parsing and predictable exit handling.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_SCRIPT="$SCRIPT_DIR/install_checkov.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
OUTPUT_FORMAT="cli"
DOWNLOAD_MODULES="false"
COMPACT_OUTPUT="false"
QUIET_MODE="false"
SKIP_CHECKS=""
RUN_CHECKS=""
SCAN_PATH=""
# Includes commonly used checkov formats.
ALLOWED_FORMATS=("cli" "json" "sarif" "gitlab_sast" "csv" "junitxml" "cyclonedx" "cyclonedx_json" "github_failed_only" "spdx")
show_help() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] <path>
Run Checkov security scanner on Terraform configurations.
ARGUMENTS:
path Path to Terraform file or directory to scan
OPTIONS:
-f, --format FORMAT Output format (default: cli)
-d, --download-modules Download external Terraform modules before scanning
-c, --compact Show compact output (only failed checks)
-q, --quiet Suppress informational output
--skip CHECKS Comma-separated list of checks to skip (e.g., CKV_AWS_20,CKV_AWS_21)
--check CHECKS Comma-separated list of checks to run (only these)
-h, --help Show this help message
EXAMPLES:
# Scan a directory with default settings
$(basename "$0") ./terraform
# Scan with JSON output
$(basename "$0") -f json ./terraform
# Scan and download external modules
$(basename "$0") -d ./terraform
# Scan with specific checks only
$(basename "$0") --check CKV_AWS_20,CKV_AWS_57 ./terraform
# Skip specific checks
$(basename "$0") --skip CKV_AWS_* ./terraform
# Scan Terraform plan JSON as file input
$(basename "$0") -f json ./tfplan.json
EOF
}
is_allowed_format() {
local format="$1"
local allowed
for allowed in "${ALLOWED_FORMATS[@]}"; do
if [ "$allowed" = "$format" ]; then
return 0
fi
done
return 1
}
require_value() {
local flag="$1"
local value="${2:-}"
if [ -z "$value" ] || [[ "$value" == -* ]]; then
echo -e "${RED}ERROR: $flag requires a value${NC}" >&2
echo "Use -h or --help for usage information" >&2
exit 1
fi
}
check_checkov_installed() {
if ! command -v checkov >/dev/null 2>&1; then
echo -e "${RED}ERROR: checkov is not installed${NC}" >&2
echo "" >&2
echo "Install checkov using one of these methods:" >&2
echo " pip3 install checkov" >&2
echo " brew install checkov (macOS only)" >&2
if [ -f "$INSTALL_SCRIPT" ]; then
echo " bash $INSTALL_SCRIPT install" >&2
fi
echo "" >&2
echo "For more information, visit: https://www.checkov.io/" >&2
exit 1
fi
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-f|--format)
require_value "$1" "${2:-}"
OUTPUT_FORMAT="$2"
shift 2
;;
-d|--download-modules)
DOWNLOAD_MODULES="true"
shift
;;
-c|--compact)
COMPACT_OUTPUT="true"
shift
;;
-q|--quiet)
QUIET_MODE="true"
shift
;;
--skip)
require_value "$1" "${2:-}"
SKIP_CHECKS="$2"
shift 2
;;
--check)
require_value "$1" "${2:-}"
RUN_CHECKS="$2"
shift 2
;;
-*)
echo -e "${RED}ERROR: Unknown option: $1${NC}" >&2
echo "Use -h or --help for usage information" >&2
exit 1
;;
*)
if [ -n "$SCAN_PATH" ]; then
echo -e "${RED}ERROR: Multiple paths provided: $SCAN_PATH and $1${NC}" >&2
exit 1
fi
SCAN_PATH="$1"
shift
;;
esac
done
if [ -z "$SCAN_PATH" ]; then
echo -e "${RED}ERROR: Path argument is required${NC}" >&2
echo "Use -h or --help for usage information" >&2
exit 1
fi
if [ ! -e "$SCAN_PATH" ]; then
echo -e "${RED}ERROR: Path does not exist: $SCAN_PATH${NC}" >&2
exit 1
fi
if ! is_allowed_format "$OUTPUT_FORMAT"; then
echo -e "${RED}ERROR: Invalid output format: $OUTPUT_FORMAT${NC}" >&2
echo "Allowed formats: ${ALLOWED_FORMATS[*]}" >&2
exit 1
fi
}
build_command() {
local cmd=(checkov)
if [ -f "$SCAN_PATH" ]; then
cmd+=(-f "$SCAN_PATH")
else
cmd+=(-d "$SCAN_PATH")
fi
if [ "$OUTPUT_FORMAT" != "cli" ]; then
cmd+=(-o "$OUTPUT_FORMAT")
fi
if [ "$DOWNLOAD_MODULES" = "true" ]; then
cmd+=(--download-external-modules true)
fi
if [ "$COMPACT_OUTPUT" = "true" ]; then
cmd+=(--compact)
fi
if [ "$QUIET_MODE" = "true" ]; then
cmd+=(--quiet)
fi
if [ -n "$SKIP_CHECKS" ]; then
cmd+=(--skip-check "$SKIP_CHECKS")
fi
if [ -n "$RUN_CHECKS" ]; then
cmd+=(--check "$RUN_CHECKS")
fi
CHECKOV_CMD=("${cmd[@]}")
}
print_command() {
local rendered=""
local arg
for arg in "${CHECKOV_CMD[@]}"; do
rendered+=$(printf "%q " "$arg")
done
echo "$rendered"
}
main() {
parse_args "$@"
check_checkov_installed
build_command
if [ "$QUIET_MODE" != "true" ]; then
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Checkov Security Scanner${NC}"
echo -e "${BLUE}========================================${NC}"
echo -e "Target: ${GREEN}$SCAN_PATH${NC}"
echo -e "Format: ${GREEN}$OUTPUT_FORMAT${NC}"
[ "$DOWNLOAD_MODULES" = "true" ] && echo -e "Modules: ${GREEN}Download enabled${NC}"
[ -n "$SKIP_CHECKS" ] && echo -e "Skip: ${YELLOW}$SKIP_CHECKS${NC}"
[ -n "$RUN_CHECKS" ] && echo -e "Run: ${YELLOW}$RUN_CHECKS${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo -e "${BLUE}Running: ${NC}$(print_command)"
echo ""
fi
set +e
"${CHECKOV_CMD[@]}"
exit_code=$?
set -e
if [ "$QUIET_MODE" != "true" ]; then
echo ""
echo -e "${BLUE}========================================${NC}"
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}Scan completed: No security issues found${NC}"
else
echo -e "${YELLOW}Scan completed: Security issues detected or scanner returned non-zero exit${NC}"
echo -e "Review the output above for details"
fi
echo -e "${BLUE}========================================${NC}"
fi
exit $exit_code
}
main "$@"
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.100.0"
constraints = ">= 5.0.0, ~> 5.0"
hashes = [
"h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=",
"zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
"zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
"zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
"zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
"zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
"zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
"zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
"zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
"zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
"zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
"zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
"zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
"zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.7.2"
constraints = "~> 3.5"
hashes = [
"h1:KG4NuIBl1mRWU0KD/BGfCi1YN/j3F7H4YgeeM7iSdNs=",
"zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f",
"zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc",
"zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab",
"zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3",
"zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212",
"zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34",
"zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967",
"zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d",
"zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62",
"zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0",
]
}
# Sample Terraform configuration for testing the validator skill
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}
provider "aws" {
region = var.aws_region
}
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
# Example VPC resource
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "Terraform"
}
}
# Example module usage
module "vpc_networking" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2"
name = "${var.environment}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
Related skills
How it compares
Pick terraform-validator for agent-assisted Terraform plan and HCL review when you want contextual explanations alongside policy checks rather than only static linter output.
FAQ
What does terraform-validator check in Terraform code?
terraform-validator inspects Terraform plans and HCL for policy violations, unsafe defaults, missing remote backends, and provider misconfigurations. It is intended to run before merge or terraform apply so infrastructure issues are caught in CI rather than production.
When should terraform-validator run in a pipeline?
terraform-validator should run after terraform plan generates output and before merge or terraform apply executes. Running it at that stage catches backend gaps, unsafe defaults, and provider misconfigurations while changes are still reversible in a pull request.