ちょっと話題の記事

AWS CDKでプロバイダーとしてTerraformが使える!!CDK for Terraformが発表されました!! #awscdk

AWS CDKがデプロイプロバイダーとしてTerraformをサポートしました!!!まだPreview版ですが、試しにVPCを作成してみました。
2020.07.17

はじめに

おはようございます、加藤です。私にとっては今年1番熱いアップデートが来ました!AWS CDKがなんとデプロイプロバイダーとしてTerraformをサポートしました!!!
ただし、今の所アルファテストステージなので、原則プロダクション環境に使うべきでありません、使う際は慎重に判断してから使用しましょう。
今まで、TerraStackIO/terrastackという同様にCDKでプロバイダーとしてTerraformを使おうとするプロジェクトはあったのですが、あまり開発は進んでいませんでした。なので、これまで AWS CDKを使う = CloudFormationを使う でCDKはAWSにしか使えませんでした(CDK8Sを除いて)。
しかし、今日の発表により今後AWS CDKでGCPを管理するといった事が可能になりました。AWSしか使わない場合にしても、TerraformはCloudFormationと比べて既存リソースのImportや継続的なアーキテクチャ更新をしやすいので(個人的感想)メリットを享受できます!!!

CDK for Terraformとは

情報を箇条書きにして整理します。

  • CDK for Terraform現在Preview版
    • TypeScriptとPythonのみサポートしている
  • AWS以外のTerraformがサポートするリソースも管理が可能
  • 既存を拡張する形ではなく別のCLIツールを使用する
  • オープンソースのプロジェクトでリポジトリはHashiCorp管理化 hashicorp/terraform-cdk

こんな感じです、これだけじゃ感触を掴めないので実際にAWSでVPCを作成してみます。

AWSにVPCを作ってみる

バージョン情報

cdktfやnodeなどのバージョンは下記の通りです。

❯ node --version
v12.18.0
❯ npm --version
6.14.5
❯ yarn -s cdktf --version
0.0.11
❯ terraform --version
Terraform v0.12.28

リポジトリを初期生成する

HashiCorpが公開している下記のチュートリアルを参考に実際に使ってみます。
CDK for Terraform: Enabling Python & TypeScript Support

チュートリアルはグローバルインストールしていますが、グローバルインストールを極力避けたいので、ローカルインストールします。
パッケージ名が cdktf-cli に対してコマンド名は cdktf です。ご注意ください。また本ブログを書いている時のバージョンは 0.0.11 でした。
なお、Terraformがインストールされていない環境で実行すると # Terraform CLI not present - Please install a current version https://learn.hashicorp.com/terraform/getting-started/install.html とエラーが表示されます、事前にインストールしておいてください。

リポジトリの初期生成はディレクトリが空じゃないと行えません、なのでローカルインストールする前に npx で一時インストールし、コマンドを使用します。
initオプションでプロジェクトを初期生成する際に、 --template オプションでTypeScriptかPythonを指定しましょう。私はTypeScriptを選択しました。

mkdir terraform-cdk && cd terraform-cdk

npx -p cdktf-cli cdktf init --template=typescript

# 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 (yes/no)? no
# 
# We will now setup the project. Please enter the details for your project.
# If you want to exit, press ^C.

# Stateをローカルに保存せずTerraform Cloud(無料版)に保存する事を勧められます。今回は検証なのでローカルに保存します。

# Project Name: (default: 'terraform-cdk')
# Project Description: (default: 'A simple getting started project for cdktf.')

# プロジェクト名と説明を求められます、デフォルトのままにしました。

# ========================================================================================================
# 
#   Your cdktf typescript project is ready!
# 
#   cat help              Print this message
# 
#   Compile:
#     npm run compile     Compile typescript code to javascript (or "yarn watch")
#     npm run watch       Watch for changes and compile typescript in the background
#     npm run build       cdktf get and compile typescript
# 
#   Synthesize:
#     cdktf synth         Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply')
# 
#   Diff:
#     cdktf diff          Perform a diff (terraform plan) for the given stack
# 
#   Deploy:
#     cdktf deploy        Deploy the given stack
# 
#   Destroy:
#     cdktf destroy       Destroy the stack
# 
# 
#  Upgrades:
#    npm run get           Import/update Terraform providers and modules (you should check-in this directory)
#    npm run upgrade       Upgrade cdktf modules to latest version
#    npm run upgrade:next  Upgrade cdktf modules to latest "@next" version (last commit)
# 
# ========================================================================================================

