클라우드 네이티브 프로그래밍을 위한 쿠버네티스 개발 전략

2024년 8월 4일

클라우드 네이티브 프로그래밍을 위한 쿠버네티스 개발 전략 표지

클라우드 네이티브 프로그래밍을 위한 쿠버네티스 개발 전략 책을 읽고 정리한 내용입니다.


1장: 애플리케이션을 빈틈없이 실행하고 우아하게 종료하기

쿠버네티스에서 실행되는 애플리케이션은 pod이고, 이는 배포 가능한 가장 작은 단위이다.

쿠버네티스 파드의 생명주기

  • 워커 노드: 파드 단위로 포장된 애플리케이션이 실제로 올라가는 노드
  • 마스터 노드: kubectl apply와 같은 명령을 전달받아 적당한 워커 노드에 파드를 올리고 그 상태를 관리하는 노드
  • 워커 노드 + 마스터 노드 = 쿠버네티스 클러스터
  1. 마스터 노드에 파드를 생성해달라는 요청을 전달
  2. 마스터 노드의 쿠버네티스 스케줄러가 파드를 실행하기 적당한 워커 노드를 선택
  3. 각 워커 노드에서 파드 관리를 담당하는 에이전트인 kubelet에 파드의 명세를 전달
  4. kubelet은 전달받은 명세에 기술된 컨테이너 이미지를 다운로드 한 뒤 워커 노드에서 실행
  5. 정상적으로 이루어지면 쿠버네티스는 파드를 running 상태로 표시

파드의 실행이 실패했을 경우

  • 이미지를 받아오지 못한 경우 파드의 상태를 ErrImagePull ⇒ ImagePullBackOff 상태로 전환하고 이미지를 다시 받아오려고 시도한다
  • 문제가 해결될때까지 시도하다 성공하면 파드를 Running 상태로 표시

파드의 단계: Running, Pending, Succeeded, Failed, Unknown

파드가 중간에 종료되었을 경우

예상치 못한 오류로 파드의 실행이 중단되면 해당 파드를 재실행한다

파드가 중단되었을 때 어떻게 대처할지는 restartPolicy에서 정의한다 (Always, OnFailure, Never)

프로브

쿠버내티스는 파드의 상태는 확인할 수 있지만 파드 내부에서 실행되는 애플리케이션의 상태를 완벽하게 알 수는 없다. 애플리케이션의 상태를 확인하는 방법을 프로브(probe)라 한다

  • 활성 프로브: 컨테이너가 실행하는 애플리케이션이 정상인지 판단. 주로 실행 중 문제가 발생항 애플리케이션을 확인하기 위해 사용
  • 준비성 프로브: 컨테이너 내의 애플리케이션이 서비스를 시작할 준비가 되었는지 판단. 준비 완료된 애플리케이션을 네트워크에 연결해 주기 위해 사용
  • 스타트업 프로브: 애플리케이션이 주어진 시간 내에 시작했는지를 판단

파드가 워커 노드에 배치되어 실행이 완료되면 쿠버네티스는 스타트업 프로브로 애필리케이션이 정해진 시간 내에 준비되었는지 확인하고, 준비성 프로브와 활성 프로브가 동작한다

활성 프로브

정해진 시간에 한번씩 api/healthcheck 과 같은 api로 애플리케이션이 정상인지 판단

모든 미들웨어의 상태를 확인한다기보다 주 서버 프로세스가 정말 살아있는지, 응답을 보낼 여력이 있는지 확인한다

준비성 프로브

쿠버네티스는 준비성 프로브의 체그가 성공하기 전까지는 해당 파드를 네트워크 연결하지 않고, 외부 요청을 라우팅하지도 않는다.

의존성이 있는 아키텍처라면 각 서비스들의 활성 프로브 api를 호출해서 내 애플리케이션의 준비성 여부를 판단할 수 있다

애플리케이션을 우아하게 종료하기

