😊

StepFunctionsからECS Taskを実行してコケたらSlack通知したい

2024/05/09に公開

cronジョブやone shotのジョブを実行する環境としてECS Taskを利用しています。
その際、リトライやタイムアウト、同時実行制御のことを考えるのが面倒なのでStepFunctionsで包むのはよくある使用法だと思います。
で、StepFunctionsがコケてエラーになったときはSlackに通知したい。
これもアルアルかなと。
ただ、このためにSlackでbotトークンを発行してLambdaを作って、とか面倒でやりたくない!
という強い気持ちでSNS + AWS Chatbot経由での通知を構築しました。
Terraformを使用します。

StepFunctions

ECSクラスタ名によって、ECSタスクのURLは変わります。
以下はsampleという名前のECSクラスタをap-northeast-1に立てた場合の例です。

locals {
  ecs_task_url       = "https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/sample/tasks/"
}

resource "aws_sfn_state_machine" "sample" {
  definition = jsonencode(
    {
      StartAt = "RunTask"
      States = {
        RunTask = {
          Type     = "Task"
          Resource = "arn:aws:states:::ecs:runTask.sync"
          Parameters = {
            LaunchType     = "FARGATE"
            Cluster        = aws_ecs_cluster.sample.arn
            TaskDefinition = replace(aws_ecs_task_definition.sample.arn, "/:\\d+$/", "")
            Overrides = {
              ContainerOverrides = [
                {
                  Name        = "sample"
                  "Command.$" = "$.command"
                }
              ]
            }
            NetworkConfiguration = {
              AwsvpcConfiguration = {
                AssignPublicIp = "ENABLED"
                SecurityGroups = [aws_security_group.sample.id]
                Subnets        = [aws_subnet.sample.id]
              }
            }
          }
          End = true
          Catch = [
            {
              ErrorEquals = [
                "States.ALL"
              ]
              Next = "ModifyOutput1"
            }
          ]
        }
        ModifyOutput1 = {
          Type = "Pass"
          Parameters = {
            "Cause.$" = "States.StringToJson($.Cause)"
          }
          Next = "ModifyOutput2"
        }
        ModifyOutput2 = {
          Type = "Pass"
          Parameters = {
            "Cause.$" = "States.StringSplit($.Cause.TaskArn, '/')"
          }
          Next = "ModifyOutput3"
        }
        ModifyOutput3 = {
          Type = "Pass"
          Parameters = {
            "Cause.$" = "States.Format('see ${local.ecs_task_url}{}/logs?region=ap-northeast-1', $.Cause[2])"
          }
          Next = "SNSPublish"
        }
        SNSPublish = {
          Type     = "Task"
          Resource = "arn:aws:states:::sns:publish"
          Parameters = {
            TopicArn = aws_sns_topic.sample.arn
            Message = {
              version = "1.0"
              source  = "custom"
              content = {
                "description.$" = "$.Cause"
                title           = "error has occurred"
                textType        = "client-markdown"
              }
            }
          }
          Next = "Fail"
        }
        Fail = {
          Type : "Fail"
        }
      }
    }
  )
  name     = "sample"
  role_arn = aws_iam_role.sample.arn
}

説明のため、Retry・Timeoutは設定していません。
RunTaskが失敗した場合は、全部CatchしてSNS経由でAWS Chatbotを起動し、Slackへ通知します。

RunTask失敗時のoutputは以下のような形式です。
ここから、ECSタスクIDを取り出したいので、StepFunctionsの組込み関数を使用します。

