🛠️

cdktfについて

2023/12/02に公開

はじめに

最近 Terraform をよく触っていますが冗長に記述する部分があったり、IDE周りの機能が弱く感じたため、Terraformの公式サイトでも紹介されているCDKTFを触ってみました。 https://developer.hashicorp.com/terraform/cdktf

CDKTF (CDK for Terraform) とは

CDKTFとは CDK(AWS Cloud Development Kit)を利用して Terraform コードの生成やデプロイを行えるツールです。
CDK はAWS CloudFormationのコードをTypeScriptやJavaなどの言語で記述できるツールで、最近Terraformの対応も進んできているようです。

なお、最近のTerraformの各リソースのドキュメントにもCDKTF (Typescriptなど) のドキュメントもあり、CDKTFのドキュメント不足も改善されつつあります。

terraformドキュメント

環境

  • Ubuntu 22.04 (WSL2)
  • terraform v1.6.5
  • node.js v20.10.0
  • npm 10.2.3

インストール手順

まずはcdktfのインストールをします。
https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-install

cdktfに必要要件として以下がインストールされている必要があります。

  • terraform cli: 1.2以上
  • Node.js と npm: 1.6以上

cdktfはnpmパッケージで提供されているので今回npmでインストールします。(Homebrewでもインストール可能です。)

$ npm install --global cdktf-cli@latest

インストールが正常にできているかを確認しておきます。

$ cdktf --version
0.19.1

これで利用できる状態になりました。(terraform cliもインストールしておきましょう)

今回実装する構成について

公式にAWSでのチュートリアルがあるのでそれに沿って進めようと思います。
https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-build

構築

まずcdktfに必要なファイル群を生成します。
初期化処理は cdktf init コマンドを実行します。

今回は記述言語をTypescriptで記述するため、以下のコマンドで初期化処理を行います。
(また tfstateファイルはローカルに保存するようにしています)

cdktf init --local --template="typescript" --providers="aws@~>4.0" --cdktf-version="0.19.1"

実行すると以下のような処理が出力されていきます。

$ cdktf init --local --template="typescript" --providers="aws@~>4.0" --cdktf-version="0.19.1"
Note: By supplying '--local' option you have chosen local storage mode for storing the state of your stack.
This means that your Terraform state file will be stored locally on disk in a file 'terraform.<STACK NAME>.tfstate' in the root of your project.
? Project Name <--- 任意のプロジェクト名を入力
? Project Description <--- プロジェクトの説明を記載 
? Do you want to start from an existing Terraform project? no <-- 既にあるterraformプロジェクトをインポートするかどうか(今回はno)
? Do you want to send crash reports to the CDKTF team? no <-- エラー生じた時にクラッシュレポートをCDKTFチーム送るかどうか(今回はno)

added 2 packages, and audited 57 packages in 3s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

added 313 packages, and audited 370 packages in 30s

38 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
========================================================================================================

  Your CDKTF TypeScript project is ready!

  cat help                Print this message

  Compile:
    npm run get           Import/update Terraform providers and modules (you should check-in this directory)
    npm run compile       Compile typescript code to javascript (or "npm run watch")
    npm run watch         Watch for changes and compile typescript in the background
    npm run build         Compile typescript

  Synthesize:
    cdktf synth [stack]   Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply')

  Diff:
    cdktf diff [stack]    Perform a diff (terraform plan) for the given stack

  Deploy:
    cdktf deploy [stack]  Deploy the given stack

  Destroy:
    cdktf destroy [stack] Destroy the stack

  Test:
    npm run test        Runs unit tests (edit __tests__/main-test.ts to add your own tests)
    npm run test:watch  Watches the tests and reruns them on change

  Upgrades:
    npm run upgrade        Upgrade cdktf modules to latest version
    npm run upgrade:next   Upgrade cdktf modules to latest "@next" version (last commit)

 Use Providers:

  You can add prebuilt providers (if available) or locally generated ones using the add command:
  
  cdktf provider add "aws@~>3.0" null kreuzwerker/docker

  You can find all prebuilt providers on npm: https://www.npmjs.com/search?q=keywords:cdktf
  You can also install these providers directly through npm:

  npm install @cdktf/provider-aws
  npm install @cdktf/provider-google
  npm install @cdktf/provider-azurerm
  npm install @cdktf/provider-docker
  npm install @cdktf/provider-github
  npm install @cdktf/provider-null

  You can also build any module or provider locally. Learn more https://cdk.tf/modules-and-providers

========================================================================================================

[2023-12-01T18:02:19.808] [INFO] default - Checking whether pre-built provider exists for the following constraints:
  provider: aws
  version : ~>4.0
  language: typescript
  cdktf   : 0.19.1

[2023-12-01T18:02:23.215] [INFO] default - Pre-built provider does not exist for the given constraints.
[2023-12-01T18:02:23.215] [INFO] default - Adding local provider registry.terraform.io/hashicorp/aws with version constraint ~>4.0 to cdktf.json
Local providers have been updated. Running cdktf get to update...
Generated typescript constructs in the output directory: .gen

すると次のような構成ファイルが生成されています。

cdktfファイル群

あとTypeScriptから呼び出しやすくするように下記パッケージも追加します。

npm install @cdktf/provider-aws

これで環境準備は終わりです。
main.ts を開くと次のようなTypescriptのコードなっています。

import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // define resources here
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

main.ts を開き、必要なリソースを記述していきます。

アプリケーションコードの定義

