Claude Media
Claude CodeでECRのライフサイクルポリシーを書く — 消してはいけないタグを先に守る

Claude CodeでECRのライフサイクルポリシーを書く — 消してはいけないタグを先に守る

ECRのライフサイクルポリシーは稼働中かどうかを見ません。残すタグを高い優先度で守る書き方と、プレビューの読み方、Claude Codeに任せる権限の線引きをまとめます。

ECR(Amazon Elastic Container Registry)のライフサイクルポリシーは、タグ・経過日数・本数の3つだけでイメージを選びます。ECSやKubernetesで動いているかどうかは、判断の材料に入りません。つまり「稼働中のイメージを除く」と書くことはできず、残したいものをタグで宣言し、高い優先度で守る書き方になります。

この記事では、守るルールを先頭に置いたポリシーの書き方、aws ecr start-lifecycle-policy-preview で削除対象を確かめる手順、稼働中のイメージと突き合わせる手順を順に扱います。Claude Codeには、案の作成とプレビューの実行だけを許可し、適用は人が押す構成にします。

ポリシーが見るもの、見ないもの

ポリシーのルールは、tagStatus・tagPatternList(またはtagPrefixList)・countType・countNumberなどの項目で構成されます。コンテナの実行状況や、タスク定義・Podからの参照を指定する項目はありません。

判断に使える判断に使えない
タグの有無とパターン(tagStatus)判断に使えないECSのサービスが参照しているか
作成からの日数(sinceImagePushed)判断に使えないKubernetesのPodが使っているか
最後のpullからの日数(sinceImagePulled)判断に使えないデプロイの履歴やロールバック先
リポジトリ内の本数(imageCountMoreThan)判断に使えない本番の承認を通したかどうか

評価の流れも先に押さえておきます。ECRは、すべてのルールを同時に評価し、そのあとルールの優先度に従って適用します。優先度の数字は小さいほど先に効きます。1枚のイメージを期限切れにできるルールは、ゼロか1つだけです。

ここから、守り方の核が出てきます。あるルールのタグ条件に合うイメージは、それより優先度の低いルールでは期限切れにされません。AWSの例でも、優先度1のprod*ルールが残したprod-2は、優先度2のbeta*ルールが対象にしても消えません。

消してはいけないタグを先頭で守るポリシー

稼働中のイメージを守る手段は、タグの規約です。本番に出すイメージには必ずprod-やv始まりのタグを付ける、というルールをCI側で決めておきます。タグを付ける側の設計は、ghcrに公開するワークフローのsha・semverタグの章と同じ考え方です。

そのうえで、次のポリシーを置きます。

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "prod-* を守る",
      "selection": {
        "tagStatus": "tagged",
        "tagPatternList": ["prod-*"],
        "countType": "imageCountMoreThan",
        "countNumber": 9999
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 2,
      "description": "v* を守る",
      "selection": {
        "tagStatus": "tagged",
        "tagPatternList": ["v*"],
        "countType": "imageCountMoreThan",
        "countNumber": 9999
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 3,
      "description": "タグなしは7日で削除",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 10,
      "description": "残りは新しい30本だけ残す",
      "selection": {
        "tagStatus": "any",
        "countType": "imageCountMoreThan",
        "countNumber": 30
      },
      "action": { "type": "expire" }
    }
  ]
}

優先度1と2は「守る」ためのルールです。countNumberに十分大きい値を入れて、実際には何も期限切れにしないまま、該当イメージを後続のルールから隠します。この「大きな本数で守る」書き方はAWSの例に載っているものではなく、上の評価規則から組み立てた設計です。効いているかどうかは、必ず次のプレビューで確かめます。

ほかの3点にも理由があります。

  • tagStatusがanyのルールは、最後に評価されるよう最大の優先度番号にする。優先度10にしたのはこのためです
  • 複数のタグを1つのtagPatternListに並べると、すべてに合うイメージだけが選ばれる(AND条件)。prod-*とv*を別ルールにしたのは、どちらか一方に合えば守りたいからです
  • countNumberに0は指定できない。「全部消す」ルールを本数で書くことはできません

