CDK for Terraform を理解する

Koki Sato

2022.10.6

はじめに

基本的な使い方をまとめてみました。

CDK for Terraform Is Now Generally Available

今回は TypeScript を使っている前提で記述するため、他の言語を利用する場合は適宜読み替えてください。

CDK for Terraform とは

CDK for Terraform | Terraform by HashiCorp

CDK for Terraform を使うと使い慣れたプログラミング言語を使ってインフラの定義やプロビジョニングを行うことができます。

これにより HCL を学ぶことなく Terraform エコシステム全体にアクセスでき、またテストや依存関係管理などの既存のツールチェインの力を活用することができます。

CDK for Terraform では 2022年08月29日現在、次の言語をサポートしています。

  • TypeScript
  • Python
  • Java
  • C#
  • Go

言語によっては実験的な新機能が利用できない場合もあります。

( CDK for Terraform 自身が TypeScript で作られているからか、 TypeScript のサポートが一番手厚い印象を受けました。 )

CDK for Terraform では Terraform Registry で利用可能な全ての Terraform プロバイダとモジュールを利用できます。また、 Terraform Cloud, Terraform Enterprise, Sentinel なども利用可能です。

準備

前提条件

  • Terraform CLI ( v1.1 以上 )
  • Node.js ( v16 以上 )

cdktf cli をインストール

CDK for Terraform は cdktf cli を使ってさまざまなオペレーションを実行します。
cdktf cli は npm でインストールすることができます。

# 安定版リリース
$ npm install --global cdktf-cli@latest

# 開発版リリース
$ npm install --global cdktf-cli@next

使い方

cdktf init – プロジェクトを作成

cdktf init でプロジェクトを作成することができます。

$ cdktf init --template=typescript --local
  • --template でテンプレートを指定します。
    用意されているテンプレートは下記ページから参照できます。
    terraform-cdk/packages/cdktf-cli/templates at main · hashicorp/terraform-cdk
  • --local で tfstate をローカルで管理することを指定します。
    ・Terraform Cloud を利用する場合はこのオプションは不要です。
    ・AWS S3 などによるリモートステートを利用する場合もここでは --local を指定します。

実行すると対話形式でさまざまな設定をしていきます。

  • プロジェクト名
  • プロジェクトの説明
  • 既存の Terraform プロジェクトを利用するかどうか ( 2022年08月29日現在 TypeScript のみ )
    利用する場合、 Terraform プロジェクトへのパス
  • クラッシュレポートを送信するかどうか
? Project Name
? Project Description
? Do you want to start from an existing Terraform project?
? Please enter the path to the Terraform project
? 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

プロジェクトの作成が完了するとカレントディレクトリに様々なファイルが作成されます。

$ tree -aFL 1
./
├── .gitignore
├── .npmrc
├── __tests__/
├── cdktf.json
├── help
├── jest.config.js
├── main.ts
├── node_modules/
├── package-lock.json
├── package.json
├── setup.js
└── tsconfig.json

2 directories, 10 files

インフラリソースの定義は主に main.ts に記述していきます。
CDK for Terraform の設定は cdktf.json で行います。

cdktf.json の例

{
  "language": "typescript",
  "app": "npx ts-node main.ts",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "sendCrashReports": "false",
  "terraformProviders": [],
  "terraformModules": [],
  "context": {
    "excludeStackIdFromLogicalIds": "true",
    "allowSepCharsInLogicalIds": "true"
  }
}

cdktf.json の詳細につきましては下記ドキュメントをご参照ください。
Configuration | Terraform by HashiCorp

cdktf provider add – プロバイダを追加する

cdktf provider add でプロバイダを追加することができます。
例えば aws プロバイダを追加する場合は次のように実行します。

$ cdktf provider add "aws@~>4.0"

cdktf provider add を実行すると、まずビルド済みのパッケージ ( 後述 ) が存在するかどうかチェックされます。
ビルド済みのパッケージが見つからなかった場合は cdktf.jsonterraformProviders にプロバイダ情報が追記され、 .gen/providers/{プロバイダ名}/ ディレクトリ以下にコードが生成されます。

次の例では sops プロバイダ を追加しています。

$ cdktf provider add "carlpett/sops@~>0.7"
Checking whether pre-built provider exists for the following constraints:
  provider: carlpett/sops
  version : ~>0.7
  language: typescript
Pre-built provider does not exist for the given constraints.
Adding local provider registry.terraform.io/carlpett/sops with version constraint ~>0.7 to cdktf.json
Generated typescript constructs in the output directory: .gen

cdktf.jsonterraformProvidersに sops プロバイダの情報が追記されます。

// cdktf.json

{
  // ...省略

  "terraformProviders": [
    "carlpett/sops@~>0.7"
  ],

  // ...省略
}

.gen/providers/sops/ ディレクトリ以下にコードが生成されています。

$ tree -aF .gen/providers
.gen/providers/
└── sops/
    ├── data-sops-external.ts
    ├── data-sops-file.ts
    ├── index.ts
    └── sops-provider.ts

1 directory, 4 files

生成されたコードは import して使用できます。

// main.ts

import { DataSopsFile } from "./.gen/providers/sops";

// ...省略

// data "sops_file" "some_file" {
//   source_file = "path/to/SOURCE_FILE"
// }
new DataSopsFile(stack, "some_file", {
  sourceFile: "path/to/SOURCE_FILE"
});

