AWS

AWS ECSにおけるローリングアップデートの安全性を向上させたい

ryo

こんにちは、ryoです。

ECSのローリングアップデートはデフォルトのデプロイ形式であり、多くのケースで利用されていると思います。
今回は、ECSのローリングアップデートの安全性を高める仕組みをご紹介します。

先にまとめ

  • ローリングアップデート時のタスク起動の失敗やヘルスチェックの失敗を検知し、以前のバージョンへ自動ロールバックをしたい
    • deployment circuit breakerを利用する
  • ローリングアップデート時に何らかのメトリクスの閾値に応じて、以前のバージョンへ自動ロールバックをしたい
    • CloudWatch Alarmsを利用する
  • 両方のケースでは、どちらの方法を取り入れることも可能

今回紹介する方法は現時点(2023年2月)でローリングアップデートのみ対応です

ECSのローリングアップデート失敗時の挙動

ECSのタスクが正常に立ち上がらないなどの理由でDesiredCountに達しない場合、ECSはサービススロットリングロジックに基づきタスクの再起動を行います。
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-throttle-logic.html

このロジックでは、タスクが正常に立ち上がりDesiredCountを満たすまで永久にタスクの再起動を行います。
そのためローリングアップデートの安全性を高めるためには、タスクの異常終了の早急な検知 +自動ロールバックの導入が役立ちます。

deployment circuit breakerの利用

概要

deployment circuit breakerはECSのローリングアップデートにおける異常を検知した際に、自動でロールバックをしてくれる機能です。

タスクの異常終了の検知はdeployment circuit breakerを使わずに実現が可能です。
例えば、タスクの終了イベントをEventBridgeでキャプチャすることが出来るので、EventBridgeから様々なAWSサービスに連携が出来ます。
しかし、タスクの異常終了の検知を行なってもロールバックなどの対応は自前で実装を行うか、その都度対応していく必要があります。

deployment circuit breakerを利用することで、ローリングアップデートにおける異常の検知 + 自動ロールバックが可能になります。

仕組み

詳しい仕組みはこちらのスライドに分かりやすくまとまっているので、ご一読いただければと思います。
https://d1.awsstatic.com/Developer%20Marketing/jp/developer-zone/ECS-Deployment-Circuit-Breaker-Rollback.pdf

基本的にはデプロイ時にタスクが立ち上がらない場合やヘルスチェックが通らない場合に、ある閾値まで再試行を行い、閾値を超えた際にロールバックを行うという挙動になります。

デプロイ時のチェックは主に2段階に分かれていて、簡単に図に表すと以下のようになります。

閾値は、10 <= Desired Count * 0.5 <= 200という式で表されます。
最小の再試行回数は10で、最大の再試行回数は200になります。

例えばDesiredCountを2で設定している場合10 <= 2 * 0.5 <= 200となり、最小試行回数である10が閾値となります。
DesiredCountが200の場合は、10 <= 200 * 0.5 <= 200となり、閾値は100になります。

CloudWatch Alarmsの利用

概要

CloudWatch Alarmsを利用することでデプロイの最中や完了後に発生したCloudWatch Alarmsの発報に応じて、自動でロールバックを行うことが可能です。
CloudWatch Alarmsではメトリクスに対して、柔軟に設定が可能で、アプリケーションに応じて様々なケースが考えられます。

仕組み


公式ブログより引用

公式ブログの図を見ると、時間軸t2から新バージョンのローリングアップデートが始まり、時間軸t3でデプロイが完了すると想定します。
またローリングアップデートが始まった時間軸t2と同時に、CloudWatch Alarmsの発報によるロールバック可能期間が開始されます。
時間軸t3 ~ t4bake timeと呼ばれる、デプロイ完了(DesiredCountを満たす)後の追加のロールバック可能期間です。
このbake timeはAlarmの設定に応じて、ECS側で自動で計算されます。

つまり、上図の時間軸t2 ~ t4の間にCloudWatch Alarmsが発報した場合にタスクのロールバックが行われる形になります。

CloudWatch Alarmsに使うメトリクスの選定について、ドキュメントに推奨のものが記載されています。
https://docs.aws.amazon.com/AmazonECS/latest/userguide/deployment-alarm-failure.html#ecs-deployment-alarms

AWS CDKで設定を行う

