CDK for Terraform で GKE をプロビジョニング

2023/09/04に公開

職場が変わったことで TypeScript に触れる機会が増えたとともに携わっているプロダクトが GKE 上で動いていました。プロダクトを知るきっかけとして、前職で培った Terraform の知識と最近勉強している TypeScript、そして GKE を触ってみるということで 「CDK for Terraform で GKE をプロビジョニングしてみる」 という自己研鑽に行き着きました。

CDK for Terraform はまだまだドキュメントが少ないこともあり、こちらの記事が今後の誰かの参考になれば嬉しいです。

CDK for Terraform が初めましての方はこちらの記事が参考になります。こちらではローカルでステートファイルを管理する形になっていますが、本記事では Cloud Storage でステートファイルを管理したいと思います。

Overview

CDKTF による GKE のプロビジョニングについて検証

  • CDKTF によって Typescript で IaC を実践しながら、GKE をプロビジョニングすることができました
  • TerraformVariable による変数化外部ファイルへの切出しによるモジュール化など試行錯誤しながら書いてみた結果もまとめてみます
    • 変数化については TerraformVariableLookup 関数で連想配列型の変数を参照する形にしてみました
    • モジュール化について外部ファイルに独自の create 関数 を定義して interface で渡すべき引数を絞り込むことで main.ts の可読性をあげるような形にしてみました

所感

もともと Terraform は書いていたので、そこまで違和感なく Terraform for CDK を書くことができました。やはりドキュメントが少ないのでベーシックな書き方しかできない点はもどかしかったですが、Typescript で IaC できるのは非常に面白いと思いました。

VS Code で JSDoc を利用できるので、どういった情報を渡せばいいかとか公式ドキュメントの参照などをスムーズに確認できるのもいいと思いましたし、Typescript のパッケージの恩恵を受けれるのもいいなと思いました。

自身の Typescript 自体の熟練度もまだまだなので、うまみを活かし切れていない部分はあるかと思いますが、今後もちょくちょくアップデートしていきたいと思います。

キーワード

GKE のプロビジョニング

まずは main.ts にプロビジョニングしたいリソース情報をごりごりハードコードしていきます。後半では TerraformVariable を使った変数の導入やモジュール化による再利用性向上といったところも挑戦したいと思います。

プロジェクトの作成

上記で紹介した記事ではステートファイルをローカルで管理するためのオプションとして --local を使用していましたが今回は最初から Cloud Storage での管理を想定したいと思います。

mkdir web-app-on-gke && cd web-app-on-gke

# ステートファイルをローカル外で管理する想定 & Google をプロバイダとするプロジェクト作成
cdktf init --template=typescript --providers google

上記を実行すると web-app-on-gke 配下にファイル/ディレクトリが作成されます。

web-app-on-gke
 |- __tests__
 |- node_modules
 |- .gitignode
 |- .npmrc
 |- cdktf.json
 |- jest.config.js
 |- main.ts
 |- package-lock.json
 |- package.json
 |- setup.js
 |- tsconfig.json

Cloud Storage でのステートファイル管理

main.tsプロバイダーステートファイルを管理するためのバックエンドのリソースを記述します。この時、Stack ごとにバックエンド(バケットやプレフィックス)を切り替える可能性を考慮しておきます。

プロジェクトID は sample-project として、事前に sample-storage という名前で Cloud Storage バケットを作成しておきます。

main.ts
import { Construct } from "constructs";
import { App, GcsBackend, TerraformStack } from "cdktf";
import { GoogleProvider } from "@cdktf/provider-google/lib/provider";

const environment: string = "dev"

interface BackendConfig {
    stateBucket: string,
    statePrefix: string,
};

class MyStack extends TerraformStack {

    constructor(scope: Construct, env: string, backendConfig: BackendConfig) {
        super(scope, env);
	
        // クラウドプロバイダーの設定
        new GoogleProvider(this, "google", {
            project: "sample-project",
        });

        // Cloud Storage でステートファイルを管理
        new GcsBackend(this, {
            bucket: backendConfig.stateBucket,
            prefix: backendConfig.statePrefix,
        });
    };
};

if (environment == "dev") {
    const app = new App();
    new MyStack(app, environment, {
        stateBucket: "sample-storage",
        statePrefix: "terraform-state/dev/web-app-on-gke"
    })
    app.synth();
};

