Da Vinci Studio Blog

株式会社 Da Vinci Studio は 2023 年 7 月に株式会社 Zaim と統合し、株式会社くふう AI スタジオになりました

EKS 素人が DL 数1,000万以上のサービスのクラスターの k8s バージョンアップをやった話【後編】

Da Vinci Studio インフラ基盤部 SRE チームの林田です。
今回は前回わたくしがお送りした

EKS 素人が MAU ウン10万人以上のサービスのクラスターの k8s バージョンアップをやった話【前編】 - Da Vinci Studio Blog

の後編をお送り致します。

blog.da-vinci-studio.com


まずは前編のおさらい

前編では主に以下のことを書きました。

  • EKS (※ Amazon Elastic Kubernetes Service) の k8s (※ Kubernetes) バージョンアップとは
  • バージョンアップの事前準備について
  • 大変なところ

そして後編

前編では結びの言葉として以下のように書きました。

果たして無事サービスにダウンタイムを発生させることなくアップデート出来たのか!?
続きは【後編】に書こうと思います~ 。


そして結果どうだったのか、、?
無事ダウンタイムなくアップデート出来たのか!?







↓ スクロール


















残念ながらダメでした orz



正確に言いますと本番環境は大丈夫でしたが、ステージング環境でやらかしてしまいました。

「な~んだ、ステージング環境ならいいじゃん」

確かにそうなのですが、弊社では諸事情によりステージング環境が壊れると以下のように色々困ることがあります。

  • 本番のデプロイにステージング環境を使用している。
  • ステージング環境にて外部にテスト用途として公開している API がある。

特に前者に関しては弊社では1日に何回も本番のデプロイを行いますので
あくまでも関係者への影響に留まりますが、色んな方にご迷惑をお掛けしたので大変焦りました。



具体的にどんな失敗をしたか

今回アップデートした k8s バージョンですが、v1.21 から v1.22 にアップデートしました。
そしてここがまず勘違いだったのですが、今回の v1.22 へのアップデートにて
デフォルトコンテナランタイムが dockerd から containerd に変更されると思ってました。

※ 正確には次の次の v1.24 から変更されるみたいです。

docs.aws.amazon.com

Amazon EKS は、Kubernetes バージョン 1.24 のリリース以降、Dockershim のサポートも終了しました。
バージョン 1.24 以降、公式に公開される Amazon EKS AMI のランタイムは、containerd のみです。


※ コンテナランタイムとは?

kubernetes.io


とはいえ、前述のとおり勘違いしておりましたので
ワーカーノード(EC2インスタンス)起動時に
コンテナランタイムとして明示的に dockerd を使うよう
起動テンプレートの userdata を変更しました。

※ 起動テンプレートとは?

docs.aws.amazon.com


※ userdata とは?

docs.aws.amazon.com


具体的にどんな感じに変更したかと言うと、、、

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//
Content-Type: text/x-shellscript; charset="us-ascii"

#!/bin/bash

sed -i '/^CONTAINER_RUNTIME=/a CONTAINER_RUNTIME=docker' /etc/eks/bootstrap.sh

--//--


こんな感じに sed コマンドを利用して
bootstrap.sh の CONTAINER_RUNTIME という環境変数の値を dockerd に変更しました。

※ 補足ですが、 EKS でワーカーノードを起動すると
 上記の bootstrap.sh というシェルスクリプトが自動実行され必要な設定がセットアップされます。
 bootstrap.sh の中身はここで確認出来ます。  

github.com


図にすると以下のようになります。

これによりコンテナランタイムとして dockerd が選択されるはず、、


また今回はマイグレーション方式でワーカノードのアップグレードを行います。
つまり既存のノードグループを残したまま新しいバージョンのノードグループを作成します。
※ 余談ですが、この方式には障害発生時にロールバックが簡単に出来るという利点があります。


よって以下のように一時的に、v1.21 のノードと v1.22 のノードが共存する形になります。




さて無事構想を練ることが出来たので構成管理ツールのコードを更新します。
※ 弊社では構成管理ツールに terraform を使用しております。

※ terraform とは?

www.terraform.io

更新後のコードは以下になります。

# これは既存のノードグループ。何も変更しない
resource "aws_eks_node_group" "eks-worker" {
  cluster_name    = "hoge-cluster"
  node_group_name = "eks-worker"
  node_role_arn   = "arn:aws:iam::1234567890:role/role-eks-worker"
  launch_template {
    name = aws_launch_template.eks-worker-template.name
    version = aws_launch_template.eks-worker-teamplate.latest_version
  }
  tags            = {
    "Name" = "eks-worker"
  }

  version         = "1.21"

  subnet_ids      = [
    "subnet-xxxxxx",
    "subnet-yyyyyy",
  ]

  depends_on = [
    aws_launch_template.eks-worker-template
  ]

  # 以下略 ...
}

