ZOZOのIstio Upgrade(v1.21.4 to v1.23.2)

## はじめに
2024/11に、ZOZOTOWNのマイクロサービスが稼働するプラットフォーム基盤上において、IstioのVersionをv1.21.4からv1.23.2にUpgradeしました。
本記事では、ZOZOにおけるIstio Upgradeの方針やその際の実施内容を紹介します。

## ZOZOでのIstio Upgrade方針
### Canary Upgrade
以前は、Control Planeやistio-ingressgatewayのUpgradeは[Istio Operator](https://istio.io/latest/docs/setup/install/operator/)を活用した[in-place upgrade](https://istio.io/latest/docs/setup/upgrade/in-place/)手法を採用していました。しかし、in-place upgradeは手軽であるものの、マイナーバージョンを2つ以上一気にUpgradeすることがサポートされておらず、Istioの各バージョンのEOLが短いことから頻繁にアップグレード作業が必要なため、運用コストの問題がありました。また、たとえばzozo-api-gatewayのistio-ingressgatewayにおいて、もしもアップグレードに伴う障害が起きた場合はマイクロサービス側へのリクエストがすべてエラーになってしまう可能性がありました。
そこで、[Canary upgrade with external traffic shifting (advanced)](https://istio.io/latest/docs/setup/additional-setup/gateway/#canary-upgrade-with-external-traffic-shifting-advanced)を参考に、ALB加重ルーティングを利用したControl Planeとistio-ingressgatewayのCanary Upgradeを採用することにしました。これにより、アップグレード作業の運用コストを抑え、万が一Upgradeに問題があった場合も影響を最小限にとどめることができます。

なお、Data Plane（Sidecarのistio-proxyがInjectされているマイクロサービス側）は、ZOZOでは[Flagger](https://flagger.app/)を用いたProgressive DeliveryによりCanary Releaseが可能なため、特別な考慮は必要ありません。

### Upgradeするタイミング
現在利用しているバージョンより2バージョン新しいバージョンがリリースされたら、Upgradeを実施します。
EOLを遵守しつつも、Upgrade頻度をなるべく下げて、工数を抑えるためです。

### 対象環境と順序
ZOZOでは、主にdev/stg/prdの3環境が存在します。当然ながら、すべての環境をUpgradeします。また、dev/stgもCanary Upgradeで実施します。dev/stgはin-placeでも問題ないのですが、prdと同じ手順で実施して問題がないことを確認してから、prdをUpgradeするためです。

## 今回のUpgrade実施内容
今回実施した、v1.21.4からv1.23.2へのUpgrade内容を以下に示します。

### 事前作業
#### リリースノートの確認
以下のドキュメントをすべて確認し、今回のUpgradeにより、問題となる変更がないことを確認しました。

- announcing
  - https://istio.io/latest/news/releases/1.21.x/announcing-1.21.5/
  - https://istio.io/latest/news/releases/1.21.x/announcing-1.21.6/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22.1/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22.2/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22.3/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22.4/
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22.5/
  - https://istio.io/latest/news/releases/1.23.x/announcing-1.23/
  - https://istio.io/latest/news/releases/1.23.x/announcing-1.23.1/
  - https://istio.io/latest/news/releases/1.23.x/announcing-1.23.2/
- change-notes
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22/change-notes/
  - https://istio.io/latest/news/releases/1.23.x/announcing-1.23/change-notes/
- upgrade-notes
  - https://istio.io/latest/news/releases/1.22.x/announcing-1.22/upgrade-notes/
  - https://istio.io/latest/news/releases/1.23.x/announcing-1.23/upgrade-notes/

ただし、Istio Operatorが正式にv1.23でdeprecatedとなり、v1.24以降では利用できなくなるため、次回のUpgrade時には代替方法の検討が必要であることが分かりました。

#### IstioとKubernetesの対応バージョン確認
予定しているIstioバージョンがKubernetesバージョンをサポートしていることを確認しました。Istio Upgrade作業時のEKSクラスターのversionは1.29なので、問題ありませんでした。
https://istio.io/latest/docs/releases/supported-releases/#support-status-of-istio-releases

![Istio___Supported_Releases.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221948/38d4b27d-cf8b-1c9f-66ab-b7fac6c1bc84.png)

#### istioctlによるprecheck
Upgrade前に`istioctl x precheck`を実行し、問題がないことを確認しました。

```
>istioctl x precheck --context {CONTEXT_NAME}
✔ No issues found when checking the cluster. Istio is safe to install or upgrade!
  To get started, check out https://istio.io/latest/docs/setup/getting-started/
```

#### 全体周知
slackで関係各所に、Istio Upgradeを実施する旨と予定スケジュールを事前に周知しました。

### Upgrade作業
dev/stg/prd環境ごとに、6StepsでUpgradeを実施しました。

#### Step1: 新しいバージョンのControl Planeリソースとistio-ingressgatewayの作成
新しいバージョンのControl Planeリソースを作成しました。具体的には、以下のリソースです。

- ClusterRole
- ClusterRoleBinding
- Deployment
- Service
- ServiceAccount
- CustomResourceDefinition

リソースのmanifestは以下のコマンドを実行して取得しました。ただし、古いバージョンのmanifest上でCRDのみ削除する必要があります。削除しないと、同一名のCRDが両バージョンで重複してしまうからです。

```
$ curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.23.2 sh -
$ istio-1.23.2/bin/istioctl operator dump --revision 1-23-2 > istio-operator.yaml
```

動作確認として、istio-operator-1-23-2のPodで想定外のエラーログがないことを確認しました。

続いて、新しいバージョンのistio-ingressgateway関連のリソースを作成しました。具体的には、以下のリソースです。

- IstioOperator
- istiodのPod
- 各種マイクロサービスのistio-ingressgatewayのPod
    - すべてのマイクロサービスでistio-ingressgatewayが存在するわけでなく、EKSクラスター外部から一次受けする可能性があるマイクロサービスにのみ用意しています

動作確認として、各Podで想定外のエラーログがないことを確認しました。

#### Step2: 新しいバージョンのistio-ingressgatewayに10%加重ルーティング
Ingressリソース（つまり、ALB）のannotationを更新し、新しいバージョンのistio-ingressgatewayに10%の加重ルーティングを行いました。以下は、zozo-api-gatewayのIngressリソースの例です。

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: "internal-api-gateway-ingress"
  annotations:
    alb.ingress.kubernetes.io/actions.forward-multiple-tg: >
      { "Type":"forward", "ForwardConfig": {"TargetGroups": [ {"ServiceName":"api-gateway-istio-ingressgateway-1-21-4","ServicePort":"80","Weight":90},{"ServiceName":"api-gateway-istio-ingressgateway-1-23-2","ServicePort":"80","Weight":10} ] }}
```

Datadog Metrics Explorerを確認し、10%のリクエストが新しいバージョンに流れていることを確認しました。想定外のエラーが発生していないことも確認しました。

#### Step3: 新しいバージョンのistio-ingressgatewayに100%加重ルーティング
Ingressのannotationを更新し、新しいバージョンのistio-ingressgatewayに100%の加重ルーティングを行いました。

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/actions.forward-multiple-tg: |
      { "Type":"forward", "ForwardConfig": {"TargetGroups": [ {"ServiceName":"api-gateway-istio-ingressgateway-1-23-2","ServicePort":"80","Weight":100} ] }}
```

同じく、Datadog Metrics Explorerを確認し、100%のリクエストが新しいバージョンに流れていることを確認しました。想定外のエラーが発生していないことも確認しました。

#### Step4: NamespaceのRevisionラベルを修正する
Data Planeのnamespaceの`metadata.labels.isito.io/rev`に新しいバージョンのRevisionを指定しました。以下は、zozo-api-gatewayのNamespaceリソースの例です。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: api-gateway
  labels:
    istio.io/rev: 1-23-2 # here
```

#### Step5: Data Planeリソースを再起動する
Data PlaneのDeploymentのannotationを更新し、Progressive Deliveryを実施します。
以下は、zozo-api-gatewayのDeploymentリソースの例です。

```yaml
apiVersion: apps/v1
kind: Deployment
namespace: api-gateway
metadata:
  name: api-gateway
spec:
  template:
    metadata:
      annotations:
        last-canary-retry-timestamp: "2024-11-06T19:02:00+0000" # here
...
```

Podが生まれ変わり、SidecarにInjectされているistio-proxyが新しいバージョンのものになったことを確認しました。
Podが作成される際に、KubernetesのAdmission Controllerがnamespaceに付与されている`istio.io/rev`ラベルの値を読み取り、そのrevisionのIstio Control Planeに接続して、そのバージョンのistio-proxyをPodにInjectしています。

#### Step6: 古いバージョンのControl Planeとistio-ingressgatewayを削除する
まず、古いバージョンのリソースをマニフェスト上から削除しました。
ZOZOでは[Flux](https://fluxcd.io/flux/)によるGitOpsを導入しているのですが、ZOZOではIstio関連のリソースはFlux対象外のため、自動では削除されません。したがって、以下のリソースを手動で削除しました。

- istiooperator
  - clusterrolebinding
  - clusterrole
  - service
  - serviceaccount
- istio-ingressgateway
  - deployment
  - service
  - horizontalpodautoscaler
  - poddisruptionbudget
  - serviceaccount
- istiod
  - poddisruptionbudget
  - clusterrolebinding
  - clusterrole
- MutatingWebhookConfiguration
- ValidatingWebhookConfiguration

以下のコマンドを実行し、古いバージョンのリソースが残っていないことを確認しました。

```
$ k get all  -A | grep 1.21.4
$ k get clusterrolebinding,clusterrole,pdb,mutatingwebhookconfiguration,validatingwebhookconfiguration,sa -A| grep istio | grep 1.21.4
```

### トラブル
#### CRDを削除してしまった
dev環境のUpgrade時に、誤ってstg/prd環境のistioperatorのCRDを削除してしまいました。本来は、古いバージョンのCRDをmanifestから削除する際には、同時に新しいバージョンのCRDを追加するべきだったのですが、stg/prdではそれが抜けていました。ZOZOのプラットフォーム基盤を構成管理するGitHubリポジトリでは、baseディレクトリで管理し、各環境でkustomizationによるoverlayをする構成になっています。dev環境のUpgradeのPull Request（以降、PR）では、baseからCRDを削除していたにもかかわらず、devの新しいバージョンのCRDしか追加していませんでした。このようなPRを作成してしまった理由は、前回のUpgrade時のPRをそのまま模倣してしまったからです。前回までのUpgradeでは、ZOZOのplatform基盤ではKubernetesのpruneを利用しない方式としていました。したがって、manifestからリソースを削除しても自動では削除されず、全環境のUpgradeが終わってからまとめて削除する手筈でした。しかしながら、今回のIstio Upgrade作業の少し前くらいからpruneを利用する方式に変わっており、baseのマニフェストからCRDを削除したタイミングで、クラスター上のCRDリソースに対して削除処理が走ってしまいました。
ただし、実際には、CRが残っていたため、CRDは削除中の状態（deletionTimestampが設定されているが存在する状態）で留まってくれていました。CRDが削除されなかったので、istio-ingressgatewayなどのPodはかろうじて残っており、サービス影響はありませんでした。不幸中の幸いです。しかしながら、その影響でistio-ingressgatewayなどのリソースを変更することができない状態になってしまいました。この状態を解消するためにIstio OperatorのCRとCRDを安全に再作成することにしました。実施した復旧手順は以下です。

1. baseディレクトリを使用せずに、各環境のディレクトリでControl Planeのすべてのリソースを定義し、manifest上でstg/prdの1-21-4のCRDを追加した。
2. istio-operatorのdeploymentのreplicasを0に変更し、Podを削除した。
  - `kubectl scale deployment istio-operator-1-21-4 --replicas=0 -n istio-operator`
3. IstioOperatorのCRとCRDを削除した
  - 具体的には、IstioOperator CRのfinalizersを空のスライスにした。
  - `kubectl patch istiooperator istio-control-plane-1-21-4 -n istio-system -p '{"metadata":{"finalizers":[]}}' --type=merge`
  - 削除されてしばらくすると、自動でCRとCRDが起動する。これは、手順1で追加したmanifest上の1-21-4のCRDをFluxが検知し、CRDを作成するため。

#### Step6でistiooperatorが削除されない
Step6の手順で、古いバージョンのistiooperatorリソースを手動で削除したのですが、削除されませんでした。これは、`kubectl edit`で該当リソースのfinalizerの値を空のスライスにすることで削除されるようになりました。ただし、deleteコマンドを実行してから、少し時間（数分ほど）待ってからeditしないと、予期せずゴミリソースが残ってしまうため、注意が必要でした。
いずれにせよ、今後はIstio Operatorがdeprecatedになるため、あまり気にする必要はなくなります。

## 最後に
本記事では、ZOZOでのIstio Upgradeの方針ややり方、そして今回のUpgrade実施内容について紹介しました。
Istioの運用をされている方の参考になれば幸いです。