Skip to content

Managing Stacks

Stacks are Brokkr’s fundamental organizational unit: collections of related Kubernetes resources that belong together. This guide covers creating stacks, configuring labels and annotations for targeting, managing their lifecycle, and connecting them to agents and deployment objects.

A stack is a container for deployment objects—the versioned snapshots of Kubernetes resources you want to deploy—and the unit that agents target. See Core Concepts for how stacks fit into Brokkr’s architecture.

Every stack belongs to a generator, so generator_id is required when creating one. Generators use their own ID (the broker rejects anything else). Admins use the UUID of the auto-created admin-generator, which you can look up first:

Terminal window
GENERATOR_ID=$(curl -s http://localhost:3000/api/v1/generators \
-H "Authorization: Bearer $ADMIN_PAK" \
| jq -r '.[] | select(.name == "admin-generator") | .id')

Then create the stack with a name, optional description, and the generator ID:

Terminal window
curl -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"payment-service\",
\"description\": \"Payment processing microservice and dependencies\",
\"generator_id\": \"$GENERATOR_ID\"
}"

The response includes the stack’s UUID which you’ll use for all subsequent operations:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "payment-service",
"description": "Payment processing microservice and dependencies",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"generator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Stack names must be non-empty strings up to 255 characters, and they are unique across the whole broker — not per generator. Creating a stack whose name is already taken by a live stack fails with 409 Conflict and error code unique_violation:

{
"code": "unique_violation",
"message": "violates unique constraint `unique_stack_name`",
"details": { "constraint": "unique_stack_name" }
}

The constraint only covers stacks that have not been deleted, so deleting a stack frees its name for reuse.

Because the namespace is shared by every tenant on the broker, prefix names with the owning team or application (e.g. payments-api-gateway-v2 rather than api-gateway-v2). Doing so avoids collisions with another generator’s stacks — and avoids leaking that a name is already in use elsewhere.

Labels and annotations provide metadata for stacks that enables dynamic targeting and integration with external systems. Labels are simple string values used for categorization and selection. Annotations are key-value pairs for richer metadata.

Labels enable pattern-based targeting where agents with matching labels automatically receive stacks. Add a label with a POST request:

Terminal window
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '"env:production"'

A label is one string: non-empty, up to 64 characters, no whitespace. The broker compares labels as exact strings, so a stack labelled env:prod matches only an agent labelled env:prod; env=prod and prod are different labels. Use one shape everywhere, on stacks and on agents: key:value. The CLI’s --target-label flag and the Multi-Cluster Targeting tutorial use the same shape.

Purpose Example labels
Environment env:production, env:staging, env:development
Region region:us-east, region:eu-west, region:apac
Tier tier:frontend, tier:backend, tier:data
Criticality criticality:critical, criticality:standard

View all labels for a stack:

Terminal window
curl http://localhost:3000/api/v1/stacks/$STACK_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK"

Remove a label by its value:

Terminal window
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID/labels/production \
-H "Authorization: Bearer $ADMIN_PAK"

Annotations carry key-value metadata for integration with external systems or configuration hints:

Terminal window
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/annotations \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "{
\"stack_id\": \"$STACK_ID\",
\"key\": \"cost-center\",
\"value\": \"engineering-team-a\"
}"

Both keys and values must be non-empty strings up to 64 characters with no whitespace. Annotations are useful for:

Purpose Example
Cost allocation cost-center=team-alpha
Owner tracking owner=platform-team
SLA classification sla-tier=gold
External references jira-project=PLAT

View all annotations for a stack:

Terminal window
curl http://localhost:3000/api/v1/stacks/$STACK_ID/annotations \
-H "Authorization: Bearer $ADMIN_PAK"

Remove an annotation by its key:

Terminal window
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID/annotations/cost-center \
-H "Authorization: Bearer $ADMIN_PAK"

Targeting establishes the relationship between stacks and the agents that should manage them. Without either an explicit target or a shared label/annotation, an agent won’t receive deployment objects from a stack regardless of other configuration.

An agent can only be explicitly targeted at a stack (via direct assignment) once it is registered with that stack’s owning generator. Registration is the agent’s opt-in consent boundary; without it, the broker rejects the target with 403 agent_not_registered (admins cannot bypass this). Label and annotation matching is gated the same way, with no error: the broker leaves out the stacks of generators the agent is not registered with when it computes the agent’s matches, so a matching label on an unregistered agent delivers nothing. The stack’s owning generator may create the target itself, on an agent that is registered with it. See Register Agents with Generators for the full operational guide and the security model for why this boundary exists.

Before creating a direct target, confirm the agent is registered with the stack’s generator. List an agent’s registrations:

Terminal window
curl http://localhost:3000/api/v1/agents/$AGENT_ID/registrations \
-H "Authorization: Bearer $ADMIN_PAK"

If the stack’s generator is missing from that list, register the agent (admin PAK required):

Terminal window
brokkr register --agent $AGENT_ID --generator $GENERATOR_ID

To remove the agent’s scope later, brokkr deregister --agent $AGENT_ID --generator $GENERATOR_ID (destructive: it also drops the agent’s targets for that generator’s stacks and prunes those resources on the next reconcile). See Register Agents with Generators for self-registration at startup, Helm configuration, and the REST API equivalents.

Create a targeting relationship by associating an agent with a specific stack:

Terminal window
curl -X POST http://localhost:3000/api/v1/agents/$AGENT_ID/targets \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "{
\"agent_id\": \"$AGENT_ID\",
\"stack_id\": \"$STACK_ID\"
}"

