引言:为什么用 Terraform 管理 OBS?
如果你在华为云控制台上手动创建过 OBS 桶,你大概经历过这样的流程:点"创建桶"→ 填名字 → 选区域 → 选存储类别 → 点确定。然后又要手动配置权限策略、开启网站托管、上传文件、绑定域名……每一步都是点击和填表。
这样做有几个问题:
- 不可复现:下次要创建一个同样的桶,你得记住每一步点过什么
- 不可审计:三个月后你很难说清这个桶的权限策略到底是怎么配的
- 不可批量:要一次创建 10 个不同配置的桶?手动操作既慢又容易出错
- 不可销毁:手动创建的资源,清理时容易遗漏,留下"僵尸桶"持续计费
Terraform 解决的就是这些问题。你用声明式代码描述"我要什么样的基础设施",Terraform 负责创建、更新、销毁,并且全程可复现、可审计、可版本化。
本文带你走完一条完整链路:从零开始,用 Terraform 在华为云上创建一个 OBS 桶,配置公共读权限,启用静态网站托管,上传网站文件,最后绑定自定义域名——全部用代码完成,一次 terraform apply 搞定。
了解 OBS 桶的基本概念(对象存储、桶、权限),但不熟悉 Terraform 的同学。如果你连 OBS 是什么都不知道,建议先看一下华为云 OBS 产品文档。
前置准备
开始写代码之前,需要准备好两样东西:Terraform CLI 和华为云的访问凭证。
安装 Terraform
如果还没有安装 Terraform,macOS 上可以用 Homebrew:
brew install hashicorp/tap/terraform
Linux 上可以直接下载二进制:
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
验证安装:
terraform version
# Terraform v1.6.0
Provider 配置
华为云 Terraform Provider 需要以下信息来认证:
| 参数 | 说明 | 获取方式 |
|---|---|---|
access_key |
AK(访问密钥 ID) | 控制台 → 我的凭证 → 访问密钥 |
secret_key |
SK(秘密访问密钥) | 创建 AK 时一并生成,仅显示一次 |
region |
区域,如 cn-north-4 | 控制台右上角区域选择器 |
project_id |
项目 ID(部分区域需要) | 我的凭证 → 项目列表 |
AK/SK 是你华为云账号的访问凭证,泄露后他人可以操作你的云资源。建议通过环境变量传入,不要硬编码到 .tf 文件中,更不要提交到 Git 仓库。
通过环境变量传入凭证(推荐做法):
# 设置华为云凭证(当前终端会话有效)
export HW_ACCESS_KEY="你的AK"
export HW_SECRET_KEY="你的SK"
export HW_REGION="cn-north-4"
Provider 块写在 provider.tf 中:
terraform {
required_providers {
huaweicloud = {
source = "huaweicloud/huaweicloud"
version = ">= 1.50.0"
}
}
}
provider "huaweicloud" {
# AK/SK 通过环境变量传入,不硬编码
region = "cn-north-4"
}
这里有个概念要解释一下:required_providers 块告诉 Terraform "我要用华为云的 provider",source 指定了从哪里下载(Terraform Registry),version 指定了版本约束。>= 1.50.0 表示"1.50.0 或更高版本"。
Provider 是 Terraform 与云平台之间的"翻译官"。Terraform 本身不知道什么是 OBS 桶,它只知道如何调用 Provider 暴露的 API。华为云 Provider 把 Terraform 的声明式语法翻译成华为云 OpenAPI 调用。
Step 1创建 OBS 桶
先从最简单的开始——用 Terraform 创建一个 OBS 桶。
在 Terraform 中,一切资源都用 resource 块来声明。OBS 桶对应的资源类型是 huaweicloud_obs_bucket。来看代码:
resource "huaweicloud_obs_bucket" "website_bucket" {
bucket = "my-terraform-blog-demo"
storage_class = "STANDARD"
acl = "private"
}
逐行解释:
resource "huaweicloud_obs_bucket" "website_bucket"— 声明一个 OBS 桶资源,website_bucket是你在 Terraform 代码中引用它的名字(逻辑名),不是桶的真实名称bucket = "my-terraform-blog-demo"— 桶的真实名称,在 OBS 中全局唯一,创建后不可修改storage_class = "STANDARD"— 存储类别,STANDARD是标准存储,还有WARM(低频访问)和COLD(归档)acl = "private"— 桶的 ACL(访问控制列表),private表示只有桶拥有者能访问。后面我们会改成public-read来支持网站托管
现在运行一下看看效果:
# 初始化:下载 provider 插件
terraform init
# 预览:看看 Terraform 要做什么
terraform plan
# 执行:真正创建资源
terraform apply
terraform plan 的输出大致是:
Terraform will perform the following actions:
# huaweicloud_obs_bucket.website_bucket will be created
+ resource "huaweicloud_obs_bucket" "website_bucket" {
+ acl = "private"
+ bucket = "my-terraform-blog-demo"
+ id = (known after apply)
+ storage_class = "STANDARD"
}
Plan: 1 to add, 0 to change, 0 to destroy.
+ 号表示"将要创建"。确认无误后输入 yes,Terraform 就会调用华为云 API 创建这个桶。
plan 不会真正执行任何操作,它只是对比"代码描述的状态"和"云端实际状态"的差异,告诉你接下来会发生什么。养成"先 plan 再 apply"的习惯,能避免很多误操作。
Step 2设置桶访问权限
静态网站托管需要桶内的对象能被公网读取,所以我们要把 ACL 从 private 改为 public-read。
在 Terraform 中,修改资源属性非常简单——直接改代码,再 apply:
resource "huaweicloud_obs_bucket" "website_bucket" {
bucket = "my-terraform-blog-demo"
storage_class = "STANDARD"
acl = "public-read" # 改为公共读
}
再次 terraform apply,Terraform 会检测到 ACL 变了,只更新这一个属性,不会重建桶。
public-read 表示任何人都能读取桶内对象,但只有拥有者能写入。对于静态网站托管这是必须的,但如果你桶里有敏感文件,请用桶策略(bucket policy)做更细粒度的控制,而不是简单设为 public-read。
如果你需要更细粒度的权限控制,可以用 huaweicloud_obs_bucket_policy 资源:
resource "huaweicloud_obs_bucket_policy" "website_policy" {
bucket = huaweicloud_obs_bucket.website_bucket.id
policy = jsonencode({
Statement = [
{
Effect = "Allow"
Action = ["GetObject"]
Resource = ["arn:aws:s3:::my-terraform-blog-demo/*"]
Principal = { ID = "*" }
}
]
})
}
这样只有 GetObject(读取对象)被允许,其他操作(如列举桶内所有对象 ListBucket)仍然需要认证。
Step 3配置静态网站托管
OBS 桶可以配置为静态网站托管模式:指定一个索引页面(如 index.html)和错误页面(如 404.html),用户访问桶的网站端点时,OBS 会自动返回对应页面。
在 Terraform 中,用 huaweicloud_obs_bucket_website 资源来配置:
resource "huaweicloud_obs_bucket_website" "static_site" {
bucket = huaweicloud_obs_bucket.website_bucket.id
index_document = "index.html"
error_document = "404.html"
}
解释:
bucket = huaweicloud_obs_bucket.website_bucket.id— 这里用到了 Terraform 的引用语法。我们引用了 Step 1 创建的桶的 ID。这样 Terraform 知道这两个资源有依赖关系,会先创建桶再配置网站托管index_document— 当用户访问网站根路径时返回的页面error_document— 当用户访问不存在的路径时返回的页面
资源类型.逻辑名.属性 是 Terraform 中引用其他资源属性的标准写法。Terraform 会自动分析这些引用来构建依赖图,决定资源的创建顺序。你不需要手动指定"先创建 A 再创建 B"。
配置完成后,桶的网站端点地址格式为:
https://my-terraform-blog-demo.obs-website.cn-north-4.myhuaweicloud.com
Step 4上传网站内容
桶和网站托管都配好了,但桶里还是空的。我们需要上传 index.html 和 404.html。
在 Terraform 中,用 huaweicloud_obs_bucket_object 资源来上传文件:
resource "huaweicloud_obs_bucket_object" "index_page" {
bucket = huaweicloud_obs_bucket.website_bucket.id
key = "index.html"
content = templatefile("${path.module}/templates/index.html", {
title = "My Static Website"
date = formatdate("YYYY-MM-DD", timestamp())
})
content_type = "text/html"
acl = "public-read"
}
resource "huaweicloud_obs_bucket_object" "error_page" {
bucket = huaweicloud_obs_bucket.website_bucket.id
key = "404.html"
content = templatefile("${path.module}/templates/404.html", {})
content_type = "text/html"
acl = "public-read"
}
这里用到了 templatefile() 函数——它读取一个本地模板文件,用提供的变量做字符串替换后返回内容。这样你可以把 HTML 模板放在 templates/ 目录下,用 Terraform 变量动态填充。
path.module 是一个内置变量,表示当前 Terraform 模块所在的目录路径。
如果文件很大(如图片)
对于大文件,用 source 而不是 content:
resource "huaweicloud_obs_bucket_object" "logo" {
bucket = huaweicloud_obs_bucket.website_bucket.id
key = "assets/logo.png"
source = "${path.module}/assets/logo.png"
content_type = "image/png"
acl = "public-read"
}
Terraform 为每个 obs_bucket_object 资源在 state 文件中记录一条记录。如果你有几百个文件,state 文件会变得很大,plan 也会变慢。对于大量文件上传,建议用 obsutil(华为云 OBS 命令行工具)或 CI/CD 流水线,Terraform 只负责基础设施(桶、权限、托管配置)。
Step 5自定义域名与 DNS 解析
OBS 默认的网站端点地址很长很难记(xxx.obs-website.cn-north-4.myhuaweicloud.com)。通常我们会绑定自己的域名,比如 blog.example.com。
这需要两步:
1. 在 OBS 桶上绑定自定义域名
resource "huaweicloud_obs_bucket_custom_domain" "blog_domain" {
bucket = huaweicloud_obs_bucket.website_bucket.id
domain = "blog.example.com"
}
2. 在华为云 DNS 中添加 CNAME 记录
将 blog.example.com CNAME 到 OBS 网站端点:
# 先查询或创建 DNS 公网 Zone
data "huaweicloud_dns_zone" "example_zone" {
name = "example.com."
}
# 添加 CNAME 记录,指向 OBS 网站端点
resource "huaweicloud_dns_recordset" "blog_cname" {
zone_id = data.huaweicloud_dns_zone.example_zone.id
name = "blog.example.com."
type = "CNAME"
ttl = 300
records = [huaweicloud_obs_bucket.website_bucket.website_endpoint]
}
这里用到了 data 块——它表示"查询一个已存在的资源",而不是创建新的。我们假设 example.com 这个 DNS Zone 已经在华为云 DNS 服务中创建好了,只是查询它的 ID。
resource 是"我要创建/管理这个资源",data 是"这个资源已经存在了,我只是要读取它的信息"。用 data 查询的资源不会被 Terraform 销毁。
3. 配置 HTTPS 证书(可选但推荐)
如果你需要 HTTPS 访问,需要在 OBS 桶上绑定 SSL 证书。证书可以通过华为云 SCM(证书管理服务)上传:
resource "huaweicloud_scm_certificate" "blog_cert" {
name = "blog-example-com-cert"
certificate = file("${path.module}/certs/cert.pem")
private_key = file("${path.module}/certs/key.pem")
}
resource "huaweicloud_obs_bucket_custom_domain" "blog_domain_https" {
bucket = huaweicloud_obs_bucket.website_bucket.id
domain = "blog.example.com"
certificate_id = huaweicloud_scm_certificate.blog_cert.id
}
完整示例:一键部署
把前面所有步骤合在一起,完整的文件结构如下:
obs-static-website/
├── provider.tf # Provider 配置
├── variables.tf # 变量定义
├── main.tf # 主资源定义
├── outputs.tf # 输出值
├── templates/
│ ├── index.html # 网站首页模板
│ └── 404.html # 404 页面模板
└── certs/
├── cert.pem # SSL 证书
└── key.pem # 私钥
variables.tf — 把可变参数抽出来:
variable "bucket_name" {
description = "OBS 桶名称(全局唯一)"
type = string
}
variable "custom_domain" {
description = "自定义域名"
type = string
default = null
}
variable "dns_zone_name" {
description = "DNS Zone 名称"
type = string
default = null
}
main.tf — 完整资源定义:
# ===== OBS 桶 =====
resource "huaweicloud_obs_bucket" "website" {
bucket = var.bucket_name
storage_class = "STANDARD"
acl = "public-read"
}
# ===== 静态网站托管 =====
resource "huaweicloud_obs_bucket_website" "config" {
bucket = huaweicloud_obs_bucket.website.id
index_document = "index.html"
error_document = "404.html"
}
# ===== 上传首页 =====
resource "huaweicloud_obs_bucket_object" "index" {
bucket = huaweicloud_obs_bucket.website.id
key = "index.html"
content = file("${path.module}/templates/index.html")
content_type = "text/html"
acl = "public-read"
}
# ===== 上传 404 页面 =====
resource "huaweicloud_obs_bucket_object" "error" {
bucket = huaweicloud_obs_bucket.website.id
key = "404.html"
content = file("${path.module}/templates/404.html")
content_type = "text/html"
acl = "public-read"
}
# ===== 自定义域名 + DNS(可选)=====
resource "huaweicloud_obs_bucket_custom_domain" "custom" {
count = var.custom_domain != null ? 1 : 0
bucket = huaweicloud_obs_bucket.website.id
domain = var.custom_domain
}
data "huaweicloud_dns_zone" "zone" {
count = var.dns_zone_name != null ? 1 : 0
name = var.dns_zone_name
}
resource "huaweicloud_dns_recordset" "cname" {
count = var.custom_domain != null ? 1 : 0
zone_id = data.huaweicloud_dns_zone.zone[0].id
name = "${var.custom_domain}."
type = "CNAME"
ttl = 300
records = [huaweicloud_obs_bucket.website.website_endpoint]
}
outputs.tf — 输出有用的信息:
output "website_endpoint" {
description = "OBS 网站访问端点"
value = huaweicloud_obs_bucket.website.website_endpoint
}
output "bucket_name" {
description = "OBS 桶名称"
value = huaweicloud_obs_bucket.website.bucket
}
部署命令:
terraform init
terraform plan -var "bucket_name=my-blog-2026" \
-var "custom_domain=blog.example.com" \
-var "dns_zone_name=example.com."
terraform apply -var "bucket_name=my-blog-2026" \
-var "custom_domain=blog.example.com" \
-var "dns_zone_name=example.com."
apply 完成后,Terraform 会输出网站端点地址,直接在浏览器打开就能看到你的静态网站了。
生命周期管理:plan / apply / destroy
Terraform 的三个核心命令构成了基础设施的完整生命周期:
| 命令 | 作用 | 会改变云资源吗 |
|---|---|---|
terraform plan |
预览变更,生成执行计划 | ❌ 不会 |
terraform apply |
执行变更,创建/更新/删除资源 | ✅ 会 |
terraform destroy |
销毁所有 Terraform 管理的资源 | ✅ 会(不可逆) |
更新资源
修改 main.tf 中的任何属性,再 apply 即可。Terraform 会自动判断哪些资源需要更新:
# 比如把存储类别改为低频访问
# 修改 main.tf: storage_class = "WARM"
terraform plan
# ~> storage_class: "STANDARD" -> "WARM"
# Plan: 0 to add, 1 to change, 0 to destroy.
terraform apply
~> 符号表示"将要修改"。
销毁资源
terraform destroy
terraform destroy 会删除所有由这些 .tf 文件管理的云资源。如果桶里有重要数据,请先备份。OBS 桶在销毁前需要先清空桶内所有对象,否则会报错——这是一种保护机制。
State 文件
Terraform 在执行后会在当前目录生成一个 terraform.tfstate 文件,记录了所有资源的当前状态。这个文件是 Terraform 管理资源的依据——plan 时就是对比 .tf 文件(期望状态)和 .tfstate(实际状态)的差异。
State 文件可能包含敏感信息(如密码、密钥)。不要提交到 Git,建议加入 .gitignore。团队协作时建议使用远程 backend(如 OBS 桶)存储 state。
常见问题与排坑
1. 桶名已存在
Error: BucketAlreadyExists: The requested bucket name is not available.
OBS 桶名全局唯一(不是你的账号内唯一,是整个华为云唯一)。换个名字就行。建议在桶名中加入项目前缀或随机后缀。
2. 桶不为空无法销毁
Error: BucketNotEmpty: The bucket you tried to delete is not empty.
OBS 要求桶必须清空才能删除。解决方法:先用 obsutil 或控制台清空桶内对象,再 terraform destroy。或者在 Terraform 中添加 force_destroy = true:
resource "huaweicloud_obs_bucket" "website" {
bucket = var.bucket_name
storage_class = "STANDARD"
acl = "public-read"
force_destroy = true # destroy 时自动清空桶内对象
}
3. Provider 版本不兼容
如果遇到 "Unsupported argument" 之类的错误,通常是 Provider 版本太低。检查 provider.tf 中的 version 约束,适当升级。
4. 网站访问 403
检查以下几点:
- 桶 ACL 是否设为
public-read - 对象的 ACL 是否设为
public-read(桶 ACL 和对象 ACL 是独立的) - 是否通过网站端点访问(
.obs-website.),而不是 REST 端点(.obs.)
总结
回顾一下本文的完整链路:
| 步骤 | Terraform 资源 | 做了什么 |
|---|---|---|
| Step 1 | huaweicloud_obs_bucket |
创建 OBS 桶 |
| Step 2 | ACL = public-read |
设置公共读权限 |
| Step 3 | huaweicloud_obs_bucket_website |
配置静态网站托管 |
| Step 4 | huaweicloud_obs_bucket_object |
上传 index.html / 404.html |
| Step 5 | huaweicloud_dns_recordset |
绑定自定义域名 + DNS 解析 |
用 Terraform 管理基础设施的核心价值:
- 一份代码就是一份文档——所有配置都在 .tf 文件中,不需要去控制台翻找
- 一键复现——
terraform apply就能重建整套环境,适合多环境部署(dev/staging/prod) - 一键清理——
terraform destroy不会遗漏,不会有"僵尸资源"持续计费 - 版本控制——.tf 文件用 Git 管理,每次基础设施变更都有 commit 记录,可追溯、可回滚
下一步,你可以尝试:
- 把这套配置封装成 Terraform Module,在多个项目中复用
- 接入 CI/CD 流水线,实现
git push自动部署 - 用 Terraform 管理更复杂的华为云资源(ECS、VPC、RDS、ELB…)
希望这篇博客能帮你迈出 IaC 的第一步。代码即基础设施,从此告别手动点击控制台。
评论(0)