💰

夜間にAWS EKSのノードを0にしてコストを削減する

2024/12/04に公開

このエントリーは一休.com Advent Calendar 2024の4日目の記事になります。


EKSのコストを削減したい。この場合、ノードのコストをどう削減するか、ということになります。Spot Instanceを活用する方法があり得ますが、ノードが急停止したときの対策や、Spot Instanceを引き当てられなかったときの準備等、考えなければならないことが多いです。もちろんSavingsPlan購入をするという手もあるんですが、事業状況等を踏まえると、オンデマンドの利点を享受したいという場合もあるでしょう。
そこで、単純に夜間帯にEKSのノードを0台にする、という方法で、EC2インスタンスのランニングコストの削減を実現してみます。もちろん、本番環境でこのようなことはできません。テスト環境や開発者向けのクラスタなら、夜間帯に動いていなくても大きな問題にはならないでしょう。

前提は以下。

  • EKSのKubernetesのバージョンは最新
  • ノードグループはmanagedである。
  • eksctl使い、cluster.yamlという名前のyamlを使ってクラスタを定義し、ノードグループを管理している。
    • このyamlは、Githubのリポジトリで管理されている。
  • cluster名は、test-cluster

実現方法は以下。

  • このリポジトリのGithub Actionsで、workflowをスケジュール実行することで、23:00に0台にする。そして、翌7:00に復帰させる。
  • 具体的には以下。
    • 23:00に、以下を実行する。
      • aws eks update-nodegroup-config を実行し、ノードグループにNO_EXECUTEのtaint を付与する。
        • NO_EXECUTEを付与せずに、0台にスケールインしても、cluster autoscalerが各種リソースをスケジュールするのに必要な必要な台数に自動的に戻してしまいます。
      • eksctl scale nodegroupを実行し、nodes-minを0に、nodesを0にする。
    • 7:00に、以下を実行する。
      • aws eks update-nodegroup-config を実行し、23:00に付与したノードグループに付与したtaintのNO_EXECUTEの を削除する。
      • eksctl scale nodegroupを実行し、元の台数に戻す
  • ノードグループ名や復帰するときのの台数は、cluster.yamlから取得する

Github ActionsのWorkflowの定義

name: change eks cluster node group size

on:
  workflow_dispatch:
    inputs:
      mode:
        description: "mode"
        required: true
        type: string
  workflow_call:
    inputs:
      mode:
        required: true
        type: string

jobs:
  update-minsize:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Install eksctl
      run: |
       ARCH=amd64
       PLATFORM=$(uname -s)_$ARCH
       curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
       tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
       sudo mv /tmp/eksctl /usr/local/bin
 
    - name: create removing NO_SCHEDULE taints commands
      if: ${{ (inputs.mode == 'revive') }}
      uses: mikefarah/yq@master
      with:
        cmd: yq e '.managedNodeGroups[] | "aws eks update-nodegroup-config --cluster-name test-cluster --nodegroup-name \(.name) --taints \\"removeTaints={key=cost,value=true,effect=NO_EXECUTE}\\""' ./cluster.yaml > taints_exec.sh && chmod +x taints_exec.sh

    - name: create node group revive commands
      if: ${{ (inputs.mode == 'revive') }}
      uses: mikefarah/yq@master
      with:
        cmd: yq e '.managedNodeGroups[] | "eksctl scale nodegroup --cluster=test-cluster --name=\(.name) --nodes-min=\(.minSize) --nodes=\(.minSize)"' ./cluster.yaml > exec.sh && chmod +x exec.sh

    - name: create adding NO_SCHEDULE taints commands
      if: ${{ (inputs.mode == 'zero') }}
      uses: mikefarah/yq@master
      with:
        cmd: yq e '.managedNodeGroups[] | "aws eks update-nodegroup-config --cluster-name test-cluster --nodegroup-name \(.name) --taints \\"addOrUpdateTaints={key=cost,value=true,effect=NO_EXECUTE}\\""' ./cluster.yaml > taints_exec.sh && chmod +x taints_exec.sh

    - name: create node group 0 scale in commands
      if: ${{ (inputs.mode == 'zero') }}
      uses: mikefarah/yq@master
      with:
        cmd: yq e '.managedNodeGroups[] | "eksctl scale nodegroup --cluster=test-cluster --name=\(.name) --nodes-min=0 --nodes=0"' ./cluster.yaml > exec.sh  && chmod +x exec.sh
       
    - name: modify nodegroups taints
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: ${{ secrets.AWS_REGION }}
      run: ./taints_exec.sh

    - run: sleep 90 # nodegroupがactiveになるまで待つ

    - name: Update minSize for each node group
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: ${{ secrets.AWS_REGION }}
      run: ./exec.sh

