猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

AWS CDK と CDK for Terraform を使ってみた感想

AWS のサーバー構築

これまで、AWS のサーバー構築には Terraform を使うことが多かったかと思います。

しかし最近では、 AWS CDK を使うという選択肢も出てきています。

そして先日、 CDK for Terraform が Generally Available、つまり、正式版公開という記事が出ました。

www.hashicorp.com

Terraform の欠点

Terraform の唯一の欠点が、 HCL (HashiCorp Configuration Language) という独自の記述言語でした。

AWS だけでなく、 GCP や Azure といった様々なクラウドに対応している Terraform ですが、HCL を書くのが非常に大変でした。プログラミング言語ほどの記述能力が無いため、しばし記述が冗長になったり、書きづらかったり、ということがありました。

AWS CDK の登場

ここで登場したのが、AWS CDK です。

AWS CDK は、 AWS CloudFormation を改善したようなものです。

AWS CloudFormation は、VPCやSubnet、EC2などのAWSリソースの情報を YAML 形式で記述すると、書いたとおりに AWS のリソースを作成してくれるものなのですが、 YAML で記述する必要があるため、 Terraform 同様、記述能力が低く、書きづらい、読みづらい、ということがありました。

これを、 YAML ではなく TypeScript で書けるようにしたのが AWS CDK でした。

書き慣れた「プログラミング言語」で書けるということで、非常に書きやすいです。表現能力も高いため、インフラの記述がスッキリします。

唯一、デメリットとしては、 CloudFormation をベースにしているため、 AWS でしか使えないことでした。

CDK for Terraform の登場

そこで CDK Terraform の登場。

これは、 AWS CDK を参考にして、 TypeScript で Terraform が書けるようにしたものです。AWS CDK と Terraform のいいとこ取りで、これが最強なのでは?という感じです。

AWS CDK を使ってみる

CDK for Terraform を使う前に、僕はそもそも AWS CDK を使ったことがないので、使ってみます。

ちなみに、 Terraform と AWS CloudFormation は使ったことがあります。

AWS CDK コマンドラインツールのインストール

npm を使ってインストールするのがオススメです。node.js はPCにインストールされている前提です。

また、 AWS CLI も必要ですが、すでに設定済みという前提で進めます。

$ npm install -g aws-cdk
$ cdk --version
2.35.0 (build 5c23578)

AWS CDK で S3 を立ててみる

# aws cdk プロジェクトを作成する
# ディレクトリを作成して、その中に入ってから cdk init をする
# カレントディレクトリに package.json などが作成されるというなかなかロックな仕様
$ mkdir cdk-sample-s3-app
$ cd cdk-sample-s3-app
$ cdk init app --language=typescript

init コマンドの第3引数には app / lib / sample-app が指定できる。ライブラリの開発をする時には lib を指定するが、通常は app を指定する。

言語は TypeScript が推奨っぽいので TypeScript を選択する。

コマンドを叩くと色々生成されるが、重要なファイルは以下

  • bin/アプリ名-stack.ts
    • エントリーポイント。ここからプログラムの実行が開始される。
  • lib/アプリ名-stack.ts
    • ここに構成を記述していくイメージ

コード

今回は cdk-sample-s3-app というアプリ名にしたので、 lib/cdk-sample-s3-app-stack.ts にコードを実際に書いていく。

import { aws_s3, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleS3AppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // cdk-bucket と書いたつもりが、dck と typo してた。が、気にせずすすめる
    const bucket = new aws_s3.Bucket(this, 'sakamoto-aws-dck-bucket', {
      bucketName: 'sakamoto-aws-dck-bucket',
    })
  }
}

デプロイ

まず最初に bootstrap をする。これは、プロジェクトを作成した時に1回だけ実行したらよい。

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://038863151084/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
 ✅  Environment aws://038863151084/ap-northeast-1 bootstrapped.

