😶‍🌫️

ECS外部デプロイ&TaskSet実践ガイド

2024/12/18に公開

本記事は、CyberAgent Group SRE Advent Calendar 2024の18日目の記事になります。

ECSの機能の中でもワースト級の知名度と思われる「外部デプロイ」と「TaskSet」の概要と使い方について、PipeCDでの実例に触れつつ解説します。これらをマスターすれば、ECSでの高度なデプロイを実現できます。

※ 本記事は2024/12/14時点での仕様に基づいています。

先にまとめ

  • TaskSet: ServiceとTaskの間にあり、TaskSet毎にリビジョンを持てる
  • 外部デプロイ: TaskSetを駆使して、自由度の高いデプロイを行うデプロイタイプ
  • これらは、ECSにおいてCanaryなどの高度なデプロイ戦略を実現するための機能

用語の説明

1. 外部デプロイとは

外部デプロイは、ECSにおいて3rd-partyのコントローラによる自由なデプロイを実現するためのデプロイタイプです。

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html

※ ECS Anywhereで登場するlaunchType: EXTERNALとは無関係です。

3種のデプロイタイプ

ECSには3種の「デプロイタイプ」があり、外部デプロイはそのうちの1つです。

  • ECS: ローリングアップデート。デフォルトであり、最もオーソドックス
  • CODE_DEPLOY: CodeDeployと統合し、単純な設定でCanaryやBlue/Greenを実現できる
  • EXTERNAL: 外部デプロイ用。後述のTaskSetを操作して、自由なデプロイを実現できる

なお、デプロイタイプはService作成後に変更できません。(重要)

CODE_DEPLOYEXTERNALの違い

  • TaskSetを操作している点は共通で、操作フローも基本的には同じです[1]
  • CODE_DEPLOYの方が実装は楽で、CodeDeployがよしなにやってくれます
  • EXTERNALは実装が複雑な分、より自由なデプロイを実現できます
    • 例)デプロイ成否の判断方法、ロールバック方法

2. TaskSetとは

TaskSetはECSのServiceとTaskの間にあり、Taskを束ねるような概念です。

TaskSetはデプロイタイプとしてCodeDeployまたは外部デプロイを選択している場合にのみ登場し、Serviceの代わりにTaskのスケジューリング・スケーリングを担っているようです。

デプロイタイプとしてローリングアップデートを選択している場合や、Standalone Taskにおいては、TaskSetに関わることはありません。

TaskSetを見てみる

TaskSetはコンソールでは見つかりません。
CodeDeployまたは外部デプロイを利用するServiceに対してDescribeServicesすると、TaskSetの存在を確認できます。
runningCountlaunchTypenetworkConfigurationなど、通常のServiceに似た項目を持っています。

$ aws ecs describe-services --cluster xxx --services yyy-service
{
    "services": [
        {
            "serviceName": "yyy-service",
            "taskSets": [ // ココ
                {
                    "id": "ecs-svc/4390851359570905103",
                    "taskSetArn": "arn:aws:ecs:ap-northeast-1:<account>:task-set/xxx/yyy-service/ecs-svc/4390851359570905103",
                    "serviceArn": "arn:aws:ecs:ap-northeast-1:<account>:service/xxx/yyy-service",
                    "clusterArn": "arn:aws:ecs:ap-northeast-1:<account>:cluster/xxx",
                    "status": "ACTIVE",
                    "taskDefinition": "arn:aws:ecs:ap-northeast-1:<account>:task-definition/zzz-taskdef:1",
                    "computedDesiredCount": 2,
                    "pendingCount": 2,
                    "runningCount": 0,
                    "createdAt": "2024-12-13T20:00:24.064000+09:00",
                    "updatedAt": "2024-12-13T20:00:28.178000+09:00",
                    "launchType": "FARGATE",
                    "platformVersion": "1.4.0",
                    "platformFamily": "Linux",
                    "networkConfiguration": {
                        "awsvpcConfiguration": {
                            "subnets": [
                                "subnet-aaa",
                                "subnet-bbb"
                            ],
                            "securityGroups": [
                                "sg-ccc"
                            ],
                            "assignPublicIp": "DISABLED"
                        }
                    },
                    "loadBalancers": [],
                    "serviceRegistries": [],
                    "scale": {
                        "value": 100.0,
                        "unit": "PERCENT"
                    },
                    "stabilityStatus": "STABILIZING",
                    "stabilityStatusAt": "2024-12-13T20:00:24.064000+09:00",
                    "tags": []
                }
            ],
            "deploymentController": {
                "type": "EXTERNAL" // 外部デプロイ
            },
            ...
        }
    ],
}

