commit 45f96176a6e80dd8531f12a07a24ca19a5370f65 Author: DESKTOP-TKLFCPR\ython Date: Sun May 24 18:50:19 2026 +0900 Initial commit: GUARDiA project setup - CLAUDE.md: project context and architecture spec - docs/: system specs, DB schema, messenger integration, deployment engine - skills/: guardia-deploy, guardia-agent, guardia-messenger - .claude/settings.json: project-level permissions - .gitignore: Python/FastAPI project Co-Authored-By: Claude Sonnet 4.6 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..eb2b4edb --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(git *)", + "Bash(python *)", + "Bash(pip *)", + "Bash(uvicorn *)", + "Bash(pytest *)", + "Bash(alembic *)", + "Bash(psql *)", + "Bash(ssh *)", + "Bash(sftp *)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0b8f67c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg +*.egg-info/ +dist/ +build/ +.eggs/ + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Environment variables +.env +.env.* +!.env.example + +# Database +*.db +*.sqlite3 + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Logs +*.log +logs/ + +# Claude Code local settings +.claude/settings.local.json + +# Secrets +secrets/ +*.pem +*.key +*.crt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..22022dcc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,88 @@ +# GUARDiA — AI 기반 레거시 인프라 자율 운영 플랫폼 + +> **Claude Code용 프로젝트 마스터 컨텍스트 파일** +> 이 파일을 읽고 프로젝트의 전체 구조와 규칙을 파악한 뒤 작업을 시작하라. + +--- + +## 프로젝트 비전 + +1,000개 이상의 다중 관공서(Multi-tenant) 레거시 인프라를 타겟으로 하는 +**AI 기반 통합 ChatOps 오케스트레이션 플랫폼**. + +- 메신저 한 줄 명령 → sLLM 파싱 → 에이전트리스(SSH/SFTP) 배포·운영 자동화 +- 에이전트 설치 **불필요** — 표준 SSH/FTP 프로토콜만 활용 +- 개발(Dev), SM 운영, PM 관리 세 역할의 워크플로우를 단일 메신저로 통합 + +--- + +## 디렉터리 구조 + +``` +C:\GUARDiA\ +├── CLAUDE.md # ← 지금 이 파일 (Claude Code 진입점) +├── docs/ +│ ├── system_spec.md # 전체 시스템 아키텍처 명세 +│ ├── ai_orchestration.md # AI 에이전트 워크플로우 규격 +│ ├── db_schema.md # CMDB + SR + Audit 테이블 DDL +│ ├── messenger_integration.md # 메신저 웹훅 & sLLM 연동 명세 +│ ├── deployment_engine.md # 에이전트리스 배포 엔진 명세 +│ ├── security_governance.md # 보안·권한·감사 정책 +│ └── shell_scripts_guide.md # SM용 원격 제어 쉘 스크립트 가이드 +├── skills/ +│ ├── guardia-deploy/SKILL.md # 배포 엔진 구현 스킬 +│ ├── guardia-agent/SKILL.md # Python 역방향 에이전트 스킬 +│ └── guardia-messenger/SKILL.md # 메신저 연동 스킬 +└── src/ (생성 예정) + ├── api/ # FastAPI 백엔드 + ├── agent/ # Python 역방향 에이전트 + ├── llm/ # sLLM 파서 모듈 + ├── deploy/ # SSH/SFTP 배포 엔진 + └── db/ # DB 마이그레이션 & 모델 +``` + +--- + +## 기술 스택 (변경 금지) + +| 레이어 | 기술 | 비고 | +|--------|------|------| +| Backend API | Python 3.11+ / FastAPI | 비동기 WebSocket 처리 | +| LLM | 온프레미스 sLLM (Llama-3-8B / Solar-10.7B) | **외부 API 호출 절대 금지** | +| Infra 연결 | `paramiko` (SSH/SFTP) | 에이전트리스 | +| Database | PostgreSQL (CMDB + SR + Audit) | | +| 배포 자동화 | 쉘 스크립트 + Ansible (선택) | | +| 프론트 | React.js 또는 HTML5/Vanilla JS | | + +--- + +## 핵심 구현 원칙 + +1. **에이전트리스**: 대상 서버에 어떤 소프트웨어도 설치하지 않는다. +2. **결정론적 파싱**: sLLM은 JSON만 출력한다. 자연어 부연 설명 금지. +3. **Fail-Safe**: 모든 배포는 백업 → 배포 → 헬스체크 → 롤백 시퀀스를 따른다. +4. **감사 추적**: 모든 명령과 결과는 `TB_AUDIT_LOG`에 기록한다. +5. **최소 권한**: 관제 전용 일반 계정(opsagent) 사용. root SSH 직접 접속 금지. +6. **보안 우선**: 서버 자격증명은 암호화 DB에만 저장. 메신저 응답에 노출 금지. + +--- + +## 작업 시작 순서 (권장) + +``` +1단계: docs/db_schema.md 읽기 → PostgreSQL DDL 작성 +2단계: docs/messenger_integration.md 읽기 → FastAPI 웹훅 서버 구현 +3단계: docs/deployment_engine.md 읽기 → SSH/SFTP 배포 엔진 구현 +4단계: docs/ai_orchestration.md 읽기 → sLLM 파서 & 워크플로우 연동 +5단계: docs/security_governance.md 읽기 → 권한 검증 & 감사 모듈 구현 +``` + +--- + +## 스킬 파일 참조 방법 + +특정 기능 구현 전 해당 스킬 파일을 반드시 먼저 읽어라. + +- 배포 엔진 작업 시 → `skills/guardia-deploy/SKILL.md` +- 역방향 에이전트 작업 시 → `skills/guardia-agent/SKILL.md` +- 메신저 연동 작업 시 → `skills/guardia-messenger/SKILL.md` diff --git a/docs/ai_orchestration.md b/docs/ai_orchestration.md new file mode 100644 index 00000000..76a68499 --- /dev/null +++ b/docs/ai_orchestration.md @@ -0,0 +1,162 @@ +# [Specification] AI Agent Workflow & SR Task Execution + +본 문서는 SR 접수부터 AI 에이전트(Claude Code)의 실제 작업 수행까지의 연동 규격이다. + +--- + +## 1. SR Task Handling Logic + +``` +Reception → 메신저 SR을 파싱 서버가 TB_SR_REQUEST에 정형 데이터로 저장 + ↓ +Prioritization → PM 우선순위 부여 → 작업 관리 엔진이 큐에 적재 + ↓ +Execution Context → 작업 관리 엔진이 context.md를 프로젝트 루트에 생성 + ↓ +Claude Code → context.md 읽고 작업 범위 파악 → 수행 + ↓ +Result → result.json 저장 → DB 업데이트 → 메신저 완료 통보 +``` + +### 상태 전이 (State Machine) + +``` +RECEIVED → PARSED → PENDING_APPROVAL → APPROVED → IN_PROGRESS → COMPLETED + ↓ + FAILED_ROLLBACK +``` + +--- + +## 2. Claude Code Interaction Pattern + +### 2.1. Context 파일 생성 규격 (`context.md`) + +```markdown +## Task Context +- SR_NO: {sr_number} +- Institution: {institution_name} +- System: {system_name} +- Target Nodes: {node_list} +- Action: {action_type} # DEPLOY / LOG_ANALYSIS / RESTART / SSL_CHECK +- Artifacts: {file_paths} +- Constraints: {policy_rules} +``` + +### 2.2. Result 파일 규격 (`result.json`) + +```json +{ + "sr_no": "SR-2026-001", + "status": "SUCCESS", // SUCCESS | FAILED | PARTIAL + "changed_files": [], + "execution_log": "...", + "health_check": {"status": "200 OK"}, + "rollback_available": true, + "completed_at": "2026-05-23T19:00:00Z" +} +``` + +--- + +## 3. sLLM Deterministic Parser + +### 3.1. System Prompt (엄격 적용) + +``` +[SYSTEM PROMPT — 절대 변경 금지] +당신은 인프라 자동화 시스템의 핵심 파서입니다. +1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다. +2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다. +3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다. +4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다. +``` + +### 3.2. 출력 JSON 스키마 + +```json +{ + "intent_type": "DEPLOY_UPGRADE | LOG_ANALYSIS | SERVICE_RESTART | SSL_CHECK | DB_QUERY | CRON_MANAGE", + "institution": "기관명", + "system_name": "시스템명", + "infrastructure_layer": "WEB | WAS | DB | ESB", + "target_nodes": ["NODE_01", "NODE_02"], + "action_sequence": ["UPLOAD", "BACKUP", "REPLACE", "ROLLING_RESTART"], + "deploy_artifacts": ["class", "html", "js", "image"], + "requires_approval": false, + "priority": "HIGH | MEDIUM | LOW" +} +``` + +--- + +## 4. Deployment Integration + +### 4.1. 정적 vs 동적 자원 처리 + +| 자원 타입 | WAS 재기동 필요 | 처리 방식 | +|-----------|----------------|-----------| +| html, js, css, image | ❌ | SFTP 카피 → 즉시 반영 | +| .class, .jar, .war | ✅ | 백업 → 롤링 재기동 | +| config 파일 | ✅ (서비스에 따라) | 확인 후 결정 | + +### 4.2. 무중단 롤링 배포 시퀀스 + +``` +WAS #1: + 1. L4/Nginx에서 트래픽 차단 + 2. 현재 버전 백업 (mv app.jar app.jar.bak_YYYYMMDD) + 3. SFTP로 신규 파일 전송 + md5sum 무결성 검증 + 4. shutdown.sh 실행 → startup.sh 실행 + 5. curl -I http://localhost:8080 → 200 OK 확인 (timeout 30s) + 6. 실패 시 즉시 rollback.sh 실행 + +WAS #2: WAS #1 완료 확인 후 동일 시퀀스 반복 +``` + +### 4.3. PM 승인 HITL 흐름 + +```python +# 작업 완료 → PM에게 Result Package 전송 +def notify_pm_for_approval(sr_id, package): + summary = f"[{sr_id}] 작업 완료\n헬스체크: {package['health']['status']}" + messenger.send_approval_request( + target="PM_TEAM", + message=summary, + callback_url=f"/api/sr/approve/{sr_id}" + ) + +# PM 승인 시 고객 완료 통보 +@app.post("/api/sr/approve/{sr_id}") +def finalize_sr(sr_id: str): + db.update_sr_status(sr_id, "COMPLETED") + messenger.send_message(requester, f"SR-{sr_id} 배포 완료") +``` + +--- + +## 5. 작업 관리 엔진 핵심 코드 구조 + +```python +class TaskManager: + def process_task_completion(self, sr_id, work_dir): + result_package = self._collect_result_package(work_dir) + self._notify_pm_for_approval(sr_id, result_package) + self.db.update_sr_status(sr_id, "PENDING_PM_VALIDATION") + + def _collect_result_package(self, work_dir): + return { + "diff": open(f"{work_dir}/changes.diff").read(), + "logs": open(f"{work_dir}/execution.log").read(), + "health": json.load(open(f"{work_dir}/health_check.json")) + } +``` + +--- + +## 6. 로깅 및 감사 표준 + +- 모든 작업 이력: JSON Lines 포맷 → `audit_logs/` 디렉토리 +- 감사 로그 SHA-256 해시 체이닝: + `Log[n].hash = SHA256(Log[n].data + Log[n-1].hash)` +- 위변조 탐지: 매시간 해시 체인 연결성 자동 검증 diff --git a/docs/db_schema.md b/docs/db_schema.md new file mode 100644 index 00000000..165d3b45 --- /dev/null +++ b/docs/db_schema.md @@ -0,0 +1,197 @@ +# [Specification] GUARDiA 데이터베이스 스키마 설계서 + +> **DBMS:** PostgreSQL 15+ +> **원칙:** root 계정 금지. 전용 서비스 계정 사용. 비밀번호 컬럼 AES-256 암호화 필수. + +--- + +## 1. 인프라 자산 영역 (CMDB) + +### TB_INST_META (관공서 마스터) +```sql +CREATE TABLE TB_INST_META ( + inst_id VARCHAR(20) PRIMARY KEY, -- 기관 코드 (예: MOF, MOIS) + inst_name VARCHAR(100) NOT NULL, -- 기관명 (예: 기획재정부) + pm_contact VARCHAR(50), -- 담당 PM 사번 + phone VARCHAR(20), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### TB_SERVER_INFO (서버 노드 마스터) +```sql +CREATE TABLE TB_SERVER_INFO ( + server_id VARCHAR(30) PRIMARY KEY, -- 서버 고유 ID (예: MOF-WAS-01) + inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id), + system_name VARCHAR(100) NOT NULL, -- 업무 시스템명 + layer VARCHAR(10) NOT NULL, -- WEB | WAS | DB | ESB + hostname VARCHAR(100), + ip_address VARCHAR(45) NOT NULL, + ssh_port INT DEFAULT 22, + ftp_path VARCHAR(255), -- 배포 기본 경로 + os_user VARCHAR(50), -- SSH 접속 계정 + os_pw_enc TEXT, -- AES-256 암호화된 비밀번호 + os_type VARCHAR(20) DEFAULT 'LINUX', -- LINUX | WINDOWS + status VARCHAR(20) DEFAULT 'ACTIVE', + last_check TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_server_inst ON TB_SERVER_INFO(inst_id, layer); +``` + +### TB_SYSTEM_OWNER (시스템 담당자 매핑) +```sql +CREATE TABLE TB_SYSTEM_OWNER ( + system_id VARCHAR(30) PRIMARY KEY, + inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id), + owner_id VARCHAR(50) NOT NULL, -- 1순위 승인권자 사번 + deputy_id VARCHAR(50), -- 2순위 대리 승인자 + supervisor_id VARCHAR(50), -- 3순위 상위 관리자 + sla_minutes INT DEFAULT 10, -- 승인 대기 SLA (분) + is_active CHAR(1) DEFAULT 'Y' -- Y: 근무중 / N: 부재중 +); +``` + +--- + +## 2. 운영 작업 영역 (Ops Task) + +### TB_OPS_TASK (운영 작업 헤더) +```sql +CREATE TABLE TB_OPS_TASK ( + task_id VARCHAR(30) PRIMARY KEY, -- 예: OPS-20260523-001 + task_category VARCHAR(50) NOT NULL, -- DEPLOY | INFRA | DATA | MAINTENANCE + task_sub_type VARCHAR(50), -- SSL_REPLACE | LOG_ANALYSIS | RESTART + raw_request TEXT NOT NULL, -- 사용자 원문 자연어 + status VARCHAR(30) DEFAULT 'RECEIVED', + requester_id VARCHAR(50) NOT NULL, + inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id), + playbook_id VARCHAR(50), -- 실행 플레이북 ID + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +-- 상태값: RECEIVED | PARSED | PENDING_APPROVAL | ESCALATED | APPROVED | IN_PROGRESS | PENDING_PM_VALIDATION | COMPLETED | FAILED_ROLLBACK +CREATE INDEX idx_task_status ON TB_OPS_TASK(status, created_at DESC); +``` + +### TB_DEPLOY_HISTORY (배포 상세 이력) +```sql +CREATE TABLE TB_DEPLOY_HISTORY ( + deploy_id BIGSERIAL PRIMARY KEY, + task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id), + server_id VARCHAR(30) REFERENCES TB_SERVER_INFO(server_id), + user_id VARCHAR(50) NOT NULL, + sr_no VARCHAR(30), + artifact_list JSONB, -- 배포된 파일 목록 + deploy_status CHAR(1) DEFAULT 'P', -- P:대기 S:성공 F:실패 + backup_path VARCHAR(255), + result_hash CHAR(64), -- SHA-256 + deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 3. 거버넌스 승인 영역 + +### TB_APPROVAL_FLOW (다단계 승인 이력) +```sql +CREATE TABLE TB_APPROVAL_FLOW ( + flow_id BIGSERIAL PRIMARY KEY, + task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id), + step_order INT NOT NULL, -- 1, 2, 3... + approver_id VARCHAR(50) NOT NULL, + approval_status CHAR(1) DEFAULT 'P', -- P:대기 A:승인 R:반려 + comments VARCHAR(500), + escalation_type VARCHAR(20), -- DIRECT | DEPUTY | SUPERVISOR + approved_at TIMESTAMP +); +CREATE INDEX idx_approval_task ON TB_APPROVAL_FLOW(task_id, step_order); +``` + +--- + +## 4. 감사 영역 (Audit — 위변조 방지) + +### TB_AUDIT_LOG (SHA-256 해시 체인) +```sql +CREATE TABLE TB_AUDIT_LOG ( + log_id BIGSERIAL PRIMARY KEY, + task_id VARCHAR(30) REFERENCES TB_OPS_TASK(task_id), + server_ip VARCHAR(45), + exec_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + command TEXT NOT NULL, + exit_code INT, + result_hash CHAR(64), -- SHA256(현재 결과 + 이전 hash) + operator_id VARCHAR(50) +); +-- 해시 체이닝: Log[n].result_hash = SHA256(Log[n].output || Log[n-1].result_hash) +``` + +### TB_SR_REQUEST (SR 티켓 — 레거시 호환) +```sql +CREATE TABLE TB_SR_REQUEST ( + sr_no VARCHAR(30) PRIMARY KEY, -- SR-2026-001 + requester VARCHAR(100), + system_id VARCHAR(30) REFERENCES TB_SERVER_INFO(server_id), + raw_text TEXT, + task_type VARCHAR(50), -- UI_FIX | DEPLOY | LOG_ANALYSIS + status VARCHAR(20) DEFAULT 'PENDING', + assigned_ai VARCHAR(50), -- 에이전트 실행 ID + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 5. 메신저 채팅 영역 + +### TB_MSG_ROOMS (대화방) +```sql +CREATE TABLE TB_MSG_ROOMS ( + room_id VARCHAR(50) PRIMARY KEY, + room_name VARCHAR(100) NOT NULL, + inst_id VARCHAR(20) REFERENCES TB_INST_META(inst_id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### TB_MSG_CHAT_LOG (채팅 이력) +```sql +CREATE TABLE TB_MSG_CHAT_LOG ( + chat_id BIGSERIAL PRIMARY KEY, + room_id VARCHAR(50) REFERENCES TB_MSG_ROOMS(room_id), + sender_id VARCHAR(50) NOT NULL, + sender_name VARCHAR(100), + sender_type VARCHAR(20) NOT NULL, -- HUMAN | BOT | AGENT + message_text TEXT, + is_command BOOLEAN DEFAULT FALSE, + task_id VARCHAR(30), + itsm_ticket_id VARCHAR(50), -- 향후 ITSM 연동용 (null 허용) + asset_code VARCHAR(50), -- 인프라 자산 코드 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 6. 더미 데이터 (로컬 테스트용) + +```sql +-- 관공서 기관 +INSERT INTO TB_INST_META (inst_id, inst_name, pm_contact) VALUES +('MOF', '기획재정부', 'PM001'), +('MOIS', '행정안전부', 'PM002'), +('MSS', '중소벤처기업부', 'PM003'); + +-- 서버 노드 +INSERT INTO TB_SERVER_INFO (server_id, inst_id, system_name, layer, ip_address, ssh_port, ftp_path, os_user) VALUES +('MOF-WAS-01', 'MOF', '예산시스템', 'WAS', '192.168.10.11', 22, '/app/mof/webapps/ROOT', 'mofadmin'), +('MOF-WAS-02', 'MOF', '예산시스템', 'WAS', '192.168.10.12', 22, '/app/mof/webapps/ROOT', 'mofadmin'), +('MOF-DB-01', 'MOF', '예산시스템', 'DB', '192.168.10.13', 5432, '/var/lib/postgresql', 'postgres'); + +-- 담당자 매핑 +INSERT INTO TB_SYSTEM_OWNER (system_id, inst_id, owner_id, deputy_id, supervisor_id) VALUES +('MOF-WAS-01', 'MOF', 'EMP-0101', 'EMP-0102', 'EMP-0100'), +('MOF-DB-01', 'MOF', 'EMP-0201', NULL, 'EMP-0200'); +``` diff --git a/docs/deployment_engine.md b/docs/deployment_engine.md new file mode 100644 index 00000000..1c6eb28b --- /dev/null +++ b/docs/deployment_engine.md @@ -0,0 +1,215 @@ +# [Specification] 에이전트리스 배포 엔진 (Agentless Deployment Engine) + +--- + +## 1. 개요 + +대상 서버에 어떤 소프트웨어도 설치하지 않고, 표준 **SSH/SFTP 프로토콜**만으로 +파일 배포 · 서비스 재기동 · 헬스체크 · 자동 롤백을 수행하는 핵심 실행 엔진. + +--- + +## 2. SSH Executor 모듈 (`src/deploy/ssh_executor.py`) + +```python +import paramiko +import hashlib + +class SSHExecutor: + def __init__(self, host: str, user: str, password: str, port: int = 22): + self.client = paramiko.SSHClient() + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.client.connect(host, port=port, username=user, password=password, timeout=10) + + def execute_command(self, command: str, timeout: int = 300) -> dict: + """원격 명령 실행 — exit_code 반드시 확인""" + stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout) + exit_code = stdout.channel.recv_exit_status() + return { + "exit_code": exit_code, + "stdout": stdout.read().decode("utf-8", errors="replace"), + "stderr": stderr.read().decode("utf-8", errors="replace") + } + + def upload_file(self, local_path: str, remote_path: str) -> str: + """SFTP 파일 업로드 + MD5 무결성 검증""" + sftp = self.client.open_sftp() + sftp.put(local_path, remote_path) + sftp.close() + + # 원격지 MD5 검증 + result = self.execute_command(f"md5sum {remote_path}") + return result["stdout"].split()[0] # 원격 MD5 + + def close(self): + self.client.close() +``` + +--- + +## 3. 롤링 배포 엔진 (`src/deploy/rolling_deployer.py`) + +```python +import time +import requests +from .ssh_executor import SSHExecutor +from ..db.audit import log_execution + +class RollingDeployer: + HEALTH_TIMEOUT = 30 # 헬스체크 최대 대기 (초) + + def deploy(self, nodes: list, artifact_path: str, sr_id: str) -> bool: + """무중단 롤링 배포 — 노드 순차 처리""" + for node in nodes: + success = self._deploy_single_node(node, artifact_path, sr_id) + if not success: + return False + return True + + def _deploy_single_node(self, node: dict, artifact_path: str, sr_id: str) -> bool: + executor = SSHExecutor(node["ip"], node["user"], node["password"], node["ssh_port"]) + try: + # 1. Pre-check: 디스크 용량 확인 + result = executor.execute_command("df -h /app | awk 'NR==2{print $5}' | sed 's/%//'") + if int(result["stdout"].strip()) > 90: + raise Exception("디스크 사용량 90% 초과 — 배포 중단") + + # 2. 백업 (타임스탬프 포함) + ts = time.strftime("%Y%m%d_%H%M%S") + backup_cmd = f"cp -rp {node['deploy_path']} {node['deploy_path']}.bak_{ts}" + executor.execute_command(backup_cmd) + + # 3. SFTP 전송 + 무결성 검증 + local_md5 = self._local_md5(artifact_path) + remote_path = f"{node['deploy_path']}/{artifact_path.split('/')[-1]}" + remote_md5 = executor.upload_file(artifact_path, remote_path) + if local_md5 != remote_md5: + raise Exception(f"MD5 불일치: {local_md5} vs {remote_md5}") + + # 4. WAS 재기동 (동적 자원인 경우) + if node.get("requires_restart", True): + executor.execute_command("sh /app/scripts/shutdown.sh") + executor.execute_command(f"cp {remote_path} {node['deploy_path']}/") + result = executor.execute_command("sh /app/scripts/startup.sh") + if result["exit_code"] != 0: + raise Exception(f"startup.sh 실패: {result['stderr']}") + + # 5. 헬스체크 + if not self._health_check(node["ip"], node.get("health_port", 8080)): + raise Exception("헬스체크 실패 — 롤백 시작") + + log_execution(sr_id, node["ip"], "DEPLOY_SUCCESS", 0, "") + return True + + except Exception as e: + self._rollback(executor, node, ts) + log_execution(sr_id, node["ip"], "DEPLOY_FAILED", 1, str(e)) + return False + finally: + executor.close() + + def _health_check(self, ip: str, port: int) -> bool: + """서비스 포트 응답 확인 — 최대 30초 대기""" + deadline = time.time() + self.HEALTH_TIMEOUT + while time.time() < deadline: + try: + r = requests.get(f"http://{ip}:{port}", timeout=3) + if r.status_code == 200: + return True + except Exception: + pass + time.sleep(2) + return False + + def _rollback(self, executor: SSHExecutor, node: dict, ts: str): + """백업본으로 자동 롤백""" + try: + executor.execute_command("sh /app/scripts/shutdown.sh") + executor.execute_command(f"cp -rp {node['deploy_path']}.bak_{ts} {node['deploy_path']}") + executor.execute_command("sh /app/scripts/startup.sh") + except Exception as e: + print(f"[ROLLBACK ERROR] {e}") + + @staticmethod + def _local_md5(path: str) -> str: + with open(path, "rb") as f: + return hashlib.md5(f.read()).hexdigest() +``` + +--- + +## 4. Command Sanitizer (보안 필터) + +```python +BLACKLIST_PATTERNS = [ + r"rm\s+-[rRf].*\/", # rm -rf / + r"\bmkfs\b", + r"\bformat\b", + r"\bdrop\s+table\b", + r"\btruncate\b", + r">\s*/dev/sda", +] + +def sanitize_command(command: str) -> None: + """위험 명령어 사전 차단 — SecurityViolationError 발생""" + import re + for pattern in BLACKLIST_PATTERNS: + if re.search(pattern, command, re.IGNORECASE): + raise SecurityViolationError(f"금지 명령어 패턴 탐지: {pattern}") +``` + +--- + +## 5. 티어드 롤링 배포 (1,000+ 사이트 대응) + +```python +def tiered_rolling_deployment(sites: list, playbook: dict) -> bool: + """ + Tier 1 (미션 크리티컬): 순차 배포, 엄격한 헬스체크 + Tier 2 (표준 사이트): 50개 단위 병렬 + Tier 3 (테스트/개발): 전량 병렬 + """ + tiers = _categorize_by_tier(sites) + + for tier_name, tier_sites in tiers.items(): + batch_size = 1 if tier_name == "TIER1" else 50 + results = [] + for i in range(0, len(tier_sites), batch_size): + batch = tier_sites[i:i+batch_size] + # 병렬 처리 (asyncio 또는 ThreadPoolExecutor) + batch_results = _deploy_batch(batch, playbook) + results.extend(batch_results) + + # Safety Gate: 실패율 > 10% 이면 중단 + fail_rate = sum(1 for r in results if not r) / len(results) + if fail_rate > 0.1: + print(f"[ABORT] {tier_name} 실패율 {fail_rate:.0%} > 10% — 전체 배포 중단") + return False + + return True +``` + +--- + +## 6. 배포 파이프라인 API 엔드포인트 + +```python +@app.post("/api/deploy/trigger") +async def trigger_deployment(sr_id: str, approver_id: str): + """승인 완료 후 배포 엔진 가동""" + task = db.get_task(sr_id) + nodes = db.get_server_nodes(task["inst_id"], task["system_name"]) + artifacts = db.get_staged_artifacts(sr_id) + + deployer = RollingDeployer() + success = deployer.deploy(nodes, artifacts[0], sr_id) + + if success: + db.update_task_status(sr_id, "PENDING_PM_VALIDATION") + notify_pm_for_hitl_review(sr_id) + else: + db.update_task_status(sr_id, "FAILED_ROLLBACK") + notify_failure(sr_id) + + return {"sr_id": sr_id, "success": success} +``` diff --git a/docs/messenger_integration.md b/docs/messenger_integration.md new file mode 100644 index 00000000..a20d7183 --- /dev/null +++ b/docs/messenger_integration.md @@ -0,0 +1,254 @@ +# [Specification] 메신저 연동 & sLLM 통합 명세 + +--- + +## 1. 아키텍처 개요 + +``` +[메신저 (슬랙/잔디/자체앱)] + │ Webhook (HTTPS POST) + ▼ +[FastAPI Webhook Server :80] + │ Nginx → proxy_pass → :3000 (Node.js/NestJS) 또는 :8000 (FastAPI) + ▼ +[sLLM Parser :11434 (Ollama) or :8000 (vLLM)] + │ JSON 정형 데이터 + ▼ +[CMDB 조회 → 권한 검증 → 승인 라우팅] + │ + ▼ +[SSH/SFTP 배포 엔진] +``` + +--- + +## 2. sLLM 서버 구성 + +### 2.1. 모델 선택 +``` +권장 1순위: Solar-10.7B-Instruct (한국어 특화) +권장 2순위: Llama-3-8B-Instruct (경량) +실행 엔진: Ollama (빠른 구축) 또는 vLLM (고성능) +양자화: 4-bit AWQ 또는 GGUF (GPU VRAM 절감) +``` + +### 2.2. Ollama 기반 로컬 실행 +```bash +# 설치 및 모델 다운로드 +ollama pull llama3:8b-instruct-q4_K_M + +# API 호출 (OpenAI 호환) +curl http://localhost:11434/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "llama3:8b-instruct-q4_K_M", "messages": [...]}' +``` + +### 2.3. Python 클라이언트 모듈 (`src/llm/parser.py`) +```python +import openai + +client = openai.OpenAI( + base_url="http://localhost:11434/v1", # 온프레미스 + api_key="local" # 더미 키 +) + +SYSTEM_PROMPT = """ +당신은 인프라 자동화 시스템의 핵심 파서입니다. +1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다. +2. 부연 설명, 인삿말은 절대 포함하지 않습니다. +3. CMDB에 없는 기관/서버는 "UNKNOWN"으로 처리합니다. +4. 배포 명령 시 requires_approval(boolean)을 반드시 판단합니다. + +출력 포맷: +{"intent_type": "...", "institution": "...", "system_name": "...", + "infrastructure_layer": "WAS|WEB|DB", "target_nodes": [], + "action_sequence": [], "deploy_artifacts": [], "requires_approval": false} +""" + +def parse_natural_language(user_text: str) -> dict: + response = client.chat.completions.create( + model="llama3:8b-instruct-q4_K_M", + response_format={"type": "json_object"}, + temperature=0.1, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_text} + ] + ) + return json.loads(response.choices[0].message.content) +``` + +--- + +## 3. FastAPI Webhook 서버 + +### 3.1. 메신저 수신 엔드포인트 (`src/api/webhook.py`) +```python +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from typing import Dict, List +import json + +app = FastAPI() + +# 연결된 세션 관리 +room_channels: Dict[str, List[WebSocket]] = {} +connected_agents: Dict[str, WebSocket] = {} + +@app.post("/messenger/webhook") +async def receive_message(payload: dict): + """메신저 Webhook 수신 — 메시지 파싱 & SR 생성""" + user_id = payload.get("user_id") + text = payload.get("text", "") + files = payload.get("files", []) + room_id = payload.get("room_id") + + # 파일 스테이징 + staged_files = await stage_files(files) + + # sLLM 파싱 + parsed = parse_natural_language(text) + + # CMDB 조회 → 서버 정보 매핑 + server_list = db.query_servers(parsed["institution"], parsed["system_name"]) + if not server_list: + return {"status": "ERROR", "message": "CMDB에 해당 서버 정보 없음"} + + # SR 생성 + sr_id = create_ops_task(parsed, user_id, staged_files) + + # 승인 라우팅 + if parsed.get("requires_approval"): + initiate_approval_process(sr_id) + return {"status": "PENDING_APPROVAL", "sr_id": sr_id} + + # 즉시 실행 + trigger_execution_engine(sr_id) + return {"status": "IN_PROGRESS", "sr_id": sr_id} +``` + +### 3.2. WebSocket 실시간 중계 (`src/api/ws_relay.py`) +```python +@app.websocket("/ws/chat/{room_id}/{client_id}/{client_type}") +async def chat_endpoint(ws: WebSocket, room_id: str, client_id: str, client_type: str): + await ws.accept() + + if client_type == "HUMAN": + room_channels.setdefault(room_id, []).append(ws) + elif client_type == "AGENT": + connected_agents[client_id] = ws + + try: + while True: + data = json.loads(await ws.receive_text()) + + if client_type == "HUMAN" and data.get("is_command"): + # AI 봇 멘션 → 파싱 → 역방향 에이전트로 라우팅 + target_agent = data.get("target_agent_id", "pc-01") + if target_agent in connected_agents: + await connected_agents[target_agent].send_text( + json.dumps({"task_id": str(uuid4()), "action": data["command_code"]}) + ) + elif client_type == "AGENT" and data.get("event") == "TASK_FINISHED": + # 에이전트 결과 → 방 전체에 브로드캐스트 + for ws in room_channels.get(room_id, []): + await ws.send_text(json.dumps({"sender": "BOT", "result": data["payload"]})) + except WebSocketDisconnect: + if client_type == "HUMAN": + room_channels.get(room_id, []).remove(ws) + else: + connected_agents.pop(client_id, None) +``` + +--- + +## 4. Nginx 웹소켓 프록시 설정 + +```nginx +upstream was_backend { + server 127.0.0.1:8000; + keepalive 32; +} + +server { + listen 80; + server_name localhost; + + location /ws { + proxy_pass http://was_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_buffering off; + client_max_body_size 100M; # 배포 파일 업로드 + } + + location / { + proxy_pass http://was_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + } +} +``` + +--- + +## 5. Proactive AI 챗봇 (대화 맥락 분석) + +```python +CONTEXT_RULES = { + "db_delay": { + "keywords": ["DB", "디비", "쿼리", "느리", "타임아웃", "락"], + "action": "FETCH_DB_LOCKS", + "message": "DB 지연 징후가 감지되었습니다. 대기 쿼리를 조회할까요?" + }, + "disk_full": { + "keywords": ["용량", "디스크", "로그", "쌓였"], + "action": "CHECK_DISK_SPACE", + "message": "디스크 공간 관련 문제가 감지되었습니다. 용량을 확인할까요?" + } +} + +async def analyze_context(room_id: str, sender: str, text: str): + """대화 맥락 분석 → 선제적 조언 Push""" + if any(kw in text for kw in ["@bot", "AI", "조회", "확인"]): + return # 이미 명시적 명령 + + for rule_name, rule in CONTEXT_RULES.items(): + if any(kw in text for kw in rule["keywords"]): + await asyncio.sleep(1) + await broadcast_to_room(room_id, { + "sender": "GUARDiA-Bot", + "sender_type": "BOT", + "message_content": f"🤖 {rule['message']}", + "interactive_action": { + "type": "BUTTON", + "label": "즉시 확인", + "command_code": rule["action"] + } + }) + break # 첫 매칭만 +``` + +--- + +## 6. 메시지 프로토콜 스키마 (ITSM 확장 호환) + +```json +{ + "message_id": "MSG-20260523-0001", + "timestamp": "2026-05-23T19:00:00Z", + "sender": "ENGINEER_01", + "sender_type": "HUMAN", + "msg_type": "CHAT", + "content": "기재부 예산시스템 WAS 재기동해줘", + "is_widget": false, + "itsm_metadata": { + "itsm_ticket_id": null, + "asset_code": "MOF-WAS-01", + "severity": "MEDIUM" + } +} +``` diff --git a/docs/security_governance.md b/docs/security_governance.md new file mode 100644 index 00000000..647cc615 --- /dev/null +++ b/docs/security_governance.md @@ -0,0 +1,183 @@ +# [Specification] 보안 & 거버넌스 정책 + +--- + +## 1. RBAC 권한 체계 + +### 1.1. 역할 정의 + +| 역할 | 권한 | 담당 | +|------|------|------| +| Developer | 소관 사이트 배포 요청 | 파일 전송 + 재기동 | +| SM 운영자 | 로그 조회, 자원 정비, 크론 관리 | 운영 유지보수 | +| 팀장 (Owner) | 1차 배포 승인 | 소관 시스템 | +| 대리 (Deputy) | 팀장 부재 시 대리 승인 | | +| PM | 최종 HITL 검증, 거버넌스 규칙 수정 | 전체 통제 | +| 보안책임자 | 인프라 변경/보안 작업 3차 승인 | | + +### 1.2. 다단계 승인 워크플로우 + +```python +APPROVAL_POLICY = { + "MAINTENANCE": {"steps": 1, "approvers": ["OWNER"]}, + "DEPLOY_NORMAL": {"steps": 2, "approvers": ["OWNER", "PM"]}, + "DEPLOY_SECURE": {"steps": 3, "approvers": ["OWNER", "PM", "SECURITY"]}, +} + +def get_approval_policy(task_category: str) -> dict: + if task_category in ["LOG_ANALYSIS", "CRON_MANAGE"]: + return APPROVAL_POLICY["MAINTENANCE"] + elif task_category in ["DEPLOY", "DATA_CLEAN"]: + return APPROVAL_POLICY["DEPLOY_NORMAL"] + else: # SSL, INFRA_CHANGE + return APPROVAL_POLICY["DEPLOY_SECURE"] +``` + +--- + +## 2. 계층적 승인 엔진 (에스컬레이션) + +```python +def get_approver_chain(system_id: str) -> str: + """SLA 초과 시 자동 에스컬레이션""" + info = db.execute( + "SELECT owner_id, deputy_id, supervisor_id, sla_minutes, is_active " + "FROM TB_SYSTEM_OWNER WHERE system_id = %s", (system_id,) + ).fetchone() + + if info["is_active"] == "Y": + return info["owner_id"] # 1순위: 담당 팀장 + elif info["deputy_id"]: + return info["deputy_id"] # 2순위: 대리 승인자 + else: + return info["supervisor_id"] # 3순위: 상위 관리자 + +def check_escalation(task_id: str): + """SLA 초과 여부 체크 → 자동 이관""" + task = db.get_task(task_id) + owner_info = db.get_system_owner(task["system_id"]) + + elapsed = (datetime.now() - task["created_at"]).seconds / 60 + if elapsed > owner_info["sla_minutes"] and task["status"] == "PENDING_APPROVAL": + next_approver = get_approver_chain(task["system_id"]) + db.update_task_approver(task_id, next_approver, "ESCALATED") + messenger.send_urgent_notification(next_approver, f"[긴급] {task_id} 승인 대기") + audit.log_action(task_id, f"Escalated to {next_approver}") +``` + +--- + +## 3. 자격증명 보안 + +### 3.1. 암호화 저장 정책 +``` +- SSH/FTP 비밀번호: AES-256 암호화 → TB_SERVER_INFO.os_pw_enc +- 실행 시점에만 메모리 로드, 로그 출력 금지 +- 메신저 응답창 노출 절대 금지 +- 주기적 비밀번호 갱신 시 DB 암호화 업데이트 +``` + +### 3.2. SSH 접속 보안 +```bash +# opsagent 계정 sudoers 설정 (대상 서버에 1회 설정) +# /etc/sudoers.d/opsagent +opsagent ALL=(ALL) NOPASSWD: /usr/bin/kill, /usr/bin/systemctl restart *, /app/scripts/shutdown.sh, /app/scripts/startup.sh +``` + +--- + +## 4. 감사 로그 해시 체이닝 + +```python +import hashlib, json + +def compute_audit_hash(log_data: dict, prev_hash: str) -> str: + """SHA-256 해시 체인 생성""" + content = json.dumps(log_data, ensure_ascii=False, sort_keys=True) + combined = f"{content}{prev_hash}" + return hashlib.sha256(combined.encode()).hexdigest() + +def log_execution(task_id: str, server_ip: str, command: str, exit_code: int, result: str): + """감사 로그 저장 — 이전 로그의 해시를 체인에 포함""" + prev = db.get_last_audit_log() + prev_hash = prev["result_hash"] if prev else "GENESIS" + + log_data = { + "task_id": task_id, + "server_ip": server_ip, + "command": command, + "exit_code": exit_code, + "exec_time": datetime.utcnow().isoformat() + } + new_hash = compute_audit_hash(log_data, prev_hash) + + db.insert_audit_log({**log_data, "result": result, "result_hash": new_hash}) + +def verify_audit_integrity() -> bool: + """해시 체인 무결성 검증 — 위변조 탐지""" + logs = db.get_all_audit_logs_ordered() + prev_hash = "GENESIS" + for log in logs: + expected_hash = compute_audit_hash( + {k: v for k, v in log.items() if k != "result_hash"}, prev_hash + ) + if expected_hash != log["result_hash"]: + alert_security_breach(log["log_id"]) + return False + prev_hash = log["result_hash"] + return True +``` + +--- + +## 5. Kill-Switch (비상 정지) + +```python +@app.post("/api/emergency/kill") +async def emergency_kill(operator_id: str, reason: str): + """메신저 '정지' 명령 → 전체 배포 프로세스 즉시 중단""" + # 진행 중인 모든 IN_PROGRESS 태스크 중단 + active_tasks = db.get_tasks_by_status("IN_PROGRESS") + for task in active_tasks: + task_manager.abort(task["task_id"]) + trigger_rollback(task["task_id"]) + + # 감사 로그 + log_execution("EMERGENCY", "ALL", f"KILL_SWITCH by {operator_id}", 0, reason) + + # PM에게 알림 + messenger.send_to_all_pm(f"🚨 비상 정지 실행: {operator_id} — {reason}") + + return {"status": "KILLED", "aborted_tasks": len(active_tasks)} +``` + +--- + +## 6. 보안 감사 체크리스트 (GS 인증 대응) + +| 항목 | 검증 방법 | 주기 | +|------|-----------|------| +| 로그 무결성 | 해시 체인 전체 역추적 | 매일 00:00 | +| 계정 잠금 탐지 | faillock 명령어 원격 스캔 | 1시간마다 | +| 설정 파일 변조 | SHA-256 Checksum 비교 | 1시간마다 | +| 인증서 만료 | openssl enddate 파싱 | 매일 04:00 | +| 디스크 임계치 | df -h 자동 스캔 | 5분마다 | +| 비인가 포트 | ss -tlnp 스캔 | 1시간마다 | + +--- + +## 7. 데이터 무결성 보호 (배포 중 롤백) + +```python +class IntegrityShield: + def protect_rollback(self, task_id: str, executor: SSHExecutor): + """롤백 수행 시에도 감사 기록 보존""" + # 롤백 직전 현재 해시 상태 기록 + log_execution(task_id, executor.host, "ROLLBACK_START", 0, "롤백 시작") + # 실제 롤백 + result = executor.execute_command("sh /app/scripts/rollback.sh") + if result["exit_code"] != 0: + # 롤백 실패 → 수동 점검 필요 상태로 변경 (다음 배포 차단) + db.update_task_status(task_id, "MANUAL_INTERVENTION_REQUIRED") + messenger.send_critical_alert(f"🔴 {task_id} 롤백 실패 — 수동 점검 필요") +``` diff --git a/docs/shell_scripts_guide.md b/docs/shell_scripts_guide.md new file mode 100644 index 00000000..69018037 --- /dev/null +++ b/docs/shell_scripts_guide.md @@ -0,0 +1,185 @@ +# [Guide] SM 운영용 원격 제어 쉘 스크립트 + +> 중계 PC에서 내부망 WEB/WAS/DB 서버를 원격 제어하는 표준 스크립트 모음. +> 모든 스크립트는 **JSON 출력** → AI 에이전트가 파싱하여 메신저로 전달. + +--- + +## 스크립트 목록 + +| 번호 | 파일명 | 역할 | +|------|--------|------| +| 1 | `check_infra_health.sh` | WEB/WAS/DB 포트·Ping 일괄 헬스체크 | +| 2 | `manage_service.sh` | 서비스 데몬 상태/기동/중지/재기동 | +| 3 | `monitor_resources.sh` | CPU/메모리/디스크 임계치 감시 | +| 4 | `collect_target_logs.sh` | 에러 로그 원격 추출 (grep + tail) | +| 5 | `check_db_status.sh` | DB 커넥션·Lock 세션 진단/해제 | +| 6 | `backup_and_clear.sh` | 로그 로테이션 및 오래된 파일 정비 | +| 7 | `security_audit.sh` | 계정 잠금·포트 스캔·파일 위변조 감사 | +| 8 | `ssl_expiry_detector.sh` | SSL 인증서 만료일 사전 감지 | +| 9 | `self_healing_defense.sh` | 디스크 임계치 초과 시 자동 정비 트리거 | +| 10 | `config_integrity_check.sh` | 설정 파일 SHA-256 무결성 검증 | +| 11 | `cron_health_checker.sh` | 크론탭 배치 가동성 진단 | +| 12 | `esb_channel_monitor.sh` | ESB 연계 채널 상태 및 큐 임계치 감시 | +| 13 | `audit_session_logger.sh` | 원격 명령 실행 이력 감사 기록 | + +--- + +## 공통 규칙 + +```bash +# 모든 스크립트 공통 패턴 +#!/bin/bash +declare -A SERVER_HOSTS=([WEB]="192.168.10.11" [WAS]="192.168.10.12" [DB]="192.168.10.13") +declare -A SERVER_USERS=([WEB]="webadmin" [WAS]="wasadmin" [DB]="postgres") + +# SSH 실행 공통 함수 +execute_remote_cmd() { + local host=$1; local user=$2; local cmd=$3 + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 "${user}@${host}" "${cmd}" 2>/dev/null +} + +# 출력: JSON (AI 파싱용) +# 로그: /app/agent/logs/YYYYMMDD.log +``` + +--- + +## 1. check_infra_health.sh + +```bash +#!/bin/bash +# 사용법: ./check_infra_health.sh +# 출력: {"timestamp":"...","results":[{"server":"WEB","ping":"UP","port_status":"OPEN"}, ...]} + +check_node() { + local name=$1 ip=$2 port=$3 + ping -c 1 -W 1 "${ip}" > /dev/null 2>&1 && ping_s="UP" || ping_s="DOWN" + timeout 2 bash -c "cat < /dev/null > /dev/tcp/${ip}/${port}" 2>/dev/null && port_s="OPEN" || port_s="CLOSED" + echo "{\"server\":\"${name}\",\"ip\":\"${ip}\",\"ping\":\"${ping_s}\",\"port_status\":\"${port_s}\"}" +} + +WEB_J=$(check_node "WEB" "192.168.10.11" "80") +WAS_J=$(check_node "WAS" "192.168.10.12" "8080") +DB_J=$(check_node "DB" "192.168.10.13" "5432") +echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"results\":[${WEB_J},${WAS_J},${DB_J}]}" +``` + +--- + +## 2. manage_service.sh + +```bash +#!/bin/bash +# 사용법: ./manage_service.sh [WEB|WAS|DB] [status|start|stop|restart] +TARGET=$1; ACTION=$2 +declare -A SERVICE_NAMES=([WEB]="nginx" [WAS]="tomcat" [DB]="postgresql") +HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]}; SVC=${SERVICE_NAMES[$TARGET]} + +case ${ACTION} in + status) + RAW=$(execute_remote_cmd "$HOST" "$USER" "systemctl is-active ${SVC}") + [ "$RAW" = "active" ] && STATUS="RUNNING" || STATUS="STOPPED" + echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"status\":\"${STATUS}\"}" + ;; + start|stop|restart) + RAW=$(execute_remote_cmd "$HOST" "$USER" "sudo systemctl ${ACTION} ${SVC}") + [ $? -eq 0 ] && RESULT="SUCCESS" || RESULT="FAILED" + echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"result\":\"${RESULT}\",\"action\":\"${ACTION}\"}" + ;; +esac +``` + +--- + +## 3. collect_target_logs.sh + +```bash +#!/bin/bash +# 사용법: ./collect_target_logs.sh [WEB|WAS|DB] [라인수(기본100)] +TARGET=$1; LINES=${2:-100} +declare -A LOG_PATHS=([WEB]="/var/log/nginx/error.log" [WAS]="/app/tomcat/logs/catalina.out" [DB]="/var/log/postgresql/postgresql.log") +HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]}; LOG=${LOG_PATHS[$TARGET]} + +REMOTE_CMD="tail -n ${LINES} ${LOG} | awk '/Exception|ERROR|Error|CRITICAL|FATAL/{print \"[ALERT] \" \$0; next}{print \$0}'" +OUTPUT=$(execute_remote_cmd "$HOST" "$USER" "$REMOTE_CMD") +CLEAN=$(echo "$OUTPUT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}') +echo "{\"server\":\"${TARGET}\",\"log_path\":\"${LOG}\",\"lines\":${LINES},\"log_data\":\"${CLEAN}\"}" +``` + +--- + +## 5. check_db_status.sh + +```bash +#!/bin/bash +# 사용법: ./check_db_status.sh DB check (Lock 세션 조회) +# ./check_db_status.sh DB kill 1234 (PID 강제 해제) +TARGET=$1; ACTION=$2; PID=$3 +HOST=${SERVER_HOSTS[$TARGET]}; USER=${SERVER_USERS[$TARGET]} + +case $ACTION in + check) + SQL="SELECT json_build_object('total_connections',(SELECT count(*) FROM pg_stat_activity),'locked_sessions',(SELECT coalesce(json_agg(json_build_object('blocked_pid',pid,'age',clock_timestamp()-query_start,'query',query)),'{}'::json) FROM pg_stat_activity WHERE wait_event_type='Lock'));" + execute_remote_cmd "$HOST" "$USER" "psql -d mes_db -t -c \"${SQL}\"" | tr -d '\n' | sed 's/ //g' + ;; + kill) + [ -z "$PID" ] && echo '{"status":"ERROR","message":"PID 필요"}' && exit 1 + OUT=$(execute_remote_cmd "$HOST" "$USER" "psql -d mes_db -t -c \"SELECT pg_terminate_backend(${PID});\"") + [[ "$OUT" == *"t"* ]] && RESULT="SUCCESS" || RESULT="FAILED" + echo "{\"server\":\"${TARGET}\",\"action\":\"KILL\",\"pid\":${PID},\"result\":\"${RESULT}\"}" + ;; +esac +``` + +--- + +## 크론탭 스케줄러 등록 + +```bash +#!/bin/bash +# agent_self_scheduler.sh install — 모든 감시 스크립트 자동 등록 + +cat << EOF > /tmp/agent_cron.tmp +# 5분마다: 자원 감시 + 자가 치유 +*/5 * * * * /app/agent/bin/self_healing_defense.sh WEB > /dev/null 2>&1 +*/5 * * * * /app/agent/bin/self_healing_defense.sh WAS > /dev/null 2>&1 +*/5 * * * * /app/agent/bin/self_healing_defense.sh DB > /dev/null 2>&1 + +# 1시간마다: 보안 감사 +0 */1 * * * /app/agent/bin/security_audit.sh WEB > /dev/null 2>&1 +0 */1 * * * /app/agent/bin/security_audit.sh WAS > /dev/null 2>&1 +0 */1 * * * /app/agent/bin/security_audit.sh DB > /dev/null 2>&1 + +# 매일 03:00: 로그 정비 +0 3 * * * /app/agent/bin/backup_and_clear.sh WAS > /dev/null 2>&1 + +# 매일 04:00: SSL 인증서 만료 스캔 +0 4 * * * /app/agent/bin/ssl_expiry_detector.sh WEB > /dev/null 2>&1 + +# 10분마다: ESB 연계 감시 +*/10 * * * * /app/agent/bin/esb_channel_monitor.sh 192.168.10.21 > /dev/null 2>&1 +EOF + +crontab /tmp/agent_cron.tmp +rm /tmp/agent_cron.tmp +echo '{"status":"SUCCESS","message":"크론탭 스케줄 등록 완료"}' +``` + +--- + +## 알림 웹훅 전송 (`alert_webhook_sender.sh`) + +```bash +#!/bin/bash +# 사용법: ./alert_webhook_sender.sh "WAS" "CRITICAL" "디스크 95% 초과" +SERVER=$1; LEVEL=$2; MSG=$3 +WEBHOOK="http://192.168.10.50:8080/api/v1/infrastructure/alerts" + +PAYLOAD="{\"system_source\":\"PC_MIDDLEWARE_AGENT\",\"target_node\":\"${SERVER}\",\"severity_level\":\"${LEVEL}\",\"incident_message\":\"${MSG}\",\"occurred_at\":\"$(date '+%Y-%m-%d %H:%M:%S')\"}" + +CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d "${PAYLOAD}" --connect-timeout 3 "${WEBHOOK}") +[ "$CODE" -eq 200 ] || [ "$CODE" -eq 201 ] \ + && echo "{\"status\":\"SUCCESS\",\"code\":\"${CODE}\"}" \ + || echo "{\"status\":\"FAILED\",\"code\":\"${CODE}\"}" +``` diff --git a/docs/system_spec.md b/docs/system_spec.md new file mode 100644 index 00000000..0d6ac5a6 --- /dev/null +++ b/docs/system_spec.md @@ -0,0 +1,96 @@ +# [Specification] GUARDiA 시스템 아키텍처 명세 + +## 1. 프로젝트 개요 + +### 1.1. 구축 배경 +- 1,000개 이상의 관공서 SM 사이트, 10,000+ 운영 환경 관리 +- 레거시 서버: 에이전트 설치 불가, 수동 FTP/SSH 반복 작업 만연 +- 목표: 메신저 자연어 명령 → AI → 에이전트리스 자동 배포·운영 + +### 1.2. 핵심 가치 +- **ChatOps**: 메신저가 유일한 터미널 +- **Agentless**: SSH/FTP 표준 프로토콜만 사용 +- **Human-in-the-loop**: PM 최종 승인 후 실행 + +--- + +## 2. 전체 아키텍처 (3-Layer) + +``` +┌─────────────────────────────────────────────────────────┐ +│ INTAKE LAYER (현장 접수) │ +│ 메신저 앱/웹 → Webhook → 입력 검증 모듈 │ +└───────────────────────┬─────────────────────────────────┘ + │ +┌───────────────────────▼─────────────────────────────────┐ +│ CONTROL LAYER (지능형 제어) │ +│ ① SR/Ops 작업 관리 엔진 (상태: RECEIVED→COMPLETED) │ +│ ② RBAC 보안 승인 엔진 (담당자→대리→상위 에스컬레이션)│ +│ ③ sLLM Parser (자연어→JSON 정형화) │ +│ ④ Claude Code 에이전트 (코드수정·스크립트 생성) │ +│ ⑤ HITL 모듈 (PM 최종 검증 대기) │ +└───────────────────────┬─────────────────────────────────┘ + │ +┌───────────────────────▼─────────────────────────────────┐ +│ EXECUTION LAYER (에이전트리스 실행) │ +│ SSH Executor / SFTP Client → 대상 서버(1,000+) │ +│ Command Sanitizer → Audit Logger (Hash Chain) │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 기술 스택 + +| 구분 | 기술 | 버전/비고 | +|------|------|-----------| +| Backend | Python / FastAPI | 3.11+ | +| LLM | Llama-3-8B 또는 Solar-10.7B | 온프레미스, 4-bit 양자화 | +| Infra 제어 | paramiko (SSH/SFTP) | 에이전트리스 | +| DB | PostgreSQL | CMDB + SR + Audit | +| 메신저 연동 | Webhook (REST) | 슬랙/잔디/자체앱 | +| 보안 | AES-256 (자격증명), SHA-256 (감사로그 해시체인) | | + +--- + +## 4. 사용자 역할별 지원 기능 + +### 4.1. Developer (배포) +- 로컬 빌드 파일(class, html, js, img) 메신저 첨부 → 자동 배포 +- 정적 파일: 카피 즉시 반영 (WAS 재기동 불필요) +- 동적 파일(class): 무중단 롤링 재기동 (WAS #1 → 헬스체크 → WAS #2) + +### 4.2. SM 운영자 +- 원격 로그 분석: 에러 키워드 grep, 타임라인 추출 +- 인프라 점검: 디스크/CPU/메모리/포트 임계치 감시 +- 자원 정비: 로그 로테이션, 오래된 아카이브 삭제 +- SSL 인증서 만료일 사전 감지 + +### 4.3. PM 관리자 +- SR 진척도 및 배포 이력 타임라인 조회 +- 민감 시스템 배포 시 2차 승인 워크플로우 제어 +- 배포 결과 HITL 최종 확인 후 고객 완료 통보 + +--- + +## 5. 핵심 보안 정책 + +| 정책 | 내용 | +|------|------| +| 자격증명 저장 | AES-256 암호화 DB 저장, 실행 시 메모리 로드 | +| SSH 접근 | 관제 전용 계정(opsagent) + sudoers 화이트리스트 | +| 명령어 필터 | Blacklist: `rm -rf /`, `mkfs`, `drop`, `truncate` | +| 감사 로그 | SHA-256 해시 체이닝 — 위변조 즉시 탐지 | +| Kill-Switch | 메신저 '정지' 명령 → 즉시 중단 + 롤백 | +| 폐쇄망 | 외부 API 호출 금지, sLLM 온프레미스만 허용 | + +--- + +## 6. 개발 로드맵 + +| Phase | 내용 | 목표 | +|-------|------|------| +| P1 | 메신저 웹훅 + DB 티켓팅 시스템 | SR 접수 → DB 저장 | +| P2 | sLLM 파서 + 승인 워크플로우 | 자연어 → JSON → HITL | +| P3 | SSH/SFTP 배포 엔진 + 롤링 재기동 | 에이전트리스 실행 | +| P4 | End-to-End 자동화 루프 완성 | SR→배포→알림 전 과정 | diff --git a/guardia_setup.ps1 b/guardia_setup.ps1 new file mode 100644 index 00000000..95a548a1 --- /dev/null +++ b/guardia_setup.ps1 @@ -0,0 +1,146 @@ +# ============================================================ +# GUARDiA - Claude Code + MCP Filesystem 자동 설치 스크립트 +# PowerShell을 관리자 권한으로 실행 후 붙여넣기 +# ============================================================ + +$ErrorActionPreference = "Stop" + +function Write-Step($msg) { + Write-Host "`n>>> $msg" -ForegroundColor Cyan +} + +function Write-OK($msg) { + Write-Host " [OK] $msg" -ForegroundColor Green +} + +function Write-Warn($msg) { + Write-Host " [!!] $msg" -ForegroundColor Yellow +} + +# ────────────────────────────────────────── +# 1. C:\GUARDiA 디렉토리 생성 +# ────────────────────────────────────────── +Write-Step "1/5 C:\GUARDiA 디렉토리 확인" + +if (-not (Test-Path "C:\GUARDiA")) { + New-Item -ItemType Directory -Path "C:\GUARDiA" | Out-Null + Write-OK "C:\GUARDiA 생성 완료" +} else { + Write-OK "C:\GUARDiA 이미 존재" +} + +# ────────────────────────────────────────── +# 2. Claude Code 설치 +# ────────────────────────────────────────── +Write-Step "2/5 Claude Code 설치" + +$claudePath = "$env:USERPROFILE\.local\bin\claude.exe" +$claudeInstalled = $false + +if (Get-Command claude -ErrorAction SilentlyContinue) { + Write-OK "claude 명령어 이미 사용 가능" + $claudeInstalled = $true +} elseif (Test-Path $claudePath) { + Write-OK "claude.exe 존재 - PATH만 추가 필요" +} else { + Write-Warn "Claude Code 설치 중..." + try { + Invoke-RestMethod https://claude.ai/install.ps1 | Invoke-Expression + Write-OK "Claude Code 설치 완료" + } catch { + Write-Warn "공식 설치 실패 - npm으로 재시도" + if (Get-Command npm -ErrorAction SilentlyContinue) { + npm install -g @anthropic-ai/claude-code + Write-OK "npm 설치 완료" + } else { + Write-Host "`n[오류] npm도 없습니다. Node.js를 먼저 설치해 주세요:" -ForegroundColor Red + Write-Host " https://nodejs.org/en/download" -ForegroundColor Yellow + exit 1 + } + } +} + +# ────────────────────────────────────────── +# 3. PATH 등록 +# ────────────────────────────────────────── +Write-Step "3/5 PATH 환경변수 등록" + +$localBin = "$env:USERPROFILE\.local\bin" +$npmGlobal = (npm config get prefix 2>$null) + +$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + +$pathsToAdd = @($localBin) +if ($npmGlobal) { $pathsToAdd += "$npmGlobal" } + +foreach ($p in $pathsToAdd) { + if ($currentPath -notlike "*$p*") { + [Environment]::SetEnvironmentVariable("PATH", "$currentPath;$p", [EnvironmentVariableTarget]::User) + $env:PATH = "$env:PATH;$p" + Write-OK "PATH 추가: $p" + } else { + Write-OK "이미 PATH에 있음: $p" + } +} + +# ────────────────────────────────────────── +# 4. .mcp.json 생성 (Claude Code 프로젝트 설정) +# ────────────────────────────────────────── +Write-Step "4/5 C:\GUARDiA\.mcp.json 생성" + +$mcpConfig = @{ + mcpServers = @{ + filesystem = @{ + command = "cmd" + args = @("/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:/GUARDiA") + } + } +} | ConvertTo-Json -Depth 5 + +$mcpPath = "C:\GUARDiA\.mcp.json" +Set-Content -Path $mcpPath -Value $mcpConfig -Encoding UTF8 +Write-OK ".mcp.json 저장 완료: $mcpPath" + +# ────────────────────────────────────────── +# 5. Claude Desktop config 업데이트 +# ────────────────────────────────────────── +Write-Step "5/5 Claude Desktop 설정 업데이트" + +$desktopConfigDir = "$env:APPDATA\Claude" +$desktopConfigPath = "$desktopConfigDir\claude_desktop_config.json" + +if (-not (Test-Path $desktopConfigDir)) { + New-Item -ItemType Directory -Path $desktopConfigDir | Out-Null +} + +$desktopConfig = @{ + mcpServers = @{ + filesystem = @{ + command = "npx" + args = @("-y", "@modelcontextprotocol/server-filesystem", "C:\\GUARDiA") + } + } +} | ConvertTo-Json -Depth 5 + +Set-Content -Path $desktopConfigPath -Value $desktopConfig -Encoding UTF8 +Write-OK "claude_desktop_config.json 저장 완료" + +# ────────────────────────────────────────── +# 완료 메시지 +# ────────────────────────────────────────── +Write-Host "`n============================================" -ForegroundColor Magenta +Write-Host " 설치 완료!" -ForegroundColor Magenta +Write-Host "============================================" -ForegroundColor Magenta +Write-Host @" + +다음 단계: + 1. PowerShell 창을 닫고 새로 여세요 (PATH 적용) + 2. Claude Desktop을 완전히 종료 후 재시작하세요 + 3. Claude Code 실행: + cd C:\GUARDiA + claude + 4. Claude Code 안에서 확인: + /mcp (MCP 서버 상태) + /doctor (전체 진단) + +"@ -ForegroundColor White diff --git a/skills/guardia-agent/SKILL.md b/skills/guardia-agent/SKILL.md new file mode 100644 index 00000000..1f5e0f86 --- /dev/null +++ b/skills/guardia-agent/SKILL.md @@ -0,0 +1,145 @@ +--- +name: guardia-agent +description: > + GUARDiA 프로젝트에서 Python 역방향 에이전트(Reverse Agent) 코드를 작성하거나 + 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: + - 내부망 중계 PC 에이전트 코드 작성 + - asyncio / websockets 기반 역방향 연결 구현 + - PostgreSQL psycopg2 DB 조회 코드 + - SM 운영 쉘 스크립트 원격 실행 파이프라인 + - 에이전트 자동 재연결(Backoff) 로직 +--- + +# GUARDiA 역방향 에이전트 스킬 + +## 개념: 역방향 연결 (Reverse Connection) + +공공기관 방화벽은 **인바운드 차단** → 에이전트가 먼저 **아웃바운드로 연결** +외부 서버로 전화를 걸어두면 서버가 그 터널을 통해 명령 전달 가능 + +``` +[내부망 에이전트] ──(Outbound WS)──► [외부 중계 WAS] + ◄──(명령 역전달)──── +``` + +## 핵심 구현 패턴 + +### 무한 재연결 루프 (Backoff) +```python +async def agent_main_loop(): + while True: + try: + async with websockets.connect(EXTERNAL_WS_URL) as ws: + print("연결 성공") + async for message in ws: + await handle_command(ws, json.loads(message)) + except (websockets.ConnectionClosed, OSError): + print("연결 끊김 — 5초 후 재연결") + await asyncio.sleep(5) + except Exception as e: + print(f"예외: {e}") + await asyncio.sleep(5) +``` + +### 화이트리스트 기반 명령 분기 (필수) +```python +async def handle_command(ws, packet): + action = packet.get("action") + params = packet.get("params", {}) + task_id = packet.get("task_id") + room_id = packet.get("room_id") + + # 절대로 임의 셸 명령 직접 실행 금지 + # 반드시 정의된 함수만 호출 + if action == "FETCH_MES_QC": + data = fetch_mes_qc(params.get("date")) + status = "SUCCESS" + elif action == "CHECK_INTERNAL_WAS_STATUS": + data = check_was_health() + status = "SUCCESS" + elif action == "CHECK_DISK_SPACE": + data = check_disk_space() + status = "SUCCESS" + else: + data = {"error": f"허용되지 않은 action: {action}"} + status = "FAIL" + + await ws.send(json.dumps({ + "event": "TASK_FINISHED", + "room_id": room_id, + "task_id": task_id, + "payload": {"status": status, "data": data} + })) +``` + +## DB 조회 표준 (psycopg2) + +```python +from psycopg2.extras import RealDictCursor +import datetime + +class InfrastructureJsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + return super().default(obj) + +def fetch_mes_qc(work_date: str) -> list: + """고정 쿼리 + 파라미터 바인딩 (SQL Injection 방지)""" + conn = psycopg2.connect(**DB_CONFIG) + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + sql = """ + SELECT work_date, prc_code, err_msg, reg_date + FROM tb_mes_wrk_prc_i + WHERE work_date = %s + ORDER BY reg_date DESC LIMIT 10 + """ + cur.execute(sql, (work_date,)) + return json.loads(json.dumps(cur.fetchall(), cls=InfrastructureJsonEncoder)) + finally: + conn.close() +``` + +## WAS 헬스체크 + +```python +def check_was_health() -> dict: + try: + r = requests.get("http://10.100.10.10:8080/actuator/health", timeout=3) + return r.json() + except requests.exceptions.ConnectionError: + return {"status": "DOWN", "error": "연결 거부"} + except requests.exceptions.Timeout: + return {"status": "DOWN", "error": "타임아웃"} +``` + +## 금지 사항 + +- `subprocess.run(user_input)` 절대 금지 +- 패스워드/API 키 하드코딩 금지 → 환경변수 또는 암호화 설정 파일 +- 예외 발생 시 에이전트 프로세스 다운(exit) 금지 → try-except로 감싸고 계속 실행 +- 메신저 응답에 내부망 IP 그대로 노출 금지 + +## 환경변수 참조 패턴 + +```python +import os +DB_CONFIG = { + "host": os.environ["DB_HOST"], + "port": int(os.environ.get("DB_PORT", 5432)), + "user": os.environ["DB_USER"], + "password": os.environ["DB_PASSWORD"], + "database": os.environ["DB_NAME"], +} +EXTERNAL_WS_URL = os.environ["EXTERNAL_WS_URL"] +``` + +## Windows 서비스 등록 (운영 배포용) + +``` +# NSSM 사용 (Non-Sucking Service Manager) +nssm install GUARDiA-Agent "python" "C:\GUARDiA\src\agent\main.py" +nssm set GUARDiA-Agent AppDirectory "C:\GUARDiA" +nssm start GUARDiA-Agent +``` diff --git a/skills/guardia-deploy/SKILL.md b/skills/guardia-deploy/SKILL.md new file mode 100644 index 00000000..75e8895f --- /dev/null +++ b/skills/guardia-deploy/SKILL.md @@ -0,0 +1,93 @@ +--- +name: guardia-deploy +description: > + GUARDiA 프로젝트에서 SSH/SFTP 기반 에이전트리스 배포 엔진 코드를 작성하거나 + 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: + - paramiko 관련 코드 작성/수정 + - 롤링 배포 로직 구현 + - 헬스체크 / 롤백 로직 작성 + - 배포 파이프라인 API 엔드포인트 작성 + - 파일 무결성(MD5/SHA-256) 검증 코드 +--- + +# GUARDiA 배포 엔진 스킬 + +## 핵심 원칙 + +1. **백업 우선**: 배포 전 반드시 타임스탬프 포함 백업 (`app.jar.bak_YYYYMMDD_HHMMSS`) +2. **무결성 검증**: SFTP 전송 후 MD5 로컬 vs 원격 비교 필수 +3. **헬스체크 강제**: 재기동 후 30초 이내 HTTP 200 미확인 시 즉시 롤백 +4. **롤링 배포**: WAS #1 완료·검증 후 WAS #2 시작 (동시 배포 금지) +5. **감사 로그**: 성공/실패 모두 `log_execution()` 호출 필수 + +## 환경 변수 + +```python +SSH_TIMEOUT = 300 # SSH 명령 실행 타임아웃 (초) +HEALTH_TIMEOUT = 30 # 헬스체크 최대 대기 (초) +HEALTH_INTERVAL = 2 # 헬스체크 폴링 간격 (초) +DISK_THRESHOLD = 90 # 배포 전 디스크 임계치 (%) +MAX_BATCH_SIZE = 50 # 티어2 병렬 배포 단위 +``` + +## 금지 사항 + +- root 계정으로 SSH 접속 금지 → opsagent + sudoers 사용 +- `rm -rf`, `mkfs`, `format`, `drop table` 등 Blacklist 명령 차단 +- 자격증명(비밀번호) 로그 출력 금지 +- 헬스체크 없이 다음 노드 진행 금지 + +## 코드 패턴 + +### SSH 실행 +```python +result = executor.execute_command("sh /app/scripts/startup.sh") +if result["exit_code"] != 0: + raise DeploymentError(f"startup 실패: {result['stderr']}") +``` + +### 파일 전송 + 무결성 +```python +remote_md5 = executor.upload_file(local_path, remote_path) +local_md5 = hashlib.md5(open(local_path, 'rb').read()).hexdigest() +assert local_md5 == remote_md5, "MD5 불일치" +``` + +### 헬스체크 +```python +deadline = time.time() + HEALTH_TIMEOUT +while time.time() < deadline: + try: + r = requests.get(f"http://{ip}:8080", timeout=3) + if r.status_code == 200: + return True + except: + pass + time.sleep(HEALTH_INTERVAL) +return False # 실패 → 즉시 rollback 트리거 +``` + +## 에러 처리 표준 + +```python +try: + # 배포 로직 +except DeploymentError as e: + self._rollback(executor, node, backup_ts) + log_execution(sr_id, node["ip"], "DEPLOY_FAILED", 1, str(e)) + messenger.notify_failure(sr_id, str(e)) + return False +finally: + executor.close() +``` + +## 정적 vs 동적 자원 판별 + +```python +STATIC_EXTENSIONS = {".html", ".js", ".css", ".png", ".jpg", ".gif", ".svg"} +DYNAMIC_EXTENSIONS = {".class", ".jar", ".war", ".properties"} + +def requires_restart(filename: str) -> bool: + ext = Path(filename).suffix.lower() + return ext in DYNAMIC_EXTENSIONS +``` diff --git a/skills/guardia-messenger/SKILL.md b/skills/guardia-messenger/SKILL.md new file mode 100644 index 00000000..9ea24b59 --- /dev/null +++ b/skills/guardia-messenger/SKILL.md @@ -0,0 +1,186 @@ +--- +name: guardia-messenger +description: > + GUARDiA 프로젝트에서 메신저 연동 및 sLLM 파서 코드를 작성하거나 + 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: + - FastAPI Webhook 엔드포인트 구현 + - WebSocket 실시간 중계 서버 코드 + - sLLM 자연어 파서 프롬프트 엔지니어링 + - 메신저 응답 포맷 및 인터랙티브 버튼 구현 + - Proactive 챗봇 대화 맥락 분석 +--- + +# GUARDiA 메신저 연동 스킬 + +## sLLM 파서 원칙 + +### 출력 포맷 강제 (절대 준수) +``` +- JSON 외 텍스트 출력 금지 +- 부연 설명, 인삿말 포함 금지 +- temperature: 0.1 (결정론적 출력) +- response_format: {"type": "json_object"} +``` + +### 시스템 프롬프트 기본 구조 +```python +SYSTEM_PROMPT = """ +당신은 인프라 자동화 시스템의 핵심 파서입니다. +1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다. +2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다. +3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다. +4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다. + +출력 포맷 (반드시 이 구조만 사용): +{ + "intent_type": "DEPLOY_UPGRADE|LOG_ANALYSIS|SERVICE_RESTART|SSL_CHECK|DB_QUERY|CRON_MANAGE", + "institution": "기관명 또는 UNKNOWN", + "system_name": "시스템명 또는 UNKNOWN", + "infrastructure_layer": "WEB|WAS|DB|ESB", + "target_nodes": [], + "action_sequence": [], + "deploy_artifacts": [], + "requires_approval": false, + "priority": "HIGH|MEDIUM|LOW" +} +""" +``` + +## Webhook 수신 표준 + +```python +@app.post("/messenger/webhook") +async def receive_message(request: Request): + # 1. 시크릿 키 검증 + signature = request.headers.get("X-Messenger-Signature") + if not verify_signature(signature, await request.body()): + raise HTTPException(status_code=403) + + payload = await request.json() + user_id = payload.get("user_id") + text = payload.get("text", "") + files = payload.get("files", []) + + # 2. 봇 자신의 메시지는 무시 (무한 루프 방지) + if payload.get("bot") or not text.strip(): + return {"status": "IGNORED"} + + # 3. 파일 스테이징 + staged = await stage_uploaded_files(files, user_id) + + # 4. sLLM 파싱 + parsed = parse_natural_language(text) + + # 5. SR 생성 및 라우팅 + sr_id = create_ops_task(parsed, user_id, staged) + route_to_engine(sr_id, parsed) + + return {"status": "ACCEPTED", "sr_id": sr_id} +``` + +## 메신저 응답 포맷 + +### 텍스트 응답 +```python +def send_text_response(room_id: str, text: str): + messenger.post_message(room_id, { + "sender": "GUARDiA-Bot", + "text": text, + "msg_type": "CHAT" + }) +``` + +### 인터랙티브 버튼 응답 +```python +def send_interactive_response(room_id: str, text: str, action_code: str, label: str): + messenger.post_message(room_id, { + "sender": "GUARDiA-Bot", + "text": text, + "is_widget": True, + "interactive_action": { + "type": "BUTTON", + "label": label, + "command_code": action_code + } + }) + +# 사용 예 +send_interactive_response( + room_id, + "🤖 DB 지연 징후 감지. 대기 쿼리를 확인할까요?", + "FETCH_DB_LOCKS", + "대기 쿼리 확인" +) +``` + +### 승인 요청 응답 +```python +def request_approval(approver_id: str, sr_id: str, summary: str): + messenger.post_direct_message(approver_id, { + "sender": "GUARDiA-Bot", + "text": f"[{sr_id}] 배포 승인 요청\n{summary}", + "is_widget": True, + "interactive_action": { + "type": "APPROVAL_BUTTONS", + "approve_url": f"/api/sr/approve/{sr_id}", + "reject_url": f"/api/sr/reject/{sr_id}" + } + }) +``` + +## 금지 사항 + +- 메신저 응답에 서버 IP, 비밀번호, SSH 계정 노출 금지 +- 봇 응답 메시지에 다시 파서 실행 금지 (무한 루프) +- 에러 메시지에 스택트레이스 전체 노출 금지 → SR ID와 요약만 전달 +- 파일 스테이징 없이 바로 원격 서버 전송 금지 → 반드시 임시 디렉토리 거침 + +## 파일 스테이징 표준 + +```python +import aiofiles +from pathlib import Path + +STAGING_BASE = Path("/app/guardia/staging") + +async def stage_uploaded_files(files: list, user_id: str) -> list: + """메신저 첨부 파일을 임시 디렉토리에 다운로드""" + staged = [] + sr_dir = STAGING_BASE / user_id / datetime.now().strftime("%Y%m%d_%H%M%S") + sr_dir.mkdir(parents=True, exist_ok=True) + + for f in files: + filename = f["name"] + url = f["url"] + dest = sr_dir / filename + + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + async with aiofiles.open(dest, "wb") as fp: + await fp.write(await resp.read()) + + staged.append({"name": filename, "path": str(dest), "type": classify_artifact(filename)}) + + return staged + +def classify_artifact(filename: str) -> str: + ext = Path(filename).suffix.lower() + if ext in {".class", ".jar", ".war"}: return "DYNAMIC" + if ext in {".html", ".js", ".css"}: return "STATIC" + if ext in {".png", ".jpg", ".gif"}: return "STATIC" + return "UNKNOWN" +``` + +## Nginx 업스트림 설정 (WebSocket) + +```nginx +location /ws { + proxy_pass http://was_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 86400s; + proxy_buffering off; + client_max_body_size 100M; +} +``` diff --git a/메신져.docx b/메신져.docx new file mode 100644 index 00000000..7cc5bdc9 Binary files /dev/null and b/메신져.docx differ diff --git a/특허출원.docx b/특허출원.docx new file mode 100644 index 00000000..12ab2b69 Binary files /dev/null and b/특허출원.docx differ