필터#k8s×

[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성

쿠버네티스 클러스터를 운영할 때 마스터 노드(컨트롤 플레인)가 1대뿐이라면, 그 노드가 죽는 순간 클러스터 전체를 제어할 수 없게 된다. 이런 단일 장애점(SPOF)을 없애기 위해 마스터 노드를 여러 대로 구성하고, 그 앞단에 로드밸런서를 두는 것이 일반적인 고가용성(HA) 구성이다. 이번 글에서는 HAproxy를 이용해서 마스터 노드 3대를 삼중화하는 과정을 정리해본다. 고가용성(HA) 구성의 원리 마스터 노드를 여러 대로 구성한다고 해서 모든 노드가 동시에 요청을 처리하는 건 아니다. 보통 3개의 마스터 노드가 있다면 그중 한 개만 활성화(Active) 상태이고, 나머지는 Standby 상태로 대기한다. 그러다가 Active 상태였던 노드에 문제가 생기면, 대기 중이던 다른 노드 하나가 새로운 Active로 전환되면서 서비스를 계속 유지하는 방식이다. 노드 개수는 왜 홀수여야 할까? 새로운 Active 마스터 노드를 선출하려면 마스터 노드의 개수를 반드시 홀수 개(3, 5, 7, 9...) 로 구성해야 한다. 이는 네트워크 분할(Network Partition) 같은 상황에서 클러스터가 과반수 이상의 동의를 얻어 의사결정 권한을 명확히 하기 위함이다. 짝수 개로 구성하면 표가 정확히 반으로 갈리는 상황(Splitbrain)이 발생할 수 있어서, 어느 쪽이 정상 다수인지 판단할 수 없게 된다. 그럼 여기서 말하는 "의사결정"이란 정확히 뭘까? 조금 더 구체적으로 들어가보자. 쿠버네티스의 모든 상태 정보 — 어떤 파드가 떠 있는지, 어떤 노드가 살아있는지, 디플로이먼트 설정이 뭔지 — 는 각 마스터 노드 안에 있는 etcd라는 저장소에 기록된다. kubectl apply로 뭔가를 배포하든, 노드가 하트비트를 보내든, 컨트롤러가 파드 개수를 조정하든, 결국은 전부 "etcd에 이 상태 변경을 기록해달라"는 요청으로 이어진다. 마스터가 3대면 etcd도 3벌이 각자 떠서 서로 동기화하고 있는데, 한 대에만 기록하고 끝내버리면 나머지 두 대는 그 사실을 모르는 채로 남게 된다. 그래서 etcd는 Raft라는 분산 합의 알고리즘을 사용해서, 상태 변경 요청이 들어올 때마다 "이 변경을 클러스터의 공식 기록으로 확정해도 되는가"를 매번 투표로 결정한다. 리더 역할을 맡은 etcd가 나머지 멤버들에게 "이거 기록해도 될까?"라고 묻고, 과반수가 "나도 기록했다"고 응답해야 그 변경이 확정(commit)된다. 과반수 동의를 못 받으면 그 요청은 보류되고, 클러스터 입장에서는 새로운 배포도, 파드 상태 갱신도 반영되지 않는 상태가 된다. 이 투표를 진행할 리더가 누구인지를 정하는 것도 같은 방식의 의사결정이다. 리더가 죽으면 남은 멤버들끼리 다시 투표해서 새 리더를 뽑는데, 이 재선출 과정 동안 잠깐 요청이 안 먹히는 시간이 생긴다. 뒤에서 소개할 장애 테스트에서 kubectl 명령이 한순간 타임아웃 나는 것도 바로 이 재선출 과정 때문이다. 쿼럼(quorum) 계산 — 몇 대까지 죽어도 괜찮을까? 마스터가 3대일 때 과반수(쿼럼)는 2표다. 즉 3대 중 2대만 살아있어도 투표는 성립하고, 클러스터는 정상적으로 의사결정을 계속할 수 있다. 반대로 3대 중 2대가 죽어서 1대만 남으면, 1표로는 과반수(2표)에 못 미치기 때문에 그때는 진짜로 쿼럼이 깨지고 클러스터가 쓰기 작업을 처리하지 못하는 상태에 빠진다. 정리하면 3대 구성에서는 딱 1대까지만 장애를 허용할 수 있는 셈이다. 그렇다면 "짝수인 4대로 구성하면 더 안전하지 않을까?" 하는 생각이 들 수 있는데, 계산해보면 그렇지 않다. 4대 구성의 과반수는 3표이므로, 4대 중 2대가 죽으면 역시 쿼럼이 깨진다. 결국 4대든 3대든 버틸 수 있는 장애 대수는 똑같이 1대뿐이고, 4대는 서버 한 대를 더 쓰면서도 내결함성은 그대로인 비효율적인 구성이 된다. 같은 수준의 내결함성을 얻는 데 필요한 최소 노드 수를 구하면 자연스럽게 홀수(3, 5, 7...)가 나오기 때문에, 마스터 노드는 관례적으로 홀수 개로 구성하는 것이다. 실습 아키텍처 이번 실습에서는 아래와 같이 구성한다. 마스터 노드 3개 (m1, m2, m3) — 각각 etcd를 포함한 컨트롤 플레인 역할 HAproxy 서버 1개 — 마스터 노드 앞단에서 로드밸런싱 + 리버스 프록시 역할 kubectl 명령은 HAproxy 서버의 가상 IP(VIP)를 향해 요청을 보내고, HAproxy가 이 요청을 뒤쪽의 m1 / m2 / m3 중 살아있는 노드로 분산해주는 구조다. 1. HAproxy 서버 설정 먼저 HAproxy 역할을 할 서버에 HAproxy를 설치한다. bash apt update y apt install y haproxy 설정 파일을 연다. bash vi /etc/haproxy/haproxy.cfg 기존 내용은 전부 지우고, 아래 내용으로 채워준다. frontend kubernetesmasterlb bind 0.0.0.0:6443 option tcplog mode tcp defaultbackend kubernetesmasternodes backend kubernetesmasternodes mode tcp balance roundrobin option tcpcheck option tcplog server k8smaster1 211.183.3.101:6443 check server k8smaster2 211.183.3.102:6443 check server k8smaster3 211.183.3.103:6443 check frontend: 클라이언트(kubectl 등)의 요청을 6443 포트로 받는다. 쿠버네티스 API 서버의 기본 포트가 6443이기 때문이다. backend: 실제 요청을 전달할 대상, 즉 마스터 노드 3대의 IP와 포트를 나열한다. balance roundrobin으로 요청을 순서대로 분산하고, option tcpcheck로 각 서버가 살아있는지 주기적으로 확인한다. 마스터 노드 3대를 위 IP들을 갖도록 구성해두면 된다. 삼중화가 잘 구성되어 있다면, 특정 Active 마스터 노드에 문제가 생기더라도 Standby 상태였던 마스터 노드가 자동으로 Active로 선출되어 서비스가 유지된다. 설정을 저장했으면 HAproxy를 재시작하고 부팅 시 자동 실행되도록 등록한다. bash systemctl restart haproxy systemctl enable haproxy 참고: 이 시점에는 아직 m1 m3 마스터 노드들이 준비되지 않았기 때문에, HAproxy 입장에서는 백엔드 서버들이 응답하지 않아 에러 로그가 뜰 수 있다. 마스터 노드들을 구성한 뒤 다시 확인하면 된다. 2. 첫 번째 마스터 노드(m1) 초기화 이제 m1 서버에서 클러스터를 초기화한다. 이때 핵심은 controlplaneendpoint에 HAproxy 서버의 IP를 지정해주는 것이다. 이렇게 해야 클러스터의 모든 구성원이 HAproxy를 거쳐서 API 서버에 접근하게 된다. bash kubeadm init podnetworkcidr=10.244.0.0/16 uploadcerts kubernetesversion=v1.30.3 \ ignorepreflighterrors=all \ controlplaneendpoint=211.183.3.50 초기화가 끝나면 kubectl을 사용할 수 있도록 kubeconfig를 설정해준다. bash mkdir p $HOME/.kube sudo cp i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id u):$(id g) $HOME/.kube/config 3. m2, m3를 컨트롤 플레인으로 조인 kubeadm init이 끝나면 콘솔에 조인용 명령어가 출력된다. 이 중 컨트롤 플레인 노드용 조인 명령을 복사해서 m2, m3 서버에 각각 붙여넣어 실행하면 된다. (워커 노드용 조인 명령과는 다른 명령이므로 헷갈리지 않도록 주의한다.) m2, m3에 컨트롤 플레인 조인 명령을 복붙해주면 마스터 노드 삼중화 구성이 완료된다. 4. HAproxy 서버에서 kubectl 사용하기 HAproxy 서버에서도 클러스터를 직접 관리할 수 있으면 편하다. 이를 위해서는 두 가지가 필요하다. 1. kubectl 명령이 설치되어 있어야 한다. 2. 해당 클러스터의 kubeconfig 파일(m1의 /etc/kubernetes/admin.conf에 존재)을 가져와야 한다. kubectl 설치 bash aptget install y apttransporthttps cacertificates curl mkdir p /etc/apt/keyrings curl fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key sudo gpg dearmor o /etc/apt/keyrings/kubernetesaptkeyring.gpg echo 'deb [signedby=/etc/apt/keyrings/kubernetesaptkeyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' sudo tee /etc/apt/sources.list.d/kubernetes.list aptget update aptget install y kubectl aptmark hold kubectl kubeconfig 가져오기 m1에서 admin.conf 내용을 출력해서 복사한다. bash 에서 cat /etc/kubernetes/admin.conf 내용 복사 haproxy 서버에서 kube 설정 디렉터리를 만들고, 복사한 내용을 붙여넣는다. bash 에서 mkdir /.kube vi /.kube/config 파일 생성 후 m1에서 복사한 내용을 붙여넣기 이제 HAproxy 서버에서 kubectl get nodes를 실행하면 m1, m2, m3 세 노드가 모두 정상적으로 조회되는 것을 확인할 수 있다. 5. 장애 상황 테스트 — Active 노드 다운 삼중화가 실제로 잘 동작하는지 확인해보자. m1 서버(현재 Active 역할)를 강제로 중지시킨 뒤, HAproxy 서버에서 다시 kubectl 명령을 날려본다. 바로 직후에는 아래처럼 명령이 먹히지 않는 순간이 발생한다. root@haproxy: kubectl get nodes Unable to connect to the server: net/http: TLS handshake timeout HAproxy가 죽은 노드를 헬스체크로 감지하고 백엔드에서 제외하기까지는 약간의 시간이 걸리기 때문에 나타나는 현상이다. 조금 뒤 watch 명령으로 다시 확인해보면, m1이 NotReady 상태로 바뀌고 나머지 m2, m3는 여전히 Ready 상태로 정상 동작하고 있는 것을 확인할 수 있다. bash watch kubectl get nodes 즉, Active였던 m1이 죽어도 나머지 마스터 노드(m2, m3)가 살아있는 한 클러스터 제어 자체는 계속 정상적으로 이루어진다는 걸 실습으로 확인할 수 있었다. 결국 HAproxy를 이용한 마스터 노드 삼중화의 핵심은 "요청을 받는 창구(HAproxy)는 하나로 통일하되, 실제로 그 요청을 처리하는 마스터는 여러 대를 두어 하나가 죽어도 나머지가 대신 응답하게 만드는 것"이다. 단일 장애점을 없애고 안정적으로 클러스터를 운영하려면 꼭 필요한 구성이라는 걸 다시 한번 체감했다.

