【小ネタ】ECS Express Modeのリソース削除の順番を間違えてDeleteCertificateが無限ループしていた話

【小ネタ】ECS Express Modeのリソース削除の順番を間違えてDeleteCertificateが無限ループしていた話

CloudTrailで`DeleteCertificate`が数秒おきに延々と記録され続ける現象に遭遇しました。その調査から原因特定、そして解消までの全てをお話しします。
2026.08.15

はじめに

皆様こんにちは、あかいけです。

先日、検証用アカウントのCloudTrailを眺めていたところ、DeleteCertificateというイベントが数秒おきに延々と記録され続けているのに気づきました。

自分で証明書を削除しようとした覚えはなかったため、原因を調査することにしました。
本記事ではその調査の流れと原因、そして対処方法をまとめます。

先にまとめ

先に結論をまとめます。

  • 事象
    • CloudTrailにacm:DeleteCertificateAccessDeniedのまま数秒おきに記録され続けていた
    • 実行元はAmazon ECS(Express Gateway Services)のマネージドロールecs-express-infrastructure-roleだった
  • 原因
    • 削除対象のACM証明書はすでに存在しておらず、マネージドポリシーのタグ条件を満たせないためAccessDeniedになっていた
    • ECS側は「削除できたか」を判定できず、同じ削除処理をリトライし続けていた
  • 解消方法
    • 該当の証明書に対する削除権限を明示的に付与したところ、ResourceNotFoundExceptionを返すようになり、リトライのループが解消した

処理の流れを図にすると以下のとおりです。

何が起きたか

CloudTrailにDeleteCertificateが並び続ける

まずはCloudTrailのイベント履歴を確認します。次のように、DeleteCertificateが短い間隔で大量に記録され続けていました。

スクリーンショット 2026-08-13 11.59.57

スクリーンショット 2026-08-13 12.00.10

イベントレコードの中身は以下の通りでした。

イベントレコード
{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:ECSGateway",
        "arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/ecs-express-infrastructure-role/ECSGateway",
        "accountId": "XXXXXXXXXXXX",
        "accessKeyId": "ASIAXXXXXXXXXXXXXXXX",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::XXXXXXXXXXXX:role/ecs-express-infrastructure-role",
                "accountId": "XXXXXXXXXXXX",
                "userName": "ecs-express-infrastructure-role"
            },
            "attributes": {
                "creationDate": "2026-08-13T02:58:19Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "ecs.amazonaws.com"
    },
    "eventTime": "2026-08-13T02:58:19Z",
    "eventSource": "acm.amazonaws.com",
    "eventName": "DeleteCertificate",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "ecs.amazonaws.com",
    "userAgent": "ecs.amazonaws.com",
    "errorCode": "AccessDenied",
    "errorMessage": "User: arn:aws:sts::XXXXXXXXXXXX:assumed-role/ecs-express-infrastructure-role/ECSGateway is not authorized to perform: acm:DeleteCertificate on resource: arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX because no identity-based policy allows the acm:DeleteCertificate action",
    "requestParameters": null,
    "responseElements": null,
    "requestID": "5fafc5db-e2ad-4e94-b9ca-7b9fe3a15097",
    "eventID": "d812a591-0b6b-4517-b55b-9a14a29191c6",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "XXXXXXXXXXXX",
    "eventCategory": "Management"
}

原因について

実行元のロールとポリシー

前述のイベントレコードからいくつか重要な点が読み取れます。

  • userIdentity.arnecs-express-infrastructure-role/ECSGatewayであり、実行元はユーザーではなくECSのマネージドなロールである
  • invokedByecs.amazonaws.comとなっており、ECSサービスが自動的に呼び出している
  • errorCodeAccessDeniedで、acm:DeleteCertificateが許可されていないために失敗している

