こしあん
2025-02-12

EventBridgeとLambdaによるFargateタスク状態通知


86{icon} {views}


EventBridgeルールを用いてFargateタスクの起動・終了を検知し、Lambdaで状態変化のログ出力や追加処理を実行できるようにする方法を紹介。Terraformコードを利用して、ECSの状態監視から通知までを一括で構築する具体的な手順と実例を示した。

はじめに

アーキテクチャー図

  • 抽象化して書くとこう。Fargateのタスクを起動したり終了したりすると、EventBridgeが反応してLambdaが起動する。

ポイント

EventBridgeのイベントルールの定義がポイントかな。このように書く

# 2. ECS Task State Change イベントルール
resource "aws_cloudwatch_event_rule" "ecs_task_state_change_rule" {
  name        = "ecs-task-state-change-rule"
  description = "ECS Task State Change rule for Fargate tasks"

  #※ 必要に応じて clusterArn のフィルター条件も追加してください。
  event_pattern = <<EOF
{
  "source": [
    "aws.ecs"
  ],
  "detail-type": [
    "ECS Task State Change"
  ],
  "detail": {
    "launchType": [
      "FARGATE"
    ],
    "clusterArn": [
        "${aws_ecs_cluster.this.arn}"
    ]
  }
}
EOF

実際に試してみる

Fargateのタスクを人工的に落としてドレインしてみる。Fargateのスポットの検証した結果をベースにする。

Lambdaに送られてくる通知

このように通知が送られてくる。

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/545993009f1f43768c33a4080ef933e0 state changed: lastStatus=DEACTIVATING, desiredStatus=STOPPED

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/ed7492378c6f47de880b83a6026c2c05 state changed: lastStatus=PROVISIONING, desiredStatus=RUNNING

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/ed7492378c6f47de880b83a6026c2c05 state changed: lastStatus=PENDING, desiredStatus=RUNNING

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/ed7492378c6f47de880b83a6026c2c05 state changed: lastStatus=ACTIVATING, desiredStatus=RUNNING

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/ed7492378c6f47de880b83a6026c2c05 state changed: lastStatus=RUNNING, desiredStatus=RUNNING

ECS Task arn:aws:ecs:ap-northeast-1:{account_id}:task/example-spot-cluster/545993009f1f43768c33a4080ef933e0 state changed: lastStatus=STOPPING, desiredStatus=STOPPED

Lambdaに送られてくるデータ

EC2と違って結構多い。

{
  "version": "0",
  "id": "324e261b-0557-66f9-f57b-b558ce5f48a4",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "****",
  "time": "2025-02-12T10:09:49Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:ecs:ap-northeast-1:****:task/example-spot-cluster/545993009f1f43768c33a4080ef933e0"
  ],
  "detail": {
    "attachments": [
      {
        "id": "b3f3b956-8b02-44f6-a4e5-86f47e15ac26",
        "type": "eni",
        "status": "ATTACHED",
        "details": [
          {
            "name": "subnetId",
            "value": "subnet-****"
          },
          {
            "name": "networkInterfaceId",
            "value": "eni-****"
          },
          {
            "name": "macAddress",
            "value": "0a:65:47:ad:53:a1"
          },
          {
            "name": "privateDnsName",
            "value": "ip-172-18-199-58.ap-northeast-1.compute.internal"
          },
          {
            "name": "privateIPv4Address",
            "value": "172.18.199.58"
          },
          {
            "name": "ipv6Address",
            "value": "2406:da14:1df3:c305:69c1:d048:ae53:653e"
          }
        ]
      },
      {
        "id": "f1a93209-7df7-4ab0-8fbf-e95d1dd5bd92",
        "type": "elb",
        "status": "ATTACHED",
        "details": []
      }
    ],
    "attributes": [
      {
        "name": "ecs.cpu-architecture",
        "value": "x86_64"
      }
    ],
    "availabilityZone": "ap-northeast-1c",
    "capacityProviderName": "FARGATE_SPOT",
    "clusterArn": "arn:aws:ecs:ap-northeast-1:****:cluster/example-spot-cluster",
    "connectivity": "CONNECTED",
    "connectivityAt": "2025-02-12T10:01:00.524Z",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:ap-northeast-1:****:container/example-spot-cluster/545993009f1f43768c33a4080ef933e0/2654a5e5-3190-49d5-b9f4-8a9acf6e6d7c",
        "lastStatus": "RUNNING",
        "name": "streamlit",
        "image": "aminehy/docker-streamlit-app",
        "imageDigest": "sha256:b8dbc8ea8915538521730e32f48ce9b8a62a924290a38754a61109f9068049e5",
        "runtimeId": "545993009f1f43768c33a4080ef933e0-0422623190",
        "taskArn": "arn:aws:ecs:ap-northeast-1:****:task/example-spot-cluster/545993009f1f43768c33a4080ef933e0",
        "networkInterfaces": [
          {
            "attachmentId": "b3f3b956-8b02-44f6-a4e5-86f47e15ac26",
            "privateIpv4Address": "172.18.199.58",
            "ipv6Address": "2406:da14:1df3:c305:69c1:d048:ae53:653e"
          }
        ],
        "cpu": "0"
      }
    ],
    "cpu": "512",
    "createdAt": "2025-02-12T10:00:55.589Z",
    "desiredStatus": "STOPPED",
    "enableExecuteCommand": false,
    "ephemeralStorage": {
      "sizeInGiB": 20
    },
    "group": "service:fargate-service-spot",
    "launchType": "FARGATE",
    "lastStatus": "DEACTIVATING",
    "memory": "2048",
    "overrides": {
      "containerOverrides": [
        {
          "name": "streamlit"
        }
      ]
    },
    "platformVersion": "1.4.0",
    "pullStartedAt": "2025-02-12T10:01:21.899Z",
    "pullStoppedAt": "2025-02-12T10:01:46.759Z",
    "startedAt": "2025-02-12T10:02:10.628Z",
    "startedBy": "ecs-svc/****",
    "stoppingAt": "2025-02-12T10:09:49.02Z",
    "stoppedReason": "Task stopped by user",
    "stopCode": "UserInitiated",
    "taskArn": "arn:aws:ecs:ap-northeast-1:****:task/example-spot-cluster/545993009f1f43768c33a4080ef933e0",
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:****:task-definition/streamlit-task:11",
    "updatedAt": "2025-02-12T10:09:49.02Z",
    "version": 5
  }
}