// ...省略

大きなスキーマを持つプロバイダの場合はコードを生成するために数分かかることがあります。

そのため、よく使われるプロバイダはビルド済みのパッケージとして公開されています。

全てのビルド済みパッケージは下記ページから参照できます。

HashiCorp
ビルド済みのパッケージが見つかった場合はそのパッケージがインストールされます。

次の例では aws プロバイダを追加しています。
aws プロバイダの場合はビルド済みの @cdktf/provider-aws パッケージが用意されているので、こちらがインストールされます。

$ cdktf provider add "aws@~>4.0"
Checking whether pre-built provider exists for the following constraints:
  provider: aws
  version : ~>4.0
  language: typescript
  cdktf   : 0.12.1

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

この場合はパッケージがインストールされるだけで、 cdktf.json は更新されず、 .gen/providers/ ディレクトリ以下にコードが生成されることもありません。
また、ビルド済みのパッケージは cdktf provider add を使わずに npm install でインストールすることもできます。

$ npm install @cdktf/provider-aws

インフラリソースを定義する

main.ts にコードを記述してインフラリソースを定義していきます。
CDK for Terraform ではスタックという単位でインフラリソースを定義していきます。
プロビジョニングはスタックごとに実行することができ、 tfstate ファイルもスタックごとに分けて管理されます。
スタックを定義するには TerraformStack クラスを継承したクラスを用意します。

次の例では S3 バケットを作成するスタックをひとつだけ作成しています。

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

// スタックを定義
class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // provider "aws" {
    //   region = "us-east-1"
    // }
    new AwsProvider(this, "aws", {
      region: "us-east-1",
    });

    // resource "aws_s3_bucket" "example_bucket" {
    //   bucket_prefix = "example-"
    // }
    new s3.S3Bucket(this, "example_bucket", {
      bucketPrefix: "example-",
    });
  }
}

const app = new App();

// スタックを作成
new MyStack(app, "example-stack");

app.synth();

スタックは複数作成することもできます。

次の例では環境ごとにスタックを作成しています。

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

// スタックを定義
class MyStack extends TerraformStack {
  // スタックのコンストラクタで環境を指定できるようにする
  constructor(scope: Construct, id: string, env: "dev" | "stg" | "prd") {
    super(scope, id);

    new AwsProvider(this, "aws", {
      region: "us-east-1",
    });

    new s3.S3Bucket(this, "example_bucket", {
      bucketPrefix: `example-${env}-`,
      tags: {
        environment: env,
      },
    });
  }
}

const app = new App();

// 環境ごとにスタックを作成
new MyStack(app, "example-stack-dev", "dev");
new MyStack(app, "example-stack-stg", "stg");
new MyStack(app, "example-stack-prd", "prd");

app.synth();

他にも用途ごとにスタックを作成したり、スタック間の依存関係を管理したりすることもできます。
詳しくは下記ドキュメントをご参照ください。
Stacks | Terraform by HashiCorp

インフラリソースをプロビジョニングする

cdktf cli を使用して定義したインフラリソースをプロビジョニングすることができます。

cdktf diff – 差分を表示

cdktf diff ( もしくは cdktf plan ) で差分を表示することができます ( = terraform plan ) 。

  • 出力例
$ cdktf diff
example-stack  Initializing the backend...
example-stack
               Successfully configured the backend "local"! Terraform will automatically
               use this backend unless the backend configuration changes.
example-stack  Initializing provider plugins...
               - Finding hashicorp/aws versions matching "4.27.0"...
example-stack  - Using hashicorp/aws v4.27.0 from the shared cache directory
example-stack  Terraform has created a lock file .terraform.lock.hcl to record the provider
               selections it made above. Include this file in your version control repository
               so that Terraform can guarantee to make the same selections by default when
               you run "terraform init" in the future.
example-stack  Terraform has been successfully initialized!
example-stack
               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.
