Kubernetes ExternalDNSを使ってさくらのDNSアプライアンス管理を自動化する

はじめに

はじめまして。テクノロジー室のリンと申します。

Kubernetesを運用していると、こんな経験はありませんか?

  • 「サービスを公開したいけれど、DNS設定が自動化されていないため手作業が発生する」
  • 「Ingress/Service同様にDNSの設定もKubernetesのリソースとして管理したい」

まさに私たちもKubernetesベースの社内共通コンテナ基盤を運用する際に、同じ課題に直面していました。そこで開発したのが external-dns-sacloud-webhook です。

本記事では、external-dns-sacloud-webhookの仕組みと使い方、さらに実際の社内利用事例を交えながら、その価値をお伝えします。

ExternalDNSとは

ExternalDNSの概要

「新しいServiceを公開したのに、DNSは手作業で更新…」。Kubernetes運用では誰もが一度はつまずくポイントです。ExternalDNSは、IngressやService(必要に応じてGateway)から “望ましいDNS名 ⇄ 宛先” を読み取り、外部DNSプロバイダーのレコードを自動で作成・更新・削除するコントローラーです。

DNSの更新作業をワークフローに溶け込ませ、オペレーションはKubernetesに集約。私たちが欲しかった「アプリを出すとDNSが勝手に整う」体験を、シンプルな仕組みで実現してくれます。

プロバイダーという概念の説明

ここで登場するのがプロバイダーという考え方です。ExternalDNS本体は “何を作るべきか(差分計画)” を決め、実際のゾーン/レコード操作はプロバイダーに委ねる構造になっています。

プロバイダー側は、各DNS製品のAPI差異(A/AAAA/CNAME/TXT/ALIAS、TTL、ゾーン構造など)を吸収し、ExternalDNSが計算した `endpoint.Endpoint` と `plan.Changes` をそのまま確実に反映します。

プロバイダーに対するコミュニティのスタンスの変遷

当初、ExternalDNSは各社向けのプロバイダーをin-tree(本体リポジトリ内)で抱えていました。しかし実装が一極集中すると、保守コストの増大、新規追加の参入障壁、リリースサイクルへの拘束といったボトルネックが露わになります。

転機は2022年10月3日。メンテナのRaffo氏がPR #3063Webhookプロバイダーを提案します。狙いは「新しいプロバイダーをcoreにマージしなくてよい設計に切り替え、メンテナ負荷を下げて本体はコア機能に集中させる」こと。AWSプロバイダーを外出しした実証も添えられ、2023年9月25日にマージされました。

こうしてコミュニティはout-of-treeへ舵を切り、各組織が自分たちのペースでプロバイダーを公開・改良できる体制に。ExternalDNS本体は安定したコアに集中し、全体の進化速度はむしろ上がる——そんな健全な分業が実現しました。

Webhookプロバイダーの紹介

Webhookプロバイダーは、ExternalDNS本体に組み込まれたHTTPクライアントが、外部のプロバイダー(Webhookサーバ)へリクエストを送り、レコード同期を委譲する方式です。

公式にはExternalDNS PodのSidecarとして同居し、`localhost`で待受ける構成が推奨されます。ネットワーク経路が短く、セキュリティ/トラブルシュートの観点でも扱いやすいのが魅力です。

実装者が準備すべき主なエンドポイントは次のとおりです(ExternalDNS公式仕様)。

  • GET /records:既存レコードの一覧を返す
  • POST /adjustendpoints:プロバイダー固有の補正を適用
  • POST /records:作成・更新・削除の適用(差分適用)
  • GET /healthz:Kubernetesプローブ向けヘルスチェック

補足:ExternalDNSは、Webhookプロバイダーとの交渉やDomainFilterの受け渡しのために `/` エンドポイントにもアクセスします(API のシリアライズ互換性は公式で保証されています)。

稼働時のシーケンス

実際の内部の動きは下記のとおりです。ExternalDNSがKubernetesリソースの変化を検知し、Webhookプロバイダーを介してさくらのDNSに差分適用します。

