# ๐ Web Portal โ Kubernetes + GitOps ํ๋ฉ ํ๋ก์ ํธ
> ์กฐ์ง ๋ด๋ถ ์นํ์ด์ง๋ฅผ ํตํฉ ๊ด๋ฆฌํ๋ ํฌํธ ์์คํ
์ **์จํ๋ ๋ฏธ์ค Kubernetes ํ๊ฒฝ**์์ ์ง์ ์ค๊ณยท๊ตฌ์ถยท์ด์ํ ํ๋ฉ ํ๋ก์ ํธ์
๋๋ค.
[](https://kubernetes.io)
[](https://argoproj.github.io/cd)
[](https://fastapi.tiangolo.com)
[](https://www.postgresql.org)
[](https://www.docker.com)
[](https://letsencrypt.org)
---
## ๐ ํ๋ก์ ํธ ๋ชฉ์
๋จ์ํ ๋ฐ๋ผํ๋ ํํ ๋ฆฌ์ผ์ด ์๋, **์ค์ ์ด์ ํ๊ฒฝ์์ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค์ ์ง์ ๋ง๋ฅ๋จ๋ฆฌ๊ณ ํด๊ฒฐ**ํ๋ ๊ฒ์ ๋ชฉํ๋ก ๊ตฌ์ถํ์ต๋๋ค.
- ์ค์ ๋๋ฉ์ธ(`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 '<', "..." 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)
### ํต์ฌ ๋ฐฐํฌ ์์
```bash
# 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 /portal-backend:latest ./backend/
docker build -t /portal-frontend:latest ./frontend/
docker push /portal-backend:latest
docker push /portal-frontend:latest
# 5. K8s ๋ฆฌ์์ค ๋ฐฐํฌ
kubectl apply -f k8s/
# 6. ArgoCD Application ๋ฑ๋ก
kubectl apply -f 06-argocd-app.yaml
```
์์ธํ ๋ฐฐํฌ ๊ฐ์ด๋๋ [๋ฐฐํฌ ๋ฌธ์](docs/deployment.md)๋ฅผ ์ฐธ๊ณ ํ์ธ์.
---
## ๐ ๋ฐฐ์ด ์ / ํต์ฌ ์ธ์ฌ์ดํธ
| ๋ฌธ์ ์ ํ | ๋ฐฐ์ด ์ |
|-----------|---------|
| **ํค์ดํ 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