Wing 맛보기

Cloud IaC의 세계는 점점 추상화되고 있습니다. 그 중 하나가 Wing 입니다. 설명보다 직접 눈으로 보는게 빠르니 해봅시다.
2023.03.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

IaC하면 뭐가 떠오르시나요? Terraform? AWS CDK? 아마 Chef, Ansible 쪽이 떠오르는 분도 있을 것 같습니다. 저도 개발을 시작할 때 이쪽이 유명했었는데 이제는 너무 많은 IaC가 생겨버렸네요.

근데 여기서 더 이것저것 무언가 만들어지고 있습니다. 그 중 하나가 Wing 입니다.

https://github.com/winglang/wing

아직 알파 버전이지만 메일링을 걸어두니 무언가 진행되고 있던 것 같아 보였어서 한번 다시 훑어봅니다.

Winglang

기본 컨셉은 이렇습니다.

Infrastructure and code in one language

Be independent and focus on your application by offloading cloud mechanics to the Wing compiler

Infrastructure as code가 아니라 Infrastructure and code 라는 표현을 사용하네요.

Wing은 기존에 IaC를 프로그래밍 언어로 사용할 수 있게 자체적으로 Winglang이라는 언어를 만들었고 이를 사용합니다.

새로운 언어를 만들기 위해 새로운 컴파일러를 만들고 있는 것을 볼 수 있었는데요. Rust로 작성하고 JSII를 통해 외부 모듈과 연결하고 WASM으로 빌드하는 형태로 보이네요.

컴파일러 https://github.com/winglang/wing/tree/v0.8.3/libs/wingc

JSII 생성기 https://github.com/winglang/wing/tree/v0.8.3/libs/wingii

그런데 JSII는 rust를 지원안하는데 어떻게 동작하는 걸까 봤더니, wasm-bindgen 이라는 모듈이 있고 이걸 사용하고 있었습니다...

https://github.com/rustwasm/wasm-bindgen

언어 자체가 궁금하시면 언어 스펙도 한번 보셔도 좋을 것 같습니다.

https://docs.winglang.io/reference/spec

또한 Wing은 스스로를 cloud-oriented language라 부르고 있습니다. Cloud 리소스 만을 위한 언어이기 때문에 이런 표현이 있는 것 같습니다.

근데 사실 AWS CDK도 이러한 형태고 멀티 플랫폼을 지원한다면 CDKTF가 있겠습니다. 새로운 바퀴의 탄생일까요? Wing의 논지는 이렇습니다.

반복 속도 - Wing 애플리케이션은 로컬 클라우드 시뮬레이터에서 실행할 수 있습니다. 이를 통해 개발자는 훨씬 더 빠른 속도로 반복하고 밀리초 대기 시간에서 증분 변경의 효과를 볼 수 있습니다.

높은 수준의 클라우드 기본 요소 - Wing을 사용하면 개발자는 풍부하고 높은 수준의 클라우드 독립적인 리소스 집합을 통해 클라우드를 최대한 활용할 수 있습니다. 이를 통해 개발자는 인프라 전문가가 아니어도 완전한 클라우드 애플리케이션을 구축할 수 있습니다.

분산 컴퓨팅 지원 - 클라우드는 대규모 분산 시스템인 반면 기존 언어는 단일 시스템에 수행할 작업을 지시하도록 설계되었습니다. Wing은 나중에 원격 시스템에서 실행되는 코드 조각인 인플라이트 기능의 개념을 도입하면서 여전히 다른 곳에서 정의된 데이터 및 리소스를 캡처하고 상호 작용할 수 있습니다.

클라우드 단위 테스트 - Wing을 사용하면 개발자가 클라우드 시뮬레이터를 단위 테스트 내부의 라이브러리로 사용하고 배포나 과도한 모킹 없이 전체 아키텍처를 테스트할 수 있습니다.

정책으로서의 인프라 - 배포, 네트워킹, 보안 및 관측 가능성과 같은 인프라 문제를 애플리케이션 코드 내부가 아닌 정책을 통해 수평적으로 적용할 수 있습니다.

원문 : https://docs.winglang.io/#why-you-should-consider-wing

살짝 맛 본 경험으로는 위의 이야기에서 시뮬레이터와 높은 추상화가 장점으로 느껴졌습니다. 어떻길래 좋다고 느꼈을까요?