ecs-express-infrastructure-roleは、ECSのExpress Gateway Services(ロードバランサーやACM証明書といった周辺リソースを、ECSがマネージドに構築・管理してくれる仕組み)が利用するロールです。このロールにアタッチされているマネージドポリシーAmazonECSInfrastructureRoleforExpressGatewayServicesの内容を確認します。

スクリーンショット 2026-08-13 12.00.56

証明書に関する権限は、次のポリシーのCertificateOperationsステートメントで定義されています。

ポリシー
AmazonECSInfrastructureRoleforExpressGatewayServices
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ServiceLinkedRoleCreateOperations",
            "Effect": "Allow",
            "Action": "iam:CreateServiceLinkedRole",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:AWSServiceName": [
                        "ecs.application-autoscaling.amazonaws.com",
                        "elasticloadbalancing.amazonaws.com"
                    ]
                }
            }
        },
        {
            "Sid": "ELBOperations",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:CreateTargetGroup",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:ModifyRule",
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:RemoveListenerCertificates",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:DeleteTargetGroup",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:DeleteRule",
                "elasticloadbalancing:DeleteListener"
            ],
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "TagOnCreateELBResources",
            "Effect": "Allow",
            "Action": "elasticloadbalancing:AddTags",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"
            ],
            "Condition": {
                "StringEquals": {
                    "elasticloadbalancing:CreateAction": [
                        "CreateLoadBalancer",
                        "CreateListener",
                        "CreateRule",
                        "CreateTargetGroup"
                    ]
                }
            }
        },
        {
            "Sid": "BlanketAllowCreateSecurityGroupsInVPCs",
            "Effect": "Allow",
            "Action": "ec2:CreateSecurityGroup",
            "Resource": "arn:aws:ec2:*:*:vpc/*"
        },
        {
            "Sid": "CreateSecurityGroupResourcesWithTags",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSecurityGroup",
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:AuthorizeSecurityGroupIngress"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:security-group-rule/*",
                "arn:aws:ec2:*:*:vpc/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:RequestTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "ModifySecurityGroupOperations",
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:DeleteSecurityGroup",
                "ec2:RevokeSecurityGroupEgress",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:vpc/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "TagOnCreateEC2Resources",
            "Effect": "Allow",
            "Action": "ec2:CreateTags",
            "Resource": [
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:security-group-rule/*"
            ],
            "Condition": {
                "StringEquals": {
                    "ec2:CreateAction": [
                        "CreateSecurityGroup",
                        "AuthorizeSecurityGroupIngress",
                        "AuthorizeSecurityGroupEgress"
                    ]
                }
            }
        },
        {
            "Sid": "CertificateOperations",
            "Effect": "Allow",
            "Action": [
                "acm:RequestCertificate",
                "acm:AddTagsToCertificate",
                "acm:DeleteCertificate",
                "acm:DescribeCertificate"
            ],
            "Resource": [
                "arn:aws:acm:*:*:certificate/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "ApplicationAutoscalingCreateOperations",
            "Effect": "Allow",
            "Action": [
                "application-autoscaling:RegisterScalableTarget",
                "application-autoscaling:TagResource",
                "application-autoscaling:DeregisterScalableTarget"
            ],
            "Resource": [
                "arn:aws:application-autoscaling:*:*:scalable-target/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "ApplicationAutoscalingPolicyOperations",
            "Effect": "Allow",
            "Action": [
                "application-autoscaling:PutScalingPolicy",
                "application-autoscaling:DeleteScalingPolicy"
            ],
            "Resource": [
                "arn:aws:application-autoscaling:*:*:scalable-target/*"
            ],
            "Condition": {
                "StringEquals": {
                    "application-autoscaling:service-namespace": "ecs"
                }
            }
        },
        {
            "Sid": "ApplicationAutoscalingReadOperations",
            "Effect": "Allow",
            "Action": [
                "application-autoscaling:DescribeScalableTargets",
                "application-autoscaling:DescribeScalingPolicies",
                "application-autoscaling:DescribeScalingActivities"
            ],
            "Resource": [
                "arn:aws:application-autoscaling:*:*:scalable-target/*"
            ]
        },
        {
            "Sid": "CloudWatchAlarmCreateOperations",
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricAlarm",
                "cloudwatch:TagResource"
            ],
            "Resource": [
                "arn:aws:cloudwatch:*:*:alarm:*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:RequestTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "CloudWatchAlarmOperations",
            "Effect": "Allow",
            "Action": [
                "cloudwatch:DeleteAlarms",
                "cloudwatch:DescribeAlarms"
            ],
            "Resource": [
                "arn:aws:cloudwatch:*:*:alarm:*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "ELBReadOperations",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeRules"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VPCReadOperations",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSubnets",
                "ec2:DescribeRouteTables",
                "ec2:DescribeVpcs"
            ],
            "Resource": "*"
        },
        {
            "Sid": "CloudWatchLogsCreateOperations",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:TagResource"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:*",
            "Condition": {
                "StringEquals": {
                    "aws:RequestTag/AmazonECSManaged": "true"
                }
            }
        },
        {
            "Sid": "CloudWatchLogsReadOperations",
            "Effect": "Allow",
            "Action": [
                "logs:DescribeLogGroups"
            ],
            "Resource": "*"
        }
    ]
}

なぜAccessDeniedでループするのか

注目すべきはCertificateOperationsステートメントのConditionです。acm:DeleteCertificate自体は許可されていますが、aws:ResourceTag/AmazonECSManagedtrueであるリソースに限る、というタグ条件(StringEquals)が付いています。

        {
            "Sid": "CertificateOperations",
            "Effect": "Allow",
            "Action": [
                "acm:RequestCertificate",
                "acm:AddTagsToCertificate",
                "acm:DeleteCertificate",
                "acm:DescribeCertificate"
            ],
            "Resource": [
                "arn:aws:acm:*:*:certificate/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/AmazonECSManaged": "true"
                }
            }
        }

調べたところ削除対象の証明書はすでに削除済みで、ACM上に存在していませんでした。
リソースが存在しないとそのタグ(aws:ResourceTag/AmazonECSManaged)を評価できないため、StringEquals条件が成立せず、Allowが適用されずにAccessDeniedとなります。

そのため今回はIAMの権限評価段階でAccessDeniedになってしまうため、ECSはACMが削除が完了したと判定できず、同じ削除処理をリトライし続けていた、というわけです。

408万回実行されたDeleteCertificate

どのくらいの頻度で叩かれていたのかを数えてみると、5秒間隔で再実行されており、1分あたり12回(60秒÷5秒)のペースでした。

そしてECS Express Modeについては以下のブログで検証していたので、おそらくその頃からずっと発生し続けていたと思われます。

https://dev.classmethod.jp/articles/ecs-express-mode-terraform/

2025年12月20日から本事象を解消した日(2026年8月13日)までは236日。単純計算すると、次のようになります。

236日 × 24時間 × 60分 × 12回/分 = 4,078,080回

およそ408万回ものDeleteCertificateを無駄に叩かせていた計算になります。
ECS氏、ACM氏、ごめんね…。

そもそもなぜこの状態に陥ったのか(推測)

本事象の原因はおそらくリソースの削除順序にあったと考えています。

ECS Express Modeでは、ACM証明書やロードバランサー、セキュリティグループといった周辺リソースはECSがマネージドに作成・管理します。
これらにはAmazonECSManaged=trueのタグが付き、先ほどのポリシーのタグ条件と対になって、ECS自身がライフサイクルを面倒見てくれます。

そのため被疑として考えられるのは検証後の片付けのときに、ECS Express本体の削除よりも先に、ECSが作成したリソース(ACM証明書)を手動で削除してしまったのではないか、というパターンです。
(あくまで憶測ですが…)

解消方法について

権限を与える

原因が分かったので、該当の証明書に対してacm:DeleteCertificateを許可する権限を明示的に付与します。
今回は景気良く「AWSCertificateManagerFullAccess」ポリシーを使ってみます。

スクリーンショット 2026-08-13 12.01.39

権限を付与したうえで、再度CloudTrailのイベントを確認します。

スクリーンショット 2026-08-13 12.02.57

すると、これまでAccessDeniedだったerrorCodeResourceNotFoundExceptionに変わっていました。
証明書がすでに存在しないことをECSが認識できるようになり、削除処理を「完了」として扱えるようになったわけですね。

イベントレコード
{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:ECSGateway",
        "arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/ecs-express-infrastructure-role/ECSGateway",
        "accountId": "XXXXXXXXXXXX",
        "accessKeyId": "ASIAXXXXXXXXXXXXXXXX",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::XXXXXXXXXXXX:role/ecs-express-infrastructure-role",
                "accountId": "XXXXXXXXXXXX",
                "userName": "ecs-express-infrastructure-role"
            },
            "attributes": {
                "creationDate": "2026-08-13T03:01:53Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "ecs.amazonaws.com"
    },
    "eventTime": "2026-08-13T03:01:53Z",
    "eventSource": "acm.amazonaws.com",
    "eventName": "DeleteCertificate",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "ecs.amazonaws.com",
    "userAgent": "ecs.amazonaws.com",
    "errorCode": "ResourceNotFoundException",
    "errorMessage": "Could not find certificate arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.",
    "requestParameters": {
        "certificateArn": "arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    },
    "responseElements": null,
    "requestID": "518d1a27-a7b5-40ea-94bb-f3bde432c1ea",
    "eventID": "e76729c7-cd69-4193-8d16-5b37543435f3",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "XXXXXXXXXXXX",
    "eventCategory": "Management"
}

証明書の後始末が済むと、残っていた周辺リソースのクリーンアップも進みます。
続いてはセキュリティグループの削除です。

スクリーンショット 2026-08-13 12.02.30

DeleteSecurityGroupClient.InvalidGroup.NotFound(対象はすでに存在しない)を返しており、こちらも問題なく完了しています。

イベントレコード
{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:ECSGateway",
        "arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/ecs-express-infrastructure-role/ECSGateway",
        "accountId": "XXXXXXXXXXXX",
        "accessKeyId": "ASIAXXXXXXXXXXXXXXXX",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::XXXXXXXXXXXX:role/ecs-express-infrastructure-role",
                "accountId": "XXXXXXXXXXXX",
                "userName": "ecs-express-infrastructure-role"
            },
            "attributes": {
                "creationDate": "2026-08-13T03:01:53Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "ecs.amazonaws.com"
    },
    "eventTime": "2026-08-13T03:01:55Z",
    "eventSource": "ec2.amazonaws.com",
    "eventName": "DeleteSecurityGroup",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "ecs.amazonaws.com",
    "userAgent": "ecs.amazonaws.com",
    "errorCode": "Client.InvalidGroup.NotFound",
    "errorMessage": "The security group 'sg-XXXXXXXXXXXXXXXXX' does not exist",
    "requestParameters": {
        "groupId": "sg-XXXXXXXXXXXXXXXXX"
    },
    "responseElements": null,
    "requestID": "af2dc28e-c6a3-4f2c-bf5a-c6832d8bcaf5",
    "eventID": "7ea172fa-1d6e-4374-a6a3-87fcca663c7d",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "XXXXXXXXXXXX",
    "eventCategory": "Management"
}

こうして一連のクリーンアップが完了したことで、DeleteCertificateのリトライループは停止しました…。

さいごに

以上、CloudTrailに延々と記録され続けるDeleteCertificateイベントの原因を追いかけました。

今回はたまたまECSでしたが、AWSのマネージドな機能は裏側で自動的にリソースを作ったり消したりしてくれる分、同じような空回りが他のサービスで起きても不思議ではありません。

今後は検証用アカウントでもたまにはCloudTrailを眺めて、妙なイベント履歴がないか確認します。(自戒)

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事