GKE でマネージド証明書を用いた https 通信を設定する

この記事は [オプトテクノロジーズアドベントカレンダー(2018)](https://qiita.com/advent-calendar/2018/opt-technologies) 5 日目のエントリーです。

## 前書き

[以前](https://cloud.google.com/blog/products/gcp/introducing-managed-ssl-for-google-app-engine)から Google App Engine ではマネージドな証明書[^managed-cert]を用いた https 通信の設定が可能になっていました。しかし、ロードバランサーレベルで TLS 終端をして任意のバックエンドにその証明書を使うといった、 [AWS Certificate Manager(ACM)](https://aws.amazon.com/jp/certificate-manager/)のような使い方はできませんでした。

[^managed-cert]: ドキュメントでは `Google-managed SSL certificates` という言い方をしているが SSL という言葉を使いたくないのでこのように表記している

ところが、突如として任意のバックエンドに使えるマネージド証明書がベータとして提供されたのです。それが、以下のリンクに記載されているものです。

https://cloud.google.com/load-balancing/docs/ssl-certificates

この記事ではこの証明書を使って GKE の Ingress を https でアクセスできるようにしていきます。


## 設定方法

実は単に設定する方法の紹介という意味では早い段階で書いている方[^prior-art]がいます。  
この方法でも設定はできるものの、GKE に任せたいところも全て手で設定しているのでなかなか大変です。ここではもう少し楽な方法(現状動くやり方)と、将来的に実装されるであろうもっと楽な方法(予想)を書きます。

[^prior-art]: [How to use Google Managed SSL Certificates on GKE – Collaborizm Blog](https://blog.collaborizm.com/how-to-use-google-managed-ssl-certificates-on-gke-7a56fc514ef8)


### もう少し楽な方法(現状動くやり方)

まずは `gcloud` コマンドを最新の状態にします。

```bash
$ gcloud components update
```

検証用のクラスタを作成し、 `kubectl` で選択されているコンテキストが作成したクラスタのものになっているか確認します。

```bash
$ gcloud container clusters create https-demo-cluster \
  --zone asia-northeast1-a \
  --cluster-version 1.11.3-gke.18 \
  --machine-type g1-small \
  --num-nodes 1
NAME                LOCATION           MASTER_VERSION  MASTER_IP      MACHINE_TYPE  NODE_VERSION   NUM_NODES  STATUS
https-demo-cluster  asia-northeast1-a  1.11.3-gke.18   35.XX.XX.XX    g1-small      1.11.3-gke.18  1          RUNNING
$ kubectl config current-context
gke_${YOUR_PROJECT_NAME}_asia-northeast1_https-demo-cluster
```

ドメインに紐づけるためのグローバルな(ロードバランサーに使える)静的 IP を払い出します。
静的な IP は使っていない間(STATUS が RESERVED の時)費用が発生するのでつかわなくなったら削除しましょう。

```bash
$ gcloud compute addresses create https-demo-ip --global
Created [https://www.googleapis.com/compute/v1/projects/${YOUR_PROJECT_NAME}/global/addresses/https-demo-ip].
$ gcloud compute addresses list
NAME             REGION    ADDRESS       STATUS
https-demo-ip              35.XX.XX.XX   RESERVED 
```

無事にアドレスが払い出されたら、このアドレスをドメインの A レコードに設定します。
この項目に関してはよしなにしてください。参考までに Cloud DNS での設定画面はこんな感じになります。

![Cloud DNS で A レコードを設定する画面](https://i.gyazo.com/7ec956606582b2b06bf0f09ec18d4e5d.png)


さて、いよいよマネージド証明書を発行します。発行直後は PROVISIONING ですが、もしかすると FAILED_NOT_VISIBLE になっているかもしれません。今は無視して進みましょう。

```bash
$ gcloud beta compute ssl-certificates create https-demo-cert \
    --domains ${YOUR_DOMAIN}
$ gcloud beta compute ssl-certificates list
NAME             TYPE     CREATION_TIMESTAMP             EXPIRE_TIME                    MANAGED_STATUS
https-demo-cert  MANAGED  2018-12-04T07:33:38.873-08:00                                 PROVISIONING
    ${YOUR_DOMAIN}: PROVISIONING
```

Deployment, Service に関しては差分が発生しないはずなのでサクッと作ってしまいます。(そして、先駆者の yaml ほぼそのままです)

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: https-demo
  name: https-demo
spec:
  selector:
    matchLabels:
      run: https-demo
  template:
    metadata:
      labels:
        run: https-demo
    spec:
      containers:
      - image: k8s.gcr.io/serve_hostname:v1.4
        name: hostname
        ports:
        - containerPort: 9376
          protocol: TCP
```

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-svc
spec:
  type: NodePort
  selector:
    run: https-demo
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
```

```bash
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
```

重要なのは Ingress の annotaions です。先ほどまでに作成した静的 IP とマネージド証明書のリソース名を正確に設定する必要があります。

```yaml:ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: https-demo
  annotations:
    kubernetes.io/ingress.global-static-ip-name: https-demo-ip
    ingress.gcp.kubernetes.io/pre-shared-cert: https-demo-cert
spec:
  rules:
  - host: ${YOUR_DOMAIN}
    http:
      paths:
      - backend:
          serviceName: demo-svc
          servicePort: 80
```

```
$ kubectl apply -f ingress.yaml
```

しれっと登場した `ingress.gcp.kubernetes.io/pre-shared-cert`[^pre-shared-cert-doc] というアノーテーションが事前に用意した証明書をロードバランサーに使わせる設定です。

[^pre-shared-cert-doc]: https://github.com/kubernetes/ingress-gce/blob/63bd8b9e8a7e0013a50ef728aabf9e5076d5ae65/docs/annotations.md#use-gcp-ssl-certificate

![Ingress を作成した直後](https://i.gyazo.com/6076bfb8f285055b2937fd5077b9f19f.png)

Ingress を作成したあとしばらく経つと(数分はかかる)証明書のステータスが ACTIVE になり設定したドメインで https 通信できるようになるはずです。

もし、うまくいかない場合はドキュメントに書いてあることがきちんと満たされているか確認してみてください。

> For the certificate provisioning process to proceed, all of the following conditions must be met:
> - The DNS records for your domain must reference the IP address of your load balancer's target proxy,
> - Your target proxy must reference the Google-managed certificate resource.
> - Your load balancer configuration must be complete, including the creation of a forwarding rule.
> https://cloud.google.com/load-balancing/docs/ssl-certificates#certificate-resource-status 

#### 謎

実は `ingress.gcp.kubernetes.io/pre-shared-cert`　はカンマ区切りで複数の証明書を指定できる仕様なので複数の証明書を作成してそれぞれ別のドメインの通信に使えるはず、だったんです。

https://cloud.google.com/load-balancing/docs/ssl-certificates#multiple-example

![ドキュメントの図のキャプチャ](https://i.gyazo.com/82fa5e00ed73c495b9f2fc2e8fd598f4.png)

手元で試し分には Ingress に同時に複数の証明書を設定して、対象のドメインもバラバラだとうまく証明書のプロビジョニングが進みませんでした。
単一ドメイン単一証明書で通信できるようにした後に、設定を足す分にはコンソール上はうまくいっているように見えますが結局通信はできないという結果になりました。
これがどこ起因の問題なのかは微妙なのですが、複数設定したい場合は現状 Ingress を分けています。

### もっと楽な方法(現状は動かない)

`ingress.gcp.kubernetes.io/pre-shared-cert` 周りを `ingress-gce` のリポジトリで漁っている際に[不思議なもの](https://github.com/kubernetes/ingress-gce/blob/v1.4.0/pkg/annotations/ingress.go#L68-L72)を見つけました。

```go
// ManagedCertificates represents the specific ManagedCertificate resources for
// the Ingress controller to use to terminate SSL. The controller *does not*
// manage ManagedCertificate resources, it is the user's responsibility to
// create/delete them.
ManagedCertificates = "gke.googleapis.com/managed-certificates"
```

そして、こんなリポジトリがあります。

https://github.com/GoogleCloudPlatform/gke-managed-certs

このリポジトリで開発されている CRD そのものはすでに動くようになっており、将来的にはこの CRD で作成した `ManagedCertificate` リソースの名前を `gke.googleapis.com/managed-certificates` に指定するだけで、 https の設定が完了するようになるかもしれません。

現状この機能は GKE のマスターで ingress controller が起動するときにフラグとして渡せば有効になる状態のようで、手軽に動作を確認する方法はなさそうだったので割と憶測混じりになっています。(コードは眺めたりしました)