13 KiB
Executable File
🌐 Web Portal — Kubernetes + GitOps 홈랩 프로젝트
조직 내부 웹페이지를 통합 관리하는 포털 시스템을 온프레미스 Kubernetes 환경에서 직접 설계·구축·운영한 홈랩 프로젝트입니다.
📌 프로젝트 목적
단순히 따라하는 튜토리얼이 아닌, 실제 운영 환경에서 발생하는 문제들을 직접 맞닥뜨리고 해결하는 것을 목표로 구축했습니다.
- 실제 도메인(
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