コード

Lambdaの関数(lambda_function.py)

import json

def lambda_handler(event, context):
    # 受け取ったイベント全体をログ出力(デバッグ用)
    print(event)

    # イベントから 'detail' 部分を取得
    detail = event.get('detail', {})

    # ECSタスクのARNを取得
    task_arn = detail.get('taskArn', 'Unknown Task ARN')

    # タスクの最新の状態と、要求されている状態を取得
    last_status = detail.get('lastStatus', 'Unknown Status')
    desired_status = detail.get('desiredStatus', 'Unknown Desired Status')

    # ログにタスクの状態変化を出力
    print(f"ECS Task {task_arn} state changed: lastStatus={last_status}, desiredStatus={desired_status}")

    # 必要に応じた追加処理をここに記述できます
    # 例: 通知送信、DBへの記録など

    return {
        'statusCode': 200,
        'body': json.dumps('Task state processed successfully')
    }

Terraform(Lambda+EventBridge)

# 1. Lambda 関数 (必要な方のみ、すでに定義済みの場合は不要)
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = ".cache/lambda_function.zip"
}

resource "aws_lambda_function" "ecs_task_state_change_handler" {
  function_name    = "ecs-task-state-change-handler"
  runtime          = "python3.12"
  handler          = "lambda_function.lambda_handler"
  role             = aws_iam_role.lambda_exec.arn
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = filebase64sha256(data.archive_file.lambda_zip.output_path)
  timeout          = 5
}

# Lambda 実行ロール(最小例)
resource "aws_iam_role" "lambda_exec" {
  name = "lambda_exec_role"

  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json
}

data "aws_iam_policy_document" "lambda_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