これをすると、AWS CloudFormation にスタックが作成され、CloudFormation の実行履歴などを記録するための S3 バケットが作成される。

スタックの名前はデフォルトだと CDKToolkit となっている。多分これは設定で変えられるはずなので、本番で運用するときは名前をちゃんとつけたほうがいい。AWS CDKを複数使っている場合に名前が被らないようにする必要がある。

続いてデプロイ。 cdk diff をすると、差分が表示されるので、本番でデプロイする時は diff を見てからデプロイするといい。今回は実験なのでいきなりデプロイすることにする。

$ cdk deploy

✨  Synthesis time: 3.46s

CdkSampleS3AppStack: deploying...
[0%] start: Publishing ea5f8f26e4fd714c8ac8e056ce25d59da72ac19555e4a1fa77081dc899140d50:current_account-current_region
[100%] success: Published ea5f8f26e4fd714c8ac8e056ce25d59da72ac19555e4a1fa77081dc899140d50:current_account-current_region
CdkSampleS3AppStack: creating CloudFormation changeset...

 ✅  CdkSampleS3AppStack

✨  Deployment time: 38.01s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:038863151084:stack/CdkSampleS3AppStack/5c29bec0-1722-11ed-a0fc-060fd357ab39

✨  Total time: 41.47s

これで AWS 上に sakamoto-aws-dck-bucket というバケットが作成された。

リソースの全削除

作ったリソースを全部削除したい場合は、cdk destroy コマンドを実行すれば良い。この場合、CloudFormation のスタックは残ったままで、リソースが削除される。

また、 AWS コンソールの AWS CloudFormation から、「削除」をすると、スタックごと消すことができる。全部なかったことにしたい場合はこちらが良い。

AWS CDK のメリット/デメリット

CDK for Terraform と比較した時のメリット/デメリットです。もちろん、通常の Terraform と比べたら TypeScript を使える分書きやすいのですが、それは CDK for Terraform と比べてメリットにはならないので除きます。

  • メリット
    • CLIの出力結果が見やすい
    • AWS CDK が AWS 公式のツールであるという安心感
    • AWS の Web のコンソールから CloudFormation の状態(実行履歴とか)が見られるのが良い
    • 環境のセットアップが簡単
  • デメリット
    • AWS でしか利用できない

唯一のデメリットが、 AWS にしか対応していない(GCP や Azure に対応していない)点です。今は AWS しか利用していないプロダクトだとしても、将来的に、 Google Cloud Platform に移動したくなったり、一部 Azure の機能を使いたくなる、といった可能性もあるので、そういった場合に AWS CDK では管理できないのがデメリットです(別途 Terraform を用意するなどして構築する必要がでています)。

CDK for Terraform を使ってみる

続いて、 CDK for Terraform を使ってみます。

CDK for Terraform コマンドラインツールのインストール

CDK for Terraform をインストールする前に、 terraform コマンドをインストールしておく必要があります。私の PC にはもう terraform コマンドがインストールされていたので、今回は省略します。

また、 AWS CLI も必要ですが、すでに設定済みという前提で進めます。

こちらも、AWS CDK 同様、 npm を使ってインストールするのがオススメです。

$ npm install --g cdktf-cli@latest
$ cdktf --version
0.12.0

CDK for Terraform で S3 を立ててみる

プロジェクトの作成

AWS CDK の時と同様に、ディレクトリを作成して、 init します。

$ mkdir cdktf-sample-app
$ cd cdktf-sample-app

そして init するのですが、 AWS CDK と異なり、設定が少し煩雑です。

Terraform Cloud remote state management というのを利用するかどうか聞かれましたが、今回は No を選択しました。AWS CDK の場合は、 S3 に勝手にバケットが作成されて、そこで状態管理や実行結果の記録がされていましたが、 CDKTF では、 デフォルトだと Terraform Cloud を使うようです。CDKTF でも同様に S3 で管理したいなと思っていたので、今回は No にしました。

