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-approvegitlib
示例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 划分
- validate
- plan
- apply 手动确认
- 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}
#===================================================================
最后更新于