用 Terraform 管理 Linode:一份配置同时创建 staging 和 prod 两台服务器、上传 SSH 公钥、建一个只允许你的 IP 登录 SSH 的云防火墙,开机用 cloud-init 装好 nginx。改配置后
terraform plan预览,terraform apply执行,terraform destroy一键全部删除。
先看清成品
成品是一个 Terraform 项目。本文已用 Terraform 1.16.6 和 Linode Provider 4.8.0 实际执行:
terraform fmt -check通过,terraform init成功,terraform validate显示Success! The configuration is valid.;terraform plan输出Plan: 4 to add, 0 to change, 0 to destroy.(防火墙、两台实例、SSH 密钥);admin_cidr填错时会显示自定义中文错误。plan 用的是假令牌,没有连接真实账号,apply没有执行。
注意:Linode(现属 Akamai Cloud)按小时计费,关机也照常收费,不用的实例要删除才停止计费。官方价格页的例子是 1 GB 实例每小时 0.0075 美元,具体价格以下单页面为准。注册需要绑定信用卡。云服务器只用于合法用途,遵守 Akamai 的可接受使用政策和所在地法律。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| 两个环境 | for_each 按 envs 变量创建 |
| SSH 密钥 | 统一上传,实例自动带上 |
| 云防火墙 | SSH 只放行你的 IP,网页对所有人开放 |
| 自动装 nginx | 首页显示环境名 |
| 输出 IP | terraform output ips |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Terraform | 1.6 或更高 | 本文使用 1.16.6 |
| Linode 令牌 | Cloud Manager → API Tokens | 给 Linodes、Firewalls 读写权限 |
| SSH 公钥 | ~/.ssh/id_ed25519.pub |
|
| 你的公网 IP | 写成 x.x.x.x/32 |
第一步:怎样安装 Terraform?
按 HashiCorp 官方安装页面操作。macOS:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version
第二步:代码怎么写?
1. 项目结构
linode-tf/
├── versions.tf
├── variables.tf
├── main.tf
├── outputs.tf
├── cloud-init.yaml.tftpl
├── terraform.tfvars.example
└── .gitignore
2. versions.tf
terraform {
required_version = ">= 1.6"
required_providers {
linode = {
source = "linode/linode"
version = "~> 4.8"
}
}
}
provider "linode" {
# 令牌从环境变量 LINODE_TOKEN 读取,不要写进文件
}
3. variables.tf
variable "region" {
type = string
default = "us-sea"
}
variable "envs" {
description = "要创建的环境,键是环境名"
type = map(object({
type = string
}))
default = {
staging = { type = "g6-nanode-1" }
prod = { type = "g6-standard-1" }
}
}
variable "ssh_pubkey_path" {
type = string
default = "~/.ssh/id_ed25519.pub"
}
variable "admin_cidr" {
description = "允许 SSH 的地址,例如你的公网 IP/32"
type = string
validation {
condition = can(cidrhost(var.admin_cidr, 0))
error_message = "admin_cidr 必须是 CIDR,例如 203.0.113.10/32。"
}
}
4. main.tf
resource "linode_sshkey" "me" {
label = "terraform-key"
ssh_key = chomp(file(pathexpand(var.ssh_pubkey_path)))
}
resource "linode_instance" "web" {
for_each = var.envs
label = "web-${each.key}"
region = var.region
type = each.value.type
image = "linode/ubuntu24.04"
authorized_keys = [linode_sshkey.me.ssh_key]
tags = ["terraform", each.key]
metadata {
user_data = base64encode(templatefile("${path.module}/cloud-init.yaml.tftpl", {
env = each.key
}))
}
}
resource "linode_firewall" "web" {
label = "web-fw"
inbound_policy = "DROP"
outbound_policy = "ACCEPT"
inbound {
label = "ssh-admin"
action = "ACCEPT"
protocol = "TCP"
ports = "22"
ipv4 = [var.admin_cidr]
}
inbound {
label = "web"
action = "ACCEPT"
protocol = "TCP"
ports = "80,443"
ipv4 = ["0.0.0.0/0"]
ipv6 = ["::/0"]
}
linodes = [for i in linode_instance.web : i.id]
}
| 要点 | 说明 |
|---|---|
for_each = var.envs |
加一个环境只需在变量里加一行 |
metadata.user_data |
需要 Base64,用 base64encode |
templatefile |
把环境名传进 cloud-init |
linodes = [...] |
防火墙自动绑定所有实例 |
5. outputs.tf、cloud-init 模板
output "ips" {
value = { for k, i in linode_instance.web : k => i.ip_address }
}
#cloud-config
package_update: true
packages: [nginx]
write_files:
- path: /var/www/html/index.html
content: |
<h1>${env} 环境</h1>
runcmd:
- systemctl enable --now nginx
6. .gitignore
.terraform/
*.tfstate*
terraform.tfvars
注意:状态文件
terraform.tfstate里有资源的完整信息,不要提交到 Git。
第三步:怎样执行?
1. 初始化和检查
export LINODE_TOKEN=你的令牌
cp terraform.tfvars.example terraform.tfvars # 改成你的 IP
terraform init
terraform validate
2. 预览
terraform plan
期望看到:
# linode_firewall.web will be created
# linode_instance.web["prod"] will be created
# linode_instance.web["staging"] will be created
# linode_sshkey.me will be created
Plan: 4 to add, 0 to change, 0 to destroy.
3. 创建
terraform apply
terraform output ips
几分钟后用浏览器打开两个 IP,应分别显示“staging 环境”和“prod 环境”。
4. 只保留 staging
把 variables.tf 里 prod 那行删掉,再 terraform plan,会显示要删除一台实例。确认后 apply。
5. 全部删除
terraform destroy
出问题时该怎么调?
1. admin_cidr 报错
本文实际输出:
admin_cidr 必须是 CIDR,例如 203.0.113.10/32。
记得在 IP 后面加 /32。
2. 401 或 Unauthorized
令牌没设置、过期,或者权限不够(缺少 Firewalls 读写)。
3. 改了 cloud-init 但服务器没变化
cloud-init 只在首次开机执行。修改后需要重建实例:
terraform apply -replace='linode_instance.web["staging"]'
常见问题怎么处理?
| 现象 | 原因 | 解决方法 |
|---|---|---|
no file exists at |
公钥路径不对 | 改 ssh_pubkey_path |
| 地区不支持 Metadata | 部分地区没有该能力 | linode-cli regions list 确认 |
| 套餐名错误 | 类型 ID 写错 | linode-cli linodes types --text --format id |
| 多人协作状态冲突 | 状态文件在本地 | 使用远程后端 |
| 在 Cloud Manager 手动改了 | 状态漂移 | terraform plan 会显示差异 |
| 家里 IP 变了连不上 SSH | 防火墙只放行旧 IP | 改 admin_cidr 后 apply |
项目资源
建议先把 envs 改成只有 staging,跑一次完整的 apply 和 destroy,确认流程和费用都清楚。
之后再加 prod,并把状态文件迁移到远程后端,方便多台电脑或多人协作。