4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECSのマネージドインスタンスドレインを試したい

Last updated at Posted at 2024-03-31

はじめに

こんにちは。

2024/01にAmazon ECSのManaged Instance Drainingリリースされました。
2019/8にAWSのコンテナのロードマップに起票されてようやく実現した機能なのですが、触る機会がなかったので背景などを整理しつつ触ってみました。

image.png
【引用】[ECS] [RFC]: Automatic management of instance draining in an ASG #256

背景の整理

はじめに:インスタンスのドレインとは

EC2起動タイプのECSで、あるEC2インスタンスで新規タスクの配置をやめ、実行中のタスクを他のインスタンスに退避する機能です。エージェントの更新などEC2インスタンスを削除する必要がある場合に利用されます。

image.png

【引用】Amazon Elastic Container Service - AWS Black Belt Online Seminar

ドレインを行うにはマネジメントコンソールやCLIなどから対象のインスタンスのステータスをDRAININGに設定します。

参考:AWS CLIでの実行例
#指定したコンテナインスタンスの"Status"を"Draining"にする

aws ecs update-container-instances-state --cluster ${CLUSTERS} \
--container-instances ${CONTAINER_INSTANCE_ID} \
--status DRAINING
参考:マネジメントコンソールでの設定箇所 「クラスター」=> 「インフラストラクチャ」タブ

image.png

マネージドインスタンスドレインとは

EC2起動タイプのECSにはCapacity Providerという機能があり、これを利用することでタスクが必要とするリソース量に応じてAutoScalingのEC2インスタンスを増減させることが可能です。

一方、インスタンスのスケールインが発生した際にドレインを自動で行いたい場合は以下のような仕組みを自作する必要がありました。

  • LifeCycleHook + Lambda
    • スケールインが発生した場合にLifeCycleHookでLambdaを発火させ、対象のインスタンスをドレインする=>AWSブログのサンプル実装
  • LifeCycleHook + Self-Draining
    • スケールインが発生した際にLifeCycleHookで削除を待機、インスタンス内から自身の状態を常に監視し削除待ちだったらドレインを実行。タスクがすべて退避したところでCompleteLifeCycleActionで削除を再開

Capacity Provider管理のECSにおいてマネージドにドレインができるようになったのが今回のアップデートになります!すべてのタスクが他のインスタンスに代替されるまで (最大 48 時間) インスタンスの削除を遅らせることができます。

AWSブログのサンプル実装について、英語の原文を見るとLambdaの再帰に関する補足が2023年に追加されています。もしご利用される場合はご一読ください。

マネージド終了保護との違い

「スケールイン時にタスクの削除を避けたいだけであればCapacityProviderのマネージド終了保護を有効化すればいいのでは」と思われた方がいるかもしれませんがその通りです。マネージド終了保護を有効化するとタスクが存在するインスタンスの削除を防止できます。

一方、AMIの世代替えをするケースでタスクを新しい世代のインスタンスに移動させるためにはドレインを別途実行する必要があります。AutoScalingのRollingUpdateでインスタンスを世代替えしつつ、必要なタスク数は維持したいようなケースにおいてはマネージドインスタンスドレインの利用が有効です。

なお、マネージド終了保護とマネージドインスタンスドレインは共存するもので併用によって最大限の保護を実現できます。

image.png
【引用】Amazon ECSマネージドインスタンスのドレイン

検証

マネージドインスタンスドレインを有効化した状態でAutoScalingのAMI更新(AL2023=>AL2)を試してみます。
なお、マネージド終了保護は無効化し、サービスの設定は以下とします。

項目 設定値 Desc
desiredCount 3 希望するタスク数
minHealthyPercent 100 最低限起動が必要なタスクの割合
maxHealthyPercent 200 起動するタスクの最大の割合

構成図

(LBありますが無視いただいて結構です。)

super2.png

設定の詳細

設定の詳細は検証に使ったCDKのコードをご確認いただければと思います。

ecs_cp.ts
import {
  aws_ecs as ecs,
  aws_ec2 as ec2,
  aws_elasticloadbalancingv2 as elbv2,
  aws_iam as iam,
  aws_s3 as s3,
  aws_autoscaling as asg,
  aws_logs as logs,
  Stack,
  StackProps,
  CfnOutput,
  Duration,
  RemovalPolicy,
} from "aws-cdk-lib";
import { Construct } from "constructs";

