🧹

ecrmを定期実行してECRの不要イメージを安全に削除する

2023/06/12に公開

こんにちは、株式会社スマートラウンドSREの@shonansurvivorsです。

当社はプロダクトのコンテナイメージをAmazon ECRに保存しています。

今回、コスト削減の一環でECRに溜まっていく不要イメージを削除することにしました。
その際、ecrmを定期的に実行して削除するようにしました。

ecrmとは

ecrmは「使用されていない」ECRのイメージを削除するOSSです。

https://github.com/fujiwara/ecrm

ECRのライフサイクルポリシーは、世代数や、イメージがpushされてからの日数などに基づいて古いイメージを削除してくれますが、ECSタスク等で使用中のイメージを削除してしまう懸念があります。

ecrmはそうしたこと無く、安全に古いイメージを削除することのできる大変ありがたいOSSです。

詳細な仕様はecrmを紹介するブログを参照ください。

https://techblog.kayac.com/ecrm-oss

定期削除の構成図

ecrmはCodeBuild上で実行することにしました。また、CodeBuildの定期実行にあたっては、EventBridge Schedulerを使うことにしました。

なお、構成図中にECSやLambdaが登場していますが、これはecrmが不要イメージの洗い出しにあたり、ECSやLambdaを参照するためです。

構成図

環境

  • ecrm v0.3.3
  • Terraform v1.4.6
  • AWS Provider v4.64.0

ecrm.yamlの作成

ecrmによるイメージ削除にあたっては、専用のコンフィグファイルが必要です。ecrm generateで作成可能なので、ローカルで実行します。

brew install fujiwara/tap/ecrm
ecrm generate
ecrm.yaml
clusters:
  - name_pattern: example-*
task_definitions:
  - name_pattern: example-*
    keep_count: 5
lambda_functions: []
repositories:
  - name_pattern: example-*
    expires: 14d
    keep_count: 5
    keep_tag_patterns:
      - latest

設定ファイルの詳細は以下を参照ください。

Terraformファイル構成

先ほどの構成図で登場した各種AWSリソースはTerraformで作成することにしました。

Terraformおよび各種yaml(yml)のファイル構成は以下です。tfファイルは説明のためにかなり細かくファイルを分けていますが、実際は1ファイルにまとめるなりして良いと思います。

├── ecrm.yaml
├── ecrm_buildspec.yml
├── ecrm_codebuild.tf 
├── ecrm_codebuild_iam.tf
├── ecrm_codebuild_scheduler.tf
└── ecrm_codebuild_scheduler_iam.tf

CodeBuild(ecrm)実行用のIAM

ecrmを実行させるCodeBuild用のIAMは以下の通りです。ECRのイメージを削除する権限のほか、ECSやLambdaを参照する権限を付与しています。

ecrm_codebuild_iam.tf
data "aws_iam_policy_document" "codebuild_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["codebuild.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "codebuild_ecrm" {
  name = "codebuild-ecrm"

  assume_role_policy = data.aws_iam_policy_document.codebuild_assume_role.json

  managed_policy_arns = [
    aws_iam_policy.codebuild_ecrm.arn,
  ]
}

resource "aws_iam_policy" "codebuild_ecrm" {
  name = "codebuild-ecrm"

  policy = jsonencode(
    {
      "Version" : "2012-10-17",
      "Statement" : [
        {
          "Effect" : "Allow",
          "Action" : [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource" : [
            "arn:aws:logs:region:account_id:log-group:/aws/codebuild/codebuld_project_name",
            "arn:aws:logs:region:account_id:log-group:/aws/codebuild/codebuld_project_name:*"
          ]
        },
        {
          "Effect" : "Allow",
          "Action" : [
            "lambda:Get*",
            "lambda:List*",
            "ecs:Describe*",
            "ecs:Get*",
            "ecs:List*",
            "ecr:BatchGet*",
            "ecr:BatchDelete*",
            "ecr:Describe*",
            "ecr:Get*",
            "ecr:List*"
          ],
          "Resource" : "*"
        }
      ]
    }
  )
}

CodeBuild

CodeBuildプロジェクトは以下の通りです。

ecrm_codebuild.tf
resource "aws_codebuild_project" "ecrm" {
  name          = "ecrm"
  description   = ""
  build_timeout = "60" # minutes
  service_role  = aws_iam_role.codebuild_ecrm.arn

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/standard:7.0"
    type         = "LINUX_CONTAINER"
  }

  source {
    type            = "GITHUB"
    location        = "https://github.com/organization/repository.git"
    buildspec       = "path/to/ecrm_buildspec.yml"
    git_clone_depth = 1
  }

  source_version = "main"
}

buildspec

CodeBuildでの実行内容を定義するbuildspecは以下の通りです。

ecrm_buildspec.yml
version: 0.2

env:
  shell: bash
  variables:
    ECRM_VERSION: 0.3.3

phases:
  install:
    commands:
      - curl -sL -O https://github.com/fujiwara/ecrm/releases/download/v${ECRM_VERSION}/ecrm_${ECRM_VERSION}_linux_amd64.tar.gz
      - tar xzvf ecrm_${ECRM_VERSION}_linux_amd64.tar.gz
      - sudo install ecrm /usr/local/bin/ecrm
      - ecrm --version

  build:
    commands:
      - ecrm --config path/to/ecrm.yaml delete --force

ecrmは、カレントディレクトリのecrm.yamlを自動的に読み込んでくれます。今回はリポジトリルートには存在しない想定で、--configオプションでパスを指定しています。

通常のdeleteでは、削除候補のイメージを洗い出した後に削除するかしないかを確認してきます。

      REPOSITORY      |    TOTAL    |   EXPIRED   |    KEEP      
