🐡

ExternalDNSで未対応のプロバイダを開発する方法

2022/09/19に公開

概要

ExternalDNSはKubernetesにおいて、IngressリソースやServiceリソース作成/更新時にDNSレコードを動的に設定してくれるものですが、プロバイダが未対応な環境ではExternalDNSを利用することが出来ません。(*v0.12.2時点での対応プロバイダ一覧)

しかし、ExternalDNSには新しいプロバイダを開発できる仕組みが用意されているため、任意の環境におけるプロバイダを開発することでExternalDNSを利用することができるようになります。

今回はExternalDNS未対応の環境としてさくらのクラウドを例に、プロバイダを開発する方法を解説します。

https://github.com/sosomasox/external-dns/pull/1/commits/7f60f9fb6b74d86a3bd7d402c22962264cc63d84

方針

公式ドキュメントに記載があるように、プロバイダを開発するにはProviderインターフェースに定義されたRecordsメソッドとApplyChangesメソッドを実装し、Providerインターフェースを満たす任意のProviderを作成することが基本的な方針となります。

https://github.com/kubernetes-sigs/external-dns/blob/4046257911958c1716f9a63ccf7846e2ee412ae6/provider/provider.go#L28-L50

実装

主に実装すべき箇所や修正すべきところを解説していきます。

パッケージを作成

provider/配下に開発するプロバイダのパッケージを作成します。
今回、さくらのクラウドのプロバイダを作成する際にはprovider/sakuracloudとしました。

プロバイダの構造体を定義

開発するプロバイダの構造体を定義します。
一般的にフィールドにはprovider.BaseProviderendpoint.DomainFilterDryRunClientを定義します。

provider.BaseProviderフィールドはProviderインターフェースを実装するためのRecordsメソッドとApplyChangesメソッド以外のメソッドが実装されているProviderです。

endpoint.DomainFilterフィールドはExternalDNS起動時にフラグ--domain-filterで指定されたDNSレコードの操作を行いたいゾーンに関する情報を保持するためのものです。

DryRunフィールドは ExternalDNS起動時にフラグ--dry-runが指定されたかどうかの情報を保持するためのものです。
dry-runモードで起動している場合はDNSレコードが作成されないようにしなければなりません。

ClientフィールドはExternalDNSに対応させたい環境のDNSレコードの操作が行えるAPIクライアントを保持するためのものです。
ExternalDNSを任意のクラウドプロバイダーのサービスに対応させたい場合はクラウドプロバイダーによって用意されたAPIクライアントを利用することができます。
これはクラウドプロバイダーのサービスに限らず、APIを用いてDNSレコードの操作が行える環境の場合は対応するAPIクライアントを利用することができます。
しかし、ExternalDNSに対応させたい環境にDNSレコードを操作するAPIがない場合は、何らかしらの方法でExternalDNSからDNSレコードを操作できる手段が必要となります。

今回はExternalDNSに対応させたい環境のAPIクライアントが利用できるものとしてプロバイダを開発していきます。
(今回、さくらのクラウドに対応させる際はsacloud/iaas-api-goを利用しました。)

type SakuraCloudProvider struct {
	provider.BaseProvider
	Client *iaas.Client
	domainFilter endpoint.DomainFilter
	DryRun bool
}

Recordsメソッドを実装

上記で定義した構造体がProviderインターフェースを満たすためにRecordsメソッドを実装します。
このメソッドは[]*endpoint.Endpointを返す必要があります。

func (p *SakuraCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
	...
}

endpoint.EndpointはExternalDNS起動時にフラグ--domain-filterで指定されたゾーンのDNSレコードの情報をExternalDNSのコアコンポーネントが利用するためのものです。

APIクライアント(Clientフィールド)を利用してDNSレコードの情報を取得し、それをもとにendpoint.Endpointを作成する必要があります。

https://github.com/kubernetes-sigs/external-dns/blob/4046257911958c1716f9a63ccf7846e2ee412ae6/endpoint/endpoint.go#L161-L179

endpoint.Endpointを作成する際はSupportedRecordType関数がTrueを返すレコードタイプのDNSレコードの情報のみ利用した方がよさそうです。
(他のプロバイダの実装コードを参照してみると上記を満たしている。)

https://github.com/kubernetes-sigs/external-dns/blob/4046257911958c1716f9a63ccf7846e2ee412ae6/provider/recordfilter.go#L21-L28

ApplyChangesメソッドを実装

次に、ApplyChangesメソッドを実装します。

