You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在GitHub Actions中实现Terraform计划免审、应用需审并PR展示计划

问题描述

我正在编写一个用Terraform将基础设施部署到Azure的GitHub Actions工作流,原本通过GitHub环境完成各环境的认证,每个环境存放部署所需的变量和密钥。

当前的问题是:现有terraform作业会同时执行计划(plan)和应用(apply),开发人员无法提前查看生成的计划。我想要实现计划自动运行,仅在应用阶段等待审批,但直接用GitHub环境会导致每个环境生成两次审批步骤;同时需要在Terraform文件变更时,自动在PR评论中展示计划。

我想到的临时方案是把所有密钥和变量放在仓库级别,只将GitHub环境用作审批闸门。请问如何实现这个需求?有没有更优的解决思路?


临时解决方案(不满意)

我已将计划和应用拆分为两个独立作业,但被迫使用PAT而非工作流自带的GitHub密钥——因为默认密钥没有读取环境变量的权限。

计划作业中新增了读取环境变量的步骤:

- name: Set environment context
  id: context
  env:
    GITHUB_TOKEN: ${{ secrets.GH_PAT }}
  run: |
    vars=$(gh variable list --env ${{ matrix.env }})

    echo "$vars" | while read -r line; do
        if [[ -n $line ]]; then
            var_name=$(echo $line | awk '{print $1}')
            var_value=$(echo $line | awk '{print $2}')
            echo "Setting environment variable $var_name=$var_value"
            echo "$var_name=$var_value" >> $GITHUB_ENV
        fi
    done

计划完成后,会保存制品供应用作业使用。


环境配置

dev环境

dev环境配置

prd环境

prd环境配置


原工作流代码
name: Terraform

on:
  pull_request:
    branches:
      - main
    paths:
      - "**.tf"
      - .github/workflows/deploy_infrastructure.yml
  workflow_dispatch:
    inputs:
      env:
        description: Environment to deploy to
        type: environment

permissions:
  id-token: write # Required for Azure login using federated credentials
  contents: read # Required for the checkout of code
  pull-requests: write # Required to write a PR comment

