🐯

既存のRoute 53リソースをCDK管理にした話

2022/01/14に公開

はじめに

こんにちは。レンティオ株式会社でエンジニアをしている木内です。

レンティオでは AWS CDK を採用していて、新規のリソース追加などは基本的に TypeScript で記述してインフラのコード化を進めています。

現在はこのような運用になってはいますが、CDK 採用前に AWS コンソール上から作成したリソースも一部残っていました。
Route 53 のリソースはその一部だったのですが、以下のような課題がありました。

  • レコードセットが多く、管理が煩雑になっていた
  • レコードセットの追加、更新の経緯が追いづらい

今回はこのような課題があった既存の Route 53 リソースを CDK 管理下に移行する作業をしたので、その手順やハマりポイントについて、記事を書いてみました。
作業中に調べてもあまりヒットしない内容だったので、どなたかのお役に立てばうれしいです。

手順では以下のような ホストゾーン、レコードセットを例に実際にインポートをしてみたいと思います。

  • ホストゾーン example.com
    • CNAMEレコード cname.example.com
    • TXTレコード txt.example.com

※ ホストゾーン名は仮の名前です。実際には別名で行っています
※ ホストゾーンを作成するとデフォルトでNSレコードとSOAレコードが作成されますが、今回は手動で作成したレコードセットを対象にするため割愛します

CDK のバージョンは 1.134.0 を使用しています。

ホストゾーンをインポート

既存リソースを CDK 管理下に置くにはまず CloudFormation のリソースインポート機能を利用する必要があります。
リソースインポート機能を使うには CFn テンプレートが必要ですので、cdk synth コマンドを使用して生成します。具体的な手順は以下です。

① 既存リソースとまったく同じ zoneName でコードを記述する

今回のサンプルは example.com です。

import * as cdk from '@aws-cdk/core'
import * as route53 from '@aws-cdk/aws-route53'

export class ExampleComStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const hostedZone = new route53.HostedZone(this, 'HostedZone', {
      zoneName: 'example.com',
    })
    hostedZone.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)
  }
}

② CFn テンプレートを作成する

上記のコードをもとに cdk synth でテンプレートを生成します。

Terminal
$ cdk synth ExampleComStack

実行すると以下のようなテンプレートが cdk.out/ 配下に生成されるかと思います。

cdk.out/ExampleComStack.template.json
{
  "Resources": {
    "HostedZoneDB99F866": {
      "Type": "AWS::Route53::HostedZone",
      "Properties": {
        "Name": "example.com."
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/HostedZone/Resource"
      }
    },
    "CDKMetadata": {
      "Type": "AWS::CDK::Metadata",
      "Properties": {
        "Analytics": "v2:deflate64:H4sIAAAAAAAA/0WNsQrCQAxAv8X9mnpUnYUuznVzK7kI12ICSU6H4/7dFgenx4MHL0IcTnA8XOePdZjWvqIoQb37jGsYhc21oIfxyROZFEVqYW9VitN5gHoTc0oPYdqjv7UWWBLBYv07XrbNdlks504Le34RTD9+ARW44jiCAAAA"
      },
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/CDKMetadata/Default"
      }
    }
  }
}

③ CFn テンプレートを加工する

リソースインポートを行うためには CDKMetadata を含まないように削除する必要があります。

{
  "Resources": {
    "HostedZoneDB99F866": {
      "Type": "AWS::Route53::HostedZone",
      "Properties": {
        "Name": "example.com."
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/HostedZone/Resource"
      }
    }
    // CDKMetadata を削除
  }
}

④ CFn テンプレートを使って AWS コンソール上からインポート

CFn テンプレートが作成できたら AWS コンソール上からインポートをします。
インポート操作の手順は最新の公式ドキュメントを参照してください。
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html

インポートができたら cdk diff コマンドで確認してみましょう。
インポート前後で CDK 管理下に置かれていることが確認できます。

インポート前

Terminal
$ cdk diff ExampleComStack
Stack ExampleComStack
Resources
[+] AWS::Route53::HostedZone HostedZone HostedZoneDB99F866

