COSMOS(homelab)

COSMOS는 프로젝트와 서비스가 태어나고 연결되는 개인 인프라다. Mac mini 위에서 SRE 운영을 연습하고 사이드 프로젝트를 배포하며, 새 도구를 부담 없이 시험한다. NAS나 미디어 서버를 대체하려는 구성은 아니다.

관련 문서: [[kubernetes]]


1. 스택

계층 도구 메모
컨테이너 런타임 OrbStack Apple Silicon 네이티브, 개인 무료
로컬 Kubernetes k3d k3s를 도커 컨테이너로 띄워 멀티노드 시뮬레이션
IaC (플랫폼) Terraform 플랫폼 컴포넌트 코드화
GitOps (앱) ArgoCD App-of-Apps 패턴
원격 접속 Tailscale NAT 관통
관측성 Prometheus + Grafana + Loki + Alloy 메트릭 / 시각화 / 로그 수집·조회
시크릿 sops + age git 저장소에 암호화 보관

2. 외부 접속 토폴로지

flowchart TB
  subgraph Internet["🌍 인터넷"]
    CP["Tailscale Control Plane<br/>(메타데이터만)"]
  end

  subgraph External["📱 외부 환경 (카페 WiFi / LTE)"]
    ExtRouter["외부 공유기<br/>🔒 NAT"]
    ExtDevice["외부 디바이스"]
    ExtDevice --- ExtRouter
  end

  subgraph Home["🏠 집 네트워크"]
    HomeRouter["공유기<br/>🔒 NAT<br/>포트포워딩 없음"]
    Host["🖥 Mac mini 호스트<br/>(tailscaled)"]
    HomeRouter --- Host
  end

  ExtDevice -. 위치 보고 .-> CP
  Host -. 위치 보고 .-> CP
  ExtDevice == "WireGuard 암호화 터널<br/>(NAT 관통)" ==> Host

  classDef ctrl fill:#fde2e4,stroke:#9d174d,color:#000
  classDef host fill:#dbeafe,stroke:#1e40af,color:#000
  classDef router fill:#fef3c7,stroke:#92400e,color:#000
  class CP ctrl
  class Host,ExtDevice host
  class HomeRouter,ExtRouter router

2.1 NAT의 동작

홈 공유기는 NAT(Network Address Translation), 라우터, 기본 방화벽 역할을 동시에 수행한다. 사설 IP(192.168.x.x)는 외부에서 직접 보이지 않는다. 외부에서 들어오는 패킷은 공유기가 라우팅 대상을 모르므로 폐기한다. 별도 방화벽 설정 없이도 인바운드 트래픽은 기본적으로 차단된다.

포트포워딩을 설정하면 특정 외부 포트를 내부 장치로 전달하는 규칙이 생긴다.

2.2 Tailscale의 NAT 관통

  1. STUN 서버를 통해 두 장치가 자신의 외부 IP와 포트를 확인한다.
  2. 두 장치가 동시에 패킷을 보내면 NAT가 기존 연결의 응답으로 인식해 통과시킨다 (hole punching).
  3. 이후 WireGuard 암호화 P2P 터널로 직접 통신한다.
  4. 관통이 실패하면 DERP 릴레이 서버가 트래픽을 중계한다.

Tailscale Control Plane은 장치의 접속 정보만 처리한다. 실제 데이터는 두 장치 사이의 터널로 흐른다.


3. 호스트 계층

flowchart TB
  subgraph Mac["🖥 Mac (Apple Silicon / Darwin)"]
    direction TB
    L1["<b>층1: macOS</b><br/>brew, kubectl, terraform, git, tailscaled<br/>~/srv/data/  ← 영구 데이터"]

    subgraph L2["층2: OrbStack"]
      direction TB
      VM["💿 Linux VM<br/>Apple Virtualization Framework"]

      subgraph CD["containerd (컨테이너 데몬)"]
        direction TB
        subgraph L3["층3: k3d cluster"]
          direction LR
          Server["서버 노드<br/>(컨테이너)"]
          Worker1["워커1 노드<br/>(컨테이너)"]
          Worker2["워커2 노드<br/>(컨테이너)"]
        end
      end
    end
  end

  Pods["Pod들<br/>(nginx, argocd, prometheus 등)"]
  Server -. Pod 호스팅 .-> Pods
  Worker1 -. Pod 호스팅 .-> Pods
  Worker2 -. Pod 호스팅 .-> Pods

  classDef l1 fill:#fef3c7,stroke:#92400e,color:#000
  classDef l2 fill:#dbeafe,stroke:#1e40af,color:#000
  classDef l3 fill:#dcfce7,stroke:#166534,color:#000
  classDef pod fill:#fce7f3,stroke:#9d174d,color:#000
  class L1 l1
  class VM,CD l2
  class Server,Worker1,Worker2 l3
  class Pods pod

