のべラボ.blog

Tech Blog | AWS や サーバーレスやコンテナ などなど

Amazon SNS でサポートされた AWS X-Ray のアクティブトレースを試してみる

今回は、つい先日にアナウンスがあった Amazon SNS の下記の新しい機能を試してみます。

aws.amazon.com

なお、この記事の内容は 2023 年 2 月 12 日時点の検証結果に基づいて記載しています。


何ができるようになったのか

これまでも、Amazon SNS トピックは、呼び出し先が AWS Lambda 関数または HTTP/HTTPS のエンドポイントの場合は、AWS X-Ray のトレースを取得するために必要なコンテキストを伝播することはできました。

そのため、下図のようにトピックを経由して AWS Lambda 関数を呼び出すまでの一連のフローを AWS X-Ray で可視化することはできました。

ただし、この場合、Amazon SNS のトピックは トレースのコンテキストを AWS Lambda 関数や HTTP/S のエンドポイントに伝播しているだけで、Amazon SNS のトピック自体は AWS X-Ray のトレースを出力することはできませんでした。

そのため、例えば トピックのサブスクライバーAmazon SQS のキューの場合は、トピックからキューに対してメッセージを送信したときのトレースは取得できませんでした。

今回の Update で Amazon SNS トピックとしてどのような動作を行ったかを AWS X-Ray のトレースとして出力できるようになりました。

この機能を適用できるのは標準トピック だけで、FIFO トピックには適用できません。

また、トレース取得の対象にできるのは、サブスクライバーが 下記の場合のみです。

上記以外のEmail や SMS ( Short Message Service ) には適用できないことは留意しておきましょう。

それでは試してみましょう!


サブスクライバーAWS Lambda 関数や Amazon SQS キュー、Email の構成で試してみる

Amazon SNS のトピックで X-Ray トレースの取得を有効化して、下図のような構成でトピックにメッセージを発行してみます。


AWS マネジメントコンソール での設定

X-Ray トレースの有効化は、AWS マネジメントコンソールの場合は トピックの設定ページの [編集] ボタンから行えます。

また、トピックの設定ページの 下部にある [統合] タブから有効化を確認できます。


取得したトレース

AWS マネジメントコンソールからトピックに対してメッセージを発行してトレースを取得して表示してみましょう。

今回は、Amazon CloudWatch のコンソールの左側のメニューで [ X-Ray トレース ] - [ トレース ] を選択して表示してみます。

トレースマップでは、Amazon SNS トピックと サブスクライバーである Amazon SQS キューや、AWS Lambda 関数が表示されています。

セグメントのタイムラインでは、Amazon SNSAmazon SQS キューに SendMessage API を発行、また AWS Lambda 関数を invoke する API を発行したことも表示され、その処理にかかった時間も表示されています。

実際はサブスクライバーとして Email も設定しメールも送信されているのですが、トレースには含まれていません。ただ Email はもともとトレース取得の対象外なので、これは想定通りですね。

次に、敢えて サブスクライバーとして設定している AWS Lambda 関数を削除してから トピックにメッセージを送信したときのトレースをみてみましょう。

AWS Lambda 関数の invoke が エラーコード 403 で失敗していることがわかりますね。またそのため、サブスクリプションに設定した デッドレターキューに メッセージが送信されたこともわかります。


シンプルなファンアウトの構成で試してみる

次に、Amazon SNSAmazon SQS、AWS Lambda を組み合わせて下図のようなシンプルなファンアウトを実装した場合、AWS X-Ray ではどのようにトレースを可視化できるのかを試してみます。


使用する AWS Lambda 関数のコード

今回は、Python の Lambda 関数を使用します。下記は、Amazon SNS の標準トピックにメッセージを発行する Lambda関数です。

メッセージとして、ハンドラ関数の引数であるイベントオブジェクトを文字列化したものを送信する前提とします。

import json
import boto3
import datetime
import os
from botocore.config import Config
from aws_xray_sdk.core import patch
patch(['boto3'])

sns = boto3.client('sns')

def lambda_handler(event, context):
    topic_arn = os.getenv('SNS_TOPIC_ARN')
    dt = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S %Z')
   
    # Topicへメッセージ送信
    response = sns.publish(
        TopicArn=topic_arn,
        Message=json.dumps(event),
        Subject='Message from Lambda Function ' + dt
    )
    
    #
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Done:"+ dt
        }),
    }

次は、トピックをサブスクライブしている Amazon SQS のキューからメッセージを取得する Lambda 関数です。

関数名とイベントオブジェクトをそのまま print 関数で出力するだけのシンプルな内容です。

ファンアウトではありますが、今回はキューからメッセージを取得する 2 つの Lambda 関数は同じコードを使用します。

import json
import boto3
import datetime
import os
from botocore.config import Config
from aws_xray_sdk.core import patch
patch(['boto3'])

