[AWS] API Gateway + LambdaをX-Rayでトレースしてみる

# X-Rayとは
分散アプリケーションの分析とデバッグのためのサービスです。
詳細は、こちら。
[AWS X-Ray](https://aws.amazon.com/jp/xray/)

# 前提
今回も、以下構成でサンプルを構築してみようと思います。

- SAM
- Python3.8

SAMをはじめとする基本知識は、こちらを参照願います。

- [[AWS] Serverless Application Model (SAM) の基本まとめ](https://qiita.com/herohit-tool/items/420f4a7b294cfcf56ed7)
- [[AWS] Serverless Application Model (SAM) でAPI Gateway + Lambda + DynamoDBなサンプルを作成してみる](https://qiita.com/herohit-tool/items/5b0fe520f6f28fb5b4bc)

# サンプルを作ってみよう
## プロジェクト作成
例によって、SAMでHello Worldベースのプロジェクトを作成していきます。

```bash
$ sam init --runtime=python3.8
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Project name [sam-app]:

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

AWS quick start application templates:
	1 - Hello World Example
	2 - EventBridge Hello World
	3 - EventBridge App from scratch (100+ Event Schemas)
	4 - Step Functions Sample App (Stock Trader)
	5 - Elastic File System Sample App
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: sam-app
Runtime: python3.8
Dependency Manager: pip
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./sam-app/README.md
```

## X-Rayの追加
### ソースコードへのデコレータ追加
まず、コードにX-Rayのデコレータを追記します。
デフォルトで生成される`hello_world/app.py`をリファクタリングも含めて、以下のようにしてみます。

```python:hello_world/app.py
import json
from aws_xray_sdk.core import xray_recorder

@xray_recorder.capture('hello world')
def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        })
    }
```

### SDKの取り込み
LambdaにX-Ray SDKのライブラリを取り込むため、`hello_world/requirements.txt`を以下のように修正します。

```text:hello_world/requirements.txt
aws-xray-sdk
```

### SAMテンプレートの修正
最後に、Lambda関数、API Gatewayのトレーシングを有効にするため、Globalsの箇所を以下のように修正します。

```yaml:template.yml修正前
Globals:
  Function:
    Timeout: 3
```

```yaml:template.yml修正後
Globals:
  Function:
    Timeout: 3
    Tracing: Active
  Api:
    TracingEnabled: True
```


## ビルド

```bash
$ sam build
Building function 'HelloWorldFunction'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

## デプロイ

```bash
$ sam deploy --guided

Configuring SAM deploy
======================

	Looking for samconfig.toml :  Found
	Reading default arguments  :  Success

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]:
	AWS Region [us-east-1]: ap-northeast-1
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: y
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: y
	HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
	Save arguments to samconfig.toml [Y/n]: y

	Looking for resources needed for deployment: Found!

		Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1rzppw621dkka
		A different default S3 bucket can be set in samconfig.toml

	Deploying with following values
	===============================
	Stack name                 : sam-app
	Region                     : ap-northeast-1
	Confirm changeset          : True
	Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-1rzppw621dkka
	Capabilities               : ["CAPABILITY_IAM"]
	Parameter overrides        : {}

Initiating deployment
=====================

	Saved arguments to config file
	Running 'sam deploy' for future deployments will use the parameters saved above.
	The above parameters can be changed by modifying samconfig.toml
	Learn more about samconfig.toml syntax at
	https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

	Deploying with following values
	===============================
	Stack name                 : sam-app
	Region                     : ap-northeast-1
	Confirm changeset          : True
	Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-1rzppw621dkka
	Capabilities               : ["CAPABILITY_IAM"]
	Parameter overrides        : {}

Initiating deployment
=====================
HelloWorldFunction may not have authorization defined.
Uploading to sam-app/8476bddd8c14756a7c801a61352b828d.template  1142 / 1142.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
------------------------------------------------------------------------------------------------
Operation                        LogicalResourceId                ResourceType
------------------------------------------------------------------------------------------------
+ Add                            HelloWorldFunctionHelloWorldPe   AWS::Lambda::Permission
                                 rmissionProd
+ Add                            HelloWorldFunctionRole           AWS::IAM::Role
+ Add                            HelloWorldFunction               AWS::Lambda::Function
+ Add                            ServerlessRestApiDeployment47f   AWS::ApiGateway::Deployment
                                 c2d5f9d
+ Add                            ServerlessRestApiProdStage       AWS::ApiGateway::Stage
+ Add                            ServerlessRestApi                AWS::ApiGateway::RestApi
------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:************:changeSet/samcli-deploy1597324152/18c44433-12d1-4e03-9fb8-00737d018991


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2020-08-13 22:09:28 - Waiting for stack create/update to complete

CloudFormation events from changeset
-------------------------------------------------------------------------------------------------
ResourceStatus           ResourceType             LogicalResourceId        ResourceStatusReason
-------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS       AWS::IAM::Role           HelloWorldFunctionRole   -
CREATE_IN_PROGRESS       AWS::IAM::Role           HelloWorldFunctionRole   Resource creation
                                                                           Initiated
CREATE_COMPLETE          AWS::IAM::Role           HelloWorldFunctionRole   -
CREATE_IN_PROGRESS       AWS::Lambda::Function    HelloWorldFunction       -
CREATE_IN_PROGRESS       AWS::Lambda::Function    HelloWorldFunction       Resource creation
                                                                           Initiated
CREATE_COMPLETE          AWS::Lambda::Function    HelloWorldFunction       -
CREATE_IN_PROGRESS       AWS::ApiGateway::RestA   ServerlessRestApi        Resource creation
                         pi                                                Initiated
CREATE_IN_PROGRESS       AWS::ApiGateway::RestA   ServerlessRestApi        -
                         pi
CREATE_COMPLETE          AWS::ApiGateway::RestA   ServerlessRestApi        -
                         pi
CREATE_IN_PROGRESS       AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   -
                         yment                    yment47fc2d5f9d
CREATE_IN_PROGRESS       AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   Resource creation
                         yment                    yment47fc2d5f9d          Initiated
CREATE_IN_PROGRESS       AWS::Lambda::Permissio   HelloWorldFunctionHell   Resource creation
                         n                        oWorldPermissionProd     Initiated
CREATE_IN_PROGRESS       AWS::Lambda::Permissio   HelloWorldFunctionHell   -
                         n                        oWorldPermissionProd
CREATE_COMPLETE          AWS::ApiGateway::Deplo   ServerlessRestApiDeplo   -
                         yment                    yment47fc2d5f9d
CREATE_IN_PROGRESS       AWS::ApiGateway::Stage   ServerlessRestApiProdS   -
                                                  tage
CREATE_COMPLETE          AWS::Lambda::Permissio   HelloWorldFunctionHell   -
                         n                        oWorldPermissionProd
CREATE_IN_PROGRESS       AWS::ApiGateway::Stage   ServerlessRestApiProdS   Resource creation
                                                  tage                     Initiated
CREATE_COMPLETE          AWS::ApiGateway::Stage   ServerlessRestApiProdS   -
                                                  tage
CREATE_COMPLETE          AWS::CloudFormation::S   sam-app                  -
                         tack
-------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------
Outputs
-------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::************:role/sam-app-HelloWorldFunctionRole-IIPXQC9S1XKJ

Key                 HelloWorldApi
Description         API Gateway endpoint URL for Prod stage for Hello World function
Value               https://tws0qc6nbc.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/

Key                 HelloWorldFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:ap-northeast-1:************:function:sam-app-
HelloWorldFunction-1OH75PSLQUSLC
-------------------------------------------------------------------------------------------------

Successfully created/updated stack - sam-app in ap-northeast-1
```

## 実行

```bash
curl https://tws0qc6nbc.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "hello world"}
```

成功しました。

## 確認
では、マネジメントコンソールで、X-RayのService Map、Traces、アナリティクスを確認してみましょう。

### Service Map

クライアントから呼び出され、API Gateway、Lambdaと順に呼び出されていく様子と、それぞれのレイテンシーを確認することができます。

<img width="1344" alt="xray1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/ea957c13-adbd-fd53-f8b3-6c56bdf59caa.png">

### Traces

こちらでは、トレースの状況を確認することができます。
ここでは、それぞれの条件に応じてフィルタリングすることができます。

- URL
- StatusCOde
- Method
- User
- UserAgent
- ClientIP
- InstanceId
- ResourceARN
- Response time root causes
- Error root causes
- Error root cause messages
- Fault root causes
- Fault root cause messages
- Annotation aws:api id

<img width="1340" alt="xray2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/53c12f10-f89a-f13a-f0e6-660f6da96994.png">

### アナリティクス
最後にアナリティクスは、その名の通り、さまざまな分析をしたり、比較を行ったりすることができます。

<img width="1343" alt="xray3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/808f9b9a-e2a6-6c25-0f60-80e3052922ab.png">

# AWSサービスをトレースしてみる
では、AWSサービスのトレースをしてみましょう。
今回は、DynamoDBにPutItemする時のトレースを追加してみます。

細かい内容は、「[[AWS] Serverless Application Model (SAM) でAPI Gateway + Lambda + DynamoDBなサンプルを作成してみる](https://qiita.com/herohit-tool/items/5b0fe520f6f28fb5b4bc)」を参照いただきたいて、X-Rayに関する部分だけポイントで説明したいと思います。

## コードの追加
まず、HelloWorldとは別に、Function用のディレクトリを作成し、その中にファイルを作成していきます。

```bash
$ mkdir sam_ddb
$ touch sam_ddb/app.py
$ touch sam_ddb/requirements.txt
```

まずは、Lambda関数の本体ですが、ここで重要なのが、`patch['boto3']`の部分です。
これを行うことによって、boto3のAWSサービス呼び出しのトレースが行えるようになります。

```python:sam_ddb/app.py
import json
import boto3
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch
from datetime import datetime

patch(['boto3'])

@xray_recorder.capture('put_item ddb')
def lambda_handler(event, context):
    event_body = json.loads(event["body"])
    dynamodb = boto3.resource("dynamodb")

    table = dynamodb.Table("Demo")
    table.put_item(
        Item={
            "Key": event_body["key"],
            "CreateDate": datetime.utcnow().isoformat()
        }
    )

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "succeeded",
        }),
    }