export class EcsCpStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, "Vpc", {
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      maxAzs: 2,
    });

    // ALBのセキュリティグループ
    const lbSG = new ec2.SecurityGroup(this, "LbSG", {
      vpc: vpc,
    });
    lbSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
    lbSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8080));

    const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
      vpc: vpc,
      internetFacing: true,
      securityGroup: lbSG,
    });
    new CfnOutput(this, "AlbDnsName", {
      value: "http://" + alb.loadBalancerDnsName,
    });

    const listener = alb.addListener("Listener", {
      port: 80,
    });
    // ECSサービスのセキュリティグループ
    const serviceSG = new ec2.SecurityGroup(this, "ServiceSG", {
      vpc: vpc,
    });
    serviceSG.connections.allowFrom(alb, ec2.Port.tcp(80));
    serviceSG.connections.allowFrom(alb, ec2.Port.tcp(8080));

    const autoscaling = new asg.AutoScalingGroup(this, "Asg", {
      vpc: vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MEDIUM
      ),
      // 検証時に片方をコメントアウトする
      machineImage: ecs.EcsOptimizedImage.amazonLinux2023(),
      // machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
      minCapacity: 0,
      maxCapacity: 6,
      securityGroup: serviceSG,
      updatePolicy: asg.UpdatePolicy.rollingUpdate(), // CDKによるローリングアップデート有効化
      newInstancesProtectedFromScaleIn: false, // スケールインの保護は無効
      cooldown: Duration.seconds(30),
    });

    const cluster = new ecs.Cluster(this, "Cluster", {
      clusterName: "EcsBgCluster",
      vpc: vpc,
    });

    // Capacity Provider
    const capacityProvider = new ecs.AsgCapacityProvider(this, "AsgCP", {
      autoScalingGroup: autoscaling,
      enableManagedDraining: true, // マネージドインスタンスドレインは有効化
      enableManagedTerminationProtection: false, // マネージド終了保護は無効化
    });
    cluster.addAsgCapacityProvider(capacityProvider);

    // ECSタスク定義
    const taskDefinition = new ecs.Ec2TaskDefinition(this, "TaskDef");

    const containerName = "web";
    taskDefinition.addContainer("Container", {
      image: ecs.ContainerImage.fromRegistry("nginx:latest"),
      containerName: containerName,
      command: [
        "/bin/sh",
        "-c",
        "echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #00FFFF;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/share/nginx/html/index.html && nginx -g 'daemon off;'",
      ],
      portMappings: [
        {
          containerPort: 80,
          hostPort: 8080,
        },
      ],
      memoryLimitMiB: 512,
      healthCheck: {
        command: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"],
        interval: Duration.seconds(60),
        retries: 10,
        startPeriod: Duration.seconds(30),
        timeout: Duration.seconds(30),
      },
    });

    const service = new ecs.Ec2Service(this, "Service", {
      cluster: cluster,
      taskDefinition: taskDefinition,
      desiredCount: 3, // 希望するタスク数
      circuitBreaker: { enable: true, rollback: true },
      capacityProviderStrategies: [
        {
          capacityProvider: capacityProvider.capacityProviderName,
          weight: 1,
          base: 0,
        },
      ],
      minHealthyPercent: 100, // 最小の保持タスク数の割合
      maxHealthyPercent: 200, // 最大の保持タスク数の割合
    });

    listener.addTargets("ECSTarget", {
      port: 8080,
      targets: [
        service.loadBalancerTarget({
          containerName: containerName,
          containerPort: 80,
        }),
      ],
      healthCheck: {
        path: "/",
        protocol: elbv2.Protocol.HTTP,
        interval: Duration.seconds(30),
        timeout: Duration.seconds(5),
        unhealthyThresholdCount: 10,
      },
    });
  }
}

bin/ecs_bg.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { EcsCpStack } from "../lib/ecs_cp";

const app = new cdk.App();
new EcsCpStack(app, "MyEcsCP");

試した

ecs_cp.tsの58行目と59行目のコードのコメントアウトを逆転してcdk deployし、ASGのAMIを更新します。
AWS CDKだと1行書き換えてデプロイするだけなので簡単ですね。

// 検証時に片方をコメントアウトする
machineImage: ecs.EcsOptimizedImage.amazonLinux2023(),
// machineImage: ecs.EcsOptimizedImage.amazonLinux2(),

コンテナインスタンスの初期状態。
image.png

いきなり終盤のほうの状態です(最初のほうキャプチャがうまくいかなかったので:bow:

ドレイン状態になった後、新しいインスタンスでタスクが起動したら、古いインスタンスのタスクを終了してますね。ASGの更新中、実行中のタスク数はminHealthyPercentmaxHealthyPercentを満たしたまま遷移しました。

image.png

image.png

最終状態。

image.png

まとめ

ECSのマネージドインスタンスドレインを試してみました。
なにかのお役に立てば幸いです。

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?