Sentry 로그 수집
이 문서에서는 Amazon S3를 사용하여 Sentry 로그를 Google Security Operations에 수집하는 방법을 설명합니다. Sentry는 이벤트, 문제, 성능 모니터링 데이터, 오류 추적 정보의 형태로 운영 데이터를 생성합니다. 이 통합을 사용하면 이러한 로그를 Google SecOps로 전송하여 분석 및 모니터링할 수 있으므로 Sentry에서 모니터링하는 애플리케이션 내의 애플리케이션 오류, 성능 문제, 사용자 상호작용을 파악할 수 있습니다.
시작하기 전에
다음 기본 요건이 충족되었는지 확인합니다.
- Google SecOps 인스턴스입니다.
- Sentry 테넌트에 대한 권한 있는 액세스 (API 범위가 있는 인증 토큰)
- AWS (S3, Identity and Access Management (IAM), Lambda, EventBridge)에 대한 권한 있는 액세스
Sentry 기본 요건 (ID, API 키, 조직 ID, 토큰) 수집
- Sentry에 로그인합니다.
- 조직 슬러그를 찾습니다.
- 설정 > 조직 > 설정 > 조직 ID로 이동합니다 (슬러그는 조직 이름 옆에 표시됨).
- 인증 토큰을 만듭니다.
- 설정 > 개발자 설정 > 개인 토큰으로 이동합니다.
- 새로 만들기를 클릭합니다.
- 범위 (최소):
org:read
,project:read
,event:read
- 토큰 값을 복사합니다 (한 번만 표시됨).
Authorization: Bearer <token>
와 같이 사용됩니다.
- (자체 호스팅인 경우) 기본 URL (예:
https://<your-domain>
)을 기록해 둡니다. 그렇지 않으면https://sentry.io
을 사용합니다.
Google SecOps용 AWS S3 버킷 및 IAM 구성
- 이 사용자 가이드(버킷 만들기)에 따라 Amazon S3 버킷을 만듭니다.
- 나중에 참조할 수 있도록 버킷 이름과 리전을 저장합니다 (예:
sentry-logs
). - 이 사용자 가이드(IAM 사용자 만들기)에 따라 사용자를 만듭니다.
- 생성된 사용자를 선택합니다.
- 보안 사용자 인증 정보 탭을 선택합니다.
- 액세스 키 섹션에서 액세스 키 만들기를 클릭합니다.
- 사용 사례로 서드 파티 서비스를 선택합니다.
- 다음을 클릭합니다.
- 선택사항: 설명 태그를 추가합니다.
- 액세스 키 만들기를 클릭합니다.
- CSV 파일 다운로드를 클릭하여 향후 참조할 수 있도록 액세스 키와 비밀 액세스 키를 저장합니다.
- 완료를 클릭합니다.
- 권한 탭을 선택합니다.
- 권한 정책 섹션에서 권한 추가를 클릭합니다.
- 권한 추가를 선택합니다.
- 정책 직접 연결을 선택합니다.
- AmazonS3FullAccess 정책을 검색합니다.
- 정책을 선택합니다.
- 다음을 클릭합니다.
- 권한 추가를 클릭합니다.
S3 업로드용 IAM 정책 및 역할 구성
- AWS 콘솔에서 IAM > 정책으로 이동합니다.
- 정책 만들기 > JSON 탭을 클릭합니다.
- 다음 정책을 복사하여 붙여넣습니다.
정책 JSON (다른 버킷 이름을 입력한 경우
sentry-logs
대체):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::sentry-logs/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::sentry-logs/sentry/events/state.json" } ] }
다음 > 정책 만들기를 클릭합니다.
IAM > 역할 > 역할 생성 > AWS 서비스 > Lambda로 이동합니다.
새로 만든 정책을 연결합니다.
역할 이름을
WriteSentryToS3Role
로 지정하고 역할 만들기를 클릭합니다.
Lambda 함수 만들기
- AWS 콘솔에서 Lambda > 함수 > 함수 만들기로 이동합니다.
- 처음부터 작성을 클릭합니다.
다음 구성 세부정보를 제공합니다.
설정 값 이름 sentry_to_s3
런타임 Python 3.13 아키텍처 x86_64 실행 역할 WriteSentryToS3Role
함수가 생성되면 코드 탭을 열고 스텁을 삭제한 후 다음 코드 (
sentry_to_s3.py
)를 붙여넣습니다.#!/usr/bin/env python3 # Lambda: Pull Sentry project events (raw JSON) to S3 using Link "previous" cursor for duplicate-safe polling import os, json, time from urllib.request import Request, urlopen from urllib.parse import urlencode, urlparse, parse_qs import boto3 ORG = os.environ["SENTRY_ORG"].strip() TOKEN = os.environ["SENTRY_AUTH_TOKEN"].strip() S3_BUCKET = os.environ["S3_BUCKET"] S3_PREFIX = os.environ.get("S3_PREFIX", "sentry/events/") STATE_KEY = os.environ.get("STATE_KEY", "sentry/events/state.json") BASE = os.environ.get("SENTRY_API_BASE", "https://sentry.io").rstrip("/") MAX_PROJECTS = int(os.environ.get("MAX_PROJECTS", "100")) MAX_PAGES_PER_PROJECT = int(os.environ.get("MAX_PAGES_PER_PROJECT", "5")) s3 = boto3.client("s3") HDRS = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json", "User-Agent": "chronicle-s3-sentry-lambda/1.0"} def _get_state() -> dict: try: obj = s3.get_object(Bucket=S3_BUCKET, Key=STATE_KEY) raw = obj["Body"].read() return json.loads(raw) if raw else {"projects": {}} except Exception: return {"projects": {}} def _put_state(state: dict): s3.put_object(Bucket=S3_BUCKET, Key=STATE_KEY, Body=json.dumps(state, separators=(",", ":")).encode("utf-8")) def _req(path: str, params: dict | None = None): url = f"{BASE}{path}" if params: url = f"{url}?{urlencode(params)}" req = Request(url, method="GET", headers=HDRS) with urlopen(req, timeout=60) as r: data = json.loads(r.read().decode("utf-8")) link = r.headers.get("Link") return data, link def _parse_link(link_header: str | None): """Return (prev_cursor, prev_has_more, next_cursor, next_has_more).""" if not link_header: return None, False, None, False prev_cursor, next_cursor = None, None prev_more, next_more = False, False parts = [p.strip() for p in link_header.split(",")] for p in parts: if "<" not in p or ">" not in p: continue url = p.split("<", 1)[1].split(">", 1)[0] rel = "previous" if 'rel="previous"' in p else ("next" if 'rel="next"' in p else None) has_more = 'results="true"' in p try: q = urlparse(url).query cur = parse_qs(q).get("cursor", [None])[0] except Exception: cur = None if rel == "previous": prev_cursor, prev_more = cur, has_more elif rel == "next": next_cursor, next_more = cur, has_more return prev_cursor, prev_more, next_cursor, next_more def _write_page(project_slug: str, payload: object, page_idx: int) -> str: ts = time.gmtime() key = f"{S3_PREFIX.rstrip('/')}/{time.strftime('%Y/%m/%d', ts)}/sentry-{project_slug}-{page_idx:05d}.json" s3.put_object(Bucket=S3_BUCKET, Key=key, Body=json.dumps(payload, separators=(",", ":")).encode("utf-8")) return key def list_projects(max_projects: int): projects, cursor = [], None while len(projects) < max_projects: params = {"cursor": cursor} if cursor else {} data, link = _req(f"/api/0/organizations/{ORG}/projects/", params) for p in data: slug = p.get("slug") if slug: projects.append(slug) if len(projects) >= max_projects: break # advance pagination _, _, next_cursor, next_more = _parse_link(link) cursor = next_cursor if next_more else None if not next_more: break return projects def fetch_project_events(project_slug: str, start_prev_cursor: str | None): # If we have a stored "previous" cursor, poll forward (newer) until no more results. # If not (first run), fetch the latest page, then optionally follow "next" (older) for initial backfill up to the limit. pages = 0 total = 0 latest_prev_cursor_to_store = None def _one(cursor: str | None): nonlocal pages, total, latest_prev_cursor_to_store params = {"cursor": cursor} if cursor else {} data, link = _req(f"/api/0/projects/{ORG}/{project_slug}/events/", params) _write_page(project_slug, data, pages) total += len(data) if isinstance(data, list) else 0 prev_c, prev_more, next_c, next_more = _parse_link(link) # capture the most recent "previous" cursor observed to store for the next run latest_prev_cursor_to_store = prev_c or latest_prev_cursor_to_store pages += 1 return prev_c, prev_more, next_c, next_more if start_prev_cursor: # Poll new pages toward "previous" until no more cur = start_prev_cursor while pages < MAX_PAGES_PER_PROJECT: prev_c, prev_more, _, _ = _one(cur) if not prev_more: break cur = prev_c else: # First run: start at newest, then (optionally) backfill a few older pages prev_c, _, next_c, next_more = _one(None) cur = next_c while next_more and pages < MAX_PAGES_PER_PROJECT: _, _, next_c, next_more = _one(cur) cur = next_c return {"project": project_slug, "pages": pages, "written": total, "store_prev_cursor": latest_prev_cursor_to_store} def lambda_handler(event=None, context=None): state = _get_state() state.setdefault("projects", {}) projects = list_projects(MAX_PROJECTS) summary = [] for slug in projects: start_prev = state["projects"].get(slug, {}).get("prev_cursor") res = fetch_project_events(slug, start_prev) if res.get("store_prev_cursor"): state["projects"][slug] = {"prev_cursor": res["store_prev_cursor"]} summary.append(res) _put_state(state) return {"ok": True, "projects": len(projects), "summary": summary} if __name__ == "__main__": print(lambda_handler())
구성 > 환경 변수로 이동합니다.
수정 > 새 환경 변수 추가를 클릭합니다.
다음 표에 제공된 환경 변수를 입력하고 예시 값을 실제 값으로 바꿉니다.
환경 변수
키 예제 값 설명 S3_BUCKET
sentry-logs
데이터가 저장될 S3 버킷 이름입니다. S3_PREFIX
sentry/events/
객체의 선택적 S3 접두사 (하위 폴더)입니다. STATE_KEY
sentry/events/state.json
선택적 상태/체크포인트 파일 키입니다. SENTRY_ORG
your-org-slug
Sentry 조직 슬러그입니다. SENTRY_AUTH_TOKEN
sntrys_************************
org:read, project:read, event:read 권한이 있는 Sentry 인증 토큰 SENTRY_API_BASE
https://sentry.io
Sentry API 기본 URL (자체 호스팅: https://<your-domain>
)MAX_PROJECTS
100
처리할 최대 프로젝트 수입니다. MAX_PAGES_PER_PROJECT
5
실행당 프로젝트별 최대 페이지 수입니다. 함수가 생성되면 해당 페이지에 머무르거나 Lambda > 함수 > your-function을 엽니다.
구성 탭을 선택합니다.
일반 구성 패널에서 수정을 클릭합니다.
제한 시간을 5분 (300초)으로 변경하고 저장을 클릭합니다.
EventBridge 일정 만들기
- Amazon EventBridge > 스케줄러 > 일정 만들기로 이동합니다.
- 다음 구성 세부정보를 제공합니다.
- 반복 일정: 요금 (
1 hour
) - 타겟: Lambda 함수
sentry_to_s3
- 이름:
sentry-1h
.
- 반복 일정: 요금 (
- 일정 만들기를 클릭합니다.
(선택사항) Google SecOps용 읽기 전용 IAM 사용자 및 키 만들기
- AWS 콘솔에서 IAM > 사용자로 이동합니다.
- Add users를 클릭합니다.
- 다음 구성 세부정보를 제공합니다.
- 사용자:
secops-reader
를 입력합니다. - 액세스 유형: 액세스 키 - 프로그래매틱 액세스를 선택합니다.
- 사용자:
- 사용자 만들기를 클릭합니다.
- 최소 읽기 정책 (맞춤) 연결: 사용자 > secops-reader > 권한 > 권한 추가 > 정책 직접 연결 > 정책 만들기
JSON:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::sentry-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::sentry-logs" } ] }
이름 =
secops-reader-policy
정책 만들기 > 검색/선택 > 다음 > 권한 추가를 클릭합니다.
secops-reader
의 액세스 키를 만듭니다(보안 사용자 인증 정보 > 액세스 키).액세스 키 만들기를 클릭합니다.
.CSV
을 다운로드합니다. (이 값은 피드에 붙여넣습니다.)
Sentry 로그를 수집하도록 Google SecOps에서 피드 구성
- SIEM 설정> 피드로 이동합니다.
- + 새 피드 추가를 클릭합니다.
- 피드 이름 필드에 피드 이름을 입력합니다 (예:
Sentry Logs
). - 소스 유형으로 Amazon S3 V2를 선택합니다.
- 로그 유형으로 Sentry를 선택합니다.
- 다음을 클릭합니다.
- 다음 입력 파라미터의 값을 지정합니다.
- S3 URI:
s3://sentry-logs/sentry/events/
- 소스 삭제 옵션: 환경설정에 따라 삭제 옵션을 선택합니다.
- 최대 파일 기간: 지난 일수 동안 수정된 파일을 포함합니다. 기본값은 180일입니다.
- 액세스 키 ID: S3 버킷에 대한 액세스 권한이 있는 사용자 액세스 키입니다.
- 보안 비밀 액세스 키: S3 버킷에 액세스할 수 있는 사용자 보안 비밀 키입니다.
- 애셋 네임스페이스: 애셋 네임스페이스입니다.
- 수집 라벨: 이 피드의 이벤트에 적용된 라벨입니다.
- S3 URI:
- 다음을 클릭합니다.
- 확정 화면에서 새 피드 구성을 검토한 다음 제출을 클릭합니다.
도움이 더 필요하신가요? 커뮤니티 회원 및 Google SecOps 전문가로부터 답변을 받으세요.