CI/CD 流水线集成

将 Terraform 集成到 CI/CD 流水线,实现自动化基础设施管理。

一、CI/CD 最佳实践

1.1 工作流程

┌─────────────────────────────────────────────────────┐
│                  Git Repository                      │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐       │
│  │   Dev     │  │  Staging  │  │   Prod    │       │
│  │  Branch   │  │   Branch  │  │  Branch   │       │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘       │
└────────┼──────────────┼──────────────┼─────────────┘
         │              │              │
         ▼              ▼              ▼
┌────────────────────────────────────────────────────┐
│              CI/CD Pipeline                        │
│  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐          │
│  │Lint  │→│Plan  │→│Apply │→│Test  │          │
│  └──────┘  └──────┘  └──────┘  └──────┘          │
└────────────────────────────────────────────────────┘
         │              │              │
         ▼              ▼              ▼
┌────────────────────────────────────────────────────┐
│           Infrastructure Environments               │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐      │
│  │    Dev    │  │  Staging  │  │   Prod    │      │
│  └───────────┘  └───────────┘  └───────────┘      │
└────────────────────────────────────────────────────┘

1.2 核心原则

  • 代码审查:所有变更必须经过 Pull Request
  • 自动化测试:验证、格式化、安全扫描
  • 环境隔离:开发、预发布、生产分离
  • 审批流程:生产环境变更需要人工审批
  • 回滚能力:保留变更历史,支持快速回滚

二、GitHub Actions 集成

2.1 基础工作流

.github/workflows/terraform.yml:

name: Terraform CI/CD

on:
  push:
    branches:
      - main
      - develop
  pull_request:
    branches:
      - main

env:
  TF_VERSION: 1.6.0
  AWS_REGION: us-west-2

jobs:
  terraform-check:
    name: Terraform Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Format
        run: terraform fmt -check -recursive
      
      - name: Terraform Init
        run: terraform init -backend=false
      
      - name: Terraform Validate
        run: terraform validate
      
      - name: Run tflint
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest
      
      - name: TFLint
        run: tflint --recursive
      
      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.3
        with:
          soft_fail: true

  terraform-plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    needs: terraform-check
    if: github.event_name == 'pull_request'
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - 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 }}
          aws-region: ${{ env.AWS_REGION }}
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Init
        run: terraform init
      
      - name: Terraform Plan
        id: plan
        run: |
          terraform plan -no-color -out=tfplan
          terraform show -no-color tfplan > plan.txt
      
      - name: Comment PR
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('plan.txt', 'utf8');
            const output = `#### Terraform Plan 📖
            <details><summary>Show Plan</summary>
            
            \`\`\`terraform
            ${plan}
            \`\`\`
            
            </details>`;
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

  terraform-apply:
    name: Terraform Apply
    runs-on: ubuntu-latest
    needs: terraform-check
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - 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 }}
          aws-region: ${{ env.AWS_REGION }}
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Init
        run: terraform init
      
      - name: Terraform Apply
        run: terraform apply -auto-approve
      
      - name: Notify Success
        if: success()
        run: echo "Terraform apply succeeded!"
      
      - name: Notify Failure
        if: failure()
        run: echo "Terraform apply failed!"

2.2 多环境工作流

.github/workflows/terraform-multi-env.yml:

name: Multi-Environment Terraform

on:
  push:
    branches:
      - develop
      - staging
      - main

env:
  TF_VERSION: 1.6.0

