CDK for Terraform × Typescript × GCP をやってみた

この記事は、RevComm Advent Calender 19 日目の記事です。
[こちら](https://tech.revcomm.co.jp/cdk-for-terraform-typescript-gcp)の企業ブログから私が執筆した記事の転記となります。

- [はじめに](#はじめに)
- [TL;DR](#TLDR)
- [CDK for Terraform とは](#CDK-for-Terraform-とは)
  - [特徴](#特徴)
  - [技術比較](#技術比較)
- [実践](#実践)
  - [環境構築](#環境構築)
  - [Definition](#Definition)
  - [Deploy](#Deploy)
- [終わりに](#終わりに)

# はじめに
こんにちは、株式会社 RevComm でフロントエンドチームでエンジニアをしている高橋（[@katakana_33](https://twitter.com/katakana_33)）と申します。

今回は、私が RevComm に入って初めて見たソースコードが Terraform だった事と関連して、CDK for Terraform と GCP の連携について、実践した使用感を書きました。

RevComm の良さの1つとして、GitHub Organization 内の他プロダクトのリポジトリを見れる事でプロダクト全体の理解を深め、自身の専門職種外の技術スキルにも目を向けられる点があります。
[RevCommのオンボーディング期間のその日のオンボードが終了した後](https://note.com/revcomm_miitel/n/n08cca89b1549)、私が初めて見たソースコードが担当するプロダクト Terraform でした。未経験の技術だったこともあり、そこで今回は、CDK for TerraformとGCPの連携について調査と実践をしました。

# TL;DR
- 0 から IaC を導入する場合は、メリットの方が大きい。
- 移行コストと導入必要性を考慮した際、移行しない事も選択肢の 1 つ。

# CDK for Terraform とは
[CDK for Terraform by Hashicorp](https://developer.hashicorp.com/terraform/cdktf)
[AWS CDKチームとHashicorp 社が共同開発](https://aws.amazon.com/jp/blogs/news/cdk-for-terraform-on-aws-jp/)をした AWS CDK と、Terraform の良い所を合わせたものです。

AWS CDK の開発者 kit を使用して、インフラリソースの定義とデプロイを行えます。
公式より以下概念図
![image](https://developer.hashicorp.com/_next/image?url=https%3A%2F%2Fcontent.hashicorp.com%2Fapi%2Fassets%3Fproduct%3Dterraform-cdk%26version%3Dv0.14.3%26asset%3Dwebsite%252Fdocs%252Fcdktf%252Fterraform-platform.png%26width%3D1776%26height%3D1317&w=1920&q=75)

## 特徴
- HCL の学習を必要とせず、普段使用しているメジャーな言語で IaC を記述できる。以下がサポートされているプログラミング言語。
  - TypeScript、Python、Java、C#、Go。（2022 年 12 月時点）
- Terraform エコシステム全体にアクセスできて、各機能のテストを行える。
- マルチプロバイダが可能な為、ベンダーロックインを回避できる。

## 技術比較
||[Terraform](https://developer.hashicorp.com/terraform)|[CDK](https://aws.amazon.com/jp/cdk/)|[CDK for Terraform](https://developer.hashicorp.com/terraform/cdktf)|
|---|---|---|---|
|HCLの学習|必要|不要|不要|
|マルチプロバイダ|可能|不可能（AWSのみ）|可能|
|参考文献（有志の記事を含む）|多い|多い|少ない（有志の記事が少ない、イレギュラーケースがまだ明るみになっていない可能性が高い）|

# 実践

## 環境構築

今回、deploy するのは GCP の以下の resources（本記事ではcloud infrastructure resources を指します。） になります。
web サービスでよく使いそうな以下の resources を作成し deploy します。

- Compute Network
- Compute SubNetwork
- Compute Disk
- IP address

CLI 実行環境を構築するために、まず以下の環境を構築します

- Terraform CLI
- Node.jsおよび npm v16
- gcloud
- GCP project
- Typescript 実行環境

上記の環境を構築したら、CDK for Terraform 独自の環境を構築します。
```
npm install --global cdktf-cli@latest     // cdktf package install
cdktf help                                // check install
mkdir learn-cdktf && cd learn-cdktf       // create workingDirectory
cdktf init --template=typescript --local  // create project（入力すると以下質問が出る）
? Project Name         // 任意の名前
? Project Description  // 任意の説明
? Do you want to start from an existing Terraform project? （デフォルトNo）
? Do you want to send crash reports to the CDKTF team? （デフォルトYes）
? What providers do you want to use? google
```

質問に答えると、以下の構成ファイル群が生成されます。
```
.gitignore
.npmrc
__tests__
cdktf.json
help
jest.config.js
main.tsc
package-lock.json
package.json
setup.js
tsconfig.json
```

GCP provider を使えるようにするには、このファイル群があるディレクトリにて以下のcommand を実行します。（GCP 以外の各種 provider が欲しい際は公式を参照ください。）
```
npm install @cdktf/provider-google
```
これで CDK for Terraform × Typescript × GCP の環境が構築できました。


## Definition
環境を構築する章は、後半に記載をしています。

Stack という Terraform 用 CDK 専用の Terraform 構成として合成するインフラストラクチャのコレクションがあります。スタックを使用すると、アプリケーション内の複数の環境の状態管理を分離できます。（[Hashicorp：Stacks](https://developer.hashicorp.com/terraform/cdktf/concepts/stacks)）

構築する provider resources は、ルートディレクトリ直下の main.ts に記述します。

以下は初期状態。
```
// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
 
class MyStack extends TerraformStack {
 constructor(scope: Construct, id: string) {
   super(scope, id);
 
   // define resources here
 }
}
 
const app = new App();
new MyStack(app, "project-name"); // 環境構築時に質問で回答したproject名がここに入る
app.synth();
``` 

provider の instance を生成します。
config 情報の project の value には、実存する GCP project 名を記入ください。
```
   new GoogleProvider(this, "google", {
     project: "project-name",
   })
```
project 名を正確に書かずに deploy をした場合は以下の error がでます。

（project 名を hoge として deploy した結果）
```
│ Error: Error creating Disk: googleapi: Error 400: Consumer 'projects/hoge' is invalid: Resource projects/hoge could not be found..

```

provider instance を作成したら、任意の resources を書きます。

今回は以下ソースコードの resources を作成し deploy します。
resources は constructor 内に定義し、instance の生成は class の外で行います。

使用するGCP resourcesの使用方法は、公式リファレンスには殆ど記載がないので、
以下の GitHub のソースコードから使用する resources を import して、各 resources に必要な key と value を渡します。

[GitHub: cdktf-provider-google](https://github.com/cdktf/cdktf-provider-google)

```
 class MyStack extends TerraformStack {
 constructor(scope: Construct, id: string) {
   super(scope, id);
   // define resources here
 
   const ZONE = "asia-northeast1-b";
   const PROJECT_ID = "katakana-id";   // 任意のID　各resourcesに付与する
   const PROJECT_NAME = "katakana-tr"; // GCP projectの名前
   const PROJECT_PREFIX = "example";
   const PROJECT_PREFIX_NAME = `${PROJECT_PREFIX}-${PROJECT_NAME}`;
   const PROJECT_PREFIX_ID = `${PROJECT_PREFIX}-${PROJECT_ID}`;
 
   new GoogleProvider(this, "google", {
     project: PROJECT_NAME,
   })
 
   const network = new ComputeNetwork(this, `${PROJECT_PREFIX_ID}-network`, {
     name: `${PROJECT_PREFIX_NAME}-network`,
     routingMode:"REGIONAL",
     autoCreateSubnetworks:false,
   })
 
   const subnetwork = new ComputeSubnetwork(this, `${PROJECT_PREFIX_ID}-subnetwork`, {
     name: `${PROJECT_PREFIX_NAME}-subnetwork`,
     network: network.selfLink,
     ipCidrRange: "10.10.0.0/16",
     region: "asia-northeast1",
 });
 
   const address = new ComputeAddress(this, `${PROJECT_PREFIX_ID}-address`, {
     name: `${PROJECT_PREFIX_NAME}-address`,
     subnetwork: subnetwork.selfLink,
     region: subnetwork.region,
     addressType: "INTERNAL",
 });
 
   const disk = new ComputeDisk(this, `${PROJECT_PREFIX_ID}-disk`, {
     name: `${PROJECT_PREFIX_NAME}-disk`,
       zone: ZONE,
       type: "pd-standard",
       size: 10,
       image: "ubuntu-os-cloud/ubuntu-2204-lts",
   });
 
   new ComputeInstance(this, `${PROJECT_PREFIX_ID}-instance`, {
     name: `${PROJECT_PREFIX_NAME}-instance`,
       zone: ZONE,
       machineType: "e2-medium",
       bootDisk: {
         autoDelete: false,
         source: disk.selfLink
       },
       networkInterface: [
         {
           networkIp: address.address,
           subnetwork: subnetwork.selfLink,
         }
       ],
       canIpForward: false,
       tags: ["iap"]
   });
 }
}
 
const app = new App();
new MyStack(app, "dev");
new MyStack(app, "stg");
new MyStack(app, "prod");
app.synth();
```

工夫している点としては、保守性とメンテナンス性を上げる為に上記の様に prefix や zone、ID を変数として持たせます。

また、Stack を 環境毎に初期化することで、同じ宣言内容で個別の deploy が可能となります。

個人的な感想としては、私が今まで試した他の IaC ツールよりは可読性が高い状態で各環境の画一性を担保をする記述ができたかと感じました。ただツールによって一長一短があるので、開発環境や技術者の得手不得手でどのツールを採用するかの判断が分かれると思います。


## Deploy
最後に deploy です。
手順としては、GCP 認証・deploy 内容の確認・対象を指定して deploy、です。

### GCP認証

以下のコマンドで GCP への認証をします。
```
gcloud auth application-default login
```
GoogleProviderClass の configという引数の credentials という入力用変数でも認証を行うことができますが、今回は楽に認証が行えるコマンド認証を使っています。

deploy内容の確認
`cdktf plan`コマンドで確認します。出力内容を全てを載せると量が多くなる為、
以下は、google_compute_instance の deploy 内容です。

```
cdktf plan
create-disk    # google_compute_instance.example-katakana-id-instance (example-katakana-id-instance) will be created
              + resource "google_compute_instance" "example-katakana-id-instance" {
                  + can_ip_forward       = false
                  + cpu_platform         = (known after apply)
                  + current_status       = (known after apply)
                  + deletion_protection  = false
                  + guest_accelerator    = (known after apply)
                  + id                   = (known after apply)
                  + instance_id          = (known after apply)
                  + label_fingerprint    = (known after apply)
                  + machine_type         = "e2-medium"
                  + metadata_fingerprint = (known after apply)
                  + min_cpu_platform     = (known after apply)
                  + name                 = "example-katakana-tr-instance"
                  + project              = (known after apply)
                  + self_link            = (known after apply)
                  + tags                 = [
                      + "iap",
                    ]
                  + tags_fingerprint     = (known after apply)
                  + zone   
省略
``` 
 
 対象を指定し deploy
複数の stack がある場合は、stack 名を指定しないと以下の error になります。

```
cdktf plan (stack 名)
Usage Error: Found more than one stack, please specify a target stack. Run cdktf deploy <stack> with one of these stacks: (stack名), (stack名2)
```

deploy 対象に間違いなければ deploy します。
この場合も、複数 stack がある場合は、指定します。
deploy 時、以下の3つの選択肢があります、Approve をすると deploy されます。

- cdktf deploy (stack名)
  - Approve
  - Dismiss
  - Stop

error が無ければ、cdktf plan で出力された内容が出力され、
最後に以下の出力が表示されて、deploy 完了です。

```
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
```

画像は、私の個人環境にて、今回 deploy された resources になります。
これで、実践の章で作りたかった GCP の Network , SubNetwork , IP address , Disk が作成できました。
<figure class="figure-image figure-image-fotolife" title="GCP deploy">[f:id:katakana333:20221216021215p:plain]<figcaption>GCP deploy</figcaption></figure>


# 終わりに
## 使用感
今回の実践にあたり、
「CDK for Terraform × Typescript × GCP」と「Terraform × HCL × AWS」の使い心地を比較する為に、両者で IaC と deploy を行いました。
その上で、CDK for Terraform の使い心地が以下となります。

- Terraform の利点を享受できたこと。（環境構築、diff、deployのしやすさ。）
- GCP で構築と deploy をしたが、実装の感覚は AWS で行った際と変わらずに実践できた。
- Typescript フレンドリーに package の import、provider resources の実装ができる。
- 各 provider で使用する resources class を把握すれば、ESLint 等の Lint を入れてる人は恩恵がわかりやすく、言語補完が効くのでスラスラ書ける。
- 変数や関数の再利用が書きやすく、読みやすい。


最後に私が個人開発をする際は、CDK for Terraform を使うと思います。
最大の理由は殆ど同じ書き方で AWS と GCP の両者の IaC 化を行える事です。
ベンダーロックインの回避と片方のクラウドが利用できなくなった際に備えるのが目的になります。

RevComm では一緒に働く仲間を募集しています。
このブログを読んで興味を持ってくれた方、そうでない方も、
ぜひ採用サイトをチェックしてみてください！
www.revcomm.co.jp