Spamhaus に登録されてしまったら通知する仕組みをAWS サーバレス構成で作ってみよう(後編)

こんにちは、SCSKでテクニカルエスコートサービスを担当してる兒玉です。

前回途中だった Spamhaus に登録されてしまったら通知する仕組みの動作確認と修正を行います。

動作確認

通常は Amazon EventBridge から n 時間(設定値)間隔で Lambda 関数を実行するのですが、今回はテストなので手っ取り早く Lambda関数のテストから実行します。
はい、失敗しました。

Lambda_テスト失敗

ログを確認すると、lambda_function というモジュールが無い、というエラーですね。うまくLambda関数がデプロイ出来ていないように思えます。
Lambda関数を確認すると、index.py しかありません。

Lambda関数に index.py しか無い

CloudFormation テンプレートに戻って確認すると、インラインで Pythonのソースコードのテキストを zip ファイル化してアップロードする記述になっています。これでは動きません。
Lambda関数に必要なモジュールとデプロイされたもの

方針転換

なぜこの様な失敗になったのかというと、CloudFormationの ZipFile プロパティは、外部ライブラリを含めることができず、コードサイズにも制限があります。

AWS::Lambda::Function Code - AWS CloudFormation
Changes to a deployment package in Amazon S3 or a container image in ECR are not detected automatically during stack upd...

また、CloudFormation 上では Lambda関数のハンドラの含まれるモジュール名を lambda_function と定義しているのですが、 index という名前になるよ、という注意書きもあります。 (出来上がった Lambda関数で index.py となっているのはこの為です)

今回は1ファイルに収まる対象のデプロイではなく、複数のモジュールやライブラリが必要だったため、AWS SAM(Serverless Application Model、サーバーレスアプリケーションを作成する際に最小限のコードで AWS Lambda 関数、Lambda の耐久性のある関数、Amazon API Gateway APIs、 Amazon DynamoDB テーブル、およびその他のサーバーレスリソースをすばやく定義できる仕組み)によるパッケージングが正解でした。

その他の案も含め、以下のような候補を考え、比較検討しました。

案1: S3バケットを別途用意して、deploy.sh 内でそこに zip化したソースコード群とライブラリを配置し、そこからデプロイするように CloudFormationテンプレートを修正する。

案2: AWS CDK へ移行する。

案3: AWS SAM を使って、CloudFormationテンプレートを修正する。SAMの機能を使ってソースコード群とライブラリをアップする。

案1は、S3バケットをこのために用意しなければならないので、今回はパス。案2のCDKはせっかく作った CloudFormationテンプレートを全部作り直しになって確認が大変そうなのでパス。案3の AWS SAM は S3 バケットは共通のものを用意してくれるし、zipファイル化もSAM側でやってくれるので余計な処理が要らなくなるし、テンプレートもスッキリする。他のリソースの場所はCloudFormationテンプレートのままでいけると思われるので、修正でいけそう、という考えで案3のAWS SAM にすることにしました。
AWS SAMについてはここでは詳細には紹介しませんが、詳しく知りたい方は以下をご参照ください。

 

AWS Serverless Application Model (AWS SAM) とは - AWS Serverless Application Model
AWS Serverless Application Model (AWS SAM) とは何か、またデベロッパーがサーバーレスアプリケーションを構築するのに役立つ方法について説明します。

 

Kiro再び

Kiroに、要件変更の理由と、CloudFormationからSAMに変更する旨を伝えて、requirements、design、tasksの修正を依頼します。

全部が書き直されるのではなく、きちんと差分を理解していて、トレーサビリティマトリクスを元に再実行必要なタスクが洗いだされました。

Kiroへ要件変更を要求

またしてもタスク実行地獄です。
Task completeになったタスクを未実行に戻す方法がわからなかったので Kiro に聞いて戻してもらいました。
修正した箇所を見ると、単純にチェックボックス が [x] となっているのを [ ] にすれば良いようです。

Task 1を修正後 AWS SAM cli をインストールして、 sam validate-templatesam build が通ったので、Task 11、12、13 と再度実行します。
実行後、出来上がった SAMテンプレート を使用してデプロイすると、今度はlambda_function.py も ライブラリ群も含まれています。

 sbl-monitor-sam

