【小ネタ】ECS Express Modeのリソース削除の順番を間違えてDeleteCertificateが無限ループしていた話
はじめに
皆様こんにちは、あかいけです。
先日、検証用アカウントのCloudTrailを眺めていたところ、DeleteCertificateというイベントが数秒おきに延々と記録され続けているのに気づきました。
自分で証明書を削除しようとした覚えはなかったため、原因を調査することにしました。
本記事ではその調査の流れと原因、そして対処方法をまとめます。
先にまとめ
先に結論をまとめます。
- 事象
- CloudTrailに
acm:DeleteCertificateがAccessDeniedのまま数秒おきに記録され続けていた - 実行元はAmazon ECS(Express Gateway Services)のマネージドロール
ecs-express-infrastructure-roleだった
- CloudTrailに
- 原因
- 削除対象のACM証明書はすでに存在しておらず、マネージドポリシーのタグ条件を満たせないため
AccessDeniedになっていた - ECS側は「削除できたか」を判定できず、同じ削除処理をリトライし続けていた
- 削除対象のACM証明書はすでに存在しておらず、マネージドポリシーのタグ条件を満たせないため
- 解消方法
- 該当の証明書に対する削除権限を明示的に付与したところ、
ResourceNotFoundExceptionを返すようになり、リトライのループが解消した
- 該当の証明書に対する削除権限を明示的に付与したところ、
処理の流れを図にすると以下のとおりです。
何が起きたか
CloudTrailにDeleteCertificateが並び続ける
まずはCloudTrailのイベント履歴を確認します。次のように、DeleteCertificateが短い間隔で大量に記録され続けていました。


イベントレコードの中身は以下の通りでした。
{
"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.arnがecs-express-infrastructure-role/ECSGatewayであり、実行元はユーザーではなくECSのマネージドなロールであるinvokedByがecs.amazonaws.comとなっており、ECSサービスが自動的に呼び出しているerrorCodeがAccessDeniedで、acm:DeleteCertificateが許可されていないために失敗している
ecs-express-infrastructure-roleは、ECSのExpress Gateway Services(ロードバランサーやACM証明書といった周辺リソースを、ECSがマネージドに構築・管理してくれる仕組み)が利用するロールです。このロールにアタッチされているマネージドポリシーAmazonECSInfrastructureRoleforExpressGatewayServicesの内容を確認します。

証明書に関する権限は、次のポリシーのCertificateOperationsステートメントで定義されています。
ポリシー
{
"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/AmazonECSManagedがtrueであるリソースに限る、というタグ条件(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については以下のブログで検証していたので、おそらくその頃からずっと発生し続けていたと思われます。
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」ポリシーを使ってみます。

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

すると、これまでAccessDeniedだったerrorCodeがResourceNotFoundExceptionに変わっていました。
証明書がすでに存在しないことを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"
}
証明書の後始末が済むと、残っていた周辺リソースのクリーンアップも進みます。
続いてはセキュリティグループの削除です。

DeleteSecurityGroupもClient.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を眺めて、妙なイベント履歴がないか確認します。(自戒)







