AWS Glue & Amazon Athena - 서버리스 데이터 레이크 구축
Glue로 데이터 카탈로깅하고 Athena로 SQL 쿼리하는 완벽 가이드
AWS Glue & Amazon Athena란?
AWS Glue는 데이터 준비 및 통합을 위한 서버리스 ETL(Extract, Transform, Load) 서비스임. 데이터 카탈로그를 생성하고 데이터를 변환하며, 다양한 데이터 소스를 통합함.
Amazon Athena는 S3에 저장된 데이터를 표준 SQL로 직접 분석할 수 있는 서버리스 대화형 쿼리 서비스임. 인프라 설정 없이 쿼리한 데이터양에 대해서만 비용 지불.
두 서비스를 함께 사용하면 서버리스 데이터 레이크를 구축할 수 있음.
왜 Glue & Athena를 사용해야 하는가?
기존 방식의 문제점
전통적인 데이터 웨어하우스 (Redshift 등):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────┐
│ 데이터 소스 (로그, 이벤트, IoT) │
│ ↓ │
│ ETL 서버 (EC2 + Apache Spark) │
│ ├─ 24시간 실행 │
│ ├─ 스케일링 관리 필요 │
│ └─ 장애 처리 복잡 │
│ ↓ │
│ Redshift Cluster │
│ ├─ 최소 노드: 2개 (ra3.xlplus) │
│ ├─ 월 비용: $1,086 │
│ ├─ 데이터 적재 시간 필요 │
│ └─ 스토리지 확장 시 노드 추가 필요 │
│ ↓ │
│ BI 도구 (Tableau 등) │
│ │
│ 총 월 비용: $1,500+ │
│ 데이터 적재: 수 시간 │
│ 운영 복잡도: 높음 │
└─────────────────────────────────────────┘
문제점:
- 높은 고정 비용: 사용하지 않아도 클러스터 비용 발생
- 데이터 적재 시간: ETL 후 웨어하우스에 로드 필요
- 스토리지 확장 제한: 노드 추가로만 확장 가능
- 운영 부담: 클러스터 관리, 백업, 패치
- 분석 지연: 배치 ETL 대기 시간
Glue & Athena의 해결 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────┐
│ 데이터 소스 │
│ ↓ │
│ S3 (Data Lake) │
│ ├─ raw-logs/ │
│ ├─ processed/ │
│ └─ analytics/ │
│ ↓ │
│ AWS Glue │
│ ├─ Crawler: 자동 스키마 발견 │
│ ├─ Data Catalog: 메타데이터 저장 │
│ └─ ETL Job: 데이터 변환 (필요 시만) │
│ ↓ │
│ Amazon Athena │
│ ├─ SQL 쿼리 (서버 없음) │
│ ├─ 스캔한 데이터만 과금 │
│ └─ 결과 → S3 │
│ ↓ │
│ BI 도구 / Lambda / QuickSight │
│ │
│ 월 비용: │
│ - Glue Crawler: $0.44/DPU-hour │
│ - Athena: $5/TB 스캔 │
│ - 실제: $20-50/월 (1TB 쿼리) │
│ │
│ 데이터 적재: 즉시 (S3 저장 = 쿼리 가능) │
│ 운영 복잡도: 매우 낮음 │
└─────────────────────────────────────────┘
장점:
- 완전 서버리스: 인프라 관리 불필요
- 종량제 과금: 사용한 만큼만 지불
- 즉시 쿼리: 데이터 적재 없이 S3에서 직접 쿼리
- 무한 확장: S3의 확장성 활용
- 비용 효율: 저빈도 분석 시 매우 저렴
AWS Glue 핵심 개념
1. Data Catalog
데이터의 메타데이터 저장소 (테이블 스키마, 파티션 정보 등).
구성 요소:
- Database: 테이블의 논리적 그룹
- Table: 데이터 소스의 스키마 정의
- Partition: 데이터를 효율적으로 쿼리하기 위한 분할 (예: 날짜별)
- Crawler: 데이터 소스를 스캔하여 자동으로 테이블 생성
2. Crawler
S3, RDS, DynamoDB 등의 데이터 소스를 스캔하여 스키마를 자동으로 감지하고 Data Catalog에 테이블 생성.
3. ETL Job
데이터를 변환하는 작업 (Python 또는 Scala 기반). Spark 엔진 사용.
4. Glue Studio
시각적 인터페이스로 ETL 작업을 생성하고 관리.
Amazon Athena 핵심 개념
1. Presto 기반 SQL 엔진
Athena는 Presto를 기반으로 하여 표준 SQL(ANSI SQL) 지원.
2. 스캔한 데이터양 과금
쿼리가 스캔한 데이터양에 대해서만 과금 ($5/TB).
3. 파티셔닝
파티션을 사용하면 스캔하는 데이터양을 줄여 비용 절감 및 성능 향상.
4. 쿼리 결과 저장
쿼리 결과는 S3에 저장되며, 재사용 가능.
실전 프로젝트 사례
1. 로그 분석 데이터 레이크 (data-lake-lab)
아키텍처:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
애플리케이션 로그
↓
Lambda (log-generator)
↓
S3 Bucket (data-lake)
├─ raw-logs/
│ └─ year=2024/month=12/day=01/*.json
├─ processed/
└─ athena-results/
↓
AWS Glue Crawler (자동 스캔)
↓
Glue Data Catalog
└─ Database: log_analytics_db
└─ Table: raw_logs (스키마 자동 생성)
↓
Amazon Athena (SQL 쿼리)
↓
쿼리 결과 → S3 (athena-results/)
↓
QuickSight / Lambda (시각화/알림)
Serverless Framework 설정:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# serverless.yml
provider:
name: aws
runtime: nodejs20.x
resources:
Resources:
# S3 Bucket (Data Lake)
DataLakeBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:service}-data-lake-${sls:stage}
LifecycleConfiguration:
Rules:
- Id: MoveToIA
Status: Enabled
Prefix: raw-logs/
Transitions:
- TransitionInDays: 30
StorageClass: STANDARD_IA
- TransitionInDays: 90
StorageClass: GLACIER
# Glue Database
GlueDatabase:
Type: AWS::Glue::Database
Properties:
CatalogId: !Ref AWS::AccountId
DatabaseInput:
Name: log_analytics_db_${sls:stage}
Description: Log analytics database
# Glue Crawler
LogDataCrawler:
Type: AWS::Glue::Crawler
Properties:
Name: log-data-crawler-${sls:stage}
Role: !GetAtt GlueCrawlerRole.Arn
DatabaseName: !Ref GlueDatabase
Targets:
S3Targets:
- Path: s3://${DataLakeBucket}/raw-logs/
Exclusions:
- "**.metadata"
SchemaChangePolicy:
UpdateBehavior: UPDATE_IN_DATABASE
DeleteBehavior: LOG
Configuration: |
{
"Version": 1.0,
"CrawlerOutput": {
"Partitions": {
"AddOrUpdateBehavior": "InheritFromTable"
}
}
}
Schedule:
ScheduleExpression: cron(0 1 * * ? *) # 매일 오전 1시 실행
# Glue Crawler IAM Role
GlueCrawlerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: glue.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
Policies:
- PolicyName: S3Access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:ListBucket
Resource:
- !GetAtt DataLakeBucket.Arn
- !Sub "${DataLakeBucket.Arn}/*"
# Athena Workgroup
AthenaWorkgroup:
Type: AWS::Athena::WorkGroup
Properties:
Name: ${self:service}-workgroup-${sls:stage}
State: ENABLED
WorkGroupConfiguration:
ResultConfiguration:
OutputLocation: s3://${DataLakeBucket}/athena-results/
EncryptionConfiguration:
EncryptionOption: SSE_S3
EnforceWorkGroupConfiguration: true
PublishCloudWatchMetricsEnabled: true
functions:
logGenerator:
handler: functions/log-generator.handler
events:
- schedule: rate(1 minute)
environment:
BUCKET_NAME: !Ref DataLakeBucket
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
Resource: !Sub "${DataLakeBucket.Arn}/*"
Lambda: 로그 생성 및 S3 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// functions/log-generator.handler
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3Client = new S3Client();
const BUCKET_NAME = process.env.BUCKET_NAME;
// 로그 타입
const LOG_TYPES = ['info', 'warning', 'error', 'debug'];
const SERVICES = ['api-server', 'worker', 'scheduler', 'webhook'];
function generateLog() {
const logType = LOG_TYPES[Math.floor(Math.random() * LOG_TYPES.length)];
const service = SERVICES[Math.floor(Math.random() * SERVICES.length)];
return {
timestamp: new Date().toISOString(),
logType,
service,
message: `Sample log message from ${service}`,
userId: `user-${Math.floor(Math.random() * 1000)}`,
requestId: `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
duration: Math.floor(Math.random() * 5000),
statusCode: logType === 'error' ? 500 : 200,
metadata: {
environment: 'production',
region: 'ap-northeast-2',
version: '1.0.0'
}
};
}
export const handler = async (event) => {
console.log('Generating logs...');
// 현재 날짜 기반 파티션 키
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
const hour = String(now.getUTCHours()).padStart(2, '0');
// 10개 로그 생성
const logs = Array.from({ length: 10 }, generateLog);
// S3에 저장 (파티션 구조: year/month/day/hour)
const key = `raw-logs/year=${year}/month=${month}/day=${day}/hour=${hour}/logs-${Date.now()}.json`;
await s3Client.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
Body: logs.map(log => JSON.stringify(log)).join('\n'), // JSONL 형식
ContentType: 'application/json'
}));
console.log(`Logs saved to: s3://${BUCKET_NAME}/${key}`);
return { statusCode: 200, logsGenerated: logs.length };
};
Glue Crawler 실행 (스크립트):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// scripts/run-crawler.mjs
import { GlueClient, StartCrawlerCommand, GetCrawlerCommand } from '@aws-sdk/client-glue';
const glueClient = new GlueClient({ region: 'ap-northeast-2' });
const CRAWLER_NAME = 'log-data-crawler-dev';
async function runCrawler() {
console.log('Starting Glue Crawler:', CRAWLER_NAME);
try {
await glueClient.send(new StartCrawlerCommand({
Name: CRAWLER_NAME
}));
console.log('Crawler started successfully');
// 크롤러 상태 확인
let state = 'RUNNING';
while (state === 'RUNNING') {
await new Promise(resolve => setTimeout(resolve, 10000)); // 10초 대기
const response = await glueClient.send(new GetCrawlerCommand({
Name: CRAWLER_NAME
}));
state = response.Crawler.State;
console.log('Crawler state:', state);
}
if (state === 'READY') {
console.log('Crawler completed successfully');
console.log('Tables updated:', response.Crawler.LastCrawl.Status);
} else {
console.error('Crawler failed:', state);
}
} catch (error) {
if (error.name === 'CrawlerRunningException') {
console.log('Crawler is already running');
} else {
throw error;
}
}
}
runCrawler();
Athena 쿼리 (스크립트):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// scripts/query-athena.mjs
import {
AthenaClient,
StartQueryExecutionCommand,
GetQueryExecutionCommand,
GetQueryResultsCommand
} from '@aws-sdk/client-athena';
const athenaClient = new AthenaClient({ region: 'ap-northeast-2' });
const DATABASE = 'log_analytics_db_dev';
const OUTPUT_LOCATION = 's3://data-lake-bucket/athena-results/';
async function executeQuery(query) {
console.log('Executing query:', query);
// 쿼리 실행
const startResponse = await athenaClient.send(new StartQueryExecutionCommand({
QueryString: query,
QueryExecutionContext: {
Database: DATABASE
},
ResultConfiguration: {
OutputLocation: OUTPUT_LOCATION
}
}));
const queryExecutionId = startResponse.QueryExecutionId;
console.log('Query execution ID:', queryExecutionId);
// 쿼리 완료 대기
let state = 'RUNNING';
while (state === 'RUNNING' || state === 'QUEUED') {
await new Promise(resolve => setTimeout(resolve, 2000)); // 2초 대기
const statusResponse = await athenaClient.send(new GetQueryExecutionCommand({
QueryExecutionId: queryExecutionId
}));
state = statusResponse.QueryExecution.Status.State;
console.log('Query state:', state);
if (state === 'FAILED') {
console.error('Query failed:', statusResponse.QueryExecution.Status.StateChangeReason);
return;
}
}
// 결과 조회
const resultsResponse = await athenaClient.send(new GetQueryResultsCommand({
QueryExecutionId: queryExecutionId
}));
console.log('Query results:');
console.log(JSON.stringify(resultsResponse.ResultSet.Rows, null, 2));
return resultsResponse.ResultSet.Rows;
}
// 예제 쿼리들
async function runExampleQueries() {
// 1. 로그 타입별 집계
await executeQuery(`
SELECT logtype, COUNT(*) as count
FROM raw_logs
GROUP BY logtype
ORDER BY count DESC
`);
// 2. 에러 로그 분석
await executeQuery(`
SELECT timestamp, service, message, userid
FROM raw_logs
WHERE logtype = 'error'
ORDER BY timestamp DESC
LIMIT 10
`);
// 3. 서비스별 평균 응답 시간
await executeQuery(`
SELECT service, AVG(duration) as avg_duration, COUNT(*) as request_count
FROM raw_logs
GROUP BY service
ORDER BY avg_duration DESC
`);
// 4. 특정 날짜 로그 조회 (파티션 활용)
await executeQuery(`
SELECT *
FROM raw_logs
WHERE year = '2024' AND month = '12' AND day = '01'
LIMIT 100
`);
// 5. 사용자별 에러 집계
await executeQuery(`
SELECT userid, COUNT(*) as error_count
FROM raw_logs
WHERE logtype = 'error'
GROUP BY userid
HAVING error_count > 5
ORDER BY error_count DESC
`);
}
runExampleQueries();
Athena 쿼리 예제 (SQL):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-- 1. 테이블 확인
SHOW TABLES IN log_analytics_db_dev;
-- 2. 스키마 확인
DESCRIBE raw_logs;
-- 3. 로그 타입 분포
SELECT logtype, COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM raw_logs
GROUP BY logtype
ORDER BY count DESC;
-- 4. 일별 에러 추이
SELECT year, month, day, COUNT(*) as error_count
FROM raw_logs
WHERE logtype = 'error'
GROUP BY year, month, day
ORDER BY year, month, day;
-- 5. 느린 요청 분석 (2초 초과)
SELECT timestamp, service, duration, message, requestid
FROM raw_logs
WHERE duration > 2000
ORDER BY duration DESC
LIMIT 100;
-- 6. 시간대별 로그 분포
SELECT hour, COUNT(*) as log_count
FROM raw_logs
GROUP BY hour
ORDER BY hour;
-- 7. JSON 필드 파싱 (메타데이터)
SELECT
logtype,
JSON_EXTRACT_SCALAR(metadata, '$.environment') as environment,
JSON_EXTRACT_SCALAR(metadata, '$.version') as version,
COUNT(*) as count
FROM raw_logs
GROUP BY logtype, environment, version;
-- 8. 특정 사용자 활동 추적
SELECT timestamp, logtype, service, message, statuscode
FROM raw_logs
WHERE userid = 'user-123'
ORDER BY timestamp DESC;
파티셔닝 전략
파티션의 중요성
Without Partition:
1
2
3
-- 전체 데이터 스캔 (10GB)
SELECT * FROM logs WHERE timestamp > '2024-12-01';
-- 비용: 10GB × $5/TB = $0.05
With Partition:
1
2
3
4
-- 특정 파티션만 스캔 (100MB)
SELECT * FROM logs
WHERE year = '2024' AND month = '12' AND day >= '01';
-- 비용: 100MB × $5/TB = $0.0005 (100배 절약!)
파티션 설계 모범 사례
1. 날짜 기반 파티션 (가장 일반적):
1
s3://bucket/logs/year=2024/month=12/day=01/hour=10/*.json
1
2
3
4
-- Glue Crawler가 자동으로 파티션 생성
-- Athena에서 자동으로 인식
SELECT * FROM logs
WHERE year = '2024' AND month = '12' AND day = '01';
2. 다차원 파티션:
1
s3://bucket/logs/region=us-east-1/logtype=error/year=2024/month=12/*.json
1
2
3
4
SELECT * FROM logs
WHERE region = 'us-east-1'
AND logtype = 'error'
AND year = '2024';
3. Hive 파티션 명명 규칙:
- 형식:
key=value - Glue Crawler가 자동 인식
- Athena에서 자동으로 파티션 프루닝
데이터 포맷 최적화
Parquet vs JSON
| 항목 | Parquet | JSON |
|---|---|---|
| 압축률 | 매우 높음 | 낮음 |
| 쿼리 속도 | 빠름 (열 기반) | 느림 (행 기반) |
| 스캔 데이터양 | 작음 | 큼 |
| 비용 | 저렴 | 비쌈 |
| 가독성 | 낮음 | 높음 |
| 사용 사례 | 프로덕션 | 개발/테스트 |
JSON → Parquet 변환 (Glue ETL Job):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Glue ETL Job (Python)
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
args = getResolvedOptions(sys.argv, ['JOB_NAME'])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
# JSON 데이터 읽기
datasource = glueContext.create_dynamic_frame.from_catalog(
database="log_analytics_db",
table_name="raw_logs"
)
# Parquet로 변환 및 저장
glueContext.write_dynamic_frame.from_options(
frame=datasource,
connection_type="s3",
connection_options={
"path": "s3://bucket/processed/",
"partitionKeys": ["year", "month", "day"]
},
format="parquet",
format_options={
"compression": "snappy"
}
)
job.commit()
성능 비교 (1TB 데이터):
1
2
3
4
5
6
7
8
9
10
11
JSON:
- 스토리지: 1TB
- Athena 스캔: 1TB × $5 = $5.00
- 쿼리 시간: 30초
Parquet (Snappy 압축):
- 스토리지: 200GB (80% 압축)
- Athena 스캔: 200GB × $5/TB = $1.00
- 쿼리 시간: 5초
절감: 스토리지 80%, 비용 80%, 시간 83%
Athena 성능 최적화
1. Columnar Format 사용
1
2
3
4
5
6
7
8
9
10
-- Parquet 테이블 생성
CREATE EXTERNAL TABLE logs_parquet (
timestamp string,
logtype string,
service string,
message string
)
PARTITIONED BY (year string, month string, day string)
STORED AS PARQUET
LOCATION 's3://bucket/processed/';
2. 필요한 컬럼만 선택
1
2
3
4
5
6
7
-- BAD: 모든 컬럼 조회
SELECT * FROM logs WHERE logtype = 'error';
-- 10GB 스캔
-- GOOD: 필요한 컬럼만
SELECT timestamp, message FROM logs WHERE logtype = 'error';
-- 2GB 스캔 (80% 절감)
3. LIMIT 사용
1
2
3
4
5
6
7
-- LIMIT는 스캔량 줄이지 않음!
SELECT * FROM logs LIMIT 100; -- 여전히 전체 스캔
-- 파티션 + LIMIT 조합
SELECT * FROM logs
WHERE year = '2024' AND month = '12'
LIMIT 100;
4. CTAS (Create Table As Select)
결과를 Parquet로 저장하여 재사용:
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE error_logs_summary
WITH (
format = 'PARQUET',
parquet_compression = 'SNAPPY',
partitioned_by = ARRAY['year', 'month']
) AS
SELECT
logtype, service, COUNT(*) as count,
year, month
FROM raw_logs
WHERE logtype = 'error'
GROUP BY logtype, service, year, month;
비용 최적화
실제 프로젝트 비용 분석
로그 분석 시스템 (월 기준, 1TB 로그):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AWS Glue:
- Crawler 실행: 1일 1회 × 0.1 DPU-hour × $0.44 × 30일 = $1.32
- Data Catalog 저장: 100만 객체 × $1/million = $1.00
- 총합: $2.32/월
Amazon Athena:
- 쿼리 스캔: 100GB/월 × $5/TB = $0.50
- (Parquet + 파티션 활용 시 10배 절감)
- 총합: $0.50/월
S3 스토리지:
- Standard: 1TB × $0.023/GB = $23.00
- Standard-IA (30일 후): 1TB × $0.0125/GB = $12.50
- 총합: ~$15/월 (평균)
전체 비용: $17.82/월
Redshift로 구현 시:
- Cluster (ra3.xlplus × 2): $1,086/월
- ETL 서버 (t3.large): $60/월
- 총합: $1,146/월
Glue + Athena 절감: $1,128.18/월 (98% 절감!)
비용 절감 팁
1. Parquet + Snappy 압축:
1
2
3
4
# Glue ETL에서 압축 설정
format_options={
"compression": "snappy" # 80% 압축률
}
2. 파티션 프루닝:
1
2
3
4
5
6
-- BAD: 파티션 없이 전체 스캔
SELECT * FROM logs WHERE timestamp > '2024-12-01';
-- GOOD: 파티션 활용
SELECT * FROM logs
WHERE year = '2024' AND month = '12' AND day >= '01';
3. 쿼리 결과 재사용:
1
2
3
-- 동일한 쿼리는 캐시에서 즉시 반환 (무료)
SELECT * FROM logs WHERE logtype = 'error'; -- 첫 실행: 과금
SELECT * FROM logs WHERE logtype = 'error'; -- 재실행: 무료 (캐시)
4. S3 Lifecycle 정책:
1
2
3
4
5
6
7
8
9
LifecycleConfiguration:
Rules:
- Id: MoveOldLogs
Status: Enabled
Transitions:
- TransitionInDays: 30
StorageClass: STANDARD_IA # 50% 저렴
- TransitionInDays: 90
StorageClass: GLACIER # 90% 저렴
Glue vs Athena vs 대안 비교
Glue + Athena vs Redshift
| 항목 | Glue + Athena | Redshift |
|---|---|---|
| 관리 | 완전 서버리스 | 클러스터 관리 |
| 가격 | 종량제 ($5/TB) | 고정 ($1,000+/월) |
| 성능 | 보통 (대량 스캔) | 빠름 (인덱스) |
| 확장성 | 무제한 | 노드 추가 필요 |
| 데이터 적재 | 불필요 (S3 직접) | ETL 필요 |
| 사용 사례 | 저빈도 분석 | 고빈도 분석 |
선택 기준:
- Glue + Athena: 저빈도 분석, 대량 데이터, 비용 중요
- Redshift: 고빈도 쿼리, 복잡한 조인, 성능 중요
Athena vs EMR
| 항목 | Athena | EMR |
|---|---|---|
| 설정 | 즉시 사용 | 클러스터 생성 필요 |
| 가격 | $5/TB 스캔 | $0.10/시간/노드 |
| 사용 사례 | 애드혹 쿼리 | 복잡한 처리 |
실전 경험에서 배운 것
1. Crawler는 스케줄보다 이벤트 기반
1
2
3
4
5
6
# BAD: 매일 실행 (빈 데이터도 크롤)
Schedule:
ScheduleExpression: cron(0 1 * * ? *)
# GOOD: S3 이벤트로 트리거
# Lambda → StartCrawler when new data arrives
2. 큰 테이블은 파티션 필수
파티션 없이 10TB 테이블 쿼리 시 $50 과금! 파티션 활용 시 $0.50.
3. Athena Named Queries 활용
자주 사용하는 쿼리는 저장:
1
2
-- Athena Console → Saved Queries
-- 팀원들과 공유 가능
4. Workgroup으로 비용 제어
1
2
3
WorkGroupConfiguration:
BytesScannedCutoffPerQuery: 10000000000 # 10GB 제한
EnforceWorkGroupConfiguration: true
마무리
AWS Glue와 Amazon Athena는 서버리스 데이터 레이크 구축의 핵심임. 인프라 관리 없이 S3에 저장된 대량의 데이터를 SQL로 분석하고, 사용한 만큼만 지불하여 비용 효율적임.
사용 권장 사항:
- 로그 분석 (CloudWatch 대신 장기 보관)
- IoT 데이터 분석
- 클릭스트림 분석
- 데이터 레이크 구축
최적화 핵심:
- Parquet + Snappy 압축
- 날짜 기반 파티션
- 필요한 컬럼만 선택
- S3 Lifecycle 정책
최근 프로젝트에서 Redshift 대신 Glue + Athena로 전환하여 월 $1,146 → $18로 비용을 98% 절감했고, 데이터 적재 없이 즉시 쿼리할 수 있어 매우 만족스러웠음.