ECRのライフサイクルポリシーをCloudFormationで定義する方法とその設定指針

「やだ、私のECR、知らないうちにアホみたいに料金かかってる…!!」

以前、こんな記事書きました。

ECRのライフサイクルポリシー設定によるリポジトリ容量の節約 | DevelopersIO

ECRに格納するコンテナイメージは、概ね100MB〜500MB(たまには1G超も)と結構な容量を食います。コンテナビルドするたびにこれが増えていくので、気にせずつかっていると、ECRの料金だけでそれなりの金額がかかってしまいます。

ちなみに、ストレージコストは1ヶ月あたりGB単位で0.10USD(料金 - Amazon EC2 Container Registry | AWS)。

そのため、上の記事でライフサイクルポリシーの設定を推奨しているわけですが、今回の記事では、それをCloudFormationで設定する方法をご紹介します。

(祭) ∧ ∧
 Y  ( ゚Д゚)
 Φ[_ソ__y_l〉     ECRダイエットダワッショイ
    |_|_|
    し'´J

ECRライフサイクルポリシーを設定したCloudFormationテンプレートの紹介

ECR作成時に、合わせてライフサイクルポリシーを設定したYAMLテンプレートはこんな感じ。シンプル。

AWSTemplateFormatVersion: '2010-09-09'
Description: ecr

Resources:
  ecr:
    Type: AWS::ECR::Repository
    Properties: 
      RepositoryName: ecr
      LifecyclePolicy:
        LifecyclePolicyText: '{"rules":[{"rulePriority":1,"description":"Delete more than 20 images","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":20},"action":{"type":"expire"}}]}'
Outputs:
	ecr:
    Value: ecr

シンプルっちゃシンプルなんですが、ハイライトした10行目LifecyclePolicyTextの部分、YAMLテンプレートの中にJSONが混ざってるのは、可読性がイマイチです。

ECRのライフサイクルポリシーはJSON形式のみサポート

CloudFormation、ECRの公式マニュアル(AWS::ECR::Repository)の記載の通り、LifecyclePolicyは、JSON形式でしか指定できません。

リポジトリに適用する JSON リポジトリポリシーテキストです。長さは 100 ~ 10,240 文字である必要があります。
引用:Amazon Elastic Container Registry Repository LifecyclePolicy - AWS CloudFormation

実は最初、Amazon ECR ライフサイクルポリシーを参考に作成したJSONを自分でYAMLに変換してテンプレートに書いていたんですが、「そんなもん無理やで」ってエラーが出たので調べていたら、JSON形式しか対応していないことに気づきました。

(TдT)

IAMポリシーがYAMLで書けるのは実は特別扱いだった!

なんでそんな勘違いしていたかというと、JSON定義リソースの代表格IAMポリシーが、CloudFormationテンプレート内ではYAMLで書けていたからです。ただ、改めてマニュアルをみてると、これはIAMポリシーだけが特別のようですね。

AWS Identity and Access Management (IAM) では、ポリシーが JSON 形式である必要があります。ただし、YAML 形式のテンプレートでは、JSON または YAML 形式で IAM ポリシーを作成できます。AWS CloudFormation は、IAM に送信する前に常にポリシーを JSON 形式に変換します。
引用:AWS::IAM::Policy - AWS CloudFormation

ちょっと話が脱線しましたが、ECRのライフサイクルポリシーはCloudFormationがYAMLテンプレートであろうが、JSONで指定する必要があります。

ECRライフサイクルポリシーのJSON指定方法

基本は、公式マニュアル(Amazon ECR ライフサイクルポリシー)を参考にしながら、以下のテンプレートを元に作っていけばOKです。

{
    "rules": [
        {
            "rulePriority": integer,
            "description": "string",
            "selection": {
                "tagStatus": "tagged"|"untagged"|"any",
                "tagPrefixList": list<string>,
                "countType": "imageCountMoreThan"|"sinceImagePushed",
                "countUnit": "string",
                "countNumber": integer
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}

例として、最新から20世代以上のイメージを自動的に削除するポリシーがこちら。

{
	"rules": [
		{
			"rulePriority": 1,
			"description": "Delete more than 20 images",
			"selection": {
				"tagStatus": "any",
				"countType": "imageCountMoreThan",
				"countNumber": 20
			},
			"action": {
				"type": "expire"
			}
		}
	]
}

YAMLにJSONを複数行で指定する場合

上のJSONを、以下のようにYAMLに対する文字列の複数行指定で挿入すればOKです。冒頭の|を利用した、リテラルスタイルがわかりやすいんじゃないでしょうか(多謝:Ryo Nakamaru(@pottava)さん)。

LifecyclePolicyText: |
	{
		"rules": [
			{
				"rulePriority": 1,
				"description": "Delete more than 20 images",
				"selection": {
					"tagStatus": "any",
					"countType": "imageCountMoreThan",
					"countNumber": 20
				},
				"action": {
					"type": "expire"
				}
			}
		]
	}

YAMLの中にいきなりJSONが混ざっているのも違和感があるかもですが、可読性はこちらが圧倒的に良いので、この方式をオススメします。もちろん、YAMLのインデントには気をつけましょう。

YAMLにJSONを一行で指定する場合

もちろん一行でもOK。jq-cオプションなどで一行に変換します(lifecyclepolicy.jsonに、上のJSONが記入されている前提)。

$ cat lifecyclepolicy.json | jq -c
{"rules":[{"rulePriority":1,"description":"Delete more than 20 images","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":20},"action":{"type":"expire"}}]}

これを、最初に紹介したYAMLテンプレートのLifecyclePolicyTextに、シングルクォーテーションでくくって文字列として指定すればOK。このとき、ダブルクォーテーションだと、JSON中のダブルクォーテーションを全部エスケープする必要があるのでご注意ください。

LifecyclePolicyText: '{"rules":[{"rulePriority":1,"description":"Delete more than 20 images","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":20},"action":{"type":"expire"}}]}'

ECRライフサイクルポリシーをGUIで設定してCLI抽出する方法

上のJSONパラメータを作るのがめんどくさければ、GUIで設定してその内容をCLIで参照して使うのが早かったりします。

ECRのメニューから適当なリポジトリを選んでライフサイクルポリシーの作成画面にはいり、自分の指定したい内容でコンソールから指定します。

作成後、aws cliのget-lifecycle-policyで、設定内容を表示。このとき、出力をjson指定していると、ダブルクォーテーションがエスケープされるので、text形式で指定することをおすすめします。

$ aws ecr get-lifecycle-policy --repository-name sample-repo --query 'lifecyclePolicyText' --output text
{"rules":[{"rulePriority":1,"description":"ステージングで10日以上経過したイメージの自動削除","selection":{"tagStatus":"tagged","tagPrefixList":["stg"],"countType":"sinceImagePushed","countUnit":"days","countNumber":10},"action":{"type":"expire"}}]}

これで、欲しいライフサイクルポリシーのJSONが取得できたので、あとは先程と同様YAMLテンプレートに挿入してCloudFormationを流しましょう。

既存のECRリポジトリにCLIから設定する方法

CloudFormationでリポジトリの作成と合わせてライフサイクルポリシーを設定する場合は上記方法で問題ないですが、すでにECRの運用が始まっていて、リポジトリたくさんあるところに後から設定したい場合も多いでしょう。その場合は、aws cliのput-lifecycle-policyを使うのが便利です。

put-lifecycle-policy — AWS CLI 1.16.104 Command Reference

上の方法で同じように、ライフサイクルポリシーをカレントディレクトリのlifecyclepolicy.jsonに保存しておき、後は以下の方法でライフサイクルポリシーの設定が可能です。sample-repoは、ECRリポジトリの名前です。

aws ecr put-lifecycle-policy --repository-name sample-repo --lifecycle-policy-text "file://lifecyclepolicy.json"

実際に設定するECRライフサイクルポリシーってどんなのが良いか?

皆さんの運用現場で最適なものを選んでいただければと思いますが、特にこだわりがなければ、最新イメージ20個以上は自動的に削除がわかりやすくて良いと思います。以下がそのポリシー。

{
	"rules": [
		{
			"rulePriority": 1,
			"description": "Delete more than 20 images",
			"selection": {
				"tagStatus": "any",
				"countType": "imageCountMoreThan",
				"countNumber": 20
			},
			"action": {
				"type": "expire"
			}
		}
	]
}

一つのリポジトリに対して複数のルールを優先順位付けて設定可能なので、イメージタグのプレフィックスによって条件を変更(prd-stg-とか)もできます。が、そもそも環境違うならリポジトリも別の方が良いのでは?と思ったりするのと、こういうルールって一度設定するとあまり外からは見えにくくなりがちなので、複雑なルールにすることを個人的にはオススメしません。

まとめ「今からでも遅くない。さくっと設定して、無駄な容量をくわないようECRの設定を見直しましょう」

すでに開発〜運用が始まっている現場だと、イメージが累積100個以上あるリポジトリもざらにあると思います。ただ、コンテナイメージは基本ソースコードがあれば復旧可能なのと、ECSタスク定義でのロールバックで使うにも、20世代もあればほとんど問題にはならないでしょう。

さくっと設定して不要な課金を抑えるように検討と設定変更していただければと思います。

それでは、今日はこのへんで。濱田(@hamako9999)でした。