들어가며
IAM User가 보낸 SES 메일 발송 이력을 확인해야 할 일이 생겼다. 감사 환경은 이미 만들어 뒀으니(CloudTrail + Athena 감사 로그 분석 환경 구축) 쿼리만 돌리면 될 줄 알았다.
결과가 비어 있었다. 기간을 넓혀도, 조건을 풀어도 한 건도 나오지 않았다.
이럴 때 가능성은 둘이다. 정말 발송이 없었거나, 기록되지 않았거나. 확인하려고 CLI로 직접 메일을 보내봤다.
aws ses send-email ... → MessageId 정상 리턴 (발송 성공)
CloudTrail 조회 → 여전히 없음
발송은 됐는데 기록이 없다. 로그가 없다는 것과 기록되지 않는다는 건 다른 문제라는 걸 여기서 알았다.
근본 원인: 관리 이벤트와 데이터 이벤트
SES의 SendEmail, SendRawEmail 같은 발송 API는 CloudTrail의 관리 이벤트(Management Event)가 아니라 **데이터 이벤트(Data Event)**로 분류된다.
| 구분 | 예시 | 기본 기록 |
|---|---|---|
| 관리 이벤트 | CreateConfigurationSet, VerifyDomainIdentity | 자동으로 기록됨 |
| 데이터 이벤트 | SendEmail, SendRawEmail | 명시적으로 켜야 기록됨 |
둘을 가르는 기준은 대상이다. 관리 이벤트는 리소스를 만들고 바꾸는 행위이고, 데이터 이벤트는 그 리소스를 사용하는 행위다. 같은 구분이 다른 서비스에도 그대로 적용된다.
| 서비스 | 관리 이벤트 (자동) | 데이터 이벤트 (수동) |
|---|---|---|
| S3 | 버킷 생성, 정책 변경 | 객체 GetObject, PutObject |
| SES | Identity 인증, 설정 변경 | 메일 발송 |
| Lambda | 함수 생성, 설정 변경 | 함수 호출 |
데이터 이벤트가 기본 비활성인 이유는 양 때문이다. 사용 행위는 관리 행위보다 수백 배 많이 발생하고, 전부 기록하면 저장과 조회 비용이 커진다. 그래서 AWS는 "필요한 리소스만 골라서 켜라"는 쪽을 기본값으로 뒀다.
문제는 이 기본값을 모르면 감사 체계가 돌아가고 있다고 착각한다는 점이다. Trail은 켜져 있고, 로그는 쌓이고, 쿼리도 동작한다. 다만 우리가 찾는 행위만 처음부터 기록되지 않았을 뿐이다.
참고로 SES 발송 이벤트를 CloudTrail로 남기는 기능 자체가 2025년 4월에 추가됐다. 그전에는 추적이 불가능했다.
- AWS 공식, Logging Amazon SES API calls with AWS CloudTrail
- AWS 공식, Amazon SES now supports logging email sending events through AWS CloudTrail (2025-04-14)
SES 데이터 이벤트 활성화
CloudTrail 콘솔에서는 SES 데이터 이벤트 유형이 아직 드롭다운에 없어서 CLI로 설정했다. 기존 basic event_selector를 advanced_event_selector로 전환하면서 SES 데이터 이벤트를 추가하는 방식이다.
aws cloudtrail put-event-selectors \
--region ap-northeast-2 \
--trail-name [trail-name] \
--advanced-event-selectors file://scripts/ses-cloudtrail-selectors.jsonscripts/ses-cloudtrail-selectors.json 내용이다.
[
{
"Name": "Management events",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Management"] }
]
},
{
"Name": "S3 data events for target buckets",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::[bucket-prod]/", "arn:aws:s3:::[bucket-dev]/"] }
]
},
{
"Name": "SES data events",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::SES::EmailIdentity"] }
]
}
]기존 S3 데이터 이벤트 설정을 같은 파일에 다시 적어둔 게 중요하다. advanced_event_selector는 기존 설정을 대체하기 때문에, 빠뜨린 항목은 그 순간부터 기록이 끊긴다.
주의사항
- basic event_selector와 advanced_event_selector는 동시에 쓸 수 없다
- CLI로 advanced를 적용하면 기존 basic 설정이 자동으로 마이그레이션된다
- Terraform의
cloudtrail.tf는 아직 basic event_selector라서terraform plan시 diff가 발생할 수 있다 - SMTP 방식 발송은 CloudTrail에 기록되지 않는다 (AWS 공식 제한)
마지막 항목은 감사 범위의 구멍이라서 따로 적어뒀다. API로 보낸 메일은 남고 SMTP로 보낸 메일은 남지 않는다면, "발송 이력이 없다"는 조회 결과가 발송이 없었다는 뜻이 아니게 된다. 감사 체계에서는 무엇을 볼 수 있는지만큼 무엇을 볼 수 없는지를 적어두는 게 중요하다. 나중에 그 구멍을 모르는 사람이 조회 결과만 보고 결론을 내리면 잘못된 판단이 된다.
비용
- SES 데이터 이벤트: 10만 건당 $0.10
- 현재 발송량(약 150건/일) 기준 월 $0.005 이하로 사실상 무료
데이터 이벤트의 비용 부담은 발송량에 비례한다. 우리 규모에서는 걱정할 수준이 아니었고, 그렇다면 켜지 않을 이유가 없었다.
활성화 후 확인 결과
활성화 이후 CloudTrail에 기록된 SES 발송 주체다.
| 주체 | 유형 | eventName | 용도 |
|---|---|---|---|
| [iam-user-dev] | IAM User | SendEmail | CLI 테스트, 앱 dev |
| [email-forward-lambda-role] | Lambda Role | SendRawEmail | 메일 수신 후 포워딩 |
| [report-lambda-role] | Lambda Role | SendEmail | Config 보고서 발송 |
| [health-check-lambda-role] | Lambda Role | SendEmail | ECS 헬스체크 알림 |
| [admin-user] | IAM User | SendRawEmail | 온프레미스 서버 발송 |
감사 로그가 알려준 뜻밖의 사실
목록을 보다가 예상과 다른 게 나왔다. dev 앱의 SES 호출이 IAM User가 아니라 ECS Task Role로 실행되고 있었다.
원인을 따라가 보니 설정 이름 불일치였다.
SesConfig 클래스: @Value("${aws.ses.secret-key:}")
application.yml: ses.secret-key 로 정의
→ 이름이 안 맞아 secretKey가 빈 문자열
→ DefaultCredentialsProvider로 fallback
→ ECS Task Role의 권한으로 발송
기본값이 있는 @Value라서 애플리케이션은 아무 오류 없이 떴고, 메일도 정상적으로 나갔다. 동작에는 문제가 없고 권한 주체만 달랐기 때문에, 감사 로그를 보기 전에는 알 방법이 없었다. 개발팀에 property 이름 수정을 안내했다.
이 발견이 이 작업에서 가장 값졌다. 감사 로그를 켜는 목적은 사고 대응인데, 실제로는 평상시의 설정 오류를 먼저 잡아줬다. 누가 무엇을 하고 있는지 보이기 시작하면, 의도와 다른 부분이 드러난다.
Athena 저장 쿼리 현황
S3 파일 접근 이력
| 쿼리명 | 환경 | 대상 | 조회 기간 |
|---|---|---|---|
s3-dev-presign-activity | 개발 | IAM User | 최근 7일 |
s3-prod-presign-activity | 운영 | IAM User | 최근 7일 |
s3-prod-taskrole-activity | 운영 | ECS Task Role | 최근 7일 |
SES 이메일 발송 이력
| 쿼리명 | 환경 | 대상 | 조회 기간 |
|---|---|---|---|
ses-all-email-activity | 통합 | 전체 (ALL) | 최근 7일 |
ses-dev-email-activity | 개발 | IAM User | 최근 30일 |
ses-prod-email-activity | 운영 | IAM User | 최근 7일 |
ses-prod-taskrole-email-activity | 운영 | ECS Task Role | 최근 7일 |
Task Role용 쿼리를 따로 만든 건 앞의 발견 때문이다. 사람이 쓰는 자격증명만 보고 있으면, 애플리케이션이 하는 일은 감사 범위 밖에 남는다.
조회 가능 시점
| 이벤트 유형 | 조회 가능 시점 |
|---|---|
| S3 객체 접근 (데이터 이벤트) | 2026-04-08 이후 |
| SES 이메일 발송 (데이터 이벤트) | 2026-04-14 이후 |
이 표가 이 글에서 가장 중요할지도 모른다. 데이터 이벤트는 켠 시점 이후만 쌓인다. 그전 기간은 지금 아무리 조회해도 나오지 않는다. 감사 설정이 사후 도구가 아니라 사전 준비인 이유다.
Terraform 반영 상태
| 항목 | 상태 | 비고 |
|---|---|---|
Athena ses-all-email-activity 쿼리 | Terraform 적용 완료 | 신규 추가 |
Athena ses-dev-email-activity 쿼리 | Terraform 코드 수정됨 | Access Key OR 조건 추가, 30일로 확장 |
| CloudTrail SES 데이터 이벤트 | CLI로 적용, Terraform 미반영 | basic에서 advanced event selector로 전환 필요 |
cloudtrail.tf의 event_selector를 advanced_event_selector로 마이그레이션하면 코드와 실제 상태가 일치한다. 다만 기존 S3 데이터 이벤트 설정도 함께 옮겨야 해서 주의가 필요하다.
이 어긋난 구간을 표에 남겨둔 이유가 있다. 코드와 실제 상태가 다른 상태는 그 자체로 위험하다. 누군가 terraform apply를 돌리면 CLI로 켜둔 SES 데이터 이벤트가 조용히 사라질 수 있다. 다음 사람이 diff를 보고 당황하지 않도록, 지금 무엇이 코드 밖에 있는지 적어두는 것까지가 작업의 일부다.
배운 점
로그가 없다는 것과 기록되지 않는다는 것은 다르다. 발송은 성공하는데 CloudTrail에는 아무것도 없어서 한참 찾았는데, 원인은 SES 발송이 데이터 이벤트였기 때문이다. 감사 체계를 만들 때 먼저 확인해야 하는 건 쿼리가 아니라, 무엇이 기본으로 기록되고 무엇을 따로 켜야 하는지였다.
감사 설정은 사후가 아니라 사전 작업이다. 데이터 이벤트는 켠 시점 이후만 쌓인다. 사건이 난 다음에 켜면 그 사건은 영영 확인할 수 없다. 비용이 크지 않다면 필요해지기 전에 켜두는 쪽이 맞다는 결론에 도달했다.
감사 로그는 사고보다 평상시의 오류를 먼저 보여줬다. 설정 이름 불일치로 엉뚱한 권한 주체가 쓰이고 있던 걸 이 작업 중에 발견했다. 동작에는 문제가 없어서 아무도 몰랐던 종류다. 관측이 생기면 의도와 실제의 차이가 드러난다는 걸, NLB 포트별 트래픽 확인에서도 비슷하게 겪었다.
볼 수 없는 것을 적어두는 것도 감사 설계다. SMTP 발송이 기록되지 않는다는 제한을 적어두지 않으면, 나중에 조회 결과만 보고 잘못된 결론을 내리게 된다. 코드와 실제 상태가 어긋난 구간도 같은 이유로 남겨뒀다.
이어 읽기
- AWS CloudTrail + Athena 감사 로그 분석 환경 구축: 이 이벤트를 조회하는 분석 환경
- Amazon SES 기반 이메일 알림 인프라 구축: 감사 대상이 된 SES 발송 구성
- Access Key에서 IAM Role 기반 인증으로 전환: 왜, 어떻게: 권한 주체를 정리하는 작업