실제 동작과정이 궁금하니 해봅시다.

해보기

Wing을 사용하는 과정은 이렇습니다.

SDK 확인 -> 코드 작성 -> 시뮬레이터 확인 -> 플랫폼에 배포

위 과정을 한번 돌아봅시다.

그런데 셋업이 필요합니다.

npm install -g winglang

로 언어를 설치하고 아래 링크에서 시뮬레이터를 다운받아주세요.

https://docs.winglang.io/getting-started/installation#wing-console

아직은 Windows, MacOS x86/Arm 만 지원하는 것 같네요. 만약 리눅스 환경이라면 시뮬레이터 작업은 스킵하셔도 됩니다.

SDK 확인

처음으로는 지원하는 SDK를 살펴봐야겠죠.

https://docs.winglang.io/reference/wingsdk-spec

버켓, 큐, 펑션 등등 일반적인 클라우드 플랫폼에 공통적으로 지원하는 항목들이 많이 있습니다. 예시 코드에 사용해보죠.

코드 작성

wing.w

bring cloud;

let queue = new cloud.Queue(timeout: 2m);
let bucket = new cloud.Bucket();
let counter = new cloud.Counter(initial: 100);

queue.on_message(inflight (body: str): str => {
  let next = counter.inc();
  let key = "myfile-${next}.txt";
  bucket.put(key, body);
});

큐, 버켓은 뭐가 만들어질지 감이 오는데, Counter는 뭘까요?

The counter resource represents a service that stores one or more integer values that can be (atomically) incremented or decremented. Counters are useful for tracking the number of times a particular event has occurred.

auto increment 같이 자동으로 증가하는 값이나 특정한 값을 저장하는 카운터라고 하는데요. 예를 들어 AWS 라면 Amazon DynamoDB에 테이블이 만들어지게 됩니다.

    this.table = new DynamodbTable(this, "Default", {
      name: ResourceNames.generateName(this, NAME_OPTS),
      attribute: [{ name: HASH_KEY, type: "S" }],
      hashKey: HASH_KEY,
      billingMode: "PAY_PER_REQUEST",
    });

...

   if (
      ops.includes(cloud.CounterInflightMethods.INC) ||
      ops.includes(cloud.CounterInflightMethods.DEC) ||
      ops.includes(cloud.CounterInflightMethods.RESET)
    ) {
      host.addPolicyStatements({
        effect: "Allow",
        action: ["dynamodb:UpdateItem"],
        resource: this.table.arn,
      });
    }

    if (ops.includes(cloud.CounterInflightMethods.PEEK)) {
      host.addPolicyStatements({
        effect: "Allow",
        action: ["dynamodb:GetItem"],
        resource: this.table.arn,
      });
    }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/target-tf-aws/counter.ts

구현체를 보면 CDKTF를 Typescript로 사용한 것을 볼 수 있습니다. 테이블을 생성할 뿐만 아니라 정책또한 연결해주는 것을 확인할 수 있습니다. 이것이 Wing이 가지고 있는 추상화의 장점입니다. 직접 IaC를 하다보면 이러한 역할, 정책 같은 것을 직접 잘 설정하기 위해 레퍼런스를 찾아야 하는데, 쉽지 않죠. 이러한 부분이 추상화 된 것은 장점이라 생각합니다.

시뮬레이터 확인

사실 제가 처음에 Wing을 발견했을 때 이러한 것이 생길거라는 이야기를 못 본건지 없었던건지 이번에 해보고 놀랐습니다.

작성한 코드를 시뮬레이터에서 사용하기 위해 커맨드가 필요합니다.

wing it wing.w

시뮬레이터가 켜져있다면 아래와 같은 화면이 나오게됩니다.

깔끔하네요.

Wing 코드에서 정의한 리소스들이 보이네요.

위에서 Wing이 리소스들을 로컬에서 테스트 가능하다고 했는데요, 어떤 느낌인지 해봅시다.

cloud.queue를 클릭해서 Send Messgae 부분에 아무말이나 입력하고 Send를 눌러봅시다.

그러면 성공했다는 메시지가 뜨네요.

이후에 버킷에 들어가보면 작성한 내용이 들어있는 파일이 생성된 것을 볼 수 있습니다.

