Post

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+                    │
│  데이터 적재: 수 시간                   │
│  운영 복잡도: 높음                       │
└─────────────────────────────────────────┘

문제점:

  1. 높은 고정 비용: 사용하지 않아도 클러스터 비용 발생
  2. 데이터 적재 시간: ETL 후 웨어하우스에 로드 필요
  3. 스토리지 확장 제한: 노드 추가로만 확장 가능
  4. 운영 부담: 클러스터 관리, 백업, 패치
  5. 분석 지연: 배치 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

항목ParquetJSON
압축률매우 높음낮음
쿼리 속도빠름 (열 기반)느림 (행 기반)
스캔 데이터양작음
비용저렴비쌈
가독성낮음높음
사용 사례프로덕션개발/테스트

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 + AthenaRedshift
관리완전 서버리스클러스터 관리
가격종량제 ($5/TB)고정 ($1,000+/월)
성능보통 (대량 스캔)빠름 (인덱스)
확장성무제한노드 추가 필요
데이터 적재불필요 (S3 직접)ETL 필요
사용 사례저빈도 분석고빈도 분석

선택 기준:

  • Glue + Athena: 저빈도 분석, 대량 데이터, 비용 중요
  • Redshift: 고빈도 쿼리, 복잡한 조인, 성능 중요

Athena vs EMR

항목AthenaEMR
설정즉시 사용클러스터 생성 필요
가격$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% 절감했고, 데이터 적재 없이 즉시 쿼리할 수 있어 매우 만족스러웠음.

This post is licensed under CC BY 4.0 by the author.