追赶 terraform,让基础设施代码化更加容易,pulumi 都做了些什么?

2020-07-28 11:32:17 浏览数 (1)

好久不写 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,可以这么写:

代码语言:javascript复制
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 的问题:

1)状态管理还处在原始社会。

terraform 作为开源软件,既有开源软件生态丰富代码相对难以作恶的优势,又有开源软件只重视核心功能不注重使用体验的劣势。状态管理是 terraform 用户体验非常差的一环,由于没有提供相应的功能,客户只能自己在开源社区里找解决方案。目前 AWS 上常用的方案是 S3 存储状态,DynamoDB 用来加锁。如果多个人部署同一个 stack,就简单粗暴去 DynamoDB 拿锁排队。这个方案在几十人的团队里还凑合,再大就会有很多麻烦。另外,状态的版本控制基本上没有,或者只能通过状态使用的存储引擎做版本管理(比如 S3),很难有效对比多个状态之间的差异。

2)缺乏可视化的手段。

状态的展示,部署的过程其实都可能做很多可视化的事情,让整体体验更好一些,减少 devOps 犯错。然而,terraform 并没有做这方面的支持。

3)代码表现力一般。

用于描述基础设施的代码是否需要强大的表现力?强大的表现力是福还是祸?这块一直有争论。然而,实际使用的时候,我们总是绕不开循环,条件判断,以及对字符串做处理等各种工作,而 terraform 在这一块的表现力太弱,使得代码写起来非常冗长,很多时候不得不复制粘贴。

4)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 可以这么写:

代码语言:javascript复制
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 代码无论是长度还是逻辑都差不多,但当你想写如下的代码时,两者高下立现:

代码语言:javascript复制
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 语言本身的局限,那么,接下来这个例子则诠释了基础设施代码化的未来:

代码语言:javascript复制
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 做更细致配置,只需要把 () => 换做下述代码:

代码语言:javascript复制
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(本地软件)的支持。

0 人点赞