k8s 직접 해보기 (6) - Helm Chart & ArgoCD를 이용한 배포

Endermaru·2025년 6월 25일

k8s 직접 해보기

목록 보기
7/11

지금까지의 선언적 방식

  • Deployment, Service 등의 Manifest 파일을 하나씩(또는 하나의 파일에) 정의하고, kubectl apply로 해당 파일에 맞게 리소스를 생성 = 개별 리소스를 하나씩 배포하는 방식
  • Hashicorp Vault, External Secret Operator처럼 앱 자체를 패키지화해서 하나의 Helm Chart 배포 단위로 관리할 수 있다면 보다 편리한 배포가 가능할 것

1. Helm Chart로 배포하기

1-1. 기존 리소스 삭제

  • Helm을 이용하지 않고 생성된 리소스 제거(ESO, Vault는 제외)
$ kubectl delete -f spring-deployment.yaml
$ kubectl delete -f redis-deployment.yaml

1-2. Helm Chart 기본 구조 생성

  • templates 폴더의 기본 생성 파일을 모두 제거 & 앞에서 사용한 spring-deployment.yaml, redis-deployment.yaml을 폴더 내부로 복사
    • k8s의 manifest 파일이 values.yaml과 함께 Helm Chart의 template 파일로 기능
    • 필요한 경우 manifest 파일 내부에서 --- 단위로 나뉜 리소스를 개별 파일로 나누어 저장 가능(deployment, service, serviceAccount...)
$ helm create spring-app

# 다음과 같은 트리 구조
spring-app/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── spring-deployment.yaml
│   ├── redis-deployment.yaml
│   └── ...

1-3. 루트의 values.yaml 편집

  • 템플릿이 공용으로 쓸 변수들 정의
    • 환경별로 달라질 수 있는 값(이미지 태그, replicas, 리소스 제한 등)
    • 여러 템플릿에서 반복적으로 사용되는 값

→ 사용자는 Helm Chart를 받아서 values.yaml을 수정하여 환경에 맞게 사용 가능

values.yaml

app:
  name: spring

image:
  repository: endermaru/22-5-team1-server
  tag: latest

deployment:
  replicas: 1
  revisionHistoryLimit: 1

service:
  type: LoadBalancer
  port: 8080
  targetPort: 8080

config:
  name: spring-config
  springProfilesActive: local
  healthShowDetails: "always"

serviceAccount:
  name: spring

namespace: default

vault:
  server: "http://vault.default.svc.cluster.local:8200"
  path: "secret"
  version: "v2"
  mountPath: "kubernetes"
  role: spring-app
  secretPath: myapp
  
secret:
  name: spring-secret
  refreshInterval: 1h

externalSecret:
  name: spring-secrets

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

redis:
  name: redis
  image:
    repository: redis
    tag: 7.0-alpine
  deployment:
    replicas: 1
  service:
    port: 6379
    targetPort: 6379
  resources:
    limits:
      cpu: 200m
      memory: 256Mi
    requests:
      cpu: 100m
      memory: 128Mi

1-4. template 파일 수정

  • values.yaml에 정의한 값들에 기반해 manifest를 Helm 템플릿으로 변환
  • 리소스 별로 파일 분리가 권장됨(여기서는 편의상 하나의 파일로 작성)

spring-deployment.yaml

# deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.app.name }}
  namespace: {{ .Values.namespace }}
spec:
  replicas: {{ .Values.deployment.replicas }}
  revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }}
  selector:
    matchLabels:
      app: {{ .Values.app.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.app.name }}
    spec:
      containers:
        - name: {{ .Values.app.name }}
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          ports:
            - containerPort: {{ .Values.service.targetPort }}
          resources:
            limits:
              cpu: {{ .Values.resources.limits.cpu }}
              memory: {{ .Values.resources.limits.memory }}
            requests:
              cpu: {{ .Values.resources.requests.cpu }}
              memory: {{ .Values.resources.requests.memory }}
          envFrom:
            - configMapRef:
                name: {{ .Values.config.name }}
            - secretRef:
                name: {{ .Values.secret.name }}