쿠버네티스에서 파드를 삭제하는데 꽤 오랜 시간이 걸린다

  1. 쿠버네티스 스케줄러가 파드에 대한 종료 명령을 받고 파드의 상태를 Terminating 상태로 표시한다
  2. 파드에 더 이상 요청이 들어가기 않도록 서비스 대상에서 제외한다
  3. 파드에 실행된 모든 컨테이너에 SIGTERM(-15) 신호를 보낸다
  4. 모든 컨테이너가 해당 신호를 받고 프로세스를 스스로 종료하면 파드를 종료한다
  5. 프로세스가 종료되지 않으면 30초 기다린다
  6. 30초가 지나도 종료되지 않으면 SIGKILL(-9) 신호를 보내 강제 종료시킨다

SIGTERM 신호를 받은 시점에 사용중인 자원을 정리하고 애플리케이션을 정산 종료하게 해주어야 한다

process.on('SIGTERM', () => { 사용 중인 리소스 정리 })

2장. 디플로이먼트를 이용해 애플리케이션을 중단 없이 업데이트하기

대부분의 애플리케이션은 인스턴스를 두 개 이상 실행시켜서 가용성을 확보한다

레플리카셋: 같은 명세를 가진 다수의 파드를 실행할 수 있도록 관리해주는 오브젝트

디플로이먼트: 레플리카 셋은 파드의 숫자만을 관리. 애플리케이션의 업데이트와 같이 파드의 명세가 바뀌는 경우를 지원

디플로이먼트 > 레플리카셋 > 파드

상태 유지 원리

replicas로 몇개의 파드를 유지할지 정의할 수 있다

리컨실레이션 루프: 사전에 정의한 명세에 현재 상태를 항상 일치시키려는 동작

파드는 죽었는데 프로세스는 계속 살아있는 경우 디플로이먼트가 목표한 파드의 숫자와 현재 파드 숫자가 일치하여 파드를 새로 생성하지 않는다. 활성 프로브를 설정해 두어 쿠버네티스가 비정상인 파드를 감지할 수 있도록 하는 것이 필요하다

디플로이먼트를 이용하여 애플리케이션을 업데이트하기

컨테이너 이미지는 불변성을 가지는 것이 원칙이라 변경점이 생겼을 때는 기존 이미지를 덮었는 것이 아니라 새로 이미지를 만든다

디플로이먼트를 업데이트 할 때

  • 재생성 전략: 기존 파드를 모두 종료한 뒤 새 버전의 파드를 실행
    • 디플로이먼트의 strategy를 recreate로 명시
  • 롤링 업데이트: 파드를 순차적으로 종료하고 생성하면서 서비스가 중단되지 않도록 해주는 전략
    • 업데이트 도중에 여러 버전의 파드가 같이 서비스 하는 구간이 존재하여 부작용이 나타날 수 있다
    • 이전 버전의 응답을 모두 유지하면서 새로운 속성을 더하거나, 호출하는 경로나 헤더에 버전을 명시하는 방법을 사용
    • maxUnavailable: 롤링 업데이트가 진행되는 동안 제거할 수 있는 파드의 수 혹은 비율

생명주기 프로브를 이용한 안정적인 업데이트

롤링 업데이트가 완전히 배포되었음을 판단하는 순간은 준비성 프로브가 정상 응답을 보내는 때

롤링 업데이트 중 무중단 업데이트에 실패하는 경우

  • maxUnavailable이 100%일 경우
  • 요청을 전달했지만 파드가 이를 재대로 처리하지 못하는 경우

종료 절차에 들어간 파드는 이미 들어온 요청에 대해 응답을 모두 보낸 뒤 종료해야한다

새로 실행된 파드는 요청을 받을 준비가 되었을때부터 요청을 받아야 한다

즉 준비성 프로브가 준비되었을때 실행해야한다

디플로이먼트 업데이트 기록은 rollout history로 확인할 수 있다

애플리케이션을 이전 버전으로 돌리고 싶으면 rollout undo

