CloudFront Log를 Athena로 쿼리하기

Kurt Lee
Vingle Tech Blog
Published in
9 min readFeb 28, 2018

CloudFront에는 S3에 law-level access-log를 쌓아주는 멋진 기능이 있습니다. 사실 아직까지도 상당수의 CDN Vendor들은 이 기능을 제대로 지원하고 있지 않고, 흥미롭게도(사실 어느정도는 예측 가능하게도) 심지어 지원하는 vendor들중 일부는 AWS S3를 스토리지로 이용하고, 유저들에게 S3 bucket을 주는 식으로 구현합니다. Akamai 가 대표적이죠.

S3야 워낙 멋진 제품이니까, 저장은 그렇다 치고, 그럼 이 데이터를 어떻게 쿼리하지? 가 문제가 되는데요. 다양한 방법이 있습니다. Local로 모든 csv.gz 파일을 다운받아서 grep을 해도 되고, EMR에 Spark나 Hive를 올려서 쿼리할수도 있겠고요. (물론 저희정도 되면 이렇게 하기엔 데이터 량이 택도 없이 많습니다..)
물론, 가장 좋은 방법은 이꼴저꼴 보지않고 관리할것도 없고, 쓰는만큼만 돈 내면 되는 Athena를 사용하는 것입니다.

만약 이미 Cloudfront와 Athena에 대한 지식이 있으신 분들이라면, 사실 이부분은 상당히 쉽습니다. 아마존 에서도 공식 가이드가 이미 나와있고요.

이런 기존 방법들의 유일한 문제 하나는, Athena의 Table Partition을 사용하지 못한다는 점입니다.

Athena에 익숙하지 않은 분들을 위해 설명하자면, Athena에서 Table partition은 해당 테이블의 데이터가 어디 (s3 location)에 있는지를 partition parameter들과 함께 정의하는 일종의 data indicator입니다.

이게 필요한 이유는, Athena의 경우 partition을 지정함으로서, 해당 테이블의 가능한 모든 데이터를 접근하는게 아니라, 특정 데이터만 접근할수 있게 할수 있기 때문입니다. 또한, Athena는 데이터를 “읽은" 만큼 만 과금됩니다. 즉 실제 쿼리에서 필요하지도 않은 데이터를 스캔하면 스캔 할수록 요금이 비싸지는 구조죠.

예를들어,

“지난 7일동안 500 에러가 난 url들을 에러 횟수와 함께 보고싶다”

고 할때, 쿼리를 어떻게 짜더라도 partition을 사용하지 않으면 athena는 “모든 데이터를” 스캔합니다.
하지만 만약 테이블의 partition을 연/월/일/시 로 지정하고 대략 아래와 같이 partition을 미리 만든다면,

year=2016/month=02/day=03/hour=00 — s3://logs/2016/02/03/00/
year=2016/month=02/day=03/hour=01 — s3://logs/2016/02/03/01/
year=2016/month=02/day=03/hour=02 — s3://logs/2016/02/03/02/

아래와 같이 쿼리를 날릴수 있고,

SELECT uri, count(1) 
FROM cloudfront_logs
WHERE status = 500
AND (year || month || day || hour) > ‘2018020100’

이러면 Athena (Presto)는 쿼리를 실행하면서 Partition에 관련된 조건문을 먼저 읽어서 이 쿼리를 실행할때 읽어야 하는 파티션을 좁히는 작업을 자동으로 먼저 하게 됩니다. 결과적으로, 이 경우 지정된 파티션 (2018/02/01/00) 이후의 데이터만 읽게 되죠.

음 그럼 뭔가 이상합니다. 파티션은 이렇게 그냥 하면 무조건 좋은건데, 아마존 공식 가이드에는 이게 왜 쏙 빠져 있는걸까요.

이유는 CloudFront의 Log file 모양 때문입니다.

CloudFront Log format 예시

음. 좀 이상하긴한데.. 그래도 어차피 S3니까, 대충 prefix 맞춰서

ALTER TABLE logs
ADD PARTITION (year="2018", month="01", day="05", hour="02")
LOCATION 's3://bucket/EI6813FON0XVG.2018.01.05.02

이렇게 만들면 되지 않을까? 네 안됩니다. 정확히는 에러는 전혀 안나지만, 해당 파티션에 데이터가 읽히지 않습니다.

