Sitemap
Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

Mastering Bindplane CLI and Fleet Management at Scale: From Zero to Bulk Agent Migrations

10 min readOct 8, 2026

--

Press enter or click to view image in full size
Bindplane CLI: Fleet Management at scale

A practical, battle-tested guide to installing the Bindplane CLI, managing collector fleets declaratively, and automating bulk fleet migrations across Linux, macOS, and Windows.

When you are managing a handful of OpenTelemetry collectors, clicking through a web UI to tweak sources or reassign configurations feels effortless. When that number grows to hundreds or thousands of collectors across Windows domain controllers, Linux application servers, and Kubernetes nodes, point-and-click administration quickly becomes a bottleneck.

That is where the Bindplane CLI (bindplane) and Fleets shine. Instead of treating each collector as an isolated pet, Fleets let you group agents by label selectors so they automatically inherit a standardized telemetry pipeline—while the CLI gives you scriptable, GitOps-friendly control from your terminal or CI/CD runner.

Get Simone Bruzzechesse’s stories in your inbox

Join Medium for free to get updates from this writer.

In this guide, we will walk through:

  1. Installing and configuring the Bindplane CLI across Linux, macOS, and Windows (including a subtle .bindplane directory gotcha on Windows).
  2. How Fleets work under the hood — including configuration priority, fallback behavior, and declarative YAML creation.
  3. A complete, real-world production use case: migrating a specific list of 50+ agents to a new Fleet in bulk using tested Bash and PowerShell automation scripts — plus how to avoid a subtle CLI exit-code trap.

Part 1: Installing and Configuring the Bindplane CLI

The bindplane client binary lets you remotely inspect collectors, manage labels, apply YAML configurations, and orchestrate phased rollouts against either Bindplane Cloud or a Self-Hosted Enterprise server.

Step 1: Install the Bindplane CLI

For Linux (amd64): Run the following commands to download the latest release archive, install the binary into /usr/local/bin, and create the required ~/.bindplane/ directory used by the CLI's local logger:

mkdir -p ~/bindplane
curl -L -o ~/bindplane/bindplane.zip https://storage.googleapis.com/bindplane-op-releases/bindplane/latest/bindplane-ee-linux-amd64.zip
unzip -o ~/bindplane/bindplane.zip -d ~/bindplane/
sudo mv ~/bindplane/bindplane /usr/local/bin/bindplane
mkdir -p ~/.bindplane/

For macOS (Apple Silicon arm64 & Intel amd64): Automatically detect your Mac architecture and install the binary:

mkdir -p ~/bindplane
ARCH=$(uname -m | sed 's/x86_64/amd64/')
curl -L -o ~/bindplane/bindplane.zip "https://storage.googleapis.com/bindplane-op-releases/bindplane/latest/bindplane-ee-darwin-${ARCH}.zip"
unzip -o ~/bindplane/bindplane.zip -d ~/bindplane/
sudo mv ~/bindplane/bindplane /usr/local/bin/bindplane
mkdir -p ~/.bindplane/

For Windows (amd64): Open PowerShell as Administrator and run the script below. Note that we explicitly create $env:USERPROFILE\.bindplane—without this folder, the CLI logger will fail to initialize on its first run:

$installPath = "$env:ProgramFiles\bindplane-cli"
if (-not (Test-Path -Path $installPath)) {
New-Item -Path $installPath -ItemType Directory | Out-Null
Write-Host "Folder created successfully: $installPath"
}

$configPath = Join-Path -Path $env:USERPROFILE -ChildPath ".bindplane"
if (-not (Test-Path -Path $configPath)) {
New-Item -Path $configPath -ItemType Directory | Out-Null
}
$currentMachinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
if ($currentMachinePath -notlike "*$installPath*") {
[System.Environment]::SetEnvironmentVariable("Path", "$currentMachinePath;$installPath", "Machine")
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
}
$downloadPath = Join-Path -Path "$env:USERPROFILE\Downloads" -ChildPath "bindplane-ee-windows-amd64.zip"
Invoke-WebRequest -Uri "https://storage.googleapis.com/bindplane-op-releases/bindplane/latest/bindplane-ee-windows-amd64.zip" -OutFile $downloadPath
Expand-Archive -Path $downloadPath -DestinationPath $installPath -Force

Step 2: Generate a Project API Key

To authenticate the CLI with Bindplane Cloud or Bindplane Enterprise Self-Hosted, generate an API key from the Web UI:

  1. Log into your Bindplane Web UI and open the Project page from the top-right settings menu.
  2. Select the API Key tab and click Generate New API Key.
  3. Copy the key immediately — it is only displayed once.