롤백하는 과정은 애플리케이션을 업데이트 하는 과정과 비슷. 새로운 레플리카셋을 생성하지는 않지만 순차적으로 파드를 종료하고 생성하면서 이전 버전을 배포한다


3장. 애플리케이션의 스케일 조정하기

서버의 규모를 늘리는 법

  • scale up: 서버 자체의 성능을 높이기. 수직적 확장
  • scale out: 서버의 숫자를 늘리기. 수평적 확장
  • auto scaling: 수직, 수평적 확장을 상황에 따라 자동으로 수행해 주기

클라우드 환경은 가상의 자원을 할당받아서 사용하기 때문에 빠르고 간단하게 스케일을 조정할 수 있다

애플리케이션의 성능 측정하기

metric server를 설치하여 감당 가능한 수준의 작업을 처리하는지, 부하가 걸리고 있는지 확인을 해야한다

메트릭 서버를 설치하면 kubectl top 명령으로 파드의 자원 사용률을 확인할 수 있다

  • kubctl top nodes: 파드가 사용할 수 있는 CPU 및 메모리 사용량의 총합은 워커노드에 할당된 자원량을 넘을 수 없다

파드 자원 사용량 정의하고 스케일 업하기

파드는 별다른 설정이 없을 경우 자원 사용량에 제한을 두지 않는다

limit을 설정하지 않으면 노드의 자원을 모두 점유하여 다른 파드들이 실행되지 못하거나 종료될 수 있다

  • limits ≥ request, request를 정의하지 않는다면 암묵적으로 동일한 값이 된다

디플로이먼트를 수동으로 스케일 아웃하기

여러 파드가 나누어서 처리할 수 있다면 파드 숫자 자체를 늘리는 것이 효과적

replicas를 조절하여 파드를 추가 생성하도록 할 수 있고, kubectl 명령어를 이용해 파드의 숫자를 즉시 조절할 수도 있다

상황에 따라 자동으로 스케일 조정하기

autoscaler: 모니터링과 스케일리을 자동으로 해주는 오브젝트

HPA(Horizontal Pod Autoscaler): 자원 사용량에 다라 파드의 숫자를 조절해 주는 오브젝트

  • 지속적으로 파드의 자원 사용량을 모니터링하면서 최적의 파드 숫자를 결정하고, replicas를 조정하여 대응한다

목표하는 replicas = 현재 파드 수 * (현재 자원 사용량 / 목표 자원 사용량)


4장. 애플리케이션의 설정을 체계적으로 관리하기

하나의 이미지를 사용하면서 쿠버네티스의 파드 명세에서 직접 실행 인자(환경)을 정의해 주는 것이 효율적

  • 인자를 전달
    args: ["--spring.profiles.active=prod"]
    
  • 환경변수를 설정해 주기
    env:
    - name: SPRING_PROFILES_ACTIVE
    	vlaue: "prod"
    

애플리케이션에 직접적인 설정값을 전달할때는 인자를 전달하지만, 애플리케이션이 실행되는 환경과 관련된 값은 환경 변수를 사용한다

환경을 구분하기 위해 네임스페이스 사용하기

  • 하나의 쿠버네티스 클러스터에 다양한 환경을 구성하고 싶을때는 namespace를 사용
  • namespace: 클러스터에서 오브젝트를 논리적으로 구분해주는 단위

컨피그맵을 이용하여 여러 설정값 한번에 바꾸기

configMap 오브젝트: 딕셔너리 형태로 필요한 설정 값을 쿠버네티스 클러스터에 저장하고 파드에서 사용할 수 있도록 도와준다

컨피그맵을 파일로 참조하는 경우 컨피그맵의 내용이 변경되면 파드를 재시작하지 않아도 파일의 내용이 같이 변경된다

시크릿을 이용해 민감한 설정값 관리하기

컨피그맵은 평문으로 저장되기 때문에 보안에 중요한 값을 저장하기 적절하지 않음

