Tutorial: Deploy Your First Application
In this tutorial, you’ll deploy an nginx web server to a Kubernetes cluster through Brokkr. You’ll learn the core workflow: creating a stack, adding Kubernetes manifests as deployment objects, and watching an agent apply them to a cluster.
What you’ll learn:
- How stacks organize Kubernetes resources
- How deployment objects carry YAML manifests
- How agents poll for and apply resources
- How to verify deployments succeeded
Prerequisites:
- A broker you can reach at
http://localhost:3000, from either a Helm install (kubectl port-forward svc/brokkr-broker 3000:3000) or the local development environment (angreal local up) - The admin PAK (Prefixed API Key) for that broker. The PAK itself is never logged. In the development environment it is the publicly known dev PAK
brokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8, which corresponds to the embedded defaultbroker.pak_hash. After a Helm install it is the PAK you minted withbrokkr-broker generate-pak— you can run that from the published image without installing anything:docker run --rm ghcr.io/colliery-io/brokkr-broker:latest generate-pak. (Only if a broker is explicitly configured with an emptybroker.pak_hashdoes it generate a fresh PAK at first startup and write it to/tmp/brokkr-keys/key.txtinside the broker container; leaving the setting untouched keeps the embedded default hash.) - A running agent connected to a Kubernetes cluster. This tutorial checks its own results with
kubectl, so an agent process has to be polling and applying — an agent record in the broker alone is not enough. The development environment pre-creates one calledbrokkr-integration-test-agent; after a Helm install it is the agent you created and installed in Quick Start steps 4 and 5, named by yourbroker.agentName. curlandjqinstalled- The
brokkrCLI on yourPATH, for the agent steps. Download it from the GitHub Release, or build it withcargo build --release -p brokkr-cli. Each step that uses it also shows thecurlform.
Export your agent’s name and the CLI’s connection settings before you start. The commands below use them:
export AGENT_NAME=brokkr-integration-test-agent # development environment# export AGENT_NAME=my-agent # Helm install: your broker.agentNameexport BROKKR_BROKER_URL=http://localhost:3000export BROKKR_PAK=<your-admin-pak>A freshly created agent — the development environment’s test agent included — starts INACTIVE and is registered only with the system generator. You’ll activate it and register it with the admin-generator in Step 3 before targeting it to your stack — see Registering Agents with Generators for the operational details. (If you already activated and registered your agent while following the installation guide, those steps are idempotent enough to re-run: re-registering returns 409 already_registered.)
Step 1: Verify the Environment
Section titled “Step 1: Verify the Environment”First, confirm the broker is running and healthy:
curl -s http://localhost:3000/healthzYou should see:
OKCheck that at least one agent is registered:
curl -s http://localhost:3000/api/v1/agents \ -H "Authorization: Bearer <your-admin-pak>" | jq '.[].name'You should see the name you exported as $AGENT_NAME in that list: "brokkr-integration-test-agent" in the development environment, or your own broker.agentName after a Helm install. If the list is empty, no agent has been created yet — go back to Quick Start step 4.
Tip: Throughout this tutorial, replace
<your-admin-pak>with your actual admin PAK. In the development environment that isbrokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8; on your own install it is the PAK whose hash you configured asbroker.pakHash/broker.pakHashExistingSecret.
Step 2: Create a Stack
Section titled “Step 2: Create a Stack”A stack is a named container that groups related Kubernetes resources. Think of it as a logical application — everything needed to run your service lives inside one stack.
Stacks are always owned by a generator (the entity that manages deployments, typically a CI/CD pipeline). The broker creates an admin-generator at first startup, linked to the admin PAK — as an admin, you’ll create the stack under it. Look up its ID first:
Admin PAK vs. generator PAK. This tutorial uses the admin PAK for everything because it’s the simplest way to learn the workflow. In real use, the admin PAK is a privileged break-glass credential — day-to-day, each CI pipeline or tenant gets its own generator (with its own PAK) that owns the stacks it manages. See Working with Generators for the production pattern.
GEN_ID=$(curl -s http://localhost:3000/api/v1/generators \ -H "Authorization: Bearer <your-admin-pak>" \ | jq -r '.[] | select(.name=="admin-generator") | .id')
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"tutorial-nginx\", \"description\": \"Tutorial: a simple nginx deployment\", \"generator_id\": \"${GEN_ID}\" }" | jq -r '.id')
echo "Stack ID: $STACK_ID"The response contains the new stack with its ID. The generator_id field ties the stack to its owning generator — we’ll explore generators in a later tutorial.
Step 3: Activate, Register, and Target the Agent
Section titled “Step 3: Activate, Register, and Target the Agent”Agents receive a stack’s resources in two ways: via agent targets — explicit assignments that require the agent to be registered with the stack’s owning generator first — or via label/annotation matching, when the stack’s selectors match the agent’s labels or annotations. In this tutorial you’ll use an agent target.
Registration is the agent’s opt-in consent boundary: an agent must be registered with a generator before any stack that generator owns can be targeted at it. A new agent — the development environment’s test agent, or one you created through the API — is registered only with the system generator, so you must register it with the admin-generator before targeting. For the operational details, see Registering Agents with Generators.
First, get the agent ID (using the $AGENT_NAME you exported in the prerequisites):
AGENT_ID=$(curl -s http://localhost:3000/api/v1/agents \ -H "Authorization: Bearer <your-admin-pak>" \ | jq -r --arg name "$AGENT_NAME" '.[] | select(.name==$name) | .id')
echo "Agent ID: $AGENT_ID"Every new agent starts with status INACTIVE, and an inactive agent skips all deployment work — nothing reaches the cluster until you activate it. While it is inactive, the agent log says so on each poll. Activate yours now (harmless if it is already ACTIVE):
brokkr agent activate "$AGENT_NAME"The output should say that the agent is ACTIVE.
The same step with curl
curl -s -X PUT "http://localhost:3000/api/v1/agents/${AGENT_ID}" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d '{"status": "ACTIVE"}' | jq '{name, status}'The response should show "status": "ACTIVE".
Next, register the agent with the admin-generator (the $GEN_ID you looked up in Step 2):
curl -s -X POST "http://localhost:3000/api/v1/generators/${GEN_ID}/register" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d "{\"agent_id\": \"${AGENT_ID}\"}" | jq .Registration gates which generators an agent can serve, ensuring agents opt in to application scopes before receiving their deployments. Without it, the next request fails with a 403 agent_not_registered error — and admins cannot bypass the gate.
Now target the agent to your stack. The first argument is the stack, the second is the agent:
brokkr stack target tutorial-nginx "$AGENT_NAME"The output says targeted stack "tutorial-nginx" at agent .... If you run it again, it says unchanged: and changes nothing.
The same step with curl
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d "{\"agent_id\": \"${AGENT_ID}\", \"stack_id\": \"${STACK_ID}\"}" | jq .The request body carries both agent_id and stack_id. The broker requires both fields, although the agent also appears in the URL.
The agent will now receive deployment objects from this stack on its next poll cycle.
Step 4: Create a Deployment Object
Section titled “Step 4: Create a Deployment Object”A deployment object contains the actual Kubernetes YAML that the agent will apply to its cluster. You can include multiple Kubernetes resources in a single deployment object using multi-document YAML (separated by ---).
Create a deployment object with an nginx namespace, deployment, and service:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d '{ "yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: tutorial-nginx\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx\n namespace: tutorial-nginx\n labels:\n app: nginx\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:1.27\n ports:\n - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: nginx\n namespace: tutorial-nginx\nspec:\n selector:\n app: nginx\n ports:\n - port: 80\n targetPort: 80" }' | jq .The response includes a sequence_id — an auto-incrementing number that orders deployment objects within a stack. The agent uses this to know which version is latest.
Step 5: Watch the Agent Apply Resources
Section titled “Step 5: Watch the Agent Apply Resources”The agent polls the broker at its configured interval — the binary’s embedded default is 10 seconds, while the Helm chart sets 30 seconds via agent.pollingInterval. Allow one full poll cycle before checking; then you should see the resources appear in your Kubernetes cluster.
Check the agent events to confirm the deployment was applied:
curl -s "http://localhost:3000/api/v1/agents/${AGENT_ID}/events" \ -H "Authorization: Bearer <your-admin-pak>" | jq '.[] | {event_type, status, message, created_at}'You should see a SUCCESS event:
{ "event_type": "DEPLOY", "status": "SUCCESS", "message": null, "created_at": "2025-01-15T10:01:30Z"}Troubleshooting: If the events list stays empty and nothing appears on the cluster, the usual cause is an agent that is still
INACTIVE. The agent log says so on each poll. Runbrokkr agent activate "$AGENT_NAME"from Step 3 again, and check thatbrokkr agent listshows the agent asACTIVE.
With kubectl pointed at the cluster your agent manages (the bundled k3s in the development environment, or the cluster you installed the agent chart into), verify the resources directly:
kubectl get all -n tutorial-nginxExpected output:
NAME READY STATUS RESTARTS AGEpod/nginx-7c5ddbdf54-abc12 1/1 Running 0 30spod/nginx-7c5ddbdf54-def34 1/1 Running 0 30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/nginx ClusterIP 10.43.120.50 <none> 80/TCP 30s
NAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/nginx 2/2 2 2 30sStep 6: Update the Deployment
Section titled “Step 6: Update the Deployment”To update a deployment, create a new deployment object in the same stack. The agent detects the new sequence_id and applies the updated manifests, reconciling the cluster state.
Scale nginx to 3 replicas:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d '{ "yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: tutorial-nginx\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx\n namespace: tutorial-nginx\n labels:\n app: nginx\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:1.27\n ports:\n - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: nginx\n namespace: tutorial-nginx\nspec:\n selector:\n app: nginx\n ports:\n - port: 80\n targetPort: 80" }' | jq .After the next poll cycle, verify the update:
kubectl get deployment nginx -n tutorial-nginxYou should see 3/3 in the READY column.
Step 7: Clean Up
Section titled “Step 7: Clean Up”To remove the deployed resources, create a deletion marker — a special deployment object with is_deletion_marker: true. This tells the agent to delete all resources previously applied for this stack from the cluster. The agent finds them by the k8s.brokkr.io/stack annotation it stamped on every object it applied, so the marker’s YAML body plays no part in deciding what gets removed. Send an empty yaml_content — that is the only body a deletion marker is allowed to skip validation for:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d '{ "yaml_content": "", "is_deletion_marker": true }' | jq .Note: “Empty” means genuinely empty. A placeholder comment such as
# deletionis not empty — it parses to a YAML document with no content, and the broker rejects it with400 invalid_deployment_object(“YAML content has no documents”). Anything else you send must be valid, non-null YAML.
The agent will remove the Kubernetes resources on its next poll. Verify:
kubectl get namespace tutorial-nginxAfter a few seconds, the namespace and all its contents will be gone.
Optionally, remove the agent target and delete the stack:
# Remove the targetcurl -s -X DELETE "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets/${STACK_ID}" \ -H "Authorization: Bearer <your-admin-pak>"
# Delete the stack (soft delete — marks as deleted but preserves the record)curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STACK_ID}" \ -H "Authorization: Bearer <your-admin-pak>"Note: Deletion in Brokkr is a “soft delete” — the record is marked with a
deleted_attimestamp but not removed from the database. Soft-deleting a stack also soft-deletes its deployment objects and automatically inserts a deletion marker, so deleting the stack alone is enough to clean its resources off the cluster. See Soft Deletion for details.
What You’ve Learned
Section titled “What You’ve Learned”- Stacks group related Kubernetes resources under a single name
- Deployment objects carry the YAML manifests inside a stack
- Agent targets connect agents to stacks, controlling which clusters receive which resources
- Sequence IDs let the agent know when a newer version is available
- Deletion markers trigger resource cleanup on the cluster
- Agents use a pull-based model — they poll the broker, so clusters behind firewalls work without inbound connections
Next Steps
Section titled “Next Steps”- Submitting a Folder of Manifests (CLI) —
brokkr apply -f ./manifests --stack <name> --generator <name>instead of hand-escaping ayaml_contentstring (the--generatorflag is for the admin PAK; a generator PAK does not need it) - Multi-Cluster Targeting — direct deployments to specific clusters using labels
- CI/CD with Generators — automate deployment pushes from a CI pipeline
- Managing Stacks — deeper guide on stack lifecycle management
- Configuration Guide — tune polling intervals, database settings, and more