cdktf-cliをdevDependenciesにインストールしよう思ったのですが、最初からインストール済みでした。

VPCを作成する

全体

VPCを作成してみます。まず、全体のコードを確認してください、各部について説明していきます。

main.ts

import { Construct } from 'constructs';
import { App, TerraformStack, Token } from 'cdktf';
import { AwsProvider, Vpc, Subnet } from './.gen/providers/aws';

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

    new AwsProvider(this, 'Aws', {
      region: 'ap-northeast-1',

    })

    const vpc = new Vpc(this, 'Vpc', {
      cidrBlock: '10.0.0.0/16'
    });
    new Subnet(this, 'Subnet', {
      vpcId: Token.asString(vpc.id),
      cidrBlock: '10.0.0.0/24'
    })

  }
}

const app = new App();
new MyStack(app, 'terraform-cdk');
app.synth();

import部分

import { AwsProvider, Vpc, Subnet } from './.gen/providers/aws';

AWS CDKでは各サービス毎に(VPC、EC2、RDSの様な単位)公開パッケージは別々に公開、いわゆるマルチパッケージ戦略を取っています。しかし、CDK for Terraformでは各サービスのパッケージは公開せず、リポジトリ初期生成時にファイルを全部手元にダウンロードするという方法でした。これが開発初期フェイズにおいて開発速度を上げる為か、TerraformのProviderの都合上なのかはわかりませんでした。

チュートリアルでは、下記の様に各サービス毎にimportしていたのですが、 ./.gen/providers/aws 配下で、まとめてexportしてくれていたので、ありがたくまとめてimportさせて貰いました。

import { Vpc } from './.gen/providers/aws/vpc';
import { Subnet } from './.gen/providers/aws/subnet';

プロバイダー部分

Terraformを書いているんだって感じが凄いする部分ですね。

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

指定できるオプションの型は下記のようになっていました。

./.gen/providers/aws/aws-provider.ts

export interface AwsProviderConfig {
  /** The access key for API operations. You can retrieve this
from the 'Security & Credentials' section of the AWS console. */
  readonly accessKey?: string;
  readonly allowedAccountIds?: string[];
  readonly forbiddenAccountIds?: string[];
  /** Explicitly allow the provider to perform "insecure" SSL requests. If omitted,default value is `false` */
  readonly insecure?: boolean;
  /** The maximum number of times an AWS API request is
being executed. If the API request still fails, an error is
thrown. */
  readonly maxRetries?: number;
  /** The profile for API operations. If not set, the default profile
created with `aws configure` will be used. */
  readonly profile?: string;
  /** The region where AWS operations will take place. Examples
are us-east-1, us-west-2, etc. */
  readonly region: string;
  /** Set this to true to force the request to use path-style addressing,
i.e., http://s3.amazonaws.com/BUCKET/KEY. By default, the S3 client will
use virtual hosted bucket addressing when possible
(http://BUCKET.s3.amazonaws.com/KEY). Specific to the Amazon S3 service. */
  readonly s3ForcePathStyle?: boolean;
  /** The secret key for API operations. You can retrieve this
from the 'Security & Credentials' section of the AWS console. */
  readonly secretKey?: string;
  /** The path to the shared credentials file. If not set
this defaults to ~/.aws/credentials. */
  readonly sharedCredentialsFile?: string;
  /** Skip the credentials validation via STS API. Used for AWS API implementations that do not have STS available/implemented. */
  readonly skipCredentialsValidation?: boolean;
  /** Skip getting the supported EC2 platforms. Used by users that don't have ec2:DescribeAccountAttributes permissions. */
  readonly skipGetEc2Platforms?: boolean;
  readonly skipMetadataApiCheck?: boolean;
  /** Skip static validation of region name. Used by users of alternative AWS-like APIs or users w/ access to regions that are not public (yet). */
  readonly skipRegionValidation?: boolean;
  /** Skip requesting the account ID. Used for AWS API implementations that do not have IAM/STS API and/or metadata API. */
  readonly skipRequestingAccountId?: boolean;
  /** session token. A session token is only required if you are
using temporary security credentials. */
  readonly token?: string;
  /** Alias name */
  readonly alias?: string;
  /** assume_role block */
  readonly assumeRole?: AwsProviderAssumeRole[];
  /** endpoints block */
  readonly endpoints?: AwsProviderEndpoints[];
  /** ignore_tags block */
  readonly ignoreTags?: AwsProviderIgnoreTags[];
}

