CDK for TerraformでGoogle Cloudのリソースを作ってみた 発動篇
こんにちは、クラウドエースの阿部です。
前回、CDK for Terraform(以降、CDKTF)について以下のような記事を公開しました。
今回は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
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に👍を投票しておくとよいでしょう。
count
メタ引数も指定自体は可能ですが、 count.index
オブジェクトが使えませんでした。(調べた限りでは)
この件はstack overflowでも話題になっており、CDKTF 0.8.6まではエスケープハッチを使う事でcount.indexを表現できたようですが、現状はcount相当の動作をするforEach/TerraformIteratorの構文で置き換えるしかないようです。
CDKTFのIssueでCount Iteratorクラスが提案されているので、CDKTFでもcount
を使いたい方は👍を投票しておくとよいでしょう。
参考: Iterators
Terraform Workspace を使いたい
CDKTFにはTerraform Workspaceを扱う構文は存在しません。しかし、Workspaceに相当する機能としてStacksがあります。
CDKTFの環境をcdktf init
で初期化すると以下のようなmain.ts
が生成されます。
ここから、リソースはTerraformStackクラスを継承したMyStackクラスのconstructor内でリソース定義を書いていくわけです。実際にMyStackクラスをインスタンス化するのはコード下部にあるnew MyStack(app, “sample”);
で行っています。
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 deploy
とcdktf 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