์ด ๋ฌธ์๋ **์ค์ AWS ์ธํ๋ผ(MSK + ECS Fargate)**์์ log-server๋ฅผ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช
ํฉ๋๋ค.
๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ(docker-compose.local.yml)๊ณผ์ ์ฐจ์ด์ , ํ๊ฒฝ๋ณ์ ์ฃผ์
๋ฐฉ๋ฒ, Kafka Connect ๋ฐฐํฌ ์ ์ฐจ๋ฅผ ๋ด๊ณ ์์ต๋๋ค.
API Server (ECS)
โโ KafkaProducer (key=member_id)
โ
โผ
MSK (kafka.t3.small ร 2, Multi-AZ)
โโโ client-event-logs (ํํฐ์
3, replication 2)
โโโ error-logs / DLQ (ํํฐ์
1, replication 2)
โ
โโ Speed Layer (ECS Fargate 0.5vCPU)
โ โโ SpeedLayerConsumer (Spring Boot)
โ โโ click_product_detail๋ง ํํฐ๋ง (RecordFilterStrategy)
โ โโ product_view_history UPSERT (PostgreSQL)
โ โโ ํ์ฑ ์คํจ โ error-logs DLQ
โ
โโ Batch Layer (ECS Fargate 0.25vCPU)
โโ Kafka Connect Worker
โโ S3 Sink โ s3://<bucket>/events/raw/dt=.../hour=.../
| ํญ๋ชฉ | ๊ฐ | ๋น๊ณ |
|---|---|---|
| ๋ธ๋ก์ปค ํ์ | kafka.t3.small |
์ผ 30๋ง ๊ฑด(3.5 TPS) ์ถฉ๋ถ |
| ๋ธ๋ก์ปค ์ | 2 (Multi-AZ) | MSK ์ต์ ๊ตฌ์ฑ |
| Kafka ๋ฒ์ | 3.6.x | MSK ์ง์ ์ต์ ์์ ๋ฒ์ |
| ์คํ ๋ฆฌ์ง | EBS 100GB (๋ธ๋ก์ปค๋น) | 1์ผ ๋ณด์กด ๊ธฐ์ค ์ถฉ๋ถ |
| ์ธ์ฆ ๋ฐฉ์ | PLAINTEXT | ๋์ผ VPC ๋ด๋ถ ํต์ |
| ํ ํฝ ์๋ ์์ฑ | ๋นํ์ฑํ | ๋ช ์์ ์์ฑ ๊ฐ์ |
# MSK ๋ถํธ์คํธ๋ฉ ์ฃผ์ ํ์ธ
MSK_BS=$(aws kafka get-bootstrap-brokers \
--cluster-arn <MSK_CLUSTER_ARN> \
--query 'BootstrapBrokerString' --output text)
# click ๋ก๊ทธ ํ ํฝ
kafka-topics.sh --bootstrap-server $MSK_BS \
--create --if-not-exists \
--topic client-event-logs \
--partitions 3 \
--replication-factor 2 \
--config retention.ms=86400000
# DLQ ํ ํฝ
kafka-topics.sh --bootstrap-server $MSK_BS \
--create --if-not-exists \
--topic error-logs \
--partitions 1 \
--replication-factor 2MSK Security Group ์ธ๋ฐ์ด๋:
- Port 9092 (PLAINTEXT)
- Source: Speed Layer ECS Task SG
- Source: Kafka Connect ECS Task SG
- Source: API Server ECS Task SG
Speed Layer ECS Task SG ์์๋ฐ์ด๋:
- MSK SG โ 9092
- PostgreSQL RDS SG โ 5432
Kafka Connect ECS Task SG ์์๋ฐ์ด๋:
- MSK SG โ 9092
- S3 โ VPC Endpoint ๋๋ NAT Gateway
- CloudWatch Logs โ VPC Endpoint ๋๋ NAT Gateway
| ํญ๋ชฉ | ๋ก์ปฌ (docker-compose.local.yml) |
MSK ์ด์ |
|---|---|---|
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
b-1.xxx.kafka.ap-northeast-2.amazonaws.com:9092,b-2.xxx...:9092 |
DB_URL |
jdbc:postgresql://localhost:5435/holliverse |
jdbc:postgresql://<RDS_ENDPOINT>:5432/<DB_NAME> |
| ์ธํ๋ผ ์คํ | docker-compose.local.yml |
AWS MSK + RDS |
| Spring Boot ์ฝ๋ ๋ณ๊ฒฝ | ์์ | ์์ (ํ๊ฒฝ๋ณ์๋ง ๊ต์ฒด) |
ECS Task Definition์ environment ๋๋ AWS Secrets Manager/Parameter Store์ ๋ค์์ ์ฃผ์
ํฉ๋๋ค.
[
{ "name": "KAFKA_BOOTSTRAP_SERVERS", "value": "b-1.xxx.kafka.ap-northeast-2.amazonaws.com:9092,b-2.xxx.kafka.ap-northeast-2.amazonaws.com:9092" },
{ "name": "KAFKA_TOPIC_CLIENT_EVENTS", "value": "client-event-logs" },
{ "name": "KAFKA_TOPIC_ERROR", "value": "error-logs" },
{ "name": "KAFKA_GROUP_SPEED", "value": "speed-layer-group" },
{ "name": "KAFKA_MAX_POLL_RECORDS", "value": "1" },
{ "name": "KAFKA_DLQ_ACKS", "value": "all" },
{ "name": "KAFKA_DLQ_RETRIES", "value": "3" },
{ "name": "DB_URL", "value": "jdbc:postgresql://<RDS_ENDPOINT>:5432/<DB_NAME>" },
{ "name": "DB_USERNAME", "value": "<DB_USERNAME>" },
{ "name": "DB_PASSWORD", "value": "<DB_PASSWORD>" },
{ "name": "JPA_DDL_AUTO", "value": "validate" }
]๋ณด์:
DB_PASSWORD๋ฑ ๋ฏผ๊ฐ ๊ฐ์valueFrom์ผ๋ก AWS Secrets Manager ARN์ ์ฐธ์กฐํ๋ ๋ฐฉ์์ ๊ถ์ฅํฉ๋๋ค.
# application.yaml (์ฝ๋ ๊ธฐ์ค โ ์ด์ ์ ํ๊ฒฝ๋ณ์๋ก ๋ฎ์ด์)
spring:
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5435/holliverse}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:validate}
app:
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
topics:
client-events: ${KAFKA_TOPIC_CLIENT_EVENTS:client-event-logs}
error: ${KAFKA_TOPIC_ERROR:error-logs}
groups:
speed: ${KAFKA_GROUP_SPEED:speed-layer-group}
listener:
max-poll-records: ${KAFKA_MAX_POLL_RECORDS:1}
ack-mode: RECORD
producer:
dlq-acks: ${KAFKA_DLQ_ACKS:all}
dlq-retries: ${KAFKA_DLQ_RETRIES:3}| ํญ๋ชฉ | ๊ฐ |
|---|---|
| Launch type | Fargate |
| vCPU | 0.5 |
| Memory | 1 GB |
| ์ด๊ธฐ ํ์คํฌ ์ | 1 (์ต๋ 3, ํํฐ์ ์ ๊ธฐ์ค) |
| Consumer Group | speed-layer-group |
| ๋คํธ์ํฌ | VPC Private Subnet |
| ํญ๋ชฉ | ๊ฐ |
|---|---|
| ์ปจํ ์ด๋ ์ด๋ฏธ์ง | confluentinc/cp-kafka-connect:7.7.1 (S3 Sink ํ๋ฌ๊ทธ์ธ ํฌํจ) |
| vCPU | 0.25 |
| Memory | 0.5 GB |
| ํ์คํฌ ์ | 1 (๊ณ ์ ) |
| Consumer Group | s3-sink-group (Connect ๋ด๋ถ ์๋ ๊ด๋ฆฌ) |
ECS Task Role์ ๋ค์ ๊ถํ์ ๋ถ์ฌํฉ๋๋ค.
{
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::<bucket>",
"arn:aws:s3:::<bucket>/events/raw/*"
]
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:/aws/ecs/*"
}
]
}[
{ "name": "CONNECT_BOOTSTRAP_SERVERS", "value": "b-1.xxx.kafka.ap-northeast-2.amazonaws.com:9092,b-2.xxx.kafka.ap-northeast-2.amazonaws.com:9092" },
{ "name": "CONNECT_REST_PORT", "value": "8083" },
{ "name": "CONNECT_GROUP_ID", "value": "kafka-connect-s3-group" },
{ "name": "CONNECT_CONFIG_STORAGE_TOPIC", "value": "connect-configs" },
{ "name": "CONNECT_OFFSET_STORAGE_TOPIC", "value": "connect-offsets" },
{ "name": "CONNECT_STATUS_STORAGE_TOPIC", "value": "connect-status" },
{ "name": "CONNECT_KEY_CONVERTER", "value": "org.apache.kafka.connect.storage.StringConverter" },
{ "name": "CONNECT_VALUE_CONVERTER", "value": "org.apache.kafka.connect.storage.StringConverter" },
{ "name": "CONNECT_PLUGIN_PATH", "value": "/usr/share/java,/usr/share/confluent-hub-components" },
{ "name": "AWS_REGION", "value": "ap-northeast-2" }
]Connect ๋ด๋ถ ํ ํฝ(
connect-configs,connect-offsets,connect-status)๋ MSK์ ์๋ ์์ฑ๋ฉ๋๋ค.
์ด์ ์์ ์ฑ์ ์ํด ์ฌ์ ์ replication-factor=2๋ก ์๋ ์์ฑ์ ๊ถ์ฅํฉ๋๋ค.
{
"name": "s3-sink-client-event-logs-jsonl",
"config": {
"connector.class": "io.confluent.connect.s3.S3SinkConnector",
"tasks.max": "1",
"topics": "client-event-logs",
"s3.region": "${AWS_REGION}",
"s3.bucket.name": "${S3_BUCKET_NAME}",
"s3.part.size": "5242880",
"flush.size": "500",
"rotate.interval.ms": "300000",
"storage.class": "io.confluent.connect.s3.storage.S3Storage",
"format.class": "io.confluent.connect.s3.format.json.JsonFormat",
"compression.type": "gzip",
"partitioner.class": "io.confluent.connect.storage.partitioner.TimeBasedPartitioner",
"timestamp.extractor": "RecordField",
"timestamp.field": "timestamp",
"path.format": "'events/raw/dt='YYYY-MM-dd'/hour='HH",
"partition.duration.ms": "3600000",
"locale": "ko_KR",
"timezone": "UTC"
}
}{
"name": "s3-sink-client-event-logs-parquet",
"config": {
"connector.class": "io.confluent.connect.s3.S3SinkConnector",
"tasks.max": "1",
"topics": "client-event-logs",
"s3.region": "${AWS_REGION}",
"s3.bucket.name": "${S3_BUCKET_NAME}",
"s3.part.size": "5242880",
"flush.size": "500",
"rotate.interval.ms": "300000",
"storage.class": "io.confluent.connect.s3.storage.S3Storage",
"format.class": "io.confluent.connect.s3.format.parquet.ParquetFormat",
"parquet.codec": "snappy",
"partitioner.class": "io.confluent.connect.storage.partitioner.TimeBasedPartitioner",
"timestamp.extractor": "RecordField",
"timestamp.field": "timestamp",
"path.format": "'events/raw/dt='YYYY-MM-dd'/hour='HH",
"partition.duration.ms": "3600000",
"locale": "ko_KR",
"timezone": "UTC"
}
}| ์ค์ | ๊ฐ | ์๋ |
|---|---|---|
flush.size |
500 | 500๊ฑด ์์ด๋ฉด S3์ ํ๋ฌ์ |
rotate.interval.ms |
300000 (5๋ถ) | 500๊ฑด ๋ฏธ๋ง์ด์ด๋ 5๋ถ๋ง๋ค ๊ฐ์ ํ๋ฌ์ |
timestamp.field |
timestamp |
์ด๋ฒคํธ ์๊ฐ ๊ธฐ์ค์ผ๋ก dt/hour ํํฐ์ ๊ณ์ฐ |
partition.duration.ms |
3600000 (1์๊ฐ) | 1์๊ฐ ๋จ์ ํํฐ์ ๋๋ ํฐ๋ฆฌ ์์ฑ |
path.format |
events/raw/dt=YYYY-MM-dd/hour=HH |
Athena/ETL ํํฐ์ ํ์ค ๊ท์น |
export S3_BUCKET_NAME=holliverse-logs
export AWS_REGION=ap-northeast-2
export CONNECT_HOST=<connect-task-private-ip> # ECS Task IP ๋๋ ALB
# JSONL.gz ์ปค๋ฅํฐ ๋ฑ๋ก
envsubst < kafka-connect/s3-sink-connector-jsonl.gz.json \
| curl -s -X POST http://$CONNECT_HOST:8083/connectors \
-H "Content-Type: application/json" \
-d @-# ๋ฑ๋ก๋ ์ปค๋ฅํฐ ๋ชฉ๋ก
curl http://$CONNECT_HOST:8083/connectors
# ํน์ ์ปค๋ฅํฐ ์ํ (RUNNING ํ์ธ)
curl http://$CONNECT_HOST:8083/connectors/s3-sink-client-event-logs-jsonl/status
# ์ปค๋ฅํฐ ์ญ์ (์ฌ๋ฑ๋ก ์)
curl -X DELETE http://$CONNECT_HOST:8083/connectors/s3-sink-client-event-logs-jsonlaws s3 ls s3://holliverse-logs/events/raw/ --recursive | head -20
# ์์ ๊ฒฝ๋ก: events/raw/dt=2026-03-05/hour=12/part-00001.jsonl.gzMSK๋ VPC ๋ด๋ถ์์๋ง ์ ์ ๊ฐ๋ฅํ๋ฏ๋ก, ๋ก์ปฌ์์๋ docker-compose.local.yml์ Kafka๋ฅผ ์ฌ์ฉํฉ๋๋ค.
# 1. Kafka + Kafka UI + PostgreSQL ์ปจํ
์ด๋ ๊ธฐ๋
docker compose -f docker-compose.local.yml up -d
# 2. ๊ธฐ๋ ํ์ธ (์ฝ 10์ด ์์)
docker logs kafka --tail 20
# 3. ํ ํฝ ์์ฑ
chmod +x scripts/create-topics.sh
./scripts/create-topics.sh --mode local
# 4. Spring Boot ์คํ (application.yaml ๊ธฐ๋ณธ๊ฐ = localhost)
./gradlew bootRun# click_product_detail โ product_view_history์ UPSERT๋จ
docker exec -i kafka kafka-console-producer \
--bootstrap-server localhost:9092 \
--topic client-event-logs <<'EOF'
{"event_id":1000000000001,"timestamp":"2026-03-05T12:00:00Z","event":"click","event_name":"click_product_detail","member_id":45,"event_properties":{"page_url":"https://api.holliverse.site/api/v1/customer/plans","product_id":10,"product_name":"5G ์๊ธ์ ","product_type":"mobile","tags":["์์OTT","์ธ๊ธฐ"]}}
EOF
# event_name์ด ๋ค๋ฅธ ๊ฒฝ์ฐ โ RecordFilterStrategy์์ ํ๊ธฐ, DB ๋ฏธ์ ์ฌ
docker exec -i kafka kafka-console-producer \
--bootstrap-server localhost:9092 \
--topic client-event-logs <<'EOF'
{"event_id":1000000000002,"timestamp":"2026-03-05T12:01:00Z","event":"click","event_name":"page_view","member_id":45,"event_properties":{"product_id":99,"product_name":"๋ฌด์๋์","product_type":"etc","tags":[]}}
EOF
# event_id ํ์
์ค๋ฅ (String) โ ์ญ์ง๋ ฌํ ์คํจ โ error-logs DLQ๋ก ์ ์ก
docker exec -i kafka kafka-console-producer \
--bootstrap-server localhost:9092 \
--topic client-event-logs <<'EOF'
{"event_id":"uuid-1234-5678","timestamp":"2026-03-05T12:02:00Z","event":"click","event_name":"click_product_detail","member_id":45,"event_properties":{"product_id":47,"product_name":"ํ์
์ค๋ฅ","product_type":"mobile","tags":[]}}
EOF# PostgreSQL ์ ์
docker exec -it postgres psql -U loguser -d logdb
# ์ ์ฌ๋ ๋ ์ฝ๋ ํ์ธ
SELECT member_id, product_id, product_name, product_type, tags, viewed_at, last_event_id
FROM product_view_history
ORDER BY viewed_at DESC;
# ์ ์ ๋ณ ์ต๊ทผ ๋ณธ ์ํ 3๊ฐ ์กฐํ (RAG ์๋น์ฉ ์ฟผ๋ฆฌ)
SELECT product_id, product_name, product_type, tags, viewed_at
FROM product_view_history
WHERE member_id = 45
ORDER BY viewed_at DESC
LIMIT 3;docker exec -it kafka kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic error-logs \
--from-beginning| ํญ๋ชฉ | ๋ก์ปฌ | MSK ์ด์ |
|---|---|---|
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
b-1.xxx...:9092,b-2.xxx...:9092 |
DB_URL |
jdbc:postgresql://localhost:5435/holliverse |
jdbc:postgresql://<RDS_ENDPOINT>:5432/<DB> |
| Kafka ๊ธฐ๋ | docker-compose.local.yml |
AWS MSK ์๋ ๊ด๋ฆฌ |
| ํ ํฝ ์์ฑ | create-topics.sh --mode local |
create-topics.sh --mode msk |
| ๋ชจ๋ํฐ๋ง | Kafka UI (localhost:8081) | CloudWatch ์๋ ์ฐ๋ |
| Spring Boot ์ฝ๋ | ๋ณ๊ฒฝ ์์ | ๋ณ๊ฒฝ ์์ (ํ๊ฒฝ๋ณ์๋ง ๊ต์ฒด) |
| Kafka Connect | ๋ก์ปฌ ๋ฏธ์ฌ์ฉ (ํ ์คํธ ๋ถํ์) | ECS Fargate Task |