Lesson 0008:IaC——服务器配置也能 commit

DevOps 四大支柱的第三根:Infrastructure as Code(基础设施即代码)

想想你现在的部署流程:手动 SSH 到服务器 → apt install → 改 Nginx 配置 → docker compose up。如果服务器挂了,你能在 5 分钟内重建吗?如果你管 10 台服务器,能保证配置一致吗?

IaC 的回答:把服务器、网络、防火墙、数据库——全部用代码定义。想改就改代码,想重建就跑命令,想回滚就 git revert。

Terraform:IaC 的事实标准

Terraform 是 HashiCorp 出品的 IaC 工具。工作方式:

你写 .tf 文件  ──→  terraform plan(预览变更)  ──→  terraform apply(执行变更)
(声明你想要什么)     (看看会发生什么)               (Do it)

核心概念:

概念是什么类比
Provider「驱动」——Terraform 通过它操作具体平台数据库驱动(mysql driver、pg driver)
Resource你要管理的一个东西(服务器、网络、域名…)一个 DOM 元素
StateTerraform 记录的「当前世界是什么样」React 的 Virtual DOM
Plan「代码」和「现实」的 diffgit diff
Apply把 diff 变成现实git commit
Terraform 是声明式的:你告诉它「我要什么」,它算出来「需要做什么」。而 Ansible 是过程式的:你告诉它「先做 A 再做 B」。两种方式各有适用场景,Terraform 偏基础设施创建,Ansible 偏服务器配置。

安装 Terraform

# Ubuntu
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y terraform
terraform version   # 验证安装

第一个 Terraform 项目:管理 Docker 容器

不用云账号。用 Docker provider 在本地练习——概念和操作完全一样。

mkdir /tmp/terraform-demo && cd /tmp/terraform-demo

# main.tf —— 主配置文件
cat > main.tf << 'EOF'
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}           # 连本机 Docker daemon

# 拉取 nginx 镜像
resource "docker_image" "nginx" {
  name         = "nginx:alpine"
  keep_locally = true           # 本地有就别再下载
}

# 创建 nginx 容器
resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tf-nginx"
  ports {
    internal = 80
    external = 8888             # 宿主机 8888 → 容器 80
  }
}

# 输出:部署后告诉我怎么访问
output "url" {
  value = "http://localhost:8888"
}
EOF

逐行解释:

运行——看魔法发生

cd /tmp/terraform-demo

# 1. 初始化(下载 provider,类似 npm install)
terraform init

# 2. 预览变更(不实际执行,看看会发生什么)
terraform plan
# 输出:+ create 表示要创建 2 个资源

# 3. 执行变更
terraform apply
# 输入 yes 确认

# 4. 验证
curl http://localhost:8888
# 看到 nginx 欢迎页!

docker ps --filter "name=tf-nginx"
# 看到 tf-nginx 容器在跑

# 5. 查看状态
terraform show     # 显示当前所有资源

# 6. 销毁一切
terraform destroy
# 输入 yes 确认后,容器和镜像全部删除

现在改一下配置,看看 Terraform 怎么处理变更:

# 把端口从 8888 改成 9999
sed -i 's/8888/9999/' main.tf

terraform plan
# 输出:-/+ destroy and then recreate(检测到端口变化,需要重建容器)

terraform apply -auto-approve
# 跳过确认直接执行
curl http://localhost:9999
# 还是 nginx 欢迎页!

变量和输出——让配置可复用

# variables.tf —— 声明变量
cat > variables.tf << 'EOF'
variable "app_port" {
  description = "External port for the app"
  type        = number
  default     = 8888
}

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}
EOF

# 在 main.tf 里用变量
sed -i 's/external = 9999/external = var.app_port/' main.tf
sed -i 's/name  = "tf-nginx"/name  = "tf-nginx-${var.environment}"/' main.tf

# 用不同参数运行
terraform apply -var="app_port=7777" -var="environment=prod" -auto-approve
# 容器名变成 tf-nginx-prod,端口 7777

State——Terraform 的记忆

Terraform 把「上次 apply 后世界长什么样」记录在 terraform.tfstate 文件里。这是它最核心的概念。

cat terraform.tfstate | python3 -m json.tool | head -20
# 看到所有资源的当前状态

团队协作时,state 文件不能存在本地——你同事的 state 跟你的不一样。解决方法:Remote State(存在 S3/GCS/Terraform Cloud 上),团队共享一份 state。

State 文件 = 敏感信息。里面可能包含数据库密码、API key。永远不要提交 terraform.tfstate 到 Git。用 .gitignore 排除它,用 remote state backend 管理。

真实场景:AWS EC2 + Security Group

换成云 provider,语法完全一样:

provider "aws" {
  region = "us-east-1"
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP and SSH"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]    # 允许所有人访问 80 端口
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]    # 允许所有人 SSH
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"            # 允许所有出站流量
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # Ubuntu 22.04
  instance_type = "t2.micro"               # 免费套餐
  security_groups = [aws_security_group.web.name]
  key_name      = "my-ssh-key"

  user_data = <<-EOF
    #!/bin/bash
    apt update && apt install -y nginx
    systemctl start nginx
  EOF

  tags = {
    Name = "web-server"
  }
}

output "public_ip" {
  value = aws_instance.web.public_ip
}

跟 Docker provider 完全一样的模式:声明 provider → 声明 resource → 引用 → output。换 provider 就是换一套 resource 类型名,核心逻辑不变。

动手练习:用 Terraform 管理 Nginx + 你的应用

cd /tmp && rm -rf tf-practice && mkdir tf-practice && cd tf-practice

cat > main.tf << 'EOF'
terraform {
  required_providers {
    docker = { source = "kreuzwerker/docker"; version = "~> 3.0" }
  }
}
provider "docker" {}

resource "docker_image" "nginx" {
  name = "nginx:alpine"
  keep_locally = true
}

resource "docker_container" "web" {
  image = docker_image.nginx.image_id
  name  = "tf-web"
  ports { internal = 80; external = 8080 }
}

output "access_url" {
  value = "http://localhost:8080"
}
EOF

terraform init
terraform apply -auto-approve
curl http://localhost:8080

# 你的任务:
# 1. 再加一个容器(如 redis:alpine),暴露端口 6379
# 2. 用 terraform plan 看变更预览
# 3. terraform apply 创建
# 4. terraform destroy 清理

小测验

Q1:Terraform 是声明式还是过程式?
Q2:terraform plan 做什么?
Q3:为什么 terraform.tfstate 不能提交到 Git?
Q4:Provider 在 Terraform 中的作用是什么?

推荐资源

下一步

最后一课:监控 & 可观测性——你的服务上线了,怎么知道它还活着?怎么在用户发现之前知道出问题了?Prometheus + Grafana,DevOps 四大支柱的最后一根。

terraform destroy 之后确认 docker ps 容器确实没了。如果还在,手动 docker rm -f tf-nginx。Terraform 管理的东西不要手动改——会造成 state 和现实不一致。
← Lesson 0007 · 术语表 · Mission