From a2b0df89187b31e782af351e5fa5b4191d509b59 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Dec 2025 15:52:08 +0000 Subject: [PATCH 1/2] docs: Add comprehensive Kubernetes architecture overview Add detailed documentation explaining how all K8s components work together to form a complete service, using example-voting-app as a practical reference. Covers traffic flow, workload hierarchy, service types, config management, storage, HPA, service mesh, monitoring, logging, and CI/CD pipelines. --- docs/k8s-architecture-overview.md | 863 ++++++++++++++++++++++++++++++ 1 file changed, 863 insertions(+) create mode 100644 docs/k8s-architecture-overview.md diff --git a/docs/k8s-architecture-overview.md b/docs/k8s-architecture-overview.md new file mode 100644 index 0000000000..daa2832b88 --- /dev/null +++ b/docs/k8s-architecture-overview.md @@ -0,0 +1,863 @@ +# Kubernetes 전체 서비스 아키텍처 이해하기 + +> 이 문서는 example-voting-app 프로젝트를 기반으로 Kubernetes의 전체 구성 요소들이 어떻게 하나의 서비스를 이루는지 설명합니다. + +## 목차 + +1. [전체 그림: 모든 것이 연결되는 방식](#1-전체-그림-모든-것이-연결되는-방식) +2. [사용자 요청부터 응답까지: 트래픽 흐름](#2-사용자-요청부터-응답까지-트래픽-흐름) +3. [Deployment → ReplicaSet → Pod 계층 구조](#3-deployment--replicaset--pod-계층-구조) +4. [Service 타입별 트래픽 노출 방식](#4-service-타입별-트래픽-노출-방식) +5. [ConfigMap / Secret → Pod 환경변수](#5-configmap--secret--pod-환경변수) +6. [PersistentVolume → PVC → Pod 볼륨](#6-persistentvolume--pvc--pod-볼륨) +7. [Horizontal Pod Autoscaler (HPA)](#7-horizontal-pod-autoscaler-hpa) +8. [Ingress Controller & Service Mesh](#8-ingress-controller--service-mesh) +9. [모니터링 스택 (Prometheus + Grafana)](#9-모니터링-스택-prometheus--grafana) +10. [로깅 스택](#10-로깅-스택) +11. [CI/CD 파이프라인](#11-cicd-파이프라인) +12. [모든 것이 연결된 전체 그림](#12-모든-것이-연결된-전체-그림) + +--- + +## 1. 전체 그림: 모든 것이 연결되는 방식 + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ 사용자 요청 흐름 │ +└─────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────────────┐ +│ ① 외부 트래픽 진입점 │ +│ ┌─────────────┐ ┌─────────────────┐ ┌──────────────────────────────────┐ │ +│ │ 사용자 │ ──▶ │ Load Balancer │ ──▶ │ Ingress Controller │ │ +│ │ (브라우저) │ │ (클라우드 LB) │ │ (nginx-ingress / istio-gateway) │ │ +│ └─────────────┘ └─────────────────┘ └──────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────────────┐ +│ ② 서비스 라우팅 계층 │ +│ ┌─────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Ingress Rules │ │ +│ │ vote.example.com ──▶ vote-service:8080 │ │ +│ │ result.example.com ──▶ result-service:8080 │ │ +│ └─────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────┴────────────┐ │ +│ ▼ ▼ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ Service: vote │ │ Service: result │ │ +│ │ type: NodePort/ClusterIP │ │ type: NodePort/ClusterIP │ │ +│ │ port: 8080 → 80 │ │ port: 8080 → 80 │ │ +│ └────────────────────────────┘ └────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────────────┐ +│ ③ 워크로드 계층 (Deployment → ReplicaSet → Pod) │ +│ │ +│ Deployment (vote) Deployment (result) │ +│ │ │ │ +│ ▼ ▼ │ +│ ReplicaSet ReplicaSet │ +│ (자동 생성) (자동 생성) │ +│ │ │ │ +│ ┌────┴────┐ ┌────┴────┐ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ +│ │Pod 1│ │Pod 2│ ◀── HPA 스케일링 ──▶ │Pod 1│ │Pod 2│ │ +│ └─────┘ └─────┘ └─────┘ └─────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────────────┐ +│ ④ 백엔드 서비스 계층 │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ redis-service │ │ db-service │ │ +│ │ port: 6379 │ │ port: 5432 │ │ +│ └────────┬────────┘ └────────┬────────┘ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Redis Pod │ │ Postgres Pod │ │ +│ │ + Volume │ │ + Volume │ │ +│ └─────────────────┘ └─────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────────────┐ +│ ⑤ 설정 및 스토리지 계층 │ +│ │ +│ ConfigMap Secret PV/PVC │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────────────────────┐ │ +│ │ DB_HOST │ │ DB_PASS │ │ PersistentVolume │ │ +│ │ REDIS │ │ API_KEY │ │ ▲ │ │ +│ │ _HOST │ │ TLS_CERT │ │ │ Bound │ │ +│ └────┬─────┘ └────┬─────┘ │ PersistentVolumeClaim │ │ +│ │ │ │ ▲ │ │ +│ └────────┬────────┘ │ │ Mount │ │ +│ ▼ │ Pod │ │ +│ Pod 환경변수 └───────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 사용자 요청부터 응답까지: 트래픽 흐름 + +### example-voting-app의 실제 흐름 + +``` +사용자 (브라우저) + │ + │ HTTP 요청: http://노드IP:31000 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ NodePort Service (vote-service) │ +│ - 외부 포트: 31000 (모든 노드에서 접근 가능) │ +│ - 서비스 포트: 8080 │ +│ - 타겟 포트: 80 (Pod의 컨테이너 포트) │ +└─────────────────────────────────────────────────────────┘ + │ + │ label selector: app=vote + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Pod (vote) │ +│ - Python Flask 앱 │ +│ - 투표 데이터를 Redis에 저장 │ +└─────────────────────────────────────────────────────────┘ + │ + │ 내부 통신: redis:6379 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Redis Pod │ +│ - 투표 데이터 큐잉 │ +└─────────────────────────────────────────────────────────┘ + │ + │ Worker가 폴링 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Worker Pod │ +│ - Redis → PostgreSQL로 데이터 이동 │ +└─────────────────────────────────────────────────────────┘ + │ + │ 내부 통신: db:5432 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ PostgreSQL Pod │ +│ - 영구 저장 │ +└─────────────────────────────────────────────────────────┘ + │ + │ Result 앱이 조회 + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Result Pod → result-service:31001 → 사용자 │ +│ - 실시간 결과 표시 (WebSocket) │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Deployment → ReplicaSet → Pod 계층 구조 + +### 현재 프로젝트의 vote-deployment.yaml + +```yaml +apiVersion: apps/v1 +kind: Deployment # ① 최상위: 선언적 업데이트 관리 +metadata: + name: vote +spec: + replicas: 1 # 원하는 Pod 수 + selector: + matchLabels: + app: vote + template: # Pod 템플릿 + spec: + containers: + - image: dockersamples/examplevotingapp_vote + name: vote +``` + +### 계층 구조가 필요한 이유 + +``` +Deployment (원하는 상태 선언) + │ + │ "나는 vote 앱이 3개 실행되길 원해" + │ + ▼ +ReplicaSet (복제 관리) + │ + │ "현재 Pod가 1개뿐이네, 2개 더 만들어야지" + │ "Pod 하나가 죽었네, 새로 만들어야지" + │ + ├──▶ Pod 1 (실제 컨테이너) + ├──▶ Pod 2 (실제 컨테이너) + └──▶ Pod 3 (실제 컨테이너) +``` + +### 롤링 업데이트 시나리오 + +``` +이미지 업데이트: v1 → v2 + +Deployment + │ + ├── ReplicaSet-v1 (이전) ReplicaSet-v2 (새로운) + │ │ │ + │ Pod(v1) ───────────────▶ Pod(v2) ← 점진적 교체 + │ Pod(v1) ───────────────▶ Pod(v2) + │ Pod(v1) ───────────────▶ Pod(v2) + │ + └── 롤백 가능: kubectl rollout undo +``` + +--- + +## 4. Service 타입별 트래픽 노출 방식 + +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ Service 타입 비교 │ +├───────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ClusterIP (기본값) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 클러스터 내부에서만 접근 가능 │ │ +│ │ 예: redis-service, db-service │ │ +│ │ │ │ +│ │ vote-pod ──▶ redis:6379 ──▶ redis-pod │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ NodePort (현재 프로젝트에서 사용) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 모든 노드의 특정 포트로 외부 노출 │ │ +│ │ │ │ +│ │ 외부 ──▶ Node1:31000 ──┐ │ │ +│ │ 외부 ──▶ Node2:31000 ──┼──▶ vote-service ──▶ vote-pod │ │ +│ │ 외부 ──▶ Node3:31000 ──┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ LoadBalancer (클라우드 환경) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 클라우드 LB가 자동 프로비저닝 │ │ +│ │ │ │ +│ │ 외부 ──▶ AWS ELB/GCP LB ──▶ Service ──▶ Pods │ │ +│ │ (퍼블릭 IP) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ Ingress (HTTP/HTTPS 라우팅) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 도메인/경로 기반 라우팅 + TLS 종료 │ │ +│ │ │ │ +│ │ vote.example.com ──┐ │ │ +│ │ ├──▶ Ingress ──┬──▶ vote-service │ │ +│ │ result.example.com─┘ └──▶ result-service │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. ConfigMap / Secret → Pod 환경변수 + +### 현재 프로젝트의 하드코딩된 값 vs 모범 사례 + +```yaml +# 현재 (하드코딩 - 권장하지 않음) +# db-deployment.yaml +env: +- name: POSTGRES_USER + value: postgres +- name: POSTGRES_PASSWORD + value: postgres # ❌ 비밀번호가 YAML에 노출됨 +``` + +### 모범 사례: ConfigMap + Secret 사용 + +```yaml +# configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + DB_HOST: "db" + REDIS_HOST: "redis" + LOG_LEVEL: "info" + +--- +# secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: db-secret +type: Opaque +data: + POSTGRES_PASSWORD: cG9zdGdyZXM= # base64 인코딩됨 + +--- +# deployment에서 사용 +spec: + containers: + - name: app + envFrom: + - configMapRef: + name: app-config # 모든 ConfigMap 키를 환경변수로 + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-secret + key: POSTGRES_PASSWORD # 개별 Secret 키 +``` + +### 데이터 흐름 + +``` +┌──────────────┐ ┌──────────────┐ +│ ConfigMap │ │ Secret │ +│ (평문 설정) │ │ (민감 정보) │ +└──────┬───────┘ └──────┬───────┘ + │ │ + └────────┬───────────┘ + │ + ▼ + ┌───────────────┐ + │ Pod │ + │ ┌─────────┐ │ + │ │Container│ │ + │ │ │ │ + │ │ $DB_HOST│ │ ← 환경변수로 주입 + │ │ $DB_PASS│ │ + │ └─────────┘ │ + └───────────────┘ +``` + +--- + +## 6. PersistentVolume → PVC → Pod 볼륨 + +### 현재 프로젝트 (EmptyDir - 휘발성) + +```yaml +# redis-deployment.yaml +volumeMounts: +- mountPath: /data + name: redis-data +volumes: +- name: redis-data + emptyDir: {} # ⚠️ Pod 재시작시 데이터 손실! +``` + +### 프로덕션: PersistentVolume 사용 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 스토리지 추상화 계층 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ StorageClass │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ "어떤 종류의 스토리지를 어떻게 프로비저닝할지 정의" │ │ +│ │ - standard: 기본 디스크 │ │ +│ │ - ssd: SSD 스토리지 │ │ +│ │ - nfs: 네트워크 파일 시스템 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ PersistentVolume (PV) - 클러스터 리소스 (관리자 생성) │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 실제 물리적 스토리지 (AWS EBS, GCP PD, NFS 등) │ │ +│ │ capacity: 10Gi │ │ +│ │ accessModes: [ReadWriteOnce] │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ Bound (바인딩) │ +│ ▼ │ +│ PersistentVolumeClaim (PVC) - 네임스페이스 리소스 (사용자 요청) │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ "10Gi 용량의 ReadWriteOnce 스토리지가 필요해요" │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ Mount (마운트) │ +│ ▼ │ +│ Pod │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ volumes: │ │ +│ │ - name: db-storage │ │ +│ │ persistentVolumeClaim: │ │ +│ │ claimName: postgres-pvc │ │ +│ │ │ │ +│ │ containers: │ │ +│ │ - volumeMounts: │ │ +│ │ - mountPath: /var/lib/postgresql/data │ │ +│ │ name: db-storage │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 7. Horizontal Pod Autoscaler (HPA) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ HPA 동작 원리 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Metrics Server │ +│ │ │ +│ │ CPU/메모리 메트릭 수집 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ HPA Controller │ │ +│ │ │ │ +│ │ 현재 CPU: 80% │ │ +│ │ 목표 CPU: 50% │ │ +│ │ │ │ +│ │ 필요한 replicas = 현재 replicas × (80/50) = 1.6 → 2 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ replicas 조정 │ +│ ▼ │ +│ Deployment (vote) │ +│ │ │ +│ ┌────┴────────────┐ │ +│ ▼ ▼ │ +│ ┌─────┐ ┌─────┐ │ +│ │Pod 1│ ──────▶ │Pod 2│ ← 자동 스케일 아웃 │ +│ └─────┘ └─────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### HPA 설정 예시 + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: vote-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: vote + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 +``` + +--- + +## 8. Ingress Controller & Service Mesh + +### Ingress (L7 라우팅) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ Ingress 아키텍처 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 인터넷 │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Ingress Controller (nginx/traefik) │ │ +│ │ (Deployment + Service LoadBalancer) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ Ingress 리소스 규칙 적용 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Ingress Rules │ │ +│ │ │ │ +│ │ Host: vote.example.com │ │ +│ │ Path: / │ │ +│ │ Backend: vote-service:8080 │ │ +│ │ │ │ +│ │ Host: result.example.com │ │ +│ │ Path: / │ │ +│ │ Backend: result-service:8080 │ │ +│ │ │ │ +│ │ TLS: │ │ +│ │ Secret: tls-secret (cert + key) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### Service Mesh (Istio) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ Service Mesh (Istio) 아키텍처 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Control Plane (istiod) │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ - 설정 배포 │ │ +│ │ - 인증서 관리 │ │ +│ │ - 서비스 디스커버리 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ 설정 푸시 │ +│ Data Plane (Envoy Sidecars) │ +│ │ +│ ┌───────────────────────┐ ┌───────────────────────┐ │ +│ │ Pod (vote) │ │ Pod (redis) │ │ +│ │ ┌───────┐ ┌─────────┐ │ │ ┌───────┐ ┌─────────┐ │ │ +│ │ │ vote │◀│ Envoy │◀├────►│ │ Envoy │▶│ redis │ │ │ +│ │ │ app │ │ sidecar │ │ │ │sidecar│ │ │ │ │ +│ │ └───────┘ └─────────┘ │ │ └───────┘ └─────────┘ │ │ +│ └───────────────────────┘ └───────────────────────┘ │ +│ │ +│ Envoy가 제공하는 기능: │ +│ ✓ mTLS (서비스간 암호화) │ +│ ✓ 트래픽 관리 (카나리 배포, A/B 테스트) │ +│ ✓ 관측성 (분산 추적, 메트릭) │ +│ ✓ 서킷 브레이커, 재시도, 타임아웃 │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 9. 모니터링 스택 (Prometheus + Grafana) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 모니터링 아키텍처 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ Grafana │ ◀── 대시보드 + 알림 설정 │ +│ │ (시각화) │ │ +│ └────────┬────────┘ │ +│ │ PromQL 쿼리 │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ Prometheus │ ◀── 메트릭 저장소 (시계열 DB) │ +│ │ (수집+저장) │ │ +│ └────────┬────────┘ │ +│ │ Pull 방식 수집 (scrape) │ +│ │ │ +│ ┌────────┼────────┬────────────────────┬──────────────────┐ │ +│ ▼ ▼ ▼ ▼ ▼ │ +│ ┌─────┐ ┌─────┐ ┌─────────────┐ ┌───────────┐ ┌──────────────┐ │ +│ │vote │ │redis│ │ kube-state- │ │ node- │ │ cadvisor │ │ +│ │/met │ │/met │ │ metrics │ │ exporter │ │(컨테이너메트릭)│ │ +│ │rics │ │rics │ │(k8s객체상태)│ │(노드메트릭)│ │ │ │ +│ └─────┘ └─────┘ └─────────────┘ └───────────┘ └──────────────┘ │ +│ │ +│ 수집되는 메트릭 예시: │ +│ - container_cpu_usage_seconds_total │ +│ - container_memory_usage_bytes │ +│ - kube_pod_status_phase │ +│ - node_cpu_seconds_total │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### Alertmanager 연동 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ Alertmanager 연동 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Prometheus Alertmanager │ +│ ┌──────────────────┐ ┌──────────────────────────────────┐ │ +│ │ Alert Rules: │ ────▶ │ - 중복 제거 (Dedup) │ │ +│ │ │ │ - 그룹화 (Grouping) │ │ +│ │ - Pod 죽음 │ │ - 라우팅 (Routing) │ │ +│ │ - 메모리 > 90% │ │ - 알림 전송 │ │ +│ │ - 5xx 에러 급증 │ └──────────────┬───────────────────┘ │ +│ └──────────────────┘ │ │ +│ ▼ │ +│ ┌─────────┬─────────┬─────────┐ │ +│ │ Slack │ Email │PagerDuty│ │ +│ └─────────┴─────────┴─────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 10. 로깅 스택 + +### ELK Stack + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ ELK Stack │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ Kibana │ ◀── 로그 검색 + 시각화 │ +│ │ (UI/검색) │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ Elasticsearch │ ◀── 로그 인덱싱 + 저장 │ +│ │ (저장/검색) │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ Logstash │ ◀── 로그 파싱 + 변환 │ +│ │ (처리/변환) │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌───────────────────────┼───────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ Fluentd │ │ Fluentd │ │ Fluentd │ │ +│ │(DaemonSet) │ │(DaemonSet) │ │(DaemonSet) │ │ +│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ +│ │ │ │ │ +│ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ +│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │ +│ │ │ │ │ │ │ │ +│ │ vote-pod │ │ redis-pod │ │ db-pod │ │ +│ │ stdout │ │ stdout │ │ stdout │ │ +│ └───────────┘ └───────────┘ └───────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### Loki Stack (경량 대안) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ Loki Stack │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ +│ │ Grafana │ ◀── │ Loki │ ◀── │ Promtail │ │ +│ │ (시각화) │ │ (저장소) │ │ (로그 수집) │ │ +│ └─────────────┘ └─────────────┘ └───────────────┘ │ +│ │ │ +│ 장점: DaemonSet으로 │ +│ - Prometheus와 통합 각 노드에서 실행 │ +│ - 경량 (인덱싱 안함) │ +│ - 라벨 기반 쿼리 │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 11. CI/CD 파이프라인 + +### 현재 프로젝트의 GitHub Actions + +```yaml +# .github/workflows/call-docker-build-vote.yaml +name: Build Vote +on: + push: + branches: ['main'] + paths: ['vote/**'] + +jobs: + call-docker-build: + uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main + with: + dockerhub-enable: true + ghcr-enable: true + image-names: | + ghcr.io/dockersamples/example-voting-app-vote + dockersamples/examplevotingapp_vote +``` + +### 전체 CI/CD 파이프라인 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ CI/CD 파이프라인 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 개발자 │ +│ │ │ +│ │ git push │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ GitHub / GitLab │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ CI Pipeline (GitHub Actions) │ │ │ +│ │ │ │ │ │ +│ │ │ 1. 코드 체크아웃 │ │ │ +│ │ │ 2. 테스트 실행 │ │ │ +│ │ │ 3. Docker 이미지 빌드 │ │ │ +│ │ │ 4. 이미지 레지스트리 푸시 (GHCR, DockerHub) │ │ │ +│ │ │ 5. 매니페스트 업데이트 (GitOps 방식) │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 이미지 태그 변경 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ CD (GitOps - ArgoCD / Flux) │ │ +│ │ │ │ +│ │ Git 저장소 (k8s-manifests) │ │ +│ │ │ │ │ +│ │ │ 감시 (Watch) │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ ArgoCD │ │ │ +│ │ │ │ │ │ +│ │ │ Git 상태 ←→ 클러스터 상태 비교 │ │ │ +│ │ │ │ │ │ +│ │ │ 차이 발견 → 자동 동기화 (Sync) │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ │ kubectl apply │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ Kubernetes Cluster │ │ │ +│ │ │ │ │ │ +│ │ │ vote-deployment (새 이미지로 롤아웃)│ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. 모든 것이 연결된 전체 그림 + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Production Kubernetes 환경 │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + + ┌──────────────┐ + │ 개발자 │ + └──────┬───────┘ + │ git push + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ CI/CD Layer │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ GitHub Actions │───▶│ Container │───▶│ ArgoCD │ │ +│ │ (빌드/테스트) │ │ Registry │ │ (GitOps 배포) │ │ +│ └──────────────────┘ └──────────────────┘ └────────┬─────────┘ │ +└──────────────────────────────────────────────────────────┬┬─────────────────────────────┘ + ││ + ┌──────────────┐ ││ + │ 사용자 │ ││ + └──────┬───────┘ ││ + │ ││ + ▼ ▼▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ingress Layer │ +│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ LoadBalancer → Ingress Controller (nginx) → TLS 종료 → 라우팅 │ │ +│ └──────────────────────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────┬────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Service Mesh Layer (Optional: Istio/Linkerd) │ +│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ mTLS │ 트래픽 관리 │ 서킷 브레이커 │ 분산 추적 │ │ +│ └──────────────────────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────┬────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Service Layer │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │vote-service │ │result-service│ │redis-service │ │ db-service │ │ +│ │ (NodePort) │ │ (NodePort) │ │ (ClusterIP) │ │ (ClusterIP) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ │ +└──────────┼───────────────────┼───────────────────┼───────────────────┼──────────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Workload Layer │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────────────┐ │ +│ │ HPA (Auto Scaling) │ │ +│ └───────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ Deployment─────────▶ ReplicaSet ──────▶ Pods │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ vote pods │ │ result pods │ │ worker pods │ │ redis/db │ │ +│ │ (Python) │ │ (Node.js) │ │ (.NET) │ │ pods │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ +└───────────────────────────────────────────────────────────────────────┼─────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Config & Storage Layer │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────────────┐ │ +│ │ ConfigMap │ │ Secret │ │ PV ←─ PVC ←─ Pod Volume Mount │ │ +│ │ (설정값) │ │ (비밀번호) │ │ (영구 스토리지) │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ Observability Layer │ +│ │ +│ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ │ +│ │ Monitoring │ │ Logging │ │ Tracing │ │ +│ │ │ │ │ │ │ │ +│ │ Prometheus + Grafana │ │ Loki/ELK + Fluentd │ │ Jaeger / Zipkin │ │ +│ │ │ │ │ │ │ │ +│ │ - CPU/Memory 메트릭 │ │ - 컨테이너 로그 수집 │ │ - 분산 요청 추적 │ │ +│ │ - 커스텀 메트릭 │ │ - 중앙 로그 검색 │ │ - 지연 시간 분석 │ │ +│ │ - 알림 (Alertmanager) │ │ - 로그 분석 │ │ - 에러 추적 │ │ +│ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 핵심 요약 + +| 구성 요소 | 역할 | 현재 프로젝트 상태 | +|----------|------|------------------| +| **Deployment** | 애플리케이션 배포 관리 | ✅ 구현됨 | +| **Service** | 네트워크 접근 제공 | ✅ NodePort 사용 | +| **Ingress** | HTTP/HTTPS 라우팅 | ❌ 미구현 (직접 NodePort 사용) | +| **ConfigMap/Secret** | 설정 분리 | ⚠️ 하드코딩됨 | +| **PV/PVC** | 영구 스토리지 | ⚠️ EmptyDir 사용 (휘발성) | +| **HPA** | 자동 스케일링 | ❌ 미구현 | +| **Service Mesh** | 서비스간 통신 관리 | ❌ 미구현 | +| **Monitoring** | 메트릭 수집/시각화 | ❌ 미구현 | +| **Logging** | 로그 수집/분석 | ❌ 미구현 | +| **CI/CD** | 자동 빌드/배포 | ✅ GitHub Actions | + +--- + +## 결론 + +이 모든 구성 요소들이 **하나의 목표**를 위해 협력합니다: + +> **"안정적이고, 확장 가능하며, 관측 가능한 서비스를 운영하는 것"** + +각 레이어는 독립적으로 동작하면서도 서로 유기적으로 연결되어 있습니다. 처음부터 모든 것을 구현할 필요는 없으며, 서비스의 성숙도와 요구사항에 따라 점진적으로 추가해 나가면 됩니다. From e7d9ee0d1e0618a662e3dc79eeddd27409becd54 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Dec 2025 16:03:48 +0000 Subject: [PATCH 2/2] docs: Add DOCX version of K8s architecture overview Add Word document format for easier download and sharing. --- docs/k8s-architecture-overview.docx | Bin 0 -> 22325 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/k8s-architecture-overview.docx diff --git a/docs/k8s-architecture-overview.docx b/docs/k8s-architecture-overview.docx new file mode 100644 index 0000000000000000000000000000000000000000..eaaf75dda12e39fc8b740eb5a8b3d24df7b072e9 GIT binary patch literal 22325 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 literal 0 HcmV?d00001