Why This Matters
MSK Connect eliminates the operational burden of running Kafka Connect infrastructure. It enables seamless data movement between Apache Kafka and external systems like databases, S3, and search engines. Understanding MSK Connect architecture, connector patterns, and error handling is essential for building reliable streaming data integration pipelines that scale automatically and handle failures gracefully. MSK Connect supports hundreds of pre-built connectors and custom plugins for any data source.
MSK Connect Architecture
Real-World Project Structure
msk-connect-project/
+-- connectors/
� +-- rds-source/
� � +-- connector-config.json
� � +-- custom-plugin.zip
� � +-- deploy.sh
� +-- s3-sink/
� � +-- connector-config.json
� � +-- custom-plugin.zip
� � +-- deploy.sh
� +-- opensearch-sink/
� +-- connector-config.json
� +-- deploy.sh
+-- plugins/
� +-- debezium-postgres-2.4.zip
� +-- confluent-s3-sink-10.7.zip
� +-- custom-transforms.zip
+-- deploy/
� +-- create-connector.sh
� +-- update-connector.sh
� +-- delete-connector.sh
� +-- cfn-template.yaml
+-- monitoring/
� +-- cloudwatch-dashboard.json
� +-- alarms.yaml
� +-- custom_metrics.py
+-- tests/
+-- test-connectors.sh
+-- validate-config.py
Source vs Sink Connectors
| Aspect | Source Connector | Sink Connector |
|---|---|---|
| Direction | External system to MSK topic | MSK topic to external system |
| Common Use | CDC from databases, log ingestion | Data lake writes, search indexing |
| Offset Tracking | Source system position | Kafka consumer offset |
| Key SMTs | TimestampRouter, RegexRouter | Flatten, InsertField |
| Error Handling | Retry with backoff | DLQ for failed records |
| Throughput | Limited by source | Limited by sink |
Source Connector Configuration
{
"name": "my-rds-source",
"kafkaConnectVersion": "2.7.1",
"capacity": {
"provisionedThroughput": {
"readCapacityUnits": 10,
"writeCapacityUnits": 10
}
},
"plugin": {
"customPluginArn": "arn:aws:kafkaconnect:us-east-1:123456789012:custom-plugin/my-rds-connector"
},
"connectorConfiguration": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "my-rds-instance.cluster-xxxx.us-east-1.rds.amazonaws.com",
"database.port": "5432",
"database.user": "admin",
"database.password": "secretsmanager:arn:aws:secretsmanager:us-east-1:123456789012:secret:db-cred:password::",
"database.dbname": "mydb",
"database.server.name": "myserver",
"plugin.name": "pgoutput",
"topic.prefix": "cdc"
}
}
Sink Connector Configuration
{
"name": "my-s3-sink",
"kafkaConnectVersion": "2.7.1",
"capacity": {
"provisionedThroughput": {
"readCapacityUnits": 10,
"writeCapacityUnits": 10
}
},
"plugin": {
"customPluginArn": "arn:aws:kafkaconnect:us-east-1:123456789012:custom-plugin/my-s3-sink"
},
"connectorConfiguration": {
"connector.class": "io.confluent.connect.s3.S3SinkConnector",
"s3.bucket.name": "my-kafka-data-bucket",
"s3.region": "us-east-1",
"topics": "cdc.server1.users",
"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",
"flush.size": "1000",
"rotate.interval.ms": "60000"
}
}
Custom Connector Deployment
Step-by-Step Process
# Step 1: Package connector with dependencies
mvn clean package
mkdir -p /tmp/connector-plugin
cp target/my-custom-connector-1.0.jar /tmp/connector-plugin/
cp target/dependency/*.jar /tmp/connector-plugin/
cd /tmp/connector-plugin && zip -r my-custom-connector.zip .
# Step 2: Upload to S3
aws s3 cp my-custom-connector.zip s3://my-connectors-bucket/plugins/my-custom-connector.zip
# Step 3: Register as MSK Connect plugin
aws kafkaconnect create-custom-plugin \
--name my-custom-connector \
--content-type ZIP \
--location-s3-location "{
\"bucketArn\": \"arn:aws:s3:::my-connectors-bucket\",
\"objectKey\": \"plugins/my-custom-connector.zip\"
}"
# Step 4: Create connector
aws kafkaconnect create-connector \
--connector-name my-connector \
--connector-configuration file://connector-config.json \
--kafkaconnect-version 2.7.1 \
--connector-plugin "{
\"customPluginArn\": \"arn:aws:kafkaconnect:us-east-1:123456789012:custom-plugin/my-custom-connector\"
}" \
--capacity "{
\"provisionedThroughput\": {
\"readCapacityUnits\": 10,
\"writeCapacityUnits\": 10
}
}" \
--worker-configuration "{
\"workerConfigurationArn\": \"arn:aws:kafkaconnect:us-east-1:123456789012:worker-configuration/my-workers\"
}" \
--kafka-cluster "{
\"apacheKafkaCluster\": {
\"bootstrapServers\": \"b-1.mycluster.abc123.c2.kafka.us-east-1.amazonaws.com:9092\",
\"vpc\": {
\"securityGroups\": [\"sg-0123456789abcdef0\"],
\"subnets\": [\"subnet-0123456789abcdef0\"]
}
}
}"
Single Message Transforms (SMTs)
| SMT | Purpose | Example Use |
|---|---|---|
| InsertField | Add a field | Add processing timestamp |
| ReplaceField | Rename or remove fields | Drop PII columns |
| MaskField | Mask sensitive values | Mask email addresses |
| TimestampRouter | Route to time-based topics | Partition by date |
| RegexRouter | Route by pattern | Send to environment topics |
| HoistField | Wrap record in new field | Restructure payload |
{
"transforms": "insertTimestamp,maskEmail",
"transforms.insertTimestamp.type": "org.apache.kafka.connect.transforms.InsertField$Value",
"transforms.insertTimestamp.timestamp.field": "event_timestamp",
"transforms.maskEmail.type": "org.apache.kafka.connect.transforms.MaskField$Value",
"transforms.maskEmail.fields": "email",
"transforms.maskEmail.replacement": "*****"
}
Performance Considerations
| Metric | Description | Target |
|---|---|---|
bytesReadPerSec | Data read rate (source) | Match source capacity |
bytesWrittenPerSec | Data write rate (sink) | Match sink capacity |
recordsReadPerSec | Record read rate | Monitor for lag |
recordsWrittenPerSec | Record write rate | Monitor throughput |
ErrorCount | Number of errors | < 1% of records |
ProvisionedCapacityUnits | Current provisioned capacity | Match consumed |
ConsumedCapacityUnits | Actual consumed capacity | Below provisioned |
| Task Status | Running/Failed/Paused | All running |
Security Considerations
| Concern | Implementation |
|---|---|
| Authentication | IAM roles for MSK and connector access |
| Authorization | MSK ACLs for topic-level access |
| Encryption in Transit | TLS 1.2+ for worker-to-broker communication |
| Secrets Management | Reference Secrets Manager in connector configs |
| Network Security | VPC deployment, security groups, private subnets |
| Audit Logging | CloudTrail for all MSK Connect API calls |
| Access Controls | IAM policies with least-privilege |
| Data Residency | Deploy in specific regions |
Interview Questions and Answers
Q1: What is MSK Connect and how does it relate to Kafka Connect?
Answer: MSK Connect is a fully managed AWS service that runs Kafka Connect connectors on Amazon MSK clusters. Kafka Connect is the open-source framework for streaming data between Kafka and other systems. MSK Connect handles all the infrastructure management - provisioning workers, scaling, patching, and failover - so you only need to define connector configurations. It eliminates the operational overhead of self-managing Kafka Connect clusters.
Q2: What is the difference between a source connector and a sink connector?
Answer: A source connector reads data from an external system (like a database, S3 bucket, or API) and writes it to an MSK topic. A sink connector reads data from an MSK topic and writes it to an external destination (like S3, Redshift, or OpenSearch). Source connectors are the entry point for data into Kafka; sink connectors are the exit point. Understanding this distinction is fundamental to designing data flow architectures.
Q3: How does MSK Connect handle scaling?
Answer: MSK Connect automatically scales the number of workers based on throughput. You define minimum and maximum capacity bounds, and MSK Connect adds or removes workers to match actual demand. This is driven by CloudWatch metrics like ConsumedCapacityUnits vs. ProvisionedCapacityUnits. Start conservative with capacity, monitor metrics, and adjust bounds based on observed traffic patterns.
Q4: What is a Dead Letter Queue (DLQ) in MSK Connect?
Answer: A DLQ is a Kafka topic where records that cannot be processed by a sink connector are sent instead of being discarded. This commonly happens due to schema mismatches, serialization errors, or data validation failures. DLQs allow you to inspect failed records and reprocess them later without losing data. Configure with errors.tolerance: all and errors.deadletterqueue.topic.name.
Q5: What are Single Message Transforms (SMTs)?
Answer: SMTs are lightweight, record-level transformations that are applied as data flows through a connector. They run inside the connector worker process and are defined in the connector configuration. Common uses include adding timestamps, masking sensitive fields, renaming fields, and routing records to different topics. For heavier transformations, use Kafka Streams or Lambda instead.
Q6: How do you deploy a custom connector to MSK Connect?
Answer: (1) Develop and test your connector locally. (2) Package it as a JAR or ZIP with all dependencies. (3) Upload the package to an S3 bucket. (4) Register it as a custom plugin using the CreateCustomPlugin API. (5) Create a connector configuration that references the plugin ARN. MSK Connect will download and run the plugin on managed workers. Always version your plugins for rollback capability.
Q7: How does MSK Connect ensure exactly-once delivery?
Answer: For source connectors, MSK Connect tracks offsets in the external system and commits them atomically with Kafka produces, so records are not duplicated on restart. For sink connectors, Kafka consumer offsets are committed only after successful writes to the destination. This coordination ensures exactly-once semantics within the connector framework, though end-to-end exactly-once depends on the external system capabilities.
Q8: When would you use MSK Connect versus Kinesis Data Firehose?
Answer: Use MSK Connect when your data pipeline is centered around Apache Kafka and you need to integrate external systems with Kafka topics using the Kafka Connect ecosystem. Use Kinesis Data Firehose when you need a simpler, fully managed delivery stream to S3, Redshift, OpenSearch, or HTTP endpoints without managing a Kafka cluster. MSK Connect is better for complex, bidirectional Kafka-integrated workflows; Firehose is better for straightforward delivery pipelines.
Common Pitfalls
| Pitfall | Impact | Solution |
|---|---|---|
| Not configuring DLQ | Data loss on errors | Always enable DLQ with error tolerance |
| Ignoring SMT performance | Slow connector throughput | Use SMTs for lightweight ops only |
| Hardcoded secrets | Security risk | Use Secrets Manager references |
| Over-provisioning capacity | Unnecessary cost | Start conservative, monitor and adjust |
| Not versioning plugins | No rollback capability | Use S3 object versioning |
| Missing CloudWatch alarms | Blind to failures | Set up alarms for ErrorCount and status |
| Not testing locally | Production failures | Test with connect-distributed first |
| Ignoring task parallelism | Bottlenecks | Configure task count per connector |
See Also
Additional Deep Dive: Connector Lifecycle Management
Connector States
| State | Description | Action Required |
|---|---|---|
| RUNNING | Connector is processing data normally | Monitor metrics |
| PAUSED | Connector has been paused by user | Resume or investigate |
| FAILED | Connector has encountered an error | Check logs, fix config |
| RESTARTING | Connector is restarting after failure | Wait for completion |
| DELETING | Connector is being deleted | None |
| UPDATING | Connector is being updated | Wait for completion |
Connector Monitoring Script
import boto3
def monitor_msk_connectors(cluster_arn: str):
"""Monitor MSK Connect connectors and report health status."""
client = boto3.client('kafkaconnect')
try:
response = client.list_connectors(
kafkaClusterArn=cluster_arn
)
for connector in response.get('connectors', []):
name = connector['connectorName']
state = connector['connectorState']
print(f"Connector: {name}, State: {state}")
if state == 'FAILED':
print(f" WARNING: {name} is in FAILED state")
# Get connector status for more details
status = client.describe_connector(
connectorArn=connector['connectorArn']
)
print(f" Status: {status['connectorStatus']}")
except Exception as e:
print(f"Error monitoring connectors: {str(e)}")
raise
Scaling Configuration
import boto3
def update_connector_capacity(connector_arn: str, read_units: int, write_units: int):
"""Update MSK Connect connector capacity."""
client = boto3.client('kafkaconnect')
try:
response = client.update_connector(
connectorArn=connector_arn,
capacityUpdate={
'provisionedThroughput': {
'readCapacityUnits': read_units,
'writeCapacityUnits': write_units
}
}
)
print(f"Capacity updated: {response}")
except Exception as e:
print(f"Error updating capacity: {str(e)}")
raise
Common SMT Configurations
{
"transforms": "addTimestamp,maskPII,routeToTopic",
"transforms.addTimestamp.type": "org.apache.kafka.connect.transforms.InsertField$Value",
"transforms.addTimestamp.timestamp.field": "processed_at",
"transforms.maskPII.type": "org.apache.kafka.connect.transforms.MaskField$Value",
"transforms.maskPII.fields": "ssn,credit_card",
"transforms.maskPII.replacement": "MASKED",
"transforms.routeToTopic.type": "org.apache.kafka.connect.transforms.RegexRouter$Value",
"transforms.routeToTopic.regex": ".*",
"transforms.routeToTopic.replacement": "processed-${topic}"
}
Error Handling Configuration
{
"errors.tolerance": "all",
"errors.deadletterqueue.topic.name": "dlq-connect-errors",
"errors.deadletterqueue.topic.replication.factor": "3",
"errors.deadletterqueue.context.headers.enable": true,
"errors.log.enable": true,
"errors.log.include.messages": true,
"errors.log.exclude.messages": false,
"errors.retry.timeout": 30000,
"errors.retry.delay.max.ms": 60000
}
Worker Configuration
{
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": true,
"value.converter.schemas.enable": true,
"tasks.max": 10,
"heartbeat.interval.ms": 30000,
"session.timeout.ms": 180000,
"max.poll.interval.ms": 300000
}
Connector Health Check Script
#!/bin/bash
# MSK Connect health check script
CONNECTOR_NAME=$1
REGION=${2:-us-east-1}
echo "Checking health of connector: $CONNECTOR_NAME"
# Get connector status
STATUS=$(aws kafkaconnect describe-connector \
--connector-arn "arn:aws:kafkaconnect:${REGION}:123456789012:connector/${CONNECTOR_NAME}" \
--region ${REGION} \
--query 'connectorStatus.connectorState' \
--output text)
echo "Connector Status: $STATUS"
if [ "$STATUS" = "FAILED" ]; then
echo "CRITICAL: Connector is in FAILED state"
exit 1
elif [ "$STATUS" = "PAUSED" ]; then
echo "WARNING: Connector is paused"
exit 2
elif [ "$STATUS" = "RUNNING" ]; then
echo "OK: Connector is running normally"
exit 0
else
echo "UNKNOWN: Connector status is $STATUS"
exit 3
fi
CloudWatch Alarm Setup
# Create alarm for connector errors
aws cloudwatch put-metric-alarm \
--alarm-name "MSKConnect-HighErrorRate" \
--alarm-description "Alarm when MSK Connect error rate exceeds threshold" \
--metric-name "ErrorCount" \
--namespace "AWS/KafkaConnect" \
--statistic Sum \
--period 300 \
--evaluation-periods 2 \
--threshold 100 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions Name=ConnectorName,Value=my-connector \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--region us-east-1
# Create alarm for connector status
aws cloudwatch put-metric-alarm \
--alarm-name "MSKConnect-ConnectorFailed" \
--alarm-description "Alarm when connector enters FAILED state" \
--metric-name "ConnectorStatus" \
--namespace "AWS/KafkaConnect" \
--statistic Maximum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions Name=ConnectorName,Value=my-connector \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--region us-east-1
Performance Tuning Guide
| Configuration | Recommended Value | Impact |
|---|---|---|
| tasks.max | 5-10 per connector | Parallelism level |
| batch.size | 1000-5000 | Records per batch |
| linger.ms | 100-500 | Batch collection time |
| buffer.memory | 32MB-64MB | Buffer size |
| max.block.ms | 60000 | Max wait time |
| compression.type | lz4 | Compression for network |
| fetch.min.bytes | 1 | Minimum fetch size |
| fetch.max.wait.ms | 500 | Max wait for fetch |
Integration Patterns
| Pattern | Description | Use Case |
|---|---|---|
| CDC | Change Data Capture | Database replication |
| Event Sourcing | Immutable event log | Audit trails |
| Data Lake Ingestion | Batch and streaming | S3 data lake |
| Search Indexing | Real-time indexing | OpenSearch/Elasticsearch |
| Cache Population | Real-time cache updates | Redis/Memcached |
| API Integration | Event-driven APIs | Microservices |