Cloud run for Anthosでカスタムドメイン宛にHTTPSリクエストを受信する構成

#はじめに
Cloud run for Anthosは、KnativeのManaged Serviceです。Cloud run for Anthosを使って、Clientから、HTTPSでカスタムドメイン宛にHTTPリクエストを受信する構成を組んでみたいと思います。

マニュアルにある概略図だと以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/be3b584a-c73c-4adf-0259-5a2eb376be60.png)

細かく描くと、以下のような構成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/3eb6a70e-641e-6f15-a349-3edb8f728a00.png)

手順は、以下のマニュアルの内容を組み合わせて実施します。

Cloud Run for Anthos on Google Cloud を使用した HTTP(S) 負荷分散の統合
https://cloud.google.com/solutions/integrating-https-load-balancing-with-istio-and-cloud-run-for-anthos-deployed-on-gke

外部 HTTP(S) 負荷分散での Ingress
https://cloud.google.com/kubernetes-engine/docs/concepts/ingress-xlb#setting_up_https_tls_between_client_and_load_balancer

Ingress による HTTP(S) 負荷分散における複数の SSL 証明書の使用
https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-multi-ssl

カスタム ドメインのマッピング
https://cloud.google.com/run/docs/mapping-custom-domains

#手順の流れ
Cloud Run for Anthos を有効にしてGKE clusterを作成します。
Cloud Run for AnthosがExternal通信の受け口として使用している、IstioのIngress gatewayの前段にIngress（External HTTPS Load Balancing）を作成します。
其の際、HTTPS通信を行うために、証明書を作成し、SecretとしてIngressに設定します。
KnativeのServiceをデプロイします。
デプロイしたKnativeのServiceにカスタムドメインでアクセスするために、カスタム ドメインのマッピングを行います。

#構成
PublicEndpointを持つ、Private clusterで構築します。
今回は、受け付けるリクエストをHTTPSに限定します。

#注意事項
マニュアルにも記載がありますが、今回のIsitoに対する変更は、GKE 用の Cloud Run アドオンのアップデートプロセスで削除される恐れがあります。

>
注: GKE 用の Cloud Run アドオンを使用する場合は、アドオンによってインストールされる Cloud Run コントロール プレーン コンポーネントへのアップグレードは、GKE マスター アップグレード プロセスで管理されます。アップグレード プロセスで、コントロール プレーン コンポーネントに適用された変更を削除できます。その場合は、このセクションの手順を繰り返して、アップグレード後に変更を再度適用します。また、GKE アドオンを使用せずに、オープンソースの Istio コンポーネントや Knative Serving コンポーネントをインストールすることもできます。その場合は、Istio と Knative Serving のアップデートをインストールする必要があります。

#手順
基本的にマニュアルに沿って構築していきます。
https://cloud.google.com/solutions/integrating-https-load-balancing-with-istio-and-cloud-run-for-anthos-deployed-on-gke

##GKEのClusterを作成します
パブリック エンドポイントへ無制限にアクセス可能な限定公開クラスタの作成
https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#endpoints
環境に合わせて、環境変数を設定してください。

```
CLUSTER=cloud-run
ZONE=asia-northeast1-a
SUBNET=mysubnet
NETWORK=myvpc

gcloud beta container clusters create $CLUSTER \
    --addons HorizontalPodAutoscaling,HttpLoadBalancing,CloudRun \
    --enable-ip-alias \
    --enable-stackdriver-kubernetes \
    --machine-type n1-standard-2 \
    --zone $ZONE \
    --network $NETWORK \
    --subnetwork=$SUBNET \
    --enable-private-nodes \
    --master-ipv4-cidr=172.16.16.0/28 \
    --no-enable-master-authorized-networks
```

GKE clusterにログインします。

```
gcloud container clusters get-credentials $CLUSTER --zone $ZONE
```

##Istio ingress gatewayに対して、Ingress(External HTTPS Load Balancing)から実施するヘルスチェックリクエストに、Istio ingress gatewayが応答する用に設定します
こちらは、マニュアルの記載通りになります。