# ログ出力用などにポリシーをアタッチ(ここでは AWS 管理ポリシーをアタッチする簡易例)
resource "aws_iam_role_policy_attachment" "lambda_exec_attach" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# 2. ECS Task State Change イベントルール
resource "aws_cloudwatch_event_rule" "ecs_task_state_change_rule" {
  name        = "ecs-task-state-change-rule"
  description = "ECS Task State Change rule for Fargate tasks"

  #※ 必要に応じて clusterArn のフィルター条件も追加してください。
  event_pattern = <<EOF
{
  "source": [
    "aws.ecs"
  ],
  "detail-type": [
    "ECS Task State Change"
  ],
  "detail": {
    "launchType": [
      "FARGATE"
    ],
    "clusterArn": [
        "${aws_ecs_cluster.this.arn}"
    ]
  }
}
EOF
}

# 3. イベントターゲットとして Lambda を指定
resource "aws_cloudwatch_event_target" "ecs_task_state_change_target" {
  rule      = aws_cloudwatch_event_rule.ecs_task_state_change_rule.name
  target_id = "ecsTaskStateChangeLambda"
  arn       = aws_lambda_function.ecs_task_state_change_handler.arn

  depends_on = [aws_lambda_permission.allow_eventbridge_to_invoke_lambda]
}

# 4. EventBridge による Lambda 呼び出しを許可
resource "aws_lambda_permission" "allow_eventbridge_to_invoke_lambda" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.ecs_task_state_change_handler.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.ecs_task_state_change_rule.arn
}

Terraform(ALB)

# 外部から与えられるパブリックサブネットのID
variable "public_subnet_ids" {
  type        = list(string)
  description = "ALBを配置するパブリックサブネットのリスト"
}

# 外部から与えられるプライベートサブネットのID
variable "private_subnet_ids" {
  type        = list(string)
  description = "ECSを配置するプライベートサブネットのリスト"
}

variable "vpc_id" {
  type        = string
  description = "VPC の ID"
}

# Route53 ドメイン関係
variable "hosted_zone_name" {
  type        = string
  description = "既存の Route53 Hosted Zone 名 (ex: example.com.)"
}

# ACM 証明書 ARN (*.example.comのようにワイルドカードつけてリクエストしておく)
variable "acm_certificate_arn" {
  type        = string
  description = "既に登録済みの ACM 証明書 ARN(東京リージョン)"
}

# ─────────────────────────────────────────────────────────────
# ALB用Security Group 
# ─────────────────────────────────────────────────────────────
resource "aws_security_group" "alb_sg" {
  name   = "alb-sg"
  vpc_id = var.vpc_id

  # インターネットの HTTP, HTTPSを許可
  ingress {
    description      = "Allow HTTP"
    from_port        = 80
    to_port          = 80
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  ingress {
    description      = "Allow HTTPS"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  # アウトバンドを全許可
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = -1
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}


# ─────────────────────────────────────────────────────────────
# ALB の作成
# Public Subnet に配置し、上記 EC2 を Targets として登録
# ─────────────────────────────────────────────────────────────
resource "aws_lb" "alb" {
  name               = "alb"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = var.public_subnet_ids

  # 必要に応じてアクセスログの設定など適宜追加
  tags = {
    Name = "alb"
  }
}

# HTTP リスナー (ポート80)
# すべてのリクエストをHTTPSにリダイレクトする
resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# HTTPS リスナー (ポート443)
# 証明書ARNとポリシーを指定し、ターゲットグループへ転送する
resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = var.acm_certificate_arn
  ssl_policy        = "ELBSecurityPolicy-2016-08"

  default_action {
    type = "fixed-response"
    fixed_response {
      content_type = "text/plain"
      message_body = "Hello ALB"
      status_code  = "200"
    }
  }
}

# ─────────────────────────────────────────────────────────────
# Route53 のHosted Zone は既存を仮定 (あるいは新規作成でもOK)
# data で取得したり、resource で作成したりする
# ─────────────────────────────────────────────────────────────
data "aws_route53_zone" "main" {
  # 既存のドメインを使う例
  name         = var.hosted_zone_name
  private_zone = false
}

# Aレコード
resource "aws_route53_record" "wildcard_record" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "*.${var.hosted_zone_name}"
  type    = "A"

  alias {
    name                   = aws_lb.alb.dns_name
    zone_id                = aws_lb.alb.zone_id
    evaluate_target_health = false
  }
}

所感

  • スマートで結構いい感じ。Fargateの場合は結構いっぱい情報が送られてきてびっくりした
  • スポットのロギングをいちいちヘルスチェック打ち続けなくていいらしい


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です