リソース部分

リソース作成部分です、ちょっと残念だったのが5行目の部分です。CDKでは作成するリソースのIDなど作成するまで未確定のものはTokenとして扱います。Tokenとして扱われていたとしても、他のリソースで参照する際は自動で実行時にTokenが解除されました。なので、 console.log(vpc.id) のような事をしなければTokenとして扱われている事に気づかない程でした。
しかし、CDK for Terraformでは、Tokenを参照する場合 Token.asString(vpc.id) の様にする必要がありました。型的には vpcId: vpc.id! って書けてしまうので、変にハマらないようにCDK for Terraformを使う際はこの仕様を頭に入れておいた方が良さそうです。

const vpc = new Vpc(this, 'Vpc', {
    cidrBlock: '10.0.0.0/16'
});
new Subnet(this, 'Subnet', {
    vpcId: Token.asString(vpc.id),
    cidrBlock: '10.0.0.0/24'
})

デプロイする

最初に私は、Assume Role with MFA環境なので、そのままではTerraformがAWSへアクセスできません。一時クレデンシャルを生成し環境変数にセットする事でTerraformからAWSへのアクセスを可能にします。

同様の環境の方は、佐伯が書いたこのブログを参考に設定してください。
[小ネタ]ディレクトリ移動した際に自動で一時クレデンシャルを取得・設定する

cdktf deploy コマンドでデプロイが行なえます。

yarn -s cdktf deploy
# Deploying Stack: terraform-cdk
# Resources
#  ✔ AWS_SUBNET           Subnet              aws_subnet.terraformcdk_Subnet_D427F1FE
#  ✔ AWS_VPC              Vpc                 aws_vpc.terraformcdk_Vpc_DF4745BC
# 
# Summary: 2 created, 0 updated, 0 destroyed.

cdktf synth コマンドでJSON形式のTerraformが生成されるので、これを使ってTerraform Planで現在との差分を見たり、Terraformコマンドを使ってデプロイする事ができます。手元からデプロイする時は不要ですが、既存のCI/CDパイプラインに載せたい場合などに便利そうです。

cdktf destroy コマンドで削除が行えます、忘れずに削除しておきましょう。

yarn -s cdktf destroy
# Destroying Stack: terraform-cdk
# Resources
#  ✔ AWS_SUBNET           Subnet              aws_subnet.terraformcdk_Subnet_D427F1FE
#  ✔ AWS_VPC              Vpc                 aws_vpc.terraformcdk_Vpc_DF4745BC
# 
# Summary: 2 destroyed.

Terraform Importしてみる

さて、Terraformには既存リソースをTerraformに取り込みImport機能があります。CDK for Terraformでも使えるか確認してみます。 マネジメントコンソールからS3バケットを作成し、Importする。その後、設定を変更した後に削除してみます。

まずは、S3バケットを作成します。 cdk-for-terraform-import-test という名前でバケットを作成して置きました。
次に、CDK上でS3バケットを定義します。

main.tf

import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';
import { AwsProvider, S3Bucket } from './.gen/providers/aws';

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

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

    new S3Bucket(this, 'Bucket');

  }
}

const app = new App();
new MyStack(app, 'terraform-cdk');
app.synth();

定義したら、下記のコマンドでTerraformファイルを合成します。

yarn -s cdktf synth

これによりcdktf.outディレクトリにcdk.tf.jsonというTerraformファイルが作成されます。このファイルを確認しTerraform上でのリソース名をメモして置いてください。ハイライトされている部分です。

cdktf.out/cdk.tf.json

