X-RayでAWS SDK呼び出しをトレースしてAWS Fargate とEC2 起動タイプのレイテンシを比較する

投稿内容は私個人の意見であり、所属企業・部門見解を代表するものではありません。

##目的

AWS Fargate とEC2 起動タイプでAWS SDK呼び出しに差があるのか計測したかったため、X-Ray SDK for Node.jsでレイテンシを比較したときのメモです。

##流れ


1. 事前準備
2. 計測用のサンプルプロジェクトの準備
2. ローカル環境でトレース実行
3. Dockerコンテナ化してローカル環境でトレース実行
4. EC2 起動タイプでトレース実行
5. AWS Fargate でトレース実行
6. 比較

##事前準備
###X-Rayのサンプリングレートを100％にする
商用環境だとすべてのSDK呼び出しを取得することはパフォーマンスが劣化する恐れがあるため推奨されないが、今回はテストなのでトレースを100%取得するように設定する。デフォルトは5%となる。
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-gettingstarted.html

###ECRでレポジトリを作成
Node.jsのサンプルアプリケーションとX-Rayデーモン用の２つのレポジトリを作成する。ここでは「my-nodejs」と「xray」という名称でレポジトリを作成した。

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html

###ECSクラスタ作成
AWS FargateとEC2タイプのクラスタをそれぞれ作成する。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create_cluster.html

EC2タイプは、セキュリティグループのインバウンドルールでサンプルアプリケーションが使用する3000ポートへのアクセスを許可する。
EC2タイプは、クラスタのEC2にパブリックIPが付与される。
ネットワークモードがawsvpcの場合は、EC2タイプ/Fargateを問わずサービスの定義でパブリックIPの付与を有効にすることができる。

##計測用のサンプルプロジェクトの準備
###Express、express-generatorをインストール

```
npm install -g express
npm install -g express-generator
```
X-rayは、Express フレームワークおよび Restify フレームワークをサポートしている。
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-nodejs-middleware.html
>X-Ray SDK for Node.js は Express フレームワークおよび Restify フレームワークを使用するアプリケーションのミドルウェアを提供します

###Expressプロジェクトを作成

```
express sampleapp
```
###依存モジュールのインストール

```
cd sampleapp && npm install
npm install aws-sdk
npm install aws-xray-sdk
```

###app.js修正
今回はSystemManagerのパラメータストアの値を取得するため、aws-sdkとaws-xray-sdkをインポートし、パラメータストアストア取得用のGetメソッドを宣言します。

```
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

//追加
var ssm_get = require('./routes/get');

var app = express();

//追加
var AWSXRay = require('aws-xray-sdk');
app.use(AWSXRay.express.openSegment('MyApp'));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

//追加
app.use('/get', ssm_get);
app.use(AWSXRay.express.closeSegment());

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
```

###./routes/get.jsを作成
local実行するため、アクセスキー、シークレットキーで認証していますが、EC2やECSで動作させる場合は、IAMロールからクレデンシャルを取得するほうがセキュリティレベルが高くなる。

```
var express = require('express');
var router = express.Router();

var AWSXRay = require('aws-xray-sdk');
var AWS = AWSXRay.captureAWS(require('aws-sdk'));
AWS.config.loadFromPath('./credentials.json');
AWS.config.update({region: 'ap-northeast-1'});
var ssm = AWSXRay.captureAWSClient(new AWS.SSM());

router.get('/', function(req, res) {
	var params = {
	    Name: '<パラメータストアのキー>', /* required */
	    WithDecryption: true || false
	};
	ssm.getParameter(params, function(err, data) {
		if (err) {
		    console.log(err, err.stack);
		}else{
		    console.log(data);
		    res.render('ssm_get', { message: "OK" });
		}
	    });
    });

module.exports = router;
```

###クレデンシャルを定義するファイル作成する(credentials.json)

```
{"accessKeyId": "<アクセスキー>", "secretAccessKey": "<シークレットアクセスキー>"}
```

###Jade作成(./views/ssm_get.jade) ※エラーが出力されるので適当に作成。。

```
extends layout

block content
  h1= title
  p #{title}
```

ここまででテスト対象となるアプリケーションが完成。

##ローカル環境でトレース実行
ここではMacOSでの手順を記載します
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-daemon-local.html

###X-Rayデーモンのダウンロードして解凍する。
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-daemon.html

```
cp ../../Downloads/aws-xray-daemon-macos-2.x.zip .

unzip aws-xray-daemon-macos-2.x.zip 
Archive:  aws-xray-daemon-macos-2.x.zip
  inflating: xray_mac                
  inflating: cfg.yaml   
```

###X-Rayデーモンの起動

