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 #3063でWebhookプロバイダーを提案します。狙いは「新しいプロバイダーを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つです。
- Sidecar(同一Pod / localhost)
特徴:最小・安全。ExternalDNSと密結合(ロールアウト分離しづらい) - クラスタ内分離(Service経由)
特徴:疎結合で更新/スケールを個別管理しやすい - クラスタ外部
特徴:最も安定したネットワークに依存する
公式は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管理の自動化と運用効率化に役立てば幸いです。