$ 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:
    /Users/yoshiyuki_sakamoto/.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-app
? Project Description A simple getting started project for cdktf.
? Do you want to start from a 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
No
npm notice created a lockfile as package-lock.json. You should commit this file.
+ constructs@10.1.70
+ cdktf@0.12.0
added 53 packages from 27 contributors and audited 53 packages in 1.624s

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

found 0 vulnerabilities

+ ts-node@10.9.1
+ @types/node@18.6.4
+ ts-jest@28.0.7
+ @types/jest@28.1.6
+ jest@28.1.3
+ typescript@4.7.4
added 300 packages from 263 contributors and audited 353 packages in 20.376s

35 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

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

cdktf/provider-aws のインストール

Terraform は、 AWS だけでなく、 GCP、Azure などの複数のプラットフォームに対応しているため、そのプラットフォームに対応する provider をインストールする必要があります。

npm install @cdktf/provider-aws

コード

main.ts が本体です。早速 S3 バケットを作成してみます。記述した感じは AWS CDK とほぼ変わりません。Terraform を書き慣れている人だと書きやすいと思います。

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

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

    new AwsProvider(this, 'aws', {
      region: 'ap-northeast-1',
    })

    new S3Bucket(this, 'cdftf-test')
  }
}

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

デプロイ

cdktf deploy コマンドを叩くと、差分が表示されたうえで、デプロイするかどうか問われます。 Approve を選択するとデプロイされます。

慣れないと差分が見づらいと思いますが、 Terraform に慣れた人ならおなじみの差分表示です。 ただ、なぜかインデントがぶっ壊れていて、見づらいです。この辺はまだ発展途上という感じがします。

適用前に差分を見せてくれたうえで、デプロイするかどうか聞いてくれるのは親切ですね。

$ cdktf deploy
cdktf-sample-app  Initializing the backend...
cdktf-sample-app  Initializing provider plugins...
cdktf-sample-app  - Reusing previous version of hashicorp/aws from the dependency lock file
cdktf-sample-app  - Using previously-installed hashicorp/aws v4.24.0
cdktf-sample-app  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-app  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-app    # aws_s3_bucket.cdftf-test (cdftf-test) will be created
                    + resource "aws_s3_bucket" "cdftf-test" {
                  + acceleration_status         = (known after apply)
                  + acl                         = (known after apply)
                  + arn                         = (known after apply)
                  + bucket                      = (known after apply)
                  + 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)
                  }
                  }
cdktf-sample-app  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-app
❯ Approve  Applies the changes outlined in the plan.
  Dismiss
  Stop

CDK for Terraform のメリット/デメリット

AWS CDK と比較した場合のメリット/デメリットを書きます。

  • メリット
    • AWS 以外にも対応している
    • AWS 公式でないとはいえ、サポート範囲はかなり広い
      • なんだかんだ AWS CDK でも対応していない AWS リソースはあったりする。それに比べて CDKTF のベースになっている Terraform は対応範囲が広い。普段使う分には AWS CDK でほぼ問題ないと思うが、サポート範囲の広さを重視したいなら CDK for Terraform が選択肢にあがる。
  • デメリット
    • CloudFormation のような Web UI や、AWS上の良い感じのシステムなのか無い
    • 初期設定が少し複雑
      • AWS CDK だと AWS 一択なので、初期設定の手間が非常に少ないが、CDKTF は選択肢が多い分初期設定がちょっとむずい

まとめ: 結局何を採用するのがいいのか

AWS しか使わないなら AWS CDK、そうでないなら CDK for Terraform がいいかなと思いました?

AWS CDK は Web 上から色々見たり操作できるメリットが

CDK for Terraform が発展途上のため、CDK を使わない Terraform を採用したくなるケースもあるかもしれませんが、Terraform は個人的にはかなり書きづらいため、Terraform を採用するなら CDK for Terraform を採用するのが良いかなと思いました。