Terraform cicd

Terraform cicd

Terraform-cicd

方案 1: github workflows
方案 2: Atlantis 根据分支合并请求 + 评论进行
方案 3: jenkins 手动发布

GitHub

  • PR 触发: 向 main 分支提交 PR 时, 流水线会运行, 主要为了校验代码和展示计划
  • Push 触发: 当代码合入 main 后, 流水线会执行实际的部署

可选事件

  • dev 环境在 dev 分支, 提交后自动触发变更, 不需人工审批
  • 生产环境走人工确认步骤
  • 将每个环境独立为不同的 workflows 文件, 避免复杂逻辑

实践 1: pr plan 生成的计划文件提交到 artifact, apply 时直接应用此文件
缺点: 计划可能过时, 如审批时间太久, 线上已经漂移, 或先合并了其它 pr 等场景, 所以一般情况不推荐这种做法;
更推荐以 main 分支为准, 提交或合并时, 执行当前最新生成的 plan 计划;

.github/workflows/terraform.yml

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
name: Terraform Validate

permissions:
  id-token: write   # 允许生成 OIDC JWT 令牌(用于云服务身份认证)
  contents: read    # 允许读取仓库代码(最基本的需求)
  pull-requests: write  # 允许对 PR 进行评论, 更新状态等

on:
  push:
    branches: [main]
    paths:
      - 'environments/**'
      - 'modules/**'
  pull_request:
    branches: [main]

env:
  TF_VERSION: '1.9.2'
  AWS_REGION: ap-northeast-1

# 并行控制 - 新任务排队 等前面的结束后再运行;
concurrency:
  group: terraform-task
  cancel-in-progress: false


jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Linter 校验检查
      - uses: terraform-linters/setup-tflint@v3
        with:
          tflint_version: v0.53.0

      - run: tflint --format=compact

      # 执行
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      # 语法和格式检查
      - name: Terraform Validate
        run: |
          for env in environments/*/; do
            echo "Validating $env"
            cd $env
            terraform init -backend=false
            terraform fmt -check -recursive
            terraform validate
            cd ../..
          done

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0


  plan:
    needs: [validate, security-scan]
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    strategy:
      matrix:
        environment: [dev, staging]
    steps:
      - uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

          # 推荐使用 OIDC 认证
          # role-to-assume: arn:aws:iam::123456789012:role/tf-prod
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Plan
        id: plan
        run: |
          cd environments/${{ matrix.environment }}
          terraform init
          terraform plan -no-color -out=tfplan

        # 忽略错误
        # 即使 plan 失败(比如权限不够或逻辑冲突), 也让流水线继续走下去;
        # 这是为了让下一步能把错误信息(如果有的话)反馈给开发者
        continue-on-error: true

      # 调用 GitHub API 在 PR 页面下方自动留言
      # 可以通过 @ 提醒指定人员
      - name: Update PR
        uses: actions/github-script@v7
        if: github.event == 'pull_request'
        with:
          script: |
            const output = `#### Terraform Plan 📖 \n\nplease @${{ github.actor }} review\n\n${{ steps.plan.outputs.stdout }}`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })


  apply:
    runs-on: ubuntu-latest
    environment: production   # 可选, 限制必须需要手动确定后才继续

    # 限制只有当代码真正合并到 main 分支时才会执行; pr 不会执行
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    strategy:
      matrix:
        environment: [dev, staging]
    steps:
      - uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      # 针对直接变更 main 分支的场景, 也进行一次语法和格式检查
      - name: Terraform Validate
        run: |
          for env in environments/*/; do
            echo "Validating $env"
            cd $env
            terraform init -backend=false
            terraform fmt -check -recursive
            terraform validate
            cd ../..
          done

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ env.AWS_REGION }}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

          # 推荐使用 OIDC 认证
          # role-to-assume: arn:aws:iam::123456789012:role/tf-prod

      - name: Terraform Apply
        run: |
          cd environments/${{ matrix.environment }}
          terraform init
          terraform apply -auto-approve

gitlib

示例1

.gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

stages:
  - validate
  - plan
  - apply
  - cleanup

variables:
  TF_VERSION: 1.8.0
  AWS_DEFAULT_REGION: ap-northeast-1

terraform:validate:
  stage: validate
  image: hashicorp/terraform:${TF_VERSION}
  before_script:
    - cd environments/prod
  script:
    - terraform init -backend=false
    - terraform validate
    - terraform fmt -check
  only:
    - merge_requests
    - main

