Amazon Athena로 S3 로그 직접 쿼리하기: 수백만 개의 로그 파일을 DB 없이 분석하는 방법

S3에 수백만 개의 로그 파일이 쌓여 있는데, 이걸 분석하려면 RDS나 Redshift에 적재해야 한다고 생각했다면 — 그 가정이 틀렸다. Amazon Athena는 S3에 저장된 파일을 그대로 두고 SQL로 직접 쿼리하는 서버리스 쿼리 엔진이다. 데이터 이동 없이, 별도 DB 프로비저닝 없이, 스캔한 데이터 양만큼만 비용을 낸다.

TL;DR — S3 로그를 Athena로 쿼리하는 핵심 요약

단계작업핵심 포인트
1S3 버킷 및 로그 구조 확인파티션 키 설계가 쿼리 비용 직결
2Glue Data Catalog에 테이블 정의SerDe 선택이 파싱 성패를 결정
3Athena 쿼리 결과 버킷 설정결과 저장 위치 없으면 쿼리 실행 불가
4파티션 등록MSCK REPAIR 또는 파티션 프로젝션 사용
5SQL 쿼리 실행WHERE 절에 파티션 컬럼 반드시 포함
6비용 최적화Parquet 변환 + 압축으로 스캔 비용 절감

Athena가 S3 로그를 쿼리하는 방식

Athena는 Apache Hive 메타스토어 호환 카탈로그(AWS Glue Data Catalog)에 테이블 스키마와 S3 위치를 등록해 둔다. 쿼리를 실행하면 Athena가 카탈로그에서 해당 테이블의 S3 경로를 조회하고, 분산 쿼리 엔진(Presto/Trino 기반)이 S3에서 직접 파일을 읽어 처리한다. 데이터는 S3에 그대로 있고, Athena는 스캔만 한다.

