Kubernetes上で負荷試験基盤を作った話
2022年Aidemy社のアドベントカレンダー7日目の記事です。
はじめに
おはこんばんにちは、へたれです。
Aidemy社でModeloy Engineeringという伴走型のDX内製化サービスを提供するチームに所属しています。
前職では主にKubernetesを主戦場としていたので、そのスキルを活かしてAidemy Businessで負荷試験基盤を作ったお話です。
概要
Aidemy BusinessはBtoBのDX人材育成サービスで、有難いことに年々利用者が増加しています。
特に新入社員が増え研修も行われる4月には上昇幅が大きく、サービスが耐えられるかを確認するために負荷試験を実施する必要がありました。
アプリケーションエンジニアの方でも試験を自分で組み簡単に実施したい一方で、実行頻度は高くないため開発・運用コストを最小にしたいという要件がありました。
技術スタック
負荷試験ツール
Locustを選定しました。
理由としては実行クラスタを組めるため負荷をスケールさせやすい、Pythonで実装でき比較的学習コストが低くアプリケーションエンジニアでも書きやすい、と考えたからです。
実行基盤
Aidemy BusinessはGoogle Kubernetes Engine(以下GKE)上で稼働しており、その基盤上にLocustのクラスタを構築することにしました。
GKEは任意の数のPodを容易に作成・削除することが可能で、負荷試験をオンデマンドで実行する基盤としてとても向いています。
またSecretsなどの機微情報を保存する機能を有効活用することで、Kubernetesの枠組みの中でセキュリティを担保することができるのも魅力的です。
しかし操作にはkubectl
が必要で、クラスタの構築や削除などをアプリケーション開発者にとってはCLIを通しての実行が難しいという壁がありました。
設計
kubectl
を通しての実行は要求するKubernetesの知識が多く、ハードルが高いという話をしました。
これらの壁を乗り越えるために、以下の設計方針を立て、構築・運用・実行のコストを最小化しました。
- なるべく
kubectl
を使わずにGKEコンソールで完結 - 実行スクリプトの切り替えや台数変更を容易に (
kubectl apply
しなくていい)
そこで最終的なアーキテクチャは以下のようになりました。
実行スクリプトや台数変更
機微情報をSecretsに、実行台数や実行スクリプトの指定などをConfigMapに格納することで、頻繁なコンテナイメージの作り直しを避けました。
- 機微情報: locust-secrets
- 実行台数、実行スクリプトの指定: locust-properties
- Locustスクリプト: locust-scripts
locust-properties.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: locust-properties
namespace: loadtest
data:
# パラメタ
WORKER_NUM: '3'
LOCUST_SCRIPT: 'script.py'
# マニフェストのテンプレート (loadtest-setupで作成)
LOCUST_MANIFEST: |
apiVersion: v1
kind: Pod
metadata:
labels:
app: locust-master
name: locust-master
namespace: loadtest
spec:
containers:
- image: asia.gcr.io/<プロジェクト名>/locust
name: locust-master
command: ["locust", "-f", "/var/loadtest/${LOCUST_SCRIPT}", "--master"]
volumeMounts:
- name: locust-scripts
mountPath: "/var/loadtest"
readOnly: true
----
apiVersion: v1
kind: Service
metadata:
name: locust-master
namespace: loadtest
spec:
selector:
app: locust-master
ports:
- name: master-and-worker-1
protocol: TCP
port: 5557
targetPort: 5557
- name: master-and-worker-2
protocol: TCP
port: 5558
targetPort: 5558
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: locust-worker
name: locust-worker
namespace: loadtest
spec:
# ワーカーPodの台数
replicas: ${WORKER_NUM}
selector:
matchLabels:
app: locust-worker
template:
metadata:
labels:
app: locust-worker
spec:
containers:
- image: asia.gcr.io/<プロジェクト名>/locust
name: locust
envFrom:
- secretRef:
name: locust-secrets
command: ["locust", "-f", "/var/loadtest/${LOCUST_SCRIPT}", "--worker", "--master-host=locust-master.loadtest.svc.cluster.local"]
ports:
- containerPort: 8089
- containerPort: 5557
- containerPort: 5558
volumeMounts:
- name: locust-scripts
mountPath: "/var/loadtest"
volumes:
- name: locust-scripts
configMap:
name: locust-scripts
またConfigMapに直接埋め込んでしまうと負荷試験スクリプトの実装中にエディタがシンタックスハイライトをしてくれないので、特定のディレクトリ配下のスクリプトファイルをConfigMapに登録してくれるシェルスクリプトupdate-scripts.sh
を組みました。
update-scripts.sh
#!/bin/sh
scripts_dir="scripts"
configmap_name="locust-scripts"
namespace="loadtest"
dry_run=${1:-""}
# check to locust-scripts is existed?
kubectl get configmap -n $namespace $configmap_name
if [ $? -ne 0 ]; then
echo "$configmap_name is not presented in $namespace!"
kubectl create configmap -n $namespace $configmap_name
if [ $? -ne 0 ]; then
exit $?
fi
fi
for script in `ls $scripts_dir`; do
args="$args --from-file=$script=$scripts_dir/$script"
done
kubectl create configmap -n $namespace $configmap_name $args -o yaml --dry-run=client | kubectl replace -f - $dry_run
exit $?
Locustクラスタの作成や削除
またCronJobはGKEのコンソール上からワンクリックでショット実行できるため、Locustクラスタの作成と削除を担うことでkubectlでの操作を行わずに誰でも実行することが可能となっています。
(勝手に実行されないように.spec.suspend=trueの設定をしています)
loadtest-setup.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: loadtest-setup
labels:
app: loadtest-setup
namespace: loadtest
spec:
schedule: "00 00 01 01 *"
suspend: true
jobTemplate:
spec:
template:
metadata:
labels:
app: loadtest-setup
spec:
serviceAccountName: loadtest-sa
containers:
- name: loadtest-setup
image: asia.gcr.io/<プロジェクト名>/locust-deployer
command: ["/bin/sh", "-c"]
args:
- 'echo "$LOCUST_MANIFEST" | envsubst | kubectl apply -f -'
- "&& kubectl wait pod/locust-master --for condition=Ready --timeout 300s"
- "&& kubectl wait deployment/locust-worker --for condition=Ready --timeout 300s"
envFrom:
- configMapRef:
name: locust-properties
loadtest-cleanup.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: loadtest-cleanup
labels:
app: loadtest-cleanup
namespace: loadtest
spec:
schedule: "30 00 01 01 *"
suspend: true
jobTemplate:
spec:
template:
metadata:
labels:
app: loadtest-cleanup
spec:
serviceAccountName: loadtest-sa
containers:
- name: loadtest-cleanup
image: asia.gcr.io/<プロジェクト名>/locust-deployer
command: ["/bin/sh", "-c"]
args: ['echo "$LOCUST_MANIFEST" | envsubst | kubectl delete --wait=true -f -']
envFrom:
- configMapRef:
name: locust-properties
実行手順
実装したLocustスクリプトのアップロード
まず最初に実装したLocustのスクリプトをlocust-scripts
アップロードします。
./update-scripts.sh
台数や実行スクリプトの指定
GKEのコンソール画面からyamlを開き、locust-properties
の中身を書き換えます。
Locustクラスタの作成
GKEのコンソール画面からlocust-setup
を単発で実行します。
ポートフォワード
locust-master
のダッシュボードにアクセスするためにポートフォワードします。
$ gcloud container clusters get-credentials <クラスタ名> --region asia-northeast1 --project <プロジェクト名> && kubectl port-forward -n loadtest service/locust-admin 8080:8089
Fetching cluster endpoint and auth data.
kubeconfig entry generated for aidemy.
Forwarding from 127.0.0.1:8080 -> 8089
Forwarding from [::1]:8080 -> 8089
ダッシュボードから負荷がけ
ダッシュボードを開き、アクセス先を指定して負荷がけをします。
実行結果をダウンロード
ダッシュボードから実行結果のHTMLをダウンロードします。
GoogleDriveなどに保存したり、スクリーンショットをドキュメントに貼ったりして試験結果レポートを作成し、改善をしていきましょう!
おわりに
以上、GKE上での負荷試験基盤を作成した話でした。
完全にゼロにはできませんでしたが、GKEのコンソールや機能をフルで活用し、なるべくCLIからの操作を最低限にしつつ、簡単に負荷試験を実施できる基盤を作ることができました。
またConfigMapで実行スクリプトやクラスタ台数を容易に変更できるようにしたことで、条件を変えて実行と計測のイテレーションを高速に回すことができたのも良かったです。
過度に自動化せず、リソースもKubernetesネイティブなものに限定し、アーキテクチャやフローも簡素化することで、コストパフォーマンスの最適解に近いところを出せたのではないかと思っています。
今後、メンテナンスや負荷試験の実施を経てどのようなフィードバックが得られるのか、とても楽しみです。
Discussion