CDK for TerraformでGoogle Cloudのリソースを作ってみた 接触篇
クラウドエースの阿部と申します。TerraformをはじめとするIaCが大好きなエンジニアのような者です。
はじめに
2022年8月1日に「CDK for Terraform (CDKTF)」がGAリリースされました。(Hashicorpのブログ)
2年ほど前からAWS向けにコミュニティプレビューとして公開されていたフレームワークですが、しばらく前からGoogle Cloud向けにも使用できるようになっており、実際にどんな感じで使えるか見てみようと思います。
なお、筆者はTerraformのHCLについてはそれなりにコードを書ける自負はありますが、Typescriptは初級レベルなのでコーディングの程度についてはご容赦ください。
また、本記事は、Terraform自体についてある程度知っている前提で記述しています。
CDK for Terraform とは
CDK for Terraform(以降、CDKTF)とは、Terraformで使われているHCLという設定言語ではなく、プログラマが使い慣れた言語(Typescript等)でIaCを記述できるフレームワークになります。
現時点では、以下の言語に対応しています。
- Typescript
- Python
- Java
- C#
- Go
TerraformのようなIaCをやってみようとは思っているが、HCLは使いづらいしそのための学習コストを負担するのはちょっとなぁ、という人には良い選択肢になりそうです。
ただし、残念ながらTerraformでサポートしているProvider(プラグイン)の全てに対応しているわけではなく、CDKTF向けにモジュールが提供されているProviderに限定されます。
現時点では、以下のProviderがPre-Built Providerとして提供されています。
- AWS Provider
- Google Provider
- Azure Provider
- Kubernetes Provider
- Docker Provider
- GitHub Provider
- Null Provider
AWS, Azure, Google Cloudの三大クラウドに対応しているので、これらのクラウドを使っている方はちょっと興味が出てきたのではないかと思います。
CDKTFのセットアップ
前提環境
まず、CDKTFは以下がインストールされていることが前提となります。
- Terraform CLI (v1.1以降)
- Node.js (v16以降)
この記事の執筆環境は以下の通りです。
- Windows 10 Professional (Ubuntu 20.04 on WSL2)
- Terraform CLI v1.2.7 (tfenvで導入)
- Node.js v16.15.0 (nvmで導入)
また、この記事では、TypescriptによるCDKTFの使用について説明していこうと思います。
セットアップ手順
以下のコマンドで、CDKTFのコマンドライン(CLI)をインストールします。
npm install --global cdktf-cli@latest
問題無ければ、cdktf
コマンドが使えるようになります。
CDKTFでIaCしてみる
初期化の手順
最初に、cdktf init
コマンドを使ってIaCを行うプロジェクトディレクトリを初期化します。
今回はtest-project
という名前のディレクトリで作業します。
まず、以下のコマンドでディレクトリを作成して移動します。
mkdir test-project
cd test-project
次に、以下のコマンドを実行します。
cdktf init --template=typescript --local
※--template=typescript
はTypescript環境の指定、 --local
はStateファイルをローカルにする指定です。
CDKTFから4つの質問が返ってきますので、それぞれ以下の要領で入力します。
- Project Name ... プロジェクトの名前。(
package.json
のname
やmain.ts
で定義するTerraformStackクラスの内部IDで使われる) デフォルトでディレクトリ名が使用される。 - Project Description ... プロジェクトの簡潔な説明。
- Do you want to start from a Terraform project? ... Terraformコードから逆生成するケースで使用すると思われる。(動作未確認) デフォルトでNoのため、今回はデフォルトのままにします。
- Do you want to send crash reports to the CDKTF team? ... CDKTFチームにクラッシュレポートを送信するかどうか。(動作未確認) デフォルトでYesになっていますが、鬱陶しければNoでもよいと思います。
4項目の入力が完了すると、裏でnpmによるモジュールダウンロードが行われます。
完了すると、test-project
ディレクトリは以下のような状態になります。
.
├── __tests__
├── cdktf.json
├── help
├── jest.config.js
├── main.ts
├── node_modules
├── package-lock.json
├── package.json
├── setup.js
└── tsconfig.json
Typescriptで開発出来る準備が整いました!
できあがったテンプレートファイルを見てみる
開発に必要なファイルやディレクトリが自動的に生成されていますが、実際に触っていくファイルはmain.ts
になります。
cdktf init
による初期化直後の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, "test-project2");
app.synth();
Google Cloudのリソース作成に必要なモジュールをインストールする
今回は、以下のリソースを作成してみようと思います。
- VPCネットワーク
- VPCサブネットワーク
- Compute Engineインスタンス(含む、ディスク、内部アドレス)
初期状態だとGoogle Cloudリソースの定義に必要なNode.jsのモジュールがインストールされていないため、以下のコマンドを実行します。
npm install @cdktf/provider-google
Google Providerを定義する
Terraformでは、Providerを使用する際は以下のような記述が必要です。
provider "google" {}
これと同様にCDKTFでもProviderに相当するクラスのインスタンスを初期化・生成する必要があります。
まず、モジュールをインポートします。 import { App, TerraformStack } from "cdktf";
の次の行に以下を記述します。
import {GoogleProvider} from "@cdktf/provider-google";
また、 MyStackのConstructorに、以下を記述します。"myProjectID"
は、実際に使われているGoogle CloudプロジェクトのIDを記述してください。
GoogleProviderクラスの第1引数はthis
固定、第2引数は内部ID(何でもよいはずですが、ここでは"google"
を指定)、第3引数にTerraformのProvider Attributeを指定します。
new GoogleProvider(this, "google", {
project: "myProjectID",
});
ここまで記述すると以下のようになります。
import {Construct} from "constructs";
import {App, TerraformStack} from "cdktf";
import {GoogleProvider} from "@cdktf/provider-google";
class MyStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
new GoogleProvider(this, "google", {
project: "myProjectID"
});
}
}
const app = new App();
new MyStack(app, "test-project2");
app.synth();
VPCネットワークリソースを定義する
無事Providerも定義できたため、作成するリソースを定義していきましょう。
まずは、google_compute_network
に相当するリソース定義です。Terraformであれば以下のような記述になります。
resource "google_compute_network" "cdktf_network" {
name = "cdktf-network"
routing_mode = "REGIONAL"
auto_create_subnetworks = false
}
まず、クラス定義をインポートします。先ほどのProviderクラスのインポートの行を以下のように修正します。
import {GoogleProvider, ComputeNetwork} from "@cdktf/provider-google";
どうやら、provider-googleのリソースクラス名のルールとしては、Terraformのリソース名からgoogle_
を抜いて、キャメルケースにした感じになるようです。
そのため、 google_compute_network
であればComputeNetwork
、という感じです。
実際のリソース定義部分は、MyStackのConstructor内に、GoogleProviderの定義に続けて以下のように記述します。
なお、GoogleProviderの定義時とは異なり、ComputeNetworkクラスのインスタンスをnetwork変数に格納しています。これは、後でリソース参照で使用するためです。
const network = new ComputeNetwork(this, "cdktf-network", {
name: "cdktf-network",
routingMode: "REGIONAL",
autoCreateSubnetworks: false,
});
VPCサブネットワークリソースを定義する
VPCネットワーク上にサブネットワークを記述します。Terraformであれば以下のような定義です。
resource "google_compute_subnetwork" "cdktf_subnetwork" {
name = "cdktf-subnetwork"
network = google_compute_network.cdktf_network.self_link
ip_cidr_range = "10.10.0.0/16"
region = "asia-northeast1"
}
ComputeNetworkのときと同様、クラス定義をインポートします。
import {GoogleProvider, ComputeNetwork, ComputeSubnetwork} from "@cdktf/provider-google";
その後、MyStackのConstructorに以下の定義を追記します。network.selfLink
は先ほど定義したComputeNetwork
インスタンスの変数です。
こんな感じで、変数を使って参照させればOKです。
ComputeSubnetwork
クラスのインスタンスも変数に格納します。後で参照して使うためです。
const subnetwork = new ComputeSubnetwork(this, "cdktf-subnetwork", {
name: "cdktf-subnetwork",
network: network.selfLink,
ipCidrRange: "10.10.0.0/16",
region: "asia-northeast1",
});
ComputeEngineインスタンスリソースを定義する
さて、VPCネットワーク周りの定義は終わったので、ComputeEngineインスタンスにまつわるリソースの定義を行います。
インスタンス、ブートディスク、内部アドレスの3つをまとめて説明しようと思います。
Terraform HCLで記述するなら、以下のようなリソースになります。
resource "google_compute_address" "cdktf_address" {
name = "cdktf-address-internal"
subnetwork = google_compute_subnetwork.cdktf_subnetwork.self_link
region = google_compute_subnetwork.cdktf_subnetwork.region
address_type = "INTERNAL",
}
resource "google_compute_disk" "cdktf_disk" {
name = "cdktf-disk"
zone = "asia-northeast1-b"
type = "pd-standard"
size = 10
image = "ubuntu-os-cloud/ubuntu-2204-lts"
}
resource "google_compute_instance" "cdktf_instance" {
name = "cdktf-instance"
zone = "asia-northeast1-b"
machine_type = "e2-medium"
boot_disk {
auto_delete = false
source = google_compute_disk.cdktf_disk.selfLink
}
networkInterface {
network_ip = google_compute_address.cdktf_address.address
subnetwork = google_compute_subnetwork.cdktf_subnetwork.self_link
}
can_ip_forward = false
tags = ["iap"]
}
これまでのリソースと同様、モジュールのインポートを行います。
import {
GoogleProvider,
ComputeNetwork,
ComputeSubnetwork,
ComputeAddress,
ComputeDisk,
ComputeInstance,
} from "@cdktf/provider-google";
※1行が長くなったためクラス毎に改行しています
MyStackのConstructorにおけるリソース定義は以下の通りです。
const address = new ComputeAddress(this, "cdktf-address", {
name: "cdktf-address-internal",
subnetwork: subnetwork.selfLink,
region: subnetwork.region,
addressType: "INTERNAL",
});
const disk = new ComputeDisk(this, "cdktf-disk", {
name: "cdktf-disk",
zone: "asia-northeast1-b",
type: "pd-standard",
size: 10,
image: "ubuntu-os-cloud/ubuntu-2204-lts",
});
new ComputeInstance(this, "cdktf-instance", {
name: "cdktf-instace",
zone: "asia-northeast1-b",
machineType: "e2-medium",
bootDisk: {
autoDelete: false,
source: disk.selfLink
},
networkInterface: [
{
networkIp: address.address,
subnetwork: subnetwork.selfLink,
}
],
canIpForward: false,
tags: ["iap"]
});
ComputeAddress
やComputeDisk
リソースはこれまでと同じ書き方だと思います。
ComputeInstance
リソースのnetworkInterface
は、TerraformのHCLを知っているとちょっと異なる書き方をしていると思います。
HCLでは、networkInterfaceはブロックであるため中括弧({}
)で定義しますが、CDKTFにおける表現はオブジェクトのリストとして定義します。つまり、ブラケット([]
)で囲った後、オブジェクト表現である中括弧({}
)で定義します。
これは、CDKTFの方がREST APIの表現に近いので、APIを知っていれば自然な書き方になっていると思います。
全てのリソースを記述すると、main.ts
は以下のようになっているはずです。
import {Construct} from "constructs";
import {App, TerraformStack} from "cdktf";
import {
GoogleProvider,
ComputeNetwork,
ComputeSubnetwork
ComputeAddress,
ComputeDisk,
ComputeInstance,
} from "@cdktf/provider-google";
class MyStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
new GoogleProvider(this, "google", {
project: "myProjectID",
});
const network = new ComputeNetwork(this, "cdktf-network", {
name: "cdktf-network",
routingMode: "REGIONAL",
autoCreateSubnetworks: false,
});
const subnetwork = new ComputeSubnetwork(this, "cdktf-subnetwork", {
name: "cdktf-subnetwork",
network: network.selfLink,
ipCidrRange: "10.10.0.0/16",
region: "asia-northeast1",
});
const address = new ComputeAddress(this, "cdktf-address", {
name: "cdktf-address-internal",
subnetwork: subnetwork.selfLink,
region: subnetwork.region,
addressType: "INTERNAL",
});
const disk = new ComputeDisk(this, "cdktf-disk", {
name: "cdktf-disk",
zone: "asia-northeast1-b",
type: "pd-standard",
size: 10,
image: "ubuntu-os-cloud/ubuntu-2204-lts",
});
new ComputeInstance(this, "cdktf-instance", {
name: "cdktf-instace",
zone: "asia-northeast1-b",
machineType: "e2-medium",
bootDisk: {
autoDelete: false,
source: disk.selfLink
},
networkInterface: [
{
networkIp: address.address,
subnetwork: subnetwork.selfLink,
}
],
canIpForward: false,
tags: ["iap"]
});
}
}
const app = new App();
new MyStack(app, "test-project");
app.synth();
CDKTFでリソースをデプロイする
main.ts
に一通りリソースを定義できたので、実際にデプロイしていきます。
Terraformでは、以下のようなコマンドを使ってリソースデプロイしていました。
# リソースデプロイの実行計画の表示
terraform plan
# リソースデプロイ
terraform apply
# 作成したリソースの破棄
terraform destroy
CDKTFでは、上記の代わりに以下のコマンドを使用します。
# リソースデプロイの実行計画の表示
# cdktf plan でも実行可能
cdktf diff
# リソースデプロイ
# cdktf apply でも実行可能
cdktf deploy
# 作成したリソースの破棄
cdktf destroy
まずは、cdktf diff
を実行してリソース作成の実行計画を表示してみます。
基本的には内部的に実行されていると思われるterraform plan
と同等の内容が表示されます。
[2022-08-16T18:09:39.908] [INFO] default - Error reporting disabled: SENTRY_DSN not set
test-project Initializing the backend...
test-project Initializing provider plugins...
- Reusing previous version of hashicorp/google from the dependency lock file
test-project - Using previously-installed hashicorp/google v4.31.0
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
test-project Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
test-project # google_compute_address.cdktf-address (cdktf-address) will be created
+ resource "google_compute_address" "cdktf-address" {
+ address = (known after apply)
+ address_type = "INTERNAL"
+ creation_timestamp = (known after apply)
+ id = (known after apply)
+ name = "cdktf-address-internal"
+ network_tier = (known after apply)
+ project = (known after apply)
+ purpose = (known after apply)
+ region = "asia-northeast1"
+ self_link = (known after apply)
+ subnetwork = (known after apply)
+ users = (known after apply)
}
# google_compute_disk.cdktf-disk (cdktf-disk) will be created
+ resource "google_compute_disk" "cdktf-disk" {
+ creation_timestamp = (known after apply)
+ id = (known after apply)
+ image = "ubuntu-os-cloud/ubuntu-2204-lts"
+ label_fingerprint = (known after apply)
+ last_attach_timestamp = (known after apply)
+ last_detach_timestamp = (known after apply)
+ name = "cdktf-disk"
+ physical_block_size_bytes = (known after apply)
+ project = (known after apply)
+ provisioned_iops = (known after apply)
+ self_link = (known after apply)
+ size = 10
+ source_image_id = (known after apply)
+ source_snapshot_id = (known after apply)
+ type = "pd-standard"
+ users = (known after apply)
+ zone = "asia-northeast1-b"
}
test-project # google_compute_instance.cdktf-instance (cdktf-instance) will be created
+ resource "google_compute_instance" "cdktf-instance" {
+ can_ip_forward = false
+ cpu_platform = (known after apply)
+ current_status = (known after apply)
+ deletion_protection = false
+ guest_accelerator = (known after apply)
+ id = (known after apply)
+ instance_id = (known after apply)
+ label_fingerprint = (known after apply)
+ machine_type = "e2-medium"
+ metadata_fingerprint = (known after apply)
+ min_cpu_platform = (known after apply)
+ name = "cdktf-instance"
+ project = (known after apply)
+ self_link = (known after apply)
+ tags = [
+ "iap",
]
+ tags_fingerprint = (known after apply)
+ zone = "asia-northeast1-b"
+ boot_disk {
+ auto_delete = false
+ device_name = (known after apply)
+ disk_encryption_key_sha256 = (known after apply)
+ kms_key_self_link = (known after apply)
+ mode = "READ_WRITE"
+ source = (known after apply)
+ initialize_params {
+ image = (known after apply)
+ labels = (known after apply)
+ size = (known after apply)
+ type = (known after apply)
}
}
+ confidential_instance_config {
+ enable_confidential_compute = (known after apply)
}
+ network_interface {
+ ipv6_access_type = (known after apply)
+ name = (known after apply)
+ network = (known after apply)
+ network_ip = (known after apply)
+ stack_type = (known after apply)
+ subnetwork = (known after apply)
+ subnetwork_project = (known after apply)
}
+ reservation_affinity {
+ type = (known after apply)
+ specific_reservation {
+ key = (known after apply)
+ values = (known after apply)
}
}
+ scheduling {
+ automatic_restart = (known after apply)
+ instance_termination_action = (known after apply)
+ min_node_cpus = (known after apply)
+ on_host_maintenance = (known after apply)
+ preemptible = (known after apply)
+ provisioning_model = (known after apply)
+ node_affinities {
+ key = (known after apply)
+ operator = (known after apply)
+ values = (known after apply)
}
}
}
# google_compute_network.cdktf-network (cdktf-network) will be created
+ resource "google_compute_network" "cdktf-network" {
+ auto_create_subnetworks = false
+ delete_default_routes_on_create = false
+ gateway_ipv4 = (known after apply)
+ id = (known after apply)
+ internal_ipv6_range = (known after apply)
+ mtu = (known after apply)
+ name = "cdktf-network"
+ project = (known after apply)
+ routing_mode = "REGIONAL"
+ self_link = (known after apply)
}
# google_compute_subnetwork.cdktf-subnetwork (cdktf-subnetwork) will be created
+ resource "google_compute_subnetwork" "cdktf-subnetwork" {
+ creation_timestamp = (known after apply)
+ external_ipv6_prefix = (known after apply)
+ fingerprint = (known after apply)
+ gateway_address = (known after apply)
+ id = (known after apply)
+ ip_cidr_range = "10.10.0.0/16"
+ ipv6_cidr_range = (known after apply)
+ name = "cdktf-subnetwork"
+ network = (known after apply)
+ private_ipv6_google_access = (known after apply)
+ project = (known after apply)
+ purpose = (known after apply)
+ region = "asia-northeast1"
+ secondary_ip_range = (known after apply)
+ self_link = (known after apply)
+ stack_type = (known after apply)
}
Plan: 5 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: plan
To perform exactly these actions, run the following command to apply:
terraform apply "plan"
terraform plan
を実行すると、同ディレクトリ内にcdktf.out
ディレクトリが作成され、その中にTerraformStackクラスのインスタンス毎にTerraform CLIに必要なコマンド結果(terraform initで初期化したProviderプラグインファイルや、terraform planのplan結果等)が格納されるようです。
では、cdktf deploy
コマンドを実行して見ます。
terraform plan
と同様に実行計画が表示されますが、以下のようなプロンプトが表示されます。
Please review the diff output above for test-project
❯ Approve Applies the changes outlined in the plan.
Dismiss
Stop
Approve
を選択した状態でEnterを押せばデプロイが開始されます。
Dismiss
、Stop
を選択した場合はデプロイされずコマンドが終了します。
Dismiss
とStop
違いは、複数のStackを定義している場合に現れるようです。
Dismiss
の場合は、表示されているStackのみデプロイされず次のStackに移行する(ただし、Stack間で依存関係がある場合は依存するStackも実行されない)、Stop
は表示されているStack以降全て停止する、という違いのようです。(が、ちゃんとは検証してません。)
無事リソースの作成が完了すると、terraform apply
と同様に以下のようなメッセージが表示されます。
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
コマンドラインで作成結果を確認してみます。無事、作成されているようです。
$ gcloud compute networks list --filter=name:cdktf
NAME SUBNET_MODE BGP_ROUTING_MODE IPV4_RANGE GATEWAY_IPV4
cdktf-network CUSTOM REGIONAL
$ gcloud compute networks subnets list --filter=name:cdktf
NAME REGION NETWORK RANGE STACK_TYPE IPV6_ACCESS_TYPE INTERNAL_IPV6_PREFIX EXTERNAL_IPV6_PREFIX
cdktf-subnetwork asia-northeast1 cdktf-network 10.10.0.0/16 IPV4_ONLY
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
cdktf-instance asia-northeast1-b e2-medium 10.10.0.2 RUNNING
cdktf deploy
コマンドを実行すると、カレントディレクトリ内にterraform.test-project.tfstate
というファイルが作成されていました。
素のTerraformとは異なり、CDKTFはStack毎にstateを管理しているように見えるため、stateファイル名にStack名を含めていると思われます。
作成してみた感想
こうして記事にしてみると、Terraform HCLより記述量が多かったり、リソースモジュール自体のマニュアルが無かったりするため一見定義し辛いようにも思いましたが、
実際は開発環境がTypescriptに対応しているとクラスのプロパティをサジェストしてくれるので思ったよりずっと簡単でした。
筆者はIntelliJ IDEAで開発しましたが、サジェストはTerraformプラグインよりも親切な気がします。
Terraformのブロック表現に慣れていると若干違和感があるくらいで、開発フローさえ確立できればCDKTFの方が言語としての柔軟性を考慮するとトータル良い気がします。
まとめ
CDK for TerraformはGAリリースになっていますが、GitHub上のバージョンは0.12であり、まだまだ発展途上のツールなのかな、という印象です。
(特に多種多様なProviderへの対応状況等)
しかし、使い慣れたプログラミング言語を使って開発できる点、特に静的型付け言語のサジェストやLINT、あるいは、単体テストを利用できることを考慮すると、今後も研究を続けなければと思う次第です。
Discussion