agent-qna 컬렉션의 documents.create 이벤트가 n8n webhook을 거쳐 heimdall Discord 채널에 embed 알림으로 전달되도록 구축. - Outline webhook subscription: ede9327f-b09e-4f7e-ab56-c507d3d1b3a6 - n8n workflow id: 8P714i5oBs9HkZPK - 컬렉션 필터 + Discord embed 빌드는 n8n Code 노드 안에 하드코딩 - secret/apps/discord 신설 (bot_token, channel/guild id) - secret/apps/n8n 의 email/api_key 갱신 (kappa@inouter.com) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
7.4 KiB
Markdown
150 lines
7.4 KiB
Markdown
---
|
|
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로 짧게 질문 → 답변은 여기에 작성)
|
|
|
|
## 2026-04-08 사고: DATABASE_URL이 Patroni replica 직결
|
|
|
|
`outline-secrets` Secret의 `DATABASE_URL`이 Patroni 노드 IP `10.100.2.5`(postgres-1)를 직접 가리키도록 설정되어 있었음. Patroni failover로 postgres-1이 replica로 강등되자 Outline의 모든 write 쿼리가 `cannot execute UPDATE in a read-only transaction` 에러로 실패. API는 500을 반환(`apiKeys.lastActiveAt` UPDATE 실패 → 인증 자체가 깨짐).
|
|
|
|
**복구**: `DATABASE_URL`을 OpenWrt HAProxy 경유로 변경.
|
|
|
|
```bash
|
|
kubectl patch secret outline-secrets -n outline --type=json \
|
|
-p='[{"op":"replace","path":"/data/DATABASE_URL","value":"<base64 of postgresql://outline:outline@192.168.9.1:5432/outline>"}]'
|
|
kubectl rollout restart deployment/outline -n outline
|
|
# RWO PVC 때문에 old pod를 수동 삭제해야 새 pod attach 가능
|
|
kubectl delete pod -n outline <old-pod>
|
|
```
|
|
|
|
**원인**: 이 Secret은 nocodb/n8n/pgcat 변경(2026-04-08 [[postgresql-ha#2026-04-08 사고 기록]])과 함께 일괄 마이그레이션되지 않았음. **나머지 Patroni 사용자도 동일 검증 필요** — `kubectl get secret -A -o json | jq -r '.items[] | select(.data.DATABASE_URL) | "\(.metadata.namespace)/\(.metadata.name)"'`로 검수 권장.
|
|
|
|
**교훈**: Patroni 사용 애플리케이션의 DB endpoint는 항상 OpenWrt HAProxy(`192.168.9.1:5432`) 또는 pgcat(`db.svc.cluster.local:6432`)을 통해야 한다. 노드 IP 직접 박지 말 것.
|
|
|
|
## Discord 통지 파이프라인 (2026-04-08 구축)
|
|
|
|
`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) 경로 확인. 2026-04-08에 stale connection으로 15h 멈춰있던 사례 있음
|
|
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)도 동일 패턴으로 통지 추가
|