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

33 KiB
Executable File
Raw Blame History

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:

{
  "builder": {
    "gc": {
      "defaultKeepStorage": "20GB",
      "enabled": true
    }
  },
  "experimental": false,
  "insecure-registries": ["192.168.10.101:30000"]
}

Apply & Restart 클릭

2단계. Gitea Registry 로그인 및 이미지 빌드 & Push

# 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 생성

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 저장소 인증 등록

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 등록

kubectl apply -f 06-argocd-app.yaml

6단계. Nginx Ingress Controller 설치 (최초 1회)

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회)

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 우회)

kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
kubectl rollout restart deployment/coredns -n kube-system

coredns-patch.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 배포

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단계. 인증서 발급 확인

kubectl get certificate -n web-portal
# READY: True 확인

12단계. 서브도메인 Ingress 적용 (Gitea, ArgoCD)

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회)

# 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만 변경한 경우 (설정 변경)

git add .
git commit -m "fix: 변경내용"
git push origin main
# → ArgoCD가 자동으로 감지해서 재배포

이미지도 변경한 경우 (코드 변경)

# 이미지 재빌드 & 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

🔧 운영 명령어

# 전체 리소스 상태 확인
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 에서 반드시 변경:

stringData:
  db-password: "강력한패스워드로변경"
  jwt-secret: "64자이상의랜덤문자열로변경"

Nginx Rate Limiting (Brute Force 방어)

frontend/nginx.conf 상단에 추가:

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

로그인 location에 적용:

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")
  • 인증서 상태 확인:
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 범위):

nodePort: 30090

변경 후 기존 서비스 삭제 및 재적용:

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 대기 시간 증가:

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으로 변경:

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 생성:

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:

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 추가:

{
  "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 설정 영구 변경:

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 내부 서비스명으로 변경:

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도 내부 주소로 재생성:

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 업데이트:

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된 상태.

해결

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로 해석하게 설정:

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 사용자 정보가 설정되지 않은 상태.

해결

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로 통일:

# 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 재생성:

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.pyasyncio.create_task()await 방식으로 수정 (동기함수 내 비동기 호출 오류 수정)

알림 설정 방법

1. Discord Webhook URL 발급 Discord 서버 → 알림받을 채널 → ⚙️ 채널 설정 → 연동 → 웹후크 → 새 웹후크 생성 → URL 복사

2. Gmail 앱 비밀번호 발급

  • Google 계정 → 보안 → 2단계 인증 활성화 (필수)
  • https://myaccount.google.com/apppasswords → 앱 이름 입력 → 16자리 비밀번호 복사

3. K8s Secret 등록

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)에서 실행:

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 테이블 수동 생성 (신규 배포 전 필요 시)

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 테이블 추가

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 교체 후 재빌드