Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cloud Storage Integration

Streaming Kafka data to cloud storage enables data lake architectures, long-term retention, and analytics workloads.


Object storage services—Amazon S3, Google Cloud Storage, Azure Blob Storage—have become the foundation of modern analytics platforms. Understanding why requires context on how data architecture has evolved.

EraStorageComputeCharacteristics
Traditional (pre-2010)SAN/NAS attached to serversTied to storageExpensive, limited scale, vendor lock-in
Hadoop era (2010s)HDFS distributed across nodesCo-located with dataBetter scale, but compute/storage coupled
Cloud-native (2020s)Object storage (S3, GCS)Separate query enginesIndependent scaling, pay-per-use, open formats

Object storage became the analytics standard because it solved fundamental problems:

ProblemObject Storage Solution
CostTypically lower cost than block storage; pay only for what’s stored
ScaleEffectively unlimited capacity; no cluster sizing decisions
DurabilityProvider-specific targets (for example S3/GCS advertise 11 nines); automatic replication across zones
Decoupled computeQuery with any engine (Spark, Presto, Athena, Snowflake) without moving data
Open formatsParquet, Avro, ORC readable by any tool; no vendor lock-in
Separation of concernsStorage team manages buckets; analytics teams query independently

A data lake is a central repository storing raw data in its native format until needed for analysis:

Data lake zones from raw to consumption in object storageData lake zones from raw to consumption in object storageObject Storage (S3/GCS)Raw Zone(as received)Curated Zone(cleaned, typed)Consumption Zone(aggregated)JSON logsCSV exportsEvent streamsParquet tablesSchema enforcedPartitionedBI datasetsML featuresAPI cachesKafka Connect(streaming)Spark/Flink(batch ETL)Analysts(queries)ETLaggregate

Kafka’s role is feeding real-time data into this architecture—streaming events directly to the raw zone as they occur.


Kafka feeding a real-time layer and a batch storage layerKafka feeding a real-time layer and a batch storage layerReal-Time LayerBatch LayerKafkaStream ProcessingCloud Storage(S3/GCS/Azure)AnalyticsBenefits:- Cost-effective long-term storage- Schema-on-read flexibility- Integration with analytics tools- Decoupled from Kafka retentionreal-timearchivebatch queries
Use CaseDescription
Data LakeCentral repository for analytics
Long-term RetentionStore data beyond Kafka retention
ComplianceAudit trails and regulatory archives
Batch ProcessingInput for Spark, Flink, Presto
Disaster RecoveryCross-region data backup

Stream directly from Kafka to cloud storage.

Direct sink pattern writing a Kafka topic to an S3 bucketDirect sink pattern writing a Kafka topic to an S3 bucketKafka ConnectS3 Sink ConnectorKafka Topic(events)S3 Bucket/data-lake/events/File structure:/year=2024/month=01/day=15/events-0-0000000000.parquetevents-0-0000001000.parquetconsumewrite Parquet

Configuration:

{
"name": "s3-events-sink",
"config": {
"connector.class": "io.confluent.connect.s3.S3SinkConnector",
"tasks.max": "6",
"topics": "events",
"s3.bucket.name": "data-lake",
"s3.region": "us-east-1",
"format.class": "io.confluent.connect.s3.format.parquet.ParquetFormat",
"partitioner.class": "io.confluent.connect.storage.partitioner.TimeBasedPartitioner",
"path.format": "'year'=YYYY/'month'=MM/'day'=dd/'hour'=HH",
"flush.size": "10000"
}
}

Kafka’s tiered storage offloads segments to cloud storage.

Tiered storage offloading segments from local disk to object storageTiered storage offloading segments from local disk to object storageKafka BrokerHot Tier(Local SSD)Cold Tier(S3)Tiered storage availabilitydepends on Kafka versionTransparent to producers/consumersControlled by topic configurationsegments olderthan threshold

Configuration:

# Broker configuration
remote.log.storage.system.enable=true
remote.log.manager.task.interval.ms=30000
remote.log.reader.threads=10
remote.log.reader.max.pending.tasks=100
# Topic configuration
remote.storage.enable=true
local.retention.ms=86400000
retention.ms=2592000000

Combine real-time and batch layers.

Lambda architecture with speed, batch, and serving layersLambda architecture with speed, batch, and serving layersSpeed LayerBatch LayerServing LayerKafka StreamsReal-time ViewsS3SparkBatch ViewsMerged ViewsKafka

Columnar format optimized for analytics queries.

CharacteristicValue
CompressionHigh (column encoding)
Query performanceExcellent (column pruning)
Schema supportEmbedded schema
Best forAnalytics, data warehousing

Configuration:

{
"format.class": "io.confluent.connect.s3.format.parquet.ParquetFormat",
"parquet.codec": "snappy"
}