secret: 설정값을 암호화하여 관리할 수 있는 오브젝트

kubectl create secret generic [앱명] —from-literal=비밀번호=’1234’ —from-literal=비밀번호2=’12345’

일반적인 kubectl 명령어로는 확인할 수 없다. 해당 값은 Base64 인코딩 되어 저장된다 kubectl get secret 앱명 -o yaml 명령어로 인코딩된 값을 찾을 수 있는데, 누구나 비밀번호를 확인할 수 있는 것처럼 보인다

  • 별로의 분리된 오브젝트로 관리: 오브젝트에 대한 접근 권한을 지정할 수 있다
  • 필요하다면 이 값을 암호화하여 사용할 수 있다

5장. 애플리케이션과 네트워크 연결하기

파드 내부의 컨테이너끼리 통신하기

  • 사이드카 패턴(sidecar pattern): 하나의 파드에 여러 컨테이너를 구성하는 경우, 파드의 목적에 맞는 주 컨테이너와 그 컨테이너에 종속된 보조 컨테이너를 구분하는 구성
  • 같은 파드에 속해 있는 컨테이너들은 물리적으로 같은 노드에서 실행된다 === 컨테이너는 저장공간과 네트워크 인터페이스를 공유한다 === 하나의 파드에 포함되어있는 컨테이너는 서로 다른 포트를 가져야 한다

파드와 파드 사이의 통신

  • 다른 파드를 호출하기 위한 IP주소와 포트 번호를 알기 어렵다
    • 서비스(service) 오브젝트: 같은 역할을 수행하는 여러개의 파드를 네트워크 공간에 노출시켜 내부의 다른 파드 혹은 외부에서 호출할 수 있도록 해주는 오브젝트
    • 파드가 다른 파드를 호출할때는 파드와 연결된 서비스를 호출
    • 서비스가 파드를 선택한다. 조건에 맞는 파드를 찾아내어 네트워크에 연결해준다
    • 클러스터 내부에서 다른 파드를 호출할때는 그 파드를 선택하고 있는 서비스의 이름으로 호출
      • 호출되는 파드는 라운드 로빈 방식으로 결정
      • 요청이 다른 파드에 도달하더라도 작업의 일관성에 문제가 없도록 stateless하게 만들어야한다

클러스터 외부에서 파드 호출

  • NodePort를 이용해 서비스 노출
    • NodePort 타입은 서비스를 노드의 특정 포트에 맵핑시켜 외부로 노출해준다
    • 따로 명시하지 않으면 쿠버네티스가 임의로 정해주고, 고정하고 싶다면 서비스를 정의할 때 nodePort 속성을 정의해주면 된다
    • 운영에서 사용하기 어려운 이유
      • 클러스터가 최대 2768개의 서비스를 노출할 수 있다는 제한
      • 직접 nodePort를 지정한다면 중복이 발생하지 않도록 관리해야하고, 임의 배정을 사용한다면 로드밸런서, 도메인 주소등을 배정해야함
  • 인그레스(Ingrees)
    • 서비스에 포함되지 않는 독립적인 오브젝트이지만 클러스터 외부에서 오는 트래픽을 규칙에 따라 분류하여 서비스에 연결해준다
    • 경로나 호스트를 기반으로 HTTP 요청을 제어

6장. 쿠버네티스의 저장소 활용하기

컨테이너 내부의 저장공간은 컨테이너가 종료될 때 같이 삭제된다

파드에 임시공간 확보하기

  • emptyDir: 파드 내부의 공간으로 임시 파일을 저장할 때 사용
  • 하나의 파드 내부에서 일회성으로 처리 가능한 요청에 대해서 사용

