EKS Coordinator And Agents
Set up finite EKS coordinator and Indexed Agent Jobs with matching identities, NATS connectivity, runner credentials and retained reports.
Matching docs
Search across docs titles, summaries, groups, and section headings.
Use Up and Down Arrow to move through results, then press Enter to open the active page.
No indexed docs matched that search. Try a broader term or open the docs hub.
What this page helps you do
What this page helps you do
Set up finite EKS coordinator and Indexed Agent Jobs with matching identities, NATS connectivity, runner credentials and retained reports.
Who this is for
Teams moving from one machine to coordinator-and-agent execution or tighter workload targeting.
Prerequisites
- A scenario that already works in a single-node run
By the end
A clearer cluster topology and the fields that must line up across nodes.
Use this page when
Use this page when execution topology, partitioning, or targeting changes how the run should be distributed.
Visual guide
Guide
Architecture
Use one coordinator Job and a finite Indexed Agent Job. Each standalone Agent waits for one signed run, executes its assignment and exits. A continuously restarted Deployment can leave idle replacement Agents after completion.
Runnable Images
Package your executable harness and its normal production SDK/runtime dependencies. The C# example uses a .NET 8 console entry point; other SDKs need their own entry points. A unit test assembly is not an application. Use the same verified immutable build on all nodes; keyless Go Agents need the compatible signed runtime preinstalled.
Explicit Configuration
Your application must read the deployment variables and pass them to the runner APIs. The sample names are not automatically bound by the SDK. Match SessionId, ClusterId, AgentGroup and scenario definitions; remote V2 requires unique stable AgentId values and the exact ExpectedAgentIds roster.
One Run Per Job
Set Indexed Job completions and parallelism to the expected Agent count. Ensure every Agent listener is ready before starting the coordinator. Wait for coordinator and agent Job Complete, inspect exit codes and diagnostics, and use new Job names and a new SessionId for each run.
NATS And Security
Use a reachable authenticated NATS broker and configure certificate trust for TLS in each language runtime. Keep broker credentials in a Secret and the entitled runner key in a separate coordinator-only Secret. Restrict namespace access and allow only required broker, licensing, sink and target network paths. Agents validate coordinator-issued authorization; do not give them the runner key.
EKS Access And Storage
Configure node or Fargate permissions to pull your immutable ECR image. AWS API calls from targets or sinks need supported AWS workload credentials, using EKS Pod Identity or IRSA as appropriate; image pull and pod API permissions are separate. Provision a suitable CSI storage class before creating the coordinator report claim.
Reports And Sinks
The coordinator writes the combined HTML, TXT, CSV and Markdown report set. Configured remote-agent sinks receive agent data; coordinator sinks receive the merged result. Final SinkErrors and DisabledSinks include child and coordinator failures. ReportingComplete describes raw observation delivery, not success of every independent final sink callback.
Artifact Retention
Retain coordinator artifacts on its PVC or export them before cleanup. Retrieve a completed Job report through a separate retention Pod or exporter. Use distinct node paths for agent JSONL output, or remote sinks; an RWO coordinator claim is not shared multi-node agent storage.
Reporting Limits
Distributed results retain aggregate and node statistics; temporal history can be unavailable and is disclosed in reports. Do not infer a merged latency time series from final counters. Size in-flight capacity and CPU/memory for offered load, and inspect explicit generator drops separately from application failures.
Validation Scope
Docker and local Kubernetes process tests verify SDK execution, Job completion and artifact delivery for the tested builds. They do not verify your EKS IAM, CNI policies, registry access, multi-node storage, broker TLS trust or external vendor credentials; validate those in the target cluster before a production load run.
Cluster setup samples
The executable harness explicitly maps environment inputs to runner APIs. Give every new run a fresh SESSION_ID and use that same value on the coordinator and all agents. The shared Jobs use stable indexed Agent identities, coordinator-only licensing and retained report storage.
If you run these examples locally, add a valid runner key before execution starts. Set it with WithRunnerKey("...") or the config key LoadStrike:RunnerKey.
Runnable Container Harness
using LoadStrike;
public static class LoadStrikeHarness
{
public static int Main()
{
string Required(string name) => Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException($"Missing {name}");
var role = Enum.Parse<LoadStrikeNodeType>(Required("NODE_ROLE"));
if (role is not (LoadStrikeNodeType.Agent or LoadStrikeNodeType.Coordinator))
throw new InvalidOperationException("NODE_ROLE must be Agent or Coordinator");
var scenario = LoadStrikeScenario.Create("checkout", _ =>
Task.FromResult(LoadStrikeResponse.Ok(statusCode: "200")))
.WithoutWarmUp()
.WithLoadSimulations(LoadStrikeSimulation.IterationsForInject(10, TimeSpan.FromSeconds(1), 100));
var runner = LoadStrikeRunner.RegisterScenarios(scenario)
.UseLoadEngineV2().WithMaxInFlight(1000)
.WithSessionId(Required("SESSION_ID")).WithClusterId(Required("CLUSTER_ID"))
.WithAgentGroup(Required("AGENT_GROUP")).WithNatsServerUrl(Required("NATS_URL"))
.WithNodeType(role).WithClusterCommandTimeout(TimeSpan.FromSeconds(180))
.WithReportFolder(Environment.GetEnvironmentVariable("REPORT_DIR") ?? "/reports")
.WithReportFormats(LoadStrikeReportFormat.Html, LoadStrikeReportFormat.Txt,
LoadStrikeReportFormat.Csv, LoadStrikeReportFormat.Md);
if (role == LoadStrikeNodeType.Agent)
runner = runner.WithAgentId("agent-" + Required("JOB_COMPLETION_INDEX"));
else
runner = runner.EnableLocalDevCluster(false).WithAgentsCount(2)
.WithExpectedAgentIds("agent-0", "agent-1").WithRunnerKey(Required("LOADSTRIKE_RUNNER_KEY"));
var result = runner.Run();
Console.WriteLine($"Requests={result.AllRequestCount}; sink errors={result.SinkErrors.Count}");
return result.AllFailCount == 0 && result.SinkErrors.Count == 0 ? 0 : 1;
}
}
package main
import (
"fmt"
loadstrike "loadstrike.com/sdk/go"
"os"
"strings"
)
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
panic("missing " + name)
}
return value
}
func main() {
role := required("NODE_ROLE")
scenario := loadstrike.CreateScenario("checkout", func(ctx loadstrike.LoadStrikeScenarioContext) loadstrike.LoadStrikeReply {
return loadstrike.LoadStrikeStep.Run("checkout", ctx, func(loadstrike.LoadStrikeScenarioContext) loadstrike.LoadStrikeReply {
// Replace with the owned application's actual checkout request.
return loadstrike.OK()
})
}).WithoutWarmUp().WithDeclaredSteps("checkout").WithLoadSimulations(loadstrike.LoadStrikeSimulation.IterationsForConstant(2, 100))
reportDir := strings.TrimSpace(os.Getenv("REPORT_DIR"))
if reportDir == "" {
reportDir = "/reports"
}
context := loadstrike.RegisterScenarios(scenario).
WithSessionId(required("SESSION_ID")).WithClusterId(required("CLUSTER_ID")).
WithAgentGroup(required("AGENT_GROUP")).WithNatsServerUrl(required("NATS_URL")).
WithAgentsCount(2).WithClusterCommandTimeout(loadstrike.DurationFromSeconds(120)).
UseLoadEngineV2().WithMaxInFlight(1000).WithReportFolder(reportDir)
switch role {
case "Coordinator":
context = context.WithNodeType(loadstrike.NodeTypeCoordinator).
WithExpectedAgentIDs("agent-0", "agent-1").WithRunnerKey(required("LOADSTRIKE_RUNNER_KEY"))
case "Agent":
index := required("JOB_COMPLETION_INDEX")
if index != "0" && index != "1" {
panic("JOB_COMPLETION_INDEX must be 0 or 1")
}
// Keyless agents require a compatible publisher-signed runtime already
// provisioned in their protected cache; startup does not download one.
context = context.WithNodeType(loadstrike.NodeTypeAgent).WithAgentID("agent-" + index)
default:
panic("NODE_ROLE must be Agent or Coordinator")
}
result := context.Run()
fmt.Printf("requests=%d ok=%d failed=%d reportingComplete=%v\n", result.AllRequestCount(), result.AllOKCount(), result.AllFailCount(), result.ReportingComplete())
if !result.ReportingComplete() || len(result.SinkErrors()) > 0 {
os.Exit(1)
}
}
import com.loadstrike.runtime.LoadStrikeRuntime;
class LoadStrikeEksExample {
public static void main(String[] args) {
String role = required("NODE_ROLE");
if (!role.equals("Agent") && !role.equals("Coordinator")) {
throw new IllegalArgumentException("NODE_ROLE must be Agent or Coordinator.");
}
String session = required("SESSION_ID");
String cluster = required("CLUSTER_ID");
String group = required("AGENT_GROUP");
String natsUrl = required("NATS_URL");
var scenario = LoadStrikeRuntime.LoadStrikeScenario.create("checkout", context -> {
// Call the application under test here.
return LoadStrikeRuntime.LoadStrikeResponse.ok("200");
}).withoutWarmUp().withLoadSimulations(
LoadStrikeRuntime.LoadStrikeSimulation.iterationsForConstant(10, 100));
var runner = LoadStrikeRuntime.LoadStrikeRunner.registerScenarios(scenario)
.useLoadEngineV2().withMaxInFlight(1000).withNodeType(role)
.withSessionId(session)
.configure(context -> context.withClusterId(cluster))
.withAgentGroup(group).withExpectedAgentIds("agent-0", "agent-1")
.withAgentsCount(2).withNatsServerUrl(natsUrl)
.withClusterCommandTimeout(120)
.withReportFolder(System.getenv().getOrDefault("REPORT_DIR", "/reports"))
.withReportFormats("html", "txt", "csv", "md");
if ("Coordinator".equals(role)) {
runner.withRunnerKey(required("LOADSTRIKE_RUNNER_KEY"));
} else {
int index = Integer.parseInt(required("JOB_COMPLETION_INDEX"));
if (index < 0 || index > 1) {
throw new IllegalArgumentException("JOB_COMPLETION_INDEX must be 0 or 1.");
}
if (System.getenv("LOADSTRIKE_RUNNER_KEY") != null) {
throw new IllegalStateException("Agents must not receive the coordinator runner key.");
}
runner.withAgentId("agent-" + index);
}
var result = runner.run();
System.out.println("requests=" + result.allRequestCount + " failed=" + result.allFailCount);
int expected = role.equals("Coordinator") ? 100 : 50;
if (result.allRequestCount != expected || result.allFailCount != 0
|| !result.reportingComplete || !result.sinkErrors.isEmpty()
|| !result.disabledSinks.isEmpty()) {
throw new IllegalStateException("The distributed workload or reporting did not complete successfully.");
}
}
private static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " is required.");
}
return value;
}
}
import os
from loadstrike_sdk import (
LoadStrikeResponse, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation,
)
def required(name):
value = os.environ.get(name)
if not value:
raise ValueError(name + " is required")
return value
role = required("NODE_ROLE") # Agent or Coordinator
if role not in ("Agent", "Coordinator"):
raise ValueError("NODE_ROLE must be Agent or Coordinator")
index = required("JOB_COMPLETION_INDEX") if role == "Agent" else None
if role == "Agent" and index not in ("0", "1"):
raise ValueError("JOB_COMPLETION_INDEX must be 0 or 1")
agent_id = "agent-" + index if index is not None else "coordinator"
scenario = LoadStrikeScenario.create("checkout", lambda _: LoadStrikeResponse.ok("200"))
scenario = scenario.without_warm_up().with_load_simulations(
LoadStrikeSimulation.iterations_for_constant(2, 100)
)
runner = LoadStrikeRunner.register_scenarios(scenario).configure(
node_type=role,
session_id=required("SESSION_ID"),
cluster_id=required("CLUSTER_ID"),
agent_group=required("AGENT_GROUP"),
agent_id=agent_id,
agents_count=2,
expected_agent_ids=["agent-0", "agent-1"],
nats_server_url=required("NATS_URL"),
runner_key=required("LOADSTRIKE_RUNNER_KEY") if role == "Coordinator" else None,
reports_enabled=role == "Coordinator",
report_folder_path=os.environ.get("REPORT_DIR") or "/reports",
report_formats=["html", "md", "txt", "csv"],
).use_load_engine_v2().with_max_in_flight(1000)
result = runner.run()
print(result["allRequestCount"])
import {
LoadStrikeResponse, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation,
} from "@loadstrike/loadstrike-sdk";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
const role = required("NODE_ROLE");
if (role !== "Agent" && role !== "Coordinator") throw new Error("NODE_ROLE must be Agent or Coordinator");
const index = role === "Agent" ? required("JOB_COMPLETION_INDEX") : undefined;
if (role === "Agent" && index !== "0" && index !== "1") throw new Error("JOB_COMPLETION_INDEX must be 0 or 1");
const agentId = role === "Agent" ? `agent-${index}` : "coordinator";
const scenario = LoadStrikeScenario.create("checkout", async () => LoadStrikeResponse.ok("200"))
.withoutWarmUp()
.withLoadSimulations(LoadStrikeSimulation.iterationsForConstant(2, 100));
const runner = LoadStrikeRunner.registerScenarios(scenario).configure({
nodeType: role,
sessionId: required("SESSION_ID"),
clusterId: required("CLUSTER_ID"),
agentGroup: required("AGENT_GROUP"),
agentId,
agentsCount: 2,
expectedAgentIds: ["agent-0", "agent-1"],
natsServerUrl: required("NATS_URL"),
runnerKey: role === "Coordinator" ? required("LOADSTRIKE_RUNNER_KEY") : undefined,
reportsEnabled: role === "Coordinator",
reportFolderPath: process.env.REPORT_DIR || "/reports",
reportFormats: ["html", "md", "txt", "csv"],
}).useLoadEngineV2().withMaxInFlight(1000);
const result = await runner.run();
console.log(result.allRequestCount);
const {
LoadStrikeResponse, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation,
} = require("@loadstrike/loadstrike-sdk");
function required(name) {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
const role = required("NODE_ROLE");
if (role !== "Agent" && role !== "Coordinator") throw new Error("NODE_ROLE must be Agent or Coordinator");
const index = role === "Agent" ? required("JOB_COMPLETION_INDEX") : undefined;
if (role === "Agent" && index !== "0" && index !== "1") throw new Error("JOB_COMPLETION_INDEX must be 0 or 1");
const agentId = role === "Agent" ? `agent-${index}` : "coordinator";
const scenario = LoadStrikeScenario.create("checkout", async () => LoadStrikeResponse.ok("200"))
.withoutWarmUp()
.withLoadSimulations(LoadStrikeSimulation.iterationsForConstant(2, 100));
const runner = LoadStrikeRunner.registerScenarios(scenario).configure({
nodeType: role,
sessionId: required("SESSION_ID"),
clusterId: required("CLUSTER_ID"),
agentGroup: required("AGENT_GROUP"),
agentId,
agentsCount: 2,
expectedAgentIds: ["agent-0", "agent-1"],
natsServerUrl: required("NATS_URL"),
runnerKey: role === "Coordinator" ? required("LOADSTRIKE_RUNNER_KEY") : undefined,
reportsEnabled: role === "Coordinator",
reportFolderPath: process.env.REPORT_DIR || "/reports",
reportFormats: ["html", "md", "txt", "csv"],
}).useLoadEngineV2().withMaxInFlight(1000);
(async () => {
const result = await runner.run();
console.log(result.allRequestCount);
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
One-Run Kubernetes Jobs (Map Inputs in Your Application)
| Parameter | Description |
|---|---|
Indexed agent Job
| Starts two finite Agent processes with stable agent-0 and agent-1 identities. Each exits after its signed assignment; both must reach Job Complete. |
Coordinator Job
| Carries the runner key through SecretKeyRef, assigns work over reachable NATS and writes merged reports. Agent pods have no runner key. |
Application inputs
| The sample Program.cs explicitly reads these variables. Use a fresh SESSION_ID for each new run, shared by its coordinator and agents. Other SDK harnesses must map equivalent role, session, roster and transport inputs to their own runner APIs. |
Retained reports
| Only the coordinator mounts the RWO report claim. Use a retention Pod or artifact exporter after Job completion; use separate paths or remote destinations for agent sinks. |
# Create the namespace and Secrets securely before applying the Jobs.
# loadstrike-coordinator-key: key = existing entitled runner key (coordinator only).
# loadstrike-nats: url = reachable authenticated NATS URL; configure TLS trust for each runtime.
# Replace the image with your immutable tested harness image. Change SESSION_ID for every run.
apiVersion: v1
kind: ConfigMap
metadata:
name: loadstrike-runtime
namespace: loadstrike
data:
SESSION_ID: orders-run-20261004-001
CLUSTER_ID: orders-cluster
AGENT_GROUP: perf-agents
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: loadstrike-reports
namespace: loadstrike
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
# Uses the cluster default storage class. On EKS configure a suitable CSI driver/class first.
# This claim is mounted only by the coordinator, not by agents on different worker nodes.
---
apiVersion: batch/v1
kind: Job
metadata:
name: loadstrike-agents
namespace: loadstrike
spec:
completionMode: Indexed
completions: 2
parallelism: 2
backoffLimit: 0
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containers:
- name: agent
image: <aws-account-id>.dkr.ecr.<region>.amazonaws.com/loadstrike-harness:<immutable-tag>
envFrom:
- configMapRef:
name: loadstrike-runtime
env:
- name: NODE_ROLE
value: Agent
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
- name: NATS_URL
valueFrom:
secretKeyRef:
name: loadstrike-nats
key: url
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
resources:
requests: {cpu: "500m", memory: "512Mi"}
limits: {cpu: "2", memory: "2Gi"}
---
apiVersion: batch/v1
kind: Job
metadata:
name: loadstrike-coordinator
namespace: loadstrike
spec:
backoffLimit: 0
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containers:
- name: coordinator
image: <aws-account-id>.dkr.ecr.<region>.amazonaws.com/loadstrike-harness:<immutable-tag>
envFrom:
- configMapRef:
name: loadstrike-runtime
env:
- name: NODE_ROLE
value: Coordinator
- name: NATS_URL
valueFrom:
secretKeyRef:
name: loadstrike-nats
key: url
- name: LOADSTRIKE_RUNNER_KEY
valueFrom:
secretKeyRef:
name: loadstrike-coordinator-key
key: key
- name: REPORT_DIR
value: /reports
volumeMounts:
- name: reports
mountPath: /reports
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
resources:
requests: {cpu: "500m", memory: "512Mi"}
limits: {cpu: "2", memory: "2Gi"}
volumes:
- name: reports
persistentVolumeClaim:
claimName: loadstrike-reports