この状態で差分を確認する cdktf diff コマンドをうちます。パッケージ周りでエラーが出たら追加で yarn add で指定されたパッケージをインストールします。

terminal
# 初回は cdktf.out のディレクトリが作成される
cdktf diff

最終的に下記の出力が出たら成功です。Cloud Storage のコンソール画面からも指定したバケットとプレフィックス配下にファイルが作成されていることを確認できます。

dev No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

VPC / Subnet / Artifact Registry etc

比較的さくっと作成できる GKE 関連外のリソースから記述していきます。パラメーターを設定するプロパティは必要に応じて増減させてください。

main.ts
・・・
import { ComputeNetwork } from "@cdktf/provider-google/lib/compute-network";
import { ComputeSubnetwork } from "@cdktf/provider-google/lib/compute-subnetwork";
import { ArtifactRegistryRepository } from "@cdktf/provider-google/lib/artifact-registry-repository";
import { ServiceAccount } from "@cdktf/provider-google/lib/service-account";
import { ArtifactRegistryRepositoryIamMember } from "@cdktf/provider-google/lib/artifact-registry-repository-iam-member";

・・・
class MyStack extends TerraformStack {

    constructor(scope: Construct, env: string, backendConfig: BackendConfig) {
        super(scope, env);

        // クラウドプロバイダーの設定
	・・・
        // Cloud Storage でステートファイルを管理
	・・・	
	
	// VPC の作成
	const network = new ComputeNetwork(this, "cdktf-vpc", {
	      name: "sample-cdktf-vpc",
	      autoCreateSubnetworks: false
	});
    
        // GKE の Worker Node がデプロイされる Subnet の作成
        new ComputeSubnetwork(this, "cdktf-subnet", {
            name: "sample-cdktf-subnet",
	    ipCidrRange: "192.168.0.0/20",
	    network: network.selfLink,
	    region: "asia-northeast1",
            privateIpGoogleAccess: true,
            logConfig: {
                flowSampling: 1.0
            },
        });

        // GKE にデプロイされるコンテナイメージが格納される Artifact Registry
        const artifactRegistry = new ArtifactRegistryRepository(this, "cdktf-artifact-registry", {
	    location: "asia-northeast1",
	    repositoryId: "sample-cdktf-gar-for-gke",
	    format: "DOCKER"
	});
	
	// GKE の Worker Node が持つサービスアカウント
	const gkeWorkerServiceAccount = new ServiceAccount(this, "cdktf-gke-worker-sa", {
	    accountId: "sample-private",
	    displayName: "sample-private",
	});

	// GKE の Worker Node が Artifact Registry に読み取りする権限付与
	new ArtifactRegistryRepositoryIamMember(this, "cdktf-gke-worker-gar-reader", {
	    repository: artifactRegistry.name,
	    location: "asia-northeast1",
	    role: "roles/artifactregistry.reader",
	    member: `serviceAccount:${gkeWorkerServiceAccount.email}`
	});
    }
}

基本的に Terraform の公式ドキュメントで定義したいリソースを検索して、先頭を小文字にしてアンダーバーの次の文字を大文字にするといった法則のもとに書き換えていきます。

下記コマンドで実際にデプロイしていきます。

terminal
# 初回コマンドで作成されたステートファイルと差分を比較
cdktf diff

# 定義したリソースをプロビジョニング
cdktf deploy

コンソール画面で各種確認して、リソースができていれば OK です。

リソースの依存関係

Terraform 同様にリソース作成順に依存関係を作りたい時があると思います。そんな時は、参照元のオブジェクト(リソースクラスのインスタンス)を利用します。公式ドキュメントの各リソースページに Attribute Reference があるので、そちらから適切な値を指定します。

今回の例でいうと、下記のような依存関係を構築しています。

  • ComputeNetworkComputeSubnetwork
  • ArtifactRegistryRepository,ServiceAccountArtifactRegistryRepositoryIamMember

GKE Cluster / Node Pool

上記と同様に各種リソースを追記していきます。
こちらでも依存関係を構築します

  • ComputeNetwork,ComputeSubnetworkContainerCluster
  • ContainerCluster,ServiceAccountContainerNodePool
main.ts
・・・
import { ContainerCluster } from "@cdktf/provider-google/lib/container-cluster";
import { ContainerNodePool } from "@cdktf/provider-google/lib/container-node-pool";

