Repository: hashicorp/agent-skills Branch: main Commit: 43ca9b0cde13 Files: 53 Total size: 335.7 KB Directory structure: gitextract_z9sttyor/ ├── .claude-plugin/ │ └── marketplace.json ├── .copywrite.hcl ├── .github/ │ ├── .tessl/ │ │ └── skill-review-cache.json │ └── workflows/ │ ├── tessl-skill-review-comment.yml │ ├── tessl-skill-review.yml │ └── validate.yml ├── AGENTS.md ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── packer/ │ ├── README.md │ ├── builders/ │ │ ├── .claude-plugin/ │ │ │ └── plugin.json │ │ └── skills/ │ │ ├── aws-ami-builder/ │ │ │ └── SKILL.md │ │ ├── azure-image-builder/ │ │ │ └── SKILL.md │ │ └── windows-builder/ │ │ └── SKILL.md │ └── hcp/ │ ├── .claude-plugin/ │ │ └── plugin.json │ └── skills/ │ └── push-to-registry/ │ └── SKILL.md ├── scripts/ │ └── validate-structure.sh └── terraform/ ├── README.md ├── code-generation/ │ ├── .claude-plugin/ │ │ └── plugin.json │ └── skills/ │ ├── azure-verified-modules/ │ │ └── SKILL.md │ ├── terraform-search-import/ │ │ ├── SKILL.md │ │ ├── references/ │ │ │ └── MANUAL-IMPORT.md │ │ └── scripts/ │ │ └── list_resources.sh │ ├── terraform-style-guide/ │ │ ├── SECURITY.md │ │ └── SKILL.md │ └── terraform-test/ │ ├── SKILL.md │ └── references/ │ ├── CI_CD.md │ ├── EXAMPLES.md │ └── MOCK_PROVIDERS.md ├── module-generation/ │ ├── .claude-plugin/ │ │ └── plugin.json │ └── skills/ │ ├── refactor-module/ │ │ └── SKILL.md │ └── terraform-stacks/ │ ├── SKILL.md │ └── references/ │ ├── api-monitoring.md │ ├── component-blocks.md │ ├── deployment-blocks.md │ ├── examples.md │ ├── linked-stacks.md │ └── troubleshooting.md └── provider-development/ ├── .claude-plugin/ │ └── plugin.json └── skills/ ├── new-terraform-provider/ │ ├── SKILL.md │ └── assets/ │ └── main.go ├── provider-actions/ │ └── SKILL.md ├── provider-docs/ │ ├── SKILL.md │ ├── agents/ │ │ └── openai.yaml │ └── references/ │ └── hashicorp-provider-docs.md ├── provider-resources/ │ └── SKILL.md ├── provider-test-patterns/ │ ├── SKILL.md │ └── references/ │ ├── checks.md │ ├── ephemeral.md │ └── sweepers.md └── run-acceptance-tests/ └── SKILL.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude-plugin/marketplace.json ================================================ { "name": "hashicorp", "owner": { "name": "HashiCorp" }, "metadata": { "description": "Official HashiCorp plugins and skills for Claude Code", "version": "1.0.0" }, "plugins": [ { "name": "terraform-code-generation", "source": "./terraform/code-generation", "description": "Terraform code generation skills including HCL generation, style guides, and testing.", "version": "1.0.0", "author": { "name": "HashiCorp" }, "keywords": ["terraform", "hcl", "infrastructure", "iac", "testing", "style-guide"], "category": "integration", "license": "MPL-2.0", "strict": false }, { "name": "terraform-module-generation", "source": "./terraform/module-generation", "description": "Terraform module generation and refactoring skills including module design and Terraform Stacks.", "version": "1.0.0", "author": { "name": "HashiCorp" }, "keywords": ["terraform", "modules", "infrastructure", "iac", "stacks", "refactoring"], "category": "integration", "license": "MPL-2.0", "strict": false }, { "name": "terraform-provider-development", "source": "./terraform/provider-development", "description": "Terraform provider development skills including resources, data sources, actions, and acceptance testing.", "version": "1.0.0", "author": { "name": "HashiCorp" }, "keywords": ["terraform", "provider", "plugin-framework", "resources", "testing"], "category": "integration", "license": "MPL-2.0", "strict": false }, { "name": "packer-builders", "source": "./packer/builders", "description": "Packer builder skills for AWS, Azure, and Windows image creation.", "version": "1.0.0", "author": { "name": "HashiCorp" }, "keywords": ["packer", "aws", "azure", "windows", "ami", "image", "builder"], "category": "integration", "license": "MPL-2.0", "strict": false }, { "name": "packer-hcp", "source": "./packer/hcp", "description": "HCP Packer registry integration for tracking and managing image metadata.", "version": "1.0.0", "author": { "name": "HashiCorp" }, "keywords": ["packer", "hcp-packer", "registry", "metadata", "image-tracking"], "category": "integration", "license": "MPL-2.0", "strict": false } ] } ================================================ FILE: .copywrite.hcl ================================================ schema_version = 1 project { license = "MPL-2.0" copyright_year = 2025 # (OPTIONAL) A list of globs that should not have copyright/license headers. # Supports doublestar glob patterns for more flexibility in defining which # files or folders should be ignored header_ignore = [ # "vendor/**", # "**autogen**", ] } ================================================ FILE: .github/.tessl/skill-review-cache.json ================================================ { "version": "1", "last_updated": "2026-02-24T00:00:00Z", "skills": {} } ================================================ FILE: .github/workflows/tessl-skill-review-comment.yml ================================================ # Companion workflow to tessl-skill-eval.yml. # # The main workflow (Tessl Skill Review) runs on pull_request events, which # don't have write access to the PR (especially for fork PRs). To work around # this, the main workflow saves the review results and the PR number as # artifacts. This workflow triggers on workflow_run (after the main workflow # completes), downloads those artifacts, and posts/updates the PR comment # with pull-requests: write permission. # # The PR number is read from the artifact file (pr-comment/pr_number) that # was written by the main workflow using github.event.pull_request.number. name: Post Tessl Review Comment on: workflow_run: workflows: ["Tessl Skill Review"] types: [completed] permissions: pull-requests: write jobs: post-comment: name: Post PR Comment runs-on: ubuntu-latest if: github.event.workflow_run.event == 'pull_request' steps: # Download the artifact produced by the main workflow's review-skills job. # run-id ties this to the specific workflow run that triggered us. - name: Download skill-review-comment artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: skill-review-comment path: skill-review-comment run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} # Read the PR number and comment body from the artifact, then create or # update the PR comment. Uses an HTML comment marker () # to find and update an existing comment instead of posting duplicates. - name: Post skill review comment uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const fs = require('fs'); const prNumber = parseInt(fs.readFileSync('skill-review-comment/pr_number', 'utf8').trim()); const body = fs.readFileSync('skill-review-comment/comment.md', 'utf8'); const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, }); const existing = comments.find(c => c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: body, }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: body, }); } ================================================ FILE: .github/workflows/tessl-skill-review.yml ================================================ name: Tessl Skill Review on: pull_request: branches: [main] paths: - '**/SKILL.md' - '**/skills/**' - '.github/workflows/tessl-skill-review.yml' push: branches: [main] paths: - '**/SKILL.md' - '**/skills/**' workflow_dispatch: permissions: contents: write # Required for cache commits jobs: review-skills: name: Review Skills runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '20' - name: Install Tessl CLI run: npm install -g @tessl/cli - name: Detect changed skills id: detect env: EVENT_NAME: ${{ github.event_name }} BASE_REF: ${{ github.base_ref }} run: | if [[ "$EVENT_NAME" == "pull_request" ]]; then CHANGED_SKILLS=$(git diff --name-only --diff-filter=ACMR \ "origin/${BASE_REF}"...HEAD \ -- '**/SKILL.md' '**/skills/**' | \ grep 'SKILL.md$' | \ xargs -I {} dirname {} | \ sort -u) else # workflow_dispatch: find all skills CHANGED_SKILLS=$(find . -name "SKILL.md" -not -path "./node_modules/*" -not -path "./.git/*" | \ xargs -I {} dirname {} | \ sed 's|^\\./||' | \ sort -u) fi if [[ -z "$CHANGED_SKILLS" ]]; then echo "No skill changes detected." echo "skills=" >> "$GITHUB_OUTPUT" else echo "Skills to review:" echo "$CHANGED_SKILLS" EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "skills<<${EOF_MARKER}" >> "$GITHUB_OUTPUT" echo "$CHANGED_SKILLS" >> "$GITHUB_OUTPUT" echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT" fi - name: Read review cache if: steps.detect.outputs.skills != '' id: cache run: | CACHE_FILE=".github/.tessl/skill-review-cache.json" if [[ -f "$CACHE_FILE" ]]; then echo "Cache file found, loading..." if CACHE_CONTENT=$(cat "$CACHE_FILE" 2>&1); then # Validate JSON if echo "$CACHE_CONTENT" | jq empty 2>/dev/null; then echo "cache_exists=true" >> "$GITHUB_OUTPUT" # Export cache to environment for review step EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "REVIEW_CACHE<<${EOF_MARKER}" >> "$GITHUB_ENV" echo "$CACHE_CONTENT" >> "$GITHUB_ENV" echo "${EOF_MARKER}" >> "$GITHUB_ENV" else echo "::warning::Cache file is invalid JSON, ignoring" echo "cache_exists=false" >> "$GITHUB_OUTPUT" fi else echo "::warning::Cache file exists but cannot be read: $CACHE_CONTENT" echo "cache_exists=false" >> "$GITHUB_OUTPUT" fi else echo "No cache file found, will create new one" echo "cache_exists=false" >> "$GITHUB_OUTPUT" fi - name: Run skill reviews if: steps.detect.outputs.skills != '' id: review env: SKILLS: ${{ steps.detect.outputs.skills }} TESSL_API_KEY: ${{ secrets.TESSL_API_KEY }} run: | FAILED=0 TABLE="| Skill | Status | Review Score | Change |" TABLE="${TABLE}\\n|-------|--------|--------------|--------|" DETAILS="" # Create temporary file for cache entries CACHE_FILE_TEMP="cache-entries.tsv" echo "Cache entries file: $CACHE_FILE_TEMP" while IFS= read -r dir; do [[ -z "$dir" ]] && continue echo "::group::Reviewing $dir" # Run review with --json flag JSON_OUTPUT=$(tessl skill review --json "$dir" 2>&1) echo "$JSON_OUTPUT" echo "::endgroup::" # Extract JSON (skip everything before first '{') JSON=$(echo "$JSON_OUTPUT" | sed -n '/{/,$p') # Look up previous score from cache PREV_SCORE="" PREV_DESC="" PREV_CONTENT="" if [[ -n "$REVIEW_CACHE" ]]; then CACHE_ENTRY=$(echo "$REVIEW_CACHE" | jq -r --arg path "$dir" '.skills[$path] // empty') if [[ -n "$CACHE_ENTRY" ]]; then PREV_SCORE=$(echo "$CACHE_ENTRY" | jq -r '.score // empty') PREV_DESC=$(echo "$CACHE_ENTRY" | jq -r '.dimensions.description // empty') PREV_CONTENT=$(echo "$CACHE_ENTRY" | jq -r '.dimensions.content // empty') fi fi # Validate PREV_SCORE is numeric if [[ -n "$PREV_SCORE" && ! "$PREV_SCORE" =~ ^[0-9]+$ ]]; then echo "::warning::Invalid previous score for $dir: $PREV_SCORE, ignoring" PREV_SCORE="" fi # Validate PREV_DESC and PREV_CONTENT are numeric if [[ -n "$PREV_DESC" && ! "$PREV_DESC" =~ ^[0-9]+$ ]]; then echo "::warning::Invalid previous description score for $dir: $PREV_DESC, ignoring" PREV_DESC="" fi if [[ -n "$PREV_CONTENT" && ! "$PREV_CONTENT" =~ ^[0-9]+$ ]]; then echo "::warning::Invalid previous content score for $dir: $PREV_CONTENT, ignoring" PREV_CONTENT="" fi # Extract fields via jq PASSED=$(echo "$JSON" | jq -r '.validation.overallPassed // false') # Calculate average score from all 8 dimensions AVG_SCORE=$(echo "$JSON" | jq -r ' def avg(obj): (obj.scores | to_entries | map(.value.score) | add) / (obj.scores | length) * 100 / 3; ( [(.descriptionJudge.evaluation | avg(.)), (.contentJudge.evaluation | avg(.))] | add / 2 ) | round ') # Validate AVG_SCORE is numeric before arithmetic if [[ ! "$AVG_SCORE" =~ ^[0-9]+$ ]]; then echo "::error::Invalid average score calculated for $dir: $AVG_SCORE" AVG_SCORE=0 fi # Calculate diff CHANGE="" if [[ -n "$PREV_SCORE" ]]; then DIFF=$((AVG_SCORE - PREV_SCORE)) if [[ $DIFF -gt 0 ]]; then CHANGE="🔺 +${DIFF}% (was ${PREV_SCORE}%)" elif [[ $DIFF -lt 0 ]]; then CHANGE="🔻 ${DIFF}% (was ${PREV_SCORE}%)" else CHANGE="➡️ no change" fi fi # Build status column if [[ "$PASSED" == "true" ]]; then STATUS="✅ PASSED" else # Extract first validation error ERROR=$(echo "$JSON" | jq -r ' .validation.checks | map(select(.status != "passed")) | first | .message // "Validation failed" ' | cut -c1-60) STATUS="❌ FAILED — ${ERROR}" FAILED=1 fi DIR_DISPLAY=$(echo "$dir" | tr '|' '/') TABLE="${TABLE}\\n| \`${DIR_DISPLAY}\` | ${STATUS} | ${AVG_SCORE}% | ${CHANGE} |" # Calculate dimension scores for cache and details DESC_SCORE=$(echo "$JSON" | jq -r ' (.descriptionJudge.evaluation.scores | to_entries | map(.value.score) | add) * 100 / ((.descriptionJudge.evaluation.scores | length) * 3) | round ') CONTENT_SCORE=$(echo "$JSON" | jq -r ' (.contentJudge.evaluation.scores | to_entries | map(.value.score) | add) * 100 / ((.contentJudge.evaluation.scores | length) * 3) | round ') # Validate dimension scores if [[ ! "$DESC_SCORE" =~ ^[0-9]+$ ]]; then echo "::warning::Invalid description score for $dir: $DESC_SCORE, using 0" DESC_SCORE=0 fi if [[ ! "$CONTENT_SCORE" =~ ^[0-9]+$ ]]; then echo "::warning::Invalid content score for $dir: $CONTENT_SCORE, using 0" CONTENT_SCORE=0 fi # --- Extract detailed review for collapsible section --- DESC_EVAL=$(echo "$JSON" | jq -r '.descriptionJudge.evaluation | " Description: " + ((.scores | to_entries | map(.value.score) | add) * 100 / ((.scores | length) * 3) | round | tostring) + "%\\n" + (.scores | to_entries | map(" \(.key): \(.value.score)/3 - \(.value.reasoning)") | join("\\n")) + "\\n\\n" + " Assessment: " + .overall_assessment ') CONTENT_EVAL=$(echo "$JSON" | jq -r '.contentJudge.evaluation | " Content: " + ((.scores | to_entries | map(.value.score) | add) * 100 / ((.scores | length) * 3) | round | tostring) + "%\\n" + (.scores | to_entries | map(" \(.key): \(.value.score)/3 - \(.value.reasoning)") | join("\\n")) + "\\n\\n" + " Assessment: " + .overall_assessment ') # Extract suggestions SUGGESTIONS=$(echo "$JSON" | jq -r ' [.descriptionJudge.evaluation.suggestions // [], .contentJudge.evaluation.suggestions // []] | flatten | map("- " + .) | join("\\n") ') # Build collapsible details block DETAILS="${DETAILS}\\n\\n
\\n${DIR_DISPLAY} — ${AVG_SCORE}% (${STATUS#* })\\n\\n" # Show score comparison if previous exists (all three must be valid) if [[ -n "$PREV_SCORE" && -n "$PREV_DESC" && -n "$PREV_CONTENT" ]]; then DETAILS="${DETAILS}**Previous:** ${PREV_SCORE}% (Description: ${PREV_DESC}%, Content: ${PREV_CONTENT}%)\\n" DETAILS="${DETAILS}**Current:** ${AVG_SCORE}% (Description: ${DESC_SCORE}%, Content: ${CONTENT_SCORE}%)\\n\\n" DETAILS="${DETAILS}---\\n\\n" fi DETAILS="${DETAILS}\`\`\`\\n${DESC_EVAL}\\n\\n${CONTENT_EVAL}\\n\`\`\`\\n" if [[ -n "$SUGGESTIONS" ]]; then DETAILS="${DETAILS}\\n**Suggestions:**\\n\\n${SUGGESTIONS}\\n" fi DETAILS="${DETAILS}\\n
" # Calculate content hash if [[ ! -f "$dir/SKILL.md" ]]; then echo "::error::SKILL.md not found for $dir" continue fi CONTENT_HASH=$(shasum -a 256 "$dir/SKILL.md" 2>&1) if [[ $? -ne 0 ]]; then echo "::error::Failed to calculate hash for $dir: $CONTENT_HASH" continue fi CONTENT_HASH="sha256:$(echo "$CONTENT_HASH" | awk '{print $1}')" # Build cache entry (compact to single line) if ! CACHE_ENTRY=$(jq -nc \ --arg score "$AVG_SCORE" \ --arg passed "$PASSED" \ --arg hash "$CONTENT_HASH" \ --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ --arg desc "$DESC_SCORE" \ --arg content "$CONTENT_SCORE" \ '{ score: ($score | tonumber), validation_passed: ($passed == "true"), content_hash: $hash, timestamp: $ts, dimensions: { description: ($desc | tonumber), content: ($content | tonumber) } }'); then echo "::error::Failed to build cache entry for $dir" continue fi # Write cache entry to file (tab-separated: pathjson) printf '%s\t%s\n' "$dir" "$CACHE_ENTRY" >> "$CACHE_FILE_TEMP" done <<< "$SKILLS" # Save cache entries file path for update step echo "CACHE_ENTRIES_FILE=$CACHE_FILE_TEMP" >> "$GITHUB_ENV" echo "Wrote $(wc -l < "$CACHE_FILE_TEMP") cache entries to $CACHE_FILE_TEMP" # Build PR comment body COMMENT_BODY=$(printf '%b' "\\n## Tessl Skill Review Results\\n\\n${TABLE}\\n\\n---\\n\\n### Detailed Review\\n${DETAILS}\\n\\n---\\n_Checks: frontmatter validity, required fields, body structure, examples, line count._\\n_Review score is informational — not used for pass/fail gating._") EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "comment<<${EOF_MARKER}" >> "$GITHUB_OUTPUT" echo "$COMMENT_BODY" >> "$GITHUB_OUTPUT" echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT" echo "$COMMENT_BODY" >> "$GITHUB_STEP_SUMMARY" if [[ "$FAILED" -eq 1 ]]; then echo "::error::One or more skills failed validation checks." exit 1 fi - name: Update review cache if: always() && steps.review.outcome != 'skipped' id: update-cache run: | CACHE_FILE=".github/.tessl/skill-review-cache.json" mkdir -p .github/.tessl # Load existing cache or create new structure if [[ -f "$CACHE_FILE" ]]; then if CACHE=$(cat "$CACHE_FILE" 2>&1); then if ! echo "$CACHE" | jq empty 2>/dev/null; then echo "::warning::Cache file is invalid JSON, recreating" CACHE='{"version":"1","last_updated":"","skills":{}}' fi else echo "::warning::Cache file exists but cannot be read: $CACHE" CACHE='{"version":"1","last_updated":"","skills":{}}' fi else echo "Creating new cache file..." CACHE='{"version":"1","last_updated":"","skills":{}}' fi # Update timestamp TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") if ! CACHE=$(echo "$CACHE" | jq --arg ts "$TIMESTAMP" '.last_updated = $ts'); then echo "::error::Failed to update cache timestamp" exit 1 fi # Merge cache updates (using TAB delimiter) MERGED_COUNT=0 FAILED_COUNT=0 if [[ -f "$CACHE_ENTRIES_FILE" ]]; then while IFS=$'\t' read -r skill_path entry_json; do [[ -z "$skill_path" ]] && continue if NEW_CACHE=$(echo "$CACHE" | jq --arg path "$skill_path" --argjson entry "$entry_json" \ '.skills[$path] = $entry' 2>&1); then CACHE="$NEW_CACHE" MERGED_COUNT=$((MERGED_COUNT + 1)) else echo "::warning::Failed to merge cache entry for $skill_path: $NEW_CACHE" FAILED_COUNT=$((FAILED_COUNT + 1)) continue fi done < "$CACHE_ENTRIES_FILE" fi # Write cache file if ! echo "$CACHE" | jq '.' > "$CACHE_FILE"; then echo "::error::Failed to write cache file" exit 1 fi # Report accurate merge counts if [[ $FAILED_COUNT -gt 0 ]]; then echo "Cache updated with $MERGED_COUNT entries ($FAILED_COUNT failed)" else echo "Cache updated with $MERGED_COUNT entries" fi - name: Upload cache file if: always() && steps.update-cache.outcome == 'success' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: skill-review-cache path: .github/.tessl/skill-review-cache.json - name: Upload cache entries artifact if: always() && steps.review.outcome != 'skipped' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: cache-entries path: cache-entries.tsv retention-days: 1 - name: Save PR comment artifact if: >- github.event_name == 'pull_request' && steps.detect.outputs.skills != '' && (steps.review.outcome == 'success' || steps.review.outputs.comment != '') env: COMMENT_BODY: ${{ steps.review.outputs.comment }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | mkdir -p pr-comment echo "$PR_NUMBER" > pr-comment/pr_number echo "$COMMENT_BODY" > pr-comment/comment.md - name: Upload PR comment artifact if: >- github.event_name == 'pull_request' && steps.detect.outputs.skills != '' && (steps.review.outcome == 'success' || steps.review.outputs.comment != '') uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: skill-review-comment path: pr-comment/ commit-cache: name: Commit Cache runs-on: ubuntu-latest needs: review-skills if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Download cache file uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: skill-review-cache - name: Move cache to correct location run: | mkdir -p .github/.tessl mv skill-review-cache.json .github/.tessl/skill-review-cache.json - name: Check for cache changes id: check run: | if git diff --quiet HEAD .github/.tessl/skill-review-cache.json; then echo "changed=false" >> "$GITHUB_OUTPUT" else echo "changed=true" >> "$GITHUB_OUTPUT" fi - name: Commit cache if: steps.check.outputs.changed == 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add .github/.tessl/skill-review-cache.json git commit -m "chore: update skill review cache [skip ci]" if ! git push; then echo "::error::Failed to push cache update to main" exit 1 fi ================================================ FILE: .github/workflows/validate.yml ================================================ name: Validate Structure on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: validate-structure: name: Validate Repository Structure runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Validate structure run: | chmod +x ./scripts/validate-structure.sh ./scripts/validate-structure.sh validate-json: name: Validate JSON Files runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Validate all JSON files run: | echo "Validating all JSON files..." ERRORS=0 for file in $(find . -name "*.json" -not -path "./node_modules/*" -not -path "./.git/*"); do if ! jq empty "$file" 2>/dev/null; then echo "ERROR: Invalid JSON in $file" ERRORS=$((ERRORS + 1)) else echo "OK: $file" fi done if [[ "$ERRORS" -gt 0 ]]; then echo "Found $ERRORS invalid JSON file(s)" exit 1 fi echo "All JSON files are valid!" validate-skills: name: Validate SKILL.md Files runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Check SKILL.md frontmatter run: | echo "Validating SKILL.md files..." ERRORS=0 for skill_file in $(find . -name "SKILL.md" -not -path "./node_modules/*"); do echo "Checking: $skill_file" # Check file starts with frontmatter if ! head -1 "$skill_file" | grep -q "^---"; then echo "ERROR: $skill_file does not start with frontmatter (---)" ERRORS=$((ERRORS + 1)) continue fi # Extract frontmatter FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$skill_file" | sed '1d;$d') # Check for required fields if ! echo "$FRONTMATTER" | grep -q "^name:"; then echo "ERROR: $skill_file missing 'name' in frontmatter" ERRORS=$((ERRORS + 1)) fi if ! echo "$FRONTMATTER" | grep -q "^description:"; then echo "ERROR: $skill_file missing 'description' in frontmatter" ERRORS=$((ERRORS + 1)) fi done if [[ "$ERRORS" -gt 0 ]]; then echo "Found $ERRORS error(s) in SKILL.md files" exit 1 fi echo "All SKILL.md files are valid!" validate-marketplace-references: name: Validate Marketplace References runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Check marketplace plugin references run: | echo "Validating marketplace.json plugin references..." ERRORS=0 MARKETPLACE=".claude-plugin/marketplace.json" if [[ ! -f "$MARKETPLACE" ]]; then echo "ERROR: marketplace.json not found" exit 1 fi # Check each plugin source path exists for source in $(jq -r '.plugins[].source' "$MARKETPLACE"); do # Remove leading ./ path="${source#./}" if [[ ! -d "$path" ]]; then echo "ERROR: Plugin path does not exist: $path" ERRORS=$((ERRORS + 1)) else echo "OK: $path exists" fi # Check plugin.json exists in that path if [[ ! -f "$path/.claude-plugin/plugin.json" ]]; then echo "ERROR: plugin.json not found in $path/.claude-plugin/" ERRORS=$((ERRORS + 1)) else echo "OK: $path/.claude-plugin/plugin.json exists" fi # Check skills directory exists if [[ ! -d "$path/skills" ]]; then echo "ERROR: skills/ directory not found in $path" ERRORS=$((ERRORS + 1)) else echo "OK: $path/skills/ exists" fi done if [[ "$ERRORS" -gt 0 ]]; then echo "Found $ERRORS error(s) in marketplace references" exit 1 fi echo "All marketplace references are valid!" ================================================ FILE: AGENTS.md ================================================ # Agent Instructions This repository contains agent skills and Claude Code plugins for HashiCorp products, including Terraform and Packer for infrastructure-as-code development. ## Repository Structure ``` agent-skills/ ├── terraform/ │ ├── code-generation/ │ │ ├── .claude-plugin/plugin.json │ │ └── skills/ │ │ ├── azure-verified-modules/ │ │ ├── terraform-style-guide/ │ │ ├── terraform-test/ │ │ └── terraform-search-import/ │ ├── module-generation/ │ │ ├── .claude-plugin/plugin.json │ │ └── skills/ │ │ ├── refactor-module/ │ │ └── terraform-stacks/ │ └── provider-development/ │ ├── .claude-plugin/plugin.json │ └── skills/ │ ├── new-terraform-provider/ │ ├── run-acceptance-tests/ │ ├── provider-actions/ │ └── provider-resources/ ├── packer/ │ ├── builders/ │ │ ├── .claude-plugin/plugin.json │ │ └── skills/ │ │ ├── aws-ami-builder/ │ │ ├── azure-image-builder/ │ │ └── windows-builder/ │ └── hcp/ │ ├── .claude-plugin/plugin.json │ └── skills/ │ └── push-to-registry/ ├── .claude-plugin/marketplace.json ├── README.md └── AGENTS.md ``` ## Installation Methods ### Method 1: Claude Code Plugin Installation Install plugins using Claude Code CLI. First add the marketplace, then install plugins: ```bash # Add the agent-skills marketplace claude plugin marketplace add hashicorp/agent-skills # Install plugins claude plugin install terraform-code-generation@hashicorp claude plugin install terraform-module-generation@hashicorp claude plugin install terraform-provider-development@hashicorp claude plugin install packer-builders@hashicorp claude plugin install packer-hcp@hashicorp ``` Or use the interactive interface within Claude Code: ``` /plugin ``` This opens a tabbed interface where you can: - **Discover**: Browse available plugins from all marketplaces - **Installed**: View and manage installed plugins - **Marketplaces**: Add, remove, or update marketplaces Additional plugin management commands: ```bash # Disable a plugin claude plugin disable terraform-code-generation@hashicorp # Re-enable a plugin claude plugin enable terraform-code-generation@hashicorp # Uninstall a plugin claude plugin uninstall terraform-code-generation@hashicorp # Update a plugin claude plugin update terraform-code-generation@hashicorp ``` Installation scopes (use `--scope` flag): - `user` (default): Available across all projects - `project`: Shared with team via `.claude/settings.json` - `local`: Project-specific, gitignored ### Method 2: Individual Skill Installation Install individual skills using `npx skills add`: ```bash # List all available skills npx skills add hashicorp/agent-skills # Code generation skills npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-style-guide npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-test npx skills add hashicorp/agent-skills/terraform/code-generation/skills/azure-verified-modules npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-search-import # Module generation skills npx skills add hashicorp/agent-skills/terraform/module-generation/skills/refactor-module npx skills add hashicorp/agent-skills/terraform/module-generation/skills/terraform-stacks # Provider development skills npx skills add hashicorp/agent-skills/terraform/provider-development/skills/new-terraform-provider npx skills add hashicorp/agent-skills/terraform/provider-development/skills/run-acceptance-tests npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-actions npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-resources # Packer builder skills npx skills add hashicorp/agent-skills/packer/builders/skills/aws-ami-builder npx skills add hashicorp/agent-skills/packer/builders/skills/azure-image-builder npx skills add hashicorp/agent-skills/packer/builders/skills/windows-builder # Packer HCP skills npx skills add hashicorp/agent-skills/packer/hcp/skills/push-to-registry ``` Skills are installed to `~/.claude/skills/` or project `.claude/skills/` directory. ### Method 3: Manual Installation Copy skills directly to your Claude Code configuration: ```bash # Clone the repository git clone https://github.com/hashicorp/agent-skills.git # Copy a plugin to Claude Code plugins directory cp -r agent-skills/terraform/code-generation ~/.claude/plugins/ # Or copy individual skills cp -r agent-skills/terraform/code-generation/skills/terraform-style-guide ~/.claude/skills/ ``` ## Plugin Contents ### terraform-code-generation Skills for generating and validating Terraform HCL code: | Skill | Description | |-------|-------------| | `terraform-style-guide` | Generate Terraform HCL code following HashiCorp style conventions and best practices | | `terraform-test` | Writing and running `.tftest.hcl` test files | | `azure-verified-modules` | Azure Verified Modules (AVM) requirements and certification | | `terraform-search-import` | Discover existing resources with Terraform Search and bulk import into state | ### terraform-module-generation Skills for creating and refactoring Terraform modules: | Skill | Description | |-------|-------------| | `refactor-module` | Transform monolithic configs into reusable modules | | `terraform-stacks` | Multi-region/environment orchestration with Terraform Stacks | ### terraform-provider-development Skills for developing Terraform providers: | Skill | Description | |-------|-------------| | `new-terraform-provider` | Scaffold a new Terraform provider | | `run-acceptance-tests` | Run and debug provider acceptance tests | | `provider-actions` | Implement provider actions (lifecycle operations) | | `provider-resources` | Implement resources and data sources | ### packer-builders Skills for building images on AWS, Azure, and Windows: | Skill | Description | |-------|-------------| | `aws-ami-builder` | Build Amazon Machine Images (AMIs) with amazon-ebs builder | | `azure-image-builder` | Build Azure managed images and Azure Compute Gallery images | | `windows-builder` | Platform-agnostic Windows image patterns with WinRM and PowerShell | ### packer-hcp Skills for HCP Packer registry integration: | Skill | Description | |-------|-------------| | `push-to-registry` | Configure hcp_packer_registry to push build metadata to HCP Packer | ## Skill Format Each skill directory contains: - `SKILL.md` - Main skill definition with YAML frontmatter (`name`, `description`) - Optional `assets/`, `references/`, or `resources/` directories ### SKILL.md Frontmatter ```yaml --- name: skill-name description: Brief description of when to use this skill. --- ``` ## When to Use Each Plugin ### terraform-code-generation Use when: - Writing new Terraform configurations - Reviewing Terraform code for style compliance - Creating test files for Terraform modules - Generating HCL for specific providers ### terraform-module-generation Use when: - Refactoring existing Terraform code into modules - Working with Terraform Stacks - Designing module interfaces and outputs - Managing multi-environment deployments ### terraform-provider-development Use when: - Creating a new Terraform provider - Adding resources or data sources to an existing provider - Implementing provider actions - Running or debugging acceptance tests ### packer-builders Use when: - Building AWS AMIs with amazon-ebs builder - Creating Azure managed images or Azure Compute Gallery images - Building Windows images (AWS, Azure, VMware, etc.) - Setting up WinRM and PowerShell provisioners - Troubleshooting Windows-specific image build issues ### packer-hcp Use when: - Integrating Packer builds with HCP Packer registry - Tracking image metadata and versions - Setting up hcp_packer_registry block - Configuring CI/CD to push to HCP Packer - Querying HCP Packer images in Terraform ## MCP Server Configuration All Terraform plugins include MCP server configuration for the Terraform MCP Server: ```json { "mcpServers": { "terraform": { "command": "docker", "args": ["run", "-i", "--rm", "-e", "TFE_TOKEN", "-e", "TFE_ADDRESS", "hashicorp/terraform-mcp-server"], "env": { "TFE_TOKEN": "${TFE_TOKEN}", "TFE_ADDRESS": "${TFE_ADDRESS}" } } } } ``` Set environment variables for HCP Terraform integration: - `TFE_TOKEN` - HCP Terraform API token - `TFE_ADDRESS` - HCP Terraform address (optional, defaults to app.terraform.io) ## References - [Terraform Documentation](https://developer.hashicorp.com/terraform) - [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) - [Terraform MCP Server](https://github.com/hashicorp/terraform-mcp-server) - [Packer Documentation](https://developer.hashicorp.com/packer) - [HCP Packer](https://developer.hashicorp.com/hcp/docs/packer) - [Packer HCL2 Configuration](https://developer.hashicorp.com/packer/guides/hcl) - [Claude Code Plugins](https://docs.anthropic.com/claude-code/plugins) ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to the HashiCorp Agent Skills. ## Unreleased ### Added - `terraform-search-import` skill for discovering existing resources with Terraform Search and bulk import ## 0.1.0 ### Added - 3 Claude Code plugins with 9 total skills - `terraform-code-generation`: terraform-style-guide, terraform-test, azure-verified-modules - `terraform-module-generation`: refactor-module, terraform-stacks - `terraform-provider-development`: new-terraform-provider, run-acceptance-tests, provider-actions, provider-resources - Marketplace manifest for Claude Code plugin installation - Support for `npx add-skill` installation ================================================ FILE: CODEOWNERS ================================================ # Default owner * @hashicorp/team-proj-mcp-servers ================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # HashiCorp Agent Skills A collection of Agent skills and Claude Code plugins for HashiCorp products. | Product | Use cases | |:--------|:----------| | [Terraform](./terraform/) | Write HCL code, build modules, develop providers, and run tests | | [Packer](./packer/) | Build machine images on AWS, Azure, and Windows; integrate with HCP Packer registry | > **Legal Note:** Your use of a third party MCP Client/LLM is subject solely to the terms of use for such MCP/LLM, and IBM is not responsible for the performance of such third party tools. IBM expressly disclaims any and all warranties and liability for third party MCP Clients/LLMs, and may not be able to provide support to resolve issues which are caused by the third party tools. ## Installation ### Individual Skills Install Agent Skills in GitHub Copilot, Claude Code, Opencode, Cursor, and more: ```bash # List all skills npx skills add hashicorp/agent-skills # Install a specific skill npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-style-guide ``` ### Claude Code Plugin First, add the marketplace, then install plugins: ```bash # Add the HashiCorp marketplace claude plugin marketplace add hashicorp/agent-skills # Install plugins claude plugin install terraform-code-generation@hashicorp claude plugin install terraform-module-generation@hashicorp claude plugin install terraform-provider-development@hashicorp claude plugin install packer-builders@hashicorp claude plugin install packer-hcp@hashicorp ``` Or use the interactive interface: ```bash /plugin ``` ## Structure ``` agent-skills/ ├── .claude-plugin/ │ └── marketplace.json ├── terraform/ # Terraform skills ├── packer/ # Packer skills ├── / # Future products (Vault, Consul, etc.) └── README.md ``` Each product folder contains plugins, and each plugin contains skills: ``` / └── / ├── .claude-plugin/plugin.json └── skills/ └── / └── SKILL.md ``` ## License MPL-2.0 ================================================ FILE: packer/README.md ================================================ # Packer Skills Agent skills for building machine images with Packer and HCP Packer. ## Plugins ### packer-builders Skills for building images on AWS, Azure, and Windows. | Skill | Description | |-------|-------------| | aws-ami-builder | Build Amazon Machine Images (AMIs) with amazon-ebs builder | | azure-image-builder | Build Azure managed images and Azure Compute Gallery images | | windows-builder | Platform-agnostic Windows image patterns with WinRM and PowerShell | ### packer-hcp Skills for HCP Packer registry integration. | Skill | Description | |-------|-------------| | push-to-registry | Configure hcp_packer_registry to push build metadata to HCP Packer | ## Installation ### Claude Code Plugin ```bash claude plugin marketplace add hashicorp/agent-skills claude plugin install packer-builders@hashicorp claude plugin install packer-hcp@hashicorp ``` ### Individual Skills ```bash # Builders npx skills add hashicorp/agent-skills/packer/builders/skills/aws-ami-builder npx skills add hashicorp/agent-skills/packer/builders/skills/azure-image-builder npx skills add hashicorp/agent-skills/packer/builders/skills/windows-builder # HCP Packer npx skills add hashicorp/agent-skills/packer/hcp/skills/push-to-registry ``` ## Structure ``` packer/ ├── builders/ │ ├── .claude-plugin/plugin.json │ └── skills/ │ ├── aws-ami-builder/ │ ├── azure-image-builder/ │ └── windows-builder/ └── hcp/ ├── .claude-plugin/plugin.json └── skills/ └── push-to-registry/ ``` ## References - [Packer Documentation](https://developer.hashicorp.com/packer) - [HCP Packer](https://developer.hashicorp.com/hcp/docs/packer) - [Amazon EBS Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs) - [Azure ARM Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/azure/latest/components/builder/arm) ================================================ FILE: packer/builders/.claude-plugin/plugin.json ================================================ { "name": "packer-builders", "version": "1.0.0", "description": "Packer builder skills for AWS, Azure, and Windows image creation.", "author": { "name": "HashiCorp", "url": "https://github.com/hashicorp" }, "homepage": "https://developer.hashicorp.com/packer", "repository": "https://github.com/hashicorp/agent-skills", "license": "MPL-2.0", "keywords": ["packer", "aws", "azure", "windows", "ami", "image", "builder"] } ================================================ FILE: packer/builders/skills/aws-ami-builder/SKILL.md ================================================ --- name: aws-ami-builder description: Build Amazon Machine Images (AMIs) with Packer using the amazon-ebs builder. Use when creating custom AMIs for EC2 instances. --- # AWS AMI Builder Build Amazon Machine Images (AMIs) using Packer's `amazon-ebs` builder. **Reference:** [Amazon EBS Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs) > **Note:** Building AMIs incurs AWS costs (EC2 instances, EBS storage, data transfer). Builds typically take 10-30 minutes depending on provisioning complexity. ## Basic AMI Template ```hcl packer { required_plugins { amazon = { source = "github.com/hashicorp/amazon" version = "~> 1.3" } } } variable "region" { type = string default = "us-west-2" } locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } source "amazon-ebs" "ubuntu" { region = var.region instance_type = "t3.micro" source_ami_filter { filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" root-device-type = "ebs" virtualization-type = "hvm" } most_recent = true owners = ["099720109477"] # Canonical } ssh_username = "ubuntu" ami_name = "my-app-${local.timestamp}" tags = { Name = "my-app" BuildDate = local.timestamp } } build { sources = ["source.amazon-ebs.ubuntu"] provisioner "shell" { inline = [ "sudo apt-get update", "sudo apt-get upgrade -y", ] } } ``` ## Common Source AMI Filters ### Ubuntu 22.04 LTS ```hcl source_ami_filter { filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" root-device-type = "ebs" virtualization-type = "hvm" } most_recent = true owners = ["099720109477"] # Canonical } ``` ### Amazon Linux 2023 ```hcl source_ami_filter { filters = { name = "al2023-ami-*-x86_64" root-device-type = "ebs" virtualization-type = "hvm" } most_recent = true owners = ["amazon"] } ``` ## Multi-Region AMI ```hcl source "amazon-ebs" "ubuntu" { region = "us-west-2" instance_type = "t3.micro" source_ami_filter { filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" } most_recent = true owners = ["099720109477"] } ssh_username = "ubuntu" ami_name = "my-app-${local.timestamp}" # Copy to additional regions ami_regions = ["us-east-1", "us-east-2", "eu-west-1"] } ``` ## Authentication Packer uses AWS credential resolution: 1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` 2. AWS credentials file: `~/.aws/credentials` 3. IAM instance profile (when running on EC2) ```bash export AWS_ACCESS_KEY_ID="your-access-key" export AWS_SECRET_ACCESS_KEY="your-secret-key" export AWS_REGION="us-west-2" packer build . ``` ## Build Commands ```bash # Initialize plugins packer init . # Validate template packer validate . # Build AMI packer build . # Build with variables packer build -var "region=us-east-1" . ``` ## Common Issues **SSH Timeout** - Ensure security group allows SSH (port 22) - Verify subnet has internet access **AMI Already Exists** - AMI names must be unique - Use timestamp in name: `my-app-${local.timestamp}` **Volume Size Too Small** - Check source AMI's volume size - Set `launch_block_device_mappings.volume_size` accordingly ## References - [Amazon EBS Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs) - [AWS AMI Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) ================================================ FILE: packer/builders/skills/azure-image-builder/SKILL.md ================================================ --- name: azure-image-builder description: Build Azure managed images and Azure Compute Gallery images with Packer. Use when creating custom images for Azure VMs. --- # Azure Image Builder Build Azure managed images and Azure Compute Gallery images using Packer's `azure-arm` builder. **Reference:** [Azure ARM Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/azure/latest/components/builder/arm) > **Note:** Building Azure images incurs costs (compute, storage, data transfer). Builds typically take 15-45 minutes depending on provisioning and OS. ## Basic Managed Image ```hcl packer { required_plugins { azure = { source = "github.com/hashicorp/azure" version = "~> 2.0" } } } variable "client_id" { type = string sensitive = true } variable "client_secret" { type = string sensitive = true } variable "subscription_id" { type = string } variable "tenant_id" { type = string } variable "resource_group" { type = string default = "packer-images-rg" } locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } source "azure-arm" "ubuntu" { client_id = var.client_id client_secret = var.client_secret subscription_id = var.subscription_id tenant_id = var.tenant_id managed_image_resource_group_name = var.resource_group managed_image_name = "my-app-${local.timestamp}" os_type = "Linux" image_publisher = "Canonical" image_offer = "0001-com-ubuntu-server-jammy" image_sku = "22_04-lts-gen2" location = "East US" vm_size = "Standard_B2s" azure_tags = { Name = "my-app" BuildDate = local.timestamp } } build { sources = ["source.azure-arm.ubuntu"] provisioner "shell" { inline = [ "sudo apt-get update", "sudo apt-get upgrade -y", ] } } ``` ## Azure Compute Gallery ```hcl source "azure-arm" "ubuntu" { client_id = var.client_id client_secret = var.client_secret subscription_id = var.subscription_id tenant_id = var.tenant_id os_type = "Linux" image_publisher = "Canonical" image_offer = "0001-com-ubuntu-server-jammy" image_sku = "22_04-lts-gen2" location = "East US" vm_size = "Standard_B2s" shared_image_gallery_destination { resource_group = "gallery-rg" gallery_name = "myImageGallery" image_name = "ubuntu-webapp" image_version = "1.0.${formatdate("YYYYMMDD", timestamp())}" replication_regions = ["East US", "West US 2"] storage_account_type = "Standard_LRS" } } ``` ## Authentication ### Service Principal ```bash # Create service principal az ad sp create-for-rbac \ --name "packer-sp" \ --role Contributor \ --scopes /subscriptions/ # Set environment variables export ARM_CLIENT_ID="" export ARM_CLIENT_SECRET="" export ARM_SUBSCRIPTION_ID="" export ARM_TENANT_ID="" ``` ### Managed Identity ```hcl source "azure-arm" "ubuntu" { use_azure_cli_auth = true subscription_id = var.subscription_id # ... rest of configuration } ``` ## Build Commands ```bash # Set authentication export ARM_CLIENT_ID="your-client-id" export ARM_CLIENT_SECRET="your-client-secret" export ARM_SUBSCRIPTION_ID="your-subscription-id" export ARM_TENANT_ID="your-tenant-id" # Initialize plugins packer init . # Validate template packer validate . # Build image packer build . ``` ## Common Issues **Authentication Failed** - Verify service principal credentials - Ensure Contributor role on resource group - Check subscription and tenant IDs **Compute Gallery Version Exists** - Image versions are immutable - Use unique version numbers with date/build number - Cannot overwrite existing versions **Timeout During Provisioning** - Check network connectivity from build VM - Verify NSG rules allow required traffic - Increase timeout if needed ## References - [Azure ARM Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/azure/latest/components/builder/arm) - [Azure Compute Gallery](https://learn.microsoft.com/en-us/azure/virtual-machines/azure-compute-gallery) ================================================ FILE: packer/builders/skills/windows-builder/SKILL.md ================================================ --- name: windows-builder description: Build Windows images with Packer using WinRM communicator and PowerShell provisioners. Use when creating Windows AMIs, Azure images, or VMware templates. --- # Windows Builder Platform-agnostic patterns for building Windows images with Packer. **Reference:** [Windows Builders](https://developer.hashicorp.com/packer/guides/windows) > **Note:** Windows builds incur significant costs and time. Expect 45-120 minutes per build due to Windows Updates. Failed builds may leave resources running - always verify cleanup. ## WinRM Communicator Setup Windows requires WinRM for Packer communication. ### AWS Example ```hcl source "amazon-ebs" "windows" { region = "us-west-2" instance_type = "t3.medium" source_ami_filter { filters = { name = "Windows_Server-2022-English-Full-Base-*" } most_recent = true owners = ["amazon"] } ami_name = "windows-server-2022-${local.timestamp}" communicator = "winrm" winrm_username = "Administrator" winrm_use_ssl = true winrm_insecure = true winrm_timeout = "15m" user_data_file = "scripts/setup-winrm.ps1" } ``` ### WinRM Setup Script (scripts/setup-winrm.ps1) ```powershell # Configure WinRM winrm quickconfig -q winrm set winrm/config '@{MaxTimeoutms="1800000"}' winrm set winrm/config/service '@{AllowUnencrypted="true"}' winrm set winrm/config/service/auth '@{Basic="true"}' # Configure firewall netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow # Restart WinRM net stop winrm net start winrm ``` ### Azure Example ```hcl source "azure-arm" "windows" { client_id = var.client_id client_secret = var.client_secret subscription_id = var.subscription_id tenant_id = var.tenant_id managed_image_resource_group_name = "images-rg" managed_image_name = "windows-${local.timestamp}" os_type = "Windows" image_publisher = "MicrosoftWindowsServer" image_offer = "WindowsServer" image_sku = "2022-datacenter-g2" location = "East US" vm_size = "Standard_D2s_v3" # Azure auto-configures WinRM communicator = "winrm" winrm_use_ssl = true winrm_insecure = true winrm_timeout = "15m" winrm_username = "packer" } ``` ## PowerShell Provisioners ### Install Software ```hcl build { sources = ["source.amazon-ebs.windows"] # Install Chocolatey provisioner "powershell" { inline = [ "Set-ExecutionPolicy Bypass -Scope Process -Force", "iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" ] } # Install applications provisioner "powershell" { inline = [ "choco install -y googlechrome", "choco install -y 7zip", ] } # Install IIS provisioner "powershell" { inline = [ "Install-WindowsFeature -Name Web-Server -IncludeManagementTools" ] } } ``` ### Windows Updates ```hcl provisioner "powershell" { inline = [ "Install-PackageProvider -Name NuGet -Force", "Install-Module -Name PSWindowsUpdate -Force", "Import-Module PSWindowsUpdate", "Get-WindowsUpdate -Install -AcceptAll -AutoReboot", ] timeout = "2h" } # Wait for reboots provisioner "windows-restart" { restart_timeout = "30m" } ``` ## Cleanup ```hcl provisioner "powershell" { inline = [ "# Clear temp files", "Remove-Item -Path 'C:\\Windows\\Temp\\*' -Recurse -Force -ErrorAction SilentlyContinue", "# Clear Windows Update cache", "Stop-Service -Name wuauserv -Force", "Remove-Item -Path 'C:\\Windows\\SoftwareDistribution\\*' -Recurse -Force -ErrorAction SilentlyContinue", "Start-Service -Name wuauserv", ] } ``` ## Common Issues **WinRM Timeout** - Increase `winrm_timeout` to 15m or more - Verify security group allows ports 5985/5986 - Check user data script completed successfully **PowerShell Execution Policy** ```hcl provisioner "powershell" { inline = [ "Set-ExecutionPolicy Bypass -Scope Process -Force", "# Your commands here", ] } ``` **Long Build Times** - Windows Updates can take 1-2 hours - Use pre-patched base images when available - Set provisioner `timeout = "2h"` ## References - [Packer Windows Builders](https://developer.hashicorp.com/packer/guides/windows) - [WinRM Communicator](https://developer.hashicorp.com/packer/docs/communicators/winrm) - [PowerShell Provisioner](https://developer.hashicorp.com/packer/docs/provisioners/powershell) ================================================ FILE: packer/hcp/.claude-plugin/plugin.json ================================================ { "name": "packer-hcp", "version": "1.0.0", "description": "HCP Packer registry integration for tracking and managing image metadata.", "author": { "name": "HashiCorp", "url": "https://github.com/hashicorp" }, "homepage": "https://developer.hashicorp.com/hcp/docs/packer", "repository": "https://github.com/hashicorp/agent-skills", "license": "MPL-2.0", "keywords": ["packer", "hcp-packer", "registry", "metadata", "image-tracking"] } ================================================ FILE: packer/hcp/skills/push-to-registry/SKILL.md ================================================ --- name: push-to-registry description: Push Packer build metadata to HCP Packer registry for tracking and managing image lifecycle. Use when integrating Packer builds with HCP Packer for version control and governance. --- # Push to HCP Packer Registry Configure Packer templates to push build metadata to HCP Packer registry. **Reference:** [HCP Packer Registry](https://developer.hashicorp.com/hcp/docs/packer) > **Note:** HCP Packer is free for basic use. Builds push metadata only (not actual images), adding minimal overhead (<1 minute). ## Basic Registry Configuration ```hcl packer { required_version = ">= 1.7.7" } variable "image_name" { type = string default = "web-server" } locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } source "amazon-ebs" "ubuntu" { region = "us-west-2" instance_type = "t3.micro" source_ami_filter { filters = { name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" } most_recent = true owners = ["099720109477"] } ssh_username = "ubuntu" ami_name = "${var.image_name}-${local.timestamp}" } build { sources = ["source.amazon-ebs.ubuntu"] hcp_packer_registry { bucket_name = var.image_name description = "Ubuntu 22.04 base image for web servers" bucket_labels = { "os" = "ubuntu" "team" = "platform" } build_labels = { "build-time" = local.timestamp } } provisioner "shell" { inline = [ "sudo apt-get update", "sudo apt-get upgrade -y", ] } } ``` ## Authentication Set environment variables before building: ```bash export HCP_CLIENT_ID="your-service-principal-client-id" export HCP_CLIENT_SECRET="your-service-principal-secret" export HCP_ORGANIZATION_ID="your-org-id" export HCP_PROJECT_ID="your-project-id" packer build . ``` ### Create HCP Service Principal 1. Navigate to HCP → Access Control (IAM) 2. Create Service Principal 3. Grant "Contributor" role on project 4. Generate client secret 5. Save client ID and secret ## Registry Configuration Options ### bucket_name (required) The image identifier. Must stay consistent across builds! ```hcl bucket_name = "web-server" # Keep this constant ``` ### bucket_labels (optional) Metadata at bucket level. Updates with each build. ```hcl bucket_labels = { "os" = "ubuntu" "team" = "platform" "component" = "web" } ``` ### build_labels (optional) Metadata for each iteration. Immutable after build completes. ```hcl build_labels = { "build-time" = local.timestamp "git-commit" = var.git_commit } ``` ## CI/CD Integration ### GitHub Actions ```yaml name: Build and Push to HCP Packer on: push: branches: [main] env: HCP_CLIENT_ID: ${{ secrets.HCP_CLIENT_ID }} HCP_CLIENT_SECRET: ${{ secrets.HCP_CLIENT_SECRET }} HCP_ORGANIZATION_ID: ${{ secrets.HCP_ORGANIZATION_ID }} HCP_PROJECT_ID: ${{ secrets.HCP_PROJECT_ID }} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-packer@main - name: Build and push run: | packer init . packer build \ -var "git_commit=${{ github.sha }}" \ . ``` ## Querying in Terraform ```hcl data "hcp_packer_artifact" "ubuntu" { bucket_name = "web-server" channel_name = "production" platform = "aws" region = "us-west-2" } resource "aws_instance" "web" { ami = data.hcp_packer_artifact.ubuntu.external_identifier instance_type = "t3.micro" tags = { PackerBucket = data.hcp_packer_artifact.ubuntu.bucket_name } } ``` ## Common Issues **Authentication Failed** - Verify HCP_CLIENT_ID and HCP_CLIENT_SECRET - Ensure service principal has Contributor role - Check organization and project IDs **Bucket Name Mismatch** - Keep `bucket_name` consistent across builds - Don't include timestamps in bucket_name - Creates new bucket if name changes **Build Fails** - Packer fails immediately if can't push metadata - Prevents drift between artifacts and registry - Check network connectivity to HCP API ## Best Practices - **Consistent bucket names** - Never change for same image type - **Meaningful labels** - Use for versions, teams, compliance - **CI/CD automation** - Automate builds and registry pushes - **Immutable build labels** - Put changing data (git SHA, date) in build_labels ## References - [HCP Packer Documentation](https://developer.hashicorp.com/hcp/docs/packer) - [hcp_packer_registry Block](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/build/hcp_packer_registry) - [HCP Terraform Provider](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/data-sources/packer_artifact) ================================================ FILE: scripts/validate-structure.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2025, 2026 # SPDX-License-Identifier: MPL-2.0 # # Validates the agent-skills repository structure # Ensures all plugins and skills follow the expected format # set -e REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ERRORS=0 # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_error() { echo -e "${RED}ERROR:${NC} $1" ERRORS=$((ERRORS + 1)) } log_success() { echo -e "${GREEN}OK:${NC} $1" } log_info() { echo -e "${YELLOW}INFO:${NC} $1" } # Check if jq is available if ! command -v jq &> /dev/null; then echo "jq is required but not installed. Please install jq." exit 1 fi echo "==========================================" echo "Validating agent-skills repository structure" echo "==========================================" echo "" # --------------------------------------------- # 1. Validate marketplace.json # --------------------------------------------- echo "1. Checking marketplace.json..." MARKETPLACE_FILE="$REPO_ROOT/.claude-plugin/marketplace.json" if [[ ! -f "$MARKETPLACE_FILE" ]]; then log_error "marketplace.json not found at $MARKETPLACE_FILE" else # Check if valid JSON if ! jq empty "$MARKETPLACE_FILE" 2>/dev/null; then log_error "marketplace.json is not valid JSON" else log_success "marketplace.json is valid JSON" # Check required fields NAME=$(jq -r '.name // empty' "$MARKETPLACE_FILE") if [[ -z "$NAME" ]]; then log_error "marketplace.json missing 'name' field" else log_success "marketplace.json has name: $NAME" fi OWNER=$(jq -r '.owner.name // empty' "$MARKETPLACE_FILE") if [[ -z "$OWNER" ]]; then log_error "marketplace.json missing 'owner.name' field" else log_success "marketplace.json has owner: $OWNER" fi # Check plugins array exists PLUGINS_COUNT=$(jq '.plugins | length' "$MARKETPLACE_FILE") if [[ "$PLUGINS_COUNT" -eq 0 ]]; then log_error "marketplace.json has no plugins defined" else log_success "marketplace.json has $PLUGINS_COUNT plugin(s) defined" fi fi fi echo "" # --------------------------------------------- # 2. Validate each plugin referenced in marketplace # --------------------------------------------- echo "2. Checking plugins referenced in marketplace.json..." if [[ -f "$MARKETPLACE_FILE" ]]; then PLUGIN_SOURCES=$(jq -r '.plugins[].source' "$MARKETPLACE_FILE" 2>/dev/null) for SOURCE in $PLUGIN_SOURCES; do # Remove leading ./ if present SOURCE_PATH="${SOURCE#./}" PLUGIN_DIR="$REPO_ROOT/$SOURCE_PATH" PLUGIN_JSON="$PLUGIN_DIR/.claude-plugin/plugin.json" echo "" log_info "Checking plugin: $SOURCE_PATH" # Check plugin directory exists if [[ ! -d "$PLUGIN_DIR" ]]; then log_error "Plugin directory not found: $PLUGIN_DIR" continue else log_success "Plugin directory exists" fi # Check plugin.json exists if [[ ! -f "$PLUGIN_JSON" ]]; then log_error "plugin.json not found: $PLUGIN_JSON" continue else log_success "plugin.json exists" fi # Validate plugin.json if ! jq empty "$PLUGIN_JSON" 2>/dev/null; then log_error "plugin.json is not valid JSON: $PLUGIN_JSON" continue else log_success "plugin.json is valid JSON" fi # Check required fields in plugin.json PLUGIN_NAME=$(jq -r '.name // empty' "$PLUGIN_JSON") if [[ -z "$PLUGIN_NAME" ]]; then log_error "plugin.json missing 'name' field" else log_success "plugin.json has name: $PLUGIN_NAME" fi PLUGIN_VERSION=$(jq -r '.version // empty' "$PLUGIN_JSON") if [[ -z "$PLUGIN_VERSION" ]]; then log_error "plugin.json missing 'version' field" else log_success "plugin.json has version: $PLUGIN_VERSION" fi PLUGIN_DESC=$(jq -r '.description // empty' "$PLUGIN_JSON") if [[ -z "$PLUGIN_DESC" ]]; then log_error "plugin.json missing 'description' field" else log_success "plugin.json has description" fi # Check skills directory exists SKILLS_DIR="$PLUGIN_DIR/skills" if [[ ! -d "$SKILLS_DIR" ]]; then log_error "skills/ directory not found in plugin: $PLUGIN_DIR" continue else log_success "skills/ directory exists" fi # Validate each skill SKILL_COUNT=0 for SKILL_DIR in "$SKILLS_DIR"/*/; do if [[ -d "$SKILL_DIR" ]]; then SKILL_NAME=$(basename "$SKILL_DIR") SKILL_MD="$SKILL_DIR/SKILL.md" if [[ ! -f "$SKILL_MD" ]]; then log_error "SKILL.md not found for skill: $SKILL_NAME" else # Check SKILL.md has frontmatter if ! head -1 "$SKILL_MD" | grep -q "^---"; then log_error "SKILL.md missing frontmatter (---) for skill: $SKILL_NAME" else # Extract and validate frontmatter FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$SKILL_MD" | sed '1d;$d') # Check for name field if ! echo "$FRONTMATTER" | grep -q "^name:"; then log_error "SKILL.md missing 'name' in frontmatter for skill: $SKILL_NAME" fi # Check for description field if ! echo "$FRONTMATTER" | grep -q "^description:"; then log_error "SKILL.md missing 'description' in frontmatter for skill: $SKILL_NAME" fi if echo "$FRONTMATTER" | grep -q "^name:" && echo "$FRONTMATTER" | grep -q "^description:"; then log_success "SKILL.md valid for skill: $SKILL_NAME" fi fi fi SKILL_COUNT=$((SKILL_COUNT + 1)) fi done if [[ "$SKILL_COUNT" -eq 0 ]]; then log_error "No skills found in $SKILLS_DIR" else log_success "Found $SKILL_COUNT skill(s) in plugin" fi done fi echo "" # --------------------------------------------- # 3. Check for orphaned plugins (not in marketplace) # --------------------------------------------- echo "3. Checking for orphaned plugins..." # Find all plugin.json files FOUND_PLUGINS=$(find "$REPO_ROOT" -path "*/.claude-plugin/plugin.json" -not -path "$REPO_ROOT/.claude-plugin/*" 2>/dev/null) for PLUGIN_JSON in $FOUND_PLUGINS; do PLUGIN_DIR=$(dirname "$(dirname "$PLUGIN_JSON")") RELATIVE_PATH="${PLUGIN_DIR#$REPO_ROOT/}" # Check if this plugin is referenced in marketplace.json if [[ -f "$MARKETPLACE_FILE" ]]; then if ! jq -r '.plugins[].source' "$MARKETPLACE_FILE" | grep -q "$RELATIVE_PATH"; then log_error "Orphaned plugin not in marketplace.json: $RELATIVE_PATH" fi fi done log_success "Orphan check complete" echo "" # --------------------------------------------- # 4. Validate product folder structure # --------------------------------------------- echo "4. Checking product folder structure..." # Get all top-level directories that could be products (excluding hidden and special) for DIR in "$REPO_ROOT"/*/; do DIR_NAME=$(basename "$DIR") # Skip special directories if [[ "$DIR_NAME" == "scripts" ]] || [[ "$DIR_NAME" == "node_modules" ]] || [[ "$DIR_NAME" =~ ^\. ]]; then continue fi # Check if this is a product folder (contains plugin subdirectories) HAS_PLUGINS=false for SUBDIR in "$DIR"/*/; do if [[ -d "$SUBDIR/.claude-plugin" ]]; then HAS_PLUGINS=true break fi done if [[ "$HAS_PLUGINS" == true ]]; then log_success "Valid product folder: $DIR_NAME" fi done echo "" echo "==========================================" echo "Validation complete" echo "==========================================" if [[ "$ERRORS" -gt 0 ]]; then echo -e "${RED}Found $ERRORS error(s)${NC}" exit 1 else echo -e "${GREEN}All checks passed!${NC}" exit 0 fi ================================================ FILE: terraform/README.md ================================================ # Terraform Skills Agent skills for Terraform infrastructure-as-code development. ## Plugins ### terraform-code-generation Skills for generating and validating Terraform HCL code. | Skill | Description | |-------|-------------| | terraform-style-guide | Generate Terraform HCL code following HashiCorp style conventions | | terraform-test | Writing and running `.tftest.hcl` test files | | azure-verified-modules | Azure Verified Modules (AVM) requirements and certification | | terraform-search-import | Discover existing resources with Terraform Search and bulk import | ### terraform-module-generation Skills for creating and refactoring Terraform modules. | Skill | Description | |-------|-------------| | refactor-module | Transform monolithic configs into reusable modules | | terraform-stacks | Multi-region/environment orchestration with Terraform Stacks | ### terraform-provider-development Skills for developing Terraform providers. | Skill | Description | |-------|-------------| | new-terraform-provider | Scaffold a new Terraform provider | | run-acceptance-tests | Run and debug provider acceptance tests | | provider-actions | Implement provider actions (lifecycle operations) | | provider-resources | Implement resources and data sources | | provider-test-patterns | Acceptance test patterns for terraform-plugin-testing | ## Installation ### Claude Code Plugin ```bash claude plugin marketplace add hashicorp/agent-skills claude plugin install terraform-code-generation@hashicorp claude plugin install terraform-module-generation@hashicorp claude plugin install terraform-provider-development@hashicorp ``` ### Individual Skills ```bash # Code generation npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-style-guide npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-test npx skills add hashicorp/agent-skills/terraform/code-generation/skills/azure-verified-modules npx skills add hashicorp/agent-skills/terraform/code-generation/skills/terraform-search-import # Module generation npx skills add hashicorp/agent-skills/terraform/module-generation/skills/refactor-module npx skills add hashicorp/agent-skills/terraform/module-generation/skills/terraform-stacks # Provider development npx skills add hashicorp/agent-skills/terraform/provider-development/skills/new-terraform-provider npx skills add hashicorp/agent-skills/terraform/provider-development/skills/run-acceptance-tests npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-actions npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-resources npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-test-patterns ``` ## MCP Server Relevant Terraform plugins include the `terraform-mcp-server` which provides access to Terraform Cloud/Enterprise APIs. Set the following environment variables: ```bash export TFE_TOKEN="your-terraform-cloud-token" export TFE_ADDRESS="https://app.terraform.io" # or your TFE instance ``` ## Structure ``` terraform/ ├── code-generation/ │ ├── .claude-plugin/plugin.json │ └── skills/ │ ├── terraform-style-guide/ │ ├── terraform-test/ │ ├── azure-verified-modules/ │ └── terraform-search-import/ ├── module-generation/ │ ├── .claude-plugin/plugin.json │ └── skills/ │ ├── terraform-stacks/ │ └── refactor-module/ └── provider-development/ ├── .claude-plugin/plugin.json └── skills/ ├── new-terraform-provider/ ├── provider-actions/ ├── provider-resources/ ├── run-acceptance-tests/ └── provider-test-patterns/ ``` ## References - [Terraform Documentation](https://developer.hashicorp.com/terraform) - [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) - [Terraform MCP Server](https://github.com/hashicorp/terraform-mcp-server) ================================================ FILE: terraform/code-generation/.claude-plugin/plugin.json ================================================ { "name": "terraform-code-generation", "version": "1.0.0", "description": "Terraform code generation skills for Claude Code, including HCL generation, style guides, and testing.", "author": { "name": "HashiCorp", "url": "https://github.com/hashicorp" }, "homepage": "https://developer.hashicorp.com/terraform/language", "repository": "https://github.com/hashicorp/agent-skills", "license": "MPL-2.0", "keywords": ["terraform", "hcl", "infrastructure", "iac", "testing", "style-guide", "search", "import", "discovery"], "mcpServers": { "terraform": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "TFE_TOKEN", "-e", "TFE_ADDRESS", "hashicorp/terraform-mcp-server" ], "env": { "TFE_TOKEN": "${TFE_TOKEN}", "TFE_ADDRESS": "${TFE_ADDRESS}" } } } } ================================================ FILE: terraform/code-generation/skills/azure-verified-modules/SKILL.md ================================================ --- name: azure-verified-modules description: Azure Verified Modules (AVM) requirements and best practices for developing certified Azure Terraform modules. Use when creating or reviewing Azure modules that need AVM certification. --- # Azure Verified Modules (AVM) Requirements This guide covers the mandatory requirements for Azure Verified Modules certification. These requirements ensure consistency, quality, and maintainability across Azure Terraform modules. **References:** - [Azure Verified Modules](https://azure.github.io/Azure-Verified-Modules/) - [AVM Terraform Requirements](https://azure.github.io/Azure-Verified-Modules/specs/terraform/) ## Table of Contents - [Module Cross-Referencing](#module-cross-referencing) - [Azure Provider Requirements](#azure-provider-requirements) - [Code Style Standards](#code-style-standards) - [Variable Requirements](#variable-requirements) - [Output Requirements](#output-requirements) - [Local Values Standards](#local-values-standards) - [Terraform Configuration Requirements](#terraform-configuration-requirements) - [Testing Requirements](#testing-requirements) - [Documentation Requirements](#documentation-requirements) - [Breaking Changes & Feature Management](#breaking-changes--feature-management) - [Contribution Standards](#contribution-standards) - [Compliance Checklist](#compliance-checklist) --- ## Module Cross-Referencing **Severity:** MUST | **Requirement:** TFFR1 When building Resource or Pattern modules, module owners **MAY** cross-reference other modules. However: - Modules **MUST** be referenced using HashiCorp Terraform registry reference to a pinned version - Example: `source = "Azure/xxx/azurerm"` with `version = "1.2.3"` - Modules **MUST NOT** use git references (e.g., `git::https://xxx.yyy/xxx.git` or `github.com/xxx/yyy`) - Modules **MUST NOT** contain references to non-AVM modules --- ## Azure Provider Requirements **Severity:** MUST | **Requirement:** TFFR3 Authors **MUST** only use the following Azure providers: | Provider | Min Version | Max Version | |----------|-------------|-------------| | azapi | >= 2.0 | < 3.0 | | azurerm | >= 4.0 | < 5.0 | **Requirements:** - Authors **MAY** select either Azurerm, Azapi, or both providers - **MUST** use `required_providers` block to enforce provider versions - **SHOULD** use pessimistic version constraint operator (`~>`) **Example:** ```hcl terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } azapi = { source = "Azure/azapi" version = "~> 2.0" } } } ``` --- ## Code Style Standards ### Lower snake_casing **Severity:** MUST | **Requirement:** TFNFR4 **MUST** use lower snake_casing for: - Locals - Variables - Outputs - Resources (symbolic names) - Modules (symbolic names) Example: `snake_casing_example` ### Resource & Data Source Ordering **Severity:** SHOULD | **Requirement:** TFNFR6 - Resources that are depended on **SHOULD** come first - Resources with dependencies **SHOULD** be defined close to each other ### Count & for_each Usage **Severity:** MUST | **Requirement:** TFNFR7 - Use `count` for conditional resource creation - **MUST** use `map(xxx)` or `set(xxx)` as resource's `for_each` collection - The map's key or set's element **MUST** be static literals **Example:** ```hcl resource "azurerm_subnet" "pair" { for_each = var.subnet_map # map(string) name = "${each.value}-pair" resource_group_name = azurerm_resource_group.example.name virtual_network_name = azurerm_virtual_network.example.name address_prefixes = ["10.0.1.0/24"] } ``` ### Resource & Data Block Internal Ordering **Severity:** SHOULD | **Requirement:** TFNFR8 **Order within resource/data blocks:** 1. **Meta-arguments (top)**: - `provider` - `count` - `for_each` 2. **Arguments/blocks (middle, alphabetical)**: - Required arguments - Optional arguments - Required nested blocks - Optional nested blocks 3. **Meta-arguments (bottom)**: - `depends_on` - `lifecycle` (with sub-order: `create_before_destroy`, `ignore_changes`, `prevent_destroy`) Separate sections with blank lines. ### Module Block Ordering **Severity:** SHOULD | **Requirement:** TFNFR9 **Order within module blocks:** 1. **Top meta-arguments**: - `source` - `version` - `count` - `for_each` 2. **Arguments (alphabetical)**: - Required arguments - Optional arguments 3. **Bottom meta-arguments**: - `depends_on` - `providers` ### Lifecycle ignore_changes Syntax **Severity:** MUST | **Requirement:** TFNFR10 The `ignore_changes` attribute **MUST NOT** be enclosed in double quotes. **Good:** ```hcl lifecycle { ignore_changes = [tags] } ``` **Bad:** ```hcl lifecycle { ignore_changes = ["tags"] } ``` ### Null Comparison for Conditional Creation **Severity:** SHOULD | **Requirement:** TFNFR11 For parameters requiring conditional resource creation, wrap with `object` type to avoid "known after apply" issues during plan stage. **Recommended:** ```hcl variable "security_group" { type = object({ id = string }) default = null } ``` ### Dynamic Blocks for Optional Nested Objects **Severity:** MUST | **Requirement:** TFNFR12 Nested blocks under conditions **MUST** use this pattern: ```hcl dynamic "identity" { for_each = ? [] : [] content { # block content } } ``` ### Default Values with coalesce/try **Severity:** SHOULD | **Requirement:** TFNFR13 **Good:** ```hcl coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg") ``` **Bad:** ```hcl var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name ``` ### Provider Declarations in Modules **Severity:** MUST | **Requirement:** TFNFR27 - `provider` **MUST NOT** be declared in modules (except for `configuration_aliases`) - `provider` blocks in modules **MUST** only use `alias` - Provider configurations **SHOULD** be passed in by module users --- ## Variable Requirements ### Not Allowed Variables **Severity:** MUST | **Requirement:** TFNFR14 Module owners **MUST NOT** add variables like `enabled` or `module_depends_on` to control entire module operation. Boolean feature toggles for specific resources are acceptable. ### Variable Definition Order **Severity:** SHOULD | **Requirement:** TFNFR15 Variables **SHOULD** follow this order: 1. All required fields (alphabetical) 2. All optional fields (alphabetical) ### Variable Naming Rules **Severity:** SHOULD | **Requirement:** TFNFR16 - Follow [HashiCorp's naming rules](https://www.terraform.io/docs/extend/best-practices/naming.html) - Feature switches **SHOULD** use positive statements: `xxx_enabled` instead of `xxx_disabled` ### Variables with Descriptions **Severity:** SHOULD | **Requirement:** TFNFR17 - `description` **SHOULD** precisely describe the parameter's purpose and expected data type - Target audience is module users, not developers - For `object` types, use HEREDOC format ### Variables with Types **Severity:** MUST | **Requirement:** TFNFR18 - `type` **MUST** be defined for every variable - `type` **SHOULD** be as precise as possible - `any` **MAY** only be used with adequate reasons - Use `bool` instead of `string`/`number` for true/false values - Use concrete `object` instead of `map(any)` ### Sensitive Data Variables **Severity:** SHOULD | **Requirement:** TFNFR19 If a variable's type is `object` and contains sensitive fields, the entire variable **SHOULD** be `sensitive = true`, or extract sensitive fields into separate variables. ### Non-Nullable Defaults for Collections **Severity:** SHOULD | **Requirement:** TFNFR20 Nullable **SHOULD** be set to `false` for collection values (sets, maps, lists) when using them in loops. For scalar values, null may have semantic meaning. ### Discourage Nullability by Default **Severity:** MUST | **Requirement:** TFNFR21 `nullable = true` **MUST** be avoided unless there's a specific semantic need for null values. ### Avoid sensitive = false **Severity:** MUST | **Requirement:** TFNFR22 `sensitive = false` **MUST** be avoided (this is the default). ### Sensitive Default Value Conditions **Severity:** MUST | **Requirement:** TFNFR23 A default value **MUST NOT** be set for sensitive inputs (e.g., default passwords). ### Handling Deprecated Variables **Severity:** MUST | **Requirement:** TFNFR24 - Move deprecated variables to `deprecated_variables.tf` - Annotate with `DEPRECATED` at the beginning of description - Declare the replacement's name - Clean up during major version releases --- ## Output Requirements ### Additional Terraform Outputs **Severity:** SHOULD | **Requirement:** TFFR2 Authors **SHOULD NOT** output entire resource objects as these may contain sensitive data and the schema can change with API or provider versions. **Best Practices:** - Output *computed* attributes of resources as discrete outputs (anti-corruption layer pattern) - **SHOULD NOT** output values that are already inputs (except `name`) - Use `sensitive = true` for sensitive attributes - For resources deployed with `for_each`, output computed attributes in a map structure **Examples:** ```hcl # Single resource computed attribute output "foo" { description = "MyResource foo attribute" value = azurerm_resource_myresource.foo } # for_each resources output "childresource_foos" { description = "MyResource children's foo attributes" value = { for key, value in azurerm_resource_mychildresource : key => value.foo } } # Sensitive output output "bar" { description = "MyResource bar attribute" value = azurerm_resource_myresource.bar sensitive = true } ``` ### Sensitive Data Outputs **Severity:** MUST | **Requirement:** TFNFR29 Outputs containing confidential data **MUST** be declared with `sensitive = true`. ### Handling Deprecated Outputs **Severity:** MUST | **Requirement:** TFNFR30 - Move deprecated outputs to `deprecated_outputs.tf` - Define new outputs in `outputs.tf` - Clean up during major version releases --- ## Local Values Standards ### locals.tf Organization **Severity:** MAY | **Requirement:** TFNFR31 - `locals.tf` **SHOULD** only contain `locals` blocks - **MAY** declare `locals` blocks next to resources for advanced scenarios ### Alphabetical Local Arrangement **Severity:** MUST | **Requirement:** TFNFR32 Expressions in `locals` blocks **MUST** be arranged alphabetically. ### Precise Local Types **Severity:** SHOULD | **Requirement:** TFNFR33 Use precise types (e.g., `number` for age, not `string`). --- ## Terraform Configuration Requirements ### Terraform Version Requirements **Severity:** MUST | **Requirement:** TFNFR25 **`terraform.tf` requirements:** - **MUST** contain only one `terraform` block - First line **MUST** define `required_version` - **MUST** include minimum version constraint - **MUST** include maximum major version constraint - **SHOULD** use `~> #.#` or `>= #.#.#, < #.#.#` format **Example:** ```hcl terraform { required_version = "~> 1.6" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } } } ``` ### Providers in required_providers **Severity:** MUST | **Requirement:** TFNFR26 - `terraform` block **MUST** contain `required_providers` block - Each provider **MUST** specify `source` and `version` - Providers **SHOULD** be sorted alphabetically - Only include directly required providers - `source` **MUST** be in format `namespace/name` - `version` **MUST** include minimum and maximum major version constraints - **SHOULD** use `~> #.#` or `>= #.#.#, < #.#.#` format --- ## Testing Requirements ### Test Tooling **Severity:** MUST | **Requirement:** TFNFR5 **Required testing tools for AVM:** - Terraform (`terraform validate/fmt/test`) - terrafmt - Checkov - tflint (with azurerm ruleset) - Go (optional for custom tests) ### Test Provider Configuration **Severity:** SHOULD | **Requirement:** TFNFR36 For robust testing, `prevent_deletion_if_contains_resources` **SHOULD** be explicitly set to `false` in test provider configurations. --- ## Documentation Requirements ### Module Documentation Generation **Severity:** MUST | **Requirement:** TFNFR2 - Documentation **MUST** be automatically generated via [Terraform Docs](https://github.com/terraform-docs/terraform-docs) - A `.terraform-docs.yml` file **MUST** be present in the module root --- ## Breaking Changes & Feature Management ### Using Feature Toggles **Severity:** MUST | **Requirement:** TFNFR34 New resources added in minor/patch versions **MUST** have a toggle variable to avoid creation by default: ```hcl variable "create_route_table" { type = bool default = false nullable = false } resource "azurerm_route_table" "this" { count = var.create_route_table ? 1 : 0 # ... } ``` ### Reviewing Potential Breaking Changes **Severity:** MUST | **Requirement:** TFNFR35 **Breaking changes requiring caution:** **Resource blocks:** 1. Adding new resource without conditional creation 2. Adding arguments with non-default values 3. Adding nested blocks without `dynamic` 4. Renaming resources without `moved` blocks 5. Changing `count` to `for_each` or vice versa **Variable/Output blocks:** 1. Deleting/renaming variables 2. Changing variable `type` 3. Changing variable `default` values 4. Changing `nullable` to false 5. Changing `sensitive` from false to true 6. Adding variables without `default` 7. Deleting outputs 8. Changing output `value` 9. Changing output `sensitive` value --- ## Contribution Standards ### GitHub Repository Branch Protection **Severity:** MUST | **Requirement:** TFNFR3 Module owners **MUST** set branch protection policies on the default branch (typically `main`): 1. Require Pull Request before merging 2. Require approval of most recent reviewable push 3. Dismiss stale PR approvals when new commits are pushed 4. Require linear history 5. Prevent force pushes 6. Not allow deletions 7. Require CODEOWNERS review 8. No bypassing settings allowed 9. Enforce for administrators --- ## Compliance Checklist Use this checklist when developing or reviewing Azure Verified Modules: ### Module Structure - [ ] Module cross-references use registry sources with pinned versions - [ ] Azure providers (azurerm/azapi) versions meet AVM requirements - [ ] `.terraform-docs.yml` present in module root - [ ] CODEOWNERS file present ### Code Style - [ ] All names use lower snake_casing - [ ] Resources ordered with dependencies first - [ ] `for_each` uses `map()` or `set()` with static keys - [ ] Resource/data/module blocks follow proper internal ordering - [ ] `ignore_changes` not quoted - [ ] Dynamic blocks used for conditional nested objects - [ ] `coalesce()` or `try()` used for default values ### Variables - [ ] No `enabled` or `module_depends_on` variables - [ ] Variables ordered: required (alphabetical) then optional (alphabetical) - [ ] All variables have precise types (avoid `any`) - [ ] All variables have descriptions - [ ] Collections have `nullable = false` - [ ] No `sensitive = false` declarations - [ ] No default values for sensitive inputs - [ ] Deprecated variables moved to `deprecated_variables.tf` ### Outputs - [ ] Outputs use anti-corruption layer pattern (discrete attributes) - [ ] Sensitive outputs marked `sensitive = true` - [ ] Deprecated outputs moved to `deprecated_outputs.tf` ### Terraform Configuration - [ ] `terraform.tf` has version constraints (`~>` format) - [ ] `required_providers` block present with all providers - [ ] No `provider` declarations in module (except aliases) - [ ] Locals arranged alphabetically ### Testing & Quality - [ ] Required testing tools configured - [ ] New resources have feature toggles - [ ] Breaking changes reviewed and documented --- ## Summary Statistics - **Functional Requirements:** 3 - **Non-Functional Requirements:** 34 - **Total Requirements:** 37 ### By Severity - **MUST:** 21 requirements - **SHOULD:** 14 requirements - **MAY:** 2 requirements --- *Based on: Azure Verified Modules - Terraform Requirements* ================================================ FILE: terraform/code-generation/skills/terraform-search-import/SKILL.md ================================================ --- name: terraform-search-import description: Discover existing cloud resources using Terraform Search queries and bulk import them into Terraform management. Use when bringing unmanaged infrastructure under Terraform control, auditing cloud resources, or migrating to IaC. metadata: copyright: Copyright IBM Corp. 2026 version: "0.1.0" compatibility: Requires Terraform >= 1.14 and providers with list resource support (always use latest provider version) --- # Terraform Search and Bulk Import Discover existing cloud resources using declarative queries and generate configuration for bulk import into Terraform state. **References:** - [Terraform Search - list block](https://developer.hashicorp.com/terraform/language/block/tfquery/list) - [Bulk Import](https://developer.hashicorp.com/terraform/language/import/bulk) ## When to Use - Bringing unmanaged resources under Terraform control - Auditing existing cloud infrastructure - Migrating from manual provisioning to IaC - Discovering resources across multiple regions/accounts ## IMPORTANT: Check Provider Support First **BEFORE starting, you MUST verify the target resource type is supported:** ```bash # Check what list resources are available ./scripts/list_resources.sh aws # Specific provider ./scripts/list_resources.sh # All configured providers ``` ## Decision Tree 1. **Identify target resource type** (e.g., aws_s3_bucket, aws_instance) 2. **Check if supported**: Run `./scripts/list_resources.sh ` 3. **Choose workflow**: - ** If supported**: Check for terraform version available. - ** If terraform version is above 1.14.0** Use Terraform Search workflow (below) - ** If not supported or terraform version is below 1.14.0 **: Use Manual Discovery workflow (see [references/MANUAL-IMPORT.md](references/MANUAL-IMPORT.md)) **Note**: The list of supported resources is rapidly expanding. Always verify current support before using manual import. ## Prerequisites Before writing queries, verify the provider supports list resources for your target resource type. ### Discover Available List Resources Run the helper script to extract supported list resources from your provider: ```bash # From a directory with provider configuration (runs terraform init if needed) ./scripts/list_resources.sh aws # Specific provider ./scripts/list_resources.sh # All configured providers ``` Or manually query the provider schema: ```bash terraform providers schema -json | jq '.provider_schemas | to_entries | map({key: (.key | split("/")[-1]), value: (.value.list_resource_schemas // {} | keys)})' ``` Terraform Search requires an initialized working directory. Ensure you have a configuration with the required provider before running queries: ```hcl # terraform.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } ``` Run `terraform init` to download the provider, then proceed with queries. ## Terraform Search Workflow (Supported Resources Only) 1. Create `.tfquery.hcl` files with `list` blocks defining search queries 2. Run `terraform query` to discover matching resources 3. Generate configuration with `-generate-config-out=` 4. Review and refine generated `resource` and `import` blocks 5. Run `terraform plan` and `terraform apply` to import ## Query File Structure Query files use `.tfquery.hcl` extension and support: - `provider` blocks for authentication - `list` blocks for resource discovery - `variable` and `locals` blocks for parameterization ```hcl # discovery.tfquery.hcl provider "aws" { region = "us-west-2" } list "aws_instance" "all" { provider = aws } ``` ## List Block Syntax ```hcl list "" "" { provider = # Required # Optional: filter configuration (provider-specific) # The `config` block schema is provider-specific. Discover available options using `terraform providers schema -json | jq '.provider_schemas."registry.terraform.io/hashicorp/".list_resource_schemas.""'` config { filter { name = "" values = ["", ""] } region = "" # AWS-specific } # Optional: limit results limit = 100 } ``` ## Supported List Resources Provider support for list resources varies by version. **Always check what's available for your specific provider version using the discovery script.** ## Query Examples ### Basic Discovery ```hcl # Find all EC2 instances in configured region list "aws_instance" "all" { provider = aws } ``` ### Filtered Discovery ```hcl # Find instances by tag list "aws_instance" "production" { provider = aws config { filter { name = "tag:Environment" values = ["production"] } } } # Find instances by type list "aws_instance" "large" { provider = aws config { filter { name = "instance-type" values = ["t3.large", "t3.xlarge"] } } } ``` ### Multi-Region Discovery ```hcl provider "aws" { region = "us-west-2" } locals { regions = ["us-west-2", "us-east-1", "eu-west-1"] } list "aws_instance" "all_regions" { for_each = toset(local.regions) provider = aws config { region = each.value } } ``` ### Parameterized Queries ```hcl variable "target_environment" { type = string default = "staging" } list "aws_instance" "by_env" { provider = aws config { filter { name = "tag:Environment" values = [var.target_environment] } } } ``` ## Running Queries ```bash # Execute queries and display results terraform query # Generate configuration file terraform query -generate-config-out=imported.tf # Pass variables terraform query -var='target_environment=production' ``` ## Query Output Format ``` list.aws_instance.all account_id=123456789012,id=i-0abc123,region=us-west-2 web-server ``` Columns: ` ` ## Generated Configuration The `-generate-config-out` flag creates: ```hcl # __generated__ by Terraform resource "aws_instance" "all_0" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" # ... all attributes } import { to = aws_instance.all_0 provider = aws identity = { account_id = "123456789012" id = "i-0abc123" region = "us-west-2" } } ``` ## Post-Generation Cleanup Generated configuration includes all attributes. Clean up by: 1. Remove computed/read-only attributes 2. Replace hardcoded values with variables 3. Add proper resource naming 4. Organize into appropriate files ```hcl # Before: generated resource "aws_instance" "all_0" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" arn = "arn:aws:ec2:..." # Remove - computed id = "i-0abc123" # Remove - computed # ... many more attributes } # After: cleaned resource "aws_instance" "web_server" { ami = var.ami_id instance_type = var.instance_type subnet_id = var.subnet_id tags = { Name = "web-server" Environment = var.environment } } ``` ## Import by Identity Generated imports use identity-based import (Terraform 1.12+): ```hcl import { to = aws_instance.web provider = aws identity = { account_id = "123456789012" id = "i-0abc123" region = "us-west-2" } } ``` ## Best Practices ### Query Design - Start broad, then add filters to narrow results - Use `limit` to prevent overwhelming output - Test queries before generating configuration ### Configuration Management - Review all generated code before applying - Remove unnecessary default values - Use consistent naming conventions - Add proper variable abstraction ## Troubleshooting | Issue | Solution | |-------|----------| | "No list resources found" | Check provider version supports list resources | | Query returns empty | Verify region and filter values | | Generated config has errors | Remove computed attributes, fix deprecated arguments | | Import fails | Ensure resource not already in state | ## Complete Example ```hcl # main.tf - Initialize provider terraform { required_version = ">= 1.14" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" # Always use latest version } } } # discovery.tfquery.hcl - Define queries provider "aws" { region = "us-west-2" } list "aws_instance" "team_instances" { provider = aws config { filter { name = "tag:Owner" values = ["platform"] } filter { name = "instance-state-name" values = ["running"] } } limit = 50 } ``` ```bash # Execute workflow terraform init terraform query terraform query -generate-config-out=generated.tf # Review and clean generated.tf terraform plan terraform apply ``` ================================================ FILE: terraform/code-generation/skills/terraform-search-import/references/MANUAL-IMPORT.md ================================================ # Manual Terraform Import Reference Use this workflow when your target resource type isn't supported by Terraform Search. ## 1. Discover Resources Using Provider CLI AWS CLI examples: ```bash # RDS instances (not yet supported by Terraform Search) aws rds describe-db-instances --query 'DBInstances[].DBInstanceIdentifier' # DynamoDB tables (not yet supported by Terraform Search) aws dynamodb list-tables --query 'TableNames[]' # API Gateway REST APIs (not yet supported by Terraform Search) aws apigateway get-rest-apis --query 'items[].id' # SNS topics (not yet supported by Terraform Search) aws sns list-topics --query 'Topics[].TopicArn' ``` ## 2. Create Resource Blocks Manually ```hcl # Example for RDS instance resource "aws_db_instance" "existing_db" { identifier = "my-existing-db" # Add other required attributes } # Example for DynamoDB table resource "aws_dynamodb_table" "existing_table" { name = "my-existing-table" # Add other required attributes } # Example for SNS topic resource "aws_sns_topic" "existing_topic" { name = "my-existing-topic" } ``` ## 3. Create Import Blocks (Config-Driven Import) ```hcl # Example for RDS instance resource "aws_db_instance" "existing_db" { identifier = "my-existing-db" # Add other required attributes } import { to = aws_db_instance.existing_db id = "my-existing-db" } # Example for DynamoDB table resource "aws_dynamodb_table" "existing_table" { name = "my-existing-table" # Add other required attributes } import { to = aws_dynamodb_table.existing_table id = "my-existing-table" } ``` ## 4. Run Import Plan ```bash # Plan the import to see what will happen terraform plan # Apply to import the resources terraform apply ``` ## Bulk Import Script Example For multiple resources of the same type: ```bash #!/bin/bash # bulk-import-dynamodb.sh # Get all table names tables=$(aws dynamodb list-tables --query 'TableNames[]' --output text) # Generate import configuration cat > dynamodb-imports.tf << 'EOF' # DynamoDB Table Resources and Imports EOF for table in $tables; do # Create resource and import blocks cat >> dynamodb-imports.tf << EOF resource "aws_dynamodb_table" "table_${table//[-.]/_}" { name = "$table" } import { to = aws_dynamodb_table.table_${table//[-.]/_} id = "$table" } EOF done echo "Generated dynamodb-imports.tf with import blocks" echo "Run 'terraform plan' to review, then 'terraform apply' to import" ``` ================================================ FILE: terraform/code-generation/skills/terraform-search-import/scripts/list_resources.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2025, 2026 # SPDX-License-Identifier: MPL-2.0 # Extract list resources supported by Terraform providers # Usage: ./list_resources.sh [provider_name] # Requires: terraform, jq # Note: Run from an initialized Terraform directory (terraform init) set -e PROVIDER=$1 # Ensure terraform is initialized if [ ! -d ".terraform" ]; then echo "Initializing Terraform..." >&2 terraform init -upgrade > /dev/null 2>&1 fi # Get provider schema and extract list_resource_schemas if [ -n "$PROVIDER" ]; then # Specific provider provider_key=$(terraform providers schema -json 2>/dev/null | jq -r '.provider_schemas | keys[]' | grep "/${PROVIDER}$" || true) if [ -n "$provider_key" ]; then terraform providers schema -json 2>/dev/null | jq -r \ "{\"$PROVIDER\": (.provider_schemas.\"${provider_key}\" | .list_resource_schemas // {} | keys | sort)}" else echo "{\"$PROVIDER\": []}" fi else # All providers terraform providers schema -json 2>/dev/null | jq -r ' .provider_schemas | to_entries | map({key: (.key | split("/")[-1]), value: (.value.list_resource_schemas // {} | keys | sort)}) | from_entries ' fi ================================================ FILE: terraform/code-generation/skills/terraform-style-guide/SECURITY.md ================================================ --- name: terraform-style-guide-security description: Generate Terraform HCL code following HashiCorp's security practices --- # Terraform Style Guide - Security When generating code, apply security hardening: - Enable encryption at rest by default - Configure private networking where applicable - Apply principle of least privilege for security groups - Enable logging and monitoring - Never hardcode credentials or secrets - Mark sensitive outputs with `sensitive = true` - Use `ephemeral` resources and write-only attributes for sensitive data when possible ## Example: Secure S3 Bucket ```hcl resource "aws_s3_bucket" "data" { bucket = "${var.project}-${var.environment}-data" tags = local.common_tags } resource "aws_s3_bucket_versioning" "data" { bucket = aws_s3_bucket.data.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "data" { bucket = aws_s3_bucket.data.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" kms_master_key_id = aws_kms_key.s3.arn } } } resource "aws_s3_bucket_public_access_block" "data" { bucket = aws_s3_bucket.data.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } ``` ## Ephemeral resources Ephemeral resources prevent sensitive data being stored in state. For more information on ephemeral resources, see the [Terraform documentation](https://developer.hashicorp.com/terraform/language/block/ephemeral). Before you generate code for an ephemeral resource, check that the Terraform version is greater than or equal to 1.11.0. Then, follow this priority order for managing sensitive attributes: 1. **First priority: Native secrets manager integration** If a resource has the ability to automatically manage a sensitive attribute by storing it in a secrets manager (e.g., AWS Secrets Manager, Azure Key Vault), use that configuration. This is the preferred approach. ```hcl # Bad resource "aws_rds_cluster" "example" { cluster_identifier = "example" database_name = "test" master_username = "test" master_password = var.db_master_password } # Good, managed by AWS Secrets Manager by default resource "aws_rds_cluster" "test" { cluster_identifier = "example" database_name = "test" manage_master_user_password = true master_username = "test" } ``` 2. **Second priority: Write-only attributes with ephemeral resources** If a resource has a write-only attribute but no native secrets manager integration, use an `ephemeral` resource for the sensitive data and pass that to the write-only attribute. Default the write-only version to 1. ```hcl # Bad resource "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?" } resource "vault_kv_secret_v2" "example" { mount = vault_mount.kvv2.path name = "secret" data_json = jsonencode( { password = "${random_password.password.result}", } ) } # Good ephemeral "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?" } resource "vault_kv_secret_v2" "example" { mount = vault_mount.kvv2.path name = "secret" data_json_wo = jsonencode( { password = "${ephemeral.random_password.password.result}", } ) data_json_wo_version = 1 } ``` If you need to retrieve a secret from a secrets manager to pass to a resource, use the `ephemeral` version of the resource to retrieve the secret and pass it to another resource. ```hcl # Good ephemeral "vault_kv_secret_v2" "db_secret" { mount = vault_mount.kvv2.path mount_id = vault_mount.kvv2.id name = vault_kv_secret_v2.db_root.name } resource "vault_database_secret_backend_connection" "postgres" { backend = vault_mount.db.path name = "postrgres-db" allowed_roles = ["*"] postgresql { connection_url = "postgresql://{{username}}:{{password}}@localhost:5432/postgres" password_authentication = "" username = "postgres" password_wo = tostring(ephemeral.vault_kv_secret_v2.db_secret.data.password) password_wo_version = 1 } } ``` 3. **Last resort: Regular resources** Only use a regular resource that has sensitive data written to state if neither of the above options are available, resource does not offer a write-only attribute or ephemeral resource alternative, or the Terraform version is less than 1.11.0. ================================================ FILE: terraform/code-generation/skills/terraform-style-guide/SKILL.md ================================================ --- name: terraform-style-guide description: Generate Terraform HCL code following HashiCorp's official style conventions and best practices. Use when writing, reviewing, or generating Terraform configurations. --- # Terraform Style Guide Generate and maintain Terraform code following HashiCorp's official style conventions and best practices. **Reference:** [HashiCorp Terraform Style Guide](https://developer.hashicorp.com/terraform/language/style) ## Code Generation Strategy When generating Terraform code: 1. Start with provider configuration and version constraints 2. Create data sources before dependent resources 3. Build resources in dependency order 4. Add outputs for key resource attributes 5. Use variables for all configurable values ## File Organization | File | Purpose | |------|---------| | `terraform.tf` | Terraform and provider version requirements | | `providers.tf` | Provider configurations | | `main.tf` | Primary resources and data sources | | `variables.tf` | Input variable declarations (alphabetical) | | `outputs.tf` | Output value declarations (alphabetical) | | `locals.tf` | Local value declarations | ### Example Structure ```hcl # terraform.tf terraform { required_version = ">= 1.14" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } # variables.tf variable "environment" { description = "Target deployment environment" type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } } # locals.tf locals { common_tags = { Environment = var.environment ManagedBy = "Terraform" } } # main.tf resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_hostnames = true tags = merge(local.common_tags, { Name = "${var.project_name}-${var.environment}-vpc" }) } # outputs.tf output "vpc_id" { description = "ID of the created VPC" value = aws_vpc.main.id } ``` ## Code Formatting ### Indentation and Alignment - Use **two spaces** per nesting level (no tabs) - Align equals signs for consecutive arguments ```hcl resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" subnet_id = "subnet-12345678" tags = { Name = "web-server" Environment = "production" } } ``` ### Block Organization Arguments precede blocks, with meta-arguments first: ```hcl resource "aws_instance" "example" { # Meta-arguments count = 3 # Arguments ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" # Blocks root_block_device { volume_size = 20 } # Lifecycle last lifecycle { create_before_destroy = true } } ``` ## Naming Conventions - Use **lowercase with underscores** for all names - Use **descriptive nouns** excluding the resource type - Be specific and meaningful - Resource names must be singular, not plural - Default to `main` for resources where a specific descriptive name is redundant or unavailable, provided only one instance exists ```hcl # Bad resource "aws_instance" "webAPI-aws-instance" {} resource "aws_instance" "web_apis" {} variable "name" {} # Good resource "aws_instance" "web_api" {} resource "aws_vpc" "main" {} variable "application_name" {} ``` ## Variables Every variable must include `type` and `description`: ```hcl variable "instance_type" { description = "EC2 instance type for the web server" type = string default = "t2.micro" validation { condition = contains(["t2.micro", "t2.small", "t2.medium"], var.instance_type) error_message = "Instance type must be t2.micro, t2.small, or t2.medium." } } variable "database_password" { description = "Password for the database admin user" type = string sensitive = true } ``` ## Outputs Every output must include `description`: ```hcl output "instance_id" { description = "ID of the EC2 instance" value = aws_instance.web.id } output "database_password" { description = "Database administrator password" value = aws_db_instance.main.password sensitive = true } ``` ## Dynamic Resource Creation ### Prefer for_each over count ```hcl # Bad - count for multiple resources resource "aws_instance" "web" { count = var.instance_count tags = { Name = "web-${count.index}" } } # Good - for_each with named instances variable "instance_names" { type = set(string) default = ["web-1", "web-2", "web-3"] } resource "aws_instance" "web" { for_each = var.instance_names tags = { Name = each.key } } ``` ### count for Conditional Creation ```hcl resource "aws_cloudwatch_metric_alarm" "cpu" { count = var.enable_monitoring ? 1 : 0 alarm_name = "high-cpu-usage" threshold = 80 } ``` ## Security Best Practices Refer to SECURITY.md. It includes guidance on encrypting resources, preventing sensitive data in state, and secure configurations. ## Version Pinning ```hcl terraform { required_version = ">= 1.14" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } ``` Use the latest major version of each provider and the latest minor version of Terraform, unless otherwise constrained by a dependency lock file or by other modules used by the configuration. **Version constraint operators:** - `= 1.0.0` - Exact version - `>= 1.0.0` - Greater than or equal - `~> 1.0` - Allow rightmost component to increment - `>= 1.0, < 2.0` - Version range ## Provider Configuration ```hcl provider "aws" { region = "us-west-2" default_tags { tags = { ManagedBy = "Terraform" Project = var.project_name } } } # Aliased provider for multi-region provider "aws" { alias = "east" region = "us-east-1" } ``` ## Version Control **Never commit:** - `terraform.tfstate`, `terraform.tfstate.backup` - `.terraform/` directory - `*.tfplan` - `.tfvars` files with sensitive data **Always commit:** - All `.tf` configuration files - `.terraform.lock.hcl` (dependency lock file) ## Validation Tools Run before committing: ```bash terraform fmt -recursive terraform validate ``` Additional tools: - `tflint` - Linting and best practices - `checkov` / `tfsec` - Security scanning ## Code Review Checklist - [ ] Code formatted with `terraform fmt` - [ ] Configuration validated with `terraform validate` - [ ] Files organized according to standard structure - [ ] All variables have type and description - [ ] All outputs have descriptions - [ ] Resource names use descriptive nouns with underscores - [ ] Version constraints pinned explicitly - [ ] Sensitive values marked with `sensitive = true` - [ ] No hardcoded credentials or secrets - [ ] Security best practices applied --- *Based on: [HashiCorp Terraform Style Guide](https://developer.hashicorp.com/terraform/language/style)* ================================================ FILE: terraform/code-generation/skills/terraform-test/SKILL.md ================================================ --- name: terraform-test description: Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating infrastructure behavior with assertions, mocking providers and data sources, testing module outputs and resource configurations, or troubleshooting Terraform test syntax and execution. metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.2" --- # Terraform Test Terraform's built-in testing framework validates that configuration updates don't introduce breaking changes. Tests run against temporary resources, protecting existing infrastructure and state files. ## Reference Files - `references/MOCK_PROVIDERS.md` — Mock provider syntax, common defaults, when to use mocks (Terraform 1.7.0+ only — skip if the user's version is below 1.7) - `references/CI_CD.md` — GitHub Actions and GitLab CI pipeline examples - `references/EXAMPLES.md` — Complete example test suite (unit, integration, and mock tests for a VPC module) Read the relevant reference file when the user asks about mocking, CI/CD integration, or wants a full example. ## Core Concepts - **Test file** (`.tftest.hcl` / `.tftest.json`): Contains `run` blocks that validate your configuration - **Run block**: A single test scenario with optional variables, providers, and assertions - **Assert block**: Conditions that must be true for the test to pass - **Mock provider**: Simulates provider behavior without real infrastructure (Terraform 1.7.0+) - **Test modes**: `apply` (default, creates real resources) or `plan` (validates logic only) ## File Structure ``` my-module/ ├── main.tf ├── variables.tf ├── outputs.tf └── tests/ ├── defaults_unit_test.tftest.hcl # plan mode — fast, no resources ├── validation_unit_test.tftest.hcl # plan mode └── full_stack_integration_test.tftest.hcl # apply mode — creates real resources ``` Use `*_unit_test.tftest.hcl` for plan-mode tests and `*_integration_test.tftest.hcl` for apply-mode tests so they can be filtered separately in CI. ## Test File Structure ```hcl # Optional: test-wide settings test { parallel = true # Enable parallel execution for all run blocks (default: false) } # Optional: file-level variables (highest precedence, override all other sources) variables { aws_region = "us-west-2" instance_type = "t2.micro" } # Optional: provider configuration provider "aws" { region = var.aws_region } # Required: at least one run block run "test_default_configuration" { command = plan assert { condition = aws_instance.example.instance_type == "t2.micro" error_message = "Instance type should be t2.micro by default" } } ``` ## Run Block ```hcl run "test_name" { command = plan # or apply (default) parallel = true # optional, since v1.9.0 # Override file-level variables variables { instance_type = "t3.large" } # Reference a specific module module { source = "./modules/vpc" # local or registry only (not git/http) version = "5.0.0" # registry modules only } # Control state isolation state_key = "shared_state" # since v1.9.0 # Plan behavior plan_options { mode = refresh-only # or normal (default) refresh = true replace = [aws_instance.example] target = [aws_instance.example] } # Assertions assert { condition = aws_instance.example.id != "" error_message = "Instance should have a valid ID" } # Expected failures (test passes if these fail) expect_failures = [ var.instance_count ] } ``` ## Common Test Patterns ### Validate outputs ```hcl run "test_outputs" { command = plan assert { condition = output.vpc_id != null error_message = "VPC ID output must be defined" } assert { condition = can(regex("^vpc-", output.vpc_id)) error_message = "VPC ID should start with 'vpc-'" } } ``` ### Conditional resources ```hcl run "test_nat_gateway_disabled" { command = plan variables { create_nat_gateway = false } assert { condition = length(aws_nat_gateway.main) == 0 error_message = "NAT gateway should not be created when disabled" } } ``` ### Resource counts ```hcl run "test_resource_count" { command = plan variables { instance_count = 3 } assert { condition = length(aws_instance.workers) == 3 error_message = "Should create exactly 3 worker instances" } } ``` ### Tags ```hcl run "test_resource_tags" { command = plan variables { common_tags = { Environment = "production" ManagedBy = "Terraform" } } assert { condition = aws_instance.example.tags["Environment"] == "production" error_message = "Environment tag should be set correctly" } assert { condition = aws_instance.example.tags["ManagedBy"] == "Terraform" error_message = "ManagedBy tag should be set correctly" } } ``` ### Data sources ```hcl run "test_data_source_lookup" { command = plan assert { condition = data.aws_ami.ubuntu.id != "" error_message = "Should find a valid Ubuntu AMI" } assert { condition = can(regex("^ami-", data.aws_ami.ubuntu.id)) error_message = "AMI ID should be in correct format" } } ``` ### Validation rules ```hcl run "test_invalid_environment" { command = plan variables { environment = "invalid" } expect_failures = [ var.environment ] } ``` ### Sequential tests with dependencies ```hcl run "setup_vpc" { command = apply assert { condition = output.vpc_id != "" error_message = "VPC should be created" } } run "test_subnet_in_vpc" { command = plan variables { vpc_id = run.setup_vpc.vpc_id } assert { condition = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id error_message = "Subnet should be in the VPC from setup_vpc" } } ``` ### Plan options (refresh-only, targeted) ```hcl run "test_refresh_only" { command = plan plan_options { mode = refresh-only } assert { condition = aws_instance.example.tags["Environment"] == "production" error_message = "Tags should be refreshed correctly" } } run "test_specific_resource" { command = plan plan_options { target = [aws_instance.example] } assert { condition = aws_instance.example.instance_type == "t2.micro" error_message = "Targeted resource should be planned" } } ``` ### Parallel modules ```hcl run "test_networking_module" { command = plan parallel = true module { source = "./modules/networking" } assert { condition = output.vpc_id != "" error_message = "VPC should be created" } } run "test_compute_module" { command = plan parallel = true module { source = "./modules/compute" } assert { condition = output.instance_id != "" error_message = "Instance should be created" } } ``` ### State key sharing ```hcl run "create_foundation" { command = apply state_key = "foundation" assert { condition = aws_vpc.main.id != "" error_message = "Foundation VPC should be created" } } run "create_application" { command = apply state_key = "foundation" variables { vpc_id = run.create_foundation.vpc_id } assert { condition = aws_instance.app.vpc_id == run.create_foundation.vpc_id error_message = "Application should use foundation VPC" } } ``` ### Cleanup ordering (S3 objects before bucket) ```hcl run "create_bucket" { command = apply assert { condition = aws_s3_bucket.example.id != "" error_message = "Bucket should be created" } } run "add_objects" { command = apply assert { condition = length(aws_s3_object.files) > 0 error_message = "Objects should be added" } } # Cleanup destroys in reverse: objects first, then bucket ``` ### Multiple aliased providers ```hcl provider "aws" { alias = "primary" region = "us-west-2" } provider "aws" { alias = "secondary" region = "us-east-1" } run "test_with_specific_provider" { command = plan providers = { aws = provider.aws.secondary } assert { condition = aws_instance.example.availability_zone == "us-east-1a" error_message = "Instance should be in us-east-1 region" } } ``` ### Complex conditions ```hcl assert { condition = alltrue([ for subnet in aws_subnet.private : can(regex("^10\\.0\\.", subnet.cidr_block)) ]) error_message = "All private subnets should use 10.0.0.0/8 CIDR range" } ``` ## Cleanup Resources are destroyed in **reverse run block order** after test completion. This matters for dependencies (e.g., S3 objects before bucket). Use `terraform test -no-cleanup` to skip cleanup for debugging. ## Running Tests ```bash terraform test # all tests terraform test tests/defaults.tftest.hcl # specific file terraform test -filter=test_vpc_configuration # by run block name terraform test -test-directory=integration-tests # custom directory terraform test -verbose # detailed output terraform test -no-cleanup # skip resource cleanup ``` ## Best Practices 1. **Naming**: `*_unit_test.tftest.hcl` for plan mode, `*_integration_test.tftest.hcl` for apply mode 2. **Test naming**: Use descriptive run block names that explain the scenario being tested 3. **Default to plan**: Use `command = plan` unless you need to test real resource behavior 4. **Use mocks** for external dependencies — faster and no credentials needed (see `references/MOCK_PROVIDERS.md`) 5. **Error messages**: Make them specific enough to diagnose failures without running the test again 6. **Negative tests**: Use `expect_failures` to verify validation rules reject bad inputs 7. **Variable coverage**: Test different variable combinations to validate all code paths — test variables have the highest precedence and override all other sources 8. **Module sources**: Test files only support local paths and registry modules — not git or HTTP URLs 9. **Parallel execution**: Use `parallel = true` for independent tests with different state files 10. **Cleanup**: Integration tests destroy resources in reverse run block order automatically; use `-no-cleanup` for debugging 11. **CI/CD**: Run unit tests on every PR, integration tests on merge (see `references/CI_CD.md`) ## Troubleshooting | Issue | Solution | |-------|----------| | Assertion failures | Use `-verbose` to see actual vs expected values | | Missing credentials | Use mock providers for unit tests | | Unsupported module source | Convert git/HTTP sources to local modules | | Tests interfering | Use `state_key` or separate modules for isolation | | Slow tests | Use `command = plan` and mocks; run integration tests separately | ## References - [Terraform Testing Documentation](https://developer.hashicorp.com/terraform/language/tests) - [Terraform Test Command](https://developer.hashicorp.com/terraform/cli/commands/test) - [Testing Best Practices](https://developer.hashicorp.com/terraform/language/tests/best-practices) ================================================ FILE: terraform/code-generation/skills/terraform-test/references/CI_CD.md ================================================ # CI/CD Integration ## GitHub Actions ```yaml name: Terraform Tests on: pull_request: branches: [ main ] push: branches: [ main ] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.9.0 - run: terraform fmt -check -recursive - run: terraform init - run: terraform validate - name: Run unit tests (plan mode, no credentials needed) run: terraform test -filter=unit_test -verbose integration-tests: runs-on: ubuntu-latest needs: unit-tests if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.9.0 - run: terraform init - name: Run integration tests run: terraform test -filter=integration_test -verbose env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ``` ## GitLab CI ```yaml stages: - validate - test terraform-unit-tests: image: hashicorp/terraform:1.9 stage: validate before_script: - terraform init script: - terraform fmt -check -recursive - terraform validate - terraform test -filter=unit_test -verbose terraform-integration-tests: image: hashicorp/terraform:1.9 stage: test before_script: - terraform init script: - terraform test -filter=integration_test -verbose only: - main ``` ## Recommended CI Strategy - Run unit tests (plan mode + mock tests) on every PR — fast, no credentials needed - Run integration tests only on merge to main or nightly — requires cloud credentials - Use `-filter=unit_test` / `-filter=integration_test` to separate test types based on naming convention - Store cloud credentials as CI secrets, never in code ================================================ FILE: terraform/code-generation/skills/terraform-test/references/EXAMPLES.md ================================================ # Example Test Suite Complete example testing a VPC module with unit, integration, and mock tests. ## Unit Tests (Plan Mode) ```hcl # tests/vpc_module_unit_test.tftest.hcl variables { environment = "test" aws_region = "us-west-2" } run "test_defaults" { command = plan variables { vpc_cidr = "10.0.0.0/16" vpc_name = "test-vpc" } assert { condition = aws_vpc.main.cidr_block == "10.0.0.0/16" error_message = "VPC CIDR should match input" } assert { condition = aws_vpc.main.enable_dns_hostnames == true error_message = "DNS hostnames should be enabled by default" } assert { condition = aws_vpc.main.tags["Name"] == "test-vpc" error_message = "VPC name tag should match input" } } run "test_subnets" { command = plan variables { vpc_cidr = "10.0.0.0/16" vpc_name = "test-vpc" public_subnets = ["10.0.1.0/24", "10.0.2.0/24"] private_subnets = ["10.0.10.0/24", "10.0.11.0/24"] } assert { condition = length(aws_subnet.public) == 2 error_message = "Should create 2 public subnets" } assert { condition = length(aws_subnet.private) == 2 error_message = "Should create 2 private subnets" } assert { condition = alltrue([ for subnet in aws_subnet.private : subnet.map_public_ip_on_launch == false ]) error_message = "Private subnets should not assign public IPs" } } run "test_outputs" { command = plan variables { vpc_cidr = "10.0.0.0/16" vpc_name = "test-vpc" } assert { condition = output.vpc_id != "" error_message = "VPC ID output should not be empty" } assert { condition = can(regex("^vpc-", output.vpc_id)) error_message = "VPC ID should have correct format" } assert { condition = output.vpc_cidr == "10.0.0.0/16" error_message = "VPC CIDR output should match input" } } run "test_invalid_cidr" { command = plan variables { vpc_cidr = "invalid" vpc_name = "test-vpc" } expect_failures = [ var.vpc_cidr ] } ``` ## Integration Tests (Apply Mode) ```hcl # tests/vpc_module_integration_test.tftest.hcl variables { environment = "integration-test" aws_region = "us-west-2" } run "integration_test_vpc_creation" { # command defaults to apply — creates real AWS resources variables { vpc_cidr = "10.100.0.0/16" vpc_name = "integration-test-vpc" } assert { condition = aws_vpc.main.id != "" error_message = "VPC should be created with valid ID" } assert { condition = aws_vpc.main.state == "available" error_message = "VPC should be in available state" } } ``` ## Mock Tests (Plan Mode, No Credentials) ```hcl # tests/vpc_module_mock_test.tftest.hcl mock_provider "aws" { mock_resource "aws_instance" { defaults = { id = "i-1234567890abcdef0" instance_type = "t2.micro" ami = "ami-12345678" public_ip = "203.0.113.1" private_ip = "10.0.1.100" } } mock_resource "aws_vpc" { defaults = { id = "vpc-12345678" cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true } } mock_resource "aws_subnet" { defaults = { id = "subnet-12345678" vpc_id = "vpc-12345678" cidr_block = "10.0.1.0/24" availability_zone = "us-west-2a" map_public_ip_on_launch = false } } mock_data "aws_ami" { defaults = { id = "ami-0c55b159cbfafe1f0" name = "ubuntu-focal-20.04-amd64" } } mock_data "aws_availability_zones" { defaults = { names = ["us-west-2a", "us-west-2b", "us-west-2c"] } } } run "test_instance_with_mocks" { command = plan variables { instance_type = "t2.micro" ami_id = "ami-12345678" } assert { condition = aws_instance.example.instance_type == "t2.micro" error_message = "Instance type should match input variable" } assert { condition = aws_instance.example.id == "i-1234567890abcdef0" error_message = "Mock should return consistent instance ID" } } run "test_data_source_with_mocks" { command = plan assert { condition = data.aws_ami.ubuntu.id == "ami-0c55b159cbfafe1f0" error_message = "Mock data source should return predictable AMI ID" } assert { condition = length(data.aws_availability_zones.available.names) == 3 error_message = "Should return 3 mocked availability zones" } assert { condition = contains(data.aws_availability_zones.available.names, "us-west-2a") error_message = "Should include us-west-2a in mocked zones" } } run "test_outputs_with_mocks" { command = plan assert { condition = output.vpc_id == "vpc-12345678" error_message = "VPC ID output should match mocked value" } assert { condition = can(regex("^vpc-", output.vpc_id)) error_message = "VPC ID output should have correct format" } } run "test_conditional_resources_with_mocks" { command = plan variables { create_bastion = true create_nat_gateway = false } assert { condition = length(aws_instance.bastion) == 1 error_message = "Bastion should be created when enabled" } assert { condition = length(aws_nat_gateway.nat) == 0 error_message = "NAT gateway should not be created when disabled" } } run "test_tag_inheritance_with_mocks" { command = plan variables { common_tags = { Environment = "test" ManagedBy = "Terraform" } } assert { condition = alltrue([ for key in keys(var.common_tags) : contains(keys(aws_instance.example.tags), key) ]) error_message = "All common tags should be present on instance" } } run "test_invalid_cidr_with_mocks" { command = plan variables { vpc_cidr = "invalid" } expect_failures = [ var.vpc_cidr ] } run "setup_vpc_with_mocks" { command = plan variables { vpc_cidr = "10.0.0.0/16" vpc_name = "test-vpc" } assert { condition = aws_vpc.main.cidr_block == "10.0.0.0/16" error_message = "VPC CIDR should match input" } } run "test_subnet_references_vpc_with_mocks" { command = plan variables { vpc_id = run.setup_vpc_with_mocks.vpc_id subnet_cidr = "10.0.1.0/24" } assert { condition = aws_subnet.example.vpc_id == run.setup_vpc_with_mocks.vpc_id error_message = "Subnet should reference VPC from previous run" } } ``` ================================================ FILE: terraform/code-generation/skills/terraform-test/references/MOCK_PROVIDERS.md ================================================ # Mock Providers Mock providers simulate provider behavior without creating real infrastructure (Terraform 1.7.0+). Use them for fast, credential-free unit tests. ## Basic Mock Provider ```hcl mock_provider "aws" { mock_resource "aws_instance" { defaults = { id = "i-1234567890abcdef0" instance_type = "t2.micro" ami = "ami-12345678" public_ip = "203.0.113.1" private_ip = "10.0.1.100" } } mock_data "aws_ami" { defaults = { id = "ami-0c55b159cbfafe1f0" } } mock_data "aws_availability_zones" { defaults = { names = ["us-west-2a", "us-west-2b", "us-west-2c"] } } } run "test_with_mocks" { command = plan # Mocks only work with plan mode assert { condition = aws_instance.example.id == "i-1234567890abcdef0" error_message = "Mock instance ID should match" } } ``` ## Aliased Mock Provider ```hcl mock_provider "aws" { alias = "mocked" mock_resource "aws_s3_bucket" { defaults = { id = "test-bucket-12345" arn = "arn:aws:s3:::test-bucket-12345" } } } run "test_with_aliased_mock" { command = plan providers = { aws = provider.aws.mocked } assert { condition = aws_s3_bucket.example.id == "test-bucket-12345" error_message = "Bucket ID should match mock" } } ``` ## Common Mock Defaults ```hcl mock_provider "aws" { mock_resource "aws_instance" { defaults = { id = "i-1234567890abcdef0" arn = "arn:aws:ec2:us-west-2:123456789012:instance/i-1234567890abcdef0" instance_type = "t2.micro" ami = "ami-12345678" availability_zone = "us-west-2a" subnet_id = "subnet-12345678" vpc_security_group_ids = ["sg-12345678"] associate_public_ip_address = true public_ip = "203.0.113.1" private_ip = "10.0.1.100" tags = {} } } mock_resource "aws_vpc" { defaults = { id = "vpc-12345678" arn = "arn:aws:ec2:us-west-2:123456789012:vpc/vpc-12345678" cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true instance_tenancy = "default" tags = {} } } mock_resource "aws_subnet" { defaults = { id = "subnet-12345678" arn = "arn:aws:ec2:us-west-2:123456789012:subnet/subnet-12345678" vpc_id = "vpc-12345678" cidr_block = "10.0.1.0/24" availability_zone = "us-west-2a" map_public_ip_on_launch = false tags = {} } } mock_resource "aws_s3_bucket" { defaults = { id = "test-bucket-12345" arn = "arn:aws:s3:::test-bucket-12345" bucket = "test-bucket-12345" bucket_domain_name = "test-bucket-12345.s3.amazonaws.com" region = "us-west-2" tags = {} } } mock_data "aws_ami" { defaults = { id = "ami-0c55b159cbfafe1f0" name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210430" architecture = "x86_64" root_device_type = "ebs" virtualization_type = "hvm" } } mock_data "aws_availability_zones" { defaults = { names = ["us-west-2a", "us-west-2b", "us-west-2c"] zone_ids = ["usw2-az1", "usw2-az2", "usw2-az3"] } } mock_data "aws_vpc" { defaults = { id = "vpc-12345678" cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true } } } ``` ## When to Use Mocks **Good fit:** - Testing Terraform logic, conditionals, `for_each`/`count` expressions - Validating variable transformations and output calculations - Local development without cloud credentials - Fast CI/CD feedback loops **Not a good fit:** - Validating actual provider API behavior - Testing real resource creation side effects - End-to-end integration testing ## Limitations - **Plan mode only** — mocks don't work with `command = apply` - Mock defaults may not reflect real computed attribute values - Mocks need manual updates when provider schemas change - Can't test real resource dependencies or timing ================================================ FILE: terraform/module-generation/.claude-plugin/plugin.json ================================================ { "name": "terraform-module-generation", "version": "1.0.0", "description": "Terraform module generation and refactoring skills for Claude Code, including module design and Terraform Stacks.", "author": { "name": "HashiCorp", "url": "https://github.com/hashicorp" }, "homepage": "https://developer.hashicorp.com/terraform/language/modules", "repository": "https://github.com/hashicorp/agent-skills", "license": "MPL-2.0", "keywords": ["terraform", "modules", "infrastructure", "iac", "stacks", "refactoring"], "mcpServers": { "terraform": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "TFE_TOKEN", "-e", "TFE_ADDRESS", "hashicorp/terraform-mcp-server" ], "env": { "TFE_TOKEN": "${TFE_TOKEN}", "TFE_ADDRESS": "${TFE_ADDRESS}" } } } } ================================================ FILE: terraform/module-generation/skills/refactor-module/SKILL.md ================================================ --- name: refactor-module description: Transform monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices. metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- # Skill: Refactor Module ## Overview This skill guides AI agents in transforming monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices. ## Capability Statement The agent will analyze existing Terraform code and systematically refactor it into well-structured modules with: - Clear interface contracts (variables and outputs) - Proper encapsulation and abstraction - Versioning and documentation - Testing frameworks - Migration path for existing state ## Prerequisites - Existing Terraform configuration to refactor - Understanding of resource dependencies - Access to current state file (for migration planning) - Knowledge of module registry patterns ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `source_directory` | string | Yes | Path to existing Terraform configuration | | `module_name` | string | Yes | Name for the new module | | `abstraction_level` | string | No | "simple", "intermediate", "advanced" (default: intermediate) | | `preserve_state` | boolean | Yes | Whether to maintain state compatibility | | `target_registry` | string | No | Target module registry (local, private, public) | ## Execution Steps ### 1. Analysis Phase ```markdown **Identify Refactoring Candidates** - Group resources by logical function - Identify repeated patterns - Map resource dependencies - Detect configuration coupling - Analyze variable usage patterns **Complexity Assessment** - Count resource relationships - Measure variable propagation depth - Identify cross-resource references - Evaluate state migration complexity ``` ### 2. Module Design #### Interface Design ```hcl # Define clear input contract variable "network_config" { description = "Network configuration parameters" type = object({ cidr_block = string availability_zones = list(string) enable_nat = bool }) validation { condition = can(cidrhost(var.network_config.cidr_block, 0)) error_message = "CIDR block must be valid IPv4 CIDR." } } # Define output contract output "vpc_id" { description = "ID of the created VPC" value = aws_vpc.main.id } output "private_subnet_ids" { description = "List of private subnet IDs" value = { for k, v in aws_subnet.private : k => v.id } } ``` #### Encapsulation Strategy ```markdown **What to Include in Module:** - Tightly coupled resources (VPC + subnets) - Resources with shared lifecycle - Configuration with clear boundaries **What to Keep Separate:** - Cross-cutting concerns (monitoring, tagging) - Resources with different lifecycles - Provider-specific configurations ``` ### 3. Code Transformation #### Before: Monolithic Configuration ```hcl # main.tf (monolithic) resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true tags = { Name = "production-vpc" Environment = "prod" } } resource "aws_subnet" "public_1" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-east-1a" tags = { Name = "public-subnet-1" Type = "public" } } resource "aws_subnet" "public_2" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "us-east-1b" tags = { Name = "public-subnet-2" Type = "public" } } resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = { Name = "production-igw" } } # ... more repetitive subnet and routing resources ``` #### After: Modular Structure ```hcl # modules/vpc/main.tf locals { subnet_count = length(var.availability_zones) } resource "aws_vpc" "main" { cidr_block = var.cidr_block enable_dns_hostnames = var.enable_dns_hostnames enable_dns_support = var.enable_dns_support tags = merge( var.tags, { Name = var.name } ) } resource "aws_subnet" "public" { for_each = var.create_public_subnets ? toset(var.availability_zones) : [] vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.cidr_block, 8, index(var.availability_zones, each.value)) availability_zone = each.value map_public_ip_on_launch = true tags = merge( var.tags, { Name = "${var.name}-public-${each.value}" Type = "public" } ) } resource "aws_internet_gateway" "main" { count = var.create_public_subnets ? 1 : 0 vpc_id = aws_vpc.main.id tags = merge( var.tags, { Name = "${var.name}-igw" } ) } # modules/vpc/variables.tf variable "name" { description = "Name prefix for all resources" type = string } variable "cidr_block" { description = "CIDR block for the VPC" type = string validation { condition = can(cidrhost(var.cidr_block, 0)) error_message = "Must be a valid IPv4 CIDR block." } } variable "availability_zones" { description = "List of availability zones" type = list(string) } variable "create_public_subnets" { description = "Whether to create public subnets" type = bool default = true } variable "enable_dns_hostnames" { description = "Enable DNS hostnames in the VPC" type = bool default = true } variable "enable_dns_support" { description = "Enable DNS support in the VPC" type = bool default = true } variable "tags" { description = "Tags to apply to all resources" type = map(string) default = {} } # modules/vpc/outputs.tf output "vpc_id" { description = "ID of the VPC" value = aws_vpc.main.id } output "vpc_cidr_block" { description = "CIDR block of the VPC" value = aws_vpc.main.cidr_block } output "public_subnet_ids" { description = "Map of availability zones to public subnet IDs" value = { for k, v in aws_subnet.public : k => v.id } } output "internet_gateway_id" { description = "ID of the internet gateway" value = try(aws_internet_gateway.main[0].id, null) } # Root configuration using module module "vpc" { source = "./modules/vpc" name = "production" cidr_block = "10.0.0.0/16" availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"] tags = { Environment = "production" ManagedBy = "Terraform" } } ``` ### 4. State Migration #### Generate Migration Plan ```hcl # migration.tf # Use moved blocks for state refactoring (Terraform 1.1+) moved { from = aws_vpc.main to = module.vpc.aws_vpc.main } moved { from = aws_subnet.public_1 to = module.vpc.aws_subnet.public["us-east-1a"] } moved { from = aws_subnet.public_2 to = module.vpc.aws_subnet.public["us-east-1b"] } moved { from = aws_internet_gateway.main to = module.vpc.aws_internet_gateway.main[0] } ``` #### Manual State Migration (Pre-1.1) ```bash # Generate state migration commands terraform state mv aws_vpc.main module.vpc.aws_vpc.main terraform state mv aws_subnet.public_1 'module.vpc.aws_subnet.public["us-east-1a"]' terraform state mv aws_subnet.public_2 'module.vpc.aws_subnet.public["us-east-1b"]' terraform state mv aws_internet_gateway.main 'module.vpc.aws_internet_gateway.main[0]' ``` ### 5. Module Documentation ```markdown # VPC Module ## Overview Creates a VPC with configurable public and private subnets across multiple availability zones. ## Features - Multi-AZ subnet deployment - Optional NAT gateway configuration - VPC Flow Logs integration - Customizable CIDR allocation ## Usage \`\`\`hcl module "vpc" { source = "./modules/vpc" name = "my-vpc" cidr_block = "10.0.0.0/16" availability_zones = ["us-east-1a", "us-east-1b"] create_public_subnets = true create_private_subnets = true enable_nat_gateway = true tags = { Environment = "production" } } \`\`\` ## Requirements | Name | Version | |------|---------| | terraform | >= 1.5.0 | | aws | ~> 5.0 | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|----------| | name | Name prefix for resources | `string` | n/a | yes | | cidr_block | VPC CIDR block | `string` | n/a | yes | | availability_zones | List of AZs | `list(string)` | n/a | yes | ## Outputs | Name | Description | |------|-------------| | vpc_id | VPC identifier | | public_subnet_ids | Map of public subnet IDs | | private_subnet_ids | Map of private subnet IDs | ## Examples See [examples/](./examples/) directory for complete usage examples. ``` ### 6. Testing Use skill terraform-test **Test File**: A `.tftest.hcl` or `.tftest.json` file containing test configuration and run blocks that validate your Terraform configuration. **Test Block**: Optional configuration block that defines test-wide settings (available since Terraform 1.6.0). **Run Block**: Defines a single test scenario with optional variables, provider configurations, and assertions. Each test file requires at least one run block. **Assert Block**: Contains conditions that must evaluate to true for the test to pass. Failed assertions cause the test to fail. **Mock Provider**: Simulates provider behavior without creating real infrastructure (available since Terraform 1.7.0). **Test Modes**: Tests run in apply mode (default, creates real infrastructure) or plan mode (validates logic without creating resources). #### File Structure Terraform test files use the `.tftest.hcl` or `.tftest.json` extension and are typically organized in a `tests/` directory. Use clear naming conventions to distinguish between unit tests (plan mode) and integration tests (apply mode): ``` my-module/ ├── main.tf ├── variables.tf ├── outputs.tf └── tests/ ├── unit_test.tftest.hcl # Unit test (plan mode) └── integration_test.tftest.hcl # Integration test (apply mode - creates real resources) ``` ## Refactoring Patterns ### Pattern 1: Resource Grouping Extract related resources into cohesive modules: - Networking (VPC, Subnets, Route Tables) - Compute (ASG, Launch Templates, Load Balancers) - Data (RDS, ElastiCache, S3) ### Pattern 2: Configuration Layering ```hcl # Base module with defaults module "vpc_base" { source = "./modules/vpc-base" # Minimal required inputs } # Environment-specific wrapper module "vpc_prod" { source = "./modules/vpc-production" # Inherits from base, adds prod-specific config } ``` ### Pattern 3: Composition ```hcl # Small, focused modules module "vpc" { source = "./modules/vpc" } module "security_groups" { source = "./modules/security-groups" vpc_id = module.vpc.vpc_id } module "application" { source = "./modules/application" vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnet_ids sg_ids = module.security_groups.app_sg_ids } ``` ## Common Pitfalls ### 1. Over-Abstraction ```hcl # ❌ Don't create overly generic modules variable "resources" { type = map(map(any)) # Too flexible, hard to validate } # ✅ Do use specific, typed interfaces variable "database_config" { type = object({ engine = string instance_class = string }) } ``` ### 2. Tight Coupling ```hcl # ❌ Don't couple modules through direct references # module A output "instance_id" { value = aws_instance.app.id } # module B (in same config) resource "aws_eip" "app" { instance = module.a.instance_id # Tight coupling } # ✅ Do pass dependencies through root module module "compute" { source = "./modules/compute" } resource "aws_eip" "app" { instance = module.compute.instance_id } ``` ### 3. State Migration Errors Always test migration in non-production first: ```bash # Create plan to verify no changes after migration terraform plan -out=migration.tfplan # Review carefully terraform show migration.tfplan # Apply only if plan shows no changes terraform apply migration.tfplan ``` ## Version Control Strategy ```hcl # Use semantic versioning for modules module "vpc" { source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.0" version = "~> 1.2" } # Pin to specific versions in production # Use version ranges in development ``` ## Success Criteria - [ ] Module has single, well-defined responsibility - [ ] All variables have descriptions and types - [ ] Validation rules prevent invalid configurations - [ ] Outputs provide sufficient information for consumers - [ ] Documentation includes usage examples - [ ] Tests verify module behavior - [ ] State migration completed without resource recreation - [ ] No plan differences after refactoring ## Related Skills - [Terraform code generation](https://raw.githubusercontent.com/hashicorp/agent-skills/refs/heads/main/terraform/code-generation/skills/terraform-style-guide/SKILL.md) - Style guide for the new Terraform Module - [Azure Verified Modules](https://raw.githubusercontent.com/hashicorp/agent-skills/refs/heads/main/terraform/code-generation/skills/azure-verified-modules/SKILL.md) - Recommended module specifications for Azure ## Resources - [Terraform Module Development](https://developer.hashicorp.com/terraform/language/modules/develop) - [Module Best Practices](https://developer.hashicorp.com/terraform/cloud-docs/registry/design) ## Revision History | Version | Date | Changes | |---------|------|---------| | 1.0.0 | 2025-11-07 | Initial skill definition | ================================================ FILE: terraform/module-generation/skills/terraform-stacks/SKILL.md ================================================ --- name: terraform-stacks description: Comprehensive guide for working with HashiCorp Terraform Stacks. Use when creating, modifying, or validating Terraform Stack configurations (.tfcomponent.hcl, .tfdeploy.hcl files), working with stack components and deployments from local modules, public registry, or private registry sources, managing multi-region or multi-environment infrastructure, or troubleshooting Terraform Stacks syntax and structure. metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- # Terraform Stacks Terraform Stacks simplify infrastructure provisioning and management at scale by providing a configuration layer above traditional Terraform modules. Stacks enable declarative orchestration of multiple components across environments, regions, and cloud accounts. ## Core Concepts **Stack**: A complete unit of infrastructure composed of components and deployments that can be managed together. **Component**: An abstraction around a Terraform module that defines infrastructure pieces. Each component specifies a source module, inputs, and providers. **Deployment**: An instance of all components in a stack with specific input values. Use deployments for different environments (dev/staging/prod), regions, or cloud accounts. **Stack Language**: A separate HCL-based language (not regular Terraform HCL) with distinct blocks and file extensions. ## File Structure Terraform Stacks use specific file extensions: - **Component configuration**: `.tfcomponent.hcl` - **Deployment configuration**: `.tfdeploy.hcl` - **Provider lock file**: `.terraform.lock.hcl` (generated by CLI) All configuration files must be at the root level of the Stack repository. HCP Terraform processes all files in dependency order. ### Recommended File Organization ``` my-stack/ ├── .terraform-version # The required Terraform version for this Stack ├── variables.tfcomponent.hcl # Variable declarations ├── providers.tfcomponent.hcl # Provider configurations ├── components.tfcomponent.hcl # Component definitions ├── outputs.tfcomponent.hcl # Stack outputs ├── deployments.tfdeploy.hcl # Deployment definitions ├── .terraform.lock.hcl # Provider lock file (generated) └── modules/ # Local modules (optional - only if using local modules) ├── s3/ └── compute/ ``` **Note**: The `modules/` directory is only required when using local module sources. Components can reference modules from: - Local file paths: `./modules/vpc` - Public registry: `terraform-aws-modules/vpc/aws` - Private registry: `app.terraform.io//vpc/aws` - Git: `git::https://github.com/org/repo.git//path?ref=v1.0.0` HCP Terraform processes all `.tfcomponent.hcl` and `.tfdeploy.hcl` files in dependency order. ## Required Terraform version (.terraform-version) Use Terraform v1.13.x or later to access the Stacks CLI plugin and to run terraform stacks CLI commands. Begin by adding a .terraform-version file to your Stack's root directory to specify the Terraform version required for your Stack. For example, the following file specifies Terraform v1.14.5: ``` 1.14.5 ``` ## Component Configuration (.tfcomponent.hcl) ### Variable Block Declare input variables for the Stack configuration. Variables must define a `type` field and do not support the `validation` argument. ```hcl variable "aws_region" { type = string description = "AWS region for deployments" default = "us-west-1" } variable "identity_token" { type = string description = "OIDC identity token" ephemeral = true # Does not persist to state file } variable "instance_count" { type = number nullable = false } ``` **Important**: Use `ephemeral = true` for credentials and tokens (identity tokens, API keys, passwords) to prevent them from persisting in state files. Use `stable` for longer-lived values like license keys that need to persist across runs. ### Required Providers Block ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } random = { source = "hashicorp/random" version = "~> 3.5.0" } } ``` ### Provider Block Provider blocks differ from traditional Terraform: 1. Support `for_each` meta-argument 2. Define aliases in the block header (not as an argument) 3. Accept configuration through a `config` block **Single Provider Configuration:** ```hcl provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` **Multiple Provider Configurations with for_each:** ```hcl provider "aws" "configurations" { for_each = var.regions config { region = each.value assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` **Authentication Best Practice**: Use **workload identity** (OIDC) as the preferred authentication method for Stacks. This approach: - Avoids long-lived static credentials - Provides temporary, scoped credentials per deployment run - Integrates with cloud provider IAM (AWS IAM Roles, Azure Managed Identities, GCP Service Accounts) - Eliminates need for platform-managed environment variables Configure workload identity using `identity_token` blocks and `assume_role_with_web_identity` in provider configuration. For detailed setup instructions for AWS, Azure, and GCP, see: https://developer.hashicorp.com/terraform/cloud-docs/dynamic-provider-credentials ### Component Block Each Stack requires at least one component block. Add a component for each module to include in the Stack. Components reference modules from local paths, registries, or Git. ```hcl component "vpc" { source = "app.terraform.io/my-org/vpc/aws" # Local, registry, or Git URL version = "2.1.0" # For registry modules inputs = { cidr_block = var.vpc_cidr name_prefix = var.name_prefix } providers = { aws = provider.aws.this } } ``` See `references/component-blocks.md` for examples of dependencies, for_each, public registry modules, Git sources, and more. **Key Points:** - Reference outputs: `component..` or `component.[key].` for for_each - Dependencies inferred automatically from component references - Aggregate with for expressions: `[for x in component.s3 : x.bucket_name]` - For components with `for_each`, reference specific instances: `component.[each.value].` - Provider references are normal values: `provider..` or `provider..[each.value]` ### Output Block Outputs require a `type` argument and do not support `preconditions`: ```hcl output "vpc_id" { type = string description = "VPC ID" value = component.vpc.vpc_id } output "endpoint_urls" { type = map(string) value = { for region, comp in component.api : region => comp.endpoint_url } sensitive = false } ``` ### Locals Block Locals blocks work the same in both `.tfcomponent.hcl` and `.tfdeploy.hcl` files: ```hcl locals { common_tags = { Environment = var.environment ManagedBy = "Terraform Stacks" Project = var.project_name } region_config = { for region in var.regions : region => { name_suffix = "${var.environment}-${region}" } } } ``` ### Removed Block Use to safely remove components from a Stack. HCP Terraform requires the component's providers to remove it. ```hcl removed { from = component.old_component source = "./modules/old-module" providers = { aws = provider.aws.this } } ``` ## Deployment Configuration (.tfdeploy.hcl) ### Identity Token Block Generate JWT tokens for OIDC authentication with cloud providers: ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } identity_token "azure" { audience = ["api://AzureADTokenExchange"] } ``` Reference tokens in deployments using `identity_token..jwt` ### Store Block Access HCP Terraform variable sets within Stack deployments: ```hcl store "varset" "aws_credentials" { id = "varset-ABC123" # Alternatively use: name = "varset_name" source = "tfc-cloud-shared" category = "terraform" # Alternatively use: category = "env" for environment variables } deployment "production" { inputs = { aws_access_key = store.varset.aws_credentials.AWS_ACCESS_KEY_ID } } ``` Use to centralize credentials and share variables across Stacks. See `references/deployment-blocks.md` for details. ### Deployment Block Define deployment instances (minimum 1, maximum 20 per Stack): ```hcl deployment "production" { inputs = { aws_region = "us-west-1" instance_count = 3 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Create multiple deployments for different environments deployment "development" { inputs = { aws_region = "us-east-1" instance_count = 1 name_suffix = "dev" role_arn = local.role_arn identity_token = identity_token.aws.jwt } } ``` **To destroy a deployment**: Set `destroy = true`, upload configuration, approve destroy run, then remove the deployment block. See `references/deployment-blocks.md` for details. ### Deployment Group Block Group deployments together for shared settings (HCP Terraform Premium tier feature). Free/standard tiers use default groups named `{deployment-name}_default`. ```hcl deployment_group "canary" { auto_approve_checks = [deployment_auto_approve.safe_changes] } deployment "dev" { inputs = { /* ... */ } deployment_group = deployment_group.canary } ``` Multiple deployments can reference the same group. See `references/deployment-blocks.md` for details. ### Deployment Auto-Approve Block Define rules to automatically approve deployment plans (HCP Terraform Premium tier feature): ```hcl deployment_auto_approve "safe_changes" { deployment_group = deployment_group.canary check { condition = context.plan.changes.remove == 0 reason = "Cannot auto-approve plans with resource deletions" } } ``` **Available context variables**: `context.plan.applyable`, `context.plan.changes.add/change/remove/total`, `context.success` **Note:** `orchestrate` blocks are deprecated. Use `deployment_group` and `deployment_auto_approve` instead. See `references/deployment-blocks.md` for all context variables and patterns. ### Publish Output and Upstream Input Blocks Link Stacks together by publishing outputs from one Stack and consuming them in another: ```hcl # In network Stack - publish outputs publish_output "vpc_id_network" { type = string value = deployment.network.vpc_id } # In application Stack - consume outputs upstream_input "network_stack" { type = "stack" source = "app.terraform.io/my-org/my-project/networking-stack" } deployment "app" { inputs = { vpc_id = upstream_input.network_stack.vpc_id_network } } ``` See `references/linked-stacks.md` for complete documentation and examples. ## Terraform Stacks CLI **Note**: Terraform Stacks is Generally Available (GA) as of Terraform CLI v1.13+. Stacks now count toward Resources Under Management (RUM) for HCP Terraform billing. ### Initialize and Validate ```bash terraform stacks init # Download providers, modules, generate lock file terraform stacks providers-lock # Regenerate lock file (add platforms if needed) terraform stacks validate # Check syntax without uploading ``` ### Deployment Workflow **Important**: No `plan` or `apply` commands. Upload configuration triggers deployment runs automatically. ```bash # 1. Upload configuration (triggers deployment runs) terraform stacks configuration upload # 2. Monitor deployments terraform stacks deployment-run list # List runs (non-interactive) terraform stacks deployment-group watch -deployment-group=... # Stream status updates # 3. Approve deployments (if auto-approve not configured) terraform stacks deployment-run approve-all-plans -deployment-run-id=... terraform stacks deployment-group approve-all-plans -deployment-group=... terraform stacks deployment-run cancel -deployment-run-id=... # Cancel if needed ``` ### Configuration Management ```bash terraform stacks configuration list # List configuration versions terraform stacks configuration fetch -configuration-id=... # Download configuration terraform stacks configuration watch # Monitor upload status ``` ### Other Commands ```bash terraform stacks create # Create new Stack (interactive) terraform stacks fmt # Format Stack files terraform stacks list # Show all Stacks terraform stacks version # Display version terraform stacks deployment-group rerun -deployment-group=... # Rerun deployment ``` ## Monitoring Deployments with HCP Terraform API For programmatic monitoring in automation, CI/CD, or non-interactive environments (like AI agents), use the HCP Terraform API instead of CLI watch commands. The API provides endpoints for: - Configuration status and validation - Deployment group summaries - Deployment run status - Deployment step details (plan/apply) - Error diagnostics with file locations and code snippets - Stack outputs via artifacts endpoint **Key points:** - CLI watch commands stream indefinitely and don't work in automation - Use artifacts endpoint to retrieve Stack outputs: `GET /api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description` - Diagnostics endpoint requires `stack_deployment_step_id` query parameter - Artifacts endpoint returns HTTP 307 redirect (use `curl -L`) For complete API workflow, authentication, polling best practices, and example scripts, see `references/api-monitoring.md`. ## Common Patterns **Component Dependencies**: Dependencies are automatically inferred when one component references another's output (e.g., `subnet_ids = component.vpc.private_subnet_ids`). **Multi-Region Deployment**: Use `for_each` on providers and components to deploy across multiple regions. Each region gets its own provider configuration and component instances. **Deferred Changes**: Stacks support deferred changes to handle dependencies where values are only known after apply. This enables complex multi-component deployments where some resources depend on runtime values from other components (cluster endpoints, generated passwords, etc.). For complete examples including multi-region deployments, component dependencies, deferred changes patterns, and linked Stacks, see `references/examples.md`. ## Best Practices 1. **Component Granularity**: Create components for logical infrastructure units that share a lifecycle 2. **Module Compatibility**: - Modules used with Stacks cannot include provider blocks (configure providers in Stack configuration) - **Test public registry modules** before using in production Stacks - some modules may have compatibility issues - Consider using raw resources for critical infrastructure if module compatibility is uncertain - Example: Some terraform-aws-modules versions have been found to have compatibility issues with Stacks (e.g., ALB and ECS modules) 3. **State Isolation**: Each deployment has its own isolated state 4. **Input Variables**: Use variables for values that differ across deployments; use locals for shared values 5. **Provider Lock Files**: Always generate and commit `.terraform.lock.hcl` to version control 6. **Naming Conventions**: Use descriptive names for components and deployments 7. **Deployment Groups**: You can organize deployments into deployment groups. Deployment groups enable auto-approval rules, logical organization, and provide a foundation for scaling. Deployment groups are an HCP Terraform Premium tier feature 8. **Testing**: Test Stack configurations in dev/staging deployments before production ## Troubleshooting **Circular Dependencies**: Refactor to break circular references or use intermediate components. **Deployment Destruction**: Cannot destroy from UI. Set `destroy = true` in deployment block, upload configuration, and HCP Terraform creates a destroy run. **Empty Diagnostics**: Add required `stack_deployment_step_id` query parameter to diagnostics API requests. **Module Compatibility**: Test public registry modules before production use. Some modules may have compatibility issues with Stacks. ## References For detailed documentation, see: - `references/component-blocks.md` - Complete component block reference with all arguments and syntax - `references/deployment-blocks.md` - Complete deployment block reference with all configuration options - `references/linked-stacks.md` - Publish outputs and upstream inputs for linking Stacks together - `references/examples.md` - Complete working examples for multi-region and component dependencies - `references/api-monitoring.md` - Full API workflow for programmatic monitoring and automation - `references/troubleshooting.md` - Detailed troubleshooting guide for common issues and solutions ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/api-monitoring.md ================================================ # API Monitoring Reference Complete guide for monitoring Terraform Stack deployments using the HCP Terraform API. Use this approach for automation, CI/CD pipelines, and non-interactive environments like AI agents. ## Table of Contents 1. [When to Use the API](#when-to-use-the-api) 2. [Authentication](#authentication) 3. [API Monitoring Workflow](#api-monitoring-workflow) 4. [Detailed Endpoint Reference](#detailed-endpoint-reference) 5. [Notes for AI Agents and Automation](#notes-for-ai-agents-and-automation) ## When to Use the API Use the HCP Terraform API instead of CLI commands when: - Running in non-interactive environments (CI/CD, automation scripts) - Building tools or integrations that need programmatic access - Monitoring multiple Stacks simultaneously - Implementing custom retry logic or error handling - Working in environments where streaming CLI commands don't work **CLI commands that don't work in automation:** - `terraform stacks deployment-run watch` - Streams output, blocks indefinitely - `terraform stacks deployment-group watch` - Streams output, blocks indefinitely - `terraform stacks configuration watch` - Streams output, blocks indefinitely ## Authentication ### Extract API Token from Credentials File ```bash TOKEN=$(jq -r '.credentials["app.terraform.io"].token' ~/.terraform.d/credentials.tfrc.json) ``` ### Alternative: Use Environment Variable ```bash export TFC_TOKEN="your-token-here" TOKEN=$TFC_TOKEN ``` ### API Request Headers All API requests require these headers: ```bash -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/vnd.api+json" ``` ## API Monitoring Workflow After uploading a configuration with `terraform stacks configuration upload`, follow this sequence to monitor deployment progress: ### Step 1: Get Configuration Status **Endpoint:** `GET /api/v2/stack-configurations/{configuration-id}` **Purpose:** Verify configuration upload completed successfully and get the configuration details. **Request:** ```bash curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "https://app.terraform.io/api/v2/stack-configurations/{configuration-id}" | jq '.' ``` **Response Fields:** - `attributes.status` - Configuration processing status (pending/completed) - `attributes.sequence-number` - Version number of this configuration - `attributes.components-detected` - Number of components found - `attributes.deployments-detected` - Number of deployments found **Example Response:** ```json { "data": { "id": "stc-ABC123", "type": "stack-configurations", "attributes": { "status": "completed", "sequence-number": 5, "components-detected": 3, "deployments-detected": 2, "created-at": "2024-01-15T10:30:00.000Z", "updated-at": "2024-01-15T10:30:45.000Z" } } } ``` ### Step 2: Get Deployment Group Summaries **Endpoint:** `GET /api/v2/stack-configurations/{configuration-id}/stack-deployment-group-summaries` **Purpose:** Get list of deployment groups, their IDs, and current status summary. **Request:** ```bash curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "https://app.terraform.io/api/v2/stack-configurations/{configuration-id}/stack-deployment-group-summaries" | jq '.' ``` **Response Fields:** - `id` - Deployment group ID (needed for next step) - `attributes.name` - Deployment group name (e.g., `dev_default`) - `attributes.status` - Overall status (running/succeeded/failed) - `attributes.status-counts` - Breakdown of deployment statuses **Example Response:** ```json { "data": [ { "id": "sdg-XYZ789", "type": "stack-deployment-group-summaries", "attributes": { "name": "dev_default", "status": "running", "status-counts": { "pending": 0, "running": 1, "succeeded": 1, "failed": 0 } } } ] } ``` ### Step 3: Get Deployment Runs **Endpoint:** `GET /api/v2/stack-deployment-groups/{group-id}/stack-deployment-runs` **Purpose:** Get list of deployment runs for a specific group with their current status. **Request:** ```bash curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "https://app.terraform.io/api/v2/stack-deployment-groups/{group-id}/stack-deployment-runs" | jq '.' ``` **Response Fields:** - `id` - Deployment run ID (needed for next step) - `attributes.status` - Current status (planning/planned/applying/applied/failed) - `attributes.created-at` - Run start time - `attributes.updated-at` - Last update time **Example Response:** ```json { "data": [ { "id": "sdr-123ABC", "type": "stack-deployment-runs", "attributes": { "status": "planning", "created-at": "2024-01-15T10:31:00.000Z", "updated-at": "2024-01-15T10:31:15.000Z" } } ] } ``` ### Step 4: Get Deployment Steps **Endpoint:** `GET /api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps` **Purpose:** Get detailed information about individual plan and apply steps. **Request:** ```bash curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps" | jq '.' ``` **Response Fields:** - `id` - Step ID (needed for diagnostics and outputs) - `attributes.operation-type` - Type of operation (plan/apply) - `attributes.status` - Step status (running/completed/failed) - `attributes.component-name` - Which component is being processed **Example Response:** ```json { "data": [ { "id": "sds-PlanStep123", "type": "stack-deployment-steps", "attributes": { "operation-type": "plan", "status": "completed", "component-name": "vpc", "created-at": "2024-01-15T10:31:05.000Z", "completed-at": "2024-01-15T10:31:30.000Z" } }, { "id": "sds-ApplyStep456", "type": "stack-deployment-steps", "attributes": { "operation-type": "apply", "status": "running", "component-name": "vpc", "created-at": "2024-01-15T10:32:00.000Z" } } ] } ``` ### Step 5: Get Error Diagnostics (When Deployment Fails) **Endpoint:** `GET /api/v2/stack-deployment-steps/{step-id}/stack-diagnostics` **Purpose:** Retrieve detailed error messages when a deployment step fails. **Critical:** The `stack_deployment_step_id` query parameter is **required**. Without it, the API returns empty results. **Request:** ```bash curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | jq '.' ``` **Response Fields:** - `attributes.severity` - Diagnostic level (error/warning) - `attributes.summary` - Brief error description - `attributes.detail` - Detailed error message - `attributes.diags` - Array of diagnostic objects with file locations and code snippets **Example Response (Error with Details):** ```json { "data": [ { "id": "stf-ErrorExampleId", "type": "stack-diagnostics", "attributes": { "severity": "error", "summary": "Diagnostics reported", "detail": "2 errors", "diags": [ { "summary": "Unsupported attribute", "detail": "This object does not have an attribute named \"target_id\".", "range": { "filename": "main.tf", "start": { "line": 634, "column": 33 }, "end": { "line": 634, "column": 43 }, "source": "registry.terraform.io/terraform-aws-modules/alb/aws@9.17.0//main.tf" }, "snippet": { "code": " target_id = each.value.target_id", "context": "resource \"aws_lb_target_group_attachment\" \"this\"" } }, { "summary": "Invalid reference", "detail": "A reference to a resource type must be followed by at least one attribute access.", "range": { "filename": "main.tf", "start": { "line": 142, "column": 15 }, "end": { "line": 142, "column": 28 }, "source": "local-module//main.tf" }, "snippet": { "code": " vpc_id = aws_vpc.main", "context": "resource \"aws_subnet\" \"private\"" } } ], "acknowledged": false, "created-at": "2024-01-15T10:32:15.000Z" } } ] } ``` **Parsing Diagnostics:** Extract error information with jq: ```bash # Get error summaries curl -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | \ jq -r '.data[].attributes.diags[]? | "\(.summary): \(.detail)"' # Get file locations curl -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | \ jq -r '.data[].attributes.diags[]? | "\(.range.filename):\(.range.start.line)"' ``` ### Step 6: Get Stack Outputs (After Successful Deployment) **Endpoint:** `GET /api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description` **Purpose:** Retrieve Stack outputs after a successful deployment completes. **Important Notes:** - This endpoint returns HTTP 307 redirect - use `curl -L` to follow redirects automatically - This is currently the **only way** to retrieve Stack outputs programmatically - This endpoint is **not documented** in public API documentation - You need the final apply step ID from Step 4 **Request:** ```bash curl -L -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description" ``` **Response Structure:** The artifact response includes an `.outputs` object where each output contains a `change.after` property with the actual output value: ```json { "outputs": { "alb_url": { "change": { "actions": ["no-op"], "before": "http://my-alb-123456789.us-west-2.elb.amazonaws.com", "after": "http://my-alb-123456789.us-west-2.elb.amazonaws.com", "after_unknown": false, "before_sensitive": false, "after_sensitive": false }, "type": "string" }, "ecr_repository_url": { "change": { "actions": ["no-op"], "before": "123456789.dkr.ecr.us-west-2.amazonaws.com/my-repo", "after": "123456789.dkr.ecr.us-west-2.amazonaws.com/my-repo", "after_unknown": false, "before_sensitive": false, "after_sensitive": false }, "type": "string" } } } ``` **Extract Only Output Values:** ```bash curl -L -s --header "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description" | \ jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"' ``` **Example Output:** ``` alb_url: http://my-alb-123456789.us-west-2.elb.amazonaws.com ecr_repository_url: 123456789.dkr.ecr.us-west-2.amazonaws.com/my-repo ``` ## Detailed Endpoint Reference ### Available Artifact Types The artifacts endpoint accepts these `name` parameter values: - `plan-description` - Terraform plan output in JSON format - `plan-debug-log` - Detailed debug logs from plan operation - `apply-description` - Terraform apply output including outputs (JSON format) - `apply-debug-log` - Detailed debug logs from apply operation ### Polling Best Practices **Recommended polling intervals:** - Configuration status: Check every 5 seconds until status is "completed" - Deployment runs: Check every 10 seconds during active deployment - Deployment steps: Check every 10 seconds for individual step status **Implement exponential backoff:** ```bash # Example polling script with backoff RETRY_COUNT=0 MAX_RETRIES=30 BACKOFF=5 while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do STATUS=$(curl -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}" | \ jq -r '.data.attributes.status') if [ "$STATUS" = "applied" ] || [ "$STATUS" = "failed" ]; then echo "Deployment finished with status: $STATUS" break fi echo "Current status: $STATUS. Waiting ${BACKOFF}s..." sleep $BACKOFF RETRY_COUNT=$((RETRY_COUNT + 1)) done ``` ## Notes for AI Agents and Automation ### CLI Command Limitations **These CLI commands DO NOT work in automation:** - `terraform stacks deployment-run watch` - Streams output, blocks indefinitely - `terraform stacks deployment-group watch` - Streams output, blocks indefinitely - `terraform stacks configuration watch` - Streams output, blocks indefinitely **Solution:** Use API polling instead of watch commands. ### No Direct Output Command There is currently no CLI command to retrieve Stack outputs. You must: 1. Use API to get deployment steps 2. Find the final apply step ID 3. Request the `apply-description` artifact 4. Parse JSON to extract outputs ### Handling Redirects The artifacts endpoint returns HTTP 307 redirect to the actual artifact location. Ensure your HTTP client follows redirects: **curl:** Use `-L` flag **Python requests:** Set `allow_redirects=True` (default) **Node.js fetch:** Set `redirect: 'follow'` (default) ### Error Handling **Common API errors:** - **401 Unauthorized:** Invalid or expired token - refresh credentials - **404 Not Found:** Invalid ID or resource doesn't exist yet - retry with backoff - **429 Too Many Requests:** Rate limited - implement exponential backoff - **Empty diagnostics:** Missing required `stack_deployment_step_id` query parameter ### Complete Monitoring Script Example ```bash #!/bin/bash # Configuration TOKEN=$(jq -r '.credentials["app.terraform.io"].token' ~/.terraform.d/credentials.tfrc.json) CONFIG_ID="stc-ABC123" BASE_URL="https://app.terraform.io/api/v2" # Helper function api_get() { curl -s -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/vnd.api+json" \ "$1" } # 1. Wait for configuration to complete echo "Checking configuration status..." while true; do STATUS=$(api_get "$BASE_URL/stack-configurations/$CONFIG_ID" | jq -r '.data.attributes.status') [ "$STATUS" = "completed" ] && break echo "Configuration status: $STATUS. Waiting..." sleep 5 done # 2. Get deployment groups echo "Getting deployment groups..." GROUP_ID=$(api_get "$BASE_URL/stack-configurations/$CONFIG_ID/stack-deployment-group-summaries" | \ jq -r '.data[0].id') # 3. Get deployment run echo "Getting deployment run..." RUN_ID=$(api_get "$BASE_URL/stack-deployment-groups/$GROUP_ID/stack-deployment-runs" | \ jq -r '.data[0].id') # 4. Monitor deployment run echo "Monitoring deployment run: $RUN_ID" while true; do STATUS=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID" | jq -r '.data.attributes.status') echo "Deployment status: $STATUS" if [ "$STATUS" = "applied" ]; then echo "Deployment succeeded!" # 5. Get outputs from final apply step APPLY_STEP=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID/stack-deployment-steps" | \ jq -r '.data[] | select(.attributes["operation-type"] == "apply") | .id' | tail -1) echo "Retrieving outputs from step: $APPLY_STEP" curl -L -s -H "Authorization: Bearer $TOKEN" \ "$BASE_URL/stack-deployment-steps/$APPLY_STEP/artifacts?name=apply-description" | \ jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"' break fi if [ "$STATUS" = "failed" ]; then echo "Deployment failed!" # Get error diagnostics FAILED_STEP=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID/stack-deployment-steps" | \ jq -r '.data[] | select(.attributes.status == "failed") | .id' | head -1) echo "Error diagnostics from step: $FAILED_STEP" api_get "$BASE_URL/stack-deployment-steps/$FAILED_STEP/stack-diagnostics?stack_deployment_step_id=$FAILED_STEP" | \ jq -r '.data[].attributes.diags[]? | "\(.summary): \(.detail)"' exit 1 fi sleep 10 done ``` This script demonstrates a complete monitoring workflow from configuration upload to output retrieval with error handling. ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/component-blocks.md ================================================ # Component Configuration Block Reference Complete reference for all blocks available in Terraform Stack component configuration files (`.tfcomponent.hcl`). ## Table of Contents 1. [Variable Block](#variable-block) 2. [Required Providers Block](#required-providers-block) 3. [Provider Block](#provider-block) 4. [Component Block](#component-block) 5. [Output Block](#output-block) 6. [Locals Block](#locals-block) 7. [Removed Block](#removed-block) ## Variable Block Declares input variables for Stack configuration. ### Syntax ```hcl variable "variable_name" { type = description = "" default = sensitive = nullable = ephemeral = } ``` ### Arguments - **type** (required): Data type (string, number, bool, list, map, object, set, tuple, any) - **description** (optional): Variable description - **default** (optional): Default value - **sensitive** (optional, default false): Mark as sensitive to redact from logs - **nullable** (optional, default true): Whether null is allowed - **ephemeral** (optional, default false): Do not persist to state file ### Differences from Traditional Terraform - **type** is required (not optional) - **validation** argument is not supported ### Examples ```hcl variable "aws_region" { type = string description = "AWS region for infrastructure" default = "us-west-1" } variable "identity_token" { type = string description = "OIDC identity token" ephemeral = true } variable "subnet_config" { type = object({ cidr_block = string availability_zone = string map_public_ip = bool }) } ``` For complete variable examples in context, see `examples.md`. ## Required Providers Block Declares provider dependencies. ### Syntax ```hcl required_providers { = { source = "" version = "" } } ``` ### Arguments - **source** (required): Provider source address (e.g., "hashicorp/aws") - **version** (optional): Version constraint (e.g., "~> 5.0") ### Examples ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } random = { source = "hashicorp/random" version = "~> 3.5.0" } azurerm = { source = "hashicorp/azurerm" version = ">= 3.0" } } ``` ## Provider Block Configures provider instances. ### Syntax ```hcl provider "" "" { for_each = # Optional config { } } ``` ### Arguments - **provider_type** (label 1, required): Provider type (e.g., "aws", "azurerm") - **alias** (label 2, required): Unique identifier for this provider configuration - **for_each** (optional): Create multiple provider instances from a map or set - **config** (required): Nested block containing provider-specific configuration ### Key Differences from Traditional Terraform 1. Alias is defined in block header, not as an argument 2. Configuration goes in a nested `config` block 3. Supports `for_each` meta-argument 4. Provider configurations are treated as first-class values ### Example ```hcl provider "aws" "main" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` For complete provider examples including for_each and multi-cloud patterns, see `examples.md`. ## Component Block Defines infrastructure components to include in the Stack. ### Syntax ```hcl component "" { for_each = # Optional source = "" inputs = { = } providers = { = provider..[] } } ``` ### Arguments - **component_name** (label, required): Unique identifier for this component - **for_each** (optional): Create multiple component instances - **source** (required): Module source (see [Source Argument](#source-argument) below) - **version** (optional): Version constraint for registry-based sources only - **inputs** (required): Map of input variables for the module - **providers** (required): Map of provider configurations ### Source Argument The `source` argument accepts the same module sources as traditional Terraform configurations. **Local File Path:** ```hcl source = "./modules/vpc" source = "../shared-modules/networking" ``` **Public Terraform Registry:** ```hcl source = "terraform-aws-modules/vpc/aws" source = "hashicorp/consul/aws" ``` Format: `//` **Private HCP Terraform Registry:** ```hcl source = "app.terraform.io/my-org/vpc/aws" source = "app.terraform.io/example-corp/networking/azurerm" ``` Format: `///` - **HCP Terraform (SaaS)**: Use hostname `app.terraform.io` - **Terraform Enterprise**: Use your instance hostname (e.g., `terraform.mycompany.com`) - **Generic hostname**: Use `localterraform.com` for deployments spanning multiple Terraform Enterprise instances **Git Repository:** ```hcl source = "git::https://github.com/org/repo.git//modules/vpc?ref=v1.0.0" source = "git::ssh://git@github.com/org/repo.git//modules/vpc?ref=main" ``` **HTTP/HTTPS Archive:** ```hcl source = "https://example.com/modules/vpc-module.tar.gz" ``` ### Version Argument The `version` argument is supported only for registry-based sources (public and private registries). Local file paths and Git sources do not support the `version` argument. ```hcl component "vpc" { source = "app.terraform.io/my-org/vpc/aws" version = "~> 2.0" # Semantic versioning constraint inputs = { cidr_block = var.vpc_cidr } providers = { aws = provider.aws.main } } ``` **Note**: Modules sourced from local file paths always share the same version as their caller and cannot have independent version constraints. ### Component References Access component outputs using: `component..` For components with `for_each`: `component.[].` ### Examples **Basic Component:** ```hcl component "vpc" { source = "app.terraform.io/my-org/vpc/aws" version = "2.1.0" inputs = { cidr_block = var.vpc_cidr name_prefix = var.name_prefix } providers = { aws = provider.aws.main } } ``` **Component with Dependencies:** ```hcl component "database" { source = "./modules/rds" inputs = { vpc_id = component.vpc.vpc_id subnet_ids = component.vpc.private_subnet_ids security_group_ids = [component.security.database_sg_id] engine_version = var.db_engine_version } providers = { aws = provider.aws.main } } ``` For complete component examples including for_each, multi-region, public registry, and multi-provider patterns, see `examples.md`. ## Output Block Exposes values from Stack configuration. ### Syntax ```hcl output "" { type = description = "" value = sensitive = ephemeral = } ``` ### Arguments - **output_name** (label, required): Unique identifier for this output - **type** (required): Data type of the output - **description** (optional): Output description - **value** (required): Expression to output - **sensitive** (optional, default false): Mark as sensitive - **ephemeral** (optional, default false): Ephemeral value ### Differences from Traditional Terraform - **type** is required - **precondition** block is not supported ### Examples ```hcl output "vpc_id" { type = string description = "VPC ID" value = component.vpc.vpc_id } output "instance_details" { type = object({ id = string public_ip = string private_ip = string }) description = "EC2 instance details" value = { id = component.compute.instance_id public_ip = component.compute.public_ip private_ip = component.compute.private_ip } } ``` For complete output examples including sensitive outputs and for expressions, see `examples.md`. ## Locals Block Defines local values for reuse within the Stack configuration. ### Syntax ```hcl locals { = } ``` ### Example ```hcl locals { common_tags = { Environment = var.environment ManagedBy = "Terraform Stacks" Project = var.project_name } name_prefix = "${var.project_name}-${var.environment}" region_config = { for region in var.regions : region => { name_suffix = region instance_count = var.environment == "prod" ? 3 : 1 } } } ``` ## Removed Block Declares components to be removed from the Stack. ### Syntax ```hcl removed { from = component. source = "" providers = { = provider.. } } ``` ### Arguments - **from** (required): Reference to the component being removed - **source** (required): Original module source - **providers** (required): Provider configurations needed for removal ### Important Notes - Required for safe component removal - Must include all providers the component used - Do not remove providers before removing components that use them ### Examples ```hcl removed { from = component.old_component source = "./modules/deprecated-module" providers = { aws = provider.aws.main } } removed { from = component.legacy_regional source = "registry.terraform.io/example/legacy/aws" providers = { aws = provider.aws.main random = provider.random.main } } ``` ## Provider References in Component Blocks ### Single Provider ```hcl providers = { aws = provider.aws.main } ``` ### Multiple Providers ```hcl providers = { aws = provider.aws.main random = provider.random.main tls = provider.tls.main } ``` ### Provider from for_each ```hcl providers = { aws = provider.aws.regional[each.value] } ``` ### Aliased Providers in Module If module requires specific provider aliases: ```hcl providers = { aws.source = provider.aws.us_east aws.dest = provider.aws.eu_west } ``` ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/deployment-blocks.md ================================================ # Deployment Configuration Block Reference Complete reference for all blocks available in Terraform Stack deployment configuration files (`.tfdeploy.hcl`). ## Table of Contents 1. [Identity Token Block](#identity-token-block) 2. [Locals Block](#locals-block) 3. [Deployment Block](#deployment-block) 4. [Deployment Group Block](#deployment-group-block) 5. [Deployment Auto-Approve Block](#deployment-auto-approve-block) **Note**: For Publish Output and Upstream Input blocks (linked Stacks), see `linked-stacks.md`. ## Identity Token Block Generates JWT tokens for OIDC authentication with cloud providers. ### Syntax ```hcl identity_token "" { audience = [] } ``` ### Arguments - **token_name** (label, required): Unique identifier for this token - **audience** (required): List of audience strings for the JWT ### Accessing Token Reference the JWT using: `identity_token..jwt` ### Cloud Provider Audiences **AWS:** ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } ``` **Azure:** ```hcl identity_token "azure" { audience = ["api://AzureADTokenExchange"] } ``` **Google Cloud:** ```hcl identity_token "gcp" { audience = ["//iam.googleapis.com/projects//locations/global/workloadIdentityPools//providers/"] } ``` **Setup Documentation:** For detailed instructions on configuring OIDC/workload identity for each cloud provider (including IAM roles, trust policies, and federated credentials), see: https://developer.hashicorp.com/terraform/cloud-docs/dynamic-provider-credentials ### Examples **Single Token:** ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } deployment "production" { inputs = { identity_token = identity_token.aws.jwt role_arn = var.role_arn } } ``` For complete working examples including multi-region identity token usage, see `examples.md`. ## Locals Block Defines local values for reuse within deployment configuration. ### Syntax ```hcl locals { = } ``` ### Example ```hcl locals { aws_regions = ["us-west-1", "us-east-1", "eu-west-1"] role_arn = "arn:aws:iam::123456789012:role/hcp-terraform-stacks" common_inputs = { project_name = "my-app" environment = "production" } } ``` ## Deployment Block Defines deployment instances of the Stack. ### Syntax ```hcl deployment "" { inputs = { = } } ``` ### Arguments - **deployment_name** (label, required): Unique identifier for this deployment - **inputs** (required): Map of input variable values - **destroy** (optional, default: false): Boolean flag to destroy this deployment ### Constraints - Minimum 1 deployment per Stack - Maximum 20 deployments per Stack - No meta-arguments supported (no `for_each`, `count`) ### Destroying a Deployment To safely remove a deployment from your Stack: 1. Set `destroy = true` in the deployment block 2. Apply the plan through HCP Terraform 3. After successful destruction, remove the deployment block from your configuration **Important**: Using the `destroy` argument ensures your configuration has the provider authentication necessary to properly destroy the deployment's resources. **Example:** ```hcl deployment "old_environment" { inputs = { aws_region = "us-west-1" instance_count = 2 role_arn = local.role_arn identity_token = identity_token.aws.jwt } destroy = true # Mark for destruction } ``` After applying this plan and the deployment is destroyed, remove the entire `deployment "old_environment"` block from your configuration. ### Examples **Single Deployment:** ```hcl deployment "production" { inputs = { aws_region = "us-west-1" instance_count = 5 instance_type = "t3.large" role_arn = local.role_arn identity_token = identity_token.aws.jwt } } ``` **Using Locals for Multiple Deployments:** ```hcl locals { common_inputs = { role_arn = "arn:aws:iam::123456789012:role/terraform" identity_token = identity_token.aws.jwt project_name = "my-app" } } deployment "dev" { inputs = merge(local.common_inputs, { aws_region = "us-east-1" instance_count = 1 environment = "dev" }) } deployment "prod" { inputs = merge(local.common_inputs, { aws_region = "us-west-1" instance_count = 5 environment = "prod" }) } ``` For complete multi-environment and multi-region deployment examples, see `examples.md`. ## Deployment Group Block Groups deployments together to configure shared settings and auto-approval rules (HCP Terraform Premium tier feature). ### Syntax ```hcl deployment_group "" { deployments = [] } ``` ### Arguments - **group_name** (label, required): Unique identifier for this deployment group - **deployments** (required): List of deployment references to include in this group ### Purpose Deployment groups allow you to: - Organize deployments logically (by environment, team, region, etc.) - Configure shared auto-approval rules for multiple deployments - Manage deployments more effectively at scale - Establish consistent configuration patterns across all Stacks ### Examples **Single Deployment Group (Best Practice):** ```hcl deployment "production" { inputs = { aws_region = "us-west-1" instance_count = 5 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment_group "production" { deployments = [deployment.production] } ``` **Multiple Deployment Groups:** ```hcl deployment_group "non_production" { deployments = [ deployment.development, deployment.staging ] } deployment_group "production" { deployments = [ deployment.prod_us_east, deployment.prod_us_west, deployment.prod_eu_west ] } ``` ## Deployment Auto-Approve Block Defines rules that automatically approve deployment plans based on specific conditions (HCP Terraform Premium feature). ### Syntax ```hcl deployment_auto_approve "" { deployment_group = deployment_group. check { condition = reason = "" } } ``` ### Arguments - **rule_name** (label, required): Unique identifier for this auto-approve rule - **deployment_group** (required): Reference to the deployment group this rule applies to - **check** (required, one or more): Condition that must be met for auto-approval ### Context Variables Access plan information through `context` object: - `context.plan.applyable` - Boolean: plan succeeded without errors - `context.plan.changes.add` - Number: resources to add - `context.plan.changes.change` - Number: resources to change - `context.plan.changes.remove` - Number: resources to remove - `context.plan.changes.import` - Number: resources to import ### Important Notes - All checks must pass for auto-approval to occur - If any check fails, manual approval is required - HCP Terraform displays the failure reason from failed checks - Auto-approve rules only apply to deployments in the specified deployment group ### Examples **Auto-approve Successful Plans:** ```hcl deployment_group "canary" { deployments = [ deployment.dev, deployment.staging ] } deployment_auto_approve "applyable_plans" { deployment_group = deployment_group.canary check { condition = context.plan.applyable reason = "Plan must be applyable without errors" } } ``` **Auto-approve Non-Destructive Changes:** ```hcl deployment_group "production" { deployments = [ deployment.prod_primary, deployment.prod_secondary ] } deployment_auto_approve "safe_production_changes" { deployment_group = deployment_group.production check { condition = context.plan.changes.remove == 0 reason = "Production deletions require manual approval" } check { condition = context.plan.applyable reason = "Plan must be successful" } } ``` **Graduated Rollout Pattern:** ```hcl deployment_group "canary" { deployments = [deployment.canary] } deployment_group "production" { deployments = [ deployment.prod_us, deployment.prod_eu, deployment.prod_asia ] } # Canary auto-approves with strict checks deployment_auto_approve "canary_strict" { deployment_group = deployment_group.canary check { condition = context.plan.changes.remove == 0 reason = "Canary cannot delete resources" } check { condition = context.plan.changes.change <= 5 reason = "Canary limited to 5 resource changes" } check { condition = context.plan.applyable reason = "Plan must be applyable" } } # Production requires manual approval after canary validation ``` For complete deployment configuration examples with all blocks, see `examples.md`. ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/examples.md ================================================ # Terraform Stacks Complete Examples Complete, working examples for common Terraform Stacks scenarios. ## Table of Contents 1. [Simple Single-Region Stack](#simple-single-region-stack) 2. [Stack with Private Registry Modules](#stack-with-private-registry-modules) 3. [Multi-Environment Stack](#multi-environment-stack) 4. [Multi-Region Stack](#multi-region-stack) 5. [Linked Stacks (Cross-Stack Dependencies)](#linked-stacks-cross-stack-dependencies) 6. [Multi-Cloud Stack](#multi-cloud-stack) 7. [Complete AWS Production Stack](#complete-aws-production-stack) 8. [Destroying Deployments](#destroying-deployments) ## Simple Single-Region Stack Basic Stack with a single environment deployment. ### File Structure ``` simple-stack/ ├── variables.tfcomponent.hcl ├── providers.tfcomponent.hcl ├── components.tfcomponent.hcl ├── deployments.tfdeploy.hcl └── modules/ └── webapp/ ├── main.tf ├── variables.tf └── outputs.tf ``` ### variables.tfcomponent.hcl ```hcl variable "aws_region" { type = string default = "us-west-1" } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } variable "app_name" { type = string } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } } provider "aws" "main" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` ### components.tfcomponent.hcl ```hcl component "webapp" { source = "./modules/webapp" inputs = { app_name = var.app_name region = var.aws_region } providers = { aws = provider.aws.main } } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } deployment "production" { inputs = { aws_region = "us-west-1" app_name = "my-webapp" role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "production" { deployments = [deployment.production] } ``` ## Stack with Private Registry Modules Example Stack using modules from a private HCP Terraform registry, combining both private and public registry sources. ### File Structure ``` private-registry-stack/ ├── variables.tfcomponent.hcl ├── providers.tfcomponent.hcl ├── components.tfcomponent.hcl ├── outputs.tfcomponent.hcl └── deployments.tfdeploy.hcl ``` ### variables.tfcomponent.hcl ```hcl variable "aws_region" { type = string default = "us-west-2" } variable "environment" { type = string } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } variable "vpc_cidr" { type = string default = "10.0.0.0/16" } variable "app_name" { type = string } variable "db_password" { type = string sensitive = true } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } random = { source = "hashicorp/random" version = "~> 3.5.0" } } provider "aws" "main" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } default_tags { tags = { Environment = var.environment ManagedBy = "Terraform Stacks" Application = var.app_name } } } } provider "random" "main" { config {} } ``` ### components.tfcomponent.hcl ```hcl locals { name_prefix = "${var.app_name}-${var.environment}" common_tags = { Project = var.app_name Environment = var.environment } } # Using a private registry module for VPC component "vpc" { source = "app.terraform.io/my-org/vpc/aws" version = "2.1.0" inputs = { name_prefix = local.name_prefix cidr_block = var.vpc_cidr availability_zones = ["${var.aws_region}a", "${var.aws_region}b", "${var.aws_region}c"] enable_nat_gateway = true single_nat_gateway = var.environment != "prod" tags = local.common_tags } providers = { aws = provider.aws.main } } # Using a private registry module for security groups component "security_groups" { source = "app.terraform.io/my-org/security-groups/aws" version = "1.5.2" inputs = { vpc_id = component.vpc.vpc_id name_prefix = local.name_prefix environment = var.environment } providers = { aws = provider.aws.main } } # Using a public registry module for RDS component "database" { source = "terraform-aws-modules/rds/aws" version = "~> 6.0" inputs = { identifier = "${local.name_prefix}-db" engine = "postgres" engine_version = "15.3" family = "postgres15" major_engine_version = "15" instance_class = var.environment == "prod" ? "db.t3.large" : "db.t3.micro" allocated_storage = var.environment == "prod" ? 100 : 20 db_name = replace(var.app_name, "-", "_") username = "dbadmin" password = var.db_password port = 5432 db_subnet_group_name = component.vpc.database_subnet_group_name vpc_security_group_ids = [component.security_groups.database_sg_id] backup_retention_period = var.environment == "prod" ? 30 : 7 skip_final_snapshot = var.environment != "prod" deletion_protection = var.environment == "prod" tags = local.common_tags } providers = { aws = provider.aws.main } } # Using a private registry module for application infrastructure component "application" { source = "app.terraform.io/my-org/ecs-application/aws" version = "3.2.1" inputs = { name_prefix = local.name_prefix vpc_id = component.vpc.vpc_id private_subnet_ids = component.vpc.private_subnet_ids public_subnet_ids = component.vpc.public_subnet_ids app_security_group_id = component.security_groups.app_sg_id container_image = "my-org/my-app:latest" container_port = 8080 desired_count = var.environment == "prod" ? 3 : 1 environment_variables = { ENVIRONMENT = var.environment DATABASE_HOST = component.database.db_instance_endpoint DATABASE_NAME = component.database.db_instance_name } tags = local.common_tags } providers = { aws = provider.aws.main } } ``` ### outputs.tfcomponent.hcl ```hcl output "vpc_id" { type = string description = "VPC ID" value = component.vpc.vpc_id } output "application_url" { type = string description = "Application load balancer URL" value = component.application.load_balancer_dns } output "database_endpoint" { type = string description = "Database endpoint" value = component.database.db_instance_endpoint sensitive = true } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" } deployment "development" { inputs = { aws_region = "us-west-2" environment = "dev" app_name = "myapp" vpc_cidr = "10.0.0.0/16" db_password = "dev-password-change-me" role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "production" { inputs = { aws_region = "us-east-1" environment = "prod" app_name = "myapp" vpc_cidr = "10.1.0.0/16" db_password = "prod-password-use-secrets-manager" role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "development" { deployments = [deployment.development] } deployment_group "production" { deployments = [deployment.production] } ``` ### Key Points - **Private registry modules** use the format `app.terraform.io///` - **Version constraints** ensure consistent module versions across environments - **Mixed sources**: Combining private registry modules (VPC, security groups, application) with public registry modules (RDS) - **Authentication**: HCP Terraform workspaces automatically authenticate to private registries; CLI users need credentials configured - **Terraform Enterprise**: Replace `app.terraform.io` with your instance hostname ## Multi-Environment Stack Stack with development, staging, and production deployments. ### variables.tfcomponent.hcl ```hcl variable "aws_region" { type = string } variable "environment" { type = string } variable "instance_count" { type = number } variable "instance_type" { type = string } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } } provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } default_tags { tags = { Environment = var.environment ManagedBy = "Terraform Stacks" } } } } ``` ### components.tfcomponent.hcl ```hcl locals { name_prefix = "myapp-${var.environment}" } component "vpc" { source = "./modules/vpc" inputs = { name_prefix = local.name_prefix cidr_block = "10.0.0.0/16" } providers = { aws = provider.aws.this } } component "compute" { source = "./modules/compute" inputs = { name_prefix = local.name_prefix vpc_id = component.vpc.vpc_id subnet_ids = component.vpc.private_subnet_ids instance_count = var.instance_count instance_type = var.instance_type } providers = { aws = provider.aws.this } } ``` ### outputs.tfcomponent.hcl ```hcl output "vpc_id" { type = string value = component.vpc.vpc_id } output "load_balancer_url" { type = string value = component.compute.load_balancer_url } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" environments = { dev = { region = "us-east-1" instance_count = 1 instance_type = "t3.micro" } staging = { region = "us-west-1" instance_count = 2 instance_type = "t3.small" } prod = { region = "us-west-1" instance_count = 5 instance_type = "t3.large" } } } deployment "development" { inputs = { aws_region = local.environments.dev.region environment = "dev" instance_count = local.environments.dev.instance_count instance_type = local.environments.dev.instance_type role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "staging" { inputs = { aws_region = local.environments.staging.region environment = "staging" instance_count = local.environments.staging.instance_count instance_type = local.environments.staging.instance_type role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "production" { inputs = { aws_region = local.environments.prod.region environment = "prod" instance_count = local.environments.prod.instance_count instance_type = local.environments.prod.instance_type role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "development" { deployments = [deployment.development] } deployment_group "non_production" { deployments = [deployment.staging] } deployment_group "production" { deployments = [deployment.production] } # Auto-approve dev deployments deployment_auto_approve "dev_auto" { deployment_group = deployment_group.development check { condition = context.plan.applyable reason = "Development plans must be applyable" } } ``` ## Multi-Region Stack Stack that deploys identical infrastructure across multiple AWS regions. ### variables.tfcomponent.hcl ```hcl variable "regions" { type = set(string) } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } variable "app_name" { type = string } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } } provider "aws" "regional" { for_each = var.regions config { region = each.value assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } default_tags { tags = { Region = each.value ManagedBy = "Terraform Stacks" AppName = var.app_name } } } } ``` ### components.tfcomponent.hcl ```hcl component "regional_infrastructure" { for_each = var.regions source = "./modules/regional-infra" inputs = { region = each.value app_name = var.app_name name_suffix = each.value } providers = { aws = provider.aws.regional[each.value] } } component "global_route53" { source = "./modules/route53" inputs = { app_name = var.app_name domain_name = "example.com" regional_lbs = { for region, comp in component.regional_infrastructure : region => comp.load_balancer_dns } } # Use one region's provider for global resources providers = { aws = provider.aws.regional["us-west-1"] } } ``` ### outputs.tfcomponent.hcl ```hcl output "regional_endpoints" { type = map(string) value = { for region, comp in component.regional_infrastructure : region => comp.load_balancer_url } } output "global_domain" { type = string value = component.global_route53.domain_name } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { regions = ["us-west-1", "us-east-1", "eu-west-1"] } deployment "multi_region_prod" { inputs = { regions = toset(local.regions) app_name = "my-global-app" role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "production" { deployments = [deployment.multi_region_prod] } ``` ## Linked Stacks (Cross-Stack Dependencies) Two Stacks where the application Stack depends on the network Stack. ### Network Stack #### network-stack/variables.tfcomponent.hcl ```hcl variable "vpc_cidr" { type = string } variable "environment" { type = string } variable "aws_region" { type = string } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } ``` #### network-stack/providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } } provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` #### network-stack/components.tfcomponent.hcl ```hcl component "vpc" { source = "./modules/vpc" inputs = { cidr_block = var.vpc_cidr environment = var.environment } providers = { aws = provider.aws.this } } component "security_groups" { source = "./modules/security-groups" inputs = { vpc_id = component.vpc.vpc_id environment = var.environment } providers = { aws = provider.aws.this } } ``` #### network-stack/outputs.tfcomponent.hcl ```hcl output "vpc_id" { type = string value = component.vpc.vpc_id } output "private_subnet_ids" { type = list(string) value = component.vpc.private_subnet_ids } output "public_subnet_ids" { type = list(string) value = component.vpc.public_subnet_ids } output "app_security_group_id" { type = string value = component.security_groups.app_sg_id } ``` #### network-stack/deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" } deployment "network" { inputs = { aws_region = "us-west-1" environment = "production" vpc_cidr = "10.0.0.0/16" role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Publish outputs for other stacks publish_output "vpc_id_network" { type = string value = deployment.network.vpc_id } publish_output "private_subnet_ids" { type = list(string) value = deployment.network.private_subnet_ids } publish_output "public_subnet_ids" { type = list(string) value = deployment.network.public_subnet_ids } publish_output "app_security_group_id" { type = string value = deployment.network.app_security_group_id } # Deployment groups deployment_group "network" { deployments = [deployment.network] } ``` ### Application Stack #### application-stack/variables.tfcomponent.hcl ```hcl variable "vpc_id" { type = string } variable "subnet_ids" { type = list(string) } variable "security_group_id" { type = string } variable "instance_count" { type = number } variable "aws_region" { type = string } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } ``` #### application-stack/providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } } provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` #### application-stack/components.tfcomponent.hcl ```hcl component "application" { source = "./modules/app" inputs = { vpc_id = var.vpc_id subnet_ids = var.subnet_ids security_group_id = var.security_group_id instance_count = var.instance_count } providers = { aws = provider.aws.this } } ``` #### application-stack/deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } # Reference the network stack upstream_input "network" { type = "stack" source = "app.terraform.io/my-org/my-project/network-stack" } deployment "application" { inputs = { aws_region = "us-west-1" vpc_id = upstream_input.network.vpc_id_network subnet_ids = upstream_input.network.private_subnet_ids security_group_id = upstream_input.network.app_security_group_id instance_count = 3 role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "application" { deployments = [deployment.application] } ``` ## Multi-Cloud Stack Stack that deploys to both AWS and Azure. ### variables.tfcomponent.hcl ```hcl variable "aws_region" { type = string } variable "azure_location" { type = string } variable "aws_identity_token" { type = string ephemeral = true } variable "aws_role_arn" { type = string } variable "azure_identity_token" { type = string ephemeral = true } variable "azure_subscription_id" { type = string } variable "azure_tenant_id" { type = string } variable "azure_client_id" { type = string } variable "app_name" { type = string } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } azurerm = { source = "hashicorp/azurerm" version = "~> 3.0" } } provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.aws_role_arn web_identity_token = var.aws_identity_token } } } provider "azurerm" "this" { config { features {} subscription_id = var.azure_subscription_id tenant_id = var.azure_tenant_id client_id = var.azure_client_id use_oidc = true oidc_token = var.azure_identity_token } } ``` ### components.tfcomponent.hcl ```hcl component "aws_infrastructure" { source = "./modules/aws-infra" inputs = { region = var.aws_region app_name = var.app_name } providers = { aws = provider.aws.this } } component "azure_infrastructure" { source = "./modules/azure-infra" inputs = { location = var.azure_location app_name = var.app_name } providers = { azurerm = provider.azurerm.this } } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } identity_token "azure" { audience = ["api://AzureADTokenExchange"] } deployment "multi_cloud" { inputs = { aws_region = "us-west-1" azure_location = "westus2" app_name = "my-multi-cloud-app" aws_role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" aws_identity_token = identity_token.aws.jwt azure_subscription_id = "12345678-1234-1234-1234-123456789012" azure_tenant_id = "87654321-4321-4321-4321-210987654321" azure_client_id = "11111111-1111-1111-1111-111111111111" azure_identity_token = identity_token.azure.jwt } } # Deployment groups deployment_group "multi_cloud" { deployments = [deployment.multi_cloud] } ``` ## Complete AWS Production Stack Full production-grade Stack with VPC, RDS, ECS, and monitoring. ### variables.tfcomponent.hcl ```hcl variable "aws_region" { type = string description = "AWS region" } variable "environment" { type = string description = "Environment name" } variable "vpc_cidr" { type = string description = "VPC CIDR block" } variable "app_name" { type = string description = "Application name" } variable "db_instance_class" { type = string description = "RDS instance class" } variable "ecs_desired_count" { type = number description = "Desired ECS task count" } variable "identity_token" { type = string ephemeral = true } variable "role_arn" { type = string } ``` ### providers.tfcomponent.hcl ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" } random = { source = "hashicorp/random" version = "~> 3.5.0" } } provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } default_tags { tags = { Environment = var.environment Application = var.app_name ManagedBy = "Terraform Stacks" } } } } provider "random" "this" { config {} } ``` ### components.tfcomponent.hcl ```hcl locals { name_prefix = "${var.app_name}-${var.environment}" } component "vpc" { source = "./modules/vpc" inputs = { name_prefix = local.name_prefix cidr_block = var.vpc_cidr azs_count = 3 } providers = { aws = provider.aws.this } } component "security_groups" { source = "./modules/security-groups" inputs = { name_prefix = local.name_prefix vpc_id = component.vpc.vpc_id } providers = { aws = provider.aws.this } } component "rds" { source = "./modules/rds" inputs = { name_prefix = local.name_prefix instance_class = var.db_instance_class subnet_ids = component.vpc.private_subnet_ids security_group_ids = [component.security_groups.database_sg_id] } providers = { aws = provider.aws.this random = provider.random.this } } component "ecs_cluster" { source = "./modules/ecs-cluster" inputs = { name_prefix = local.name_prefix } providers = { aws = provider.aws.this } } component "ecs_service" { source = "./modules/ecs-service" inputs = { name_prefix = local.name_prefix cluster_id = component.ecs_cluster.cluster_id desired_count = var.ecs_desired_count subnet_ids = component.vpc.private_subnet_ids security_group_id = component.security_groups.app_sg_id database_endpoint = component.rds.endpoint } providers = { aws = provider.aws.this } } component "alb" { source = "./modules/alb" inputs = { name_prefix = local.name_prefix vpc_id = component.vpc.vpc_id subnet_ids = component.vpc.public_subnet_ids security_group_id = component.security_groups.alb_sg_id target_group_arn = component.ecs_service.target_group_arn } providers = { aws = provider.aws.this } } component "cloudwatch" { source = "./modules/cloudwatch" inputs = { name_prefix = local.name_prefix cluster_name = component.ecs_cluster.cluster_name service_name = component.ecs_service.service_name } providers = { aws = provider.aws.this } } ``` ### outputs.tfcomponent.hcl ```hcl output "load_balancer_url" { type = string description = "Application load balancer URL" value = component.alb.dns_name } output "database_endpoint" { type = string description = "RDS endpoint" value = component.rds.endpoint sensitive = true } output "vpc_id" { type = string value = component.vpc.vpc_id } output "ecs_cluster_name" { type = string value = component.ecs_cluster.cluster_name } ``` ### deployments.tfdeploy.hcl ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" } deployment "staging" { inputs = { aws_region = "us-west-1" environment = "staging" app_name = "myapp" vpc_cidr = "10.1.0.0/16" db_instance_class = "db.t3.small" ecs_desired_count = 2 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "production" { inputs = { aws_region = "us-west-1" environment = "production" app_name = "myapp" vpc_cidr = "10.0.0.0/16" db_instance_class = "db.r5.large" ecs_desired_count = 5 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "staging" { deployments = [deployment.staging] } deployment_group "production" { deployments = [deployment.production] } # Auto-approve staging with safety checks deployment_auto_approve "staging_safe" { deployment_group = deployment_group.staging check { condition = context.plan.changes.remove == 0 reason = "Cannot auto-approve deletions in staging" } check { condition = context.plan.applyable reason = "Plan must be applyable" } } ``` ## Testing Configurations ### Validate Stack Configuration ```bash terraform stacks providers lock terraform stacks validate ``` ### Plan Specific Deployment ```bash terraform stacks plan --deployment=development terraform stacks plan --deployment=production ``` ### Apply Deployment ```bash terraform stacks apply --deployment=staging ``` ## Destroying Deployments Example of safely removing a deployment from your Stack. ### Scenario You want to decommission the "development" deployment while keeping staging and production active. ### Step 1: Mark Deployment for Destruction Update your `deployments.tfdeploy.hcl` file to set `destroy = true`: ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" } # Mark this deployment for destruction deployment "development" { inputs = { aws_region = "us-east-1" environment = "dev" instance_count = 1 role_arn = local.role_arn identity_token = identity_token.aws.jwt } destroy = true # This tells HCP Terraform to destroy all resources } # Keep these deployments active deployment "staging" { inputs = { aws_region = "us-west-1" environment = "staging" instance_count = 2 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "production" { inputs = { aws_region = "us-west-1" environment = "prod" instance_count = 5 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "staging" { deployments = [deployment.staging] } deployment_group "production" { deployments = [deployment.production] } ``` ### Step 2: Plan and Apply ```bash # Review the destruction plan terraform stacks plan --deployment=development # Apply the destruction terraform stacks apply --deployment=development ``` HCP Terraform will destroy all resources in the development deployment. ### Step 3: Remove the Deployment Block After the deployment is successfully destroyed, remove the entire deployment block from your configuration: ```hcl identity_token "aws" { audience = ["aws.workload.identity"] } locals { role_arn = "arn:aws:iam::123456789012:role/terraform-stacks" } # deployment "development" block has been removed deployment "staging" { inputs = { aws_region = "us-west-1" environment = "staging" instance_count = 2 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } deployment "production" { inputs = { aws_region = "us-west-1" environment = "prod" instance_count = 5 role_arn = local.role_arn identity_token = identity_token.aws.jwt } } # Deployment groups deployment_group "staging" { deployments = [deployment.staging] } deployment_group "production" { deployments = [deployment.production] } ``` ### Important Notes - **Provider Authentication**: The `destroy` argument ensures your configuration retains the provider authentication needed to destroy resources - **Do Not Remove Immediately**: Don't remove the deployment block until after the destruction is complete - **Verify Before Removing**: Check HCP Terraform UI to confirm all resources are destroyed before removing the block - **Alternative**: You could manually destroy resources through HCP Terraform UI, but using `destroy = true` is the recommended approach for maintaining infrastructure-as-code practices ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/linked-stacks.md ================================================ # Linked Stacks Reference Complete reference for linking Terraform Stacks together using published outputs and upstream inputs. ## Publish Output Block Exports outputs from a Stack for consumption by other Stacks (linked Stacks). ###Syntax ```hcl publish_output "" { type = value = } ``` ### Arguments - **output_name** (label, required): Unique identifier for this published output - **type** (required): Data type of the output - **value** (required): Expression to export ### Accessing Deployment Outputs Reference deployment outputs using: `deployment..` ### Important Notes - Must apply the Stack's deployment configuration before downstream Stacks can reference outputs - Published outputs create a snapshot that other Stacks can read - Changes to published outputs automatically trigger runs in downstream Stacks ### Examples **Basic Published Output:** ```hcl publish_output "vpc_id" { type = string value = deployment.network.vpc_id } publish_output "subnet_ids" { type = list(string) value = deployment.network.private_subnet_ids } ``` **Multiple Deployment Outputs:** ```hcl publish_output "regional_vpc_ids" { type = map(string) value = { us_east = deployment.us_east.vpc_id us_west = deployment.us_west.vpc_id eu_west = deployment.eu_west.vpc_id } } ``` **Complex Output:** ```hcl publish_output "database_config" { type = object({ endpoint = string port = number name = string }) value = { endpoint = deployment.production.db_endpoint port = deployment.production.db_port name = deployment.production.db_name } } ``` **Regional Endpoints:** ```hcl publish_output "api_endpoints" { type = map(object({ url = string region = string })) value = { for env in ["dev", "staging", "prod"] : env => { url = deployment[env].api_url region = deployment[env].region } } } ``` ## Upstream Input Block References published outputs from another Stack (linked Stacks). ### Syntax ```hcl upstream_input "" { type = "stack" source = "" } ``` ### Arguments - **input_name** (label, required): Local name for this upstream input - **type** (required): Must be "stack" - **source** (required): Full Stack address in format: `app.terraform.io///` ### Accessing Upstream Outputs Reference upstream outputs using: `upstream_input..` ### Important Notes - Creates a dependency on the upstream Stack - Upstream Stack must have applied its deployment configuration - Changes in upstream Stack automatically trigger downstream Stack runs - Only works with Stacks in the same HCP Terraform project ### Examples **Basic Upstream Reference:** ```hcl upstream_input "network" { type = "stack" source = "app.terraform.io/my-org/my-project/networking-stack" } deployment "application" { inputs = { vpc_id = upstream_input.network.vpc_id subnet_ids = upstream_input.network.subnet_ids } } ``` **Multiple Upstream Stacks:** ```hcl upstream_input "network" { type = "stack" source = "app.terraform.io/my-org/my-project/network-stack" } upstream_input "database" { type = "stack" source = "app.terraform.io/my-org/my-project/database-stack" } deployment "application" { inputs = { vpc_id = upstream_input.network.vpc_id subnet_ids = upstream_input.network.private_subnet_ids database_endpoint = upstream_input.database.endpoint database_credentials = upstream_input.database.credentials } } ``` **Regional Upstream Dependencies:** ```hcl upstream_input "regional_network" { type = "stack" source = "app.terraform.io/my-org/my-project/regional-networks" } deployment "us_east_app" { inputs = { region = "us-east-1" vpc_id = upstream_input.regional_network.regional_vpc_ids["us_east"] subnet_ids = upstream_input.regional_network.regional_subnet_ids["us_east"] } } ``` ## Complete Working Example For a complete example showing full Stack configurations with all files (variables, providers, components, outputs, deployments) for both upstream and downstream Stacks, see the "Linked Stacks (Cross-Stack Dependencies)" section in `examples.md`. ================================================ FILE: terraform/module-generation/skills/terraform-stacks/references/troubleshooting.md ================================================ # Troubleshooting Reference Common issues and solutions when working with Terraform Stacks. ## Table of Contents 1. [Configuration Issues](#configuration-issues) 2. [Deployment Issues](#deployment-issues) 3. [Provider and Authentication Issues](#provider-and-authentication-issues) 4. [Module Compatibility Issues](#module-compatibility-issues) 5. [State and Dependency Issues](#state-and-dependency-issues) 6. [API and CLI Issues](#api-and-cli-issues) ## Configuration Issues ### Circular Dependencies **Issue:** Component A references Component B, and Component B references Component A. **Error Message:** ``` Error: Cycle detected in component dependencies ``` **Solutions:** 1. **Break the circular reference** by refactoring components: ```hcl # Before (circular dependency) component "vpc" { source = "./modules/vpc" inputs = { security_group_id = component.app.security_group_id # References app } } component "app" { source = "./modules/app" inputs = { vpc_id = component.vpc.vpc_id # References vpc } } # After (broken circular reference) component "vpc" { source = "./modules/vpc" inputs = { # Remove reference to app } } component "security_group" { source = "./modules/security-group" inputs = { vpc_id = component.vpc.vpc_id } } component "app" { source = "./modules/app" inputs = { vpc_id = component.vpc.vpc_id security_group_id = component.security_group.id } } ``` 2. **Use intermediate components** to break the dependency chain 3. **Refactor modules** to remove the circular dependency at the module level ### Validation Errors on Variables **Issue:** Variable block validation errors during `terraform stacks validate`. **Error Message:** ``` Error: Unsupported argument on variables.tfcomponent.hcl line 5: 5: validation { Validation blocks are not supported in Stack configurations ``` **Solution:** Remove `validation` blocks from variable declarations. Stacks do not support validation blocks: ```hcl # Incorrect variable "instance_count" { type = number validation { condition = var.instance_count > 0 error_message = "Instance count must be positive" } } # Correct variable "instance_count" { type = number description = "Number of instances (must be positive)" } ``` Move validation logic into the underlying modules if needed. ### Missing Type in Variable Declarations **Issue:** Variables fail validation when `type` is not specified. **Error Message:** ``` Error: Missing required argument on variables.tfcomponent.hcl line 3: 3: variable "region" { The argument "type" is required in Stack variable declarations ``` **Solution:** Always specify `type` for variables - it's required in Stacks (unlike traditional Terraform): ```hcl # Incorrect variable "region" { default = "us-west-1" } # Correct variable "region" { type = string default = "us-west-1" } ``` ### Provider Configuration in Modules **Issue:** Modules with embedded provider blocks cause errors. **Error Message:** ``` Error: Provider configuration not allowed in module Modules used with Terraform Stacks cannot contain provider blocks ``` **Solution:** 1. **Remove provider blocks from modules** - configure providers in Stack configuration instead 2. **Use modules that don't contain provider blocks** (most public registry modules are compatible) 3. **Fork and modify modules** if necessary to remove provider blocks ## Deployment Issues ### Cannot Destroy Deployment from UI **Issue:** The HCP Terraform UI doesn't provide an option to destroy Stack deployments. **Why:** Stack deployment destruction is only available through configuration, not the UI. **Solution:** Set `destroy = true` in the deployment block and upload the configuration: ```hcl deployment "old_environment" { inputs = { aws_region = "us-west-1" instance_count = 2 role_arn = local.role_arn identity_token = identity_token.aws.jwt } destroy = true # Marks deployment for destruction } ``` **Workflow:** 1. Add `destroy = true` to the deployment block 2. Run `terraform stacks configuration upload` 3. HCP Terraform creates a destroy run automatically 4. Approve the destroy run (if auto-approve is not configured) 5. After destruction completes, remove the deployment block entirely 6. Upload configuration again to clean up the deployment definition **Important:** You cannot destroy deployments from the UI. This is by design to prevent accidental destruction. ### Deployment Stuck in "Planning" State **Issue:** Deployment remains in "planning" state indefinitely. **Possible Causes:** 1. **Provider authentication failed** - Check OIDC configuration and IAM roles 2. **Module download failed** - Verify module sources are accessible 3. **Provider version conflict** - Check `.terraform.lock.hcl` matches required providers **Diagnosis:** ```bash # Get deployment step diagnostics terraform stacks deployment-run list # Note the run ID, then: curl -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps" | \ jq '.data[] | {id, status: .attributes.status, component: .attributes["component-name"]}' ``` **Solutions:** 1. Check diagnostics for the stuck step 2. Verify provider authentication is configured correctly 3. Ensure all module sources are accessible 4. Check provider lock file matches required providers ### Deployment Requires Approval But No Approval Prompt **Issue:** Deployment is waiting for approval but CLI doesn't show approval prompt. **Why:** CLI monitoring commands are non-blocking and don't automatically prompt for approval. **Solution:** **Option 1: Approve via CLI** ```bash # Approve all pending plans in a deployment run terraform stacks deployment-run approve-all-plans -deployment-run-id=sdr-ABC123 # Or approve all plans in a deployment group terraform stacks deployment-group approve-all-plans -deployment-group=canary ``` **Option 2: Configure auto-approve** (Premium feature) ```hcl deployment_auto_approve "safe_changes" { deployment_group = deployment_group.canary check { condition = context.plan.applyable reason = "Plan must be successful" } } ``` ## Provider and Authentication Issues ### OIDC Authentication Failing **Issue:** Provider authentication fails with OIDC/workload identity. **Error Messages:** ``` Error: Error assuming role with web identity Error: Failed to retrieve credentials Error: Invalid identity token ``` **Diagnosis Steps:** 1. **Verify identity token configuration:** ```hcl # Check identity_token block exists identity_token "aws" { audience = ["aws.workload.identity"] } # Check deployment references the token deployment "production" { inputs = { identity_token = identity_token.aws.jwt } } ``` 2. **Verify provider configuration:** ```hcl provider "aws" "this" { config { region = var.aws_region assume_role_with_web_identity { role_arn = var.role_arn web_identity_token = var.identity_token } } } ``` 3. **Check IAM role trust policy:** **AWS - Verify trust policy includes HCP Terraform:** ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam:::oidc-provider/app.terraform.io" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "app.terraform.io:aud": "aws.workload.identity" }, "StringLike": { "app.terraform.io:sub": "organization::project::stack::deployment:" } } } ] } ``` **Azure - Verify federated credential:** - Application ID matches the one in provider configuration - Subject matches: `organization::project::stack::deployment:` - Issuer is `https://app.terraform.io` **GCP - Verify workload identity pool:** - Provider configuration includes correct workload identity provider - Service account has necessary IAM permissions - Attribute mapping includes `google.subject` from token claims **Solutions:** 1. Fix IAM role trust policy to include correct HCP Terraform OIDC provider 2. Ensure audience matches between identity_token block and IAM trust policy 3. Verify subject pattern matches your organization/project/stack/deployment names 4. Check that the role_arn is correct in provider configuration ### Provider Version Lock File Issues **Issue:** Provider version conflicts or "could not retrieve provider" errors. **Error Messages:** ``` Error: Failed to install provider Error: Provider version not found Error: Checksum mismatch for provider ``` **Solutions:** 1. **Regenerate provider lock file:** ```bash terraform stacks providers-lock ``` 2. **Add additional platforms** (if deploying from different OS): ```bash terraform stacks providers-lock \ -platform=linux_amd64 \ -platform=darwin_amd64 \ -platform=darwin_arm64 ``` 3. **Verify required_providers block:** ```hcl required_providers { aws = { source = "hashicorp/aws" version = "~> 5.7.0" # Ensure version constraint is valid } } ``` 4. **Commit `.terraform.lock.hcl`** to version control ## Module Compatibility Issues ### Public Registry Module Errors **Issue:** Modules from the Terraform public registry cause errors during plan or apply. **Common Errors:** ``` Error: Unsupported attribute Error: Invalid reference Error: Missing required argument ``` **Known Problematic Modules:** - `terraform-aws-modules/alb/aws` - Some versions have compatibility issues - `terraform-aws-modules/ecs-service/aws` - May have issues with certain configurations **Solutions:** 1. **Test modules in dev deployment first** before using in production 2. **Check module compatibility** by reviewing recent issues on the module repository 3. **Use specific module versions** rather than latest: ```hcl component "alb" { source = "terraform-aws-modules/alb/aws" version = "8.7.0" # Use specific version known to work # ... } ``` 4. **Consider using raw resources** for critical infrastructure: ```hcl # Instead of using a module that has issues component "alb" { source = "./modules/alb" # Create local module with raw resources # ... } ``` 5. **Fork and fix modules** if you have the resources to maintain them 6. **Report compatibility issues** to module maintainers ### Local Module Not Found **Issue:** Stack can't find local module sources. **Error Message:** ``` Error: Module not found Could not load module ./modules/vpc ``` **Solutions:** 1. **Verify module path is relative** to Stack root: ```hcl # Correct component "vpc" { source = "./modules/vpc" } # Incorrect (absolute paths don't work) component "vpc" { source = "/Users/username/project/modules/vpc" } ``` 2. **Ensure module directory exists** with proper structure: ``` my-stack/ ├── components.tfcomponent.hcl └── modules/ └── vpc/ ├── main.tf ├── variables.tf └── outputs.tf ``` 3. **Check file permissions** on module directories ## State and Dependency Issues ### Component Output Not Available **Issue:** Component output is not available to referencing component. **Error Message:** ``` Error: Reference to unknown component Component "vpc" has not been defined ``` **Solutions:** 1. **Verify component exists** in configuration: ```hcl component "vpc" { source = "./modules/vpc" # Must define component before referencing it } component "app" { source = "./modules/app" inputs = { vpc_id = component.vpc.vpc_id # Now valid } } ``` 2. **Check output is defined in module:** ```hcl # In modules/vpc/outputs.tf output "vpc_id" { value = aws_vpc.main.id } ``` 3. **For components with for_each**, reference specific instance: ```hcl component "regional" { for_each = var.regions # ... } component "app" { inputs = { # Correct - reference specific instance vpc_id = component.regional["us-west-1"].vpc_id # Incorrect - can't reference for_each component directly # vpc_id = component.regional.vpc_id } } ``` ### Deferred Changes Not Converging **Issue:** Deployment with deferred changes doesn't complete after multiple iterations. **Error Message:** ``` Error: Maximum deferred change iterations reached ``` **Cause:** Dependency cycle or values that never stabilize. **Solutions:** 1. **Review component dependencies** for logical cycles 2. **Check for computed values that change on every run** 3. **Refactor to break dependency chain** 4. **Consider multi-stage deployments** if resources truly can't be created together ## API and CLI Issues ### Empty Diagnostics Response **Issue:** API request for diagnostics returns empty results. **Request:** ```bash curl "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics" ``` **Response:** ```json { "data": [] } ``` **Solution:** Add required `stack_deployment_step_id` query parameter: ```bash curl "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" ``` ### Cannot Retrieve Stack Outputs **Issue:** No CLI command to retrieve Stack outputs after deployment. **Why:** Currently no direct CLI command for outputs retrieval. **Solution:** Use the artifacts API endpoint: ```bash # Get final apply step ID first APPLY_STEP=$(terraform stacks deployment-run list --json | \ jq -r '.[0].deployment_steps[] | select(.operation_type == "apply") | .id' | tail -1) # Get outputs curl -L -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/$APPLY_STEP/artifacts?name=apply-description" | \ jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"' ``` ### CLI Watch Commands Hang in CI/CD **Issue:** Commands like `terraform stacks deployment-run watch` never return in CI/CD pipelines. **Why:** Watch commands stream output indefinitely and are designed for interactive use. **Solution:** Use API polling instead of watch commands. See `api-monitoring.md` for complete workflow. ### Artifacts Endpoint Returns 404 **Issue:** Request to artifacts endpoint returns 404 Not Found. **Possible Causes:** 1. **Step hasn't completed yet** - wait for step status to be "completed" 2. **Wrong artifact name** - use one of: plan-description, plan-debug-log, apply-description, apply-debug-log 3. **Invalid step ID** - verify step ID from deployment-steps endpoint **Solution:** ```bash # Check step status first curl -s -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}" | \ jq '.data.attributes.status' # Only request artifacts when status is "completed" if [ "$STATUS" = "completed" ]; then curl -L -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description" fi ``` ### HTTP 307 Redirect Not Followed **Issue:** Artifacts endpoint returns redirect response instead of artifact content. **Why:** The endpoint returns HTTP 307 redirect to the actual artifact URL. **Solution:** Configure HTTP client to follow redirects: ```bash # curl: Use -L flag curl -L -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description" # Python requests: allow_redirects=True (default) import requests response = requests.get(url, headers=headers, allow_redirects=True) # Node.js fetch: redirect: 'follow' (default) const response = await fetch(url, { headers: headers, redirect: 'follow' }); ``` ## Getting Additional Help ### Enable Debug Logging For more detailed error information, enable debug logging: ```bash # CLI commands TF_LOG=DEBUG terraform stacks validate TF_LOG=DEBUG terraform stacks configuration upload # API artifacts # Request the debug-log artifact instead of description curl -L -H "Authorization: Bearer $TOKEN" \ "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-debug-log" ``` ### Check HCP Terraform Status If experiencing widespread issues, check HCP Terraform status page: - https://status.hashicorp.com ### Review Configuration Version List recent configurations to identify when issues started: ```bash terraform stacks configuration list ``` ### Contact Support For issues not covered here: 1. Gather relevant error messages and diagnostics 2. Note the configuration sequence number 3. Include deployment run IDs 4. Contact HashiCorp Support with details ================================================ FILE: terraform/provider-development/.claude-plugin/plugin.json ================================================ { "name": "terraform-provider-development", "version": "1.0.0", "description": "Terraform provider development skills for Claude Code, including resources, data sources, actions, and acceptance testing.", "author": { "name": "HashiCorp", "url": "https://github.com/hashicorp" }, "homepage": "https://developer.hashicorp.com/terraform/plugin/framework", "repository": "https://github.com/hashicorp/agent-skills", "license": "MPL-2.0", "keywords": ["terraform", "provider", "plugin-framework", "resources", "testing"] } ================================================ FILE: terraform/provider-development/skills/new-terraform-provider/SKILL.md ================================================ --- name: new-terraform-provider description: Use this when scaffolding a new Terraform provider. license: MPL-2.0 metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- To scaffold a new Terraform provider with Plugin Framework: 1. If I am already in a Terraform provider workspace, then confirm that I want to create a new workspace. If I do not want to create a new workspace, then skip all remaining steps. 1. Create a new workspace root directory. The root directory name should be prefixed with "terraform-provider-". Perform all subsequent steps in this new workspace. 1. Initialize a new Go module.. 1. Run `go get -u github.com/hashicorp/terraform-plugin-framework@latest`. 1. Write a main.go file that follows [the example](assets/main.go). 1. Remove TODO comments from `main.go` 1. Run `go mod tidy` 1. Run `go build -o /dev/null` 1. Run `go test ./...` ================================================ FILE: terraform/provider-development/skills/new-terraform-provider/assets/main.go ================================================ // Copyright IBM Corp. 2025, 2026 // SPDX-License-Identifier: MPL-2.0 package main import ( "context" "flag" "log" "example.org/terraform-provider-demo/internal/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" ) var ( // these will be set by the goreleaser configuration // to appropriate values for the compiled binary. version string = "dev" // goreleaser can pass other information to the main package, such as the specific commit // https://goreleaser.com/cookbooks/using-main.version/ ) func main() { var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() opts := providerserver.ServeOpts{ // TODO: Update this string with the published name of your provider. // Also update the tfplugindocs generate command to either remove the // -provider-name flag or set its value to the updated provider name. Address: "registry.terraform.io/example/demo", Debug: debug, } err := providerserver.Serve(context.Background(), provider.New(version), opts) if err != nil { log.Fatal(err.Error()) } } ================================================ FILE: terraform/provider-development/skills/provider-actions/SKILL.md ================================================ --- name: provider-actions description: Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after create, update, destroy). metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- # Terraform Provider Actions Implementation Guide ## Overview Terraform Actions enable imperative operations during the Terraform lifecycle. Actions are experimental features that allow performing provider operations at specific lifecycle events (before/after create, update, destroy). **References:** - [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) - [Terraform Actions RFC](https://github.com/hashicorp/terraform/blob/main/docs/plugin-protocol/actions.md) ## File Structure Actions follow the standard service package structure: ``` internal/service// ├── _action.go # Action implementation ├── _action_test.go # Action tests └── service_package_gen.go # Auto-generated service registration ``` Documentation structure: ``` website/docs/actions/ └── _.html.markdown # User-facing documentation ``` Changelog entry: ``` .changelog/ └── .txt # Release note entry ``` ## Action Schema Definition Actions use the Terraform Plugin Framework with a standard schema pattern: ```go func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ // Required configuration parameters "resource_id": schema.StringAttribute{ Required: true, Description: "ID of the resource to operate on", }, // Optional parameters with defaults "timeout": schema.Int64Attribute{ Optional: true, Description: "Operation timeout in seconds", Default: int64default.StaticInt64(1800), Computed: true, }, }, } } ``` ### Common Schema Issues **Pay special attention to the schema definition** - common issues after a first draft: 1. **Type Mismatches** - Using `types.String` instead of `fwtypes.String` in model structs - Using `types.StringType` instead of `fwtypes.StringType` in schema - Mixing framework types with plugin-framework types 2. **List/Map Element Types** ```go // WRONG - missing ElementType "items": schema.ListAttribute{ Optional: true, } // CORRECT "items": schema.ListAttribute{ Optional: true, ElementType: fwtypes.StringType, } ``` 3. **Computed vs Optional** - Attributes with defaults must be both `Optional: true` and `Computed: true` - Don't mark action inputs as `Computed` unless they have defaults 4. **Validator Imports** ```go // Ensure proper imports "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" ``` 5. **Region/Provider Attribute** - Use framework-provided region handling when available - Don't manually define provider-specific config in schema if framework handles it 6. **Nested Attributes** - Use appropriate nested object types for complex structures - Ensure nested types are properly defined ### Schema Validation Checklist Before submitting, verify: - [ ] All attributes have descriptions - [ ] List/Map attributes have ElementType defined - [ ] Validators are imported and applied correctly - [ ] Model struct uses correct framework types - [ ] Optional attributes with defaults are marked Computed - [ ] Code compiles without type errors - [ ] Run `go build` to catch type mismatches ## Action Invoke Method The Invoke method contains the action logic: ```go func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { var data actionModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) // Create provider client conn := a.Meta().Client(ctx) // Progress updates for long-running operations resp.Progress.Set(ctx, "Starting operation...") // Implement action logic with error handling // Use context for timeout management // Poll for completion if async operation resp.Progress.Set(ctx, "Operation completed") } ``` ## Key Implementation Requirements ### 1. Progress Reporting - Use `resp.SendProgress(action.InvokeProgressEvent{...})` for real-time updates - Provide meaningful progress messages during long operations - Update progress at key milestones - Include elapsed time for long operations ### 2. Timeout Management - Always include configurable timeout parameter (default: 1800s) - Use `context.WithTimeout()` for API calls - Handle timeout errors gracefully - Validate timeout ranges (typically 60-7200 seconds) ### 3. Error Handling - Add diagnostics with `resp.Diagnostics.AddError()` - Provide clear error messages with context - Include API error details when relevant - Map provider error types to user-friendly messages - Document all possible error cases Example error handling: ```go // Handle specific errors var notFound *types.ResourceNotFoundException if errors.As(err, ¬Found) { resp.Diagnostics.AddError( "Resource Not Found", fmt.Sprintf("Resource %s was not found", resourceID), ) return } // Generic error handling resp.Diagnostics.AddError( "Operation Failed", fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err), ) ``` ### 4. Provider SDK Integration - Use provider SDK clients from `a.Meta().Client(ctx)` - Handle pagination for list operations - Implement retry logic for transient failures - Use appropriate error types ### 5. Parameter Validation - Use framework validators for input validation - Validate resource existence before operations - Check for conflicting parameters - Validate against provider naming requirements ### 6. Polling and Waiting For operations that require waiting for completion: ```go result, err := wait.WaitForStatus(ctx, func(ctx context.Context) (wait.FetchResult[*ResourceType], error) { // Fetch current status resource, err := findResource(ctx, conn, id) if err != nil { return wait.FetchResult[*ResourceType]{}, err } return wait.FetchResult[*ResourceType]{ Status: wait.Status(resource.Status), Value: resource, }, nil }, wait.Options[*ResourceType]{ Timeout: timeout, Interval: wait.FixedInterval(5 * time.Second), SuccessStates: []wait.Status{"AVAILABLE", "COMPLETED"}, TransitionalStates: []wait.Status{"CREATING", "PENDING"}, ProgressInterval: 30 * time.Second, ProgressSink: func(fr wait.FetchResult[any], meta wait.ProgressMeta) { resp.SendProgress(action.InvokeProgressEvent{ Message: fmt.Sprintf("Status: %s, Elapsed: %v", fr.Status, meta.Elapsed.Round(time.Second)), }) }, }, ) ``` ## Common Action Patterns ### Batch Operations - Process items in configurable batches - Report progress per batch - Handle partial failures gracefully - Support prefix/filter parameters ### Command Execution - Submit command and get operation ID - Poll for completion status - Retrieve and report output - Handle timeout during polling - Validate resources exist before execution ### Service Invocation - Invoke service with parameters - Wait for completion (if synchronous) - Return output/results - Handle service-specific errors ### Resource State Changes - Validate current state - Apply state change - Poll for target state - Handle transitional states ### Async Job Submission - Submit job with configuration - Get job ID - Optionally wait for completion - Report job status ## Action Triggers Actions are invoked via `action_trigger` lifecycle blocks in Terraform configurations: ```hcl action "provider_service_action" "name" { config { parameter = value } } resource "terraform_data" "trigger" { lifecycle { action_trigger { events = [after_create] actions = [action.provider_service_action.name] } } } ``` ### Available Trigger Events **Terraform 1.14.0 Supported Events:** - `before_create` - Before resource creation - `after_create` - After resource creation - `before_update` - Before resource update - `after_update` - After resource update **Not Supported in Terraform 1.14.0:** - `before_destroy` - Not available (will cause validation error) - `after_destroy` - Not available (will cause validation error) ## Testing Actions ### Acceptance Tests - Test action invocation with valid parameters - Test timeout scenarios - Test error conditions - Verify provider state changes - Test progress reporting - Test with custom parameters - Test trigger-based invocation ### Test Pattern ```go func TestAccServiceAction_basic(t *testing.T) { ctx := acctest.Context(t) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, Steps: []resource.TestStep{ { Config: testAccActionConfig_basic(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceExists(ctx, "provider_resource.test"), ), }, }, }) } ``` ### Test Cleanup with Sweep Functions Add sweep functions to clean up test resources: ```go func sweepResources(region string) error { ctx := context.Background() client := /* get client for region */ input := &service.ListInput{ // Filter for test resources } var sweeperErrs *multierror.Error pages := service.NewListPaginator(client, input) for pages.HasMorePages() { page, err := pages.NextPage(ctx) if err != nil { sweeperErrs = multierror.Append(sweeperErrs, err) continue } for _, item := range page.Items { id := item.Id // Skip non-test resources if !strings.HasPrefix(id, "tf-acc-test") { continue } _, err := client.Delete(ctx, &service.DeleteInput{ Id: id, }) if err != nil { sweeperErrs = multierror.Append(sweeperErrs, err) } } } return sweeperErrs.ErrorOrNil() } ``` ### Testing Best Practices **Service-Specific Prerequisites** - Always check for service-specific prerequisites that must be met before actions can succeed - Document prerequisites in action documentation and test configurations **Error Pattern Matching** - Terraform wraps action errors with additional context - Use flexible regex patterns: `regexache.MustCompile(\`(?s)Error Title.*key phrase\`)` **Test Patterns Not Applicable to Actions** 1. Actions trigger on lifecycle events, not config reapplication 2. Before/After Destroy Tests: Not supported in Terraform 1.14.0 ### Running Tests Compile test to check for errors: ```bash go test -c -o /dev/null ./internal/service/ ``` Run specific action tests: ```bash TF_ACC=1 go test ./internal/service/ -run TestAccServiceAction_ -v ``` Run sweep to clean up test resources: ```bash TF_ACC=1 go test ./internal/service/ -sweep= -v ``` ## Documentation Standards Each action documentation file must include: 1. **Front Matter** ```yaml --- subcategory: "Service Name" layout: "provider" page_title: "Provider: provider_service_action" description: |- Brief description of what the action does. --- ``` 2. **Header with Warnings** - Beta/Alpha notice about experimental status - Warning about potential unintended consequences - Link to provider documentation 3. **Example Usage** - Basic usage example - Advanced usage with all options - Trigger-based example with `terraform_data` - Real-world use case examples 4. **Argument Reference** - List all required and optional arguments - Include descriptions and defaults - Note any validation rules 5. **Documentation Linting** - Run `terrafmt fmt` before submission - Verify with `terrafmt diff` ## Changelog Entry Format Create a changelog entry in `.changelog/` directory: ``` .changelog/.txt ``` Content format: ```release-note:new-action action/provider_service_action: Brief description of the action ``` ## Pre-Submission Checklist Before submitting your action implementation: - [ ] Code compiles: `go build -o /dev/null .` - [ ] Tests compile: `go test -c -o /dev/null ./internal/service/` - [ ] Code formatted: `make fmt` - [ ] Documentation formatted: `terrafmt fmt website/docs/actions/.html.markdown` - [ ] Changelog entry created - [ ] Schema uses correct types - [ ] All List/Map attributes have ElementType - [ ] Progress updates implemented for long operations - [ ] Error messages include context and resource identifiers - [ ] Documentation includes multiple examples - [ ] Documentation includes prerequisites and warnings ## References - [Terraform Plugin Framework Documentation](https://developer.hashicorp.com/terraform/plugin/framework) - [Terraform Provider Development](https://developer.hashicorp.com/terraform/plugin) - [terraform-plugin-framework GitHub](https://github.com/hashicorp/terraform-plugin-framework) - [terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing) ================================================ FILE: terraform/provider-development/skills/provider-docs/SKILL.md ================================================ --- name: provider-docs description: Create, update, and review Terraform provider documentation for Terraform Registry using HashiCorp-recommended patterns, tfplugindocs templates, and schema descriptions. Use when adding or changing provider configuration, resources, data sources, ephemeral resources, list resources, functions, or guides; when validating generated docs; and when troubleshooting missing or incorrect Registry documentation. --- # Terraform Provider Docs ## Follow This Workflow 1. Confirm scope and documentation targets. - Map code changes to the exact doc targets: provider index, resources, data sources, ephemeral resources, list resources, functions, or guides. - Decide whether content should come from schema descriptions, templates, or both. 2. Write schema descriptions first. - Add precise user-facing descriptions to schema fields so generated docs stay aligned with behavior. - Keep wording specific to argument purpose, constraints, defaults, and computed behavior. 3. Add or update template files in `docs/`. - Create only files that map to implemented provider objects. - Use HashiCorp-recommended template paths: - `docs/index.md.tmpl` - `docs/data-sources/.md.tmpl` - `docs/resources/.md.tmpl` - `docs/ephemeral-resources/.md.tmpl` - `docs/list-resources/.md.tmpl` - `docs/functions/.md.tmpl` - `docs/guides/.md.tmpl` - Keep templates focused on overview and examples; rely on generated sections for field-by-field details. 4. Generate documentation with `tfplugindocs`. - Prefer repository defaults when configured: ```bash go generate ./... ``` - Otherwise run the generator directly: ```bash go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-name ``` - Re-run generation after every schema or template edit. 5. Validate the generated markdown. - Verify files in `docs/` match the current provider implementation. - Verify examples are valid HCL and reflect current argument/attribute names. - Verify required/optional/computed semantics in docs match schema behavior. 6. Apply Registry publication rules before release. - Use semantic version tags prefixed with `v` (for example `v1.2.3`). - Create release tags from the default branch. - Keep `terraform-registry-manifest.json` in the repository root. - Expect docs to be versioned in Registry and switchable with the version selector. 7. Preview or troubleshoot publication when needed. - Use the HashiCorp preview process to inspect rendered docs before release when accuracy risk is high. - If docs are missing in Registry, check tag format, tag source branch, manifest file presence, and provider publication status. ## Enforce Quality Bar - Keep documentation behaviorally accurate; never describe unsupported arguments or attributes. - Keep examples minimal, realistic, and runnable. - Keep terminology and naming consistent across provider, resources, and data sources. - Avoid duplicating generated argument/attribute blocks in manual templates. - Keep doc changes tied to the same PR as schema/API changes whenever possible. ## Load References On Demand - Read `references/hashicorp-provider-docs.md` for source-backed rules and official links. - Load only the sections needed for the current change to keep context lean. ================================================ FILE: terraform/provider-development/skills/provider-docs/agents/openai.yaml ================================================ # Copyright IBM Corp. 2025, 2026 # SPDX-License-Identifier: MPL-2.0 interface: display_name: "Terraform Provider Docs" short_description: "Best practices for Terraform provider docs" default_prompt: "Use $terraform-provider-docs to create or update Terraform Registry provider documentation with HashiCorp-aligned structure and style." ================================================ FILE: terraform/provider-development/skills/provider-docs/references/hashicorp-provider-docs.md ================================================ # HashiCorp Provider Documentation Reference Source of truth for this skill: - https://developer.hashicorp.com/terraform/registry/providers/docs ## Core Rules - Publish provider docs through Terraform Registry using `tfplugindocs`. - Generate provider docs from schema descriptions and markdown templates. - Store templates under the repository `docs/` directory with expected naming conventions. - Keep release tags and manifest metadata valid so Registry can render and display docs. ## Template Paths Use these template paths when the corresponding provider objects exist: - `docs/index.md.tmpl` - `docs/data-sources/.md.tmpl` - `docs/resources/.md.tmpl` - `docs/ephemeral-resources/.md.tmpl` - `docs/list-resources/.md.tmpl` - `docs/functions/.md.tmpl` - `docs/guides/.md.tmpl` ## Generation Workflow HashiCorp recommends wiring generator execution through `go generate`: ```go //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-name ``` Run from repository root: ```bash go generate ./... ``` Alternative direct execution: ```bash go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-name ``` ## Release and Publication Constraints - Use semantic version tags prefixed with `v`. - Create tags from the default branch. - Keep `terraform-registry-manifest.json` in the repository root. - Understand docs appear by provider version in Registry once the provider release is published. ## Preview and Troubleshooting - Use HashiCorp's preview process to verify rendering before release when needed. - If docs are missing or stale in Registry, verify: - tag naming and tag branch source - manifest file presence and validity - provider version publication state ## Related Canonical Pages - Provider docs guidance: - https://developer.hashicorp.com/terraform/registry/providers/docs - Terraform Plugin Docs (`tfplugindocs`) source and usage: - https://github.com/hashicorp/terraform-plugin-docs ================================================ FILE: terraform/provider-development/skills/provider-resources/SKILL.md ================================================ --- name: provider-resources description: Implement Terraform Provider resources and data sources using the Plugin Framework. Use when developing CRUD operations, schema design, state management, and acceptance testing for provider resources. metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- # Terraform Provider Resources Implementation Guide ## Overview This guide covers developing Terraform Provider resources and data sources using the Terraform Plugin Framework. Resources represent infrastructure objects that Terraform manages through Create, Read, Update, and Delete (CRUD) operations. **References:** - [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) - [Resource Development](https://developer.hashicorp.com/terraform/plugin/framework/resources) - [Data Source Development](https://developer.hashicorp.com/terraform/plugin/framework/data-sources) ## File Structure Resources follow the standard service package structure: ``` internal/service// ├── .go # Resource implementation ├── _test.go # Acceptance tests ├── _data_source.go # Data source (if applicable) ├── find.go # Finder functions ├── exports_test.go # Test exports └── service_package_gen.go # Auto-generated registration ``` Documentation structure: ``` website/docs/r/ └── _.html.markdown # Resource documentation website/docs/d/ └── _.html.markdown # Data source documentation ``` ## Resource Structure ### SDKv2 Resource Pattern ```go func ResourceExample() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceExampleCreate, ReadWithoutTimeout: resourceExampleRead, UpdateWithoutTimeout: resourceExampleUpdate, DeleteWithoutTimeout: resourceExampleDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: validation.StringLenBetween(1, 255), }, "arn": { Type: schema.TypeString, Computed: true, }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), }, CustomizeDiff: verify.SetTagsDiff, } } ``` ### Plugin Framework Resource Pattern ```go type resourceExample struct { framework.ResourceWithConfigure } func (r *resourceExample) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_example" } func (r *resourceExample) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "id": framework.IDAttribute(), "name": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, Validators: []validator.String{ stringvalidator.LengthBetween(1, 255), }, }, "arn": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, }, } } ``` ## CRUD Operations ### Create Operation ```go func (r *resourceExample) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data resourceExampleModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } conn := r.Meta().ExampleClient(ctx) input := &example.CreateExampleInput{ Name: data.Name.ValueStringPointer(), } output, err := conn.CreateExample(ctx, input) if err != nil { resp.Diagnostics.AddError( "Error creating Example", fmt.Sprintf("Could not create example %s: %s", data.Name.ValueString(), err), ) return } data.ID = types.StringPointerValue(output.Id) data.ARN = types.StringPointerValue(output.Arn) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } ``` ### Read Operation ```go func (r *resourceExample) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data resourceExampleModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } conn := r.Meta().ExampleClient(ctx) output, err := findExampleByID(ctx, conn, data.ID.ValueString()) if tfresource.NotFound(err) { resp.Diagnostics.AddWarning( "Resource not found", fmt.Sprintf("Example %s not found, removing from state", data.ID.ValueString()), ) resp.State.RemoveResource(ctx) return } if err != nil { resp.Diagnostics.AddError( "Error reading Example", fmt.Sprintf("Could not read example %s: %s", data.ID.ValueString(), err), ) return } data.Name = types.StringPointerValue(output.Name) data.ARN = types.StringPointerValue(output.Arn) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } ``` ### Update Operation ```go func (r *resourceExample) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan, state resourceExampleModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } conn := r.Meta().ExampleClient(ctx) if !plan.Description.Equal(state.Description) { input := &example.UpdateExampleInput{ Id: plan.ID.ValueStringPointer(), Description: plan.Description.ValueStringPointer(), } _, err := conn.UpdateExample(ctx, input) if err != nil { resp.Diagnostics.AddError( "Error updating Example", fmt.Sprintf("Could not update example %s: %s", plan.ID.ValueString(), err), ) return } } resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } ``` ### Delete Operation ```go func (r *resourceExample) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data resourceExampleModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } conn := r.Meta().ExampleClient(ctx) _, err := conn.DeleteExample(ctx, &example.DeleteExampleInput{ Id: data.ID.ValueStringPointer(), }) if tfresource.NotFound(err) { return } if err != nil { resp.Diagnostics.AddError( "Error deleting Example", fmt.Sprintf("Could not delete example %s: %s", data.ID.ValueString(), err), ) return } } ``` ## Schema Design ### Attribute Types | Terraform Type | Framework Type | Use Case | |----------------|----------------|----------| | `string` | `schema.StringAttribute` | Names, ARNs, IDs | | `number` | `schema.Int64Attribute`, `schema.Float64Attribute` | Counts, sizes | | `bool` | `schema.BoolAttribute` | Feature flags | | `list` | `schema.ListAttribute` | Ordered collections | | `set` | `schema.SetAttribute` | Unordered unique items | | `map` | `schema.MapAttribute` | Key-value pairs | | `object` | `schema.SingleNestedAttribute` | Complex nested config | ### Plan Modifiers ```go // Force replacement when value changes stringplanmodifier.RequiresReplace() // Preserve unknown value during plan stringplanmodifier.UseStateForUnknown() // Custom plan modifier stringplanmodifier.RequiresReplaceIf( func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { // Custom logic }, "description", "markdown description", ) ``` ### Validators ```go // String validators stringvalidator.LengthBetween(1, 255) stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-]+$`), "must be lowercase alphanumeric with hyphens") stringvalidator.OneOf("option1", "option2", "option3") // Int64 validators int64validator.Between(1, 100) int64validator.AtLeast(1) int64validator.AtMost(1000) // List validators listvalidator.SizeAtLeast(1) listvalidator.SizeAtMost(10) ``` ### Sensitive Attributes ```go "password": schema.StringAttribute{ Required: true, Sensitive: true, Validators: []validator.String{ stringvalidator.LengthAtLeast(8), }, } ``` ## State Management ### Handling Resource Not Found ```go func findExampleByID(ctx context.Context, conn *example.Client, id string) (*example.Example, error) { input := &example.GetExampleInput{ Id: &id, } output, err := conn.GetExample(ctx, input) if err != nil { var notFound *types.ResourceNotFoundException if errors.As(err, ¬Found) { return nil, &retry.NotFoundError{ LastError: err, LastRequest: input, } } return nil, err } if output == nil || output.Example == nil { return nil, tfresource.NewEmptyResultError(input) } return output.Example, nil } ``` ### Waiting for Resource States ```go func waitExampleCreated(ctx context.Context, conn *example.Client, id string, timeout time.Duration) (*example.Example, error) { stateConf := &retry.StateChangeConf{ Pending: []string{"CREATING", "PENDING"}, Target: []string{"ACTIVE", "AVAILABLE"}, Refresh: statusExample(ctx, conn, id), Timeout: timeout, } outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*example.Example); ok { return output, err } return nil, err } func statusExample(ctx context.Context, conn *example.Client, id string) retry.StateRefreshFunc { return func() (interface{}, string, error) { output, err := findExampleByID(ctx, conn, id) if tfresource.NotFound(err) { return nil, "", nil } if err != nil { return nil, "", err } return output, string(output.Status), nil } } ``` ## Testing ### Basic Acceptance Test ```go func TestAccExampleResource_basic(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "provider_example.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckExampleDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccExampleConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckExampleExists(ctx, resourceName), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttrSet(resourceName, "arn"), ), }, { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, }, }) } ``` ### Disappears Test ```go func TestAccExampleResource_disappears(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "provider_example.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckExampleDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccExampleConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckExampleExists(ctx, resourceName), acctest.CheckResourceDisappears(ctx, acctest.Provider, ResourceExample(), resourceName), ), ExpectNonEmptyPlan: true, }, }, }) } ``` ### Test Helper Functions ```go func testAccCheckExampleExists(ctx context.Context, name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { return fmt.Errorf("Not found: %s", name) } conn := acctest.Provider.Meta().(*conns.Client).ExampleClient(ctx) _, err := findExampleByID(ctx, conn, rs.Primary.ID) return err } } func testAccCheckExampleDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.Client).ExampleClient(ctx) for _, rs := range s.RootModule().Resources { if rs.Type != "provider_example" { continue } _, err := findExampleByID(ctx, conn, rs.Primary.ID) if tfresource.NotFound(err) { continue } if err != nil { return err } return fmt.Errorf("Example %s still exists", rs.Primary.ID) } return nil } } ``` ### Running Tests ```bash # Compile tests go test -c -o /dev/null ./internal/service/ # Run acceptance tests TF_ACC=1 go test ./internal/service/ -run TestAccExample -v -timeout 60m # Run with specific provider version TF_ACC=1 go test ./internal/service/ -run TestAccExample -v # Run sweeper to clean up TF_ACC=1 go test ./internal/service/ -sweep= -v ``` ## Error Handling ### Common Error Patterns ```go // Handle specific API errors var notFound *types.ResourceNotFoundException if errors.As(err, ¬Found) { // Resource doesn't exist } var conflict *types.ConflictException if errors.As(err, &conflict) { // Resource state conflict } var throttle *types.ThrottlingException if errors.As(err, &throttle) { // Rate limited - SDK handles retry } ``` ### Diagnostics ```go // Add error resp.Diagnostics.AddError( "Error creating resource", fmt.Sprintf("Could not create resource: %s", err), ) // Add warning resp.Diagnostics.AddWarning( "Resource modified outside Terraform", "Resource was modified outside of Terraform, state may be inconsistent", ) // Add attribute error resp.Diagnostics.AddAttributeError( path.Root("name"), "Invalid name", "Name must be lowercase alphanumeric", ) ``` ## Documentation Standards ### Resource Documentation ```markdown --- subcategory: "Service Name" layout: "provider" page_title: "Provider: provider_example" description: |- Manages an Example resource. --- # Resource: provider_example Manages an Example resource. ## Example Usage ### Basic Usage \```hcl resource "provider_example" "example" { name = "my-example" } \``` ## Argument Reference * `name` - (Required) Name of the example. * `description` - (Optional) Description of the example. ## Attribute Reference * `id` - ID of the example. * `arn` - ARN of the example. ## Import Example can be imported using the ID: \``` $ terraform import provider_example.example example-id-12345 \``` ``` ## Pre-Submission Checklist - [ ] Code compiles without errors - [ ] All tests pass locally - [ ] Resource has all CRUD operations implemented - [ ] Import is implemented and tested - [ ] Disappears test is included - [ ] Documentation is complete with examples - [ ] Error messages are clear and actionable - [ ] Sensitive attributes are marked - [ ] Plan modifiers are appropriate - [ ] Validators cover edge cases ## References - [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework) - [Terraform Plugin SDKv2](https://developer.hashicorp.com/terraform/plugin/sdkv2) - [Acceptance Testing](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests) - [terraform-plugin-framework GitHub](https://github.com/hashicorp/terraform-plugin-framework) ================================================ FILE: terraform/provider-development/skills/provider-test-patterns/SKILL.md ================================================ --- name: provider-test-patterns description: >- Terraform provider acceptance test patterns using terraform-plugin-testing with the Plugin Framework. Covers test structure, TestCase/TestStep fields, ConfigStateChecks with custom statecheck.StateCheck implementations, plan checks, CompareValue for cross-step assertions, config helpers, import testing with ImportStateKind, sweepers, and scenario patterns (basic, update, disappears, validation, regression), and ephemeral resource testing with the echoprovider package. Use when writing, reviewing, or debugging provider acceptance tests, including questions about statecheck, plancheck, TestCheckFunc, CheckDestroy, ExpectError, import state verification, ephemeral resources, or how to structure test files. metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- # Provider Acceptance Test Patterns Patterns for writing acceptance tests using [terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing) with the [Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework). Source: [HashiCorp Testing Patterns](https://developer.hashicorp.com/terraform/plugin/testing/testing-patterns) **References** (load when needed): - `references/checks.md` — statecheck, plancheck, knownvalue types, tfjsonpath, comparers - `references/sweepers.md` — sweeper setup, TestMain, dependencies - `references/ephemeral.md` — ephemeral resource testing, echoprovider, multi-step patterns --- ## Test Lifecycle The framework runs each TestStep through: **plan → apply → refresh → final plan**. If the final plan shows a diff, the test fails (unless `ExpectNonEmptyPlan` is set). After all steps, destroy runs followed by `CheckDestroy`. This means every test automatically verifies that configurations apply cleanly and produce no drift — no assertions needed for that. --- ## Test Function Structure ```go func TestAccExample_basic(t *testing.T) { var widget example.Widget rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "example_widget.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, CheckDestroy: testAccCheckExampleDestroy, Steps: []resource.TestStep{ { Config: testAccExampleConfig_basic(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(rName)), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()), }, }, }, }) } ``` Use `resource.ParallelTest` by default. Use `resource.Test` only when tests share state or cannot run concurrently. --- ## Provider Factory ```go // provider_test.go — Plugin Framework with Protocol 6 (use Protocol5 variant if needed) var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ "example": providerserver.NewProtocol6WithError(New("test")()), } ``` --- ## TestCase Fields | Field | Purpose | |-------|---------| | `PreCheck` | `func()` — verify prerequisites (env vars, API access) | | `ProtoV6ProviderFactories` | Plugin Framework provider factories | | `CheckDestroy` | `TestCheckFunc` — verify resources destroyed after all steps | | `Steps` | `[]TestStep` — sequential test operations | | `TerraformVersionChecks` | `[]tfversion.TerraformVersionCheck` — gate by CLI version | --- ## TestStep Fields ### Config Mode | Field | Purpose | |-------|---------| | `Config` | Inline HCL string to apply | | `ConfigStateChecks` | `[]statecheck.StateCheck` — modern assertions (preferred) | | `ConfigPlanChecks` | `resource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{...}}` | | `ExpectError` | `*regexp.Regexp` — expect failure matching pattern | | `ExpectNonEmptyPlan` | `bool` — expect non-empty plan after apply | | `PlanOnly` | `bool` — plan without applying | | `Destroy` | `bool` — run destroy step | | `PreConfig` | `func()` — setup before step | ### Import Mode | Field | Purpose | |-------|---------| | `ImportState` | `true` to enable import mode | | `ImportStateVerify` | Verify imported state matches prior state | | `ImportStateVerifyIgnore` | `[]string` — attributes to skip during verify | | `ImportStateKind` | `resource.ImportBlockWithID` — import block generation | | `ResourceName` | Resource address to import | | `ImportStateId` | Override the ID used for import | --- ## Check Functions ### Modern: ConfigStateChecks (preferred) Type-safe with aggregated error reporting. Compose built-in checks with custom `statecheck.StateCheck` implementations. See `references/checks.md` for full knownvalue types, tfjsonpath navigation, and comparers. ```go ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact("my-widget")), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enabled"), knownvalue.Bool(true)), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()), statecheck.ExpectSensitiveValue(resourceName, tfjsonpath.New("api_key")), }, ``` Do not mix `Check` (legacy) and `ConfigStateChecks` in the same step. ### Legacy: Check (for CheckDestroy and migration) `CheckDestroy` on `TestCase` requires `TestCheckFunc`. The `Check` field on `TestStep` also accepts `TestCheckFunc` but prefer `ConfigStateChecks` for new tests. ```go Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(name, "key", "expected"), resource.TestCheckResourceAttrSet(name, "id"), resource.TestCheckNoResourceAttr(name, "removed"), resource.TestMatchResourceAttr(name, "url", regexp.MustCompile(`^https://`)), resource.TestCheckResourceAttrPair(res1, "ref_id", res2, "id"), ), ``` `ComposeAggregateTestCheckFunc` reports all errors; `ComposeTestCheckFunc` fails fast on the first. --- ## Config Helpers Use numbered format verbs — `%[1]q` for quoted strings, `%[1]s` for raw: ```go func testAccExampleConfig_basic(rName string) string { return fmt.Sprintf(` resource "example_widget" "test" { name = %[1]q } `, rName) } func testAccExampleConfig_full(rName, description string) string { return fmt.Sprintf(` resource "example_widget" "test" { name = %[1]q description = %[2]q enabled = true } `, rName, description) } ``` --- ## Scenario Patterns ### Basic + Update (combine in one test — updates are supersets of basic) ```go Steps: []resource.TestStep{ { Config: testAccExampleConfig_basic(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(rName)), }, }, { Config: testAccExampleConfig_full(rName, "updated"), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("description"), knownvalue.StringExact("updated")), }, }, }, ``` ### Import After a config step, verify import produces identical state. Use `ImportStateKind` for import block generation: ```go { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateKind: resource.ImportBlockWithID, }, ``` ### Disappears (resource deleted externally) ```go { Config: testAccExampleConfig_basic(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), stateCheckExampleDisappears(resourceName), }, ExpectNonEmptyPlan: true, }, ``` ### Validation (expect error) ```go { Config: testAccExampleConfig_invalidName(""), ExpectError: regexp.MustCompile(`name must not be empty`), }, ``` ### Regression (two-commit workflow) A proper bug fix uses at least two commits: first commit the regression test (which fails, confirming the bug), then commit the fix (test passes). This lets reviewers independently verify the test reproduces the issue by checking out the first commit, then advancing to the fix. Name and document regression tests to identify the issue they fix. Include a link to the original bug report when possible. ```go // TestAccExample_regressionGH1234 verifies fix for https://github.com/org/repo/issues/1234 func TestAccExample_regressionGH1234(t *testing.T) { rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "example_widget.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, CheckDestroy: testAccCheckExampleDestroy, Steps: []resource.TestStep{ { // Reproduce the issue: this config triggered the bug Config: testAccExampleConfig_regressionGH1234(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, nil), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("computed_field"), knownvalue.NotNull()), }, }, }, }) } ``` --- ## Helper Functions ### Custom StateCheck: Exists Implement `statecheck.StateCheck` for API existence verification. Separate the exists check into its own function for reuse across steps — the source recommends this as a design principle: ```go type exampleExistsCheck struct { resourceAddress string widget *example.Widget } func (e exampleExistsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { r, err := stateResourceAtAddress(req.State, e.resourceAddress) if err != nil { resp.Error = err return } id, ok := r.AttributeValues["id"].(string) if !ok { resp.Error = fmt.Errorf("no id found for %s", e.resourceAddress) return } conn := testAccAPIClient() widget, err := conn.GetWidget(id) if err != nil { resp.Error = fmt.Errorf("%s not found via API: %w", e.resourceAddress, err) return } if e.widget != nil { *e.widget = *widget } } func stateCheckExampleExists(name string, widget *example.Widget) statecheck.StateCheck { return exampleExistsCheck{resourceAddress: name, widget: widget} } ``` ### Custom StateCheck: Disappears Delete a resource via API to simulate external deletion: ```go type exampleDisappearsCheck struct { resourceAddress string } func (e exampleDisappearsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { r, err := stateResourceAtAddress(req.State, e.resourceAddress) if err != nil { resp.Error = err return } id := r.AttributeValues["id"].(string) conn := testAccAPIClient() resp.Error = conn.DeleteWidget(id) } func stateCheckExampleDisappears(name string) statecheck.StateCheck { return exampleDisappearsCheck{resourceAddress: name} } ``` ### State Resource Lookup (shared utility) ```go func stateResourceAtAddress(state *tfjson.State, address string) (*tfjson.StateResource, error) { if state == nil || state.Values == nil || state.Values.RootModule == nil { return nil, fmt.Errorf("no state available") } for _, r := range state.Values.RootModule.Resources { if r.Address == address { return r, nil } } return nil, fmt.Errorf("not found in state: %s", address) } ``` ### Destroy Check (TestCheckFunc — required by CheckDestroy) ```go func testAccCheckExampleDestroy(s *terraform.State) error { conn := testAccAPIClient() for _, rs := range s.RootModule().Resources { if rs.Type != "example_widget" { continue } _, err := conn.GetWidget(rs.Primary.ID) if err == nil { return fmt.Errorf("widget %s still exists", rs.Primary.ID) } if !isNotFoundError(err) { return err } } return nil } ``` ### PreCheck ```go func testAccPreCheck(t *testing.T) { t.Helper() if os.Getenv("EXAMPLE_API_KEY") == "" { t.Fatal("EXAMPLE_API_KEY must be set for acceptance tests") } } ``` ================================================ FILE: terraform/provider-development/skills/provider-test-patterns/references/checks.md ================================================ # State Checks and Plan Checks Reference Detailed reference for `statecheck` and `plancheck` packages from `terraform-plugin-testing`. Read this when writing assertions for test steps. Source: [State Checks](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/state-checks/resource), [Plan Checks](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/plan-checks) --- ## Table of Contents 1. [State Checks](#state-checks) 2. [Known Value Types](#known-value-types) 3. [tfjsonpath Navigation](#tfjsonpath-navigation) 4. [Value Comparers](#value-comparers) 5. [Plan Checks](#plan-checks) --- ## State Checks Use via `ConfigStateChecks` field on `TestStep`. All assertion errors are aggregated and reported together. ### ExpectKnownValue Assert an attribute has a specific type and value: ```go statecheck.ExpectKnownValue("example_widget.test", tfjsonpath.New("name"), knownvalue.StringExact("my-widget")) ``` ### ExpectSensitiveValue Assert an attribute is marked sensitive (requires Terraform 1.4.6+): ```go TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_4_6), }, // ... statecheck.ExpectSensitiveValue("example_widget.test", tfjsonpath.New("api_key")) ``` ### CompareValue Compare the same attribute across sequential test steps: ```go compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) Steps: []resource.TestStep{ { Config: testAccConfig_v1(rName), ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue("example_widget.test", tfjsonpath.New("id")), }, }, { Config: testAccConfig_v2(rName), ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue("example_widget.test", tfjsonpath.New("id")), }, }, }, ``` ### CompareValuePairs Compare attributes between two resources: ```go statecheck.CompareValuePairs( "example_widget.test", tfjsonpath.New("vpc_id"), "example_vpc.test", tfjsonpath.New("id"), compare.ValuesSame()) ``` ### CompareValueCollection Check if a value exists in a collection attribute: ```go statecheck.CompareValueCollection( "example_widget.test", tfjsonpath.New("tags"), "example_widget.test", tfjsonpath.New("name"), compare.ValuesSame()) ``` --- ## Known Value Types Use with `ExpectKnownValue` to assert attribute values: | Type | Example | |------|---------| | `knownvalue.StringExact("value")` | Exact string match | | `knownvalue.StringRegexp(regexp.MustCompile(`^arn:`))` | Regex match | | `knownvalue.Bool(true)` | Boolean value | | `knownvalue.Int64Exact(42)` | Exact int64 | | `knownvalue.Float64Exact(3.14)` | Exact float64 | | `knownvalue.NotNull()` | Value is set (not null) | | `knownvalue.Null()` | Value is null | | `knownvalue.ListExact([]knownvalue.Check{...})` | Exact list match | | `knownvalue.ListPartial(map[int]knownvalue.Check{0: ...})` | Partial list match | | `knownvalue.ListSizeExact(3)` | List has N elements | | `knownvalue.SetExact([]knownvalue.Check{...})` | Exact set match | | `knownvalue.SetPartial([]knownvalue.Check{...})` | Set contains items | | `knownvalue.SetSizeExact(2)` | Set has N elements | | `knownvalue.MapExact(map[string]knownvalue.Check{...})` | Exact map match | | `knownvalue.MapPartial(map[string]knownvalue.Check{...})` | Map contains keys | | `knownvalue.MapSizeExact(1)` | Map has N keys | | `knownvalue.ObjectExact(map[string]knownvalue.Check{...})` | Exact object match | | `knownvalue.ObjectPartial(map[string]knownvalue.Check{...})` | Object has attributes | | `knownvalue.Float32Exact(1.5)` | Exact float32 | | `knownvalue.Int32Exact(42)` | Exact int32 | | `knownvalue.NumberExact(big.NewFloat(42))` | Exact number (`*big.Float`) | | `knownvalue.TupleExact([]knownvalue.Check{...})` | Exact tuple match | | `knownvalue.TuplePartial(map[int]knownvalue.Check{0: ...})` | Partial tuple match | | `knownvalue.TupleSizeExact(3)` | Tuple has N elements | ### Nested Value Example ```go statecheck.ExpectKnownValue("example_widget.test", tfjsonpath.New("settings"), knownvalue.ObjectExact(map[string]knownvalue.Check{ "mode": knownvalue.StringExact("production"), "enabled": knownvalue.Bool(true), })) ``` --- ## tfjsonpath Navigation Navigate nested attributes in state: ```go tfjsonpath.New("attribute") // top-level attribute tfjsonpath.New("block").AtMapKey("key") // nested map/object key tfjsonpath.New("list_attr").AtSliceIndex(0) // list element by index tfjsonpath.New("block").AtMapKey("nested").AtMapKey("deep") // deep nesting ``` --- ## Value Comparers Use with `CompareValue`, `CompareValuePairs`, `CompareValueCollection`: | Comparer | Purpose | |----------|---------| | `compare.ValuesSame()` | Values are identical | | `compare.ValuesDiffer()` | Values are different | --- ## Plan Checks Use via `ConfigPlanChecks` or `RefreshPlanChecks` on `TestStep`. Plan checks inspect the plan file at specific phases. ### ConfigPlanChecks Phases ```go ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{...}, // after plan, before apply PostApplyPreRefresh: []plancheck.PlanCheck{...}, // after apply, before refresh PostApplyPostRefresh: []plancheck.PlanCheck{...}, // after refresh }, ``` ### Built-in Plan Checks ```go // Expect no changes in plan plancheck.ExpectEmptyPlan() // Expect changes in plan plancheck.ExpectNonEmptyPlan() // Expect specific resource action plancheck.ExpectResourceAction("example_widget.test", plancheck.ResourceActionCreate) plancheck.ExpectResourceAction("example_widget.test", plancheck.ResourceActionUpdate) plancheck.ExpectResourceAction("example_widget.test", plancheck.ResourceActionDestroy) plancheck.ExpectResourceAction("example_widget.test", plancheck.ResourceActionNoop) // Expect known plan value plancheck.ExpectKnownValue("example_widget.test", tfjsonpath.New("name"), knownvalue.StringExact("my-widget")) // Expect unknown (computed) value in plan plancheck.ExpectUnknownValue("example_widget.test", tfjsonpath.New("computed_field")) // Expect sensitive value in plan plancheck.ExpectSensitiveValue("example_widget.test", tfjsonpath.New("api_key")) ``` ### No-Op After Update Example Verify that updating a config back to original values produces no diff: ```go Steps: []resource.TestStep{ { Config: testAccConfig_basic(rName), }, { Config: testAccConfig_updated(rName), }, { Config: testAccConfig_basic(rName), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), }, }, }, }, ``` ================================================ FILE: terraform/provider-development/skills/provider-test-patterns/references/ephemeral.md ================================================ # Ephemeral Resource Testing Reference Testing patterns for ephemeral resources using `terraform-plugin-testing`. Ephemeral resources reference external data without persisting it to plan or state artifacts, which means standard plan checks and state checks cannot directly assert on ephemeral resource data. Source: [Ephemeral Resource Acceptance Tests](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/ephemeral-resources) **Requires Terraform >= 1.10.0** — gate all ephemeral tests with `tfversion.SkipBelow(tfversion.Version1_10_0)`. --- ## Table of Contents 1. [Testing Approaches](#testing-approaches) 2. [Direct Integration Testing](#direct-integration-testing) 3. [Echo Provider Pattern](#echo-provider-pattern) 4. [Multi-Step Testing](#multi-step-testing) --- ## Testing Approaches Two strategies for testing ephemeral resources: | Approach | When to use | |----------|-------------| | **Direct integration** | Verify the ephemeral resource successfully provides data to a dependent resource or provider | | **Echo provider** | Assert on specific attribute values using `ConfigStateChecks` via the `echoprovider` package | --- ## Direct Integration Testing Test that an ephemeral resource successfully provides data to a dependent resource. No direct assertions on ephemeral data — the test passes if the dependent resource applies cleanly. ```go func TestExampleCloudSecret_DnsKerberos(t *testing.T) { resource.UnitTest(t, resource.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_10_0), }, ExternalProviders: map[string]resource.ExternalProvider{ "dns": { Source: "hashicorp/dns", }, }, ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "examplecloud": providerserver.NewProtocol5WithError(New()), }, Steps: []resource.TestStep{ { Config: ` ephemeral "examplecloud_secret" "krb" { name = "example_kerberos_user" } provider "dns" { update { server = "ns.example.com" gssapi { realm = ephemeral.examplecloud_secret.krb.secret_data.realm username = ephemeral.examplecloud_secret.krb.secret_data.username password = ephemeral.examplecloud_secret.krb.secret_data.password } } } resource "dns_a_record_set" "record_set" { zone = "example.com." addresses = ["192.168.0.1", "192.168.0.2", "192.168.0.3"] } `, }, }, }) } ``` --- ## Echo Provider Pattern The `echoprovider` package (Protocol V6) captures ephemeral data into a managed resource's state, making it assertable with standard `ConfigStateChecks`. ### Setup Register both your provider and the echo provider: ```go import ( "github.com/hashicorp/terraform-plugin-testing/echoprovider" ) func TestExampleCloudSecret(t *testing.T) { resource.UnitTest(t, resource.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_10_0), }, ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "examplecloud": providerserver.NewProtocol5WithError(New()), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "echo": echoprovider.NewProviderServer(), }, Steps: []resource.TestStep{ // test configurations }, }) } ``` ### Config Pattern Pass ephemeral data to the echo provider's `data` attribute, then assert on the `echo` managed resource: ```terraform ephemeral "examplecloud_secret" "krb" { name = "example_kerberos_user" } provider "echo" { data = ephemeral.examplecloud_secret.krb.secret_data } resource "echo" "test_krb" {} ``` ### State Assertions Assert on the echo resource's `data` attribute using standard state checks: ```go Steps: []resource.TestStep{ { Config: `...`, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("realm"), knownvalue.StringExact("EXAMPLE.COM")), statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("username"), knownvalue.StringExact("john-doe")), statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("password"), knownvalue.StringRegexp(regexp.MustCompile(`^.{12}$`))), }, }, }, ``` --- ## Multi-Step Testing The echo resource has special behavior to accommodate ephemeral data variability: - During planning for new resources, the `data` attribute is marked unknown - Existing echo resources preserve prior state regardless of config changes - Refresh operations always return prior state Because of this, **create new echo resource instances for each test step** rather than reusing the same one: ```go Steps: []resource.TestStep{ { Config: ` ephemeral "examplecloud_secret" "krb" { name = "user_one" } provider "echo" { data = ephemeral.examplecloud_secret.krb } resource "echo" "test_krb_one" {} `, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("echo.test_krb_one", tfjsonpath.New("data").AtMapKey("name"), knownvalue.StringExact("user_one")), }, }, { Config: ` ephemeral "examplecloud_secret" "krb" { name = "user_two" } provider "echo" { data = ephemeral.examplecloud_secret.krb } resource "echo" "test_krb_two" {} `, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("echo.test_krb_two", tfjsonpath.New("data").AtMapKey("name"), knownvalue.StringExact("user_two")), }, }, }, ``` ================================================ FILE: terraform/provider-development/skills/provider-test-patterns/references/sweepers.md ================================================ # Test Sweepers Reference Sweepers clean up infrastructure resources that leak during acceptance tests — when test infrastructure fails to be destroyed due to API errors or test failures. Source: [Sweepers](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/sweepers) --- ## Setup ### TestMain (required) Add to a dedicated file (e.g., `sweep_test.go`): ```go func TestMain(m *testing.M) { resource.TestMain(m) } ``` This parses the `-sweep` flag and invokes registered sweepers. ### Register a Sweeper Register in the test file for the resource being swept, using `init()`: ```go func init() { resource.AddTestSweepers("example_widget", &resource.Sweeper{ Name: "example_widget", F: sweepWidgets, }) } func sweepWidgets(region string) error { client, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("getting client: %w", err) } conn := client.(*Client) widgets, err := conn.ListWidgets() if err != nil { return fmt.Errorf("listing widgets: %w", err) } for _, w := range widgets { if !strings.HasPrefix(w.Name, "test-acc") { continue } if err := conn.DeleteWidget(w.ID); err != nil { log.Printf("[WARN] Failed to delete widget %s: %s", w.ID, err) } } return nil } ``` Use a consistent test name prefix (e.g., `"test-acc"`) to identify test-created resources. ### Dependencies When resources have ordering requirements (e.g., child resources must be deleted before parents), the **parent** sweeper declares children as dependencies so they run first: ```go resource.AddTestSweepers("example_widget", &resource.Sweeper{ Name: "example_widget", Dependencies: []string{"example_widget_child"}, F: sweepWidgets, }) ``` Dependencies run **before** the sweeper that declares them. In this example, `example_widget_child` is swept first, then `example_widget`. ### Shared Client Create a helper to build an API client for the sweep region: ```go func sharedClientForRegion(region string) (any, error) { // Build and return a configured API client return NewClient(region) } ``` ## Running Sweepers ```bash # Run all sweepers for a region TF_ACC=1 go test ./internal/service/example -sweep=us-east-1 -v # Makefile target (common convention) make sweep ``` ================================================ FILE: terraform/provider-development/skills/run-acceptance-tests/SKILL.md ================================================ --- name: run-acceptance-tests description: Guide for running acceptance tests for a Terraform provider. Use this when asked to run an acceptance test or to run a test with the prefix `TestAcc`. license: MPL-2.0 metadata: copyright: Copyright IBM Corp. 2026 version: "0.0.1" --- An acceptance test is a Go test function with the prefix `TestAcc`. To run a focussed acceptance test named `TestAccFeatureHappyPath`: 1. Run `go test -run=TestAccFeatureHappyPath` with the following environment variables: - `TF_ACC=1` Default to non-verbose test output. 1. The acceptance tests may require additional environment variables for specific providers. If the test output indicates missing environment variables, then suggest how to set up these environment variables securely. To diagnose a failing acceptance test, use these options, in order. These options are cumulative: each option includes all the options above it. 1. Run the test again. Use the `-count=1` option to ensure that `go test` does not use a cached result. 1. Offer verbose `go test` output. Use the `-v` option. 1. Offer debug-level logging. Enable debug-level logging with the environment variable `TF_LOG=debug`. 1. Offer to persist the acceptance test's Terraform workspace. Enable persistance with the environment variable `TF_ACC_WORKING_DIR_PERSIST=1`. A passing acceptance test may be a false negative. To "flip" a passing acceptance test named `TestAccFeatureHappyPath`: 1. Edit the value of one of the TestCheckFuncs in one of the TestSteps in the TestCase. 1. Run the acceptance test. Expect the test to fail. 1. If the test fails, then undo the edit and report a successful flip. Else, keep the edit and report an unsuccessful flip.