노드의 저장공간을 파드에서 활용하기

  • hostPath: 워커노드에 저장. 워커노드의 특정 디렉터리에 저장
  • 노드에게 영향을 주지 않는, 시스템의 동작에 영향을 주지 않는 독립적인 공간을 지정해야
  • 하나의 노드만 사용한느 경우는 유용하지만, 여러 노드를 사용한다면 파드가 다른 노드에서 실행되어 디렉터리의 내용이 의도와 달리 영속성을 가지지 못할수도
  • 모든 노드에 존재하는 특정한 파일을 컨테이너에서 참조하고자 할때 유용

퍼시스턴트 볼륨을 이용한 정적 저장공간 할당

  • 파드가 필요한 볼륨을 요청하여 사용할 수 있도록 클러스터가 제공하는 저장공간
  • 쿠버네티스 클러스터 전체에 제공되는 추상화된 저장공간
  • 퍼시스턴트 볼륨 클레임: 필요한 저장공간의 속성을 정의하여 요청, 클레임이 삭제되더라도 데이터는 그대로 유지

스토리지 클래스를 활용한 동적 저장공간 할당

볼륨에 대한 수요를 미리 예측하지 않고서도 동적으로 할당할 수 있음

  • 동적 볼륨 프로비저닝: 블록 스토리지 서비스

7장. 쿠버네티스를 활용한 애플리케이션 개발 모범 사례

애플리케이션과 컨테이너의 로그 처리

  • 컨테이너가 종료되면 컨테이너 내부에 있는 파일이 사라진다 ⇒ 퍼시스턴트 볼륨에 저장하자
    • 여러개의 파드가 구동된다면? 퍼시스턴트 볼륨을 NFS로 정의
      • 자원을 공유하는 만큼 이름이 겹칠 수 있다
      • 파드가 많아지면 NFS의 입출력 성능이 로그의 기록 속도를 따라가지 못할 수 있다
  • 표준 출력(standard output)으로 내보내기
    • 로그를 저장하거나 전송하는 일은 애플리케이션 밖에서
    • 애플리케이션이 표준 출력으로 보낸 로그는 컨테이너 레벨에서 처리
  • 로그 관리 시스템 이용
    • 로그 수집기: 로그를 수집하여 필요한 형태로 가공해 로그 저장소로 전달, 애플리케이션과 같은 파드 내에 다른 컨테이너의 형태로 실행(사이드카 패턴)
      • Logstash, FluentD: 수집, 변형이 가능, 다양한 입출력 제공
      • FileBeat, FluentBit: 로그를 전달하는 역할에 집중한 가벼운 형태
    • 로그 저장소: 로그를 저장하고, 필요할때만 조회하고 검색할 수 있도록 해줌, 일래스틱서치(ElasticSearch), 오픈서치(OpenSearch)
    • 로그 시각화 도구: 저장된 로그를 조회하거나 검색하는 UI 제공, Kibana, Grafana, splunk

데이터베이스의 설치와 연결

이 부분은 DB에 대한 이해도가 떨어져서 짧게만,,,

  • 쿠버네티스에서 데이터베이스를 실행할 때 생기는 문제
    • 어떤 노드에서 실행되는지 알 수 없어 hostPath 저장소로 사용하기 어렵다
  • 데이터베이스를 별도의 호스트나 가상머신 혹은 관리형 데이터베이스 등 쿠버네티스와 무관한 위치에 배치한 후 내부 네트워크로 연결

애플리케이션의 세션 처리하기

세션: 애플리케이션 서버가 저장하고 관리하는 사용자 정보의 집합

  • sticky session: 클라이언트가 하나의 서버하고만 요청과 응답을 주고받을 수 있도록 요청 경로를 고정. 사용자에 따라 전달해야 할 서버를 고정
    • session affinity: 쿠버네티스에서 스티키 세션을 제공하는 서비스
      • 파드 숫자가 달라질때 대응하기 어렵다 ⇒ 쿠버네티스의 장점을 포기해야
      • ip 기준으로 요청을 분산하면 부하가 균형 있게 분산되기 어렵다
  • 세션 저장소
    • 애플리케이션 레벨에서 세션을 공유, spring session과 redis를 조합하여 설정

