🛠

CDK for TerraformでGoogle Cloudのリソースを作ってみた 発動篇

2022/09/05に公開

こんにちは、クラウドエースの阿部です。
前回、CDK for Terraform(以降、CDKTF)について以下のような記事を公開しました。
https://zenn.dev/cloud_ace/articles/cdk-for-terraform-startup

今回はTerraformで使っていた機能がCDKTFだとどの程度使えるのかについて確認していきたいと思います。

リモートバックエンドを使いたい

リモートバックエンドはTerraformのStateファイルをリモートのストレージに配置することでチーム開発を支援する機能です。
CDKTFではモジュールインポート行で、xxxBackendクラスを追加すればよいようです。
Google Cloudで開発する場合、GCSバックエンドを使用することが一般的ですが、その場合はGcsBackendを追加します。具体的には以下のように記述します。

import { App, TerraformStack, GcsBackend } from "cdktf";

次に、TerraformStackのconstructorで、以下のように宣言します。

new GcsBackend(this, {
    bucket: "your-bucket-name",
    prefix: "state-path",
});

ローカルバックエンドで開発して後からリモートバックエンドの設定を追加した場合、CDKTFは自動で移行してくれないため、手動でStateファイルを移行する必要があります。
cdktf.out/stacks ディレクトリ配下にあるstack(.terraformディレクトリ等が保管されている)ディレクトリでterraform init -migrate-stateを実行する必要があります。

参考: Remote Backends

Terraform関数(Functions)を使いたい

Terraform HCLでは、便利な関数(Functions)がありました。
CDKTFでは、NumericやStringを扱うような関数はTerraform由来ではなく、言語由来のもの(Typescriptであれば、Typescript標準のメソッド等)を使った方がよいと思いますが、Networkのcidrhost関数等、便利な関数は使いたいところです。
この場合、cdktfモジュールのFnクラスをインポートします。

import {Fn} from "cdktf";

使う時はFnクラスの静的メソッドとして以下のように呼び出します。

const subnet = new ComputeSubnetwork(this, "subnetwork", {
    name: "cdktf-network",
    region: "asia-northeast1",
    network: network.selfLink,
    ipCidrRange: "10.10.0.0/16", //この範囲からホストアドレスを作成したい
    privateIpGoogleAccess: true,
});

new ComputeAddress(this, "address", {
    name: "cdktf-address",
    region: subnet.region,
    subnetwork: subnet.selfLink,
    addressType: "INTERNAL",
	address: Fn.cidrhost(subnet.ipCidrRange, 2), //こんな感じで使える。 10.10.0.2 として出力される。
});

参考: Functions

Dataリソースを使いたい

Dataリソースを使いたい場合は、通常のリソースと同様にDataリソース用のクラスをインポートしてクラスインスタンスを作成すればよいです。
Dataリソースのクラス名はDataから始まるため、クラス名を自動補完できるIDEであればインポートするときに探しやすいと思います。

//import
import {ComputeFirewall} from "@cdktf/provider-google";
import {DataGoogleNetblockIpRanges} from "@cdktf/provider-google";

//Data Resource
const iap_netblock = new DataGoogleNetblockIpRanges(this, "iap-netblock", {
            rangeType: "iap-forwarders"
        });

//Firewall Resource
new ComputeFirewall(this, "firewall-iap", {
            name: `allow-iap-ssh-${network.name}`,
            network: network.selfLink,
            priority: 1000,
            allow: [
                {
                    ports: ["22"],
                    protocol: "tcp"
                }
            ],
            sourceRanges: iap_netblock.cidrBlocks
        });

参考: Data Sources

Terraform変数を使いたい

Terraform変数(variable)を使用する場合は、cdktfモジュールのTerraformVariableクラスをインポートして、以下のように記述します。

//import
import {TerraformVariable} from "cdktf";

//define TerraformVariable
const projectId = new TerraformVariable(this, "PROJECT_ID", {
            type: 'string',
        });