example-stack  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:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be created
                 + resource "aws_s3_bucket" "example_bucket" {
               + acceleration_status         = (known after apply)
               + acl                         = (known after apply)
               + arn                         = (known after apply)
               + bucket                      = (known after apply)
               + bucket_domain_name          = (known after apply)
               + bucket_prefix               = "example-"
               + 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.

               ─────────────────────────────────────────────────────────────────────────────
example-stack  Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"

cdktf deploy – プロビジョニングを実行

cdktf deploy ( もしくは cdktf apply ) で定義に合わせてインフラリソースをプロビジョニングします ( = terraform apply ) 。

  • 出力例
$ cdktf deploy
example-stack  Initializing the backend...
example-stack  Initializing provider plugins...
               - Reusing previous version of hashicorp/aws from the dependency lock file
example-stack  - Using previously-installed hashicorp/aws v4.27.0

               Terraform has been successfully initialized!
example-stack
               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.
example-stack  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:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be created
                 + resource "aws_s3_bucket" "example_bucket" {
               + acceleration_status         = (known after apply)
               + acl                         = (known after apply)
               + arn                         = (known after apply)
               + bucket                      = (known after apply)
               + bucket_domain_name          = (known after apply)
               + bucket_prefix               = "example-"
               + 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.

example-stack
               ─────────────────────────────────────────────────────────────────────────────

               Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"
example-stack  aws_s3_bucket.example_bucket (example_bucket): Creating...
example-stack  aws_s3_bucket.example_bucket (example_bucket): Creation complete after 5s [id=example-20220828121939889900000001]
example-stack
               Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


No outputs found.

実行すると ApproveDismissStop の選択肢が表示されます。

方向キーで選択し、 Enter キーで決定します。

Please review the diff output above for {スタック名}
❯ Approve
  Dismiss
  Stop
選択肢説明
Approve現在のスタックのプロビジョニングを実行する。
Dismiss現在のスタックのプロビジョニングを中止し、次のスタックの確認に進む。また、中止したスタックに依存しているスタックのプロビジョニングも中止される。
Stop以降の全てのスタックのプロビジョニングを中止する。

--auto-approve フラグを指定すると確認をスキップして全てのスタックのプロビジョニングが実行されます。

cdktf destroy – リソースを削除

cdktf destroy でインフラリソースを削除します ( = terraform destroy ) 。

  • 出力例
$ cdktf destroy --auto-approve
example-stack  Initializing the backend...
example-stack  Initializing provider plugins...
               - Reusing previous version of hashicorp/aws from the dependency lock file
example-stack  - Using previously-installed hashicorp/aws v4.27.0
example-stack  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.
example-stack  aws_s3_bucket.example_bucket (example_bucket): Refreshing state... [id=example-20220828121939889900000001]
example-stack  Terraform used the selected providers to generate the following execution
               plan. Resource actions are indicated with the following symbols:
               - destroy

               Terraform will perform the following actions:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be destroyed
                 - resource "aws_s3_bucket" "example_bucket" {
               - arn                         = "arn:aws:s3:::example-20220828121939889900000001" -> null
               - bucket                      = "example-20220828121939889900000001" -> null
               - bucket_domain_name          = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - bucket_prefix               = "example-" -> null
               - bucket_regional_domain_name = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - force_destroy               = false -> null
               - hosted_zone_id              = "**************" -> null
               - id                          = "example-20220828121939889900000001" -> null
               - object_lock_enabled         = false -> null
               - region                      = "us-east-1" -> null
               - request_payer               = "BucketOwner" -> null
               - tags                        = {} -> null
               - tags_all                    = {} -> null

               - grant {
               - id          = "****************************************************************" -> null
               - permissions = [
               - "FULL_CONTROL",
               ] -> null
               - type        = "CanonicalUser" -> null
               }

               - versioning {
               - enabled    = false -> null
               - mfa_delete = false -> null
               }
               }

               Plan: 0 to add, 0 to change, 1 to destroy.

               ─────────────────────────────────────────────────────────────────────────────

               Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"
example-stack  aws_s3_bucket.example_bucket (example_bucket): Refreshing state... [id=example-20220828121939889900000001]
example-stack  Terraform used the selected providers to generate the following execution
               plan. Resource actions are indicated with the following symbols:
               - destroy

               Terraform will perform the following actions:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be destroyed
                 - resource "aws_s3_bucket" "example_bucket" {
               - arn                         = "arn:aws:s3:::example-20220828121939889900000001" -> null
               - bucket                      = "example-20220828121939889900000001" -> null
               - bucket_domain_name          = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - bucket_prefix               = "example-" -> null
               - bucket_regional_domain_name = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - force_destroy               = false -> null
               - hosted_zone_id              = "**************" -> null
               - id                          = "example-20220828121939889900000001" -> null
               - object_lock_enabled         = false -> null
               - region                      = "us-east-1" -> null
               - request_payer               = "BucketOwner" -> null
               - tags                        = {} -> null
               - tags_all                    = {} -> null

               - grant {
               - id          = "****************************************************************" -> null
               - permissions = [
               - "FULL_CONTROL",
               ] -> null
               - type        = "CanonicalUser" -> null
               }

               - versioning {
               - enabled    = false -> null
               - mfa_delete = false -> null
               }
               }

               Plan: 0 to add, 0 to change, 1 to destroy.

example-stack  aws_s3_bucket.example_bucket (example_bucket): Destroying... [id=example-20220828121939889900000001]
example-stack  aws_s3_bucket.example_bucket (example_bucket): Destruction complete after 1s
example-stack
               Destroy complete! Resources: 1 destroyed.

こちらも cdktf deploy と同様に ApproveDismissStop の選択肢が表示され、 --auto-approve フラグでそれらをスキップすることができます。

Terraform Module を利用する

CDK for Terraform では、 Terraform RegistryGitHub にある Terraform Module を使うこともできます。
まず cdktf.jsonterraformModules に利用するモジュール情報を追記します。

次の例では、 aws の vpc モジュールとローカルに存在する local-module モジュールを追記しています。

// cdktf.json

{
  // ...省略

  "terraformModules": [
    // https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws
    {
      "name": "vpc",
      "source": "terraform-aws-modules/vpc/aws",
      "version": "~> 3.0"
    },
    // ローカルのモジュールを指定することも可能
    {
      "name": "local-module",
      "source": "./path/to/local-module"
    }
  ],

  // ...省略
}

この状態で cdktf get コマンドを実行すると .gen/modules/ ディレクトリ以下にコードが生成されます。

$ cdktf get
Generated typescript constructs in the output directory: .gen

$ tree -F .gen/modules
.gen/modules/
├── local-module.ts
└── vpc.ts

0 directories, 2 files

生成されたコードは import して利用することができます。

import { Vpc } from "./.gen/modules/vpc";

// ...省略

new Vpc(stack, "example_vpc", {
  name: "my-vpc",
  cidr: "10.0.0.0/16",
  azs: ["us-west-2a", "us-west-2b", "us-west-2c"],
  privateSubnets: ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
  publicSubnets: ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
  enableNatGateway: true,
});

// ...省略

cdktf get によるコード生成を利用せずに TerraformHclModule を使用してモジュールを参照することもできます。
ただし、この場合は引数の型ヒントが利用できないことに注意してください。

import { TerraformHclModule } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws";

// ...省略

const provider = new AwsProvider(stack, "provider", {
  region: "us-east-1",
});

const module = new TerraformHclModule(stack, "example_vpc", {
  source: "terraform-aws-modules/vpc/aws",
  variables: {
    name: "my-vpc",
    cidr: "10.0.0.0/16",
    azs: ["us-west-2a", "us-west-2b", "us-west-2c"],
    privateSubnets: ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
    publicSubnets: ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
    enableNatGateway: true,
  },
  providers: [provider],
});

// ...省略

ユニットテスト

CDK for Terraform ではユニットテスト用のライブラリが提供されています。

// 引用: <https://www.terraform.io/cdktf/test/unit-tests#write-assertions>
import { Testing } from "cdktf";
import { Image, Container } from "../.gen/providers/docker";
import MyApplicationsAbstraction from "../app"; // Could be a class extending from Construct

describe("Unit testing using assertions", () => {
  it("should contain a container", () => {
    expect(
      Testing.synthScope((scope) => {
        new MyApplicationsAbstraction(scope, "my-app", {});
      })
    ).toHaveResource(Container);
  });

  it("should use an ubuntu image", () => {
    expect(
      Testing.synthScope((scope) => {
        new MyApplicationsAbstraction(scope, "my-app", {});
      })
    ).toHaveResourceWithProperties(Image, { name: "ubuntu:latest" });
  });
});

詳しくは下記ドキュメントをご参照ください。
Unit Tests | Terraform by HashiCorp

cdktf synth – Terraform CLI から利用できる設定ファイルを生成する

cdktf synth は Terraform CLI から利用できる JSON 形式の設定ファイルを生成します。
実行すると ./cdktf.out/stacks/{スタック名}/cdk.tf.json という名前で JSON ファイルが生成されます。

$ cdktf synth

Generated Terraform code for the stacks: cdktf-aws-example

$ ls cdktf.out/stacks/{スタック名}/
cdk.tf.json
  • cdk.tf.json の例
{
  "//": {
    "metadata": {
      "backend": "local",
      "stackName": "example-stack",
      "version": "0.12.2"
    },
    "outputs": {
    }
  },
  "provider": {
    "aws": [
      {
        "region": "us-east-1"
      }
    ]
  },
  "resource": {
    "aws_s3_bucket": {
      "example_bucket": {
        "//": {
          "metadata": {
            "path": "example-stack/example_bucket",
            "uniqueId": "example_bucket"
          }
        },
        "bucket_prefix": "example-"
      }
    }
  },
  "terraform": {
    "backend": {
      "local": {
        "path": "/path/to/terraform.example-stack.tfstate"
      }
    },
    "required_providers": {
      "aws": {
        "source": "aws",
        "version": "4.27.0"
      }
    }
  }
}

cdk.tf.json があるディレクトリ内で Terraform CLI を実行することができます。

$ cd ./cdktf.out/stacks/{スタック名}/
$ terraform init
$ terraform apply

cdktf convert – HCL を CDK for Terraform のコードに変換する

cdktf convert で HCL で書かれた tf コードを CDK for Terraform のコードに変換することができます。
( 2022年08月29日現在 Go では使えません )

例えば次のような s3.tf ファイルが存在する場合

# s3.tf
resource "aws_s3_bucket" "example_bucket" {
  bucket_prefix = "example"
}

次のように実行することで標準出力に変換後のコードが出力されます。

# `--language` で言語を指定することができる
$ cat s3.tf | cdktf convert --language typescript

出力例:

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

/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: aws.
For a more precise conversion please use the --provider flag in convert.*/
new aws.s3.S3Bucket(this, "example_bucket", {
  bucket_prefix: "example",
});

公式サンプル

下記ページで言語ごとに様々なサンプルが用意されています。
CDKTF Examples and Guides | Terraform by HashiCorp

個人的な感想

  • Terraform に慣れていればそれほど苦労せずにキャッチアップできると思います。
  • 特に使いづらかったりすることもなく結構いい感じでした。
    ただ、plan のインデントが潰されるのはどうにかしてほしい。
  • HCL で満足している人が多いと思うので、あまり流行るイメージはない印象を受けました。
    「HCL を学ばなくていい」とはいうものの、 HCL 自体そんなに学習コストがかかる言語でもないはずなので、
    HCL の良さの一つとしてとにかくシンプル ( 読めばわかる、誰が書いても大体同じようなコードになる ) というのがあるが、 こういった HCL の恩恵を放棄してまで CDK for Terraform を使うことにどれほどのメリットがあるのか、みたいなところが気になりました。
  • 類似のツールとしては Pulumi があります。こちらも機会があれば触ってみたいです。
    Pulumi – Universal Infrastructure as Code

CDK for Terraform を理解する

Koki Sato

2022.10.6

はじめに

基本的な使い方をまとめてみました。

CDK for Terraform Is Now Generally Available

今回は TypeScript を使っている前提で記述するため、他の言語を利用する場合は適宜読み替えてください。

CDK for Terraform とは

CDK for Terraform | Terraform by HashiCorp

CDK for Terraform を使うと使い慣れたプログラミング言語を使ってインフラの定義やプロビジョニングを行うことができます。

これにより HCL を学ぶことなく Terraform エコシステム全体にアクセスでき、またテストや依存関係管理などの既存のツールチェインの力を活用することができます。

CDK for Terraform では 2022年08月29日現在、次の言語をサポートしています。

  • TypeScript
  • Python
  • Java
  • C#
  • Go

言語によっては実験的な新機能が利用できない場合もあります。

( CDK for Terraform 自身が TypeScript で作られているからか、 TypeScript のサポートが一番手厚い印象を受けました。 )

CDK for Terraform では Terraform Registry で利用可能な全ての Terraform プロバイダとモジュールを利用できます。また、 Terraform Cloud, Terraform Enterprise, Sentinel なども利用可能です。

準備

前提条件

  • Terraform CLI ( v1.1 以上 )
  • Node.js ( v16 以上 )

cdktf cli をインストール

CDK for Terraform は cdktf cli を使ってさまざまなオペレーションを実行します。
cdktf cli は npm でインストールすることができます。

# 安定版リリース
$ npm install --global cdktf-cli@latest

# 開発版リリース
$ npm install --global cdktf-cli@next

使い方

cdktf init – プロジェクトを作成

cdktf init でプロジェクトを作成することができます。

$ cdktf init --template=typescript --local
  • --template でテンプレートを指定します。
    用意されているテンプレートは下記ページから参照できます。
    terraform-cdk/packages/cdktf-cli/templates at main · hashicorp/terraform-cdk
  • --local で tfstate をローカルで管理することを指定します。
    ・Terraform Cloud を利用する場合はこのオプションは不要です。
    ・AWS S3 などによるリモートステートを利用する場合もここでは --local を指定します。

実行すると対話形式でさまざまな設定をしていきます。

  • プロジェクト名
  • プロジェクトの説明
  • 既存の Terraform プロジェクトを利用するかどうか ( 2022年08月29日現在 TypeScript のみ )
    利用する場合、 Terraform プロジェクトへのパス
  • クラッシュレポートを送信するかどうか
? Project Name
? Project Description
? Do you want to start from an existing Terraform project?
? Please enter the path to the Terraform project
? 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

プロジェクトの作成が完了するとカレントディレクトリに様々なファイルが作成されます。

$ tree -aFL 1
./
├── .gitignore
├── .npmrc
├── __tests__/
├── cdktf.json
├── help
├── jest.config.js
├── main.ts
├── node_modules/
├── package-lock.json
├── package.json
├── setup.js
└── tsconfig.json

2 directories, 10 files

インフラリソースの定義は主に main.ts に記述していきます。
CDK for Terraform の設定は cdktf.json で行います。

cdktf.json の例

{
  "language": "typescript",
  "app": "npx ts-node main.ts",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "sendCrashReports": "false",
  "terraformProviders": [],
  "terraformModules": [],
  "context": {
    "excludeStackIdFromLogicalIds": "true",
    "allowSepCharsInLogicalIds": "true"
  }
}

cdktf.json の詳細につきましては下記ドキュメントをご参照ください。
Configuration | Terraform by HashiCorp

cdktf provider add – プロバイダを追加する

cdktf provider add でプロバイダを追加することができます。
例えば aws プロバイダを追加する場合は次のように実行します。

$ cdktf provider add "aws@~>4.0"

cdktf provider add を実行すると、まずビルド済みのパッケージ ( 後述 ) が存在するかどうかチェックされます。
ビルド済みのパッケージが見つからなかった場合は cdktf.jsonterraformProviders にプロバイダ情報が追記され、 .gen/providers/{プロバイダ名}/ ディレクトリ以下にコードが生成されます。

次の例では sops プロバイダ を追加しています。

$ cdktf provider add "carlpett/sops@~>0.7"
Checking whether pre-built provider exists for the following constraints:
  provider: carlpett/sops
  version : ~>0.7
  language: typescript
Pre-built provider does not exist for the given constraints.
Adding local provider registry.terraform.io/carlpett/sops with version constraint ~>0.7 to cdktf.json
Generated typescript constructs in the output directory: .gen

cdktf.jsonterraformProvidersに sops プロバイダの情報が追記されます。

// cdktf.json

{
  // ...省略

  "terraformProviders": [
    "carlpett/sops@~>0.7"
  ],

  // ...省略
}

.gen/providers/sops/ ディレクトリ以下にコードが生成されています。

$ tree -aF .gen/providers
.gen/providers/
└── sops/
    ├── data-sops-external.ts
    ├── data-sops-file.ts
    ├── index.ts
    └── sops-provider.ts

1 directory, 4 files

生成されたコードは import して使用できます。

// main.ts

import { DataSopsFile } from "./.gen/providers/sops";

// ...省略

// data "sops_file" "some_file" {
//   source_file = "path/to/SOURCE_FILE"
// }
new DataSopsFile(stack, "some_file", {
  sourceFile: "path/to/SOURCE_FILE"
});

// ...省略

大きなスキーマを持つプロバイダの場合はコードを生成するために数分かかることがあります。

そのため、よく使われるプロバイダはビルド済みのパッケージとして公開されています。

全てのビルド済みパッケージは下記ページから参照できます。

HashiCorp
ビルド済みのパッケージが見つかった場合はそのパッケージがインストールされます。

次の例では aws プロバイダを追加しています。
aws プロバイダの場合はビルド済みの @cdktf/provider-aws パッケージが用意されているので、こちらがインストールされます。

$ cdktf provider add "aws@~>4.0"
Checking whether pre-built provider exists for the following constraints:
  provider: aws
  version : ~>4.0
  language: typescript
  cdktf   : 0.12.1

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

この場合はパッケージがインストールされるだけで、 cdktf.json は更新されず、 .gen/providers/ ディレクトリ以下にコードが生成されることもありません。
また、ビルド済みのパッケージは cdktf provider add を使わずに npm install でインストールすることもできます。

$ npm install @cdktf/provider-aws

インフラリソースを定義する

main.ts にコードを記述してインフラリソースを定義していきます。
CDK for Terraform ではスタックという単位でインフラリソースを定義していきます。
プロビジョニングはスタックごとに実行することができ、 tfstate ファイルもスタックごとに分けて管理されます。
スタックを定義するには TerraformStack クラスを継承したクラスを用意します。

次の例では S3 バケットを作成するスタックをひとつだけ作成しています。

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

// スタックを定義
class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // provider "aws" {
    //   region = "us-east-1"
    // }
    new AwsProvider(this, "aws", {
      region: "us-east-1",
    });

    // resource "aws_s3_bucket" "example_bucket" {
    //   bucket_prefix = "example-"
    // }
    new s3.S3Bucket(this, "example_bucket", {
      bucketPrefix: "example-",
    });
  }
}

