服务器配置为什么总在"漂移"
几乎每个站长都经历过这种事:半年前搭好的服务器,当时记了笔记,改了哪些配置也写在文档里。半年后再上去看,发现 Nginx 配置被人手动加过几行、防火墙规则多了一条不知道谁加的、某个 systemd 服务被临时改过又忘了改回来。文档和现实对不上,这就是所谓的"配置漂移"。等哪天真要重装系统或者迁移到新机器,你会发现根本复现不出当初的环境,只能对着老机器一点点抠,抠到凌晨。
Terraform 解决的正是这个问题。它的核心思想叫"基础设施即代码"(Infrastructure as Code,简称 IaC):把服务器的规格、网络、防火墙、DNS 记录、云资源这些本来要靠控制台点鼠标或者敲命令完成的操作,全部写成一份声明式的配置文件。你描述"我要什么",Terraform 负责算出"现在还差什么"并把它补齐。配置文件进 Git,谁改了什么一目了然,重建环境只需一条命令。
本文面向个人站长,从零讲清楚 Terraform 的核心概念,用一个真实可跑的例子(在 VPS 服务商上开一台机器并配好防火墙)走完全流程,再讲几个新手最容易踩的坑。目标不是让你变成 DevOps 工程师,而是让你有能力把自己的站"代码化",从此告别手工配置。
声明式 vs 命令式:思维方式的转变
大多数人一开始写的是命令式脚本,比如一段 Bash:登录服务器,执行 apt install nginx,然后 systemctl start nginx,接着改配置文件,再 systemctl reload。命令式的问题是,它记录的是"步骤",而不是"目标状态"。脚本跑第二遍时你得自己判断每步是否已经完成,稍微有偏差就出错。而且它不知道"当前系统离目标还差多少"。
Terraform 是声明式的。你只写:
resource "hcloud_server" "web" {
name = "web-01"
server_type = "cx22"
image = "debian-12"
location = "fsn1"
}它表达的是"我要一台叫 web-01 的服务器存在"。当你运行 terraform apply,Terraform 会先读取当前状态(state),再和配置文件对比,算出差异,最后只执行必要的操作。如果机器已经存在且规格一致,它就什么都不做。这种"收敛"的特性,让重复执行变得安全。
三个必须搞懂的核心概念
第一是 Provider。Terraform 本身不认识任何云平台,它通过 Provider 插件与外部 API 对话。比如 Hetzner Cloud 用 hetznercloud/hcloud,Cloudflare 用 cloudflare/cloudflare,AWS 用 hashicorp/aws。Provider 需要在配置里声明并指定版本:
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}注意版本号用了 ~> 约束符,表示允许 1.45 以上的小版本升级但不跨大版本,这能避免 Provider 突然破坏性变更把你的流程搞崩。生产环境一定要锁版本。
第二是 Resource,也就是你要管理的具体对象,一台机器、一条 DNS 记录、一个防火墙规则都是一个 resource。第三是 State,这是 Terraform 最关键也最容易被忽视的部分。State 是一个记录"配置文件里的每个资源对应现实中哪个真实对象"的账本,默认存在本地的 terraform.tfstate 文件里。它不是可有可无的缓存,而是 Terraform 做差异计算的基础。
变量、输出与目录结构
硬编码配置值是不好的习惯。用变量把敏感信息和环境差异抽出来:
variable "hcloud_token" {
type = string
sensitive = true
}
variable "ssh_key_name" {
type = string
default = "my-laptop"
}
output "server_ip" {
value = hcloud_server.web.ipv4_address
}变量可以在 terraform.tfvars 里赋值,敏感值比如 API Token 应该通过环境变量 TF_VAR_hcloud_token 传入,绝不提交到 Git。一个合理的目录结构长这样:
infra/
main.tf # 资源和 provider
variables.tf # 变量声明
outputs.tf # 输出
terraform.tfvars # 非敏感变量赋值
.gitignore # 忽略 tfstate 和 tfvars.gitignore 里至少要排除 *.tfstate、*.tfstate.backup、.terraform/ 和 terraform.tfvars。把 state 提交到 Git 是常见事故,因为里面可能含明文密钥和全部资源信息。
完整走一遍:开机器 + 配防火墙 + 挂 DNS
下面这个例子在 Hetzner Cloud 上开一台 Debian 12 的小机器,配好只放行 22/80/443 的防火墙,再把域名 A 记录指向它。先写防火墙资源:
resource "hcloud_firewall" "web_fw" {
name = "web-fw"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}然后开机器并绑定防火墙:
resource "hcloud_server" "web" {
name = "web-01"
server_type = "cx22"
image = "debian-12"
location = "fsn1"
ssh_keys = [var.ssh_key_name]
firewall_ids = [hcloud_firewall.web_fw.id]
user_data = <<-EOF
#cloud-config
package_update: true
packages:
- nginx
runcmd:
- systemctl enable --now nginx
EOF
}这里用了 user_data(cloud-init 脚本)在机器首次启动时自动装 Nginx,这是"基础设施即代码"里的初始化环节。注意 firewall_ids 直接引用了上面资源的 id,Terraform 会自动推导出"先建防火墙、再建机器"的依赖顺序,这就是隐式依赖,比手写 depends_on 更优雅。
最后接上 DNS:
resource "cloudflare_record" "www" {
zone_id = var.cf_zone_id
name = "www"
type = "A"
content = hcloud_server.web.ipv4_address
proxied = true
}运行流程是固定的三步。第一步 terraform init,下载 Provider 插件并初始化工作目录。第二步 terraform plan,它会打印一份"将要执行的操作"预览,绿色的 + 是新增,~ 是修改,红色的 - 是删除。每次 apply 前必须认真看 plan,这是 IaC 的安全带。第三步 terraform apply,确认后真正执行。
改配置、销毁与漂移检测
想扩一台机器?改改配置加一个 resource,再 plan 一次,你会看到它只新增这一台,不影响已有的。想删掉所有资源?terraform destroy 会按依赖关系逆序清理,先解绑 DNS 再删机器再删防火墙,不会留下悬空资源。这正是手工操作难以做到的整洁。
如果有人在控制台手工改了某条防火墙规则,terraform plan 会检测到并显示"要把规则改回来"。这既是优点——保证实际状态与代码一致——也提醒你一条纪律:配置的一切变更都走代码,不走控制台。否则你会在每次 plan 时看到一堆意料之外的差异,陷入混乱。
Remote State:多人协作与安全
本地 state 只适合单人单机。一旦多于一个人操作,或者你想在 CI 里跑,就必须用远程后端。以 S3 兼容存储(比如 Cloudflare R2 或 MinIO)为例:
terraform {
backend "s3" {
bucket = "tfstate"
key = "prod/terraform.tfstate"
region = "auto"
endpoints = { s3 = "https://xxx.r2.cloudflarestorage.com" }
skip_credentials_validation = true
skip_region_validation = true
}
}远程 state 的最大好处是支持锁(state locking),防止两个人同时 apply 导致状态损坏。绝大多数后端都自带锁机制,这是本地文件做不到的。同时要记得给存放 state 的桶加密,因为里面可能含敏感数据。
新手最容易踩的五个坑
第一,手改线上后不 import。如果机器是你手工开的,现在想纳入 Terraform 管理,必须用 terraform import hcloud_server.web 1234567 把它导入 state,否则 Terraform 会试图再开一台新的。第二,state 丢失。state 文件没了,Terraform 就失去了对现实资源的追踪,重建时会造成重复资源,所以远程 state 加自动备份是必备。
第三,把密钥写进配置文件。API Token、密码一律走环境变量或专用的密钥管理,代码仓库里的明文密钥迟早泄露。第四,不锁 Provider 版本。随便跑一次 terraform init -upgrade 可能就把 Provider 升到有破坏性变更的版本,导致 plan 出一堆莫名其妙的差异,务必用版本约束加 .terraform.lock.hcl 提交锁文件。
第五,把 Terraform 当配置管理工具用。Terraform 擅长管理"资源的生命周期"——创建、修改、销毁机器和网络,但它不适合做机器内部的软件配置细节,那是 Ansible、cloud-init 或者 Nix 的地盘。用 Terraform 的 user_data 做一次性初始化是合理的,但别指望它去管一个软件包里几十个配置文件。把职责分清楚,工具组合才好用:Terraform 管资源,Ansible 管配置,这是业界的经典搭配。
对个人站长的现实价值
你可能会想,我就一台 VPS,值得上 Terraform 吗?如果只有一台机器、从不折腾,确实没必要。但只要你符合下面任一条,它就值回票价:经常试用不同 VPS 服务商、需要快速在多地区布点、想把整套环境备份成可复现的代码、或者担心哪天服务器被删能快速重建。
实实在在的价值是"可恢复性"和"可复现性"。当你的整站基础设施都在一个 Git 仓库里,重装、扩容、迁移都从"熬夜手工操作"变成"改几行、跑一条命令、看着 plan 执行"。这不仅是效率,更是一种对意外的保险。哪怕最终你只把它用于管理 DNS 和防火墙,也比全部手工有据可查。基础设施即代码的真正门槛不在工具,而在于你是否愿意把"配置"当成"代码"来对待——版本化、评审、留痕、可回滚。养成这个习惯,服务器运维就从救火变成了工程。