Chapter 27Lesson 05~330 minutes

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.

CheckpointImage provenanceNative parity2×100=200Artifact persistence

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.
Abort: public/shared Kubernetes context, unexpected image/digest/runtime, writable input mounts, real credential in build/image/JMX, non-private target path, Docker run >100 target events, Kubernetes design >200 configured events without new authorization, OOM/nonzero exit, missing artifacts, unexpected retry/extra Pod, or generator resource saturation.

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

  1. Pull eclipse-temurin:17.0.20_8-jre-jammy and save RepoDigests.
  2. Build custom JMeter image; SHA-512 verification must pass.
  3. Save final JMeter image ID and docker history --no-trunc.
  4. Run image jmeter -v and java -version.
  5. Hash JMX/config/CSV and preserve the manifest.
  6. 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 + matching jmeter.log from the host mount;
  • target server-events.jsonl via docker cp before 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-1 map 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

Example: “A custom non-root JMeter 5.6.3 image was built from the Apache tarball after SHA-512 verification on Eclipse Temurin 17.0.20+8; the resolved Java base digest and final JMeter image ID were retained. The same hashed JMX/config/CSV ran natively and in Docker with 5 threads ×20 loops. Both runs produced exactly 100 successful JTL rows and 100 target events. Docker mounted inputs read-only, artifacts to a host directory, used a private user-defined network, 1 CPU/768 MiB/256-PID limits and a read-only root filesystem; exit code was zero and OOM=false. Therefore the test assets are portable across the two execution forms at this correctness/load level. The optional kind design uses two indexed Pods, so configured total is 200; that design must independently prove two 100-sample artifacts/target counts and Pod resource validity before any performance conclusion.”

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?

Why must native and Docker runs use different target.host values while keeping the same JMX?

What is the optional Job's configured total sample count?

A completed Job is deleted before kubectl cp and used emptyDir. What failed?

What is Chapter 28's bridge?

Next chapter

CI/CD Performance Gates in GitHub Actions, GitLab CI, and Jenkins

Chapter 28 turns reproducible ephemeral injectors into auditable automated performance gates.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.