jobs:
  determine-environment:
    runs-on: ubuntu-latest
    outputs:
      environment: ${{ steps.set-env.outputs.environment }}
      tfvars: ${{ steps.set-env.outputs.tfvars }}
    steps:
      - name: Determine Environment
        id: set-env
        run: |
          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "environment=production" >> $GITHUB_OUTPUT
            echo "tfvars=prod.tfvars" >> $GITHUB_OUTPUT
          elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then
            echo "environment=staging" >> $GITHUB_OUTPUT
            echo "tfvars=staging.tfvars" >> $GITHUB_OUTPUT
          else
            echo "environment=development" >> $GITHUB_OUTPUT
            echo "tfvars=dev.tfvars" >> $GITHUB_OUTPUT
          fi

  terraform:
    name: Terraform ${{ needs.determine-environment.outputs.environment }}
    runs-on: ubuntu-latest
    needs: determine-environment
    environment: ${{ needs.determine-environment.outputs.environment }}
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - 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 }}
          aws-region: us-west-2
          role-to-assume: ${{ secrets[format('AWS_ROLE_{0}', needs.determine-environment.outputs.environment)] }}
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Init
        run: |
          terraform init \
            -backend-config="key=${{ needs.determine-environment.outputs.environment }}/terraform.tfstate"
      
      - name: Terraform Plan
        run: |
          terraform plan \
            -var-file="environments/${{ needs.determine-environment.outputs.tfvars }}" \
            -out=tfplan
      
      - name: Terraform Apply
        run: terraform apply tfplan

三、GitLab CI/CD 集成

3.1 基础配置

.gitlab-ci.yml:

image:
  name: hashicorp/terraform:1.6
  entrypoint: [""]

variables:
  TF_ROOT: ${CI_PROJECT_DIR}
  TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/default

cache:
  paths:
    - ${TF_ROOT}/.terraform

before_script:
  - cd ${TF_ROOT}
  - terraform --version
  - terraform init

stages:
  - validate
  - plan
  - apply

validate:
  stage: validate
  script:
    - terraform validate
    - terraform fmt -check
  only:
    - merge_requests
    - main

plan:
  stage: plan
  script:
    - terraform plan -out=tfplan
    - terraform show -json tfplan > plan.json
  artifacts:
    paths:
      - tfplan
      - plan.json
    expire_in: 1 week
  only:
    - merge_requests
    - main

apply:
  stage: apply
  script:
    - terraform apply -auto-approve tfplan
  dependencies:
    - plan
  only:
    - main
  when: manual
  environment:
    name: production
    action: start

3.2 多环境配置

.gitlab-ci.yml:

variables:
  TF_VERSION: 1.6.0

.terraform_base:
  image:
    name: hashicorp/terraform:${TF_VERSION}
    entrypoint: [""]
  before_script:
    - cd ${TF_ROOT}
    - terraform init -backend-config="key=${CI_ENVIRONMENT_NAME}/terraform.tfstate"

stages:
  - validate
  - plan:dev
  - apply:dev
  - plan:staging
  - apply:staging
  - plan:prod
  - apply:prod

validate:
  extends: .terraform_base
  stage: validate
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
  script:
    - terraform validate
    - terraform fmt -check
  only:
    - merge_requests

# Development
plan:dev:
  extends: .terraform_base
  stage: plan:dev
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: dev
  script:
    - terraform plan -var-file="environments/dev.tfvars" -out=tfplan
  artifacts:
    paths:
      - tfplan
  environment:
    name: dev
  only:
    - develop

apply:dev:
  extends: .terraform_base
  stage: apply:dev
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: dev
  script:
    - terraform apply -auto-approve tfplan
  dependencies:
    - plan:dev
  environment:
    name: dev
    action: start
  only:
    - develop

# Staging
plan:staging:
  extends: .terraform_base
  stage: plan:staging
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: staging
  script:
    - terraform plan -var-file="environments/staging.tfvars" -out=tfplan
  artifacts:
    paths:
      - tfplan
  environment:
    name: staging
  only:
    - staging

apply:staging:
  extends: .terraform_base
  stage: apply:staging
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: staging
  script:
    - terraform apply -auto-approve tfplan
  dependencies:
    - plan:staging
  environment:
    name: staging
    action: start
  only:
    - staging
  when: manual

# Production
plan:prod:
  extends: .terraform_base
  stage: plan:prod
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: prod
  script:
    - terraform plan -var-file="environments/prod.tfvars" -out=tfplan
  artifacts:
    paths:
      - tfplan
  environment:
    name: prod
  only:
    - main

