AWS CDK vs Pulumi - Deep Dive

# 導入
こんにちは。富士通株式会社の中谷です。
AWS CDKはTypeScriptやPythonなどの一般的なプログラミング言語でIaCのコードを記述できますが、実は似たようなIaCツールとしてPulumiというものがあります。ということで、AWS CDKとPulumiの違いを調べてみます。コードの実装というよりは、動作原理などの低レイヤの話がメインです。

# 想定読者
現在AWS CDKを使っている人

# 機能比較
以下のPulumiの公式の記事[^1]**Pulumi vs AWS CDK**の中では、以下のような主張がなされています。

Pulumi vs. AWS CDK | Pulumi Docs

https://www.pulumi.com/docs/concepts/vs/cloud-template-transpilers/aws-cdk/

- AWS CDKのサポート対象はAWSのみだが、Pulumiは60以上のクラウドおよびSaaSプロバイダをサポートしている
- AWS CDKではリソースをデプロイする際に一度CloudFormationのテンプレートを生成し、デプロイの際はCloudFormationを介する必要があるが、Pulumiは各プロバイダと直接通信する
- AWS CDKはデプロイ処理がCloudFormationに依存しているため、CloudFormationと同様の利点/欠点が発生する
- AWS CDKとPulumiはどちらも単体テストを実施可能だが、Pulumiではプロバイダへの外部コールをモックするインメモリでの単体テストを実行できる。対してAWS CDKでは、生成されたCloudFormationテンプレートに対してのみテストを実行できる。

表にすると以下のような感じです。

| 機能 | Pulumi | AWS CDK      | 
| ---- | ------ | -------- | 
| 対応言語   | TypeScript, JavaScript, Python, Go, C#, F#, VB.NET, Java, YAML     | Python, TypeScript, JavaScript, Go (プレビュー), C#, Java | 
| 状態管理   | Pulumi Cloud or 自己管理     | CloudFormation       | 
| サポート対象のプロバイダ     |  すべての主要クラウドプロバイダをサポート      | AWSのみ         | 
| テスト | 単体テスト、プロパティテストなど一般的なテストをサポート | 生成されたCfnのYAML/JSONに対してアサートする単体テストのみ |
| サードパーティ製CI/CDツールのサポート | あり | AWS CodePipeline |
| 既存のリソースのインポート | インポート過程の一部としてコードを生成 | 不可 | 

それでは様々な観点で違いを詳しく見ていきます。

# Pulumiの動作原理

Pulumiはざっくりと以下のようなフローで動作します。[^2]

1. Language hostがコードからインフラリソースのあるべき状態を計算
2. 1の状態と現在の状態を比較し、リソースの作成、更新、削除の要否を決定
3. 各プロバイダを通じてリソースをデプロイし、リソースの状態を更新

図で表すと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/232d59d0-a6c8-c826-b3c8-a5511537e339.png)

出典: How Pulumi Works | Pulumi Docs

https://www.pulumi.com/docs/concepts/how-pulumi-works/

## Language host
図中のLanguage hostは、プログラムの実行や後述するDeployment Engineへのリソースの登録を行うための環境のセットアップなどを行います。Language hostは**Language Executor**と**Language Runtime**という要素から構成されます。[^2]

- Language Executor
CLIに付属する、ランタイムを起動するためのバイナリ。
- Language Runtime
プログラムの実行準備を担当するモジュール。プログラムの実行を監視しつつ、リソースのDeployment Engineへの登録要否を検出する。登録が必要な場合、Deployment Engineにリクエストをキックする。各種ランタイムはnpmの`@pulumi/pulumi`のような形で通常のパッケージとして配布される。

## Deployment Engine
図中のCLI and Engineと記述されている部分は、PulumiのCLIの中にDeployment Engineというものが内包される形になっています。Deployment Engineは、リソースの状態管理やCRUD実行の役割を担っています。上述したLanguage hostホストからリソースの登録依頼が来ると、Deployment Engineはローカルのリソースの状態をチェックした上でプロバイダと通信し、あるべき状態を実現するために必要な指令を実行します。[^2]

