Amazon ECSのタスクを常に新鮮に保つ仕組みをStep Functionsで

SREチームの藤原です。今回はAmazon ECSのサービス内のタスクを定期的に再起動することで、日々のメンテナンスコストを削減する話です。SRE連載 3月号になります。

3行でまとめ

  • ECS Fargateのタスクは時々再起動が必要
  • 人間が対応するのは面倒
  • Step Functionsを定期実行して常に新鮮なタスクに入れ換えて予防しよう

ECS Fargateのタスクは時々再起動する必要がある

ECS Fargateでサービスを運用していると、数ヶ月に一度ほどの頻度でこのようなお知らせがやってきます。

[要対応] サービス更新のお知らせ - AWS Fargate で実行されている Amazon ECS サービスの更新が必要です [Action Required] Service Update Notification - Your Amazon ECS Service Running on AWS Fargate Needs an Update

Fargateをホストしている基盤に更新があったりセキュリティパッチが当たった場合、その上で動いているタスクを起動し直す必要があるためです。

このお知らせを放置していても、予告された期限を過ぎると自動的に新しいタスクが起動し、今動いているタスクは終了するため、サービス自体はダウンタイムなく維持されます(されるはずです)。なので基本放置でよいかというと……これが原因で障害を起こした例が過去にありました。

  1. ECSが新しいタスクを起動しようとした
  2. ECR(Elastic Container Registry)からイメージが消えていた1などの理由で、新しいタスクが起動できなかった
  3. ECSは新しいタスクを起動しようとし続けたが、ひっそりと数日間失敗し続け…
  4. 新しいタスクが起動しないまま、ついに古いタスクが終了し、サービスのタスクが全滅した

今動いているタスクと同じタスク定義だとしても、そのタスク定義から新しいタスクが起動できるかは、実際に起動してみるまでは分からないのでした。

人間がやるのは面倒→機械にやらせましょう

サービス内のタスクを再起動するには、例えばaws cliで以下のようなコマンドを実行するだけです。--force-new-deployment オプションによって、サービス内のタスクを強制的に入れ換えます。簡単ですね。

$ aws ecs update-service \
     --service service_name \
     --cluster cluster_name \
     --force-new-deployment

しかしコマンドを一発打つだけの簡単な作業とはいえ、多数のAWSアカウントで多数のECSサービスを運用している場合、それら全てについて一つ一つCLIを実行するのも面倒です。

ECSサービスのタスクはいつ入れ替わってもよいように作られているべきです(そうでなければアプリケーションの作りを見直しましょう)。単にこの作業を自動化し、毎日再起動してしまえばよいでしょう。

デプロイ頻度が少ないサービスではとても長い間、数年間動き続けているタスクがあったりします。このようなサービスでは、実際に再起動が必要になったタイミングでタスクが新しく起動できるか不安になってしまいます(人間が)。

常に新鮮なタスクに入れ換えることで、以下の要因を取り除くことができます。

  • このタスクは起動できるだろうかという不安
  • お知らせが来てからタスクを再起動する作業
  • このようなお知らせに払う関心
    • 古い基盤で動いているタスクがひとつもなければ、更新が必要という通知も来ないはずです

Step FunctionsでECSサービスの再起動を実装

定期的にタスクを再起動する処理を実装する方法はいろいろ考えられますが、この程度の作業にaws cliやSDKを使ったLambdaなどを用意するのも大袈裟です。今回はStep FunctionsのAWS SDK統合を利用して実装してみました。

aws.amazon.com

実装するState Machineはごくシンプルなものです。リトライなどの考慮はありませんが、仮に失敗したところで次回の実行で成功すれば問題ないものなので、シンプルに徹しました。

このState MachineはTerraformで管理しています。

terraformのコードは以下のようになりました。ECSクラスター mycluster に存在しているECSサービス service1, service2 を再起動するState Machineをそれぞれ定義するものです。

locals {
  ecs_services = ["service1", "service2"] // サービス名
}

resource "aws_sfn_state_machine" "refresh-ecs-service" {
  for_each = toset(local.ecs_services)
  name     = "refresh-ecs-${each.value}"
  role_arn = aws_iam_role.refresh-sfn.arn // ecs:UpdateService できる権限を持ったrole
  definition = jsonencode({
    Comment = "ECS Service ${each.value} タスク入れ換え"
    StartAt = "UpdateService"

    States = {
      UpdateService = {
        Comment = "ECS Service ${each.value} refresh"
        End     = true
        Parameters = {
          Cluster            = "mycluster"
          Service            = each.value
          ForceNewDeployment = true  // 強制的にタスクを入れ換える
        }
        Resource = "arn:aws:states:::aws-sdk:ecs:updateService"
        Type     = "Task"
      }
    }
  })
}
  • aws_sfn_state_machineリソースを使います
  • State Machineの定義(definition)は、HCLで書いた定義をjsonencode関数でJSON文字列化して与えます
  • AWS SDK統合でECSのUpdateService APIを呼び出すリソース名は arn:aws:states:::aws-sdk:ecs:updateService です。uが小文字なことに気をつけてください

あとは、このState Machineを適宜EventBridgeから定期実行してやるだけです。

まとめ

今回は些細な工夫ですが、Amazon ECSサービス内のタスクを定期的に再起動する作業を自動化し、対応コストを減らす取り組みを紹介しました。

クラウドで動いているコンピューティングリソース(ECSタスク、オートスケールしているEC2のインスタンスなど)はあまり長期間動かしたままにせず、定期的に新しいものに交換していくことをお勧めします。結果的に人間が手を動かして対応するコストを減らすことに繋がることが多いためです。

カヤックでは楽をするためのに手を動かすことが好きなエンジニアを募集しています!


  1. ECRのライフサイクルルールで使用中のイメージが削除されてしまう問題とその対策については ecrm - Amazon ECRから不要イメージを安全に削除するOSSを作った も参照してください