On-Premise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화
On-Premise 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 파이프라인 구축
목차
- 프로젝트 선정 이유
- 프로젝트 개요
- 전체 아키텍처
- 하이브리드 네트워크 설계 (Tailscale)
- 온프레미스 K8s 클러스터 구성
- 초기 매니페스트 작성
- Helm Chart 고도화
- ECR 프라이빗 레지스트리 연동 트러블슈팅
- Pod → RDS 접근 트러블슈팅
- ArgoCD 기반 CI/CD 파이프라인 구축
- 배포 검증 결과
- 회고 및 개선 방향
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 흐름:
text1개발자 코드 Push 2 │ 3 ▼ 4GitHub Actions (CI) 5 │ Docker 이미지 빌드 6 │ Amazon ECR Push 7 ▼ 8ArgoCD (CD / GitOps) 9 │ Git 상태 감지 → Sync 10 ▼ 11On-Premise K8s 클러스터 (VMware 211.183.3.0/24) 12 ├── Master Node (211.183.3.200) 13 ├── Prod: worker1 (211.183.3.210) + worker2 (211.183.3.220) 14 └── Dev: dev-worker (211.183.3.230)
외부 접근 흐름:

text1사용자 → Route53(prod.dongkyu.cloud) 2 → EC2 nginx 리버스 프록시 3 → Tailscale 터널 4 → K8s NGINX Ingress Controller (NodePort 31018) 5 → Service → Pod
4. 하이브리드 네트워크 설계 (Tailscale)
4-1. 환경 제약과 네트워크 방안 비교
온프레미스 K8s 클러스터에서 AWS Private VPC의 리소스(RDS 등)에 접근해야 했지만, 학원 환경은 이중 NAT 구조로 공인 IP가 없었습니다.
| 방안 | 설명 | 채택 여부 | 이유 |
|---|---|---|---|
| AWS Site-to-Site 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만으로 동작, 무료 |
4-2. Tailscale 채택 근거
- 이중 NAT에서도 동작: outbound HTTPS(443)만 사용하므로 방화벽/NAT 뒤에서도 연결 가능
- 공인 IP 불필요: 양쪽 모두 Tailscale coordination 서버에 outbound 연결만 하면 됨
- Subnet Router 기능: EC2 한 대를 서브넷 라우터로 설정하면 VPC 전체 대역에 접근 가능
- WireGuard 기반: 커널 레벨 동작, OpenVPN 대비 3~4배 빠른 성능
- 무료 티어: 개인 사용 시 100대 디바이스까지 무료
4-3. 구성 방법
EC2 — Subnet Router 설정:
bash1# 1. IP 포워딩 활성화 2sudo sysctl -w net.ipv4.ip_forward=1 3echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf 4 5# 2. EC2 소스/대상 확인 비활성화 (AWS 콘솔) 6 7# 3. VPC 대역 광고 8sudo tailscale up --advertise-routes=10.0.0.0/16 --accept-dns=false 9 10# 4. Tailscale Admin Console에서 서브넷 라우팅 승인
k8s 마스터 노드 — 라우트 수락:
bash1sudo tailscale up --accept-routes
4-4. 외부 접근 구조 (nginx 리버스 프록시)
nginx1# prod — prod.dongkyu.cloud 2server { 3 listen 80; 4 server_name prod.dongkyu.cloud; 5 location / { 6 proxy_pass http://100.100.150.8:31018; 7 proxy_set_header Host $host; 8 proxy_set_header X-Real-IP $remote_addr; 9 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 proxy_set_header X-Forwarded-Proto $scheme; 11 } 12}
100.100.150.8: 마스터 노드의 Tailscale IP31018: K8s NGINX Ingress Controller의 NodePort- prod/dev 모두 같은 NodePort로 보내고, Host 헤더를 그대로 전달하기 때문에 K8s Ingress가 Host 기반으로 dev/prod를 구분
4-5. 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 클러스터 구성
5-1. 클러스터 노드 구성
| 역할 | 호스트명 | IP | 환경 |
|---|---|---|---|
| Master | toy-master | 211.183.3.200/24 | 제어 플레인 |
| Worker | toy-worker1 | 211.183.3.210/24 | Prod |
| Worker | toy-worker2 | 211.183.3.220/24 | Prod |
| Worker | dev-toy-worker | 211.183.3.230/24 | Dev |
kubeadm v1.30.14, Ubuntu 24.04, Flannel CNI로 구성했으며 Pod CIDR은 10.244.0.0/16을 사용했습니다.
5-2. Prod / Dev 환경 분리
단순히 Namespace만 분리하면 Pod가 어느 노드에든 스케줄링될 수 있습니다. 노드 라벨과 nodeSelector를 조합해 Prod Pod는 Prod 노드에만, Dev Pod는 Dev 노드에만 배치되도록 강제했습니다.
bash1kubectl create namespace dev 2kubectl create namespace prod 3 4kubectl label node toy-worker1 env=prod 5kubectl label node toy-worker2 env=prod 6kubectl label node dev-toy-worker env=dev
yaml1# Deployment spec 일부 2spec: 3 template: 4 spec: 5 nodeSelector: 6 env: prod # dev 환경은 env: dev
| 항목 | Dev | Prod |
|---|---|---|
| Namespace | dev | prod |
| Node 라벨 | env=dev | env=prod |
| 도메인 | dev.dongkyu.cloud | prod.dongkyu.cloud |
| 레플리카 | 1 | 2 |
6. 초기 매니페스트 작성
6-1. 애플리케이션 구조
Backend(Spring Boot, 8080 포트)와 Frontend(Nginx, 80 포트)를 각각 Deployment + Service로 정의하고, NGINX Ingress Controller로 외부 라우팅을 구성했습니다.
yaml1# app-deploy.yml — backend Deployment (일부) 2apiVersion: apps/v1 3kind: Deployment 4metadata: 5 name: backend-dep 6spec: 7 replicas: 2 8 selector: 9 matchLabels: 10 app: backend 11 template: 12 spec: 13 nodeSelector: 14 env: prod 15 imagePullSecrets: 16 - name: ecr-secret 17 containers: 18 - name: backend 19 image: 431538665162.dkr.ecr.ap-northeast-2.amazonaws.com/backend:latest 20 ports: 21 - containerPort: 8080
6-2. Ingress 라우팅 설계
/api/(.*) 경로는 backend로, 나머지 경로(/?(.*))는 frontend로 분기합니다.
yaml1# ingress.yml 2apiVersion: networking.k8s.io/v1 3kind: Ingress 4metadata: 5 name: app-ingress 6 annotations: 7 nginx.ingress.kubernetes.io/rewrite-target: /$1 8 nginx.ingress.kubernetes.io/use-regex: "true" 9spec: 10 ingressClassName: nginx 11 rules: 12 - host: prod.dongkyu.cloud 13 http: 14 paths: 15 - path: /api/(.*) 16 pathType: ImplementationSpecific 17 backend: 18 service: 19 name: backend 20 port: 21 number: 8080 22 - path: /?(.*) 23 pathType: ImplementationSpecific 24 backend: 25 service: 26 name: frontend 27 port: 28 number: 80
7. Helm Chart 고도화
초기 매니페스트는 환경마다 중복 YAML을 작성해야 했습니다. Helm Chart로 템플릿화하여 하나의 Chart를 values 파일만 바꿔 dev/prod에 재사용하는 구조로 개선했습니다.
GitHub: LDK511/aws13-k8s-project
7-1. 디렉토리 구조
text1helm/ 2├── backend/ 3│ ├── Chart.yaml 4│ ├── templates/ 5│ │ ├── deployment.yaml 6│ │ ├── service.yaml 7│ │ └── ingress.yaml 8│ ├── values.yaml # 공통 기본값 9│ ├── values-dev.yaml # Dev 오버라이드 10│ └── values-prod.yaml # Prod 오버라이드 11└── frontend/ 12 ├── Chart.yaml 13 ├── templates/ 14 │ ├── deployment.yaml 15 │ └── service.yaml 16 ├── values.yaml 17 ├── values-dev.yaml 18 └── values-prod.yaml
7-2. 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 로그인 정보와 미호환 |
해결 방법
bash1# ECR 인증 토큰으로 K8s Secret 생성 (유효시간 12시간) 2kubectl create secret docker-registry ecr-secret \ 3 --docker-server=431538665162.dkr.ecr.ap-northeast-2.amazonaws.com \ 4 --docker-username=AWS \ 5 --docker-password=$(aws ecr get-login-password --region ap-northeast-2)
yaml1# Deployment에 imagePullSecrets 명시 2spec: 3 template: 4 spec: 5 imagePullSecrets: 6 - name: ecr-secret
운영 개선 포인트: ECR 토큰은 12시간마다 만료됩니다. 실제 운영 환경에서는 CronJob을 통한 자동 갱신이 필요합니다.
9. Pod → RDS 접근 트러블슈팅
문제 상황
마스터 노드에서는 Tailscale을 통해 RDS(10.0.22.7)에 정상 접속되지만, Pod에서는 접근이 불가능한 문제가 발생했습니다.
원인 분석
Tailscale은 **정책 라우팅(table 52)**을 사용합니다. 호스트 프로세스는 table 52를 참조하지만, Pod 트래픽은 메인 라우팅 테이블을 참조하여 VPC 경로를 찾지 못했습니다.
text1호스트 프로세스 → ip rule → table 52 → tailscale0 → OK 2Pod 트래픽 → ip rule → main table → 경로 없음 → FAIL
bash1# 확인: table 52에는 경로가 있음 2ip route get 10.0.0.0 3# → 10.0.0.0 dev tailscale0 table 52 src 100.100.150.8 4 5# 확인: 메인 테이블에는 없음 6ip route | grep tailscale 7# → (아무것도 없음!)
해결 과정
| 단계 | 내용 | 결과 |
|---|---|---|
| 1차 시도 | ip rule + iptables FORWARD/MASQUERADE | 마스터 Pod만 성공, 워커 실패 |
| 2차 시도 | FORWARD 체인 순서 수정 (-A → -I) | 여전히 실패 |
| 근본 원인 | Flannel CNI가 워커에서 이미 MASQUERADE를 수행하여 src IP가 변경됨 | — |
| 최종 해결 | iptables 규칙에서 소스 제한 제거, 인터페이스 + 목적지 기준으로 매칭 | 성공 |
최종 해결 명령
bash1# 마스터 노드 2ip rule add to 10.0.0.0/16 lookup 52 priority 5000 3iptables -I FORWARD -d 10.0.0.0/16 -o tailscale0 -j ACCEPT 4iptables -I FORWARD -s 10.0.0.0/16 -i tailscale0 -m state --state RELATED,ESTABLISHED -j ACCEPT 5iptables -t nat -I POSTROUTING -d 10.0.0.0/16 -o tailscale0 -j MASQUERADE 6 7# 워커 노드 (각각) 8ip route add 10.0.0.0/16 via 211.183.3.200
영구화 (systemd 서비스)
bash1# /etc/systemd/system/vpc-route.service 로 등록하여 재부팅 후에도 자동 적용
MASQUERADE가 필요한 이유
text1[MASQUERADE 없이] 2Pod(10.244.1.5) → RDS 도착 → RDS 응답: "10.244.1.5로 보내야지" 3→ AWS: "10.244.1.5? 모르는 IP인데?" → DROP 4 5[MASQUERADE 있으면] 6Pod(10.244.1.5) → 마스터에서 src를 100.100.150.8로 변환 → RDS 도착 7→ RDS 응답: "100.100.150.8로 보내야지" 8→ Tailscale 네트워크로 정상 라우팅 → 마스터 도착 9→ 마스터가 다시 dst를 10.244.1.5로 복원 → Pod 도착
10. ArgoCD 기반 CI/CD 파이프라인 구축
10-1. CI/CD 흐름
text1개발자 Push (main → Prod, develop → Dev) 2 │ 3 ▼ 4GitHub Actions 5 ├── Docker 이미지 빌드 6 └── Amazon ECR Push (이미지 태그: commit SHA) 7 │ 8 ▼ 9 ArgoCD (Git 저장소 감지) 10 │ helm/backend/values.yaml의 image.tag 변경 감지 11 ▼ 12 K8s 클러스터 자동 Sync 13 ├── dev namespace ← develop 브랜치 14 └── prod namespace ← main 브랜치
10-2. Application CRD 구성
yaml1# argocd/backend-dev.yaml 2apiVersion: argoproj.io/v1alpha1 3kind: Application 4metadata: 5 name: backend-dev 6 namespace: argocd 7spec: 8 project: default 9 source: 10 repoURL: https://github.com/LDK511/aws13-k8s-project.git 11 path: helm/backend 12 targetRevision: develop 13 helm: 14 valueFiles: 15 - values-dev.yaml 16 destination: 17 server: https://kubernetes.default.svc 18 namespace: dev 19 syncPolicy: 20 automated: 21 selfHeal: true 22 prune: true
총 4개의 Application CRD를 작성하여 backend-dev, backend-prod, frontend-dev, frontend-prod 모두 자동 배포가 가능하도록 구성했습니다.
10-3. 브랜치 전략 연계
| 브랜치 | ArgoCD Target | 배포 환경 | 네임스페이스 |
|---|---|---|---|
main | main | Prod | prod |
develop | develop | Dev | dev |
11. 배포 검증 결과
Ingress 라우팅 검증
bash1kubectl get ingress -A 2# NAMESPACE NAME CLASS HOSTS ADDRESS PORTS 3# dev app-ingress nginx dev.dongkyu.cloud 211.183.3.230 80 4# prod app-ingress nginx prod.dongkyu.cloud 211.183.3.230 80
ArgoCD 동기화 상태
4개 Application(backend-dev, backend-prod, frontend-dev, frontend-prod) 모두 Healthy / Synced 상태로 정상 동작을 확인했습니다.
RDS 연결 검증
bash1root@toy-master:~# mysql -h database-1.cxey8usueno9.ap-northeast-2.rds.amazonaws.com -u admin -p 2Welcome to the MySQL monitor. Commands end with ; or \g. 3Your MySQL connection id is 163 4Server version: 8.4.8 Source distribution 5mysql>
→ 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%+ 개선 (30 |
| TLS/HTTPS 적용 (cert-manager) | 현재 HTTP로만 서비스 중 |
| HPA (Horizontal Pod Autoscaler) | 트래픽 기반 자동 스케일링 |
| Prometheus + Grafana 모니터링 강화 | Pod/노드 메트릭 시각화 |
이 프로젝트의 핵심 가치는 "서비스를 만드는 것"이 아니라 "서비스가 어떻게 운영되는가"를 직접 설계하고 증명한 것입니다.
네트워크 방안 분석 → Tailscale 하이브리드 연결 → DNS → Ingress → Service → Pod로 이어지는 전체 흐름, ECR 인증 구조, Pod→RDS 정책 라우팅 트러블슈팅, Helm 기반 환경 분리, GitOps 배포를 경험하며 실무적인 관점에서 바라볼 수 있었습니다.
Further Reading
Older
–댓글 0개
첫 번째 댓글을 남겨보세요