インポート後
There were no differences となり、CDK 管理下になりました。

Terminal
$ cdk diff ExampleComStack
Stack ExampleComStack
There were no differences

これらの手順で既存のホストゾーンを CDK 管理下に置くことができましたが、実はホストゾーン内のレコードセットはこの手順で CDK 管理下に置くことはできません...
理由は公式ドキュメントにもある通りで、レコードセットは2022年1月現在リソースインポート機能がサポートされていないためです。
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html?icmpid=docs_cfn_console

冒頭でも少し書いたように、レコードセットを含めてのインポートは調べても全然記事が見つからない...と、ここで諦めかけていたのですが、以下の手順でレコードセットも CDK 管理下に含めることができました🎉

レコードセットをインポート

① 既存のレコードセットとまったく同じ設定値、属性でレコード定義を CDK で記述する

ホストゾーンは先ほどインポートしたホストゾーンを指定します。

import * as cdk from '@aws-cdk/core'
import * as route53 from '@aws-cdk/aws-route53'

export class ExampleComStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const hostedZone = new route53.HostedZone(this, 'HostedZone', {
      zoneName: 'example.com',
    })
    hostedZone.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)

    // 追加
    new route53.CnameRecord(this, 'CnameRecord', {
      zone: hostedZone,
      recordName: 'cname.example.com',
      domainName: 'example.com',
      ttl: cdk.Duration.seconds(300),
    }).applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)

    // 追加
    new route53.TxtRecord(this, 'TestTxtRecord', {
      zone: hostedZone,
      recordName: 'txt.example.com',
      values: ['foobarbaz'],
      ttl: cdk.Duration.seconds(300),
    }).applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)
  }
}

cdk deploy

まったく同じ設定値で記述した後に cdk deploy します。

Terminal
$ cdk deploy ExampleComStack
ExampleComStack: deploying...
ExampleComStack: creating CloudFormation changeset...

 ✅  ExampleComStack

...略

ここ注意ですが、まったく同じ設定値でないとエラーが発生します。
以下は cname.example.com の ttl を異なる値(500)で cdk deploy した例です。

Terminal
$ cdk deploy ExampleComStack
ExampleComStack: deploying...
ExampleComStack: creating CloudFormation changeset...
...略
[Tried to create resource record set [name='cname.example.com.', type='CNAME'] but it already exists]

③ CDK 管理下になる

試しに cdk synth でテンプレートを出力してみましょう。

Terminal
$ cdk synth ExampleComStack

先ほど cdk deploy した レコードセットがスタックに含まれていることが確認できます。

{
  "Resources": {
    "HostedZoneDB99F866": {
      "Type": "AWS::Route53::HostedZone",
      "Properties": {
        "Name": "example.com."
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/HostedZone/Resource"
      }
    },
    "CnameRecord2351B7B9": {
      "Type": "AWS::Route53::RecordSet",
      "Properties": {
        "Name": "cname.example.com.",
        "Type": "CNAME",
        "HostedZoneId": {
          "Ref": "HostedZoneDB99F866"
        },
        "ResourceRecords": [
          "example.com"
        ],
        "TTL": "300"
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/CnameRecord/Resource"
      }
    },
    "TestTxtRecord4C1BB28D": {
      "Type": "AWS::Route53::RecordSet",
      "Properties": {
        "Name": "txt.example.com.",
        "Type": "TXT",
        "HostedZoneId": {
          "Ref": "HostedZoneDB99F866"
        },
        "ResourceRecords": [
          "\"foobarbaz\""
        ],
        "TTL": "300"
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/TestTxtRecord/Resource"
      }
    },
    "CDKMetadata": {
      "Type": "AWS::CDK::Metadata",
      "Properties": {
        "Analytics": "v2:deflate64:H4sIAAAAAAAA/02OMQvDIBSEf0t3YyppOxeydDaduom+ggnxge+ZFsT/XsWl093Bd8cpqaaLPJ/u5kODdduYLUaQeWFjNzFjII7JspjfQQNhihaKaGzExHCdZH4gMbgXBmjQfwpmBw11z/V6cwuweH65h1JEQAdypfFQt/qkHlnJ+yGmwH4Hqbv+ANxfmWqlAAAA"
      },
      "Metadata": {
        "aws:cdk:path": "ExampleComStack/CDKMetadata/Default"
      }
    }
  }
}