本記事で扱うexternal-dns-sacloud-webhookは、さくらのクラウドのGo SDK(iaas-api-go / iaas-service-go)を用いて上記エンドポイントを実装し、ExternalDNSからの呼び出しに応じてゾーン取得・レコード一覧・差分適用(作成/更新/削除)を行います。設計上はALIASやTXTレジストリにも対応しています(README参照)。

使ってみよう

external-dns-sacloud-webhookに用意しているクイックテスト用スクリプト example/reset-and-deploy.sh や今後対応する予定のHelm Chartも利用できますが、理解を深めるため、あえてManifestを手動適用する手順で解説します。初学者の方はそのまま写経で動かせるように、経験者の方は設計意図やオプション選定の理由が分かるように、ポイントを補足します。

ステップ0:準備

利用前提

  • さくらのクラウドのAPI Token / Secretと、DNSゾーン(例:example.com) を保有していること
  • ExternalDNSで利用予定のサブドメインの権限移譲がされていること

参考(公式マニュアル)

ステップ1:コントローラーのデプロイ(ExternalDNS本体)

ExternalDNSはKubernetesリソース(Ingress/Serviceなど)を監視し、望ましいDNSレコードを算出するためのコントローラーですので、実際のDNS変更を担うWebhookサーバ(アドオン)をデプロイする前に、まずこちらをデプロイします。

作業用Namespaceを作る

kubectl create namespace external-dns

external-dns(本体)のRBAC

ExternalDNSは、対象とするソースに応じてget/list/watch権限が必要です。以下はIngressとService/Endpointsをソースとする最小例です(Gateway APIを使う場合は、該当APIグループを追加してください)。

