Files
nginx-portal/README.md
qorgh529 72689d8647
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
docs: 2026-05 허브 홈페이지 구축 및 URL 구조 개편 내용 추가
2026-06-10 18:15:14 +09:00

956 lines
33 KiB
Markdown
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Web Portal - Kubernetes + GitOps 배포 가이드
## 📋 프로젝트 개요
조직 내부 웹페이지를 통합 관리하는 포털 시스템입니다.
로그인 후 본인에게 할당된 웹페이지 목록을 확인하고 접속할 수 있으며,
관리자는 페이지 및 사용자 권한을 관리할 수 있습니다.
---
## 🏗️ 전체 아키텍처
```
사용자 (외부 인터넷)
├── https://cyanburu.com → Hub 홈페이지 (포트폴리오/서비스 허브)
├── https://cyanburu.com/portal → Web Portal
├── https://cyanburu.com/kingscup → King's Cup 게임 (개발 예정)
├── https://gitea.cyanburu.com → Gitea
└── https://argo.cyanburu.com → ArgoCD
MSI 라우터 (포트포워딩 80/443)
Nginx Ingress Controller ← TLS 종료, 도메인/경로별 라우팅
cert-manager ← Let's Encrypt 인증서 자동 발급/갱신
Kubernetes 네임스페이스별 서비스
├── hub
│ └── Nginx (정적 HTML) ← cyanburu.com/ 허브 홈페이지
├── web-portal
│ ├── Nginx Frontend (ClusterIP: 80)
│ ├── FastAPI Backend (ClusterIP: 8000)
│ └── PostgreSQL DB (ClusterIP: 5432)
├── gitea
│ └── Gitea (ClusterIP: 3000)
└── argocd
└── ArgoCD Server (ClusterIP: 443)
개발자 (git push)
Gitea → ArgoCD 자동 감지 & 배포
```
---
## 🛠️ 기술 스택
| 구분 | 기술 |
|------|------|
| Frontend | Nginx + HTML/CSS/JS (SPA) |
| Backend | Python FastAPI |
| Database | PostgreSQL |
| Cache | Redis (King's Cup 세션) |
| Container | Docker Desktop |
| Orchestration | Kubernetes (Docker Desktop 내장) |
| GitOps | Gitea + ArgoCD |
| Image Registry | Gitea Container Registry |
| Ingress | Nginx Ingress Controller |
| TLS | cert-manager + Let's Encrypt |
| Domain | cyanburu.com (후이즈) |
| 서브도메인 | gitea.cyanburu.com, argo.cyanburu.com |
---
## 📁 프로젝트 구조
```
nginx-portal/
├── .gitea/
│ └── workflows/
│ └── build-and-push.yaml # Gitea Actions CI (선택사항)
├── backend/
│ ├── main.py # FastAPI 전체 API 로직
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── index.html # 싱글 페이지 앱 (SPA)
│ ├── nginx.conf # Nginx 설정 + /api/* 프록시 (/portal subpath 대응)
│ └── Dockerfile
├── hub/ # 허브 홈페이지 (cyanburu.com/)
│ ├── index.html # 포트폴리오/서비스 허브 정적 페이지
│ └── Dockerfile
├── k8s/
│ ├── 00-hub.yaml # hub 네임스페이스 + Deployment + Service
│ ├── 01-namespace.yaml # web-portal 네임스페이스
│ ├── 02-postgres.yaml # PostgreSQL + PVC + Service
│ ├── 03-secrets.yaml # DB/JWT 시크릿
│ ├── 04-backend.yaml # FastAPI Deployment + Service
│ ├── 05-frontend.yaml # Nginx Deployment + NodePort(30090)
│ ├── 07-clusterissuer.yaml # Let's Encrypt ClusterIssuer
│ ├── 08-ingress.yaml # Web Portal Ingress (cyanburu.com/portal)
│ ├── 09-ingress-gitea.yaml # Gitea Ingress (gitea.cyanburu.com)
│ ├── 10-ingress-argocd.yaml # ArgoCD Ingress (argo.cyanburu.com)
│ ├── 11-notify-secrets.yaml # Discord Webhook / Gmail Secret
│ ├── 12-???.yaml # 기존 파일
│ └── 13-ingress-hub.yaml # Hub Ingress (cyanburu.com/)
├── 06-argocd-app.yaml # ArgoCD Application 정의 (k8s 폴더 밖에 위치)
└── README.md
```
> ⚠️ `06-argocd-app.yaml` 은 반드시 `k8s/` 폴더 **밖**에 위치해야 합니다.
> ArgoCD가 `k8s/` 폴더를 감시하므로 해당 파일이 안에 있으면 순환 참조 문제가 발생합니다.
---
## 🔑 기본 계정
| 구분 | ID | Password |
|------|-----|----------|
| 관리자 | `admin` | `admin1234` |
| 일반사용자 | `user1` | `user1234` |
---
## ✨ 기능 설명
### 일반 사용자
- 로그인 후 **MY Page List** 에서 본인에게 할당된 웹페이지를 카드 형태로 확인
- 카드의 Favicon 자동 표시 (없을 경우 기본 아이콘)
- 카드 클릭 시 **새 탭에서 해당 URL로 이동**
- **공지사항** 탭에서 관리자가 등록한 공지 확인 및 댓글 작성
- **관리자 요청** 탭에서 게시글 작성 및 답글 작성
- 로그인 비밀번호 표시/숨김 토글 버튼
- 로그인 실패 시 아이디 유지 (비밀번호만 초기화)
- **비밀번호 변경** 메뉴 (헤더에서 언제든 변경 가능)
- **최초 로그인 시 비밀번호 강제 변경** (변경 전까지 서비스 이용 불가)
- **비밀번호 5회 오류 시 계정 자동 잠금** → 관리자에게 잠금 해제 요청 필요
### 관리자
일반 사용자 기능 + 추가:
- **페이지 관리**: 웹페이지 추가 / 수정 / 삭제
- **사용자 관리**: 계정 생성 / 삭제
- **권한 설정**: 사용자별 접근 가능 페이지를 체크박스로 지정
- **비밀번호 변경**: 특정 사용자의 비밀번호 직접 변경 (변경 후 해당 사용자 강제 변경 적용)
- **임시 비밀번호 발급**: 랜덤 임시 비밀번호 자동 생성 후 화면에 표시
- **계정 잠금 해제**: 잠긴 계정을 버튼 하나로 해제
- **사용자 상태 확인**: 정상 / 🔒잠김 / 초기PW / 변경요청 태그로 한눈에 확인
- **공지사항 작성**: 공지 탭에서 전체 사용자에게 공지 등록 / 삭제
- **🏠 홈 카드 관리**: `cyanburu.com` 메인 허브 홈페이지에 표시될 카드 추가 / 수정 / 삭제 / 순서 변경
### 게시판 (공지 / 관리자 요청)
| 구분 | 공지 | 관리자 요청 |
|------|------|------------|
| 글 작성 | 관리자만 가능 | 모든 사용자 가능 |
| 댓글/답글 | 모든 사용자 가능 | 모든 사용자 가능 |
| 글 삭제 | 관리자만 가능 | 본인 또는 관리자 |
| 댓글 삭제 | 본인 또는 관리자 | 본인 또는 관리자 |
| 목록 항목 | 번호, 제목, 작성자, 작성일(시간포함) | 번호, 제목, 작성자, 작성일(시간포함) |
---
## 🚀 최초 배포 순서
### 1단계. Docker Desktop insecure-registry 설정
Gitea Registry가 HTTP이므로 Docker Desktop에서 허용 설정 필요.
Docker Desktop → Settings → Docker Engine:
```json
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"insecure-registries": ["192.168.10.101:30000"]
}
```
**Apply & Restart** 클릭
### 2단계. Gitea Registry 로그인 및 이미지 빌드 & Push
```bash
# Registry 로그인
docker login 192.168.10.101:30000 -u <Gitea계정>
# 백엔드 이미지 빌드 & Push
docker build -t 192.168.10.101:30000/<계정>/portal-backend:latest ./backend/
docker push 192.168.10.101:30000/<계정>/portal-backend:latest
# 프론트엔드 이미지 빌드 & Push
docker build -t 192.168.10.101:30000/<계정>/portal-frontend:latest ./frontend/
docker push 192.168.10.101:30000/<계정>/portal-frontend:latest
```
### 3단계. K8s Registry Secret 생성
```bash
kubectl create namespace web-portal
kubectl create secret docker-registry gitea-registry-secret \
--namespace=web-portal \
--docker-server=gitea-http.gitea.svc.cluster.local:3000 \
--docker-username=<Gitea계정> \
--docker-password=<Gitea패스워드>
```
### 4단계. ArgoCD에 Gitea 저장소 인증 등록
```bash
kubectl create secret generic gitea-repo-secret \
--namespace=argocd \
--from-literal=type=git \
--from-literal=url=http://192.168.10.101:30000/<계정>/nginx-portal.git \
--from-literal=username=<Gitea계정> \
--from-literal=password=<Gitea패스워드>
kubectl label secret gitea-repo-secret \
-n argocd \
argocd.argoproj.io/secret-type=repository
```
### 5단계. ArgoCD Application 등록
```bash
kubectl apply -f 06-argocd-app.yaml
```
### 6단계. Nginx Ingress Controller 설치 (최초 1회)
```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml
kubectl get pods -n ingress-nginx
```
### 7단계. cert-manager 설치 (최초 1회)
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml
kubectl get pods -n cert-manager
```
### 8단계. CoreDNS 내부 도메인 등록 (헤어핀 NAT 우회)
```bash
kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
kubectl rollout restart deployment/coredns -n kube-system
```
> `coredns-patch.yaml` 내용:
> ```yaml
> data:
> Corefile: |
> cyanburu.com {
> hosts {
> 192.168.10.101 cyanburu.com
> fallthrough
> }
> cache 30
> }
> .:53 {
> ... (기존 내용 유지)
> }
> ```
### 9단계. 라우터 포트포워딩 설정
MSI 라우터에서 설정:
| 공용 포트 | 내부 IP | 비공개 포트 |
|-----------|---------|-------------|
| 80 | 192.168.10.101 | 80 |
| 443 | 192.168.10.101 | 443 |
### 10단계. Ingress + ClusterIssuer 배포
```bash
git add k8s/07-clusterissuer.yaml k8s/08-ingress.yaml k8s/05-frontend.yaml
git commit -m "feat: Ingress + cert-manager HTTPS 설정"
git push origin main
# ArgoCD가 자동 배포
```
### 11단계. 인증서 발급 확인
```bash
kubectl get certificate -n web-portal
# READY: True 확인
```
### 12단계. 서브도메인 Ingress 적용 (Gitea, ArgoCD)
```bash
kubectl apply -f 09-ingress-gitea.yaml
kubectl apply -f 10-ingress-argocd.yaml
# 인증서 발급 확인
kubectl get certificate -n gitea
kubectl get certificate -n argocd
```
### 13단계. 접속 확인
| 서비스 | URL |
|--------|-----|
| Hub 홈페이지 | `https://cyanburu.com` |
| Web Portal | `https://cyanburu.com/portal` |
| Gitea | `https://gitea.cyanburu.com` |
| ArgoCD | `https://argo.cyanburu.com` |
### 14단계. Hub 홈페이지 배포 (최초 1회)
```bash
# hub 네임스페이스 생성
kubectl create namespace hub
# Registry Secret 생성
kubectl create secret docker-registry gitea-registry-secret \
--namespace=hub \
--docker-server=192.168.10.101:30000 \
--docker-username=<계정> \
--docker-password=<패스워드>
# 이미지 빌드 & Push
docker build -t 192.168.10.101:30000/<계정>/hub:latest ./hub/
docker push 192.168.10.101:30000/<계정>/hub:latest
# 배포
kubectl apply -f k8s/00-hub.yaml
kubectl apply -f k8s/13-ingress-hub.yaml
```
---
## 🔄 이후 배포 방법 (코드 수정 시)
### yaml만 변경한 경우 (설정 변경)
```bash
git add .
git commit -m "fix: 변경내용"
git push origin main
# → ArgoCD가 자동으로 감지해서 재배포
```
### 이미지도 변경한 경우 (코드 변경)
```bash
# 이미지 재빌드 & Push
docker build -t 192.168.10.101:30000/<계정>/portal-backend:latest ./backend/
docker push 192.168.10.101:30000/<계정>/portal-backend:latest
# Pod 재시작
kubectl rollout restart deployment/backend -n web-portal
# yaml도 변경했다면
git add .
git commit -m "feat: 변경내용"
git push origin main
```
---
## 🔧 운영 명령어
```bash
# 전체 리소스 상태 확인
kubectl get all -n web-portal
# 백엔드 로그 확인
kubectl logs -n web-portal deployment/backend -f
# 프론트엔드 로그 확인
kubectl logs -n web-portal deployment/frontend -f
# Pod 재시작
kubectl rollout restart deployment/backend -n web-portal
kubectl rollout restart deployment/frontend -n web-portal
# 전체 삭제
kubectl delete namespace web-portal
```
---
## 🔐 운영 시 보안 설정
### K8s Secret 변경
`k8s/03-secrets.yaml` 에서 반드시 변경:
```yaml
stringData:
db-password: "강력한패스워드로변경"
jwt-secret: "64자이상의랜덤문자열로변경"
```
### Nginx Rate Limiting (Brute Force 방어)
`frontend/nginx.conf` 상단에 추가:
```nginx
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
```
로그인 location에 적용:
```nginx
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend-service.web-portal.svc.cluster.local:8000/api/auth/login;
...
}
```
같은 IP에서 분당 5회 초과 시 429 응답 반환. 계정 잠금(5회 실패)과 함께 이중으로 Brute Force를 방어합니다.
### 추가 보안 권고사항
- **Fail2ban**: Nginx 로그 감시 후 반복 실패 IP를 방화벽으로 자동 차단 (온프레미스 환경 권장)
- **CAPTCHA**: 로그인 3회 실패 시 Google reCAPTCHA 표시 (추가 개발 필요)
- **JWT 만료 시간 단축**: `main.py` 에서 `timedelta(hours=8)``timedelta(hours=2)` 변경 가능
### HTTPS / TLS
- cert-manager가 Let's Encrypt 인증서를 **자동으로 갱신** (만료 30일 전)
- HTTP 접속 시 자동으로 HTTPS로 리다이렉트 (`ssl-redirect: "true"`)
- 인증서 상태 확인:
```bash
kubectl get certificate -n web-portal # cyanburu.com
kubectl get certificate -n gitea # gitea.cyanburu.com
kubectl get certificate -n argocd # argo.cyanburu.com
```
---
## ❗ 트러블슈팅
### 1. NodePort 포트 충돌
**증상**
```
Service "frontend-service" is invalid: spec.ports[0].nodePort:
Invalid value: 30080: provided port is already allocated
```
**원인** 해당 NodePort가 다른 서비스에서 이미 사용 중.
**해결**
`k8s/05-frontend.yaml` 에서 nodePort를 다른 번호로 변경 (30000~32767 범위):
```yaml
nodePort: 30090
```
변경 후 기존 서비스 삭제 및 재적용:
```bash
kubectl delete service frontend-service -n web-portal
kubectl apply -f k8s/05-frontend.yaml
```
---
### 2. Backend Liveness Probe 실패로 인한 CrashLoopBackOff
**증상**
```
Liveness probe failed: HTTP probe failed with statuscode: 404
Back-off restarting failed container
```
**원인** Backend가 DB 연결을 기다리는 동안 liveness probe가 먼저 실패해서 K8s가 강제 재시작.
**해결**
`k8s/04-backend.yaml` 의 probe 대기 시간 증가:
```yaml
readinessProbe:
initialDelaySeconds: 20
periodSeconds: 5
failureThreshold: 6
livenessProbe:
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 5
```
---
### 3. Nginx가 API 요청을 백엔드로 프록시 못함
**증상** 로그인 시 `Unexpected token '<', "<html>..." is not valid JSON` 에러.
**원인** Nginx가 `/api/` 요청을 백엔드로 전달하지 못하고 HTML을 반환.
**해결**
`frontend/nginx.conf` 에서 백엔드 주소를 FQDN으로 변경:
```nginx
location /api/ {
proxy_pass http://backend-service.web-portal.svc.cluster.local:8000/api/;
}
```
---
### 4. ArgoCD - authentication required: Unauthorized
**증상**
```
failed to list refs: authentication required: Unauthorized
```
**원인** ArgoCD가 Gitea 저장소에 접근할 인증 정보가 없음.
**해결**
kubectl로 직접 인증 Secret 생성:
```bash
kubectl create secret generic gitea-repo-secret \
--namespace=argocd \
--from-literal=type=git \
--from-literal=url=http://192.168.10.101:30000/<계정>/nginx-portal.git \
--from-literal=username=<계정> \
--from-literal=password=<패스워드>
kubectl label secret gitea-repo-secret \
-n argocd \
argocd.argoproj.io/secret-type=repository
```
---
### 5. RepeatedResourceWarning - 리소스 중복
**증상**
```
Resource apps/Deployment/web-portal/backend appeared 2 times among application resources
```
**원인** `k8s/` 폴더 안에 동일한 리소스를 정의하는 yaml 파일이 중복 존재 (`portal.yaml` 등).
**해결**
중복 파일 삭제 후 push:
```bash
rm k8s/portal.yaml
git add .
git commit -m "fix: 중복 yaml 파일 제거"
git push origin main
```
---
### 6. ImagePullBackOff - Gitea Registry HTTP 접근 오류
**증상**
```
Failed to pull image: server gave HTTP response to HTTPS client
```
**원인** Gitea Registry가 HTTP인데 Docker가 HTTPS로 접근 시도.
**해결**
Docker Desktop → Settings → Docker Engine에 insecure-registry 추가:
```json
{
"insecure-registries": ["192.168.10.101:30000"]
}
```
Apply & Restart 후 이미지 재빌드 & Push.
---
### 7. Gitea Container Registry 로그인 실패 (context deadline exceeded)
**증상**
```
Error response from daemon: Get "http://192.168.10.101:30000/v2/":
context deadline exceeded
```
**원인** Gitea의 ROOT_URL이 `localhost` 로 설정되어 있어 token 요청이 외부로 나가지 못함.
또한 Packages(Container Registry) 기능이 비활성화된 상태.
**해결**
Helm upgrade로 Gitea 설정 영구 변경:
```bash
helm repo add gitea https://dl.gitea.com/charts/
helm repo update
helm upgrade gitea gitea/gitea -n gitea \
--set gitea.config.server.DOMAIN=192.168.10.101 \
--set gitea.config.server.ROOT_URL=http://192.168.10.101:30000 \
--set gitea.config.server.HTTP_PORT=3000 \
--set gitea.config.packages.ENABLED=true \
--set service.http.type=NodePort \
--set service.http.nodePort=30000 \
--reuse-values
```
---
### 8. K8s 내부에서 Gitea Registry 이미지 Pull 실패
**증상** Pod가 `ImagePullBackOff` 상태. 외부에서는 push가 되지만 K8s Pod는 이미지를 못 가져옴.
**원인** K8s Pod는 외부 IP(`192.168.10.101:30000`)로 접근이 불안정하므로
내부 서비스명으로 접근해야 함.
**해결**
yaml의 image 주소를 K8s 내부 서비스명으로 변경:
```bash
sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/04-backend.yaml
sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/05-frontend.yaml
```
Registry Secret도 내부 주소로 재생성:
```bash
kubectl delete secret gitea-registry-secret -n web-portal
kubectl create secret docker-registry gitea-registry-secret \
--namespace=web-portal \
--docker-server=gitea-http.gitea.svc.cluster.local:3000 \
--docker-username=<계정> \
--docker-password=<패스워드>
```
---
### 9. 로그인 실패 - 비밀번호 해시 오염
**증상** 올바른 계정으로 로그인해도 `아이디 또는 비밀번호가 올바르지 않습니다` 출력.
**원인** 터미널 색상 코드(`\x1B[0m` 등)가 비밀번호 해시에 섞여 DB에 저장됨.
**해결**
Backend Pod에서 직접 Python으로 해시 재생성 후 DB 업데이트:
```bash
kubectl exec -n web-portal deployment/backend -- python3 -c "
import bcrypt, psycopg2
conn = psycopg2.connect(host='postgres-service', database='portaldb', user='portaluser', password='portalpass')
cur = conn.cursor()
h1 = bcrypt.hashpw('admin1234'.encode(), bcrypt.gensalt()).decode()
h2 = bcrypt.hashpw('user1234'.encode(), bcrypt.gensalt()).decode()
cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h1, 'admin'))
cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h2, 'user1'))
conn.commit()
print('완료')
"
```
---
### 10. git push 거절 (non-fast-forward)
**증상**
```
error: failed to push some refs
hint: Updates were rejected because the tip of your current branch is behind
```
**원인** Gitea UI에서 직접 파일을 수정해서 로컬과 원격 브랜치가 diverge된 상태.
**해결**
```bash
git pull origin main --rebase
git push origin main
```
---
### 11. cert-manager HTTP01 Challenge pending (헤어핀 NAT)
**증상**
```
propagation check failed: failed to perform self check GET request
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
```
**원인** cert-manager가 K8s 내부에서 외부 도메인(`cyanburu.com`)으로 self-check 요청을 보낼 때,
공인 IP → 라우터 → 내부 PC로 돌아오는 헤어핀 NAT이 지원되지 않아 타임아웃 발생.
**해결**
CoreDNS에 내부 도메인을 직접 등록해서 K8s 내부에서 도메인을 내부 IP로 해석하게 설정:
```bash
kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
kubectl rollout restart deployment/coredns -n kube-system
```
---
### 12. Ingress Controller EXTERNAL-IP가 localhost로 표시
**증상** `kubectl get svc -n ingress-nginx` 에서 EXTERNAL-IP가 `localhost` 로 표시됨.
**원인** Docker Desktop 환경의 정상적인 동작. `localhost` = 실제 PC를 의미.
**해결** 포트포워딩을 NodePort(30118, 30963)가 아닌 **80, 443 → PC내부IP:80, 443** 으로 설정.
Docker Desktop이 80/443을 받아서 Ingress Controller로 자동 전달.
---
### 13. git commit 시 Author identity unknown
**증상**
```
Author identity unknown
fatal: unable to auto-detect email address
```
**원인** Git 사용자 정보가 설정되지 않은 상태.
**해결**
```bash
git config --global user.email "계정@gitea.com"
git config --global user.name "계정명"
```
---
### 14. Gitea Registry ImagePullBackOff — 토큰 인증 헤어핀 NAT 문제
**증상**
```
Failed to pull image: Error response from daemon:
Get "http://gitea-http.gitea.svc.cluster.local:3000/v2/token?...": context deadline exceeded
```
**원인** Docker Desktop 환경에서 kubelet이 이미지를 Pull할 때 Docker 데몬을 통해 수행하는데,
Docker 데몬은 K8s 내부 DNS(`gitea-http.gitea.svc.cluster.local`)를 해석하지 못함.
Gitea Registry가 토큰 인증을 내부 서비스명으로 리다이렉트하면 Docker 데몬이 접근 불가 → timeout 발생.
**해결**
이미지 주소와 Registry Secret을 외부 IP로 통일:
```bash
# yaml 이미지 주소 변경
sed -i "s|gitea-http.gitea.svc.cluster.local:3000|192.168.10.101:30000|g" k8s/04-backend.yaml
sed -i "s|gitea-http.gitea.svc.cluster.local:3000|192.168.10.101:30000|g" k8s/05-frontend.yaml
# Registry Secret 재생성 (외부 IP 기준)
kubectl delete secret gitea-registry-secret -n web-portal
kubectl create secret docker-registry gitea-registry-secret \
--namespace=web-portal \
--docker-server=192.168.10.101:30000 \
--docker-username=<계정> \
--docker-password=<패스워드>
# 적용
kubectl apply -f k8s/04-backend.yaml
kubectl apply -f k8s/05-frontend.yaml
kubectl rollout restart deployment/backend -n web-portal
kubectl rollout restart deployment/frontend -n web-portal
```
> ⚠️ Gitea ROOT_URL을 내부 서비스명으로 변경해도 Docker 데몬은 K8s 내부 DNS를 사용할 수 없으므로
> Docker Desktop 환경에서는 반드시 외부 IP(`192.168.10.101:30000`)를 사용해야 함.
---
### 15. 모니터링 알림 환경변수 미적용 (notifier skipping)
**증상**
```
[NOTIFIER] Discord webhook URL not set, skipping
[NOTIFIER] Gmail config not set, skipping
```
**원인** `kubectl set image` 명령어로 이미지를 변경하면 deployment의 환경변수 설정이 초기화됨.
또는 Secret 이름이 yaml과 다르게 생성된 경우.
**해결**
yaml의 Secret 이름(`notify-secrets`)과 key 이름을 확인 후 동일하게 Secret 재생성:
```bash
kubectl delete secret notify-secrets -n web-portal
kubectl create secret generic notify-secrets \
--namespace=web-portal \
--from-literal=discord-webhook-url="https://discord.com/api/webhooks/..." \
--from-literal=gmail-user="발송계정@gmail.com" \
--from-literal=gmail-app-password="xxxx xxxx xxxx xxxx" \
--from-literal=alert-email-to="수신계정@gmail.com"
kubectl rollout restart deployment/backend -n web-portal
```
yaml 변경 후에는 반드시 `kubectl apply -f k8s/04-backend.yaml` 로 재적용할 것.
---
## 📅 변경 이력
### 2026-04-06 (초기 구축)
- Kubernetes 환경 구성 (Docker Desktop)
- FastAPI 백엔드 + Nginx 프론트엔드 + PostgreSQL 배포
- Gitea + ArgoCD GitOps 파이프라인 구성
- Gitea Container Registry 연동
### 2026-04-10 (기능 추가 + 도메인 연결)
#### 기능 추가
- **MY Page**: 탭명/목록 제목 영문 변경, URL 미표기, Favicon 자동 표시
- **비밀번호 보안 강화**
- 로그인 비밀번호 표시/숨김 토글 버튼
- 로그인 실패 시 아이디 유지 (비밀번호만 초기화)
- 최초 로그인 시 비밀번호 강제 변경
- 비밀번호 5회 오류 시 계정 자동 잠금
- 관리자의 사용자 비밀번호 변경 / 임시 비밀번호 발급 / 잠금 해제
- **공지사항 탭**: 관리자 작성 전용, 모든 사용자 댓글 가능
- **관리자 요청 탭**: 게시판 형태, 모든 사용자 작성/답글 가능
- **Nginx Rate Limiting**: 로그인 API 분당 5회 제한 (Brute Force 방어)
#### 도메인 연결 (HTTPS)
- **Nginx Ingress Controller** 설치 및 구성
- **cert-manager** 설치 + Let's Encrypt 인증서 자동 발급
- **cyanburu.com** 도메인 연결 (후이즈)
- **MSI 라우터** 포트포워딩 설정 (80/443)
- **CoreDNS** 내부 도메인 등록 (헤어핀 NAT 우회)
- **HTTPS 자동 리다이렉트** 적용
- 최종 접속 URL: `https://cyanburu.com`
#### 서브도메인 연결
- **gitea.cyanburu.com** → Gitea (Let's Encrypt 인증서 자동 발급)
- **argo.cyanburu.com** → ArgoCD (Let's Encrypt 인증서 자동 발급)
- CoreDNS에 서브도메인 내부 IP 등록 (헤어핀 NAT 우회)
### 2026-04-27 (모니터링 알림 추가 + 장애 복구)
#### 기능 추가
- **Discord 알림**: Pod 이상/복구, 계정 잠금, 임시 비밀번호 발급 시 Discord Webhook 알림
- **Gmail 알림**: Pod 이상/복구, 인증서 만료 임박 시 이메일 알림
- **자동 모니터링 스케줄러**: Pod 상태 1분마다 체크, 인증서 만료 24시간마다 체크
- `notifier.py` — Discord/Gmail 알림 모듈 추가
- `monitor.py` — APScheduler 기반 모니터링 모듈 추가
- `main.py``asyncio.create_task()``await` 방식으로 수정 (동기함수 내 비동기 호출 오류 수정)
#### 알림 설정 방법
**1. Discord Webhook URL 발급**
Discord 서버 → 알림받을 채널 → ⚙️ 채널 설정 → 연동 → 웹후크 → 새 웹후크 생성 → URL 복사
**2. Gmail 앱 비밀번호 발급**
- Google 계정 → 보안 → 2단계 인증 활성화 (필수)
- `https://myaccount.google.com/apppasswords` → 앱 이름 입력 → 16자리 비밀번호 복사
**3. K8s Secret 등록**
```bash
kubectl create secret generic notify-secrets \
--namespace=web-portal \
--from-literal=discord-webhook-url="https://discord.com/api/webhooks/..." \
--from-literal=gmail-user="발송계정@gmail.com" \
--from-literal=gmail-app-password="xxxx xxxx xxxx xxxx" \
--from-literal=alert-email-to="수신계정@gmail.com"
```
**4. 알림 테스트**
브라우저 콘솔(F12)에서 실행:
```javascript
fetch('/api/admin/notify-test', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('portal_token') }
}).then(r => r.json()).then(console.log)
```
#### 장애 복구 내용
- **backend ImagePullBackOff** — Gitea Registry 토큰 인증 문제로 발생, 외부 IP 방식으로 해결
- **backend/frontend 이미지 주소** — 내부 서비스명(`gitea-http.gitea.svc.cluster.local:3000`) → 외부 IP(`192.168.10.101:30000`)로 변경
- **notifier.py, monitor.py 누락** — Dockerfile에 파일이 있었으나 `--no-cache` 재빌드로 해결
- **notify-secrets** — 기존 Secret이 잘못된 값으로 등록되어 있어 재생성
### 2026-04-27 (알림 채널 관리 UI 추가)
#### 기능 추가
- **🔔 알림 채널 관리 페이지** — 관리자 탭에 추가, 웹에서 직접 알림 채널 추가/수정/삭제 가능
- **다중 채널 지원** — Discord 전용 / Gmail 전용 / Discord+Gmail 혼합 채널을 복수로 등록 가능
- **채널 유형별 발송 (B방식)** — 채널 유형과 알림 종류의 교집합으로 발송 대상 자동 결정
- **DB 기반 알림 설정** — `notify_channels` 테이블에 저장, Pod 재시작 후에도 설정 유지
- **환경변수 fallback** — DB 채널 미등록 시 K8s Secret 환경변수로 자동 대체
- **notifier.py 전면 개선** — 매 발송 시 DB에서 활성 채널 목록 직접 조회 후 발송
#### 채널 유형별 발송 규칙
| 채널 유형 | notify_both (Pod 이상/복구) | notify_discord_only (계정잠금/임시PW) | notify_email_only (인증서만료) |
|---------|--------------------------|--------------------------------------|-------------------------------|
| Discord+Gmail | Discord + Gmail | Discord만 | Gmail만 |
| Discord 전용 | Discord만 | Discord만 | 발송 안 함 |
| Gmail 전용 | Gmail만 | 발송 안 함 | Gmail만 |
#### 알림 채널 등록 방법 (웹 UI)
1. 관리자 로그인 → **🔔 알림 설정** 탭
2. 좌측 폼에서 채널 이름, 유형, Webhook URL / Gmail 정보 입력
3. **💾 저장** 클릭 → 우측 목록에 추가됨
4. 목록에서 ✏️ 클릭 → 폼에 값 자동 입력 후 수정/저장
5. 🗑️ 클릭 → 채널 삭제
6. **📨 테스트 발송** 버튼으로 등록된 모든 채널에 테스트 알림 발송
#### DB 테이블 수동 생성 (신규 배포 전 필요 시)
```bash
kubectl exec -n web-portal deployment/backend -- python3 -c "
import psycopg2
conn = psycopg2.connect(host='postgres-service', database='portaldb', user='portaluser', password='portalpass')
cur = conn.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS notify_channels (
id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, type VARCHAR(20) NOT NULL,
discord_webhook_url TEXT DEFAULT chr(0), gmail_user VARCHAR(200) DEFAULT chr(0),
gmail_app_password TEXT DEFAULT chr(0), alert_email_to VARCHAR(200) DEFAULT chr(0),
enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW()
)''')
conn.commit()
print('완료')
"
```
> 새 `main.py` 배포 후에는 startup 시 자동으로 테이블이 생성되므로 수동 생성 불필요.
#### 프로젝트 구조 변경
```
backend/
├── main.py ← 알림 채널 CRUD API 추가, notify_channels 테이블 자동 생성
├── notifier.py ← DB 기반 다중 채널 발송으로 전면 재작성
└── monitor.py ← 변경 없음 (notifier 인터페이스 동일)
frontend/
└── index.html ← 🔔 알림 설정 탭 추가 (좌측 폼 + 우측 리스트 UI)
```
---
### 2026-05-19 (허브 홈페이지 구축 + URL 구조 개편)
#### URL 구조 변경
| 변경 전 | 변경 후 | 내용 |
|---------|---------|------|
| `cyanburu.com/` | `cyanburu.com/portal` | 기존 웹 포털 경로 변경 |
| — | `cyanburu.com/` | 신규 허브 홈페이지 (루트) |
| — | `cyanburu.com/kingscup` | 킹컵 게임 (개발 예정) |
#### 기능 추가
**허브 홈페이지 (`cyanburu.com/`)**
- 포트폴리오 겸 서비스 허브 페이지 신규 구축
- 에디토리얼 매거진 + 사이버펑크 믹스 디자인
- `Cormorant Garamond` 세리프 폰트 + 크림 배경 (에디토리얼)
- 배경 격자 그리드, 민트 네온 호버 효과 (사이버펑크)
- 터미널 스타일 클러스터 상태 표시 + 실시간 업타임 카운터
- 커스텀 커서 (민트 점 + 링)
- DB 기반 동적 카드 로딩 — `/portal/api/homepage/cards` API 호출
- API 실패 시 정적 카드로 자동 폴백
- `hub` 네임스페이스에 독립 배포 (nginx 정적 서빙)
**홈 카드 관리 (관리자)**
- `cyanburu.com/portal` 관리자 탭에 `🏠 홈 카드 관리` 추가
- 카드 추가 / 수정 / 삭제 / 순서 변경 / 공개 여부 설정
- 변경 사항이 허브 홈페이지에 즉시 반영 (재배포 불필요)
- `homepage_cards` 테이블 (PostgreSQL) 신규 추가
**web-portal 경로 변경 (`/` → `/portal`)**
- Nginx Ingress `rewrite-target` 으로 `/portal` prefix strip 처리
- `frontend/nginx.conf` subpath 대응 수정
- `frontend/index.html` API 경로 `/api``/portal/api` 수정
- `backend/main.py` `root_path="/portal"` 추가
#### 신규 k8s 파일
| 파일 | 내용 |
|------|------|
| `k8s/00-hub.yaml` | hub 네임스페이스 + Deployment + Service |
| `k8s/13-ingress-hub.yaml` | Hub Ingress (`cyanburu.com/`) |
#### DB 테이블 추가
```sql
CREATE TABLE IF NOT EXISTS homepage_cards (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
subtitle VARCHAR(200),
description TEXT,
url VARCHAR(500) NOT NULL,
tag VARCHAR(20) DEFAULT 'LIVE',
sort_order INTEGER DEFAULT 0,
visible BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
```
> 신규 배포 시 `main.py` startup 이벤트에서 자동 생성. 실패 시 psql에서 직접 실행.
#### 프로젝트 구조 변경
```
hub/ ← 신규 추가
├── index.html ← 허브 홈페이지 (동적 카드 로딩)
└── Dockerfile ← nginx:alpine 기반 정적 서빙
backend/
└── main.py ← homepage_cards CRUD API 추가, root_path="/portal" 설정
frontend/
├── index.html ← API 경로 /portal/api 로 수정, 🏠 홈 카드 관리 탭 추가
└── nginx.conf ← /portal subpath 대응 수정
k8s/
├── 00-hub.yaml ← 신규 추가
├── 08-ingress.yaml ← /portal 경로로 변경
└── 13-ingress-hub.yaml ← 신규 추가 (cyanburu.com/ → hub)
```
#### 트러블슈팅 (5월)
**hub namespace not found**
- Registry Secret 생성 전 네임스페이스가 없어서 발생
- 해결: `kubectl create namespace hub` 먼저 실행 후 Secret 생성
**Ingress host+path 충돌 (BadRequest)**
- 기존 web-portal-ingress가 `cyanburu.com /` 를 점유하고 있어서 hub-ingress 생성 불가
- 해결: `08-ingress.yaml``/portal` 경로로 먼저 apply 후 `13-ingress-hub.yaml` apply
**홈 카드 관리 탭 내용 미표시**
- `page-admin-hub-cards` div가 `</main>` 밖에 위치해서 JS가 null 반환
- 해결: sed 명령으로 div를 `</main>` 안으로 이동
**hub 홈페이지 카드 미반영**
- `hub/index.html`에 동적 로딩 코드가 없는 구버전 파일이 배포됨
- 해결: 동적 카드 로딩 버전으로 `hub/index.html` 교체 후 재빌드