Press enter or click to view image in full size
1. Open the Project page from the top-right settings menu.
Press enter or click to view image in full size
2. Navigate to the API Key tab and click Generate New API Key.
Press enter or click to view image in full size
3. Copy the newly generated API key immediately.

Step 3: Configure Your CLI Profile

Bindplane CLI supports named profiles so you can easily switch between staging, production, or self-hosted environments. Configure your default profile and test your connection:

# 1. Attach your API key to the default profile
bindplane profile set default --api-key YOUR_API_KEY_HERE

# 2. Set the Remote URL (use https://app.bindplane.com for Bindplane Cloud,
# or http://<your-server-ip>:3001 for Self-Hosted Enterprise)
bindplane profile set default --remote-url https://app.bindplane.com
# Optional: If using a Scoped API Key (starts with bps_), also set --project:
# bindplane profile set default --remote-url https://app.bindplane.com \
# --api-key bps_YOUR_SCOPED_KEY --project YOUR_PROJECT_ID

# 3. Activate the default profile
bindplane profile use default

# 4. Verify client & server connectivity
bindplane version
bindplane get source-types

Part 2: How Bindplane Fleets Work Under the Hood

Before running bulk operations, it helps to understand the four rules that govern Fleets in Bindplane:

  • Configuration Priority: Collectors automatically inherit the configuration assigned to their Fleet. A Fleet configuration always takes priority over any individual configuration assigned to a collector.
  • Safe Fallback on Removal: Collectors can join or leave a Fleet without altering the Fleet’s configuration. If an agent leaves a Fleet, it automatically reverts to its previous individual fallback configuration (or a safe empty no-op configuration if none existed).
  • Label Selector Binding: A Fleet matches agents via selector.matchLabels (by convention, fleet: <fleet-name>). Labeling an agent with fleet=<fleet-name> immediately associates it with that Fleet.
  • Single-Fleet Membership: An agent can belong to only one Fleet at a time. When moving an agent from an existing Fleet to a new one via the CLI, you must pass --overwrite.

Inspecting and Filtering Collectors

You can list all collectors (aliased as agents or collectors in the CLI) and filter them using Bindplane's token-based query syntax (--query):

# List all agents and display all attached labels
bindplane get agents --show-all-labels

# Filter connected Windows agents
bindplane get agents --query "platform:windows status:Connected"

# Filter agents currently belonging to the 'windows' fleet
bindplane get agents --query "fleet:windows"

Creating a Fleet Declaratively via YAML

To manage Fleets as code, define a Fleet manifest (fleet-windows-secops-prod.yaml) and apply it with bindplane apply -f:

apiVersion: bindplane.observiq.com/v1
kind: Fleet
metadata:
name: windows-secops-prod
displayName: Windows SecOps Production Fleet
description: Standardized Windows Event Log telemetry pipeline for production servers
labels:
platform: windows
agent-type: observiq-otel-collector
spec:
configuration: windows-security-baseline
selector:
matchLabels:
fleet: windows-secops-prod

Apply the Fleet and ensure its assigned configuration has been rolled out:

bindplane apply -f fleet-windows-secops-prod.yaml
bindplane get fleets
bindplane rollout start windows-security-baseline
bindplane rollout status windows-security-baseline

Pro Tip: Always verify that the Fleet’s configuration has been rolled out. Until the rollout is active, collectors in the Fleet will continue running their individual configurations.

Part 3: Real-World Use Case — Bulk Updating the Fleet for a List of Agents

Imagine your Security Operations (SecOps) team has just finalized a hardened Windows telemetry pipeline under the Fleet windows-secops-prod. You are given a change ticket containing a list of 50 specific production agents (exported from a CMDB or maintenance wave) that are currently scattered across a legacy fleet (fleet=legacy-windows) or running standalone configs. Your goal is to migrate this exact list of agents to windows-secops-prod, log every change for compliance, and verify fleet health.

A Subtle CLI Trap to Watch Out For

When we tested bindplane label agent <id> fleet=windows-secops-prod --overwrite against the Bindplane API using a non-existent or mistyped Agent ID, we uncovered an important behavior: the server returns HTTP 200 with {"count": 0}, and the CLI exits with exit code 0 while printing:

0 collectors labeled

Meanwhile, a valid agent match prints:

1 collector labeled

