목록으로
kubernetes

On-Premise Kubernetes 클러스터 구축 및 CI/CD 파이프라인 자동화 프로젝트

김민석2026년 05월 10일12분 읽기
이미지

On-Premise Kubernetes 클러스터 구축 및 CI/CD 파이프라인 자동화

프로젝트 기간: 2026.04.27 – 2026.04.30
기술 스택: Kubernetes · Helm · ArgoCD · GitHub Actions · Amazon ECR · AWS
역할: 온프레미스 K8s 클러스터 세팅 / 초기 매니페스트 작성 / Helm Chart 고도화 / CI/CD 파이프라인 구축


목차

  1. 프로젝트 개요
  2. 전체 아키텍처
  3. 온프레미스 K8s 클러스터 구성
  4. 초기 매니페스트 작성
  5. Helm Chart 고도화
  6. ECR 프라이빗 레지스트리 연동 트러블슈팅
  7. ArgoCD 기반 CI/CD 파이프라인 구축
  8. 배포 검증 결과
  9. 회고 및 개선 방향

1. 프로젝트 개요

단순히 애플리케이션을 컨테이너로 실행하는 수준을 넘어서, 실제 운영 환경에 가까운 배포 자동화 구조를 직접 설계하고 구현하는 것을 목표로 했습니다.

온프레미스 VMware 환경에 Kubernetes 클러스터를 직접 구성하고, 원시 YAML 매니페스트에서 출발해 Helm Chart로 고도화한 뒤 GitHub Actions → ECR → ArgoCD 로 이어지는 GitOps 기반 CI/CD 파이프라인까지 완성했습니다.

핵심 목표:

  • 쿠버네티스 핵심 리소스(Deployment, Service, Ingress, Namespace)를 실제 클러스터에서 직접 경험
  • Prod / Dev 환경을 노드 단위로 분리하여 운영 안정성 확보
  • Helm Chart로 환경별 설정을 코드로 관리
  • GitOps 방식으로 배포 이력 추적 및 자동화

2. 전체 아키텍처

이미지

text
1개발자 코드 Push
234GitHub Actions (CI)
5    │  Docker 이미지 빌드
6    │  Amazon ECR Push
78ArgoCD (CD / GitOps)
9    │  Git 상태 감지 → Sync
1011On-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)

외부 접근 흐름:

text
1사용자 → Route53(prod.dongkyu.cloud)
2    → EC2 nginx 리버스 프록시
3    → Tailscale 터널
4    → K8s NGINX Ingress Controller (NodePort 31018)
5    → Service → Pod

3. 온프레미스 K8s 클러스터 구성

3-1. 클러스터 노드 구성

역할호스트명IP환경
Mastertoy-master211.183.3.200/24제어 플레인
Workertoy-worker1211.183.3.210/24Prod
Workertoy-worker2211.183.3.220/24Prod
Workerdev-toy-worker211.183.3.230/24Dev

kubeadm v1.30.14, Ubuntu 24.04, Flannel CNI로 구성했으며 Pod CIDR은 10.244.0.0/16을 사용했습니다.


3-2. Prod / Dev 환경 분리

단순히 Namespace만 분리하면 Pod가 어느 노드에든 스케줄링될 수 있습니다. 노드 라벨과 nodeSelector를 조합해 Prod Pod는 Prod 노드에만, Dev Pod는 Dev 노드에만 배치되도록 강제했습니다.

bash
1kubectl 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
yaml
1# Deployment spec 일부
2spec:
3  template:
4    spec:
5      nodeSelector:
6        env: prod   # dev 환경은 env: dev

항목DevProd
Namespacedevprod
Node 라벨env=devenv=prod
도메인dev.dongkyu.cloudprod.dongkyu.cloud
레플리카12


4. 초기 매니페스트 작성

4-1. 애플리케이션 구조

Backend(Spring Boot, 8080 포트)와 Frontend(Nginx, 80 포트)를 각각 Deployment + Service로 정의하고, NGINX Ingress Controller로 외부 라우팅을 구성했습니다.

yaml
1# 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

4-2. Ingress 라우팅 설계

/api/(.*) 경로는 backend로, 나머지 경로(/?(.*))는 frontend로 분기합니다.

yaml
1# 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


이미지

외부 요청 경로:

text
1prod.dongkyu.cloud
2  └─> DNS: 211.183.3.231 (Ingress Controller External IP)
3        └─> NGINX Ingress: Host 헤더 확인
4              ├─> /api/* → backend Service → Pod
5              └─> /*     → frontend Service → Pod


5. Helm Chart 고도화

초기 매니페스트는 환경마다 중복 YAML을 작성해야 했습니다. Helm Chart로 템플릿화하여 하나의 Chart를 values 파일만 바꿔 dev/prod에 재사용하는 구조로 개선했습니다.

GitHub: LDK511/aws13-k8s-project

5-1. 디렉토리 구조

text
1helm/
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

5-2. values.yaml 구조

yaml
1# backend/values.yaml
2env: prod
3imagePullSecret: ecr-secret
4replicaCount: 2
5image:
6  repository: 431538665162.dkr.ecr.ap-northeast-2.amazonaws.com/backend
7  tag: "latest"
8service:
9  port: 8080
10ingress:
11  enabled: true
12  className: nginx
13  host: prod.dongkyu.cloud
yaml
1# backend/values-dev.yaml  — dev 환경 오버라이드
2env: dev
3replicaCount: 1
4image:
5  tag: "latest"
6ingress:
7  host: dev.dongkyu.cloud

5-3. Helm 템플릿 — Deployment

yaml
1# backend/templates/deployment.yaml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5  name: backend
6spec:
7  replicas: {{ .Values.replicaCount }}
8  selector:
9    matchLabels:
10      app: backend
11  template:
12    spec:
13      nodeSelector:
14        env: {{ .Values.env }}
15      imagePullSecrets:
16        - name: {{ .Values.imagePullSecret }}
17      containers:
18        - name: backend
19          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
20          ports:
21            - containerPort: 8080

5-4. 배포 명령 흐름

bash
1# 문법 검사
2helm lint ./backend && helm lint ./frontend
3
4# 렌더링 미리보기 (실제 배포 전 확인)
5helm template backend-dev ./backend -f ./backend/values-dev.yaml
6
7# Dev 배포
8helm install backend-dev  ./backend  -n dev  -f ./backend/values-dev.yaml
9helm install frontend-dev ./frontend -n dev  -f ./frontend/values-dev.yaml
10
11# Prod 배포
12helm install backend-prod  ./backend  -n prod -f ./backend/values-prod.yaml
13helm install frontend-prod ./frontend -n prod -f ./frontend/values-prod.yaml
14
15# 업데이트
16helm upgrade backend-dev ./backend -n dev -f ./backend/values-dev.yaml

Before vs After 비교:

항목초기 매니페스트Helm Chart 고도화 후
환경별 파일dev/prod 각각 별도 YAMLvalues 파일만 교체
재사용성없음 (복사·붙여넣기)하나의 Chart 재사용
배포 이력수동 관리helm history로 추적
롤백이전 파일 재적용helm rollback 1커맨드


6. ECR 프라이빗 레지스트리 연동 트러블슈팅

문제 상황

ECR 프라이빗 저장소에서 이미지를 Pull할 때 ImagePullBackOff 오류가 지속적으로 발생했습니다. 터미널에서 docker login을 성공했음에도 동일한 에러가 반복되었습니다.


근본 원인 분석

원인설명
인증 주체 분리터미널의 docker login 정보를 Kubelet이 자동으로 공유하지 않음
저장 위치 차이로그인 정보는 ~/.docker/config.json (유저 홈) → Kubelet은 참조 불가
노드 전파 안 됨마스터 노드 인증은 워커 노드에 전파되지 않음
런타임 차이최신 K8s는 containerd 사용 → Docker 로그인 정보와 미호환

해결 방법

ECR 로그인 토큰을 Kubernetes Secret으로 등록하고 Deployment에 명시적으로 연결합니다.

핵심 원리: IAM 키는 "열쇠를 만들 권한"이고, 실제로 이미지를 Pull하는 Kubelet이 열 수 있는 "물리적 열쇠"(ecr-secret)를 네임스페이스에 직접 비치해야 합니다.

bash
1# Step 1: 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)
yaml
1# Step 2: Deployment에 imagePullSecrets 명시
2spec:
3  template:
4    spec:
5      imagePullSecrets:
6        - name: ecr-secret
7      containers:
8        - name: backend
9          image: 431538665162.dkr.ecr.ap-northeast-2.amazonaws.com/backend:latest

운영 개선 포인트: ECR 토큰은 12시간마다 만료됩니다. 실제 운영 환경에서는 CronJob을 통한 자동 갱신이 필요합니다.



7. ArgoCD 기반 CI/CD 파이프라인 구축

7-1. CI/CD 흐름

text
1개발자 Push (main → Prod, develop → Dev)
234GitHub Actions
5    ├── Docker 이미지 빌드
6    └── Amazon ECR Push (이미지 태그: commit SHA)
789    ArgoCD (Git 저장소 감지)
10            │  helm/backend/values.yaml의 image.tag 변경 감지
1112    K8s 클러스터 자동 Sync
13            ├── dev namespace ← develop 브랜치
14            └── prod namespace ← main 브랜치

7-2. ArgoCD 설치 (Helm)

bash
1helm repo add argo https://argoproj.github.io/argo-helm
2helm repo update
3
4helm install argocd argo/argo-cd \
5  --namespace argocd \
6  --create-namespace \
7  --set server.service.type=NodePort \
8  --set server.service.nodePortHttps=31443

Tailscale IP를 통해 ArgoCD UI에 접근합니다: https://100.100.150.8:31443


7-3. Application CRD 구성

ArgoCD에게 "어떤 Git 저장소의 어떤 Chart를 어떤 네임스페이스에 배포할 것인가"를 선언하는 Application CRD를 작성합니다.

yaml
1# 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          # Dev → develop 브랜치
13    helm:
14      valueFiles:
15        - values-dev.yaml
16  destination:
17    server: https://kubernetes.default.svc
18    namespace: dev
19  syncPolicy:
20    automated:                       # Git 변경 감지 시 자동 배포
21      selfHeal: true                 # 클러스터 상태가 Git과 달라지면 자동 복구
22      prune: true                    # Git에서 삭제된 리소스 자동 제거

총 4개의 Application CRD를 작성하여 backend-dev, backend-prod, frontend-dev, frontend-prod 모두 자동 배포가 가능하도록 구성했습니다.

bash
1kubectl apply -f argocd/backend-dev.yaml
2kubectl apply -f argocd/backend-prod.yaml
3kubectl apply -f argocd/frontend-dev.yaml
4kubectl apply -f argocd/frontend-prod.yaml

7-4. 브랜치 전략 연계

브랜치ArgoCD Target배포 환경네임스페이스
mainmainProdprod
developdevelopDevdev


8. 배포 검증 결과

8-1. Pod 배치 확인

이미지 이미지

8-3. Ingress 라우팅 검증

bash
1kubectl 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

8-4. ArgoCD 동기화 상태

이미지

4개 Application(backend-dev, backend-prod, frontend-dev, frontend-prod) 모두 Healthy / Synced 상태로 정상 동작을 확인했습니다.



9. 회고 및 개선 방향

잘 된 점

  • 초기 단순 매니페스트에서 Helm Chart 고도화, CI/CD 연동까지 전체 배포 사이클을 한 번에 경험
  • nodeSelector를 통한 Prod/Dev 노드 분리로 리소스 격리 구현
  • ECR ImagePullBackOff 트러블슈팅 과정에서 **K8s 인증 체계(Secret, Kubelet, containerd)**에 대한 깊은 이해 획득
  • GitOps 방식으로 배포 이력 추적 및 selfHeal을 통한 클러스터 자동 복구 경험

개선할 점 / 향후 계획

개선 항목이유
ECR Secret 자동 갱신 CronJob12시간 토큰 만료 문제 해결
TLS/HTTPS 적용 (cert-manager)현재 HTTP로만 서비스 중
HPA (Horizontal Pod Autoscaler)트래픽 기반 자동 스케일링
Prometheus + Grafana 모니터링 강화Pod/노드 메트릭 시각화

이 프로젝트의 핵심 가치는 "서비스를 만드는 것"이 아니라 "서비스가 어떻게 운영되는가"를 직접 설계하고 증명한 것입니다.
DNS → Ingress → Service → Pod로 이어지는 네트워크 흐름, ECR 인증 구조, Helm 기반 환경 분리, GitOps 배포 방식은 클라우드/DevOps 직무에서 즉시 활용 가능한 역량입니다.

This post is licensed under CC BY 4.0 by the author.

Share:

Further Reading

댓글 0

첫 번째 댓글을 남겨보세요

댓글 작성