ECR ライフサイクルポリシーで古いイメージを自動削除する
Amazon ECR のライフサイクルポリシーを使って古いコンテナイメージを自動削除する設定を解説。タグ付きと untagged の使い分け・rollback 用イメージの保護・コスト削減効果まで実例で紹介します。
はじめに
GitHub Actions で毎回 ECR にイメージを push していると、リポジトリには 数百・数千のイメージが溜まります。古いイメージは月々のストレージコスト($0.10/GB)を押し上げる原因になりますし、コンソールで一覧を眺めるのも辛くなります。ECR のライフサイクルポリシーで自動掃除すると、運用負荷とコストの両方を下げられます。
課金構造の確認
ECR の主なコスト要素は次の 2 つ。
- ストレージ: $0.10 / GB / 月
- データ転送: 同一リージョン内 ECS / Lambda への pull は無料、外部リージョンや AWS 外への転送は別途課金
平均 200 MB のイメージを 1,000 個保管すると、200 MB × 1,000 = 200 GB、月 $20 のストレージコスト。掃除する価値はあります。
ライフサイクルポリシーの基本構造
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 production images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["prod-"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep last 5 dev images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["dev-"],
"countType": "imageCountMoreThan",
"countNumber": 5
},
"action": { "type": "expire" }
},
{
"rulePriority": 3,
"description": "Delete untagged images older than 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": { "type": "expire" }
}
]
}
ルールは rulePriority 番号の小さい順に評価されます。あるルールが「保持」と判定したイメージは、後続ルールで再評価されません。
推奨ルール構成(4 種)
Rule 1: 本番タグは多めに保持
{
"rulePriority": 10,
"description": "Keep last 30 prod-* images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["prod-"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
}
本番にデプロイ済みのイメージは rollback 用に多めに残します。30 個あれば、トラブル時に少なくとも数日前まで戻せます。
Rule 2: dev タグは少なめ
{
"rulePriority": 20,
"description": "Keep last 10 dev-* images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["dev-"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
}
dev は再ビルドしやすいので少なめで十分。
Rule 3: latest は守る(タグ前提)
latest タグは ECS サービスが参照しているので消さない。専用ルールを作らないことで、デフォルトで保持されます(タグ付きで他のルールに当たらないため)。
Rule 4: untagged は最短で消す
{
"rulePriority": 100,
"description": "Delete untagged after 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": { "type": "expire" }
}
タグなしイメージは「タグを付け替えて孤立した古いレイヤー」のことが多く、すぐ消して安全です。
SemVer タグ運用との組み合わせ
タグ付け方式が v1.2.3 のような SemVer なら、バージョン番号の正規表現でフィルタできます(ECR は正規表現ではなく tagPrefixList のみ対応)。SemVer なら v1 のような prefix で大版数を指定し、minor 以下は数で保持します。
実用上は次のような戦略が綺麗:
prod-{git_sha}: 本番デプロイ済みの commit ベースdev-{git_sha}: dev 環境用pr-{number}: PR プレビュー用(短命)latest,stable: 不変の参照タグ
pr- プレフィックスは「7 日経過で削除」など別ルールにすると、merge されない PR のイメージが残らず綺麗に保てます。
Terraform で管理する
CDK / Terraform で IaC 管理しておくと、本番・dev で同じポリシーを適用できます。
resource "aws_ecr_lifecycle_policy" "portal" {
repository = aws_ecr_repository.portal.name
policy = jsonencode({
rules = [
{
rulePriority = 10
description = "Keep last 30 prod-* images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["prod-"]
countType = "imageCountMoreThan"
countNumber = 30
}
action = { type = "expire" }
},
{
rulePriority = 100
description = "Delete untagged after 1 day"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 1
}
action = { type = "expire" }
}
]
})
}
ドライランで確認
ECR コンソールには 「ライフサイクルポリシーのプレビュー」 機能があります。本番に適用する前に「これを今動かしたら何が消えるか」を確認できます。AWS CLI でも実行可能:
aws ecr start-lifecycle-policy-preview \
--repository-name nagiyu-portal \
--lifecycle-policy-text file://policy.json
aws ecr get-lifecycle-policy-preview \
--repository-name nagiyu-portal \
--max-results 100
「想定外のタグが expired 候補に上がっていないか」を必ず確認してから本番適用します。
実装ノート
この記事では本番 / dev / untagged の 3〜4 ルールを推奨しましたが、nagiyu-platform の実際のポリシーはもっと割り切っています。共通の EcrStackBase がすべてのリポジトリに「Keep last N images(N はデフォルト 10)」というライフサイクルルールを 1 本だけ貼る作りで、maxImageCount: 10 を基本にしています。imageScanOnPush: true、タグ可変性は MUTABLE、削除ポリシーは prod が RETAIN・dev が DESTROY という設定です。SemVer タグや prod-/dev- プレフィックスでの細かい出し分けはあえてせず、「直近 10 個だけ残す」という単純なルールに寄せたのは、運用ルールを覚えなくて済むことを自分が優先したからです。
現在の運用
タグ運用で自分が気に入っているのは Quick Clip の構成です。Web イメージは GitHub Actions のデプロイで commit SHA と latest の両方を push し(SHA を追えば「どのコミットが本番にいるか」が一目で分かる)、Batch イメージは同じ latest を使わず、あえて batch-latest という別タグで運用しています。CDK のジョブ定義側も :batch-latest を参照しているので、Web と Batch の「最新」が混線しません。
正直に書くと、本文で勧めた「untagged を最短で消す」専用ルールは nagiyu-platform ではまだ入れていません。今は maxImageCount の数制限だけで回しているので、untagged イメージの自動削除は今後の改善余地として残しています。
ハマったポイント
ライフサイクルポリシーを運用してきて、自分が実際に怖いと思っているポイントを残します。
latestを巻き込む削除:latestが指す実体は別 SHA。tagStatus: "tagged"で雑に絞ると latest 込みで消えかねないので、消すならtagPrefixListで明示的に対象を限定する。- rulePriority の重複: 同じ番号は使えない。私は priority を 10, 20, 30… と飛び飛びに振って、後からルールを差し込めるようにしています。
- 現役 image manifest の巻き添え: ECS / Batch が今まさに参照しているイメージも、条件を満たせば削除候補になる。「現行の
latest/batch-latestは必ず残る」ことだけは死守します。 - マルチアーキテクチャイメージ: amd64 と arm64 を同タグでまとめている場合、片方だけ消えるとデプロイ時に解決失敗する。マルチアーキ運用ならルール検証を慎重に。
- 即時反映ではない: ライフサイクルは 1 日 1 回程度の評価。ルールを変えても消えないと焦りがちですが、半日ほど待つと適用されます。自分も最初これで「効いてない」と勘違いしました。
まとめ
ECR ライフサイクルポリシーを設定しておくだけで、コンテナイメージのストレージコストとリポジトリのノイズを大きく減らせます。本番 / dev / untagged の 3 ルール程度を最初に入れておけば、運用しながら育てる土台になります。