EKSをカナリアリリースでupdateした話 (1.20 -> 1.23)

# はじめに
先日、プロダクション環境のEKSをupdateしました
その際に得られた知見を共有します

※ベストプラクティスである保証はできません

## 元々の環境
- EKS v1.20
  - terraform管理
- Flux2 v0.13.0
  - Github Enterpriseと同期

## アップデート戦略
EKSのアップデート戦略については、以下の記事がとても参考になります
https://zenn.dev/nameless_gyoza/articles/how-to-update-eks-cluster-safely

ざっくり、以下の方法があります
- in-place update
- in-place update(control planeのみ) + node migration
- ブルーグリーンデプロイメント
- カナリアリリース

弊チームでは、以下の理由で**カナリアリリースを採用**しました

運用サービスのSLOが高い
→ 何かあった時、すぐ戻せる状態にしたい
→ 新規クラスターを作成し、切り替える方式が良さそう
→ ブルーグリーンはリスクが大きい (問題があるとサービスが止まる)
→ **カナリアリリースだ！**

しかし、**in-place updateの方が圧倒的に楽です**
具体的に、カナリア・ブルーグリーンは以下の手順が必要になります
- 各種リソース複製（リソース名・バージョンが若干異なるので、レビュー必要）
  - terraformリソース
  - CI config
  - k8sマニフェスト
- 動作確認
- 切り替え
- 後片付け（旧リソース削除）

---
また、カナリアリリースにおける重み付けは、ALBとRoute53のどちらかで実施することになります
https://dev.classmethod.jp/articles/alb-blue-green-deployment/

弊チームではALBを切り替えたくない事情があるため、**ALBで行う**ことにしました


# 事前準備
## EKS最新バージョン確認
EKSの最新バージョンがいくつか調べます
日本語だと情報が古いことがあるので、英語にして確認しましょう（1敗）
https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html

作業時（2022年9月）、最新は`v1.23.7`らしいです

## apirVersion確認
k8sリソースの`apiVersion`の差分を確認します

非推奨・削除を検出するツールである`Pluto`を用います
https://kakakakakku.hatenablog.com/entry/2022/07/20/091424

### Pluto インストール
```
$ brew install FairwindsOps/tap/pluto

$ pluto version
Version:5.10.6 Commit:0c3ca2eb1e34d03473948eb0dc82fc2cd6b8c9cf
```

### Pluto 実行
```
$ pluto detect-files -d . -o wide --target-versions k8s=v1.23.7
NAME      NAMESPACE   KIND                  VERSION          REPLACEMENT   DEPRECATED   DEPRECATED IN   REMOVED   REMOVED IN  
xxx-pdb   xxx         PodDisruptionBudget   policy/v1beta1   policy/v1     true         v1.21.0         false     v1.25.0     
yyy       yyy         CronJob               batch/v1beta1    batch/v1      true         v1.21.0         false     v1.25.0     
```

PDBとCronJobが非推奨バージョンを用いてるみたいです

以下のようにk8sマニフェストを修正すると良さそうです

- PodDisruptionBudget
  - policy/v1beta1 -> policy/v1 
- CronJob
  - batch/v1beta1 -> batch/v1

## addon バージョン確認
addonのバージョンを、以下コマンドを用いて確認します
```
$ aws eks describe-addon-versions --kubernetes-version 1.23
```

上記結果をもとに、以下のように修正すると良さそうです
- kube-proxy
  - v1.20.4-eksbuild.2 -> v1.23.7-eksbuild.1
- coredns
  - v1.8.3-eksbuild.1 -> v1.8.7-eksbuild.2
- vpc_cni
  - v1.7.10-eksbuild.1 -> v1.11.3-eksbuild.1


## Flux2 バージョン確認
弊チームのk8s環境は、Flux2を用いて`GitHub Enterpriseと同期しています。

Flux2がずっとアップデートできていないので、これを機にアップデートしていきます

```
$ brew upgrade fluxcd/tap/flux

$ flux --version
flux version 0.33.0
```

ローカルで準備ができたら、あとは以下コマンドで生成したリソースをapplyするだけです
（新クラスター完成後に行います）
```
flux install \
  --version=v0.33.0 \
  --namespace=flux-system \
  --components=source-controller,kustomize-controller,helm-controller \
  --components-extra=image-reflector-controller,image-automation-controller \
  --log-level=error \
  --export > crd.yaml