1時間に1回の実行の設定でデプロイしたので、暫く経つと Lambda 関数にログが出力されているはずです。Lambda関数の モニタリングから CloudWatch Logs を確認してみると…

Lambda関数実行ログ
おお、キチンとログが出力されていました。Kiro スペックモードでは、ログに関してもかなりバッチリ作ってくれたので、見やすいログが出力されています。
ちょっと興味があったので、Kiro に StructuredLogger の構成を Mermaid 図に起こしてもらったらこのようになっていました。
きちんと、センシティブな情報は SensitiveDataFilter でフィルタしてくれる処理も実装されているようです。

StructuredLogger 構成図

「プロトタイプからプロダクションまで(Agentic AI development from prototype to production)」と、いうキャッチフレーズに恥じない作りですね。

初期設定として 192.168.1.1 (プライベートIPアドレスなので、Spamhaus には登録されない)を監視対象としたので、 “is_listed” : false と、期待通りの実行結果です。
このアドレスを、試しに Spamhaus SBLに登録されてしまった検証に利用できる 127.0.0.2 を設定します。

「127.0.0.2」のテストIPについて
「Spamhausがテスト用に提供している 127.0.0.2 を入力することで、実際にブラックリストに登録された際と同じ挙動を安全にシミュレーションできます。」
 

Lambda関数の環境変数にSBL でNGが返るIPアドレスを設定

Lambda関数の新しいバージョンを発行して、(ここではバージョン2) Lambda関数の live エイリアス値 を 1 -> 2 へ変更します。
Lambda関数の新しいバージョンを発行

Lambda関数の live エイリアスを バージョン 2 に変更

live のリンクからになったところで、テストを実行すれば、Spamhausの SBL に登録されてしまったメールが来るはずです。 
Lambda関数の live エイリアス版からテスト

SNSからメールが来ました! うまく作成できたようです!
SNSから SBL に登録されてしまったメールを受信

 

出来上がりイメージ

出来上がったコードはかなりしっかりと作ってくれたこともあり、すべてを この blog 上で乗せることができないような分量になってしました。
ですが、せっかくですので、アーキテクチャ図と、出来あがった SAM テンプレートを乗せておきます。

アーキテクチャ図は Kiro に書いてもらったものが全部入りで混沌としていたので、適度な範囲で分割しました。Mermaid記法 はテキストベースなので、分割したり修正したりが簡単なのが便利ですね!

SAM の デプロイアーキテクチャ図

SBL monotor SAM-Deployment-Architecture

SBL Monitor のメインアーキテクチャ図

Lambda関数のアーキテクチャ

Lambda 関数アーキテクチャ

SAM テンプレート

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Spamhaus SBL Monitor - Serverless monitoring system for IP addresses on Spamhaus Blocklist using SAM with automatic dependency packaging'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Monitoring Configuration"
        Parameters:
          - IPAddresses
          - MonitoringInterval
      - Label:
          default: "Notification Configuration"
        Parameters:
          - NotificationEmail
      - Label:
          default: "Resource Configuration"
        Parameters:
          - ResourceNamePrefix
          - CostAllocationTag
    ParameterLabels:
      IPAddresses:
        default: "IP Addresses to Monitor"
      MonitoringInterval:
        default: "Monitoring Interval (hours)"
      NotificationEmail:
        default: "Email Address for Notifications"
      ResourceNamePrefix:
        default: "Resource Name Prefix"
      CostAllocationTag:
        default: "Cost Allocation Tag Name"

