AWS X-Rayをとりあえず使ってみたいハンズオン

この記事は[ニフティグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nifty)の23日目の記事です。
#はじめに
こんにちは。普段はサービス開発運用を担当していますTakumi-Miuraと申します。
普段はAWS+pythonでユーザー管理アプリケーションを作ってます。最近VRデビューしました。

#AWS X-Rayとは？
AWSにあるサービスの一つで、「本番環境や分散アプリケーションの分析とデバッグ」と銘打たれています。[^1]
AWSにデプロイされたアプリケーションにSDKを組み込むことによって、アプリケーションのリクエスト・レスポンスの状況や、アプリケーションから発行されたデータベース、APIへのアクセスのレスポンス時間など、アプリケーションの稼働状況に関する様々な情報を取得することができるというものです。

#X-Rayを調べる経緯(=使う理由)
私の担当するシステムはさまざまな社内・社外システムと連携して情報取得や申込受付などを実施しているのですが、連携先システムのトラブルでチームのシステムにも影響が及ぶ、ということがしばしば発生します。
もちろんログなどでシステムエラーが発生して検知できるのですが、最近はパフォーマンスの測定と改善にも照準が当たってきており、普段のシステムの健康状態を知りたいという気持ちが出てきました。
担当システムではAWSを利用しているのでAWSのサービスで測定に主眼を置いたサービスはないかと探したところ、X-Rayというサービスを知り、試しに使ってみたら「ええやん！」となったのでここで書いておきます。

#ハンズオン
AWSでチュートリアルが用意されていますがそれだとつまらないので、自分でSDKを組み込んだアプリケーションを作成してAWS環境に乗せて稼働状況をウォッチしてみたいと思います。

##デプロイする環境の用意
AWSの中でも最もシンプルに構築ができるVPC+EC2でWEBアプリケーションを2台起動したいと思います。
VPC+EC2でアプリケーションを手軽に構築する部分については詳しい説明を省きます。(たくさん良記事があるはずなので)
最終的には以下のような構成を作成しておけば、今回の検証が可能になります。
![aws_xray_handson.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246182/af746ad5-2f9c-2cb0-32a6-037da2b89107.jpeg)
気を付けるポイントは、EC2に対してIAMロールでX-Rayへのアクセス権限を付与しておく必要があることです。EC2に設定するIAMロールに`AWSXRayDaemonWriteAccess`ポリシーを付与しておきましょう。ついでに`AmazonEC2RoleforSSM`ポリシーを付与しておくと、セッションマネージャーを利用してコンソールから直接ログインできて便利です。

##アプリケーションの準備
今回はpython+Flaskで簡単なAPIサーバーを2種類作成し、2台のサーバーにそれぞれ載せたいと思います。なぜAPIを2種類建てるかというと、自前で建てたAPIサーバー同士の通信をX-Rayを使って測定したいからです。
X-RayのSDKはpythonのWEBアプリケーションフレームワークのうち`Django`、`Flask`、`Bottle`に対応しています。フレームワークに対応していなくても組み込みは可能ですが、この3つのフレームワークであればAWSが記述例を掲載してくれているため非常に簡単にX-Rayを導入できます。[^2]
以下が今回用意したソースです。`<>`で括った箇所は適宜書き替えてください。

<details><summary>API1号機のソース</summary><div>

```py:app1.py
from aws_xray_sdk.core import xray_recorder, patch
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware
from flask import Flask, request
import os
import requests

app = Flask(__name__)

# APIのIPを設定
api_2_ip = "<API2号機用のEC2に割り振られたグローバルIP>"

# X-Ray設定
plugins = ('EC2Plugin',)
xray_recorder.configure(plugins=plugins)
xray_recorder.configure(service='python_test_API_1')

# HTTPリクエストに利用するモジュールを指定
libraries = (['requests'])
patch(libraries)

# X-Rayサンプリングルールの設定
sampling_rule_path = os.getcwd() + "/" + "sampling_rule.json"
xray_recorder.configure(sampling_rules=sampling_rule_path)

# FlaskとX-Rayを連携
XRayMiddleware(app, xray_recorder)

# API1号機から固定値を返却
@app.route("/api/1/fixed_value", methods=["GET"])
def api_1_fixed_value():
    return {"message": "API_1_TEST_RESPONSE"}, 200

# API1号機からAPI2号機へリクエストを送信する
@app.route("/api/1/to2", methods=["GET"])
def api_1_value_request_to_2():
    path = "http://" + api_2_ip + ":5000/api/2/fixed_value"
    response = requests.get(path)
    return response.json(), 200

# API1号機からAPI2号機へリクエストを送信し、その先でさらに別のAPIへリクエストする
@app.route("/api/1/to_other_api", methods=["GET"])
def api_1_to_other_api():
    path = "http://" + api_2_ip + "/api/2/to_other_api"
    response = requests.get(path)
    return response.json(), 200

if __name__=='__main__':
    app.run(host="0.0.0.0", debug=True, port=5000)

```