이번엔 진짜 클라우드에 올려볼까요?

플랫폼에 배포

저는 AWS에 올려보겠습니다. 올리기 위해 일단 wing 파일을 컴파일 해줍시다.

 wing compile -t tf-aws wing.w

target/ 폴더 내부에 이것저것 파일들이 생성된 것을 확인할 수 있습니다.

tf 파일이 보이네요. main.tf.json 파일을 살펴보면 작성한 리소스들과 역할, 정책들이 생성되려 하는 것을 확인할 수 있습니다.

Terraform으로 정의된 리소스들을 AWS에 배포하기 위해 AWS cli와 Terraform 설정이 필요합니다.

https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html

https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli

cd ./target/wing.tfaws
export AWS_REGION=ap-northeast-2 # 서울 리전
terraform init
terraform apply

Apply complete! Resources: 12 added, 0 changed, 0 destroyed. 로그가 나오면 성공입니다.

이후 AWS 콘솔에서 Amazons SQS로 이동한 뒤 생성된 큐를 클릭한 뒤 우측 상단의 메시지 전송 및 수신 에서 아무 메지나 전송해봅시다.

Amazon DynamoDB에 생성된 테이블에는 카운터 값이 증가한걸 확인할 수 있고, Amazon S3로 이동한 뒤 생성된 버킷에서 전송한 메시지가 적혀있는 파일이 생성된 것을 확인할 수 있습니다! ???


잘 놀았지만, 리소스를 남겨두고 싶지 않으니 삭제하려면 버킷은 안에 파일이 없어야 하니 생성된 파일을 삭제하고 terraform destroy로 리소스들을 삭제해주세요.

아니 그런데 어떻게 카운터 값이 증가하고 파일이 생성된거죠?

저는 큐에 메시지를 보냈을 뿐인데 왜 파일이 생성되고, 카운터 값이 증가한걸까요... AWS Lambda라도 생성되서 일련의 작업을 대신 처리라도 한걸까요?

맞습니다. 이러한 일련의 작업을 추상화 시키는 것도 Wing의 역할입니다.

살짜 SDK 코드 맛좀 볼까요? 다행히도 Typescript 코드라 쉽게 읽을 수 있겠네요 좀 봅시다.

컴파일된 코드

SDK를 보기전에 작성한 코드가 어떻게 컴파일되었는지 부터 확인해봅시다. 컴파일된 코드들을 살펴보다가target/wing.tfaws/.wing/preflight.js 파일을 보니 작성한 코드들과 매칭된 코드를 확인할 수 있었습니다.

const $stdlib = require('@winglang/sdk');
const $outdir = process.env.WING_SYNTH_DIR ?? ".";

function __app(target) {
    switch (target) {
        case "sim":
            return $stdlib.sim.App;
        case "tfaws":
        case "tf-aws":
            return $stdlib.tfaws.App;
        case "tf-gcp":
            return $stdlib.tfgcp.App;
        case "tf-azure":
            return $stdlib.tfazure.App;
        default:
            throw new Error(`Unknown WING_TARGET value: "${process.env.WING_TARGET ?? ""}"`);
    }
}
const $App = __app(process.env.WING_TARGET);

const cloud = require('@winglang/sdk').cloud;
class MyApp extends $App {
constructor() {
  super({ outdir: $outdir, name: "wing", plugins: $plugins });

  const queue = this.node.root.newAbstract("@winglang/sdk.cloud.Queue",this,"cloud.Queue",{ timeout: $stdlib.std.Duration.fromSeconds(120) });
  const bucket = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket");
  const counter = this.node.root.newAbstract("@winglang/sdk.cloud.Counter",this,"cloud.Counter",{ initial: 100 });
  (queue.onMessage(new $stdlib.core.Inflight(this, "$Inflight1", {
    code: $stdlib.core.NodeJsCode.fromFile(require.resolve("./proc.파일이름해쉬/index.js".replace(/\\/g, "/"))),
    bindings: {
      bucket: {
        obj: bucket,
        ops: ["delete","get","get_json","list","public_url","put","put_json"]
      },
      counter: {
        obj: counter,
        ops: ["dec","inc","peek","reset"]
      },
    }
  })));
}
}
new MyApp().synth();