apply:prod:
  extends: .terraform_base
  stage: apply:prod
  variables:
    TF_ROOT: ${CI_PROJECT_DIR}
    CI_ENVIRONMENT_NAME: prod
  script:
    - terraform apply -auto-approve tfplan
  dependencies:
    - plan:prod
  environment:
    name: prod
    action: start
  only:
    - main
  when: manual

四、Terraform Cloud 集成

4.1 VCS 驱动工作流

配置 Terraform Cloud:

  1. 创建组织和工作区
  2. 连接 VCS(GitHub/GitLab)
  3. 配置工作区设置

terraform.tf:

terraform {
  cloud {
    organization = "my-org"
    
    workspaces {
      name = "production"
    }
  }
}

GitHub Actions 触发:

name: Terraform Cloud

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
      
      - name: Terraform Init
        run: terraform init
      
      - name: Terraform Format
        run: terraform fmt -check
      
      - name: Terraform Plan
        run: terraform plan

4.2 API 驱动工作流

使用 Terraform Cloud API:

#!/bin/bash

# 配置
ORG_NAME="my-org"
WORKSPACE_NAME="production"
TF_TOKEN="your-token"
API_URL="https://app.terraform.io/api/v2"

# 创建配置版本
CONFIG_VERSION=$(curl \
  --header "Authorization: Bearer $TF_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  --request POST \
  --data '{"data":{"type":"configuration-versions"}}' \
  "$API_URL/workspaces/$WORKSPACE_ID/configuration-versions" \
  | jq -r '.data.id')

# 上传配置
tar -czf config.tar.gz -C . .
curl \
  --header "Content-Type: application/octet-stream" \
  --request PUT \
  --data-binary @config.tar.gz \
  "$UPLOAD_URL"

# 创建运行
RUN_ID=$(curl \
  --header "Authorization: Bearer $TF_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  --request POST \
  --data "{\"data\":{\"type\":\"runs\",\"attributes\":{\"message\":\"Triggered via API\"},\"relationships\":{\"workspace\":{\"data\":{\"type\":\"workspaces\",\"id\":\"$WORKSPACE_ID\"}}}}}" \
  "$API_URL/runs" \
  | jq -r '.data.id')

echo "Run created: $RUN_ID"

五、安全最佳实践

5.1 密钥管理

使用 GitHub Secrets:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}

使用 AWS Secrets Manager:

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/database/password"
}

resource "aws_db_instance" "main" {
  password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"]
}

5.2 权限最小化

IAM 策略示例:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "ec2:Get*",
        "ec2:List*"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateTags",
        "ec2:DeleteTags"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "ec2:CreateAction": ["RunInstances", "CreateVolume"]
        }
      }
    }
  ]
}

5.3 审计和合规

启用 CloudTrail:

resource "aws_cloudtrail" "terraform" {
  name                          = "terraform-audit"
  s3_bucket_name                = aws_s3_bucket.logs.id
  include_global_service_events = true
  is_multi_region_trail         = true
  
  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }
}

添加标签追踪:

provider "aws" {
  default_tags {
    tags = {
      ManagedBy  = "Terraform"
      Pipeline   = "GitHub Actions"
      CommitSHA  = var.commit_sha
      DeployedBy = var.deployed_by
    }
  }
}

六、故障排查

6.1 常见问题

状态锁定:

# GitHub Actions
- name: Force Unlock (if needed)
  if: failure()
  run: terraform force-unlock -force $LOCK_ID

并发冲突:

# 使用 concurrency 控制
concurrency:
  group: terraform-${{ github.ref }}
  cancel-in-progress: false

6.2 调试技巧

启用详细日志:

env:
  TF_LOG: DEBUG
  TF_LOG_PATH: ./terraform.log

- name: Upload Logs
  if: always()
  uses: actions/upload-artifact@v3
  with:
    name: terraform-logs
    path: terraform.log

小结

CI/CD 集成的关键要点:

  • 自动化流程:Lint、Plan、Apply、Test
  • 环境隔离:开发、预发布、生产
  • 审批机制:人工审核生产变更
  • 安全实践:密钥管理、权限控制
  • 可观测性:日志、审计、监控

建立完善的 CI/CD 流程是 IaC 成功的关键!