시스템

[개선기] AI SRE Auto-Remediator v3.0 - 텔레그램 봇 연동과 "이상한 명령어" 해결기

달빛궁전- 2026. 10. 11. 15:09

이전 글: AI SRE Auto-Remediator 구축기 (seonggi.kr/295)
GitHub 저장소: https://github.com/SeongGi/AI-SRE-System

AI SRE Auto-Remediator v3.0 대표 이미지


며칠 운영해보니 터져 나온 문제들

지난번 글(seonggi.kr/295)에서 리눅스 시스템 로그를 실시간으로 지켜보다가 에러가 발생하면 Gemini가 원인을 분석하고 복구 명령어를 슬랙으로 제안하는 구조를 만들어 올렸었습니다.

구상할 때만 해도 꽤 그럴듯해 보였는데, 실제로 서버에 올려두고 며칠 돌려보니 예상치 못한 문제들이 연달아 나타났습니다.

  • 슬랙으로 날아오는 복구 명령어가 깨진 문자열이거나 엉뚱한 명령어였습니다.
  • 시스템에 별다른 문제가 없는데도 난데없이 sudo systemctl restart sshd 같은 뜬금없는 명령을 추천했습니다.
  • 슬랙에서 승인 버튼을 눌렀을 때 화면 반응이 없거나 조치가 제대로 되지 않는 일이 있었습니다.
  • 슬랙 인터랙티브 기능을 쓰려면 외부 공인 IP와 포트 개방(7788)이 필수여서, 홈랩이나 사설망 환경에서는 포트포워딩 설정이 여간 번거로운 게 아니었습니다.

단순히 프롬프트를 조금 고치는 수준으로는 해결이 안 되겠다 싶어, 로그 수집 파이프라인부터 LLM 응답 처리 방식, 그리고 알림 채널 구조까지 전체적으로 손을 보게 되었습니다. 그 과정에서 겪은 내용들을 정리해 보았습니다.


왜 이상한 명령어만 나왔을까?

서버 로그와 코드를 하나씩 확인해보니 크게 네 가지 문제가 얽혀 있었습니다.

1. 외부 스캐너 노이즈에 억지로 답을 쥐어짜낸 구조

공인 IP가 연결된 리눅스 서버에는 24시간 내내 22번 포트를 찔러보는 무차별 스캔 트래픽이 찍힙니다.

sshd[17693]: error: kex_exchange_identification: Connection closed by remote host
sshd[14179]: error: kex_exchange_identification: client sent invalid protocol identifier "GET / HTTP/1.1"

단순한 인터넷 노이즈일 뿐 시스템 장애는 아니었습니다. 그런데 기존 코드는 err 레벨 로그가 보이기만 하면 무조건 Bash 명령어를 하나 만들어 내도록 LLM에게 요청하고 있었습니다. 조치할 필요도 없는 상황에서 억지로 답을 내놓으라고 하니, Gemini가 멀쩡한 systemctl restart sshd 같은 위험한 명령어를 지어내고 있었던 것입니다.

2. 정규식 파서가 한글을 날려버린 문제

Gemini가 "이 로그는 외부 스캔이라 조치가 불필요합니다"라고 친절하게 한글로 답을 적어주면, 파이썬 코드의 re.sub(r'[^\x00-\x7F]+', '', first_line)이 한글을 전부 지워버렸습니다. 그 결과 알파벳 찌꺼기만 남은 sshd . 같은 깨진 문자열이 실행 명령어로 넘어가고 있었습니다.

3. 저널 파이프라인의 4KB 블록 버퍼링 지연

journalctl -f 출력을 파이썬 subprocess.PIPE로 넘기다 보니, 리눅스 표준 입출력이 터미널 모드가 아닌 블록 버퍼링(4KB)으로 동작했습니다. 에러가 발생해도 로그가 바로 넘어오지 않고 한참 뒤에 뭉텅이로 처리되는 지연이 생겼습니다.

4. 슬랙 3초 타임아웃과 응답 URL 처리 누락

슬랙 인터랙티브 버튼은 클릭 후 3초 안에 응답을 주지 않으면 타임아웃을 띄웁니다. 게다가 버튼이 달린 메시지 카드를 실행 결과로 교체하려면 요청에 담겨 온 response_url로 별도 POST 요청을 보내야 하는데, 이 부분이 빠져 있어 화면 반응이 멈춘 것처럼 보였습니다.


전면 수정한 내용들

flowchart TD
    A["Linux System Logs (journald)"] -->|"stdbuf -oL"| B["Log Ingestion & Pre-Filter"]
    B -->|"Bot Scans (kex_exchange 등)"| C["Drop (Quota 절약)"]
    B -->|"Actual Incident Log"| D["Gemini 2.5 Flash (Structured JSON)"]

    D --> E{"Action Required?"}
    E -->|"No"| F["Benign Log 처리 (무시)"]
    E -->|"Yes"| G["Security Inspection (Blacklist/Regex)"]

    G --> H{"Dual Channel Dispatch"}

    subgraph "Slack Channel (WebHook)"
        H --> I["Slack Block Kit 카드 발송"]
        I --> J["사용자 [승인] 클릭"]
        J --> K["POST /slack/interactive"]
        K --> L["비동기 Worker 실행 & 카드 교체"]
    end

    subgraph "Telegram Channel (Long Polling - No Port Open)"
        H --> M["Telegram sendMessage (HTML + Inline Keyboard)"]
        M --> N["사용자 [승인 및 실행] 클릭"]
        N --> O["getUpdates Callback Query 수신"]
        O --> P["비동기 Worker 실행 & editMessageText 교체"]
    end

    L --> Q["명령어 실행 및 결과 보고"]
    P --> Q

Pydantic JSON Structured Output 적용

