記事一覧はこちら
背景・モチベーション
EKS(or GKE)を運用するときにお世話になる external-dns
をふんわりとだけ理解て利用してきたのですが、どのようにDNSレコードを操作しているのかをちゃんと理解したいと思ったのがきっかけです。
使い始めた当初はなかった、Route53のルーティングポリシーのサポートなどが加わったりと順調に進化しているように見える、このプロダクトを少し紐解いてみようと思います。
external-dnsとは
KubernetesのリソースをPublicなDNSサーバを利用して公開してくれます。 kubednsのように、公開されたServiceやIngressをKubernetes APIを利用して列挙し、desiredなDNSレコードのリストを作成し、DNSプロバイダと同期します。
DNS プロバイダによって安定レベルが異なります。レベルはStable, Beta, Alphaに分けられますが、Stableになっているのは、 Google Cloud DNS
と AWS Route 53
のみです(参考)。
EKSでの導入方法
Route53の手順はこちらに記載されている。
下記のようにIAM Policyの作成して、EKSでOIDCプロバイダの機能を利用してk8sのサービスアカウント単位で権限を付与します。 IAM Policy はサンプルのままだと権限が強すぎるので、下記のようにホストゾーンを指定して権限を付与してあげるのが良さそう。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:ChangeResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/ABCD1234EFG567", "arn:aws:route53:::hostedzone/1234ABCD567EFG", ] }, { "Effect": "Allow", "Action": [ "route53:ListHostedZones", "route53:ListResourceRecordSets" ], "Resource": [ "*" ] } ] }
サービスアカウント external-dns
に権限が付与できたら、マニフェストをデプロイする。チュートリアルで明示されている引数は下記です。
コマンド引数の説明を見るときに一番簡単なのは、 docker run k8s.gcr.io/external-dns/external-dns:v0.7.6 --help
です。
コマンドオプションはproviderごとにもあるので、数がかなり多いのですが、AWS適用例で指定している引数に関しては記載してみました。
Required
--source
: エンドポイントを提供するリソース名(service, ingress, node, istio-gateway等から1つ以上選択)--provider
: DNSレコードが作成されるDNSプロバイダ(aws, google, azure等から1つ選択)
Optional
--policy
: sourceとprovider間を同期させる方針、デフォルトはsync
。sync
はレコードの削除も行うので万全を期すなら、upsert-only
かcreate-only
を選択する。--domain-filter
: ドメインのsuffixで制限をかける--aws-zone-type
: ゾーンのタイプで制限をかける(public, privateから選択)--registry
: DNSレコードの所有権を追跡する利用する実装、デフォルトではtxtレコードに情報を書き込む(txt, noop, aws-sdから選択)--txt-owner-id
: txt regisryを利用しているときにこのexternal-dnsからの更新であることを認識させるためのID、デフォルトではdefault
という文字列になる
# マニフェストから一部を抜粋 containers: - name: external-dns image: k8s.gcr.io/external-dns/external-dns:v0.7.6 args: - --source=service - --source=ingress - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-hostedzone-identifier
あとは、ServiceリソースやIngressリソースをexternal-dnsから認識させてあげればOKです。Ingressリソースは、.spec.rule.hosts で宣言しておけば認識されます。また、 external-dns.alpha.kubernetes.io/hostname
のアノテーションを利用することで、ServiceとIngressどちらでも紐づけることが可能です。
アノテーションにはいくつか種類があり、例えばRoute53のRouting Policyが使えたり、TTLを指定出来たりします。
apiVersion: v1 kind: Ingress metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com external-dns.alpha.kubernetes.io/ttl: 60 # set-identifierはルーティングポリシーを利用する上で必須の設定項目 external-dns.alpha.kubernetes.io/set-identifier: default external-dns.alpha.kubernetes.io/aws-weight: "30" spec: {} # 省略
Route53を更新するまでの処理を追う
エントリポイント
external-dnsは起動後に設定を読み込んだ後は、設定したInterval(デフォルト1分)に従い、処理を定期実行しています(該当コード)。 Tickerを使って、処理時間を除いて1秒毎に時間のチェックをしているのは、自前で定期処理を作成として参考になるかも(処理を含めて定期処理であればcronのライブラリで良いと思うけど)。
// Run runs RunOnce in a loop with a delay until context is canceled func (c *Controller) Run(ctx context.Context) { ticker := time.NewTicker(time.Second) defer ticker.Stop() for { if c.ShouldRunOnce(time.Now()) { if err := c.RunOnce(ctx); err != nil { log.Error(err) } } select { case <-ticker.C: // ループが最短でも1秒ごとの処理になるように case <-ctx.Done(): log.Info("Terminating main controller loop") return } } }
定期的に行っている更新処理の流れ
この中で行っている、 RunOnce(ctx context.Context)
がメインの処理となります(該当コード)。
流れを箇条書きにすると下記のようになります。
- Route53に存在するDNSレコードを列挙します
- k8sのsource(例えば、serviceやingressリソース)からエンドポイントを列挙します
- 1つ前の処理で作成した望んだ状態に、実際の状態を同期させるために、必要なアクションを計算します
- keyが文字列の2次元HashMapを作成します、1つ目のKeyがDNS名で2つ目のkeyが
SetIdentifier
となっています。Valueは現在の状態と望んだ状態を持つ planTableRow構造体です。 - Mapにそれぞれ currentState と desiredStateを書き込んでいきます。
- もし、desiredStateとして挙げられたDNS名が既に存在している場合には、そのコンフリクトを解決する
- 起動引数に設定したPolicy(
upsert
等)に従い、アクションをフィルタする
- keyが文字列の2次元HashMapを作成します、1つ目のKeyがDNS名で2つ目のkeyが
- 計算したアクションを実行する
- Route53の変更セットに変換する
- 一つのトランザクションに集約して更新を行う
まとめ
k8sの変更監視とGoのinterfaceの使い方がかなり勉強になりました。 k8sの周辺ツールは各クラウドプロバイダによる処理の違いを、interfaceとその実装で持たせることが多くて、コード読んでて面白いです。
DNSを扱うというデリケートな処理をどのように実装しているかを俯瞰することができてよかった!以上!