이유는 간단합니다. Presto (Athena는 facebook의 Presto engine을 fully-managed 형태로 만든 것입니다) 는 기본적으로 HDFS같은 “파일시스템" 을 위해 설계되었습니다. 아마존에서 이걸 S3를 대상으로 쓸수 있도록 만들긴 했지만, 애초에 Presto에 있던 제약, 즉 “Partition은 반드시 “폴더"”를 대상으로 지정해야 한다” 는 해결하지 못한거죠.
심지어 S3에서는 “폴더" 라는게 존재 하지 않습니다. “/” 로 표시되는 것들은 “prefix”에 불과합니다. 그걸 S3 Console에서는 보기 좋으라고 폴더 모양으로 묶어서 보여주긴 합니다만.. 그건 UI에 불과하고요.
여하튼, 다른 방법이 없습니다. 아마존에 문의도 해봤습니다만, S3에서 폴더가 아닌건 알지만 현재 구현상으로는 무조건 “/”로 끝나는 prefix 안에 들어가야 한답니다

그래서 이걸 Athena / CloudFront Logs와 마찬가지로, serverless형태로 infra 관리를 전혀 안해도 되는 형태로 구성해봅시다.

  1. S3에 Event Trigger를 연결해서, 새로운 파일이 올라오면 Lambda로 알려준다
  2. Lambda에서는 해당 파일을 “폴더" 로 Copy한다.
    예를들어
    s3://bucket/log/distributionId.2018–02–01–00.12345.gz
    → s3://bucket/logs/distributionId/2018/02/01/00/12345.gz
    (Move 가 아니라 Copy인 이유는 S3에는 Move가 존재하지 않기 때문입니다. Copy는 Native API로 다행히 지원됩니다…)
  3. Lambda에서, 새로 만들어진 폴더를 Athena에 Partition 으로 등록한다.

빙글에서는 새로만든 거의 모든 infra를 CloudFormation (serverless.yml 안의) 로 관리하고 있기 때문에, 대부분의 configuration을 cloudformation을 통해 진행했습니다

--- serverless.ymlfunctions: 
CloudFrontLogIndexer:
handler: cloudfront-log-indexer.handler
memorySize: 256
timeout: 120
role: CloudFrontLogIndexerRole
environment:
CLOUDFRONT_LOG_S3_BUCKET: bucket-name
resources:
Resources:
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
...
Logging:
Bucket:
Fn::GetAtt: [CloudfrontLogS3Bucket, DomainName]
Prefix: "log/"
CloudfrontLogS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: bucket-name
NotificationConfiguration:
LambdaConfigurations:
- Event: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: Prefix
Value: log/
- Name: Suffix
Value: .gz
Function:
Fn::GetAtt: [CloudFrontLogIndexerLambdaFunction, Arn]

CloudFrontLogIndexerPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Ref: CloudFrontLogIndexerLambdaFunction
Principal: s3.amazonaws.com
SourceAccount:
Ref: AWS::AccountId
SourceArn:
Fn::GetAtt: [CloudfrontLogS3Bucket, Arn]

하지만 물론 상황에 따라, CloudFormation을 안쓰고 계시다면 위 작업을 AWS Console이나 API를 이용해 직접 해주셔야 합니다.

마지막으로, Lambda Code는 상당히 간단합니다.

이로써 알아서 파티션까지 되는 CloudFront Log용 Athena Table이 생겼습니다. 구성 요소들은 다 알아서 scaling / manage 되는것들이니 딱히 관리할 건 없습니다. 아마도 유일한 모니터링이 필요한 요소는 Lambda이겠습니다. Lambda는 어쨌든 Error가 날수 있으니까요

왼쪽은 Data Scanned가 1GB, 오른쪽은 Data Scanned가 1.5GB로 쿼리에 따라 정확히 필요한 부분만 읽었음을 알수 있습니다.

빙글에서는 이 CloudFront Log가 정말 엄청난 사이즈로 쌓이기 때문에, 이런식의 “필요한 날짜 범위의 데이터만” 스캔할수 있는게 많은 도움이 되고 있습니다.

빙글에는 이런 문제를 함께 풀어갈 사람을 언제나 기다립니다.

--

--

Kurt Lee
Vingle Tech Blog

AWS Serverless Hero, Seoul. Love building beautiful product, Proudly working hard at startup. Serverless, Typescript, AWS, Microservices