Parameters:
  IPAddresses:
    Type: String
    Description: 'Comma-separated list of IP addresses to monitor for SBL registration'
    AllowedPattern: '^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(,\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})*$'
    ConstraintDescription: 'Must be valid IPv4 addresses separated by commas (e.g., 192.168.1.1,10.0.0.1)'
    Default: '192.168.1.1'
  MonitoringInterval:
    Type: Number
    Description: 'Interval in hours between SBL checks (1-24 hours)'
    MinValue: 1
    MaxValue: 24
    Default: 1
    ConstraintDescription: 'Must be between 1 and 24 hours'
  NotificationEmail:
    Type: String
    Description: 'Email address to receive SBL detection notifications'
    AllowedPattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    ConstraintDescription: 'Must be a valid email address'
  ResourceNamePrefix:
    Type: String
    Description: 'Prefix for all resource names (maximum 10 characters)'
    MaxLength: 10
    MinLength: 1
    AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-]*$'
    ConstraintDescription: 'Must start with a letter and contain only alphanumeric characters and hyphens'
    Default: 'sbl'
  CostAllocationTag:
    Type: String
    Description: 'Value for the Cost allocation tag applied to all resources'
    MinLength: 1
    MaxLength: 256
    AllowedPattern: '^([\p{L}\p{Z}\p{N}_.:\/=+\-@]*)$'
    ConstraintDescription: 'Must contain only alphanumeric characters, spaces, periods, colons, slashes, equals, plus, hyphens, underscores, and at symbols'
    Default: 'spamhaus-monitor'

Globals:
  Function:
    Runtime: python3.13
    MemorySize: 256
    Timeout: 300
    Environment:
      Variables:
        LOG_LEVEL: INFO
    Tags:
      Cost: !Ref CostAllocationTag
      Application: 'Spamhaus-SBL-Monitor'
    Tracing: Active
    ReservedConcurrentExecutions: 5
    AutoPublishAlias: live
    DeploymentPreference:
      Type: AllAtOnce

# Stack-level tags applied to all resources (moved to Metadata section for SAM compatibility)
Conditions:
  IsSingleHour: !Equals [!Ref MonitoringInterval, 1]

Resources:
  # IAM Role for Lambda Function (Least Privilege)
  SBLMonitorFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ResourceNamePrefix}-lambda-execution-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
      Policies:
        - PolicyName: SBLMonitorPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref DQSSecret
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref SBLNotificationTopic
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${ResourceNamePrefix}-sbl-monitor:*'
      Tags:
        - Key: Cost
          Value: !Ref CostAllocationTag
        - Key: Application
          Value: 'Spamhaus-SBL-Monitor'
        - Key: Component
          Value: 'IAM-Role'

  # Lambda Function for SBL Monitoring
  SBLMonitorFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub '${ResourceNamePrefix}-sbl-monitor'
      CodeUri: src/
      Handler: lambda_function.lambda_handler
      Runtime: python3.13
      Role: !GetAtt SBLMonitorFunctionRole.Arn
      Description: 'Monitors IP addresses for Spamhaus SBL registration and sends notifications with automatic dependency packaging'
      Environment:
        Variables:
          IP_ADDRESSES: !Ref IPAddresses
          DQS_SECRET_NAME: !Ref DQSSecret
          SNS_TOPIC_ARN: !Ref SBLNotificationTopic
      Events:
        ScheduledMonitoring:
          Type: Schedule
          Properties:
            Schedule: !If
              - IsSingleHour
              - 'rate(1 hour)'
              - !Sub 'rate(${MonitoringInterval} hours)'
            Name: !Sub '${ResourceNamePrefix}-sbl-monitor-schedule'
            Description: 'Scheduled trigger for Spamhaus SBL monitoring with retry and error handling'
            Enabled: true
            RetryPolicy:
              MaximumRetryAttempts: 3
            DeadLetterConfig:
              Arn: !GetAtt SBLMonitorDLQ.Arn
      Tags:
        Component: 'Lambda-Function'

  # Dead Letter Queue for failed EventBridge executions
  SBLMonitorDLQ:
    Type: AWS::SQS::Queue
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      QueueName: !Sub '${ResourceNamePrefix}-sbl-monitor-dlq'
      MessageRetentionPeriod: 1209600  # 14 days
      Tags:
        - Key: Cost
          Value: !Ref CostAllocationTag
        - Key: Application
          Value: 'Spamhaus-SBL-Monitor'
        - Key: Component
          Value: 'SQS-DLQ'

  # SNS Topic for SBL Notifications
  SBLNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${ResourceNamePrefix}-sbl-notifications'
      DisplayName: 'Spamhaus SBL Detection Alerts'
      KmsMasterKeyId: alias/aws/sns
      DeliveryStatusLogging:
        - Protocol: http/s
          SuccessFeedbackRoleArn: !GetAtt SNSLoggingRole.Arn
          FailureFeedbackRoleArn: !GetAtt SNSLoggingRole.Arn
      Tags:
        - Key: Cost
          Value: !Ref CostAllocationTag
        - Key: Application
          Value: 'Spamhaus-SBL-Monitor'
        - Key: Component
          Value: 'SNS-Topic'

  # IAM Role for SNS Delivery Status Logging
  SNSLoggingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ResourceNamePrefix}-sns-logging-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SNSLogsDeliveryRolePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:PutMetricFilter
                  - logs:PutRetentionPolicy
                Resource: '*'
      Tags:
        - Key: Cost
          Value: !Ref CostAllocationTag
        - Key: Application
          Value: 'Spamhaus-SBL-Monitor'
        - Key: Component
          Value: 'IAM-Role'

  # Email Subscription to SNS Topic
  SBLNotificationSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref SBLNotificationTopic
      Protocol: email
      Endpoint: !Ref NotificationEmail
      FilterPolicy:
        severity:
          - CRITICAL
          - WARNING
      FilterPolicyScope: MessageAttributes
      DeliveryPolicy:
        healthyRetryPolicy:
          numRetries: 3
          minDelayTarget: 20
          maxDelayTarget: 20
          numMinDelayRetries: 0
          numMaxDelayRetries: 0
          numNoDelayRetries: 0
          backoffFunction: linear

  # Secrets Manager Secret for DQS Key
  DQSSecret:
    Type: AWS::SecretsManager::Secret
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      Name: !Sub '${ResourceNamePrefix}-dqs-key'
      Description: 'Spamhaus Data Query Service (DQS) authentication key with encryption and access policies'
      SecretString: |
        {
          "DQS_KEY": "CHANGE-YOUR-DQS-KEY-LATER"
        }
      KmsKeyId: alias/aws/secretsmanager
      ReplicaRegions: []
      Tags:
        - Key: Cost
          Value: !Ref CostAllocationTag
        - Key: Application
          Value: 'Spamhaus-SBL-Monitor'
        - Key: Component
          Value: 'Secrets-Manager'

  # Resource Policy for DQS Secret (Least Privilege Access)
  DQSSecretResourcePolicy:
    Type: AWS::SecretsManager::ResourcePolicy
    Properties:
      SecretId: !Ref DQSSecret
      ResourcePolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowLambdaAccess
            Effect: Allow
            Principal:
              AWS: !GetAtt SBLMonitorFunctionRole.Arn
            Action:
              - secretsmanager:GetSecretValue
            Resource: '*'
            Condition:
              StringEquals:
                'secretsmanager:ResourceTag/Application': 'Spamhaus-SBL-Monitor'