Sep 08, 2026kubernetes
[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성

[Kubernetes] HPA(HorizontalPodAutoscaler) 개념 정리 및 실습

쿠버네티스로 서비스를 운영하다 보면 트래픽이 갑자기 몰리는 순간이 반드시 온다. 이럴 때마다 사람이 직접 파드 개수를 늘렸다 줄였다 하는 건 사실상 불가능하다. 이번 글에서는 이 문제를 자동으로 해결해주는 쿠버네티스 리소스인 HPA(Horizontal Pod Autoscaler) 를 개념부터 실습까지 정리해본다. HPA란? HPA는 CPU, 메모리 사용량 혹은 사용자 정의 메트릭(Custom Metrics)의 부하에 따라 파드의 개수를 자동으로 늘리거나 줄여주는(Scaling) 쿠버네티스 리소스다. 트래픽이 갑자기 늘어나면 파드를 추가로 띄워서 대응하고 (Scaleout) 트래픽이 줄어들면 불필요한 파드를 줄여서 자원 낭비를 막는다 (Scalein) 즉, 사람이 개입하지 않아도 부하에 맞춰 자원을 효율적으로 관리해주는 것이 HPA의 목적이다. Vertical Scaling vs Horizontal Scaling 오토스케일링은 크게 두 가지 방향으로 나눌 수 있다. Vertical (Scale up/down): 파드 개수는 그대로 두고, 파드 하나가 쓸 수 있는 자원(CPU, 메모리)을 늘리는 방식 Horizontal (Scale out/in): 파드 자체의 개수를 늘리거나 줄이는 방식 HPA는 이름 그대로 후자, Horizontal 방식이다. Vertical 방식은 파드를 재시작해야 하는 등 비활성화 구간이 생기기 쉬운 반면, Horizontal 방식은 파드를 추가/제거하는 방식이라 무중단으로 대응하기 좋다는 장점이 있다. 다만 DB처럼 상태를 갖는(stateful) 워크로드는 여러 개로 늘려도 유의미하지 않은 경우가 많아서, 상태 없는(stateless) 서비스에 주로 적합하다. 실습 환경 준비 먼저 실습용 디렉터리를 하나 만들어준다. bash mkdir hpa cd hpa 1. 리소스를 제한하는 파드 생성 HPA가 CPU 사용률을 기준으로 스케일링 여부를 판단하려면, 먼저 파드에 CPU/메모리 자원을 얼마나 할당할지(requests)와 최대 얼마까지 쓸 수 있는지(limits)를 정의해줘야 한다. bash vi res.yml yaml spec: containers: name: nginx image: nginx:latest resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" requests: 컨테이너 생성 시 최소한 보장받는 자원. 스케줄러가 파드를 배치할 노드를 고를 때 이 값을 기준으로 판단한다. limits: 컨테이너가 최대로 사용할 수 있는 자원. 이 값을 넘기면 제한이 걸린다. 이렇게 파드에 자원을 할당해두면, 파드에 부여된 리소스(CPU, RAM) 사용률이 특정 임계값 이상이거나 이하일 때 파드 개수를 자동으로 늘리거나 줄일 수 있게 된다. 다만 이걸 하려면 대상 파드의 자원 사용량을 실시간으로 모니터링해줄 메트릭 서버가 반드시 필요하다. 2. 메트릭 서버(Metrics Server) 설치 HPA는 파드의 CPU/메모리 사용량을 직접 측정하지 않는다. 클러스터 내에 별도로 떠 있는 메트릭 서버로부터 이 값을 가져와서 판단한다. 그래서 HPA를 쓰기 전에 메트릭 서버부터 설치해야 한다. 메트릭서버 매니페스트는 아래 깃허브에서 받을 수 있다. https://github.com/kubernetessigs/metricsserver bash curl Ls https://github.com/kubernetessigs/metricsserver/releases/latest/download/components.yaml o metric.yml 다운로드한 매니페스트를 열어서 몇 가지를 수정해줘야 한다. bash vi metric.yml 수정이 필요한 부분은 containers.args 쪽이다. yaml spec: containers: args: kubeletinsecuretls 추가 certdir=/tmp secureport=10250 kubeletpreferredaddresstypes=InternalIP 수정 kubeletusenodestatusport metricresolution=15s kubeletinsecuretls 옵션을 추가해서 kubelet과의 통신 시 TLS 인증서 검증을 생략하도록 한다. (사설/테스트 환경에서 인증서 문제로 메트릭 서버가 뜨지 않는 걸 방지하기 위함) kubeletpreferredaddresstypes 값에서 ,ExternalIP,Hostname을 삭제하고 InternalIP만 남겨서, 노드의 내부 IP로만 kubelet에 접근하도록 해준다. 수정이 끝났으면 클러스터에 적용한다. bash kubectl apply f metric.yml 3. 테스트용 Deployment / Service 배포 HPA로 스케일링을 테스트해볼 대상이 필요하다. nginx 기반의 간단한 Deployment와 Service를 하나 만들어준다. bash vi dep.yml yaml apiVersion: v1 kind: Service metadata: name: mysvc spec: selector: app: myweb ports: port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 3 selector: matchLabels: app: myweb template: metadata: labels: app: myweb spec: containers: image: 61.254.18.30:5000/ipnginx name: mywebcon resources: requests: cpu: "10m" limits: cpu: "20m" CPU 사용률을 기준으로 스케일링을 확인할 것이기 때문에, 일부러 requests.cpu, limits.cpu 값을 아주 작게(10m, 20m) 잡았다. 이렇게 해야 약간의 부하만 줘도 사용률(%)이 쉽게 임계치를 넘어서 스케일링 동작을 눈으로 빠르게 확인할 수 있다. bash kubectl apply f dep.yml 배포 후 kubectl top pod로 각 파드의 실제 자원 사용량을 확인할 수 있다. 4. HPA 생성하기 — 명령어로 바로 생성 가장 간단하게는 kubectl autoscale 명령어로 바로 HPA를 만들 수 있다. bash kubectl autoscale deploy mydep min=1 max=5 cpupercent=50 CPU 사용량 임계치를 50%로 설정해서, 최소 1개 최대 5개 사이에서 파드 개수를 자동으로 조절하겠다는 의미다. 5. HPA를 매니페스트로 작성하기 위에서 명령어 한 줄로 만든 것과 동일한 설정을 매니페스트(YAML) 파일로도 작성할 수 있다. 실제 운영 환경에서는 이렇게 선언적으로 관리하는 걸 선호한다. bash vi as.yml yaml apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: hpanginx spec: maxReplicas: 5 minReplicas: 1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: mydep 오토스케일링 대상 Deployment 이름 명시 targetCPUUtilizationPercentage: 50 scaleTargetRef에 스케일링 대상이 되는 Deployment를 지정해주고, targetCPUUtilizationPercentage로 임계치를, minReplicas/maxReplicas로 파드 개수의 하한선과 상한선을 정해주는 구조다. 6. 부하를 줘서 스케일 아웃 확인하기 이제 실제로 트래픽을 흉내 내서 HPA가 정말 동작하는지 확인해볼 차례다. 아래 명령어로 0.1초 간격으로 서비스에 계속 curl 요청을 날려서 부하를 준다. bash i=1; while true; do sleep 0.001; echo $((i++)) curl s ; done 여기서 IP는 kubectl get svc로 확인한 대상 서비스(예: mysvc)의 ClusterIP를 넣어주면 된다. 7. 결과 확인 부하를 주는 동안 다른 터미널에서 HPA와 파드 상태를 실시간으로 지켜보면 스케일 아웃이 일어나는 걸 확인할 수 있다. bash kubectl get hpa 현재 오토스케일링 설정이 어떻게 되어 있는지, 그리고 CPU 사용률이 임계치 대비 몇 %인지 확인할 수 있다. bash kubectl top pod watch 명령어와 함께 사용하면 파드 개수가 늘어나는 과정을 실시간으로 관찰할 수 있다. bash watch kubectl get hpa watch kubectl top pod 부하를 주기 시작하면 CPU 사용률이 임계치(50%)를 넘으면서, 처음 3개였던 파드가 최대 5개까지 늘어나는 것을 볼 수 있다. 8. 부하가 사라지면 다시 Scalein 반대로 부하 명령을 멈추면 어떻게 될까? CPU 사용률이 떨어지면서 HPA가 자동으로 파드 개수를 줄이기 시작한다. [이미지 삽입: 부하 종료 후 REPLICAS가 줄어드는 kubectl get hpa / kubectl top pod 캡처] 흥미로운 점은, 처음 Deployment 매니페스트에서 지정했던 replicas: 3과는 상관없이 HPA가 설정한 minReplicas 값인 1개까지 파드 수가 줄어든다는 것이다. HPA가 붙는 순간, 파드 개수의 기준은 Deployment의 replicas가 아니라 HPA의 minReplicas / maxReplicas가 된다는 걸 실습을 통해 확인할 수 있었다. 마무리 이번 실습으로 정리한 흐름을 요약하면 다음과 같다. 1. 파드에 requests/limits로 CPU·메모리 자원을 할당한다. 2. 메트릭 서버를 설치해서 파드의 실시간 자원 사용량을 수집할 수 있게 한다. 3. 스케일링 대상이 될 Deployment/Service를 배포한다. 4. kubectl autoscale 명령 또는 HPA 매니페스트로 오토스케일링 정책(최소/최대 개수, CPU 임계치)을 설정한다. 5. 부하를 줘서 Scaleout을, 부하를 멈춰서 Scalein을 눈으로 확인한다. HPA는 결국 "메트릭 서버가 수집한 지표"와 "내가 설정한 임계치"를 비교해서 파드 개수를 알아서 조절해주는 도구다. Deployment의 replicas는 초기값일 뿐, HPA가 붙는 순간부터는 HPA의 설정값이 우선한다는 점만 기억해두면 실무에서 헷갈릴 일이 없을 것 같다.

Sep 02, 2026kubernetes
[Kubernetes] HPA(HorizontalPodAutoscaler) 개념 정리 및 실습

[Kubernetes] Pod, 리소스, Ingress 정리

Pod란? 한 개 이상의 컨테이너로 구성된 쿠버네티스의 기본 단위 같은 Pod면 IP가 같다 같은 Pod 내 컨테이너들은 포트로 구분한다 Pod도 결국 컨테이너로 구성되어 있으며, 이 컨테이너를 실행하기 위해 docker, containerd, CRIO 같은 CRI(컨테이너 런타임)가 필요하다. 1\. kubeapiserver 내/외부의 모든 요청을 주고받는 서버. contoller나 scheduler,proxy의 주시대상. 호텔지배인. 2\. kubescheduler 생성될 리소스들을 어떤 노드에 '배치'할지 결정(스케쥴링) 호텔 로비 직원 3\. kubecontroller 다양한 리소스에 대한 여러가지 컨트롤러들이 존재한다. 원하는 상태(Desired state)에 현재 상태(current state)가 수렴하도록 지속적으로 모니터링. 문제가 생기면 고치거나 리소스를 재생성. ex) 하우스키퍼 4\. etcd 클러스터 및 모든 리소스에 대한 정보를 key:value 형태로 저장하는 일종의 데이터베이스. ex) 장부 모든 노드에 존재하는 컴포넌트 1\. kubelet 노드 관리자. 실질적으로 각 노드에 존재하는 리소스 관리 2\. kubeproxy 노드 안과 밖을 넘나드는 수직트래픽을 관리. 매니페스트 (yml 파일) 매니페스트란 내가 원하는 상태를 적어둔 명세서다. bash vi testpod.yml yml apiVersion: v1 kind: Pod metadata: name: test spec: containers: image: public.ecr.aws/docker/library/httpd:latest name: testcon bash kubectl apply f testpod.yml kubectl apply f는 docker compose up, docker stack deploy와 비슷하다. 내가 원하는 상태(Desired State)가 미리 정의된 매니페스트 파일을 구성해놓고 apply해서 반영한다. 매니페스트 수정 후 다시 apply하면 바로 변경사항을 반영시킬 수 있다. bash kubectl delete f testpod.yml 앞으로는 명령어로 직접 리소스를 생성하거나 지우지 말고 매니페스트 파일을 f 옵션을 통해 apply하거나 delete하도록 하자. label 중요 bash vi labels.yml yml apiVersion: v1 kind: Pod metadata: name: testlabelpod labels: app: myweb spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: testlabelcon label은 여러 개를 쓸 수 있다. bash kubectl apply f labels.yml 생성. bash kubectl describe pod testlabelpod 조회. label은 리소스를 컨트롤(원하는 상태, 현재 상태)하고 찾아가기 위한 용도다. 쿠버네티스의 다양한 리소스들 1\. pod 한 개 이상의 컨테이너로 구성된 쿠버네티스의 기본 배포 단위. 2\. replicaset Pod의 복제본 수를 유지해주는 리소스. 지정한 수만큼 Pod가 항상 실행되도록 보장한다. 3\. Deployment ReplicaSet을 관리하며 롤링 업데이트, 롤백 등을 지원하는 리소스. 실무에서 가장 많이 사용한다. 4\. namespace 클러스터 내에서 리소스를 논리적으로 분리하는 단위. 팀이나 프로젝트별로 격리된 환경을 만들 수 있다. 5\. Service (너무 중요!) 작은 로드밸런서라고도 할 수 있다. Pod에 접근하기 위한 고정된 엔드포인트를 제공하는 리소스이다. Pod는 재생성될 때마다 IP가 바뀌기 때문에 Service를 통해 안정적으로 접근한다. 서비스라는 리소스를 생성 시 하나의 접속지점이 생성된다. 이 서비스를 통해 모든 노드에 존재하는 pod에 트래픽을 인가할 수 있다. 인가하는 기준은 labels 를 통해 해당 pod를 특정하면 된다. 서비스는 다양한 종류(type)이 존재한다. 51. ClusterIP 타입 (svc의 default 타입) 클러스터 내부에서만 유효한 IP 내부 테스트 용도, 외부로 배포를 안하는 경우. 내부에 존재하는 서비스들끼리만 통신할 때. bash vi dep.yml yml apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 3 selector: matchLabels: app: myweb template: metadata: labels: app: myweb spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: mywebcon bash kubectl apply f dep.yml svc를 연결할 deployment 생성. bash vi depsvc.yml 서비스 매니페스트 정의. yml apiVersion: v1 kind: Service metadata: name: svcmyweb spec: selector: app: myweb ports: port: 80 targetPort: 80 bash kubectl apply f depsvc.yml svc 생성. bash kubectl describe svc svcmyweb 자세한 정보 확인. Endpoint에 뜨는 pod들은 건강한 pod만 뜬다. 52. NodePort 타입 노드의 포트 서비스를 제공받는 사용자 입장에서 내부로 진입하여 pod에 접근하려면 노드 포트로 진입해야 한다. bash mkdir svc cd svc vi svcnodeport.yml yml apiVersion: v1 kind: Service metadata: name: svcdep spec: selector: app: mydep type: NodePort ports: nodePort: 30001 port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 1 selector: matchLabels: app: mydep template: metadata: labels: app: mydep spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: depcon bash kubectl apply f svcnodeport.yml bash kubectl describe svc svcdep 3개의 포트(NodePort, Port, TargetPort)가 각각 어떤 대상인지 구분할 수 있어야 한다. Port 서비스 포트 TargetPort pod NodePort Node 외부에서 노드로 통신만 된다면 노드 포트를 통해 pod로 접속 가능하다. 어떤 노드로 들어가는지는 중요하지 않고, NodePort로 접근하면 동일한 공간(오버레이 네트워크가 구성된 podnetwork)으로 들어간다는 사실을 인지하자. pod가 어떤 노드에 존재하는지는 신경 쓸 필요도 없고 중요하지도 않다. pod가 worker1에 띄워져 있어도 worker2의 노드 포트를 통해 접근 가능하다. 53. LoadBalancer 타입 클러스터 관리자의 도움이 필요한 서비스 타입. 쿠버네티스 외부 네트워크 대역의 IP를 자동으로 할당하고 관리해 주는 사설 로드밸런서 관리자가 MetalLB다. EKS 같은 쿠버네티스 클러스터의 경우 클라우드 서비스 제공자(AWS)가 LB를 제공해줄 수 있지만, 온프레미스에 구성한 클러스터는 그렇지 않다. 따라서 svc를 LoadBalancer 타입으로 만들었을 때 누군가는 LB를 생성하면서 노드 대역대의 IP를 할당해줘야 한다. 그 기능을 활성화하기 위해 MetalLB가 필요하다. bash vi configmetal.yml LB가 생성됐을 때 뿌려줄 IP 범위 설정. yml apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: firstpool namespace: metallbsystem spec: addresses: 211.183.3.200211.183.3.240 안겹치게 수정. LB가 부여받을 IP 범위 apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: example namespace: metallbsystem bash kubectl apply f configmetal.yml vi lbtom.yml yml apiVersion: v1 kind: Service metadata: name: svctom spec: selector: app: tom type: LoadBalancer ports: port: 80 targetPort: 8080 apiVersion: apps/v1 kind: Deployment metadata: name: deptom spec: replicas: 1 selector: matchLabels: app: tom template: metadata: labels: app: tom spec: containers: image: public.ecr.aws/docker/library/tomcat:10.1.40jre11 name: tomcon svc의 타입으로 LoadBalancer 타입을 지정해준다. nodePort는 삭제한다. bash kubectl apply f lbtom.yml 생성된 LB는 svc의 포트를 따라간다. Ingress (제일 중요!) path 기반 라우팅 (/board로 가면 board 앱으로, /login으로 가면 login 앱으로) 일반적인 svc는 path 기반 라우팅이 불가능하다. (LoadBalancer, NodePort, ClusterIP) 서비스는 한 종류의 라벨만 품을 수 있기 때문에 이런 한계가 발생한다. 합칠 수 없다 = 같은 주소가 될 수 없다. 여러 개의 컨테이너에 각 기능들을 구현하면 path로 라우팅이 가능해야 한다. 일반적인 svc는 path 기반 라우팅이 불가능하기 때문에 Ingress라는 리소스가 필요하다. Ingress를 구성하면 path 기반 라우팅이 가능하다. ex) www.naver.com/board 로 오면 svcboard라는 svc로 보내줘. ex) www.naver.com/login 으로 오면 svclogin이라는 svc로 보내줘. → 하나의 접속지점(www.naver.com)을 통해 여러 개의 svc를 구성할 수 있다. Ingress를 구성하기 위해서는 Ingress Controller가 필요하다. (AWS EKS에서는 Ingress Controller를 LoadBalancer Controller라고 부른다.) Ingress Controller 설치 bash vi deploy.yaml 필요한 부분 수정. (type 을 NodePort에서 LoadBalancer로 수정한다.) bash kubectl apply f deploy.yaml kubectl get svc n ingressnginx Ingress 매니페스트 작성 bash cp ../svc/ip.yml . vi ip.yml yml apiVersion: v1 kind: Service metadata: name: svcipnginx spec: selector: app: myipnginx ports: port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: ipdep spec: replicas: 2 selector: matchLabels: app: myipnginx template: metadata: labels: app: myipnginx spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: ipcon bash kubectl apply f ip.yml kubectl describe svc svcipnginx 서비스가 정상인지 확인. bash vi ingip.yml yml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingip annotations: nginx.ingress.kubernetes.io/rewritetarget: / spec: ingressClassName: "nginx" rules: host: rapa.com http: paths: path: /httpd pathType: Prefix backend: service: name: svcipnginx port: number: 80 annotation : 추가적인 정보. labels와 비슷하지만 주로 부가적인 기능 명시. ingressClassName : ingresscontroller의 종류 중에 nginx 방식을 사용. rewritetarget: / → 비록 /httpd라는 경로로 들어왔더라도 실제 pod에서는 /라는 경로로 바꿔주는 기능. host : 영문주소. IP는 안됨. DNS 기능이 필요하다. 인증서가 있다면 https 통신도 가능하다. path : 한 종류의 앱. pathType: Prefix → /httpd로 접근하는 애들 전부. 서비스의 이름을 마치 주소처럼 사용하고 있다. bash kubectl apply f ingip.yml ingress를 describe 했을 때 endpoint들(pod들)이 잘 떠있는 것만 봐도 ingresssvcpod가 잘 연결되어 있는 걸 어느정도 확인할 수 있다. 원래는 DNS를 통해서 rapa.com에 해당하는 IP를 매핑시켜줘야 하지만, 미니 DNS인 /etc/hosts 파일을 사용하자. bash vi /etc/hosts Ingress 트래픽 흐름 쿠버네티스에서 외부 사용자가 도메인 이름(rapa.com)을 치고 들어와서 실제 앱(pod)까지 도달하는 ingress 트래픽 흐름. rapa.com → ingresscontroller의 svc ExternalIP(DNS) → ingress(path 기반 라우팅) → svc → pod

