external-dns + AWS Route53でKubernetesリソースからDNSレコードを自動生成する

Kubernetesでアプリケーションを運用していると、Service や Ingress に紐づくDNSレコードの管理が必要になります。このDNSレコードの作成・更新・削除を自動化できれば、運用の手間を大幅に削減できます。

本記事では、KubernetesリソースからAWS Route53のDNSレコードを自動的に作成・管理するための「external-dns」の使い方を解説します。

レコードの管理を自動化するには、Crossplaneなども選択肢に入りますが、これはクラウドベンダーごとにコンポジションというマッピングルールを作らないといけませんが、external-dnsを使うと、AWS Route53だけでなく様々なネームサーバーに統一的な方法で対応できるので、様々なネームサーバーに対応したいときにはかなり便利です。

## 前提条件

- Kubernetesクラスターがセットアップされていること
- 必要なコマンドツールがインストールされていること
    - kubectl
    - helm
    - aws
    - dig

## external-dnsとAWS Route53について

### external-dnsとは

external-dnsは、Kubernetesリソースを監視し、DNSレコードを自動的に更新するツールです。Service、Ingress、Gatewayなどのリソースを監視し、設定に基づいてDNSレコードを作成・更新・削除します。

### AWS Route53とは

AWS Route53は、AWSが提供するマネージドDNSサービスです。高可用性と拡張性を備えており、複数のルーティングポリシーやヘルスチェックなどの機能を提供します。

## セットアップ手順

### 1. AWS Route53 Hosted Zoneの作成

まず、AWSの認証情報を環境変数に設定します：

```bash
export AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

認証情報が正しく設定されているか確認します：

```bash
aws sts get-caller-identity
```

次に、AWS Route53でHosted Zoneを作成します：

```bash
aws route53 create-hosted-zone --name example-tutorial.com --caller-reference external-dns-tutorial
```

### 2. external-dnsのインストール

external-dnsのHelmチャートは、kubernetes-sigsが提供しているものがありますが、ここではBitnamiが提供しているものを使用します。こちらのほうがオプションが多く、AWS Route53の設定が簡単にできるためです。

```bash
helm install external-dns \
  --set provider=aws \
  --set aws.zoneType=public \
  --set aws.credentials.accessKey="$AWS_ACCESS_KEY_ID" \
  --set aws.credentials.secretKey="$AWS_SECRET_ACCESS_KEY" \
  --set txtOwnerId=EXAMPLE-ZONE-ID123 \
  --set "domainFilters[0]=example-tutorial.com" \
  --set policy=sync \
  --set "sources[0]=crd" \
  --set crd.create=true \
  --set crd.apiversion=externaldns.k8s.io/v1alpha1 \
  --set crd.kind=DNSEndpoint \
  oci://registry-1.docker.io/bitnamicharts/external-dns
```

#### インストールオプションの説明

| オプション | 説明 |
|------------|------|
| `provider=aws` | AWS Route53を使用することを指定 |
| `aws.zoneType=public` | パブリックゾーンを使用することを指定 |
| `aws.credentials.accessKey` | AWSのアクセスキー |
| `aws.credentials.secretKey` | AWSのシークレットキー |
| `txtOwnerId` | TXTレコードのオーナーID |
| `domainFilters[0]` | 監視対象のドメイン |
| `policy=sync` | DNSレコード同期ポリシー |
| `sources[0]=crd` | CRDを使用することを指定 |
| `crd.create=true` | CRDの作成を有効化 |
| `crd.apiversion` | CRDのAPIバージョン |
| `crd.kind` | CRDの種類 |

インストール後、external-dnsが正常に起動していることを確認します：

```bash
kubectl logs -l app.kubernetes.io/name=external-dns -f
```

次のようなログが表示されれば、正常に起動しています：

```
time="2024-11-06T05:12:29Z" level=info msg="Instantiating new Kubernetes client"
time="2024-11-06T05:12:29Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2024-11-06T05:12:29Z" level=info msg="Created Kubernetes client https://10.43.0.1:443"
time="2024-11-06T05:12:31Z" level=info msg="Applying provider record filter for domains: [example-tutorial.com. .example-tutorial.com.]"
```

## external-dnsにレコードを作らせてみる

external-dnsによって、KubernetesリソースからDNSレコードが作成されることを確認します。

external-dnsは、Service、Ingress、Gatewayなどのリソースを監視し、DNSレコードを作成します。今回はお試しなので、実体リソースが一切不要なCRDのDNSEndpointを使用します。

### 1. テストレコードの作成

CRDを使用してDNSレコードを作成してみましょう。次のマニフェストを作成します：

```yaml:test.example-tutorial.com.yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: test.example-tutorial.com
spec:
  endpoints:
  - dnsName: test.example-tutorial.com
    recordTTL: 180
    recordType: A
    targets:
    - 127.0.0.1
