카테고리 없음

[Infra] 쿠버네티스 적용기(Feat. Helm)

처누 2025. 4. 13. 17:15

 

이번 프로젝트에서 도커로 운영하던 서버를 쿠버네티스로 전환하여 서버를 운영하기로 했다.

 

현재 프로젝트의 상황은 아래와 같다.

  • 메인 EC2
    • nginx, springboot, jenkins
    • prometheus, promtail, loki, grafana
    • kafka 
  • 서브 EC2(프리 티어)
    • Config Server
    • Sonarqube
    • Rabbitmq

쿠버네티스로 전환하기 위해 사용할 수 있는 스펙을 가진 서버는 메인 EC2 서버 한 대 였기 때문에 단일 노드(마스터 + 워커) 클러스터로 구성하기로 했다.

 

쿠버네티스는 원래 분산, 복제, 무중단 업데이트를 목표로 설계된 것이기 때문에 단일 노드로는 그 장점을 살릴 수 없었다. 그래도 최대한 단일 노드 클러스터를 극한까지 활용하는 방향으로 구축하려 했다.

 

대략적인 다이어그램은 아래와 같다.

ChatGPT를 통해 생성한 다이어그램

 

  1. Helm Chart를 통한 배포 자동화
  2. ArgoCD를 통한 GitOps 자동화
  3. Deployment.yaml에서 RollingUpdate 설정
  4. Pod Health Check(livenessProbe, readinessProbe)
  5. PodDisruptionBudget(PDB) 설정
  6. HPA 설정
  7. Ingress + SSL
  8. 메트릭과 로그 수집 및 모니터링
  9. Alert 시스템
  10. 자동 백업 스크립트

쿠버네티스 설치

 

Download Kubernetes

Kubernetes ships binaries for each component as well as a standard set of client applications to bootstrap or interact with a cluster. Components like the API server are capable of running within container images inside of a cluster. Those components are a

kubernetes.io

 

1. 사전 설정

  • 컨테이너 네트워크 간 통신 (Pod <-> Pod)
  • 서비스 라우팅 (ClusterIP, NodePort 등)
  • iptables 정책을 브릿지 네트워크에도 적용
# 사전 설정 (필수 커널 및 모듈 설정)
# 필요한 커널 모듈 로드
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay # 컨테이너 파일시스템에서 오버레이 파일 시스템을 지원 (Docker/Containered에서 기본)
br_netfilter # 브릿지 네트워크를 통한 패킷도 ptables에서 필터링을 할 수 있게 해줌 (쿠버네티스 Pod 네트워크 필수)
EOF

# 바로 커널에 로드, k8s.conf는 재부팅 후에도 자동 로드되도록 등록
sudo modprobe overlay
sudo modprobe br_netfilter

# 네트워크 관련 sysctl 설정
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1 # 브릿지 네트워크에서 전달되는 패킷도 iptables 규칙을 따르게 함(필수)
net.ipv4.ip_forward                 = 1 # 위와 같지만 IPv6용
net.bridge.bridge-nf-call-ip6tables = 1 # 리눅스가 라우터처럼 IP 포워딩을 할 수 있게 해줌(Pod <-> 외부 통신 위해 필수)
EOF

sudo sysctl --system

 

2. 쿠버네티스 설치( kubectl, kubeadm, kubelet)

  • kubectl : Kubernetes 클러스터를 관리/제어하는 CLI, 클러스터에 명령어 요청
  • kubeadm : Kubernetes 클러스터를 설치/초기화하는 CLI, 마스터 노드 초기화, 워커 노드 조인
  • kubelet : 각 노드에서 Pod를 실행하고 관리하는 에이전트, Pod 스케줄링, 실행 상태 체크, 리포트
# 쿠버네티스 설치(kubectl, kubeadm, kubelet)
# 키 등록 및 패키지 소스 추가
sudo curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/kubernetes.gpg
echo "deb [signed-by=/etc/apt/trusted.gpg.d/kubernetes.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list

# 설치
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

 

 

3. 쿠버네티스 초기화

이 과정에서 출력되는 kubeadm join ... 은 나중에 노드 추가할 때 필요(현재는 싱글 노드라 사용하지 않음)

# 쿠버네티스 초기화
# 마스터 노드 초기화 (flannel 네트워크 범위 사용)
sudo kubeadm init --pod-network-cidr=10.244.0.0/16

 

4. kubectl 클러스터 연결 설정

# 클러스터 연결 설정
mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

 

5. 단일 노드에서 워커 역할 허용

# 단일 노드에서 워커 역할 허용
kubectl taint nodes --all node-role.kubernetes.io/control-plane-

 

6. CNI 설치 (Calico)

# Calico 설치
curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml -O
kubectl apply -f calico.yaml

 

7. 설치 확인

# 클러스터 구축 확인
kubectl get nodes
kubectl get pods -A