# 実行時の挙動
AWS CDKの場合、記述したコードは実行時にCloudFormationのテンプレートとその他のアセット（アプリにバンドルするDockerイメージやLambdaレイヤーなど）に変換されますが、Pulumiはコード実行時に、Deployment Engine（CLI）との通信が行われます。前述したように状態管理はこのDeployment Engineの内部で行われるので、AWS CDKにおけるCloudFormationの役割がPulumiのCLIに内包されているイメージです。[^3]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/b7199d48-cf8b-30a5-c84d-78e894ab7760.png)
*AWS CDKにおけるデプロイフロー*

出典: AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf


# インフラの再利用とモジュール性
AWS CDKとPulumiはいずれも、関数やクラス、モジュール、パッケージといったプログラミング言語の機能を通じて、コードの再利用が可能です。CDKの場合は抽象化の基本単位としてConstructという概念が使用されていますが、PulumiではComponent Resourceという概念によって、モジュールの階層構造の実現やカプセル化を行うことが出来ます。[^4]

ただ、AWS CDKの場合はリソースの抽象度に応じてL1~L3 Constructが存在しますが、Pulumiの場合はこのような段階的な抽象化は行われていません。一応、対応する概念としてAWS ClassicというライブラリがL1 Constructに相当し、Pulumi Crosswalk for AWSというライブラリがL2~L3 Constructに相当するイメージです。Pulumiの場合はCDKの自作Constructのように、必要に応じて自分たちの使うリソースを入れたクラスを都度作成するという開発スタイルのようです。

Pulumiで新たなクラスを自身で定義する場合、ComponentResourceというクラスを継承すると、Pulumiがリソースの状態管理や差分の確認をできるようになります。
以下はS3バケットとポリシーを含んだクラスを定義している例です。