```

このマニフェストを適用します：

```bash
kubectl apply -f test.example-tutorial.com.yaml
```

### 2. DNSレコードの確認

DNSレコードが作成されたことを確認します。 `@ns-1694.awsdns-19.co.uk` は、AWS Route53のネームサーバーのホスト名です。この部分は、前の手順で作成したHosted Zoneによって異なりますので、ご自身の環境に合わせてください。

まず、Aレコードを確認します：

```bash
dig @ns-1694.awsdns-19.co.uk +noall +answer -t A test.example-tutorial.com
```

正常に作成されていれば、次のような結果が表示されます：

```
test.example-tutorial.com. 180  IN      A       127.0.0.1
```

### 3. TXTレコードの確認

また、TXTレコードも作成されていることを確認します：

```bash
dig @ns-1694.awsdns-19.co.uk +noall +answer -t TXT test.example-tutorial.com
```

結果：
```
test.example-tutorial.com. 300  IN      TXT     "heritage=external-dns,external-dns/owner=EXAMPLE-ZONE-ID123,external-dns/resource=crd/default/test.example-tutorial.com"
```

このTXTレコードは、external-dnsが管理のために作成したものです。この管理情報には、次の情報が含まれています：

1. `heritage=external-dns`
   - このDNSレコードがexternal-dnsによって管理されていることを示す
2. `external-dns/owner=EXAMPLE-ZONE-ID123`
   - 特定のexternal-dnsインスタンスを識別するためのownerID
   - 複数インスタンス間の競合を防止
3. `external-dns/resource=crd/default/test.example-tutorial.com`
   - DNSレコードの元となったKubernetesリソースの情報
   - この例では`default`ネームスペースの`test.example-tutorial.com` CRDを参照

こうした管理情報はTXTレコードとして公開されてしまいますが、暗号化することも可能らしいです。このへんの機能は、試してみたらまた記事にしたいと思います。

## 掃除

### レコードの削除

DNSレコードを削除するには、CRDリソースを削除します：

```bash
kubectl delete -f test.example-tutorial.com.yaml
```

しばらくすると、external-dnsがDNSレコードを削除します。external-dnsのログを確認してください。

```
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  a-test.example-tutorial.com TXT" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  test.example-tutorial.com A" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="Desired change:  DELETE  test.example-tutorial.com TXT" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
time="2024-11-06T05:48:24Z" level=info msg="3 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z08033563HFN15GSXJ766 zoneName=example-tutorial.com.
```

DNSレコードが削除されたことを確認しておきます。

```bash
dig @ns-1694.awsdns-19.co.uk +noall +answer -t A test.example-tutorial.com
```

### external-dnsの削除

Helmリリースを削除します：

```bash
helm uninstall external-dns
```

### 3. Hosted Zoneの削除

まず、Hosted Zoneの一覧を確認します：

```bash
aws route53 list-hosted-zones
```

確認したIDを使用してHosted Zoneを削除します：

```bash
aws route53 delete-hosted-zone --id Z08033563HFN15GSXJ766
```

## まとめ

以上です！本記事では、external-dnsとAWS Route53を使用して、KubernetesリソースからDNSレコードを自動的に管理する方法を解説しました。

実践で使うには、他にもいろいろと考慮する事項があると思いますが、external-dnsの取っ掛かりとしてこのチュートリアルがお役に立てば嬉しいです！