凌云时刻 · 技术
导读:真正引领大家进入到基础设施代码化的是 terraform,现在 pulumi 正在追赶它。
作者 | 陈小天
来源 | 程序人生
前言
好久不写 devOps 代码,程序君感觉莫名手欠。最近看着一个开源项目 pulumi 比较有意思,这个周末就在自己的 aws 账号里作死尝试了一把,嗯,还挺香。究竟有多香呢,我们来一起探索吧。
在具体深入 pulumi 前,我们先来回答两个问题:
1. 为什么要让基础设施代码化(Infrastructure as Code)?
2. 基础设施代码化领域都有哪些产品?
我们知道,在 2006 年亚马逊通过 AWS 撬动开云服务的巨大蛋糕后,云服务便以不可阻挡之势深入互联网的各个角落。如今,除了一线互联网大厂和准大厂外,其它大大小小的公司都在使用云服务。
然而渐渐地,传统的运维手段在云时代开始难以为继,一个 devOps 动辄要管理成百上千的机器,如果手工去干,谁受得了?
所以逐渐催生出来基础设施代码化的需求。通过代码,我们可以更好地描述软件系统对基础设施的需求,更容易审核增量更新,也(潜在地)更容易测试变更,以及更容易复制和扩展现有的工作。
这样下来最终导致的结果是,我们可以更进一步用更少的人力来管理更多的设施,还更加高效和更难出错。听起来是不是很讽刺?我们程序员就是这么浪,自己开心地写代码断自己的后路。
不过这就是从工业革命以来时代发展的必然:高效的生产力战胜并消灭低效的生产力。
基础设施代码化起源于 AWS 的 cloudformation,它于 2011 年发布。通过 cloudformation,用户可以使用脚本来描述 AWS 上的资源的 CRUD。
但真正引领大家进入到基础设施代码化的,是 terraform,它的 v0.1 版本发布于 2014 年 7 月。巧的是,同年 9 月,kubernetes 第一个 release v0.2 在 github 上发布。
两者的使用场景虽然大不一样,但竞争的领域都是基础设施代码化这一块,关于 kubernetes 的前世今生,我们先放下不表。terraform 的初衷是通过对不同云服务的各种资源的抽象,让大家可以以几乎同样的方式撰写 AWS,Azure,google cloud,openstack 以及阿里云的基础设施的代码。
注意,很多人误解以为 terraform 可以一份代码搞定多种云,这是不对的。
就像 react native / flutter 一套代码搞定多个端一样,你只是不需要写不同语言的实现而已,具体到各种云的细节,还是需要不同的实现。
terraform 的崛起
terraform 背后的公司是 Hashicorp,就是在基础设施工具领域里大名鼎鼎的 concul(服务发现),vault(密钥管理),nomad(服务运行时,这个没怎么用过)等工具的开发者。Hashicorp 财务稳健,客户数量和收入连续四年翻番,今年 3 月份,赶巧在美国疫情爆发前敲定了 1.75 亿美金的 E 轮融资,富得流油,投后估值 51 亿美金,可见这个领域未来巨大的潜力。
hashicorp 为 terraform 设计了一套语言 HCL(Hashicorp Configuration Language)来描述基础设施资源的状态。比如我们要在 AWS 上创建一台运行 openresty 的 EC2,可以这么写:
provider "aws" {
region = "us-west-2"
}
data "aws_ami" "openresty" {
most_recent = true
filter {
name = "name"
values = ["openresty-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["xxxx"] # 我个人的 aws 账号 ID
}
resource "aws_security_group" "lb_sg" {
name = "lb_sg"
description = "allow http/https access"
vpc_id = "${aws_vpc.main.id}"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "lb" {
ami = "${data.aws_ami.openresty.id}"
instance_type = "t2.micro"
tags = {
Name = "lb"
}
}
resource "aws_network_interface_sg_attachment" "sg_attachment" {
security_group_id = "${aws_security_group.lb_sg.id}"
network_interface_id = "${aws_instance.lb.primary_network_interface_id}"
}
这段代码读起来并不复杂:
首先我们声明了用 aws provider 来创建资源,所以下述的资源都会创建在 aws 的 us-west-2 区域,就是美国西海岸俄勒冈的数据中心。
然后我们描述要使用的 AMI(Amazon Machine Image),这里我使用了我自己个人账号下的通过 packer(也是 hashicorp 的一个开源项目)构建好的名为 "openresty-xxx" 的 AMI。
随后描述一个资源:security group,开放 80/443 端口。
之后描述一个资源:EC2 实例,使用刚才描述的 AMI,实例大小用 t2.micro。
最后,描述如何把 security group 和 EC2 实例绑定起来。
从这段代码我们可以看出,terraform 是声明式语言(Declarative Language),它描述这个脚本运行完云平台应该具有什么状态。所以 terraform 脚本在运行的时候,会拿代码中的状态和服务器端的状态进行对比,得出一个 diff,然后生成为实现这个 diff 所需要的 cloudformation(对于 aws 而言)代码,最后执行之。
当然,如果每次都去云平台拿所有相关资源的状态,效率太低,所以 terraform 会将上一次执行完的结果的状态保存在本地或者公共的存储(一般是 S3),对比代码和上一次执行完保存的状态即可。
虽然 terraform 写起来很简单,但当我们撰写越来越多的 terraform 代码后,我们会发现,要能够很好地复用代码,还是要下一番功夫的。
terraform 支持模块(module),一个模块就像一个函数,有输入输出,以及函数的主体。上面的代码如果封装成一个模块,那么其输入可以是 security group 想要开放的端口,EC2 实例的大小,磁盘大小,使用的 AMI 的名字等等,而输出可以是 EC2 实例的 id,public / private IP 等等。
除了模块外,terraform 还支持各种各样的 provider,比如各个云服务商的基础设施相关的 provider,以及丰富的在软件生命周期内可能涉及的各种 IT 服务,比如管理代码的 github,处理监控的 datadog,静态网站部署的 netlify,监控报警用的 opsgenie,进行单点登录(SSO)的 okta 等。
这些 provider 让 terraform 的生命力非常旺盛,前景非常广阔。
目前,大部分基础设施代码化的工作还聚焦在生产环境的代码化上面,而未来企业的 IT 系统的架构的代码化,将会是一座巨大的金矿,这也是程序君持续看好 Hashicorp 这家公司的主要原因。
前面都在吹 terraform 的特点和优势,我们也来看看 terraform 的问题:
状态管理还处在原始社会
terraform 作为开源软件,既有开源软件生态丰富代码相对难以作恶的优势,又有开源软件只重视核心功能不注重使用体验的劣势。
状态管理是 terraform 用户体验非常差的一环,由于没有提供相应的功能,客户只能自己在开源社区里找解决方案。
目前 AWS 上常用的方案是 S3 存储状态,DynamoDB 用来加锁。如果多个人部署同一个 stack,就简单粗暴去 DynamoDB 拿锁排队。这个方案在几十人的团队里还凑合,再大就会有很多麻烦。
另外,状态的版本控制基本上没有,或者只能通过状态使用的存储引擎做版本管理(比如 S3),很难有效对比多个状态之间的差异。
缺乏可视化的手段
状态的展示,部署的过程其实都可能做很多可视化的事情,让整体体验更好一些,减少 devOps 犯错。然而,terraform 并没有做这方面的支持。
代码表现力一般
用于描述基础设施的代码是否需要强大的表现力?强大的表现力是福还是祸?这块一直有争论。
然而,实际使用的时候,我们总是绕不开循环,条件判断,以及对字符串做处理等各种工作。而 terraform 在这一块的表现力太弱,使得代码写起来非常冗长,很多时候不得不复制粘贴。
terraform cloud 才刚刚起步
头两个问题也许在 terraform 的企业版中得到解决,但我和我的公司都没有用过,具体怎么样不得而知,也许是迫于接下来要讲的 pulumi 在市场上的压力吧。
Hashicorp 在 2019 年 9 月开始提供 terraform cloud,为小团队解决这两个问题。然而,目前 terraform cloud 更像是一个临时拼凑的 CI 工具,还有很长的路要走。
pulumi 闪亮登场
pulumi 诞生于 2017 年,是微软和亚马逊云服务的老兵 Joe Duffy(CEO) 和 Luke Hoban(CTO)创建的,对标 terraform 的一款软件。
和 terraform 一样, pulumi 也采用了开源 + 增值服务的方案。也许是发现很多用户都受制于上述的状态管理和可视化的问题,pulumi 走得比较坚决,缺省就帮助用户保存状态(虽然也允许用户自己保存状态)。
这使得 pulumi 上手的难度比 terraform 瞬间低了一个层级。
pulumi 另一个特点是使用你所熟悉的编程语言来编写 devOps 代码。它支持 javascript / typescript / go / python / dotnet core (C#, F#, VB) 等多种语言,因为开源,所以第三方也可以加入新的语言的支持。
比如上文中创建一个 openresty EC2 实例的代码,用 typescript 可以这么写:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const ami = pulumi.output(aws.getAmi({
filters: [{
name: "name",
values: ["openresty-*"],
}],
owners: ["xxxx"], // 我个人的 aws 账号 ID
mostRecent: true,
}));
const group = new aws.ec2.SecurityGroup("lb_sg", {
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
]
});
export const server = new aws.ec2.Instance("lb", {
instanceType: "t2.micro",
securityGroups: ["default", group.name],
ami: ami.id,
tags: {
"Name": "lb",
},
});
这段代码任何一个 nodejs 工程师应该都能看得懂。我就不详细介绍了。
可以看到,在做这样简单的资源管理时,pulumi 代码和 terraform 代码无论是长度还是逻辑都差不多。
但当你想写如下的代码时,两者高下立现:
for (let item of require("fs").readdirSync(siteDir)) {
let filePath = require("path").join(siteDir, item);
let object = new aws.s3.BucketObject(item, {
bucket: bucket,
source: new pulumi.asset.FileAsset(filePath), // use FileAsset to point to a file
contentType: mime.getType(filePath) || undefined, // set the MIME type of the file
});
}
这如果你是 terraform 的用户,不妨想想这样的代码如何在 terraform 里完成。
如果说这个例子让你仅仅感受到 terraform 语言本身的局限,那么接下来这个例子则诠释了基础设施代码化的未来:
import * as aws from "@pulumi/aws";
import * as https from "https";
export const table = new aws.dynamodb.Table("hackernews", {
attributes: [{ name: "id", type: "S", }],
hashKey: "id",
billingMode: "PAY_PER_REQUEST",
});
export const cron = aws.cloudwatch.onSchedule("daily-yc-snapshot", "cron(30 8 * * ? *)", () => {
https.get("https://news.ycombinator.com", res => {
let content = "";
res.setEncoding("utf8");
res.on("data", chunk => content += chunk);
res.on("end", () => new aws.sdk.DynamoDB.DocumentClient().put({
TableName: table.name.get(),
Item: { date: Date.now(), content },
}).promise());
}).end();
});
在这段代码中,部署脚本和 lambda 代码水乳交融,浑然天成。
onSchedule 的回调是一个 lambda 函数,这个 aws lambda 函数隐含的配置和权限都被 pulumi 根据上下文自动设置好,无比自然,即便你需要为 lambda 做更细致配置,只需要把 () => 换做下述代码:
new aws.lambda.CallbackFunction("hackerNewsCrawler", {
memorySize: 256 /*MB*/,
callback: (e) => {
// lambda dcode
},
}
为什么我觉得这是基础设施代码化的未来呢?
因为现在互联网软件的开发越来越离不开基础设施的运维,而 Serverless 会加速这一过程。未来的编程语言一定是能够无缝地结合运维,开发者在开发各种各样的系统时,会直接或者间接地在撰写分配资源的代码。
这么说大家可能还是比较困惑,我们打个比方。
如果把 AWS 看做是一个操作系统,那么 API Gateway,Kenesis,ELB,S3 Stream 就是在处理这个操作系统的外部输入,而对应的 lambda 就是对外部输入的响应;SQS / S3 stream 等也是这个操作系统的 IPC(进程间通讯),对应的 lambda 就是处理进程间消息的手段。
当你构建 unix 系统下的服务时,资源已经在那里,你只需要撰写服务的业务逻辑就好;而在云系统下做服务时,你往往需要同时撰写分配资源和处理业务逻辑的代码。这样的代码如果一部分交由 devOps 来写,一部分由 app 开发者撰写,那么开发效率一定是很低的。
因而,terraform 代表着上一代的 devOps,即大部分运维的活还是 devOps 干;而 pulumi 代表着下一代的 devOps,大部分运维的活直接由程序员完成,甚至很多应用的逻辑和资源部署的逻辑是放在一起的。
这,也是为何 pulumi 要支持多种开发语言。
我一开始对这一点非常不解,觉得支持多个语言是在给自己下套,让自己分心不能专注把核心功能做好,为什么不只提供 typescript 的支持并将其做到极致呢?
但考虑到未来资源部署和业务逻辑的代码的界限会渐渐模糊,开发者会为自己项目撰写大量 devOps 代码的这一趋势,pulumi 的下注就显得目标清晰且有远见了。
如果只做 javascript/typescript 支持,那么一套 golang 撰写的服务,还需要用 typescript 来撰写 devOps 代码,显然无法很好地充分利用开发者的才智。
结语
当然,作为一个还不到三年的项目,pulumi 的缺点也是显而易见的:
1. 它的生态比 terraform 还差得很远,这里需要时间慢慢追赶。然而对创业公司来说,时间往往是最大的敌人。terraform 也许很快上市,也许很快成为一个价值数百亿美金的「巨头」,它可以等待 pulumi 培育好了市场,利用自己在行业中的口碑和地位不慌不忙地追赶。
2. 资源部署和业务逻辑代码的混合,挑战不小,pulumi 还需要在更复杂的业务场景下证明自己走出的路是可行。目前绝大多数组合使用简单 serverless 的场景,pulumi 已经完全干趴下 serverless framework。但 pulumi 还需要更复杂的场景,更完备的客户的使用案例来证明自己。
3. 用开发人员熟悉的代码描述资源,表现力足够强,但会不会难以阅读和调试?会不会抢了 devOps 的饭碗而导致其很难推行?这个问题和 terraform 第三个问题是一个硬币的两面。公有公理,婆有婆理。有时候我们会大大低估人类的固执和墨守成规,在很多传统 IT 公司,这意味着 IT 部门和研发部门间蛋糕的分配,甚至研发部门内部组织结构间蛋糕的分配。康威定律告诉我们:
设计系统的架构受制于产生这些设计的组织的沟通结构。
因而应用 pulumi 意味着组织架构的调整,所以新兴公司(穷小子)更容易使用 pulumi,而传统公司(富二代)更容易使用 terraform。
4. 用 pulumi 提供的状态管理方案,虽然很容易上手,但规模大一点的公司都会有疑虑。所以 pulumi 还需要提供 on premise(本地软件)的支持。
END
往期精彩文章回顾
重磅!容器存储解决方案蓝皮书发布
这本书,值2000亿!
闲鱼靠什么支撑起万亿的交易规模?
请回答2020
后疫情时代这家在线教育机构如何乘“云”而上
Darabonba:多语言SDK开发终极解决方案
阿里云吴天议:应用驱动云网络深度融合
如何轻松搞定SAP HANA数据库备份?
进阶之路:Java 日志框架全画传(下)
进阶之路:Java 日志框架全画传(中)
长按扫描二维码关注凌云时刻
每日收获前沿技术与科技洞见