Checkpoint Lab — Containers, Kubernetes, Ephemeral Injectors, and Infrastructure Automation
The checkpoint proves portability without hiding infrastructure differences. The same JMX runs natively and in a constrained non-root Docker injector. Both must produce exactly 100 successful samples and persistent JTL/log evidence. Then you design—not require cloud execution of—a local Kubernetes Job where two indexed Pods each run the same 100-sample plan, making configured total load explicitly 200.
Learning objectives
- Record image/build/input provenance before execution.
- Run one bounded private Docker injector with read-only inputs and host artifacts.
- Prove the same JMX works in native JMeter 5.6.3.
- Verify counts/results/exit state independently of container logs.
- Design a two-Pod local kind Job with exact aggregate load math and limits.
- Define artifact collection/cleanup before deleting ephemeral infrastructure.
1. Exact assumptions and load ceilings
| Item | Checkpoint baseline |
|---|---|
| JMeter | Apache JMeter 5.6.3 from verified Apache tarball. |
| Java | Eclipse Temurin 17.0.20+8 JRE in image; native comparison uses Java 17. |
| Plugins | None. |
| JMeter base image |
Custom devops-academy/jmeter:5.6.3-p27; final
local image ID recorded.
|
| Fixture image |
devops-academy/p27-fixture:1 built from Python
Docker Official Image 3.13.7-slim-bookworm.
|
| Docker network |
Private p27-net; no target port published.
|
| Docker workload | 5 threads ×20 loops =100 samples, 100 ms pacing, 2 KiB payload. |
| Container resources | 1 CPU, 768 MiB memory, 256 PIDs, Java heap 256–512 MiB. |
| Inputs | JMX/config/CSV mounted read-only + SHA-256 manifest. |
| Artifacts | Host-mounted JTL/jmeter.log + copied target events + inspect/exit/provenance files. |
| Native proof | Same JMX, 5×20=100 against 127.0.0.1:8027. |
| Optional kind | kind v0.32.0; 2 Indexed Job Pods ×100 samples =200 configured. |
| Kubernetes retry safety | restartPolicy Never, backoffLimit 0, activeDeadlineSeconds 90. |
2. Predictions before execution
P1 — portability: identical JMX + properties/data will produce 100 successful samples both natively and in Docker when only target/data path/injector properties are overridden.
P2 — network identity: native target host
=127.0.0.1; Docker target host =p27-fixture. Using
127.0.0.1 in Docker will fail and produce zero fixture events.
P3 — persistence: deleting
p27-jmeter after the run will not delete JTL/jmeter.log
because /artifacts is a host bind mount.
P4 — resources: container inspect shows the declared CPU/memory/PID/read-only-root limits and exit 0/OOM false.
P5 — Kubernetes load math: two Indexed Job Pods each run 5×20=100, so configured total =200 and target should show two injector IDs with 100 each if executed.
3. Freeze provenance and inputs
-
Pull
eclipse-temurin:17.0.20_8-jre-jammyand save RepoDigests. - Build custom JMeter image; SHA-512 verification must pass.
-
Save final JMeter image ID and
docker history --no-trunc. -
Run image
jmeter -vandjava -version. - Hash JMX/config/CSV and preserve the manifest.
- Confirm no third-party plugin was added.
4. Execute and verify the Docker run
Use Lesson 2's exact p27-net, fixture and JMeter
commands with run ID p27-check-docker. Change only:
-Jrun.id=p27-check-docker
-Jinjector.id=docker
-Jtarget.host=p27-fixture
-Jdata.file=/inputs/data/scenario.csv
Persist:
-
results.jtl+ matchingjmeter.logfrom the host mount; -
target
server-events.jsonlviadocker cpbefore fixture removal; - JMeter container inspect/mount/resource/exit-state output;
- image/input provenance files.
Verifier gate: 100 JTL +100 target events, zero failures, injector docker, exit=0, OOM=false.
5. Execute and verify the same JMX natively
Run the same JMX under native JMeter 5.6.3/Java 17 with run ID
p27-check-native, injector=native,
host=127.0.0.1 and the host absolute CSV path. Require
100/100 successes.
Compare:
| Invariant | Native | Docker |
|---|---|---|
| JMX SHA | same | same mounted file. |
| Config/data SHA | same | same mounted files. |
| Threads/loops | 5×20 | 5×20. |
| Expected samples | 100 | 100. |
| HTTP semantics | HttpClient4 + keep-alive + same assertion | same. |
| Target identity | 127.0.0.1:8027 | p27-fixture:8027 private Docker DNS. |
| Injector identity | native | docker. |
| Artifacts | host filesystem | host bind mount. |
| Runtime constraints | host process | 1 CPU/768MiB/PID/read-only-root. |
Do not compare native versus Docker latency as a performance regression unless generator/network/resource environments are intentionally normalized and measured.
6. Optional local kind design: prepare inputs
The Kubernetes path is optional and local. Confirm context/name first:
kind version
kubectl config current-context
kind create cluster --name p27
kind load docker-image `
devops-academy/jmeter:5.6.3-p27 `
devops-academy/p27-fixture:1 `
--name p27
kind documentation recommends avoiding latest for
locally loaded images; the manifest uses
imagePullPolicy: Never.
Create a ConfigMap from the same versioned inputs:
kubectl create configmap p27-inputs `
--from-file=container-local.jmx=.\plans\container-local.jmx `
--from-file=container.properties=.\config\container.properties `
--from-file=scenario.csv=.\data\scenario.csv `
--dry-run=client -o yaml `
> .\k8s\p27-inputs.yaml
ConfigMap is suitable only for this tiny non-secret teaching input. Large/private data belongs in an explicitly versioned storage/image mechanism; credentials belong in Secrets/approved external secret systems.
7. Optional local fixture Deployment/Service
k8s/p27-fixture.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: p27-fixture
spec:
replicas: 1
selector:
matchLabels:
app: p27-fixture
template:
metadata:
labels:
app: p27-fixture
spec:
containers:
- name: fixture
image: devops-academy/p27-fixture:1
imagePullPolicy: Never
args: ["--host", "0.0.0.0", "--port", "8027", "--log", "/tmp/server-events.jsonl"]
ports:
- containerPort: 8027
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "256Mi"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: p27-fixture
spec:
selector:
app: p27-fixture
ports:
- port: 8027
targetPort: 8027
The Service is ClusterIP only—no Ingress/NodePort/LoadBalancer. Target stays inside the disposable cluster.
8. Optional Indexed Job with explicit 200-sample math
k8s/p27-job.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: p27-jmeter
spec:
completions: 2
parallelism: 2
completionMode: Indexed
backoffLimit: 0
activeDeadlineSeconds: 90
template:
spec:
restartPolicy: Never
containers:
- name: jmeter
image: devops-academy/jmeter:5.6.3-p27
imagePullPolicy: Never
command: ["/bin/sh", "-ec"]
args:
- |
exec /opt/jmeter/bin/jmeter \
-n \
-t /inputs/container-local.jmx \
-q /inputs/container.properties \
-Jtarget.host=p27-fixture \
-Jtarget.port=8027 \
-Jrun.id=p27-k8s \
-Jinjector.id="k8s-${JOB_COMPLETION_INDEX}" \
-Jdata.file=/inputs/scenario.csv \
-l "/artifacts/results-${JOB_COMPLETION_INDEX}.jtl" \
-j "/artifacts/jmeter-${JOB_COMPLETION_INDEX}.log"
env:
- name: HEAP
value: "-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m"
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
resources:
requests:
cpu: "250m"
memory: "384Mi"
limits:
cpu: "500m"
memory: "768Mi"
volumeMounts:
- name: inputs
mountPath: /inputs
readOnly: true
- name: artifacts
mountPath: /artifacts
volumes:
- name: inputs
configMap:
name: p27-inputs
- name: artifacts
emptyDir: {}
Configured load:
2 completions × (5 threads × 20 loops × 1 Work sampler)
= 2 × 100
= 200 configured target samples
injector IDs:
k8s-0 -> expected 100
k8s-1 -> expected 100
backoffLimit: 0 avoids retrying a failed Pod in this
teaching run. activeDeadlineSeconds: 90 bounds runtime.
Resource limits are per Pod; aggregate requested/limited resources
also double with parallelism.
9. Optional execution/inspection
kubectl apply -f .\k8s\p27-inputs.yaml
kubectl apply -f .\k8s\p27-fixture.yaml
kubectl rollout status deployment/p27-fixture --timeout=60s
kubectl apply -f .\k8s\p27-job.yaml
kubectl wait --for=condition=complete job/p27-jmeter --timeout=90s
kubectl get job p27-jmeter -o wide
kubectl get pods -l job-name=p27-jmeter -o wide
kubectl describe job p27-jmeter
kubectl top pod 2>$null
If a Pod fails or count exceeds 200, preserve state/logs and stop.
Do not increase parallelism to “make it finish.”
10. Collect Pod artifacts before deleting the Job
The example uses emptyDir, so each Pod's artifacts
persist only while that Pod exists. Collect them explicitly:
$pods = kubectl get pods -l job-name=p27-jmeter `
-o jsonpath='{.items[*].metadata.name}'
New-Item -ItemType Directory -Force .\results\p27-k8s | Out-Null
foreach ($pod in $pods -split ' ') {
New-Item -ItemType Directory -Force ".\results\p27-k8s\$pod" | Out-Null
kubectl cp "$pod`:/artifacts/." ".\results\p27-k8s\$pod"
kubectl logs $pod > ".\results\p27-k8s\$pod\container.log"
kubectl get pod $pod -o yaml > ".\results\p27-k8s\$pod\pod.yaml"
}
kubectl get job p27-jmeter -o yaml `
> .\results\p27-k8s\job.yaml
Do not set a short ttlSecondsAfterFinished until
artifact automation can guarantee collection before deletion.
11. Kubernetes validity gate
If executed, require:
- two successful Indexed Pods, no retry/extra failed Pod;
- each JTL contains exactly 100 successful rows;
-
injector IDs
k8s-0/k8s-1map to 100 samples each; - fixture stats/events show exactly 200 total;
- no OOMKilled/deadline/resource-throttle evidence severe enough to invalidate achieved load;
- all JTL/logs collected before Pod/Job deletion.
12. Required evidence packet
| Artifact | Required content |
|---|---|
| Image provenance | Temurin RepoDigest, JMeter Apache SHA-512/build log, final image ID/history. |
| Runtime versions | Java/JMeter/plugin set, Docker; optional kind/kubectl versions/context. |
| Mounted inputs | JMX/config/CSV SHA-256 + Docker mount inspection/ConfigMap manifest. |
| Network | p27-net/container names; optional ClusterIP Service/Pod topology. |
| Resources | Docker CPU/memory/PID/rootfs + optional Pod requests/limits. |
| Artifacts | native/container JTL + matching jmeter.log; target events; sizes/paths. |
| Exit state | Docker ExitCode/OOM; optional Job/Pod conditions/restarts. |
| Load math | 100 native, 100 Docker; optional 2×100=200 Kubernetes. |
| Validity note | configured/achieved counts + generator/target limitations; no native/container latency equivalence assumed. |
13. Example validity statement
14. Cleanup / rollback
# Docker
docker rm p27-jmeter
docker rm -f p27-fixture 2>$null
docker network rm p27-net
# Optional Kubernetes after artifact collection
kubectl delete -f .\k8s\p27-job.yaml --ignore-not-found
kubectl delete -f .\k8s\p27-fixture.yaml --ignore-not-found
kubectl delete -f .\k8s\p27-inputs.yaml --ignore-not-found
kind delete cluster --name p27
Keep image/input manifests and run artifacts until review. Image deletion is optional; if retained, its ID becomes part of provenance. No production/public target, cloud resource, real secret, host firewall/OS tuning or remote RMI system was changed.
15. What Chapter 27 adds to the operating model
The production performance-testing operating model now has an ephemeral injector infrastructure contract: image/base provenance and integrity verification, plugin/runtime versions, immutable mounted inputs, container/Pod network target identity, CPU/memory/PID/JVM limits, non-root/filesystem permissions, secret injection boundary, container/Job count and retry load math, persistent JTL/log/telemetry path, exit/OOM/restart state, configured-versus-achieved target counts and cleanup are required before ephemeral infrastructure is considered reproducible.
Chapter 28 moves to CI/CD Performance Gates in GitHub Actions, GitLab CI, and Jenkins. It takes this reproducible injector contract into automated pipelines where runner identity, caches, secrets, service startup, artifacts, thresholds, exit codes and flaky-environment policy determine whether a performance gate is trustworthy.
Knowledge check
What proves the Docker JMeter runtime was not an unknown community image?
The custom Dockerfile, version-specific Java base RepoDigest, Apache JMeter SHA-512 verification and final local image ID/runtime version evidence.
Why must native and Docker runs use different target.host values while keeping the same JMX?
Their network namespaces differ; properties adapt target identity without modifying the test-plan source.
What is the optional Job's configured total sample count?
200 = 2 indexed Pods × (5 threads ×20 loops ×1 sampler).
A completed Job is deleted before kubectl cp and used emptyDir. What failed?
Artifact-lifecycle design: the evidence was left in Pod-ephemeral storage and not persisted before cleanup.
What is Chapter 28's bridge?
Run the pinned/portable injector in CI while preserving runner/service/secrets/artifacts/threshold/exit-state evidence for trustworthy performance gates.
Official references and version notes
- Apache JMeter downloads — JMeter 5.6.3, Java requirement, SHA-512/PGP integrity verification.
- Apache-published JMeter 5.6.3 SHA-512 — checksum used by the lab Dockerfile.
- Eclipse Temurin Docker Official Image — version-specific Java 17.0.20+8 JRE base used by the custom injector image.
- Docker bind mounts — read-only input mounts and writable host artifact mounts.
- Docker run reference — read-only root filesystem, CPU/memory limits, mounts, networks and exit-state inspection.
-
Kubernetes Jobs
— run-to-completion semantics, parallelism/completions,
restartPolicy, backoff and active deadlines. - kind Quick Start — local cluster lifecycle and loading locally built images.
Version-sensitive statements were rechecked against current
primary documentation on 2026-09-05. The course baseline remains
Apache JMeter 5.6.3, requiring Java 8+; this
chapter uses
Eclipse Temurin 17.0.20+8 JRE (Jammy) as the
version-specific Java base and records its resolved registry
digest locally. The JMeter layer is built from Apache's binary
tarball and verifies Apache's published SHA-512:
5978a1a35edb5a7d428e270564ff49d2b1b257a65e17a759d259a9283fc17093e522fe46f474a043864aea6910683486340706d745fcdf3db1505fd71e689083. Apache JMeter does not need a third-party plugin for the
mandatory lab. Docker bind mounts are writable by default, so the
JMX/data/config inputs are explicitly readonly; only
the artifact mount is writable. The optional Kubernetes path uses
current batch/v1 Job semantics with
restartPolicy: Never, backoffLimit: 0,
activeDeadlineSeconds, resource requests/limits, and
no cloud requirement. The local cluster example assumes
kind v0.32.0; a current minikube installation is
an optional equivalent.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.