3.1 층1: macOS

CLI 도구(brew, kubectl, terraform, git), Tailscale 데몬이 실행된다. 영구 데이터 위치는 ~/srv/data/로 고정한다.

3.2 층2: OrbStack

컨테이너는 리눅스 커널의 기능(namespaces + cgroups + chroot)이며, 리눅스 커널 없이 존재할 수 없다. macOS는 Darwin 커널을 사용하므로 컨테이너를 직접 실행할 수 없다.

OrbStack은 Apple Virtualization Framework로 리눅스 VM을 한 대 띄우고 그 안에서 containerd를 실행한다. macOS 위에 컨테이너를 실행할 리눅스 환경을 마련하는 역할이다.

3.3 층3: k3d 클러스터

k3d는 k3s(경량 Kubernetes 배포판)를 도커 컨테이너로 실행하는 도구다. 각 "노드"는 OrbStack VM 안의 도커 컨테이너이며, 그 컨테이너 안에서 k3s 바이너리가 실행된다. Pod는 그 안에서 다시 컨테이너로 실행된다.

docker ps
# k3d-cosmos-server-0   ← 노드 (컨테이너)
# k3d-cosmos-agent-0    ← 노드 (컨테이너)
# k3d-cosmos-agent-1    ← 노드 (컨테이너)

kubectl get pods -A
# 위 노드 컨테이너 안에서 실행되는 Pod들

4. 클러스터 구조

4.1 노드

flowchart TB
  subgraph Cluster["k3d Cluster"]
    CP["<b>Control Plane Node</b><br/>(k3s 용어 - server)"]
    W1["<b>Worker Node 1</b><br/>(k3s 용어 - agent)"]
    W2["<b>Worker Node 2</b><br/>(k3s 용어 - agent)"]

    CP --- W1
    CP --- W2
  end

  classDef cp fill:#fde68a,stroke:#92400e,color:#000
  classDef wk fill:#bbf7d0,stroke:#166534,color:#000
  class CP cp
  class W1,W2 wk
종류 k3s 용어 역할
Control Plane server 클러스터 상태 관리, 변경 권한
Worker agent Pod 실제 실행

Control Plane이 멈추면 새 Pod 배치, 자동 복구, kubectl 같은 클러스터 변경 작업도 멈춘다. 이미 실행 중인 Pod의 트래픽은 워커가 계속 처리한다.

운영 환경에서는 etcd의 raft 합의를 위해 Control Plane을 홀수(3, 5, 7…)로 둔다.

4.2 컨트롤 플레인 컴포넌트

flowchart TB
  subgraph CP["Control Plane Node"]
    API["<b>kube-apiserver</b><br/>모든 통신의 허브<br/>etcd 직접 접근하는 유일한 컴포넌트"]
    ETCD[("<b>etcd</b><br/>(k3s = sqlite)<br/>desired + current state")]
    SCH["<b>scheduler</b><br/>새 Pod의 워커 결정"]
    CM["<b>controller-manager</b><br/>watch + reconcile loop<br/>여러 컨트롤러의 집합"]

    API --- ETCD
    SCH -- watch --> API
    CM -- watch --> API
  end

  Kubectl["kubectl"] -- RPC --> API
  Kubelet["kubelet (워커)"] <-- long-watch --> API

  classDef hub fill:#fde68a,stroke:#92400e,stroke-width:3px,color:#000
  classDef store fill:#fde2e4,stroke:#9d174d,color:#000
  classDef ctrl fill:#dbeafe,stroke:#1e40af,color:#000
  class API hub
  class ETCD store
  class SCH,CM ctrl
