GKE Ingress（GCE）の導入ではまったところ

先日 GKE で Ingress を導入したのですがその際に予想外にはまったので備忘録も兼ねてまとめます。

## 前置き
GKEではIngressと言うと以下の2種類があります。

- GCE Ingress
- NGINX Ingress

今回対象となるのは`GCE Ingress`になりますが、理解を深めるためにそれぞれの特徴について軽く触れます。

### GCE Ingress

GCE IngressではIngressオブジェクトを通じてGoogle CloudのL7 Load Balancerを設定します。また、コンテナネイティブ負荷分散機能を利用することでL7 Load Balancerからコンテナに対して直接通信できるようになります。

HTTPSの終端をLoad Balancerに任せることができたり、Ingress Controllerを意識しなくて済むなど、マネージドサービスを利用している恩恵を享受することができるのがメリットです。

### NGINX Ingress

`NGINX Ingress`はGCE Ingressでは実現できないより細やかな制御が可能です。GCE IngressではL7 Load BalancerがHTTPSの終端となってルーティング制御をするのに対し、NGINX Ingressは`NGINX Ingress Controller`というPodがその役割を担っています。

以下がNGINX Ingressの構成図になります。ユーザーからのリクエストをL4 Load Balancerで受け付け、L7レイヤの処理はNGINX Ingress Controllerが担当します。NGINX Ingress Controllerの実体はNGINXなので、NGINXで実現可能な制御全てを設定できます。

![スクリーンショット 2021-09-28 3.54.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206595/eeb686a6-0fd4-40b3-9c41-baf07a10ac33.png)

NGINX Ingressを使うとHTTPSの終端がPodになるのでマネージド証明書が使えなかったり、NGINX Ingress Controllerを自分たちで管理する必要があったりと運用コストが大きいので、GCE Ingressで要件を満たす場合には採用動機は無いと思います。

### コンテナネイティブ負荷分散について

GKEクラスタ内のコンテナはGoogle Cloudのリソースではないため、デフォルトではL7 Load Balancerから直接通信することができません。そのためK8s ServiceをNodePortとして公開し、対象のインスタンスグループと公開したポートをバックエンドサービスとしてL7 Load Balancerに紐づけてやる必要があります。

これはNodePortを管理する運用コストを増加させます。またNodePortはネームスペース間で共有されるので、マルチテナント構成でGKEを運用している場合は環境の分離が難しくなります。

そこで利用できる機能がコンテナネイティブ負荷分散です。コンテナネイティブ負荷分散では、NEG(Network Endpoint Group)を利用して各Podに対応するネットワークエンドポイントを作成することで、L7 Load Balancerからコンテナに直接通信できるようになります。結果としてK8s ServiceをClusterIPとして公開した場合にも外部からアクセス可能になり、NodePortを管理する必要性が無くなります。

GCE Ingressを使うとこのNEGもIngress Controllerが自動で作成してくれます。

## はまったところ（1） - HTTP 負荷分散の有効化

ここで4時間くらいはまりました...（こんなところで4時間もはまるな、という感じですが）。

結論から書くとGCE Ingressを利用する場合は以下のようにGKEクラスタのHTTP負荷分散を有効化しておく必要があります。

![スクリーンショット 2021-09-28 9.36.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206595/3264e94b-2f69-3b3b-aeda-bbd211fb41b1.png)

GCE Ingressの場合、GKE組み込みのIngress Controllerが制御を行います。HTTP負荷分散が無効になっている場合、Ingress ControllerのPod(l7-default-backend)が起動しないため、Ingressオブジェクトを作成しようとしてもいつまでも作成が完了しません。

この状態ではまると、ログが一切出力されないので途方に暮れてしまいます。

## はまったところ（2） - ヘルスチェック

上述の通りコンテナネイティブ負荷分散を利用するとK8s ServiceをClusterIPを利用することができます。具体的には以下のようにannotationを設定することでServiceが紐付いているPodに対応したNEGが作成されるようになります。

```yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    cloud.google.com/neg: '{"ingress": true}'
...
```

GCE Ingressを利用している場合、上記annotationを付与した状態でServiceを作り、対象のサービスにルーティングするようIngressオブジェクトを作成すると、Ingress ControllerがGoogle Cloud上にNEGとL7 Load Balancerを自動で作成してくれます。また、Readiness Probeの設定値をもとにHealth Checkも併せて作成してくれます。

私はこの時の導入でバックエンドにArgo CDの画面を設定していたのですが、NEGとL7 Load Balancerが作成されたもののヘルスチェックのステータスが異常になってしまいました。

これはGoogleのHealth CheckからArgo CD（厳密には画面用Podのargocd-server）にヘルスチェックが通らないために発生していました。Argo CDのReadiness Probeは以下のようにHTTPでアクセスするように設定されています。

```yaml
readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 3
  periodSeconds: 30
```

これでReadiness Probe自体は成功しますが、GoogleのHealth Checkからはアクセスが通りません。擬似再現としてargocd-serverに対してポートフォワードした状態でリクエストを投げると証明書エラーになってしまいます。

```bash
❯ curl -v -L http://localhost:8080/healthz
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /healthz HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 307 Temporary Redirect
< Content-Type: text/html; charset=utf-8
< Location: https://localhost:8080/healthz
< Date: Mon, 27 Sep 2021 08:27:04 GMT
< Content-Length: 66
< 
* Ignoring the response-body
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'https://localhost:8080/healthz'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 1
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
* Closing connection 0
```

これはargocd-serverがデフォルトでHTTPSでリクエストを受け付けることを前提にしている事に起因します。L7 Load Balancerがフロントエンドに配置されている構成では、HTTPSの終端はLoad Balancerになるためargocd-serverはHTTPでリクエストを受け付けるよう設定しても構いません。

ですので以下のようにargocd-serverの起動コマンドに「--insecure」オプションを付与することでHTTPリクエストを受け付けるよう設定を変更しました。

```yaml
spec:
  template:
    spec:
      containers:
      - name: argocd-server
        command:
        - /argocd-server
        - --staticassets
        - /shared/app
        - --repo-server
        - argocd-repo-server:8081
        - --insecure
```

これにより外からのヘルスチェックが通るようになり、ステータスが正常になりました。

```bash
❯ curl -v -L http://localhost:8080/healthz
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /healthz HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Mon, 27 Sep 2021 08:41:47 GMT
< Content-Length: 3
< Content-Type: text/plain; charset=utf-8
< 
ok
* Connection #0 to host localhost left intact
* Closing connection 0
```

なお、以下条件を満たしている場合はannotationを付与せずに、Service作成時に自動でNEGが作成されます。

- GKE クラスタ 1.17.6-gke.7 以降で作成された Service の場合
- VPC ネイティブ クラスタの使用
- 共有 VPC を使用しない
- GKE ネットワーク ポリシーを使用しない

https://cloud.google.com/kubernetes-engine/docs/concepts/ingress#container-native_load_balancing

## まとめ

GCE Ingressとコンテナネイティブ負荷分散は非常に便利な機能ですので積極的に利用するべきだと思いますが、理屈を理解していないと思わぬ所でつまづきます。

挙動が意図した通りにならない場合は、公式ドキュメントに加えこの記事を参考にして頂けると幸いです。