```
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: health
  namespace: knative-serving
spec:
  gateways:
  - gke-system-gateway
  hosts:
  - "*"
  http:
  - match:
    - headers:
        user-agent:
          prefix: GoogleHC
      method:
        exact: GET
      uri:
        exact: /
    rewrite:
      authority: istio-ingress.gke-system.svc.cluster.local:15020
      uri: /healthz/ready
    route:
    - destination:
        host: istio-ingress.gke-system.svc.cluster.local
        port:
          number: 15020
EOF
```

##Kubernetes Ingress で使用する Istio Ingress Gateway に修正を加える
Cloud Run for Anthosを有効にして、GKE Clusterを作成すると、IstioのService meshが自動的に構成されます。
Knative(＝Cloud Run for Anthos）は、内部的にIsitoのService meshを使用しているためです。
Cloud Run for Anthosを有効にした段階では、IstioのService meshで使用される、Istio ingress gatewayのServiceは、Type: LoadBalancerで作成されます。つまり、External HTTP(S) Load Balancing（Ingress）ではなく、External TCP/UDP Network Load Balancingが作成されています。
今回は、External HTTP(S) Load Balancing（Ingress）を使用するため、Istio Ingress GatewayのServiceのタイプを、Type: LoadBalancerからType: NodePortに変更します。

```
cat <<EOF > istio-ingress-patch.json
[
  {
    "op": "replace",
    "path": "/spec/type",
    "value": "NodePort"
  },
  {
    "op": "remove",
    "path": "/status"
  }
]
EOF

kubectl -n gke-system patch svc istio-ingress \
    --type=json -p="$(cat istio-ingress-patch.json)" \
    --dry-run=true -o yaml | kubectl apply -f -
```

また、Instance groupを使ったIngressではなく、Negを使ったコンテナ ネイティブの負荷分散を構成します。
https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing?hl=ja
コンテナ ネイティブの負荷分散を使用すると、いわゆる2Hop問題を回避することができ、ロードバランサは Kubernetes Pod を直接ターゲットにすることができ、またトラフィックを Pod に均等に分配できます。

```
kubectl annotate svc istio-ingress -n gke-system cloud.google.com/neg='{"exposed_ports": {"80":{}}}'
```

##Ingressを作成します
今回は、HTTPSを使用したいので、マニュアルのYamlファイルに、HTTPSの設定を追加する必要があります。
まずは、以下のマニュアルを参考に証明書と鍵を作成します。
https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-multi-ssl?hl=ja

```
DOMAIN=<ドメイン名>
openssl genrsa -out test-ingress-1.key 2048
openssl req -new -key test-ingress-1.key -out test-ingress-1.csr -subj "/CN=$DOMAIN"
openssl x509 -req -days 365 -in test-ingress-1.csr -signkey test-ingress-1.key -out test-ingress-1.crt
```

作成した証明書と鍵をSecretに格納します。IngressとIstio ingress gatewayと同じく、gke-systemネームスペースに作成します。

```
kubectl create secret tls secret-cert --cert test-ingress-1.crt --key test-ingress-1.key -n gke-system
```

前準備が終わったので、Ingressを作成します。
HTTPSのみ受け付けるために、annotationを設定します。また、HTTPS通信のために先程の作成したSecretを指定します。


```
kubectl apply -f - <<EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
  namespace: gke-system
  annotations:
    kubernetes.io/ingress.allow-http: "false"
spec:
  tls:
  - secretName: secret-cert
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: istio-ingress
          servicePort: 80
EOF
```

Ingress（External HTTP(S) Load Balancing）の作成状況を確認します。my-ingress ADDRESS が IP アドレスに変わるまで待ちます。

```
kubectl get ingress my-ingress -n gke-system --watch
```

しばらく待っても作成が終わらない場合は、describeコマンドでエラーが発生しているかどうか確認できます。

```
kubectl describe ingress my-ingress -n gke-system
```

##テスト用のCloud Run for Anthosアプリケーションをデプロイします

```
gcloud run deploy my-service \
    --namespace default \
    --image gcr.io/knative-samples/simple-api \
    --platform gke \
    --cluster $CLUSTER \
    --cluster-location $ZONE
```

>
Kubernetes Ingress オブジェクトの作成後に HTTP(S) 負荷分散がルーティングできるようになるまで数分かかる場合があります。その間に、クラスタ内のサービスにリクエストを送信すると、HTTP/1.1 404 Not Found や HTTP/1.1 502 Bad Gateway などのエラーが発生することがあります。

##カスタム ドメインのマッピングを実施します
Cloud Run fully managedの場合は、前提として、使用するドメインについて、ドメイン所有者であることを証明する必要があります。Cloud Run for Anthosの場合は不要です。
以下のマニュアルに沿って実施します。
https://support.google.com/a/answer/183895

証明できると、gcloud domains list-user-verifiedコマンドで確認できます。

```
$ gcloud domains list-user-verified
ID
xxx.com
```

カスタム ドメインのマッピングを実施します。
https://cloud.google.com/run/docs/mapping-custom-domains

カスタム ドメインのマッピングを実施すると、DNSレコードに登録するAレコードの情報が提供されるのですが、現状Type: Loadbalancer構成でしか、Cloud RunのCRDのDomainMappingが作成できないので、一旦、NodePortからLoadBalancerに設定を戻します。

現状は、NodePortになっています。

```
$ k get svc istio-ingress -n gke-system
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                                      AGE
istio-ingress   NodePort   10.196.0.174   <none>        15020:30107/TCP,80:30122/TCP,443:32565/TCP   86m
```

再度、パッチを当てます。

```
cat <<EOF > istio-ingress-patch.json
[
  {
    "op": "replace",
    "path": "/spec/type",
    "value": "LoadBalancer"
  },
  {
    "op": "remove",
    "path": "/status"
  }
]
EOF

kubectl -n gke-system patch svc istio-ingress \
    --type=json -p="$(cat istio-ingress-patch.json)" \
    --dry-run=true -o yaml | kubectl apply -f -
```

External TCP/UDP Network Load Balancingが作成され、External IPが作成されました。

```
$ k get svc istio-ingress -n gke-system
NAME            TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)                                      AGE
istio-ingress   LoadBalancer   10.196.0.174   35.187.199.153   15020:30107/TCP,80:30122/TCP,443:32565/TCP   91m
```

ちなみに、NodePortになっていて、istio-ingressにExternal IPがない場合は、以下の画面のままになりカスタムドメインマッピングが完了しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/f514583e-9794-8f02-16c5-8e6cc851640f.png)