const app = new App();

// スタックを作成
new MyStack(app, "example-stack");

app.synth();

スタックは複数作成することもできます。

次の例では環境ごとにスタックを作成しています。

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

// スタックを定義
class MyStack extends TerraformStack {
  // スタックのコンストラクタで環境を指定できるようにする
  constructor(scope: Construct, id: string, env: "dev" | "stg" | "prd") {
    super(scope, id);

    new AwsProvider(this, "aws", {
      region: "us-east-1",
    });

    new s3.S3Bucket(this, "example_bucket", {
      bucketPrefix: `example-${env}-`,
      tags: {
        environment: env,
      },
    });
  }
}

const app = new App();

// 環境ごとにスタックを作成
new MyStack(app, "example-stack-dev", "dev");
new MyStack(app, "example-stack-stg", "stg");
new MyStack(app, "example-stack-prd", "prd");

app.synth();

他にも用途ごとにスタックを作成したり、スタック間の依存関係を管理したりすることもできます。
詳しくは下記ドキュメントをご参照ください。
Stacks | Terraform by HashiCorp

インフラリソースをプロビジョニングする

cdktf cli を使用して定義したインフラリソースをプロビジョニングすることができます。

cdktf diff – 差分を表示

cdktf diff ( もしくは cdktf plan ) で差分を表示することができます ( = terraform plan ) 。

  • 出力例
