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..