# 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 # 백엔드 이미지 빌드 & 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= \ --docker-password= ``` ### 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= \ --from-literal=password= 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 '<', "..." 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가 `` 밖에 위치해서 JS가 null 반환 - 해결: sed 명령으로 div를 `` 안으로 이동 **hub 홈페이지 카드 미반영** - `hub/index.html`에 동적 로딩 코드가 없는 구버전 파일이 배포됨 - 해결: 동적 카드 로딩 버전으로 `hub/index.html` 교체 후 재빌드