{
  "//": {
    "metadata": {
      "version": "0.0.11",
      "stackName": "terraform-cdk"
    }
  },
  "terraform": {
    "required_providers": {
      "aws": "~> 2.0"
    }
  },
  "provider": {
    "aws": [
      {
        "region": "ap-northeast-1"
      }
    ]
  },
  "resource": {
    "aws_s3_bucket": {
      "YOUR_RESOURCE_NAME": {
        "//": {
          "metadata": {
            "path": "terraform-cdk/Bucket",
            "uniqueId": "terraformcdk_Bucket_XXXXXXXX",
            "stackTrace": [...]
          }
        }
      }
    }
  }
}

これでTerraformに定義されているが、実際に作成はされていない(Stateに存在しない)状態になりました。Importします、コマンドはリポジトリのルートディレクトリで実行してください。
-config オプションでディレクトリを指定するところがポイントです。

# llでディレクトリを間違えていないかチェックした
ll
# .rw-r--r--  132 kato.ryo 17 Jul  8:41  cdktf.json
# drwxr-xr-x    - kato.ryo 17 Jul  8:51  cdktf.out
# .rw-r--r-- 1.1k kato.ryo 17 Jul  6:55  help
# .rw-r--r--   11 kato.ryo 17 Jul  8:49  main.d.ts
# .rw-r--r-- 2.5k kato.ryo 17 Jul  8:49  main.js
# .rw-r--r--  448 kato.ryo 17 Jul  8:56  main.ts
# drwxr-xr-x    - kato.ryo 17 Jul  7:03  node_modules
# .rw-r--r--  657 kato.ryo 17 Jul  6:56  package.json
# .rw-r--r-- 1.7k kato.ryo 17 Jul  8:52  terraform.tfstate
# .rw-r--r--  158 kato.ryo 17 Jul  8:52  terraform.tfstate.backup
# .rw-r--r--  716 kato.ryo 17 Jul  6:55  tsconfig.json
# .rw-r--r--  59k kato.ryo 17 Jul  7:03  yarn.lock

terraform import -config="cdktf.out" aws_s3_bucket.${YOUR_RESOURCE_NAME}

diffを見てみると何か差分があるようですが詳細が不明です。diffのサブオプションに詳細を表示する様なものが無かったので、Terraformコマンドで詳細な差分を確認してみます。ディレクトリ移動してから実行する必要がある事に注意してください。

yarn -s cdktf diff
# Stack: terraform-cdk
# Resources
#  ~ AWS_S3_BUCKET        Bucket              aws_s3_bucket.terraformcdk_Bucket_6808E103
# 
# Diff: 0 to create, 1 to update, 0 to delete.

cd cdktf.out
terraform plan -state="../terraform.tfstate"
# Refreshing Terraform state in-memory prior to plan...
# The refreshed state will be used to calculate this plan, but will not be
# persisted to local or remote state storage.
# 
# aws_s3_bucket.terraformcdk_Bucket_6808E103: Refreshing state... [id=cdk-for-terraform-import-test]
# 
# ------------------------------------------------------------------------
# 
# An execution plan has been generated and is shown below.
# Resource actions are indicated with the following symbols:
#   ~ update in-place
# 
# Terraform will perform the following actions:
# 
#   # aws_s3_bucket.terraformcdk_Bucket_6808E103 will be updated in-place
#   ~ resource "aws_s3_bucket" "terraformcdk_Bucket_6808E103" {
#       + acl                         = "private"
#         arn                         = "arn:aws:s3:::cdk-for-terraform-import-test"
#         bucket                      = "cdk-for-terraform-import-test"
#         bucket_domain_name          = "cdk-for-terraform-import-test.s3.amazonaws.com"
#         bucket_regional_domain_name = "cdk-for-terraform-import-test.s3.ap-northeast-1.amazonaws.com"
#       + force_destroy               = false
#         hosted_zone_id              = "Z2M4EHUR26P7ZW"
#         id                          = "cdk-for-terraform-import-test"
#         region                      = "ap-northeast-1"
#         request_payer               = "BucketOwner"
#         tags                        = {}
# 
#         versioning {
#             enabled    = false
#             mfa_delete = false
#         }
#     }
# 
# Plan: 0 to add, 1 to change, 0 to destroy.
# 
# ------------------------------------------------------------------------
# 
# Note: You didn't specify an "-out" parameter to save this plan, so Terraform
# can't guarantee that exactly these actions will be performed if
# "terraform apply" is subsequently run.

