nabeo がピーしているブログ (仮)

どーも、nabeop です

cdktf 再入門

どうも、型にハマらない人生を歩みたいけど、コードを書く時は型が欲しい id:nabeop です。

1年くらい前に cdktf に入門したあとは changelog は追っていたけど、使う機会がなかった。 ちょうど AWS アカウント単位で検証環境を複数作ることになったので、cdktf に再入門してみた。

最初に触ったときは v0.0.18 で現時点では v0.9.0 だった。

変わったところ

cdktf init 直後のディレクトリ構造がシンプルになった

cdktf init によって作られる初期状態のディレクトリ構造にちょっとした変化があった。

以下は過去のエントリに書いていた v0.0.18 時点のディレクトリ構造

$ ls -a
./                 .gitignore         main.d.ts          node_modules/      tsconfig.json
../                cdktf.json         main.js            package-lock.json
.gen/              help               main.ts            package.json
$

で、v0.9.0 では以下のようになっていた

$ ls -a
./                 .npmrc             help               node_modules/      setup.js
../                __tests__/         jest.config.js     package-lock.json  tsconfig.json
.gitignore         cdktf.json         main.ts            package.json
$ 

パッと目につくところだと、以下の3つくらい。

  • テスト用グッズが揃っている
  • .gen ディレクトリがなくなった
  • main.ts 向けの .d.ts.js がなくなった

テストの用のグッズが揃っているのはありがたい。__tests__/main.test.ts にテストの雛形があるので、ひとまずはこのファイルのコメントを読みながら必要なテストを書いて npm run test をすればテストが実行できるようになっている。v0.0.18 時点でもテストのための機能はあったけど、テスト環境は自前で構築する必要があった。

