はじめに
はじめまして、スペースリーでインフラエンジニアをしている久下です。
アーロンチェアを買おうかずっと迷っていたのですが先日、中古品を購入しました。
想像より状態も良くて、リモートワーク環境がより整って良い感じになりました。
さて、この記事では 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 のタスク数が増加し、夜間の負荷が低い時間帯はタスク数が減少していることが確認できます。
また、下記は ECS のコストのグラフですが、夜間にタスクの最小数を減らすことによって 1 日あたり約 $17 のコストが削減できました。
最後に
EC2 から ECS への移行によって、リソースの柔軟な管理が可能になり効率的なシステム運用ができています。
ECS のオートスケーリングは設定することが多いと思いますので、何かの参考になれば幸いです。
最後に、スペースリーでは一緒に働いてくださる方を大募集中です。
少しでもご興味がある方は上記よりご連絡ください!