```typescript
// Create a class that encapsulates the functionality by subclassing
// pulumi.ComponentResource.
class OurBucketComponent extends pulumi.ComponentResource {
    public bucket: aws.s3.Bucket;
    private bucketPolicy: aws.s3.BucketPolicy;

    private policies: { [K in PolicyType]: aws.iam.PolicyStatement } = {
        default: {
            Effect: "Allow",
            Principal: "*",
            Action: [
                "s3:GetObject"
            ],
        },
        locked: {
            /* ... */
        },
        permissive: {
            /* ... */
        },
    };

    private getBucketPolicy(policyType: PolicyType): aws.iam.PolicyDocument {
        return {
            Version: "2012-10-17",
            Statement: [{
                ...this.policies[policyType],
                Resource: [
                    pulumi.interpolate`${this.bucket.arn}/*`,
                ],
            }],
        }
    };

    constructor(name: string, args: { policyType: PolicyType }, opts?: pulumi.ComponentResourceOptions) {

        // By calling super(), we ensure any instantiation of this class
        // inherits from the ComponentResource class so we don't have to
        // declare all the same things all over again.
        super("pkg:index:OurBucketComponent", name, args, opts);

        this.bucket = new aws.s3.Bucket(name, {}, { parent: this });

        this.bucketPolicy = new aws.s3.BucketPolicy(`${name}-policy`, {
            bucket: this.bucket.id,
            policy: this.getBucketPolicy(args.policyType),
        }, { parent: this });

        // We also need to register all the expected outputs for this
        // component resource that will get returned by default.
        this.registerOutputs({
            bucketName: this.bucket.id,
        });
    }
}

const bucket = new OurBucketComponent("laura-bucket-1", {
    policyType: "permissive",
});
```

出典: Abstraction & Encapsulation Overview | Learn Pulumi | Pulumi

https://www.pulumi.com/learn/abstraction-encapsulation/

また、リソースを包括するモジュールの階層構造に関してですが、AWS CDKの場合は上位から下位にかけてApp→Stack→Constructという概念が存在します。これらは全てConstructクラスを基底クラスとして派生したクラス群であり、この一連の木構造をConstruct Treeと呼びます。このような分割単位を設けることで、AWS CDKではリソースの依存関係やデプロイ単位を管理しています。[^5]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/83ae7afc-cd4d-29a6-fbd4-00d66313a965.png)

出典: Boost your infrastructure with the AWS CDK | AWS News Blog
https://aws.amazon.com/jp/blogs/aws/boost-your-infrastructure-with-cdk/

一方、Pulumiにはこのような階層構造は存在しません。リソースの管理単位としてスタックという概念がありますが、これはAWS CDKにおけるスタックとは意味合いが異なります。AWS CDKの場合、スタックはConstruct Treeによって表現されるリソースの階層構造の一部という位置づけですが、Pulumiのスタックは環境（本番、開発、ステージングなど）ごとのリソースの状態を分離・管理する役割を持つにすぎません。

# テスト及び検証
AWS CDKでの単体テストはCDKのコードそのものに対してではなく、生成されたCloudFormationのテンプレートの内容に対して行われます。
テスト手法には以下のようなものがあります。
- Snapshot Test
生成されたCloudFormationテンプレートを以前のものと比較し、両者に差分があるかをテストする手法。
- Fine-grained Assertions
生成されたCloudFormationのリソースやアウトプットが、期待通りに生成されているかテストする手法。

以上のような手法のため、AWS CDKで単体テストを記述するためには、まずCDKからCloudFormationへの変換を理解する必要があります。

一方、Pulumiはインフラ定義に使用されるプログラミング言語と同じオブジェクトモデルを使用してテストを実行することが可能です。Pulumiの単体テストには以下のような手法があります。[^6]
- Unit Tests
コード内で各リソースのプロパティが適切に設定されているかをテストする手法。テスト自体はインメモリで外部へのコールをモックして行われる。
- Property Tests
実際にデプロイされたスタックの出力が特定のプロパティを満たすことを確認するテスト手法。確認が必要なプロパティはポリシーとして事前に定義しておく。
- Integration Tests
クラウド上に実際にリソースをデプロイし、エンドポイントへの疎通確認などのテストを実施する手法。リソースは一時的に作成され、テスト終了後に削除される。

AWS CDKでは、テスト時にテンプレート内のリソース間で参照を行うためにCloudFormationの組み込み関数であるRef関数を使用します。Ref関数は、特定のリソースの物理的なID（例えば、S3バケットの名前やEC2インスタンスのIDなど）を返します。
しかし、テスト時にはリソースが実際にデプロイされるわけではいないため、Ref関数が返すべき具体的なリソースIDが存在せず、Ref関数の結果を直接確認することはできません。従って、AWS CDKのテストは主に静的な検証に限定されます。

例として、以下のようなコードを考えます。
```typescript
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cdk from 'aws-cdk-lib';

export class MyStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.Bucket(this, 'MyBucket', {
      versioned: true,
    });

    new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName });
  }
}
```

このコードでは、S3バケットを作成し、そのバケット名を出力として表示しています。しかし、`bucket.bucketName`は内部的にRef関数を使用しているため、テスト実行段階では実際にどのような値が返却されるのかを確認することはできません。
そのため、以下のようにテンプレート内に出力自体が存在することを確認するテストを記述することはできますが、その出力が具体的にどのような値であるかを確認することはできません。

```typescript
import { expect as expectCDK, haveOutput } from 'aws-cdk-lib/assert-internal';
import * as cdk from 'aws-cdk-lib';
import * as MyStack from '../lib/my-stack';

test('Output BucketName', () => {
    const app = new cdk.App();
    // WHEN
    const stack = new MyStack.MyStack(app, 'MyTestStack');
    // THEN
    expectCDK(stack).to(haveOutput({
        outputName: 'BucketName',
    }));
});
```

このように、AWS CDKでは生成されるCloudFormationテンプレートが期待通りのリソースと設定を持っているかを静的に検証することが主なテスト手法となります。

一方、Pulumiでは、プログラムを実行時にプロバイダへの外部コールをモックすることで、リソースの状態と振る舞いを直接テストすることができます。これにより、リソース間の依存関係やリソースの設定が正しく機能していることを確認できます。以下は、S3バケットの名前が期待したものと一致することを確認するテストコードの例です。

```typescript
import * as pulumi from "@pulumi/pulumi";
import * as infra from "./index";
import { assert } from "chai";

describe("Infrastructure", function() {
    it("should output the correct bucket name", async function() {
        const bucketName = await pulumi.output(infra.bucket.id).apply(id => id);
        assert.equal(bucketName, "expectedBucketName");
    });
});
```

また、Pulumiのテストフレームワークでは、リソースの作成および更新がプログラム内部でどのように実行されるかを詳細に検証することが可能です。これにより、AWS CDKのようにCloudFormationテンプレートに対する静的な検証だけでなく、プログラムの動的な挙動も確認することができます。

# アプリケーションのコードへの埋め込み
Pulumi独自の機能として、Automation API[^7]というものを使用すると、プログラム内でPulumiの操作を実行することができます。具体的には、リソースのデプロイ（`pulumi up`）やスタックの作成（`pulumi stack init`）などの通常CLIを経由して行う操作が、プログラム内部からライブラリを通じて実行可能となります。

CI/CDを例として考えると、通常はパイプラインの定義をYAMLで記述し、実行する処理もYAML内にbashをインラインで記述するとったケースが多いと思います。しかし、Automation APIを使用するとパイプライン内の処理をプログラムに移譲し、ビルドやデプロイ処理をプログラム内の条件分岐やエラーハンドリング等でより詳細に制御するといったことが可能です。

私はこのAutomation APIのユースケースについてまだあまりイメージがついていないですが、従来のIaCツールよりもさらに柔軟なデプロイ方法を実現できそうです。
以下のブログでは、PythonのFlaskで立てたサーバにリクエストを送信すると、リクエスト内容に応じて動的にS3バケットが作成されるといった例が記載されています。

「Pulumi Automation API」でPulumi CLIの機能をコード化しよう | Think IT（シンクイット）

https://thinkit.co.jp/article/22313



# まとめ
Pulumiの公式資料を基に書いたのでややPulumiびいきなところはあると思いますが、CDKとの違いが理解出来て面白かったです。

CDKは中間にCloudFormationを挟むことで、スタック間参照のようなCloudFormation固有の問題が発生したり、デバッグ時にもエラーの原因調査の際にCloudformationの知識が必要だったりする部分が個人的にやや煩雑な印象でした。PulumiだとCloudformationを介さずに直接AWSや他のクラウドのAPIを呼び出すことが出来るので、デプロイ時の挙動がすっきりしそうです。

現状ではCDKの方がコミュニティが盛んな印象（IaCにPulumiを使ってる例を自分はほぼ見たことがない）ですが、今後の動きに注目したいです。 

# 参考文献
[^1]: Pulumi Docs. "AWS CDK". 2023. https://www.pulumi.com/docs/concepts/vs/cloud-template-transpilers/aws-cdk/. (参照 2023-12-01).

[^2]: Pulumi Docs. "How Pulumi Works". 2023. https://www.pulumi.com/docs/concepts/how-pulumi-works/. (参照 2023-12-01).

[^3]: AWS Black Belt Online Seminar. "AWS Cloud Development Kit (CDK)". 2023. https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf. (参照 2023-12-01).

[^4]: Pulumi Docs. "Abstraction & Encapsulation Overview". 2023. https://www.pulumi.com/learn/abstraction-encapsulation/. (参照 2023-12-01).

[^5]: AWS News Blog. "Boost your infrastructure with the AWS CDK". 2018. https://aws.amazon.com/jp/blogs/aws/boost-your-infrastructure-with-cdk/. (参照 2023-12-01).

[^6]: Pulumi Docs. "Testing Pulumi Programs". 2023. https://www.pulumi.com/docs/using-pulumi/testing/. (参照 2023-12-01).

[^7]: Pulumi Docs. "Embedding Pulumi". 2023. https://www.pulumi.com/learn/embedding-pulumi/. (参照 2023-12-01).