//reference TerraformVariable
new GoogleProvider(this, "google", {
            project: projectId.stringValue,
        });

Terraform CLIではvariableのdefault値がなく環境変数としても未設定の場合は、variableの入力を促すプロンプトが表示されました。
CDKTFの場合はdefault値がなく環境変数未設定の場合は実行時エラーになります。

│ Error: No value for required variable
             │
             │   on cdk.tf.json line 113, in variable:
             │  113:     "PROJECT_ID": {
             │
             │ The root module input variable "PROJECT_ID" is not set, and has no default
             │ value. Use a -var or -var-file command line argument to provide a value for
             │ this variable.

CDKTFでTerraform Variableを入力したい場合は、環境変数としてTF_VAR_{Variable_name}の形式でセットすればよいようです。

TF_VAR_PROJECT_ID="your-project-id" cdktf deploy

参考: Variables and Outputs

count, for_eachを使いたい

CDKTFでもメタ引数(Meta-Argument)である count, for_each は使用できるようです。
Terraformにおけるfor_eachは以下のように表現できます。

locals {
  subnets = [
    {
      name   = "subnet-tokyo"
      cidr   = "10.0.0.0/16"
      region = "asia-northeast1"
    },
    {
      name   = "subnet-osaka"
      cidr   = "10.1.0.0/16"
      region = "asia-northeast2"
    },
    {
      name   = "subnet-taiwan"
      cidr   = "10.2.0.0/16"
      region = "asia-east1"
    }
  ]
}

resource "google_compute_network" "network" {
  name                    = "cdktf-network"
  routing_mode            = "REGIONAL"
  auto_create_subnetworks = false
}

resource "google_compute_subnet" "subnetwork" {
  for_each                 = { for v in local.subnets : format("%s/%s", v["region"], v["name"]) => v }
  name                     = each.value["name"]
  region                   = each.value["region"]
  network                  = google_compute_network.network.self_link
  ip_cidr_range            = each.value["cidr"]
  private_ip_google_access = true
}

では、CDKTFではどうかけるかというと、ドキュメントによるときっとこんな感じになるかな、と試しに書いてみました。

const subnets = TerraformIterator.fromList([
    {
        name: "subnet-tokyo",
        cidr: "10.0.0.0/16",
        region: "asia-northeast1",
    },
    {
        name: "subnet-osaka",
        cidr: "10.1.0.0/16",
        region: "asia-northeast2",
    },
    {
        name: "subnet-taiwan",
        cidr: "10.2.0.0/16",
        region: "asia-east1",
    },
]);

const network = new ComputeNetwork(this, "network", {
    name: "cdktf-network",
    routingMode: "REGIONAL",
    autoCreateSubnetworks: false,
});

new ComputeSubnetwork(this, "subnetwork", {
    forEach: subnets,
    name: subnets.getString("name"),
    region: subnets.getString("region"),
    network: network.selfLink,
    ipCidrRange: subnets.getString("cidr"),
    privateIpGoogleAccess: true,
});

しかし、このサンプルコードはうまく動きません。以下のようなTypescriptのコンパイルエラーが出力されます。

TSError: ⨯ Unable to compile TypeScript:
    main.ts(35,17): error TS2322: Type '{ name: string; cidr: string; region: string; }' is not assignable to type 'boolean | IResolvable'.
      Object literal may only specify known properties, and 'name' does not exist in type 'IResolvable'.
    main.ts(40,17): error TS2322: Type '{ name: string; cidr: string; region: string; }' is not assignable to type 'boolean | IResolvable'.
      Object literal may only specify known properties, and 'name' does not exist in type 'IResolvable'.
    main.ts(45,17): error TS2322: Type '{ name: string; cidr: string; region: string; }' is not assignable to type 'boolean | IResolvable'.
      Object literal may only specify known properties, and 'name' does not exist in type 'IResolvable'.

うーん、何故なのか。以下のように書き換えてみました。

const subnets = new TerraformLocal(this, "subnets", [
    {
        name: "subnet-tokyo",
        cidr: "10.0.0.0/16",
        region: "asia-northeast1",
    },
    {
        name: "subnet-osaka",
        cidr: "10.1.0.0/16",
        region: "asia-northeast2",
    },
    {
        name: "subnet-taiwan",
        cidr: "10.2.0.0/16",
        region: "asia-east1",
    },
]);

const subnetIterator = TerraformIterator.fromList(subnets.asList);

const network = new ComputeNetwork(this, "network", {
    name: "cdktf-network",
    routingMode: "REGIONAL",
    autoCreateSubnetworks: false,
});

new ComputeSubnetwork(this, "subnetwork", {
    forEach: subnetIterator,
    name: subnetIterator.getString("name"),
    region: subnetIterator.getString("region"),
    network: network.selfLink,
    ipCidrRange: subnetIterator.getString("cidr"),
    privateIpGoogleAccess: true,
});

上記コードだとTypescriptのコンパイルは通りますが、やはり以下のような実行時エラーが出ます。

[2022-08-30T19:31:04.155] [ERROR] default - ╷
│ Error: Invalid for_each set argument
│
│   on cdk.tf.json line 94, in resource.google_compute_subnetwork.subnetwork:
│   94:         "for_each": "${toset(local.subnets)}",
│     ├────────────────
│     │ local.subnets is tuple with 3 elements
│
│ The given "for_each" argument value is unsuitable: "for_each" supports maps
│ and sets of strings, but you have provided a set containing type object.
cdktf-test3  ╷
             │ Error: Invalid for_each set argument
             │
             │   on cdk.tf.json line 94, in resource.google_compute_subnetwork.subnetwork (subnetwork):
             │   94:         "for_each": "${toset(local.subnets)}",
             │     ├────────────────
             │     │ local.subnets is tuple with 3 elements
             │
             │ The given "for_each" argument value is unsuitable: "for_each" supports maps
             │ and sets of strings, but you have provided a set containing type object.
             ╵

Terraform CLIからすると、MAPオブジェクトのリスト(Complex Type List)なのに、toset関数でfor_eachに入力するTerraformコードになったため、実行時エラーとなったようです。
toset関数はlist(string)のようなプリミティブ型のリストでないと上手く動作しないため、エラーになります。
どうもうまく行かないのでCDKTFのIssue等を探りましたが、どうやらComplex TypeのIteratorはうまく動作しない問題があるようです。今後の動作改善に期待します。
CDKTFでfor_eachを使いたい方は下記のIssueに👍を投票しておくとよいでしょう。

https://github.com/hashicorp/terraform-cdk/issues/2001

countメタ引数も指定自体は可能ですが、 count.index オブジェクトが使えませんでした。(調べた限りでは)
この件はstack overflowでも話題になっており、CDKTF 0.8.6まではエスケープハッチを使う事でcount.indexを表現できたようですが、現状はcount相当の動作をするforEach/TerraformIteratorの構文で置き換えるしかないようです。

https://stackoverflow.com/questions/73209878/how-to-use-count-index-with-cdktf-iterators

CDKTFのIssueでCount Iteratorクラスが提案されているので、CDKTFでもcountを使いたい方は👍を投票しておくとよいでしょう。

https://github.com/hashicorp/terraform-cdk/issues/2021

参考: Iterators

Terraform Workspace を使いたい

CDKTFにはTerraform Workspaceを扱う構文は存在しません。しかし、Workspaceに相当する機能としてStacksがあります。
CDKTFの環境をcdktf initで初期化すると以下のようなmain.tsが生成されます。
ここから、リソースはTerraformStackクラスを継承したMyStackクラスのconstructor内でリソース定義を書いていくわけです。実際にMyStackクラスをインスタンス化するのはコード下部にあるnew MyStack(app, “sample”);で行っています。

main.ts
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    // define resources here
  }
}

