[kubernetes] 쿠버네티스에서 사용되는 디자인 패턴을 알아보자 - (1)
※ 쿠버네티스에 대한 기본적인 개념을 알고 읽으시는 걸 추천드립니다
쿠버네티스는 어떤 방식으로 컨테이너를 띄우고, 관리할까? 쿠버네티스는 흔히 얘기하는 클라우드 네티이브 (cloud-native) 한 플랫폼입니다. 여기서 말하는 클라우드 네이티브는 아래와 같은 특징을 가지고 있습니다.
- "인프라 장애 및 변경에 항상 대처할 수 있다"
- "더 작고, 느슨하게 결합하여 독립적으로 배포해 릴리스 할 수 있다"
- "동적으로 확장되고 지속적으로 작동한다"
위의 특징들을 만족시키기 위해 쿠버네티스는 몇가지의 디자인 패턴을 가지고 있습니다.
- Foundational 패턴 (본 포스팅)
- Behavioral 패턴
- Structual 패턴
- Configuration 패턴
- Advanced 패턴
위 패턴들을 이용하여 쿠버네티스에서는 Deployment 를 관리하고, 사용자 정의 기반의 Custom Resource Defination (CRD) 도 관리할 수 있고, 어떻게 Pod 가 죽었을 때, 다시 띄울 수 있는지 등을 알 수 있습니다. 우선, 본 글에서는 첫번째 패턴인 Foundational 패턴에 대해서 알아보도록 하겠습니다.
첫번째 Foundational 패턴
Foundational 패턴 도 몇가지의 패턴들이 존재합니다.
1. predictable demands
간단하게 말하면, 컨테이너가 사용할 리소스 즉 런타임 의존적인 리소스(volume, configmap,..) 와 컴퓨팅 리소스 (memory, CPU) 를 미리 정의하여 올릴 수 있다는 뜻입니다. 많은 컨테이너들은 어플리케이션의 특징에 따라 각각 리소스를 얼마나 할당할 지 달라지게 됩니다. 이러한 상황에서, 컨테이너가 사용할 리소스를 미리 정의 할 수 있다면 얻을 수 있는 장점이 있습니다.
즉, "쿠버네티스가 이 컨테이너의 리소스를 보고 어떤 노드에 할당 할 지 결정할 수 있습니다. 이렇게 되면 다양한 어플리케이션이 존재할 때, 부하가 큰 어플리케이션과 부하가 작은 어플리케이션을 적절하게 배치하여 효율적으로 관리할 수 있습니다."
만약, 리소스가 정의되어 있지 않은 채로 시작된다면, 어플리케이션이 예상보다 많은 리소스를 사용하게 되었을때 같은 쿠버네티스 클러스터에 존재하는 다른 어플리케이션 들에게도 큰 영향을 끼치게 됩니다.
아래는 Pod 를 정의한 yaml 파일의 예시입니다. 아래처럼 resource 항목에 requests 와 limits 로 각각 cpu, memory 값을 설정할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: my-app-service
spec:
containers:
- image: my-registry/my-app-service
name: my-app-service
resources:
requests: # Initial resource request.
cpu: 100m
memory: 100Mi
limits: # Upper resource limit.
cpu: 200m
memory: 200Mi
※ Pod Priority
resource 의 requests, limits 에 설정된 값에 따라 pod 의 Quality of Service(QoS) 가 달라지게 됩니다. 크게, 3종류가 있는데 이 종류마다 Pod 의 priority 가 결정됩니다. 여기서 priority 는 만약 클러스터에 자원이 가득 차서 더이상 pod 가 못 뜨게 되는 상황일 때 어떤 pod 를 우선적으로 띄어야 할지를 결정하는 값입니다.
- Best-Effort: requests, limits 모두 설정되지 않은 경우 (가장 낮은 priority)
- Bustable: requests, limits 같지 않은 경우 (중간 priority)
- Guaranteed: requests, limits 이 같은 경우 (가장 높은 priority)
2 Declarative Deployment
흔히 말하는 선언적 배포입니다. 선언적 배포가 갖는 특징이 뭐가 있을까요?
바로 "컨테이너 스펙을 캡슐화 하여 자원을 관리하고, 선언된 스펙에 맞추어 자원을 일치시킨다" 입니다.
선언적 과 대응되는 용어는 명령형 입니다. 만약 api-server 라는 이름의 이미지를 2개의 서버에서 띄워야 할 경우, 선언형과 명령형은 어떤 차이가 있을까요? 간단하게 두 방식의 차이를 살펴보면 아래의 표와 같습니다.
선언형 | 명령형 |
$ cat spec.yaml image: api-server replica: 2 $ run spec.yaml |
server1@ubuntu$ docker run api-server server2@ubuntu$ docker run api-server |
(위 표의 명령어와 실행 방식은 실제로 존재하지 않는 동작 방식이고, 하나의 예일 뿐입니다.)
선언형으로 배포하게 된다면, 내가 원하는 스펙을 하나의 파일 형식으로 정의하여 넘겨주게 됩니다. 그러면 쿠버네티스 같은 프로비저닝 툴이 해당 스펙을 보고 현재 상황과 비교해서 필요한 작업을 수행하게 됩니다. 하지만 명령형으로 갈 경우, 직접 서버 하나씩 들어가서 api-server를 실행시켜 줘야 합니다. 그리고, 어딘가에 문서화 시켜놓지 않는이상 어떤 이미지를 몇 대 실행시켰는지 직접 확인해봐야 알 수 있죠.
하지만, 선언형으로 관리하게 된다면 하나의 파일로 캡슐화 하여 관리하기 때문에, 현재 어떤 상태인지, 그리고 그 파일들의 히스토리를 저장해 둔다면 과거 배포 이력 까지 확인할 수 있습니다.
쿠버네티스는 이러한 선언적 배포를 이용하여 rolling, blue-green, canary 등의 배포 방식을 지원합니다.
3. Health Probe
쿠버네티스는 실행중인 pod 에 오류가 생겼을 시, 자동으로 복구 시키거나, 계속 재 생성을 시도하는 로직을 갖고 있는 것을 알고 있을 것입니다. (deployment, replicaset 동작 방식 참고) 어떻게 오류가 생겼다는걸 감지할 수 있을까요, 바로 이 Health Probe (정상상태 점검) 때문입니다.
쿠버네티스는 Health Probe를 통해
- 어플리케이션이 실행 되었는지
- 요청을 처리할 수 있는 상황인지
확인할 수 있습니다.
위 Health Probe를 제공하기 위해 "liveness probe" , "readiness probe" 두가지 방식을 제공합니다. 그리고 이 두가지 방식은 사용자가 직접 입력할 수 있습니다.
예시.
apiVersion: v1
kind: Pod
metadata:
name: my-app-service
spec:
containers:
- image: my-registry/my-app-service
name: my-app-service
livenessProbe: // 어플리케이션이 살아있는지 확인하는 방법으로 httpGet이 정상적인가 확인하는 방법을 설정
httpGet:
path: /healthcheck
port: 8080
initialDelaySeconds: 30 // 첫 liveness probe 확인하기 까지 30초간 대기
readiness: // 어플리케이션이 요청을 받을 준비가 되었는지 확인하는 방법으로 아래 명령어 수행
exec:
command: ["stat", "/var/run/random-generator-ready"]
그냥 pod 가 비정상 종료됨을 감지하는 방법만 사용할 수도 있지만, 서비스는 정상적으로 실행되고 있지만, 내부 에러로 인해 요청이 안될 상황도 존재하기 때문에, 이런 상황에서 빠르게 감지하고 쿠버네티스가 복구시킬 수 있도록 liveness probe, readiness probe를 직접 설정할 수 있습니다.