🍣

CDK for TerraformがGAになってたので触ってみた

2022/12/13に公開

本記事は Sansan Advent Calendar 2022 13日目の記事です。

TerraformをAWS CDKテイストで書ける CDK for Terraform(CDKTF) が2022/08/01にGAとなったため触ってみた内容と所感をまとめたいと思います。

CDK for Terraformとは

https://developer.hashicorp.com/terraform/cdktf

以下、ドキュメントからの引用です。

The Cloud Development Kit for Terraform (CDKTF) generates JSON Terraform configuration from code in C#, Python, TypeScript, Java, or Go, and creates infrastructure using Terraform. With CDKTF, you can use hundreds of providers and thousands of module definitions provided by HashiCorp and the Terraform community. By using your programming language of choice, you can take advantage of the features and development workflows you are familiar with.
CDKTF uses the Cloud Development Kit from AWS, which provides a set of language-native frameworks for defining infrastructure, and adapters that let underlying provisioning tools use those definitions. CDK for Terraform generates Terraform configuration to provision infrastructure with Terraform. The adapter works with existing Terraform providers and modules, and integrates with Terraform Cloud and Terraform Enterprise. CDKTF uses the core Terraform workflow, including planning and applying your infrastructure changes.

altテキスト

Install CDK for Terraform and Run a Quick Start Demo

要は

  • プログラミング言語を使ってTerraformを書くことができるよ
  • AWS CDK(Cloud Development Kit)が言語ネイティブなフレームワークと、Terraformでそれらの定義を使用するためのアダプターを提供するよ

ということです。