---
# service
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.app.name }}
  namespace: {{ .Values.namespace }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ .Values.app.name }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
---
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Values.config.name }}
  namespace: {{ .Values.namespace }}
data:
  SPRING_PROFILES_ACTIVE: {{ .Values.config.springProfilesActive }}
  MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: {{ .Values.config.healthShowDetails | quote }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Values.serviceAccount.name }}
  namespace: {{ .Values.namespace }}
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: vault-store
spec:
  provider:
    vault:
      server: {{ .Values.vault.server | quote }}
      path: {{ .Values.vault.path | quote }}
      version: {{ .Values.vault.version | quote }}
      auth:
        kubernetes:
          mountPath: {{ .Values.vault.mountPath | quote }}
          role: {{ .Values.vault.role | quote }}
          serviceAccountRef:
            name: {{ .Values.serviceAccount.name }}
            namespace: {{ .Values.namespace }}
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: {{ .Values.externalSecret.name }}
  namespace: {{ .Values.namespace }}
spec:
  refreshInterval: {{ .Values.secret.refreshInterval }}
  secretStoreRef:
    name: vault-store
    kind: ClusterSecretStore
  target:
    name: {{ .Values.secret.name }}
    creationPolicy: Owner
  dataFrom:
    - extract:
        key: {{ .Values.vault.secretPath }}

redis-deployment.yaml

# Redis Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.redis.name }}
  namespace: {{ .Values.namespace }}
spec:
  replicas: {{ .Values.redis.deployment.replicas }}
  selector:
    matchLabels:
      app: {{ .Values.redis.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.redis.name }}
    spec:
      containers:
        - name: {{ .Values.redis.name }}
          image: {{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}
          ports:
            - containerPort: {{ .Values.redis.service.targetPort }}
          resources:
            limits:
              cpu: {{ .Values.redis.resources.limits.cpu }}
              memory: {{ .Values.redis.resources.limits.memory }}
            requests:
              cpu: {{ .Values.redis.resources.requests.cpu }}
              memory: {{ .Values.redis.resources.requests.memory }}
---
# Redis Service
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.redis.name }}
  namespace: {{ .Values.namespace }}
spec:
  type: ClusterIP
  selector:
    app: {{ .Values.redis.name }}
  ports:
    - port: {{ .Values.redis.service.port }}
      targetPort: {{ .Values.redis.service.targetPort }}

1-5. template 확인(로컬 렌더링 테스트)

  • values.yaml에 의해 값이 적용된 template을 출력, 잘못 구성되면 에러 발생
  • Helm이 자동으로 리소스 생성 순서를 최적화(ServiceAccount → ConfigMap → ...)
  • value가 제대로 적용되는지, 누락된 값은 없는지 확인 가능
$ helm template spring-app ./spring-app
---
# Source: spring-app/templates/spring-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spring
  namespace: default
---
# Source: spring-app/templates/spring-deployment.yaml
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: spring-config
  namespace: default
data:
  SPRING_PROFILES_ACTIVE: local
  MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: "always"
---
...

1-6. 클러스터 적용될 내용 미리 보기

  • 실제로는 리소스를 생성하지 않고, 적용될 내용만 출력
  • -debug: 어떤 값이 들어갔는지 디버그용 출력도 같이 나옴
  • 이전에 Helm을 거치지 않고 생성된 리소스들은 삭제 필요(Vault, ESO 제외)
  • 문제 발생할 경우 에러 로그를 확인해 수정 가능
$ helm install spring-app ./spring-app --dry-run --debug
install.go:225: 2025-06-25 16:17:56.308445 +0900 KST m=+0.080271701 [debug] Original chart version: ""
install.go:242: 2025-06-25 16:17:56.3089554 +0900 KST m=+0.080782101 [debug] CHART PATH: D:\desktop\toyProject\k8s2\spring-app

NAME: spring-app
...

1-7. Helm을 이용해 앱 설치

$ helm install spring-app ./spring-app

