769 lines
25 KiB
Markdown
Executable File
769 lines
25 KiB
Markdown
Executable File
# Web Portal - Kubernetes + GitOps 배포 가이드
|
|
|
|
## 📋 프로젝트 개요
|
|
|
|
조직 내부 웹페이지를 통합 관리하는 포털 시스템입니다.
|
|
로그인 후 본인에게 할당된 웹페이지 목록을 확인하고 접속할 수 있으며,
|
|
관리자는 페이지 및 사용자 권한을 관리할 수 있습니다.
|
|
|
|
---
|
|
|
|
## 🏗️ 전체 아키텍처
|
|
|
|
```
|
|
사용자 (외부 인터넷)
|
|
├── https://cyanburu.com → Web Portal
|
|
├── https://gitea.cyanburu.com → Gitea
|
|
└── https://argo.cyanburu.com → ArgoCD
|
|
↓
|
|
MSI 라우터 (포트포워딩 80/443)
|
|
↓
|
|
Nginx Ingress Controller ← TLS 종료, 도메인별 라우팅
|
|
↓
|
|
cert-manager ← Let's Encrypt 인증서 자동 발급/갱신
|
|
↓
|
|
Kubernetes 네임스페이스별 서비스
|
|
├── 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 |
|
|
| 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/* 프록시
|
|
│ └── Dockerfile
|
|
├── k8s/
|
|
│ ├── 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)
|
|
├── 06-argocd-app.yaml # ArgoCD Application 정의 (k8s 폴더 밖에 위치)
|
|
└── README.md
|
|
|
|
k8s/ 폴더 내 추가 파일:
|
|
├── 07-clusterissuer.yaml # Let's Encrypt ClusterIssuer
|
|
├── 08-ingress.yaml # Web Portal Ingress (cyanburu.com)
|
|
├── 09-ingress-gitea.yaml # Gitea Ingress (gitea.cyanburu.com)
|
|
└── 10-ingress-argocd.yaml # ArgoCD Ingress (argo.cyanburu.com)
|
|
```
|
|
|
|
> ⚠️ `06-argocd-app.yaml` 은 반드시 `k8s/` 폴더 **밖**에 위치해야 합니다.
|
|
> ArgoCD가 `k8s/` 폴더를 감시하므로 해당 파일이 안에 있으면 순환 참조 문제가 발생합니다.
|
|
|
|
---
|
|
|
|
## 🔑 기본 계정
|
|
|
|
| 구분 | ID | Password |
|
|
|------|-----|----------|
|
|
| 관리자 | `admin` | `admin1234` |
|
|
| 일반사용자 | `user1` | `user1234` |
|
|
|
|
---
|
|
|
|
## ✨ 기능 설명
|
|
|
|
### 일반 사용자
|
|
- 로그인 후 **MY Page List** 에서 본인에게 할당된 웹페이지를 카드 형태로 확인
|
|
- 카드의 Favicon 자동 표시 (없을 경우 기본 아이콘)
|
|
- 카드 클릭 시 **새 탭에서 해당 URL로 이동**
|
|
- **공지사항** 탭에서 관리자가 등록한 공지 확인 및 댓글 작성
|
|
- **관리자 요청** 탭에서 게시글 작성 및 답글 작성
|
|
- 로그인 비밀번호 표시/숨김 토글 버튼
|
|
- 로그인 실패 시 아이디 유지 (비밀번호만 초기화)
|
|
- **비밀번호 변경** 메뉴 (헤더에서 언제든 변경 가능)
|
|
- **최초 로그인 시 비밀번호 강제 변경** (변경 전까지 서비스 이용 불가)
|
|
- **비밀번호 5회 오류 시 계정 자동 잠금** → 관리자에게 잠금 해제 요청 필요
|
|
|
|
### 관리자
|
|
일반 사용자 기능 + 추가:
|
|
- **페이지 관리**: 웹페이지 추가 / 수정 / 삭제
|
|
- **사용자 관리**: 계정 생성 / 삭제
|
|
- **권한 설정**: 사용자별 접근 가능 페이지를 체크박스로 지정
|
|
- **비밀번호 변경**: 특정 사용자의 비밀번호 직접 변경 (변경 후 해당 사용자 강제 변경 적용)
|
|
- **임시 비밀번호 발급**: 랜덤 임시 비밀번호 자동 생성 후 화면에 표시
|
|
- **계정 잠금 해제**: 잠긴 계정을 버튼 하나로 해제
|
|
- **사용자 상태 확인**: 정상 / 🔒잠김 / 초기PW / 변경요청 태그로 한눈에 확인
|
|
- **공지사항 작성**: 공지 탭에서 전체 사용자에게 공지 등록 / 삭제
|
|
|
|
### 게시판 (공지 / 관리자 요청)
|
|
|
|
| 구분 | 공지 | 관리자 요청 |
|
|
|------|------|------------|
|
|
| 글 작성 | 관리자만 가능 | 모든 사용자 가능 |
|
|
| 댓글/답글 | 모든 사용자 가능 | 모든 사용자 가능 |
|
|
| 글 삭제 | 관리자만 가능 | 본인 또는 관리자 |
|
|
| 댓글 삭제 | 본인 또는 관리자 | 본인 또는 관리자 |
|
|
| 목록 항목 | 번호, 제목, 작성자, 작성일(시간포함) | 번호, 제목, 작성자, 작성일(시간포함) |
|
|
|
|
---
|
|
|
|
## 🚀 최초 배포 순서
|
|
|
|
### 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 <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 생성
|
|
```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=<Gitea계정> \
|
|
--docker-password=<Gitea패스워드>
|
|
```
|
|
|
|
### 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=<Gitea계정> \
|
|
--from-literal=password=<Gitea패스워드>
|
|
|
|
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 |
|
|
|--------|-----|
|
|
| Web Portal | `https://cyanburu.com` |
|
|
| Gitea | `https://gitea.cyanburu.com` |
|
|
| ArgoCD | `https://argo.cyanburu.com` |
|
|
|
|
---
|
|
|
|
## 🔄 이후 배포 방법 (코드 수정 시)
|
|
|
|
### 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 '<', "<html>..." 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이 잘못된 값으로 등록되어 있어 재생성
|