Wing 定制平台:将基础设施策略转化为代码

2025-06-10

Wing 定制平台:将基础设施策略转化为代码

哇,现在是 2024 年,几乎已经走过了 21 世纪的四分之一,如果您正在阅读这篇文章,您可能应该为自己鼓掌,因为您做到了!

你们已经度过了过去几年中疯狂的过山车之旅,从流行病到持续不断的战争和全球不安全局势。

2024 年终于到来了,我们都会问自己:“今年一切终于开始恢复正常了吗?”……可能不会!

不过,当我们都紧张地等待着下一场全球危机来临时(我的宾果卡显示鼹鼠人浮出水面),我们也可以从一线希望中得到慰藉。翼型定制平台风靡一时,而且建造起来比以往任何时候都更容易!

在本系列博客中,我将逐步讲解如何构建、发布和使用您自己的 Wing 自定义平台。由于这只是第一部分,后续可能会有很多次迭代被拖延,因此在深入探讨之前,我们先快速进行一下关卡设置。

让我来介绍一下 Wing

一种用于云的编程语言。

Wing 将基础设施和运行时代码结合在一种语言中,使开发人员能够保持创作流程,并更快、更安全地交付更好的软件。

灵光一现

请加星标⭐Wing


什么是 Wing 定制平台?

这篇文章的目的并非解释 Wing Platforms 的所有枯燥细节,那是 Wing 文档的工作(我会在下方提供参考链接)。我们更想体验搭建 Wing Platforms 的乐趣,所以我会简单解释一下。

Wing 自定义平台为我们提供了一种钩住 Wing 应用程序编译过程的方法。这可以通过自定义平台可以实现的各种钩子来实现。截至目前,这些钩子包括:

  • preSynth:在编译器开始合成之前调用,并让我们访问构造树中的根应用程序。
  • postSynth:在工件合成后立即调用,并允许我们操作生成的配置。对于 Terraform 供应器来说,这是 Terraform JSON 配置。
  • 验证:在钩子之后立即调用postSynth并提供相同的输入,但关键区别在于传递的配置是不可变的。这对于验证操作至关重要。

尽管还存在其他几个钩子,但我们不会在本博客中讨论所有这些钩子。

让我们开始建造吧!

在我们开始构建我们自己的定制平台之前,我们还需要一点重要的信息:“我们的平台要做什么?”

很高兴您提出这个问题!我们将构建一个自定义平台,以增强开发人员使用基于 Terraform 的平台时的体验,其中一些平台已内置于 Wing 安装中,例如tf-awstf-azuretf-gcp

我们想要添加的具体增强功能是配置如何使用 Terraform 后端管理 Terraform 状态文件的功能。默认情况下,所有基于 Terraform 的内置平台都将使用本地状态文件配置,这对于快速实验来说很不错,但对于生产质量部署而言则缺乏一定的严谨性。

目标

构建并发布 Wing 自定义平台,提供配置 Terraform 后端状态管理的方法。

为了简洁起见,我们将重点介绍 3 种后端类型s3,、azurermgcs

所需材料

  • 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"
    ]
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json为了简洁起见,我省略了一些其他细节,因为其他一些选项只是个人偏好。这里值得注意的是我决定如何构建项目。我所有的代码都放在一个文件夹中,并且src我的预期是编译输出也位于该lib文件夹中。现在,您可以设置不同的项目,这没问题,但如果您只是跟着我一起操作,那么值得解释一下。

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "rootDir": "./src",
        "outDir": "./lib",
        "lib": [
            "es2020",
            "dom"
        ],
    },
    "include": [
        "./src/**/*"
    ],
    "exclude": [
        "./node_modules"
    ]
}
Enter fullscreen mode Exit fullscreen mode

然后准备依赖项,我们只需运行npm install

让我们编码吧!

好的,现在初始设置已经完成,是时候开始编写我们的平台了!!

首先,我将创建一个文件,src/platform.ts其中包含我们平台的主要代码,供 Wing 编译器使用。平台所需的最低限度代码如下所示:

import { platform } from "@winglang/sdk";

export class Platform implements platform.IPlatform {
  read-only target = "tf-*";
}
Enter fullscreen mode Exit fullscreen mode

这里我们创建并导出了 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
    }
}
Enter fullscreen mode Exit fullscreen mode

太棒了,现在我们开始行动了。我们的平台将要求用户在编译 Wing 代码时设置两个环境变量,TF_BACKEND_TYPE目前TF_STATE_FILE_KEY我们只是将这些数据作为实例变量持久化。

我们需要做的另一项日常事务是导出我们的平台代码,为此,让我们创建index.ts一个如下所示的单行代码:

