Open16

既存のGCP Cloud DNSをCDKTFで管理したい

k-sakak-saka

背景

  • CloudDNSで会社ドメインのゾーンを作っている
  • メールの認証やらSaasやらのドメイン認証系のレコードが雑多に存在してて管理しきれないのでIacしたい

ゴール

  • CDKTFでDNSレコードの管理ができている状態
  • メンバーがPRを投げてDNSレコードを変更できるような状態を目指す
k-sakak-saka

とりあえずcdktfの初期化をする

cdktf init

言語はTypescriptを選択, デフォルトだとnpmベースなので諸々整えてpnpmへ移行

k-sakak-saka

prebuildなプロバイダーを使う

package.json
  "dependencies": {
    "cdktf": "0.19.1",
    "constructs": "10.3.0",
    "@cdktf/provider-google": "12.0.3",
    "@cdktf/provider-google-beta": "12.0.3"
  },
k-sakak-saka

GCPバックエンド用のバケットを作る

gcloud --project=*** storage buckets create gs://***** --location=asia-northeast1 --versioning
k-sakak-saka

とりあえず、バックエンド向けの設定を書きつつストレージ作ってみる

main.ts
import { GoogleProvider } from "@cdktf/provider-google/lib/provider"
import { StorageBucket } from "@cdktf/provider-google/lib/storage-bucket"
import { App, TerraformStack, GcsBackend } from "cdktf"
import { Construct } from "constructs"

class DNS extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id)

    new GcsBackend(this, {
      bucket: "terraform-state",
      prefix: "terraform/state",
    })

    new GoogleProvider(this, "google", {
      project: "*****",
    })

    new StorageBucket(this, "bucket", {
      name: "****",
      location: "asia-northeast1",
    })
  }
}

const app = new App()
new DNS(app, "dns")
app.synth()
pnpm cdktf plan                                                                                                                                    

なんかいい感じにバックエンドの初期化が行われつつDiffが出てくる。
デプロイしてみる

pnpm cdktf deploy

がちゃがちゃ出力されつつ作成GCSが作成された

k-sakak-saka
main.ts
    new StorageBucket(this, "bucket", {
      name: "****",
      location: "asia-northeast1",
    })

ここを削除してもっかい deploy して消しとく

k-sakak-saka

おっかなすぎるので、バックアップはとっとく

gcloud dns record-sets export records.yaml --zone=terass***
k-sakak-saka

https://cloud.google.com/docs/terraform/resource-management/import?hl=ja#import-resources-one-at-a-time
これを試してみる
generateConfigForImport を使い。stateファイルには取り込まずにcdktfのconfigを吐かせてみる。適当にGCS作ってそいつをimportしてみる。tera-one-adsfa ってのをコンソールから作成し、

    StorageBucket.generateConfigForImport(
      this,
      "gcs-import-sample",
      "tera-one-adsfa",
    )

こんな感じで、定義する。

pnpm cdktf plan                                                                                                                                    

するとつらつら出てくるが、

import { Construct } from "constructs"
/*
 * Provider bindings are generated by running `cdktf get`.
 * See https://cdk.tf/provider-generation for more details.
 */
import { StorageBucket } from "./.gen/providers/google/storage-bucket"
class MyConvertedCode extends Construct {
  constructor(scope: Construct, name: string) {
    super(scope, name)
    /*The following providers are missing schema information and might need manual adjustments to synthesize correctly: google.
    For a more precise conversion please use the --provider flag in convert.*/
    new StorageBucket(this, "gcs-import-sample", {
      default_event_based_hold: false,
      enable_object_retention: false,
      force_destroy: false,
      labels: [{}],
      location: "ASIA-NORTHEAST1",
      name: "tera-one-adsfa",
      project: "****",
      public_access_prevention: "enforced",
      requester_pays: false,
      storage_class: "STANDARD",
      timeouts: [
        {
          create: [null],
          read: [null],
          update: [null],
        },
      ],
      uniform_bucket_level_access: true,
    })
  }
}

それっぽいのが出てきた。
プロバイダー云々言われているが、下記のようにProvider指定しても出力は変わらなかった。

    const provider = new GoogleProvider(this, "google", {
      project: "***",
    })

    StorageBucket.generateConfigForImport(
      this,
      "gcs-import-sample",
      "tera-one-adsfa",
      provider,
    )

prebuildのものではなくビルドすべきなのだろうか?分からないが多分こんなもんなんだろうと思う。
今回はこのまま進めてみる
おためしで、これをそのままつかって取り込んでみる。
上記コードをmain.tfのスタック中にいれ、importFromStorageBucketのインスタスに対してコールしてやるとよさそう。

Though at this point, your resource has not been imported. To import, first add the new generated configuration to your project, then remove the initial call of generateConfigForImport. Finally, follow the steps outlined in the section "How To Import" above. On apply, your resource will be imported, then becoming managed by CDKTF.
https://developer.hashicorp.com/terraform/cdktf/concepts/resources#importing-resources

main.ts
    new StorageBucket(this, "gcs-import-sample", {
      location: "ASIA-NORTHEAST1",
      name: "tera-one-adsfa",
      project: "***",
      uniformBucketLevelAccess: true,
    }).importFrom("tera-one-adsfa")

