跳至内容
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

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


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



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

#===================================================================
最后更新于