K6 Operatorを試す

[k6](https://k6.io/)とはOSSの負荷テストツールで、[2016年頃から開発されている](https://k6.io/about/)、そこそこ歴史のある負荷ツールである。
現在はGrafana Labsの下で継続して開発が行われている。GitHubは[こちら](https://github.com/grafana/k6)。
これにはOperator版も[用意されている](https://github.com/grafana/k6-operator)ので、これを使ってPodに負荷をかけた時のメモ。
[Running distributed k6 tests on Kubernetes](https://k6.io/blog/running-distributed-tests-on-k8s/)を参考にしている。
## デプロイ
Operatorのデプロイはコードをcloneしてmakeする形で行う。コンテキストを切り替えた状態で以下を実行する。
```sh
git clone https://github.com/grafana/k6-operator && cd k6-operator
make deploy
```
デプロイに成功すると、`k6-operator-system`というNamespaceにPodが作成される。
```sh
$ kubectl get pod -n k6-operator-system
NAME                                              READY   STATUS    RESTARTS   AGE
k6-operator-controller-manager-54488d95cd-gxd7b   2/2     Running   0          27s
```
なおMac環境でkustomizeコマンドが入っていないと以下のようなエラーになる。
```
cd config/manager && /Users/hogehoge/go/bin/kustomize edit set image controller=ghcr.io/grafana/k6-operator:latest
/bin/sh: line 1: 10199 Killed: 9               /Users/hogehoge/go/bin/kustomize edit set image controller=ghcr.io/grafana/k6-operator:latest
make: *** [deploy] Error 137
```
この場合は`brew install kustomize`を実行してkustomizeを入れればOKなはず。

## 検証
動作確認をする。
テストコードを作成する。
```js
cat << EOF > test.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { target: 200, duration: '30s' },
    { target: 0, duration: '30s' },
  ],
};

export default function () {
  const result = http.get('https://test-api.k6.io/public/crocodiles/');
  check(result, {
    'http response status code is 200': result.status === 200,
  });
}
EOF
```
ConfigMapとして作成する。
```sh
kubectl create ns k6-test
kubectl create configmap crocodile-stress-test --from-file ./test.js -n k6-test
```
k6のカスタムリソースを作成する。
※古いドキュメントだと`kind: K6`となっているが、`kind: TestRun`に変更された。
```yaml
cat << EOF > ./k6-test.yaml
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
  name: k6-sample
spec:
  parallelism: 4
  script:
    configMap:
      name: crocodile-stress-test
      file: test.js
EOF
```
applyするとテストが実行される。
```sh
kubectl apply -f ./k6-test.yaml -n k6-test
```
testrunリソースを見ると、`STAGE`が`started`になっている。
```sh
$ kubectl get testrun -n k6-test
NAME        STAGE     AGE   TESTRUNID
k6-sample   started   28s
```
Podは以下のようになっていて、`parallelism`で4並列で動かすよう指定していたため、テスト本体は4つ同時に動いている。
```sh
$ kubectl get pod -n k6-test
NAME                          READY   STATUS      RESTARTS   AGE
k6-sample-1-gf698             1/1     Running     0          41s
k6-sample-2-n2rbl             1/1     Running     0          41s
k6-sample-3-tbk6f             1/1     Running     0          41s
k6-sample-4-v657j             1/1     Running     0          41s
k6-sample-initializer-7cxff   0/1     Completed   0          50s
k6-sample-starter-ftdzh       0/1     Completed   0          34s
```
ちなみに各Podの`command`は以下のような感じ。
```sh:initializer
    - mkdir -p $(dirname /tmp/test.js.archived.tar) && k6 archive /test/test.js -O
      /tmp/test.js.archived.tar  2> /tmp/k6logs && k6 inspect --execution-requirements
      /tmp/test.js.archived.tar 2> /tmp/k6logs ; ! cat /tmp/k6logs | grep 'level=error'
```
```sh:starter
    - 'curl --retry 3 -X PATCH -H ''Content-Type: application/json'' http://10.98.39.51:6565/v1/status
      -d ''{"data":{"attributes":{"paused":false,"stopped":false},"id":"default","type":"status"}}'''
```
```sh: 本体
    - k6
    - run
    - --quiet
    - /test/test.js
    - --address=0.0.0.0:6565
    - --paused
    - --tag
    - instance_id=1
    - --tag
    - job_name=k6-sample-1
```
initializerでテストコードのアーカイブ化などを行い、starterで起動したテスト用Podが`paused`なのを解除して一斉にテストを実行している感じだと思われる。

テスト終了後は`STAGE`が`finished`になる。
```sh
$ kubectl get testrun -n k6-test
NAME        STAGE      AGE   TESTRUNID
k6-sample   finished   89s
```
ログを見るとテストの結果を確認することが出来る。
```sh
$ kubectl logs -n k6-test k6-sample-1-gf698

     ✓ http response status code is 200

     checks.........................: 100.00% ✓ 3557      ✗ 0
     data_received..................: 3.3 MB  56 kB/s
     data_sent......................: 450 kB  7.5 kB/s
     http_req_blocked...............: avg=4.01ms   min=1.8µs    med=5.56µs   max=270.01ms p(90)=6.9µs    p(95)=8.8µs
     http_req_connecting............: avg=1.92ms   min=0s       med=0s       max=120.45ms p(90)=0s       p(95)=0s
     http_req_duration..............: avg=432.43ms min=103.84ms med=124.67ms max=2.93s    p(90)=1.59s    p(95)=2.09s
       { expected_response:true }...: avg=432.43ms min=103.84ms med=124.67ms max=2.93s    p(90)=1.59s    p(95)=2.09s
     http_req_failed................: 0.00%   ✓ 0         ✗ 3557
     http_req_receiving.............: avg=125.08µs min=23.3µs   med=120.21µs max=3.12ms   p(90)=157.28µs p(95)=182.03µs
     http_req_sending...............: avg=35.59µs  min=7.79µs   med=28.69µs  max=735.26µs p(90)=51.94µs  p(95)=58.07µs
     http_req_tls_handshaking.......: avg=2.05ms   min=0s       med=0s       max=126.54ms p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=432.27ms min=103.71ms med=124.54ms max=2.93s    p(90)=1.59s    p(95)=2.08s
     http_reqs......................: 3557    59.281034/s
     iteration_duration.............: avg=436.66ms min=104.08ms med=125.48ms max=2.93s    p(90)=1.59s    p(95)=2.09s
     iterations.....................: 3557    59.281034/s
     vus............................: 2       min=0       max=50
     vus_max........................: 50      min=50      max=50
```

非常に簡単にテストを動かすことが出来た。

## 注意点
Istioを使って自動的にサイドカーを挿入している場合、以下のような感じで初期化用Podが終わらない。
```sh
$ kubectl get pod -n k6-test
NAME                          READY   STATUS     RESTARTS   AGE
k6-sample-initializer-9q5qd   1/2     NotReady   0          5m56s
```
これはk9sなんかで中を見ると理由が分かる。
```sh
┌─────────────────────────────────────────────────────────────────── Containers(k6-test/k6-sample-initializer-9q5qd)[3] ────────────────────────────────────────────────────────────────────┐
│ NAME↑             PF IMAGE                                                                        READY  STATE      INIT   RESTARTS PROBES(L:R) CPU MEM  CPU/R:L  MEM/R:L %CPU/R %CPU/L R │
│ istio-proxy       ●  public.ecr.aws/v6x6b8s5/vmwareallspark/proxyv2:1.17.3-release-v1-distroless  true   Running    false         0 off:on       7↑  68 100:2000 128:1024     7↑      0 3 │
│ istio-validation  ●  public.ecr.aws/v6x6b8s5/vmwareallspark/proxyv2:1.17.3-release-v1-distroless  true   Completed  true          0 off:off       0   0 100:2000 128:1024      0      0 0 │
│ k6                ●  ghcr.io/grafana/k6-operator:latest-runner                                    false  Completed  false         0 off:off       0   0      0:0      0:0    n/a    n/a a
```
Jobが終わっているにも関わらず、istio-proxyが動き続けるため、Jobが完了出来ずにいる状態となっている。
これはKubernetes v1.28以降の`SidecarContainers`を使えば対応できると思われる。（参考記事：[Kubernetes v1.28: Introducing native sidecar containers](https://kubernetes.io/blog/2023/08/25/native-sidecar-containers/)）
ただ、とりあえず手っ取り早く動かしたい人は、Istio環境ではOperatorの利用を諦めて、以下のようなPodを作成するのが早いと思う。
```yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: k6
  name: k6-sample
spec:
  containers:
  - command:
    - k6
    - run
    - /test/test.js
    image: ghcr.io/grafana/k6
    name: k6
    ports:
    volumeMounts:
    - mountPath: /test
      name: k6-test-volume
  volumes:
  - configMap:
      name: k6-test
    name: k6-test-volume
```