はじめに
無料でKubernetes IngressでTLS化するためのメモです。
無料と言いつつ、自分は以前AWSのRoute53で購入したパブリックドメインがあるので、ドメインを持っている前提となりますのでご了承ください。
前回の記事でセットアップしたGKEでHTTPSのIngressを使えるようにしたいと思います。
ALBなどを利用するのであれば、ACMで無料で証明書を作れるのですが、個人利用のためロードバランサーの追加費用は払えません。。
またGKEについても、Network Load Balancerのコストは払えません。。
そのため、今回はGKEにCert-Managerをデプロイし、Let's Encryptから証明書を発行し、Ingress ControllerのNodePortを外部公開してTLS通信を実現します。
環境
前回構築した環境です。
-
Kubernetes:
- Master: GKE v1.15.3
- Worker: e2-small(preemptive instance) × 1 Node
-
Domain:
- AWS Route53 Domainで取得
- 取得したドメイン(例:example.com)のRoute53 hostedzoneがある。
- example.comのhostedzoneに、上記のWorkerのIPがAレコードで設定されている。
cert-manager
kubernetes上で証明書の発行、更新、管理なんかをしてくれます。https://cert-manager.io/
cert-managerのインストール
Ingress Controller
Contourのインストール
なんでも良いので、今回はContourを使用します。
こちらもインストールは簡単ですが、自分はGCPのNetwork Load Balancerを使いたくないので、少し変更します。
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
Daemonsetで配置されるEnvoyのServiceがtype: Loadbalancer
なので、外部Load Balancerを使う前提になっています。
今回は外部ロードバランサーは使わないので、type: NodePort
に変更し、NodePortを固定化するため80 -> 30080, 443 -> 30443 にしました。
kubectl edit service/envoy -n projectcontour
...
ports:
type: NodePort # <- NodePortに変更
- name: http
nodePort: 30080 # <- 追加
port: 80
protocol: TCP
targetPort: 80
- name: https
nodePort: 30443 # <- 追加
port: 443
protocol: TCP
targetPort: 443
...
hostPortで80と443を開けても良かったのですが、バックエンドのアプリしか動かさない予定なので、特にポートにこだわりはありません。
Ingress自体にCluster外からどうやって到達するかについては、ingress-nginxに様々なパターンを紹介してくれているドキュメントがあります。
Bare-metal considerations
https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#via-the-host-network
最終的にこんな感じでインストール完了です。
kubectl get all -n projectcontour
NAME READY STATUS RESTARTS AGE
pod/contour-5f5bf9cd8c-r2bwf 1/1 Running 6 21h
pod/contour-certgen-f5hd4 0/1 Completed 0 21h
pod/envoy-j6fh6 2/2 Running 0 6h19m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/contour ClusterIP 10.0.13.41 <none> 8001/TCP 21h
service/envoy NodePort 10.0.3.136 <none> 80:30080/TCP,443:30443/TCP 21h
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/envoy 1 1 1 1 1 <none> 21h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/contour 1/1 1 1 21h
NAME DESIRED CURRENT READY AGE
replicaset.apps/contour-5f5bf9cd8c 1 1 1 21h
NAME COMPLETIONS DURATION AGE
job.batch/contour-certgen 1/1 5s 21h
issuerの作成
Route53にLet's EncryptでDNS検証で証明書を作成します。
公式ドキュメントに沿ってRoute53にアクセスするためのIAMを作成します。
Configuration / ACME / DNS01 / Route53
https://cert-manager.io/docs/configuration/acme/dns01/route53/
普通はk8s on AWSやEKSで利用すると思うので、IAM Roleを使えばいいと思いますが、
今回はGKEから使うので、IAM Userに上記で作成したポリシーをアタッチして、アクセスキーとシークレットをCluster内に置いちゃいます。
シークレットアクセスキーのsecretを作成します。(アクセスキーは後述するissuerのyamlに)
kubectl -n cert-manager create secret generic prod-route53-credentials-secret --from-literal=secret-access-key=<SECRET_ACCESS_KEY>
IssuerやClusterIssuerというCRDができているので、Let's Encryptのissuerを作成します。
Cluster全体で使うので、今回はClusterIssuerを作成します。
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
spec:
acme:
# まずはstagingで試して、問題なく証明書ができたらprodで再実行
#server: https://acme-v02.api.letsencrypt.org/directory # prod
server: https://acme-staging-v02.api.letsencrypt.org/directory # staging
email: <YOUR_MAIL>
privateKeySecretRef:
name: letsencrypt-issuer
solvers:
- selector:
dnsZones:
- "example.com" // Route53 Domainsで取得したドメイン
dns01:
route53:
region: ap-northeast-1
accessKeyID: <ACCESS_KEY> // アクセスキー
secretAccessKeySecretRef:
name: prod-route53-credentials-secret
key: secret-access-key
applyしてステータスが問題なさそうなら大丈夫だと思います。
kubectl describe clusterissuer
...
Status:
Acme:
Last Registered Email: <YOUR_MAIL>
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/xxxxxxxx
Conditions:
Last Transition Time: 2020-04-21T05:28:09Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
証明書の作成
以下の2つの方法があります。
- cert-managerのIngress自動証明書作成機能を使用して、Certificateリソースを自動作成する。
- 手動でCertificateリソースを作成し、作成された証明書シークレットをIngressから参照する。
Ingress自動証明書作成機能
Ingressに特定のアノテーションを付与することで、cert-managerがIngressの内容から自動で証明書を作成してくれます。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-issuer # <- 大事なのはここ
spec:
ingressClassName: contour
tls:
- hosts:
- "*.example.com" # <- この名前の証明書が作成されます。ワイルドカード指定も可能
secretName: example-com-tls # <- 存在しない名前ならなんでもいいです。この名前の証明書シークレットが作成されます。
rules:
- host: app.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: example-app
port: 8080
手動でCertificateリソースを作成
基本的には上記の方法で自動で作成するのがいいと思いますが、手動で作成してみると勉強になると思います。
Certificateリソースを自前で作成します。
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com
namespace: <YOUR_NAMESPACES>
spec:
secretName: example-com-tls
duration: 2160h # 90d
renewBefore: 360h # 15d
organization:
- example
commonName: "*.example.com" // ワイルドカード証明書
isCA: false
keySize: 2048
keyAlgorithm: rsa
keyEncoding: pkcs1
usages:
- server auth
- client auth
dnsNames:
- example.com // Route53に登録した
- "*.example.com" // commonNameに指定した名前がdnsNamesにないと失敗します。
# Issuer references are always required.
issuerRef:
name: letsencrypt-issuer
kind: ClusterIssuer
applyしてしばらく待ちます。Certificateリソースを作成すると
Certificate
-> CertificateRequest
-> Order
-> Challenge
の順にリソースが作成されていきます。
注意点としては、エラーの場合も、ずっとCertificateのステータスがInProgress
のままになっていることです。(リトライしようとしているっぽい)
email検証だから時間がかかると思いつつも、おかしいと思って上記のリソースを順番にDescribeして見てみると、Challengeなどで失敗していることがありました。
しばらく経ってもCertificateがReady
にならない場合は、CertificateRequest以降が失敗している可能性があります。どれかのリソースのDescribeしてEventのところにエラーが出ているはずです。
トラブルシューティングに調べ方の記載がありますが、InProgress
だとエラーになっているかわからないです。
Troubleshooting Issuing ACME Certificates
https://cert-manager.io/docs/faq/acme/
問題なくCertificateができると、大丈夫そうなステータスになります。
またCertificateRequest以降のリソースはしばらくすると削除されていなくなります。
kubectl describe certificate -A
...
Status:
Conditions:
Last Transition Time: 2020-04-22T05:41:38Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-07-21T04:41:38Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 32m cert-manager Generated a new private key
Normal Requested 32m cert-manager Created new CertificateRequest resource "example-tls-2084312343"
Normal Issued 30m cert-manager Certificate issued successfully
作成が完了すると、Certificateと同じNamespace内にSecretが作成されています。
Ingressでこれを指定します。
$ kubectl describe secret example-com-tls
Name: example-com-tls
Namespace: <YOUR_NAMESPACE>
Labels: <none>
Annotations: cert-manager.io/alt-names: *.example.com,example.com
cert-manager.io/certificate-name: example-com
cert-manager.io/common-name: *.example.com
cert-manager.io/ip-sans:
cert-manager.io/issuer-kind: ClusterIssuer
cert-manager.io/issuer-name: letsencrypt-issuer
cert-manager.io/uri-sans:
Type: kubernetes.io/tls
Data
====
tls.key: 1675 bytes
ca.crt: 0 bytes
tls.crt: 3574 bytes
Ingressで証明書シークレットを利用する。
あとはIngressを作成するだけです。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-app
spec:
ingressClassName: contour
tls:
- hosts:
- "*.example.com"
secretName: example-com-tls # <- 先ほど確認した証明書シークレットの名前
rules:
- host: example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: example-app
port: 8080
最後に
最後に忘れてはいけないファイアウォールルールの設定です。
NodeのVMインスタンスのファイアウォールルールで先ほど公開したNodePortを開きます。
これで、https://example.com:30443 で繋がるようになると思います。
GKEとRoute53という謎構成でしたが、少しでもご参考になれば幸いです。