</div></details>
<details><summary>API2号機のソース</summary><div>

```py:app2.py
from aws_xray_sdk.core import xray_recorder, patch
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware
from flask import Flask, request
import os
import requests

app = Flask(__name__)

# X-Ray設定
plugins = ('EC2Plugin',)
xray_recorder.configure(plugins=plugins)
xray_recorder.configure(service='python_test_API_2')

# HTTPリクエストに利用するモジュールを指定
libraries = (['requests'])
patch(libraries)

# X-Rayサンプリングルールの設定
sampling_rule_path = os.getcwd() + "/" + "sampling_rule.json"
xray_recorder.configure(sampling_rules=sampling_rule_path)

# FlaskとX-Rayを連携
XRayMiddleware(app, xray_recorder)

# API2号機から固定値を返却
@app.route("/api/2/fixed_value", methods=["GET"])
def api_2_fixed_value():
    return {"message": "API_2_TEST_RESPONSE"}, 200

# 任意のAPIで情報取得する
@app.route("/api/2/to_other_api", methods=["GET"])
def api_2_to_other_api():
    path = "<任意のAPIのURL>"
    response = requests.get(path)
    return response.json(), response.status_code

if __name__=='__main__':
    app.run(host="0.0.0.0", debug=True, port=5000)
```

</div></details>
<details><summary>API共通のパッケージリスト</summary><div>

```txt:requirements.txt
aws-xray-sdk==2.8.0
botocore==1.23.20
certifi==2021.10.8
charset-normalizer==2.0.9
click==8.0.3
colorama==0.4.4
Flask==2.0.2
future==0.18.2
idna==3.3
itsdangerous==2.0.1
Jinja2==3.0.3
jmespath==0.10.0
MarkupSafe==2.0.1
python-dateutil==2.8.2
requests==2.26.0
six==1.16.0
urllib3==1.26.7
Werkzeug==2.0.2
wrapt==1.13.3
```

</div></details>
<details><summary>X-Ray設定</summary><div>

```json:sampling_rule.json
{
    "version": 2,
    "rules": [
      {
        "description": "python_test_API sampling_rule",
        "host": "*",
        "http_method": "*",
        "url_path": "/api/*",
        "fixed_target": 1,
        "rate": 1
      }
    ],
    "default": {
      "fixed_target": 10,
      "rate": 0.1
    }
}
```

</div></details>

X-Rayに関わる箇所を抜粋して確認していきます。

```py
from aws_xray_sdk.core import xray_recorder, patch
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware

# X-Ray設定
plugins = ('EC2Plugin',)
xray_recorder.configure(plugins=plugins)
xray_recorder.configure(service='python_test_API_1')

```

パッケージをインポートして、X-Rayへの設定を行っています。pluginsでEC2を指定することでインスタンスIDやアベイラビリティーゾーンの情報を付与することができます。またserviceで名前を付けていますが、X-Rayはserviceごとに情報を集積しており、同じserviceであればダッシュボードでも統一して表示するようになります。

```py
# HTTPリクエストに利用するモジュールを指定
libraries = (['requests'])
patch(libraries)
```

API2号機では外部のAPIを呼び出すためにrequestsを利用しています。SDKが対応しているライブラリであれば、ライブラリ名を配列にして`patch`で適用することでX-Rayによしなにデータを送信してくれます。[^4]


```py
# X-Rayサンプリングルールの設定
sampling_rule_path = os.getcwd() + "/" + "sampling_rule.json"
xray_recorder.configure(sampling_rules=sampling_rule_path)
```
ここではX-Rayのサンプリングルールを読み込んでいます。
サンプリングルールでは、大量のアクセスの中からどんなパターンのリクエストについて情報を収集するか、記録するデータの量はどのくらいかを指定しています。