자연어로 된 답변을 쪼개서 명령어를 뽑아내던 방식을 완전히 버렸습니다. Gemini API에 Pydantic 스키마(needs_action, severity, summary, analysis, command)를 전달해 정형화된 JSON으로만 응답을 받도록 고쳤습니다. 조치가 필요 없는 단순 노이즈면 needs_action=False로 처리해 불필요한 명령어 생성을 원천 차단했습니다.

노이즈 사전 차단과 60초 디바운스

kex_exchange_identification 같은 스캐너 패턴은 정규식으로 수집 단계에서 바로 걸러내도록 했습니다. 또 같은 에러가 짧은 시간에 반복해서 터질 때 API 할당량이 낭비되지 않도록, 60초 안에 발생한 동일 로그는 1회만 처리하도록 디바운스 로직을 추가했습니다.

stdbuf -oL로 라인 버퍼링 유지

journalctl 파이프라인 앞에 stdbuf -oL을 걸어 표준 출력을 라인 단위 버퍼링으로 고정했습니다. 이제 로그가 한 줄 찍히는 즉시 딜레이 없이 에이전트로 넘어옵니다.

슬랙 비동기 워커 구조로 변경

슬랙 요청이 들어오면 우선 HTTP 200을 바로 반환해 타임아웃을 피하고, 실제 명령어 실행과 카드 갱신은 백그라운드 스레드에서 response_url을 호출해 처리하도록 구조를 바꿨습니다.


텔레그램 봇을 추가한 배경

슬랙도 좋지만, 개인 홈랩이나 사설망 서버에서는 아쉬운 점이 있었습니다. 인터랙티브 버튼 이벤트를 받으려면 공유기 포트포워딩을 하거나 외부 접속용 터널을 따로 열어주어야 했습니다.

반면에 텔레그램 봇은 롱폴링(getUpdates) 방식을 지원합니다.

  • 서버가 텔레그램 서버로 먼저 요청을 보내 업데이트를 받아오는 방식이라 방화벽 인바운드 포트를 열 필요가 전혀 없었습니다.
  • 공유기 안쪽 사설망이든 클라우드 VM이든 아웃바운드 인터넷만 연결되면 바로 동작했습니다.
  • 인라인 키보드(inline_keyboard)를 활용하면 슬랙의 승인/거절 버튼과 똑같은 UX를 만들 수 있었고, 조치 후 editMessageText로 카드를 결과 보고서로 깔끔하게 교체할 수 있었습니다.

그래서 기존 슬랙 웹훅과 함께 텔레그램 롱폴링도 동시에 돌아가도록 듀얼 채널로 구성했습니다.


실제 동작 테스트

Redis 포트가 충돌하는 상황을 가상으로 만들어 테스트를 진행해 보았습니다.

1. 에러 감지와 승인 요청 카드

Telegram Alert Card

Redis 포트(6379) 충돌 로그가 발생하자마자 에러 분석 내용과 함께 점검 명령어(ss -tulpn | grep :6379)가 인라인 버튼과 함께 텔레그램으로 도착했습니다.

2. 승인 버튼 클릭과 결과 보고서 교체

Telegram Executed Report

버튼을 누르자 서버에서 명령어가 실행되고, 기존 메시지가 터미널 실행 결과가 담긴 결과 보고서로 교체되었습니다.

초기 테스트 시 겪었던 일 (sudo: lsof: command not found)
처음에는 복구 명령어로 sudo lsof -i :6379가 추천되었으나, 최소 설치된 우분투 서버 환경이라 lsof 패키지가 없어 command not found 에러가 발생했었습니다.
이에 서버에 패키지를 보완하는 것뿐만 아니라, 어떤 기본 리눅스 환경에서도 즉시 동작할 수 있도록 표준 기본 도구인 ss -tulpn | grep :<port>를 우선 사용하도록 프롬프트를 개선했습니다. 위 스크린샷은 개선된 프롬프트에 따라 ss 명령어가 안정적으로 자동 실행된 모습입니다.


Gemini Studio API 외에 고민해본 대안들

무료 티어인 Google AI Studio는 사용량이 몰리면 간헐적으로 503 UNAVAILABLE 에러가 나거나 하루 할당량에 걸리는 경우가 있었습니다. 그래서 몇 가지 대안도 같이 검토해 보았습니다.

  1. Vertex AI (GCP 기업 계정)
    코드 수정 없이 google-genai 클라이언트에서 vertexai=True 옵션만 켜고 GCP 서비스 계정을 연결해주면 되는 방식입니다. SLA가 보장되어 503 에러 걱정 없이 안정적으로 쓸 수 있었습니다.
  2. Mac mini 로컬 LLM (Ollama 연동)
    집에 있는 맥미니에 Ollama를 깔고 qwen2.5-coder:7b 같은 모델을 띄운 뒤, 우분투 서버에서 내부망 API(http://<mac-ip>:11434/api/generate)로 질의하는 방식도 테스트해 보았습니다. API 호출 비용이 전혀 들지 않고 서버 로그가 외부망으로 나가지 않아 보안 면에서도 장점이 있었습니다.

소스코드와 깃헙 저장소

전체 소스코드는 GitHub 저장소에 업데이트해 두었습니다.

git clone https://github.com/SeongGi/AI-SRE-System.git
cd AI-SRE-System
chmod +x ai-agent-system.sh
./ai-agent-system.sh

저장소의 ai-agent-system.sh 스크립트를 실행하면 가상환경 구성부터 systemd 서비스 등록, 프롬프트 파일 설정까지 한 번에 완료되도록 구성했습니다. 슬랙과 텔레그램 둘 다 연동해두었으니 환경에 맞춰 편하게 활용할 수 있게 되었습니다.