基础设施即代码(IaC):Terraform入门、声明式配置管理、状态文件管理、模块化设计

好,咱们今天聊聊基础设施即代码。说白了,就是把以前靠鼠标点来点去的运维操作,变成写代码、存仓库、能回滚。我刚开始接触这个概念时,也觉得不就是自动化吗?后来踩了坑才明白,IaC 的核心不是自动化,而是「声明式」——你告诉系统最终要什么状态,它自己想办法去达成。

一、Terraform 入门:从零到第一个资源

Terraform 是目前最流行的 IaC 工具,没有之一。为什么?因为它多云、声明式、生态好。我个人习惯把 Terraform 当作「云资源的 Git」——你写代码描述资源,它帮你 diff、apply、destroy。

先看一个最简单的例子:在 AWS 上创建一个 EC2 实例。

# main.tf
provider "aws" {
  region = "cn-north-1"
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "HelloWorld"
  }
}

然后执行:

terraform init
terraform plan
terraform apply

嗯,这里要注意:terraform init 是必须的第一步,它会下载 provider 插件。我见过不少新手直接跳过 init 就 apply,结果报错一脸懵。

小技巧: 每次写 Terraform 代码前,先跑 terraform fmt 格式化代码。这能帮你避免很多缩进导致的语法错误。

二、声明式配置管理:你只管「要什么」,别管「怎么要」

声明式和命令式的区别,我举个例子你就明白了。

命令式(像 Ansible 的某些写法):

  • 先安装 Nginx
  • 再复制配置文件
  • 然后启动服务

声明式(Terraform 的写法):

  • 我要一个 Nginx 服务,监听 80 端口,配置文件内容如下...

你想想看,声明式的好处是什么?幂等性。不管跑多少次,结果都一样。我在项目中遇到过一个问题:用命令式脚本部署,某次网络中断导致安装了一半,脚本报错退出。下次再跑,它以为 Nginx 已经装好了,直接跳过安装步骤,结果配置文件没复制成功——服务挂了。换成 Terraform 后,每次 apply 都会重新检查当前状态和目标状态是否一致,不一致就修正。

核心原则: Terraform 只关心「最终状态」,不关心「中间过程」。你删掉一个资源,它不会问「你确定吗?」——它直接删。所以生产环境一定要加 prevent_destroylifecycle 块。

三、状态文件管理:Terraform 的「大脑」

状态文件(terraform.tfstate)是 Terraform 的核心。它记录了当前所有资源的状态。说白了,Terraform 就是靠这个文件知道「现在有什么」和「目标要什么」之间的差距。

状态文件有多重要?我举个例子:

有一次,我手贱在 AWS 控制台手动删了一个安全组。然后跑 terraform apply,Terraform 发现状态文件里记录着这个安全组,但实际已经没了。它尝试重新创建,但创建时发现依赖的 VPC ID 变了——结果整个环境崩了。嗯,从那以后,我坚决不允许任何人手动修改云资源。

状态文件管理有几个关键点:

问题 解决方案 我的建议
本地文件丢失 使用远程后端(S3、OSS、Consul) 团队协作必须用远程后端
多人同时操作 状态锁(DynamoDB、Consul) 不加锁会出大问题
状态文件泄露 加密存储、访问控制 状态文件里可能有密码
状态文件损坏 定期备份、使用 terraform state 命令修复 我每周自动备份一次
警告: 永远不要手动编辑 terraform.tfstate 文件!我曾经见过有人用 Vim 打开状态文件改了一个 IP 地址,结果 Terraform 认为整个资源需要重建,导致线上服务中断了 20 分钟。如果你需要修改状态,请使用 terraform state mvterraform state rm 等官方命令。

远程后端的配置示例:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "prod/network/terraform.tfstate"
    region         = "cn-north-1"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

配置好后,每次 terraform apply 都会自动从 S3 拉取最新状态,apply 完成后自动上传。多人协作时,DynamoDB 会加锁,防止两个人同时修改。

四、模块化设计:别把代码写成一坨

我刚开始用 Terraform 时,把所有资源都写在一个 main.tf 里。结果项目越来越大,一个文件上千行,找资源全靠 Ctrl+F。后来我学会了模块化——把可复用的资源封装成模块。

模块化设计的好处:

  • 复用性: 同样的 VPC 配置,开发、测试、生产环境直接引用
  • 可维护性: 每个模块只做一件事,改起来不担心影响其他地方
  • 版本控制: 模块可以打 tag,不同环境用不同版本

一个典型的模块结构:

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
  ecs/
    main.tf
    variables.tf
    outputs.tf
  rds/
    main.tf
    variables.tf
    outputs.tf

environments/
  dev/
    main.tf
    terraform.tfvars
  prod/
    main.tf
    terraform.tfvars

调用模块的方式:

# environments/dev/main.tf
module "vpc" {
  source = "../../modules/vpc"
  vpc_cidr = "10.0.0.0/16"
  env_name = "dev"
}

module "ecs" {
  source = "../../modules/ecs"
  vpc_id = module.vpc.vpc_id
  subnet_ids = module.vpc.public_subnet_ids
}
避坑指南: 我曾经把模块的 source 直接写成本地路径,结果团队其他人拉下来后路径不对。后来我改用 Git 仓库地址 + tag:source = "git::https://gitlab.com/terraform-modules/vpc.git?ref=v1.2.0"。这样所有人都能拿到同一份代码。

模块设计时,有几个原则要记住:

  1. 输入输出明确:variables.tf 定义输入,用 outputs.tf 定义输出。不要直接在模块里写死值。
  2. 单一职责: 一个模块只管理一类资源。VPC 模块只管网络,别在里面创建 EC2。
  3. 版本化: 模块发布到 Git 仓库后,打上语义化版本标签。生产环境锁定版本,开发环境可以用 latest。
  4. 文档化: 每个模块写 README,说明输入参数、输出值、使用示例。相信我,三个月后你自己都会忘记这个模块怎么用。

最后说一句:Terraform 的学习曲线其实不陡,但状态文件管理和模块化设计这两个点,是区分「会用」和「用好」的分水岭。我见过太多人把 Terraform 用成了「高级版 CloudFormation」——所有代码写在一个文件里,状态文件存在本地,出了问题全靠手动恢复。嗯,别这样干。

下一章,咱们聊聊 CI/CD 流水线怎么和 Terraform 集成,实现真正的「自动化运维」。