```

```text:sam_ddb/requirements.txt
aws-xray-sdk
boto3
```

最後に、`template.yml`を編集します。
変更が必要なのは、`Resources`と、`Outputs`の部分です。
IAM Roleで、X-Rayに関する権限が必要なので注意してください。

```yaml:template.yml変更前
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    Tracing: Active
  Api:
    TracingEnabled: True

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
```

```yaml:template.yml変更後
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    Tracing: Active
  Api:
    TracingEnabled: True

Resources:
  DynamoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Demo
      AttributeDefinitions:
        - AttributeName: Key
          AttributeType: S
        - AttributeName: CreateDate
          AttributeType: S
      KeySchema:
        - AttributeName: Key
          KeyType: HASH
        - AttributeName: CreateDate
          KeyType: RANGE
      ProvisionedThroughput: 
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

  SamDdbFunction:
    Type: AWS::Serverless::Function
    Properties:
      Role: !GetAtt SamDdbFunctionIamRole.Arn
      CodeUri: sam_ddb/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        SamDdb:
          Type: Api
          Properties:
            Path: /ddb
            Method: post

  SamDdbFunctionIamRole:
    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/CloudWatchLogsFullAccess'
      Policies:
        - PolicyName: 'SamDdbPolicy'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                Resource: !GetAtt DynamoTable.Arn
              - Effect: Allow
                Action:
                  - xray:PutTraceSegments
                  - xray:PutTelemetryRecords
                Resource: '*'

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
  SamDdbApi:
    Description: "API Gateway endpoint URL for Prod stage for SAM DDB function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ddb/"