いらないところを削除しつつ、main.tsに記述 & generateConfigForImport文を削除
この状態で plan してみる

$ pnpm cdktf plan
...
tera-one-dns    # google_storage_bucket.gcs-import-sample (gcs-import-sample) will be imported
                  resource "google_storage_bucket" "gcs-import-sample" {
                      default_event_based_hold    = false
                      effective_labels            = {}
                      enable_object_retention     = false
                      force_destroy               = false
                      id                          = "tera-one-adsfa"
                      labels                      = {}
                      location                    = "ASIA-NORTHEAST1"
                      name                        = "tera-one-adsfa"
                      project                     = "***"
                      public_access_prevention    = "enforced"
                      requester_pays              = false
                      self_link                   = "https://www.googleapis.com/storage/v1/b/tera-one-adsfa"
                      storage_class               = "STANDARD"
                      terraform_labels            = {}
                      uniform_bucket_level_access = true
                      url                         = "gs://tera-one-adsfa"

                      timeouts {}
                  }

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

cdktf deploy(terraform apply)すれば上記の状態にterraform stateが更新されるようだ。
やってみる

$ pnpm cdktf deploy
...
tera-one-dns  google_storage_bucket.gcs-import-sample (gcs-import-sample): Importing... [id=tera-one-adsfa]
              google_storage_bucket.gcs-import-sample (gcs-import-sample): Import complete [id=tera-one-adsfa]
...

インポートされたっぽい。この状態で importFrom("tera-one-adsfa") という記述を削除して、もう一度cdktf planしてみる

[kosaka]% pnpm cdktf plan
tera-one-dns  Initializing the backend...
tera-one-dns  Initializing provider plugins...
tera-one-dns  - Reusing previous version of hashicorp/google from the dependency lock file
tera-one-dns  - Using previously-installed hashicorp/google v5.6.0
tera-one-dns  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.
tera-one-dns  google_storage_bucket.gcs-import-sample (gcs-import-sample): Refreshing state... [id=tera-one-adsfa]
tera-one-dns  No changes. Your infrastructure matches the configuration.

              Terraform has compared your real infrastructure against your configuration
              and found no differences, so no changes are needed.

良さそう。StorageBucketの記述をしつつ、変更がないってことはstateに無事反映されたということだ

k-sakak-saka

じゃあ、cloud dnsをやっていく
基本的な構成を調べる

dns_managed_zone, dns_record_set当たりを使えば良さそう。
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_record_set

dns_managed_zoneをインポートしてみる

main.ts
DnsManagedZone.generateConfigForImports(this, ....)
$ pnpm cdktf plan
.....
new DnsManagedZone(this, "terass-dev", {
                    cloud_logging_config: [
                      {
                        enable_logging: false,
                      },
                    ],
                    description: [null],
                    dns_name: "......",
....

StorageBucket と同じ流れて、上記アウトプットを必要な部分だけ抜き出してmain.tsに書いてimportFromする

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

これを今度はcdktf deployして、importFromを消してやる

$ pnpm cdktf deploy
.....
# importFrom を削除して
$ pnpm cdktf plan
....
tera-one-dns  No changes. Your infrastructure matches the configuration.

ということで、zoneをterraform stateファイルとtypescriptのソース両方に取り込めた

k-sakak-saka

record_setが問題だ。
importしようにも数が多い。リソースとしてそれぞれ定義する形になるため一つづつimportする必要がある。今回はとりあえず開発用のテストzoneなので数がないからいいとしても、本番どうしよう? コード生成 or 手動で書きつつ抜けの内容にテストを書いてみる or その他もっとイケてる方法...
バックアップのところでexportしたテキストファイルもあるし、数の多いAレコード, CNAME, TXTレコードあたりだけでも生成できると楽かもしれない

k-sakak-saka

とりあえず、数の問題は忘れて一旦適当にimport してみるか

k-sakak-saka

exportしたファイル読んで生成することにする
.importFrom付きとそうじゃないバージョンを出力できるようにして、import時の確認ができるように作ってみる

k-sakak-saka
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("tera-one-dev", name, type, ttl, rrdatas))
}

こんな感じか?

$ pnpm ts-node recordSetGen.ts 
....
new DnsRecordSet(this, "A-****dev", {
  managedZone: "tera-one-dev",
  name: "import****",
  rrdatas: ["192.168.0.1"],
  ttl: 300,
  type: "A",
}).importFrom("tera-one-dev/*****/A")
....

出力できたので、これをmain.tsに突っ込んでplanしてみる。

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

ズラッと出ますが、結果レコードがimportされて0 changeなので実際のインフラとの差分はなさそうです。変更があると変更箇所と変更数が結果にでるが、今回はでてないので大丈夫そう。

インポートするぞ

$ pnpm cdktf deploy
...
 Apply complete! Resources: 7 imported, 0 added, 0 changed, 0 destroyed.
...

大丈夫そう。importFromの記述を削除してplanしてみる

$ pnpm cdktf plan
...
tera-one-dns  No changes. Your infrastructure matches the configuration.
...

レコードインポートできた。

k-sakak-saka

main.tsに全部書いてくのは量も増えるし管理しにくいので分離できるようにしてみる

k-sakak-saka

コンストラクタの中でthis を引数で渡しつつ関数呼び出しすればおk