Terraform Patterns Predictable infrastructure. Secure state. Modules that compose. No drift. Opinionated Terraform workflow that turns sprawling HCL into well-structured, secure, production-grade infrastructure code. Covers module design, state management, provider patterns, security hardening, and CI/CD integration. Not a Terraform tutorial — a set of concrete decisions about how to write infrastructure code that doesn't break at 3 AM. --- Slash Commands | Command | What it does | |---------|-------------| | | Analyze Terraform code for anti-patterns, security issues, and structure problems…

)\n\n# Expected files in a well-structured module\nEXPECTED_FILES = {\n \"main.tf\": \"Primary resources\",\n \"variables.tf\": \"Input variables\",\n \"outputs.tf\": \"Output values\",\n \"versions.tf\": \"Provider and Terraform version requirements\",\n}\n\nOPTIONAL_FILES = {\n \"locals.tf\": \"Computed local values\",\n \"data.tf\": \"Data sources\",\n \"backend.tf\": \"Remote state backend configuration\",\n \"providers.tf\": \"Provider configuration\",\n \"README.md\": \"Module documentation\",\n}\n\n\ndef find_tf_files(directory):\n \"\"\"Find all .tf files in a directory (non-recursive).\"\"\"\n tf_files = {}\n for entry in sorted(os.listdir(directory)):\n if entry.endswith(\".tf\"):\n filepath = os.path.join(directory, entry)\n with open(filepath, encoding=\"utf-8\") as f:\n tf_files[entry] = f.read()\n return tf_files\n\n\ndef parse_resources(content):\n \"\"\"Extract resource declarations from HCL content.\"\"\"\n resources = []\n for match in re.finditer(\n r'^resource\\s+\"([^\"]+)\"\\s+\"([^\"]+)\"', content, re.MULTILINE\n ):\n resources.append({\n \"type\": match.group(1),\n \"name\": match.group(2),\n \"provider\": match.group(1).split(\"_\")[0],\n })\n return resources\n\n\ndef parse_data_sources(content):\n \"\"\"Extract data source declarations.\"\"\"\n sources = []\n for match in re.finditer(\n r'^data\\s+\"([^\"]+)\"\\s+\"([^\"]+)\"', content, re.MULTILINE\n ):\n sources.append({\"type\": match.group(1), \"name\": match.group(2)})\n return sources\n\n\ndef parse_variables(content):\n \"\"\"Extract variable declarations with metadata.\"\"\"\n variables = []\n # Match variable blocks\n for match in re.finditer(\n r'^variable\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.MULTILINE | re.DOTALL,\n ):\n name = match.group(1)\n body = match.group(2)\n var = {\n \"name\": name,\n \"has_description\": \"description\" in body,\n \"has_type\": bool(re.search(r'\\btype\\s*=', body)),\n \"has_default\": bool(re.search(r'\\bdefault\\s*=', body)),\n \"has_validation\": \"validation\" in body,\n \"is_sensitive\": \"sensitive\" in body and bool(\n re.search(r'\\bsensitive\\s*=\\s*true', body)\n ),\n }\n variables.append(var)\n return variables\n\n\ndef parse_outputs(content):\n \"\"\"Extract output declarations with metadata.\"\"\"\n outputs = []\n for match in re.finditer(\n r'^output\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.MULTILINE | re.DOTALL,\n ):\n name = match.group(1)\n body = match.group(2)\n out = {\n \"name\": name,\n \"has_description\": \"description\" in body,\n \"is_sensitive\": \"sensitive\" in body and bool(\n re.search(r'\\bsensitive\\s*=\\s*true', body)\n ),\n }\n outputs.append(out)\n return outputs\n\n\ndef parse_modules(content):\n \"\"\"Extract module calls.\"\"\"\n modules = []\n for match in re.finditer(\n r'^module\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.MULTILINE | re.DOTALL,\n ):\n name = match.group(1)\n body = match.group(2)\n source_match = re.search(r'source\\s*=\\s*\"([^\"]+)\"', body)\n source = source_match.group(1) if source_match else \"unknown\"\n modules.append({\"name\": name, \"source\": source})\n return modules\n\n\ndef check_naming(resources, data_sources):\n \"\"\"Check naming conventions.\"\"\"\n issues = []\n for r in resources:\n if not VALID_RESOURCE_NAME.match(r[\"name\"]):\n issues.append({\n \"severity\": \"medium\",\n \"message\": f\"Resource '{r['type']}.{r['name']}' uses non-standard naming — use lowercase with underscores\",\n })\n if r[\"name\"].startswith(r[\"provider\"] + \"_\"):\n issues.append({\n \"severity\": \"low\",\n \"message\": f\"Resource '{r['type']}.{r['name']}' name repeats the provider prefix — redundant\",\n })\n for d in data_sources:\n if not VALID_RESOURCE_NAME.match(d[\"name\"]):\n issues.append({\n \"severity\": \"medium\",\n \"message\": f\"Data source '{d['type']}.{d['name']}' uses non-standard naming\",\n })\n return issues\n\n\ndef check_variables(variables):\n \"\"\"Check variable quality.\"\"\"\n issues = []\n for v in variables:\n if not v[\"has_description\"]:\n issues.append({\n \"severity\": \"medium\",\n \"message\": f\"Variable '{v['name']}' missing description — consumers won't know what to provide\",\n })\n if not v[\"has_type\"]:\n issues.append({\n \"severity\": \"high\",\n \"message\": f\"Variable '{v['name']}' missing type constraint — accepts any value\",\n })\n # Check if name suggests a secret\n secret_patterns = [\"password\", \"secret\", \"token\", \"key\", \"api_key\", \"credentials\"]\n name_lower = v[\"name\"].lower()\n if any(p in name_lower for p in secret_patterns) and not v[\"is_sensitive\"]:\n issues.append({\n \"severity\": \"high\",\n \"message\": f\"Variable '{v['name']}' looks like a secret but is not marked sensitive = true\",\n })\n return issues\n\n\ndef check_outputs(outputs):\n \"\"\"Check output quality.\"\"\"\n issues = []\n for o in outputs:\n if not o[\"has_description\"]:\n issues.append({\n \"severity\": \"low\",\n \"message\": f\"Output '{o['name']}' missing description\",\n })\n return issues\n\n\ndef check_file_structure(tf_files):\n \"\"\"Check if expected files are present.\"\"\"\n issues = []\n filenames = set(tf_files.keys())\n for expected, purpose in EXPECTED_FILES.items():\n if expected not in filenames:\n issues.append({\n \"severity\": \"medium\" if expected != \"versions.tf\" else \"high\",\n \"message\": f\"Missing '{expected}' — {purpose}\",\n })\n return issues\n\n\ndef analyze_directory(tf_files):\n \"\"\"Run full analysis on a set of .tf files.\"\"\"\n all_content = \"\\n\".join(tf_files.values())\n\n resources = parse_resources(all_content)\n data_sources = parse_data_sources(all_content)\n variables = parse_variables(all_content)\n outputs = parse_outputs(all_content)\n modules = parse_modules(all_content)\n\n # Collect findings\n findings = []\n findings.extend(check_file_structure(tf_files))\n findings.extend(check_naming(resources, data_sources))\n findings.extend(check_variables(variables))\n findings.extend(check_outputs(outputs))\n\n # Check for backend configuration\n has_backend = any(\n re.search(r'\\bbackend\\s+\"', content)\n for content in tf_files.values()\n )\n if not has_backend:\n findings.append({\n \"severity\": \"high\",\n \"message\": \"No remote backend configured — state is stored locally\",\n })\n\n # Check for terraform required_version\n has_tf_version = any(\n re.search(r'required_version\\s*=', content)\n for content in tf_files.values()\n )\n if not has_tf_version:\n findings.append({\n \"severity\": \"medium\",\n \"message\": \"No required_version constraint — any Terraform version can be used\",\n })\n\n # Providers in child modules check\n for filename, content in tf_files.items():\n if filename not in (\"providers.tf\", \"versions.tf\", \"backend.tf\"):\n if re.search(r'^provider\\s+\"', content, re.MULTILINE):\n findings.append({\n \"severity\": \"medium\",\n \"message\": f\"Provider configuration found in '{filename}' — keep providers in root module only\",\n })\n\n # Sort findings\n severity_order = {\"critical\": 0, \"high\": 1, \"medium\": 2, \"low\": 3}\n findings.sort(key=lambda f: severity_order.get(f[\"severity\"], 4))\n\n # Unique providers\n providers = sorted(set(r[\"provider\"] for r in resources))\n\n return {\n \"files\": sorted(tf_files.keys()),\n \"file_count\": len(tf_files),\n \"resources\": resources,\n \"resource_count\": len(resources),\n \"data_sources\": data_sources,\n \"data_source_count\": len(data_sources),\n \"variables\": variables,\n \"variable_count\": len(variables),\n \"outputs\": outputs,\n \"output_count\": len(outputs),\n \"modules\": modules,\n \"module_count\": len(modules),\n \"providers\": providers,\n \"findings\": findings,\n }\n\n\ndef generate_report(analysis, output_format=\"text\"):\n \"\"\"Generate analysis report.\"\"\"\n findings = analysis[\"findings\"]\n\n # Score\n deductions = {\"critical\": 25, \"high\": 15, \"medium\": 5, \"low\": 2}\n score = max(0, 100 - sum(deductions.get(f[\"severity\"], 0) for f in findings))\n\n counts = {\n \"critical\": sum(1 for f in findings if f[\"severity\"] == \"critical\"),\n \"high\": sum(1 for f in findings if f[\"severity\"] == \"high\"),\n \"medium\": sum(1 for f in findings if f[\"severity\"] == \"medium\"),\n \"low\": sum(1 for f in findings if f[\"severity\"] == \"low\"),\n }\n\n result = {\n \"score\": score,\n \"files\": analysis[\"files\"],\n \"resource_count\": analysis[\"resource_count\"],\n \"data_source_count\": analysis[\"data_source_count\"],\n \"variable_count\": analysis[\"variable_count\"],\n \"output_count\": analysis[\"output_count\"],\n \"module_count\": analysis[\"module_count\"],\n \"providers\": analysis[\"providers\"],\n \"findings\": findings,\n \"finding_counts\": counts,\n }\n\n if output_format == \"json\":\n print(json.dumps(result, indent=2))\n return result\n\n # Text output\n print(f\"\\n{'=' * 60}\")\n print(f\" Terraform Module Analysis Report\")\n print(f\"{'=' * 60}\")\n print(f\" Score: {score}/100\")\n print(f\" Files: {', '.join(analysis['files'])}\")\n print(f\" Providers: {', '.join(analysis['providers']) if analysis['providers'] else 'none detected'}\")\n print()\n print(f\" Resources: {analysis['resource_count']} | Data Sources: {analysis['data_source_count']}\")\n print(f\" Variables: {analysis['variable_count']} | Outputs: {analysis['output_count']} | Modules: {analysis['module_count']}\")\n print()\n print(f\" Findings: {counts['critical']} critical | {counts['high']} high | {counts['medium']} medium | {counts['low']} low\")\n print(f\"{'─' * 60}\")\n\n for f in findings:\n icon = {\"critical\": \"!!!\", \"high\": \"!!\", \"medium\": \"!\", \"low\": \"~\"}.get(f[\"severity\"], \"?\")\n print(f\"\\n {icon} {f['severity'].upper()}\")\n print(f\" {f['message']}\")\n\n if not findings:\n print(\"\\n No issues found. Module structure looks good.\")\n\n print(f\"\\n{'=' * 60}\\n\")\n return result\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"terraform-patterns: Terraform module analyzer\"\n )\n parser.add_argument(\n \"directory\", nargs=\"?\",\n help=\"Path to Terraform directory (omit for demo)\",\n )\n parser.add_argument(\n \"--output\", \"-o\",\n choices=[\"text\", \"json\"],\n default=\"text\",\n help=\"Output format (default: text)\",\n )\n args = parser.parse_args()\n\n if args.directory:\n dirpath = Path(args.directory)\n if not dirpath.is_dir():\n print(f\"Error: Not a directory: {args.directory}\", file=sys.stderr)\n sys.exit(1)\n tf_files = find_tf_files(str(dirpath))\n if not tf_files:\n print(f\"Error: No .tf files found in {args.directory}\", file=sys.stderr)\n sys.exit(1)\n else:\n print(\"No directory provided. Running demo analysis...\\n\")\n tf_files = DEMO_FILES\n\n analysis = analyze_directory(tf_files)\n generate_report(analysis, args.output)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13995,"content_sha256":"b191076b4b1566bbcb0601b230a467a9d56c32aa707b9055e674224a44b94847"},{"filename":"scripts/tf_security_scanner.py","content":"#!/usr/bin/env python3\n\"\"\"\nterraform-patterns: Terraform Security Scanner\n\nScan .tf files for common security issues including hardcoded secrets,\noverly permissive IAM policies, open security groups, missing encryption,\nand sensitive variable misuse.\n\nUsage:\n python scripts/tf_security_scanner.py ./terraform\n python scripts/tf_security_scanner.py ./terraform --output json\n python scripts/tf_security_scanner.py ./terraform --strict\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nfrom pathlib import Path\n\n\n# --- Demo Terraform File ---\n\nDEMO_TF = \"\"\"\nprovider \"aws\" {\n region = \"us-east-1\"\n access_key = \"AKIAIOSFODNN7EXAMPLE\"\n secret_key = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"\n}\n\nvariable \"db_password\" {\n type = string\n default = \"supersecret123\"\n}\n\nresource \"aws_instance\" \"web\" {\n ami = \"ami-12345678\"\n instance_type = \"t3.micro\"\n\n tags = {\n Name = \"web-server\"\n }\n}\n\nresource \"aws_security_group\" \"web\" {\n name = \"web-sg\"\n\n ingress {\n from_port = 22\n to_port = 22\n protocol = \"tcp\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n\n ingress {\n from_port = 0\n to_port = 65535\n protocol = \"tcp\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n\n egress {\n from_port = 0\n to_port = 0\n protocol = \"-1\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n}\n\nresource \"aws_iam_policy\" \"admin\" {\n name = \"admin-policy\"\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Effect = \"Allow\"\n Action = \"*\"\n Resource = \"*\"\n }\n ]\n })\n}\n\nresource \"aws_s3_bucket\" \"data\" {\n bucket = \"my-data-bucket\"\n}\n\nresource \"aws_db_instance\" \"main\" {\n engine = \"mysql\"\n instance_class = \"db.t3.micro\"\n password = \"hardcoded-password\"\n publicly_accessible = true\n skip_final_snapshot = true\n}\n\"\"\"\n\n# --- Security Rules ---\n\nSECRET_PATTERNS = [\n {\n \"id\": \"SEC001\",\n \"name\": \"aws_access_key\",\n \"severity\": \"critical\",\n \"pattern\": r'(?:access_key|aws_access_key_id)\\s*=\\s*\"(AKIA[A-Z0-9]{16})\"',\n \"message\": \"AWS access key hardcoded in configuration\",\n \"fix\": \"Use environment variables, AWS profiles, or IAM roles instead\",\n },\n {\n \"id\": \"SEC002\",\n \"name\": \"aws_secret_key\",\n \"severity\": \"critical\",\n \"pattern\": r'(?:secret_key|aws_secret_access_key)\\s*=\\s*\"[A-Za-z0-9/+=]{40}\"',\n \"message\": \"AWS secret key hardcoded in configuration\",\n \"fix\": \"Use environment variables, AWS profiles, or IAM roles instead\",\n },\n {\n \"id\": \"SEC003\",\n \"name\": \"generic_password\",\n \"severity\": \"critical\",\n \"pattern\": r'(?:password|passwd)\\s*=\\s*\"[^\"]{4,}\"',\n \"message\": \"Password hardcoded in resource or provider configuration\",\n \"fix\": \"Use a variable with sensitive = true, or fetch from Vault/SSM/Secrets Manager\",\n },\n {\n \"id\": \"SEC004\",\n \"name\": \"generic_secret\",\n \"severity\": \"critical\",\n \"pattern\": r'(?:secret|token|api_key)\\s*=\\s*\"[^\"]{8,}\"',\n \"message\": \"Secret or token hardcoded in configuration\",\n \"fix\": \"Use a sensitive variable or secrets manager\",\n },\n {\n \"id\": \"SEC005\",\n \"name\": \"private_key\",\n \"severity\": \"critical\",\n \"pattern\": r'-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----',\n \"message\": \"Private key embedded in Terraform configuration\",\n \"fix\": \"Reference key file with file() function or use secrets manager\",\n },\n]\n\nIAM_PATTERNS = [\n {\n \"id\": \"SEC010\",\n \"name\": \"iam_wildcard_action\",\n \"severity\": \"critical\",\n \"pattern\": r'Action\\s*=\\s*\"\\*\"',\n \"message\": \"IAM policy with wildcard Action = \\\"*\\\" — grants all permissions\",\n \"fix\": \"Scope Action to specific services and operations\",\n },\n {\n \"id\": \"SEC011\",\n \"name\": \"iam_wildcard_resource\",\n \"severity\": \"high\",\n \"pattern\": r'Resource\\s*=\\s*\"\\*\"',\n \"message\": \"IAM policy with wildcard Resource = \\\"*\\\" — applies to all resources\",\n \"fix\": \"Scope Resource to specific ARN patterns\",\n },\n {\n \"id\": \"SEC012\",\n \"name\": \"iam_star_star\",\n \"severity\": \"critical\",\n \"pattern\": r'Action\\s*=\\s*\"\\*\"[^}]*Resource\\s*=\\s*\"\\*\"',\n \"message\": \"IAM policy with Action=* AND Resource=* — effectively admin access\",\n \"fix\": \"Follow least-privilege: grant only the specific actions and resources needed\",\n },\n]\n\nNETWORK_PATTERNS = [\n {\n \"id\": \"SEC020\",\n \"name\": \"sg_ssh_open\",\n \"severity\": \"critical\",\n \"pattern\": None, # Custom check\n \"message\": \"Security group allows SSH (port 22) from 0.0.0.0/0\",\n \"fix\": \"Restrict to known CIDR blocks, or use SSM Session Manager instead\",\n },\n {\n \"id\": \"SEC021\",\n \"name\": \"sg_rdp_open\",\n \"severity\": \"critical\",\n \"pattern\": None, # Custom check\n \"message\": \"Security group allows RDP (port 3389) from 0.0.0.0/0\",\n \"fix\": \"Restrict to known CIDR blocks, or use a bastion host\",\n },\n {\n \"id\": \"SEC022\",\n \"name\": \"sg_all_ports\",\n \"severity\": \"critical\",\n \"pattern\": None, # Custom check\n \"message\": \"Security group allows all ports (0-65535) from 0.0.0.0/0\",\n \"fix\": \"Open only the specific ports your application needs\",\n },\n]\n\nENCRYPTION_PATTERNS = [\n {\n \"id\": \"SEC030\",\n \"name\": \"s3_no_encryption\",\n \"severity\": \"high\",\n \"pattern\": None, # Custom check\n \"message\": \"S3 bucket without server-side encryption configuration\",\n \"fix\": \"Add aws_s3_bucket_server_side_encryption_configuration resource\",\n },\n {\n \"id\": \"SEC031\",\n \"name\": \"rds_no_encryption\",\n \"severity\": \"high\",\n \"pattern\": None, # Custom check\n \"message\": \"RDS instance without storage encryption\",\n \"fix\": \"Set storage_encrypted = true on aws_db_instance\",\n },\n {\n \"id\": \"SEC032\",\n \"name\": \"ebs_no_encryption\",\n \"severity\": \"medium\",\n \"pattern\": None, # Custom check\n \"message\": \"EBS volume without encryption\",\n \"fix\": \"Set encrypted = true on aws_ebs_volume or enable account-level default encryption\",\n },\n]\n\nACCESS_PATTERNS = [\n {\n \"id\": \"SEC040\",\n \"name\": \"rds_public\",\n \"severity\": \"high\",\n \"pattern\": r'publicly_accessible\\s*=\\s*true',\n \"message\": \"RDS instance is publicly accessible\",\n \"fix\": \"Set publicly_accessible = false and access via VPC/bastion\",\n },\n {\n \"id\": \"SEC041\",\n \"name\": \"s3_public_acl\",\n \"severity\": \"high\",\n \"pattern\": r'acl\\s*=\\s*\"public-read(?:-write)?\"',\n \"message\": \"S3 bucket with public ACL\",\n \"fix\": \"Remove public ACL and add aws_s3_bucket_public_access_block\",\n },\n]\n\n\ndef find_tf_files(directory):\n \"\"\"Find all .tf files in a directory (non-recursive).\"\"\"\n tf_files = {}\n for entry in sorted(os.listdir(directory)):\n if entry.endswith(\".tf\"):\n filepath = os.path.join(directory, entry)\n with open(filepath, encoding=\"utf-8\") as f:\n tf_files[entry] = f.read()\n return tf_files\n\n\ndef check_regex_rules(content, rules):\n \"\"\"Run regex-based security rules against content.\"\"\"\n findings = []\n for rule in rules:\n if rule[\"pattern\"] is None:\n continue\n for match in re.finditer(rule[\"pattern\"], content, re.MULTILINE | re.IGNORECASE):\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": rule[\"message\"],\n \"fix\": rule[\"fix\"],\n \"line\": match.group(0).strip()[:80],\n })\n return findings\n\n\ndef check_security_groups(content):\n \"\"\"Custom check for open security groups.\"\"\"\n findings = []\n\n # Parse ingress blocks within security group resources\n sg_blocks = re.finditer(\n r'resource\\s+\"aws_security_group\"[^{]*\\{(.*?)\\n\\}',\n content,\n re.DOTALL,\n )\n\n for sg_match in sg_blocks:\n sg_body = sg_match.group(1)\n ingress_blocks = re.finditer(\n r'ingress\\s*\\{(.*?)\\}', sg_body, re.DOTALL\n )\n\n for ingress in ingress_blocks:\n block = ingress.group(1)\n has_open_cidr = '0.0.0.0/0' in block or '::/0' in block\n\n if not has_open_cidr:\n continue\n\n from_port_match = re.search(r'from_port\\s*=\\s*(\\d+)', block)\n to_port_match = re.search(r'to_port\\s*=\\s*(\\d+)', block)\n\n if from_port_match and to_port_match:\n from_port = int(from_port_match.group(1))\n to_port = int(to_port_match.group(1))\n\n # SSH open\n if from_port \u003c= 22 \u003c= to_port:\n rule = next(r for r in NETWORK_PATTERNS if r[\"id\"] == \"SEC020\")\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": rule[\"message\"],\n \"fix\": rule[\"fix\"],\n \"line\": f\"ingress port 22, cidr 0.0.0.0/0\",\n })\n\n # RDP open\n if from_port \u003c= 3389 \u003c= to_port:\n rule = next(r for r in NETWORK_PATTERNS if r[\"id\"] == \"SEC021\")\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": rule[\"message\"],\n \"fix\": rule[\"fix\"],\n \"line\": f\"ingress port 3389, cidr 0.0.0.0/0\",\n })\n\n # All ports open\n if from_port == 0 and to_port >= 65535:\n rule = next(r for r in NETWORK_PATTERNS if r[\"id\"] == \"SEC022\")\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": rule[\"message\"],\n \"fix\": rule[\"fix\"],\n \"line\": f\"ingress ports 0-65535, cidr 0.0.0.0/0\",\n })\n\n return findings\n\n\ndef check_encryption(content):\n \"\"\"Custom check for missing encryption on storage resources.\"\"\"\n findings = []\n\n # S3 buckets without encryption\n s3_buckets = re.findall(\n r'resource\\s+\"aws_s3_bucket\"\\s+\"([^\"]+)\"', content\n )\n s3_encryption = re.findall(\n r'resource\\s+\"aws_s3_bucket_server_side_encryption_configuration\"', content\n )\n # Also check inline encryption (older format)\n inline_encryption = re.findall(\n r'server_side_encryption_configuration', content\n )\n if s3_buckets and not s3_encryption and not inline_encryption:\n rule = next(r for r in ENCRYPTION_PATTERNS if r[\"id\"] == \"SEC030\")\n for bucket in s3_buckets:\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": f\"{rule['message']} (bucket: {bucket})\",\n \"fix\": rule[\"fix\"],\n \"line\": f'aws_s3_bucket.{bucket}',\n })\n\n # RDS without encryption\n rds_blocks = re.finditer(\n r'resource\\s+\"aws_db_instance\"\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.DOTALL,\n )\n for rds_match in rds_blocks:\n name = rds_match.group(1)\n body = rds_match.group(2)\n if 'storage_encrypted' not in body or re.search(\n r'storage_encrypted\\s*=\\s*false', body\n ):\n rule = next(r for r in ENCRYPTION_PATTERNS if r[\"id\"] == \"SEC031\")\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": f\"{rule['message']} (instance: {name})\",\n \"fix\": rule[\"fix\"],\n \"line\": f'aws_db_instance.{name}',\n })\n\n # EBS volumes without encryption\n ebs_blocks = re.finditer(\n r'resource\\s+\"aws_ebs_volume\"\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.DOTALL,\n )\n for ebs_match in ebs_blocks:\n name = ebs_match.group(1)\n body = ebs_match.group(2)\n if 'encrypted' not in body or re.search(\n r'encrypted\\s*=\\s*false', body\n ):\n rule = next(r for r in ENCRYPTION_PATTERNS if r[\"id\"] == \"SEC032\")\n findings.append({\n \"id\": rule[\"id\"],\n \"severity\": rule[\"severity\"],\n \"message\": f\"{rule['message']} (volume: {name})\",\n \"fix\": rule[\"fix\"],\n \"line\": f'aws_ebs_volume.{name}',\n })\n\n return findings\n\n\ndef check_sensitive_variables(content):\n \"\"\"Check if variables that look like secrets are marked sensitive.\"\"\"\n findings = []\n var_blocks = re.finditer(\n r'variable\\s+\"([^\"]+)\"\\s*\\{(.*?)\\n\\}',\n content,\n re.DOTALL,\n )\n secret_names = [\"password\", \"secret\", \"token\", \"api_key\", \"private_key\", \"credentials\"]\n\n for var_match in var_blocks:\n name = var_match.group(1)\n body = var_match.group(2)\n name_lower = name.lower()\n\n if any(s in name_lower for s in secret_names):\n if not re.search(r'sensitive\\s*=\\s*true', body):\n findings.append({\n \"id\": \"SEC050\",\n \"severity\": \"medium\",\n \"message\": f\"Variable '{name}' appears to be a secret but is not marked sensitive = true\",\n \"fix\": \"Add sensitive = true to prevent the value from appearing in logs and plan output\",\n \"line\": f'variable \"{name}\"',\n })\n\n # Check for hardcoded default\n default_match = re.search(r'default\\s*=\\s*\"([^\"]+)\"', body)\n if default_match and len(default_match.group(1)) > 0:\n findings.append({\n \"id\": \"SEC051\",\n \"severity\": \"critical\",\n \"message\": f\"Variable '{name}' has a hardcoded default value for a secret\",\n \"fix\": \"Remove the default value — require it to be passed at runtime via tfvars or env\",\n \"line\": f'variable \"{name}\" default = \"{default_match.group(1)[:20]}...\"',\n })\n\n return findings\n\n\ndef scan_content(content, strict=False):\n \"\"\"Run all security checks on content.\"\"\"\n findings = []\n\n findings.extend(check_regex_rules(content, SECRET_PATTERNS))\n findings.extend(check_regex_rules(content, IAM_PATTERNS))\n findings.extend(check_regex_rules(content, ACCESS_PATTERNS))\n findings.extend(check_security_groups(content))\n findings.extend(check_encryption(content))\n findings.extend(check_sensitive_variables(content))\n\n if strict:\n for f in findings:\n if f[\"severity\"] == \"medium\":\n f[\"severity\"] = \"high\"\n elif f[\"severity\"] == \"low\":\n f[\"severity\"] = \"medium\"\n\n # Deduplicate by (id, line)\n seen = set()\n unique = []\n for f in findings:\n key = (f[\"id\"], f.get(\"line\", \"\"))\n if key not in seen:\n seen.add(key)\n unique.append(f)\n findings = unique\n\n # Sort by severity\n severity_order = {\"critical\": 0, \"high\": 1, \"medium\": 2, \"low\": 3}\n findings.sort(key=lambda f: severity_order.get(f[\"severity\"], 4))\n\n return findings\n\n\ndef generate_report(content, output_format=\"text\", strict=False):\n \"\"\"Generate security scan report.\"\"\"\n findings = scan_content(content, strict)\n\n # Score\n deductions = {\"critical\": 25, \"high\": 15, \"medium\": 5, \"low\": 2}\n score = max(0, 100 - sum(deductions.get(f[\"severity\"], 0) for f in findings))\n\n counts = {\n \"critical\": sum(1 for f in findings if f[\"severity\"] == \"critical\"),\n \"high\": sum(1 for f in findings if f[\"severity\"] == \"high\"),\n \"medium\": sum(1 for f in findings if f[\"severity\"] == \"medium\"),\n \"low\": sum(1 for f in findings if f[\"severity\"] == \"low\"),\n }\n\n result = {\n \"score\": score,\n \"findings\": findings,\n \"finding_counts\": counts,\n \"total_findings\": len(findings),\n }\n\n if output_format == \"json\":\n print(json.dumps(result, indent=2))\n return result\n\n # Text output\n print(f\"\\n{'=' * 60}\")\n print(f\" Terraform Security Scan Report\")\n print(f\"{'=' * 60}\")\n print(f\" Score: {score}/100\")\n print()\n print(f\" Findings: {counts['critical']} critical | {counts['high']} high | {counts['medium']} medium | {counts['low']} low\")\n print(f\"{'─' * 60}\")\n\n for f in findings:\n icon = {\"critical\": \"!!!\", \"high\": \"!!\", \"medium\": \"!\", \"low\": \"~\"}.get(f[\"severity\"], \"?\")\n print(f\"\\n [{f['id']}] {icon} {f['severity'].upper()}\")\n print(f\" {f['message']}\")\n if f.get(\"line\"):\n print(f\" Match: {f['line']}\")\n print(f\" Fix: {f['fix']}\")\n\n if not findings:\n print(\"\\n No security issues found. Configuration looks clean.\")\n\n print(f\"\\n{'=' * 60}\\n\")\n return result\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"terraform-patterns: Terraform security scanner\"\n )\n parser.add_argument(\n \"target\", nargs=\"?\",\n help=\"Path to Terraform directory or .tf file (omit for demo)\",\n )\n parser.add_argument(\n \"--output\", \"-o\",\n choices=[\"text\", \"json\"],\n default=\"text\",\n help=\"Output format (default: text)\",\n )\n parser.add_argument(\n \"--strict\",\n action=\"store_true\",\n help=\"Strict mode — elevate warnings to higher severity\",\n )\n args = parser.parse_args()\n\n if args.target:\n target = Path(args.target)\n if target.is_dir():\n tf_files = find_tf_files(str(target))\n if not tf_files:\n print(f\"Error: No .tf files found in {args.target}\", file=sys.stderr)\n sys.exit(1)\n content = \"\\n\".join(tf_files.values())\n elif target.is_file() and target.suffix == \".tf\":\n content = target.read_text(encoding=\"utf-8\")\n else:\n print(f\"Error: {args.target} is not a directory or .tf file\", file=sys.stderr)\n sys.exit(1)\n else:\n print(\"No target provided. Running demo scan...\\n\")\n content = DEMO_TF\n\n generate_report(content, args.output, args.strict)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":18523,"content_sha256":"0c1c25a78571c4209692a4e04a89aa3406526c37936507537fbc1ce25a477bbb"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Terraform Patterns","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Predictable infrastructure. Secure state. Modules that compose. No drift.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Opinionated Terraform workflow that turns sprawling HCL into well-structured, secure, production-grade infrastructure code. Covers module design, state management, provider patterns, security hardening, and CI/CD integration.","type":"text"}]},{"type":"paragraph","content":[{"text":"Not a Terraform tutorial — a set of concrete decisions about how to write infrastructure code that doesn't break at 3 AM.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Slash Commands","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it does","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/terraform:review","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Analyze Terraform code for anti-patterns, security issues, and structure problems","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/terraform:module","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Design or refactor a Terraform module with proper inputs, outputs, and composition","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/terraform:security","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Audit Terraform code for security vulnerabilities, secrets exposure, and IAM misconfigurations","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"When This Skill Activates","type":"text"}]},{"type":"paragraph","content":[{"text":"Recognize these patterns from the user:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Review this Terraform code\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Design a Terraform module for...\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"My Terraform state is...\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Set up remote state backend\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Multi-region Terraform deployment\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Terraform security review\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Module structure best practices\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Terraform CI/CD pipeline\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Any request involving: ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files, HCL, Terraform modules, state management, provider configuration, infrastructure-as-code","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the user has ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files or wants to provision infrastructure with Terraform → this skill applies.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"/terraform:review","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Terraform Code Review","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analyze current state","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read all ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files in the target directory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identify module structure (flat vs nested)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Count resources, data sources, variables, outputs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check naming conventions","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apply review checklist","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"MODULE STRUCTURE\n├── Variables have descriptions and type constraints\n├── Outputs expose only what consumers need\n├── Resources use consistent naming: {provider}_{type}_{purpose}\n├── Locals used for computed values and DRY expressions\n└── No hardcoded values — everything parameterized or in locals\n\nSTATE & BACKEND\n├── Remote backend configured (S3, GCS, Azure Blob, Terraform Cloud)\n├── State locking enabled (DynamoDB for S3, native for others)\n├── State encryption at rest enabled\n├── No secrets stored in state (or state access is restricted)\n└── Workspaces or directory isolation for environments\n\nPROVIDERS\n├── Version constraints use pessimistic operator: ~> 5.0\n├── Required providers block in terraform {} block\n├── Provider aliases for multi-region or multi-account\n└── No provider configuration in child modules\n\nSECURITY\n├── No hardcoded secrets, keys, or passwords\n├── IAM follows least-privilege principle\n├── Encryption enabled for storage, databases, secrets\n├── Security groups are not overly permissive (no 0.0.0.0/0 ingress on sensitive ports)\n└── Sensitive variables marked with sensitive = true","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate report","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/tf_module_analyzer.py ./terraform","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run security scan","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/tf_security_scanner.py ./terraform","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"/terraform:module","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Module Design","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identify module scope","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Single responsibility: one module = one logical grouping","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Determine inputs (variables), outputs, and resource boundaries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Decide: flat module (single directory) vs nested (calling child modules)","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apply module design checklist","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"STRUCTURE\n├── main.tf — Primary resources\n├── variables.tf — All input variables with descriptions and types\n├── outputs.tf — All outputs with descriptions\n├── versions.tf — terraform {} block with required_providers\n├── locals.tf — Computed values and naming conventions\n├── data.tf — Data sources (if any)\n└── README.md — Usage examples and variable documentation\n\nVARIABLES\n├── Every variable has: description, type, validation (where applicable)\n├── Sensitive values marked: sensitive = true\n├── Defaults provided for optional settings\n├── Use object types for related settings: variable \"config\" { type = object({...}) }\n└── Validate with: validation { condition = ... }\n\nOUTPUTS\n├── Output IDs, ARNs, endpoints — things consumers need\n├── Include description on every output\n├── Mark sensitive outputs: sensitive = true\n└── Don't output entire resources — only specific attributes\n\nCOMPOSITION\n├── Root module calls child modules\n├── Child modules never call other child modules\n├── Pass values explicitly — no hidden data source lookups in child modules\n├── Provider configuration only in root module\n└── Use module \"name\" { source = \"./modules/name\" }","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate module scaffold","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output file structure with boilerplate","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Include variable validation blocks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add lifecycle rules where appropriate","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"/terraform:security","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Security Audit","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Code-level audit","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Severity","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hardcoded secrets in ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Critical","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use variables with sensitive = true or vault","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IAM policy with ","type":"text"},{"text":"*","type":"text","marks":[{"type":"code_inline"}]},{"text":" actions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Critical","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scope to specific actions and resources","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security group with 0.0.0.0/0 on port 22/3389","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Critical","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Restrict to known CIDR blocks or use SSM/bastion","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"S3 bucket without encryption","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"server_side_encryption_configuration","type":"text","marks":[{"type":"code_inline"}]},{"text":" block","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"S3 bucket with public access","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"aws_s3_bucket_public_access_block","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RDS without encryption","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"storage_encrypted = true","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RDS publicly accessible","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"publicly_accessible = false","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CloudTrail not enabled","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Medium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"aws_cloudtrail","type":"text","marks":[{"type":"code_inline"}]},{"text":" resource","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text"},{"text":"prevent_destroy","type":"text","marks":[{"type":"code_inline"}]},{"text":" on stateful resources","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Medium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"lifecycle { prevent_destroy = true }","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Variables without ","type":"text"},{"text":"sensitive = true","type":"text","marks":[{"type":"code_inline"}]},{"text":" for secrets","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Medium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"sensitive = true","type":"text","marks":[{"type":"code_inline"}]},{"text":" to secret variables","type":"text"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"State security audit","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Severity","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Local state file","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Critical","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrate to remote backend with encryption","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remote state without encryption","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable encryption on backend (SSE-S3, KMS)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No state locking","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"High","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable DynamoDB for S3, native for TF Cloud","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"State accessible to all team members","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Medium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Restrict via IAM policies or TF Cloud teams","type":"text"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate security report","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/tf_security_scanner.py ./terraform\npython3 scripts/tf_security_scanner.py ./terraform --output json","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tooling","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"scripts/tf_module_analyzer.py","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"CLI utility for analyzing Terraform directory structure and module quality.","type":"text"}]},{"type":"paragraph","content":[{"text":"Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resource and data source counting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Variable and output analysis (missing descriptions, types, validation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Naming convention checks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Module composition detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"File structure validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JSON and text output","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Usage:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Analyze a Terraform directory\npython3 scripts/tf_module_analyzer.py ./terraform\n\n# JSON output\npython3 scripts/tf_module_analyzer.py ./terraform --output json\n\n# Analyze a specific module\npython3 scripts/tf_module_analyzer.py ./modules/vpc","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"scripts/tf_security_scanner.py","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"CLI utility for scanning ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files for common security issues.","type":"text"}]},{"type":"paragraph","content":[{"text":"Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoded secret detection (AWS keys, passwords, tokens)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Overly permissive IAM policy detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Open security group detection (0.0.0.0/0 on sensitive ports)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing encryption checks (S3, RDS, EBS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Public access detection (S3, RDS, EC2)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sensitive variable audit","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JSON and text output","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Usage:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Scan a Terraform directory\npython3 scripts/tf_security_scanner.py ./terraform\n\n# JSON output\npython3 scripts/tf_security_scanner.py ./terraform --output json\n\n# Strict mode (elevate warnings)\npython3 scripts/tf_security_scanner.py ./terraform --strict","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Module Design Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pattern 1: Flat Module (Small/Medium Projects)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"infrastructure/\n├── main.tf # All resources\n├── variables.tf # All inputs\n├── outputs.tf # All outputs\n├── versions.tf # Provider requirements\n├── terraform.tfvars # Environment values (not committed)\n└── backend.tf # Remote state configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"Best for: Single application, \u003c 20 resources, one team owns everything.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pattern 2: Nested Modules (Medium/Large Projects)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"infrastructure/\n├── environments/\n│ ├── dev/\n│ │ ├── main.tf # Calls modules with dev params\n│ │ ├── backend.tf # Dev state backend\n│ │ └── terraform.tfvars\n│ ├── staging/\n│ │ └── ...\n│ └── prod/\n│ └── ...\n├── modules/\n│ ├── networking/\n│ │ ├── main.tf\n│ │ ├── variables.tf\n│ │ └── outputs.tf\n│ ├── compute/\n│ │ └── ...\n│ └── database/\n│ └── ...\n└── versions.tf","type":"text"}]},{"type":"paragraph","content":[{"text":"Best for: Multiple environments, shared infrastructure patterns, team collaboration.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pattern 3: Mono-Repo with Terragrunt","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"infrastructure/\n├── terragrunt.hcl # Root config\n├── modules/ # Reusable modules\n│ ├── vpc/\n│ ├── eks/\n│ └── rds/\n├── dev/\n│ ├── terragrunt.hcl # Dev overrides\n│ ├── vpc/\n│ │ └── terragrunt.hcl # Module invocation\n│ └── eks/\n│ └── terragrunt.hcl\n└── prod/\n ├── terragrunt.hcl\n └── ...","type":"text"}]},{"type":"paragraph","content":[{"text":"Best for: Large-scale, many environments, DRY configuration, team-level isolation.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Provider Configuration Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Version Pinning","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"terraform {\n required_version = \">= 1.5.0\"\n\n required_providers {\n aws = {\n source = \"hashicorp/aws\"\n version = \"~> 5.0\" # Allow 5.x, block 6.0\n }\n random = {\n source = \"hashicorp/random\"\n version = \"~> 3.5\"\n }\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Multi-Region with Aliases","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"provider \"aws\" {\n region = \"us-east-1\"\n}\n\nprovider \"aws\" {\n alias = \"west\"\n region = \"us-west-2\"\n}\n\nresource \"aws_s3_bucket\" \"primary\" {\n bucket = \"my-app-primary\"\n}\n\nresource \"aws_s3_bucket\" \"replica\" {\n provider = aws.west\n bucket = \"my-app-replica\"\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Multi-Account with Assume Role","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"provider \"aws\" {\n alias = \"production\"\n region = \"us-east-1\"\n\n assume_role {\n role_arn = \"arn:aws:iam::PROD_ACCOUNT_ID:role/TerraformRole\"\n }\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"State Management Decision Tree","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Single developer, small project?\n├── Yes → Local state (but migrate to remote ASAP)\n└── No\n ├── Using Terraform Cloud/Enterprise?\n │ └── Yes → TF Cloud native backend (built-in locking, encryption, RBAC)\n └── No\n ├── AWS?\n │ └── S3 + DynamoDB (encryption, locking, versioning)\n ├── GCP?\n │ └── GCS bucket (native locking, encryption)\n ├── Azure?\n │ └── Azure Blob Storage (native locking, encryption)\n └── Other?\n └── Consul or PostgreSQL backend\n\nEnvironment isolation strategy:\n├── Separate state files per environment (recommended)\n│ ├── Option A: Separate directories (dev/, staging/, prod/)\n│ └── Option B: Terraform workspaces (simpler but less isolation)\n└── Single state file for all environments (never do this)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"CI/CD Integration Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"GitHub Actions Plan/Apply","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"# .github/workflows/terraform.yml\nname: Terraform\non:\n pull_request:\n paths: ['terraform/**']\n push:\n branches: [main]\n paths: ['terraform/**']\n\njobs:\n plan:\n runs-on: ubuntu-latest\n if: github.event_name == 'pull_request'\n steps:\n - uses: actions/checkout@v4\n - uses: hashicorp/setup-terraform@v3\n - run: terraform init\n - run: terraform validate\n - run: terraform plan -out=tfplan\n - run: terraform show -json tfplan > plan.json\n # Post plan as PR comment\n\n apply:\n runs-on: ubuntu-latest\n if: github.ref == 'refs/heads/main' && github.event_name == 'push'\n environment: production\n steps:\n - uses: actions/checkout@v4\n - uses: hashicorp/setup-terraform@v3\n - run: terraform init\n - run: terraform apply -auto-approve","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Drift Detection","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"# Run on schedule to detect drift\nname: Drift Detection\non:\n schedule:\n - cron: '0 6 * * 1-5' # Weekdays at 6 AM\n\njobs:\n detect:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: hashicorp/setup-terraform@v3\n - run: terraform init\n - run: |\n terraform plan -detailed-exitcode -out=drift.tfplan 2>&1 | tee drift.log\n EXIT_CODE=$?\n if [ $EXIT_CODE -eq 2 ]; then\n echo \"DRIFT DETECTED — review drift.log\"\n # Send alert (Slack, PagerDuty, etc.)\n fi","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Proactive Triggers","type":"text"}]},{"type":"paragraph","content":[{"text":"Flag these without being asked:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No remote backend configured","type":"text","marks":[{"type":"strong"}]},{"text":" → Migrate to S3/GCS/Azure Blob with locking and encryption.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provider without version constraint","type":"text","marks":[{"type":"strong"}]},{"text":" → Add ","type":"text"},{"text":"version = \"~> X.0\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" to prevent breaking upgrades.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoded secrets in .tf files","type":"text","marks":[{"type":"strong"}]},{"text":" → Use variables with ","type":"text"},{"text":"sensitive = true","type":"text","marks":[{"type":"code_inline"}]},{"text":", or integrate Vault/SSM.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IAM policy with ","type":"text","marks":[{"type":"strong"}]},{"text":"\"Action\": \"*\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" → Scope to specific actions. No wildcard actions in production.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security group open to 0.0.0.0/0 on SSH/RDP","type":"text","marks":[{"type":"strong"}]},{"text":" → Restrict to bastion CIDR or use SSM Session Manager.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No state locking","type":"text","marks":[{"type":"strong"}]},{"text":" → Enable DynamoDB table for S3 backend, or use TF Cloud.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resources without tags","type":"text","marks":[{"type":"strong"}]},{"text":" → Add default_tags in provider block. Tags are mandatory for cost tracking.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text","marks":[{"type":"strong"}]},{"text":"prevent_destroy","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" on databases/storage","type":"text","marks":[{"type":"strong"}]},{"text":" → Add lifecycle block to prevent accidental deletion.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Multi-Cloud Provider Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"When a single root module must provision across AWS, Azure, and GCP simultaneously.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Provider Aliasing Pattern","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"terraform {\n required_providers {\n aws = {\n source = \"hashicorp/aws\"\n version = \"~> 5.0\"\n }\n azurerm = {\n source = \"hashicorp/azurerm\"\n version = \"~> 3.0\"\n }\n google = {\n source = \"hashicorp/google\"\n version = \"~> 5.0\"\n }\n }\n}\n\nprovider \"aws\" {\n region = var.aws_region\n}\n\nprovider \"azurerm\" {\n features {}\n subscription_id = var.azure_subscription_id\n}\n\nprovider \"google\" {\n project = var.gcp_project_id\n region = var.gcp_region\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Shared Variables Across Providers","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"variable \"environment\" {\n description = \"Environment name used across all providers\"\n type = string\n validation {\n condition = contains([\"dev\", \"staging\", \"prod\"], var.environment)\n error_message = \"Must be dev, staging, or prod.\"\n }\n}\n\nlocals {\n common_tags = {\n environment = var.environment\n managed_by = \"terraform\"\n project = var.project_name\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When to Use Multi-Cloud","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Yes","type":"text","marks":[{"type":"strong"}]},{"text":": Regulatory requirements mandate data residency across providers, or the org has existing workloads on multiple clouds.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No","type":"text","marks":[{"type":"strong"}]},{"text":": \"Avoiding vendor lock-in\" alone is not sufficient justification. Multi-cloud doubles operational complexity. Prefer single-cloud unless there is a concrete business requirement.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"OpenTofu Compatibility","type":"text"}]},{"type":"paragraph","content":[{"text":"OpenTofu is an open-source fork of Terraform maintained by the Linux Foundation under the MPL 2.0 license.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Migration from Terraform to OpenTofu","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. Install OpenTofu\nbrew install opentofu # macOS\nsnap install --classic tofu # Linux\n\n# 2. Replace the binary — state files are compatible\ntofu init # Re-initializes with OpenTofu\ntofu plan # Identical plan output\ntofu apply # Same apply workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"License Considerations","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Terraform (1.6+)","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenTofu","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"License","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"BSL 1.1 (source-available)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MPL 2.0 (open-source)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Commercial use","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Restricted for competing products","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unrestricted","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Community governance","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HashiCorp","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Linux Foundation","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Feature Parity","type":"text"}]},{"type":"paragraph","content":[{"text":"OpenTofu tracks Terraform 1.6.x features. Key additions unique to OpenTofu:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Client-side state encryption (","type":"text"},{"text":"tofu init -encryption","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Early variable/locals evaluation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provider-defined functions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When to Choose OpenTofu","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You need a fully open-source license for your supply chain.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You want client-side state encryption without Terraform Cloud.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Otherwise, either tool works — the HCL syntax and provider ecosystem are identical.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Infracost Integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Infracost estimates cloud costs from Terraform code before resources are provisioned.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PR Workflow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Show cost breakdown for current code\ninfracost breakdown --path .\n\n# Compare cost difference between current branch and main\ninfracost diff --path . --compare-to infracost-base.json","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"GitHub Actions Cost Comment","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"# .github/workflows/infracost.yml\nname: Infracost\non: [pull_request]\n\njobs:\n cost:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: infracost/actions/setup@v3\n with:\n api-key: ${{ secrets.INFRACOST_API_KEY }}\n - run: infracost breakdown --path ./terraform --format json --out-file /tmp/infracost.json\n - run: infracost comment github --path /tmp/infracost.json --repo $GITHUB_REPOSITORY --pull-request ${{ github.event.pull_request.number }} --github-token ${{ secrets.GITHUB_TOKEN }} --behavior update","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Budget Thresholds and Cost Policy","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"# infracost.yml — policy file\nversion: 2.9.0\npolicies:\n - path: \"*\"\n max_monthly_cost: \"5000\" # Fail PR if estimated cost exceeds $5,000/month\n max_cost_increase: \"500\" # Fail PR if cost increase exceeds $500/month","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Import Existing Infrastructure","type":"text"}]},{"type":"paragraph","content":[{"text":"Bring manually-created resources under Terraform management.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"terraform import Workflow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. Write the resource block first (empty body is fine)\n# main.tf:\n# resource \"aws_s3_bucket\" \"legacy\" {}\n\n# 2. Import the resource into state\nterraform import aws_s3_bucket.legacy my-existing-bucket-name\n\n# 3. Run plan to see attribute diff\nterraform plan\n\n# 4. Fill in the resource block until plan shows no changes","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Bulk Import with Config Generation (Terraform 1.5+)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Generate HCL for imported resources\nterraform plan -generate-config-out=generated.tf\n\n# Review generated.tf, then move resources into proper files","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Common Pitfalls","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resource drift after import","type":"text","marks":[{"type":"strong"}]},{"text":": The imported resource may have attributes Terraform does not manage. Run ","type":"text"},{"text":"terraform plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" immediately and resolve every diff.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"State manipulation","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"terraform state mv","type":"text","marks":[{"type":"code_inline"}]},{"text":" to rename or reorganize. Use ","type":"text"},{"text":"terraform state rm","type":"text","marks":[{"type":"code_inline"}]},{"text":" to remove without destroying. Always back up state before manipulation: ","type":"text"},{"text":"terraform state pull > backup.tfstate","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sensitive defaults","type":"text","marks":[{"type":"strong"}]},{"text":": Imported resources may expose secrets in state. Restrict state access and enable encryption.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Terragrunt Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Terragrunt is a thin wrapper around Terraform that provides DRY configuration for multi-environment setups.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Root terragrunt.hcl (Shared Config)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"# terragrunt.hcl (root)\nremote_state {\n backend = \"s3\"\n generate = {\n path = \"backend.tf\"\n if_exists = \"overwrite_terragrunt\"\n }\n config = {\n bucket = \"my-org-terraform-state\"\n key = \"${path_relative_to_include()}/terraform.tfstate\"\n region = \"us-east-1\"\n encrypt = true\n dynamodb_table = \"terraform-locks\"\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Child terragrunt.hcl (Environment Override)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"# prod/vpc/terragrunt.hcl\ninclude \"root\" {\n path = find_in_parent_folders()\n}\n\nterraform {\n source = \"../../modules/vpc\"\n}\n\ninputs = {\n environment = \"prod\"\n cidr_block = \"10.0.0.0/16\"\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Dependencies Between Modules","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"hcl"},"content":[{"text":"# prod/eks/terragrunt.hcl\ndependency \"vpc\" {\n config_path = \"../vpc\"\n}\n\ninputs = {\n vpc_id = dependency.vpc.outputs.vpc_id\n subnet_ids = dependency.vpc.outputs.private_subnet_ids\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When Terragrunt Adds Value","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Yes","type":"text","marks":[{"type":"strong"}]},{"text":": 3+ environments with identical module structure, shared backend config, or cross-module dependencies.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No","type":"text","marks":[{"type":"strong"}]},{"text":": Single environment, small team, or simple directory-based isolation already works. Terragrunt adds a learning curve and another binary to manage.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Installation","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"One-liner (any tool)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"git clone https://github.com/alirezarezvani/claude-skills.git\ncp -r claude-skills/engineering/terraform-patterns ~/.claude/skills/","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Multi-tool install","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"./scripts/convert.sh --skill terraform-patterns --tool codex|gemini|cursor|windsurf|openclaw","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OpenClaw","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"clawhub install terraform-patterns","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"senior-devops","type":"text","marks":[{"type":"strong"}]},{"text":" — Broader DevOps scope (CI/CD, monitoring, containerization). Complementary — use terraform-patterns for IaC-specific work, senior-devops for pipeline and infrastructure operations.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"aws-solution-architect","type":"text","marks":[{"type":"strong"}]},{"text":" — AWS architecture design. Complementary — terraform-patterns implements the infrastructure, aws-solution-architect designs it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"senior-security","type":"text","marks":[{"type":"strong"}]},{"text":" — Application security. Complementary — terraform-patterns covers infrastructure security posture, senior-security covers application-level threats.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ci-cd-pipeline-builder","type":"text","marks":[{"type":"strong"}]},{"text":" — Pipeline construction. Complementary — terraform-patterns defines infrastructure, ci-cd-pipeline-builder automates deployment.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"terraform-patterns","author":"@skillopedia","source":{"stars":16818,"repo_name":"claude-skills","origin_url":"https://github.com/alirezarezvani/claude-skills/blob/HEAD/engineering/terraform-patterns/skills/terraform-patterns/SKILL.md","repo_owner":"alirezarezvani","body_sha256":"36547b0b5167f9ae53360e45ec57a1640b00fc92bac5dca7823e86ba63117002","cluster_key":"2638c32e79e4854bee16ae889502eaeb6cfbbde5b0aeceeff3d11fd37d31353b","clean_bundle":{"format":"clean-skill-bundle-v1","source":"alirezarezvani/claude-skills/engineering/terraform-patterns/skills/terraform-patterns/SKILL.md","attachments":[{"id":"06cf04aa-793b-580f-8ce5-4d6fc67c5e9c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/06cf04aa-793b-580f-8ce5-4d6fc67c5e9c/attachment.md","path":"references/module-patterns.md","size":9900,"sha256":"d75ccd64948b6fa9d74deb6b23eef3ca3a83939eb39502cca30ef0ac476af67f","contentType":"text/markdown; charset=utf-8"},{"id":"8d1f51ce-6be7-5b71-846f-09d6429fc4e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8d1f51ce-6be7-5b71-846f-09d6429fc4e2/attachment.md","path":"references/state-management.md","size":12177,"sha256":"2facd9d41b7852dcb23767db525b8751d2722b51a67adac4878bac33864e5b49","contentType":"text/markdown; charset=utf-8"},{"id":"3802615d-75a9-5c5e-b95d-66757c28d395","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3802615d-75a9-5c5e-b95d-66757c28d395/attachment.py","path":"scripts/tf_module_analyzer.py","size":13995,"sha256":"b191076b4b1566bbcb0601b230a467a9d56c32aa707b9055e674224a44b94847","contentType":"text/x-python; charset=utf-8"},{"id":"5c18fc30-f112-555e-a414-b0451ce1efa8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5c18fc30-f112-555e-a414-b0451ce1efa8/attachment.py","path":"scripts/tf_security_scanner.py","size":18523,"sha256":"0c1c25a78571c4209692a4e04a89aa3406526c37936507537fbc1ce25a477bbb","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"9368a9ac4a794ec1ad6fe70758135b262dbefd2c4293bdfd5c787fb9d1fdeea7","attachment_count":4,"text_attachments":4,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":2,"skill_md_path":"engineering/terraform-patterns/skills/terraform-patterns/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":1},"license":"MIT","version":"v1","category":"security","metadata":{"author":"Alireza Rezvani","updated":"2026-03-15T00:00:00.000Z","version":"1.0.0","category":"engineering"},"import_tag":"clean-skills-v1","description":"Terraform infrastructure-as-code agent skill and plugin for Claude Code, Codex, Gemini CLI, Cursor, OpenClaw. Covers module design patterns, state management strategies, provider configuration, security hardening, policy-as-code with Sentinel/OPA, and CI/CD plan/apply workflows. Use when: user wants to design Terraform modules, manage state backends, review Terraform security, implement multi-region deployments, or follow IaC best practices."}},"renderedAt":1782980007401}

Terraform Patterns Predictable infrastructure. Secure state. Modules that compose. No drift. Opinionated Terraform workflow that turns sprawling HCL into well-structured, secure, production-grade infrastructure code. Covers module design, state management, provider patterns, security hardening, and CI/CD integration. Not a Terraform tutorial — a set of concrete decisions about how to write infrastructure code that doesn't break at 3 AM. --- Slash Commands | Command | What it does | |---------|-------------| | | Analyze Terraform code for anti-patterns, security issues, and structure problems…