結論から
-
aws ecs update-service --force-new-deploymentは 現在サービスに設定されているタスク定義リビジョンをそのまま再デプロイ するコマンドである。新しく作られたタスク定義リビジョンを自動で適用したり、新リビジョンでタスクを更新したりはしない。 - ただし、タスク定義が
:latestなど可変タグを参照している場合は 、 再デプロイ時にタグが再解決される ため、ECR上の新しいイメージが参照される。 - タスク定義の設定変更・更新を反映したい場合は、
:latestを利用していても--force-new-deploymentだけでは不十分であり、新しいタスク定義リビジョンを登録(マネジメントコンソール orregister-task-definition) してから、update-service --task-definitionで切り替える必要がある。 - (本番運用を行う場合にはイミュータブルタグ + タスク定義リビジョン更新が最も確実)
1. 背景:発生した問題
イメージタグに:latestを使っているECSサービスに対して、以下の手順でデプロイ作業を行いました。
- 手動でタスク定義を更新 (環境変数など更新した新しいリビジョンを作成)
- Gitlab CIの手動承認アクションで修正したコードを
:latestタグでECRにプッシュ -
aws ecs update-service --force-new-deploymentでコンテナを更新
最初は「タスク定義も更新したし、イメージも(Gitlab CIの中で)pushしたから、新しい設定・新しいコードが反映されるはず」と考えていました。
しかし実際には、サービスのタスク定義が古いまま(新しく作成したタスク定義が反映されない)という現象に気づきました。コンテナの中身(コード)は新しくなっているのに、タスク定義は古いままでした。
ここに--force-new-deploymentに対する勘違いがあるので、整理していきたいと思います。
2. 前提知識:ECS のデプロイ構成要素を整理
原因を理解するために、まずECSの登場人物を整理します。
2.1 タスク定義/タスク/サービスの関係
| 用語 | 役割 |
|---|---|
| タスク定義(Task Definition) | コンテナの設計図。使用するイメージ、CPU/メモリ、環境変数などを定義 |
| タスク(Task) | タスク定義から起動された実行中のインスタンス(コンテナ) |
| サービス(Service) | 指定した数のタスクを維持し、デプロイを管理する仕組み |
2.2 タスク定義は「イミュータブルなリビジョン」である
タスク定義は一度登録すると上書きでの変更はできません。内容を変更したい場合は、my-definition:1 → my-definition:2 のように新しいリビジョンを登録する必要があります。
つまり、タスク定義に設定されたImageの参照も変更されません。例えば、my-definition:2にimage: my-repository:v1 と設定されていれば、my-definition:2はずっとmy-repository:v1を参照し続けます。参照イメージを変更したい場合も、やはり新しくリビジョンを更新する必要があります。
2.3 サービスが参照するのは「特定のタスク定義リビジョン」
サービスは『my-definition:2を2タスク維持する』というように、具体的なリビジョン番号を参照しています。この参照を書き換えない限り、サービスがどのイメージを起動するのか、どのタスク定義で起動するのかは変わりません。
3. なぜ--force-new-deployment では反映されないのか
--force-new-deployment は、現在サービスに設定されているタスク定義リビジョンを、そのまま強制的に再デプロイ(タスクを起動し直し)するオプションです。
ここで重要なのは、次の2点です。
-
タスク定義リビジョンは変わらない:新しいリビジョンを勝手に登録・適用することはしません。サービスが
my-definition:2を参照していれば、あくまでmy-definition:2が再デプロイされる。 - イメージの取得(pull)は再デプロイのたびに行われる:タスクを起動し直すので、タスク定義に書かれたイメージ参照は「そのタイミングで」解決される。
この「イメージ参照がどう解決されるか」が、可変タグ(:latest) か イミュータブルタグ(コミットハッシュ) かで大きく変わります。
3.2 具体例で見る挙動
以下の状況を考えます。
-
現在のサービスの設定:タスク定義
my-definition:2(イメージ指定はmy-repository:latest)で稼働中 -
裏での動き1(タスク定義):誰かが環境変数を変えた
my-definition:3を新しく作成(※サービスへの適用はまだ) -
裏での動き2(ECRイメージ):開発者がプログラムを修正し、新しいイメージを
:latestタグで ECR にプッシュした
この状態で --force-new-deployment を実行すると、次のように適用されます。
| 対象 | どうなるか |
|---|---|
| タスク定義 | 新しい my-definition:3 は無視され、現在サービスに設定されている my-definition:2 がそのまま適用される(環境変数や CPU/メモリ設定は古いまま) |
| コンテナイメージ |
my-definition:2 に書かれた :latest を読み、ECR にある最新のイメージを引っ張ってきて起動する(新しいコードは反映される) |
つまり :latest を使っている場合、--force-new-deployment で コードの更新は反映されるが、タスク定義の設定変更(環境変数など)は反映されない、という一見ちぐはぐな状態になります。これが冒頭で遭遇した「手動で新リビジョンを作ったのにタスク定義が新しくならない」問題の正体です。手動で作った新リビジョンは、サービスに明示的に適用しない限り無視されるのです。
3.3 新リビジョンは「明示的に適用」しないと無視される
冒頭の問題を整理すると、原因は次の一点に集約されます。
手動で作った新しいタスク定義リビジョンは、サービスに「このリビジョンを使う」と明示的に指定しない限り、
--force-new-deploymentでは適用されない。
--force-new-deployment はあくまで「今サービスが参照しているリビジョン」を再デプロイするだけなので、いくら新リビジョンを作っても、サービスの参照先を切り替えなければ無視されます。設定変更(環境変数・CPU/メモリなど)を反映するには、update-service --task-definition <新リビジョン> で参照先を明示的に切り替える必要があります。
これが今回の「タスク定義が新しくならない」問題の正体です。
3.4 補足:イミュータブルタグ(コミットハッシュ)を使う場合の挙動
今回は :latest 運用でしたが、イメージタグにコミットハッシュのようなイミュータブルタグを使っている場合は、さらに事情が異なります。
この場合、--force-new-deployment してもイメージすら更新されません。たとえばmy-definition:2が my-repository:abc123 を参照していると、新しいコードを my-repository:def456 という別タグで push しても、my-definition:2が指すのは変わらず abc123 のまま。何度再デプロイしても古いイメージが起動します。
つまりイミュータブルタグでは、コードの反映にも「タスク定義リビジョンの更新」が必須になります。これは一見手間が増えるように見えますが、実は次に述べる通り運用上のメリットが大きく、:latest よりも推奨されます。
3.5 それでも :latest を避けるべき理由(可変タグの落とし穴)
「新リビジョンを明示適用すればいいなら、:latest のままでも運用できるのでは」と思うかもしれません。しかし、:latest などの可変タグには本質的な問題があります。
-
どのイメージが動いているか追跡できない:
:latestが指す実体(ダイジェスト)は時々刻々と変わるため、「本番で動いているのはどのコミットか」を特定できない。 -
ロールバックが困難:
:latestは常に最新を指すので、過去のリビジョンに戻しても、そのリビジョンが参照する:latestは現在の最新イメージを引いてしまう。「1つ前の安定版のコード」に確実に戻せない。
結論として、:latest は再現性・トレーサビリティ・ロールバックの観点で本番運用には不向きです。ではどのように本番運用を行うのでしょうか。
4. 解決策:最新のタスク定義リビジョンを明示的に適用する
ここでは、タスク定義の管理(イメージタグや環境変数の更新)は、手動または CloudFormation / Terraform などで CI パイプラインの外で行うという、実運用でよくある前提に立ちます。この場合 CI パイプラインの役割はシンプルで、**「最新のタスク定義リビジョンを取得し、それをサービスに明示的に適用する」**だけです。
4.1 正しいデプロイフロー(全体像)
ポイントは、第3章で見た通り --force-new-deployment 単体ではなく --task-definition で参照先を明示的に指定することです。
4.2 実装:最新のタスク定義リビジョンを取得して適用
タスク定義ファミリー名を指定すれば、describe-task-definition は 最新の ACTIVE リビジョンを返します。その ARN を update-service に渡すだけです。
# 最新の ACTIVE なタスク定義リビジョンの ARN を取得
LATEST_TASK_DEF_ARN=$(aws ecs describe-task-definition \
--task-definition "${TARGET_TASK_DEF_WORKER}" \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
# そのリビジョンをサービスに明示的に適用
aws ecs update-service \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--task-definition "${LATEST_TASK_DEF_ARN}"
describe → register のような加工が不要になり、CI 側は「登録済みの最新リビジョンを指すだけ」という責務に絞られます。
補足:
--task-definitionを指定して update-service を行うと、参照先が変わるため自動的に新しいデプロイがトリガーされます。前回と同じリビジョンを指すケース(=タスク定義に変更がない場合)でも強制的に再デプロイしたいときは、あわせて--force-new-deploymentを付けます。
4.3 GitLab CI(.gitlab-ci.yml)への組み込み例
deploy:
stage: deploy
image: amazon/aws-cli:latest
script:
- |
# パイプライン外で更新済みの最新タスク定義リビジョンを取得
LATEST_TASK_DEF_ARN=$(aws ecs describe-task-definition \
--task-definition "${TARGET_TASK_DEF_WORKER}" \
--query 'taskDefinition.taskDefinitionArn' --output text)
# サービスに明示的に適用
aws ecs update-service \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--task-definition "${LATEST_TASK_DEF_ARN}"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- when: manual # 手動承認でデプロイする場合
4.4 補足:CI パイプライン内でイメージタグまで差し替えたい場合
「タスク定義の登録も含めて CI で完結させたい」場合は、現行タスク定義を取得し、jq でイメージタグを書き換えて新リビジョンを登録する方法もあります。パイプライン外管理よりも複雑ですが、コード変更のたびにイメージタグ(コミットハッシュ)を自動で反映したいケースに向いています。
# 現行タスク定義を取得し、jq でイメージタグを置換(register-task-definition が
# 受け付けない読み取り専用フィールドは削除する)
NEW_TASK_DEF=$(aws ecs describe-task-definition \
--task-definition "${TARGET_TASK_DEF_WORKER}" --query 'taskDefinition' --output json | jq \
--arg IMAGE "${ECR_REGISTRY}/${TARGET_ECR_REPO_WORKER}:${CI_COMMIT_SHORT_SHA}" \
'del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy) |
.containerDefinitions[0].image = $IMAGE')
# 新リビジョンを登録し、その ARN でサービスを更新
NEW_TASK_DEF_ARN=$(echo "$NEW_TASK_DEF" | aws ecs register-task-definition \
--cli-input-json file:///dev/stdin --query 'taskDefinition.taskDefinitionArn' --output text)
aws ecs update-service \
--cluster "${CLUSTER}" --service "${SERVICE}" \
--task-definition "${NEW_TASK_DEF_ARN}"
5. タグ戦略の比較:latest vs イミュータブルタグ
| 観点 |
:latest(可変タグ) |
コミットハッシュ(イミュータブルタグ) |
|---|---|---|
| 再現性 | 低い(同じタグで中身が変わる) | 高い(タグ=一意のイメージ) |
| ロールバック | 困難(過去の実体を特定できない) | 容易(過去のタグ/リビジョンを指定するだけ) |
| キャッシュ問題 | 起きやすい | 起きない(毎回別タグ) |
| トレーサビリティ | 弱い(どのコミットか不明) | 強い(タグからコミットを特定可能) |
| 監査 / インシデント調査 | しにくい | しやすい |
コミットハッシュタグが推奨される理由
-
CI_COMMIT_SHORT_SHA(GitLab)やgithub.sha(GitHub Actions)を使えば、イメージとソースコードが 1:1 で対応します。 - 「本番で動いているのはどのコミットか」が即座に分かり、障害調査やロールバックが確実になります。
ECR のタグイミュータビリティ設定
ECR リポジトリでは タグの上書きを禁止する設定(Tag immutability) を有効にできます。これにより「同じタグに別のイメージを push する」事故を防げます。
aws ecr put-image-tag-mutability \
--repository-name "${TARGET_ECR_REPO_WORKER}" \
--image-tag-mutability IMMUTABLE
6. 一歩進んだ運用
6.1 ローリングデプロイ vs Blue/Green(CodeDeploy)
- ローリングアップデート(ECS 標準):本記事のフロー。設定が簡単で追加コストなし。ただし切替中は新旧が混在する。
- Blue/Green(CodeDeploy 連携):新環境(Green)に全タスクを立ててから一括切替。即時ロールバックやカナリア/リニアなトラフィックシフトが可能。ミッションクリティカルな環境で有効。
6.2 デプロイの安定化:Deployment circuit breaker / ヘルスチェック
新リビジョンが起動失敗を繰り返す場合に、自動でロールバックさせる仕組みです。
aws ecs update-service \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--task-definition "${NEW_TASK_DEF_ARN}" \
--deployment-configuration '{"deploymentCircuitBreaker":{"enable":true,"rollback":true}}'
これに加え、ALB のヘルスチェックと healthCheckGracePeriodSeconds を適切に設定することで、起動直後の誤判定を防ぎます。
6.3 ロールバック戦略
イミュータブルタグ運用なら、ロールバックは過去のタスク定義リビジョンを指定するだけです。
# 1つ前の安定リビジョンに切り戻す
aws ecs update-service \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--task-definition my-definition:42
6.4 IaC との整合性(Terraform / CDK 管理下での注意点)
タスク定義を Terraform や CDK で管理している場合、CLI で登録した新リビジョンは IaC の状態と乖離(ドリフト) します。
-
対策1:Terraform 側で
ignore_changes = [task_definition]を設定し、デプロイは CI に任せる。 - 対策2:イメージタグの更新も含めて IaC を Single Source of Truth とし、CI から IaC を実行する。
チームの運用ポリシーに応じて、「誰がタスク定義の実体を管理するのか」を明確に決めておくことが重要です。
6.5 IAM 最小権限
CI 用のロールに付与する権限は最小限に絞ります。特に iam:PassRole を忘れがちなので注意が必要です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
"arn:aws:iam::<ACCOUNT_ID>:role/<taskRole>"
]
}
]
}
register-task-definition はタスク実行ロール/タスクロールを設定するため、それらの PassRole 権限がないと失敗します。
7. まとめ
-
--force-new-deploymentは 現行タスク定義リビジョンの「再デプロイ」。新しいリビジョンを自動で適用はしない(可変タグならイメージは再解決されるが、タスク定義の設定変更は反映されない)。 - イミュータブルタグ運用では、新リビジョン登録 →
update-service --task-definitionが新イメージ反映の正攻法。 - イミュータブルタグ(コミットハッシュ)+ リビジョン更新 を使うことで、再現性・ロールバック・トレーサビリティが劇的に向上する。