terraform:plan:
  stage: plan
  image: hashicorp/terraform:${TF_VERSION}
  before_script:
    - cd environments/prod
    - alias aws='aws --region ${AWS_DEFAULT_REGION}'
  script:
    - terraform init
    - terraform plan -out=tfplan
  artifacts:
    name: tfplan
    paths:
      - environments/prod/tfplan
    expire_in: 1 day
  only:
    - merge_requests
    - main

terraform:apply:
  stage: apply
  image: hashicorp/terraform:${TF_VERSION}
  before_script:
    - cd environments/prod
    - alias aws='aws --region ${AWS_DEFAULT_REGION}'
  script:
    - terraform init
    - terraform apply -auto-approve tfplan
  environment:
    name: production
  when: manual
  only:
    - main

示例2

变量注入:
Terraform 配置 云 AccessKey, 将 AK/SK 通过 CI/CD 变量注入;

变量说明

  • Masked 变量在所有 Job 日志中被 [MASKED] 替代,防止泄露
  • Protected 变量仅在 protected 分支的 Pipeline 中生效
  • Terraform alicloud provider 自动读取 ALICLOUD_ACCESS_KEY/SECRET_KEY 环境变量

Stage 划分

  1. validate
  2. plan
  3. apply 手动确认
  4. destroy 手动确认

envs/dev.tfvars
envs/prod.tfvars

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148


# .gitlab-ci.yml
# ============================================
# 全局配置
# ============================================
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "web"
default:
  image:
    name: hashicorp/terraform:1.9

    # 覆盖 Terraform 镜像默认入口点
    entrypoint: [""]
  tags:
    - terraform
stages:
  - validate
  - plan
  - apply
  - destroy

# ============================================
# 全局变量
# ============================================
variables:
  TF_ROOT: ${CI_PROJECT_DIR}/terraform-landing-zone
  TF_ENV: "dev"
  TF_INPUT: "false"             # 禁用交互式提示,适配 CI 环境
  TF_IN_AUTOMATION: "true"

# 缓存 Provider 插件
cache:
  key: terraform-${TF_ENV}
  paths:
    - ${TF_ROOT}/.terraform/

# 公共脚本片段
.terraform_init: &terraform_init
  - cd ${TF_ROOT}
  - terraform --version
  - mkdir -p ~/.ssh && echo "${SSH_PUBLIC_KEY}" > ~/.ssh/id_ed25519.pub
  - |
    cat > ~/.terraformrc << 'EOF'
    provider_installation {
      network_mirror {
        url = "https://mirrors.aliyun.com/terraform/"
        include = [
          "registry.terraform.io/aliyun/alicloud"
        ]
      }
      direct {
        exclude = [
          "registry.terraform.io/aliyun/alicloud"
        ]
      }
    }
    EOF
  - terraform init -reconfigure
# ============================================
# Stage 1: 代码验证
# ============================================
fmt:
  stage: validate
  script:
    - cd ${TF_ROOT}
    - terraform fmt -recursive -check -diff
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
validate:
  stage: validate
  script:
    - *terraform_init
    - terraform validate
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# ============================================
# Stage 2: 执行计划
# ============================================
plan:
  stage: plan
  script:
    - *terraform_init
    - terraform plan
      -var-file="envs/${TF_ENV}.tfvars"
      -out="${TF_ENV}.tfplan"
    - terraform show -no-color "${TF_ENV}.tfplan"
      > "${TF_ENV}-plan-output.txt"
  artifacts:
    name: "plan-${TF_ENV}-${CI_COMMIT_SHORT_SHA}"
    paths:
      - ${TF_ROOT}/${TF_ENV}.tfplan
      - ${TF_ROOT}/${TF_ENV}-plan-output.txt
    expire_in: 7 days

    # 在 MR 中显示 plan 摘要
    reports:
      terraform:
        - ${TF_ROOT}/${TF_ENV}-plan-output.txt
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# ============================================
# Stage 3: 部署基础设施
# ============================================
apply:
  stage: apply
  script:
    - *terraform_init
    - terraform apply -auto-approve "${TF_ENV}.tfplan"
  dependencies:
    - plan      # 依赖阶段
  environment:
    name: ${TF_ENV}
    action: start
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual          # 需人工在界面中点击触发
      allow_failure: false
  resource_group: ${TF_ENV}     # 确保同环境不并发执行

# ============================================
# Stage 4: 销毁基础设施
# ============================================
destroy:
  stage: destroy
  script:
    - *terraform_init
    - terraform destroy
      -var-file="envs/${TF_ENV}.tfvars"
      -auto-approve
  environment:
    name: ${TF_ENV}
    action: stop
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
      allow_failure: true
  resource_group: ${TF_ENV}

#===================================================================
Licensed under CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,知会作者并注明出处
不得用于商业用途!
最后更新于 2025-09-20 00:00 UTC