Kubernetesでhttpエラー503を回避するための設定を入れる

前提として、httpエラー503というのは、ロードバランサーからみてバックエンドの応答がない、どこかで処理が詰まっているのかリソースが足らないのかという話という認識です。
これは要は、クラスタ更新・イメージ更新・ノードのドレイン・部分障害時に部分的にでもアクセスが可能なように設定を入れておくための備忘録です。
用いることがある環境はAKSです。書いてる時点ではkubernetes本体バージョンは1.21です。ご自身で動作を確かめてみてください。

- hpa、replica数を2以上にスケールする設定を入れる
ステートレスな類のPodの場合、水平にスケールさす設定を入れて最大と最小のレプリカPod数を定義する
https://kubernetes.io/ja/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/

```
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: test-st
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: test-st
  minReplicas: 2
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 90
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 85
```
クラスタ本体ノードプール側のスケーリングも気にする↓
https://docs.microsoft.com/ja-jp/azure/aks/cluster-autoscaler#create-an-aks-cluster-and-enable-the-cluster-autoscaler
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster#max_count
例えば特定の時間だけバーストして時間がはっきりわかってるような場合はそういう機能があるわけではなさそうなのでノードプール追加してAutomationかDevOpsかなにかでスケールノード数の制御をする時限設定を仕込んでおくといいのかもとは思ったけど試してはないです。

- PodDisruptionBudget
https://kubernetes.io/docs/tasks/run-application/configure-pdb/
同じ種類のPodの最低必要稼働数を定義し更新時のサービス影響を予防する。（ただしテスト環境でノードが1台の場合に`minAvailable: 1`を指定するとノードがドレインできなくなったりはする。要は、最低限必要なPod数を稼働させ続けるための動きをするがノードの数が少ないとPodが刺さったままノードがドレインできなくなるので注意が必要。）

```yaml:pod-disruption-budget.yaml
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: test-ingress-error
  namespace: default
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: test-ingress-error
```

- 各Podの種類に適した終了時のライフサイクルpreStop処理を設定
https://kubernetes.io/blog/2021/04/21/graceful-node-shutdown-beta/
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
既存のトラフィックがサービスアウトする前にエラー終了せずに正常終了させる
俗にいうGracefulShutdownのこと。ここではkubelet側ではなくてPod毎の設定を定義（AKSだとkubelet側の定義が不可能だしそれは全体の設定しかできなくて個々のPod種別毎に必要な値が違うケースがある）
https://docs.microsoft.com/ja-jp/azure/aks/custom-node-configuration


```
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-ingress-error
spec:
  selector:
    matchLabels:
      app: test-ingress-error
  replicas: 1
  template:
    metadata:
      labels:
        app: test-ingress-error
        nginx: allowed
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: test-ingress-error
#～略～
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh","-c","echo prestop.; date; sleep 20"]
```
`terminationGracePeriodSeconds = sleep秒数 + コンテナ停止にかかる秒数`
に定義する。特別な終了処理を指定できる場合はsleepじゃなくてそのコマンドを指定するとより良いが、sleepだけでも503エラーが結構減ってることが計測できてたのでやらないよりマシではないかと思われる。
terminationGracePeriodSecondsを増やしすぎるとノードのドレインにその分時間がかかりクラスタアップデート時のノードプール更新時間が増える。

- 必要に応じて各Podの起動時間にあったStartupProbe処理を設定する
https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes
起動が遅いコンテナはこれを指定しないと他のプローブ設定が失敗し続けて永遠に起動できなくてステータスがCrashloopBackoffになったりする。要は、Podの起動直後にこの設定で指定した分だけ待ってから他のprobeの確認が始まる。