컴포넌트 역할
kube-apiserver 모든 통신의 진입점. etcd에 직접 접근하는 유일한 컴포넌트
etcd (k3s = SQLite) 클러스터의 desired state + current state 저장소
scheduler 새 Pod이 어느 워커에 배치될지 결정
controller-manager deployment, replicaset, node, endpoint 등 여러 컨트롤러의 집합. desired state를 watch해 reconcile

모든 컴포넌트는 apiserver를 경유해 통신한다. 컴포넌트 간 직접 호출은 없다.

4.3 kubectl apply 처리 흐름

kubectl apply -f deployment.yaml 실행 시 일어나는 일:

  1. kubectl → apiserver: deployment 등록 요청
  2. apiserver → etcd: deployment 객체 저장
  3. controller-manager의 deployment-controller가 watch로 감지 → ReplicaSet 생성 요청
  4. controller-manager의 replicaset-controller가 watch로 감지 → Pod 객체 N개 생성 (nodeName 미지정)
  5. scheduler가 nodeName 미지정 Pod 발견 → 워커 결정 후 nodeName 필드 채움
  6. 각 워커의 kubelet이 자기 노드에 할당된 Pod을 watch로 받음 → 컨테이너 런타임에 생성 명령
  7. Pod 실행 후 status를 apiserver에 보고

각 컴포넌트는 apiserver의 변경을 감지하고 맡은 작업을 수행한 뒤 결과를 다시 기록한다. 이 흐름이 Kubernetes의 선언적 reconcile loop다.

4.4 워커 컴포넌트

flowchart TB
  subgraph Worker["Worker Node"]
    KL["<b>kubelet</b><br/>apiserver와 long-watch<br/>컨테이너 런타임에 명령"]
    KP["<b>kube-proxy</b><br/>Service ClusterIP → Pod IP 라우팅<br/>iptables / IPVS 룰 관리"]
    CR["<b>Container Runtime</b><br/>(containerd)<br/>실제 컨테이너 프로세스 엔진"]

    subgraph Pods["Pod들"]
      direction LR
      P1["Pod 1"]
      P2["Pod 2"]
      P3["Pod 3"]
    end

    KL -- 컨테이너 명령 --> CR
    CR --- Pods
  end

  classDef agent fill:#dbeafe,stroke:#1e40af,color:#000
  classDef runtime fill:#fef3c7,stroke:#92400e,color:#000
  classDef pod fill:#dcfce7,stroke:#166534,color:#000
  class KL,KP agent
  class CR runtime
  class P1,P2,P3 pod
컴포넌트 역할
kubelet apiserver와 long-lived watch 연결. 자기 노드에 할당된 Pod을 컨테이너 런타임에 명령
kube-proxy Service ClusterIP → 실제 Pod IP 라우팅. iptables 또는 IPVS 룰 관리
Container Runtime (containerd) 실제 컨테이너 프로세스 실행

kubelet은 scheduler나 controller-manager와 직접 통신하지 않는다. 모든 상호작용은 apiserver를 통해 이루어진다.

sequenceDiagram
  participant K as kubelet
  participant A as apiserver
  K->>A: long-lived watch 연결
  A-->>K: 네 노드에 Pod 할당됨
  K->>A: Pod 실행됨, status 보고

4.5 네임스페이스

네임스페이스는 논리적 라벨이다. 노드를 물리적으로 나누지 않는다. 같은 워커 노드에 여러 네임스페이스의 Pod이 함께 실행된다.

flowchart LR
  subgraph W1["워커 1"]
    PA["Pod-A<br/>(apps ns)"]
    PB["Pod-B<br/>(platform ns)"]
    PC["Pod-C<br/>(monitoring ns)"]
  end
  subgraph W2["워커 2"]
    PD["Pod-D<br/>(apps ns)"]
    PE["Pod-E<br/>(monitoring ns)"]
    PF["Pod-F<br/>(platform ns)"]
  end

  classDef apps fill:#dcfce7,stroke:#166534,color:#000
  classDef plat fill:#fde68a,stroke:#92400e,color:#000
  classDef mon fill:#dbeafe,stroke:#1e40af,color:#000
  class PA,PD apps
  class PB,PF plat
  class PC,PE mon