また、試しに ttl を300 -> 500 にしてみて cdk diff をしてみると差分を確認でき、CDK ネイティブのリソースとして管理できるようになっています。

Terminal
$ cdk diff ExampleComStack
Stack ExampleComStack
Resources
[~] AWS::Route53::RecordSet CnameRecord CnameRecord2351B7B9
 └─ [~] TTL
     ├─ [-] 300
     └─ [+] 500

なお今回のようにテスト用のレコードセットを用意して事前に検証しましたが、cdk deploy 中のダウンタイム等はありませんでした。

ハマりポイント

以上の手順でうまくいったのですが、ハマりポイントがあったので最後に共有させていただきます。
基本同じ設定値にすればうまくいくのですが、TxtRecord をインポートする際にうまくいかないことがありました。
具体的には TXTRecord でまれに値にダブルクオートを含むことがありますが、その場合だと TXTRecord コンストラクタ を使用するとうまくいきませんでした。
https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-route53.TxtRecord.html
たとえば以下のようにダブルクオートが含まれるような値だと、エスケープ処理がうまくいかないみたいでした。

  new route53.TxtRecord(this, 'TxtRecord', {
    zone: hostedZone,
    recordName: 'txt.example.com',
    values: ['foobar""baz'],
    ttl: cdk.Duration.seconds(300),
  }).applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)

試しに cdk synth してみると、

以下のようになってしまい、まったく同じ設定値とみなされずに例により [Tried to create resource record set [name='txt.example.com.', type='TXT'] but it already exists]
となってしまいました。

"ResourceRecords": [
  "\"foobar\\\"\\\"baz\""
],

これは0から CDK で TXTRecord を作るときも起きる現象だった(※2022/1/7現在)ので、厳密にはインポートとは関係はなかったのですが、代わりに RecordSet コンストラクタを使うとうまくいきました。
https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-route53.RecordSet.html

  // RecordSet に変更
  new route53.RecordSet(this, 'TxtRecord', {
    zone: hostedZone,
    recordType: route53.RecordType.TXT,
    target: route53.RecordTarget.fromValues(
      '"foobar""baz"',
    ),
    recordName: 'txt.example.com',
    ttl: cdk.Duration.seconds(300),
  }).applyRemovalPolicy(cdk.RemovalPolicy.RETAIN)

ドキュメントのサンプルコードに記載があるコメントで Will be quoted for you, and " will be escaped automatically. とあるので、
内部でのエスケープ処理がうまくいっていないのかもしれないですね。

declare const myZone: route53.HostedZone;

new route53.TxtRecord(this, 'TXTRecord', {
  zone: myZone,
  recordName: '_foo',  // If the name ends with a ".", it will be used as-is;
                       // if it ends with a "." followed by the zone name, a trailing "." will be added automatically;
                       // otherwise, a ".", the zone name, and a trailing "." will be added automatically.
                       // Defaults to zone root if not specified.
  values: [            // Will be quoted for you, and " will be escaped automatically.
    'Bar!',
    'Baz?',
  ],
  ttl: Duration.minutes(90),       // Optional - default is 30 minutes
});

まとめ

長くなりましたが以上になります。
最終的にスタックに含まれるリソースは以下のようになり、CDK を通じて更新などを行えるようになりました。

結構なレアケースだとは思いますが、過去にコンソール上から作成した既存リソースを CDK 管理下にしたい方がもしいたら、お役に立てたらうれしいです。

採用情報

レンティオでは絶賛、エンジニアを募集しています!
https://www.wantedly.com/companies/rentio

Discussion