const app = new App();
new MyStack(app, "sample"); //これを複数回インスタンス化することで環境を複製することができる
app.synth();

つまり、この MyStackクラスのインスタンス化を複数回行えば、その分だけStackが生成され、リソースを複製できることになります。

VPCネットワークを環境毎に複製したい場合のコードを書いてみます。

import {Construct} from "constructs";
import {App, TerraformStack} from "cdktf";
import {GoogleProvider, ComputeNetwork, ComputeSubnetwork} from "@cdktf/provider-google"

interface MyStackConfig {
    environment: string;
    region?: string;
}

class MyStack extends TerraformStack {
    constructor(scope: Construct, name: string, config: MyStackConfig) {
        super(scope, name);

        new GoogleProvider(this, "google", {
            project: "your-project-id",
        });

        const network = new ComputeNetwork(this, "network", {
            name: `cdktf-network-${config.environment}`,
            routingMode: "REGIONAL",
            autoCreateSubnetworks: false,
        });

        const {region = "us-central1"} = config;
        new ComputeSubnetwork(this, "subnetwork", {
            name: `cdktf-subnetwork-${region}`,
            region: region,
            ipCidrRange: "10.10.0.0/16",
            network: network.selfLink,
            privateIpGoogleAccess: true,
        });
    }
}