Use direct assignment when the stack-agent mapping is fixed and explicit. If the broker returns 403 agent_not_registered, the agent is not registered with the stack’s generator—register it first (see Registration (Prerequisite) above).

Beyond direct assignment, stacks and agents are associated dynamically through their metadata: a stack is associated with an agent if it shares any label string with the agent, or any annotation key-value pair (OR semantics). The broker evaluates this match at query time whenever the agent polls—no targeting rows are created and no configuration step is required. Deployment objects from every matched stack flow into the agent’s target state automatically.

For example, to have every production agent receive a stack:

Terminal window
# Label the stack
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '"env:production"'
# Add the same label to each agent that should manage it
curl -X POST http://localhost:3000/api/v1/agents/$AGENT_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "{
\"agent_id\": \"$AGENT_ID\",
\"label\": \"production\"
}"

From the next poll onward, the agent’s target state includes the stack’s deployment objects. Removing the shared label (or annotation) dissolves the association just as automatically.

A single stack can be targeted by multiple agents, enabling multi-cluster deployment scenarios. Each agent independently polls for the stack’s deployment objects and applies them to its cluster. This is useful for:

  • High availability across regions
  • Disaster recovery setups
  • Consistent infrastructure across environments

Once a stack exists and is targeted to agents, you populate it with deployment objects containing Kubernetes resources.

The deployment object’s body is a single multi-document YAML stream (the complete desired state for the stack). The endpoint accepts it two ways.

Raw YAML (recommended). Send the manifest as the request body with Content-Type: application/yaml — no JSON wrapping or newline escaping:

Terminal window
curl -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/yaml" \
--data-binary @resources.yaml

Because the body is just a YAML stream, anything that emits one pipes straight in:

Terminal window
kustomize build ./overlay | curl -X POST ".../stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK" -H "Content-Type: application/yaml" --data-binary @-
helm template app ./chart -f values.yaml | curl ... --data-binary @-

To submit a deletion marker this way, add ?deletion_marker=true (the body may be empty):

Terminal window
curl -X POST ".../stacks/$STACK_ID/deployment-objects?deletion_marker=true" \
-H "Authorization: Bearer $ADMIN_PAK" -H "Content-Type: application/yaml" --data-binary ''

JSON envelope. The original form still works, with the YAML embedded as a string field:

Terminal window
curl -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg yaml "$(cat resources.yaml)" '{yaml_content: $yaml, is_deletion_marker: false}')"

Either way the broker validates the YAML parses on ingest (malformed input is rejected with 400 invalid_deployment_object) and computes the checksum. Each deployment object receives a sequence ID that guarantees ordering; the latest is the stack’s desired state.

You can also pull a deployment object back out as raw YAML with Accept: application/yaml:

Terminal window
curl ".../deployment-objects/$OBJECT_ID" -H "Authorization: Bearer $ADMIN_PAK" -H "Accept: application/yaml"

View all deployment objects in a stack:

Terminal window
curl "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK"

For standardized deployments, instantiate a template into a deployment object:

Terminal window
curl -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects/from-template" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"template_id": "template-uuid-here",
"parameters": {
"replicas": 3,
"image_tag": "v1.2.3"
}
}'

The template’s parameters are validated against its JSON Schema before rendering. If the template has labels, they must match the stack’s labels for instantiation to succeed.

The update endpoint takes the complete stack object (PUT replaces, it does not patch), so fetch the current record, modify the fields you want, and send the whole object back:

Terminal window
curl -s http://localhost:3000/api/v1/stacks/$STACK_ID \
-H "Authorization: Bearer $ADMIN_PAK" \
| jq '.name = "payment-service-v2" | .description = "Updated payment service with new features"' \
| curl -X PUT http://localhost:3000/api/v1/stacks/$STACK_ID \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d @-

Brokkr uses soft deletion for stacks. When you delete a stack, it’s marked with a deleted_at timestamp rather than being removed from the database:

Terminal window
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID \
-H "Authorization: Bearer $ADMIN_PAK"

Soft deletion does three things, atomically via a database trigger:

  • The stack stops appearing in list queries, while the underlying data remains intact for audit purposes
  • All of the stack’s deployment objects are soft-deleted along with it
  • An empty deletion marker deployment object is inserted automatically

The deletion marker is how cluster cleanup happens: agents targeting the stack receive it on their next poll and delete the stack’s resources rather than apply them. No manual step is needed—deleting the stack is sufficient.

You can also create a deletion marker yourself by submitting a deployment object with is_deletion_marker: true. This removes the stack’s resources from clusters while keeping the stack itself alive—useful for clearing out a stack you intend to repopulate. For stack deletion, the marker is created for you by the trigger described above.

Generators are external systems like CI/CD pipelines that create stacks programmatically. When a generator creates a stack, that stack’s generator_id links it to the creating generator, establishing ownership and access control.

Generators can only access stacks they created. This scoping ensures pipelines can’t accidentally modify resources belonging to other systems. See the Generators Guide for details on integrating CI/CD systems.

Deletion removes cluster resources: Deleting a stack automatically creates a deletion marker, and every targeted agent will delete the stack’s resources from its cluster. Confirm which agents are associated with the stack before deleting.

Shared labels create associations: Because any shared label or annotation associates a stack with an agent, an overly generic label (e.g., default) can unintentionally deploy a stack to agents you didn’t have in mind. Choose label values deliberately.