# default namespace의 모든 리소스 확인
$ kubectl get all -n default

# external secret 연결 & spring-secret 생성 확인
$ kubectl get externalsecret -n default
$ kubectl get secret spring-secret -n default -o yaml

# (Optional) 앱을 삭제
# 설치된 릴리스 목록 확인(default 네임스페이스) & 삭제
$ helm list
$ helm uninstall spring-app
  • 만약 Spring pod가 unhealthy & Error: secret "spring-secret" not found 에러가 뜬다면, Vault 설정을 다시 확인 & Vault 설정 후 spring-app을 삭제 후 재설치

2. ArgoCD로 배포하기

  • 로컬의 helm chart 대신 git 저장소를 이용한 배포 = ArgoCD

2-1. helm chart로 설치한 릴리스 삭제(spring-app, ESO)

  • vault는 인증 설정, 정책 등록, 비밀 키 입력 등 '운영자 작업'이 필요한 툴이기 때문에 그대로 helm 설치 버전을 로컬에 유지
$ helm uninstall spring-app
$ helm uninstall external-secrets -n external-secrets-system
# namespace가 default가 아니면 직접 지정 필요

2-2. 트리 구조 변경 & Git 저장소 commit & push

spring-app-gitops/
└── helm/
    └── spring-app/
        ├── Chart.yaml
        ├── values.yaml
        └── templates/...

2-3. Argo CD 설치 & 초기화

# Argo CD 리소스를 위한 새 네임스페이스 argocd를 생성
$ kubectl create namespace argocd

# Argo CD 공식 설치 매니페스트를 가져와서 argocd 네임스페이스에 설치
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

2-4. App of Apps 패턴 적용

spring-app-gitops/
├── apps/                        # Child apps
│   ├── argocd-spring.yaml
│   ├── argocd-eso.yaml
│
├── helm/
│   └── spring-app/             # spring 앱의 Helm Chart
└── root-app.yaml                # Root app
  • 지금까지는 Helm을 이용해 vault, ESO, spring-app을 각각 설치했다면
  • ArgoCd에서는 vault를 제외한 ESO, spring-app을 한 번에 설치
    → 여러 Application 리소스를 하나의 “Root Application”으로 감싸서 관리 가능
    • Root App: 다른 Application들을 관리하는 상위 Application
    • Child Apps: 실제 워크로드를 배포하는 하위 Application들
  • 모든 ArgoCD Application은 argocd 네임스페이스에 설치하는 것이 표준이자 권장사항
    → spring-app, ESO 모두 앱은 argocd 네임스페이스에 설치되고, 워크로드(deployment, service...)는 spec.destination.namespace에 정의된 네임스페이스에 추가됨

namespace 구분하기

  • argocd: 관리자 권한이 필요한 영역
    → 배포 설정을 담은 ArgoCD Application(Root, Child App)
  • destination: 실제 애플리케이션이 실행되는 영역
    → 실제로 배포되는 워크로드(Deployment, Service, Pod)

./apps/argocd-spring.yaml

  • ./helm/spring-app에 위치한 Helm Chart인 spring-app을 ArgoCD에 등록
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: spring-app        # 식별할 이름
  namespace: argocd       # app이 위치할 namespace = argocd가 설치된 namespace
  finalizers:                                  
    - resources-finalizer.argocd.argoproj.io # 앱이 삭제될 때 리소스가 삭제되는 cascading 삭제
spec:
  project: default
  source:
    repoURL: https://github.com/endermaru/spring-app-gitops 
    targetRevision: main      # 배포할 기준이 되는 브랜치
    path: helm/spring-app     # Helm Chart 위치
    helm:
      valueFiles:
        - values.yaml         # Helm에 넘길 values 파일
  destination:
    server: https://kubernetes.default.svc  
    namespace: default        # 앱이 배포될 네임스페이스
  syncPolicy:
    automated:
      prune: true             # Git에서 제거된 리소스를 클러스터에서도 삭제
      selfHeal: true          # 클러스터 상태가 Git과 다르면 자동 복구