{
  "name": "RunTask",
  "output": "{\"Error\":\"States.TaskFailed\",\"Cause\":\"{\\\"Attachments\\\":[{\\\"Details\\\":[{\\\"Name\\\":\\\"subnetId\\\",\\\"Value\\\":\\\"subnet-00000000000000000\\\"},{\\\"Name\\\":\\\"networkInterfaceId\\\",\\\"Value\\\":\\\"eni-00000000000000000\\\"},{\\\"Name\\\":\\\"macAddress\\\",\\\"Value\\\":\\\"0a:0a:0a:0a:0a:0a\\\"},{\\\"Name\\\":\\\"privateIPv4Address\\\",\\\"Value\\\":\\\"10.0.0.1\\\"}],\\\"Id\\\":\\\"7975e065-b8b9-477d-b7f2-4ececfa1a5e3\\\",\\\"Status\\\":\\\"DELETED\\\",\\\"Type\\\":\\\"eni\\\"}],\\\"Attributes\\\":[{\\\"Name\\\":\\\"ecs.cpu-architecture\\\",\\\"Value\\\":\\\"arm64\\\"}],\\\"AvailabilityZone\\\":\\\"ap-northeast-1c\\\",\\\"ClusterArn\\\":\\\"arn:aws:ecs:ap-northeast-1:000000000000:cluster/sample\\\",\\\"Connectivity\\\":\\\"CONNECTED\\\",\\\"ConnectivityAt\\\":1715151108682,\\\"Containers\\\":[{\\\"ContainerArn\\\":\\\"arn:aws:ecs:ap-northeast-1:000000000000:container/sample/7446f4e4ff8441eabc8919fe51d8cac5/f5d4dacc-0c3f-43b5-a12d-3b6faf9b0ce5\\\",\\\"Cpu\\\":\\\"0\\\",\\\"ExitCode\\\":1,\\\"GpuIds\\\":[],\\\"Image\\\":\\\"000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/sample:latest\\\",\\\"ImageDigest\\\":\\\"sha256:478663e9244a6f8c1866ea8e59fdc3e8356e08cebb99e5fcc92b2d8dacf891b8\\\",\\\"LastStatus\\\":\\\"STOPPED\\\",\\\"ManagedAgents\\\":[],\\\"Name\\\":\\\"sample\\\",\\\"NetworkBindings\\\":[],\\\"NetworkInterfaces\\\":[{\\\"AttachmentId\\\":\\\"7975e065-b8b9-477d-b7f2-4ececfa1a5e3\\\",\\\"PrivateIpv4Address\\\":\\\"10.0.0.1\\\"}],\\\"RuntimeId\\\":\\\"7446f4e4ff8441eabc8919fe51d8cac5-3286335836\\\",\\\"TaskArn\\\":\\\"arn:aws:ecs:ap-northeast-1:000000000000:task/sample/7446f4e4ff8441eabc8919fe51d8cac5\\\"}],\\\"Cpu\\\":\\\"256\\\",\\\"CreatedAt\\\":1715151105029,\\\"DesiredStatus\\\":\\\"STOPPED\\\",\\\"EnableExecuteCommand\\\":false,\\\"EphemeralStorage\\\":{\\\"SizeInGiB\\\":20},\\\"ExecutionStoppedAt\\\":1715151119396,\\\"Group\\\":\\\"family:sample\\\",\\\"InferenceAccelerators\\\":[],\\\"LastStatus\\\":\\\"STOPPED\\\",\\\"LaunchType\\\":\\\"FARGATE\\\",\\\"Memory\\\":\\\"512\\\",\\\"Overrides\\\":{\\\"ContainerOverrides\\\":[{\\\"Command\\\":[],\\\"Environment\\\":[],\\\"EnvironmentFiles\\\":[],\\\"Name\\\":\\\"sample\\\",\\\"ResourceRequirements\\\":[]}],\\\"InferenceAcceleratorOverrides\\\":[]},\\\"PlatformVersion\\\":\\\"1.4.0\\\",\\\"PullStartedAt\\\":1715151117893,\\\"PullStoppedAt\\\":1715151119256,\\\"StartedAt\\\":1715151119431,\\\"StartedBy\\\":\\\"AWS Step Functions\\\",\\\"StopCode\\\":\\\"EssentialContainerExited\\\",\\\"StoppedAt\\\":1715151144686,\\\"StoppedReason\\\":\\\"Essential container in task exited\\\",\\\"StoppingAt\\\":1715151129486,\\\"Tags\\\":[],\\\"TaskArn\\\":\\\"arn:aws:ecs:ap-northeast-1:000000000000:task/sample/7446f4e4ff8441eabc8919fe51d8cac5\\\",\\\"TaskDefinitionArn\\\":\\\"arn:aws:ecs:ap-northeast-1:000000000000:task-definition/sample:6\\\",\\\"Version\\\":5}\"}",
  "outputDetails": {
    "truncated": false
  }
}

StepFunctions組込み関数

組込み関数をネストして使う、みたいなことができればよかったのですが、できなそうでした。
(StepFunctionsコンソールで保存しようとすると、バリデーションで落ちる)
諦めて複数ステップで変換します。

1. escape済みJSON文字列をJSONに変換する

   ModifyOutput1 = {
       Type = "Pass"
       Parameters = {
           "Cause.$" = "States.StringToJson($.Cause)"
       }
       Next = "ModifyOutput2"
   }

2. JSONからTaskArnを取り出し、'/'を区切り文字として配列に変換する

   ModifyOutput2 = {
       Type = "Pass"
       Parameters = {
           "Cause.$" = "States.StringSplit($.Cause.TaskArn, '/')"
       }
       Next = "ModifyOutput3"
   }
{
  "Cause": "arn:aws:ecs:ap-northeast-1:000000000000:task/sample/7446f4e4ff8441eabc8919fe51d8cac5"
}

{
  "Cause": ["arn:aws:ecs:ap-northeast-1:000000000000:task", "sample", "7446f4e4ff8441eabc8919fe51d8cac5"]
}

3. 配列からタスクID部分を取り出し、ECSタスクのログを表示するURLに変換する

   ModifyOutput3 = {
       Type = "Pass"
       Parameters = {
           "Cause.$" = "States.Format('see ${local.ecs_task_url}{}/logs?region=ap-northeast-1', $.Cause[2])"
       }
       Next = "SNSPublish"
   }
{
  "Cause": ["arn:aws:ecs:ap-northeast-1:000000000000:task", "sample", "7446f4e4ff8441eabc8919fe51d8cac5"]
}

{
  "Cause": "https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/sample/tasks/7446f4e4ff8441eabc8919fe51d8cac5/logs?region=ap-northeast-1"
}

4. SNSに渡して、AWS Chatbotのカスタム通知用に整形する

   SNSPublish = {
       Type     = "Task"
       Resource = "arn:aws:states:::sns:publish"
       Parameters = {
           TopicArn = aws_sns_topic.sample.arn
           Message = {
               version = "1.0"
               source  = "custom"
               content = {
                   "description.$" = "$.Cause"
                   title           = "error has occurred"
                   textType        = "client-markdown"
               }
           }
       }
       Next = "Fail"
   }

まとめ

CloudWatch LogsのURLを飛ばすことも考えたのですが、いったんは簡単に作れそうだったECSタスクのURLとしています。
ということで、無事にLambda不要でSlack通知できるようになりました。

Discussion