
안전한 AI DevOps 에이전트 구축하기: Slack 진단부터 검증된 Pull Request까지
요약
운영 환경에서 안전하게 작동하는 AI DevOps 에이전트 구축을 위한 아키텍처와 보안 전략을 다룹니다. 모델의 프롬프트에 의존하기보다 런타임 경계와 5단계 운영 모델(탐지, 제안, 알림, 승인, 실행)을 통해 시스템의 안정성을 확보하는 방법을 설명합니다.
핵심 포인트
- 모델 외부의 런타임 경계(Runtime Boundaries)를 통한 보안 확보
- 읽기/쓰기 경로 분리 및 수명이 짧은 자격 증명 사용
- 탐지부터 실행까지 이어지는 5단계 운영 루프 모델
- 사람의 승인을 거치는 불변의 실행 계획(Immutable Approval Plans) 구조
AI 에이전트에게 운영 환경(production)에 대한 접근 권한을 부여하는 것은 쉽습니다. 하지만 에이전트가 무엇을 읽을 수 있는지, 무엇을 변경할 수 있는지, 그리고 속임수에 빠졌을 때 어떤 일이 발생하는지를 정확하게 설명하는 것은 훨씬 더 어렵습니다.
SlackOps DevOps 에이전트는 Slack에서 장애를 조사할 수 있는 읽기 전용(read-only) 어시스턴트로 시작되었습니다. 첫 번째 버전은 Slack과 웹 대시보드가 하나의 이벤트 기반 작업 큐(event-driven job queue)를 공유할 수 있음을 증명했습니다. V2는 더 엄격한 질문을 던집니다:
에이전트가 조용히 실행할 수 있는 권한을 전혀 갖지 않은 상태에서, 실제 운영 데이터를 조사하고 변경 사항을 준비할 수 있는가?
그 답은 더 나은 시스템 프롬프트(system prompt)에 있지 않습니다. 그것은 모델 외부의 런타임 경계(runtime boundaries) 세트입니다: 고정된 읽기 어댑터(read adapters), 격리된 신뢰할 수 없는 데이터(untrusted data), 불변의 승인 계획(immutable approval plans), 수명이 짧은 자격 증명(short-lived credentials), 결정론적 실행(deterministic execution), 제한된 네트워크 이그레스(network egress), 그리고 관찰된 동작으로부터 구축된 감사 추적(audit trail)입니다.
이 글에서는 현재의 아키텍처, V1 이후 변경된 사항, 그리고 실제 AWS 환경에서 검증된 내용을 설명합니다.
그림 1 — 현재 아키텍처. Slack, 웹 대시보드, 상주 에이전트(resident agent), 그리고 이벤트 기반 Lambda 프로듀서(producer)는 하나의 DynamoDB 제어 평면(control plane)을 공유합니다. 읽기 및 쓰기 경로는 분리된 상태로 유지됩니다.
운영 모델: 탐지, 제안, 알림, 승인, 실행
시스템은 5단계 루프를 따릅:
- 탐지 (Detect) — CloudWatch 알람, Slack 메시지, 예약된 모니터 또는 웹 요청이 잠재적인 문제를 식별합니다.
- 제안 (Propose) — 시스템이 제한된 범위의 증거(evidence)를 수집하고 DynamoDB에 작업을 생성합니다.
- 알림 (Notify) — Slack과 웹 대시보드에 동일한 작업, 증거 및 제안된 변경 사항이 표시됩니다.
- 승인 (Approve) — 사람이 차이점(diff)을 검토합니다. 승인은 하나의 정확한 실행 계획(execution plan)에 결합됩니다.
- 실행 및 검증 (Execute and verify) — 런타임(runtime) 코드가 계획을 재확인하고, 수명이 짧은 쓰기 자격 증명(write credential)을 획득하며, 풀 리퀘스트(pull request)를 생성하고, 원격 결과를 검증하고, 자격 증명을 취소한 후 결과를 기록합니다.
이벤트 기반 프로듀서(event-driven producer)는 의도적으로 제한되어 있습니다. CloudWatch 알람은 EventBridge를 통해 AWS Lambda 함수로 흐르며, 이 함수는 EC2 워커(worker)가 중지된 상태에서도 DynamoDB에 제안을 생성할 수 있습니다. Lambda는 제안을 승인하거나 실행할 수 없습니다. Lambda는 단지 Slack, 대시보드 및 상주 모니터(resident monitor)가 사용하는 것과 동일한 큐(queue)에 작업만 추가할 뿐입니다.
이를 통해 워커를 항상 켜져 있는 권한 있는 서비스(privileged service)로 만들지 않고도 저비용의 탐지 경로를 사용할 수 있습니다.
V1 이후 변경된 사항
V1은 진단을 위해 범용 읽기 전용 AWS API MCP 서버를 사용했습니다. 읽기 전용 액세스는 안전해 보였지만, 노출 범위(surface)가 작업에 필요한 것보다 여전히 넓었습니다. 범용 API 도구는 관련 없는 데이터를 노출할 수 있으며, 해당 도구의 결과는 다른 증거들과 마찬가지로 신뢰할 수 없는 데이터 경계(untrusted-data boundary)를 통과하지 않았습니다.
V2는 해당 런타임 의존성을 제거했습니다.
logs, diagnose, detect의 경우, 이제 명령별 boto3 어댑터가 필요한 AWS 데이터만 가져옵니다. 결과는 제한(capped) 및 정화(sanitized)되어 모델에 도달하기 전에 단일 <untrusted_data> 경계 내에 배치됩니다. 모델은 이러한 L0 분석 명령을 위한 AWS 도구를 받지 않습니다. 즉, 도구 허용 목록(allowlist)이 비어 있습니다.
별도의 내부 FastMCP 서버는 유지되지만, 그 목적은 더 좁아졌습니다. 이 서버는 SlackOps 제어 평면(control plane)에 제안을 생성할 수 있습니다. 이는 일반적인 AWS 읽기 노출 영역이 아닙니다.
이 구분이 중요합니다: 애플리케이션은 증거를 수집하고, 모델은 증거를 분석합니다.
다섯 가지 런타임 경계 (Runtime boundaries)
1. 범용 도구 대신 고정된 읽기 (Fixed reads instead of generic tools)
각 진단 명령은 명시적인 서비스, API, 계정, 리전 (Region), 로그 접두사 (log prefix) 및 결과 제한을 가진 작은 어댑터 (adapter)를 소유합니다. 정책은 프롬프트 지시 사항 (prompt instruction)이 아닌 결정론적 코드 (deterministic code)로 표현됩니다.
요청된 리소스가 구성된 범위를 벗어나는 경우, 어댑터는 데이터를 가져오기 전에 요청을 거부합니다. 이 거부 기록은 정책 이벤트 (policy event)로 기록됩니다.
2. 신뢰할 수 없는 콘텐츠를 위한 단일 경계 (One boundary for untrusted content)
Slack 메시지, CloudWatch 로그 라인, Kubernetes 출력, Git 출력 및 어댑터 오류는 모델의 관점에서 데이터입니다. 여기에는 “이전 지침을 무시하세요 (ignore previous instructions)” 또는 위조된 닫기 태그와 같은 텍스트가 포함될 수 있습니다.
SlackOps는 경계와 유사한 텍스트를 정화(sanitize)하고 외부 콘텐츠를 한 번 감쌉니다:
<untrusted_data>
...경계가 지정된 운영 증거 (bounded operational evidence)...
</untrusted_data>
이 래퍼 (wrapper)가 완벽한 프롬프트 인젝션 (prompt-injection) 해결책으로 제시되는 것은 아닙니다. 더 강력한 속성은 L0 분석 단계에 실행 도구가 없다는 점입니다. 설령 모델이 데이터 내의 악의적인 지침을 따르더라도, 직접적인 실행 경로가 없습니다.
3. 분리된 신원 및 단기 자격 증명 (Separate identities and short-lived credentials)
EC2 인스턴스 프로파일 (instance profile)은 부트스트랩 (bootstrap) 용도로만 사용됩니다. 런타임 서비스는 별도의 1시간짜리 AWS STS 자격 증명 (credentials)을 받으며, 이는 루트 소유 (root-owned) 프로세스에 의해 45분마다 갱신됩니다. 런타임, 내부 MCP, 그리고 감사 기록기 (audit writer)는 서로 다른 역할 (role)을 사용합니다.
에이전트 서비스는 EC2 인스턴스 메타데이터를 직접 쿼리할 수 없으며, 루트 소유의 감사 환경을 읽을 수 없습니다. 전용 감사 역할 (audit role)은 배포 소유의 CloudWatch Logs 보안 싱크 (security sink)에만 추가(append)할 수 있습니다. 일반 런타임 역할은 해당 싱크에 대해 명시적인 거부 (explicit deny) 설정을 가집니다.
GitHub 쓰기 작업을 위해, 워커 (worker)는 개인 액세스 토큰 (personal access token)이나 상시 유지되는 gh 자격 증명을 보유하지 않습니다. 사람이 특정 계획을 승인한 후, 런타임은 리포지토리 및 권한 범위가 지정된 GitHub App 설치 토큰 (installation token)을 생성합니다. 이 토큰은 단 하나의 자식 프로세스에만 전달되며, 모든 종료 경로에서 취소됩니다.
4. 승인은 정확한 상태를 결합합니다 (Approval binds an exact state)
표시된 diff (차이점)만으로는 충분하지 않습니다. 승인 이후에 파일이 변경될 수 있으며, 자연어 요청은 이를 실행할 정확한 툴 체인 (tool chain)을 식별하지 못합니다.
SlackOps는 다음과 같은 내용을 포함하는 정형화된 ExecutionPlan (실행 계획)을 구축합니다:
- 요청 및 diff 해시 (hashes);
- 승인된 경로 및 워크스페이스 루트 (workspace root);
- 정책 버전, AWS 계정 및 리전 (Region);
- 전체 실행 툴 체인 (tool chain);
- 선언된 기능 (capabilities), 위험 점수 (risk score), 그리고 승인 시점의 위험 상한선 (risk ceiling).
사람의 승인은 해당 정확한 계획의 해시를 저장합니다. 실행 직전에 워커 (worker)는 이를 다시 계산하여 비교합니다. 변경된 diff, 누락된 계획, 확장된 툴 체인, 경로 탐색 (path traversal), 심볼릭 링크 (symlink), 추적되지 않은 파일 (untracked file), 워크스페이스 불일치, 또는 기능 확장 등이 발생하면 작업은 'fail closed' (실패 시 차단) 상태가 됩니다.
따라서 승인이란 "나중에 이와 유사한 작업을 수행하라"가 아니라, **"이 검토된 변경 사항을 이 정책 하에 실행하라"**는 의미입니다.
5. 모델을 쓰기 경로 (write path)에서 제거합니다
준비 (prepare) 단계에서는 모델을 사용하여 변경 사항을 제안하고 편집할 수 있습니다. 하지만 승인 이후에는 더 이상 모델이 필요하지 않습니다.
app.pr_execution.open_pr은 다음과 같은 고정된 시퀀스를 수행합니다:
브랜치 생성 (create branch)
→ 승인된 경로만 추가 (add only approved paths)
→ 커밋 (commit)
...
모델의 출력값이 푸시 (push) 방식을 결정하지 않습니다. 이는 더 안전할 뿐만 아니라 더 신뢰할 수 있습니다. 즉, 창의적인 부분은 권한이 시작되기 전에 종료됩니다.
그림 2 — 실제 Slack 제안은 제한된 변경 사항, diff 미리보기, 예상 사용량, 그리고 명시적인 검토 (Review), 승인 (Approve), 거절 (Reject) 컨트롤을 보여줍니다.
명령 경계는 argv 스키마입니다
도구 허용 목록 (allowlists)은 유용하지만, 이를 최종적인 보안 경계로 취급하지는 않습니다.
테스트 중에 echo만 허용하는 것처럼 보였던 헤드 매칭 (head-matching) Bash 패턴이 체이닝된 명령 (chained command) 또한 허용한다는 사실이 발견되었습니다. 따라서 SlackOps는 모든 Bash 호출을 정규화 (normalize)하고, PreToolUse 훅 (hook)에서 명령별 인자 스키마 (argument schema)를 통해 이를 검증합니다. 구분자 (separators), 치환 (substitutions), 리다이렉션 (redirections), 예상치 못한 플래그 (flags), 그리고 알 수 없는 서브커맨드 (subcommands)는 모두 차단 (fail closed)됩니다.
명령을 추가하려면 허용 목록 (allowlist) 항목과 그에 일치하는 가드 스키마 (guard schema)가 모두 필요합니다. 임포트 시점의 교차 검증 (import-time cross-check)을 통해 일관되지 않은 설정은 거부됩니다.
이 가드는 이후 감사 (audit) 및 완료 게이트 (completion gates)에서 사용되는 기능 분류기 (capability classifier) 역할도 수행합니다. 시스템은 “명령이 실제로 무엇을 수행했는가”를 파악하기 위해 잠재적으로 불일치가 발생할 수 있는 별도의 파서 (parser)를 유지하지 않습니다.
기능은 선언될 뿐만 아니라 측정됩니다
허용된 모든 도구는 다음 다섯 가지 기능 중 하나를 가집니다:
read · sensitive-read · write-low · write-high · privileged
분류되지 않은 도구는 오류로 처리됩니다. 플랜 (plan)은 고유한 기능들을 합산하여 그 결과를 RISK_CEILING=10과 비교합니다. 영향력이 크거나 권한이 높은 (privileged) 기능은 별도의 예외 목록 없이도 이 임계값 (ceiling)을 초과합니다. L2 실행은 여전히 비활성화 상태로 유지됩니다.
Claude Code는 stream-json 출력을 사용하여 실행되므로, 워커 (worker)가 관찰된 도구 호출을 재구성할 수 있습니다. 워커는 동일한 명령 가드를 통해 각 호출을 해결하고, 이를 감사 트리 (audit tree)의 자식 단계 (child step)로 기록하며, 관찰된 기능 (observed capabilities)을 다시 계산합니다.
관찰된 기능이 승인된 집합 또는 점수를 초과하면 작업은 DONE 상태가 될 수 없습니다. 대신 별도의 capability_drift 감사 이벤트와 함께 실패 처리됩니다.
이를 통해 텔레메트리 (telemetry)가 강제 집행 지점 (enforcement point)으로 전환됩니다. 시스템은 무엇이 허용되었는지뿐만 아니라, 실제로 무엇이 실행되었는지도 확인합니다.
네트워크 이그레스 (Network egress)는 에이전트 정책의 일부입니다
워커에는 인바운드 포트 (inbound port)가 없습니다. Slack은 소켓 모드 (Socket Mode)를 사용하며, 대시보드는 에이전트에 콜백 엔드포인트 (callback endpoint)를 여는 대신 DynamoDB를 통해 통신합니다.
아웃바운드 트래픽 (Outbound traffic)은 필수 서비스에 대한 소규모 목적지 허용 목록 (allowlist)을 가진 localhost Squid 프록시를 통해 전달됩니다. 직접적인 이그레스 (Direct egress) 및 목록에 없는 목적지는 차단됩니다. 프록시 감사 기록 (Proxy audit records)에는 보안 로그에 민감한 쿼리 데이터가 복사되는 것을 방지하기 위해 요청된 URL 대신 거부 메타데이터 (denial metadata)가 포함됩니다.
이는 "치명적인 삼중주 (lethal trifecta)" 프레임워크를 따릅니다. 즉, 하나의 에이전트가 프라이빗 데이터 (private data), 신뢰할 수 없는 콘텐츠 (untrusted content), 외부 통신 (external communication)을 모두 보유하게 될 때 이 세 가지 요소가 위험해진다는 것입니다. SlackOps는 각 단계를 좁히며, 이그레스 경계 (egress boundary)가 데이터 유출 (data exfiltration)에 대한 최종 장벽 역할을 합니다.
구현 내용을 OWASP에 매핑하기
| OWASP 리스크 | SlackOps 제어 (control) | 런타임 증거 (Runtime evidence) |
|---|---|---|
| LLM01 프롬프트 인젝션 (Prompt Injection) | sanitizer, 하나의 <untrusted_data> 경계, L0 도구 = 0 | 실행 경로 없이 인젝션 명령이 거부됨 |
| ... |
이 매핑은 OWASP Top 10 for Large Language Model Applications 및 OWASP Top 10 for Agentic Applications를 설계 참조로 사용합니다. 이는 컴플라이언스 (compliance) 준수 선언이 아닙니다.
하나의 제어 평면 (control plane), 다수의 프로듀서 (producers)
DynamoDB는 단일 테이블 디자인 (single-table design)을 통해 작업 (jobs), 감사 이벤트 (audit events), 텔레메트리 (telemetry), 대화 (conversations), 메트릭 (metrics) 및 탐지 구성 (detection configuration)을 저장합니다. 조건부 쓰기 (Conditional writes)를 통해 상태 머신 (state machine)과 원자적 점유 (atomic claim)를 구현합니다.
중요한 속성은 모든 데이터가 하나의 테이블에 존재한다는 것이 아닙니다. 모든 프로듀서가 동일한 전이 규칙 (transition rules)을 따른다는 점입니다.
PENDING → RUNNING → AWAITING_APPROVAL → APPROVED → DONE
└────────→ REJECTED
Slack, 웹 대시보드, 상주 모니터 (resident monitor), 그리고 Lambda 알람 프로듀서는 별도의 승인 의미론 (approval semantics)을 임의로 만들어낼 수 없습니다. 워커 (worker)는 조건부 쓰기를 통해 작업을 점유하므로, 동시에 작동하는 폴러 (pollers)가 작업을 두 번 실행할 수 없습니다. 중단된 RUNNING 작업은 두 번째 푸시로 재실행되는 대신 실패한 것으로 복구됩니다.
그림 3 — 컴팩트한 운영 루프: 에이전트가 감지 및 제안하고, 사람이 변경 사항을 검토하며, 결정론적 경로(deterministic path)가 승인된 결과를 실행합니다.
검증된 사항
본 프로젝트는 자동화된 체크(automated checks), 라이브 리허설(live rehearsals), 그리고 스캐폴딩(scaffolding)을 구분합니다:
make check: 2026-07-17 기준으로 Ruff, 엄격한 mypy, 그리고 문서 예산 게이트(documentation budget gate)가 모두 통과되었으며, 563개의 테스트를 통과했습니다.- AWS 경계 리허설(AWS boundary rehearsal): 역할 분리, 1시간 유효 기간의 자격 증명(credentials), 45분 주기 갱신, IMDS 거부, 고정된 읽기 범위(read scope), 직접 송신(direct-egress) 거부, 허용 목록(allowlisted) 프록시 액세스, 그리고 루트 소유의 감사 싱크(audit sink)가 2026-07-15에 새로운 EC2 인스턴스에서 검증되었습니다.
- GitHub 쓰기 경로(GitHub write path): 자연어 제안, 준비, 사람의 승인, 결정론적 실행, 원격 검증, 그리고 자격 증명 취소 과정을 통해 2026-07-17에 실제 Pull Request #3부터 #5까지 생성되었습니다.
- Slack 네이티브 승인: 버튼과 “변경 사항 검토(Review change)” 모달이 실패 시 차단(fail-closed) 방식의 승인자 허용 목록(approver allowlist)과 함께 검증되었습니다.
- 관리형 AWS MCP: 별도 계정 파일럿은 CI로 잠긴 스캐폴딩(scaffold) 상태로 유지됩니다. SlackOps 런타임 내에서 관리형 엔드포인트, 역할(role) 또는 세션은 활성화되지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기