🌐 Web Portal — Kubernetes + GitOps 홈랩 프로젝트

조직 내부 웹페이지를 통합 관리하는 포털 시스템을 온프레미스 Kubernetes 환경에서 직접 설계·구축·운영한 홈랩 프로젝트입니다.

Kubernetes ArgoCD FastAPI PostgreSQL Docker Let's Encrypt


📌 프로젝트 목적

단순히 따라하는 튜토리얼이 아닌, 실제 운영 환경에서 발생하는 문제들을 직접 맞닥뜨리고 해결하는 것을 목표로 구축했습니다.

  • 실제 도메인(cyanburu.com) + HTTPS 적용으로 외부 인터넷에서 접근 가능한 서비스 운영
  • GitOps 방식으로 코드 변경 시 자동 배포되는 CI/CD 파이프라인 구성
  • Pod 이상 감지, 인증서 만료 임박 시 Discord·Gmail 자동 알림 구현
  • 발생한 장애 14건을 모두 문서화하여 재현·해결 과정 기록

🏗️ 전체 아키텍처

사용자 (외부 인터넷)
    ├── https://cyanburu.com        → Web Portal
    ├── https://gitea.cyanburu.com  → Gitea (Self-hosted Git)
    └── https://argo.cyanburu.com   → ArgoCD (GitOps)
              ↓
    MSI 라우터 (포트포워딩 80/443)
              ↓
    Nginx Ingress Controller  ← TLS 종료, 도메인별 라우팅
              ↓
    cert-manager              ← Let's Encrypt 인증서 자동 발급/갱신
              ↓
    Kubernetes 네임스페이스
      ├── web-portal
      │     ├── Nginx Frontend  (SPA)
      │     ├── FastAPI Backend (REST API)
      │     └── PostgreSQL DB   (PVC 영구 저장)
      ├── gitea     → Self-hosted Git + Container Registry
      └── argocd    → GitOps 자동 배포 엔진
              ↑
    개발자 git push → Gitea → ArgoCD 자동 감지 & 배포

🛠️ 기술 스택

구분 기술 선택 이유
Container Orchestration Kubernetes (Docker Desktop 내장) 온프레미스 환경에서 프로덕션과 동일한 구조 구현
GitOps Gitea + ArgoCD Git을 단일 진실 소스로, 선언적 배포 자동화
Image Registry Gitea Container Registry 외부 의존 없이 사내 레지스트리 자체 운영
Ingress Nginx Ingress Controller 도메인 기반 라우팅, TLS 종료 처리
TLS cert-manager + Let's Encrypt 인증서 발급·갱신 완전 자동화
Backend Python FastAPI 비동기 REST API, JWT 인증
Database PostgreSQL + PVC 컨테이너 재시작 후에도 데이터 영속
Monitoring APScheduler (커스텀) Pod 상태 1분 주기, 인증서 만료 24시간 주기 체크
Alerting Discord Webhook + Gmail 장애 발생 시 즉시 알림

구현 기능

보안 (Security)

  • JWT 기반 인증 / 세션 관리
  • 비밀번호 5회 오류 시 계정 자동 잠금 → 관리자 해제
  • 최초 로그인 강제 비밀번호 변경 (임시 비밀번호 발급 포함)
  • Nginx Rate Limiting — 로그인 API 분당 5회 제한 (Brute Force 방어)
  • HTTPS 자동 리다이렉트 (ssl-redirect: "true")

모니터링 & 알림 (Observability)

  • Pod 상태 1분마다 자동 체크 → 이상/복구 시 Discord + Gmail 알림
  • 인증서 만료 24시간 주기 체크 → 만료 임박 시 Gmail 알림
  • 웹 UI에서 알림 채널 관리 (Discord/Gmail/혼합 채널 다중 등록)
  • DB 기반 알림 설정 → Pod 재시작 후에도 설정 유지

관리자 기능

  • 사용자 계정 생성/삭제, 접근 권한 설정 (체크박스 UI)
  • 임시 비밀번호 자동 생성 및 발급
  • 공지사항 / 관리자 요청 게시판 (댓글·답글 포함)
  • 사용자 상태 태그: 정상 / 🔒잠김 / 초기PW / 변경요청

📅 구축 이력