イメージに複数のタグが付いているときも、守りは働きます。pr-12とprod-3の両方が付いたイメージは、優先度1のルールに合うので、pr-*を消すルールを後ろに足しても対象から外れます。

Claude Codeに任せる範囲を権限で切る

ポリシーの下書きとプレビューまでをClaude Codeに任せ、適用と削除は人の確認に残します。.claude/settings.jsonに次の権限を置きます。

{
  "permissions": {
    "allow": [
      "Bash(aws ecr describe-images *)",
      "Bash(aws ecr start-lifecycle-policy-preview *)",
      "Bash(aws ecr get-lifecycle-policy-preview *)",
      "Bash(aws ecs describe-task-definition *)",
      "Bash(kubectl get pods *)"
    ],
    "ask": [
      "Bash(aws ecr put-lifecycle-policy *)"
    ],
    "deny": [
      "Bash(aws ecr batch-delete-image *)",
      "Bash(aws ecr delete-lifecycle-policy *)"
    ]
  }
}

ルールは、deny、ask、allowの順に評価され、最初に当たったものが結果を決めます。広いdenyの内側に狭いallowを置いて例外にすることはできません。aws ecr *を丸ごとdenyにして個別にallowを足す書き方は成立しないので、上のように個別のコマンドで書き分けます。

*はサブコマンドの後ろに置きます。Bash(aws ecr put-lifecycle-policy *)のように、何を許す・止めるかを決める語を*の前に書くのが決まりです。

もう1点、実運用で効くのがパイプです。|や&&で繋いだコマンドは、すべての部分が許可されていないとプロンプトが出ます。jqで整形するより、AWS CLIの--query(JMESPath)で絞るほうが、許可ルールを増やさずに済みます。

プレビューで削除対象を確かめる

ポリシーをpolicy.jsonに保存し、プレビューを開始します。

aws ecr start-lifecycle-policy-preview \
  --repository-name app/api \
  --lifecycle-policy-text file://policy.json

--lifecycle-policy-textを省くと、リポジトリに適用済みのポリシーが評価されます。すでに動いているポリシーの影響を調べ直すときに使えます。ポリシー本文の長さは100〜30,720文字の範囲です。

開始直後のstatusはIN_PROGRESSです。取得側の状態にはIN_PROGRESS・COMPLETE・EXPIRED・FAILEDの4種類があるので、COMPLETEになるまで待ちます。

aws ecr get-lifecycle-policy-preview \
  --repository-name app/api \
  --query 'status'

COMPLETEになったら、削除対象をタグ・ダイジェスト・push日時・適用されたルールの優先度で一覧にします。

aws ecr get-lifecycle-policy-preview \
  --repository-name app/api \
  --query 'previewResults[].{tags:imageTags,digest:imageDigest,pushed:imagePushedAt,rule:appliedRulePriority}' \
  --output json

見るべき点は3つです。

  1. prod-*やv*のタグを持つイメージが1件も混ざっていないこと。混ざっていれば、守りのルールが想定どおりに効いていません
  2. ruleが10のイメージが、想定した「古い開発用イメージ」であること
  3. summary.expiringImageTotalCountが、リポジトリの総数に対して極端に大きくないこと

稼働中のイメージと突き合わせる

タグ規約で守っていても、規約から外れたタグで本番に出ているイメージがあるかもしれません。プレビュー結果を、実際に動いているイメージの一覧と照合します。

ECSなら、タスク定義からイメージ参照を取り出します。

aws ecs describe-task-definition \
  --task-definition api \
  --query 'taskDefinition.containerDefinitions[].image' \
  --output text

Kubernetesなら、全名前空間のPodが使うイメージをkubectlのjsonpathで取れます。

kubectl get pods --all-namespaces \
  -o jsonpath="{.items[*].spec['initContainers', 'containers'][*].image}"

出てきたイメージ名は、リポジトリ:タグの形か、リポジトリ@sha256:...のダイジェスト指定のどちらかです。プレビューのimageTagsとimageDigestの両方で照合します。タグだけで比べると、ダイジェスト指定のデプロイを見落とします。

