CFNカスタムリソースでECRイメージを削除する

CloudFormationカスタムリソースでECRイメージを削除する

CloudFormationを使ってECRを作成し、そこにイメージをプッシュすると、CloudFormationスタック時にエラーが発生する場合があります。
これはイメージが残った状態のECRを削除しようとしたことに起因します。

With the image remaining in ECR, the stack deletion fails.

今回はCloudFormationカスタムリソースを使って、CloudFormationスタック削除時に、自動的にECRリポジトリからイメージを削除します。
事前にイメージを削除することによって、エラーなくスタックが削除することを目指します。

構築する環境

Diagram of delete ECR images using CloudFormation custom resources

CloudFormationスタックを作成し、その内部に2つのリソースを定義します。

1つ目はECRです。
このリポジトリに対してイメージをプッシュします。

2つ目はLambda関数です。
この関数をカスタムリソースとして設定します。
スタック削除時に、自動的にECRリポジトリからイメージを削除するように設定します。
関数のランタイムはPython3.8とします。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/078

テンプレートファイルのポイント解説

本ページはカスタムリソースを使って、ECRイメージを削除する方法を中心に取り上げます。

CloudFormationカスタムリソースに関する詳細は、以下のページをご確認ください。

あわせて読みたい
CloudFormationカスタムリソース入門 【CloudFormationカスタムリソースの挙動を確認する構成】 CloudFormationの機能の1つにカスタムリソースがあります。 カスタムリソースを使用すると、テンプレートにカ...

カスタムリソースで実行するLambda関数

Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          account_id = os.environ['ACCOUNT_ID']
          ecr_repository_name = os.environ['ECR_REPOSITORY_NAME']

          ecr_client = boto3.client('ecr')

          DELETE = 'Delete'
          response_data = {}

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == DELETE:
                list_images_response = ecr_client.list_images(
                  registryId=account_id,
                  repositoryName=ecr_repository_name
                  )

                image_ids = list_images_response['imageIds']

                if len(image_ids) == 0:
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
                  return

                batch_delete_image_response = ecr_client.batch_delete_image(
                  registryId=account_id,
                  repositoryName=ecr_repository_name,
                  imageIds=image_ids
                  )
                print(batch_delete_image_response)

              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      Environment:
        Variables:
          ACCOUNT_ID: !Ref AWS::AccountId
          ECR_REPOSITORY_NAME: !Ref ECRRepositoryName
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)

関数そのものの設定に特別な項目はありません。
1つ挙げるとすると、環境変数がポイントです。
イメージを削除するためのパラメータ(リポジトリ名やアカウントID)を、環境変数として関数に渡します。

event[‘RequestType’]の値を参照して、スタック操作に応じた処理を実装します。
スタック削除時は「Delete」となります。
ですからif文を使って、スタック削除時に以下の処理が実行されるようにします。

  • list_imagesメソッドで、リポジトリ内に格納されているイメージのリストを取得する。
  • イメージ数が0の場合は処理を中断する。
  • batch_delete_imageメソッドで、イメージを削除する。

関数の実行完了メッセージをCloudFormationスタックに返す必要があります。
今回はcfnresponse.sendを使って実装しています。

なお今回実装したオブジェクト削除に関して、課題が1つあります。
それは1000以上のイメージを削除することを想定していないことです。
boto3のlist_imagesは1000件を超えるイメージデータを取得しようとすると、残りのデータを取得するためのトークンを返す仕様となっています。
今回のコードでは、その辺りの処理が未実装となっています。

以下は上記関数用のIAMロールです。

Resources:
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-ECRDeleteImagesPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ecr:BatchDeleteImage
                  - ecr:ListImages
                Resource:
                  - !Ref ECRRepositoryArn
Code language: YAML (yaml)

先に確認した通り、関数はECRに格納されているイメージのリストと、削除を行います。
ですからこれらを許可する権限を付与します。

カスタムリソース

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !Ref FunctionArn
Code language: YAML (yaml)

ServiceTokenプロパティに、先述のLambda関数のARNを指定します。
この設定によって、CloudFormationスタックの操作時に、都度関数が実行されることになります。

ECR

Resources:
  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref Prefix
Code language: YAML (yaml)

ECRリポジトリです。
特別な設定は不要です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ECR:fa-078

動作確認

イメージプッシュ

準備が整いましたので、ECRにイメージをプッシュします。

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [account-id].dkr.ecr.ap-northeast-1.amazonaws.com
...
Login Succeeded


$ docker build -t fa-078 .
...
Successfully tagged fa-078:latest


$ docker tag fa-078:latest [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-078:latest


$ docker push [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-078:latest
The push refers to repository [[account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-078]
209cb42bdfb7: Pushed
latest: digest: sha256:4adf0089f316607778fd6a5e073205b767bd849ac8a2234921fddc4351139b96 size: 529Code language: JavaScript (javascript)

イメージのビルド・プッシュが正常に実行できました。

ECRを確認します。

Detail of ECR 1.

確かにイメージが格納されています。

CloudFormationスタック削除

ECRにイメージが残っている状態でスタック削除を試みます。

Detail of ECR 2.

削除処理が完了するまで待機します。

Detail of ECR 3.

スタック削除が完了しました。
つまりスタック削除時に、CloudFormationカスタムリソースによって、先んじてECRリポジトリ内に残っていたイメージが削除されたということです。

カスタムリソースに関連づいているLambda関数の動作を確認します。

Detail of CloudFormation Custom Resources Action.

CloudWatch Logsに配信されたログを見ると、2つのことがわかります。

1つ目はECRに格納されているイメージのリストを取得していることです。
今回は1つのイメージが格納されていることが読み取れます。

2つ目は本関数がCloudFormationカスタムリソースとして動作したということです。
cfnresponseモジュールを使って、CloudFormationスタックに対して、メッセージを送信していることが読み取れます。

以上のことから、スタック削除時に、先んじて、自動的にECRリポジトからイメージを削除していることがわかります。

まとめ

CloudFormationカスタムリソースを使って、CloudFormationスタック削除時に、自動的にECRリポジトリからイメージを削除する方法を確認しました。