```
# 起動にMax202秒かかる場合
startupProbe:
  httpGet:
    path: /health
    port: 21099
  #initialDelaySeconds: 120 #不要の認識
  timeoutSeconds: 2 #Probeがタイムアウトになるまでの秒数
  failureThreshold: 20 #Probeが失敗した場合、KubernetesはfailureThresholdに設定した回数までProbeを試行
  successThreshold: 1 #1のみ選択可能
  periodSeconds: 12 #Probeが実行される頻度(秒数)
  #起動完了までfailureThreshold * periodSeconds=20x12=240秒猶予がある認識
```
- AntiAffinityとDeschedulerで同じ種類のPodが別のワーカーノードに分散するよう定義する
イングレスコントローラのPodが2つあったとして同じノードにどっちも居た場合、そのノードをドレインされると503エラーが出る。
https://kubernetes.io/ja/docs/concepts/scheduling-eviction/assign-pod-node/#pod%E9%96%93%E3%82%A2%E3%83%95%E3%82%A3%E3%83%8B%E3%83%86%E3%82%A3%E3%81%A8%E3%82%A2%E3%83%B3%E3%83%81%E3%82%A2%E3%83%95%E3%82%A3%E3%83%8B%E3%83%86%E3%82%A3
```
    spec:
      affinity:
        podAntiAffinity:
          #requiredDuringSchedulingIgnoredDuringExecution:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
            - labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - test-stat
              topologyKey: "kubernetes.io/hostname"
      terminationGracePeriodSeconds: 30
      containers:
      - name: test-stat
```
※requiredDuringSchedulingIgnoredDuringExecutionだと必須なので違反するPodはスケジュールされないが、preferredDuringSchedulingIgnoredDuringExecutionは必須でなく推奨になるためweightに従って計算されて違反しても配置はされるのでちょっと安全。（ingress-nginxのhelmチャートのvalues.yamlのコメントに書かれてる初期値がそうなっている）
※AntiAffinityはノード数百台だと負荷に問題出るらしいのとtopologySpreadConstraints(1.19から)はzoneの分散も可能らしいが確かめてはいない。
※上記はPod作成時のみ効果があるため、DeschedulerでPodのスケジューリングに違反する偏ってるPodを落とすという機能を追加する必要がある。
https://github.com/kubernetes-sigs/descheduler
helmチャートをpullしてきてchartsに入れてvalues.yamlで上書きする場合こんなかんじ↓

```
descheduler:
  deschedulerPolicy:
    strategies:
      RemoveDuplicates:
        enabled: true
      RemovePodsViolatingNodeTaints:
        enabled: false
      RemovePodsViolatingNodeAffinity:
        enabled: false
      RemovePodsViolatingInterPodAntiAffinity:
        enabled: true
      LowNodeUtilization:
        enabled: false
      RemovePodsViolatingTopologySpreadConstraint:
        enabled: true
      RemovePodsHavingTooManyRestarts:
        enabled: true
```

ポリシーの意味とかは以下のあたりに。
https://github.com/kubernetes-sigs/descheduler/blob/master/docs/user-guide.md
https://github.com/kubernetes-sigs/descheduler/blob/master/examples/policy.yaml
テストするときは設定入れるまえに偏った状態をノードドレインとかで再現しておいてから。

- フロントからバックエンドに行くにつれてそれぞれのタイムアウト値が狭まるように設定する
手前のロードバランサのタイムアウト値がイングレスのプロキシタイムアウト関連よりも秒数少ない場合、処理は継続的に行われて正常終了していたとしてもLB側が503エラーを返したりする。
https://yasuhiroa24.hateblo.jp/entry/2020/04/22/180326
http://public.dhe.ibm.com/software/dw/jp/websphere/was/was7_update/wasv7updatews05design_rv.pdf
https://docs.microsoft.com/ja-jp/azure/frontdoor/troubleshoot-issues#troubleshooting-steps
https://aws.amazon.com/jp/premiumsupport/knowledge-center/apache-backend-elb/
https://cloud.google.com/load-balancing/docs/https#timeouts_and_retries

- kuredじゃなくて公式のイメージ更新方法にする
kuredだと更新時に更新されずに1ノードだけ残ってしまうパターンがあるため公式の方がいいみたいだけども、メンテスケジュール時間指定はまだプレビューらしいのでTerraformのパラメータが見当たらないかもしれない。
https://docs.microsoft.com/ja-jp/azure/aks/upgrade-cluster#set-auto-upgrade-channel
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster#automatic_channel_upgrade
- メモリ保持メンテ等のメンテリソースを自動でドレインしてくれる新機能が気になる
https://azure.microsoft.com/en-us/updates/public-preview-node-autodrain-for-aks/
インスタンスメタデータ有効化が要るっぽい
https://docs.microsoft.com/en-us/azure/virtual-machines/linux/instance-metadata-service?tabs=linux

余談で、503じゃなくて403なんですが、ingressコントローラのPodのServiceで指定するloadBalancerIPを固定にしていて、`externalTrafficPolicy: "Local"`の場合に再送時にSNATでEgressのIPから届く関係で、もしIP制限している場合はEgressのIPをWhitelistとかに追加しないとアップデート時等にhttpエラー403で再送失敗します。

以上です。