ガイドに沿ってconstructorの間にコードを記述していきます。
ガイドではEC2インスタンスを1つ立てるだけなので以下のようなコードになります。

import { Construct } from "constructs";
import { App, TerraformOutput, TerraformStack, Token } from "cdktf";

import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { DataAwsAmi } from "@cdktf/provider-aws/lib/data-aws-ami";
import { Instance } from "@cdktf/provider-aws/lib/instance";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Provider definition
    new AwsProvider(this, "aws", {
      region: "ap-northeast-1",
    });

    // EC2
    const ec2Instance = new Instance(this, "compute", {
      ami: "ami-012261b9035f8f938",
      instanceType: "t2.micro",
      tags: {
        Name: "test-instance"
      }
    });

    // Output
    new TerraformOutput(this, "public_ip", {
      value: ec2Instance.publicIp,
    });
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

記述ができたら、terraform実行用コードに生成できるか cdktf synth を実行して確認します。以下のような出力であればデプロイ可能なコードとなっています。

$ cdktf synth

Generated Terraform code for the stacks: infra

このまま続けてEC2をデプロイします。 cdktf deploy と実行します。

$ cdktf deploy

実行すると terraform apply と同様にデプロイを本当に実施するかの質問が出力されます。

Please review the diff output above for infra
❯ Approve  Applies the changes outlined in the plan.
  Dismiss
  Stop

"Approve"を選択して、処理を進めるとEC2インスタンスが作成されています。
※処理は terraform apply の処理が実行されています。

またコンソールには下記のように TerraformOutput で設定した値が出力されています。

infra  
       Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
       
       Outputs:
infra  public_ip = "54.238.105.77"

  infra
  public_ip = 54.238.105.77

デプロイ前に差分を確認したい場合は cdktf diff を使うことでterraformの差分結果が表示されます。

$ cdktf diff
cdktf diff 実行ログ

もう少し進んだ書き方

もう少しサンプルをいじってみましょう
今はAMIがハードコードになっているので、dataリソースから最新のAMIが取得できるようにしてみます。
以下のように書き換えてみました。

import { Construct } from "constructs";
import { App, TerraformOutput, TerraformStack, Token } from "cdktf";

import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { DataAwsAmi } from "@cdktf/provider-aws/lib/data-aws-ami";
import { Instance } from "@cdktf/provider-aws/lib/instance";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Provider definition
    new AwsProvider(this, "aws", {
      region: "ap-northeast-1",
      defaultTags: [
        {tags: {"ManagedBy": "CDKTF"}},
      ],
    });

    // EC2
    const amiImage = new DataAwsAmi(this, "ami", {
      filter: [
        {
          name: "architecture",
          values: ["x86_64"],
        },
        {
          name: "name",
          values: ["al2023-ami-2023*"],
        },
      ],
      mostRecent: true,
      owners: ["amazon"],
    });
    const ec2Instance = new Instance(this, "compute", {
      ami: Token.asString(amiImage.id),
      instanceType: "t2.micro",
      tags: {
        Name: "test-instance"
      }
    });

    // Output
    new TerraformOutput(this, "public_ip", {
      value: ec2Instance.publicIp,
    });
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

まず以下のコードですが、terraformでのdefault_tagsを追加しています。

    new AwsProvider(this, "aws", {
      region: "ap-northeast-1",
      defaultTags: [
        {tags: {"ManagedBy": "CDKTF"}},
      ],
    });

次にcdktfでdataリソースを呼び出し、AMIを動的に取得できるようにしています。

    const amiImage = new DataAwsAmi(this, "ami", {
      filter: [
        {
          name: "architecture",
          values: ["x86_64"],
        },
        {
          name: "name",
          values: ["al2023-ami-2023*"],
        },
      ],
      mostRecent: true,
      owners: ["amazon"],
    });

最後にAMIの値をハードコードな記述からdataリソース取得した値をセットするように変えます。

    const ec2Instance = new Instance(this, "compute", {
      ami: Token.asString(amiImage.id),
      instanceType: "t2.micro",
      tags: {
        Name: "test-instance"
      }
    });

なおここで、Token.asString() を使用しているのは、dataリソースから取得した値は構成を適用するまで値が確定せず、正しくTerraformのコードが出力されない可能性があります。
安全にTerraformのコードが出力できるように Token.asString() を使用しています。

これを cdktf deploy するとdataリソースからAMIのIDを取得して、EC2が生成されるようになります。

さいごに

さいごに後片付けとして今回作ったリソースは削除します。
コマンドは cdktf destroy となります。

$ cdktf destroy
実行ログ

こんな形でterraformの定義をTypescriptで作っていくことができます。
作っていてよかった点は、Typescriptなど既存の言語機能を使用できるため、エコシステムが整っており、コードを書いていて楽しかったです。
また今回は短いコードでしたが、リソースが複数あるようなケースはファイル構成もやりやすそうだなと感じました。

弱い点は、結局インフラは設定値が決まっていたりもするので Terraform のような宣言的記述のほうが理解しやすい人が多いのかな、というところと
事例が少ないのでドキュメントや情報が見つけにくいところかなと思います。(Terraform公式でもチュートリアルが拡充されていて改善されつつはあります)

今後も楽しみなツールで適時新しい情報を仕入れていきたいなと思います。
こういった機能がほしいなとかあればこちらの ConstructHub で公開されている機能でよりインフラ構築がやりやすくなるかもしれません。
https://constructs.dev/search?cdk=cdktf&sort=downloadsDesc&offset=0

Discussion