New to Kestra?
Use blueprints to kickstart your first workflows.
Detect Schema Registry subjects off the governed compatibility level or mode, pin them back behind an approval gate, then re-read and fail if any still drifts.
A Confluent Schema Registry decides whether a new schema version may be registered by checking the subject's compatibility level, then whether a producer may write at all by checking the subject's mode. Both are per subject settings with a global default. Both are changed by one HTTP call. Neither is reported anywhere once changed.
Set a subject to NONE for a one off migration then forget to put it back. Every breaking change after that is accepted in silence. The registry answers 200 with a new schema id, the producer ships, the consumers break on the next deploy. Leave a subject in READONLY or IMPORT after a restore. Every write is then refused with HTTP 422, which reads like a client bug rather than a registry setting. The registry serves both requests normally in both cases, so nothing raises it.
This blueprint reads both settings for every subject that backs a live Kafka topic, compares each against a baseline the operator declares as an input, pins the drifted ones back behind an approval gate, then re-reads both settings straight from the registry and fails the run if any targeted subject is still off baseline.
This blueprint was created by zkasuran.
Measured against Confluent Schema Registry 7.9.0 while building this.
NONE the registry reports is_compatible: true for a schema that adds a mandatory field with no default, then registers it as a new version. With the same subject at BACKWARD the identical change is refused with HTTP 409 and READER_FIELD_MISSING_DEFAULT_VALUE. The level is the whole safety net.GET /config/<subject> answers HTTP 404 with error_code 40408 when the subject carries no override, GET /mode/<subject> answers 404 with 40409. A 404 there means the subject inherits the global value. It is not a failure, which is why both reads allow a failed status and the code is part of the reading.PUT /config/<subject> takes the key compatibility while GET returns compatibilityLevel. Reading back what you just wrote needs the other name.READONLY or IMPORT back to READWRITE needs no force. Moving a subject that already holds schemas into IMPORT is refused with HTTP 422 and Cannot import since found existing subjects, so IMPORT is not a baseline a live registry can hold. A run that asks for it fails on the re-read rather than reporting a fix.auto_remediate true, dry_run false, a human resuming the Pause with approved=true. An unanswered resume reads as not approved.exempt_subjects are dropped before the audit, so a subject deliberately held at NONE for a migration is never read or written.GET of both endpoints per targeted subject after the writes. A rejected write fails the run instead of being reported as fixed.list_subjects (io.kestra.plugin.core.http.Request) reads GET /subjects, the subjects the registry serves right now.list_topics (io.kestra.plugin.kafka.TopicList, read-only) returns the live topics on the cluster. That set is what scopes the audit.read_global_config and read_global_mode read GET /config plus GET /mode. globals pulls out the two inherited values so every subject is judged against the same pair.scope keeps the registered subjects that follow the <topic>-value or <topic>-key naming for a live non internal topic. The subjects input replaces that derivation when it is set. exempt_subjects is dropped either way.in_scope (io.kestra.plugin.core.flow.If) skips straight to a log line when nothing is in scope.audit (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) reads GET /config/<subject> plus GET /mode/<subject> for each subject, with a failed status allowed so a 404 is read as inherited. Each iteration folds its row into one KV accumulator, which is reset at the start of every run.assess applies the baseline once to the whole reading. A subject drifts when its effective value differs from the baseline. It also drifts when inherited_is_compliant is false and it carries no override. Compatibility and mode are judged separately, so one subject can drift on either or both.route branches on whether anything drifted.all_clean logs that the net is on everywhere. No alert.log_findings logs the per subject reading, including the global values so the cause is visible, then alert posts it to Slack.remediate_gate runs only when auto_remediate is true and dry_run is false.approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved.remediate_pin (Loop, concurrencyLimit: 1) issues PUT /config/<subject> for a compatibility drift plus PUT /mode/<subject> for a mode drift, one subject at a time.verify then re-reads both endpoints for every targeted subject into a second accumulator, verify_assess applies the same rule to that reading, verify_result fails the run through io.kestra.plugin.core.execution.Fail if any targeted subject is still off baseline.not_approved logs that nothing changed.errors posts to Slack on any failure, including an unreachable registry plus a write the registry refused.curl can turn off compatibility checking for a whole subject.Reading two settings per subject is two HTTP calls. Doing it for every subject that backs a live topic, every day, against a declared baseline, is a pipeline. Kestra adds the schedule, the live topic set from the broker that scopes the audit, the Pause that turns a governance write into an approved change, the re-read that proves the write landed, the Slack alert, a history of every run you can point an auditor at.
On Kestra 2.0.5 OSS against Apache Kafka 3.9.1 (KRaft, single node) with Confluent Schema Registry 7.9.0 and a local HTTP sink standing in for Slack. Topics orders, payments, shipments, with four subjects: orders-value (compatibility override), orders-key (no override at all), payments-value (mode override), shipments-value (an override equal to the baseline). Every state was confirmed with GET /config/<subject> plus GET /mode/<subject> straight against the registry before and after each run.
| Run | Inputs | Result |
|---|---|---|
| 1 | defaults, orders-value at NONE, payments-value in READONLY |
4 audited, 2 off baseline (1 compatibility, 1 mode). Nothing written |
| 2 | auto_remediate: true, dry_run: true |
Same report, no Pause, registry unchanged |
| 3 | auto_remediate: true, dry_run: false, resumed with approved=false |
Paused, declined, nothing written |
| 4 | auto_remediate: true, dry_run: false, approved |
orders-value NONE to BACKWARD, payments-value READONLY to READWRITE, re-read confirmed both. orders-key plus shipments-value untouched |
| 5 | defaults on the repaired registry | 4 audited, 0 off baseline, all_clean, no alert |
| 6 | inherited_is_compliant: false |
4 off baseline (2 compatibility, 3 mode), since every subject misses at least one override |
| 7 | orders-value back at NONE, exempt_subjects: ["orders-value"] |
3 audited, 0 off baseline. The exempt subject was never read |
| 8 | baseline_mode: IMPORT, approved |
Every PUT /mode refused with HTTP 422. The re-read showed 4 of 4 still off baseline, the run FAILED, Slack alerted. The compatibility write in the same run did land |
| 9 | registry URL pointed at a closed port | list_subjects failed, run FAILED, errors alerted |
| 10 | subjects: ["no-such-subject"] |
Nothing in scope, nothing read, nothing written |
| 11 | global level set to NONE, defaults |
2 off baseline, both reported as inherited NONE. The two subjects with explicit BACKWARD overrides stayed clean |
| 12 | same, approved | Both pinned with a subject level BACKWARD override. The global level was left at NONE |
The detector was proven able to fail rather than only to pass: identical inputs gave 2 off baseline on the degraded registry (run 1) then 0 on the repaired one (run 5). The same registry gave 0 with the drifted subject exempt (run 7). The safety net itself was measured, not assumed: at NONE the registry accepted a schema adding a mandatory field with no default and bumped the subject to version 2, then after run 4 pinned it back to BACKWARD the same class of change was refused with HTTP 409.
/config plus /mode. Remediation also needs write on both. A broker principal allowed to list topics.PUT /mode/<subject> depends on the registry's mode.mutability setting, which the test registry for this blueprint was started with enabled.TopicNameStrategy for the topic scoped audit. Any other naming can be audited by listing the subjects in the subjects input.SLACK_WEBHOOK_URL: used by alert and the errors block. In Kestra OSS, provide it as the base64 encoded environment variable SECRET_SLACK_WEBHOOK_URL.| Input | Default | Purpose |
|---|---|---|
schema_registry_url |
http://localhost:8081 |
Registry base URL, no trailing slash |
bootstrap_servers |
localhost:9092 |
Brokers, read the live topic set |
baseline_compatibility |
BACKWARD |
The level every audited subject must resolve to |
baseline_mode |
READWRITE |
The mode every audited subject must resolve to |
inherited_is_compliant |
true |
False requires every audited subject to carry its own override |
subjects |
[] |
Audit these subjects instead of deriving them from live topics |
exempt_subjects |
[] |
Subjects allowed to sit off baseline |
auto_remediate |
false |
Allow the write |
dry_run |
true |
Must be false for a write to apply |
SLACK_WEBHOOK_URL secret.schema_registry_url plus bootstrap_servers.baseline_compatibility plus baseline_mode to the rule your team actually governs by. List any deliberate exception in exempt_subjects.auto_remediate: true plus dry_run: false, then resume the paused execution with approved=true.daily_scan trigger, which always runs in safe mode.inherited_is_compliant to false to require an explicit override on every governed subject, so a later change to the global level cannot quietly weaken them.schema_registry_url, so one Kestra instance governs several clusters on one schedule.NONE left behind by a migration opens a ticket as well as a Slack message.subjects input from your own inventory instead of letting live topics scope it.