CDK for Terraform で GKE をプロビジョニング
職場が変わったことで 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
による変数化や外部ファイルへの切出しによるモジュール化など試行錯誤しながら書いてみた結果もまとめてみます- 変数化については
TerraformVariable
とLookup
関数で連想配列型の変数を参照する形にしてみました - モジュール化について外部ファイルに独自の
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 バケットを作成しておきます。
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
で指定されたパッケージをインストールします。
# 初回は 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 関連外のリソースから記述していきます。パラメーターを設定するプロパティは必要に応じて増減させてください。
・・・
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 の公式ドキュメントで定義したいリソースを検索して、先頭を小文字にしてアンダーバーの次の文字を大文字にするといった法則のもとに書き換えていきます。
下記コマンドで実際にデプロイしていきます。
# 初回コマンドで作成されたステートファイルと差分を比較
cdktf diff
# 定義したリソースをプロビジョニング
cdktf deploy
コンソール画面で各種確認して、リソースができていれば OK です。
リソースの依存関係
Terraform 同様にリソース作成順に依存関係を作りたい時があると思います。そんな時は、参照元のオブジェクト(リソースクラスのインスタンス)を利用します。公式ドキュメントの各リソースページに Attribute Reference があるので、そちらから適切な値を指定します。
今回の例でいうと、下記のような依存関係を構築しています。
-
ComputeNetwork
→ComputeSubnetwork
-
ArtifactRegistryRepository
,ServiceAccount
→ArtifactRegistryRepositoryIamMember
GKE Cluster / Node Pool
上記と同様に各種リソースを追記していきます。
こちらでも依存関係を構築します
-
ComputeNetwork
,ComputeSubnetwork
→ContainerCluster
-
ContainerCluster
,ServiceAccount
→ContainerNodePool
・・・
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(公式ドキュメント)
今回はリソースごとに 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 をこのように記述してみます。
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
ディレクトリの直下に配置します。
commonConfig = {
projectId = "sample-project"
location = "asia-northeast1"
}
dev-config.tfvars
Dev 環境用の tfvars ファイルにはリソースごとの Config を定義してみます。
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
オプションを渡すことで実行可能です。
# --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
で定義します。
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
ではこの関数を呼び出して、必要な引数を渡します。
・・・
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 は最近の風潮だと思うので、引き続きウォッチしていきたいトピックだと思っています。
もっとこうしたらどうか?といったコメントがありましたらぜひお願いします!
参考
- 実践! CDK for Terraform #1 導入
- CDK for Terraform で Google Cloud のリソースを作ってみた 発動編
- CDK for Terraform 入門
- TypeScript で cdktf
さいごに
AWS と Google Cloud で構築したデータ基盤の開発・運用に携わっているデータエンジニアです。5 年くらい携わっていて、この業務がきっかけで Google Cloud が好きになりました。
現在は React のフロントエンジニアとして修行中です。
X では Google Cloud 関連の情報を発信をしています。
Discussion