0
Help us understand the problem. What are the problem?

posted at

Lambda(golang)でFargate Spotの終了通知を受けたECSのタスクをNLBから切り離す

初めに

生産技術部で製品の検査工程を担当しているエンジニアです。今回は、Fargate spot上のECSに中断通知が来た時のELBに対するDeregisterTargetの実行が保証されない課題に対して取り組みました。

Fargate Spotを利用する目的は、以下の資料にあるように、コスト削減が可能になるからです。
また、Fargateで立ち上げたECSのCapacity ProviderにFargateとFargate Spotを併用することで、システムの安定性とコスト削減を両立した仕組みを実現します。

Capacity Providerの導入

参考実装:

Clusterのデフォルト値、Serviceに実際に使用するCapacity Providerを設定します。Baseは最小実行タスク数を示し、以下の例では2つのタスクがFargateで実行されます。Weightは実行するタスクの総数に対する相対的な割合を示し、スケールアウトした場合でも、1対1になるようにFargateとFargate Spot上に配置されます。AWSコンソールからは、各タスクの設定を確認するとどちらを利用しているか確認できます。

注意するポイントは、ServiceにCapacityProviderStrategyとLaunchTypeを両方設定することは出来ないことです。

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: ecs
      CapacityProviders:
        - FARGATE
        - FARGATE_SPOT
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Base: 2
          Weight: 1
        - CapacityProvider: FARGATE_SPOT
          Base: 0
          Weight: 1
  Service:
    Type: AWS::ECS::Service
    Properties:
      CapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Base: 2
          Weight: 1
        - CapacityProvider: FARGATE_SPOT
          Base: 0
          Weight: 1

デフォルトで SIGTERM の 30 秒後に SIGKILL が発⾏されますが、 StopTimeout を設定することで120秒などの設定に変更できます。

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: logstash
          StopTimeout: 120

Fargate Spotの終了通知をLambdaで処理する

参考実装:

Event Bridgeから終了通知を受け取り、対象のタスクをNLBのターゲットから解除します。

  1. Event Bridgeからイベントを受け取り予め要した構造体に必要なデータを格納
  2. StopCodeが終了通知でなければ、Lambdaを終了
  3. 対象タスクのIPを取得
  4. 環境変数に設定したAWS::ElasticLoadBalancingV2::LoadBalancerのAmazon Resource Name (以下、ARN)、AWS::ElasticLoadBalancingV2::TargetGroupのARNを取得
  5. aws-lambda-go、aws-sdk-go-v2のライブラリを使用するための初期設定
  6. LoadBalancerのARNからLoadbalancerを取得
  7. TargetGroupのARNからTargetGroupを取得
  8. TargetGroupに対象タスクのIPがあれば登録を解除
type NetworkInterface struct {
	PrivateIpv4Address string `json:"privateIpv4Address"`
}
type Container struct {
	NetworkInterfaces []NetworkInterface
	Name              string `json:"name"`
}
type EcsEvent struct {
	StopCode   string `json:"stopCode"`
	Containers []Container
}

func HandleLambdaEvent(_ context.Context, event events.CloudWatchEvent) {
	// 1. Event Bridgeからイベントを受け取る
	var ecsEvent EcsEvent
	if err := json.Unmarshal(event.Detail, &ecsEvent); err != nil {
		os.Exit(1)
	}
	// 2. 終了通知かチェック
	fmt.Printf("stopCode = %s\n", ecsEvent.StopCode)
	if ecsEvent.StopCode != "TerminationNotice" {
		return
	}
	// 3. 対象タスクのIP取得
	var ecsIp string
	for _, contaier := range ecsEvent.Containers {
		if contaier.Name == "logstash" {
			for _, ni := range contaier.NetworkInterfaces {
				ecsIp = ni.PrivateIpv4Address
			}
		}
	}
	fmt.Printf("ip v4 = %s\n", ecsIp)
	// 4. 環境変数取得
	nlbId := os.Getenv("NlbId")
	nlbTargetGroupId := os.Getenv("NlbTargetGroupId")
	fmt.Printf("GET ENV AlbId: %s AlbTargetGroupId: %s\n", nlbId, nlbTargetGroupId)
	// 5. 初期設定
	svc := Init()
	// 6. 指定したLoadbalancerを取得
	lb := GetSpecifiedLoadbalancer(svc, nlbId)
	fmt.Printf("GET LoadbalancerName: %s LoadbalancerArn: %s\n", *lb.LoadBalancerName, *lb.LoadBalancerArn)
	// 7. 指定したLoadbalancerのTargetGroupを取得
	tg := GetSpecifiedTargetGroup(svc, lb, nlbTargetGroupId)
	fmt.Printf("GET TargetGroupName: %s TargetGroupArn: %s\n", *tg.TargetGroupName, *tg.TargetGroupArn)
	// 8. TargetGroupからTargetの登録を解除
	if HasTarget(svc, tg, ecsIp) {
		const tcpPort = 5044
		DeregisterSpecifiedTarget(svc, tg, ecsIp, tcpPort)
		fmt.Println("DEREGISTER")
	}
}

CloudformationにLambda、Event Bridgeのルールを追加

参考実装:

ECSのドキュメントに記載されている様にルールを設定しました。

  EventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: detach ecs task that received terminate notification from nlb
      Name: detach-task-to-be-terminated-from-nlb
      EventPattern:
        source:
          - aws.ecs
        detail-type:
          - ECS Task State Change
        detail:
          clusterArn:
            - !Ref ClusterId
      State: ENABLED
      Targets:
        - Arn: !GetAtt Function.Arn
          Id: lambda

最後に

Lambdaのテストを使って対象のタスクがNLBから削除されることを確認しました。ECS上で動いているアプリのGraceful Shutdownについては、利用するアプリのドキュメントを参照して対処するのが良いかと思います。今回利用するLogstashについては以下に記載されています。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?