./apps/argocd-eso.yaml

  • External Secret Operator Helm Chart를 ArgoCD에 등록
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets
  namespace: argocd
  finalizers:                                  
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://charts.external-secrets.io
    chart: external-secrets
    targetRevision: 0.16.1
  destination:
    server: https://kubernetes.default.svc
    namespace: external-secrets-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true      # 네임스페이스 자동 생성

root-app.yaml

  • 다른 Application들을 관리하는 상위 Application
    (Child App은 spec.destination.namespace에서 별도로 정의)
# root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
  finalizers:                                  
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/endermaru/spring-app-gitops
    targetRevision: main
    path: apps                 # 여러 Child apps의 yaml들이 있는 디렉토리
    directory:
      recurse: false           # apps/ 디렉토리만 읽고 하위 디렉토리는 무시
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd          # root-app 자체는 argocd에 생성
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

2-5. ArgoCD로 등록

  • root-app.yaml에 기반하여 설치됨 - 앞으로 root-app.yaml만 있어도 배포 가능
  • helm 내부의 yaml 템플릿 파일이 아닌 git 저장소의 template을 참조
  • 앱 설치 후 minikube service spring으로 앱 접근 가능
# git push
$ git add .
$ git commit -m "init"
$ git push origin main

$ kubectl apply -f root-app.yaml

# 앱 확인 - 설치 & 실행에 시간이 걸릴 수 있음
$ kubectl get application -n argocd
NAME               SYNC STATUS   HEALTH STATUS
external-secrets   Synced        Healthy
root-app           Synced        Healthy
spring-app         Synced        Progressing   
# LoadBalancer인 서비스가 실제로는 NodePort로 작동 = ExternalIP가 Pending

spring-app의 Health Status가 Progressing인 이유

  • 일반적인 Kubernetes에서는
    • type: LoadBalancer 서비스가 클라우드 로드밸런서 (예: AWS ELB)로 연결
    • 클러스터 밖에서도 EXTERNAL-IP로 접근 가능
  • Minikube는 로컬 클러스터이기 때문에
    • 실제 LoadBalancer가 없음,EXTERNAL-IP: <pending> 상태
    • 외부에서 접근할 수 있는 포트가 열리지 않음

2-6. ArgoCD Web UI

비밀번호를 조회 & 웹 UI 열기

# 비밀번호 조회
$ kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
Pe6-LrrcQS3om6o5

# 로컬 머신의 localhost:8080 ↔ Argo CD 서버의 443(HTTPS) 포트를 연결
# http://localhost:8080로 Argo CD 웹 UI 접속 가능
$ kubectl port-forward svc/argocd-server -n argocd 8080:443
# 해당 터미널이 열려 있어야 함
  • http://localhost:8080로 Argo CD web ui 접속
    • username: admin
    • password: 위에서 조회한 비밀번호(Pe6-LrrcQS3om6o5)

확인할 수 있는 것들

(1) Application 상태 개요

  • ArgoCD에 등록된 각 Application의 상태:
    • 🔵 Synced / OutOfSync
    • 🟢 Healthy / Degraded / Missing
  • 최근 배포 로그, Git revision

(2) 리소스 트리 구조 시각화

  • 리소스 간 의존성과 연결 구조가 시각적으로 나타남

(3) 수동 또는 자동 동기화 트리거

  • OutOfSync 상태일 때 클릭 한 번으로 Sync 가능
  • "Sync → Apply" 과정이 Web UI에서 실시간으로 표시됨

(4) History & Rollback

  • 배포 이력 관리
  • 특정 시점으로 되돌리기 (rollback) 가능

2-7. 리소스 정리

# 1. 앱, 리소스 삭제 
$ kubectl delete application root-app -n argocd

# 2. Argo CD 리소스 삭제 
$ kubectl delete ns argocd

# 3. Minikube 정지 + 삭제(optional)
minikube stop
minikube delete

ArgoCD 배포가 완료된 프로젝트 레포

0개의 댓글