용도:

  • 격리: RBAC, 리소스 쿼터, NetworkPolicy의 단위
  • 이름 충돌 방지: 다른 네임스페이스에 같은 이름의 리소스가 공존 가능 (apps/nginx, monitoring/nginx)
  • 삭제 단위: kubectl delete namespace foo → 네임스페이스 안의 모든 리소스 일괄 삭제
  • 시야 분리: kubectl get pods는 현재 네임스페이스만 표시

Service와의 차이:

  • Service: Pod 라우팅 (가상 IP, DNS, 변경되는 Pod IP를 추상화)
  • Namespace: Pod 그룹화 (조직, 권한, 격리)

4.6 datastore 장애 영향

데이터 상태 결과
데이터 파일 무사 (k3s state.db) 자동 재시작 → 정상 복귀
데이터 파일 소실 선언된 클러스터 상태가 사라져 새 클러스터처럼 됨
백업 존재 백업 시점으로 복구

datastore 장애 시 영향:

  • 이미 실행 중인 Pod: 워커에서 계속 실행
  • kubectl 명령: 실패
  • scheduler / controller-manager: watch 중단
  • 신규 배포 / 자동 복구: 동작하지 않음

5. 배포 책임

5.1 구성 순서

flowchart LR
  Bootstrap["bootstrap.sh<br/>k3d 클러스터 + kubeconfig"]
  Terraform["terraform apply<br/>namespace + ingress-nginx<br/>ArgoCD + root Application"]
  Argo["ArgoCD<br/>gitops/apps 배포"]
  Reconcile["지속적인 reconcile<br/>git 반영 + drift 복구"]

  Bootstrap ==> Terraform ==> Argo --> Reconcile

  classDef bootstrap fill:#fde68a,stroke:#92400e,color:#000
  classDef platform fill:#dbeafe,stroke:#1e40af,color:#000
  classDef gitops fill:#dcfce7,stroke:#166534,color:#000
  class Bootstrap bootstrap
  class Terraform platform
  class Argo,Reconcile gitops

5.2 책임 경계

  • bootstrap은 k3d 클러스터와 kubeconfig를 만든다.
  • Terraform은 kubeconfig를 받아 namespace, ingress-nginx, ArgoCD와 root Application을 설치한다.
  • ArgoCD는 gitops/apps/의 Application을 배포하고 git과 다른 상태를 되돌린다.

5.3 App-of-Apps 패턴

Terraform은 ArgoCD와 root Application까지만 설치한다. root Application은 gitops/apps/ 아래의 Application을 읽어 실제 앱을 배포한다.

cosmos/gitops/
├── apps/
│   ├── podinfo.yaml
│   ├── kube-prometheus-stack.yaml
│   ├── loki.yaml
│   └── alloy.yaml
└── projects/

Terraform과 ArgoCD의 책임은 나뉘어 있다.

  • Terraform: ArgoCD 최초 설치 + root-app 등록까지
  • ArgoCD: gitops/apps/에 선언된 앱 배포와 상태 복구
  • Terraform 재실행: 클러스터를 다시 만들거나 플랫폼 구성을 바꿀 때

6. 트래픽 종류

클러스터 안의 트래픽은 사용자, GitOps, 관측 경로로 나눠 볼 수 있다.

6.1 사용자 트래픽

flowchart TB
  Browser["🌐 브라우저"]
  Host["🖥 Mac mini 호스트<br/>(80/443)"]
  VM["💿 OrbStack VM<br/>노출 포트"]
  LB["🔵 k3d 로드밸런서<br/>(serverlb 컨테이너)"]
  Ing["📦 ingress-nginx Pod<br/>(워커, platform ns)<br/>host header / path 라우팅"]
  Svc["🎯 Service ClusterIP<br/>(가상 IP, kube-proxy 변환)"]
  Pod["🟢 앱 Pod"]

  Browser --> Host --> VM --> LB --> Ing --> Svc --> Pod

  classDef edge fill:#fde2e4,stroke:#9d174d,color:#000
  classDef k8s fill:#dbeafe,stroke:#1e40af,color:#000
  classDef pod fill:#dcfce7,stroke:#166534,color:#000
  class Browser,Host,VM edge
  class LB,Ing,Svc k8s
  class Pod pod