export * from "./platform"
Enter fullscreen mode Exit fullscreen mode

测试我们的平台

在进一步讨论之前,我先演示一下如何在本地测试你的平台,看看它是否正常工作。为了测试这段代码,我们需要先使用命令编译它。npx tsc由于我们已经在 中定义了所有内容,因此tsconfig.json我们将创建一个名为 的文件夹lib,其中包含所有生成的 JavaScript 代码。

让我们创建一个超级简单的 Wing 应用程序来使用这个平台。

// main.w
bring cloud;

new cloud.Bucket();
Enter fullscreen mode Exit fullscreen mode

上述 Wing 代码将仅导入云库并使用它来创建 Bucket 资源。

接下来,我们将使用我们的平台结合其他基于 Terraform 的平台运行 Wing 编译命令,在我的例子中它将是tf-aws

wing compile main.w --platform tf-aws --platform ./lib
Enter fullscreen mode Exit fullscreen mode

注意:我们提供了两个平台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.
Enter fullscreen mode Exit fullscreen mode

在你惊慌失措之前,先知道这是一个好错误 :) 我们确实可以看到我们的平台代码已加载并运行,因为抛出的错误需要TF_BACKEND_TYPE环境变量。如果我们现在使用所需的变量重新运行编译命令,应该就能成功编译。

TF_BACKEND_TYPE=s3 TF_STATE_FILE_KEY=mystate.tfstate wing compile main.w -t tf-aws -t ./lib
Enter fullscreen mode Exit fullscreen mode

为了确保编译成功,我们可以检查生成的 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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我们应该看到正在创建一个单独的 Bucket,但是它仍然在使用localTerraform 后端,这是因为我们还有一些工作要做!

实现 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以看到这里发生了一些控制流逻辑,如果用户想要使用s3后端,我们将需要一些额外的输入,例如存储桶的名称和区域,我们将使用TF_S3_BACKEND_BUCKETTF_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
Enter fullscreen mode Exit fullscreen mode

瞧!现在我们应该能够查看 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"
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

它还活着!!!

如果你一直在关注,那就再给自己点赞吧!现在,除了熬过 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;
}
Enter fullscreen mode Exit fullscreen mode

现在有几个实现此接口的后端类

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,
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们已经定义了后端类,可以更新平台代码来使用它们了。最终的平台代码如下所示:

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(", ")})`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

轰隆隆!!我们的平台现在支持我们想要支持的所有 3 种不同的后端!

请随意构建和测试每一个。

发布我们的平台以供使用

现在我不会解释有关npm包如何工作的所有复杂细节,因为我不会很好地完成这项工作,正如我下面的例子将使用一个版本0.0.3(第三次魅力!)的事实所表明的那样。

不过,如果您一直按照步骤操作,您将能够运行以下命令。
注意:要发布此库,您需要定义一个您有权发布的包名称。如果您使用我的 (@wingplatforms/tf-backends),那么您就得好好休息一下了。

# compile platform code again
npx tsc

# package your code
npm pack

# publish your package
npm publish
Enter fullscreen mode Exit fullscreen mode

如果操作正确,你应该看到类似这样的内容

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
Enter fullscreen mode Exit fullscreen mode

使用已发布的平台

创建平台后,我们来尝试一下。
注意:我建议使用干净的目录来操作。

使用与以前相同的简单 Wing 应用程序

// main.w
bring cloud;

new cloud.Bucket()
Enter fullscreen mode Exit fullscreen mode

要使用自定义平台,我们还需要添加一项,该package.json文件只需要将已发布的平台定义为依赖项:

{
  "dependencies": {
    "@wingplatforms/tf-backends": "0.0.3",
  }
}
Enter fullscreen mode Exit fullscreen mode

创建这两个文件后,让我们使用以下命令安装我们的自定义平台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
Enter fullscreen mode Exit fullscreen mode

现在我们应该能够看到生成的 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"
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

下一步是什么?

现在我们已经构建并发布了我们的第一个机翼定制平台,一切皆有可能!赶快行动起来,按照你的喜好开始构建定制平台吧<3,敬请期待平台构建系列的下一篇!

同时,请确保您加入 Wing Slack 社区:https://t.winglang.io/slack并分享您正在处理的工作或遇到的任何问题。

想要了解更多有关 Wing Platforms 的信息吗?


请加星标⭐Wing

鏂囩珷鏉ユ簮锛�https://dev.to/winglang/crafting-custom-platforms-in-a-cloudy-world-3ib1
PREV
无服务器函数并发的本地模拟
NEXT
使用 Winglang 和 LangChain 构建云原生电子表格 Copilot