# 今回作成したノードグループ。分かり易いように suffix にバージョンを付けました。
resource "aws_eks_node_group" "eks-worker-v1-22" {
  cluster_name    = "hoge-cluster"
  node_group_name = "eks-worker-v1-22"
  node_role_arn   = "arn:aws:iam::1234567890:role/role-eks-worker"
  launch_template {
    name = aws_launch_template.eks-worker-template.name
    version = aws_launch_template.eks-worker-teamplate.latest_version
  }
  tags            = {
    "Name" = "eks-worker-v1-22"
  }

  # ここで新バージョンを指定
  version         = "1.22"

  subnet_ids      = [
    "subnet-xxxxxx",
    "subnet-yyyyyy",
  ]

  depends_on = [
    aws_launch_template.eks-worker-template
  ]

  # 以下略 ...
}

resource "aws_launch_template" "eks-worker-template" {
  name = "eks-worker-template"

  user_data = base64encode(<<EOF
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//

#!/bin/bash
set -o xtrace

# ここでコンテナランタイムに dockerd を指定
sed -i '/^CONTAINER_RUNTIME=/a CONTAINER_RUNTIME=docker' /etc/eks/bootstrap.sh

--//--
EOF
)

  instance_type = "m5a.large"

  block_device_mappings {
    device_name = "/dev/xvda"

    ebs {
      volume_size = 20
      volume_type = "gp3"
    }
  }

  # 以下略 ...
}

一応コードの説明をしておきますと、、、

  • 既存の aws_launch_template(起動テンプレート)eks-worker-templateuser_data を追加して、「明示的にコンテナランタイムに dockerd を指定」
  • 新しい aws_eks_node_group(ノードグループ)eks-worker-v1-22 を作成


これでコードの更新が完了したので apply します。
想定通り、v1.22 のノードグループが作成されているようです。

terraform apply

aws_launch_template.eks-worker-template: Modifying... [id=lt-0000xxxx11111]
aws_launch_template.eks-worker-template: Modifications complete after 0s [id=lt-0000xxxx11111]
aws_eks_node_group.eks-worker-v1-22: Creating...


