Wing 定制平台:将基础设施策略转化为代码
哇,现在是 2024 年,几乎已经走过了 21 世纪的四分之一,如果您正在阅读这篇文章,您可能应该为自己鼓掌,因为您做到了!
你们已经度过了过去几年中疯狂的过山车之旅,从流行病到持续不断的战争和全球不安全局势。
2024 年终于到来了,我们都会问自己:“今年一切终于开始恢复正常了吗?”……可能不会!
不过,当我们都紧张地等待着下一场全球危机来临时(我的宾果卡显示鼹鼠人浮出水面),我们也可以从一线希望中得到慰藉。翼型定制平台风靡一时,而且建造起来比以往任何时候都更容易!
在本系列博客中,我将逐步讲解如何构建、发布和使用您自己的 Wing 自定义平台。由于这只是第一部分,后续可能会有很多次迭代被拖延,因此在深入探讨之前,我们先快速进行一下关卡设置。
让我来介绍一下 Wing
一种用于云的编程语言。
Wing 将基础设施和运行时代码结合在一种语言中,使开发人员能够保持创作流程,并更快、更安全地交付更好的软件。

什么是 Wing 定制平台?
这篇文章的目的并非解释 Wing Platforms 的所有枯燥细节,那是 Wing 文档的工作(我会在下方提供参考链接)。我们更想体验搭建 Wing Platforms 的乐趣,所以我会简单解释一下。
Wing 自定义平台为我们提供了一种钩住 Wing 应用程序编译过程的方法。这可以通过自定义平台可以实现的各种钩子来实现。截至目前,这些钩子包括:
- preSynth:在编译器开始合成之前调用,并让我们访问构造树中的根应用程序。
- postSynth:在工件合成后立即调用,并允许我们操作生成的配置。对于 Terraform 供应器来说,这是 Terraform JSON 配置。
- 验证:在钩子之后立即调用
postSynth
并提供相同的输入,但关键区别在于传递的配置是不可变的。这对于验证操作至关重要。
尽管还存在其他几个钩子,但我们不会在本博客中讨论所有这些钩子。
让我们开始建造吧!
在我们开始构建我们自己的定制平台之前,我们还需要一点重要的信息:“我们的平台要做什么?”
很高兴您提出这个问题!我们将构建一个自定义平台,以增强开发人员使用基于 Terraform 的平台时的体验,其中一些平台已内置于 Wing 安装中,例如tf-aws
、tf-azure
和tf-gcp
。
我们想要添加的具体增强功能是配置如何使用 Terraform 后端管理 Terraform 状态文件的功能。默认情况下,所有基于 Terraform 的内置平台都将使用本地状态文件配置,这对于快速实验来说很不错,但对于生产质量部署而言则缺乏一定的严谨性。
目标
构建并发布 Wing 自定义平台,提供配置 Terraform 后端状态管理的方法。
为了简洁起见,我们将重点介绍 3 种后端类型s3
,、azurerm
和gcs
所需材料
- 翼
- NPM 和 Node
- 一些 Typescript 知识
- 一个愿望和一个祈祷
创建项目
首先,让我们创建一个新的 npm 项目,在本指南中,我将介绍一些基本内容,因此我将创建package.json
一个tsconfig.json
下面是我的package.json
文件,关于它唯一真正有趣的部分是开发依赖@winglang/sdk
它,这样我们就可以使用一些公开的平台类型,我们很快就会看到一个例子。
{
"name": "@wingplatforms/tf-backends",
"version": "0.0.1",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/hasanaburayyan/wing-tf-backends"
},
"license": "ISC",
"devDependencies": {
"typescript": "5.3.3",
"@winglang/sdk": "0.54.30"
},
"files": [
"lib"
]
}
tsconfig.json
为了简洁起见,我省略了一些其他细节,因为其他一些选项只是个人偏好。这里值得注意的是我决定如何构建项目。我所有的代码都放在一个文件夹中,并且src
我的预期是编译输出也位于该lib
文件夹中。现在,您可以设置不同的项目,这没问题,但如果您只是跟着我一起操作,那么值得解释一下。
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./lib",
"lib": [
"es2020",
"dom"
],
},
"include": [
"./src/**/*"
],
"exclude": [
"./node_modules"
]
}
然后准备依赖项,我们只需运行npm install
让我们编码吧!
好的,现在初始设置已经完成,是时候开始编写我们的平台了!!
首先,我将创建一个文件,src/platform.ts
其中包含我们平台的主要代码,供 Wing 编译器使用。平台所需的最低限度代码如下所示:
import { platform } from "@winglang/sdk";
export class Platform implements platform.IPlatform {
read-only target = "tf-*";
}
这里我们创建并导出了 Platform 类,它实现了该IPlatform
接口。所有平台钩子都是可选的,因此我们无需定义任何其他内容即可使其在技术上有效。
现在需要做的就是定义target
这种机制,允许平台定义与之兼容的配置引擎和云提供商。截至本文撰写时,这种兼容性实际上尚未强制执行,但……我们猜想它应该有效 :)
好的,我们有了一个基本平台,但它实际上还没有用,让我们来改变它!首先,我们计划使用环境变量来确定用户想要使用哪种类型的后端,以及key
状态文件的内容。
因此我们将在我们的平台中提供一个构造函数:
import { platform } from "@winglang/sdk";
export class Platform implements platform.IPlatform {
readonly target = "tf-*";
readonly backendType: string;
readonly stateFileKey: string;
constructor() {
if (!process.env.TF_BACKEND_TYPE) {
throw new Error(`TF_BACKEND_TYPE environment variable must be set.`)
}
if (!process.env.TF_STATE_FILE_KEY) {
throw new Error("TF_STATE_FILE_KEY environment variable must be set.")
}
this.backendType = process.env.TF_BACKEND_TYPE
this.stateFileKey = process.env.TF_STATE_FILE_KEY
}
}
太棒了,现在我们开始行动了。我们的平台将要求用户在编译 Wing 代码时设置两个环境变量,TF_BACKEND_TYPE
目前TF_STATE_FILE_KEY
我们只是将这些数据作为实例变量持久化。
我们需要做的另一项日常事务是导出我们的平台代码,为此,让我们创建index.ts
一个如下所示的单行代码:
export * from "./platform"
测试我们的平台
在进一步讨论之前,我先演示一下如何在本地测试你的平台,看看它是否正常工作。为了测试这段代码,我们需要先使用命令编译它。npx tsc
由于我们已经在 中定义了所有内容,因此tsconfig.json
我们将创建一个名为 的文件夹lib
,其中包含所有生成的 JavaScript 代码。
让我们创建一个超级简单的 Wing 应用程序来使用这个平台。
// main.w
bring cloud;
new cloud.Bucket();
上述 Wing 代码将仅导入云库并使用它来创建 Bucket 资源。
接下来,我们将使用我们的平台结合其他基于 Terraform 的平台运行 Wing 编译命令,在我的例子中它将是tf-aws
wing compile main.w --platform tf-aws --platform ./lib
注意:我们提供了两个平台tf-aws
以及指向已编译平台的相对路径。./lib
这些平台的顺序也很重要,tf-aws
必须放在最前面,因为它是实现 API 的平台newApp()
。本文不会深入探讨这个问题,但如果您想深入了解,下面的参考资料会提供链接。
现在运行此代码将导致以下错误:
wing compile main.w -t tf-aws -t ./lib
An error occurred while loading the custom platform: Error: TF_BACKEND_TYPE environment variable must be set.
在你惊慌失措之前,先知道这是一个好错误 :) 我们确实可以看到我们的平台代码已加载并运行,因为抛出的错误需要TF_BACKEND_TYPE
环境变量。如果我们现在使用所需的变量重新运行编译命令,应该就能成功编译。
TF_BACKEND_TYPE=s3 TF_STATE_FILE_KEY=mystate.tfstate wing compile main.w -t tf-aws -t ./lib
为了确保编译成功,我们可以检查生成的 Terraform 代码target/main.tfaws/main.tf.json
{
"//": {
"metadata": {
"backend": "local",
"stackName": "root",
"version": "0.17.0"
},
"outputs": {
}
},
"provider": {
"aws": [
{
}
]
},
"resource": {
"aws_s3_bucket": {
"cloudBucket": {
"//": {
"metadata": {
"path": "root/Default/Default/cloud.Bucket/Default",
"uniqueId": "cloudBucket"
}
},
"bucket_prefix": "cloud-bucket-c87175e7-",
"force_destroy": false
}
}
},
"terraform": {
"backend": {
"local": {
"path": "./terraform.tfstate"
}
},
"required_providers": {
"aws": {
"source": "aws",
"version": "5.31.0"
}
}
}
}
我们应该看到正在创建一个单独的 Bucket,但是它仍然在使用local
Terraform 后端,这是因为我们还有一些工作要做!
实现 postSynth Hook
由于我们希望在代码合成后编辑生成的 Terraform 配置文件,因此我们将实现 postSynth 钩子。正如我之前解释的那样,此钩子在合成完成后立即调用,并传递生成的配置文件。
这个钩子更有用的是它允许我们返回配置文件的变异版本。
为了实现这个钩子,我们将用它来更新我们的平台代码
export class Platform implements platform.IPlatform {
// ...
postSynth(config: any): any {
if (this.backendType === "s3") {
if (!process.env.TF_S3_BACKEND_BUCKET) {
throw new Error("TF_S3_BACKEND_BUCKET environment variable must be set.")
}
if (!process.env.TF_S3_BACKEND_BUCKET_REGION) {
throw new Error("TF_S3_BACKEND_BUCKET_REGION environment variable must be set.")
}
config.terraform.backend = {
s3: {
bucket: process.env.TF_S3_BACKEND_BUCKET,
region: process.env.TF_S3_BACKEND_BUCKET_REGION,
key: this.stateFileKey,
}
}
}
return config;
}
}
现在我们可以看到这里发生了一些控制流逻辑,如果用户想要使用s3
后端,我们将需要一些额外的输入,例如存储桶的名称和区域,我们将使用TF_S3_BACKEND_BUCKET
和TF_S3_BACKEND_BUCKET_REGION
配置它们。
假设所有必需的环境变量都已存在,我们就可以操作提供的配置对象,并在其中设置config.terraform.backend
使用s3
配置块。最后返回配置对象。
现在,为了实际操作,我们需要编译代码(npx tsc
)并提供所有四个必需的 s3 环境变量。为了方便阅读,我将使用多行命令来执行这些命令:
# compile platform code
npx tsc
# set env vars
export TF_BACKEND_TYPE=s3
export TF_STATE_FILE_KEY=mystate.tfstate
export TF_S3_BACKEND_BUCKET=myfavorites3bucket
export TF_S3_BACKEND_BUCKET_REGION=us-east-1
# compile wing code!
wing compile main.w -t tf-aws -t ./lib
瞧!现在我们应该能够查看 Terraform 配置,并看到正在使用远程 s3 后端:
// Parts of the config have been omitted for brevity
{
"terraform": {
"required_providers": {
"aws": {
"version": "5.31.0",
"source": "aws"
}
},
"backend": {
"s3": {
"bucket": "myfavorites3bucket",
"region": "us-east-1",
"key": "mystate.tfstate"
}
}
},
"resource": {
"aws_s3_bucket": {
"cloudBucket": {
"bucket_prefix": "cloud-bucket-c87175e7-",
"force_destroy": false,
"//": {
"metadata": {
"path": "root/Default/Default/cloud.Bucket/Default",
"uniqueId": "cloudBucket"
}
}
}
}
}
}
它还活着!!!
如果你一直在关注,那就再给自己点赞吧!现在,除了熬过 2020 年代初,你还编写了自己的第一个 Wing Custom Platform!
现在,在我们讨论如何让其他 Wingnut 可以使用它之前,让我们先让我们的代码更简洁一些,更有用一些。
支持多个后端
为了名副其实,tf-backends
它应该支持多后端!为了实现这一点,我们只需要用一些老套的编码技巧来抽象一下。
我们希望我们的平台能够支持s3
,,azurerm
并且gcs
为了实现这一点,我们只需要config.terraform.backend
根据所需的后端定义不同的块。
为了完成这项工作,我将创建更多文件:
src/backends/backend.ts
// simple interface to define a backend behavior
export interface IBackend {
generateConfigBlock(stateFileKey: string): void;
}
现在有几个实现此接口的后端类
src/backends/s3.ts
import { IBackend } from "./backend";
export class S3 implements IBackend {
readonly backendBucket: string;
readonly backendBucketRegion: string;
constructor() {
if (!process.env.TF_S3_BACKEND_BUCKET) {
throw new Error("TF_S3_BACKEND_BUCKET environment variable must be set.")
}
if (!process.env.TF_S3_BACKEND_BUCKET_REGION) {
throw new Error("TF_S3_BACKEND_BUCKET_REGION environment variable must be set.")
}
this.backendBucket = process.env.TF_S3_BACKEND_BUCKET;
this.backendBucketRegion = process.env.TF_S3_BACKEND_BUCKET_REGION;
}
generateConfigBlock(stateFileKey: string): any {
return {
s3: {
bucket: this.backendBucket,
region: this.backendBucketRegion,
key: stateFileKey,
}
}
}
}
src/backends/azurerm.ts
import { IBackend } from "./backend";
export class AzureRM implements IBackend {
readonly backendStorageAccountName: string;
readonly backendStorageAccountResourceGroupName: string;
readonly backendContainerName: string;
constructor() {
if (!process.env.TF_AZURERM_BACKEND_STORAGE_ACCOUNT_NAME) {
throw new Error("TF_AZURERM_BACKEND_STORAGE_ACCOUNT_NAME environment variable must be set.")
}
if (!process.env.TF_AZURERM_BACKEND_STORAGE_ACCOUNT_RESOURCE_GROUP_NAME) {
throw new Error("TF_AZURERM_BACKEND_STORAGE_ACCOUNT_RESOURCE_GROUP_NAME environment variable must be set.")
}
if (!process.env.TF_AZURERM_BACKEND_CONTAINER_NAME) {
throw new Error("TF_AZURERM_BACKEND_CONTAINER_NAME environment variable must be set.")
}
this.backendStorageAccountName = process.env.TF_AZURERM_BACKEND_STORAGE_ACCOUNT_NAME;
this.backendStorageAccountResourceGroupName = process.env.TF_AZURERM_BACKEND_STORAGE_ACCOUNT_RESOURCE_GROUP_NAME;
this.backendContainerName = process.env.TF_AZURERM_BACKEND_CONTAINER_NAME;
}
generateConfigBlock(stateFileKey: string): any {
return {
azurerm: {
storage_account_name: this.backendStorageAccountName,
resource_group_name: this.backendStorageAccountResourceGroupName,
container_name: this.backendContainerName,
key: stateFileKey,
}
}
}
}
src/backends/gcs.ts
import { IBackend } from "./backend";
export class GCS implements IBackend {
readonly backendBucket: string;
constructor() {
if (!process.env.TF_GCS_BACKEND_BUCKET) {
throw new Error("TF_GCS_BACKEND_BUCKET environment variable must be set.")
}
if (!process.env.TF_GCS_BACKEND_PREFIX) {
throw new Error("TF_GCS_BACKEND_PREFIX environment variable must be set.")
}
this.backendBucket = process.env.TF_GCS_BACKEND_BUCKET;
}
generateConfigBlock(stateFileKey: string): any {
return {
gcs: {
bucket: this.backendBucket,
key: stateFileKey,
}
}
}
}
现在我们已经定义了后端类,可以更新平台代码来使用它们了。最终的平台代码如下所示:
import { platform } from "@winglang/sdk";
import { S3 } from "./backends/s3";
import { IBackend } from "./backends/backend";
import { AzureRM } from "./backends/azurerm";
import { GCS } from "./backends/gcs";
import { Local } from "./backends/local";
// TODO: support more backends: https://developer.hashicorp.com/terraform/language/settings/backends/local
const SUPPORTED_TERRAFORM_BACKENDS = [
"s3",
"azurerm",
"gcs"
]
export class Platform implements platform.IPlatform {
readonly target = "tf-*";
readonly backendType: string;
readonly stateFileKey: string;
constructor() {
if (!process.env.TF_BACKEND_TYPE) {
throw new Error(`TF_BACKEND_TYPE environment variable must be set. Available options: (${SUPPORTED_TERRAFORM_BACKENDS.join(", ")})`)
}
if (!process.env.TF_STATE_FILE_KEY) {
throw new Error("TF_STATE_FILE_KEY environment variable must be set.")
}
this.backendType = process.env.TF_BACKEND_TYPE
this.stateFileKey = process.env.TF_STATE_FILE_KEY
}
postSynth(config: any): any {
config.terraform.backend = this.getBackend().generateConfigBlock(this.stateFileKey);
return config;
}
/**
* Determine which backend class to initialize based on the backend type
*
* @returns the backend instance based on the backend type
*/
getBackend(): IBackend {
switch (this.backendType) {
case "s3": return new S3();
case "azurerm": return new AzureRM();
case "gcs": return new GCS();
default: throw new Error(`Unsupported backend type: ${this.backendType}, available options: (${SUPPORTED_TERRAFORM_BACKENDS.join(", ")})`);
}
}
}
轰隆隆!!我们的平台现在支持我们想要支持的所有 3 种不同的后端!
请随意构建和测试每一个。
发布我们的平台以供使用
现在我不会解释有关npm
包如何工作的所有复杂细节,因为我不会很好地完成这项工作,正如我下面的例子将使用一个版本0.0.3
(第三次魅力!)的事实所表明的那样。
不过,如果您一直按照步骤操作,您将能够运行以下命令。
注意:要发布此库,您需要定义一个您有权发布的包名称。如果您使用我的 (@wingplatforms/tf-backends),那么您就得好好休息一下了。
# compile platform code again
npx tsc
# package your code
npm pack
# publish your package
npm publish
如果操作正确,你应该看到类似这样的内容
npm notice === Tarball Details ===
npm notice name: @wingplatforms/tf-backends
npm notice version: 0.0.3
npm notice filename: wingplatforms-tf-backends-0.0.3.tgz
npm notice package size: 36.8 kB
npm notice unpacked size: 119.5 kB
npm notice shasum: 0186c558fa7c1ff587f2caddd686574638c9cc4c
npm notice integrity: sha512-mWIeg8yRE7CG/[...]cT8Kh8q/QwlGg==
npm notice total files: 17
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access
使用已发布的平台
创建平台后,我们来尝试一下。
注意:我建议使用干净的目录来操作。
使用与以前相同的简单 Wing 应用程序
// main.w
bring cloud;
new cloud.Bucket()
要使用自定义平台,我们还需要添加一项,该package.json
文件只需要将已发布的平台定义为依赖项:
{
"dependencies": {
"@wingplatforms/tf-backends": "0.0.3",
}
}
创建这两个文件后,让我们使用以下命令安装我们的自定义平台npm install
最后,我们设置 GCS 的所有环境变量并运行 Wing 编译命令。注意:由于我们使用的是已安装的 npm 库,因此我们将提供包名称,不再提供任何./lib
信息!
export TF_BACKEND_TYPE=gcs
export TF_STATE_FILE_KEY=mystate.tfstate
export TF_GCS_BACKEND_BUCKET=mygcsbucket
wing compile main.w -t tf-aws -t @wingplatforms/tf-backends
现在我们应该能够看到生成的 Terraform 配置正在使用正确的远程后端!
{
"terraform": {
"required_providers": {
"aws": {
"version": "5.31.0",
"source": "aws"
}
},
"backend": {
"gcs": {
"bucket": "mygcsbucket",
"key": "mystate.tfstate"
}
}
},
"resource": {
"aws_s3_bucket": {
"cloudBucket": {
"bucket_prefix": "cloud-bucket-c87175e7-",
"force_destroy": false,
"//": {
"metadata": {
"path": "root/Default/Default/cloud.Bucket/Default",
"uniqueId": "cloudBucket"
}
}
}
}
}
}
下一步是什么?
现在我们已经构建并发布了我们的第一个机翼定制平台,一切皆有可能!赶快行动起来,按照你的喜好开始构建定制平台吧<3,敬请期待平台构建系列的下一篇!
同时,请确保您加入 Wing Slack 社区:https://t.winglang.io/slack并分享您正在处理的工作或遇到的任何问题。
想要了解更多有关 Wing Platforms 的信息吗?
-
欢迎查看完整源代码:https ://github.com/hasanaburayyan/wing-tf-backends
鏂囩珷鏉ユ簮锛�https://dev.to/winglang/crafting-custom-platforms-in-a-cloudy-world-3ib1