사용자 트래픽은 apiserver를 거치지 않고 data plane으로 흐른다.

6.2 GitOps 트래픽

flowchart TB
  Daniel["👨 사용자 IDE"]
  GH["☁ GitHub<br/>(cosmos repo, gitops/ 하위)"]
  Argo["📦 ArgoCD Pod<br/>(워커, platform ns)<br/>desired (git) vs current (cluster) 비교"]
  API["apiserver"]
  ETCD[("etcd")]
  CM["controller-manager"]
  Cluster["🔧 클러스터 상태 변경"]

  Daniel -- git push --> GH
  GH -. polling 또는 webhook .-> Argo
  Argo -- RPC --> API
  API --- ETCD
  API --> CM
  CM --> Cluster

  classDef green fill:#dcfce7,stroke:#166534,color:#000
  class Daniel,GH,Argo,API,ETCD,CM,Cluster green

ArgoCD는 git을 3분마다 확인한다. GitHub webhook도 함께 사용해 push 직후 동기화를 시작하며, polling은 webhook이 실패했을 때를 대비한 안전망으로 남긴다.

ArgoCD는 일반 Kubernetes client처럼 apiserver를 호출하며 etcd에는 직접 접근하지 않는다. GitOps 트래픽은 control plane 트래픽으로 분류된다.

6.3 관측 트래픽

Prometheus, Grafana, Alertmanager, exporter, Loki, Alloy는 monitoring 네임스페이스에서 실행된다.

메트릭 (Prometheus, pull 방식)

flowchart LR
  API["kube-apiserver"]
  Monitor["ServiceMonitor / PodMonitor CR"]
  PromCR["Prometheus CR"]
  Operator["Prometheus Operator"]
  Runtime["StatefulSet controller / kubelet"]
  Prom["Prometheus Pod<br/>+ config-reloader"]
  Graf["Grafana"]
  User["사용자"]

  subgraph Targets["수집 대상"]
    Kubelet["kubelet / cAdvisor"]
    Node["node-exporter"]
    KSM["kube-state-metrics"]
    App["앱 /metrics"]
  end

  Monitor --> |CR 저장| API
  PromCR --> |CR 저장| API
  Operator -. "CR list/watch" .-> API
  Operator --> |StatefulSet·config Secret create/update| API
  Runtime -. "desired resource watch" .-> API
  Runtime --> |Pod 실행| Prom
  Prom -. "service discovery list/watch" .-> API
  KSM -. "Kubernetes 객체 list/watch" .-> API
  Prom == "주기적 scrape" ==> Kubelet
  Prom == "주기적 scrape" ==> Node
  Prom == "주기적 scrape" ==> KSM
  Prom == "주기적 scrape" ==> App
  User --> Graf -- "PromQL" --> Prom

  classDef blue fill:#dbeafe,stroke:#1e40af,color:#000
  class API,Monitor,PromCR,Operator,Runtime,Prom,Graf,User,Kubelet,Node,KSM,App blue

Prometheus Operator는 Prometheus, ServiceMonitor, PodMonitor 리소스를 보고 StatefulSet과 수집 설정을 만든다. Prometheus는 apiserver에서 수집 대상을 찾은 뒤 각 exporter와 앱의 /metrics를 직접 읽는다. Grafana는 PromQL로 Prometheus를 조회하며 메트릭을 따로 저장하지 않는다.

로그 (Alloy → Loki, push 방식)

flowchart LR
  API["kube-apiserver"]
  User["사용자"]
  Graf["Grafana"]

  subgraph Node["각 k3d 노드"]
    Pod["Pod stdout / stderr"]
    Files["/var/log/pods"]
    Alloy["Grafana Alloy DaemonSet"]
    Position["/var/lib/alloy<br/>읽은 위치"]
    Pod --> |CRI 로그| Files --> |tail| Alloy
    Alloy --- Position
  end

  Loki["Loki SingleBinary"]
  Gateway["Loki gateway"]
  PVC["local-path PVC"]

  Alloy -. "Pod get/list/watch<br/>node field selector" .-> API
  Alloy == "Loki push API" ==> Gateway --> Loki
  Loki --- PVC
  User --> Graf -- "LogQL" --> Gateway

  classDef blue fill:#dbeafe,stroke:#1e40af,color:#000
  class API,User,Graf,Pod,Files,Alloy,Position,Gateway,Loki,PVC blue

