Terraform 学习笔记:从基础概念到 Docker 发布实战
本文系统整理 Terraform 的核心概念、日常工作流、状态管理、模块化、多环境管理,以及 Docker 和 AWS 示例。示例用于学习与复习;执行任何 apply 或 destroy 前,请先核对账号、环境、计划范围和费用影响。
目录
- Terraform 的本质与工作原理
- 基本工作流程与命令
- 文件分工与配置块
- 变量、tfvars 与保存的执行计划
- State、资源地址与配置漂移
- count、for_each 与条件过滤
- Module、依赖与多环境管理
- moved 与生命周期保护
- Data Source、模板与 JSON
- Docker 容器管理实战
- 镜像构建、升级与回退
- 实战踩坑与排障
- 复习清单与实验目录
- 补充:本地 AWS EC2 示例
1. Terraform 的本质与工作原理
Terraform 用配置描述目标状态,通过 Provider 对比实际资源与受管理资源的记录,生成并执行变更计划。
例如:希望有一个 Nginx 容器、监听指定端口、加入指定网络,并限制内存。你描述这些要求,Terraform 根据依赖安排创建或修改操作。
配置:希望资源是什么样
+
State:配置地址与实际资源的对应记录
+
Provider 查询到的实际资源信息
↓
plan:计算新增、修改、替换、删除
↓
apply:通过 Provider 执行变更
↓
更新 State,并验证实际效果
三个关键点:
- Terraform 是声明式工具:主要描述目标,执行顺序由依赖关系决定。
- Provider 是 Terraform 与 Docker、AWS、文件系统等平台交互的插件。
- Terraform CLI 不会一直在后台修复资源;通常在运行命令时检查和执行变更。
学习的核心习惯:修改并保存配置 → 看懂 plan → 执行 apply → 验证实际效果。
2. 基本工作流程与命令
| 命令 | 作用 | 需要记住的边界 |
|---|---|---|
terraform init | 初始化工作目录、Backend、Provider 和模块 | 首次使用或依赖等配置变化后执行 |
terraform fmt | 统一 HCL 格式 | 格式正确不等于逻辑正确 |
terraform validate | 检查配置语法和内部一致性 | 不保证云端权限、配额和实际部署都成功 |
terraform plan | 预览变更 | 不执行计划中的资源变更,但可能读取平台数据 |
terraform apply | 生成并执行计划,或执行已保存计划 | 可能创建、修改、替换或删除资源 |
terraform output | 读取状态中保存的输出值 | 输出存在不等于服务当前可访问 |
terraform console | 交互式检查表达式 | 可用于检查变量、集合和计算结果 |
terraform state list | 列出当前状态中的资源地址 | 注意当前目录和 Workspace |
terraform state show 地址 | 查看某个资源的状态记录 | 需要再结合平台侧检查 |
terraform destroy | 计划并删除当前状态管理的资源 | 应检查删除范围后确认 |
日常操作模板:
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform output
terraform plan
最后一次 plan 用来检查是否仍有预期外差异。-no-color 可去掉终端颜色,便于阅读或保存输出。
常见计划标记:
| 标记 | 含义 |
|---|---|
+ | 创建 |
~ | 原地修改 |
- | 删除 |
-/+ | 先删除,再创建,即替换 |
+/- | 先创建,再删除,即另一种替换顺序 |
known after apply | 执行后才能确定的值 |
No changes | 本次计划没有检测到需要执行的变更 |
3. 文件分工与配置块
3.1 文件名是组织约定
| 文件 | 常见用途 |
|---|---|
main.tf | 资源、模块调用等主要配置 |
variables.tf | 输入变量、类型、默认值和校验 |
outputs.tf / output.tf | 输出定义,两种文件名都可以 |
terraform.tfvars | 当前根模块的具体变量值 |
*.tfvars | 其他变量文件,普通文件名需要通过 -var-file 指定 |
.terraform.lock.hcl | Provider 版本选择及校验信息,通常应提交 Git |
terraform.tfstate | 本地 State,可能包含敏感数据,不应提交 Git |
.terraform/ | 本地工作数据、下载的 Provider 和模块等 |
*.tfplan | 保存的二进制执行计划,可能包含敏感数据 |
同一个模块目录里的 .tf 文件会一起读取,main.tf 没有特殊优先级。
新增 release.tf 后,再执行 plan 会同时考虑该目录内其他 .tf 文件。子目录不会自动并入当前模块,需要通过模块调用等方式使用。
3.2 八种常见配置块
| 块 | 作用 | 典型引用 |
|---|---|---|
terraform | Terraform 版本、Provider 要求、Backend 等 | — |
provider | 平台连接配置,如区域、Docker 地址 | — |
resource | 创建和管理资源 | random_string.demo.result |
data | 读取已有对象或信息 | data.local_file.team.content |
variable | 定义外部输入 | var.environment |
locals | 定义内部计算值 | local.resource_name |
output | 暴露模块结果 | 根模块用 terraform output 查看 |
module | 调用可复用配置 | module.name.name |
本地随机字符串实验中的关系:
variable "string_length" {
type = number
default = 12
}
resource "random_string" "demo" {
length = var.string_length
special = false
}
output "result" {
value = random_string.demo.result
}
以上是核心片段;完整实验还包含 Provider 要求、其他变量和批量资源。
4. 变量、tfvars 与保存的执行计划
4.1 定义参数与填写参数
# variables.tf:定义允许输入什么
variable "environment" {
type = string
default = "dev"
}
# terraform.tfvars:填写本次使用的值
environment = "prod"
variables.tf 相当于参数接口;terraform.tfvars 给接口传值。locals 则适合内部计算:
locals {
resource_name = "${var.environment}-${random_string.demo.result}"
}
4.2 已练习的变量优先级
在此前练习涉及的输入方式中:
命令行 -var / -var-file > terraform.tfvars > variable 的 default
这是所学输入方式的简化比较,不是 Terraform 所有输入来源的完整列表。同一命令中,重复设置同一个变量时,后面的 -var / -var-file 会覆盖前面的。
terraform plan -var="string_length=20"
这个值用于本次命令,不会写回 .tf 或 .tfvars 文件。
普通命名的变量文件要显式指定:
terraform plan -var-file="experiment.tfvars"
4.3 类型和校验
学过的类型包括 string、number、bool、set(string)、map(object(...)) 和 object(...)。
当前随机字符串实验要求长度为 8~32 的整数:
validation {
condition = (
var.string_length >= 8 &&
var.string_length <= 32 &&
floor(var.string_length) == var.string_length
)
error_message = "字符串长度必须是 8 到 32 之间的整数。"
}
4.4 普通 plan 不会被下一次 apply 自动复用
terraform plan -var="string_length=20"
terraform apply
第二条命令重新计算计划,不会自动继承第一条临时传入的 20。
需要执行同一份计划时:
terraform plan -var="string_length=20" -out="lesson.tfplan"
terraform show -no-color lesson.tfplan
terraform apply lesson.tfplan
保存的计划包含计划时使用的变量值。执行已保存计划通常不会再询问 yes,所以应先检查计划。计划保存后再修改配置,并不会自动改变该计划中的内容。
5. State、资源地址与配置漂移
5.1 区分三种信息
| 信息 | 表达什么 |
|---|---|
| 配置 | 希望管理成什么样 |
| State | 哪个配置地址对应哪个实际对象,以及记录的属性 |
| 实际资源 | Docker、文件系统、云平台中真实存在的对象 |
例如 docker_container.nginx 是 Terraform 的资源地址;terraform-learning-nginx 是 Docker 容器名称。两者用途不同。
terraform state list
terraform state show docker_container.nginx
5.2 配置漂移
在 Terraform 外部修改受管理对象,可能产生漂移,例如手动修改生成文件,或修改容器参数。Terraform 能否发现、如何处理,取决于 Provider 读取哪些属性,以及配置是否管理相应字段。
Terraform CLI 不会因你保存了文件就立即执行变更,也不会持续巡检。对容器运行状态的持续管理来自 Docker 重启策略或 Kubernetes 等运行系统。
5.3 状态保护
- 不要把删除 State 当成清理实际资源;实际资源可能仍然存在。
- 不随意手工编辑 State。
- State 和保存的计划可能包含敏感信息。
- 团队协作时需要考虑远程状态、访问控制及状态锁;这些是后续进阶主题,本文不声称已完成相关实验。
6. count、for_each 与条件过滤
6.1 count:按数量创建
当前 JSON 文件实验使用:
count = var.json_config.enabled ? 1 : 0
启用时,资源地址带索引,例如 local_file.json_info[0]。从 1 改为 0,会计划删除已有受管理实例。
6.2 for_each:按键创建
随机字符串实验按环境名创建实例:
resource "random_string" "per_env" {
for_each = var.environments
length = var.string_length
special = false
}
例如地址为 random_string.per_env["dev"]。模块实验则使用环境名到配置对象的映射:
environments = {
dev = {
length = 10
enabled = true
}
prod = {
length = 12
enabled = true
}
}
6.3 过滤集合
locals {
enabled_environments = {
for name, config in var.environments :
name => config
if config.enabled
}
}
已有环境从集合中被过滤掉,会导致对应实例进入删除计划。enabled = false 在这个实现中代表不再保留该实例,不是暂停管理。
6.4 for 表达式与 for_each
for_each 创建多个资源或模块实例;for 表达式把一个集合转换成另一个值。例如汇总输出:
output "names" {
value = {
for env, instance in module.environment_names :
env => instance.name
}
}
7. Module、依赖与多环境管理
7.1 Module:输入、内部资源、输出
本地 naming 模块接收 prefix 和 length,创建随机后缀,再输出完整名称。
module "environment_names" {
source = "./modules/naming"
for_each = local.enabled_environments
prefix = each.key
length = each.value.length
}
同一模块可以调用多次,每个实例有自己的输入与受管理资源。根模块的 terraform.tfvars 不会自动给子模块注入变量,需要像上面一样显式传入。
7.2 引用建立依赖
image = docker_image.nginx.image_id
这里容器依赖镜像资源的输出,Terraform 会据此安排顺序。它并不按 .tf 文件的上下顺序逐行执行。显式 depends_on 用于无法通过普通引用表达的依赖,不必给每个资源都加。
7.3 Workspace 与独立目录
| 方式 | 作用 | 边界 |
|---|---|---|
| CLI Workspace | 同一配置使用不同 State | 不自动选择环境变量值,也不等于账号或权限隔离 |
| 独立环境目录 | 每个根模块维护自己的参数和状态位置 | 可复用同一模块;配置远程 Backend 时仍需确保状态地址不同 |
Workspace 常用命令:
terraform workspace list
terraform workspace show
terraform workspace new dev
terraform workspace select dev
切换 Workspace 后,仍然要检查当前变量和资源命名;不同 State 可以意外指向同一个外部对象,因此不能只靠 Workspace 名称判断隔离是否完整。
本地环境目录实验:
03-environments/
├─ modules/naming/
└─ envs/
├─ dev/
└─ prod/
分别在 dev、prod 中执行命令,两个根模块都通过 ../../modules/naming 复用命名模块。
8. moved 与生命周期保护
8.1 moved:告诉 Terraform 地址变了
改资源或模块的配置名称,可能使 Terraform 把旧地址视为删除、新地址视为新增。moved 声明地址之间的迁移关系。
当前模块实验包含连续迁移记录:
moved {
from = module.dev_name
to = module.names["dev"]
}
moved {
from = module.names
to = module.environment_names
}
这两条描述从最初单独调用到批量模块,再到模块改名的历史。moved 处理的是地址迁移,不会阻止因实际属性变化导致的资源替换。迁移后应检查计划中的创建、销毁数量。
单个资源增加 count 时,本地也练过:
moved {
from = local_file.json_info
to = local_file.json_info[0]
}
8.2 prevent_destroy
lifecycle {
prevent_destroy = true
}
它可在相关配置仍保留时,阻止 Terraform 执行删除或需要删除的替换计划;无法阻止外部手动删除。把整个资源块及保护配置从代码移除后,不能继续依赖这条保护。该片段属于此前学习内容,不代表当前每个实验资源都启用了它。
9. Data Source、模板与 JSON
9.1 resource 与 data
data "local_file" "team" {
filename = "${path.module}/operations-team.txt"
}
这里读取现有文件;resource "local_file" 则负责创建和管理输出文件。
9.2 templatefile
当前开发环境用模板生成说明文件:
content = templatefile(
"${path.module}/environment-info.tftpl",
{
environment = var.environment
team = trimspace(data.local_file.team.content)
name = module.name.name
}
)
模板内容:
Environment: ${environment}
Team: ${team}
Name: ${name}
Managed by: Terraform
9.3 jsonencode 与条件输出
content = jsonencode({
environment = var.environment
team = trimspace(data.local_file.team.content)
name = module.name.name
name_length = var.name_length
managed = true
})
用 jsonencode 生成 JSON,可以减少手工处理逗号、引号和转义的错误。
当资源可能不存在时,输出也需要处理禁用情况:
output "json_file_path" {
value = var.json_config.enabled ? local_file.json_info[0].filename : null
}
常用表达式速查:
| 写法 | 用途 |
|---|---|
path.module | 当前模块的目录位置 |
abspath(...) | 转换为绝对路径 |
trimspace(...) | 去掉字符串首尾空白 |
templatefile(...) | 渲染文本模板 |
jsonencode(...) | 把值编码为 JSON |
condition ? a : b | 条件选择 |
alltrue(...) | 检查一组布尔值是否全部为真 |
10. Docker 容器管理实战
实验目录:terraform-learning/04-docker。
当前代码配置 Windows Docker named pipe:
provider "docker" {
host = "npipe:////./pipe/docker_engine"
}
运行前需要 Docker 引擎可用。这份连接配置针对 Windows;迁移到其他系统时需要相应调整。
10.1 已覆盖的资源与行为
| 配置 | 作用 |
|---|---|
docker_image.nginx | 管理 Nginx 镜像 |
docker_container.nginx | 管理网页容器 |
ports | 把主机端口映射到容器 80 端口 |
volumes | 把主机网页目录只读挂载到容器 |
docker_network.learning | 创建网络,网页容器加入该网络 |
docker_volume.learning_data | 创建命名卷;当前文件没有把该卷挂载进网页容器 |
healthcheck | 检测容器内网页是否可访问 |
restart | 配置 Docker 的重启策略 |
| 内存与 CPU 参数 | 限制容器可使用的资源 |
10.2 端口与目录挂载
ports {
internal = 80
external = var.http_port
ip = "127.0.0.1"
}
volumes {
host_path = abspath("${path.module}/html")
container_path = "/usr/share/nginx/html"
read_only = true
}
当前 terraform.tfvars 设置 http_port = 8081,覆盖变量默认值 8080。只监听 127.0.0.1,访问入口是本机。
目录挂载会读取主机上已保存的文件。单纯修改挂载目录中的网页,一般不需要重新构建镜像;浏览器是否立即显示变化,还要考虑缓存。
10.3 健康检查与重启策略
restart = "unless-stopped"
healthcheck {
test = ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:80/ || exit 1"]
interval = "10s"
timeout = "3s"
start_period = "10s"
retries = 3
}
这里的 127.0.0.1:80 位于容器内部。要区分:
Up:容器主进程正在运行。healthy / unhealthy:健康检查的结果。- 重启策略:处理符合条件的容器退出。
普通 Docker 容器不会仅因为变成 unhealthy 就自动重启。 unless-stopped 会尊重主动停止行为;运行 docker stop 后,不应期待它马上自己启动。
10.4 内存与 CPU
memory = 128
memory_swap = 128
cpu_period = 100000
cpu_quota = 50000
本实验实际验证的内存上限为 134217728 字节,即 128 MiB。memory_swap 是内存加 swap 的总上限;两者相等时不额外分配 swap 使用空间。
CPU 配额计算:50000 / 100000 = 0.5,即相当于半核的计算时间上限,不代表绑定半个核心,也不代表持续消耗半核。
CPU 配额用完通常表现为限速;内存超限可能触发 OOM。具体实际行为仍需结合运行时与应用检查。
验证命令:
docker inspect --format '{{.HostConfig.Memory}}' terraform-learning-nginx
docker inspect --format 'Period={{.HostConfig.CpuPeriod}} Quota={{.HostConfig.CpuQuota}}' terraform-learning-nginx
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' terraform-learning-nginx
docker inspect --format '{{.State.Health.Status}}' terraform-learning-nginx
docker stats --no-stream terraform-learning-nginx
11. 镜像构建、升级与回退
11.1 目录挂载与镜像打包
| 方式 | 页面来源 | 修改网页后的流程 |
|---|---|---|
| Bind mount 目录挂载 | 主机目录中的文件 | 保存文件,刷新并检查缓存 |
| Dockerfile COPY | 构建时复制进镜像的文件 | 构建新镜像,用新镜像创建容器 |
此前使用的 Dockerfile:
FROM nginx:stable-alpine
COPY html/ /usr/share/nginx/html/
构建命令在 04-docker 目录执行:
docker build -t terraform-learning-web:v1 .
# 修改并保存网页后,再构建另一个版本
docker build -t terraform-learning-web:v2 .
最后的 . 是构建上下文。.dockerignore 用于限制发送的文件,避免把 State 等无关文件包含进去。
只构建新镜像,不会自动更新已运行的旧容器。实验中的 v1、v2 是人为约定的标签,需要保留对应旧镜像才能按该方式回退。
11.2 用 Terraform 选择版本
当前 release.tf 的核心配置:
variable "release_version" {
type = string
default = "v1"
}
data "docker_image" "release" {
name = "terraform-learning-web:${var.release_version}"
}
resource "docker_container" "release" {
name = "terraform-learning-release"
image = data.docker_image.release.id
ports {
internal = 80
external = 8084
ip = "127.0.0.1"
}
}
数据源读取已存在的本地镜像,不负责构建。因为 release.tf 与 main.tf 在同一目录,运行整个目录的计划前,就应确保所选版本镜像存在。
升级时在 terraform.tfvars 中设置:
release_version = "v2"
保存后执行 plan → apply,检查发布容器的替换是否符合预期。回退时改回 v1 并重复同样流程。实际页面来自容器中的镜像内容,不能只看变量或输出地址判断发布成功。
这种单容器替换可能有短暂中断;应用镜像回退也不会自动回退数据库数据。
11.3 实验中的地址
| 端口 | 实验用途 | 管理方式 |
|---|---|---|
| 8081 | 挂载 Windows 网页目录的 Nginx | Terraform |
| 8082 | 手动启动的镜像测试容器,曾用于 v1/v2 切换 | Docker 手动命令 |
| 8083 | 手动启动的 v2 对比容器 | Docker 手动命令 |
| 8084 | 通过 release_version 选择镜像的发布容器 | Terraform |
这些是历史实验端口,不保证当前所有端口都在运行。当前文件中的 release_version 为 v1,这只表明配置选择;实际运行版本仍需检查。
12. 实战踩坑与排障
| 现象 | 此前定位出的原因或判断方法 | 处理思路 |
|---|---|---|
| 修改后页面没变 | 文件未保存,或访问了使用旧镜像的另一个端口 | 先核对磁盘内容、端口和挂载来源 |
| Dockerfile 构建失败 | 当时磁盘上的 Dockerfile 是 0 字节 | 保存实际内容,再构建 |
| 新镜像构建成功,旧容器没更新 | 构建与替换运行容器是两个步骤 | 用新镜像创建或替换容器 |
| 手动停止容器后没有恢复 | 健康检查不负责重启;unless-stopped 尊重主动停止 | 区分进程状态、健康检查和重启策略 |
| 内存参数更新失败 | 当时已有容器更新需要同时处理 swap 总上限 | 检查 memory 与 memory_swap 的组合 |
| CPU 配置写入但限制仍为 0 | 历史实验中的 Docker Provider 3.9.0 更新路径未正确处理相应参数 | 当时经 Terraform 替换实验容器后,验证配额生效 |
No changes,效果仍不符合预期 | Provider 记录与实际效果可能存在偏差 | 用 Docker inspect、页面和日志交叉验证 |
| 改资源名出现删除与新增 | Terraform 地址发生变化 | 检查是否需要 moved |
| 关闭一个开关却出现删除 | count 变为 0 或 for_each 键被移除 | 先看删除计划,理解开关的实现语义 |
CPU 问题是特定实验和版本下的历史记录,不应推断所有 Provider 版本都会如此。相关历史源码:Docker Provider v3.9.0 容器实现。
网页不可访问时,按顺序检查:
Docker 引擎可用吗?
↓
容器运行了吗?
↓
端口映射和访问地址正确吗?
↓
日志里有没有请求和错误?
↓
网页文件和挂载路径正确吗?
↓
Nginx 配置有效吗?
docker version
docker ps -a --filter name=terraform-learning-nginx
docker logs --tail 20 terraform-learning-nginx
docker exec terraform-learning-nginx cat /usr/share/nginx/html/index.html
docker exec terraform-learning-nginx nginx -t
docker inspect --format '{{.Config.Image}}' terraform-learning-release
docker exec 要求目标容器处于可执行命令的运行状态。排查后需要长期保留的资源配置,应更新到 Terraform 配置中。
13. 复习清单与实验目录
13.1 十个自测问题
resource与data分别负责什么?variables.tf与terraform.tfvars有什么区别?plan -var=...后直接apply,变量会继承吗?- 保存的
.tfplan与普通plan有什么不同? - 配置、State、实际资源分别表示什么?
- 删除一个
for_each键,会对对应资源产生什么影响? - Module 为什么可以被多次调用?引用如何建立依赖?
- Workspace 能自动选择参数或提供权限隔离吗?
- 改名后如何通过
moved保持资源地址连续性? - 为什么
apply成功和No changes之后,还要验证实际服务?
13.2 本地实验位置
| 目录 | 学习内容 |
|---|---|
terraform-learning/01-random | 随机字符串、变量、校验、locals、for_each、输出 |
terraform-learning/02-modules | 子模块、环境对象、过滤、模块批量调用、moved |
terraform-learning/03-environments | 独立环境目录、Data Source、模板、JSON、条件资源 |
terraform-learning/04-docker | 镜像、容器、网络、挂载、健康检查、资源限制、发布回退 |
旧 README 中有早期练习的简化描述。例如第一课已从直接写 length = 8 演进为 length = var.string_length;Docker 项目也已增加 release.tf。复习和操作时以当前代码和本次计划为准。
建议按目录顺序复习,每一课都能够完成:解释输入 → 预测计划 → 检查实际结果 → 解释差异。
14. 补充:本地 AWS EC2 示例
另外发现已有目录:terraform-aws-ec2-demo。本节基于本地代码梳理,不表示此次创建或验证了 AWS 资源。
代码包含 VPC、公有子网、Internet Gateway、路由表及关联、安全组、EC2,以及读取 Amazon Linux 2023 AMI 的数据源。EC2 的启动脚本安装 Nginx。
默认参数:
aws_region = "ap-southeast-1"
instance_type = "t3.micro"
create_instance = false
这些资源使用 count = var.create_instance ? 1 : 0 控制。初次、空状态且开关为 false 时,不计划创建这些资源,但 AMI 数据查询仍需要 AWS 身份和 API 访问。如果状态中已经有资源,把开关设为 false 可能产生删除计划。
复习流程:
cd terraform-aws-ec2-demo
aws sts get-caller-identity
terraform init
terraform fmt -check
terraform validate
terraform plan
需要真正创建时,原示例采用保存计划后执行的方式:
terraform plan -var="create_instance=true" -out="demo.tfplan"
terraform show -no-color demo.tfplan
# 确认账号、区域及变更范围后再执行
terraform apply demo.tfplan
这会创建真实云资源,可能产生费用。清理应依据该目录的实际状态和销毁计划进行;本文没有执行这些命令,也没有检查云端现存资源。
参考资料
这些链接供继续查阅;本文主要归纳此前会话与本地实验内容,不作为所有最新版本行为的完整说明。