```

## ビルドとデプロイ

`sam build`と、`sam deploy --guided`を実行してください。
（内容は省略します）

## 実行
今回はAPIを1個追加しているので、比較のためにHelloWorldのAPIも呼び出してみます。

```bash
$ curl https://fp8nyhpv87.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "hello world"}
```

続いて、DynamoDBに書き込むPostのAPIを呼び出してみます。

```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"key": "demo-data"}' https://fp8nyhpv87.execute-api.ap-northeast-1.amazonaws.com/Prod/ddb/
{"message": "succeeded"}
```

## X-Rayの確認
まずは、Service Mapから。
なぜか配置がねじれてますが、DynamoDBへのアクセス部分が切り出されて表示されているのがわかると思います。
DBの書き込み部分が620msかかっていることがわかりますね。

<img width="1332" alt="xray4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/aa0b770e-9daf-5c73-4945-2eea6725bacf.png">

Tracesでも同様です。
DynamoDBの部分が独立してトレースできます。

<img width="1338" alt="xray5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/5e9a8118-4137-1352-74b3-efea74d43cf9.png">

## 念のためDynamoDBも確認
ばっちりデータが登録されてます。

<img width="1338" alt="ddb.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/2cba1ef1-b5ea-6d00-ce74-186e92d0be08.png">

# まとめ
X-Rayによるトレーシングで、様々な分析、解析が可能になります。
事後でも簡単に追加できますが、できれば、初期の段階から導入されることをおすすめします。

# サンプルコードリポジトリ
https://github.com/hito-psv/sam-demo-003