iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Build on AWS

亞馬遜熱帶雨林生存日記系列 第 20

Day 20: 使用AWS SNS設計文章發佈通知—如何搭配 API Gateway 和 Lambda 設計系統

  • 分享至 

  • xImage
  •  

昨天釐清需求和設計完架構之後,就可以拿著需求請 AI 幫忙寫 CloudFormation 的 template ,接著透過 CloudFormation 把 infra 和程式碼部署到 AWS 即可。下面是 CloudFormation template的範例,部署的時候,會需要先填三個預設的 email 當範例,等到 infra 部署完成,記得去 email 點驗證連結通過驗證。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway + Lambda + DynamoDB + SNS: Store Article and Notify on Success (by hjoru)'

Parameters:
  Email1:
    Type: String
    Description: The first notification email address
  Email2:
    Type: String
    Description: The second notification email address
  Email3:
    Type: String
    Description: The third notification email address

Resources:
  ArticlesTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Articles
      AttributeDefinitions:
        - AttributeName: ArticleId
          AttributeType: S
      KeySchema:
        - AttributeName: ArticleId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  ArticleNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: DirectPublishTopic
      DisplayName: BlogNotifications

  EmailSubscription1:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref ArticleNotificationTopic
      Protocol: email
      Endpoint: !Ref Email1

  EmailSubscription2:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref ArticleNotificationTopic
      Protocol: email
      Endpoint: !Ref Email2

  EmailSubscription3:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref ArticleNotificationTopic
      Protocol: email
      Endpoint: !Ref Email3

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-LambdaRole'
      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: DynamoDBSNSAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                Resource: !GetAtt ArticlesTable.Arn
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref ArticleNotificationTopic

  StoreAndNotifyLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: StoreArticleAndNotify
      Runtime: python3.9
      Handler: index.handler
      Timeout: 20
      MemorySize: 128
      Environment:
        Variables:
          TOPIC_ARN: !Ref ArticleNotificationTopic
          TABLE_NAME: !Ref ArticlesTable
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import json
          import boto3
          import uuid
          from datetime import datetime
          import os

          dynamodb = boto3.resource('dynamodb')
          sns = boto3.client('sns')
          table_name = os.environ['TABLE_NAME']
          topic_arn = os.environ['TOPIC_ARN']
          table = dynamodb.Table(table_name)

          def handler(event, context):
              try:
                  # Try to parse API Gateway POST body or direct invoke
                  if 'body' in event and event['body']:
                      try:
                          body = json.loads(event['body'])
                      except Exception:
                          body = event['body']
                  else:
                      body = event

                  # Required fields, others are optional
                  title = body.get('title', '')
                  content = body.get('content', '')
                  author = body.get('author', '')
                  summary = body.get('summary', '')
                  url = body.get('url', '')
                  columnId = body.get('columnId', '')

                  article_id = str(uuid.uuid4())
                  now = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')

                  item = {
                      'ArticleId': article_id,
                      'Title': title,
                      'Content': content,
                      'Author': author,
                      'Summary': summary,
                      'Url': url,
                      'ColumnId': columnId,
                      'CreatedAt': now
                  }

                  # 1. Write to DynamoDB
                  table.put_item(Item=item)

                  # 2. Notify via SNS only if DynamoDB write is successful
                  subject = f"New Article: {title or '(No Title)'}"
                  html_message = f"""
                  <html>
                  <head><title>{title}</title></head>
                  <body>
                      <h2>{title}</h2>
                      <p><b>Author:</b> {author}</p>
                      <p>{summary}</p>
                      <a href="{url}" target="_blank">Read more</a>
                      <p><small>Published at: {now}</small></p>
                  </body>
                  </html>
                  """

                  # SNS Email only supports plain text & limited subject, so send both
                  message = f"Title: {title}\nAuthor: {author}\nSummary: {summary}\nURL: {url}\nCreated At: {now}"

                  sns.publish(
                      TopicArn=topic_arn,
                      Message=message,
                      Subject=subject
                  )

                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'message': 'Article stored and notification sent',
                          'articleId': article_id
                      })
                  }
              except Exception as e:
                  return {
                      'statusCode': 500,
                      'body': json.dumps({'error': str(e)})
                  }

  # API Gateway (REST API for Lambda)
  ArticleApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: ArticleApi
      Description: 'API for storing articles and sending notifications'

  ArticleApiResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt ArticleApi.RootResourceId
      PathPart: article
      RestApiId: !Ref ArticleApi

  ArticleApiMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ArticleApi
      ResourceId: !Ref ArticleApiResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
          - { LambdaArn: !GetAtt StoreAndNotifyLambda.Arn }

  ArticleApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: ArticleApiMethod
    Properties:
      RestApiId: !Ref ArticleApi
      StageName: prod

  LambdaInvokePermissionApiGateway:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt StoreAndNotifyLambda.Arn
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ArticleApi}/*/*

Outputs:
  ApiEndpoint:
    Description: "POST endpoint for storing articles"
    Value: !Sub "https://${ArticleApi}.execute-api.${AWS::Region}.amazonaws.com/prod/article"
  TableName:
    Description: "DynamoDB Table Name"
    Value: !Ref ArticlesTable
  TopicArn:
    Description: "SNS Topic ARN"
    Value: !Ref ArticleNotificationTopic
  LambdaFunction:
    Description: "Lambda function for storing articles and sending notifications"
    Value: !Ref StoreAndNotifyLambda

部署完之後如果要測試,可以先到 API Gateway 取得 API 連結。

接著使用 curl 或 Postman 打 API ,收到成功的 response 之後,就可以去 email 查看有沒有收到通知。

curl --location 'https://av1qag0me0.execute-api.us-east-1.amazonaws.com/prod/article' \
--header 'Content-Type: application/json' \
--data '{
    "columnId": "tech-column-101",
    "title": "DynamoDB 存文章實作",
    "content": "這篇文章示範如何用 Lambda 存資料到 DynamoDB...",
    "author": "hjoru",
    "summary": "Lambda + DynamoDB 實戰",
    "url": "https://yourblog.com/posts/dynamodb-integration"
}'

email 收到信件就可以確認整個流程沒問題!


上一篇
Day 19: 使用AWS SNS設計文章發佈通知—流程和需求分析
下一篇
Day 21: 使用AWS SNS設計文章發佈通知—如何搭配CloudWatch Metrics和Dashboard追蹤系統問題 (上)
系列文
亞馬遜熱帶雨林生存日記23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言