MyApp 부분이 작성된 코드를 포함하는 부분이고 이외의 부분은 이 코드가 실제로 작동되게 도와주는 부분 같네요.

MyApp을 확인해봅시다.

class MyApp extends $App {
constructor() {
  super({ outdir: $outdir, name: "wing", plugins: $plugins });

  const queue = this.node.root.newAbstract("@winglang/sdk.cloud.Queue",this,"cloud.Queue",{ timeout: $stdlib.std.Duration.fromSeconds(120) });
  const bucket = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket");
  const counter = this.node.root.newAbstract("@winglang/sdk.cloud.Counter",this,"cloud.Counter",{ initial: 100 });
  (queue.onMessage(new $stdlib.core.Inflight(this, "$Inflight1", {
    code: $stdlib.core.NodeJsCode.fromFile(require.resolve("./proc.파일이름해쉬/index.js".replace(/\\/g, "/"))),
    bindings: {
      bucket: {
        obj: bucket,
        ops: ["delete","get","get_json","list","public_url","put","put_json"]
      },
      counter: {
        obj: counter,
        ops: ["dec","inc","peek","reset"]
      },
    }
  })));
}
}

정의한 큐, 버킷, 카운터가 MyApp의 생성자로 사용되어지고 있네요. 그런데 $App을 상속하고 있네요 $App이 무엇일까요?

const $stdlib = require('@winglang/sdk');
const $outdir = process.env.WING_SYNTH_DIR ?? ".";

function __app(target) {
    switch (target) {
        case "sim":
            return $stdlib.sim.App;
        case "tfaws":
        case "tf-aws":
            return $stdlib.tfaws.App;
        case "tf-gcp":
            return $stdlib.tfgcp.App;
        case "tf-azure":
            return $stdlib.tfazure.App;
        default:
            throw new Error(`Unknown WING_TARGET value: "${process.env.WING_TARGET ?? ""}"`);
    }
}
const $App = __app(process.env.WING_TARGET);

$App은 컴파일시에 타겟으로 설정한 플랫폼의 SDK를 사용하도록 설정하네요. 그런데 WING_TARGET은 어디서 설정된걸까요?

컴파일시에 wing compile -t tf-aws wing.w 커맨드를 사용하였는데요. -t 가 cli에 타겟을 넘겨주게 되었고 아래 코드의 process.env["WING_TARGET"] = options.target; 부분에서 환경변수로 설정하게 해주고 있네요.

