Webhook icon
Commands icon
Docker icon
Script icon
If icon
Fail icon
SlackIncomingWebhook icon
Exit icon
Set icon
Pause icon
Switch icon

Move Kafka Partitions Safely with a Minimal Plan, a Throttle and a Rollback

Drain or fill a Kafka broker safely. Minimal reassignment plan, health checks, approval, replication throttle, cancel with rollback, throttle clean-up.

Categories
Infrastructure

Draining a broker before decommissioning it, or filling a new one, is a routine Kafka job that goes wrong in well-known ways. kafka-reassign-partitions --generate reshuffles replicas that never needed to move. An unthrottled move saturates the network and starves producers and consumers. A move started while partitions are under-replicated can remove the only in-sync copy. And the throttle you set is only removed by a --verify run after the move completes; forget it, and replication on those brokers stays throttled for months.

This blueprint runs the whole procedure with the stock Kafka tools and a small Python planner. It moves only the replicas that sit on brokers outside the target set, refuses to start on an unhealthy cluster, saves the current assignment for rollback, waits for a person to approve the numbers, follows the move with --verify until it is done or a time limit is reached, and a finally step makes sure no throttle is left behind.

How it works

  1. move_request (Webhook, body {"topics": "...", "target_brokers": "..."}) or a manual run with the same inputs. concurrency limit 1.
  2. inspect (shell.Commands, Kafka image): brokers that answer (and a check that every target broker is one of them), the current assignment and a --generate proposal, under-replicated and leaderless partitions, running reassignments, partition sizes (kafka-log-dirs), and broker throttle configs. Read-only.
  3. plan (python.Script):
    • Smallest move. A replica stays where it is if its broker is a target; only replicas on other brokers are replaced, in place, by the least loaded target broker. In the tests this moved 7 replicas where --generate proposed 7 to 11 for the same request (its plan is random).
    • Preconditions. No under-replicated or leaderless partition, no reassignment already running, enough target brokers for the replication factor.
    • Size. MiB each broker receives, and the time at the throttle.
    • Leftover throttles from an earlier move nobody verified are reported.
  4. preflight (If + Fail) stops with every problem found. anything_to_move (If + Exit) ends cleanly when the topics are already on the target brokers.
  5. save_rollback (kv.Set) stores the current assignment of every partition that will move.
  6. request_approval (Slack) with the moves, the MiB per broker and the estimate, then approve_move (Pause, 4 h; no decision means no move).
  7. move (shell.Commands):
    • checks again that nothing else started a move during the approval;
    • --execute --throttle;
    • --verify every 10 s, which is also what removes the throttle once every partition has moved;
    • at max_minutes, on_timeout: cancel runs --cancel and then re-applies the saved assignment. --cancel restores each partition's replica set but not always its order, which moves the preferred leader; in the tests 4 of 7 partitions came back in a different order until the saved assignment was re-applied (no data is copied for that).
  8. after_move checks every partition has the planned replicas (or the original ones after a cancel), and elects the preferred leaders of the moved partitions.
  9. report (Switch on done, cancelled, running): Slack, or Fail when the result does not match the plan or the move was cancelled.
  10. finally → throttle_guard, after every approved move: once no reassignment is running, it removes any replication throttle still set on a broker or on the topics; a broker that does not answer is named so its throttle can be removed when it is back.

What you get

  • Messages from the test runs:
    • "Kafka reassignment ready: 7 replica(s) of 7 partition(s) of orders,payments onto brokers 1,2,4 (the random plan of --generate for the same request moved 11). Data to copy: 154.1 MiB (broker 4: 154.1 MiB), about 0.3 min at 10 MiB/s per broker."
    • "Kafka reassignment done: 7 partition(s) of orders,payments now on brokers 1,2,4, 154.1 MiB in 28 s under a 10 MiB/s throttle. Throttle removed by --verify; preferred leaders elected; 0 under-replicated partition(s) in the cluster."
    • "FAILED at refuse: not started, nothing changed: 7 partition(s) are under-replicated (orders-0, orders-1, …): moving data now removes a replica that may be the only in-sync copy."
    • "FAILED at report_cancelled: not finished within 1 min (3 of 7 partitions had moved). Cancelled: every partition is back on its original replicas in the original order."
  • Flow outputs plan (moves, MiB per broker, estimate, problems, warnings) and result (state, seconds, partitions done, checks after the move).
  • KV kafka_reassignment_rollback: the assignment before the move, ready for kafka-reassign-partitions --execute.

