方案 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 划分
- validate
- plan
- apply 手动确认
- 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}
#===================================================================
|