파드를 조회해보니 위 오류가 계속 발생 

calico-kube-controllers와 corens가 pending상태!! -> 스케줄러가 Pod를 배치하지 못하고 있음.

 

아래 링크를 통해 해결했다!!

https://stackoverflow.com/questions/72844687/invalid-capacity-0-on-image-filesystem-lens-id-kubernetes

 

invalid capacity 0 on image filesystem, Lens ID Kubernetes

I am creating k8s cluster from digital ocean but every time I am getting same warning after I create cluster and open that cluster in lens ID. Here is the screenshot of warning: I tried every solu...

stackoverflow.com

 

containerd가 /var/lib/containerd 디스크 상태를 kubelet에 제대로 넘겨주지 못하고 있었는데,
containerd와 kubelet을 둘 다 재시작하니까 정보가 다시 동기화되어 해결함.

kubelet이 디스크 용량을 정확히 인식 못 했던 게 문제였고, 재시작으로 그 캐시가 깨끗하게 초기화되면서 문제가 해결됨!!

 

 


 

쿠버네티스에서의 배포는 원래 YAML 파일이 수십~수백 개씩 생기고 복잡해진다.

이걸 각각 작성하고, 수정하고, 환경별로 나누고 배포하기에는 너무 복잡해지기 떄문에 Helm을 사용하여 관리한다.

 

Helm은 이런 모든 리소스를 하나의 Chart로 묶어서 관리해주는 도구!!

 

Helm의 장점

  1. 배포 자동화 : 하나의 명령어로 여러 리소스를 한 번에 배포
  2. 패키징 : 앱을 Chart로 만들어 어디서든 재사용 가능
  3. 환경별 분리 : dev, staging, prod에 맞는 values.yaml 적용 가능
  4. 업데이트 및 롤백 : 버전별 관리, helm rollback으로 복구 가능
  5. 템플릿화 : YAML을 템플릿 {{ }}으로 만들어 유연한 배포 가능

Helm Chart 주요 파일