```
./xray_mac -o -n ap-northeast-1 &
[1] 74741
8c8590130da5:sampleapp atsum$ 2018-11-12T16:06:02+09:00 [Info] Initializing AWS X-Ray daemon 2.1.3
2018-11-12T16:06:02+09:00 [Info] Using buffer memory limit of 81 MB
2018-11-12T16:06:02+09:00 [Info] 1296 segment buffers allocated
2018-11-12T16:06:02+09:00 [Info] Using region: ap-northeast-1
```

-o: ローカル実行
-n: リージョン指定

###サンプルアプリケーションの実行

```
node bin/www
```

###getしてみる
```
curl localhost:3000/get
```

###X-Rayのコンソールでトレースを確認する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/78184/abb8bb3b-91c0-1ba5-a71c-4c54ade8c87e.png)

##Dockerコンテナ化してローカル環境でトレース実行

###./routes/get.jsの編集

```

//コメントアウト
//AWS.config.loadFromPath('./credentials.json');                                              
//追加
AWS.config.loadFromPath('/src/credentials.json');
```

###Dockerfileを作成
先程作成したサンプルアプリケーションをDockerコンテナ上にコピーするようにDockerファイルを作成します。

```
FROM node

RUN cd /src; npm install express;npm install aws-sdk;npm install aws-xray-sdk

COPY . /src

EXPOSE  3000

CMD ["node", "/src/bin/www", "&"]
```

###ビルド実行
```
docker build -t my-nodejs .
```

###コンテナ起動
```
docker run  -it my-nodejs:latest /bin/bash
```
###getしてみる
```
node /src/bin/www &
curl localhost:49160/get
```

##EC2 起動タイプでトレース実行

###X-RayデーモンをECSで実行するためにDockerfileを作成する
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-daemon-ecs.html

```
FROM amazonlinux
RUN yum install -y unzip
RUN curl -o daemon.zip https://s3.dualstack.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-linux-2.x.zip
RUN unzip daemon.zip && cp xray /usr/bin/xray
ENTRYPOINT ["/usr/bin/xray", "-b", "0.0.0.0:2000"]
EXPOSE 2000/udp
```
###X-RayデーモンのコンテナイメージをBuildする
```
docker build -t xray .
```

###BuildしたDockerimageをECRにPush(サンプルアプリケーション)

```
$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker tag my-nodejs:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-nodejs:latest
docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-nodejs:latest
```
###BuildしたDockerimageをECRにPush(X-Rayデーモン)

```
$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker tag xray:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/xray:latest
docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/xray:latest
```
###タスク定義作成
ネットワークモード:host　#簡単に検証するためにhostモードを使用する
タスク実行ロール:SystemMangerのパラメータストアの値が取得できる権限が必要
コンテナの定義:サンプルアプリケーションとX-Rayデーモン用のコンテナを２つ登録する

タスク定義の例

```
{
  "executionRoleArn": "arn:aws:iam::<AWSアカウント>:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "dnsSearchDomains": null,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/p-test-node-ec2",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "entryPoint": null,
      "portMappings": [],
      "command": null,
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "ulimits": null,
      "dnsServers": null,
      "mountPoints": [],
      "workingDirectory": null,
      "dockerSecurityOptions": null,
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "image": "<AWSアカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/my-nodejs",
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": null,
      "hostname": null,
      "extraHosts": null,
      "pseudoTerminal": null,
      "user": null,
      "readonlyRootFilesystem": null,
      "dockerLabels": null,
      "systemControls": null,
      "privileged": null,
      "name": "my-amazonlinux"
    },
    {
      "dnsSearchDomains": null,
      "logConfiguration": null,
      "entryPoint": null,
      "portMappings": [],
      "command": null,
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "ulimits": null,
      "dnsServers": null,
      "mountPoints": [],
      "workingDirectory": null,
      "dockerSecurityOptions": null,
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "image": "<AWSアカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/xray",
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": null,
      "hostname": null,
      "extraHosts": null,
      "pseudoTerminal": null,
      "user": null,
      "readonlyRootFilesystem": null,
      "dockerLabels": null,
      "systemControls": null,
      "privileged": null,
      "name": "xray-deamon"
    }
  ],
  "placementConstraints": [],
  "memory": "2048",
  "taskRoleArn": "arn:aws:iam::<AWSアカウント>:role/ecsTaskExecutionRole",
  "compatibilities": [
    "EC2"
  ],
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:<AWSアカウント>:task-definition/p-test-node-ec2:8",
  "family": "p-test-node-ec2",
  "requiresAttributes": [
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.execution-role-ecr-pull"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.ecr-auth"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.task-iam-role"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.execution-role-awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.task-iam-role-network-host"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
    }
  ],
  "requiresCompatibilities": [
    "EC2"
  ],
  "networkMode": "host",
  "cpu": "1024",
  "revision": 8,
  "status": "ACTIVE",
  "volumes": []
}
```

