既存のGCPインフラをCDKTFにImportするぞ

2023/12/01に公開

こんにちはこんにちはTERASSのこうさかです。
この記事はterraform Advent Calendar 2023の10日目の記事です。
構築済みのGCPインフラをCDKTFに取り込む機会があったので、実際に取り組んだ手順をまとめます。

背景

会社ドメインのDNSレコードをGCP Cloud DNSで管理しておりレコードが大量にあり、最早なにがどのレコードか分からない状態になっている。なのでこれをなんとかしてコメント追加やセグメント分けして分かりやすく管理できる状態にしたい。特にterraform管理しているわけではなくて、レコードの追加依頼をもらったら手動でポチポチ追加するというオペレーションで回してました。

ゴール

  • DNSゾーン, DNSレコードをCDKTF配下で管理できるようになること

やりかた

config-driven import使い、コードの生成, コードへの取り込み, ステートへの取り込みという流れでやってみました。

  1. generateConfigForImport メソッドを各リソースに対して呼び(cdktf plan)CDKTFのコードを生成する
  2. 生成されたコードを転記しコード上のCDKTFのスタックに追加
  3. 転記したリソースのimportFromメソッドを呼び(cdktf plan)、実インフラとの差分を確認する
  4. cdktf deploy してステートファイルへ取り込み
  5. importFrom メソッドをコードから削除

この手順でやると、最初からコード書かずにある程度生成にまかせてインポートすることができ楽でした。

参考:
https://developer.hashicorp.com/terraform/cdktf/concepts/resources#importing-resources
https://dev.classmethod.jp/articles/cdktf-config-driven-import/

やってみる

1. generateConfigForImport メソッドを各リソースに対して呼び(cdktf plan)CDKTFのコードを生成する

今回は DnSManagedZone のインポートをやっていきます。
まず、GCPのManagedZone コード生成するためのコードをCDKTF配下のmain.tsに書きます。

main.ts
DnsManagedZone.generateConfigForImports(this, <resorce_id>)

上記のように欲しいリソースに対して generateConfigForImports を呼び出すことでcdktf plan した際に標準出力に実際のインフラのコードが出力されます。 resource_id の指定はインポートしようとしているリソースごとに異なりますが、このように terraformのリファレンスにIdの形式が書いてあるのでそれに従って指定します。
cdktf planしてみます。

$ cdktf plan

....
new DnsManagedZone(this, "terass-dns", {
  cloud_logging_config: [
    {
      enable_logging: false,
    },
  ],
  description: [null],
  dns_name: "*****",
  ....
})
....

がちゃがちゃ長いログが出ますが、上記のようにTypeScriptのコードが出力されます。

2. 生成されたコードを転記しコード上のCDKTFのスタックに追加

上記で出力されたソースに貼り付け修正してきます。
今回修正は

  • 特に設定していない項目がデフォルト値入で出てくるのでそこの削除
  • スネークケースをキャメルケースに変更
  • terraform上でのidの変更
    を行いました。特にデフォルト値の項目が大量に入っていると大変ややこしいので必要な項目に絞りコード上に反映します。また、プロパティがスネークケースになっておりTypeエラーが出るのでフィールド名変更を行いました。

3. 転記したリソースのimportFromメソッドを呼び(cdktf plan)、実インフラとの差分を見つつ取り込む

転記して修正したコードに対し、下記のようにimportFromメソッドを叩きます。

main.ts
new DnsRecordSnew DnsManagedZone(this, "terass-dev", {
  cloudLoggingConfig: {
    enableLogging: false,
  },
  dnsName: "....",
  name: "terass-dns",
  visibility: "public",
}).importFrom(<resourceId>)

この状態で cdktf plan を叩きます。

$ cdktf plan
....
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
....

planの結果 1 to import0 change なので既存のGCP側の設定と変更なく取り込めそうです。

4. cdktf deploy してステートファイルへ取り込み

$ cdktf deploy
terass-dns *** (terass-dev): Importing... [id=<resource_id>]
           *** (terass-dev): Import complete [id=<resource_id>]

叩くとterraformステートにとりこまれました。これでソースファイルとterraform ステート両方にとりこまれたことになります

5.importFrom メソッドをコードから削除

importFrom を書いたままだとdeploy のたびに取り込むことになるので、削除してソースとステートが揃ってる状態にして取り込み完了です。

main.ts
new DnsRecordSnew DnsManagedZone(this, "terass-dev", {
  cloudLoggingConfig: {
    enableLogging: false,
  },
  dnsName: "....",
  name: "terass-dns",
  visibility: "public",
})

DNS recordの取り込み

ここからはCDKTFのインポートの方法と直接関わりが無いですが、アプローチとしてはありだと思うので興味ない人は読み飛ばしてください。
DNSレコードも上記と同じ方法で取り込めるのですが、レコード一つ一つがリソースとして独立しているので、100レコードあれば100リソースを取り込む必要があります。手動で書いていくのは大変めんどくさいし労力がかかります。幸いrecordの定義自体はかなりシンプルなので、recordをエクスポートしてそこからコード生成することにしました。

レコードセット自体は

gcloud dns record-sets export records.yaml --zone=<zone-name>
// https://cloud.google.com/sdk/gcloud/reference/dns/record-sets/export

このようなコマンドで吐き出せます。yaml形式でゾーン内のレコードを全て吐き出せます。
このyamlをもとにcdktfのコードを吐き出して対応することにします。

recordSetGen.ts
import fs from "fs"

import { parseAllDocuments } from "yaml"

const records = parseAllDocuments(
  fs.readFileSync("./records.yaml").toString(),
).map((doc) => doc.toJS())

const genRecord = (
  zone: string,
  name: string,
  type: string,
  ttl: number,
  rrdatas: string[],
) => {
  return `
new DnsRecordSet(this, "${type}-${name
    .split(".")
    .filter((c) => c)
    .join("-")}", {
  managedZone: "${zone}",
  name: "${name}",
  rrdatas: [${rrdatas
    .map((r) => (r.includes(" ") && type === "TXT" ? `"\\"${r}\\""` : `"${r}"`))
    .join(",")}],
  ttl: ${ttl},
  type: "${type}",
}).importFrom("${zone}/${name}/${type}")`
}

for (const record of records) {
  const name = record.name
  const type = record.type
  const ttl = record.ttl
  const rrdatas = record.rrdatas

  console.log(genRecord(<zone_name>, name, type, ttl, rrdatas))
}

上記のコードでyamlを読み取ってレコードのリソースを定義しました。
generateConfigForImports のかわりにコード生成を用いました。生成するコードに importFrom をつけておいてインポート用の記述として出力。 出力したコードを含んだ状態でcdktf plan,cdktf deployして、その後 importFrom 分を削除した記述に差し替えて置くことでインポートしました。手作業でやるよりはだいぶ楽になりました。
もしかしたらterrform importを用いて全てインポートした後コンバートする事もできるかもしれませんが、試してないです。もっといい方法があったら教えてほしい

所管

config-driven import の仕組みを使い、部分的にソースとステートを同期することができました。
実インフラとステート, コードそれぞれの差分を確認しながら作業できるのである程度安心して作業できたと思います。特にimportFrom メソッドは差分があればコードとインフラの違いが出力されるのでわかりやすかったです。config-driven import 素晴らしい!!

作業のログ

参考までに
https://zenn.dev/k_saka/scraps/9bdb65ce359e99

Terass Tech Blog

Discussion