def lambda_handler(event, context):
    print("Lambda function name:", context.function_name)
    print("-----")
    print(event)
    #
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Done"
        }),
    }

なお、AWS X-RaySDK を使用していますが、AWS X-Ray SDK のパッケージは AWS Lambda レイヤーとして用意し、今回使用する全てのLambda 関数から共用する前提にしています。


シンプルなファンアウトを構築するための AWS SAM テンプレート

もちろん、AWS マネジメントコンソールから構築しても良かったんですが、再利用性を考え 今回は AWS SAM テンプレートで構築することにしました。

Amazon API GatewayAPIAWS Lambda 関数、そして今回のテーマである Amazon SNS トピックと全て AWS X-Ray のトレース取得を有効化する前提です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-demo-xray-fanout

  Sample SAM Template for sam-demo-xray-fanout

Parameters:
  SNSTOPIC:
    Type: String
    Default: 'DemoXRayFanoutTopic'
  SQSQUEUE1:
    Type: String
    Default: 'DemoXRayFanoutQueue1'
  SQSQUEUE2:
    Type: String
    Default: 'DemoXRayFanoutQueue2'  
Globals:
  Function:
    Timeout: 15
    Tracing: Active
    Layers:
      - !Ref DemoXRayLambdaLayer
  Api:
    TracingEnabled: True
Resources:
  DemoXRayFanoutSNSPublishFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: 'DemoXRayFanoutSNSPublishFunction'
      CodeUri: demo_xray_fanout_sns_publish_function/
      Handler: app.lambda_handler
      Runtime: python3.8
      Role: !GetAtt DemoXRayFunctionRole.Arn
      Environment:
        Variables:
          SNS_TOPIC_ARN: !GetAtt DemoXRayTopic.TopicArn
      Events:
        DemoXRayEvent:
          Type: Api 
          Properties:
            Path: /xray
            Method: get
  DemoXRayFanoutSQSReceiveFunction1:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: 'DemoXRayFanoutSQSReceiveFunction1'
      CodeUri: demo_xray_fanout_sqs_receive_function/
      Handler: app.lambda_handler
      Runtime: python3.8
      Role: !GetAtt DemoXRayFunctionRole.Arn
      Events:
        DemoSQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt DemoXRayQueue1.Arn
            BatchSize: 1
  DemoXRayFanoutSQSReceiveFunction2:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: 'DemoXRayFanoutSQSReceiveFunction2'
      CodeUri: demo_xray_fanout_sqs_receive_function/
      Handler: app.lambda_handler
      Runtime: python3.8
      Role: !GetAtt DemoXRayFunctionRole.Arn
      Events:
        DemoSQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt DemoXRayQueue2.Arn
            BatchSize: 1
  DemoXRayLambdaLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: 'DemoXRayLambdaLayer'
      CompatibleRuntimes:
        - python3.8
      ContentUri: demo_xray_layer/xray-python.zip
  DemoXRayFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal: 
              Service: "lambda.amazonaws.com"
      Policies:
        - PolicyName: "my-demo-xray-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:*"
                  - "sns:*"
                  - "sqs:*"
                  - "xray:PutTraceSegments"
                  - "xray:PutTelemetryRecords"
                Resource: "*"
  DemoXRayTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Ref SNSTOPIC
      TracingConfig: Active
  DemoXRayQueue1:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Ref SQSQUEUE1
  DemoXRayQueue2:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Ref SQSQUEUE2  
  DemoXRayTopicSubscription1:
    Type: 'AWS::SNS::Subscription'
    Properties:
      TopicArn: !Ref DemoXRayTopic
      Endpoint: !GetAtt 
        - DemoXRayQueue1
        - Arn
      Protocol: sqs
      RawMessageDelivery: 'true'   
  DemoXRayTopicSubscription2:
    Type: 'AWS::SNS::Subscription'
    Properties:
      TopicArn: !Ref DemoXRayTopic
      Endpoint: !GetAtt 
        - DemoXRayQueue2
        - Arn
      Protocol: sqs
      RawMessageDelivery: 'true'
  DemoXRayQueuePolicy: 
    Type: AWS::SQS::QueuePolicy
    Properties: 
      Queues: 
        - !Ref DemoXRayQueue1
        - !Ref DemoXRayQueue2
      PolicyDocument: 
        Statement: 
          - 
            Action: 
              - "SQS:SendMessage" 
            Effect: "Allow"
            Resource: 
              - !GetAtt DemoXRayQueue1.Arn
              - !GetAtt DemoXRayQueue2.Arn
            Principal:  
              Service: 
                -  "sns.amazonaws.com"
            Condition:
              ArnLike:
                aws:SourceArn:
                  - !GetAtt DemoXRayTopic.TopicArn
  DemoXRayResoucePolicy:
    Type: AWS::XRay::ResourcePolicy
    Properties: 
      BypassPolicyLockoutCheck: True
      PolicyName: "PolicyforDemoXRayFanoutTopic" 
      PolicyDocument: !Sub | 
        {
          "Version": "2012-10-17",
          "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": "sns.amazonaws.com"
            },
            "Action": [
              "xray:PutTraceSegments",
              "xray:GetSamplingRules",
              "xray:GetSamplingTargets"
            ],
            "Resource": "*",
            "Condition": {
              "StringEquals": {
                "aws:SourceAccount": "${AWS::AccountId}"
              },
              "StringLike": {
                "aws:SourceArn": "${DemoXRayTopic.TopicArn}"
              }
            }
          }
        ]
        }
