📖

Kubernetes上で負荷試験基盤を作った話

2022/12/06に公開

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ネイティブなものに限定し、アーキテクチャやフローも簡素化することで、コストパフォーマンスの最適解に近いところを出せたのではないかと思っています。
今後、メンテナンスや負荷試験の実施を経てどのようなフィードバックが得られるのか、とても楽しみです。

Aidemy Tech Blog

Discussion