Alloy는 각 노드의 Pod 로그를 읽어 Loki로 보낸다. spec.nodeName으로 자기 노드의 Pod만 찾고, 읽은 위치는 /var/lib/alloy에 보관한다. 로그에는 namespace, pod, container, node, job, cluster 라벨이 붙는다. Promtail은 지원이 끝났기 때문에 사용하지 않는다.

6.4 경로 비교

트래픽 apiserver 경유 경로
사용자 아니오 data plane (ingress → service → pod)
GitOps control plane (ArgoCD → apiserver → etcd)
관측 메타데이터 조회만 data plane + apiserver (타겟 목록)
flowchart TB
  Daniel["👨 사용자"]
  Browser["🌐 브라우저"]
  Prom["📦 Prometheus"]

  subgraph K8s["k3d Cluster"]
    API["apiserver"]
    ETCD[("etcd")]
    Workers["workers"]
    Ing["ingress-nginx"]
    Svc["Service"]
    Pods["Pods"]

    API --- ETCD
    API --> Workers
    Ing --> Svc --> Pods
  end

  Daniel == "🟢 GitOps (git push → ArgoCD)" ==> API
  Browser ==> |🔴 사용자| Ing
  Prom -. "🔵 메타조회 (타겟 목록)" .-> API
  Prom == "🔵 데이터 pull (/metrics)" ==> Pods

  classDef green fill:#dcfce7,stroke:#166534,color:#000
  classDef red fill:#fde2e4,stroke:#9d174d,color:#000
  classDef blue fill:#dbeafe,stroke:#1e40af,color:#000
  class Daniel green
  class Browser red
  class Prom blue

6.5 관측성 배포 구조

flowchart TB
  Git["cosmos repo<br/>gitops/apps"]
  Terraform["Terraform"]

  subgraph Cluster["k3d-cosmos"]
    Argo["ArgoCD<br/>root Application"]
    Storage["local-path StorageClass<br/>PV ReclaimPolicy: Delete"]

    subgraph NS["monitoring namespace"]
      subgraph Metrics["kube-prometheus-stack"]
        Operator["Prometheus Operator"]
        Prom["Prometheus"]
        Graf["Grafana"]
        Alert["Alertmanager"]
        Exporters["kube-state-metrics<br/>node-exporter"]
      end

      subgraph Logs["로그 파이프라인"]
        Alloy["Alloy DaemonSet<br/>노드마다 1개"]
        Gateway["Loki gateway"]
        Loki["Loki SingleBinary<br/>replica 1"]
      end

      PromPVC["Prometheus PVC"]
      LokiPVC["Loki PVC"]
    end
  end

  HostStorage["호스트 영속 저장소"]
  ColdBackup["무결성을 검증한 콜드 백업"]

  Terraform --> |namespace 생성| NS
  Terraform --> |ArgoCD Helm 설치| Argo
  Git --> Argo
  Argo --> |Application reconcile| Metrics
  Argo --> |Application reconcile| Logs
  Prom --- PromPVC
  Loki --- LokiPVC
  PromPVC -. "dynamic provisioning" .-> Storage
  LokiPVC -. "dynamic provisioning" .-> Storage
  Storage --- HostStorage
  HostStorage -. "정기 백업" .-> ColdBackup
  Graf --> |PromQL query| Prom
  Graf --> |LogQL query| Gateway
  Alloy --> Gateway --> Loki

  classDef infra fill:#fef3c7,stroke:#92400e,color:#000
  classDef gitops fill:#dcfce7,stroke:#166534,color:#000
  classDef observe fill:#dbeafe,stroke:#1e40af,color:#000
  classDef storage fill:#f3f4f6,stroke:#4b5563,color:#000
  class Terraform,NS infra
  class Git,Argo gitops
  class Operator,Prom,Graf,Alert,Exporters,Alloy,Gateway,Loki observe
  class PromPVC,LokiPVC,Storage,HostStorage,ColdBackup storage

