Skip to main content

S3 버킷의 로그 살펴보기

오늘은 S3 버킷의 액세스 로그를 생성해 볼 건데요. 액세스 로그 생성시 주의할 점이 한 가지 있습니다.

caution

액세스 로그가 쌓일 대상 버킷과 소스 버킷을 동일한 버킷으로 지정할 경우 무한 로깅(무한 루프)이 발생하여 요금 명세서 폭탄을 받을 수 있으니 필히 주의하시기를 바랍니다.

제가 액세스 로깅의 소스 버킷으로 이 블로그에서 사용하고 있는 S3 오브젝트 스토리지 ghdwlsgur.github.io 버킷의 로그를 사용할 것입니다.

로깅 대상 버킷 생성

먼저 ghdwlsgur.github.io 버킷의 로그가 쌓일 버킷을 생성해 줄 건데요. 이 버킷 이름은 ghdwlsgur.github.io.log-bucket으로 생성했습니다. 로깅 버킷으로만 사용할 것이므로 모든 퍼블릭 액세스를 차단하고 데이터 관리를 위해 버전 활성화를 해주었습니다. 이후 로그가 쌓일 폴더인 access-log라는 빈 폴더 하나를 생성합니다. 여기까지 진행하셨다면 대상 로그 설정은 모두 완료되었습니다.

이제 다시 ghdwlsgur.github.io 속성으로 가서 액세스 로그 설정을 해줍시다 !

소스 버킷 설정

속성 - 서버 액세스 로깅 - 활성화 - 대상 버킷: s3://ghdwlsgur.github.io.log-bucket/access-log/ 으로 위에서 생성한 대상 버킷을 지정해줍니다.

여기서 끝이 아니라 버킷 정책 또는 ACL을 통해 권한을 부여할 수 있는데요. 버킷 ACL을 사용하여 로그 전달 그룹에 권한을 부여하는 방식은 AWS S3 공식문서에서도 권장하지 않고 있으므로 버킷 정책 설정을 통해 접근 제어 권한을 부여하였습니다.

대상 버킷 설정

마지막으로 아래처럼 버킷 정책을 편집해주면 최종적으로 액세스 로그 설정이 끝났습니다. 하지만 ghdwlsgur.github.io 버킷의 객체에 접근해도 ghdwlsgur.github.io.log-bucket에는 아무 로그도 쌓이지 않는 것을 확인할 수 있습니다. 설정 후에 약 1시간 정도의 딜레이가 있으므로 참고해주세요. 저는 약 50분 후에 로그가 쌓인 것을 확인했습니다. 상황에 따라 몇시간이 걸릴 수도 있으니 느긋하게 확인해주세요.

