見出し画像

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で段階的にアップグレードと移行を進めます。

  1. eks_managed_node_groupモジュールのcluster_versionの定義をコメントアウトした上で、variables.tfで宣言されたk8s_versionの値を上げる

  2. Terraformで新規のNode Groupを作成し、アプリのtarget nodegroupを新規のNode Groupに向けて再デプロイ

  3. アプリが全て新規の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:UnsplashPlanet Volumes