kubectl get domainMappingコマンドで確認すると、Gateway IPの取得待ちになっています。"message: Waiting for Gateway IP"

```
$ kubectl get domainMapping my-service.<ドメイン名> -o yaml
apiVersion: domains.cloudrun.com/v1alpha1
kind: DomainMapping
metadata:
  creationTimestamp: "2020-08-29T06:52:45Z"
  generation: 1
  name: my-service.<ドメイン名>
  namespace: default
  resourceVersion: "260556"
  selfLink: /apis/domains.cloudrun.com/v1alpha1/namespaces/default/domainmappings/my-service.<ドメイン名>
  uid: 025ea6de-521d-4743-83a2-fecf333f837a
spec:
  certificateMode: AUTOMATIC
  routeName: my-service
status:
  conditions:
  - lastTransitionTime: "2020-08-29T06:52:45Z"
    message: Certificate is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: CertificateProvisioned
  - lastTransitionTime: "2020-08-29T06:52:45Z"
    message: DomainMapping specific Gateway is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: DomainMappingGatewayReady
  - lastTransitionTime: "2020-08-29T06:52:45Z"
    message: Waiting for Gateway IP
    reason: GatewayNotReady
    status: Unknown
    type: GatewayReady
  - lastTransitionTime: "2020-08-29T06:52:45Z"
    message: Waiting for Gateway IP
    reason: GatewayNotReady
    status: Unknown
    type: Ready
  - lastTransitionTime: "2020-08-29T06:52:45Z"
    status: "True"
    type: RouteReady
  mappedRouteName: my-service
```