###ECSサービス作成
EC2タイプのECSクラスタでサービスを定義する。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create-service.html
先程作成したタスク定義を指定する。
ELBオプション、サービスの検出なし。
Service Auto Scaling無効化
タスク(コンテナ)がRUNNINGになるまで待つ

##getしてみる

```
curl <EC2のパブリックIPアドレス>:3000/get
```

EC2のパブリックIPアドレスは、クラスタのEC2インスタンスタブにある対象のEC2インスタンスIDをクリックしてEC2のコンソールで確認する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/78184/5b2bc435-022d-9b40-dd69-28cf220288a3.png)

###X-Rayのコンソールでトレースを確認する

##AWS Fargate でトレース実行

###タスク定義作成
ネットワークモード:awsvpc
タスク実行ロール:SystemMangerのパラメータストアの値が取得できる権限が必要
コンテナの定義:サンプルアプリケーションとX-Rayデーモン用のコンテナを２つ登録する

テスク定義の例

```

{
  "executionRoleArn": "arn:aws:iam::<AWSアカウント>:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "dnsSearchDomains": null,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/p-test-node-fargate",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "entryPoint": null,
      "portMappings": [],
      "command": null,
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "ulimits": null,
      "dnsServers": null,
      "mountPoints": [],
      "workingDirectory": null,
      "dockerSecurityOptions": null,
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "image": "<AWSアカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/my-nodejs",
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": null,
      "hostname": null,
      "extraHosts": null,
      "pseudoTerminal": null,
      "user": null,
      "readonlyRootFilesystem": null,
      "dockerLabels": null,
      "systemControls": null,
      "privileged": null,
      "name": "my-amazonlinux"
    },
    {
      "dnsSearchDomains": null,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/p-test-node-fargate",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "entryPoint": null,
      "portMappings": [],
      "command": null,
      "linuxParameters": null,
      "cpu": 0,
      "environment": [],
      "ulimits": null,
      "dnsServers": null,
      "mountPoints": [],
      "workingDirectory": null,
      "dockerSecurityOptions": null,
      "memory": null,
      "memoryReservation": null,
      "volumesFrom": [],
      "image": "<AWSアカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/xray",
      "disableNetworking": null,
      "interactive": null,
      "healthCheck": null,
      "essential": true,
      "links": null,
      "hostname": null,
      "extraHosts": null,
      "pseudoTerminal": null,
      "user": null,
      "readonlyRootFilesystem": null,
      "dockerLabels": null,
      "systemControls": null,
      "privileged": null,
      "name": "xray-daemon"
    }
  ],
  "placementConstraints": [],
  "memory": "2048",
  "taskRoleArn": "arn:aws:iam::<AWSアカウント>:role/ecsTaskExecutionRole",
  "compatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:<AWSアカウント>:task-definition/p-test-node-fargate:5",
  "family": "p-test-node-fargate",
  "requiresAttributes": [
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.execution-role-ecr-pull"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.task-eni"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.ecr-auth"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.task-iam-role"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "ecs.capability.execution-role-awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
    },
    {
      "targetId": null,
      "targetType": null,
      "value": null,
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "networkMode": "awsvpc",
  "cpu": "1024",
  "revision": 5,
  "status": "ACTIVE",
  "volumes": []
}
```

###ECSサービス作成
FargateのECSクラスタでサービスを定義する。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create-service.html
先程作成したタスク定義を指定する。
セキュリティグループ:セキュリティグループのインバウンドルールでサンプルアプリケーションが使用する3000ポートへのアクセスを許可する
パブリック IP の自動割り当て:有効
ELBオプション、サービスの検出なし。
Service Auto Scaling無効化
タスク(コンテナ)がRUNNINGになるまで待つ

##getしてみる

```
curl <fargateのコンテナのパブリックIPアドレス>:3000/get
```

fargateのコンテナのパブリックIPアドレスは、クラスタのタスクタブのタスクをクリックして、Public IPを確認する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/78184/29810db7-aa23-cc5e-745c-f5abfaa42ccb.png)

##比較


###前提
EC2タイプ
インスタンスタイプ:m4.large
タスクサイズ:
タスクメモリ (MiB)2048
タスク CPU (単位):1024

Fargate
タスクサイズ:
タスクメモリ (MiB)2048
タスク CPU (単位)1024

###(雑な)比較結果
![image.png](https://qiita-image-store.s3.amazonaws.com/0/78184/4cfd5854-ad08-4105-19e4-94f064623dc4.png)
＿人人人人人人人人＿
＞　そんなに差はなし　＜
￣Y^Y^Y^Y^Y^Y^Y￣

##参考
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-nodejs-middleware.html
https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html
https://inokara.hateblo.jp/entry/2017/05/07/195717