Kubernetes cert-managerでRoute53で作成したドメインの証明書を作成し、Ingressで利用する。

# はじめに

無料でKubernetes IngressでTLS化するためのメモです。

無料と言いつつ、自分は以前AWSのRoute53で購入したパブリックドメインがあるので、ドメインを持っている前提となりますのでご了承ください。

[前回の記事](https://qiita.com/jlandowner/items/466afb3fa90b2e776200)でセットアップしたGKEでHTTPSのIngressを使えるようにしたいと思います。
ALBなどを利用するのであれば、ACMで無料で証明書を作れるのですが、個人利用のためロードバランサーの追加費用は払えません。。
またGKEについても、Network Load Balancerのコストは払えません。。

そのため、今回はGKEにCert-Managerをデプロイし、Let's Encryptから証明書を発行し、Ingress ControllerのNodePortを外部公開してTLS通信を実現します。

# 環境
[前回](https://qiita.com/jlandowner/items/466afb3fa90b2e776200)構築した環境です。

- 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のインストール
https://cert-manager.io/docs/installation/


# Ingress Controller
## Contourのインストール

なんでも良いので、今回はContourを使用します。
こちらもインストールは簡単ですが、自分はGCPのNetwork Load Balancerを使いたくないので、少し変更します。

https://projectcontour.io/getting-started/

```bash
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
```

Daemonsetで配置されるEnvoyのServiceが`type: Loadbalancer`なので、外部Load Balancerを使う前提になっています。

今回は外部ロードバランサーは使わないので、`type: NodePort`に変更し、NodePortを固定化するため80 -> 30080, 443 -> 30443 にしました。

```bash:Serviceの編集
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

最終的にこんな感じでインストール完了です。

```bash
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に）

```bash:secret作成
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を作成します。

```yaml:letsencrypte-issuer.yaml
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してステータスが問題なさそうなら大丈夫だと思います。

```bash:describe
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つの方法があります。
1. cert-managerのIngress自動証明書作成機能を使用して、Certificateリソースを自動作成する。
1. 手動でCertificateリソースを作成し、作成された証明書シークレットをIngressから参照する。

### Ingress自動証明書作成機能
Ingressに特定のアノテーションを付与することで、cert-managerがIngressの内容から自動で証明書を作成してくれます。

https://cert-manager.io/docs/usage/ingress/

```yaml:ingress.yaml
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リソースを自前で作成します。

```yaml:certificate.yaml
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以降のリソースはしばらくすると削除されていなくなります。

```bash:describe
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でこれを指定します。

```bash
$ 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を作成するだけです。

```yaml:ingress.yaml
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という謎構成でしたが、少しでもご参考になれば幸いです。