날짜 주요 작업
2026-04-06 K8s 초기 구축, FastAPI + Nginx + PostgreSQL 배포, Gitea + ArgoCD GitOps 구성
2026-04-10 도메인 연결 (HTTPS), cert-manager, CoreDNS 헤어핀 NAT 우회, 보안 기능 강화
2026-04-27 Discord/Gmail 알림 추가, APScheduler 모니터링, 알림 채널 관리 UI

🔥 트러블슈팅 (14건 해결)

실제 구축 과정에서 겪은 장애와 해결 방법을 기록합니다.

1. NodePort 포트 충돌

증상 provided port is already allocated 원인 해당 NodePort가 다른 서비스에서 이미 사용 중 해결 k8s/05-frontend.yaml에서 NodePort 번호 변경 후 서비스 재적용

2. Backend CrashLoopBackOff — Liveness Probe 실패

증상 Liveness probe failed: HTTP probe failed with statuscode: 404 원인 DB 연결 대기 중 liveness probe가 먼저 실패해 K8s가 강제 재시작 해결 initialDelaySeconds: 60, failureThreshold: 5로 대기 시간 증가

3. Nginx API 프록시 실패 — JSON 파싱 오류

증상 로그인 시 Unexpected token '<', "<html>..." is not valid JSON 원인 Nginx가 /api/ 요청을 백엔드로 전달하지 못하고 HTML 반환 해결 proxy_pass를 K8s 내부 FQDN(backend-service.web-portal.svc.cluster.local)으로 변경

4. ArgoCD — authentication required: Unauthorized

증상 failed to list refs: authentication required 원인 ArgoCD가 Gitea 저장소 인증 정보 없음 해결 argocd.argoproj.io/secret-type=repository 레이블이 있는 Secret 직접 생성

5. RepeatedResourceWarning — 리소스 중복

증상 Resource appeared 2 times among application resources 원인 k8s/ 폴더 내 동일 리소스를 정의하는 YAML 중복 존재 해결 중복 파일 삭제 후 push

6. ImagePullBackOff — Gitea Registry HTTP 접근 오류

증상 server gave HTTP response to HTTPS client 원인 Gitea Registry가 HTTP인데 Docker가 HTTPS로 접근 시도 해결 Docker Desktop insecure-registries 설정 추가

7. Gitea Container Registry 로그인 실패 (context deadline exceeded)

증상 Get "http://...:30000/v2/": context deadline exceeded 원인 Gitea ROOT_URL이 localhost로 설정되어 token 요청이 외부로 나가지 못함 해결 Helm upgrade로 ROOT_URL, DOMAIN, Packages 활성화 설정 변경

8. K8s 내부 Gitea Registry 이미지 Pull 실패

증상 외부에서는 push 성공, Pod는 ImagePullBackOff 원인 K8s Pod → 외부 IP 경로 불안정 해결 image 주소를 K8s 내부 서비스명(gitea-http.gitea.svc.cluster.local:3000)으로 변경

9. 로그인 실패 — 비밀번호 해시 오염