If your automation script only checks if bindplane label agent ...; then (or $LASTEXITCODE -eq 0 in PowerShell), mistyped or stale Agent IDs will silently report as SUCCESS! In the scripts below, we explicitly assert both a zero exit code and that the output matches ^[1-9][0-9]* collector.

Step 1: Prepare Your Target Agent List

Create a text file named target_agents.txt with one Agent ID per line (comments starting with # and blank lines are ignored):

# Production Wave 1 - Windows Server Agent IDs
0195a3b0-eeaf-xxxx-898d-001122334455
0195a3b0-eeaf-xxxx-898d-001122334456
0195a3b0-eeaf-xxxx-898d-001122334457

What if your change ticket only lists hostnames (hostnames.txt) instead of Agent UUIDs? Because bindplane get agents -o json returns a clean JSON array of agent objects, you can resolve hostnames to Agent IDs automatically with jq:

while IFS= read -r host || [[ -n "$host" ]]; do
host=$(echo "$host" | tr -d '[:space:]')
[[ -z "$host" || "$host" =~ ^# ]] && continue
bindplane get agents --query "hostname:$host" -o json | jq -r '.[].id'
done < hostnames.txt > target_agents.txt

Method A: Audited Bulk Migration Script in Bash (Linux / macOS)

Save the following script as update_fleet_bulk.sh. It iterates through target_agents.txt, overwrites each agent's fleet label, verifies that at least 1 collector labeled was returned, writes a timestamped audit log, and queries the final Fleet state:

#!/usr/bin/env bash
set -euo pipefail

AGENT_LIST_FILE="${1:-target_agents.txt}"
TARGET_FLEET="${2:-windows-secops-prod}"
LOG_FILE="fleet_migration_$(date +%Y%m%d_%H%M%S).log"

# Ensure ~/.bindplane directory exists for CLI logger
mkdir -p ~/.bindplane

if [[ ! -f "$AGENT_LIST_FILE" ]]; then
echo "Error: Agent list file '$AGENT_LIST_FILE' not found." >&2
exit 1
fi

echo "Starting bulk fleet update to fleet='$TARGET_FLEET'..." | tee -a "$LOG_FILE"
SUCCESS_COUNT=0
FAIL_COUNT=0

while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
# Strip whitespace/CR and skip empty lines or comments
agent_id=$(echo "$raw_line" | tr -d '[:space:]')
[[ -z "$agent_id" || "$agent_id" =~ ^# ]] && continue

echo -n "Updating agent [$agent_id] -> fleet=$TARGET_FLEET ... "
if output=$(bindplane label agent "$agent_id" "fleet=$TARGET_FLEET" --overwrite 2>&1) && [[ "$output" =~ ^[1-9][0-9]*[[:space:]]+collector ]]; then
echo "SUCCESS ($output)"
echo "[SUCCESS] $agent_id: $output" >> "$LOG_FILE"
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
else
echo "FAILED ($output)"
echo "[ERROR] $agent_id: $output" >> "$LOG_FILE"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
done < "$AGENT_LIST_FILE"

echo ""
echo "Migration Summary: $SUCCESS_COUNT succeeded, $FAIL_COUNT failed. Full log: $LOG_FILE"

# Verify agents now reporting under the target fleet
echo "Verifying current agents in fleet '$TARGET_FLEET':"
bindplane get agents --query "fleet:$TARGET_FLEET"

Run the script:

chmod +x update_fleet_bulk.sh
./update_fleet_bulk.sh target_agents.txt windows-secops-prod

Fast Single-API-Call Alternative: Under the hood, bindplane label agent calls PATCH /v1/agents/labels and accepts multiple Agent IDs in a single command. If you don't need per-agent audit logging, you can update all IDs in target_agents.txt in a single HTTP request: bindplane label agent $(grep -vE '^\s*(#|$)' target_agents.txt | tr -d '\r') fleet=windows-secops-prod --overwrite

Method B: Audited Bulk Migration Script in PowerShell (Windows)

For Windows administrators, save the following script as Update-BindplaneFleet.ps1. It updates each agent in target_agents.txt and exports a structured CSV audit trail (fleet_migration_audit.csv):

param (
[string]$AgentListFile = ".\target_agents.txt",
[string]$TargetFleet = "windows-secops-prod"
)

$ConfigDir = Join-Path -Path $env:USERPROFILE -ChildPath ".bindplane"
if (-not (Test-Path -Path $ConfigDir)) {
New-Item -Path $ConfigDir -ItemType Directory | Out-Null
}

if (-not (Test-Path -Path $AgentListFile)) {
Write-Error "Agent list file not found: $AgentListFile"
exit 1
}

$SuccessCount = 0
$FailCount = 0
$Results = @()

$AgentIds = Get-Content -Path $AgentListFile | Where-Object {
$_.Trim() -ne "" -and -not $_.Trim().StartsWith("#")
}

Write-Host "Updating $($AgentIds.Count) agents to fleet=$TargetFleet..." -ForegroundColor Cyan

foreach ($RawId in $AgentIds) {
$AgentId = $RawId.Trim()
Write-Host -NoNewline "Assigning agent [$AgentId] -> fleet=$TargetFleet ... "

$Output = (& bindplane label agent $AgentId "fleet=$TargetFleet" --overwrite 2>&1) -join " "
if (($LASTEXITCODE -eq 0) -and ($Output -match "^[1-9]\d*\s+collector")) {
Write-Host "SUCCESS ($Output)" -ForegroundColor Green
$SuccessCount++
$Status = "Success"
} else {
Write-Host "FAILED ($Output)" -ForegroundColor Red
$FailCount++
$Status = "Failed"
}

$Results += [PSCustomObject]@{
Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
AgentId = $AgentId
TargetFleet = $TargetFleet
Status = $Status
Details = $Output
}
}

$Results | Export-Csv -Path ".\fleet_migration_audit.csv" -NoTypeInformation
Write-Host "`nCompleted: $SuccessCount succeeded, $FailCount failed. Audit saved to fleet_migration_audit.csv" -ForegroundColor Cyan

# Verify updated agents in the target fleet
bindplane get agents --query "fleet:$TargetFleet"

Method C: Dynamic Bulk Updates Using CLI Queries and Selectors

When your target collectors share common metadata — such as an existing Fleet name, OS platform, environment label, or hostname pattern — you don’t even need an input file. Use --query or --selector directly with bindplane label agent:

# 1. Preview the exact agents matching your filter first
bindplane get agents --query "platform:windows fleet:legacy-windows status:Connected"

# 2. Reassign all matching agents to the new fleet in one command
bindplane label agent \
--query "platform:windows fleet:legacy-windows status:Connected" \
fleet=windows-secops-prod \
--overwrite

# Alternative: Use a Kubernetes-style label selector
bindplane label agent \
--selector "environment=production,role=domain-controller" \
fleet=windows-secops-prod \
--overwrite

Post-Migration Verification & Instant Rollback

Once your agents have been reassigned, run these three checks to confirm every collector picked up the new Fleet configuration cleanly:

# 1. Confirm all migrated agents are listed in the new fleet
bindplane get agents --query "fleet:windows-secops-prod" --show-all-labels

# 2. Check if any agent in the fleet reported a configuration error
bindplane get agents --query "fleet:windows-secops-prod status:Error"

# 3. Inspect rollout progress
bindplane rollout status windows-security-baseline

Need to roll back an agent to its previous Fleet or remove its Fleet association altogether?

# Roll back an agent to its previous fleet
bindplane label agent <agent-id> fleet=legacy-windows --overwrite

# Or remove the fleet label entirely (reverts to individual fallback config)
bindplane label agent <agent-id> fleet- --overwrite

Part 4: 5 Best Practices for Bindplane Fleet Management

  1. Design One Fleet per Shared Telemetry Profile: Group machines that share the same OS platform and collection sources (e.g., linux-k8s-nodes, windows-domain-controllers, linux-nginx-edge).
  2. Assign Fleets at Install Time: Include the fleet=<fleet-name> label in your collector installation command so newly provisioned hosts join their target Fleet and inherit its pipeline immediately on first heartbeat.
  3. Use Phased Rollouts for Fleet Changes: Never edit Fleet agents individually. Update the Fleet’s configuration and use progressive rollouts (bindplane rollout start <config> --initial 5 --multiplier 2 --max-errors 3) to catch incompatibilities early.
  4. Standardize Label Taxonomy: Keep label keys consistent (fleet, environment, region, service) so CLI --query and --selector filters stay predictable across teams.
  5. Version-Control Your Fleets and Configs: Export configurations before making changes (bindplane get configuration <name> --export -o yaml > backup.yaml) and store both Fleet and Configuration YAML manifests in Git.

Further Reading

--

--

Google Cloud - Community
Google Cloud - Community

Published in Google Cloud - Community

A collection of technical articles and blogs published or curated by Google Cloud Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.