spacelyのブログ

Spacely Engineer's Blog

ECS でオートスケーリングを設定しました!

はじめに

はじめまして、スペースリーでインフラエンジニアをしている久下です。
アーロンチェアを買おうかずっと迷っていたのですが先日、中古品を購入しました。
想像より状態も良くて、リモートワーク環境がより整って良い感じになりました。
さて、この記事では ECS のオートスケーリングについて書いていきたいと思います。

なぜやったか

2024 年 6 月にスペースリーの Rails アプリケーションを EC2 から ECS へ移行しました。
EC2 時代は負荷が高くなった際はスケールアウトを手動で行っていました。
ECS では自動でスケールイン、スケールアウトを行いたいということになりました。

やったこと

負荷によるオートスケーリング

ECS へ移行した初期段階から Rails アプリケーションのコンテナと、ジョブを実行する Sidekiq のコンテナをそれぞれオートスケールするように設定しました。
Rails コンテナは CPU 使用率によってオートスケールするようにしました。
また、Sidekiq コンテナは Redis のキューサイズによってオートスケールするように設定しました。
CPU 使用率は ECS のメトリクスがあるのでそちらを使うことができました。
しかし、Redis のキューサイズは Sidekiq の管理画面から確認することはできましたが、CloudWatch のメトリクスとしては取得していませんでした。
そこで EventBridge で Lambda を定期実行し Redis のキューサイズを CloudWatch のカスタムメトリクスに登録することにしました。
環境変数や Sidekiq の設定ファイルは別途必要となりますが、下記のように実装しました。

require 'logger'
require 'time'
require 'json'
require 'yaml'
require 'sidekiq/api'
require 'aws-sdk-cloudwatch'

module LambdaFunctions
  class Handler
    class << self
      def process(event:, context:)
        logger = Logger.new($stdout)
        logger.formatter = proc do |severity, datetime, _, msg|
          request_id = context.aws_request_id
          # ISO 8601 形式の日時に変換する
          # 2024-05-07 05:43:09 +0000 -> 2024-05-07T05:43:09.399Z
          formatted_datetime = datetime.utc.iso8601(3)
          {
            timestamp: formatted_datetime,
            level: severity,
            requestId: request_id,
            message: msg
          }.to_json
        end

        configure_sidekiq

        total_queue_size_result = total_queue_size
        logger.info({ total_queue_size: total_queue_size_result })

        put_metric_data(total_queue_size_result)
      end

      private

      def configure_sidekiq
        redis_url = "redis://#{ENV['ELASTICACHE_PRIMARY_ENDPOINT']}:6379"
        Sidekiq.configure_client do |config|
          config.redis = { url: redis_url, namespace: 'sidekiq' }
        end
      end

      def total_queue_size
        config = YAML.load_file('config/sidekiq.yml')
        config[:queues].map do |queue|
          Sidekiq::Queue.new(queue).size.to_i
        end.sum
      end

      def put_metric_data(queue_size)
        cloudwatch = Aws::CloudWatch::Client.new(region: ENV['REGION'])

        cloudwatch.put_metric_data(
          {
            namespace: 'App/Redis',
            metric_data: [
              {
                metric_name: 'TotalQueueSize',
                dimensions: [
                  {
                    name: 'SidekiqProcessName',
                    value: 'Web'
                  }
                ],
                value: queue_size
              }
            ]
          }
        )
      end
    end
  end
end

スケジュールによるオートスケーリング

第 2 段階として、夜間はアクセスが少ないためタスクの最小数を減らすことにしました。
スケジュールに基づくスケーリングは、本記事の執筆時点ではマネージメントコンソールからは設定できません。
弊社では AWS のリソースは Terraform で管理しているため、特に問題はありませんでした。
Terraform では下記のようなイメージで設定が可能です。

resource "aws_appautoscaling_scheduled_action" "scale_in" {
  name               = "scale-in"
  service_namespace  = "<service_namespace>"
  resource_id        = "<resource_id>"
  scalable_dimension = "<scalable_dimension>"

  # 毎日23:00 JSTにスケールインする
  schedule = "cron(0 23 * * ? *)"
  timezone = "Asia/Tokyo"

  scalable_target_action {
    min_capacity = 2
    max_capacity = 10
  }
}

resource "aws_appautoscaling_scheduled_action" "scale_out" {
  name               = "scale-out"
  service_namespace  = "<service_namespace>"
  resource_id        = "<resource_id>"
  scalable_dimension = "<scalable_dimension>"

  # 毎日09:00 JSTにスケールアウトする
  schedule = "cron(0 9 * * ? *)"
  timezone = "Asia/Tokyo"

  scalable_target_action {
    min_capacity = 3
    max_capacity = 10
  }
}

検討したこと

Lambda をどの言語で実装するか

Python

  • 既存の Lambda 関数で使われていることが多いです。

Ruby

  • Web アプリケーションで使っている言語です。

今回は同じライブラリが使えることを重視し Ruby を採用しました

キャッチアップしたこと

今年スペースリーに入社した状況で、本対応を進めるにあたっていくつかキャッチアップが必要でした。

lambroll

スペースリーでは Lambda のデプロイに lambroll を使っています。 lambroll を使うのが初めてだったため、解説本(https://zenn.dev/fujiwara/books/ecspresso-handbook-v2)を購入してキャッチアップしました。

GitHub Actions

これまでの現場では CI/CD ツールは Jenkins や GitLab CI/CD を使うことが多かったです。 既存の Lambda 関数のワークフローの yaml ファイルの内容を理解することでキャッチアップしました。

Ruby

過去に何回か使った程度だったため、Ruby の入門書をざっと読みました。

つまづいたこと

Lambda 関数をパブリックサブネットに関連付けてもインターネットに直接接続できない

Lambda をテスト実行したところタイムアウトが発生しました。原因の調査に時間がかかりましたが、最終的には VPC なしの Lambda 関数で成功したため、本事象に気づきました。

Redis で namespace を指定しておらず、メッセージが溜まっている状況でもキューのサイズが 0 となった

redis-cli で調査をしたところ namespace を指定していなかったことが原因ということが分かりました。

どうなったか

下記は Datadog で作成している Rails の ECS サービスの CPU 使用率とタスク数のグラフです。
CPU 使用率が上昇すると ECS のタスク数が増加し、夜間の負荷が低い時間帯はタスク数が減少していることが確認できます。

Datadogのダッシュボード

また、下記は ECS のコストのグラフですが、夜間にタスクの最小数を減らすことによって 1 日あたり約 $17 のコストが削減できました。

Cost Explorer(サービス: Elastic Container Service)

最後に

EC2 から ECS への移行によって、リソースの柔軟な管理が可能になり効率的なシステム運用ができています。
ECS のオートスケーリングは設定することが多いと思いますので、何かの参考になれば幸いです。
最後に、スペースリーでは一緒に働いてくださる方を大募集中です。
少しでもご興味がある方は上記よりご連絡ください!