・・・
class MyStack extends TerraformStack {
    constructor(scope: Construct, id: string) {
     ・・・
        // クラウドプロバイダーの設定
        ・・・
        // Cloud Storage でステートファイルを管理
        ・・・
        // VPC の作成
        ・・・
   
        // GKE の Worker Node がデプロイされる Subnet の作成
        const subnet = new ComputeSubnetwork(this, "cdktf-subnet", {
            ・・・
        });

          // GKE にデプロイされるコンテナイメージが格納される Artifact Registry
          const artifactRegistry = new ArtifactRegistryRepository(this, "cdktf-artifact-registry", {
                ・・・
          });

	// GKE の Worker Node が持つサービスアカウント
            ・・・  
	// GKE の Worker Node が Artifact Registry に読み取りする権限付与
	・・・
	
	// GKE Cluster
	const gkeCluster = new ContainerCluster(this, "cdktf-gke-cluster", {
            name:  "sample-cdktf-gke-cluster",
	    location: "asia-northeast1-a",
	    network: network.name,
	    subnetwork: subnet.selfLink,
	    initialNodeCount: 1,
	    removeDefaultNodePool: true,
	    enableShieldedNodes: true,
	    networkingMode: "VPC_NATIVE",
	    privateClusterConfig: {
	        enablePrivateNodes: true,
		enablePrivateEndpoint: false,
		masterIpv4CidrBlock: "192.168.20.0/28",
	    },
	    enableKubernetesAlpha: false,
	    enableLegacyAbac: false,
	    ipAllocationPolicy: {
	        clusterIpv4CidrBlock: "192.168.64.0/20",
	    }
	});
	
	// GKE Cluster の Worker となる Nood Pool
	new ContainerNodePool(this, "cdktf-gker-worker", {
	    cluster: gkeCluster.id,
	    name: "sample-cdktf-node-pool",
	    location: "asia-northeast1-a",
	    nodeCount: 1,
	    autoscaling: {
		totalMaxNodeCount: 3,
		totalMinNodeCount: 1
            },
	    nodeConfig: {
	        imageType: "COS_CONTAINERD",
		diskType: "pd-standard",
		diskSizeGb: 30,
		machineType: "e2-standard-2",
		serviceAccount: gkeWorkerServiceAccount.email,
	    },
	    maxPodsPerNode: 30
        });
    };
};

作成には時間がかかりますが問題なくいけば下記のコマンドで確認できます。

# GKE Cluster の認証情報を取得
gcloud container clusters get-credentials sample-cdktf-gke-cluster --zone asia-northeast1-a --project sample-project

# ノード情報の確認
kubectl get node

Terraform Variable による変数化

Terraform では Variable を利用してパラメーターを変数にすることで抽象化などができました。CDK for Terraform でも用意されているのですが、サンプルも少なかったので構成については自己流でやってみました。

今回はリソースごとに Terraform Variable を利用した Config を設定してみます。試しに ProjectID などの共通 Config とリソースごとの Confing として Subnet を書き換えてみます。

Config ファイルを考慮したディレクトリ構成

今回は共通の Config環境ごとに変更したい Config の 2 種類を用意したいと思います。下記は、その 2 種類を共通の Config を common-config.auto.tfvas と環境ごとに変更したい Config を environments/hoge-config.tfvars とします。

Dev 環境の Config なので、dev-config.tfvars と命名します。

web-app-on-gke
 |- __tests__
 |- node_modules
 |- .gitignode
 |- .npmrc
 |- cdktf.json
 |- jest.config.js
 |- main.ts
 |- common-config.auto.tfvars <- New!!
 |- environments
     |- dev-config.tfvars <- New!!
 |- package-lock.json
 |- package.json
 |- setup.js
 |- tsconfig.json

連想配列型っぽく Config ファイルを定義

Terraform Variable で変数を 1 つ定義するならこんな感じでできるみたいです。(下記は公式ドキュメントからの引用です。)

const imageId = new TerraformVariable(this, "imageId", {
  type: "string",
  default: "ami-abcde123",
  description: "What AMI to use to create an instance",
});

new Instance(this, "hello", {
  ami: imageId.value,
  instanceType: "t2.micro",
});

しかし、変数を 1 つ 1 つ定義するとコード量が多くなるかなと思い、もう一方の連想配列型っぽく変数を定義できる方に挑戦してみました。(下記は公式ドキュメントからの引用です。)

