From 8451333c3ac83005ebfe7a80caf4ad539fbd833a Mon Sep 17 00:00:00 2001 From: Adam Carr Date: Sat, 19 Sep 2026 18:16:57 -0700 Subject: [PATCH] Load-test rig: k6 swarm script + in-cluster target drone --- manifests/loadtest/k6-script.yaml | 38 ++++++++ manifests/loadtest/target-drone.yaml | 31 +++++++ manifests/monitoring/grafana.yaml | 65 ++++++++++++++ manifests/monitoring/node-exporter.yaml | 41 +++++++++ manifests/monitoring/prometheus.yaml | 115 ++++++++++++++++++++++++ scripts/loadtest.sh | 47 ++++++++++ 6 files changed, 337 insertions(+) create mode 100644 manifests/loadtest/k6-script.yaml create mode 100644 manifests/loadtest/target-drone.yaml create mode 100644 manifests/monitoring/grafana.yaml create mode 100644 manifests/monitoring/node-exporter.yaml create mode 100644 manifests/monitoring/prometheus.yaml create mode 100755 scripts/loadtest.sh diff --git a/manifests/loadtest/k6-script.yaml b/manifests/loadtest/k6-script.yaml new file mode 100644 index 0000000..94fa7f3 --- /dev/null +++ b/manifests/loadtest/k6-script.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: loadtest +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: k6-script + namespace: loadtest +data: + test.js: | + // Generic load generator. Configured by env: + // TARGET_URL - full URL to hit + // VUS - virtual users per pod (default 20) + // DURATION - k6 duration string (default "60s") + import http from 'k6/http'; + import { check, sleep } from 'k6'; + + const target = __ENV.TARGET_URL || 'http://example.com'; + const vus = parseInt(__ENV.VUS || '20'); + const duration = __ENV.DURATION || '60s'; + + export const options = { + stages: [ + { duration: '10s', target: vus }, + { duration: duration, target: vus }, + { duration: '5s', target: 0 }, + ], + thresholds: { http_req_failed: ['rate<0.01'] }, + noConnectionReuse: false, + }; + + export default function () { + const res = http.get(target, { timeout: '10s' }); + check(res, { 'status 2xx/3xx': (r) => r.status < 300 }); + sleep(1 / vus * 10); + } diff --git a/manifests/loadtest/target-drone.yaml b/manifests/loadtest/target-drone.yaml new file mode 100644 index 0000000..1860bae --- /dev/null +++ b/manifests/loadtest/target-drone.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: target-drone + namespace: loadtest +spec: + replicas: 3 + selector: + matchLabels: {app: target-drone} + template: + metadata: + labels: {app: target-drone} + spec: + containers: + - name: nginx + image: nginx:1.29-alpine + ports: [{containerPort: 80}] + resources: + requests: {cpu: 10m, memory: 16Mi} + limits: {memory: 64Mi} +--- +apiVersion: v1 +kind: Service +metadata: + name: target-drone + namespace: loadtest +spec: + selector: {app: target-drone} + ports: + - port: 80 + targetPort: 80 diff --git a/manifests/monitoring/grafana.yaml b/manifests/monitoring/grafana.yaml new file mode 100644 index 0000000..323ef21 --- /dev/null +++ b/manifests/monitoring/grafana.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-datasources + namespace: monitoring +data: + datasources.yaml: | + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus.monitoring.svc:9090 + isDefault: true +--- +apiVersion: v1 +kind: Secret +metadata: + name: grafana-admin + namespace: monitoring +stringData: + admin-user: adamcarr + admin-password: planck-lab-admin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: {app: grafana} + template: + metadata: + labels: {app: grafana} + spec: + containers: + - name: grafana + image: grafana/grafana:12.0.2 + env: + - {name: GF_SECURITY_ADMIN_USER, valueFrom: {secretKeyRef: {name: grafana-admin, key: admin-user}}} + - {name: GF_SECURITY_ADMIN_PASSWORD, valueFrom: {secretKeyRef: {name: grafana-admin, key: admin-password}}} + - {name: GF_AUTH_ANONYMOUS_ENABLED, value: "true"} + ports: + - containerPort: 3000 + resources: + requests: {cpu: 50m, memory: 128Mi} + limits: {memory: 512Mi} + volumeMounts: + - {name: datasources, mountPath: /etc/grafana/provisioning/datasources} + volumes: + - name: datasources + configMap: {name: grafana-datasources} +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + namespace: monitoring +spec: + selector: {app: grafana} + ports: + - port: 3000 + targetPort: 3000 diff --git a/manifests/monitoring/node-exporter.yaml b/manifests/monitoring/node-exporter.yaml new file mode 100644 index 0000000..ba308e4 --- /dev/null +++ b/manifests/monitoring/node-exporter.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: node-exporter + namespace: monitoring + labels: {app: node-exporter} +spec: + selector: + matchLabels: {app: node-exporter} + template: + metadata: + labels: {app: node-exporter} + spec: + hostNetwork: true + hostPID: true + tolerations: + - operator: Exists + containers: + - name: node-exporter + image: quay.io/prometheus/node-exporter:v1.9.1 + args: + - --path.procfs=/host/proc + - --path.sysfs=/host/sys + - --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+)($|/) + ports: + - containerPort: 9100 + hostPort: 9100 + resources: + requests: {cpu: 20m, memory: 32Mi} + limits: {memory: 64Mi} + volumeMounts: + - {name: proc, mountPath: /host/proc, readOnly: true} + - {name: sys, mountPath: /host/sys, readOnly: true} + - {name: root, mountPath: /rootfs, readOnly: true} + volumes: + - name: proc + hostPath: {path: /proc} + - name: sys + hostPath: {path: /sys} + - name: root + hostPath: {path: /} diff --git a/manifests/monitoring/prometheus.yaml b/manifests/monitoring/prometheus.yaml new file mode 100644 index 0000000..28e638f --- /dev/null +++ b/manifests/monitoring/prometheus.yaml @@ -0,0 +1,115 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus + namespace: monitoring +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus +rules: + - apiGroups: [""] + resources: [nodes, nodes/proxy, services, endpoints, pods] + verbs: [get, list, watch] + - apiGroups: ["extensions", "networking.k8s.io"] + resources: [ingresses] + verbs: [get, list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: prometheus +subjects: + - kind: ServiceAccount + name: prometheus + namespace: monitoring +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: monitoring +data: + prometheus.yml: | + global: + scrape_interval: 15s + external_labels: + cluster: planck + scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + - job_name: node-exporter + kubernetes_sd_configs: + - role: node + relabel_configs: + - source_labels: [__meta_kubernetes_node_address_InternalIP] + target_label: instance_ip + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + metric_relabel_configs: + - source_labels: [__name__] + regex: go_.* + action: drop +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: prometheus + namespace: monitoring +spec: + serviceName: prometheus + replicas: 1 + selector: + matchLabels: {app: prometheus} + template: + metadata: + labels: {app: prometheus} + spec: + serviceAccountName: prometheus + containers: + - name: prometheus + image: prom/prometheus:v3.6.0 + args: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.retention.time=15d + - --storage.tsdb.path=/data + ports: + - containerPort: 9090 + resources: + requests: {cpu: 150m, memory: 512Mi} + limits: {memory: 1Gi} + volumeMounts: + - {name: config, mountPath: /etc/prometheus} + - {name: data, mountPath: /data} + volumes: + - name: config + configMap: {name: prometheus-config} + volumeClaimTemplates: + - metadata: {name: data} + spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: {storage: 20Gi} +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: monitoring +spec: + selector: {app: prometheus} + ports: + - port: 9090 + targetPort: 9090 diff --git a/scripts/loadtest.sh b/scripts/loadtest.sh new file mode 100755 index 0000000..9d8cea0 --- /dev/null +++ b/scripts/loadtest.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Spawn a k6 load swarm against a target. +# Usage: ./scripts/loadtest.sh [pods] [vus-per-pod] [duration] +# Example: ./scripts/loadtest.sh https://staging.example.com 20 50 5m +# -> 20 pods x 50 virtual users = 1000 concurrent requests, 5 minutes +set -euo pipefail + +TARGET=${1:?usage: loadtest.sh [pods] [vus-per-pod] [duration]} +PODS=${2:-20} +VUS=${3:-20} +DURATION=${4:-60s} +NAME="loadtest-$(date +%s)" + +kubectl -n loadtest apply -f - <