대상 버킷의 버킷 정책

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3ServerAccessLogsPolicy",
"Effect": "Allow",
"Principal": {
"Service": "logging.s3.amazonaws.com"
},
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::ghdwlsgur.github.io.log-bucket/*",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:s3:::ghdwlsgur.github.io"
}
}
}
]
}

약 1시간이 지난 지금 시점에서 로깅 대상 버킷인 ghdwlsgur.github.io.log-bucket에 로그를 아래 사진과 같이 확인할 수 있습니다.

logging1

액세스 레코드 형식은 아래 공식 문서에서 확인하실 수 있습니다.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/LogFormat.html

레코드 형식을 보지 않고 보면 대강 내용 파악은 되겠지만 정확하게 로그를 읽을 수 없으므로 공식문서를 보고 각 레코드가 어떤 의미를 나타내는지 아래와 같이 분석해보았어요.

blablablablablablur123 # 버킷 소유자
ghdwlsgur.github.io # 요청이 처리된 버킷의 이름
[07/Jan/2023:08:40:47 +0000] # 요청이 수신된 시간 UTC
123.123.123.123 # REQUEST IP
arn:aws:sts::420578220663:federated-user/hongjinhyeok # 요청자의 정식 사용자 ID 또는 인증되지 않은 요청일 경우 -
ZMHDHCV446Y3TRWW # 각 요청을 고유하게 식별하는 문자열의 요청 ID
REST.GET.NOTIFICATION # 작업 REST.[HTTP Method].[RESOURCE TYPE]
- # 요청의 키 부분으로 URL로 인코딩되거나 키 파라미터가 없을 경우 "-"
"GET /ghdwlsgur.github.io?notification= HTTP/1.1" # HTTP 요청 메시지의 Request-URI 부분
200 # HTTP 응답코드
- # Amazon S3 오류 코드 또는 오류가 없을 경우 "-"
115 # HTTP 프로토콜 오버헤드를 제외한 보낸 응답 바이트 수
- # 객체 크기
18 # 서버 관점에서 요청이 플라이트 상태를 유지한 시간(밀리초)
- # 반환시간
"-" # Referer
"S3Console/0.4, aws-internal/3 aws-sdk-java/1.11.1030 Linux/5.10.157-122.673.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.352-b08 java/1.8.0_352 vendor/Oracle_Corporation cfg/retry-mode/standard" # 사용자 에이전트
- # 버전 ID
CPZ8LbcIYlCTZWWVPtyJX27QvtHaOTvd7DFQ0QNkz94BQ5QOOFdTko9RN8j2MZGNSrW9Mv/NlJY= # 호스트 ID (x-amz-id-2 또는 Amazon S3 혹장 요청 ID)
SigV4 # 서명 버전
ECDHE-RSA-AES128-GCM-SHA256 # 암호 그룹
AuthHeader # 인증 유형
s3.ap-northeast-2.amazonaws.com TLSv1.2 # 호스트 헤더 (Amazon S3에 연결하는 데 사용된 엔드포인트)
- # TLS 버전
- # 액세스 포인트 ARN(Amazon 리소스 이름)

쌓인 로그를 하나하나 열어보기에는 손이 너무 많이 가는 작업이기도 하고 그 많은 로그를 언제 하나하나 살펴볼까요 ?

Amazon에서는 로그를 분석할 수 있도록 Athena를 제공합니다. Athena는 AWS S3 내 데이터 파일을 기반으로 표준 SQL문을 사용하여 직접 데이터를 쿼리할 수 있으며 서버리스 서비스이므로 운영 및 관리 필요 없이 데이터 분석이 필요할 때만 콘솔에서 언제든지 사용할 수 있습니다. 비용은 각 쿼리에서 스캔한 데이터 양에 따라 요금이 부과되며 제가 사용한 서울 리전을 기준으로 1TB당 $5 달러의 비용이 소모됩니다.

아래 링크에서 Athena 비용을 리전별로 확인할 수 있습니다 ! https://aws.amazon.com/ko/athena/pricing/

또한 아마존 아테나는 Presto를 사용하고 있으므로 Presto 성능을 고려하여 쿼리를 작성하신다면 쉽게 최적화를 할 수도 있습니다. 따라서 저는 Athena를 사용하여 쌓인 로그들을 빠르게 분석해보고자 합니다.

그 전에 ! 먼저 Athena 쿼리 결과를 저장할 버킷을 저장해야 하는데요. 이에 저는 미리 로깅 대상 버킷인 ghdwlsgur.github.io.log-bucket에 athena-log 폴더를 하나 생성해주었습니다. 서버측 암호화는 기존처럼 Amazon S3 관리형 키(SSE-S3)를 선택해줍니다.

Amazon Athena > 쿼리 편집기 > 설정 관리로 아래 사진처럼 설정해줬어요. 예상 버킷 소유자는 계정 ID를 입력하면 됩니다. 보안상 중요한 정보이므로 블러 처리를 했어요. (민감 정보 유출은 언제나 주의해서 확인해요 !) 버킷 소유자에게 쿼리 결과에 대한 전체 제어 권한을 할당하여 S3 쿼리 결과 버킷의 소유자가 쿼리 결과에 대한 전체 제어 권한을 가질 수 있도록 해줬습니다 !

logging2

Athena 데이터베이스 생성

먼저 Athena 쿼리 편집기에서 DDL을 실행하여 s3_access_logs_db라는 데이터베이스를 생성해줍니다.

create database s3_access_logs_db

Athena 테이블 생성

데이터 베이스를 생성해줬으니 이제 테이블을 생성해 줄 차례입니다. 테이블을 생성해 주는 이유는 로깅 데이터를 가지고 있는 S3의 버킷 로그를 입력 데이터로 필요로 하기 때문입니다. 아래에서는 LOCATION 뒤에 입력 데이터를 제공할 버킷의 ARN을 입력해 줍니다. 아래와 같이 입력하고 쿼리를 실행합니다.

CREATE EXTERNAL TABLE `s3_access_logs_db.mybucket_logs`(
`bucketowner` STRING,
`bucket_name` STRING,
`requestdatetime` STRING,
`remoteip` STRING,
`requester` STRING,
`requestid` STRING,
`operation` STRING,
`key` STRING,
`request_uri` STRING,
`httpstatus` STRING,
`errorcode` STRING,
`bytessent` BIGINT,
`objectsize` BIGINT,
`totaltime` STRING,
`turnaroundtime` STRING,
`referrer` STRING,
`useragent` STRING,
`versionid` STRING,
`hostid` STRING,
`sigv` STRING,
`ciphersuite` STRING,
`authtype` STRING,
`endpoint` STRING,
`tlsversion` STRING)
ROW FORMAT SERDE
'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
'input.regex'='([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$')
STORED AS INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
's3://ghdwlsgur.github.io.log-bucket/access-log'

쿼리 결과 데이터는 쿼리 실행 후 아래에 쿼리 결과에서도 확인할 수 있으며 쿼리 결과 데이터의 저장 위치로 지정한 ghdwlsgur.github.io.log-bucket/athena-log에서도 확인할 수 있었습니다. 또한 쿼리 결과를 곧바로 엑셀 파일로 다운로드 받을 수 있어서 편리한 것 같네요 ㅎㅎ..

이외에도 다음 예시를 활용 및 참고하여 원하는 데이터를 추출하며 실습해보세요.

삭제된 객체에 대한 요청 찾기

SELECT * FROM s3_access_logs-db.mybucket_logs WHERE
key = 'images/picture.jpg' AND operation like '%DELETE%';

403 Access Denied 오류를 발생시킨 요청에 대해 Amazon S3 요청 ID 표시

SELECT requestdatetime, requester, operation, requestid,
hostid FROM s3_access_logs_db.mybucket_logs WHERE httpstatus = '403';

특정 기간의 HTTP 5xx 오류에 대한 Amazon S3 요청 ID를 찾기

SELECT requestdatetime, key, httpstatus, errorcode, requestid, hostid FROM s3_access_logs_db.mybucket_logs
WHERE httpstatus like '5%' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-09-18:07:00:00','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-09-18:08:00:00','yyyy-MM-dd:HH:mm:ss');

객체를 삭제한 사람과 시간을 표시

SELECT requestdatetime, remoteip, requester, key FROM s3_access_logs_db.mybucket_logs WHERE
key = 'images/picture.jpg' AND operation like '%DELETE%';

IAM 역할이 실행한 모든 운영을 표시

SELECT * FROM s3_access_logs_db.mybucket_logs WHERE
requester='arn:aws:iam::123456789123:user/user_name';

특정 기간에 객체에 수행한 모든 운영 표시

SELECT SUM(bytessent) as uploadtotal,
SUM(objectsize) as downloadtotal,
SUM(bytessent + objectsize) AS total FROM s3_access_logs_db.mybucket_logs
WHERE remoteIP='1.2.3.4' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01','yyyy-MM-dd')
AND parse_datetime('2021-08-01','yyyy-MM-dd');

특정 기간 동안 IP 주소를 통해 전송한 데이터의 양 표시

SELECT SUM(bytessent) as uploadtotal,
SUM(objectsize) as downloadtotal,
SUM(bytessent + objectsize) AS total FROM s3_access_logs_db.mybucket_logs
WHERE remoteIP='1.2.3.4' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01','yyyy-MM-dd')
AND parse_datetime('2021-08-01','yyyy-MM-dd');

특정 기간에 수명 주기 규칙에 따라 수행한 모든 만료 운영 표시

SELECT *
FROM s3_access_logs_db.mybucket_logs
WHERE operation = 'S3.EXPIRE.OBJECT' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-09-18:00:00:00','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-09-19:00:00:00','yyyy-MM-dd:HH:mm:ss');

특정 기간에 만료된 객체 수를 계산

SELECT count(*) as ExpireCount
FROM s3_access_logs_db.mybucket_logs
WHERE operation = 'S3.EXPIRE.OBJECT' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-09-18:00:00:00','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-09-19:00:00:00','yyyy-MM-dd:HH:mm:ss');

특정 기간에 수명 주기 규칙에 따라 수행한 모든 전환 운영 표시

SELECT * FROM s3_access_logs_db.mybucket_logs
WHERE operation like 'S3.TRANSITION%' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-09-18:00:00:00','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-09-19:00:00:00','yyyy-MM-dd:HH:mm:ss');

서명 버전별로 그룹화된 모든 요청자를 표시

SELECT requester, Sigv, Count(Sigv) as SigCount
FROM s3_access_logs_db.mybucket_logs
GROUP BY requester, Sigv;

특정 기간에 요청한 모든 익명 요청자를 표시

SELECT Bucket, Requester, RemoteIP, Key, HTTPStatus, ErrorCode, RequestDateTime
FROM s3_access_logs_db.mybucket_logs
WHERE Requester IS NULL AND
parse_datetime(RequestDateTime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01:00:42:42','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-07-02:00:42:42','yyyy-MM-dd:HH:mm:ss')

특정 기간에 PUT 객체 요청을 보낸 모든 요청자를 표시

SELECT Bucket, Requester, RemoteIP, Key, HTTPStatus, ErrorCode, RequestDateTime
FROM s3_access_logs_db
WHERE Operation='REST.PUT.OBJECT' AND
parse_datetime(RequestDateTime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01:00:42:42','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-07-02:00:42:42','yyyy-MM-dd:HH:mm:ss')

특정 기간에 GET 객체 요청을 보낸 모든 요청자를 표시

SELECT Bucket, Requester, RemoteIP, Key, HTTPStatus, ErrorCode, RequestDateTime
FROM s3_access_logs_db
WHERE Operation='REST.GET.OBJECT' AND
parse_datetime(RequestDateTime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01:00:42:42','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-07-02:00:42:42','yyyy-MM-dd:HH:mm:ss')

특정 기간에 요청한 모든 익명 요청자를 표시

SELECT Bucket, Requester, RemoteIP, Key, HTTPStatus, ErrorCode, RequestDateTime
FROM s3_access_logs_db.mybucket_logs
WHERE Requester IS NULL AND
parse_datetime(RequestDateTime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-07-01:00:42:42','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-07-02:00:42:42','yyyy-MM-dd:HH:mm:ss')

모든 요청자를 표시

SELECT * FROM s3_access_logs_db.mybucket_logs
NOT turnaroundtime='-' AND
parse_datetime(requestdatetime,'dd/MMM/yyyy:HH:mm:ss Z')
BETWEEN parse_datetime('2021-09-18:00:00:00','yyyy-MM-dd:HH:mm:ss')
AND
parse_datetime('2021-09-19:00:00:00','yyyy-MM-dd:HH:mm:ss')
ORDER BY CAST(turnaroundtime AS INT) DESC;

추가로 원하는 쿼리문만 실행하고 싶을 때 아래 처럼 드래그 후에 다시 실행을 클릭하세요 ! 또한 파란색 네모 안에 스캔한 데이터를 잘 봐주세요. 이 데이터 량에 따라 과금량이 달라집니다 !

logging3

참고자료

https://aws.amazon.com/ko/premiumsupport/knowledge-center/analyze-logs-athena/