マルチリージョン並列デプロイで発生するサービスリンクロール競合問題とその対策

マルチリージョン並列デプロイで発生するサービスリンクロール競合問題とその対策

2025.12.30

こんにちは。サービス開発室の武田です。

AWSのリージョナルサービスをマルチリージョンに展開する際、処理時間短縮のために並列実行を採用することがあります。しかし、この並列実行が思わぬ問題を引き起こすケースがあります。

今回は、GuardDuty Malware Protectionをマルチリージョンで並列有効化した際に遭遇したサービスリンクロール(SLR)の競合問題と、その対策についてまとめます。

発生した事象

複数のAWSリージョンで同時にGuardDuty Malware Protectionを有効化しようとしたところ、一部のリージョンで有効化に失敗するという問題が発生しました。

APIは例外を発生させず 一見問題なく完了しているのですが、一部リージョンでEBS_MALWARE_PROTECTIONが有効化されていません。

CloudTrailを確認すると、当該リージョンで次のようなレスポンスが記録されていました。

{
    "responseElements": {
        "detectorId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "unprocessedDataSources": {
            "malwareProtection": {
                "scanEc2InstanceWithFindings": {
                    "ebsVolumes": {
                        "status": "DISABLED",
                        "reason": "Internal server error."
                    }
                }
            }
        }
    }
}

unprocessedDataSourcesInternal server error.が記録されています。

原因

GuardDuty Malware Protectionを有効化すると、AWSは自動的にサービスリンクロールを作成します。具体的にはAWSServiceRoleForAmazonGuardDutyMalwareProtectionというロールです。

ここでポイントになるのは、サービスリンクロールはIAMリソースであり、グローバルリソースであるという点です。

複数リージョンで並列にMalware Protectionを有効化すると、各リージョンの処理が同時にこのグローバルなSLRを作成しようとします。最初のリクエストがSLRの作成を開始すると、作成完了までの間に来た他のリクエストは競合により、サイレントに失敗すると考えられます。

問題が発生するコード例

次のように複数リージョンで並列にGuardDuty Detectorを作成すると、競合する可能性があります。

import asyncio
import boto3

async def enable_guardduty(region: str):
    """各リージョンでGuardDutyを有効化"""
    session = boto3.Session(region_name=region)
    guardduty = session.client("guardduty")

    # この呼び出しがSLRを自動作成しようとする
    guardduty.create_detector(
        Enable=True,
        Features=[
            {
                "Name": "EBS_MALWARE_PROTECTION",
                "Status": "ENABLED"
            }
        ]
    )

async def main():
    regions = ["ap-northeast-1", "us-east-1", "eu-west-1", "ap-southeast-1"]

    # 並列実行すると競合が発生する可能性がある
    await asyncio.gather(*[
        enable_guardduty(region) for region in regions
    ])

対策:サービスリンクロールの事前作成

サービスを有効化する前に、必要なサービスリンクロールを明示的に作成しておくことで、競合を回避できます。

import asyncio
import boto3
from botocore.exceptions import ClientError

def create_service_linked_role_if_not_exists(
    service_name: str,
    role_name: str
) -> None:
    """サービスリンクロールを事前作成する

    Args:
        service_name: AWSサービス名(例: malware-protection.guardduty.amazonaws.com)
        role_name: 作成されるロール名(例: AWSServiceRoleForAmazonGuardDutyMalwareProtection)
    """
    iam = boto3.client("iam")

    # 既に存在するか確認
    try:
        iam.get_role(RoleName=role_name)
        print(f"SLR {role_name} は既に存在します")
        return
    except ClientError as e:
        if e.response["Error"]["Code"] != "NoSuchEntity":
            raise

    iam.create_service_linked_role(AWSServiceName=service_name)
    print(f"SLR {role_name} を作成しました")


async def enable_guardduty(region: str):
    """各リージョンでGuardDutyを有効化"""
    session = boto3.Session(region_name=region)
    guardduty = session.client("guardduty")

    guardduty.create_detector(
        Enable=True,
        Features=[
            {
                "Name": "EBS_MALWARE_PROTECTION",
                "Status": "ENABLED"
            }
        ]
    )
    print(f"{region}: GuardDuty を有効化しました")


async def main():
    regions = ["ap-northeast-1", "us-east-1", "eu-west-1", "ap-southeast-1"]

    # 1. まずSLRを事前作成(グローバルなので1回でOK)
    create_service_linked_role_if_not_exists(
        service_name="malware-protection.guardduty.amazonaws.com",
        role_name="AWSServiceRoleForAmazonGuardDutyMalwareProtection"
    )

    # 2. SLR作成完了後に並列でGuardDutyを有効化
    await asyncio.gather(*[
        enable_guardduty(region) for region in regions
    ])


if __name__ == "__main__":
    asyncio.run(main())

この対策でポイントとなるのは次の3点です。

SLR作成は1回だけ実行します。IAMはグローバルサービスですので、どのリージョンから作成しても同じです。またすでに存在する場合はスキップし、冪等性を確保しています。そしてSLR作成完了後にサービス有効化を並列実行することで、順序を保証しています。

同様の問題が発生する条件

今回はGuardDuty Malware Protectionで問題が発生しました。次の3つの条件をすべて満たす場合、同様の問題を引き起こす可能性があります。

  1. リージョナルリソースであること(各リージョンで個別に有効化が必要なサービス)
  2. 有効化時に暗黙的にSLRを作成すること(サービス有効化APIが内部でSLRを自動作成する)
  3. 並列実行すること(複数リージョンで同時に有効化処理を実行)

SLRを使用するサービスであっても、グローバルサービスであったり、SLRが事前に作成されている場合は問題にならないでしょう。

まとめ

サービスリンクロールはグローバルリソースであり、複数リージョンから同時に作成しようとすると競合する可能性があります。対策として、サービス有効化前にSLRを明示的に事前作成しておくとよいでしょう。GuardDuty以外にも、SLRを使用するサービスのマルチリージョン並列デプロイでは同様の問題に注意が必要です。

マルチリージョン展開の自動化を行う際は、グローバルリソースの存在を意識した設計を心がけてください。

この記事をシェアする

FacebookHatena blogX

関連記事