配置语言的黄金时代
对于一般用例来说,所有这些正在形成的模式都将使基础设施供应与应用程序本身难以区分。
现在,在大多数公司中,传统的 IT 团队已经将自己更名为 DevOps,并广泛采用 AWS,而不是本地的 VMWare 集群。他们使用 Terraform 而不是 bash 脚本,并且通常更为敏捷,采用了许多开发实践。他们都是些熟悉网络的专业人员,了解 IAM 在 AWS 中的工作方式。他们负责搭建网络和基础设施环境,保障其安全性,并将其移交给使用它们的应用程序。
开发人员多半会觉得这样很不错,因为他们不想学习 AWS IAM 或 VPC 的复杂之处。我回想起了在 2000 年进入这个行业时如何处理数据库的方方面面。那时,应用程序不会涉及数据库的结构,由 DBA 在生产系统上运行数据库脚本。这些脚本将创建数据库、表、索引,这差不多是整个数据库结构了。然后,开发人员将这些映射到他们的代码中,只要在确定的模式(由其他人管理)上运行该应用程序,则执行 DML。如今,我对基础设施有相同的看法。
只要 ORM 和其他各种框架开始管理这一领域,这些问题就会被捆绑到应用程序中。没有理由认为在基础设施上不会发生同样的事情。唯一的问题是,我们没有合适的框架来做这件事,但现在我们正在开始得到它们。
大规模处理应用程序基础设施(我认为这与管理广告、电子邮件、金融系统之类的核心业务服务基础设施不同)的需求出现在虚拟化时代,始于 CFEngine。向那些不了解 CFEngine 的人介绍一下,CFEngine 是我们今天所要了解的配置管理系统中的第一批产品其中的一款,在它之后是 Puppet、Chef 和其他的配置管理系统。CFEngine 出现的年代,大多数供应都是手工完成的,或者是按照现有文档的一步一步的操作说明来完成的。该提议的核心是马克·伯吉斯 (Mark Burgess) 撰写的一份白皮书 1。其理论的主线是:
当今的计算机系统是脆弱的、不可靠的。在计算机系统运维的每个阶段,都有人类参与维护和维修。这么高度的人类参与在未来将不可能维持下去。同等复杂度甚至更复杂的生物和社会系统具有自我修复过程,这对其生存至关重要。如果我们未来的计算机系统想要在复杂而不利的环境中生存下去,就有必要模拟这样的系统。
如何构建能大规模运行并共享源自于免疫系统的一些思想的自我修复系统?为破解该难题涌现出了第一批招法,CFEngine 正是其中之一。为了实现这一点,它祭出 3 个重要法宝:1) 它使用 DSL 来描述所需的状态,而不是过程式的语言。这将尝试抽象组件,以使管理员能够进行参数化和重用。2) CFEngine 具有聚合语义,即描述一个系统应该是什么样子的,当系统处于那种状态时 CFEngine 就变成惰性的。3) 在几个管理单元独立工作、几乎没有机会交流的情况下,它可以防止任务重复和进程挂起。
使用这些特性,可以实现一个自校正和自修复系统,从而得到一个可保持容错性的系统。现在有了 AWS,我们可以通过利用多区域性的服务来设计一个表现有相同属性的系统。从本质上讲,如果精心设计,这些服务可以将这些属性传递给应用程序。
在此期间或不久之后,出现了许多其他工具,每一种工具的侧重点是最初那份价值主张的不同方面。其中,可能最受欢迎的是 Puppet 和 Chef。他们各有所长。在我就职的公司,我们使用 Puppet 来处理基础设施配置,主要的原因是非编程人员更容易理解它。从系统管理员的视角来看,在不深入编码的情况下完成某些工作是很具吸引力的。随着时间的推移,这被证明是一个错误的选择,它对于我们来说弊大于利。
Puppet 有它自己的 DSL、它自己的术语和特性。它有自己的工具生态系统、仪表板和扩展,可以帮助你在管理基础设施方面走得很远。它的优势是处理各系统之间不需要分布式协调的单独的服务器系统。可以通过导出资源和 PuppetDB 在多个服务器之间进行协调,但对我来说,这总是让人觉得很不爽(现在可能和当时有所不同了,但我已经好几年没有关注这个领域了)。
#https://github.com/voxpupuli/puppet-minecraft/blob/master/manifests/user.pp
class minecraft::user {
group { $minecraft::group:
ensure => present,
system => true,
}
user { $minecraft::user:
ensure => present,
gid => $minecraft::group,
home => $minecraft::install_dir,
managehome => true,
system => true,
require => Group[$minecraft::group],
}
# Ensures deletion of install_dir does not break module, setup for plugins
$dirs = [$minecraft::install_dir, '${minecraft::install_dir}/plugins']
file { $dirs:
ensure => directory,
owner => $minecraft::user,
group => $minecraft::group,
require => User[$minecraft::user],
}
}
在处理简单的基础设施组件时,这完全没有问题,我们可以做大量的自动化来管理数百台服务器。但像大多数事情一样,让你痛苦不堪的往往是一些极端情况。由于 Puppet 语言是一种 DSL,简单的问题开始变成大问题。比如,无法做到基本的 for 循环,甚至连字符串操作也做不到。大多数配置语言都存在这些问题。最后,你可能还会遇到这样的情况:你需要扩展它们以涵盖特定的用例,通常,要做到这一点,就需要编写真正的代码。如果不使用“真正的”语言,你能做的事情也就会十分有限,所以我们可能从一开始就应该选择合适的做法。现在回想起来,也许 Chef 会是一个更好的选择。至少使用它的时候,我学到的很多东西可以迁移到我职业生涯的其他地方(Chef 使用 ruby 而不是他们自己的 DSL)。我对 Ansible 和 Salt 没有太多的经验,但我觉得它们都有同样的缺点和相似的优点。
在配置语言的领域中,有一类稍微有所不同,那就是 Terraform 和 AWS Cloud Formation。
他们都借鉴了配置管理系统的遗风,并试图与他们的内部观点保持同步,即所见应该与事实相一致。除了表达意图的方式 (仍然使用 DSL,而不是非常成熟的语言) 之外,主要的区别在于它们的设计定位是云提供商层。
正如 Puppet 和 Chef 非常擅长管理机器上的典型资源 (服务、包、配置文件),Terraform 和 AWSCloud Formation 非常擅长管理云服务。它们是基于云基础设施的概念打造的。这并不意味着你不能使用 Puppet、Chef 和其他第二代配置语言做同样的事情。虽说如此,但由于 Terraform 和 AWSCloud Formation 非常快速地适应了云的现实情况,再加上它们设法通过云赚钱的方式等等原因,Terraform 成为了基于 DSL 的云基础设施管理领域无可争议的王者,而 Cloud Formation 则在 AWS 领域独霸天下。
所有这些工具都采纳了 CFEngine 中最好的地方,其中最重要的是收敛状态的概念。他们会把你表达的意图,与机器进行比较,找出任何依赖关系和步骤顺序,使资源达到它想要的状态。通常,它们还包含一个编译阶段,在此阶段,它们将 DSL 映射到内部逻辑并创建执行计划。这还将捕捉基本的错误。这些都是经过实践检验过的好想法,现在已经成为处理基础设施的默认方式。
然而,随着我们的进步,我们消费和处理基础设施组件的方式正在发生根本性的变化。现在你可以利用 AWS 服务了。你可以构建一个非常复杂的应用程序,使用 CloudFront 来进行静态内容分发,使用 Lambda 的 API 网关来构建 API 路由并向其添加业务功能,可以通过 Cognito 来处理身份管理。在后台,这些 Lambda 函数可以与整个基础设施生态系统直接交互,如 RDS 或 DynamoDB。你可以通过 Redshift 与分析系统交互,也可以通过 QuickSight 展示可视化数据。除了使用 AWS EMR 或 Glue 处理具有步骤函数的工作流驱动、异步批处理、ETL 任务之外,还可以由 Lambda 处理后台任务。由于 AWS 服务很好地集成在一起,只需相对较少的粘合代码即可组合这些服务以实现巨大的业务价值。
当前,正在涌现出新的应用程序类别,它们试图更好地适应这种环境。比方说,由 AWS 称之为无服务器的这一类。使用 Terraform 或 Cloud Formation 为这些类型的应用提供服务可能不会那么顺畅。在这样一个紧密集成的模型中,你的基础设施将随着应用程序的发展而发展,因此这可能是最重要的应用程序关注点之一。AWS 在这里推行的是 SAM 模型,但我可以预见到未来类似 Ruby-on-Rails 或 Django 的框架将把 AWS 基础设施视为数据库。那么,像“rails migrate”之类的对云会有什么影响呢?它对数据模式会有什么影响呢?这对于 Rails 开发人员来说,可能相当不错。无论是好是坏,我认为我们正在沿着这条路走下去。
那么我们做到了吗?我不这么想。随着一些常见抽象概念尘埃落定,你在所有云中可以使用它们了,我们才可能会做到。大家做过一些尝试,比如 go 世界的“go-cloud”,它试图在最常见的标准之上构建一个抽象,但这是一场艰苦的战斗。
目前,我把赌注押在 Pulumi 和他们的自动化 api 之上。让我们稍稍回溯一下。Pulumi 是一个框架 (你可以称它为配置语言框架),它允许你用诸如 javascript、typescript、python、go、c# 之类的主流语言编写代码。它使用的仍然是与其他配置语言相同的概念,而且大多数支持实际上是建立在 Terraform 之上的。它真正有趣的是,既然你在写代码,就真的是在写代码。你可以利用你喜欢的所有包和你喜欢的所有编程范例,以及语言生态系统中的 IDE 和构建工具。
他们在网站上展示的第一个例子是:
// Create a serverless REST API
import * as awsx from '@pulumi/awsx';
// Serve a simple REST API at `GET /hello`.
let app = new awsx.apigateway.API('my-app', {
routes: [{
path: '/hello',
method: 'GET',
eventHandler: async (event) => {
return {
statusCode: 200,
body: JSON.stringify({ hello: 'World!' }),
};
},
}],
});
export let url = app.url;
它所做的,是在 AWS 中创建一个带有“/hello”路由的 API 网关,并将其代理给一个用 javascript 编写的 AWS Lambda 函数。这个 lambda 函数只返回 200 编码和一个 HTML 体,其中包含一个 JSON 对象,内容为:{hello: 'World'!}。
让我们来看另一个例子:
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
export = async () => {
const config = new pulumi.Config('aws');
const providerOpts = { provider: new aws.Provider('prov', { region: <aws.Region>config.require('envRegion') }) };
const vpc = awsx.ec2.Vpc.getDefault(providerOpts);
// Create a security group to let traffic flow.
const sg = new awsx.ec2.SecurityGroup('web-sg', { vpc }, providerOpts);
const ipv4egress = sg.createEgressRule('ipv4-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv4Location(),
});
const ipv6egress = sg.createEgressRule('ipv6-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv6Location(),
});
// Creates an ALB associated with the default VPC for this region and listen on port 80.
const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer('web-traffic',
{ vpc, external: true, securityGroups: [ sg ] }, providerOpts);
const listener = alb.createListener('web-listener', { port: 80 });
// For each subnet, and each subnet/zone, create a VM and a listener.
const publicIps: pulumi.Output<string>[] = [];
const subnets = await vpc.publicSubnets;
for (let i = 0; i < subnets.length; i++) {
const getAmiResult = await aws.getAmi({
filters: [
{ name: 'name', values: [ 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*' ] },
{ name: 'virtualization-type', values: [ 'hvm' ] },
],
mostRecent: true,
owners: [ '099720109477' ], // Canonical
}, { ...providerOpts, async: true });
const vm = new aws.ec2.Instance(`web-${i}`, {
ami: getAmiResult.id,
instanceType: 'm5.large',
subnetId: subnets[i].subnet.id,
availabilityZone: subnets[i].subnet.availabilityZone,
vpcSecurityGroupIds: [ sg.id ],
userData: `#!/bin/bash
echo 'Hello World, from Server ${i+1}!' > index.html
nohup python -m SimpleHTTPServer 80 &`,
}, providerOpts);
publicIps.push(vm.publicIp);
alb.attachTarget('target-' + i, vm);
}
// Export the resulting URL so that it's easy to access.
return { endpoint: listener.endpoint, publicIps: publicIps };
};
这是一个更复杂的示例 2,但是如果感觉太复杂,可以使用 Fargate3作为计算部分,这个版本会更简单。
https://cosminilie.ro/posts/evolution-of-configuration-languages/#fn:2
让我们来看看它是做了什么。首先构建一个内部的 Pulumi 上下文,以了解在 AWS 中使用哪个区域,之后,它将配置 AWS VPC 的网络部分。这相当重要,因为它非常体贴地考虑了所要做的(甚至是手工要做的)AWS 内部网络的领域知识。尽管如此,只需要一行代码,即可干净利落地完成。
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
export = async () => {
const config = new pulumi.Config('aws');
const providerOpts = { provider: new aws.Provider('prov', { region: <aws.Region>config.require('envRegion') }) };
const vpc = awsx.ec2.Vpc.getDefault(providerOpts);
// Create a security group to let traffic flow.
const sg = new awsx.ec2.SecurityGroup('web-sg', { vpc }, providerOpts);
const ipv4egress = sg.createEgressRule('ipv4-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv4Location(),
});
const ipv6egress = sg.createEgressRule('ipv6-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv6Location(),
});
// Creates an ALB associated with the default VPC for this region and listen on port 80.
const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer('web-traffic',
{ vpc, external: true, securityGroups: [ sg ] }, providerOpts);
const listener = alb.createListener('web-listener', { port: 80 });
// For each subnet, and each subnet/zone, create a VM and a listener.
const publicIps: pulumi.Output<string>[] = [];
const subnets = await vpc.publicSubnets;
for (let i = 0; i < subnets.length; i++) {
const getAmiResult = await aws.getAmi({
filters: [
{ name: 'name', values: [ 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*' ] },
{ name: 'virtualization-type', values: [ 'hvm' ] },
],
mostRecent: true,
owners: [ '099720109477' ], // Canonical
}, { ...providerOpts, async: true });
const vm = new aws.ec2.Instance(`web-${i}`, {
ami: getAmiResult.id,
instanceType: 'm5.large',
subnetId: subnets[i].subnet.id,
availabilityZone: subnets[i].subnet.availabilityZone,
vpcSecurityGroupIds: [ sg.id ],
userData: `#!/bin/bash
echo 'Hello World, from Server ${i+1}!' > index.html
nohup python -m SimpleHTTPServer 80 &`,
}, providerOpts);
publicIps.push(vm.publicIp);
alb.attachTarget('target-' + i, vm);
}
// Export the resulting URL so that it's easy to access.
return { endpoint: listener.endpoint, publicIps: publicIps };
};
这一句执行完成之后,你将有一个默认的 VPC,包括私有和公共子网,设置一个互联网网关和路由表,这样指定的公共子网就有它的默认路由 (0.0.0.0) 指向互联网网关。当我们在公共子网中创建 EC2 实例时,它们将可以从 internet 访问,并具有出站 internet 连接,而私有子网中的实例将只能在 VPC 中访问,不可以访问 internet。这是 AWS 推荐的设置,默认情况下是安全的。接下来,它创建一个安全组 (以及 AWS EC2 特性,它的工作原理类似于防火墙规则),只允许通过 ipv6 和 ipv4 向附加了安全组的资源发送 web 流量。具体在本例中,它将成为一个负载均衡器 (Application Load Balancer 是 AWS 产品名称,即应用程序负载均衡器)。
// Create a security group to let traffic flow.
const sg = new awsx.ec2.SecurityGroup('web-sg', { vpc }, providerOpts);
const ipv4egress = sg.createEgressRule('ipv4-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv4Location(),
});
const ipv6egress = sg.createEgressRule('ipv6-egress', {
ports: new awsx.ec2.AllTraffic(),
location: new awsx.ec2.AnyIPv6Location(),
});
// Creates an ALB associated with the default VPC for this region and listen on port 80.
const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer('web-traffic',
{ vpc, external: true, securityGroups: [ sg ] }, providerOpts);
const listener = alb.createListener('web-listener', { port: 80 });
现在我们有了自己的负载均衡器,我们将等待子网的创建 (参见 await 关键字)。一旦完成,我们就可以遍历所有公共子网,并在每个子网中使用 ubuntu AMI 创建一个 EC2 实例。出于测试目的,我们将使用 userData 脚本注入一个小的 bash 脚本来创建 HTML 页面。这将启动一个 python 嵌入式 web 服务器来为它提供服务。在这里,我们可以做任何事情 (例如,从 s3 获取一个 spring boot 应用程序或者任何类型的应用程序并启动和运行它)。最后,我们将把 EC2 实例附加到 ELB 上,这样就完成了。
const subnets = await vpc.publicSubnets;
for (let i = 0; i < subnets.length; i++) {
const getAmiResult = await aws.getAmi({
filters: [
{ name: 'name', values: [ 'ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*' ] },
{ name: 'virtualization-type', values: [ 'hvm' ] },
],
mostRecent: true,
owners: [ '099720109477' ], // Canonical
}, { ...providerOpts, async: true });
const vm = new aws.ec2.Instance(`web-${i}`, {
ami: getAmiResult.id,
instanceType: 'm5.large',
subnetId: subnets[i].subnet.id,
availabilityZone: subnets[i].subnet.availabilityZone,
vpcSecurityGroupIds: [ sg.id ],
userData: `#!/bin/bash
echo 'Hello World, from Server ${i+1}!' > index.html
nohup python -m SimpleHTTPServer 80 &`,
}, providerOpts);
alb.attachTarget('target-' + i, vm);
}
继续,在本例中,我们创建了一个 AWS VPC,并根据 AWS 最佳实践配置了所有网络。我们建立了一个负载均衡器,确保它不允许非期望的流量,在每个 AWS 可用性区域部署了几个 AWS EC2 实例以获得容错性 (这也是 AWS 的最佳实践),然后部署了我们的网页。不需要专门的教育、培训,不需要启用专门的人才,是么?我们在没有“DevOps”工程师的情况下做到了这一点。当然,与任何领域特定的框架一样,需要一些该领域的知识,但是一旦你学习了一些 SDK,云与你正在使用的任何其他框架没有什么不同。
现在,所有这些都弄好了,但你如何将它融入到你自己的应用中呢?不幸的是,这个问题的答案仍在研究中。但是考虑到它很可能与应用程序使用相同的语言,所以让所有东西都在相同的 repo 中会更合理。它仍然需要一个单独的工具来运行 (Pulumi),但你可以把它看作是该工具链中的另一个工具。如果是这样的话,若不使用构建应用程序和在云基础设施中所用的程序语言,还有什么意义呢?例如,如果我不得不使用一个单独的工具,那么它与使用 Terraform 并没有什么不同。这就是 Pulumi 自动化 api 的由来。
import { InlineProgramArgs, LocalWorkspace } from '@pulumi/pulumi/x/automation';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
import * as pulumi from '@pulumi/pulumi';
import * as mysql from 'mysql';
const process = require('process');
const args = process.argv.slice(2);
let destroy = false;
if (args.length > 0 && args[0]) {
destroy = args[0] === 'destroy';
}
const run = async () => {
// This is our pulumi program in 'inline function' form
const pulumiProgram = async () => {
const vpc = awsx.ec2.Vpc.getDefault();
const subnetGroup = new aws.rds.SubnetGroup('dbsubnet', {
subnetIds: vpc.publicSubnetIds,
});
// make a public SG for our cluster for the migration
const securityGroup = new awsx.ec2.SecurityGroup('publicGroup', {
egress: [
{
protocol: '-1',
fromPort: 0,
toPort: 0,
cidrBlocks: ['0.0.0.0/0'],
}
],
ingress: [
{
protocol: '-1',
fromPort: 0,
toPort: 0,
cidrBlocks: ['0.0.0.0/0'],
}
]
});
// example only, you should change this
const dbName = 'hellosql';
const dbUser = 'hellosql';
const dbPass = 'hellosql';
// provision our db
const cluster = new aws.rds.Cluster('db', {
engine: 'aurora-mysql',
engineVersion: '5.7.mysql_aurora.2.03.2',
databaseName: dbUser,
masterUsername: dbName,
masterPassword: dbPass,
skipFinalSnapshot: true,
dbSubnetGroupName: subnetGroup.name,
vpcSecurityGroupIds: [securityGroup.id],
});
const clusterInstance = new aws.rds.ClusterInstance('dbInstance', {
clusterIdentifier: cluster.clusterIdentifier,
instanceClass: 'db.t3.small',
engine: 'aurora-mysql',
engineVersion: '5.7.mysql_aurora.2.03.2',
publiclyAccessible: true,
dbSubnetGroupName: subnetGroup.name,
});
return {
host: pulumi.interpolate`${cluster.endpoint}`,
dbName,
dbUser,
dbPass
};
};
// Create our stack
const args: InlineProgramArgs = {
stackName: 'dev',
projectName: 'databaseMigration',
program: pulumiProgram
};
// create (or select if one already exists) a stack that uses our inline program
const stack = await LocalWorkspace.createOrSelectStack(args);
console.info('successfully initialized stack');
console.info('installing plugins...');
await stack.workspace.installPlugin('aws', 'v3.6.1');
console.info('plugins installed');
console.info('setting up config');
await stack.setConfig('aws:region', { value: 'us-west-2' });
console.info('config set');
console.info('refreshing stack...');
await stack.refresh({ onOutput: console.info });
console.info('refresh complete');
if (destroy) {
console.info('destroying stack...');
await stack.destroy({ onOutput: console.info });
console.info('stack destroy complete');
process.exit(0);
}
console.info('updating stack...');
const upRes = await stack.up({ onOutput: console.info });
console.log(`update summary: \n${JSON.stringify(upRes.summary.resourceChanges, null, 4)}`);
console.log(`db host url: ${upRes.outputs.host.value}`);
console.info('configuring db...');
// establish mysql client
const connection = mysql.createConnection({
host: upRes.outputs.host.value,
user: upRes.outputs.dbUser.value,
password: upRes.outputs.dbPass.value,
database: upRes.outputs.dbName.value
});
connection.connect();
console.log('creating table...')
// make sure the table exists
connection.query(`
CREATE TABLE IF NOT EXISTS hello_pulumi(
id int(9) NOT NULL,
color varchar(14) NOT NULL,
PRIMARY KEY(id)
);
`, function (error, results, fields) {
if (error) throw error;
console.log('table created!')
console.log('Result: ', JSON.stringify(results));
console.log('seeding initial data...')
});
// seed the table with some data to start
connection.query(`
INSERT IGNORE INTO hello_pulumi (id, color)
VALUES
(1, 'Purple'),
(2, 'Violet'),
(3, 'Plum');
`, function (error, results, fields) {
if (error) throw error;
console.log('rows inserted!')
console.log('Result: ', JSON.stringify(results));
console.log('querying to veryify data...')
});
// read the data back
connection.query(`SELECT COUNT(*) FROM hello_pulumi;`, function (error, results, fields) {
if (error) throw error;
console.log('Result: ', JSON.stringify(results));
console.log('database, tables, and rows successfuly configured!')
});
connection.end();
};
run().catch(err => console.log(err));
就代码行数而言,这个例子比较大,但要点是:我们是在相同的代码库中创建的基础结构和表结构。我们来看一下。
第一部分负责 AWS 中的网络设置,并创建一个允许所有访问的安全组。
const vpc = awsx.ec2.Vpc.getDefault();
const subnetGroup = new aws.rds.SubnetGroup('dbsubnet', {
subnetIds: vpc.publicSubnetIds,
});
// make a public SG for our cluster for the migration
const securityGroup = new awsx.ec2.SecurityGroup('publicGroup', {
egress: [
{
protocol: '-1',
fromPort: 0,
toPort: 0,
cidrBlocks: ['0.0.0.0/0'],
}
],
ingress: [
{
protocol: '-1',
fromPort: 0,
toPort: 0,
cidrBlocks: ['0.0.0.0/0'],
}
]
});
基础设施部分的第二部分使用 Aurora 创建数据库实例:
// example only, you should change this
const dbName = 'hellosql';
const dbUser = 'hellosql';
const dbPass = 'hellosql';
// provision our db
const cluster = new aws.rds.Cluster('db', {
engine: 'aurora-mysql',
engineVersion: '5.7.mysql_aurora.2.03.2',
databaseName: dbUser,
masterUsername: dbName,
masterPassword: dbPass,
skipFinalSnapshot: true,
dbSubnetGroupName: subnetGroup.name,
vpcSecurityGroupIds: [securityGroup.id],
});
const clusterInstance = new aws.rds.ClusterInstance('dbInstance', {
clusterIdentifier: cluster.clusterIdentifier,
instanceClass: 'db.t3.small',
engine: 'aurora-mysql',
engineVersion: '5.7.mysql_aurora.2.03.2',
publiclyAccessible: true,
dbSubnetGroupName: subnetGroup.name,
});
return {
host: pulumi.interpolate`${cluster.endpoint}`,
dbName,
dbUser,
dbPass
};
};
现在,所有这些都被封装在 Pulumi“栈”中,运行以创建基础设施:
// Create our stack
const args: InlineProgramArgs = {
stackName: 'dev',
projectName: 'databaseMigration',
program: pulumiProgram
};
// create (or select if one already exists) a stack that uses our inline program
const stack = await LocalWorkspace.createOrSelectStack(args);
//load pulumi plugins
await stack.workspace.installPlugin('aws', 'v3.6.1');
//set the aws region and logging
await stack.setConfig('aws:region', { value: 'us-west-2' });
await stack.refresh({ onOutput: console.info });
//in case we run the program with the destroy flag to remove everythig
if (destroy) {
console.info('destroying stack...');
await stack.destroy({ onOutput: console.info });
console.info('stack destroy complete');
process.exit(0);
}
//wait for the infra to be provisioned
const upRes = await stack.up({ onOutput: console.info });
一旦有了基础设施,我们就可以开始使用它了:
// establish mysql client
const connection = mysql.createConnection({
host: upRes.outputs.host.value,
user: upRes.outputs.dbUser.value,
password: upRes.outputs.dbPass.value,
database: upRes.outputs.dbName.value
});
connection.connect();
// make sure the table exists
connection.query(`
CREATE TABLE IF NOT EXISTS hello_pulumi(
id int(9) NOT NULL,
color varchar(14) NOT NULL,
PRIMARY KEY(id)
);
`, function (error, results, fields) {
if (error) throw error;
console.log('table created!')
console.log('Result: ', JSON.stringify(results));
console.log('seeding initial data...')
});
// seed the table with some data to start
connection.query(`
INSERT IGNORE INTO hello_pulumi (id, color)
VALUES
(1, 'Purple'),
(2, 'Violet'),
(3, 'Plum');
`, function (error, results, fields) {
if (error) throw error;
console.log('rows inserted!')
console.log('Result: ', JSON.stringify(results));
console.log('querying to veryify data...')
});
// read the data back
connection.query(`SELECT COUNT(*) FROM hello_pulumi;`, function (error, results, fields) {
if (error) throw error;
console.log('Result: ', JSON.stringify(results));
console.log('database, tables, and rows successfuly configured!')
});
该模型真正的亮点是在创建“无服务器”应用程序的时候。在此,开发人员已经创建了非常好的抽象,它使工作流平滑地工作,并向我们展示了未来的方向:
// Define a new GET endpoint backed by a lambda function and a static folder mapped to / which will be saved in s3.
const api = new awsx.apigateway.API('test', {
routes: [{
path: '/',
localPath: 'www',
},
{
path: '/test',
method: 'GET',
eventHandler: async (event) => {
// This code runs in an AWS Lambda anytime `/test` is hit.
return {
statusCode: 200,
body: 'Hello, API Gateway!',
};
},
}],
})
它所做的是创建一个具有两个路由的 API 网关,一个用于根端点 (/),另一个用于 /test 端点。当这个程序运行时,/ 路由将从本地 www 目录上传 s3 bucket 中的内容。/test 端点的背后是一个 lambda 函数,其中的上下文取自事件处理程序代码块。这例子,但是考虑到我们到目前为止所讨论的内容,它还是非常吸引人的。虽然到目前为止我的大多数例子都是以 Pulumi 为基础,但它们并不是朝着唯一这个方向发展的。例如,AWS 推出了“AWS CDK”4 或云开发工具包。这允许你用你选择的语言编写代码,它将在运行时被“合成”进云结构堆栈。甚至还有一个“构造库”,允许你使用已经由 AWS 创建并将其包含在你的代码库中的组件。这些组件将许多复杂性 (例如网络) 抽象为易于使用的小单元,它们是安全的,并遵循了最佳实践。
import * as core from '@aws-cdk/core';
import * as apigateway from '@aws-cdk/aws-apigateway';
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
export class WidgetService extends core.Construct {
constructor(scope: core.Construct, id: string) {
super(scope, id);
const bucket = new s3.Bucket(this, 'WidgetStore');
const handler = new lambda.Function(this, 'WidgetHandler', {
runtime: lambda.Runtime.NODEJS_10_X, // So we can use async in widget.js
code: lambda.Code.asset('resources'),
handler: 'widgets.main',
environment: {
BUCKET: bucket.bucketName
}
});
bucket.grantReadWrite(handler); // was: handler.role);
const api = new apigateway.RestApi(this, 'widgets-api', {
restApiName: 'Widget Service',
description: 'This service serves widgets.'
});
const getWidgetsIntegration = new apigateway.LambdaIntegration(handler, {
requestTemplates: { 'application/json': '{ 'statusCode': '200' }' }
});
api.root.addMethod('GET', getWidgetsIntegration); // GET /
}
}
这就是 AWS CDK 样品的工作原理,就像我们在 Pulumi 上所做的一样。甚至 Terraform 也在朝着这个方向发展,它有一个基于 AWS CDK 的项目,你可以用 typescript 和 python 编写脚本。这些构造在底层使用了 Terraform 模块,用于跨多个云提供商提供基础设施。
不管是好是坏,我认为我们正朝着一个方向前进,在最好的情况下,基础设施将与代码共存,就像构建文件与代码共存一样。但是,我认为大家会尝试更进一步,将基础设施代码集成到实际的应用程序中。所有这些都将由应用程序在运行时自行管理。现在这里有一个光谱,像大多数东西一样,不同的应用程序将处于其中某个地方。应用程序的类型将起到决大多数的决定作用。例如,我发现很难想象这对由 Postgres 实例支持的单体 java 应用程序的影响会像在 AWS 中运行的无服务器应用程序的影响那么大。
对于 DevOps 工程师来说,其含义现在也会有所不同。一些公司的理解是创建一个负责构建 ci 管道的 DevOps 工程师角色,与之类似,有些公司的开发人员将接管整个职责,并使用本文提到的工具和实践来完成所有工作。作为职业生涯中大部分时间都在参与 DevOps 的从业者,我的建议是学习 typescript 并熟悉 Pulumi。若不出所料,它会取得成功的,只是还需要些时日。如果这没有发生,至少,你可以随时切换成前端开发人员,因为我相信对他们的需求量会不断增长的。不过,AWS 在用户体验方面做得很烂,可能不会来找你工作。
原文链接:
https://cosminilie.ro/posts/evolution-of-configuration-languages/