external-dns-rbac.yaml (▶をクリックするとコードが表示されます)
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: external-dns
rules:
  - apiGroups: [""]
    resources: ["services", "endpoints"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: external-dns
subjects:
  - kind: ServiceAccount
    name: external-dns
    namespace: external-dns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns

マニフェストの適用:

kubectl apply -f external-dns-rbac.yaml

ExternalDNSのDeployment

external-dns-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: external-dns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
        - name: external-dns
          image: registry.k8s.io/external-dns/external-dns:v0.18.0
          args:
            - --log-level=debug
            - --source=ingress
            - --provider=webhook
            - --webhook-provider-url=http://external-dns-provider.external-dns.svc.cluster.local:8080
            - --domain-filter=example.com
            - --registry=txt
            - --txt-prefix=_external-dns.
            - --txt-owner-id=example-owner
            - --interval=30s
            - --events
            - --min-event-sync-interval=10s
            - --policy=sync
            - --annotation-filter=external-dns.alpha.kubernetes.io/managed=true

引数の確認ポイント(要約)

  • --source=ingress:何を監視するか
  • --provider=webhook / --webhook-provider-url=...:Webhookプロバイダーに委譲
  • --registry=txt / --txt-owner-id=example-owner / --txt-prefix=_external-dns.:所有権の衝突回避
  • --policy=sync:レコードを作成・削除管理を完全委任(upsert-onlyは削除しない)
  • --interval / --events / --min-event-sync-interval:反映速度とAPI負荷のチューニング
  • --domain-filter=example.com:管理対象ドメインを指定

ステップ2:アドオンのデプロイ(Webhookプロバイダー)

WebhookプロバイダーはExternalDNSからのHTTPリクエストを受け取り、さくらのクラウドDNS APIと対話して、ゾーン/レコードの作成・更新・削除を行うコンポーネントです。

デプロイ方式は次の3つです。

  1. Sidecar(同一Pod / localhost)
    特徴:最小・安全。ExternalDNSと密結合(ロールアウト分離しづらい)
  2. クラスタ内分離(Service経由)
    特徴:疎結合で更新/スケールを個別管理しやすい
  3. クラスタ外部
    特徴:最も安定したネットワークに依存する

公式はSidecarを推奨していますが、本記事は運用上の柔軟性を優先し 2. クラスタ内分離 を採用します。

接続先指定の例:http://webhook-provider.namespace.svc.cluster.local:8080

WebhookプロバイダーのConfig例

zone-nameは管理対象のFQDN(例:example.com)、txt-owner-idはExternalDNS本体と同じ値を指定します。

webhook-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
    name: external-dns-webhook-config
    namespace: external-dns
data:
    config.yaml: |
        provider-ip: "0.0.0.0"
        provider-port: "8080"
        zone-name: "example.com"
        registry-txt: true
        txt-owner-id: "example-owner"

WebhookプロバイダーのSecret例

機密情報(API Token/Secret)はVault+AVP(argocd-vault-plugin)やKMS連携など、Secretの外部管理を推奨します。下記の例ではサンプルのためSecretマニフェストも示しますが、運用ではシークレット管理ツール等に移行してください。

webhook-credentials.yaml
---
apiVersion: v1
kind: Secret
metadata:
    name: external-dns-webhook-secret
    namespace: external-dns
type: Opaque
data:
    api-token: "<your-sakura-api-token>"
    api-secret: "<your-sakura-api-secret>"

WebhookプロバイダーのDeployment例

webhook-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns-provider
  namespace: external-dns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: external-dns-provider
  template:
    metadata:
      labels:
        app: external-dns-provider
    spec:
      containers:
        - name: external-dns-provider
          image: ghcr.io/sacloud/external-dns-sacloud-webhook:v0.1.2
          args:
            - "--sakura-api-token=$(SAKURA_API_TOKEN)"
            - "--sakura-api-secret=$(SAKURA_API_SECRET)"
            - "--config=/etc/config/config.yaml"
          env:
            - name: SAKURA_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: external-dns-webhook-credentials
                  key: sakura-api-token
            - name: SAKURA_API_SECRET
              valueFrom:
                secretKeyRef:
                  name: external-dns-webhook-credentials
                  key: sakura-api-secret
          volumeMounts:
            - name: config
              mountPath: /etc/config
              readOnly: true
          ports:
            - containerPort: 8080
      volumes:
        - name: config
          configMap:
            name: external-dns-webhook-config
            items:
              - key: config.yaml
                path: config.yaml

WebhookプロバイダーのService例

webhook-service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: external-dns-provider
  namespace: external-dns
spec:
  selector:
    app: external-dns-provider
  ports:
    - name: http
      port: 8080
      targetPort: 8080

マニフェストの適用

kubectl apply -f webhook-configmap.yaml
kubectl apply -f webhook-credentials.yaml
kubectl apply -f webhook-deployment.yaml
kubectl apply -f webhook-service.yaml

ステップ3:動作確認

サンプルアプリ(whoami)を用意して、3種類のレコードタイプのIngressを作成したら、さくらのクラウドDNSにレコードの自動作成がされることを確認してみましょう。

サンプルアプリのDeployment例

whoami-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
        - name: whoami
          image: hashicorp/http-echo:0.2.3
          args: ["-text=hello"]
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: whoami
spec:
  selector:
    app: whoami
  ports:
    - port: 80
      targetPort: 5678

AレコードのIngress作成例(IPを直接指す)

本記事では最小構成でExternalDNSの挙動を紹介するため、Ingress Controllerを導入せず、ExternalDNSも `--source=ingress` のみで起動しています。 この条件だとIngressの `.status.loadBalancer` が空のままになり、ExternalDNSは自動で宛先(target)を推論できません。そのため、以下のIngress作成例ではexternal-dns.alpha.kubernetes.io/targetでAレコードの宛先を明示的に上書きしています。

a-test-ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
  annotations:
    external-dns.alpha.kubernetes.io/target: "198.0.2.1" # RFC5737 の例示用IP
    external-dns.alpha.kubernetes.io/managed: "true"
spec:
  rules:
    - host: a-test.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80

補足:通常の運用では、Ingress ControllerやService(type=LoadBalancer)が .status.loadBalancer に IP/ホスト名を設定するため、targetを書かなくても自動反映されます。

CNAMEレコードのIngress作成例

cname-test-ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cname-test-ingress
  annotations:
    external-dns.alpha.kubernetes.io/target: "a-test.example.com."
    external-dns.alpha.kubernetes.io/managed: "true"
    external-dns.alpha.kubernetes.io/ttl: "120"
spec:
  rules:
    - host: cname-test.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80

補足:末尾のドット . はFQDNを明示するために必要な場合があります(さくらのDNSプロバイダーの仕様のため)。

ALIASレコードのIngress作成例

alias-test-ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: alias-test-ingress
  annotations:
    external-dns.alpha.kubernetes.io/managed: "true"
    external-dns.alpha.kubernetes.io/alias:   "true"
    external-dns.alpha.kubernetes.io/target: "a-test.example.net."
    external-dns.alpha.kubernetes.io/ttl: "10"
spec:
  rules:
    - host: alias-test.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80

マニフェストの適用

kubectl apply -f whoami-deployment.yaml
kubectl apply -f a-test-ingress.yaml
kubectl apply -f cname-test-ingress.yaml
kubectl apply -f alias-test-ingress.yaml

確認してみよう:

正常に適用された場合は、さくらのクラウドDNSのゾーン管理画面にA/CNAME/ALIASレコードと各レコードが ExternalDNSに管理されることを明示するTXTレコードが自動で追加されます。

トラブル時のチェック:

もしうまくいかない場合は、次のポイントを確認してください。

ゾーン不一致

  • 症状:no suitable zones found
  • 対処:ExternalDNSの --domain-filter=example.com とWebhookの zone-name: example.com を一致させる

TXT が作成されない / 競合

  • 症状:レコードはできるがTXTが無い or 既存と競合
  • 対処:--registry=txt/--txt-owner-id/--txt-prefix の統一。途中で値を変えない

反映が遅い

  • 対処:--events を有効にする、--interval の値を短くする、--min-event-sync-interval を適切な値にする

社内の利用事例

課題

社内共通コンテナ基盤は、さくらのクラウドリソースで構築したKubernetesクラスタで運用していますNamespace単位でマルチテナントに貸し出す形となっていて、Kubernetesクラスタ上で動作するユーザー管理システムやミドルウェアを社内ネットに公開するためにIngressリソースを利用しています。

従来の運用は「Ingressを作成 → IPAMにレコードを追加 → IPAMのリストをもとにTerraformでさくらのクラウド DNSを更新」という三段階フローで、典型的ではありますが、現場では次の“あるある”が積み重なっていました。

  • ゾーン管理が煩雑:サブドメインの命名・重複チェック・TTL調整など、人手の判断と確認待ちが多い。
  • 変更窓口が多い:IPAM・Terraform・レビュー・適用…と関与者と変更点が分散し、ミスの温床に。
  • 削除漏れ:Kubernetes側でリソースを消しても、DNSレコードが残る“ゾンビ”がときどき発生。

とある日、運用メンバーから「Ingressはできているのに名前が引けない」というフィードバックがありました。調べてみると、IPAMへの追加を忘れたのが原因でした。

こうした“細かい綻び”が運用の効率をじわじわ損ねるのが気になっていました。

改善

sacloud-external-dnsを導入することで、上記の課題を解決しました。具体的には以下のような改善がありました。

  • 手作業の削減:ゾーン管理・登録作業の多くがクラスタの宣言(Ingress)に集約。
  • 変更の一貫性:KubernetesのGitOpsとDNSの反映が自然に連動。
  • 消し忘れの解消:リソース削除に伴い、DNSも自動でクリーンアップ。
  • 運用負荷の平準化:申請・レビュー待ちの小さな“詰まり”が減り、開発者の体験が向上。

終わりに

最後まで読んでいただきありがとうございます。今回は、ExternalDNSのWebhookプロバイダー を活用した external-dns-sacloud-webhookの仕組み・使い方・社内利用事例をご紹介しました。初期リリースのv0.1.2は出発点に過ぎません。今後も継続的に改善していきます。直近ではHelm Chartの提供やTXTレコードの暗号化機能に取り組み、使い勝手とセキュリティを両立する改善を進めています。フィードバックやコントリビューションは大歓迎です。ぜひGitHubリポジトリをご覧いただき、一緒にさくらのクラウドを利用するKubernetesユーザーの体験向上を進めていきましょう。さくらのクラウ DNSを利用しているKubernetesのユーザーにとって、本ツールがDNS管理の自動化と運用効率化に役立てば幸いです。