📺

Cert managerを拡張し未対応のDNSプロバイダで利用する

2020/12/14に公開

概要

Cert managerはデフォルトでCloud DNSやRoute 53をサポートしていますが、Alibaba Cloud DNSやさくらのクラウドのDNSアプライアンスなどはサポートしておらず、そのままインストールしても利用できません。

// github.com/jetstack/cert-manager/pkg/issuer/acme/dns/dns.go

type dnsProviderConstructors struct {
	cloudDNS     func(project string, serviceAccount []byte, dns01Nameservers []string, ambient bool, hostedZoneName string) (*clouddns.DNSProvider, error)
	cloudFlare   func(email, apikey, apiToken string, dns01Nameservers []string) (*cloudflare.DNSProvider, error)
	route53      func(accessKey, secretKey, hostedZoneID, region, role string, ambient bool, dns01Nameservers []string) (*route53.DNSProvider, error)
	azureDNS     func(environment, clientID, clientSecret, subscriptionID, tenantID, resourceGroupName, hostedZoneName string, dns01Nameservers []string, ambient bool) (*azuredns.DNSProvider, error)
	acmeDNS      func(host string, accountJson []byte, dns01Nameservers []string) (*acmedns.DNSProvider, error)
	digitalOcean func(token string, dns01Nameservers []string) (*digitalocean.DNSProvider, error)
}

しかしCert managerにはWebhookの拡張用インターフェースが用意されており、当該インターフェースに準拠したWebサーバを開発することでデフォルトでサポートしているDNSプロバイダ以外でもCert managerを利用することが可能となります。

今回はそのWebhookの仕組みに沿ってCert managerを拡張する方法を解説します。

開発

Webhookでの拡張機能開発用に公式(jetstack)から以下のテンプレートリポジトリが提供されており、これに沿って開発を進めることができます。

https://github.com/jetstack/cert-manager-webhook-example

上記のテンプレートリポジトリにあるmain.goは以下のように定義されており、それぞれのイベントに対応したメソッドが用意されています。

package main

import (
	"encoding/json"
	"fmt"
	"os"

	extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	"k8s.io/client-go/rest"

	"github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
	"github.com/jetstack/cert-manager/pkg/acme/webhook/cmd"
)

var GroupName = os.Getenv("GROUP_NAME")

func main() {
	if GroupName == "" {
		panic("GROUP_NAME must be specified")
	}

	cmd.RunWebhookServer(GroupName,
		&customDNSProviderSolver{},
	)
}

type customDNSProviderSolver struct {
}

type customDNSProviderConfig struct {
}

func (c *customDNSProviderSolver) Name() string {
	return "my-custom-solver"
}

func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
	cfg, err := loadConfig(ch.Config)
	if err != nil {
		return err
	}
	return nil
}

func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
	return nil
}

func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
	return nil
}

func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) {
	cfg := customDNSProviderConfig{}
	if cfgJSON == nil {
		return cfg, nil
	}
	if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil {
		return cfg, fmt.Errorf("error decoding solver config: %v", err)
	}

	return cfg, nil
}

実装

主に実装すべき箇所となるmain.goにある構造体やメソッドを順に解説していきます。

customDNSProviderConfig

この構造体は以下のように定義されており主にDNSプロバイダのAPIキーなど何らか設定情報のために用意されています。

type customDNSProviderConfig struct {
	...
}

customDNSProviderSolver

この構造体はCert managerのイベントに対応したロジックを定義したメソッドを保持します。

type customDNSProviderSolver struct {
	:
}

groupName及びsolverName

webhookで実装された拡張機能は事前に定義・設定されたgroupNamesolverNameによって識別されます。

main.goでは環境変数であるGROUP_NAMEから値を取得しmain()関数内でcmd.RunWebhookServer()関数の第一引数として取得した値を渡すことでgroupNameを設定します。

var GroupName = os.Getenv("GROUP_NAME")

func main() {
	if GroupName == "" {
		panic("GROUP_NAME must be specified")
	}

	cmd.RunWebhookServer(GroupName,
		&customDNSProviderSolver{},
	)
}

加えてcustomDNSProviderSolver構造体はName()というメソッドを持っており、この戻り値がsolverNameとして扱われます。

func (c *customDNSProviderSolver) Name() string ...

DNS01チャレンジ用TXTレコードの追加及び削除

Let's encriptのDNS01チャレンジで用いるTXTレコードの追加及び削除は以下のメソッド内で行います。

func (solver *SacloudDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error ... // 追加
func (solver *SacloudDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error ... // 削除

上記メソッド内で行う処理はDNSプロバイダによって様々でプロバイダ側から提供されているライブラリやAPIを用いてチャレンジ用のレコードの作成及び削除を行う必要があります。

これで実装すべき箇所は以上となります。

デプロイ

Dockerイメージ化

Kubernetesのクラスタ上にデプロイする際は上記で実装したwebhookを事前にDockerイメージ化する必要があります。自身はGithub Actionsでコードのビルド及びタグ付け、レジストリへのpushまでを自動化しています。

Helm Chartの用意

デプロイするためのマニフェストはテンプレートリポジトリのdeploy/example-webhookディレクトリにあるHelm Chartを雛形として利用します。

当該ディレクトリ内のvalues.yamlは以下のように定義されており必要に応じてDockerイメージのレジストリやイメージ、タグなどの値を設定します。

groupName: acme.mycompany.com

certManager:
  namespace: cert-manager
  serviceAccountName: cert-manager

image:
  repository: mycompany/webhook-image
  tag: latest
  pullPolicy: IfNotPresent

nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP
  port: 443

resources: {}

nodeSelector: {}

tolerations: []

affinity: {}

Apply

以下のコマンドでHelmとしてインストールします。

$ helm install ./deploy/example-webhook/ --generate-name

デバッグ

デバッグは主にk8s.io/klogパッケージでコード内にログの出力処理を埋め込んで行います。その他にもcert-managerのnamespace内に存在するPodからもログが出力されるので参考にできると思います。

留意点

以下のプルリクで投げられている修正は適応する必要があります。

https://github.com/jetstack/cert-manager-webhook-example/pull/14/

参考

自身はさくらのクラウドで提供されているDNSアプライアンス向けにwebhookを実装しましたが他にもAlibabaなどの実装が存在し非常に参考になりました。

https://github.com/DEVmachine-fr/cert-manager-alidns-webhook

Discussion