증상 올바른 계정으로도 로그인 실패 원인 터미널 색상 코드(\x1B[0m)가 bcrypt 해시에 섞여 DB에 저장됨 해결 Backend Pod에서 Python으로 해시 재생성 후 DB 직접 업데이트

10. git push 거절 — non-fast-forward

증상 Updates were rejected because the tip of your current branch is behind 원인 Gitea UI에서 직접 파일 수정으로 로컬-원격 브랜치 diverge 해결 git pull origin main --rebase 후 push

11. cert-manager HTTP01 Challenge Pending — 헤어핀 NAT

증상 propagation check failed: context deadline exceeded 원인 cert-manager가 K8s 내부에서 외부 도메인으로 self-check 시 헤어핀 NAT 미지원으로 타임아웃 해결 CoreDNS에 내부 도메인을 직접 등록 → K8s 내부에서 도메인을 내부 IP로 해석

12. Ingress Controller EXTERNAL-IP가 localhost로 표시

증상 kubectl get svc -n ingress-nginx → EXTERNAL-IP: localhost 원인 Docker Desktop 환경의 정상 동작 (localhost = 실제 PC) 해결 포트포워딩을 NodePort가 아닌 80/443 → PC 내부 IP:80/443으로 직접 설정

13. git commit 실패 — Author identity unknown

증상 fatal: unable to auto-detect email address 원인 Git 사용자 정보 미설정 해결 git config --global user.email, user.name 설정

14. Gitea Registry ImagePullBackOff — 토큰 인증 헤어핀 NAT

증상 Get "http://gitea-http...svc.cluster.local:3000/v2/token?...": context deadline exceeded 원인 Docker 데몬이 K8s 내부 DNS를 해석하지 못해 토큰 인증 실패 해결 image 주소와 Registry Secret을 외부 IP(192.168.10.101:30000)로 통일


📁 프로젝트 구조

nginx-portal/
├── backend/
│   ├── main.py          # FastAPI 전체 API 로직 (인증, 사용자 관리, 게시판)
│   ├── notifier.py      # Discord/Gmail 알림 모듈 (DB 기반 다중 채널)
│   ├── monitor.py       # APScheduler 모니터링 (Pod 상태, 인증서 만료)
│   ├── requirements.txt
│   └── Dockerfile
├── frontend/
│   ├── index.html       # 싱글 페이지 앱 (SPA)
│   ├── nginx.conf       # Nginx 설정 + Rate Limiting + /api/* 프록시
│   └── Dockerfile
├── k8s/
│   ├── 01-namespace.yaml
│   ├── 02-postgres.yaml      # PostgreSQL + PVC
│   ├── 03-secrets.yaml       # DB / JWT 시크릿
│   ├── 04-backend.yaml       # FastAPI Deployment
│   ├── 05-frontend.yaml      # Nginx Deployment
│   ├── 07-clusterissuer.yaml # Let's Encrypt ClusterIssuer
│   ├── 08-ingress.yaml       # cyanburu.com
│   ├── 09-ingress-gitea.yaml
│   └── 10-ingress-argocd.yaml
├── 06-argocd-app.yaml   # ArgoCD Application (k8s/ 폴더 밖 — 순환 참조 방지)
└── README.md

🚀 배포 방법

사전 요구 사항

  • Docker Desktop (Kubernetes 활성화)
  • kubectl, helm 설치
  • 공인 도메인 (A 레코드 → 공인 IP 연결)
  • 라우터 포트포워딩 설정 (80, 443)

핵심 배포 순서

# 1. Nginx Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml

# 2. cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml

# 3. CoreDNS 헤어핀 NAT 우회 설정
kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
kubectl rollout restart deployment/coredns -n kube-system

# 4. 이미지 빌드 & Push
docker build -t <registry>/portal-backend:latest ./backend/
docker build -t <registry>/portal-frontend:latest ./frontend/
docker push <registry>/portal-backend:latest
docker push <registry>/portal-frontend:latest

# 5. K8s 리소스 배포
kubectl apply -f k8s/

# 6. ArgoCD Application 등록
kubectl apply -f 06-argocd-app.yaml

자세한 배포 가이드는 배포 문서를 참고하세요.


📊 배운 점 / 핵심 인사이트

문제 유형 배운 점
헤어핀 NAT K8s 내부에서 외부 도메인으로 self-check 시 발생하는 네트워크 루프 → CoreDNS 내부 오버라이드로 해결
Docker 데몬 vs kubelet DNS kubelet은 K8s 내부 DNS 사용 가능, Docker 데몬은 호스트 DNS만 사용 → 레지스트리 주소 통일 필요
GitOps 순환 참조 ArgoCD Application YAML을 감시 대상 폴더 안에 넣으면 무한 루프 → 폴더 밖 별도 관리
컨테이너 시작 순서 DB 준비 전 백엔드 probe 실행 → initialDelaySeconds로 의존성 순서 제어
bcrypt 해시 오염 터미널 ANSI 색상 코드가 해시에 섞이는 엣지 케이스 → 출력값 직접 검증 필요

🔜 개선 예정 (Next Steps)

  • GitHub Actions CI/CD 파이프라인 구성 (자동 빌드 → Push → ArgoCD sync)
  • Helm Chart로 패키징 (values.yaml 환경별 분리)
  • Prometheus + Grafana 모니터링 스택 도입
  • Terraform으로 Azure 인프라 IaC 관리
  • Trivy 이미지 취약점 스캔 → GitHub Actions 통합

📄 라이선스

MIT License

Description
nginx로 사용한 Web portal
Readme 311 KiB
Languages
HTML 61.3%
Python 36.1%
Shell 2.3%
Dockerfile 0.3%