JMeterをFargate for EKSで動かしてみる

この記事は、[ゆるWeb勉強会@札幌 Advent Calendar 2019の20日目（2019-12-20）](https://adventar.org/calendars/4273)の記事です。  

##はじめに
この記事ではEKSクラスタ上にmaster-slave構成のJMeterを構築し、
ある程度大量のリクエストが可能な負荷試験環境を作成することをゴールとしています。
※少量の負荷であれば１台のJMeterもしくは、ApacheBenchや[vegeta](https://github.com/chidakiyo/vegeta)（READMEが好き）などで十分

当初、EKSのnodegroup上にJMeterクラスタを構築し高負荷を与える検証環境を作る予定でしたが、
Re:InventでFargate for EKSが発表されたのでそちらに切り替えて作成していきます。
##### Fargate for EKS  
従来はECSのみで利用可能だったFargateがEKSでも使えるようになり、
EC2ワーカーノードの管理を必要とせずまたKubernetersの専門的な知識を必要とせず
クラスターを稼働させることが出来るとても素晴らしいアップデートと思います。

詳しくは、
https://aws.amazon.com/jp/blogs/news/amazon-eks-on-aws-fargate-now-generally-available/
https://dev.classmethod.jp/cloud/aws/try-amazon-fargate-for-amazon-eks/
などで紹介されているのでここでは特に言及はしません。

##### Fargate for EKS料金
気になる料金についてはpodを実行するために必要なvCPU,メモリリソースを支払うFargateの料金（[Fargateの料金プラン](https://aws.amazon.com/jp/fargate/pricing/?nc=sn&loc=2)）に加えEKSクラスタの利用料金が発生します。
負荷環境としては必要なときだけ立ち上げて利用すればいいのでコスト面はそれほど気にせず利用できます。

##ソースコード
今回検証で利用したソースコードはここに置いてあります
https://github.com/hirokawai/load-test

この記事では一部説明するために長ったらしく書いてますが、
実行コマンドはスクリプトにまとめてあり、実行手順はREADMEに書いてあるのでその通りに実行すれば稼働します。

##EKSクラスタの作成

実行環境のツールバージョンは以下です

```shell
% aws --version
aws-cli/1.16.290 Python/3.7.5 Darwin/18.2.0 botocore/1.13.26
% eksctl version
[ℹ]  version.Info{BuiltAt:"", GitCommit:"", GitTag:"0.11.1"}
% kubectl version --client --short
Client Version: v1.16.3
```
#### 環境作成と実行の流れ
1. ECRイメージの作成
2. EKSクラスタの作成
3. fargateprofileの作成
4. JMeterクラスタの作成
5. 負荷テスト
6. 集計データの取得
7. 後片付け・削除

といった流れで実行していきます

##ECRイメージの作成

slaveとmasterのDockerfileを用意しbuildします。
基本的にはalpine上にJMeterをインストールしserverモードで起動できるようにセットアップしていくだけです。
コンテナのimageの取得先としてECRにimageを登録していきます。

##### docker imageの作成
・jmeter-slave:latest 
・jmeter-master:latest
というタグがついたimageを作成します

```shell
% docker build --tag="jmeter-slave:latest" -f Dockerfile_slave .
% docker build --tag="jmeter-master:latest" -f Dockerfile_master .
```

Dockerfile_master

masterは必要なアプリケーションをインストールし、
後ほどdeploymentで定義しますが起動しっぱなしする想定で作成します。

```dockerfile
FROM alpine:3.9

ARG JMETER_VERSION="5.1.1"
ARG PLUGIN_VERSION="1.4.0"

ENV JMETER_HOME /opt/apache-jmeter-${JMETER_VERSION}
ENV JMETER_BIN  ${JMETER_HOME}/bin
ENV JMETER_DOWNLOAD_URL  https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz
ENV PLUGIN_DOWNLOAD_URL  https://jmeter-plugins.org/downloads/file/JMeterPlugins-Standard-${PLUGIN_VERSION}.zip

# Install extra packages
ARG TZ="Asia/Tokyo"
RUN    apk update \
    && apk upgrade \
    && apk add ca-certificates \
    && update-ca-certificates \
    && apk add --update openjdk8-jre tzdata curl unzip bash \
    && apk add --no-cache nss \
    && apk add python \
    && rm -rf /var/cache/apk/* \
    && mkdir -p /tmp/dependencies  \
    && curl -L --silent ${JMETER_DOWNLOAD_URL} >  /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz  \
    && curl -L --silent ${PLUGIN_DOWNLOAD_URL} >  /tmp/dependencies/JMeterPlugins-${PLUGIN_VERSION}.zip  \
    && mkdir -p /opt  \
    && tar -xzf /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz -C /opt  \
    && unzip -oq "/tmp/dependencies/JMeterPlugins-${PLUGIN_VERSION}.zip" -d $JMETER_HOME  \
    && rm -rf /tmp/dependencies

ENV PATH $PATH:$JMETER_BIN

WORKDIR ${JMETER_HOME}

#==============
# Expose Ports
#==============
EXPOSE 60000
```

Dockerfile_slave 

slaveは必要なアプリケーションをインストールし、
container_start_slave.shでJMeterを起動します

```dockerfile
FROM alpine:3.9

ARG JMETER_VERSION="5.1.1"
ARG PLUGIN_VERSION="1.4.0"

ENV JMETER_HOME /opt/apache-jmeter-${JMETER_VERSION}
ENV JMETER_BIN  ${JMETER_HOME}/bin
ENV JMETER_DOWNLOAD_URL  https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz
ENV PLUGIN_DOWNLOAD_URL  https://jmeter-plugins.org/downloads/file/JMeterPlugins-Standard-${PLUGIN_VERSION}.zip

# Install extra packages
ARG TZ="Asia/Tokyo"
RUN    apk update \
    && apk upgrade \
    && apk add ca-certificates \
    && update-ca-certificates \
    && apk add --update openjdk8-jre tzdata curl unzip bash \
    && apk add --no-cache nss \
    && apk add python \
    && rm -rf /var/cache/apk/* \
    && mkdir -p /tmp/dependencies  \
    && curl -L --silent ${JMETER_DOWNLOAD_URL} >  /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz  \
    && curl -L --silent ${PLUGIN_DOWNLOAD_URL} >  /tmp/dependencies/JMeterPlugins-${PLUGIN_VERSION}.zip  \
    && mkdir -p /opt  \
    && tar -xzf /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz -C /opt  \
    && unzip -oq "/tmp/dependencies/JMeterPlugins-${PLUGIN_VERSION}.zip" -d $JMETER_HOME  \
    && rm -rf /tmp/dependencies

ENV PATH $PATH:$JMETER_BIN

WORKDIR ${JMETER_HOME}

ADD container_start_slave.sh /usr/local/bin
RUN chmod +x /usr/local/bin/container_start_slave.sh

#==============
# Expose Ports
#==============
EXPOSE 1099 50000

ENTRYPOINT []
CMD ["container_start_slave.sh"]
```

##### ECRレポジトリを作成
```shell
% aws ecr create-repository --repository-name jmeter-slave
% aws ecr create-repository --repository-name jmeter-master
```

##### ECRにpush
```shell
ECRログイン
% $(aws ecr get-login --no-include-email --region ap-northeast-1)

master
% docker tag jmeter-master:latest xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-master:latest
% docker push xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-master:latest

slave
% docker tag jmeter-slave:latest xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-slave:latest
% docker push xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-slave:latest
```

### EKSクラスタの作成

ポイントは「--fargate」を指定している部分です。
このオプションによりFargate profileが作成され、kube-system namespaceに作成されるPODがFargate上で実行可能になります。

```shell
--fargate Create a Fargate profile scheduling pods in the default and kube-system namespaces onto Fargate
```


```shell
% eksctl create cluster --name jmeter-load-test --region ap-northeast-1--version 1.14 --vpc-private-subnets=subnet-aaa,subnet-bbb --vpc-public-subnets=subnet-ccc,subnet-ddd --node-private-networking --fargate --without-nodegroup
```
※ 負荷を与える対象がVPCに存在しVPC内で実行したい背景があったので、subnet指定等しています

### fargateprofileの作成

今回namespace「jmeter」を作成し実行したいのでfargateprofileを作成します。
この設定によりPod selectorの指定に合致したPodがFargate上で実行できます。

```shell
% eksctl create fargateprofile --name fp-jmeter-load-test --namespace jmeter --cluster jmeter-load-test
```

### JMeterクラスタの作成

・namespaceとしてjmeter
・Deploymentとしてjmeter-master,jmeter-salves
・Serviceとしてjmeter-slaves-svc
・ConfigMap(masterの実行スクリプトを定義しているだけ） jmeter-load-test
を作成します。

詳細はmanifestファイルをGithubにあげているので割愛しますが
リモートクライアントとして1099ポートで待受を行い、
master-slave間はPODのIPを取得しRMI通信を50000ポートで行うようになっています。

```shell
% kubectl create namespace jmeter
% kubectl create -n jmeter -f jmeter_slaves_deploy.yaml
% kubectl create -n jmeter -f jmeter_slaves_svc.yaml
% kubectl create -n jmeter -f jmeter_master_configmap.yaml
% kubectl create -n jmeter -f jmeter_master_deploy.yaml
```

コンテナリソース設定が必要であればDeploymentに適当に設定します

```
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
```


クラスタの状態を確認するとこのような状態になっています

```shell
% kubectl get all -o wide -n jmeter                                                                                                                                                                    
NAME                     READY   STATUS    RESTARTS   AGE    IP              NODE                                                       NOMINATED NODE   READINESS GATES
pod/jmeter-master-1111   1/1     Running   0          175m   172.20.17.40    fargate-ip-172-20-17-40.ap-northeast-1.compute.internal    <none>           <none>
pod/jmeter-slaves-2222   1/1     Running   0          175m   172.20.21.151   fargate-ip-172-20-21-151.ap-northeast-1.compute.internal   <none>           <none>

NAME                        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)              AGE    SELECTOR
service/jmeter-slaves-svc   ClusterIP   None         <none>        1099/TCP,50000/TCP   175m   jmeter_mode=slave

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS   IMAGES                                                                   SELECTOR
deployment.apps/jmeter-master   1/1     1            1           175m   jmmaster     xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-master:latest   jmeter_mode=master
deployment.apps/jmeter-slaves   1/1     1            1           175m   jmslave      xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-slave:latest    jmeter_mode=slave

NAME                                       DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES                                                                   SELECTOR
replicaset.apps/jmeter-master-665cc5d67f   1         1         1       175m   jmmaster     xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-master:latest   jmeter_mode=master,pod-template-hash=xxxx
replicaset.apps/jmeter-slaves-6ff95f6cbd   1         1         1       175m   jmslave      xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/jmeter-slave:latest    jmeter_mode=slave,pod-template-hash=xxxx
```

### 負荷テスト

slaveの台数はreplica数で増減可能です。負荷の規模に応じて調整します。

```shell
kubectl -n jmeter scale deployment/jmeter-slaves --replicas=5
```

-f で負荷シナリオファイル
-G でシナリオに展開するプロパティ＆値を設定

シナリオファイルはGUIで作成しExportしました。
この例ではユーザ定義変数がある想定で外から渡せるようにしてあります。

```shell
./container_start_test.sh -f/load-test.jmx -GTARGET_HOST=xxxx -GTHREAD=1 -GRAMPUP=10 -GLOOP=10
```

実行するとログが出力されていきます

```shell
Creating summariser <summary>
Created the tree successfully using /load-test.jmx
Configuring remote engine: 172.20.16.202
Configuring remote engine: 172.20.16.229
Configuring remote engine: 172.20.16.68
Configuring remote engine: 172.20.17.60
Configuring remote engine: 172.20.21.151
Starting remote engines
Starting the test @ Tue Dec 10 08:49:52 GMT 2019 (1575967792047)
summary +      1 in 00:00:01 =    0.8/s Avg:   933 Min:   933 Max:   933 Err:     0 (0.00%) Active: 1 Started: 0 Finished: 0
Remote engines have been started
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
Tidying up remote @ Tue Dec 10 08:50:05 GMT 2019 (1575967805280)
Tidying up remote @ Tue Dec 10 08:50:09 GMT 2019 (1575967809218)
Tidying up remote @ Tue Dec 10 08:50:09 GMT 2019 (1575967809975)
... end of run
```

### 集計データの取得

stdoutにログが流れていくのでそれで十分だったりもしますが、
jmeterの集計結果はmasterの/report/result.jtlに作成されるようにしてあります。

以下の様にファイルを取得可能です。
ベストプラクティス的にはログは外部に出し、
ステートを持たない方が良いのですが小軽く実行できるようにしています。

```shell
master_pod=`kubectl get po -n jmeter | grep jmeter-master | awk '{print $1}'`
kubectl cp -n jmeter "$master_pod:/report" ./report
```

### 後片付け・削除

負荷試験がしたくなったらまた構築すればいいので、
検証が終わったら作成したリソースを削除し料金が発生しないようにしておきます

```shell
kubernetesリソースを削除
% kubectl delete -n jmeter -f jmeter_slaves_deploy.yaml
% kubectl delete -n jmeter -f jmeter_slaves_svc.yaml
% kubectl delete -n jmeter -f jmeter_master_configmap.yaml
% kubectl delete -n jmeter -f jmeter_master_deploy.yaml
% kubectl delete namespace jmeter

eksクラスタを削除
% eksctl delete cluster --name jmeter-load-test
[ℹ]  eksctl version 0.11.1
[ℹ]  using region ap-northeast-1
[ℹ]  deleting EKS cluster "tmp-load-test"
[ℹ]  deleting Fargate profile "fp-default"
[ℹ]  deleted Fargate profile "fp-jmeter-load-test"
[ℹ]  deleted 3 Fargate profile(s)
[✔]  kubeconfig has been updated
[ℹ]  cleaning up LoadBalancer services
[ℹ]  1 task: { delete cluster control plane "jmeter-load-test" [async] }
[ℹ]  will delete stack "eksctl-jmeter-load-test-cluster"
[✔]  all cluster resources were deleted

ECRの削除
```

##まとめ
以上の手順でとあるサービスに秒間数千リクエストの負荷を与えられる環境が出来ました。
Fargate for EKSのおかげでワーカーノードを全く意識せずにJMeterのクラスタ環境を構築出来ています。
アプリケーションに集中できるマネージドサービスの恩恵をがっちり享受出来た気がします。

こういったツールが手軽に実行できるのもコンテナの魅力の一つですね。
今後は運用サービスでも利用し事例を作っていければと思います。