네임스페이스를 이용하여 개발환경 구분하기

namespace: 애플리케이션을 구분하여 관리할 수 있도록, 파드, 디플로이먼트, 서비스 등 애플리케이션을 구동하는 데 필요한 쿠버네티스 오브젝트를 논리적으로 구분해 주는 단위

  • A 네임스페이스에 있는 오브젝트, 서비스는 B 네임스페이스에 있는 파드와 상호작용 불가

8장. 쿠버네티스 기반 배치 프로그램의 실행과 관리

  • job: 파드가 어떤 작업을 수행한 뒤 종료될 수 있도록 해주는 오브젝트. 파드가 성공적으로 종료하는 것을 목표
    • 작업을 실패하면 시간을 배수로 늘려가며 재실행
    • 따로 명시하지 않으면 최대 6번 실행
    • backoffLimit: 횟수를 제한
    • activeDeadLineSecond: 시간으로 제한
    • ttlSecondsAfterFinished: 잡이 종료 후 삭제될 수 있도록 설정
  • 배치 프로그램을 병렬로 실행
    • completions: 배치 프로그램이 n번 성공할때까지 실행
    • parallelism: 파드를 병렬로 실행(몇개를 병렬로 실행할지)
  • 주기에 맞추어 배치 프로그램 실행
    • cronJob: 일정한 주기에 맞춰 잡을 실행
    • cronSchedule 규격에 맞춰 실행해줌

9장. 애플리케이션을 쿠버네티스에 배포하기

직접 애플리케이션을 배포하고 관리

  • kubectl apply로 클러스터에 적용
  • yaml 파일을 git으로 공유하여 유지

커스터마이즈를 이용해 환경별 오브젝트 관리

  • 커스터마이즈: 명세에 공통이 되는 부분과 환경별로 달라지는 부분을 분리하여 관리할 수 있게 해준다
  • 디플로이 관련 yaml, kustomization.yaml을 base라는 디렉터리에 모아두는 것이 관례
  • dev.env 파일과 같은 환경변수는 컨피그맵 제너레이터 설정으로 컨피그맵으로 전환할 수 있다
  • 각 환경에 맞춰 오브젝트를 생성하고 관리 할 수 있음

헬름을 이용하여 복잡한 배포 환경에 대응하기

  • 애플리케이션의 버전이 올라가거나 패치가 추가되었을 때 대응이 어려움
  • 애플리케이션을 실행하는 형태가 바뀌었을 때 명세도 바뀌어야하는데 이를 처리하기 어려움
  • 쿠버네티스의 패키지 매니저가 헬름(helm)
brew install helm
helm search repo [레포명]
helm install [어디에] [레포명] --namespace dev

애플리케이션을 헬름 차트로 만들어서 배포

helm create

  • chart.yaml: 차트의 메타데이터를 가지고 있는 파일. 차트의 이름, 버전 설명등을 포함
  • templates: 차트를 만들기 위한 템플릿 파일이 들어 있는 디렉터리
  • values.yaml: 템플릿 파일과 결합하여 실제로 쿠버네티스 오브젝트 명세를 만들 때 사용하는 값들을 담고 있는 파일
  • charts: 차트를 실행하기 위해서 다른 차트(서브차트)에 의존성을 가지는 경우 여기에 저장

차트를 수정해주면 chart.yaml의 버전을 바꿔줘야한다


생각보다 얇은 책이지만 매우 알차게 정리를 잘해둔 책이었다. 쿠버네티스의 기본적인 단어들을 한번 들어본 분들에게 추천. 예시도 적절하게 담겨있었고, 설명도 이해가 잘되어서 좋았다. 이 책을 추천해준 ㅈㅂㅁ양의 사수분에게 감사인사를 회사에서 노션 못쓰게 해서 개발자스럽게 올려볼려고 한다ㅠ 회사 깃헙에 올린건 어케될지 모르긴까..

Designed by prefer2 © 2023 All rights reserved