문서 최신화: Traefik 와일드카드 실운영 / TenantGuard / K8s Secret 주의

This commit is contained in:
2026-04-24 20:29:53 +09:00
parent 6d4f486e35
commit 06998cd2a5
+80 -33
View File
@@ -114,32 +114,46 @@ invyone (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이
서브도메인 기반 멀티테넌시는 **환경마다 DNS 해결 방식이 달라야** 한다. 이 절이 이 문서 전체에서 가장 중요.
### 4.1 운영 (프로덕션)
### 4.1 운영 (프로덕션) — **2026-04-24 실운영 중**
**와일드카드 DNS + Nginx 리버스 프록시.** 한 번 세팅하면 회사 무한히 추가해도 추가 작업 0.
**와일드카드 DNS (Porkbun) + Traefik v2.11 리버스 프록시 + Let's Encrypt DNS-01 와일드카드 TLS.**
한 번 세팅하면 회사 무한히 추가해도 추가 작업 0.
```
[DNS Provider]
A *.invyone.com → 운영서버공인IP
[DNS Provider — Porkbun]
A *.invyone.com → 183.99.177.40 (운영서버 공인IP)
A solution.invyone.com → 183.99.177.40 (메인 사이트, 명시적)
[Nginx conf]
server {
listen 443 ssl;
server_name *.invyone.com;
ssl_certificate .../fullchain.pem; # Let's Encrypt wildcard
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host; # ★ Host 헤더 보존 필수
}
location /api/ {
proxy_pass http://backend:8081;
proxy_set_header Host $host; # ★
}
}
[Traefik v2.11 — 운영서버 /opt/docker/traefik/docker-compose.yml]
- HTTP (80) → HTTPS 자동 redirect
- HTTPS (443) → K8s NodePort 로 전달 (HostRegexp 로 *.invyone.com 매칭)
- certResolvers:
le — HTTP-01 challenge (개별 도메인용, solution 등)
ledns — DNS-01 challenge via Porkbun API (와일드카드 *.invyone.com)
[Traefik dynamic config — /opt/docker/traefik/dynamic/invyone-tenant.yml]
HostRegexp(`{sub:[a-z0-9-]+}.invyone.com`)
→ service invyone-tenant-frontend (host.docker.internal:30000)
HostRegexp(...) && PathPrefix(`/api`)
→ service invyone-tenant-api (host.docker.internal:30081)
tls.domains.sans: "*.invyone.com" # 와일드카드 인증서 발급 조건
[Porkbun API Key]
서버의 /opt/docker/traefik/.env 에 보관 (git 에 없음)
PORKBUN_API_KEY / PORKBUN_SECRET_API_KEY
```
- **Let's Encrypt 와일드카드 인증서**: `certbot certonly --manual --preferred-challenges=dns -d invyone.com -d '*.invyone.com'`
- 이후 사용자 `http://qnc.invyone.com` 접속 → Nginx 가 Host 헤더 보존해 backend 프록시 → SubdomainResolverFilter → 자동 라우팅.
- 사용자 `https://qnc.invyone.com` 접속 → Porkbun DNS 풀림 → Traefik HostRegexp 매칭
Host 헤더 보존해 backend 프록시 → SubdomainResolverFilter → 자동 라우팅.
- `solution.invyone.com` 은 Host() 명시 라우터로 Traefik priority 높아 와일드카드 위에 탐.
- Traefik 은 **K8s 바깥에 도커로 별도 실행** (`.gitea/workflows/deploy.yml` 과 무관 — 설정 변경은 서버에서 수동 + `docker compose restart traefik`)
#### 재현 스냅샷 (2026-04-24)
- **DNS 와일드카드**: Porkbun 콘솔에서 `A * 183.99.177.40` 한 줄 추가 (전파 즉시)
- **Traefik dynamic 라우트**: `notes/gbpark/2026-04-24-traefik-wildcard/invyone-tenant.yml` 참고
- **Traefik static + env**: `notes/gbpark/2026-04-24-traefik-wildcard/docker-compose.yml` 참고 (Porkbun provider + environment 블록 포함)
- **Let's Encrypt acme-dns.json**: 서버 `/opt/docker/traefik/letsencrypt/` 에 저장 (90일 자동 갱신)
### 4.2 로컬 개발 (본인 머신에서 docker up)
@@ -178,9 +192,9 @@ invyone (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이
| 환경 | 접속 방식 | 설정 필요도 |
|---|---|---|
| **운영** | 와일드카드 DNS + Nginx + Let's Encrypt wildcard TLS | 최초 1회, 이후 0 |
| **운영** | Porkbun 와일드카드 DNS + Traefik v2.11 + Let's Encrypt DNS-01 wildcard | **완료 (2026-04-24)** |
| **로컬 (직접 up)** | `*.localhost` 자동 | 0 |
| **원격 공유 개발 서버** | `nip.io` (추천) / hosts 편집 / 개발서버 Nginx | 경우 따라 |
| **원격 공유 개발 서버** | `nip.io` (추천) / hosts 편집 | 경우 따라 |
---
@@ -222,29 +236,61 @@ cors:
`frontend/lib/api/client.ts` 의 분기 **순서가 중요**:
```typescript
// 1) 테넌트 서브도메인 → 직접 백엔드 포트 (Next rewrite 우회)
// 1) 테넌트 서브도메인 → 동일 호스트의 /api (Traefik 이 프록시)
// Host 헤더 보존 + https mixed-content 회피.
if (currentHost.endsWith(".invyone.com")) {
return `http://${currentHost}:8083/api`;
return `https://${currentHost}/api`;
}
// 2) 프로덕션 메인 도메인
// 2) (레거시) invion.com 메인 도메인
if (currentHost === "v1.invion.com") return "https://api.invion.com/api";
// 3) NEXT_PUBLIC_API_URL (docker-compose 주입)
// 4) localhost 기본값
```
> ★ NEXT_PUBLIC_API_URL=/api 같은 rewrite 방식은 **Host 헤더를 변조**해 서브도메인 파싱이 실패한다. 테넌트 도메인은 반드시 직접 포트로.
> ★ NEXT_PUBLIC_API_URL=/api 같은 Next rewrite **Host 헤더를 변조**해 서브도메인 파싱이 실패한다. 테넌트 도메인은 위 1) 분기로 처리해 브라우저가 직접 `https://<host>/api` 로 Traefik 에 요청 → Traefik 이 Host 보존하며 backend 로 프록시.
---
## 7. 운영 배포 체크리스트
## 6.1 TenantGuard — 미등록 서브도메인 차단
Git push 한 번으로 자동 배포되는 구조라도, 아래 3가지는 **Git 바깥**이라 수동으로 확인해야 한다:
프론트 레이아웃에 `<TenantGuard>` 를 wrap. 페이지 마운트 시 호스트 분석 → 예약어(solution/www/admin 등) 제외 → 백엔드 `/api/tenant/check?subdomain=xxx` 호출 → `exists: false` 면 `/tenant-not-found` 로 replace.
- [ ] `.env` 의 `CORS_ALLOWED_ORIGINS` 에 **`*.invyone.com:[*]` 패턴 포함**
- [ ] DB 마이그레이션 `RUN_079`, `RUN_080`, `RUN_081` 운영 DB 에서 **1회 실행**
- [ ] Docker 이미지 재빌드 (Dockerfile 의 `postgresql16-client` 반영) — `docker compose build --no-cache backend-spring`
- [ ] `tenant.provisioning.require-super-admin=true` (프로덕션에선 필수, 개발은 false)
- [ ] (최초 1회) 와일드카드 DNS A 레코드 + Nginx wildcard server_name + Let's Encrypt wildcard 인증서
| 파일 | 역할 |
|---|---|
| `backend-spring/.../tenant/TenantController.java` | `GET /api/tenant/check` 퍼블릭 API (메타 DB 강제 라우팅) |
| `frontend/lib/tenant/subdomain.ts` | `extractTenantSubdomain(host)` — 예약어면 null |
| `frontend/components/TenantGuard.tsx` | client-side guard, sessionStorage 로 재체크 방지 |
| `frontend/app/tenant-not-found/page.tsx` | 미등록 회사 에러 페이지 (v5 solid+glow) |
| `frontend/app/layout.tsx` | `<TenantGuard>{children}</TenantGuard>` |
예약어 리스트는 백엔드의 `ProvisioningController.RESERVED_SUBDOMAINS` 와 **같은 값으로 유지**해야 함.
---
## 7. 운영 배포 체크리스트 (2026-04-24 완료 상태)
Git push 한 번으로 자동 배포되는 구조지만, 아래는 **Git 바깥**이라 신규 환경 붙일 때 수동 작업:
- [x] `CORS_ALLOWED_ORIGINS` `k8s/configmap.yaml` 에 명시 (`*.invyone.com` 패턴 포함)
- [x] DB 마이그레이션 `RUN_079/080/081` 운영 DB 에 1회 실행 완료
- [x] `docker/deploy/backend-spring.Dockerfile` 에 `postgresql16-client` 포함
- [x] `TENANT_PROVISIONING_REQUIRE_SUPER_ADMIN=true` (configmap + deployment env wiring)
- [x] Porkbun 와일드카드 DNS A 레코드 (`*.invyone.com → 183.99.177.40`)
- [x] Traefik dynamic config `*.invyone.com` 라우트 (서버 `/opt/docker/traefik/dynamic/invyone-tenant.yml`)
- [x] Porkbun API 키 서버 `/opt/docker/traefik/.env` 에 박음
- [x] Let's Encrypt DNS-01 와일드카드 인증서 발급 (Apr 24 ~ Jul 23, 자동 갱신)
- [x] 프론트 `TenantGuard` + `/tenant-not-found` 페이지
### ⚠️ K8s Secret 수동 갱신 포인트
`.gitea/workflows/deploy.yml` 은 `k8s/secrets-real.yaml` 을 **apply 하지 않는다** (namespace/configmap/pvc/backend/frontend/networkpolicy 만). Secret 내용(DB 비번 등) 이 바뀌면 서버에서 1회 수동 적용:
```bash
kubectl apply -f /path/to/secrets-real.yaml -n invyone
kubectl rollout restart deployment backend-spring -n invyone
```
DB 비번 회전 시 **반드시** Secret 도 같이 갱신해야 pod 이 새 비번으로 재연결.
---
@@ -264,6 +310,7 @@ Git push 한 번으로 자동 배포되는 구조라도, 아래 3가지는 **Git
- `notes/gbpark/2026-04-24-company-db-provisioning-execution-plan.md` — 초기 설계 + Codex 리뷰 반영 + 실행 체크리스트
- `notes/gbpark/2026-04-24-company-mgmt-ui-schema.md` — 회사관리 UI 필드 ↔ COMPANY_MNG 컬럼 ↔ derived 집계 매핑
- `notes/gbpark/2026-04-24-traefik-wildcard/` — 운영 Traefik 와일드카드 설정 산출물 (docker-compose.yml, invyone-tenant.yml, db-rename.sh)
---