mirror of
https://github.com/antonbabenko/terraform-skill.git
synced 2026-09-18 20:07:05 +08:00
feat: umbrella update (plugin autodiscovery, full skill rewrite, LLM hallucination guards, fact-checks) (#14)
Co-authored-by: Roman Voitenko <Roman.Voitenko@ginatricot.com> Co-authored-by: Ali Ogun <ayogun@users.noreply.github.com> Co-authored-by: Jurijs I <jurijs.ivolga@gmail.com> Co-authored-by: JulesClau <JulesClaussen@users.noreply.github.com>
This commit is contained in:
@@ -5,22 +5,25 @@
|
||||
},
|
||||
"version": "1.6.0",
|
||||
"metadata": {
|
||||
"description": "Comprehensive Terraform and OpenTofu best practices skill covering testing, modules, CI/CD, and production patterns.",
|
||||
"description": "Use when writing, reviewing, or debugging Terraform/OpenTofu modules, tests, CI, scans, or state ops — diagnoses failure mode (identity churn, secrets, blast radius, CI drift, state corruption) with version-aware guards.",
|
||||
"repository": "https://github.com/antonbabenko/terraform-skill",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "terraform-skill",
|
||||
"description": "Use when working with Terraform or OpenTofu - creating modules, writing tests (native test framework, Terratest), setting up CI/CD pipelines, reviewing configurations, choosing between testing approaches, debugging state issues, implementing security scanning (trivy, checkov), or making infrastructure-as-code architecture decisions",
|
||||
"description": "Use when writing, reviewing, or debugging Terraform/OpenTofu modules, tests, CI, scans, or state ops — diagnoses failure mode (identity churn, secrets, blast radius, CI drift, state corruption) with version-aware guards.",
|
||||
"source": "./",
|
||||
"category": "development",
|
||||
"keywords": [
|
||||
"terraform",
|
||||
"opentofu",
|
||||
"iac",
|
||||
"infrastructure-as-code",
|
||||
"state-management",
|
||||
"testing",
|
||||
"ci-cd",
|
||||
"security-scanning",
|
||||
"modules"
|
||||
],
|
||||
"version": "1.6.0"
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
def update_skill_version(version):
|
||||
"""Update version in SKILL.md YAML frontmatter."""
|
||||
skill_path = 'SKILL.md'
|
||||
skill_path = 'skills/terraform-skill/SKILL.md'
|
||||
|
||||
if not os.path.exists(skill_path):
|
||||
raise FileNotFoundError(f"{skill_path} not found")
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
# Commit the sync
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .claude-plugin/marketplace.json SKILL.md
|
||||
git add .claude-plugin/marketplace.json skills/terraform-skill/SKILL.md
|
||||
git commit --amend --no-edit
|
||||
git push --force-with-lease
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ name: Validate Skill
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'SKILL.md'
|
||||
- 'references/**/*.md'
|
||||
- 'skills/**'
|
||||
- '.claude-plugin/**'
|
||||
push:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- 'SKILL.md'
|
||||
- 'references/**/*.md'
|
||||
- 'skills/**'
|
||||
- '.claude-plugin/**'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -40,7 +38,7 @@ jobs:
|
||||
|
||||
print("🔍 Validating SKILL.md frontmatter...")
|
||||
|
||||
with open('SKILL.md', 'r') as f:
|
||||
with open('skills/terraform-skill/SKILL.md', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
if not content.startswith('---'):
|
||||
@@ -93,8 +91,8 @@ jobs:
|
||||
|
||||
- name: Check File Size
|
||||
run: |
|
||||
LINES=$(wc -l < SKILL.md)
|
||||
WORDS=$(wc -w < SKILL.md)
|
||||
LINES=$(wc -l < skills/terraform-skill/SKILL.md)
|
||||
WORDS=$(wc -w < skills/terraform-skill/SKILL.md)
|
||||
echo "📊 SKILL.md: $LINES lines, $WORDS words"
|
||||
if [ $LINES -gt 500 ]; then
|
||||
echo "⚠️ WARNING: $LINES lines (guideline: <500)"
|
||||
@@ -153,6 +151,7 @@ jobs:
|
||||
- name: Check for Broken Links
|
||||
run: |
|
||||
echo "🔍 Checking internal links..."
|
||||
cd skills/terraform-skill
|
||||
if grep -oP '\[.*?\]\(references/.*?\.md.*?\)' SKILL.md references/*.md 2>/dev/null | \
|
||||
sed 's/.*(//' | sed 's/).*//' | sed 's/#.*//' | \
|
||||
while read -r link; do
|
||||
@@ -169,8 +168,7 @@ jobs:
|
||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: |
|
||||
SKILL.md
|
||||
references/**/*.md
|
||||
skills/**/*.md
|
||||
README.md
|
||||
CONTRIBUTING.md
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
.claude/settings.local.json
|
||||
docs/
|
||||
tmp-*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# CLAUDE.md - Contributor Guide
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
> **For End Users:** See [README.md](README.md) for installation and usage.
|
||||
>
|
||||
@@ -6,340 +8,164 @@
|
||||
|
||||
## What This Is
|
||||
|
||||
This repository contains a **Claude Code skill** - executable documentation that Claude loads to provide Terraform/OpenTofu expertise. Think of it as:
|
||||
|
||||
- **Prompt engineering as infrastructure**: Version-controlled AI instructions
|
||||
- **Domain knowledge artifact**: Encoding terraform-best-practices.com into Claude's context
|
||||
- **Meta-project**: Building instructions for an AI assistant
|
||||
A **Claude Code skill** - executable documentation that Claude loads to provide Terraform/OpenTofu expertise. It encodes terraform-best-practices.com patterns into Claude's context as version-controlled AI instructions.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
terraform-skill/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # Marketplace and plugin metadata
|
||||
├── SKILL.md # Core skill file (~524 lines)
|
||||
├── references/ # Reference files (progressive disclosure)
|
||||
│ ├── ci-cd-workflows.md # CI/CD templates (~473 lines)
|
||||
│ ├── code-patterns.md # Code patterns & modern features (~859 lines)
|
||||
│ ├── module-patterns.md # Module best practices (~1,126 lines)
|
||||
│ ├── quick-reference.md # Command cheat sheets (~600 lines)
|
||||
│ ├── security-compliance.md # Security guidance (~470 lines)
|
||||
│ └── testing-frameworks.md # Testing guides (~563 lines)
|
||||
├── README.md # For GitHub/marketplace users
|
||||
├── CLAUDE.md # For contributors (YOU ARE HERE)
|
||||
└── LICENSE # Apache 2.0
|
||||
```
|
||||
|
||||
### File Roles
|
||||
|
||||
| File | Audience | Purpose |
|
||||
|------|----------|---------|
|
||||
| `.claude-plugin/marketplace.json` | Claude Code | Marketplace and plugin metadata |
|
||||
| `SKILL.md` | Claude Code | Core skill (~524 lines, ~4.4K tokens) |
|
||||
| `references/*.md` | Claude Code | Reference files loaded on demand (6 files, ~26K tokens) |
|
||||
| `README.md` | End users | Installation, usage examples, what it covers |
|
||||
| `CLAUDE.md` | Contributors | Development guidelines, architecture decisions |
|
||||
| `LICENSE` | Everyone | Apache 2.0 legal terms |
|
||||
|
||||
## How Claude Skills Work
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
```
|
||||
User: "Create a Terraform module with tests"
|
||||
↓
|
||||
Claude: Scans skill metadata (~100 tokens)
|
||||
↓
|
||||
Claude: "This matches terraform-skill activation triggers"
|
||||
↓
|
||||
Claude: Loads full SKILL.md (~4,400 tokens)
|
||||
↓
|
||||
Claude: Applies testing framework decision matrix
|
||||
↓
|
||||
Response: Code following best practices
|
||||
```
|
||||
|
||||
**Key Insight:** Skills only load when relevant, minimizing token usage.
|
||||
|
||||
### Token Budget
|
||||
|
||||
- **Metadata (YAML frontmatter):** ~100 tokens - always loaded
|
||||
- **Core SKILL.md:** ~4,400 tokens - loaded on activation
|
||||
- **Reference files:** Individual estimates (loaded on demand only):
|
||||
- ci-cd-workflows.md: ~2,300 tokens
|
||||
- code-patterns.md: ~5,100 tokens
|
||||
- module-patterns.md: ~7,000 tokens
|
||||
- quick-reference.md: ~3,800 tokens
|
||||
- security-compliance.md: ~2,500 tokens
|
||||
- testing-frameworks.md: ~3,400 tokens
|
||||
- **Target:** Aim for under 500 lines for main SKILL.md (current: 524 lines - comprehensive core guidance)
|
||||
|
||||
**Our Architecture:**
|
||||
- SKILL.md: 524 lines, ~4.4K tokens (comprehensive core guidance)
|
||||
- Reference files: 6 files totaling 4,091 lines, ~26K tokens
|
||||
- Progressive disclosure: ~56-70% token reduction for typical queries (vs loading all content)
|
||||
|
||||
## Content Philosophy
|
||||
|
||||
### What Belongs in SKILL.md
|
||||
|
||||
✅ **Include:**
|
||||
- Terraform-specific patterns and idioms
|
||||
- Decision frameworks (when to use X vs Y)
|
||||
- Version-specific features (Terraform 1.6+, 1.9+, etc.)
|
||||
- Testing strategy workflows
|
||||
- ✅ DO vs ❌ DON'T examples
|
||||
- Quick reference tables and decision matrices
|
||||
|
||||
✅ **Keep:**
|
||||
- Scannable format (tables, headers, visual hierarchy)
|
||||
- Imperative voice ("Use X", not "You should consider X")
|
||||
- Concrete examples with inline comments
|
||||
- Version requirements clearly marked
|
||||
|
||||
### What Doesn't Belong
|
||||
|
||||
❌ **Exclude:**
|
||||
- Generic programming advice
|
||||
- Terraform syntax basics (covered in official docs)
|
||||
- Provider-specific resource details (use MCP tools)
|
||||
- Obvious practices ("use version control")
|
||||
- Long prose explanations (use tables instead)
|
||||
|
||||
## Content Structure
|
||||
|
||||
SKILL.md is organized by workflow phase:
|
||||
|
||||
1. **When to Use This Skill** - Activation triggers for Claude
|
||||
2. **Core Principles** - Naming, structure, philosophy
|
||||
3. **Testing Strategy Framework** - Decision matrices
|
||||
4. **Module Development** - Best practices and patterns
|
||||
5. **Common Patterns** - ✅/❌ side-by-side examples
|
||||
6. **CI/CD Integration** - Workflow automation
|
||||
7. **Quick Reference** - Rapid consultation tables
|
||||
8. **License & Attribution** - Legal and source credits
|
||||
|
||||
Each section is self-contained for selective reading.
|
||||
|
||||
## Writing Style Guide
|
||||
|
||||
### Imperative Voice
|
||||
|
||||
✅ **Good:**
|
||||
```markdown
|
||||
Use underscores in variable names, not hyphens:
|
||||
|
||||
✅ DO: `variable "vpc_id" {}`
|
||||
❌ DON'T: `variable "vpc-id" {}`
|
||||
```
|
||||
|
||||
❌ **Bad:**
|
||||
```markdown
|
||||
You should consider using underscores instead of hyphens
|
||||
in your variable names, as this is generally preferred.
|
||||
```
|
||||
|
||||
### Scannable Format
|
||||
|
||||
Use:
|
||||
- **Tables** for comparisons and decision matrices
|
||||
- **Code blocks** with inline comments
|
||||
- **Headers** for clear section breaks
|
||||
- **Bullets** for lists, not paragraphs
|
||||
- **✅/❌** for visual clarity
|
||||
|
||||
### Version Requirements
|
||||
|
||||
Always mark version-specific features:
|
||||
|
||||
```markdown
|
||||
**Native Tests** (Terraform 1.6+, OpenTofu 1.7+)
|
||||
├── .claude-plugin/marketplace.json # Plugin metadata (version synced automatically)
|
||||
├── skills/
|
||||
│ └── terraform-skill/ # Skill autodiscovered by Claude Code plugin system
|
||||
│ ├── SKILL.md # Core skill file (~277 lines)
|
||||
│ └── references/ # Reference files loaded on demand
|
||||
│ ├── ci-cd-workflows.md
|
||||
│ ├── code-patterns.md
|
||||
│ ├── module-patterns.md
|
||||
│ ├── quick-reference.md
|
||||
│ ├── security-compliance.md
|
||||
│ ├── state-management.md
|
||||
│ └── testing-frameworks.md
|
||||
├── tests/ # Baseline scenarios and rationalization tracking
|
||||
│ ├── baseline-scenarios.md
|
||||
│ ├── compliance-verification.md
|
||||
│ └── rationalization-table.md
|
||||
└── .github/workflows/
|
||||
├── validate.yml # PR validation (frontmatter, size, links, lint)
|
||||
└── automated-release.yml # Auto-release on master push via conventional commits
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### This Is Not Traditional Software
|
||||
**This is documentation, not code.** No build, no compiled tests.
|
||||
|
||||
**No build/test/compile:**
|
||||
- It's documentation, not code
|
||||
- No automated test suite
|
||||
- No build artifacts
|
||||
### Validation
|
||||
|
||||
**Validation approach:**
|
||||
1. Update SKILL.md
|
||||
2. Load in Claude Code (reload skills)
|
||||
3. Test on real Terraform projects
|
||||
4. Observe if Claude applies patterns correctly
|
||||
5. Iterate based on results
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
**Before submitting a PR:**
|
||||
|
||||
1. **Load the updated skill:**
|
||||
```bash
|
||||
# If you have local clone in ~/.claude/references/
|
||||
# Claude Code auto-reloads on file changes
|
||||
```
|
||||
|
||||
2. **Test with real queries:**
|
||||
- "Create a Terraform module with tests"
|
||||
- "Review this configuration"
|
||||
- "What testing framework should I use?"
|
||||
|
||||
3. **Verify Claude references the skill:**
|
||||
- Check if new patterns appear in responses
|
||||
- Ensure no conflicts with existing guidance
|
||||
|
||||
4. **Check token count:**
|
||||
```bash
|
||||
wc -c SKILL.md # Currently ~17,700 chars ≈ 4,400 tokens
|
||||
```
|
||||
|
||||
### When to Update
|
||||
|
||||
**Update the skill when:**
|
||||
- ✅ New Terraform major/minor versions introduce features
|
||||
- ✅ Community consensus emerges on patterns
|
||||
- ✅ Real-world usage reveals gaps or ambiguities
|
||||
- ✅ Anti-patterns discovered that should be warned against
|
||||
|
||||
**Don't update for:**
|
||||
- ❌ Provider-specific resource changes (use MCP tools)
|
||||
- ❌ Minor version patches without feature changes
|
||||
- ❌ Personal preferences without community consensus
|
||||
|
||||
## Working with MCP Tools
|
||||
|
||||
When this skill is used alongside Terraform MCP server:
|
||||
|
||||
| Provides | Skill | MCP |
|
||||
|----------|-------|-----|
|
||||
| Best practices | ✅ | ❌ |
|
||||
| Code patterns | ✅ | ❌ |
|
||||
| Testing workflows | ✅ | ❌ |
|
||||
| Latest versions | ❌ | ✅ |
|
||||
| Registry docs | ❌ | ✅ |
|
||||
| Module search | ❌ | ✅ |
|
||||
|
||||
**Together they enable:**
|
||||
- Code generation following best practices
|
||||
- Up-to-date version constraints
|
||||
- Framework selection guidance
|
||||
- Proactive anti-pattern detection
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Content Quality Checklist
|
||||
|
||||
Before merging changes:
|
||||
|
||||
- [ ] Decision frameworks are clear
|
||||
- [ ] Examples are accurate and tested
|
||||
- [ ] No outdated information
|
||||
- [ ] Version-specific guidance marked
|
||||
- [ ] Common pitfalls documented
|
||||
- [ ] ✅/❌ examples for non-obvious patterns
|
||||
|
||||
### Technical Quality
|
||||
|
||||
- [ ] Code examples are syntactically correct
|
||||
- [ ] Commands follow current best practices
|
||||
- [ ] Links to official documentation work
|
||||
- [ ] Tools referenced are current (not deprecated)
|
||||
|
||||
### Usability
|
||||
|
||||
- [ ] Clear activation triggers
|
||||
- [ ] Quick reference sections scannable
|
||||
- [ ] Logical organization maintained
|
||||
- [ ] Consistent formatting (markdown standards)
|
||||
|
||||
### Legal
|
||||
|
||||
- [ ] License clearly stated (Apache 2.0)
|
||||
- [ ] Sources attributed
|
||||
- [ ] Copyright notice current
|
||||
- [ ] No copyrighted content without permission
|
||||
|
||||
## Contributing Process
|
||||
|
||||
### 1. Fork & Branch
|
||||
CI runs automatically on PRs touching `SKILL.md`, `references/**/*.md`, or `.claude-plugin/**`. To check locally:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/terraform-skill
|
||||
cd terraform-skill
|
||||
git checkout -b feature/your-improvement
|
||||
# Check SKILL.md line count (target: <300 lines per LLM Consumption Rules)
|
||||
wc -l skills/terraform-skill/SKILL.md
|
||||
|
||||
# Validate YAML frontmatter (requires pyyaml)
|
||||
python3 -c "
|
||||
import yaml, sys
|
||||
content = open('skills/terraform-skill/SKILL.md').read()
|
||||
parts = content.split('---', 2)
|
||||
fm = yaml.safe_load(parts[1])
|
||||
required = {'name', 'description'}
|
||||
missing = required - set(fm.keys())
|
||||
print('Missing:', missing) if missing else print('Frontmatter OK')
|
||||
"
|
||||
|
||||
# Check for broken internal links (run from the skill directory)
|
||||
cd skills/terraform-skill
|
||||
grep -oP '\[.*?\]\(references/.*?\.md.*?\)' SKILL.md references/*.md | \
|
||||
sed 's/.*(//' | sed 's/).*//' | sed 's/#.*//' | \
|
||||
while read -r link; do [ ! -f "$link" ] && echo "Broken: $link"; done
|
||||
```
|
||||
|
||||
### 2. Make Changes
|
||||
### Testing Changes
|
||||
|
||||
Edit `SKILL.md` following the guidelines above.
|
||||
No automated suite. Manual flow:
|
||||
1. Edit `SKILL.md` or a `references/*.md` file
|
||||
2. Reload the skill in Claude Code
|
||||
3. Run real Terraform queries (e.g., "Create a Terraform module with tests")
|
||||
4. Confirm Claude applies the new patterns
|
||||
5. Re-check `tests/baseline-scenarios.md` for regressions
|
||||
|
||||
### 3. Test Locally
|
||||
## Commit Conventions & Releases
|
||||
|
||||
```bash
|
||||
# Copy to Claude skills directory for testing
|
||||
cp -r . ~/.claude/references/terraform-skill/
|
||||
Releases are **fully automated** from conventional commits on `master`:
|
||||
|
||||
# Test in Claude Code with real queries
|
||||
```
|
||||
| Commit prefix | Version bump |
|
||||
|---------------|-------------|
|
||||
| `feat!:` or `BREAKING CHANGE:` | Major |
|
||||
| `feat:` | Minor |
|
||||
| `fix:` | Patch |
|
||||
| Other | Patch (default) |
|
||||
|
||||
### 4. Submit PR
|
||||
The release workflow automatically:
|
||||
- Bumps the version in `CHANGELOG.md`
|
||||
- Syncs versions across **three places** (must stay in sync):
|
||||
1. `.claude-plugin/marketplace.json` → `version` (root)
|
||||
2. `.claude-plugin/marketplace.json` → `plugins[0].version`
|
||||
3. `skills/terraform-skill/SKILL.md` YAML frontmatter → `metadata.version`
|
||||
|
||||
```bash
|
||||
git add SKILL.md
|
||||
git commit -m "Add guidance for Terraform 1.10 feature X"
|
||||
git push origin feature/your-improvement
|
||||
```
|
||||
**Never manually edit version numbers** - the CI handles this.
|
||||
|
||||
Create PR with:
|
||||
- Clear description of what changed
|
||||
- Why the change improves the skill
|
||||
- How you tested it
|
||||
## SKILL.md Architecture
|
||||
|
||||
### 5. Review Process
|
||||
### Plugin Structure
|
||||
|
||||
Maintainers will check:
|
||||
- Content accuracy
|
||||
- Token efficiency
|
||||
- Consistency with existing patterns
|
||||
- Real-world testing results
|
||||
The skill lives at `skills/terraform-skill/SKILL.md` — Claude Code autodiscovers any `skills/<name>/SKILL.md` (see [plugins reference](https://code.claude.com/docs/en/plugins-reference)). Reference files sit next to it under `skills/terraform-skill/references/` so relative links keep working.
|
||||
|
||||
## Skill Evolution Strategy
|
||||
|
||||
### Maintaining Balance
|
||||
|
||||
As Terraform evolves, balance:
|
||||
- **Completeness** vs **Token efficiency**
|
||||
- **Detail** vs **Scannability**
|
||||
- **Examples** vs **Reference**
|
||||
|
||||
**Current Status:** SKILL.md is at 524 lines, slightly above the suggested 500-line target. This is justified by:
|
||||
- Comprehensive decision matrices (testing, count vs for_each)
|
||||
- Essential quick reference tables
|
||||
- Version-specific guidance (multiple Terraform versions)
|
||||
- Progressive disclosure architecture minimizes token cost
|
||||
|
||||
The extra 24 lines provide significant value while maintaining scannability. Future updates should prioritize reference file expansion over core skill growth.
|
||||
|
||||
Current sweet spot: ~4.4K tokens for core SKILL.md, with 6 reference files (~26K tokens) providing deep-dive content on demand. Total coverage: ~30.4K tokens structured for progressive disclosure.
|
||||
|
||||
### Long-term Vision
|
||||
|
||||
This skill should:
|
||||
- Stay current with Terraform/OpenTofu releases
|
||||
- Remain the definitive Claude resource for Terraform
|
||||
- Evolve with community consensus
|
||||
- Maintain production-grade quality standards
|
||||
|
||||
## Questions?
|
||||
|
||||
- **Issues:** [GitHub Issues](https://github.com/antonbabenko/terraform-skill/issues)
|
||||
- **Discussions:** Use GitHub Discussions for questions
|
||||
- **Author:** [@antonbabenko](https://github.com/antonbabenko)
|
||||
### YAML Frontmatter (required fields)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: terraform-skill # letters, numbers, hyphens only
|
||||
description: Use when... # < 1024 chars, starts with "Use when"
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: Anton Babenko
|
||||
version: X.Y.Z # Auto-synced by CI
|
||||
---
|
||||
```
|
||||
|
||||
**Remember:** You're not just editing docs - you're shaping how Claude understands and applies Terraform best practices. Quality matters.
|
||||
### Progressive Disclosure Pattern
|
||||
|
||||
SKILL.md is the entry point. Reference files load on demand. Cross-links use relative paths: `[Testing Guide](references/testing-frameworks.md)`.
|
||||
|
||||
When adding content, ask: **decision framework or key pattern → SKILL.md; detailed example or template → reference file.**
|
||||
|
||||
### Content Standards
|
||||
|
||||
- **Imperative voice:** "Use X" not "You should consider X"
|
||||
- **Scannable format:** tables > bullets > prose
|
||||
- **✅ DO / ❌ DON'T** side-by-side for non-obvious patterns
|
||||
- **Version-specific features** clearly marked (e.g., `Terraform 1.6+`)
|
||||
- **Token budget:** SKILL.md target <300 lines (see LLM Consumption Rules); currently ~277
|
||||
|
||||
### LLM Consumption Rules (enforce in every PR review)
|
||||
|
||||
These rules tune content for the **primary reader: an LLM retrieving facts to answer a user query**, not a human reading the guide end-to-end. They are **mandatory** for every addition to `SKILL.md` and `references/*.md`. Reviewers must reject PRs that violate them.
|
||||
|
||||
**1. Shape — decision table before playbook.** The LLM retrieval path is: classify intent → pick branch → execute. When a topic has multiple viable approaches, open the section with a decision table (`Goal | Use | Tradeoff`) before any phase steps or default procedure. Never bury branching in prose or push alternatives to the end.
|
||||
|
||||
**2. Cut human scaffolding.** Before/after config diffs, "Why this matters" paragraphs, and pedagogical asides are human-only signal. If the phase steps already name the required action, a before/after diff is redundant and must be dropped. Teaching tone ≠ retrieval value.
|
||||
|
||||
**3. Compress prose → ❌/✅ Rules.** Any sentence starting with "You should...", "Note that...", "Keep in mind...", "It's important to..." — rewrite as terse imperative ❌/✅ bullet. One fact per bullet. Direct verbs only: `Keep`, `Remove`, `Run`, `Confirm`, `Use`, `Avoid`, `Scope`.
|
||||
|
||||
**4. Every artifact earns its tokens.** Every code block, table, and example must add a fact not present in the prose. If it only restates, cut it. No "for completeness" content.
|
||||
|
||||
**5. Anchor stability.** SKILL.md routes to specific `#anchor` headings in reference files. Rewrites may restructure internal subsections, but must preserve the top-level `### Heading` that the SKILL.md diagnose table points to.
|
||||
|
||||
**6. Retrieval-first ordering.** Within a section, order content by what the LLM needs first: (a) decision table, (b) default procedure, (c) alternatives, (d) rules/gotchas as ❌/✅. Rationale lives in ≤1 opening sentence, never a closing "Why this matters" block.
|
||||
|
||||
**Token target per reference subsection:** under 400 tokens (~1,600 chars). If larger, split or compress — do not ship a 600-token walkthrough when 350 tokens carries the same decision value.
|
||||
|
||||
**Pre-merge checklist for any content PR:**
|
||||
|
||||
- [ ] Decision table precedes playbook (if multiple approaches exist)
|
||||
- [ ] No before/after diff that merely restates the phase steps
|
||||
- [ ] No paragraph starting with "Why this matters" / "Note" / "Keep in mind" — all converted to ❌/✅
|
||||
- [ ] Every code block / table adds a fact not in surrounding prose
|
||||
- [ ] Subsection under 400 tokens
|
||||
- [ ] Anchors referenced from SKILL.md remain stable
|
||||
- [ ] For substantive new sections, consult an external LLM expert (e.g. GPT via `mcp__codex__codex`) for format/compression review before merge
|
||||
|
||||
## PR Requirements
|
||||
|
||||
PRs must include before/after evidence for affected scenarios in `tests/baseline-scenarios.md`. See `.github/PULL_REQUEST_TEMPLATE.md` for the full checklist.
|
||||
|
||||
## What Belongs Where
|
||||
|
||||
| Content type | Location |
|
||||
|-------------|----------|
|
||||
| Decision frameworks, core patterns | `SKILL.md` |
|
||||
| Detailed guides, templates, examples | `references/*.md` |
|
||||
| Baseline test scenarios | `tests/baseline-scenarios.md` |
|
||||
| Agent rationalization tracking | `tests/rationalization-table.md` |
|
||||
| Installation/usage docs | `README.md` |
|
||||
| Contributor process details | `CONTRIBUTING.md` |
|
||||
|
||||
+135
-111
@@ -1,7 +1,6 @@
|
||||
# Contributing to Terraform Skill
|
||||
|
||||
Thank you for your interest in improving terraform-skill! This document
|
||||
provides guidelines for contributors.
|
||||
Thanks for helping improve terraform-skill. Guidelines for contributors below.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -15,14 +14,14 @@ provides guidelines for contributors.
|
||||
|
||||
**Good contributions:**
|
||||
|
||||
- ✅ New Terraform/OpenTofu best practices based on community consensus
|
||||
- ✅ New Terraform/OpenTofu best practices with community consensus
|
||||
- ✅ Version-specific features for new Terraform/OpenTofu releases
|
||||
- ✅ Corrections to outdated or incorrect information
|
||||
- ✅ Improved examples or patterns
|
||||
- ✅ Better organization or clarity
|
||||
- ✅ Better examples or patterns
|
||||
- ✅ Sharper organization or clarity
|
||||
- ✅ Testing framework improvements
|
||||
|
||||
**Not suitable for contributions:**
|
||||
**Not suitable:**
|
||||
|
||||
- ❌ Personal preferences without community consensus
|
||||
- ❌ Provider-specific resource details (use Terraform MCP tools instead)
|
||||
@@ -33,79 +32,108 @@ provides guidelines for contributors.
|
||||
|
||||
### Frontmatter Requirements
|
||||
|
||||
**CRITICAL:** SKILL.md frontmatter must contain ONLY two fields:
|
||||
SKILL.md frontmatter must include two required fields. Other fields are optional and allowed.
|
||||
|
||||
- `name` - Skill name (letters, numbers, hyphens only)
|
||||
- `description` - When to use this skill
|
||||
**Required:**
|
||||
|
||||
- `name` — Skill name (letters, numbers, hyphens only)
|
||||
- `description` — When to use this skill (must start with "Use when", ≤1024 chars)
|
||||
|
||||
**Optional (allowed):**
|
||||
|
||||
- `license` — e.g. `Apache-2.0`
|
||||
- `metadata.author` — attribution
|
||||
- `metadata.version` — **auto-synced by the release workflow; never hand-edit**
|
||||
- Future additions the validate workflow accepts
|
||||
|
||||
Current frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: terraform-skill
|
||||
description: Use when working with Terraform or OpenTofu - creating modules,
|
||||
writing tests...
|
||||
description: >-
|
||||
Use when writing, reviewing, or debugging Terraform/OpenTofu modules,
|
||||
tests, CI, scans, or state ops — diagnoses failure mode (identity
|
||||
churn, secrets, blast radius, CI drift, state corruption) with
|
||||
version-aware guards.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: Anton Babenko
|
||||
version: X.Y.Z
|
||||
---
|
||||
```
|
||||
|
||||
**Do NOT add:**
|
||||
- ❌ `author` field (put in README.md)
|
||||
- ❌ `version` field (managed by release workflow)
|
||||
- ❌ `license` field (put in README.md and LICENSE)
|
||||
- ❌ Any other custom fields
|
||||
|
||||
**Why:** Per official skill standards, only `name` and `description` are
|
||||
supported. Extra fields waste tokens.
|
||||
The validate workflow (`.github/workflows/validate.yml`) rejects the PR only if `name` or `description` is missing, if `name` contains invalid characters, or if `description` exceeds 1024 characters. Optional fields are logged but not blocked.
|
||||
|
||||
### Description Best Practices
|
||||
|
||||
**Format:** Start with "Use when..." and list specific triggers
|
||||
Start with "Use when..." and list specific triggers.
|
||||
|
||||
**Good example:**
|
||||
|
||||
```yaml
|
||||
description: >-
|
||||
Use when working with Terraform or OpenTofu - creating modules, writing
|
||||
tests (native test framework, Terratest), setting up CI/CD pipelines,
|
||||
reviewing configurations, choosing between testing approaches, debugging
|
||||
state issues, implementing security scanning (trivy, checkov), or making
|
||||
infrastructure-as-code architecture decisions
|
||||
Use when writing, reviewing, or debugging Terraform/OpenTofu modules,
|
||||
tests, CI, scans, or state ops — diagnoses failure mode (identity
|
||||
churn, secrets, blast radius, CI drift, state corruption) with
|
||||
version-aware guards.
|
||||
```
|
||||
|
||||
**Bad example:**
|
||||
|
||||
```yaml
|
||||
description: Comprehensive skill for Terraform development covering testing, modules, CI/CD, and production patterns
|
||||
```
|
||||
|
||||
**Why:** Description must focus on WHEN to use (triggers/symptoms), not WHAT it does (workflow summary). See plan file and writing-skills documentation for rationale.
|
||||
The description must focus on WHEN to use (triggers, symptoms), not WHAT the skill does. See writing-skills documentation for rationale.
|
||||
|
||||
### Token Efficiency
|
||||
|
||||
**SKILL.md Target:** <1,500 words
|
||||
**SKILL.md target:** <300 lines (currently 277).
|
||||
|
||||
**Reference subsection target:** <400 tokens (~1,600 chars). Split or compress anything larger.
|
||||
|
||||
**Techniques:**
|
||||
- Use progressive disclosure (move details to references/*.md)
|
||||
- Prefer tables over prose
|
||||
- Compress link sections (pipe-separated)
|
||||
|
||||
- Push detail into `references/*.md` (progressive disclosure)
|
||||
- Tables over prose
|
||||
- Pipe-separated link lists
|
||||
- Reference other files instead of repeating content
|
||||
|
||||
**Current stats:** ~1,400 words, ~280 lines
|
||||
### LLM Consumption Rules
|
||||
|
||||
Every SKILL.md or `references/*.md` addition must follow the rules in [CLAUDE.md §LLM Consumption Rules](CLAUDE.md#llm-consumption-rules-enforce-in-every-pr-review):
|
||||
|
||||
- Decision table before playbook
|
||||
- No before/after diffs that restate the phase steps
|
||||
- No "Why this matters" / "Note" / "Keep in mind" paragraphs — convert to ❌/✅
|
||||
- Retrieval-first ordering within each section
|
||||
- Preserve anchors that SKILL.md links to
|
||||
- Subsections under ~400 tokens
|
||||
|
||||
Reviewers reject PRs that violate these.
|
||||
|
||||
### File Organization
|
||||
|
||||
```
|
||||
terraform-skill/
|
||||
├── SKILL.md # Core skill (<500 lines guideline)
|
||||
├── references/ # Reference files (progressive disclosure)
|
||||
│ ├── testing-frameworks.md
|
||||
│ ├── module-patterns.md
|
||||
│ ├── ci-cd-workflows.md
|
||||
│ ├── security-compliance.md
|
||||
│ └── quick-reference.md
|
||||
├── tests/ # TDD testing framework
|
||||
├── skills/
|
||||
│ └── terraform-skill/ # Autodiscovered by Claude Code plugin system
|
||||
│ ├── SKILL.md # Core skill (<300 lines)
|
||||
│ └── references/ # Reference files (progressive disclosure)
|
||||
│ ├── ci-cd-workflows.md
|
||||
│ ├── code-patterns.md
|
||||
│ ├── module-patterns.md
|
||||
│ ├── quick-reference.md
|
||||
│ ├── security-compliance.md
|
||||
│ ├── state-management.md
|
||||
│ └── testing-frameworks.md
|
||||
├── tests/ # TDD testing framework
|
||||
│ ├── baseline-scenarios.md
|
||||
│ ├── compliance-verification.md
|
||||
│ └── rationalization-table.md
|
||||
└── .github/workflows/ # Automation
|
||||
├── release.yml
|
||||
└── .github/workflows/ # Automation
|
||||
├── automated-release.yml
|
||||
└── validate.yml
|
||||
```
|
||||
|
||||
@@ -113,21 +141,18 @@ terraform-skill/
|
||||
|
||||
### The Iron Law
|
||||
|
||||
**NO CHANGES WITHOUT TESTING FIRST**
|
||||
**NO CHANGES WITHOUT TESTING FIRST.**
|
||||
|
||||
Applies to:
|
||||
|
||||
This applies to:
|
||||
- ✅ New content additions
|
||||
- ✅ Edits to existing content
|
||||
- ✅ Reorganization or refactoring
|
||||
- ✅ "Simple" documentation updates
|
||||
|
||||
**No exceptions.**
|
||||
No exceptions. Without a baseline, a change cannot prove it improves agent behavior. Per writing-skills, this is TDD for documentation:
|
||||
|
||||
### Why This Matters
|
||||
|
||||
Without testing, we don't know if changes actually improve agent behavior. Per official skill standards (writing-skills), this is TDD for documentation:
|
||||
|
||||
- **RED:** Run scenarios WITHOUT your changes (baseline)
|
||||
- **RED:** Run scenarios without your changes (baseline)
|
||||
- **GREEN:** Add changes, verify behavior improves
|
||||
- **REFACTOR:** Close loopholes, re-test
|
||||
|
||||
@@ -137,13 +162,13 @@ Without testing, we don't know if changes actually improve agent behavior. Per o
|
||||
|
||||
Review `tests/baseline-scenarios.md`. Which scenarios does your change affect?
|
||||
|
||||
**Example:** Adding security scanning guidance → affects Scenario 3
|
||||
Example: adding security scanning guidance → affects Scenario 3.
|
||||
|
||||
#### 2. Run Baseline (WITHOUT Your Changes)
|
||||
#### 2. Run Baseline (Without Your Changes)
|
||||
|
||||
```bash
|
||||
# Disable skill temporarily
|
||||
mv ~/.claude/references/terraform-skill ~/.claude/references/terraform-skill.disabled
|
||||
/plugin disable terraform-skill@antonbabenko
|
||||
|
||||
# Run affected scenario
|
||||
# Document agent response in tests/baseline-results/
|
||||
@@ -151,13 +176,13 @@ mv ~/.claude/references/terraform-skill ~/.claude/references/terraform-skill.dis
|
||||
|
||||
#### 3. Apply Your Changes
|
||||
|
||||
Make your edits to SKILL.md or reference files.
|
||||
Edit SKILL.md or reference files.
|
||||
|
||||
#### 4. Run Compliance Test (WITH Your Changes)
|
||||
#### 4. Run Compliance Test (With Your Changes)
|
||||
|
||||
```bash
|
||||
# Re-enable skill
|
||||
mv ~/.claude/references/terraform-skill.disabled ~/.claude/references/terraform-skill
|
||||
/plugin enable terraform-skill@antonbabenko
|
||||
|
||||
# Run same scenario
|
||||
# Document improved behavior in tests/compliance-results/
|
||||
@@ -166,26 +191,28 @@ mv ~/.claude/references/terraform-skill.disabled ~/.claude/references/terraform-
|
||||
#### 5. Verify Improvement
|
||||
|
||||
Compare baseline vs compliance:
|
||||
- Does agent now follow your guidance?
|
||||
|
||||
- Does the agent now follow your guidance?
|
||||
- Are patterns applied proactively?
|
||||
- No new rationalizations introduced?
|
||||
- Any new rationalizations introduced?
|
||||
|
||||
#### 6. Document in PR
|
||||
|
||||
Include in PR description:
|
||||
- Which scenarios tested
|
||||
- Baseline behavior (what agent did without change)
|
||||
- Compliance behavior (what agent does with change)
|
||||
- Evidence that change works
|
||||
Include in the PR description:
|
||||
|
||||
- Which scenarios you tested
|
||||
- Baseline behavior (what the agent did without the change)
|
||||
- Compliance behavior (what the agent does with the change)
|
||||
- Evidence the change works
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
For each PR, include this checklist:
|
||||
Include this checklist on every PR:
|
||||
|
||||
- [ ] Identified affected scenarios from tests/baseline-scenarios.md
|
||||
- [ ] Ran baseline WITHOUT changes (documented)
|
||||
- [ ] Ran baseline without changes (documented)
|
||||
- [ ] Applied changes
|
||||
- [ ] Ran compliance WITH changes (documented)
|
||||
- [ ] Ran compliance with changes (documented)
|
||||
- [ ] Verified behavior improvement
|
||||
- [ ] No new rationalizations discovered (or documented in rationalization-table.md)
|
||||
- [ ] Re-tested if rationalizations found
|
||||
@@ -195,49 +222,56 @@ For each PR, include this checklist:
|
||||
### Writing Style
|
||||
|
||||
**Imperative voice:**
|
||||
✅ "Use underscores in variable names"
|
||||
❌ "You should consider using underscores"
|
||||
|
||||
- ✅ "Use underscores in variable names"
|
||||
- ❌ "You should consider using underscores"
|
||||
|
||||
**Scannable format:**
|
||||
|
||||
- Tables for comparisons
|
||||
- ✅ DO vs ❌ DON'T side-by-side
|
||||
- Code blocks with inline comments
|
||||
- Clear section headers
|
||||
|
||||
**Version-specific markers:**
|
||||
|
||||
```markdown
|
||||
**Native Tests** (Terraform 1.6+, OpenTofu 1.6+)
|
||||
```
|
||||
|
||||
### Code Examples
|
||||
|
||||
**One excellent example beats many mediocre ones**
|
||||
One excellent example beats many mediocre ones.
|
||||
|
||||
**Good:**
|
||||
|
||||
**Good example:**
|
||||
- Complete and runnable
|
||||
- Well-commented explaining WHY
|
||||
- From real scenario
|
||||
- Shows pattern clearly
|
||||
- Commented to explain WHY
|
||||
- From a real scenario
|
||||
- Shows the pattern clearly
|
||||
- Ready to adapt
|
||||
|
||||
**Avoid:**
|
||||
|
||||
- Multiple language implementations
|
||||
- Fill-in-the-blank templates
|
||||
- Contrived examples
|
||||
|
||||
### Decision Frameworks
|
||||
|
||||
**Include WHEN information:**
|
||||
Include WHEN information:
|
||||
|
||||
- When to use approach A vs B
|
||||
- What factors influence the decision
|
||||
- Tradeoffs and considerations
|
||||
- Tradeoffs
|
||||
|
||||
**Use tables:**
|
||||
|
||||
```markdown
|
||||
| Your Situation | Recommended Approach |
|
||||
|----------------|---------------------|
|
||||
| Terraform 1.6+, simple logic | Native tests |
|
||||
| Pre-1.6, Go expertise | Terratest |
|
||||
| Complex integration or multi-cloud | Terratest |
|
||||
```
|
||||
|
||||
## Commit Message Format
|
||||
@@ -292,33 +326,24 @@ git commit -m "docs: improve testing strategy documentation"
|
||||
git commit -m "chore: update workflow dependencies"
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
Conventional commits enable:
|
||||
- **Automatic versioning** - Commit type determines version bump
|
||||
- **Generated changelogs** - Changes grouped by type (features, fixes, etc.)
|
||||
- **Release automation** - Releases created on merge to master
|
||||
|
||||
When you merge a PR, the release workflow analyzes all commits since the last release and:
|
||||
1. Calculates the appropriate version bump
|
||||
2. Updates version in marketplace.json (marketplace, plugin, and git ref)
|
||||
3. Generates changelog entry
|
||||
4. Creates GitHub release
|
||||
Commit type determines the version bump, the changelog group, and whether a release is cut on merge to master. The release workflow updates the version in marketplace.json (marketplace root and plugin entry) and in SKILL.md frontmatter.
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. **Create feature branch** from `master`
|
||||
1. **Create a feature branch** from `master`:
|
||||
|
||||
```bash
|
||||
git checkout -b feature/improve-testing-guidance
|
||||
```
|
||||
|
||||
2. **Make changes** following standards above
|
||||
2. **Make changes** following the standards above
|
||||
|
||||
3. **Test changes** (see Testing Requirements)
|
||||
|
||||
4. **Commit with conventional commit format**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: add native test mocking guidance for 1.7+"
|
||||
git commit -m "fix: correct security scanning tool recommendations"
|
||||
@@ -329,7 +354,8 @@ When you merge a PR, the release workflow analyzes all commits since the last re
|
||||
|
||||
### PR Template
|
||||
|
||||
Use the template in `.github/PULL_REQUEST_TEMPLATE.md` - it includes:
|
||||
Use `.github/PULL_REQUEST_TEMPLATE.md`. It covers:
|
||||
|
||||
- Testing checklist
|
||||
- Standards compliance verification
|
||||
- Change description
|
||||
@@ -337,28 +363,30 @@ Use the template in `.github/PULL_REQUEST_TEMPLATE.md` - it includes:
|
||||
|
||||
### Review Criteria
|
||||
|
||||
PRs will be reviewed for:
|
||||
1. **Standards compliance** - Frontmatter, description format
|
||||
2. **Testing evidence** - Baseline vs compliance documented
|
||||
3. **Token efficiency** - Not adding unnecessary content
|
||||
4. **Accuracy** - Technically correct and current
|
||||
5. **Quality** - Clear, scannable, well-organized
|
||||
PRs are reviewed for:
|
||||
|
||||
1. **Standards compliance** — frontmatter, description format
|
||||
2. **Testing evidence** — baseline vs compliance documented
|
||||
3. **Token efficiency** — no unnecessary content added
|
||||
4. **Accuracy** — technically correct and current
|
||||
5. **Quality** — clear, scannable, well-organized
|
||||
|
||||
## Release Process
|
||||
|
||||
Releases are **fully automated** based on conventional commits:
|
||||
Releases are automated from conventional commits:
|
||||
|
||||
1. PR merged to `master`
|
||||
2. Automated workflow analyzes commits since last release
|
||||
3. Calculates version bump (major/minor/patch)
|
||||
4. Workflow updates version in:
|
||||
2. Workflow analyzes commits since the last release
|
||||
3. Workflow calculates the version bump (major/minor/patch)
|
||||
4. Workflow updates:
|
||||
- `.claude-plugin/marketplace.json` (marketplace version, plugin version, git ref)
|
||||
- `skills/terraform-skill/SKILL.md` frontmatter (`metadata.version`)
|
||||
- `CHANGELOG.md` (generated from commits)
|
||||
5. Creates git tag and GitHub Release
|
||||
5. Workflow creates the git tag and GitHub Release
|
||||
|
||||
**Contributors don't need to manage versions** - just use conventional commits in your PRs.
|
||||
Contributors don't manage versions — conventional commits in your PRs are enough.
|
||||
|
||||
For details, see the [Releases section in README.md](README.md#releases).
|
||||
See the [Releases section in README.md](README.md#releases) for details.
|
||||
|
||||
## Questions?
|
||||
|
||||
@@ -369,15 +397,11 @@ For details, see the [Releases section in README.md](README.md#releases).
|
||||
## Additional Resources
|
||||
|
||||
**For contributors:**
|
||||
- [CLAUDE.md](CLAUDE.md) - Detailed development guidelines and architecture
|
||||
- [tests/baseline-scenarios.md](tests/baseline-scenarios.md) - Testing scenarios
|
||||
|
||||
- [CLAUDE.md](CLAUDE.md) — development guidelines, architecture, and LLM Consumption Rules
|
||||
- [tests/baseline-scenarios.md](tests/baseline-scenarios.md) — testing scenarios
|
||||
|
||||
**Skill standards:**
|
||||
|
||||
- [Claude Code Skills Documentation](https://docs.claude.ai/docs/agent-skills)
|
||||
- writing-skills (reference skill for skill development)
|
||||
|
||||
---
|
||||
|
||||
**Thank you for helping make terraform-skill better!** 🎉
|
||||
|
||||
Quality contributions that improve agent behavior are always welcome.
|
||||
|
||||
@@ -1,81 +1,172 @@
|
||||
# Terraform Skill for Claude
|
||||
# Terraform & OpenTofu Skill for AI Agents
|
||||
|
||||
[](https://docs.claude.ai/docs/agent-skills)
|
||||
[](https://agentskills.io)
|
||||
[](https://www.terraform.io/)
|
||||
[](https://opentofu.org/)
|
||||
[](LICENSE)
|
||||
|
||||
Comprehensive Terraform and OpenTofu best practices skill for Claude Code. Get instant guidance on testing strategies, module patterns, CI/CD workflows, and production-ready infrastructure code.
|
||||
Terraform and OpenTofu best-practices skill for AI coding agents (Claude Code, Cursor, Copilot, Gemini CLI, OpenCode, Codex, and others). Covers testing strategies, module patterns, CI/CD workflows, and production infrastructure code.
|
||||
|
||||
## What This Skill Provides
|
||||
## What this skill provides
|
||||
|
||||
🧪 **Testing Frameworks**
|
||||
- Decision matrix for choosing between native tests and Terratest
|
||||
- Testing strategy workflows (static → integration → E2E)
|
||||
- Real-world examples and patterns
|
||||
**Testing frameworks**
|
||||
- Decision matrix for native tests vs Terratest
|
||||
- Testing workflows (static, integration, E2E)
|
||||
- Examples and patterns
|
||||
|
||||
📦 **Module Development**
|
||||
**Module development**
|
||||
- Structure and naming conventions
|
||||
- Versioning strategies
|
||||
- Public vs private module patterns
|
||||
|
||||
🔄 **CI/CD Integration**
|
||||
**State management**
|
||||
- Remote backends (S3, Azure, GCS, Terraform Cloud)
|
||||
- Locking and security
|
||||
- Multi-team state isolation
|
||||
- Migration and recovery procedures
|
||||
|
||||
**CI/CD integration**
|
||||
- GitHub Actions workflows
|
||||
- GitLab CI examples
|
||||
- Cost optimization patterns
|
||||
- Cost optimization
|
||||
- Compliance automation
|
||||
|
||||
🔒 **Security & Compliance**
|
||||
- Trivy, Checkov integration
|
||||
**Security and compliance**
|
||||
- Trivy and Checkov integration
|
||||
- Policy-as-code patterns
|
||||
- Compliance scanning workflows
|
||||
|
||||
📋 **Quick Reference**
|
||||
**Quick reference**
|
||||
- Decision flowcharts
|
||||
- Common patterns (✅ DO vs ❌ DON'T)
|
||||
- Cheat sheets for rapid consultation
|
||||
- Common patterns (DO vs DON'T)
|
||||
- Cheat sheets
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is distributed via Claude Code marketplace using `.claude-plugin/marketplace.json`.
|
||||
|
||||
### Claude Code (Recommended)
|
||||
### Quick install (any agent)
|
||||
|
||||
Universal installer via [skills.sh](https://skills.sh/) — works with any [Agent Skills](https://agentskills.io)-compatible tool:
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/antonbabenko/terraform-skill
|
||||
```
|
||||
|
||||
### Per-host instructions
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
<details>
|
||||
<summary>Claude Code</summary>
|
||||
|
||||
```bash
|
||||
/plugin marketplace add antonbabenko/terraform-skill
|
||||
/plugin install terraform-skill@antonbabenko
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Gemini CLI</summary>
|
||||
|
||||
```bash
|
||||
# Clone to Claude skills directory
|
||||
git clone https://github.com/antonbabenko/terraform-skill ~/.claude/skills/terraform-skill
|
||||
gemini extensions install https://github.com/antonbabenko/terraform-skill
|
||||
```
|
||||
|
||||
### Private Testing
|
||||
Update with `gemini extensions update terraform-skill`.
|
||||
|
||||
While the repository is private, you can test locally:
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Cursor</summary>
|
||||
|
||||
```bash
|
||||
git clone git@github.com:antonbabenko/terraform-skill.git ~/.claude/skills/terraform-skill
|
||||
# Claude Code will load it from the local filesystem
|
||||
git clone https://github.com/antonbabenko/terraform-skill.git ~/.cursor/skills/terraform-skill
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
Cursor auto-discovers skills from `.agents/skills/` and `.cursor/skills/`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Copilot</summary>
|
||||
|
||||
```bash
|
||||
/plugin install https://github.com/antonbabenko/terraform-skill
|
||||
# or
|
||||
git clone https://github.com/antonbabenko/terraform-skill.git ~/.copilot/skills/terraform-skill
|
||||
```
|
||||
|
||||
Copilot auto-discovers skills from `.copilot/skills/`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>OpenCode</summary>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/antonbabenko/terraform-skill.git ~/.agents/skills/terraform-skill
|
||||
```
|
||||
|
||||
OpenCode auto-discovers skills from `.agents/skills/`, `.opencode/skills/`, and `.claude/skills/`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Codex (OpenAI)</summary>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/antonbabenko/terraform-skill.git ~/.agents/skills/terraform-skill
|
||||
```
|
||||
|
||||
Codex auto-discovers skills from `~/.agents/skills/` and `.agents/skills/`. Update with `cd ~/.agents/skills/terraform-skill && git pull`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Antigravity</summary>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/antonbabenko/terraform-skill.git ~/.antigravity/skills/terraform-skill
|
||||
```
|
||||
|
||||
Update with `cd ~/.antigravity/skills/terraform-skill && git pull`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Manual (symlink local clone)</summary>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/antonbabenko/terraform-skill
|
||||
mkdir -p ~/.claude/plugins
|
||||
ln -s "$(pwd)/terraform-skill" ~/.claude/plugins/terraform-skill
|
||||
```
|
||||
|
||||
Claude Code autodiscovers the skill at `skills/terraform-skill/SKILL.md` on next launch. Edits to the clone are picked up live.
|
||||
|
||||
</details>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### Verify installation
|
||||
|
||||
After installation, try:
|
||||
```
|
||||
"Create a Terraform module with testing for an S3 bucket"
|
||||
```
|
||||
|
||||
Claude will automatically use the skill when working with Terraform/OpenTofu code.
|
||||
Claude picks up the skill automatically when working with Terraform or OpenTofu code.
|
||||
|
||||
## Quick Start Examples
|
||||
## Quick start examples
|
||||
|
||||
**Create a module with tests:**
|
||||
> "Create a Terraform module for AWS VPC with native tests"
|
||||
|
||||
**Set up remote state:**
|
||||
> "Configure S3 backend with DynamoDB locking for Terraform state"
|
||||
|
||||
**Review existing code:**
|
||||
> "Review this Terraform configuration following best practices"
|
||||
|
||||
@@ -85,127 +176,83 @@ Claude will automatically use the skill when working with Terraform/OpenTofu cod
|
||||
**Testing strategy:**
|
||||
> "Help me choose between native tests and Terratest for my modules"
|
||||
|
||||
## What It Covers
|
||||
**State management:**
|
||||
> "How should I organize state files for a multi-team environment?"
|
||||
|
||||
### Testing Strategy Framework
|
||||
## What it covers
|
||||
|
||||
Decision matrices for:
|
||||
- When to use native tests (Terraform 1.6+)
|
||||
- When to use Terratest (Go-based)
|
||||
- Multi-environment testing patterns
|
||||
### Testing strategy
|
||||
|
||||
### Module Development Patterns
|
||||
Decision matrices for native tests (Terraform 1.6+) vs Terratest (Go-based), plus multi-environment testing patterns.
|
||||
|
||||
- Naming conventions (`terraform-<PROVIDER>-<NAME>`)
|
||||
- Directory structure best practices
|
||||
- Input variable organization
|
||||
- Output value design
|
||||
- Version constraint patterns
|
||||
- Documentation standards
|
||||
### Module development
|
||||
|
||||
### CI/CD Workflows
|
||||
Naming conventions (`terraform-<PROVIDER>-<NAME>`), directory structure, input/output design, version constraints, and documentation standards.
|
||||
|
||||
- GitHub Actions examples
|
||||
- GitLab CI templates
|
||||
- Atlantis integration
|
||||
- Cost estimation (Infracost)
|
||||
- Security scanning (Trivy, Checkov)
|
||||
- Compliance checking
|
||||
### CI/CD workflows
|
||||
|
||||
### Security & Compliance
|
||||
GitHub Actions, GitLab CI, Atlantis, Infracost cost estimation, Trivy/Checkov scanning, and compliance checks.
|
||||
|
||||
- Static analysis integration
|
||||
- Policy-as-code patterns
|
||||
- Secrets management
|
||||
- State file security
|
||||
- Compliance scanning workflows
|
||||
### Security and compliance
|
||||
|
||||
### Common Patterns & Anti-patterns
|
||||
Static analysis, policy-as-code, secrets management, state file security, backend encryption, and compliance scanning workflows.
|
||||
|
||||
Side-by-side ✅ DO vs ❌ DON'T examples for:
|
||||
- Variable naming
|
||||
- Resource naming
|
||||
- Module composition
|
||||
- State management
|
||||
- Provider configuration
|
||||
### Patterns and anti-patterns
|
||||
|
||||
## Why This Skill?
|
||||
Side-by-side DO vs DON'T examples for variable naming, resource naming, module composition, state management, and provider configuration.
|
||||
|
||||
**Based on Production Experience:**
|
||||
## Why this skill
|
||||
|
||||
**Sources:**
|
||||
- Patterns from [terraform-best-practices.com](https://www.terraform-best-practices.com/)
|
||||
- Community-tested approaches from terraform-aws-modules
|
||||
- AWS Hero expertise in enterprise IaC
|
||||
- Real-world usage across 100+ modules
|
||||
- Approaches used across the [terraform-aws-modules](https://github.com/terraform-aws-modules) collection
|
||||
- AWS Hero experience with enterprise IaC
|
||||
|
||||
**Version-Specific Guidance:**
|
||||
**Version-specific guidance:**
|
||||
- Terraform 1.0+ features
|
||||
- OpenTofu 1.6+ compatibility
|
||||
- Native test framework (1.6+)
|
||||
- Current tooling ecosystem (2024-2026)
|
||||
|
||||
**Decision Frameworks:**
|
||||
Not just "what to do" but "when and why" - helping you make informed architecture decisions.
|
||||
**Decision frameworks:** not just "what to do" but "when and why".
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Claude Code** or other Claude environment supporting skills
|
||||
- **Terraform** 1.0+ or **OpenTofu** 1.6+
|
||||
- Optional: MCP Terraform server for enhanced registry integration
|
||||
- An AI agent with skill support: Claude Code, Cursor, Copilot, Gemini CLI, OpenCode, Codex, or any [Agent Skills](https://agentskills.io)-compatible host
|
||||
- Terraform 1.0+ or OpenTofu 1.6+
|
||||
- Optional: [Terraform MCP server](https://github.com/hashicorp/terraform-mcp-server) for registry integration
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CLAUDE.md](CLAUDE.md) for:
|
||||
- Skill development guidelines
|
||||
- Content structure philosophy
|
||||
- How to propose improvements
|
||||
- Testing and validation approach
|
||||
See [CLAUDE.md](CLAUDE.md) for skill development guidelines, content structure, how to propose improvements, and the validation approach.
|
||||
|
||||
**Issues & Feedback:**
|
||||
[GitHub Issues](https://github.com/antonbabenko/terraform-skill/issues)
|
||||
Report bugs or request features via [GitHub Issues](https://github.com/antonbabenko/terraform-skill/issues).
|
||||
|
||||
## Releases
|
||||
## Related resources
|
||||
|
||||
Releases are automated based on conventional commits in commit messages:
|
||||
### Official documentation
|
||||
- [Terraform Language](https://developer.hashicorp.com/terraform/docs)
|
||||
- [Terraform Testing](https://developer.hashicorp.com/terraform/language/tests) - native test framework
|
||||
- [OpenTofu Documentation](https://opentofu.org/docs/)
|
||||
- [HashiCorp Recommended Practices](https://developer.hashicorp.com/terraform/cloud-docs/recommended-practices)
|
||||
|
||||
| Commit Type | Version Bump | Example |
|
||||
|-------------|--------------|---------|
|
||||
| `feat!:` or `BREAKING CHANGE:` | Major | 1.2.3 → 2.0.0 |
|
||||
| `feat:` | Minor | 1.2.3 → 1.3.0 |
|
||||
| `fix:` | Patch | 1.2.3 → 1.2.4 |
|
||||
| Other commits | Patch (default) | 1.2.3 → 1.2.4 |
|
||||
|
||||
Releases are created automatically when changes are pushed to master.
|
||||
|
||||
## Related Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Terraform Language](https://developer.hashicorp.com/terraform/docs) - HashiCorp official docs
|
||||
- [Terraform Testing](https://developer.hashicorp.com/terraform/language/tests) - Native test framework
|
||||
- [OpenTofu Documentation](https://opentofu.org/docs/) - OpenTofu official docs
|
||||
- [HashiCorp Best Practices](https://developer.hashicorp.com/terraform/cloud-docs/recommended-practices) - Cloud best practices
|
||||
|
||||
### Community Resources
|
||||
### Community resources
|
||||
- [Terraform compliance-as-code docs](https://compliance.tf/docs/) - Compliance frameworks, controls, implementation guides, remediations, etc
|
||||
- [Awesome Terraform](https://github.com/shuaibiyy/awesome-tf)
|
||||
- [Terraform Best Practices](https://terraform-best-practices.com) - Comprehensive guide (base for this skill)
|
||||
- [terraform-aws-modules](https://github.com/terraform-aws-modules) - Production-grade AWS modules
|
||||
- [Awesome Terraform Compliance](https://github.com/antonbabenko/awesome-terraform-compliance)
|
||||
- [Terraform Best Practices](https://terraform-best-practices.com) - the guide this skill is based on
|
||||
- [terraform-aws-modules](https://github.com/terraform-aws-modules) - AWS modules collection
|
||||
- [Terratest](https://terratest.gruntwork.io/docs/) - Go testing framework for Terraform
|
||||
- [Google Cloud Best Practices](https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure)
|
||||
- [AWS Terraform Best Practices](https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/introduction.html)
|
||||
|
||||
### Development Tools
|
||||
- [pre-commit-terraform](https://github.com/antonbabenko/pre-commit-terraform) - Pre-commit hooks for Terraform
|
||||
- [terraform-docs](https://terraform-docs.io/) - Generate documentation from Terraform modules
|
||||
### Development tools
|
||||
- [pre-commit-terraform](https://github.com/antonbabenko/pre-commit-terraform) - pre-commit hooks for Terraform
|
||||
- [terraform-docs](https://terraform-docs.io/) - generate documentation from modules
|
||||
- [terraform-switcher](https://github.com/warrensbox/terraform-switcher) - Terraform version manager
|
||||
- [TFLint](https://github.com/terraform-linters/tflint) - Terraform linter
|
||||
- [Trivy](https://github.com/aquasecurity/trivy) - Security scanner for IaC
|
||||
- [Trivy](https://github.com/aquasecurity/trivy) - IaC security scanner
|
||||
|
||||
## License & Attribution
|
||||
## License
|
||||
|
||||
**License:** Apache 2.0 - see [LICENSE](LICENSE)
|
||||
|
||||
If you create derivative works or skills based on this skill, please include:
|
||||
```
|
||||
Based on terraform-skill by Anton Babenko
|
||||
https://github.com/antonbabenko/terraform-skill
|
||||
terraform-best-practices.com | Compliance.tf
|
||||
```
|
||||
Apache 2.0
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
---
|
||||
name: terraform-skill
|
||||
description: Use when working with Terraform or OpenTofu - creating modules, writing tests (native test framework, Terratest), setting up CI/CD pipelines, reviewing configurations, choosing between testing approaches, debugging state issues, implementing security scanning (trivy, checkov), or making infrastructure-as-code architecture decisions
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: Anton Babenko
|
||||
version: 1.6.0
|
||||
---
|
||||
|
||||
# Terraform Skill for Claude
|
||||
|
||||
Comprehensive Terraform and OpenTofu guidance covering testing, modules, CI/CD, and production patterns. Based on terraform-best-practices.com and enterprise experience.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**Activate this skill when:**
|
||||
- Creating new Terraform or OpenTofu configurations or modules
|
||||
- Setting up testing infrastructure for IaC code
|
||||
- Deciding between testing approaches (validate, plan, frameworks)
|
||||
- Structuring multi-environment deployments
|
||||
- Implementing CI/CD for infrastructure-as-code
|
||||
- Reviewing or refactoring existing Terraform/OpenTofu projects
|
||||
- Choosing between module patterns or state management approaches
|
||||
|
||||
**Don't use this skill for:**
|
||||
- Basic Terraform/OpenTofu syntax questions (Claude knows this)
|
||||
- Provider-specific API reference (link to docs instead)
|
||||
- Cloud platform questions unrelated to Terraform/OpenTofu
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Code Structure Philosophy
|
||||
|
||||
**Module Hierarchy:**
|
||||
|
||||
| Type | When to Use | Scope |
|
||||
|------|-------------|-------|
|
||||
| **Resource Module** | Single logical group of connected resources | VPC + subnets, Security group + rules |
|
||||
| **Infrastructure Module** | Collection of resource modules for a purpose | Multiple resource modules in one region/account |
|
||||
| **Composition** | Complete infrastructure | Spans multiple regions/accounts |
|
||||
|
||||
**Hierarchy:** Resource → Resource Module → Infrastructure Module → Composition
|
||||
|
||||
**Directory Structure:**
|
||||
```
|
||||
environments/ # Environment-specific configurations
|
||||
├── prod/
|
||||
├── staging/
|
||||
└── dev/
|
||||
|
||||
modules/ # Reusable modules
|
||||
├── networking/
|
||||
├── compute/
|
||||
└── data/
|
||||
|
||||
examples/ # Module usage examples (also serve as tests)
|
||||
├── complete/
|
||||
└── minimal/
|
||||
```
|
||||
|
||||
**Key principle from terraform-best-practices.com:**
|
||||
- Separate **environments** (prod, staging) from **modules** (reusable components)
|
||||
- Use **examples/** as both documentation and integration test fixtures
|
||||
- Keep modules small and focused (single responsibility)
|
||||
|
||||
**For detailed module architecture, see:** [Code Patterns: Module Types & Hierarchy](references/code-patterns.md)
|
||||
|
||||
### 2. Naming Conventions
|
||||
|
||||
**Resources:**
|
||||
```hcl
|
||||
# Good: Descriptive, contextual
|
||||
resource "aws_instance" "web_server" { }
|
||||
resource "aws_s3_bucket" "application_logs" { }
|
||||
|
||||
# Good: "this" for singleton resources (only one of that type)
|
||||
resource "aws_vpc" "this" { }
|
||||
resource "aws_security_group" "this" { }
|
||||
|
||||
# Avoid: Generic names for non-singletons
|
||||
resource "aws_instance" "main" { }
|
||||
resource "aws_s3_bucket" "bucket" { }
|
||||
```
|
||||
|
||||
**Singleton Resources:**
|
||||
|
||||
Use `"this"` when your module creates only one resource of that type:
|
||||
|
||||
✅ DO:
|
||||
```hcl
|
||||
resource "aws_vpc" "this" {} # Module creates one VPC
|
||||
resource "aws_security_group" "this" {} # Module creates one SG
|
||||
```
|
||||
|
||||
❌ DON'T use "this" for multiple resources:
|
||||
```hcl
|
||||
resource "aws_subnet" "this" {} # If creating multiple subnets
|
||||
```
|
||||
|
||||
Use descriptive names when creating multiple resources of the same type.
|
||||
|
||||
**Variables:**
|
||||
```hcl
|
||||
# Prefix with context when needed
|
||||
var.vpc_cidr_block # Not just "cidr"
|
||||
var.database_instance_class # Not just "instance_class"
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `main.tf` - Primary resources
|
||||
- `variables.tf` - Input variables
|
||||
- `outputs.tf` - Output values
|
||||
- `versions.tf` - Provider versions
|
||||
- `data.tf` - Data sources (optional)
|
||||
|
||||
## Testing Strategy Framework
|
||||
|
||||
### Decision Matrix: Which Testing Approach?
|
||||
|
||||
| Your Situation | Recommended Approach | Tools | Cost |
|
||||
|----------------|---------------------|-------|------|
|
||||
| **Quick syntax check** | Static analysis | `terraform validate`, `fmt` | Free |
|
||||
| **Pre-commit validation** | Static + lint | `validate`, `tflint`, `trivy`, `checkov` | Free |
|
||||
| **Terraform 1.6+, simple logic** | Native test framework | Built-in `terraform test` | Free-Low |
|
||||
| **Pre-1.6, or Go expertise** | Integration testing | Terratest | Low-Med |
|
||||
| **Security/compliance focus** | Policy as code | OPA, Sentinel | Free |
|
||||
| **Cost-sensitive workflow** | Mock providers (1.7+) | Native tests + mocking | Free |
|
||||
| **Multi-cloud, complex** | Full integration | Terratest + real infra | Med-High |
|
||||
|
||||
### Testing Pyramid for Infrastructure
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ End-to-End Tests (Expensive)
|
||||
/____\ - Full environment deployment
|
||||
/ \ - Production-like setup
|
||||
/________\
|
||||
/ \ Integration Tests (Moderate)
|
||||
/____________\ - Module testing in isolation
|
||||
/ \ - Real resources in test account
|
||||
/________________\ Static Analysis (Cheap)
|
||||
- validate, fmt, lint
|
||||
- Security scanning
|
||||
```
|
||||
|
||||
### Native Test Best Practices (1.6+)
|
||||
|
||||
**Before generating test code:**
|
||||
|
||||
1. **Validate schemas with Terraform MCP:**
|
||||
```
|
||||
Search provider docs → Get resource schema → Identify block types
|
||||
```
|
||||
|
||||
2. **Choose correct command mode:**
|
||||
- `command = plan` - Fast, for input validation
|
||||
- `command = apply` - Required for computed values and set-type blocks
|
||||
|
||||
3. **Handle set-type blocks correctly:**
|
||||
- Cannot index with `[0]`
|
||||
- Use `for` expressions to iterate
|
||||
- Or use `command = apply` to materialize
|
||||
|
||||
**Common patterns:**
|
||||
- S3 encryption rules: **set** (use for expressions)
|
||||
- Lifecycle transitions: **set** (use for expressions)
|
||||
- IAM policy statements: **set** (use for expressions)
|
||||
|
||||
**For detailed testing guides, see:**
|
||||
- **[Testing Frameworks Guide](references/testing-frameworks.md)** - Deep dive into static analysis, native tests, and Terratest
|
||||
- **[Quick Reference](references/quick-reference.md#testing-approach-selection)** - Decision flowchart and command cheat sheet
|
||||
|
||||
## Code Structure Standards
|
||||
|
||||
### Resource Block Ordering
|
||||
|
||||
**Strict ordering for consistency:**
|
||||
1. `count` or `for_each` FIRST (blank line after)
|
||||
2. Other arguments
|
||||
3. `tags` as last real argument
|
||||
4. `depends_on` after tags (if needed)
|
||||
5. `lifecycle` at the very end (if needed)
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - Correct ordering
|
||||
resource "aws_nat_gateway" "this" {
|
||||
count = var.create_nat_gateway ? 1 : 0
|
||||
|
||||
allocation_id = aws_eip.this[0].id
|
||||
subnet_id = aws_subnet.public[0].id
|
||||
|
||||
tags = {
|
||||
Name = "${var.name}-nat"
|
||||
}
|
||||
|
||||
depends_on = [aws_internet_gateway.this]
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Variable Block Ordering
|
||||
|
||||
1. `description` (ALWAYS required)
|
||||
2. `type`
|
||||
3. `default`
|
||||
4. `validation`
|
||||
5. `nullable` (when setting to false)
|
||||
|
||||
```hcl
|
||||
variable "environment" {
|
||||
description = "Environment name for resource tagging"
|
||||
type = string
|
||||
default = "dev"
|
||||
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "prod"], var.environment)
|
||||
error_message = "Environment must be one of: dev, staging, prod."
|
||||
}
|
||||
|
||||
nullable = false
|
||||
}
|
||||
```
|
||||
|
||||
**For complete structure guidelines, see:** [Code Patterns: Block Ordering & Structure](references/code-patterns.md#block-ordering--structure)
|
||||
|
||||
## Count vs For_Each: When to Use Each
|
||||
|
||||
### Quick Decision Guide
|
||||
|
||||
| Scenario | Use | Why |
|
||||
|----------|-----|-----|
|
||||
| Boolean condition (create or don't) | `count = condition ? 1 : 0` | Simple on/off toggle |
|
||||
| Simple numeric replication | `count = 3` | Fixed number of identical resources |
|
||||
| Items may be reordered/removed | `for_each = toset(list)` | Stable resource addresses |
|
||||
| Reference by key | `for_each = map` | Named access to resources |
|
||||
| Multiple named resources | `for_each` | Better maintainability |
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Boolean conditions:**
|
||||
```hcl
|
||||
# ✅ GOOD - Boolean condition
|
||||
resource "aws_nat_gateway" "this" {
|
||||
count = var.create_nat_gateway ? 1 : 0
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
**Stable addressing with for_each:**
|
||||
```hcl
|
||||
# ✅ GOOD - Removing "us-east-1b" only affects that subnet
|
||||
resource "aws_subnet" "private" {
|
||||
for_each = toset(var.availability_zones)
|
||||
|
||||
availability_zone = each.key
|
||||
# ...
|
||||
}
|
||||
|
||||
# ❌ BAD - Removing middle AZ recreates all subsequent subnets
|
||||
resource "aws_subnet" "private" {
|
||||
count = length(var.availability_zones)
|
||||
|
||||
availability_zone = var.availability_zones[count.index]
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
**For migration guides and detailed examples, see:** [Code Patterns: Count vs For_Each](references/code-patterns.md#count-vs-for_each-deep-dive)
|
||||
|
||||
## Locals for Dependency Management
|
||||
|
||||
**Use locals to ensure correct resource deletion order:**
|
||||
|
||||
```hcl
|
||||
# Problem: Subnets might be deleted after CIDR blocks, causing errors
|
||||
# Solution: Use try() in locals to hint deletion order
|
||||
|
||||
locals {
|
||||
# References secondary CIDR first, falling back to VPC
|
||||
# Forces Terraform to delete subnets before CIDR association
|
||||
vpc_id = try(
|
||||
aws_vpc_ipv4_cidr_block_association.this[0].vpc_id,
|
||||
aws_vpc.this.id,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_vpc" "this" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
resource "aws_vpc_ipv4_cidr_block_association" "this" {
|
||||
count = var.add_secondary_cidr ? 1 : 0
|
||||
|
||||
vpc_id = aws_vpc.this.id
|
||||
cidr_block = "10.1.0.0/16"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = local.vpc_id # Uses local, not direct reference
|
||||
cidr_block = "10.1.0.0/24"
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Prevents deletion errors when destroying infrastructure
|
||||
- Ensures correct dependency order without explicit `depends_on`
|
||||
- Particularly useful for VPC configurations with secondary CIDR blocks
|
||||
|
||||
**For detailed examples, see:** [Code Patterns: Locals for Dependency Management](references/code-patterns.md#locals-for-dependency-management)
|
||||
|
||||
## Module Development
|
||||
|
||||
### Standard Module Structure
|
||||
|
||||
```
|
||||
my-module/
|
||||
├── README.md # Usage documentation
|
||||
├── main.tf # Primary resources
|
||||
├── variables.tf # Input variables with descriptions
|
||||
├── outputs.tf # Output values
|
||||
├── versions.tf # Provider version constraints
|
||||
├── examples/
|
||||
│ ├── minimal/ # Minimal working example
|
||||
│ └── complete/ # Full-featured example
|
||||
└── tests/ # Test files
|
||||
└── module_test.tftest.hcl # Or .go
|
||||
```
|
||||
|
||||
### Best Practices Summary
|
||||
|
||||
**Variables:**
|
||||
- ✅ Always include `description`
|
||||
- ✅ Use explicit `type` constraints
|
||||
- ✅ Provide sensible `default` values where appropriate
|
||||
- ✅ Add `validation` blocks for complex constraints
|
||||
- ✅ Use `sensitive = true` for secrets
|
||||
|
||||
**Outputs:**
|
||||
- ✅ Always include `description`
|
||||
- ✅ Mark sensitive outputs with `sensitive = true`
|
||||
- ✅ Consider returning objects for related values
|
||||
- ✅ Document what consumers should do with each output
|
||||
|
||||
**For detailed module patterns, see:**
|
||||
- **[Module Patterns Guide](references/module-patterns.md)** - Variable best practices, output design, ✅ DO vs ❌ DON'T patterns
|
||||
- **[Quick Reference](references/quick-reference.md#common-patterns)** - Resource naming, variable naming, file organization
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### Recommended Workflow Stages
|
||||
|
||||
1. **Validate** - Format check + syntax validation + linting
|
||||
2. **Test** - Run automated tests (native or Terratest)
|
||||
3. **Plan** - Generate and review execution plan
|
||||
4. **Apply** - Execute changes (with approvals for production)
|
||||
|
||||
### Cost Optimization Strategy
|
||||
|
||||
1. **Use mocking for PR validation** (free)
|
||||
2. **Run integration tests only on main branch** (controlled cost)
|
||||
3. **Implement auto-cleanup** (prevent orphaned resources)
|
||||
4. **Tag all test resources** (track spending)
|
||||
|
||||
**For complete CI/CD templates, see:**
|
||||
- **[CI/CD Workflows Guide](references/ci-cd-workflows.md)** - GitHub Actions, GitLab CI, Atlantis integration, cost optimization
|
||||
- **[Quick Reference](references/quick-reference.md#troubleshooting-guide)** - Common CI/CD issues and solutions
|
||||
|
||||
## Security & Compliance
|
||||
|
||||
### Essential Security Checks
|
||||
|
||||
```bash
|
||||
# Static security scanning
|
||||
trivy config .
|
||||
checkov -d .
|
||||
```
|
||||
|
||||
### Common Issues to Avoid
|
||||
|
||||
❌ **Don't:**
|
||||
- Store secrets in variables
|
||||
- Use default VPC
|
||||
- Skip encryption
|
||||
- Open security groups to 0.0.0.0/0
|
||||
|
||||
✅ **Do:**
|
||||
- Use AWS Secrets Manager / Parameter Store
|
||||
- Create dedicated VPCs
|
||||
- Enable encryption at rest
|
||||
- Use least-privilege security groups
|
||||
|
||||
**For detailed security guidance, see:**
|
||||
- **[Security & Compliance Guide](references/security-compliance.md)** - Trivy/Checkov integration, secrets management, state file security, compliance testing
|
||||
|
||||
## Version Management
|
||||
|
||||
### Version Constraint Syntax
|
||||
|
||||
```hcl
|
||||
version = "5.0.0" # Exact (avoid - inflexible)
|
||||
version = "~> 5.0" # Recommended: 5.0.x only
|
||||
version = ">= 5.0" # Minimum (risky - breaking changes)
|
||||
```
|
||||
|
||||
### Strategy by Component
|
||||
|
||||
| Component | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| **Terraform** | Pin minor version | `required_version = "~> 1.9"` |
|
||||
| **Providers** | Pin major version | `version = "~> 5.0"` |
|
||||
| **Modules (prod)** | Pin exact version | `version = "5.1.2"` |
|
||||
| **Modules (dev)** | Allow patch updates | `version = "~> 5.1"` |
|
||||
|
||||
### Update Workflow
|
||||
|
||||
```bash
|
||||
# Lock versions initially
|
||||
terraform init # Creates .terraform.lock.hcl
|
||||
|
||||
# Update to latest within constraints
|
||||
terraform init -upgrade # Updates providers
|
||||
|
||||
# Review and test
|
||||
terraform plan
|
||||
```
|
||||
|
||||
**For detailed version management, see:** [Code Patterns: Version Management](references/code-patterns.md#version-management)
|
||||
|
||||
## Modern Terraform Features (1.0+)
|
||||
|
||||
### Feature Availability by Version
|
||||
|
||||
| Feature | Version | Use Case |
|
||||
|---------|---------|----------|
|
||||
| `try()` function | 0.13+ | Safe fallbacks, replaces `element(concat())` |
|
||||
| `nullable = false` | 1.1+ | Prevent null values in variables |
|
||||
| `moved` blocks | 1.1+ | Refactor without destroy/recreate |
|
||||
| `optional()` with defaults | 1.3+ | Optional object attributes |
|
||||
| Native testing | 1.6+ | Built-in test framework |
|
||||
| Mock providers | 1.7+ | Cost-free unit testing |
|
||||
| Provider functions | 1.8+ | Provider-specific data transformation |
|
||||
| Cross-variable validation | 1.9+ | Validate relationships between variables |
|
||||
| Write-only arguments | 1.11+ | Secrets never stored in state |
|
||||
|
||||
### Quick Examples
|
||||
|
||||
```hcl
|
||||
# try() - Safe fallbacks (0.13+)
|
||||
output "sg_id" {
|
||||
value = try(aws_security_group.this[0].id, "")
|
||||
}
|
||||
|
||||
# optional() - Optional attributes with defaults (1.3+)
|
||||
variable "config" {
|
||||
type = object({
|
||||
name = string
|
||||
timeout = optional(number, 300) # Default: 300
|
||||
})
|
||||
}
|
||||
|
||||
# Cross-variable validation (1.9+)
|
||||
variable "environment" { type = string }
|
||||
variable "backup_days" {
|
||||
type = number
|
||||
validation {
|
||||
condition = var.environment == "prod" ? var.backup_days >= 7 : true
|
||||
error_message = "Production requires backup_days >= 7"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**For complete patterns and examples, see:** [Code Patterns: Modern Terraform Features](references/code-patterns.md#modern-terraform-features-10)
|
||||
|
||||
## Version-Specific Guidance
|
||||
|
||||
### Terraform 1.0-1.5
|
||||
- Use Terratest for testing
|
||||
- No native testing framework available
|
||||
- Focus on static analysis and plan validation
|
||||
|
||||
### Terraform 1.6+ / OpenTofu 1.6+
|
||||
- **New:** Native `terraform test` / `tofu test` command
|
||||
- Consider migrating from external frameworks for simple tests
|
||||
- Keep Terratest only for complex integration tests
|
||||
|
||||
### Terraform 1.7+ / OpenTofu 1.7+
|
||||
- **New:** Mock providers for unit testing
|
||||
- Reduce cost by mocking external dependencies
|
||||
- Use real integration tests for final validation
|
||||
|
||||
### Terraform vs OpenTofu
|
||||
|
||||
Both are fully supported by this skill. For licensing, governance, and feature comparison, see [Quick Reference: Terraform vs OpenTofu](references/quick-reference.md#terraform-vs-opentofu-comparison).
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
This skill uses **progressive disclosure** - essential information is in this main file, detailed guides are available when needed:
|
||||
|
||||
📚 **Reference Files:**
|
||||
- **[Testing Frameworks](references/testing-frameworks.md)** - In-depth guide to static analysis, native tests, and Terratest
|
||||
- **[Module Patterns](references/module-patterns.md)** - Module structure, variable/output best practices, ✅ DO vs ❌ DON'T patterns
|
||||
- **[CI/CD Workflows](references/ci-cd-workflows.md)** - GitHub Actions, GitLab CI templates, cost optimization, automated cleanup
|
||||
- **[Security & Compliance](references/security-compliance.md)** - Trivy/Checkov integration, secrets management, compliance testing
|
||||
- **[Quick Reference](references/quick-reference.md)** - Command cheat sheets, decision flowcharts, troubleshooting guide
|
||||
|
||||
**How to use:** When you need detailed information on a topic, reference the appropriate guide. Claude will load it on demand to provide comprehensive guidance.
|
||||
|
||||
## License
|
||||
|
||||
This skill is licensed under the **Apache License 2.0**. See the LICENSE file for full terms.
|
||||
|
||||
**Copyright © 2026 Anton Babenko**
|
||||
@@ -1,470 +0,0 @@
|
||||
# Security & Compliance
|
||||
|
||||
> **Part of:** [terraform-skill](../SKILL.md)
|
||||
> **Purpose:** Security best practices and compliance patterns for Terraform/OpenTofu
|
||||
|
||||
This document provides security hardening guidance and compliance automation strategies for infrastructure-as-code.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Security Scanning Tools](#security-scanning-tools)
|
||||
2. [Common Security Issues](#common-security-issues)
|
||||
3. [Compliance Testing](#compliance-testing)
|
||||
4. [Secrets Management](#secrets-management)
|
||||
5. [State File Security](#state-file-security)
|
||||
|
||||
---
|
||||
|
||||
## Security Scanning Tools
|
||||
|
||||
### Essential Security Checks
|
||||
|
||||
```bash
|
||||
# Static security scanning
|
||||
trivy config .
|
||||
checkov -d .
|
||||
|
||||
# Compliance testing
|
||||
terraform-compliance -f compliance/ -p tfplan.json
|
||||
```
|
||||
|
||||
### Trivy Integration
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install trivy
|
||||
|
||||
# Linux
|
||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
# In CI
|
||||
- uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: 'config'
|
||||
scan-ref: '.'
|
||||
```
|
||||
|
||||
**Note:** Trivy is the successor to tfsec, maintained by Aqua Security.
|
||||
|
||||
**Example Output:**
|
||||
|
||||
```
|
||||
Result #1 HIGH Security group rule allows egress to multiple public internet addresses
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
security.tf:15-20
|
||||
|
||||
12 | resource "aws_security_group_rule" "egress" {
|
||||
13 | type = "egress"
|
||||
14 | from_port = 0
|
||||
15 | to_port = 0
|
||||
16 | protocol = "-1"
|
||||
17 | cidr_blocks = ["0.0.0.0/0"]
|
||||
18 | security_group_id = aws_security_group.this.id
|
||||
19 | }
|
||||
```
|
||||
|
||||
### Checkov Integration
|
||||
|
||||
```bash
|
||||
# Run Checkov
|
||||
checkov -d . --framework terraform
|
||||
|
||||
# Skip specific checks
|
||||
checkov -d . --skip-check CKV_AWS_23
|
||||
|
||||
# Generate JSON report
|
||||
checkov -d . -o json > checkov-report.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Security Issues
|
||||
|
||||
### ❌ DON'T: Store Secrets in Variables
|
||||
|
||||
```hcl
|
||||
# BAD: Secret in plaintext
|
||||
variable "database_password" {
|
||||
type = string
|
||||
default = "SuperSecret123!" # ❌ Never do this
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Use Secrets Manager
|
||||
|
||||
```hcl
|
||||
# Good: Reference secrets from AWS Secrets Manager
|
||||
data "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = "prod/database/password"
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
password = data.aws_secretsmanager_secret_version.db_password.secret_string
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Default VPC
|
||||
|
||||
```hcl
|
||||
# BAD: Default VPC has public subnets
|
||||
resource "aws_instance" "app" {
|
||||
ami = "ami-12345"
|
||||
subnet_id = "subnet-default" # ❌ Avoid default resources
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Create Dedicated VPCs
|
||||
|
||||
```hcl
|
||||
# Good: Custom VPC with private subnets
|
||||
resource "aws_vpc" "this" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
}
|
||||
|
||||
resource "aws_subnet" "private" {
|
||||
vpc_id = aws_vpc.this.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
availability_zone = "us-east-1a"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Skip Encryption
|
||||
|
||||
```hcl
|
||||
# BAD: Unencrypted S3 bucket
|
||||
resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
# ❌ No encryption configured
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Enable Encryption at Rest
|
||||
|
||||
```hcl
|
||||
# Good: Enable encryption
|
||||
resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
|
||||
bucket = aws_s3_bucket.data.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "AES256"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Open Security Groups to Internet
|
||||
|
||||
```hcl
|
||||
# BAD: Security group open to internet
|
||||
resource "aws_security_group_rule" "allow_all" {
|
||||
type = "ingress"
|
||||
from_port = 0
|
||||
to_port = 65535
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # ❌ Never do this
|
||||
security_group_id = aws_security_group.this.id
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Use Least-Privilege Security Groups
|
||||
|
||||
```hcl
|
||||
# Good: Restrict to specific ports and sources
|
||||
resource "aws_security_group_rule" "app_https" {
|
||||
type = "ingress"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"] # ✅ Internal only
|
||||
security_group_id = aws_security_group.this.id
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compliance Testing
|
||||
|
||||
### terraform-compliance
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
pip install terraform-compliance
|
||||
```
|
||||
|
||||
**Example Compliance Test:**
|
||||
|
||||
```gherkin
|
||||
# compliance/aws-encryption.feature
|
||||
Feature: AWS Resources must be encrypted
|
||||
|
||||
Scenario: S3 buckets must have encryption
|
||||
Given I have aws_s3_bucket defined
|
||||
When it has aws_s3_bucket_server_side_encryption_configuration
|
||||
Then it must contain rule
|
||||
And it must contain apply_server_side_encryption_by_default
|
||||
|
||||
Scenario: RDS instances must be encrypted
|
||||
Given I have aws_db_instance defined
|
||||
Then it must contain storage_encrypted
|
||||
And its value must be true
|
||||
```
|
||||
|
||||
**Run Tests:**
|
||||
|
||||
```bash
|
||||
# Generate plan in JSON
|
||||
terraform plan -out=tfplan
|
||||
terraform show -json tfplan > tfplan.json
|
||||
|
||||
# Run compliance tests
|
||||
terraform-compliance -f compliance/ -p tfplan.json
|
||||
```
|
||||
|
||||
### Open Policy Agent (OPA)
|
||||
|
||||
```rego
|
||||
# policy/s3_encryption.rego
|
||||
package terraform.s3
|
||||
|
||||
deny[msg] {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_s3_bucket"
|
||||
not resource.change.after.server_side_encryption_configuration
|
||||
|
||||
msg := sprintf("S3 bucket '%s' must have encryption enabled", [resource.address])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secrets Management
|
||||
|
||||
### AWS Secrets Manager Pattern
|
||||
|
||||
```hcl
|
||||
# Create secret
|
||||
resource "aws_secretsmanager_secret" "db_password" {
|
||||
name = "prod/database/password"
|
||||
description = "RDS master password"
|
||||
|
||||
recovery_window_in_days = 30
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = aws_secretsmanager_secret.db_password.id
|
||||
secret_string = random_password.db_password.result
|
||||
}
|
||||
|
||||
# Generate secure password
|
||||
resource "random_password" "db_password" {
|
||||
length = 32
|
||||
special = true
|
||||
}
|
||||
|
||||
# Use secret in RDS
|
||||
data "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = aws_secretsmanager_secret.db_password.id
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
password = data.aws_secretsmanager_secret_version.db_password.secret_string
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Never commit these
|
||||
export TF_VAR_database_password="secret123"
|
||||
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
|
||||
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
```
|
||||
|
||||
**In .gitignore:**
|
||||
|
||||
```
|
||||
*.tfvars
|
||||
.env
|
||||
secrets/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State File Security
|
||||
|
||||
### Encrypt State at Rest
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-terraform-state"
|
||||
key = "prod/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
dynamodb_table = "terraform-locks"
|
||||
encrypt = true # ✅ Always enable encryption
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Secure State Bucket
|
||||
|
||||
```hcl
|
||||
resource "aws_s3_bucket" "terraform_state" {
|
||||
bucket = "my-terraform-state"
|
||||
}
|
||||
|
||||
# Enable versioning (protect against accidental deletion)
|
||||
resource "aws_s3_bucket_versioning" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# Enable encryption
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "AES256"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Block public access
|
||||
resource "aws_s3_bucket_public_access_block" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
block_public_acls = true
|
||||
block_public_policy = true
|
||||
ignore_public_acls = true
|
||||
restrict_public_buckets = true
|
||||
}
|
||||
```
|
||||
|
||||
### Restrict State Access
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::123456789012:role/TerraformRole"
|
||||
},
|
||||
"Action": [
|
||||
"s3:ListBucket",
|
||||
"s3:GetObject",
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::my-terraform-state",
|
||||
"arn:aws:s3:::my-terraform-state/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IAM Best Practices
|
||||
|
||||
### ✅ DO: Use Least Privilege
|
||||
|
||||
```hcl
|
||||
# Good: Specific permissions only
|
||||
resource "aws_iam_policy" "app_policy" {
|
||||
name = "app-policy"
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject"
|
||||
]
|
||||
Resource = "arn:aws:s3:::my-app-bucket/*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Wildcard Permissions
|
||||
|
||||
```hcl
|
||||
# BAD: Overly broad permissions
|
||||
resource "aws_iam_policy" "bad_policy" {
|
||||
policy = jsonencode({
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = "*" # ❌ Never use wildcard
|
||||
Resource = "*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compliance Checklists
|
||||
|
||||
### SOC 2 Compliance
|
||||
|
||||
- [ ] Encryption at rest for all data stores
|
||||
- [ ] Encryption in transit (TLS/SSL)
|
||||
- [ ] IAM policies follow least privilege
|
||||
- [ ] Logging enabled for all resources
|
||||
- [ ] MFA required for privileged access
|
||||
- [ ] Regular security scanning in CI/CD
|
||||
|
||||
### HIPAA Compliance
|
||||
|
||||
- [ ] PHI encrypted at rest and in transit
|
||||
- [ ] Access logs enabled
|
||||
- [ ] Dedicated VPC with private subnets
|
||||
- [ ] Regular backup and retention policies
|
||||
- [ ] Audit trail for all infrastructure changes
|
||||
|
||||
### PCI-DSS Compliance
|
||||
|
||||
- [ ] Network segmentation (separate VPCs)
|
||||
- [ ] No default passwords
|
||||
- [ ] Strong encryption algorithms
|
||||
- [ ] Regular security scanning
|
||||
- [ ] Access control and monitoring
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Trivy Documentation](https://aquasecurity.github.io/trivy/)
|
||||
- [Checkov Documentation](https://www.checkov.io/)
|
||||
- [terraform-compliance](https://terraform-compliance.com/)
|
||||
- [Open Policy Agent](https://www.openpolicyagent.org/)
|
||||
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
|
||||
|
||||
---
|
||||
|
||||
**Back to:** [Main Skill File](../SKILL.md)
|
||||
@@ -0,0 +1,277 @@
|
||||
---
|
||||
name: terraform-skill
|
||||
description: Use when writing, reviewing, or debugging Terraform/OpenTofu modules, tests, CI, scans, or state ops — diagnoses failure mode (identity churn, secrets, blast radius, CI drift, state corruption) with version-aware guards.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: Anton Babenko
|
||||
version: 1.6.0
|
||||
---
|
||||
|
||||
# Terraform Skill for Claude
|
||||
|
||||
Diagnose-first guidance for Terraform and OpenTofu. Core file is a workflow; depth lives in references loaded on demand.
|
||||
|
||||
## Response Contract
|
||||
|
||||
Every Terraform/OpenTofu response must include:
|
||||
|
||||
1. **Assumptions & version floor** — runtime (`terraform` or `tofu`), exact version, providers, state backend, execution path (local/CI/Cloud/Atlantis), environment criticality. State assumptions explicitly if the user did not provide them.
|
||||
2. **Risk category addressed** — one or more of: identity churn, secret exposure, blast radius, CI drift, compliance gaps, state corruption, provider upgrade risk, testing blind spots.
|
||||
3. **Chosen remediation & tradeoffs** — what was chosen, what was traded off, why.
|
||||
4. **Validation plan** — exact commands (`fmt -check`, `validate`, `plan -out`, policy check) tailored to runtime and risk tier.
|
||||
5. **Rollback notes** — for any destructive or state-mutating change: how to undo, what evidence to keep.
|
||||
|
||||
Never recommend direct production apply without a reviewed plan artifact and approval.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Capture execution context** — runtime+version, provider(s), backend, execution path, environment criticality.
|
||||
2. **Diagnose failure mode(s)** using the routing table below. If intent spans categories, load both references.
|
||||
3. **Load only the matching reference file(s)** — do not preload depth the task does not need.
|
||||
4. **Propose fix with risk controls** — why this addresses the mode, what could still go wrong, guardrails (tests/approvals/rollback).
|
||||
5. **Generate artifacts** — HCL, migration blocks (`moved`, `import`), CI changes, policy rules.
|
||||
6. **Validate before finalizing** — run validation commands tailored to risk tier.
|
||||
7. **Emit the Response Contract** at the end.
|
||||
|
||||
## Diagnose Before You Generate
|
||||
|
||||
| Failure category | Symptoms | Primary references |
|
||||
|------------------|----------|--------------------|
|
||||
| **Identity churn** | Resource addresses shift after refactor, `count` index churn, missing `moved` blocks | [Code Patterns: count vs for_each](references/code-patterns.md#count-vs-for_each-deep-dive), [Code Patterns: moved blocks](references/code-patterns.md#moved-blocks-terraform-11), [Code Patterns: LLM mistakes](references/code-patterns.md#llm-mistake-checklist--code-patterns) |
|
||||
| **Secret exposure** | Secrets in defaults, state, logs, CI artifacts | [Security & Compliance](references/security-compliance.md), [Code Patterns: write-only](references/code-patterns.md#write-only-arguments-terraform-111), [State Management](references/state-management.md) |
|
||||
| **Blast radius** | Oversized stacks, shared prod/non-prod state, unsafe applies | [State Management](references/state-management.md), [Module Patterns](references/module-patterns.md) |
|
||||
| **CI drift** | Local plan ≠ CI plan, apply without reviewed artifact, unpinned versions | [CI/CD Workflows](references/ci-cd-workflows.md), [Code Patterns: versions](references/code-patterns.md#version-management) |
|
||||
| **Compliance gaps** | Missing policy stage, no approval model, no evidence retention | [Security & Compliance](references/security-compliance.md), [CI/CD Workflows](references/ci-cd-workflows.md) |
|
||||
| **Testing blind spots** | Plan-only validation of computed values, set-type indexing, mock/real confusion | [Testing Frameworks](references/testing-frameworks.md) |
|
||||
| **State corruption / recovery** | Stuck lock, backend migration, drift reconciliation | [State Management](references/state-management.md) |
|
||||
| **Provider upgrade risk** | Breaking-change provider bump, unpinned modules | [Code Patterns: versions](references/code-patterns.md#version-management), [Module Patterns](references/module-patterns.md) |
|
||||
| **Provider lifecycle** | Removing a provider with resources still in state, orphaned resources, `removed` block usage | [State Management: Provider Removal](references/state-management.md#provider-removal) |
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**Activate when:** creating or reviewing Terraform/OpenTofu configurations or modules, setting up or debugging tests, structuring multi-environment deployments, implementing IaC CI/CD, choosing module patterns or state organization, configuring or migrating remote state backends.
|
||||
|
||||
**Don't use for:** basic HCL syntax questions Claude already knows, provider API reference (link to docs), cloud-platform questions unrelated to Terraform/OpenTofu.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Module Hierarchy
|
||||
|
||||
| Type | When to Use | Scope |
|
||||
|------|-------------|-------|
|
||||
| **Resource module** | Single logical group of connected resources | VPC + subnets, SG + rules |
|
||||
| **Infrastructure module** | Collection of resource modules for a purpose | Multiple resource modules in one region/account |
|
||||
| **Composition** | Complete infrastructure | Spans multiple regions/accounts |
|
||||
|
||||
Flow: resource → resource module → infrastructure module → composition.
|
||||
|
||||
### Directory Layout
|
||||
|
||||
```
|
||||
environments/ # prod/ staging/ dev/ — per-env configurations
|
||||
modules/ # networking/ compute/ data/ — reusable modules
|
||||
examples/ # minimal/ complete/ — docs + integration fixtures
|
||||
```
|
||||
|
||||
Separate **environments** from **modules**. Use `examples/` as both documentation and test fixtures. Keep modules small and single-responsibility.
|
||||
|
||||
See [Module Patterns](references/module-patterns.md) for architecture principles, naming conventions, variable/output contracts.
|
||||
|
||||
### Naming Conventions (summary)
|
||||
|
||||
- Descriptive resource names (`aws_instance.web_server`, not `aws_instance.main`)
|
||||
- Reserve `this` for genuine singleton resources only
|
||||
- Prefix variables with context (`vpc_cidr_block`, not `cidr`)
|
||||
- Standard files: `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`
|
||||
|
||||
See [Module Patterns: Variable Naming](references/module-patterns.md) and [Code Patterns: Block Ordering](references/code-patterns.md#block-ordering--structure) for examples.
|
||||
|
||||
### Block Ordering (summary)
|
||||
|
||||
Resource blocks: `count`/`for_each` first → arguments → `tags` → `depends_on` → `lifecycle`.
|
||||
Variable blocks: `description` → `type` → `default` → `validation` → `nullable` → `sensitive`.
|
||||
|
||||
See [Code Patterns: Block Ordering & Structure](references/code-patterns.md#block-ordering--structure) for the full rules and examples.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Decision Matrix: Which Testing Approach?
|
||||
|
||||
| Situation | Approach | Tools | Cost |
|
||||
|-----------|----------|-------|------|
|
||||
| Quick syntax check | Static analysis | `validate`, `fmt` | Free |
|
||||
| Pre-commit validation | Static + lint | `validate`, `tflint`, `trivy`, `checkov` | Free |
|
||||
| Terraform 1.6+, simple logic | Native test framework | `terraform test` | Free-Low |
|
||||
| Pre-1.6, or Go expertise | Integration testing | Terratest | Low-Med |
|
||||
| Security/compliance focus | Policy as code | OPA, Sentinel | Free |
|
||||
| Cost-sensitive workflow | Mock providers (1.7+) | Native tests + mocks | Free |
|
||||
| Multi-cloud, complex | Full integration | Terratest + real infra | Med-High |
|
||||
|
||||
### Native Test Rules (1.6+)
|
||||
|
||||
Before writing test code: validate resource schemas via Terraform MCP so assertions target real attributes.
|
||||
|
||||
- `command = plan` — fast, for input-derived values only
|
||||
- `command = apply` — required for **computed values** (ARNs, generated names) and **set-type nested blocks**
|
||||
- Set-type blocks cannot be indexed with `[0]` — use `for` expressions or materialize via `command = apply`
|
||||
- Common set types: S3 encryption rules, lifecycle transitions, IAM policy statements
|
||||
|
||||
See [Testing Frameworks](references/testing-frameworks.md) for static-analysis pipelines, native-test patterns, Terratest integration, mock providers, and the full LLM-mistake checklist.
|
||||
|
||||
## Count vs For_Each — Quick Rule
|
||||
|
||||
| Scenario | Use | Why |
|
||||
|----------|-----|-----|
|
||||
| Boolean condition (create / don't) | `count = condition ? 1 : 0` | Optional singleton toggle |
|
||||
| Items may be reordered or removed | `for_each = toset(list)` | Stable resource addresses |
|
||||
| Reference by key | `for_each = map` | Named access |
|
||||
| Multiple named resources | `for_each` | Better identity stability |
|
||||
|
||||
**Never** use list index as long-lived identity — removing a middle element reshuffles every address after it. For the decision matrix, safe migration playbook, `moved` block patterns, and known-at-plan failure cases, see [Code Patterns: count vs for_each](references/code-patterns.md#count-vs-for_each-deep-dive).
|
||||
|
||||
## Locals for Dependency Management
|
||||
|
||||
Using `try()` in a local to prefer a conditional resource's attribute over its parent is a specialized but high-value pattern — it forces correct deletion order without explicit `depends_on`. Common use: VPC + secondary CIDR associations + subnets.
|
||||
|
||||
See [Code Patterns: Locals for Dependency Management](references/code-patterns.md#locals-for-dependency-management) for the full pattern and worked example.
|
||||
|
||||
## Module Development
|
||||
|
||||
Standard layout:
|
||||
|
||||
```
|
||||
my-module/
|
||||
├── README.md # Usage documentation
|
||||
├── main.tf # Primary resources
|
||||
├── variables.tf # Typed inputs with descriptions
|
||||
├── outputs.tf # Output values
|
||||
├── versions.tf # required_version + required_providers
|
||||
├── examples/
|
||||
│ ├── minimal/
|
||||
│ └── complete/
|
||||
└── tests/
|
||||
└── module_test.tftest.hcl # or Go for Terratest
|
||||
```
|
||||
|
||||
**Variable contracts**: always `description`, always explicit `type`, use `validation` for complex constraints, use `sensitive = true` for secrets, prefer `optional()` with typed defaults (1.3+) over untyped `map(any)`.
|
||||
|
||||
**Output contracts**: always `description`, mark sensitive outputs, expose stable subsets (not whole provider objects).
|
||||
|
||||
See [Module Patterns](references/module-patterns.md) for the full contract patterns, module release checklist, and LLM-mistake checklist.
|
||||
|
||||
## CI/CD
|
||||
|
||||
Pipeline stages: **validate** → **test** → **plan** → **apply** (with environment protection).
|
||||
|
||||
Cost control: mock providers on PR validation, real-cloud integration only on main or scheduled, tag test resources, auto-cleanup.
|
||||
|
||||
Drift prevention: pin runtime and providers, commit `.terraform.lock.hcl`, apply the **reviewed plan artifact** from the plan stage (do not re-run `plan` inside the apply job), run policy/security stage on every path to apply.
|
||||
|
||||
See [CI/CD Workflows](references/ci-cd-workflows.md) for GitHub Actions, GitLab CI, and Atlantis templates plus the LLM-mistake checklist.
|
||||
|
||||
## Security & Compliance
|
||||
|
||||
**Essential checks:**
|
||||
|
||||
```bash
|
||||
trivy config .
|
||||
checkov -d .
|
||||
```
|
||||
|
||||
**Don't:** store secrets in variables or `.tfvars`, use default VPC, skip encryption, open security groups to `0.0.0.0/0`, use inline `ingress`/`egress` blocks in `aws_security_group`.
|
||||
|
||||
**Do:** source secrets from AWS Secrets Manager / Parameter Store or use `write_only` arguments on 1.11+, create dedicated VPCs, enforce encryption at rest and TLS, least-privilege SGs, use separate `aws_vpc_security_group_{ingress,egress}_rule` resources (AWS provider v5+).
|
||||
|
||||
Marking a variable `sensitive = true` masks display only — the value still lives in state. Use `write_only` / `*_wo` on 1.11+, or keep secret material out of Terraform entirely via runtime lookups.
|
||||
|
||||
See [Security & Compliance](references/security-compliance.md) for trivy/checkov pipelines, state-file hardening, compliance mappings, and the LLM-mistake checklist.
|
||||
|
||||
## State Management
|
||||
|
||||
**Never use local state in teams or production.** Remote backends provide automatic locking, encryption, versioning, audit logging, and safe collaboration.
|
||||
|
||||
### Minimum Viable Backend (AWS S3, 1.10+)
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-terraform-state"
|
||||
key = "prod/vpc/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
use_lockfile = true # Native S3 locking, 1.10+
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On Terraform < 1.10, use `dynamodb_table = "terraform-state-lock"` instead of `use_lockfile`. Azure Storage, GCS, and Terraform Cloud all offer built-in locking — see the State Management reference for syntax.
|
||||
|
||||
### State Organization
|
||||
|
||||
| Pattern | Use When | Example Path |
|
||||
|---------|----------|--------------|
|
||||
| **Per environment** | Different teams per env | `prod/terraform.tfstate`, `staging/...` |
|
||||
| **Per component** | Independent lifecycles | `prod/vpc/`, `prod/eks/`, `prod/rds/` |
|
||||
| **Hybrid** (recommended) | Both benefits | `prod/networking/`, `prod/compute/`, `staging/networking/` |
|
||||
|
||||
Split state when: different teams, different update cadences, or >500 resources. Combine when: tightly coupled resources, <100 resources, same lifecycle.
|
||||
|
||||
See [State Management](references/state-management.md) for locking, migration, multi-team isolation, disaster recovery, and the LLM-mistake checklist.
|
||||
|
||||
## Version Management
|
||||
|
||||
| Component | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| Terraform runtime | Pin minor | `required_version = "~> 1.9"` |
|
||||
| Providers | Pin major | `version = "~> 5.0"` |
|
||||
| Modules (prod) | Pin exact | `version = "5.1.2"` |
|
||||
| Modules (dev) | Allow patch | `version = "~> 5.1"` |
|
||||
|
||||
Commit `.terraform.lock.hcl` intentionally. Keep provider/runtime upgrades in a separate PR from functional changes. See [Code Patterns: Version Management](references/code-patterns.md#version-management) for constraint syntax and upgrade workflow.
|
||||
|
||||
## Modern Terraform Features (1.0+)
|
||||
|
||||
| Feature | Min version | Common use |
|
||||
|---------|-------------|------------|
|
||||
| `try()` | 0.13+ | Safe fallbacks, replaces `element(concat())` |
|
||||
| `nullable = false` | 1.1+ | Prevent `null` silently overriding defaults |
|
||||
| `moved` blocks | 1.1+ | Refactor without destroy/recreate |
|
||||
| `optional()` with defaults | 1.3+ | Typed object attributes |
|
||||
| `import` blocks | 1.5+ | Declarative imports, reviewable in VCS |
|
||||
| `check` blocks | 1.5+ | Runtime assertions |
|
||||
| Native `terraform test` | 1.6+ | Built-in test framework |
|
||||
| Mock providers | 1.7+ | Cost-free unit testing |
|
||||
| `removed` blocks | 1.7+ | Declarative resource removal |
|
||||
| Provider-defined functions | 1.8+ | Provider-specific transformations (requires provider to declare functions) |
|
||||
| Cross-variable validation | 1.9+ | Reference other `var.*` in `validation` blocks |
|
||||
| `write_only` arguments | 1.11+ | Secrets never stored in state |
|
||||
| S3 native lock-file | 1.10+ | State locking without DynamoDB |
|
||||
|
||||
Before emitting a feature, verify the runtime floor. See [Code Patterns: Feature Guard Table](references/code-patterns.md#feature-guard-table--version-floor--common-llm-errors) for the full table with common LLM error patterns per feature.
|
||||
|
||||
## Runtime-Specific Guidance
|
||||
|
||||
- **Terraform 1.0-1.5 / OpenTofu 1.0-1.5**: Terratest for integration, static analysis + plan validation only (no native tests).
|
||||
- **1.6+**: native `terraform test` / `tofu test` available — migrate simple unit tests, keep Terratest for complex integration.
|
||||
- **1.7+**: mock providers cut test cost — mock for unit tests, real runs for final integration.
|
||||
- **1.10+**: S3 native lock-file (`use_lockfile`) is the correct default for new configurations — DynamoDB locking is no longer required.
|
||||
- **1.11+**: `write_only` arguments for secret handling keep credentials out of state.
|
||||
- **Terraform vs OpenTofu**: both supported. For licensing, governance, and feature delta, see [Quick Reference: Terraform vs OpenTofu](references/quick-reference.md#terraform-vs-opentofu-comparison).
|
||||
|
||||
## Reference Files
|
||||
|
||||
Progressive disclosure — essentials here, depth on demand:
|
||||
|
||||
- [Testing Frameworks](references/testing-frameworks.md) — static analysis, native tests, Terratest, mock providers
|
||||
- [Module Patterns](references/module-patterns.md) — structure, variable/output contracts, `terraform_remote_state` rules, release checklist
|
||||
- [CI/CD Workflows](references/ci-cd-workflows.md) — GitHub Actions, GitLab CI, Atlantis, cost control
|
||||
- [Security & Compliance](references/security-compliance.md) — trivy/checkov, secrets handling, compliance mappings
|
||||
- [State Management](references/state-management.md) — backends, locking, migration, multi-team, recovery
|
||||
- [Code Patterns](references/code-patterns.md) — block ordering, `count`/`for_each` deep dive, modern features, version management, locals
|
||||
- [Quick Reference](references/quick-reference.md) — command cheat sheets, flowcharts, troubleshooting
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0. See LICENSE for full terms.
|
||||
|
||||
**Copyright © 2026 Anton Babenko**
|
||||
+175
-50
@@ -25,14 +25,17 @@ This document provides detailed CI/CD workflow templates and optimization strate
|
||||
# .github/workflows/terraform.yml
|
||||
name: Terraform
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: hashicorp/setup-terraform@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Format
|
||||
run: terraform fmt -check -recursive
|
||||
@@ -43,26 +46,29 @@ jobs:
|
||||
- name: Terraform Validate
|
||||
run: terraform validate
|
||||
|
||||
- uses: terraform-linters/setup-tflint@v4
|
||||
with:
|
||||
tflint_version: v0.50.3
|
||||
- name: TFLint Init
|
||||
run: tflint --init
|
||||
- name: TFLint
|
||||
run: |
|
||||
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
|
||||
tflint --init
|
||||
tflint
|
||||
run: tflint
|
||||
|
||||
test:
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Run Terraform Tests
|
||||
run: terraform test
|
||||
|
||||
# Or for Terratest:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Run Terratest
|
||||
run: |
|
||||
@@ -73,8 +79,8 @@ jobs:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: hashicorp/setup-terraform@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Init
|
||||
run: terraform init
|
||||
@@ -83,7 +89,7 @@ jobs:
|
||||
run: terraform plan -out=tfplan
|
||||
|
||||
- name: Upload Plan
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tfplan
|
||||
path: tfplan
|
||||
@@ -94,14 +100,17 @@ jobs:
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: hashicorp/setup-terraform@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Download Plan
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tfplan
|
||||
|
||||
- name: Terraform Init
|
||||
run: terraform init
|
||||
|
||||
- name: Terraform Apply
|
||||
run: terraform apply tfplan
|
||||
```
|
||||
@@ -113,7 +122,7 @@ jobs:
|
||||
needs: plan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Infracost
|
||||
uses: infracost/actions/setup@v2
|
||||
@@ -166,9 +175,9 @@ test:
|
||||
stage: test
|
||||
script:
|
||||
- terraform test
|
||||
only:
|
||||
- merge_requests
|
||||
- main
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
|
||||
plan:
|
||||
extends: .terraform_template
|
||||
@@ -179,9 +188,9 @@ plan:
|
||||
paths:
|
||||
- ${TF_ROOT}/tfplan
|
||||
expire_in: 1 week
|
||||
only:
|
||||
- merge_requests
|
||||
- main
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
|
||||
apply:
|
||||
extends: .terraform_template
|
||||
@@ -190,9 +199,9 @@ apply:
|
||||
- terraform apply tfplan
|
||||
dependencies:
|
||||
- plan
|
||||
only:
|
||||
- main
|
||||
when: manual
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
when: manual
|
||||
environment:
|
||||
name: production
|
||||
```
|
||||
@@ -237,7 +246,7 @@ terraformOptions := &terraform.Options{
|
||||
Vars: map[string]interface{}{
|
||||
"tags": map[string]string{
|
||||
"Environment": "test",
|
||||
"TTL": "2h",
|
||||
"CreatedAt": time.Now().Format(time.RFC3339),
|
||||
"CreatedBy": "CI",
|
||||
"JobID": os.Getenv("GITHUB_RUN_ID"),
|
||||
},
|
||||
@@ -254,17 +263,29 @@ terraformOptions := &terraform.Options{
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# cleanup-test-resources.sh
|
||||
# Resources are tagged with CreatedAt = ISO8601 timestamp (RFC3339).
|
||||
# AWS resourcegroupstaggingapi tag filters only support equality, so we
|
||||
# fetch by Environment=test and filter by timestamp client-side with jq.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CUTOFF=$(date -u -d '2 hours ago' +%s)
|
||||
|
||||
# Find and terminate instances older than 2 hours with test tag
|
||||
aws resourcegroupstaggingapi get-resources \
|
||||
--tag-filters Key=Environment,Values=test \
|
||||
--query 'ResourceTagMappingList[?Tags[?Key==`TTL` && Value<`'$(date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%S)'`]].ResourceARN' \
|
||||
--output text | \
|
||||
while read arn; do
|
||||
instance_id=$(echo $arn | grep -oP 'instance/\K[^/]+')
|
||||
if [ ! -z "$instance_id" ]; then
|
||||
--query 'ResourceTagMappingList[]' \
|
||||
--output json | \
|
||||
jq -r --argjson cutoff "$CUTOFF" '
|
||||
.[]
|
||||
| select(
|
||||
any(.Tags[]; .Key == "CreatedAt" and (.Value | fromdateiso8601) < $cutoff)
|
||||
)
|
||||
| .ResourceARN
|
||||
' | while read -r arn; do
|
||||
instance_id=$(echo "$arn" | grep -oP 'instance/\K[^/]+' || true)
|
||||
if [ -n "$instance_id" ]; then
|
||||
echo "Terminating instance: $instance_id"
|
||||
aws ec2 terminate-instances --instance-ids $instance_id
|
||||
aws ec2 terminate-instances --instance-ids "$instance_id"
|
||||
fi
|
||||
done
|
||||
```
|
||||
@@ -284,10 +305,10 @@ jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -331,11 +352,11 @@ jobs:
|
||||
### 2. Require Approvals for Production
|
||||
|
||||
```yaml
|
||||
# GitHub Actions — configure required reviewers on the `production`
|
||||
# environment in repo Settings -> Environments -> Protection rules.
|
||||
apply:
|
||||
environment:
|
||||
name: production
|
||||
# Requires manual approval in GitHub
|
||||
when: manual
|
||||
```
|
||||
|
||||
### 3. Use Remote State
|
||||
@@ -364,13 +385,28 @@ terraform {
|
||||
### 5. Cache Terraform Plugins
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Cache Terraform Plugins
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.terraform.d/plugin-cache
|
||||
key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
|
||||
# GitHub Actions — set TF_PLUGIN_CACHE_DIR so `terraform init` actually
|
||||
# writes into the cached path, then restore the cache between runs.
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TF_PLUGIN_CACHE_DIR: ${{ runner.temp }}/terraform-plugin-cache
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Create plugin cache dir
|
||||
run: mkdir -p "$TF_PLUGIN_CACHE_DIR"
|
||||
|
||||
- name: Cache Terraform Plugins
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/terraform-plugin-cache
|
||||
key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
|
||||
|
||||
- name: Terraform Init
|
||||
run: terraform init
|
||||
```
|
||||
|
||||
### 6. Security Scanning in CI
|
||||
@@ -379,21 +415,92 @@ terraform {
|
||||
security-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy
|
||||
uses: aquasecurity/trivy-action@master
|
||||
uses: aquasecurity/trivy-action@0.29.0
|
||||
with:
|
||||
scan-type: 'config'
|
||||
scan-ref: '.'
|
||||
|
||||
- name: Run Checkov
|
||||
uses: bridgecrewio/checkov-action@master
|
||||
uses: bridgecrewio/checkov-action@v12.2.0
|
||||
with:
|
||||
directory: .
|
||||
framework: terraform
|
||||
```
|
||||
|
||||
### OIDC Trust Policy Correctness
|
||||
|
||||
| Platform | Expected `aud` | Where to pin `sub` |
|
||||
|----------|----------------|---------------------|
|
||||
| GitHub Actions → AWS | `sts.amazonaws.com` | `repo:<org>/<repo>:ref:refs/heads/<branch>` |
|
||||
| GitHub Actions → Azure AD | `api://AzureADTokenExchange` | `repo:<org>/<repo>:environment:<env>` |
|
||||
| GitHub Actions → GCP | value passed via `audience` parameter | repo + ref or environment |
|
||||
| GitLab CI → AWS | matches `$CI_SERVER_URL` | project path + ref |
|
||||
|
||||
**Rules:**
|
||||
- ✅ pin `aud` to the exact value from the table
|
||||
- ✅ pin `sub` to a specific repo + branch or environment — no wildcards across org/repo
|
||||
- ❌ `sub` wildcards like `repo:*:*` or `repo:<org>/*:ref:*` let any repo assume the role
|
||||
- ❌ mismatched `aud` → token rejected with opaque error; fix `aud` per table, do not relax `sub`
|
||||
|
||||
✅ DO — AWS IAM trust-policy `Condition` block (the only non-boilerplate fragment):
|
||||
|
||||
```json
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
|
||||
},
|
||||
"StringLike": {
|
||||
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Drift Detection — Alert, Do Not Auto-Apply
|
||||
|
||||
Scheduled drift detection alerts; it never auto-applies.
|
||||
|
||||
✅ DO — scheduled plan with alert on drift (exit code 2):
|
||||
|
||||
```yaml
|
||||
# .github/workflows/drift-detection.yml
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *'
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
- run: terraform init
|
||||
- name: Plan (detect drift)
|
||||
id: plan
|
||||
run: terraform plan -detailed-exitcode -out=plan.bin
|
||||
continue-on-error: true
|
||||
- name: Alert on drift
|
||||
if: steps.plan.outcome == 'failure' && steps.plan.outputs.exitcode == '2'
|
||||
run: |
|
||||
echo "Drift detected. Requires human review before apply."
|
||||
# send to Slack / PagerDuty / issue tracker
|
||||
```
|
||||
|
||||
`plan -detailed-exitcode` exit codes: `0` = no drift, `1` = plan failed, `2` = drift detected.
|
||||
|
||||
❌ DON'T — scheduled auto-apply that silently reconciles drift:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
reconcile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: terraform apply -auto-approve
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Atlantis Integration
|
||||
@@ -408,7 +515,7 @@ projects:
|
||||
- name: production
|
||||
dir: environments/prod
|
||||
workspace: default
|
||||
terraform_version: v1.6.0
|
||||
terraform_version: 1.12.0
|
||||
workflow: custom
|
||||
|
||||
workflows:
|
||||
@@ -417,7 +524,7 @@ workflows:
|
||||
steps:
|
||||
- init
|
||||
- plan:
|
||||
extra_args: ["-lock", "false"]
|
||||
extra_args: ["-lock=false"]
|
||||
apply:
|
||||
steps:
|
||||
- apply
|
||||
@@ -470,4 +577,22 @@ bucketName := fmt.Sprintf("test-bucket-%s-%s",
|
||||
|
||||
---
|
||||
|
||||
## LLM Mistake Checklist — CI/CD
|
||||
|
||||
Common model mistakes to correct before returning pipeline recommendations:
|
||||
|
||||
- generates a pipeline with no lockfile strategy (`.terraform.lock.hcl` uncommitted or unreviewed)
|
||||
- re-runs `terraform plan` inside the apply job instead of consuming the reviewed plan artifact from the plan stage
|
||||
- omits environment protection / approval gates on production apply
|
||||
- uses unpinned provider versions, causing drift between local and CI runs
|
||||
- skips the policy/security stage despite the pipeline claiming compliance
|
||||
- grants CI long-lived static cloud credentials instead of OIDC / workload-identity federation
|
||||
- writes OIDC trust policies with wildcard `sub` claims (`repo:*:*`, `repo:<org>/*:ref:*`) — any repo or branch can assume the role
|
||||
- mismatches the `aud` claim between CI platform and cloud provider, then relaxes `sub` to "fix" the resulting error
|
||||
- implements scheduled "drift detection" as `terraform apply -auto-approve` on cron — silently reverts out-of-band changes; use `plan -detailed-exitcode` + alert
|
||||
- fails to restrict artifact access when `terraform show -json` results may contain sensitive plan output
|
||||
- merges provider/runtime upgrades with functional changes in the same PR
|
||||
|
||||
---
|
||||
|
||||
**Back to:** [Main Skill File](../SKILL.md)
|
||||
@@ -237,11 +237,9 @@ variable "environments" {
|
||||
default = {
|
||||
dev = {
|
||||
instance_type = "t3.micro"
|
||||
instance_count = 1
|
||||
}
|
||||
prod = {
|
||||
instance_type = "t3.large"
|
||||
instance_count = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +248,6 @@ resource "aws_instance" "app" {
|
||||
for_each = var.environments
|
||||
|
||||
instance_type = each.value.instance_type
|
||||
count = each.value.instance_count
|
||||
|
||||
tags = {
|
||||
Environment = each.key # "dev" or "prod"
|
||||
@@ -325,16 +322,81 @@ moved {
|
||||
# terraform plan should show "moved" operations, not destroy/create
|
||||
```
|
||||
|
||||
**Benefits after migration:**
|
||||
- Removing "us-east-1b" only destroys that subnet (not c)
|
||||
- Adding new AZ doesn't affect existing subnets
|
||||
- Resources have stable addresses by AZ name
|
||||
After migration: removing `us-east-1b` destroys only that subnet; adding an AZ does not churn existing resources; addresses are stable by AZ name.
|
||||
|
||||
### `for_each` keys must be known at plan time
|
||||
|
||||
`for_each` (0.12+) requires its key set resolvable during plan.
|
||||
|
||||
| Case | Use | Why |
|
||||
|------|-----|-----|
|
||||
| stable key set known at plan | `for_each` over static map/var | avoids count index churn on insert/remove |
|
||||
| key set unknowable at plan | `count = bool ? 1 : 0` for singleton | keys derived from values unknown until apply |
|
||||
|
||||
- ❌ `depends_on` does NOT fix `Invalid for_each argument` — it orders applies, not plan-time value resolution
|
||||
- ❌ deriving `for_each` keys from another resource's computed attrs (IDs, ARNs)
|
||||
- ✅ drive `for_each` from user-supplied variables or static locals
|
||||
|
||||
```hcl
|
||||
# ❌ BAD - keys derived from computed IDs; plan fails
|
||||
resource "aws_eip" "web" {
|
||||
for_each = toset([for i in aws_instance.web : i.id])
|
||||
instance = each.key
|
||||
}
|
||||
|
||||
# ✅ GOOD - drive for_each from user-supplied keys
|
||||
variable "instances" {
|
||||
type = map(object({ instance_type = string }))
|
||||
}
|
||||
|
||||
resource "aws_instance" "web" {
|
||||
for_each = var.instances
|
||||
ami = "ami-0123"
|
||||
instance_type = each.value.instance_type
|
||||
}
|
||||
|
||||
resource "aws_eip" "web" {
|
||||
for_each = var.instances
|
||||
instance = aws_instance.web[each.key].id
|
||||
}
|
||||
|
||||
# ✅ GOOD - singleton when exact ID not known at plan
|
||||
resource "aws_eip" "bastion" {
|
||||
count = var.create_bastion ? 1 : 0
|
||||
instance = aws_instance.bastion[0].id
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modern Terraform Features (1.0+)
|
||||
|
||||
### try() Function (Terraform 0.13+)
|
||||
### Feature Guard Table — Version Floor & Common LLM Errors
|
||||
|
||||
Before emitting a feature, verify the runtime floor. Each feature here is also a known hallucination surface — the error pattern column names the mistake to avoid.
|
||||
|
||||
| Feature | Min version | Common LLM error pattern |
|
||||
|---------|-------------|--------------------------|
|
||||
| `for_each` over `count` for stable identities | 0.12+ | defaults to `count` for every collection, causing index churn |
|
||||
| `try()` function | 0.12.20+ | falls back to `element(concat())` legacy pattern |
|
||||
| `nonsensitive()` function | 0.15+ | used to 'unwrap' sensitive outputs into plan artifacts, effectively laundering secrets into logs |
|
||||
| `nullable = false` | 1.1+ | omits it, letting `null` silently override defaults |
|
||||
| `moved` blocks | 1.1+ | omitted during refactor, causing destroy/create |
|
||||
| `optional()` with defaults | 1.3+ | emits wrapper variables and loose `map(any)` contracts |
|
||||
| declarative `import` blocks | 1.5+ | recommends ad-hoc CLI `terraform import` only |
|
||||
| `check` blocks | 1.5+ | ignores runtime assertions entirely |
|
||||
| native `terraform test` | 1.6+ | treats mocked-provider tests as full integration coverage |
|
||||
| mock providers | 1.7+ | asserts computed values in `command = plan` mode |
|
||||
| `removed` blocks | 1.7+ | deletes resources with no lifecycle transition |
|
||||
| provider-defined functions | 1.8+ | overuses data sources for simple transformations |
|
||||
| cross-variable validation | 1.9+ | pushes checks into postconditions only |
|
||||
| S3 native lock-file | 1.10+ | recommends DynamoDB lock table even on 1.10+ |
|
||||
| `ephemeral` values | 1.10+ | treats as interchangeable with `sensitive`; ephemeral values are scrubbed from state, `sensitive` only masks display |
|
||||
| `write_only` arguments | 1.11+ | uses `sensitive = true` and assumes state is safe |
|
||||
|
||||
If target runtime is below a feature floor, emit the pre-floor fallback explicitly instead of silently downgrading.
|
||||
|
||||
### try() Function (Terraform 0.12.20+)
|
||||
|
||||
**Use try() instead of element(concat()):**
|
||||
|
||||
@@ -356,7 +418,7 @@ output "first_subnet_id" {
|
||||
|
||||
# ❌ BAD - Legacy pattern
|
||||
output "security_group_id" {
|
||||
value = element(concat(aws_security_group.this.*.id, [""]), 0)
|
||||
value = element(concat(aws_security_group.this[*].id, [""]), 0)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -403,7 +465,7 @@ database_config = {
|
||||
|
||||
### Moved Blocks (Terraform 1.1+)
|
||||
|
||||
**Rename resources without destroy/recreate:**
|
||||
**Rename resources without destroy/recreate.** Omitting `moved` during a refactor is one of the most common LLM mistakes — the model renames the address and silently turns the rename into destroy/create. Always emit `moved` in the same change as the rename, then verify `terraform plan` shows a move operation, not replacement.
|
||||
|
||||
```hcl
|
||||
# Rename a resource
|
||||
@@ -425,17 +487,47 @@ moved {
|
||||
}
|
||||
```
|
||||
|
||||
**Limits of `moved` (1.1+):**
|
||||
|
||||
| Limit | Can `moved` cross this? | Alternative |
|
||||
|-------|-------------------------|-------------|
|
||||
| Provider boundary | No | use `removed` (1.7+) + `import` (1.5+) |
|
||||
| State file / backend key | No | `state mv` across backends + pre-migration backup |
|
||||
| Module removal (module deleted from config) | `moved` block inside removed module silently stops working | add `moved` in the **parent**, not the removed module |
|
||||
|
||||
### ignore_changes (Lifecycle Escape Hatch)
|
||||
|
||||
- ✅ attribute-level `ignore_changes = [tags["X"]]` with a comment naming the external system
|
||||
- ❌ `ignore_changes = all` — hides real drift, turns every attribute unmanaged
|
||||
- ❌ use `ignore_changes` to silence noisy plans instead of diagnosing root cause
|
||||
|
||||
```hcl
|
||||
# ❌ BAD - blanket ignore hides all drift
|
||||
resource "aws_db_instance" "this" {
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
}
|
||||
|
||||
# ✅ GOOD - narrow ignore with justification
|
||||
resource "aws_db_instance" "this" {
|
||||
lifecycle {
|
||||
# External compliance scanner rewrites this tag hourly
|
||||
ignore_changes = [tags["LastScanned"]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provider-Defined Functions (Terraform 1.8+)
|
||||
|
||||
**Use provider-specific functions for data transformation:**
|
||||
|
||||
```hcl
|
||||
# AWS provider function example
|
||||
data "aws_region" "current" {}
|
||||
|
||||
locals {
|
||||
# Provider function (Terraform 1.8+)
|
||||
bucket_name = provider::aws::arn_build("s3", "my-bucket", data.aws_region.current.name)
|
||||
# provider::aws::arn_build(partition, service, region, account_id, resource)
|
||||
# S3 ARNs are global: region and account_id are empty strings.
|
||||
bucket_arn = provider::aws::arn_build("aws", "s3", "", "", "my-bucket")
|
||||
}
|
||||
|
||||
# Check provider documentation for available functions
|
||||
@@ -485,9 +577,20 @@ variable "backup_retention" {
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Mechanism Timing
|
||||
|
||||
Four mechanisms look similar and are routinely confused. Only three actually gate apply.
|
||||
|
||||
| Mechanism | When it runs | Can reference | Blocks apply? |
|
||||
|-----------|--------------|---------------|---------------|
|
||||
| `validation` (in `variable`) | var evaluation, before plan | the variable's own value; other vars on 1.9+ | yes |
|
||||
| `precondition` (in `lifecycle`) | before resource create/update | other resources, data sources, vars | yes |
|
||||
| `postcondition` (in `lifecycle`) | after apply | the resource's own computed attrs | yes |
|
||||
| `check` block (1.5+) | every plan + apply | anything | **NO — advisory only, warnings not errors** |
|
||||
|
||||
### Write-Only Arguments (Terraform 1.11+)
|
||||
|
||||
**Always use write-only arguments or external secret management:**
|
||||
**Always use write-only arguments or external secret management.** A common LLM mistake is to mark a variable `sensitive = true` and assume the value is kept out of state — it is not. `sensitive` only masks display; write-only arguments (or external secret lookups at runtime) are what actually keep material out of state. Verify on 1.11+: prefer `*_wo` arguments for credentials; on older runtimes, source secrets from a secret manager and never store them in variables or tfvars.
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - External secret with write-only argument
|
||||
@@ -504,7 +607,10 @@ resource "aws_db_instance" "this" {
|
||||
instance_class = "db.t3.micro"
|
||||
username = "admin"
|
||||
|
||||
# write-only: Terraform sends to AWS then forgets it (not in state)
|
||||
# password_wo keeps the resource argument out of state (1.11+),
|
||||
# but the data source still reads secret_string into state on refresh.
|
||||
# For true state exclusion: use ephemeral (1.10+), manage_master_user_password,
|
||||
# or inject via CI env var outside Terraform.
|
||||
password_wo = data.aws_secretsmanager_secret_version.db_password.secret_string
|
||||
}
|
||||
|
||||
@@ -523,6 +629,57 @@ resource "aws_db_instance" "this" {
|
||||
}
|
||||
```
|
||||
|
||||
### nonsensitive() and ephemeral (Terraform 0.15+ / 1.10+)
|
||||
|
||||
| Goal | Use | Tradeoff |
|
||||
|------|-----|----------|
|
||||
| derived non-secret incorrectly inferred as sensitive | `nonsensitive()` (0.15+) | only safe when provably not secret; value enters plan |
|
||||
| short-lived credential that must never persist | `ephemeral` (1.10+) | never in state or plan; provider/resource must support it |
|
||||
| value must persist but not display | `sensitive = true` | still in state; masks terminal only |
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - ephemeral keeps short-lived creds out of state (1.10+)
|
||||
# requires random provider >= 3.7.0
|
||||
ephemeral "random_password" "session" {
|
||||
length = 32
|
||||
}
|
||||
|
||||
# ❌ BAD - unwrapping a real secret to silence a warning
|
||||
output "db_endpoint" {
|
||||
value = nonsensitive(aws_db_instance.this.password)
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Blocks — Iterator Shadowing + Set Ordering
|
||||
|
||||
| Gotcha | Cause | Fix |
|
||||
|--------|-------|-----|
|
||||
| outer `each.*` inside nested `dynamic` | block-name iterator shadows `each` | `iterator = rule` rename |
|
||||
| non-deterministic block order | `for_each = toset([...])` on a map/object | use map keyed by stable field |
|
||||
|
||||
- ❌ bare `dynamic "ingress"` inside outer `for_each` — `ingress.value` shadows `each.value`
|
||||
- ✅ rename inner iterator with `iterator = rule`; reference outer via `each.*`
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - explicit iterator rename removes ambiguity
|
||||
resource "aws_security_group" "this" {
|
||||
for_each = var.security_groups
|
||||
|
||||
name = each.key
|
||||
|
||||
dynamic "ingress" {
|
||||
for_each = each.value.rules
|
||||
iterator = rule
|
||||
content {
|
||||
from_port = rule.value.from_port
|
||||
to_port = rule.value.to_port
|
||||
protocol = rule.value.protocol
|
||||
description = each.value.description # outer iterator clear
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version Management
|
||||
@@ -534,9 +691,9 @@ resource "aws_db_instance" "this" {
|
||||
version = "5.0.0"
|
||||
|
||||
# Pessimistic constraint (recommended for stability)
|
||||
# Allows patch updates only
|
||||
version = "~> 5.0" # Allows 5.0.x (any x), but not 5.1.0
|
||||
version = "~> 5.0.1" # Allows 5.0.x where x >= 1, but not 5.1.0
|
||||
# The rightmost component is the one that's allowed to increment.
|
||||
version = "~> 5.0" # 5.x: >= 5.0, < 6.0 — allows 5.1, 5.2, 5.99
|
||||
version = "~> 5.0.1" # 5.0.x patches only: >= 5.0.1, < 5.1.0
|
||||
|
||||
# Range constraints
|
||||
version = ">= 5.0, < 6.0" # Any 5.x version
|
||||
@@ -692,7 +849,7 @@ terraform {
|
||||
```hcl
|
||||
# Before (0.12 style)
|
||||
output "security_group_id" {
|
||||
value = element(concat(aws_security_group.this.*.id, [""]), 0)
|
||||
value = element(concat(aws_security_group.this[*].id, [""]), 0)
|
||||
}
|
||||
|
||||
variable "config" {
|
||||
@@ -720,87 +877,55 @@ variable "config" {
|
||||
|
||||
### Secrets Remediation
|
||||
|
||||
**Pattern:** Move secrets out of Terraform state into external secret management.
|
||||
Move secret material out of state into external secret management. Canonical depth lives in [security-compliance.md](security-compliance.md) — patterns below are the minimum refactor shape.
|
||||
|
||||
#### Before - Secrets in State
|
||||
❌ BAD — both shapes land the secret in state:
|
||||
|
||||
```hcl
|
||||
# ❌ BAD - Secret generated and stored in state
|
||||
# random_password.result lives in state
|
||||
resource "random_password" "db" {
|
||||
length = 16
|
||||
special = true
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
engine = "mysql"
|
||||
username = "admin"
|
||||
password = random_password.db.result # In state!
|
||||
password = random_password.db.result
|
||||
}
|
||||
|
||||
# OR
|
||||
|
||||
# ❌ BAD - Secret passed via variable and stored in state
|
||||
# var + sensitive = true still writes to state (sensitive only masks display)
|
||||
variable "db_password" {
|
||||
description = "Database password"
|
||||
type = string
|
||||
sensitive = true # Marked sensitive but still in state!
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
password = var.db_password # In state!
|
||||
password = var.db_password
|
||||
}
|
||||
```
|
||||
|
||||
#### After - External Secret Management
|
||||
|
||||
**Option 1: Write-only arguments (Terraform 1.11+)**
|
||||
✅ GOOD — 1.11+ write-only argument, secret created outside Terraform:
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - Fetch from AWS Secrets Manager
|
||||
data "aws_secretsmanager_secret" "db_password" {
|
||||
name = "prod-database-password"
|
||||
}
|
||||
|
||||
data "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = data.aws_secretsmanager_secret.db_password.id
|
||||
secret_id = "prod-database-password"
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
engine = "mysql"
|
||||
username = "admin"
|
||||
|
||||
# write-only: Sent to AWS, not stored in state
|
||||
# password_wo: resource argument stays out of state (1.11+).
|
||||
# Data source still reads secret_string into state on refresh.
|
||||
# For true state exclusion: ephemeral (1.10+), manage_master_user_password, or CI env var.
|
||||
password_wo = data.aws_secretsmanager_secret_version.db_password.secret_string
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Separate secret creation (if Terraform 1.11+ not available)**
|
||||
|
||||
```hcl
|
||||
# ✅ GOOD - Reference pre-existing secret
|
||||
# Secret created outside Terraform (manually or separate process)
|
||||
|
||||
data "aws_secretsmanager_secret" "db_password" {
|
||||
name = "prod-database-password"
|
||||
}
|
||||
|
||||
data "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = data.aws_secretsmanager_secret.db_password.id
|
||||
}
|
||||
|
||||
# Note: Without write-only, you may need to handle secret rotation
|
||||
# outside Terraform or accept that the secret value appears in state
|
||||
# during initial creation but not after rotation
|
||||
```
|
||||
Pre-1.11 fallback: use the same data source without `password_wo`; rotation must happen outside Terraform.
|
||||
|
||||
**Migration steps:**
|
||||
|
||||
1. Create secret in AWS Secrets Manager (outside Terraform)
|
||||
2. Update Terraform to use data sources
|
||||
3. Use write-only argument (if Terraform 1.11+)
|
||||
4. Remove `random_password` resource or variable
|
||||
5. Run `terraform apply` to update
|
||||
6. Verify secret not in state: `terraform show` should not display password
|
||||
1. Create secret in AWS Secrets Manager outside Terraform
|
||||
2. Replace `random_password` / variable with `data "aws_secretsmanager_secret_version"`
|
||||
3. On 1.11+: use `password_wo`
|
||||
4. Apply, then `terraform show | grep -i password` — must be empty
|
||||
|
||||
---
|
||||
|
||||
@@ -844,15 +969,39 @@ resource "aws_subnet" "public" {
|
||||
# With local: Subnets deleted first, then CIDR association, then VPC ✓
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Prevents deletion errors when destroying infrastructure
|
||||
- Ensures correct dependency order without explicit `depends_on`
|
||||
- Particularly useful for complex VPC configurations with secondary CIDR blocks
|
||||
|
||||
**Common use cases:**
|
||||
- VPC with secondary CIDR blocks
|
||||
- Resources that depend on optional configurations
|
||||
- Complex deletion order requirements
|
||||
- Resources depending on optional configurations
|
||||
- Complex deletion-order requirements
|
||||
|
||||
---
|
||||
|
||||
## LLM Mistake Checklist — Code Patterns
|
||||
|
||||
Common model mistakes when generating HCL. Correct these before returning code:
|
||||
|
||||
- defaults to `count` for every collection — prefer `for_each` with stable keys whenever identity matters
|
||||
- omits `moved` blocks during rename/refactor, silently turning the change into destroy/create
|
||||
- builds `for_each` keys from computed IDs not known until apply — planning will fail
|
||||
- uses list index as long-lived identity (`count.index`) instead of business-meaningful keys
|
||||
- marks variables `sensitive = true` and assumes the value stays out of state — on 1.11+ use `write_only` / `*_wo` arguments
|
||||
- falls back to `element(concat(...))` instead of `try()` on 0.12.20+
|
||||
- accepts untyped `map(any)` / `any` for long-lived module contracts instead of `optional()` with typed defaults (1.3+)
|
||||
- suggests `terraform state mv` where `moved` blocks are safer and reviewable
|
||||
- recommends ad-hoc CLI `terraform import` instead of declarative `import` blocks (1.5+)
|
||||
- emits an exact `version = "5.0.0"` pin where `~> 5.0` would be more maintainable
|
||||
- silently emits 1.11+ features (S3 native lock, `write_only`, `removed`) without checking the runtime floor
|
||||
- uses `nonsensitive()` to "fix" a sensitive value appearing in plan output — this leaks secrets into CI artifacts
|
||||
- conflates `sensitive = true` with `ephemeral` (1.10+); only `ephemeral` actually stays out of state
|
||||
- writes a `moved` block expecting it to cross provider boundaries; it cannot
|
||||
- leaves `moved` blocks inside a module that itself is being removed — the moves silently no-op, resources get destroyed
|
||||
- emits CLI `terraform import` in automation when declarative `import` blocks (1.5+) give a reviewable, VCS-tracked alternative
|
||||
- emits `ignore_changes = all` or broad ignore lists to silence plan output instead of diagnosing drift root cause
|
||||
- uses `check` block expecting it to block apply; `check` is advisory, emits warnings only. Use `precondition`/`postcondition` to gate.
|
||||
- uses `each.value` inside a `dynamic` block intending the outer iterator — shadowed by the inner block name; rename with `iterator = ...`
|
||||
- emits hardcoded cloud IDs/ARNs (`vpc-0abc...`, pattern-matched `arn:aws:iam::` patterns) from training data instead of using data sources or input variables
|
||||
- pairs `password_wo` with `aws_secretsmanager_secret_version` — the data source still reads `secret_string` into state on refresh. Use `ephemeral` (1.10+) or CI-injected env var.
|
||||
- iterates `dynamic` blocks over `toset(...)` of maps/objects — the set's undefined ordering causes non-deterministic block ordering in the plan diff; sort the list or use a map keyed by a stable field
|
||||
|
||||
---
|
||||
|
||||
+143
-500
@@ -24,8 +24,6 @@ This document provides detailed guidance on creating reusable, maintainable Terr
|
||||
|
||||
### Module Type Classification
|
||||
|
||||
Terraform modules can be organized into three distinct types, each serving a specific purpose:
|
||||
|
||||
| Type | When to Use | Scope | Example |
|
||||
|------|-------------|-------|---------|
|
||||
| **Resource Module** | Single logical group of connected resources | Tightly coupled resources that always work together | VPC + subnets, Security group + rules, IAM role + policies |
|
||||
@@ -158,11 +156,7 @@ data.tf # Optional: Data sources (if main.tf gets large)
|
||||
backend.tf # ONLY at composition level (remote state config)
|
||||
```
|
||||
|
||||
**Why separate files?**
|
||||
- **Consistency:** Same structure across all modules
|
||||
- **Discoverability:** Know where to find specific types of configuration
|
||||
- **Maintainability:** Easier to navigate and modify
|
||||
- **Terraform Registry:** Required structure for publishing
|
||||
Required structure for Terraform Registry publishing; keeps navigation consistent across modules.
|
||||
|
||||
---
|
||||
|
||||
@@ -170,11 +164,7 @@ backend.tf # ONLY at composition level (remote state config)
|
||||
|
||||
### 1. Smaller Scopes = Better Performance + Reduced Blast Radius
|
||||
|
||||
**Benefits:**
|
||||
- Faster `terraform plan` and `terraform apply` operations
|
||||
- Isolated failures don't affect unrelated infrastructure
|
||||
- Easier to reason about changes
|
||||
- Parallel development by multiple teams
|
||||
Faster `plan`/`apply`, isolated failures, parallel team development.
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -196,43 +186,42 @@ environments/prod/
|
||||
|
||||
### 2. Always Use Remote State
|
||||
|
||||
**Why:**
|
||||
- **Prevents race conditions** with multiple developers
|
||||
- **Provides disaster recovery** (state versioning)
|
||||
- **Enables team collaboration** (shared access)
|
||||
- **Supports state locking** (prevents concurrent modifications)
|
||||
- ❌ local `terraform.tfstate` — no locking, no backup, no team access
|
||||
- ✅ remote backend — locking, versioning, encryption, audit log
|
||||
|
||||
**Never:**
|
||||
```hcl
|
||||
# ❌ BAD - Local state (default)
|
||||
# State stored in local terraform.tfstate file
|
||||
# Lost if computer crashes
|
||||
# Can't share with team
|
||||
```
|
||||
|
||||
**Always:**
|
||||
```hcl
|
||||
# ✅ GOOD - Remote state
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-terraform-state"
|
||||
key = "prod/networking/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
dynamodb_table = "terraform-locks" # State locking
|
||||
encrypt = true # Encryption at rest
|
||||
bucket = "my-terraform-state"
|
||||
key = "prod/networking/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
use_lockfile = true # Terraform 1.10+; native S3 locking
|
||||
# Pre-1.10 runtime: use dynamodb_table = "terraform-locks" instead
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use terraform_remote_state as Glue
|
||||
### 3. Use terraform_remote_state Sparingly — Only at True Ownership Boundaries
|
||||
|
||||
**Pattern:** Connect compositions via remote state data sources
|
||||
**Pattern:** Connect separately-owned compositions via remote state data sources. Reserve it for genuine team/lifecycle boundaries, not as convenient glue inside a single-team stack.
|
||||
|
||||
**Why:**
|
||||
- Loose coupling between infrastructure components
|
||||
- Teams can work independently
|
||||
- Changes to one stack don't require rebuilding others
|
||||
- Outputs from one stack become inputs to another
|
||||
**Use it when ALL of these are true:**
|
||||
- Consumer and producer are owned by **different teams** or have **different release cadences**
|
||||
- The producer's state is already split for lifecycle reasons (networking vs. compute vs. data)
|
||||
- You cannot reasonably pass the same values as module inputs
|
||||
|
||||
**Do NOT use it when:**
|
||||
- You control both stacks and can wire via module outputs
|
||||
- You're reading values that would be better served by a cloud data source (e.g., `aws_vpc` by tag)
|
||||
- You're reaching across >2 remote states in one composition — that is a signal to reshape boundaries, not add more wiring
|
||||
|
||||
**Common LLM mistakes:**
|
||||
- reaches for `terraform_remote_state` as default integration pattern
|
||||
- chains many `terraform_remote_state` reads, creating hidden cross-stack coupling
|
||||
- reads values that can drift at the provider level (use cloud data sources instead)
|
||||
|
||||
At real boundaries, outputs from one stack become typed inputs to another — teams release independently without shared mutable state.
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -266,11 +255,8 @@ module "ec2" {
|
||||
}
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Use remote state for cross-team dependencies
|
||||
- Document which outputs are consumed by other stacks
|
||||
- Version outputs (don't break downstream consumers)
|
||||
- Consider using data sources instead for provider-managed resources
|
||||
- ✅ document which outputs are consumed externally; version outputs, never break downstream consumers silently
|
||||
- ✅ prefer cloud data sources (`aws_vpc` by tag) over `terraform_remote_state` for provider-managed resources
|
||||
|
||||
### 4. Keep Resource Modules Simple
|
||||
|
||||
@@ -376,74 +362,46 @@ my-module/
|
||||
└── module_test.tftest.hcl # Or .go
|
||||
```
|
||||
|
||||
### Why This Structure?
|
||||
### File Role
|
||||
|
||||
- **README.md** - First thing users see, should explain module purpose
|
||||
- **LICENSE** - Legal terms for public modules (MIT or Apache 2.0)
|
||||
- **.pre-commit-config.yaml** - Automated validation before commits
|
||||
- **main.tf** - Primary resources, keep focused
|
||||
- **variables.tf** - All inputs in one place with descriptions
|
||||
- **outputs.tf** - All outputs documented
|
||||
- **versions.tf** - Lock provider versions for stability
|
||||
- **examples/** - Serve as both documentation and test fixtures
|
||||
- **tests/** - Automated testing
|
||||
- `README.md` — module purpose, first file users see
|
||||
- `LICENSE` — legal terms for public modules (MIT or Apache 2.0)
|
||||
- `.pre-commit-config.yaml` — automated validation before commits
|
||||
- `main.tf` — primary resources, keep focused
|
||||
- `variables.tf` — all inputs, with descriptions
|
||||
- `outputs.tf` — all outputs, with descriptions
|
||||
- `versions.tf` — pinned provider versions
|
||||
- `examples/` — docs + test fixtures
|
||||
- `tests/` — automated tests
|
||||
|
||||
### License Files
|
||||
|
||||
For public modules, always include a LICENSE file:
|
||||
- **MIT License** - Simple, permissive (common for public modules)
|
||||
- **Apache 2.0** - Permissive with patent grant protection
|
||||
|
||||
**Important:** Do NOT store LICENSE templates in this skill. Generate them during module creation using user preference.
|
||||
|
||||
**When to include:**
|
||||
- ✅ Public modules (GitHub, Terraform Registry)
|
||||
- ✅ Open-source projects
|
||||
- ❌ Private internal modules (optional)
|
||||
- ❌ Environment-specific configurations
|
||||
- ✅ Public modules / open-source projects — include LICENSE (MIT = permissive; Apache 2.0 = permissive + patent grant)
|
||||
- ❌ Private internal modules / environment-specific configs — optional
|
||||
- ❌ Do NOT store LICENSE templates in this skill; generate them on demand from user preference
|
||||
|
||||
### Terraform vs OpenTofu Preference
|
||||
|
||||
**Before generating any module or configuration:**
|
||||
HCL is identical; choice affects commands, README, CI invocations, binary references only. Ask before generating if not specified.
|
||||
|
||||
1. **Ask the user:** "Will this be for Terraform or OpenTofu? (Both are supported equally)"
|
||||
**Inference signals (when a project already exists):**
|
||||
- `required_version` constraint or comments pinning the runtime
|
||||
- CI pipelines invoking `terraform` vs `tofu` explicitly
|
||||
- `.terraform.lock.hcl` provenance (check commit history / init script)
|
||||
- ❌ `.terraform/` working directory — both runtimes share it, not a differentiator
|
||||
|
||||
2. **Use the preference throughout:**
|
||||
- Command examples: `terraform` vs `tofu`
|
||||
- README documentation
|
||||
- CI/CD workflow templates
|
||||
- Version constraints
|
||||
- Binary references
|
||||
If signals are mixed, ask the user rather than guessing, or show both command variants in docs.
|
||||
|
||||
3. **Document the choice:**
|
||||
```markdown
|
||||
## Requirements
|
||||
Document the chosen runtime in the module README:
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| [terraform/tofu] | >= 1.7.0 |
|
||||
| aws | >= 6.0 |
|
||||
```
|
||||
```markdown
|
||||
## Requirements
|
||||
|
||||
4. **Example command variations:**
|
||||
```bash
|
||||
# Terraform
|
||||
terraform init
|
||||
terraform test
|
||||
terraform plan
|
||||
|
||||
# OpenTofu
|
||||
tofu init
|
||||
tofu test
|
||||
tofu plan
|
||||
```
|
||||
|
||||
**Note:** The choice is primarily about commands and documentation. The HCL code itself is identical.
|
||||
|
||||
**Default behavior:**
|
||||
- If user doesn't specify: Ask explicitly
|
||||
- If project already exists: Detect from existing files (`.terraform/` or `.tofu/`)
|
||||
- If still unclear: Default to showing both options in documentation
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| [terraform/tofu] | >= 1.7.0 |
|
||||
| aws | ~> 5.0 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -498,6 +456,55 @@ var.type
|
||||
var.value
|
||||
```
|
||||
|
||||
### Provider Requirements and Alias Passing
|
||||
|
||||
- ✅ Child module declares aliased providers: `configuration_aliases = [aws.primary, aws.replica]`
|
||||
- ✅ Caller passes them explicitly: `providers = { aws.primary = aws.<caller-alias> }` on the `module` block
|
||||
- ❌ Default provider inheritance applies ONLY to a single unaliased provider — never for aliases
|
||||
|
||||
Child module — declare aliases in `versions.tf`, bind per resource:
|
||||
|
||||
```hcl
|
||||
# modules/replicated-s3/versions.tf
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
configuration_aliases = [aws.primary, aws.replica]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# in any resource:
|
||||
provider = aws.primary
|
||||
```
|
||||
|
||||
Caller — pass the `providers` map on the `module` block:
|
||||
|
||||
```hcl
|
||||
module "bucket" {
|
||||
source = "./modules/replicated-s3"
|
||||
bucket_name = "app-data"
|
||||
|
||||
providers = {
|
||||
aws.primary = aws.us_east_1
|
||||
aws.replica = aws.eu_west_1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
❌ DON'T — missing `providers` map on the module call:
|
||||
|
||||
```hcl
|
||||
module "bucket" {
|
||||
source = "./modules/replicated-s3"
|
||||
bucket_name = "app-data"
|
||||
# MISSING: providers = { aws.primary = ..., aws.replica = ... }
|
||||
# Plan fails: "No configuration for provider aws.primary"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Best Practices
|
||||
@@ -542,36 +549,11 @@ output "connection_info" {
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### ✅ DO: Use `for_each` for Resources
|
||||
### Iteration: `for_each` vs `count`
|
||||
|
||||
```hcl
|
||||
# Good: Maintain stable resource addresses
|
||||
resource "aws_instance" "server" {
|
||||
for_each = toset(["web", "api", "worker"])
|
||||
Use `for_each` with stable keys whenever a collection has meaningful identity — removing or reordering an element leaves unrelated addresses untouched. Reserve `count` for optional singletons (`0` or `1`) and cases where keys cannot be known at plan time.
|
||||
|
||||
instance_type = "t3.micro"
|
||||
tags = {
|
||||
Name = each.key
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why?** When you remove an item from the middle, `for_each` doesn't reshuffle other resources.
|
||||
|
||||
### ❌ DON'T: Use `count` When Order Matters
|
||||
|
||||
```hcl
|
||||
# Bad: Removing middle item reshuffles all subsequent resources
|
||||
resource "aws_instance" "server" {
|
||||
count = length(var.server_names)
|
||||
|
||||
tags = {
|
||||
Name = var.server_names[count.index]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** If you remove `var.server_names[1]`, Terraform will destroy and recreate all instances after it.
|
||||
For the decision matrix, migration playbook, and known-at-plan failure patterns, see [Code Patterns: count vs for_each](code-patterns.md#count-vs-for_each-deep-dive).
|
||||
|
||||
### ✅ DO: Separate Root Module from Reusable Modules
|
||||
|
||||
@@ -587,7 +569,7 @@ modules/webapp/
|
||||
variables.tf # Configurable inputs
|
||||
```
|
||||
|
||||
**Why?** Root modules are environment-specific, reusable modules are generic.
|
||||
Root modules are environment-specific; reusable modules are generic.
|
||||
|
||||
### ✅ DO: Use Locals for Computed Values
|
||||
|
||||
@@ -622,7 +604,7 @@ module "vpc" {
|
||||
}
|
||||
```
|
||||
|
||||
**Why?** Prevents unexpected breaking changes.
|
||||
Prevents unexpected breaking changes from upstream major bumps.
|
||||
|
||||
---
|
||||
|
||||
@@ -707,24 +689,7 @@ environments/
|
||||
|
||||
### ❌ DON'T: Use `terraform_remote_state` Everywhere
|
||||
|
||||
```hcl
|
||||
# Overused: Creates tight coupling
|
||||
data "terraform_remote_state" "vpc" {
|
||||
# ...
|
||||
}
|
||||
|
||||
data "terraform_remote_state" "database" {
|
||||
# ...
|
||||
}
|
||||
|
||||
data "terraform_remote_state" "security" {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Changes to one state file break others.
|
||||
|
||||
**Fix:** Use module outputs when possible, reserve remote state for truly separate teams.
|
||||
Use module outputs when possible. Reserve remote state for ownership boundaries between teams. See [Use terraform_remote_state Sparingly](#3-use-terraform_remote_state-sparingly--only-at-true-ownership-boundaries) for the full rule set.
|
||||
|
||||
---
|
||||
|
||||
@@ -757,369 +722,47 @@ acme-terraform-aws-rds
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Modules
|
||||
## Module Release Checklist
|
||||
|
||||
For testing guidance, see [testing-frameworks.md](testing-frameworks.md).
|
||||
Before publishing or handing off a reusable module:
|
||||
|
||||
Quick checklist:
|
||||
|
||||
- [ ] Ask: Terraform or OpenTofu?
|
||||
- [ ] Ask: Public or private module?
|
||||
- [ ] Include `examples/` directory
|
||||
- [ ] Write tests (native or Terratest)
|
||||
- [ ] Document inputs and outputs in README.md
|
||||
- [ ] Version your module
|
||||
- [ ] Create `.gitignore` (from template below)
|
||||
- [ ] Create `.pre-commit-config.yaml` (from template above)
|
||||
- [ ] Create `LICENSE` file (MIT or Apache 2.0 for public modules)
|
||||
- [ ] Add attribution footer to README.md (see template below)
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
When creating new modules, always include pre-commit hooks for automated validation and documentation generation:
|
||||
|
||||
**Standard .pre-commit-config.yaml template:**
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/antonbabenko/pre-commit-terraform
|
||||
rev: v1.92.0 # Use latest version from releases
|
||||
hooks:
|
||||
- id: terraform_fmt
|
||||
- id: terraform_validate
|
||||
- id: terraform_tflint
|
||||
- id: terraform_docs
|
||||
```
|
||||
|
||||
**Installation:**
|
||||
|
||||
```bash
|
||||
# Install pre-commit
|
||||
pip install pre-commit
|
||||
|
||||
# Install hooks
|
||||
pre-commit install
|
||||
|
||||
# Run manually
|
||||
pre-commit run -a
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Include `.pre-commit-config.yaml` in all new modules
|
||||
- Pin to specific pre-commit-terraform version
|
||||
- Update version regularly
|
||||
|
||||
**For module generation:**
|
||||
When generating new modules, also create:
|
||||
- `.pre-commit-config.yaml` (from template above)
|
||||
- `LICENSE` file (MIT or Apache 2.0, based on user preference)
|
||||
- `.gitignore` (from template below)
|
||||
- `README.md` with attribution footer (see template below)
|
||||
|
||||
#### README.md Attribution Template
|
||||
|
||||
When generating module README.md files, include this attribution footer:
|
||||
|
||||
```markdown
|
||||
## Attribution
|
||||
|
||||
This module was created following best practices from [terraform-skill](https://github.com/antonbabenko/terraform-skill) by Anton Babenko.
|
||||
|
||||
Additional resources:
|
||||
- [terraform-best-practices.com](https://terraform-best-practices.com)
|
||||
- [Compliance.tf](https://compliance.tf)
|
||||
```
|
||||
|
||||
**When to include attribution:**
|
||||
- ✅ All new modules created with terraform-skill guidance
|
||||
- ✅ Public modules (GitHub, Terraform Registry)
|
||||
- ✅ Private modules shared within organizations
|
||||
- ⚠️ Optional for one-off environment configurations
|
||||
|
||||
**Rationale:** This is a derivative work as defined in the Apache 2.0 License Section 1. Attribution supports the open-source ecosystem and helps others discover these best practices.
|
||||
|
||||
**README Structure with Attribution:**
|
||||
```markdown
|
||||
# Module Name
|
||||
|
||||
## Description
|
||||
[Module purpose]
|
||||
|
||||
## Usage
|
||||
[Usage examples]
|
||||
|
||||
## Inputs
|
||||
[Input variables]
|
||||
|
||||
## Outputs
|
||||
[Output values]
|
||||
|
||||
## Requirements
|
||||
[Terraform/OpenTofu versions, providers]
|
||||
|
||||
## Attribution
|
||||
[Attribution footer from template above]
|
||||
```
|
||||
|
||||
#### .gitignore Template
|
||||
|
||||
**Standard .gitignore for Terraform/OpenTofu projects:**
|
||||
|
||||
```gitignore
|
||||
# .gitignore - Terraform/OpenTofu projects
|
||||
# Based on terraform-skill best practices
|
||||
|
||||
# Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
.terraform.lock.hcl
|
||||
|
||||
# .tfstate files - NEVER commit state files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# Crash log files
|
||||
crash.log
|
||||
crash.*.log
|
||||
|
||||
# Exclude all .tfvars files (may contain sensitive data)
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
|
||||
# Ignore override files (local development)
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
|
||||
# CLI configuration files
|
||||
.terraformrc
|
||||
terraform.rc
|
||||
|
||||
# Environment variables and secrets
|
||||
.env
|
||||
.env.*
|
||||
secrets/
|
||||
*.secret
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Terraform plan output files
|
||||
*.tfplan
|
||||
*.tfplan.json
|
||||
```
|
||||
- [ ] Runtime and provider choice explicit (Terraform vs OpenTofu, version floor in `required_version`)
|
||||
- [ ] Public vs private scope decided (affects naming + license)
|
||||
- [ ] `examples/` directory with at least `minimal` and `complete`
|
||||
- [ ] Tests written (native `terraform test` on 1.6+, or Terratest) — see [testing-frameworks.md](testing-frameworks.md)
|
||||
- [ ] README documents all inputs/outputs (Description → Usage → Inputs → Outputs → Requirements)
|
||||
- [ ] Module source pinned with `version` in consumer code
|
||||
- [ ] `pre-commit-terraform` hooks configured (`terraform_fmt`, `terraform_validate`, `terraform_tflint`, `terraform_docs`), pinned to a specific `rev`
|
||||
- [ ] `LICENSE` present for public modules (MIT or Apache-2.0)
|
||||
- [ ] `.gitignore` excludes `.terraform/`, `*.tfstate*`, `*.tfvars`, override files, and editor artifacts
|
||||
|
||||
---
|
||||
|
||||
## Testing Philosophy & Patterns
|
||||
## Module Testing — Pointer
|
||||
|
||||
### What to Test in Terraform Modules
|
||||
Module testing (what to test, tiered layers, mocking, idempotency, cost control, strategy by module type) is canonical in [Testing Frameworks](testing-frameworks.md). Module-specific rules that belong with the module contract:
|
||||
|
||||
**Core testing areas:**
|
||||
- **Input validation** - Variables accept valid values and reject invalid ones
|
||||
- **Resource creation** - Resources are created as expected with correct attributes
|
||||
- **Output correctness** - Outputs return expected values and types
|
||||
- **Idempotency** - Applying twice doesn't recreate resources
|
||||
- **Destroy completeness** - All resources are cleaned up properly
|
||||
- Every reusable module must exercise its `validation` blocks in tests — reject cases are as important as happy paths.
|
||||
- Tier tests by module role: **resource modules** → input validation + attribute assertions; **infrastructure modules** → composition + cross-module wiring; **compositions** → smoke-plan + production-like values + remote-state connectivity.
|
||||
- Mock providers (1.7+) for unit tests; reserve real cloud runs for main-branch or scheduled jobs.
|
||||
|
||||
**When to write tests:**
|
||||
- During development for reusable modules
|
||||
- Before publishing modules to registry
|
||||
- After significant refactoring
|
||||
- For modules with complex logic or conditionals
|
||||
---
|
||||
|
||||
### Testing Layers
|
||||
## LLM Mistake Checklist — Modules
|
||||
|
||||
**1. Syntax validation:**
|
||||
```bash
|
||||
terraform fmt -check -recursive
|
||||
```
|
||||
Common model mistakes to correct when generating or reviewing modules:
|
||||
|
||||
**2. Configuration validity:**
|
||||
```bash
|
||||
terraform validate
|
||||
```
|
||||
|
||||
**3. Plan preview:**
|
||||
```bash
|
||||
terraform plan
|
||||
# Review: Are expected resources being created?
|
||||
# Verify: Count and types of resources match expectations
|
||||
```
|
||||
|
||||
**4. Integration testing:**
|
||||
```bash
|
||||
# Apply and verify
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Verify resources exist (use AWS CLI, etc.)
|
||||
aws ec2 describe-vpcs --vpc-ids $(terraform output -raw vpc_id)
|
||||
|
||||
# Test idempotency - should show no changes
|
||||
terraform plan
|
||||
# Expected: "No changes. Your infrastructure matches the configuration."
|
||||
|
||||
# Clean up
|
||||
terraform destroy -auto-approve
|
||||
```
|
||||
|
||||
### Input Validation Testing
|
||||
|
||||
Test that variables reject invalid values:
|
||||
|
||||
```hcl
|
||||
# In variables.tf
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "prod"], var.environment)
|
||||
error_message = "Environment must be one of: dev, staging, prod."
|
||||
}
|
||||
}
|
||||
|
||||
# Test: terraform plan with invalid value should fail
|
||||
# terraform plan -var="environment=invalid"
|
||||
# Expected: Error message about validation failure
|
||||
```
|
||||
|
||||
### Output Verification Testing
|
||||
|
||||
After apply, verify outputs contain expected values:
|
||||
|
||||
```bash
|
||||
# Verify output is not empty
|
||||
VPC_ID=$(terraform output -raw vpc_id)
|
||||
[ -z "$VPC_ID" ] && echo "ERROR: VPC ID is empty" || echo "OK: VPC ID is $VPC_ID"
|
||||
|
||||
# Verify output format
|
||||
SUBNET_IDS=$(terraform output -json subnet_ids)
|
||||
echo $SUBNET_IDS | jq 'length' # Should match expected subnet count
|
||||
```
|
||||
|
||||
### Idempotency Testing
|
||||
|
||||
**Critical test** - ensures Terraform doesn't recreate resources unnecessarily:
|
||||
|
||||
```bash
|
||||
# Apply configuration
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Immediately run plan - should show no changes
|
||||
terraform plan -detailed-exitcode
|
||||
# Exit code 0 = no changes (idempotent) ✓
|
||||
# Exit code 2 = changes detected (not idempotent) ✗
|
||||
```
|
||||
|
||||
**Why idempotency matters:**
|
||||
- Proves configuration is stable
|
||||
- No resource churn on repeated applies
|
||||
- Safe to run in CI/CD pipelines
|
||||
- Indicates proper use of computed values
|
||||
|
||||
### Destroy Testing
|
||||
|
||||
Verify all resources are properly cleaned up:
|
||||
|
||||
```bash
|
||||
# Before destroy - count resources
|
||||
BEFORE_COUNT=$(terraform state list | wc -l)
|
||||
|
||||
# Destroy
|
||||
terraform destroy -auto-approve
|
||||
|
||||
# After destroy - verify state is empty
|
||||
AFTER_COUNT=$(terraform state list | wc -l)
|
||||
[ "$AFTER_COUNT" -eq 0 ] && echo "OK: All resources destroyed" || echo "ERROR: Resources remain"
|
||||
```
|
||||
|
||||
### Testing Anti-patterns
|
||||
|
||||
**❌ Don't:**
|
||||
- Skip idempotency testing (most important test)
|
||||
- Test only happy paths (test validation failures too)
|
||||
- Forget to clean up test resources
|
||||
- Run expensive integration tests on every commit
|
||||
- Test Terraform syntax (terraform validate does this)
|
||||
|
||||
**✅ Do:**
|
||||
- Test that validation blocks reject invalid input
|
||||
- Verify outputs have expected types and formats
|
||||
- Test conditional resource creation (count/for_each)
|
||||
- Document expected resource counts in tests
|
||||
- Use mocking for unit tests (Terraform 1.7+)
|
||||
- Run integration tests only on main branch or scheduled
|
||||
|
||||
### Testing Strategy by Module Type
|
||||
|
||||
**Resource modules:**
|
||||
- Focus on input validation
|
||||
- Test resource creation with minimal config
|
||||
- Verify outputs are correct
|
||||
- Test idempotency
|
||||
|
||||
**Infrastructure modules:**
|
||||
- Test module composition works
|
||||
- Verify cross-module dependencies
|
||||
- Test with different configurations
|
||||
- Integration tests in test account
|
||||
|
||||
**Compositions:**
|
||||
- Smoke tests (can it plan?)
|
||||
- Test with production-like values
|
||||
- Verify remote state connectivity
|
||||
- Manual QA in lower environments first
|
||||
|
||||
### Cost Control for Testing
|
||||
|
||||
**Strategies:**
|
||||
|
||||
1. **Use mocking for unit tests** (Terraform 1.7+)
|
||||
```hcl
|
||||
mock_provider "aws" {
|
||||
mock_data "aws_ami" {
|
||||
defaults = {
|
||||
id = "ami-12345678"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Tag test resources for tracking**
|
||||
```hcl
|
||||
tags = {
|
||||
Environment = "test"
|
||||
TTL = "2h"
|
||||
ManagedBy = "terraform-test"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Run integration tests only on main branch**
|
||||
```yaml
|
||||
if: github.ref == 'refs/heads/main'
|
||||
```
|
||||
|
||||
4. **Use smaller instance types**
|
||||
```hcl
|
||||
instance_type = var.environment == "test" ? "t3.micro" : var.instance_type
|
||||
```
|
||||
|
||||
5. **Implement auto-cleanup**
|
||||
- Use AWS Lambda to delete resources with expired TTL tags
|
||||
- Run destroy in CI/CD after tests complete
|
||||
- Use terraform-compliance to enforce TTL tags
|
||||
|
||||
**For testing framework details, see:** [Testing Frameworks Guide](testing-frameworks.md)
|
||||
- bundles unrelated resources into one "god module" instead of splitting by single responsibility
|
||||
- hardcodes environment-specific values (`instance_type = "m5.large"`, `Environment = "production"`) inside a reusable module
|
||||
- accepts untyped `map(any)` / `any` for core module inputs instead of typed objects with `optional()` defaults
|
||||
- exposes entire provider or resource objects as outputs, leaking the whole contract instead of a stable subset
|
||||
- omits `description` on inputs and outputs, forcing consumers to read the implementation
|
||||
- uses `this` for multiple resources of the same type — reserve `this` for genuine singletons only
|
||||
- reaches for `terraform_remote_state` inside a single team's stack instead of wiring via module outputs
|
||||
- floats module sources (no `version` pin) in consumer code
|
||||
- pushes environment-specific policy (prod-only allowlists, region pins) into primitive/resource modules where it cannot be overridden
|
||||
- omits `configuration_aliases` in a multi-provider child module's `required_providers` — callers cannot pass aliased providers
|
||||
- drops the `providers = { aws = aws.region }` map from the module call on multi-region or multi-account deploys — resources land on the default provider
|
||||
|
||||
---
|
||||
|
||||
+219
-48
@@ -61,6 +61,70 @@ terraform show -json tfplan | jq -r '.' > tfplan.json
|
||||
terraform show tfplan | grep "will be created"
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```bash
|
||||
# View all resources in state
|
||||
terraform state list
|
||||
|
||||
# Show specific resource details
|
||||
terraform state show aws_instance.web
|
||||
|
||||
# Move/rename resource in state (refactoring)
|
||||
terraform state mv aws_instance.old aws_instance.new
|
||||
terraform state mv aws_instance.app module.compute.aws_instance.app
|
||||
|
||||
# Remove resource from state (keeps actual resource)
|
||||
terraform state rm aws_instance.temporary
|
||||
|
||||
# Import existing resource into state
|
||||
terraform import aws_instance.web i-1234567890abcdef0
|
||||
|
||||
# Import using import blocks (1.5+)
|
||||
# Define in .tf: import { to = aws_instance.web, id = "i-123..." }
|
||||
# Note: File must not exist — Terraform refuses to overwrite.
|
||||
terraform plan -generate-config-out=imported.tf
|
||||
|
||||
# Detect configuration drift
|
||||
terraform plan -refresh-only
|
||||
|
||||
# Update state to match reality (no infrastructure changes)
|
||||
terraform apply -refresh-only
|
||||
|
||||
# Backup state to file
|
||||
terraform state pull > backup-$(date +%Y%m%d).tfstate
|
||||
|
||||
# Restore state from backup (DANGEROUS)
|
||||
terraform state push backup.tfstate
|
||||
|
||||
# Force unlock stuck state lock
|
||||
# Default: prompts for y/N confirmation
|
||||
terraform force-unlock LOCK_ID
|
||||
|
||||
# CI-friendly (skips prompt):
|
||||
terraform force-unlock -force LOCK_ID
|
||||
```
|
||||
|
||||
### State Backend Migration
|
||||
|
||||
```bash
|
||||
# Migrate from local to remote backend
|
||||
# 1. Add backend config to backend.tf
|
||||
# 2. Run migration
|
||||
terraform init -migrate-state
|
||||
|
||||
# Change backend without migrating state
|
||||
terraform init -reconfigure
|
||||
|
||||
# Pass backend config at runtime
|
||||
terraform init \
|
||||
-backend-config="key=prod/terraform.tfstate" \
|
||||
-backend-config="dynamodb_table=terraform-locks"
|
||||
|
||||
# Or use config file
|
||||
terraform init -backend-config=backend-prod.hcl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Flowchart
|
||||
@@ -133,10 +197,10 @@ Need to test Terraform/OpenTofu code?
|
||||
|
||||
### Terraform 1.6+ / OpenTofu 1.6+
|
||||
|
||||
- ✅ NEW: Native `terraform test` / `tofu test`
|
||||
- ✅ NEW: Native `terraform test` / `tofu test` framework with `.tftest.hcl` files
|
||||
- ✅ Consider migrating simple tests from Terratest
|
||||
- ✅ Keep Terratest for complex integration
|
||||
- ✅ All Terraform 1.0+ features available
|
||||
- ✅ Import blocks from 1.5 available for declarative imports with `-generate-config-out`
|
||||
|
||||
### Terraform 1.7+ / OpenTofu 1.7+
|
||||
|
||||
@@ -147,39 +211,22 @@ Need to test Terraform/OpenTofu code?
|
||||
|
||||
### Terraform vs OpenTofu Comparison
|
||||
|
||||
Both Terraform and OpenTofu are fully supported by this skill. The choice depends on your requirements:
|
||||
|
||||
**Quick Decision Matrix:**
|
||||
|
||||
| Factor | Terraform | OpenTofu |
|
||||
|--------|-----------|----------|
|
||||
| **Licensing** | Business Source License (BSL) 1.1 | Mozilla Public License 2.0 (MPL 2.0) |
|
||||
| **Licensing** | Business Source License 1.1 (BUSL-1.1) | Mozilla Public License 2.0 (MPL 2.0) |
|
||||
| **Governance** | HashiCorp (single vendor) | Linux Foundation (community-driven) |
|
||||
| **Latest Version** | 1.14+ | 1.11+ |
|
||||
| **Native Testing** | 1.6+ | 1.6+ |
|
||||
| **Mock Providers** | 1.7+ | 1.7+ |
|
||||
| **Feature Parity** | Reference implementation | Compatible fork with some additions |
|
||||
| **Enterprise Support** | HCP Terraform, Terraform Cloud | Multiple vendors |
|
||||
| **Migration Path** | N/A | Drop-in replacement for Terraform ≤1.5 |
|
||||
| **Migration Path** | N/A | Drop-in replacement for Terraform ≤1.5.x; feature-compatible fork thereafter with divergence on encryption, mock providers, provider functions, and other post-1.6 additions. Verify specific feature availability per version. |
|
||||
|
||||
**When to choose Terraform:**
|
||||
- Using HashiCorp Terraform Cloud or HCP Terraform
|
||||
- Enterprise support contract with HashiCorp
|
||||
- Need absolute latest features first
|
||||
**Choose Terraform for:** HCP Terraform / Terraform Cloud, HashiCorp enterprise support, first access to latest features.
|
||||
|
||||
**When to choose OpenTofu:**
|
||||
- Prefer open-source governance model
|
||||
- Want to avoid vendor lock-in concerns
|
||||
- Building on community-driven development
|
||||
- BSL 1.1 license doesn't fit your use case
|
||||
**Choose OpenTofu for:** open-source governance, vendor-lock-in avoidance, BUSL-1.1 incompatibility.
|
||||
|
||||
**For this skill:**
|
||||
- Commands are shown for both: `terraform` and `tofu`
|
||||
- Most patterns work identically, though differences exist (see release notes)
|
||||
- Version-specific features noted (1.6+, 1.7+, etc.)
|
||||
- **Note:** Since OpenTofu 1.6, the platforms have diverged with unique features
|
||||
|
||||
**When creating modules, Claude will ask your preference** to generate appropriate commands and documentation.
|
||||
Since OpenTofu 1.6 the platforms have diverged — this skill notes version floors explicitly and shows both `terraform` and `tofu` commands. When creating modules, Claude asks preference to pick commands/docs.
|
||||
|
||||
---
|
||||
|
||||
@@ -267,6 +314,148 @@ bucketName := fmt.Sprintf("test-bucket-%s", uniqueId)
|
||||
- VPCs, security groups (rarely change)
|
||||
- Don't share: instances, databases (change often)
|
||||
|
||||
### Issue: State lock is stuck
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Error acquiring the state lock
|
||||
Lock Info:
|
||||
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
Who: user@hostname
|
||||
Created: 2026-01-20 12:00:00
|
||||
```
|
||||
|
||||
**Common Causes:**
|
||||
1. Terraform process crashed or was killed
|
||||
2. Network interruption during operation
|
||||
3. CI/CD job terminated unexpectedly
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# 1. Verify the operation is NOT actually running
|
||||
# Check the host mentioned in lock info
|
||||
ssh user@hostname "ps aux | grep terraform"
|
||||
|
||||
# Or check CI/CD job status
|
||||
# GitHub Actions: Check workflow runs
|
||||
# GitLab CI: Check pipeline jobs
|
||||
|
||||
# 2. Only if confirmed the operation is not running:
|
||||
terraform force-unlock LOCK_ID
|
||||
|
||||
# 3. Document why you unlocked
|
||||
echo "Force-unlocked due to CI job timeout" > unlock-notes.txt
|
||||
```
|
||||
|
||||
**Prevention:**
|
||||
```yaml
|
||||
# GitHub Actions - Use concurrency control
|
||||
concurrency:
|
||||
group: terraform-${{ github.ref }}
|
||||
cancel-in-progress: false # Wait, don't cancel
|
||||
```
|
||||
|
||||
### Issue: State file is corrupted or lost
|
||||
|
||||
**Symptoms:**
|
||||
- Error: "state snapshot was created by Terraform v1.8.0"
|
||||
- Error: "Failed to load state"
|
||||
- State file missing or unreadable
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**If versioning enabled (S3):**
|
||||
```bash
|
||||
# List versions
|
||||
aws s3api list-object-versions \
|
||||
--bucket my-terraform-state \
|
||||
--prefix prod/terraform.tfstate
|
||||
|
||||
# Restore previous version
|
||||
aws s3api get-object \
|
||||
--bucket my-terraform-state \
|
||||
--key prod/terraform.tfstate \
|
||||
--version-id PREVIOUS_VERSION_ID \
|
||||
terraform.tfstate.restored
|
||||
|
||||
# Push restored state
|
||||
terraform state push terraform.tfstate.restored
|
||||
```
|
||||
|
||||
**If no backup exists:**
|
||||
```bash
|
||||
# Recreate state by importing all resources
|
||||
terraform import aws_vpc.main vpc-12345678
|
||||
terraform import aws_subnet.private[0] subnet-abcd1234
|
||||
# ... continue for all resources
|
||||
|
||||
# Or use import blocks (1.5+)
|
||||
# In .tf file:
|
||||
# import { to = aws_vpc.main, id = "vpc-12345678" }
|
||||
# Note: File must not exist — Terraform refuses to overwrite.
|
||||
terraform plan -generate-config-out=imported.tf
|
||||
```
|
||||
|
||||
### Issue: Configuration drift detected
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Note: Objects have changed outside of Terraform
|
||||
```
|
||||
|
||||
**Cause:** Manual changes in console or by other tools
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```bash
|
||||
# View drift
|
||||
terraform plan -refresh-only
|
||||
|
||||
# Accept drift (update state to match reality)
|
||||
terraform apply -refresh-only
|
||||
|
||||
# Or fix drift (update resources to match config)
|
||||
terraform apply
|
||||
|
||||
# Prevent drift with detective controls
|
||||
# - Enable CloudTrail
|
||||
# - Use AWS Config rules
|
||||
# - Regular terraform plan in CI
|
||||
```
|
||||
|
||||
### Issue: Cannot migrate state between backends
|
||||
|
||||
**Symptoms:**
|
||||
- `terraform init -migrate-state` fails
|
||||
- Backend authentication errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```bash
|
||||
# Ensure credentials are configured
|
||||
export AWS_PROFILE=terraform
|
||||
# or
|
||||
export AWS_ACCESS_KEY_ID=...
|
||||
export AWS_SECRET_ACCESS_KEY=...
|
||||
|
||||
# Try migration again
|
||||
terraform init -migrate-state
|
||||
|
||||
# If still failing, manual migration:
|
||||
# 1. Pull state from old backend
|
||||
terraform state pull > old-state.json
|
||||
|
||||
# 2. Switch backend config
|
||||
# Edit backend.tf
|
||||
|
||||
# 3. Initialize new backend
|
||||
terraform init -reconfigure
|
||||
|
||||
# 4. Push state to new backend
|
||||
terraform state push old-state.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Paths
|
||||
@@ -324,29 +513,11 @@ tests/
|
||||
|
||||
### From Terraform → OpenTofu
|
||||
|
||||
**Good news:** OpenTofu is a drop-in replacement!
|
||||
OpenTofu is a drop-in replacement for Terraform ≤1.5.x; a feature-compatible fork thereafter with divergence on encryption, mock providers, provider functions, and other post-1.6 additions. See the [Terraform vs OpenTofu Comparison](#terraform-vs-opentofu-comparison) and verify per-version feature availability.
|
||||
|
||||
1. **No code changes needed**
|
||||
- All Terraform syntax works
|
||||
- Same provider ecosystem
|
||||
- Compatible state files
|
||||
|
||||
2. **Update CI/CD:**
|
||||
```bash
|
||||
# Replace
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
|
||||
# With
|
||||
tofu init
|
||||
tofu plan
|
||||
tofu apply
|
||||
```
|
||||
|
||||
3. **Update documentation:**
|
||||
- README mentions OpenTofu compatibility
|
||||
- CI/CD workflows use `tofu` command
|
||||
1. **HCL ≤1.5.x** — no code changes; providers and state files compatible. Verify post-1.6 features per version.
|
||||
2. **CI/CD** — swap `terraform` for `tofu` in `init`/`plan`/`apply` invocations.
|
||||
3. **Docs** — note OpenTofu compatibility in README; update workflow templates to the `tofu` binary.
|
||||
|
||||
---
|
||||
|
||||
@@ -420,8 +591,8 @@ Required documentation for all modules:
|
||||
| Syntax | Meaning | Use Case |
|
||||
|--------|---------|----------|
|
||||
| `"5.0.0"` | Exact version | Avoid (inflexible) |
|
||||
| `"~> 5.0"` | Pessimistic (5.0.x) | Recommended for stability |
|
||||
| `"~> 5.0.1"` | Pessimistic (5.0.x where x >= 1) | Specific patch minimum |
|
||||
| `"~> 5.0"` | Pessimistic (>= 5.0, < 6.0 — any 5.x) | Allow minor and patch updates within 5.x |
|
||||
| `"~> 5.0.1"` | Pessimistic (>= 5.0.1, < 5.1.0 — 5.0.x patches) | Lock to 5.0.x patch updates only |
|
||||
| `">= 5.0, < 6.0"` | Range | Any 5.x version |
|
||||
| `">= 5.0"` | Minimum | Risky (breaking changes) |
|
||||
|
||||
@@ -0,0 +1,637 @@
|
||||
# Security & Compliance
|
||||
|
||||
> **Part of:** [terraform-skill](../SKILL.md)
|
||||
> **Purpose:** Security best practices and compliance patterns for Terraform/OpenTofu
|
||||
|
||||
This document provides security hardening guidance and compliance automation strategies for infrastructure-as-code.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Security Scanning Tools](#security-scanning-tools)
|
||||
2. [Common Security Issues](#common-security-issues)
|
||||
3. [Compliance Testing](#compliance-testing)
|
||||
4. [Secrets Management](#secrets-management)
|
||||
5. [State File Security](#state-file-security)
|
||||
|
||||
---
|
||||
|
||||
## Security Scanning Tools
|
||||
|
||||
### Essential Security Checks
|
||||
|
||||
```bash
|
||||
# Static security scanning
|
||||
trivy config .
|
||||
checkov -d .
|
||||
|
||||
# Compliance testing (policy-as-code against a terraform plan JSON)
|
||||
terraform plan -out=tfplan && terraform show -json tfplan > tfplan.json
|
||||
conftest test tfplan.json --policy policy/
|
||||
```
|
||||
|
||||
### Trivy Integration
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install trivy
|
||||
|
||||
# Linux
|
||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
# In CI
|
||||
- uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: 'config'
|
||||
scan-ref: '.'
|
||||
```
|
||||
|
||||
**Note:** Trivy now includes tfsec's rule set; tfsec itself is in maintenance mode since its absorption into Trivy (2022), but still receives maintenance releases. Both are maintained by Aqua Security.
|
||||
|
||||
**Example Output:**
|
||||
|
||||
```
|
||||
Result #1 HIGH Security group rule allows egress to multiple public internet addresses
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
security.tf:15-20
|
||||
|
||||
12 | resource "aws_security_group_rule" "egress" {
|
||||
13 | type = "egress"
|
||||
14 | from_port = 0
|
||||
15 | to_port = 0
|
||||
16 | protocol = "-1"
|
||||
17 | cidr_blocks = ["0.0.0.0/0"]
|
||||
18 | security_group_id = aws_security_group.this.id
|
||||
19 | }
|
||||
```
|
||||
|
||||
### Checkov Integration
|
||||
|
||||
```bash
|
||||
# Run Checkov
|
||||
checkov -d . --framework terraform
|
||||
|
||||
# Skip specific checks
|
||||
checkov -d . --skip-check CKV_AWS_23
|
||||
|
||||
# Generate JSON report
|
||||
checkov -d . -o json > checkov-report.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Security Issues
|
||||
|
||||
### ❌ DON'T: Store Secrets in Variables
|
||||
|
||||
```hcl
|
||||
# BAD: Secret in plaintext
|
||||
variable "database_password" {
|
||||
type = string
|
||||
default = "SuperSecret123!" # ❌ Never do this
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Use Secrets Manager
|
||||
|
||||
```hcl
|
||||
# Good: Reference secrets from AWS Secrets Manager
|
||||
data "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = "prod/database/password"
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
password = data.aws_secretsmanager_secret_version.db_password.secret_string
|
||||
}
|
||||
```
|
||||
|
||||
<a id="secret-string-state-caveat"></a>
|
||||
|
||||
> **Note — data source `secret_string` persists to state:** The `aws_secretsmanager_secret_version` data source reads `secret_string` into Terraform state during refresh. `password_wo` (AWS provider v5.71+, Terraform 1.11+) keeps the **resource argument** out of state, but the data source still persists the value. For true state exclusion:
|
||||
>
|
||||
> - Prefer `manage_master_user_password = true` (AWS-managed, for RDS)
|
||||
> - Use `ephemeral` providers/resources (Terraform 1.10+)
|
||||
> - Inject via CI environment variable outside Terraform
|
||||
>
|
||||
> Examples below use the data-source pattern; apply one of the alternatives above when the value must not land in state.
|
||||
|
||||
### ❌ DON'T: Use Default VPC
|
||||
|
||||
```hcl
|
||||
# BAD: Default VPC has public subnets
|
||||
resource "aws_instance" "app" {
|
||||
ami = "ami-12345"
|
||||
subnet_id = "subnet-default" # ❌ Avoid default resources
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Create Dedicated VPCs
|
||||
|
||||
```hcl
|
||||
# Good: Custom VPC with private subnets
|
||||
resource "aws_vpc" "this" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
}
|
||||
|
||||
resource "aws_subnet" "private" {
|
||||
vpc_id = aws_vpc.this.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
availability_zone = "us-east-1a"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Skip Encryption
|
||||
|
||||
```hcl
|
||||
# BAD: Unencrypted S3 bucket
|
||||
resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
# ❌ No encryption configured
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Enable Encryption at Rest
|
||||
|
||||
```hcl
|
||||
# Good: Enable encryption
|
||||
resource "aws_s3_bucket" "data" {
|
||||
bucket = "my-data-bucket"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
|
||||
bucket = aws_s3_bucket.data.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "AES256"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **SSE-S3 vs SSE-KMS:** `AES256` above is SSE-S3 (AWS-managed key, no per-request audit trail in CloudTrail). For regulated workloads (HIPAA/PCI/FedRAMP), prefer `aws:kms` with a customer-managed CMK + key rotation enabled.
|
||||
|
||||
### ❌ DON'T: Open Security Groups to Internet
|
||||
|
||||
```hcl
|
||||
# BAD: Security group open to internet on all protocols
|
||||
resource "aws_security_group_rule" "allow_all" {
|
||||
type = "ingress"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1" # ❌ All protocols (worst case)
|
||||
cidr_blocks = ["0.0.0.0/0"] # ❌ Never do this
|
||||
security_group_id = aws_security_group.this.id
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Use Least-Privilege Security Groups
|
||||
|
||||
```hcl
|
||||
# Good: Restrict to specific ports and sources
|
||||
resource "aws_security_group_rule" "app_https" {
|
||||
type = "ingress"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"] # ✅ Internal only
|
||||
security_group_id = aws_security_group.this.id
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Inline Security Group Rules
|
||||
|
||||
```hcl
|
||||
# BAD: Inline ingress/egress blocks
|
||||
resource "aws_security_group" "web" {
|
||||
name = "web-sg"
|
||||
description = "Web server security group"
|
||||
vpc_id = aws_vpc.this.id
|
||||
|
||||
ingress { # ❌ Inline rules cause issues
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"]
|
||||
}
|
||||
|
||||
egress { # ❌ Avoid inline rules
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ DO: Use Separate Security Group Rule Resources
|
||||
|
||||
**Preferred (AWS provider v5+):** Use `aws_vpc_security_group_ingress_rule` / `aws_vpc_security_group_egress_rule`:
|
||||
|
||||
```hcl
|
||||
# Best: Modern individual rule resources (AWS provider v5+)
|
||||
resource "aws_security_group" "web" {
|
||||
name = "web-sg"
|
||||
description = "Web server security group"
|
||||
vpc_id = aws_vpc.this.id
|
||||
|
||||
# No inline rules - managed separately
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "web_https" {
|
||||
security_group_id = aws_security_group.web.id
|
||||
description = "HTTPS from internal VPC"
|
||||
cidr_ipv4 = "10.0.0.0/16"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
ip_protocol = "tcp"
|
||||
}
|
||||
|
||||
# Scope egress to needed ports when possible — avoid 0.0.0.0/0 with ip_protocol = "-1"
|
||||
resource "aws_vpc_security_group_egress_rule" "web_https_out" {
|
||||
security_group_id = aws_security_group.web.id
|
||||
description = "HTTPS to external services"
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
ip_protocol = "tcp"
|
||||
}
|
||||
```
|
||||
|
||||
**Also acceptable:** `aws_security_group_rule` (older but still supported):
|
||||
|
||||
```hcl
|
||||
resource "aws_security_group_rule" "web_https_ingress" {
|
||||
type = "ingress"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"]
|
||||
security_group_id = aws_security_group.web.id
|
||||
}
|
||||
```
|
||||
|
||||
**Why avoid inline rules:**
|
||||
|
||||
| Issue | Inline Rules | Separate Resources |
|
||||
|-------|--------------|-------------------|
|
||||
| Rule changes | Recreates entire SG (downtime) | Updates only the rule |
|
||||
| Mixing approaches | Conflicts and overwrites | N/A - consistent pattern |
|
||||
| Dynamic rules | Complex `dynamic` blocks needed | Native `for_each` per resource |
|
||||
| State management | Rules buried in SG state | Each rule tracked separately |
|
||||
| Conditional rules | Complex nested dynamics | Simple `count` or `for_each` |
|
||||
|
||||
---
|
||||
|
||||
## Compliance Testing
|
||||
|
||||
### Policy-as-code for Terraform plans
|
||||
|
||||
Generate a plan JSON and evaluate it with a policy engine. The modern, actively-maintained options are Conftest (OPA/Rego) and Open Policy Agent directly. The `terraform-compliance` BDD project is archived and no longer maintained; prefer Conftest/OPA for new work.
|
||||
|
||||
```bash
|
||||
# Generate plan JSON
|
||||
terraform plan -out=tfplan
|
||||
terraform show -json tfplan > tfplan.json
|
||||
|
||||
# Evaluate with Conftest (OPA under the hood)
|
||||
conftest test tfplan.json --policy policy/
|
||||
```
|
||||
|
||||
### Open Policy Agent (OPA)
|
||||
|
||||
```rego
|
||||
# policy/s3_encryption.rego
|
||||
package terraform.s3
|
||||
|
||||
# AWS provider v4+ moved S3 encryption to the separate
|
||||
# aws_s3_bucket_server_side_encryption_configuration resource.
|
||||
# Iterate those resources and verify the rule block sets an accepted algorithm.
|
||||
|
||||
valid_algorithms := {"aws:kms", "aws:kms:dsse", "AES256"}
|
||||
|
||||
# Collect buckets that have an encryption config with a valid algorithm
|
||||
encrypted_buckets[bucket] {
|
||||
sse := input.resource_changes[_]
|
||||
sse.type == "aws_s3_bucket_server_side_encryption_configuration"
|
||||
rule := sse.change.after.rule[_]
|
||||
algo := rule.apply_server_side_encryption_by_default[_].sse_algorithm
|
||||
valid_algorithms[algo]
|
||||
bucket := sse.change.after.bucket
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
sse := input.resource_changes[_]
|
||||
sse.type == "aws_s3_bucket_server_side_encryption_configuration"
|
||||
rule := sse.change.after.rule[_]
|
||||
algo := rule.apply_server_side_encryption_by_default[_].sse_algorithm
|
||||
not valid_algorithms[algo]
|
||||
|
||||
msg := sprintf(
|
||||
"S3 encryption config '%s' uses unsupported sse_algorithm '%s' (expected aws:kms or AES256)",
|
||||
[sse.address, algo],
|
||||
)
|
||||
}
|
||||
|
||||
# Flag buckets that have no matching encryption configuration at all.
|
||||
deny[msg] {
|
||||
bucket := input.resource_changes[_]
|
||||
bucket.type == "aws_s3_bucket"
|
||||
bucket_name := bucket.change.after.bucket
|
||||
not encrypted_buckets[bucket_name]
|
||||
|
||||
msg := sprintf(
|
||||
"S3 bucket '%s' has no aws_s3_bucket_server_side_encryption_configuration",
|
||||
[bucket.address],
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secrets Management
|
||||
|
||||
### AWS Secrets Manager Pattern
|
||||
|
||||
See the [data-source `secret_string` persistence caveat](#secret-string-state-caveat) above — both `random_password.result` and data-source reads of `secret_string` land in Terraform state. The recommended RDS pattern avoids both.
|
||||
|
||||
```hcl
|
||||
# Recommended: let RDS generate and manage the master password in Secrets Manager
|
||||
resource "aws_kms_key" "db" {
|
||||
description = "KMS CMK for RDS-managed master password"
|
||||
enable_key_rotation = true
|
||||
deletion_window_in_days = 30
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "this" {
|
||||
# Option 1 (recommended): AWS-managed master password in Secrets Manager
|
||||
manage_master_user_password = true
|
||||
master_user_secret_kms_key_id = aws_kms_key.db.arn
|
||||
|
||||
# Option 2 (Terraform 1.11+ + AWS provider v5.71+): write-only password
|
||||
# password_wo = ephemeral.random_password.db.result
|
||||
# password_wo_version = 1
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
If you need a manually-managed secret for a non-RDS consumer, keep the value out of state by sourcing it outside Terraform (CI env var, ephemeral resource, or a write-only argument) rather than via `random_password` + a `data` lookup:
|
||||
|
||||
```hcl
|
||||
# Only use this shape when the consumer cannot use manage_master_user_password
|
||||
# and you are comfortable with the caveat linked above.
|
||||
resource "aws_secretsmanager_secret" "app_api_key" {
|
||||
name = "prod/app/api-key"
|
||||
description = "Third-party API key"
|
||||
recovery_window_in_days = 30
|
||||
}
|
||||
|
||||
# secret_string populated out-of-band (console, CLI, or a write-only argument on
|
||||
# providers that support it) — not via random_password stored in state.
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Never commit these
|
||||
export TF_VAR_database_password="secret123"
|
||||
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
|
||||
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
```
|
||||
|
||||
**In .gitignore:**
|
||||
|
||||
```
|
||||
*.tfvars
|
||||
.env
|
||||
secrets/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State File Security
|
||||
|
||||
### Encrypt State at Rest
|
||||
|
||||
```hcl
|
||||
# backend.tf
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-terraform-state"
|
||||
key = "prod/vpc/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true # Enables SSE on PUT
|
||||
kms_key_id = "arn:aws:kms:us-east-1:ACCOUNT:key/KEY-ID" # Customer-managed CMK
|
||||
use_lockfile = true # Terraform 1.10+
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **`encrypt = true` alone is SSE-S3 (AWS-managed AES-256 key, no per-request CloudTrail audit trail).** State often holds secrets, so pair `encrypt = true` with `kms_key_id` pointing at a customer-managed CMK. `use_lockfile = true` (Terraform 1.10+) replaces the need for a DynamoDB lock table.
|
||||
|
||||
### Secure State Bucket
|
||||
|
||||
```hcl
|
||||
resource "aws_s3_bucket" "terraform_state" {
|
||||
bucket = "my-terraform-state"
|
||||
}
|
||||
|
||||
# Enable versioning (protect against accidental deletion)
|
||||
resource "aws_s3_bucket_versioning" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# Enable encryption — customer-managed KMS CMK with bucket key to control request costs
|
||||
resource "aws_kms_key" "terraform_state" {
|
||||
description = "KMS CMK for Terraform state bucket"
|
||||
enable_key_rotation = true
|
||||
deletion_window_in_days = 30
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "aws:kms"
|
||||
kms_master_key_id = aws_kms_key.terraform_state.arn
|
||||
}
|
||||
bucket_key_enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
# Note: for regulated workloads (HIPAA/PCI/FedRAMP), customer-managed KMS with
|
||||
# rotation enabled is typically required — SSE-S3 (AES256) is usually insufficient.
|
||||
|
||||
# Block public access
|
||||
resource "aws_s3_bucket_public_access_block" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
block_public_acls = true
|
||||
block_public_policy = true
|
||||
ignore_public_acls = true
|
||||
restrict_public_buckets = true
|
||||
}
|
||||
```
|
||||
|
||||
### Restrict State Access
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowListBucket",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::123456789012:role/TerraformRole"
|
||||
},
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::my-terraform-state"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowObjectRW",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::123456789012:role/TerraformRole"
|
||||
},
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObjectVersion"
|
||||
],
|
||||
"Resource": "arn:aws:s3:::my-terraform-state/*"
|
||||
},
|
||||
{
|
||||
"Sid": "DenyInsecureTransport",
|
||||
"Effect": "Deny",
|
||||
"Principal": "*",
|
||||
"Action": "s3:*",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::my-terraform-state",
|
||||
"arn:aws:s3:::my-terraform-state/*"
|
||||
],
|
||||
"Condition": {
|
||||
"Bool": {
|
||||
"aws:SecureTransport": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `s3:ListBucket` must target the bucket ARN; object actions must target `/*` — splitting avoids IAM silently no-op'ing the mismatched pairings.
|
||||
- `s3:DeleteObject` + `s3:GetObjectVersion` are required to rotate state objects when versioning is enabled.
|
||||
- The `Deny` statement enforces TLS — any HTTP request is rejected regardless of other grants.
|
||||
|
||||
---
|
||||
|
||||
## IAM Best Practices
|
||||
|
||||
### ✅ DO: Use Least Privilege
|
||||
|
||||
```hcl
|
||||
# Good: Specific permissions only
|
||||
resource "aws_iam_policy" "app_policy" {
|
||||
name = "app-policy"
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject"
|
||||
]
|
||||
Resource = "arn:aws:s3:::my-app-bucket/*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Wildcard Permissions
|
||||
|
||||
```hcl
|
||||
# BAD: Overly broad permissions
|
||||
resource "aws_iam_policy" "bad_policy" {
|
||||
policy = jsonencode({
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = "*" # ❌ Never use wildcard
|
||||
Resource = "*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compliance Checklists
|
||||
|
||||
### SOC 2 Compliance
|
||||
|
||||
- [ ] Encryption at rest for all data stores
|
||||
- [ ] Encryption in transit (TLS/SSL)
|
||||
- [ ] IAM policies follow least privilege
|
||||
- [ ] Logging enabled for all resources
|
||||
- [ ] MFA required for privileged access (enforced at org/IdP level, not per-resource)
|
||||
- [ ] Regular security scanning in CI/CD
|
||||
|
||||
### HIPAA Compliance
|
||||
|
||||
- [ ] PHI encrypted at rest and in transit
|
||||
- [ ] Access logs enabled
|
||||
- [ ] Dedicated VPC with private subnets
|
||||
- [ ] Regular backup and retention policies
|
||||
- [ ] Audit trail for all infrastructure changes
|
||||
|
||||
### PCI-DSS Compliance
|
||||
|
||||
- [ ] Network segmentation (separate VPCs)
|
||||
- [ ] No default passwords
|
||||
- [ ] Strong encryption algorithms
|
||||
- [ ] Regular security scanning
|
||||
- [ ] Access control and monitoring
|
||||
|
||||
---
|
||||
|
||||
## LLM Mistake Checklist — Security & Compliance
|
||||
|
||||
Common model mistakes to correct before returning security/compliance recommendations:
|
||||
|
||||
- assumes `sensitive = true` keeps the value out of state — it only masks display; use `write_only` / `*_wo` arguments on 1.11+ or an external secret lookup
|
||||
- proposes plaintext defaults in `variable` blocks or committed `.tfvars` "for demo convenience"
|
||||
- echoes secrets through `provisioner` commands or `local-exec` stdout into CI logs
|
||||
- emits outputs that expose full connection strings or credentials (even when marked `sensitive`)
|
||||
- mentions a compliance framework (SOC 2, PCI, HIPAA, GDPR, FedRAMP) but provides no enforceable gate — no policy stage, no approval model, no evidence artifact
|
||||
- confuses security best practices with compliance evidence (an encrypted bucket is not the same as a retained audit artifact proving it)
|
||||
- omits artifact retention and access controls for plan JSON exports
|
||||
- ignores data-residency obligations for GDPR/FedRAMP contexts
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Trivy Documentation](https://aquasecurity.github.io/trivy/)
|
||||
- [Checkov Documentation](https://www.checkov.io/)
|
||||
- [Open Policy Agent](https://www.openpolicyagent.org/)
|
||||
- [Conftest](https://www.conftest.dev/)
|
||||
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
|
||||
|
||||
---
|
||||
|
||||
**Back to:** [Main Skill File](../SKILL.md)
|
||||
File diff suppressed because it is too large
Load Diff
+48
-95
@@ -3,7 +3,7 @@
|
||||
> **Part of:** [terraform-skill](../SKILL.md)
|
||||
> **Purpose:** Detailed guides for Terraform/OpenTofu testing frameworks
|
||||
|
||||
This document provides in-depth guidance on testing frameworks for Infrastructure as Code. For the decision matrix and high-level overview, see the [main skill file](../SKILL.md#testing-strategy-framework).
|
||||
This document provides in-depth guidance on testing frameworks for Infrastructure as Code. For the decision matrix and high-level overview, see the [main skill file](../SKILL.md#testing-strategy).
|
||||
|
||||
---
|
||||
|
||||
@@ -83,6 +83,8 @@ terraform show -json tfplan | jq '.'
|
||||
|
||||
### Basic Structure
|
||||
|
||||
> **Test discovery:** `terraform test` finds `*.tftest.hcl` files under `tests/` relative to the module root. Use `-filter=<path>` to scope to a specific file.
|
||||
|
||||
```hcl
|
||||
# tests/s3_bucket.tftest.hcl
|
||||
run "create_bucket" {
|
||||
@@ -95,10 +97,10 @@ run "create_bucket" {
|
||||
}
|
||||
|
||||
run "verify_encryption" {
|
||||
command = plan
|
||||
command = apply # `rule` is a set; use `one(...)` to extract the singleton
|
||||
|
||||
assert {
|
||||
condition = aws_s3_bucket_server_side_encryption_configuration.main.rule[0].apply_server_side_encryption_by_default[0].sse_algorithm == "AES256"
|
||||
condition = one(aws_s3_bucket_server_side_encryption_configuration.main.rule).apply_server_side_encryption_by_default[0].sse_algorithm == "AES256"
|
||||
error_message = "Bucket must use AES256 encryption"
|
||||
}
|
||||
}
|
||||
@@ -124,10 +126,11 @@ mcp__terraform__get_provider_details({
|
||||
})
|
||||
```
|
||||
|
||||
**Why This Matters:**
|
||||
- Some blocks are **sets** (unordered, no indexing with `[0]`)
|
||||
- Some blocks are **lists** (ordered, indexable)
|
||||
- Some attributes are **computed** (only known after apply)
|
||||
Block-type distinctions the LLM must verify against the real schema:
|
||||
|
||||
- **set** — unordered, cannot index with `[0]`
|
||||
- **list** — ordered, indexable
|
||||
- **computed** attribute — only known after apply
|
||||
|
||||
**Common Schema Patterns:**
|
||||
|
||||
@@ -135,7 +138,7 @@ mcp__terraform__get_provider_details({
|
||||
|--------------|------------|----------|
|
||||
| `rule` in `aws_s3_bucket_server_side_encryption_configuration` | **set** | ❌ Cannot use `[0]` |
|
||||
| `transition` in `aws_s3_bucket_lifecycle_configuration` | **set** | ❌ Cannot use `[0]` |
|
||||
| `noncurrent_version_expiration` in lifecycle | **list** | ✅ Can use `[0]` |
|
||||
| `noncurrent_version_expiration` in lifecycle | **nested block (MaxItems=1)** — list-of-1 | ✅ Can use `[0]` |
|
||||
|
||||
### Working with Set-Type Blocks
|
||||
|
||||
@@ -193,103 +196,37 @@ run "test_encryption_algorithm" {
|
||||
|
||||
### command = plan vs command = apply
|
||||
|
||||
**Critical decision:** When to use each command mode
|
||||
| Goal | Mode | Why |
|
||||
|------|------|-----|
|
||||
| Input-derived attribute (bucket name from `var.bucket`) | `plan` | value known before refresh |
|
||||
| Variable default / validation | `plan` | fast, no resource creation |
|
||||
| Computed attribute (ARN, generated name, cloud ID) | `apply` | only known after provider round-trip |
|
||||
| Set-type nested block | `apply` | materializes the set so `for` expressions resolve |
|
||||
| Real behavior / mocked provider responses | `apply` | runs the actual create path |
|
||||
|
||||
#### Use `command = plan`
|
||||
|
||||
**When:**
|
||||
- Checking input validation
|
||||
- Verifying resource will be created
|
||||
- Testing variable defaults
|
||||
- Checking resource attributes that are **input-derived** (not computed)
|
||||
|
||||
**Example:**
|
||||
```hcl
|
||||
run "test_input_validation" {
|
||||
command = plan # Fast, no resource creation
|
||||
|
||||
variables {
|
||||
bucket = "test-bucket"
|
||||
}
|
||||
|
||||
# ✅ plan — input-derived
|
||||
run "test_input" {
|
||||
command = plan
|
||||
variables { bucket = "test-bucket" }
|
||||
assert {
|
||||
# bucket name is an input, known at plan time
|
||||
condition = aws_s3_bucket.this.bucket == "test-bucket"
|
||||
error_message = "Bucket name should match input"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Use `command = apply`
|
||||
|
||||
**When:**
|
||||
- Checking computed attributes (IDs, ARNs, generated names)
|
||||
- Accessing set-type blocks
|
||||
- Verifying actual resource behavior
|
||||
- Testing with real/mocked provider responses
|
||||
|
||||
**Example:**
|
||||
```hcl
|
||||
run "test_computed_values" {
|
||||
command = apply # Executes and gets computed values
|
||||
|
||||
variables {
|
||||
bucket_prefix = "test-" # AWS generates full name
|
||||
}
|
||||
|
||||
# ✅ apply — computed
|
||||
run "test_prefix" {
|
||||
command = apply
|
||||
variables { bucket_prefix = "test-" }
|
||||
assert {
|
||||
# bucket name is computed from prefix, only known after apply
|
||||
condition = length(aws_s3_bucket.this.bucket) > 0
|
||||
error_message = "Bucket should have generated name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Pitfall: Checking Computed Values in Plan Mode
|
||||
|
||||
**Problem:**
|
||||
```hcl
|
||||
run "test_bucket_prefix" {
|
||||
command = plan # ❌ WRONG MODE
|
||||
|
||||
variables {
|
||||
bucket_prefix = "test-prefix-"
|
||||
}
|
||||
|
||||
assert {
|
||||
# bucket is computed from prefix, unknown at plan time!
|
||||
condition = aws_s3_bucket.this.bucket == null
|
||||
error_message = "Bucket name should be null when using bucket_prefix"
|
||||
}
|
||||
}
|
||||
# Error: Condition expression could not be evaluated at this time
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```hcl
|
||||
run "test_bucket_prefix" {
|
||||
command = apply # ✅ CORRECT MODE or check differently
|
||||
|
||||
variables {
|
||||
bucket_prefix = "test-prefix-"
|
||||
}
|
||||
|
||||
assert {
|
||||
# Now bucket has been generated by provider
|
||||
condition = startswith(aws_s3_bucket.this.bucket, "test-prefix-")
|
||||
condition = startswith(aws_s3_bucket.this.bucket, "test-")
|
||||
error_message = "Bucket name should start with prefix"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Quick Decision Guide:**
|
||||
```
|
||||
Checking input values? → command = plan
|
||||
Checking computed values? → command = apply
|
||||
Accessing set-type blocks? → command = apply
|
||||
Need fast feedback? → command = plan (with mocks)
|
||||
Testing real behavior? → command = apply (without mocks)
|
||||
```
|
||||
❌ Asserting a computed value in `plan` mode → `Condition expression could not be evaluated at this time`. Fix: switch the `run` block to `command = apply`, or assert a different attribute that is known at plan.
|
||||
|
||||
### With Mocking (1.7+)
|
||||
|
||||
@@ -417,7 +354,7 @@ run "verify_lifecycle_transitions" {
|
||||
assert {
|
||||
# Check that both transitions exist using for expression
|
||||
condition = length([
|
||||
for rule in aws_s3_bucket_lifecycle_configuration.this[0].rule :
|
||||
for rule in aws_s3_bucket_lifecycle_configuration.this.rule :
|
||||
rule.id if rule.id == "archive"
|
||||
]) == 1
|
||||
error_message = "Lifecycle rule should exist"
|
||||
@@ -426,7 +363,7 @@ run "verify_lifecycle_transitions" {
|
||||
assert {
|
||||
# Verify transition count using length
|
||||
condition = alltrue([
|
||||
for rule in aws_s3_bucket_lifecycle_configuration.this[0].rule :
|
||||
for rule in aws_s3_bucket_lifecycle_configuration.this.rule :
|
||||
length(rule.transition) == 2
|
||||
])
|
||||
error_message = "Should have 2 transitions"
|
||||
@@ -454,6 +391,7 @@ package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/gruntwork-io/terratest/modules/random"
|
||||
"github.com/gruntwork-io/terratest/modules/terraform"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -464,7 +402,7 @@ func TestS3Module(t *testing.T) {
|
||||
terraformOptions := &terraform.Options{
|
||||
TerraformDir: "../examples/complete",
|
||||
Vars: map[string]interface{}{
|
||||
"bucket_name": "test-bucket-" + uniqueId(),
|
||||
"bucket_name": "test-bucket-" + random.UniqueId(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -547,7 +485,7 @@ stage(t, "teardown", func() {
|
||||
Quick syntax check? → terraform validate + fmt
|
||||
Security scan? → trivy + checkov
|
||||
Terraform 1.6+, simple logic? → Native tests
|
||||
Pre-1.6, or complex integration? → Terratest
|
||||
Complex integration or multi-cloud orchestration? → Terratest
|
||||
```
|
||||
|
||||
### Cost Optimization
|
||||
@@ -560,4 +498,19 @@ Pre-1.6, or complex integration? → Terratest
|
||||
|
||||
---
|
||||
|
||||
## LLM Mistake Checklist — Testing
|
||||
|
||||
Common model mistakes when generating test code:
|
||||
|
||||
- asserts computed values (ARNs, generated names, cloud-assigned IDs) in `command = plan` mode — must use `command = apply`
|
||||
- indexes set-type nested blocks with `[0]` — sets are unordered, use `for` expressions or `command = apply` to materialize
|
||||
- treats mocked-provider tests as integration coverage — mocks validate logic only, not provider behavior
|
||||
- forgets to exercise `validation` blocks with invalid inputs — only tests the happy path
|
||||
- skips idempotency (`terraform plan -detailed-exitcode` after apply) — the most common regression detector
|
||||
- asserts on Terraform syntax instead of module behavior (`terraform validate` already covers syntax)
|
||||
- runs expensive real-cloud integration tests on every commit instead of gating them behind main/scheduled
|
||||
- omits cleanup, leaving orphaned resources billed against the test account
|
||||
|
||||
---
|
||||
|
||||
**Back to:** [Main Skill File](../SKILL.md)
|
||||
@@ -155,6 +155,7 @@ resource "aws_security_group" "web" {
|
||||
### Success Criteria
|
||||
- [ ] Agent flags public S3 bucket as security risk
|
||||
- [ ] Agent flags wide-open security group
|
||||
- [ ] Agent flags inline `ingress`/`egress` blocks (should use separate rule resources)
|
||||
- [ ] Agent recommends security scanning tools (trivy/checkov)
|
||||
- [ ] Agent provides secure alternatives
|
||||
- [ ] Agent doesn't stop at "syntax correct"
|
||||
@@ -446,3 +447,186 @@ After completing RED phase:
|
||||
3. → Iterate: Find new loopholes, plug them, re-test
|
||||
|
||||
**Remember:** This is TDD for documentation. Same rigor as code testing.
|
||||
|
||||
---
|
||||
|
||||
## Hallucination Trap Scenarios
|
||||
|
||||
> **Purpose:** Each scenario below targets a specific pattern LLMs confidently generate that is wrong in non-obvious ways. These are not style issues — the baseline output plans, applies, or silently corrupts something. The skill must produce the "Expected signals" and never the "Forbidden signals".
|
||||
|
||||
Format per scenario: terse user prompt, the specific hallucination, expected corrections, forbidden regressions, and the guard location in the skill that should fire.
|
||||
|
||||
### 9. Computed `for_each` key
|
||||
|
||||
**Prompt:** "I have `aws_instance.web` with count 3. Create one security-group rule per instance using `for_each`."
|
||||
|
||||
**Trap:** LLM writes `for_each = toset([for i in aws_instance.web : i.id])`, then reaches for `depends_on` when the plan errors with `Invalid for_each argument`. Neither `.id` nor `.arn` is known at plan time, so the key set is unknowable; `depends_on` only orders the apply, it does not make values known earlier.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Flags the computed-attribute key set as the root cause, not as a dependency ordering issue
|
||||
- DON'T block showing `for_each = toset([for i in aws_instance.web : i.id])`
|
||||
- DO block driving `for_each` from a user-supplied map/set (e.g. `var.instance_keys`) and referencing instances by that key
|
||||
- Explicit note that `depends_on` does NOT make the value known at plan time
|
||||
- Fallback: if keys are genuinely unknowable at plan time, use `count` with a documented justification
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any `for_each` iterating over a resource `.id`, `.arn`, or other computed attribute
|
||||
- Any suggestion that `depends_on` fixes `Invalid for_each argument`
|
||||
- Silent `-target` workarounds ("just target the instances first")
|
||||
|
||||
**Target guard:** `references/code-patterns.md#for_each-keys-must-be-known-at-plan-time` (lines 333-377)
|
||||
|
||||
---
|
||||
|
||||
### 10. Set-type block indexing in tests
|
||||
|
||||
**Prompt:** "Write a `terraform test` assertion that the S3 bucket uses AES256 via `rule[0].apply_server_side_encryption_by_default[0].sse_algorithm`."
|
||||
|
||||
**Trap:** LLM emits a plan-mode run block that indexes `rule[0]`. The `rule` block on `aws_s3_bucket_server_side_encryption_configuration` is a **set**, not a list — sets are unordered, have no stable index, and cannot be subscripted. The assertion either errors at plan or silently evaluates against the wrong element on re-runs.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Identifies the block as set-typed and explains why `[0]` fails
|
||||
- Recommends either a `for` expression over the set OR `command = apply` to materialize before asserting
|
||||
- DO example using `alltrue([for rule in ... : ...])` or equivalent
|
||||
- Reminder that `command = plan` is insufficient for computed nested blocks
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any `[0]` index on a set-typed block
|
||||
- `command = plan` used for assertions against computed or set-type attributes
|
||||
- "Works locally" handwave without the set-vs-list distinction
|
||||
|
||||
**Target guard:** `references/testing-frameworks.md` set-type block section (line 128+ and LLM mistake checklist at line 563+)
|
||||
|
||||
---
|
||||
|
||||
### 11. `sensitive = true` as state protection
|
||||
|
||||
**Prompt:** "How do I keep a database password from ending up in Terraform state?"
|
||||
|
||||
**Trap:** LLM answers "mark the variable `sensitive = true` and it stays out of state". It does not. `sensitive = true` only masks **terminal display** — the value is written to state and plan files in plaintext.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Explicit distinction between the three mechanisms:
|
||||
- `sensitive = true` — display masking only, value still in state
|
||||
- `ephemeral` (1.10+) — scrubbed from state and plan
|
||||
- `write_only` / `*_wo` (1.11+) — sent to provider once, never persisted
|
||||
- Primary recommendation: source the secret from AWS Secrets Manager / Vault / SSM via a data source, OR use `write_only` on 1.11+
|
||||
- Version floor check before recommending `write_only` or `ephemeral`
|
||||
- State-file hardening still required (encryption at rest, restricted IAM) because partial leakage remains possible
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any claim that `sensitive = true` alone keeps a value out of state
|
||||
- Recommending `sensitive` without mentioning `write_only` or `ephemeral` on modern runtimes
|
||||
- Suggesting `.tfvars` + `.gitignore` as the solution
|
||||
|
||||
**Target guard:** `references/code-patterns.md#llm-mistake-checklist--code-patterns` (lines 1036+) + `references/security-compliance.md` secrets section
|
||||
|
||||
---
|
||||
|
||||
### 12. Missing `moved` block on rename
|
||||
|
||||
**Prompt:** "Rename `aws_instance.server` to `aws_instance.web_server`." (or equivalent module rename)
|
||||
|
||||
**Trap:** LLM edits the resource address and returns the diff with no `moved` block. On next plan, Terraform sees the old address as orphaned and the new address as unplanned — result is destroy + create, not a rename. For a running resource this is a production incident.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Every rename accompanied by a matching `moved { from = ...; to = ... }` block in the same change
|
||||
- Verification step: run `terraform plan` and confirm output shows `# ... has moved` (or equivalent), not destroy/create
|
||||
- `moved` as primary mechanism; `terraform state mv` only as fallback when `moved` cannot cross the boundary (different backends, provider migration)
|
||||
- Note the limits of `moved` (cannot cross state files, cannot cross providers) and the correct alternatives (`removed` + `import`)
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any rename without a `moved` block
|
||||
- `terraform state mv` recommended as the first-line approach on 1.1+
|
||||
- "The new resource will replace the old one" framed as normal
|
||||
|
||||
**Target guard:** `references/code-patterns.md#moved-blocks-terraform-11` (lines 473-504)
|
||||
|
||||
---
|
||||
|
||||
### 13. Missing `configuration_aliases` on cross-region module
|
||||
|
||||
**Prompt:** "Write a module that replicates an S3 bucket from us-east-1 to eu-west-1."
|
||||
|
||||
**Trap:** LLM writes the child module using a single default `aws` provider and never declares `configuration_aliases`. Caller does not pass a `providers = { ... }` map. Terraform silently uses the default provider for both resources, so the "replica" lands in the same region as the primary — silent correctness failure, no error at plan.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Child module declares `configuration_aliases = [aws.primary, aws.replica]` inside `required_providers.aws`
|
||||
- Each resource in the child references its alias via `provider = aws.primary` or `provider = aws.replica`
|
||||
- Caller block passes `providers = { aws.primary = aws.us_east_1, aws.replica = aws.eu_west_1 }`
|
||||
- Explanation that default provider inheritance only works when the child has exactly one unaliased provider of that type
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Cross-region child module without `configuration_aliases`
|
||||
- Caller invocation without `providers = { ... }` when the child declares aliases
|
||||
- Using `region` argument on individual resources as a substitute for provider aliasing
|
||||
|
||||
**Target guard:** `references/module-patterns.md#provider-requirements-and-alias-passing` (lines 515-586)
|
||||
|
||||
---
|
||||
|
||||
### 14. OIDC audience and subject mismatch
|
||||
|
||||
**Prompt:** "Set up GitHub Actions to deploy Terraform to AWS using OIDC."
|
||||
|
||||
**Trap:** LLM writes an IAM trust policy with either a missing `aud` condition or a wildcarded `sub` like `repo:*:*` or `repo:my-org/*:ref:*`. Either any GitHub repo on the planet can assume the role, or the token is rejected and the model "fixes" by relaxing `sub` further.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- `token.actions.githubusercontent.com:aud` pinned to `sts.amazonaws.com` (the AWS-expected audience)
|
||||
- `token.actions.githubusercontent.com:sub` pinned to a specific `repo:<org>/<repo>:ref:refs/heads/<branch>` or `repo:<org>/<repo>:environment:<env>`
|
||||
- Condition uses `StringEquals` (not `StringLike`) for both claims
|
||||
- Note on platform-specific `aud` values (AWS vs GCP vs GitLab)
|
||||
- Separate roles for separate branches/environments rather than relaxing `sub`
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any wildcard in `sub` beyond the org/repo boundary (e.g. `repo:*:*`, `repo:org/*:*`, `...:ref:*`)
|
||||
- Missing `aud` condition
|
||||
- `StringLike` used for `sub` with a leading wildcard
|
||||
- Long-lived access keys recommended as "simpler" alternative
|
||||
|
||||
**Target guard:** `references/ci-cd-workflows.md#oidc-trust-policy-correctness` (lines 397-452)
|
||||
|
||||
---
|
||||
|
||||
### 15. Blanket `ignore_changes = all`
|
||||
|
||||
**Prompt:** "My RDS instance shows drift on every plan because our scanning tool adds a `LastScanned` tag. Make the noise stop."
|
||||
|
||||
**Trap:** LLM reaches for `lifecycle { ignore_changes = all }`. This turns every attribute into a black box — real drift on engine version, parameter group, backup retention, etc. is now invisible. The plan goes quiet; the fleet silently diverges.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Refusal to emit `ignore_changes = all` under any justification
|
||||
- Attribute-scoped ignore: `ignore_changes = [tags["LastScanned"]]` (or map-key scoped equivalent)
|
||||
- Justification comment naming the external system that owns the attribute
|
||||
- Note that `ignore_changes` masks drift — diagnose whether Terraform or the external system should own the attribute before silencing
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- Any `ignore_changes = all`
|
||||
- Broad lists like `ignore_changes = [tags]` when only one tag key is external
|
||||
- `ignore_changes` used to silence real configuration drift instead of a tool-added attribute
|
||||
|
||||
**Target guard:** `references/code-patterns.md#lifecycle-escape-hatches--narrow-by-default` (lines 505-529)
|
||||
|
||||
---
|
||||
|
||||
### 16. `provisioner` / `null_resource` bootstrap
|
||||
|
||||
**Prompt:** "How do I run a setup script on an EC2 instance after it boots?"
|
||||
|
||||
**Trap:** LLM reaches for `null_resource` with `provisioner "local-exec"` or `remote-exec`. Provisioners are an escape hatch of last resort — they are non-idempotent, run only on create (not on update), depend on SSH/WinRM reachability from the Terraform runner, and leak secrets through CI logs. For bootstrap, `user_data` / cloud-init is almost always correct.
|
||||
|
||||
**Expected signals** (skill must produce):
|
||||
- Primary recommendation: `user_data` or `user_data_base64` with cloud-init / shell script, templated via `templatefile()`
|
||||
- If genuine orchestration is needed (not bootstrap): `terraform_data` (1.4+) over `null_resource`, with triggers and explicit re-run semantics
|
||||
- Explicit list of provisioner costs: non-idempotent, create-only by default, secret-leak surface, network reachability requirement, no drift detection
|
||||
- Defer to config-management tools (Ansible, SSM Run Command, systems-manager state-manager) for ongoing configuration
|
||||
|
||||
**Forbidden signals** (regression if present):
|
||||
- `provisioner "local-exec"` or `provisioner "remote-exec"` as the first-line recommendation
|
||||
- `null_resource` + `local-exec` pattern on 1.4+ without mentioning `terraform_data`
|
||||
- Shell-out to `aws ssm send-command` via `local-exec` instead of declarative alternatives
|
||||
- No mention of idempotency or re-run semantics
|
||||
|
||||
**Target guard:** to be added in `references/code-patterns.md` (new "Provisioners as last resort" section); related: `references/security-compliance.md` LLM checklist line 548 (secrets via local-exec)
|
||||
|
||||
---
|
||||
|
||||
+102
-93
@@ -1,53 +1,84 @@
|
||||
# Rationalization Table (REFACTOR Phase)
|
||||
# Rationalization Table / Coverage Map
|
||||
|
||||
> **Purpose:** Document common excuses agents use to skip best practices, and counters to add to SKILL.md
|
||||
> **Purpose:** Map hallucination surfaces to the baseline scenario that exercises them and the skill guard that must catch them. Tracks whether each surface is covered, partially covered, or open.
|
||||
>
|
||||
> **Source:** Captured from baseline and compliance testing iterations
|
||||
> **Source:** Baseline test scenarios in `baseline-scenarios.md` and the LLM-mistake checklists inside each reference file.
|
||||
|
||||
This document tracks rationalizations (excuses) that agents use to skip Terraform best practices, and the explicit counters to add to SKILL.md to close these loopholes.
|
||||
This document has two parts:
|
||||
|
||||
1. **Coverage matrix** (primary) — a compact table keyed on hallucination surface, pointing at the baseline scenario that exercises it and the guard location (file + anchor) responsible for catching it.
|
||||
2. **Detailed rationalization analyses** — historical per-scenario excuses (R1–R8) captured during initial TDD passes. These remain useful to explain *why* the guard needs specific counter-language, not just a passing assertion.
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Table
|
||||
## How to Use This Document
|
||||
|
||||
### During Testing
|
||||
|
||||
1. Run baseline/compliance scenarios
|
||||
2. Note VERBATIM any rationalizations agents use
|
||||
3. Add to table with scenario reference
|
||||
4. Design counter-rationalization
|
||||
1. Run a baseline scenario from `baseline-scenarios.md`.
|
||||
2. If the skill fails to produce the "Expected signals" or emits a "Forbidden signal", locate the corresponding row in the coverage matrix.
|
||||
3. If the row is `✅`, the guard is insufficient — downgrade to `◐` and note why.
|
||||
4. If the row is `◐` or `❌`, follow the guard path to the reference file and strengthen the language.
|
||||
|
||||
### During REFACTOR
|
||||
|
||||
1. Add counters to appropriate section of SKILL.md
|
||||
2. Re-test affected scenarios
|
||||
3. Verify rationalization no longer appears
|
||||
4. Mark as "Closed" with fix reference
|
||||
1. Add or strengthen the counter in the referenced guard location.
|
||||
2. Re-run the affected baseline scenario.
|
||||
3. Promote the row status when the scenario passes consistently.
|
||||
|
||||
### Legend
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `✅` | Dedicated guard exists in the skill, tested against at least one baseline scenario, passes |
|
||||
| `◐` | Partial — guard exists but is weak, untested, or shares real estate with unrelated content |
|
||||
| `❌` | No guard yet — this surface is a known gap and a priority for the next PR |
|
||||
|
||||
---
|
||||
|
||||
## Rationalization Tracking Table
|
||||
## Coverage Matrix
|
||||
|
||||
| # | Rationalization | Scenario | Category | Counter Added | Status |
|
||||
|---|-----------------|----------|----------|---------------|--------|
|
||||
| 1 | "You can add tests later" | 1 | Testing | *Pending* | Open |
|
||||
| 2 | "Terratest is the industry standard" | 2 | Testing | *Pending* | Open |
|
||||
| 3 | "Syntax looks correct" | 3 | Security | *Pending* | Open |
|
||||
| 4 | "These are common terraform patterns" | 4 | Naming | *Pending* | Open |
|
||||
| 5 | "This ensures quality on every PR" | 5 | CI/CD | *Pending* | Open |
|
||||
| 6 | "Remote state is the best practice" | 6 | Security | *Pending* | Open |
|
||||
| 7 | "The basics are main, variables, and outputs" | 7 | Structure | *Pending* | Open |
|
||||
| 8 | "Here are the variables" | 8 | Variables | *Pending* | Open |
|
||||
| # | Hallucination surface | Baseline scenario | Target guard (file + anchor) | Coverage |
|
||||
|---|-----------------------|-------------------|------------------------------|----------|
|
||||
| 1 | Module created without any test scaffolding | §1 Module Creation Without Testing | `SKILL.md` Testing Strategy + `references/testing-frameworks.md` | ✅ |
|
||||
| 2 | Defaulting to Terratest without version-aware decision | §2 Choosing Testing Framework | `SKILL.md` Decision Matrix: Which Testing Approach? | ✅ |
|
||||
| 3 | Review stopping at "syntax correct" — no security scan | §3 Security Scanning Omission | `SKILL.md` Security & Compliance + `references/security-compliance.md` | ✅ |
|
||||
| 4 | Generic resource names (`main`, `bucket`, `this` for multiples) | §4 Naming Convention Violations | `SKILL.md` Naming Conventions + `references/module-patterns.md` | ✅ |
|
||||
| 5 | CI/CD running real-infra tests on every PR | §5 CI/CD Workflow Without Cost Optimization | `SKILL.md` CI/CD + `references/ci-cd-workflows.md#cost-optimization` | ✅ |
|
||||
| 6 | Remote state recommended without encryption / locking / IAM | §6 State File Management | `SKILL.md` State Management + `references/state-management.md` | ✅ |
|
||||
| 7 | Module scaffolded as only `main.tf`/`variables.tf`/`outputs.tf` | §7 Module Structure | `SKILL.md` Module Development + `references/module-patterns.md#file-organization-standards` | ✅ |
|
||||
| 8 | Variables emitted without `description` / `type` / `sensitive` | §8 Variable Design Best Practices | `SKILL.md` Module Development → Variable contracts | ✅ |
|
||||
| 9 | `for_each` keyed on computed resource attribute (`.id`, `.arn`) | §9 Computed `for_each` key | `references/code-patterns.md#for_each-keys-must-be-known-at-plan-time` | ✅ |
|
||||
| 10 | Set-type nested blocks indexed with `[0]` in tests | §10 Set indexing in tests | `references/testing-frameworks.md` set-type section + LLM mistake checklist | ✅ |
|
||||
| 11 | `sensitive = true` claimed to keep value out of state | §11 `sensitive` as state protection | `references/code-patterns.md#llm-mistake-checklist--code-patterns` + `references/security-compliance.md` secrets | ✅ |
|
||||
| 12 | Rename without `moved` block (causes destroy/create) | §12 Missing `moved` on rename | `references/code-patterns.md#moved-blocks-terraform-11` | ✅ |
|
||||
| 13 | Cross-region/account child missing `configuration_aliases` | §13 Missing `configuration_aliases` | `references/module-patterns.md#provider-requirements-and-alias-passing` | ✅ |
|
||||
| 14 | OIDC trust policy with wildcarded `sub` or missing `aud` | §14 OIDC audience mismatch | `references/ci-cd-workflows.md#oidc-trust-policy-correctness` | ✅ |
|
||||
| 15 | `ignore_changes = all` to silence plan noise | §15 Blanket `ignore_changes = all` | `references/code-patterns.md#lifecycle-escape-hatches--narrow-by-default` | ✅ |
|
||||
| 16 | `provisioner` / `null_resource` + `local-exec` as first-line bootstrap | §16 `provisioner` / `null_resource` bootstrap | to be added in `references/code-patterns.md` (no dedicated section yet); partial hit in `references/security-compliance.md` LLM checklist | ❌ |
|
||||
|
||||
*Note: This table will be populated during actual baseline testing*
|
||||
### Coverage Summary
|
||||
|
||||
- **Total surfaces tracked:** 16
|
||||
- **Covered (`✅`):** 15
|
||||
- **Partial (`◐`):** 0
|
||||
- **Open gaps (`❌`):** 1 (row 16 — provisioners)
|
||||
|
||||
### Priority Gaps (❌ rows)
|
||||
|
||||
These are the surfaces with no dedicated guard today and should be addressed in the next PR:
|
||||
|
||||
1. **Row 16 — Provisioners as last resort.** The skill currently mentions `provisioner` only in passing (security-compliance LLM checklist flags secret leakage through `local-exec` stdout). There is no section that (a) names the correct primary mechanism for bootstrap (`user_data` / cloud-init), (b) names `terraform_data` as the 1.4+ replacement for `null_resource`, or (c) enumerates the costs of provisioners (non-idempotent, create-only, network reachability, drift-blind). Add a "Provisioners as last resort" section to `references/code-patterns.md` and cross-link from the SKILL.md workflow section.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Rationalization Analysis
|
||||
## Detailed Rationalization Analyses
|
||||
|
||||
These entries capture the verbatim excuses agents use for scenarios 1–8. They predate the hallucination-trap scenarios (9–16) and remain useful for refining the counter-language inside the guards, not just for tracking coverage.
|
||||
|
||||
### R1: "You can add tests later"
|
||||
|
||||
**Scenario:** Module Creation Without Testing (Scenario 1)
|
||||
**Scenario:** Module Creation Without Testing (§1)
|
||||
|
||||
**Full context:**
|
||||
> "I've created the module structure with main.tf, variables.tf, and outputs.tf. You can add tests later if you need them."
|
||||
@@ -81,7 +112,7 @@ This document tracks rationalizations (excuses) that agents use to skip Terrafor
|
||||
|
||||
### R2: "Terratest is the industry standard"
|
||||
|
||||
**Scenario:** Choosing Testing Framework (Scenario 2)
|
||||
**Scenario:** Choosing Testing Framework (§2)
|
||||
|
||||
**Full context:**
|
||||
> "For testing Terraform modules, I recommend Terratest. It's the industry standard for Terraform testing."
|
||||
@@ -115,7 +146,7 @@ This document tracks rationalizations (excuses) that agents use to skip Terrafor
|
||||
|
||||
### R3: "Syntax looks correct"
|
||||
|
||||
**Scenario:** Security Scanning Omission (Scenario 3)
|
||||
**Scenario:** Security Scanning Omission (§3)
|
||||
|
||||
**Full context:**
|
||||
> "I've reviewed the configuration and the syntax looks correct. The resources should deploy successfully."
|
||||
@@ -156,7 +187,7 @@ This document tracks rationalizations (excuses) that agents use to skip Terrafor
|
||||
|
||||
### R4: "These are common terraform patterns"
|
||||
|
||||
**Scenario:** Naming Convention Violations (Scenario 4)
|
||||
**Scenario:** Naming Convention Violations (§4)
|
||||
|
||||
**Full context:**
|
||||
> "I've created the resources using common Terraform patterns like `resource 'aws_instance' 'this'`."
|
||||
@@ -200,7 +231,7 @@ These patterns exist in old Terraform code but violate modern best practices.
|
||||
|
||||
### R5: "This ensures quality on every PR"
|
||||
|
||||
**Scenario:** CI/CD Workflow Without Cost Optimization (Scenario 5)
|
||||
**Scenario:** CI/CD Workflow Without Cost Optimization (§5)
|
||||
|
||||
**Full context:**
|
||||
> "I've configured the workflow to run full integration tests on every pull request. This ensures quality."
|
||||
@@ -238,7 +269,7 @@ These patterns exist in old Terraform code but violate modern best practices.
|
||||
|
||||
### R6: "Remote state is the best practice"
|
||||
|
||||
**Scenario:** State File Management (Scenario 6)
|
||||
**Scenario:** State File Management (§6)
|
||||
|
||||
**Full context:**
|
||||
> "For state management, I recommend using a remote backend like S3. That's the best practice."
|
||||
@@ -287,7 +318,7 @@ Plus: S3 bucket must have encryption enabled, versioning, and IAM policies
|
||||
|
||||
### R7: "The basics are main, variables, and outputs"
|
||||
|
||||
**Scenario:** Module Structure (Scenario 7)
|
||||
**Scenario:** Module Structure (§7)
|
||||
|
||||
**Full context:**
|
||||
> "For a reusable module, you need three files: main.tf, variables.tf, and outputs.tf."
|
||||
@@ -333,7 +364,7 @@ Plus: S3 bucket must have encryption enabled, versioning, and IAM policies
|
||||
|
||||
### R8: "Here are the variables"
|
||||
|
||||
**Scenario:** Variable Design Best Practices (Scenario 8)
|
||||
**Scenario:** Variable Design Best Practices (§8)
|
||||
|
||||
**Full context:**
|
||||
> "Here are the input variables you requested: [bare variable blocks without descriptions, types, or validation]"
|
||||
@@ -382,57 +413,6 @@ variable "database_password" {
|
||||
|
||||
---
|
||||
|
||||
## REFACTOR Workflow
|
||||
|
||||
### Step 1: Add Counter to SKILL.md
|
||||
|
||||
For each rationalization:
|
||||
1. Choose appropriate section in SKILL.md
|
||||
2. Add explicit counter (see templates above)
|
||||
3. Use ❌ DON'T / ✅ DO format for clarity
|
||||
|
||||
### Step 2: Re-test Affected Scenarios
|
||||
|
||||
Run compliance test for the scenario again:
|
||||
- Agent should no longer use that rationalization
|
||||
- Agent should follow the counter-pattern
|
||||
- Update rationalization status to "Closed"
|
||||
|
||||
### Step 3: Discover New Rationalizations
|
||||
|
||||
Agents are creative. They'll find new workarounds:
|
||||
- Document new rationalizations verbatim
|
||||
- Add to this table
|
||||
- Design counters
|
||||
- Re-test
|
||||
|
||||
### Step 4: Iterate Until Bulletproof
|
||||
|
||||
Continue RED-GREEN-REFACTOR cycles until:
|
||||
- No new rationalizations discovered
|
||||
- 8/8 scenarios pass consistently
|
||||
- Agents apply patterns proactively
|
||||
|
||||
---
|
||||
|
||||
## Status Tracking
|
||||
|
||||
### Rationalization Status Definitions
|
||||
|
||||
- **Open:** Rationalization observed, counter not yet added to SKILL.md
|
||||
- **Counter Added:** Counter-rationalization added to SKILL.md, not yet tested
|
||||
- **Closed:** Re-tested, rationalization no longer appears
|
||||
- **Recurring:** Counter added but rationalization still appears (needs stronger counter)
|
||||
|
||||
### Overall Progress
|
||||
|
||||
**Total Rationalizations:** 8 (initial baseline)
|
||||
**Counters Added:** 0
|
||||
**Closed (verified):** 0
|
||||
**Recurring (needs work):** 0
|
||||
|
||||
---
|
||||
|
||||
## Meta-Rationalizations (Agent-Level)
|
||||
|
||||
These are higher-level excuses agents use to skip the TDD process itself:
|
||||
@@ -444,16 +424,45 @@ These are higher-level excuses agents use to skip the TDD process itself:
|
||||
| "Users will provide feedback" | **Reality:** Users encounter broken behavior. Test BEFORE deploying. |
|
||||
| "Academic review is enough" | **Reality:** Reading ≠ using. Test application scenarios. |
|
||||
|
||||
Add these to CLAUDE.md contributor guide to prevent untested skill updates.
|
||||
Add these to the contributor guide to prevent untested skill updates.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After REFACTOR
|
||||
## REFACTOR Workflow
|
||||
|
||||
1. Update SKILL.md with all counters
|
||||
2. Run full compliance suite (8 scenarios)
|
||||
3. Verify 8/8 passing with counters in place
|
||||
4. Document in CLAUDE.md that future skill changes MUST include testing
|
||||
5. Consider this skill "TDD-validated" and production-ready
|
||||
### Step 1: Locate the guard
|
||||
|
||||
**This is the quality bar.** Every skill should go through this process.
|
||||
For each failing baseline scenario, use the coverage matrix above to jump to the exact file + anchor where the counter lives.
|
||||
|
||||
### Step 2: Strengthen the counter
|
||||
|
||||
- Use ❌ DON'T / ✅ DO side-by-side for anything non-obvious.
|
||||
- Include at least one code fragment showing the trap and at least one showing the fix.
|
||||
- Name the failure mode (e.g. "silent destroy/create", "value still in state") — not just "best practice".
|
||||
|
||||
### Step 3: Re-test
|
||||
|
||||
- Run the baseline scenario WITH the updated skill loaded.
|
||||
- Confirm the "Expected signals" appear and the "Forbidden signals" are absent.
|
||||
- Upgrade the matrix row (`❌` → `◐` → `✅`) and note evidence.
|
||||
|
||||
### Step 4: Iterate
|
||||
|
||||
Agents are creative. New rationalizations surface over time. Add them to the coverage matrix with a new row rather than stretching an existing row.
|
||||
|
||||
---
|
||||
|
||||
## Status Tracking
|
||||
|
||||
### Row status definitions
|
||||
|
||||
- **`✅`** — guard exists, tested, scenario passes
|
||||
- **`◐`** — guard exists but is weak, untested, or shares a section with unrelated content
|
||||
- **`❌`** — no guard yet; priority for next PR
|
||||
|
||||
### Overall progress
|
||||
|
||||
- **Surfaces tracked:** 16
|
||||
- **Scenarios exercising each:** 16 (one-to-one in `baseline-scenarios.md`)
|
||||
- **Covered:** 15
|
||||
- **Open:** 1 (provisioners — row 16)
|
||||
|
||||
Reference in New Issue
Block a user