Ingest a streaming Kafka topic¶
Your order events don’t wait for a nightly batch — they land on a Kafka topic the moment each one happens. To get them into the lakehouse you need a continuous reader: connect to the brokers, subscribe to the topic, authenticate to a secured cluster, and keep streaming.
You could hand-write that reader — spark.readStream.format("kafka") with a
stack of .option(...) calls, a dbutils.secrets.get(...) for the
truststore password, then a create_streaming_table and an append_flow to
land the rows. Or you declare a type: kafka load and let Lakehouse Plumber
write that Lakeflow for you. That is the idea on every page: declare your ETL,
don’t hand-write it.
Let’s stream an orders topic off a TLS-secured Kafka cluster into a bronze
streaming table, without touching PySpark.
Before you start¶
You need Lakehouse Plumber installed and a project to work in. If you followed
the Get Started course you already have one; otherwise scaffold an empty project
first. This guide assumes the orders topic already exists on your Kafka
cluster, and that the truststore password is stored in a Databricks secret scope
named kafka — a Kafka load reads a live topic, it does not create it.
Declare the Kafka load¶
A pipeline in Lakehouse Plumber is a flowgroup: a short YAML file describing
a sequence of actions. This one has two — a load that streams the topic
into a view, and a write that appends the view into a streaming table.
Create pipelines/orders_stream.yaml:
# Ingest guide: stream a Kafka topic into a bronze streaming table.
# Action chain: load (kafka) -> write (streaming_table).
pipeline: bronze_ingest
flowgroup: orders_stream
actions:
# 1. Load: stream the "orders" topic from a TLS-secured Kafka cluster.
# Kafka is always a streaming source; the truststore password comes
# from a Databricks secret rather than living in the YAML.
- name: load_orders_kafka
type: load
source:
type: kafka
bootstrap_servers: "${kafka_bootstrap}"
subscribe: orders
options:
startingOffsets: earliest
kafka.security.protocol: SSL
kafka.ssl.truststore.location: "${truststore_path}"
kafka.ssl.truststore.password: "${secret:kafka/truststore-password}"
target: v_orders_kafka_raw
# 2. Write: append the raw Kafka envelope into a bronze streaming table.
- name: write_orders_bronze
type: write
source: v_orders_kafka_raw
write_target:
type: streaming_table
catalog: "${catalog}"
schema: "${bronze_schema}"
table: orders_raw
The load action names the cluster with bootstrap_servers and picks the
topic with subscribe. Kafka is always a streaming source, so there is no
batch alternative: LHP generates spark.readStream.format("kafka") and
readMode defaults to stream. Everything under options is passed
straight to the Kafka reader — startingOffsets: earliest reads the topic from
the beginning on the first run, and the three kafka.* keys turn on TLS and
point at the truststore.
The truststore password is a secret, not a literal. ${secret:kafka/truststore-password}
references the truststore-password key in the kafka secret scope; LHP
turns that into a dbutils.secrets.get(...) call at generation time, so the
password never appears in your YAML or in the generated Python. The other
${...} tokens are per-environment placeholders you resolve next.
subscribe is one of three subscription methods — the load action reference
covers subscribePattern (a topic regex) and assign (specific
partitions), the extra kafka.* authentication options for AWS MSK and Azure
Event Hubs, and offset controls like failOnDataLoss. This guide stays on the
single-topic case.
Point the tokens at your environment¶
The ${...} tokens resolve from a per-environment substitutions file. Create
substitutions/dev.yaml:
# Development environment tokens.
# Referenced from flowgroups with ${token} syntax.
dev:
catalog: dev_catalog
bronze_schema: bronze
kafka_bootstrap: kafka-dev.internal:9092
truststore_path: /Volumes/dev_catalog/kafka/truststore.jks
This is the only place environment-specific values live. To stream the same
topic from a production cluster later, you write a prod.yaml with the
production brokers, truststore, and catalog — the flowgroup itself never
changes.
Generate the pipeline¶
Now compile the YAML into Lakeflow code. Validate first, then generate:
$ lhp validate --env dev
✓ discover (0.01s)
✓ preflight (0.00s)
✓ bronze_ingest 0 files
✓ validate (0.32s)
1 validated · 0.3s
$ lhp generate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✓ bronze_ingest 1 file
✓ generate (0.36s)
✓ format (0.10s)
✓ monitoring (0.00s)
1 pipeline generated · 1 file · 0.5s
validate resolves every token and secret reference and checks the actions
before you commit to generating; generate writes the Python.
Read what Lakehouse Plumber wrote¶
Open generated/dev/bronze_ingest/orders_stream.py. This is the entire
output — nothing is hidden behind a runtime:
# Generated by LakehousePlumber
# Pipeline: bronze_ingest
# FlowGroup: orders_stream
from pyspark import pipelines as dp
# Pipeline Configuration
PIPELINE_ID = "bronze_ingest"
FLOWGROUP_ID = "orders_stream"
# ============================================================================
# SOURCE VIEWS
# ============================================================================
@dp.temporary_view()
def v_orders_kafka_raw():
"""Load data from Kafka topics at kafka-dev.internal:9092"""
df = (
spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka-dev.internal:9092")
.option("subscribe", "orders")
.option("startingOffsets", "earliest")
.option("kafka.security.protocol", "SSL")
.option(
"kafka.ssl.truststore.location", "/Volumes/dev_catalog/kafka/truststore.jks"
)
.option(
"kafka.ssl.truststore.password",
dbutils.secrets.get(scope="kafka", key="truststore-password"),
)
.load()
)
return df
# ============================================================================
# TARGET TABLES
# ============================================================================
# Create the streaming table
dp.create_streaming_table(
name="dev_catalog.bronze.orders_raw", comment="Streaming table: orders_raw"
)
# Define append flow(s)
@dp.append_flow(
target="dev_catalog.bronze.orders_raw",
name="f_orders_bronze",
comment="Append flow to dev_catalog.bronze.orders_raw",
)
def f_orders_bronze():
"""Append flow to dev_catalog.bronze.orders_raw"""
# Streaming flow
df = spark.readStream.table("v_orders_kafka_raw")
return df
It’s ordinary Lakeflow. The source view is a @dp.temporary_view wrapping
spark.readStream.format("kafka"), with every option(...) you declared
resolved in order and the token values substituted in. The secret became a bare
dbutils.secrets.get(scope="kafka", key="truststore-password") call — a live
lookup at run time, not a string. The target is a dp.create_streaming_table,
and a @dp.append_flow streams the view into it. You described a topic and a
target table; LHP wrote the reader, the secret lookup, and the two pieces of
plumbing that connect them.
Note
A Kafka read returns a fixed schema: key and value as raw binary
columns, plus topic, partition, offset, timestamp, and
timestampType. This flowgroup lands that envelope as-is in the bronze
orders_raw table — the durable, replayable record of what arrived. To turn
the binary value into typed columns, deserialize it downstream with a SQL
transform (CAST(value AS STRING) then from_json(...)), covered in the
SQL transform guide.
What you just did¶
Twenty-three lines of YAML. Sixty lines of Lakeflow Python. Zero lines of
PySpark from you — you never wrote readStream.format("kafka"), the six
.option(...) calls, the dbutils.secrets.get lookup, create_streaming_table,
or append_flow. And the output is code you own: version it, diff it, and open
it in the Databricks editor like anything else.
The payoff compounds. Subscribing to a second topic, switching brokers per environment, or authenticating with MSK IAM is a few more lines of YAML — not another sixty lines of Python each time.
What’s next¶
Deserialize the payload. The binary
valuecolumn becomes typed rows with a SQL transform —CAST(value AS STRING)andfrom_json(...)— readingstream(v_orders_kafka_raw). See the SQL transform guide.Authenticate to a managed broker. AWS MSK IAM and Azure Event Hubs OAuth are extra
kafka.sasl.*options on the same load, with credentials supplied as secrets. The load action reference documents both, end to end.See every Kafka option.
subscribePatternandassign,startingOffsets,failOnDataLoss, and the fullkafka.*set are in the load action reference, with their constraints.