使用 Terraform 管理云资源
文档用途
介绍 Terraform 这个最常用的 IaC 工具, 以及如何配合 GitLab CI/CD 使用.
Terraform 的简介 (ChatGPT)
Terraform 是一种基础设施即代码 (Infrastructure as Code, IaC) 工具, 它允许开发人员和运维团队通过编写代码来管理云基础设施资源. 使用 Terraform, 您可以定义基础设施的各种组件, 如虚拟机, 存储, 网络设置等, 以及它们之间的关系和配置. 一旦定义完成, Terraform 可以自动化地创建, 修改和销毁这些基础设施资源, 而无需手动进行操作.
Terraform 使用声明性语言来描述基础设施的状态和所需的最终状态, 然后根据这些描述执行操作. 它支持多种云服务提供商, 如 AWS, Azure, Google Cloud 等, 以及其他基础设施提供商和服务. 这使得 Terraform 成为跨多个云平台和环境进行基 础设施管理的强大工具.
参考链接
- Terraform - HashiCorp
- GitLab IaC - GitLab Docs
- Terraform explained in 15 mins - YouTube
- Terraform has forever changed the way I deploy code - YouTube
- Beginners Tutorial to Terraform with Azure - YouTube
主要优势
- 可以同时管理多个云服务商的资源, 方便复制或组合使用.
- 提供了方便人类阅读的配置语言, 可以通过写代码来配置云上基础设施.
- 提供了 state 用于追踪云资源的状态及其改动的历史.
- 可以使用熟悉的版本控制工具 (Git) 协作完成云资源的管理.
重要概念
Iac - Infrastructure as Code.
将基础设施 (云服务资源) 与代码 (配置文件) 对应.
这样一来, "云服务资源的管理" 就变成了 "对代码的编辑".
声明式 (declarative) 与 命令式 (imperative).
Terraform 选择了声明式, 这意味着 "直接描述结果".
而命令式则意味着 "描述一步步的过程".
工作原理
重要的前提: 各个云服务商都提供命令行工具, 用于操作云服务资源.
例如亚马逊云的 aws, Azure 的 azure-cli, Google Cloud 的 gcloud.
理论上, 只要有代码能正确执行这些云服务的命令行工具, 就能管理云服务资源.
Terraform 所做的事情就包括上面说的 "正确执行这些云服务命令行工具".
且由于 Terraform 采用了声明式的语法, 使其能对比 "云服务资源的当前状态" 与
"代码所描述的目标状态" 之间的差异. 从而判断 "需要依次执行哪些命令", 并自动执行完成.
INFO
Terraform 并不会直接调用 aws, az, gcloud 等命令, 而是模仿他们的行为.
毕竟 aws, az, gcloud 等命令行工具要想管理云服务资源, 都需要通过网络请求.
(很可能是基于 HTTP 协议的 RESTful API)
简单示例
前提条件
这里假设我们使用 Azure 作为云服务商, 需要拥有账号.
安装 terraform 命令行工具和 azure-cli:
scoop install terraform
scoop install azure-clibrew install terraform
brew install azure-cli执行 az login 命令, 在网页中登录 Azure , 这将会授权命令行工具.
WARNING
这里只是为了做个简单示例, 因此选择使用 Azure 主账户来操作.
在生产环境中, 不应该将主账号交给 terraform 使用, 而应该使用 service principle.
执行以下命令, 确保自己已经登录, 并且账户信息符合预期:
# 查看当前账户信息 (例如正在使用哪个 subscription)
az account show编写代码
然后找个临时的路径创建文件夹, 在其中创建 main.tf 文件, 并输入以下内容:
terraform {
required_version = ">= 1.7.2"
required_providers {
# 创建名为 azurerm 的 provide, 在后面会用到
azurerm = {
# 选择名为 hashicorp/azurerm 的 registry
# 详见 https://registry.terraform.io/providers/hashicorp/azurerm
source = "hashicorp/azurerm"
version = "~> 3.90"
}
}
}
provider "azurerm" {
# 此语句块必须存在, 内容可以为空
features {}
}
# 定义 resource group 这个资源
resource "azurerm_resource_group" "rg" {
# 名称为 myTFResourceGroup, 之后可以在 Azure 网页看到
name = "myTFResourceGroup"
location = "West Europe"
}TIP
若使用 VSCode 作为代码编辑器, 建议安装官方插件, 详见 下文.
执行命令
接着执行以下命令:
# 根据 main.tf 中的配置初始化.
terraform init
# 看看当前配置会对云服务资源造成什么影响.
terraform plan
# 按照配置创建云服务资源,
# 成功后可以去网页验证结果.
terraform apply
# 当测试结束后, 通过此命令移除所创建的资源.
terraform destroyTIP
Terraform 配合 Git 工作流以及 CI/CD 才能发挥更大作用.
使用 CI/CD 自动执行 terraform 命令相比手动执行更为常见.
编辑技巧
VSCode 插件
推荐安装 Terraform 官方插件.
此插件提供了语言服务器 (语法高亮, 错误提示, 自动补全).

