refactor: organize infra/ into compute/network/security/data/platform

This commit is contained in:
heimdall
2026-04-16 13:43:36 +09:00
parent 66d2f51743
commit f0e51daafd
36 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
---
title: anomaly-detect (VictoriaLogs + ollama 기반 이상 트래픽 감지)
updated: 2026-04-08 agentic 재설계
tags: [security, crowdsec, victorialogs, ollama, gemma, anomaly]
---
> 코드: `gitea.inouter.com/kaffa/anomaly-detect` (private)
> 아키텍처: OpenRouter agentic (Grok-4-fast) + VictoriaLogs tool + CrowdSec LAPI tool
# anomaly-detect
## 아키텍처
OpenRouter `x-ai/grok-4-fast`에 tool 2개(logsql_query, ban_ips)만 노출하는 agentic 구조. fallback 모델 `qwen/qwen3-235b-a22b-2507`.
설계 반복 및 모델 벤치마크 이력: [[2026-04-08-anomaly-detect-iterations|history]]
### 새 아키텍처
```
systemd timer (5분)
analyzer.py (Python oneshot)
├─ OpenRouter → x-ai/grok-4-fast (tools=[logsql_query, ban_ips])
├─ agent loop (max 10턴)
│ ├─ tool: logsql_query(query, start, limit) — VictoriaLogs 자유 조회
│ │ ※ 응답에서 사설망/Tailscale/RFC5737 IP 자동 제거 (서버측 guardrail)
│ └─ tool: ban_ips(ips, reason, scenario) — CrowdSec LAPI batch POST
│ ※ dedup 24h, MAX_BAN_PER_CYCLE cap, 사설망 거부
├─ DRY_RUN=1 (기본): ban_ips가 "would ban" 로그만 찍고 실제 POST 안 함
└─ dedup.json 갱신
```
### 환경변수
| 변수 | 기본값 | 비고 |
|------|--------|------|
| `OPENROUTER_API_KEY` | (from `/etc/anomaly-detect/openrouter.env`) | Vault `secret/ai/openrouter` |
| `OR_MODEL` | `x-ai/grok-4-fast` | 주 모델 |
| `OR_FALLBACK_MODEL` | `qwen/qwen3-235b-a22b-2507` | OR 장애 시 재시도 |
| `WINDOW_MIN` | 5 | 조사 윈도우 |
| `MAX_TURNS` | 10 | agent loop 상한 |
| `MAX_BAN_PER_CYCLE` | 2000 | 한 사이클 ban 상한 (대규모 DDoS 대비) |
| `LAPI_BAN_CHUNK` | 500 | LAPI POST를 500건씩 쪼개 발송 (부분 실패 허용) |
| `BAN_DURATION` | 4h | |
| `DRY_RUN` | **1** | ⚠ 초기 안전장치 |
### 서버측 guardrail (중요)
LLM은 지시만 받고 강제할 수 없다. 따라서 `logsql_query``ban_ips` 두 tool 모두 **파이썬 코드 레벨에서** 다음을 enforce:
- `is_skippable_ip()`: `ipaddress.is_private` + Tailscale 100.64/10 + RFC5737 TEST-NET-2/3 포함
- Python 3.9의 `ipaddress.ip_address('203.0.113.42').is_private == True` — 문서 IP도 자동 차단됨 (2026-04-08 벤치마크에서 확인)
LLM 프롬프트가 무시되어도 실수로 사설망이 ban되지 않음.
### Vault 위치
- OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키)
- 컨테이너 배포본: `/etc/anomaly-detect/openrouter.env` (mode 600, systemd `EnvironmentFile=`)
### 운영 중 주의사항
- **injected 로그 잔재**: E2E 테스트 중 주입한 270 rows가 vlogs retention 기간 동안 남음. `sim_e2e` 마커 필드로 식별 가능. 다음 cycle에서 다시 탐지될 수 있으나 dedup 24h으로 재ban 차단됨.
- **재테스트 시**: 테스트 후 반드시 `cscli decisions delete -s anomaly-detect/<scenario>` + `/var/lib/anomaly-detect/dedup.json` 업데이트 (해당 IP 추가하거나 리셋)
### 비용 모니터링
매 사이클 journalctl에 한 줄 요약 출력 (`48eb489` 이후):
```
cycle usage: turns=5 prompt=9142 completion=2670 total=11812 cost=$0.001866
```
집계 명령:
```bash
# 오늘 누적
ssh incus-hp2 "incus exec anomaly-detect -- bash -c '
journalctl -u anomaly-detect.service --since today --no-pager |
grep -oE \"cost=\\\$[0-9.]+\" |
awk -F\\\$ \"{sum+=\\\$2} END {printf \\\"today: \\\$%.6f (%d cycles)\\n\\\", sum, NR}\"
'"
# 사이클별 상세
ssh incus-hp2 "incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since today --no-pager | grep 'cycle usage'"
```
**OpenRouter `/activity` API의 지연** (~2-3주)으로 실시간 비용 조회는 여기(journalctl) 경로를 써야 한다. dashboard는 https://openrouter.ai/activity 에서 UI 확인 가능 (더 빠름). management(provisioning) 키는 Vault `secret/ai/openrouter``PROVISIONING_KEY` 에 저장됨.
**월 비용 예상**: 평시 사이클당 ~$0.001-0.002, 5분 주기 × 288/일 × 30일 = **~$10-15/월**. DDoS 발생 시 사이클당 $0.01 수준 (massive_ddos 시나리오 기준)까지 튈 수 있으나 일시적.
[[crowdsec-safeline#~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)|폐기된 ddos-detect]] 후속. [[victorialogs|VictoriaLogs]]에 적재된 서울+오사카 APISIX access log를 5분마다 분석하여 봇/공격성 IP를 [[crowdsec-safeline|CrowdSec]]에 자동 ban으로 등록한다.
## 위치 / 사양
| 항목 | 값 |
|------|-----|
| 호스트 | incus-hp2 |
| 컨테이너 | `anomaly-detect` (default 프로젝트, Debian 13 trixie) |
| IP | 10.100.2.164 |
| 사양 | 1 vCPU, 512MB RAM, 5GB |
| 설치 경로 | `/opt/anomaly-detect/{venv,analyzer.py}`, `/etc/anomaly-detect/lapi.yaml`, `/etc/anomaly-detect/openrouter.env`, `/var/lib/anomaly-detect/dedup.json` |
| systemd | `anomaly-detect.service` (oneshot) + `anomaly-detect.timer` (`OnCalendar=*:0/5`, `Persistent=true`, `RandomizedDelaySec=20`) |
## 데이터 흐름
```
[5분 주기 systemd timer]
analyzer.py
├─ 1) https://vl.inouter.com — LogsQL: program:apisix log_type:access 지난 5분
├─ 2) per-IP 통계 게이트 (count/4xx/5xx/499/distinct paths)
├─ 3) 후보 N개 (default max 5)
├─ 4) 각 후보 → http://100.87.221.126:11434/api/generate (kaffa-macmini ollama)
│ 모델: gemma4:e4b (Q4_K_M, 8.0B), format=json
├─ 5) verdict=yes → CrowdSec LAPI alert POST
│ http://10.253.100.240:8080/v1/alerts
│ profiles.yaml의 default_ip_remediation이 자동 ban 생성
└─ 6) dedup.json에 처리 IP + 타임스탬프 기록 (24h 내 재처리 안 함)
```
## CrowdSec LAPI 등록
`anomaly-detect`라는 watcher machine을 jp1 crowdsec에 등록하고, credentials를 컨테이너 안 `/etc/anomaly-detect/lapi.yaml`에 저장:
```yaml
url: http://10.253.100.240:8080
login: anomaly-detect
password: <vault: secret/apps/anomaly-detect>
```
> [!warning] cscli machines add 함정
> `cscli machines add NAME --auto`는 default로 `/etc/crowdsec/local_api_credentials.yaml`을 덮어씀 — 이건 jp1 crowdsec **daemon 자체의 LAPI 클라이언트 설정**이라 덮어쓰면 daemon이 새 password로 LAPI 인증을 시도하면서 동기화가 깨짐. 반드시 `--file <별도 경로>` 옵션을 줘야 한다. 만약 실수로 덮어썼다면 `cscli machines add <default-machine-name> --auto --force --file /etc/crowdsec/local_api_credentials.yaml`로 default machine 새 credentials 발급 후 `systemctl reload crowdsec`로 복구.
## 운영 명령
```bash
# 컨테이너 진입
ssh incus-hp2 'incus exec anomaly-detect -- bash'
# 수동 1회 실행
incus exec anomaly-detect -- /opt/anomaly-detect/venv/bin/python /opt/anomaly-detect/analyzer.py
# 상태
incus exec anomaly-detect -- systemctl status anomaly-detect.timer
incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since "30 minutes ago"
# dedup 초기화 (모든 IP 재분석 허용)
incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.json'
# 게이트/모델/주기 변경 → /etc/systemd/system/anomaly-detect.service의 Environment=
```
## 검증 (최초 배포)
- `cscli machines list``anomaly-detect` 등록 확인
- `curl https://vl.inouter.com/select/logsql/query?query=program:apisix\&limit=1` 200 OK (컨테이너 내부)
- `curl http://100.87.221.126:11434/api/tags``gemma4:e4b` 노출
- `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test)
## 향후 작업
- [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅
전임자 (`ddos-detect`, 1/2차 구현) 폐기 이력: [[2026-04-08-anomaly-detect-iterations|history]]

100
infra/platform/brokkr.md Normal file
View File

@@ -0,0 +1,100 @@
---
title: Brokkr — 홈페이지 제작 에이전트
updated: 2026-04-01
tags: [infra, agent, incus, claude-code]
---
## 개요
Brokkr는 홈페이지 제작 전담 에이전트. 북유럽 신화의 드워프 대장장이(토르의 묠니르 제작자)에서 이름을 따옴.
| 항목 | 값 |
|------|-----|
| 위치 | incus-kr1, default 프로젝트 |
| OS | Debian 13 (trixie) |
| IP | 10.100.3.54 |
| 접속 | `ssh root@10.100.3.54` (incus-kr1 경유) |
| 역할 | 홈페이지/웹사이트 제작 에이전트 |
## 설치된 도구
| 도구 | 버전 |
|------|------|
| Claude Code | v2.1.87 |
| Node.js | 22.22.2 |
| bun | 1.3.11 |
| git | 2.47.3 |
| jq | 1.7.1 |
| uv | 최신 |
| tmux | 3.5a |
| Python | 3.13.5 |
| gcloud SDK | 563.0.0 |
| socat | 1.8.0.3 |
## PATH 설정
`/root/.bashrc`에 추가됨:
```
export PATH=/root/.bun/bin:/opt/google-cloud-sdk/bin:/root/.local/bin:$PATH
```
bun은 `/usr/local/bin/bun`에도 심볼릭 링크.
## 설정
- SSH: root 키 인증 (kaffa@KaFFa-Mac-mini.lan)
- tmux: dotfiles 기반 Dracula 테마, Debian 서버 환경 최적화 (`~/.tmux.conf`)
- CLAUDE.md: 역할, 디자인 워크플로우, Figma/Stitch 사용법, 에셋 가이드 포함
## 실행 방법
```bash
# tmux 세션으로 실행
tmux new -d -s brokkr 'export PATH=/root/.bun/bin:/opt/google-cloud-sdk/bin:/root/.local/bin:$PATH && claude --channels plugin:discord@claude-plugins-official'
# 신뢰 확인 프롬프트 통과
tmux send-keys -t brokkr Enter
# 상태 확인
tmux capture-pane -t brokkr -p
```
## MCP 서버
| MCP | 상태 | 용도 |
|-----|------|------|
| plugin:figma:figma | ✓ | Figma 디자인 읽기/쓰기 (OAuth, credentials.json에 토큰 저장) |
| plugin:context7:context7 | ✓ | 라이브러리 문서 조회 |
| plugin:playwright:playwright | ✓ | 브라우저 자동화 |
| outline | ✓ | Outline Wiki 문서 CRUD (API 키: Vault `secret/apps/outline`, 키명 brokkr-api-key) |
| stitch | ✓ | Google Stitch AI 디자인 (gcloud 인증 필요) |
| claude.ai Google Calendar | ✓ | 캘린더 |
| claude.ai Gmail | ✓ | 이메일 |
| claude.ai Cloudflare | ✓ | Cloudflare Workers/R2 |
## Discord 연결
| 항목 | 값 |
|------|-----|
| 봇 ID | 1488674138359857152 |
| 봇 토큰 | Vault `secret/messaging/discord-brokkr` |
| 채널 1 | 1488669324393578506 (brokkr 전용) |
| 채널 2 | 1488679091514380450 |
| 권한 | Send Messages, Read Message History, View Channels, Add Reactions |
## 담당 작업
- Ironclad 홈페이지 제작 (ironclad.it.com)
- 기획서: https://outline.inouter.com/doc/ironclad-AlZo9my3KN
- 기술 스택: Astro + Google Stitch + Tailwind CSS
- 배포: cf-multisite (Gitea → R2 → Cloudflare Workers)
## 주의사항
- Figma OAuth 토큰은 `/root/.claude/.credentials.json`에 저장됨. 로컬 Mac keychain에서 복사한 토큰.
- gcloud 인증은 로컬에서 복사한 application_default_credentials.json 사용
## 관련 문서
- [[infra-hosts]] — 인프라 호스트 전체
- [[outline]] — Outline Wiki (기획서 위치)
- [[heimdall]] — 헤임달 에이전트 (인프라 관리)

View File

@@ -0,0 +1,147 @@
---
title: Helm 차트 관리 체계
updated: 2026-04-13
tags: [infra, k3s, helm, argocd, gitops]
---
## 개요
K3s 클러스터의 모든 서비스를 Helm으로 관리. 자체 차트는 Gitea에 호스팅하고 ArgoCD로 GitOps 배포.
## 차트 저장소
| 항목 | 값 |
|------|-----|
| Git 레포 | `gitea.inouter.com/kaffa/helm-charts` (private) |
| Helm 레지스트리 | `gitea.inouter.com/api/packages/kaffa/helm` |
| ArgoCD 레포 시크릿 | `repo-helm-charts` (argocd 네임스페이스) |
### BunnyCDN 캐시 바이패스
`iron-git` Pull Zone (ID 5584382)에 Edge Rule 적용:
- 패턴: `*/api/packages/*`
- ActionType: 3 (OverrideCacheTime), 값: 0
- 차트 업로드 후 index.yaml 캐시 문제 방지
## 자체 차트 구조
```
helm-charts/
├── charts/
│ ├── app/ # 범용 차트 (v0.4.0) — 단순 서비스용
│ ├── kroki/ # 전용 차트 — Kroki + Mermaid
│ ├── searxng/ # 전용 차트 — SearXNG + UA 패치 + TLS proxy CA
│ ├── outline/ # 전용 차트 — Outline Wiki + Redis
│ └── openmemory/ # 전용 차트 — MCP + UI + Qdrant
└── values/ # app 차트용 서비스별 values
├── juiceshop.yaml
├── cfb-manager.yaml
├── bunnycdn-mcp.yaml
├── smtp-relay.yaml
├── namecheap-api.yaml
├── vultr-api.yaml
├── pgcat.yaml # sidecar(monitor) 포함
├── proxysql.yaml # Galera 설정 포함
├── nas-proxy.yaml # external service (Synology NAS)
└── vault-mcp.yaml # external service (Vault) + extraIngressRoutes
```
### app 차트 기능 (v0.4.0)
- Deployment + Service + IngressRoute 기본 구성
- `sidecars` — 사이드카 컨테이너 지원
- `configMaps` — multiline ConfigMap 생성
- `secrets` — Secret 생성
- `external` — selector 없는 Service + EndpointSlice (외부 서비스 프록시)
- `extraIngressRoutes` — 복수 도메인 IngressRoute
- `probes` — liveness/readiness probe
## 전체 Helm 릴리스 목록
### 공식/외부 차트 (20개)
| 릴리스 | 네임스페이스 | 차트 |
|--------|-------------|------|
| apisix | apisix | apisix |
| apisix-ingress-controller | apisix | apisix-ingress-controller |
| argocd | argocd | argo-cd |
| cert-manager | cert-manager | cert-manager |
| external-secrets | external-secrets | external-secrets |
| gitea | gitea | gitea |
| longhorn | longhorn-system | longhorn |
| metallb | metallb-system | metallb |
| n8n | n8n | n8n |
| nfs-provisioner | nfs-provisioner | nfs-subdir-external-provisioner |
| nocodb | tools | zekker6/nocodb |
| portainer | portainer | portainer |
| reflector | kube-system | reflector |
| safeline | safeline | safeline |
| sftpgo | sftpgo | sftpgo |
| sshpiper | sshpiper | sshpiper |
| synology-iscsi | democratic-csi | democratic-csi |
| teleport-cluster | teleport | teleport-cluster |
| traefik | kube-system | traefik |
| vector | logging | vector |
| vlogs | logging | victoria-logs-single |
| vm-stack | monitoring | victoria-metrics-k8s-stack |
### 자체 app 차트 (10개, ArgoCD 관리)
| 릴리스 | 네임스페이스 | 비고 |
|--------|-------------|------|
| juiceshop | juiceshop | |
| cfb-manager | tools | SSH key 마운트 |
| bunnycdn-mcp | mcp | IngressRoute: bunny.inouter.com |
| smtp-relay | mail | Mailgun relay |
| namecheap-api | api | |
| vultr-api | api | |
| pgcat | db | monitor sidecar 포함 |
| proxysql | db | Galera hostgroup 설정 |
| nas-proxy | tools | external → 192.168.9.100:5000 |
| vault-mcp | tools | external → 10.253.101.58, hcv.inouter.com + vault-mcp.inouter.com |
### 전용 차트 (4개, ArgoCD 관리)
| 릴리스 | 네임스페이스 | 구성 |
|--------|-------------|------|
| kroki | kroki | kroki + mermaid sidecar |
| searxng | searxng | UA 패치, TLS proxy CA, Google proxy |
| outline | outline | outline + redis + PVC + IngressRoute |
| openmemory | openmemory | MCP + UI + Qdrant + PVC 2개 |
### Operator 관리 (ArgoCD/Helm 외)
| 서비스 | 네임스페이스 | 관리 방식 |
|--------|-------------|-----------|
| rabbitmq-cluster-operator | rabbitmq-system | 직접 매니페스트 |
| rabbitmq-server | mq | RabbitmqCluster CRD → Operator |
## ArgoCD 앱 목록
14개 Application, 모두 `automated + prune + selfHeal`:
```
juiceshop, cfb-manager, bunnycdn-mcp, smtp-relay, namecheap-api, vultr-api,
pgcat, proxysql, nas-proxy, vault-mcp, kroki, searxng, outline, openmemory
```
### 주의사항
- **EndpointSlice는 ArgoCD 제외 대상** — `discovery.k8s.io` 그룹이 argocd-cm에서 제외됨. external service의 EndpointSlice는 수동 관리 필요
- ArgoCD 앱 소스는 Gitea `helm-charts` 레포의 `charts/` 경로 참조
## 배포 워크플로우
```
1. charts/ 또는 values/ 수정
2. helm template 로 렌더링 확인
3. git push → Gitea helm-charts
4. ArgoCD 자동 sync (또는 hard refresh)
5. 동작 확인
```
## 관련 문서
- [[external-secrets]] — ESO + Vault 시크릿 동기화
- [[k3s-ingress-architecture]] — Traefik/APISIX 인그레스 구조
- [[cert-manager]] — TLS 인증서

View File

@@ -0,0 +1,17 @@
---
title: Forge 인프라 에이전트
updated: 2026-03-12
tags: [infra, agent, forge]
---
## 지식 베이스
infra.yaml 지식 베이스 + enrichment(IP→네트워크/인스턴스 레이블) + LLM(gemini-2.5-flash)으로 종합 보고
## 메시지 타입
메시지: INFRA_REPORT(전체보고), INFRA_QUERY(특정질문)
## MCP 구조
구조는 server.py(초기화), config.py(설정), haproxy_client.py(Runtime API), ssh_ops.py(SSH), db.py(SQLite), tools/(domains, servers, health, monitoring, configuration, certificates)로 구성되어 있다. → [[infra-hosts]], [[apisix]]

View File

@@ -0,0 +1,34 @@
---
title: 인프라 프로비저닝 (OpenTofu)
updated: 2026-03-12
---
## OpenTofu
OpenTofu v1.11.5 installed at /usr/local/bin/tofu
## Tofu API
Tofu API (infra-tool 컨테이너, incus-jp1): 포트 8080 (uvicorn --port 8080). 파일 /opt/infra-tool/tofu_router.py. 엔드포인트: GET /tofu/plan, GET /tofu/state, GET /tofu/output, POST /tofu/server(provider/region/plan/label), DELETE /tofu/server/{name}, GET /tofu/plans/{provider}. 접근: ssh kaffa@100.109.123.1 → incus exec infra-tool -- bash. TF 템플릿 경로: /opt/infra/templates/{linode,vultr,alicloud,zenlayer}.tf.tpl. 참고: [[infra-forge|Forge]] consumer.py는 아직 tofu API를 사용하지 않고 Linode/Vultr API를 직접 호출하는 구조임.
## Zenlayer BM 프로비저닝
Zenlayer BM 프로비저닝: 직접 API → OpenTofu 전환 완료 (2026-03-04)
## Zenlayer OS 이미지
Zenlayer BM OS 이미지 매핑: zl-debian12 → 'Debian 12.x 64bit' (catalog: debian), zl-ubuntu2204 → 'Ubuntu 22.04 64bit' (catalog: ubuntu), zl-centos7 → 'CentOS 7.4 64bit' (catalog: centos)
## 통일 워크플로
전 프로바이더(Linode/Vultr/Alicloud/Zenlayer) 통일된 TF 워크플로로 동작
## 트래픽 모니터링 TODO
TODO: 트래픽 전송량 모니터링 + 자동 차단 서비스 구현 필요
- Linode: Cloud Firewall API로 outbound DROP 적용
- Vultr: Firewall API로 deny all 적용
- Alibaba: ModifyInstanceNetworkSpec API로 InternetMaxBandwidthOut=0 설정
- 흐름: 주기적 전송량 모니터링 → 임계치 도달 → 방화벽/대역폭 차단 → 고객 Telegram 알림 → 고객 동의 시 해제
- 3개 프로바이더 모두 서버 중지 없이 네트워크만 차단 가능
- 별도 서비스 또는 기존 consumer-service에 모니터링 컨슈머 추가 방식 검토

59
infra/platform/kaniko.md Normal file
View File

@@ -0,0 +1,59 @@
---
title: Kaniko - K8s 네이티브 컨테이너 이미지 빌드
updated: 2026-04-12
tags:
- k8s
- image-build
- kaniko
---
## 개요
Kaniko는 Google에서 만든 Kubernetes 네이티브 컨테이너 이미지 빌드 도구. Docker 데몬 없이 컨테이너 내부에서 Dockerfile을 빌드하고 레지스트리에 푸시할 수 있다.
- 프로젝트: [googlecontainertools/kaniko](https://github.com/GoogleContainerTools/kaniko)
- 이미지: `gcr.io/kaniko-project/executor`
## 도입 배경
- K3s 클러스터에서 컨테이너 이미지를 빌드하기 위해 호스트(incus-kr1)에 Docker CE를 설치해 사용했으나, Kaniko 발견 후 Docker CE 제거 (2026-04-12)
- Docker 데몬 불필요 → 호스트에 Docker 설치할 필요 없음
- K8s Job/Pod으로 빌드 실행 가능
## 사용 방법
K8s Job으로 실행하는 기본 예시:
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-build
spec:
template:
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/user/repo.git"
- "--destination=registry.example.com/image:tag"
restartPolicy: Never
```
## 컨텍스트 소스
- Git 저장소: `git://`
- S3/GCS 버킷
- 로컬 디렉토리 (PVC 마운트)
- tar 파일
## 레지스트리 인증
- K8s Secret을 `/kaniko/.docker/config.json`에 마운트
- 또는 `--registry-mirror`, `--insecure-registry` 플래그 사용
## 관련 문서
- [[infra-hosts]] - K3s 클러스터 구성

133
infra/platform/outline.md Normal file
View File

@@ -0,0 +1,133 @@
---
title: Outline Wiki
updated: 2026-04-08
tags: [k3s, wiki, outline]
---
## 개요
Outline은 팀 위키/문서 관리 플랫폼. K3s 클러스터에 배포.
| 항목 | 값 |
|------|-----|
| URL | https://outline.inouter.com |
| 네임스페이스 | outline |
| 이미지 | `outlinewiki/outline:0.82.0` |
| 인증 | Gitea OAuth2 (OIDC) |
| 기본 언어 | 한국어 |
## 구성 요소
| 컴포넌트 | 설정 |
|-----------|------|
| DB | Patroni HA via OpenWrt HAProxy (`192.168.9.1:5432`), DB명: outline, 유저: outline |
| Redis | outline-redis (outline 네임스페이스 내 전용) |
| 파일 저장소 | 로컬 (Longhorn PVC 5Gi, `/var/lib/outline/data`) |
| TLS (Traefik) | wildcard-inouter-tls (*.inouter.com) |
| TLS (CDN) | Let's Encrypt via BunnyCDN |
| CDN | BunnyCDN iron-kr 존 (ID 5555227, 쿠키 허용) |
| DNS | outline.inouter.com CNAME → iron-kr.b-cdn.net (Cloudflare, proxied OFF) |
| Ingress | Traefik IngressRoute (CRD) |
## 인증 (Gitea OAuth2)
| 항목 | 값 |
|------|-----|
| OIDC Provider | Gitea (gitea.inouter.com) |
| Client ID | cb804835-416c-4730-86b4-26d2c129b164 |
| Redirect URI | https://outline.inouter.com/auth/oidc.callback |
| OIDC Endpoints | authorize, access_token, userinfo (Gitea 표준) |
| Client Secret | Vault `secret/apps/outline` 참조 |
## 시크릿
Kubernetes Secret: `outline-secrets` (outline 네임스페이스)
- SECRET_KEY, UTILS_SECRET, DATABASE_URL, REDIS_URL
- OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC 엔드포인트들
## pgcat 연동
현재 pgcat 미사용 (직접 PostgreSQL 접속). 필요 시 pgcat pool 추가 가능.
## MCP 연동
Outline MCP 서버 도입 시 헤임달이 직접 문서 CRUD 가능.
- 참고: https://github.com/Vortiago/mcp-outline
- heimdall `~/.claude.json` `/root` 프로젝트 mcpServers에 `outline` 항목 추가 (stdio, `uvx mcp-outline`, `OUTLINE_API_KEY`는 Vault `secret/apps/outline` brokkr-api-key, `OUTLINE_API_URL=https://outline.inouter.com`)
- 새 컬렉션 `agent-qna` (id `c3ab34ab-fae4-4642-8f4e-12728e293e1b`) — 에이전트 간 장문 Q&A 교환 공간 (kappa↔heimdall이 tmux로 짧게 질문 → 답변은 여기에 작성)
DB endpoint는 OpenWrt HAProxy(`192.168.9.1:5432`) 경유. 과거 Patroni failover 사고 이력: [[2026-04-08-patroni-failover-incident|history]]
## Discord 통지 파이프라인
`agent-qna` 컬렉션에 새 문서가 만들어지면 heimdall Discord 채널(#heimdall, id `1488119168145555486`)에 자동 알림이 뜬다. 다른 컬렉션은 무시한다.
```
Outline (documents.create webhook)
→ n8n webhook (https://n8n.inouter.com/webhook/outline-to-discord)
→ Code 노드 (collectionId 필터 + Discord embed 빌드)
→ HTTP Request 노드 (Discord Bot API: POST /channels/{id}/messages)
```
### 구성 요소 위치
| 항목 | 값 |
|------|-----|
| n8n workflow id | `8P714i5oBs9HkZPK` |
| n8n workflow 이름 | `outline-to-discord (heimdall)` |
| n8n webhook path | `/webhook/outline-to-discord` (POST, responseMode=onReceived) |
| n8n 노드 구성 | Webhook → Filter+Format(Code) → Discord POST(HTTP) |
| Outline webhook id | `ede9327f-b09e-4f7e-ab56-c507d3d1b3a6` (`documents.create` 이벤트) |
| Outline collection 필터 | `c3ab34ab-fae4-4642-8f4e-12728e293e1b` (agent-qna) — Code 노드 안에 하드코딩 |
| Discord channel | guild `1469999069190557799``#heimdall` 채널 (id `1488119168145555486`) |
| Discord bot | username `Heimdall` (id `1487694864232611922`) |
### 시크릿
| 비밀 | Vault 경로 |
|------|-----------|
| Discord bot token | `secret/apps/discord``bot_token` (현재는 n8n Code 노드에 하드코딩되어 있음 — Credentials로 분리하는 게 더 깔끔, 후속 작업) |
| Discord channel/guild ID | `secret/apps/discord``heimdall_channel_id`, `heimdall_guild_id` |
| n8n API key | `secret/apps/n8n``api_key` (label: `heimdall-automation`) |
| n8n owner login | `secret/apps/n8n``email`(`kappa@inouter.com`), `password` |
| Outline API token | `secret/apps/outline``brokkr-api-key` |
### 메시지 포맷 (Discord embed)
- title: `📝 <doc title>`
- url: `https://outline.inouter.com<payload.model.url>`
- description: 본문 앞 200자 (개행 → 공백)
- author.name: `outline / agent-qna · <createdBy.name>`
- color: `0x00bcd4` (cyan)
- timestamp: `payload.model.createdAt`
### 트러블슈팅
증상별 점검 순서:
1. **알림이 아예 안 옴**
- Outline UI 또는 API로 webhook subscription 활성 확인
```bash
curl -sS -X POST https://outline.inouter.com/api/webhookSubscriptions.list \
-H "Authorization: Bearer $OL_TOKEN" -d '{}' | jq .
```
- n8n에서 workflow active 여부 확인 (`/api/v1/workflows/8P714i5oBs9HkZPK`)
- n8n executions에 webhook 도달했는지 확인 (`/api/v1/executions?workflowId=8P714i5oBs9HkZPK&limit=10`)
2. **n8n에 도달하나 Discord POST가 안 일어남**
- 거의 항상 collectionId 필터 미스매치. Filter+Format Code 노드의 `out items: 0` 이면 정상 (필터된 것)
- 필터를 통과해야 하는데 0 items면 payload.event 또는 payload.model.collectionId 구조 확인
3. **n8n DB connection timeout**
- n8n pod 재시작: `kubectl -n n8n delete pod -l app.kubernetes.io/name=n8n`
- pgcat → 192.168.9.1:5432 (Patroni leader) 경로 확인
4. **Discord 401/403**
- bot token 재발급 필요 (`secret/apps/discord` 갱신, n8n Code 노드의 하드코딩도 함께 갱신)
- bot이 채널에 access 권한 있는지: `GET /channels/{channel_id}` 200이면 OK
5. **Outline → n8n 호출이 BunnyCDN 508**
- 일시적. 재시도하면 풀린다. BunnyCDN edge의 `cdn-loopcount` 룰 (자세한 원인 불명, retry로 회피)
### 후속 작업 (선택)
- [ ] Discord bot token을 n8n Credentials로 분리 (현재 Code 노드에 하드코딩)
- [ ] Outline signing secret 지원되면 HMAC 서명 검증 추가 (현재 미적용 — 같은 인프라 내부 호출이라 위험 낮음)
- [ ] `documents.update` 이벤트도 추가 (현재 create만)
- [ ] 다른 컬렉션 (예: incident postmortem)도 동일 패턴으로 통지 추가

View File

@@ -0,0 +1,187 @@
---
title: VictoriaLogs (K3s 로그 저장)
updated: 2026-04-08 Grafana 통합
tags: [k3s, logging, vector, observability, grafana]
---
# VictoriaLogs
K3s 클러스터의 raw 로그 저장소. 메트릭은 [[victoriametrics-stack|VictoriaMetrics]], 로그는 VictoriaLogs로 분리. 같은 VM 팀 제품이라 운영 패턴 일치.
## 접속
| 항목 | 값 |
|------|------|
| Web UI | https://vl.inouter.com/select/vmui/ |
| HTTP API | https://vl.inouter.com/ |
| 클러스터 내부 | http://vlogs-victoria-logs-single-server.logging.svc.cluster.local:9428 |
| 노출 방식 | LAN 직접 (Cloudflare DNS → k3s.inouter.com → MetalLB 192.168.9.53 → Traefik IngressRoute), BunnyCDN 우회 |
| TLS | wildcard-inouter-tls (reflector로 logging ns에 자동 복제) |
| 인증 | 없음 (LAN/Tailscale 안에서만 접근 가능) |
## 설치
```bash
helm repo add vm https://victoriametrics.github.io/helm-charts/
helm install vlogs vm/victoria-logs-single -n logging -f vlogs-values.yaml
```
values 핵심:
```yaml
server:
retentionPeriod: 14d
persistentVolume:
enabled: true
storageClassName: longhorn
size: 50Gi
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 1000m, memory: 1Gi }
```
- 단일 노드 (`victoria-logs-single`), 14일 보존, longhorn 50GiB PVC
- 네임스페이스: `logging` (Vector와 같은 곳)
## Traefik IngressRoute
`logging` ns:
```yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: vlogs
namespace: logging
spec:
entryPoints: [websecure]
routes:
- match: Host(`vl.inouter.com`)
kind: Rule
services:
- name: vlogs-victoria-logs-single-server
port: 9428
tls:
secretName: wildcard-inouter-tls
```
## 데이터 파이프라인
### APISIX 로그 (현재 가동 중)
```
APISIX (apisix ns, 2 replica) stdout (nginx access log 형식)
→ Vector daemonset (logging ns, 3 노드)
- source: apisix_logs (kubernetes_logs, label=app.kubernetes.io/name=apisix)
- transform: parse_apisix (정규식 파싱 → method/path/status/request_time/host/UA 등 구조화)
- sink: vlogs (elasticsearch bulk API)
→ VictoriaLogs (/insert/elasticsearch/_bulk)
```
Vector helm values는 [[vector|vector]] 문서 또는 `helm get values vector -n logging` 참조.
### 다른 잠재 source
- Traefik 로그는 별도 sink로 [[crowdsec-safeline|CrowdSec]]에 전송 (vector의 다른 transform/sink). VictoriaLogs로는 안 보내고 있음. 필요하면 sink inputs에 `parse_traefik` 추가하면 됨.
## Grafana 통합
vm-stack의 Grafana(`monitoring/vm-stack-grafana`)에 등록되어 있음. Explore에서 data source를 `VictoriaLogs`로 선택하면 LogsQL 쿼리 가능.
### 등록 방법
두 단계로 구성:
1. **플러그인 설치** — vm-stack helm values의 `grafana.plugins``victoriametrics-logs-datasource` 추가하고 `helm upgrade vm-stack`. Grafana 시작 시 자동 다운로드/설치 (현재 v0.26.3).
2. **datasource 등록**`grafana.additionalDataSources`는 vm-stack chart 구조상 sub-chart로 전달이 안 되므로 무시됨. 대신 **sidecar ConfigMap 패턴** 사용:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vlogs-grafana-datasource
namespace: monitoring
labels:
grafana_datasource: "1" # ← grafana-sc-datasources sidecar가 자동 발견
data:
vlogs.yaml: |
apiVersion: 1
datasources:
- name: VictoriaLogs
type: victoriametrics-logs-datasource
access: proxy
url: http://vlogs-victoria-logs-single-server.logging.svc.cluster.local:9428
isDefault: false
editable: true
```
`kubectl apply -f` 한 번이면 sidecar가 즉시 picks up하고 grafana가 hot reload (`Writing /etc/grafana/provisioning/datasources/vlogs.yaml`).
### 주의
- Grafana PVC가 ReadWriteOnce이므로 helm upgrade 시 새 pod이 다른 노드에 스케줄되면 Multi-Attach error → 기존 pod 수동 삭제 필요. (또는 Grafana Deployment의 strategy를 Recreate로 변경)
- helm values의 `grafana.additionalDataSources`는 무시되지만 plugin 설치는 작동하므로, plugin 라인은 helm values에 두는 게 맞음.
## LogsQL 쿼리 예시
```logsql
# 모든 APISIX 로그
program:apisix
# 5xx 에러만
program:apisix status:>=500
# 특정 path
program:apisix path:/apisix/admin/routes
# 느린 요청 (1초 이상)
program:apisix request_time:>1.0
# 특정 클라이언트 외부 IP (real_ip 적용 후 진짜 IP)
program:apisix remote_addr:211.211.28.97
# 특정 호스트
program:apisix host:juiceshop.keepanker.cv
# 특정 호스트의 4xx/5xx
program:apisix host:juiceshop.keepanker.cv status:>=400
# 특정 노드 pod
program:apisix _stream:{kubernetes.pod_name="apisix-f5764d796-kdj2x"}
```
> [!info] 진짜 IP 추적
> 2026-04-08 [[apisix#real_ip 설정 (2026-04-08 patch 반영)|APISIX real_ip patch]] 이후 `remote_addr`에 BunnyCDN edge가 전달한 진짜 외부 IP가 찍힘. `xff`/`xrip` 필드로 X-Forwarded-For/X-Real-IP 헤더 원본도 보존. 그 이전 로그는 K3s pod CIDR(10.42.x.x)만 찍혀 있음.
> [!warning] parse_apisix 정규식과 APISIX log format은 짝
> APISIX의 `access_log_format`을 바꾸면 vector helm values의 `parse_apisix` 정규식도 같이 업데이트해야 함. 안 그러면 매치 실패해서 `log_type=raw`로 떨어지고 구조화 필드(method/status/path 등) 추출이 안 됨.
UI에서 직접 쿼리하거나 HTTP API:
```bash
curl -s "https://vl.inouter.com/select/logsql/query?query=program:apisix+status:>=500&limit=20"
```
## 운영 명령
```bash
# 메트릭 (ingest 통계)
curl -s https://vl.inouter.com/metrics | grep vl_rows_ingested_total
# 디스크 사용량
kubectl exec -n logging vlogs-victoria-logs-single-server-0 -- du -sh /storage
# Vector 상태
kubectl logs -n logging -l app.kubernetes.io/name=vector --tail=50
# Vector configmap 직접 보기
kubectl get cm vector -n logging -o jsonpath='{.data.vector\.yaml}'
```
## 향후 작업
- [x] Grafana data source 추가 (2026-04-08 완료, 위 "Grafana 통합" 섹션 참조)
- [ ] CrowdSec와 통합 (APISIX 로그 → CrowdSec 시나리오)
- [ ] AI 분석 컴포넌트 (CronJob 또는 streaming, 새 공격 패턴 자동 차단 룰 생성)
- [ ] Traefik 로그도 VL로 동시 sink (현재는 CrowdSec only)