terraform-github-actions-deploy by accolver/skill-maker
npx skills add https://github.com/accolver/skill-maker --skill terraform-github-actions-deploy创建生产级的 GitHub Actions 工作流,在拉取请求时执行 Terraform 计划,在合并时执行应用,使用无密钥云认证和纵深防御安全策略。
以下情况请勿使用:
terraform-style-guide 技能)terraform-test 技能)github-actions-templates 技能)在编写工作流之前,先了解现有情况:
.github/workflows/ 中是否存在现有的 Terraform 工作流infrastructure/ 目录是使用 Terragrunt 还是普通 Terraform 设置广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
这是不可协商的。 切勿在 GitHub Actions 中使用静态服务账户密钥或访问密钥。
工作流需要 id-token: write 权限才能从 GitHub 请求 OIDC 令牌,GCP 的工作负载身份池会验证此令牌并将其交换为短期凭证。
permissions:
contents: read
id-token: write # OIDC 令牌所需
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
如果 WIF 基础设施尚不存在,请先创建它。参见 references/wif-setup.md。
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
此工作流在触及基础设施文件的拉取请求上运行。它会生成一个计划并将其作为 PR 评论发布。
关键要求:
paths: ['infrastructure/**'])concurrency 取消过时的计划(安全,因为计划是只读的)-input=false 和 -no-color完整模板请参见 references/workflow-templates.md。
此工作流在 PR 合并到 main 分支时运行。它会重新计划并应用。
关键要求:
cancel-in-progress — 取消正在运行的应用会损坏状态needs: 依赖关系在 PR 上至少添加一个 IaC 安全扫描器作为必需的状态检查:
terraform plan -detailed-exitcode,在发生漂移时创建问题id-token: write 权限uses: 行都通过完整的 40 字符提交 SHA 固定(不是标签,不是分支 — 包括设置、缓存、扫描器和实用程序操作)permissions 块将 GITHUB_TOKEN 限制在所需的最小范围concurrency 且 cancel-in-progress: falseconcurrency 且 cancel-in-progress: true-input=false 标志-no-color 标志run: 块中github-actions 生态系统配置了 .github/dependabot.yml计划工作流(.github/workflows/terraform-plan.yml):
name: Terraform Plan
on:
pull_request:
branches: [main]
paths:
- "infrastructure/**"
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-plan-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_wrapper: false
- name: Terraform Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Terraform Plan
id: plan
run: terraform plan -no-color -input=false -out=tfplan 2>&1 | tee plan.txt
working-directory: infrastructure/
continue-on-error: true
- name: Comment Plan on PR
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PLAN: ${{ steps.plan.outputs.stdout }}
PLAN_OUTCOME: ${{ steps.plan.outcome }}
with:
script: |
const output = `#### Terraform Plan: \`${process.env.PLAN_OUTCOME}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${process.env.PLAN.substring(0, 60000)}
\`\`\`
</details>
*Triggered by @${{ github.actor }} on \`${{ github.event.pull_request.head.ref }}\`*`;
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(c =>
c.user.type === 'Bot' && c.body.includes('Terraform Plan:')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: output,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output,
});
}
- name: Fail if plan failed
if: steps.plan.outcome == 'failure'
run: exit 1
应用工作流(.github/workflows/terraform-apply.yml):
name: Terraform Apply
on:
push:
branches: [main]
paths:
- "infrastructure/**"
permissions:
contents: read
id-token: write
concurrency:
group: terraform-apply
cancel-in-progress: false # 切勿取消正在运行的应用
jobs:
apply:
name: Apply
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_wrapper: false
- name: Terraform Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Terraform Plan
run: terraform plan -no-color -input=false -out=tfplan
working-directory: infrastructure/
- name: Terraform Apply
run: terraform apply -input=false tfplan
working-directory: infrastructure/
| 错误 | 修复方法 |
|---|---|
| 在 GitHub Secrets 中使用服务账户密钥 JSON | 使用工作负载身份联合(GCP)或 OIDC(AWS)。零静态凭据。 |
通过标签(@v4)或分支(@master)固定操作 | 通过完整的 40 字符提交 SHA 固定所有操作:@abc123def456... # v4.2.2。这包括每一个 uses: 行 — checkout、auth、setup、cache、扫描器、script、upload-sarif 等。 |
在应用工作流上使用 cancel-in-progress: true | 设置为 false。取消正在运行的应用会损坏 Terraform 状态。 |
在 run: 块中插值 PR 标题/分支 | 使用中间 env: 变量来防止 shell 注入。 |
在 terraform 命令中缺少 -input=false | CI 会挂起等待交互式输入。始终传递 -input=false。 |
permissions 范围过宽 | 在工作流级别设置 contents: read。仅在需要的地方添加 id-token: write 和 pull-requests: write。 |
| 应用 PR 中的过时计划 | 在合并提交上应用前重新计划。PR 计划可能已过时。 |
| 没有并发控制 | 为每个环境添加 concurrency.group。并行应用会损坏状态。 |
| 所有环境使用单一服务账户 | 使用具有最小权限角色的按环境服务账户。通过分支限制生产环境的 WIF。 |
| 不在 PR 上运行安全扫描 | 添加 tfsec、checkov 或 trivy 作为必需的状态检查。 |
| 操作 | 模式 |
|---|---|
| GCP 身份验证(WIF) | google-github-actions/auth@v2 配合 workload_identity_provider + service_account |
| AWS 身份验证(OIDC) | aws-actions/configure-aws-credentials@v4 配合 role-to-assume |
| 设置 Terraform | hashicorp/setup-terraform@v3 配合 terraform_wrapper: false 以获取原始输出 |
| 并发(计划) | group: tf-plan-${{ github.event.pull_request.number }}, cancel-in-progress: true |
| 并发(应用) | group: tf-apply-${{ inputs.environment }}, cancel-in-progress: false |
| 发布 PR 评论 | actions/github-script 通过 env: 传递计划输出(非直接插值) |
| 漂移检测 | 计划任务 plan -detailed-exitcode,退出代码 2 = 漂移,创建 GitHub 问题 |
| 成本估算 | infracost/actions/setup@v3 + infracost/actions/comment@v1 |
| Dependabot 用于操作 | .github/dependabot.yml 配合 package-ecosystem: github-actions |
| Terragrunt 缓存 | 使用 actions/cache@v4 缓存 ~/.terraform.d/plugin-cache 和 ~/.terragrunt-cache |
零静态凭据 — 始终使用 OIDC/WIF。GitHub Secrets 中的服务账户密钥是等待发生的安全事件。来自 OIDC 的短期令牌作用域仅限于单个工作流运行。
切勿取消正在运行的应用 — 应用工作流上的 cancel-in-progress: false 是不可协商的。取消的 terraform apply 会使状态部分写入,资源处于未知状态。计划可以安全取消。
通过 SHA 固定每一个操作 — 无一例外 — 标签是可变的引用,可以重新分配。SHA 固定是不可变的。这适用于工作流中所有的 uses: 引用 — 不仅仅是 actions/checkout 和 google-github-actions/auth,还包括设置操作(hashicorp/setup-terraform、gruntwork-io/setup-terragrunt)、安全扫描器(aquasecurity/tfsec-action、bridgecrewio/checkov-action)、实用程序操作(actions/cache、actions/github-script)和上传操作(github/codeql-action)。格式:uses: org/action@<full-40-char-sha> # v1.2.3。使用 Dependabot 保持它们更新。
纵深防御 — 分层保护:分支保护、环境保护规则、必需评审者、CODEOWNERS、安全扫描、并发控制。没有单一层次是足够的。
处处最小权限 — 在工作流级别限制 GITHUB_TOKEN 权限。使用按环境服务账户。将 WIF 属性条件限定到特定仓库和分支。
每周安装数
1
仓库
GitHub 星标数
2
首次出现
1 天前
安全审计
安装于
amp1
cline1
openclaw1
opencode1
cursor1
continue1
Create production-grade GitHub Actions workflows that plan Terraform on pull requests and apply on merge, using keyless cloud authentication and defense-in-depth security.
Do NOT use when:
terraform-style-guide skill instead)terraform-test skill instead)github-actions-templates skill)Before writing workflows, understand what exists:
.github/workflows/ for existing Terraform workflowsinfrastructure/ for Terragrunt vs plain Terraform setupThis is non-negotiable. Never use static service account keys or access keys in GitHub Actions.
The workflow needs id-token: write permission to request an OIDC token from GitHub, which GCP's Workload Identity Pool validates and exchanges for a short-lived credential.
permissions:
contents: read
id-token: write # Required for OIDC token
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
If WIF infrastructure doesn't exist yet, create it first. See references/wif-setup.md.
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
This runs on pull requests that touch infrastructure files. It produces a plan and posts it as a PR comment.
Key requirements:
paths: ['infrastructure/**'])concurrency to cancel stale plans (safe because plans are read-only)-input=false and -no-colorSee references/workflow-templates.md for complete templates.
This runs when PRs merge to main. It re-plans and applies.
Key requirements:
cancel-in-progress — cancelling a running apply corrupts stateneeds: dependenciesAdd at least one IaC security scanner as a required status check on PRs:
terraform plan -detailed-exitcode that opens issues on driftid-token: write permission is setuses: line is pinned by full 40-char commit SHA (not tags, not branches — including setup, cache, scanner, and utility actions)permissions block restricts GITHUB_TOKEN to minimum required scopeconcurrency with cancel-in-progress: falseconcurrency with cancel-in-progress: true-input=false flag on all terraform/terragrunt commands-no-color flag on plan output used in PR commentsPlan workflow (.github/workflows/terraform-plan.yml):
name: Terraform Plan
on:
pull_request:
branches: [main]
paths:
- "infrastructure/**"
permissions:
contents: read
id-token: write
pull-requests: write
concurrency:
group: terraform-plan-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_wrapper: false
- name: Terraform Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Terraform Plan
id: plan
run: terraform plan -no-color -input=false -out=tfplan 2>&1 | tee plan.txt
working-directory: infrastructure/
continue-on-error: true
- name: Comment Plan on PR
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PLAN: ${{ steps.plan.outputs.stdout }}
PLAN_OUTCOME: ${{ steps.plan.outcome }}
with:
script: |
const output = `#### Terraform Plan: \`${process.env.PLAN_OUTCOME}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${process.env.PLAN.substring(0, 60000)}
\`\`\`
</details>
*Triggered by @${{ github.actor }} on \`${{ github.event.pull_request.head.ref }}\`*`;
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(c =>
c.user.type === 'Bot' && c.body.includes('Terraform Plan:')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: output,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output,
});
}
- name: Fail if plan failed
if: steps.plan.outcome == 'failure'
run: exit 1
Apply workflow (.github/workflows/terraform-apply.yml):
name: Terraform Apply
on:
push:
branches: [main]
paths:
- "infrastructure/**"
permissions:
contents: read
id-token: write
concurrency:
group: terraform-apply
cancel-in-progress: false # NEVER cancel running applies
jobs:
apply:
name: Apply
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: auth
uses: google-github-actions/auth@62cf5bd3e4211a0a0b51f2c6d6a37129d828611d # v2.1.7
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.TERRAFORM_SA }}
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_wrapper: false
- name: Terraform Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Terraform Plan
run: terraform plan -no-color -input=false -out=tfplan
working-directory: infrastructure/
- name: Terraform Apply
run: terraform apply -input=false tfplan
working-directory: infrastructure/
| Mistake | Fix |
|---|---|
| Using service account key JSON in GitHub Secrets | Use Workload Identity Federation (GCP) or OIDC (AWS). Zero static credentials. |
Pinning actions by tag (@v4) or branch (@master) | Pin ALL actions by full 40-char commit SHA: @abc123def456... # v4.2.2. This includes EVERY uses: line — checkout, auth, setup, cache, scanners, script, upload-sarif, etc. |
cancel-in-progress: true on apply workflows | Set to false. Cancelling a running apply corrupts Terraform state. |
Interpolating PR title/branch in run: blocks |
| Operation | Pattern |
|---|---|
| GCP auth (WIF) | google-github-actions/auth@v2 with workload_identity_provider + service_account |
| AWS auth (OIDC) | aws-actions/configure-aws-credentials@v4 with role-to-assume |
| Setup Terraform | hashicorp/setup-terraform@v3 with terraform_wrapper: false for raw output |
| Concurrency (plan) |
Zero static credentials — Always use OIDC/WIF. Service account keys in GitHub Secrets are a security incident waiting to happen. Short-lived tokens from OIDC are scoped to single workflow runs.
Never cancel a running apply — cancel-in-progress: false on apply workflows is non-negotiable. A cancelled terraform apply leaves state partially written and resources in an unknown state. Plans are safe to cancel.
Pin EVERY action by SHA — no exceptions — Tags are mutable references that can be reassigned. SHA pins are immutable. This applies to ALL uses: references in the workflow — not just actions/checkout and google-github-actions/auth, but also setup actions (hashicorp/setup-terraform, gruntwork-io/setup-terragrunt), security scanners (, ), utility actions (, ), and upload actions (). Format: . Use Dependabot to keep them updated.
Weekly Installs
1
Repository
GitHub Stars
2
First Seen
1 day ago
Security Audits
Gen Agent Trust HubPassSocketFailSnykPass
Installed on
amp1
cline1
openclaw1
opencode1
cursor1
continue1
Azure Data Explorer (Kusto) 查询技能:KQL数据分析、日志遥测与时间序列处理
114,200 周安装
run: blocks.github/dependabot.yml configured for github-actions ecosystemUse intermediate env: variables to prevent shell injection. |
Missing -input=false on terraform commands | CI hangs waiting for interactive input. Always pass -input=false. |
Overly broad permissions | Set contents: read at workflow level. Add id-token: write and pull-requests: write only where needed. |
| Applying a stale plan from PR | Re-plan before apply on the merge commit. PR plan may be outdated. |
| No concurrency controls | Add concurrency.group per environment. Parallel applies corrupt state. |
| Single service account for all environments | Use per-environment SAs with least-privilege roles. Restrict WIF by branch for prod. |
| Not running security scans on PRs | Add tfsec, checkov, or trivy as required status checks. |
group: tf-plan-${{ github.event.pull_request.number }}, cancel-in-progress: true |
| Concurrency (apply) | group: tf-apply-${{ inputs.environment }}, cancel-in-progress: false |
| Post PR comment | actions/github-script with plan output via env: (not direct interpolation) |
| Drift detection | Scheduled plan -detailed-exitcode, exit code 2 = drift, open GitHub issue |
| Cost estimation | infracost/actions/setup@v3 + infracost/actions/comment@v1 |
| Dependabot for actions | .github/dependabot.yml with package-ecosystem: github-actions |
| Terragrunt caching | Cache ~/.terraform.d/plugin-cache and ~/.terragrunt-cache with actions/cache@v4 |
aquasecurity/tfsec-actionbridgecrewio/checkov-actionactions/cacheactions/github-scriptgithub/codeql-actionuses: org/action@<full-40-char-sha> # v1.2.3Defense in depth — Layer protections: branch protection, environment protection rules, required reviewers, CODEOWNERS, security scanning, concurrency controls. No single layer is sufficient.
Least privilege everywhere — Restrict GITHUB_TOKEN permissions at workflow level. Use per-environment service accounts. Scope WIF attribute conditions to specific repos and branches.