| name | scaffold-terraform |
| description | Scaffold a Terraform infrastructure project with CI/CD (plan on PR, apply on push), AWS OIDC auth, .envrc, and standard files. Uses terraform CLI as the native tool. Loads on top of scaffold-project (run that first for cross-language standard files). Use when creating infrastructure repos, or when the user mentions "new Terraform project", "terraform init", "AWS infra", or "infrastructure scaffold". Do NOT use for Go, Rust, Python, or Node application projects; use the matching scaffold-<lang> skill.
|
| allowed-tools | Read, Grep, Glob, Edit, Write, Bash(terraform *), Bash(git *) |
| user-invocable | true |
| metadata | {"title":"Scaffold Terraform Project","category":"development","order":14} |
Scaffold Terraform Project
Generate a production-ready Terraform project following established CI/CD patterns. Read the scaffold-project skill first for standard files (README, AGENTS.md, LICENSE, CONTRIBUTING.md, llms.txt).
When to Use
- Creating a new infrastructure-as-code repository
- Adding CI/CD to an existing Terraform project
- Standardizing a Terraform project to match org conventions
Generated Files
.github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: terraform-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
id-token: write
pull-requests: write
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.plan.outputs.has_changes }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
terraform_wrapper: false
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check
continue-on-error: true
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: |
terraform plan -no-color -out=tfplan -detailed-exitcode 2>&1 | tee plan_output.txt || exit_code=$?
if [ "${exit_code:-0}" -eq 2 ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
if [ "${exit_code:-0}" -eq 1 ]; then
exit 1
fi
- name: Upload Plan
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
retention-days: 1
- name: Comment PR with Plan
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('plan_output.txt', 'utf8');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes('Terraform Plan')
);
const truncatedPlan = plan.length > 60000
? plan.substring(0, 60000) + '\n\n... (truncated)'
: plan;
const body = `## Terraform Plan\n<details>\n<summary>Click to expand plan output</summary>\n\n\`\`\`hcl\n${truncatedPlan}\n\`\`\`\n</details>\n\n**Workflow Run:** [View Details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: botComment.id, body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, body
});
}
- name: Plan Summary
run: |
echo "## Terraform Plan" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat plan_output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
apply:
name: Terraform Apply
needs: plan
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.plan.outputs.has_changes == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
- name: Download Plan
uses: actions/download-artifact@v4
with:
name: tfplan
- name: Terraform Apply
run: terraform apply -auto-approve tfplan
- name: Terraform Output Summary
run: |
echo "## Terraform Apply Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Infrastructure has been updated successfully." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Outputs" >> $GITHUB_STEP_SUMMARY
terraform output -no-color >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "No outputs defined" >> $GITHUB_STEP_SUMMARY
No separate ci.yml + release.yml. Terraform uses a single terraform.yml with plan-on-PR and apply-on-push.
Common Commands
No justfile. Terraform CLI is the native tool:
terraform init
terraform fmt -recursive
terraform validate
terraform plan
terraform apply
terraform output
For multi-environment setups (dev/staging/prod workspaces, multiple state files), add a justfile to manage environment switching and orchestration.
.envrc
dotenv
Project Layout
main.tf # primary resources
variables.tf # input variables
outputs.tf # output values
providers.tf # provider configuration
backend.tf # state backend (S3)
terraform.tfvars # variable values (NOT committed if secrets)
Required GitHub Secrets
| Secret | Purpose |
|---|
AWS_ROLE_ARN | IAM role ARN for OIDC assumption |
AWS_REGION | AWS region (e.g., us-east-1) |
Gotchas
cancel-in-progress: false never cancel a running Terraform operation
terraform_wrapper: false in plan job to get raw exit codes (exit code 2 = changes detected)
- Plan is saved as artifact and downloaded for apply. Ensures apply matches what was planned
- PR comments truncate at 60k chars to stay within GitHub's limits
- OIDC auth (
id-token: write + aws-actions/configure-aws-credentials). No long-lived AWS keys
terraform fmt -check uses continue-on-error: true. Warns but doesn't block
- No sr.yaml or semantic release. Infrastructure changes are applied directly, not versioned/tagged
- Single concurrency group prevents concurrent applies that could corrupt state