TaskSetの使い所

TaskSetを活用すると、CanaryリリースやBlue/Greenデプロイを実現できます。
というのも、TaskSet毎に異なるTargetGroupとTaskDefinitionを指定できるからです。

  • 「v1のTaskDefinitionを使うTaskSetにTargetGroup A」
  • 「v2のTaskDefinitionを使うTaskSetにTargetGroup B」

を割り当て、ALB側でTargetGroup間のWeightを制御すればよいです。


TaskSetを活用したCanaryリリース、Blue/Greenデプロイ

TaskSetの操作方法

TaskSetを操作するAPIは5点のみです。
TaskSet内部はほとんど操作できず、複数のTaskSetを操作することが必要です。

  1. CreateTaskSet
  2. DescribeTaskSets
    • このAPIは事前にTaskSet IDの取得が必要なので面倒です
    • DescribeServicesの方が簡単にTaskSetの情報は取得できるのでおすすめです
  3. DeleteTaskSet
  4. UpdateTaskSet
    • scaleしか更新できません
      • scale: ServiceのdesiredCountに対して、何%のTaskをこのTaskSetで動かすか
        • 例)desiredCountが10、scaleが30%の場合、TaskSetはTask数3を維持します
        • 0〜100で指定します
      • Serviceがオートスケールする(=desiredCountが変わる)と、TaskSetのTask数も自動で変化します
    • taskDefinitionも変更できないので、Immutableなタグ運用をしていれば、同一TaskSet内のTaskは必ず同一イメージとなります
  5. UpdateServicePrimaryTaskSet
    • Service内の1つのTaskSetのstatusPRIMARYに変更します

    • この操作は必須ではないですが、PrimaryのTaskSetはDeleteTaskSetでは削除不可になるので、安全のために実行します[2]

    • あるTaskSetをPrimaryにすると、元々PrimaryだったTaskSetはACTIVEに戻ります

    • Primary TaskSetの設定の一部がServiceに伝播されます(launchType,taskDefinitionなど)

      • Primary TaskSetがまだ存在しないとき:

        $ aws ecs describe-services --cluster xxx --services yyy-service
        {
            "services": [
                {
                    "serviceName": "yyy-service",
                    "launchType": "EC2", // Service作成時はEC2
                    "taskSets": [
                        {
                            "id": "ecs-svc/8721983045402263235",
                            "status": "ACTIVE", // この段階ではPRIMARYではない
                            "taskDefinition": "arn:aws:ecs:ap-northeast-1:<account>:task-definition/zzz-taskdef:3",
                            "launchType": "FARGATE",
                            ...
        
        
      • UpdateServicePrimaryTaskSet実行後:

        $ aws ecs describe-services --cluster xxx --services yyy-service
        {
            "services": [
                {
                    "serviceName": "yyy-service",
                    "launchType": "FARGATE", // TaskSetから伝播
                    "taskDefinition": "arn:aws:ecs:ap-northeast-1:<account>:task-definition/zzz-taskdef:3", // TaskSetから伝播
                    "taskSets": [
                        {
                            "id": "ecs-svc/8721983045402263235",
                            "status": "PRIMARY", // PRIMARYに
                            "taskDefinition": "arn:aws:ecs:ap-northeast-1:<account>:task-definition/zzz-taskdef:3",
                            "launchType": "FARGATE",
                            ...
        

外部デプロイによるCanaryリリースの例

外部デプロイのやり方は、ロールバック含めて複数考えられます。
以下では2つの例を紹介します
詳細はこちらのDocsを参照してください。

方式A: CanaryをPrimaryに昇格させていく

事前準備

ALB、ListenerRule、TargetGroup2点、ECS Cluster、TaskDefinition(v1,v2)は作成済とします

事前準備1. Serviceを作成する

CreateServiceでServiceを作成します。

  • [必須] deploymentController.typeEXTERNALにします

  • ECSデプロイタイプと比べて、指定する項目は少ないです

    • taskDefinition, launchType, networkConfigurationさえ指定不要です
    • 詳細はこちら
$ aws ecs create-service \
--cluster <cluster-name> \
--service-name <service-name> \
--desired-count 2 \
--deployment-controller type=EXTERNAL

この段階では、TaskSetもTaskも作成されません。

事前準備2. TaskSet(v1)を作成する

CreateTaskSetで最初のTaskSetを作成します。

  • launchTypetaskDefinitionなど、通常はServiceに持たせる様々な項目を指定します
  • loadBalancers[].targetGroupArn: ALBからトラフィックを受けるTargetGroupを指定します
  • 最初のTaskSetのscaleは100%が良いです。
$ aws ecs create-task-set \
--cluster <cluster-name> \
--service <service-name> \
--task-definition <task-def-arn-v1> \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-ccc],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=<tg-arn-1>,containerName=web,containerPort=80" \ # TaskDefinitionに合わせる
--scale unit=PERCENT,value=100


事前準備完了後の状態

※ 追加でUpdateServicePrimaryTaskを実行してもよいですが、必須ではないので省略します

デプロイフロー

1. TaskSet(v2)を作成する

事前準備2.とほぼ同じですが、task-definitiontargetGroupArnscaleを変更します。

$ aws ecs create-task-set \
--cluster <cluster-name> \
--service <service-name> \
--task-definition <task-def-arn-v2> \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-ccc],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=<tg-arn-2>,containerName=web,containerPort=80" \ # TaskDefinitionに合わせる
--scale unit=PERCENT,value=10

この段階では、v2のTaskSetにトラフィックは流れません。まだv2のTargetGroupのWeightが0だからです。


Canary TaskSetを作成

2. Canaryにトラフィックを流す

ELBのModifyRuleで、TargetGroupのWeightを変更します。

$ aws elbv2 modify-rule \
--rule-arn <listener-rule-arn> \
--actions '[{
    "Type": "forward", 
    "ForwardConfig": {
        "TargetGroups": [
            {
                "TargetGroupArn": "<tg-arn-1>", 
                "Weight": 90
            },
            {
                "TargetGroupArn": "<tg-arn-2>", 
                "Weight": 10
            }
        ]
    }
}]' 


Canaryにトラフィックを流し始める

なお、Listenerのデフォルトルールを書き換えるにはModifyListenerを使います。
また、複数のListenerRuleを書き換えたい場合は、対象となるListenerRuleを全て取得しておく必要があります。

3. 問題なければ、Canaryへのトラフィックを100%にする

※ Canaryのscaleを100未満にしていた場合、まずUpdateTaskSetscaleを100にします。

再度ELBのModifyRuleで、TargetGroupのWeightを変更します。

$ aws elbv2 modify-rule \
--rule-arn <listener-rule-arn> \
--actions '[{
    "Type": "forward", 
    "ForwardConfig": {
        "TargetGroups": [
            {
                "TargetGroupArn": "<tg-arn-1>", 
                "Weight": 0
            },
            {
                "TargetGroupArn": "<tg-arn-2>", 
                "Weight": 100
            }
        ]
    }
}]' 


Canaryに100%のトラフィックを流す

4. Canary TaskSetをPrimaryに昇格させる

4.1. Canary TaskSetのIDを取得する

$ aws ecs describe-services \
--cluster <cluster-name> \
--services <service-name> \
| jq '.services[0].taskSets[]'    

レスポンスの中から、Canary TaskSetのIDを探します。
statusPRIMARY/ACTIVEのどちらなのかで判断するのが一つのやり方です。

4.2. UpdateServicePrimaryTaskSetにより、 Canary TaskSetをPrimaryに変更する

$ aws ecs update-service-primary-task-set \
--cluster <cluster-name> \
--service <service-name> \
--primary-task-set <ecs-svc/xxx> # 4.1.で取得した値のうち、Canaryの方

5. 古いTaskSetを削除する

DeleteTaskSetにより、v1のTaskSetを削除します。

$ aws ecs delete-task-set \
--cluster <cluster-name> \
--service <service-name> \
--task-set <ecs-svc/xxx> # 4.1.で取得した値のうち、古い方


デプロイ完了後の状態

ロールバック時

  • 「2. Canaryにトラフィックを流す」または「3. Canaryへのトラフィックを100%にする」で失敗した場合は、以下の流れでロールバックできます。

    1. TargetGroup Aへのトラフィックを100%に戻す
    2. Canary TaskSetを削除する
  • 「5. 古いTaskSetを削除する」で失敗した場合は、以下の流れでロールバックできます。

    1. TaskDefinition v1とTargetGroup Aを用いてTaskSetを新規作成
    2. そのTaskSetをPrimaryに更新する
    3. TargetGroup Aにトラフィックを100%流す
    4. Canary TaskSetを削除する

方式Aの課題

TargetGroupが新旧どちらのTaskSetを指すのか固定されないため、デプロイのたびに「今どちらを指しているのか」を取得する必要があります。
特にロールバックの際に複雑になります。

方式B: 3つのTaskSetを使用

方式Aの課題を解決する別方式を簡単に紹介します。
「現・Canary・新」の3つのTaskSetを使用します。

ちなみに、PipeCDではこの方式が採用されています。

デプロイフロー

0. 初期状態

1. Canaryを作成

CreateTaskSetelbv2::ModifyRuleを行います。
Canary TaskSetのscaleは100%未満でもよいです。

2. 問題なければ、さらに新しいTaskSetを作成

TargetGroup AとTaskDefinition v2を使用して、CreateTaskSetをします。

この段階で3つ目のTaskSetにもトラフィックが流れます。

3. 古いTaskSetを全て削除

UpdateServicePrimaryTaskSetelbv2::ModifyRuleDeleteTaskSetを行います。

これで初期状態と同様の形になりました。
次回デプロイ時も、TargetGroup Aが最新版を指している状態で開始できます。

ロールバック時

  • どのフェーズで失敗したとしても、以下の流れで初期状態にロールバックできます。

    1. TaskDefinition v1とTargetGroup Aを用いてTaskSetを新規作成
    2. そのTaskSetをPrimaryに更新する
    3. TargetGroup Aにトラフィックを100%流す
    4. 他のTaskSetを全て削除する
  • 「3. 古いTaskSetを全て削除」以前の失敗であれば、以下の流れだけで済み、高速です。

    1. TargetGroup Aにトラフィックを100%流す
    2. 他のTaskSetを全て削除する

Pros/Cons

Pros:

  • TargetGroupが新旧どちらを指すのか固定され、操作が楽

Cons:

  • TaskSetを2度立ち上げるため、デプロイに時間がかかる
  • TaskSetが3つになるため、費用がかかる

on EC2でも実現可能

on Fargateだけでなくon EC2でも方式Bを試しましたが、問題ありませんでした。

  • deploymentConfiguration.maximumPercentによる制限を懸念していました。
  • 「最大でもdesiredCountの2倍までしかTaskを起動できず、方式Bを使えないのでは?」と思っていましたが、maximumPercentは無視されました。
  • ※ 「on Fargateの外部デプロイではmaximumPercentが無視される」旨は、Docsに明記されています。

PipeCDで方式Bを使う方法

外部デプロイは扱うのが面倒です(特にロールバックを含めると)。
PipeCDでは、以下のようなパイプラインを定義するだけで、方式BによるCanaryリリースを実現できます。
ロールバックもよしなにやってくれます。

app.pipecd.yaml
  pipeline:
    stages:
      # 1. Canary TaskSetの作成
      - name: ECS_CANARY_ROLLOUT 
        with:
          scale: 10
      # 2. Canaryに10%のトラフィックを流す
      - name: ECS_TRAFFIC_ROUTING 
        with:
          canary: 10
      # 3. 承認フェーズ
      - name: WAIT_APPROVAL
      # 4. 新しいPrimary TaskSetを作成
      - name: ECS_PRIMARY_ROLLOUT 
      # 5. 新しいPrimary TaskSetに100%のトラフィックを流す
      - name: ECS_TRAFFIC_ROUTING 
        with:
          primary: 100
       # 6. 不要なTaskSetを削除
      - name: ECS_CANARY_CLEAN

上記設定ファイルの全体はこちら

外部デプロイの細かい注意点

外部デプロイを利用する際には、多くの注意点があります。

Serviceの様々な機能・設定項目がサポートされていない

  • 未対応の例

  • 「外部デプロイで未サポートの項目」が一覧化されていないため、Docs内で"Deployment"などのキーワードで検索することをおすすめします

    • Service Connectの例:

      Only services that use rolling deployments are supported with Service Connect.

  • 私の把握する限り、TaskDefinitionには外部デプロイ固有の制約はないと思います。

ECSのアップデートが来ても、外部デプロイは対象外になりがち

他のデプロイタイプからの移行に一苦労

  • 先述の通り、Service作成後にデプロイタイプの変更はできません (個人的に最も辛い)
    • そのため、「このServiceを外部デプロイで管理したい」となったときは、別のServiceを作成してトラフィックを徐々に流していくような移行作業が必要です
  • また、外部デプロイでは様々な項目がサポートされていないため、Serviceの設定項目を見直す必要があります
    • いくつかの項目は、CreateServiceではなくCreateTaskSetに渡すことになります

TaskSetへのタグ付与は全てユーザが行う必要がある

コンソール上で情報が少ない

  • TaskSetそのものはECSコンソール上では表示されません。CLIでどうにかする必要があります

  • 先述の通り「デプロイメント履歴」も外部デプロイに関しては従来と変わりません

  • 参考)PipeCDでは、Service/TaskSet/Taskの状態をUI上で可視化しています。[3]

    PipeCDのUI(Canaryデプロイ中の状態)

その他細かい制約

  • 1つのTaskSetに複数のTargetGroupを紐づけることはできません
  • 1つのServiceに同時に存在できるTaskSetは最大5つのようです
    • そのため、ServiceではdesiredCountの最大5倍のTaskを動かせます(scale100 x TaskSet5点)
    • Docs内に記載はありませんでした
    • TaskSetが5つ存在する状態でさらに作成しようとすると、以下のエラーが発生します
      An error occurred (InvalidParameterException) when calling the CreateTaskSet operation: 5 deployments exist on the service. Unable to create new TaskSet since this exceeds the maximum limit.
      

余談

余談1: なぜ知名度が低いのか?

外部デプロイとTaskSetを知っている方をあまり見かけないのですが、
その理由として2つの仮説があります。

  1. コンソールに「外部デプロイ/TaskSet」がほとんど見当たらないから

    サービス作成画面。「外部デプロイ」が見当たらない
    • なお、CODE_DEPLOYではひっそり名前が出ている

      CodeDeployのデプロイ設定
  2. ECSCODE_DEPLOYのデプロイタイプで満足されているから(本当に満足か?

余談2: ECSデプロイタイプではTaskSetは存在しないのか?

A.内部的にTaskSetが使われている可能性はあるが、真相は不明

以下の2点から"TaskSetの気配"を感じますが、TaskSetと全く同じかは怪しいです。

  1. TaskのStartedBy(開始者)がecs-svc/xxx形式で、外部デプロイの場合と形式が同じ

    Taskの"開始者"
    • TaskSetのIDはecs-svc/xxx形式であり、TaskSet内のTaskのStartedByはそのecs-svc/xxxとなります。
  2. そのecs-svc/xxxxxxサービスリビジョンのIDに一致する

    サービスリビジョン

サービスリビジョンの仕組みがTaskSetと似ていると思いますが、ARNの形式などが異なります。

なお、ecs-svc/xxxに対してDescribeTaskSetsを実行してもエラーとなります。

$ aws ecs describe-task-sets --cluster aaa --service bbb --task-sets ecs-svc/xxx

An error occurred (InvalidParameterException) when calling the DescribeTaskSets operation: Amazon ECS only supports task set management on services configured to use external deployment controllers.

おわりに

外部デプロイはドキュメントにも情報が少なく、制約・難しさが色々とありますが、使いこなすと強力です。
Service ConnectとVPC Latticeが対応されるとありがたいです。

外部デプロイは他のデプロイタイプからの移行が大変ですが、
PipeCDでは外部デプロイ以外も「プラグイン」によって近い将来扱えるようになると思います。
「プラグイン」の詳細はこちらの記事を参照してください。
https://zenn.dev/cadp/articles/pipecd-plugin-intro

外部デプロイ検証時に便利なコマンドツールも個人で開発中なので、いずれ公開予定です。

脚注
  1. https://qiita.com/takahash_3/items/793480920c0d283340c0 ↩︎

  2. DeleteTaskSetを呼び出すと、このようなエラーメッセージが表示されます。An error occurred (InvalidParameterException) when calling the DeleteTaskSet operation: Primary TaskSet cannot be deleted ↩︎

  3. https://pipecd.dev/docs/user-guide/managing-application/application-live-state/ ↩︎

サイバーエージェント Developer Productivity室

Discussion