🐥

SNS + Lambdaで簡単なPub/Sub構成を作ってみる【Node.js + CloudFormation】

に公開

はじめに

AWS SNSとLambdaを組み合わせたシンプルなPub/Sub構成をNode.jsとCloudFormationを使って構築してみます。


前提

  • AWS CLIがインストール済みであること。
  • AWS CLI実行時のIAMユーザのアクセスキーおよびシークレットアクセスキーがcredentialsファイルに記載されていること。
  • AWS CLI実行時のIAMユーザにAdministratorAccessのIAMロールが付与されていること
  • Node.js 22系がインストールされていること

全体の流れ

  1. プロジェクトを作成する
  2. Lambdaコードを作成する
  3. SNS / Lambda / IAM等一括作成するためのCloudFormationを作成する
  4. デプロイ用スクリプトを作成する
  5. デプロイする
  6. 動作確認

ディレクトリ構成

最終的に以下のようなディレクトリ構成になります。

sns-lambda-demo/
├── app/
│   ├── publisher.js
│   └── subscriber.js
├── deploy/
│   ├── app.zip          # デプロイ操作時に生成されます
│   └── deploy.sh
├── infrastructure/
│   └── template.yaml
└── package.json

1. プロジェクトを作成

mkdir sns-lambda-demo && cd $_

npm init -y
npm pkg set type="module"

mkdir -p app deploy infrastructure

2. Lambdaコードを作成

app/publisher.js
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';

const client = new SNSClient({ region: process.env.AWS_REGION });

export const handler = async () => {
  const command = new PublishCommand({
    TopicArn: process.env.TOPIC_ARN,
    Message: 'Hello from publisher Lambda!'
  });
  await client.send(command);
  return { statusCode: 200, body: JSON.stringify('Message published') };
};
app/subscriber.js
export const handler = async (event) => {
  console.log('Received SNS Event:', JSON.stringify(event, null, 2));
};

3. SNS / Lambda / IAM等一括作成するためのCloudFormationを作成する

infrastructure/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda Pub/Sub demo (Node.js 22)

Parameters:
  LambdaCodeBucket:
    Type: String
    Description: S3 bucket that contains *app.zip*
  ZipKey:
    Type: String
    Default: app.zip
    Description: Object key of the Lambda artifact

Resources:
  # SNS Topic
  DemoTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${AWS::StackName}-demo-topic"

  # IAM roles
  PublisherLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      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
      Policies:
        - PolicyName: AllowSNSPublish
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: sns:Publish
                Resource: !Ref DemoTopic

  SubscriberLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      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

  # Publisher Lambda
  PublisherLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${AWS::StackName}-publisher"
      Runtime: nodejs22.x
      Handler: app/publisher.handler
      Role: !GetAtt PublisherLambdaExecutionRole.Arn
      Code:
        S3Bucket: !Ref LambdaCodeBucket
        S3Key: !Ref ZipKey
      Environment:
        Variables:
          TOPIC_ARN: !Ref DemoTopic

  # Subscriber Lambda
  SubscriberLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${AWS::StackName}-subscriber"
      Runtime: nodejs22.x
      Handler: app/subscriber.handler
      Role: !GetAtt SubscriberLambdaExecutionRole.Arn
      Code:
        S3Bucket: !Ref LambdaCodeBucket
        S3Key: !Ref ZipKey

  # SNS → Lambda subscription
  DemoSubscription:
    DependsOn: AllowSNSInvokeLambda
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref DemoTopic
      Protocol: lambda
      Endpoint: !GetAtt SubscriberLambda.Arn

  AllowSNSInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref SubscriberLambda
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref DemoTopic

4. デプロイ用スクリプトを作成する

deploy/deploy.sh
#!/usr/bin/env bash
set -eu

STACK_NAME=sns-lambda-demo
REGION=${AWS_REGION:-ap-northeast-1}
BUCKET_NAME=${STACK_NAME}-$(date +%Y%m%d%H%M%S)
ZIP_KEY=app.zip

# 1) S3 バケット用意(無ければ作成)
if ! aws s3api head-bucket --bucket "$BUCKET_NAME" 2>/dev/null; then
  aws s3 mb "s3://$BUCKET_NAME" --region "$REGION"
fi

# 2) ZIP 作成(appフォルダ直下の.jsをまとめる)
zip -q -r deploy/app.zip app package.json

# 3) アップロード
aws s3 cp deploy/app.zip "s3://$BUCKET_NAME/$ZIP_KEY"

# 4) デプロイ
aws cloudformation deploy \
  --template-file infrastructure/template.yaml \
  --stack-name "$STACK_NAME" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides LambdaCodeBucket=$BUCKET_NAME ZipKey=$ZIP_KEY

5. デプロイする

プロジェクト直下のディレクトリまで移動した後、
実行権を付与してから、デプロイ用スクリプトを実行します。

# 1) プロジェクト直下のディレクトリまで移動
# /path/toの部分は環境に合わせて書き換えてください
cd /path/to/sns-lambda-demo

# 2) デプロイスクリプトに実行権を付与
chmod +x deploy/deploy.sh

# 3) デプロイ用スクリプトを実行
./deploy/deploy.sh

スクリプト実行後、CloudFormationのコンソールに以下のようにCREATE_COMPLETEの状態でスタックが作成されていれば成功です。


6. 動作確認

Publisher Lambdaを手動で呼び出してみます。

aws lambda invoke \
  --function-name sns-lambda-demo-publisher \
  --payload '{}' response.json && cat response.json

CloudWatch Logsで /aws/lambda/sns-lambda-demo-subscriberのロググループを開き、ログメッセージ内にReceived SNS Event:で始まるログが含まれていれば、SNS → Lambdaの受信を確認できたことになります。


まとめ

今回は、AWSのSNSとLambdaを組み合わせて、シンプルなPub/Sub構成を構築しました。
また、インフラからソースコード、デプロイ手順までを自動化し、最小限の手動操作で動作確認できる仕組みを目指しました。

この記事を通じて、Pub/Subの基本的な流れと、CloudFormationによるリソース管理のイメージをつかんでいただければ幸いです。

Discussion