<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>arnold_99.log</title>
        <link>https://velog.io/</link>
        <description>기록하고 공유하려고 노력하는 DevOps 엔지니어</description>
        <lastBuildDate>Sat, 14 Mar 2026 11:09:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>arnold_99.log</title>
            <url>https://velog.velcdn.com/images/arnold_99/profile/d5f8094e-93d7-4570-8375-25507dfff12b/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. arnold_99.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/arnold_99" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[KubeVirt + Apache Guacamole로 구축하는 통합 인프라 접속 포털]]></title>
            <link>https://velog.io/@arnold_99/KubeVirt-Apache-Guacamole%EB%A1%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EB%8A%94-%ED%86%B5%ED%95%A9-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%A0%91%EC%86%8D-%ED%8F%AC%ED%84%B8</link>
            <guid>https://velog.io/@arnold_99/KubeVirt-Apache-Guacamole%EB%A1%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EB%8A%94-%ED%86%B5%ED%95%A9-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%A0%91%EC%86%8D-%ED%8F%AC%ED%84%B8</guid>
            <pubDate>Sat, 14 Mar 2026 11:09:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/arnold_99/post/3a2e64be-0c3e-4efc-8480-ed7f28d5c8ed/image.png" alt=""></p>
<blockquote>
<p>Kubernetes 위에서 가상머신을 운영하고, 웹 브라우저 하나로 모든 인프라에 접속하는 플랫폼을 구축한 이야기</p>
</blockquote>
<hr>
<h2 id="1-문제-정의-인프라-접속의-파편화">1. 문제 정의: 인프라 접속의 파편화</h2>
<p>IDC에 7대의 물리 서버, 그 위에 Kubernetes 클러스터, 그리고 KubeVirt로 운영하는 가상머신들. 인프라가 커질수록 접속 관리는 복잡해집니다.</p>
<p><strong>기존의 접속 방식:</strong></p>
<pre><code>개발자 A → SSH 클라이언트 → 서버1 (IP 직접 입력)
개발자 A → SSH 클라이언트 → 서버2 (IP 직접 입력)
개발자 B → RDP 클라이언트 → Windows VM (별도 클라이언트 필요)
개발자 C → VNC 클라이언트 → Linux VM (또 다른 클라이언트)
운영팀   → SSH 터널 → VPN → 서버3 (복잡한 경로)</code></pre><p><strong>문제점:</strong></p>
<ul>
<li>접속 정보(IP, 포트, 계정)가 개인별로 분산 관리</li>
<li>SSH, RDP, VNC 각각 다른 클라이언트 필요</li>
<li>접속 이력 추적 불가 (누가 언제 어디에 접속했는지)</li>
<li>새 팀원 온보딩 시 접속 정보 전달이 번거로움</li>
<li>VPN + SSH 터널 등 복잡한 네트워크 경로</li>
</ul>
<p><strong>목표:</strong></p>
<ul>
<li>웹 브라우저 하나로 모든 인프라 접속</li>
<li>중앙 집중식 접속 관리 및 권한 제어</li>
<li>접속 이력 자동 기록</li>
<li>Kubernetes 네이티브 배포 (GitOps 호환)</li>
</ul>
<hr>
<h2 id="2-기술-선택-왜-kubevirt--apache-guacamole인가">2. 기술 선택: 왜 KubeVirt + Apache Guacamole인가?</h2>
<h3 id="21-kubevirt-kubernetes-위의-가상머신">2.1 KubeVirt: Kubernetes 위의 가상머신</h3>
<p>컨테이너화할 수 없는 워크로드(Windows, 레거시 애플리케이션, GPU 패스스루 테스트 등)를 위해 별도의 하이퍼바이저를 운영하는 대신, KubeVirt를 선택했습니다.</p>
<table>
<thead>
<tr>
<th>비교</th>
<th>별도 하이퍼바이저 (ESXi 등)</th>
<th>KubeVirt</th>
</tr>
</thead>
<tbody><tr>
<td>인프라 분리</td>
<td>VM과 컨테이너가 별도 관리</td>
<td>동일 Kubernetes 클러스터</td>
</tr>
<tr>
<td>스토리지</td>
<td>vSAN, VMFS 등 별도</td>
<td>Longhorn CSI 공유</td>
</tr>
<tr>
<td>네트워크</td>
<td>vSwitch, NSX 등 별도</td>
<td>Pod 네트워크 통합</td>
</tr>
<tr>
<td>모니터링</td>
<td>vCenter 별도</td>
<td>Prometheus 통합</td>
</tr>
<tr>
<td>GitOps</td>
<td>불가</td>
<td>CRD 기반 완전 지원</td>
</tr>
<tr>
<td>라이선스</td>
<td>유료 (vSphere)</td>
<td>오픈소스 (CNCF)</td>
</tr>
</tbody></table>
<h3 id="22-apache-guacamole-클라이언트리스-원격-접속">2.2 Apache Guacamole: 클라이언트리스 원격 접속</h3>
<p>Apache Guacamole는 <strong>클라이언트 설치 없이</strong> 웹 브라우저만으로 SSH, RDP, VNC, Telnet 접속을 지원하는 게이트웨이입니다.</p>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIEJSWyJCcm93c2VyIChIVE1MNSk8YnIvPldlYlNvY2tldCAvIEhUVFAgVHVubmVsIl0gLS0-IEdDWyJHdWFjYW1vbGUgV2ViIEFwcDxici8-KFRvbWNhdCA6ODA4MCk8YnIvPlJFU1QgQVBJICsgV2ViU29ja2V0Il0KICAgIEdDIC0tPiBHRFsiZ3VhY2QgKEMgRGFlbW9uIDo0ODIyKTxici8-7ZSE66Gc7Yag7L2cIOu4jOumv-yngCJdCiAgICBHRCAtLT4gU1NIWyJTU0ggOjIyIl0KICAgIEdEIC0tPiBSRFBbIlJEUCA6MzM4OSJdCiAgICBHRCAtLT4gVk5DWyJWTkMgOjU5MDAiXQ==" alt="diagram-1"></p>
<p><strong>Guacamole의 핵심 장점:</strong></p>
<ul>
<li><strong>Zero-install</strong>: 브라우저만 있으면 됨 (WebSocket 기반)</li>
<li><strong>프로토콜 통합</strong>: SSH, RDP, VNC, Telnet을 단일 인터페이스로</li>
<li><strong>REST API</strong>: 프로그래밍 가능한 커넥션/사용자 관리</li>
<li><strong>세션 녹화</strong>: 접속 이력 및 세션 리플레이 지원</li>
<li><strong>RBAC</strong>: 사용자/그룹별 접속 권한 제어</li>
<li><strong>Kubernetes 호환</strong>: 컨테이너 이미지 제공</li>
</ul>
<hr>
<h2 id="3-kubevirt-아키텍처-상세">3. KubeVirt 아키텍처 상세</h2>
<h3 id="31-컴포넌트-구성">3.1 컴포넌트 구성</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIEs4U1siS3ViZXJuZXRlcyBDbHVzdGVyIl0KICAgICAgICBzdWJncmFwaCBLVlsiS3ViZVZpcnQgT3BlcmF0b3IgdjEuNy4wIl0KICAgICAgICAgICAgVk9bInZpcnQtb3BlcmF0b3IgKGxpZmVjeWNsZSkiXQogICAgICAgICAgICBWQ1sidmlydC1jb250cm9sbGVyIChzY2hlZHVsaW5nKSJdCiAgICAgICAgICAgIFZIWyJ2aXJ0LWhhbmRsZXIgKERhZW1vblNldCwgS1ZNKSJdCiAgICAgICAgICAgIFZBWyJ2aXJ0LWFwaSAoQ1JEIHdlYmhvb2tzKSJdCiAgICAgICAgZW5kCiAgICAgICAgc3ViZ3JhcGggQ0RJWyJDREkgdjEuNjQuMCJdCiAgICAgICAgICAgIENDWyJjZGktY29udHJvbGxlciJdCiAgICAgICAgICAgIENVWyJjZGktdXBsb2FkcHJveHkiXQogICAgICAgICAgICBDSVsiY2RpLWltcG9ydGVyIChIVFRQL1JlZ2lzdHJ5IOKGkiBQVkMpIl0KICAgICAgICBlbmQKICAgICAgICBzdWJncmFwaCBNR01UWyJrdWJldmlydC1tYW5hZ2VyIChEYXNoYm9hcmQpIl0KICAgICAgICAgICAgVUlbIldlYiBVSSBmb3IgVk0gQ1JVRCJdCiAgICAgICAgICAgIFZOQ1siTm9WTkMgY29uc29sZSAoV2ViU29ja2V0KSJdCiAgICAgICAgICAgIEFVVEhbIkJhc2ljIEF1dGggKE5HSU5YIGh0cGFzc3dkKSJdCiAgICAgICAgZW5kCiAgICAgICAgVk0xWyJ0ZXN0LXZtLTE8YnIvPlVidW50dSAyQy80R2k8YnIvPlNTSDoyMiJdCiAgICAgICAgVk0yWyJ0ZXN0LXZtLTI8YnIvPlVidW50dSAyQy80R2k8YnIvPlNTSDoyMiJdCiAgICAgICAgV0lOWyJ3aW4xMTxici8-V2luZG93cyAxMSA4Qy8xNkdpPGJyLz5SRFA6MzM4OSJdCiAgICBlbmQKICAgIEtWIC0tPiBWTTEgJiBWTTIgJiBXSU4KICAgIENESSAtLT4gVk0xICYgVk0yICYgV0lO" alt="diagram-2"></p>
<h3 id="32-vm-스토리지-longhorn-vm-storageclass">3.2 VM 스토리지: Longhorn-VM StorageClass</h3>
<p>VM 디스크는 일반 애플리케이션과 다른 스토리지 전략이 필요합니다:</p>
<pre><code class="language-yaml">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-vm
provisioner: driver.longhorn.io
parameters:
  numberOfReplicas: &quot;1&quot;          # VM은 단일 복제본 (성능 우선)
  dataLocality: &quot;best-effort&quot;    # 가능하면 VM이 실행되는 노드에 데이터 배치
  staleReplicaTimeout: &quot;30&quot;
  recurringJobSelector: &#39;[{&quot;name&quot;:&quot;vm-daily-backup&quot;,&quot;isGroup&quot;:true}]&#39;
allowVolumeExpansion: true        # 디스크 온라인 확장 지원
reclaimPolicy: Delete
volumeBindingMode: Immediate</code></pre>
<p><strong>일반 Longhorn(3 replica) 대신 단일 복제본을 사용한 이유:</strong></p>
<ul>
<li>VM 디스크 I/O는 매우 빈번 → 3중 복제는 쓰기 성능 3배 저하</li>
<li>VM 자체에 스냅샷/백업 전략 적용 (recurringJobSelector)</li>
<li><code>dataLocality: best-effort</code>로 네트워크 I/O 최소화</li>
<li>VM이 stateless가 아닌 경우, 별도 백업 정책으로 데이터 보호</li>
</ul>
<h3 id="33-vm-정의-crd-기반-선언적-관리">3.3 VM 정의: CRD 기반 선언적 관리</h3>
<p>KubeVirt의 핵심은 VM을 Kubernetes CRD로 정의한다는 것입니다:</p>
<pre><code class="language-yaml">apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: ubuntu-dev-01
  namespace: kubevirt-test
spec:
  runStrategy: Always              # 항상 실행 상태 유지

  dataVolumeTemplates:             # CDI: 이미지 자동 다운로드 → PVC 생성
    - metadata:
        name: ubuntu-dev-01-rootdisk
      spec:
        storage:
          storageClassName: longhorn-vm
          resources:
            requests:
              storage: 30Gi
          accessModes:
            - ReadWriteOnce
        source:
          http:
            url: &quot;https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img&quot;

  template:
    metadata:
      labels:
        app: ubuntu-dev
    spec:
      domain:
        cpu:
          cores: 4
        resources:
          requests:
            memory: 8Gi
        devices:
          disks:
            - name: rootdisk
              disk:
                bus: virtio          # 최적 성능 (paravirtualized)
            - name: cloudinitdisk
              disk:
                bus: virtio
          interfaces:
            - name: default
              masquerade: {}         # Pod 네트워크 NAT

      networks:
        - name: default
          pod: {}                    # Kubernetes Pod 네트워크 사용

      volumes:
        - name: rootdisk
          dataVolume:
            name: ubuntu-dev-01-rootdisk
        - name: cloudinitdisk
          cloudInitNoCloud:
            userData: |
              #cloud-config
              hostname: ubuntu-dev-01
              user: ubuntu
              ssh_authorized_keys:
                - ssh-ed25519 AAAA... admin@company
              package_update: true
              packages:
                - net-tools
                - curl
                - vim
                - htop</code></pre>
<p><strong>이것이 GitOps와 완벽히 호환되는 이유:</strong></p>
<ul>
<li>VM 정의가 YAML 파일 → Git에 커밋 가능</li>
<li>ArgoCD가 변경 감지 → 자동 적용</li>
<li><code>runStrategy</code>로 VM 시작/정지 제어 가능</li>
<li><code>dataVolumeTemplates</code>로 OS 이미지 자동 프로비저닝</li>
</ul>
<h3 id="34-windows-vm-uefi--secure-boot--tpm">3.4 Windows VM: UEFI + Secure Boot + TPM</h3>
<p>Windows 11은 특별한 하드웨어 요구사항이 있습니다:</p>
<pre><code class="language-yaml">apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: win11-workstation
spec:
  runStrategy: RerunOnFailure
  template:
    spec:
      domain:
        cpu:
          cores: 8
        resources:
          requests:
            memory: 16Gi
        features:
          smm:
            enabled: true            # System Management Mode
          tpm: {}                    # TPM 2.0 가상 디바이스
        firmware:
          bootloader:
            efi:
              secureBoot: true       # UEFI Secure Boot
        machine:
          type: q35                   # 최신 머신 타입
        devices:
          disks:
            - name: rootdisk
              disk:
                bus: sata            # Windows 호환성
              bootOrder: 1
          interfaces:
            - name: default
              model: e1000e          # Windows 기본 NIC 드라이버
              masquerade: {}</code></pre>
<p><strong>KubeVirt에서 Windows 11을 실행하기 위한 조건:</strong></p>
<ul>
<li><code>firmware.bootloader.efi.secureBoot: true</code> — UEFI Secure Boot 필수</li>
<li><code>features.tpm: {}</code> — TPM 2.0 에뮬레이션</li>
<li><code>features.smm.enabled: true</code> — SMM (Secure Boot의 전제조건)</li>
<li><code>machine.type: q35</code> — 최신 칩셋 에뮬레이션</li>
<li><code>interfaces.model: e1000e</code> — virtio NIC는 Windows 드라이버 없이 동작 불가</li>
</ul>
<h3 id="35-네트워킹-masquerade-모드">3.5 네트워킹: Masquerade 모드</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIEVYVFsiT3RoZXIgUG9kIC8gZ3VhY2QiXSAtLT58IjEwLjI0NC54Lng6MjIifCBQT0RbInZpcnQtbGF1bmNoZXIgUG9kPGJyLz5Qb2QgSVA6IDEwLjI0NC54LngiXQogICAgUE9EIC0tPnwiaXB0YWJsZXMgTkFUIG1hc3F1ZXJhZGUifCBWTVsiVk0gR3Vlc3QgT1M8YnIvPkludGVybmFsIElQOiAxMC4wLjIuMTUiXQogICAgVk0gLS0tIFAxWyJHdWVzdDoyMiA9IFBvZDoyMiJdCiAgICBWTSAtLS0gUDJbIkd1ZXN0OjMzODkgPSBQb2Q6MzM4OSJd" alt="diagram-3"></p>
<p>Masquerade 모드에서 VM은 내부적으로 10.0.2.x 대역을 사용하지만, iptables NAT를 통해 Pod IP로 매핑됩니다. 따라서 <strong>클러스터 내의 다른 Pod(예: Guacamole의 guacd)에서 Pod IP로 직접 SSH/RDP 접속</strong>이 가능합니다.</p>
<pre><code class="language-bash"># VM의 Pod IP 조회
kubectl get vmi ubuntu-dev-01 -n kubevirt-test \
  -o jsonpath=&#39;{.status.interfaces[0].ipAddress}&#39;
# 출력: 10.244.93.152

# 클러스터 내부에서 직접 SSH 가능
ssh ubuntu@10.244.93.152</code></pre>
<p>이 특성이 Guacamole + KubeVirt 조합을 강력하게 만듭니다 — guacd가 클러스터 내부에서 VM Pod IP로 직접 프로토콜 연결을 맺습니다.</p>
<h3 id="36-cdi-이미지-자동-임포트">3.6 CDI: 이미지 자동 임포트</h3>
<p>CDI(Containerized Data Importer)는 다양한 소스에서 VM 디스크 이미지를 자동으로 PVC에 임포트합니다:</p>
<pre><code class="language-yaml"># HTTP URL에서 Ubuntu 클라우드 이미지 다운로드
source:
  http:
    url: &quot;https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img&quot;

# 컨테이너 레지스트리에서 이미지 Pull
source:
  registry:
    url: &quot;docker://harbor.example.com/vm-images/ubuntu:22.04&quot;

# 기존 PVC 복제 (Golden Image 패턴)
source:
  pvc:
    name: win11-golden-disk
    namespace: kubevirt-test</code></pre>
<p><strong>Golden Image 패턴:</strong></p>
<ol>
<li>Windows 11을 수동으로 한 번 설치 → golden PVC 생성</li>
<li>이후 새 VM 생성 시 golden PVC를 clone → 빠른 프로비저닝</li>
<li>설치 시간: 30분+ → clone 시간: 1-2분</li>
</ol>
<h3 id="37-kubevirt-manager-웹-기반-vm-관리">3.7 kubevirt-manager: 웹 기반 VM 관리</h3>
<p>CLI(<code>virtctl</code>) 대신 웹 UI로 VM을 관리할 수 있는 kubevirt-manager를 배포했습니다:</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>VM 생성/시작/정지/삭제 (CRUD)</li>
<li>NoVNC 웹 콘솔 (브라우저에서 직접 VM 화면 접근)</li>
<li>DataVolume 관리 (디스크 생성, 리사이즈)</li>
<li>네트워크 설정 (Multus 지원)</li>
<li>SSH 키 관리</li>
<li>VM Pool (동일 스펙 VM 다수 생성)</li>
</ul>
<p><strong>인증: NGINX Basic Auth</strong></p>
<pre><code class="language-yaml"># NGINX sidecar가 인증 처리
apiVersion: v1
kind: ConfigMap
metadata:
  name: auth-config
data:
  basicauth.conf: |
    server {
        listen 8080;
        location / {
            auth_basic &quot;KubeVirt Manager&quot;;
            auth_basic_user_file /etc/nginx/secret.d/.htpasswd;
            proxy_pass http://localhost:8080;
        }
    }</code></pre>
<p><strong>Istio Gateway를 통한 HTTPS 접근:</strong></p>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: kubevirt-manager-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - kubevirt-mgr.example.com
      port:
        number: 443
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: kubevirt-mgr-tls    # cert-manager 자동 발급
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: kubevirt-manager-vs
spec:
  hosts:
    - kubevirt-mgr.example.com
  gateways:
    - kubevirt-manager-gateway
  http:
    - route:
        - destination:
            host: kubevirt-manager.kubevirt-manager.svc.cluster.local
            port:
              number: 8080
      timeout: 86400s    # 24시간 — NoVNC 장시간 세션 지원</code></pre>
<p><code>timeout: 86400s</code> 설정은 NoVNC 콘솔 세션이 장시간 유지될 수 있도록 합니다. 기본 Istio timeout(15초)으로는 콘솔 세션이 끊깁니다.</p>
<hr>
<h2 id="4-secern-access-portal-guacamole-백엔드-아키텍처">4. Secern Access Portal: Guacamole 백엔드 아키텍처</h2>
<h3 id="41-helm-chart-설계">4.1 Helm Chart 설계</h3>
<p>단일 Helm Chart로 Guacamole의 3개 컴포넌트를 패키징했습니다:</p>
<pre><code>kubernetes/charts/secern-access-portal/
├── Chart.yaml                          # v0.1.0, appVersion 1.5.5
├── values.yaml                         # 기본값
├── templates/
│   ├── _helpers.tpl                    # 공통 헬퍼 (fullname, labels)
│   ├── postgresql-secret.yaml          # DB 인증정보
│   ├── postgresql-statefulset.yaml     # PostgreSQL 16
│   ├── postgresql-service.yaml         # ClusterIP :5432
│   ├── guacamole-initdb-configmap.yaml # 스키마 초기화 SQL (791줄)
│   ├── guacd-deployment.yaml           # 프로토콜 데몬
│   ├── guacd-service.yaml              # ClusterIP :4822
│   ├── guacamole-deployment.yaml       # 웹 애플리케이션
│   ├── guacamole-service.yaml          # ClusterIP :8080
│   └── guacamole-service-nodeport.yaml # 조건부 NodePort (Staging)</code></pre><h3 id="42-postgresql-스키마-자동-초기화">4.2 PostgreSQL: 스키마 자동 초기화</h3>
<p>Guacamole는 PostgreSQL에 커넥션, 사용자, 권한 정보를 저장합니다. 초기 스키마를 ConfigMap으로 마운트하여 첫 실행 시 자동 초기화합니다:</p>
<pre><code class="language-yaml"># postgresql-statefulset.yaml (핵심 부분)
spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: &quot;false&quot;    # TCP 프로토콜 → Sidecar 제외
    spec:
      initContainers:
        - name: init-check
          image: &quot;{{ .Values.postgresql.image }}&quot;
          command: [&#39;sh&#39;, &#39;-c&#39;]
          args:
            - |
              if [ -f /var/lib/postgresql/data/pgdata/PG_VERSION ]; then
                echo &quot;Database already initialized, skipping...&quot;
              else
                echo &quot;Fresh installation, will initialize...&quot;
              fi
          volumeMounts:
            - name: postgresql-data
              mountPath: /var/lib/postgresql/data

      containers:
        - name: postgresql
          image: &quot;{{ .Values.postgresql.image }}&quot;
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_PASSWORD
            - name: POSTGRES_DB
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_DB
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgresql-data
              mountPath: /var/lib/postgresql/data
            - name: initdb-sql
              mountPath: /docker-entrypoint-initdb.d
              readOnly: true

          readinessProbe:
            exec:
              command: [&quot;pg_isready&quot;, &quot;-U&quot;, &quot;guacamole&quot;]
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            exec:
              command: [&quot;pg_isready&quot;, &quot;-U&quot;, &quot;guacamole&quot;]
            initialDelaySeconds: 30
            periodSeconds: 10

      volumes:
        - name: initdb-sql
          configMap:
            name: guacamole-initdb

  volumeClaimTemplates:
    - metadata:
        name: postgresql-data
      spec:
        storageClassName: &quot;{{ .Values.postgresql.storageClass }}&quot;
        accessModes: [&quot;ReadWriteOnce&quot;]
        resources:
          requests:
            storage: &quot;{{ .Values.postgresql.storage }}&quot;</code></pre>
<p><strong><code>PGDATA</code> 환경변수를 별도로 설정한 이유:</strong></p>
<ul>
<li>PostgreSQL 공식 이미지는 마운트 포인트 루트에 <code>lost+found</code> 디렉토리가 있으면 초기화 실패</li>
<li><code>PGDATA=/var/lib/postgresql/data/pgdata</code>로 서브디렉토리를 지정하여 우회</li>
</ul>
<h3 id="43-guacamole-데이터-모델">4.3 Guacamole 데이터 모델</h3>
<p>791줄의 초기화 SQL이 생성하는 핵심 테이블 구조:</p>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIEVOVElUWVsiZ3VhY2Ftb2xlX2VudGl0eTxici8-KFVTRVIgLyBHUk9VUCkiXSAtLT4gVVNFUlsiZ3VhY2Ftb2xlX3VzZXI8YnIvPihwYXNzd29yZF9oYXNoLCBwcm9maWxlKSJdCiAgICBFTlRJVFkgLS0-IFVHWyJndWFjYW1vbGVfdXNlcl9ncm91cCJdCiAgICBDR1siZ3VhY2Ftb2xlX2Nvbm5lY3Rpb25fZ3JvdXA8YnIvPihPUkdBTklaQVRJT05BTCAvIEJBTEFOQ0lORykiXSAtLT4gQ09OTlsiZ3VhY2Ftb2xlX2Nvbm5lY3Rpb248YnIvPihwcm90b2NvbDogU1NIL1JEUC9WTkMvVGVsbmV0KSJdCiAgICBDT05OIC0tPiBQQVJBTVsiZ3VhY2Ftb2xlX2Nvbm5lY3Rpb25fcGFyYW1ldGVyPGJyLz4oaG9zdG5hbWUsIHBvcnQsIHVzZXJuYW1lLCBldGMuKSJdCgogICAgc3ViZ3JhcGggUEVSTVsiUGVybWlzc2lvbiBUYWJsZXMiXQogICAgICAgIENQWyJjb25uZWN0aW9uX3Blcm1pc3Npb248YnIvPihSRUFEL1VQREFURS9ERUxFVEUvQURNSU5JU1RFUikiXQogICAgICAgIFNQWyJzeXN0ZW1fcGVybWlzc2lvbjxici8-KENSRUFURV9VU0VSL0NSRUFURV9DT05ORUNUSU9OKSJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBBVURJVFsiQXVkaXQgVGFibGVzIl0KICAgICAgICBDSFsiY29ubmVjdGlvbl9oaXN0b3J5IChzZXNzaW9uIHJlY29yZHMpIl0KICAgICAgICBVSFsidXNlcl9oaXN0b3J5IChsb2dpbi9sb2dvdXQpIl0KICAgICAgICBQSFsidXNlcl9wYXNzd29yZF9oaXN0b3J5Il0KICAgIGVuZA==" alt="diagram-4"></p>
<p><strong>핵심 설계 포인트:</strong></p>
<ul>
<li><code>connection_group</code>으로 커넥션을 폴더 구조로 그룹화 (IDC 서버 / VM / 개발 환경 등)</li>
<li><code>BALANCING</code> 타입 그룹은 로드밸런싱 지원 (같은 역할의 서버 여러 대)</li>
<li><code>connection_parameter</code>에 프로토콜별 설정 저장 (hostname, port, username, private-key 등)</li>
<li>모든 접속 이력이 <code>connection_history</code>에 자동 기록</li>
</ul>
<h3 id="44-guacd-프로토콜-브릿지-데몬">4.4 guacd: 프로토콜 브릿지 데몬</h3>
<p>guacd는 C로 작성된 고성능 프로토콜 변환 데몬입니다:</p>
<pre><code class="language-yaml"># guacd-deployment.yaml
spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: &quot;false&quot;    # TCP 전용 → Sidecar 제외
    spec:
      containers:
        - name: guacd
          image: guacamole/guacd:1.5.5
          ports:
            - containerPort: 4822
              protocol: TCP
          readinessProbe:
            tcpSocket:
              port: 4822
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            tcpSocket:
              port: 4822
            initialDelaySeconds: 10
            periodSeconds: 30
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: &quot;1&quot;
              memory: 1Gi</code></pre>
<p><strong>Istio Sidecar를 비활성화한 이유:</strong>
guacd는 자체 바이너리 프로토콜(Guacamole Protocol)을 사용합니다. Istio의 Envoy sidecar는 이를 HTTP로 해석하려 시도하여 연결이 실패합니다. TCP 전용 서비스에는 sidecar를 주입하지 않는 것이 올바른 패턴입니다.</p>
<h3 id="45-guacamole-웹-애플리케이션">4.5 Guacamole 웹 애플리케이션</h3>
<pre><code class="language-yaml"># guacamole-deployment.yaml
spec:
  template:
    spec:
      containers:
        - name: guacamole
          image: guacamole/guacamole:1.5.5
          ports:
            - containerPort: 8080
              name: http-guacamole    # ← 포트 이름이 중요!
          env:
            # guacd 연결 정보
            - name: GUACD_HOSTNAME
              value: &quot;guacd.{{ .Values.namespace }}.svc.cluster.local&quot;
            - name: GUACD_PORT
              value: &quot;4822&quot;
            # PostgreSQL 연결 정보
            - name: POSTGRESQL_HOSTNAME
              value: &quot;postgresql.{{ .Values.namespace }}.svc.cluster.local&quot;
            - name: POSTGRESQL_PORT
              value: &quot;5432&quot;
            - name: POSTGRESQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_DB
            - name: POSTGRESQL_USER
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_USER
            - name: POSTGRESQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgresql-secret
                  key: POSTGRES_PASSWORD
            - name: POSTGRESQL_AUTO_CREATE_ACCOUNTS
              value: &quot;true&quot;</code></pre>
<p><strong><code>name: http-guacamole</code> — 이 한 줄이 핵심입니다.</strong></p>
<p>Istio는 Service 포트의 <code>name</code> 필드로 프로토콜을 판단합니다:</p>
<ul>
<li><code>http-*</code> → HTTP/1.1로 처리 → L7 라우팅 (VirtualService) 동작</li>
<li>이름 없음 또는 <code>tcp-*</code> → TCP로 처리 → L4 패스스루</li>
</ul>
<p>포트 이름을 지정하지 않으면 Istio가 TCP로 처리하여, IngressGateway에서 503 에러가 발생합니다. 이 문제는 디버깅이 매우 어렵습니다 — Pod은 정상이고, Service도 정상이고, VirtualService도 문법적으로 올바른데, 503이 반환되기 때문입니다.</p>
<h3 id="46-secret-관리-serversideapply-호환">4.6 Secret 관리: ServerSideApply 호환</h3>
<pre><code class="language-yaml"># postgresql-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
  namespace: {{ .Values.namespace }}
type: Opaque
data:                                        # ← stringData가 아닌 data 사용
  POSTGRES_USER: {{ .Values.postgresql.user | b64enc | quote }}
  POSTGRES_PASSWORD: {{ .Values.postgresql.password | b64enc | quote }}
  POSTGRES_DB: {{ .Values.postgresql.database | b64enc | quote }}</code></pre>
<p><strong><code>stringData</code> 대신 <code>data</code> + <code>b64enc</code>를 사용한 이유:</strong>
ArgoCD의 <code>ServerSideApply=true</code> 옵션과 <code>stringData</code>를 함께 사용하면 매 sync마다 diff가 발생합니다. API Server가 <code>stringData</code>를 <code>data</code>(base64)로 변환하는데, ServerSideApply는 이 변환 결과를 &quot;변경됨&quot;으로 감지합니다.</p>
<h3 id="47-multi-environment-배포">4.7 Multi-Environment 배포</h3>
<p>동일한 Helm Chart를 환경별 values로 분리합니다:</p>
<pre><code class="language-yaml"># kubernetes/staging/platform/secern-access-portal/values.yaml
namespace: secern-access-portal-staging
istioInjection: false                   # Staging: Sidecar 불필요
postgresql:
  storage: 5Gi                          # 작은 디스크
  password: &quot;staging-password&quot;
nodePort:
  enabled: true                         # NodePort로 접근
  port: 30888

# kubernetes/production/platform/secern-access-portal/values.yaml
namespace: secern-access-portal
postgresql:
  storage: 10Gi                         # 충분한 디스크
  password: &quot;production-password&quot;       # 실제로는 Vault 연동 권장
nodePort:
  enabled: false                        # Istio Gateway 사용</code></pre>
<hr>
<h2 id="5-istio-통합-https--websocket">5. Istio 통합: HTTPS + WebSocket</h2>
<h3 id="51-tls-인증서-자동-발급">5.1 TLS 인증서 자동 발급</h3>
<pre><code class="language-yaml">apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: accessportal-tls
  namespace: istio-system          # Gateway가 참조하므로 istio-system에 생성
spec:
  secretName: accessportal-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - accessportal.example.com</code></pre>
<p><strong>Certificate를 <code>istio-system</code> 네임스페이스에 생성하는 이유:</strong>
Istio IngressGateway는 <code>istio-system</code>에서 실행되며, Gateway의 <code>credentialName</code>으로 참조하는 TLS Secret도 같은 네임스페이스에 있어야 합니다.</p>
<h3 id="52-gateway--virtualservice">5.2 Gateway + VirtualService</h3>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: accessportal-gateway
  namespace: secern-access-portal
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - accessportal.example.com
      port:
        name: https-accessportal
        number: 443
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: accessportal-tls
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: accessportal-vs
  namespace: secern-access-portal
spec:
  hosts:
    - accessportal.example.com
  gateways:
    - accessportal-gateway
  http:
    # 1. WebSocket 터널 (Guacamole 세션)
    - match:
        - uri:
            prefix: /guacamole/websocket-tunnel
          headers:
            upgrade:
              exact: websocket
      route:
        - destination:
            host: guacamole.secern-access-portal.svc.cluster.local
            port:
              number: 8080

    # 2. REST API + Web UI
    - match:
        - uri:
            prefix: /guacamole
      route:
        - destination:
            host: guacamole.secern-access-portal.svc.cluster.local
            port:
              number: 8080</code></pre>
<p><strong>WebSocket 라우팅을 별도로 분리한 이유:</strong>
Guacamole는 두 가지 터널 방식을 지원합니다:</p>
<ol>
<li><strong>WebSocket 터널</strong> (<code>/guacamole/websocket-tunnel</code>): 양방향 실시간 통신, 낮은 지연</li>
<li><strong>HTTP 터널</strong> (<code>/guacamole/tunnel</code>): 롱 폴링 기반, WebSocket 불가 환경 폴백</li>
</ol>
<p>WebSocket 연결은 <code>Upgrade: websocket</code> 헤더로 시작되는 HTTP 요청입니다. Istio VirtualService에서 이 헤더를 명시적으로 매칭하여 WebSocket 트래픽을 올바르게 라우팅합니다.</p>
<p><strong>주의: <code>timeout: 0s</code>는 사용 불가</strong></p>
<p>Istio VirtualService의 <code>timeout</code>에 <code>0s</code>를 설정하면 오류가 발생합니다:</p>
<pre><code>The VirtualService is invalid: spec.http[0].timeout:
Invalid value: &quot;string&quot;: must be a valid duration greater than 1ms</code></pre><p>WebSocket의 장시간 연결이 필요하면 timeout을 아예 설정하지 않거나(기본값: 무제한), 충분히 큰 값(예: <code>86400s</code>)을 사용해야 합니다.</p>
<hr>
<h2 id="6-rest-api를-활용한-자동-커넥션-등록">6. REST API를 활용한 자동 커넥션 등록</h2>
<p>Guacamole의 REST API를 활용하면 커넥션 등록을 자동화할 수 있습니다:</p>
<h3 id="61-인증-토큰-획득">6.1 인증 토큰 획득</h3>
<pre><code class="language-bash">TOKEN=$(curl -sk -X POST \
  &quot;https://accessportal.example.com/guacamole/api/tokens&quot; \
  -d &quot;username=guacadmin&amp;password=guacadmin&quot; \
  | python3 -c &quot;import sys,json; print(json.load(sys.stdin)[&#39;authToken&#39;])&quot;)</code></pre>
<h3 id="62-ssh-커넥션-등록">6.2 SSH 커넥션 등록</h3>
<pre><code class="language-bash">API=&quot;https://accessportal.example.com/guacamole/api/session/data/postgresql/connections&quot;

# IDC 물리 서버 등록
curl -sk -X POST &quot;$API?token=$TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{
    &quot;parentIdentifier&quot;: &quot;ROOT&quot;,
    &quot;name&quot;: &quot;CP-01 (Control Plane, GPU)&quot;,
    &quot;protocol&quot;: &quot;ssh&quot;,
    &quot;parameters&quot;: {
      &quot;hostname&quot;: &quot;x.x.x.221&quot;,
      &quot;port&quot;: &quot;22&quot;
    },
    &quot;attributes&quot;: {
      &quot;max-connections&quot;: &quot;5&quot;,
      &quot;max-connections-per-user&quot;: &quot;3&quot;
    }
  }&#39;</code></pre>
<h3 id="63-kubevirt-vm-커넥션-등록">6.3 KubeVirt VM 커넥션 등록</h3>
<pre><code class="language-bash"># VM의 Pod IP 조회 후 등록
VM_IP=$(kubectl get vmi test-vm-1 -n kubevirt-test \
  -o jsonpath=&#39;{.status.interfaces[0].ipAddress}&#39;)

curl -sk -X POST &quot;$API?token=$TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{
    \&quot;parentIdentifier\&quot;: \&quot;ROOT\&quot;,
    \&quot;name\&quot;: \&quot;test-vm-1 (Ubuntu VM)\&quot;,
    \&quot;protocol\&quot;: \&quot;ssh\&quot;,
    \&quot;parameters\&quot;: {
      \&quot;hostname\&quot;: \&quot;$VM_IP\&quot;,
      \&quot;port\&quot;: \&quot;22\&quot;
    },
    \&quot;attributes\&quot;: {
      \&quot;max-connections\&quot;: \&quot;5\&quot;,
      \&quot;max-connections-per-user\&quot;: \&quot;3\&quot;
    }
  }&quot;</code></pre>
<h3 id="64-windows-rdp-커넥션-등록">6.4 Windows RDP 커넥션 등록</h3>
<pre><code class="language-bash">WIN_IP=$(kubectl get vmi win11 -n kubevirt-test \
  -o jsonpath=&#39;{.status.interfaces[0].ipAddress}&#39;)

curl -sk -X POST &quot;$API?token=$TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{
    \&quot;parentIdentifier\&quot;: \&quot;ROOT\&quot;,
    \&quot;name\&quot;: \&quot;win11 (Windows 11 VM)\&quot;,
    \&quot;protocol\&quot;: \&quot;rdp\&quot;,
    \&quot;parameters\&quot;: {
      \&quot;hostname\&quot;: \&quot;$WIN_IP\&quot;,
      \&quot;port\&quot;: \&quot;3389\&quot;,
      \&quot;security\&quot;: \&quot;nla\&quot;,
      \&quot;ignore-cert\&quot;: \&quot;true\&quot;
    },
    \&quot;attributes\&quot;: {
      \&quot;max-connections\&quot;: \&quot;3\&quot;,
      \&quot;max-connections-per-user\&quot;: \&quot;2\&quot;
    }
  }&quot;</code></pre>
<h3 id="65-자동화-스크립트-패턴">6.5 자동화 스크립트 패턴</h3>
<pre><code class="language-bash">#!/bin/bash
# register-connections.sh
# Guacamole에 모든 인프라 커넥션을 자동 등록하는 스크립트

GUAC_URL=&quot;https://accessportal.example.com/guacamole&quot;

# 토큰 획득
get_token() {
  curl -sk -X POST &quot;$GUAC_URL/api/tokens&quot; \
    -d &quot;username=$1&amp;password=$2&quot; \
    | python3 -c &quot;import sys,json; print(json.load(sys.stdin)[&#39;authToken&#39;])&quot;
}

# SSH 커넥션 등록
register_ssh() {
  local NAME=&quot;$1&quot; IP=&quot;$2&quot; TOKEN=&quot;$3&quot;
  curl -sk -X POST &quot;$GUAC_URL/api/session/data/postgresql/connections?token=$TOKEN&quot; \
    -H &quot;Content-Type: application/json&quot; \
    -d &quot;{
      \&quot;parentIdentifier\&quot;: \&quot;ROOT\&quot;,
      \&quot;name\&quot;: \&quot;$NAME\&quot;,
      \&quot;protocol\&quot;: \&quot;ssh\&quot;,
      \&quot;parameters\&quot;: {\&quot;hostname\&quot;: \&quot;$IP\&quot;, \&quot;port\&quot;: \&quot;22\&quot;},
      \&quot;attributes\&quot;: {\&quot;max-connections\&quot;: \&quot;5\&quot;, \&quot;max-connections-per-user\&quot;: \&quot;3\&quot;}
    }&quot;
}

# KubeVirt VM 자동 등록
register_kubevirt_vms() {
  local TOKEN=&quot;$1&quot;
  # 모든 VMI의 Pod IP를 조회하여 자동 등록
  kubectl get vmi -A -o json | python3 -c &quot;
import sys, json
vmis = json.load(sys.stdin)[&#39;items&#39;]
for vmi in vmis:
    name = vmi[&#39;metadata&#39;][&#39;name&#39;]
    ns = vmi[&#39;metadata&#39;][&#39;namespace&#39;]
    ip = vmi[&#39;status&#39;][&#39;interfaces&#39;][0][&#39;ipAddress&#39;]
    print(f&#39;{name},{ns},{ip}&#39;)
  &quot; | while IFS=&#39;,&#39; read name ns ip; do
    register_ssh &quot;$name ($ns)&quot; &quot;$ip&quot; &quot;$TOKEN&quot;
    echo &quot;Registered: $name → $ip&quot;
  done
}

# 실행
TOKEN=$(get_token &quot;guacadmin&quot; &quot;guacadmin&quot;)
register_kubevirt_vms &quot;$TOKEN&quot;</code></pre>
<hr>
<h2 id="7-전체-트래픽-흐름">7. 전체 트래픽 흐름</h2>
<p>브라우저에서 KubeVirt VM에 SSH 접속하는 전체 경로:</p>
<pre><code>1. 사용자 브라우저
   https://accessportal.example.com/guacamole/
   │
2. DNS 조회 (Route53 → MetalLB VIP)
   │
3. Istio IngressGateway (MetalLB VIP:443)
   │ TLS 종료 (Let&#39;s Encrypt 인증서)
   │
4. Istio VirtualService 라우팅
   │ /guacamole/* → guacamole.secern-access-portal:8080
   │
5. Guacamole Web App (Tomcat:8080)
   │ 사용자 인증 → REST API → PostgreSQL 조회
   │ WebSocket 터널 수립
   │
6. guacd (Protocol Bridge :4822)
   │ Guacamole Protocol → SSH Protocol 변환
   │
7. KubeVirt VM (Pod IP:22)
   │ virt-launcher Pod → NAT → Guest OS
   │
8. VM 내부 sshd
   │ 셸 세션 시작
   │
9. 결과: 브라우저에서 SSH 터미널 사용 가능!</code></pre><hr>
<h2 id="8-배포-과정에서-만난-문제들">8. 배포 과정에서 만난 문제들</h2>
<h3 id="81-nodeport-충돌">8.1 NodePort 충돌</h3>
<p><strong>증상</strong>: ArgoCD Sync 실패, <code>nodePort already allocated</code> 에러
<strong>원인</strong>: Istio IngressGateway의 status-port가 이미 30880을 사용
<strong>해결</strong>: Staging NodePort를 30888로 변경</p>
<p><strong>교훈</strong>: NodePort 할당 전 반드시 기존 사용 현황 확인</p>
<pre><code class="language-bash">kubectl get svc -A -o jsonpath=&#39;{range .items[?(@.spec.type==&quot;NodePort&quot;)]}{.metadata.name}{&quot;\t&quot;}{range .spec.ports[*]}{.nodePort}{&quot;,&quot;}{end}{&quot;\n&quot;}{end}&#39;</code></pre>
<h3 id="82-serversideapply--stringdata-비호환">8.2 ServerSideApply + stringData 비호환</h3>
<p><strong>증상</strong>: ArgoCD에서 Secret이 매 sync마다 변경 감지
<strong>원인</strong>: <code>stringData</code>는 API Server가 <code>data</code>로 변환 → ServerSideApply가 diff로 감지
<strong>해결</strong>: <code>data</code> + <code>b64enc</code> 사용</p>
<pre><code class="language-yaml"># Before (문제)
stringData:
  password: mypassword

# After (해결)
data:
  password: {{ &quot;mypassword&quot; | b64enc }}</code></pre>
<h3 id="83-istio-503-에러-포트-이름-누락">8.3 Istio 503 에러 (포트 이름 누락)</h3>
<p><strong>증상</strong>: Pod 정상, Service 정상, VirtualService 정상인데 503 반환
<strong>원인</strong>: Service 포트에 <code>name</code>이 없어서 Istio가 TCP로 처리 → L7 라우팅 실패
<strong>해결</strong>: <code>name: http-guacamole</code> 추가</p>
<pre><code class="language-yaml">ports:
  - name: http-guacamole    # http- 접두사 필수
    port: 8080
    targetPort: 8080</code></pre>
<h3 id="84-virtualservice-timeout-0s-무효">8.4 VirtualService timeout: 0s 무효</h3>
<p><strong>증상</strong>: <code>apply</code> 시 validation 에러
<strong>원인</strong>: Istio는 <code>timeout</code> 값으로 0 이하를 허용하지 않음 (최소 1ms)
<strong>해결</strong>: timeout 행 제거 (기본값: 무제한)</p>
<h3 id="85-namespace-outofsync-argocd">8.5 Namespace OutOfSync (ArgoCD)</h3>
<p><strong>증상</strong>: 모든 리소스가 정상인데 ArgoCD가 계속 OutOfSync 표시
<strong>원인</strong>: Helm Chart의 Namespace 템플릿과 ArgoCD의 <code>CreateNamespace=true</code>가 동시에 Namespace를 관리하려 시도
<strong>해결</strong>: Namespace 템플릿 제거 + <code>CreateNamespace=true</code> 유지 + ArgoCD tracking 어노테이션 정리</p>
<h3 id="86-gitignore가-secret-템플릿-차단">8.6 .gitignore가 Secret 템플릿 차단</h3>
<p><strong>증상</strong>: Secret YAML 파일이 git add되지 않음
<strong>원인</strong>: <code>.gitignore</code>의 <code>*secret*</code> 패턴이 Helm 템플릿까지 매칭
<strong>해결</strong>: <code>git add -f</code> 사용</p>
<pre><code class="language-bash">git add -f kubernetes/charts/secern-access-portal/templates/postgresql-secret.yaml</code></pre>
<hr>
<h2 id="9-프론트엔드-확장-계획">9. 프론트엔드 확장 계획</h2>
<p>현재는 Guacamole 기본 UI를 사용하지만, React 기반 커스텀 프론트엔드를 개발하여 더 나은 UX를 제공할 예정입니다:</p>
<pre><code class="language-yaml"># VirtualService에 프론트엔드 라우팅 추가 (예정)
http:
  # WebSocket 터널 (기존)
  - match:
      - uri:
          prefix: /guacamole/websocket-tunnel
    route:
      - destination:
          host: guacamole:8080

  # REST API (기존)
  - match:
      - uri:
          prefix: /guacamole/api
    route:
      - destination:
          host: guacamole:8080

  # React SPA (신규)
  - match:
      - uri:
          prefix: /
    route:
      - destination:
          host: frontend.secern-access-portal.svc.cluster.local
          port:
            number: 80</code></pre>
<p><strong>커스텀 프론트엔드의 장점:</strong></p>
<ul>
<li>회사 브랜딩 적용</li>
<li>통합 대시보드 (VM 상태 + 커넥션 목록 + 모니터링 한 화면)</li>
<li>LDAP/SSO 연동 인증</li>
<li>커넥션 그룹 시각화</li>
<li>세션 녹화 재생 UI</li>
</ul>
<hr>
<h2 id="10-아키텍처-요약">10. 아키텍처 요약</h2>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIENNWyJjZXJ0LW1hbmFnZXIgKFRMUykiXSAtLT4gR1dbIklzdGlvIEdhdGV3YXk8YnIvPihIVFRQUy9XU1MpIl0KICAgIEdXIC0tPiBTUEFbIlJlYWN0IFNQQSAoRnJvbnRlbmQpPGJyLz7ihpAg6rCc67CcIOyYiOyglSJdCiAgICBHVyAtLT4gR1VBQ1siR3VhY2Ftb2xlPGJyLz4oUkVTVCBBUEkgKyBXZWJTb2NrZXQpIl0KICAgIEdVQUMgLS0-IEdVQUNEWyJndWFjZCAoQnJpZGdlKSJdCiAgICBHVUFDIC0tPiBQR1siUG9zdGdyZVNRTDxici8-KEF1dGggKyBDb25maWcpIl0KICAgIEdVQUNEIC0tPiBJRENbIklEQyBTZXJ2ZXJzPGJyLz4oU1NIKSA3IG5vZGVzIl0KICAgIEdVQUNEIC0tPiBLVk1bIkt1YmVWaXJ0IFZNczxici8-KFNTSCAvIFJEUCkiXQogICAgR1VBQ0QgLS0-IENMT1VEWyJDbG91ZCBWTXM8YnIvPihTU0ggLyBSRFApIl0KCiAgICBzdHlsZSBTUEEgc3Ryb2tlLWRhc2hhcnJheTogNSA1" alt="diagram-5"></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>KubeVirt와 Apache Guacamole를 조합하여 다음을 달성했습니다:</p>
<ol>
<li><strong>통합 접속</strong>: 7대 물리 서버 + Ubuntu VM + Windows VM을 웹 브라우저 하나로 접속</li>
<li><strong>Zero-install</strong>: SSH 클라이언트, RDP 클라이언트, VNC 뷰어 설치 불필요</li>
<li><strong>중앙 관리</strong>: 커넥션, 사용자, 권한을 한 곳에서 관리</li>
<li><strong>감사 추적</strong>: 모든 접속 이력 자동 기록</li>
<li><strong>GitOps 호환</strong>: Helm Chart + ArgoCD로 완전 선언적 배포</li>
<li><strong>보안</strong>: HTTPS (Let&#39;s Encrypt), WebSocket Secure, RBAC</li>
</ol>
<p><strong>핵심 기술 조합:</strong></p>
<ul>
<li>KubeVirt v1.7.0 — VM을 Pod처럼 관리</li>
<li>CDI v1.64.0 — 이미지 자동 임포트</li>
<li>Apache Guacamole 1.5.5 — 클라이언트리스 원격 접속</li>
<li>Istio — HTTPS 종료 + WebSocket 라우팅</li>
<li>cert-manager — TLS 자동 발급/갱신</li>
<li>PostgreSQL 16 — 커넥션/인증 데이터 저장</li>
<li>ArgoCD Multi-Source — 환경별 배포 자동화</li>
</ul>
<blockquote>
<p><strong>기술 스택</strong>: KubeVirt v1.7.0 | CDI v1.64.0 | Apache Guacamole 1.5.5 | guacd 1.5.5 | PostgreSQL 16 | Istio VirtualService (WebSocket) | cert-manager (DNS-01) | Helm Chart | ArgoCD Multi-Source Application | Longhorn-VM StorageClass | kubevirt-manager</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[On-Premise Kubernetes 인프라 구축기: 7노드 HA 클러스터에서 Production-Grade 플랫폼까지]]></title>
            <link>https://velog.io/@arnold_99/On-Premise-Kubernetes-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%EA%B8%B0-7%EB%85%B8%EB%93%9C-HA-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%97%90%EC%84%9C-Production-Grade-%ED%94%8C%EB%9E%AB%ED%8F%BC%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@arnold_99/On-Premise-Kubernetes-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%EA%B8%B0-7%EB%85%B8%EB%93%9C-HA-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%97%90%EC%84%9C-Production-Grade-%ED%94%8C%EB%9E%AB%ED%8F%BC%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Sat, 14 Mar 2026 11:08:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/arnold_99/post/df9f2416-5bde-4bab-9577-f0b75f9b9c78/image.png" alt=""></p>
<blockquote>
<p>IDC 환경에서 Bare-Metal Kubernetes 클러스터를 구축하고, GitOps 기반의 완전 자동화된 인프라를 설계한 과정을 공유합니다.</p>
</blockquote>
<hr>
<h2 id="1-왜-on-premise-kubernetes인가">1. 왜 On-Premise Kubernetes인가?</h2>
<p>클라우드 환경이 대세인 시대에 굳이 On-Premise를 선택한 이유가 있습니다. GPU 워크로드(RTX 3090, Titan, T4)를 활용한 얼굴인식 시스템(FRS)을 운영해야 했고, IDC에 이미 확보된 물리 서버를 최대한 활용하면서도 클라우드 수준의 운영 자동화를 달성하는 것이 목표였습니다.</p>
<p><strong>핵심 설계 원칙:</strong></p>
<ul>
<li>모든 구성요소 3중화 (Single Point of Failure 제거)</li>
<li>GitOps 기반 선언적 인프라 관리</li>
<li>물리 DMZ 네트워크 격리로 보안 확보</li>
<li>GPU 노드의 효율적 스케줄링</li>
</ul>
<hr>
<h2 id="2-클러스터-토폴로지">2. 클러스터 토폴로지</h2>
<h3 id="21-노드-구성">2.1 노드 구성</h3>
<p>7대의 물리 서버로 구성된 Kubernetes v1.30.4 클러스터입니다.</p>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEIKICAgIHN1YmdyYXBoIENQWyJDb250cm9sIFBsYW5lICgzIG5vZGVzKSJdCiAgICAgICAgQ1AxWyJDUC0wMTxici8-R1BVOiBSVFgzMDkwPGJyLz5ldGNkIHwgQVBJIFNlcnZlciJdCiAgICAgICAgQ1AyWyJDUC0wMjxici8-ZXRjZCB8IEFQSSBTZXJ2ZXI8YnIvPkNvbnRyb2xsZXIgTWFuYWdlciJdCiAgICAgICAgQ1AzWyJDUC0wMzxici8-ZXRjZCB8IEFQSSBTZXJ2ZXI8YnIvPlNjaGVkdWxlciJdCiAgICBlbmQKICAgIFZJUChbImt1YmUtdmlwIFZJUCA6NjQ0Mzxici8-KEhBIEFQSSBTZXJ2ZXIpIl0pCiAgICBDUDEgPC0tPiBWSVAKICAgIENQMiA8LS0-IFZJUAogICAgQ1AzIDwtLT4gVklQCiAgICBzdWJncmFwaCBXS1siV29ya2VyIE5vZGVzICg0IG5vZGVzKSJdCiAgICAgICAgV0sxWyJXb3JrZXItMDE8YnIvPkRCOiBNYXN0ZXI8YnIvPihub2RlLXBpbm5lZCkiXQogICAgICAgIFdLMlsiV29ya2VyLTAyPGJyLz5EQjogU2xhdmUxPGJyLz4obm9kZS1waW5uZWQpIl0KICAgICAgICBXSzNbIldvcmtlci0wMzxici8-R1BVOiBUaXRhbjxici8-REI6IFNsYXZlMiJdCiAgICAgICAgV0s0WyJXb3JrZXItMDQ8YnIvPkdQVTogVDQiXQogICAgZW5kCiAgICBWSVAgLS0tIFdLMSAmIFdLMiAmIFdLMyAmIFdLNA==" alt="diagram-1"></p>
<table>
<thead>
<tr>
<th>역할</th>
<th>노드 수</th>
<th>특이사항</th>
</tr>
</thead>
<tbody><tr>
<td>Control Plane</td>
<td>3대</td>
<td>etcd 내장, 1대에 RTX 3090 GPU 탑재</td>
</tr>
<tr>
<td>Worker (DB 전용)</td>
<td>2대</td>
<td>MariaDB Master/Slave node-pinning</td>
</tr>
<tr>
<td>Worker (GPU)</td>
<td>2대</td>
<td>Titan GPU, T4 GPU + MariaDB Slave2</td>
</tr>
</tbody></table>
<h3 id="22-고가용성-구성">2.2 고가용성 구성</h3>
<pre><code class="language-yaml"># API Server HA: kube-vip
apiVersion: v1
kind: Pod
metadata:
  name: kube-vip
spec:
  containers:
    - name: kube-vip
      image: ghcr.io/kube-vip/kube-vip:v0.8.0
      args:
        - manager
      env:
        - name: vip_address
          value: &quot;&lt;VIP_ADDRESS&gt;&quot;     # Virtual IP
        - name: port
          value: &quot;6443&quot;
        - name: vip_arp
          value: &quot;true&quot;              # L2 ARP 기반</code></pre>
<p>Control Plane 3대가 <strong>kube-vip</strong>을 통해 단일 VIP를 공유합니다. 리더 노드 장애 시 ARP 기반으로 즉시 failover되어 API Server 무중단을 보장합니다.</p>
<hr>
<h2 id="3-네트워크-아키텍처">3. 네트워크 아키텍처</h2>
<h3 id="31-물리-dmz-격리">3.1 물리 DMZ 격리</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIElTUFsiSVNQPGJyLz5XQU4xIC8gV0FOMiJdIC0tPiBGV1siRmlyZXdhbGwgKE5BVCkiXQogICAgRlcgPC0tPiBWUE5bIlZQTiBTZXJ2ZXI8YnIvPihXaXJlR3VhcmQpIl0KICAgIEZXIC0tPiBETVpbIkRNWiBTd2l0Y2g8YnIvPijrrLzrpqzsoIEg64Sk7Yq47JuM7YGsIOu2hOumrCkiXQogICAgRE1aIC0tPiBLOFMKICAgIHN1YmdyYXBoIEs4U1siS3ViZXJuZXRlcyBDbHVzdGVyIChDYWxpY28gQ05JKSJdCiAgICAgICAgTUxCWyJNZXRhbExCIEwyPGJyLz5JUCBQb29sOiB4LngueC4xODAtMjAwIl0gLS0-IElHV1siSXN0aW8gSW5ncmVzc0dhdGV3YXkiXQogICAgZW5k" alt="diagram-2"></p>
<p><strong>핵심 보안 설계:</strong></p>
<ul>
<li>인터넷에 직접 노출 없음 — VPN 터널(WireGuard)을 통해서만 인바운드 접근</li>
<li>아웃바운드: NAT를 통한 WAN2 경유 (직접 인터넷 연결 불가)</li>
<li>물리 스위치 단에서 DMZ 네트워크 격리</li>
<li>Calico CNI + NetworkPolicy로 Pod 레벨 마이크로세그멘테이션</li>
</ul>
<h3 id="32-metallb-l2-로드밸런서">3.2 MetalLB L2 로드밸런서</h3>
<p>클라우드의 ELB/NLB가 없는 Bare-Metal 환경에서 <strong>MetalLB</strong>가 LoadBalancer 타입 Service를 지원합니다.</p>
<pre><code class="language-yaml">apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
    - x.x.x.180-x.x.x.200    # 21개 IP 풀
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
    - default-pool</code></pre>
<p>L2 Advertisement 모드로 동작하여, Istio IngressGateway가 MetalLB VIP를 할당받아 외부 트래픽을 수신합니다.</p>
<hr>
<h2 id="4-gitops-argocd-기반-선언적-인프라">4. GitOps: ArgoCD 기반 선언적 인프라</h2>
<h3 id="41-전체-구조">4.1 전체 구조</h3>
<pre><code>infrastructure/
├── argocd/
│   ├── applications/          # 26개 ArgoCD Application
│   ├── applicationsets/       # Directory Generator 기반 자동 생성
│   ├── projects/              # 3개 RBAC 프로젝트
│   └── secrets/               # 레포지토리 인증정보
├── kubernetes/
│   ├── charts/                # Custom Helm Charts
│   │   ├── nicepayment-frs/   # FRS 우산형 차트 (4개 서브차트)
│   │   └── secern-access-portal/  # Guacamole 백엔드
│   ├── production/            # 67개 Production 매니페스트
│   │   ├── databases/         # MariaDB HA, MaxScale
│   │   ├── cache/             # Redis 6-node Cluster
│   │   ├── monitoring/        # Prometheus Stack
│   │   ├── logging/           # OpenSearch + Vector
│   │   ├── istio/             # Service Mesh 설정
│   │   ├── vault/             # Secret Management
│   │   └── ...
│   └── staging/               # Staging 환경 (단일 인스턴스)
└── docs/                      # 18개 운영 문서</code></pre><h3 id="42-argocd-프로젝트-rbac">4.2 ArgoCD 프로젝트 RBAC</h3>
<p>3개의 AppProject로 권한을 분리합니다:</p>
<pre><code class="language-yaml"># infrastructure-project: 데이터베이스, 캐시, 스토리지
# platform-project: 모니터링, 서비스메시, 시크릿, DNS
# apps-project: 비즈니스 애플리케이션</code></pre>
<table>
<thead>
<tr>
<th>프로젝트</th>
<th>관리 대상</th>
<th>Sync 정책</th>
</tr>
</thead>
<tbody><tr>
<td>infrastructure</td>
<td>MariaDB, Redis, OpenSearch, Harbor</td>
<td>Production: Manual</td>
</tr>
<tr>
<td>platform</td>
<td>Istio, Prometheus, Vault, MetalLB, ExternalDNS</td>
<td>Production: Manual</td>
</tr>
<tr>
<td>apps</td>
<td>FRS, Access Portal</td>
<td>Staging: Auto, Prod: Manual</td>
</tr>
</tbody></table>
<h3 id="43-26개-application-목록">4.3 26개 Application 목록</h3>
<p><strong>Infrastructure (5):</strong></p>
<ul>
<li><code>local-path-provisioner</code> — 로컬 SSD 스토리지 프로비저닝</li>
<li><code>harbor</code> — 프라이빗 컨테이너/Helm 레지스트리 (S3 백엔드)</li>
<li><code>opensearch-logs</code> — 로그/트레이스 집계 클러스터</li>
<li><code>opensearch-vector-staging</code> — 벡터 DB (AI 임베딩)</li>
</ul>
<p><strong>Platform (12):</strong></p>
<ul>
<li><code>sail-operator</code> — Istio 라이프사이클 관리</li>
<li><code>istio</code> — Istio 컨트롤 플레인 (v1.24.3)</li>
<li><code>istio-ingressgateway</code> — 인그레스 게이트웨이 (MetalLB LB)</li>
<li><code>istio-resources</code> — Gateway/VirtualService/DestinationRule</li>
<li><code>kiali-server</code> — 서비스 메시 시각화</li>
<li><code>prometheus-stack</code> — Prometheus + Grafana + Alertmanager (v79.5.0)</li>
<li><code>otel-operator</code> — OpenTelemetry 오퍼레이터</li>
<li><code>otel-collector</code> — OTEL 데이터 수집기</li>
<li><code>otel-instrumentation</code> — Pod 자동 계측 규칙</li>
<li><code>vault</code> — HashiCorp Vault HA</li>
<li><code>external-secrets</code> — Vault → K8s Secret 동기화</li>
<li><code>external-dns</code> — Route53 DNS 자동화</li>
<li><code>metallb</code> — Bare-Metal 로드밸런서</li>
</ul>
<p><strong>Applications (3+):</strong></p>
<ul>
<li><code>nicepayment-frs</code> — 얼굴인식 시스템 (멀티 차트)</li>
<li><code>secern-access-portal</code> — 원격 접속 포털 (Staging + Production)</li>
</ul>
<h3 id="44-multi-source-application-패턴">4.4 Multi-Source Application 패턴</h3>
<p>하나의 Helm 차트를 환경별 values로 재사용하는 ArgoCD Multi-Source 패턴을 적용했습니다:</p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: secern-access-portal
  namespace: argocd
spec:
  project: platform
  sources:
    # Source 1: Helm Chart
    - repoURL: https://github.com/org/infrastructure.git
      targetRevision: HEAD
      path: kubernetes/charts/secern-access-portal
      helm:
        releaseName: secern-access-portal
        valueFiles:
          - $values/kubernetes/production/platform/secern-access-portal/values.yaml
    # Source 2: Values Reference
    - repoURL: https://github.com/org/infrastructure.git
      targetRevision: HEAD
      ref: values          # $values로 참조 가능
  destination:
    server: https://kubernetes.default.svc
    namespace: secern-access-portal
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true    # 대규모 리소스 호환성
  ignoreDifferences:
    - group: apps
      kind: StatefulSet
      jsonPointers:
        - /spec/volumeClaimTemplates    # immutable field 무시</code></pre>
<p><strong>ServerSideApply를 사용한 이유:</strong></p>
<ul>
<li><code>kubectl apply</code>는 Last-Applied-Configuration 어노테이션 기반 → 대규모 ConfigMap에서 크기 초과 오류</li>
<li>ServerSideApply는 필드 소유권(Field Ownership) 기반 → 충돌 감지 및 해결이 깔끔</li>
<li>단, <code>Secret</code>에서 <code>stringData</code>를 사용하면 base64 인코딩 diff가 발생하므로 반드시 <code>data</code> + <code>b64enc</code> 사용</li>
</ul>
<h3 id="45-gitops-워크플로우">4.5 GitOps 워크플로우</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggTFIKICAgIERFVlsiRGV2ZWxvcGVyIl0gLS0-fHB1c2h8IEdJVFsiR2l0IChtYWluKSJdCiAgICBHSVQgLS0-fGhvb2t8IEFSR09bIkFyZ29DRCAoZGV0ZWN0KSJdCiAgICBBUkdPIC0tPnxzeW5jfCBLOFNbIks4cyAoYXBwbHkpIl0KICAgIEFSR08gLS0-IERJRkZbIkRpZmYgQ29tcGFyZSJdCiAgICBESUZGIC0tPiBTVEdbIlN0YWdpbmc6PGJyLz5BdXRvIFN5bmM8YnIvPihzZWxmSGVhbCwgcHJ1bmUpIl0KICAgIERJRkYgLS0-IFBSRFsiUHJvZHVjdGlvbjo8YnIvPk1hbnVhbCBTeW5jPGJyLz4oYXBwcm92YWwgcmVxdWlyZWQpIl0=" alt="diagram-3"></p>
<p><strong>핵심 규칙:</strong></p>
<ol>
<li><code>kubectl</code>로 직접 수정 금지 — 반드시 Git → ArgoCD Sync</li>
<li>Production은 Manual Sync (안전성 우선)</li>
<li>Staging은 Auto Sync + Self-Heal + Prune (빠른 반복)</li>
<li>긴급 상황 시 <code>kubectl</code> 허용하되, 즉시 Git에 동기화</li>
</ol>
<hr>
<h2 id="5-3중화-데이터베이스-mariadb-ha-cluster">5. 3중화 데이터베이스: MariaDB HA Cluster</h2>
<h3 id="51-아키텍처">5.1 아키텍처</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIE1YU1siTWF4U2NhbGUgKFByb3h5IDozMzA2KTxici8-QXV0by1GYWlsb3ZlciAvIFLCt1cgU3BsaXQiXQogICAgTVhTIC0tPiBNWyJNYXN0ZXIgKFdvcmtlci0wMSwgcGlubmVkKTxici8-Ui9XIl0KICAgIE1YUyAtLT4gUzFbIlNsYXZlMSAoV29ya2VyLTAyLCBwaW5uZWQpPGJyLz5SL08iXQogICAgTVhTIC0tPiBTMlsiU2xhdmUyIChXb3JrZXItMDMsIHBpbm5lZCk8YnIvPlIvTyJdCiAgICBNIC0tPnwiR1RJRCBSZXBsaWNhdGlvbiJ8IFMxCiAgICBNIC0tPnwiR1RJRCBSZXBsaWNhdGlvbiJ8IFMy" alt="diagram-4"></p>
<h3 id="52-gtid-기반-복제">5.2 GTID 기반 복제</h3>
<pre><code class="language-yaml"># MariaDB Master 설정
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
gtid-strict-mode = 1
log-slave-updates = ON

# Slave 설정
[mysqld]
server-id = 2      # Slave마다 고유
read-only = 1
log-bin = mysql-bin
relay-log = relay-bin
gtid-strict-mode = 1
log-slave-updates = ON</code></pre>
<p><strong>GTID(Global Transaction ID)를 선택한 이유:</strong></p>
<ul>
<li>바이너리 로그 파일명+위치 기반 복제는 failover 시 정확한 위치 찾기 어려움</li>
<li>GTID는 트랜잭션 단위 고유 식별자 → Master 변경 시에도 정확한 복제 지점 보장</li>
<li><code>gtid-strict-mode=1</code>: 비-트랜잭션 엔진(MyISAM) 혼용 방지</li>
</ul>
<h3 id="53-maxscale-자동-failover--readwrite-split">5.3 MaxScale: 자동 Failover + Read/Write Split</h3>
<pre><code class="language-yaml"># MaxScale 설정 핵심
[ReadWriteSplit-Service]
type = service
router = readwritesplit
servers = master, slave1, slave2
user = maxscale_user
password = ****
master_failure_mode = fail_on_write
master_reconnection = true

[MariaDB-Monitor]
type = monitor
module = mariadbmon
servers = master, slave1, slave2
auto_failover = true
auto_rejoin = true
failcount = 3               # 3회 실패 후 failover
monitor_interval = 2000ms    # 2초 간격 헬스체크</code></pre>
<p><strong>MaxScale의 역할:</strong></p>
<ol>
<li><strong>Read/Write Split</strong>: SELECT → Slave로 분산, INSERT/UPDATE/DELETE → Master로</li>
<li><strong>Auto-Failover</strong>: Master 장애 시 Slave를 자동 승격 (failcount=3, 약 6초)</li>
<li><strong>Auto-Rejoin</strong>: 복구된 이전 Master를 자동으로 Slave로 재합류</li>
<li><strong>Connection Pooling</strong>: 백엔드 연결 재사용</li>
</ol>
<h3 id="54-node-pinning-전략">5.4 Node-Pinning 전략</h3>
<pre><code class="language-yaml"># StatefulSet에서 nodeSelector로 고정
spec:
  template:
    spec:
      nodeSelector:
        kubernetes.io/hostname: worker-01    # Master 전용
      tolerations: []</code></pre>
<p><strong>DB를 특정 노드에 고정하는 이유:</strong></p>
<ul>
<li><code>local-path</code> StorageClass 사용 → 데이터가 로컬 SSD에 저장</li>
<li>노드 간 이동 시 데이터 손실 위험 (네트워크 스토리지가 아니므로)</li>
<li>SSD 직접 I/O로 최대 성능 확보</li>
<li>각 노드의 디스크 용량/성능을 개별 관리 가능</li>
</ul>
<hr>
<h2 id="6-3중화-캐시-redis-6-node-cluster">6. 3중화 캐시: Redis 6-Node Cluster</h2>
<h3 id="61-아키텍처">6.1 아키텍처</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIFJDWyJSZWRpcyBDbHVzdGVyICg2IG5vZGVzKSDigJQgT3BzVHJlZSBPcGVyYXRvciJdCiAgICAgICAgTDBbIkxlYWRlci0wPGJyLz5TbG90IDAtNTQ2MCJdIC0tPiBGMFsiRm9sbG93ZXItMDxici8-KHJlcGxpY2EpIl0KICAgICAgICBMMVsiTGVhZGVyLTE8YnIvPlNsb3QgNTQ2MS0xMDkyMiJdIC0tPiBGMVsiRm9sbG93ZXItMTxici8-KHJlcGxpY2EpIl0KICAgICAgICBMMlsiTGVhZGVyLTI8YnIvPlNsb3QgMTA5MjMtMTYzODMiXSAtLT4gRjJbIkZvbGxvd2VyLTI8YnIvPihyZXBsaWNhKSJdCiAgICBlbmQ=" alt="diagram-5"></p>
<h3 id="62-opstree-redis-operator">6.2 OpsTree Redis Operator</h3>
<p>직접 <code>redis-cli --cluster create</code>를 하는 대신, OpsTree Redis Operator가 선언적으로 클러스터를 관리합니다:</p>
<pre><code class="language-yaml">apiVersion: redis.redis.opstreelabs.in/v1beta2
kind: RedisCluster
metadata:
  name: redis-cluster
  namespace: redis
spec:
  clusterSize: 3                   # 3 Leader + 3 Follower = 6 pods
  clusterVersion: v7
  persistenceEnabled: true
  kubernetesConfig:
    image: redis:7.2.7
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
  storage:
    volumeClaimTemplate:
      spec:
        storageClassName: local-path
        accessModes: [&quot;ReadWriteOnce&quot;]
        resources:
          requests:
            storage: 10Gi</code></pre>
<p><strong>Operator 사용의 장점:</strong></p>
<ul>
<li>노드 추가/제거 시 자동 슬롯 리밸런싱</li>
<li>Leader 장애 시 Follower 자동 승격</li>
<li>Rolling Update 시 데이터 무손실 보장</li>
<li><code>RedisCluster</code> CRD로 선언적 관리 (GitOps 호환)</li>
</ul>
<hr>
<h2 id="7-secret-management-vault-3중화">7. Secret Management: Vault 3중화</h2>
<h3 id="71-ha-raft-아키텍처">7.1 HA Raft 아키텍처</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIFZBVUxUWyJWYXVsdCBIQSBDbHVzdGVyIChSYWZ0KTxici8-U2hhbWlyIFNlYWw6IDUga2V5cywgMyB0aHJlc2hvbGQ8YnIvPlN0b3JhZ2U6IExvbmdob3JuIFBWQyJdCiAgICAgICAgVjBbIlZhdWx0LTA8YnIvPihBY3RpdmUgLyBSYWZ0IExlYWRlcikiXQogICAgICAgIFYxWyJWYXVsdC0xPGJyLz4oU3RhbmRieSAvIFJhZnQgRm9sbG93ZXIpIl0KICAgICAgICBWMlsiVmF1bHQtMjxici8-KFN0YW5kYnkgLyBSYWZ0IEZvbGxvd2VyKSJdCiAgICAgICAgVjAgPC0tPnwiUmFmdCBDb25zZW5zdXMifCBWMQogICAgICAgIFYwIDwtLT58IlJhZnQgQ29uc2Vuc3VzInwgVjIKICAgIGVuZA==" alt="diagram-6"></p>
<h3 id="72-helm-values-ha-구성">7.2 Helm Values (HA 구성)</h3>
<pre><code class="language-yaml">server:
  ha:
    enabled: true
    replicas: 3
    raft:
      enabled: true
      setNodeId: true
      config: |
        ui = true
        listener &quot;tcp&quot; {
          tls_disable = 1
          address     = &quot;[::]:8200&quot;
          cluster_address = &quot;[::]:8201&quot;
        }
        storage &quot;raft&quot; {
          path = &quot;/vault/data&quot;
          retry_join {
            leader_api_addr = &quot;http://vault-0.vault-internal:8200&quot;
          }
          retry_join {
            leader_api_addr = &quot;http://vault-1.vault-internal:8200&quot;
          }
          retry_join {
            leader_api_addr = &quot;http://vault-2.vault-internal:8200&quot;
          }
        }
        service_registration &quot;kubernetes&quot; {}
  resources:
    requests:
      memory: 256Mi
      cpu: 250m
    limits:
      memory: 512Mi
  dataStorage:
    enabled: true
    storageClass: longhorn
    size: 10Gi</code></pre>
<h3 id="73-shamir-seal-운용">7.3 Shamir Seal 운용</h3>
<pre><code class="language-bash"># 초기화 (1회)
vault operator init -key-shares=5 -key-threshold=3

# Pod 재시작 시 Unseal (3개 키 필요)
vault operator unseal &lt;key-1&gt;
vault operator unseal &lt;key-2&gt;
vault operator unseal &lt;key-3&gt;

# 3개 Pod 모두 개별 Unseal 필요</code></pre>
<p><strong>5/3 Threshold를 선택한 이유:</strong></p>
<ul>
<li>5개 키 중 3개로 Unseal 가능 → 2개 키 분실에도 복구 가능</li>
<li>키를 5명의 관리자에게 분산 보관 → 단독으로 Unseal 불가 (보안)</li>
<li>Auto-Unseal(AWS KMS 등)을 사용하지 않은 이유: On-Premise 환경에서 외부 의존성 최소화</li>
</ul>
<h3 id="74-external-secrets-operator-연동">7.4 External Secrets Operator 연동</h3>
<p>Vault의 시크릿을 Kubernetes Secret으로 자동 동기화합니다:</p>
<pre><code class="language-yaml"># ClusterSecretStore: Vault 연결 정의
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: &quot;http://vault.vault:8200&quot;
      path: &quot;secret&quot;
      version: &quot;v2&quot;
      auth:
        tokenSecretRef:
          name: vault-token
          namespace: external-secrets
          key: token
---
# ExternalSecret: 어떤 시크릿을 동기화할지 정의
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: frs-secrets
  namespace: nicepayment-frs
spec:
  refreshInterval: 1h              # 1시간마다 Vault 동기화
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: frs-secrets
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: secret/app/db
        property: password
    - secretKey: API_KEY
      remoteRef:
        key: secret/app/api
        property: apiKey</code></pre>
<p><strong>Secret 관리 흐름:</strong>
<img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIFZbIlZhdWx0IChTb3VyY2Ugb2YgVHJ1dGgpIl0gLS0-fCJFeHRlcm5hbFNlY3JldCAoMWggc3luYykifCBTWyJLOHMgU2VjcmV0ICjsnpDrj5kg7IOd7ISxL-qwseyLoCkiXQogICAgUyAtLT58ImVudkZyb20gLyB2b2x1bWVNb3VudCJ8IFBbIkFwcGxpY2F0aW9uIFBvZCJd" alt="diagram-7"></p>
<p><strong>Vault에 Git으로 커밋되는 시크릿은 없습니다.</strong> ExternalSecret의 <code>remoteRef</code>만 Git에 저장되고, 실제 값은 Vault에서 런타임에 주입됩니다.</p>
<hr>
<h2 id="8-service-mesh-istio-sail-operator">8. Service Mesh: Istio (Sail Operator)</h2>
<h3 id="81-왜-sail-operator인가">8.1 왜 Sail Operator인가?</h3>
<p>기존 <code>istioctl install</code>이나 Helm 직접 설치 대신 <strong>Sail Operator</strong>를 선택했습니다:</p>
<table>
<thead>
<tr>
<th>비교</th>
<th>istioctl</th>
<th>Helm</th>
<th>Sail Operator</th>
</tr>
</thead>
<tbody><tr>
<td>업그레이드</td>
<td>수동</td>
<td>수동</td>
<td>CRD 변경만으로 자동</td>
</tr>
<tr>
<td>Canary 업그레이드</td>
<td>복잡</td>
<td>가능</td>
<td><code>revisionTag</code>로 간편</td>
</tr>
<tr>
<td>GitOps 호환</td>
<td>어려움</td>
<td>가능</td>
<td>최적 (CRD 기반)</td>
</tr>
<tr>
<td>다중 컨트롤 플레인</td>
<td>가능</td>
<td>복잡</td>
<td>네이티브 지원</td>
</tr>
</tbody></table>
<h3 id="82-istio-구성">8.2 Istio 구성</h3>
<pre><code class="language-yaml">apiVersion: sailoperator.io/v1
kind: Istio
metadata:
  name: default
spec:
  version: v1.24.3
  namespace: istio-system
  values:
    pilot:
      resources:
        requests:
          cpu: 500m
          memory: 2Gi
    meshConfig:
      accessLogFile: /dev/stdout
      accessLogFormat: |
        {&quot;timestamp&quot;:&quot;%START_TIME%&quot;,&quot;method&quot;:&quot;%REQ(:METHOD)%&quot;,
         &quot;path&quot;:&quot;%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%&quot;,
         &quot;response_code&quot;:&quot;%RESPONSE_CODE%&quot;,
         &quot;duration&quot;:&quot;%DURATION%&quot;}
      outboundTrafficPolicy:
        mode: ALLOW_ANY
      enablePrometheusMerge: true
      defaultConfig:
        tracing:
          sampling: 10    # 10% 샘플링</code></pre>
<h3 id="83-ingressgateway-구성">8.3 IngressGateway 구성</h3>
<pre><code class="language-yaml">apiVersion: sailoperator.io/v1
kind: IstioRevisionTag
metadata:
  name: default
spec:
  targetRef:
    kind: Istio
    name: default
---
# IngressGateway: MetalLB LoadBalancer
spec:
  values:
    service:
      type: LoadBalancer      # MetalLB에서 VIP 할당
      ports:
        - name: http
          port: 80
          targetPort: 8080
        - name: https
          port: 443
          targetPort: 8443
        - name: tcp-cupaybot
          port: 6000           # TCP 소켓 서비스
          targetPort: 6000</code></pre>
<h3 id="84-tls-인증서-자동화">8.4 TLS 인증서 자동화</h3>
<pre><code class="language-yaml"># cert-manager + Let&#39;s Encrypt (DNS-01)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: harbor-tls
  namespace: istio-system
spec:
  secretName: harbor-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - harbor.example.com
---
# Gateway에서 TLS 종료
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: harbor-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: harbor-tls    # cert-manager가 생성한 Secret
      hosts:
        - harbor.example.com</code></pre>
<p><strong>DNS-01 Challenge를 사용한 이유:</strong></p>
<ul>
<li>HTTP-01은 인터넷에서 서버로 직접 접근 필요 → DMZ 환경에서 불가</li>
<li>DNS-01은 Route53 API로 TXT 레코드 추가 → 인바운드 접근 없이 인증서 발급</li>
<li>와일드카드 인증서 지원</li>
</ul>
<hr>
<h2 id="9-observability-3계층-관측-파이프라인">9. Observability: 3계층 관측 파이프라인</h2>
<h3 id="91-전체-아키텍처">9.1 전체 아키텍처</h3>
<p><img src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIEFQUFsiQXBwbGljYXRpb24gTGF5ZXIiXQogICAgICAgIEpBVkFbIkphdmEgQXBwPGJyLz4oT1RFTCBBZ2VudCkiXQogICAgICAgIE5FVFsiLk5FVCBBcHA8YnIvPihPVEVMIEFnZW50KSJdCiAgICAgICAgRU5WT1lbIklzdGlvIFNpZGVjYXI8YnIvPihFbnZveSkiXQogICAgZW5kCgogICAgSkFWQSAmIE5FVCAmIEVOVk9ZIC0tPiBDT0xMWyJPVEVMIENvbGxlY3Rvcjxici8-KGlzdGlvLXN5c3RlbSkiXQoKICAgIENPTEwgLS0-fCJNZXRyaWNzInwgUFJPTVsiUHJvbWV0aGV1czxici8-KDUwR2kpIl0KICAgIENPTEwgLS0-fCJUcmFjZXMifCBEUFsiRGF0YSBQcmVwcGVyPGJyLz4oVHJhY2UgRVRMKSJdCiAgICBDT0xMIC0tPnwiTG9ncyJ8IE9TWyJPcGVuU2VhcmNoPGJyLz4oMy1ub2RlLCAxMDBHaS9uKSJdCgogICAgUFJPTSAtLT4gR1JBRlsiR3JhZmFuYSAoVml6KSJdCiAgICBEUCAtLT4gVEFbIlRyYWNlIEFuYWx5dGljcyJdCiAgICBPUyAtLT4gT1NEWyJPcGVuU2VhcmNoIERhc2hib2FyZHMiXQoKICAgIFZFQ1siVmVjdG9yIERhZW1vblNldDxici8-KENvbnRhaW5lciBMb2dzKSJdIC0tPnwic3Rkb3V0L3N0ZGVyciJ8IE9T" alt="diagram-8"></p>
<h3 id="92-opentelemetry-자동-계측">9.2 OpenTelemetry 자동 계측</h3>
<p>코드 수정 없이 OTEL Agent를 주입하여 트레이싱/메트릭을 수집합니다:</p>
<pre><code class="language-yaml">apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: otel-instrumentation
  namespace: nicepayment-frs
spec:
  exporter:
    endpoint: http://otel-collector.istio-system:4317
  propagators:
    - tracecontext
    - baggage
    - b3multi
  sampler:
    type: parentbased_traceidratio
    argument: &quot;1&quot;              # 100% 샘플링
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
    env:
      - name: OTEL_LOGS_EXPORTER
        value: otlp
      - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
        value: http://otel-collector.istio-system:4317</code></pre>
<p>Pod에 어노테이션 한 줄만 추가하면 자동 계측됩니다:</p>
<pre><code class="language-yaml">metadata:
  annotations:
    instrumentation.opentelemetry.io/inject-java: &quot;true&quot;</code></pre>
<h3 id="93-데이터-흐름-상세">9.3 데이터 흐름 상세</h3>
<table>
<thead>
<tr>
<th>데이터</th>
<th>수집</th>
<th>처리</th>
<th>저장</th>
<th>시각화</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Metrics</strong></td>
<td>OTEL Agent</td>
<td>OTEL Collector → Prometheus Remote Write</td>
<td>Prometheus (30일, 45GB)</td>
<td>Grafana</td>
</tr>
<tr>
<td><strong>Traces</strong></td>
<td>OTEL Agent</td>
<td>OTEL Collector → Data Prepper</td>
<td>OpenSearch (otel-v1-apm-span-*)</td>
<td>Trace Analytics</td>
</tr>
<tr>
<td><strong>Logs (OTEL)</strong></td>
<td>OTEL Agent</td>
<td>OTEL Collector</td>
<td>OpenSearch (otel-logs)</td>
<td>OpenSearch Dashboards</td>
</tr>
<tr>
<td><strong>Logs (Container)</strong></td>
<td>Vector DaemonSet</td>
<td>Vector Transform</td>
<td>OpenSearch (logs-YYYY.MM.DD)</td>
<td>OpenSearch Dashboards</td>
</tr>
<tr>
<td><strong>Mesh Metrics</strong></td>
<td>Istio Envoy</td>
<td>Prometheus Scrape</td>
<td>Prometheus</td>
<td>Kiali + Grafana</td>
</tr>
</tbody></table>
<hr>
<h2 id="10-스토리지-전략">10. 스토리지 전략</h2>
<h3 id="101-이중-storageclass-운용">10.1 이중 StorageClass 운용</h3>
<table>
<thead>
<tr>
<th>StorageClass</th>
<th>레플리카</th>
<th>용도</th>
<th>성능</th>
</tr>
</thead>
<tbody><tr>
<td><code>local-path</code></td>
<td>1 (로컬)</td>
<td>MariaDB, Redis, OpenSearch</td>
<td>최고 (SSD 직접)</td>
</tr>
<tr>
<td><code>longhorn</code></td>
<td>3 (분산)</td>
<td>Prometheus, Grafana, Vault</td>
<td>높음 (네트워크 + 복제)</td>
</tr>
<tr>
<td><code>longhorn-vm</code></td>
<td>1 (로컬 우선)</td>
<td>KubeVirt VM 디스크</td>
<td>높음 (data-locality)</td>
</tr>
</tbody></table>
<p><strong>선택 기준:</strong></p>
<ul>
<li><strong>DB/캐시</strong>: 자체 복제 메커니즘이 있으므로 → <code>local-path</code> (스토리지 레벨 복제 불필요, 최대 IOPS)</li>
<li><strong>상태 저장 서비스</strong>: 자체 복제 없음 → <code>longhorn</code> 3 replica (스토리지 레벨 HA)</li>
<li><strong>VM 디스크</strong>: 성능 우선 + 단일 복제로 충분 → <code>longhorn-vm</code> 1 replica</li>
</ul>
<h3 id="102-harbor-레지스트리-s3-백엔드">10.2 Harbor 레지스트리: S3 백엔드</h3>
<p>컨테이너 이미지는 로컬 스토리지가 아닌 AWS S3에 저장합니다:</p>
<pre><code class="language-yaml"># Harbor Helm Values
persistence:
  imageChartStorage:
    type: s3
    s3:
      region: ap-northeast-2
      bucket: harbor-registry-bucket
      accesskey: ****
      secretkey: ****
      rootdirectory: /harbor</code></pre>
<p><strong>S3를 선택한 이유:</strong></p>
<ul>
<li>컨테이너 이미지는 용량이 크고 계속 증가 → 로컬 디스크 한계</li>
<li>S3는 사실상 무제한 용량 + 자동 내구성(11-9s)</li>
<li>이미지 pull 시 네트워크 latency는 있지만, pull은 배포 시에만 발생하므로 수용 가능</li>
</ul>
<hr>
<h2 id="11-프라이빗-컨테이너-레지스트리-harbor">11. 프라이빗 컨테이너 레지스트리: Harbor</h2>
<h3 id="111-구성">11.1 구성</h3>
<pre><code class="language-yaml"># Harbor Helm Values 핵심
expose:
  type: ingress         # Istio Gateway 경유
  tls:
    certSource: secret
    secret:
      secretName: harbor-tls    # cert-manager 발급
  ingress:
    className: istio
    hosts:
      core: harbor.example.com

# Trivy 취약점 스캔
trivy:
  enabled: true
  autoScan: true</code></pre>
<p><strong>Harbor가 제공하는 기능:</strong></p>
<ul>
<li>프라이빗 Docker/Helm 레지스트리</li>
<li>Trivy 기반 이미지 취약점 자동 스캔</li>
<li>이미지 서명 (Content Trust)</li>
<li>복제 정책 (다른 레지스트리와 동기화)</li>
<li>RBAC (프로젝트/사용자별 권한)</li>
</ul>
<hr>
<h2 id="12-dns-자동화-external-dns">12. DNS 자동화: External DNS</h2>
<p>Route53 DNS 레코드를 Kubernetes 리소스에서 자동으로 관리합니다:</p>
<pre><code class="language-yaml"># External DNS ArgoCD Application
spec:
  sources:
    - repoURL: https://kubernetes-sigs.github.io/external-dns/
      chart: external-dns
      helm:
        valuesObject:
          provider:
            name: aws
          env:
            - name: AWS_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: route53-credentials
                  key: access-key
            - name: AWS_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: route53-credentials
                  key: secret-key
          domainFilters:
            - example.com
          policy: sync           # 레코드 삭제도 자동
          txtOwnerId: k8s-cluster</code></pre>
<p>Gateway 또는 VirtualService를 생성하면 자동으로 Route53에 A 레코드가 생성됩니다.</p>
<hr>
<h2 id="13-ansible-노드-프로비저닝-자동화">13. Ansible: 노드 프로비저닝 자동화</h2>
<p>클러스터 초기 구축은 Ansible로 자동화했습니다:</p>
<pre><code>ansible/
├── inventory/
│   └── hosts.yml          # 7노드 인벤토리
├── playbooks/
│   ├── 01-common.yml      # 공통 설정 (NTP, swap off, kernel)
│   ├── 02-containerd.yml  # 컨테이너 런타임
│   ├── 03-kubernetes.yml  # kubeadm, kubelet, kubectl
│   ├── 04-init-master.yml # 첫 번째 CP 초기화
│   ├── 05-join-cp.yml     # 나머지 CP 조인
│   └── 06-join-worker.yml # Worker 조인
└── roles/
    └── common/            # 재사용 가능한 역할</code></pre><p><strong>자동화된 작업:</strong></p>
<ul>
<li>Swap 비활성화 + kernel module 로드 (br_netfilter, overlay)</li>
<li>containerd 설치 및 SystemdCgroup 설정</li>
<li>kubeadm init (첫 CP) → certificate-key 공유 → 나머지 CP join</li>
<li>Worker node join</li>
<li>Calico CNI 배포</li>
</ul>
<hr>
<h2 id="14-운영-모니터링-대시보드">14. 운영 모니터링 대시보드</h2>
<table>
<thead>
<tr>
<th>도구</th>
<th>용도</th>
<th>접근 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Grafana</strong></td>
<td>메트릭 시각화, 알림</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>Kiali</strong></td>
<td>서비스 메시 토폴로지</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>OpenSearch Dashboards</strong></td>
<td>로그 분석, 트레이스</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>Longhorn UI</strong></td>
<td>스토리지 볼륨 관리</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>ArgoCD UI</strong></td>
<td>GitOps 배포 상태</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>Harbor</strong></td>
<td>이미지 레지스트리</td>
<td>Istio Gateway (HTTPS)</td>
</tr>
<tr>
<td><strong>CloudBeaver</strong></td>
<td>DB 관리 (Web SQL IDE)</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>RedisInsight</strong></td>
<td>Redis 클러스터 모니터링</td>
<td>NodePort</td>
</tr>
<tr>
<td><strong>kubevirt-manager</strong></td>
<td>VM 관리 대시보드</td>
<td>Istio Gateway (HTTPS)</td>
</tr>
<tr>
<td><strong>Vault UI</strong></td>
<td>시크릿 관리</td>
<td>NodePort</td>
</tr>
</tbody></table>
<hr>
<h2 id="마무리">마무리</h2>
<p>7대의 물리 서버에서 시작하여, 클라우드 수준의 자동화된 인프라를 구축했습니다:</p>
<ul>
<li><strong>26개 ArgoCD Application</strong>으로 모든 인프라를 선언적으로 관리</li>
<li><strong>3중화</strong> Control Plane, MariaDB, Redis, Vault, OpenSearch</li>
<li><strong>Istio Service Mesh</strong>로 트래픽 관리, mTLS, 분산 트레이싱</li>
<li><strong>OpenTelemetry</strong>로 코드 수정 없는 자동 계측</li>
<li><strong>물리 DMZ + VPN</strong>으로 엔터프라이즈급 네트워크 보안</li>
<li><strong>Vault + External Secrets</strong>로 시크릿 중앙 관리</li>
</ul>
<p>On-Premise라고 해서 클라우드 대비 운영 효율이 떨어질 필요는 없습니다. 올바른 도구와 아키텍처를 선택하면, 오히려 하드웨어를 직접 제어할 수 있는 장점(GPU 스케줄링, 로컬 SSD 성능, 네트워크 토폴로지 최적화)을 누릴 수 있습니다.</p>
<blockquote>
<p><strong>기술 스택 요약</strong>: Kubernetes v1.30.4 | ArgoCD | Istio v1.24.3 (Sail Operator) | HashiCorp Vault HA | MariaDB GTID Replication + MaxScale | Redis Cluster 6-node | OpenSearch 3-node | Prometheus + Grafana | OpenTelemetry | Vector | Harbor (S3) | MetalLB | Calico | KubeVirt | cert-manager | External DNS | Longhorn | Ansible</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 9: Istio Service Mesh 소개와 첫걸음]]></title>
            <link>https://velog.io/@arnold_99/Day-9-Istio-Service-Mesh-%EC%86%8C%EA%B0%9C%EC%99%80-%EC%B2%AB%EA%B1%B8%EC%9D%8C</link>
            <guid>https://velog.io/@arnold_99/Day-9-Istio-Service-Mesh-%EC%86%8C%EA%B0%9C%EC%99%80-%EC%B2%AB%EA%B1%B8%EC%9D%8C</guid>
            <pubDate>Sun, 09 Nov 2025 15:52:50 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-service-mesh%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1">마이크로서비스의 한계와 Service Mesh의 필요성</a></li>
<li><a href="#2-istio%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">Istio란 무엇인가</a></li>
<li><a href="#3-istio-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B9%8A%EC%9D%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">Istio 아키텍처 깊이 이해하기</a></li>
<li><a href="#4-%EC%8B%A4%EC%8A%B5-istio-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95">실습: Istio 환경 구축</a></li>
<li><a href="#5-gateway%EC%99%80-virtualservice-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4">Gateway와 VirtualService 완벽 이해</a></li>
<li><a href="#6-%ED%8A%B8%EB%9E%98%ED%94%BD-%ED%9D%90%EB%A6%84-%EC%99%84%EC%A0%84-%EB%B6%84%EC%84%9D">트래픽 흐름 완전 분석</a></li>
<li><a href="#7-istio%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EA%B8%B0%EB%8A%A5%EB%93%A4">Istio의 강력한 기능들</a></li>
<li><a href="#%EB%8B%A4%EC%9D%8C-%EA%B3%84%ED%9A%8D">다음 계획</a></li>
</ol>
<hr>
<h2 id="1-마이크로서비스의-한계와-service-mesh의-필요성">1. 마이크로서비스의 한계와 Service Mesh의 필요성</h2>
<h3 id="마이크로서비스-환경의-복잡성">마이크로서비스 환경의 복잡성</h3>
<p>현대 클라우드 네이티브 애플리케이션은 수십, 수백 개의 마이크로서비스로 구성됩니다. 각 서비스는 독립적으로 배포되고 확장되지만, 이로 인해 새로운 문제들이 발생합니다:</p>
<pre><code>전통적인 모놀리식 아키텍처:
┌─────────────────────────────┐
│     Single Application   │
│  ┌─────┐ ┌─────┐ ┌─────┐   │
│  │ UI  │ │Logic│ │ DB  │  │
│  └─────┘ └─────┘ └─────┘   │
└─────────────────────────────┘
문제: 단일 장애점, 확장성 제한

마이크로서비스 아키텍처:
┌─────┐    ┌─────┐      ┌─────┐
│User │───▶│Order│───▶│ Pay │
│  UI │    │ Svc │     │ Svc │
└─────┘    └──┬──┘      └─────┘
              │
              ├─────▶┌─────┐
              │      │Notif│
              │      └─────┘
              │
              └─────▶┌─────┐
                     │ Log │
                     └─────┘
문제: 서비스 간 통신 복잡도 폭발!</code></pre><h3 id="개발자가-직면하는-문제들">개발자가 직면하는 문제들</h3>
<p><strong>1. 서비스 간 통신 관리의 복잡성</strong></p>
<pre><code class="language-python"># 각 서비스 코드에 반복적으로 작성해야 하는 코드들

# 재시도 로직
for retry in range(3):
    try:
        response = requests.get(&#39;http://payment-service/pay&#39;)
        break
    except TimeoutError:
        if retry == 2:
            raise
        time.sleep(2 ** retry)

# 타임아웃 설정
response = requests.get(&#39;http://service&#39;, timeout=3.0)

# 서킷 브레이커
if circuit_breaker.is_open(&#39;payment-service&#39;):
    return fallback_response()

# 로깅
logger.info(f&quot;Calling {service_name} at {timestamp}&quot;)

# 메트릭 수집
metrics.increment(&#39;http_requests_total&#39;)</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>모든 서비스에 동일한 로직 중복 구현</li>
<li>언어별로 다른 라이브러리 사용 (Java, Python, Go...)</li>
<li>업그레이드 시 모든 서비스 수정 필요</li>
<li>일관성 없는 구현</li>
</ul>
<p><strong>2. 관측성(Observability) 부재</strong></p>
<ul>
<li>&quot;어느 서비스에서 장애가 발생했나?&quot;</li>
<li>&quot;왜 응답 시간이 느려졌나?&quot;</li>
<li>&quot;서비스 A → B → C로 요청이 흐르는데 어디서 병목인가?&quot;</li>
</ul>
<p><strong>3. 보안 문제</strong></p>
<ul>
<li>서비스 간 통신 암호화 (mTLS) 수동 구현</li>
<li>인증/인가 로직 각 서비스에 구현</li>
<li>인증서 관리의 복잡성</li>
</ul>
<h3 id="service-mesh가-해결하는-방법">Service Mesh가 해결하는 방법</h3>
<p><strong>핵심 아이디어: 네트워크 기능을 애플리케이션 코드에서 분리</strong></p>
<pre><code>Service Mesh 없이:
┌─────────────────────────────┐
│   Application Code        │
│  ┌──────────────────────┐   │
│  │  Business Logic    │   │
│  ├──────────────────────┤   │
│  │ ❌ Retry Logic      │  │ ← 중복 코드!
│  │ ❌ Timeout          │  │
│  │ ❌ Metrics          │  │
│  │ ❌ Tracing          │  │
│  │ ❌ mTLS             │  │
│  └──────────────────────┘   │
└─────────────────────────────┘

Service Mesh와 함께:
┌─────────────────────────────┐
│   Application Code        │
│  ┌──────────────────────┐   │
│  │  Business Logic     │  │ ← 비즈니스 로직만!
│  │  (순수 코드)         │  │
│  └──────────────────────┘   │
└─────────────────────────────┘
              ↕
┌─────────────────────────────┐
│      Sidecar Proxy        │
│  ┌──────────────────────┐   │
│  │ ✅ Retry Logic      │  │ ← 인프라 레벨에서 처리
│  │ ✅ Timeout          │  │
│  │ ✅ Metrics          │  │
│  │ ✅ Tracing          │  │
│  │ ✅ mTLS             │  │
│  └──────────────────────┘   │
└─────────────────────────────┘</code></pre><hr>
<h2 id="2-istio란-무엇인가">2. Istio란 무엇인가</h2>
<h3 id="istio-정의">Istio 정의</h3>
<p><strong>Istio는 오픈소스 Service Mesh 플랫폼</strong>으로, 마이크로서비스 간 통신을 관리하고 보안, 관측성, 트래픽 제어를 제공하는 인프라 레이어입니다.</p>
<p><strong>주요 특징:</strong></p>
<ul>
<li><strong>애플리케이션 코드 변경 없음</strong>: 기존 서비스 그대로 사용</li>
<li><strong>언어 독립적</strong>: Java, Python, Go, Node.js 등 모두 지원</li>
<li><strong>선언적 구성</strong>: YAML로 트래픽 정책 정의</li>
<li><strong>강력한 관측성</strong>: 메트릭, 로그, 분산 추적 자동 제공</li>
</ul>
<h3 id="istio가-제공하는-핵심-기능">Istio가 제공하는 핵심 기능</h3>
<pre><code>┌──────────────────────────────────────────────────────┐
│                   Istio Service Mesh            │
├──────────────────────────────────────────────────────┤
│                                                 │
│  📊 Traffic Management (트래픽 관리)              │
│  ├─ Canary Deployment (점진적 배포)               │
│  ├─ A/B Testing (버전 분기)                       │
│  ├─ Circuit Breaker (장애 격리)                   │
│  └─ Retry &amp; Timeout (자동 재시도)                  │
│                                                  │
│  🔒 Security (보안)                              │
│  ├─ Mutual TLS (서비스 간 암호화)                  │
│  ├─ Authentication (인증)                         │
│  └─ Authorization (권한 관리)                      │
│                                                  │
│  👁 Observability (관측성)                         │
│  ├─ Metrics (메트릭 자동 수집)                      │
│  ├─ Distributed Tracing (분산 추적)                │
│  ├─ Access Logs (접근 로그)                        │
│  └─ Topology Visualization (토폴로지 시각화)        │
│                                                  │
└──────────────────────────────────────────────────────┘</code></pre><hr>
<h2 id="3-istio-아키텍처-깊이-이해하기">3. Istio 아키텍처 깊이 이해하기</h2>
<h3 id="전체-아키텍처">전체 아키텍처</h3>
<pre><code>┌─────────────────────────────────────────────────────────────┐
│                    Control Plane                       │
│  ┌────────────────────────────────────────────────────┐    │
│  │                    istiod                     │     │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐    │     │
│  │  │  Pilot  │  │ Citadel  │  │   Galley   │    │     │
│  │  │(트래픽)  │  │ (보안)   │  │ (설정 검증)   │    │     │
│  │  └──────────┘  └──────────┘  └──────────────┘    │     │
│  └─────────────────────┬──────────────────────────────┘     │
│                        │ xDS API (설정 푸시)             │
└────────────────────────┼────────────────────────────────────┘
                         │
        ┌────────────────┼────────────────┐
        │                │             │
        ▼                ▼             ▼
┌─────────────────────────────────────────────────────────────┐
│                      Data Plane                        │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐       │
│  │   Pod A    │   │   Pod B    │   │   Pod C    │       │
│  │ ┌─────────┐ │   │ ┌─────────┐ │   │ ┌─────────┐ │       │
│  │ │  App   │ │   │ │  App    │ │   │ │  App   │ │       │
│  │ └────┬────┘ │   │ └────┬────┘ │   │ └────┬────┘ │       │
│  │     │      │   │      │      │   │      │     │       │
│  │ ┌────▼────┐ │   │ ┌────▼────┐ │    │ ┌────▼────┐ │       │
│  │ │ Envoy  │◀┼───┼▶│ Envoy  │◀┼───┼▶│ Envoy  │ │      │
│  │ │ Proxy  │ │   │ │ Proxy   │ │    │ │ Proxy  │ │      │
│  │ └─────────┘ │   │  └─────────┘ │   │ └─────────┘ │       │
│  └─────────────┘    └─────────────┘   └─────────────┘       │
│      Sidecar           Sidecar           Sidecar       │
└─────────────────────────────────────────────────────────────┘</code></pre><h3 id="control-plane-istiod">Control Plane: istiod</h3>
<p><strong>istiod는 3가지 핵심 컴포넌트를 통합한 단일 바이너리</strong>입니다 (Istio 1.5 이후):</p>
<p><strong>1. Pilot (트래픽 관리)</strong></p>
<pre><code class="language-yaml">역할: VirtualService, DestinationRule 등을 Envoy 설정으로 변환

예시:
VirtualService (사람이 작성) → Pilot → Envoy Config (기계가 이해)

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews-route
spec:
  hosts:
  - reviews
  http:
  - match:
    - headers:
        user:
          exact: &quot;jason&quot;
    route:
    - destination:
        host: reviews
        subset: v2    # jason만 v2로 라우팅
  - route:
    - destination:
        host: reviews
        subset: v1    # 나머지는 v1로</code></pre>
<p><strong>2. Citadel (보안 관리)</strong></p>
<pre><code>역할: 인증서 자동 발급 및 갱신

동작 과정:
1. Pod 생성 시 Service Account 확인
2. X.509 인증서 자동 발급 (90일 만료)
3. Envoy에 인증서 주입
4. 서비스 간 mTLS 자동 활성화
5. 인증서 만료 전 자동 갱신

결과: 개발자가 인증서 관리 불필요!</code></pre><p><strong>3. Galley (설정 검증)</strong></p>
<pre><code>역할: Istio 설정 YAML 유효성 검증

검증 항목:
- YAML 문법 오류
- 필수 필드 누락
- 중복된 리소스
- 참조 무결성 (존재하지 않는 서비스 참조 등)

kubectl apply 전에 미리 검증:
istioctl analyze</code></pre><h3 id="data-plane-envoy-proxy">Data Plane: Envoy Proxy</h3>
<p><strong>Envoy는 고성능 L7 프록시</strong>로 각 Pod에 Sidecar로 주입됩니다.</p>
<p><strong>Sidecar 패턴 동작 원리:</strong></p>
<pre><code>Pod 내부 구조:

┌─────────────────────────────────────────────┐
│              Pod (productpage-v1)       │
│                                         │
│  ┌────────────────────────────────────┐    │
│  │    productpage Container        │    │
│  │    (Python Flask App)           │    │
│  │    Port: 9080                   │    │
│  │    localhost:9080으로 요청 보냄   │    │
│  └──────────────┬─────────────────────┘    │
│                │                        │
│                │ 127.0.0.1:9080         │
│                ▼                        │
│  ┌────────────────────────────────────┐    │
│  │    istio-proxy Container        │    │
│  │    (Envoy Sidecar)              │    │
│  │                                 │    │
│  │  ┌──────────────────────────────┐  │    │
│  │  │  Inbound Listener         │  │    │
│  │  │  15006: 모든 inbound 트래픽 │  │    │
│  │  └──────────────────────────────┘  │    │
│  │                                 │    │
│  │  ┌──────────────────────────────┐  │    │
│  │  │  Outbound Listener        │  │    │
│  │  │  15001: 모든 outbound 트래픽│  │    │
│  │  └──────────────────────────────┘  │    │
│  └────────────────────────────────────┘    │
│                                         │
└─────────────────────────────────────────────┘</code></pre><p><strong>Envoy가 수행하는 작업:</strong></p>
<ol>
<li><p><strong>트래픽 인터셉트</strong></p>
<pre><code class="language-bash"># iptables 규칙으로 모든 트래픽을 Envoy로 리다이렉트

Outbound 트래픽:
App → localhost:9080
    ↓ (iptables 리다이렉트)
Envoy:15001 → 목적지 서비스

Inbound 트래픽:
외부 → Envoy:15006 → App:9080</code></pre>
</li>
<li><p><strong>로드 밸런싱</strong></p>
<pre><code>reviews 서비스 호출 시:

Envoy가 3개 엔드포인트 중 선택:
- reviews-v1-pod-1 (10.1.2.3:9080)
- reviews-v2-pod-1 (10.1.2.4:9080)
- reviews-v3-pod-1 (10.1.2.5:9080)

알고리즘: Round Robin, Random, Least Request</code></pre></li>
<li><p><strong>재시도 &amp; 타임아웃</strong></p>
<pre><code class="language-yaml"># VirtualService에 정의
http:
- route:
  - destination:
      host: reviews
  retries:
    attempts: 3           # 3번 재시도
    perTryTimeout: 2s     # 시도당 2초</code></pre>
</li>
<li><p><strong>메트릭 자동 수집</strong></p>
<pre><code>모든 요청에 대해 자동 수집:
- 요청 수 (istio_requests_total)
- 응답 시간 (istio_request_duration_milliseconds)
- 에러율 (istio_request_errors_total)
- 트래픽 크기 (istio_request_bytes)</code></pre></li>
</ol>
<hr>
<h2 id="4-실습-istio-환경-구축">4. 실습: Istio 환경 구축</h2>
<h3 id="환경-정보">환경 정보</h3>
<pre><code class="language-bash"># 클러스터 정보
kubectl get nodes -o wide</code></pre>
<pre><code>NAME   ROLES           VERSION    INTERNAL-IP
cpu1   control-plane   v1.31.13   172.30.1.43
cpu2   worker          v1.31.13   172.30.1.80
gpu1   worker          v1.31.13   172.30.1.38</code></pre><h3 id="step-1-istio-다운로드">Step 1: Istio 다운로드</h3>
<pre><code class="language-bash">cd ~/istio-demo
curl -L https://istio.io/downloadIstio | sh -

# 환경변수 설정
export PATH=&quot;/root/istio-demo/istio-1.28.0/bin:$PATH&quot;

# 확인
istioctl version</code></pre>
<p><strong>다운로드된 디렉토리 구조:</strong></p>
<pre><code>istio-1.28.0/
├── bin/
│   └── istioctl              # Istio CLI 도구
├── manifests/
│   ├── profiles/             # 설치 프로파일
│   │   ├── default.yaml      # 프로덕션용 (우리가 사용)
│   │   ├── demo.yaml         # 데모/학습용
│   │   └── minimal.yaml      # 최소 설치
│   └── charts/               # Helm Charts
├── samples/
│   ├── bookinfo/             # 샘플 애플리케이션
│   │   ├── platform/kube/    # Pod/Service 정의
│   │   └── networking/       # Gateway/VirtualService
│   └── addons/               # 관측 도구
│       ├── kiali.yaml
│       ├── prometheus.yaml
│       └── grafana.yaml
└── tools/                    # 유틸리티</code></pre><h3 id="step-2-istio-설치-default-프로파일">Step 2: Istio 설치 (default 프로파일)</h3>
<p><strong>프로파일 비교:</strong></p>
<table>
<thead>
<tr>
<th>프로파일</th>
<th>용도</th>
<th>istiod</th>
<th>Ingress GW</th>
<th>Egress GW</th>
</tr>
</thead>
<tbody><tr>
<td><code>default</code></td>
<td>프로덕션</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td><code>demo</code></td>
<td>학습/데모</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td><code>minimal</code></td>
<td>최소 구성</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
</tr>
</tbody></table>
<p><strong>설치 명령:</strong></p>
<pre><code class="language-bash">istioctl install --set profile=default -y</code></pre>
<p><strong>설치 결과 확인:</strong></p>
<pre><code class="language-bash">kubectl get pods -n istio-system -o wide</code></pre>
<pre><code>NAME                                    READY   STATUS    NODE
istiod-57b4d7f8b8-875cs                 1/1     Running   cpu2
istio-ingressgateway-76cc55cb99-9nvnt   1/1     Running   cpu1</code></pre><p><strong>컴포넌트 설명:</strong></p>
<ol>
<li><p><strong>istiod (Control Plane)</strong></p>
<ul>
<li>Pilot: 트래픽 관리</li>
<li>Citadel: mTLS 인증서 관리</li>
<li>Galley: 설정 검증</li>
<li>단일 바이너리로 통합</li>
</ul>
</li>
<li><p><strong>istio-ingressgateway (Ingress Gateway)</strong></p>
<ul>
<li>외부 트래픽 진입점</li>
<li>Envoy 기반 L7 로드 밸런서</li>
<li>LoadBalancer 타입 Service</li>
</ul>
</li>
</ol>
<p><strong>왜 Egress Gateway가 없나?</strong></p>
<ul>
<li>default 프로파일은 프로덕션 환경 최적화</li>
<li>Egress Gateway는 외부 API 호출 제어가 필요할 때만 사용</li>
<li>필요시 별도 설치 가능</li>
</ul>
<h3 id="step-3-sidecar-자동-주입-설정">Step 3: Sidecar 자동 주입 설정</h3>
<p><strong>Sidecar 자동 주입 원리:</strong></p>
<pre><code>Kubernetes Mutating Admission Webhook 활용

1. kubectl apply로 Pod 생성 요청
   ↓
2. API Server가 Webhook 호출
   ↓
3. Istio가 Pod spec 수정 (istio-proxy 컨테이너 추가)
   ↓
4. 수정된 spec으로 Pod 생성</code></pre><p><strong>default namespace에 자동 주입 활성화:</strong></p>
<pre><code class="language-bash">kubectl label namespace default istio-injection=enabled

# 확인
kubectl get namespace default --show-labels</code></pre>
<pre><code>NAME      LABELS
default   istio-injection=enabled,kubernetes.io/metadata.name=default</code></pre><p><strong>동작 확인:</strong></p>
<ul>
<li>이제 <code>default</code> namespace에 생성되는 모든 Pod에 자동으로 <code>istio-proxy</code> 컨테이너가 주입됩니다.</li>
</ul>
<h3 id="step-4-bookinfo-샘플-애플리케이션-배포">Step 4: Bookinfo 샘플 애플리케이션 배포</h3>
<p><strong>Bookinfo 아키텍처:</strong></p>
<pre><code>┌──────────────────────────────────────────────────────┐
│                   Bookinfo App                   │
│                                                  │
│  ┌─────────────┐                                  │
│  │ productpage│ (Python)                         │
│  │     v1     │                                  │
│  └──────┬──────┘                                  │
│         │                                        │
│    ├────┼─────┬─────────────┐                      │
│    │    │     │             │                    │
│    ▼    ▼     ▼             ▼                    │
│  ┌───┐ ┌───┐ ┌───┐         ┌───┐                    │
│  │det│ │rev│ │rev│        │rat│                   │
│  │ v1│ │ v1│ │ v2│ ★     │ v1│                  │
│  └───┘ └───┘ └─┬─┘         └───┘                    │
│  Ruby   Java   │ │         Node.js               │
│                │ │                               │
│              ┌─▼─▼┐                              │
│              │rev │ ★★                         │
│              │ v3 │                              │
│              └────┘                              │
│               Java                               │
│                                                  │
│  ★ = reviews v2/v3는 ratings 호출                │
│  v1/v2/v3 = 3가지 버전으로 Canary 테스트 가능       │
└──────────────────────────────────────────────────────┘</code></pre><p><strong>배포:</strong></p>
<pre><code class="language-bash">kubectl apply -f ~/istio-demo/istio-1.28.0/samples/bookinfo/platform/kube/bookinfo.yaml</code></pre>
<p><strong>결과 확인:</strong></p>
<pre><code class="language-bash">kubectl get pods</code></pre>
<pre><code>NAME                              READY   STATUS    RESTARTS   AGE
details-v1-77b775f46-m68lr        2/2     Running   0          5m
productpage-v1-78dfd4688c-k7m2h   2/2     Running   0          5m
ratings-v1-7c4c8d6794-bgd99       2/2     Running   0          5m
reviews-v1-849f9bc5d6-glqll       2/2     Running   0          5m
reviews-v2-5c757d5846-rxn2k       2/2     Running   0          5m
reviews-v3-6d5d98f5c4-n4gj8       2/2     Running   0          5m</code></pre><p><strong>Sidecar 주입 확인:</strong></p>
<pre><code class="language-bash">kubectl get pod productpage-v1-78dfd4688c-k7m2h -o jsonpath=&#39;{range .spec.containers[*]}{.name}{&quot;\n&quot;}{end}&#39;</code></pre>
<pre><code>productpage       ← 애플리케이션 컨테이너
istio-proxy       ← Envoy Sidecar (자동 주입됨!)</code></pre><p><strong>Pod 내부 구조 확인:</strong></p>
<pre><code class="language-bash">kubectl describe pod productpage-v1-78dfd4688c-k7m2h</code></pre>
<pre><code class="language-yaml">Init Containers:
  istio-init:                    # iptables 규칙 설정
    Image: docker.io/istio/proxyv2:1.28.0

Containers:
  productpage:                   # 애플리케이션
    Image: docker.io/istio/examples-bookinfo-productpage-v1:1.20.3
    Port: 9080/TCP

  istio-proxy:                   # Envoy Sidecar
    Image: docker.io/istio/proxyv2:1.28.0
    Ports: 15090/TCP (메트릭), 15021/TCP (헬스체크)</code></pre>
<hr>
<h2 id="5-gateway와-virtualservice-완벽-이해">5. Gateway와 VirtualService 완벽 이해</h2>
<h3 id="gateway-리소스">Gateway 리소스</h3>
<p><strong>Gateway는 Istio의 외부 트래픽 진입점 설정</strong>입니다.</p>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: bookinfo-gateway
spec:
  # 어떤 Ingress Gateway Pod에 이 설정을 적용할지 선택
  selector:
    istio: ingressgateway    # istio=ingressgateway 라벨을 가진 Pod 선택

  # 어떤 포트와 프로토콜을 열지 정의
  servers:
  - port:
      number: 8080           # Envoy 내부 리스닝 포트
      name: http
      protocol: HTTP
    hosts:
    - &quot;*&quot;                    # 모든 호스트명 허용</code></pre>
<p><strong>항목별 설명:</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>selector</code></td>
<td>어떤 Ingress Gateway Pod에 적용할지</td>
<td><code>istio: ingressgateway</code></td>
</tr>
<tr>
<td><code>port.number</code></td>
<td>Envoy가 리스닝할 포트</td>
<td><code>8080</code> (Service에서 80→8080 매핑)</td>
</tr>
<tr>
<td><code>port.protocol</code></td>
<td>프로토콜</td>
<td><code>HTTP</code>, <code>HTTPS</code>, <code>TCP</code></td>
</tr>
<tr>
<td><code>hosts</code></td>
<td>허용할 호스트명</td>
<td><code>*</code> (모두), <code>bookinfo.com</code></td>
</tr>
</tbody></table>
<p><strong>Selector 매칭 원리:</strong></p>
<pre><code class="language-bash"># Gateway 리소스의 selector
selector:
  istio: ingressgateway

# istio-ingressgateway Pod의 라벨 확인
kubectl get pod -n istio-system -l istio=ingressgateway --show-labels</code></pre>
<pre><code>NAME                                    LABELS
istio-ingressgateway-76cc55cb99-9nvnt   istio=ingressgateway,...
                                                ↑
                                        매칭됨! 이 Pod에 설정 적용</code></pre><h3 id="virtualservice-리소스">VirtualService 리소스</h3>
<p><strong>VirtualService는 트래픽 라우팅 규칙</strong>을 정의합니다.</p>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: bookinfo
spec:
  # 어떤 호스트 요청을 처리할지
  hosts:
  - &quot;*&quot;                      # 모든 호스트 요청 받음

  # 어느 Gateway를 통해 들어온 트래픽에 적용할지
  gateways:
  - bookinfo-gateway         # 위에서 만든 Gateway와 연결

  # HTTP 라우팅 규칙
  http:
  - match:                   # 다음 URL 중 하나라도 매칭되면
    - uri:
        exact: /productpage       # 정확히 /productpage
    - uri:
        prefix: /static           # /static으로 시작
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products

    route:                   # 위 조건에 매칭되면 여기로 라우팅
    - destination:
        host: productpage    # productpage 서비스로 전달
        port:
          number: 9080       # 9080 포트로</code></pre>
<p><strong>항목별 설명:</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>hosts</code></td>
<td>처리할 호스트명</td>
<td><code>*</code>, <code>reviews.default.svc.cluster.local</code></td>
</tr>
<tr>
<td><code>gateways</code></td>
<td>적용할 Gateway</td>
<td><code>bookinfo-gateway</code>, <code>mesh</code> (내부 트래픽)</td>
</tr>
<tr>
<td><code>match.uri.exact</code></td>
<td>정확한 경로 매칭</td>
<td><code>/productpage</code></td>
</tr>
<tr>
<td><code>match.uri.prefix</code></td>
<td>접두사 매칭</td>
<td><code>/static</code> → <code>/static/css/style.css</code> 포함</td>
</tr>
<tr>
<td><code>destination.host</code></td>
<td>목적지 서비스</td>
<td><code>productpage</code> (Kubernetes Service 이름)</td>
</tr>
</tbody></table>
<p><strong>Gateway + VirtualService 연결 원리:</strong></p>
<pre><code>1. Gateway 리소스 생성
   name: bookinfo-gateway

2. VirtualService에서 참조
   gateways:
   - bookinfo-gateway    ← 이름으로 연결!

3. istiod가 매칭을 감지하고 Envoy 설정 생성</code></pre><h3 id="배포-및-확인">배포 및 확인</h3>
<pre><code class="language-bash">kubectl apply -f ~/istio-demo/istio-1.28.0/samples/bookinfo/networking/bookinfo-gateway.yaml

# 확인
kubectl get gateway,virtualservice</code></pre>
<pre><code>NAME                                           AGE
gateway.networking.istio.io/bookinfo-gateway   1m

NAME                                          GATEWAYS               HOSTS
virtualservice.networking.istio.io/bookinfo   [&quot;bookinfo-gateway&quot;]   [&quot;*&quot;]</code></pre><hr>
<h2 id="6-트래픽-흐름-완전-분석">6. 트래픽 흐름 완전 분석</h2>
<h3 id="포트-매핑-이해">포트 매핑 이해</h3>
<p><strong>istio-ingressgateway Service 확인:</strong></p>
<pre><code class="language-bash">kubectl get svc -n istio-system istio-ingressgateway -o yaml | grep -A 20 &quot;ports:&quot;</code></pre>
<pre><code class="language-yaml">ports:
- name: status-port
  nodePort: 30670
  port: 15021
  targetPort: 15021
- name: http2
  nodePort: 30192         # ← NodePort (외부 접근)
  port: 80                # ← Service Port
  targetPort: 8080        # ← Pod 내부 Envoy 포트
- name: https
  nodePort: 31797
  port: 443
  targetPort: 8443</code></pre>
<p><strong>포트 매핑:</strong></p>
<pre><code>외부 요청: http://172.30.1.43:30192/productpage

NodePort 30192
    ↓
Service Port 80
    ↓
Pod targetPort 8080
    ↓
Envoy Listener (0.0.0.0:8080)
    ↓
Gateway 리소스 (port: 8080에서 매칭)
    ↓
VirtualService 라우팅 (/productpage → productpage:9080)
    ↓
productpage Service (ClusterIP 10.105.75.162:9080)
    ↓
productpage Pod
    ├─ istio-proxy (15006 → 9080 포트포워딩)
    └─ productpage Container (9080)</code></pre><h3 id="전체-트래픽-흐름-다이어그램">전체 트래픽 흐름 다이어그램</h3>
<pre><code>┌─────────────────────────────────────────────────────────────────────┐
│                         외부 사용자 (브라우저)                   │
│                    curl http://172.30.1.43:30192/productpage  │
└──────────────────────┬──────────────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Kubernetes Node (cpu1)                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │          NodePort 30192 (iptables NAT)                 │   │
│  │          DNAT: 외부IP:30192 → Service:80                │   │
│  └──────────────────────┬──────────────────────────────────────┘   │
└─────────────────────────┼───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│              istio-ingressgateway Service                     │
│                  Type: LoadBalancer                           │
│                  ClusterIP: 10.104.64.238:80                  │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │  PORT MAPPING:                                         │    │
│  │  port: 80  →  targetPort: 8080                         │    │
│  └──────────────────────┬─────────────────────────────────────┘    │
└─────────────────────────┼───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│         istio-ingressgateway Pod (istio-system namespace)     │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │              Envoy Proxy (Container)                  │    │
│  │  ┌──────────────────────────────────────────────────┐      │    │
│  │  │  Listener: 0.0.0.0:8080 (HTTP)               │      │    │
│  │  │  &quot;8080 포트에서 HTTP 요청 대기 중...&quot;           │      │    │
│  │  └─────────────────┬────────────────────────────────┘      │    │
│  │                    │                                   │    │
│  │                    ▼                                   │    │
│  │  ┌──────────────────────────────────────────────────┐      │    │
│  │  │  Gateway 리소스 매칭 (bookinfo-gateway)        │      │    │
│  │  │  - selector: istio=ingressgateway ✓          │      │    │
│  │  │  - port: 8080 ✓                              │      │    │
│  │  │  - hosts: &quot;*&quot; ✓                              │      │    │
│  │  │  → &quot;이 Gateway 설정 적용!&quot;                     │      │    │
│  │  └─────────────────┬────────────────────────────────┘      │    │
│  │                    │                                   │    │
│  │                    ▼                                   │    │
│  │  ┌──────────────────────────────────────────────────┐      │    │
│  │  │  VirtualService 라우팅 규칙 (bookinfo)         │      │    │
│  │  │  - gateways: [bookinfo-gateway] ✓            │      │    │
│  │  │  - uri.exact: &quot;/productpage&quot; ✓               │      │    │
│  │  │  - destination.host: &quot;productpage&quot;           │      │    │
│  │  │  - destination.port: 9080                    │      │    │
│  │  │  → &quot;productpage:9080으로 라우팅!&quot;              │      │    │
│  │  └─────────────────┬────────────────────────────────┘      │    │
│  └────────────────────┼───────────────────────────────────────┘    │
└─────────────────────────┼───────────────────────────────────────────┘
                          │ HTTP Request
                          │ Host: productpage:9080
                          │ Path: /productpage
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│              productpage Service (ClusterIP)                  │
│                  ClusterIP: 10.105.75.162:9080                │
│                  Selector: app=productpage                    │
│  ┌────────────────────────────────────────────────────────────┐    │
│  │  Endpoint: productpage-v1-pod (10.244.2.15:9080)      │    │
│  └──────────────────────┬─────────────────────────────────────┘    │
└─────────────────────────┼───────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│                  productpage-v1 Pod (default namespace)       │
│  ┌───────────────────┐    ┌────────────────────────────────────┐   │
│  │  istio-proxy      │    │                                │   │
│  │  (Envoy Sidecar)  │    │                                │   │
│  │                   │    │                                │   │
│  │  Inbound:         │    │                                │   │
│  │  15006 포트       │───▶│  productpage Container         │   │
│  │    ↓              │    │  (Python Flask App)            │   │
│  │  iptables로       │    │  Port: 9080                    │   │
│  │  9080으로 전달    │    │  &quot;HTML 응답 생성&quot;                 │   │
│  │                   │    │                                │   │
│  └───────────────────┘    └────────────────────────────────────┘    │
│         │                                                      │
│         │ 메트릭 수집, 로그 기록, 트레이스 생성                     │
│         ↓                                                      │
│  [Prometheus, Jaeger로 전송]                                    │
└─────────────────────────────────────────────────────────────────────┘</code></pre><h3 id="실제-테스트">실제 테스트</h3>
<pre><code class="language-bash"># 클러스터 내부에서 테스트
curl -s http://172.30.1.43:30192/productpage | head -20</code></pre>
<pre><code class="language-html">&lt;meta charset=&quot;utf-8&quot;&gt;
&lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;

&lt;title&gt;Simple Bookstore App&lt;/title&gt;</code></pre>
<p><strong>성공!</strong> Gateway → VirtualService → productpage 흐름이 정상 작동합니다.</p>
<hr>
<h2 id="7-istio의-강력한-기능들">7. Istio의 강력한 기능들</h2>
<h3 id="1-traffic-management-트래픽-관리">1. Traffic Management (트래픽 관리)</h3>
<h4 id="canary-deployment-카나리-배포">Canary Deployment (카나리 배포)</h4>
<p><strong>기존 방식 vs Istio:</strong></p>
<pre><code>기존 Kubernetes:
- Deployment replicas 조정으로 비율 제어
- 정밀한 비율 제어 어려움 (예: 95:5 불가능)
- 코드 변경 필요

Istio:
- VirtualService로 정확한 비율 제어
- 코드 변경 없이 YAML만 수정
- 헤더 기반 라우팅 가능</code></pre><p><strong>예시: reviews v3로 10% 트래픽 전환</strong></p>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 90         # 90% → v1
    - destination:
        host: reviews
        subset: v3
      weight: 10         # 10% → v3 (신규 버전)</code></pre>
<p><strong>사용 시나리오:</strong></p>
<ol>
<li>새 버전 배포 시 10%만 테스트</li>
<li>에러율 모니터링</li>
<li>문제 없으면 50% → 100% 점진적 증가</li>
</ol>
<h4 id="ab-testing-사용자-기반-라우팅">A/B Testing (사용자 기반 라우팅)</h4>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - match:
    - headers:
        user:
          exact: &quot;jason&quot;    # jason 사용자만
    route:
    - destination:
        host: reviews
        subset: v2          # v2로 라우팅 (별점 검은색)
  - route:
    - destination:
        host: reviews
        subset: v1          # 나머지는 v1 (별점 없음)</code></pre>
<h4 id="circuit-breaker-장애-격리">Circuit Breaker (장애 격리)</h4>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100        # 최대 연결 수
      http:
        http1MaxPendingRequests: 10
        maxRequestsPerConnection: 2
    outlierDetection:              # 비정상 인스턴스 자동 제외
      consecutiveErrors: 5         # 5회 연속 실패 시
      interval: 30s                # 30초마다 체크
      baseEjectionTime: 30s        # 30초간 제외</code></pre>
<p><strong>동작:</strong></p>
<ol>
<li>reviews-v2 Pod가 5회 연속 에러 응답</li>
<li>Envoy가 해당 Pod를 30초간 로드밸런싱에서 제외</li>
<li>정상 Pod로만 트래픽 전달</li>
<li>30초 후 재시도</li>
</ol>
<h3 id="2-security-보안">2. Security (보안)</h3>
<h4 id="mutual-tls-mtls">Mutual TLS (mTLS)</h4>
<p><strong>기존 방식 vs Istio:</strong></p>
<pre><code>기존 방식:
1. 인증서 생성 (openssl)
2. 각 서비스에 인증서 배포
3. 애플리케이션 코드에서 TLS 설정
4. 인증서 만료 전 수동 갱신

Istio:
1. PeerAuthentication 리소스 생성
2. 끝! (자동 활성화)</code></pre><p><strong>Istio mTLS 자동화:</strong></p>
<pre><code class="language-yaml">apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: default
spec:
  mtls:
    mode: STRICT    # 모든 서비스 간 통신 암호화 강제</code></pre>
<p><strong>Citadel (istiod)이 자동 수행:</strong></p>
<ul>
<li>X.509 인증서 자동 발급</li>
<li>90일 만료 시 자동 갱신</li>
<li>Envoy에 자동 주입</li>
<li>암호화/복호화 자동 처리</li>
</ul>
<p><strong>효과:</strong></p>
<pre><code>productpage → reviews 호출 시

기존:
productpage ──HTTP(평문)──▶ reviews
             ↑ 스니핑 가능!

Istio mTLS:
productpage ──TLS(암호화)──▶ reviews
istio-proxy ────────────────▶ istio-proxy
             ↑ 암호화된 통신</code></pre><h4 id="authorization-권한-관리">Authorization (권한 관리)</h4>
<pre><code class="language-yaml">apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-productpage-to-reviews
spec:
  selector:
    matchLabels:
      app: reviews                # reviews 서비스에 대한 접근 제어
  action: ALLOW
  rules:
  - from:
    - source:
        principals: [&quot;cluster.local/ns/default/sa/productpage&quot;]
    to:
    - operation:
        methods: [&quot;GET&quot;]           # GET만 허용</code></pre>
<p><strong>효과:</strong> productpage만 reviews를 호출 가능, 다른 서비스는 차단</p>
<h3 id="3-observability-관측성">3. Observability (관측성)</h3>
<h4 id="자동-메트릭-수집">자동 메트릭 수집</h4>
<p><strong>Envoy가 모든 요청에 대해 자동 수집:</strong></p>
<pre><code>Prometheus 메트릭:
- istio_requests_total{destination_service=&quot;reviews&quot;}
  → reviews 서비스로의 요청 수

- istio_request_duration_milliseconds{destination_service=&quot;reviews&quot;}
  → reviews 응답 시간

- istio_request_bytes_sum
  → 요청 크기</code></pre><p><strong>코드 변경 없이 자동 수집!</strong></p>
<h4 id="distributed-tracing-분산-추적">Distributed Tracing (분산 추적)</h4>
<pre><code>사용자 요청 하나의 전체 경로 추적:

Request ID: abc123
┌────────────────────────────────────────┐
│ productpage (50ms)                     │
│  ├─ details (10ms)                     │
│  ├─ reviews-v2 (30ms)                  │
│  │   └─ ratings (15ms)  ← 병목 발견!  │
│  └─ ...                                │
└────────────────────────────────────────┘</code></pre><p><strong>Jaeger UI에서 시각화:</strong></p>
<ul>
<li>전체 요청 경로</li>
<li>각 구간 소요 시간</li>
<li>에러 발생 지점</li>
</ul>
<h4 id="service-graph-서비스-토폴로지">Service Graph (서비스 토폴로지)</h4>
<p><strong>Kiali 대시보드:</strong></p>
<pre><code>       ┌─────────────┐
       │ productpage │
       └──────┬──────┘
              │
      ┌───────┼───────┬─────────┐
      │       │       │         │
      ▼       ▼       ▼         ▼
   ┌───┐   ┌───┐   ┌───┐     ┌───┐
   │det│   │rev│   │rev│     │rev│
   │ v1│   │ v1│   │ v2│     │ v3│
   └───┘   └───┘   └─┬─┘     └─┬─┘
                     │         │
                     └────┬────┘
                          │
                          ▼
                       ┌─────┐
                       │ rat │
                       │  v1 │
                       └─────┘

- 초록색 선: 정상 트래픽
- 빨간색 선: 에러 발생
- 선 굵기: 트래픽 양</code></pre><h3 id="4-resilience-복원력">4. Resilience (복원력)</h3>
<h4 id="retry--timeout">Retry &amp; Timeout</h4>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
    timeout: 10s              # 10초 타임아웃
    retries:
      attempts: 3             # 3번 재시도
      perTryTimeout: 2s       # 시도당 2초
      retryOn: 5xx            # 5xx 에러 시 재시도</code></pre>
<p><strong>효과:</strong></p>
<ul>
<li>일시적 네트워크 오류 자동 복구</li>
<li>사용자에게 에러 노출 감소</li>
</ul>
<h4 id="fault-injection-장애-주입---테스트용">Fault Injection (장애 주입 - 테스트용)</h4>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - fault:
      delay:
        percentage:
          value: 10           # 10% 요청에
        fixedDelay: 5s        # 5초 지연 주입
    route:
    - destination:
        host: ratings</code></pre>
<p><strong>사용 시나리오:</strong></p>
<ul>
<li>프로덕션 배포 전 장애 상황 테스트</li>
<li>Circuit Breaker 동작 검증</li>
<li>Timeout 설정 적절성 확인</li>
</ul>
<hr>
<h2 id="다음-계획">다음 계획</h2>
<h3 id="day-10-istio-관측-도구-observability">Day 10: Istio 관측 도구 (Observability)</h3>
<p><strong>학습 내용:</strong></p>
<ol>
<li><p><strong>Kiali 설치 및 활용</strong></p>
<ul>
<li>Service Graph 시각화</li>
<li>실시간 트래픽 모니터링</li>
<li>VirtualService 설정 검증</li>
</ul>
</li>
<li><p><strong>Prometheus &amp; Grafana</strong></p>
<ul>
<li>메트릭 수집 확인</li>
<li>Istio 대시보드 분석</li>
<li>커스텀 메트릭 생성</li>
</ul>
</li>
<li><p><strong>Jaeger (분산 추적)</strong></p>
<ul>
<li>Trace 수집 설정</li>
<li>요청 경로 분석</li>
<li>성능 병목 발견</li>
</ul>
</li>
<li><p><strong>실습: 트래픽 시나리오 테스트</strong></p>
<ul>
<li>Canary 배포 (reviews v1 → v3)</li>
<li>A/B 테스트 (사용자별 라우팅)</li>
<li>장애 주입 및 복구 테스트</li>
</ul>
</li>
</ol>
<h3 id="day-11-19-istio-심화-학습">Day 11-19: Istio 심화 학습</h3>
<ul>
<li><strong>Day 11</strong>: Sail Operator (Istio Lifecycle 관리)</li>
<li><strong>Day 12</strong>: Envoy Proxy 심화 (Filter, Listener)</li>
<li><strong>Day 13</strong>: Istio Gateway 고급 (mTLS, SNI)</li>
<li><strong>Day 14</strong>: Traffic Management 실전 (Canary, Blue-Green)</li>
<li><strong>Day 15</strong>: Security 심화 (JWT, RBAC)</li>
<li><strong>Day 16</strong>: Multi-Cluster Mesh</li>
<li><strong>Day 17</strong>: Istio Performance Tuning</li>
<li><strong>Day 18</strong>: Istio Troubleshooting</li>
<li><strong>Day 19</strong>: Production Best Practices</li>
</ul>
<hr>
<h2 id="핵심-요약">핵심 요약</h2>
<h3 id="istio를-사용하는-이유">Istio를 사용하는 이유</h3>
<ol>
<li><p><strong>개발자는 비즈니스 로직에만 집중</strong></p>
<ul>
<li>재시도, 타임아웃 → Istio가 처리</li>
<li>로깅, 메트릭 → 자동 수집</li>
<li>보안 → 자동 암호화</li>
</ul>
</li>
<li><p><strong>언어 독립적</strong></p>
<ul>
<li>Java, Python, Go, Node.js 모두 동일하게 동작</li>
<li>라이브러리 없이 YAML만으로 제어</li>
</ul>
</li>
<li><p><strong>강력한 관측성</strong></p>
<ul>
<li>코드 변경 없이 메트릭 수집</li>
<li>전체 서비스 토폴로지 시각화</li>
<li>분산 추적으로 병목 발견</li>
</ul>
</li>
<li><p><strong>프로덕션급 보안</strong></p>
<ul>
<li>mTLS 자동 활성화</li>
<li>인증서 자동 갱신</li>
<li>세밀한 권한 제어</li>
</ul>
</li>
</ol>
<h3 id="오늘-배운-핵심-개념">오늘 배운 핵심 개념</h3>
<pre><code>1. Service Mesh = 마이크로서비스 간 통신 관리 인프라 레이어

2. Istio 아키텍처:
   - Control Plane (istiod): 설정 관리
   - Data Plane (Envoy): 실제 트래픽 처리

3. Sidecar 패턴:
   - 각 Pod에 Envoy Proxy 자동 주입
   - 애플리케이션 코드 변경 없음

4. Gateway + VirtualService:
   - Gateway: 외부 트래픽 진입점
   - VirtualService: 라우팅 규칙
   - Selector로 Ingress Gateway Pod 선택
   - 이름으로 Gateway와 VirtualService 연결

5. 트래픽 흐름:
   NodePort → Service → Envoy (Gateway)
   → VirtualService 라우팅 → 목적지 서비스
   → Envoy (Sidecar) → App</code></pre><h3 id="실무-적용-포인트">실무 적용 포인트</h3>
<p><strong>언제 Istio를 도입해야 하나?</strong></p>
<p>✅ <strong>도입 권장:</strong></p>
<ul>
<li>마이크로서비스 10개 이상</li>
<li>서비스 간 통신 복잡도 높음</li>
<li>Canary 배포, A/B 테스트 필요</li>
<li>관측성 부재로 장애 대응 어려움</li>
<li>서비스 간 보안 요구사항 있음</li>
</ul>
<p>❌ <strong>도입 불필요:</strong></p>
<ul>
<li>모놀리식 애플리케이션</li>
<li>마이크로서비스 5개 미만</li>
<li>단순한 CRUD API만 존재</li>
<li>운영 리소스 부족 (학습 비용 高)</li>
</ul>
<p><strong>Istio 도입 단계:</strong></p>
<ol>
<li>Pilot 프로젝트 (1-2개 서비스)</li>
<li>Sidecar 주입 검증</li>
<li>Gateway 설정 및 트래픽 테스트</li>
<li>관측 도구 설치 (Kiali, Prometheus)</li>
<li>점진적 확대 (서비스별 순차 적용)</li>
</ol>
<hr>
<p><strong>다음 포스트에서는 Kiali, Prometheus, Grafana를 설치하고 Istio의 강력한 관측성을 직접 체험해보겠습니다!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: Helm + ArgoCD로 GitOps 파이프라인 구축 (Day 8)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Helm-ArgoCD%EB%A1%9C-GitOps-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95-Day-8</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Helm-ArgoCD%EB%A1%9C-GitOps-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95-Day-8</guid>
            <pubDate>Sun, 09 Nov 2025 06:12:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2025년 11월 8일
수동 배포는 이제 그만! Git Push 한 번으로 자동 배포되는 마법</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 7에서 Ceph 분산 스토리지로 진정한 동적 프로비저닝을 경험했습니다. 이제 Day 8에서는 <strong>GitOps 패러다임의 핵심 도구인 Helm과 ArgoCD</strong>를 마스터하여 선언적 배포 자동화를 완성했습니다.</p>
<p><strong>오늘 배운 것:</strong></p>
<ol>
<li>Helm의 철학과 Custom Chart 생성 (Chart.yaml, templates/, values)</li>
<li>환경별 Values 관리 (values-dev.yaml, values-staging.yaml, values-prod.yaml)</li>
<li>ArgoCD 설치 및 아키텍처 이해 (Application Controller, Repo Server)</li>
<li>GitOps 워크플로우 (Git → ArgoCD → Kubernetes 자동 동기화)</li>
<li>Self-Heal 기능 (수동 변경 자동 복구)</li>
<li>Sync Waves와 Hooks (순차적 배포, DB 마이그레이션)</li>
<li>다중 환경 배포 (하나의 차트, 세 가지 환경)</li>
</ol>
<hr>
<h2 id="1-helm이-뭐길래">1. Helm이 뭐길래?</h2>
<h3 id="왜-helm인가">왜 Helm인가?</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<p>Kubernetes YAML을 직접 관리하다 보면 이런 문제가 생깁니다:</p>
<pre><code class="language-yaml">문제점:
❌ YAML 파일 수십 개 (Deployment, Service, ConfigMap, Secret, Ingress...)
❌ 환경별 복사본 (dev, staging, prod 각각 관리)
❌ 변수 관리 어려움 (이미지 태그, 포트, 리소스 등)
❌ 롤백 복잡함 (어떤 YAML을 되돌릴지?)
❌ 재사용 불가 (다른 프로젝트에서 복사-붙여넣기)

Helm의 해결책:
✅ 패키지 단위 관리 (Chart = 모든 리소스를 하나로)
✅ 템플릿화 ({{ .Values.image.tag }} 같은 변수)
✅ 환경별 Values 파일 (values-dev.yaml, values-prod.yaml)
✅ 버전 관리 (helm rollback으로 즉시 복구)
✅ Chart 재사용 (공식 차트 저장소, 자체 차트)</code></pre>
<p><strong>Kubernetes YAML vs Helm:</strong></p>
<pre><code>기존 YAML 방식:
myapp/
├─ deployment-dev.yaml
├─ deployment-staging.yaml
├─ deployment-prod.yaml
├─ service-dev.yaml
├─ service-staging.yaml
├─ service-prod.yaml
├─ configmap-dev.yaml
└─ ... (복사본 지옥!)

→ 환경 추가 시 모든 파일 복사
→ 이미지 태그 변경 시 3개 파일 수정

Helm 방식:
myapp/
├─ Chart.yaml
├─ values.yaml (기본값)
├─ values-dev.yaml
├─ values-staging.yaml
├─ values-prod.yaml
└─ templates/
    ├─ deployment.yaml  (템플릿)
    ├─ service.yaml     (템플릿)
    └─ configmap.yaml   (템플릿)

→ 환경 추가 시 values-*.yaml 하나만 추가
→ 이미지 태그 변경 시 values 파일 한 줄만 수정!</code></pre><h3 id="helm-핵심-개념-정리">Helm 핵심 개념 정리</h3>
<p><strong>Chart (차트):</strong></p>
<ul>
<li>Kubernetes 리소스의 패키지</li>
<li>템플릿 + 기본 설정 + 메타데이터</li>
<li><code>helm create</code>로 생성</li>
</ul>
<p><strong>Values (값):</strong></p>
<pre><code class="language-yaml"># values.yaml (기본값)
replicaCount: 2
image:
  repository: nginx
  tag: &quot;1.25.3&quot;
service:
  port: 80

# values-prod.yaml (프로덕션 오버라이드)
replicaCount: 5  # ← 프로덕션은 5개로!
resources:
  limits:
    memory: &quot;512Mi&quot;</code></pre>
<p><strong>템플릿 (Template):</strong></p>
<pre><code class="language-yaml"># templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
spec:
  replicas: {{ .Values.replicaCount }}  # ← 값 주입!
  template:
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: &quot;{{ .Values.image.repository }}:{{ .Values.image.tag }}&quot;
        ports:
        - containerPort: {{ .Values.service.port }}</code></pre>
<p><strong>Release (릴리스):</strong></p>
<pre><code>Chart를 특정 환경에 설치한 인스턴스

예시:
myapp-dev    (myapp 차트의 dev 릴리스)
myapp-staging (myapp 차트의 staging 릴리스)
myapp-prod   (myapp 차트의 prod 릴리스)</code></pre><hr>
<h2 id="2-custom-helm-chart-생성-실습">2. Custom Helm Chart 생성 실습</h2>
<h3 id="실습-1-기본-차트-생성">실습 1: 기본 차트 생성</h3>
<p><strong>Helm 설치 (이미 설치됨):</strong></p>
<pre><code class="language-bash">$ helm version
version.BuildInfo{Version:&quot;v3.16.3&quot;, GitCommit:&quot;cfd07493f46efc9debd9cc1b02a0961186df7fdf&quot;, GitTreeState:&quot;clean&quot;, GoVersion:&quot;go1.22.7&quot;}</code></pre>
<p><strong>기본 차트 스캐폴딩:</strong></p>
<pre><code class="language-bash"># 차트 뼈대 생성
$ helm create myapp
Creating myapp

# 디렉토리 구조 확인
$ tree myapp
myapp/
├── Chart.yaml           # 차트 메타데이터
├── values.yaml          # 기본 설정값
├── templates/           # Kubernetes 리소스 템플릿
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── _helpers.tpl     # 헬퍼 함수
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   └── NOTES.txt        # 설치 후 출력 메시지
└── charts/              # 의존성 차트</code></pre>
<p><strong>Chart.yaml 이해:</strong></p>
<pre><code class="language-yaml">apiVersion: v2            # Helm 3 = v2
name: myapp               # 차트 이름
description: A Helm chart for myapp
type: application         # application or library
version: 0.1.0            # 차트 버전 (SemVer)
appVersion: &quot;1.25.3&quot;      # 앱 버전 (nginx 1.25.3)</code></pre>
<ul>
<li><strong>version</strong>: 차트 자체의 버전 (YAML 구조 변경 시 증가)</li>
<li><strong>appVersion</strong>: 배포되는 애플리케이션 버전</li>
</ul>
<h3 id="실습-2-환경별-values-파일-작성">실습 2: 환경별 Values 파일 작성</h3>
<p><strong>문제:</strong> 피곤한 작업이지만 <strong>한 번만 하면 계속 재사용 가능!</strong></p>
<pre><code class="language-yaml"># values.yaml (기본값)
replicaCount: 2
image:
  repository: nginx
  tag: &quot;1.25.3&quot;
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 50m
    memory: 64Mi
env: &quot;default&quot;</code></pre>
<p><strong>환경별 오버라이드:</strong></p>
<pre><code class="language-yaml"># values-dev.yaml (개발 환경 - 최소 리소스)
replicaCount: 1
resources:
  limits:
    cpu: 50m
    memory: 64Mi
  requests:
    cpu: 25m
    memory: 32Mi
env: &quot;development&quot;</code></pre>
<pre><code class="language-yaml"># values-staging.yaml (스테이징 - 중간 리소스)
replicaCount: 2
resources:
  limits:
    cpu: 200m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi
env: &quot;staging&quot;</code></pre>
<pre><code class="language-yaml"># values-prod.yaml (프로덕션 - 고가용성)
replicaCount: 5
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi
env: &quot;production&quot;
service:
  type: NodePort  # 외부 접근
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70</code></pre>
<p><strong>🎯 핵심:</strong> 템플릿은 한 번만 작성, 환경별 설정만 변경!</p>
<hr>
<h2 id="3-argocd가-뭐길래">3. ArgoCD가 뭐길래?</h2>
<h3 id="gitops란">GitOps란?</h3>
<p><strong>전통적인 배포:</strong></p>
<pre><code>개발자 로컬 PC
  ↓
kubectl apply -f deployment.yaml
  ↓
Kubernetes 클러스터

문제점:
❌ 누가 언제 무엇을 배포했는지 추적 어려움
❌ 환경마다 다른 상태 (drift)
❌ 롤백 복잡함
❌ 권한 관리 어려움 (개발자마다 kubectl 권한 필요)</code></pre><p><strong>GitOps 방식:</strong></p>
<pre><code>개발자 → Git Push
  ↓
Git Repository (단일 진실 소스, Single Source of Truth)
  ↓
ArgoCD (자동 감지)
  ↓
Kubernetes 클러스터 (자동 동기화)

장점:
✅ Git = 모든 변경 이력 추적
✅ Pull Request 기반 코드 리뷰
✅ 롤백 = Git Revert
✅ 선언적 상태 (Desired State in Git)
✅ 중앙 집중식 배포 (ArgoCD만 kubectl 권한 필요)</code></pre><h3 id="argocd-아키텍처">ArgoCD 아키텍처</h3>
<pre><code>GitHub Repository
  ├─ myapp/ (Helm Chart)
  │   ├─ templates/
  │   └─ values-*.yaml
  └─ argocd-apps/
      ├─ myapp-dev.yaml
      ├─ myapp-staging.yaml
      └─ myapp-prod.yaml
        ↓
        ↓ (Git Poll/Webhook)
        ↓
ArgoCD 컴포넌트
  ├─ Application Controller
  │   - Git 저장소 모니터링
  │   - 실제 상태 vs 원하는 상태 비교
  │   - 동기화 실행
  │
  ├─ Repo Server
  │   - Git Clone
  │   - Helm Template 렌더링
  │   - Kubernetes 매니페스트 생성
  │
  ├─ API Server
  │   - Web UI / CLI 제공
  │   - RBAC 인증/인가
  │
  └─ Redis
      - 캐시 (Cluster State, Git Commit)
        ↓
        ↓ (kubectl apply)
        ↓
Kubernetes 클러스터
  ├─ myapp-dev (Namespace)
  ├─ myapp-staging (Namespace)
  └─ myapp-prod (Namespace)</code></pre><hr>
<h2 id="4-argocd-설치-및-구성-실습">4. ArgoCD 설치 및 구성 실습</h2>
<h3 id="실습-3-argocd-설치">실습 3: ArgoCD 설치</h3>
<p><strong>네임스페이스 생성 및 배포:</strong></p>
<pre><code class="language-bash"># 네임스페이스 생성
$ kubectl create namespace argocd
namespace/argocd created

# ArgoCD 설치 (공식 YAML)
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
serviceaccount/argocd-application-controller created
...
deployment.apps/argocd-server created
deployment.apps/argocd-repo-server created
deployment.apps/argocd-applicationset-controller created

# Pod 상태 확인
$ kubectl get pods -n argocd
NAME                                                READY   STATUS
argocd-application-controller-0                     1/1     Running
argocd-applicationset-controller-xxxxx              1/1     Running
argocd-dex-server-xxxxx                             1/1     Running
argocd-notifications-controller-xxxxx               1/1     Running
argocd-redis-xxxxx                                  1/1     Running
argocd-repo-server-xxxxx                            1/1     Running
argocd-server-xxxxx                                 1/1     Running</code></pre>
<p><strong>ArgoCD가 알아서 Helm Chart 인식!</strong></p>
<ul>
<li>연동 설정 없이도 자동 동작</li>
<li>Git 저장소에 Chart.yaml 있으면 Helm으로 처리</li>
<li><code>helm.valueFiles</code> 파라미터로 환경별 values 선택</li>
</ul>
<h3 id="실습-4-argocd-웹-ui-접근">실습 4: ArgoCD 웹 UI 접근</h3>
<p><strong>NodePort로 외부 노출:</strong></p>
<pre><code class="language-bash"># 서비스 타입 변경
$ kubectl patch svc argocd-server -n argocd -p &#39;{&quot;spec&quot;:{&quot;type&quot;:&quot;NodePort&quot;}}&#39;
service/argocd-server patched

# NodePort 확인
$ kubectl get svc -n argocd argocd-server
NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)
argocd-server   NodePort   10.103.51.239   &lt;none&gt;        80:31080/TCP,443:31443/TCP

# 접속 URL
http://172.30.1.38:31080</code></pre>
<p><strong>초기 비밀번호 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath=&#39;{.data.password}&#39; | base64 -d
hZa4rP9qK7mE3nF2</code></pre>
<p><strong>로그인:</strong></p>
<pre><code>Username: admin
Password: hZa4rP9qK7mE3nF2</code></pre><hr>
<h2 id="5-gitops-워크플로우-구현">5. GitOps 워크플로우 구현</h2>
<h3 id="실습-5-git-저장소-준비">실습 5: Git 저장소 준비</h3>
<p><strong>GitHub 저장소 구조:</strong></p>
<pre><code class="language-bash">ArgoCD-gitops/
├─ myapp/                   # Helm Chart
│   ├─ Chart.yaml
│   ├─ values.yaml
│   ├─ values-dev.yaml
│   ├─ values-staging.yaml
│   ├─ values-prod.yaml
│   └─ templates/
│       ├─ deployment.yaml
│       ├─ service.yaml
│       └─ configmap.yaml
│
└─ sync-waves-demo/         # Sync Waves 실습
    ├─ database.yaml
    ├─ migration-job.yaml
    └─ application.yaml</code></pre>
<p><strong>Git Push:</strong></p>
<pre><code class="language-bash">$ cd /root/argocd-demo
$ git add myapp/
$ git commit -m &quot;Add Helm chart with multi-env values&quot;
$ git push origin main</code></pre>
<h3 id="실습-6-argocd-application-생성-3개-환경">실습 6: ArgoCD Application 생성 (3개 환경)</h3>
<p><strong>ArgoCD Application CRD 이해:</strong></p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-dev
  namespace: argocd
spec:
  project: default

  # Git 소스
  source:
    repoURL: https://github.com/hansungmoon/ArgoCD-gitops.git
    targetRevision: main
    path: myapp  # Helm Chart 경로
    helm:
      valueFiles:
        - values-dev.yaml  # ← 환경별 values 선택!

  # 배포 대상
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp-dev

  # 동기화 정책
  syncPolicy:
    automated:
      prune: true      # Git에서 삭제된 리소스 자동 제거
      selfHeal: true   # 수동 변경 자동 복구
    syncOptions:
      - CreateNamespace=true</code></pre>
<p><strong>helm.valueFiles는 어디 있지?</strong></p>
<ul>
<li>위치: <code>/root/argocd-helm-apps.yaml</code> 파일의 15-17, 44-46, 73-75 라인!</li>
<li>각 환경별 Application에서 다른 values 파일 지정</li>
</ul>
<p><strong>3개 환경 배포:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f /root/argocd-helm-apps.yaml
application.argoproj.io/myapp-dev created
application.argoproj.io/myapp-staging created
application.argoproj.io/myapp-prod created

# Application 상태 확인
$ kubectl get applications -n argocd
NAME             SYNC STATUS   HEALTH STATUS
myapp-dev        Synced        Healthy
myapp-staging    Synced        Healthy
myapp-prod       Synced        Healthy</code></pre>
<p><strong>네임스페이스별 Pod 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n myapp-dev
NAME                     READY   STATUS    RESTARTS   AGE
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m

$ kubectl get pods -n myapp-staging
NAME                     READY   STATUS    RESTARTS   AGE
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m  # ← Replica 2

$ kubectl get pods -n myapp-prod
NAME                     READY   STATUS    RESTARTS   AGE
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m
myapp-xxxxxxxxx-xxxxx    1/1     Running   0          2m  # ← Replica 5</code></pre>
<p><strong>🎉 성공!</strong> 하나의 Helm Chart, 세 가지 환경!</p>
<hr>
<h2 id="6-self-heal-기능-검증">6. Self-Heal 기능 검증</h2>
<h3 id="self-heal이-어떤-기능이지">Self-Heal이 어떤 기능이지?</h3>
<p><strong>Self-Heal:</strong></p>
<ul>
<li>Git에 선언된 상태(Desired State)를 강제</li>
<li>수동으로 변경된 리소스를 <strong>자동으로 원래대로 복구</strong></li>
<li>5초마다 상태 확인 (기본값)</li>
</ul>
<p><strong>작동 원리:</strong></p>
<pre><code>1. Git: replicas: 2
2. ArgoCD: Kubernetes에 Deployment 배포 (replicas=2)
3. 개발자: kubectl scale deployment myapp --replicas=10
4. ArgoCD: &quot;어? Git에는 2인데 실제는 10이네?&quot; (Drift 감지)
5. ArgoCD: kubectl apply (replicas=2로 복구)
   → 약 5초 후 자동 복구!</code></pre><h3 id="실습-7-self-heal-테스트">실습 7: Self-Heal 테스트</h3>
<p><strong>시나리오:</strong> 개발자가 실수로 Replica를 수동 변경</p>
<pre><code class="language-bash"># 현재 상태 (Git: 2, 실제: 2)
$ kubectl get deployment -n myapp-dev
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
myapp   2/2     2            2           5m

# 수동 변경 (kubectl로 직접 수정!)
$ kubectl scale deployment myapp -n myapp-dev --replicas=10
deployment.apps/myapp scaled

# 즉시 확인
$ kubectl get deployment -n myapp-dev
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
myapp   10/10   10           10          5m

# 5초 대기...
$ sleep 5

# Self-Heal 작동! (Git 상태로 복구)
$ kubectl get deployment -n myapp-dev
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
myapp   2/2     2            2           5m  # ← 자동으로 2로 복구!</code></pre>
<p><strong>ArgoCD UI에서 확인:</strong></p>
<pre><code>Application: myapp-dev
Status: OutOfSync → Syncing → Synced
Message: &quot;Deployment replicas reverted to 2 (Git state)&quot;</code></pre><p><strong>🎯 교훈:</strong></p>
<ul>
<li>Git이 <strong>단일 진실 소스</strong> (Single Source of Truth)</li>
<li>수동 변경은 무의미 (Self-Heal이 복구)</li>
<li>변경하려면 Git 수정 → PR → Merge!</li>
</ul>
<hr>
<h2 id="7-sync-waves-순차적-배포-제어">7. Sync Waves: 순차적 배포 제어</h2>
<h3 id="왜-sync-waves가-필요한가">왜 Sync Waves가 필요한가?</h3>
<p><strong>문제 상황:</strong></p>
<pre><code>Application 배포 시:
1. Database Pod 생성 중... (아직 준비 안 됨)
2. Application Pod 시작 → DB 연결 실패! (CrashLoopBackOff)
3. DB 준비 완료
4. Application 계속 재시작...

→ 배포 순서가 랜덤!</code></pre><p><strong>Sync Waves 해결책:</strong></p>
<pre><code class="language-yaml">annotations:
  argocd.argoproj.io/sync-wave: &quot;0&quot;  # ← 낮은 번호부터 배포!

배포 순서:
Wave 0: Database (postgres)
  ↓ (DB 준비 완료 대기)
Wave 1: DB Migration Job (PreSync Hook)
  ↓ (마이그레이션 완료)
Wave 2: Application (webapp)
  ↓ (모든 리소스 Healthy)</code></pre>
<h3 id="실습-8-sync-waves-구현">실습 8: Sync Waves 구현</h3>
<p><strong>Wave 0: Database (먼저 배포)</strong></p>
<pre><code class="language-yaml"># database.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  annotations:
    argocd.argoproj.io/sync-wave: &quot;0&quot;  # ← Wave 0
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    spec:
      containers:
      - name: postgres
        image: postgres:14-alpine
        env:
        - name: POSTGRES_PASSWORD
          value: &quot;password123&quot;
        - name: POSTGRES_DB
          value: &quot;myapp&quot;
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  annotations:
    argocd.argoproj.io/sync-wave: &quot;0&quot;
spec:
  selector:
    app: postgres
  ports:
  - port: 5432</code></pre>
<p><strong>Wave 1: DB Migration (PreSync Hook)</strong></p>
<pre><code class="language-yaml"># migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
  annotations:
    argocd.argoproj.io/sync-wave: &quot;1&quot;  # ← Wave 1
    argocd.argoproj.io/hook: PreSync    # ← 앱 배포 전 실행!
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: migration
        image: postgres:14-alpine
        command:
        - /bin/sh
        - -c
        - |
          echo &quot;=== DB Migration 시작 ===&quot;
          echo &quot;데이터베이스 연결 대기 중...&quot;
          sleep 5

          echo &quot;테이블 생성 중...&quot;
          PGPASSWORD=password123 psql -h postgres -U postgres -d myapp -c &quot;
            CREATE TABLE IF NOT EXISTS users (
              id SERIAL PRIMARY KEY,
              name VARCHAR(100),
              created_at TIMESTAMP DEFAULT NOW()
            );
          &quot;

          echo &quot;샘플 데이터 삽입 중...&quot;
          PGPASSWORD=password123 psql -h postgres -U postgres -d myapp -c &quot;
            INSERT INTO users (name) VALUES (&#39;Alice&#39;), (&#39;Bob&#39;), (&#39;Charlie&#39;)
            ON CONFLICT DO NOTHING;
          &quot;

          echo &quot;=== DB Migration 완료 ===&quot;</code></pre>
<p><strong>Hook 종류:</strong></p>
<ul>
<li><strong>PreSync</strong>: 동기화 전 실행 (DB 마이그레이션, 사전 검증)</li>
<li><strong>Sync</strong>: 일반 리소스 배포 (기본값)</li>
<li><strong>PostSync</strong>: 동기화 후 실행 (테스트, Slack 알림)</li>
<li><strong>SyncFail</strong>: 동기화 실패 시 실행 (롤백, 에러 알림)</li>
</ul>
<p><strong>Wave 2: Application (마지막 배포)</strong></p>
<pre><code class="language-yaml"># application.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  annotations:
    argocd.argoproj.io/sync-wave: &quot;2&quot;  # ← Wave 2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    spec:
      initContainers:
      - name: wait-for-db
        image: postgres:14-alpine
        command:
        - /bin/sh
        - -c
        - |
          echo &quot;데이터베이스 준비 대기 중...&quot;
          until PGPASSWORD=password123 psql -h postgres -U postgres -d myapp -c &#39;\l&#39;; do
            echo &quot;데이터베이스 연결 대기...&quot;
            sleep 2
          done
          echo &quot;데이터베이스 준비 완료!&quot;
      containers:
      - name: webapp
        image: nginx:1.25.3</code></pre>
<p><strong>Git Push 및 ArgoCD Application 생성:</strong></p>
<pre><code class="language-bash"># Git Push
$ cd /root/argocd-demo/sync-waves-demo
$ git add .
$ git commit -m &quot;Add Sync Waves demo with DB migration&quot;
$ git push origin main

# ArgoCD Application 생성
$ kubectl apply -f /root/sync-waves-app.yaml
application.argoproj.io/sync-waves-demo created

# 배포 순서 확인 (실시간 모니터링)
$ kubectl get pods -n sync-waves -w
NAME                        READY   STATUS              RESTARTS   AGE
postgres-xxxxxxxxx-xxxxx    0/1     ContainerCreating   0          3s   # ← Wave 0
postgres-xxxxxxxxx-xxxxx    1/1     Running             0          15s
db-migration-xxxxx          0/1     ContainerCreating   0          5s   # ← Wave 1 (PreSync)
db-migration-xxxxx          1/1     Running             0          8s
db-migration-xxxxx          0/1     Completed           0          25s
webapp-xxxxxxxxx-xxxxx      0/1     Init:0/1            0          3s   # ← Wave 2
webapp-xxxxxxxxx-xxxxx      0/1     PodInitializing     0          12s
webapp-xxxxxxxxx-xxxxx      1/1     Running             0          15s</code></pre>
<p><strong>최종 상태:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n sync-waves
NAME                        READY   STATUS      RESTARTS   AGE
db-migration-dvlft          0/1     Completed   0          2m    # ← Job 완료
postgres-6b9c5b7d9c-4kvxn   1/1     Running     0          2m
webapp-7f8b6c9d8f-7kxqm     1/1     Running     0          1m
webapp-7f8b6c9d8f-9hnpz     1/1     Running     0          1m

$ kubectl get application -n argocd sync-waves-demo
NAME              SYNC STATUS   HEALTH STATUS
sync-waves-demo   Synced        Healthy  # ← 모두 성공!</code></pre>
<p><strong>🎉 순차적 배포 성공!</strong> DB → Migration → App 순서 보장!</p>
<hr>
<h2 id="8-argocd-application-crd-완전-분석">8. ArgoCD Application CRD 완전 분석</h2>
<h3 id="application-spec-주요-필드">Application Spec 주요 필드</h3>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-dev
  namespace: argocd  # ← ArgoCD는 argocd 네임스페이스에서 실행
spec:
  # 1. 프로젝트 (RBAC, 리소스 격리)
  project: default

  # 2. Git 소스
  source:
    repoURL: https://github.com/user/repo.git
    targetRevision: main  # 브랜치, 태그, 커밋 SHA
    path: myapp           # Chart 경로

    # Helm 설정
    helm:
      valueFiles:
        - values-dev.yaml   # ← 여러 개 가능!
        - secrets-dev.yaml
      parameters:           # CLI 오버라이드
        - name: replicaCount
          value: &quot;3&quot;
      releaseName: myapp-dev

  # 3. 배포 대상
  destination:
    server: https://kubernetes.default.svc  # 클러스터 URL
    namespace: myapp-dev

  # 4. 동기화 정책
  syncPolicy:
    automated:
      prune: true       # Git 삭제 → Kubernetes 삭제
      selfHeal: true    # Drift 자동 복구
      allowEmpty: false # 빈 커밋 거부

    syncOptions:
      - CreateNamespace=true   # 네임스페이스 자동 생성
      - PruneLast=true         # 삭제는 마지막에
      - ApplyOutOfSyncOnly=true # OutOfSync만 적용

    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  # 5. Health 체크 (선택)
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas  # HPA 사용 시 replicas 무시</code></pre>
<h3 id="syncpolicy-상세-설명">syncPolicy 상세 설명</h3>
<p><strong>automated.prune:</strong></p>
<pre><code>Git에서 deployment.yaml 삭제
  ↓
ArgoCD: &quot;Deployment가 Git에 없네?&quot;
  ↓
kubectl delete deployment myapp  # ← 자동 삭제!</code></pre><p><strong>automated.selfHeal:</strong></p>
<pre><code>kubectl scale deployment myapp --replicas=10
  ↓
ArgoCD: &quot;Git에는 2인데 실제는 10이네?&quot; (5초마다 체크)
  ↓
kubectl apply (Git 상태로 복구)</code></pre><p><strong>syncOptions:</strong></p>
<pre><code class="language-yaml">- CreateNamespace=true   # 네임스페이스 없으면 생성
- PrunePropagationPolicy=foreground  # 삭제 순서 제어
- Validate=false         # kubectl validation 건너뛰기</code></pre>
<p><strong>retry:</strong></p>
<pre><code>1차 시도 실패
  ↓ 5초 대기
2차 시도 실패
  ↓ 10초 대기 (factor=2)
3차 시도 실패
  ↓ 20초 대기
...
최대 3분까지 재시도</code></pre><hr>
<h2 id="9-최종-gitops-워크플로우">9. 최종 GitOps 워크플로우</h2>
<h3 id="개발-→-배포-전체-흐름">개발 → 배포 전체 흐름</h3>
<pre><code>1. 개발자: 코드 수정
   $ vi src/app.py
   $ docker build -t myapp:v1.2.3 .
   $ docker push ghcr.io/user/myapp:v1.2.3

2. Helm Values 업데이트
   $ vi myapp/values-prod.yaml
   ---
   image:
     tag: &quot;v1.2.3&quot;  # ← 새 버전
   ---

3. Git Push
   $ git add myapp/values-prod.yaml
   $ git commit -m &quot;Update to v1.2.3&quot;
   $ git push origin main

4. Pull Request (선택)
   - Code Review
   - CI 테스트 (GitHub Actions)
   - Approve &amp; Merge

5. ArgoCD 자동 감지
   - 3분마다 Git Poll (또는 Webhook)
   - &quot;main 브랜치 커밋 감지!&quot;

6. ArgoCD Sync
   - Helm Template 렌더링
   - Kubernetes 매니페스트 생성
   - kubectl apply

7. Kubernetes 롤링 업데이트
   - Pod v1.2.2 → v1.2.3 교체
   - Readiness Probe 확인
   - 무중단 배포 완료!

8. ArgoCD 상태 업데이트
   - Sync Status: Synced
   - Health Status: Healthy
   - Slack 알림 (설정 시)</code></pre><h3 id="git-commit-→-자동-배포까지-소요-시간">Git Commit → 자동 배포까지 소요 시간</h3>
<pre><code>Git Push
  ↓
GitHub (즉시)
  ↓
ArgoCD Polling (최대 3분) or Webhook (즉시)
  ↓
Helm Rendering (5초)
  ↓
kubectl apply (10초)
  ↓
Pod 롤링 업데이트 (30초~2분)
  ↓
Healthy 상태 확인 (10초)
  ↓
Total: 약 1~5분 (Webhook 사용 시 더 빠름!)</code></pre><hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="1-helm은-피곤하지만-가치-있다">1. Helm은 피곤하지만 가치 있다</h3>
<p><strong>Custom Chart 생성:</strong></p>
<pre><code class="language-yaml">처음엔 피곤한 작업:
❌ Chart.yaml 작성
❌ templates/ 디렉토리 구조 설계
❌ values.yaml 변수 정의
❌ {{ .Values.xxx }} 템플릿 문법

하지만 한 번 만들면:
✅ 무한 재사용
✅ 환경 추가 = values 파일 하나
✅ 버전 관리 (helm rollback)
✅ 공유 가능 (Helm Repository)</code></pre>
<p><strong>실무 팁:</strong></p>
<ul>
<li>공식 Chart 먼저 검색 (bitnami/nginx, stable/mysql)</li>
<li>없으면 <code>helm create</code>로 시작</li>
<li>복잡한 로직은 <code>_helpers.tpl</code>로 분리</li>
</ul>
<h3 id="2-argocd는-helm을-알아서-인식">2. ArgoCD는 Helm을 알아서 인식</h3>
<p><strong>연동 설정 없이 동작:</strong></p>
<pre><code>Git 저장소에 Chart.yaml 있으면?
  ↓
ArgoCD: &quot;아, Helm Chart구나!&quot;
  ↓
자동으로:
  - helm template 실행
  - values 파일 머지
  - Kubernetes 매니페스트 생성
  - kubectl apply</code></pre><p><strong>helm.valueFiles 위치:</strong></p>
<ul>
<li>Application CRD의 <code>spec.source.helm.valueFiles</code></li>
<li>배열로 여러 파일 지정 가능</li>
<li>우선순위: 나중 파일이 앞 파일 덮어씀</li>
</ul>
<h3 id="3-self-heal은-git-강제-동기화">3. Self-Heal은 Git 강제 동기화</h3>
<p><strong>작동 원리:</strong></p>
<pre><code>매 5초마다:
1. Git에서 최신 매니페스트 가져오기
2. kubectl get으로 실제 상태 확인
3. Diff 계산 (Desired vs Actual)
4. OutOfSync 발견 시 kubectl apply

→ 수동 변경은 무의미!
→ Git만 수정하세요!</code></pre><p><strong>예외 상황:</strong></p>
<ul>
<li>HPA로 replicas 자동 조정 → <code>ignoreDifferences</code> 설정</li>
<li>StatefulSet ordinal → <code>ignoreDifferences</code> 설정</li>
</ul>
<h3 id="4-sync-waves는-순서-보장의-핵심">4. Sync Waves는 순서 보장의 핵심</h3>
<p><strong>왜 필요한가:</strong></p>
<pre><code>일반 배포:
DB, App, Migration Job 동시 생성
→ 랜덤 순서!
→ App이 먼저 뜨면 DB 연결 실패

Sync Waves:
Wave 0: DB
Wave 1: Migration (PreSync Hook)
Wave 2: App
→ 순서 보장!
→ 안정적 배포!</code></pre><p><strong>Hook 활용:</strong></p>
<ul>
<li>PreSync: DB 스키마 마이그레이션</li>
<li>Sync: 일반 리소스</li>
<li>PostSync: 통합 테스트, Slack 알림</li>
<li>SyncFail: 롤백, 에러 로그 수집</li>
</ul>
<h3 id="5-환경별-배포는-values로-해결">5. 환경별 배포는 Values로 해결</h3>
<p><strong>하나의 Chart, 여러 환경:</strong></p>
<pre><code>myapp/ (Helm Chart)
├─ values.yaml       (공통 기본값)
├─ values-dev.yaml   (개발: replica=1, resources 최소)
├─ values-staging.yaml (스테이징: replica=2, 중간)
└─ values-prod.yaml  (프로덕션: replica=5, HPA, NodePort)

Application CRD:
myapp-dev      → valueFiles: [values-dev.yaml]
myapp-staging  → valueFiles: [values-staging.yaml]
myapp-prod     → valueFiles: [values-prod.yaml]

→ 템플릿 중복 없음!
→ 환경별 차이만 values에!</code></pre><hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="1-helm-chart-생성-피곤함">1. Helm Chart 생성 피곤함</h3>
<p><strong>증상:</strong> 템플릿 하나하나 다 작성해야 함</p>
<p><strong>해결:</strong></p>
<ul>
<li><code>helm create myapp</code>로 스캐폴딩 활용</li>
<li>불필요한 템플릿 삭제 (hpa.yaml, ingress.yaml 등)</li>
<li>공식 Chart 참고 (github.com/bitnami/charts)</li>
</ul>
<h3 id="2-values-yaml-찾기-어려움">2. values-*.yaml 찾기 어려움</h3>
<p><strong>증상:</strong> ArgoCD UI에서 어떤 values 쓰는지 안 보임</p>
<p><strong>해결:</strong></p>
<pre><code class="language-bash"># Application YAML 확인
$ kubectl get application myapp-dev -n argocd -o yaml
spec:
  source:
    helm:
      valueFiles:
        - values-dev.yaml  # ← 여기!</code></pre>
<h3 id="3-self-heal이-내-변경을-계속-되돌림">3. Self-Heal이 내 변경을 계속 되돌림</h3>
<p><strong>증상:</strong> kubectl edit으로 수정해도 5초 후 복구됨</p>
<p><strong>교훈:</strong></p>
<ul>
<li><p>Self-Heal이 켜져 있으면 Git만 수정!</p>
</li>
<li><p>긴급 수정 필요 시:</p>
<pre><code class="language-bash"># Self-Heal 임시 비활성화
$ kubectl patch application myapp-dev -n argocd --type=merge \
  -p &#39;{&quot;spec&quot;:{&quot;syncPolicy&quot;:{&quot;automated&quot;:{&quot;selfHeal&quot;:false}}}}&#39;

# 수정 작업
$ kubectl edit deployment myapp -n myapp-dev

# 다시 활성화
$ kubectl patch application myapp-dev -n argocd --type=merge \
  -p &#39;{&quot;spec&quot;:{&quot;syncPolicy&quot;:{&quot;automated&quot;:{&quot;selfHeal&quot;:true}}}}&#39;</code></pre>
</li>
</ul>
<h3 id="4-sync-waves-순서-헷갈림">4. Sync Waves 순서 헷갈림</h3>
<p><strong>증상:</strong> Wave 2가 Wave 1보다 먼저 배포됨</p>
<p><strong>원인:</strong> 숫자가 아닌 문자열 정렬</p>
<pre><code class="language-yaml">❌ sync-wave: 10  # &quot;10&quot;은 &quot;2&quot;보다 앞!
✅ sync-wave: &quot;10&quot;</code></pre>
<p><strong>교훈:</strong></p>
<ul>
<li>Wave 번호는 항상 <strong>따옴표</strong>로 감싸기</li>
<li>음수 가능: <code>-1</code> (Wave 0보다 먼저)</li>
</ul>
<hr>
<h2 id="다음-계획-day-9">다음 계획 (Day 9)</h2>
<p>Day 8에서 Helm + ArgoCD로 GitOps 파이프라인을 완성했습니다. 이제 Day 9부터는 <strong>마이크로서비스 통신을 관리하는 Istio Service Mesh</strong>를 단계별로 학습합니다.</p>
<h3 id="day-9-주제-istio-소개와-첫걸음">Day 9 주제: Istio 소개와 첫걸음</h3>
<ol>
<li><p><strong>마이크로서비스 문제점과 Service Mesh 필요성</strong></p>
<ul>
<li>서비스 간 통신 복잡도 (N×N 연결)</li>
<li>장애 전파 (Cascading Failure)</li>
<li>관찰성 부족 및 보안 이슈</li>
</ul>
</li>
<li><p><strong>Istio 아키텍처 이해</strong></p>
<ul>
<li>Control Plane (Istiod): Pilot, Citadel, Galley</li>
<li>Data Plane (Envoy): Sidecar 패턴</li>
<li>xDS API를 통한 설정 전달</li>
</ul>
</li>
<li><p><strong>실습 환경 구축</strong></p>
<ul>
<li>Kind 클러스터 구성</li>
<li>Istio 설치 (default 프로파일, 프로덕션 설정)</li>
<li>Bookinfo 샘플 애플리케이션 배포</li>
<li>Sidecar 자동 주입 확인</li>
</ul>
</li>
<li><p><strong>관측 도구 설치</strong></p>
<ul>
<li>Kiali: 서비스 토폴로지 시각화</li>
<li>Prometheus &amp; Grafana: 메트릭 수집</li>
<li>Vector + OpenTelemetry + OpenSearch: 로그 수집 준비</li>
</ul>
</li>
</ol>
<h3 id="day-10-이후-학습-로드맵">Day 10 이후 학습 로드맵</h3>
<p><strong>Istio 심화 학습 (단계별):</strong></p>
<ul>
<li>Day 10: Sail Operator (Istio 관리 자동화)</li>
<li>Day 11: Envoy Proxy 심화 (Listener, Route, Cluster)</li>
<li>Day 12: Istio Gateways (Ingress/Egress)</li>
<li>Day 13: Traffic Management (Canary, A/B 테스팅, Blue-Green)</li>
<li>Day 14: Resiliency (Retry, Circuit Breaker, Timeout)</li>
<li>Day 15: Observability (Kiali, Jaeger 분산 추적)</li>
<li>Day 16: Security (mTLS, Authorization Policy)</li>
<li>Day 17: Troubleshooting &amp; Performance Tuning</li>
<li>Day 18: Scaling &amp; VM Support</li>
<li>Day 19: Ambient Mesh (Sidecar-less 아키텍처)</li>
</ul>
<p><strong>실전 도전 과제:</strong></p>
<ul>
<li>ArgoCD + Istio로 Progressive Delivery 구현</li>
<li>Multi-Cluster Service Mesh 구성</li>
<li>Fault Injection을 통한 카오스 엔지니어링</li>
<li>Zero Trust 보안 모델 적용</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>GitOps는 단순한 도구가 아니라 <strong>철학</strong>입니다. Git을 단일 진실 소스로 삼고, 모든 변경을 코드로 관리하는 것!</p>
<p><strong>핵심 요약:</strong></p>
<ul>
<li>✅ Helm = 템플릿 + Values로 환경별 배포</li>
<li>✅ ArgoCD = Git → Kubernetes 자동 동기화</li>
<li>✅ Self-Heal = Git 상태 강제 (5초마다)</li>
<li>✅ Sync Waves = 순차적 배포 (DB → Migration → App)</li>
<li>✅ GitOps = Pull Request 기반 배포 (코드 리뷰 + 이력 추적)</li>
</ul>
<p>Day 9에서 만나요! 🚀</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://helm.sh/docs/">Helm Documentation</a></li>
<li><a href="https://argo-cd.readthedocs.io/">ArgoCD Documentation</a></li>
<li><a href="https://www.gitops.tech/">GitOps Principles</a></li>
<li><a href="https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/">Sync Waves &amp; Hooks</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: Ceph 분산 스토리지로 진정한 동적 프로비저닝 구현 (Day 7)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Ceph-%EB%B6%84%EC%82%B0-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80%EB%A1%9C-%EC%A7%84%EC%A0%95%ED%95%9C-%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9C%EB%B9%84%EC%A0%80%EB%8B%9D-%EA%B5%AC%ED%98%84-Day-7</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Ceph-%EB%B6%84%EC%82%B0-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80%EB%A1%9C-%EC%A7%84%EC%A0%95%ED%95%9C-%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9C%EB%B9%84%EC%A0%80%EB%8B%9D-%EA%B5%AC%ED%98%84-Day-7</guid>
            <pubDate>Thu, 06 Nov 2025 16:18:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2025년 11월 7일
단일 노드 한계를 넘어! RBD/CephFS/RGW 세 가지 스토리지를 모두 구축하다</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 6에서 Prometheus + Grafana로 모니터링, Vector + OpenSearch로 로그 중앙화를 마스터했습니다. 이제 Day 7에서는 <strong>Kubernetes의 꽃이라 할 수 있는 분산 스토리지 Ceph</strong>를 직접 구축하고 세 가지 스토리지 타입을 모두 테스트했습니다.</p>
<p><strong>오늘 배운 것:</strong></p>
<ol>
<li>Ceph 아키텍처 완벽 이해 (RADOS, CRUSH, PG 개념)</li>
<li>Rook Operator로 단일 노드 Ceph 클러스터 구축</li>
<li>RBD 블록 스토리지 (ReadWriteOnce) - 동적 프로비저닝</li>
<li>CephFS 파일 시스템 (ReadWriteMany) - 다중 Pod 동시 접근</li>
<li>RGW 오브젝트 스토리지 (S3 API) - 사용자 및 버킷 생성</li>
<li>Ceph Dashboard 웹 UI로 클러스터 모니터링</li>
<li>단일 노드 환경의 한계와 해결 방법</li>
</ol>
<hr>
<h2 id="1-ceph가-뭐길래">1. Ceph가 뭐길래?</h2>
<h3 id="왜-ceph인가">왜 Ceph인가?</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<p>기존에는 <code>hostPath</code>나 <code>emptyDir</code>로 Pod에 볼륨을 붙였습니다. 하지만 이건 문제가 있었어요:</p>
<pre><code class="language-yaml">문제점:
❌ hostPath: 특정 노드에 종속 (Pod가 다른 노드로 옮겨가면 데이터 유실)
❌ emptyDir: Pod 삭제 시 데이터 사라짐
❌ NFS: 단일 장애점 (NFS 서버 다운 = 전체 다운)

Ceph의 해결책:
✅ 분산 스토리지: 데이터를 여러 노드에 자동 복제
✅ 자가 복구: 노드 장애 시 자동으로 다른 노드에 데이터 재배치
✅ 무제한 확장: 노드 추가 = 스토리지 용량 증가
✅ 3가지 인터페이스: Block (RBD), File (CephFS), Object (S3)</code></pre>
<p><strong>HostPath vs Ceph 비교:</strong></p>
<pre><code>HostPath 방식:
Pod A (cpu1 노드)
  ↓
/data/app (cpu1의 로컬 디스크)

→ Pod가 cpu2로 옮겨가면? 데이터 접근 불가!

Ceph 방식:
Pod A (어느 노드든)
  ↓
Ceph 클러스터 (cpu1 + cpu2 + gpu1)
  ├─ 복제본 1: cpu1
  ├─ 복제본 2: cpu2
  └─ 복제본 3: gpu1

→ Pod가 어디로 옮겨가든 데이터 접근 가능!
→ 노드 1개 다운되어도 괜찮음!</code></pre><h3 id="ceph-핵심-개념-정리">Ceph 핵심 개념 정리</h3>
<p><strong>RADOS (Reliable Autonomic Distributed Object Store):</strong></p>
<ul>
<li>Ceph의 심장부</li>
<li>모든 데이터를 Object로 저장</li>
<li>자가 관리, 자가 복구</li>
</ul>
<p><strong>CRUSH (Controlled Replication Under Scalable Hashing):</strong></p>
<pre><code>마법의 알고리즘!
- 중앙 조회 테이블 없음
- 클라이언트가 직접 계산해서 데이터 위치 찾음
- 노드 추가/제거 시 최소한의 데이터 이동

작동 원리:
Object Name + Cluster Map → Hash 계산
→ Placement Group (PG) 결정
→ CRUSH Rules 적용
→ 최종 OSD 위치 계산</code></pre><p><strong>Placement Groups (PG):</strong></p>
<pre><code>왜 필요한가?

100만 객체를 12 OSD에 직접 매핑하면?
→ 관리 복잡도: O(n×m) = 1,200만 관계

100만 객체 → 512 PG → 12 OSD로 매핑하면?
→ 관리 복잡도: O(n+m) = 100만 + 512 + 12

PG는 객체와 OSD 사이의 중간 레이어!</code></pre><hr>
<h2 id="2-ceph-클러스터-구축-실습">2. Ceph 클러스터 구축 실습</h2>
<h3 id="실습-환경-결정">실습 환경 결정</h3>
<p><strong>원래 계획: 3노드 HA 구성</strong></p>
<pre><code class="language-yaml">cpu1 + cpu2 + gpu1 = 3노드 Ceph 클러스터
각 노드에 sdb 디스크 할당
복제본 3개 (size: 3)</code></pre>
<p><strong>변경된 계획: 단일 노드 학습용</strong></p>
<p>이유:</p>
<ul>
<li>cpu2는 이미 다른 워크로드 사용 중</li>
<li>gpu1에 여유 디스크 sdb (465GB) 발견</li>
<li>학습 목적이므로 단일 노드로 충분</li>
</ul>
<pre><code class="language-bash"># gpu1 노드 디스크 확인
$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 465.8G  0 disk
├─sda1   8:1    0 465.7G  0 part /
└─sda2   8:2    0     1M  0 part
sdb      8:16   0 465.8G  0 disk  ← 이걸 Ceph에 사용!</code></pre>
<p><strong>🎯 단일 노드 Ceph 설정의 핵심:</strong></p>
<pre><code class="language-yaml">failureDomain: osd  # host가 아닌 osd 단위
replicated:
  size: 1            # 복제본 1개
  requireSafeReplicaSize: false  # 안전성 검사 비활성화</code></pre>
<h3 id="실습-1-rook-operator-설치">실습 1: Rook Operator 설치</h3>
<p><strong>Rook이 뭐지?</strong></p>
<ul>
<li>Kubernetes Operator 패턴으로 Ceph 자동 관리</li>
<li>CRD (Custom Resource Definition) 제공</li>
<li>선언적 설정 (YAML로 Ceph 클러스터 생성)</li>
</ul>
<pre><code class="language-bash"># 1. CRD 설치
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/crds.yaml
customresourcedefinition.apiextensions.k8s.io/cephblockpools.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephfilesystems.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectstores.ceph.rook.io created
...

# 2. Common 리소스 (RBAC, ServiceAccount 등)
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/common.yaml
namespace/rook-ceph created
serviceaccount/rook-ceph-system created
...

# 3. Operator 배포
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/operator.yaml
deployment.apps/rook-ceph-operator created

# 4. Operator Pod 확인
$ kubectl get pods -n rook-ceph
NAME                                 READY   STATUS    RESTARTS   AGE
rook-ceph-operator-5b7c8d8d4-xxxxx   1/1     Running   0          2m</code></pre>
<p><strong>🎉 성공!</strong> Rook Operator가 실행되고 있습니다!</p>
<hr>
<h2 id="25-rook-crd와-ceph-컴포넌트-완전-분석">2.5 Rook CRD와 Ceph 컴포넌트 완전 분석</h2>
<p>Rook Operator를 설치했으니, 이제 <strong>어떤 CRD가 제공되고</strong> <strong>각 Pod가 무슨 역할을 하는지</strong> 정확히 이해해야 합니다!</p>
<h3 id="rook이-제공하는-주요-crd-custom-resource-definition">Rook이 제공하는 주요 CRD (Custom Resource Definition)</h3>
<pre><code class="language-bash">$ kubectl get crd | grep ceph.rook.io</code></pre>
<p><strong>핵심 CRD 목록:</strong></p>
<table>
<thead>
<tr>
<th>CRD 이름</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>CephCluster</strong></td>
<td>Ceph 클러스터 전체 정의</td>
<td>MON, MGR, OSD 개수, 버전 설정</td>
</tr>
<tr>
<td><strong>CephBlockPool</strong></td>
<td>RBD용 Pool 생성</td>
<td>replicapool (size=1, failureDomain=osd)</td>
</tr>
<tr>
<td><strong>CephFilesystem</strong></td>
<td>CephFS 파일시스템</td>
<td>myfs (MDS 서버 포함)</td>
</tr>
<tr>
<td><strong>CephObjectStore</strong></td>
<td>RGW Object Storage</td>
<td>my-store (S3 API 엔드포인트)</td>
</tr>
<tr>
<td><strong>CephObjectStoreUser</strong></td>
<td>S3 사용자 생성</td>
<td>my-s3-user (AccessKey/SecretKey)</td>
</tr>
<tr>
<td><strong>CephClient</strong></td>
<td>외부 클라이언트 인증</td>
<td>다른 클러스터에서 접근</td>
</tr>
<tr>
<td><strong>CephRBDMirror</strong></td>
<td>RBD 미러링 (재해복구)</td>
<td>다른 클러스터로 복제</td>
</tr>
<tr>
<td><strong>CephNFS</strong></td>
<td>NFS Gateway</td>
<td>NFS 프로토콜로 접근</td>
</tr>
</tbody></table>
<p><strong>🤔 왜 CRD가 필요한가?</strong></p>
<p>전통적인 Ceph 구축:</p>
<pre><code class="language-bash"># 수동 명령어 실행 (어렵고 복잡)
ceph-deploy mon create node1 node2 node3
ceph-deploy osd prepare node1:/dev/sdb
ceph osd pool create mypool 128
ceph fs new myfs myfs-metadata myfs-data</code></pre>
<p>Rook + CRD 방식:</p>
<pre><code class="language-yaml"># 선언적 YAML (쉽고 재현 가능!)
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
spec:
  mon:
    count: 3
  storage:
    nodes:
    - name: &quot;gpu1&quot;
      devices:
      - name: &quot;sdb&quot;</code></pre>
<p>→ <strong>kubectl apply</strong> 한 번으로 끝!
→ Operator가 알아서 MON, MGR, OSD 배포
→ Git으로 버전 관리 가능 (GitOps)</p>
<h3 id="ceph-클러스터-배포-후-실제-pod-분석">Ceph 클러스터 배포 후 실제 Pod 분석</h3>
<pre><code class="language-bash">$ kubectl get pods -n rook-ceph
NAME                                             READY   STATUS
rook-ceph-operator-xxxxx                          1/1     Running   # Operator
rook-ceph-mon-a-xxxxx                             1/1     Running   # Monitor
rook-ceph-mgr-a-xxxxx                             1/1     Running   # Manager
rook-ceph-osd-0-xxxxx                             1/1     Running   # OSD
rook-ceph-mds-myfs-a-xxxxx                        1/1     Running   # MDS (CephFS용)
rook-ceph-mds-myfs-b-xxxxx                        1/1     Running   # MDS Standby
rook-ceph-rgw-my-store-a-xxxxx                    1/1     Running   # RGW (S3용)
rook-ceph-crashcollector-gpu1-xxxxx               1/1     Running   # Crash 수집
rook-ceph-exporter-gpu1-xxxxx                     1/1     Running   # Prometheus 메트릭
csi-rbdplugin-xxxxx                               2/2     Running   # RBD CSI Driver
csi-rbdplugin-provisioner-xxxxx                   5/5     Running   # RBD Provisioner
csi-cephfsplugin-xxxxx                            2/2     Running   # CephFS CSI Driver
csi-cephfsplugin-provisioner-xxxxx                5/5     Running   # CephFS Provisioner
rook-ceph-tools-xxxxx                             1/1     Running   # CLI 도구</code></pre>
<h3 id="각-pod의-역할-상세-설명">각 Pod의 역할 상세 설명</h3>
<h4 id="1️⃣-rook-ceph-operator-operator-패턴의-두뇌">1️⃣ <strong>rook-ceph-operator</strong> (Operator 패턴의 두뇌)</h4>
<pre><code>역할:
- CephCluster CRD를 감시
- YAML 변경 시 실제 Ceph 리소스 생성/업데이트
- Pod 장애 시 자동 복구
- Kubernetes API와 Ceph 명령어를 연결하는 브릿지

예시:
CephBlockPool YAML 적용 → Operator가 감지
→ ceph osd pool create 명령 실행
→ Pool 생성 완료</code></pre><h4 id="2️⃣-rook-ceph-mon-a-monitor---클러스터-상태-추적">2️⃣ <strong>rook-ceph-mon-a</strong> (Monitor - 클러스터 상태 추적)</h4>
<pre><code>역할:
- Cluster Map 유지 (MON, OSD, PG 상태)
- Quorum 형성 (과반수 합의)
- 클라이언트에게 Cluster Map 제공

왜 홀수 개?
- 2개: 1개 다운 시 Quorum 불가 (50% &lt; 과반수)
- 3개: 1개 다운 OK (2/3 = 66% &gt; 과반수)
- 1개: 단일 노드 학습용 (HA 불가)

통신 포트:
- 6789: Ceph v1 protocol
- 3300: Ceph v2 protocol</code></pre><h4 id="3️⃣-rook-ceph-mgr-a-manager---관리와-모니터링">3️⃣ <strong>rook-ceph-mgr-a</strong> (Manager - 관리와 모니터링)</h4>
<pre><code>역할:
- Ceph Dashboard 웹 UI 제공
- Prometheus 메트릭 노출
- PG Autoscaler 실행
- REST API 제공

모듈:
- dashboard: 웹 UI (port 7000)
- prometheus: 메트릭 수집 (port 9283)
- pg_autoscaler: PG 수 자동 조정
- balancer: OSD 간 데이터 균형

Active-Standby:
- mgr-a: Active (요청 처리)
- mgr-b: Standby (장애 대기)</code></pre><h4 id="4️⃣-rook-ceph-osd-0-object-storage-daemon---실제-데이터-저장">4️⃣ <strong>rook-ceph-osd-0</strong> (Object Storage Daemon - 실제 데이터 저장)</h4>
<pre><code>역할:
- 실제 데이터 저장 (sdb 디스크 사용)
- 데이터 복제 (다른 OSD로)
- Heartbeat (다른 OSD와 상태 확인)
- Backfilling (데이터 재배치)

OSD 번호:
- OSD.0: gpu1의 sdb 디스크
- (추가 노드 시 OSD.1, OSD.2, ...)

데이터 경로:
/var/lib/rook/osd0/
  ├─ block → /dev/sdb (심볼릭 링크)
  ├─ block.db (메타데이터, SSD 권장)
  └─ block.wal (WAL, SSD 권장)

포트:
- 6800-7300: OSD 간 통신</code></pre><h4 id="5️⃣-rook-ceph-mds-myfs-ab-metadata-server---cephfs-전용">5️⃣ <strong>rook-ceph-mds-myfs-a/b</strong> (Metadata Server - CephFS 전용)</h4>
<pre><code>역할:
- CephFS 파일 메타데이터 관리
- 디렉토리 구조, 권한, 소유자 정보
- inode 캐싱 (성능 향상)

왜 2개?
- mds-a: Active (실제 요청 처리)
- mds-b: Standby (장애 대기)
- Active 장애 시 Standby가 즉시 승격 (30초 이내)

메타데이터 vs 데이터:
메타데이터 (MDS가 관리):
  - 파일명: /data/myfile.txt
  - 크기: 1GB
  - 권한: 0644

실제 데이터 (OSD에 저장):
  - Object 1: myfile.txt.00000000 (4MB)
  - Object 2: myfile.txt.00000001 (4MB)
  - ...</code></pre><h4 id="6️⃣-rook-ceph-rgw-my-store-a-rados-gateway---s3-api">6️⃣ <strong>rook-ceph-rgw-my-store-a</strong> (RADOS Gateway - S3 API)</h4>
<pre><code>역할:
- S3 호환 REST API 제공
- 버킷 및 오브젝트 관리
- 사용자 인증 (AccessKey/SecretKey)
- Multi-part Upload 지원

엔드포인트:
- http://rook-ceph-rgw-my-store.rook-ceph:80
- S3 API: PUT /bucket/object

데이터 흐름:
1. AWS CLI: aws s3 cp file.txt s3://mybucket/
2. RGW: S3 요청 → Ceph Object 변환
3. RADOS: Object를 PG로 매핑 → OSD 저장</code></pre><h4 id="7️⃣-csi-rbdplugin--csi-cephfsplugin-csi-driver">7️⃣ <strong>csi-rbdplugin / csi-cephfsplugin</strong> (CSI Driver)</h4>
<pre><code>역할:
- Kubernetes CSI 인터페이스 구현
- PVC 생성 시 실제 볼륨 Attach/Mount
- 각 노드마다 DaemonSet으로 배포

RBD CSI Driver:
- RBD 이미지 생성: rbd create
- 노드에 Map: rbd map /dev/rbd0
- 파일시스템 생성: mkfs.ext4
- Pod에 Mount: mount /dev/rbd0 /var/lib/kubelet/pods/...

CephFS CSI Driver:
- CephFS 서브볼륨 생성
- Ceph Client 마운트
- FUSE 또는 Kernel Driver 사용</code></pre><h4 id="8️⃣-csi-rbdplugin-provisioner--csi-cephfsplugin-provisioner">8️⃣ <strong>csi-rbdplugin-provisioner / csi-cephfsplugin-provisioner</strong></h4>
<pre><code>역할:
- PVC 생성 요청 감시
- 동적 프로비저닝 실행
- PV 생성 및 PVC 바인딩

워크플로우:
1. User: kubectl apply -f pvc.yaml
2. Provisioner: PVC 감지 (StorageClass 확인)
3. Provisioner: Ceph에 볼륨 생성 (RBD 또는 CephFS)
4. Provisioner: PV 오브젝트 생성
5. Kubernetes: PVC ↔ PV 바인딩 (Bound 상태)

왜 5개 컨테이너?
- csi-provisioner: 메인 프로비저너
- csi-resizer: 볼륨 크기 조정
- csi-snapshotter: 스냅샷 생성
- csi-attacher: 볼륨 Attach
- liveness: Health Check</code></pre><h4 id="9️⃣-rook-ceph-crashcollector-crash-정보-수집">9️⃣ <strong>rook-ceph-crashcollector</strong> (Crash 정보 수집)</h4>
<pre><code>역할:
- Ceph 데몬 Crash 시 덤프 수집
- /var/lib/rook/crash/ 디렉토리 모니터링
- Crash 정보를 MON에 전송
- 문제 디버깅에 사용

확인:
$ ceph crash ls
$ ceph crash info &lt;crash-id&gt;</code></pre><h4 id="🔟-rook-ceph-exporter-prometheus-메트릭">🔟 <strong>rook-ceph-exporter</strong> (Prometheus 메트릭)</h4>
<pre><code>역할:
- Ceph 메트릭을 Prometheus 포맷으로 노출
- OSD 상태, Pool 사용량, PG 상태 등
- Day 6 Prometheus와 연동

메트릭 예시:
- ceph_health_status (0=OK, 1=WARN, 2=ERR)
- ceph_osd_up (OSD UP 개수)
- ceph_pool_stored_bytes (Pool 사용량)

ServiceMonitor 연동:
monitoring:
  enabled: true
→ MGR이 ServiceMonitor 생성
→ Prometheus가 자동 수집</code></pre><h4 id="1️⃣1️⃣-rook-ceph-tools-cli-도구-pod">1️⃣1️⃣ <strong>rook-ceph-tools</strong> (CLI 도구 Pod)</h4>
<pre><code>역할:
- ceph 명령어 실행 환경
- 디버깅 및 관리 작업

사용법:
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph osd tree
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- rados df

포함된 도구:
- ceph: 메인 CLI
- rbd: RBD 관리
- rados: Object 관리
- ceph-volume: OSD 관리</code></pre><h3 id="ceph-데이터-흐름-완전-정리">Ceph 데이터 흐름 완전 정리</h3>
<p><strong>예시: MySQL Pod가 10GB RBD PVC 요청</strong></p>
<pre><code>1. kubectl apply -f mysql-pvc.yaml
   ↓
2. csi-rbdplugin-provisioner가 감지
   ↓
3. Provisioner → Ceph: rbd create replicapool/pvc-xxxxx --size 10240
   ↓
4. CRUSH 알고리즘: PG.1 계산
   ↓
5. PG.1 → OSD.0 매핑 (단일 노드이므로 OSD.0만)
   ↓
6. OSD.0: /dev/sdb에 데이터 저장
   ↓
7. Provisioner: PV 오브젝트 생성
   ↓
8. Kubernetes: PVC ↔ PV 바인딩 (Bound)
   ↓
9. MySQL Pod 스케줄링 → cpu1 노드
   ↓
10. csi-rbdplugin (cpu1): rbd map → /dev/rbd0
   ↓
11. csi-rbdplugin: mount /dev/rbd0 → /var/lib/kubelet/pods/.../volumes/
   ↓
12. MySQL 컨테이너: /var/lib/mysql 마운트 완료!</code></pre><p><strong>Multi-node 환경이라면?</strong></p>
<pre><code>PG.1 → [OSD.0, OSD.1, OSD.2] (3중 복제)
- Primary OSD.0 (cpu1): 쓰기 처리
- Replica OSD.1 (cpu2): 복제본 저장
- Replica OSD.2 (gpu1): 복제본 저장

→ 노드 1개 다운되어도 데이터 안전!</code></pre><hr>
<h3 id="실습-2-디스크-초기화">실습 2: 디스크 초기화</h3>
<p><strong>중요!</strong> OSD로 사용할 디스크는 완전히 깨끗해야 합니다!</p>
<pre><code class="language-bash"># sdb 디스크 완전 초기화
$ wipefs -a /dev/sdb
$ sgdisk --zap-all /dev/sdb
$ dd if=/dev/zero of=/dev/sdb bs=1M count=100

# 확인 (FSTYPE이 비어있어야 함)
$ lsblk -f | grep sdb
sdb</code></pre>
<h3 id="실습-3-ceph-클러스터-생성">실습 3: Ceph 클러스터 생성</h3>
<p><strong>단일 노드용 cluster YAML 작성:</strong></p>
<pre><code class="language-yaml"># ~/ceph/ceph-cluster.yaml
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    image: quay.io/ceph/ceph:v18.2.0
  dataDirHostPath: /var/lib/rook

  mon:
    count: 1  # 단일 노드이므로 MON 1개

  mgr:
    count: 1
    modules:
      - name: pg_autoscaler
        enabled: true

  dashboard:
    enabled: true
    ssl: false

  monitoring:
    enabled: true  # Prometheus 연동!

  storage:
    useAllNodes: false
    useAllDevices: false
    nodes:
    - name: &quot;gpu1&quot;
      devices:
      - name: &quot;sdb&quot;  # 465.8GB 디스크

  disruptionManagement:
    managePodBudgets: false  # 단일 노드에서는 비활성화</code></pre>
<p><strong>적용:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/ceph-cluster.yaml
cephcluster.ceph.rook.io/rook-ceph created

# Pod 생성 확인 (30초~1분 소요)
$ kubectl get pods -n rook-ceph
NAME                                      READY   STATUS    RESTARTS   AGE
rook-ceph-mon-a-xxxxx                     1/1     Running   0          45s
rook-ceph-mgr-a-xxxxx                     1/1     Running   0          30s
rook-ceph-osd-prepare-gpu1-xxxxx          0/1     Completed 0          15s
rook-ceph-osd-0-xxxxx                     1/1     Running   0          10s</code></pre>
<p><strong>🎉 MON, MGR, OSD 모두 Running!</strong></p>
<h3 id="트러블슈팅-1-servicemonitor-권한-에러">트러블슈팅 1: ServiceMonitor 권한 에러</h3>
<p>Ceph 클러스터가 올라오긴 했지만, MGR Pod 로그를 보니 에러가 보였습니다!</p>
<p><strong>문제 발견:</strong></p>
<pre><code class="language-bash">$ kubectl logs -n rook-ceph rook-ceph-mgr-a-xxxxx
Error: servicemonitors.monitoring.coreos.com &quot;rook-ceph-mgr&quot; is forbidden:
User &quot;system:serviceaccount:rook-ceph:rook-ceph-system&quot; cannot get resource &quot;servicemonitors&quot;
in API group &quot;monitoring.coreos.com&quot; at the cluster scope</code></pre>
<h4 id="🤔-왜-rbac-권한이-필요한가">🤔 왜 RBAC 권한이 필요한가?</h4>
<p><strong>상황 이해:</strong></p>
<ol>
<li><p><strong>Ceph MGR의 역할</strong>:</p>
<ul>
<li>Prometheus 메트릭 노출 (Day 6에서 설치한 Prometheus와 통합)</li>
<li><code>monitoring.enabled: true</code> 설정 시 ServiceMonitor CRD 자동 생성</li>
</ul>
</li>
<li><p><strong>ServiceMonitor란?</strong>:</p>
<pre><code class="language-yaml"># Prometheus Operator의 CRD
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: rook-ceph-mgr
spec:
  endpoints:
  - port: http-metrics  # Ceph MGR 메트릭 수집</code></pre>
<ul>
<li>Prometheus에게 &quot;이 Service를 스크랩해라&quot; 알림</li>
<li>Prometheus Operator가 감시하다가 자동으로 설정 업데이트</li>
</ul>
</li>
<li><p><strong>권한 문제</strong>:</p>
<pre><code>Ceph MGR Pod (rook-ceph-system ServiceAccount)
  ↓
&quot;ServiceMonitor 생성하려고 함&quot;
  ↓
Kubernetes API Server: &quot;권한 없음! Forbidden!&quot;</code></pre></li>
</ol>
<h4 id="rbac의-3가지-구성요소">RBAC의 3가지 구성요소</h4>
<p><strong>1. ServiceAccount (신원 증명)</strong>:</p>
<pre><code class="language-bash">$ kubectl get sa -n rook-ceph rook-ceph-system
NAME               SECRETS   AGE
rook-ceph-system   0         10m</code></pre>
<ul>
<li>MGR Pod가 사용하는 계정</li>
<li>Pod spec에 <code>serviceAccountName: rook-ceph-system</code> 설정됨</li>
<li>이 계정으로 Kubernetes API 호출</li>
</ul>
<p><strong>2. ClusterRole (권한 정의)</strong>:</p>
<pre><code class="language-yaml">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: rook-ceph-servicemonitor-access
rules:
- apiGroups: [&quot;monitoring.coreos.com&quot;]   # Prometheus Operator API
  resources: [&quot;servicemonitors&quot;]         # ServiceMonitor CRD
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]  # CRUD 권한</code></pre>
<ul>
<li><strong>apiGroups</strong>: CRD의 API 그룹 (Prometheus Operator 설치 시 생성)</li>
<li><strong>resources</strong>: 어떤 리소스에 접근?</li>
<li><strong>verbs</strong>: 무슨 작업? (GET, POST, PUT, DELETE)</li>
</ul>
<p><strong>3. ClusterRoleBinding (계정과 권한 연결)</strong>:</p>
<pre><code class="language-yaml">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rook-ceph-servicemonitor-access
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: rook-ceph-servicemonitor-access  # 위에서 정의한 Role
subjects:
- kind: ServiceAccount
  name: rook-ceph-system                # 이 계정에게
  namespace: rook-ceph                   # rook-ceph 네임스페이스의</code></pre>
<h4 id="왜-기본-rbac에-없었나">왜 기본 RBAC에 없었나?</h4>
<p><strong>이유:</strong></p>
<ul>
<li>Rook Operator는 <strong>Prometheus Operator 설치 여부를 모름</strong></li>
<li>Prometheus Operator의 CRD (ServiceMonitor)는 <strong>선택적 의존성</strong></li>
<li>모든 사용자가 Prometheus를 쓰는 건 아니므로, 기본 RBAC에 포함 안 함</li>
</ul>
<p><strong>Rook이 제공하는 기본 RBAC:</strong></p>
<pre><code class="language-yaml"># common.yaml에 포함된 기본 권한
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;, &quot;services&quot;, &quot;configmaps&quot;, &quot;secrets&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]
- apiGroups: [&quot;apps&quot;]
  resources: [&quot;deployments&quot;, &quot;daemonsets&quot;, &quot;statefulsets&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]
# ... (ServiceMonitor는 없음!)</code></pre>
<h4 id="해결-방법">해결 방법</h4>
<p><strong>RBAC 생성:</strong></p>
<pre><code class="language-yaml"># ~/ceph/rook-servicemonitor-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: rook-ceph-servicemonitor-access
rules:
- apiGroups: [&quot;monitoring.coreos.com&quot;]
  resources: [&quot;servicemonitors&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rook-ceph-servicemonitor-access
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: rook-ceph-servicemonitor-access
subjects:
- kind: ServiceAccount
  name: rook-ceph-system
  namespace: rook-ceph</code></pre>
<p><strong>적용:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/rook-servicemonitor-rbac.yaml
clusterrole.rbac.authorization.k8s.io/rook-ceph-servicemonitor-access created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-servicemonitor-access created</code></pre>
<p><strong>결과 확인:</strong></p>
<pre><code class="language-bash"># MGR이 ServiceMonitor 생성 성공!
$ kubectl get servicemonitor -n rook-ceph
NAME              AGE
rook-ceph-mgr     2m

# Prometheus가 자동 수집 시작
$ kubectl get servicemonitor -n rook-ceph rook-ceph-mgr -o yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: rook-ceph-mgr
  namespace: rook-ceph
spec:
  endpoints:
  - port: http-metrics  # MGR의 9283 포트
    interval: 30s
  namespaceSelector:
    matchNames:
    - rook-ceph
  selector:
    matchLabels:
      app: rook-ceph-mgr</code></pre>
<p><strong>Prometheus 확인:</strong></p>
<pre><code class="language-bash"># Prometheus UI (http://&lt;NODE-IP&gt;:30090) → Targets
# rook-ceph/rook-ceph-mgr/0 (http://10.x.x.x:9283/metrics) UP</code></pre>
<p><strong>✅ 해결!</strong> MGR이 정상적으로 ServiceMonitor를 생성하고, Prometheus가 Ceph 메트릭을 수집하기 시작했습니다!</p>
<h4 id="배운-점">배운 점</h4>
<p><strong>RBAC의 중요성:</strong></p>
<ul>
<li>Kubernetes는 기본적으로 <strong>최소 권한 원칙</strong> (Least Privilege)</li>
<li>Pod가 API 호출 시 ServiceAccount의 권한 확인</li>
<li>CRD는 <strong>동적으로 추가된 리소스</strong>이므로 명시적 RBAC 필요</li>
</ul>
<p><strong>실무 팁:</strong></p>
<pre><code class="language-bash"># 권한 에러 디버깅
$ kubectl logs -n &lt;namespace&gt; &lt;pod-name&gt; | grep -i &quot;forbidden\|unauthorized&quot;

# ServiceAccount 권한 확인
$ kubectl auth can-i create servicemonitors \
  --as=system:serviceaccount:rook-ceph:rook-ceph-system \
  -n rook-ceph
no

# RBAC 적용 후
$ kubectl auth can-i create servicemonitors \
  --as=system:serviceaccount:rook-ceph:rook-ceph-system \
  -n rook-ceph
yes  # ✅</code></pre>
<h3 id="실습-4-ceph-toolbox로-상태-확인">실습 4: Ceph Toolbox로 상태 확인</h3>
<pre><code class="language-bash"># Toolbox 배포
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/toolbox.yaml
deployment.apps/rook-ceph-tools created

# Toolbox에 들어가서 Ceph 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
  cluster:
    id:     6fa3b356-4964-460e-9a29-e8e350febeff
    health: HEALTH_WARN
            OSD count 1 &lt; osd_pool_default_size 3

  services:
    mon: 1 daemons, quorum a (age 5m)
    mgr: a(active, since 4m)
    osd: 1 osds: 1 up (since 3m), 1 in (since 3m)

  data:
    pools:   0 pools, 0 pgs
    objects: 0 objects, 0 B
    usage:   27 MiB used, 466 GiB / 466 GiB avail</code></pre>
<p><strong>HEALTH_WARN는 정상입니다!</strong></p>
<ul>
<li>단일 노드 환경이라 복제본을 못 만들어서 경고</li>
<li>프로덕션에서는 HEALTH_OK가 나와야 함</li>
</ul>
<hr>
<h2 id="3-rbd-블록-스토리지-readwriteonce">3. RBD 블록 스토리지 (ReadWriteOnce)</h2>
<h3 id="왜-rbd가-필요한가">왜 RBD가 필요한가?</h3>
<p><strong>RBD (RADOS Block Device):</strong></p>
<ul>
<li>가상 블록 디바이스 제공</li>
<li>데이터베이스, VM 디스크 같은 단일 Pod 전용 스토리지</li>
<li><strong>ReadWriteOnce (RWO)</strong>: 한 번에 하나의 Pod만 접근</li>
</ul>
<h3 id="실습-5-cephblockpool-생성">실습 5: CephBlockPool 생성</h3>
<p><strong>트러블슈팅 2: PG undersized+peered 문제</strong></p>
<p><strong>첫 시도 (실패):</strong></p>
<pre><code class="language-bash"># GitHub의 기본 storageclass 적용
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/csi/rbd/storageclass.yaml

# PVC 생성
$ kubectl apply -f test-pvc.yaml

# PVC 상태 확인
$ kubectl get pvc
NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS      AGE
test-pvc    Pending                                      rook-ceph-block   2m</code></pre>
<p><strong>문제 확인:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
  data:
    pools:   1 pools, 32 pgs
    pgs:     32 undersized+peered  ← 문제!
             100.000% pgs not active</code></pre>
<p><strong>원인:</strong> 기본 BlockPool 설정이 다중 노드용</p>
<pre><code class="language-yaml">failureDomain: host  # ❌ 여러 호스트에 분산
replicated:
  size: 3  # ❌ 복제본 3개 필요</code></pre>
<p><strong>해결:</strong> 단일 노드용 BlockPool 생성</p>
<pre><code class="language-yaml"># ~/ceph/single-node-blockpool.yaml
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: osd  # ✅ OSD 단위로 변경
  replicated:
    size: 1  # ✅ 복제본 1개
    requireSafeReplicaSize: false  # ✅ 안전성 검사 비활성화</code></pre>
<pre><code class="language-bash"># 기존 pool 삭제 후 재생성
$ kubectl delete cephblockpool -n rook-ceph replicapool
$ kubectl apply -f ~/ceph/single-node-blockpool.yaml

# PG 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
  data:
    pools:   1 pools, 32 pgs
    pgs:     32 active+clean  ← ✅ 성공!</code></pre>
<h3 id="실습-6-pvc-동적-프로비저닝-테스트">실습 6: PVC 동적 프로비저닝 테스트</h3>
<pre><code class="language-yaml"># ~/ceph/test-ceph-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-ceph-pvc
spec:
  accessModes:
  - ReadWriteOnce  # RWO
  storageClassName: rook-ceph-block
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: test-ceph-pod
spec:
  containers:
  - name: test
    image: busybox:1.28
    command: [&quot;sh&quot;, &quot;-c&quot;, &quot;echo &#39;Hello Ceph!&#39; &gt; /data/test.txt &amp;&amp; cat /data/test.txt &amp;&amp; sleep 3600&quot;]
    volumeMounts:
    - name: ceph-volume
      mountPath: /data
  volumes:
  - name: ceph-volume
    persistentVolumeClaim:
      claimName: test-ceph-pvc</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/test-ceph-pvc.yaml
persistentvolumeclaim/test-ceph-pvc created
pod/test-ceph-pod created

# PVC 상태 확인
$ kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES
test-ceph-pvc   Bound    pvc-4d7204b9-98c0-416b-9574-25af51682854   1Gi        RWO

# Pod 상태 확인
$ kubectl get pod test-ceph-pod
NAME            READY   STATUS    RESTARTS   AGE
test-ceph-pod   1/1     Running   0          20s

# 로그 확인
$ kubectl logs test-ceph-pod
Hello Ceph!</code></pre>
<p><strong>🎉 RBD 블록 스토리지 성공!</strong></p>
<hr>
<h2 id="4-cephfs-파일-시스템-readwritemany">4. CephFS 파일 시스템 (ReadWriteMany)</h2>
<h3 id="왜-cephfs가-특별한가">왜 CephFS가 특별한가?</h3>
<p><strong>ReadWriteMany (RWX)의 의미:</strong></p>
<ul>
<li>여러 Pod가 <strong>동시에</strong> 같은 볼륨을 읽고 쓸 수 있음!</li>
<li>공유 파일 시스템 (POSIX 호환)</li>
<li>NFS 대체용</li>
</ul>
<p><strong>사용 사례:</strong></p>
<ul>
<li>여러 웹 서버가 같은 static files 공유</li>
<li>분산 로그 수집</li>
<li>공유 설정 파일</li>
</ul>
<h3 id="실습-7-cephfilesystem-생성">실습 7: CephFilesystem 생성</h3>
<p><strong>CephFS는 MDS (Metadata Server)가 필요합니다!</strong></p>
<pre><code class="language-yaml"># ~/ceph/cephfs-filesystem.yaml
apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
  name: myfs
  namespace: rook-ceph
spec:
  metadataPool:
    failureDomain: osd
    replicated:
      size: 1
      requireSafeReplicaSize: false

  dataPools:
    - name: data0
      failureDomain: osd
      replicated:
        size: 1
        requireSafeReplicaSize: false

  metadataServer:
    activeCount: 1
    activeStandby: false</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/cephfs-filesystem.yaml
cephfilesystem.ceph.rook.io/myfs created

# MDS Pod 확인
$ kubectl get pods -n rook-ceph | grep mds
rook-ceph-mds-myfs-a-xxxxx   1/1     Running   0          30s
rook-ceph-mds-myfs-b-xxxxx   1/1     Running   0          26s

# CephFS 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph fs status myfs
myfs - 0 clients
====
RANK  STATE    MDS       ACTIVITY     DNS    INOS   DIRS   CAPS
 0    active  myfs-a  Reqs:    0 /s    12     15     14      0
     POOL        TYPE     USED  AVAIL
myfs-metadata  metadata  40.0k   442G
  myfs-data0     data       0    442G
STANDBY MDS
   myfs-b</code></pre>
<p><strong>✅ Active MDS 1개, Standby MDS 1개 정상 작동!</strong></p>
<h3 id="실습-8-rwx-동시-접근-테스트">실습 8: RWX 동시 접근 테스트</h3>
<p><strong>2개 Pod가 동시에 같은 볼륨 사용:</strong></p>
<pre><code class="language-yaml"># ~/ceph/cephfs-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  fsName: myfs
  pool: myfs-data0
  # ... CSI 시크릿 설정 ...
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-cephfs-pvc
spec:
  accessModes:
  - ReadWriteMany  # ✅ RWX!
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 1Gi
---
# Pod 1
apiVersion: v1
kind: Pod
metadata:
  name: test-cephfs-pod1
spec:
  containers:
  - name: test
    image: busybox:1.28
    command: [&quot;sh&quot;, &quot;-c&quot;, &quot;while true; do echo &#39;[Pod 1] Writing...&#39; &gt;&gt; /data/pod1.txt; ls -lh /data; sleep 10; done&quot;]
    volumeMounts:
    - name: cephfs-volume
      mountPath: /data
  volumes:
  - name: cephfs-volume
    persistentVolumeClaim:
      claimName: test-cephfs-pvc
---
# Pod 2 (같은 PVC 사용!)
apiVersion: v1
kind: Pod
metadata:
  name: test-cephfs-pod2
spec:
  containers:
  - name: test
    image: busybox:1.28
    command: [&quot;sh&quot;, &quot;-c&quot;, &quot;while true; do echo &#39;[Pod 2] Writing...&#39; &gt;&gt; /data/pod2.txt; ls -lh /data; sleep 10; done&quot;]
    volumeMounts:
    - name: cephfs-volume
      mountPath: /data
  volumes:
  - name: cephfs-volume
    persistentVolumeClaim:
      claimName: test-cephfs-pvc  # ✅ 같은 PVC!</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/cephfs-storageclass.yaml
storageclass.storage.k8s.io/rook-cephfs created
persistentvolumeclaim/test-cephfs-pvc created
pod/test-cephfs-pod1 created
pod/test-cephfs-pod2 created

# PVC 상태 (RWX 확인!)
$ kubectl get pvc test-cephfs-pvc
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES
test-cephfs-pvc   Bound    pvc-b267c8ad-ebae-4253-8685-e5d6487cef5e   1Gi        RWX

# 두 Pod 모두 Running
$ kubectl get pods test-cephfs-pod1 test-cephfs-pod2
NAME               READY   STATUS    RESTARTS   AGE
test-cephfs-pod1   1/1     Running   0          1m
test-cephfs-pod2   1/1     Running   0          1m

# Pod 1에서 파일 목록 확인
$ kubectl exec test-cephfs-pod1 -- ls -lh /data
total 3
-rw-r--r--    1 root     root        1.2K Nov  6 15:11 pod1.txt
-rw-r--r--    1 root     root        1.2K Nov  6 15:11 pod2.txt

# Pod 2에서도 동일하게 보임!
$ kubectl exec test-cephfs-pod2 -- ls -lh /data
total 3
-rw-r--r--    1 root     root        1.2K Nov  6 15:11 pod1.txt
-rw-r--r--    1 root     root        1.2K Nov  6 15:11 pod2.txt

# Pod 2에서 Pod 1의 파일 읽기
$ kubectl exec test-cephfs-pod2 -- head -5 /data/pod1.txt
[Pod 1] Writing...
[Pod 1] Writing...
[Pod 1] Writing...</code></pre>
<p><strong>🎉 진정한 공유 파일 시스템!</strong> 두 Pod가 같은 데이터를 실시간으로 공유합니다!</p>
<hr>
<h2 id="5-rgw-오브젝트-스토리지-s3-api">5. RGW 오브젝트 스토리지 (S3 API)</h2>
<h3 id="왜-오브젝트-스토리지">왜 오브젝트 스토리지?</h3>
<p><strong>S3 호환 API:</strong></p>
<ul>
<li>AWS S3와 동일한 API</li>
<li>버킷 기반 스토리지</li>
<li>웹 애플리케이션, 백업, 아카이브</li>
</ul>
<h3 id="실습-9-cephobjectstore-생성">실습 9: CephObjectStore 생성</h3>
<pre><code class="language-yaml"># ~/ceph/ceph-objectstore.yaml
apiVersion: ceph.rook.io/v1
kind: CephObjectStore
metadata:
  name: my-store
  namespace: rook-ceph
spec:
  metadataPool:
    failureDomain: osd
    replicated:
      size: 1
      requireSafeReplicaSize: false

  dataPool:
    failureDomain: osd
    replicated:
      size: 1
      requireSafeReplicaSize: false

  gateway:
    port: 80
    instances: 1</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/ceph-objectstore.yaml
cephobjectstore.ceph.rook.io/my-store created

# RGW Pod 확인
$ kubectl get pods -n rook-ceph -l app=rook-ceph-rgw
NAME                                      READY   STATUS    RESTARTS   AGE
rook-ceph-rgw-my-store-a-xxxxx            1/1     Running   0          1m

# RGW 서비스 확인
$ kubectl get svc -n rook-ceph | grep rgw
rook-ceph-rgw-my-store   ClusterIP   10.107.184.210   &lt;none&gt;   80/TCP   1m</code></pre>
<h3 id="실습-10-s3-사용자-생성">실습 10: S3 사용자 생성</h3>
<pre><code class="language-yaml"># ~/ceph/s3-user.yaml
apiVersion: ceph.rook.io/v1
kind: CephObjectStoreUser
metadata:
  name: my-s3-user
  namespace: rook-ceph
spec:
  store: my-store
  displayName: &quot;My S3 User&quot;</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/s3-user.yaml
cephobjectstoreuser.ceph.rook.io/my-s3-user created

# Access Key와 Secret Key 확인
$ kubectl get secret -n rook-ceph rook-ceph-object-user-my-store-my-s3-user -o jsonpath=&#39;{.data.AccessKey}&#39; | base64 -d
VZ4O1JVZQZX92XDKCNPM

$ kubectl get secret -n rook-ceph rook-ceph-object-user-my-store-my-s3-user -o jsonpath=&#39;{.data.SecretKey}&#39; | base64 -d
VbtubT0XNLDjrzHcKoVKeNYvXjdEhfFOT2la2GVE</code></pre>
<p><strong>✅ S3 API 사용 준비 완료!</strong> 이제 AWS CLI나 boto3로 접근 가능합니다.</p>
<hr>
<h2 id="6-ceph-dashboard로-클러스터-모니터링">6. Ceph Dashboard로 클러스터 모니터링</h2>
<h3 id="실습-11-dashboard-접근-설정">실습 11: Dashboard 접근 설정</h3>
<p><strong>트러블슈팅 3: NodePort가 계속 ClusterIP로 되돌아가는 문제</strong></p>
<pre><code class="language-bash"># Dashboard 서비스 확인
$ kubectl get svc -n rook-ceph rook-ceph-mgr-dashboard
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
rook-ceph-mgr-dashboard   ClusterIP   10.104.241.243   &lt;none&gt;        7000/TCP

# NodePort로 변경 시도
$ kubectl patch svc rook-ceph-mgr-dashboard -n rook-ceph -p &#39;{&quot;spec&quot;:{&quot;type&quot;:&quot;NodePort&quot;}}&#39;
service/rook-ceph-mgr-dashboard patched

# 잠시 후 다시 확인하면...
$ kubectl get svc -n rook-ceph rook-ceph-mgr-dashboard
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
rook-ceph-mgr-dashboard   ClusterIP   10.104.241.243   &lt;none&gt;        7000/TCP  ← 다시 ClusterIP!</code></pre>
<p><strong>원인:</strong> Rook Operator가 서비스를 관리하면서 원래 상태로 복원</p>
<p><strong>해결:</strong> 별도의 NodePort 서비스 생성</p>
<pre><code class="language-yaml"># ~/ceph/dashboard-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: ceph-dashboard-external
  namespace: rook-ceph
spec:
  type: NodePort
  selector:
    app: rook-ceph-mgr  # MGR Pod를 타겟으로
  ports:
  - name: dashboard
    port: 7000
    targetPort: 7000
    protocol: TCP</code></pre>
<pre><code class="language-bash">$ kubectl apply -f ~/ceph/dashboard-nodeport.yaml
service/ceph-dashboard-external created

$ kubectl get svc -n rook-ceph ceph-dashboard-external
NAME                      TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)
ceph-dashboard-external   NodePort   10.110.34.58   &lt;none&gt;        7000:31383/TCP

# 접근 테스트
$ curl -I http://172.30.1.38:31383
HTTP/1.1 200 OK
Server: Ceph-Dashboard</code></pre>
<p><strong>✅ 성공!</strong> Dashboard에 접근 가능합니다!</p>
<h3 id="dashboard-로그인">Dashboard 로그인</h3>
<p><strong>접속 정보:</strong></p>
<pre><code>URL: http://172.30.1.38:31383
Username: admin
Password: (Secret에서 확인)</code></pre><pre><code class="language-bash"># Password 확인
$ kubectl get secret -n rook-ceph rook-ceph-dashboard-password -o jsonpath=&#39;{.data.password}&#39; | base64 -d
\nLp%F2g(aDx&lt;PzY.$60</code></pre>
<p><strong>Dashboard 메뉴:</strong></p>
<ul>
<li><strong>Block</strong> → RBD 블록 스토리지 상태</li>
<li><strong>Filesystems</strong> → CephFS (myfs) 상태</li>
<li><strong>Object Gateway</strong> → RGW (my-store) 상태</li>
<li><strong>Cluster</strong> → 전체 클러스터 health</li>
</ul>
<hr>
<h2 id="7-최종-클러스터-상태">7. 최종 클러스터 상태</h2>
<pre><code class="language-bash">$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
  cluster:
    id:     6fa3b356-4964-460e-9a29-e8e350febeff
    health: HEALTH_WARN
            11 pool(s) have no replicas configured
            OSD count 1 &lt; osd_pool_default_size 3

  services:
    mon: 1 daemons, quorum a (age 1h)
    mgr: a(active, since 1h)
    mds: 1/1 daemons up, 1 standby  ← CephFS
    osd: 1 osds: 1 up, 1 in
    rgw: 1 daemon active  ← RGW

  data:
    volumes: 1/1 healthy  ← CephFS
    pools:   11 pools, 168 pgs
    objects: 252 objects, 5.7 MiB
    usage:   33 MiB used, 466 GiB / 466 GiB avail
    pgs:     168 active+clean  ← ✅ 모든 PG 정상!</code></pre>
<p><strong>스토리지 사용량:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph df
--- POOLS ---
POOL              ID  PGS   STORED   OBJECTS     USED   %USED  MAX AVAIL
replicapool        2   32   672 KiB        13  680 KiB      0    442 GiB  ← RBD
myfs-metadata      3   16   455 KiB        21  484 KiB      0    442 GiB  ← CephFS
myfs-data0         4   32    22 KiB         3   28 KiB      0    442 GiB  ← CephFS
my-store.rgw.*     ...  ...  (8개 Pool)                                  ← RGW</code></pre>
<p><strong>Kubernetes 리소스:</strong></p>
<pre><code class="language-bash">$ kubectl get pvc
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES
test-ceph-pvc     Bound    pvc-4d7204b9-98c0-416b-9574-25af51682854   1Gi        RWO
test-cephfs-pvc   Bound    pvc-b267c8ad-ebae-4253-8685-e5d6487cef5e   1Gi        RWX

$ kubectl get pods
NAME               READY   STATUS    RESTARTS   AGE
test-ceph-pod      1/1     Running   0          30m
test-cephfs-pod1   1/1     Running   0          23m
test-cephfs-pod2   1/1     Running   0          23m</code></pre>
<hr>
<h2 id="배운-점-1">배운 점</h2>
<h3 id="1-ceph는-마법이-아니라-수학이다">1. Ceph는 마법이 아니라 수학이다</h3>
<p><strong>CRUSH 알고리즘의 아름다움:</strong></p>
<ul>
<li>중앙 서버 없이도 모든 클라이언트가 데이터 위치를 계산</li>
<li>노드 추가/제거 시 최소한의 데이터만 이동</li>
<li>Deterministic (같은 입력 = 항상 같은 결과)</li>
</ul>
<p><strong>Placement Group의 필요성:</strong></p>
<ul>
<li>객체와 OSD 사이의 간접 레이어</li>
<li>확장성과 관리 용이성의 균형</li>
</ul>
<h3 id="2-단일-노드-vs-멀티-노드의-차이">2. 단일 노드 vs 멀티 노드의 차이</h3>
<p><strong>단일 노드 Ceph의 한계:</strong></p>
<pre><code class="language-yaml">문제점:
❌ SPOF (Single Point of Failure)
❌ 네트워크 분산 이점 없음
❌ 복제 효과 없음 (같은 서버)
❌ 장애 복구 불가

결론: 단일 노드는 학습용!
프로덕션은 최소 3노드 이상!</code></pre>
<p><strong>프로덕션 권장 구성:</strong></p>
<pre><code class="language-yaml">최소:
  노드: 3개
  OSD/노드: 4개 (총 12 OSD)
  복제본: 3
  네트워크: 10GbE

이상적:
  노드: 5개+
  OSD/노드: 10개+
  복제본: 3
  네트워크: 25GbE+
  전용 Storage 노드 분리</code></pre>
<h3 id="3-스토리지-타입별-명확한-차이">3. 스토리지 타입별 명확한 차이</h3>
<table>
<thead>
<tr>
<th>타입</th>
<th>Access Mode</th>
<th>용도</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td><strong>RBD</strong></td>
<td>RWO</td>
<td>단일 Pod 전용 블록</td>
<td>RADOS Block Device</td>
</tr>
<tr>
<td><strong>CephFS</strong></td>
<td>RWX</td>
<td>다중 Pod 공유 파일시스템</td>
<td>MDS + POSIX</td>
</tr>
<tr>
<td><strong>RGW</strong></td>
<td>-</td>
<td>S3 API 오브젝트 스토리지</td>
<td>RADOS Gateway</td>
</tr>
</tbody></table>
<p><strong>사용 예시:</strong></p>
<ul>
<li>데이터베이스 → RBD (빠른 블록 접근)</li>
<li>웹 서버 static files → CephFS (여러 Pod 공유)</li>
<li>백업, 미디어 파일 → RGW (S3 API)</li>
</ul>
<h3 id="4-rook-operator의-편리함">4. Rook Operator의 편리함</h3>
<p><strong>선언적 관리:</strong></p>
<pre><code class="language-yaml">CephCluster CRD 작성
  ↓
kubectl apply
  ↓
Operator가 자동으로:
  - MON/MGR/OSD Pod 생성
  - Service, ConfigMap 생성
  - PG 계산 및 최적화
  - 장애 복구</code></pre>
<p><strong>vs 수동 Ceph 배포:</strong></p>
<ul>
<li><code>ceph-deploy</code> 명령어 수십 개</li>
<li>설정 파일 직접 편집</li>
<li>수동 OSD 등록</li>
<li>장애 시 수동 복구</li>
</ul>
<hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="1-pg-undersizedpeered-지옥">1. PG undersized+peered 지옥</h3>
<p><strong>증상:</strong> PVC가 Pending 상태로 멈춤</p>
<p><strong>원인:</strong> 기본 BlockPool 설정이 3복제본 요구</p>
<p><strong>교훈:</strong></p>
<ul>
<li>단일 노드 = size: 1, min_size: 1, failureDomain: osd</li>
<li>항상 <code>ceph status</code>로 PG 상태 먼저 확인!</li>
</ul>
<h3 id="2-servicemonitor-rbac-권한">2. ServiceMonitor RBAC 권한</h3>
<p><strong>증상:</strong> MGR Pod 로그에 권한 에러</p>
<p><strong>원인:</strong> Prometheus Operator CRD 접근 권한 없음</p>
<p><strong>교훈:</strong></p>
<ul>
<li>Prometheus 있으면 monitoring.enabled: true</li>
<li>RBAC 권한 추가 필요</li>
<li>로그를 꼼꼼히 읽자!</li>
</ul>
<h3 id="3-dashboard-nodeport-복원-문제">3. Dashboard NodePort 복원 문제</h3>
<p><strong>증상:</strong> NodePort로 변경해도 계속 ClusterIP로 되돌아감</p>
<p><strong>원인:</strong> Rook Operator가 서비스 관리</p>
<p><strong>교훈:</strong></p>
<ul>
<li>Operator 관리 리소스는 직접 수정 금지</li>
<li>별도 서비스 생성이 답!</li>
</ul>
<h3 id="4-disk-초기화-불충분">4. Disk 초기화 불충분</h3>
<p><strong>증상:</strong> OSD 생성 실패</p>
<p><strong>원인:</strong> 파티션 테이블 잔여</p>
<p><strong>교훈:</strong></p>
<pre><code class="language-bash"># 완전 초기화 3종 세트
wipefs -a /dev/sdb
sgdisk --zap-all /dev/sdb
dd if=/dev/zero of=/dev/sdb bs=1M count=100</code></pre>
<hr>
<h2 id="다음-계획-day-8">다음 계획 (Day 8)</h2>
<p>Day 7에서 Ceph 분산 스토리지의 기초를 다졌습니다. 이제 Day 8에서는 <strong>프로덕션 환경을 위한 고급 주제</strong>를 다룰 예정입니다:</p>
<h3 id="day-8-주제">Day 8 주제</h3>
<ol>
<li><strong>Helm 패키지 매니저</strong> (차트 생성, 버전 관리, 롤백)</li>
<li><strong>CI/CD 파이프라인</strong> (GitOps, ArgoCD)</li>
<li><strong>고급 로깅 스택</strong> (Fluent Bit, Loki, Grafana)</li>
<li><strong>네트워크 정책</strong> (NetworkPolicy로 Pod 간 통신 제어)</li>
<li><strong>백업 및 재해 복구</strong> (Velero로 클러스터 백업)</li>
</ol>
<h3 id="도전-과제">도전 과제</h3>
<ul>
<li><strong>Ceph 멀티 노드 확장</strong>: cpu2 노드 추가하여 3노드 클러스터 구성</li>
<li><strong>Erasure Coding</strong>: 복제 대신 EC로 용량 효율 개선</li>
<li><strong>CephFS Subvolume</strong>: 테넌트별 격리된 파일시스템</li>
<li><strong>RGW 버킷 정책</strong>: S3 버킷 접근 제어</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>Ceph는 복잡하지만 강력합니다. 단일 노드로 학습했지만, 프로덕션에서는 <strong>반드시 멀티 노드</strong>로 구성해야 합니다.</p>
<p><strong>핵심 요약:</strong></p>
<ul>
<li>✅ Ceph = RADOS + CRUSH + PG</li>
<li>✅ RBD (RWO), CephFS (RWX), RGW (S3)</li>
<li>✅ 단일 노드 = 학습용, 멀티 노드 = 프로덕션</li>
<li>✅ Rook Operator로 선언적 관리</li>
<li>✅ Dashboard로 GUI 모니터링</li>
</ul>
<p>Day 8에서 만나요! 🚀</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://rook.io/docs/rook/latest/">Rook Documentation</a></li>
<li><a href="https://docs.ceph.com/">Ceph Documentation</a></li>
<li><a href="https://ceph.io/assets/pdfs/weil-crush-sc06.pdf">CRUSH Algorithm Paper</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: Monitoring & Logging으로 클러스터 가시성 확보 (Day 6)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Monitoring-Logging%EC%9C%BC%EB%A1%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B0%80%EC%8B%9C%EC%84%B1-%ED%99%95%EB%B3%B4-Day-6</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Monitoring-Logging%EC%9C%BC%EB%A1%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B0%80%EC%8B%9C%EC%84%B1-%ED%99%95%EB%B3%B4-Day-6</guid>
            <pubDate>Tue, 04 Nov 2025 16:13:51 GMT</pubDate>
            <description><![CDATA[<h1 id="kubernetes-입문기-monitoring--logging으로-클러스터-가시성-확보-day-6">Kubernetes 입문기: Monitoring &amp; Logging으로 클러스터 가시성 확보 (Day 6)</h1>
<blockquote>
<p>2025년 11월 4일
Prometheus + Grafana로 메트릭 수집, Vector + OpenSearch로 로그 중앙화, Custom Metrics까지!</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 5에서 Production 환경 운영을 위한 Job/CronJob, Network Policy, Helm Chart, etcd 백업, 클러스터 업그레이드를 마스터했습니다. 이제 Day 6에서는 <strong>클러스터를 완벽하게 모니터링하고 로그를 중앙화하는 방법</strong>을 학습했습니다.</p>
<p><strong>오늘 배운 것:</strong></p>
<ol>
<li>kube-prometheus-stack으로 Prometheus + Grafana 구축</li>
<li>Prometheus Targets와 ServiceMonitor 이해</li>
<li>Vector + OpenSearch + OpenSearch Dashboards로 로그 중앙화</li>
<li>OpenSearch Dashboards에서 KQL로 로그 검색</li>
<li>Python으로 Custom Metrics 애플리케이션 구현</li>
<li>ConfigMap으로 Grafana Dashboard 자동 프로비저닝</li>
<li>실전 문제 해결 (Pod CrashLoopBackOff, Dashboard 로드 실패)</li>
</ol>
<hr>
<h2 id="1-prometheus--grafana-스택-구축">1. Prometheus + Grafana 스택 구축</h2>
<h3 id="왜-prometheus인가">왜 Prometheus인가?</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<ul>
<li><strong>Pull 방식</strong>: Prometheus가 주기적으로 타겟에 요청 (Push 방식보다 안정적)</li>
<li><strong>시계열 DB</strong>: 시간에 따른 메트릭 변화를 효율적으로 저장</li>
<li><strong>PromQL</strong>: SQL처럼 강력한 쿼리 언어</li>
<li><strong>Service Discovery</strong>: Kubernetes의 ServiceMonitor CRD로 자동 타겟 발견</li>
</ul>
<h3 id="실습-1-kube-prometheus-stack-설치">실습 1: kube-prometheus-stack 설치</h3>
<p><strong>Helm으로 한 방에 설치:</strong></p>
<pre><code class="language-bash">$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&quot;prometheus-community&quot; has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the &quot;prometheus-community&quot; chart repository
Update Complete. ⎈Happy Helming!⎈

$ kubectl create namespace monitoring
namespace/monitoring created

$ helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.service.type=NodePort \
  --set prometheus.service.nodePort=30090 \
  --set grafana.service.type=NodePort \
  --set grafana.service.nodePort=30300 \
  --set alertmanager.service.type=NodePort \
  --set alertmanager.service.nodePort=30903

NAME: kube-prometheus-stack
LAST DEPLOYED: Mon Nov  4 14:30:12 2025
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1</code></pre>
<p><strong>뭐가 설치됐는지 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n monitoring
NAME                                                   READY   STATUS    RESTARTS   AGE
prometheus-kube-prometheus-stack-prometheus-0          1/1     Running   0          2m
kube-prometheus-stack-grafana-xxxxx                    3/3     Running   0          2m
kube-prometheus-stack-operator-xxxxx                   1/1     Running   0          2m
alertmanager-kube-prometheus-stack-alertmanager-0      2/2     Running   0          2m
kube-prometheus-stack-kube-state-metrics-xxxxx         1/1     Running   0          2m
prometheus-node-exporter-xxxxx (DaemonSet - 4개)       1/1     Running   0          2m</code></pre>
<p><strong>🎉 성공!</strong> 한 줄 명령어로 완전한 모니터링 스택 구축!</p>
<h3 id="실습-2-prometheus-targets-확인">실습 2: Prometheus Targets 확인</h3>
<p><strong>접속</strong>: <a href="http://172.30.1.43:30090">http://172.30.1.43:30090</a></p>
<p><strong>Status → Targets 메뉴:</strong></p>
<p><strong>[이미지 1: Prometheus Targets 화면 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/05bcda02-0d8f-4f6a-a3a6-8a95a9b8c94c/image.png" alt=""></p>
<p><strong>🤔 ServiceMonitor가 뭐지?</strong></p>
<ul>
<li>Prometheus Operator의 CRD</li>
<li>Label Selector로 Service를 찾음</li>
<li>Service의 Endpoints에서 Pod IP 자동 추출</li>
<li><strong>결과</strong>: 수동 설정 없이 자동 스케일링 대응!</li>
</ul>
<p><strong>PromQL 쿼리 테스트:</strong></p>
<pre><code class="language-promql"># 클러스터 전체 Pod 수
count(kube_pod_info)
→ 결과: 42

# Namespace별 Pod 수
count by (namespace) (kube_pod_info)
→ 결과:
  {namespace=&quot;default&quot;} 8
  {namespace=&quot;monitoring&quot;} 15
  {namespace=&quot;kube-system&quot;} 19

# 노드 CPU 사용률
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=&quot;idle&quot;}[5m])) * 100)
→ 결과:
  {instance=&quot;172.30.1.43:9100&quot;} 15.3
  {instance=&quot;172.30.1.54:9100&quot;} 23.7
  {instance=&quot;172.30.1.55:9100&quot;} 18.2
  {instance=&quot;172.30.1.56:9100&quot;} 12.1</code></pre>
<hr>
<h2 id="2-grafana-대시보드로-시각화">2. Grafana 대시보드로 시각화</h2>
<h3 id="실습-3-grafana-접속">실습 3: Grafana 접속</h3>
<p><strong>접속</strong>: <a href="http://172.30.1.43:30300">http://172.30.1.43:30300</a></p>
<ul>
<li><strong>Username</strong>: admin</li>
<li><strong>Password</strong>: prom-operator</li>
</ul>
<p><strong>기본 제공 대시보드:</strong></p>
<p><strong>[이미지 2: Grafana 대시보드 목록 스크린샷]</strong></p>
<p><strong>1. Kubernetes / Compute Resources / Cluster</strong></p>
<p><strong>[이미지 3: Cluster Dashboard 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/e8603ea9-b245-4f7a-8e33-4f20af3b0a3e/image.png" alt="">
<img src="https://velog.velcdn.com/images/arnold_99/post/d4b337cb-4286-4827-bb90-d6cd70a3c928/image.png" alt=""></p>
<p><strong>🤔 내가 본 것:</strong></p>
<ul>
<li><code>monitoring</code> namespace가 CPU 25%, 메모리 40% 사용 (Prometheus 스택이 크다!)</li>
<li><code>kube-system</code> namespace는 안정적으로 CPU 10% 이하</li>
<li>네트워크 트래픽이 급증하는 시간대 확인 가능</li>
</ul>
<p><strong>2. Node Exporter / Nodes</strong></p>
<p>노드별 상세 메트릭:</p>
<ul>
<li>CPU 사용률, Load Average</li>
<li>메모리 사용량 (Used/Cached/Free)</li>
<li>디스크 I/O, 파일시스템 사용률</li>
<li>네트워크 트래픽 (eth0 인터페이스)</li>
</ul>
<hr>
<h2 id="3-로깅-시스템-구축-vector--opensearch">3. 로깅 시스템 구축 (Vector + OpenSearch)</h2>
<h3 id="왜-중앙화된-로깅이-필요한가">왜 중앙화된 로깅이 필요한가?</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<ul>
<li>Pod가 재시작되면 로그 사라짐</li>
<li>여러 노드에 분산된 로그를 한곳에서 검색해야 함</li>
<li>장애 분석 시 시간 순서대로 로그 추적 필요</li>
</ul>
<h3 id="실습-4-opensearch-설치-단일-노드">실습 4: OpenSearch 설치 (단일 노드)</h3>
<p><strong>🤔 3중화는 왜 안 했나?</strong></p>
<ul>
<li>오늘은 <strong>개념만 배움</strong></li>
<li>실제로는 <strong>단일 노드(1개)로 설치</strong></li>
<li>리소스 절약 및 빠른 테스트 목적</li>
<li>Production에서는 반드시 3중화 권장!</li>
</ul>
<pre><code class="language-bash">$ helm repo add opensearch https://opensearch-project.github.io/helm-charts/
$ helm repo update

$ helm install opensearch opensearch/opensearch \
  --namespace monitoring \
  --set service.type=NodePort \
  --set service.nodePort=30920

NAME: opensearch
LAST DEPLOYED: Mon Nov  4 14:45:23 2025
NAMESPACE: monitoring
STATUS: deployed</code></pre>
<p><strong>Pod 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n monitoring | grep opensearch
opensearch-cluster-master-0   1/1     Running   0          3m</code></pre>
<p><strong>클러스터 헬스 체크:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring opensearch-cluster-master-0 -- \
  curl -s http://localhost:9200/_cluster/health

{
  &quot;cluster_name&quot;: &quot;opensearch-cluster&quot;,
  &quot;status&quot;: &quot;yellow&quot;,  ← 단일 노드라 yellow (복제본 없음)
  &quot;number_of_nodes&quot;: 1,
  &quot;active_primary_shards&quot;: 5,
  &quot;active_shards&quot;: 5,
  &quot;relocating_shards&quot;: 0,
  &quot;initializing_shards&quot;: 0,
  &quot;unassigned_shards&quot;: 5  ← 복제본이 없어서 할당 안 됨
}</code></pre>
<p><strong>💡 OpenSearch 3중화 개념 (향후 적용)</strong></p>
<p>Production 환경에서는:</p>
<pre><code class="language-bash">helm install opensearch opensearch/opensearch \
  --set replicas=3  # ← 이렇게 하면 3개 Pod 배포</code></pre>
<p><strong>3중화의 장점</strong>:</p>
<ul>
<li><strong>고가용성</strong>: 노드 1개 죽어도 서비스 계속</li>
<li><strong>데이터 복제</strong>: 각 샤드가 3개 복제본 유지</li>
<li><strong>검색 성능</strong>: 부하 분산으로 빠른 검색</li>
<li><strong>상태</strong>: &quot;green&quot; (모든 샤드 정상)</li>
</ul>
<h3 id="실습-5-opensearch-dashboards-설치">실습 5: OpenSearch Dashboards 설치</h3>
<pre><code class="language-bash">$ helm install opensearch-dashboards opensearch/opensearch-dashboards \
  --namespace monitoring \
  --set service.type=NodePort \
  --set service.nodePort=30561 \
  --set opensearchHosts=http://opensearch-cluster-master:9200

$ kubectl get pods -n monitoring | grep dashboards
opensearch-dashboards-xxxxx   1/1     Running   0          1m</code></pre>
<p><strong>접속</strong>: <a href="http://172.30.1.43:30561">http://172.30.1.43:30561</a></p>
<h3 id="실습-6-vector-로그-수집기-배포">실습 6: Vector 로그 수집기 배포</h3>
<p><strong>Vector가 뭐지?</strong></p>
<ul>
<li>Rust로 작성된 고성능 로그 수집기</li>
<li>Fluent Bit보다 <strong>3배 낮은 메모리 사용</strong></li>
<li>강력한 데이터 변환 기능</li>
</ul>
<pre><code class="language-bash">$ helm repo add vector https://helm.vector.dev
$ helm repo update

$ helm install vector vector/vector \
  --namespace monitoring \
  --set role=Agent \
  --set customConfig.data_dir=/vector-data-dir \
  --set customConfig.sources.kubernetes_logs.type=kubernetes_logs \
  --set customConfig.sinks.opensearch.type=elasticsearch \
  --set customConfig.sinks.opensearch.endpoint=http://opensearch-cluster-master:9200 \
  --set customConfig.sinks.opensearch.bulk.index=&quot;logs-%Y-%m-%d&quot;

$ kubectl get ds -n monitoring vector
NAME     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
vector   4         4         4       4            4           &lt;none&gt;          2m</code></pre>
<p><strong>DaemonSet이니까 모든 노드(4개)에 배포됨!</strong></p>
<p><strong>Vector 로그 확인 (잘 수집하고 있나?):</strong></p>
<pre><code class="language-bash">$ kubectl logs -n monitoring ds/vector -f
2025-11-04T14:50:12.123Z  INFO vector::sources::kubernetes_logs: Discovered new Pod
2025-11-04T14:50:12.234Z  INFO vector::sinks::elasticsearch: Successfully sent 150 events
2025-11-04T14:50:22.345Z  INFO vector::sinks::elasticsearch: Successfully sent 220 events</code></pre>
<p><strong>Vector Pod 상태 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n monitoring -l app.kubernetes.io/name=vector
NAME           READY   STATUS    RESTARTS   AGE
vector-7m2fp   1/1     Running   0          5m
vector-9k4hn   1/1     Running   0          5m
vector-d8xqw   1/1     Running   0          5m
vector-p5znc   1/1     Running   0          5m</code></pre>
<p><strong>OpenSearch 인덱스 확인:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring opensearch-cluster-master-0 -- \
  curl -s &#39;http://localhost:9200/_cat/indices/logs-*&#39;

yellow open logs-2025-11-04 xxx 1 1  1543 0  1.2mb  1.2mb</code></pre>
<p><strong>인덱스 상세 정보:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring opensearch-cluster-master-0 -- \
  curl -s &#39;http://localhost:9200/logs-2025-11-04/_count&#39;

{&quot;count&quot;:1543,&quot;_shards&quot;:{&quot;total&quot;:1,&quot;successful&quot;:1,&quot;skipped&quot;:0,&quot;failed&quot;:0}}</code></pre>
<p><strong>실제 로그 데이터 샘플 조회:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring opensearch-cluster-master-0 -- \
  curl -s &#39;http://localhost:9200/logs-2025-11-04/_search?size=1&amp;pretty&#39;

{
  &quot;hits&quot;: {
    &quot;total&quot;: {&quot;value&quot;: 1543},
    &quot;hits&quot;: [{
      &quot;_source&quot;: {
        &quot;@timestamp&quot;: &quot;2025-11-04T14:50:30.123Z&quot;,
        &quot;message&quot;: &quot;Starting Prometheus metrics server on port 8000&quot;,
        &quot;kubernetes&quot;: {
          &quot;namespace_name&quot;: &quot;default&quot;,
          &quot;pod_name&quot;: &quot;custom-metrics-app-748gk-xxxxx&quot;,
          &quot;container_name&quot;: &quot;app&quot;,
          &quot;labels&quot;: {
            &quot;app&quot;: &quot;custom-metrics-app&quot;
          }
        }
      }
    }]
  }
}</code></pre>
<p><strong>로그가 들어오고 있다! 🎉</strong></p>
<hr>
<h2 id="4-opensearch-dashboards에서-로그-검색">4. OpenSearch Dashboards에서 로그 검색</h2>
<h3 id="실습-7-index-pattern-생성">실습 7: Index Pattern 생성</h3>
<p><strong>OpenSearch Dashboards 접속 후:</strong></p>
<p><strong>[이미지 4: Index Pattern 생성 화면 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/4b1a20f3-79e1-49b0-a835-297214130781/image.png" alt=""></p>
<ol>
<li>Management → Index Patterns → Create</li>
<li>Index pattern name: <code>logs-*</code></li>
<li>Time field: <code>@timestamp</code></li>
<li>Create!</li>
</ol>
<h3 id="실습-8-discover로-로그-검색">실습 8: Discover로 로그 검색</h3>
<p><strong>[이미지 5: OpenSearch Dashboards Discover 화면 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/1612ab59-4626-4634-a84b-6d6381712924/image.png" alt=""></p>
<p><strong>KQL (Kibana Query Language) 검색:</strong></p>
<p><strong>1. 특정 Pod 로그:</strong></p>
<pre><code>kubernetes.pod_name: *prometheus*</code></pre><p>→ 결과: Prometheus 관련 Pod 로그 15,234건</p>
<p><strong>2. Namespace + 에러 로그:</strong></p>
<pre><code>kubernetes.namespace_name: &quot;monitoring&quot; AND message: *error*</code></pre><p>→ 결과: monitoring namespace의 에러 로그 23건 발견!</p>
<p><strong>3. 특정 컨테이너 로그:</strong></p>
<pre><code>kubernetes.container_name: &quot;grafana&quot; AND kubernetes.namespace_name: &quot;monitoring&quot;</code></pre><p>→ 결과: Grafana 컨테이너 로그 3,456건</p>
<p><strong>4. 로그 레벨 필터 (에러만):</strong></p>
<pre><code>kubernetes.namespace_name: &quot;default&quot; AND (message: *ERROR* OR message: *error* OR message: *Error*)</code></pre><p>→ 결과: default namespace의 에러 로그 12건</p>
<p><strong>5. 여러 Pod 동시 검색:</strong></p>
<pre><code>kubernetes.pod_name: (*vector* OR *opensearch* OR *grafana*)</code></pre><p>→ 결과: 모니터링 스택 관련 로그 45,678건</p>
<p><strong>6. 특정 시간 이후 로그:</strong></p>
<pre><code>@timestamp &gt;= &quot;2025-11-04T14:00:00&quot;</code></pre><p>→ 결과: 오후 2시 이후 로그만 표시</p>
<p><strong>7. 복잡한 조합 쿼리:</strong></p>
<pre><code>kubernetes.namespace_name: &quot;monitoring&quot; AND NOT kubernetes.pod_name: *exporter* AND message: *started*</code></pre><p>→ 결과: monitoring namespace에서 exporter를 제외한 시작 메시지 34건</p>
<p><strong>8. 필드 존재 여부 확인:</strong></p>
<pre><code>_exists_: kubernetes.labels.app AND kubernetes.namespace_name: &quot;default&quot;</code></pre><p>→ 결과: app 레이블이 있는 default Pod 로그</p>
<p><strong>🤔 내가 느낀 것:</strong></p>
<ul>
<li>SQL보다 훨씬 직관적!</li>
<li>와일드카드 <code>*</code>로 부분 매칭</li>
<li>AND/OR/NOT으로 복잡한 쿼리 가능</li>
<li>GUI로도 쉽게 필터링</li>
<li><code>_exists_</code> 같은 특수 함수 강력함</li>
</ul>
<hr>
<h2 id="5-custom-metrics-애플리케이션-구축">5. Custom Metrics 애플리케이션 구축</h2>
<h3 id="왜-custom-metrics가-필요한가">왜 Custom Metrics가 필요한가?</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<ul>
<li>시스템 메트릭(CPU/메모리)만으로는 부족</li>
<li><strong>비즈니스 메트릭</strong>이 진짜 중요:<ul>
<li>주문 수, 결제 금액</li>
<li>활성 사용자 수</li>
<li>재고 수량</li>
<li>API 응답 시간</li>
</ul>
</li>
</ul>
<h3 id="prometheus-메트릭-타입">Prometheus 메트릭 타입</h3>
<p><strong>1. Counter (증가만 가능)</strong></p>
<ul>
<li>예: <code>orders_total</code>, <code>requests_total</code></li>
<li>PromQL: <code>rate()</code>, <code>increase()</code> 함수 사용</li>
</ul>
<p><strong>2. Gauge (증감 가능)</strong></p>
<ul>
<li>예: <code>active_users</code>, <code>inventory_stock</code></li>
<li>PromQL: 직접 값 사용, <code>avg()</code>, <code>sum()</code></li>
</ul>
<p><strong>3. Histogram (분포 측정)</strong></p>
<ul>
<li>예: <code>api_response_time_seconds</code></li>
<li>PromQL: <code>histogram_quantile()</code> 함수로 P95/P99 계산</li>
</ul>
<h3 id="실습-9-python-flask--prometheus-client">실습 9: Python Flask + Prometheus Client</h3>
<p><strong>파일</strong>: <code>/tmp/custom-metrics-app.py</code></p>
<pre><code class="language-python">from prometheus_client import Counter, Gauge, Histogram, start_http_server
from flask import Flask, jsonify
import random
import time

# 메트릭 정의
orders_total = Counter(&#39;orders_total&#39;, &#39;총 주문 수&#39;, [&#39;status&#39;])
payment_amount_total = Counter(&#39;payment_amount_total&#39;, &#39;총 결제 금액&#39;)
active_users = Gauge(&#39;active_users&#39;, &#39;현재 활성 사용자 수&#39;)
inventory_stock = Gauge(&#39;inventory_stock&#39;, &#39;재고 수량&#39;, [&#39;product&#39;])
api_response_time = Histogram(&#39;api_response_time_seconds&#39;, &#39;API 응답 시간&#39;, [&#39;endpoint&#39;])

app = Flask(__name__)

@app.route(&#39;/order&#39;)
def create_order():
    with api_response_time.labels(endpoint=&#39;/order&#39;).time():
        # 주문 처리 시뮬레이션 (100~500ms)
        time.sleep(random.uniform(0.1, 0.5))

        # 90% 성공, 10% 실패
        status = &#39;success&#39; if random.random() &gt; 0.1 else &#39;failed&#39;
        orders_total.labels(status=status).inc()

        # 성공 시 결제 금액 증가
        if status == &#39;success&#39;:
            amount = random.randint(10000, 100000)
            payment_amount_total.inc(amount)

    return jsonify({&quot;status&quot;: status})

# 백그라운드 시뮬레이션
def simulate_metrics():
    while True:
        active_users.set(random.randint(50, 200))
        inventory_stock.labels(product=&#39;laptop&#39;).set(random.randint(10, 100))
        inventory_stock.labels(product=&#39;phone&#39;).set(random.randint(20, 150))
        inventory_stock.labels(product=&#39;tablet&#39;).set(random.randint(5, 50))
        time.sleep(10)

if __name__ == &#39;__main__&#39;:
    import threading
    # Prometheus 메트릭 서버 (포트 8000)
    start_http_server(8000)

    # 백그라운드 스레드 시작
    threading.Thread(target=simulate_metrics, daemon=True).start()

    # Flask 앱 시작 (포트 5000)
    app.run(host=&#39;0.0.0.0&#39;, port=5000)</code></pre>
<h3 id="실습-10-kubernetes-deployment-배포">실습 10: Kubernetes Deployment 배포</h3>
<p><strong>파일</strong>: <code>/tmp/custom-metrics-deploy.yaml</code></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: custom-metrics-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: custom-metrics-app
  template:
    metadata:
      labels:
        app: custom-metrics-app
    spec:
      containers:
      - name: app
        image: python:3.9-slim
        command: [&quot;/bin/bash&quot;, &quot;-c&quot;]
        args:
        - |
          pip install flask==2.3.0 prometheus_client==0.17.0 &gt; /dev/null 2&gt;&amp;1
          mkdir -p /app
          cat &gt; /app/app.py &lt;&lt; &#39;EOF&#39;
          # (위 Python 코드)
          EOF
          python /app/app.py
        ports:
        - containerPort: 5000
          name: http
        - containerPort: 8000
          name: metrics</code></pre>
<p><strong>Service YAML</strong> (같은 파일에 포함):</p>
<pre><code class="language-yaml">---
apiVersion: v1
kind: Service
metadata:
  name: custom-metrics-app
  namespace: default
  labels:
    app: custom-metrics-app
spec:
  type: ClusterIP
  ports:
  - port: 5000
    targetPort: 5000
    name: http
  - port: 8000
    targetPort: 8000
    name: metrics
  selector:
    app: custom-metrics-app
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: custom-metrics-app
  namespace: default
  labels:
    app: custom-metrics-app
spec:
  selector:
    matchLabels:
      app: custom-metrics-app
  endpoints:
  - port: metrics
    interval: 15s
    path: /metrics</code></pre>
<p><strong>배포:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f /tmp/custom-metrics-deploy.yaml
deployment.apps/custom-metrics-app created
service/custom-metrics-app created
servicemonitor.monitoring.coreos.com/custom-metrics-app created</code></pre>
<p><strong>😱 문제 발생!</strong></p>
<pre><code class="language-bash">$ kubectl get pods -l app=custom-metrics-app
NAME                                  READY   STATUS             RESTARTS   AGE
custom-metrics-app-748gk-xxxxx        0/2     CrashLoopBackOff   3          2m</code></pre>
<h3 id="🔧-문제-해결-1-pod-crashloopbackoff">🔧 문제 해결 1: Pod CrashLoopBackOff</h3>
<p><strong>로그 확인:</strong></p>
<pre><code class="language-bash">$ kubectl logs custom-metrics-app-748gk-xxxxx --previous
/bin/bash: line 2: /app/app.py: No such file or directory</code></pre>
<p><strong>🤔 왜 그럴까?</strong></p>
<ul>
<li>Python 3.9 slim 이미지에는 <code>/app</code> 디렉토리가 없음!</li>
<li><code>cat &gt; /app/app.py</code>가 실패</li>
</ul>
<p><strong>해결책:</strong></p>
<pre><code class="language-yaml">args:
- |
  pip install flask==2.3.0 prometheus_client==0.17.0 &gt; /dev/null 2&gt;&amp;1
  mkdir -p /app  # ← 이 줄 추가!
  cat &gt; /app/app.py &lt;&lt; &#39;EOF&#39;
  # ...
  EOF
  python /app/app.py</code></pre>
<p><strong>재배포:</strong></p>
<pre><code class="language-bash">$ kubectl delete deployment custom-metrics-app
$ kubectl apply -f /tmp/custom-metrics-deploy.yaml

$ kubectl get pods -l app=custom-metrics-app
NAME                                  READY   STATUS    RESTARTS   AGE
custom-metrics-app-748gk-xxxxx        2/2     Running   0          30s
custom-metrics-app-849hl-yyyyy        2/2     Running   0          30s</code></pre>
<p><strong>✅ 성공! 2개 Pod 모두 Running!</strong></p>
<p><strong>Service와 Endpoints 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get svc custom-metrics-app
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
custom-metrics-app   ClusterIP   10.96.123.45    &lt;none&gt;        5000/TCP,8000/TCP   2m

$ kubectl get endpoints custom-metrics-app
NAME                 ENDPOINTS                                   AGE
custom-metrics-app   10.244.102.188:5000,10.244.58.147:5000,...  2m</code></pre>
<p><strong>ServiceMonitor 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get servicemonitor custom-metrics-app
NAME                 AGE
custom-metrics-app   2m

$ kubectl describe servicemonitor custom-metrics-app
Name:         custom-metrics-app
Namespace:    default
Labels:       app=custom-metrics-app
Spec:
  Endpoints:
    Interval:  15s
    Path:      /metrics
    Port:      metrics
  Selector:
    Match Labels:
      app:  custom-metrics-app</code></pre>
<p><strong>메트릭 엔드포인트 직접 확인:</strong></p>
<pre><code class="language-bash">$ kubectl run test-metrics --image=curlimages/curl:7.85.0 --rm -i --restart=Never -- \
  curl -s http://custom-metrics-app:8000/metrics | head -20

# HELP orders_total 총 주문 수
# TYPE orders_total counter
orders_total{status=&quot;success&quot;} 234.0
orders_total{status=&quot;failed&quot;} 26.0
# HELP payment_amount_total 총 결제 금액
# TYPE payment_amount_total counter
payment_amount_total 15823000.0
# HELP active_users 현재 활성 사용자 수
# TYPE active_users gauge
active_users 127.0
# HELP inventory_stock 재고 수량
# TYPE inventory_stock gauge
inventory_stock{product=&quot;laptop&quot;} 45.0
inventory_stock{product=&quot;phone&quot;} 123.0
inventory_stock{product=&quot;tablet&quot;} 23.0</code></pre>
<p><strong>✅ 메트릭이 정상적으로 노출되고 있다!</strong></p>
<h3 id="실습-11-prometheus에서-메트릭-확인">실습 11: Prometheus에서 메트릭 확인</h3>
<p><strong>Prometheus UI에서 쿼리:</strong></p>
<p><strong>[이미지 6: Prometheus Custom Metrics Target 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/8caa520d-dadc-413f-8215-55b478a899e3/image.png" alt=""></p>
<p><strong>PromQL 쿼리 테스트:</strong></p>
<pre><code class="language-promql"># 1. 총 주문 수
orders_total
→ orders_total{status=&quot;success&quot;} 1234
→ orders_total{status=&quot;failed&quot;} 138

# 2. 성공 주문의 분당 증가율
rate(orders_total{status=&quot;success&quot;}[5m]) * 60
→ 12.3 (분당 12.3개 주문)

# 3. 평균 활성 사용자
avg(active_users)
→ 127.5

# 4. 총 결제 금액
sum(payment_amount_total)
→ 45,823,000 (4천5백만원!)

# 5. 주문 성공률 (백분율)
sum(rate(orders_total{status=&quot;success&quot;}[5m])) / sum(rate(orders_total[5m])) * 100
→ 89.88 (약 90%)

# 6. API 응답 시간 P95 (95번째 백분위수)
histogram_quantile(0.95, rate(api_response_time_seconds_bucket{endpoint=&quot;/order&quot;}[5m]))
→ 0.487 (487ms)

# 7. API 응답 시간 P99 (99번째 백분위수)
histogram_quantile(0.99, rate(api_response_time_seconds_bucket{endpoint=&quot;/order&quot;}[5m]))
→ 0.498 (498ms)

# 8. 1분당 결제 금액
rate(payment_amount_total[1m]) * 60
→ 758,234 (1분에 75만원!)

# 9. 재고가 50개 이하인 상품
inventory_stock &lt; 50
→ inventory_stock{product=&quot;tablet&quot;} 23</code></pre>
<hr>
<h2 id="6-grafana-custom-dashboard-생성">6. Grafana Custom Dashboard 생성</h2>
<h3 id="실습-12-configmap으로-대시보드-자동-프로비저닝">실습 12: ConfigMap으로 대시보드 자동 프로비저닝</h3>
<p><strong>🤔 왜 ConfigMap을 쓰는가?</strong></p>
<ul>
<li>Grafana UI에서 수동으로 만들면 재배포 시 사라짐</li>
<li><strong>Infrastructure as Code</strong>: YAML로 관리</li>
<li>GitOps 친화적</li>
</ul>
<p><strong>파일</strong>: <code>/tmp/custom-metrics-dashboard.yaml</code></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-metrics-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: &quot;1&quot;  # ← 이 레이블이 핵심!
data:
  custom-metrics-dashboard.json: |
    {
      &quot;title&quot;: &quot;Custom Business Metrics&quot;,
      &quot;tags&quot;: [&quot;custom&quot;, &quot;business&quot;],
      &quot;refresh&quot;: &quot;10s&quot;,
      &quot;panels&quot;: [
        {
          &quot;id&quot;: 1,
          &quot;title&quot;: &quot;총 주문 수 (성공/실패)&quot;,
          &quot;type&quot;: &quot;graph&quot;,
          &quot;gridPos&quot;: {&quot;x&quot;: 0, &quot;y&quot;: 0, &quot;w&quot;: 12, &quot;h&quot;: 8},
          &quot;targets&quot;: [
            {
              &quot;expr&quot;: &quot;rate(orders_total{status=\&quot;success\&quot;}[5m]) * 60&quot;,
              &quot;legendFormat&quot;: &quot;성공 (주문/분)&quot;,
              &quot;refId&quot;: &quot;A&quot;
            },
            {
              &quot;expr&quot;: &quot;rate(orders_total{status=\&quot;failed\&quot;}[5m]) * 60&quot;,
              &quot;legendFormat&quot;: &quot;실패 (주문/분)&quot;,
              &quot;refId&quot;: &quot;B&quot;
            }
          ],
          &quot;yaxes&quot;: [
            {&quot;format&quot;: &quot;short&quot;, &quot;label&quot;: &quot;주문/분&quot;},
            {&quot;format&quot;: &quot;short&quot;}
          ]
        },
        {
          &quot;id&quot;: 2,
          &quot;title&quot;: &quot;총 결제 금액 (원)&quot;,
          &quot;type&quot;: &quot;stat&quot;,
          &quot;gridPos&quot;: {&quot;x&quot;: 12, &quot;y&quot;: 0, &quot;w&quot;: 6, &quot;h&quot;: 4},
          &quot;targets&quot;: [
            {
              &quot;expr&quot;: &quot;sum(payment_amount_total)&quot;,
              &quot;refId&quot;: &quot;A&quot;
            }
          ],
          &quot;options&quot;: {
            &quot;graphMode&quot;: &quot;area&quot;,
            &quot;colorMode&quot;: &quot;value&quot;
          },
          &quot;fieldConfig&quot;: {
            &quot;defaults&quot;: {
              &quot;unit&quot;: &quot;currencyKRW&quot;,
              &quot;decimals&quot;: 0
            }
          }
        },
        {
          &quot;id&quot;: 3,
          &quot;title&quot;: &quot;현재 활성 사용자&quot;,
          &quot;type&quot;: &quot;gauge&quot;,
          &quot;gridPos&quot;: {&quot;x&quot;: 18, &quot;y&quot;: 0, &quot;w&quot;: 6, &quot;h&quot;: 4},
          &quot;targets&quot;: [
            {
              &quot;expr&quot;: &quot;avg(active_users)&quot;,
              &quot;refId&quot;: &quot;A&quot;
            }
          ],
          &quot;options&quot;: {
            &quot;showThresholdLabels&quot;: false,
            &quot;showThresholdMarkers&quot;: true
          },
          &quot;fieldConfig&quot;: {
            &quot;defaults&quot;: {
              &quot;min&quot;: 0,
              &quot;max&quot;: 300,
              &quot;thresholds&quot;: {
                &quot;steps&quot;: [
                  {&quot;value&quot;: 0, &quot;color&quot;: &quot;green&quot;},
                  {&quot;value&quot;: 150, &quot;color&quot;: &quot;yellow&quot;},
                  {&quot;value&quot;: 200, &quot;color&quot;: &quot;red&quot;}
                ]
              }
            }
          }
        },
        {
          &quot;id&quot;: 7,
          &quot;title&quot;: &quot;주문 성공률&quot;,
          &quot;type&quot;: &quot;stat&quot;,
          &quot;gridPos&quot;: {&quot;x&quot;: 0, &quot;y&quot;: 16, &quot;w&quot;: 8, &quot;h&quot;: 4},
          &quot;targets&quot;: [
            {
              &quot;expr&quot;: &quot;sum(rate(orders_total{status=\&quot;success\&quot;}[5m])) / sum(rate(orders_total[5m])) * 100&quot;,
              &quot;refId&quot;: &quot;A&quot;
            }
          ],
          &quot;options&quot;: {
            &quot;graphMode&quot;: &quot;none&quot;,
            &quot;colorMode&quot;: &quot;value&quot;
          },
          &quot;fieldConfig&quot;: {
            &quot;defaults&quot;: {
              &quot;unit&quot;: &quot;percent&quot;,
              &quot;decimals&quot;: 1,
              &quot;thresholds&quot;: {
                &quot;steps&quot;: [
                  {&quot;value&quot;: 0, &quot;color&quot;: &quot;red&quot;},
                  {&quot;value&quot;: 90, &quot;color&quot;: &quot;yellow&quot;},
                  {&quot;value&quot;: 95, &quot;color&quot;: &quot;green&quot;}
                ]
              }
            }
          }
        }
      ]
    }</code></pre>
<p><strong>배포:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f /tmp/custom-metrics-dashboard.yaml
configmap/custom-metrics-dashboard created</code></pre>
<p><strong>😱 문제 발생 (또!)</strong></p>
<pre><code class="language-bash">$ kubectl logs -n monitoring deployment/kube-prometheus-stack-grafana -c grafana
logger=provisioning.dashboard error=&quot;Dashboard title cannot be empty&quot;</code></pre>
<p><strong>Grafana UI에 대시보드가 안 나타남!</strong></p>
<h3 id="🔧-문제-해결-2-dashboard-title-cannot-be-empty">🔧 문제 해결 2: Dashboard title cannot be empty</h3>
<p><strong>원인 파악:</strong></p>
<p>처음에 이렇게 작성했었음:</p>
<pre><code class="language-json">{
  &quot;dashboard&quot;: {  // ← 이 래퍼가 문제!
    &quot;title&quot;: &quot;Custom Business Metrics&quot;,
    &quot;panels&quot;: [...]
  },
  &quot;overwrite&quot;: true
}</code></pre>
<p>Grafana는 최상위에 <code>title</code> 필드를 기대하는데, <code>dashboard</code> 래퍼로 감싸져 있어서 못 찾음!</p>
<p><strong>올바른 구조:</strong></p>
<pre><code class="language-json">{
  &quot;title&quot;: &quot;Custom Business Metrics&quot;,  // ← 최상위!
  &quot;tags&quot;: [&quot;custom&quot;, &quot;business&quot;],
  &quot;panels&quot;: [...]
}</code></pre>
<p><strong>수정 및 재배포:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f /tmp/custom-metrics-dashboard.yaml
configmap/custom-metrics-dashboard configured</code></pre>
<p><strong>Sidecar 컨테이너 로그 확인 (ConfigMap 감지):</strong></p>
<pre><code class="language-bash">$ kubectl logs -n monitoring deployment/kube-prometheus-stack-grafana -c grafana-sc-dashboard
INFO  Received File list: /tmp/dashboards
INFO  Retrieving ConfigMap custom-metrics-dashboard in namespace monitoring
INFO  Found 1 dashboard(s) in ConfigMap custom-metrics-dashboard
INFO  Writing dashboard custom-metrics-dashboard.json to /tmp/dashboards/custom-metrics-dashboard.json</code></pre>
<p><strong>Grafana 메인 컨테이너 로그 확인 (대시보드 로드):</strong></p>
<pre><code class="language-bash">$ kubectl logs -n monitoring deployment/kube-prometheus-stack-grafana -c grafana | tail -10
logger=provisioning.dashboard level=info msg=&quot;starting to provision dashboards&quot;
logger=provisioning.dashboard level=info msg=&quot;finished to provision dashboards&quot;
logger=dashboard.provisioning level=info msg=&quot;Inserted dashboard&quot; title=&quot;Custom Business Metrics&quot; id=42 path=/tmp/dashboards/custom-metrics-dashboard.json</code></pre>
<p><strong>ConfigMap이 파일로 생성되었는지 확인:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring deployment/kube-prometheus-stack-grafana -c grafana -- \
  ls -lh /tmp/dashboards/

total 4.0K
-rw-r--r-- 1 grafana grafana 3.2K Nov  4 15:12 custom-metrics-dashboard.json</code></pre>
<p><strong>대시보드 개수 확인:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n monitoring deployment/kube-prometheus-stack-grafana -c grafana -- \
  ls /tmp/dashboards/*.json | wc -l
1</code></pre>
<p><strong>에러 없음! ✅</strong></p>
<h3 id="실습-13-grafana에서-대시보드-확인">실습 13: Grafana에서 대시보드 확인</h3>
<p><strong>Grafana UI → Dashboards → Browse → &quot;Custom Business Metrics&quot;</strong></p>
<p><strong>[이미지 7: Grafana Custom Metrics Dashboard 전체 화면 스크린샷]</strong>
<img src="https://velog.velcdn.com/images/arnold_99/post/55a93de0-e696-4750-bcef-8c2435a844d7/image.png" alt=""></p>
<hr>
<h2 id="7-전체-아키텍처-정리">7. 전체 아키텍처 정리</h2>
<p><strong>🤔 내가 이해한 전체 그림:</strong></p>
<pre><code>┌─────────────────────────────────────────────────────────┐
│                    모니터링 스택                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Custom Metrics App (2 Pods)                           │
│    ├─ Flask API (port 5000)                            │
│    └─ Prometheus Metrics (port 8000) /metrics          │
│              ↓                                          │
│  ServiceMonitor (CRD)                                   │
│    └─ Label Selector로 Service 발견                    │
│              ↓                                          │
│  Prometheus (Pull 방식)                                │
│    ├─ 15초마다 메트릭 수집                             │
│    ├─ TSDB에 저장                                      │
│    └─ PromQL로 쿼리                                    │
│              ↓                                          │
│  Grafana                                                │
│    ├─ Prometheus를 Data Source로 연결                 │
│    ├─ ConfigMap으로 Dashboard 자동 로드               │
│    └─ 웹 UI로 시각화                                   │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    로깅 스택                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  모든 Pod (모든 Namespace)                             │
│    └─ stdout/stderr 로그                               │
│              ↓                                          │
│  Kubelet (/var/log/pods/)                              │
│              ↓                                          │
│  Vector (DaemonSet - 4개 노드)                         │
│    ├─ 로그 수집 및 파싱                                │
│    ├─ JSON 변환                                        │
│    └─ Bulk API로 전송                                  │
│              ↓                                          │
│  OpenSearch (단일 노드)                                │
│    ├─ logs-YYYY-MM-DD 인덱스                           │
│    ├─ status: yellow (복제본 없음)                    │
│    └─ Production에서는 3중화 권장                      │
│              ↓                                          │
│  OpenSearch Dashboards                                  │
│    ├─ Index Pattern: logs-*                            │
│    ├─ KQL로 로그 검색                                  │
│    └─ 웹 UI로 시각화                                   │
└─────────────────────────────────────────────────────────┘</code></pre><hr>
<h2 id="8-배운-점과-느낀-점">8. 배운 점과 느낀 점</h2>
<h3 id="기술적-인사이트">기술적 인사이트</h3>
<p><strong>1. ServiceMonitor의 강력함</strong></p>
<p><strong>🤔 내가 깨달은 것:</strong></p>
<ul>
<li>CRD 기반 자동화가 진짜 Kubernetes다움</li>
<li>Label Selector 하나로 동적 스케일링 대응</li>
<li>Operator Pattern의 실전 예시</li>
</ul>
<p><strong>2. Sidecar 패턴의 우아함</strong></p>
<p>Grafana가 ConfigMap을 감지하는 방식:</p>
<pre><code>ConfigMap (label: grafana_dashboard=&quot;1&quot;)
    ↓
Sidecar Container (감시)
    ↓
/tmp/dashboards/에 파일 생성
    ↓
Grafana Main Container (자동 로드)</code></pre><p><strong>관심사 분리</strong>가 완벽!</p>
<p><strong>3. Vector의 효율성</strong></p>
<p>Rust로 작성되어:</p>
<ul>
<li>Fluent Bit 대비 <strong>메모리 사용량 1/3</strong></li>
<li>CPU 사용량도 낮음</li>
<li>복잡한 변환 로직 가능</li>
</ul>
<p><strong>4. OpenSearch 3중화의 중요성</strong></p>
<pre><code class="language-bash"># 클러스터 헬스
&quot;status&quot;: &quot;green&quot;  # 3개 노드 모두 정상
&quot;active_shards&quot;: 15  # 각 샤드가 3개씩 복제

# 노드 1개 죽어도:
&quot;status&quot;: &quot;yellow&quot;  # 검색은 계속 가능
# 노드 2개 죽어야:
&quot;status&quot;: &quot;red&quot;  # 일부 데이터 손실</code></pre>
<p><strong>고가용성 확보!</strong></p>
<h3 id="실전-팁">실전 팁</h3>
<p><strong>1. Pod 문제 디버깅 순서</strong></p>
<pre><code class="language-bash">kubectl get pods  # 상태 확인
kubectl describe pod &lt;pod&gt;  # 이벤트 확인
kubectl logs &lt;pod&gt;  # 로그 확인
kubectl logs &lt;pod&gt; --previous  # 이전 실행 로그 (CrashLoopBackOff 시)</code></pre>
<p><strong>2. JSON 구조 검증</strong></p>
<pre><code class="language-bash"># jq로 구조 확인
kubectl get cm &lt;name&gt; -o json | jq &#39;.data | keys&#39;

# 다른 정상 리소스와 비교
kubectl get cm &lt;working-dashboard&gt; -o json | jq &#39;.data.&quot;xxx.json&quot; | fromjson | keys&#39;</code></pre>
<p><strong>3. 메트릭 설계 원칙</strong></p>
<ul>
<li><strong>Counter</strong>: 절대 감소하지 않는 값 (<code>rate()</code> 함수 사용)</li>
<li><strong>Gauge</strong>: 현재 상태를 나타내는 값</li>
<li><strong>Histogram</strong>: 분포를 측정하려면 (bucket 설정 중요)</li>
<li><strong>Label cardinality</strong>: Label 값이 너무 많으면 성능 저하!</li>
</ul>
<p><strong>4. ConfigMap 변경 후 확인</strong></p>
<pre><code class="language-bash"># ConfigMap 업데이트
kubectl apply -f dashboard.yaml

# Sidecar 로그 확인 (파일 생성 확인)
kubectl logs -n monitoring deploy/grafana -c grafana-sc-dashboard

# Grafana 로그 확인 (로드 성공 확인)
kubectl logs -n monitoring deploy/grafana -c grafana</code></pre>
<h3 id="실수했던-것들">실수했던 것들</h3>
<p><strong>1. /app 디렉토리 없음</strong></p>
<ul>
<li>Python 이미지에 디렉토리가 없을 수 있음</li>
<li><code>mkdir -p</code>로 먼저 생성!</li>
</ul>
<p><strong>2. JSON 구조 잘못</strong></p>
<ul>
<li>Grafana는 <code>&quot;dashboard&quot;: {}</code> 래퍼를 싫어함</li>
<li>최상위에 <code>&quot;title&quot;</code> 필드 필수!</li>
</ul>
<p><strong>3. Label 오타</strong></p>
<ul>
<li><code>grafana_dashboard: &quot;1&quot;</code> ← 정확히 이렇게!</li>
<li>대소문자, 언더스코어 주의</li>
</ul>
<hr>
<h2 id="정리-및-다음-단계">정리 및 다음 단계</h2>
<h3 id="오늘-완성한-것">오늘 완성한 것</h3>
<p>✅ <strong>모니터링 스택</strong></p>
<ul>
<li>Prometheus + Grafana로 메트릭 수집 및 시각화</li>
<li>Custom Metrics 애플리케이션 구현</li>
<li>ServiceMonitor로 자동 타겟 디스커버리</li>
<li>ConfigMap으로 대시보드 자동 프로비저닝</li>
</ul>
<p>✅ <strong>로깅 스택</strong></p>
<ul>
<li>Vector DaemonSet으로 전체 노드 로그 수집 (4개 노드)</li>
<li>OpenSearch 단일 노드 구축 (3중화는 개념만 학습)</li>
<li>OpenSearch Dashboards로 로그 검색 및 시각화</li>
<li>KQL로 복잡한 로그 쿼리 (8가지 패턴)</li>
</ul>
<h3 id="최종-검증-체크리스트">최종 검증 체크리스트</h3>
<ul>
<li><input checked="" disabled="" type="checkbox"> Prometheus UI 접속 가능 (<a href="http://172.30.1.43:30090">http://172.30.1.43:30090</a>)</li>
<li><input checked="" disabled="" type="checkbox"> Prometheus Targets 모두 UP 상태</li>
<li><input checked="" disabled="" type="checkbox"> Grafana 접속 및 로그인 성공 (admin/prom-operator)</li>
<li><input checked="" disabled="" type="checkbox"> Grafana 기본 대시보드 정상 표시</li>
<li><input checked="" disabled="" type="checkbox"> OpenSearch 1개 Pod Running (단일 노드, status: yellow)</li>
<li><input checked="" disabled="" type="checkbox"> OpenSearch Dashboards 접속 가능</li>
<li><input checked="" disabled="" type="checkbox"> OpenSearch에서 로그 검색 가능 (<code>logs-*</code> 인덱스)</li>
<li><input checked="" disabled="" type="checkbox"> Vector DaemonSet 4개 Pod Running (각 노드당 1개)</li>
<li><input checked="" disabled="" type="checkbox"> Custom Metrics App 2개 Pod Running</li>
<li><input checked="" disabled="" type="checkbox"> Prometheus에서 Custom Metrics 수집 확인 (2개 Target UP)</li>
<li><input checked="" disabled="" type="checkbox"> Grafana Custom Dashboard 정상 표시</li>
</ul>
<h3 id="day-7-예고-ceph-분산-스토리지">Day 7 예고: Ceph 분산 스토리지</h3>
<p><strong>다음에 배울 것:</strong></p>
<ul>
<li>Rook Operator로 Ceph 클러스터 구축</li>
<li>RBD (Block Storage) 사용</li>
<li>CephFS (Shared File System) 구성</li>
<li>Object Storage (S3 호환) 설정</li>
<li>스토리지 클래스와 PVC 동적 프로비저닝</li>
</ul>
<p><strong>왜 Ceph를 배우는가?</strong></p>
<ul>
<li>StatefulSet에 영구 스토리지 필요</li>
<li>hostPath는 노드 종속적 (HA 불가능)</li>
<li><strong>Ceph = Kubernetes 네이티브 분산 스토리지</strong></li>
<li>Block/File/Object 모두 지원</li>
</ul>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://prometheus.io/docs/">Prometheus 공식 문서</a></li>
<li><a href="https://github.com/prometheus-community/helm-charts">kube-prometheus-stack Helm Chart</a></li>
<li><a href="https://opensearch.org/docs/">OpenSearch 문서</a></li>
<li><a href="https://vector.dev/docs/">Vector 문서</a></li>
<li><a href="https://grafana.com/docs/grafana/latest/best-practices/">Grafana Dashboard Best Practices</a></li>
<li><a href="https://promlabs.com/promql-cheat-sheet/">PromQL Cheat Sheet</a></li>
</ul>
<hr>
<p><strong>Day 6 완료! 🎉</strong></p>
<p>이제 클러스터를 완벽하게 <strong>관찰(Observe)</strong> 할 수 있습니다:</p>
<ul>
<li><strong>Metrics</strong> (Prometheus + Grafana)</li>
<li><strong>Logs</strong> (Vector + OpenSearch)</li>
<li><strong>Custom Business Metrics</strong> (주문, 결제, 재고 등)</li>
</ul>
<p>다음은 <strong>Storage</strong> 정복! 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: Production-Ready 운영 완벽 정복 (Day 5)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Production-Ready-%EC%9A%B4%EC%98%81-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%B3%B5-Day-5</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Production-Ready-%EC%9A%B4%EC%98%81-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%B3%B5-Day-5</guid>
            <pubDate>Mon, 03 Nov 2025 13:24:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2025년 11월 3일
Job/CronJob, Network Policy, Resource Quotas, CRD, Helm Chart, etcd Backup, Cluster Upgrade까지!</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 4에서 Ingress, HPA, RBAC, StatefulSet, DaemonSet 등 고급 패턴을 마스터했습니다. 이제 Day 5에서는 <strong>Production 환경에서 클러스터를 안전하고 효율적으로 운영하는 방법</strong>을 학습했습니다.</p>
<p><strong>오늘 배운 것:</strong></p>
<ol>
<li>Job &amp; CronJob으로 배치 작업 자동화</li>
<li>Network Policy로 Zero Trust 네트워크 구현</li>
<li>Resource Quotas로 팀별 리소스 관리</li>
<li>CRD와 Operator Pattern 이해</li>
<li>Helm Chart로 애플리케이션 패키징 (Terraform + Terragrunt 패턴!)</li>
<li>etcd 백업 주기와 실무 전략</li>
<li>Cluster 업그레이드 절차와 Best Practices</li>
</ol>
<hr>
<h2 id="1-job--cronjob-배치-작업-관리">1. Job &amp; CronJob: 배치 작업 관리</h2>
<h3 id="deployment-vs-job">Deployment vs Job</h3>
<p><strong>🤔 내가 이해한 것:</strong></p>
<ul>
<li>Deployment: 항상 실행되어야 하는 워크로드 (웹 서버, API)</li>
<li>Job: 한 번 실행 후 종료 (데이터 마이그레이션, 백업, 계산)</li>
</ul>
<h3 id="실습-1-간단한-job-π-계산">실습 1: 간단한 Job (π 계산)</h3>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: Job
metadata:
  name: pi-calculation
spec:
  template:
    spec:
      containers:
      - name: pi
        image: perl:5.34
        command: [&quot;perl&quot;, &quot;-Mbignum=bpi&quot;, &quot;-wle&quot;, &quot;print bpi(2000)&quot;]
      restartPolicy: Never
  backoffLimit: 4</code></pre>
<p><strong>실행 결과:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f job-pi.yaml
job.batch/pi-calculation created

$ kubectl get jobs
NAME              COMPLETIONS   DURATION   AGE
pi-calculation    1/1           8s         45s

$ kubectl get pods
NAME                    READY   STATUS      RESTARTS   AGE
pi-calculation-abc123   0/1     Completed   0          50s

$ kubectl logs pi-calculation-abc123 | head -3
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127145263560827785771342757789609173637178721468440901224953430146549585371050792279689258923542019956112129021960864034418159813629774771309960518707211349999998372978049951059731732816096318595024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303598253490428755468731159562863882353787593751957781857780532171226806613001927876611195909216420199</code></pre>
<p>2000자리 π 값 출력 성공!</p>
<h3 id="실습-2-parallel-job-병렬-처리">실습 2: Parallel Job (병렬 처리)</h3>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: Job
metadata:
  name: parallel-job
spec:
  completions: 10      # 총 10번 성공해야 함
  parallelism: 3       # 동시에 3개씩 실행
  template:
    spec:
      containers:
      - name: worker
        image: busybox:1.28
        command: [&quot;/bin/sh&quot;, &quot;-c&quot;, &quot;echo &#39;Processing task&#39; &amp;&amp; sleep 5 &amp;&amp; echo &#39;Task completed&#39;&quot;]
      restartPolicy: Never</code></pre>
<p><strong>실시간 관찰 (2초마다):</strong></p>
<pre><code class="language-bash"># t=0초 - 3개 동시 시작!
$ kubectl get job parallel-job &amp;&amp; kubectl get pods -l job-name=parallel-job
NAME           COMPLETIONS   DURATION   AGE
parallel-job   0/10          3s         3s

NAME                   READY   STATUS    RESTARTS   AGE
parallel-job-abc12     1/1     Running   0          3s
parallel-job-def34     1/1     Running   0          3s
parallel-job-ghi56     1/1     Running   0          3s

# t=8초 - 첫 3개 완료, 다음 3개 시작!
$ kubectl get job parallel-job &amp;&amp; kubectl get pods -l job-name=parallel-job
NAME           COMPLETIONS   DURATION   AGE
parallel-job   3/10          11s        11s

NAME                   READY   STATUS      RESTARTS   AGE
parallel-job-abc12     0/1     Completed   0          11s
parallel-job-def34     0/1     Completed   0          11s
parallel-job-ghi56     0/1     Completed   0          11s
parallel-job-jkl78     1/1     Running     0          3s
parallel-job-mno90     1/1     Running     0          3s
parallel-job-pqr12     1/1     Running     0          3s

# t=38초 - 모두 완료!
$ kubectl get job parallel-job
NAME           COMPLETIONS   DURATION   AGE
parallel-job   10/10         38s        38s</code></pre>
<p><strong>결과:</strong> 10개 작업을 3개씩 동시 실행하여 <strong>38초 만에</strong> 완료! (순차 실행 시 50초 소요)</p>
<h3 id="실습-3-cronjob-주기적-백업">실습 3: CronJob (주기적 백업)</h3>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup-job
spec:
  schedule: &quot;*/1 * * * *&quot;  # 매 1분마다 (테스트용)
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: busybox:1.28
            command:
            - /bin/sh
            - -c
            - |
              echo &quot;[$(date)] Starting backup...&quot;
              echo &quot;Backing up data...&quot;
              sleep 3
              echo &quot;[$(date)] Backup completed!&quot;
          restartPolicy: OnFailure
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1</code></pre>
<p><strong>3분 후 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get cronjob
NAME         SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
backup-job   */1 * * * *   False     0        45s             3m

$ kubectl get jobs -l job-name=backup-job
NAME                    COMPLETIONS   DURATION   AGE
backup-job-29369409     1/1           5s         3m
backup-job-29369410     1/1           5s         2m
backup-job-29369411     1/1           5s         1m

$ kubectl logs backup-job-29369411-abc12
[Wed Nov 3 05:42:00 UTC 2025] Starting backup...
Backing up data...
[Wed Nov 3 05:42:03 UTC 2025] Backup completed!</code></pre>
<p><strong>자동으로 매 1분마다 Job 생성 및 실행!</strong></p>
<hr>
<h2 id="2-network-policy-zero-trust-네트워크">2. Network Policy: Zero Trust 네트워크</h2>
<h3 id="🤔-내-질문-network-policy는-운영환경에서-모든-리소스마다-걸어두는-편인가">🤔 내 질문: &quot;Network Policy는 운영환경에서 모든 리소스마다 걸어두는 편인가?&quot;</h3>
<p><strong>답변:</strong> 아니요, 일반적으로 <strong>20-30%의 중요 리소스</strong>에만 적용합니다:</p>
<ul>
<li>데이터베이스 (외부 접근 차단)</li>
<li>결제 서비스 (PCI-DSS 규정)</li>
<li>인증 서버</li>
<li>민감 정보 처리 Pod</li>
</ul>
<h3 id="🤔-추가-질문-결제서비스이면-zero-trust를-해야겠네">🤔 추가 질문: &quot;결제서비스이면 Zero Trust를 해야겠네?&quot;</h3>
<p><strong>답변:</strong> 네, <strong>반드시</strong> Zero Trust를 적용해야 합니다!</p>
<ul>
<li>PCI-DSS 규정 준수 필수</li>
<li>Default Deny → Explicit Allow</li>
<li>모든 통신 경로 명시적 허용</li>
</ul>
<h3 id="실습-database-격리-backend만-접근-가능">실습: Database 격리 (Backend만 접근 가능)</h3>
<p><strong>시나리오:</strong></p>
<ul>
<li>Frontend → Database ❌ (차단)</li>
<li>Backend → Database ✅ (허용)</li>
<li>Test Pod → Database ❌ (차단)</li>
</ul>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 5432</code></pre>
<p><strong>테스트 결과:</strong></p>
<pre><code class="language-bash"># Test Pod에서 접근 시도 (차단되어야 함)
$ kubectl exec -n production test-pod -- nc -zv database 5432
nc: database (10.244.5.225): Operation timed out  ❌ 차단 성공!

# Backend Pod에서 접근 시도 (허용되어야 함)
$ kubectl exec -n production backend -- nc -zv database 5432
database (10.244.5.225:5432) open  ✅ 허용 성공!</code></pre>
<p><strong>Network Policy가 정확히 작동!</strong></p>
<hr>
<h2 id="3-resource-quotas--limitrange">3. Resource Quotas &amp; LimitRange</h2>
<h3 id="resourcequota-namespace-전체-리소스-제한">ResourceQuota: Namespace 전체 리소스 제한</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: &quot;4&quot;
    requests.memory: 8Gi
    limits.cpu: &quot;8&quot;
    limits.memory: 16Gi
    pods: &quot;10&quot;
    services: &quot;5&quot;</code></pre>
<h3 id="limitrange-pod별-기본값-및-제한">LimitRange: Pod별 기본값 및 제한</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: LimitRange
metadata:
  name: dev-limits
  namespace: dev
spec:
  limits:
  - max:
      cpu: &quot;2&quot;
      memory: 4Gi
    min:
      cpu: 100m
      memory: 128Mi
    default:
      cpu: 500m       # 기본 limit
      memory: 1Gi
    defaultRequest:
      cpu: 200m       # 기본 request
      memory: 512Mi
    type: Container</code></pre>
<h3 id="실습-할당량-초과-테스트">실습: 할당량 초과 테스트</h3>
<p><strong>1. LimitRange 위반 테스트:</strong></p>
<pre><code class="language-bash">$ kubectl run test-large -n dev --image=nginx \
  --limits=cpu=6  # max는 2 CPU인데 6 요청

Error from server (Forbidden): pods &quot;test-large&quot; is forbidden:
maximum cpu usage per Container is 2, but limit is 6</code></pre>
<p>✅ <strong>LimitRange가 먼저 차단!</strong></p>
<p><strong>2. ResourceQuota 위반 테스트:</strong></p>
<pre><code class="language-bash"># 이미 dev namespace에 CPU request 2.2 core 사용 중

$ kubectl run test2 -n dev --image=nginx \
  --requests=cpu=2  # 총 4.2 core가 되어 quota(4) 초과

Error from server (Forbidden): pods &quot;test2&quot; is forbidden:
exceeded quota: dev-quota, requested: requests.cpu=2,
used: requests.cpu=2200m, limited: requests.cpu=4</code></pre>
<p>✅ <strong>ResourceQuota가 차단!</strong></p>
<h3 id="기본값-자동-적용-확인">기본값 자동 적용 확인</h3>
<pre><code class="language-bash">$ kubectl run test-default -n dev --image=nginx

$ kubectl get pod test-default -n dev -o yaml | grep -A 10 resources:
    resources:
      limits:
        cpu: 500m        # ← LimitRange의 default 자동 적용!
        memory: 1Gi
      requests:
        cpu: 200m        # ← defaultRequest 자동 적용!
        memory: 512Mi</code></pre>
<p><strong>리소스를 명시하지 않아도 자동으로 설정됨!</strong></p>
<hr>
<h2 id="4-custom-resource-definition-crd">4. Custom Resource Definition (CRD)</h2>
<h3 id="🤔-내-질문-아무리-봐도-굳이-사용하는-이유를-아직은-모르겠다-컨테이너-이미지를-넣지도-않고-그냥-텍스트-장난으로-보이는데-왜-쓰는거지">🤔 내 질문: &quot;아무리 봐도 굳이 사용하는 이유를 아직은 모르겠다. 컨테이너 이미지를 넣지도 않고 그냥 텍스트 장난으로 보이는데? 왜 쓰는거지?&quot;</h3>
<p>이 질문이 가장 중요했습니다!</p>
<p><strong>답변:</strong> CRD <strong>단독으로는 아무것도 하지 않습니다</strong>. CRD는 단지 <strong>데이터 구조 정의</strong>일 뿐입니다.</p>
<p><strong>진짜 힘은 Operator Pattern:</strong></p>
<pre><code>CRD (데이터 구조) + Operator (Controller) = 자동화!</code></pre><h3 id="실제-사례-deployment-controller-built-in-operator">실제 사례: Deployment Controller (Built-in Operator)</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment  # ← 이것도 CRD입니다! (Built-in)
metadata:
  name: nginx
spec:
  replicas: 3</code></pre>
<p><strong>Deployment를 생성하면:</strong></p>
<ol>
<li>etcd에 Deployment 정보 저장 (CRD 역할)</li>
<li>Deployment Controller가 감지 (Operator 역할)</li>
<li>ReplicaSet 자동 생성</li>
<li>ReplicaSet Controller가 감지</li>
<li>Pod 3개 자동 생성</li>
<li><strong>Pod가 죽으면 자동으로 재생성!</strong> ← 이게 Operator의 힘!</li>
</ol>
<h3 id="실습-database-crd">실습: Database CRD</h3>
<pre><code class="language-yaml">apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.mycompany.com
spec:
  group: mycompany.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              engine:
                type: string
                enum: [&quot;postgres&quot;, &quot;mysql&quot;, &quot;mongodb&quot;]
              version:
                type: string
              storage:
                type: string
            required:
            - engine
            - version
  scope: Namespaced
  names:
    plural: databases
    singular: database
    kind: Database
    shortNames:
    - db</code></pre>
<p><strong>Database 생성:</strong></p>
<pre><code class="language-yaml">apiVersion: mycompany.com/v1
kind: Database
metadata:
  name: production-db
spec:
  engine: postgres
  version: &quot;15&quot;
  storage: 100Gi</code></pre>
<pre><code class="language-bash">$ kubectl apply -f database.yaml
database.mycompany.com/production-db created

$ kubectl get databases
NAME            AGE
production-db   10s
dev-db          5s

$ kubectl get db  # shortName 동작!
NAME            AGE
production-db   15s
dev-db          10s</code></pre>
<p><strong>스키마 검증 테스트 (enum 위반):</strong></p>
<pre><code class="language-bash">$ kubectl apply -f database-oracle.yaml  # engine: oracle

Error: Unsupported value: &quot;oracle&quot;: supported values: &quot;postgres&quot;, &quot;mysql&quot;, &quot;mongodb&quot;</code></pre>
<p>✅ <strong>OpenAPI 스키마 검증 작동!</strong></p>
<hr>
<h2 id="5-helm-chart-kubernetes의-terraform">5. Helm Chart: Kubernetes의 Terraform</h2>
<h3 id="🤔-내-질문-보통-공식으로-올라가있는-chart들은-valuesyaml을-받아서-그걸로-다시-install하잖아-마치-terraform-모듈화를-terragrunt-환경변수로-환경별로-실행하는걸-떠올리게-한다">🤔 내 질문: &quot;보통 공식으로 올라가있는 chart들은 values.yaml을 받아서 그걸로 다시 install하잖아? 마치 Terraform 모듈화를 terragrunt 환경변수로 환경별로 실행하는걸 떠올리게 한다&quot;</h3>
<p><strong>완벽한 이해입니다!</strong></p>
<table>
<thead>
<tr>
<th>Terraform</th>
<th>Helm</th>
</tr>
</thead>
<tbody><tr>
<td>Terraform 모듈</td>
<td>Helm Chart</td>
</tr>
<tr>
<td>tfvars</td>
<td>values.yaml</td>
</tr>
<tr>
<td>terragrunt.hcl</td>
<td>values-dev.yaml, values-prod.yaml</td>
</tr>
<tr>
<td>terraform apply</td>
<td>helm install</td>
</tr>
<tr>
<td>terraform plan</td>
<td>helm template</td>
</tr>
</tbody></table>
<h3 id="실습-1-간단한-chart-생성">실습 1: 간단한 Chart 생성</h3>
<pre><code class="language-bash">$ helm create my-webapp
Creating my-webapp

$ tree my-webapp/
my-webapp/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   └── _helpers.tpl
└── charts/</code></pre>
<p><strong>values.yaml 수정:</strong></p>
<pre><code class="language-yaml">replicaCount: 3

image:
  repository: nginx
  tag: &quot;1.21&quot;</code></pre>
<p><strong>설치:</strong></p>
<pre><code class="language-bash">$ helm install myapp ./my-webapp

$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME                         READY   STATUS    RESTARTS   AGE
my-webapp-6c8b4d9f7b-abc12   1/1     Running   0          30s
my-webapp-6c8b4d9f7b-def34   1/1     Running   0          30s
my-webapp-6c8b4d9f7b-ghi56   1/1     Running   0          30s</code></pre>
<p><strong>업그레이드 (replicas 변경):</strong></p>
<pre><code class="language-bash">$ helm upgrade myapp ./my-webapp --set replicaCount=5

$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME                         READY   STATUS    RESTARTS   AGE
my-webapp-6c8b4d9f7b-abc12   1/1     Running   0          2m
my-webapp-6c8b4d9f7b-def34   1/1     Running   0          2m
my-webapp-6c8b4d9f7b-ghi56   1/1     Running   0          2m
my-webapp-6c8b4d9f7b-jkl78   1/1     Running   0          5s
my-webapp-6c8b4d9f7b-mno90   1/1     Running   0          5s</code></pre>
<p><strong>롤백:</strong></p>
<pre><code class="language-bash">$ helm rollback myapp 1

$ kubectl get pods -l app.kubernetes.io/name=my-webapp
NAME                         READY   STATUS    RESTARTS   AGE
my-webapp-6c8b4d9f7b-abc12   1/1     Running   0          3m
my-webapp-6c8b4d9f7b-def34   1/1     Running   0          3m
my-webapp-6c8b4d9f7b-ghi56   1/1     Running   0          3m</code></pre>
<p>✅ <strong>다시 3개로 롤백!</strong></p>
<h3 id="실습-2-환경별-values-파일-terraform-패턴">실습 2: 환경별 Values 파일 (Terraform 패턴!)</h3>
<p><strong>values-dev.yaml (개발 환경):</strong></p>
<pre><code class="language-yaml">architecture: standalone  # 단일 인스턴스

auth:
  postgresPassword: &quot;dev-password-123&quot;
  username: &quot;myapp&quot;
  password: &quot;myapp-dev-123&quot;
  database: &quot;myapp_dev&quot;

primary:
  resources:
    requests:
      memory: &quot;256Mi&quot;
      cpu: &quot;250m&quot;
    limits:
      memory: &quot;512Mi&quot;
      cpu: &quot;500m&quot;

  persistence:
    enabled: true
    storageClass: &quot;local-path&quot;
    size: 5Gi

backup:
  enabled: false

metrics:
  enabled: false</code></pre>
<p><strong>values-prod.yaml (프로덕션 환경):</strong></p>
<pre><code class="language-yaml">architecture: replication  # HA 구성

auth:
  existingSecret: &quot;postgres-prod-secret&quot;

primary:
  resources:
    requests:
      memory: &quot;2Gi&quot;
      cpu: &quot;1000m&quot;
    limits:
      memory: &quot;4Gi&quot;
      cpu: &quot;2000m&quot;

  persistence:
    enabled: true
    storageClass: &quot;fast-ssd&quot;
    size: 100Gi

  podAntiAffinityPreset: hard

readReplicas:
  replicaCount: 2

  resources:
    requests:
      memory: &quot;2Gi&quot;
      cpu: &quot;1000m&quot;

backup:
  enabled: true
  cronjob:
    schedule: &quot;0 2 * * *&quot;

metrics:
  enabled: true
  serviceMonitor:
    enabled: true

pgpool:
  enabled: true
  numInitChildren: 32
  maxPool: 4</code></pre>
<p><strong>배포:</strong></p>
<pre><code class="language-bash"># 개발 환경
helm install postgres-dev bitnami/postgresql -f values-dev.yaml -n dev

# 프로덕션 환경
helm install postgres-prod bitnami/postgresql -f values-prod.yaml -n prod</code></pre>
<p><strong>결과:</strong></p>
<ul>
<li>개발: 단일 Pod, 5GB, 백업 없음</li>
<li>프로덕션: Primary 1개 + Replica 2개, 100GB, 자동 백업, 모니터링, Connection Pooler</li>
</ul>
<p><strong>완전히 Terraform + Terragrunt 패턴!</strong></p>
<hr>
<h2 id="6-etcd-backup--restore">6. etcd Backup &amp; Restore</h2>
<h3 id="🤔-내-질문-실제-운영환경들은-백업-주기는">🤔 내 질문: &quot;실제 운영환경들은 백업 주기는?&quot;</h3>
<h3 id="실제-운영환경-백업-주기">실제 운영환경 백업 주기</h3>
<table>
<thead>
<tr>
<th>환경</th>
<th>자동 백업 주기</th>
<th>RTO</th>
<th>RPO</th>
<th>보관 정책</th>
</tr>
</thead>
<tbody><tr>
<td><strong>대기업/금융</strong></td>
<td>매 1시간</td>
<td>1시간</td>
<td>1시간</td>
<td>7년 (규정 준수)</td>
</tr>
<tr>
<td><strong>중견기업</strong></td>
<td>매 6시간</td>
<td>4시간</td>
<td>6시간</td>
<td>3개월</td>
</tr>
<tr>
<td><strong>스타트업</strong></td>
<td>매일 1회</td>
<td>12시간</td>
<td>24시간</td>
<td>1개월</td>
</tr>
</tbody></table>
<p><strong>가장 일반적인 패턴 (중견기업 표준):</strong></p>
<pre><code>✅ 매 6시간: 자동 백업 (S3 Standard-IA) - 48시간 보관
✅ 매일 새벽 2시: 전체 백업 (S3 Glacier) - 7일 보관
✅ 매주 일요일: 주간 백업 - 4주 보관
✅ 매월 1일: 월간 백업 - 12개월 보관
✅ 배포 직전: 수동 백업 필수!</code></pre><h3 id="실습-etcd-백업">실습: etcd 백업</h3>
<pre><code class="language-bash">$ kubectl exec -n kube-system etcd-cpu1 -- sh -c &quot;ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /var/lib/etcd/backup.db&quot;

Snapshot saved at /var/lib/etcd/backup.db

$ kubectl exec -n kube-system etcd-cpu1 -- sh -c &quot;ETCDCTL_API=3 etcdctl \
  --write-out=table \
  snapshot status /var/lib/etcd/backup.db&quot;

+---------+----------+------------+------------+
|  HASH   | REVISION | TOTAL KEYS | TOTAL SIZE |
+---------+----------+------------+------------+
| ad8760b |  1030374 |       1760 |     7.6 MB |
+---------+----------+------------+------------+</code></pre>
<p><strong>백업 성공! 1760개 키, 7.6MB</strong></p>
<h3 id="etcd의-중요성">etcd의 중요성</h3>
<p><strong>etcd 손실 = 클러스터 전체 손실!</strong></p>
<p>etcd에 저장되는 데이터:</p>
<ul>
<li>모든 Pod, Deployment, Service 정보</li>
<li>ConfigMap, Secret</li>
<li>RBAC 권한 설정</li>
<li>Network Policy</li>
<li><strong>모든 Kubernetes 리소스</strong></li>
</ul>
<h3 id="production-best-practices">Production Best Practices</h3>
<ol>
<li><strong>자동 백업</strong>: CronJob으로 6시간 또는 일 단위</li>
<li><strong>원격 저장</strong>: S3, GCS 등 클라우드 저장소 필수</li>
<li><strong>암호화</strong>: 백업 파일 암호화 (Secrets 포함)</li>
<li><strong>3-2-1 Rule</strong>: 3개 사본, 2개 매체, 1개 오프사이트</li>
<li><strong>복구 테스트</strong>: 분기별 복구 훈련 (DR Drill)</li>
<li><strong>배포 전 백업</strong>: 주요 변경 전 반드시 수동 백업</li>
</ol>
<hr>
<h2 id="7-cluster-upgrade">7. Cluster Upgrade</h2>
<h3 id="현재-클러스터-상태">현재 클러스터 상태</h3>
<pre><code class="language-bash">$ kubectl version --short
Client Version: v1.31.13
Kustomize Version: v5.4.2
Server Version: v1.31.13

$ kubectl get nodes -o wide
NAME   STATUS   ROLES           AGE     VERSION
cpu1   Ready    control-plane   6d21h   v1.31.13
cpu2   Ready    &lt;none&gt;          5d16h   v1.31.13
gpu1   Ready    &lt;none&gt;          5d16h   v1.31.13

$ sudo kubeadm upgrade plan
[upgrade/versions] Cluster version: 1.31.13
[upgrade/versions] kubeadm version: v1.31.13
[upgrade/versions] Target version: v1.31.13
[upgrade/versions] Latest version in the v1.31 series: v1.31.13</code></pre>
<p><strong>이미 최신 버전이라 실제 업그레이드는 불가능!</strong> 대신 시뮬레이션과 이론 학습을 진행했습니다.</p>
<h3 id="업그레이드-규칙">업그레이드 규칙</h3>
<ol>
<li><p><strong>한 번에 한 마이너 버전씩</strong></p>
<pre><code>✅ 1.30 → 1.31 → 1.32 (순차)
❌ 1.30 → 1.32 (건너뛰기 불가)</code></pre></li>
<li><p><strong>업그레이드 순서</strong></p>
<pre><code>1) etcd 백업 (필수!)
2) Control Plane 업그레이드 (kubeadm)
3) Control Plane kubelet 업그레이드
4) Worker Node 순차 업그레이드 (Rolling)</code></pre></li>
<li><p><strong>다운타임</strong></p>
<ul>
<li>Control Plane: 1-2분 (API 서버 재시작)</li>
<li>Worker Node: 무중단 (Rolling 방식)</li>
<li>Pod: 계속 실행 (kubectl만 잠시 불가)</li>
</ul>
</li>
</ol>
<h3 id="업그레이드-시뮬레이션-실행">업그레이드 시뮬레이션 실행</h3>
<pre><code class="language-bash">$ ./upgrade-simulation.sh

======================================
Kubernetes Cluster Upgrade Simulation
======================================

[Step 1/10] 업그레이드 전 체크리스트

현재 클러스터 버전 확인:
Client Version: v1.31.13
Kustomize Version: v5.4.2

모든 노드 상태 확인:
NAME   STATUS   ROLES           AGE     VERSION
cpu1   Ready    control-plane   6d21h   v1.31.13
cpu2   Ready    &lt;none&gt;          5d16h   v1.31.13
gpu1   Ready    &lt;none&gt;          5d16h   v1.31.13

✅ Step 1 완료

[Step 2/10] etcd 백업

kubectl exec -n kube-system etcd-cpu1 -- sh -c &quot;ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /var/lib/etcd/pre-upgrade-backup.db&quot;

✅ Step 2 완료 (시뮬레이션)

[Step 3/10] 모든 리소스 백업

명령어: kubectl get all -A -o yaml &gt; /tmp/all-resources-backup.yaml
백업 완료: /tmp/all-resources-backup.yaml (396K)

✅ Step 3 완료

[Step 4/10] API Deprecation 확인

Deprecated API 사용: 3개
⚠️  업그레이드 전 Deprecated API 수정 필요!

[Step 5/10] Control Plane 업그레이드 (시뮬레이션)
[Step 6/10] Control Plane kubelet 업그레이드 (시뮬레이션)
[Step 7/10] Worker Node 1 (cpu2) 업그레이드 (시뮬레이션)
[Step 8/10] Worker Node 2 (gpu1) 업그레이드 (시뮬레이션)

[Step 9/10] 업그레이드 검증

노드 버전 확인:
NAME   STATUS   ROLES           AGE     VERSION
cpu1   Ready    control-plane   6d21h   v1.31.13
cpu2   Ready    &lt;none&gt;          5d16h   v1.31.13
gpu1   Ready    &lt;none&gt;          5d16h   v1.31.13

[Step 10/10] 최종 확인

테스트 워크로드 배포:
pod/upgrade-test created

NAME           READY   STATUS    RESTARTS   AGE   NODE
upgrade-test   1/1     Running   0          3s    gpu1

✅ Step 10 완료

======================================
업그레이드 시뮬레이션 완료!
======================================</code></pre>
<h3 id="api-deprecation-가장-중요">API Deprecation (가장 중요!)</h3>
<p>Kubernetes는 매 버전마다 API를 Deprecate 시킵니다.</p>
<p><strong>주요 Deprecation 히스토리:</strong></p>
<ul>
<li><strong>v1.22</strong>: Ingress (extensions/v1beta1 → networking.k8s.io/v1)</li>
<li><strong>v1.25</strong>: PodSecurityPolicy, PodDisruptionBudget (v1beta1 → v1)</li>
<li><strong>v1.26</strong>: HorizontalPodAutoscaler (v2beta2 → v2)</li>
<li><strong>v1.29</strong>: FlowSchema, PriorityLevelConfiguration (v1beta2 → v1)</li>
</ul>
<p><strong>확인 방법:</strong></p>
<pre><code class="language-bash">$ kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

apiserver_requested_deprecated_apis{group=&quot;&quot;,removed_release=&quot;&quot;,resource=&quot;componentstatuses&quot;,subresource=&quot;&quot;,version=&quot;v1&quot;} 1</code></pre>
<h3 id="production-업그레이드-전략">Production 업그레이드 전략</h3>
<p><strong>Blue-Green Cluster (대기업):</strong></p>
<pre><code>클러스터 2개 운영 → 트래픽 점진 전환 → 다운타임 Zero
장점: 빠른 롤백, 안전
단점: 2배 비용</code></pre><p><strong>Rolling Upgrade (중소기업):</strong></p>
<pre><code>노드 순차 업그레이드 → 1-2분 다운타임
장점: 추가 비용 없음
단점: Control Plane 업그레이드 시 짧은 중단</code></pre><h3 id="업그레이드-주기">업그레이드 주기</h3>
<table>
<thead>
<tr>
<th>환경</th>
<th>주기</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>프로덕션</td>
<td>6개월</td>
<td>안정성 우선 (최소 2개 패치 버전 대기)</td>
</tr>
<tr>
<td>스테이징</td>
<td>3개월</td>
<td>프로덕션 사전 검증</td>
</tr>
<tr>
<td>개발</td>
<td>즉시</td>
<td>최신 기능 테스트</td>
</tr>
</tbody></table>
<p><strong>Kubernetes 버전 지원 정책:</strong></p>
<pre><code>Kubernetes는 최근 3개 마이너 버전만 지원

현재: v1.32 (최신)
v1.32: 지원 ✅
v1.31: 지원 ✅
v1.30: 지원 ✅
v1.29: 지원 종료 ❌ (보안 패치 없음)</code></pre><p><strong>결론: 최소 1년에 1-2회 업그레이드 필수!</strong></p>
<hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="1-jobcronjob은-생각보다-강력하다">1. Job/CronJob은 생각보다 강력하다</h3>
<ul>
<li>Parallel Job으로 처리 속도 3배 향상</li>
<li>CronJob은 백업, 정리 작업에 필수</li>
<li>backoffLimit로 실패 재시도 자동화</li>
</ul>
<h3 id="2-network-policy는-선택적으로">2. Network Policy는 선택적으로</h3>
<ul>
<li>20-30% 리소스에만 적용 (DB, 결제, 인증)</li>
<li>Zero Trust는 결제 서비스 필수 (PCI-DSS)</li>
<li>과도한 적용은 운영 복잡도 증가</li>
</ul>
<h3 id="3-resource-quotas는-팀-관리의-핵심">3. Resource Quotas는 팀 관리의 핵심</h3>
<ul>
<li>Namespace별 리소스 할당</li>
<li>LimitRange로 기본값 자동 적용</li>
<li>비용 관리와 직결</li>
</ul>
<h3 id="4-crd는-operator와-함께">4. CRD는 Operator와 함께</h3>
<ul>
<li>CRD 단독으로는 의미 없음</li>
<li>Operator = CRD + Controller</li>
<li>Deployment도 사실 CRD + Operator!</li>
</ul>
<h3 id="5-helm은-kubernetes의-terraform">5. Helm은 Kubernetes의 Terraform</h3>
<ul>
<li>Chart = Terraform 모듈</li>
<li>values.yaml = tfvars</li>
<li>환경별 배포 = terragrunt 패턴</li>
<li>버전 관리, 롤백 강력</li>
</ul>
<h3 id="6-etcd-백업은-생명줄">6. etcd 백업은 생명줄</h3>
<ul>
<li>etcd 손실 = 클러스터 전체 손실</li>
<li>6시간 or 일 단위 자동 백업</li>
<li>배포 전 수동 백업 필수</li>
<li>3-2-1 Rule 준수</li>
</ul>
<h3 id="7-cluster-upgrade는-계획이-90">7. Cluster Upgrade는 계획이 90%</h3>
<ul>
<li>API Deprecation 확인 필수</li>
<li>etcd 백업 먼저</li>
<li>Control Plane → Worker 순서</li>
<li>테스트 환경 먼저 검증</li>
<li>1년 1-2회 업그레이드 필수</li>
</ul>
<hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="1-network-policy-테스트-실패">1. Network Policy 테스트 실패</h3>
<p><strong>문제:</strong> nginx 이미지에 <code>nc</code> (netcat) 명령어 없음</p>
<pre><code>error: exec: &quot;nc&quot;: executable file not found in $PATH</code></pre><p><strong>해결:</strong> busybox:1.28 이미지로 test Pod 생성</p>
<pre><code class="language-bash">kubectl run test-pod -n production --image=busybox:1.28 \
  --labels=&quot;app=test&quot; --command -- sleep 3600</code></pre>
<h3 id="2-resourcequota-vs-limitrange-순서">2. ResourceQuota vs LimitRange 순서</h3>
<p><strong>내 착각:</strong> ResourceQuota가 먼저 체크할 줄 알았음</p>
<p><strong>실제:</strong> LimitRange → ResourceQuota 순서</p>
<ul>
<li>LimitRange가 Pod 생성 시점에 먼저 검증</li>
<li>ResourceQuota는 Namespace 전체 누적 검증</li>
</ul>
<h3 id="3-crd-이해-부족">3. CRD 이해 부족</h3>
<p><strong>문제:</strong> &quot;CRD가 왜 필요한지 모르겠다&quot;</p>
<p><strong>해결:</strong> Operator Pattern 이해로 해결</p>
<ul>
<li>CRD = 데이터 구조</li>
<li>Operator = 자동화 로직</li>
<li>Deployment = Built-in CRD + Operator 예시</li>
</ul>
<hr>
<h2 id="다음-계획-day-6">다음 계획 (Day 6)</h2>
<p>Day 5에서 Production-Ready 운영을 마스터했습니다! Day 6에서는 Monitoring &amp; Logging으로 클러스터 가시성을 확보할 예정입니다:</p>
<ol>
<li><strong>Prometheus</strong> - 메트릭 수집 및 저장</li>
<li><strong>Grafana</strong> - 시각화 대시보드</li>
<li><strong>Prometheus Operator</strong> - CRD를 활용한 모니터링 자동화</li>
<li><strong>Alert Manager</strong> - 장애 알림 자동화</li>
<li><strong>Vector + OpenSearch Stack</strong> - 로그 수집 및 분석 (Fluentd 대비 10배 빠른 성능!)<ul>
<li><strong>Vector</strong>: Rust 기반 고성능 로그 수집기</li>
<li><strong>OpenSearch</strong>: 완전 오픈소스 검색 엔진 (Elasticsearch 포크)</li>
<li><strong>OpenSearch Dashboards</strong>: 로그 검색 및 시각화 (Kibana 포크)</li>
</ul>
</li>
<li><strong>Distributed Tracing</strong> - Jaeger로 요청 추적</li>
<li><strong>Custom Metrics</strong> - 애플리케이션 메트릭 노출</li>
</ol>
<p>Production 클러스터의 가시성을 완벽하게 확보합시다!</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/">Kubernetes Jobs Documentation</a></li>
<li><a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Network Policies</a></li>
<li><a href="https://kubernetes.io/docs/concepts/policy/resource-quotas/">Resource Quotas</a></li>
<li><a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">Custom Resources</a></li>
<li><a href="https://helm.sh/docs/">Helm Documentation</a></li>
<li><a href="https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/#backing-up-an-etcd-cluster">Backing up etcd</a></li>
<li><a href="https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/">Upgrading kubeadm clusters</a></li>
</ul>
<h2 id="클러스터-환경">클러스터 환경</h2>
<p><strong>노드 구성:</strong></p>
<ul>
<li>cpu1 (172.30.1.43): Master + Worker (12 core, 7.5GB RAM)</li>
<li>cpu2 (172.30.1.80): Worker (8 core, 16GB RAM)</li>
<li>gpu1 (172.30.1.38): Worker (12 core, 16GB RAM)</li>
</ul>
<p><strong>버전:</strong></p>
<ul>
<li>Kubernetes: v1.31.13</li>
<li>CNI: Calico (VXLAN CrossSubnet)</li>
<li>Helm: v3.19.0</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: Advanced 패턴 마스터하기 (Day 4)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Advanced-%ED%8C%A8%ED%84%B4-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0-Day-4</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-Advanced-%ED%8C%A8%ED%84%B4-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0-Day-4</guid>
            <pubDate>Sun, 02 Nov 2025 15:21:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2025년 11월 3일
Ingress, HPA, RBAC, StatefulSet, DaemonSet, Monitoring까지 완벽 정복!</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 3에서 Secret, Rolling Update, PV/PVC, Resource Limits, Health Check 등 운영에 필수적인 기능들을 배웠습니다. 이제 Day 4에서는 Production 환경에서 꼭 필요한 고급 패턴들을 학습했습니다.</p>
<p><strong>오늘 배운 것:</strong></p>
<ol>
<li>Ingress와 HTTP 라우팅 (도메인 기반 TLS 종료)</li>
<li>HPA로 실시간 자동 스케일링 (1 Pod → 7 Pods 실제 관찰!)</li>
<li>RBAC로 외부 개발자 kubectl 접속 설정</li>
<li>StatefulSet + Headless Service의 완벽한 이해</li>
<li>DaemonSet으로 모든 노드에 자동 배포</li>
<li>Monitoring with kube-ops-view</li>
</ol>
<hr>
<h2 id="1-ingress-http-라우팅의-완성판">1. Ingress: HTTP 라우팅의 완성판</h2>
<h3 id="ingress가-필요한-이유">Ingress가 필요한 이유</h3>
<p>Day 2에서 NodePort를 사용했을 때 문제점:</p>
<ul>
<li>포트 번호가 랜덤 (32000-32767)</li>
<li>URL이 <code>http://172.30.1.43:30456/</code> 같은 형태</li>
<li>여러 서비스마다 다른 포트 필요</li>
</ul>
<p>Ingress를 사용하면:</p>
<ul>
<li>도메인 이름으로 접근 (<code>http://myapp.local</code>)</li>
<li>Path 기반 라우팅 (<code>/app1</code>, <code>/app2</code>)</li>
<li>HTTPS/TLS 종료</li>
<li>여러 서비스를 하나의 진입점으로</li>
</ul>
<h3 id="실습-nginx-ingress-controller-배포">실습: NGINX Ingress Controller 배포</h3>
<pre><code class="language-bash">kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml</code></pre>
<p><strong>실제 출력:</strong></p>
<pre><code>namespace/ingress-nginx created
serviceaccount/ingress-nginx created
...
service/ingress-nginx-controller created  # NodePort로 생성됨
deployment.apps/ingress-nginx-controller created</code></pre><h3 id="bare-metal-환경에서는-nodeport-사용">Bare-metal 환경에서는 NodePort 사용</h3>
<p>클라우드(AWS, GCP)에서는 LoadBalancer 타입이 자동으로 작동하지만, 저희 Bare-metal 클러스터에서는 NodePort를 사용합니다.</p>
<pre><code class="language-bash">$ kubectl get svc -n ingress-nginx
NAME                                 TYPE        CLUSTER-IP       PORT(S)
ingress-nginx-controller             NodePort    10.102.172.125   80:32456/TCP,443:32756/TCP
ingress-nginx-controller-admission   ClusterIP   10.96.66.231     443/TCP</code></pre>
<p><strong>핵심 포인트:</strong></p>
<ul>
<li>HTTP: 32456 포트</li>
<li>HTTPS: 32756 포트</li>
</ul>
<h3 id="path-based-routing-경로-기반-라우팅">Path-based Routing (경로 기반 라우팅)</h3>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: path-based-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.local
    http:
      paths:
      - path: /app1
        pathType: Prefix
        backend:
          service:
            name: app1
            port:
              number: 80
      - path: /app2
        pathType: Prefix
        backend:
          service:
            name: app2
            port:
              number: 80</code></pre>
<p><strong>/etc/hosts 설정 추가:</strong></p>
<pre><code>172.30.1.43 myapp.local</code></pre><p><strong>테스트 결과:</strong></p>
<pre><code class="language-bash">$ curl http://myapp.local:32456/app1
Hello from App1!

$ curl http://myapp.local:32456/app2
Hello from App2!</code></pre>
<h3 id="tlshttps-설정-자체-서명-인증서">TLS/HTTPS 설정 (자체 서명 인증서)</h3>
<p><strong>🤔 내 질문:</strong> &quot;TLS 지금 나 SSL 인증서나 도메인 없는데 무료 인증서로 되니?&quot;</p>
<p><strong>답변:</strong> 자체 서명(self-signed) 인증서로 테스트 가능합니다!</p>
<pre><code class="language-bash"># 자체 서명 인증서 생성
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /tmp/tls.key -out /tmp/tls.crt \
  -subj &quot;/CN=myapp.local/O=myapp&quot;

# Secret 생성
kubectl create secret tls myapp-tls \
  --cert=/tmp/tls.crt \
  --key=/tmp/tls.key</code></pre>
<p><strong>Ingress에 TLS 추가:</strong></p>
<pre><code class="language-yaml">spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - myapp.local
    secretName: myapp-tls
  rules:
  - host: myapp.local
    ...</code></pre>
<p><strong>HTTPS 테스트 (자체 서명이라 -k 옵션 필요):</strong></p>
<pre><code class="language-bash">$ curl -k https://myapp.local:32756/
Hello from App1!</code></pre>
<h3 id="host-based-routing-도메인-기반-라우팅">Host-based Routing (도메인 기반 라우팅)</h3>
<pre><code class="language-yaml">spec:
  ingressClassName: nginx
  rules:
  - host: app1.local  # 도메인별로 다른 서비스
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1
            port:
              number: 80
  - host: app2.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app2
            port:
              number: 80</code></pre>
<hr>
<h2 id="2-hpa-자동-스케일링의-마법">2. HPA: 자동 스케일링의 마법</h2>
<h3 id="metrics-server-설치-및-bare-metal-문제-해결">metrics-server 설치 및 bare-metal 문제 해결</h3>
<pre><code class="language-bash">kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml</code></pre>
<p><strong>문제 발생:</strong> Pod가 0/1 Ready 상태로 멈춤</p>
<pre><code class="language-bash">$ kubectl get pod -n kube-system -l k8s-app=metrics-server
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-5f9f776df5-abc12   0/1     Running   0          2m</code></pre>
<p><strong>원인:</strong> Bare-metal 환경에서 kubelet이 유효한 TLS 인증서가 없음</p>
<p><strong>해결:</strong> <code>--kubelet-insecure-tls</code> 플래그 추가</p>
<pre><code class="language-bash">kubectl patch deployment metrics-server -n kube-system --type=&#39;json&#39; \
  -p=&#39;[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/spec/template/spec/containers/0/args/-&quot;, &quot;value&quot;: &quot;--kubelet-insecure-tls&quot;}]&#39;</code></pre>
<p><strong>30-60초 후 확인:</strong></p>
<pre><code class="language-bash">$ kubectl top nodes
NAME   CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
cpu1   483m         4%     3145Mi          41%
cpu2   234m         2%     2891Mi          17%
gpu1   178m         1%     2654Mi          16%</code></pre>
<h3 id="실시간-hpa-테스트">실시간 HPA 테스트</h3>
<p><strong>테스트 애플리케이션 배포:</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: php-apache
  template:
    metadata:
      labels:
        app: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m  # HPA가 이 값 기준으로 계산
          limits:
            cpu: 500m</code></pre>
<p><strong>HPA 생성 (CPU 50% 목표):</strong></p>
<pre><code class="language-bash">kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10</code></pre>
<pre><code class="language-bash">$ kubectl get hpa
NAME         REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache   0%/50%    1         10        1          10s</code></pre>
<h3 id="부하-생성-및-실시간-관찰">부하 생성 및 실시간 관찰</h3>
<pre><code class="language-bash">kubectl run load-generator --image=busybox:1.28 \
  -- /bin/sh -c &quot;while true; do wget -q -O- http://php-apache; done&quot;</code></pre>
<p><strong>스케일링 과정 실시간 관찰 (30초마다 체크):</strong></p>
<pre><code class="language-bash"># t=30초
$ kubectl get hpa
NAME         TARGETS    MINPODS   MAXPODS   REPLICAS
php-apache   200%/50%   1         10        1

$ kubectl get pods -l app=php-apache
NAME                          READY   STATUS    RESTARTS   AGE
php-apache-79544c9bd9-abc12   1/1     Running   0          5m

# t=60초 - 스케일 업 시작!
$ kubectl get hpa
NAME         TARGETS    MINPODS   MAXPODS   REPLICAS
php-apache   200%/50%   1         10        4        # 4개로 증가

$ kubectl get pods -l app=php-apache
NAME                          READY   STATUS              RESTARTS   AGE
php-apache-79544c9bd9-abc12   1/1     Running             0          5m30s
php-apache-79544c9bd9-def34   0/1     ContainerCreating   0          3s
php-apache-79544c9bd9-ghi56   0/1     ContainerCreating   0          3s
php-apache-79544c9bd9-jkl78   0/1     ContainerCreating   0          3s

# t=90초 - 더 많은 Pod 생성
$ kubectl get hpa
NAME         TARGETS    MINPODS   MAXPODS   REPLICAS
php-apache   180%/50%   1         10        7        # 7개로 증가!

$ kubectl get pods -l app=php-apache
NAME                          READY   STATUS    RESTARTS   AGE   NODE
php-apache-79544c9bd9-abc12   1/1     Running   0          6m    cpu1
php-apache-79544c9bd9-def34   1/1     Running   0          33s   cpu2
php-apache-79544c9bd9-ghi56   1/1     Running   0          33s   gpu1
php-apache-79544c9bd9-jkl78   1/1     Running   0          33s   cpu1
php-apache-79544c9bd9-mno90   1/1     Running   0          3s    cpu2
php-apache-79544c9bd9-pqr12   1/1     Running   0          3s    gpu1
php-apache-79544c9bd9-stu34   1/1     Running   0          3s    cpu2

# t=120초 - CPU 안정화
$ kubectl get hpa
NAME         TARGETS   MINPODS   MAXPODS   REPLICAS
php-apache   47%/50%   1         10        7        # 목표 달성!

$ kubectl top pod -l app=php-apache
NAME                          CPU(cores)   MEMORY(bytes)
php-apache-79544c9bd9-abc12   95m          10Mi
php-apache-79544c9bd9-def34   93m          10Mi
php-apache-79544c9bd9-ghi56   91m          10Mi
php-apache-79544c9bd9-jkl78   94m          10Mi
php-apache-79544c9bd9-mno90   92m          10Mi
php-apache-79544c9bd9-pqr12   90m          10Mi
php-apache-79544c9bd9-stu34   93m          10Mi</code></pre>
<p><strong>결과:</strong></p>
<ul>
<li>1 Pod → 4 Pods → 7 Pods (60초 만에!)</li>
<li>CPU: 0% → 200% → 47% (안정화)</li>
<li>각 Pod가 CPU request(200m)의 약 47% 사용</li>
</ul>
<h3 id="스케일-다운-scale-down">스케일 다운 (Scale-down)</h3>
<p>부하 생성기 삭제 후:</p>
<pre><code class="language-bash">kubectl delete pod load-generator</code></pre>
<p><strong>5분 후 (기본 scale-down 대기 시간):</strong></p>
<pre><code class="language-bash">$ kubectl get hpa
NAME         TARGETS   MINPODS   MAXPODS   REPLICAS
php-apache   0%/50%    1         10        7        # 아직 7개

# 5분 경과 후
$ kubectl get hpa
NAME         TARGETS   MINPODS   MAXPODS   REPLICAS
php-apache   0%/50%    1         10        1        # 1개로 감소!</code></pre>
<hr>
<h2 id="3-rbac-외부-개발자-kubectl-접속-설정">3. RBAC: 외부 개발자 kubectl 접속 설정</h2>
<h3 id="🤔-내-질문-rbac는-어쩔때-쓰는거야">🤔 내 질문: &quot;RBAC는 어쩔때 쓰는거야?&quot;</h3>
<p><strong>답변:</strong> RBAC는 다음 3가지 상황에서 필수입니다:</p>
<ol>
<li><p><strong>Pod가 Kubernetes API에 접근할 때</strong></p>
<ul>
<li>예: Prometheus가 메트릭 수집</li>
<li>ServiceAccount로 권한 부여</li>
</ul>
</li>
<li><p><strong>외부 개발자가 kubectl 사용할 때</strong></p>
<ul>
<li>개발자 컴퓨터에서 kubectl로 클러스터 접속</li>
<li>X.509 인증서로 신원 확인</li>
</ul>
</li>
<li><p><strong>CI/CD 시스템이 배포할 때</strong></p>
<ul>
<li>Jenkins, GitLab CI가 kubectl apply</li>
<li>ServiceAccount Token 사용</li>
</ul>
</li>
</ol>
<h3 id="🤔-내-추가-질문-개발자-컴퓨터가-워커노드로-연결되어-있지-않아도-본인-로컬에-3개의-인증서-파일을-넣고-그-경로는-어떻게-지정해서-config에-넣어줘야하는것이며-그냥-kubectl-만-깔려있으면-명령어-사용이-가능하다는거지">🤔 내 추가 질문: &quot;개발자 컴퓨터가 워커노드로 연결되어 있지 않아도 본인 로컬에 3개의 인증서 파일을 넣고 그 경로는 어떻게 지정해서 config에 넣어줘야하는것이며 그냥 kubectl 만 깔려있으면 명령어 사용이 가능하다는거지?&quot;</h3>
<p><strong>답변:</strong> 네, 맞습니다! 개발자 컴퓨터는 <strong>워커 노드가 아닙니다</strong>. 그냥 kubectl 클라이언트만 설치하면 됩니다.</p>
<h3 id="실습-john-개발자-계정-생성">실습: john 개발자 계정 생성</h3>
<p><strong>1. 개인키 생성:</strong></p>
<pre><code class="language-bash">openssl genrsa -out john.key 2048</code></pre>
<p><strong>2. CSR (Certificate Signing Request) 생성:</strong></p>
<pre><code class="language-bash">openssl req -new -key john.key -out john.csr \
  -subj &quot;/CN=john/O=dev-team&quot;</code></pre>
<ul>
<li>CN=john: 사용자 이름</li>
<li>O=dev-team: 그룹 이름</li>
</ul>
<p><strong>3. Kubernetes CA로 서명:</strong></p>
<pre><code class="language-bash">sudo openssl x509 -req -in john.csr \
  -CA /etc/kubernetes/pki/ca.crt \
  -CAkey /etc/kubernetes/pki/ca.key \
  -CAcreateserial -out john.crt -days 365</code></pre>
<p><strong>실제 출력:</strong></p>
<pre><code>Certificate request self-signature ok
subject=CN = john, O = dev-team</code></pre><p><strong>4. kubeconfig 파일 생성:</strong></p>
<p><code>~/.kube/config</code> (개발자 로컬):</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority: /Users/john/.kube/certs/ca.crt
    server: https://172.30.1.43:6443
  name: production-cluster

contexts:
- context:
    cluster: production-cluster
    user: john
    namespace: dev
  name: john@production

current-context: john@production

users:
- name: john
  user:
    client-certificate: /Users/john/.kube/certs/john.crt
    client-key: /Users/john/.kube/certs/john.key</code></pre>
<p><strong>5. 권한 테스트 (기본 = 모두 거부):</strong></p>
<pre><code class="language-bash">$ kubectl get pods
Error from server (Forbidden): pods is forbidden: User &quot;john&quot; cannot list resource &quot;pods&quot; in API group &quot;&quot; in the namespace &quot;default&quot;

$ kubectl get pods -n dev
Error from server (Forbidden): pods is forbidden: User &quot;john&quot; cannot list resource &quot;pods&quot; in API group &quot;&quot; in the namespace &quot;dev&quot;</code></pre>
<p><strong>Kubernetes의 기본 정책: Deny by default!</strong></p>
<p><strong>6. dev namespace에 view 권한 부여:</strong></p>
<pre><code class="language-bash">kubectl create namespace dev

kubectl create rolebinding john-view \
  --clusterrole=view \
  --user=john \
  --namespace=dev</code></pre>
<p><strong>권한 확인:</strong></p>
<pre><code class="language-bash">$ kubectl auth can-i get pods -n dev --as=john
yes

$ kubectl auth can-i get pods -n default --as=john
no</code></pre>
<p><strong>john 계정으로 테스트:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n dev
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          2m

$ kubectl get pods -n default
Error from server (Forbidden): pods is forbidden: User &quot;john&quot; cannot list resource &quot;pods&quot;</code></pre>
<h3 id="개발자-패키지-전달">개발자 패키지 전달</h3>
<p>개발자에게 전달할 파일:</p>
<pre><code>developer-package/
├── ca.crt           # 클러스터 CA 인증서
├── john.crt         # 개발자 인증서
├── john.key         # 개발자 개인키 (절대 공유 금지!)
├── kubeconfig-sample  # kubeconfig 예제
└── README.md        # 설정 가이드</code></pre><p><strong>개발자 컴퓨터 요구사항:</strong></p>
<ul>
<li>✅ kubectl 설치만 필요</li>
<li>✅ 네트워크: API Server (172.30.1.43:6443) 접근 가능</li>
<li>❌ Docker, kubelet 불필요!</li>
<li>❌ 워커노드 아님! 클러스터 join 불필요!</li>
</ul>
<hr>
<h2 id="4-statefulset--headless-service-완벽한-이해">4. StatefulSet + Headless Service: 완벽한 이해</h2>
<h3 id="🤔-내-질문-headless서비스는-정확히-왜-statefulset에-필수이며-왜-필요한거고-뭔지-설명이-부족했어">🤔 내 질문: &quot;headless서비스는 정확히 왜 statefulset에 필수이며 왜 필요한거고 뭔지 설명이 부족했어&quot;</h3>
<p>이 질문에 답하기 위해 심도 있게 파고들었습니다!</p>
<h3 id="headless-service란">Headless Service란?</h3>
<p><strong>일반 Service:</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: normal-service
spec:
  clusterIP: 10.96.100.50  # 가상 IP 할당됨
  selector:
    app: myapp
  ports:
  - port: 80</code></pre>
<p>DNS 쿼리 결과:</p>
<pre><code class="language-bash">$ nslookup normal-service
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      normal-service
Address 1: 10.96.100.50  # Service IP 1개만 반환</code></pre>
<p>→ kube-proxy가 로드밸런싱
→ 어떤 Pod에 연결될지 모름 (랜덤)</p>
<p><strong>Headless Service:</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: nginx-headless
spec:
  clusterIP: None  # ← Headless!
  selector:
    app: nginx-stateful
  ports:
  - port: 80</code></pre>
<p>DNS 쿼리 결과:</p>
<pre><code class="language-bash">$ nslookup nginx-headless
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      nginx-headless
Address 1: 10.244.102.162  # Pod-0 IP
Address 2: 10.244.5.234    # Pod-1 IP
Address 3: 10.244.184.94   # Pod-2 IP</code></pre>
<p>→ 모든 Pod IP를 직접 반환!
→ 개별 Pod DNS도 제공:</p>
<pre><code class="language-bash">$ nslookup nginx-stateful-0.nginx-headless
Name:      nginx-stateful-0.nginx-headless
Address 1: 10.244.102.162  # Pod-0 IP만 반환</code></pre>
<h3 id="왜-statefulset에-필수인가">왜 StatefulSet에 필수인가?</h3>
<p><strong>실제 MongoDB Replica Set 예시:</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb-headless  # ← Headless Service 연결
  replicas: 3
  ...</code></pre>
<p>MongoDB 초기화:</p>
<pre><code class="language-javascript">rs.initiate({
  _id: &quot;rs0&quot;,
  members: [
    { _id: 0, host: &quot;mongodb-0.mongodb-headless:27017&quot; },  # Primary
    { _id: 1, host: &quot;mongodb-1.mongodb-headless:27017&quot; },  # Secondary
    { _id: 2, host: &quot;mongodb-2.mongodb-headless:27017&quot; }   # Secondary
  ]
})</code></pre>
<p>애플리케이션 연결:</p>
<pre><code class="language-javascript">const uri = &quot;mongodb://mongodb-0.mongodb-headless:27017,mongodb-1.mongodb-headless:27017,mongodb-2.mongodb-headless:27017/mydb?replicaSet=rs0&quot;</code></pre>
<p><strong>만약 일반 Service를 쓴다면?</strong></p>
<ul>
<li><code>mongodb-service:27017</code> 하나의 주소만 가능</li>
<li>kube-proxy가 랜덤하게 로드밸런싱</li>
<li>Primary/Secondary 구분 불가능!</li>
<li>MongoDB Replica Set 구성 실패!</li>
</ul>
<h3 id="실습-statefulset-순차적-생성">실습: StatefulSet 순차적 생성</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: nginx-headless
spec:
  clusterIP: None
  selector:
    app: nginx-stateful
  ports:
  - port: 80
    name: web
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-stateful
spec:
  serviceName: nginx-headless
  replicas: 3
  selector:
    matchLabels:
      app: nginx-stateful
  template:
    metadata:
      labels:
        app: nginx-stateful
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
      volumes:
      - name: www
        emptyDir: {}</code></pre>
<p><strong>실제 생성 과정 (2초 간격):</strong></p>
<pre><code class="language-bash">$ kubectl apply -f statefulset.yaml

# t=0초
--- 15:00:00 ---
NAME               READY   STATUS              RESTARTS   AGE
nginx-stateful-0   0/1     ContainerCreating   0          0s

# t=2초
--- 15:00:02 ---
NAME               READY   STATUS    RESTARTS   AGE
nginx-stateful-0   1/1     Running   0          2s        # Pod-0 Ready!
nginx-stateful-1   0/1     Pending   0          0s        # Pod-1 생성 시작

# t=4초
--- 15:00:04 ---
NAME               READY   STATUS    RESTARTS   AGE
nginx-stateful-0   1/1     Running   0          4s
nginx-stateful-1   1/1     Running   0          2s        # Pod-1 Ready!
nginx-stateful-2   0/1     Pending   0          0s        # Pod-2 생성 시작

# t=6초
--- 15:00:06 ---
NAME               READY   STATUS    RESTARTS   AGE
nginx-stateful-0   1/1     Running   0          6s
nginx-stateful-1   1/1     Running   0          4s
nginx-stateful-2   1/1     Running   0          2s        # Pod-2 Ready!</code></pre>
<p><strong>순차적 생성!</strong> Pod-0이 Running이 되어야 Pod-1이 생성 시작!</p>
<p><strong>Pod 분포 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -o wide -l app=nginx-stateful
NAME               READY   STATUS    RESTARTS   AGE   IP              NODE
nginx-stateful-0   1/1     Running   0          2m    10.244.102.162  cpu2
nginx-stateful-1   1/1     Running   0          2m    10.244.5.234    gpu1
nginx-stateful-2   1/1     Running   0          2m    10.244.184.94   cpu1</code></pre>
<p><strong>안정적인 네트워크 ID 확인:</strong></p>
<pre><code class="language-bash">$ kubectl run -it --rm dns-test --image=busybox:1.28 --restart=Never -- \
  nslookup nginx-stateful-0.nginx-headless

Name:      nginx-stateful-0.nginx-headless.default.svc.cluster.local
Address 1: 10.244.102.162</code></pre>
<p>Pod를 재시작해도:</p>
<ul>
<li>IP는 변경될 수 있음: 10.244.102.162 → 10.244.5.240</li>
<li>DNS 이름은 불변: <code>nginx-stateful-0.nginx-headless</code></li>
</ul>
<hr>
<h2 id="5-daemonset-모든-노드에-자동-배포">5. DaemonSet: 모든 노드에 자동 배포</h2>
<h3 id="🤔-내-질문-그러면-daemonset에서는-이게-필요없는가">🤔 내 질문: &quot;그러면 Daemonset에서는 이게 필요없는가?&quot;</h3>
<p><strong>답변:</strong> 네, DaemonSet은 Headless Service가 <strong>필요 없습니다</strong>!</p>
<h3 id="daemonset-vs-statefulset">DaemonSet vs StatefulSet</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>StatefulSet</th>
<th>DaemonSet</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Pod 개수</strong></td>
<td>replicas로 지정 (3개)</td>
<td>노드 개수만큼 자동</td>
</tr>
<tr>
<td><strong>Pod 이름</strong></td>
<td>순차적 (mongodb-0, -1, -2)</td>
<td>랜덤 (fluentd-abc)</td>
</tr>
<tr>
<td><strong>배치</strong></td>
<td>어느 노드든 상관없음</td>
<td><strong>각 노드당 1개 필수</strong></td>
</tr>
<tr>
<td><strong>안정적 ID</strong></td>
<td>✅ 필요</td>
<td>❌ 불필요</td>
</tr>
<tr>
<td><strong>Pod 간 통신</strong></td>
<td>✅ 필요 (DB Cluster)</td>
<td>❌ 불필요 (독립 동작)</td>
</tr>
<tr>
<td><strong>Headless Service</strong></td>
<td>✅ <strong>필수</strong></td>
<td>❌ <strong>불필요</strong></td>
</tr>
</tbody></table>
<h3 id="실습-fluentd-로그-수집">실습: fluentd 로그 수집</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd:v1.14-1
        volumeMounts:
        - name: varlog
          mountPath: /var/log
          readOnly: true
      volumes:
      - name: varlog
        hostPath:
          path: /var/log</code></pre>
<p><strong>배포 결과:</strong></p>
<pre><code class="language-bash">$ kubectl get daemonset -n kube-system fluentd
NAME      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
fluentd   3         3         3       3            3           &lt;none&gt;          1m

$ kubectl get pods -n kube-system -l app=fluentd -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP             NODE
fluentd-ntq9z   1/1     Running   0          1m    10.244.5.237   gpu1
fluentd-pnx9h   1/1     Running   0          1m    10.244.102.166 cpu2
fluentd-vq7wd   1/1     Running   0          1m    10.244.184.95  cpu1</code></pre>
<p><strong>자동으로 3개 노드에 각 1개씩 배포!</strong></p>
<h3 id="nodeselector로-특정-노드만-선택">nodeSelector로 특정 노드만 선택</h3>
<pre><code class="language-bash"># gpu1 노드에 라벨 추가
kubectl label nodes gpu1 disktype=ssd

# gpu1 노드에만 배포되는 DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter-ssd
spec:
  selector:
    matchLabels:
      app: node-exporter-ssd
  template:
    spec:
      nodeSelector:
        disktype: ssd  # ← disktype=ssd 라벨이 있는 노드에만!
      containers:
      - name: node-exporter
        image: prom/node-exporter:v1.3.1</code></pre>
<p><strong>결과:</strong></p>
<pre><code class="language-bash">$ kubectl get daemonset -n kube-system node-exporter-ssd
NAME                DESIRED   CURRENT   READY   NODE SELECTOR
node-exporter-ssd   1         1         1       disktype=ssd

$ kubectl get pods -n kube-system -l app=node-exporter-ssd -o wide
NAME                      READY   STATUS    RESTARTS   AGE   NODE
node-exporter-ssd-wqjvn   1/1     Running   0          30s   gpu1</code></pre>
<p><strong>cpu2 노드에도 라벨 추가:</strong></p>
<pre><code class="language-bash">kubectl label nodes cpu2 disktype=ssd</code></pre>
<p><strong>자동으로 Pod 추가 생성!</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n kube-system -l app=node-exporter-ssd -o wide
NAME                      READY   STATUS    RESTARTS   AGE   NODE
node-exporter-ssd-lf8g2   1/1     Running   0          3s    cpu2  # 자동 생성!
node-exporter-ssd-wqjvn   1/1     Running   0          36s   gpu1</code></pre>
<hr>
<h2 id="6-monitoring-kube-ops-view">6. Monitoring: kube-ops-view</h2>
<h3 id="삽질-포인트-redis-에러">삽질 포인트: Redis 에러</h3>
<p>처음에 공식 YAML로 설치했더니 Redis 연결 에러 발생:</p>
<pre><code>redis.exceptions.ConnectionError: Error -2 connecting to kube-ops-view-redis:6379. Name or service not known.</code></pre><p>Redis가 포함되지 않은 불완전한 설치였습니다.</p>
<h3 id="해결-helm으로-재설치">해결: Helm으로 재설치</h3>
<pre><code class="language-bash"># Helm 설치
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# geek-cookbook repo 추가
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/

# kube-ops-view 설치 (Redis 포함)
helm install kube-ops-view geek-cookbook/kube-ops-view \
  --version 1.2.2 \
  --set service.main.type=NodePort,service.main.ports.http.nodePort=30005 \
  --set env.TZ=&quot;Asia/Seoul&quot; \
  --namespace kube-system</code></pre>
<p><strong>설치 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kube-ops-view   1/1     1            1           29s

NAME                                 READY   STATUS    RESTARTS   AGE
pod/kube-ops-view-657dbc6cd8-g7s7t   1/1     Running   0          29s

NAME                    TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/kube-ops-view   NodePort   10.100.118.167   &lt;none&gt;        8080:30005/TCP   29s

NAME                      ENDPOINTS             AGE
endpoints/kube-ops-view   10.244.102.168:8080   29s</code></pre>
<p><strong>웹 접속:</strong></p>
<pre><code>http://172.30.1.43:30005</code></pre><p>실시간으로 볼 수 있는 것:</p>
<ul>
<li>3개 노드 시각화 (cpu1, cpu2, gpu1)</li>
<li>각 노드별 Pod 분포</li>
<li>CPU/메모리 사용량</li>
<li>StatefulSet Pods (nginx-stateful-0, -1, -2)</li>
<li>DaemonSet Pods (fluentd x 3)</li>
<li>Ingress Controller Pods</li>
</ul>
<hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="1-ingress는-production의-기본">1. Ingress는 Production의 기본</h3>
<ul>
<li>NodePort는 개발/테스트용</li>
<li>Ingress로 도메인 기반 라우팅, TLS 종료</li>
<li>Bare-metal에서는 NodePort로 Ingress Controller 노출</li>
</ul>
<h3 id="2-hpa는-생각보다-빠르다">2. HPA는 생각보다 빠르다</h3>
<ul>
<li>60초 만에 1 Pod → 7 Pods</li>
<li>metrics-server 필수 (bare-metal은 --kubelet-insecure-tls)</li>
<li>Scale-down은 5분 대기 (안정성)</li>
</ul>
<h3 id="3-rbac는-보안의-핵심">3. RBAC는 보안의 핵심</h3>
<ul>
<li>기본 정책: Deny by default</li>
<li>X.509 인증서로 외부 개발자 접속</li>
<li>Namespace 범위 권한 부여 가능</li>
</ul>
<h3 id="4-headless-service의-진짜-의미">4. Headless Service의 진짜 의미</h3>
<ul>
<li>StatefulSet: 개별 Pod DNS 필요 (mongodb-0.svc)</li>
<li>DaemonSet: 불필요 (각 노드에서 독립 동작)</li>
<li>MongoDB, Kafka, Elasticsearch 같은 Cluster 구성에 필수</li>
</ul>
<h3 id="5-daemonset은-자동화의-극치">5. DaemonSet은 자동화의 극치</h3>
<ul>
<li>노드 추가 → Pod 자동 생성</li>
<li>노드 삭제 → Pod 자동 제거</li>
<li>nodeSelector로 특정 노드만 선택 가능</li>
</ul>
<h3 id="6-helm이-복잡한-애플리케이션에는-필수">6. Helm이 복잡한 애플리케이션에는 필수</h3>
<ul>
<li>YAML 여러 개 관리의 어려움</li>
<li>Redis, DB 등 dependency 자동 설치</li>
<li>버전 관리 용이</li>
</ul>
<hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="1-metrics-server-pod가-01-ready로-멈춤">1. metrics-server Pod가 0/1 Ready로 멈춤</h3>
<p><strong>원인:</strong> Bare-metal 클러스터는 kubelet TLS 인증서 없음
<strong>해결:</strong> <code>--kubelet-insecure-tls</code> 플래그 추가</p>
<h3 id="2-kube-ops-view-redis-에러">2. kube-ops-view Redis 에러</h3>
<p><strong>원인:</strong> 공식 YAML에 Redis 미포함
<strong>해결:</strong> Helm Chart 사용 (dependency 자동 설치)</p>
<h3 id="3-ingress-service가-pod를-못-찾음">3. Ingress Service가 Pod를 못 찾음</h3>
<p><strong>원인:</strong> Service selector와 Deployment label 불일치
<strong>해결:</strong> Label 정확히 매칭 (application: kube-ops-view)</p>
<h3 id="4-statefulset-pvc-pending">4. StatefulSet PVC Pending</h3>
<p><strong>원인:</strong> Bare-metal에 StorageClass 없음
<strong>해결:</strong> emptyDir 사용 (데모용)</p>
<hr>
<h2 id="다음-계획-day-5">다음 계획 (Day 5)</h2>
<p>Day 4에서 Advanced 패턴을 마스터했습니다. Day 5에서는 Production 운영에 필요한 추가 기능들을 학습할 예정입니다:</p>
<ol>
<li><strong>Job &amp; CronJob</strong> - 배치 작업 실행</li>
<li><strong>Network Policy</strong> - Pod 간 네트워크 격리</li>
<li><strong>Resource Quotas &amp; LimitRange</strong> - Namespace별 리소스 제한</li>
<li><strong>Custom Resource Definition (CRD)</strong> - Kubernetes 확장</li>
<li><strong>Helm Chart 작성</strong> - 자체 애플리케이션 패키징</li>
<li><strong>Backup &amp; Restore</strong> - etcd 백업/복구</li>
<li><strong>Cluster Upgrade</strong> - 무중단 클러스터 업그레이드</li>
</ol>
<p>Production-ready Kubernetes Cluster를 완성합시다!</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">Kubernetes Ingress Documentation</a></li>
<li><a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/">HPA Walkthrough</a></li>
<li><a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC Authorization</a></li>
<li><a href="https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/">StatefulSet Basics</a></li>
<li><a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/">DaemonSet</a></li>
</ul>
<h2 id="클러스터-환경">클러스터 환경</h2>
<p><strong>노드 구성:</strong></p>
<ul>
<li>cpu1 (172.30.1.43): Master + Worker (12 core, 7.5GB RAM)</li>
<li>cpu2 (172.30.1.80): Worker (8 core, 16GB RAM)</li>
<li>gpu1 (172.30.1.38): Worker (12 core, 16GB RAM)</li>
</ul>
<p><strong>버전:</strong></p>
<ul>
<li>Kubernetes: v1.31.13</li>
<li>CNI: Calico (VXLAN CrossSubnet)</li>
<li>Ingress: NGINX Ingress Controller v1.8.2</li>
<li>Helm: v3.19.0</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: 운영 필수 기술 마스터하기 (Day 3)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-%EC%9A%B4%EC%98%81-%ED%95%84%EC%88%98-%EA%B8%B0%EC%88%A0-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0-Day-3</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%A0%95%EB%B3%B5%EA%B8%B0-%EC%9A%B4%EC%98%81-%ED%95%84%EC%88%98-%EA%B8%B0%EC%88%A0-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0-Day-3</guid>
            <pubDate>Sat, 01 Nov 2025 13:57:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>3-node 클러스터에서 직접 실습하며 배운 Kubernetes Operations의 모든 것</p>
</blockquote>
<hr>
<h2 id="들어가며">들어가며</h2>
<p>Day 2에서 클러스터 아키텍처와 네트워킹을 이해했다면, Day 3는 <strong>실전 운영</strong>에 필요한 기술들을 익히는 날이었습니다. Secret 관리부터 Rolling Update, Health Check까지 - Production 환경에서 반드시 알아야 할 개념들을 직접 실습하며 체득했습니다.</p>
<p>특히 이번 Day에서는 &quot;왜 이 기능이 필요한가?&quot;라는 질문을 끊임없이 던지며, 단순히 명령어를 외우는 것이 아니라 <strong>설계 철학</strong>을 이해하는 데 집중했습니다.</p>
<h3 id="학습-환경">학습 환경</h3>
<ul>
<li><strong>Kubernetes</strong>: v1.31.13</li>
<li><strong>클러스터 구성</strong>:<ul>
<li>cpu1 (172.30.1.43): Master + Worker (12 core, 7.5GB RAM)</li>
<li>cpu2 (172.30.1.34): Worker (8 core, 16GB RAM)</li>
<li>gpu1 (172.30.1.38): Worker (12 core, 16GB RAM)</li>
</ul>
</li>
<li><strong>CNI</strong>: Calico (VXLAN CrossSubnet)</li>
</ul>
<hr>
<h2 id="1-secret과-configmap-민감-정보는-어떻게-관리할까">1. Secret과 ConfigMap: 민감 정보는 어떻게 관리할까?</h2>
<h3 id="configmap-vs-secret-차이가-뭘까">ConfigMap vs Secret: 차이가 뭘까?</h3>
<p>처음엔 의문이었습니다. &quot;둘 다 설정 정보 저장하는 거 아닌가? 왜 굳이 나눠놨을까?&quot;</p>
<p><strong>핵심 차이:</strong></p>
<pre><code class="language-yaml"># ConfigMap: 일반 텍스트
data:
  app.env: |
    LOG_LEVEL=info
    MAX_CONNECTIONS=100

# Secret: base64 인코딩
data:
  password: c3VwZXJzZWNyZXQxMjM=  # echo -n &#39;supersecret123&#39; | base64</code></pre>
<p>Secret은 base64로 인코딩되고, etcd에 암호화되어 저장됩니다. (etcd encryption 설정 시)</p>
<h3 id="secret-사용-방법-실습">Secret 사용 방법 실습</h3>
<p><strong>1. 환경변수로 주입:</strong></p>
<pre><code class="language-bash">kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=supersecret123</code></pre>
<pre><code class="language-yaml"># Pod에서 환경변수로 사용
env:
- name: DB_USER
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: username</code></pre>
<p><strong>검증:</strong></p>
<pre><code class="language-bash">$ kubectl exec secret-env-pod -- env | grep DB_USER
DB_USER=admin</code></pre>
<p><strong>2. 볼륨으로 마운트:</strong></p>
<pre><code class="language-yaml">volumes:
- name: secret-volume
  secret:
    secretName: db-credentials</code></pre>
<p><strong>실제로 들어가보니 신기한 구조:</strong></p>
<pre><code class="language-bash">$ kubectl exec secret-volume-pod -- ls -la /etc/secrets
total 0
drwxrwxrwt 3 root root  120 Nov  1 10:23 .
drwxr-xr-x 1 root root 4096 Nov  1 10:23 ..
drwxr-xr-x 2 root root   80 Nov  1 10:23 ..2025_11_01_10_23_45.1234567890
lrwxrwxrwx 1 root root   32 Nov  1 10:23 ..data -&gt; ..2025_11_01_10_23_45.1234567890
lrwxrwxrwx 1 root root   15 Nov  1 10:23 password -&gt; ..data/password
lrwxrwxrwx 1 root root   15 Nov  1 10:23 username -&gt; ..data/username</code></pre>
<p><strong>심볼릭 링크 구조!</strong> 이렇게 하면 Secret을 업데이트해도 파일 경로는 동일하게 유지됩니다.</p>
<h3 id="배운-점">배운 점</h3>
<ul>
<li>Secret은 단순히 &quot;보안&quot;만이 아니라 <strong>RBAC과 통합</strong>되어 권한 관리가 가능</li>
<li>볼륨 마운트 시 심볼릭 링크 구조로 <strong>무중단 업데이트</strong> 가능</li>
</ul>
<hr>
<h2 id="2-rolling-update-무중단-배포의-마법">2. Rolling Update: 무중단 배포의 마법</h2>
<h3 id="maxsurge와-maxunavailable의-비밀">maxSurge와 maxUnavailable의 비밀</h3>
<p>문서에서 &quot;Rolling Update는 무중단 배포를 지원합니다&quot;라는 말은 많이 봤지만, <strong>실제로 어떻게</strong> 동작하는지 보고 싶었습니다.</p>
<p><strong>전략 설정:</strong></p>
<pre><code class="language-yaml">strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 0        # 추가 Pod 생성 안 함
    maxUnavailable: 1  # 한 번에 1개씩만 교체</code></pre>
<p><strong>업데이트 실행:</strong></p>
<pre><code class="language-bash">$ kubectl set image deployment/nginx-rolling nginx=nginx:1.21</code></pre>
<p><strong>실시간 모니터링 결과:</strong></p>
<pre><code># 시작 (3개 모두 Running)
nginx-rolling-56d8f-abc   1/1   Running
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running

# 첫 번째 교체
nginx-rolling-56d8f-abc   1/1   Terminating     &lt;- 종료 중
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   0/1   ContainerCreating  &lt;- 생성 중

# 새 Pod Ready
nginx-rolling-56d8f-def   1/1   Running
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   1/1   Running         &lt;- Ready!

# 두 번째 교체
nginx-rolling-56d8f-def   1/1   Terminating
nginx-rolling-56d8f-ghi   1/1   Running
nginx-rolling-7c9d4-jkl   1/1   Running
nginx-rolling-7c9d4-mno   0/1   ContainerCreating</code></pre><p><strong>놀라운 점:</strong></p>
<ul>
<li>정확히 <strong>한 번에 하나씩만</strong> 교체됨</li>
<li>새 Pod가 Running이 된 후에야 다음 Pod 종료 시작</li>
<li>서비스 중단 없이 완벽하게 업데이트!</li>
</ul>
<h3 id="배운-점-1">배운 점</h3>
<pre><code>maxSurge=0, maxUnavailable=1
→ 총 Pod 수는 항상 3개 유지
→ 하나씩 차근차근 교체

maxSurge=1, maxUnavailable=1 (기본값)
→ 총 Pod 수는 3~4개 (최대 4개까지 가능)
→ 더 빠른 업데이트, 약간의 리소스 오버헤드</code></pre><hr>
<h2 id="3-pvpvc-스토리지-추상화의-필요성">3. PV/PVC: 스토리지 추상화의 필요성</h2>
<h3 id="hostpath의-함정">HostPath의 함정</h3>
<p>처음엔 간단하게 생각했습니다. &quot;각 노드에 같은 경로로 PV 만들면 되겠지?&quot;</p>
<p><strong>현실은...</strong></p>
<p><strong>gpu1 노드에서 데이터 생성:</strong></p>
<pre><code class="language-bash">$ kubectl exec pvc-test-pod -- sh -c &quot;echo &#39;Data from gpu1&#39; &gt; /data/test.txt&quot;
$ kubectl exec pvc-test-pod -- cat /data/test.txt
Data from gpu1</code></pre>
<p><strong>Pod를 cpu1 노드로 이동:</strong></p>
<pre><code class="language-bash">$ kubectl delete pod pvc-test-pod
$ kubectl apply -f pvc-test-pod.yaml  # nodeSelector: cpu1
$ kubectl exec pvc-test-pod -- cat /data/test.txt
cat: can&#39;t open &#39;/data/test.txt&#39;: No such file or directory</code></pre>
<p><strong>아하!</strong> HostPath는 <strong>노드 로컬 스토리지</strong>였습니다. 다른 노드에선 접근 불가!</p>
<h3 id="ceph가-뭐길래">Ceph가 뭐길래?</h3>
<p>&quot;그럼 여러 노드에서 같은 데이터를 쓰려면 어떻게 하지?&quot;</p>
<p><strong>해답: 분산 스토리지 시스템</strong></p>
<pre><code>┌─────────────────────────────────────┐
│  Kubernetes Cluster                 │
│  ┌─────┐  ┌─────┐  ┌─────┐         │
│  │ cpu1│  │ cpu2│  │ gpu1│         │
│  └──┬──┘  └──┬──┘  └──┬──┘         │
│     │        │        │             │
│     └────────┼────────┘             │
│              │                      │
│         ┌────▼─────┐                │
│         │   Ceph   │ ← 네트워크 스토리지
│         │  Cluster │                │
│         └──────────┘                │
│  (cpu1, cpu2, gpu1의 디스크를       │
│   통합하여 하나의 스토리지로 제공)   │
└─────────────────────────────────────┘</code></pre><p><strong>Ceph:</strong></p>
<ul>
<li>여러 노드의 SSD/HDD를 <strong>하나의 스토리지 풀</strong>로 통합</li>
<li>데이터 복제 (Replication)로 안정성 보장</li>
<li>ReadWriteMany (RWX) 지원 - 여러 Pod가 동시 접근 가능</li>
</ul>
<p><strong>AWS EFS, GCP Persistent Disk 등도 같은 원리!</strong></p>
<h3 id="pv-vs-pvc-왜-나눴을까">PV vs PVC: 왜 나눴을까?</h3>
<p>&quot;PV만 있으면 되는 거 아냐? PVC는 왜 필요하지?&quot;</p>
<p><strong>설계 철학:</strong></p>
<pre><code>PV (PersistentVolume)
  ├─ 클러스터 레벨 리소스
  ├─ 관리자가 프로비저닝
  └─ 실제 스토리지 백엔드 정의

PVC (PersistentVolumeClaim)
  ├─ 네임스페이스 레벨 리소스
  ├─ 개발자가 요청
  └─ 필요한 용량/접근모드만 명시</code></pre><p><strong>비유:</strong></p>
<ul>
<li>PV = 아파트 (실제 부동산)</li>
<li>PVC = 임대 계약서 (사용 권한)</li>
</ul>
<p><strong>장점:</strong></p>
<ol>
<li><strong>추상화</strong>: 개발자는 스토리지 구현 몰라도 됨</li>
<li><strong>격리</strong>: 네임스페이스별 권한 관리</li>
<li><strong>동적 프로비저닝</strong>: StorageClass로 자동 생성 가능</li>
</ol>
<h3 id="배운-점-2">배운 점</h3>
<ul>
<li>HostPath는 개발/테스트용, Production에선 Ceph/NFS 필수</li>
<li>PV/PVC 분리는 <strong>관심사의 분리</strong> (Separation of Concerns)</li>
</ul>
<hr>
<h2 id="4-qos-리소스-압박-시-누구를-살릴-것인가">4. QoS: 리소스 압박 시 누구를 살릴 것인가?</h2>
<h3 id="처음엔-이해-안-됐던-qos">처음엔 이해 안 됐던 QoS</h3>
<p>&quot;limits 넘으면 OOMKilled되는데, QoS는 또 뭐지?&quot;</p>
<p><strong>핵심 차이:</strong></p>
<pre><code>OOMKilled (개별 Pod)
  ├─ Pod가 자신의 limits를 초과할 때
  ├─ 언제든 발생 가능
  └─ 해당 Pod만 종료

QoS Eviction (노드 전체)
  ├─ 노드 전체 메모리 부족 시
  ├─ 여러 Pod 중 누구를 죽일지 결정
  └─ 우선순위: BestEffort &gt; Burstable &gt; Guaranteed</code></pre><h3 id="qos-클래스-실습">QoS 클래스 실습</h3>
<p><strong>1. Guaranteed: 최고 우선순위</strong></p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;100m&quot;
    memory: &quot;128Mi&quot;
  limits:
    cpu: &quot;100m&quot;      # requests = limits
    memory: &quot;128Mi&quot;</code></pre>
<p><strong>2. Burstable: 중간 우선순위</strong></p>
<pre><code class="language-yaml">resources:
  requests:
    memory: &quot;128Mi&quot;
  limits:
    memory: &quot;256Mi&quot;  # limits &gt; requests</code></pre>
<p><strong>3. BestEffort: 최하위 우선순위</strong></p>
<pre><code class="language-yaml">resources: {}  # 아무것도 지정 안 함</code></pre>
<p><strong>검증:</strong></p>
<pre><code class="language-bash">$ kubectl get pod qos-guaranteed -o jsonpath=&#39;{.status.qosClass}&#39;
Guaranteed

$ kubectl get pod qos-burstable -o jsonpath=&#39;{.status.qosClass}&#39;
Burstable</code></pre>
<h3 id="실용적-사용-사례-핵심">실용적 사용 사례 (핵심!)</h3>
<p>처음엔 &quot;다 필요한 Pod인데 왜 죽여?&quot;라고 생각했지만, 실제 사례를 들으니 <strong>보험</strong> 같은 개념이라는 걸 깨달았습니다.</p>
<p><strong>시나리오 1: 클라우드 비용 최적화</strong></p>
<pre><code class="language-yaml"># 핵심 API 서버 - Guaranteed
api-server:
  resources:
    requests: {memory: 2Gi}
    limits: {memory: 2Gi}

# 로그 수집기 - Burstable
log-collector:
  resources:
    requests: {memory: 256Mi}
    limits: {memory: 1Gi}

# 통계 분석 - BestEffort
analytics:
  resources: {}  # 평소엔 여유 리소스 사용, 압박 시 희생</code></pre>
<p><strong>시나리오 2: 피크 타임 대응</strong></p>
<ul>
<li>평소: 모든 서비스 정상 동작</li>
<li>트래픽 급증:<ul>
<li>Guaranteed (API) → 절대 보호</li>
<li>BestEffort (통계) → 자동 종료</li>
<li>Burstable (로그) → 상황에 따라</li>
</ul>
</li>
</ul>
<p><strong>시나리오 3: 스팟 인스턴스 활용</strong></p>
<ul>
<li>저렴한 스팟 인스턴스에는 BestEffort Pod 배치</li>
<li>인스턴스 종료되어도 핵심 서비스 무사</li>
</ul>
<h3 id="배운-점-3">배운 점</h3>
<ul>
<li>QoS는 &quot;Pod 죽이는 기능&quot;이 아니라 <strong>리소스 압박 시 우선순위 보험</strong></li>
<li>&quot;Replica 줄여도 되는 서비스&quot;에 낮은 QoS 부여</li>
<li>멀티테넌트 클러스터에서 특히 중요</li>
</ul>
<hr>
<h2 id="5-health-check-kubernetes가-애플리케이션-상태를-아는-법">5. Health Check: Kubernetes가 애플리케이션 상태를 아는 법</h2>
<h3 id="liveness-vs-readiness-헷갈리는-두-probe">Liveness vs Readiness: 헷갈리는 두 Probe</h3>
<p><strong>Liveness Probe</strong></p>
<ul>
<li>&quot;살아있니?&quot;</li>
<li>실패 시 → <strong>Pod 재시작</strong></li>
<li>데드락, 무한루프 같은 상황 복구</li>
</ul>
<p><strong>Readiness Probe</strong></p>
<ul>
<li>&quot;트래픽 받을 준비 됐니?&quot;</li>
<li>실패 시 → <strong>Service Endpoints에서 제외</strong></li>
<li>초기화, DB 연결 대기 등</li>
</ul>
<h3 id="liveness-probe-실습">Liveness Probe 실습</h3>
<p><strong>시나리오: 30초 후 파일 삭제</strong></p>
<pre><code class="language-yaml">livenessProbe:
  exec:
    command:
      - cat
      - /tmp/healthy
  initialDelaySeconds: 5
  periodSeconds: 5

# Container command
command:
  - sh
  - -c
  - |
    touch /tmp/healthy
    sleep 30
    rm -f /tmp/healthy  # 30초 후 삭제!
    sleep 600</code></pre>
<p><strong>결과:</strong></p>
<pre><code class="language-bash">$ kubectl get pod liveness-test -w
NAME            READY   STATUS    RESTARTS   AGE
liveness-test   1/1     Running   0          10s
liveness-test   1/1     Running   0          30s
liveness-test   1/1     Running   1          40s  &lt;- 재시작!</code></pre>
<p><strong>정확히 30초 후 재시작!</strong> Liveness Probe가 실패를 감지하고 자동 복구했습니다.</p>
<h3 id="readiness-probe-실습">Readiness Probe 실습</h3>
<p><strong>시나리오: /ready 파일 없으면 트래픽 차단</strong></p>
<pre><code class="language-yaml">readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 5</code></pre>
<p><strong>배포 직후:</strong></p>
<pre><code class="language-bash">$ kubectl get pod -n production
NAME                          READY   STATUS    RESTARTS   AGE
production-app-7c9d4-abc      0/1     Running   0          10s  &lt;- 0/1!
production-app-7c9d4-def      0/1     Running   0          10s</code></pre>
<p><strong>Endpoints 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get endpoints production-app -n production
NAME             ENDPOINTS   AGE
production-app   &lt;none&gt;      30s  &lt;- 비어있음!</code></pre>
<p><strong>/ready 파일 생성:</strong></p>
<pre><code class="language-bash">$ kubectl exec -n production production-app-7c9d4-abc -- \
  sh -c &quot;echo &#39;ready&#39; &gt; /usr/share/nginx/html/ready&quot;

$ kubectl get pod -n production
production-app-7c9d4-abc      1/1     Running   0          1m  &lt;- 1/1!

$ kubectl get endpoints production-app -n production
production-app   10.244.5.224:80,10.244.102.153:80,10.244.184.91:80</code></pre>
<p><strong>완벽하게 동작!</strong> Readiness가 통과되자 Endpoints에 자동 등록되었습니다.</p>
<h3 id="startup-probe-느린-애플리케이션을-위한-배려">Startup Probe: 느린 애플리케이션을 위한 배려</h3>
<pre><code class="language-yaml">startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30    # 30번 실패까지 허용
  periodSeconds: 10       # 10초마다 체크
  # → 최대 300초(5분) 대기</code></pre>
<p>Java Spring Boot처럼 초기화가 오래 걸리는 앱에 유용합니다.</p>
<h3 id="배운-점-4">배운 점</h3>
<ul>
<li>Liveness: &quot;죽은 Pod&quot; 재시작</li>
<li>Readiness: &quot;준비 안 된 Pod&quot; 트래픽 차단</li>
<li>Startup: &quot;느린 Pod&quot; 보호</li>
<li><strong>세 가지를 함께 사용</strong>해야 완벽한 Health Check!</li>
</ul>
<hr>
<h2 id="6-실전-시나리오-production-ready-애플리케이션-배포">6. 실전 시나리오: Production-Ready 애플리케이션 배포</h2>
<h3 id="배운-모든-것을-하나로">배운 모든 것을 하나로</h3>
<p>Day 3의 모든 개념을 통합한 <strong>Production 애플리케이션</strong>을 배포했습니다.</p>
<p><strong>아키텍처:</strong></p>
<pre><code>production namespace
├── ConfigMap (nginx.conf + app.env)
├── Secret (DB_PASSWORD, API_KEY)
├── Deployment (3 replicas)
│   ├── Resource requests/limits (QoS: Burstable)
│   ├── Liveness Probe (/health)
│   ├── Readiness Probe (/ready)
│   ├── Rolling Update (maxSurge: 1, maxUnavailable: 1)
│   └── ConfigMap/Secret mount
├── Service (ClusterIP)
└── Service (NodePort 30080)</code></pre><h3 id="deployment-yaml-핵심-부분">Deployment YAML (핵심 부분)</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-app
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        # QoS: Burstable
        resources:
          requests:
            cpu: &quot;100m&quot;
            memory: &quot;128Mi&quot;
          limits:
            cpu: &quot;200m&quot;
            memory: &quot;256Mi&quot;
        # Health Checks
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        # Secret 환경변수
        env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: DB_PASSWORD
        # ConfigMap 마운트
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
      volumes:
      - name: nginx-config
        configMap:
          name: app-config</code></pre>
<h3 id="배포-검증">배포 검증</h3>
<p><strong>1. Pod 분산 배포 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n production -o wide
NAME                              NODE   IP               READY
production-app-849b867f78-9vkkp   cpu1   10.244.184.91    1/1
production-app-849b867f78-b2fjx   gpu1   10.244.5.224     1/1
production-app-849b867f78-kkdjv   cpu2   10.244.102.153   1/1</code></pre>
<p><strong>완벽하게 3개 노드에 분산!</strong></p>
<p><strong>2. QoS 클래스 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pod production-app-849b867f78-9vkkp -n production \
  -o jsonpath=&#39;{.status.qosClass}&#39;
Burstable</code></pre>
<p><strong>3. 내부 접근 테스트:</strong></p>
<pre><code class="language-bash">$ kubectl run test-v1 --rm -i --restart=Never -n production \
  --image=busybox:1.28 -- wget -qO- http://production-app

Hello from Production App v1.0</code></pre>
<p><strong>4. 외부 접근 테스트 (NodePort):</strong></p>
<pre><code class="language-bash">$ curl http://172.30.1.43:30080
Hello from Production App v1.0</code></pre>
<h3 id="rolling-update-실전-v10-→-v20">Rolling Update 실전 (v1.0 → v2.0)</h3>
<p><strong>ConfigMap 업데이트:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f app-config-v2.yaml
configmap/app-config configured</code></pre>
<p><strong>Deployment restart로 Rolling Update 트리거:</strong></p>
<pre><code class="language-bash">$ kubectl rollout restart deployment production-app -n production
deployment.apps/production-app restarted

$ kubectl rollout status deployment production-app -n production
deployment &quot;production-app&quot; successfully rolled out</code></pre>
<p><strong>업데이트 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -n production
NAME                              READY   STATUS    AGE
production-app-849b867f78-9vkkp   1/1     Running   34s  &lt;- 모두 새로 생성!
production-app-849b867f78-b2fjx   1/1     Running   34s
production-app-849b867f78-kkdjv   1/1     Running   23s

$ curl http://172.30.1.43:30080
Hello from Production App v2.0 - UPDATED!</code></pre>
<p><strong>무중단 배포 성공!</strong> 서비스 중단 없이 v2.0으로 업데이트되었습니다.</p>
<h3 id="배운-점-5">배운 점</h3>
<ul>
<li>모든 Best Practice를 한 번에 적용하는 게 <strong>Production-Ready</strong></li>
<li>ConfigMap/Secret 분리로 설정 관리 용이</li>
<li>Health Check + Rolling Update = 무중단 배포의 핵심</li>
<li>NodePort로 외부 접근 가능 (Ingress 전 단계)</li>
</ul>
<hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="1-configmap-업데이트했는데-pod가-안-바뀌어요">1. ConfigMap 업데이트했는데 Pod가 안 바뀌어요!</h3>
<p><strong>문제:</strong></p>
<pre><code class="language-bash">$ kubectl apply -f new-configmap.yaml
configmap/app-config configured

$ curl http://app
Hello from v1.0  &lt;- 여전히 v1.0!</code></pre>
<p><strong>원인:</strong></p>
<ul>
<li>ConfigMap/Secret을 업데이트해도 <strong>기존 Pod는 자동으로 재시작되지 않음</strong></li>
<li>환경변수로 주입한 경우: Pod 재시작 필수</li>
<li>볼륨 마운트: 심볼릭 링크로 업데이트되지만 애플리케이션이 리로드해야 함</li>
</ul>
<p><strong>해결:</strong></p>
<pre><code class="language-bash">$ kubectl rollout restart deployment production-app -n production</code></pre>
<h3 id="2-pvc가-pending-상태로-멈춰요">2. PVC가 Pending 상태로 멈춰요!</h3>
<p><strong>문제:</strong></p>
<pre><code class="language-bash">$ kubectl get pvc
NAME           STATUS    VOLUME   CAPACITY   STORAGECLASS
pvc-hostpath   Pending</code></pre>
<p><strong>원인:</strong></p>
<ul>
<li>PV와 PVC의 <strong>accessModes가 불일치</strong></li>
<li>PV의 용량이 PVC의 요청보다 작음</li>
<li>StorageClass를 쓰는데 Provisioner가 없음</li>
</ul>
<p><strong>해결:</strong></p>
<pre><code class="language-bash">$ kubectl describe pvc pvc-hostpath
Events:
  Warning  ProvisioningFailed  no persistent volumes available</code></pre>
<p>PV의 accessModes와 capacity를 확인하고 매칭시키세요!</p>
<h3 id="3-qos를-guaranteed로-하고-싶은데-burstable이-돼요">3. QoS를 Guaranteed로 하고 싶은데 Burstable이 돼요!</h3>
<p><strong>문제:</strong></p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;100m&quot;
    memory: &quot;128Mi&quot;
  limits:
    cpu: &quot;200m&quot;    # requests와 다름!
    memory: &quot;128Mi&quot;</code></pre>
<p><strong>원인:</strong></p>
<ul>
<li>Guaranteed는 <strong>모든 컨테이너</strong>의 <strong>모든 리소스</strong>(CPU, Memory)에서 requests = limits 필요</li>
<li>하나라도 다르면 Burstable</li>
</ul>
<p><strong>해결:</strong></p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;100m&quot;
    memory: &quot;128Mi&quot;
  limits:
    cpu: &quot;100m&quot;     # requests와 동일!
    memory: &quot;128Mi&quot;</code></pre>
<h3 id="4-readiness-probe-실패인데-pod가-안-죽어요">4. Readiness Probe 실패인데 Pod가 안 죽어요!</h3>
<p><strong>이건 정상입니다!</strong></p>
<ul>
<li>Liveness → Pod 재시작</li>
<li>Readiness → Service Endpoints에서만 제외</li>
</ul>
<p>Readiness 실패로 Pod를 죽이고 싶다면 Liveness Probe도 함께 설정하세요.</p>
<hr>
<h2 id="핵심-개념-정리">핵심 개념 정리</h2>
<h3 id="configmap-vs-secret">ConfigMap vs Secret</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>ConfigMap</th>
<th>Secret</th>
</tr>
</thead>
<tbody><tr>
<td>용도</td>
<td>일반 설정</td>
<td>민감 정보</td>
</tr>
<tr>
<td>인코딩</td>
<td>없음</td>
<td>base64</td>
</tr>
<tr>
<td>etcd 암호화</td>
<td>선택</td>
<td>권장</td>
</tr>
<tr>
<td>RBAC</td>
<td>가능</td>
<td>가능</td>
</tr>
<tr>
<td>예시</td>
<td>nginx.conf, app.env</td>
<td>password, API key</td>
</tr>
</tbody></table>
<h3 id="rolling-update-전략">Rolling Update 전략</h3>
<pre><code>maxSurge=1, maxUnavailable=0
  → 새 Pod 먼저 생성 후 기존 Pod 종료
  → 리소스 오버헤드 있지만 가장 안전

maxSurge=0, maxUnavailable=1
  → 기존 Pod 종료 후 새 Pod 생성
  → 리소스 절약, 약간의 Capacity 감소

maxSurge=1, maxUnavailable=1
  → 균형잡힌 기본값</code></pre><h3 id="pvpvc-관계">PV/PVC 관계</h3>
<pre><code>PV (관리자)
  ├─ hostPath: /mnt/data
  ├─ capacity: 1Gi
  └─ accessModes: ReadWriteOnce

      ⬇ Binding (1:1)

PVC (개발자)
  ├─ requests: 500Mi
  └─ accessModes: ReadWriteOnce

      ⬇ Mount

Pod
  └─ volumeMounts: /data</code></pre><h3 id="qos-우선순위">QoS 우선순위</h3>
<pre><code>리소스 압박 시 Eviction 순서:
1. BestEffort (requests/limits 없음)
2. Burstable (requests &lt; limits)
3. Guaranteed (requests = limits) ← 최후까지 보호</code></pre><h3 id="health-check-조합">Health Check 조합</h3>
<pre><code class="language-yaml"># 가장 권장하는 조합
livenessProbe:   # 데드락 복구
  httpGet: /health
  initialDelaySeconds: 30

readinessProbe:  # 초기화 대기
  httpGet: /ready
  initialDelaySeconds: 5

startupProbe:    # 느린 시작 허용
  httpGet: /health
  failureThreshold: 30
  periodSeconds: 10</code></pre>
<hr>
<h2 id="다음-계획-day-4">다음 계획 (Day 4)</h2>
<p>Day 3에서 개별 기능들을 익혔다면, Day 4는 <strong>고급 패턴</strong>과 <strong>실전 시나리오</strong>를 다룰 예정입니다:</p>
<ol>
<li><strong>Ingress</strong>: HTTP 라우팅, 도메인 기반 라우팅, TLS 종료</li>
<li><strong>HPA (Horizontal Pod Autoscaler)</strong>: CPU 기반 자동 스케일링</li>
<li><strong>RBAC</strong>: 역할 기반 접근 제어, ServiceAccount</li>
<li><strong>StatefulSet</strong>: Stateful 애플리케이션 (DB, 메시지큐)</li>
<li><strong>DaemonSet</strong>: 모든 노드에 Pod 배포 (로그 수집, 모니터링)</li>
<li><strong>Monitoring</strong>: metrics-server, kube-ops-view로 시각화</li>
</ol>
<p>특히 HPA와 Ingress는 Production 환경에서 거의 필수이기 때문에, Day 4의 하이라이트가 될 것 같습니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>Day 3를 돌아보니, 단순히 &quot;어떻게 하는가&quot;를 넘어 <strong>&quot;왜 이렇게 설계되었는가&quot;</strong>를 이해하는 데 집중했던 것 같습니다.</p>
<p>특히 기억에 남는 것들:</p>
<ul>
<li>Secret의 심볼릭 링크 구조</li>
<li>Rolling Update의 실시간 Pod 교체 과정</li>
<li>HostPath vs Ceph의 명확한 차이</li>
<li>QoS가 단순한 &quot;Pod 죽이기&quot;가 아니라 <strong>리소스 압박 시 보험</strong>이라는 깨달음</li>
<li>ConfigMap 업데이트 후 Pod를 수동으로 재시작해야 한다는 함정</li>
</ul>
<p>3-node 클러스터에서 직접 실습하며, 문서로만 봤을 때는 몰랐던 <strong>실제 동작</strong>을 눈으로 확인할 수 있었던 게 가장 큰 수확입니다.</p>
<p>Day 4에서는 더 복잡한 시나리오와 Production 환경에서의 Best Practice를 익혀보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: 네트워킹 심화와 첫 애플리케이션 배포 (Day 2)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%9E%85%EB%AC%B8%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EC%8B%AC%ED%99%94%EC%99%80-%EC%B2%AB-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-Day-2</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%9E%85%EB%AC%B8%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EC%8B%AC%ED%99%94%EC%99%80-%EC%B2%AB-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-Day-2</guid>
            <pubDate>Thu, 30 Oct 2025 23:47:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>3노드 클러스터 환경에서 Control Plane부터 실제 애플리케이션 배포까지</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>Day 1에서 클러스터의 기본 구조를 이해했다면, Day 2는 <strong>실전</strong>이었습니다. Control Plane이 어떻게 동작하는지, Pod 간 통신이 실제로 어떻게 이루어지는지, 그리고 마침내 외부에서 접근 가능한 웹 애플리케이션을 배포하는 것까지 경험했습니다.</p>
<p>특히 이번 실습에서는 예상치 못한 네트워킹 문제를 직접 해결하면서, Calico CNI의 동작 원리와 Linux 네트워킹에 대해 깊이 이해할 수 있었습니다.</p>
<h3 id="학습-환경">학습 환경</h3>
<ul>
<li><strong>클러스터</strong>: 3노드 (cpu1: Master+Worker, cpu2/gpu1: Worker)</li>
<li><strong>Kubernetes</strong>: v1.31.13</li>
<li><strong>CNI</strong>: Calico (VXLAN CrossSubnet 모드)</li>
<li><strong>Pod Network</strong>: 10.244.0.0/16</li>
<li><strong>Service Network</strong>: 10.96.0.0/12</li>
</ul>
<hr>
<h2 id="실습-내용">실습 내용</h2>
<h3 id="1-control-plane-심화-검증">1. Control Plane 심화 검증</h3>
<h4 id="etcd-kubernetes의-두뇌">etcd: Kubernetes의 두뇌</h4>
<p>첫 번째로 etcd가 실제로 무엇을 저장하는지 확인했습니다.</p>
<pre><code class="language-bash">kubectl exec -n kube-system etcd-cpu1 -- sh -c \
  &quot;ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/ --prefix --keys-only&quot; | head -20</code></pre>
<p><strong>출력 결과:</strong></p>
<pre><code>/registry/apiregistration.k8s.io/apiservices/v1.
/registry/apiregistration.k8s.io/apiservices/v1.admissionregistration.k8s.io
/registry/clusterrolebindings/calico-kube-controllers
/registry/deployments/calico-system/calico-kube-controllers
/registry/pods/calico-system/calico-node-ftrzj
/registry/services/endpoints/default/kubernetes
...</code></pre><p>모든 리소스가 <code>/registry/</code> 아래에 계층 구조로 저장되어 있었습니다. Deployment, Pod, Service 모두 etcd에 영구 저장되는 것을 직접 확인했습니다.</p>
<h4 id="api-server-rest-api-직접-호출">API Server: REST API 직접 호출</h4>
<pre><code class="language-bash">kubectl get --raw /version</code></pre>
<pre><code class="language-json">{
  &quot;major&quot;: &quot;1&quot;,
  &quot;minor&quot;: &quot;31&quot;,
  &quot;gitVersion&quot;: &quot;v1.31.13&quot;
}</code></pre>
<p>API Server는 80개의 API 리소스를 제공하고 있었습니다. <code>kubectl</code>이 실제로는 이 REST API를 호출하는 클라이언트에 불과하다는 것을 깨달았습니다.</p>
<h4 id="controller-manager와-scheduler-검증">Controller Manager와 Scheduler 검증</h4>
<p>Deployment를 생성하면 Controller Manager가 ReplicaSet을 생성하고, ReplicaSet Controller가 Pod를 생성하고, Scheduler가 노드를 선택하는 전체 체인을 추적했습니다.</p>
<pre><code class="language-bash">kubectl get events --sort-by=&#39;.lastTimestamp&#39; | grep test-controller</code></pre>
<pre><code>Successfully assigned default/test-controller-xxx to cpu2
Pulling image &quot;nginx:alpine&quot;
Created container nginx
Started container nginx</code></pre><p>이 과정이 1초도 안 걸렸습니다. 각 컴포넌트가 얼마나 빠르게 동작하는지 놀라웠습니다.</p>
<hr>
<h3 id="2-vxlan-네트워킹의-진실">2. VXLAN 네트워킹의 진실</h3>
<h4 id="가장-큰-오해-pod-통신은-layer-2">가장 큰 오해: &quot;Pod 통신은 Layer 2?&quot;</h4>
<p>처음에는 &quot;Pod 통신이 Layer 2라서 UDP로 감싸야 한다&quot;고 완전히 잘못 이해했습니다. 이 오해를 바로잡는 과정이 Day 2의 가장 큰 학습이었습니다.</p>
<p><strong>핵심 질문:</strong></p>
<blockquote>
<p>&quot;BGP로 라우터 정보 넣어서 미리 CIDR 땡겨오게 설정해두면 VXLAN을 사용하지 않아도 되는 것처럼 설명했는데 그게 맞아? Layer 2라서 UDP로 감아야 된다며?&quot;</p>
</blockquote>
<p><strong>답변:</strong></p>
<p><strong>Pod 통신은 원래 Layer 3 (IP 기반)입니다!</strong></p>
<p>Pod는 각자 고유한 IP 주소를 가지고 있으며, IP 패킷으로 통신합니다. VXLAN은 &quot;필수&quot;가 아니라 &quot;특정 상황에서의 해결책&quot;입니다.</p>
<h4 id="calico의-3가지-네트워킹-모드-비교">Calico의 3가지 네트워킹 모드 비교</h4>
<h5 id="1-bgp-mode-vxlan-없음">1. BGP Mode (VXLAN 없음)</h5>
<pre><code>[Pod A: 10.244.1.10]
    ↓ (IP routing)
[Node1: 172.30.1.43]
    ↓ (BGP 라우팅 정보 교환)
[Node2: 172.30.1.80]
    ↓ (IP routing)
[Pod B: 10.244.2.20]</code></pre><ul>
<li><strong>조건</strong>: 노드 간 L3 라우팅 가능 (물리 라우터가 BGP 지원)</li>
<li><strong>장점</strong>: VXLAN overhead 없음, 성능 최고</li>
<li><strong>단점</strong>: 물리 네트워크가 BGP를 지원해야 함</li>
<li><strong>MTU</strong>: 1500 (overhead 없음)</li>
</ul>
<h5 id="2-vxlan-mode-순수-overlay">2. VXLAN Mode (순수 overlay)</h5>
<pre><code>[Pod A: 10.244.1.10]
    ↓ (IP packet)
[VXLAN 캡슐화: UDP 4789]
    ↓ (Outer IP: 172.30.1.43 → 172.30.1.80)
[물리 네트워크]
    ↓
[VXLAN 역캡슐화]
    ↓
[Pod B: 10.244.2.20]</code></pre><ul>
<li><strong>조건</strong>: BGP 불가능 (클라우드 VPC, 제한된 네트워크)</li>
<li><strong>장점</strong>: 물리 네트워크와 무관하게 동작</li>
<li><strong>단점</strong>: 50 bytes overhead, 성능 저하</li>
<li><strong>MTU</strong>: 1450</li>
</ul>
<h5 id="3-vxlan-crosssubnet-mode-우리-클러스터">3. VXLAN CrossSubnet Mode (우리 클러스터!)</h5>
<pre><code>같은 서브넷:
[Pod A] → [Direct IP routing] → [Pod B]  (MTU 1500)

다른 서브넷:
[Pod A] → [VXLAN tunnel] → [Pod B]  (MTU 1450)</code></pre><ul>
<li><strong>조건</strong>: 일부 노드는 같은 서브넷, 일부는 다른 서브넷</li>
<li><strong>장점</strong>: 최적의 성능 (같은 서브넷) + 유연성 (다른 서브넷)</li>
<li><strong>우리 환경</strong>: 모든 노드가 172.30.1.0/24 → <strong>실제로는 Direct routing만 사용!</strong></li>
</ul>
<h4 id="vxlan은-언제-필요한가">VXLAN은 언제 필요한가?</h4>
<p><strong>VXLAN이 필요한 경우:</strong></p>
<ol>
<li>클라우드 환경 (AWS VPC, GCP, Azure)에서 BGP 불가</li>
<li>물리 라우터가 BGP를 지원하지 않음</li>
<li>보안 정책으로 BGP peer 설정 불가</li>
<li>서로 다른 데이터센터/서브넷을 연결</li>
</ol>
<p><strong>VXLAN이 불필요한 경우:</strong></p>
<ol>
<li>물리 라우터가 BGP 지원 (Calico BGP mode 사용)</li>
<li>같은 L2 네트워크 내 (Calico CrossSubnet의 direct routing)</li>
<li>노드가 적고 static route로 충분</li>
</ol>
<p><strong>우리 클러스터는?</strong></p>
<ul>
<li>모든 노드: 172.30.1.0/24 (같은 서브넷)</li>
<li>Calico 모드: VXLAN CrossSubnet</li>
<li><strong>실제 동작</strong>: Direct IP routing (VXLAN 미사용)</li>
<li><strong>VXLAN 인터페이스</strong>: 존재하지만 대기 상태</li>
</ul>
<h4 id="vxlan-실제-구성-확인">VXLAN 실제 구성 확인</h4>
<pre><code class="language-bash">ip -d link show type vxlan</code></pre>
<pre><code>20: vxlan.calico: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450
    vxlan id 4096 local 172.30.1.43 dev enp2s0 srcport 0 0 dstport 4789</code></pre><ul>
<li><strong>VNI</strong>: 4096 (Virtual Network Identifier)</li>
<li><strong>UDP Port</strong>: 4789 (VXLAN 표준 포트)</li>
<li><strong>MTU</strong>: 1450 = 1500(물리) - 50(VXLAN overhead)</li>
<li><strong>상태</strong>: 인터페이스는 존재하지만 <strong>실제로는 사용 안 됨</strong></li>
</ul>
<h4 id="mtu가-1450인-이유">MTU가 1450인 이유</h4>
<p>VXLAN을 사용할 때의 헤더 구조:</p>
<pre><code>[Outer Ethernet: 14 bytes]
[Outer IP: 20 bytes]
[UDP: 8 bytes]
[VXLAN: 8 bytes]
[Inner Ethernet: 14 bytes]
[Inner IP: 20 bytes]
[Payload]

Total Overhead: 14+20+8+8 = 50 bytes</code></pre><p>물리 인터페이스 MTU가 1500이므로:</p>
<ul>
<li>VXLAN MTU: 1500 - 50 = <strong>1450</strong></li>
<li>Fragmentation 방지</li>
</ul>
<p>우리 클러스터는 Direct routing을 사용하므로 실제로는 overhead가 없지만, VXLAN으로 전환 시를 대비해 1450으로 설정되어 있습니다.</p>
<h4 id="crosssubnet-모드-동작-확인">CrossSubnet 모드 동작 확인</h4>
<p>우리 클러스터는 모든 노드가 172.30.1.0/24 서브넷에 있어서, <strong>VXLAN 없이 직접 라우팅</strong>을 사용합니다:</p>
<pre><code class="language-bash">ip route | grep 10.244</code></pre>
<pre><code>10.244.5.192/26 via 172.30.1.38 dev enp2s0 proto 80 onlink     # gpu1으로 가는 경로
10.244.102.128/26 via 172.30.1.80 dev enp2s0 proto 80 onlink   # cpu2로 가는 경로</code></pre><p><strong>해석:</strong></p>
<ul>
<li><code>via 172.30.1.38</code>: gpu1 노드 IP를 next-hop으로 사용</li>
<li><code>dev enp2s0</code>: 물리 Ethernet 인터페이스 사용 (VXLAN 터널 아님!)</li>
<li><code>proto 80</code>: Calico가 설정한 라우팅 (BIRD BGP)</li>
<li><code>onlink</code>: Gateway가 직접 연결된 링크에 있음</li>
</ul>
<p><strong>확인 방법: VXLAN이 실제로 사용되는지?</strong></p>
<pre><code class="language-bash"># VXLAN 인터페이스의 트래픽 카운터 확인
ip -s link show vxlan.calico</code></pre>
<pre><code>20: vxlan.calico: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450
    RX: bytes  packets  errors  dropped
        0       0        0       0      # ← 수신 패킷 0
    TX: bytes  packets  errors  dropped
        0       0        0       0      # ← 송신 패킷 0</code></pre><p><strong>결과</strong>: VXLAN 인터페이스를 통한 트래픽이 전혀 없음! Direct routing만 사용되고 있습니다.</p>
<h4 id="layer-2-vs-layer-3-최종-정리">Layer 2 vs Layer 3: 최종 정리</h4>
<p><strong>오해:</strong></p>
<ul>
<li>&quot;Pod끼리 통신하려면 Layer 2 연결이 필요하다&quot;</li>
<li>&quot;그래서 VXLAN으로 Layer 2를 overlay해야 한다&quot;</li>
</ul>
<p><strong>진실:</strong></p>
<ul>
<li><strong>Pod 통신은 순수 Layer 3 (IP routing)</strong></li>
<li>Pod는 서로 다른 IP 서브넷에 있어도 됨</li>
<li>각 노드의 Calico가 IP 라우팅 정보 교환 (BGP)</li>
<li>VXLAN은 BGP가 불가능할 때의 &quot;우회 방법&quot;</li>
</ul>
<p><strong>비유:</strong></p>
<ul>
<li>BGP Mode: 도시 간 고속도로 (직접 연결, 빠름)</li>
<li>VXLAN Mode: 지하 터널 (우회, 느리지만 어디든 갈 수 있음)</li>
<li>CrossSubnet: 같은 도시는 고속도로, 다른 도시는 터널</li>
</ul>
<hr>
<h3 id="3-pod-네트워킹-구조">3. Pod 네트워킹 구조</h3>
<h4 id="veth-pair-이해하기-그리고-헷갈렸던-인터페이스-번호">veth pair 이해하기 (그리고 헷갈렸던 인터페이스 번호)</h4>
<p>각 Pod는 완전히 격리된 network namespace를 가지며, <strong>veth pair</strong>라는 가상 이더넷 케이블로 호스트와 연결됩니다.</p>
<p><strong>Pod 내부에서 확인:</strong></p>
<pre><code class="language-bash">kubectl exec nettest -- ip addr show</code></pre>
<pre><code>1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536
    inet 127.0.0.1/8 scope host lo

2: eth0@if22: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450
    inet 10.244.102.137/32 scope global eth0</code></pre><p><strong>호스트에서 확인:</strong></p>
<pre><code class="language-bash">ip link show | grep cali</code></pre>
<pre><code>22: calie107cf6613e@if2: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue
    link-netns cni-2594a3d8-7097-90e6-9db2-a3aaef8edf77</code></pre><h4 id="인터페이스-번호의-혼동">인터페이스 번호의 혼동</h4>
<p>처음에는 이렇게 이해했습니다 (잘못됨):</p>
<ul>
<li>Pod의 <code>eth0@if22</code> → &quot;if22는 22번 인터페이스를 의미하니까... 호스트도 22번이겠지?&quot;</li>
<li>호스트의 <code>22: cali...@if2</code> → &quot;22번 인터페이스 맞네! 근데 @if2는 뭐지?&quot;</li>
</ul>
<p><strong>이건 완전히 틀렸습니다!</strong></p>
<p>올바른 이해:</p>
<ul>
<li><p>Pod 입장: <code>2: eth0@if22</code></p>
<ul>
<li><strong>내 인터페이스 번호는 2번</strong> (eth0)</li>
<li><strong>상대방(호스트)의 인터페이스 번호는 22번</strong> (@if22)</li>
</ul>
</li>
<li><p>Host 입장: <code>22: calie107cf6613e@if2</code></p>
<ul>
<li><strong>내 인터페이스 번호는 22번</strong> (calie107cf6613e)</li>
<li><strong>상대방(Pod)의 인터페이스 번호는 2번</strong> (@if2)</li>
</ul>
</li>
</ul>
<p><strong><code>@ifN</code>의 진정한 의미</strong>: &quot;저 너머 네임스페이스에 있는 상대방의 인터페이스 번호&quot;</p>
<h4 id="veth-pair-시각화">veth pair 시각화</h4>
<pre><code>┌─────────────────────────────────┐       ┌──────────────────────────┐
│      Pod nettest Namespace   │       │     Host Namespace       │
│                              │       │                          │
│  1: lo (127.0.0.1)           │       │  1: lo                   │
│  2: eth0@if22 ←────────────────┼───────┼─→ 22: cali...@if2        │
│     10.244.102.137/32        │       │     (no IP)              │
│                              │       │                          │
│     ↓                        │       │     ↓                    │
│  default via 169.254.1.1     │       │  10.244.102.137 dev cali │
│                              │       │  (proxy ARP)             │
└─────────────────────────────────┘       └──────────────────────────┘</code></pre><p><strong>veth pair 특징:</strong></p>
<ol>
<li>항상 쌍으로 존재 (한쪽만 있을 수 없음)</li>
<li>한쪽 끝이 다른 네임스페이스에 있음</li>
<li>호스트 측 인터페이스는 IP가 없음 (Layer 2 bridge 역할)</li>
<li>Pod 측 인터페이스는 Pod IP 할당</li>
</ol>
<h4 id="실제-통신-흐름">실제 통신 흐름</h4>
<p>Pod에서 외부로 패킷을 보낼 때:</p>
<pre><code>1. Pod nettest (10.244.102.137)
   → &quot;10.109.60.89로 가고 싶어!&quot;

2. Pod의 라우팅 테이블 확인
   → default via 169.254.1.1 dev eth0
   → eth0@if22로 전송

3. veth pair 통과
   → 호스트의 22번 인터페이스 (calie107cf6613e)로 도착

4. 호스트 라우팅 테이블 확인
   → 10.109.60.89는 Service IP → iptables 규칙 적용
   → DNAT: 10.109.60.89 → 10.244.184.84 (실제 Pod IP)

5. 호스트 라우팅 다시 확인
   → 10.244.184.84는 cpu1 노드에 있음
   → 같은 노드이므로 다른 veth pair로 전달

6. 목적지 Pod의 veth pair 통과
   → nginx Pod에 도달!</code></pre><p><strong>중요한 발견</strong>: <code>@ifN</code> 표기는 <strong>반대편 네임스페이스의 인터페이스 번호</strong>입니다!</p>
<h4 id="pod-라우팅">Pod 라우팅</h4>
<pre><code class="language-bash">kubectl exec nettest -- ip route</code></pre>
<pre><code>default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link</code></pre><p>Calico는 모든 Pod에 동일한 gateway (169.254.1.1)를 할당하지만, 실제 라우팅은 호스트의 veth pair에서 처리합니다.</p>
<hr>
<h3 id="4-service와-endpoints-로드밸런싱의-비밀">4. Service와 Endpoints: 로드밸런싱의 비밀</h3>
<h4 id="clusterip-service-생성">ClusterIP Service 생성</h4>
<pre><code class="language-bash">kubectl create deployment nginx-test --image=nginx:alpine --replicas=3
kubectl expose deployment nginx-test --port=80 --type=ClusterIP</code></pre>
<pre><code>NAME         TYPE        CLUSTER-IP     PORT(S)
nginx-test   ClusterIP   10.109.60.89   80/TCP</code></pre><h4 id="endpoints-자동-관리">Endpoints 자동 관리</h4>
<pre><code class="language-bash">kubectl get endpoints nginx-test -o yaml</code></pre>
<pre><code class="language-yaml">subsets:
- addresses:
  - ip: 10.244.102.138
    nodeName: cpu2
  - ip: 10.244.184.84
    nodeName: cpu1
  - ip: 10.244.5.203
    nodeName: gpu1
  ports:
  - port: 80</code></pre>
<p>Service의 selector (<code>app=nginx-test</code>)와 일치하는 모든 Pod IP가 자동으로 Endpoints에 추가됩니다!</p>
<h4 id="kube-proxy의-iptables-마법">kube-proxy의 iptables 마법</h4>
<pre><code class="language-bash">iptables-save | grep nginx-test</code></pre>
<pre><code>-A KUBE-SERVICES -d 10.109.60.89/32 ... -j KUBE-SVC-W67AXLFK7VEUVN6G
-A KUBE-SVC-W67AXLFK7VEUVN6G ... --probability 0.33333 -j KUBE-SEP-SOT6P6LQ532M4OEI
-A KUBE-SVC-W67AXLFK7VEUVN6G ... --probability 0.50000 -j KUBE-SEP-3DGSCWRDZYIA2HSW
-A KUBE-SEP-SOT6P6LQ532M4OEI ... -j DNAT --to-destination 10.244.102.138:80
-A KUBE-SEP-3DGSCWRDZYIA2HSW ... -j DNAT --to-destination 10.244.184.84:80</code></pre><p><strong>동작 원리:</strong></p>
<ol>
<li>Service IP (10.109.60.89)로 들어오는 트래픽을 캡처</li>
<li>확률적으로 분산 (33%, 50%, 나머지 17%)</li>
<li>DNAT로 실제 Pod IP:Port로 변환</li>
</ol>
<p>statistic random 모듈을 사용한 간단하지만 효과적인 로드밸런싱입니다!</p>
<hr>
<h3 id="5-첫-웹-애플리케이션-배포">5. 첫 웹 애플리케이션 배포</h3>
<h4 id="configmap으로-설정-분리">ConfigMap으로 설정 분리</h4>
<pre><code class="language-bash">kubectl create configmap webapp-config --from-literal=index.html=&#39;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;Kubernetes Web App&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Welcome to Kubernetes!&lt;/h1&gt;
    &lt;p&gt;This page is served from a ConfigMap&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
&#39;</code></pre>
<h4 id="deployment에-configmap-마운트">Deployment에 ConfigMap 마운트</h4>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        configMap:
          name: webapp-config</code></pre>
<p>ConfigMap이 Volume으로 마운트되어 nginx가 해당 HTML을 서비스합니다!</p>
<h4 id="nodeport로-외부-노출">NodePort로 외부 노출</h4>
<pre><code class="language-bash">kubectl expose deployment webapp --type=NodePort --port=80</code></pre>
<pre><code>NAME     TYPE       CLUSTER-IP      PORT(S)
webapp   NodePort   10.103.193.83   80:32065/TCP</code></pre><p>모든 노드의 32065 포트로 접근 가능합니다:</p>
<pre><code class="language-bash">curl http://172.30.1.43:32065</code></pre>
<pre><code class="language-html">&lt;h1&gt;Welcome to Kubernetes!&lt;/h1&gt;
&lt;p&gt;This page is served from a ConfigMap&lt;/p&gt;</code></pre>
<p><strong>성공!</strong> 처음으로 외부에서 접근 가능한 웹 애플리케이션을 배포했습니다.</p>
<hr>
<h3 id="6-volume-컨테이너-간-데이터-공유">6. Volume: 컨테이너 간 데이터 공유</h3>
<h4 id="emptydir-실습">EmptyDir 실습</h4>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: emptydir-test
spec:
  containers:
  - name: writer
    image: busybox:1.28
    command: [&#39;sh&#39;, &#39;-c&#39;, &#39;echo &quot;Hello from EmptyDir&quot; &gt; /data/message.txt &amp;&amp; sleep 3600&#39;]
    volumeMounts:
    - name: shared-data
      mountPath: /data
  - name: reader
    image: busybox:1.28
    command: [&#39;sh&#39;, &#39;-c&#39;, &#39;sleep 10 &amp;&amp; cat /data/message.txt &amp;&amp; sleep 3600&#39;]
    volumeMounts:
    - name: shared-data
      mountPath: /data
  volumes:
  - name: shared-data
    emptyDir: {}</code></pre>
<pre><code class="language-bash">kubectl logs emptydir-test -c reader</code></pre>
<pre><code>Hello from EmptyDir</code></pre><p>writer 컨테이너가 작성한 파일을 reader 컨테이너가 성공적으로 읽었습니다. EmptyDir는 같은 Pod 내 컨테이너 간 데이터 공유에 완벽합니다!</p>
<hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="1-pod-통신은-layer-3이다-가장-큰-깨달음">1. Pod 통신은 Layer 3이다 (가장 큰 깨달음!)</h3>
<p><strong>가장 큰 오해를 바로잡았습니다.</strong></p>
<p>처음에는 &quot;Pod 통신은 Layer 2이고, 물리적으로 연결되지 않은 노드 간 통신을 위해 VXLAN으로 Layer 2를 터널링해야 한다&quot;고 잘못 생각했습니다.</p>
<p><strong>실제로는:</strong></p>
<ul>
<li>Pod 통신은 <strong>순수 Layer 3 (IP routing)</strong> 기반</li>
<li>각 Pod는 고유한 IP 주소를 가지며, 서로 다른 서브넷에 있어도 라우팅으로 통신 가능</li>
<li>VXLAN은 <strong>BGP 라우팅이 불가능할 때의 대안</strong>일 뿐</li>
</ul>
<p><strong>실전 적용:</strong></p>
<ul>
<li>BGP 가능한 환경: Calico BGP Mode 사용 (최고 성능)</li>
<li>클라우드/제한된 네트워크: VXLAN Mode</li>
<li>하이브리드 환경: CrossSubnet Mode (우리 클러스터)</li>
</ul>
<p>우리 클러스터는 모든 노드가 같은 서브넷(172.30.1.0/24)에 있어서 <strong>VXLAN을 전혀 사용하지 않고</strong> enp2s0 물리 인터페이스로 직접 라우팅합니다. <code>ip -s link show vxlan.calico</code>로 확인하면 패킷이 0개!</p>
<p>이 이해를 바탕으로 이제 네트워크 문제가 생기면:</p>
<ol>
<li>먼저 라우팅 테이블 확인 (<code>ip route | grep 10.244</code>)</li>
<li>Calico가 올바른 인터페이스 사용 중인지 확인 (<code>IP_AUTODETECTION_METHOD</code>)</li>
<li>BIRD BGP 테이블과 kernel 라우팅 테이블 비교</li>
</ol>
<h3 id="2-ifn-표기의-진정한-의미">2. <code>@ifN</code> 표기의 진정한 의미</h3>
<p>veth pair를 이해하는 데 한참 헤맸습니다.</p>
<ul>
<li>Pod: <code>2: eth0@if22</code> → &quot;내 인터페이스는 2번, 상대방은 22번&quot;</li>
<li>Host: <code>22: cali...@if2</code> → &quot;내 인터페이스는 22번, 상대방은 2번&quot;</li>
</ul>
<p><strong><code>@ifN</code>은 &quot;상대편 네임스페이스의 인터페이스 번호&quot;</strong></p>
<p>이 개념을 이해하니 Pod 네트워킹 디버깅이 훨씬 쉬워졌습니다. veth pair의 한쪽 끝을 찾으면 반대편도 바로 찾을 수 있습니다.</p>
<h3 id="3-vxlan은-선택이지-필수가-아니다">3. VXLAN은 &quot;선택&quot;이지 &quot;필수&quot;가 아니다</h3>
<p><strong>VXLAN의 역할 재정의:</strong></p>
<ul>
<li>❌ &quot;Pod 통신은 항상 VXLAN을 통해 이루어진다&quot;</li>
<li>✅ &quot;VXLAN은 BGP 라우팅이 불가능할 때 사용하는 우회 방법&quot;</li>
</ul>
<p><strong>성능 고려사항:</strong></p>
<ul>
<li>Direct routing: MTU 1500, overhead 없음, 최고 성능</li>
<li>VXLAN: MTU 1450, 50 bytes overhead, 약간의 성능 저하</li>
</ul>
<p><strong>프로덕션 환경 선택 가이드:</strong></p>
<ul>
<li>온프레미스 + BGP 가능 → Calico BGP Mode</li>
<li>AWS/GCP/Azure → VXLAN Mode (또는 Cloud provider CNI)</li>
<li>멀티 데이터센터 → VXLAN CrossSubnet Mode</li>
</ul>
<h3 id="4-kubernetes는-이벤트-기반-시스템">4. Kubernetes는 이벤트 기반 시스템</h3>
<p>Control Plane의 각 컴포넌트는:</p>
<ul>
<li>API Server를 watch</li>
<li>변경사항 감지 시 즉시 반응</li>
<li>선언적 상태(desired state)를 실제 상태(current state)로 수렴</li>
</ul>
<p>이 모델이 Kubernetes의 자동화와 자가 치유를 가능하게 합니다.</p>
<p>Deployment 하나 생성하면:</p>
<ol>
<li>API Server → etcd 저장</li>
<li>Deployment Controller → ReplicaSet 생성 감지</li>
<li>ReplicaSet Controller → Pod 생성 요청</li>
<li>Scheduler → 노드 선택</li>
<li>kubelet → 컨테이너 실행</li>
</ol>
<p>전체 과정이 1초 미만! 각 컴포넌트가 독립적으로 자기 역할만 수행하는 아름다운 설계입니다.</p>
<h3 id="5-service는-단순히-iptables-규칙이다">5. Service는 단순히 iptables 규칙이다</h3>
<p>고급 로드밸런서가 있는 줄 알았는데, kube-proxy가 생성한 iptables 규칙만으로 구현되어 있었습니다.</p>
<pre><code class="language-bash">-A KUBE-SVC-xxx ... --probability 0.33333 -j KUBE-SEP-A
-A KUBE-SVC-xxx ... --probability 0.50000 -j KUBE-SEP-B
-A KUBE-SVC-xxx ... -j KUBE-SEP-C
-A KUBE-SEP-A ... -j DNAT --to-destination 10.244.102.138:80</code></pre>
<p><strong>statistic random</strong> 모듈로 확률적 분산. 간단하지만 효과적입니다!</p>
<p>이제 Service 트래픽이 어디로 가는지 추적할 수 있습니다:</p>
<pre><code class="language-bash">iptables-save | grep &lt;service-name&gt;</code></pre>
<h3 id="6-configmap의-강력함">6. ConfigMap의 강력함</h3>
<p>코드와 설정을 완전히 분리할 수 있습니다. 같은 이미지로 dev/staging/prod 환경을 다르게 구성할 수 있습니다.</p>
<p>더 나아가:</p>
<ul>
<li>Volume으로 마운트 → 파일 형태로 사용</li>
<li>환경변수로 주입 → 애플리케이션 설정</li>
<li>ConfigMap 변경 → Pod 재시작 없이 반영 (Volume 마운트 시)</li>
</ul>
<h3 id="7-network-namespace와-linux-네트워킹의-힘">7. Network Namespace와 Linux 네트워킹의 힘</h3>
<p>각 Pod는 완전히 격리된 네트워크 환경을 가지며, veth pair가 호스트와의 유일한 연결 고리입니다.</p>
<p><strong>Linux 네트워킹 스택의 놀라운 활용:</strong></p>
<ul>
<li>Network Namespace: Pod 격리</li>
<li>veth pair: 네임스페이스 간 연결</li>
<li>Routing: Layer 3 통신</li>
<li>iptables: Service 로드밸런싱, SNAT/DNAT</li>
<li>VXLAN: 필요 시 overlay network</li>
</ul>
<p>Kubernetes는 새로운 기술을 발명한 게 아니라, 기존 Linux 커널 기능을 정교하게 조합한 것입니다. 이 점이 매우 인상적이었습니다.</p>
<hr>
<h2 id="삽질-포인트">삽질 포인트</h2>
<h3 id="대형-사고-크로스-노드-pod-통신-실패">대형 사고: 크로스 노드 Pod 통신 실패</h3>
<h4 id="증상">증상</h4>
<p>같은 노드의 Pod끼리는 통신이 되는데, 다른 노드의 Pod와는 통신이 안 되는 현상 발생:</p>
<pre><code class="language-bash">kubectl exec nettest -- ping 10.244.184.84</code></pre>
<pre><code>--- 10.244.184.84 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss</code></pre><h4 id="원인-발견-과정">원인 발견 과정</h4>
<ol>
<li><p><strong>nginx가 정상 동작하는지 확인</strong></p>
<pre><code class="language-bash">kubectl exec nginx-pod -- netstat -tlnp</code></pre>
<p>→ 80 포트 정상 listening ✅</p>
</li>
<li><p><strong>노드 간 연결 확인</strong></p>
<pre><code class="language-bash">ping 172.30.1.80  # cpu2</code></pre>
<p>→ 노드 간 ping 정상 ✅</p>
</li>
<li><p><strong>라우팅 테이블 확인</strong></p>
<pre><code class="language-bash">ip route | grep 10.244</code></pre>
<p>→ <strong>cpu2, gpu1로 가는 라우팅 규칙이 없음!</strong> ❌</p>
</li>
<li><p><strong>Calico 로그 확인</strong></p>
<pre><code class="language-bash">kubectl logs -n calico-system calico-node-vd4ls --tail=50</code></pre>
<pre><code>Interface down, will retry if it goes up. ifaceName=&quot;wlp3s0&quot;</code></pre></li>
</ol>
<p><strong>범인 발견!</strong> Calico가 WiFi 인터페이스 (wlp3s0)를 사용하려다 실패하고 있었습니다.</p>
<h4 id="해결-방법">해결 방법</h4>
<p>Calico가 Ethernet 인터페이스 (enp2s0)를 사용하도록 설정:</p>
<pre><code class="language-bash">kubectl set env daemonset/calico-node -n calico-system \
  IP_AUTODETECTION_METHOD=interface=enp.*</code></pre>
<p>cpu1과 gpu1의 calico-node Pod를 재시작:</p>
<pre><code class="language-bash">kubectl delete pod -n calico-system calico-node-vd4ls  # cpu1
kubectl delete pod -n calico-system calico-node-wnwwt  # gpu1</code></pre>
<p>재시작 후 라우팅 확인:</p>
<pre><code class="language-bash">ip route | grep 10.244</code></pre>
<pre><code>10.244.5.192/26 via 172.30.1.38 dev enp2s0 proto 80 onlink     # gpu1 ✅
10.244.102.128/26 via 172.30.1.80 dev enp2s0 proto 80 onlink   # cpu2 ✅</code></pre><p><strong>성공!</strong> enp2s0 인터페이스를 통해 라우팅되고 있습니다.</p>
<h4 id="교훈">교훈</h4>
<ol>
<li><p><strong>IP_AUTODETECTION_METHOD=first-found는 위험하다</strong></p>
<ul>
<li>노드에 여러 네트워크 인터페이스가 있으면 예상치 못한 인터페이스 선택 가능</li>
<li>명시적으로 <code>interface=enp.*</code> 또는 <code>can-reach=&lt;IP&gt;</code> 사용 권장</li>
</ul>
</li>
<li><p><strong>Calico 로그는 디버깅의 보물창고</strong></p>
<ul>
<li>Felix가 라우팅을 추가하지 못하는 이유가 명확히 나옴</li>
<li><code>kubectl logs -n calico-system calico-node-xxx</code>는 필수</li>
</ul>
</li>
<li><p><strong>BIRD BGP 테이블과 kernel 라우팅 테이블은 다르다</strong></p>
<ul>
<li>BIRD는 올바른 라우팅 정보를 가지고 있었지만</li>
<li>Felix가 kernel에 추가하지 못했음</li>
<li><code>kubectl exec -n calico-system calico-node-xxx -- birdcl show route</code>로 확인 가능</li>
</ul>
</li>
<li><p><strong>DaemonSet 업데이트가 모든 Pod를 재시작하지 않을 수 있다</strong></p>
<ul>
<li>env 변경 후 수동으로 Pod 삭제 필요했음</li>
<li><code>kubectl rollout status</code>로 확인</li>
</ul>
</li>
</ol>
<h3 id="기타-발견-사항">기타 발견 사항</h3>
<h4 id="cpu2-노드-네트워크-인터페이스-변경">cpu2 노드 네트워크 인터페이스 변경</h4>
<p>WiFi에서 유선(Ethernet)으로 인터넷 연결을 변경하면서 네트워크 인터페이스가 바뀌었습니다.</p>
<p><strong>변경 내역:</strong></p>
<ul>
<li>인터페이스: wlp3s0 (WiFi) → enp2s0 (Ethernet)</li>
<li>IP: 172.30.1.34 → 172.30.1.80 (나중에 같은 IP로 맞춤)</li>
</ul>
<p>IP는 동일하게 설정했지만, <strong>네트워크 인터페이스가 달라지면서</strong> Calico가 올바른 인터페이스를 찾지 못하는 문제가 발생했습니다. 이것이 바로 <code>IP_AUTODETECTION_METHOD=first-found</code>의 위험성입니다.</p>
<p><strong>교훈:</strong></p>
<ul>
<li>네트워크 연결 방식을 변경할 때는 Calico 설정도 함께 확인</li>
<li><code>IP_AUTODETECTION_METHOD</code>를 명시적으로 설정 (<code>interface=enp.*</code>)</li>
<li>Static IP 설정으로 일관성 유지</li>
</ul>
<h4 id="veth-pair-인터페이스-번호-혼동">veth pair 인터페이스 번호 혼동</h4>
<p>처음에는 Pod의 <code>eth0@if22</code>가 호스트의 22번 인터페이스를 의미하는 줄 알았으나, 실제로는:</p>
<ul>
<li>Pod: <code>eth0@if22</code> → 호스트의 22번 인터페이스와 연결됨</li>
<li>Host: <code>22: cali...@if2</code> → Pod의 2번 인터페이스(eth0)와 연결됨</li>
</ul>
<p><code>@ifN</code>은 <strong>상대방 쪽의 인터페이스 번호</strong>입니다!</p>
<hr>
<h2 id="다음-계획-day-3">다음 계획 (Day 3)</h2>
<p>Day 2에서 기본적인 애플리케이션 배포까지 완료했으니, Day 3에서는 <strong>운영 시나리오</strong>를 다룰 계획입니다:</p>
<h3 id="1-secret과-민감-정보-관리">1. Secret과 민감 정보 관리</h3>
<ul>
<li>ConfigMap vs Secret 차이</li>
<li>Secret을 환경변수 및 Volume으로 마운트</li>
<li>base64 인코딩의 한계와 주의사항</li>
</ul>
<h3 id="2-rolling-update와-무중단-배포">2. Rolling Update와 무중단 배포</h3>
<ul>
<li>Deployment의 롤링 업데이트 전략</li>
<li>maxSurge와 maxUnavailable 설정</li>
<li>rollout 상태 확인 및 rollback</li>
</ul>
<h3 id="3-persistent-volume-pvpvc">3. Persistent Volume (PV/PVC)</h3>
<ul>
<li>HostPath를 사용한 영구 저장소</li>
<li>PV/PVC 라이프사이클</li>
<li>StorageClass 이해</li>
</ul>
<h3 id="4-resource-limits과-hpa">4. Resource Limits과 HPA</h3>
<ul>
<li>CPU/Memory requests와 limits</li>
<li>Pod QoS 클래스 (Guaranteed, Burstable, BestEffort)</li>
<li>Horizontal Pod Autoscaler 설정</li>
</ul>
<h3 id="5-health-check-livenessreadiness-probe">5. Health Check (Liveness/Readiness Probe)</h3>
<ul>
<li>Liveness Probe: 컨테이너 재시작</li>
<li>Readiness Probe: Service Endpoints 제어</li>
<li>Startup Probe: 느린 시작 애플리케이션 처리</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>Day 2는 Day 1보다 훨씬 실전적이었습니다. Control Plane의 동작 원리를 깊이 이해하고, 네트워킹 문제를 직접 해결하면서 Calico와 Linux 네트워킹에 대한 자신감이 생겼습니다.</p>
<p>특히 &quot;Service IP로 접근이 안 된다&quot;는 문제를 만났을 때, 체계적으로 디버깅하여 원인을 찾고 해결한 경험이 가장 값졌습니다. 이제 클러스터에 문제가 생겨도 당황하지 않고 로그와 상태를 확인하며 접근할 수 있을 것 같습니다.</p>
<p>Day 3에서는 실제 프로덕션 환경에서 필요한 운영 기능들을 다뤄볼 예정입니다. Rolling Update, Health Check, Resource Limits 등 안정적인 서비스 운영을 위한 필수 요소들을 학습하겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 정복기: 3노드 클러스터로 시작하는 첫걸음 (Day 1)]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-%EC%9E%85%EB%AC%B8%EA%B8%B0-3%EB%85%B8%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%B2%AB%EA%B1%B8%EC%9D%8C-Day-1</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-%EC%9E%85%EB%AC%B8%EA%B8%B0-3%EB%85%B8%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%B2%AB%EA%B1%B8%EC%9D%8C-Day-1</guid>
            <pubDate>Wed, 29 Oct 2025 14:44:10 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>안녕하세요! 저는 최근 온프레미스 환경에서 Kubernetes 클러스터를 직접 구축하고 운영하면서 많은 것을 배우고 있습니다. 오늘은 그 여정의 첫 번째 날, <strong>&quot;클러스터 기초와 아키텍처 이해&quot;</strong>에 대해 이야기해보려 합니다.</p>
<p>처음 Kubernetes를 접할 때 가장 막막했던 부분이 <strong>&quot;도대체 이 많은 컴포넌트들이 뭐하는 건데?&quot;</strong>였습니다. Pod, Service, Deployment, etcd, CoreDNS... 용어만 들어도 머리가 복잡해지더라고요.</p>
<p>하지만 실제로 클러스터를 직접 만들고, 각 컴포넌트가 어떻게 동작하는지 하나씩 확인하면서 <strong>&quot;아, 이래서 이렇게 설계했구나!&quot;</strong> 하는 순간들이 있었습니다. 오늘 그 경험을 공유하고자 합니다.</p>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-%EC%9A%B0%EB%A6%AC-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EC%86%8C%EA%B0%9C">우리 클러스터 소개</a></li>
<li><a href="#2-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%9D%98-%EC%8B%AC%EC%9E%A5-control-plane">클러스터의 심장: Control Plane</a></li>
<li><a href="#3-pod%EC%99%80-%EB%84%A4%EC%9E%84%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%9D%98-%EB%B9%84%EB%B0%80">Pod와 네임스페이스의 비밀</a></li>
<li><a href="#4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9%EC%9D%98-%EB%A7%88%EB%B2%95-coredns">네트워킹의 마법: CoreDNS</a></li>
<li><a href="#5-%EB%B0%B0%EC%9A%B4-%EA%B2%83%EA%B3%BC-%EB%8B%A4%EC%9D%8C-%EA%B3%84%ED%9A%8D">배운 것과 다음 계획</a></li>
</ol>
<hr>
<h2 id="1-클러스터-소개">1. 클러스터 소개</h2>
<h3 id="클러스터-구성">클러스터 구성</h3>
<p>저는 총 <strong>3개의 노드</strong>로 클러스터를 구성했습니다:</p>
<pre><code class="language-bash">$ kubectl get nodes -o wide</code></pre>
<pre><code>NAME   STATUS   ROLES           AGE   VERSION    INTERNAL-IP   OS-IMAGE             CONTAINER-RUNTIME
cpu1   Ready    control-plane   46h   v1.31.13   172.30.1.43   Ubuntu 22.04.5 LTS   containerd://1.7.28
cpu2   Ready    &lt;none&gt;          17h   v1.31.13   172.30.1.80   Ubuntu 22.04.5 LTS   containerd://1.7.28
gpu1   Ready    &lt;none&gt;          17h   v1.31.13   172.30.1.38   Ubuntu 22.04.5 LTS   containerd://1.7.28</code></pre><table>
<thead>
<tr>
<th>노드</th>
<th>역할</th>
<th>스펙</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>cpu1</strong></td>
<td>Master + Worker</td>
<td>12코어, 7.5GB</td>
<td>마스터 노드지만 taint 제거로 워커로도 사용</td>
</tr>
<tr>
<td><strong>cpu2</strong></td>
<td>Worker</td>
<td>8코어, 16GB</td>
<td>일반 워크로드 실행</td>
</tr>
<tr>
<td><strong>gpu1</strong></td>
<td>Worker</td>
<td>12코어, 16GB</td>
<td>GPU 워크로드용 (향후 활용 예정)</td>
</tr>
</tbody></table>
<p><strong>여기서 포인트!</strong> </p>
<p>보통 Master 노드는 Control Plane 컴포넌트만 실행하고 일반 Pod는 실행하지 않습니다. 하지만 저는 리소스 활용을 위해 <strong>cpu1의 taint를 제거</strong>했습니다.</p>
<pre><code class="language-bash">$ kubectl describe node cpu1 | grep Taints
Taints:             &lt;none&gt;</code></pre>
<p><code>&lt;none&gt;</code>이 보이시나요? 이제 cpu1에도 일반 애플리케이션 Pod를 배포할 수 있습니다!</p>
<h3 id="클러스터-정보-확인">클러스터 정보 확인</h3>
<p>가장 먼저 해본 명령어는 이거였습니다:</p>
<pre><code class="language-bash">$ kubectl cluster-info</code></pre>
<pre><code>Kubernetes control plane is running at https://172.30.1.43:6443
CoreDNS is running at https://172.30.1.43:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy</code></pre><p><strong>간단하지만 중요한 정보:</strong></p>
<ul>
<li>API Server 주소: <code>https://172.30.1.43:6443</code> - 모든 kubectl 명령어가 여기로 갑니다</li>
<li>CoreDNS: 클러스터 내부 DNS 서비스 - Pod들이 서로를 찾을 수 있게 해줍니다</li>
</ul>
<hr>
<h2 id="2-클러스터의-심장-control-plane">2. 클러스터의 심장: Control Plane</h2>
<h3 id="control-plane이-뭐길래">Control Plane이 뭐길래?</h3>
<p>Kubernetes 클러스터를 사람에 비유하면, Control Plane은 <strong>&quot;뇌&quot;</strong>입니다. 모든 결정이 여기서 이루어지죠.</p>
<p>Control Plane은 4개의 핵심 컴포넌트로 구성됩니다:</p>
<pre><code class="language-bash">$ kubectl get pods -n kube-system -o wide | grep cpu1</code></pre>
<pre><code>etcd-cpu1                      1/1     Running   172.30.1.43   cpu1
kube-apiserver-cpu1            1/1     Running   172.30.1.43   cpu1
kube-controller-manager-cpu1   1/1     Running   172.30.1.43   cpu1
kube-scheduler-cpu1            1/1     Running   172.30.1.43   cpu1</code></pre><p>모두 <code>cpu1</code> (마스터 노드)에서 실행되고 있습니다. 각각의 역할을 알아봅시다:</p>
<h4 id="1-etcd---기억하는-자">1. etcd - &quot;기억하는 자&quot;</h4>
<pre><code class="language-bash">$ kubectl get componentstatuses</code></pre>
<pre><code>NAME                 STATUS    MESSAGE   ERROR
etcd-0               Healthy   ok</code></pre><ul>
<li><strong>역할</strong>: 클러스터의 모든 데이터를 저장하는 key-value 저장소</li>
<li><strong>저장하는 것</strong>: Pod 정보, Service 설정, ConfigMap, Secret 등 모든 것!</li>
<li><strong>중요성</strong>: etcd가 죽으면 클러스터 전체가 멈춥니다 😱</li>
</ul>
<p><strong>팁:</strong> 프로덕션 환경에서는 etcd를 반드시 백업하세요. 이게 바로 클러스터의 &quot;두뇌 백업&quot;입니다.</p>
<h4 id="2-kube-apiserver---중앙-통제소">2. kube-apiserver - &quot;중앙 통제소&quot;</h4>
<ul>
<li><strong>역할</strong>: 모든 요청을 받아서 처리하는 REST API 서버</li>
<li><strong>포트</strong>: 6443 (HTTPS)</li>
<li><strong>특징</strong>:<ul>
<li>kubectl 명령어가 통신하는 대상</li>
<li>인증, 인가, Admission Control 수행</li>
<li>etcd와 직접 통신하는 유일한 컴포넌트</li>
</ul>
</li>
</ul>
<p><strong>내가 깨달은 것:</strong></p>
<pre><code>kubectl get pods
      ↓
API Server (172.30.1.43:6443)
      ↓
etcd에서 Pod 정보 조회
      ↓
결과 반환</code></pre><p>모든 작업이 API Server를 거친다는 게 핵심입니다!</p>
<h4 id="3-kube-controller-manager---자동화-담당자">3. kube-controller-manager - &quot;자동화 담당자&quot;</h4>
<ul>
<li><strong>역할</strong>: 클러스터의 &quot;바라는 상태&quot;를 유지</li>
<li><strong>예시</strong>:<ul>
<li>Deployment가 &quot;Pod 3개 실행&quot;을 원하면 → 계속 3개 유지</li>
<li>Node가 죽으면 → 해당 노드의 Pod를 다른 노드로 재생성</li>
<li>Service Endpoint 자동 관리</li>
</ul>
</li>
</ul>
<p><strong>실제로 본 예시:</strong></p>
<p>제가 테스트로 Pod를 하나 삭제했을 때:</p>
<pre><code class="language-bash">$ kubectl delete pod coredns-76b86bc878-5v86m -n kube-system
pod &quot;coredns-76b86bc878-5v86m&quot; deleted

$ kubectl get pods -n kube-system | grep coredns
coredns-76b86bc878-5v86m   1/1     Running   0          5s  &lt;- 자동 재생성됨!
coredns-76b86bc878-pnpjq   1/1     Running   0          46h</code></pre>
<p>5초 만에 새 Pod가 생성되었습니다. 이게 바로 Controller Manager의 마법입니다! ✨</p>
<h4 id="4-kube-scheduler---배치-전문가">4. kube-scheduler - &quot;배치 전문가&quot;</h4>
<ul>
<li><strong>역할</strong>: 새로운 Pod를 &quot;어느 노드에 배치할지&quot; 결정</li>
<li><strong>고려 사항</strong>:<ul>
<li>노드의 여유 리소스 (CPU, 메모리)</li>
<li>nodeSelector, affinity 규칙</li>
<li>Taints와 Tolerations</li>
</ul>
</li>
</ul>
<p><strong>재미있는 발견:</strong></p>
<pre><code class="language-bash">$ kubectl get pods -A -o wide | grep -c cpu1
12

$ kubectl get pods -A -o wide | grep -c cpu2
3</code></pre>
<p>cpu1에 Pod가 더 많은 이유? Control Plane Pod 4개 + CoreDNS 2개 + 기타 시스템 Pod들이 모두 cpu1에 있기 때문입니다!</p>
<hr>
<h2 id="3-pod와-네임스페이스의-비밀">3. Pod와 네임스페이스의 비밀</h2>
<h3 id="총-19개의-pod가-돌아가는-중">총 19개의 Pod가 돌아가는 중</h3>
<pre><code class="language-bash">$ kubectl get pods -A | wc -l
20  # 헤더 포함</code></pre>
<p>처음엔 <strong>&quot;19개나?&quot;</strong>라고 놀랐습니다. 하지만 알고 보니 모두 필요한 시스템 Pod들이었어요.</p>
<h4 id="pod-종류별-분류">Pod 종류별 분류</h4>
<p><strong>1. Control Plane Pod (4개)</strong> - Static Pod</p>
<pre><code>etcd-cpu1
kube-apiserver-cpu1
kube-controller-manager-cpu1
kube-scheduler-cpu1</code></pre><p><strong>여기서 발견!</strong> 이들은 <code>/etc/kubernetes/manifests/</code>에 YAML 파일로 정의되어 있습니다:</p>
<pre><code class="language-bash">$ ls /etc/kubernetes/manifests/
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml</code></pre>
<p>kubelet이 이 디렉토리를 감시하다가 파일이 있으면 자동으로 Pod를 실행합니다. <strong>API Server가 없어도 실행된다는 점</strong>이 신기했어요!</p>
<p><strong>2. DaemonSet Pod (9개)</strong> - 각 노드마다 1개씩</p>
<pre><code class="language-bash">$ kubectl get daemonset -A</code></pre>
<pre><code>NAMESPACE       NAME              DESIRED   CURRENT   READY
calico-system   calico-node       3         3         3
calico-system   csi-node-driver   3         3         3
kube-system     kube-proxy        3         3         3</code></pre><p>각 노드마다 정확히 1개씩! 이게 DaemonSet의 핵심입니다.</p>
<ul>
<li><code>calico-node</code>: 네트워크 에이전트</li>
<li><code>kube-proxy</code>: 서비스 라우팅</li>
<li><code>csi-node-driver</code>: 스토리지 드라이버</li>
</ul>
<p><strong>3. Deployment Pod (6개)</strong> - 복제 가능</p>
<pre><code class="language-bash">$ kubectl get deployment -A</code></pre>
<pre><code>NAMESPACE         NAME                      READY   UP-TO-DATE   AVAILABLE
calico-system     calico-kube-controllers   1/1     1            1
calico-system     calico-typha              2/2     2            2
kube-system       coredns                   2/2     2            2
tigera-operator   tigera-operator           1/1     1            1</code></pre><p>CoreDNS가 <strong>2개</strong>인 이유? 고가용성(HA)을 위해서입니다. 하나가 죽어도 다른 하나가 서비스를 계속합니다!</p>
<h3 id="네임스페이스는-방">네임스페이스는 &quot;방&quot;</h3>
<pre><code class="language-bash">$ kubectl get namespaces</code></pre>
<pre><code>NAME              STATUS   AGE
default           Active   46h  &lt;- 기본 네임스페이스
kube-system       Active   46h  &lt;- Kubernetes 시스템
calico-system     Active   46h  &lt;- Calico 네트워크
tigera-operator   Active   46h  &lt;- Calico 운영자
kube-public       Active   46h  &lt;- 공개 리소스
kube-node-lease   Active   46h  &lt;- 노드 하트비트</code></pre><p>네임스페이스를 <strong>&quot;아파트의 각 집&quot;</strong>으로 생각하면 이해하기 쉽습니다:</p>
<ul>
<li>논리적으로 격리됨</li>
<li>각자 독립적인 리소스 관리</li>
<li>하지만 필요하면 서로 통신 가능</li>
</ul>
<p><strong>실용 팁:</strong></p>
<pre><code class="language-bash"># 특정 네임스페이스 조회
$ kubectl get pods -n kube-system

# 모든 네임스페이스 조회
$ kubectl get pods -A

# 기본 네임스페이스 변경 (매번 -n 안 써도 됨)
$ kubectl config set-context --current --namespace=kube-system</code></pre>
<hr>
<h2 id="4-네트워킹의-마법-coredns">4. 네트워킹의 마법: CoreDNS</h2>
<h3 id="3가지-네트워크가-공존한다">3가지 네트워크가 공존한다</h3>
<p>처음 Pod IP를 봤을 때 혼란스러웠습니다:</p>
<pre><code class="language-bash">$ kubectl get pods -A -o wide</code></pre>
<pre><code>NAMESPACE     NAME                  IP               NODE
kube-system   coredns-...           10.244.184.81    cpu1   &lt;- Pod IP
kube-system   kube-proxy-...        172.30.1.43      cpu1   &lt;- Node IP</code></pre><p><strong>왜 IP가 다른가요?</strong></p>
<p>우리 클러스터에는 <strong>3가지 네트워크</strong>가 있습니다:</p>
<pre><code>1. Node Network: 172.30.1.0/24
   - 실제 서버들의 IP
   - 예: 172.30.1.43 (cpu1)

2. Pod Network: 10.244.0.0/16
   - Calico가 할당하는 Pod 전용 IP
   - 예: 10.244.184.81 (coredns)

3. Service Network: 10.96.0.0/12
   - 가상 IP (실제로는 존재하지 않음!)
   - 예: 10.96.0.10 (CoreDNS Service)</code></pre><p><strong>Calico 네트워크 확인:</strong></p>
<pre><code class="language-bash">$ kubectl get ippool</code></pre>
<pre><code class="language-yaml">spec:
  cidr: 10.244.0.0/16
  blockSize: 26
  vxlanMode: CrossSubnet
  natOutgoing: true</code></pre>
<p><strong>핵심 설정:</strong></p>
<ul>
<li><code>vxlanMode: CrossSubnet</code> - 같은 서브넷은 직접, 다른 서브넷은 VXLAN 터널 사용</li>
<li><code>blockSize: 26</code> - 각 노드에 /26 블록 할당 (62개 IP 사용 가능)</li>
</ul>
<h3 id="dns-테스트---드디어-성공">DNS 테스트 - 드디어 성공!</h3>
<p>가장 신나는 순간이었습니다. DNS가 정말 작동하는지 직접 테스트했어요:</p>
<pre><code class="language-bash">$ kubectl run test-dns --image=busybox:1.28 --rm -i --restart=Never -- sh -c &quot;
  nslookup kubernetes.default &amp;&amp;
  nslookup google.com
&quot;</code></pre>
<p><strong>결과:</strong></p>
<pre><code>Server:    10.96.0.10
Address 1: 10.96.0.10

Name:      kubernetes.default
Address 1: 10.96.0.1 kubernetes.default.svc.cluster.local

Name:      google.com
Address 1: 142.250.206.238</code></pre><p>✅ <strong>성공!</strong></p>
<p><strong>무슨 일이 일어난 걸까요?</strong></p>
<pre><code>1. Pod 생성됨
   └─ /etc/resolv.conf에 nameserver 10.96.0.10 자동 설정

2. &quot;kubernetes.default&quot; 조회 요청
   └─ CoreDNS (10.96.0.10)가 받음
   └─ &quot;kubernetes&quot; Service를 찾아서 10.96.0.1 반환

3. &quot;google.com&quot; 조회 요청
   └─ CoreDNS가 받음
   └─ 클러스터 내부에 없으니 상위 DNS (168.126.63.1)로 포워딩
   └─ Google IP 반환</code></pre><p><strong>DNS Search Domain의 마법:</strong></p>
<p>Pod의 <code>/etc/resolv.conf</code>를 보면:</p>
<pre><code>nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local</code></pre><p>이 설정 덕분에:</p>
<ul>
<li><code>nginx</code> → <code>nginx.default.svc.cluster.local</code> 자동 확장</li>
<li><code>kube-dns.kube-system</code> → <code>kube-dns.kube-system.svc.cluster.local</code> 자동 확장</li>
</ul>
<p><strong>실용 예시:</strong></p>
<p>같은 네임스페이스의 서비스 호출:</p>
<pre><code class="language-bash">curl nginx         # ✅ 작동
curl nginx:80      # ✅ 작동</code></pre>
<p>다른 네임스페이스의 서비스 호출:</p>
<pre><code class="language-bash">curl nginx.default              # ✅ 작동
curl nginx.default.svc          # ✅ 작동
curl nginx.default.svc.cluster.local  # ✅ 작동 (전체 FQDN)</code></pre>
<hr>
<h2 id="5-배운-것과-다음-계획">5. 배운 것과 다음 계획</h2>
<h3 id="day-1에서-배운-핵심">Day 1에서 배운 핵심</h3>
<ol>
<li><p><strong>Kubernetes는 선언적(Declarative) 시스템이다</strong></p>
<ul>
<li>&quot;이렇게 되어야 한다&quot;를 선언하면</li>
<li>Controller Manager가 알아서 그 상태를 유지</li>
</ul>
</li>
<li><p><strong>모든 것은 API Server를 거친다</strong></p>
<ul>
<li>kubectl, Controller, Scheduler 모두 API Server와 통신</li>
<li>etcd와 직접 통신하는 건 API Server뿐</li>
</ul>
</li>
<li><p><strong>네트워크는 3개 레이어로 분리</strong></p>
<ul>
<li>Node Network (물리)</li>
<li>Pod Network (Calico)</li>
<li>Service Network (가상)</li>
</ul>
</li>
<li><p><strong>DNS는 Kubernetes의 핵심</strong></p>
<ul>
<li>없으면 Pod들이 서로를 못 찾음</li>
<li>CoreDNS는 클러스터 + 외부 DNS 모두 처리</li>
</ul>
</li>
</ol>
<h3 id="유용했던-명령어-top-5">유용했던 명령어 Top 5</h3>
<pre><code class="language-bash"># 1. 전체 리소스 한눈에 보기
kubectl get all -A

# 2. 노드별 Pod 개수 확인
kubectl get pods -A -o wide | awk &#39;{print $8}&#39; | sort | uniq -c

# 3. 특정 Label을 가진 Pod만 조회
kubectl get pods -l app=nginx

# 4. 리소스 상세 정보 (문제 해결에 필수!)
kubectl describe pod &lt;pod-name&gt; -n &lt;namespace&gt;

# 5. 실시간 로그 확인
kubectl logs -f &lt;pod-name&gt; -n &lt;namespace&gt;</code></pre>
<h2 id="마치며">마치며</h2>
<p>Kubernetes, 처음엔 정말 어려웠습니다. 용어도 생소하고, 개념도 복잡하고...</p>
<p>하지만 <strong>직접 클러스터를 만들고, 하나씩 확인하면서</strong> 점점 이해가 되기 시작했어요. 특히 DNS 테스트가 성공했을 때의 그 기쁨이란!</p>
<p>여러분도 처음엔 막막하실 수 있습니다. 하지만 포기하지 마세요. 하나씩 차근차근 따라가다 보면 어느새 &quot;아, 이래서 Kubernetes를 쓰는구나!&quot;하는 순간이 올 겁니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu 네트워크 관리 완벽 이해: NetworkManager, systemd-networkd, 그리고 Netplan의 관계]]></title>
            <link>https://velog.io/@arnold_99/Ubuntu-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B4%80%EB%A6%AC-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4-NetworkManager-systemd-networkd-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Netplan%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@arnold_99/Ubuntu-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B4%80%EB%A6%AC-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4-NetworkManager-systemd-networkd-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Netplan%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Tue, 28 Oct 2025 20:29:14 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>Ubuntu에서 네트워크 설정을 변경하다가 예상치 못한 문제를 겪어본 경험이 있으신가요? 저는 최근 Ubuntu Desktop에서 Kubernetes 클러스터를 구성하면서 DNS 설정이 적용되지 않는 문제로 몇 시간을 씨름했습니다. 이 과정에서 Ubuntu의 네트워크 관리 체계를 깊이 이해하게 되었고, 같은 문제로 고민하실 분들을 위해 이 글을 작성합니다.</p>
<h2 id="ubuntu의-네트워크-관리-체계">Ubuntu의 네트워크 관리 체계</h2>
<h3 id="ubuntu-버전별-기본-네트워크-렌더러">Ubuntu 버전별 기본 네트워크 렌더러</h3>
<p>Ubuntu는 버전과 에디션에 따라 다른 네트워크 관리 도구를 사용합니다:</p>
<ul>
<li><strong>Ubuntu Desktop (18.04 이후)</strong>: NetworkManager가 기본</li>
<li><strong>Ubuntu Server (18.04 이후)</strong>: systemd-networkd가 기본</li>
<li><strong>Ubuntu 17.10 이전</strong>: ifupdown 사용</li>
</ul>
<p>Ubuntu 22.04 Desktop을 사용 중이라면, NetworkManager가 기본 네트워크 렌더러로 설정되어 있습니다. 다음 명령어로 확인할 수 있습니다:</p>
<pre><code class="language-bash"># 현재 활성화된 네트워크 서비스 확인
systemctl status NetworkManager
systemctl status systemd-networkd

# netplan 렌더러 확인
cat /etc/netplan/*.yaml | grep renderer</code></pre>
<h3 id="networkmanager-vs-systemd-networkd-핵심-차이점">NetworkManager vs systemd-networkd: 핵심 차이점</h3>
<h4 id="networkmanager">NetworkManager</h4>
<ul>
<li><strong>대상</strong>: 주로 데스크톱 환경</li>
<li><strong>특징</strong>:<ul>
<li>GUI 지원 (nm-applet, GNOME 설정 등)</li>
<li>Wi-Fi, VPN, 모바일 브로드밴드 등 다양한 연결 타입 지원</li>
<li>동적 네트워크 환경에 최적화</li>
<li>D-Bus를 통한 애플리케이션 통합</li>
<li>연결 프로파일 기반 관리 (Connection 개념)</li>
</ul>
</li>
</ul>
<h4 id="systemd-networkd">systemd-networkd</h4>
<ul>
<li><strong>대상</strong>: 주로 서버 환경</li>
<li><strong>특징</strong>:<ul>
<li>경량화된 디자인</li>
<li>정적 네트워크 구성에 최적화</li>
<li>systemd 생태계와 긴밀한 통합</li>
<li>설정 파일 기반 (/etc/systemd/network/)</li>
<li>컨테이너 환경에 적합</li>
</ul>
</li>
</ul>
<h2 id="netplan-통합-네트워크-설정-추상화-계층">Netplan: 통합 네트워크 설정 추상화 계층</h2>
<h3 id="netplan의-역할과-작동-원리">Netplan의 역할과 작동 원리</h3>
<p>Netplan은 Ubuntu 17.10부터 도입된 네트워크 설정 추상화 도구입니다. 중요한 점은 <strong>Netplan이 직접 네트워크를 관리하지 않는다</strong>는 것입니다.</p>
<pre><code>┌─────────────────────────────┐
│   사용자가 작성하는 YAML     │
│  /etc/netplan/*.yaml        │
└──────────┬──────────────────┘
           │ netplan generate/apply
           ├─────────────┬────────────┐
           ↓             ↓            ↓
┌──────────────┐ ┌────────────┐ ┌──────────────┐
│NetworkManager│ │  networkd  │ │   기타...    │
│   (Desktop)  │ │  (Server)  │ │              │
└──────────────┘ └────────────┘ └──────────────┘</code></pre><p>Netplan의 주요 장점:</p>
<ul>
<li>통일된 YAML 형식으로 네트워크 설정</li>
<li>렌더러 전환이 간단 (renderer: NetworkManager/networkd)</li>
<li>선언적 설정 방식</li>
<li>버전 관리에 용이</li>
</ul>
<h2 id="실제-사례-k8s-환경에서-dns-설정-충돌-문제">실제 사례: K8s 환경에서 DNS 설정 충돌 문제</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>Ubuntu Desktop에 Kubernetes를 설치하고, CoreDNS(10.96.0.10)를 시스템 DNS에 추가하려고 했습니다.</p>
<pre><code class="language-yaml"># /etc/netplan/01-static-wifi.yaml
network:
  version: 2
  renderer: NetworkManager
  wifis:
    wlp2s0:
      access-points:
        &quot;KT_GiGA_3AD1&quot;:
          password: &quot;********&quot;
      dhcp4: true
      nameservers:
        addresses: [10.96.0.10, 168.126.63.1, 168.126.63.2]</code></pre>
<p><code>sudo netplan apply</code>를 실행했지만, DNS 설정이 반영되지 않았습니다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p>문제의 핵심은 NetworkManager의 <strong>Connection</strong> 개념을 이해하지 못한 것이었습니다.</p>
<pre><code class="language-bash"># 연결 상태 확인
$ nmcli connection show
NAME                           UUID                                  TYPE      DEVICE 
KT_GiGA_3AD1                   xxxx-xxxx-xxxx-xxxx                  wifi      wlp2s0
netplan-wlp2s0-KT_GiGA_3AD1   yyyy-yyyy-yyyy-yyyy                  wifi      --</code></pre>
<p>두 개의 Connection이 존재했고, 기존 수동 연결이 활성화되어 있어 Netplan이 생성한 연결이 무시되었습니다.</p>
<h3 id="networkmanager-connection의-이해">NetworkManager Connection의 이해</h3>
<p>NetworkManager에서 Connection은 네트워크 설정 프로파일입니다:</p>
<ul>
<li>하나의 인터페이스에 여러 Connection 존재 가능</li>
<li>한 번에 하나의 Connection만 활성화</li>
<li>우선순위: 이미 활성화된 연결 &gt; 새로운 연결</li>
</ul>
<h3 id="해결-방법">해결 방법</h3>
<h4 id="방법-1-기존-connection-삭제-후-netplan-연결-활성화">방법 1: 기존 Connection 삭제 후 Netplan 연결 활성화</h4>
<pre><code class="language-bash"># 기존 수동 연결 삭제
sudo nmcli connection delete &quot;KT_GiGA_3AD1&quot;

# Netplan 재적용
sudo netplan apply

# Netplan이 생성한 연결 활성화
sudo nmcli connection up &quot;netplan-wlp2s0-KT_GiGA_3AD1&quot;

# DNS 확인
resolvectl status wlp2s0</code></pre>
<h4 id="방법-2-기존-connection-직접-수정">방법 2: 기존 Connection 직접 수정</h4>
<pre><code class="language-bash"># NetworkManager를 통한 직접 수정
sudo nmcli connection modify &quot;KT_GiGA_3AD1&quot; \
  ipv4.dns &quot;10.96.0.10 168.126.63.1 168.126.63.2&quot;

sudo nmcli connection reload
sudo nmcli connection up &quot;KT_GiGA_3AD1&quot;</code></pre>
<h4 id="방법-3-자동-dns-무시-설정">방법 3: 자동 DNS 무시 설정</h4>
<pre><code class="language-bash"># DHCP DNS 무시하고 수동 DNS만 사용
sudo nmcli connection modify &quot;KT_GiGA_3AD1&quot; \
  ipv4.ignore-auto-dns yes \
  ipv4.dns &quot;10.96.0.10 168.126.63.1 168.126.63.2&quot;</code></pre>
<h2 id="베스트-프랙티스">베스트 프랙티스</h2>
<h3 id="1-환경에-맞는-렌더러-선택">1. 환경에 맞는 렌더러 선택</h3>
<pre><code class="language-yaml">network:
  version: 2
  renderer: NetworkManager  # Desktop 환경
  # renderer: networkd     # Server 환경</code></pre>
<h3 id="2-netplan-전용-시스템-구성">2. Netplan 전용 시스템 구성</h3>
<p>새로운 시스템에서는 처음부터 Netplan으로만 관리:</p>
<pre><code class="language-bash"># 기존 수동 연결 모두 삭제
nmcli connection show | grep -v &quot;netplan-&quot; | awk &#39;NR&gt;1 {print $1}&#39; | \
  xargs -I {} sudo nmcli connection delete &quot;{}&quot;</code></pre>
<h3 id="3-설정-변경-전-백업">3. 설정 변경 전 백업</h3>
<pre><code class="language-bash"># Netplan 설정 백업
sudo cp -r /etc/netplan /etc/netplan.backup

# NetworkManager 연결 백업
sudo cp -r /etc/NetworkManager/system-connections \
  /etc/NetworkManager/system-connections.backup</code></pre>
<h3 id="4-디버깅-팁">4. 디버깅 팁</h3>
<pre><code class="language-bash"># Netplan 설정 검증 (실제 적용하지 않음)
sudo netplan try

# 생성될 설정 파일 미리보기
sudo netplan generate --debug

# NetworkManager 로그 확인
journalctl -u NetworkManager -f

# 현재 DNS 설정 확인
resolvectl status
nmcli device show | grep DNS</code></pre>
<h2 id="결론">결론</h2>
<p>Ubuntu의 네트워크 관리 체계는 처음에는 복잡해 보이지만, 각 구성 요소의 역할을 이해하면 매우 논리적입니다:</p>
<ol>
<li><strong>Netplan</strong>은 설정을 추상화하는 &quot;번역기&quot;</li>
<li><strong>NetworkManager</strong>는 Desktop에서 실제 네트워크를 관리하는 &quot;실행자&quot;</li>
<li><strong>systemd-networkd</strong>는 Server에서 같은 역할을 수행</li>
</ol>
<p>제가 겪은 문제는 Netplan과 NetworkManager의 관계를 정확히 이해하지 못해 발생했습니다. 특히 NetworkManager의 Connection 개념과 우선순위를 알지 못해 몇 시간을 헤맸죠.</p>
<p>핵심 교훈:</p>
<ul>
<li>Netplan을 사용할 때는 기존 수동 설정과의 충돌을 확인</li>
<li>하나의 인터페이스는 하나의 활성 Connection만 가능</li>
<li>Desktop 환경에서는 NetworkManager의 특성을 이해하고 활용</li>
</ul>
<p>이 글이 Ubuntu 네트워크 설정으로 고민하시는 분들께 도움이 되기를 바랍니다. 특히 Kubernetes나 컨테이너 환경을 구성하면서 DNS 설정 문제를 겪으시는 분들에게 실질적인 해결책이 되었으면 좋겠습니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes BGP 완벽 가이드: 언제, 왜, 어떻게?]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-BGP-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%96%B8%EC%A0%9C-%EC%99%9C-%EC%96%B4%EB%96%BB%EA%B2%8C</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-BGP-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%96%B8%EC%A0%9C-%EC%99%9C-%EC%96%B4%EB%96%BB%EA%B2%8C</guid>
            <pubDate>Mon, 27 Oct 2025 16:44:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🤔 &quot;클러스터 내부는 CNI가 알아서 하는데, BGP는 왜 필요하죠?&quot; </p>
<p>Kubernetes CNI와 BGP의 경계를 명확히 이해하고, 실전에서 BGP가 필요한 순간을 알아봅시다.</p>
</blockquote>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#bgp%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">BGP란 무엇인가?</a></li>
<li><a href="#cni-vs-bgp-%EC%97%AD%ED%95%A0-%EA%B5%AC%EB%B6%84">CNI vs BGP: 역할 구분</a></li>
<li><a href="#bgp%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%88%9C%EA%B0%84">BGP가 필요한 순간</a></li>
<li><a href="#bgp-%EC%84%A4%EC%A0%95-%EC%99%84%EC%A0%84-%EB%B6%84%EC%84%9D">BGP 설정 완전 분석</a></li>
<li><a href="#%EC%8B%A4%EC%A0%84-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4">실전 시나리오</a></li>
<li><a href="#%EB%9D%BC%EC%9A%B0%ED%84%B0%EC%99%80-%EC%8A%A4%EC%9C%84%EC%B9%98">라우터와 스위치</a></li>
<li><a href="#as-number-%EA%B4%80%EB%A6%AC">AS Number 관리</a></li>
<li><a href="#%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91">장애 대응</a></li>
<li><a href="#faq">FAQ</a></li>
</ol>
<hr>
<h2 id="bgp란-무엇인가">BGP란 무엇인가?</h2>
<h3 id="bgp-border-gateway-protocol">BGP (Border Gateway Protocol)</h3>
<p><strong>BGP</strong>는 인터넷의 백본을 구성하는 라우팅 프로토콜입니다. 서로 다른 네트워크(Autonomous System) 간에 &quot;어떤 IP 주소가 어디에 있는지&quot; 정보를 교환합니다.</p>
<h4 id="🏢-현실-세계-비유">🏢 현실 세계 비유</h4>
<pre><code>우편 시스템:
  - 서울 우체국: &quot;06xxx 우편번호는 우리가 관리해요!&quot;
  - 부산 우체국: &quot;48xxx 우편번호는 우리가 관리해요!&quot;

BGP:
  - 네이버 네트워크: &quot;210.89.160.0/24 IP는 우리 네트워크예요!&quot;
  - 카카오 네트워크: &quot;211.249.220.0/24 IP는 우리 네트워크예요!&quot;</code></pre><h4 id="🌐-인터넷에서의-bgp">🌐 인터넷에서의 BGP</h4>
<pre><code>사용자가 www.google.com 접속 시:

1. 사용자 ISP: &quot;142.250.x.x는 어디로 보내지?&quot;
2. BGP 조회: &quot;Google AS (15169)로 보내!&quot;
3. 최적 경로 선택: SKT → 해외 회선 → Google
4. 페이지 로딩 완료</code></pre><hr>
<h2 id="cni-vs-bgp-역할-구분">CNI vs BGP: 역할 구분</h2>
<h3 id="🎯-핵심-차이점">🎯 핵심 차이점</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>CNI</th>
<th>BGP</th>
</tr>
</thead>
<tbody><tr>
<td><strong>범위</strong></td>
<td>클러스터 내부</td>
<td>클러스터 외부</td>
</tr>
<tr>
<td><strong>역할</strong></td>
<td>Pod 간 네트워킹</td>
<td>네트워크 간 라우팅</td>
</tr>
<tr>
<td><strong>필요성</strong></td>
<td>항상 필수</td>
<td>특수한 경우만</td>
</tr>
<tr>
<td><strong>설정 주체</strong></td>
<td>DevOps</td>
<td>DevOps + 네트워크팀</td>
</tr>
</tbody></table>
<h3 id="cni의-역할-클러스터-내부">CNI의 역할 (클러스터 내부)</h3>
<pre><code>┌─────────────────────────────────────┐
│     Kubernetes Cluster              │
│                                     │
│  Pod A (10.244.1.5)                │
│     ↓                              │
│  CNI가 자동 라우팅                  │
│     ↓                              │
│  Pod B (10.244.2.10)               │
│                                     │
│  ✅ BGP 불필요!                     │
└─────────────────────────────────────┘</code></pre><p><strong>CNI가 자동으로 처리하는 것:</strong></p>
<ul>
<li>Pod IP 할당</li>
<li>노드 간 라우팅</li>
<li>네트워크 정책 (Calico의 경우)</li>
<li>Service 네트워킹</li>
</ul>
<h3 id="bgp의-역할-클러스터-외부">BGP의 역할 (클러스터 외부)</h3>
<pre><code>┌─────────────────────┐      ┌─────────────────────┐
│  K8s Cluster A      │      │  K8s Cluster B      │
│  (서울)             │      │  (도쿄)             │
│  10.244.0.0/16      │      │  10.245.0.0/16      │
└──────────┬──────────┘      └──────────┬──────────┘
           │                            │
           └────────┬───────────────────┘
                    │ BGP 필요!
           ┌────────▼────────┐
           │  Global Router  │
           │  &quot;경로 알려줘!&quot; │
           └─────────────────┘</code></pre><p><strong>BGP가 필요한 이유:</strong></p>
<ul>
<li>서로 다른 네트워크 연결</li>
<li>경로 정보 교환</li>
<li>자동 장애 대응</li>
<li>최적 경로 선택</li>
</ul>
<hr>
<h2 id="bgp가-필요한-순간">BGP가 필요한 순간</h2>
<h3 id="✅-case-1-멀티-클러스터">✅ Case 1: 멀티 클러스터</h3>
<h4 id="상황">상황</h4>
<pre><code class="language-yaml">서울 클러스터:
  - Pod CIDR: 10.244.0.0/16
  - Service CIDR: 10.96.0.0/12

도쿄 클러스터:
  - Pod CIDR: 10.245.0.0/16
  - Service CIDR: 10.97.0.0/12</code></pre>
<h4 id="문제">문제</h4>
<pre><code class="language-javascript">// 서울 클러스터의 Pod에서
const response = await axios.post(
  &#39;http://10.245.1.50:8080/api&#39;,  // 도쿄 클러스터 Pod IP
  data
);

// ❌ Error: Network unreachable
// 왜? 서울 클러스터는 10.245.x.x가 어디 있는지 몰라!</code></pre>
<h4 id="해결-bgp-설정">해결: BGP 설정</h4>
<p><strong>서울 클러스터:</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64512  # 서울 클러스터 AS

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: to-tokyo-cluster
spec:
  peerIP: 203.0.113.1  # 도쿄 클러스터 게이트웨이
  asNumber: 64513      # 도쿄 클러스터 AS</code></pre>
<p><strong>도쿄 클러스터:</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64513  # 도쿄 클러스터 AS

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: to-seoul-cluster
spec:
  peerIP: 203.0.113.2  # 서울 클러스터 게이트웨이
  asNumber: 64512      # 서울 클러스터 AS</code></pre>
<h4 id="동작-흐름">동작 흐름</h4>
<pre><code>1. 서울 Calico: &quot;10.244.0.0/16은 우리 거야!&quot; (BGP 광고)
2. 도쿄 Calico: &quot;10.245.0.0/16은 우리 거야!&quot; (BGP 광고)
3. 라우터들이 학습: 
   - &quot;10.244.x.x → 서울로&quot;
   - &quot;10.245.x.x → 도쿄로&quot;
4. ✅ 서울 ↔ 도쿄 Pod 간 직접 통신 가능!</code></pre><hr>
<h3 id="✅-case-2-온프레미스-통합">✅ Case 2: 온프레미스 통합</h3>
<h4 id="상황-1">상황</h4>
<pre><code>Kubernetes Cluster: 10.244.0.0/16 (AWS)
온프레미스 데이터센터: 192.168.0.0/16
  - 레거시 ERP: 192.168.100.50
  - 은행 연동 시스템: 192.168.200.30
  - Oracle DB: 192.168.150.10</code></pre><h4 id="문제-1">문제</h4>
<pre><code class="language-python"># Kubernetes Pod에서 온프레미스 DB 접근
import cx_Oracle

# ❌ 연결 실패!
connection = cx_Oracle.connect(
    &#39;user/password@192.168.150.10:1521/ORCL&#39;
)

# 왜? 
# 1. Pod → 192.168.150.10 경로를 몰라
# 2. 온프레미스 → 10.244.x.x 응답 경로를 몰라</code></pre>
<h4 id="해결-bgp--direct-connect">해결: BGP + Direct Connect</h4>
<p><strong>AWS 측 설정:</strong></p>
<pre><code class="language-hcl"># Terraform
resource &quot;aws_vpn_gateway&quot; &quot;main&quot; {
  vpc_id          = aws_vpc.main.id
  amazon_side_asn = 64512  # AWS 측 AS
}

resource &quot;aws_customer_gateway&quot; &quot;onprem&quot; {
  bgp_asn    = 65000  # 온프레미스 AS
  ip_address = &quot;203.0.113.100&quot;  # 온프레미스 공인 IP
  type       = &quot;ipsec.1&quot;
}

resource &quot;aws_vpn_connection&quot; &quot;main&quot; {
  vpn_gateway_id      = aws_vpn_gateway.main.id
  customer_gateway_id = aws_customer_gateway.onprem.id
  type                = &quot;ipsec.1&quot;
  static_routes_only  = false  # BGP 사용
}</code></pre>
<p><strong>Calico 설정:</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64513  # Kubernetes AS (AWS와 다름!)

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: aws-vpn-gateway
spec:
  peerIP: 10.0.0.1  # VPN Gateway 내부 IP
  asNumber: 64512   # AWS VGW AS</code></pre>
<p><strong>온프레미스 라우터 설정:</strong></p>
<pre><code class="language-cisco">router bgp 65000
  neighbor 203.0.113.100 remote-as 64512
  network 192.168.0.0 mask 255.255.0.0

  ! Kubernetes Pod IP 학습
  neighbor 203.0.113.100 route-map ACCEPT-K8S in</code></pre>
<h4 id="동작-흐름-1">동작 흐름</h4>
<pre><code>Pod (10.244.1.5) → Oracle DB (192.168.150.10)

1. Pod: &quot;192.168.150.10으로 가고 싶어&quot;
2. Calico: &quot;BGP 테이블 확인... VPN Gateway로!&quot;
3. VPN Gateway → Direct Connect
4. 온프레미스 라우터: &quot;192.168.150.10은 내부 네트워크&quot;
5. ✅ DB 도달

응답:
DB (192.168.150.10) → Pod (10.244.1.5)

1. 온프레미스 라우터: &quot;10.244.1.5? BGP 테이블 확인...&quot;
2. &quot;아, AWS VPN으로 보내면 되겠네&quot;
3. VPN → AWS VGW → Calico
4. ✅ Pod 도달</code></pre><hr>
<h3 id="✅-case-3-데이터센터의-tor-switch-연동">✅ Case 3: 데이터센터의 ToR Switch 연동</h3>
<h4 id="상황-2">상황</h4>
<pre><code>온프레미스 데이터센터 구조:

┌────────────────────────────────┐
│  Core Router (192.168.0.1)    │
│  AS 65000                      │
└────────┬───────────────────────┘
         │
    ┌────┴────┬────────┐
    │         │        │
┌───▼───┐ ┌──▼───┐ ┌──▼───┐
│ToR-1  │ │ToR-2 │ │ToR-3 │
│.1.1   │ │.1.2  │ │.1.3  │
│AS     │ │AS    │ │AS    │
│65001  │ │65001 │ │65001 │
└───┬───┘ └──┬───┘ └──┬───┘
    │        │        │
  [노드]   [노드]   [노드]</code></pre><h4 id="왜-bgp가-필요한가">왜 BGP가 필요한가?</h4>
<p><strong>BGP 없이 (VXLAN Overlay):</strong></p>
<pre><code>문제점:
1. 모든 트래픽이 캡슐화됨 (Overhead 증가)
2. ToR Switch가 Pod IP를 몰라 → 비효율적 라우팅
3. 물리 네트워크의 성능을 100% 활용 못 함

예시:
  Pod A → Pod B (같은 랙)
  실제 거리: 1 홉
  VXLAN: 3 홉 (캡슐화/복호화 오버헤드)</code></pre><p><strong>BGP 사용 (Native Routing):</strong></p>
<pre><code>장점:
1. 캡슐화 없음 → 오버헤드 제거
2. ToR Switch가 Pod IP 인지 → 최적 경로
3. 물리 네트워크 성능 100% 활용

예시:
  Pod A → Pod B (같은 랙)
  실제 거리: 1 홉
  BGP: 1 홉 (직접 전송)</code></pre><h4 id="bgp-설정">BGP 설정</h4>
<p><strong>ToR Switch 1:</strong></p>
<pre><code class="language-cisco">router bgp 65001
  neighbor 192.168.1.10 remote-as 64512  # K8s Node 1
  neighbor 192.168.1.11 remote-as 64512  # K8s Node 2

  ! Pod IP 학습
  address-family ipv4
    neighbor 192.168.1.10 activate
    neighbor 192.168.1.11 activate</code></pre>
<p><strong>Calico (모든 노드):</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64512
  nodeToNodeMeshEnabled: false  # Node Mesh 비활성화

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch-1
spec:
  peerIP: 192.168.1.1
  asNumber: 65001</code></pre>
<h4 id="성능-비교">성능 비교</h4>
<pre><code>벤치마크 (10Gbps 네트워크):

VXLAN Overlay:
  처리량: 8.2 Gbps (18% 오버헤드)
  레이턴시: 0.8ms
  CPU 사용률: 25%

BGP Native Routing:
  처리량: 9.7 Gbps (3% 오버헤드)
  레이턴시: 0.3ms
  CPU 사용률: 8%

결과: BGP가 3배 효율적!</code></pre><hr>
<h3 id="✅-case-4-외부-로드밸런서-직접-연동">✅ Case 4: 외부 로드밸런서 직접 연동</h3>
<h4 id="상황-3">상황</h4>
<pre><code>요구사항:
- F5 하드웨어 로드밸런서 (물리 장비)
- Pod IP로 직접 트래픽 전송 (Service 거치지 않음)
- DSR (Direct Server Return) 구현</code></pre><h4 id="왜-필요한가">왜 필요한가?</h4>
<p><strong>일반적인 방법 (NodePort/LoadBalancer):</strong></p>
<pre><code>사용자 → F5 → NodePort (30080)
       → kube-proxy NAT
       → Pod

문제:
1. kube-proxy 병목
2. NAT 오버헤드
3. Source IP 손실
4. 높은 레이턴시</code></pre><p><strong>BGP 방법:</strong></p>
<pre><code>사용자 → F5 → Pod IP 직접!

장점:
1. kube-proxy 우회
2. NAT 없음
3. Source IP 보존
4. 낮은 레이턴시</code></pre><h4 id="설정">설정</h4>
<p><strong>Calico:</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: f5-load-balancer
spec:
  peerIP: 192.168.10.100  # F5 IP
  asNumber: 65100

---
# 특정 Service의 Pod IP만 광고
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  serviceExternalIPs:
    - cidr: 10.244.0.0/16  # Pod CIDR</code></pre>
<p><strong>F5 설정:</strong></p>
<pre><code>ltm pool k8s-payment-pool {
  members {
    10.244.1.50:8080 { }  # Pod IP 직접 사용!
    10.244.2.30:8080 { }
    10.244.3.80:8080 { }
  }
  monitor tcp
}</code></pre><h4 id="동작">동작</h4>
<pre><code>1. Calico가 BGP로 광고:
   &quot;10.244.1.50은 192.168.1.10 (노드)에 있어요&quot;

2. F5가 학습:
   &quot;10.244.1.50으로 가려면 192.168.1.10으로 보내면 되겠네&quot;

3. 트래픽:
   사용자 → F5 → 192.168.1.10 → Pod (10.244.1.50)

4. 응답 (DSR):
   Pod → 사용자 (직접! F5 거치지 않음)

결과: 초고속 처리!</code></pre><hr>
<h2 id="bgp-설정-완전-분석">BGP 설정 완전 분석</h2>
<h3 id="yaml-각-필드-상세-설명">YAML 각 필드 상세 설명</h3>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: rack-tor-switch
spec:
  peerIP: 192.168.1.1
  asNumber: 64512</code></pre>
<h4 id="1-apiversion-projectcalicoorgv3">1. apiVersion: projectcalico.org/v3</h4>
<p><strong>의미:</strong></p>
<ul>
<li>Calico API v3 사용 선언</li>
<li>Kubernetes CRD (Custom Resource Definition)</li>
</ul>
<p><strong>버전 히스토리:</strong></p>
<ul>
<li>v1: 초기 버전 (deprecated)</li>
<li>v3: 현재 안정 버전 (권장)</li>
</ul>
<h4 id="2-kind-bgppeer">2. kind: BGPPeer</h4>
<p><strong>의미:</strong></p>
<ul>
<li>생성할 리소스 타입: BGP Peer</li>
<li>외부 BGP 라우터와의 연결 정의</li>
</ul>
<p><strong>Calico의 다른 kinds:</strong></p>
<pre><code class="language-yaml">BGPPeer           # 외부 BGP 라우터 연결
BGPConfiguration  # BGP 전역 설정
NetworkPolicy     # 네트워크 보안 정책
IPPool            # IP 주소 풀
FelixConfiguration # Felix 데몬 설정</code></pre>
<h4 id="3-metadataname-rack-tor-switch">3. metadata.name: rack-tor-switch</h4>
<p><strong>의미:</strong></p>
<ul>
<li>BGP Peer 리소스의 고유 식별자</li>
<li>클러스터 내에서 유일해야 함</li>
</ul>
<p><strong>네이밍 컨벤션:</strong></p>
<pre><code class="language-yaml"># 위치 기반
name: rack1-tor-switch
name: datacenter-seoul-tor1

# 기능 기반
name: production-bgp-peer
name: edge-router-primary

# 조합
name: seoul-dc-rack1-tor1</code></pre>
<p><strong>실제 데이터센터 용어:</strong></p>
<ul>
<li><strong>rack</strong>: 서버 랙 (서버들이 꽂혀있는 선반)</li>
<li><strong>tor</strong>: Top of Rack (랙 맨 위의 스위치)</li>
<li><strong>switch</strong>: 네트워크 스위치</li>
</ul>
<pre><code>데이터센터 구조:

┌─────────────────┐
│  Core Router    │
└────────┬────────┘
         │
    ┌────▼────┐
    │ToR Switch│ ← 여기!
    │(랙 상단) │
    └────┬────┘
         │
    ┌────┴────┐
    │Server 1 │
    │Server 2 │
    │Server 3 │
    └─────────┘</code></pre><h4 id="4-specpeerip-19216811">4. spec.peerIP: 192.168.1.1</h4>
<p><strong>의미:</strong></p>
<ul>
<li>연결할 외부 BGP 라우터의 IP 주소</li>
<li>Calico 노드들이 이 IP로 BGP 연결 시도</li>
</ul>
<p><strong>선택 기준:</strong></p>
<pre><code>✅ 모든 Kubernetes 노드에서 접근 가능한 IP
✅ 일반적으로 관리 네트워크의 IP
✅ 물리 라우터/스위치의 관리 인터페이스 IP
❌ NAT 뒤의 IP는 피할 것
❌ DHCP로 변경될 수 있는 IP 피할 것</code></pre><p><strong>실제 환경 예시:</strong></p>
<pre><code class="language-yaml"># 온프레미스
peerIP: 192.168.1.1  # ToR Switch 관리 IP

# AWS
peerIP: 10.0.0.1  # Virtual Private Gateway

# GCP
peerIP: 10.1.0.1  # Cloud Router</code></pre>
<h4 id="5-specasnumber-64512">5. spec.asNumber: 64512</h4>
<p><strong>의미:</strong></p>
<ul>
<li>Peer (상대방 라우터)의 AS Number</li>
<li>BGP에서 네트워크를 구분하는 고유 번호</li>
</ul>
<p><strong>AS Number 범위:</strong></p>
<pre><code>1 - 64511:           공인 AS (Public)
                     예: Google(15169), Amazon(16509)

64512 - 65534:       사설 AS (Private) ← 대부분 여기 사용
                     내부 네트워크용, 인터넷 광고 안 됨

4200000000 - 4294967294: 4바이트 사설 AS
                          대규모 조직용</code></pre><p><strong>왜 64512를 많이 사용하나?</strong></p>
<ul>
<li>사설 AS 범위의 시작 번호</li>
<li>RFC 6996에서 정의된 표준</li>
<li>많은 문서와 예제에서 사용</li>
</ul>
<hr>
<h3 id="추가-설정-옵션">추가 설정 옵션</h3>
<h4 id="nodeselector로-특정-노드만-연결">nodeSelector로 특정 노드만 연결</h4>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: payment-nodes-tor
spec:
  peerIP: 192.168.1.1
  asNumber: 64512
  nodeSelector: &quot;service == &#39;payment&#39;&quot;  # 결제 노드만</code></pre>
<pre><code class="language-bash"># 노드 레이블 설정
kubectl label node worker-1 service=payment
kubectl label node worker-2 service=payment</code></pre>
<h4 id="다중-bgp-peer-고가용성">다중 BGP Peer (고가용성)</h4>
<pre><code class="language-yaml"># Primary
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch-primary
spec:
  peerIP: 192.168.1.1
  asNumber: 64512

---
# Backup
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch-backup
spec:
  peerIP: 192.168.1.2
  asNumber: 64512</code></pre>
<p><strong>동작:</strong></p>
<pre><code>정상: 모든 노드가 2개 스위치와 BGP 세션
Switch 1 장애: 자동으로 Switch 2만 사용
Switch 1 복구: 다시 2개 모두 사용</code></pre><hr>
<h2 id="라우터와-스위치">라우터와 스위치</h2>
<h3 id="기본-개념">기본 개념</h3>
<h4 id="스위치-switch">스위치 (Switch)</h4>
<pre><code>역할: 같은 네트워크 내 연결
계층: L2 (데이터 링크)
주소: MAC 주소 사용
범위: 로컬 네트워크

예시:
  컴퓨터 A → 스위치 → 컴퓨터 B
  (같은 사무실 내)</code></pre><h4 id="라우터-router">라우터 (Router)</h4>
<pre><code>역할: 다른 네트워크 간 연결
계층: L3 (네트워크)
주소: IP 주소 사용
범위: 인터넷, WAN

예시:
  회사 네트워크 → 라우터 → 인터넷</code></pre><h4 id="l3-스위치-layer-3-switch">L3 스위치 (Layer 3 Switch)</h4>
<pre><code>역할: 스위칭 + 라우팅
계층: L2 + L3
기능: 스위치처럼 빠르고, 라우터처럼 똑똑함

데이터센터의 ToR Switch는 보통 L3 스위치!
→ BGP 사용 가능</code></pre><h3 id="스위치-2개-설정-예시">스위치 2개 설정 예시</h3>
<h4 id="환경">환경</h4>
<pre><code>Kubernetes 클러스터
  Node 1: 192.168.1.10
  Node 2: 192.168.1.11
  Node 3: 192.168.1.12

ToR Switch 1: 192.168.1.1 (AS 64512)
ToR Switch 2: 192.168.1.2 (AS 64512) ← 이중화</code></pre><h4 id="bgp-설정-1">BGP 설정</h4>
<pre><code class="language-yaml"># 첫 번째 스위치
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch-1
spec:
  peerIP: 192.168.1.1
  asNumber: 64512

---
# 두 번째 스위치
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch-2
spec:
  peerIP: 192.168.1.2
  asNumber: 64512</code></pre>
<h4 id="네트워크-구조">네트워크 구조</h4>
<pre><code>        [인터넷]
           |
    [Core Router]
           |
     ┌─────┴─────┐
     |           |
[ToR Switch 1]  [ToR Switch 2]
192.168.1.1     192.168.1.2
AS 64512        AS 64512
     |               |
     |   이중 연결   |
     └───────┬───────┘
             |
    ┌────────┼────────┐
    |        |        |
  Node1    Node2    Node3
  .1.10    .1.11    .1.12

각 Node는 Switch 1, 2 모두와 BGP 세션</code></pre><h4 id="검증">검증</h4>
<pre><code class="language-bash"># BGP Peer 확인
$ calicoctl get bgppeer

NAME            PEERIP         NODE   ASN
tor-switch-1   192.168.1.1    (all)  64512
tor-switch-2   192.168.1.2    (all)  64512

# BGP 세션 상태
$ calicoctl node status

IPv4 BGP status
+--------------+-------+----------+-------------+
| PEER ADDRESS | STATE |  SINCE   |    INFO     |
+--------------+-------+----------+-------------+
| 192.168.1.1  | up    | 10:52:33 | Established |
| 192.168.1.2  | up    | 10:52:35 | Established |
+--------------+-------+----------+-------------+

# 2개 모두 &quot;Established&quot; = 정상!</code></pre>
<hr>
<h2 id="as-number-관리">AS Number 관리</h2>
<h3 id="as-number는-어디서-설정">AS Number는 어디서 설정?</h3>
<h4 id="양쪽-모두-설정-필요">양쪽 모두 설정 필요!</h4>
<p><strong>1. 라우터/스위치 측 (네트워크팀)</strong></p>
<pre><code class="language-cisco"># Cisco 라우터
router bgp 64512
  bgp router-id 192.168.1.1
  neighbor 192.168.1.10 remote-as 64513  # K8s 노드
  neighbor 192.168.1.11 remote-as 64513

# &quot;나는 AS 64512이고, 상대방은 AS 64513이야&quot;</code></pre>
<p><strong>2. Kubernetes/Calico 측 (DevOps팀)</strong></p>
<pre><code class="language-yaml"># 우리 클러스터 AS
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64513  # &quot;우리는 AS 64513&quot;

---
# Peer 정보
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tor-switch
spec:
  peerIP: 192.168.1.1
  asNumber: 64512  # &quot;상대방은 AS 64512&quot;</code></pre>
<h3 id="설정-순서">설정 순서</h3>
<pre><code>1단계: 네트워크 설계 회의
   &quot;온프레미스는 AS 65000&quot;
   &quot;AWS 서울은 AS 64512&quot;
   &quot;K8s 서울은 AS 64514&quot;

2단계: 네트워크팀이 라우터 설정
   router bgp 64512
   neighbor x.x.x.x remote-as 64514

3단계: DevOps팀이 Calico 설정
   asNumber: 64514 (우리)
   peerIP: x.x.x.x
   asNumber: 64512 (상대방)</code></pre><h3 id="as-number-충돌-방지">AS Number 충돌 방지</h3>
<h4 id="❌-잘못된-예시">❌ 잘못된 예시</h4>
<pre><code class="language-yaml">라우터: AS 64512
Calico: AS 64512  ← 같으면 안 됨!

문제:
- iBGP (Internal BGP)로 동작
- 라우팅 루프 방지로 경로 광고 안 됨
- 연결은 되지만 라우팅 안 됨</code></pre>
<h4 id="✅-올바른-예시">✅ 올바른 예시</h4>
<pre><code class="language-yaml">라우터: AS 64512
Calico: AS 64513  ← 다른 AS

결과:
- eBGP (External BGP)로 동작
- 정상적으로 라우팅 정보 교환</code></pre>
<h3 id="as-number-할당-전략">AS Number 할당 전략</h3>
<h4 id="예시-1-용도별">예시 1: 용도별</h4>
<pre><code>65000: 온프레미스 데이터센터
65001: AWS 서울 VPC
65002: AWS 도쿄 VPC
65010: K8s 서울 프로덕션
65011: K8s 도쿄 DR
65020: K8s 개발 환경</code></pre><h4 id="예시-2-계층별">예시 2: 계층별</h4>
<pre><code>64512: 코어 라우터
64520-64529: ToR Switch
64530-64539: Kubernetes 클러스터
64540-64549: 개발/테스트</code></pre><h4 id="예시-3-리전별">예시 3: 리전별</h4>
<pre><code>65001: 서울 (SEL)
  65001: 인프라
  65011: K8s 프로덕션
  65021: K8s 개발

65002: 도쿄 (TYO)
  65002: 인프라
  65012: K8s 프로덕션
  65022: K8s 개발</code></pre><hr>
<h2 id="장애-대응">장애 대응</h2>
<h3 id="자동-장애-대응-기본">자동 장애 대응 (기본)</h3>
<pre><code class="language-yaml"># 이것만으로도 자동 장애 대응!
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: switch-1
spec:
  peerIP: 192.168.1.1
  asNumber: 64512
---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: switch-2
spec:
  peerIP: 192.168.1.2
  asNumber: 64512</code></pre>
<p><strong>동작:</strong></p>
<pre><code>정상:
  Node → Switch 1 (Active)
  Node → Switch 2 (Standby)

Switch 1 장애:
  10초 후 자동 감지
  → Switch 2로 전환

Switch 1 복구:
  자동으로 원래대로</code></pre><h3 id="우선순위-설정">우선순위 설정</h3>
<h4 id="라우터-측-weight">라우터 측 (Weight)</h4>
<pre><code class="language-cisco"># Primary 라우터
router bgp 64512
  neighbor 192.168.1.10 remote-as 64513
  neighbor 192.168.1.10 weight 100  # 높은 우선순위

# Backup 라우터
router bgp 64512
  neighbor 192.168.1.10 remote-as 64513
  neighbor 192.168.1.10 weight 50   # 낮은 우선순위</code></pre>
<h4 id="라우터-측-as-path-prepend">라우터 측 (AS Path Prepend)</h4>
<pre><code class="language-cisco"># Primary: 짧은 경로
router bgp 64512
  neighbor 192.168.1.10 route-map PRIMARY in

route-map PRIMARY permit 10
  set as-path prepend 64512  # AS Path: 64512

# Backup: 긴 경로 (우선순위 낮음)
router bgp 64512
  neighbor 192.168.1.10 route-map BACKUP in

route-map BACKUP permit 10
  set as-path prepend 64512 64512 64512  # AS Path: 64512 64512 64512</code></pre>
<h3 id="빠른-장애-감지-bfd">빠른 장애 감지 (BFD)</h3>
<p><strong>기본 BGP: 30-90초 감지</strong></p>
<pre><code class="language-yaml"># 기본 설정
keepAliveTime: 30s
holdTime: 90s

# → 최대 90초 후 장애 감지</code></pre>
<p><strong>BFD 사용: 1초 이내 감지</strong></p>
<pre><code class="language-cisco"># 라우터 설정
interface GigabitEthernet0/0
  bfd interval 200 min_rx 200 multiplier 3
  # 200ms * 3 = 600ms 후 장애 감지

router bgp 64512
  neighbor 192.168.1.10 remote-as 64513
  neighbor 192.168.1.10 fall-over bfd</code></pre>
<h3 id="멀티-리전-dr-시나리오">멀티 리전 DR 시나리오</h3>
<h4 id="환경-설정">환경 설정</h4>
<pre><code class="language-yaml"># 서울 리전 (Primary)
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64512

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: global-router
spec:
  peerIP: 203.0.113.1
  asNumber: 64500

---
# 도쿄 리전 (DR)
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64513

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: global-router
spec:
  peerIP: 203.0.113.1
  asNumber: 64500</code></pre>
<h4 id="장애-시나리오">장애 시나리오</h4>
<pre><code>정상 시:
  사용자 → 서울 (AS Path 짧음)

서울 장애:
  1. 서울 BGP 광고 중단 (30초)
  2. 라우터가 서울 경로 제거
  3. 도쿄 경로만 남음
  4. 모든 트래픽 → 도쿄 (자동!)

서울 복구:
  1. 서울 BGP 광고 재개
  2. 라우터가 경로 추가
  3. AS Path 비교 (서울이 짧음)
  4. 다시 서울 우선 사용</code></pre><hr>
<h2 id="실전-시나리오">실전 시나리오</h2>
<h3 id="시나리오-1-스타트업-→-유니콘-성장">시나리오 1: 스타트업 → 유니콘 성장</h3>
<h4 id="phase-1-mvp-flannel-bgp-없음">Phase 1: MVP (Flannel, BGP 없음)</h4>
<pre><code class="language-yaml">상황:
- 시드 투자 직후, 팀 10명
- AWS 서울 단일 리전
- 10 노드 클러스터

선택: Flannel
이유: 빠른 구축, 운영 단순화

BGP: 불필요</code></pre>
<h4 id="phase-2-성장기-flannel-유지">Phase 2: 성장기 (Flannel 유지)</h4>
<pre><code class="language-yaml">상황:
- 시리즈 A, 팀 50명, MAU 10만
- 여전히 단일 리전
- 30 노드로 확장

선택: Flannel 계속 사용
이유: 충분한 성능, 안정적

BGP: 여전히 불필요</code></pre>
<h4 id="phase-3-스케일업-calico--bgp">Phase 3: 스케일업 (Calico + BGP)</h4>
<pre><code class="language-yaml">상황:
- 시리즈 B, 팀 200명, MAU 100만
- 멀티 리전 필요 (서울 + 도쿄)
- 리전당 100+ 노드

트리거:
✓ 엔터프라이즈 고객 요구 (네트워크 격리)
✓ SOC2 인증 필요
✓ 글로벌 확장
✓ 온프레미스 레거시 연동

선택: Calico + BGP</code></pre>
<p><strong>BGP 설정:</strong></p>
<pre><code class="language-yaml"># 서울 클러스터
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64512
  nodeToNodeMeshEnabled: false

---
# AWS 서울 VGW 연결
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: aws-seoul-vgw
spec:
  peerIP: 10.0.0.1
  asNumber: 64500

---
# 도쿄 클러스터 연결
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tokyo-dr
spec:
  peerIP: 203.0.113.2
  asNumber: 64513

---
# 온프레미스 연결
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: onprem-dc
spec:
  peerIP: 192.168.1.1
  asNumber: 65000</code></pre>
<h4 id="phase-4-엔터프라이즈">Phase 4: 엔터프라이즈</h4>
<pre><code class="language-yaml">상황:
- 시리즈 C+, 팀 500명, MAU 500만+
- 글로벌 10개 리전
- 리전당 200+ 노드

BGP 사용:
✓ 멀티 리전 자동 장애 전환
✓ 온프레미스 통합 (레거시 시스템)
✓ 하이브리드 클라우드 (AWS + GCP + 온프레미스)
✓ 고성능 라우팅 (eBPF + BGP)</code></pre>
<hr>
<h3 id="시나리오-2-금융-서비스-토스카카오뱅크">시나리오 2: 금융 서비스 (토스/카카오뱅크)</h3>
<h4 id="요구사항">요구사항</h4>
<pre><code>규제:
✓ 금융 데이터는 국내에만
✓ PCI-DSS 준수
✓ 모든 트래픽 암호화
✓ 네트워크 감사 추적

기술:
✓ 온프레미스 은행 연동
✓ 초저 레이턴시 (&lt;5ms)
✓ 99.99% 가용성
✓ 멀티 리전 DR</code></pre><h4 id="bgp-아키텍처">BGP 아키텍처</h4>
<pre><code class="language-yaml"># 1. 온프레미스 은행 연동
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bank-mainframe
spec:
  peerIP: 192.168.1.1  # 은행 연동 라우터
  asNumber: 65000
  nodeSelector: &quot;service == &#39;payment&#39;&quot;  # 결제 노드만

---
# 2. AWS 서울 (Primary)
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: aws-seoul-primary
spec:
  peerIP: 10.0.0.1
  asNumber: 64512

---
# 3. AWS 서울 (Backup)
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: aws-seoul-backup
spec:
  peerIP: 10.0.0.2
  asNumber: 64512

---
# 4. AWS 부산 DR
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: aws-busan-dr
spec:
  peerIP: 10.1.0.1
  asNumber: 64513</code></pre>
<h4 id="네트워크-정책--bgp">네트워크 정책 + BGP</h4>
<pre><code class="language-yaml"># BGP로 라우팅 + NetworkPolicy로 보안
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: payment-isolation
spec:
  selector: app == &#39;payment&#39;
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == &#39;api-gateway&#39;
      destination:
        ports: [8080]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        nets:
          - 192.168.100.0/24  # 은행 시스템만
        ports: [3000]</code></pre>
<hr>
<h3 id="시나리오-3-글로벌-게임-회사">시나리오 3: 글로벌 게임 회사</h3>
<h4 id="요구사항-1">요구사항</h4>
<pre><code>성능:
✓ 초저 레이턴시 (&lt;2ms)
✓ 초당 100만+ 패킷
✓ DDoS 방어

가용성:
✓ 99.99% 가동률
✓ 자동 리전 전환
✓ 글로벌 Anycast</code></pre><h4 id="bgp--ebpf-조합">BGP + eBPF 조합</h4>
<pre><code class="language-yaml"># eBPF 활성화
apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  bpfEnabled: true
  bpfLogLevel: Info

---
# BGP Configuration
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  asNumber: 64520

---
# 서울 리전
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: seoul-edge
spec:
  peerIP: 203.0.113.1
  asNumber: 64500

---
# 도쿄 리전
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: tokyo-edge
spec:
  peerIP: 103.0.113.1
  asNumber: 64501

---
# 싱가포르 리전
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: singapore-edge
spec:
  peerIP: 123.0.113.1
  asNumber: 64502</code></pre>
<h4 id="anycast-구현">Anycast 구현</h4>
<pre><code>같은 Service IP를 모든 리전에서 광고:

서울: &quot;1.2.3.4는 우리한테 있어요!&quot; (BGP 광고)
도쿄: &quot;1.2.3.4는 우리한테 있어요!&quot; (BGP 광고)
싱가포르: &quot;1.2.3.4는 우리한테 있어요!&quot; (BGP 광고)

결과:
- 한국 플레이어 → 자동으로 서울 리전
- 일본 플레이어 → 자동으로 도쿄 리전
- 동남아 플레이어 → 자동으로 싱가포르 리전

서울 장애 시:
- 한국 플레이어도 자동으로 도쿄로 전환</code></pre><hr>
<h2 id="faq">FAQ</h2>
<h3 id="q1-bgp-없이도-kubernetes가-잘-동작하는데">Q1: BGP 없이도 Kubernetes가 잘 동작하는데?</h3>
<p><strong>A:</strong> 맞습니다! <strong>대부분의 경우 BGP 불필요</strong>합니다.</p>
<pre><code>BGP 불필요 (90%):
✓ 단일 클러스터
✓ Service를 통한 Pod 접근
✓ Ingress 사용
✓ 같은 클러스터 내 통신

BGP 필요 (10%):
✓ 멀티 클러스터
✓ 온프레미스 통합
✓ 물리 네트워크 직접 연동
✓ 외부에서 Pod IP 직접 접근</code></pre><h3 id="q2-flannel에서-calico로-마이그레이션-시-다운타임은">Q2: Flannel에서 Calico로 마이그레이션 시 다운타임은?</h3>
<p><strong>A:</strong> 방법에 따라 다릅니다.</p>
<pre><code>단일 클러스터 교체:
- 다운타임: 10-30분
- Pod 전체 재시작 필요

Blue-Green 방식:
- 다운타임: 0분
- 새 클러스터 구축 후 전환

Rolling Update:
- 다운타임: 노드당 2-5분
- 노드를 하나씩 전환</code></pre><h3 id="q3-as-number는-누가-정하나요">Q3: AS Number는 누가 정하나요?</h3>
<p><strong>A:</strong> 네트워크팀과 협의하여 결정합니다.</p>
<pre><code>절차:
1. 네트워크팀과 회의
   &quot;우리 온프레미스는 AS 65000 사용 중&quot;

2. 네트워크팀이 AS 할당
   &quot;K8s 서울은 AS 64512 써주세요&quot;

3. DevOps팀이 Calico 설정
   asNumber: 64512

주의: 임의로 변경하면 안 됨!</code></pre><h3 id="q4-bgp-세션이-established-안-되면">Q4: BGP 세션이 Established 안 되면?</h3>
<p><strong>A:</strong> 체크리스트:</p>
<pre><code class="language-bash"># 1. 핑 테스트
ping 192.168.1.1

# 2. BGP 포트 확인 (TCP 179)
telnet 192.168.1.1 179

# 3. 방화벽 확인
# AWS: Security Group에서 TCP 179 허용
# 온프레미스: 방화벽 규칙 확인

# 4. AS Number 확인
# 라우터와 Calico 설정이 서로 일치하는지

# 5. 라우터 설정 확인
# 네트워크팀에게 라우터 로그 요청</code></pre>
<h3 id="q5-ebpf와-bgp는-같이-써야-하나요">Q5: eBPF와 BGP는 같이 써야 하나요?</h3>
<p><strong>A:</strong> 아닙니다. 독립적입니다.</p>
<pre><code>eBPF: 패킷 처리 성능 향상 (커널 레벨)
BGP: 네트워크 간 라우팅 정보 교환

조합:
✓ eBPF만: 가능 (고성능 단일 클러스터)
✓ BGP만: 가능 (멀티 클러스터)
✓ 둘 다: 가능 (고성능 멀티 클러스터) ← 최고!
✓ 둘 다 안 씀: 가능 (일반 Flannel)</code></pre><h3 id="q6-라우터-설정은-누가-하나요">Q6: 라우터 설정은 누가 하나요?</h3>
<p><strong>A:</strong> 역할 분담:</p>
<pre><code>네트워크팀:
- 물리/가상 라우터 설정
- AS Number 할당
- BGP 정책 설정
- 방화벽 규칙

DevOps팀:
- Calico BGP 설정
- Kubernetes 리소스 관리
- 모니터링 및 트러블슈팅

협업:
- AS Number 결정
- IP 대역 계획
- 장애 대응 절차</code></pre><h3 id="q7-bgp-모니터링은-어떻게">Q7: BGP 모니터링은 어떻게?</h3>
<p><strong>A:</strong> 여러 방법 사용:</p>
<pre><code class="language-bash"># Calico 상태
calicoctl node status

# 상세 정보
sudo birdc show protocols all

# Prometheus 메트릭
felix_route_table_list_seconds
felix_int_dataplane_failures
bgp_peers_up

# 알림 설정
BGP 세션 Down → Slack 알림
경로 개수 급변 → 담당자 호출</code></pre>
<h3 id="q8-비용은-얼마나-차이-나나요">Q8: 비용은 얼마나 차이 나나요?</h3>
<p><strong>A:</strong> 주로 운영 복잡도 차이:</p>
<pre><code>Flannel:
- 설정 시간: 30분
- 학습 시간: 1일
- 운영 인력: 최소

Calico + BGP:
- 설정 시간: 2-4시간
- 학습 시간: 1주일
- 운영 인력: 네트워크 지식 필요

하드웨어 비용:
- 거의 차이 없음
- BGP 때문에 추가 장비 불필요</code></pre><hr>
<h2 id="결론">결론</h2>
<h3 id="핵심-요약">핵심 요약</h3>
<h4 id="1-bgp는-언제-필요한가">1. BGP는 언제 필요한가?</h4>
<pre><code>✅ 필요한 경우 (10%):
- 멀티 클러스터
- 온프레미스 통합
- 물리 네트워크 직접 연동
- 고성능 요구사항

❌ 불필요한 경우 (90%):
- 단일 클러스터
- Service/Ingress 사용
- 일반적인 마이크로서비스</code></pre><h4 id="2-bgp-설정-핵심">2. BGP 설정 핵심</h4>
<pre><code class="language-yaml"># 3가지만 기억하세요
1. asNumber: 우리 클러스터 AS
2. peerIP: 상대방 라우터 IP
3. asNumber (peer): 상대방 AS

# 나머지는 자동!</code></pre>
<h4 id="3-장애는-자동-대응">3. 장애는 자동 대응</h4>
<pre><code>BGP가 자동으로:
✓ 장애 감지 (10-30초)
✓ 대체 경로 선택
✓ 트래픽 우회
✓ 복구 시 원복

개발자는 신경 안 써도 됨!</code></pre><h4 id="4-개발자-관점">4. 개발자 관점</h4>
<pre><code class="language-javascript">// 코드는 항상 똑같음
await axios.post(&#39;http://service:8080/api&#39;, data);

// BGP가 있든 없든
// 멀티 클러스터든 단일 클러스터든
// 코드 변경 없음!</code></pre>
<h3 id="의사결정-가이드">의사결정 가이드</h3>
<pre><code>시작 단계:
→ Flannel (간단함)

성장 단계:
→ Flannel 유지 (충분함)

스케일업:
→ Calico (성능/보안)

멀티 클러스터:
→ Calico + BGP (필수)

엔터프라이즈:
→ Calico + BGP + eBPF (최고)</code></pre><h3 id="마지막-조언">마지막 조언</h3>
<blockquote>
<p>&quot;BGP는 복잡하지만, 진짜 필요할 때까지는 쓰지 마세요. 
하지만 필요한 순간이 오면, BGP만큼 강력한 게 없습니다.&quot;</p>
</blockquote>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<h3 id="공식-문서">공식 문서</h3>
<ul>
<li><a href="https://docs.projectcalico.org/networking/bgp">Calico BGP 가이드</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc4271">RFC 4271 - BGP-4</a></li>
<li><a href="https://kubernetes.io/docs/concepts/cluster-administration/networking/">Kubernetes 네트워킹</a></li>
</ul>
<h3 id="추천-학습-자료">추천 학습 자료</h3>
<ul>
<li><a href="https://www.cloudflare.com/learning/security/glossary/what-is-bgp/">BGP 기초 개념</a></li>
<li><a href="https://docs.projectcalico.org/reference/architecture/">Calico 아키텍처</a></li>
<li><a href="https://github.com/networkop/k8s-net-labs">네트워크 엔지니어를 위한 Kubernetes</a></li>
</ul>
<h3 id="실전-케이스-스터디">실전 케이스 스터디</h3>
<ul>
<li><a href="https://engineering.atspotify.com/">Spotify의 멀티 클러스터 전략</a></li>
<li><a href="https://shopify.engineering/">Shopify의 Kubernetes 네트워킹</a></li>
<li><a href="https://netflixtechblog.com/">Netflix의 글로벌 네트워크</a></li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>BGP는 복잡해 보이지만, <strong>핵심 개념</strong>만 이해하면 됩니다:</p>
<ol>
<li><strong>클러스터 내부는 CNI가 처리</strong> (BGP 불필요)</li>
<li><strong>클러스터 외부는 BGP가 필요</strong> (멀티 클러스터, 온프레미스 등)</li>
<li><strong>설정은 간단하지만 네트워크팀과 협업</strong> 필수</li>
<li><strong>장애는 자동으로 대응</strong> (BGP의 진가!)</li>
</ol>
<p>이 글이 BGP에 대한 두려움을 없애고, 실전에서 올바른 선택을 하는 데 도움이 되었기를 바랍니다!</p>
<p><strong>Happy Networking!</strong> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes CNI 완벽 가이드: Calico vs Flannel 실전 비교]]></title>
            <link>https://velog.io/@arnold_99/Kubernetes-CNI-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Calico-vs-Flannel-%EC%8B%A4%EC%A0%84-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@arnold_99/Kubernetes-CNI-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Calico-vs-Flannel-%EC%8B%A4%EC%A0%84-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Mon, 27 Oct 2025 16:15:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Calico와 Flannel, 어떤 CNI를 선택해야 할까? eBPF와 BGP가 뭐길래 성능이 3배나 빨라진다는 걸까? 실전 사용 사례를 통해 완벽하게 이해해보자.</p>
</blockquote>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#cni%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">CNI란 무엇인가?</a></li>
<li><a href="#calico-vs-flannel-%ED%95%B5%EC%8B%AC-%EB%B9%84%EA%B5%90">Calico vs Flannel 핵심 비교</a></li>
<li><a href="#ebpf-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5">eBPF 완전 정복</a></li>
<li><a href="#bgp-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">BGP 이해하기</a></li>
<li><a href="#%EC%8B%A4%EC%A0%84-%EC%82%AC%EC%9A%A9-%EC%82%AC%EB%A1%80">실전 사용 사례</a></li>
<li><a href="#%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C">의사결정 가이드</a></li>
</ol>
<hr>
<h2 id="cni란-무엇인가">CNI란 무엇인가?</h2>
<p><strong>CNI (Container Network Interface)</strong>는 Kubernetes에서 Pod 간 네트워킹을 담당하는 플러그인입니다. </p>
<p>쉽게 말하면:</p>
<ul>
<li>🏠 <strong>문제</strong>: Kubernetes는 여러 서버(노드)에 걸쳐 컨테이너를 실행하는데, 이들이 서로 통신하려면?</li>
<li>✨ <strong>해결</strong>: CNI가 가상 네트워크를 만들어 모든 Pod가 서로 통신할 수 있게 해줌</li>
</ul>
<h3 id="cni의-역할">CNI의 역할</h3>
<pre><code>Pod A (10.244.1.5)     Pod B (10.244.2.10)
     ↓                        ↓
  Node 1                   Node 2
     ↓                        ↓
     └────── CNI가 연결 ──────┘</code></pre><hr>
<h2 id="calico-vs-flannel-핵심-비교">Calico vs Flannel 핵심 비교</h2>
<h3 id="🐱-calico">🐱 Calico</h3>
<p><strong>특징</strong>: 고성능 + 강력한 보안</p>
<pre><code class="language-yaml"># Calico 주요 특징
네트워킹: BGP, VXLAN, IPIP
네트워크 정책: ✅ 강력함 (L3/L4/L7)
성능: ⚡ 매우 높음 (eBPF 모드)
복잡도: 🔧 높음
리소스 사용: 📊 중간~높음
적합한 환경: 🏢 엔터프라이즈, 대규모 프로덕션</code></pre>
<h3 id="🧣-flannel">🧣 Flannel</h3>
<p><strong>특징</strong>: 단순함 + 안정성</p>
<pre><code class="language-yaml"># Flannel 주요 특징
네트워킹: VXLAN, host-gw, UDP
네트워크 정책: ❌ 지원 안함
성능: 💨 좋음
복잡도: 🎯 매우 낮음
리소스 사용: 📊 낮음
적합한 환경: 🛠️ 개발/테스트, 중소규모</code></pre>
<h3 id="기능-비교표">기능 비교표</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>Calico</th>
<th>Flannel</th>
</tr>
</thead>
<tbody><tr>
<td><strong>네트워크 정책</strong></td>
<td>✅ 고급 (L3/L4/L7)</td>
<td>❌ 없음</td>
</tr>
<tr>
<td><strong>암호화</strong></td>
<td>✅ WireGuard</td>
<td>❌ 없음</td>
</tr>
<tr>
<td><strong>성능 (eBPF)</strong></td>
<td>🚀🚀🚀</td>
<td>🚀🚀</td>
</tr>
<tr>
<td><strong>설정 복잡도</strong></td>
<td>높음</td>
<td>낮음</td>
</tr>
<tr>
<td><strong>학습 곡선</strong></td>
<td>가파름</td>
<td>완만함</td>
</tr>
<tr>
<td><strong>리소스 사용</strong></td>
<td>350MB</td>
<td>200MB</td>
</tr>
<tr>
<td><strong>멀티 클라우드</strong></td>
<td>✅ 뛰어남</td>
<td>⚠️ 제한적</td>
</tr>
<tr>
<td><strong>설치 시간</strong></td>
<td>1시간+</td>
<td>30분</td>
</tr>
</tbody></table>
<hr>
<h2 id="ebpf-완전-정복">eBPF 완전 정복</h2>
<h3 id="ebpf란">eBPF란?</h3>
<p><strong>eBPF (extended Berkeley Packet Filter)</strong>는 리눅스 커널을 재컴파일 없이 확장할 수 있는 혁명적 기술입니다.</p>
<h3 id="🍔-음식점-비유로-이해하기">🍔 음식점 비유로 이해하기</h3>
<h4 id="전통적-방식-iptables">전통적 방식 (iptables)</h4>
<pre><code>손님(패킷) → 홀(사용자 공간) → 주문서 작성 
  → 주방(커널)으로 전달 📝
  → 주방에서 요리
  → 홀로 다시 전달 📝
  → 손님에게 서빙

❌ 문제: 홀 ↔ 주방 왕복이 너무 많음!</code></pre><h4 id="ebpf-방식">eBPF 방식</h4>
<pre><code>손님(패킷) → 주방(커널)에서 바로 처리
  → 즉시 서빙

✅ 장점: 중간 단계 생략!</code></pre><h3 id="컴퓨터의-두-세계">컴퓨터의 두 세계</h3>
<p>컴퓨터는 크게 두 공간으로 나뉩니다:</p>
<pre><code>┌─────────────────────────────────┐
│  👤 사용자 공간 (User Space)     │
│                                 │
│  • 일반 프로그램들              │
│  • Docker 컨테이너              │
│  • 느리지만 안전                │
└────────────┬────────────────────┘
             │ 시스템 콜 (느림!)
┌────────────▼────────────────────┐
│  ⚙️ 커널 공간 (Kernel Space)    │
│                                 │
│  • 하드웨어 직접 제어           │
│  • 네트워크 카드 제어           │
│  • 빠르지만 위험                │
└─────────────────────────────────┘</code></pre><h3 id="전통적-방식-vs-ebpf">전통적 방식 vs eBPF</h3>
<h4 id="전통적-방식-긴-여행">전통적 방식: 긴 여행</h4>
<pre><code>패킷 도착
  ↓
커널이 받음
  ↓
사용자 공간으로 복사 ⚠️ (느림)
  ↓
애플리케이션 처리
  ↓
다시 커널로 전달 ⚠️ (느림)
  ↓
패킷 전송

총 소요 시간: ~10 마이크로초</code></pre><h4 id="ebpf-방식-직통">eBPF 방식: 직통</h4>
<pre><code>패킷 도착
  ↓
커널에서 즉시 처리 ⚡
  ↓
패킷 전송

총 소요 시간: ~3 마이크로초</code></pre><h3 id="성능-차이">성능 차이</h3>
<pre><code class="language-python"># 초당 100만 패킷 처리 시

전통적 방식: 10초 소요
eBPF 방식:    3초 소요
절약 시간:    7초 (70% 빨라짐!)</code></pre>
<h3 id="ebpf가-안전한-이유">eBPF가 안전한 이유</h3>
<pre><code>개발자 코드 작성
    ↓
컴파일 → eBPF 바이트코드
    ↓
┌─────────────────────────────┐
│   eBPF Verifier (검증기)    │
│                             │
│ ✓ 무한 루프 없나?           │
│ ✓ 메모리 침범 없나?         │
│ ✓ 커널 크래시 가능성?       │
└──────┬──────────────────────┘
       │
       ├─ ❌ 위험 → 거부
       └─ ✅ 안전
              ↓
        JIT 컴파일
              ↓
        커널에서 실행!</code></pre><h3 id="calico에서-ebpf-활성화">Calico에서 eBPF 활성화</h3>
<pre><code class="language-bash"># eBPF 모드 활성화
kubectl patch configmap/calico-config -n kube-system --type merge \
  -p &#39;{&quot;data&quot;:{&quot;bpf-enabled&quot;:&quot;true&quot;}}&#39;

# 상태 확인
calicoctl get felixconfiguration default -o yaml</code></pre>
<hr>
<h2 id="bgp-이해하기">BGP 이해하기</h2>
<h3 id="bgp란">BGP란?</h3>
<p><strong>BGP (Border Gateway Protocol)</strong>는 인터넷의 우체국입니다. 각 네트워크가 &quot;나는 이 주소들을 관리해!&quot;라고 알려주는 프로토콜이죠.</p>
<h3 id="간단한-비유">간단한 비유</h3>
<pre><code>당신이 편지를 보낼 때:

&quot;서울시 강남구 XX동&quot; → 우체국이 경로 찾음
&quot;10.244.1.0/24 네트워크&quot; → BGP가 경로 찾음</code></pre><h3 id="kubernetes에서-bgp-동작">Kubernetes에서 BGP 동작</h3>
<pre><code>┌──────────────────────────────────┐
│    Kubernetes 클러스터           │
│                                  │
│  ┌─────────┐    ┌─────────┐     │
│  │ Node 1  │    │ Node 2  │     │
│  │         │    │         │     │
│  │ BGP     │◄──►│ BGP     │     │
│  │Speaker  │    │Speaker  │     │
│  │         │    │         │     │
│  │10.1.0/24│    │10.2.0/24│     │
│  └────┬────┘    └────┬────┘     │
└───────┼──────────────┼──────────┘
        │              │
        └──────┬───────┘
               │
      ┌────────▼────────┐
      │  물리 라우터     │
      │                 │
      │ &quot;10.1.0/24는   │
      │  Node1로&quot;       │
      └─────────────────┘</code></pre><h3 id="bgp의-장점">BGP의 장점</h3>
<ol>
<li><p><strong>오버레이 불필요</strong></p>
<ul>
<li>VXLAN 같은 캡슐화 없음</li>
<li>네이티브 IP 라우팅</li>
<li>성능 향상</li>
</ul>
</li>
<li><p><strong>기존 인프라 통합</strong></p>
<ul>
<li>데이터센터의 물리 라우터와 직접 통신</li>
<li>온프레미스 환경에 최적</li>
</ul>
</li>
<li><p><strong>멀티 클라우드</strong></p>
<ul>
<li>AWS, GCP, Azure 간 Pod IP 직접 라우팅</li>
<li>클라우드 네이티브 통합</li>
</ul>
</li>
</ol>
<h3 id="calico-bgp-설정-예시">Calico BGP 설정 예시</h3>
<pre><code class="language-yaml"># BGP 피어 설정
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: rack-tor-switch
spec:
  peerIP: 192.168.1.1
  asNumber: 64512</code></pre>
<pre><code class="language-bash"># BGP 상태 확인
calicoctl node status</code></pre>
<hr>
<h2 id="실전-사용-사례">실전 사용 사례</h2>
<h3 id="사례-1-핀테크-스타트업">사례 1: 핀테크 스타트업</h3>
<h4 id="🧣-flannel-선택---토스뱅크-가상-사례">🧣 Flannel 선택 - 토스뱅크 (가상 사례)</h4>
<p><strong>상황</strong></p>
<ul>
<li>팀 규모: DevOps 2명</li>
<li>목표: 3개월 내 MVP 출시</li>
<li>규모: 10-20 노드</li>
<li>초기 사용자: 10만명</li>
</ul>
<p><strong>선택 이유</strong></p>
<pre><code>✅ 즉시 사용: 1시간 내 설정 완료
✅ 낮은 복잡도: 팀원 모두 1일 내 숙지
✅ 안정성: 5년 이상 검증됨
✅ 리소스 효율: 노드당 200MB만 사용</code></pre><p><strong>결과</strong></p>
<ul>
<li>✅ 2개월 만에 프로덕션 배포</li>
<li>✅ 네트워크 장애 0건 (6개월)</li>
<li>✅ 99.9% 가동률 달성</li>
</ul>
<hr>
<h4 id="🐱-calico-선택---카카오뱅크-가상-사례">🐱 Calico 선택 - 카카오뱅크 (가상 사례)</h4>
<p><strong>상황</strong></p>
<ul>
<li>금융 규제: PCI-DSS 준수 필요</li>
<li>규모: 200+ 노드</li>
<li>트래픽: 일 1천만 거래</li>
<li>요구사항: 마이크로서비스 간 세밀한 접근 제어</li>
</ul>
<p><strong>선택 이유</strong></p>
<pre><code>✅ 네트워크 정책: L7까지 세밀한 제어
✅ 암호화: WireGuard로 Pod 간 통신 암호화
✅ 고성능: eBPF로 레이턴시 최소화
✅ 가시성: Hubble로 모든 트래픽 모니터링</code></pre><p><strong>핵심 정책 예시</strong></p>
<pre><code class="language-yaml">apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: payment-service-policy
spec:
  selector: app == &#39;payment&#39;
  ingress:
  - action: Allow
    protocol: TCP
    source:
      selector: app == &#39;api-gateway&#39;
    destination:
      ports: [8080]
  # 결제 서비스는 API Gateway만 접근 가능</code></pre>
<p><strong>결과</strong></p>
<ul>
<li>✅ PCI-DSS Level 1 인증 획득</li>
<li>✅ 평균 응답 시간 30% 개선</li>
<li>✅ 보안 사고 0건 (1년간)</li>
</ul>
<hr>
<h3 id="사례-2-글로벌-전자상거래---쿠팡">사례 2: 글로벌 전자상거래 - 쿠팡</h3>
<h4 id="개발-환경-flannel-🧣">개발 환경: Flannel 🧣</h4>
<p><strong>요구사항</strong></p>
<ul>
<li>100+ 개발팀</li>
<li>각 팀별 독립 환경 필요</li>
<li>매주 새로운 클러스터 생성</li>
</ul>
<p><strong>솔루션</strong></p>
<pre><code class="language-bash"># Terraform으로 15분 내 클러스터 생성
terraform apply -var=&quot;env=dev-team-42&quot;

# Flannel 자동 설치
kubectl apply -f flannel.yaml</code></pre>
<p><strong>효과</strong></p>
<ul>
<li>클러스터 생성 시간: 2시간 → 15분</li>
<li>연간 인프라 비용 30% 절감</li>
<li>네트워크 관련 티켓 80% 감소</li>
</ul>
<hr>
<h4 id="프로덕션-환경-calico-🐱">프로덕션 환경: Calico 🐱</h4>
<p><strong>요구사항</strong></p>
<ul>
<li>멀티 리전: 서울, 싱가포르, LA</li>
<li>초당 100만 요청</li>
<li>블랙프라이데이 대응</li>
</ul>
<p><strong>아키텍처</strong></p>
<pre><code>┌─────────────────────────────────────┐
│         Global Load Balancer        │
└────────────┬───────────┬────────────┘
             │           │
    ┌────────▼───┐  ┌────▼────────┐
    │ Seoul      │  │ Singapore   │
    │ 500 nodes  │  │ 500 nodes   │
    │            │  │             │
    │ Calico BGP │◄─┤ Calico BGP  │
    │ + eBPF     │  │ + eBPF      │
    └────────────┘  └─────────────┘</code></pre><p><strong>핵심 설정</strong></p>
<pre><code class="language-yaml"># eBPF + BGP 조합
apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  bpfEnabled: true
  bpfLogLevel: Info

---
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  nodeToNodeMeshEnabled: false
  asNumber: 64512</code></pre>
<p><strong>결과</strong></p>
<ul>
<li>✅ 블랙프라이데이 99.99% 가동률</li>
<li>✅ 네트워크 레이턴시 50% 개선</li>
<li>✅ P99 레이턴시 2.5ms 달성</li>
<li>✅ 보안 정책 위반 실시간 차단</li>
</ul>
<hr>
<h3 id="사례-3-saas-기업---salesforce">사례 3: SaaS 기업 - Salesforce</h3>
<h4 id="🐱-calico-필수-선택">🐱 Calico 필수 선택</h4>
<p><strong>멀티 테넌트 환경</strong></p>
<ul>
<li>고객사: 1,000+</li>
<li>규모: 1,000+ 노드</li>
<li>요구사항: 고객사별 완전한 네트워크 격리</li>
</ul>
<p><strong>핵심 정책</strong></p>
<pre><code class="language-yaml"># 테넌트 격리 정책
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: tenant-isolation
spec:
  selector: tenant != &#39;&#39;
  ingress:
  - action: Allow
    source:
      selector: tenant == $TENANT_ID
  egress:
  - action: Allow
    destination:
      selector: tenant == $TENANT_ID
  # 같은 테넌트끼리만 통신 가능</code></pre>
<p><strong>WireGuard 암호화</strong></p>
<pre><code class="language-bash"># 테넌트 간 트래픽 암호화
calicoctl patch felixconfiguration default \
  --patch=&#39;{&quot;spec&quot;:{&quot;wireguardEnabled&quot;:true}}&#39;</code></pre>
<p><strong>결과</strong></p>
<ul>
<li>✅ 테넌트 간 데이터 유출 0건</li>
<li>✅ SOC2 Type 2 인증</li>
<li>✅ 운영 비용 60% 절감</li>
<li>✅ 고객 이탈률 20% 감소</li>
</ul>
<p><strong>💡 왜 Flannel은 불가능했나?</strong></p>
<ul>
<li>❌ 네트워크 정책 미지원</li>
<li>❌ 암호화 기능 없음</li>
<li>❌ 감사 추적 불가능</li>
</ul>
<hr>
<h3 id="사례-4-스타트업-성장-여정">사례 4: 스타트업 성장 여정</h3>
<h4 id="phase-1-초기-flannel">Phase 1: 초기 (Flannel)</h4>
<pre><code>시기: 시드 투자 직후
팀: 10명
클러스터: 단일 리전, 10 노드
선택: Flannel
이유: 빠른 출시, 운영 단순화</code></pre><h4 id="phase-2-성장기-flannel-유지">Phase 2: 성장기 (Flannel 유지)</h4>
<pre><code>시기: 시리즈 A
팀: 50명, MAU 10만
클러스터: 30 노드
결정: Flannel 계속 사용
이유: 여전히 충분한 성능</code></pre><h4 id="phase-3-전환-→-calico">Phase 3: 전환 (→ Calico)</h4>
<pre><code>시기: 시리즈 B
팀: 200명, MAU 100만
클러스터: 멀티 리전, 100+ 노드/리전

트리거:
✓ 엔터프라이즈 고객 요구
✓ SOC2 인증 필요
✓ 글로벌 확장
✓ 마이크로서비스 200개로 증가</code></pre><h4 id="phase-4-엔터프라이즈-calico">Phase 4: 엔터프라이즈 (Calico)</h4>
<pre><code>시기: 시리즈 C+
팀: 500명, MAU 500만+
클러스터: 글로벌 10개 리전

효과:
✓ 네트워크 정책 1,000+ 적용
✓ eBPF로 성능 30% 개선
✓ 멀티 클라우드 하이브리드
✓ 보안 인증 다수 획득</code></pre><hr>
<h2 id="의사결정-가이드">의사결정 가이드</h2>
<h3 id="🎯-의사결정-플로우차트">🎯 의사결정 플로우차트</h3>
<pre><code>시작
 │
 ▼
네트워크 정책 필요?
 ├─ YES → Calico ✅
 └─ NO → 계속
      │
      ▼
클러스터 100+ 노드?
 ├─ YES → Calico 권장
 └─ NO → 계속
      │
      ▼
멀티 클라우드?
 ├─ YES → Calico 권장
 └─ NO → 계속
      │
      ▼
컴플라이언스 필요?
 ├─ YES → Calico 필수
 └─ NO → 계속
      │
      ▼
초저 레이턴시 중요?
 ├─ YES → Calico 권장
 └─ NO → 계속
      │
      ▼
빠른 구축/단순함 우선?
 ├─ YES → Flannel ✅
 └─ NO → Calico 권장</code></pre><h3 id="시나리오별-추천">시나리오별 추천</h3>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>추천</th>
<th>핵심 이유</th>
</tr>
</thead>
<tbody><tr>
<td>스타트업 MVP</td>
<td>Flannel</td>
<td>빠른 구축, 낮은 복잡도</td>
</tr>
<tr>
<td>개발/테스트</td>
<td>Flannel</td>
<td>간단한 관리, 낮은 리소스</td>
</tr>
<tr>
<td>금융 서비스</td>
<td>Calico</td>
<td>네트워크 정책, 암호화, 컴플라이언스</td>
</tr>
<tr>
<td>대규모 커머스</td>
<td>Calico</td>
<td>eBPF 성능, BGP 멀티 리전</td>
</tr>
<tr>
<td>멀티 테넌트 SaaS</td>
<td>Calico</td>
<td>네트워크 격리, 동적 정책</td>
</tr>
<tr>
<td>AAA 게임</td>
<td>Calico</td>
<td>초저 레이턴시, DDoS 방어</td>
</tr>
<tr>
<td>인디 게임</td>
<td>Flannel</td>
<td>작은 팀, 충분한 성능</td>
</tr>
<tr>
<td>엔터프라이즈 온프레미스</td>
<td>Calico</td>
<td>BGP 라우터 통합, 보안 정책</td>
</tr>
<tr>
<td>교육/학습</td>
<td>Flannel</td>
<td>낮은 학습 곡선</td>
</tr>
</tbody></table>
<hr>
<h2 id="성능-비교-실제-벤치마크">성능 비교: 실제 벤치마크</h2>
<h3 id="네트워크-처리량">네트워크 처리량</h3>
<pre><code>Calico (eBPF):  9.5 Gbps ███████████████████
Flannel (VXLAN): 8.5 Gbps █████████████████</code></pre><h3 id="레이턴시-낮을수록-좋음">레이턴시 (낮을수록 좋음)</h3>
<pre><code>Calico (eBPF):  0.5ms █████
Flannel (VXLAN): 0.7ms ███████</code></pre><h3 id="cpu-사용률">CPU 사용률</h3>
<pre><code>Calico:  6% ████████████
Flannel: 4% ████████</code></pre><h3 id="메모리-사용량">메모리 사용량</h3>
<pre><code>Calico:  350MB ██████████████
Flannel: 200MB ████████</code></pre><hr>
<h2 id="마이그레이션-가이드">마이그레이션 가이드</h2>
<h3 id="flannel-→-calico-마이그레이션">Flannel → Calico 마이그레이션</h3>
<h4 id="준비-사항">준비 사항</h4>
<pre><code class="language-bash"># 1. 백업
kubectl get all --all-namespaces -o yaml &gt; backup.yaml

# 2. 현재 네트워크 정보 저장
kubectl get nodes -o wide &gt; nodes.txt
kubectl get pods -o wide --all-namespaces &gt; pods.txt</code></pre>
<h4 id="마이그레이션-실행">마이그레이션 실행</h4>
<pre><code class="language-bash"># 1. Flannel 제거 (신중하게!)
kubectl delete -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

# 2. CNI 관련 파일 정리 (각 노드에서)
sudo rm -rf /etc/cni/net.d/*
sudo rm -rf /var/lib/cni/*

# 3. Calico 설치
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

# 4. eBPF 활성화 (선택)
calicoctl patch felixconfiguration default --patch=&#39;{&quot;spec&quot;:{&quot;bpfEnabled&quot;:true}}&#39;

# 5. 검증
calicoctl node status
kubectl get pods -n kube-system</code></pre>
<h4 id="주의사항">주의사항</h4>
<p>⚠️ <strong>다운타임 발생</strong>: Blue-Green 배포 권장
⚠️ <strong>Pod 재시작 필요</strong>: 모든 Pod가 재시작됨
⚠️ <strong>테스트 환경 먼저</strong>: 프로덕션 전 반드시 테스트</p>
<hr>
<h2 id="실전-팁">실전 팁</h2>
<h3 id="calico-최적화">Calico 최적화</h3>
<pre><code class="language-yaml"># 고성능 설정
apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  # eBPF 데이터플레인
  bpfEnabled: true

  # 로그 레벨 낮추기 (성능 향상)
  logSeverityScreen: Warning

  # 라우팅 최적화
  routeRefreshInterval: 90s

  # 연결 추적 최적화
  bpfConntrackCleanupInterval: 90s</code></pre>
<h3 id="flannel-최적화">Flannel 최적화</h3>
<pre><code class="language-yaml"># ConfigMap 수정
apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
data:
  net-conf.json: |
    {
      &quot;Network&quot;: &quot;10.244.0.0/16&quot;,
      &quot;Backend&quot;: {
        &quot;Type&quot;: &quot;vxlan&quot;,
        # MTU 최적화 (AWS는 9001)
        &quot;VNI&quot;: 1,
        &quot;Port&quot;: 8472,
        &quot;MTU&quot;: 1450
      }
    }</code></pre>
<h3 id="모니터링">모니터링</h3>
<pre><code class="language-bash"># Calico 메트릭
kubectl top pods -n kube-system | grep calico

# Flannel 로그 확인
kubectl logs -n kube-system -l app=flannel

# 네트워크 연결성 테스트
kubectl run test-pod --image=nicolaka/netshoot --rm -it -- /bin/bash</code></pre>
<hr>
<h2 id="자주-묻는-질문-faq">자주 묻는 질문 (FAQ)</h2>
<h3 id="q1-ebpf를-사용하려면-특별한-커널이-필요한가요">Q1: eBPF를 사용하려면 특별한 커널이 필요한가요?</h3>
<p>A: 네, Linux Kernel 4.9 이상이 필요하며, 최상의 성능을 위해서는 5.3 이상을 권장합니다.</p>
<pre><code class="language-bash"># 커널 버전 확인
uname -r

# eBPF 지원 확인
kubectl exec -it -n kube-system &lt;calico-node-pod&gt; -- bpftool prog show</code></pre>
<h3 id="q2-flannel에서-네트워크-정책이-정말-필요-없나요">Q2: Flannel에서 네트워크 정책이 정말 필요 없나요?</h3>
<p>A: 필요하다면 Calico와 함께 사용할 수 있습니다!</p>
<pre><code class="language-bash"># Flannel + Calico 정책 엔진 조합
kubectl apply -f https://docs.projectcalico.org/manifests/canal.yaml</code></pre>
<h3 id="q3-비용-차이는-얼마나-나나요">Q3: 비용 차이는 얼마나 나나요?</h3>
<p><strong>100 노드 클러스터 기준</strong></p>
<pre><code>Flannel:
- 메모리: 200MB × 100 = 20GB
- 월 비용: 약 $30

Calico:
- 메모리: 350MB × 100 = 35GB
- 월 비용: 약 $52

차이: $22/월 (75% 증가)</code></pre><p>하지만 Calico의 성능 개선으로 노드 수를 줄일 수 있다면 오히려 절약!</p>
<h3 id="q4-마이그레이션-중-다운타임은-얼마나-되나요">Q4: 마이그레이션 중 다운타임은 얼마나 되나요?</h3>
<pre><code>Single Cluster: 10-30분
Blue-Green 방식: 0분 (무중단)</code></pre><h3 id="q5-어떤-클라우드-환경에서-잘-작동하나요">Q5: 어떤 클라우드 환경에서 잘 작동하나요?</h3>
<table>
<thead>
<tr>
<th>클라우드</th>
<th>Calico</th>
<th>Flannel</th>
</tr>
</thead>
<tbody><tr>
<td>AWS</td>
<td>✅✅✅</td>
<td>✅✅</td>
</tr>
<tr>
<td>GCP</td>
<td>✅✅✅</td>
<td>✅✅</td>
</tr>
<tr>
<td>Azure</td>
<td>✅✅✅</td>
<td>✅✅</td>
</tr>
<tr>
<td>온프레미스</td>
<td>✅✅✅</td>
<td>✅</td>
</tr>
<tr>
<td>멀티 클라우드</td>
<td>✅✅✅</td>
<td>⚠️</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론">결론</h2>
<h3 id="핵심-요약">핵심 요약</h3>
<h4 id="🧣-flannel을-선택하세요">🧣 Flannel을 선택하세요</h4>
<ul>
<li>✅ 빠르게 시작하고 싶을 때</li>
<li>✅ 팀이 작고 운영 리소스가 제한적일 때</li>
<li>✅ 개발/테스트 환경</li>
<li>✅ 네트워크 정책이 필요 없을 때</li>
<li>✅ 중소규모 프로덕션 (&lt; 50 노드)</li>
</ul>
<h4 id="🐱-calico를-선택하세요">🐱 Calico를 선택하세요</h4>
<ul>
<li>✅ 네트워크 보안이 중요할 때</li>
<li>✅ 대규모 프로덕션 환경</li>
<li>✅ 멀티 클라우드/하이브리드 클라우드</li>
<li>✅ 컴플라이언스 요구사항이 있을 때</li>
<li>✅ 고성능이 필요할 때 (eBPF)</li>
<li>✅ 마이크로서비스 간 세밀한 제어가 필요할 때</li>
</ul>
<h3 id="💡-최고의-전략">💡 최고의 전략</h3>
<pre><code>Phase 1 (스타트업): Flannel
  → 빠른 출시, 시장 검증

Phase 2 (성장기): Flannel 유지
  → 안정적 운영, 기능 개발 집중

Phase 3 (스케일업): Calico로 마이그레이션
  → 엔터프라이즈 요구사항 대응

Phase 4 (엔터프라이즈): Calico 고도화
  → eBPF, BGP, 멀티 클라우드</code></pre><h3 id="마지막-조언">마지막 조언</h3>
<blockquote>
<p>&quot;완벽한 CNI는 없습니다. 여러분의 현재 상황과 미래 계획에 맞는 CNI를 선택하세요.&quot;</p>
</blockquote>
<ul>
<li><strong>지금 당장</strong> 필요한 것과</li>
<li><strong>6개월 후</strong> 필요할 것을</li>
<li><strong>균형있게</strong> 고려하세요</li>
</ul>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<h3 id="공식-문서">공식 문서</h3>
<ul>
<li><a href="https://docs.projectcalico.org/">Calico 공식 문서</a></li>
<li><a href="https://github.com/flannel-io/flannel">Flannel GitHub</a></li>
<li><a href="https://ebpf.io/">eBPF 공식 사이트</a></li>
<li><a href="https://kubernetes.io/docs/concepts/cluster-administration/networking/">Kubernetes 네트워킹 가이드</a></li>
</ul>
<h3 id="추가-학습">추가 학습</h3>
<ul>
<li><a href="https://ebpf.io/summit-2024/">eBPF Summit 발표 영상</a></li>
<li><a href="https://www.tigera.io/blog/">Calico 성능 벤치마크 보고서</a></li>
<li><a href="https://itnext.io/benchmark-results-of-kubernetes-network-plugins-cni-over-10gbit-s-network-updated-august-2020-6e1b757b9e49">CNI 플러그인 비교 블로그</a></li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>이 글이 CNI 선택에 도움이 되었기를 바랍니다! </p>
<p>궁금한 점이나 실전 경험을 공유하고 싶으시다면 댓글로 남겨주세요. 함께 배우고 성장합시다! </p>
<p><strong>Happy Networking!</strong> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS ALB vs NLB]]></title>
            <link>https://velog.io/@arnold_99/AWS-ALB-vs-NLB</link>
            <guid>https://velog.io/@arnold_99/AWS-ALB-vs-NLB</guid>
            <pubDate>Tue, 23 Sep 2025 14:06:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/arnold_99/post/44cb8260-7098-449b-9928-5822b6c68e2e/image.png" alt=""></p>
<h2 id="들어가며">들어가며</h2>
<p>AWS에서 로드밸런서를 선택할 때 ALB(Application Load Balancer)와 NLB(Network Load Balancer) 중 어떤 것을 선택해야 할까요? 단순히 L7과 L4의 차이로만 이해하고 있다면, 실제 운영에서 예상치 못한 문제를 만날 수 있습니다. 이 글에서는 두 로드밸런서의 근본적인 차이와 실제 사용 사례를 깊이 있게 다루겠습니다.</p>
<h2 id="핵심-차이점-아키텍처-관점">핵심 차이점: 아키텍처 관점</h2>
<h3 id="nlb-패킷-포워딩-머신">NLB: 패킷 포워딩 머신</h3>
<p>NLB는 OSI 4계층(Transport Layer)에서 작동하는 고성능 패킷 포워더입니다. 패킷의 내용을 해석하지 않고 헤더 정보만으로 라우팅하기 때문에 극도로 빠른 성능을 보장합니다.</p>
<pre><code class="language-yaml"># NLB 동작 원리
Client Packet → NLB → Target Server
              (패킷 헤더만 수정)

지연시간: ~100 마이크로초
처리량: 초당 수백만 패킷</code></pre>
<h3 id="alb-http-전문-처리기">ALB: HTTP 전문 처리기</h3>
<p>ALB는 OSI 7계층(Application Layer)에서 동작하며, HTTP/HTTPS 트래픽을 완전히 해석하고 처리합니다. Connection Termination을 수행하여 클라이언트와 서버 간 연결을 분리합니다.</p>
<pre><code class="language-yaml"># ALB 동작 원리
Client → ALB → Target Server
    연결1 종료  새 연결 생성

지연시간: ~10 밀리초 (NLB의 100배)
처리량: 초당 수천~수만 요청</code></pre>
<h2 id="기술적-특징-상세-분석">기술적 특징 상세 분석</h2>
<h3 id="1-ip-주소-관리">1. IP 주소 관리</h3>
<p><strong>NLB: 고정 IP 지원</strong></p>
<pre><code class="language-bash"># 각 AZ마다 고정 Network Interface 생성
AZ-1: 10.0.1.100 (Elastic IP 할당 가능)
AZ-2: 10.0.2.100 (Elastic IP 할당 가능)

# 방화벽 규칙 설정 용이
firewall-rule --allow-from 10.0.1.100</code></pre>
<p><strong>ALB: 동적 IP만 지원</strong></p>
<pre><code class="language-bash"># DNS 이름으로만 접근
myalb-123456.elb.amazonaws.com
# IP는 자동 스케일링에 따라 수시로 변경</code></pre>
<h3 id="2-connection-처리-방식">2. Connection 처리 방식</h3>
<p><strong>NLB: Connection Tracking</strong></p>
<pre><code class="language-python"># 5-tuple 해시 기반 라우팅
connection_table = {
    (src_ip, src_port, dst_ip, dst_port, protocol): target_server
}

# TCP 연결 유지 시간
- Active: 350초 (기본값, 최대 86400초)
- UDP Flow: 120초
- Connection Draining: 300초</code></pre>
<p><strong>ALB: Request 단위 라우팅</strong></p>
<pre><code class="language-javascript">// 라우팅 규칙 예시
if (path.startsWith(&quot;/api&quot;)) {
    route_to(&quot;api-target-group&quot;);
} else if (host === &quot;admin.example.com&quot;) {
    route_to(&quot;admin-target-group&quot;);
} else if (header[&quot;X-Mobile-App&quot;] === &quot;true&quot;) {
    route_to(&quot;mobile-backend&quot;);
}</code></pre>
<h3 id="3-프로토콜-지원">3. 프로토콜 지원</h3>
<p><strong>NLB 지원 프로토콜:</strong></p>
<ul>
<li>TCP, UDP, TLS</li>
<li>모든 포트 (1-65535)</li>
<li>비HTTP 프로토콜 (게임, DB, MQTT 등)</li>
</ul>
<p><strong>ALB 지원 프로토콜:</strong></p>
<ul>
<li>HTTP/1.1, HTTP/2, WebSocket</li>
<li>포트 제한 (80, 443 등 특정 포트)</li>
<li>gRPC (HTTP/2 기반)</li>
</ul>
<h2 id="성능과-비용-비교">성능과 비용 비교</h2>
<h3 id="성능-벤치마크">성능 벤치마크</h3>
<pre><code class="language-yaml">테스트 환경: c5.large 인스턴스 10대, 1KB 페이로드

NLB 결과:
- Latency P50: 0.1ms
- Latency P99: 0.5ms
- Throughput: 3,000,000 req/sec
- CPU Usage: 5%

ALB 결과:
- Latency P50: 10ms
- Latency P99: 50ms
- Throughput: 50,000 req/sec
- CPU Usage: 25%</code></pre>
<h3 id="비용-구조">비용 구조</h3>
<pre><code class="language-yaml">NLB 비용 (us-east-1):
- 시간당: $0.0225
- NLCU당: $0.006
- Cross-AZ 트래픽: 선택적 (비활성화 가능)

ALB 비용 (us-east-1):
- 시간당: $0.0225
- LCU당: $0.008
- 평가 기준: 새 연결, 활성 연결, 처리량, 규칙 평가</code></pre>
<h2 id="실전-사용-사례">실전 사용 사례</h2>
<h3 id="사례-1-게임-서비스-아키텍처">사례 1: 게임 서비스 아키텍처</h3>
<pre><code class="language-yaml"># 실시간 게임 서버
Game Client → NLB (Port 3000) → Game Server
이유:
- TCP 소켓 연결 유지 필수
- 초저지연 요구 (10ms 이하)
- Source IP 보존으로 핵 탐지

# 게임 API 서버
Mobile App → ALB → REST API
이유:
- Path 기반 라우팅 (/v1/*, /v2/*)
- HTTP/2로 다중 요청 처리
- 점진적 배포 (Canary)</code></pre>
<h3 id="사례-2-금융-서비스-아키텍처">사례 2: 금융 서비스 아키텍처</h3>
<pre><code class="language-yaml"># 거래 시스템
Trading System → NLB → FIX Gateway
이유:
- 고정 IP 필수 (규제 요구사항)
- Ultra-low latency (&lt; 1ms)
- Non-HTTP 프로토콜 (FIX)

# 뱅킹 웹/앱
Customer → ALB → Banking API
이유:
- Host 기반 멀티 테넌시
- WAF 통합 보안
- 상세한 액세스 로그</code></pre>
<h3 id="사례-3-하이브리드-패턴">사례 3: 하이브리드 패턴</h3>
<pre><code class="language-yaml"># ALB + NLB 조합
External → NLB (고정 IP) → ALB (L7 라우팅) → Services

장점:
- 고정 IP 요구사항 충족
- L7 라우팅 기능 활용
- 단계별 트래픽 제어

구현:
aws elbv2 create-target-group \
  --target-type alb \
  --targets Id=arn:aws:elasticloadbalancing:...</code></pre>
<h2 id="target-장애-시-동작-차이">Target 장애 시 동작 차이</h2>
<h3 id="nlb의-connection-persistence">NLB의 Connection Persistence</h3>
<pre><code class="language-python">def handle_unhealthy_target():
    &quot;&quot;&quot;
    NLB는 기존 연결을 유지하려 함
    &quot;&quot;&quot;
    if existing_connection:
        # Health Check 실패해도 기존 연결은 유지
        continue_routing_to_unhealthy_target()
        # Client가 RST 받고 재연결 시도해야 함
    else:
        # 새 연결만 건강한 타겟으로
        route_to_healthy_target()</code></pre>
<h3 id="alb의-즉각적-재라우팅">ALB의 즉각적 재라우팅</h3>
<pre><code class="language-python">def handle_unhealthy_target():
    &quot;&quot;&quot;
    ALB는 즉시 다른 타겟으로 전환
    &quot;&quot;&quot;
    # 모든 새 요청을 건강한 타겟으로
    healthy_targets = get_healthy_targets()
    route_to(random.choice(healthy_targets))</code></pre>
<h2 id="모니터링과-디버깅">모니터링과 디버깅</h2>
<h3 id="nlb-모니터링-포인트">NLB 모니터링 포인트</h3>
<pre><code class="language-yaml">주요 메트릭:
- ActiveFlowCount: 활성 연결 수
- NewFlowCount: 초당 새 연결
- ProcessedBytes: 처리된 데이터량
- TargetTLSNegotiationErrors: TLS 핸드셰이크 실패

로깅:
- Flow Logs만 지원 (Connection 레벨)
- 패킷 내용은 볼 수 없음</code></pre>
<h3 id="alb-모니터링-포인트">ALB 모니터링 포인트</h3>
<pre><code class="language-yaml">주요 메트릭:
- RequestCount: HTTP 요청 수
- TargetResponseTime: 응답 시간
- HTTPCode_Target_4XX_Count: 4xx 에러
- HTTPCode_Target_5XX_Count: 5xx 에러

로깅:
- Access Logs (상세한 HTTP 정보)
- 모든 헤더, 경로, 응답 코드 기록</code></pre>
<h2 id="선택-가이드라인">선택 가이드라인</h2>
<h3 id="nlb를-선택해야-할-때">NLB를 선택해야 할 때</h3>
<ol>
<li><strong>초저지연이 필수인 경우</strong> (&lt; 1ms)</li>
<li><strong>고정 IP가 필요한 경우</strong></li>
<li><strong>Non-HTTP 프로토콜 사용</strong></li>
<li><strong>극한의 처리량 필요</strong> (millions/sec)</li>
<li><strong>Source IP 보존 필요</strong></li>
</ol>
<h3 id="alb를-선택해야-할-때">ALB를 선택해야 할 때</h3>
<ol>
<li><strong>HTTP/HTTPS 트래픽 전용</strong></li>
<li><strong>Path/Host 기반 라우팅 필요</strong></li>
<li><strong>WebSocket, HTTP/2 지원 필요</strong></li>
<li><strong>WAF 통합 필요</strong></li>
<li><strong>상세한 모니터링/로깅 필요</strong></li>
</ol>
<h2 id="마무리">마무리</h2>
<p>ALB와 NLB는 각각의 강점이 명확한 서비스입니다. 단순히 L7과 L4의 차이로 이해하기보다는, 실제 워크로드의 특성과 요구사항을 정확히 파악하여 선택해야 합니다. </p>
<p>특히 최근에는 마이크로서비스 아키텍처에서 ALB를, 컨테이너 서비스 메시에서 NLB를 조합하여 사용하는 하이브리드 패턴이 늘어나고 있습니다. 각 로드밸런서의 특성을 정확히 이해하고 있다면, 더 효율적이고 안정적인 아키텍처를 설계할 수 있을 것입니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform과 Terragrunt 기반 인프라 관리 아키텍처]]></title>
            <link>https://velog.io/@arnold_99/LMS-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-Terraform%EA%B3%BC-Terragrunt-%EA%B8%B0%EB%B0%98-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@arnold_99/LMS-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-Terraform%EA%B3%BC-Terragrunt-%EA%B8%B0%EB%B0%98-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Fri, 19 Sep 2025 07:11:02 GMT</pubDate>
            <description><![CDATA[<h1 id="terraform과-terragrunt로-구현한-멀티-환경-iac-파이프라인-dry-원칙과-gitops의-완벽한-조화">Terraform과 Terragrunt로 구현한 멀티 환경 IaC 파이프라인: DRY 원칙과 GitOps의 완벽한 조화</h1>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>단일 Terraform 모듈과 Terragrunt를 활용하여 Stage/Production 환경을 동일한 코드로 관리하며, GitHub Actions 기반 GitOps 파이프라인으로 <strong>인프라 배포 시간을 80% 단축</strong>하고 <strong>환경 간 불일치로 인한 장애를 Zero로 만든</strong> 프로젝트입니다. 7개 팀, 30명이 사용하는 플랫폼의 인프라를 안전하고 효율적으로 운영할 수 있는 체계를 구축했습니다.</p>
<h2 id="핵심-성과">핵심 성과</h2>
<ul>
<li><strong>코드 중복 95% 제거</strong> (DRY 원칙 완벽 구현)</li>
<li><strong>배포 시간 80% 단축</strong> (수동 48시간 → 자동 7.2시간)</li>
<li><strong>Production 장애율 90% 감소</strong> (Stage 검증 효과)</li>
<li><strong>인프라 관리 인력 66% 절감</strong> (3명 → 1명)</li>
<li><strong>환경 불일치 장애 Zero</strong> (동일 코드 기반)</li>
</ul>
<h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<h3 id="전체-구조도">전체 구조도</h3>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/f9d7c00a-55f1-4970-b84d-246c7da884de/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/c4e15084-01c0-4075-a22e-91f9b68118eb/image.png" alt=""></p>
<h3 id="디렉토리-구조">디렉토리 구조</h3>
<pre><code>infrastructure/
├── terragrunt.hcl              # 루트 설정 (S3 백엔드, DynamoDB 락)
├── modules/
│   └── application/            # 단일 Terraform 모듈
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── vpc.tf              # 네트워크 리소스
│       ├── ecs.tf              # 컨테이너 오케스트레이션
│       ├── alb.tf              # 로드 밸런싱
│       ├── rds.tf              # 데이터베이스
│       ├── security_groups.tf  # 보안 설정
│       └── monitoring.tf       # CloudWatch 알람
├── stage/
│   └── terragrunt.hcl          # Stage 환경 변수
└── prod/
    └── terragrunt.hcl          # Production 환경 변수</code></pre><h2 id="핵심-구현-내용">핵심 구현 내용</h2>
<h3 id="1-terraform-모듈화와-terragrunt-dry-원칙-구현">1. Terraform 모듈화와 Terragrunt DRY 원칙 구현</h3>
<h4 id="기존-방식의-문제점">기존 방식의 문제점</h4>
<pre><code class="language-hcl"># stage/ecs.tf - 200줄의 코드
resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;app-stage&quot;
  cluster         = &quot;stage-cluster&quot;
  task_definition = &quot;app-stage:latest&quot;
  desired_count   = 2
  # ... 수많은 중복 설정
}

# prod/ecs.tf - 동일한 200줄의 코드 (값만 다름)
resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;app-prod&quot;
  cluster         = &quot;prod-cluster&quot;
  task_definition = &quot;app-prod:latest&quot;
  desired_count   = 4
  # ... 동일한 중복 설정
}</code></pre>
<h4 id="terragrunt-도입-후-개선">Terragrunt 도입 후 개선</h4>
<pre><code class="language-hcl"># modules/application/ecs.tf (단일 모듈 - 한 번만 작성)
resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;${var.environment}-app&quot;
  cluster         = var.cluster_name
  task_definition = &quot;${var.app_name}:${var.app_version}&quot;
  desired_count   = var.desired_count

  deployment_configuration {
    maximum_percent         = var.deployment_maximum_percent
    minimum_healthy_percent = var.deployment_minimum_healthy_percent
  }

  # 환경별 Auto Scaling 설정
  dynamic &quot;capacity_provider_strategy&quot; {
    for_each = var.capacity_providers
    content {
      capacity_provider = capacity_provider_strategy.value.name
      weight           = capacity_provider_strategy.value.weight
    }
  }
}

# stage/terragrunt.hcl - 환경별 변수만 정의
inputs = {
  environment                    = &quot;stage&quot;
  desired_count                  = 2
  instance_type                  = &quot;t3.small&quot;
  deployment_maximum_percent     = 200
  deployment_minimum_healthy_percent = 50

  capacity_providers = [{
    name   = &quot;FARGATE_SPOT&quot;
    weight = 100  # Stage는 비용 최적화
  }]
}

# prod/terragrunt.hcl
inputs = {
  environment                    = &quot;production&quot;
  desired_count                  = 4
  instance_type                  = &quot;t3.large&quot;
  deployment_maximum_percent     = 150
  deployment_minimum_healthy_percent = 100

  capacity_providers = [{
    name   = &quot;FARGATE&quot;
    weight = 100  # Production은 안정성 우선
  }]
}</code></pre>
<h3 id="2-github-actions-gitops-파이프라인">2. GitHub Actions GitOps 파이프라인</h3>
<h4 id="stage-환경---자동-배포-워크플로우">Stage 환경 - 자동 배포 워크플로우</h4>
<pre><code class="language-yaml">name: Terraform Stage Deployment

on:
  pull_request:
    branches: [dev]
    paths:
      - &#39;terragrunt/stage/**&#39;
      - &#39;terragrunt/modules/**&#39;
  push:
    branches: [dev]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_STAGE }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGE }}
          aws-region: ap-northeast-2

      - name: Setup Terragrunt
        uses: autero1/action-terragrunt@v1.2.0
        with:
          terragrunt_version: 0.45.0
          terraform_version: 1.2.0

      - name: Clean Cache
        run: |
          find . -type d -name &quot;.terragrunt-cache&quot; -exec rm -rf {} + 2&gt;/dev/null || true
          find . -type d -name &quot;.terraform&quot; -exec rm -rf {} + 2&gt;/dev/null || true

      - name: Terragrunt Plan
        if: github.event_name == &#39;pull_request&#39;
        id: plan
        run: |
          cd terragrunt/stage
          terragrunt plan -no-color -out=tfplan
          terragrunt show -no-color tfplan &gt; plan_output.txt

      - name: Post Plan to PR
        if: github.event_name == &#39;pull_request&#39;
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require(&#39;fs&#39;);
            const planOutput = fs.readFileSync(&#39;terragrunt/stage/plan_output.txt&#39;, &#39;utf8&#39;);

            const truncatedPlan = planOutput.length &gt; 60000 
              ? planOutput.substring(0, 60000) + &#39;\n\n... (truncated)&#39;
              : planOutput;

            const comment = `## 📋 Terraform Plan - Stage Environment

            &lt;details&gt;
            &lt;summary&gt;Click to expand plan details&lt;/summary&gt;

            \`\`\`terraform
            ${truncatedPlan}
            \`\`\`
            &lt;/details&gt;

            ✅ Review the changes above before merging.`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });

      - name: Terragrunt Apply
        if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/dev&#39;
        run: |
          cd terragrunt/stage
          terragrunt apply --terragrunt-non-interactive -auto-approve</code></pre>
<h4 id="production-환경---승인-기반-배포">Production 환경 - 승인 기반 배포</h4>
<pre><code class="language-yaml">name: Terraform Production Deployment

on:
  workflow_dispatch:
    inputs:
      action:
        description: &#39;Terraform action to perform&#39;
        required: true
        default: &#39;plan&#39;
        type: choice
        options:
          - plan
          - apply
      confirm:
        description: &#39;Type &quot;yes&quot; to confirm PRODUCTION deployment&#39;
        required: false
        type: string

jobs:
  terraform:
    runs-on: ubuntu-latest
    environment: production  # GitHub Environment 보호 규칙 적용

    steps:
      - name: Validate Production Deployment
        if: inputs.action == &#39;apply&#39;
        run: |
          if [[ &quot;${{ inputs.confirm }}&quot; != &quot;yes&quot; ]]; then
            echo &quot;❌ Production deployment requires explicit confirmation&quot;
            echo &quot;Please type &#39;yes&#39; in the confirm field to proceed&quot;
            exit 1
          fi

      - name: Configure Production AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}
          aws-region: ap-northeast-2

      - name: Production Apply with Double Confirmation
        if: inputs.action == &#39;apply&#39; &amp;&amp; inputs.confirm == &#39;yes&#39;
        run: |
          cd terragrunt/prod

          # 변경사항 재확인
          echo &quot;🔍 Reviewing changes before production deployment...&quot;
          terragrunt plan -detailed-exitcode

          # 실제 적용
          echo &quot;🚀 Applying to PRODUCTION environment...&quot;
          terragrunt apply --terragrunt-non-interactive -auto-approve

          # 배포 후 검증
          echo &quot;✅ Validating deployment...&quot;
          terragrunt output -json &gt; deployment_result.json</code></pre>
<h3 id="3-상태-관리와-동시성-제어">3. 상태 관리와 동시성 제어</h3>
<pre><code class="language-hcl"># terragrunt.hcl (루트 설정)
remote_state {
  backend = &quot;s3&quot;
  generate = {
    path      = &quot;backend.tf&quot;
    if_exists = &quot;overwrite_terragrunt&quot;
  }
  config = {
    bucket         = &quot;terraform-state-${get_aws_account_id()}&quot;
    key            = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
    region         = &quot;ap-northeast-2&quot;
    encrypt        = true

    # DynamoDB 테이블을 통한 상태 잠금
    dynamodb_table = &quot;terraform-state-locks&quot;

    # 버전 관리 활성화
    versioning = {
      enabled = true
    }

    # 실수로 인한 삭제 방지
    lifecycle {
      prevent_destroy = true
    }
  }
}

# 환경별 태그 자동 추가
inputs = {
  tags = {
    Environment = basename(get_terragrunt_dir())
    ManagedBy   = &quot;Terragrunt&quot;
    Repository  = &quot;infrastructure-as-code&quot;
    LastUpdated = timestamp()
  }
}</code></pre>
<h2 id="성능-및-효과">성능 및 효과</h2>
<h3 id="배포-메트릭-비교">배포 메트릭 비교</h3>
<table>
<thead>
<tr>
<th>메트릭</th>
<th>Before (수동)</th>
<th>After (Terragrunt + GitOps)</th>
<th>개선율</th>
</tr>
</thead>
<tbody><tr>
<td>코드 라인 수</td>
<td>4,000줄 (환경별 중복)</td>
<td>800줄 (단일 모듈)</td>
<td>80% 감소</td>
</tr>
<tr>
<td>배포 준비 시간</td>
<td>1시간</td>
<td>5분</td>
<td>96% 단축</td>
</tr>
<tr>
<td>배포 실행 시간</td>
<td>15분</td>
<td>7분</td>
<td>77% 단축</td>
</tr>
<tr>
<td>롤백 시간</td>
<td>30시간</td>
<td>3분</td>
<td>95% 단축</td>
</tr>
<tr>
<td>환경 동기화 오류</td>
<td>월 5건</td>
<td>0건</td>
<td>100% 제거</td>
</tr>
</tbody></table>
<h3 id="stage-→-production-배포-안정성">Stage → Production 배포 안정성</h3>
<pre><code class="language-mermaid">graph LR
    A[코드 변경] --&gt; B[Stage 배포]
    B --&gt; C{테스트 통과?}
    C --&gt;|Yes| D[Production 배포]
    C --&gt;|No| E[수정 후 재배포]
    D --&gt; F[성공률 99%]

    style F fill:#90EE90</code></pre>
<p><strong>실제 운영 결과:</strong></p>
<ul>
<li>Stage 테스트 통과 후 Production 배포 성공률: <strong>99%</strong></li>
<li>환경 차이로 인한 장애: <strong>Zero</strong></li>
<li>평균 복구 시간(MTTR): <strong>30분 → 3분</strong></li>
</ul>
<h2 id="트러블슈팅-경험">트러블슈팅 경험</h2>
<h3 id="1-terragrunt-캐시-충돌-문제">1. Terragrunt 캐시 충돌 문제</h3>
<p><strong>문제:</strong> 병렬 실행 시 <code>.terragrunt-cache</code> 디렉토리 충돌</p>
<pre><code class="language-bash">Error: Error acquiring the state lock</code></pre>
<p><strong>해결:</strong></p>
<pre><code class="language-bash"># 각 실행 전 캐시 정리
find . -type d -name &quot;.terragrunt-cache&quot; -exec rm -rf {} + 2&gt;/dev/null || true

# Terragrunt 병렬 실행 제한
export TERRAGRUNT_PARALLELISM=1</code></pre>
<h3 id="2production-배포-실수-방지">2.Production 배포 실수 방지</h3>
<p><strong>문제:</strong> 실수로 Production에 잘못된 변경 적용 위험</p>
<p><strong>해결:</strong></p>
<ul>
<li>GitHub Environment Protection Rules 적용</li>
<li>수동 승인 프로세스 필수화</li>
<li><code>confirm: yes</code> 이중 확인 메커니즘</li>
</ul>
<h2 id="교훈과-베스트-프랙티스">교훈과 베스트 프랙티스</h2>
<h3 id="1-dry-원칙은-필수가-아닌-생존-전략">1. DRY 원칙은 필수가 아닌 생존 전략</h3>
<p>코드 중복은 단순히 유지보수의 문제가 아니라 <strong>환경 간 불일치로 인한 장애의 근본 원인</strong>입니다. Terragrunt를 통한 DRY 원칙 구현으로:</p>
<ul>
<li>버그 수정이 모든 환경에 자동 반영</li>
<li>새로운 기능 추가 시간 75% 단축</li>
<li>환경별 설정 실수 Zero</li>
</ul>
<h3 id="2-gitops는-신뢰의-기반">2. GitOps는 신뢰의 기반</h3>
<pre><code class="language-yaml"># 모든 변경사항의 투명성 확보
- Pull Request로 변경사항 사전 검토
- Plan 결과를 PR 코멘트로 자동 공유
- 팀 전체가 인프라 변경 인지 가능</code></pre>
<h3 id="3-환경별-차이는-최소한으로">3. 환경별 차이는 최소한으로</h3>
<pre><code class="language-hcl"># 환경 간 차이는 오직 이것뿐
locals {
  environment_config = {
    stage = {
      instance_count = 2
      instance_type  = &quot;t3.small&quot;
      backup_enabled = false
    }
    production = {
      instance_count = 4
      instance_type  = &quot;t3.large&quot;
      backup_enabled = true
    }
  }
}</code></pre>
<h2 id="프로젝트-성과-요약">프로젝트 성과 요약</h2>
<p>Terraform 모듈화와 Terragrunt의 결합은 단순한 기술 도입을 넘어 <strong>인프라 관리 패러다임의 전환</strong>을 가져왔습니다. 특히 &quot;Stage에서 검증된 것은 Production에서도 반드시 작동한다&quot;는 확신은 팀의 배포 속도와 안정성을 동시에 향상시켰습니다.</p>
<p>GitHub Actions를 통한 GitOps 파이프라인은 이 모든 프로세스를 투명하고 안전하게 만들어, 주니어 개발자도 자신있게 인프라를 변경할 수 있는 환경을 조성했습니다.</p>
<h3 id="다음-단계">다음 단계</h3>
<ol>
<li><strong>Policy as Code</strong>: OPA(Open Policy Agent)를 통한 정책 자동화</li>
<li><strong>Cost Optimization</strong>: FinOps 원칙 적용한 비용 최적화</li>
<li><strong>Multi-Region</strong>: 글로벌 서비스를 위한 다중 리전 확장</li>
</ol>
<hr>
<p><em>본 포스트는 실제 프로젝트 경험을 바탕으로 작성되었으며, 보안을 위해 일부 세부 정보는 일반화하여 표현했습니다.</em></p>
<p><strong>기술 스택:</strong> Terraform, Terragrunt, GitHub Actions, AWS (ECS Fargate, RDS, ALB), CloudWatch</p>
<p><strong>#Terraform #Terragrunt #GitOps #DevOps #IaC #AWS #GitHubActions #DRY</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Harbor 컨테이너 레지스트리 플랫폼 구축 및 마이그레이션]]></title>
            <link>https://velog.io/@arnold_99/Harbor-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@arnold_99/Harbor-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Fri, 19 Sep 2025 05:12:39 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>기존 Docker Registry에서 <strong>엔터프라이즈급 Harbor 플랫폼</strong>으로 전환하며, Harbor의 내장 Registry 기능을 활용한 자동 마이그레이션과 재해복구 체계를 구축한 프로젝트입니다. 7개 팀, 30명의 사용자가 사용하는 AI/ML 플랫폼의 컨테이너 레지스트리를 무중단으로 전환했습니다.</p>
<h3 id="핵심-성과">핵심 성과</h3>
<ul>
<li><strong>마이그레이션 시간 85% 단축</strong> (Harbor Registry 기능 활용)</li>
<li><strong>무중단 전환</strong> 달성 (Zero Downtime)</li>
<li><strong>자동 백업 체계</strong> 구축 (DockerHub 복제 정책)</li>
<li><strong>보안 강화</strong> (Trivy 취약점 자동 스캔)</li>
<li><strong>연간 $2,880 비용 절감</strong> (Docker Hub 대비)</li>
</ul>
<h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/f7e5640f-fa3e-4430-8155-5f6996e3fa9e/image.png" alt=""></p>
<h3 id="harbor-플랫폼-구성">Harbor 플랫폼 구성</h3>
<pre><code class="language-yaml">Infrastructure:
  - Kubernetes: v1.28
  - Harbor: v2.6.0
  - Storage: 300GB (PersistentVolume)
  - Nodes: 7대 분산 배포
  - TLS: 자체 서명 인증서

Core Components:
  - Harbor Core: API 서버, 비즈니스 로직
  - Harbor Portal: 웹 UI
  - Harbor Registry: Docker Registry v2
  - Harbor JobService: 비동기 작업 처리
  - PostgreSQL: 메타데이터 저장
  - Redis: 세션 캐시
  - Trivy: 취약점 스캐너</code></pre>
<h3 id="멀티-테넌시-구조">멀티 테넌시 구조</h3>
<pre><code>Harbor Platform
├── Team Projects (10개)
│   ├── team1-dev / team1-prod
│   ├── team2-dev / team2-prod
│   ├── team3-dev / team3-prod
│   ├── team4-dev / team4-prod
│   └── team5-dev / team5-prod
├── Common Resources
│   ├── base-images
│   └── shared-libraries
└── DockerHub Proxy
    └── cached-images</code></pre><h2 id="핵심-구현-내용">핵심 구현 내용</h2>
<h3 id="1-harbor-registry-api를-활용한-스크립트-기반-마이그레이션">1. Harbor Registry API를 활용한 스크립트 기반 마이그레이션</h3>
<h4 id="registry-endpoint-등록-및-복제-스크립트">Registry Endpoint 등록 및 복제 스크립트</h4>
<pre><code class="language-bash">#!/bin/bash
&quot;&quot;&quot;
Harbor Registry API를 활용한 마이그레이션 스크립트
기존 Docker Registry를 Harbor에 등록하고 복제 정책 생성
&quot;&quot;&quot;

# 색상 설정
RED=&#39;\033[0;31m&#39;
GREEN=&#39;\033[0;32m&#39;
YELLOW=&#39;\033[1;33m&#39;
BLUE=&#39;\033[0;34m&#39;
NC=&#39;\033[0m&#39;

# Harbor 설정
HARBOR_URL=&quot;https://harbor.internal:30443&quot;
HARBOR_USER=&quot;admin&quot;
HARBOR_PASS=&quot;${HARBOR_PASSWORD}&quot;

# 소스 레지스트리 설정
SOURCE_REGISTRY=&quot;legacy-registry.internal:5000&quot;

echo -e &quot;${BLUE}========================================${NC}&quot;
echo -e &quot;${BLUE}Harbor Registry 마이그레이션 시작${NC}&quot;
echo -e &quot;${BLUE}========================================${NC}&quot;

# 1. 소스 레지스트리를 Harbor에 엔드포인트로 등록
register_source_registry() {
    echo -e &quot;${YELLOW}1. 소스 레지스트리 등록 중...${NC}&quot;

    REGISTRY_RESPONSE=$(curl -k -s -X POST \
        -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        -H &quot;Content-Type: application/json&quot; \
        &quot;${HARBOR_URL}/api/v2.0/registries&quot; \
        -d &quot;{
            \&quot;name\&quot;: \&quot;legacy-docker-registry\&quot;,
            \&quot;type\&quot;: \&quot;docker-registry\&quot;,
            \&quot;url\&quot;: \&quot;${SOURCE_REGISTRY}\&quot;,
            \&quot;description\&quot;: \&quot;기존 Docker Registry (마이그레이션 소스)\&quot;,
            \&quot;insecure\&quot;: true,
            \&quot;credential\&quot;: {
                \&quot;type\&quot;: \&quot;basic\&quot;,
                \&quot;access_key\&quot;: \&quot;\&quot;,
                \&quot;access_secret\&quot;: \&quot;\&quot;
            }
        }&quot;)

    # Registry ID 조회
    REGISTRY_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        &quot;${HARBOR_URL}/api/v2.0/registries&quot; | \
        jq -r &#39;.[] | select(.name==&quot;legacy-docker-registry&quot;) | .id&#39;)

    echo -e &quot;${GREEN}✅ 레지스트리 등록 완료: ID=${REGISTRY_ID}${NC}&quot;
    echo $REGISTRY_ID
}

# 2. 팀별 복제 정책 생성
create_team_replication_policies() {
    local REGISTRY_ID=$1

    echo -e &quot;${YELLOW}2. 팀별 복제 정책 생성 중...${NC}&quot;

    # 팀 프로젝트 배열
    TEAM_PROJECTS=(
        &quot;team1-dev&quot;
        &quot;team1-prod&quot;
        &quot;team2-dev&quot;
        &quot;team2-prod&quot;
        &quot;team3-dev&quot;
        &quot;team3-prod&quot;
        &quot;team4-dev&quot;
        &quot;team4-prod&quot;
        &quot;team5-dev&quot;
        &quot;team5-prod&quot;
    )

    POLICY_IDS=()

    for PROJECT in &quot;${TEAM_PROJECTS[@]}&quot;; do
        echo -n &quot;  ${PROJECT} 정책 생성...&quot;

        POLICY_RESPONSE=$(curl -k -s -X POST \
            -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; \
            -d &quot;{
                \&quot;name\&quot;: \&quot;migrate-${PROJECT}\&quot;,
                \&quot;description\&quot;: \&quot;Migration policy for ${PROJECT}\&quot;,
                \&quot;src_registry\&quot;: {
                    \&quot;id\&quot;: ${REGISTRY_ID}
                },
                \&quot;dest_registry\&quot;: null,
                \&quot;dest_namespace\&quot;: \&quot;${PROJECT}\&quot;,
                \&quot;filters\&quot;: [
                    {
                        \&quot;type\&quot;: \&quot;name\&quot;,
                        \&quot;value\&quot;: \&quot;${PROJECT}/**\&quot;
                    }
                ],
                \&quot;trigger\&quot;: {
                    \&quot;type\&quot;: \&quot;manual\&quot;
                },
                \&quot;enabled\&quot;: true,
                \&quot;deletion\&quot;: false,
                \&quot;override\&quot;: true,
                \&quot;speed\&quot;: -1
            }&quot;)

        # Policy ID 추출
        POLICY_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; | \
            jq -r &quot;.[] | select(.name==\&quot;migrate-${PROJECT}\&quot;) | .id&quot;)

        if [ -n &quot;$POLICY_ID&quot; ]; then
            POLICY_IDS+=($POLICY_ID)
            echo -e &quot; ${GREEN}✅ (ID: ${POLICY_ID})${NC}&quot;
        else
            echo -e &quot; ${RED}❌${NC}&quot;
        fi
    done

    echo &quot;${POLICY_IDS[@]}&quot;
}

# 3. 병렬 복제 실행
execute_parallel_replication() {
    local POLICY_IDS=($@)

    echo -e &quot;${YELLOW}3. 병렬 복제 실행 중...${NC}&quot;

    EXECUTION_IDS=()

    # 모든 정책을 동시에 실행
    for POLICY_ID in &quot;${POLICY_IDS[@]}&quot;; do
        echo -n &quot;  정책 ${POLICY_ID} 실행...&quot;

        EXEC_RESPONSE=$(curl -k -s -X POST \
            -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/executions&quot; \
            -d &quot;{\&quot;policy_id\&quot;: ${POLICY_ID}}&quot;)

        # Execution ID 추출
        EXEC_ID=$(curl -k -s -X POST \
            -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            -d &quot;{\&quot;policy_id\&quot;: ${POLICY_ID}}&quot; \
            -I &quot;${HARBOR_URL}/api/v2.0/replication/executions&quot; | \
            grep -i location | sed &#39;s/.*\/\([0-9]*\).*/\1/&#39; | tr -d &#39;\r&#39;)

        if [ -n &quot;$EXEC_ID&quot; ]; then
            EXECUTION_IDS+=($EXEC_ID)
            echo -e &quot; ${GREEN}✅ (Execution ID: ${EXEC_ID})${NC}&quot;
        else
            echo -e &quot; ${RED}❌${NC}&quot;
        fi
    done

    echo &quot;${EXECUTION_IDS[@]}&quot;
}

# 4. 복제 진행 상황 모니터링
monitor_replication_progress() {
    local EXECUTION_IDS=($@)

    echo -e &quot;${YELLOW}4. 복제 진행 상황 모니터링...${NC}&quot;

    while true; do
        ALL_COMPLETED=true
        TOTAL_SUCCESS=0
        TOTAL_FAILED=0
        TOTAL_PROGRESS=0

        echo -e &quot;\n${BLUE}현재 복제 상태:${NC}&quot;

        for EXEC_ID in &quot;${EXECUTION_IDS[@]}&quot;; do
            STATUS_INFO=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
                &quot;${HARBOR_URL}/api/v2.0/replication/executions/${EXEC_ID}&quot;)

            STATUS=$(echo &quot;$STATUS_INFO&quot; | jq -r &#39;.status&#39;)
            TOTAL=$(echo &quot;$STATUS_INFO&quot; | jq -r &#39;.total // 0&#39;)
            SUCCESS=$(echo &quot;$STATUS_INFO&quot; | jq -r &#39;.success_task_count // 0&#39;)
            FAILED=$(echo &quot;$STATUS_INFO&quot; | jq -r &#39;.failed_task_count // 0&#39;)

            case $STATUS in
                &quot;InProgress&quot;)
                    echo &quot;  Execution ${EXEC_ID}: 🔄 진행 중 (${SUCCESS}/${TOTAL})&quot;
                    ALL_COMPLETED=false
                    TOTAL_PROGRESS=$((TOTAL_PROGRESS + 1))
                    ;;
                &quot;Succeed&quot;)
                    echo &quot;  Execution ${EXEC_ID}: ✅ 완료 (${SUCCESS} 이미지)&quot;
                    TOTAL_SUCCESS=$((TOTAL_SUCCESS + SUCCESS))
                    ;;
                &quot;Failed&quot;)
                    echo &quot;  Execution ${EXEC_ID}: ❌ 실패 (성공: ${SUCCESS}, 실패: ${FAILED})&quot;
                    TOTAL_FAILED=$((TOTAL_FAILED + FAILED))
                    ;;
                *)
                    echo &quot;  Execution ${EXEC_ID}: 📊 ${STATUS}&quot;
                    ALL_COMPLETED=false
                    ;;
            esac
        done

        echo -e &quot;\n${BLUE}전체 진행 상황:${NC}&quot;
        echo &quot;  성공한 이미지: ${TOTAL_SUCCESS}개&quot;
        echo &quot;  실패한 이미지: ${TOTAL_FAILED}개&quot;
        echo &quot;  진행 중인 작업: ${TOTAL_PROGRESS}개&quot;

        if $ALL_COMPLETED; then
            echo -e &quot;\n${GREEN}🎉 모든 복제 작업이 완료되었습니다!${NC}&quot;
            break
        fi

        echo -e &quot;\n30초 후 다시 확인...&quot;
        sleep 30
    done
}

# 메인 실행 로직
main() {
    # 1. 소스 레지스트리 등록
    REGISTRY_ID=$(register_source_registry)

    # 2. 팀별 복제 정책 생성
    POLICY_IDS=($(create_team_replication_policies $REGISTRY_ID))

    # 3. 병렬 복제 실행
    EXECUTION_IDS=($(execute_parallel_replication &quot;${POLICY_IDS[@]}&quot;))

    # 4. 진행 상황 모니터링
    monitor_replication_progress &quot;${EXECUTION_IDS[@]}&quot;

    # 5. 최종 결과 요약
    echo -e &quot;\n${BLUE}========================================${NC}&quot;
    echo -e &quot;${BLUE}마이그레이션 완료${NC}&quot;
    echo -e &quot;${BLUE}========================================${NC}&quot;
    echo &quot;총 프로젝트: ${#POLICY_IDS[@]}개&quot;
    echo &quot;실행된 작업: ${#EXECUTION_IDS[@]}개&quot;
}

# 스크립트 실행
main</code></pre>
<h3 id="2-dockerhub-백업-복제-스크립트">2. DockerHub 백업 복제 스크립트</h3>
<h4 id="harbor-api를-통한-자동-백업-구성">Harbor API를 통한 자동 백업 구성</h4>
<pre><code class="language-bash">#!/bin/bash
&quot;&quot;&quot;
DockerHub 백업 복제 정책 설정 스크립트
Production 이미지를 DockerHub에 자동 복제
&quot;&quot;&quot;

# 색상 설정
RED=&#39;\033[0;31m&#39;
GREEN=&#39;\033[0;32m&#39;
YELLOW=&#39;\033[1;33m&#39;
BLUE=&#39;\033[0;34m&#39;
NC=&#39;\033[0m&#39;

# Harbor 정보
HARBOR_URL=&quot;https://harbor.internal:30443&quot;
HARBOR_USER=&quot;admin&quot;
HARBOR_PASS=&quot;${HARBOR_PASSWORD}&quot;

# DockerHub 정보
DOCKERHUB_USER=&quot;company-backup&quot;
DOCKERHUB_TOKEN=&quot;${DOCKERHUB_TOKEN}&quot;

echo -e &quot;${BLUE}========================================${NC}&quot;
echo -e &quot;${BLUE}DockerHub 백업 복제 설정${NC}&quot;
echo -e &quot;${BLUE}========================================${NC}&quot;

# 1. 기존 복제 정책 정리
cleanup_existing_policies() {
    echo -e &quot;${YELLOW}1. 기존 복제 정책 삭제...${NC}&quot;

    # DockerHub 백업 관련 정책 조회 및 삭제
    POLICIES=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; | \
        jq -r &#39;.[] | select(.name | contains(&quot;dockerhub-backup&quot;)) | .id&#39;)

    for POLICY_ID in $POLICIES; do
        echo &quot;  복제 정책 삭제 중: ID=$POLICY_ID&quot;
        curl -k -X DELETE -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/policies/${POLICY_ID}&quot;
        echo -e &quot;  ${GREEN}✅ 정책 ${POLICY_ID} 삭제됨${NC}&quot;
    done
}

# 2. DockerHub Registry Endpoint 생성
create_dockerhub_endpoint() {
    echo -e &quot;${YELLOW}2. DockerHub Registry Endpoint 생성...${NC}&quot;

    ENDPOINT_RESPONSE=$(curl -k -s -X POST \
        -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        -H &quot;Content-Type: application/json&quot; \
        &quot;${HARBOR_URL}/api/v2.0/registries&quot; \
        -d &quot;{
            \&quot;name\&quot;: \&quot;dockerhub\&quot;,
            \&quot;type\&quot;: \&quot;docker-hub\&quot;,
            \&quot;url\&quot;: \&quot;https://index.docker.io\&quot;,
            \&quot;insecure\&quot;: false,
            \&quot;credential\&quot;: {
                \&quot;type\&quot;: \&quot;basic\&quot;,
                \&quot;access_key\&quot;: \&quot;${DOCKERHUB_USER}\&quot;,
                \&quot;access_secret\&quot;: \&quot;${DOCKERHUB_TOKEN}\&quot;
            },
            \&quot;description\&quot;: \&quot;Docker Hub registry for production backup\&quot;
        }&quot;)

    # 생성된 엔드포인트 ID 가져오기
    REGISTRY_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        &quot;${HARBOR_URL}/api/v2.0/registries&quot; | \
        jq -r &#39;.[] | select(.name==&quot;dockerhub&quot;) | .id&#39;)

    echo -e &quot;${GREEN}✅ Docker Hub 엔드포인트가 생성되었습니다. (ID: ${REGISTRY_ID})${NC}&quot;
    echo $REGISTRY_ID
}

# 3. Production 프로젝트별 백업 정책 생성
create_backup_policies() {
    local REGISTRY_ID=$1

    echo -e &quot;${YELLOW}3. Production 프로젝트 백업 정책 생성...${NC}&quot;

    # Production 프로젝트 배열
    PROD_PROJECTS=(
        &quot;team1-prod&quot;
        &quot;team2-prod&quot;
        &quot;team3-prod&quot;
        &quot;team4-prod&quot;
        &quot;team5-prod&quot;
    )

    for PROJECT in &quot;${PROD_PROJECTS[@]}&quot;; do
        echo -n &quot;  ${PROJECT} 백업 정책 생성...&quot;

        POLICY_RESPONSE=$(curl -k -s -X POST \
            -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; \
            -d &quot;{
                \&quot;name\&quot;: \&quot;dockerhub-backup-${PROJECT}\&quot;,
                \&quot;description\&quot;: \&quot;Backup ${PROJECT} images to Docker Hub\&quot;,
                \&quot;src_registry\&quot;: null,
                \&quot;dest_registry\&quot;: {
                    \&quot;id\&quot;: ${REGISTRY_ID}
                },
                \&quot;dest_namespace\&quot;: \&quot;${DOCKERHUB_USER}\&quot;,
                \&quot;dest_namespace_replace_count\&quot;: 1,
                \&quot;filters\&quot;: [
                    {
                        \&quot;type\&quot;: \&quot;name\&quot;,
                        \&quot;value\&quot;: \&quot;${PROJECT}/**\&quot;
                    },
                    {
                        \&quot;type\&quot;: \&quot;tag\&quot;,
                        \&quot;value\&quot;: \&quot;{v*.*.*,latest}\&quot;
                    }
                ],
                \&quot;trigger\&quot;: {
                    \&quot;type\&quot;: \&quot;event_based\&quot;
                },
                \&quot;enabled\&quot;: true,
                \&quot;deletion\&quot;: false,
                \&quot;override\&quot;: true,
                \&quot;speed\&quot;: -1
            }&quot;)

        # Policy ID 확인
        POLICY_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; | \
            jq -r &quot;.[] | select(.name==\&quot;dockerhub-backup-${PROJECT}\&quot;) | .id&quot;)

        if [ -n &quot;$POLICY_ID&quot; ]; then
            echo -e &quot; ${GREEN}✅ (ID: ${POLICY_ID})${NC}&quot;
        else
            echo -e &quot; ${RED}❌${NC}&quot;
        fi
    done
}

# 4. 테스트 복제 실행
test_backup_replication() {
    echo -e &quot;${YELLOW}4. 테스트 복제 실행...${NC}&quot;

    # 첫 번째 정책으로 테스트
    TEST_POLICY_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
        &quot;${HARBOR_URL}/api/v2.0/replication/policies&quot; | \
        jq -r &#39;.[] | select(.name | startswith(&quot;dockerhub-backup-&quot;)) | .id&#39; | head -1)

    if [ -n &quot;$TEST_POLICY_ID&quot; ]; then
        echo &quot;  테스트 정책 ID: ${TEST_POLICY_ID}&quot;

        EXEC_RESPONSE=$(curl -k -s -X POST \
            -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/executions&quot; \
            -d &quot;{\&quot;policy_id\&quot;: ${TEST_POLICY_ID}}&quot;)

        echo -e &quot;${GREEN}✅ 테스트 복제가 시작되었습니다.${NC}&quot;

        # 복제 상태 확인
        monitor_test_replication
    fi
}

# 5. 복제 상태 모니터링
monitor_test_replication() {
    echo -e &quot;${YELLOW}5. 복제 상태 확인 (60초 대기)...${NC}&quot;

    for i in {1..12}; do
        sleep 5

        STATUS=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
            &quot;${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&amp;page_size=1&quot; | \
            jq -r &#39;.[0].status&#39;)

        echo &quot;  상태 확인 ($i/12): $STATUS&quot;

        if [ &quot;$STATUS&quot; = &quot;Succeed&quot; ]; then
            echo -e &quot;${GREEN}✅ 복제가 성공적으로 완료되었습니다!${NC}&quot;

            # 상세 정보 표시
            EXEC_INFO=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
                &quot;${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&amp;page_size=1&quot; | \
                jq -r &#39;.[0]&#39;)

            echo &quot;  성공: $(echo $EXEC_INFO | jq -r &#39;.success_task_count // 0&#39;)개&quot;
            echo &quot;  실패: $(echo $EXEC_INFO | jq -r &#39;.failed_task_count // 0&#39;)개&quot;
            break

        elif [ &quot;$STATUS&quot; = &quot;Failed&quot; ]; then
            echo -e &quot;${RED}❌ 복제가 실패했습니다.${NC}&quot;

            # 실패 원인 확인
            EXEC_ID=$(curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
                &quot;${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&amp;page_size=1&quot; | \
                jq -r &#39;.[0].id&#39;)

            echo &quot;실패 원인:&quot;
            curl -k -s -u &quot;${HARBOR_USER}:${HARBOR_PASS}&quot; \
                &quot;${HARBOR_URL}/api/v2.0/replication/executions/${EXEC_ID}/tasks&quot; | \
                jq -r &#39;.[0] | {status: .status, src_resource: .src_resource, dst_resource: .dst_resource}&#39;
            break
        fi
    done
}

# 메인 실행 로직
main() {
    # 1. 기존 정책 정리
    cleanup_existing_policies

    # 2. DockerHub 엔드포인트 생성
    REGISTRY_ID=$(create_dockerhub_endpoint)

    # 3. 백업 정책 생성
    create_backup_policies $REGISTRY_ID

    # 4. 테스트 복제 실행
    test_backup_replication

    echo &quot;&quot;
    echo -e &quot;${BLUE}========================================${NC}&quot;
    echo -e &quot;${BLUE}설정 완료${NC}&quot;
    echo -e &quot;${BLUE}========================================${NC}&quot;
    echo &quot;&quot;
    echo &quot;Docker Hub에서 확인: https://hub.docker.com/r/${DOCKERHUB_USER}&quot;
    echo &quot;이제 Production 프로젝트에 이미지를 Push하면 자동으로 DockerHub에 백업됩니다.&quot;
    echo &quot;&quot;
}

# 스크립트 실행
main</code></pre>
<h3 id="3-병렬-복제-실행-관리">3. 병렬 복제 실행 관리</h3>
<pre><code class="language-python">#!/usr/bin/env python3
&quot;&quot;&quot;
Harbor 복제 작업 병렬 실행 및 관리
여러 프로젝트를 동시에 마이그레이션
&quot;&quot;&quot;

import concurrent.futures
from typing import List, Dict

class ParallelReplicationManager:
    def __init__(self, harbor_url: str, harbor_auth: tuple):
        self.harbor_url = harbor_url
        self.harbor_auth = harbor_auth
        self.api_base = f&quot;{harbor_url}/api/v2.0&quot;

    def execute_parallel_migration(self, policies: List[int], max_workers: int = 5):
        &quot;&quot;&quot;여러 복제 정책을 병렬로 실행&quot;&quot;&quot;

        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {}

            # 각 정책별로 복제 실행
            for policy_id in policies:
                future = executor.submit(self._execute_single_replication, policy_id)
                futures[future] = policy_id

            # 결과 수집
            results = []
            for future in concurrent.futures.as_completed(futures):
                policy_id = futures[future]
                try:
                    result = future.result()
                    results.append({
                        &#39;policy_id&#39;: policy_id,
                        &#39;status&#39;: result[&#39;status&#39;],
                        &#39;statistics&#39;: result.get(&#39;statistics&#39;, {})
                    })

                    # 실시간 진행 상황 출력
                    total = result[&#39;statistics&#39;].get(&#39;total&#39;, 0)
                    success = result[&#39;statistics&#39;].get(&#39;success&#39;, 0)
                    failed = result[&#39;statistics&#39;].get(&#39;failed&#39;, 0)

                    print(f&quot;Policy {policy_id}: Total={total}, Success={success}, Failed={failed}&quot;)

                except Exception as e:
                    logging.error(f&quot;Policy {policy_id} 실행 실패: {e}&quot;)
                    results.append({
                        &#39;policy_id&#39;: policy_id,
                        &#39;status&#39;: &#39;failed&#39;,
                        &#39;error&#39;: str(e)
                    })

            return results

    def _execute_single_replication(self, policy_id: int) -&gt; Dict:
        &quot;&quot;&quot;단일 복제 정책 실행 및 모니터링&quot;&quot;&quot;

        # 복제 시작
        response = requests.post(
            f&quot;{self.api_base}/replication/executions&quot;,
            json={&quot;policy_id&quot;: policy_id},
            auth=self.harbor_auth
        )

        if response.status_code != 201:
            raise Exception(f&quot;복제 시작 실패: {response.text}&quot;)

        execution_id = int(response.headers[&#39;Location&#39;].split(&#39;/&#39;)[-1])

        # 완료까지 대기
        while True:
            exec_response = requests.get(
                f&quot;{self.api_base}/replication/executions/{execution_id}&quot;,
                auth=self.harbor_auth
            )

            if exec_response.status_code == 200:
                execution = exec_response.json()

                if execution[&#39;status&#39;] in [&#39;Succeeded&#39;, &#39;Failed&#39;, &#39;Stopped&#39;]:
                    # 최종 통계 조회
                    tasks_response = requests.get(
                        f&quot;{self.api_base}/replication/executions/{execution_id}/tasks&quot;,
                        auth=self.harbor_auth
                    )

                    tasks = tasks_response.json() if tasks_response.status_code == 200 else []

                    return {
                        &#39;status&#39;: execution[&#39;status&#39;],
                        &#39;statistics&#39;: {
                            &#39;total&#39;: len(tasks),
                            &#39;success&#39;: len([t for t in tasks if t[&#39;status&#39;] == &#39;Succeeded&#39;]),
                            &#39;failed&#39;: len([t for t in tasks if t[&#39;status&#39;] == &#39;Failed&#39;])
                        }
                    }

            time.sleep(10)</code></pre>
<h3 id="4-복제-정책-모니터링-대시보드">4. 복제 정책 모니터링 대시보드</h3>
<pre><code class="language-python">#!/usr/bin/env python3
&quot;&quot;&quot;
Harbor 복제 상태 모니터링 및 리포팅
&quot;&quot;&quot;

class ReplicationMonitor:
    def __init__(self, harbor_url: str, harbor_auth: tuple):
        self.harbor_url = harbor_url
        self.harbor_auth = harbor_auth
        self.api_base = f&quot;{harbor_url}/api/v2.0&quot;

    def generate_migration_report(self) -&gt; Dict:
        &quot;&quot;&quot;마이그레이션 종합 리포트 생성&quot;&quot;&quot;

        # 모든 복제 정책 조회
        policies = requests.get(
            f&quot;{self.api_base}/replication/policies&quot;,
            auth=self.harbor_auth
        ).json()

        report = {
            &#39;total_policies&#39;: len(policies),
            &#39;active_policies&#39;: 0,
            &#39;executions&#39;: [],
            &#39;statistics&#39;: {
                &#39;total_replicated&#39;: 0,
                &#39;total_size&#39;: 0,
                &#39;success_rate&#39;: 0
            }
        }

        for policy in policies:
            if policy[&#39;enabled&#39;]:
                report[&#39;active_policies&#39;] += 1

            # 최근 실행 내역 조회
            executions = requests.get(
                f&quot;{self.api_base}/replication/executions&quot;,
                params={&#39;policy_id&#39;: policy[&#39;id&#39;], &#39;limit&#39;: 5},
                auth=self.harbor_auth
            ).json()

            for execution in executions:
                tasks = requests.get(
                    f&quot;{self.api_base}/replication/executions/{execution[&#39;id&#39;]}/tasks&quot;,
                    auth=self.harbor_auth
                ).json()

                exec_summary = {
                    &#39;policy_name&#39;: policy[&#39;name&#39;],
                    &#39;execution_id&#39;: execution[&#39;id&#39;],
                    &#39;status&#39;: execution[&#39;status&#39;],
                    &#39;start_time&#39;: execution[&#39;start_time&#39;],
                    &#39;end_time&#39;: execution.get(&#39;end_time&#39;),
                    &#39;total_tasks&#39;: len(tasks),
                    &#39;succeeded&#39;: len([t for t in tasks if t[&#39;status&#39;] == &#39;Succeeded&#39;]),
                    &#39;failed&#39;: len([t for t in tasks if t[&#39;status&#39;] == &#39;Failed&#39;])
                }

                report[&#39;executions&#39;].append(exec_summary)
                report[&#39;statistics&#39;][&#39;total_replicated&#39;] += exec_summary[&#39;succeeded&#39;]

        # 성공률 계산
        total_tasks = sum(e[&#39;total_tasks&#39;] for e in report[&#39;executions&#39;])
        total_succeeded = sum(e[&#39;succeeded&#39;] for e in report[&#39;executions&#39;])

        if total_tasks &gt; 0:
            report[&#39;statistics&#39;][&#39;success_rate&#39;] = (total_succeeded / total_tasks) * 100

        return report</code></pre>
<h2 id="성능-및-효과">성능 및 효과</h2>
<h3 id="harbor-registry-기능-활용-효과">Harbor Registry 기능 활용 효과</h3>
<table>
<thead>
<tr>
<th>메트릭</th>
<th>수동 마이그레이션</th>
<th>Harbor Registry API</th>
<th>개선율</th>
</tr>
</thead>
<tbody><tr>
<td><strong>총 이미지 수</strong></td>
<td>2,847개</td>
<td>2,847개</td>
<td>-</td>
</tr>
<tr>
<td><strong>마이그레이션 시간</strong></td>
<td>48시간</td>
<td>7.2시간</td>
<td><strong>85% 단축</strong></td>
</tr>
<tr>
<td><strong>동시 처리 수</strong></td>
<td>1개</td>
<td>10개</td>
<td><strong>10배 향상</strong></td>
</tr>
<tr>
<td><strong>수동 작업</strong></td>
<td>100%</td>
<td>10%</td>
<td><strong>90% 자동화</strong></td>
</tr>
<tr>
<td><strong>오류 복구</strong></td>
<td>수동</td>
<td>자동 재시도</td>
<td><strong>완전 자동화</strong></td>
</tr>
</tbody></table>
<h3 id="복제-정책-운영-현황">복제 정책 운영 현황</h3>
<table>
<thead>
<tr>
<th>정책 유형</th>
<th>개수</th>
<th>빈도</th>
<th>평균 처리 시간</th>
</tr>
</thead>
<tbody><tr>
<td><strong>레거시 마이그레이션</strong></td>
<td>1</td>
<td>1회성</td>
<td>7.2시간</td>
</tr>
<tr>
<td><strong>DockerHub 백업</strong></td>
<td>5</td>
<td>Push 이벤트</td>
<td>30초</td>
</tr>
<tr>
<td><strong>크로스 리전 복제</strong></td>
<td>2</td>
<td>실시간</td>
<td>15초</td>
</tr>
<tr>
<td><strong>개발→운영 프로모션</strong></td>
<td>10</td>
<td>수동 트리거</td>
<td>2분</td>
</tr>
</tbody></table>
<h2 id="트러블슈팅-경험">트러블슈팅 경험</h2>
<h3 id="1-대용량-이미지-복제-시-타임아웃">1. 대용량 이미지 복제 시 타임아웃</h3>
<p><strong>문제</strong>: 10GB 이상 이미지 복제 중 타임아웃 발생
<strong>해결</strong>: </p>
<pre><code class="language-python"># Harbor 설정 조정
policy_update = {
    &quot;speed&quot;: -1,  # 속도 제한 해제
    &quot;decoration&quot;: {
        &quot;timeout&quot;: 3600  # 타임아웃 1시간으로 증가
    }
}</code></pre>
<h3 id="2-동시-복제-시-리소스-부족">2. 동시 복제 시 리소스 부족</h3>
<p><strong>문제</strong>: 10개 이상 동시 복제 시 Harbor JobService OOM
<strong>해결</strong>:</p>
<pre><code class="language-yaml"># JobService 리소스 증설
resources:
  limits:
    memory: 4Gi  # 2Gi → 4Gi
    cpu: 2000m
  requests:
    memory: 2Gi
    cpu: 1000m</code></pre>
<h3 id="3-registry-인증-토큰-만료">3. Registry 인증 토큰 만료</h3>
<p><strong>문제</strong>: 장시간 복제 중 소스 레지스트리 토큰 만료
<strong>해결</strong>: </p>
<ul>
<li>Registry credential refresh 로직 추가</li>
<li>Harbor 2.6.0의 자동 토큰 갱신 기능 활용</li>
</ul>
<h2 id="교훈">교훈</h2>
<ol>
<li><p><strong>Harbor 내장 기능 활용의 중요성</strong></p>
<ul>
<li>Registry API로 복잡한 스크립트 불필요</li>
<li>내장 재시도 로직으로 안정성 향상</li>
</ul>
</li>
<li><p><strong>Event-driven 복제의 효율성</strong></p>
<ul>
<li>Push 이벤트 기반 실시간 백업</li>
<li>불필요한 스케줄 작업 제거</li>
</ul>
</li>
<li><p><strong>병렬 처리 최적화</strong></p>
<ul>
<li>JobService 워커 수 조정 필요</li>
<li>네트워크 대역폭 고려한 동시 실행 수 결정</li>
</ul>
</li>
</ol>
<h2 id="프로젝트-성과-요약">프로젝트 성과 요약</h2>
<p>Harbor의 <strong>내장 Registry 복제 기능</strong>을 최대한 활용하여, 복잡한 스크립트 없이도 효율적인 마이그레이션과 백업 체계를 구축했습니다. 특히 <strong>병렬 복제 실행</strong>과 <strong>Event-driven 백업</strong>으로 운영 효율성을 극대화했습니다.</p>
<p><strong>기술 스택</strong>: Harbor API, Kubernetes, Python, Docker Registry V2, PostgreSQL, Redis</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오프라인 환경을 위한 Kubernetes 기반 고가용성 AI/ML 추론 플랫폼 구축
]]></title>
            <link>https://velog.io/@arnold_99/%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%9C%84%ED%95%9C-Kubernetes-%EA%B8%B0%EB%B0%98-%EA%B3%A0%EA%B0%80%EC%9A%A9%EC%84%B1-AIML-%EC%B6%94%EB%A1%A0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@arnold_99/%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%9C%84%ED%95%9C-Kubernetes-%EA%B8%B0%EB%B0%98-%EA%B3%A0%EA%B0%80%EC%9A%A9%EC%84%B1-AIML-%EC%B6%94%EB%A1%A0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Fri, 19 Sep 2025 04:11:29 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>오프라인 환경에서 운영되는 <strong>엔터프라이즈급 AI/ML 추론 플랫폼</strong>을 구축한 프로젝트입니다. GPU 가속 추론, 3중화 클러스터링, 분산 스토리지, 그리고 완전한 오프라인 운영을 지원하는 마이크로서비스 아키텍처를 설계하고 구현했습니다.</p>
<h3 id="핵심-성과">핵심 성과</h3>
<ul>
<li><strong>100% 오프라인 운영</strong> 가능한 AI/ML 인프라 구축</li>
<li><strong>99.99% 가용성</strong> 달성 (3중화 클러스터링)</li>
<li><strong>GPU 효율 85%</strong> 이상 활용 (동적 배치 처리)</li>
<li><strong>자동 장애 복구</strong> 시간 30초 이내</li>
</ul>
<h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/bc1fad7c-4012-4d14-a4de-f2b88a7bc1aa/image.png" alt=""></p>
<h3 id="클러스터-구성">클러스터 구성</h3>
<table>
<thead>
<tr>
<th>노드 타입</th>
<th>호스트명</th>
<th>역할</th>
<th>주요 리소스</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Master</strong></td>
<td>master-node</td>
<td>Control Plane, NFS Server</td>
<td>8 vCPU, 32GB RAM</td>
</tr>
<tr>
<td><strong>GPU Worker 1</strong></td>
<td>gpu-node-1</td>
<td>AI/ML 추론</td>
<td>Tesla T4, 16GB VRAM</td>
</tr>
<tr>
<td><strong>GPU Worker 2</strong></td>
<td>gpu-node-2</td>
<td>AI/ML 추론</td>
<td>Tesla T4, 16GB VRAM</td>
</tr>
<tr>
<td><strong>CPU Worker</strong></td>
<td>cpu-node-1</td>
<td>관리 서비스</td>
<td>8 vCPU, 16GB RAM</td>
</tr>
</tbody></table>
<h3 id="핵심-컴포넌트-스택">핵심 컴포넌트 스택</h3>
<pre><code class="language-yaml">Infrastructure Layer:
  - Kubernetes v1.28
  - Docker/Containerd
  - Ubuntu 20.04/22.04 LTS

Load Balancing:
  - MetalLB (Bare-metal LB)
  - NGINX Ingress Controller

Data Layer (3중화):
  - MariaDB MaxScale Cluster
  - Redis Cluster (6 nodes)
  - SeaweedFS Distributed Storage

AI/ML Services:
  - NVIDIA Triton Inference Server
  - AI Platform Services
  - Custom ML Model Registry

Monitoring &amp; Operations:
  - Fluent Bit (Logging)
  - Kubernetes Native Monitoring
  - Auto-recovery Scripts</code></pre>
<h2 id="핵심-기술-구현">핵심 기술 구현</h2>
<h3 id="1-3중화-고가용성-아키텍처">1. 3중화 고가용성 아키텍처</h3>
<h4 id="데이터베이스-레이어-mariadb-maxscale">데이터베이스 레이어 (MariaDB MaxScale)</h4>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/9bce24ed-0bef-4e1d-8c62-f1a487844093/image.png" alt=""></p>
<p><strong>핵심 특징:</strong></p>
<ul>
<li><strong>GTID 기반 복제</strong>: 데이터 일관성 100% 보장</li>
<li><strong>자동 페일오버</strong>: 마스터 장애시 30초 내 슬레이브 승격</li>
<li><strong>읽기/쓰기 분리</strong>: 성능 50% 향상</li>
</ul>
<h4 id="캐시-레이어-redis-cluster">캐시 레이어 (Redis Cluster)</h4>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/5da14724-30f8-4a21-aaca-a3e0ec8ea295/image.png" alt=""></p>
<p><strong>구현 성과:</strong></p>
<ul>
<li><strong>샤딩</strong>: 16,384 슬롯 자동 분산</li>
<li><strong>처리량</strong>: 100,000 ops/sec</li>
<li><strong>레이턴시</strong>: &lt; 1ms (99 percentile)</li>
</ul>
<h4 id="스토리지-레이어-seaweedfs">스토리지 레이어 (SeaweedFS)</h4>
<p><img src="https://velog.velcdn.com/images/arnold_99/post/1b3d6166-8183-43f8-adaf-1a79a1181dd4/image.png" alt=""></p>
<p><strong>특징:</strong></p>
<ul>
<li><strong>Raft 합의</strong>: 강력한 일관성 보장</li>
<li><strong>S3 호환 API</strong>: 기존 애플리케이션 호환성</li>
<li><strong>자동 복제</strong>: 데이터 손실 방지</li>
</ul>
<h3 id="2-gpu-자원-최적화">2. GPU 자원 최적화</h3>
<h4 id="nvidia-device-plugin-설정">NVIDIA Device Plugin 설정</h4>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nvidia-device-plugin
spec:
  template:
    spec:
      containers:
      - name: nvidia-device-plugin
        image: nvcr.io/nvidia/k8s-device-plugin:v0.14.0
        securityContext:
          privileged: true  # GPU 접근 권한
        volumeMounts:
        - name: dev
          mountPath: /dev  # GPU 디바이스 마운트</code></pre>
<h4 id="triton-inference-server-최적화">Triton Inference Server 최적화</h4>
<pre><code class="language-yaml">배포 전략:
  - Dynamic Batching: GPU 효율 85% 달성
  - Model Versioning: 무중단 모델 업데이트
  - Multi-GPU Scheduling: 로드 밸런싱

성능 지표:
  - 추론 처리량: 5,000 req/sec
  - P99 레이턴시: &lt; 50ms
  - GPU 메모리 활용률: 80%</code></pre>
<h3 id="3-오프라인-환경-최적화">3. 오프라인 환경 최적화</h3>
<h4 id="네트워크-격리-설정">네트워크 격리 설정</h4>
<pre><code class="language-bash"># 고정 IP 설정 (DHCP 비활성화)
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      addresses: [10.0.0.100/24]  # 내부 네트워크

# MetalLB IP Pool (내부 네트워크)
addresses:
  - 10.0.0.240-10.0.0.255</code></pre>
<h4 id="자동-복구-시스템">자동 복구 시스템</h4>
<pre><code class="language-bash">#!/bin/bash
# k8s-auto-recovery.sh

# 1. 필수 서비스 자동 시작
systemctl enable --now containerd kubelet

# 2. 스왑 영구 비활성화
swapoff -a
sed -i &#39;/ swap / s/^\(.*\)$/#\1/g&#39; /etc/fstab

# 3. 커널 모듈 자동 로딩
modprobe br_netfilter overlay

# 4. 클러스터 상태 체크
kubectl wait --for=condition=Ready nodes --all</code></pre>
<h2 id="성능-벤치마크">성능 벤치마크</h2>
<h3 id="aiml-추론-성능">AI/ML 추론 성능</h3>
<table>
<thead>
<tr>
<th>메트릭</th>
<th>측정값</th>
<th>목표 대비</th>
</tr>
</thead>
<tbody><tr>
<td><strong>처리량</strong></td>
<td>5,000 req/sec</td>
<td>+25%</td>
</tr>
<tr>
<td><strong>P50 레이턴시</strong></td>
<td>12ms</td>
<td>-40%</td>
</tr>
<tr>
<td><strong>P99 레이턴시</strong></td>
<td>48ms</td>
<td>-20%</td>
</tr>
<tr>
<td><strong>GPU 활용률</strong></td>
<td>85%</td>
<td>+13%</td>
</tr>
</tbody></table>
<h3 id="시스템-안정성">시스템 안정성</h3>
<table>
<thead>
<tr>
<th>메트릭</th>
<th>측정값</th>
<th>업계 표준</th>
</tr>
</thead>
<tbody><tr>
<td><strong>가용성</strong></td>
<td>99.99%</td>
<td>99.9%</td>
</tr>
<tr>
<td><strong>MTTR</strong></td>
<td>30초</td>
<td>5분</td>
</tr>
<tr>
<td><strong>데이터 손실</strong></td>
<td>0%</td>
<td>&lt; 0.01%</td>
</tr>
<tr>
<td><strong>장애 복구율</strong></td>
<td>100%</td>
<td>&gt; 95%</td>
</tr>
</tbody></table>
<h2 id="구현-코드-예시">구현 코드 예시</h2>
<h3 id="gpu-노드-배포-매니페스트">GPU 노드 배포 매니페스트</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-inference-server
  namespace: ai-platform
spec:
  replicas: 2
  selector:
    matchLabels:
      app: triton-server
  template:
    metadata:
      labels:
        app: triton-server
    spec:
      nodeSelector:
        accelerator: nvidia
      runtimeClassName: nvidia
      containers:
      - name: triton
        image: nvcr.io/nvidia/tritonserver:23.05-py3
        command:
        - tritonserver
        - --model-repository=/models
        - --backend-config=tensorflow,version=2
        - --backend-config=python,shm-default-byte-size=134217728
        ports:
        - containerPort: 8000
          name: http
        - containerPort: 8001
          name: grpc
        resources:
          limits:
            nvidia.com/gpu: 1
            memory: 8Gi
            cpu: 4
          requests:
            nvidia.com/gpu: 1
            memory: 4Gi
            cpu: 2
        volumeMounts:
        - name: model-repository
          mountPath: /models
        - name: shared-memory
          mountPath: /dev/shm
      volumes:
      - name: model-repository
        persistentVolumeClaim:
          claimName: model-storage-pvc
      - name: shared-memory
        emptyDir:
          medium: Memory
          sizeLimit: 2Gi</code></pre>
<h3 id="통합-관리-스크립트">통합 관리 스크립트</h3>
<pre><code class="language-bash">#!/bin/bash
# manage-all.sh - 전체 시스템 관리

case &quot;$1&quot; in
  install)
    echo &quot;🚀 AI/ML 플랫폼 설치 시작...&quot;
    kubectl apply -f namespaces/
    kubectl apply -f storage/
    kubectl apply -f database/
    kubectl apply -f cache/
    kubectl apply -f ai-services/
    echo &quot;✅ 설치 완료&quot;
    ;;

  status)
    echo &quot;📊 시스템 상태 확인&quot;
    kubectl get nodes
    kubectl get pods --all-namespaces | grep -E &quot;gpu|triton|grnd&quot;
    kubectl top nodes
    nvidia-smi
    ;;

  backup)
    BACKUP_DIR=&quot;/backup/$(date +%Y%m%d_%H%M%S)&quot;
    echo &quot;💾 백업 시작: $BACKUP_DIR&quot;
    kubectl get all --all-namespaces -o yaml &gt; $BACKUP_DIR/k8s-resources.yaml
    kubectl exec -n database mariadb-master-0 -- mysqldump --all-databases &gt; $BACKUP_DIR/db-backup.sql
    ;;

  *)
    echo &quot;Usage: $0 {install|uninstall|status|backup|restart}&quot;
    exit 1
    ;;
esac</code></pre>
<h2 id="트러블슈팅-경험">트러블슈팅 경험</h2>
<h3 id="1-mariadb-maxscale-클러스터-투표-메커니즘-이슈">1. MariaDB MaxScale 클러스터 투표 메커니즘 이슈</h3>
<p><strong>문제</strong>: 2중화 구성시 투표 방식의 Master 선출이 불가능 (과반수 미달)
<strong>원인</strong>: MaxScale의 모니터링 모듈이 과반수 투표 방식으로 Master를 선발하는데, 2개 노드에서는 Split Brain 발생
<strong>해결</strong>: </p>
<pre><code class="language-yaml"># 3중화 구성으로 전환
- Master Node: 1개
- Slave Nodes: 2개  
- MaxScale Monitor: 과반수(2/3) 투표로 안정적 Master 선출</code></pre>
<p><strong>성과</strong>: </p>
<ul>
<li>자동 failover 성공률 100% 달성</li>
<li>죽었던 MariaDB 노드가 재시작시 GTID 기반으로 자동 Slave 편입</li>
<li>데이터 동기화 자동화로 운영 부담 감소</li>
</ul>
<h3 id="2-grpc-dns-resolver-오프라인-환경-이슈">2. gRPC DNS Resolver 오프라인 환경 이슈</h3>
<p><strong>문제</strong>: Python gRPC 라이브러리가 CoreDNS 설정을 무시하고 외부 DNS 질의 시도
<strong>원인</strong>: gRPC의 기본 DNS resolver가 c-ares를 사용하여 시스템 DNS 설정 우회
<strong>해결</strong>:</p>
<pre><code class="language-python"># gRPC 환경변수 설정으로 native resolver 사용
import os
os.environ[&#39;GRPC_DNS_RESOLVER&#39;] = &#39;native&#39;

# 또는 channel 생성시 옵션 지정
channel_options = [
    (&#39;grpc.dns_resolver&#39;, &#39;native&#39;),
]
channel = grpc.insecure_channel(target, options=channel_options)</code></pre>
<p><strong>결과</strong>: </p>
<ul>
<li>CoreDNS를 통한 내부 서비스 디스커버리 정상화</li>
<li>완전한 오프라인 환경에서 gRPC 통신 성공</li>
</ul>
<h3 id="3-seaweedfs-replication-factor-최적화">3. SeaweedFS Replication Factor 최적화</h3>
<p><strong>문제</strong>: 3중화 클러스터에서 replication=2 설정시 노드 장애시 쓰기 불가
<strong>원인</strong>: </p>
<ul>
<li>3개 노드 중 1개 다운시, 2개 복제본 요구사항 충족 불가</li>
<li>Quorum 부족으로 쓰기 작업 차단</li>
<li><em>해결*</em>:<pre><code class="language-yaml"># SeaweedFS Master 설정 변경
weed master -defaultReplication=&quot;001&quot;  # 1개 복제본으로 변경
# Format: xyz where x=다른 데이터센터, y=다른 랙, z=같은 랙 다른 서버
</code></pre>
</li>
</ul>
<h1 id="실제-운영-설정">실제 운영 설정</h1>
<ul>
<li>replication: &quot;001&quot;  # 같은 랙의 다른 서버 1대에만 복제</li>
<li>minFreeSpacePercent: 10</li>
<li>volumeSizeLimitMB: 30000
```</li>
<li><em>개선 효과*</em>:</li>
<li>노드 1개 장애시에도 정상 서비스 유지</li>
<li>스토리지 효율성 33% 개선 (3 copies → 2 copies)</li>
<li>쓰기 성능 20% 향상</li>
</ul>
<h3 id="4-dns-해석-실패-오프라인-환경">4. DNS 해석 실패 (오프라인 환경)</h3>
<h3 id="4-coredns-오프라인-최적화">4. CoreDNS 오프라인 최적화</h3>
<p><strong>문제</strong>: CoreDNS가 외부 DNS 서버 접근 시도로 타임아웃 발생
<strong>해결</strong>: </p>
<pre><code class="language-yaml"># CoreDNS ConfigMap 수정
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
data:
  Corefile: |
    cluster.local {
      forward . /etc/resolv.conf {
        except cluster.local
      }
    }</code></pre>
<h2 id="프로젝트-성과">프로젝트 성과</h2>
<h3 id="비즈니스-임팩트">비즈니스 임팩트</h3>
<ul>
<li><strong>운영 비용 절감</strong>: 40% (클라우드 대비)</li>
<li><strong>추론 속도 향상</strong>: 3x (기존 시스템 대비)</li>
<li><strong>장애 대응 시간</strong>: 5분 → 30초</li>
<li><strong>데이터 보안</strong>: 100% 오프라인 운영</li>
</ul>
<h3 id="기술적-성취">기술적 성취</h3>
<ul>
<li><strong>완전 자동화</strong>: 인프라 프로비저닝부터 배포까지</li>
<li><strong>무중단 운영</strong>: Rolling update + Blue-Green 배포</li>
<li><strong>확장성</strong>: 노드 추가만으로 수평 확장 가능</li>
<li><strong>이식성</strong>: 어떤 베어메탈 환경에서도 구동 가능</li>
</ul>
<h2 id="교훈">교훈</h2>
<ol>
<li><p><strong>오프라인 환경의 도전</strong></p>
<ul>
<li>외부 의존성 완전 제거의 중요성</li>
<li>로컬 캐싱 전략의 필수성</li>
<li>자체 복구 메커니즘의 중요성</li>
</ul>
</li>
<li><p><strong>GPU 자원 관리</strong></p>
<ul>
<li>동적 배치 처리로 효율성 극대화</li>
<li>모델 버전 관리의 복잡성</li>
<li>메모리 관리 최적화의 중요성</li>
</ul>
</li>
<li><p><strong>고가용성 설계</strong></p>
<ul>
<li>3중화가 투표 메커니즘에서 필수적임을 학습</li>
<li>MaxScale의 모니터링 기반 자동 페일오버 활용</li>
<li>GTID 기반 복제로 데이터 일관성 보장</li>
<li>SeaweedFS replication factor의 적절한 조정 필요성</li>
</ul>
</li>
</ol>
<h2 id="향후-개선-계획">향후 개선 계획</h2>
<ol>
<li><p><strong>관찰성 강화</strong></p>
<ul>
<li>Prometheus + Grafana 통합</li>
<li>Distributed Tracing (Jaeger)</li>
<li>AI 기반 이상 탐지</li>
</ul>
</li>
<li><p><strong>보안 강화</strong></p>
<ul>
<li>Service Mesh (Istio) 도입</li>
<li>Zero Trust Network Architecture</li>
<li>암호화된 통신 (mTLS)</li>
</ul>
</li>
</ol>
<h2 id="기술-스택-상세">기술 스택 상세</h2>
<pre><code class="language-yaml">Container Orchestration:
  - Kubernetes: v1.28
  - containerd: v1.7
  - CNI: Flannel v0.22

GPU Computing:
  - CUDA: 11.8
  - cuDNN: 8.6
  - TensorRT: 8.5
  - Triton Server: 23.05

Data Management:
  - MariaDB: 10.11
  - MaxScale: 23.08
  - Redis: 7.0
  - SeaweedFS: 3.55

Monitoring &amp; Logging:
  - Fluent Bit: 2.1
  - Kubernetes Metrics Server: 0.6
  - Custom Health Checks

Development Tools:
  - Helm: 3.12
  - Kustomize: 5.0
  - GitOps: ArgoCD Ready</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform에서 Terragrunt로: DRY 원칙을 통한 인프라 코드 혁신]]></title>
            <link>https://velog.io/@arnold_99/Terraform%EC%97%90%EC%84%9C-Terragrunt%EB%A1%9C-DRY-%EC%9B%90%EC%B9%99%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%BD%94%EB%93%9C-%ED%98%81%EC%8B%A0</link>
            <guid>https://velog.io/@arnold_99/Terraform%EC%97%90%EC%84%9C-Terragrunt%EB%A1%9C-DRY-%EC%9B%90%EC%B9%99%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%BD%94%EB%93%9C-%ED%98%81%EC%8B%A0</guid>
            <pubDate>Mon, 19 May 2025 05:17:15 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>현대 클라우드 환경에서 인프라를 코드로 관리하는 것은 필수가 되었습니다. Terraform은 이러한 흐름을 선도하는 도구이지만, 복잡한 멀티 환경 구성에서는 여러 한계에 부딪히게 됩니다. 이 글에서는 Terraform에서 발생하는 코드 중복 문제를 해결하기 위해 Terragrunt를 도입하게 된 과정과 그 성과에 대해 자세히 다루겠습니다.</p>
<h2 id="terraform-사용-시-직면했던-구체적인-문제점">Terraform 사용 시 직면했던 구체적인 문제점</h2>
<p>Terraform을 여러 환경(개발, 테스트, 스테이징, 운영)에서 사용하다 보니 다음과 같은 명확한 한계점이 드러났습니다:</p>
<h3 id="1-과도한-코드-중복">1. 과도한 코드 중복</h3>
<p>각 환경별로 거의 동일한 코드를 복사-붙여넣기 하는 상황이 빈번했습니다. 예를 들어:</p>
<pre><code class="language-hcl"># dev/main.tf
provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
}

module &quot;vpc&quot; {
  source = &quot;../modules/vpc&quot;
  vpc_cidr = &quot;10.0.0.0/16&quot;
  environment = &quot;dev&quot;
  subnet_count = 2
}

module &quot;ec2&quot; {
  source = &quot;../modules/ec2&quot;
  instance_type = &quot;t2.micro&quot;
  environment = &quot;dev&quot;
  vpc_id = module.vpc.vpc_id
}

terraform {
  backend &quot;s3&quot; {
    bucket = &quot;terraform-state&quot;
    key    = &quot;dev/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;
    dynamodb_table = &quot;terraform-locks&quot;
    encrypt = true
  }
}

# prod/main.tf (거의 동일한 코드)
provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
}

module &quot;vpc&quot; {
  source = &quot;../modules/vpc&quot;
  vpc_cidr = &quot;10.1.0.0/16&quot;  // 차이점
  environment = &quot;prod&quot;      // 차이점
  subnet_count = 3         // 차이점
}

module &quot;ec2&quot; {
  source = &quot;../modules/ec2&quot;
  instance_type = &quot;t2.large&quot;  // 차이점
  environment = &quot;prod&quot;        // 차이점
  vpc_id = module.vpc.vpc_id
}

terraform {
  backend &quot;s3&quot; {
    bucket = &quot;terraform-state&quot;
    key    = &quot;prod/terraform.tfstate&quot;  // 차이점
    region = &quot;ap-northeast-2&quot;
    dynamodb_table = &quot;terraform-locks&quot;
    encrypt = true
  }
}</code></pre>
<p>이러한 구조에서는 환경마다 달라지는 값이 몇 개 없음에도 불구하고 전체 코드를 반복해야 했습니다.</p>
<h3 id="2-백엔드-구성의-반복">2. 백엔드 구성의 반복</h3>
<p>모든 환경에서 원격 상태 저장을 위한 백엔드 구성이 반복되었습니다. 백엔드 설정 변경 시 모든 환경의 코드를 수정해야 했습니다.</p>
<h3 id="3-변수-관리의-복잡성">3. 변수 관리의 복잡성</h3>
<p>환경별 변수를 관리하는 것이 복잡했습니다. 특히 변수 파일이 늘어날수록 관리가 어려웠습니다:</p>
<pre><code>project/
├── dev/
│   ├── main.tf
│   ├── variables.tf
│   └── terraform.tfvars
├── stage/
│   ├── main.tf
│   ├── variables.tf
│   └── terraform.tfvars
└── prod/
    ├── main.tf
    ├── variables.tf
    └── terraform.tfvars</code></pre><h3 id="4-모듈-간-종속성-관리의-어려움">4. 모듈 간 종속성 관리의 어려움</h3>
<p>모듈 간 종속성을 관리하기 위해 복잡한 출력 변수 참조가 필요했으며, 이로 인해 코드가 더 복잡해졌습니다.</p>
<h3 id="5-일관성-유지의-어려움">5. 일관성 유지의 어려움</h3>
<p>환경마다 동일한 코드를 유지해야 하는데, 한 환경에서 코드를 개선했을 때 다른 환경에도 똑같이 적용해야 하는 번거로움이 있었습니다.</p>
<h2 id="terragrunt란-상세한-이해">Terragrunt란? 상세한 이해</h2>
<p>Terragrunt는 Terraform의 얇은 래퍼(wrapper)로, Terraform의 기능을 확장하여 이러한 문제를 해결합니다. 구체적인 기능을 살펴보겠습니다:</p>
<h3 id="1-구성-파일의-계층화">1. 구성 파일의 계층화</h3>
<p>Terragrunt는 <code>terragrunt.hcl</code> 파일을 통해 구성을 계층화합니다. 이를 통해 구성을 상속하고 재사용할 수 있습니다.</p>
<pre><code class="language-hcl"># 루트 terragrunt.hcl
remote_state {
  backend = &quot;s3&quot;
  generate = {
    path      = &quot;backend.tf&quot;
    if_exists = &quot;overwrite&quot;
  }
  config = {
    bucket         = &quot;terraform-state-${get_aws_account_id()}&quot;
    key            = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
    region         = &quot;ap-northeast-2&quot;
    encrypt        = true
    dynamodb_table = &quot;terraform-locks&quot;
  }
}

# 환경 공통 설정
generate &quot;provider&quot; {
  path      = &quot;provider.tf&quot;
  if_exists = &quot;overwrite&quot;
  contents  = &lt;&lt;EOF
provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
  default_tags {
    tags = {
      Environment = &quot;${local.environment}&quot;
      Terraform   = &quot;true&quot;
    }
  }
}
EOF
}

# 로컬 변수 설정 (각 환경의 terragrunt.hcl에서 오버라이드)
locals {
  environment = &quot;default&quot;
}</code></pre>
<h3 id="2-자동-원격-상태-관리">2. 자동 원격 상태 관리</h3>
<p>Terragrunt는 각 모듈의 원격 상태를 자동으로 구성합니다. 이를 통해 상태 파일 경로를 동적으로 생성하여 중복 코드를 줄입니다.</p>
<pre><code class="language-hcl">remote_state {
  backend = &quot;s3&quot;
  config = {
    bucket = &quot;terraform-state&quot;
    key    = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;
    dynamodb_table = &quot;terraform-locks&quot;
    encrypt = true
  }
}</code></pre>
<h3 id="3-의존성-블록을-통한-명시적인-종속성-관리">3. 의존성 블록을 통한 명시적인 종속성 관리</h3>
<p>Terragrunt는 <code>dependency</code> 블록을 통해 모듈 간 종속성을 명시적으로 정의할 수 있습니다:</p>
<pre><code class="language-hcl"># app/terragrunt.hcl
dependency &quot;vpc&quot; {
  config_path = &quot;../vpc&quot;

  # 의존성 모듈의 출력을 참조할 때 건너뛸 수 있는 옵션
  mock_outputs = {
    vpc_id = &quot;mock-vpc-id&quot;
  }
  mock_outputs_allowed_terraform_commands = [&quot;validate&quot;, &quot;plan&quot;]
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}</code></pre>
<p>이는 모듈 간 명확한 종속성을 정의하고, 출력 변수를 쉽게 참조할 수 있게 합니다.</p>
<h3 id="4-before_hook과-after_hook을-통한-실행-맥락-확장">4. before_hook과 after_hook을 통한 실행 맥락 확장</h3>
<p>Terragrunt는 Terraform 명령 전후에 추가 작업을 실행할 수 있는 훅 기능을 제공합니다:</p>
<pre><code class="language-hcl">terraform {
  before_hook &quot;before_hook&quot; {
    commands     = [&quot;apply&quot;, &quot;plan&quot;]
    execute      = [&quot;echo&quot;, &quot;Running Terraform&quot;]
  }

  after_hook &quot;after_hook&quot; {
    commands     = [&quot;apply&quot;]
    execute      = [&quot;echo&quot;, &quot;Terraform apply completed&quot;]
    run_on_error = true
  }
}</code></pre>
<h3 id="5-입력-변수의-계층화">5. 입력 변수의 계층화</h3>
<p>여러 레벨의 입력 변수를 결합할 수 있어 코드 중복을 최소화합니다:</p>
<pre><code class="language-hcl"># prod/region/service/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

include &quot;region_vars&quot; {
  path = find_in_parent_folders(&quot;region.hcl&quot;)
}

include &quot;environment_vars&quot; {
  path = find_in_parent_folders(&quot;env.hcl&quot;)
}

inputs = {
  service_specific_var = &quot;value&quot;
}</code></pre>
<h2 id="dry-원칙을-준수할-수-있었던-심층적-이유">DRY 원칙을 준수할 수 있었던 심층적 이유</h2>
<p>Terragrunt가 어떻게 DRY 원칙을 실현하는지 자세히 살펴보겠습니다:</p>
<h3 id="1-계층적-구성-상속">1. 계층적 구성 상속</h3>
<p>Terragrunt의 핵심 기능인 구성 상속은 공통 설정을 한 번만 정의하고 모든 환경에서 재사용할 수 있게 합니다:</p>
<pre><code class="language-hcl"># terragrunt/terragrunt.hcl (루트)
remote_state {
  backend = &quot;s3&quot;
  config = {
    bucket = &quot;terraform-state&quot;
    region = &quot;ap-northeast-2&quot;
    encrypt = true
    dynamodb_table = &quot;terraform-locks&quot;
    key = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
  }
}

# 공통 변수 정의
inputs = {
  aws_region = &quot;ap-northeast-2&quot;
  tags = {
    ManagedBy = &quot;Terraform&quot;
  }
}

# env-common.hcl (환경별 공통 설정)
locals {
  common_tags = {
    Owner = &quot;DevOps-Team&quot;
  }
}

# 개발 환경별 설정
inputs = merge(
  local.common_tags,
  {
    environment = &quot;dev&quot;
  }
)

# terragrunt/dev/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

include &quot;env&quot; {
  path = find_in_parent_folders(&quot;env-common.hcl&quot;)
}

inputs = {
  vpc_cidr = &quot;10.0.0.0/16&quot;
}</code></pre>
<p>이를 통해 환경별, 서비스별 설정을 계층적으로 관리할 수 있습니다.</p>
<h3 id="2-동적-백엔드-구성-생성">2. 동적 백엔드 구성 생성</h3>
<p>백엔드 구성을 동적으로 생성하여 모든 모듈에서 백엔드 설정 코드를 제거할 수 있습니다:</p>
<pre><code class="language-hcl"># 루트 terragrunt.hcl
remote_state {
  backend = &quot;s3&quot;
  generate = {
    path      = &quot;backend.tf&quot;
    if_exists = &quot;overwrite&quot;
  }
  config = {
    bucket         = &quot;terraform-state-${get_aws_account_id()}&quot;
    key            = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
    region         = local.aws_region
    encrypt        = true
    dynamodb_table = &quot;terraform-locks&quot;
  }
}

locals {
  aws_region = &quot;ap-northeast-2&quot;
}</code></pre>
<p>이 설정은 각 모듈 디렉토리에 Terraform 실행 시 자동으로 <code>backend.tf</code> 파일을 생성합니다. 이로써 백엔드 설정을 한 번만 정의하고 모든 모듈에서 사용할 수 있습니다.</p>
<h3 id="3-변수-주입-메커니즘">3. 변수 주입 메커니즘</h3>
<p>Terragrunt의 <code>inputs</code> 블록은 변수를 Terraform 모듈에 자동으로 주입합니다. 이를 통해 환경별 변수를 효율적으로 관리할 수 있습니다:</p>
<pre><code class="language-hcl"># dev/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

terraform {
  source = &quot;../../modules//vpc&quot;
}

inputs = {
  vpc_cidr        = &quot;10.0.0.0/16&quot;
  public_subnets  = [&quot;10.0.1.0/24&quot;, &quot;10.0.2.0/24&quot;]
  private_subnets = [&quot;10.0.10.0/24&quot;, &quot;10.0.20.0/24&quot;]
}

# prod/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

terraform {
  source = &quot;../../modules//vpc&quot;
}

inputs = {
  vpc_cidr        = &quot;10.1.0.0/16&quot;
  public_subnets  = [&quot;10.1.1.0/24&quot;, &quot;10.1.2.0/24&quot;, &quot;10.1.3.0/24&quot;]
  private_subnets = [&quot;10.1.10.0/24&quot;, &quot;10.1.20.0/24&quot;, &quot;10.1.30.0/24&quot;]
}</code></pre>
<h3 id="4-함수와-헬퍼를-통한-동적-구성">4. 함수와 헬퍼를 통한 동적 구성</h3>
<p>Terragrunt는 다양한 내장 함수를 제공하여 구성을 동적으로 생성할 수 있습니다:</p>
<pre><code class="language-hcl">locals {
  account_vars = read_terragrunt_config(find_in_parent_folders(&quot;account.hcl&quot;))
  region_vars  = read_terragrunt_config(find_in_parent_folders(&quot;region.hcl&quot;))
  environment_vars = read_terragrunt_config(find_in_parent_folders(&quot;env.hcl&quot;))

  account_id   = local.account_vars.locals.aws_account_id
  aws_region   = local.region_vars.locals.aws_region
  environment  = local.environment_vars.locals.environment
}

inputs = {
  tags = {
    Account     = local.account_id
    Region      = local.aws_region
    Environment = local.environment
  }
}</code></pre>
<h2 id="도입-후-변화-정량적정성적-효과">도입 후 변화: 정량적/정성적 효과</h2>
<p>Terragrunt 도입 후 다음과 같은 구체적인 변화가 있었습니다:</p>
<h3 id="1-코드-양의-극적인-감소">1. 코드 양의 극적인 감소</h3>
<p>기존 Terraform 코드는 환경당 약 500줄 정도였으나, Terragrunt 도입 후 환경별 설정은 50줄 이하로 줄였습니다. 전체적으로는 약 70%의 코드 감소 효과가 있었습니다.</p>
<h3 id="2-배포-시간-단축">2. 배포 시간 단축</h3>
<p>여러 모듈이 의존성을 가진 경우, <code>terragrunt run-all apply</code>를 통해 의존성 순서를 자동으로 계산하고 병렬로 배포할 수 있어 배포 시간이 약 40% 단축되었습니다.</p>
<h3 id="3-오류-감소율">3. 오류 감소율</h3>
<p>환경 간 설정 불일치로 인한 오류가 85% 이상 감소했습니다. 한 환경에서 검증된 코드는 다른 환경에서도 동일하게 작동했습니다.</p>
<h3 id="4-신규-환경-구성-시간-단축">4. 신규 환경 구성 시간 단축</h3>
<p>새로운 환경(예: QA, Sandbox)을 추가할 때 필요한 시간이 기존 2-3일에서 1시간 이내로 단축되었습니다.</p>
<h3 id="5-백엔드-관리-간소화">5. 백엔드 관리 간소화</h3>
<p>모든 환경의 백엔드 설정을 한 번에 변경할 수 있어, AWS 계정 변경이나 리전 이전 시 유연성이 크게 향상되었습니다.</p>
<h2 id="실제-적용-사례-상세-구현">실제 적용 사례: 상세 구현</h2>
<p>실제 프로젝트에서는 다음과 같은 구조로 Terragrunt를 적용했습니다:</p>
<pre><code>terragrunt/
├── terragrunt.hcl            # 루트 설정 (백엔드, 프로바이더 등)
├── account.hcl               # AWS 계정 정보
├── env-common.hcl            # 환경 공통 설정
├── modules/                  # 공통 Terraform 모듈
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   └── elasticache/
├── dev/                      # 개발 환경
│   ├── env.hcl               # 개발 환경 전역 변수
│   ├── vpc/
│   │   └── terragrunt.hcl
│   ├── eks/
│   │   └── terragrunt.hcl
│   └── database/
│       └── terragrunt.hcl
└── prod/                     # 운영 환경
    ├── env.hcl               # 운영 환경 전역 변수
    ├── vpc/
    │   └── terragrunt.hcl
    ├── eks/
    │   └── terragrunt.hcl
    └── database/
        └── terragrunt.hcl</code></pre><p>이러한 구조에서 각 컴포넌트별 설정은 다음과 같이 구성됩니다:</p>
<h3 id="루트-terragrunthcl">루트 terragrunt.hcl</h3>
<pre><code class="language-hcl"># 원격 상태 관리
remote_state {
  backend = &quot;s3&quot;
  generate = {
    path      = &quot;backend.tf&quot;
    if_exists = &quot;overwrite&quot;
  }
  config = {
    bucket         = &quot;terraform-state-${local.account_id}&quot;
    key            = &quot;${path_relative_to_include()}/terraform.tfstate&quot;
    region         = local.aws_region
    encrypt        = true
    dynamodb_table = &quot;terraform-locks&quot;
  }
}

# AWS 공급자 구성 자동 생성
generate &quot;provider&quot; {
  path      = &quot;provider.tf&quot;
  if_exists = &quot;overwrite&quot;
  contents  = &lt;&lt;EOF
provider &quot;aws&quot; {
  region = &quot;${local.aws_region}&quot;
  default_tags {
    tags = {
      Environment = &quot;${local.environment}&quot;
      ManagedBy   = &quot;Terraform&quot;
      Workspace   = &quot;${terraform.workspace}&quot;
    }
  }
}
EOF
}

# 로컬 변수 설정
locals {
  # 하위 HCL 파일에서 로드
  account_vars = read_terragrunt_config(find_in_parent_folders(&quot;account.hcl&quot;))
  environment_vars = try(
    read_terragrunt_config(find_in_parent_folders(&quot;env.hcl&quot;)),
    { locals = { environment = &quot;default&quot; } }
  )

  account_id   = local.account_vars.locals.aws_account_id
  aws_region   = local.account_vars.locals.aws_region
  environment  = local.environment_vars.locals.environment
}

# 공통 입력 변수
inputs = {
  aws_region  = local.aws_region
  account_id  = local.account_id
  environment = local.environment
}</code></pre>
<h3 id="계정-설정-accounthcl">계정 설정: account.hcl</h3>
<pre><code class="language-hcl">locals {
  aws_account_id = &quot;123456789012&quot;  # AWS 계정 ID
  aws_region     = &quot;ap-northeast-2&quot;  # 기본 AWS 리전
}</code></pre>
<h3 id="환경별-설정-devenvhcl">환경별 설정: dev/env.hcl</h3>
<pre><code class="language-hcl">locals {
  environment = &quot;dev&quot;

  # 개발 환경 특화 설정
  domain_name = &quot;dev.example.com&quot;

  # 인프라 사이징
  instance_types = {
    bastion = &quot;t3.micro&quot;
    app     = &quot;t3.small&quot;
  }

  rds_config = {
    instance_class    = &quot;db.t3.medium&quot;
    allocated_storage = 20
    multi_az          = false
  }
}</code></pre>
<h3 id="환경별-설정-prodenvhcl">환경별 설정: prod/env.hcl</h3>
<pre><code class="language-hcl">locals {
  environment = &quot;prod&quot;

  # 운영 환경 특화 설정
  domain_name = &quot;example.com&quot;

  # 인프라 사이징
  instance_types = {
    bastion = &quot;t3.small&quot;
    app     = &quot;m5.large&quot;
  }

  rds_config = {
    instance_class    = &quot;db.m5.large&quot;
    allocated_storage = 100
    multi_az          = true
  }
}</code></pre>
<h3 id="컴포넌트-설정-devvpcterragrunthcl">컴포넌트 설정: dev/vpc/terragrunt.hcl</h3>
<pre><code class="language-hcl">include {
  path = find_in_parent_folders()
}

terraform {
  source = &quot;../../modules//vpc&quot;
}

inputs = {
  vpc_name       = &quot;dev-vpc&quot;
  vpc_cidr       = &quot;10.0.0.0/16&quot;
  azs            = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2c&quot;]
  public_subnets = [&quot;10.0.1.0/24&quot;, &quot;10.0.2.0/24&quot;]
  private_subnets = [&quot;10.0.10.0/24&quot;, &quot;10.0.20.0/24&quot;]

  enable_nat_gateway = true
  single_nat_gateway = true  # 개발 환경에서는 비용 절감을 위해 단일 NAT 게이트웨이 사용

  tags = {
    Terraform   = &quot;true&quot;
    Environment = &quot;dev&quot;
  }
}</code></pre>
<h3 id="컴포넌트-설정-deveksterragrunthcl">컴포넌트 설정: dev/eks/terragrunt.hcl</h3>
<pre><code class="language-hcl">include {
  path = find_in_parent_folders()
}

# VPC 모듈에 대한 종속성 정의
dependency &quot;vpc&quot; {
  config_path = &quot;../vpc&quot;

  # 의존성 모듈이 아직 배포되지 않았을 때 모의 출력을 사용 (계획/검증용)
  mock_outputs = {
    vpc_id = &quot;mock-vpc-id&quot;
    private_subnets = [&quot;mock-subnet-1&quot;, &quot;mock-subnet-2&quot;]
  }
  mock_outputs_allowed_terraform_commands = [&quot;validate&quot;, &quot;plan&quot;]
}

terraform {
  source = &quot;../../modules//eks&quot;
}

inputs = {
  cluster_name = &quot;dev-eks&quot;
  vpc_id       = dependency.vpc.outputs.vpc_id
  subnet_ids   = dependency.vpc.outputs.private_subnets

  cluster_version = &quot;1.24&quot;

  # 노드 그룹 설정
  node_groups = {
    main = {
      desired_capacity = 2
      min_capacity     = 1
      max_capacity     = 3
      instance_types   = [&quot;t3.medium&quot;]
      disk_size        = 50
    }
  }

  # 관리형 노드 그룹 설정
  managed_node_groups = {
    system = {
      name           = &quot;system&quot;
      instance_types = [&quot;t3.medium&quot;]
      min_size       = 1
      max_size       = 3
      desired_size   = 1
      capacity_type  = &quot;ON_DEMAND&quot;
    }
  }
}</code></pre>
<h2 id="cicd-파이프라인과의-통합">CI/CD 파이프라인과의 통합</h2>
<p>Terragrunt는 GitHub Actions와 같은 CI/CD 시스템과 쉽게 통합됩니다:</p>
<pre><code class="language-yaml"># GitHub Actions 워크플로우 파일
name: &#39;Terraform Infrastructure Deployment&#39;

on:
  push:
    branches:
      - main
    paths:
      - &#39;terragrunt/**&#39;
  pull_request:
    branches:
      - main
    paths:
      - &#39;terragrunt/**&#39;

jobs:
  terraform:
    name: &#39;Terraform&#39;
    runs-on: ubuntu-latest

    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v3

      - name: AWS 자격 증명 설정
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Terraform 설치
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: &#39;1.4.0&#39;

      - name: Terragrunt 설치
        run: |
          curl -L -o /tmp/terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/v0.45.0/terragrunt_linux_amd64
          chmod +x /tmp/terragrunt
          sudo mv /tmp/terragrunt /usr/local/bin/terragrunt

      - name: Terragrunt 캐시 초기화
        run: |
          find terragrunt -type d -name &quot;.terragrunt-cache&quot; -exec rm -rf {} +
          find terragrunt -type d -name &quot;.terraform&quot; -exec rm -rf {} +

      - name: Terragrunt 초기화 및 계획
        id: plan
        run: |
          cd terragrunt/dev
          terragrunt run-all init
          terragrunt run-all plan -out=tfplan.binary

      - name: 변경사항 요약
        run: |
          cd terragrunt/dev
          terragrunt run-all show tfplan.binary | grep -A 20 &quot;Plan:&quot;

      - name: Terragrunt 적용
        if: github.event_name == &#39;push&#39;
        run: |
          cd terragrunt/dev
          terragrunt run-all apply -auto-approve</code></pre>
<h2 id="고급-terragrunt-기능-활용">고급 Terragrunt 기능 활용</h2>
<h3 id="1-원격-모듈-참조-최적화">1. 원격 모듈 참조 최적화</h3>
<p>Terragrunt는 Terraform 모듈을 효율적으로 참조할 수 있습니다:</p>
<pre><code class="language-hcl">terraform {
  source = &quot;git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.3&quot;
}</code></pre>
<h3 id="2-의존성-다이어그램-생성">2. 의존성 다이어그램 생성</h3>
<p>의존성 구조를 시각화할 수 있습니다:</p>
<pre><code class="language-bash">terragrunt graph-dependencies | dot -Tpng &gt; dependencies.png</code></pre>
<h3 id="3-병렬-실행을-통한-배포-속도-향상">3. 병렬 실행을 통한 배포 속도 향상</h3>
<p>여러 모듈을 병렬로 배포하여 시간을 단축할 수 있습니다:</p>
<pre><code class="language-bash">terragrunt run-all apply --terragrunt-parallelism 10</code></pre>
<h3 id="4-특정-모듈만-선택적-실행">4. 특정 모듈만 선택적 실행</h3>
<p>태그 또는 패턴을 사용하여 특정 모듈만 실행할 수 있습니다:</p>
<pre><code class="language-bash">terragrunt run-all apply --terragrunt-include-dir &quot;**/vpc&quot;</code></pre>
<h2 id="결론-terragrunt의-비즈니스-가치">결론: Terragrunt의 비즈니스 가치</h2>
<p>Terragrunt는 단순한 기술적 개선을 넘어 다음과 같은 비즈니스 가치를 제공합니다:</p>
<ol>
<li><strong>운영 효율성</strong>: 코드 중복 감소와 자동화를 통해 인프라 관리 효율성이 크게 향상됩니다.</li>
<li><strong>위험 감소</strong>: 환경 간 일관성 향상으로 운영 실수와 위험이 감소합니다.</li>
<li><strong>신속한 환경 프로비저닝</strong>: 새로운 환경 구축 시간이 대폭 단축되어 비즈니스 요구에 빠르게 대응할 수 있습니다.</li>
<li><strong>비용 최적화</strong>: 환경별 인프라 설정을 보다 세밀하게 조정하여 비용을 최적화할 수 있습니다.</li>
<li><strong>개발자 경험 향상</strong>: 간결하고 일관된 코드 작성 방식으로 개발자 만족도와 생산성이 향상됩니다.</li>
</ol>
<p>Terraform을 사용하면서 코드 중복, 모듈 관리, 환경별 구성의 어려움을 겪고 계신다면, Terragrunt는 이러한 문제를 해결할 수 있는 강력한 도구입니다. DRY 원칙을 기반으로 인프라 코드의 품질과 유지보수성을 크게 향상시켜 보다 안정적이고 효율적인 인프라 관리가 가능해집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[마이크로서비스 기반 로그 및 트레이스 분석 시스템 개발기]]></title>
            <link>https://velog.io/@arnold_99/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8-%EB%B0%8F-%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%8A%A4-%EB%B6%84%EC%84%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-MCP-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@arnold_99/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8-%EB%B0%8F-%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%8A%A4-%EB%B6%84%EC%84%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-MCP-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Fri, 02 May 2025 09:57:58 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 최근에 완성한 <strong>마이크로서비스 기반 로그 및 트레이스 분석 시스템</strong>의 개발 과정과 아키텍처에 대해 공유하려고 합니다. 특히 <strong>Model Context Protocol(MCP)</strong>을 활용한 접근 방식이 어떻게 효율적인 모니터링 솔루션으로 이어졌는지 설명드리겠습니다.</p>
<hr>
<h2 id="시스템-개요">시스템 개요</h2>
<p>본 시스템은 <strong>마이크로서비스 아키텍처 환경에서 발생하는 로그와 트레이스 데이터를 통합적으로 수집하고, 분석하는 플랫폼</strong>입니다. 여기에 <strong>대규모 언어 모델(LLM)</strong>을 접목해 복잡한 시스템 로그와 트레이스 데이터를 <strong>자연어로 해석</strong>하고, 운영자가 직관적으로 이해할 수 있도록 지원합니다.</p>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li>🧠 <strong>자연어 기반 로그/트레이스 검색</strong>: 운영자는 LogQL이나 TraceQL을 몰라도 &quot;지난 3시간 동안의 오류 로그 보여줘&quot; 같은 자연어로 검색 가능</li>
<li>🔍 <strong>로그-트레이스 연계 분석</strong>: 특정 에러 로그와 관련된 트레이스를 추적해 원인 파악</li>
<li>🚨 <strong>이상 탐지 및 진단</strong>: AI가 이상 징후를 감지하고, 원인을 요약해 제공</li>
<li>📊 <strong>시각화 대시보드</strong>: Grafana 및 Streamlit을 활용한 인터랙티브 UI 제공</li>
</ul>
<hr>
<h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<p>아래는 시스템 전반의 구성 요소와 데이터 흐름을 간단한 블록 다이어그램 형식으로 표현한 이미지입니다.
<img src="https://velog.velcdn.com/images/arnold_99/post/b8c87d4b-d3a5-4bd2-9a48-02dc55472bed/image.png" alt=""></p>
<h3 id="구성-계층-요약">구성 계층 요약</h3>
<ul>
<li><strong>UI 계층</strong>: Streamlit 기반 자연어 쿼리 입력 및 결과 시각화</li>
<li><strong>분석 계층</strong>: LangGraph가 자연어 쿼리를 분석하고 의도 추출 및 LogQL/TraceQL 생성</li>
<li><strong>MCP 계층</strong>: Loki/Tempo MCP 서버가 클라이언트 요청을 중계하고 응답 포맷 가공</li>
<li><strong>API 계층</strong>: Loki/Tempo API 서버가 원시 로그/트레이스 쿼리를 수행</li>
<li><strong>데이터 소스</strong>: Loki/Tempo DB가 실제 데이터를 저장 및 제공</li>
<li><strong>시각화 대시보드</strong>: Grafana를 통해 실시간 모니터링 UI 제공</li>
</ul>
<hr>
<h2 id="mcp란-무엇인가">MCP란 무엇인가?</h2>
<p>모니터링 시스템을 개발하면서 가장 먼저 고민했던 것은 &quot;<strong>컴포넌트 간 상호작용을 어떻게 일관되게 관리할 것인가?</strong>&quot;였습니다. 이에 도입한 것이 <strong>Model Context Protocol(MCP)</strong>입니다.</p>
<h3 id="mcp의-특징">MCP의 특징</h3>
<ul>
<li><strong>컨텍스트 관리</strong>: 세션, 이전 요청, 사용자 의도를 유지해 일관성 있는 분석</li>
<li><strong>데이터 변환</strong>: 프론트 요청을 백엔드에서 처리 가능한 포맷으로 변환</li>
<li><strong>세션 기반 인터랙션</strong>: 단발성 요청이 아닌, 지속적인 대화형 상호작용 가능</li>
</ul>
<hr>
<h2 id="json-rpc를-선택한-이유">JSON-RPC를 선택한 이유</h2>
<p>MCP와 LangGraph 간, 그리고 MCP와 API 서버 간 통신에는 <strong>JSON-RPC 2.0</strong>을 사용했습니다.</p>
<h3 id="선택-이유">선택 이유</h3>
<ul>
<li>🧩 <strong>메서드 기반 인터페이스</strong>: <code>method</code>, <code>params</code>, <code>id</code>가 명확하게 구조화</li>
<li>📉 <strong>가벼운 오버헤드</strong>: REST 대비 적은 메타데이터로 속도와 효율성 향상</li>
<li>🔁 <strong>양방향 통신 구조</strong>: 이벤트 기반 응답, SSE, 배치 처리 지원</li>
</ul>
<pre><code class="language-json">{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;method&quot;: &quot;query_loki&quot;,
  &quot;params&quot;: {
    &quot;query&quot;: &quot;{service=\&quot;order-service\&quot;}&quot;,
    &quot;start&quot;: &quot;2023-05-01T10:00:00Z&quot;,
    &quot;end&quot;: &quot;2023-05-01T11:00:00Z&quot;
  },
  &quot;id&quot;: 1
}</code></pre>
<hr>
<h2 id="구성-요소-상세-설명">구성 요소 상세 설명</h2>
<h3 id="langgraph">LangGraph</h3>
<ul>
<li>Google Gemini 기반 자연어 해석 및 LogQL/TraceQL 생성</li>
<li>로그/트레이스 통합 분석, 인사이트 추출, 샘플 생성</li>
<li>FastAPI, LangChain 기반 Python 서버</li>
</ul>
<h3 id="lokitempo-api-서버">Loki/Tempo API 서버</h3>
<ul>
<li>로그/트레이스 원시 데이터 처리 전담</li>
<li>FastAPI로 작성, HTTPX로 Loki/Tempo와 통신</li>
</ul>
<h3 id="mcp-서버">MCP 서버</h3>
<ul>
<li>MCP-Loki / MCP-Tempo 서버는 API 요청 중계 + 컨텍스트 관리</li>
<li>요청 변환, 응답 포맷팅, 캐싱, 세션 연계 기능 포함</li>
</ul>
<h3 id="streamlit-ui">Streamlit UI</h3>
<ul>
<li>자연어 질의 입력 및 분석 결과 시각화</li>
<li>로그 레벨, 서비스명, 시간 범위 필터링 지원</li>
</ul>
<hr>
<h2 id="주요-시나리오-예시">주요 시나리오 예시</h2>
<h3 id="✅-로그-분석-예시">✅ 로그 분석 예시</h3>
<blockquote>
<p>&quot;지난 3시간 동안 order-service에서 발생한 에러 로그 보여줘&quot;</p>
</blockquote>
<ul>
<li>LangGraph는 이를 <code>LOG_QUERY</code>로 인식, <code>service=\&quot;order-service\&quot;</code> 및 시간 범위 추출</li>
<li>LogQL 쿼리 생성 → Loki-MCP → Loki API → Loki DB</li>
<li>분석된 로그와 요약 결과가 Streamlit UI에 표시됨</li>
</ul>
<h3 id="✅-트레이스-분석-예시">✅ 트레이스 분석 예시</h3>
<blockquote>
<p>&quot;product-service의 API 호출 지연이 있는 트레이스 보여줘&quot;</p>
</blockquote>
<ul>
<li>LangGraph가 <code>TRACE_QUERY</code>로 분류하고 쿼리 수행</li>
<li>Tempo MCP를 통해 Tempo DB 조회 → 지연 시간 기반 트레이스 필터링 및 요약 제공</li>
</ul>
<h3 id="✅-통합-분석-예시">✅ 통합 분석 예시</h3>
<blockquote>
<p>&quot;order-service의 에러 로그와 관련된 트레이스를 찾아서 분석해줘&quot;</p>
</blockquote>
<ul>
<li>LangGraph가 복합 쿼리로 분류</li>
<li>로그에서 트레이스 ID 추출 → Tempo에 트레이스 요청</li>
<li>로그-트레이스 연관 분석 결과 제공</li>
</ul>
<hr>
<h2 id="docker-compose-기반-배포">Docker Compose 기반 배포</h2>
<pre><code class="language-yaml">services:
  langgraph:
    build: ./langgraph
    ports:
      - &quot;8001:8001&quot;
    environment:
      - GOOGLE_API_KEY=${GOOGLE_API_KEY}
      - MCP_URL=http://loki-mcp:8003
      - TEMPO_MCP_URL=http://tempo-mcp:8004

  loki-api:
    build: ./loki-api
    ports:
      - &quot;8002:8002&quot;
    environment:
      - LOKI_URL=${LOKI_URL}

  loki-mcp:
    build: ./loki-mcp
    ports:
      - &quot;8003:8003&quot;
    environment:
      - LOKI_API_URL=http://loki-api:8002

  tempo-api:
    build: ./tempo-api
    ports:
      - &quot;8005:8005&quot;
    environment:
      - TEMPO_URL=${TEMPO_URL}

  tempo-mcp:
    build: ./tempo-mcp
    ports:
      - &quot;8004:8004&quot;
    environment:
      - TEMPO_API_URL=http://tempo-api:8005

  streamlit:
    build: ./streamlit
    ports:
      - &quot;8501:8501&quot;
    environment:
      - LANGGRAPH_URL=http://langgraph:8001</code></pre>
<hr>
<h2 id="확장성-및-향후-계획">확장성 및 향후 계획</h2>
<ul>
<li>📈 <strong>메트릭 데이터 통합</strong>: Prometheus 연동을 통해 CPU, Memory, Network 등 실시간 메트릭 분석까지 지원 예정</li>
<li>🌐 <strong>멀티 클러스터 확장</strong>: 여러 마이크로서비스 클러스터에 대한 통합 분석을 위한 수평 확장 설계 적용</li>
<li>🧠 <strong>AI 기반 Root Cause Analysis</strong>: LangGraph에 사고 시나리오 학습 기능 추가로 더 정밀한 원인 분석 가능</li>
</ul>
<hr>
<p>이상으로 <strong>LLM 기반 마이크로서비스 로그/트레이스 분석 시스템</strong>에 대한 개발기를 마칩니다. MCP의 도입과 JSON-RPC 구조화 통신 방식이 모니터링의 직관성과 확장성을 크게 향상시켰습니다.</p>
]]></description>
        </item>
    </channel>
</rss>