Outputs:
  DemoXRayFunctionApi:
    Description: "API Gateway endpoint URL for Prod stage "
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/xray/"

この AWS SAM テンプレートでポイントになる部分は 2つです。

1 つ目は、104 行目 (下記)です。

TracingConfig: Active

この Amazon SNS トピックの TracingConfig プロパティで Active を指定することで、AWS X-Ray トレースの取得を有効化します。

2 つ目は、153 行目から 183 行目で、AWS X-Ray のリソースベースのポリシーを作成している部分です。

AWS マネジメントコンソールから Amazon SNS トピックに対して トレースを有効化したときには自動的に AWS X-Ray のリソースベースのポリシーも設定してくれたのですが、AWS SAM などを使用する場合は、明示的に作成する必要があります。

また、このポリシーの内容については 158行目からの PolicyDocument で指定するのですが、このプロパティは Type (型)が String になっています。

docs.aws.amazon.com

そのため、例えば 89 行目にある IAM ロールのポリシードキュメントの記述方法が異なるので注意が必要です。

89行目からの IAM ロールのポリシードキュメントは、Type が Json なので、下記のように記述できます。

PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:*"
                  - "sns:*"
                  - "sqs:*"
                  - "xray:PutTraceSegments"
                  - "xray:PutTelemetryRecords"
                Resource: "*"

しかし 153行目からの AWS X-Ray の リソースポリシーのポリシードキュメントは、Type が String なので、下記のように記述する必要があります。

 PolicyDocument: !Sub | 
        {
          "Version": "2012-10-17",
          "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": "sns.amazonaws.com"
            },
            "Action": [
              "xray:PutTraceSegments",
              "xray:GetSamplingRules",
              "xray:GetSamplingTargets"
            ],
            "Resource": "*",
            "Condition": {
              "StringEquals": {
                "aws:SourceAccount": "${AWS::AccountId}"
              },
              "StringLike": {
                "aws:SourceArn": "${DemoXRayTopic.TopicArn}"
              }
            }
          }
        ]
        }

この SAM テンプレートをデプロイしてスタックを作成し、Amazon API GatewayAPI のエンドポイント URL に GET リクエストを発行することで、今回のアプリケーションが実行され、AWS X-Ray トレースも取得されます。


取得したトレース

では、取得された AWS X-Ray トレースを AWS マネジメントコンソールからみてみましょう。

まずは、Amazon CloudWatch のページから表示した サービスマップ です。

赤枠部分で、ファンアウトで Amazon SQS のキューが表示されていますね。そのキューから、さらに AWS Lambda コンテキストや AWS Lambda 関数が表示されていることがわかりますね。

これは、Amazon SNS トピックがサブスクライバーである Amazon SQS の 2 つのキューにメッセージを送信していることを示しており、そのキューをイベントソースにしている AWS Lambda 関数が実行されたこともわかります。

Amazon SQS のキューにいったんメッセージは入りますが、トレースはそこで途切れずに AWS Lambda 関数が実行されたところまでが1つのマップとして表示されています。これについては以前、ブログで記事にしたので下記を参考にしてみて下さい。

nobelabo.hatenablog.com

次に Amazon CloudWatch のページからトレースを選択して、トレースマップ をみてみます。

サービスマップと似ていますが、明示的に「現在のトレース」と「リンクされたトレース」を枠で分けて表示してくれていますね。

その下に表示される セグメントのタイムライン をみてみます。

こちらも、明示的に「現在のトレース」と「リンクされたトレース」をわけて表示してくれていますね。

また、Amazon SNS のトピックをサブスクライブしている Amazon SQS の Queue について表示されており、トピックから SendMessage され、それが成功したこと、それらにかかった時間も確認できます。


最後に

これまでの Amazon SNS では AWS X-Ray については、正直、部分的な機能しかサポートされていないと感じていたのですが、今回の Update で Amazon SNS を使用したファンアウトのような構成を全体的に可視化できるようになったのは、とても便利だと感じました。

また、今回の検証を通じて、AWS X-Ray のリソースポリシーを AWS SAM ( AWS CloudFormation )で作成する方法も確認できたのも収穫でした!

/* -----codeの行番号----- */