----------------------+-------------+-------------+--------------
  example-web         | 55 (924 MB) | -5 (84 MB)  | 50 (840 MB)  
  example-app         | 55 (19 GB)  | -5 (1.7 GB) | 50 (17 GB)   
Do you delete 5 images on example-app? (y/n) [n]: 

今回はCodeBuildによる自動実行なので、--forceオプションを付けることで確認無しでそのまま削除させます。

EventBridge Scheduler用のIAM

CodeBuildを定期実行するためのEventBridge Scheduler用のIAMは以下の通りです。

なお、これは別途マネジメントコンソールで作成したIAMをそのままコード化しています。

ecrm_codebuild_scheduler_iam.tf
data "aws_caller_identity" "current" {}

resource "aws_iam_role" "codebuild_ecrm_scheduler" {
  name = "codebuild-ecrm-scheduler"
  path = "/service-role/"

  assume_role_policy = jsonencode(
    {
      "Version" : "2012-10-17",
      "Statement" : [
        {
          "Effect" : "Allow",
          "Principal" : {
            "Service" : "scheduler.amazonaws.com"
          },
          "Action" : "sts:AssumeRole",
          "Condition" : {
            "StringEquals" : {
              "aws:SourceAccount" : data.aws_caller_identity.current.account_id
            }
          }
        }
      ]
    }
  )

  managed_policy_arns = [
    aws_iam_policy.codebuild_ecrm_scheduler.arn
  ]
}

resource "aws_iam_policy" "codebuild_ecrm_scheduler" {
  name = "${var.project_name}-${var.env}-codebuild-ecrm-scheduler"

  policy = jsonencode(
    {
      "Version" : "2012-10-17",
      "Statement" : [
        {
          "Effect" : "Allow",
          "Action" : [
            "codebuild:StartBuild"
          ],
          "Resource" : [
            aws_codebuild_project.ecrm.arn
          ]
        }
      ]
    }
  )
}

EventBridge Scheduler

EventBridge Schedulerは以下の通りです。日本時間の月曜8時に実行するように設定しています。

ecrm_codebuild_scheduler
resource "aws_scheduler_schedule" "codebuild_ecrm" {
  name       = "codebuild-ecrm"
  group_name = "default"

  state = "ENABLED"

  flexible_time_window {
    maximum_window_in_minutes = 60
    mode                      = "FLEXIBLE"
  }

  schedule_expression          = "cron(0 8 ? * 2 *)"
  schedule_expression_timezone = "Asia/Tokyo"


  target {
    arn      = aws_codebuild_project.ecrm.arn
    input    = jsonencode({})
    role_arn = aws_iam_role.codebuild_ecrm_scheduler.arn

    retry_policy {
      maximum_event_age_in_seconds = 86400
      maximum_retry_attempts       = 2
    }
  }
}

実行結果

以上で、必要なリソースの構築は完了です。
指定した時間になると、EventBridge Scheduler経由でCodeBuildが起動し、ECRのイメージが削除されます。

// 略
      REPOSITORY      |    TOTAL    |   EXPIRED   |    KEEP      
----------------------+-------------+-------------+--------------
  example-web         | 55 (924 MB) | -5 (84 MB)  | 50 (840 MB)  
  example-app         | 55 (19 GB)  | -5 (1.7 GB) | 50 (17 GB) 
// 略
2023/06/11 23:31:41 [info] Deleted 5 images on example-web
// 略
2023/06/11 23:31:42 [info] Deleted 5 images on example-app

[Container] 2023/06/11 23:31:42 Phase complete: BUILD State: SUCCEEDED
[Container] 2023/06/11 23:31:42 Phase context status code:  Message: 
[Container] 2023/06/11 23:31:42 Entering phase POST_BUILD
[Container] 2023/06/11 23:31:42 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2023/06/11 23:31:42 Phase context status code:  Message: 

おまけ1:Slack通知

CodeBuildの開始や終了をSlack通知したい場合は、aws_codestarnotifications_notification_ruleを作成するのがお手軽です。

resource "aws_codestarnotifications_notification_rule" "codebuild_ecrm" {
  name     = aws_codebuild_project.ecrm.name
  resource = aws_codebuild_project.ecrm.arn

  detail_type = "BASIC"

  event_type_ids = [
    "codebuild-project-build-state-failed",
    "codebuild-project-build-state-succeeded",
    "codebuild-project-build-state-in-progress",
    "codebuild-project-build-state-stopped",
  ]

  target {
    address = aws_sns_topic.this.arn # AWS ChatbotがサブスクライブしているSNSトピックを指定
  }
}

AWS Chatbotを通して以下のようなSlack通知を行えます。これだけでは処理結果の詳細はわかりませんが、定期的に実行されているかどうかはわかります。

Slack

なお、SNSトピックのアクセスポリシーではcodestar-notifications.amazonaws.comからのPublishを許可するようにしてください。

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowPublish",
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "codestar-notifications.amazonaws.com",
          "events.amazonaws.com"
        ]
      },
      "Action": "sns:Publish",
      "Resource": "*"
    }
  ]
}

おまけ2:Security Hub

Security Hubのセキュリティ基準の1つである「AWS 基礎セキュリティのベストプラクティス v1.0.0」では、

[ECR.3] ECR リポジトリには、少なくとも 1 つのライフサイクルポリシーが設定されている必要があります
https://docs.aws.amazon.com/securityhub/latest/userguide/ecr-controls.html#ecr-3

というコントロールがあります。

全てのECRについて、ライフサイクルポリシーを使わず、ecrmで不要イメージを削除する運用を行うのであれば、このコントロールは無効化してしまって良いかと思います。

スマートラウンド テックブログ

Discussion