nodecontrol.yamlという名前でこのWorkflowを保存します。このWorkflowは再利用されることを意図して定義しています。利用側はmodeという入力を渡します。modeがzeroなら、0台にスケールインする。modeが reviveなら、元の台数に復帰する、という使い方を想定します。

Workflowでは、コマンドを生成し、それを実行しています。modeが、zeroなら、0台にスケールインするためのコマンドを作成し、実行します。modeが reviveなら、元の台数に復帰するためのコマンドを生成し、実行します。

コマンドの生成というのは、yqを使って、cluster.yamlを解析し、定義されているノードグループから情報を抽出して、shファイルに保存することを示しています。

例えば、ノードグループにtaintを付与するには、以下のコマンドを実行する必要があります。

aws eks update-nodegroup-config --cluster-name test-cluster --nodegroup-name <ノードグループ名> --taints "addOrUpdateTaints={key=cost,value=true,effect=NO_EXECUTE}"

ここで、ノードグループ名を、cluster.yamlから取得することで、ノードグループの定義が変わっても、workflowを直す必要がなくなります。yqを使うと以下。

yq e '.managedNodeGroups[] | "aws eks update-nodegroup-config --cluster-name test-cluster --nodegroup-name \(.name) --taints \\"addOrUpdateTaints={key=cost,value=true,effect=NO_EXECUTE}\\""' ./cluster.yaml > taints_exec.sh && chmod +x taints_exec.sh

NO_EXECUTEtaintの削除や、eksctlを使ったスケールイン/アウトコマンドも同様です。
また、taintの操作と、スケールの操作の間に、90秒sleepしています。これは、taintの操作が完了し、ノードグループがactiveになるのを待っています。

少し簡略化していますが、cluster.yamlの定義は以下の通り。
managedNodeGroupsのnameminSizeをyqで抽出して、復帰時の台数を指定しています。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: test-cluster
  region: ap-northeast-1
  version: "1.31"

vpc:
  id: "vpc-xxxxx"
  cidr: "10.0.0.0/16"
  subnets:
    private:
      ap-northeast-1a:
        id: "subnet-aaaaaa"
        cidr: "10.0.1.0/22"

      ap-northeast-1c:
        id: "subnet-bbbbbb"
        cidr: "10.0.2.0/22"

      ap-northeast-1d:
        id: "subnet-ccccc"
        cidr: "10.0.3.0/22"
managedNodeGroups:
  - name: managed-ng-01
    amiFamily: AmazonLinux2
    instanceType: c5.xlarge
    desiredCapacity: 7
    minSize: 7
    maxSize: 8
  - name:  managed-ng-02
    amiFamily: AmazonLinux2
    instanceType: m5.2xlarge
    desiredCapacity: 2
    minSize: 2
    maxSize: 4

あとは、呼び出し元のWorkflowを定義します。以下は、ノードグループを0台にスケールインための定義です。

name: zero scale in

on:
  workflow_dispatch:
  schedule:
    - cron: '0 14 * * *' #JST 23時に0台にする

jobs:
  zeroscalein:
    name: zero scale in
    uses: ./.github/workflows/nodecontrol.yaml
    with:
      mode: 'zero'
    secrets: inherit

以下は、ノードグループを元の台数に戻すための定義です。

name: revive

on:
  workflow_dispatch:
  schedule:
    - cron: '0 22 * * *' # JST 07:00にnodeの台数を元に戻す

jobs:
  zeroscalein:
    name: revive
    uses: ./.github/workflows/nodecontrol.yaml
    with:
      mode: 'revive'
    secrets: inherit  

これで実際に動かしてみると、夜間帯にtest-clusterのノードグループのEC2が0台になっていることが確かめられました。


一休では、ともに良いサービスをつくっていく仲間を募集中です。クラウドインフラの運用やSREに興味がある方はぜひ募集ください。

https://hrmos.co/pages/ikyu/jobs/1693126708022206468

カジュアル面談もやっています。

https://www.ikyu.co.jp/recruit/engineer/

Discussion