1つのKubernetesクラスタで複数のシングルテナント向けの証明書を取り扱う

# 背景
あるところに、[Azure Kubernetes Service](https://azure.microsoft.com/ja-jp/services/kubernetes-service/)を用いたWebサービス公開に携わる一人のエンジニアがおりました。
ある日彼はこんなことを考えました。

(-"-)「今開発中の機能はサーバー負荷がかなり大きい…」
(-"-)「利用が集中すると他のユーザーに影響する恐れがある…」
(-"-)「でもユーザーによってはそういった形での業務影響が許されない…」
(・_・)！「よし、シングルテナント構成を取れるようにしよう」

そこで彼はハイパフォーマンスを要求するユーザー向けに個別のクラスタ構成を定義し、それぞれを別のnamespaceで動かすことを考えました。また各ユーザー向けには専用のサブドメインを切り、[Azure DNS](https://azure.microsoft.com/ja-jp/services/dns/)を用いてドメイン毎に別のクラスタが利用されるよう振り分けを行うことにしました。

めちゃ雑に図にするとこんな感じです。

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76172/7bf259ab-230e-9322-cdbc-a22a87d382bb.png)

# 本題

さて、もはや常時HTTPSが当たり前の時代で、彼のWebサービスでもHTTPS越しにサービスを公開しています。ドメインが増えればその当然そのサーバー証明書が必要になります。

証明書はお金で買うことも出来ますし、買った証明書はKubernetesのsecretを経由することで比較的に各Podへの配布ができます。しかし彼はケチでものぐさなエンジニアだったので、**証明書を買うのにお金をかけることはしたくなかった**し、**証明書の期限が切れるたびにsecretを更新する手間をかけたくもなかった**のです。

無料の証明書と言えばそう、[Let's Encrypt](https://letsencrypt.org/ja/)の出番です。幸い世の中にはLet's Encryptからの証明書取得を行うためのKubernetesアドオンがありましたし、親切にもAzureにはこれを利用するための公式ドキュメントがありました。

[jetstack/cert-manager](https://github.com/jetstack/cert-manager)
[Azure Kubernetes Service (AKS) で HTTPS イングレス コントローラーを作成する](https://docs.microsoft.com/ja-jp/azure/aks/ingress-tls)

## 壁その1: Let's Encryptのレート制限

Let's Encryptに関して彼は全くの素人でしたが、ネット上のサンプルを参考に証明書の取得をすることは簡単にできました。

```yaml:issuer.yml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: hoge@example.com
    privateKeySecretRef:
      name: letsencrypt-key
    http01: {}
```

```yaml:certificate.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-certificate
spec:
  secretName: certs-example
  issuerRef:
    kind: Issuer
    name: letsencrypt
  commonName: "a.example.com"
  dnsNames:
    - "a.example.com"
```

たったこれだけで　`a.example.com` 向けの証明書が取得できたので、意気揚々と各名前空間上に同様のオブジェクトをデプロイしました。これはとても上手く行きました。

しかし価値を確信した刹那、とある記事が彼の目に留まりました。

[レート制限 -Let's Encrypt-](https://letsencrypt.org/ja/docs/rate-limits/)

その記事にはこうあります。

> 主なレート制限としては、登録ドメインごとの証明書数 (1週間に50個まで) があります。登録ドメインとは、一般に言うと、あなたがドメイン名レジストラから購入したドメインの一部のことです。たとえば、www.example.com の場合、登録ドメインは example.com です。new.blog.example.co.uk の場合、登録ドメインは example.co.uk になります。

これは今後ユーザーが増え続けたとき、名前空間の数だけ証明書のリクエストが発生することでレート制限に引っ掛かる可能性を意味していました。

彼は詰みました。

### 解決策

ユーザー数が増えるにつれて大量のドメインが要るとは言っても、所詮は `example.com` のサブドメインが増えるに過ぎません。
ならば、ワイルドカード証明書 `*.example.com` を使うことができれば証明書を1つにまとめることができるはずです。

軽く調べてみるとLet's Encryptはワイルドカード証明書に対応していることがわかりました。
やったね！

## 壁その2: ワイルドカード証明書の取得に関する制限

早速、取得する証明書をワイルドカード形式にしてみることにしました。

```yaml:certificate.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-certificate
spec:
  secretName: certs-example
  issuerRef:
    kind: Issuer
    name: letsencrypt
  commonName: "*.example.com"
  dnsNames:
    - "*.example.com"
```

結果、これまで上手くいっていた証明書取得は謎のエラーにより動かなくなりました。

彼はふたたび詰みました。

### 解決策

どうやらLet's Encryptでのワイルドカード証明書の取得には、よりセキュアなチャレンジ方式を用いる必要があるようでした。
彼が使っていたのは最もベーシックな `HTTP-01` でしたが、これをより信頼性の高い `DNS-01` チャレンジにすることで解決することがわかりました。

cert-managerにはAzure DNSを用いて `DNS-01` チャレンジを行うための仕様が用意されていたので、設定してみることにしました。

```yaml:issuer.yml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: hoge@example.com
    privateKeySecretRef:
      name: letsencrypt-key
    solvers:
      - dns01:
          azureDNS:
            environment: AzurePublicCloud
            subscriptionID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
            resourceGroupName: hogehoge
            hostedZoneName: example.com
            tenantID: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
            clientID: zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz
            clientSecretSecretRef:
              name: azuredns
              key: azure-client-secret
```

この設定は見事に動き、生成されたワイルドカード証明書がSecretとして保存されました。
よかったね！

## 壁その3: Secretのスコープ

しかしここで彼は気づきました。

「Secretって他の名前空間で使いまわせなくね？」

そう、せっかく作った証明書のSecretですが、これを各名前空間で共通して使うことができないと意味がなかったのです。そして、そんな仕組みは今のKubernetesにはありませんでした。

彼は三たび詰みました。

### 解決策

彼に残されたのは、たった1つのシンプルな答えでした。

「複数の名前空間で使いまわせなければ、Secretをコピーすればいいじゃない」

そして ~~もはやヤケクソになった~~ 彼は ~~クソ雑な~~ コードを書き上げました。

https://gist.github.com/hy3/39f929816083a602c2f748d11d2c98f2

そう、彼を最後に助けたのはコードを書く腕力でした。
あとはCronJobによる定期実行が全てを解決してくれました。
世界に平和が訪れたのです。

がんばったね！

# まとめ

そんなわけで、ユーザーごとに異なるコンテナを異なるドメインで提供するようなサービス構成を作ってみたら思いのほか苦労したよ！というお話でした。
苦心した甲斐あって今では証明書の調達に悩まされることもなく、安定してサービス提供が出来ていて万々歳です。

……そして彼はのちに複数の名前空間に分かれた各環境のバージョン管理や運用にてもっと苦労することになるのですが、それはまた別のお話ということで。