Error handling

  • Refuses before changing anything on an unreachable cluster, an unknown topic or target broker, under-replicated or leaderless partitions, or a reassignment already running. The approval message names leftover throttles.
  • Re-checks after the approval, which can come hours later, that no other move started meanwhile.
  • Time limit. cancel stops the move and restores the original replicas and their order; keep_running leaves it running and says what to do, because its throttle stays until someone runs --verify.
  • The throttle never outlives the move. --verify removes it on completion; after a cancel, a failed task or a dead worker, the finally step removes it once nothing is moving and names any broker it could not reach. --cancel uses --preserve-throttles, because clearing throttles needs every broker to answer and a dead destination broker is a common reason for a move to overrun.
  • Every failure sends one Slack alert with the task, the reason and where the rollback assignment is.

What this blueprint teaches about Kafka and Kestra

  • Flow-level finally for a clean-up that must run after success, a decline, a timeout and a failure alike, guarded by an If so it only acts after an approved move.
  • Shell and Python tasks sharing files: the Kafka task saves raw tool output as outputFiles, the Python task reads them as inputFiles, so each step runs in the image that suits it.
  • Pause with a typed onResume input as an approval gate, Switch on a task output, and Exit to end a run cleanly when there is nothing to do.
  • kv.Set to keep a rollback plan outside the execution.
  • The webhook body overriding inputs through trigger.body.x ?? inputs.x in variables.

Prerequisites

  • A Kafka 2.6+ cluster (ZooKeeper or KRaft; --cancel needs 2.6), reachable from the Kestra worker. The Kafka tasks use networkMode: host and run as root in the Kafka image so they can write their files.
  • A Kestra worker that can run Docker containers.
  • A Slack incoming webhook.

Secrets

  • SLACK_WEBHOOK_URL.
  • KAFKA_REASSIGN_WEBHOOK_KEY: a long random string for the webhook.

Inputs

  • bootstrap_servers, topics, target_brokers (the webhook body can set the last two).
  • throttle_mb_per_sec (10), max_minutes (120), on_timeout (cancel or keep_running).
  • kafka_image (apache/kafka:3.9.1), python_image (python:3.12-slim).
  • The approval waits 4 hours (pauseDuration of approve_move).

Outputs

  • plan (JSON) and result (JSON), described above.

Quick start

  1. Start a four-broker KRaft cluster (nodes 1 to 3 are also controllers, node 4 is the new broker) and two topics on brokers 1 to 3:
    for n in 1 2 3 4; do
      if [ $n -le 3 ]; then roles=broker,controller; lst="PLAINTEXT://127.0.0.1:909$n,CONTROLLER://127.0.0.1:1909$n"; else roles=broker; lst="PLAINTEXT://127.0.0.1:909$n"; fi
      docker run -d --name kafka$n --network host -e KAFKA_HEAP_OPTS="-Xmx384m -Xms384m" -e CLUSTER_ID=4L6g3nShT-eMCtK--X86sw \
        -e KAFKA_NODE_ID=$n -e KAFKA_PROCESS_ROLES=$roles -e KAFKA_LISTENERS=$lst -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:909$n \
        -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
        -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT -e KAFKA_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:19091,2@127.0.0.1:19092,3@127.0.0.1:19093 \
        -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=3 apache/kafka:3.9.1
    done
    sleep 20; k="docker exec kafka1 /opt/kafka/bin"
    $k/kafka-topics.sh --bootstrap-server 127.0.0.1:9091 --create --topic orders --replica-assignment 1:2,2:3,3:1,1:3,2:1,3:2
    $k/kafka-topics.sh --bootstrap-server 127.0.0.1:9091 --create --topic payments --replica-assignment 1:2:3,2:3:1,3:1:2
    for t in orders:200000 payments:20000; do $k/kafka-producer-perf-test.sh --topic ${t%:*} --num-records ${t#*:} --record-size 1000 --throughput -1 --producer-props bootstrap.servers=127.0.0.1:9091; done
    
  2. Add the secrets and run with the defaults: it drains broker 3 onto broker 4. Read the plan in Slack and resume the execution with Start the reassignment now? checked.
  3. Real result from this cluster: 7 replicas, 154.1 MiB, done in 28 s under a 10 MiB/s throttle, throttle removed, preferred leaders elected.

How to extend

  • Rack awareness: give the planner each broker's rack (kafka-broker-api-versions prints it) and only pick a replacement in a rack the partition does not use yet.
  • Move one topic at a time from a Loop over the topic list, so each step needs less headroom.
  • Raise the throttle during quiet hours with a second flow that runs kafka-configs --alter on the brokers in the plan.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.