NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    カスタムリソースでAWS IAM Access Analyzerのアーカイブルールを自動で適用してみる

    本記事は  AWSアワード受賞者祭り  17日目の記事です。
    ✨🏆  16日目  ▶▶ 本記事 ▶▶  18日目  🏆✨

    はじめに

    こんにちは、藤本です。
    この度、「2025 Japan AWS Jr. Champions」、「2025 Japan All AWS Certifications Engineers」に選出いただきました。 「2025 Japan AWS Jr. Champions」の選出の経緯は、ブログでまとめているので、よかったら見てください!

    tech.nri-net.com

    今回は、以前執筆したブログ「マルチアカウント環境における AWS IAM Access Analyzer を用いた権限管理」をベースに、 カスタムリソースによるIAM Access Analyzerのアーカイブルールの自動適用についてまとめてみました。 IAM Access Analyzerとは、IAMロールやリソースに対するアクセス許可を分析するサービスです。 詳しい概要については、こちらのブログでまとめているので、ご参照ください。


    アーカイブルールとは

    アーカイブルール概要

    アーカイブルール (Archive rules) とは、IAM Access Analyzerが検出した外部からのアクセスが可能であるリソースに対して、特定の条件に合致するアクセスを自動的にアーカイブするための設定です。検出結果として確認不要なアクセスをアーカイブルールによって、フィルタリングすることで検出結果のノイズを減らします。例えば、一時的または検証目的で作成されるようなテスト環境用のリソースや監視目的のSaaSで許可されているリソース等がアーカイブ対象となります。
    アーカイブルールを多用しすぎると、意図しないアクセスを見逃す可能性があるため、アーカイブルールの取り扱いには注意が必要です。そのため定期的に、アーカイブルールを見直すような運用が必要となります。

    アーカイブルールを作成しても既存の結果には適用されない

    アーカイブルールの注意点として、アーカイブルールの作成時に既存の検出結果には適用されない点があります。 新規に検出されたアクセスに対しては自動でアーカイブされますが、作成前に検知済みであるアクセスに関してはアーカイブされません。 また、アーカイブルールを変更した場合でも、既存の結果には再適用が行われません。 そのため、アーカイブルール作成・更新後には、明示的に適用を行う必要があります。 アーカイブルールの適用を実行するにはマネジメントコンソール、CLI、カスタムリソースによる適用のいずれかの対応が必要になります。

    マネジメントコンソール上からアーカイブルールを適用する場合は、アーカイブルール作成時に「アクティブな検出結果を作成およびアーカイブ」を選択することで、既存の分析結果にも適用されます。 「ルールを作成」を選択すると、既存の結果には適用されず、アーカイブルールの作成のみが行われます。

    マネジメントコンソールのアーカイブルール作成画面

    CLIによるアーカイブルール適用の場合は、apply-archive-ruleコマンドを実行します。 作成されたアーカイブルールの一覧を確認したい場合は、list-archive-rulesコマンドを実行します。

    aws accessanalyzer apply-archive-rule \
      --analyzer-arn <アナライザーARN> \
      --rule-name <アーカイブルール名>
    

    apply-archive-rule — AWS CLI 2.27.61 Command Reference

    aws accessanalyzer list-archive-rules \
        --analyzer-name <アナライザー名>
    

    list-archive-rules — AWS CLI 2.27.61 Command Reference

    カスタムリソースによる適用に関しては、次の章で解説します。


    カスタムリソースによるアーカイブルールの適用

    カスタムリソースとは

    カスタムリソースとは、AWS CloudFormationで定義されていないリソースや複雑な処理を、AWS LambdaやAmazon SNSなどを使って拡張的に実行する仕組みのことです。 CloudFormationが対応していない特定のリソースのAPI呼び出しや外部のサービスとの連携を行う際などに利用されます。 CloudFormationでIAM AccessAnalyzerを展開する場合、IAM Access Analyzerのアーカイブルール適用は、 CloudFormationでは対応していない操作になるため、アーカイブルールの作成と適用を同時に行いたい場合は、Lambdaによるカスタムリソースの実装が必要になります。

    カスタムリソースの実行タイミング

    カスタムリソースは、CloudFormationスタックの作成・更新・削除のタイミングで実行されます。 これらの操作に応じて、CloudFormationはカスタムリソースに対してリクエスト(イベントタイプ)を送信します。

    イベントタイプ 発生のタイミング
    Create CloudFormationスタックの新規作成時
    Update CloudFormationスタックの更新時
    Delete CloudFormationスタックの削除時

    Lambda関数では、イベントタイプごとに処理を分けて実装することが推奨されており、分岐を行わないと意図しない動作やスタックエラーの原因となる可能性があります。 例えば、 IAM Access Analyzerの場合、CloudFormationの作成・更新時にのみアーカイブルールを適用し、削除時にはカスタムリソースのLambda関数による処理を行わないように制御します。 イベントタイプは、Lambda関数のeventのRequestTypeに含まれています。

    def lambda_handler(event, context):
        request_type = event["RequestType"]
        
        if request_type == "Create":
            # スタックの新規作成時の処理
        elif request_type == "Update":
            # スタック更新時の処理
        elif request_type == "Delete":
            # スタック削除時の処理
    

    CloudFormationに対してレスポンスが必要

    Lambda関数の処理が完了したら、CloudFormationに対して、Lambda関数の処理結果(SUCCESSまたはFAILED)を通知する必要があります。 この通知は、CloudFormationから渡されるリクエスト内に含まれている ResponseURL(署名付きS3のURL) に対して、HTTP PUTリクエストでレスポンスを送信することで行います。 レスポンスが行われない場合、CloudFormationはLambda関数からの応答をデフォルトのタイムアウト時間である1時間(3600秒)待機し、それでも応答がない場合はスタック操作を失敗(FAILED)として終了します。 タイムアウト時間が経過するまでは、スタックへの操作が実行できません。

    レスポンスの処理を簡単に行う方法として、cfnresponse-sendモジュールがあります。 これは、Lambda関数を呼び出したカスタムリソースへの応答の送信を簡素化するAWS公式のライブラリのことです。

    docs.aws.amazon.com

    cfnresponse-sendモジュールは、CloudFormationテンプレート内のZipFileプロパティを使用して、Pythonコードをインラインで記述する場合にのみ利用可能です。 つまり、テンプレート内にPythonコードを直接埋め込む形式で利用することが前提となります。
    ただし、インライン記述にはいくつかのデメリットがあります。

    • 保守性が低い
      コードの可読性や再利用性が下がり、バージョン管理もしにくくなります。

    • デバッグが困難
      問題がCloudFormation側にあるのか、Pythonコード側にあるのか切り分けが難しくなります。 そのため、CloudFormation テンプレートと Lambdaコードは分離した方が望ましいです。

    • コードサイズ制限
      ZipFileプロパティには、4096Byte(4KB)までというサイズ制限があります。

    これらの理由から、一般的には Lambda関数のコードをS3バケットに格納し、そこから参照する方法が採用されています。 インライン記述を採用する場合は、ごく単純な処理に限定して使用します。

    S3バケットに格納されたコードでは、cfnresponse-sendモジュールは使用できません。 これは、cfnresponse-sendモジュールがLambdaランタイムに標準で含まれておらず、ModuleNotFoundErrorが発生するためです。 そのため、S3上のコードでCloudFormationに応答を返す必要がある場合は、レスポンス処理を独自に関数として実装する必要があります。
    今回は、小西さんが以前執筆されたブログ「AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の内容とAWSのドキュメントを参考にレスポンス処理を独自の関数で作成しました。

    テンプレート

    CloudFormationテンプレート

    CloudFormationテンプレートでは、主にIAM Access Analyzerのアナライザーとアーカイブルール適用のためのLambdaを作成しています。

    CloudFormationテンプレート

    AWSTemplateFormatVersion: '2010-09-09'
    Description: Create IAM Access Analyzer and apply archive rule automatically via Lambda (Custom Resource)
    Parameters:
      AnalyzerName:
        Description: The name of the Analyzer.
        Type: String
        Default: IAMAccessAnalyzer
      CustomResourceUpdateToken:
        Description: Execute a single CustomResource.
        Type: String
        Default: 0
      LambdaCodeBucket:
        Description: S3 bucket containing Lambda source code (zip).
        Type: String
      LambdaCodeKey:
        Description: S3 key (path) to Lambda deployment package.
        Type: String
    
    Resources:
      Analyzer:
        Type: AWS::AccessAnalyzer::Analyzer
        Properties:
          AnalyzerName: !Sub "${AnalyzerName}-${AWS::AccountId}-${AWS::Region}"
          Type: ACCOUNT
          ArchiveRules:
            - Filter:
              - Property: resourceType
                Eq:
                - AWS::IAM::Role
              - Property: resource
                Contains:
                  - test
              RuleName: PrefixIAMRoleRule
    
      RunCustomResourceApplyArchiveRule:
        Type: 'Custom::ApplyArchiveRule'
        DependsOn: ApplyArchiveRuleLambda
        Properties:
          ServiceToken: !GetAtt ApplyArchiveRuleLambda.Arn
          ServiceTimeout: 60
          UpdateDummyInput: {Ref: CustomResourceUpdateToken}
    
      ApplyArchiveRuleLambda:
        Type: AWS::Lambda::Function
        DependsOn:
          - ApplyArchiveRuleRole
          - Analyzer
        Properties:
          FunctionName: ApplyArchiveRule
          Description : apply_archive_rule
          Runtime: python3.12
          MemorySize: 1024
          Timeout: 900
          Role: !GetAtt ApplyArchiveRuleRole.Arn
          Handler: index.lambda_handler
          Code:
            S3Bucket: !Ref LambdaCodeBucket
            S3Key: !Ref LambdaCodeKey
          Environment:
            Variables:
              ANALYZER_ARN: !GetAtt Analyzer.Arn
              ANALYZER_NAME: !Sub "${AnalyzerName}-${AWS::AccountId}-${AWS::Region}"
    
      ApplyArchiveRuleRole: 
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Sub 'ApplyArchiveRule-${AWS::Region}'
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Principal:
                  Service:
                    - lambda.amazonaws.com
                Action:
                  - sts:AssumeRole
          Policies:
            - PolicyName: !Sub 'IAMPolicy-ApplyArchiveRule-${AWS::Region}'
              PolicyDocument:
                Version: '2012-10-17'
                Statement:
                  - Sid: ListRules
                    Effect: Allow
                    Action:
                      - access-analyzer:ListArchiveRules
                    Resource: "*"
                  - Sid: ApplyRule
                    Effect: Allow
                    Action:
                      - access-analyzer:ApplyArchiveRule
                    Resource: !GetAtt Analyzer.Arn
          ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    

    CloudFormationによるIAM Access Analyzer作成の詳細は、AWS CloudFormationユーザーガイド AccessAnalyzerをご確認ください。

    CloudFormationテンプレート作成において、Tipsを紹介します。

    ダミーパラメータを設定する

    CloudFormationは、カスタムリソースのPropertiesに変更がない限り、スタック更新時にLambda関数を再実行しません。 カスタムリソースの処理を再実行したい場合、見かけ上の変更を加える必要があります。 そのために、ダミーパラメータ(今回の場合は、CustomResourceUpdateTokenと定義)を使用します。 このダミーパラメータの値を変更し、CloudFormationを更新させることで、カスタムリソースを強制的に再実行します。 今回のケースだと、マネジメントコンソールなどでアーカイブルール作成のみが行われた後、ダミーパラメータ(CustomResourceUpdateToken)の値を変更することで、アーカイブルールの適用を別途実行できます。

    ServiceTimeoutを利用する

    「CloudFormationに対してレスポンスが必要」の章で説明した通り、CloudFormationのカスタムリソースでは、Lambda関数からのレスポンスがない場合、デフォルトで最大1時間(3600秒)スタック操作が待機状態になります。 タイムアウト時間が経過するまでは、他の操作も実行できないため、開発・検証時には大きなボトルネックになります。 この問題を回避するために、カスタムリソースにServiceTimeoutを設定します。

    docs.aws.amazon.com

    ServiceTimeoutは、カスタムリソースのタイムアウト時間を秒単位で明示的に指定できるプロパティです。 ServiceTimeout: 300と設定すれば、Lambda関数がエラーになった場合でもCloudFormationは5分後に処理を失敗(FAILED)として終了し、不要な待機時間を防ぐことができます。

      RunCustomResourceApplyArchiveRule:
        Type: 'Custom::ApplyArchiveRule'
        DependsOn: ApplyArchiveRuleLambda
        Properties:
          ServiceToken: !GetAtt ApplyArchiveRuleLambda.Arn
          ServiceTimeout: 60 # タイムアウト時間設定
          UpdateDummyInput: {Ref: CustomResourceUpdateToken} # ダミーパラメータ
    

    カスタムリソースのLambda関数のコード

    今回作成したカスタムリソースのLambda関数では、IAM Access Analyzerの名前やARNをPythonコード内でハードコーディングせず、CloudFormationの環境変数(Environment:)から取得するようにしています。 これにより、可読性や保守性が向上し、CloudFormation側の値を変更するだけで、柔軟に対応できるようになります。

    カスタムリソースのLambda関数のコード

    import os
    import json
    import boto3
    import botocore
    import logging
    import urllib.request
    
    # ロガー設定
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    # 環境情報
    region = os.environ.get('AWS_REGION')
    sts_client = boto3.client("sts", region_name=region)
    accountId = sts_client.get_caller_identity()["Account"]
    
    # CloudFormationの環境変数を受け取る
    ANALYZER_ARN = os.environ["ANALYZER_ARN"]
    ANALYZER_NAME = os.environ.get("ANALYZER_NAME") or ANALYZER_ARN.split("/")[-1]
    
    def lambda_handler(event, context):
        logger.info(f"Received event: {json.dumps(event)}")
    
        try:
            request_type = event['RequestType']
    
            if request_type == 'Delete': # スタック削除時
                logger.info("Delete event received. No action needed.")
                cfn_response_send(event, context, 'SUCCESS', {}, reason='Delete request - no action')
                return
    
            elif request_type in ['Create', 'Update']: # スタック作成・更新時
                archive_rule_names = get_list_archiverules()
                if archive_rule_names:
                    apply_archiverules(archive_rule_names)
                    message = f"Applied archive rules: {archive_rule_names}"
                else:
                    message = "No archive rules found"
                cfn_response_send(event, context, 'SUCCESS', {'Message': message})
                return
    
            cfn_response_send(event, context, 'FAILED', {}, reason=f"Unexpected RequestType: {request_type}")
    
        except Exception as e:
            logger.error(f"Unhandled exception: {e}")
            cfn_response_send(event, context, 'FAILED', {}, reason=str(e))
    
    # アーカイブルールの一覧を取得
    def get_list_archiverules():
        accessanalyzer_client = boto3.client('accessanalyzer')
        archive_rule_names = []
    
        try:
            paginator = accessanalyzer_client.get_paginator('list_archive_rules')
            for page in paginator.paginate(analyzerName=ANALYZER_NAME):
                archive_rule_names.extend(rule['ruleName'] for rule in page.get('archiveRules', []))
        except botocore.exceptions.ClientError as e:
            logger.error(f"Failed to list archive rules: {e}")
    
        return archive_rule_names
    
    # アーカイブルールを適用
    def apply_archiverules(archive_rule_names):
        accessanalyzer_client = boto3.client('accessanalyzer')
        for rule_name in archive_rule_names:
            try:
                accessanalyzer_client.apply_archive_rule(
                    analyzerArn=ANALYZER_ARN,
                    ruleName=rule_name
                )
                logger.info(f"Applied archive rule: {rule_name}")
            except botocore.exceptions.ClientError as e:
                logger.error(f"Error applying archive rule {rule_name}: {e}")
                continue
    
    # 独自のレスポンス処理
    def cfn_response_send(event, context, response_status, response_data,
                          physical_resource_id=None, no_echo=False, reason=None):
        response_url = event['ResponseURL']
        logger.info(f"Response URL: {response_url}")
    
        response_body = {
            'Status': response_status,
            'Reason': reason or f"See the details in CloudWatch Log Stream: {context.log_stream_name}",
            'PhysicalResourceId': physical_resource_id or context.log_stream_name,
            'StackId': event['StackId'],
            'RequestId': event['RequestId'],
            'LogicalResourceId': event['LogicalResourceId'],
            'NoEcho': no_echo,
            'Data': response_data
        }
    
        json_response_body = json.dumps(response_body)
        logger.info(f"Response body: {json_response_body}")
    
        headers = {
            'Content-Type': '',
            'Content-Length': str(len(json_response_body))
        }
    
        try:
            request = urllib.request.Request(
                response_url,
                data=json_response_body.encode('utf-8'),
                headers=headers,
                method='PUT'
            )
            with urllib.request.urlopen(request) as response:
                logger.info(f"CloudFormation response sent. Status code: {response.status}")
        except Exception as e:
            logger.error(f"Failed to send CloudFormation response: {e}")
    
    

    デプロイ手順

    共有したカスタムリソースのLambda関数のコードとCloudFormationテンプレートを用いて、デプロイを行います。

    1.Lambda関数のコードをS3にアップロード
    Lambda関数のコード(index.py)をzip化した上で、S3バケットにアップロードします。 S3バケットは別途作成します。

    2.Cloud Shellにて、CloudFormationコマンド実行
    Cloud Shell上にCloudFormationテンプレート(YAMLファイル)をアップロードした上で、以下のコマンドを実行します。

    aws cloudformation deploy \
      --template-file <CloudFormationテンプレート名> \
      --stack-name <スタック名> \
      --capabilities CAPABILITY_NAMED_IAM \
      --parameter-overrides \
          LambdaCodeBucket=<アップロード先のS3バケット名>   \
          LambdaCodeKey=<S3バケットにアップロードしたzipファイル名>
    

    終わりに

    アーカイブルールの自動適用というニッチな内容でしたが、いかがでしたでしょうか。 本ブログで、CloudFormationにおけるカスタムリソースの使い方・アーカイブルール自動適用の参考にしていただければと思います。 また、前のブログでも触れましたが、IAM Access Analyzerではリソースの意図しない共有を検出することはできますが、アナライザー自体が自動的に修正を行うことはありません。 そのため、検出された内容に対する対応方針や運用プロセスを事前に検討しておくことが重要です。

    執筆者:藤本 匠海

    インフラエンジニア

    2025 Japan AWS Jr. Champions, 2025 Japan All AWS Certifications Engineers


    執筆記事一覧:https://tech.nri-net.com/archive/author/nnc-t-fujimoto