$ cdktf diff
example-stack  Initializing the backend...
example-stack
               Successfully configured the backend "local"! Terraform will automatically
               use this backend unless the backend configuration changes.
example-stack  Initializing provider plugins...
               - Finding hashicorp/aws versions matching "4.27.0"...
example-stack  - Using hashicorp/aws v4.27.0 from the shared cache directory
example-stack  Terraform has created a lock file .terraform.lock.hcl to record the provider
               selections it made above. Include this file in your version control repository
               so that Terraform can guarantee to make the same selections by default when
               you run "terraform init" in the future.
example-stack  Terraform has been successfully initialized!
example-stack
               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.
example-stack  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:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be created
                 + resource "aws_s3_bucket" "example_bucket" {
               + acceleration_status         = (known after apply)
               + acl                         = (known after apply)
               + arn                         = (known after apply)
               + bucket                      = (known after apply)
               + bucket_domain_name          = (known after apply)
               + bucket_prefix               = "example-"
               + 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.

               ─────────────────────────────────────────────────────────────────────────────
example-stack  Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"

cdktf deploy – プロビジョニングを実行

cdktf deploy ( もしくは cdktf apply ) で定義に合わせてインフラリソースをプロビジョニングします ( = terraform apply ) 。

  • 出力例
$ cdktf deploy
example-stack  Initializing the backend...
example-stack  Initializing provider plugins...
               - Reusing previous version of hashicorp/aws from the dependency lock file
example-stack  - Using previously-installed hashicorp/aws v4.27.0

               Terraform has been successfully initialized!
example-stack
               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.
example-stack  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:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be created
                 + resource "aws_s3_bucket" "example_bucket" {
               + acceleration_status         = (known after apply)
               + acl                         = (known after apply)
               + arn                         = (known after apply)
               + bucket                      = (known after apply)
               + bucket_domain_name          = (known after apply)
               + bucket_prefix               = "example-"
               + 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.

example-stack
               ─────────────────────────────────────────────────────────────────────────────

               Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"
example-stack  aws_s3_bucket.example_bucket (example_bucket): Creating...
example-stack  aws_s3_bucket.example_bucket (example_bucket): Creation complete after 5s [id=example-20220828121939889900000001]
example-stack
               Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


No outputs found.

実行すると ApproveDismissStop の選択肢が表示されます。

方向キーで選択し、 Enter キーで決定します。

Please review the diff output above for {スタック名}
❯ Approve
  Dismiss
  Stop
選択肢説明
Approve現在のスタックのプロビジョニングを実行する。
Dismiss現在のスタックのプロビジョニングを中止し、次のスタックの確認に進む。また、中止したスタックに依存しているスタックのプロビジョニングも中止される。
Stop以降の全てのスタックのプロビジョニングを中止する。

--auto-approve フラグを指定すると確認をスキップして全てのスタックのプロビジョニングが実行されます。

cdktf destroy – リソースを削除

cdktf destroy でインフラリソースを削除します ( = terraform destroy ) 。

  • 出力例
$ cdktf destroy --auto-approve
example-stack  Initializing the backend...
example-stack  Initializing provider plugins...
               - Reusing previous version of hashicorp/aws from the dependency lock file
example-stack  - Using previously-installed hashicorp/aws v4.27.0
example-stack  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.
example-stack  aws_s3_bucket.example_bucket (example_bucket): Refreshing state... [id=example-20220828121939889900000001]
example-stack  Terraform used the selected providers to generate the following execution
               plan. Resource actions are indicated with the following symbols:
               - destroy

               Terraform will perform the following actions:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be destroyed
                 - resource "aws_s3_bucket" "example_bucket" {
               - arn                         = "arn:aws:s3:::example-20220828121939889900000001" -> null
               - bucket                      = "example-20220828121939889900000001" -> null
               - bucket_domain_name          = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - bucket_prefix               = "example-" -> null
               - bucket_regional_domain_name = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - force_destroy               = false -> null
               - hosted_zone_id              = "**************" -> null
               - id                          = "example-20220828121939889900000001" -> null
               - object_lock_enabled         = false -> null
               - region                      = "us-east-1" -> null
               - request_payer               = "BucketOwner" -> null
               - tags                        = {} -> null
               - tags_all                    = {} -> null

               - grant {
               - id          = "****************************************************************" -> null
               - permissions = [
               - "FULL_CONTROL",
               ] -> null
               - type        = "CanonicalUser" -> null
               }

               - versioning {
               - enabled    = false -> null
               - mfa_delete = false -> null
               }
               }

               Plan: 0 to add, 0 to change, 1 to destroy.

               ─────────────────────────────────────────────────────────────────────────────

               Saved the plan to: plan

               To perform exactly these actions, run the following command to apply:
               terraform apply "plan"
example-stack  aws_s3_bucket.example_bucket (example_bucket): Refreshing state... [id=example-20220828121939889900000001]
example-stack  Terraform used the selected providers to generate the following execution
               plan. Resource actions are indicated with the following symbols:
               - destroy

               Terraform will perform the following actions:
example-stack    # aws_s3_bucket.example_bucket (example_bucket) will be destroyed
                 - resource "aws_s3_bucket" "example_bucket" {
               - arn                         = "arn:aws:s3:::example-20220828121939889900000001" -> null
               - bucket                      = "example-20220828121939889900000001" -> null
               - bucket_domain_name          = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - bucket_prefix               = "example-" -> null
               - bucket_regional_domain_name = "example-20220828121939889900000001.s3.amazonaws.com" -> null
               - force_destroy               = false -> null
               - hosted_zone_id              = "**************" -> null
               - id                          = "example-20220828121939889900000001" -> null
               - object_lock_enabled         = false -> null
               - region                      = "us-east-1" -> null
               - request_payer               = "BucketOwner" -> null
               - tags                        = {} -> null
               - tags_all                    = {} -> null

               - grant {
               - id          = "****************************************************************" -> null
               - permissions = [
               - "FULL_CONTROL",
               ] -> null
               - type        = "CanonicalUser" -> null
               }

               - versioning {
               - enabled    = false -> null
               - mfa_delete = false -> null
               }
               }

               Plan: 0 to add, 0 to change, 1 to destroy.

example-stack  aws_s3_bucket.example_bucket (example_bucket): Destroying... [id=example-20220828121939889900000001]
example-stack  aws_s3_bucket.example_bucket (example_bucket): Destruction complete after 1s
example-stack
               Destroy complete! Resources: 1 destroyed.

こちらも cdktf deploy と同様に ApproveDismissStop の選択肢が表示され、 --auto-approve フラグでそれらをスキップすることができます。

Terraform Module を利用する

CDK for Terraform では、 Terraform RegistryGitHub にある Terraform Module を使うこともできます。
まず cdktf.jsonterraformModules に利用するモジュール情報を追記します。

次の例では、 aws の vpc モジュールとローカルに存在する local-module モジュールを追記しています。

// cdktf.json

{
  // ...省略

  "terraformModules": [
    // https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws
    {
      "name": "vpc",
      "source": "terraform-aws-modules/vpc/aws",
      "version": "~> 3.0"
    },
    // ローカルのモジュールを指定することも可能
    {
      "name": "local-module",
      "source": "./path/to/local-module"
    }
  ],

  // ...省略
}

この状態で cdktf get コマンドを実行すると .gen/modules/ ディレクトリ以下にコードが生成されます。

$ cdktf get
Generated typescript constructs in the output directory: .gen

$ tree -F .gen/modules
.gen/modules/
├── local-module.ts
└── vpc.ts

0 directories, 2 files

生成されたコードは import して利用することができます。

import { Vpc } from "./.gen/modules/vpc";

// ...省略

new Vpc(stack, "example_vpc", {
  name: "my-vpc",
  cidr: "10.0.0.0/16",
  azs: ["us-west-2a", "us-west-2b", "us-west-2c"],
  privateSubnets: ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
  publicSubnets: ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
  enableNatGateway: true,
});

// ...省略

cdktf get によるコード生成を利用せずに TerraformHclModule を使用してモジュールを参照することもできます。
ただし、この場合は引数の型ヒントが利用できないことに注意してください。

import { TerraformHclModule } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws";

// ...省略

const provider = new AwsProvider(stack, "provider", {
  region: "us-east-1",
});

const module = new TerraformHclModule(stack, "example_vpc", {
  source: "terraform-aws-modules/vpc/aws",
  variables: {
    name: "my-vpc",
    cidr: "10.0.0.0/16",
    azs: ["us-west-2a", "us-west-2b", "us-west-2c"],
    privateSubnets: ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
    publicSubnets: ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
    enableNatGateway: true,
  },
  providers: [provider],
});

// ...省略

ユニットテスト

CDK for Terraform ではユニットテスト用のライブラリが提供されています。

// 引用: <https://www.terraform.io/cdktf/test/unit-tests#write-assertions>
import { Testing } from "cdktf";
import { Image, Container } from "../.gen/providers/docker";
import MyApplicationsAbstraction from "../app"; // Could be a class extending from Construct

describe("Unit testing using assertions", () => {
  it("should contain a container", () => {
    expect(
      Testing.synthScope((scope) => {
        new MyApplicationsAbstraction(scope, "my-app", {});
      })
    ).toHaveResource(Container);
  });

  it("should use an ubuntu image", () => {
    expect(
      Testing.synthScope((scope) => {
        new MyApplicationsAbstraction(scope, "my-app", {});
      })
    ).toHaveResourceWithProperties(Image, { name: "ubuntu:latest" });
  });
});

詳しくは下記ドキュメントをご参照ください。
Unit Tests | Terraform by HashiCorp

cdktf synth – Terraform CLI から利用できる設定ファイルを生成する

cdktf synth は Terraform CLI から利用できる JSON 形式の設定ファイルを生成します。
実行すると ./cdktf.out/stacks/{スタック名}/cdk.tf.json という名前で JSON ファイルが生成されます。

$ cdktf synth

Generated Terraform code for the stacks: cdktf-aws-example

$ ls cdktf.out/stacks/{スタック名}/
cdk.tf.json
  • cdk.tf.json の例
{
  "//": {
    "metadata": {
      "backend": "local",
      "stackName": "example-stack",
      "version": "0.12.2"
    },
    "outputs": {
    }
  },
  "provider": {
    "aws": [
      {
        "region": "us-east-1"
      }
    ]
  },
  "resource": {
    "aws_s3_bucket": {
      "example_bucket": {
        "//": {
          "metadata": {
            "path": "example-stack/example_bucket",
            "uniqueId": "example_bucket"
          }
        },
        "bucket_prefix": "example-"
      }
    }
  },
  "terraform": {
    "backend": {
      "local": {
        "path": "/path/to/terraform.example-stack.tfstate"
      }
    },
    "required_providers": {
      "aws": {
        "source": "aws",
        "version": "4.27.0"
      }
    }
  }
}

cdk.tf.json があるディレクトリ内で Terraform CLI を実行することができます。

$ cd ./cdktf.out/stacks/{スタック名}/
$ terraform init
$ terraform apply

cdktf convert – HCL を CDK for Terraform のコードに変換する

cdktf convert で HCL で書かれた tf コードを CDK for Terraform のコードに変換することができます。
( 2022年08月29日現在 Go では使えません )

例えば次のような s3.tf ファイルが存在する場合

# s3.tf
resource "aws_s3_bucket" "example_bucket" {
  bucket_prefix = "example"
}

次のように実行することで標準出力に変換後のコードが出力されます。

# `--language` で言語を指定することができる
$ cat s3.tf | cdktf convert --language typescript

出力例:

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

/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: aws.
For a more precise conversion please use the --provider flag in convert.*/
new aws.s3.S3Bucket(this, "example_bucket", {
  bucket_prefix: "example",
});

公式サンプル

下記ページで言語ごとに様々なサンプルが用意されています。
CDKTF Examples and Guides | Terraform by HashiCorp

個人的な感想

  • Terraform に慣れていればそれほど苦労せずにキャッチアップできると思います。
  • 特に使いづらかったりすることもなく結構いい感じでした。
    ただ、plan のインデントが潰されるのはどうにかしてほしい。
  • HCL で満足している人が多いと思うので、あまり流行るイメージはない印象を受けました。
    「HCL を学ばなくていい」とはいうものの、 HCL 自体そんなに学習コストがかかる言語でもないはずなので、
    HCL の良さの一つとしてとにかくシンプル ( 読めばわかる、誰が書いても大体同じようなコードになる ) というのがあるが、 こういった HCL の恩恵を放棄してまで CDK for Terraform を使うことにどれほどのメリットがあるのか、みたいなところが気になりました。
  • 類似のツールとしては Pulumi があります。こちらも機会があれば触ってみたいです。
    Pulumi – Universal Infrastructure as Code

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