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:
- 创建组织和工作区
- 连接 VCS(GitHub/GitLab)
- 配置工作区设置
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 成功的关键!