云端部署

用 Terraform 管理 Linode

用 Terraform 管理 Linode:一份配置同时创建 staging 和 prod 两台服务器、上传 SSH 公钥、建一个只允许你的 IP 登录 SSH 的云防火墙,开机用 cloud-init 装好 nginx。改配置后 terraform plan 预览,terraform apply 执行,terraform destroy 一键全部删除。

BINBHO · 图文教程

用 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

项目资源

用途 地址
Linode Terraform Provider https://registry.terraform.io/providers/linode/linode/latest/docs
安装 Terraform https://developer.hashicorp.com/terraform/install
Metadata 服务 https://techdocs.akamai.com/cloud-computing/docs/overview-of-the-metadata-service
云防火墙 https://techdocs.akamai.com/cloud-computing/docs/cloud-firewall
价格 https://www.linode.com/pricing/

建议先把 envs 改成只有 staging,跑一次完整的 apply 和 destroy,确认流程和费用都清楚。

之后再加 prod,并把状态文件迁移到远程后端,方便多台电脑或多人协作。