```json:sampling_rule.json
{
    "version": 2,
    "rules": [
      {
        "description": "python_test_API sampling_rule",
        "host": "*",
        "http_method": "*",
        "url_path": "/api/*",
        "fixed_target": 1,
        "rate": 1
      }
    ],
    "default": {
      "fixed_target": 10,
      "rate": 0.1
    }
}
```

ここでは、ホストについては何でもよし、URLは`/api/`で始まるものを取得するようになっています。また、`fixed_target`で秒間の最初のリクエストを必ず取得するように、`rate`でその後何%のリクエストを受け取るかを指定しています。今回は1秒間の1回目のリクエストと、その後のリクエストの100%を取得するように設定しています。

```py
# FlaskとX-Rayを連携
XRayMiddleware(app, xray_recorder)
```
X-RayとFlaskを連携させています。これにより、Flaskのリクエスト・レスポンスを追うことができるようになりました。

##起動
上記のファイルをEC2インスタンス上に搭載します。Gitで落とすでもコピペでEC2インスタンスに直接ファイルを作成するでもよいですが、すべてのファイルが同じディレクトリにあるようにしましょう。

```
適当なディレクトリ/
　├ app1.py
　├ requirements.txt
　└ sampling_rule.json
```

X-RayではX-Rayデーモンをサーバー上で起動しリクエストを待ち受けている状態にする必要があります。デーモンをインストールして実行しておきましょう。
設定方法は[こちら](https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-daemon-ec2.html)に掲載されてます。

```sh
sudo curl https://s3.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-3.x.rpm -o /tmp/xray.rpm
sudo yum install -y /tmp/xray.rpm
```

以下コマンドでAPIを起動していきます。`pip3`でパッケージをインストールして`python3 app1.py`で起動していくだけです。

```sh
cd <適当なディレクトリ>
pip3 install -r requirements.txt
python3 app1.py
```

これで準備完了です。APIへ向かってリクエストを投げていきましょう。

##X-Rayコンソールでのモニタリング
APIへ向かって適当にリクエストを投げた後、AWSコンソールからX-Rayを表示すると、すでにアクセス情報やダッシュボードが入っているはずです。

![Inkedx-ray_service_map_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246182/20be21c8-cb68-9008-6a71-fc079394b1ff.jpeg)
サービスマップで視覚的にシステムのレスポンスや状態を確認することができます。API1号機からAPI2号機へのリクエスト状況、API2号機からほかのAPIへのリクエストも確認できました。障害時には赤色になるので、どこがトラブルとなっているかがすぐ分かります。
![x-ray_trace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246182/3ba3bbe2-5c83-3686-b10b-42f50a81991d.png)
トレースではより詳細なリクエストごとの情報が載っています。自分のアクセス元IPも載っていて最初ちょっとビビりました。

#使ってみた感想
##API連携,DBアクセスがあるシステムでの効力は凄い
X-Rayは稼働直後からサービス間の連携がグラフィカルに表示されるため、非常にとっつきやすく効果を感じられるサービスに思えますし、実際連携システムが複数に渡るときに障害点がどこかを探るのも非常にやりやすいなと感じられました。
マイクロサービスが主流になりつつある中、連携システムのレスポンスを把握しつつシステム自体の品質も手軽に確認できるため、非常に重宝されるサービスではないかなと思います。逆に、システムそれ自体で完結している場合はそこまで威力を発揮しないとも思えました。

##SDK導入によるアプリケーションの維持コスト増
導入に際してアプリケーションに対してSDKを組み込まなければいけないことが難点ではあります。バージョン管理や脆弱性対応が発生した際など、様々な場面で考慮すべきミドルウェアが一つ増えるというのは手間になると思います。
ミドルウェアやアプリケーション自体の更新が容易であるようなアーキテクチャやインフラの採用、デプロイが積極的に行われるような運用フローの整備、アプリケーション自体が塩漬けにならないような仕組み作りも同時に必要になるでしょう。
そもそもSDKが用意されていない言語だと組み込めないという弱点もあります。X-Rayに言語選定が引きずられるようなことはしないほうがよいでしょう。

#おわりに
以上簡単なX-Rayのハンズオンでした。普段からAPIでの連携をしまくっているシステムを受け持っている私にはとても魅力的なサービスなので、担当システムへの組み込みを含め様々に検討していきたいと思います！

[^1]: https://aws.amazon.com/jp/xray/?nc2=type_a
[^2]: https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-python-middleware.html
[^3]: https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-python-configuration.html
[^4]: https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-python-httpclients.html