Gatewayとは、Isito ingress gatewayとなり、Isito ingress gatewayにExternal IPがつくのは、Type: Loadbalancerの場合になります。現状は、NodePortなのでExternal IPアドレスは付与されいないため、カスタムドメインのマッピングが完了しません。

```
$ k get svc istio-ingress -n gke-system
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                                      AGE
istio-ingress   NodePort   10.40.11.200   <none>        15020:32600/TCP,80:32760/TCP,443:32520/TCP   17h
```

手順に戻ります。
Cloud Runのコンソールで、カスタムドメインの管理をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/4ffeff94-d035-5398-a9f5-9d5662d512be.png)

ADD MAPPING＞サービスドメインマッピングを追加をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/eef7e30e-2126-fa7d-d619-25fb93340e0d.png)

マッピングする、Cloud Run for Anthos(Knatvie)のサービスとカスタムドメイン名を入力して続行をクリックします。
カスタムドメイン名は、元々のサービス名がmy-service.default.example.comの場合で、test.comを使いたい場合は、my-service.test.comになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/832c91d4-0454-1d05-3760-2884e0ec0ec0.png)


DomainMappingが作成されているか確認します。

```
$ k get DomainMapping
NAME                           IP               ROUTE        READY   REASON   URL
my-service.xxx.com   xx.xx.xx.xx   my-service   True             http://my-service.xxx.com
```

```
$ k get DomainMapping my-service.xxx.com -o yaml
apiVersion: domains.cloudrun.com/v1alpha1
kind: DomainMapping
metadata:
  creationTimestamp: "2020-09-14T11:26:24Z"
  generation: 1
  name: my-service.xxx.com
  namespace: default
  resourceVersion: "647602"
  selfLink: /apis/domains.cloudrun.com/v1alpha1/namespaces/default/domainmappings/my-service.xxx.com
  uid: 590b332b-3faf-43de-be9e-f7fff6c73006
spec:
  certificateMode: AUTOMATIC
  routeName: my-service
status:
  conditions:
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    message: Certificate is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: CertificateProvisioned
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    message: DomainMapping specific Gateway is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: DomainMappingGatewayReady
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    status: "True"
    type: GatewayReady
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    status: "True"
    type: Ready
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    status: "True"
    type: RouteReady
  mappedRouteName: my-service
  resourceRecords:
  - name: my-service.default
    rrdata: xx.xx.xx.xx
    type: A
  url: http://my-service.xxx.com
```

登録するDNSのAレコードが表示されます。ただし、こちらは、External TCP/UDP Network Load BalancingのIPアドレスになるため、実際は、External HTTP(S) Load BalancingのIPアドレスをAレコードに登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/c932667b-fe5f-ae62-4451-99c050ed14ae.png)

今回は、Cloud DNSを使用するので、Cloud DNSのコンソール画面で設定します。
DNS名に名前解決を行う、Cloud Run for Anthos(Knatvie)のサービス名、IPv4アドレスにIngressのIPアドレスを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/b26e21c4-b621-0089-e0df-fd37d1e9e7a3.png)


##接続確認します
名前解決できているか確認します。

```
$ host my-service.<ドメイン名>
my-service.<ドメイン名> has address <IngressのIPアドレス>
```

IngressのIPアドレスを取得します。

```
INGRESS_IP=$(kubectl get ingress my-ingress -n gke-system \
    --output jsonpath='{.status.loadBalancer.ingress[0].ip}')
```

サンプル サービスに HTTP GET リクエストを送信します。
Hostヘッダーでカスタムドメインを指定してみる。

```
$ curl --insecure -siH "Host: my-service.<ドメイン名>" https://$INGRESS_IP
HTTP/2 200
content-length: 2
content-type: text/plain; charset=utf-8
date: Sat, 29 Aug 2020 06:33:19 GMT
x-envoy-upstream-service-time: 5
server: istio-envoy
via: 1.1 google
alt-svc: clear
```

ヘッダーは、明示的に指定しなくても、暗黙的にHostヘッダーが指定されているため接続できます。

