
Terraform+Gitで実現!無停止でEKSを2バージョンアップ-検証/作業編-
はじめに
こんにちは。株式会社SHIFT サービスプラットフォームグループの武井です。
この記事では前回紹介したEKSのアップグレードの準備を踏まえて、実際のアップグレード検証で起こった事象と最終的な作業手順をご紹介します。
前回の記事はこちらをご覧ください。
アップグレード検証
検証に当たり注意したこと
手始めに、サンドボックス環境でアップグレードの検証を実施しました。
基本的に壊しても影響のない環境ではありますが、以下の点から慎重に検証を進める必要がありました。
環境再作成のリスク
無停止でのアップグレードを目指すにあたり、アプリケーションの挙動を確認しながらの検証は必須です。
EKS単体であれば作って壊してが容易でしたが、アプリケーションを含めたシステム全体の作り直しはそれなりに時間がかかります。
EOLが迫っている、且つ入社直後で全体像を把握していないということもあり、なるべく環境の再作成は避けたいと考えました。
ダウングレードができない
K8sではダウングレードを行うことはできません。
それはEKSでも例外ではなく、公式ドキュメント にも重要なポイントとして案内がされています。
クラスターをアップグレードすると、以前のバージョンにダウングレードすることはできません。新しい Kubernetes バージョンに更新する前に、「EKS の Kubernetes バージョンライフサイクルを理解する」で情報を確認し、さらに本トピック内の更新手順でも確認することをお勧めします。
従って、バージョンアップ自体は成功したけど途中でアプリケーションが止まってしまった、などの失敗ができないことになります。(今回は2バージョン上げるため、1回は失敗しても良いという計画を立てました)
とりあえずやってみよう (失敗)
検証実施
とはいえ実際に動かしてみないことには細かな挙動を把握することはできません。
一度失敗する覚悟でサンドボックス環境でアップグレードを実施してみました。
環境
EOLが迫っていたEKS v1.28の環境
K8sリソースを含めてTerraform管理している
TerraformはGitLabで管理
ワークロードはEC2のManaged Node Group
準備編の記事でも記述した通り、私たちのチームはK8sのリソースも含めて全てTerraformで管理しています。
まずはaws_eks_clustermoduleのcluster_versionの値を変更してterraform applyをかけてみました。
私たちの環境では、以下のようにk8s_versionという変数が宣言されていてdefaultに設定された値が適用されます。
つまり、以下のような場合は1.29が適用されます。
# main.tf
module "eks" {
# source/version
source = "terraform-aws-modules/eks/aws"
version = "19.6.0"
# cluster
cluster_name = "cat-${terraform.workspace}"
cluster_version = var.k8s_version ## ←クラスタバージョンに関する変数
vpc_id = data.terraform_remote_state.vpc_subnets.outputs.vpc_id
subnet_ids = data.terraform_remote_state.vpc_subnets.outputs.private_subnets
cluster_endpoint_public_access = true
.
.
.
# variables.tf
.
.
.
variable "k8s_version" {
description = "Kubernetes cluster version"
default = "1.29" ## 1.28 → 1.29 に変更
}
しかしながら、この方法では無停止でアップグレードできないことが判明します。
何が起こったか
コントロールプレーンのアップグレード終了と同時に全NodeGroupのアップグレードが自動で走った
アップグレードの間、Podがすべて停止する動きを観測した。(数秒~数十秒で復帰)
k9s を使ってPodの観察を行いながらアップグレードを行っていたのですが、綺麗に真っ赤に染まってしまいました・・・(画面キャプチャしておけばよかった)
アップグレード自体は成功したものの、無停止という要件はこのままでは満たすことはできません。
方法を再度検討するためにもう一度terraformのコードを確認したところ、以下の見落としが判明しました。
# main.tf
.
.
.
module "eks_managed_node_group" {
for_each = var.node_groups
# source
source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group"
# node group
name = "cat-${terraform.workspace}-${each.key}"
use_name_prefix = false
cluster_name = module.eks.cluster_name ## ←上述したaws_eks_cluster moduleのcluster_versionを見ている
cluster_version = module.eks.cluster_version
subnet_ids = local.node_group_subnet_ids[each.key]
eks_managed_node_groupモジュールのcluster_versionも、同じmain.tf内のeksモジュールで定義されているcluster_versionを参照していることがわかります。つまり、Node Groupのバージョン定義もコントロールプレーンと同じ変数を参照する作りになっており、コントロールプレーンに引きずられる形でアップグレードがかかってしまいます。また、Node Groupのスケーリング機能(desire sizeの調整)についても特に考慮せずにアップグレードをかけてしまったためにPodのスケジューリングが上手くできずに停止時間を発生させてしまいました。(サンドボックス環境で良かった)
ちなみに、Node Groupのスケーリング機能に任せることも考えましたが、以下の理由から別のやり方が良いと判断しました。
最適なサイズに調整するまで追加の検証が必要
インフラ(Node Group)の挙動に引っ張られてアプリケーションのスケールインが走るのは望ましい姿ではない
アップグレードが完了するまで待機するしかないのは想定外事象の対応ができず、心理的負荷も高い
最終的にどのような方法をとったか
この時点でサンドボックス環境での検証のチャンスは残り1回です。
環境の再作成のタスクが増えることにならないよう、慎重に計画を立てました。
ここで再度アップグレードの要件を整理してみましょう。
EKS v1.28からv1.30にアップグレードする
アプリ無停止
なるべくコマンドの手動実行をせずTerraformで管理
Node Groupのアップグレードとアプリの移動を自身でコントロールする
このように書くとなかなかハードルが高そうですが、検討した結果以下4つのステップに分割してそれぞれterraform applyすることで実現できました。また、それぞれのステップごとにGitLabでタグを作成し、git checkoutとterraform applyで段階的にアップグレードと移行を進めます。
eks_managed_node_groupモジュールのcluster_versionの定義をコメントアウトした上で、variables.tfで宣言されたk8s_versionの値を上げる
Terraformで新規のNode Groupを作成し、アプリのtarget nodegroupを新規のNode Groupに向けて再デプロイ
アプリが全て新規のNode Groupに移動したことを確認し、古いNode Groupを削除
次章から、上記ステップごとの詳細な解説を行います。
各ステップの解説
Step1
k8s_versionの値を変更した上で、
# variables.tf
.
.
.
variable "k8s_version" {
description = "Kubernetes cluster version"
default = "1.30" ## 1.29→1.30
}
eks_managed_node_groupモジュールのcluster_versionをコメントアウトします。
# main.tf
.
.
.
module "eks_managed_node_group" {
for_each = var.node_groups
# source
source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group"
# node group
name = "cat-${terraform.workspace}-${each.key}"
use_name_prefix = false
cluster_name = module.eks.cluster_name
# cluster_version = module.eks.cluster_version ## ここをコメントアウトしVersionを宣言しないようにする
subnet_ids = local.node_group_subnet_ids[each.key]
この時点でupgrade-step1など判別しやすいようなタグを切り、そのタグにcheckoutした上ででterraform applyをかけます。
Step1完了時点で、以下のような構成になります。
コントロールプレーン: v1.30
旧Node Group: v1.29
アプリケーションの場所: 旧Node Group
Step2
続いて、新規で空のNodeGroupを構築します。
私たちはNode GroupとTarget Node Groupの定義を各環境のtfvarsに入れています。
既存で使用しているold-groupと同様のスペックでnew-groupを定義し、current_nodegroupをnew-groupに変更します。
current_nodegroup = "new-group"
node_groups = {
old-group = {
ami_type = "AL2_x86_64"
instance_type = ["m6a.large"]
capacity_type = "ON_DEMAND"
disk_size = 20
volume_type = "gp3"
desired_size = 2
min_size = 2
max_size = 5
},
new-group = {
ami_type = "AL2_x86_64"
instance_type = ["m6a.large"]
capacity_type = "ON_DEMAND"
disk_size = 20
volume_type = "gp3"
desired_size = 2
min_size = 2
max_size = 5
}
}
この時点でStep1同様upgrade-step2などタグを切り、そのタグにcheckoutした上でterraform applyをかけます。
新規のNode Groupは自動的にコントロールプレーンのバージョンに合わせて作成されるので、terraform apply完了時点で以下のようになります。ポイントとしてはアプリケーションの配置先を変えないまま新しいVersionの空のNode Groupを作成する点です。
コントロールプレーン: v1.30
旧Node Group: v1.29
新Node Group: v1.30
アプリケーションの場所: 旧Node Group
続いて、Node Groupの配置先を変えるためアプリの再デプロイを行います。
主要リソースのディレクトリ構成は以下の通 りです。
├── applications
├── aurora
├── cronjobs
├── eks
├── global-accelerator
├── ingresses
├── k8s-components
├── loadbalancers
├── modules
└── secrets
これまでの作業はeksディレクトリで作業を実施していましたが、applicationsディレクトリに移動してアプリの再デプロイ(terraform apply)を行います。
以下がapplications配下の、とあるアプリのディレクトリ構造です。
sample_app
├── conf
├── configmap.tf
├── datadog-logs.json
├── deployment.tf
├── env-vars
├── hpa.tf
├── outputs.tf
├── providers.tf
├── service.tf
└── variables.tf
deploymentをはじめとして、そのアプリに必要なK8sリソースがterraform applyによって全てデプロイされる仕組みです。
以下がdeployment.tfの抜粋です。
deployment.tf
resource "kubernetes_deployment_v1" "sample_app" {
spec {
replicas = var.hpa.min_replicas
selector {
match_labels = {
app = local.app_name
}
}
strategy {
type = "RollingUpdate"
rolling_update {
max_surge = "1"
max_unavailable = "0"
}
}
template {
.
.
.
spec {
.
.
.
node_selector = {
nodegroup = (var.nodegroup == null)? module.eks.current_nodegroup : var.nodegroup
再デプロイを行う際は、Rolling Updateの機能が働きます。 そのため、設定したreplicaの最低数を維持しながらの更新が可能です。(無停止で実施できます)
上記のdeployment.tfの記述に従うとすると、最低限1つのレプリカを保ったまま更新されます。
また、applicationsディレクトリ配下にもアプリごとにディレクトリが10以上分かれているのですが、-tオプションを使用しながらapplyすることで、こちら側である程度コントロールしながらアプリの移動をすることが可能です。
そして肝心のNode Groupのデプロイ先ですが、以下の流れで決定されます。
nodegroup変数がnullであるかどうかをチェック
nullである場合はStep2で示したtfvarsで定義されているcurrent_nodegroupに従ってnew-groupにアプリが再デプロイ
以下がapplicationsディレクトリ配下にあるvariables.tfの抜粋です。
default = nullで定義されており、特別なことがない限りnullを渡す運用にしています。
variable "nodegroup" {
type = string
description = "nodegroup name defined on eks/variables.tf/node_groups: if null, module.eks.current_nodegroup_tcm is applied"
default = null
}
アプリの移動完了時点で以下のような構成になります。
コントロールプレーン: v1.30
旧Node Group: v1.29
新Node Group: v1.30
アプリケーションの場所: 新Node Group
Step3
最後に旧Node Groupを削除して完了です。
tfvarsからold-groupの定義を消し、これまで同様upgrade-step3などタグを切り、そのタグにcheckoutした上でterraform applyします。
current_nodegroup = "new-group"
node_groups = {
new-group = {
ami_type = "AL2_x86_64"
instance_type = ["m6a.large"]
capacity_type = "ON_DEMAND"
disk_size = 20
volume_type = "gp3"
desired_size = 2
min_size = 2
max_size = 5
}
}
最終的な構成は以下のようになります。
コントロールプレーン: v1.30
新Node Group: v1.30
アプリケーションの場所: 新Node Group
アドオンなどのアップグレードもTerraformで行う場合は、Step1の前段でStep0として実行する必要がありますが、基本的に上記の3ステップで実現可能です。
また、複数バージョンを跨いだアップグレードを行う場合は、この3ステップを繰り返すだけです。
まとめ
前編 と合わせて長編になりましたが、まとめると以下の内容です。
実施環境
EOLが迫っていたEKS v1.28の環境
K8sリソースを含めてTerraform管理している
TerraformはGitLabで管理
ワークロードはEC2のManaged Node Group
アップグレード要件
EKS v1.28からv1.30にアップグレードする
アプリ無停止
なるべくコマンドの手動実行をせずTerraformで管理
Node Groupのアップグレードとアプリの移動を自身でコントロールする
実施手順
事前準備
Terraformのコード断面をステップごとに用意し、Gitのtagを切り替えながら段階的に実施
実施ステップ
Step1
コントロールプレーンのアップグレード
Step2
新規Node Groupを作成
アプリケーションのTarget Node Groupを新規Node Groupに変更
アプリケーションを再デプロイ
Step3
旧Node Groupを削除
また、この手順を組んでみて感じたメリットとデメリットも紹介します。
メリット
作業が全てGit管理されるので作業履歴を残すことができる
IaCで定義されているあるべき状態を維持したままアップグレードができる
作業実施時のヒューマンエラーのリスクを減らすことができる
デメリット
タグの作成が大変
ステップごとにタグを切っていくため、コードの修正作業などが煩雑になる
最後に
今回はTerraform+GitでのEKSアップグレードの方法をご紹介しました。
この記事がどなたかの役に立てば幸いです。
執筆者プロフィール:武井竜一
ネットワークエンジニアからキャリアをスタートさせ、OpenStackやOpenFlow系SDNを扱った仮想化基盤プロジェクトのリードエンジニア、ソリューションアーキテクトなどを経験し2024年9月にSHIFTに入社。KubernetesとAWSが好き。
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/
PHOTO:UnsplashのPlanet Volumes