export async function compile(entrypoint: string, options: ICompileOptions): Promise<string> {
  const targetdir = join(dirname(entrypoint), "target");
  const wingFile = entrypoint;
  log("wing file: %s", wingFile);
  const wingDir = dirname(wingFile);
  log("wing dir: %s", wingDir);
  const synthDir = resolveSynthDir(targetdir, wingFile, options.target);
  log("synth dir: %s", synthDir);
  const workDir = resolve(synthDir, ".wing");
  log("work dir: %s", workDir);


  // clean up before
  await rm(synthDir, { recursive: true, force: true });


  process.env["WING_SYNTH_DIR"] = synthDir;
  process.env["WING_NODE_MODULES"] = resolve(join(wingDir, "node_modules") );
  process.env["WING_TARGET"] = options.target;

https://github.com/winglang/wing/blob/v0.8.3/apps/wing/src/commands/compile.ts#L67-L83

그러면 tfaws.App을 상속하고 있으니 한번 봅시다.

SDK

여러 플랫폼에서 사용하기 위해서 Terraform CDKTF를 추상화한 또다른 클래스를 상속하고 있네요. 그러면 코어인 부분을 가봅시다.

...

import { CdktfApp, AppProps } from "../core";

/**
 * An app that knows how to synthesize constructs into a Terraform configuration
 * for AWS resources.
 */
export class App extends CdktfApp {
  constructor(props: AppProps = {}) {
    super(props);
    new AwsProvider(this, "aws", {});
  }
...

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/target-tf-aws/app.ts

이 코어 코드를 보고 비로소 컴파일된 코드에서 사용된 newAbstract 를 이해하게 됩니다.

  /**
   * Creates a new object of the given abstract class FQN.
   */
  public newAbstract(
    fqn: string,
    scope: Construct,
    id: string,
    ...args: any[]
  ): any {
    // delegate to "tryNew" first, which will allow derived classes to inject
    const instance = this.tryNew(fqn, scope, id, ...args);
    if (!instance) {
      throw new Error(
        `Unable to create an instance of abstract type \"${fqn}\" for this target`
      );
    }

    return instance;
  }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/app.ts#L110-L128

여기서 newAbstract로 인스턴스를 생성하는 것처럼 보이죠.

  const queue = this.node.root.newAbstract("@winglang/sdk.cloud.Queue",this,"cloud.Queue",{ timeout: $stdlib.std.Duration.fromSeconds(120) });
  const bucket = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket");
  const counter = this.node.root.newAbstract("@winglang/sdk.cloud.Counter",this,"cloud.Counter",{ initial: 100 });

newAbstract는 플랫폼 별 ApptryNew 함수를 통해 인스턴스를 생성하는데요. tryNew 함수는 아래와 같습니다.

  protected tryNew(
    fqn: string,
    scope: Construct,
    id: string,
    ...args: any[]
  ): any {
    switch (fqn) {
      case API_FQN:
        return new Api(scope, id, args[0]);

      case FUNCTION_FQN:
        return new Function(scope, id, args[0], args[1]);

      case BUCKET_FQN:
        return new Bucket(scope, id, args[0]);

      case LOGGER_FQN:
        return new Logger(scope, id);

      case QUEUE_FQN:
        return new Queue(scope, id, args[0]);

      case TOPIC_FQN:
        return new Topic(scope, id, args[0]);

      case COUNTER_FQN:
        return new Counter(scope, id, args[0]);

      case SCHEDULE_FQN:
        return new Schedule(scope, id, args[0]);

      case TABLE_FQN:
        return new Table(scope, id, args[0]);

      case TOPIC_FQN:
        return new Topic(scope, id, args[0]);
    }

    return undefined;
  }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/target-tf-aws/app.ts#L35-L74

그렇군요, 리소스들이 위의 일련의 방법으로 인스턴스들이 생성되는 건 알겠는데, 결국 어떻게 AWS Lambda가 생성되었는지는 찾지 못 했습니다. 컴파일된 코드로 돌아가보죠.

Inflight

  const queue = this.node.root.newAbstract("@winglang/sdk.cloud.Queue",this,"cloud.Queue",{ timeout: $stdlib.std.Duration.fromSeconds(120) });
  const bucket = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket");
  const counter = this.node.root.newAbstract("@winglang/sdk.cloud.Counter",this,"cloud.Counter",{ initial: 100 });
  (queue.onMessage(new $stdlib.core.Inflight(this, "$Inflight1", {
    code: $stdlib.core.NodeJsCode.fromFile(require.resolve("./proc.파일이름해쉬/index.js".replace(/\\/g, "/"))),
    bindings: {
      bucket: {
        obj: bucket,
        ops: ["delete","get","get_json","list","public_url","put","put_json"]
      },
      counter: {
        obj: counter,
        ops: ["dec","inc","peek","reset"]
      },
    }
  })));

이제 리소스가 생성된 것을 이해했고, 리소스 생성 밑에 queue.onmessage 이후가 신경쓰이네요. new $stdlib.core.Inflight 이 부분에 뭔가 있는 것 같습니다. 밑에 code, bindings, bucket, counter 가 있는 걸 봐서요.

core.Inflight 를 보기전에 Inflight를 Wing에서 어떻게 설명하고 있는지 봅시다.

https://docs.winglang.io/concepts/inflights

클라우드에서 각각의 리소스들은 분산 시스템이고 이러한 다양한 컴퓨팅 환경에서의 상호작용을 위해서 특정한 역할하는 리소스가 필요합니다. 이러한 기능을 가진 리소스를 Wing에서는 Inflight라 정의했습니다.

상호작용을 위해서는 정의한 동작 이외에도 플랫폼에서 정의하는 역할이나 정책같은 리소스들의 접근에 대한 규칙을 생성해야 하는데 이러한 부분도 Wing의 Inflight가 자동으로 처리하게 됩니다.

따라서, Inflight 내부에 정의된 동작이 자바스크립트 코드 같이 동작가능한 코드로 컴파일되고 이 코드를 해당 플랫폼에 맞는 함수 (AWS라면 AWS Lambda 같은) 에 추가하며 생성되게 됩니다.

Inflight SDK

SDK 쪽 코드를 봅시다. 위에서 본 new $stdlib.core.Inflight 부터 보면 되겠네요.

import { makeHandler } from "./internal";

...

export class Inflight extends Construct implements IResource {
  /** @internal */
  public _connections: Connection[] = []; // thrown away


  /**
   * Information on how to display a resource in the UI.
   */
  public readonly display = new Display();


  constructor(scope: Construct, id: string, props: InflightProps) {
    super(null as any, ""); // thrown away


    this.display.hidden = true;
    this.display.title = "Inflight";
    this.display.description = "An inflight resource";


    if (props.code.language !== Language.NODE_JS) {
      throw new Error("Only Node.js code is supported");
    }


    return makeHandler(scope, id, props.code.text, props.bindings, {
      hidden: this.display.hidden,
      title: this.display.title,
      description: this.display.description,
    });
  }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/inflight.ts#LL94-L119C4

Inflight 의 생성자에서 마지막에 makeHandler 라는 함수를 리턴하네요. props.code.text를 함수에 넘겨주는 걸 보니 실제 함수가 만들어지는 곳이 맞을 것 같네요.

더 들어가보죠.

export function makeHandler(
  scope: IConstruct,
  id: string,
  code: string,
  bindings: InflightBindings = {},
  display?: DisplayProps
): Resource {
  const clients: Record = {};


  for (const [k, v] of Object.entries(bindings)) {
    clients[k] = serializeImmutableData(v.obj);
  }


  // implements IFunctionHandler
  class Handler extends Resource {
    public readonly stateful = false;


    constructor() {
      super(scope, id);


      // pretend as if we have a field for each binding
      for (const [field, value] of Object.entries(bindings)) {
        (this as any)[field] = value.obj;
      }


      this.display.title = display?.title;
      this.display.description = display?.description;
      this.display.hidden = display?.hidden;
    }


    public _toInflight(): NodeJsCode {
      return NodeJsCode.fromInline(
        `new ((function(){
return class Handler {
  constructor(clients) {
    for (const [name, client] of Object.entries(clients)) {
      this[name] = client;
    }
  }
  ${code}
};
})())({
${Object.entries(clients)
  .map(([name, client]) => `${name}: ${client}`)
  .join(",\n")}
})`
      );
    }
  }


  const annotation: Record<string, { ops: Array }> = {};


  for (const [k, v] of Object.entries(bindings)) {
    annotation["this." + k] = { ops: v.ops ?? [] };
  }


  Handler._annotateInflight("handle", annotation);


  return new Handler();
}

<a href="https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/internal.ts#L6-L65">https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/internal.ts#L6-L65</a>

처음으로 `serializeImmutableData` 함수로 특정 값들을 보내 생성하고 있네요. 

```ts
  const clients: Record<string, string> = {};

  for (const [k, v] of Object.entries(bindings)) {
    clients[k] = serializeImmutableData(v.obj);
  }

bindings 는 컴파일된 코드에서 inflight 내부에서 바인딩 된 리소스들을 가리키고 있었죠.

 bindings: {
      bucket: {
        obj: bucket,
        ops: ["delete","get","get_json","list","public_url","put","put_json"]
      },
      counter: {
        obj: counter,
        ops: ["dec","inc","peek","reset"]
      },
    }

따라서 v.objbucket, counter 리소스들을 가리키게 되죠. serializeImmutableData 함수를 좀 더 보죠.

      // if the object is a resource (i.e. has a "_toInflight" method"), we use it to serialize
      // itself.
      if (typeof (obj as IResource)._toInflight === "function") {
        return (obj as IResource)._toInflight().text;
      }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/internal.ts#L95-L99

전달한 객체가 _toInflight 함수를 가지고 있는지 확인하고 리턴 값에서 text를 받고 있네요. _toInflight 의 타입은 Code 으로 text 가 코드의 텍스트인 것을 알 수 있겠네요.

  /**
   * Return a code snippet that can be used to reference this resource inflight.
   * @internal
   */
  _toInflight(): Code;

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/resource.ts#L51-L55

/**
 * Reference to a piece of code.
 */
export abstract class Code {
  /**
   * The language of the code.
   */
  public abstract readonly language: Language;


  /**
   * The code.
   */
  public abstract readonly text: string;


  /**
   * Generate a hash of the code contents.
   */
  public get hash(): string {
    return createHash("sha512").update(this.text).digest("hex");
  }
}

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/inflight.ts#L10-L30

위 과정들을 거친뒤 Handler 라는 클래스를 생성하면서 리턴하게 됩니다.

  // implements IFunctionHandler
  class Handler extends Resource {
    public readonly stateful = false;


    constructor() {
      super(scope, id);


      // pretend as if we have a field for each binding
      for (const [field, value] of Object.entries(bindings)) {
        (this as any)[field] = value.obj;
      }


      this.display.title = display?.title;
      this.display.description = display?.description;
      this.display.hidden = display?.hidden;
    }


    public _toInflight(): NodeJsCode {
      return NodeJsCode.fromInline(
        `new ((function(){
return class Handler {
  constructor(clients) {
    for (const [name, client] of Object.entries(clients)) {
      this[name] = client;
    }
  }
  ${code}
};
})())({
${Object.entries(clients)
  .map(([name, client]) => `${name}: ${client}`)
  .join(",\n")}
})`
      );
    }
  }


  const annotation: Record<string, { ops: Array }> = {};


  for (const [k, v] of Object.entries(bindings)) {
    annotation["this." + k] = { ops: v.ops ?? [] };
  }


  Handler._annotateInflight("handle", annotation);


  return new Handler();

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/core/internal.ts#L19-L64

거의 끝이 난 것 같습니다. 이후 이 생선한 인스턴스를 queue.onMessage 의 함수로 넘겨주고 있으니 queue.onMessage 를 살펴보죠.

  public onMessage(
    inflight: cloud.IQueueOnMessageHandler,
    props: cloud.QueueOnMessageProps = {}
  ): cloud.Function {
    const hash = inflight.node.addr.slice(-8);
    const functionHandler = convertBetweenHandlers(
      this.node.scope!, // ok since we're not a tree root
      `${this.node.id}-OnMessageHandler-${hash}`,
      inflight,
      join(__dirname, "queue.onmessage.inflight.js"),
      "QueueOnMessageHandlerClient"
    );


    const fn = Function._newFunction(
      this.node.scope!, // ok since we're not a tree root
      `${this.node.id}-OnMessage-${hash}`,
      functionHandler,
      props
    );

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/target-tf-aws/queue.ts#L43-L61

여기서 Function 리소스를 생성하고 있었군요. _newFunction 을 살펴봅시다.

  public static _newFunction(
    scope: Construct,
    id: string,
    inflight: IFunctionHandler,
    props: FunctionProps = {}
  ): Function {
    return App.of(scope).newAbstract(FUNCTION_FQN, scope, id, inflight, props);
  }

https://github.com/winglang/wing/blob/v0.8.3/libs/wingsdk/src/cloud/function.ts#L50-L57

이렇게 함수 리소스가 생성되고 있는걸 볼 수 있네요.

마지막으로

클라우드의 리소스들을 관리하는 것이 쉽지 않죠. 따라서 이러한 문제를 해결하기 위해 IaC들이 많이 생겼고, 많이 생긴 IaC들을 더 추상화시키는 형태도 발현되고 있는 것 같습니다.

클라우드가 제공하는 리소스들이 많다 보니 아직 Wing이 모든 리소스들을 대응하고 있지는 않습니다. 로드맵을 보면 열심히 하고 있는걸 볼 수 있습니다.

그 중에서 AWS App Runner나, Google Cloud의 Cloud Run 등도 Service라는 이름으로 로드맵에 있습니다. 문서가 존재해서 확인 해봤더니 아직 구현중이었습니다. ?

https://docs.winglang.io/reference/wingsdk-spec#service

https://github.com/orgs/winglang/projects/3/views/1?query=is%3Aopen+sort%3Aupdated-desc&pane=issue&itemId=19139763

확실히 매력적인 개념을 구현한 프로젝트고 조금 더 개발이 된다면, 간단하게 클라우드 설정을 할 수 있는 역할로써 한 자리를 맡을 것 같습니다.

그건 그렇고 SDK 구조가 잘 짜여져 있어서 이해하는 과정이 재밌었네요. ?

긴 글 읽어주셔서 감사합니다!