処理が完了するまで暫く待ちます、、、、
ログを見て頂いたらお解りのように20分経過しても終わりません、、長い、、(;^_^A
EC2 インスタンスを数台起動しているからだと思われます。

aws_eks_node_group.eks-worker-v-1-22: Still creating... [22m50s elapsed]
aws_eks_node_group.eks-worker-v-1-22: Still creating... [23m0s elapsed]
aws_eks_node_group.eks-worker-v-1-22: Still creating... [23m10s elapsed]
aws_eks_node_group.eks-worker-v-1-22: Still creating... [23m20s elapsed]


まあ気長に待つか、、と思った矢先!
下記のエラーメッセージが表示されノードグループの作成に失敗してしまいました!

╷
│ Error: error waiting for EKS Node Group (hoge-cluster:eks-worker) version update (63fdf20b-90d1-3fc3-ac0c-2c0be351fa95): unexpected state 'Failed', wanted target 'Successful'. last error: 1 error occurred:
│       * : NodeCreationFailure: Couldn't proceed with upgrade process as new nodes are not joining node group eks-worker
│
│
│
│   with aws_eks_node_group.eks-worker,
│   on eks-node-group.tf line 1, in resource "aws_eks_node_group" "eks-worker":
│    1: resource "aws_eks_node_group" "eks-worker" {
│
╵
╷
│ Error: error waiting for EKS Node Group (hoge-cluster:eks-worker-v1-22) to create: unexpected state 'CREATE_FAILED', wanted target 'ACTIVE'. last error: 1 error occurred:
│       * i-0251b8f22831fd2ce, i-03698f503b86a2455, i-039eb2b2b8a621eef, i-05125f93bf0fe1a30, i-069da1793770b10e0, i-0c17ae0f26687f17f: NodeCreationFailure: Instances failed to join the kubernetes cluster
│
│
│
│   with aws_eks_node_group.eks-worker-v1-22,
│   on eks-node-group.tf line 29, in resource "aws_eks_node_group" "eks-worker-v1-22":
│   29: resource "aws_eks_node_group" "eks-worker-v1-22" {
│
╵


AWS の Web コンソールを見てノードグループの状態を確認してみます。
がしかし、「Instances failed to join the kubernetes cluster」というエラーメッセージのみ、、
※ う~~ん、なんて不親切なんだ、、


このあと何度か(※おそらく5、6回)コードを修正してリトライするも失敗を繰り返します。
エラーメッセージは相変わらず、、

NodeCreationFailure: Instances failed to join the kubernetes cluster


そこで、userdata の書き方、特に sed コマンドの書き方に間違いがあるのかと思い
試しにコメントアウトしてみました。

  user_data = base64encode(<<EOF
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//

#!/bin/bash
set -o xtrace

# コメントアウト
# sed -i '/^CONTAINER_RUNTIME=/a CONTAINER_RUNTIME=docker' /etc/eks/bootstrap.sh

--//--
EOF
)


すると、、
なんと無事成功!
やはり sed コマンドが間違っていたようでした。

Apply complete! Resources: 1 added, 2 changed, 0 destroyed.


一体なにがおかしいのだろう?とソースコードを眺めていると
実にしょ~もないミスをしていることに気づきました。

sed -i '/^CONTAINER_RUNTIME=/a CONTAINER_RUNTIME=docker' /etc/eks/bootstrap.sh


そう、docker ではなく dockerd って書かないといけないんですね~
いや~しょ~もない

× CONTAINER_RUNTIME=docker
〇 CONTAINER_RUNTIME=dockerd


さ~て、原因もわかったことだしリトライするか~と思った矢先、、
アプリケーション開発チームの方からチャットで問い合わせが来ました。


(-_-)「なんかステージング環境繋がらないんだけど何かやってます?」
俺「ふぁっ!?」


私の方でもブラウザから特定のサービスにリクエストを投げてみました。
すると、、


恐怖の 502 Bad Gateway 発生です。
ぎゃ~~~~(+o+)


これは想定外でした。
というのも今回は前述したとおりマイグレーション方式を取っているため
既存のノードグループは変更されないので、その上に乗っているサービスには影響しないはずです。


状況を理解出来ずにいると、あることに気づきます、、
terrafrom のログを確認してみると、、、

Apply complete! Resources: 1 added, 2 changed, 0 destroyed.


あれ?「2 changed」ってなってる!? Why !?


ここでおさらいになりますが、今回行った変更は下記2点です。

  • 新しいノードグループの作成
  • 既存の launch template の変更


なので

1 added, 1 changed

となると思っていたのに、、、現実は、、、

1 added, 2 changed


一体何が変更されてしまったのかと思い、さらにログを遡ってみます、、、
すると!

aws_eks_node_group.eks-worker: Modifying... [id=hoge-cluster:eks-worker]
aws_eks_node_group.eks-worker-v1-22: Still creating... [10s elapsed]
aws_eks_node_group.eks-worker: Still modifying... [id=hoge-cluster:eks-worker, 10s elapsed]
aws_eks_node_group.eks-worker-v1-22: Still creating... [20s elapsed]
aws_eks_node_group.eks-worker: Still modifying... [id=hoge-cluster:eks-worker, 20s elapsed]
aws_eks_node_group.eks-worker-v1-22: Still creating... [30s elapsed]
aws_eks_node_group.eks-worker: Still modifying... [id=hoge-cluster:eks-worker, 30s elapsed]


注目↓↓↓

aws_eks_node_group.eks-worker: Modifying... [id=hoge-cluster:eks-worker]


何ということでしょう、、、 、 既存のノードグループも意図せず更新されちゃってます。

原因を探ります。
ここでまたもやおさらいですが前述した構成図を見返してみます。


前述した通り今回変更した launch template は、既存のノードグループも利用しているものです。
よってそれを変更したがために既存のノードグループにも変更がかかったということですね~
納得。
※ 単に自分が無知であったため、userdata の内容を変えただけでノードグループの変更がかかると思っていなかっただけです。はい。


がしかし、ここで新たな疑問が生まれました。
というのもノードグループに変更がかかっただけで、502 Bad Gateway になるのは不可解だからです。


というのも AWS のマネージドノードグループに関するドキュメントを読んでみると
ノードグループに変更がかかった場合下記のように自動で処理が行われるようだからです。

  • 配下の ノード(※EC2 インスタンス)が自動で新しい物に1台づつ入れ替わる
  • 乗っている Pod は自動で drain される

docs.aws.amazon.com


[Update strategy] (更新戦略) で、次のいずれかのオプションを選択します。

[Rolling update] (ローリング更新) - このオプションは、クラスターの pod の中断予算を尊重します。pod 中断予算の問題により、Amazon EKS がこのノードグループで実行されている pods を正常にドレーンできない場合、更新が失敗します。

ここに書かれている通りローリングアップデートが行われるようなので、これなら 502 は発生しないはずです。


はて(・・?


とりあえず何が起こっているのか確認するためクラスターの状態を確認してみます。
すると、、

kubectl get pod --all-namespaces

NAMESPACE       NAME                            READY   STATUS                RESTARTS
default         hoge-pod-7b44b7cd-jcsdz         0/1     ImagePullBackOff      5
default         fuga-pod-d7f89f847-vbbkd        0/1     ImagePullBackOff      5
default         uga-pod-7b8c77d4bd-v9mqp        0/1     ImagePullBackOff      5


Pod のステータスが ImagePullBackOff になっている~~~!!(+o+)
Why!?


※ ImagePullBackOff ステータスとは?
kubernetes.io


kubernetes 公式ドキュメントによると

ImagePullBackOffステータスは、KubernetesがコンテナイメージをPullできないために、コンテナを開始できないことを意味します

とのことなので、要はコンテナイメージの Pull に失敗しているようです。


原因を探るためより詳細なログを見てみます。
すると、、

kubectl describe pod hoge-pod-7b44b7cd-jcsdz

Message
-------
Failed to pull image "nginx:1.2x": rpc error: code = Unknown desc = Error response from daemon: toomanyrequests: 
You have reached your pull rate limit. 
You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit



Docker Hub 「You have reached your pull rate limit.」
俺 「(゜゜)!!」


何ということでしょう、、
Docker Hub の Pull 回数制限に引っかかってしまったようです、、


※ Docker Hub の Pull 回数制限とは?

www.docker.com


匿名および無料の Docker Hub ユーザーは、6 時間あたり 100 および 200 のコンテナー イメージのプル リクエストに制限されています

匿名で使用する場合、6 時間あたり 100 個のコンテナ イメージ リクエストのレート制限


つまりこの小一時間くらいでいつの間にか私は、100回以上 Pull してしまったわけですね、、
はて?そんなに Pull した覚えがないぞと思いながらも少し記憶を辿って自分が行った行為を振り返ってみます。

  1. Launch Template の設定を書き換えました。
  2. 新ノードグループが作成を試みました。
  3. 旧ノードグループが意図せず再起動が走りました。
  4. エラーになり失敗しました。
  5. Launch Template の設定を修正しました。1に戻る

そう Launch Template の設定を何にも考えずに書き換えまくったせいで
ノードグループの再起動が何度もかかってしまったわけですね~~
当然乗っている Pod も再起動がかかりイメージの Pull も連発してしまうと。


さらに全 Pod 数を数えてみると、、、

kubectl get pod --all-namespaces | wc -l

363


この中でいくつかの Pod はイメージキャッシュが無効になっていたようであえなく 100 を超えてしまったというわけです。はい。


※ イメージキャッシュとは?

kubernetes.io

取り敢えず現状はっきりしていることは、、
これから 6 時間待たないと Pull 出来ない という事実です。


そんなに待ってられるかよ、ということで対策を取ります。


いくつか案が出ました。

  • Docker アカウントを作成し、ログインして Pull する。(※ 匿名での Pull を止める)
  • ECR に同イメージを Push して、そこから Pull する。


今回はより恒久的に安全な方法を取りたかったので後者の ECR に同イメージを Push して、そこから Pull する を選びました。


※ ECR (Amazon Elastic Container Registry) とは?

aws.amazon.com


これなら必要な IAM 権限さえあれば回数制限とか気にせず Pull 出来ます。


とは言え対象イメージ数が相当数ありましたので結構大変な作業でした。
一つ一つを Docker Hub からローカルに Pull して来てタグ付けして ECR に Push しないといけない。
※ これについては詳細を別途ブログにしようと思います。


教訓

そもそも Launch Template を弄る時は下記のような構成にすべきでした。


ちゃんとそれぞれのノードグループ用の Launch Template を用意する形ですね~~
これなら既存のノードに影響を与えない。

おしまい

今回のステージング環境での失敗もあり
後日の本番環境での作業は万全の状態で臨むことが出来ました。
結果ユーザー様に影響を与えるような大きなアクシデントなくアップデートを終えることが出来ましたとさ~


めでたしめでたし!


Da Vinci Studioでは、働く仲間を募集しています!
興味のある方は こちら か recruit@da-vinci-studio.net までご連絡ください。

da-vinci-studio.com