WARNING
官方插件存在性能问题, 例如自动格式化代码时会短暂地卡一下 (大概 0.2 秒).
如果在意这个问题, 则 不要 为 Terraform 工程开启 "保存时自动格式化代码" (editor.formatOnSave).
可以在编辑完成后, git commit 前, 通过 terraform fmt 命令统一将代码格式化.
插件还不支持 "变量重命名". 若想要为变量改名, 就可能需要在多个地方手动修改.
因此在编写过程中, 取名 的时候请考虑清楚, 之后尽量不去修改它.
辅助编辑命令
Terraform 有一些子命令方便编辑:
# 将当前路径下的 *.tf 文件格式化
terraform fmt
# 检查是否存在配置错误
terraform validateAzure
权限管理
在生产环境中, 我们需要为 Terraform 提供恰当的权限.
而对于 Azure 这个云服务商, 我们应该使用 Service Principle.
简单地讲, Service Principle 是有着指定权限的 azure 子账户 (但不是用来给人类使用).
通过以下命令, 我们可以创建绑定了 Contributor 角色的 Service Principle:
几乎所有云服务商都采用 "基于角色的访问控制" (RBAC - Role-Based Access Control),
例如 Azure RBAC, 并且提供了很多 内置角色, 其中就包括了这里使用的 Contributor.
# 执行此命令, 并复制输出中的 "id".
az account show
# 创建 service principle 并绑定 "Contributor" 角色.
# 注意用前面复制的 "id" 替换这里的 <SUBSCRIPTION_ID>.
az ad sp create-for-rbac --role="Contributor" \
--scopes="/subscriptions/<SUBSCRIPTION_ID>"通过以上命令成功创建后, 会在命令行的输出中看到以下内容:
{
"appId": "xxx",
"displayName": "xxx",
"password": "xxx",
"tenant": "xxx"
}WARNING
不要 将以上信息写入代码中或被 Git Commit 历史记录. 恰当的做法举例:
若使用 GitHub, 则在仓库的 Settings / Secrets and variables 中添加 repository secrets.
若使用 GitLab, 则在仓库的 Settings / CICD / Variables 中添加 variables, 并勾选 masked.
这样一来, 就可以在 CI/CD 管线中, 将 Service Principle 的信息作为环境变量使用.
前面创建好了 Service Principle 并且绑定了角色,
如果想要验证, 可以通过一下命令查看, 在输出中应该能找到:
# 列出所有角色绑定信息
az role assignment list环境变量
要想让 CI/CD 操控 Azure 相关资源, 则需要提供以下环境变量:
ARM_CLIENT_ID: 对应前面的 "appId"ARM_CLIENT_SECRET: 对应前面的 "password"ARM_SUBSCRIPTION_ID: 对应前面复制的 SUBSCRIPTION_ID.ARM_TENANT_ID: 对应前面的 "tenant"
如果不为 CI/CD 提供以上环境变量, 则会在执行 terraform plan 时遇到以下错误:

GitLab
TF State
GitLab 与 Terraform 结合地很好, 若团队本就使用 GitLab 来托管远程仓库, 则推荐使用.
GitLab 提供了 Terraform http backend, 并围绕它提供了相关的页面. 好处在于:
- 有了唯一的 terraform state, 任意变更都基于这个状态, 这确保了 "一致性".
- 提供了 "锁", 一旦我们将状态锁住, 任何变更命令都无执行, 这能够避免误操作.
打开代码仓库的 Operate / Terraform states 页面:

然后点击页面中 Copy terraform init command 按钮:

为了执行弹窗中的命令, 需要提前创建 Access Token.
在代码仓库的 Settings / Access Tokens 页面中创建.
创建时应该选择 Maintainer 角色, 并勾选 api 权限.
之后记得将弹窗中命令的 username 设置为刚刚所创建的 Access Token 的名称.
并将弹窗命令中的 password 设置为 Access Token 的密码.
TIP
以上操作用于说明如何将 gitlab terraform state 初始化, 最好手动执行一遍确保没问题.
真实的项目中, 需要将这里的 terraform init 相关命令写入 CI/CD, 每次都要执行.
状态锁
在 CI/CD 中执行 terraform apply 成功至少一次后,
就能在 Operate / Terraform states 中看到 state 文件.