관측성 스택의 Helm chart 버전은 다음과 같다.

역할 Chart Chart 버전 App 버전 배포 형태
메트릭·대시보드·알림 kube-prometheus-stack 87.17.0 v0.92.1 Prometheus/Grafana/Operator/Alertmanager
로그 저장·조회 loki 7.1.0 3.6.8 SingleBinary 1 replica
노드 로그 수집 alloy 1.10.1 v1.17.1 DaemonSet 3 replicas

Prometheus와 Loki는 각각 5Gi PVC를 쓴다. local-path 볼륨의 실제 데이터는 k3d 노드 수명과 분리된 호스트 저장소에 놓고, 무결성을 검증한 콜드 백업으로 별도 보존한다. Grafana의 Prometheus·Loki 데이터 소스는 배포할 때 자동으로 등록된다.

Grafana는 http://cosmos.tail511b20.ts.net/grafana에서 연다. COSMOS Overview에는 노드와 Pod 상태, 리소스 사용량, 알림, 최근 로그가 모여 있다. 시간대는 Asia/Seoul, 새로 고침 주기는 30초다. 메트릭 수집 경로는 하위 경로에 맞춰 /grafana/metrics로 지정했다.

Alloy에는 Pod 조회 권한만 준다. Loki는 사용하지 않는 rules sidecar와 서비스 계정 토큰 마운트를 끈다. Grafana Pod는 monitoring 네임스페이스의 ConfigMap을 읽을 수 있지만 Secret은 읽을 수 없다.

Loki는 로그를 7일 보관하고, 만료된 데이터는 compactor가 비동기로 지운다. StatefulSet을 축소하거나 삭제해도 PVC는 남는다. PVC를 직접 삭제하면 local-path PV의 Delete 정책에 따라 데이터도 지워질 수 있으므로, PVC와 호스트 저장소는 백업을 대신하지 않는다.

자동 복구는 파일이 존재하는지만 확인하지 않는다. 클러스터를 다시 시작한 뒤 모든 노드와 관측성 Pod의 readiness, 새로고침한 GitOps 동기화 상태까지 통과해야 완료된다. 이어지는 운영 검증에서 백업 시점 이전의 메트릭과 로그가 실제로 조회되는지 확인한다.

Prometheus·Loki·Grafana와 Alloy Pod를 교체해도 컨트롤러가 새 Pod를 만들고 기존 PVC를 다시 연결한다. 이 과정에서 재시작 전 메트릭과 로그가 유지되고 Alloy의 신규 로그 수집도 재개되는 것을 확인했다. ArgoCD가 관리하는 Deployment의 replica 수를 Git 선언과 다르게 바꾸면 self-heal이 선언된 값으로 되돌린다.

공유기 포트포워딩은 열지 않고 Tailscale MagicDNS로 접속한다. MagicDNS는 접근할 이름을 제공할 뿐, ingress의 출발지 제한까지 보장하지는 않는다. k3d가 호스트의 80/443을 열기 때문에 엄격한 접근 제한에는 별도의 호스트 바인딩이나 방화벽 설정이 필요하다.

k3s는 scheduler와 controller-manager를 별도 Pod가 아닌 단일 서버 프로세스에 포함하며, 현재 단일 server 구성의 datastore는 SQLite다. 따라서 etcd monitor는 비활성화하고, 표준 배포처럼 독립된 in-cluster scrape endpoint가 노출되지 않는 scheduler/controller-manager monitor도 비활성화해 거짓 장애 신호를 막는다.


7. 리포지토리 구조

Terraform 영역과 ArgoCD가 감시하는 영역을 한 저장소 안에서 디렉토리로 나눈다.

cosmos/
├── infra/                    # Terraform 영역 (ArgoCD watch ❌)
│   ├── bootstrap/
│   │   ├── install.sh
│   │   └── restore-secrets.sh
│   ├── secrets/              # sops 암호문
│   ├── terraform/
│   │   ├── modules/
│   │   └── envs/local/
├── gitops/                   # ArgoCD watch 영역
│   ├── apps/                 # podinfo, prometheus-stack, loki, alloy
│   └── projects/
└── docs/
    ├── remote-access.md
    ├── secrets.md
    └── decisions/