May 12, 2026kubernetes
[Kubernetes] Pod, 리소스, Ingress 정리

On-Premise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화

OnPremise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화 프로젝트 기간: 2026.04.27 – 2026.04.30 기술 스택: Kubernetes · Helm · ArgoCD · GitHub Actions · Amazon ECR · AWS · Tailscale 역할: 온프레미스 K8s 클러스터 세팅 / 하이브리드 네트워크 설계 / 초기 매니페스트 작성 / Helm Chart 고도화 / CI/CD 파이프라인 구축 목차 1. 프로젝트 선정 이유 2. 프로젝트 개요 3. 전체 아키텍처 4. 하이브리드 네트워크 설계 (Tailscale) 5. 온프레미스 K8s 클러스터 구성 6. 초기 매니페스트 작성 7. Helm Chart 고도화 8. ECR 프라이빗 레지스트리 연동 트러블슈팅 9. Pod → RDS 접근 트러블슈팅 10. ArgoCD 기반 CI/CD 파이프라인 구축 11. 배포 검증 결과 12. 회고 및 개선 방향 1. 프로젝트 선정 이유 AWS Cloud School 교육 과정에서 네트워크, 리눅스, AWS, Docker, Kubernetes를 순차적으로 학습한 뒤, "배운 기술을 실제 운영 환경에 가깝게 통합해보자" 는 목표로 이 프로젝트를 기획했습니다. 단순히 로컬에서 K8s를 돌리는 것이 아니라, 다음과 같은 실무에서 마주할 수 있는 제약 조건을 의도적으로 설정했습니다: 제약 조건 설명 이중 NAT 환경 학원 공유기 → VMware NAT, 공인 IP 없음 포트 포워딩 불가 네트워크 관리 권한 없음 Private RDS 연동 필요 보안 관점에서 RDS 퍼블릭 노출은 안티패턴 멀티 환경 운영 Dev/Prod 분리 + 동일 Chart 재사용 이 제약을 해결하는 과정 자체가 클라우드/DevOps 엔지니어에게 요구되는 핵심 역량(네트워크 설계, IaC, GitOps, 트러블슈팅)을 증명할 수 있다고 판단했습니다. 2. 프로젝트 개요 단순히 애플리케이션을 컨테이너로 실행하는 수준을 넘어서, 실제 운영 환경에 가까운 배포 자동화 구조를 직접 설계하고 구현하는 것을 목표로 했습니다. 온프레미스 VMware 환경에 Kubernetes 클러스터를 직접 구성하고, 원시 YAML 매니페스트에서 출발해 Helm Chart로 고도화한 뒤 GitHub Actions → ECR → ArgoCD 로 이어지는 GitOps 기반 CI/CD 파이프라인까지 완성했습니다. 핵심 목표: 쿠버네티스 핵심 리소스(Deployment, Service, Ingress, Namespace)를 실제 클러스터에서 직접 경험 온프레미스 ↔ AWS VPC 하이브리드 네트워크 연결 (Tailscale Subnet Router) Prod / Dev 환경을 노드 단위로 분리하여 운영 안정성 확보 Helm Chart로 환경별 설정을 코드로 관리 GitOps 방식으로 배포 이력 추적 및 자동화 3. 전체 아키텍처 CI/CD 흐름: 개발자 코드 Push │ ▼ GitHub Actions (CI) │ Docker 이미지 빌드 │ Amazon ECR Push ▼ ArgoCD (CD / GitOps) │ Git 상태 감지 → Sync ▼ OnPremise K8s 클러스터 (VMware 211.183.3.0/24) ├── Master Node (211.183.3.200) ├── Prod: worker1 (211.183.3.210) + worker2 (211.183.3.220) └── Dev: devworker (211.183.3.230) 외부 접근 흐름: 사용자 → Route53(prod.dongkyu.cloud) → EC2 nginx 리버스 프록시 → Tailscale 터널 → K8s NGINX Ingress Controller (NodePort 31018) → Service → Pod 4. 하이브리드 네트워크 설계 (Tailscale) 41. 환경 제약과 네트워크 방안 비교 온프레미스 K8s 클러스터에서 AWS Private VPC의 리소스(RDS 등)에 접근해야 했지만, 학원 환경은 이중 NAT 구조로 공인 IP가 없었습니다. 방안 설명 채택 여부 이유 AWS SitetoSite VPN IPsec 터널 불가 고정 공인 IP + VPN 장비 필요 AWS Direct Connect 전용선 불가 물리적 전용선, 월 수십만 원 VPC Endpoint (PrivateLink) 프라이빗 접근 부적합 온프레미스→AWS 방향 해결 불가, NLB 비용 월 $25+ Public RDS + IP 허용 RDS 퍼블릭 노출 부적합 보안 안티패턴, 이중 NAT로 공인 IP 변동 Tailscale Subnet Router WireGuard 메시 VPN 채택 outbound HTTPS만으로 동작, 무료 42. Tailscale 채택 근거 이중 NAT에서도 동작: outbound HTTPS(443)만 사용하므로 방화벽/NAT 뒤에서도 연결 가능 공인 IP 불필요: 양쪽 모두 Tailscale coordination 서버에 outbound 연결만 하면 됨 Subnet Router 기능: EC2 한 대를 서브넷 라우터로 설정하면 VPC 전체 대역에 접근 가능 WireGuard 기반: 커널 레벨 동작, OpenVPN 대비 34배 빠른 성능 무료 티어: 개인 사용 시 100대 디바이스까지 무료 43. 구성 방법 EC2 — Subnet Router 설정: bash 1. IP 포워딩 활성화 sudo sysctl w net.ipv4.ipforward=1 echo 'net.ipv4.ipforward = 1' sudo tee a /etc/sysctl.conf 2. EC2 소스/대상 확인 비활성화 (AWS 콘솔) 3. VPC 대역 광고 sudo tailscale up advertiseroutes=10.0.0.0/16 acceptdns=false 4. Tailscale Admin Console에서 서브넷 라우팅 승인 k8s 마스터 노드 — 라우트 수락: bash sudo tailscale up acceptroutes 44. 외부 접근 구조 (nginx 리버스 프록시) nginx prod — prod.dongkyu.cloud server { listen 80; servername prod.dongkyu.cloud; location / { proxypass http://100.100.150.8:31018; proxysetheader Host $host; proxysetheader XRealIP $remoteaddr; proxysetheader XForwardedFor $proxyaddxforwardedfor; proxysetheader XForwardedProto $scheme; } } 100.100.150.8: 마스터 노드의 Tailscale IP 31018: K8s NGINX Ingress Controller의 NodePort prod/dev 모두 같은 NodePort로 보내고, Host 헤더를 그대로 전달하기 때문에 K8s Ingress가 Host 기반으로 dev/prod를 구분 45. Route53 도메인 설정 레코드 타입 값 용도 prod.dongkyu.cloud A EC2 Elastic IP 운영 환경 접속 dev.dongkyu.cloud A EC2 Elastic IP (동일) 개발 환경 접속 derp.dongkyu.cloud A EC2 Elastic IP (동일) 자체 DERP 서버 5. 온프레미스 K8s 클러스터 구성 51. 클러스터 노드 구성 역할 호스트명 IP 환경 Master toymaster 211.183.3.200/24 제어 플레인 Worker toyworker1 211.183.3.210/24 Prod Worker toyworker2 211.183.3.220/24 Prod Worker devtoyworker 211.183.3.230/24 Dev kubeadm v1.30.14, Ubuntu 24.04, Flannel CNI로 구성했으며 Pod CIDR은 10.244.0.0/16을 사용했습니다. 52. Prod / Dev 환경 분리 단순히 Namespace만 분리하면 Pod가 어느 노드에든 스케줄링될 수 있습니다. 노드 라벨과 nodeSelector를 조합해 Prod Pod는 Prod 노드에만, Dev Pod는 Dev 노드에만 배치되도록 강제했습니다. bash kubectl create namespace dev kubectl create namespace prod kubectl label node toyworker1 env=prod kubectl label node toyworker2 env=prod kubectl label node devtoyworker env=dev yaml Deployment spec 일부 spec: template: spec: nodeSelector: env: prod dev 환경은 env: dev 항목 Dev Prod Namespace dev prod Node 라벨 env=dev env=prod 도메인 dev.dongkyu.cloud prod.dongkyu.cloud 레플리카 1 2 6. 초기 매니페스트 작성 61. 애플리케이션 구조 Backend(Spring Boot, 8080 포트)와 Frontend(Nginx, 80 포트)를 각각 Deployment + Service로 정의하고, NGINX Ingress Controller로 외부 라우팅을 구성했습니다. yaml appdeploy.yml — backend Deployment (일부) apiVersion: apps/v1 kind: Deployment metadata: name: backenddep spec: replicas: 2 selector: matchLabels: app: backend template: spec: nodeSelector: env: prod imagePullSecrets: name: ecrsecret containers: name: backend image: 431538665162.dkr.ecr.apnortheast2.amazonaws.com/backend:latest ports: containerPort: 8080 62. Ingress 라우팅 설계 /api/(.) 경로는 backend로, 나머지 경로(/?(.))는 frontend로 분기합니다. yaml ingress.yml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: appingress annotations: nginx.ingress.kubernetes.io/rewritetarget: /$1 nginx.ingress.kubernetes.io/useregex: "true" spec: ingressClassName: nginx rules: host: prod.dongkyu.cloud http: paths: path: /api/(.) pathType: ImplementationSpecific backend: service: name: backend port: number: 8080 path: /?(.) pathType: ImplementationSpecific backend: service: name: frontend port: number: 80 7. Helm Chart 고도화 초기 매니페스트는 환경마다 중복 YAML을 작성해야 했습니다. Helm Chart로 템플릿화하여 하나의 Chart를 values 파일만 바꿔 dev/prod에 재사용하는 구조로 개선했습니다. GitHub: LDK511/aws13k8sproject 71. 디렉토리 구조 helm/ ├── backend/ │ ├── Chart.yaml │ ├── templates/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── ingress.yaml │ ├── values.yaml 공통 기본값 │ ├── valuesdev.yaml Dev 오버라이드 │ └── valuesprod.yaml Prod 오버라이드 └── frontend/ ├── Chart.yaml ├── templates/ │ ├── deployment.yaml │ └── service.yaml ├── values.yaml ├── valuesdev.yaml └── valuesprod.yaml 72. Before vs After 비교 항목 초기 매니페스트 Helm Chart 고도화 후 환경별 파일 dev/prod 각각 별도 YAML values 파일만 교체 재사용성 없음 (복사·붙여넣기) 하나의 Chart 재사용 배포 이력 수동 관리 helm history로 추적 롤백 이전 파일 재적용 helm rollback 1커맨드 8. ECR 프라이빗 레지스트리 연동 트러블슈팅 문제 상황 ECR 프라이빗 저장소에서 이미지를 Pull할 때 ImagePullBackOff 오류가 지속적으로 발생했습니다. 터미널에서 docker login을 성공했음에도 동일한 에러가 반복되었습니다. 근본 원인 분석 원인 설명 인증 주체 분리 터미널의 docker login 정보를 Kubelet이 자동으로 공유하지 않음 저장 위치 차이 로그인 정보는 /.docker/config.json (유저 홈) → Kubelet은 참조 불가 노드 전파 안 됨 마스터 노드 인증은 워커 노드에 전파되지 않음 런타임 차이 최신 K8s는 containerd 사용 → Docker 로그인 정보와 미호환 해결 방법 bash ECR 인증 토큰으로 K8s Secret 생성 (유효시간 12시간) kubectl create secret dockerregistry ecrsecret \ dockerserver=431538665162.dkr.ecr.apnortheast2.amazonaws.com \ dockerusername=AWS \ dockerpassword=$(aws ecr getloginpassword region apnortheast2) yaml Deployment에 imagePullSecrets 명시 spec: template: spec: imagePullSecrets: name: ecrsecret 운영 개선 포인트: ECR 토큰은 12시간마다 만료됩니다. 실제 운영 환경에서는 CronJob을 통한 자동 갱신이 필요합니다. 9. Pod → RDS 접근 트러블슈팅 문제 상황 마스터 노드에서는 Tailscale을 통해 RDS(10.0.22.7)에 정상 접속되지만, Pod에서는 접근이 불가능한 문제가 발생했습니다. 원인 분석 Tailscale은 정책 라우팅(table 52)을 사용합니다. 호스트 프로세스는 table 52를 참조하지만, Pod 트래픽은 메인 라우팅 테이블을 참조하여 VPC 경로를 찾지 못했습니다. 호스트 프로세스 → ip rule → table 52 → tailscale0 → OK Pod 트래픽 → ip rule → main table → 경로 없음 → FAIL bash 확인: table 52에는 경로가 있음 ip route get 10.0.0.0 → 10.0.0.0 dev tailscale0 table 52 src 100.100.150.8 확인: 메인 테이블에는 없음 ip route grep tailscale → (아무것도 없음!) 해결 과정 단계 내용 결과 1차 시도 ip rule + iptables FORWARD/MASQUERADE 마스터 Pod만 성공, 워커 실패 2차 시도 FORWARD 체인 순서 수정 (A → I) 여전히 실패 근본 원인 Flannel CNI가 워커에서 이미 MASQUERADE를 수행하여 src IP가 변경됨 — 최종 해결 iptables 규칙에서 소스 제한 제거, 인터페이스 + 목적지 기준으로 매칭 성공 최종 해결 명령 bash 마스터 노드 ip rule add to 10.0.0.0/16 lookup 52 priority 5000 iptables I FORWARD d 10.0.0.0/16 o tailscale0 j ACCEPT iptables I FORWARD s 10.0.0.0/16 i tailscale0 m state state RELATED,ESTABLISHED j ACCEPT iptables t nat I POSTROUTING d 10.0.0.0/16 o tailscale0 j MASQUERADE 워커 노드 (각각) ip route add 10.0.0.0/16 via 211.183.3.200 영구화 (systemd 서비스) bash /etc/systemd/system/vpcroute.service 로 등록하여 재부팅 후에도 자동 적용 MASQUERADE가 필요한 이유 [MASQUERADE 없이] Pod(10.244.1.5) → RDS 도착 → RDS 응답: "10.244.1.5로 보내야지" → AWS: "10.244.1.5? 모르는 IP인데?" → DROP [MASQUERADE 있으면] Pod(10.244.1.5) → 마스터에서 src를 100.100.150.8로 변환 → RDS 도착 → RDS 응답: "100.100.150.8로 보내야지" → Tailscale 네트워크로 정상 라우팅 → 마스터 도착 → 마스터가 다시 dst를 10.244.1.5로 복원 → Pod 도착 10. ArgoCD 기반 CI/CD 파이프라인 구축 101. CI/CD 흐름 개발자 Push (main → Prod, develop → Dev) │ ▼ GitHub Actions ├── Docker 이미지 빌드 └── Amazon ECR Push (이미지 태그: commit SHA) │ ▼ ArgoCD (Git 저장소 감지) │ helm/backend/values.yaml의 image.tag 변경 감지 ▼ K8s 클러스터 자동 Sync ├── dev namespace ← develop 브랜치 └── prod namespace ← main 브랜치 102. Application CRD 구성 yaml argocd/backenddev.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: backenddev namespace: argocd spec: project: default source: repoURL: https://github.com/LDK511/aws13k8sproject.git path: helm/backend targetRevision: develop helm: valueFiles: valuesdev.yaml destination: server: https://kubernetes.default.svc namespace: dev syncPolicy: automated: selfHeal: true prune: true 총 4개의 Application CRD를 작성하여 backenddev, backendprod, frontenddev, frontendprod 모두 자동 배포가 가능하도록 구성했습니다. 103. 브랜치 전략 연계 브랜치 ArgoCD Target 배포 환경 네임스페이스 main main Prod prod develop develop Dev dev 11. 배포 검증 결과 Ingress 라우팅 검증 bash kubectl get ingress A NAMESPACE NAME CLASS HOSTS ADDRESS PORTS dev appingress nginx dev.dongkyu.cloud 211.183.3.230 80 prod appingress nginx prod.dongkyu.cloud 211.183.3.230 80 ArgoCD 동기화 상태 4개 Application(backenddev, backendprod, frontenddev, frontendprod) 모두 Healthy / Synced 상태로 정상 동작을 확인했습니다. RDS 연결 검증 bash root@toymaster: mysql h database1.cxey8usueno9.apnortheast2.rds.amazonaws.com u admin p Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 163 Server version: 8.4.8 Source distribution mysql → Tailscale Subnet Router를 통해 Private Subnet의 RDS에 정상 접근 확인 12. 회고 및 개선 방향 잘 된 점 5가지 네트워크 방안을 트레이드오프 분석하여 환경에 최적인 Tailscale Subnet Router를 선택하고 구현 Pod→RDS 정책 라우팅 문제를 3단계에 걸쳐 근본 원인까지 추적하고 해결 초기 단순 매니페스트에서 Helm Chart 고도화, CI/CD 연동까지 전체 배포 사이클을 한 번에 경험 nodeSelector를 통한 Prod/Dev 노드 분리로 리소스 격리 구현 ECR ImagePullBackOff 트러블슈팅 과정에서 K8s 인증 체계(Secret, Kubelet, containerd)에 대한 깊은 이해 획득 GitOps 방식으로 배포 이력 추적 및 selfHeal을 통한 클러스터 자동 복구 경험 개선할 점 / 향후 계획 개선 항목 이유 ECR Secret 자동 갱신 CronJob 12시간 토큰 만료 문제 해결 서울 자체 DERP 서버 구축 도쿄 DERP 대비 레이턴시 50%+ 개선 (3050ms → 515ms) TLS/HTTPS 적용 (certmanager) 현재 HTTP로만 서비스 중 HPA (Horizontal Pod Autoscaler) 트래픽 기반 자동 스케일링 Prometheus + Grafana 모니터링 강화 Pod/노드 메트릭 시각화 이 프로젝트의 핵심 가치는 "서비스를 만드는 것"이 아니라 "서비스가 어떻게 운영되는가"를 직접 설계하고 증명한 것입니다. 네트워크 방안 분석 → Tailscale 하이브리드 연결 → DNS → Ingress → Service → Pod로 이어지는 전체 흐름, ECR 인증 구조, Pod→RDS 정책 라우팅 트러블슈팅, Helm 기반 환경 분리, GitOps 배포를 경험하며 실무적인 관점에서 바라볼 수 있었습니다.

May 10, 2026kubernetes
On-Premise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화