点击上图中右侧的按钮, 可以 Lock 或 Unlock.
上图中名为 default 的 terraform state 处于被锁住的状态.
在 Locked 状态下, 任何变更相关的 terraform 命令都无法成功执行.
如果执行, 则会遇到类似于下图的错误:

CI/CD
Terraform 配合 CI/CD 才是 "完全体", 实践中肯定要搭配起来用.
这里记录一些注意事项, 以及 GitLab CI/CD 的配置文件示例.
.gitlab-ci.yml
# .gitlab-ci.yml
default:
# 请恰当设置 tag, 用于筛选 docker 类型的 executor
tags: []
artifacts:
expire_in: "3 days"
image:
name: "hashicorp/terraform:1.7"
entrypoint: [""]
variables:
TF_STATE_NAME: default
# 请根据实际情况替换下面 url 中的域名, 例如自己搭建的 gitlab 域名肯定 **不是** gitlab.com
TF_ADDRESS: "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
stages:
- build
- deploy
- cleanup
init:
stage: .pre
script:
- echo $TF_ADDRESS
# TF_USERNAME 和 TF_PASSWORD 在代码仓库的 Settings / CICD / Variables 中被赋值.
# 它们分别对应 Access Token 的名称与密码.
- terraform init
-backend-config="address=${TF_ADDRESS}"
-backend-config="lock_address=${TF_ADDRESS}/lock"
-backend-config="unlock_address=${TF_ADDRESS}/lock"
-backend-config="username=${TF_USERNAME}"
-backend-config="password=${TF_PASSWORD}"
-backend-config="lock_method=POST"
-backend-config="unlock_method=DELETE"
-backend-config="retry_wait_min=5"
artifacts:
# 为了避免 artifacts 中的敏感信息泄露,
# 详见 https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
public: false
paths:
- ".terraform"
plan:
stage: build
script:
# 将结果输出到 plan.cache 文件中, 后面会用到
- terraform plan -out=plan.cache
artifacts:
public: false
paths:
- plan.cache
apply:
stage: deploy
rules:
- when: manual
allow_failure: true
script:
# 使用前面 terraform plan 所输出的 plan.cache 文件,
# 这可以确保 terraform apply 命令所做的操作与 plan 时一致.
- terraform apply -auto-approve plan.cache
destroy:
stage: cleanup
rules:
- when: manual
allow_failure: true
script:
# 通过 -auto-approve 选项避免在交互式命令行中输入 "yes"
- terraform apply -destroy -auto-approve注意事项:
- gitlab-runner 的 executor 应该选择 docker 类型, 可以通过
tags字段来筛选. - 需要提前创建 Access Token 并且在代码仓库的 Settings / CICD / Variables 中设置好.
- 不同的云服务提供商有着不同的环境变量需求, 例如 Azure 需要设置 这些环境变量.
Terraform 笔记
Output
默认情况下, terraform plan 等命令, 会将所有云服务资源的变更情况输出.
当云服务架构比较复杂时, 我们可能会希望只查看那些值得被关注的信息.
为此我们可以创建名为 outputs.tf 的文件, 在其中加入 output 语句块.
# outputs.tf
# 利用 output, 在命令行中输出值得关注的信息.
# 这在云服务资源较多的情况下挺有用, 可以屏蔽 "噪音".
output "resource_group_id" {
# 例如这里会输出 "名为 rg 的 azurerm_resource_group 类型资源的 id"
value = azurerm_resource_group.rg.id
}output 相关信息会被写入 state 文件, 并可以通过以下命令查看:
terraform outputINFO
如果 terraform 相关文件发生变动, 但是 "只涉及 output, 不涉及云服务资源",
那么在执行 terraform apply 之后, 则只会将新的 output 写入 state 文件, 不会影响云资源.
terraform output 有一些有趣的用法, 可以提供参数, 例如:
# 通过此命令获取 AKS 的资格证书, 之后可以借助 kubectl 操控集群.
# 需要先执行 terraform apply 命令, 使得 terraform output 命令能获取到正确的值.
az aks get-credentials \
--resource-group $(terraform output -raw resource_group_name) \
--name $(terraform output -raw kubernetes_cluster_name)# 将 my-dir/ 内的文件上传至 AWS S3 Bucket.
# 需要先执行 terraform apply 命令, 使得 terraform output 命令能获取到正确的值.
aws s3 cp my-dir/ s3://$(terraform output -raw website_bucket_name)/ --recursive对于 敏感数据, output 有专门的处理办法:
output "my_sensitive_token" {
value = module.my_module.token
sensitive = true
}对于那些被标记为 sensitive 的 output value, 默认不会被打印到命令行中.
如果需要查看他们的值, 需要提供变量的准确名字, 例如:
terraform output my_sensitive_tokenVariable
声明与使用
有些时候我们可能不希望硬编码, 此时可以使用 variable 语句块.
按照惯例, 建议新建 variables.tf 文件, 并在其中定义 variable.
# variables.tf
variable "resource_group_name" {
# 可以选择像这样提供默认值
default = "my-terraform-rg"
type = string
}在 main.tf 中的使用方式举例:
# main.tf
resource "azurerm_resource_group" "rg" {
name = "my-terraform-rg"
name = var.resource_group_name
# 也可以这么用
name = "${var.resource_group_name}-suffix"
location = "westeurope"
}为 Variable 赋值
在执行 terraform apply 命令时可以利用 -var 选项来赋值, 例如:
terraform apply -var "resource_group_name=my-tf-demo"当需要赋值的 variable 数量较多时, 更推荐使用 .tfvars 或 .tfvars.json 文件:
image_id = "ami-abc123"
availability_zone_names = [
"us-east-1a",
"us-west-1c",
]{
"image_id": "ami-abc123",
"availability_zone_names": ["us-west-1a", "us-west-1c"]
}执行 terraform 相关命令时, 位于工作路径中的 "特定名称" 的相关文件会被自动加载:
- 名称为
terraform.tfvars或terraform.tfvars.json的文件. - 名称以
.auto.tfvars或.auto.tfvars.json作为开头的文件.
当某个变量在不同的地方被多次赋值时, 优先级详见 Variable Definition Precedence.
TIP
用于给变量赋值的文件, 可能包含敏感信息, 因此建议在 .gitignore 中忽略这类文件.
Module
INFO
详见官方文档 Modules - Terraform Docs.
当配置较为复杂时, 建议拆分成多个 module. 这会让可维护性更好, 并且能一定程度上复用.
我们可以在工程根目录创建文件夹并在其中创建 .tf 文件, 这就是新的 module.
默认情况下, module 不会 自动加载, 但我们可以在 main.tf 中通过 module 语句块来加载:
module "my_module" {
source = "./module-dir"
}我们还可以使用其他开发者写好的 module (通常位于 registry.terraform.io).
例如 Azure/naming 这个 module, 可以帮助我们保持命名的一致性:
module "naming" {
source = "Azure/naming/azurerm"
suffix = [ "test" ]
}
resource "azurerm_resource_group" "example" {
name = module.naming.resource_group.name_unique
location = "West Europe"
}
resource "azurerm_kubernetes_cluster" "default" {
name = module.naming.kubernetes_cluster.name
# ...
}需要注意, 在代码中声明 module 语句块之后, 还需要使用 terraform get 命令初始化.
那些来自外部的 module, 被初始化之后会保存在 .terraform 文件夹内, 之后还可以用此命令更新.
那些写在工程内部的 module, 被初始化之后, 会直接建立路径层面的联系, 不再需要重复执行命令.
TIP
在 module 中应该声明 required_providers, 但不应该包含 provider 代码块.
建议将 provider 代码块写在工程根目录的 .tf 文件中, 例如 providers.tf.
# ./modules/my-module/main.tf
terraform {
# required_providers 写在 module 中
required_providers {
random = {
source = "hashicorp/random"
}
}
}# providers.tf
# provider 写在工程根目录的 .tf 文件中
provider "random" {}Troubleshoot
Checksum
在 CI/CD 中执行命令时, 可能会遇到这个问题:
xxx... does not match any of the checksums recorded in the dependency lock file这是因为我们在本地设备中执行 terraform init 命令后, .terraform.lock.hcl 文件中仅包含了当前操作系统 (例如 windows_amd64) 对应的可执行文件的 checksum 信息. 而在 CI/CD 中 (如果使用 docker), 对应的的操作系统很可能是 linux_amd64 或 linux_arm64.
为了解决这个问题, 只需要在本地执行以下命令并提交:
参考 stackoverflow 的回答.
# 请根据实际需求调整 -platform 的值
terraform providers lock \
-platform=windows_amd64 \
-platform=darwin_arm64 \
-platform=linux_arm64 \
-platform=linux_amd64State Locked
在调用 terraform apply 或 terraform destroy 命令时, 可能会遇到此问题:
Error message: HTTP remote state already locked这是因为: 使用了 http backend, 并且被锁住了.
解决方案: 只需要 解锁 即可正常执行命令.
Quota Exceeded
在执行 terraform apply 可能会看到这个错误:
这是因为云服务设置中的资源配额不够用了.
Original Error: Code="QuotaExceeded"解决方案: 在网页控制台申请提高相关资源的配额.