파일명 용도 상세 기능
Chart.yaml Chart 메타데이터 정의 Chart 이름, 버전, 설정, 의존성을 설정하는 파일. Helm이 이걸 읽고 Chart를 인식함.
values.yaml 기본 설정값 모음 템플릿 파일들이 참조할 기본 변수값을 정의. 환경(dev/prod)별로 override 가능.
deployment.yaml Kubernetes Deployment 생성 템플릿 앱(Pod)을 배포하는 핵심 리소스. replica 수, container image, 롤링 업데이트 전략 등을 정의.
statefulset.yaml 상태를 가지는 애플리케이션 배포 리소스 Pod의 이름, 순서, 볼륨을 고정해서 관리해야 할 때 사용. 데이터가 필요한 앱을 Pod 이름 + 스토리지 고정해서 관리하려고 사용.
service.yaml Kubernetes Service 생성 템플릿 Pod들을 외부에 노출하거나, ClusterIP/LoadBalancer/NLB 형태로 통신 가능하게 해줌.
ingress.yaml Kubernetes Ingress 생성 템플릿(선택) HTTP/HTTPS 요청을 Service로 라우팅. 도메인 기반 접근 설정 가능.
configmap.yaml Kubernetes ConfigMMap 생성 템플릿(선택) 앱 설정 파일, 환경변수처럼 비밀이 아닌 설정 데이터를 저장하는 리소스.
secret.yaml Kubernetes Secret 생성 템플릿(선택) 민감 데이터(비밀번호, 인증키)를 저장하는 리소스. base64 인코딩된 형태로 저장.
hpa.yaml Horizontal Pod Autoscaler 생성 템플릿(선택) CPU 사용량 기반으로 Pod 수를 자도응로 늘리거나 줄이는 리소스.
serviceaccount.yaml ServiceAccount 생성 템플릿(선택) Pod가 사용할 Kubernetes API 접근 권한을 설정. 보안상 필요할 때 생성.
role/rolebinding.yaml RBAC 권한 설정 템플릿(선택) ServiceAccount에 필요한 권한을 부여할 때 사용. Role과 RoleBinding 리소스를 생성.
pvc.yaml PersistentVolumeClaim 생성 템플릿(선택) 앱이 데이터를 저장할 수 있도록 외부 저장소를 요청하는 리소스. EBS, NFS 등에 연결할 때 필요.
tests/디렉토리 안 파일 Chart 설치 후 테스트하는 Job 리소스 Helm의 helm test 명령어로 실행할 수 있는 간단한 테스트 케이스들 작성.
_helpers.tpl 템플릿 헬퍼 함수 모음 파일 이름 자동 생성(Name Templates), Label 공통화 등 DRY(Don't Repeat Yourself)하게 만드는 도우미 함수 파일.

 

해당 프로젝트에서는 Spring Cloud Config를 사용하여 환경변수를 관리하고 있었기 때문에 Helm에서는 Deployment, Service 등만 관리하도록 하면됐다.


Helm 설치

 

헬름 설치하기

헬름 설치하고 작동하는 방법 배우기.

helm.sh

 

$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh

 

설치 확인

helm version

 

Spring Cloud Config 설정은 아래와 같이 하면 된다.

Secret 생성

kubectl create secret generic config-server-credentials \
  --from-literal=username=config_server_username\
  --from-literal=password=config_server_password

 

values.yaml 일부

# 환경별로 Secret 이름만 바꾸면 되도록 분리
secretName: config-server-credentials
env:
  - name: SPRING_PROFILES_ACTIVE
    value: dev

  - name: SPRING_CONFIG_IMPORT
    value: optional:configserver:http://configserver가 올라간 ec2 Public IP:포트번호

  - name: CONFIG_USERNAME
    valueFrom:
      secretKeyRef:
        name: config-server-credentials
        key: username

  - name: CONFIG_PASSWORD
    valueFrom:
      secretKeyRef:
        name: config-server-credentials
        key: password

 

ingress 설정 일부

ingress:
  tls:
    - host:
        - 도메인
      secretName: sercret_name

 

도메인을 SSL/TLS 등록을 해놨다면 Secret 등록 및 ingress 설정에서 위와 같은 설정을 해줘야한다.

 

TLS Secret 생성

 sudo kubectl create secret tls <SECRET_NAME> \
    --cert=<CERT_PATH> \
    --key=<KEY_PATH> \
    -n <namespace>

 

namespace를 추가할 때마다 위 명령어로 매번 secret을 생성하는 것이 번거롭다고 생각했고, 스크립트를 만들어서 namespace만 추가하면 간단한 명령어로 secret을 생성할 수 있도록 했다.

 

create-tls.sh

CERT_PATH=<fullchain.pem 디렉토리 경로>
KEY_PATH=<privkey.pem 디렉토리 경로>
SECRET_NAME=<secret_name>

NAMESPACES=("추가할 namespace1" "namespace2" "namespace3" ...)

for ns in "${NAMESPACES[@]}"; do
  
  if ! kubectl get ns "$ns" > /dev/null 2>&1; then
    kubectl create namespace "$ns"
  fi

  kubectl delete secret "$SECRET_NAME" -n "$ns" --ignore-not-found

  sudo kubectl create secret tls "$SECRET_NAME" \
    --cert="$CERT_PATH" \
    --key="$KEY_PATH" \
    -n "$ns"

done

 

또한, 도커 허브에서 이미지를 불러오는데 해당 프로젝트에 대한 도커 허브 레포지토리는 private으로 설정해놨기 때문에 로그인 정보가 필요했다.

GitHub를 통해 Docker Hub로 로그인을 해서 Git 계정에 대한 Access Token이 필요!!

 

Docker Hub Access

kubectl create secret docker-registry docker-hub \
  --docker-username=<GitHub Username> \
  --docker-password=<GitHub personal Access Token> \
  --docker-email=<GitHub Email> \
  -n <Docker Hub에 접근할 namespace>

 

backend helm-chart/values.yaml

replicaCount: 2

image:
  repository: Docker Hub Repository
  tag: Image tag
  pullPolicy: IfNotPresent

service:
  type: ClusterIP/NodePort/...
  port: 8080

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

serviceAccount:
  create: true
  name: ""

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: 도메인
      paths:
        - path:
          pathType: Prefix/ImplementationSpecific/...
  tls:
    - host:
        - 도메인
      secretName: <secret_name>

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 5
  targetCPUUtilizationPercentage: 80

imagePullSecrets:
  - name: <Docker Hub 접송 관련해 생성한 ecrect_name>

containerPort: 8080

# 환경별로 Secret 이름만 바꾸면 되도록 분리
secretName: config-server-credentials
env:
  - name: SPRING_PROFILES_ACTIVE
    value: <Profile_name>

  - name: SPRING_CONFIG_IMPORT
    value: optional:configserver:http://configserver가 올라간 ec2 Public IP:포트번호

  - name: CONFIG_USERNAME
    valueFrom:
      secretKeyRef:
        name: config-server-credentials
        key: username

  - name: CONFIG_PASSWORD
    valueFrom:
      secretKeyRef:
        name: config-server-credentials
        key: password

 

나머지 YAML파일 설정은 배포 환경마다 다르기 때문에 스킵하도록 하겠음!!

파일 설정후 아래 명령어로 렌더링 잘 되는지 확인 및 배포!

# 렌더링 확은
helm template <namespace> <helm chart가 위치한 프로젝트 루트 디렉토리>

# 배포
helm upgrade --install <namespace> <helm chart가 위치한 프로젝트 루트 디렉토리>

 


참고:

https://etloveguitar.tistory.com/141

https://dev-scratch.tistory.com/177