ArgoCD는 gitops/apps만 감시한다. infra/의 Terraform 상태나 bootstrap 스크립트 변경은 자동 배포 대상이 아니다.

infra/terraform/ 하위의 state 파일은 .gitignore로 보호한다.

infra/terraform/**/.terraform/
infra/terraform/**/*.tfstate
infra/terraform/**/*.tfstate.backup

8. 운영

8.1 백업 우선순위

우선순위 대상 위치 잃었을 때 영향
1 age 개인키 호스트 외부의 별도 보관소 Git에 저장한 Secret 암호문을 복호화할 수 없음
2 terraform.tfstate 로컬 파일 Terraform이 기존 리소스를 인식하지 못해 중복 생성이나 충돌이 발생할 수 있음
3 클러스터 선언 Terraform + GitOps 저장소 플랫폼과 앱의 desired state를 재구성할 수 없음
4 영구 데이터 호스트 영속 저장소 stateful 앱 데이터 손실
자동 git repo GitHub (GitHub이 보존)

8.2 호스트 이전 절차

새 호스트에는 다음 네 항목을 옮긴다.

  1. cosmos repo (GitHub clone)
  2. terraform.tfstate (로컬 파일, 직접 옮김)
  3. ~/srv/data/ (rsync)
  4. 호스트 외부에 보관한 age 개인키

새 호스트에서 실행:

brew install orbstack k3d kubectl helm terraform sops age tailscale
git clone github.com/currenjin/cosmos
# tfstate, ~/srv/data, sops key 옮기기
cd cosmos && ./infra/bootstrap/install.sh   # 빈 k3d 클러스터 생성
cd infra/terraform/envs/local && terraform apply  # platform 다시 설치
cd ../../../.. && ./infra/bootstrap/restore-secrets.sh  # ArgoCD repo credential 복원
# ArgoCD가 root-app을 sync해 모든 앱 자동 복원

Terraform이 root Application을 먼저 만들기 때문에 repository credential 복원 전에는 일시적으로 sync가 실패할 수 있다. Secret 복원 후 ArgoCD가 재시도하며, root와 하위 Application이 Synced / Healthy인지 확인한다.

8.3 디버깅 진입점

증상 점검 위치
외부에서 앱 접속 안 됨 Tailscale 연결 → ingress-nginx Pod 상태 → Service endpoints → 앱 Pod
kubectl 명령 실패 apiserver 상태 → k3s datastore (state.db) 정상성
git push했는데 배포 안 됨 ArgoCD가 git 변경을 감지했는지 (polling/webhook) → root-app sync 상태 → application Pod
메트릭 빈 칸 Prometheus의 ServiceMonitor/PodMonitor → /metrics 엔드포인트 응답
로그 안 보임 Alloy DaemonSet의 파일 읽기 상태 → Loki 전송 상태 → LogQL 조회 상태
클러스터 전체 사망 bootstrap → terraform apply → ArgoCD 자동 복구
호스트 이전 tfstate + data + git + sops 키

9. 결정 사항

9.1 이전 가능성 원칙

호스트가 바뀌어도 같은 구성을 다시 만들 수 있어야 한다.

  • 호스트 IP/이름 하드코딩 금지 (localhost, magic DNS, ClusterIP 사용)
  • Kubernetes PVC 데이터는 호스트 영속 저장소에 둔다.
  • 시크릿은 sops + age로 git에 암호화 저장
  • Terraform / ArgoCD / 매니페스트 모두 git에 보관

9.2 외부 접속 정책

  • 공유기 포트포워딩은 사용하지 않고 Tailscale MagicDNS를 기본 접속 경로로 사용
  • 공개 앱이 필요해지면 Cloudflare Tunnel 추가

9.3 Mac mini를 24시간 운영하기

  • pmset -c sleep 0, pmset -c disksleep 0
  • 시스템 설정 → 일반 → 공유 → 원격 로그인 (SSH)
  • macOS hostname은 home-mac, Tailscale hostname은 cosmos으로 고정

10. 참고