jobs:
  deployment-matrix:
    name: Set deployment matrix
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
    - id: set-matrix
      shell: bash
      run: |
        includes=()

        if [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then
          includes+=('{"env": "${{ inputs.env }}"}')
        else
          includes+=('{"env": "dev"}')

          if [ "$GITHUB_REF_NAME" == "main" ]; then
            includes+=('{"env": "prd"}')
          fi
        fi

        includes_string=$(IFS=,; echo "${includes[*]}")
        deployments="{\"include\": [$includes_string]}"

        echo "Generated strategy matrix: $deployments"

        echo "matrix=${deployments}" >> $GITHUB_OUTPUT

  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    needs:
      - deployment-matrix
    strategy:
      matrix: ${{fromJson(needs.deployment-matrix.outputs.matrix)}}
      max-parallel: 1
    environment: ${{ matrix.env }}
    defaults:
      run:
        working-directory: terraform

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.ARM_CLIENT_ID }}
          tenant-id: ${{ secrets.ARM_TENANT_ID }}
          subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}

      - name: Format
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Init
        id: init
        run: |
          terraform init \
            -backend-config="resource_group_name=${{ vars.RESOURCE_GROUP_NAME }}" \
            -backend-config="storage_account_name=${{ vars.TF_STORAGE_ACCOUNT_NAME }}" \
            -backend-config="client_id=${{ secrets.ARM_CLIENT_ID }}" \
            -backend-config="tenant_id=${{ secrets.ARM_TENANT_ID }}" \
            -backend-config="subscription_id=${{ secrets.ARM_SUBSCRIPTION_ID }}"

      - name: Validate
        id: validate
        run: terraform validate

      - name: Plan
        id: plan
        continue-on-error: true
        run: |
          terraform plan -var="environment=${{ matrix.env }}" -no-color -out=tfplan > /dev/null
          terraform show -no-color tfplan

      - uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        name: PR Comment
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            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 => {
              return comment.user.type === 'Bot' && comment.body.includes('Terraform summary')
            });

            if (botComment) {
              github.rest.issues.deleteComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
              });
            }

            const output = `# Terraform summary
            #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`

            <details><summary>Show plan</summary>

            \`\`\`\n
            ${{ steps.plan.outputs.stdout }}
            \`\`\`

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

      - name: Apply
        if: steps.fmt.outcome == 'success' && steps.plan.outcome == 'success'
        run: terraform apply tfplan

更优解决方案

核心思路

保留GitHub环境的变量/密钥隔离能力,拆分plan和apply作业:仅让apply作业关联GitHub环境触发审批,plan作业通过配置环境读取权限直接访问对应环境的变量,无需额外PAT。

具体实现步骤

1. 拆分作业为Plan和Apply独立任务

jobs:
  # 保留原有的部署矩阵生成作业
  deployment-matrix:
    name: Set deployment matrix
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
    - id: set-matrix
      shell: bash
      run: |
        includes=()

        if [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then
          includes+=('{"env": "${{ inputs.env }}"}')
        else
          includes+=('{"env": "dev"}')

          if [ "$GITHUB_REF_NAME" == "main" ]; then
            includes+=('{"env": "prd"}')
          fi
        fi

        includes_string=$(IFS=,; echo "${includes[*]}")
        deployments="{\"include\": [$includes_string]}"

        echo "Generated strategy matrix: $deployments"
        echo "matrix=${deployments}" >> $GITHUB_OUTPUT

  # 独立的Plan作业:自动运行,无审批
  terraform-plan:
    name: Terraform Plan (${{ matrix.env }})
    runs-on: ubuntu-latest
    needs: deployment-matrix
    strategy:
      matrix: ${{fromJson(needs.deployment-matrix.outputs.matrix)}}
      max-parallel: 1
    # 配置权限:允许读取环境变量
    permissions:
      environments: read
      id-token: write
      contents: read
      pull-requests: write
    defaults:
      run:
        working-directory: terraform

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.ARM_CLIENT_ID_${{ matrix.env }} }}
          tenant-id: ${{ secrets.ARM_TENANT_ID_${{ matrix.env }} }}
          subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID_${{ matrix.env }} }}

      - name: Format
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Init
        id: init
        run: |
          terraform init \
            -backend-config="resource_group_name=${{ vars.RESOURCE_GROUP_NAME_${{ matrix.env }} }}" \
            -backend-config="storage_account_name=${{ vars.TF_STORAGE_ACCOUNT_NAME_${{ matrix.env }} }}" \
            -backend-config="client_id=${{ secrets.ARM_CLIENT_ID_${{ matrix.env }} }}" \
            -backend-config="tenant_id=${{ secrets.ARM_TENANT_ID_${{ matrix.env }} }}" \
            -backend-config="subscription_id=${{ secrets.ARM_SUBSCRIPTION_ID_${{ matrix.env }} }}"

      - name: Validate
        id: validate
        run: terraform validate

      - name: Plan
        id: plan
        continue-on-error: true
        run: |
          terraform plan -var="environment=${{ matrix.env }}" -no-color -out=tfplan > /dev/null
          terraform show -no-color tfplan

      - name: Upload Plan Artifact
        uses: actions/upload-artifact@v3
        with:
          name: tfplan-${{ matrix.env }}
          path: terraform/tfplan

      # 保留PR评论逻辑
      - uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        name: PR Comment
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            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 => {
              return comment.user.type === 'Bot' && comment.body.includes('Terraform summary')
            });

            if (botComment) {
              github.rest.issues.deleteComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
              });
            }

            const output = `# Terraform summary
            #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`

            <details><summary>Show plan</summary>

            \`\`\`\n
            ${{ steps.plan.outputs.stdout }}
            \`\`\`

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

  # 独立的Apply作业:关联环境触发审批
  terraform-apply:
    name: Terraform Apply (${{ matrix.env }})
    runs-on: ubuntu-latest
    needs: terraform-plan
    strategy:
      matrix: ${{fromJson(needs.deployment-matrix.outputs.matrix)}}
      max-parallel: 1
    # 仅此作业关联环境,触发审批流程
    environment: ${{ matrix.env }}
    defaults:
      run:
        working-directory: terraform

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.ARM_CLIENT_ID_${{ matrix.env }} }}
          tenant-id: ${{ secrets.ARM_TENANT_ID_${{ matrix.env }} }}
          subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID_${{ matrix.env }} }}

      - name: Download Plan Artifact
        uses: actions/download-artifact@v3
        with:
          name: tfplan-${{ matrix.env }}
          path: terraform/

      - name: Init
        id: init
        run: |
          terraform init \
            -backend-config="resource_group_name=${{ vars.RESOURCE_GROUP_NAME_${{ matrix.env }} }}" \
            -backend-config="storage_account_name=${{ vars.TF_STORAGE_ACCOUNT_NAME_${{ matrix.env }} }}" \
            -backend-config="client_id=${{ secrets.ARM_CLIENT_ID_${{ matrix.env }} }}" \
            -backend-config="tenant_id=${{ secrets.ARM_TENANT_ID_${{ matrix.env }} }}" \
            -backend-config="subscription_id=${{ secrets.ARM_SUBSCRIPTION_ID_${{ matrix.env }} }}"

      - name: Apply
        run: terraform apply tfplan

2. 环境变量/密钥命名规范

给不同环境的变量/密钥添加环境后缀,实现隔离:

  • dev环境:ARM_CLIENT_ID_DEV、RESOURCE_GROUP_NAME_DEV
  • prd环境:ARM_CLIENT_ID_PRD、RESOURCE_GROUP_NAME_PRD

这样Plan作业可以通过${{ secrets.ARM_CLIENT_ID_${{ matrix.env }} }}的语法动态读取对应环境的变量,无需自定义脚本。

3. 权限配置

在Plan作业的permissions块中添加environments: read,让默认的GITHUB_TOKEN拥有读取环境变量的权限,彻底替代PAT。

方案优势

  1. 保留GitHub环境的变量/密钥隔离能力,无需将敏感信息移至仓库级别
  2. 仅Apply阶段触发审批,避免重复审批流程
  3. 利用GitHub原生语法动态读取环境变量,无需自定义脚本
  4. Plan作业自动运行并在PR中展示结果,符合开发流程预期

内容的提问来源于stack exchange,提问作者jokarl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 19:42:02