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 <noreply@anthropic.com>
This commit is contained in:
commit
45f96176a6
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git *)",
|
||||
"Bash(python *)",
|
||||
"Bash(pip *)",
|
||||
"Bash(uvicorn *)",
|
||||
"Bash(pytest *)",
|
||||
"Bash(alembic *)",
|
||||
"Bash(psql *)",
|
||||
"Bash(ssh *)",
|
||||
"Bash(sftp *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@ -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
|
||||
88
CLAUDE.md
Normal file
88
CLAUDE.md
Normal file
@ -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`
|
||||
162
docs/ai_orchestration.md
Normal file
162
docs/ai_orchestration.md
Normal file
@ -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)`
|
||||
- 위변조 탐지: 매시간 해시 체인 연결성 자동 검증
|
||||
197
docs/db_schema.md
Normal file
197
docs/db_schema.md
Normal file
@ -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');
|
||||
```
|
||||
215
docs/deployment_engine.md
Normal file
215
docs/deployment_engine.md
Normal file
@ -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}
|
||||
```
|
||||
254
docs/messenger_integration.md
Normal file
254
docs/messenger_integration.md
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
183
docs/security_governance.md
Normal file
183
docs/security_governance.md
Normal file
@ -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} 롤백 실패 — 수동 점검 필요")
|
||||
```
|
||||
185
docs/shell_scripts_guide.md
Normal file
185
docs/shell_scripts_guide.md
Normal file
@ -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}\"}"
|
||||
```
|
||||
96
docs/system_spec.md
Normal file
96
docs/system_spec.md
Normal file
@ -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→배포→알림 전 과정 |
|
||||
146
guardia_setup.ps1
Normal file
146
guardia_setup.ps1
Normal file
@ -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
|
||||
145
skills/guardia-agent/SKILL.md
Normal file
145
skills/guardia-agent/SKILL.md
Normal file
@ -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
|
||||
```
|
||||
93
skills/guardia-deploy/SKILL.md
Normal file
93
skills/guardia-deploy/SKILL.md
Normal file
@ -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
|
||||
```
|
||||
186
skills/guardia-messenger/SKILL.md
Normal file
186
skills/guardia-messenger/SKILL.md
Normal file
@ -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;
|
||||
}
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user