<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Accept</title>
    <link>https://accept.tistory.com/</link>
    <description>https://github.com/hrbds</description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 22:30:54 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hrbds</managingEditor>
    <image>
      <title>Accept</title>
      <url>https://tistory1.daumcdn.net/tistory/4691965/attach/cc825c36c22b49a4989a95f77cfa394f</url>
      <link>https://accept.tistory.com</link>
    </image>
    <item>
      <title>[Kubernetes | Part 2] Redis on Kubernetes</title>
      <link>https://accept.tistory.com/110</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Redis on K8s를 배우는가&lt;/li&gt;
&lt;li&gt;StatefulSet vs Deployment&lt;/li&gt;
&lt;li&gt;PersistentVolume / PersistentVolumeClaim&lt;/li&gt;
&lt;li&gt;Helm &amp;mdash; K8s 패키지 매니저&lt;/li&gt;
&lt;li&gt;StatefulSet으로 Redis 구성&lt;/li&gt;
&lt;li&gt;데이터 영속성 확인&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Redis on K8s를 배우는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영 환경에서는 K8s 위에서 Redis를 올려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Deployment로 Redis를 올리면 Redis Pod 재배포 시 데이터 유지 되지 않는 문제가 생긴다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Deployment로 Redis 배포 시
├── Pod 재시작 시 데이터 날아감
├── Pod 이름이 랜덤하게 바뀜 (redis-75fd-hsk82)
└── Redis Cluster 구성이 깨짐
    (노드 이름이 바뀌면 클러스터 인식 불가)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 &lt;b&gt;상태를 가진(Stateful) 앱&lt;/b&gt;이라 일반 Deployment가 아닌 &amp;rarr; &lt;b&gt;StatefulSet + PersistentVolume&lt;/b&gt;&amp;nbsp;구성 필요&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. StatefulSet vs Deployment&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Deployment&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;StatefulSet&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod 이름&lt;/td&gt;
&lt;td&gt;랜덤 (nginx-75fd-hsk82)&lt;/td&gt;
&lt;td&gt;고정 (redis-0, redis-1, redis-2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod 생성 순서&lt;/td&gt;
&lt;td&gt;동시에 생성&lt;/td&gt;
&lt;td&gt;순서대로 생성 (0 &amp;rarr; 1 &amp;rarr; 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;삭제 시 소멸&lt;/td&gt;
&lt;td&gt;PV로 영구 보존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스토리지&lt;/td&gt;
&lt;td&gt;공유 또는 임시&lt;/td&gt;
&lt;td&gt;Pod마다 개별 PVC 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;무상태 앱 (Spring Boot, Vue.js, Nginx 등)&lt;/td&gt;
&lt;td&gt;상태 앱 (Redis, MySQL, Kafka)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod 생성 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulSet은 순서를 보장한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;redis-0 Running 확인
    │
    ▼
redis-1 생성 시작
    │
    ▼
redis-1 Running 확인
    │
    ▼
redis-2 생성 시작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster에서 노드가 순서 없이 뜨면 클러스터 구성이 꼬일 수 있어서 순서 보장이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. PersistentVolume / PersistentVolumeClaim&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;PersistentVolume (PV)
&amp;rarr; 실제 스토리지 (디스크)
&amp;rarr; 클러스터 관리자가 미리 생성하거나 동적으로 생성

PersistentVolumeClaim (PVC)
&amp;rarr; Pod가 PV를 요청하는 티켓
&amp;rarr; &quot;나 100Mi짜리 스토리지 필요해&quot;라고 K8s에 요청
&amp;rarr; K8s가 조건에 맞는 PV를 찾아서 연결
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;StatefulSet에서 PVC 동작&lt;/h3&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;volumeClaimTemplates 설정 시
├── redis-0 생성 &amp;rarr; redis-data-redis-0 (PVC) 자동 생성 &amp;rarr; PV 연결
├── redis-1 생성 &amp;rarr; redis-data-redis-1 (PVC) 자동 생성 &amp;rarr; PV 연결
└── redis-2 생성 &amp;rarr; redis-data-redis-2 (PVC) 자동 생성 &amp;rarr; PV 연결
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Pod가 &lt;b&gt;자기만의 독립된 스토리지&lt;/b&gt;를 가진다. Redis 노드마다 데이터가 분리되어야 하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod 재시작 시 PVC 재연결&lt;/h3&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;redis-0 (Pod) 삭제
    │
    ▼
redis-data-redis-0 (PVC) &amp;rarr; 유지 (Pod와 독립적으로 존재)
    │
    ▼
redis-0 (Pod) 재생성 &amp;rarr; 같은 PVC 재연결 &amp;rarr; 데이터 그대로
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. StatefulSet으로 Redis 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;yaml 파일 작성&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet          # Deployment 대신 StatefulSet 사용
metadata:
  name: redis
spec:
  serviceName: redis       # Headless Service 이름 (Pod DNS 생성에 필요)
                           # redis-0.redis, redis-1.redis 형태로 접근 가능
  replicas: 3              # Pod 3개 (순서대로 생성)
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7.2
        ports:
        - containerPort: 6379
        volumeMounts:
        - name: redis-data
          mountPath: /data   # 컨테이너 내부 /data 경로에 볼륨 마운트
                             # Redis 데이터 저장 경로

  volumeClaimTemplates:      # Pod마다 개별 PVC 자동 생성 (StatefulSet 핵심)
  - metadata:
      name: redis-data
    spec:
      accessModes: [&quot;ReadWriteOnce&quot;]  # 하나의 Node에서만 읽기/쓰기
      resources:
        requests:
          storage: 100Mi     # 각 Pod마다 100MB PV 요청
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deployment와 핵심 차이&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# Deployment
# volumeClaimTemplates 없음
# 모든 Pod가 같은 Volume 공유 또는 임시 Volume 사용

# StatefulSet
volumeClaimTemplates:
- metadata:
    name: redis-data       # 이 이름 + Pod 이름으로 PVC 자동 생성
                           # redis-data-redis-0
                           # redis-data-redis-1
                           # redis-data-redis-2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl apply -f redis-statefulset.yaml
kubectl get pods

NAME      READY   STATUS    RESTARTS   AGE
redis-0   1/1     Running   0          95s   &amp;larr; 먼저 생성
redis-1   1/1     Running   0          85s   &amp;larr; 다음
redis-2   1/1     Running   0          84s   &amp;larr; 마지막
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PVC / PV 확인&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get pvc

NAME                 STATUS   VOLUME                  CAPACITY   ACCESS MODES
redis-data-redis-0   Bound    pvc-79146dad-...         100Mi      RWO
redis-data-redis-1   Bound    pvc-d8ce2d9c-...         100Mi      RWO
redis-data-redis-2   Bound    pvc-d13806cf-...         100Mi      RWO
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get pv

NAME                    CAPACITY   RECLAIM POLICY   STATUS   CLAIM
pvc-79146dad-...        100Mi      Delete           Bound    redis-data-redis-0
pvc-d8ce2d9c-...        100Mi      Delete           Bound    redis-data-redis-1
pvc-d13806cf-...        100Mi      Delete           Bound    redis-data-redis-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Redis Pod마다 독립된 PVC &amp;rarr; PV가 자동으로 생성됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 데이터 영속성 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 저장&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl exec -it redis-0 -- redis-cli set ari:card:1 &quot;legendary&quot;
# OK

kubectl exec -it redis-0 -- redis-cli get ari:card:1
# &quot;legendary&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod 강제 삭제 후 데이터 유지 확인&lt;/h3&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;kubectl delete pod redis-0
# pod &quot;redis-0&quot; deleted

kubectl get pods
# NAME      READY   STATUS    RESTARTS   AGE
# redis-0   1/1     Running   0          4s    &amp;larr; 자동 복구, 이름 고정
# redis-1   1/1     Running   0          5m24s
# redis-2   1/1     Running   0          5m23s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deployment와 차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Deployment  &amp;rarr; 삭제 후 nginx-75fd-abc12 (랜덤 이름)
StatefulSet &amp;rarr; 삭제 후 redis-0 (고정 이름) ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;kubectl exec -it redis-0 -- redis-cli get ari:card:1
# &quot;legendary&quot; &amp;larr; 데이터 그대로 유지 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 삭제되고 재생성됐는데도 데이터가 살아있다. PVC가 Pod와 독립적으로 존재하기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Redis on K8s 핵심 원칙
├── StatefulSet: 상태 앱 배포 표준
│   ├── Pod 이름 고정 (redis-0, redis-1, redis-2)
│   ├── 순서대로 생성/삭제
│   └── Pod 재시작 시 같은 이름으로 복구
├── volumeClaimTemplates: Pod마다 개별 PVC 자동 생성
│   ├── redis-data-redis-0 (redis-0 전용)
│   ├── redis-data-redis-1 (redis-1 전용)
│   └── redis-data-redis-2 (redis-2 전용)
├── PVC는 Pod 삭제 시 유지 &amp;rarr; 데이터 영속성 보장
└── Helm uninstall 후 PVC는 수동 삭제 필요

앱별 K8s 배포 방식
├── Stateless (Spring Boot, Vue.js, Nginx) &amp;rarr; Deployment
└── Stateful (Redis, MySQL, Kafka)         &amp;rarr; StatefulSet
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>cluster</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>redis</category>
      <category>Redis Cluster</category>
      <category>stateful</category>
      <category>StatefulSet</category>
      <category>stateless</category>
      <category>volumneClaimTemplates</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/110</guid>
      <comments>https://accept.tistory.com/110#entry110comment</comments>
      <pubDate>Sun, 3 May 2026 21:05:52 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes | Part 1] Kubernetes 기초</title>
      <link>https://accept.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Kubernetes가 필요한가&lt;/li&gt;
&lt;li&gt;K8s 핵심 개념&lt;/li&gt;
&lt;li&gt;로컬 클러스터 구성 (minikube)&lt;/li&gt;
&lt;li&gt;Pod / Deployment / ReplicaSet&lt;/li&gt;
&lt;li&gt;Service &amp;mdash; 네트워크 접근&lt;/li&gt;
&lt;li&gt;Rolling Update / Rollback&lt;/li&gt;
&lt;li&gt;ConfigMap / Secret&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Kubernetes가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아리 앱은 4개 MSA 서비스 + Redis + MySQL + Kafka로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 수십 개로 늘어나면 Docker만으로는 관리가 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Docker만 사용 시
├── 컨테이너가 죽으면? &amp;rarr; 수동으로 재시작
├── 트래픽 폭증하면? &amp;rarr; 수동으로 컨테이너 추가
└── 배포하면?        &amp;rarr; 서비스 중단

Kubernetes 사용 시
├── 컨테이너가 죽으면? &amp;rarr; 자동 재시작 (자가 치유)
├── 트래픽 폭증하면? &amp;rarr; 자동 스케일링 (HPA)
└── 배포하면?        &amp;rarr; 무중단 Rolling Update
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. K8s 핵심 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Control Plane (두뇌)
├── API Server    &amp;rarr; 모든 요청의 진입점
├── etcd          &amp;rarr; 클러스터 상태 저장소
├── Scheduler     &amp;rarr; Pod를 어느 Node에 배치할지 결정
└── Controller    &amp;rarr; 원하는 상태 유지 담당

Worker Node (손발)
├── kubelet       &amp;rarr; Node에서 Pod 실행 담당
├── kube-proxy    &amp;rarr; 네트워크 규칙 관리
└── Container Runtime &amp;rarr; 실제 컨테이너 실행

&quot;kubectl apply하면 API Server가 받아서 Scheduler가 Pod를 Node에 배치하고 kubelet이 실제로 컨테이너를 실행한다&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 오브젝트&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Pod&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컨테이너를 감싸는 최소 배포 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ReplicaSet&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Pod를 지정한 수만큼 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Deployment&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ReplicaSet 관리, Rolling Update 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Pod에 접근하는 네트워크 엔드포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Ingress&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외부 트래픽을 서비스로 라우팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ConfigMap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;설정값 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Secret&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;민감 정보 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;K8s 오브젝트 연결 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s 오브젝트들은 &lt;b&gt;이름 또는 라벨&lt;/b&gt;로 서로 참조한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;연결 방식
├── Service   &amp;rarr; selector.app    (라벨로 Pod 선택)
├── ConfigMap &amp;rarr; metadata.name   (이름으로 참조)
└── Secret    &amp;rarr; metadata.name   (이름으로 참조)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 라벨 기반이라 같은 라벨을 가진 Pod면 자동으로 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 재시작되어 IP가 바뀌어도 Service가 라벨로 자동으로 찾아준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;minikube vs 실제 K8s&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;실제 K8s 환경
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ Control     │  │ Worker      │  │ Worker      │
│ Plane 서버  │  │ Node 서버 1 │  │ Node 서버 2 │
└─────────────┘  └─────────────┘  └─────────────┘

minikube (로컬)
┌──────────────────────────────┐
│  맥북 안에서 전부 가상으로    │
│  Control Plane + Worker Node │
└──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minikube에서 익힌 yaml은 실제 K8s 환경에서도 동일하게 동작한다. 차이는 인프라 레벨(서버 구성)에만 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 로컬 클러스터 구성&lt;/h2&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;minikube start

kubectl get nodes
# NAME       STATUS   ROLES           AGE   VERSION
# minikube   Ready    control-plane   4m    v1.35.1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Pod / Deployment / ReplicaSet&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod &amp;mdash; 최소 배포 단위&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Pod 직접 실행 (실무에서 사용 안 함)
kubectl run nginx-test --image=nginx
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod를 직접 띄우면 죽었을 때 자동 재생성이 안된다. ReplicaSet이 없기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kubectl delete pod nginx-test
kubectl get pods
# No resources found  &amp;larr; 그냥 사라짐
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Deployment &amp;mdash; 표준 배포 방식&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl apply -f nginx-deployment.yaml
kubectl get pods

NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-75fdcbbc74-hsk82   1/1     Running   0          8s
nginx-deployment-75fdcbbc74-rg6dw   1/1     Running   0          8s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 이름 구조&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;nginx-deployment-75fdcbbc74-hsk82
│                │           │
│                │           └── Pod 고유 ID (랜덤)
│                └── ReplicaSet ID
└── Deployment 이름
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자가 치유 (Self-healing) 실습&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl delete pod nginx-deployment-75fdcbbc74-hsk82

kubectl get pods
# NAME                                READY   STATUS    AGE
# nginx-deployment-75fdcbbc74-lph55   1/1     Running   9s   &amp;larr; 새로 생성
# nginx-deployment-75fdcbbc74-rg6dw   1/1     Running   65s  &amp;larr; 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReplicaSet이 항상 2개를 유지하도록 자동으로 새 Pod를 생성한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Deployment / ReplicaSet / Pod 관계&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get deployment
kubectl get replicaset
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Deployment (nginx-deployment)
└── ReplicaSet (nginx-deployment-75fdcbbc74)
    ├── Pod (lph55)
    └── Pod (rg6dw)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReplicaSet은 Auto Scaling (HPA)과 같이 최소 N개 유지라는 점에서는 비슷한 점이 있음..&lt;/h3&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;ReplicaSet
&amp;rarr; &quot;항상 N개를 유지해라&quot; (고정)
&amp;rarr; Pod가 죽으면 자동 재생성

HPA (Horizontal Pod Autoscaler)
&amp;rarr; &quot;트래픽에 따라 N~M개 사이로 자동 조절해라&quot;
&amp;rarr; CPU/메모리 사용률 기반으로 Pod 수 자동 증감
&amp;rarr; K8s의 Auto Scaling
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Service &amp;mdash; 네트워크 접근&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod IP는 클러스터 내부에서만 접근 가능하고, Pod가 재시작될 때마다 바뀐다. Service는 고정된 접근 창구를 제공한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
  type: NodePort
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl apply -f nginx-service.yaml
kubectl get service

NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)
nginx-service   NodePort    10.109.88.102   &amp;lt;none&amp;gt;        80:31887/TCP
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 접근 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부에서 직접 호출 (NodePort)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl 127.0.0.1:62068      (Mac 로컬, minikube 터널)
    │
    ▼
