Edited at

GKE で TLS 証明書を自動管理(cert-manager DNS-01 編)

More than 1 year has passed since last update.

この記事は Kubernetes Advent Calendar 2017 の9日目です。


追記

Google Kubernetes Engine の中の人の @ahmetb が GKE と cert-manager HTTP-01 を組み合わせるチュートリアルを書いているので、英語で良いならそちらも推奨します。

https://github.com/ahmetb/gke-letsencrypt


まえがき

昨年は kube-legokube-cert-manager を使って Let's Encrypt で証明書を自動取得する記事を書きました。また当時はまだ証明書を Kubernetes で自動管理する手法は出てきたばかりでしたが、この1年間カジュアルに運用してみたという話はかなり多かったように思います。

実際に運用してみると、 kube-lego に苦しまされた方が多かったようです。理由としては、 Ingress を動的に更新することで ACME の HTTP-01 チャレンジに対応しており、更新時の挙動が安定しなかった GCE Ingress Controller(GKE でデフォルトで動作) で不可解な挙動になることが多かったことがあげられます。

また kube-lego は環境変数で設定するため、 Let's Encrypt のアカウントの設定やステージング・本番環境などを切り替えることが比較的困難でした。色々試しているうちにレートリミットに引っかかってしまうという話もありました。

更に kube-lego の説明には "Non-production use case 😆" という表記が消える気配がありません。

いつになったらプロダクションで使えるようになるのかと思って確認してみると、開発している JetStack は今は cert-manager を開発していて kube-lego はプロダクションユースに耐える状態になる可能性は低いことがわかります。

https://github.com/jetstack/kube-lego/issues/156#issuecomment-338867450

https://github.com/jetstack/kube-lego/issues/26#issuecomment-339993658

ということで、今後決定版になることが期待できる cert-manager を使って DNS-01 チャレンジで証明書を管理するにはどういう手順になるかを確認したのがこの記事です。

GKE 以外でも使えますが、私が GKE を使いたいので GKE で説明します。


環境情報

Google Cloud SDK: 182.0.0(インストール済)

Kubernetes: 1.8.4(kubectl インストール済)

Helm: 2.7.2

cert-manager: 0.2.2


準備


GKE クラスタの作成

Kubernetes クラスタを作りましょう。この記事では GKE を使って進めていきます。

クラスタ情報は何度か使うのでシェル変数にしておきます。

$ GCLOUD_PROJECT=apstndb-sandbox

