Kubernetes+Let’s Encryptでワイルドカード証明書の自動発行基盤を作る

はじめに

SEROKU の開発フローでは、開発用のリポジトリに Mercurial を用い、各機能実装ごとにブランチを用意した上で開発を行っています。

その上でさらに、社内に Kubernetes クラスタを用意して、ブランチごとにデモ環境を立ち上げられるような仕組みを整備しています。

関連記事

SaaS運用での大障害の思い出と対策の共有(大噴火編)

Kubernetes時代のCI/CD Jenkins Xとは?-前編

当初は、各ブランチごとにドメインを作成し、Let’s Encrypt による SSL 証明書を発行していたのですが、並行で開発するブランチ数が多くなってくるとやがて rate-limit に当たるようになりました。
(参考: Rate Limits – Let’s Encrypt – Free SSL/TLS Certificates)

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

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

必要となる前提知識

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

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

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

  1. kubectl コマンドを利用して、Kubernetes クラスタにログイン・操作できること

  2. helm コマンドを利用して、Kubernetes クラスタに任意の Chart をインストールできること

  3. 証明書を発行する予定のドメインを保持していること

    • 本記事では、証明書を発行する対象ドメインをexample.com とします
  4. AWS Route 53 で上記ドメインが管理できていること

    • ドメインの NS レコードが Route 53 に向いていることを前提とします

本記事のゴール

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

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

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

本記事では、Kubernetes クラスタ上で証明書の自動発行・更新を担ってくれるミドルウェアとして 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 へアクセスします
  2. 左側のサイドバーより「ユーザー」をクリックします
  3. 上部にある「ユーザーを追加」ボタンをクリックします
  4. 「ユーザー名」に適当な文字列を入れます
    • 自分が後で見て分かりやすい文字列を入力しておきましょう
    • 例: k8s-cert-manager など
  5. 「アクセスの種類」は「プログラムによるアクセス」にチェックを入れます
  6. 「次のステップ: アクセス権限」ボタンをクリックします
  7. 「xxx のアクセス権限を設定」の箇所で、「既存のポリシーを直接アタッチ」をクリックします
  8. 「ポリシーの作成」ボタンをクリックします
  9. 開いたタブ/ウィンドウの中で「JSON」タブをクリックします
  10. 出てきたテキストエリアに以下のテキストをコピーします
    {
        "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": "*"
            }
        ]
    }
    • 公式ドキュメント に IAM に設定すべきポリシーが記載されていますが、その内容だと cert-manager がうまく Route 53 にアクセスできないため、こちらの Issue に挙がっているポリシーを使います((2019/09/12 現在修正されている可能性があります))
  11. 画面下部にある「Review Policy」ボタンをクリックします
  12. 「名前」に適当な文字列を入力します
    • 例: route53-access-for-cert-manager
  13. 「Create Policy」ボタンをクリックします
  14. 「xxx が作成されました」という表示が確認出来たら、そのウィンドウ/タブは閉じてください
  15. 元のユーザ追加画面で「更新」ボタンを押してから、作成したポリシーを検索し、画面左端のチェックボックスをオンにします
  16. 「次のステップ: 確認」ボタンをクリックします
  17. 内容に問題がないようであれば、そのまま「ユーザーの作成」ボタンをクリックします
  18. 作成に完了すると、アクセスキーID/シークレットアクセスキーが表示されますので、忘れずにメモしましょう
    • ここで表示されるアクセスキーID/シークレットアクセスキーをこの後の手順で利用します
    • ちなみにメモし忘れたとしても、今回作成したユーザについて別のアクセスキーを発行することが可能です

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

公式ドキュメント通り、helm を用いて cert-manager を Kubernetes クラスタへインストールします。

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

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

$ 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 というリソースで管理します。

Issuer/ClusterIssuer は namespace 毎に利用可能とするか、クラスタ全体で利用可能とするかの違いで、本記事ではクラスタ全体で利用できるように ClusterIssuer として登録します。

  1. まず、IAM ユーザー作成時に発行したシークレットアクセスキーを Secret として登録します
    $ kubectl -n kube-system create secret generic prod-route53-credentials-secret --from-literal=secret-access-key=<シークレットアクセスキー>
  2. 登録できたか確認します
    $ 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 クラスタ上に登録します
    $ 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 のアカウント発行状況を確認します
    $ 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. 証明書の発行

ここまで準備できたら、いよいよ証明書を発行してみます。

cert-manager では、発行する証明書の中身を定義する設定を Certificate というリソースで管理します。

本記事では、*.example.com に関するワイルドカード証明書を発行する、という想定で設定を記載していきます。

  1. Certificate を作成し、Kubernetes クラスタ上に登録します
    $ 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. 証明書の発行状況を確認します

    $ 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</nil>
    • DNS-01 によるドメイン検証は DNS 伝搬に時間がかかるため、証明書発行が完了するまで少し待つ必要があります
      • DNS 伝搬が完了するまで待機する作業も cert-manager ではやってくれています!
    • 証明書発行が完了すると、上記のように Certificate renewed successfully というメッセージが確認できます
  3. 証明書が出力されたことを確認します
    $ 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 を登録してみます。
Ingress で指定する証明書に、今回発行した証明書を指定しています。

  1. サンプルアプリを立ち上げます
    $ 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」の箇所)
    $ 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 へアクセスし、上記のアドレスに対して nginx.example.com でアクセスできるように A レコードを追加します。

7. Let’s アクセス!!

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

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

サンプルで利用したサービスの設定は以下で全部削除できます。

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

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

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

詳しくは、cert-manager の公式ドキュメントもご覧ください。

まとめ

いかがでしたでしょうか。本記事で紹介した基盤を Kubernetes クラスタ上に用意しておくことで、デモ環境をよりたくさん、安全に用意することが可能になります。

是非、CI/CD と併せて皆さんの開発環境に取り入れていただき、本記事が開発・リリース速度を向上させる上で参考になれば幸いです。

関連記事

WESEEKで採用している CI/CD 実現手法

システム開発におけるテストの概要や概念 | |第2話

システム開発におけるテストの概要や概念 |第1話