.gen ディレクトリがなくなったのは後述の provider のネイティブ化によって cdktf.jsonterraformProviders から AWS 向けのプロバイダーの指定がなくなっているからぽい。ただし、以前のエントリではあった main.tsコンパイルした結果の main.d.tsmain.js がなくなっている関係から、cdktf init したときに cdktf compile 相当の処理がなくなったからかもしれない。ただし、terraformProviders`terraformModules が空の状態で cdktf get しても .gen ディレクトリは生成されないので初期状態で不要なディレクトリが生成されないのは嬉しい。

シェルでの補完関数が提供されるようになった

cdktf completion によってシェルの補完関数が使えるようになっていた。ただし、zsh でつかってみたら cdktf completion に含まれる cdktf へのパスが絶対パスになっていて、nodenv と相性が悪かったので以下のように変更している。

_cdktf_yargs_completions()
{
  local reply
  local si=$IFS
  IFS=$'
  ' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" "$( nodenv root )/shims/cdktf" --get-yargs-completions "${words[@]}"))
  IFS=$si
  _describe 'values' reply
}
compdef _cdktf_yargs_completions cdktf

必要なコマンドは npm script に登録しているのであんまり使う機会はないけど、補完されるのは嬉しい。

よく使う (と思われる) provider がネイティブ化していた

v0.7.0 からの機能で AWS 向けの provider がネイティブ化されていた。どうやら cdktf 的には Namespaced AWS Provider というらしい。

以前は cdktf.jsonterraformProviders に使用するプロバイダーの設定を書いて、.gen ディレクトリ以下に typescript 向けのファイルを生成してから import { Ec2 } from "./.gen/provider/aws" と呼び出していた。

ネイティブ化されたので provider は npm install @cdktf/provider としてインストールして、import { Ec2 } from "@cdktf/provider-aws" と呼び出すことができる。

このおかげで pakcage.jsonpackage-lock.json に使用するプロバイダーが記述されるので、renovate などによる更新の対象になってくれた。

v0.9.0 の時点では AWS 向けプロバイダーの他に以下のプロバイダーも同様にネイティブ化されているらしい。

  • Google Cloud 向けプロバイダー (@cdktf/provider-google)
  • Azure 向けプロバイダー (@cdktf/provider-azurerm)
  • Docker 向けプロバイダー (@cdktf/provider-docker)
  • Github 向けプロバイダー (@cdktf/provider-github)
  • Null プロバイダー (@cdktf/provider-null)

今回の再入門で確認したこと

state ファイルの単位としての Stack

前回は cdktf における Stack について余り深く考えないで使っていたけど、冷静に考えたら Stack は aws-cdk というか CloudFormation 由来の用語なので、Terraform (cdktf) における Stack とは??となったので、ちゃんとみてみた。

結論をから書くと cdktf における Stack とは state の単位になる。つまり、Stack ごとに state は生成されるし、cdktf によって生成される Terraform 向けのファイル群は cdktf.out/stacks/<stack name> というディレクトリ以下に生成される。v0.0.18 時点では cdktf.out 直下に Terraform の作業ディレクトリが生成されていたので、このあたりも扱いが変わったのかもしれない。

v0.9.0 時点での sample Stack の Terraform の作業ディレクトリは以下のようになっていた。

$ ls -a cdktf.out/stacks/sample
./                   .terraform/          cdk.tf.json
../                  .terraform.lock.hcl  plan
$

つまり、cdktf-cli 経由ではなく、terraform コマンドを直接実行したいときは -chdir オプションを使えば良い。例えば、 terraform state list など cdktf-cli では実装されていない操作をしたい時は以下のようにする

terraform -chdir=cdktf.out/stacks/sample state list

cdktf における Stack については Stacks | Terraform by HashiCorp としてドキュメントがまとまっている。

Stack の持つ意味がわかったら cdktf における cross stack 参照が remote state の参照に相当するということもすんなり腹落ちする。また、v0.9.0 によって cross stack 参照がより aws-cdk ぽい感じになっていて記述しやすくなっていた。

Typescript で記述したリソースと state ファイル内のリソースの対応関係

Terraform を運用していると terraform state mvterraform import などで直接 state ファイルを書き換える必要がでくる。直接 state ファイルを書き換える場合はコード側も追従させる必要がある。当然 cdktf でも同様の作業は発生するはずなので、Typescript のコードで state 内のリソースがどのように表現されるかは知っておく必要がある。AWS-CDK だと AWS-CDK 側が CloudFormation における論理 ID や物理 ID をよしなに管理されてしまうが、同じことが cdktf でもおきると困るなーという感じです。

たとえば、EIP を確保するために以下のようなコードを書いたとする。

import { Eip } from '@cdktf/provider-aws/lib/ec2';
...
new Eip(this, 'ReservedEip', {
  vpc: true,
});

同様の定義を hcl で書くと以下のようになる。

resource "aws_eip" "ReservedEip" {
  vpc      = true
}

つまり、id の部分がそのまま id として使われる。したがって、state ファイルでは以下のようになっているはず。

{
  "mode": "managed",
  "type": "aws_eip",
  "name": "ReservedEip",
  "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
...
      },
      "sensitive_attributes": [],
      "private": "********************"
    }
  ]
}

前述の通り、state ファイルを直接変更することは稀によくあることなので、このことを頭の片隅に入れてコードを書いておいた方が良い。

Terraform Cloud が使えるようになった

これは v0.0.18 の時点から使えていたぽいけど、このときは Terraform Cloud を使っていなかったのでスルーしていた。今回は Terraform Cloud が使える環境にあったので、試してみた。

v0.1.0 の時点で Terraform Cloud Support by skorfmann · Pull Request #449 · hashicorp/terraform-cdk としてリモート実行モードでも使えるようになっていたけど、この変更を見逃していてローカル実行モードで試していた。

実際に cdktf init --template=typescript としてプロジェクトを作ると、バックエンドは remote (Terraform Cloud) を使うように生成されるが、Terraform Cloud 側はリモート実行モードとして構築されていた。

何も設定していない限り cdktf synth によって生成される terraform 向けのファイルは cdktf.out/stacks/<stack name>/ 以下に生成されるので、Terraform Cloud をリモート実行モードとするときはこのディレクトリ以下が転送されて、Terraform Cloud 側で terraform planterraform apply が実行されるんじゃないかと予想している。

hcl で書かれている terrafrom module を cdktf (typescript) で使うときに注意すること

v0.7.0 で Technical preview として導入された AWS Adapter を使えば、間接的に aws-cdk を呼び出せるので hcl で書かれている terraform module をわざわざ cdktf で使う必要はないかもしれない。

ただ、今回は AWS Adapter のことを忘れていて、hcl 向けの terraform module を使っていた。aws-cdk が提供している L2 Construct や L3 Construct による開発体験を知ってしまっているので機会があれば使ってみたい。あと、cdktf 自体が beta であると宣言しているうえで AWS Adapter だけ Technical preview として名言されているので、何かしらの罠があるんだろうと思っている。(楽しみ)

hcl 向けに提供されているモジュールを使うので .gen 以下に cdktf で使えるように変換する必要がある。hcl 製の terraform module を使う時は cdktf.jsonterraformModules に使用する terraform moudle を追記して、cdktf get を実行する必要がある。例えば、terraform-aws-modules/terraform-aws-vpc: Terraform module which creates VPC resources on AWS を使いたい場合は、cdktf.jsonterraformModules で以下のように書く。

"terraformModules": [{
  "name": "vpc",
  "source": "terraform-aws-modules/vpc/aws",
  "version": "=3.11.5"
}],

ts 側では import { Vpc } from "./.gen/moduels/vpc" として呼び出す。

使っている時に気づいた注意点としては本来は terraform module ごとに provider を指定できるはずだけど、cdktf gen によって生成された TerraformModule は provider を渡せない実装になっていた。

先ほどの terraform-aws-modules/vpc/aws によって生成された Vpc クラスを例にとると、Vpc は provider を受け付けることができる TerraformModuleクラスを extends しているが、Vpc クラスの引数である VpcOptionsTerraformModule クラスの引数である TerraformModuleOptionsextends していないので provider を渡すことができないように見えた。.gen/moduels/vpc.ts を書き換えれば動きそうな気がしたけど、そこまではやっていない。

つまり、同一 Stack の中で provider を切り替えて使用することができないので、Stack 設計時に気をつける必要がある。

また、元になった terraform module で必須となっている input が cdktf gen によって生成されたクラスでは strings | undefined となっていたり、配列で生成される output が壊れているなど気になるところが多かった。

hcl で書かれた既存の Terraform を cdktf のプロジェクトとしてインポートできるようになった

cdktf init のオプションとして --from-terraform-projectが生えていた。これは cdktf-cli の v0.5.0 で導入された機能だった。

ドキュメントを読む限り、hcl で書かれた既存の Terraform を cdktf のプロジェクトとしてインポートできるらしい。

試しに試しに手近な Terraform プロジェクトをインポートしてみたところ、いくつかのプロジェクトでは解析に失敗したが、シンプルな構造のプロジェクトでインポートに成功した。

インポート前の Terraform プロジェクトのディレクトリ構成:

$ ls -a
./                   .gitignore           .terraform.lock.hcl  main.tf              terraform.tf
../                  .terraform/          README.md            modules/
$

インポート後の cdktf プロジェクトのディレクトリ構成:

./                 .npmrc             jest.config.js     package-lock.json
../                __tests__/         main.ts            package.json
.gen/              cdktf.json         modules/           setup.js
.gitignore         help               node_modules/      tsconfig.json

インポート後の構成をみてみると、オリジナルでは複数の tf ファイルで分割していた内容が main.ts に統合されている。また、main.ts を見る限り、インポート前後で state におけるアドレスは維持されているように見えたので、そのまま使えそうな気配を感じました。

触ってみた感想

以前のエントリでは以下のように感想をまとめていた。

  • TypeScript の資産が活かせるので開発はすんなり始められそう
    • Terraform プロバイダーから半ば機械的に生成された ts ファイルをベースにしているので、HCL を書いている感はある
  • AWS CDK に慣れた身では cdktf diff の出力内容だけだと、どのような変更が行われるかわからず不安になる
    • 変更があるリソースはわかるけど、変更内容まではわからないというところが厳しかった

未整備なところもあるけど、Terraform のお作法や aws-cdk での知見を活かすための機能追加はあった。ただし、今の状態で cdktf を本番環境に導入するか?と聞かれたら、まだまだ厳しいと答えると思う。理由は

  • hcl のテンプレートエンジンとして cdktf を使うには cdktf gen によって生成されるクラスに難がある
  • aws-cdk の資産が使えそうな AWS Adapter は Technical preview になっていて本番環境で使うには不安がある

というところが大きい。

ただし、大きなプロジェクトや重要なプロジェクトではなく、小規模であったり重要度の低いプロジェクトであればパイロットプロジェクト的な立ち位置で導入してみることはできるかもしれない。例えば cdktf から Terraform への変更は state ファイルの構造を維持するように変更することは可能だろうし、いざとなたったら IaC による管理をやめてしまうということも可能だろうという見込みもある。

前回の感想でかいていた cdktf diff の出力では変更されるリソースの詳細がわからないから厳しい、というのはそのうち改善されそうだし、cdktf.out 以下に生成される terraform 向けの作業ディレクトリで terraform plan をすれば良いかという気分になっている。

いずれにせよ、これからも cdktf は追いかけていきたい。

ところで、cdktf の正式な名称はなんだろう? 開発元の HashiCorp のサイトでも「CDK for Terraform」とか「CDKTF」とか書かれていて、よくわからない。Google で "CDK for Terraform" というキーワードで検索した時と "cdktf" で検索した時を比較すると、Google 的には「CDK for Terraform」が正式名称と認識していそうな雰囲気を感じた。このあたりも正式リリースまでにはっきりしてほしいところではある。