$ GKE_CLUSTER=cert-manager-example
$ GKE_ZONE=us-central1-a
$ gcloud config set project ${GCLOUD_PROJECT}
$ gcloud container clusters create --zone=${GKE_ZONE} --machine-type=g1-small --disk-size=30 --num-nodes=1 ${GKE_CLUSTER} --cluster-version=1.8.4-gke.0
$ gcloud container clusters create --zone=${GKE_ZONE} --machine-type=g1-small --disk-size=30 --num-nodes=1 ${GKE_CLUSTER} --cluster-version=1.8.4-gke.0
Creating cluster cert-manager-example...done.
Created [https://container.googleapis.com/v1/projects/apstndb-sandbox/zones/us-central1-a/clusters/cert-manager-example].
kubeconfig entry generated for cert-manager-example.
NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
cert-manager-example us-central1-a 1.8.4-gke.0 35.194.38.97 g1-small 1.8.4-gke.0 1 RUNNING

kubectl コマンドを使えるようにしましょう。

$ gcloud container clusters get-credentials ${GKE_CLUSTER} --zone=${GKE_ZONE}

Fetching cluster endpoint and auth data.
kubeconfig entry generated for cert-manager-example.

$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-cert-manager-example-default-pool-45780ba0-vtmt Ready <none> 10m v1.8.4-gke.0


Helm のインストール

cert-manager の標準のデプロイ方法は Kubernetes パッケージマネージャの Helm を使うことになっています。

Installing Helm を参考に Helm クライアントをインストールしましょう。macOS の場合は次のようになります。

$ brew install kubernetes-helm

Updating Homebrew...
==> Downloading https://homebrew.bintray.com/bottles/kubernetes-helm-2.7.2.sierra.bottle.tar.gz
######################################################################## 100.0%
==> Pouring kubernetes-helm-2.7.2.sierra.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
/usr/local/etc/bash_completion.d
==> Summary
🍺 /usr/local/Cellar/kubernetes-helm/2.7.2: 50 files, 93.6MB

Kubernetes 1.8 で RBAC はステーブルになり、 GKE ではデフォルトで RBAC を使う設定になっています。Helm を RBAC 環境で使えるようにするには Kubernetes 内でも準備が必要です。ここでは単純化のために cluster-admin クラスタロールにバインドされたサービスアカウントとして tiller を作ります。

より細かく設定したい場合は Helm のドキュメントの Role-based Access Control を参照してください。

$ kubectl create serviceaccount tiller --namespace kube-system

serviceaccount "tiller" created
$ kubectl create clusterrolebinding tiller --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
clusterrolebinding "tiller" created

上で作ったサービスアカウント tiller を使う形で Helm のクラスタ内サーバの Tiller をインストールします。

$ helm init --upgrade --service-account tiller

Creating /Users/apstndb/.helm
Creating /Users/apstndb/.helm/repository
Creating /Users/apstndb/.helm/repository/cache
Creating /Users/apstndb/.helm/repository/local
Creating /Users/apstndb/.helm/plugins
Creating /Users/apstndb/.helm/starters
Creating /Users/apstndb/.helm/cache/archive
Creating /Users/apstndb/.helm/repository/repositories.yaml
Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com
Adding local repo with URL: http://127.0.0.1:8879/charts
$HELM_HOME has been configured at /Users/apstndb/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!


cert-manager のインストール

Helm を使って cert-manager をインストールします。今回はリリース済みの 0.2.2 の配布物の中の chart からインストールします。

$ curl -sL https://github.com/jetstack/cert-manager/archive/v0.2.2.tar.gz | tar xv

$ helm install --name cert-manager --namespace kube-system cert-manager-0.2.2/contrib/charts/cert-manager
NAME: cert-manager
LAST DEPLOYED: Sat Dec 9 19:09:38 2017
NAMESPACE: kube-system
STATUS: DEPLOYED

RESOURCES:
==> v1/ServiceAccount
NAME SECRETS AGE
cert-manager-cert-manager 1 1s

==> v1beta1/CustomResourceDefinition
NAME AGE
certificates.certmanager.k8s.io 1s
clusterissuers.certmanager.k8s.io 1s
issuers.certmanager.k8s.io 1s

==> v1beta1/ClusterRole
cert-manager-cert-manager 1s

==> v1beta1/ClusterRoleBinding
NAME AGE
cert-manager-cert-manager 1s

==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
cert-manager-cert-manager 1 1 1 0 1s

==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
cert-manager-cert-manager-575544b5c4-kdx4j 0/2 ContainerCreating 0 1s

これで cert-manager のインストールができました。


cert-manager を使う


Issuer と Certificate について

cert-manager は CustomResourceDefinition(以下 CRD) を使って証明書を生成する CA の設定を Issuer, 実際の証明書を Certificate としてそれぞれ管理します。

(この記事では使いませんが Namespace に属さない Issuer である ClusterIssuer というものもあります。ユースケースによっては Issuer の代わりに使うことになるでしょう。)

Issuer が Certificate とは独立した CRD になっていることで、デプロイされた cert-manager や個々の証明書とは直交して、証明書申請者のメールアドレスや Let's Encrypt のステージング、本番のエンドポイントの区別などを管理できます。これが kube-lego や kube-cert-manager に対して差別化している点です。

Issuer では ACMEの設定では ACME プロトコルに対応した CA から証明書を取得することができます。ACME は Let's Encrypt が実装しているプロトコルであり、他に有力な実装がない現時点ではほぼ Let's Encrypt サポートと同義です。

現時点で Issuer にはオレオレ認証局として動作する CA もありますが、この記事では特に説明しません。

cert-manager は ACME の HTTP-01 と DNS-01 の2種類のチャレンジに対応しています。



  • HTTP-01 チャレンジは kube-lego のように Ingress を使って動的に ACME プロバイダがアクセスするエンドポイントに返答します。


  • DNS-01 チャレンジでは DNS の TXT レコードを動的に生成します。 現在 Cloud DNS, Cloudflare, Route53 に対応しています。

この記事では DNS-01 を使うため DNS の TXT レコードを操作できる必要があります。

今回は DNS には Cloud DNS を使うため、 cert-manager が Cloud DNS を使えるように GCP のサービスアカウント設定します。(GCE デフォルトサービスアカウントで Cloud DNS スコープを有効にしても構いません)

$ gcloud iam service-accounts create cert-manager --display-name "cert-manager"

Created service account [cert-manager].

$ gcloud projects add-iam-policy-binding ${GCLOUD_PROJECT} --member serviceAccount:cert-manager@${GCLOUD_PROJECT}.iam.gserviceaccount.com --role roles/dns.admin
(省略)
- members:
- serviceAccount:cert-manager@apstndb-sandbox.iam.gserviceaccount.com
role: roles/dns.admin
(省略)

$ gcloud iam service-accounts keys create cert-manager-key.json --iam-account cert-manager@${GCLOUD_PROJECT}.iam.gserviceaccount.com

生成した cert-manager サービスアカウントの JSON キーファイルを clouddns-service-account という名前の Secret として登録します。

$ kubectl create secret generic clouddns-service-account --from-file=cert-manager-key.json=cert-manager-key.json

secret "clouddns-service-account" created

上のサービスアカウントを使って Let's Encrypt の本番環境で証明書を取得する Issuer を生成します。

$ kubectl apply -f - << EOF

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
name: letsencrypt-prod
spec:
acme:
# The ACME server URL
server: https://acme-v01.api.letsencrypt.org/directory
# Email address used for ACME registration
email: admin@apstndb.net
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-prod
# Enable the HTTP-01 challenge provider
http01: {}
# ACME dns-01 provider configurations
dns01:
# Here we define a list of DNS-01 providers that can solve DNS challenges
providers:
- name: prod-dns
clouddns:
# A secretKeyRef to a the google cloud json service account
serviceAccountSecretRef:
name: clouddns-service-account
key: cert-manager-key.json
# The project in which to update the DNS zone
project: apstndb-sandbox
EOF
issuer "letsencrypt-prod" created

上の Issuer を使って cert-manager-tls という名前の Secret として sandbox.apstndb.net の証明書を管理するための CRD である Certificate を作ります。

$ kubectl apply -f - <<EOF

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: sandbox-apstndb-net
namespace: default
spec:
secretName: cert-manager-tls
issuerRef:
name: letsencrypt-prod
commonName: sandbox.apstndb.net
dnsNames:
- www.sandbox.apstndb.net
acme:
config:
- dns01:
provider: prod-dns
domains:
- sandbox.apstndb.net
- www.sandbox.apstndb.net
EOF
certificate "sandbox-apstndb-net" created

作った Certificate を describe すると証明書の取得状況が確認できます。

% kubectl describe certificate

Name: sandbox-apstndb-net
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"certmanager.k8s.io/v1alpha1","kind":"Certificate","metadata":{"annotations":{},"name":"sandbox-apstndb-net","namespace":"default"},"spec...
API Version: certmanager.k8s.io/v1alpha1
Kind: Certificate
Metadata:
Cluster Name:
Creation Timestamp: 2017-12-09T12:34:09Z
Deletion Grace Period Seconds: <nil>
Deletion Timestamp: <nil>
Resource Version: 21646
Self Link: /apis/certmanager.k8s.io/v1alpha1/namespaces/default/certificates/sandbox-apstndb-net
UID: 40d55c08-dcdd-11e7-a59b-42010a8000fc
Spec:
Acme:
Config:
Dns 01:
Provider: prod-dns
Domains:
sandbox.apstndb.net
www.sandbox.apstndb.net
Common Name: sandbox.apstndb.net
Dns Names:
www.sandbox.apstndb.net
Issuer Ref:
Name: letsencrypt-prod
Secret Name: cert-manager-tls
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ErrorCheckCertificate 41s cert-manager-controller Error checking existing TLS certificate: secret "cert-manager-tls" not found
Normal PrepareCertificate 41s cert-manager-controller Preparing certificate with issuer
Normal PresentChallenge 41s cert-manager-controller Presenting dns-01 challenge for domain www.sandbox.apstndb.net
Normal PresentChallenge 41s cert-manager-controller Presenting dns-01 challenge for domain sandbox.apstndb.net
Normal SelfCheck 39s cert-manager-controller Performing self-check for domain www.sandbox.apstndb.net
Normal SelfCheck 39s cert-manager-controller Performing self-check for domain sandbox.apstndb.net

取得できた証明書の中身を確認してみましょう。(kubectl の jsonpath がいまいち使いやすくないので jq を使っています。)

$ kubectl get secret cert-manager-tls -o json | jq -r '.data["tls.crt"]' | base64 -D | openssl x509 -text

Certificate:
Data:
Version: 3 (0x2)
Serial Number:
03:9f:5a:76:64:a3:f5:a7:79:25:8c:c7:20:71:91:f1:14:27
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
Validity
Not Before: Dec 9 11:35:48 2017 GMT
Not After : Mar 9 11:35:48 2018 GMT
Subject: CN=sandbox.apstndb.net


証明書の使用

実際に証明書を Ingress で使用して HTTPS でアクセスしてみましょう。

アクセス対象の Pod 群としての Deployment と NodePort Service を作成します。

$ kubectl run nginx --image nginx --port 80

kubectl get deploymentdeployment "nginx" created
$ kubectl expose deployment nginx --type NodePort
service "nginx" exposed

A レコードを設定する対象の静的 IP アドレスを用意してます。今回は GCE Ingress を使うのでグローバルである必要があります。

$ gcloud compute addresses create nginx-ip --global

Created [https://www.googleapis.com/compute/v1/projects/apstndb-sandbox/global/addresses/nginx-ip].

上で作った静的 IP アドレスに対して DNS で A レコードを設定しておきます。既に Cloud DNS に sandbox というゾーンが登録されていて、そのゾーンの中にある sandbox.apstndb.net に設定する場合はこんな感じです。

$ CLOUDDNS_ZONE=sandbox

$ STATIC_IP_ADDRESS=$(gcloud compute addresses describe nginx-ip --global --format='get(address)')
$ gcloud dns record-sets transaction start --zone=${CLOUDDNS_ZONE}
Transaction started [transaction.yaml].
$ gcloud dns record-sets transaction add --zone=${CLOUDDNS_ZONE} --name sandbox.apstndb.net. --ttl 500 --type A "${STATIC_IP_ADDRESS}"
Record addition appended to transaction at [transaction.yaml].
$ gcloud dns record-sets transaction execute --zone=${CLOUDDNS_ZONE}
Executed transaction [transaction.yaml] for managed-zone [sandbox].
Created [https://www.googleapis.com/dns/v1/projects/apstndb-sandbox/managedZones/sandbox/changes/9].
ID START_TIME STATUS
9 2017-12-09T12:50:32.611Z pending

これまでに作成した静的 IP アドレスと証明書を使用する Ingress を作ります。

kubectl apply -f - << EOF

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx
annotations:
kubernetes.io/ingress.global-static-ip-name: "nginx-ip"
kubernetes.io/ingress.class: "gce"
spec:
tls:
- secretName: cert-manager-tls
hosts:
- sandbox.apstndb.net
backend:
serviceName: nginx
servicePort: 80
EOF

$ ingress "nginx" created

Ingress に設定した IP アドレスが割り当てられるのを待ちます。

% kubectl get ingress --watch

NAME HOSTS ADDRESS PORTS AGE
nginx * 35.227.233.236 80, 443 1m

openssl コマンドで読んでみると、正しく証明書が外から取得できたことがわかります。

$ openssl s_client -connect sandbox.apstndb.net:443

CONNECTED(00000003)
depth=1 /C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
verify error:num=20:unable to get local issuer certificate
verify return:0
---
Certificate chain
0 s:/CN=sandbox.apstndb.net
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
(省略)

ブラウザからも問題なく HTTPS でアクセスできるようになったことが確認できます。GCE HTTPS Load Balancing はデフォルトで HTTP/2 を提供するので HTTP/2 indicatator も青くなっています。

スクリーンショット 2017-12-09 22.03.16.png


後片付け

課金されるものを削除しておきましょう。GCE Ingress を使った場合は GKE クラスタを削除する前に Ingress を削除して GCE 上も削除されていることを確認しましょう。GCE 静的 IP アドレスも放っておくと課金されるので削除されたことを確認しましょう。

$ kubectl delete ingress nginx

$ gcloud container clusters delete cert-manager-example --zone=GKE_ZONE
$ gcloud compute addresses delete nginx-ip --global


まとめ

cert-manager で TLS 証明書を取得するところまでを確認しました。

cert-manager は今後本番利用に耐える実装を目指しており、今後も機能が追加されていくことが期待できます。

この記事では実際に証明書を使うための DNS の A レコードの更新は自前で行ったため Cloud DNS 依存の記述が多くなっていましたが、今後は DNS の A レコードも含めて自律的に管理できるようなものが登場することに期待したいですね。


私信

特に Kubernetes が活発な会社で働いているわけではないですが CKA を取りました。来年もヲチしていきます。

Kobito.kva7wi.png