func (p *SakuraCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
	...
}

引数changesにはExternalDNSのコアコンポーネントが作成した反映すべきDNSレコードの情報が保持されているので、APIクライアント(Clientフィールド)を利用してDNSレコードの作成/更新/削除の処理を行う必要があります。

https://github.com/kubernetes-sigs/external-dns/blob/4046257911958c1716f9a63ccf7846e2ee412ae6/plan/plan.go#L55-L65

フラグ--dry-runが指定されている場合(DryRunフィールドがTrue)、DNSレコードが作成/更新/削除を行わないようにします。

NewHogeHogeProvider関数を作成

開発するプロバイダの構造体を初期化する関数を作成します。
主な処理はAPIクライアントの認証情報の取得処理やAPIクライアントの生成になると思います。
開発するプロバイダの構造体の定義内容に応じて作成します。

さくらのクラウドに対応したプロバイダ開発の場合、下記のようになりました。

func NewSakuraCloudProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SakuraCloudProvider, error) {
	token, ok := os.LookupEnv("SAKURACLOUD_ACCESS_TOKEN")
	if !ok {
		return nil, fmt.Errorf("No token found")
	}
	secret, ok := os.LookupEnv("SAKURACLOUD_ACCESS_TOKEN_SECRET")
	if !ok {
		return nil, fmt.Errorf("No secret found")
	}

	client := iaas.NewClient(token,secret)

	provider := &SakuraCloudProvider{
		Client:       client,
		domainFilter: domainFilter,
		DryRun      : dryRun,
	}

	return provider, nil

pkg/apis/externaldns/types.go

開発したプロバイダを利用してExternalDNSを起動できるように、providerフラグに任意のプロバイダ名を追加します。

@@ -413,7 +413,7 @@ func (cfg *Config) ParseFlags(args []string) error {
	app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)

	// Flags related to providers
-	app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns")
+	app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns", "sakuracloud")
	app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
	app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
	app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)

main.go

プロバイダの開発で作成したパッケージをインポートし、開発したプロバイダを利用するためにproviderフラグに任意のプロバイダ名が指定された際、対応するプロバイダが作成されるようなコードを追加します。

@@ -68,6 +68,7 @@ import (
	"sigs.k8s.io/external-dns/provider/ultradns"
	"sigs.k8s.io/external-dns/provider/vinyldns"
	"sigs.k8s.io/external-dns/provider/vultr"
+	"sigs.k8s.io/external-dns/provider/sakuracloud"
	"sigs.k8s.io/external-dns/registry"
	"sigs.k8s.io/external-dns/source"
)
@@ -333,6 +334,8 @@ func main() {
		p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun)
	case "safedns":
		p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
+	case "sakuracloud":
+		p, err = sakuracloud.NewSakuraCloudProvider(domainFilter, cfg.DryRun)
	default:
		log.Fatalf("unknown dns provider: %s", cfg.Provider)
	}

まとめ

ExternalDNSで未対応のプロバイダを開発する方法を解説しました。

ExternalDNSには新しいプロバイダを開発できる仕組みが用意されているため、任意の環境におけるプロバイダを開発することでExternalDNSを利用することができるようになります。

Providerインターフェースに定義されたRecordsメソッドとApplyChangesメソッドを実装し、Providerインターフェースを満たす任意のProviderを作成することが基本的な開発方針となります。

主に実装すべき箇所や修正すべきところを解説しました。

最後に

主にRecordsメソッドとApplyChangesメソッドの実装解説では、メソッド内でどういうデータを使って、どういうデータが、どういう処理が必要があるのか、という基本的な実装方針を解説しました。

必要に応じて、クライアントAPIを利用して取得したDNSレコードの情報をもとに[]*endpoint.Endpointを作成する処理(または関数)やその逆のことを行う処理(または関数)などを作成し、RecordsメソッドとApplyChangesメソッドを実装してみてください。

本記事を作成するにあたって、ExternalDNS未対応の環境であるさくらのクラウドのプロバイダを開発しました。参考になれば幸いです。

https://github.com/sosomasox/external-dns/pull/1/commits/7f60f9fb6b74d86a3bd7d402c22962264cc63d84

参考

sacloud-archives/external-dns
をたいへん参考にさせていただきました。

また、AWSやCloudflare、GoogleなどExternalDNS対応プロバイダのコードがたくさんありますので開発する際の参考になりました。
https://github.com/kubernetes-sigs/external-dns/tree/master/provider

Discussion