Row-based binary format with schema.

CharacteristicValue
CompressionGood
Query performanceGood (row-based)
Schema supportEmbedded + Registry
Best forETL, schema evolution

Configuration:

{
"format.class": "io.confluent.connect.s3.format.avro.AvroFormat",
"avro.codec": "snappy"
}

Human-readable format.

CharacteristicValue
CompressionLow (text)
Query performancePoor (parse overhead)
Schema supportNone embedded
Best forDebugging, simple integrations

Configuration:

{
"format.class": "io.confluent.connect.s3.format.json.JsonFormat"
}
AspectParquetAvroJSON
File sizeSmallMediumLarge
Write speedMediumFastFast
Read speed (analytics)FastMediumSlow
Schema evolutionLimitedExcellentNone
Human readableNoNoYes

Organize data by time for efficient querying.

s3://bucket/topics/events/
├── year=2024/
│ ├── month=01/
│ │ ├── day=15/
│ │ │ ├── hour=00/
│ │ │ │ ├── events+0+0000000000.parquet
│ │ │ │ └── events+0+0000010000.parquet
│ │ │ └── hour=01/
│ │ │ └── events+0+0000020000.parquet

Configuration:

{
"partitioner.class": "io.confluent.connect.storage.partitioner.TimeBasedPartitioner",
"path.format": "'year'=YYYY/'month'=MM/'day'=dd/'hour'=HH",
"partition.duration.ms": "3600000",
"locale": "en-US",
"timezone": "UTC"
}

Partition by record field values.

s3://bucket/topics/events/
├── region=us-east/
│ ├── type=click/
│ │ └── events+0+0000000000.parquet
│ └── type=view/
│ └── events+0+0000005000.parquet
├── region=eu-west/
│ └── type=click/
│ └── events+0+0000002000.parquet

Configuration:

{
"partitioner.class": "io.confluent.connect.storage.partitioner.FieldPartitioner",
"partition.field.name": "region,type"
}

Combine time and field partitioning.

s3://bucket/topics/events/
├── region=us-east/
│ └── year=2024/month=01/day=15/
│ └── events+0+0000000000.parquet
├── region=eu-west/
│ └── year=2024/month=01/day=15/
│ └── events+0+0000001000.parquet

{
"flush.size": "10000"
}

Create new file every 10,000 records.

{
"rotate.schedule.interval.ms": "3600000"
}

Create new file every hour.

Example starting points; tune based on throughput and query patterns.

WorkloadFlush SizeRotation Interval
High volume (>10k msg/s)50,000-100,00010-30 minutes
Medium volume (1k-10k msg/s)10,000-50,00030-60 minutes
Low volume (<1k msg/s)1,000-10,0001-4 hours

Avoid many small files (>1000 files/partition/day)—impacts query performance.


Query S3 data directly with SQL.

CREATE EXTERNAL TABLE events (
event_id STRING,
event_type STRING,
timestamp BIGINT,
user_id STRING,
payload STRING
)
PARTITIONED BY (year STRING, month STRING, day STRING, hour STRING)
STORED AS PARQUET
LOCATION 's3://data-lake/topics/events/';
-- Load partitions
MSCK REPAIR TABLE events;
-- Query
SELECT event_type, COUNT(*)
FROM events
WHERE year = '2024' AND month = '01' AND day = '15'
GROUP BY event_type;

Automatically discover schema and partitions.

{
"Name": "events-crawler",
"Role": "arn:aws:iam::123456789:role/GlueRole",
"Targets": {
"S3Targets": [
{"Path": "s3://data-lake/topics/events/"}
]
},
"SchemaChangePolicy": {
"UpdateBehavior": "UPDATE_IN_DATABASE"
}
}
# Read from S3
events = spark.read.parquet("s3://data-lake/topics/events/")
# Query with partition pruning
recent_events = events.filter(
(events.year == "2024") &
(events.month == "01") &
(events.day == "15")
)
# Aggregate
recent_events.groupBy("event_type").count().show()

ClassUse CaseCost
S3 StandardFrequently accessed dataHighest
S3 Intelligent-TieringUnknown access patternsVariable
S3 Standard-IAInfrequently accessedLower
S3 GlacierArchive (>90 days)Lowest
{
"Rules": [
{
"ID": "ArchiveOldData",
"Status": "Enabled",
"Filter": {"Prefix": "topics/"},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
],
"Expiration": {"Days": 2555}
}
]
}
FactorImpactOptimization
StoragePer GB/monthCompression, lifecycle policies
RequestsPer PUT/GETLarger files, fewer rotations
Data transferPer GB outSame-region processing
Query scansPer TB scannedPartitioning, columnar format