Tutorial: CI/CD with Generators
In this tutorial, you’ll set up a generator — Brokkr’s mechanism for CI/CD integration — and use it to push deployments from a simulated pipeline. Generators are non-admin identities with scoped permissions, designed for automation.
What you’ll learn:
- What generators are and why they exist
- How to create a generator and manage its PAK
- How generators create and manage stacks
- How agents must register with a generator before being targeted to its stacks
- How to push deployment objects from a CI/CD pipeline
- Access control differences between admin and generator roles
Prerequisites:
- A broker you can reach at
http://localhost:3000— either a Helm install withkubectl port-forward svc/brokkr-broker 3000:3000running, or the local development environment (angreal local up) - Your admin PAK for that broker (see Adapting the commands to your install)
curlandjqinstalled- One existing agent, which Step 4 registers with your new generator. A broker-side record is enough to follow every step; a running agent attached to a cluster is what makes the pushed deployment actually appear there.
- Completed the Deploy Your First Application tutorial — including its Step 3 agent activation. Agents start
INACTIVEand apply nothing until activated; this tutorial assumes yours is alreadyACTIVE.
As in the first tutorial, export the agent’s name up front:
export AGENT_NAME=brokkr-integration-test-agent # development environment# export AGENT_NAME=my-agent # Helm install: your broker.agentNameStep 1: Create a Generator
Section titled “Step 1: Create a Generator”Generators represent automated systems (CI/CD pipelines, GitOps controllers, deployment scripts) that push resources to Brokkr. They have their own PAK (Prefixed API Key) and can only manage resources they own.
A generator is also an isolated application scope: any agent that will run a generator’s stacks must first be registered with that generator. Registration is the agent’s opt-in boundary — it prevents a stack from one application accidentally being targeted at an agent that never agreed to serve it. You’ll register an agent in Step 4 before targeting. For the full picture, see Generator Registration and Application Scopes.
Create a generator via the API (the brokkr-broker CLI can do this too — see the CLI Reference):
GENERATOR_RESPONSE=$(curl -s -X POST http://localhost:3000/api/v1/generators \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d '{"name": "github-actions", "description": "GitHub Actions deployment pipeline"}')
GENERATOR_ID=$(echo "$GENERATOR_RESPONSE" | jq -r '.generator.id')GENERATOR_PAK=$(echo "$GENERATOR_RESPONSE" | jq -r '.pak')
echo "Generator ID: $GENERATOR_ID"echo "Generator PAK: $GENERATOR_PAK"Save this PAK immediately — it’s only shown once. You’ll store it as a CI secret.
Why registration is needed only for application generators: Behind the scenes, every new agent is automatically registered with the built-in system generator (
__system__), which carries fleet-wide stacks that reach all agents without any explicit opt-in. The application generator you just created is different — its stacks may be proprietary or customer-specific, so an agent must explicitly register before they can be targeted at it.
Step 2: Create a Stack as the Generator
Section titled “Step 2: Create a Stack as the Generator”Switch to using the generator’s PAK. The generator can create stacks, and those stacks become “owned” by the generator. The request must carry a generator_id, and a generator PAK may only pass its own ID — anything else is rejected with a 403:
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \ -H "Authorization: Bearer ${GENERATOR_PAK}" \ -H "Content-Type: application/json" \ -d "{\"name\": \"myapp-v2\", \"description\": \"My application deployed via CI/CD\", \"generator_id\": \"${GENERATOR_ID}\"}" \ | jq -r '.id')
echo "Stack ID: $STACK_ID"The stack’s generator_id field is set to your generator’s ID. This ownership means:
- The generator can update and delete this stack
- The generator can push deployment objects to this stack
- Other generators cannot modify this stack
- Admins can still manage any stack
Step 3: See Generator Permissions in Action
Section titled “Step 3: See Generator Permissions in Action”Generators have scoped access — they can only manage resources they own. Let’s see this in practice:
# Generator can list its own stackscurl -s http://localhost:3000/api/v1/stacks \ -H "Authorization: Bearer ${GENERATOR_PAK}" | jq '.[].name'
# Generator CANNOT list agents (admin-only)curl -s http://localhost:3000/api/v1/agents \ -H "Authorization: Bearer ${GENERATOR_PAK}"# Returns: 403 ForbiddenThe key rule: generators can create, update, and delete their own stacks and push deployment objects to them, and they can target their own stacks at agents that are registered with them. They cannot list or manage agents, register agents, or touch other generators’ resources. Registering an agent with a generator is an admin operation (or the agent acting on itself) — a generator PAK can’t register agents on its own. See the Security Model for the complete access control matrix.
Step 4: Register and Target an Agent (So Deployments Reach a Cluster)
Section titled “Step 4: Register and Target an Agent (So Deployments Reach a Cluster)”Before pushing deployment objects, an agent must be targeted to the stack. Otherwise the deployment exists in the broker but no agent will apply it. Targeting has a prerequisite: the agent must first be registered with the stack’s generator. Targeting an unregistered agent is rejected with a 403 (agent_not_registered) — admins can’t bypass this gate either.
First, grab your agent’s ID by name and register it with your generator. (This is the agent you activated in the first tutorial — if it is still INACTIVE, deployments will sit in the broker unapplied.) With no agent_id in the body the agent would register itself; as admin you supply the agent_id you want to register:
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')
curl -s -X POST "http://localhost:3000/api/v1/generators/${GENERATOR_ID}/register" \ -H "Authorization: Bearer <your-admin-pak>" \ -H "Content-Type: application/json" \ -d "{\"agent_id\": \"${AGENT_ID}\"}" | jq .Re-running this registration returns 409 already_registered (harmless if the agent is already registered). Now target the agent at the stack. The generator does this itself, with its own PAK, because the agent is registered with it (an admin PAK works too):
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets" \ -H "Authorization: Bearer ${GENERATOR_PAK}" \ -H "Content-Type: application/json" \ -d "{\"agent_id\": \"${AGENT_ID}\", \"stack_id\": \"${STACK_ID}\"}" | jq .Note: Generators cannot manage agents or registrations — that requires admin access (or the agent acting on itself). A target for the generator’s own stack on a registered agent is the one agent-side call a generator may make. In production, an admin registers the agent once and tells the generator its id; the generator targets and pushes on its own.
Troubleshooting: A
403with codeagent_not_registeredmeans the agent isn’t registered with this stack’s generator. Register it withPOST /generators/{id}/register(as above), or start the agent with--generator-ids {id}so it self-registers. See the error-codes reference and the operational agent-registration how-to.
Alternative: Agent Self-Registration at Startup
Section titled “Alternative: Agent Self-Registration at Startup”Manual admin registration is convenient in a tutorial, but in production agents usually declare their generator scopes when they start, so no per-agent API call is needed. The agent resolves its scopes from, in precedence order:
- The
--generator-idsCLI flag (highest priority) - The
BROKKR__AGENT__GENERATOR_IDSenvironment variable (config keyagent.generator_ids) - The legacy bare
BROKKR_GENERATOR_IDSvariable (deprecated — still honored, but logs a warning)
Each is a comma-separated list of generator UUIDs; an empty value leaves the agent in system/fleet scope only. For example:
brokkr-agent start --generator-ids "${GENERATOR_ID}"Self-registration happens alongside the automatic system-generator registration, so a self-registering agent still serves fleet-wide stacks. See the environment-variables reference for the config keys and the agent-registration how-to for the full workflow.
Step 5: Push a Deployment (Simulating CI/CD)
Section titled “Step 5: Push a Deployment (Simulating CI/CD)”Now simulate what a CI/CD pipeline would do — push a deployment object after building an image:
First write the manifests to a file — in a real pipeline these come straight from your repo (or kustomize build / helm template output):
cat > myapp.yaml <<'YAML'apiVersion: v1kind: Namespacemetadata: name: myapp---apiVersion: apps/v1kind: Deploymentmetadata: name: myapp namespace: myapp labels: app: myappspec: replicas: 2 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myregistry.example.com/myapp:v1.2.3 ports: - containerPort: 8080 env: - name: VERSION value: v1.2.3YAMLThen submit the file as-is. With Content-Type: application/yaml the raw body is the manifest — no JSON envelope or \n-escaping:
# This is what your CI/CD pipeline would run after buildingcurl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer ${GENERATOR_PAK}" \ -H "Content-Type: application/yaml" \ --data-binary @myapp.yaml | jq '{id, sequence_id, yaml_checksum}'Each push creates a new deployment object with an incrementing sequence_id. The agent sees the new sequence and applies the latest version — allow one poll cycle (about 10 seconds in the development environment; the Helm chart’s default is 30 seconds) before checking the cluster.
Step 6: Simulate a Deployment Update
Section titled “Step 6: Simulate a Deployment Update”Push a new version (as a CI/CD pipeline would on the next merge):
# Bump the image tag in the same file and re-submit it.sed -i 's/v1\.2\.3/v1.3.0/g' myapp.yaml
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer ${GENERATOR_PAK}" \ -H "Content-Type: application/yaml" \ --data-binary @myapp.yaml | jq '{id, sequence_id, yaml_checksum}'Notice the sequence_id incremented. The agent will apply this new version.
Step 7: A Real GitHub Actions Workflow
Section titled “Step 7: A Real GitHub Actions Workflow”This is the one CI pattern in these docs; the generators how-to and the CLI page point here. The stack exists before the pipeline runs (the application is the stack; each push is a new deployment object in it), and the pipeline pushes the rendered manifest as YAML:
# .github/workflows/deploy.ymlname: Deploy to Brokkr
on: push: branches: [main]
env: BROKKR_URL: https://brokkr.example.com STACK_ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build and push image run: | docker build -t myregistry.example.com/myapp:${{ github.sha }} . docker push myregistry.example.com/myapp:${{ github.sha }}
- name: Generate deployment YAML run: | cat > deployment.yaml << 'YAML' apiVersion: v1 kind: Namespace metadata: name: myapp --- apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: myapp spec: replicas: 2 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myregistry.example.com/myapp:${{ github.sha }} ports: - containerPort: 8080 YAML
- name: Push to Brokkr run: | # Submit the rendered file as-is — application/yaml means no jq # escaping or JSON envelope. curl -sf -X POST "${BROKKR_URL}/api/v1/stacks/${STACK_ID}/deployment-objects" \ -H "Authorization: Bearer ${{ secrets.BROKKR_GENERATOR_PAK }}" \ -H "Content-Type: application/yaml" \ --data-binary @deployment.yamlFor a non-Rust-curl pipeline, the brokkr CLI is even simpler — brokkr apply -f ./manifests --stack <name> is idempotent (it re-submits only when the bundle changed), so it drops straight into a CI step.
Store the generator PAK as BROKKR_GENERATOR_PAK in your repository’s GitHub Actions secrets.
Step 8: Rotate the Generator PAK
Section titled “Step 8: Rotate the Generator PAK”PAKs should be rotated periodically (also possible via the brokkr-broker CLI — see the PAK Management guide):
GENERATOR_PAK=$(curl -s -X POST "http://localhost:3000/api/v1/generators/${GENERATOR_ID}/rotate-pak" \ -H "Authorization: Bearer ${GENERATOR_PAK}" | jq -r '.pak')echo "New PAK: ${GENERATOR_PAK}"The response contains the new PAK (captured above so the cleanup step below keeps working). Update your CI secrets immediately — the old PAK stops working the moment rotation succeeds.
Clean Up
Section titled “Clean Up”# Delete the stack (as generator)curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STACK_ID}" \ -H "Authorization: Bearer ${GENERATOR_PAK}"
# Delete the generator (requires admin)curl -s -X DELETE "http://localhost:3000/api/v1/generators/${GENERATOR_ID}" \ -H "Authorization: Bearer <your-admin-pak>"What You’ve Learned
Section titled “What You’ve Learned”- Generators are scoped identities for CI/CD pipeline integration
- Each generator gets its own PAK for authentication
- Generators own the stacks they create — other generators can’t modify them
- An agent must be registered with a generator before its stacks can be targeted at that agent — manually via the API or by self-registering at startup with
--generator-ids - Pushing deployment objects is as simple as a
curlPOST with YAML content - Sequence IDs ensure agents always apply the latest version
- Generator PAKs should be stored as CI secrets and rotated periodically
Next Steps
Section titled “Next Steps”- Standardized Deployments with Templates — reduce YAML duplication with templates
- Working with Generators — detailed generator management guide
- Generators Reference — complete API reference
- Security Model — understand the full authorization model