Outputs:
  LambdaFunctionArn:
    Description: 'ARN of the SBL Monitor Lambda function'
    Value: !GetAtt SBLMonitorFunction.Arn
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunctionArn'
  SNSTopicArn:
    Description: 'ARN of the SBL Notification SNS topic'
    Value: !Ref SBLNotificationTopic
    Export:
      Name: !Sub '${AWS::StackName}-SNSTopicArn'
  SecretsManagerArn:
    Description: 'ARN of the DQS Key secret in Secrets Manager'
    Value: !Ref DQSSecret
    Export:
      Name: !Sub '${AWS::StackName}-SecretsManagerArn'
  DeadLetterQueueUrl:
    Description: 'URL of the Dead Letter Queue for failed executions'
    Value: !Ref SBLMonitorDLQ
    Export:
      Name: !Sub '${AWS::StackName}-DeadLetterQueueUrl'
  MonitoringConfiguration:
    Description: 'Summary of monitoring configuration'
    Value: !Sub 'Monitoring ${IPAddresses} every ${MonitoringInterval} hours, notifications to ${NotificationEmail}'

 

おわりに

Spamhaus に登録されてしまったら通知する仕組みをサーバレスで作成しました。

Kiro が作成してくれたコードは、分量の関係で全て公開はできませんでしたが、しっかりとした構成で処理を記載してくれていましたし、アーキテクチャ図やログ出力の丁寧な作りは納得の出来でした!

みなさんも良い ダイレクトメール管理生活を!

タイトルとURLをコピーしました