ECS外部デプロイ&TaskSet実践ガイド
本記事は、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のコントローラによる自由なデプロイを実現するためのデプロイタイプです。
※ ECS Anywhereで登場するlaunchType: EXTERNAL
とは無関係です。
3種のデプロイタイプ
ECSには3種の「デプロイタイプ」があり、外部デプロイはそのうちの1つです。
-
ECS
: ローリングアップデート。デフォルトであり、最もオーソドックス -
CODE_DEPLOY
: CodeDeployと統合し、単純な設定でCanaryやBlue/Greenを実現できる -
EXTERNAL
: 外部デプロイ用。後述のTaskSetを操作して、自由なデプロイを実現できる
なお、デプロイタイプはService作成後に変更できません。(重要)
CODE_DEPLOY
とEXTERNAL
の違い
- 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の存在を確認できます。
runningCount
、launchType
、networkConfiguration
など、通常の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を操作することが必要です。
CreateTaskSet
-
DescribeTaskSets
- このAPIは事前にTaskSet IDの取得が必要なので面倒です
-
DescribeServices
の方が簡単にTaskSetの情報は取得できるのでおすすめです
DeleteTaskSet
-
UpdateTaskSet
-
scale
しか更新できません-
scale
: ServiceのdesiredCount
に対して、何%のTaskをこのTaskSetで動かすか- 例)
desiredCount
が10、scale
が30%の場合、TaskSetはTask数3を維持します - 0〜100で指定します
- 例)
- Serviceがオートスケールする(=
desiredCount
が変わる)と、TaskSetのTask数も自動で変化します
-
-
taskDefinition
も変更できないので、Immutableなタグ運用をしていれば、同一TaskSet内のTaskは必ず同一イメージとなります
-
-
UpdateServicePrimaryTaskSet
-
Service内の1つのTaskSetの
status
をPRIMARY
に変更します -
この操作は必須ではないですが、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.type
はEXTERNAL
にします -
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を作成します。
-
launchType
やtaskDefinition
など、通常は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-definition
、targetGroupArn
、scale
を変更します。
$ 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未満にしていた場合、まずUpdateTaskSet
でscale
を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を探します。
status
がPRIMARY
/ACTIVE
のどちらなのかで判断するのが一つのやり方です。
UpdateServicePrimaryTaskSet
により、 Canary TaskSetをPrimaryに変更する
4.2. $ 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%にする」で失敗した場合は、以下の流れでロールバックできます。
- TargetGroup Aへのトラフィックを100%に戻す
- Canary TaskSetを削除する
-
「5. 古いTaskSetを削除する」で失敗した場合は、以下の流れでロールバックできます。
- TaskDefinition v1とTargetGroup Aを用いてTaskSetを新規作成
- そのTaskSetをPrimaryに更新する
- TargetGroup Aにトラフィックを100%流す
- Canary TaskSetを削除する
方式Aの課題
TargetGroupが新旧どちらのTaskSetを指すのか固定されないため、デプロイのたびに「今どちらを指しているのか」を取得する必要があります。
特にロールバックの際に複雑になります。
方式B: 3つのTaskSetを使用
方式Aの課題を解決する別方式を簡単に紹介します。
「現・Canary・新」の3つのTaskSetを使用します。
ちなみに、PipeCDではこの方式が採用されています。
デプロイフロー
0. 初期状態
1. Canaryを作成
CreateTaskSet
とelbv2::ModifyRule
を行います。
Canary TaskSetのscale
は100%未満でもよいです。
2. 問題なければ、さらに新しいTaskSetを作成
TargetGroup AとTaskDefinition v2を使用して、CreateTaskSet
をします。
この段階で3つ目のTaskSetにもトラフィックが流れます。
3. 古いTaskSetを全て削除
UpdateServicePrimaryTaskSet
、elbv2::ModifyRule
、DeleteTaskSet
を行います。
これで初期状態と同様の形になりました。
次回デプロイ時も、TargetGroup Aが最新版を指している状態で開始できます。
ロールバック時
-
どのフェーズで失敗したとしても、以下の流れで初期状態にロールバックできます。
- TaskDefinition v1とTargetGroup Aを用いてTaskSetを新規作成
- そのTaskSetをPrimaryに更新する
- TargetGroup Aにトラフィックを100%流す
- 他のTaskSetを全て削除する
-
「3. 古いTaskSetを全て削除」以前の失敗であれば、以下の流れだけで済み、高速です。
- TargetGroup Aにトラフィックを100%流す
- 他の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リリースを実現できます。
ロールバックもよしなにやってくれます。
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の様々な機能・設定項目がサポートされていない
-
未対応の例
- Service Connect
- VPC Lattice (Internal-ALB経由なら可能)
- Deployment Circuit Breaker
- Deployment Alarms
-
「外部デプロイで未サポートの項目」が一覧化されていないため、Docs内で"Deployment"などのキーワードで検索することをおすすめします
- Service Connectの例:
Only services that use rolling deployments are supported with Service Connect.
- Service Connectの例:
-
私の把握する限り、TaskDefinitionには外部デプロイ固有の制約はないと思います。
ECSのアップデートが来ても、外部デプロイは対象外になりがち
- 2024年11月に「デプロイメントの可視性向上」によりECSのデプロイ履歴関連のUI/APIが強化されたが、外部デプロイは従来のままです
- 2024年11月にVPC LatticeとECSがネイティブ統合されましたが、外部デプロイは従来通りInternal-ALBが必要です
他のデプロイタイプからの移行に一苦労
- 先述の通り、Service作成後にデプロイタイプの変更はできません (個人的に最も辛い)
- そのため、「このServiceを外部デプロイで管理したい」となったときは、別のServiceを作成してトラフィックを徐々に流していくような移行作業が必要です
- また、外部デプロイでは様々な項目がサポートされていないため、Serviceの設定項目を見直す必要があります
- いくつかの項目は、
CreateService
ではなくCreateTaskSet
に渡すことになります
- いくつかの項目は、
TaskSetへのタグ付与は全てユーザが行う必要がある
- TaskSetに対してServiceやTaskDefinitionからタグを伝播(
propagateTags
)させることはサポートされていません - また、
enableECSManagedTags
をONにしたServiceでも、TaskSetにManagedのタグは付与されません
コンソール上で情報が少ない
-
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.
- そのため、Serviceでは
余談
余談1: なぜ知名度が低いのか?
外部デプロイとTaskSetを知っている方をあまり見かけないのですが、
その理由として2つの仮説があります。
- コンソールに「外部デプロイ/TaskSet」がほとんど見当たらないから
サービス作成画面。「外部デプロイ」が見当たらない- なお、
CODE_DEPLOY
ではひっそり名前が出ている
CodeDeployのデプロイ設定
- なお、
-
ECS
やCODE_DEPLOY
のデプロイタイプで満足されているから(本当に満足か?)
ECS
デプロイタイプではTaskSetは存在しないのか?
余談2: A.内部的にTaskSetが使われている可能性はあるが、真相は不明
以下の2点から"TaskSetの気配"を感じますが、TaskSetと全く同じかは怪しいです。
- Taskの
StartedBy
(開始者)がecs-svc/xxx
形式で、外部デプロイの場合と形式が同じ
Taskの"開始者"- TaskSetのIDは
ecs-svc/xxx
形式であり、TaskSet内のTaskのStartedBy
はそのecs-svc/xxx
となります。
- TaskSetのIDは
- その
ecs-svc/xxx
のxxx
がサービスリビジョンの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では外部デプロイ以外も「プラグイン」によって近い将来扱えるようになると思います。
「プラグイン」の詳細はこちらの記事を参照してください。
外部デプロイ検証時に便利なコマンドツールも個人で開発中なので、いずれ公開予定です。
-
DeleteTaskSet
を呼び出すと、このようなエラーメッセージが表示されます。An error occurred (InvalidParameterException) when calling the DeleteTaskSet operation: Primary TaskSet cannot be deleted
↩︎ -
https://pipecd.dev/docs/user-guide/managing-application/application-live-state/ ↩︎
Discussion