Claude Codeには、この照合を頼めます。依頼の形は次のとおりです。

app/api のライフサイクルポリシーをプレビューして、previewResults の
imageTags と imageDigest が、ECS のタスク定義と kubectl の出力に
1件でも含まれていないか照合して。含まれていたら適用せず、
該当イメージとルールの優先度を一覧で報告して。

照合で重なりが出たら、そのイメージのタグを守りのパターンに合わせて付け直すか、ポリシー側のパターンを足します。重なりが0件になったのを見てから、適用に進みます。

稼働中のものだけでなく、ロールバック先のタスク定義やDeploymentの直近リビジョンが参照するイメージも対象です。「今動いている」だけで判断すると、戻したときに取得できなくなります。ECSの直近リビジョンを何世代まで残すかは、チームの戻し方で決める項目です。

適用と、適用後に起きること

プレビューの結果に納得したら、put-lifecycle-policyで適用します。ここは権限をaskにしてあるので、人が承認します。

aws ecr put-lifecycle-policy \
  --repository-name app/api \
  --lifecycle-policy-text file://policy.json

適用後、条件に合うイメージは24時間以内に期限切れになります。実行された操作はAWS CloudTrailにイベントとして記録されるので、あとから「何が消えたか」を追えます。

消すのではなく移したいときは、アクションをtransitionにしてアーカイブ用のストレージに送る書き方があります。アーカイブされたイメージをsinceImageTransitionedで期限切れにする場合、アーカイブに最低90日置く条件があります。本番のロールバック用イメージを消さずに安く持ちたいときの選択肢です。

つまずきやすい点

落とし穴

pull時刻で消すと、長く動いているイメージが対象になる

sinceImagePulledは、最後のpullからの日数で選びます。長期間再起動していないタスクやPodのイメージは、動いていてもpullされないので、古いpull時刻のまま残ります。稼働中の保護には使わず、prod-*の守りルールを前に置いてから使います。pull時刻は少なくとも24時間に1回の更新で、正確な最終pull時刻とずれることもあります。

  • レプリケーションを使っているとき。ポリシーは作成したリージョンのリポジトリにしか効きません。レプリカ側にも別にポリシーを作ります
  • マニフェストリスト(マルチアーキテクチャ)のイメージ。マニフェストリストが参照しているイメージは、リストが先に削除・アーカイブされないと期限切れになりません。参照されている側のタグが古くても、すぐには消えないことがあります
  • ワイルドカードは1つの文字列に4個まで。*a*b*c*d*eのようなパターンは無効です
  • プレビューの状態がEXPIRED。時間が経ったプレビュー結果は使えなくなります。適用の直前にもう一度開始し直します

S3側の同じ作業は、S3ライフサイクルルールの記事で扱っています。AWS全体で未使用のものを洗い出す段階は、describe系だけ許可して削除はTerraformへ回す手順が前段になります。ECSタスク側の見直しはFargateタスク定義の記事にあります。

まとめ

ECRのライフサイクルポリシーは、稼働状況を見ないので、残したいものはタグ規約と優先度で守ります。優先度の高いルールに合うイメージは、低いルールでは消えません。この性質を使い、prod-*やv*を先頭の守りルールに置いてから、タグなしと古い開発用イメージを消すルールを足します。

Claude Codeには、案の作成、プレビューの実行、稼働中イメージとの照合までを任せられます。適用をask、削除系をdenyにし、さらにIAMで削除権限を持たせない構成にすれば、プレビューで確かめる前に消える事故は起こしにくくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html
  3. docs.aws.amazon.com/AmazonECR/latest/userguide/lifecycle_policy_parameters.html
  4. docs.aws.amazon.com/AmazonECR/latest/userguide/lifecycle_policy_examples.html
  5. docs.aws.amazon.com/AmazonECR/latest/userguide/lpp_creation.html
  6. docs.aws.amazon.com/cli/latest/reference/ecr/get-lifecycle-policy-preview.html
この記事を共有:XはてブLinkedIn