graph LR User["사용자 / 애플리케이션"] -->|"SQL 제출"| Athena["Amazon Athena
(Presto/Trino 엔진)"] Athena -->|"스키마 및 S3 경로 조회"| Glue["Glue Data Catalog"] Glue -->|"테이블 메타데이터 반환"| Athena Athena -->|"파티션 프루닝 후 파일 스캔"| S3Logs["S3 로그 버킷
app-logs/year=2024/..."] S3Logs -->|"파일 데이터 반환"| Athena Athena -->|"쿼리 결과 저장"| S3Results["S3 결과 버킷
query-results/"] User -->|"결과 조회"| S3Results
  1. SQL 제출: 사용자가 Athena 콘솔 또는 API로 SQL을 제출한다.
  2. 카탈로그 조회: Athena가 Glue Data Catalog에서 테이블 스키마와 S3 경로를 읽는다.
  3. 파티션 프루닝: WHERE 절의 파티션 조건으로 스캔 대상 S3 prefix를 좁힌다.
  4. 분산 스캔: 분산 워커가 S3에서 파일을 병렬로 읽고 SerDe로 파싱한다.
  5. 결과 저장: 쿼리 결과가 지정된 S3 결과 버킷에 CSV로 저장된다.
Athena를 '쿼리 엔진이 S3로 출장 가는 것'으로 이해하면 편하다. 데이터가 엔진으로 오는 게 아니라, 엔진이 데이터 있는 곳으로 간다. 그래서 데이터 이동 비용이 없고, 스캔 비용만 발생한다.

S3 로그 구조와 파티션 설계 — Athena 쿼리 비용의 핵심

Athena 쿼리 비용은 스캔한 데이터 양에 비례한다. 파티션을 잘못 설계하면 수백 GB를 풀스캔하는 쿼리가 나온다. 로그 파일을 S3에 저장할 때부터 Hive 스타일 파티션 경로를 사용하는 것이 권장된다.

Hive 스타일 파티션 경로 예시:

s3://my-log-bucket/app-logs/year=2024/month=01/day=15/hour=09/logfile_001.json
s3://my-log-bucket/app-logs/year=2024/month=01/day=15/hour=09/logfile_002.json

이 구조를 사용하면 WHERE year='2024' AND month='01' AND day='15' 조건만으로 해당 날짜 파일만 스캔한다. 날짜 구분 없이 평탄하게 저장된 로그라면 모든 쿼리가 전체 버킷을 풀스캔하게 된다 — 이게 청구서 폭탄의 주원인이다.

Athena로 S3 로그 쿼리하기: 단계별 설정

1단계: Athena 쿼리 결과 버킷 설정

Athena는 쿼리 결과를 S3에 저장한다. 이 위치를 먼저 설정하지 않으면 어떤 쿼리도 실행되지 않는다. Athena 워크그룹별로 결과 위치를 강제 지정할 수 있어, 팀별 비용 추적에도 유용하다.

aws athena update-work-group \
  --work-group primary \
  --configuration-updates 'ResultConfigurationUpdates={OutputLocation=s3://my-athena-results-bucket/query-results/}' \
  --region us-east-1

2단계: Glue Data Catalog에 데이터베이스 생성

Athena 콘솔의 쿼리 편집기에서 직접 DDL을 실행하거나, AWS CLI로 Glue 데이터베이스를 생성한다.

aws glue create-database \
  --database-input '{"Name": "app_logs_db", "Description": "Application log database for Athena queries"}' \
  --region us-east-1

3단계: 테이블 생성 — JSON 로그 예시

로그 형식에 맞는 SerDe(Serializer/Deserializer)를 선택하는 것이 핵심이다. JSON 로그라면 org.openx.data.jsonserde.JsonSerDe, Apache 액세스 로그라면 Regex SerDe, CSV라면 LazySimpleSerDe를 사용한다. SerDe가 로그 형식과 맞지 않으면 모든 컬럼이 NULL로 파싱된다.

🔽 JSON 로그 테이블 생성 DDL (클릭하여 펼치기)
CREATE EXTERNAL TABLE IF NOT EXISTS app_logs_db.application_logs (
  request_id  STRING,
  timestamp   STRING,
  level       STRING,
  service     STRING,
  message     STRING,
  duration_ms BIGINT,
  status_code INT,
  user_id     STRING
)
PARTITIONED BY (
  year  STRING,
  month STRING,
  day   STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
  'ignore.malformed.json' = 'TRUE'
)
STORED AS TEXTFILE
LOCATION 's3://my-log-bucket/app-logs/'
TBLPROPERTIES (
  'has_encrypted_data' = 'false'
);

이 DDL을 Athena 쿼리 편집기에서 직접 실행하거나, CLI로 실행할 수 있다:

aws athena start-query-execution \
  --query-string file://create_table.sql \
  --query-execution-context Database=app_logs_db \
  --result-configuration OutputLocation=s3://my-athena-results-bucket/query-results/ \
  --region us-east-1

4단계: 파티션 등록

테이블을 만들었다고 Athena가 자동으로 파티션을 인식하지 않는다. 파티션을 명시적으로 등록해야 한다. 두 가지 방법이 있다.

방법 A: MSCK REPAIR TABLE — S3의 기존 파티션을 자동으로 스캔해서 카탈로그에 등록한다. 파티션이 수천 개 이상이면 타임아웃이 발생할 수 있다.

aws athena start-query-execution \
  --query-string 'MSCK REPAIR TABLE app_logs_db.application_logs;' \
  --query-execution-context Database=app_logs_db \
  --result-configuration OutputLocation=s3://my-athena-results-bucket/query-results/ \
  --region us-east-1

방법 B: 파티션 프로젝션(Partition Projection) — 파티션을 카탈로그에 등록하지 않고, Athena가 쿼리 시점에 파티션 경로를 동적으로 계산한다. 로그처럼 날짜 기반으로 파티션이 계속 생성되는 경우에 훨씬 실용적이다. MSCK REPAIR를 주기적으로 실행할 필요가 없어진다.

🔽 파티션 프로젝션 적용 테이블 DDL (클릭하여 펼치기)
CREATE EXTERNAL TABLE IF NOT EXISTS app_logs_db.application_logs_projected (
  request_id  STRING,
  timestamp   STRING,
  level       STRING,
  service     STRING,
  message     STRING,
  duration_ms BIGINT,
  status_code INT,
  user_id     STRING
)
PARTITIONED BY (
  year  STRING,
  month STRING,
  day   STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
  'ignore.malformed.json' = 'TRUE'
)
STORED AS TEXTFILE
LOCATION 's3://my-log-bucket/app-logs/'
TBLPROPERTIES (
  'projection.enabled'       = 'true',
  'projection.year.type'     = 'integer',
  'projection.year.range'    = '2023,2030',
  'projection.month.type'    = 'integer',
  'projection.month.range'   = '1,12',
  'projection.month.digits'  = '2',
  'projection.day.type'      = 'integer',
  'projection.day.range'     = '1,31',
  'projection.day.digits'    = '2',
  'storage.location.template'= 's3://my-log-bucket/app-logs/year=${year}/month=${month}/day=${day}'
);

5단계: SQL 쿼리 실행

파티션 컬럼을 WHERE 절에 반드시 포함시켜야 한다. 빠뜨리면 전체 S3 경로를 풀스캔한다.

-- 특정 날짜의 에러 로그 조회
SELECT
  request_id,
  timestamp,
  service,
  message,
  status_code
FROM app_logs_db.application_logs
WHERE year  = '2024'
  AND month = '01'
  AND day   = '15'
  AND level = 'ERROR'
ORDER BY timestamp DESC
LIMIT 100;
-- 서비스별 평균 응답 시간 집계 (특정 월)
SELECT
  service,
  COUNT(*)              AS request_count,
  AVG(duration_ms)      AS avg_duration_ms,
  MAX(duration_ms)      AS max_duration_ms
FROM app_logs_db.application_logs
WHERE year  = '2024'
  AND month = '01'
GROUP BY service
ORDER BY avg_duration_ms DESC;

CLI로 쿼리를 제출하고 결과를 확인하는 방법:

# 쿼리 제출
QUERY_EXECUTION_ID=$(aws athena start-query-execution \
  --query-string "SELECT service, COUNT(*) AS cnt FROM app_logs_db.application_logs WHERE year='2024' AND month='01' AND day='15' AND level='ERROR' GROUP BY service;" \
  --query-execution-context Database=app_logs_db \
  --result-configuration OutputLocation=s3://my-athena-results-bucket/query-results/ \
  --region us-east-1 \
  --query 'QueryExecutionId' \
  --output text)

echo "Query Execution ID: $QUERY_EXECUTION_ID"

# 쿼리 상태 확인
aws athena get-query-execution \
  --query-execution-id $QUERY_EXECUTION_ID \
  --region us-east-1 \
  --query 'QueryExecution.Status'

# 결과 조회
aws athena get-query-results \
  --query-execution-id $QUERY_EXECUTION_ID \
  --region us-east-1

IAM 권한 설정 — Athena 쿼리에 필요한 최소 권한

Athena 쿼리를 실행하는 IAM 역할 또는 사용자에게는 Athena 실행 권한, Glue 카탈로그 읽기 권한, S3 로그 버킷 읽기 권한, S3 결과 버킷 읽기/쓰기 권한이 필요하다.

🔽 Athena 쿼리 실행 IAM 정책 (클릭하여 펼치기)
{
  'Version': '2012-10-17',
  'Statement': [
    {
      'Sid': 'AthenaQueryExecution',
      'Effect': 'Allow',
      'Action': [
        'athena:StartQueryExecution',
        'athena:GetQueryExecution',
        'athena:GetQueryResults',
        'athena:StopQueryExecution',
        'athena:ListQueryExecutions'
      ],
      'Resource': 'arn:aws:athena:us-east-1:123456789012:workgroup/primary'
    },
    {
      'Sid': 'GlueCatalogRead',
      'Effect': 'Allow',
      'Action': [
        'glue:GetDatabase',
        'glue:GetTable',
        'glue:GetTables',
        'glue:GetPartition',
        'glue:GetPartitions',
        'glue:BatchGetPartition'
      ],
      'Resource': [
        'arn:aws:glue:us-east-1:123456789012:catalog',
        'arn:aws:glue:us-east-1:123456789012:database/app_logs_db',
        'arn:aws:glue:us-east-1:123456789012:table/app_logs_db/*'
      ]
    },
    {
      'Sid': 'S3LogBucketRead',
      'Effect': 'Allow',
      'Action': [
        's3:GetObject',
        's3:ListBucket'
      ],
      'Resource': [
        'arn:aws:s3:::my-log-bucket',
        'arn:aws:s3:::my-log-bucket/*'
      ]
    },
    {
      'Sid': 'S3ResultsBucketReadWrite',
      'Effect': 'Allow',
      'Action': [
        's3:GetObject',
        's3:PutObject',
        's3:ListBucket'
      ],
      'Resource': [
        'arn:aws:s3:::my-athena-results-bucket',
        'arn:aws:s3:::my-athena-results-bucket/*'
      ]
    }
  ]
}

실제 운영에서 마주치는 함정 — 증상, 오진, 실제 원인

처음 Athena를 설정하고 쿼리를 날렸을 때 모든 컬럼이 NULL로 반환되는 경험을 하게 되는 경우가 있다. 로그 파일이 분명히 S3에 있고, 테이블도 만들었고, 파티션도 등록했는데 — 결과가 비어 있다.

처음엔 파티션 등록이 안 된 줄 알고 MSCK REPAIR를 여러 번 돌린다. 파티션은 정상적으로 등록되어 있다. 그 다음엔 S3 경로가 틀렸나 확인한다. 경로도 맞다.

실제 원인은 SerDe 불일치였다. 로그 파일이 JSON처럼 생겼지만 실제로는 각 줄이 완전한 JSON이 아니라 멀티라인 JSON이었고, ignore.malformed.jsonTRUE로 설정했기 때문에 파싱 오류가 조용히 무시되면서 NULL이 반환된 것이다. SerDe 속성을 제거하고 단일 라인 JSON으로 로그 형식을 수정하거나, OpenX JSON SerDe 대신 Hive JSON SerDe(org.apache.hive.hcatalog.data.JsonSerDe)로 교체하면 해결된다.

SerDe 선택은 테이블 생성 이후 수정이 어렵다. 로그 샘플 파일 몇 개를 먼저 확인하고 SerDe를 결정하는 것이 순서다.

graph TD Symptom["증상: 쿼리 성공
모든 컬럼 NULL 반환"] --> Misdiag["오진: 파티션 미등록
또는 S3 경로 오류"] Misdiag -->|"MSCK REPAIR 재실행
경로 재확인 — 모두 정상"| RealCause["실제 원인: SerDe 불일치
malformed JSON 무시 설정"] RealCause --> Fix["수정: 로그 형식에 맞는
SerDe로 테이블 재생성"]
  1. NULL 결과 증상: 쿼리는 성공하지만 모든 컬럼이 NULL로 반환된다.
  2. 오진 경로: 파티션 미등록 또는 S3 경로 오류로 잘못 판단하기 쉽다.
  3. 실제 원인: SerDe가 실제 로그 형식과 불일치하고, malformed 무시 설정으로 오류가 숨겨진다.
  4. 수정: 로그 형식에 맞는 SerDe로 테이블을 재생성한다.

비용 최적화 — Parquet 변환과 압축

JSON 텍스트 파일을 그대로 쿼리하면 Athena가 모든 컬럼을 읽어야 한다. Parquet 또는 ORC 같은 컬럼형 포맷으로 변환하면 쿼리가 필요한 컬럼만 읽어 스캔 비용이 대폭 줄어든다. 실제 운영 환경에서는 JSON 로그를 일정 주기로 Parquet으로 변환해서 별도 테이블로 관리하는 패턴이 일반적이다.

Athena에서 직접 CTAS(Create Table As Select)로 Parquet 변환 테이블을 만들 수 있다:

CREATE TABLE app_logs_db.application_logs_parquet
WITH (
  format            = 'PARQUET',
  parquet_compression = 'SNAPPY',
  external_location = 's3://my-log-bucket/app-logs-parquet/',
  partitioned_by    = ARRAY['year', 'month', 'day']
) AS
SELECT
  request_id,
  timestamp,
  level,
  service,
  message,
  duration_ms,
  status_code,
  user_id,
  year,
  month,
  day
FROM app_logs_db.application_logs
WHERE year = '2024';

Parquet + Snappy 압축 조합은 원본 JSON 대비 스캔 데이터 양을 크게 줄여준다. 정확한 절감 비율은 로그 구조와 컬럼 카디널리티에 따라 달라지므로, 실제 데이터로 직접 측정하는 것이 권장된다.

S3 액세스 로그와 ALB 로그 — 사전 정의된 테이블 구조 활용

AWS 서비스 자체 로그(S3 서버 액세스 로그, ALB 액세스 로그, CloudFront 로그)는 AWS 공식 문서에서 Athena 테이블 DDL을 직접 제공한다. 이 경우 SerDe를 직접 설계할 필요 없이 공식 DDL을 그대로 사용하면 된다.

ALB 액세스 로그 Athena 테이블 DDL은 AWS Athena 공식 문서 — ALB 로그 쿼리에서 확인할 수 있다.

Athena로 S3 로그 쿼리하기 — 다음 단계

Athena로 S3 로그를 쿼리하는 기본 흐름은 Glue 카탈로그 테이블 정의 → 파티션 등록 → SQL 실행이다. 운영 환경에서는 파티션 프로젝션으로 파티션 관리 오버헤드를 줄이고, Parquet 변환으로 스캔 비용을 최적화하는 것이 실질적인 다음 단계다.

비용과 쿼리 성능은 파티션 설계와 파일 포맷 선택에서 결정된다. 이 두 가지를 처음부터 제대로 잡으면 나머지는 SQL 문제다.

핵심 용어 정리

용어설명
SerDeSerializer/Deserializer. Athena가 파일을 읽고 파싱하는 방식을 정의하는 라이브러리. 로그 형식(JSON, CSV, Regex 등)에 맞게 선택해야 한다.
파티션 프로젝션파티션을 Glue 카탈로그에 등록하지 않고 쿼리 시점에 S3 경로를 동적으로 계산하는 Athena 기능. 날짜 기반 로그에 특히 유용하다.
CTASCreate Table As Select. SELECT 결과를 새 테이블로 저장하는 DDL. JSON을 Parquet으로 변환할 때 사용한다.
Glue Data CatalogAthena가 테이블 스키마와 S3 위치를 조회하는 메타데이터 저장소. Hive 메타스토어 호환.
파티션 프루닝WHERE 절의 파티션 조건으로 스캔 대상 S3 경로를 줄이는 최적화. 쿼리 비용 절감의 핵심 메커니즘.

댓글

이 블로그의 인기 게시물

EC2 SSH 연결 타임아웃 완전 해결 가이드: Security Group 인바운드 규칙부터 라우팅까지

EC2 SSH 연결 시간 초과: 확인해야 할 보안 그룹(Security Group) 규칙

IAM User vs IAM Role 차이점 완전 정리 — EC2에서 S3 접근 시 무엇을 써야 하는가