KubernetesでのExternalDNS:CloudflareとRoute53へのDNSレコード自動同期

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

誰も語らない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を管理し、チームは本当に重要なことに集中できるようになった。

Share: