こんにちは、SCSKでテクニカルエスコートサービスを担当してる兒玉です。
前回途中だった Spamhaus に登録されてしまったら通知する仕組みの動作確認と修正を行います。
動作確認
通常は Amazon EventBridge から n 時間(設定値)間隔で Lambda 関数を実行するのですが、今回はテストなので手っ取り早く Lambda関数のテストから実行します。
はい、失敗しました。
ログを確認すると、lambda_function というモジュールが無い、というエラーですね。うまくLambda関数がデプロイ出来ていないように思えます。
Lambda関数を確認すると、index.py しかありません。
CloudFormation テンプレートに戻って確認すると、インラインで Pythonのソースコードのテキストを zip ファイル化してアップロードする記述になっています。これでは動きません。

方針転換
なぜこの様な失敗になったのかというと、CloudFormationの ZipFile プロパティは、外部ライブラリを含めることができず、コードサイズにも制限があります。
また、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についてはここでは詳細には紹介しませんが、詳しく知りたい方は以下をご参照ください。
Kiro再び
Kiroに、要件変更の理由と、CloudFormationからSAMに変更する旨を伝えて、requirements、design、tasksの修正を依頼します。
全部が書き直されるのではなく、きちんと差分を理解していて、トレーサビリティマトリクスを元に再実行必要なタスクが洗いだされました。
またしてもタスク実行地獄です。
Task completeになったタスクを未実行に戻す方法がわからなかったので Kiro に聞いて戻してもらいました。
修正した箇所を見ると、単純にチェックボックス が [x] となっているのを [ ] にすれば良いようです。
Task 1を修正後 AWS SAM cli をインストールして、 sam validate-template、sam build が通ったので、Task 11、12、13 と再度実行します。
実行後、出来上がった SAMテンプレート を使用してデプロイすると、今度はlambda_function.py も ライブラリ群も含まれています。
1時間に1回の実行の設定でデプロイしたので、暫く経つと Lambda 関数にログが出力されているはずです。Lambda関数の モニタリングから CloudWatch Logs を確認してみると…

おお、キチンとログが出力されていました。Kiro スペックモードでは、ログに関してもかなりバッチリ作ってくれたので、見やすいログが出力されています。
ちょっと興味があったので、Kiro に StructuredLogger の構成を Mermaid 図に起こしてもらったらこのようになっていました。
きちんと、センシティブな情報は SensitiveDataFilter でフィルタしてくれる処理も実装されているようです。
「プロトタイプからプロダクションまで(Agentic AI development from prototype to production)」と、いうキャッチフレーズに恥じない作りですね。
初期設定として 192.168.1.1 (プライベートIPアドレスなので、Spamhaus には登録されない)を監視対象としたので、 “is_listed” : false と、期待通りの実行結果です。
このアドレスを、試しに Spamhaus SBLに登録されてしまった検証に利用できる 127.0.0.2 を設定します。
Lambda関数の新しいバージョンを発行して、(ここではバージョン2) Lambda関数の live エイリアス値 を 1 -> 2 へ変更します。

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

出来上がりイメージ
出来上がったコードはかなりしっかりと作ってくれたこともあり、すべてを この blog 上で乗せることができないような分量になってしました。
ですが、せっかくですので、アーキテクチャ図と、出来あがった SAM テンプレートを乗せておきます。
アーキテクチャ図は Kiro に書いてもらったものが全部入りで混沌としていたので、適度な範囲で分割しました。Mermaid記法 はテキストベースなので、分割したり修正したりが簡単なのが便利ですね!
SAM の デプロイアーキテクチャ図
SBL Monitor のメインアーキテクチャ図
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 が作成してくれたコードは、分量の関係で全て公開はできませんでしたが、しっかりとした構成で処理を記載してくれていましたし、アーキテクチャ図やログ出力の丁寧な作りは納得の出来でした!
みなさんも良い ダイレクトメール管理生活を!












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