一、从一个别扭的现状说起
做开发的朋友,手头多半会有几套环境。比如拿来练手的开发环境、给客户看的演示环境、上线后跑真实流水的生产环境。这几套环境,名字不一样,配置也不一样:开发环境用最小的云服务器,能跑就行;生产环境得用大规格的机器,还要开高可用存储,日志也必须得留。
以前我是怎么处理的呢?要么直接登录云平台,对着控制台鼠标点来点去,一个参数一个参数地改;要么把复制出来的配置文件都塞进项目里,比如 dev-config.json、prod-config.json,后来发现这些文件根本没法保证同步。今天我改了一处,明天忘了改另一处,等上了生产才发现不对劲,那叫一个头疼。
后来我换了个思路:把环境里的“可变点”全部抽出来,当成参数来管理。环境还是那么几个,但基础设置不再是硬编码在代码里的一堆数字,而是变成了一份份可以切换的配置。谁参数化得早,谁下班就早。
二、Pulumi:让你用写代码的方式管云资源
要把参数化落地,你需要一个称手的工具。这里说说 Pulumi。简单讲,Pulumi 是一种“基础设施即代码”工具,英文缩写 IaC。啥意思呢?就是你用 TypeScript、Python、Go 这类常规编程语言,把“我要一台云服务器”“我要一个存储桶”这些需求写出来,然后交给 Pulumi 去云平台上创建。
有人会问,这跟 Terraform 有什么区别?Terraform 也有自己的语法模板,其实就是 HCL,你写代码还得学它那一套,写起来有点像 JSON 和 YAML 的混合体。Pulumi 不一样,它直接复用你熟悉的编程语言,写起来就像在拼积木。更关键的是,Pulumi 把“配置”这个概念做得很顺滑,天生适合拿来处理多环境问题。
在 Pulumi 的世界里,有几个名词你至少要混个脸熟:项目(Project)对应一块基础设施的代码;堆栈(Stack)对应一套运行环境和它自己的状态。比如我可以建一个叫 dev 的堆栈,再建一个叫 prod 的堆栈。同一个项目代码,放在不同堆栈里,就会根据各自的配置文件生成不同的资源。这里的配置,就是我们搞参数化的主战场。
三、多环境参数化的核心思路
我们常常说“多环境”,听起来很高大上,其实本质就是:同一份应用代码,在不同场景下有不同表现。比如开发环境接口可以打日志,生产环境就不允许把敏感信息泄露到日志里。基础设施也一样:开发环境可以开一台小机器,生产环境需要五台大机器。
3.1 环境和配置的关系
把环境想象成一间屋子,配置就是屋子里的调光旋钮。屋子还是那间屋子,但不同时间你想要的亮度不一样。所以你不需要建很多间屋子,只需要根据场景把旋钮调到不同位置。
Pulumi 的堆栈就是这个旋钮。你可以给 dev 堆栈设置一套旋钮值,给 prod 堆栈设置另一套。旋钮值写在哪?写在堆栈自己的配置文件里。代码本身保持干净,不掺任何环境相关的硬编码。
3.2 把环境差异变成“变量”
有些同学初次接触时容易陷入一个误区:为每个环境复制出一份完整的代码。其实正确做法是,把环境之间的差异找出来,定义成变量。比如:
- 服务器实例规格(开发用小规格,生产用大规格)
- 存储桶名前缀(带上环境名,避免冲突)
- 是否需要开启详细监控
- 安全组放行的 IP 段
- 版本控制是否开启
这些变量在代码里就是一个个普通的变量,只不过它们的值不是写在代码里,而是从配置中读取。这样一来,同一个代码仓库,不管切换哪个环境,都是同一套逻辑,只是参数值变了。
四、动手把配置玩起来
光说不练假把式。下面我用一个具体的例子,带你把刚才这套思路跑通。咱们统一使用 TypeScript 语言,云平台用 AWS。
4.1 准备项目结构
先创建一个新的目录,并初始化 Pulumi 项目:
# 创建一个新目录
mkdir pulumi-multi-env
cd pulumi-multi-env
# 初始化一个 TypeScript 项目,项目名我习惯叫 myapp
pulumi new typescript --name myapp
执行完之后,你会看到几个核心文件:Pulumi.yaml 是项目总配置;index.ts 用来写资源定义;以后还会出现 Pulumi.dev.yaml、Pulumi.prod.yaml 这些按堆栈区分的配置文件。第一次运行部署命令之前,还需要安装一下 AWS 的依赖包:
# 安装 AWS 资源包
npm install @pulumi/aws
4.2 编写参数化代码
接下来,把 index.ts 里的内容改成下面这样。这份代码会创建三个东西:一个 S3 存储桶、一个安全组、一台 EC2 云服务器。其中所有环境差异都来自配置项。
// 引入 Pulumi 的 AWS 包和核心工具
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
// 这里使用命名空间 myapp,和后续配置文件中的键对应
const config = new pulumi.Config("myapp");
// 读取环境名:dev、staging、prod
const env = config.require("env");
// 读取服务器规格,不设置时默认用 t3.micro
const instanceType = config.get("instanceType") ?? "t3.micro";
// 读取是否开启详细监控,默认关闭
const detailedMonitoring = config.getBoolean("detailedMonitoring") ?? false;
// 创建一个叫“数据桶”的 S3 存储桶,名字前面带上环境名
const dataBucket = new aws.s3.Bucket(`${env}-data-bucket`, {
bucketPrefix: `${env}-data-`, // 实际桶名会再追加随机后缀
acl: "private", // 私有访问权限
versioning: { // 只有生产环境开启版本控制
enabled: env === "prod",
},
tags: {
Environment: env, // 打上环境标签,方便识别
ManagedBy: "Pulumi",
},
});
// 创建一个安全组,生产环境只允许内网 SSH,其他环境放宽
const securityGroup = new aws.ec2.SecurityGroup(`${env}-sg`, {
ingress: [
{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: env === "prod" ? ["10.0.0.0/8"] : ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
],
tags: { Environment: env },
});
// 查找最新的 Amazon Linux 2 官方镜像,省得自己写 AMI ID
const ami = aws.ec2.getAmiOutput({
mostRecent: true,
owners: ["amazon"],
filters: [
{ name: "name", values: ["amzn2-ami-hvm-*-x86_64-ebs"] },
],
});
// 用参数创建一台服务器
const server = new aws.ec2.Instance(`${env}-server`, {
instanceType: instanceType, // 来自配置的实例规格
ami: ami.id, // 上面查到的镜像 ID
vpcSecurityGroupIds: [securityGroup.id],
monitoring: detailedMonitoring, // 是否开启详细监控
tags: { Environment: env },
});
// 导出几个关键结果,方便部署后查看
export const bucketName = dataBucket.bucket;
export const securityGroupId = securityGroup.id;
export const serverId = server.id;
这段代码看起来长,但核心逻辑其实就四步:读配置、建存储桶、建安全组、建服务器。后面不管你切到哪个环境,跑的代码都是同一份,只有配置文件不一样。
4.3 为不同环境准备配置文件
现在给两个环境分别生成配置文件。你可以直接手动创建,也可以用命令行来设置。先看手动创建的效果。下面是一个 Pulumi.dev.yaml:
config:
aws:region: us-east-1
myapp:env: dev
myapp:instanceType: t3.micro
myapp:detailedMonitoring: false
再看 Pulumi.prod.yaml:
config:
aws:region: us-west-2
myapp:env: prod
myapp:instanceType: t3.large
myapp:detailedMonitoring: true
注意看,键名前面的 myapp 对应的就是代码里 new pulumi.Config("myapp") 的命名空间。如果你项目名不叫 myapp,这里要跟着改。aws:region 是 AWS provider 自己的配置,用来指定在哪个区域创建资源,跟我们的参数化没有直接关系,但是不能少。
4.4 用命令行切换环境
手动编辑 YAML 文件只是其中一种方式。实际更推荐用命令行来设置配置,因为它会自动帮你写好文件,还能检查类型。比如你想给生产环境把实例规格调大一点,可以这样:
# 切换到生产环境的堆栈
pulumi stack select prod
# 修改生产环境的实例规格
pulumi config set myapp:instanceType t3.xlarge
# 修改生产环境的详细监控开关
pulumi config set myapp:detailedMonitoring true
执行之后,Pulumi 会把 myapp:instanceType 和 myapp:detailedMonitoring 写入 Pulumi.prod.yaml。如果你当前在 dev 堆栈下运行同样的命令,它就会改动 Pulumi.dev.yaml。是不是觉得很灵活?
切换到开发环境部署,只需要:
# 选择开发环境堆栈
pulumi stack select dev
# 创建或更新资源
pulumi up
回到生产环境一样简单,把 dev 换成 prod。整个切换过程,就像换遥控器频道一样快。
4.5 进阶:读取复杂配置对象
有时候你不想一个字段一个字段地设置,而是想把一整组配置打包成一个对象。Pulumi 也支持。比如网络规划里有 VPC 网段和一组子网段,就可以在某个堆栈的配置文件里加上这样的内容:
config:
myapp:network: '{"vpcCidr":"10.100.0.0/16","subnetCidrs":["10.100.1.0/24","10.100.2.0/24"]}'
代码里可以用 requireObject 把这整包配置读出来,继续沿用之前创建好的 config 实例:
// 读取结构化配置 network
const networkConfig = config.requireObject<{
vpcCidr: string;
subnetCidrs: string[];
}>("network");
// 根据配置中的网段创建 VPC
const vpc = new aws.ec2.Vpc("custom-vpc", {
cidrBlock: networkConfig.vpcCidr,
enableDnsHostnames: true,
tags: { Name: "custom-vpc" },
});
// 循环创建子网
networkConfig.subnetCidrs.forEach((cidr, index) => {
new aws.ec2.Subnet(`subnet-${index}`, {
vpcId: vpc.id,
cidrBlock: cidr,
tags: { Name: `subnet-${index}` },
});
});
这种方式适合管理相互之间有依赖关系的参数,比如网络段、安全组规则、负载均衡策略。整包读取,结构清晰,不容易出错。
五、这套方法能带来什么
聊完了操作,咱们再回头看看这套参数化方法真正的价值,顺便也说一下它有什么短处。
5.1 应用场景
首先,最常见的场景就是多环境部署。开发、测试、预发、生产,每个环境都有自己的配置文件,代码共用,流程统一,不再担心环境之间互相污染。
其次,适合多人协作。新同事接手项目时,不需要理解一坨条件判断,只需要看配置文件和代码里读配置的位置,就能明白环境差异在哪儿。
再者,适合临时环境。比如为某个功能分支临时拉一个堆栈,用一套配置把资源建好,用完再销毁。参数化让这种临时环境成本变得很低。
还有,生产环境变更需要走审批时,你可以把配置变更做成一个代码评审请求,评审通过后交给 Pulumi 执行。这比直接登录控制台改东西安全得多。
5.2 技术优缺点
优点大概有三样。第一,减少重复劳动,一份代码跑天下;第二,可审查可回滚,配置变更和代码变更一样留痕;第三,用常规编程语言表达逻辑,还能用循环、条件语句,不像某些模板语言那么死板。
缺点也实实在在。首先,Pulumi 本身有一定学习曲线,你至少得知道状态文件、堆栈、Provider 这些概念;其次,它和你使用的云厂商深度绑定,不同云的资源定义不一致,切换云平台会有成本;再者,如果配置项太多而没有规划,容易变成一团乱麻,大家都往配置文件里塞东西,反而难维护。
5.3 需要注意的坑
有一点要特别留意:不要把密码、密钥直接写进配置文件。哪怕你只是把配置存在本地,也写一个忽略规则,把包含敏感信息的堆栈配置排除在版本库之外。Pulumi 的配置系统里有一个加密设置功能,可以把值加密保存,该用的时候必须用。
另外,配置项的命名要统一。别在代码里一会儿读 env,一会儿读 environment。建议先列一张配置清单,把所有需要的参数写清楚,再动手写代码。每个参数都必须有默认值,但生产环境的敏感项要强制设置,避免漏配。
还要注意,堆栈和物理环境不是永远一对一。有时候你同一个 dev 堆栈可能会被多个开发人员同时用,最好约定每个人的个人堆栈,比如 dev-zhangsan,避免互相踩踏。
六、收个尾
配置管理这件事,看着不起眼,却能把多环境运维从“手忙脚乱”变成“气定神闲”。把环境差异装进参数,也就是把重复劳动交给了工具。Pulumi 给你搭好了舞台,你只需要做好参数的设计,剩下的,让每个堆栈各取所需就好。
希望这篇文章能帮你少踩几个坑,早点下班。如果你还没试过用配置参数来管理基础设施,不妨从你今天手头的环境开始,先定义一个“环境名”,再定义几个“可变点”,慢慢就会感受到它的香。
评论
围绕“通过Pulumi配置管理实现多环境基础设施参数化”参与讨论