個人的には以下の点からTerraformとCDKの良いとこ取りをしたようなツールだと考えています。

  • 使い慣れたプログラミング言語で書ける(C#, Python, TypeScript, Java, Goをサポート)
    • Terraform独自のHCLを習得しなくてもOK
  • マルチクラウドに対応している
    • AWS CDKはCloudFormationテンプレートを生成するためAWSでしか使えなかった

CDKTFを使うことで、プログラミング言語の型や抽象化のメリットを生かしつつ、HashiCorpやTerraformコミュニティが提供する数百のプロバイダーと数千のモジュール定義を利用することができちゃいます。

GitHub ActionsでCDKTFの diff や deploy を実行するためのActionsも公開されているようです。
https://github.com/marketplace/actions/terraform-cdk-action

ということで、さっそく試してみたいと思います。

実行環境

CDKTFを使用するには以下が必要です。

  • Terraform CLI 1.1以上
  • Node.js 16以上

実際に使用した環境はこちらです。

  • Node.js 18.12.1
  • npm 8.19.2
  • Terraform 1.1.4
  • CDKTF 0.14.1
  • TypeScript 4.9.3

セットアップ

cdktf-cliをインストール

npm i --global cdktf-cli@latest

ディレクトリ作成

mkdir cdktf-sample && cd cdktf-sample

CDKTFプロジェクトの初期生成

❯ cdktf init --template="typescript"
Welcome to CDK for Terraform!

By default, cdktf allows you to manage the state of your stacks using Terraform Cloud for free.
cdktf will request an API token for app.terraform.io using your browser.

If login is successful, cdktf will store the token in plain text in
the following file for use by subsequent Terraform commands:
    /home/tsujita/.terraform.d/credentials.tfrc.json

Note: The local storage mode isn't recommended for storing the state of your stacks.

? Do you want to continue with Terraform Cloud remote state management? No
? Project Name cdktf-sample
? Project Description A simple getting started project for cdktf.
? Do you want to start from an existing Terraform project? No
? Do you want to send crash reports to the CDKTF team? See https://www.terraform.io/cdktf/create-and-deploy/configuration-file#enable-crash-reporting-for-the-cli for more information Yes

added 2 packages, and audited 57 packages in 2s

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

found 0 vulnerabilities

added 296 packages, and audited 353 packages in 24s

33 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

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

Note: You can always add providers using 'cdktf provider add' later on
? What providers do you want to use? aws
Checking whether pre-built provider exists for the following constraints:
  provider: aws
  version : latest
  language: typescript
  cdktf   : 0.14.1

Found pre-built provider.
Adding package @cdktf/provider-aws @ 11.0.5
Installing package @cdktf/provider-aws @ 11.0.5 using npm.
Package installed.

初期構築時の設定内容は以下です。

Do you want to continue with Terraform Cloud remote state management?

StateをTerraform Cloudに保存するかどうか
今回はお試しなのでNoにしました。ローカルに保存されます。

Project Name

プロジェクト名
デフォルトではcdktf initを実行したディレクトリ名が設定されます。

Project Description

プロジェクトの説明
デフォルトではA simple getting started project for cdktf.が設定されます。

Do you want to start from an existing Terraform project?

既存のTerraformプロジェクトから開始するか
今回は新規作成から試したいためNoしました。既存から読み込むパターンは後ほど紹介します。

Do you want to send crash reports to the CDKTF team?

CDKTFが異常終了した際にレポートを送るか
Yesにしました。

What providers do you want to use?

どのプロバイダーを使用するか
CDKTFでは人気のあるプロバイダーはビルド済みのパッケージとして提供されているため、使用したいプロバイダーを選択します。全プロバイダー一覧はこちら

今回はawsを選択しました。

プロバイダーはcdktf provider addコマンドで後から追加することもできます。

ディレクトリ

生成されたファイルは以下です。main.tsにリソースを追加していていきます。

.
|-- __tests__
|-- cdktf.json
|-- help
|-- jest.config.js
|-- main.ts
|-- node_modules
|-- package-lock.json
|-- package.json
|-- setup.js
`-- tsconfig.json

S3を構築してみる

S3のリソースを追加して適用してみます。

S3のリソースを追加

main.tsにコードを追加します。

import { Construct } from "constructs";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";
import { App, TerraformStack } from "cdktf";

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

    // AWSプロバイダーを東京リージョンで使用
    new AwsProvider(this, "AWS", {
      region: "ap-northeast-1",
    });

    // S3バケットを作成
    new S3Bucket(this, "sample_bucket", {
      bucket: "cdktf-sample-bucket",
    });
  }
}

const app = new App();
new MyStack(app, "cdktf-sample");
app.synth();

CDKでの書きっぷりとほぼ同じですが、スタックを作成する際にextendするクラスがTerraformStackになってます。

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

次にプロバイダーを作成する部分です。CDKではAWS前提だったのでプロバイダーに関するクラスは存在しませんでしたが、CDKTFでは使用したいプロバイダーをnewして使用することとなります。

new AwsProvider(this, "AWS", {
  region: "ap-northeast-1",
});

terraformのプロバイダー指定部分と一致します。

provider "aws" {
  region = "ap-northeast-1"
}

デプロイ

デプロイしてみます。

❯ cdktf deploy
cdktf-sample  Initializing the backend...
cdktf-sample  Initializing provider plugins...
              - Reusing previous version of hashicorp/aws from the dependency lock file
cdktf-sample  - Using previously-installed hashicorp/aws v4.45.0
cdktf-sample  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.
cdktf-sample  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:
cdktf-sample    # aws_s3_bucket.sample_bucket (sample_bucket) will be created
                + resource "aws_s3_bucket" "sample_bucket" {
                    + acceleration_status         = (known after apply)
                    + acl                         = (known after apply)
                    + arn                         = (known after apply)
                    + bucket                      = "cdktf-sample-bucket-misaosyushi"
                    + bucket_domain_name          = (known after apply)
                    + bucket_regional_domain_name = (known after apply)
                    + force_destroy               = false
                    + hosted_zone_id              = (known after apply)
                    + id                          = (known after apply)
                    + object_lock_enabled         = (known after apply)
                    + policy                      = (known after apply)
                    + region                      = (known after apply)
                    + request_payer               = (known after apply)
                    + tags_all                    = (known after apply)
                    + website_domain              = (known after apply)
                    + website_endpoint            = (known after apply)

                    + cors_rule {
                        + allowed_headers = (known after apply)
                        + allowed_methods = (known after apply)
                        + allowed_origins = (known after apply)
                        + expose_headers  = (known after apply)
                        + max_age_seconds = (known after apply)
                      }

                    + grant {
                        + id          = (known after apply)
                        + permissions = (known after apply)
                        + type        = (known after apply)
                        + uri         = (known after apply)
                      }

                    + lifecycle_rule {
                        + abort_incomplete_multipart_upload_days = (known after apply)
                        + enabled                                = (known after apply)
                        + id                                     = (known after apply)
                        + prefix                                 = (known after apply)
                        + tags                                   = (known after apply)

                        + expiration {
                            + date                         = (known after apply)
                            + days                         = (known after apply)
                            + expired_object_delete_marker = (known after apply)
                          }

                        + noncurrent_version_expiration {
                            + days = (known after apply)
                          }

                        + noncurrent_version_transition {
                            + days          = (known after apply)
                            + storage_class = (known after apply)
                          }

                        + transition {
                            + date          = (known after apply)
                            + days          = (known after apply)
                            + storage_class = (known after apply)
                          }
                      }

                    + logging {
                        + target_bucket = (known after apply)
                        + target_prefix = (known after apply)
                      }

                    + object_lock_configuration {
                        + object_lock_enabled = (known after apply)

                        + rule {
                            + default_retention {
                                + days  = (known after apply)
                                + mode  = (known after apply)
                                + years = (known after apply)
                              }
                          }
                      }

                    + replication_configuration {
                        + role = (known after apply)

                        + rules {
                            + delete_marker_replication_status = (known after apply)
                            + id                               = (known after apply)
                            + prefix                           = (known after apply)
                            + priority                         = (known after apply)
                            + status                           = (known after apply)

                            + destination {
                                + account_id         = (known after apply)
                                + bucket             = (known after apply)
                                + replica_kms_key_id = (known after apply)
                                + storage_class      = (known after apply)

                                + access_control_translation {
                                    + owner = (known after apply)
                                  }

                                + metrics {
                                    + minutes = (known after apply)
                                    + status  = (known after apply)
                                  }

                                + replication_time {
                                    + minutes = (known after apply)
                                    + status  = (known after apply)
                                  }
                              }

                            + filter {
                                + prefix = (known after apply)
                                + tags   = (known after apply)
                              }

                            + source_selection_criteria {
                                + sse_kms_encrypted_objects {
                                    + enabled = (known after apply)
                                  }
                              }
                          }
                      }

                    + server_side_encryption_configuration {
                        + rule {
                            + bucket_key_enabled = (known after apply)

                            + apply_server_side_encryption_by_default {
                                + kms_master_key_id = (known after apply)
                                + sse_algorithm     = (known after apply)
                              }
                          }
                      }

                    + versioning {
                        + enabled    = (known after apply)
                        + mfa_delete = (known after apply)
                      }

                    + website {
                        + error_document           = (known after apply)
                        + index_document           = (known after apply)
                        + redirect_all_requests_to = (known after apply)
                        + routing_rules            = (known after apply)
                      }
                  }

              Plan: 1 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"

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

見慣れたterraformの差分がでてきます。cdktf deployコマンドは裏ではterraform applyが実行されます。

Approveすると、無事にバケットが作成できました!

cdktf-sample  aws_s3_bucket.sample_bucket (sample_bucket): Creating...
cdktf-sample  aws_s3_bucket.sample_bucket (sample_bucket): Creation complete after 1s [id=cdktf-sample-bucket-misaosyushi]
cdktf-sample
              Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


No outputs found.

既存のTerraformをCDKTFに変換してみる

既存のTerraformプロジェクトから生成することもできるようなので試してみます。

cdktf initコマンドの以下の質問でYesにし、terraformプロジェクトのパスを指定します。

? Do you want to start from an existing Terraform project? Yes
? Please enter the path to the Terraform project /path/to/existing-terraform-project

今回はこのようなmain.tfを用意し読み込ませてみました。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_s3_bucket" "sample_bucket" {
  bucket = "cdktf-sample-bucket-misaosyushi2"
}

生成されるファイルは空のプロジェクトを構築するときと同じでした。
main.tsの中身を確認してみると

/*Provider bindings are generated by running cdktf get.
See https://cdk.tf/provider-generation for more details.*/
import * as aws from "./.gen/providers/aws";

// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";

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

    new aws.provider.AwsProvider(this, "aws", {
      region: "ap-northeast-1",
    });
    new aws.s3Bucket.S3Bucket(this, "sample_bucket", {
      bucket: "cdktf-sample-bucket-misaosyushi2",
    });
  }
}

const app = new App();
new MyStack(app, "cdktf-sample2");
app.synth();

ちゃんとterraformと同じS3バケットがCDKTFのコードとして生成されました。素晴らしい!

まとめ

2年ほど前から楽しみにしていたプロジェクトなのでついにGAになって嬉しいですし、とっても便利そうでワクワクしました。Terraformは便利ですが書いていてあまり楽しくないなあと個人的には感じていました。型の恩恵を生かしてTerraoformが書けるのは良い開発体験になりそうです。

Terraformのmoduleで抽象化しているリソースはCDKTFでどうやって書くのかな?と気になったんですが、jsonを読み込ませることでコードバインディングを生成してくれる的なことが書いてありました。
https://developer.hashicorp.com/terraform/cdktf/concepts/modules

実プロジェクトに取り入れるにはmodule部分のコード化は必須かなと思っているのでこのあたりも今度試してみたいと思います。

以上、CDKTFを触ってみたまとめと所感でした!

Discussion