```
$ curl --insecure -si https://my-service.<ドメイン名>
HTTP/2 200
content-length: 2
content-type: text/plain; charset=utf-8
date: Sat, 29 Aug 2020 07:07:24 GMT
x-envoy-upstream-service-time: 5
server: istio-envoy
via: 1.1 google
alt-svc: clear
```

```
* Expire in 3 ms for 1 (transfer 0x561f34d59fb0)
* Expire in 4 ms for 1 (transfer 0x561f34d59fb0)
*   Trying 35.190.234.153...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x561f34d59fb0)
* Connected to my-service.xxx.com (xx.xx.xx.xx) port 80 (#0)
> GET / HTTP/1.1
> Host: my-service.default.uimoz.com
> User-Agent: curl/7.64.0
> Accept: */*
```

#落ち穂拾い
##再度Type:LoadBalancerを変更した場合どうなるか？
StatusがUnknownになりますが、カスタムドメインでの接続は可能です。

```
$ k get DomainMapping
NAME                           IP    ROUTE        READY     REASON            URL
my-service.xxx.com         my-service   Unknown   GatewayNotReady
```

message: Waiting for Gateway IPというメッセージが確認できます。

```
$ k get DomainMapping my-service.xxx.com -o yaml
apiVersion: domains.cloudrun.com/v1alpha1
kind: DomainMapping
metadata:
  creationTimestamp: "2020-09-14T11:26:24Z"
  generation: 1
  name: my-service.xxx.com
  namespace: default
  resourceVersion: "660691"
  selfLink: /apis/domains.cloudrun.com/v1alpha1/namespaces/default/domainmappings/my-service.xxx.com
  uid: 590b332b-3faf-43de-be9e-f7fff6c73006
spec:
  certificateMode: AUTOMATIC
  routeName: my-service
status:
  conditions:
  - lastTransitionTime: "2020-09-14T12:19:41Z"
    message: Certificate is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: CertificateProvisioned
  - lastTransitionTime: "2020-09-14T12:19:41Z"
    message: DomainMapping specific Gateway is not needed as TLS is not enabled.
    reason: NotNeeded
    status: "True"
    type: DomainMappingGatewayReady
  - lastTransitionTime: "2020-09-14T12:19:41Z"
    message: Waiting for Gateway IP
    reason: GatewayNotReady
    status: Unknown
    type: GatewayReady
  - lastTransitionTime: "2020-09-14T12:19:41Z"
    message: Waiting for Gateway IP
    reason: GatewayNotReady
    status: Unknown
    type: Ready
  - lastTransitionTime: "2020-09-14T11:26:24Z"
    status: "True"
    type: RouteReady
  mappedRouteName: my-service
```

接続を確認してみると、デフォルトドメインでもカスタムドメインでも接続はできます。
カスタムドメインのみで接続。

```
$ curl --insecure -si https://my-service.xxx.com
HTTP/2 200
content-length: 2
content-type: text/plain; charset=utf-8
date: Mon, 14 Sep 2020 14:20:10 GMT
x-envoy-upstream-service-time: 1404
server: istio-envoy
via: 1.1 google
alt-svc: clear

```
##GKEクラスタのデフォルトドメインの変更
https://cloud.google.com/run/docs/gke/default-domain
テスト用途
理由は、ワイルドカードDNSを使うことになるから。
ワイルドカードDNSは、いろいろ課題がある。
https://en.wikipedia.org/wiki/Wildcard_DNS_record

パッチを適用する。

```
$ kubectl patch configmap config-domain --namespace knative-serving --patch \
> '{"data": {"example.com": null, "xxx.com": ""}}'
configmap/config-domain patched
```

Cluster default domainのMappingが作成される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/7cdca617-5709-ab2c-e795-02d9c3240c2f.png)

CRDは作成されない。

```
$ k get DomainMapping
No resources found in default namespace.
```

カスタムドメインでアクセス可能。

```
$ curl  -si http://my-service.xxx.com
HTTP/1.1 200 OK
content-length: 2
content-type: text/plain; charset=utf-8
date: Mon, 14 Sep 2020 14:30:57 GMT
x-envoy-upstream-service-time: 1
server: istio-envoy
```