Kubernetes + Let's Encrypt + Route 53 でワイルドカード証明書を自動発行できる基盤を作ってみよう

*本記事は、自分が[別のサイトへ記載した内容](https://seroku.jp/wp/kubernetes-lets-encrypt-%E3%81%A7%E3%83%AF%E3%82%A4%E3%83%AB%E3%83%89%E3%82%AB%E3%83%BC%E3%83%89%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%82%92%E8%87%AA%E5%8B%95%E7%99%BA%E8%A1%8C%E3%81%A7%E3%81%8D%E3%82%8B/)を、一部編集して記載してます。*

# はじめに

自分が今いる会社[^1]では、プロダクト開発を行うときに、開発用のリポジトリに git/Mercurial を用い、各機能実装ごとにブランチを用意した上で開発を行っています。
その上で、最近では社内に Rancher + Kubernetes クラスタを用意して、ブランチごとに CI を行いデモ環境を立ち上げられるような仕組みを整備しています。[^2]

[^1]: https://weseek.co.jp
[^2]: CI については別途、[こちら](https://seroku.jp/wp/seroku%E3%82%92%E6%94%AF%E3%81%88%E3%82%8B%E6%8A%80%E8%A1%93%E3%80%9Ccicd%E7%B7%A8%E3%80%9C/) に記載しています

当初は、各ブランチごとにドメインを作成し、Let's Encrypt による SSL 証明書を発行していたのですが、並行で開発するブランチ数が多くなってくるとやがて証明書発行数の rate-limit [^3]に当たるようになりました。

[^3]: [Rate Limits - Let's Encrypt - Free SSL/TLS Certificates](https://letsencrypt.org/docs/rate-limits/))

今回は、それを回避するために必要となった、Kubernetes クラスタ上で Let's Encrypt を用いたワイルドカード証明書を自動発行・更新できる基盤の作り方についてご紹介します。

会社では AWS Route 53 でドメインを管理しているため、本記事では Route 53 を利用したドメインを対象とします。

# 必要となる前提知識

本記事では、以下の技術用語については説明しませんので、まだご存知ないという方は以下の参考サイトをご覧になってから本記事をご覧になると、より理解が進むと思います。

- Kubernetes
    - [Kubernetes: Production-Grade Container Orchestration](https://kubernetes.io/)
    - [今さら人に聞けない Kubernetes とは？ - Qiita](https://qiita.com/MahoTakara/items/85096f8b2632c802ab22)
- Ingress
    - [KubernetesクラスタにServiceとIngressを追加し、Webアプリケーションを外部に公開](https://codezine.jp/article/detail/10523)
    - [kubernetesにingressを導入する方法 - Qiita](https://qiita.com/Hirata-Masato/items/8e6b4536b6f1b23c5270)
- Helm
    - [Helm - The Kubernetes Package Manager](https://helm.sh/)
    - [Kubernetes: パッケージマネージャHelm - Qiita](https://qiita.com/tkusumi/items/12857780d8c8463f9b9c)
- Let's Encrypt
    - [Let's Encrypt - Free SSL/TLS Certificates](https://letsencrypt.org/)
    - [Let's Encrypt 総合ポータル](https://letsencrypt.jp/)
- ワイルドカード証明書
    - [ワイルドカード SSLサーバ証明書とは？](https://www.toritonssl.com/about_ssl/wildcard_ssl_certificate.html)
- DNS-01
    - [CertbotでDNSによる認証(DNS-01)で無料のSSL/TLS証明書を取得する](http://blog.jicoman.info/2017/04/certbot_dns_01/)
- AWS Route 53
    - [Amazon Route 53（クラウド DNS - ドメインネームサーバー） | AWS](https://aws.amazon.com/jp/route53/)

# 基盤構築にあたって必要となるスタート条件

本記事では、以下の条件が揃った状態をスタートに必要な条件とします。

1. `kubectl` コマンドを利用して、Kubernetes クラスタにログイン・操作できること
2. `helm` コマンドを利用して、Kubernetes クラスタに任意の Chart をインストールできること
3. 証明書を発行する予定のドメインを保持していること
    - 本記事では、証明書を発行する対象ドメインを `example.com` とします
4. AWS Route 53 で上記ドメインが管理できていること
    - ドメインの NS レコードが Route 53 に向いていることを前提とします

# 本記事のゴール

Kubernetes クラスタ上で以下ができるようになることをゴールとします。

- 保持しているドメインに関するワイルドカード証明書を発行できること
- 発行した証明書を利用して、任意の Web サービスを公開できること

# 本記事でご紹介するミドルウェア(cert-manager)について

本記事では、Kubernetes クラスタ上で証明書の自動発行・更新を担ってくれるミドルウェアとして [cert-manager](https://github.com/jetstack/cert-manager) v0.3.0(執筆時最新) を利用します。

このミドルウェアは、Let's Encrypt で証明書を発行・更新する上で必要となる以下の作業を自動化してくれます。

- Let's Encrypt API へ適宜アクセスし、ドメイン検証用コードを発行し、証明書を発行する
- Route 53 へのドメイン検証用 TXT レコードの作成
- 発行した証明書を Kubernetes クラスタへ格納する

# 構築手順
## 1. AWS IAM role の用意

まず、cert-manager が Route 53 を操作するために必要なユーザを追加します。

1. [AWS IAM の Management Console](https://console.aws.amazon.com/iam/home) へアクセスします
2. 左側のサイドバーより「ユーザー」をクリックします
3. 上部にある「ユーザーを追加」ボタンをクリックします
4. 「ユーザー名」に適当な文字列を入れます
    - 自分が後で見て分かりやすい文字列を入力しておきましょう
    - 例: `k8s-cert-manager` など
5. 「アクセスの種類」は「プログラムによるアクセス」にチェックを入れます
6. 「次のステップ: アクセス権限」ボタンをクリックします
7. 「xxx のアクセス権限を設定」の箇所で、「既存のポリシーを直接アタッチ」をクリックします
8. 「ポリシーの作成」ボタンをクリックします
9. 開いたタブ/ウィンドウの中で「JSON」タブをクリックします
10. 出てきたテキストエリアに以下のテキストをコピーします

    ```json
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "route53:GetChange",
                "Resource": "arn:aws:route53:::change/*"
            },
            {
                "Effect": "Allow",
                "Action": "route53:ChangeResourceRecordSets",
                "Resource": "arn:aws:route53:::hostedzone/*"
            },
            {
                "Effect": "Allow",
                "Action": "route53:ListHostedZonesByName",
                "Resource": "*"
            }
        ]
    }
    ```
    - [公式ドキュメント](https://cert-manager.readthedocs.io/en/latest/reference/issuers/acme/dns01.html#amazon-route53) に IAM に設定すべきポリシーが記載されていますが、その内容だと cert-manager がうまく Route 53 にアクセスできないため、[こちらの Issue](https://github.com/jetstack/cert-manager/issues/403#issuecomment-381054872) に挙がっているポリシーを使います
11. 画面下部にある「Review Policy」ボタンをクリックします
12. 「名前」に適当な文字列を入力します
    - 例: `route53-access-for-cert-manager`
13. 「Create Policy」ボタンをクリックします
14. 「xxx が作成されました」という表示が確認出来たら、そのウィンドウ/タブは閉じてください
15. 元のユーザ追加画面で「更新」ボタンを押してから、作成したポリシーを検索し、画面左端のチェックボックスをオンにします
16. 「次のステップ: 確認」ボタンをクリックします
17. 内容に問題がないようであれば、そのまま「ユーザーの作成」ボタンをクリックします
18. 作成に完了すると、アクセスキーID/シークレットアクセスキーが表示されますので、**忘れずにメモしましょう**
    - ここで表示されるアクセスキーID/シークレットアクセスキーをこの後の手順で利用します
    - ちなみにメモし忘れたとしても、今回作成したユーザについて別のアクセスキーを発行することが可能です

## 2. Kubernetes クラスタ上に cert-manager のインストール

[公式ドキュメント](https://cert-manager.readthedocs.io/en/latest/getting-started/2-installing.html#with-helm)通り、helm を用いて cert-manager を Kubernetes クラスタへインストールします。

```bash
$ helm install \
    --name cert-manager \
    --namespace kube-system \
    stable/cert-manager
```

インストールが完了すると、以下の様に `kube-system` namespace に `cert-manager` の Pod が動き始めます。

```bash
$ kubectl -n kube-system get pod
NAME                                           READY     STATUS    RESTARTS   AGE
cert-manager-cert-manager-56fcf79bc8-dx85q     1/1       Running   0          39m
```

## 3. 証明書を発行するための設定を行う

cert-manager では、証明書を発行するための ACME プロパイダ(Let's Encrypt)や DNS プロパイダなどの設定を Issuer/ClusterIssuer というリソースで管理します。<br>
Issuer/ClusterIssuer は namespace 毎に利用可能とするか、クラスタ全体で利用可能とするかの違いで、本記事ではクラスタ全体で利用できるように ClusterIssuer として登録します。

1. まず、IAM ユーザー作成時に発行したシークレットアクセスキーを Secret として登録します

    ```bash
    $ kubectl -n kube-system create secret generic prod-route53-credentials-secret --from-literal=secret-access-key=<シークレットアクセスキー>
    ```

2. 登録できたか確認します

    ```bash
    $ kubectl -n kube-system get secret prod-route53-credentials-secret
    NAME                              TYPE      DATA      AGE
    prod-route53-credentials-secret   Opaque    1         59d
    ```

3. DNS-01 でドメインを検証できるようにする ClusterIssuer を作成し、Kubernetes クラスタ上に登録します

    ```bash
    $ cat <<'EOF'> clusterissuer-letsencrypt.yaml
    apiVersion: certmanager.k8s.io
    kind: ClusterIssuer
    metadata:
      name: letsencrypt
    spec:
      acme:
        email: <任意のメールアドレス>
        server: https://acme-staging-v02.api.letsencrypt.org/directory
        privateKeySecretRef:
          name: letsencrypt-private-key
        dns01:
          providers:
          - name: route53
            route53:
              accessKeyID: <アクセスキーID>
              region: us-east-1
              secretAccessKeySecretRef:
                key: secret-access-key
                name: prod-route53-credentials-secret
    EOF
    $ kubectl apply -f clusterissuer-letsencrypt.yaml
    ```
    - `<任意のメールアドレス>` の部分は、ご自身で受信できるメールアドレスを記載してください
        - 発行した証明書の期限が近づくと、メールで通知が来るようになります
    - `<アクセスキーID>` の部分は、IAM ユーザー作成時に発行したアクセスキーIDを記載してください
4. Let's Encrypt のアカウント発行状況を確認します

    ```bash
    $ kubectl describe clusterissuer letsencrypt
    ...(snip)...
    Status:
      Acme:
        Uri:  https://acme-v02.api.letsencrypt.org/acme/acct/XXXXXXXX
      Conditions:
        Last Transition Time:  2018-05-18T15:20:48Z
        Message:               The ACME account was registered with the ACME server
        Reason:                ACMEAccountRegistered
        Status:                True
        Type:                  Ready
    Events:                    <none>
    ```
    - 上記のように、Message が `The ACME account was registered with the ACME server` となっていれば、Let's Encrypt アカウントを正常に作成できています

## 4. 証明書の発行

ここまで準備できたら、いよいよ証明書を発行してみます。<br>
cert-manager では、発行する証明書の中身を定義する設定を Certificate というリソースで管理します。<br>
本記事では、`*.test.example.com` に関するワイルドカード証明書を発行する、という想定で設定を記載していきます。

1. Certificate を作成し、Kubernetes クラスタ上に登録します

    ```bash
    $ cat <<'EOF'> cert-wildcard-example.yaml
    apiVersion: certmanager.k8s.io/v1alpha1
    kind: Certificate
    metadata:
      name: wildcard-example
    spec:
      acme:
        config:
        - dns01:
            provider: route53
          domains:
          - '*.example.com'
      commonName: '*.example.com
      issuerRef:
        kind: ClusterIssuer
        name: letsencrypt
      secretName: cert-wildcard-example
    EOF
    $ kubectl apply -f cert-wildcard-example.yaml
    ```
    - `kubectl apply` によるリソースの登録が完了すると、cert-manager によってすぐに証明書発行作業が開始されます
    - 証明書発行が完了すると、`spec.secretName` に設定された Secret に取得した証明書の内容が出力されます
2. 証明書の発行状況を確認します

    ```bash
    $ kubectl describe cert wildcard-example
    ...(snip)...
    Status:
      Acme:
        Order:
          URL:  https://acme-v02.api.letsencrypt.org/acme/order/XXXXXXXX/XXXXXXXX
      Conditions:
        Last Transition Time:  2018-06-18T03:26:11Z
        Message:               Certificate renewed successfully
        Reason:                CertRenewed
        Status:                True
        Type:                  Ready
        Last Transition Time:  <nil>
        Message:               Order validated
        Reason:                OrderValidated
        Status:                False
        Type:                  ValidateFailed
    ```
    - DNS-01 によるドメイン検証は DNS 伝搬に時間がかかるため、証明書発行が完了するまで少し待つ必要があります
        - DNS 伝搬が完了するまで待機する作業も cert-manager ではやってくれています！
    - 証明書発行が完了すると、上記のように `Certificate renewed successfully` というメッセージが確認できます
3. 証明書が出力されたことを確認します

    ```bash
    $ kubectl get secret cert-wildcard-example
    NAME                    TYPE                DATA      AGE
    cert-wildcard-example   kubernetes.io/tls   2         25d
    ```
    - 上記のように `kubernetes.io/tls` タイプの Secret の存在が確認できれば、証明書は発行できています！

## 5. Kubernetes クラスタ上に立てた Web サービスの公開

本記事では、サンプルのための nginx Pod を Kubernetes クラスタ上に立てて、それを外部に公開するための Service/Ingress を登録してみます。<br>
Ingress で指定する証明書に、今回発行した証明書を指定しています。

1. サンプルアプリを立ち上げます

    ```bash
    $ cat <<'EOF' > example-nginx.yaml
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: nginx-example-deployment
      labels:
        app: example-nginx
    spec:
      replicas: 1
      template:
        metadata:
          labels:
            app: example-nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            ports:
            - containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: example-nginx
      labels:
        app: example-nginx
    spec:
      ports:
      - name: http
        port: 80
        protocol: TCP
        targetPort: 80
      selector:
        app: example-nginx
      type: NodePort
    ---
    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      name: example-nginx
      labels:
        app: example-nginx
    spec:
      rules:
      - host: nginx.example.com
        http:
          paths:
          - backend:
              serviceName: example-nginx
              servicePort: 80
      tls:
      - hosts:
        - nginx.example.com
        secretName: cert-wildcard-example
    EOF
    $ kubectl apply -f example-nginx.yaml
    ```
2. Ingress で取得できたアドレスを確認します(以下の「ADDRESS」の箇所)

    ```bash
    $ kubectl get ing
    NAME            HOSTS               ADDRESS                  PORTS     AGE
    example-nginx   nginx.example.com   XXX.XXX.XXX.XXX          80, 443   3m
    ```
    - このアドレスに対して名前が引けるように DNS を設定します

## 6. AWS Route 53 の設定

[AWS Route53](https://console.aws.amazon.com/route53/home) へアクセスし、上記のアドレスに対して `nginx.example.com` でアクセスできるように A レコードを追加します。

## 7. Let's アクセス!!

https://nginx.example.com へアクセスして、以下の様に発行された証明書がブラウザ上で確認できれば、証明書発行基盤の構築は完了です！

![access-successfully.png](https://qiita-image-store.s3.amazonaws.com/0/42324/be2f7139-36af-c554-697d-99f13163d492.png)

- 注: `example.com` のドメインは所有できないものなので、この画像は合成です

サンプルで利用したサービスの設定は、以下のコマンドを実行することで全て削除できます。

```bash
$ kubectl delete deploy,svc,ing -l 'app=example-nginx'
```

# その他のドメイン検証方法

本記事では、ワイルドカード証明書を発行するために必要な DNS-01 方式のドメイン検証をする設定を作成しましたが、cert-manager では DNS-01 方式以外にも HTTP-01 方式でのドメイン検証にも対応しています。

詳しくは、[cert-manager の公式ドキュメント](https://cert-manager.readthedocs.io/)もご覧ください。