192.168.49.2:31887         (minikube VM(Docker Container) IP:NodePort)
    │
    ▼
10.244.0.x:80              (Pod 내부 IP)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 내부 통신 (ClusterIP)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;application.yml &amp;rarr; host: redis-service
    │ K8s 내부 DNS
    ▼
10.109.88.102:80           (ClusterIP)
    │
    ▼
10.244.0.x:80              (Pod 내부 IP)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 역할 정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;IP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127.0.0.1:62068&lt;/td&gt;
&lt;td&gt;minikube 터널 (로컬 개발용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.49.2&lt;/td&gt;
&lt;td&gt;minikube VM IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31887&lt;/td&gt;
&lt;td&gt;NodePort (외부 접근용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.109.88.102&lt;/td&gt;
&lt;td&gt;ClusterIP (클러스터 내부 통신용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.244.0.x&lt;/td&gt;
&lt;td&gt;Pod 내부 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 내부에서는 IP 대신 &lt;b&gt;Service 이름&lt;/b&gt;으로 통신한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# application.yml
spring:
  datasource:
    url: jdbc:mysql://mysql-service:3306/aridb
  redis:
    host: redis-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s 내부 DNS가 mysql-service &amp;rarr; ClusterIP로 자동 변환해준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Rolling Update / Rollback&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rolling Update &amp;mdash; 무중단 배포&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;kubectl set image deployment/nginx-deployment nginx=nginx:1.25
kubectl rollout status deployment/nginx-deployment

Waiting for deployment &quot;nginx-deployment&quot; rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment &quot;nginx-deployment&quot; rollout to finish: 1 old replicas are pending termination...
deployment &quot;nginx-deployment&quot; successfully rolled out
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 전부 교체하는 게 아니라 순차적으로 교체한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;기존: [nginx:latest] [nginx:latest]
         │
         ▼
중간: [nginx:latest] [nginx:1.25]  &amp;larr; 동시에 2개 버전 운영
         │
         ▼
완료: [nginx:1.25]  [nginx:1.25]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rollback &amp;mdash; 이전 버전으로 복구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 이미지 배포 시&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl set image deployment/nginx-deployment nginx=nginx:잘못된버전

kubectl get pods
# nginx-deployment-5fd577784b-x5wdr   1/1   Running          &amp;larr; 기존 Pod 유지
# nginx-deployment-5fd577784b-zrgqz   1/1   Running          &amp;larr; 기존 Pod 유지
# nginx-deployment-68ffc8f76d-kdvkx   0/1   InvalidImageName &amp;larr; 새 Pod 실패
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Pod가 살아있어서 서비스는 계속 동작한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 이전 버전으로 롤백
kubectl rollout undo deployment/nginx-deployment

# 특정 버전으로 롤백
kubectl rollout undo deployment/nginx-deployment --to-revision=1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 히스토리&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;kubectl rollout history deployment/nginx-deployment

REVISION  CHANGE-CAUSE
1         &amp;lt;none&amp;gt;
3         &amp;lt;none&amp;gt;
4         &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s는 Deployment 변경 이력을 revision으로 관리한다. 실무에서는 변경 이유를 기록해두는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;kubectl annotate deployment/nginx-deployment \
  kubernetes.io/change-cause=&quot;nginx 1.25로 업데이트&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. ConfigMap / Secret&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConfigMap &amp;mdash; 설정값 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지와 설정값을 분리하여 관리한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  APP_ENV: production
  APP_NAME: ari-app
  LOG_LEVEL: info
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Secret &amp;mdash; 민감 정보 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호, API 키 등 민감한 정보는 Secret으로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# nginx-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ari-secret
type: Opaque
stringData:
  DB_PASSWORD: ari1234
  REDIS_PASSWORD: redis1234
  JWT_SECRET: my-super-secret-key
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConfigMap vs Secret 차이&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl describe configmap nginx-config
# APP_ENV: production    &amp;larr; 값이 그대로 노출

kubectl describe secret ari-secret
# DB_PASSWORD: 7 bytes   &amp;larr; 값이 숨겨짐 (바이트 수만 표시)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod에 주입&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# nginx-with-config.yaml
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    envFrom:
    - configMapRef:
        name: nginx-config   &amp;larr; ConfigMap metadata.name
    - secretRef:
        name: ari-secret     &amp;larr; Secret metadata.name
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;kubectl exec -it &amp;lt;pod명&amp;gt; -- env | grep -E &quot;APP_ENV|DB_PASSWORD&quot;
# APP_ENV=production    &amp;larr; ConfigMap에서 주입
# DB_PASSWORD=ari1234   &amp;larr; Secret에서 주입
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아리 앱에서의 활용&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# application.yml
spring:
  datasource:
    password: ${DB_PASSWORD}    # Secret에서 주입
  redis:
    password: ${REDIS_PASSWORD} # Secret에서 주입
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지에 비밀번호를 직접 넣지 않고 K8s가 실행 시점에 주입해준다. 이미지를 공개 Registry에 올려도 비밀번호가 노출되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Secret 보안 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secret은 base64 인코딩이지 암호화가 아니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기본 Secret      &amp;rarr; base64만, 보안 취약
etcd 암호화      &amp;rarr; etcd 저장 시 암호화
AWS Secrets Manager &amp;rarr; 외부 시크릿 관리, 자동 로테이션
HashiCorp Vault  &amp;rarr; 가장 강력한 시크릿 관리 도구
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소한 &lt;b&gt;Secret yaml 파일은 git에 절대 올리지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;K8s 핵심 원칙
├── Pod: 최소 배포 단위, 직접 띄우지 않음
├── Deployment: 실무 표준, ReplicaSet + Rolling Update 관리
├── ReplicaSet: 항상 N개 Pod 유지 (자가 치유)
├── Service: Pod 접근 창구, 라벨로 Pod 선택
│   ├── ClusterIP  &amp;rarr; 클러스터 내부 통신
│   └── NodePort   &amp;rarr; 외부 접근
├── ConfigMap: 공개 설정값 (이미지와 분리)
└── Secret: 민감 정보 (base64, git 제외 필수)

오브젝트 연결 방식
├── Service   &amp;rarr; selector.app  (라벨로 Pod 선택)
├── ConfigMap &amp;rarr; metadata.name (이름으로 참조)
└── Secret    &amp;rarr; metadata.name (이름으로 참조)
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>configmap</category>
      <category>k8s</category>
      <category>K8s Rollback</category>
      <category>k8s secret</category>
      <category>k8s service</category>
      <category>kubernetes</category>
      <category>minikube</category>
      <category>POD</category>
      <category>replicaset</category>
      <category>Rolling Update</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/109</guid>
      <comments>https://accept.tistory.com/109#entry109comment</comments>
      <pubDate>Fri, 1 May 2026 22:10:33 +0900</pubDate>
    </item>
    <item>
      <title>[Redis HA | Part 2] Redis Cluster &amp;mdash; 샤딩 + HA</title>
      <link>https://accept.tistory.com/108</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Redis Cluster가 필요한가&lt;/li&gt;
&lt;li&gt;핵심 개념 &amp;mdash; 샤딩과 해시 슬롯&lt;/li&gt;
&lt;li&gt;Redis Cluster 구성&lt;/li&gt;
&lt;li&gt;슬롯 라우팅 확인&lt;/li&gt;
&lt;li&gt;Failover 실습&lt;/li&gt;
&lt;li&gt;과반수 장애 시 클러스터 중단&lt;/li&gt;
&lt;li&gt;노드 추가 (Scale-out)&lt;/li&gt;
&lt;li&gt;노드 제거 (Scale-in)&lt;/li&gt;
&lt;li&gt;Sentinel vs Cluster&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Redis Cluster가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Part 6에서 Sentinel로 HA를 구성했다. Sentinel은 Master 장애 시 자동 Failover를 보장하지만 한계가 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Sentinel 한계
├── Master 1대에 모든 데이터가 쌓임
├── 메모리 한계 도달 시 수평 확장 불가
└── 아리 앱 카드 100만 장, 좋아요 수억 건 &amp;rarr; 단일 Master로 감당 불가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis Cluster는 데이터를 여러 노드에 분산 저장(샤딩)하여 수평 확장을 가능하게 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; 샤딩과 해시 슬롯&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;샤딩(Sharding)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 여러 노드에 분산 저장하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;단일 Master (Sentinel)       Redis Cluster
┌─────────────┐              ┌────┐ ┌────┐ ┌────┐
│  모든 데이터   │              │ P1 │ │ P2 │ │ P3 │
│  Master 1대  │              │33% │ │33% │ │34% │
└─────────────┘              └────┘ └────┘ └────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 슬롯(Hash Slot)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 &lt;b&gt;16384개의 해시 슬롯&lt;/b&gt;으로 데이터를 분배한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Key &amp;rarr; CRC16 해시 &amp;rarr; 16384로 나눈 나머지 &amp;rarr; 해당 슬롯의 노드로 라우팅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary 3개 기준 슬롯 배분&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Primary 1 &amp;rarr; 슬롯 0 ~ 5460
Primary 2 &amp;rarr; 슬롯 5461 ~ 10922
Primary 3 &amp;rarr; 슬롯 10923 ~ 16383
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 그냥 set key value만 하면 된다. 어느 노드에 저장되는지는 Redis가 자동으로 처리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수평 확장 가능 범위&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Primary 3   &amp;rarr; 슬롯 5461개씩
Primary 6   &amp;rarr; 슬롯 2730개씩
Primary 12  &amp;rarr; 슬롯 1365개씩
Primary 최대 1000개까지 확장 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 태그 (Hash Tag)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mset, pipeline 등 여러 키를 한 번에 처리할 때 키들이 서로 다른 노드에 있으면 Cross-slot 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{} 안의 값이 같으면 무조건 같은 슬롯 &amp;rarr; 같은 노드에 저장되도록 강제할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# Cross-slot 에러 발생 가능
mset ari:card:1 &quot;legendary&quot; ari:card:2 &quot;epic&quot;

# 해시 태그로 같은 슬롯 강제 배치
set {ari:card}:1 &quot;legendary&quot;
set {ari:card}:2 &quot;epic&quot;
# 둘 다 &quot;ari:card&quot; 기준으로 슬롯 계산 &amp;rarr; 같은 노드에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아리 앱 카드 10연차 뽑기처럼 여러 데이터를 한 번에 처리할 때 이 패턴이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 서버 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이상적인 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 1대 = 노드 1개(짝수대)가 이상적이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;서버 1 &amp;rarr; Primary 1
서버 2 &amp;rarr; Primary 2
서버 3 &amp;rarr; Primary 3
서버 4 &amp;rarr; Replica 1 (Primary 1 백업)
서버 5 &amp;rarr; Replica 2 (Primary 2 백업)
서버 6 &amp;rarr; Replica 3 (Primary 3 백업)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 서버가 죽어도 해당 노드만 영향받고 나머지는 정상 운영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비용 절감 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 1대에 노드 2개씩 배치하는 타협안이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;Primary와 자신의 Replica는 반드시 다른 서버에&lt;/b&gt; 배치해야 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;서버 1 &amp;rarr; Primary 1 + Replica 2 (Primary 2의 Replica)
서버 2 &amp;rarr; Primary 2 + Replica 3 (Primary 3의 Replica)
서버 3 &amp;rarr; Primary 3 + Replica 1 (Primary 1의 Replica)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버1이 죽으면&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Primary 1 죽음 &amp;rarr; Replica 1(서버3)이 승격 ✅
Replica 2 죽음 &amp;rarr; Primary 2의 백업 없음 ⚠️
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 서버2까지 죽으면 Primary 2개 장애 &amp;rarr; cluster_state:fail이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3대 구성은 이 리스크를 감수하는 비용 절감 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비용이 허락하면 Primay, Replica를 별도 서버에 구성하도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Stateful 시스템과 Auto Scaling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster에서 Auto Scaling이 어려운 이유&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;일반 앱 Auto Scaling (Stateless)
트래픽 증가 &amp;rarr; 서버 추가 &amp;rarr; 즉시 완료 (수초)

Redis Cluster Scale-out (Stateful)
트래픽 증가 &amp;rarr; 노드 추가 &amp;rarr; 슬롯 리밸런싱 (데이터 이동, 수분~수시간)
&amp;rarr; 이동 중 네트워크/CPU 부하 발생 &amp;rarr; 완료 후 정상화
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis, MySQL 같은 Stateful 시스템은 데이터가 특정 노드에 묶여있어 즉각적인 Auto Scaling이 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 대응 방식:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Self-managed&lt;/b&gt;: 트래픽 예측 후 노드 미리 구성, 증설은 새벽 계획된 작업으로 진행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관리형 서비스(ElastiCache)&lt;/b&gt;: 슬롯 리밸런싱 자동 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Redis Cluster 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드 6개 실행&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;for i in 1 2 3 4 5 6; do
  docker run -d \
    --name redis-cluster-$i \
    --network redis-cluster-net \
    redis:7.2 \
    redis-server \
      --cluster-enabled yes \
      --cluster-config-file nodes.conf \
      --cluster-node-timeout 5000 \
      --appendonly yes
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 확인&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker ps -q --filter name=redis-cluster | xargs docker inspect \
  --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

/redis-cluster-1 172.18.0.2
/redis-cluster-2 172.18.0.3
/redis-cluster-3 172.18.0.4
/redis-cluster-4 172.18.0.5
/redis-cluster-5 172.18.0.6
/redis-cluster-6 172.18.0.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--cluster-replicas 1 &amp;mdash; Primary 1개당 Replica 1개 자동 배정&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli --cluster create \
  172.18.0.2:6379 \
  172.18.0.3:6379 \
  172.18.0.4:6379 \
  172.18.0.5:6379 \
  172.18.0.6:6379 \
  172.18.0.7:6379 \
  --cluster-replicas 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성 결과&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Master[0] &amp;rarr; 슬롯 0 ~ 5460     (172.18.0.2) &amp;larr; Replica: 172.18.0.6
Master[1] &amp;rarr; 슬롯 5461 ~ 10922  (172.18.0.3) &amp;larr; Replica: 172.18.0.7
Master[2] &amp;rarr; 슬롯 10923 ~ 16383 (172.18.0.4) &amp;larr; Replica: 172.18.0.5

[OK] All 16384 slots covered.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli cluster info

cluster_state:ok              &amp;rarr; 클러스터 정상 동작
cluster_slots_assigned:16384  &amp;rarr; 16384개 슬롯 전부 배정
cluster_slots_ok:16384        &amp;rarr; 전부 정상
cluster_known_nodes:6         &amp;rarr; 6개 노드 인식
cluster_size:3                &amp;rarr; Primary 3개
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 슬롯 라우팅 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster 모드에서는 반드시 -c 옵션을 사용해야 한다. 없으면 다른 슬롯의 키 접근 시 에러가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli -c set ari:card:1 &quot;legendary&quot;
docker exec -it redis-cluster-1 redis-cli -c set ari:card:2 &quot;epic&quot;
docker exec -it redis-cluster-1 redis-cli -c set ari:card:3 &quot;rare&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯 확인&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli cluster keyslot ari:card:1
# 12659 &amp;rarr; Master[2] (10923~16383) 담당

docker exec -it redis-cluster-1 redis-cli cluster keyslot ari:card:2
# 272 &amp;rarr; Master[0] (0~5460) 담당

docker exec -it redis-cluster-1 redis-cli cluster keyslot ari:card:3
# 4401 &amp;rarr; Master[0] (0~5460) 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Failover 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Primary 노드 강제 종료&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker stop redis-cluster-3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Failover 확인 (10초 후)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli cluster nodes

172.18.0.4 &amp;rarr; master,fail  &amp;larr; redis-cluster-3 장애 감지
172.18.0.5 &amp;rarr; master       &amp;larr; redis-cluster-4 자동 승격 (slave &amp;rarr; master)
             slots 10923-16383 인수
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 유지 확인&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli -c get ari:card:1
# &quot;legendary&quot; &amp;larr; 데이터 그대로 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 과반수 장애 시 클러스터 중단&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 Primary 과반수가 죽으면 클러스터 전체가 중단된다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;docker stop redis-cluster-1 redis-cluster-2

docker exec -it redis-cluster-4 redis-cli cluster info | grep cluster_state
# cluster_state:fail

docker exec -it redis-cluster-4 redis-cli -c get ari:card:1
# (error) CLUSTERDOWN The cluster is down
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;docker start redis-cluster-1 redis-cluster-2 redis-cluster-3

docker exec -it redis-cluster-1 redis-cli cluster info | grep cluster_state
# cluster_state:ok

docker exec -it redis-cluster-1 redis-cli -c get ari:card:1
# &quot;legendary&quot; &amp;larr; 데이터 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 노드 추가 (Scale-out)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원칙&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 서비스는 무중단, 노드 관리는 수동&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;무중단 ✅
├── 슬롯 이동 중에도 클러스터 정상 운영
├── 기존 데이터 접근 가능
└── 클라이언트 자동 리다이렉트

수동 관리 필요 ⚠️
├── reshard로 슬롯 직접 이동
├── add-node / del-node 직접 실행
└── 슬롯 0개 확인 후 노드 제거
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯 리밸런싱은 네트워크/CPU 부하가 발생하므로 &lt;b&gt;트래픽이 낮은 새벽에 계획된 작업&lt;/b&gt;으로 진행하는 것이 원칙이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 1. 새 노드 실행
docker run -d \
  --name redis-cluster-7 \
  --network redis-cluster-net \
  redis:7.2 \
  redis-server \
    --cluster-enabled yes \
    --cluster-config-file nodes.conf \
    --cluster-node-timeout 5000 \
    --appendonly yes

# 2. IP 확인
docker inspect redis-cluster-7 \
  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 172.18.0.8

# 3. 클러스터에 새 노드 추가
# 두 번째 IP는 클러스터 접속 진입점 (클러스터 안의 아무 노드 IP나 가능)
docker exec -it redis-cluster-1 redis-cli --cluster add-node \
  172.18.0.8:6379 172.18.0.2:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 직후 상태 &amp;mdash; 슬롯이 없는 master&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;172.18.0.8 &amp;rarr; master, connected (슬롯 없음)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;슬롯 리밸런싱&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli --cluster reshard 172.18.0.2:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값 설명&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;How many slots do you want to move?
&amp;rarr; 4096
  # 16384 &amp;divide; 4 = 4096 (4개 노드 균등 분배 기준)
  # 기존 3개 노드에서 각 1365개씩 가져옴

What is the receiving node ID?
&amp;rarr; 새 노드(172.18.0.8)의 ID 입력
  # 슬롯을 받을 노드 지정

Source node #1:
&amp;rarr; all 입력
  # 기존 모든 노드에서 균등하게 가져옴
  # 특정 노드만 지정하려면 해당 노드 ID 입력 후 done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱 후 슬롯 배분&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Primary 1 &amp;rarr; 1365~5460    (4096개)
Primary 2 &amp;rarr; 6827~10922   (4096개)
Primary 3 &amp;rarr; 12288~16383  (4096개)
Primary 4 &amp;rarr; 0~1364 + 5461~6826 + 10923~12287 (4096개, 3개 구간으로 분리 &amp;mdash; 정상)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯이 연속적이지 않은 것은 기존 3개 노드에서 균등하게 가져왔기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 유지 확인&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli -c get ari:card:1
# &quot;legendary&quot; &amp;larr; 슬롯 이동 중에도 데이터 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replica 추가&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 새 Replica 노드 실행
docker run -d \
  --name redis-cluster-8 \
  --network redis-cluster-net \
  redis:7.2 \
  redis-server \
    --cluster-enabled yes \
    --cluster-config-file nodes.conf \
    --cluster-node-timeout 5000

# Primary 4의 Replica로 추가
# 노드 ID 확인: redis-cli cluster nodes | grep 172.18.0.8
docker exec -it redis-cluster-1 redis-cli --cluster add-node \
  172.18.0.9:6379 172.18.0.2:6379 \
  --cluster-slave \
  --cluster-master-id &amp;lt;Primary4의노드ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 노드 제거 (Scale-in)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드를 그냥 삭제하면 해당 노드의 슬롯이 사라져 데이터 유실이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 &lt;b&gt;Replica 먼저 제거 &amp;rarr; 슬롯 이동 &amp;rarr; Primary 제거&lt;/b&gt; 순서로 진행해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 1. Replica 먼저 제거
docker exec -it redis-cluster-1 redis-cli --cluster del-node \
  172.18.0.2:6379 &amp;lt;Replica4의노드ID&amp;gt;

# 2. Primary 4의 슬롯을 다른 노드로 이동
docker exec -it redis-cluster-1 redis-cli --cluster reshard 172.18.0.2:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값 설명&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;How many slots do you want to move?
&amp;rarr; 4096  # 제거할 Primary가 가진 슬롯 수

What is the receiving node ID?
&amp;rarr; Primary 1의 ID
  # 슬롯을 받을 노드 지정 (균등 분배 원한다면 reshard를 3번 나눠서 진행)

Source node #1:
&amp;rarr; Primary 4의 ID 입력
  # 제거할 노드만 소스로 지정
Source node #2:
&amp;rarr; done
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 3. 슬롯 이동 후 자동으로 slave로 강등 확인
docker exec -it redis-cluster-1 redis-cli cluster nodes | grep 172.18.0.8
# 172.18.0.8 &amp;rarr; slave (슬롯 0개, 자동 강등)

# 4. Primary 제거
docker exec -it redis-cluster-1 redis-cli --cluster del-node \
  172.18.0.2:6379 &amp;lt;Primary4의노드ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯이 0개가 되면 Redis Cluster가 자동으로 해당 노드를 slave로 강등시킨다. 이후 del-node로 명시적으로 제거하면 완료된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 유지 확인&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker exec -it redis-cluster-1 redis-cli -c get ari:card:1
docker exec -it redis-cluster-1 redis-cli -c get ari:card:2
docker exec -it redis-cluster-1 redis-cli -c get ari:card:3
# &quot;legendary&quot; / &quot;epic&quot; / &quot;rare&quot; &amp;larr; 노드 추가/제거 전 과정에서 데이터 유실 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Sentinel vs Cluster 비교&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; width: 29.0698%;&quot;&gt;&lt;b&gt;구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 13.4884%;&quot;&gt;&lt;b&gt;Failover&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 7.7907%;&quot;&gt;&lt;b&gt;샤딩&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 10.3488%;&quot;&gt;&lt;b&gt;수평확장&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 18.7209%;&quot;&gt;&lt;b&gt;Auto Scaling&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 20.4651%;&quot;&gt;&lt;b&gt;적합한 규모&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.0698%;&quot;&gt;Master-Replica&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;수동&lt;/td&gt;
&lt;td style=&quot;width: 7.7907%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;개발/스테이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.0698%;&quot;&gt;Sentinel&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;자동 ✅&lt;/td&gt;
&lt;td style=&quot;width: 7.7907%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;Replica만 가능&lt;/td&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;중소규모&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.0698%;&quot;&gt;Redis Cluster (Self-managed)&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;자동 ✅&lt;/td&gt;
&lt;td style=&quot;width: 7.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;⚠️ 어려움&lt;/td&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;중대형&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.0698%;&quot;&gt;ElastiCache Cluster&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;자동 ✅&lt;/td&gt;
&lt;td style=&quot;width: 7.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;AWS 등 클라우드 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Redis Cluster 핵심 원칙
├── 16384개 해시 슬롯을 Primary 수에 맞게 자동 분배
├── 키 &amp;rarr; CRC16 &amp;rarr; 슬롯 &amp;rarr; 노드 라우팅 (자동, 개발자 개입 없음)
├── 해시 태그 {} &amp;rarr; 여러 키를 같은 노드에 강제 배치 (mset, pipeline 사용 시)
├── 이상적 구성: 서버 6대 (노드 1개씩)
├── 비용 절감: 서버 3대 (노드 2개씩, 리스크 감수)
├── Primary 과반수 생존 시 클러스터 정상 유지
├── Primary 과반수 장애 시 cluster_state:fail (전체 중단)
├── 노드 추가/제거
│   ├── 클러스터 서비스는 무중단
│   ├── 슬롯 리밸런싱은 수동 (새벽 계획된 작업)
│   ├── 슬롯 0개 시 자동으로 slave 강등
│   └── del-node로 명시적 제거 필요
└── Stateful 특성상 Auto Scaling 대신 용량 계획(Capacity Planning)이 중요
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>cluster</category>
      <category>redis</category>
      <category>redis failover</category>
      <category>redis-cluster</category>
      <category>sentinel</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/108</guid>
      <comments>https://accept.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 30 Apr 2026 21:50:06 +0900</pubDate>
    </item>
    <item>
      <title>[Docker | Part 5] Docker 네트워크 원리 이해</title>
      <link>https://accept.tistory.com/107</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Docker 네트워크를 알아야 하는가&lt;/li&gt;
&lt;li&gt;Docker 네트워크 종류&lt;/li&gt;
&lt;li&gt;컨테이너 간 DNS 통신 원리&lt;/li&gt;
&lt;li&gt;커스텀 네트워크 구성 실습&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Docker 네트워크를 알아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Master-Replica를 구성할 때 Replica가 Master를 어떻게 찾는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s에서 Spring Boot 앱이 Redis에 접속할 때 IP 대신 이름을 쓰는 이유는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너끼리 통신하는 원리를 모르면 이 질문에 답할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 네트워크는 단순한 설정이 아니라 &lt;b&gt;MSA 서비스 간 통신의 기반&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker 네트워크 종류&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.2791%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;b&gt;bridge&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.2791%;&quot;&gt;기본 네트워크. 컨테이너끼리 격리된 공간에서 통신&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;단일 호스트에서 컨테이너 간 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;b&gt;host&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.2791%;&quot;&gt;컨테이너가 Host OS 네트워크를 그대로 사용&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;네트워크 성능이 중요한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;b&gt;none&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.2791%;&quot;&gt;네트워크 완전 차단&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;완전 격리가 필요한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.2093%;&quot;&gt;&lt;b&gt;overlay&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.2791%;&quot;&gt;여러 호스트에 걸친 네트워크&lt;/td&gt;
&lt;td style=&quot;width: 41.3953%;&quot;&gt;K8s, Docker Swarm 등 멀티 호스트 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 단일 호스트 기준으로는 &lt;b&gt;bridge&lt;/b&gt;가 표준이다. K8s 환경에서는 &lt;b&gt;overlay&lt;/b&gt;가 기반이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 bridge vs 커스텀 bridge&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 설치하면 기본 bridge 네트워크(bridge)가 자동 생성된다. 기본 bridge는 컨테이너 이름으로 DNS 통신이 안 된다. IP로만 통신 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커스텀 bridge 네트워크를 만들면 컨테이너 이름으로 DNS 통신이 가능하다.&lt;/b&gt; 이것이 커스텀 네트워크를 쓰는 핵심 이유다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기본 bridge   &amp;rarr; IP로만 통신 (172.17.0.2 등)
커스텀 bridge &amp;rarr; 컨테이너 이름 / 서비스 이름으로 통신
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 컨테이너 간 DNS 통신 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 커스텀 네트워크 안의 컨테이너끼리는 &lt;b&gt;이름이 자동으로 IP로 해석&lt;/b&gt;된다. Docker가 내부 DNS 서버를 자동으로 구성해주기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;redis-master (172.20.0.2)
redis-replica-1 (172.20.0.3)
redis-replica-2 (172.20.0.4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis-replica-1이 redis-master에 접속할 때 :&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;redis-master &amp;rarr; DNS 조회 &amp;rarr; 172.20.0.2 &amp;rarr; 연결
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP를 직접 지정하지 않아도 된다. 컨테이너가 재시작되어 IP가 바뀌어도 이름으로 찾으니까 설정 변경이 필요 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 연결&lt;/b&gt; AWS VPC 안에서 EC2 인스턴스끼리 Private DNS로 통신하는 것과 같은 원리다. Spring Boot application.yml에서 host: redis-master로 쓸 수 있는 이유가 바로 이것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부에서 컨테이너 내부 접근&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mac(Host)에서 컨테이너 내부 네트워크에 직접 접근은 불가하다. 같은 네트워크에 있는 컨테이너를 &lt;b&gt;Bastion Host처럼 경유&lt;/b&gt;해서 접근한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Mac (Host)
    │
    │ docker exec -it redis-master redis-cli
    ▼
redis-master (컨테이너)
    │
    │ redis-master &amp;rarr; redis-replica-1 (이름으로 통신)
    ▼
redis-replica-1 (컨테이너)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 커스텀 네트워크 구성 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 생성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network create redis-cluster-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 확인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network ls
docker network inspect redis-cluster-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너를 커스텀 네트워크에 연결&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;docker run -d \
  --name redis-master \
  --network redis-cluster-net \
  redis:7.2

docker run -d \
  --name redis-replica-1 \
  --network redis-cluster-net \
  redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DNS 통신 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# redis-master에서 redis-replica-1의 IP 조회
docker exec -it redis-master sh -c &quot;getent hosts redis-replica-1&quot;
# 172.20.0.3      redis-replica-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 자동으로 IP로 해석되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 명령어&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 네트워크 목록
docker network ls

# 네트워크 상세 정보 (연결된 컨테이너 확인)
docker network inspect redis-cluster-net

# 실행 중인 컨테이너에 네트워크 추가 연결
docker network connect redis-cluster-net 컨테이너명

# 네트워크 삭제
docker network rm redis-cluster-net
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Docker 네트워크 핵심 원칙
├── 기본 bridge: IP로만 통신 (DNS 안 됨)
├── 커스텀 bridge: 이름으로 DNS 통신 가능 &amp;rarr; 실무 표준
├── overlay: 멀티 호스트 환경 (K8s 기반)
└── 외부 접근: 같은 네트워크 컨테이너를 Bastion Host처럼 경유
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>bridge</category>
      <category>docker</category>
      <category>Docker Network</category>
      <category>overlay</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/107</guid>
      <comments>https://accept.tistory.com/107#entry107comment</comments>
      <pubDate>Thu, 30 Apr 2026 17:33:43 +0900</pubDate>
    </item>
    <item>
      <title>[Docker | Part 4] Volume + Network + Registry</title>
      <link>https://accept.tistory.com/106</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Volume / Network / Registry가 필요한가&lt;/li&gt;
&lt;li&gt;Volume &amp;mdash; 데이터 영속성&lt;/li&gt;
&lt;li&gt;Network &amp;mdash; 컨테이너 격리&lt;/li&gt;
&lt;li&gt;Registry &amp;mdash; 이미지 배포&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 이걸 알아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 데이터가 날아간다 :&lt;/b&gt; MySQL 컨테이너를 삭제하면 DB 데이터가 사라진다. Part1에서 배운 쓰기 가능 레이어가 컨테이너와 함께 소멸하기 때문이다 &amp;rarr; &lt;b&gt;Volume&lt;/b&gt;으로 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 네트워크 격리가 필요하다 :&lt;/b&gt; 모든 컨테이너가 같은 네트워크에 있으면 DB에 누구나 접근 가능하다 &amp;rarr; &lt;b&gt;Network&lt;/b&gt;로 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 이미지를 서버에 전달해야 한다 :&lt;/b&gt; 로컬에서 빌드한 이미지를 K8s 서버에 어떻게 올리나? &amp;rarr; &lt;b&gt;Registry&lt;/b&gt;로 해결&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Volume &amp;mdash; 데이터 영속성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume 없을 때&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;docker run -d \
  --name mysql-no-volume \
  -e MYSQL_ROOT_PASSWORD=ari1234 \
  -e MYSQL_DATABASE=aridb \
  mysql:8

# 데이터 삽입
docker exec -it mysql-no-volume mysql -uroot -pari1234 -e \
  &quot;USE aridb; CREATE TABLE ari_card (id INT, name VARCHAR(50)); INSERT INTO ari_card VALUES (1, 'Legendary Ari');&quot;

# 컨테이너 삭제 후 재생성
docker rm -f mysql-no-volume
docker run -d --name mysql-no-volume ...

# 데이터 확인
docker exec -it mysql-no-volume mysql -uroot -pari1234 -e \
  &quot;USE aridb; SELECT * FROM ari_card;&quot;
# ERROR 1146: Table 'aridb.ari_card' doesn't exist  &amp;larr; 데이터 소멸
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 삭제되면 쓰기 가능 레이어도 함께 사라진다. 그 안에 저장된 데이터도 날아간다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume으로 해결&lt;/h3&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;docker run -d \
  --name mysql-with-volume \
  -e MYSQL_ROOT_PASSWORD=ari1234 \
  -e MYSQL_DATABASE=aridb \
  -v mysql-data:/var/lib/mysql \
  mysql:8
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-v mysql-data:/var/lib/mysql &amp;mdash; mysql-data Named Volume을 컨테이너의 /var/lib/mysql에 마운트한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;컨테이너 내부           Docker Volume (외부)
/var/lib/mysql   &amp;larr;&amp;rarr;   mysql-data
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동기화&lt;/b&gt;가 아니라 &lt;b&gt;마운트&lt;/b&gt;다. 같은 저장소를 바라보는 것이라 별도 복사 과정이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 삭제하고 재생성해도 데이터가 존재하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;docker rm -f mysql-with-volume
docker run -d --name mysql-with-volume -v mysql-data:/var/lib/mysql ...

docker exec -it mysql-with-volume mysql -uroot -pari1234 -e \
  &quot;USE aridb; SELECT * FROM ari_card;&quot;

+------+---------------+
| id   | name          |
+------+---------------+
|    1 | Legendary Ari |   &amp;larr; 데이터 유지
+------+---------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume 종류 3가지&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;명령어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Named Volume&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;-v mysql-data:/var/lib/mysql&lt;/td&gt;
&lt;td&gt;Docker가 관리, 경로 신경 안 써도 됨 &amp;rarr; &lt;b&gt;실무 표준&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Bind Mount&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;-v /home/user/data:/var/lib/mysql&lt;/td&gt;
&lt;td&gt;호스트 특정 경로에 직접 마운트 &amp;rarr; 로컬 개발 시 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;tmpfs&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;--tmpfs /tmp&lt;/td&gt;
&lt;td&gt;메모리에만 저장, 컨테이너 종료 시 소멸 &amp;rarr; 임시 데이터용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volume 관련 명령어&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# Volume 목록 확인
docker volume ls

# Volume 상세 정보 (실제 저장 경로 확인)
docker volume inspect mysql-data

# Volume 삭제
docker volume rm mysql-data

# 사용하지 않는 Volume 전체 삭제
docker volume prune
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker compose down과 Volume&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;docker compose down            # 컨테이너 + 네트워크 삭제, Volume 유지
docker compose down --volumes  # Volume(DB 데이터)까지 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Network &amp;mdash; 컨테이너 격리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 네트워크의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Volume 없이 띄운 컨테이너는 기본 bridge 네트워크에 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 컨테이너가 같은 네트워크에 있으면 누구나 DB에 접근 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 분리 구조&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[Clinet]
    │
    ▼
[frontend-net]
    │
    ▼
[ari-app]  &amp;larr; frontend-net + backend-net 양쪽 연결
    │
    ▼
[backend-net]
    │
    ▼
[MySQL / Redis]  &amp;larr; 외부에서 직접 접근 불가&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 네트워크 생성
docker network create frontend-net
docker network create backend-net

# MySQL을 backend-net에만 연결
docker run -d \
  --name mysql-with-volume \
  --network backend-net \
  -e MYSQL_ROOT_PASSWORD=ari1234 \
  -e MYSQL_DATABASE=aridb \
  -v mysql-data:/var/lib/mysql \
  mysql:8

# ari-app을 backend-net으로 시작
docker run -d \
  --name ari-app \
  --network backend-net \
  redis:7.2

# ari-app을 frontend-net에도 추가 연결
docker network connect frontend-net ari-app
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리 확인&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# frontend-net에만 있는 컨테이너에서 MySQL 접근 시도
docker run -d --name frontend-only --network frontend-net redis:7.2
docker exec -it frontend-only sh -c &quot;getent hosts mysql-with-volume&quot;
# (아무것도 안 나옴) &amp;larr; 접근 불가

# ari-app에서 MySQL 접근 (양쪽 네트워크 연결됨)
docker exec -it ari-app sh -c &quot;getent hosts mysql-with-volume&quot;
# 172.20.0.2      mysql-with-volume  &amp;larr; 접근 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;frontend-only에서는 MySQL이 보이지 않고, ari-app에서는 접근 가능하다. 네트워크 격리가 정확히 동작하고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Network 관련 명령어&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 네트워크 목록 확인
docker network ls

# 네트워크 상세 정보 (연결된 컨테이너 확인)
docker network inspect backend-net

# 네트워크 생성
docker network create backend-net

# 실행 중인 컨테이너에 네트워크 추가 연결
docker network connect frontend-net ari-app

# 네트워크 삭제
docker network rm frontend-net backend-net
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Registry &amp;mdash; 이미지 배포&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Registry란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 저장하고 배포하는 저장소다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker build &amp;rarr; 이미지 생성
     │
     │ docker push
     ▼
Registry (Docker Hub or 자체 Registry)
     │
     │ docker pull
     ▼
K8s 서버 / 팀원 PC&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Hub vs 자체 Registry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub 자체 Registry (Harbor 등)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;오픈소스, 개인/소규모&lt;/td&gt;
&lt;td&gt;보안이 중요한 기업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;무료 (공개) / 유료 (비공개)&lt;/td&gt;
&lt;td&gt;자체 구축 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외부 서버에 이미지 저장&lt;/td&gt;
&lt;td&gt;내부망에서만 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;속도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외부 네트워크 속도 의존&lt;/td&gt;
&lt;td&gt;내부망이라 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융, 의료 등 규제 산업이나 고객사 코드가 포함된 경우 &lt;b&gt;자체 Registry가 필수&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 태깅 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;latest를 쓰면 안 되는 이유:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 버전인지 알 수 없음&lt;/li&gt;
&lt;li&gt;K8s에서 캐시된 이전 이미지를 그대로 쓸 수 있음&lt;/li&gt;
&lt;li&gt;롤백이 불가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 표준은 &lt;b&gt;git SHA 기반 태깅&lt;/b&gt;이다:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# git SHA 기반 (CI/CD 표준)
docker build -t hrbds/ari-app:abc1234 .

# 버전 기반
docker build -t hrbds/ari-app:1.0.0 .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습 &amp;mdash; Docker Hub Push / Pull&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 로그인
docker login

# 태깅
docker tag ari-app-final:latest hrbds/ari-app:1.0.0

# push
docker push hrbds/ari-app:1.0.0
# 1.0.0: digest: sha256:7653... size: 856

# 로컬 이미지 삭제 후 pull 검증
docker rmi hrbds/ari-app:1.0.0
docker pull hrbds/ari-app:1.0.0
# Digest: sha256:7653...  &amp;larr; push할 때와 동일한 다이제스트 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub든 자체 Registry든 명령어는 동일하다. 차이는 이미지 태그에 Registry 주소가 포함되느냐 차이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Docker Hub
docker push hrbds/ari-app:1.0.0

# 자체 Registry
docker push registry.mycompany.com/ari-app:1.0.0

# 자체 Registry 로그인
docker login registry.mycompany.com
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Volume
├── Named Volume: Docker가 관리, 컨테이너 삭제해도 데이터 유지
├── Bind Mount: 호스트 경로 직접 마운트 (로컬 개발용)
└── tmpfs: 메모리 저장, 컨테이너 종료 시 소멸

Network
├── 네트워크 분리로 DB 직접 접근 차단
├── 앱 컨테이너만 frontend + backend 양쪽 연결
└── getent hosts [서비스명]으로 DNS 접근 가능 여부 확인

Registry
├── latest 태그 금지 &amp;rarr; git SHA 또는 버전 기반 태깅
├── Docker Hub / 자체 Registry 명령어 동일
└── 보안 중요 환경 &amp;rarr; 자체 Registry (Harbor 등) 필수
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>docker</category>
      <category>Docker Network</category>
      <category>docker registry</category>
      <category>docker volume</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/106</guid>
      <comments>https://accept.tistory.com/106#entry106comment</comments>
      <pubDate>Thu, 30 Apr 2026 17:03:52 +0900</pubDate>
    </item>
    <item>
      <title>[Docker | Part 3] Docker Compose</title>
      <link>https://accept.tistory.com/105</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Docker Compose가 필요한가&lt;/li&gt;
&lt;li&gt;docker-compose.yml 구조&lt;/li&gt;
&lt;li&gt;서비스 간 DNS 통신&lt;/li&gt;
&lt;li&gt;healthcheck + depends_on&lt;/li&gt;
&lt;li&gt;.env 파일로 환경변수 분리&lt;/li&gt;
&lt;li&gt;핵심 명령어 정리&lt;/li&gt;
&lt;li&gt;환경별 Compose 파일 분리&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Docker Compose가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 띄운다고 하면&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=ari1234 \
  -e MYSQL_DATABASE=aridb \
  -p 3306:3306 \
  mysql:8

docker run -d --name redis \
  -p 6379:6379 \
  redis:7.2

docker run -d --name ari-app \
  -p 8080:8080 \
  --link mysql --link redis \
  ari-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 3개만 돼도 명령어가 이렇게 길어진다. 네트워크 연결, 환경변수, 실행 순서까지 고려하면 수동 관리가 불가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker Compose는 이걸 하나의 파일로 해결한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 전체 스택이 올라온다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. docker-compose.yml 구조&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  redis:
    image: redis:7.2
    container_name: ari-redis
    ports:
      - &quot;6379:6379&quot;
    healthcheck:
      test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
      interval: 5s
      timeout: 3s
      retries: 3

  mysql:
    image: mysql:8
    container_name: ari-mysql
    ports:
      - &quot;3306:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-h&quot;, &quot;localhost&quot;, &quot;-uroot&quot;, &quot;-p${MYSQL_ROOT_PASSWORD}&quot;]
      interval: 5s
      timeout: 3s
      retries: 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;사용할 Docker 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;container_name&lt;/td&gt;
&lt;td&gt;컨테이너 이름 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ports&lt;/td&gt;
&lt;td&gt;호스트:컨테이너 포트 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;environment&lt;/td&gt;
&lt;td&gt;환경변수 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;healthcheck&lt;/td&gt;
&lt;td&gt;컨테이너 상태 확인 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 서비스 간 DNS 통신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 서비스들을 같은 네트워크에 자동으로 연결한다. 같은 네트워크 안에서는 &lt;b&gt;서비스 이름으로 DNS 통신&lt;/b&gt;이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;docker exec -it ari-mysql sh -c &quot;getent hosts redis&quot;
# 172.19.0.3      redis

docker exec -it ari-mysql sh -c &quot;getent hosts mysql&quot;
# 172.19.0.2      mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 이름이 자동으로 IP로 해석된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 덕분에 Spring Boot 등에서 MySQL 접속 시 IP 대신 서비스 이름을 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# application.yml
spring:
  datasource:
    url: jdbc:mysql://mysql:3306/aridb
  redis:
    host: redis
    port: 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP가 바뀌어도 설정을 수정할 필요가 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. healthcheck + depends_on&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 depends_on: mysql만 쓰면 컨테이너가 &lt;b&gt;시작됐다는 것만&lt;/b&gt; 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이 실제로 요청을 받을 준비가 됐는지는 보장하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 앱이 MySQL보다 먼저 뜨면 DB 연결 실패가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;healthcheck로 실제 준비 상태를 확인하고, depends_on으로 순서를 보장한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  mysql:
    image: mysql:8
    healthcheck:
      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-h&quot;, &quot;localhost&quot;]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    image: ari-app
    depends_on:
      mysql:
        condition: service_healthy  &amp;larr; mysql이 healthy 상태일 때만 앱 시작
      redis:
        condition: service_healthy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;healthcheck 적용 후 docker compose ps 결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME        STATUS
ari-mysql   Up 5 seconds (healthy)   &amp;larr; healthy 확인
ari-redis   Up 5 seconds (healthy)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. .env 파일로 환경변수 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호, API 키 같은 민감한 정보를 docker-compose.yml에 직접 넣으면 git에 노출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env 파일로 분리하는 것이 실무 표준이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# .env
MYSQL_ROOT_PASSWORD=ari1234
MYSQL_DATABASE=aridb
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml
environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  MYSQL_DATABASE: ${MYSQL_DATABASE}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env는 반드시 .gitignore에 추가한다:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo &quot;.env&quot; &amp;gt;&amp;gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker-compose.yml   &amp;rarr; git에 올림 (공통 설정)
.env                 &amp;rarr; git에 올리지 않음 (민감 정보)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env 파일은 서버에서 직접 관리하고, docker-compose.yml은 git으로 공유하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 핵심 명령어 정리&lt;/h2&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 전체 서비스 시작 (백그라운드)
docker compose up -d

# 서비스 상태 확인
docker compose ps
docker compose ps -a  # 중지된 것 포함

# 로그 확인
docker compose logs           # 전체 서비스
docker compose logs mysql     # 특정 서비스
docker compose logs -f redis  # 실시간 로그

# 서비스 중지 / 시작
docker compose stop   # 컨테이너만 중지 (삭제 안 함)
docker compose start  # 중지된 컨테이너 재시작

# 정리
docker compose down            # 컨테이너 + 네트워크 삭제
docker compose down --volumes  # 볼륨(DB 데이터)까지 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;stop vs down 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;명령어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;컨테이너&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;네트워크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;볼륨&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stop&lt;/td&gt;
&lt;td&gt;중지&lt;/td&gt;
&lt;td&gt;유지&lt;/td&gt;
&lt;td&gt;유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;down&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;down --volumes&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잠깐 멈출 때 &amp;rarr; stop&lt;/li&gt;
&lt;li&gt;완전히 정리할 때 &amp;rarr; down&lt;/li&gt;
&lt;li&gt;DB 초기화할 때 &amp;rarr; down --volumes&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 환경별 Compose 파일 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 docker-compose.yml로 개발/운영 환경을 모두 관리하면 설정이 복잡해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 분리하고 -f 옵션으로 오버라이드하는 방식이 표준이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;docker-compose.yml         &amp;larr; 공통 설정 (git에 올림)
docker-compose.dev.yml     &amp;larr; 개발 전용 오버라이드
docker-compose.prod.yml    &amp;larr; 운영 전용 오버라이드
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 개발 환경
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# 운영 환경
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 파일이 첫 번째 파일을 &lt;b&gt;오버라이드(덮어쓰기)&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 설정은 그대로 두고, 환경별로 다른 부분만 재정의하는 구조다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.dev.yml
services:
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: dev1234
      MYSQL_DATABASE: aridb_dev

# docker-compose.prod.yml
services:
  mysql:
    environment:
      MYSQL_ROOT_PASSWORD: prod_super_secret
      MYSQL_DATABASE: aridb_prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 개발 환경(docker-compose.dev.yml)으로 띄운 후 확인한다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;docker exec -it ari-mysql mysql -uroot -pdev1234 -e &quot;show databases;&quot;

+--------------------+
| Database           |
+--------------------+
| aridb_dev          |   &amp;larr; dev 설정이 적용됨
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Docker Compose 핵심 원칙
├── docker-compose.yml: 서비스 정의 (image / ports / environment / healthcheck)
├── 서비스 간 통신: 서비스 이름으로 DNS 자동 해석 (IP 불필요)
├── healthcheck + depends_on: 실제 준비 상태 확인 후 순서 보장
├── .env 파일: 민감 정보 분리 &amp;rarr; .gitignore 추가 필수
└── 환경별 분리: -f 옵션으로 dev/prod 오버라이드
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>docker</category>
      <category>docker-compose</category>
      <category>docker-compose.yml</category>
      <category>도커</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/105</guid>
      <comments>https://accept.tistory.com/105#entry105comment</comments>
      <pubDate>Thu, 30 Apr 2026 15:48:55 +0900</pubDate>
    </item>
    <item>
      <title>[Docker | Part 2] Dockerfile + Multi-stage Build</title>
      <link>https://accept.tistory.com/104</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHY &amp;mdash; 왜 Dockerfile을 알아야 하는가&lt;/li&gt;
&lt;li&gt;Dockerfile 핵심 문법&lt;/li&gt;
&lt;li&gt;레이어 캐시 전략&lt;/li&gt;
&lt;li&gt;CMD vs ENTRYPOINT&lt;/li&gt;
&lt;li&gt;Spring Boot 앱 이미지 만들기&lt;/li&gt;
&lt;li&gt;Multi-stage Build&lt;/li&gt;
&lt;li&gt;.dockerignore&lt;/li&gt;
&lt;li&gt;전체 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WHY &amp;mdash; 왜 Dockerfile을 알아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Part 1에서 redis:7.2를 pull해서 컨테이너로 띄웠다. 그건 누군가 만들어둔 이미지를 가져다 쓴 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아리 팬 커뮤니티의 카드 뽑기 서비스, 채팅 서비스를 K8s에 올리려면? &lt;b&gt;내 Spring Boot 앱을 직접 이미지로 만들어야 한다.&lt;/b&gt; Dockerfile을 모르면 그게 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Dockerfile (설계도)
    │
    │ docker build
    ▼
Image (결과물)
    │
    │ docker run
    ▼
Container (실행)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Dockerfile 핵심 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FROM&lt;/td&gt;
&lt;td&gt;Base 이미지 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RUN&lt;/td&gt;
&lt;td&gt;이미지 빌드 시 실행 (레이어 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COPY&lt;/td&gt;
&lt;td&gt;로컬 파일을 이미지 안으로 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENV&lt;/td&gt;
&lt;td&gt;환경변수 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARG&lt;/td&gt;
&lt;td&gt;빌드 시점에만 쓰는 변수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPOSE&lt;/td&gt;
&lt;td&gt;컨테이너가 사용할 포트 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WORKDIR&lt;/td&gt;
&lt;td&gt;작업 디렉토리 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMD&lt;/td&gt;
&lt;td&gt;컨테이너 시작 시 실행할 기본 명령어 (덮어쓰기 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENTRYPOINT&lt;/td&gt;
&lt;td&gt;고정 실행 명령어 (덮어쓰기 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 Dockerfile 예시&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM ubuntu:22.04

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y curl

WORKDIR /app
COPY ari.txt .

ENV DOG_NAME=Ari
ENV DOG_AGE=11

CMD [&quot;cat&quot;, &quot;/app/ari.txt&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 및 실행:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker build -t ari-hello .
docker run --rm ari-hello
# 아리는 11살 믹스견, 최고의 강아지입니다!
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 레이어 캐시 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile 명령어 한 줄이 레이어 하나다. 특정 레이어가 바뀌면 &lt;b&gt;그 아래 레이어는 전부 캐시가 무효화&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나쁜 예 &amp;mdash; 매번 패키지를 재설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM ubuntu:22.04
COPY ari.txt .              &amp;larr; 자주 바뀜
RUN apt-get install curl    &amp;larr; 매번 재실행됨 (27초 낭비)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 예 &amp;mdash; 캐시 최대 활용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM ubuntu:22.04
RUN apt-get install curl    &amp;larr; 잘 안 바뀜 &amp;rarr; CACHED (0초)
COPY ari.txt .              &amp;larr; 자주 바뀜 &amp;rarr; 여기만 재실행
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원칙: 자주 바뀌는 것은 아래에, 잘 안 바뀌는 것은 위에&lt;/b&gt; Spring Boot 앱 기준으로는 라이브러리 설치는 위에, 내 코드 COPY는 아래에 배치한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 확인한 결과:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 첫 빌드
[2/4] RUN apt-get install curl   27.5s

# 두 번째 빌드 (코드만 변경)
[2/4] CACHED RUN apt-get install curl   0.0s  &amp;larr; 캐시 재사용
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CMD vs ENTRYPOINT&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 컨테이너 시작 시 실행되는 명령어지만 동작 방식이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD ENTRYPOINT&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;기본 명령어&lt;/td&gt;
&lt;td&gt;고정 명령어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker run 시 덮어쓰기&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실무 사용&lt;/td&gt;
&lt;td&gt;기본값 지정&lt;/td&gt;
&lt;td&gt;항상 같은 실행 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ENTRYPOINT [&quot;cat&quot;]
CMD [&quot;/app/ari.txt&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# CMD 기본값 사용
docker run --rm ari-hello
# 아리는 11살 믹스견!

# CMD를 덮어써서 다른 파일 출력
docker run --rm ari-hello /etc/hostname
# 148efbe34f9f
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker run 이미지명 뒤에 붙이는 인자는 &lt;b&gt;CMD를 대체&lt;/b&gt;한다. ENTRYPOINT [&quot;cat&quot;]는 항상 고정이고, 인자만 바뀐다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Boot 앱 기준 실무 패턴&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java -jar는 항상 고정이므로 ENTRYPOINT로 지정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Spring Boot 앱 이미지 만들기 (단순 방식)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 gradle로 빌드 후 jar를 COPY하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;./gradlew build
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;build/libs/
├── ari-0.0.1-SNAPSHOT-plain.jar  &amp;larr; 의존성 없는 jar (사용 안 함)
└── ari-0.0.1-SNAPSHOT.jar        &amp;larr; 실행 가능한 fat jar (이걸 씀)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM eclipse-temurin:17-jre-jammy

WORKDIR /app
COPY build/libs/ari-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;docker build -t ari-app .
docker run -d --name ari-app -p 8080:8080 ari-app
curl http://localhost:8080/health
# Ari is alive!  
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Multi-stage Build&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순 방식의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 gradle로 빌드하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬에 Java, Gradle이 설치되어 있어야 함&lt;/li&gt;
&lt;li&gt;CI/CD 서버에도 동일한 환경 구성 필요&lt;/li&gt;
&lt;li&gt;빌드 환경이 표준화되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-stage Build 구조&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Stage 1 (builder)
└── gradle:8-jdk17 (1.24GB)
    소스코드 빌드 &amp;rarr; jar 생성
         │
         │ jar 파일만 꺼냄
         ▼
Stage 2 (runner)
└── eclipse-temurin:17-jre-jammy
    jar만 실행
    
최종 이미지 = 401MB (빌드 환경 1.24GB는 버려짐)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# Stage 1: 빌드
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon -x test

# Stage 2: 실행
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/ari-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JDK vs JRE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK JRE&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;포함 내용&lt;/td&gt;
&lt;td&gt;컴파일러 + 실행 환경&lt;/td&gt;
&lt;td&gt;실행 환경만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 위치&lt;/td&gt;
&lt;td&gt;Stage 1 (빌드)&lt;/td&gt;
&lt;td&gt;Stage 2 (운영)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;큼&lt;/td&gt;
&lt;td&gt;작음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;공격 면적 큼&lt;/td&gt;
&lt;td&gt;공격 면적 작음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 &lt;b&gt;JRE만&lt;/b&gt; 포함하는 것이 표준이다. 컴파일이 필요 없고, 이미지 크기와 보안 측면에서 모두 유리하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-stage Build의 진짜 가치&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;개발자 git push
       │
       ▼
GitHub Actions
       │
       ├── docker build
       │   (Dockerfile 안에서 gradle build &amp;rarr; jar 생성)
       │
       ├── docker push (Registry)
       │
       └── kubectl apply (K8s 배포)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile 하나에 빌드 환경까지 포함되어 있으므로, 개발자는 소스코드만 push하면 이후는 전부 자동이다. &lt;b&gt;로컬에 Java, Gradle이 없어도 빌드 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. .dockerignore&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker build 시 빌드 컨텍스트에 포함되는 파일을 제한한다. 불필요한 파일이 포함되면 빌드가 느려지고 이미지가 커진다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# .dockerignore
.gradle
build
.gitignore
*.md
HELP.md
.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 전후 빌드 컨텍스트 크기 비교:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# .dockerignore 적용 전
transferring context: 456.08kB

# .dockerignore 적용 후
transferring context: 5.06kB   &amp;larr; 약 90배 감소
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build/ 폴더(컴파일된 클래스, jar)와 .gradle/(캐시)가 제외된 덕분이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 전체 요약&lt;/h2&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;Dockerfile 핵심 원칙
├── 레이어 캐시: 자주 바뀌는 것은 아래에, 안 바뀌는 것은 위에
├── ENTRYPOINT: 고정 실행 파일 지정 (java -jar)
├── CMD: 기본 인자 지정 (docker run 시 덮어쓰기 가능)
└── .dockerignore: 불필요한 파일 제외

Multi-stage Build 핵심 원칙
├── Stage 1: JDK + 빌드 도구 (최종 이미지에 포함 안 됨)
├── Stage 2: JRE만 (운영 환경 표준)
├── 소스코드가 최종 이미지에 없음 &amp;rarr; 보안
└── 로컬 환경 없이 빌드 가능 &amp;rarr; CI/CD 연동
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 Dockerfile&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# Stage 1: 빌드
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon -x test

# Stage 2: 실행
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>docker</category>
      <category>dockerfile</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/104</guid>
      <comments>https://accept.tistory.com/104#entry104comment</comments>
      <pubDate>Tue, 28 Apr 2026 14:08:22 +0900</pubDate>
    </item>
    <item>
      <title>[Docker | Part 1] Docker 핵심 구조 이해</title>
      <link>https://accept.tistory.com/103</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경 준비&lt;/li&gt;
&lt;li&gt;컨테이너 vs VM &amp;mdash; 왜 컨테이너가 가벼운가&lt;/li&gt;
&lt;li&gt;Image 레이어 구조&lt;/li&gt;
&lt;li&gt;Image &amp;rarr; Container 생성 원리&lt;/li&gt;
&lt;li&gt;핵심 명령어 실습&lt;/li&gt;
&lt;li&gt;전체 흐름 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 환경 준비&lt;/h2&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;docker --version
# Docker version 28.0.4

docker images
# redis:7.2 이미지 준비 (없으면 아래 명령어로 pull)
docker pull redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 컨테이너 vs VM &amp;mdash; 왜 컨테이너가 가벼운가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VM의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM은 하드웨어를 통째로 가상화한다. Hypervisor 위에 Guest OS를 올리고, 그 위에 앱을 올리는 구조다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Hardware]
    └── Hypervisor
            ├── Guest OS (수 GB) &amp;rarr; App A
            └── Guest OS (수 GB) &amp;rarr; App B
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Guest OS 자체가 수 GB 단위이고, 부팅에도 수십 초~수 분이 걸린다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 Guest OS를 올리지 않는다. &lt;b&gt;Host OS의 커널을 공유&lt;/b&gt;하고, 앱 실행에 필요한 것만 패키징해서 올린다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Hardware]
    └── Host OS (커널 공유)
            ├── Container A (MB 단위)
            └── Container B (MB 단위)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MB 단위이고, 시작도 수 초 안에 완료된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이걸 가능하게 하는 리눅스 커널 기능 2가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;namespace&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컨테이너마다 독립된 프로세스/네트워크/파일시스템 공간 제공. 컨테이너 입장에서는 자기만의 세계처럼 보임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;cgroup&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CPU, 메모리 등 리소스 사용량을 컨테이너별로 제한하고 격리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 연결&lt;/b&gt; K8s에서 Pod에 아래처럼 리소스 제한을 설정하는 것이 바로 cgroup을 활용하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;resources:
  limits:
    cpu: &quot;500m&quot;
    memory: &quot;256Mi&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Image 레이어 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이어란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Image는 여러 레이어가 쌓인 구조다. 각 레이어는 &lt;b&gt;이전 레이어와의 차이(diff)만 저장&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;Layer 8: CMD [&quot;redis-server&quot;]          &amp;larr; 컨테이너 시작 명령어
Layer 7: COPY docker-entrypoint.sh    &amp;larr; 실행 스크립트
Layer 6: WORKDIR /data                &amp;larr; 작업 디렉토리
Layer 5: RUN mkdir /data              &amp;larr; 데이터 디렉토리 생성
Layer 4: RUN redis 소스 컴파일        &amp;larr; 38.1MB (무거운 레이어)
Layer 3: RUN gosu 설치               &amp;larr; 4.28MB
Layer 2: RUN apt-get install          &amp;larr; 기본 패키지
Layer 1: Debian bookworm (Base OS)    &amp;larr; 108MB (제일 무거운 레이어)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이어는 읽기 전용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 레이어는 &lt;b&gt;읽기 전용(Read-Only)&lt;/b&gt;이다. 컨테이너를 실행하면, 이 읽기 전용 레이어들 위에 &lt;b&gt;얇은 쓰기 가능 레이어&lt;/b&gt;가 하나 추가된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[ 쓰기 가능 레이어 ]  &amp;larr; 컨테이너 실행 시 추가. 컨테이너 삭제 시 같이 사라짐
[ Layer 8 ] (읽기 전용)
[ Layer 7 ] (읽기 전용)
[ Layer 6 ] (읽기 전용)
     ...
[ Layer 1 ] (읽기 전용)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 안에서 파일을 수정하면 쓰기 가능 레이어에만 기록된다. 컨테이너를 삭제하면 이 레이어도 같이 사라진다 &amp;mdash; &lt;b&gt;그래서 데이터가 날아가는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Union Filesystem (overlayfs)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 레이어를 하나의 파일시스템처럼 합쳐서 보여주는 기술이다. docker inspect에서 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;docker image inspect redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;&quot;GraphDriver&quot;: {
    &quot;Name&quot;: &quot;overlayfs&quot;   &amp;larr; Union Filesystem 구현체
},
&quot;RootFS&quot;: {
    &quot;Type&quot;: &quot;layers&quot;,
    &quot;Layers&quot;: [
        &quot;sha256:dc784058...&quot;,   &amp;larr; Layer 1
        &quot;sha256:1a60e8b5...&quot;,   &amp;larr; Layer 2
        ...
        &quot;sha256:7965070342...&quot;  &amp;larr; Layer 8
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이어 히스토리 확인&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;docker history redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;IMAGE          CREATED BY                                SIZE
8e674b7c7ae4   CMD [&quot;redis-server&quot;]                      0B
&amp;lt;missing&amp;gt;      EXPOSE map[6379/tcp:{}]                   0B
&amp;lt;missing&amp;gt;      ENTRYPOINT [&quot;docker-entrypoint.sh&quot;]       0B
&amp;lt;missing&amp;gt;      COPY docker-entrypoint.sh /usr/local/bin  20.5kB
&amp;lt;missing&amp;gt;      WORKDIR /data                             4.1kB
&amp;lt;missing&amp;gt;      VOLUME [/data]                            0B
&amp;lt;missing&amp;gt;      RUN mkdir /data &amp;amp;&amp;amp; chown redis:redis...   8.19kB
&amp;lt;missing&amp;gt;      RUN set -eux; redis 소스 컴파일...         38.1MB  &amp;larr; 무거운 레이어
&amp;lt;missing&amp;gt;      ENV REDIS_VERSION=7.2.13                  0B      &amp;larr; 환경변수는 0B
&amp;lt;missing&amp;gt;      RUN apt-get install gosu...               4.28MB
&amp;lt;missing&amp;gt;      RUN apt-get update...                     41kB
&amp;lt;missing&amp;gt;      RUN groupadd -r redis...                  41kB
&amp;lt;missing&amp;gt;      # debian.sh bookworm...                   108MB   &amp;larr; Base OS
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포인트 두 가지&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ENV는 파일시스템 변화가 없으므로 용량이 0B&lt;/li&gt;
&lt;li&gt;용량이 큰 레이어(Base OS, 컴파일)가 어딘지 보인다 &amp;rarr; Chapter 2 Dockerfile 최적화의 기반&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 연결&lt;/b&gt; 레이어 구조를 이해해야 Dockerfile 캐시 전략을 짤 수 있다. 자주 바뀌는 코드는 뒤에, 잘 안 바뀌는 라이브러리 설치는 앞에 배치하는 것이 기본 원칙이다. 이 내용은 Chapter 2에서 직접 실습한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Image &amp;rarr; Container 생성 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker run을 실행하는 순간 내부에서 일어나는 일:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① Image를 로컬에서 검색
   └── 없으면 Registry(Docker Hub)에서 pull

② Image 레이어들 위에 쓰기 가능 레이어 추가

③ namespace로 격리된 공간 생성
   (프로세스 / 네트워크 / 파일시스템)

④ cgroup으로 리소스 제한 설정

⑤ 컨테이너 프로세스 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Host OS 입장에서 컨테이너는 그냥 프로세스다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker inspect로 확인하면:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;State&quot;: {
    &quot;Status&quot;: &quot;running&quot;,
    &quot;Pid&quot;: 605   &amp;larr; Host OS에서의 실제 프로세스 ID
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host OS 입장에서는 PID 605번 프로세스다. 컨테이너 내부에서는 namespace 덕분에 PID 1로 보인다. &lt;b&gt;격리된 것처럼 보일 뿐, 실제로는 Host OS 위에서 돌아가는 프로세스다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 핵심 명령어 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker run -d --name redis-test redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-d&lt;/td&gt;
&lt;td&gt;백그라운드 실행 (detached)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--name&lt;/td&gt;
&lt;td&gt;컨테이너 이름 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 명령어 정리&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 실행 중인 컨테이너 확인
docker ps

# 중지된 컨테이너 포함 전체 확인
docker ps -a

# 컨테이너 내부에서 명령어 실행
docker exec -it redis-test redis-cli ping
# PONG

# 컨테이너 로그 확인 (장애 시 제일 먼저 보는 명령어)
docker logs redis-test

# 컨테이너 상세 정보 확인
docker inspect redis-test

# 컨테이너 중지 / 삭제
docker stop redis-test
docker rm redis-test

# 이미지 목록 확인
docker images

# 이미지 삭제
docker rmi redis:7.2

# 이미지 다운로드 (레이어 단위로 내려받는 것 확인 가능)
docker pull redis:7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker pull 시 레이어 단위 다운로드 확인&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;docker pull redis:7.2

7.2: Pulling from library/redis
9b802525d298: Pull complete   &amp;larr; Layer 1
e1a1f94098c7: Pull complete   &amp;larr; Layer 2
c9c9059745d0: Pull complete   &amp;larr; Layer 3
9f0514948e1f: Pull complete   &amp;larr; Layer 4
d7d4fb1d90cf: Pull complete   &amp;larr; Layer 5
bc1b38586f4d: Pull complete   &amp;larr; Layer 6
4f4fb700ef54: Pull complete   &amp;larr; Layer 7
46ac7a0b9811: Pull complete   &amp;larr; Layer 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker history에서 봤던 8개 레이어가 그대로 하나씩 내려온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 연결&lt;/b&gt; 레이어가 이미 로컬에 있으면 Already exists로 건너뛴다. 여러 이미지가 같은 Base OS 레이어를 공유하면 중복 다운로드가 없다. 이것이 Docker가 디스크를 효율적으로 쓰는 원리다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 전체 흐름 요약&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Dockerfile
    │
    │ docker build
    ▼
Image (읽기 전용 레이어들)
    │
    │ docker run
    ▼
Container
    ├── 읽기 전용 레이어 (Image 그대로)
    └── 쓰기 가능 레이어 (컨테이너 삭제 시 소멸)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;namespace&lt;/td&gt;
&lt;td&gt;컨테이너별 독립 공간 제공 (프로세스/네트워크/파일시스템)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cgroup&lt;/td&gt;
&lt;td&gt;컨테이너별 리소스 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;overlayfs&lt;/td&gt;
&lt;td&gt;여러 레이어를 하나의 파일시스템으로 합침&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쓰기 가능 레이어&lt;/td&gt;
&lt;td&gt;컨테이너 실행 시 추가. 삭제 시 소멸 &amp;rarr; 데이터 날아가는 이유&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;➡️ 다음 파트 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chapter 1에서는 남이 만든 이미지(redis:7.2)의 구조를 분석했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chapter 2에서는 &lt;b&gt;내 앱(Spring Boot)을 직접 이미지로 만드는 법&lt;/b&gt;을 다룬다. Dockerfile 문법, 레이어 캐시 전략, Multi-stage Build로 이미지 크기를 줄이는 실습까지 진행한다.&lt;/p&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>cgroup</category>
      <category>docker</category>
      <category>docker image</category>
      <category>Docker 명령어</category>
      <category>dockerfile</category>
      <category>namespace</category>
      <category>컨테이너</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/103</guid>
      <comments>https://accept.tistory.com/103#entry103comment</comments>
      <pubDate>Thu, 23 Apr 2026 16:17:07 +0900</pubDate>
    </item>
    <item>
      <title>[Redis HA | Part 1] Redis Master-Replica + Sentinel 구성</title>
      <link>https://accept.tistory.com/102</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;환경 준비&lt;/li&gt;
&lt;li&gt;Docker 네트워크 구성&lt;/li&gt;
&lt;li&gt;Redis Master-Replica 구성&lt;/li&gt;
&lt;li&gt;Redis Sentinel 구성&lt;/li&gt;
&lt;li&gt;Failover 실습&lt;/li&gt;
&lt;li&gt;장애 복구 확인&lt;/li&gt;
&lt;li&gt;전체 HA 흐름 요약&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 환경 준비&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;docker --version
docker compose version
docker info | grep -E &quot;Memory|CPUs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker 네트워크 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 네트워크 생성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network create redis-cluster-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 확인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network ls
docker network inspect redis-cluster-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 네트워크 안의 컨테이너끼리는 &lt;b&gt;컨테이너 이름으로 DNS 통신&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;AWS VPC 안의 EC2 인스턴스끼리 통신하는 원리와 동일&lt;/li&gt;
&lt;li&gt;외부(Mac)에서 컨테이너 내부 접근 시 &amp;rarr; 같은 네트워크의 컨테이너를 &lt;b&gt;Bastion Host처럼 경유&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Redis Master-Replica 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;redis-cluster-net
├── redis-master    (6379) - 읽기/쓰기
├── redis-replica-1 (6379) - 읽기전용 + Master 백업
└── redis-replica-2 (6379) - 읽기전용 + Master 백업
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Master 실행&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;docker run -d \
  --name redis-master \
  --network redis-cluster-net \
  redis:7.2 \
  redis-server --appendonly yes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replica 실행&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# Replica-1
docker run -d \
  --name redis-replica-1 \
  --network redis-cluster-net \
  redis:7.2 \
  redis-server --replicaof redis-master 6379 --appendonly yes

# Replica-2
docker run -d \
  --name redis-replica-2 \
  --network redis-cluster-net \
  redis:7.2 \
  redis-server --replicaof redis-master 6379 --appendonly yes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker exec -it redis-master redis-cli info replication
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 동작 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# Master에 데이터 쓰기
docker exec -it redis-master redis-cli set mykey &quot;hello-replication&quot;

# Replica에서 읽기 (자동 복제 확인)
docker exec -it redis-replica-1 redis-cli get mykey
docker exec -it redis-replica-2 redis-cli get mykey

# Replica에 쓰기 시도 (읽기전용 확인)
docker exec -it redis-replica-1 redis-cli set testkey &quot;write-to-replica&quot;
# (error) READONLY You can't write against a read only replica.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 모니터링 지표 (info replication)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 설명 실무 알람 기준&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;connected_slaves&lt;/td&gt;
&lt;td&gt;연결된 Replica 수&lt;/td&gt;
&lt;td&gt;2 미만 시 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lag&lt;/td&gt;
&lt;td&gt;복제 지연&lt;/td&gt;
&lt;td&gt;10 이상 시 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repl_offset&lt;/td&gt;
&lt;td&gt;복제 위치&lt;/td&gt;
&lt;td&gt;Master와 차이 벌어지면 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;master_replid&lt;/td&gt;
&lt;td&gt;Master 고유 ID&lt;/td&gt;
&lt;td&gt;변경 시 전체 재동기화 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부분 동기화 vs 전체 재동기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 backlog replid 동기화 방식&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;네트워크 순단&lt;/td&gt;
&lt;td&gt;살아있음&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;부분 동기화 ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로세스 재시작 (정상종료)&lt;/td&gt;
&lt;td&gt;살아있음&lt;/td&gt;
&lt;td&gt;유지 가능&lt;/td&gt;
&lt;td&gt;부분 동기화 ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨테이너/서버 재시작&lt;/td&gt;
&lt;td&gt;소멸&lt;/td&gt;
&lt;td&gt;새로 발급&lt;/td&gt;
&lt;td&gt;전체 재동기화 ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 크래시&lt;/td&gt;
&lt;td&gt;소멸&lt;/td&gt;
&lt;td&gt;새로 발급&lt;/td&gt;
&lt;td&gt;전체 재동기화 ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 재시작 절차 (Master)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 1. Replica 동기화 상태 확인 (lag=0 확인)
redis-cli info replication

# 2. 반드시 정상 종료 (replid 보존)
redis-cli shutdown save   # save: RDB 스냅샷 저장 후 종료
                          # shutdown nosave 절대 금지

# 3. 재시작 후 Replica 재연결 확인
redis-cli info replication
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Master-Replica 한계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master 장애 시 &lt;b&gt;쓰기 불가&lt;/b&gt; (READONLY)&lt;/li&gt;
&lt;li&gt;수동으로 Replica를 Master로 승격시켜야 함&lt;/li&gt;
&lt;li&gt;그 사이 서비스 장애 지속 &amp;rarr; &lt;b&gt;Sentinel로 자동화 필요&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Redis Sentinel 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;redis-cluster-net
├── redis-master      (6379)
├── redis-replica-1   (6379)
├── redis-replica-2   (6379)
├── sentinel-1        (26379) - 감시 전용
├── sentinel-2        (26379) - 감시 전용
└── sentinel-3        (26379) - 감시 전용
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sentinel을 홀수(3대)로 운영하는 이유&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Sentinel 1대 &amp;rarr; 자체가 SPOF (단일장애점)
Sentinel 2대 &amp;rarr; 1대 죽으면 과반수 확보 불가 (50%)
Sentinel 3대 &amp;rarr; 1대 죽어도 2대가 과반수 (2/3) ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sentinel.conf 작성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mkdir -p ~/redis-ha/sentinel
cd ~/redis-ha/sentinel

cat &amp;gt; sentinel.conf &amp;lt;&amp;lt; 'EOF'
port 26379
sentinel monitor mymaster &amp;lt;MASTER_IP&amp;gt; 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Master IP 확인 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker inspect redis-master | grep IPAddress
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sentinel.conf 주요 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 값 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;port&lt;/td&gt;
&lt;td&gt;26379&lt;/td&gt;
&lt;td&gt;Sentinel 전용 포트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sentinel monitor&lt;/td&gt;
&lt;td&gt;quorum 2&lt;/td&gt;
&lt;td&gt;3대 중 2대 동의 시 failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;down-after-milliseconds&lt;/td&gt;
&lt;td&gt;5000ms&lt;/td&gt;
&lt;td&gt;5초 응답 없으면 장애 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failover-timeout&lt;/td&gt;
&lt;td&gt;60000ms&lt;/td&gt;
&lt;td&gt;60초 내 failover 완료 안되면 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parallel-syncs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;failover 후 1대씩 순차 동기화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sentinel 3대 실행&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;for i in 1 2 3; do
  docker run -d \
    --name sentinel-$i \
    --network redis-cluster-net \
    -v ~/redis-ha/sentinel/sentinel.conf:/etc/redis/sentinel.conf \
    redis:7.2 \
    redis-sentinel /etc/redis/sentinel.conf
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sentinel 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# Master 감시 상태
docker exec -it sentinel-1 redis-cli -p 26379 sentinel masters

# Replica 목록 확인
docker exec -it sentinel-1 redis-cli -p 26379 sentinel replicas mymaster
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 확인 항목&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&quot;flags&quot; &quot;master&quot;         &amp;rarr; 정상 (장애 시 s_down, o_down)
&quot;num-slaves&quot; &quot;2&quot;         &amp;rarr; Replica 2대 인식
&quot;num-other-sentinels&quot; &quot;2&quot;&amp;rarr; 다른 Sentinel 2대 인식 (총 3대)
&quot;quorum&quot; &quot;2&quot;             &amp;rarr; failover 과반수 기준
&quot;master-link-status&quot; &quot;ok&quot;&amp;rarr; Master 연결 정상
&quot;slave-priority&quot; &quot;100&quot;   &amp;rarr; 승격 우선순위 (0이면 승격 제외)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Failover 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실시간 모니터링 (터미널 A)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker logs -f sentinel-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Master 강제 종료 (터미널 B)&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker stop redis-master
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Failover 로그 타임라인&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;+sdown master mymaster &amp;lt;IP&amp;gt; 6379
&amp;rarr; Sentinel 1대가 Master 장애 감지 (주관적 판단)

+new-epoch 1
&amp;rarr; 새로운 failover 라운드 시작

+vote-for-leader &amp;lt;sentinel_id&amp;gt; 1
&amp;rarr; Sentinel끼리 failover 리더 투표

+odown master mymaster &amp;lt;IP&amp;gt; 6379 #quorum 2/2
&amp;rarr; 과반수(2/2) 동의, 공식 장애 확정

+switch-master mymaster &amp;lt;구Master_IP&amp;gt; 6379 &amp;lt;새Master_IP&amp;gt; 6379
&amp;rarr; 새 Master 승격 완료 (약 1초 내)

+slave slave &amp;lt;구Master_IP&amp;gt;:6379 @ mymaster &amp;lt;새Master_IP&amp;gt; 6379
&amp;rarr; 구 Master, 복구 시 Replica로 편입 예정
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Failover 후 쓰기 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 새 Master에 쓰기 (정상 동작 확인)
docker exec -it redis-replica-1 redis-cli set afterfailover &quot;I am new master&quot;
docker exec -it redis-replica-1 redis-cli get afterfailover
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 장애 복구 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구 Master 재시작&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker start redis-master
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구 후 상태 확인 (30초 후)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;docker exec -it redis-replica-1 redis-cli info replication
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구 결과&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;role: master              &amp;rarr; 새 Master 유지
connected_slaves: 2       &amp;rarr; 구 Master가 Replica로 자동 편입 ✅
master_replid2: &amp;lt;구ID&amp;gt;    &amp;rarr; 구 Master ID 기억 (부분 동기화 활용)
second_repl_offset: &amp;lt;값&amp;gt;  &amp;rarr; 구 Master였을 때 마지막 offset
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size23&quot;&gt;기타 컨테이너 관리 명령어&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 실행 중인 컨테이너 목록
docker ps

# 전체 컨테이너 IP 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

# 네트워크 기준 전체 확인
docker network inspect redis-cluster-net

# 중지된 컨테이너 포함 전체 확인
docker ps -a

# 컨테이너 정리
docker rm -f &amp;lt;컨테이너명&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 전체 HA 흐름 요약&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;① 정상 운영
   Master &amp;larr; 쓰기/읽기
   Replica x2 &amp;larr; 읽기 + 실시간 복제
   Sentinel x3 &amp;larr; 주기적 헬스체크

② Master 장애 발생
   Sentinel이 5초 내 감지 (s_down)
   Sentinel 3대가 투표 (quorum 2/2 동의)
   Replica 중 1대를 Master로 자동 승격 (1초 내)

③ 자동 복구
   구 Master 재시작 시 자동으로 새 Master의 Replica로 편입
   전체 과정에서 사람이 개입할 필요 없음&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HA 구성 방식 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성 Failover 샤딩 수평확장 적합한 규모&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Master-Replica&lt;/td&gt;
&lt;td&gt;수동&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;개발/스테이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentinel&lt;/td&gt;
&lt;td&gt;자동 ✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;중소규모&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis Cluster&lt;/td&gt;
&lt;td&gt;자동 ✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;중대형 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElastiCache&lt;/td&gt;
&lt;td&gt;자동 ✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;AWS 클라우드&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Infra &amp;amp; Network</category>
      <category>devops</category>
      <category>docker</category>
      <category>Failover</category>
      <category>Ha</category>
      <category>MasterReplica</category>
      <category>redis</category>
      <category>replication</category>
      <category>sentinel</category>
      <category>고가용성</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/102</guid>
      <comments>https://accept.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 22 Apr 2026 14:13:26 +0900</pubDate>
    </item>
    <item>
      <title>git 실무 활용 명령어 모음</title>
      <link>https://accept.tistory.com/101</link>
      <description>&lt;table style=&quot;background-color: #1e1e1e; color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;(Mac 기준) brew install gh&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;gh auth login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;새 레포&lt;/td&gt;
&lt;td&gt;git init&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git add .&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git commit&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;gh repo create&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일상 관리&lt;/td&gt;
&lt;td&gt;git add .&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git commit -m &quot;...&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;브랜치&lt;/td&gt;
&lt;td&gt;git checkout -b 브랜치명&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 작업 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git push origin 브랜치명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;되돌리기&lt;/td&gt;
&lt;td&gt;git restore&lt;span&gt;&amp;nbsp;&lt;/span&gt;(변경 취소) /&lt;span&gt;&amp;nbsp;&lt;/span&gt;git revert&lt;span&gt;&amp;nbsp;&lt;/span&gt;(커밋 취소)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초기 설정 &amp;rarr; 프로젝트 &amp;rarr; 레포지토리 생성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Git 전역 설정 (최초 1회)
git config --global user.name &quot;이름&quot;
git config --global user.email &quot;이메일&quot;

# GitHub CLI 설치 및 인증
brew install gh
gh auth login

# 프로젝트 폴더 생성 및 이동
mkdir my-project
cd my-project

# Git 초기화
git init

# 파일 생성 (예시)
touch README.md

# 첫 커밋
git add .
git commit -m &quot;first commit&quot;

# GitHub 레포지토리 생성 + 연결 + push
gh repo create my-project --public --source=. --push
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 추가한 파일 반영&lt;/h2&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 새 파일 확인
git status

# 특정 파일만 추가
git add 파일명

# 전체 추가
git add .

# 커밋
git commit -m &quot;feat: 파일 추가&quot;

# push
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 수정한 파일 반영&lt;/h2&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 변경 내용 확인
git diff

# 변경된 파일 확인
git status

# 스테이징
git add 파일명   # 특정 파일
git add .        # 전체

# 커밋
git commit -m &quot;feat: 파일 수정&quot;

# push
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 변경사항 되돌리기&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# ========== 커밋 전 ==========
# 수정사항 취소 (add 전)
git restore 파일명      # 특정 파일
git restore .          # 전체

# add 취소
git restore --staged 파일명

# ========== 커밋 후 push 전 ==========
# 커밋 삭제 (혼자 쓰는 브랜치에서만)
git reset --hard HEAD~1

# ========== push 후 ==========
# 되돌리는 새 커밋 생성 (이력 보존 - 권장)
git revert HEAD
git push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. 이력 조회&lt;/h2&gt;
&lt;pre id=&quot;code_1776758831652&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기본 - 전체 이력
git log

# 한줄로 깔끔하게 (실무에서 제일 자주 씀)
git log --oneline

# 브랜치 흐름까지 시각적으로
git log --oneline --graph

# 특정 파일의 이력만
git log --oneline 파일명

# 최근 n개만
git log --oneline -5&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Etc.</category>
      <category>git</category>
      <category>github</category>
      <author>hrbds</author>
      <guid isPermaLink="true">https://accept.tistory.com/101</guid>
      <comments>https://accept.tistory.com/101#entry101comment</comments>
      <pubDate>Tue, 21 Apr 2026 16:31:14 +0900</pubDate>
    </item>
  </channel>
</rss>