既存のRoute 53リソースをCDK管理にした話
はじめに
こんにちは。レンティオ株式会社でエンジニアをしている木内です。
レンティオでは AWS CDK を採用していて、新規のリソース追加などは基本的に TypeScript で記述してインフラのコード化を進めています。
現在はこのような運用になってはいますが、CDK 採用前に AWS コンソール上から作成したリソースも一部残っていました。
Route 53 のリソースはその一部だったのですが、以下のような課題がありました。
- レコードセットが多く、管理が煩雑になっていた
- レコードセットの追加、更新の経緯が追いづらい
今回はこのような課題があった既存の Route 53 リソースを CDK 管理下に移行する作業をしたので、その手順やハマりポイントについて、記事を書いてみました。
作業中に調べてもあまりヒットしない内容だったので、どなたかのお役に立てばうれしいです。
手順では以下のような ホストゾーン、レコードセットを例に実際にインポートをしてみたいと思います。
- ホストゾーン
example.com
- CNAMEレコード
cname.example.com
- TXTレコード
txt.example.com
- CNAMEレコード
※ ホストゾーン名は仮の名前です。実際には別名で行っています
※ ホストゾーンを作成するとデフォルトで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
でテンプレートを生成します。
$ cdk synth ExampleComStack
実行すると以下のようなテンプレートが cdk.out/
配下に生成されるかと思います。
{
"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 コンソール上からインポートをします。
インポート操作の手順は最新の公式ドキュメントを参照してください。
インポートができたら cdk diff
コマンドで確認してみましょう。
インポート前後で CDK 管理下に置かれていることが確認できます。
インポート前
$ cdk diff ExampleComStack
Stack ExampleComStack
Resources
[+] AWS::Route53::HostedZone HostedZone HostedZoneDB99F866
インポート後
There were no differences
となり、CDK 管理下になりました。
$ cdk diff ExampleComStack
Stack ExampleComStack
There were no differences
これらの手順で既存のホストゾーンを CDK 管理下に置くことができましたが、実はホストゾーン内のレコードセットはこの手順で CDK 管理下に置くことはできません...
理由は公式ドキュメントにもある通りで、レコードセットは2022年1月現在リソースインポート機能がサポートされていないためです。
冒頭でも少し書いたように、レコードセットを含めてのインポートは調べても全然記事が見つからない...と、ここで諦めかけていたのですが、以下の手順でレコードセットも 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
します。
$ cdk deploy ExampleComStack
ExampleComStack: deploying...
ExampleComStack: creating CloudFormation changeset...
✅ ExampleComStack
...略
ここ注意ですが、まったく同じ設定値でないとエラーが発生します。
以下は cname.example.com
の ttl を異なる値(500)で cdk deploy
した例です。
$ 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
でテンプレートを出力してみましょう。
$ 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 ネイティブのリソースとして管理できるようになっています。
$ cdk diff ExampleComStack
Stack ExampleComStack
Resources
[~] AWS::Route53::RecordSet CnameRecord CnameRecord2351B7B9
└─ [~] TTL
├─ [-] 300
└─ [+] 500
なお今回のようにテスト用のレコードセットを用意して事前に検証しましたが、cdk deploy
中のダウンタイム等はありませんでした。
ハマりポイント
以上の手順でうまくいったのですが、ハマりポイントがあったので最後に共有させていただきます。
基本同じ設定値にすればうまくいくのですが、TxtRecord
をインポートする際にうまくいかないことがありました。
具体的には TXTRecord
でまれに値にダブルクオートを含むことがありますが、その場合だと TXTRecord
コンストラクタ を使用するとうまくいきませんでした。
たとえば以下のようにダブルクオートが含まれるような値だと、エスケープ処理がうまくいかないみたいでした。
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
コンストラクタを使うとうまくいきました。
// 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 管理下にしたい方がもしいたら、お役に立てたらうれしいです。
採用情報
レンティオでは絶賛、エンジニアを募集しています!
Discussion