The k6 Kubernetes Operator lets you run k6 load tests directly within your Kubernetes cluster, treating them as native Kubernetes Jobs.

Imagine you’re running a load test with k6, but you want it to be managed by Kubernetes. You don’t want to set up your own k6 runners, manage their scaling, or worry about their lifecycle. That’s where the k6 Kubernetes Operator comes in. It acts as a controller that watches for custom resources you define, specifically k6 resources. When it sees one, it spins up the necessary Kubernetes components—like Pods and Jobs—to execute your k6 script.

Let’s see it in action. First, you need to install the operator itself. This is typically done by applying a YAML manifest:

apiVersion: v1
kind: Namespace
metadata:
  name: k6
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k6-controller-manager
  namespace: k6
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: k6-metrics-reader
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: k6-system-leader-election-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: k6-metrics-reader
subjects:
  - kind: ServiceAccount
    name: k6-controller-manager
    namespace: k6
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k6-controller-manager
  namespace: k6
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k6-controller-manager
  template:
    metadata:
      labels:
        app: k6-controller-manager
    spec:
      serviceAccountName: k6-controller-manager
      containers:
        - name: manager
          image: grafana/k6-operator:v0.3.0 # Use the latest stable version
          command:
            - /manager
          args:
            - --leader-election-id=k6-operator-leader-election
            - --metrics-bind-address=127.0.0.1:8080
            - --health-probe-bind-address=127.0.0.1:9090
          ports:
            - containerPort: 9090
              name: health
            - containerPort: 8080
              name: metrics
          resources:
            limits:
              cpu: 500m
              memory: 128Mi
            requests:
              cpu: 100m
              memory: 64Mi

Once the operator is running in your k6 namespace, you can define a k6 custom resource to run a test. Here’s an example that runs a simple k6 script against http://your-application.example.com:

apiVersion: k6.io/v1
kind: k6
metadata:
  name: my-first-test
spec:
  # The k6 script to run. Can be inline or a reference to a ConfigMap.
  script:
    inline: |
      import http from 'k6/http';
      import { sleep } from 'k6';

      export const options = {
        vus: 10,
        duration: '30s',
      };

      export default function () {
        http.get('http://your-application.example.com');
        sleep(1);
      }
  # Optional: specify test runner image
  runnerImage: "grafana/k6:v0.45.0" # Use a specific k6 version
  # Optional: specify where to send results
  # reporter:
  #    memungkinkan:
  #     - type: prometheus
  #       url: "http://prometheus-k8s.monitoring.svc.cluster.local:9090/metrics"

When you apply this manifest (kubectl apply -f my-test.yaml), the k6 operator will:

  1. Create a Kubernetes Job: This Job will have a Pod that runs the k6 test.
  2. Mount the k6 script: The script.inline content is typically packaged into a ConfigMap and then mounted into the k6 runner Pod. If you used script.configMap or script.volume, it would mount those instead.
  3. Configure k6: The operator sets up the k6 runner with the specified options (like vus and duration) and ensures it uses the correct k6 version (runnerImage).
  4. Monitor the Job: It watches the status of the k6 Job. When the Job completes successfully, the k6 custom resource status will be updated. If it fails, the status will reflect the error.

The beauty of this approach is that your load tests become first-class citizens in Kubernetes. You can manage them with kubectl, integrate them into CI/CD pipelines, and leverage Kubernetes’ built-in features like resource limits, network policies, and secrets.

The operator also supports more advanced scenarios. For example, you can distribute the load across multiple k6 instances by setting parallelism in the Job template, or have k6 write results to external systems like Prometheus or Loki via the reporter configuration.

One detail that often trips people up is how k6 scripts are loaded. While inline is convenient for simple tests, for larger or more complex scripts, you’ll want to use script.configMap or script.volume. The operator will then create or reference the necessary Kubernetes ConfigMap or Volume to make your script available to the k6 Pod. This keeps your k6 resource definition clean and separates the test logic from its execution configuration.

If you configure a reporter, the operator will ensure that the k6 runner Pod is set up to send metrics or logs to your specified endpoint. For instance, a prometheus reporter will configure k6 to expose its internal metrics on a port that can be scraped by Prometheus.

After a test completes, you can inspect its status using kubectl get k6 my-first-test -o yaml. You’ll see information about whether the test passed or failed, and potentially links to logs or other relevant resources.

The next step in leveraging k6 within Kubernetes is exploring distributed load testing with the parallelism field and integrating test execution into your GitOps workflows.

Want structured learning?

Take the full K6 course →