const nodeGroupConfig = new TerraformVariable(this, "node-group-config", {
  type: VariableType.object({
    node_group_name: VariableType.STRING,
    instance_types: VariableType.list(VariableType.STRING),
    min_size: VariableType.NUMBER,
    desired_size: VariableType.NUMBER,
    max_size: VariableType.NUMBER,
  }),
  nullable: false,
  description: "Node group configuration",
});

new TerraformResource(this, "resource", {
  nodeGroupConfig: nodeGroupConfig.value,
});

このように定義した上でどのように値を渡すかでかなり試行錯誤しました。。。
ひとまず、共通 Config と Subnet Config をこのように記述してみます。

main.ts
import { Construct } from "constructs";
import { App, Fn, GcsBackend, TerraformStack, TerraformVariable, VariableType } from "cdktf";
・・・

class MyStack extends TerraformStack {
    constructor(scope: Construct, env: string, backendConfig: BackendConfig) {
        super(scope, env);

        // 共通の Config
        const commonConfig = new TerraformVariable(this, "commonConfig", {
            type: VariableType.object({
                projectId: VariableType.STRING,
                location: VariableType.STRING,
            }),
        });

        // クラウドプロバイダーの設定
        new GoogleProvider(this, "google", {
            project: Fn.lookup(commonConfig.value, "projectId", ""),
        });

        // Cloud Storage で状態管理ファイルを管理
        new GcsBackend(this, {
            bucket: backendConfig.stateBucket,
            prefix: backendConfig.statePrefix,
        });

        // Subnet Config
        const subnetConfig = new TerraformVariable(this, "subnetConfig", {
            type: VariableType.object({
                name: VariableType.STRING,
                vpcName: VariableType.STRING,
                ipCidrRange: VariableType.STRING,
            })
        })

        // GKE の Worker Node がデプロイされる Subnet
        const subnet = new ComputeSubnetwork(this, "cdktf-subnet", {
            name: Fn.lookup(subnetConfig.value, "name", ""),
            ipCidrRange: Fn.lookup(subnetConfig.value, "ipCidrRange", ""),
            network: Fn.lookup(subnetConfig.value, "vpcName", ""),
            region: Fn.lookup(commonConfig.value, "locaiton", ""),
            privateIpGoogleAccess: true,
            logConfig: {
                flowSampling: 1.0
            },
        });
	
	・・・
    };
};

Lookup 関数

Terraform で使えた Lookup 関数を CDKTF でも利用できます。

  • 第 1 引数:連想配列型の値
  • 第 2 引数:取得したい値の Key
  • 第 3 引数:第 2 引数で渡した Key がなかった時の値

下記は commonConfig の連想配列型の値から Key の projectId に設定した Value を取得する例です。

project: Fn.lookup(commonConfig.value, "projectId", "")

外部ファイルに引数を集約

外部ファイルの拡張子には 2 種類あるようです。

  • hogehoge.auto.tfvars / terraform.tfvars:自動で読み込まれる
  • hogehoge.tfvars: --var-file=hogehoge.tfvars で読み込ませる

common-config.auto.tfvars

自動で読み込まれる tfvars ファイルには共通 Config を定義してみます。
web-app-on-gke ディレクトリの直下に配置します。

common-config.auto.tfvars
commonConfig = {
    projectId = "sample-project"
    location = "asia-northeast1"
}

dev-config.tfvars

Dev 環境用の tfvars ファイルにはリソースごとの Config を定義してみます。

environments/dev-config.tfvars
subnetConfig = {
    name = "sample-cdktf-subnet"
    vpcName = "sample-cdktf-vpc"
    ipCidrRange = "192.168.0.0/20"
}

artifactRegistryConfig = {
    registryId = "sample-cdktf-gar-for-gke"
}

gkeConfig = {
    containerClusterName = "sample-cdktf-gke-cluster"
    containerClusterLocation = "asia-northeast1-a"
    containerClusterMasterIp = "192.168.20.0/28"
    containerClusterSecoundIp =  "192.168.64.0/20"
    containerClusterNodePoolName = "sample-cdktf-node-pool"
    containerClusterNodePoolServiceAccount =  "sample-private"
}

これらの変数を利用できるように main.ts を書き換えて Lookup 関数を使っていきます。

Config ファイルを読み込ませて実行

