Notification
새로운 알림이 없습니다.

[Observability] Datadog Alert 분석 SRE OpsAgent 구축(3)

Datadog, SRE, OpsAgent
Datadog Logo
코드가 돌아가는 걸 확인했으면, 다음은 "어디에 어떻게 올릴 것인가"다. 로컬에서 잘 되는 건 반쪽짜리고, 알람이 올 때 자동으로 뜨고 꺼지는 환경을 만들어야 진짜 쓸 수 있다. 이번 글에서는 Cloud Run 배포, CI/CD 파이프라인, 그리고 비밀값 관리를 다룬다.
1. Cloud Run 배포
왜 Cloud Run인가
SRE-OpsAgent는 Datadog 알람이 올 때만 실행된다. 트래픽이 없으면 인스턴스가 필요 없고 알람이 몰리면 여러 인스턴스가 필요하다. 이런 패턴에 서버리스가 딱 맞는다.
Cloud Run은 요청이 없으면 인스턴스를 0으로 줄이고(min-instances 0), 동시 요청이 많으면 max-instances까지 자동으로 늘린다. 알람이 없는 밤 시간대에는 비용이 0원이고 장애가 터져서 알람이 동시에 여러 개 오면 알아서 스케일아웃한다.
배포 스크립트 구조
배포는 deploy.sh 하나로 처리한다.
gcloud run deploy "$SERVICE" \ --source . \ --project "$PROJECT_ID" \ --region "$REGION" \ --allow-unauthenticated \ --timeout 600 \ --memory 1Gi \ --min-instances 0 \ --max-instances 5 \ --set-secrets "GEMINI_API_KEY=gemini-api:latest,..."
--source .는 Cloud Run의 소스 배포 기능이다. Dockerfile을 감지해서 Cloud Build로 이미지를 빌드하고, Artifact Registry에 푸시한 뒤, Cloud Run에 배포까지 한 번에 처리한다. 초기 개발 단계에서 빠르게 확인할 때 유용하다.
운영 환경에서는 Jenkins 파이프라인을 쓰는데, 뒤에서 다룬다.
타임아웃과 메모리 설정 근거
--timeout 600 --memory 1Gi
타임아웃 600초 (10분) — LLM 에이전트가 도구를 여러 번 호출하면 시간이 걸린다. 한 턴에 Datadog API 호출이 2~3초, LLM 응답이 5~10초. 이걸 3~5턴 반복하면 한 사이클에 30초~1분이다. 여기에 MCP 세션 초기화, skill prefetch, Slack 전송까지 더하면 전체 처리 시간은 보통 1~3분이다.
Cloud Run 기본 타임아웃이 300초(5분)인데, 복잡한 알람에서 도구 호출이 많아지거나 Datadog API 응답이 느려지면 5분을 넘길 수 있다. 600초로 잡으면 여유가 생긴다. Cloud Run 최대값은 3600초(1시간)까지 가능하지만 10분이면 충분하다.
메모리 1Gi — Python 런타임 자체가 200~300MB를 쓰고, FastAPI + uvicorn이 추가로 50~100MB. 여기에 httpx 커넥션, Datadog API 응답 페이로드(로그 검색 결과가 크면 수 MB), LLM 요청/응답 버퍼까지 합치면 500~700MB 정도가 실제 사용량이다. 512Mi로도 돌아가긴 하지만 로그 검색 결과가 큰 경우에 OOM이 날 수 있어서 1Gi로 설정했다.
스케일링 설정
--min-instances 0 --max-instances 5
min-instances 0은 요청이 없으면 인스턴스를 완전히 내린다. 콜드 스타트(첫 요청 시 컨테이너 기동)에 3~5초 걸리지만, SRE 알람 분석에서 이 정도 지연은 문제가 되지 않는다.
max-instances 5는 동시에 처리하는 알람 수의 상한이다. 대규모 장애에서 알람이 동시에 10~20개 올 수 있는데, 5개 인스턴스면 순차적으로 처리하더라도 밀리지 않는다. 비용 제어 목적도 있다.
2. Jenkins + ArgoCD CI/CD 파이프라인
Jenkins와 ArgoCD를 이용한 GitOps 배포 파이프라인이다. 이 부분은 기존 블로그 글에서 여러 차례 다뤘으므로 간략하게 핵심만 짚는다.
파이프라인 흐름
Git Push → Jenkins 트리거 → Docker Build + Push (Artifact Registry) → GitOps 저장소 이미지 태그 업데이트 → ArgoCD 자동 감지 → Cloud Run 배포
Jenkinsfile 단계별
1단계: Git Clone + Docker Build
stage('Build Docker Image') { steps { withCredentials([file(credentialsId: 'gcp-sa-cicd', variable: 'GCP_SERVICE_ACCOUNT_KEY')]) { script { def commitHash = env.GIT_COMMIT.take(7) env.APP_VERSION = "${projectVersion}-${commitHash}" sh "gcloud auth activate-service-account \ --key-file=${GCP_SERVICE_ACCOUNT_KEY}" sh "docker build -t ${REGISTRY}/${IMAGE}:${APP_VERSION} ." } } } }
소스를 클론하고, GCP 서비스 계정으로 인증한 뒤, Docker 이미지를 빌드한다. 이미지 태그는 v0.0.1-abc1234 형태로, 프로젝트 버전 + 커밋 해시 7자리를 조합한다. 어떤 커밋에서 빌드된 이미지인지 추적할 수 있다.
2단계: Push + 현재 리비전 확인
빌드된 이미지를 Artifact Registry에 푸시하고, manifest inspect로 정상 업로드를 확인한 뒤, 로컬 이미지를 삭제한다. 그리고 현재 Cloud Run에 떠 있는 리비전 이름을 조회해서 배포 전후 비교용으로 기록한다.
3단계: ArgoCD 이미지 태그 업데이트
stage('ArgoCD Image Tag Update') { steps { script { dir("cd-repository/${repoName}") { sh """ sed -i "s|image: .*|image: ${REGISTRY}/${IMAGE}:${APP_VERSION}|" \ ./sre-opsagent.yaml """ sh """ git commit -m "Update image tag to ${APP_VERSION}" git push origin ${DEPLOY_BRANCH} """ } } } }
핵심은 여기다. 배포용 GitOps 저장소에 있는 YAML 파일의 이미지 태그를 새 버전으로 바꾸고 커밋한다. ArgoCD가 이 저장소를 감시하다가 커밋이 들어오면 변경된 이미지 태그를 감지해서 Cloud Run 서비스를 자동으로 업데이트한다.
소스 저장소와 배포 저장소를 분리하는 이유는 관심사 분리다. 소스 저장소에는 애플리케이션 코드만, 배포 저장소에는 인프라 설정(YAML)만 둔다. 배포 저장소의 git log만으로 "어떤 버전이 배포되었는가"를 추적할 수 있다.
4단계: Slack 알림
빌드 성공/실패 시 Slack으로 알림을 보낸다. Jenkins의 post 블록에서 처리한다.
post { success { slackNotify("✅ Build ${APP_VERSION} Success!") } failure { slackNotify("❌ Build ${APP_VERSION} Failed!") } }
3. 보안 — 비밀값 관리와 웹훅 검증
문제: Cloud Run은 퍼블릭
Cloud Run에 배포하면 HTTPS URL이 발급된다. 문제는 Datadog 웹훅이 GCP IAM 인증을 지원하지 않는다는 점이다. 이 URL을 아는 사람은 누구나 POST 요청을 보낼 수 있다.
--allow-unauthenticated로 열어놓되, 애플리케이션 레벨에서 검증해야 한다.
X-DD-Secret 헤더 검증
Datadog 웹훅 설정에서 커스텀 헤더를 추가할 수 있다. 여기에 공유 시크릿을 넣고, 코드에서 검증한다.
@app.post("/webhook") async def datadog_webhook( request: Request, x_dd_secret: str | None = Header(default=None, alias="X-DD-Secret"), ): if x_dd_secret != settings.webhook_secret: raise HTTPException(status_code=401, detail="invalid secret")
Datadog 웹훅 설정:
• URL: https://<cloud-run-url>/webhook
• Custom Headers: X-DD-Secret: <시크릿 값>
시크릿이 맞지 않으면 401을 반환하고 요청을 처리하지 않는다. URL이 노출되더라도 시크릿 없이는 에이전트를 기동할 수 없다.
비밀값 관리: Secret Manager
이 프로젝트에서 다루는 비밀값은 5개다.
비밀값 용도
GEMINI_API_KEY Gemini LLM API 인증
DD_API_KEY Datadog API 인증
DD_APP_KEY Datadog Application 인증
WEBHOOK_SECRET 웹훅 요청 검증
SLACK_BOT_TOKEN Slack 메시지 발송
이 값들을 환경변수, Dockerfile, 코드에 넣으면 안 된다. GCP Secret Manager에 등록하고, Cloud Run이 런타임에 주입하게 한다.
# deploy.sh에서 비밀값 등록 create_or_update_secret "gemini-api" "${GEMINI_API_KEY}" create_or_update_secret "dd-api" "${DD_API_KEY}" create_or_update_secret "dd-app" "${DD_APP_KEY}" create_or_update_secret "wh-secret" "${WEBHOOK_SECRET}" create_or_update_secret "slack-bot" "${SLACK_BOT_TOKEN}" # Cloud Run 배포 시 시크릿 마운트 --set-secrets "GEMINI_API_KEY=gemini-api:latest,DD_API_KEY=dd-api:latest,..."
--set-secrets 옵션을 쓰면 Cloud Run이 컨테이너 시작 시 Secret Manager에서 값을 가져와 환경변수로 주입한다. 코드에서는 os.environ.get("GEMINI_API_KEY")로 읽으면 된다.
create_or_update_secret 함수는 시크릿이 이미 존재하면 새 버전을 추가하고, 없으면 새로 생성한다. Secret Manager는 버전 관리를 지원하므로 키를 로테이션할 때 이전 버전을 유지하면서 새 버전을 추가할 수 있다.
시작 시 검증
# main.py — 모듈 로드 시점에 실행 settings = load_settings()
load_settings()는 필수 환경변수 6개를 모두 확인하고, 하나라도 없으면 RuntimeError를 던진다.
def _require(name: str) -> str: val = os.environ.get(name) if not val: raise RuntimeError(f"필수 환경변수 누락: {name}") return val
모듈 로드 시점에 실행하는 이유는 "빨리 죽기" 전략이다. 비밀값이 누락된 채로 배포가 되면, 알람이 올 때까지는 문제를 모른다. 시작 시 검증하면 배포 직후 컨테이너가 크래시하고, Cloud Run의 헬스체크가 이를 감지해서 롤백한다.
Datadog 서비스 계정 권한
Datadog Application Key는 서비스 계정으로 발급한다. 사용자 개인 키를 쓰면 해당 사용자가 퇴사하거나 권한이 변경될 때 에이전트가 멈춘다.
서비스 계정의 권한은 최소한으로 설정한다.
metrics_read — 메트릭 조회 logs_read_data — 로그 검색 monitors_read — 모니터 상세 events_read — 배포/변경 이벤트
현재는 읽기 전용이다. 에이전트가 인프라를 변경하는 기능은 아직 없다. 추후 자동 조치(스케일링, 롤백 등)를 추가하면 그때 권한을 확장하되, 별도 서비스 계정으로 분리할 계획이다.
정리
배포와 보안 관련 설정을 정리하면 아래와 같다.
항목 설정 근거
타임아웃 600초 LLM + 도구 호출 3~5턴에 1~3분, 여유분 포함
메모리 1Gi Python + API 응답 버퍼, OOM 방지
스케일링 0~5 인스턴스 비용 0원(대기) ~ 동시 5건 처리
인증 X-DD-Secret 헤더 Datadog이 IAM 미지원, 애플리케이션 레벨 검증
비밀값 Secret Manager 코드/이미지에 노출 안 됨, 버전 관리 지원
CI/CD Jenkins → ArgoCD 소스/배포 저장소 분리, 이미지 태그 기반 GitOps
DD 권한 읽기 전용 4개 스코프 최소 권한 원칙, 서비스 계정
끝으로
3편에 걸쳐 설계부터 배포까지 정리했다. 만들면서 느낀 점을 짧게 남긴다.
LLM에게 날것을 주지 마라. raw JSON을 넘기면 파싱에 턴을 쓰고 시간대를 틀린다. 코드가 잘하는 일은 코드에게.
지시보다 예시. "병렬 호출하라"를 아무리 써도 안 듣다가, Few-shot 예시 하나에 바로 바뀌었다.
도구는 적을수록 좋다. 25개 전부 열어주면 고민에 턴을 낭비한다. 6개로 줄이니 바로 본론.
SDK가 안 되면 직접 만들어라. SSE 호환 문제를 107줄 POST 클라이언트로 해결. 스펙을 보면 생각보다 간단하다.
읽기 전용부터. 자동 조치는 신뢰가 쌓인 뒤에. 처음부터 쓰기 권한을 주면 사고난다.
LLM 에이전트의 품질은 모델보다 주변 엔지니어링에서 갈렸다. 시행착오가 가장 많았던 부분이고, 배운 것도 가장 많았다.
Datadog Observability
Stan Cloud
Stan Cloud
An avid cloud engineering Fan
대화 참여하기
댓글 쓰기