OKEでのロードバランサーの設定と継続アクセスの設定について

# はじめに

Kubernetesを構築する際、Podの作成時にロードバランサーも設定することはほぼ必須と言えます。  
OCIはロードバランサー機能としてOCI Native Ingress Controllerを公開しており、自分もこれを使い設定しました。  
また、ロードバランサーは対象としているPodが何らかの影響で消えてしまっても、新しく生成されたPodを対象と自動設定してくれますが、このPodの削除とロードバランサーの対象の再設定は非同期で行われます。  
このままではPodの設定変更などで再生成した際に、サービスへ一時的にアクセスができない状態になってしまいます。  
そのためにpreStopなどを設定し、Podの停止開始から実際に終了するまでに猶予期間を設けてやらなければいけません。
以上2つの導入を行い、結果として成功はしましたが結構苦戦してしまったため、忘備録も兼ねて設定までの手順を大きく2つに分けて記述します。  
- OCI Native Ingress Controllerのインストール手順
- Pod削除時にアクセスが止まらないようにする設定

# OCI Native Ingress Controllerのインストール

OCI Native Ingress Controllerの設定方法としてはhelmやスタンドアロン・プログラムなどがありますが、自分はアドオン・クラスタによる設定を行います。  
セットアップ手順は[こちらの公式ドキュメント](https://docs.oracle.com/ja-jp/iaas/Content/ContEng/Tasks/contengsettingupnativeingresscontroller-cluster-addon-top-level.htm)に書かれていますが、色々な情報があってわかりにくいので、最低限ロードバランサーが使えるまでの導入手順を改めて説明します。

## OCIネイティブ・イングレス・コントローラをクラスタ・アドオンとしてデプロイするための前提条件

### ociコマンドのインストール

今回のロードバランサー導入ではociコマンドを使います。そのため、OCI CLIをインストールしていない場合はインストールしてください。  
インストールは[こちらを参考](https://docs.oracle.com/ja-jp/iaas/Content/API/SDKDocs/cliinstall.htm)にしてください。

### cert-managerのインストール

OCIネイティブ・イングレス・コントローラはWebフックを使用してポッド・レディネス・ゲートを使用しますが、そのWebフックはcert-managerを使いWebフック・サーバーの証明書とキーを生成および管理します。  
そのため、はじめにcert-managerをインストールします。インストールは以下のコマンドを実行してください。  
```
kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
```
その後、`kubectl get pod -n cert-manager`を実行し、以下のようにcert-manager Podが起動していることが確認出来たらインストール成功です。
```
$ kubectl get pod -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-74b56b6655-c4xxb              1/1     Running   0          16d
cert-manager-cainjector-55d94dc4cc-jvcmh   1/1     Running   0          16d
cert-manager-webhook-564f647c66-7jnwp      1/1     Running   0          16d
```

## アドオンでOCIネイティブ・イングレス・コントローラをインストール

### OCIネイティブ・イングレス・コントローラ・アドオンのデプロイ

はじめに、適切なディレクトリに以下のjsonファイルを作成します。ここで、`<compartment-id>`は公式ドキュメントでは「OCIネイティブ・イングレス・コントローラがOCIロード・バランサおよび証明書を作成するコンパートメントのOCIDです」と書いてありますが、OKEやノードを配置したコンパートメントを指定すれば大丈夫です。また、`<load-balancer-subnet-ocid>`は公式の記述通りロード・バランサーを配置するサブネットのOCIDです。
```
{
  "addonName": "NativeIngressController",
  "configurations": [
    {
      "key": "compartmentId",
      "value": "<compartment-ocid>"
    },
    {
      "key": "loadBalancerSubnetId",
      "value": "<load-balancer-subnet-ocid>"
    }    
  ]
}
```

次に、以下のコマンドを実行してアドオンをクラスタにデプロイします。ここで、`<cluster-ocid>`はアドオンを入れるクラスタのOCIDを、`file://./<path-to-config-file>`は先ほど作成したjsonファイルまでのパスを記述してください。
```
oci ce cluster install-addon --addon-name NativeIngressController --cluster-id <cluster-ocid> --from-json file://./<path-to-config-file>
```
最後に、`oci ce cluster list-addons --cluster-id <cluster-ocid>`を実行し、以下のようにNativeIngressControllerが"ACTIVE"になっていることが確認出来たらインストール成功です。
```
$ oci ce cluster list-addons --cluster-id ocid1.cluster.oc1.ap-tokyo-1.aaa~~~
{
  "data": [
    {
~~~
    },
    {
      "addon-error": null,
      "current-installed-version": "v1.4.1",
      "lifecycle-state": "ACTIVE",
      "name": "NativeIngressController",
      "time-created": "2025-01-14T02:49:52+00:00",
      "version": null
    },
~~~
  ]
}
```

### イングレス関連リソースの作成

はじめに、`IngressClassParameters`リソースを作成します。これは、ロードバランサーの詳細を設定するリソースです。適切なディレクトリに以下のyamlファイルを作成してください。  
ここで、`<lb-name>`は作成したいロードバランサーの名前を記入してください。  
また、`<max-bw>`はロードバランサーがサポートする帯域幅の上限で、`<min-bw>`はロードバランサーが常時使用する帯域幅を表します。
```
apiVersion: "ingress.oraclecloud.com/v1beta1"
kind: IngressClassParameters
metadata:
  name: <icp-name>
  namespace: <ns-name>
spec:
  compartmentId: "<compartment-ocid>"
  subnetId: "<subnet-ocid>"
  loadBalancerName: "<lb-name>"
  isPrivate: false
  maxBandwidthMbps: <max-bw>
  minBandwidthMbps: <min-bw>
  reservedPublicAddressId: <reserved-ip-ocid>
```

次に、`IngressClass`リソースを作成します。これは、ロードバランサーとして使うIngressを指定するものです。適切なディレクトリに以下のyamlファイルを作成してください。先ほどの`IngressClassParameters`リソースの下に追記しても大丈夫です。  
ここは、基本的には以下のコードの内、nameやnamespaceを変えたものをそのまま使ってもいいです。
```
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: <ic-name>
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
spec:
  controller: oci.oraclecloud.com/native-ingress-controller
  parameters:
    scope: Namespace
    namespace: native-ingress-controller-system
    apiGroup: ingress.oraclecloud.com
    kind: ingressclassparameters
    name: <icp-name>
```

最後に、`Ingress`リソースを作成します。これは、実際にロードバランサーとして機能するものです。以下はnginxで動いているPodにアクセスするロードバランサーの設定例です。  
例の`spec.rules`以下はロードバランサーでのアクセス設定で変わりますので、この部分は柔軟に変更してください。ここでは`nginx-test`Serviceをバックエンドとして指定しています。
```
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-test
spec:
  ingressClassName: <ic-name>
  rules:
    - http:
        paths:
          - pathType: Prefix
            path: /
            backend:
              service:
                name: nginx-test
                port:
                  number: 80
```

以上のイングレスリソースの設定ができたらデプロイしてください。

### ロードバランサーの設定確認

先ほどのイングレスを設定した後、OCIコンソールの"ネットワーキング > ロード・バランサ"より、`IngressClassParameters`リソースの`loadBalancerName`で指定したロード・バランサが作成されていることを確認してください。
![ロードバランサーの設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001956/ca2a51f1-b16c-4de9-b585-8aea9765984e.png)

次に、作成したロードバランサーをクリックし、そこの"詳細"欄の"IPアドレス"欄のIPアドレスに対して`curl`コマンドを実行し、接続できることを確認してください。
![ロードバランサーのIPアドレス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001956/696c9a04-7095-47a7-a58a-9ce730b30e3c.png)
```
$ curl http://151.145~~
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

次に、同じページ下の"リソース"欄にある"バックエンド・セット"をクリックし、出てきた"バックエンド・セット"欄を出します。そして、"バックエンドの数"がロードバランサーの宛先として指定しているPodの数と同じバックエンド・セットを選択してください。
![バックエンド・セットの場所.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001956/839a47fe-28bd-4a07-b67e-9b04fa85a01e.png)

移動先の画面の下の"バックエンド"を選択し、そこで出てきた宛先のIPアドレスがPodのものと一致していることを確認してください。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001956/784a8ff6-8413-4d74-ad33-2a438a1e9a99.png)
```
$ kubectl get pod -o wide -n default
NAME                          READY   STATUS    RESTARTS   AGE     IP            NODE          NOMINATED NODE   READINESS GATES
nginx-test-54b967d8f7-d872l   1/1     Running   0          5h44m   10.0.20.209   10.0.20.43    <none>           1/1
nginx-test-54b967d8f7-dl9fv   1/1     Running   0          5h44m   10.0.20.85    10.0.20.159   <none>           1/1
```

これで、ロードバランサーの設定は完了です。

# Pod削除時にアクセスが止まらないようにする設定

ロードバランサーを設定したことでPodに何かあって再作成されてしまっても、自動で宛先を再生成後のPodに変えてくれます。しかし、この宛先設定とPodの再生成は非同期なので、Podへアクセスできない期間が発生してしまいます。  
このままではアプリの処理ミスなどが発生してしまうため、必ずロードバランサーの宛先変更が行われてからPodを終了するようにしたいです。そのための設定を試してみました。

## 安全にPodを終了する設定

(おそらく)最も一般的なのは、preStopフックの設定です。これはプロセス終了前に実行する事前処理で、ここで`sleep`を設定してやることで、ロードバランサーの宛先変更が行われてからPodが終了してくれるので、アクセスできない期間を無くすことができます。  

## Pod開始時の設定

こちらも極々一般的な設定ですが、`livenessProbe` `readinessProbe` `startupProbe`を設定します。  
これらはそれぞれ以下の意味を持ちます。詳細についてはKubernetes公式や他のQiita記事などで詳しく書かれていますので、そちらを参照してください。  
- livenessProbe  
Podが正常に動作しているかどうかの確認を行います。  
確認処理が失敗した場合は、Podの再起動を実行します。
- readinessProbe  
Podがリクエストを処理する準備ができているかの確認を行います。  
確認処理が失敗した場合は、そのPodに対しリクエストを送らないようにします。
- startupProbe  
コンテナが正常に起動したかの確認を行います。  
確認処理が失敗した場合は、そのPodを終了します。

## 実際に設定してみた

バックエンドに指定したPodのdeploymentマニフェストの`spec.templete.spec.containers`に以下の設定を記述しました。
```
livenessProbe:
  httpGet:
    path: /
    port: 80
  periodSeconds: 1
readinessProbe:
  httpGet:
    path: /
    port: 80
  periodSeconds: 1
startupProbe:
  httpGet:
    path: /
    port: 80
  failureThreshold: 30
  periodSeconds: 5
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]
```

また、テストは以下のスクリプトを実行し、その最中に先ほどのdeploymentを再起動してもエラーコードが返ってこないかどうかで行います。  
なお、このスクリプトはcurlコマンドを0.1秒ごとに送り続け、その返信のHTTPステータスコードだけ表示するものです。
```
#!/bin/bash

URL="http://{ロードバランサーのIPアドレス}"

while true; do
  response=$(curl -o /dev/null -s -w "%{http_code} %{time_total}" "$URL")
  http_code=$(echo $response | awk '{print $1}')
  time_total=$(echo $response | awk '{print $2}')
  
  if [ "$http_code" -ne 200 ]; then
    echo "Error: HTTP status code $http_code, response time $time_total seconds"
  else
    echo "Success: HTTP status code $http_code, response time $time_total seconds"
  fi
  
  sleep 0.1
done
```

## 結果

以下のように、エラーコードが返ってきてしまいました・・・。
```
Success: HTTP status code 200, response time 0.210196 seconds
Success: HTTP status code 200, response time 0.214259 seconds
Success: HTTP status code 200, response time 0.217532 seconds
Error: HTTP status code 504, response time 2.210239 seconds
Error: HTTP status code 504, response time 2.210982 seconds
Error: HTTP status code 504, response time 2.210882 seconds
Error: HTTP status code 504, response time 2.213557 seconds
Error: HTTP status code 504, response time 2.214295 seconds
Error: HTTP status code 504, response time 2.213659 seconds
~~~
Error: HTTP status code 502, response time 0.210954 seconds
Error: HTTP status code 502, response time 0.210817 seconds
Error: HTTP status code 502, response time 0.211473 seconds
Error: HTTP status code 502, response time 0.211086 seconds
Error: HTTP status code 502, response time 0.211842 seconds
Error: HTTP status code 502, response time 0.211449 seconds
Success: HTTP status code 200, response time 0.216021 seconds
Success: HTTP status code 200, response time 0.218019 seconds
Success: HTTP status code 200, response time 0.219808 seconds
Success: HTTP status code 200, response time 0.213846 seconds
Success: HTTP status code 200, response time 0.219713 seconds
Success: HTTP status code 200, response time 0.219700 seconds
Success: HTTP status code 200, response time 0.213490 seconds
```

sleep期間が足りなかったのかと思い10→30に増やしてみましたが、同じ結果になりました。   
`kubectl get pod`でPodの生成を確認しても、元のPodが残ってる状態で新しいPodが立ち上がっているのに、です。  
OCIコンソールのロードバランサーを見ても、全てのPodが準備中になっている期間があり、やはり宛先切り替えがPod再生成に追いついていなさそうです。
他の設定を弄ってみても解決せず、これがOCIの仕様かとも考え始めました。

## 解決法

OCIの公式ドキュメントを読んでみると、「ポッド準備ゲート」なるものがありました。これはポッドがトラフィックを受信する準備ができていることを示す追加の条件で、Podに対して以下のコマンドでラベル付けして設定します。
```
kubectl label ns <namespace> podreadiness.ingress.oraclecloud.com/pod-readiness-gate-inject=enabled
```

これだけ聞くと先の`readinessProbe`と同じことを行っているので、設定しても変わらないはずですが、一応設定してみることに。  

結果は以下の通り解決しました。
```
HTTP status code 200, response time 0.217102 seconds
HTTP status code 200, response time 0.220322 seconds
HTTP status code 200, response time 0.214657 seconds
HTTP status code 200, response time 0.219846 seconds
~~~
HTTP status code 200, response time 0.216225 seconds
HTTP status code 200, response time 0.219052 seconds
HTTP status code 200, response time 0.215893 seconds
```

この機能自体はOCI独自の物ではなく、[Kubernetesの公式](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)にも記載があります。
どうもポッド準備ゲートは`readinessProbe`の拡張機能をOCI用に改修したものっぽいですが、詳細は分かっていません・・・。  
分かり次第追記します。

# わかったこと

OCIに限らず、一般的な方法で解決しなかったらベンダーのドキュメントを読んでみると解決するかもしれないです。

# 参考元

OCI公式: クラスタ・アドオンとしてのOCIネイティブ・イングレス・コントローラの操作  
https://docs.oracle.com/ja-jp/iaas/Content/ContEng/Tasks/contengsettingupnativeingresscontroller-cluster-addon-top-level.htm

Zenn記事: アルパカでもわかる安全なPodの終了  
https://zenn.dev/hhiroshell/articles/kubernetes-graceful-shutdown#%E5%AF%BE%E7%AD%961%3A-prestop%E3%83%95%E3%83%83%E3%82%AF%E3%81%A7%E3%81%AEsleep

Kubernetes公式: Liveness Probe、Readiness ProbeおよびStartup Probeを使用する  
https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/