Resource 资源创建模式
资源的构造函数签名
Pulumi 中每个云资源都对应一个 SDK 类,通过 new(TypeScript)或直接调用(Python)来创建。构造函数通常接受三个参数:
// TypeScript 资源创建签名
// new ResourceType(name, args, opts?)
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.BucketV2(
"my-bucket", // name: Pulumi 内部名称(在 State 中标识资源)
{ // args: 资源属性
tags: {
Environment: "dev",
ManagedBy: "Pulumi",
},
},
{ // opts: 资源选项(可选)
protect: true,
}
);
# Python 资源创建
import pulumi_aws as aws
bucket = aws.s3.BucketV2(
"my-bucket", # 第一个参数:Pulumi 资源名
tags={ # 资源属性(通过关键字参数传递)
"Environment": "dev",
"ManagedBy": "Pulumi",
},
opts=pulumi.ResourceOptions( # 资源选项
protect=True
)
)
构造函数的第一个参数 name 是 Pulumi 在 State 中识别资源的唯一名称,修改它会导致资源被销毁并重建!它与云资源的实际名称(如 S3 Bucket 名)不同——云资源名由 args 中的属性控制(如 bucket="my-actual-name")。如果不指定,Pulumi 会在 Pulumi 资源名后附加随机后缀作为云资源名(实现名称唯一性)。
Output<T>:异步引用值
为什么需要 Output<T>?
当你创建一个 AWS EC2 实例时,它的公网 IP 在实例实际创建完成之前是未知的。Pulumi 用 Output<T> 类型来表示这类"部署后才能知道的值"。
Output<T> 就像 JavaScript 的 Promise<T>:它代表一个未来的值。你不能直接读取其内部的字符串,而必须用 apply() 方法来转换它。
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
const server = new aws.ec2.Instance("web-server", {
ami: "ami-0c02fb55956c7d316",
instanceType: "t3.micro",
});
// server.publicIp 的类型是 Output<string>
// 不能直接这样做(编译错误!):
// const url = "http://" + server.publicIp; // ❌
// 正确方式:用 apply() 转换 Output
const url: pulumi.Output<string> = server.publicIp.apply(
ip => `http://${ip}`
);
// 导出(在 pulumi stack output 中查看)
export const serverUrl = url;
import pulumi
import pulumi_aws as aws
server = aws.ec2.Instance(
"web-server",
ami="ami-0c02fb55956c7d316",
instance_type="t3.micro",
)
# server.public_ip 是 Output[str]
# 用 apply() 转换
url = server.public_ip.apply(lambda ip: f"http://{ip}")
# 导出
pulumi.export("server_url", url)
pulumi.concat():拼接包含 Output 的字符串
// TypeScript:使用 pulumi.interpolate 模板字符串
const connectionStr = pulumi.interpolate
`postgres://user:pass@${db.endpoint}:5432/mydb`;
// 或使用 pulumi.concat()
const url = pulumi.concat("https://", domain.name, "/api");
# Python:Output.concat() 或 apply()
conn_str = pulumi.Output.concat(
"postgres://user:pass@", db.endpoint, ":5432/mydb"
)
# 多个 Output 合并处理:pulumi.Output.all()
pulumi.Output.all(db.host, db.port, db.name).apply(
lambda args: f"postgres://user@{args[0]}:{args[1]}/{args[2]}"
)
资源选项(Resource Options)
资源选项通过 ResourceOptions(Python)或第三个参数对象(TypeScript)传递,控制资源的生命周期行为。
pulumi destroy 会失败,防止误删生产资源(如 RDS 数据库)。import pulumi
import pulumi_aws as aws
# 创建 VPC
vpc = aws.ec2.Vpc("main-vpc", cidr_block="10.0.0.0/16")
# 创建子网(父资源:vpc)
subnet = aws.ec2.Subnet(
"main-subnet",
vpc_id=vpc.id, # Output[str] — 自动推断依赖
cidr_block="10.0.1.0/24",
opts=pulumi.ResourceOptions(parent=vpc),
)
# 创建受保护的 RDS 数据库
db = aws.rds.Instance(
"prod-db",
instance_class="db.t3.micro",
engine="postgres",
allocated_storage=20,
username="admin",
password="secret123",
skip_final_snapshot=True,
opts=pulumi.ResourceOptions(
protect=True, # 防止意外删除
ignore_changes=["password"], # 忽略密码变更
depends_on=[subnet], # 显式依赖子网
),
)
ComponentResource:自定义组件
ComponentResource 让你将多个资源封装为一个逻辑单元,就像定义一个类一样。这是 Pulumi 最强大的抽象机制之一。
import pulumi
import pulumi_aws as aws
from typing import Optional
class StaticWebsite(pulumi.ComponentResource):
"""封装 S3 静态网站的 ComponentResource"""
def __init__(
self,
name: str,
index_document: str = "index.html",
opts: Optional[pulumi.ResourceOptions] = None
):
# 调用父类构造器,注册组件类型
super().__init__("mycompany:web:StaticWebsite", name, {}, opts)
# 子资源:opts 中传入 parent=self
child_opts = pulumi.ResourceOptions(parent=self)
self.bucket = aws.s3.BucketV2(
f"{name}-bucket",
opts=child_opts
)
self.website_config = aws.s3.BucketWebsiteConfigurationV2(
f"{name}-website",
bucket=self.bucket.id,
index_document=aws.s3.BucketWebsiteConfigurationV2IndexDocumentArgs(
suffix=index_document
),
opts=child_opts
)
# 注册输出(使组件的属性可被外部引用)
self.register_outputs({
"bucket_name": self.bucket.id,
"website_url": self.website_config.website_endpoint,
})
# 使用组件:像使用普通资源一样
site = StaticWebsite("my-site", index_document="index.html")
pulumi.export("website_url", site.website_config.website_endpoint)
Dynamic Provider:自定义资源类型
当 Pulumi 官方 Provider 不支持某个 API 时,可以用 Dynamic Provider 自定义资源类型,实现任意第三方 API 的 CRUD 操作。
import pulumi
from pulumi.dynamic import Resource, ResourceProvider, CreateResult
class GithubLabelProvider(ResourceProvider):
"""自定义 GitHub Issue Label 资源"""
def create(self, props):
# 调用 GitHub API 创建 Label
import requests
resp = requests.post(
f"https://api.github.com/repos/{props['repo']}/labels",
json={"name": props["name"], "color": props["color"]},
headers={"Authorization": f"token {props['token']}"}
)
label_id = resp.json()["id"]
return CreateResult(str(label_id), props)
def delete(self, resource_id, props):
import requests
requests.delete(
f"https://api.github.com/repos/{props['repo']}/labels/{props['name']}",
headers={"Authorization": f"token {props['token']}"}
)
class GithubLabel(Resource):
def __init__(self, name, props, opts=None):
super().__init__(GithubLabelProvider(), name, props, opts)
# 使用自定义资源
label = GithubLabel("bug-label", {
"repo": "myorg/myrepo",
"name": "bug",
"color": "d73a4a",
"token": "ghp_xxx",
})
实战:创建 AWS S3 存储桶(完整示例)
# __main__.py — 完整的 S3 Bucket 示例
import pulumi
import pulumi_aws as aws
import json
# 1. 创建 Bucket
bucket = aws.s3.BucketV2(
"app-assets",
tags={"Environment": "dev", "Team": "platform"},
)
# 2. 关闭公开访问屏蔽(允许静态网站访问)
public_access = aws.s3.BucketPublicAccessBlock(
"app-assets-pab",
bucket=bucket.id,
block_public_acls=False,
block_public_policy=False,
ignore_public_acls=False,
restrict_public_buckets=False,
opts=pulumi.ResourceOptions(depends_on=[bucket]),
)
# 3. 配置静态网站
website = aws.s3.BucketWebsiteConfigurationV2(
"app-assets-website",
bucket=bucket.id,
index_document=aws.s3.BucketWebsiteConfigurationV2IndexDocumentArgs(
suffix="index.html"
),
error_document=aws.s3.BucketWebsiteConfigurationV2ErrorDocumentArgs(
key="404.html"
),
)
# 4. 添加公开读取策略(apply() 变换 Output)
policy = aws.s3.BucketPolicy(
"app-assets-policy",
bucket=bucket.id,
policy=bucket.id.apply(
lambda bucket_name: json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": f"arn:aws:s3:::{bucket_name}/*",
}]
})
),
opts=pulumi.ResourceOptions(depends_on=[public_access]),
)
# 5. 导出输出值
pulumi.export("bucket_name", bucket.id)
pulumi.export("website_url", website.website_endpoint)
如果 apply() 的回调函数返回另一个 Output,Pulumi 会自动"展开"嵌套的 Output,你得到的仍然是一个扁平的 Output<T>,无需手动处理嵌套。这与 Promise 的行为类似(类似 flatMap)。
本章小结
- 资源构造函数三参数:name(Pulumi 内部名称)、args(资源属性)、opts(资源选项),修改 name 会导致资源重建。
- Output<T> 是核心类型:代表部署后才能知道的值(IP/ARN/ID等),不能直接读取,需通过
apply()变换。 - pulumi.Output.all() / pulumi.interpolate:合并多个 Output 的实用工具,构建包含多个动态值的字符串。
- 资源依赖自动推断:在 args 中引用另一个资源的 Output,Pulumi 自动建立依赖关系,确保正确创建顺序。
- protect + ignoreChanges:生产资源的两个重要保护选项,防止意外删除和不必要的变更。
- ComponentResource:封装多个子资源为一个逻辑组件,是 Pulumi 最重要的抽象复用机制。