```

また、GHE連携できないバグがあるみたいです
(v0.31.0の時に再現しました)
https://github.com/fluxcd/source-controller/issues/652

上記issueによると、`source-controlle`の`v0.22.0`から発生しているようです。
うまく連携できない方は、上記搭載前の`v0.27.4`にすると良いかもしれません

また、私の環境だと`v0.33.0`で`v0.27.4`を`flux intall`することができなかったので、
`v0.27.4`をローカルインストールする方法を載せておきます

https://christina04.hatenablog.com/entry/install-old-version-with-homebrew

```
$ brew tap-new iwakirik/taps
$ brew extract fluxcd/tap/flux iwakirik/taps --version 0.27.4
$ brew install iwakirik/taps/flux@0.27.4
```

## カナリア用リソース名の考察
カナリアリリースのために、切り替え先のリソースを作る必要があります
こちらの名前について、最初はprefixに`canary`を付けていました
(例: xxx_cluster -> xxx_cluster_canary)

しかし、これだと、`*_canary`リソースが残ってしまう形になります

これを解消するには、**リリースを終えた後に**、以下のような作業が必要になります

### 方法1: カナリア用リソースを削除する方法
元クラスター→新クラスター→元クラスターで2回カナリアリリースを行うイメージです
- 元リソースを更新する
  - EKS version
  - apiVersion
  - addon version
  - flux2 version
- 元clusterの動作確認
- ALBの向き先を戻す（カナリアで？）
- 様子見
- 問題無さそうなら、カナリア用リリースの削除

### 方法2: カナリア用リソースを使い続ける方法
- 元リソースを削除する
- カナリア用リソースの名前・パラメータ・ディレクトリ構造を、元リソースに合わせる(戻す）
  - terraform import等の不毛な作業が必要

---

はい、リリース後の作業が沢山になっちゃいます

リリース完了したら、旧リソースを削除するだけで終わりにしたいです。
つまり、**カナリアリリース用のリソースだけが残っても自然な形**にしたいです。

そのため、**リソース名のprefixはversion名**にすることにしました
(例: xxx_cluster -> xxx_cluster_v1_23)

これだと、今後のupdateも自然なリソース名にすることができます
（`xxx_20221001`といった日付も良いと思います）

# 実作業
## リソース複製
各種リソースを複製しつつ、準備編で挙げたルール・バージョンで変更します。

弊チームでは、ざっくり以下リソースを複製・追記しました
- aws_eks_cluster
- aws_eks_node_group
- aws_iam_openid_connect_provider
- aws_launch_template
- aws_eks_addon
- aws_lb_target_group
- aws_autoscaling_attachment
- 各種k8sマニフェスト（Flux2 crd, Datadog等のHelm Chart含む）
- 各種CI設定

また、リソース複製から動作までの間に、いくつかトラブルに遭遇しました。
そちらの詳細と解決方法は後の章にまとめてあります。

## 動作確認
前手順で作成したクラスターで動作確認を行います。
ざっくり、主要な機能が動作するかといったことを検証しました。

また、以下のようなリスナールールを用意すると、
カナリアリリースの重み付けの中でもアクセスが任意で行えて便利でした。
```
resource "aws_lb_listener_rule" "<リソース名>" {
  listener_arn = aws_lb_listener.<リスナー名>.arn

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.<ターゲット名>.arn
  }

  condition {
    http_header {
      http_header_name = "canary"
      values           = ["true"]
    }
  }
}
```

## カナリアリリース
動作確認も終えたので、実際にカナリアリリースを行います。
以下の重み付け推移で、1日毎にリリースを行いました。

0.1% -> 1% ->10% -> 50% ->100%

## リソース削除
100%リリースから数日経過し、安定稼働を確認したら、旧リソースを削除します

# 遭遇したトラブル
ここからは、今回遭遇したトラブルと解決方法をまとめていきます。
たくさんリソースを生成するわけですから、どこかでミスは起こりそうですね。
初歩的なものも多いですが、自分への戒めとして残しておきます。

## アプリケーションが動かない
新しく生成したクラスターのsecurity groupが不足していたようです。
追加して対応しました。

## エラー：configmaps “aws-auth” already exists
k8s関連リソースの複製中に起こったエラーです。
どうやら、元々terraform管理していた`aws-auth`と、クラスター生成時に生成される`aws-auth`が衝突しちゃったようです。

とりあえず、手動削除して再applyで解決しようとします
```
$ aws eks update-kubeconfig --region <リージョン名> --name <クラスター名> --profile <プロファイル名>
$ kubectl delete cm aws-auth -n kube-system
error: You must be logged in to the server (Unauthorized)
```

また怒られちゃいました。
どうやら、デフォルトの`aws-auth`では、EKSを作成したユーザーしか権限を持っていないようです。

弊チームでは、terraformをGitHub管理し、CircleCIでterraform applyしています。
そのため、CircleCI userでしかterraform applyできません。

CircleCIにsshして上記手順を行い、解消しました。

## レスポンスタイム急上昇

アプリケーションの動作が取れた後、数分間レスポンスタイムが急上昇する問題が発生しました。
調査に時間をかけたり、サポートへ問い合わせたりしましたが、結局未解決でした…

原因が分からなかった大きな理由として、`ログを有効化していなかった`所にあります
（サポートから、「ログがないから調査できない」との返答がありました）

アプリレイヤーではログを取得してますが、ALBではログは有効にしていませんでした。
少なくとも安定稼働までは、各種リソースでログを有効にするのが良さそうですね。

# まとめ
以下の反省点がありました
- update戦略はちゃんと考える
  - リソース複製方式は想像よりずっと大変！
- 安定稼働までの間、ログを取る
  - 原因を特定できないリスク

この辺りを反省して、次の業務に活かしていきたいところです。

（マサカリあったら遠慮なくお願いします！）