Config ファイルを読みませるには cdktf diff もしくは cdk apply--var-fie オプションを渡すことで実行可能です。

terminal
# --var-file で Config ファイルを読み込ませる
cdktf diff --var-file=./environments/dev-config.tfvars

cdktf apply --var-file=./environments/dev-config.tfvars

こうすることで、環境ごとに渡したい値を変更したいときにスムーズに切り替えができるかなと思いました!
どうでしょうかね?笑

外部ファイル切出しによるモジュール化

Terraform でもモジュール化して参照するという方法がありました、CDK for Terraform だとどうすればよいのでしょうか。

今回は試しにこんな感じにしてみました。

web-app-on-gke
 |- __tests__
 |- node_modules
 |- .gitignode
 |- .npmrc
 |- cdktf.json
 |- jest.config.js
 |- main.ts
 |- common-config.auto.tfvars
 |- environments
     |- dev-config.tfvars
 |- modules
     |- subnet.ts <- New!!
 |- package-lock.json
 |- package.json
 |- setup.js
 |- tsconfig.json

Subnet のモジュール化

Subnet のモジュールファイルを modules/subnet.ts とします。

main.ts から必要最低限の引数を受け取るように interface で定義します。

modules/subnet,ts
import { ComputeSubnetwork } from "@cdktf/provider-google/lib/compute-subnetwork";
import { Fn } from "cdktf";
import { Construct } from "constructs";

interface SubnetConfig {
    name: string,
    vpcName: string,
    ipCidrRange: string,
}

export function createComputeSubnetwork(
    scope: Construct,
    commonConfig: any,
    subnetConfig: SubnetConfig
): any {
    return new ComputeSubnetwork(scope, "cdktf-subnet", {
        name: subnetConfig.name,
        ipCidrRange: subnetConfig.ipCidrRange,
        network: subnetConfig.vpcName,
        region: Fn.lookup(commonConfig.value, "locaiton", ""),
        privateIpGoogleAccess: true,
        logConfig: {
            flowSampling: 1.0
        },
    });
};

main.ts ではこの関数を呼び出して、必要な引数を渡します。

main.ts
・・・
import { createComputeSubnetwork } from "./modules/subentwork";

・・・
class MyStack extends TerraformStack {

    constructor(scope: Construct, env: string, backendConfig: BackendConfig) {
        super(scope, env);
	・・・

        // Subnet Config
        const subnetConfig = new TerraformVariable(this, "subnetConfig", {
            type: VariableType.object({
                name: VariableType.STRING,
                vpcName: VariableType.STRING,
                ipCidrRange: VariableType.STRING,
            })
        });

        const subnet = createComputeSubnetwork(this, commonConfig, {
            name: Fn.lookup(subnetConfig.value, "name", ""),
            vpcName:  Fn.lookup(subnetConfig.value, "vpcName", ""),
            ipCidrRange: Fn.lookup(subnetConfig.value, "ipCidrRange", ""),
        });
	・・・
    };
};

このようにすることで、モジュールに渡すべき引数を interface で定義できるので main.ts で引数不足を回避できたりモジュール固有の値(subnet でいえば privateIpGoogleAccess など)をモジュール側に寄せれるので main.ts に記載すべき内容を極力減らすことができて可読性が上がったりするのかなと思いました。

ここらへんは、まだまだ模索中なので、納得いくものができたら随時更新していきたいと思います。

まとめ

CDK for Terraform で GKE をプロビジョニングするためのコードを書いてみました。
まだメジャーバーションが出ておらず、サンプルも少ないですが Typescript の恩恵を受けつつ IaC できるのは面白いなと感じました。

また、試行錯誤中ではありますが、Terraform Variable を利用した変数化の導入やモジュールとして外部ファイルに切り出して main.ts の可読性をあげるなどに挑戦してみました。

AWS CDK や Pulimi などプログミング言語による IaC は最近の風潮だと思うので、引き続きウォッチしていきたいトピックだと思っています。

もっとこうしたらどうか?といったコメントがありましたらぜひお願いします!

参考

さいごに

AWS と Google Cloud で構築したデータ基盤の開発・運用に携わっているデータエンジニアです。5 年くらい携わっていて、この業務がきっかけで Google Cloud が好きになりました。

現在は React のフロントエンジニアとして修行中です。

X では Google Cloud 関連の情報を発信をしています。

https://twitter.com/pHaya72

Discussion