const app = new App();
new MyStack(app, "sample-production", {
    environment: "production", region: "asia-northeast1"
});
new MyStack(app, "sample-development2", {
    environment: "development2", region: "asia-northeast2"
});
new MyStack(app, "sample-development", {
    environment: "development"
})
app.synth();

Terraform Workspaceでは切替で使える変数はterraform.workspace(Workspace名)のみですが、CDKTFにおけるMyStackクラスはコンストラクタ引数を追加することで自由に変数を追加することができます。
サンプルコードの例では、 MyStackConfigインターフェースを追加してMyStackクラスコンストラクタの第3引数にしています。そうすることで、terraform.workspaceに相当する第2引数name以外の様々な情報をパラメータとして使用する事が可能になります。
Terraform HCLでは、Workspace名をキーとしたLocals変数を駆使して環境毎の設定値を管理していたので、これは非常に便利ですね。

cdktf deployを実行するときは、MyStackインスタンスに付与したnameを付けます。サンプルコードの場合は sample-development, sample-development2, sample-productionの3つのStack名のいずれかを付与して実行することになります。

cdktf deploy STACK_NAME

また、Stack名にはワイルドカードを使う事ができるため、各Stackをまとめて実行したい場合は以下のようにすることが可能です。(シェルのワイルドカードグロブと混同されないようにクオーテーションすることをお勧めします)

# sample-development, sample-development2, sample-productionのdeployをまとめて実行
cdktf deploy 'sample-*'

ただし、ワイルドカードによるStack指定はcdktf deploycdktf destroyで扱う事ができ、cdktf diffはワイルドカードに対応していませんでした。 むしろ差分チェックはまとめて実行したいところなので、今後に期待です。

参考: Stacks

Terraform CLIのバージョンを固定したい

Terraform HCLで言うところの以下のような機能をCDKTFの構文で設定できるかですが、マニュアルやCommunityの情報を探った限りでは出てきませんでした。

terraform {
  requried_version = "1.2.8"
}

Terraform CLIのバージョン固定は、CI/CDツールと手元の環境を一致させて安定実行させたいケース等で有用な設定ですが、現時点ではそうしたTerraform CLIのバージョンを固定する設定はなさそうです。

まとめ

TerraformらしいコードからCDKTFに移行しようとすると、現状いくつか問題が出てしまいます。(主にfor_eachに関連する構文)
ただ、筆者もスキル的な問題でTypescriptらしいコードを書けていない面もあるため、今後もキャッチアップして解決策を模索できればと思っています。
CDKTFは強力なツールであり、こうした問題点が解決できればIaCの構成も改善できると思うので、頑張っていきたいです。

Discussion