deployment circuit breakerとCloudWatch Alarmsを組み込んだローリングアップデートをCDKで作成したECSに組み込む方法を紹介します。
今回はさくっと作りたいので、L3コンストラクタであるecs-patternsを利用して、ALB + Fargateのインフラ定義を行います。

各種バージョン

ツール バージョン
Node.js v18.12.0
aws-cdk v2.64.0

コード

import { Stack, StackProps } from "aws-cdk-lib";
import { Cluster, ContainerImage, CfnService } from "aws-cdk-lib/aws-ecs";
import { Platform } from "aws-cdk-lib/aws-ecr-assets";
import { ApplicationLoadBalancedFargateService } from "aws-cdk-lib/aws-ecs-patterns";
import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
import { Construct } from "constructs";
import { HttpCodeElb } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as path from "path";

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

    // ECS Cluster
    const cluster = new Cluster(this, "ECSCluster");

    // Fargate + ALB
    const fargateService = new ApplicationLoadBalancedFargateService(
      this,
      "ECSService",
      {
        cluster,
        taskImageOptions: {
          image: ContainerImage.fromAsset(
            path.resolve(__dirname, "../", "app"),
            {
              platform: Platform.LINUX_AMD64,
            }
          ),
          containerPort: 8000,
        },
        desiredCount: 2,
      }
    );

    // ELB5xxに対してアラームの設定を行う
    const elb5xxAlarm = new Alarm(this, "DeploymentAlarm", {
      metric: fargateService.loadBalancer.metrics.httpCodeElb(
        HttpCodeElb.ELB_5XX_COUNT
      ), // 適宜変更を行う
      threshold: 3, // 適宜変更を行う
      evaluationPeriods: 1, // 適宜変更を行う
    });

    // L1コンストラクタへのキャスト
    const service = fargateService.service.node.defaultChild as CfnService;

    // deployment circuit breakerとCloudWatch Alarmsの両方を利用する
    service.deploymentConfiguration = {
      deploymentCircuitBreaker: {
        // deployment circuit breaker
        enable: true,
        rollback: true,
      },
      maximumPercent: 200,
      minimumHealthyPercent: 50,
      alarms: {
        // CloudWatch Alarms統合
        alarmNames: [elb5xxAlarm.alarmName],
        enable: true,
        rollback: true,
      },
    };
  }
}

CDKコードの概要

ecs-patternsのApplicationLoadBalancedFargateServiceとL2コンストラクタのFargateServiceは、deployment circuit breakerには対応してますが、CloudWatch Alarmsとの統合にはまだ対応していないため、今回はエスケープハッチを利用して設定を行います。
CDKのGitHubを見るとPR出している方がいたので、もうすぐエスケープハッチを利用しなくても設定出来るようになりそうです。
https://github.com/aws/aws-cdk/pull/24182

エスケープハッチは高レベルコンストラクタからL1コンストラクタにアクセスするための方法で、L1コンストラクタはCloudFormationのリソースと1対1で紐づいているため、柔軟な設定が可能です。
詳細はこちらを参照ください。
https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html

CloudFormationではDeploymentConfigurationで該当の設定が可能なので、エスケープハッチからこちらの設定を行います。
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-deploymentconfiguration.html

deploymentCircuitBreakeralarmsにはenablerollbackがあり、別々に設定可能です。
enable: truerollback: falseといった設定も可能で、この場合はデプロイ失敗時にEventBridgeへの通知のみ走ります。

    // L1コンストラクタへのキャスト
    const service = fargateService.service.node.defaultChild as CfnService;

    // deployment circuit breakerとCloudWatch Alarmsの両方を利用する
    service.deploymentConfiguration = {
      deploymentCircuitBreaker: {
        // deployment circuit breaker
        enable: true,
        rollback: true,
      },
      maximumPercent: 200,
      minimumHealthyPercent: 50,
      alarms: {
        // CloudWatch Alarms統合
        alarmNames: [elb5xxAlarm.alarmName],
        enable: true,
        rollback: true,
      },
    };

まとめ

今回はローリングアップデートをより安全に行う方法と、CDKでの設定方法を紹介させていただきました。
deployment circuit breakerとCloudWatch Alarmsを利用した場合では、bake timeを除くと評価期間が被りますが、それぞれの目的は全く異なるものです。
ぜひ取り入れて少しでも安全なデプロイにしていきましょう!

どなたかの参考になれば幸いでございます。

参考

AUTHOR
ryo
ryo
記事URLをコピーしました