誰も語らないDNSの問題
Kubernetesクラスターは快調に動いている。Ingressコントローラーはデプロイ済み、Serviceは稼働中、Podも正常だ。そこで気づく——新しいものをデプロイするたびに、CloudflareかRoute53に手動でDNSレコードを作りに行かなければならないことに。LoadBalancerのIPが変わったとき(特にマネージドクラスターではよく起こる)、また手動でレコードを更新しなければならない。
根本的な原因は、KubernetesにはDNSプロバイダーと通信するネイティブな仕組みがないことだ。クラスターはIPアドレスとホスト名を把握しているが、CloudflareやRoute53の存在すら知らない。このギャップを埋めるためにExternalDNSは作られた。
ExternalDNSはKubernetesリソース——特にIngressオブジェクトとLoadBalancerタイプのService——を監視し、そこで定義したホスト名をDNSプロバイダーに直接同期する。ホストがapp.example.comのIngressを作ると、ExternalDNSが自動的にロードバランサーのIPを指すAレコードを作成する。そのIngressを削除すれば、DNSレコードも一緒に消える。
私はEKSとGKEの両方の本番環境でこのアプローチを適用してきたが、結果は一貫して安定していた——古いDNSレコードはなくなり、手動更新も不要になり、チームの運用負担が大幅に軽減された。
インストール
前提条件
ExternalDNSをインストールする前に、以下を確認してください:
- 稼働中のKubernetesクラスター(1.19以上)
- クラスターにアクセスできるkubectlの設定
- DNSプロバイダーのアカウント——CloudflareまたはAWS Route53
- Helm 3のインストール(推奨インストール方法)
Helmでインストール
ExternalDNSを動かす最もシンプルな方法は、Bitnami Helmチャートを使うことだ。リポジトリを追加して更新する:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
インストール内容はDNSプロバイダーによってカスタマイズが必要だ。以下のセクションでCloudflareとRoute53それぞれの手順を説明する——該当する方を選んでほしい。
設定
オプションA:Cloudflare
まず、CloudflareダッシュボードでAPIトークンを生成する。マイプロフィール → APIトークン → トークンを作成に移動し、以下の権限を付与する:
- Zone → DNS → Edit
- Zone → Zone → Read
ExternalDNSで管理したい特定のゾーン(ドメイン)にトークンのスコープを絞る。グローバルAPIキーは使わないこと——スコープを絞ったトークンの方が本番環境ではるかに安全で、認証情報が漏洩した際の被害範囲を最小限に抑えられる。
CloudflareトークンでKubernetesシークレットを作成する:
kubectl create namespace external-dns
kubectl create secret generic cloudflare-api-token \
--from-literal=apiToken=YOUR_CLOUDFLARE_API_TOKEN \
-n external-dns
ExternalDNSの設定を記述したvalues-cloudflare.yamlファイルを作成する:
provider: cloudflare
cloudflare:
secretName: cloudflare-api-token
proxied: false # Cloudflareプロキシ(オレンジ色のクラウド)を使う場合はtrueに設定
domainFilters:
- example.com # 実際のドメインに置き換える
policy: sync # "sync" = 作成 + 削除、"upsert-only" = 作成/更新のみ
sources:
- ingress
- service
txtOwnerId: k8s-cluster-prod # このクラスターの一意の識別子
logLevel: info
ExternalDNSをインストールする:
helm install external-dns bitnami/external-dns \
-n external-dns \
-f values-cloudflare.yaml
オプションB:AWS Route53
Route53の場合、ExternalDNSはDNSレコードを管理するためにIAM権限が必要だ。EKSではサービスアカウント用IAMロール(IRSA)が推奨アプローチだ。他の環境ではアクセスキーを持つ専用IAMユーザーでも問題なく動作する。
IAMポリシードキュメントを作成し、IAMユーザーまたはロールにアタッチする:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource"
],
"Resource": ["*"]
}
]
}
IAM認証情報をKubernetesシークレットとして保存する:
kubectl create namespace external-dns
kubectl create secret generic aws-credentials \
--from-literal=access-key-id=YOUR_AWS_ACCESS_KEY \
--from-literal=secret-access-key=YOUR_AWS_SECRET_KEY \
-n external-dns
values-route53.yamlを作成する:
provider: aws
aws:
credentials:
secretKey: aws-credentials
accessKey: access-key-id
secretAccessKey: secret-access-key
region: us-east-1 # 使用するAWSリージョン
zoneType: public # "public"または"private"
domainFilters:
- example.com
policy: sync
sources:
- ingress
- service
txtOwnerId: k8s-cluster-prod
logLevel: info
ExternalDNSをインストールする:
helm install external-dns bitnami/external-dns \
-n external-dns \
-f values-route53.yaml
IngressとServiceへのアノテーション設定
ExternalDNSはKubernetesリソースからホスト名を自動的に読み取る。Ingressオブジェクトの場合、追加設定なしでspec.rules[].hostフィールドを取得する。LoadBalancer Serviceの場合は、DNS名を宣言するアノテーションを追加する:
# Ingress — ExternalDNSがhostフィールドを自動的に読み取る
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
# DNSアノテーション付きLoadBalancer Service
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
annotations:
external-dns.alpha.kubernetes.io/hostname: service.example.com
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app
理解しておく価値のある点として:ExternalDNSは各DNSエントリとともにTXTレコードを作成する。このTXTレコードはオーナーシップマーカーとして機能し、ExternalDNSが管理するレコードと誰かが手動で作成したレコードを区別するために使われる。txtOwnerIdの値はこのTXTレコードに埋め込まれる。同じDNSゾーンを複数のクラスターが参照する場合、競合を避けるために各クラスターには一意のtxtOwnerIdが必要だ。
動作確認とモニタリング
デプロイメントの確認
ExternalDNSが正常に動作しているか確認する:
kubectl get pods -n external-dns
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns --tail=50
ExternalDNSがIngressまたはServiceを処理すると、次のようなログエントリが表示される:
level=info msg="変更予定: app.example.com A [203.0.113.42] を作成"
level=info msg="変更予定: app.example.com TXT [\"heritage=external-dns,...\"] を作成"
level=info msg="ゾーン example.com. の2件のレコードが正常に更新されました"
DNSレコードの確認
digを使ってDNSにレコードが登録されているか確認する:
# Aレコードを確認
dig app.example.com +short
# TXTオーナーシップレコードを確認
dig TXT "externaldns-app.example.com" +short
変更は通常1〜2分以内に反映される。ExternalDNSはデフォルトで60秒ごとに同期ループを実行する。開発中に素早いリコンシリエーションが必要な場合や、本番環境でAPIコールを減らしたい場合は、Helmバリューのinterval設定で調整できる。
よくある問題のトラブルシューティング
レコードが作成されない場合は、まず以下を確認する:
- domainFiltersの設定ミス:
domainFiltersリストに一致しないホスト名はExternalDNSにスキップされる。Ingressのホスト名がフィルターと完全に一致しているか確認する。 - 権限の不足:Route53の場合はIAMポリシーが正しいユーザーまたはロールにアタッチされているか確認する。Cloudflareの場合はAPIトークンのスコープが正しいゾーンを含んでいるか確認する。
- ポリシーがupsert-onlyに設定されている:
policy: upsert-onlyの場合、IngressやServiceを削除してもExternalDNSはDNSレコードを削除しない。完全なライフサイクル管理にはpolicy: syncに切り替える。 - Ingressに外部IPが割り当てられていない:ExternalDNSは
status.loadBalancer.ingressのIPを待つ。IngressコントローラーがまだIPを割り当てていない場合、ExternalDNSは何も処理できない——まずIngressコントローラーを確認する。
Prometheusメトリクス
ExternalDNSはポート7979でPrometheusメトリクスを公開する。クラスター内でPrometheusが稼働している場合は、HelmバリューにPodアノテーションを追加してスクレイピングを有効にする:
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "7979"
注目すべき主要メトリクスはexternal_dns_controller_last_sync_timestamp_seconds(停止した同期ループを検出)とexternal_dns_registry_endpoints_total(ExternalDNSが管理するレコード数を追跡)だ。最後の同期タイムスタンプが5分以上古い場合にアラートを発火させるシンプルなアラートルールを設定しておくと、認証情報の問題やDNSプロバイダーの障害をユーザーへの影響が出る前に検知できる。
ExternalDNSを運用することで、私のワークフローから運用上の手間が一つ丸ごとなくなった。デプロイ後にDNSレコードの更新を誰かが忘れたことで起きる「このドメインがなぜ名前解決できないのか」というインシデントはもうない。クラスターが自律的にDNSを管理し、チームは本当に重要なことに集中できるようになった。