aclとforce_destroyの設定で差分が出ていますね。しかし、tfstateを見てみるとどちらもnullになっており、"private"とfalseをCDKで設定しても差分は消えませんでした。検証も兼ねてこのままデプロイします。

yarn -s cdktf deploy
# Deploying Stack: terraform-cdk
# Resources
#  ✔ AWS_S3_BUCKET        Bucket              aws_s3_bucket.terraformcdk_Bucket_6808E103
# 
# Summary: 0 created, 1 updated, 0 destroyed.

デプロイすると、tfstateのnullだった箇所には、"private"とfalseが設定されていました。Importではうまく取り込めないという事だと理解しました。tfstateを手動で編集していれば恐らく差分を消せたでしょう。

バケットにタグ付けをすることで、Importしたリソースを操作できるか確認してみます。main.tsを編集しタグを定義します。

main.ts

import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';
import { AwsProvider, S3Bucket } from './.gen/providers/aws';

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

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

    new S3Bucket(this, 'Bucket', {
        tags: {
          testTag: 'value'
        }
    });

  }
}

const app = new App();
new MyStack(app, 'terraform-cdk');
app.synth();

デプロイし、CLIでタグが付けられているか確認します。

yarn -s cdktf deploy
# Deploying Stack: terraform-cdk
# Resources
#  ✔ AWS_S3_BUCKET        Bucket              aws_s3_bucket.terraformcdk_Bucket_6808E103
# 
# Summary: 0 created, 1 updated, 0 destroyed.

aws s3api get-bucket-tagging --bucket cdk-for-terraform-import-test
# {
#     "TagSet": [
#         {
#             "Key": "testTag",
#             "Value": "value"
#         }
#     ]
# }

期待通りに変更できました、最後に削除してみます。

yarn -s cdk destroy
# Destroying Stack: terraform-cdk
# Resources
#  ✔ AWS_S3_BUCKET        Bucket              aws_s3_bucket.terraformcdk_Bucket_6808E103
# 
# Summary: 1 destroyed.

aws s3api get-bucket-tagging --bucket cdk-for-terraform-import-test

# An error occurred (NoSuchBucket) when calling the GetBucketTagging operation: The specified bucket does not exist

NoSuchBucketが返って来ており、期待通りにバケットを削除できました!!!

AWS以外を扱う方法

今のリポジトリの状態では、.gen/provider配下にawsしか存在しないので、AWS以外は扱えません。 扱うためにはProviderを取得する必要があります。取得するには、まずcdkts.jsonterraformProvidersに追加したいProviderの名前を追加します。

Provider一覧で使いたいProviderを探し、リンク先のProvider定義名から追加すべき名前を確認できます。

cdktf.json

{
  "language": "typescript",
  "app": "npm run --silent compile && node main.js",
  "terraformProviders": [
    "aws@~> 2.0",
    "dns",
    "github",
    "google"
  ]
}

追加後に get コマンドでライブラリのダウンロードが行えます。

yarn -s cdktf get
# Generated typescript constructs in the output directory: .gen

後は本ブログで紹介したVPCを作成した方法と同様にコードを書けばAWS以外でもCDKでリソースが作成できます!!
使う際には一度hashicorp/terraform-cdkのドキュメント全体に目を通してからがおすすめです。しかし、ドキュメントもまだ充実していないので、型を頼りに書くことになると思われます。

考察とあとがき

AWSのサービスではないのでアップデートと言うのは変なのですが、本当に熱いアップデートですね!!!
ですが、CDK for Terraformをプロダクションで使える様になるのもう少し先かなと思いました。その最大の理由は、L1 Constructsしか存在しない事です。私が思うCDKの最大のメリットはコードとリソースが1:1ではなく1:N、つまり高レベルに抽象化されている事です。しかし、現状CDK for TerraformにはL1 Constructsしか存在しません。L2 Constructsがぜひとも欲しいです。
とはいえ、既存環境がTerraformでCDKを使いたいがCloudFormationへ移行する気が無く使えなかったという人にとっては今の状態でもかなり有用だと思います。
今後の発展がとても楽しみなプロジェクトです!!!CDKは大好きなプロジェクトなので、本家同様こちらにもなるべくコントリビュートしていきます!!!
以上でした

参考