쿠버네티스 소개
쿠버네티스 클러스터 이해
- 마스터 노드는 전체 쿠버네티스 시스템을 제어하고 관리하는 쿠버네티스 컨트롤 플레인을 실행한다
- 워커 노드는 실제 배포되는 컨테이너 어플리케이션을 실행한다
컨트롤 플레인
컨트롤 플레인은 클러스터를 제어하고 작동시킨다. 하나의 마스터 노드에서 실행되거나 여러 노드로 분할되고 복제돼 고가용성을 보장할 수 있는 여러 구성 요소로 구성된다.
- 쿠버네티스 API 서버는 사용자, 컨트롤 플레인 구성 요소와 통신한다.
- 스케줄러는 애플리케이션의 배포를 담당한다.(애플리케이션의 배포 가능한 각 구성 요소를 워크 노드에 할당)
- 컨트롤러 매니저는 구성 요소 복제본, 워커 노드 추적, 노드 장애 처리 등과 같은 크러스터단의 기능을 수행한다.
- Etcd는 클러스터 구성을 지속적으로 저장하는 신뢰할 수 있는 분산 데이터 저장소다.
노드
워커 노드는 컨테이너화된 애플리케이션을 실행하는 시스템이다.
- 컨테이너를 실행하는 도커, rkt 또는 다른 컨테이너 런타임
- API 서버와 통신하고 노드의 컨테이너를 관리하는 Kubelet
- 애플리케이션 구성 요소 간에 네트워크 트래픽을 로드밸런싱하는 쿠버네티스 서비스 프록시(kube-proxy)
쿠버네티스 사용의 장점
- 애플리케이션 배포의 단순화
- 쿠버네티스는 모든 워커 노드를 하나의 배포 플랫폼으로 제공하기 때문에 개발자 자체적으로 배포를 시작할 수 있으며 클러스터를 구성하는 서버에 관해 알 필요가 없다.
- 하드웨어 활용도 높이기
- 애플리케이션 실행 지시가 내려지면 애플리케이션의 리소스 요구 사항에 대한 디스크립션과 각 노드에서 사용 가능한 리소스에 따라 애플리케이션을 실행할 가장 적합한 노드를 선택할 수 있다.
- 상태 확인과 자가 치유
- 쿠버네티스는 애플리케이션 구성 요소와 애플리케이션이 구동 중인 노드를 모니터링하다가 노드 장애 발생 시 자동으로 애플리케이션을 다른 노드로 스케줄링한다.
- 오토스케일링
- 쿠버네티스는 각 애플리케이션에서 사용하는 리소스를 모니터링하고 각 애플리케이션의 실행 중인 인스턴스 수를 계속 조정하도록 지시할 수 있다. 클라우드 인프라에서 쿠버네티스가 실행 중인 경우 API를 통해 노드를 추가하면 배포된 애플리케이션의 부하에 따라 전체 클러스터 크기를 자동으로 확장하거나 축소할 수 있다.
쿠버네티스 첫 걸음
- 클러스터 노드 조회하기
- kubectl get nodes
- 오브젝트 세부 정보 가져오기
- kubectl describe node [node 명]
- CPU, 메모리, 시스템 정보, 노드에 실행 중인 컨테이너 등을 포함한 노드 상태를 보여준다
- 별칭 설정하기
- alias k=kubectl
- 애플리케이션 구동하기
- k run kubia --image=bepoz/kubia --port=8080
- bepoz/kubia는 nodesJs에 대한 이미지이다.
- Pod가 생성된다.
- 파드 조회하기
- k get pods
백그라운드에서 일어난 동작
kubectl 명령어를 실행하면 쿠버네티스의 API 서버로 REST HTTP 요청을 전달하고 클러스터에 새로운 파드를 생성한다. 그리고 스케줄러에 의해 워커 노드 중 하나에 스케줄링이 된다. 해당 워커 노드의 Kubelet은 파드가 스케줄링 됐다는 것을 보고 이미지가 로컬에 없기 때문에 도커에게 레지스트리에서 특정 이미지를 풀하도록 지시한다. 이미지를 다운로드한 후 도커는 컨테이너를 생성하고 실행한다.
애플리케이션 접근
팟은 자체 IP주소를 가지고 있지만 내부주소이므로 외부에서 접근이 불가능하다. 따라서 외부에서 접근을 할 수 있게끔 하려면 서비스를 통해 노출을 해야하는데 일반적인 서비스를 생성하면 이것은 클러스터 내부에서만 접근 가능하기 떄문에 이 경우에는 LoadBalancer 유형의 특별한 서비스를 생성해야 한다.
- k expose po kubia --type=LoadBalancer --name kubia-http
팟은 언제든 사라질 수 있고 새로운 팟으로 대체될 수 있다. 이때 새로운 내부 IP를 받기 때문에 팟의 IP를 이용해 접근하기에는 무리가 있다. 따라서 서비스를 이용해서 팟에 접근하는 것이다. 서비스는 팟이 어떤 IP를 갖는지 관계없이 연결을 해주기 떄문이다.
애플리케이션 수평 확장
먼저 deployment를 생성해주었다.
- k create deployment kubia --image=bepoz/kubia --port=8080
그 후 아래의 명령어를 사용하여 확장해주었다.
- k scale deployment kubia --replicas=3
애플리케이션이 실행 중인 노드 검사하기
- k get pods -o wide
- 팟이 실행 중인 노드까지 표기가 된다
- k describe pods [pod-name]
- 팟의 세부 정보를 살펴본다
파드: 쿠버네티스에서 컨테이너 실행
파드 소개
파드 이해하기
- 파드의 모든 컨테이너는 동일한 네트워크 네임스페이스와 와 UTS 네임스페이스 안에서 실행되기 때문에, 모든 컨테이너는 같은 호스트 이름과 네트워크 인터페이스를 공유한다.
- 그러나 대부분의 컨테이너 파일시스템은 컨테이너 이미지에서 나오기 때문에, 기본적인 파일시스템은 다른 컨테이너와 완전히 분리된다.
- 파드 안의 컨테이너가 동일한 네트워크 네임스페이스에서 실행되기 때문에, 동일한 IP 주소와 포트 공간을 공유한다. 따라서 동일한 파드 안 컨테이너에서 실행 중인 프로세스가 같은 포트 번호를 사용하지 않도록 주의해야 한다.
- 파드 안에 있는 모든 컨테이너는 동일한 루프백 네트워크 인터페이스를 갖기 때문에, 컨테이너들이 로컬호스트를 통해 서로 통신할 수 있다.
- 쿠버네티스 클러스터의 모든 파드는 하나의 플랙한 공유 네트워크 주소 공간에 상주하므로 모든 파드는 다른 파드의 IP 주소를 사용해 접근하는 것이 가능하다. 둘 사이에 어떠한 NAT도 존재하지 않고, 두 파드가 서로 네트워크 패킷을 보내면, 상대방의 실제 IP 주소를 패킷 안에 있는 출발지 IP 주소에서 찾을 수 있다.
- 동일한 파드에서 실행한 프로세스는 각 프로세스가 컨테이너 안에 캡슐화돼 있다는 점을 제외하면, 같은 물리적 혹은 가상머신에서 동작하는 것과 동일하다.
파드에서 컨테이너의 적절한 구성
파드는 상대적으로 가볍기 때문에 오버헤드 없이 필요한 만큼 파드를 가질 수 있다. 모든 것을 파드 하나에 넣는 대신에 애플리케이션을 여러 파드로 구성하고, 각 파드에는 밀접하게 관련 있는 구성 요소나 프로세스만 포함해야 한다.
- 다계층 애플리케이션을 여러 파드로 분할
- fe와 be가 같은 파드에 있따면, 항상 같은 노드에서 실행이 된다. 2개의 노드가 있고 파드가 하나가 있다면 워커 노드를 1개만 사용하게되고 나머지 1개는 활용되지 않고 버리게 될 것이다. 파드를 2개로 분리하면 쿠버네티스가 각각 다른 노드로 스케줄링해 인프라스트럭처의 활용도를 향상시킬 수 있다.
- 개별 확장이 가능하도록 여러 파드로 분할
- fe와 be 컨테이너를 한 파드에 두면 확장 시에 fe 컨테이너 2개와 be 컨테이너 2개를 갖게된다. fe와 be의 스케일링 요구 사항이 다르기 때문에 개별적으로 스케일링이 필요한 경우가 있기 때문에 별도 파드에 배포해야 한다.
- 파드에서 여러 컨테이너를 사용하는 경우
- 여러 컨테이너를 단일 파드에 넣는 경우도 있다. 로그 수집이나 통신 어댑터 등 지원 컨테이너의 경우가 대표적인 예이다.
- 파드에서 여러 컨테이너를 사용하는 경우 결정
- 컨테이너를 함께 실행해야 하는가, 혹은 서로 다른 호스트에서 실행할 수 있는가?
- 여러 컨테이너가 모여 하나의 구성 요소를 나타내는가, 혹은 개별적인 구성 요소인가?
- 컨테이너가 함께, 혹은 개별적으로 스케일링돼야 하는가?
YAML 또는 JSON 디스크립터로 파드 생성
앞서 사용했던 kubectl run
명령어 말고 JSON이나 YAML를 통해 생서이 가능하다.
기존에 생성한 파드의 정보를 kubectl get pod [pod name] -o yaml
를 통해 살펴보면 크게 아래와같이 나눌 수가 있다.
- 사용한 쿠버네티스 API 버전
- 쿠버네티스 오브젝트 유형
- 파드 메타데이터 (Metadata)
- 이름, 네임스페이스, 레이블 및 파드에 관한 기타 정보를 포함한다.
- 파드 정의/내용 (Spec)
- 파드 컨테이너 목록, 볼륨, 기타 데이터 등 파드 자체에 관해 실제 명세를 가진다.
- 파드와 그 안의 여러 컨테이너의 상세한 상태 (Status)
- 파드 상태, 각 컨테이너 설명과 상태, 파드 내부 IP, 기타 기본 정보 등 현재 실행 중인 파드에 관한 현재 정보를 포함한다.
파드 정의 yaml 작성하기
apiVersion: v1
kind: Pod // 오브젝트 종류는 파드
metadata:
name: kubia-manual // 파드 이름
spec:
containers:
- image: bepoz/kubia
name: kubia // 컨테이너 이름
ports:
- containerPort: 8080 // 애플리케이션이 수신하는 포트
protocol: TCP
컨테이너의 포트를 지정해둔 것은 단지 정보일 뿐이다. 생략되어도 다른 클라이언트가 포트를 통해 파드에 연결할 수 있는 여부에 영향을 미치지 않는다. 그러나 명시적으로 포트를 정의한다면, 클러스터를 사용하는 모든 사람이 파드에서 노출한 포트를 빠르게 볼 수 있다. 또한 포트를 명시적으로 정의하면 포트에 이름을 지정해 편리하게 사용할 수 있다.
위의 yml 파일을 이용해 생성을 하려면 kubectl create -f [yml name]
을 하면된다.kubectl get pods [pod name] -o yaml
을 통해 정의를 가져올 수 있다. yaml 대신 json 으로 입력하면 json 형식으로 출력이 된다.
애플리케이션 로그 보기
컨테이너화된 애플리케이션은 로그를 파일에 쓰기보다는 표준 출력과 표준 에러에 로그를 남기는 게 일반적이다. 이를 통해 사용자는 다른 애플리케이션 로그를 간단하고 동일한 방식으로 볼 수 있다.
컨테이너 런타임(여기서는 도커)은 이러한 스트림을 파일로 전달하고, kubectl logs [pod name]
을 이용해 컨테이너 로그를 가져올 수가 있다.
컨테이너 이름을 지정해 로그를 가져올 수도 있다. kubectl logs [pod name] -c [container name]
현재 존재하는 파드의 컨테이너 로그만 가져올 수 있다는 점에 유의해야 한다. 파드가 삭제되면 해당 로그도 같이 삭제된다.
파드가 삭제된 후에도 파드의 로그를 보기 위해서는 모든 로그를 중앙 저장소에 저장하는 클러스터 전체의 중앙집중식 로깅을 설정해야 한다.
파드에 요청 보내기
서비스를 거치지 않고 특정 파드와 대화하고 싶을 때 쿠버네티스는 해당 파드로 향하는 포트 포워딩을 구성해준다.
kubectl port-forward kubia-manual 8888:8080
이 명렁어다.
이 명령은 머신의 로컬 포트 8888을 kubia-manual 파드의 8080포트로 향하게 한다.
레이블을 이용한 파드 구성
파드가 많아짐에 따라 각 파드에 대해 개별적으로 작업을 수행하기보다 특정 그룹에 속한 모든 파드에 관해 한 번에 작업하기를 원할 것이다. 레이블을 통해 파드와 기타 다른 쿠버네티스 오브젝트의 조직화가 이뤄진다.
레이블 소개
레이블은 리소스에 첨부하는 키-값 쌍으로, 이 쌍은 레이블 셀렉터를 사용해 리소스를 선택할 때 활용된다.
위와 같이 레이블을 이용해 시스템 구조와 각 파드가 적합한 위치에 있는지 볼 수 있다.
파드를 생성할 때 레이블 지정
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual-v2
labels:
creation_method: manual
env: prod
spec:
containers:
- image: bepoz/kubia
name: kubia
ports:
- containerPort: 8080
protocol: TCP
위와 같이 labels 를 추가하고 k get pods --show-labels
명령어를 호출하면 레이블과 함께 파드의 정보가 출력된다.
k get pods -L creation-method,env
를 입력하면 특정 레이블의 값만 출력하게끔 할 수도 있다.
레이블 추가를 위해서는 kubectl label po kubia-manual creation_method=manual
와 같이 사용한다.
기존의 레이블을 변경하기 위해서는 위의 명령어 형식에 뒤에 --overwrite
를 사용하면 된다.
레이블 셀렉터를 이용한 파드 부분 집합 나열
레이블 셀렉터를 사용해 파드 나열
- 특정 레이블이 달린 파드 조회
k get po -l [label key]=[label value]
- 특정 레이블을 가지고 있지만 값은 무엇이든 상관 없는 파드 조회
k get po -l [label key]
- 특정 레이블을 가지고 있지 않은 파드 조회
k get po -l '![lable key]'
- 특정 레이블의 값이 아닌 파드 조회
k get po -l '[label key]!=[label value]'
- 특정 레이블의 값이 주어진 값 들에 속하는 파드 조회
k get po -l '[label key] in (label value들을 ','로 구분해서 쓰면 됨)'
- 특정 레이블의 값이 주어진 값 들에 속하지 않는 파드 조회
k get po -l '[lable key] notin (label value들)'
- 여러 레이블 기준을 둘 수도 있다.
k get po -l [label key]=[label name],[label key]=[label name]
레이블과 셀렉터를 이용해 파드 스케줄링 제한
생성된 파드는 워커 노드 전체에 무작위로 스케줄링 된다. 하지만 만약 어떤 워커 노드는 HDD이고 어떤 노드는 SDD 인 경우 특 정 파드를 한 그룹에 나머지 파드는 다른 그룹에 스케줄링되도록 할 수 있다. GPU 가속을 제공하는 노드에만 GPU 계산이 필요한 파드를 스케줄링하는 것도 좋은 예시일 것이다.
쿠버네티스의 전체적인 아이디어는 그 위에 실행되는 애플리케이션으로부터 실제 인프라스트럭처를 숨기는 것에 있기에 파드가 어떤 노드에 스케줄링돼야 하는지 구체적으로 지정하고 싶지는 않을 것이다. 그로 인해 애플리케이션이 인프라스트럭처에 결합되기 때문이다. 그러나 정확한 노드를 지정하는 대신 필요한 노드 요구 사항을 기술하고 쿠버네티스가 요구 사항을 만족하는 노드를 선택하도록 한다. 이는 노드 레이블과 레이블 셀렉터를 통해 할 수 있다.
워커 노드 분류에 레이블 사용
k label nodes [node name] [label key]=[label value]
로 노드에도 레이블을 부여할 수가 있다.
특정 노드에 파드 스케줄링
apiVersion: v1
kind: Pod
metadata:
name: kubia-conqueror
spec:
nodeSelector:
conqueror: bepoz
containers:
- image: bepoz/kubia
name: kubia
nodeSelector
속성을 줘서 conqueror: bepoz
레이블이 붙은 노드에 스케줄링 되게끔 했다.
파드에 어노테이션 달기
어노테이션은 키-값 쌍으로 레이블과 거의 비슷하지만 식별 정보를 갖지 않는다. 오브젝트를 묶을 수도 없고 어노테이션 셀렉터도 없다. 주로 도구들(tools)에서 사용된다. 쿠버네티스에서 새로운 기능을 추가할 때 흔히 사용된다. 일반적으로 새로운 기능의 알파 혹은 베타 버전은 API 오브젝트에 새로운 필드를 바로 도입하지 않는다. 필드 대신 어노테이션을 사용하고, 필요한 API 변경이 명확해지고 쿠버네티스 개발자가 이에 동의하면 새로운 필드가 도입된다.
어노테이션이 유용하게 사용되는 경우는 파드나 다른 API 오브젝트에 설명을 추가해 두는 것이다. 이렇게 하면 클러스터를 사용하는 모든 사람이 개별 오브젝트에 관한 정보를 신속하게 찾아볼 수 있다. 예를 들어 오브젝트를 만든 사람 이름을 어노테이션으로 지정해두면, 클러스터에서 작업하는 사람들이 좀 더 쉽게 협업할 수 있다.
오브젝트의 어노테이션 조회
k get po [pod name] -o yaml
로 yaml 파일을 확인해보자.k describe pod [pod name]
으로도 확인가능하다.
어노테이션 정보가 있는 것을 확인할 수가 있을 것이다.
레이블에는 짧은 데이터를, 그에 비해 어노테이션에는 상대적으로 큰 데이터를 넣을 수 있다 (총 256KB까지).
어노테이션 추가 및 수정
k annotate pod [pod name] [annotation key]=[annotation value]
변경을 원할 때에는 레이블 때와 마찬가지로 --overwrite
을 뒤에 붙여주면 된다.
네임스페이스를 사용한 리소스 그룹화
각 오브젝트는 여러 레이블을 가질 수 있기 때문에 오브젝트 그룹은 서로 겹쳐질 수 있다.
또한 클러스터에서 작업을 수행할 때 레이블 셀렉터를 명시적으로 지정하지 않으면 항상 모든 오브젝트를 보게 된다.
오브젝트를 겹치지 않는 그룹으로 분할하고자 할 때, 즉 한 번에 하나의 그룹 안에서만 작업하고 싶을 때에 네임스페이스로 그룹화한다.
네임스페이스의 필요성
여러 네임스페이스를 사용하면 많은 구성 요소를 가진 복잡한 시스템을 좀 더 작은 개별그룹으로 분리할 수 있다.
리소스 이름은 네임스페이스 안에서만 고유하면 된다. 서로 다른 두 네임스페이스는 동일한 이름의 리소스를 가질 수 있다.
대부분의 리소스 유형은 네임스페이스 안에 속하지만 일부는 그렇지 않다. 그 가운데 하나는 노드 리소스인데, 이 리소스는 전역이며 단일 네임스페이스에 얽매이지 않는다.
다른 네임스페이스와 파드 살펴보기
k get ns
를 하면 네임스페이스의 목록이 나열된다.
k get [object] -n [namespace]
를 입력하면 해당 네임스페이스에 속해있는 오브젝트를 확인할 수 있다.
네임스페이스 생성
apiVersion: v1
kind: Namespace
metadata:
name: custom-bepoz
이렇게 yml파일로 생성도 가능하지만 k create namespace [namespace 명]
으로도 생성이 가능하다.
다른 네임스페이스의 오브젝트 관리
metadata 섹션에 namespace: custom-bepoz
와 같은 항목을 넣거나,k create -f [yaml 파일] -n [namespace]
와 같이 네임스페이스를 지정해주는 방법을 사용하면 된다.
또는 k config set-context --current --namespace=[namespace 명]
명령어로 현재 컨텍스트를 default에서 본인이 원하는 namespace로 변경할 수 있다.
네임스페이스가 제공하는 격리 이해
네임스페이스를 사용하면 오브젝트를 별도 그룳으로 분리해 특정한 네임스페이스 안에 속한 리소스를 대상으로 작업할 수 있게 해주지만, 실행 중인 오브젝트에 대한 격리는 제공하지 않는다.
다른 네임스페이스를 가진 파드가 배포되어 있다고해서 네트워크 격리가 된 것은 아니다. 이것은 쿠버네티스와 함께 배포하는 네트워킹 솔루션에 따라 다르다. 격리가 제공되지 않는다면 foo 네임스페이스를 가진 파드에서 bar 네임스페이스를 가진 파드의 IP 주소로 HTTP 요청과 같은 트래픽을 다른 파드로 보내는 것에 아무런 제약 사항이 없다.
파드 중지와 제거
k delete po [pod name]
으로 파드를 삭제한다.
파드를 삭제하면 쿠버네티스는 파드 안에 있는 모든 컨테이너를 종료하도록 지시한다.
쿠버네티스는 SIGTERM 신호를 프로세스에 보내고 지정된 시간(기본 30초) 동안 기다린다. 시간 내에 종료되지 않으면 SIGKILL 신호를 통해 종료한다. 프로세스가 항상 정상적으로 종료되게 하기 위해서는 SIGTERM 신호를 올바르게 처리해야 한다.
k delete po [pod name1] [pod name2]
이렇게 파드 이름 중간에 공백을 두어 여러 파드를 삭제할 수도 있다.
k delete po -l [label name]=[label value]
와 같이 레이블 셀렉터를 이용해 삭제할 수 있다.
k delete ns [namespace]
로 네임스페이스 전체를(파드는 네임스페이스와 함께 자동으로 삭제된다) 삭제할 수 있다.
k delete po --all
명령어로 파드를 모두 삭제할 수 있다. 레플리카셋이 관리하고 있는 파드들이면 바로 재생성될 것이다.
k delete all --all
이 명령어로 현재 네임스페이스에 있는 모든 리소스를 삭제할 수 있다.
레플리케이션과 그 밖의 컨트롤러: 관리되는 파드 배포
파드를 안정적으로 유지하기
라이브니스 프로브
쿠버네티스는 라이브니스 프로브(liveness probe)를 통해 컨테이너가 살아 있는지 확인할 수 있다.
파드의 스펙에 각 컨테이너의 라이브니스 프로브를 지정할 수 있다. 쿠버네티스는 주기적으로 프로브를 실행하고 프로브가 실패한 경우 컨테이너를 다시 시작한다.
쿠버네티스는 3가지 메커니즘을 사용해 컨테이너에 프로브를 실행한다.
- HTTP GET 프로브는 지정한 IP 주소, 포트, 경로에 HTTP GET 요청을 수행한다. 프로브가 응답을 수신하고 응답 코드가 오류를 나타내지 않는 경우에 프로브가 성공했다고 간주된다. 응답을 하지 않거나 오류 응답 코드를 반환하면 프로브가 실패한 것으로 간주돼 컨테이너를 다시 시작한다.
- TCP 소켓 프로브는 컨테이너의 지정된 포트에 TCP 연결을 시도한다. 연결에 성공하면 프로브가 성공한 것이고, 그렇지 않으면 컨테이너가 다시 시작된다.
- Exec 프로브는 컨테이너 내의 임의의 명령을 실행하고 명령의 종료 상태 코드를 확인한다. 상태 코드가 0이면 프로브가 성공한 것이다. 모든 다른 코드는 실패로 간주된다.
HTTP 기반 라이브니스 프로브 생성
apiVersion: v1
kind: pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe: // Http GET을 수행하는 라이브니스 프로브
httpGet:
path: /
port: 8080 // 프로브가 연결해야 하는 네트워크 포트
이미지는 5번째 요청부터는 500을 반환하는 이미지이다.
쿠버네티스가 주기적으로 "/" 경로와 8080 포트에 HTTP GET을 요청해서 컨테이너가 정상 동작하는지 확인한다.
다섯 번의 요청 이후에는 500을 반환하는 이미지이므로 5번째 부터는 쿠버네티스가 프로브를 실패한 것으로 간주해 컨테이너를 다시 시작한다.
동작 중인 라이브니스 프로브 확인
파드 상태를 확인해보면 RESTARTS 횟수가 늘어나있는 것을 확인할 수가 있다.
3장에서 배웠듯이 k logs [pod name]
을 이용해서 파드의 로그를 확인할 수가 있다. 만약 이전 컨테이너의 로그를 보고 싶다면 k logs [pod name] --previous
를 입력하면 된다.
k describe po [pod-name]
을 통해 살펴보면 더욱 상세하게 확인할 수가 있다. 위 이미지의 경우 137 exit code를 반환하고 중지되었다고 나오는데 이는 128 + x를 더한 값으로 x는 프로세스에 전송된 시그널 번호이며 이 예제에서는 SIGKILL 시그널 번호인 9이며 프로세스가 강제로 종료됐음을 의미한다.
라이브니스 프로브의 추가 속성 설정
파드 describe를 살펴보면 Liveness: http-get http://:8080/ delay=0s timeout=1s period=10s #success=1 #failure=3
라이브니스 프로브에 관한 추가 정보도 표시되는 것을 확인할 수가 있다.
지연시간, 제한 시간, 기간이 나와있다.
delay 값이 0인데 컨테이너가 시작된 후 바로 프로브가 시작된다는 뜻이다.
timeout 값은 1인데 1초 안에 응답해야 한다는 뜻이고, period는 10초마다 수행된다는 뜻이다. failure가 3이므로 프로브가 3번 연속 실패하면 컨테이너가 다시 시작되게 된다.
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 15
다음과 같이 initialDelaySeconds
옵션을 주면 위에서 살펴본 delay 값이 15로 변경되는 것을 확인할 수가 있다.
애플리케이션 시작 시간을 고려해서 설정해주는 편이 좋다.
효과적인 라이브니스 프로브 생성
- 더 나은 라이브니스 프로브를 위해 특정 URL 경로에 요청하도록 프로브를 구성하자. 애플리케이션의 내부만 체크하도록 하자.
- 라이브니스 프로브는 너무 많은 연산 리소스를 사용해서는 안 되며, 완료하는 데 너무 오래 걸리지 않아야 한다.
- 프로브에 재시도 루프를 구현하지 마라.
레플리케이션컨트롤러 소개
레플리케이션 컨트롤러는 쿠버네티스 리소스로서 파드가 항상 실행되도록 보장한다. 노드가 사라지거나 노드에서 파드가 제거된 경우 레플리케이션컨트롤러는 사라진 파드를 감지해 교체 파드를 생성한다.
레플리케이션컨트롤러의 동작
특정 type의 실제 파드 수가 의도하는 수와 일치하는지 항상 확인한다. 파드가 너무 적게 실행 중이면 파드 템플릿에서 새 복제본을 만든다. 많은 파드가 실행 중이면 초과 복제본이 제거된다.
- 레이블 셀렉터는 레플리케이션컨트롤러의 범위에 있는 파드를 결정한다
- 레플리카 수는 실행할 파드의 의도하는 수를 지정한다
- 파드 템플릿은 새로운 파드 레플리카를 만들 때 사용된다
레이블 셀럭터와 파드 템플릿을 변경해도 기존 파드에는 영향을 미치지 않는다.
레이블 셀렉터를 변경하면 기존 파드가 레플리케이션컨트롤러의 범위를 벗어나므로 해당 파드에 대한 관리가 중지된다.
rc는 파드 생성 후 컨테이너 이미지, 환경변수 등에 신경을 쓰지 않으므로 템플릿은 rc로 새 파드를 생성할 때만 영향을 미친다.
레플리케이션컨트롤러 사용 시 이점
- 기존 파드가 사라지면 새 파드를 시작해 파드가 항상 실행되도록 한다
- 클러스터 노드에 장애가 발생하면 장애가 발생한 노드에서 실행 중인 모든 파드에 관한 교체 복제본이 생성된다
- 수동 또는 자동으로 파드를 쉽게 수평으로 확장할 수 있게 한다
레플리케이션컨트롤러 생성
apiVersion: v1
kind: ReplicationController // rc의 메니페스트 정의
metadata:
name: kubia // rc의 이름
spec:
replicas: 3 // 의도하는 파드 인스턴스 수
selector:
app: kubia // 파드 셀렉터로 rc가 관리하는 파드 선택
template: // 새 파드에 사용될 파드 템플릿
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080
레플리케이션 작동 확인
k delete po로 파드를 삭제해도 다시 재생성되는 것을 확인할 수가 있다.k get rc
로 레플리케이션컨트롤러를 확인할 수가 있다.
k describe rc [rc name]
을 하면 더 상세하게 확인할 수가 있다.
새로운 파드를 생성한 원인 정확히 이해하기
파드를 삭제했을 때에 파드를 재생성하는 것은, 파드를 삭제한 것에 대한 대응이 아니라 결과적인 상태에 대응하는 것이다.
삭제에 대한 통지 자체는 받지만 이 통지가 파드를 재생성하게 하는 것은 아니다. 파드 수를 확인하고 조치를 취하게 된다.
노드가 장애가 나도 개발자가 수동으로 애플리케이션을 다른 노드에 마이그레이션 할 필요 없이 쿠버네티스가 자동으로 수행해준다.
레플리케이션컨트롤러의 범위 안팎으로 파드 이동하기
rc는 레이블 셀렉터와 일치하는 파드만을 관리한다. 그렇다면 파드에 레이블이 추가되도 상관하지 않을까?
레이블을 추가했는데 이전과 큰 차이점이 없어보인다.
rc가 관리하던 레이블은 app=kubia
였다. 이 값을 변경을하니 파드의 수가 설정한 레플리카 인스턴스 수와 다르다는 것을 감지하고 새로운 파드를 생성해주었다.
만약 특정 파드가 오작동하기 대문에 새로운 파드로 교체를 원한다면 이렇게 레이블을 변경해서 rc의 관리범위에서 벗어나게끔 해서 처리할 수도 있다.
파드 템플릿 변경
rc의 파드 템플릿을 변경한다고 해도 기존의 파드에는 영향을 주지 않는다. 새롭게 생성되는 파드에만 영향을 준다.
따라서 기존 파드를 수정하려면 해당 파드를 삭제하고 rc가 새로운 템플릿을 이용해 파드를 생성하게끔 하면 된다.
k edit rc [rc name]
을 입력하면 rc를 편집할 수 있다.
수평 파드 스케일링
현재 3개의 인스턴스가 유지되고 있는데 이를 10개로 조정해보겠다.
k scale rc kubia --replicas=10
명령어를 입력하고 k edit rc
로 정의를 확인해보면 spec.replicas
가 변경되어있는 것을 확인할 수가 있을 것이다.
레플리케이션컨트롤러 삭제
k delete rc
를 이용하면 레플리케이션컨트롤러가 삭제된다. rc가 관리하던 파드 또한 삭제가 된다.--cascade=false
옵션을 같이 부여하면 파드는 삭제되지 않고 rc만 삭제하는 것이 가능하다.
rc가 삭제되고 파드만 남은 상태에서는 파드는 어디에도 속해 있지 않지만 새로운 rc를 작성해 다시 관리하는 것이 가능하다.
레플리케이션컨트롤러 대신 레플리카셋 사용하기
레플리카셋은 차세대 레플리케이션컨트롤러이다.
레플리카셋과 레플리케이션컨트롤러 비교
rc는 특정 레이블이 있는 파드만 매칭시킬 수 있지만 레플리카셋은 특정 레이블이 없는 파드나 레이블의 값과 상관없이 특정 레이블의 키를 갖는 파드를 매칭시킬 수 있다.
rc는 env=foo
의 레이블을 갖고있는 파드와 env=bar
의 레이블을 갖고있는 파드를 동시에 매칭시킬 수 없지만 rs는 가능하다.
레플리카셋 정의하기
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels: // rc와 유사한 matchLabels 셀렉터를 사용한다
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
k get rs
, k describe rs
로 rs 정보를 알 수 있다.
레플리카셋의 더욱 표현적인 레이블 셀렉터 사용하기
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchExpressions:
- key: app // 이 셀렉터는 파드의 키가 'app'인 레이블을 포함해야 한다
operator: In // 레이블의 값은 'kubia' 여야 한다
values:
- kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
- In은 레이블의 값이 지정된 값 중 하나와 일치해야 한다
- NotIn은 레이블의 값이 지정된 값과 일치하지 않아야 한다
- Exists는 파드가 지정된 키를 가진 레이블이 포함돼야 한다(값은 중요하지 않음). 이 연산자를 사용할 때는 값 필드를 지정하지 않아야 한다.
- DoesNotExists는 파드에 지정된 키를 가진 레이블이 포함돼 있지 않아야 한다. 값 필드를 지정하지 않아야 한다.
여러 표현식을 지정하는 경우 모든 표현식이 true 여야 한다. matchLabels와 matchExpressions를 모두 지정하면, 셀렉터가 파드를 매칭하기 위해서는, 모든 레이블이 일치하고, 모든 표현식이 true로 평가돼야 한다.
데몬셋을 사용해 각 노드에서 정확히 한 개의 파드 실행하기
클러스터의 모든 노드에 노드당 하나의 파드만 실행되길 원하는 경우가 있을 수 있다.
모든 노드에서 로그 수집기와 리소스 모니터를 실행하려는 경우가 좋은 예다.
데몬셋으로 모든 노드에 파드 실행하기
모든 클러스터 노드마다 파드를 하나만 실행하려면 DaemonSet 오브젝트를 생성해야 한다.
데몬셋에 의해 생성되는 파드는 타깃 노드가 이미 지정돼 있고 쿠버네티스 스케줄러를 건너뛰는 것을 제외하면 rs, rc와 유사하다.
노드가 죽어도 다른 노드에 파드를 추가하지 않는다. 데몬셋 또한 구성된 파드 템플릿으로 파드를 생성한다.
데몬셋을 사용해 특정 노드에서만 파드를 실행하기
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: main
image: luksa/ssd-monitor
nodeSelector
를 통해 disk:ssd
라는 레이블이 붙은 노드를 고르게끔 하였다.
k label node [node name] [label key]=[label value]
로 노드에 레이블을 달고난 뒤에 확인을 해보면
데몬셋이 파드를 생ㅅ어한 것을 확인할 수가 있다.
완료 가능한 단일 태스크를 수행하는 파드 실행
계속해서 실행되는 파드가 아닌 작업을 완료한 후에는 종료되는 태스크를가 필요한 경우 잡 리소스를 사용하면 된다.
잡 리소스 소개
잡은 파드의 컨테이너 내부에서 실행 중인 프로세스가 성공적으로 완료되면 컨테이너를 다시 시작하지 않는 파드를 실행할 수 있다.
노드에 장애가 발생한 경우 잡이 관리하는 파드는 rs 파드와 같은 방식으로 다른 노드로 다시 스케줄링된다.
잡 리소스 정의
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
template: // 셀렉터를 지정하지 않았다. 파드 템플릿의 레이블을 기반으로 만들어진다.
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure // 잡은 기본 재시작 정책을 사용할 수 없다.
containers:
- name: main
image: luksa/batch-job
위 이미지는 2분간 sleep 후 완료되는 이미지이다. 생성 후 k get jobs
로 확인할 수가 있다.
k get po
를 통해서 파드를 확인해보면 Running 상태이다가 2분 후에 Completed 상태가 되면서 조회가 안된다.k get po -a
를 입력하면 완료된 파드까지 조회를 할 수 있게 된다. 파드가 완료될 때 파드가 삭제되지 않은 이유는 파드의 로그를 검사할 수 있게 하기 위해서다. 파드를 삭제하거나 해당 파드를 생성한 잡을 삭제하면 파드가 삭제된다.
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
completions: 5 // 이 잡은 다섯 개의 파드를 순차적으로 실행한다.
template:
...
위와 같이 completions 옵션으로 파드의 개수를 둘 수도 있다. 처음에는 파드를 하나 만들고, 파드의 컨테이너가 완료되면 두 번째 파드를 만들어 다섯 개의 파드가 성공적으로 완료될 때 까지 반복한다.
completions: 5
parallelism: 2
parallelism 속성을 이용해 병렬로 실행할 파드 수를 지정할 수 있따.
kk scale job [job name] --replicas 3
를 이용해 parallelism 속성을 변경할 수도 있다.
기존에 2개의 파드로 동작하고 있었다면 명령어를 입력하면 파드가 하나 더 생성되게된다.
activeDeadlineSeconds
속성을 설정해 파드의 실행 시간을 제한할 수 있다. 이보다 오래 실행되면 시스템이 종료를 시도하고 잡을 실패한 것으로 표시한다.
잡을 주기적으로 또는 한 번 실행되도록 스케줄링하기
잡은 생성하면 바로 실행이 되는데 이를 cron을 설정할 수가 있다. 이 때 사용하는 것이 크론잡 리로스다.
크론잡 생성하기
apiVersion: batch/v1
kind: CronJob
metadata:
name: batch-job-every-fifteen-minutes
spec:
schedule: "*/15 * * * *"
startingDeadlineSeconds: 15
jobTemplate:
spec: // 크론잡이 생성하는 잡 리소스의 템플릿
template:
metadata:
labels:
app: periodic-batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
15분 마다 잡이 실행되는 크론잡 yaml 파일이다.
잡 리소스는 크론잡 리로스에서 생성이 되고, 잡은 파드를 생성한다. 잡이나 파드가 상대적으로 늦게 생성되고 실행될 수 있다.
이때 startingDeadlineSeconds
옵션을 크론잡의 spec 하위에 두어 해당 값(초 단위) 안에 잡이 실행되지 않으면 실패로 표시된다.
서비스: 클라이언트가 파드를 검색하고 통신을 가능하게 함
파드는 일시적이고 IP가 변경이 일어날 수도 있고 스케일링이 일어날 수도 있다.
따라서 쿠버네티스는 서비스를 이용해 파드 접근 문제를 해결한다.
서비스 소개
쿠버네티스의 서비스는 동일한 서비스를 제공하는 파드 그룹에 지속적인 단일 접점을 만들려고 할 때 생성하는 리소스다.
각 서비스는 서비스가 존재하는 동안 절대 바뀌지 않는 IP 주소와 포트가 있다.
클라이언트는 해당 IP와 포트로 접속한 다음 해당 서비스를 지원하는 파드 중 하나로 연결된다.
백엔드와 프론트엔드의 서비스를 생각하면 대략 아래와 같이 나타낼 수 있다.
서비스 생성
서비스 연결은 서비스 뒷단의 모든 파드로 로드밸런싱된다. 어떤 파드가 서비스의 일부분인지는 레이블 셀렉터로 기억한다.
k expose
로 서비스를 간단히 생성할 수도 있다. 하지만 yaml로 더 상세하게 설정이 가능하다.
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
포트 80으로 들어오면 app = kubia
레이블을 가진 파드한테 8080포트로 연결해준다는 의미이다.
생성 후 확인해보면 cluster-ip가 할당이 되어있는 것을 확인할 수가 있다. 이는 클러스터 내부에서만 액세스할 수 있다.
k exec [pod-name] -- curl -s [custer-ip]
를 입력하면 접근이 가능하다. 명령어의 흐름은 아래와 같다.
k exec
를 사용하면 기존 파드의 컨테이너 내에서 원격으로 임의의 명령어를 실행할 수 있다.
--
는 kubectl 명령줄 옵션의 끝을 의미한다.
명령 때마다 임의의 파드를 선택해 연결을 하게 되는데, sessionAffinity: ClientIP
이렇게 클라이언트 ip를 지정하면 동일한 클라이언트 IP의 모든 요청을 동일한 파드로 전달하게 된다.
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia
파드에서 여러 포트를 수신받는다면 위와 같이 멀티 포트 서비스를 생성할 수도 있다.
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http
containerPort: 8080
- name: https
containerPorts: 8443
위와 같이 파드를 포트 번호로만 참조하지 않고 포트에 이름을 부여할 수 있고 이를 아래와 같이서비스에서 참조할 수가 있다.
apiVersion: v1
kind: Service
spec:
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https
이렇게 설정을 하면 이후 서비스 스펙을 변경하지 않고도 포트 번호를 변경할 수 있다는 장점이 있다.
서비스 검색
파드는 항상 서비스의 IP 주소로 액세스할 수 있어야 한다.
파드가 시작되면 쿠버네티스는 해당 시점에 존재하는 각 서비스를 가리키는 환경변수 세트를 초기화한다. 클라이언트 파드를 생성하기 전에 서비스를 생성하면 해당 파드의 프로세스는 환경변수를 검사해 서비스의 IP 주소와 포트를 얻을 수 있다.
k exec [pod name] env
를 하면 서비스의 클러스터 IP와 제공되는 포트가 출력되는 것을 확인할 수가 있다.
각 서비스는 내부 DNS 서버에서 DNS 항목을 가져오고 서비스 이름을 알고 있는 클라이언트 파드는 환ㄱ여변수 대신 FQDN(정규화된 도메인 이름)으로 액세스할 수 있다.
backend-database.default.svc.cluster.local
이라는 FQDN은 서비스 이름이 backend-database, 네임스페이스가 default, svc.cluster.local은 모든 클러스터의 로컬 서비스 이름에 사용되는 클러스터의 도메인 접미사다.
k exec -it [pod name]
으로 파드의 컨테이너 내에서 bash를 실행할 수 있다.
이후 내부에서 curl http://[svc name].[namespace].svc.cluster.local
을 입력하면 해당 서비스에 호출이 가는 것을 확인할 수가 있다.
svc.cluster.local
은 모든 클러스터의 로컬 서비스 이름에 사용되는 클러스터의 도메인 접미사다.
서비스 IP로 ping을 해서 작동 여부를 확인하는 것은 불가능하다. 서비스의 클러스터 IP가 가상 IP이므로 서비스 포트와 결합된 경우에만 의미가 있기 때문이다.
클러스터 외부에 있는 서비스 연결
서비스가 클러스터 내에 있는 파드로 연결을 전달하는 게 아니라, 외부 IP와 포트로 연결을 전달하는 것을 알아보겠다.
서비스 엔드포인트 소개
서비스는 사실 파드에 직접 연결되지 않는다. 대신 엔드포인트 리소스가 그 사이에 있다.
서비스를 describe 해보면 서비스의 엔드포인트를 나타내는 파드 IP와 포트 목록을 확인할 수가 있다.
k get endpoints [svc name]
을 이용하면 엔드포인트만 따로 확인할 수가 있다.
서비스 엔드포인트 수동 구성
서비스에 파드 셀렉터 없이 만들면 엔드포인트 리소스를 만들지 못한다. 어떤 파드인지 알 수 없기 때문이다. 서비스와 엔드포인트를 수동으로 설정해서 사용할 수 있다.
apiVersion: v1
kind: Service
metadata:
name: external-service // 엔드포인트 오브젝트 이름과 일치해야 한다
spec:
ports:
- port: 80
----
apiVersion: v1
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 11.11.11.11 // 서비스가 연결을 전달할 엔드포인트의 IP
- ip: 22.22.22.22
ports:
- port: 80 // 엔드포인트의 대상 포트
결과적으로 위와 같은 형태가 된다.
외부 서비스를 위한 별칭 생성
apiVersion: v1
kind: Service
metadata:
name: external-service // 엔드포인트 오브젝트 이름과 일치해야 한다
spec:
type: ExternalName
externalName: someapi.somecompany.com //FQDN 이름
ports:
- port: 80
외부 서비스의 별칭으로 사용되는 서비스를 만들려면 유형 필드를 ExternalName으로 설정해 서비스 리소스를 만들면된다.
서비스가 생성되면 파드는 서비스의 FQDN을 사용하는 대신 external-service.default.svc.cluster.local 도메인 이름으로 외부 서비스에 연결할 수 있다.
이렇ㅎ게 하면 서비스를 사용하는 파드에서 실제 서비스 이름과 위치가 숨겨져 나중에 externalName 속성을 변경하거나 유형을 다시 ClusterIP로 변경하고 서비스 스펙을 만들어 수정하면 나중에 다른 서비스를 가리킬 수 있다.
ExternalName 서비스는 DNS 레벨에서만 구현된다. 서비스에 관한 간단한 CNAME DNS 레코드가 생성된다. 따라서 서비스에 연결하는 클라이언트는 서비스 프록시를 완전히 무시하고 외부 서비스에 직접 연결된다. 이러한 이유로 ExternalName 유형의 서비스는 ClusterIP를 얻지 못한다.
외부 클라이언트에 서비스 노출
이번에는 외부 클라이언트가 내가 만든 서비스에 접근할 수 있게끔 해보자. 방법은 3가지가 있다.
- NordPort 서비스: 노드포트 서비스의 경우 각 클러스터 노드는 노드 자체에서 포트를 열고 해당 포트로 수신된 트래픽을 서비스로 전달한다. 이 서비스는 내부 클러스터 IP와 포트로 액세스 할 수 있고, 모든 노드의 전용 포트로도 액세스 할 수 있다.
- LoadBalancer 서비스: 쿠버네티스가 실행 중인 클라우드 인프라에서 프로비저닝된 전용 로드밸런서로 서비스에 액세스할 수 있다. 로드밸런서는 트래픽을 모든 노드의 노드포트로 전달한다. 클라이언트는 로드밸런서의 IP로 서비스에 액세스한다.
- 단일 IP 주소로 여러 서비스를 노출하는 인그레스 리소스 만들기: HTTP 레벨에서 작동하므로 4계층 서비스보다 더 많은 기능 제공이 가능하다.
노드포트 서비스 사용
모든 노드에 특정 포트를 할당하고 서비스를 구성하는 파드로 들어오는 연결을 전달한다. 일반 서비스(실제 유형은 Cluster IP)와 유사하지만 서비스의 내부 클러스터 IP뿐만 아니라 모든 노드의 IP와 할당된 노트포트로 서비스에 액세스 할 수 있다.
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: NodePort // 서비스 유형을 노드포트로 설정
ports:
- port: 80 // 서비스 내부 클러스터 IP의 포트
targetPort: 8080 // 서비스 대상 파드의 포트
nodePort: 30123 // 각 클러스터 노드의 포트 30123으로 서비스에 액세스 할 수 있다.
selector:
app: kubia
생성을 하고 조회를하면 아래와 같이 나온다.
내 예시의 경우 노드의 외부 ip가 설정이 안되었지만 원래라면 노드의 ip가 할당이 되고 해당 ip의 30123 포트로 접근을 하면 해당 서비스로 접근이 되고 서비스는 파드를 연결해주게 된다.
외부 로드밸런서로 서비스 노출
클라우드 공급자에서 실행되는 쿠버네티스 클러스터는 일반적으로 클라우드 인프라에서 로드밸런서를 자동으로 프로비저닝하는 기능을 제공한다. 로드밸런서는 공개적으로 엑세스 가능한 고유한 IP 주소를 가지며 모든 연결을 서비스로 전달한다. 따라서 로드밸런서의 IP 주소로 서비스에 액세스할 수 있다.
쿠버네티스가 로드밸런서 서비스를 지원하지 않는 환경에서 실행 중인 경우 로드밸런서는 프로비저닝되지 않지만 서비스는 노드포트 서비스처럼 작동한다. 로드밸런서 서비스는 노드포트 서비스의 확장이기 때문이다.
apiVersion: v1
kind: Service
metadata:
name: kubia-loadbalancer
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
노드포트를 지정할 수 있지만 지정하지 않는다. 이렇게되면 쿠버네티스가 포트를 선택하게 된다.
이제 외부에서 curl을 통해서 접근이 가능하게 된다.
curl로 호출하면 매번 다른 파드로 연결이 되지만, 웹 브라우저로 연결을 하면 같은 파드만 찌르게 된다. 그 이유는 세션 어피니티가 None이지만 브라우저는 keep-alive 연결을 사용하고 같은 연결로 모든 요청을 보내기 때문이다.
외부 연결의 특성 이해
외부 클라이언트가 노드포트로 서비스에 접속할 경우 임의로 선택된 파드가 연결을 수신한 동일한 노드에서 실행 중일 수도 있고, 그렇지 않을 수도 있다. 외부의 연결을 수신한 노드에서 실행 중인 파드로만 외부 트래픽을 전달하도록 서비스를 구성해 이 추가 홉을 방지할 수 있다.
서비스 스펙 섹션에서 externalTrafficPolicy: Local
을 설정하면 된다.
인그레스 리소스로 서비스 외부 노출
인그레스는 한 IP 주소로 수십 개의 서비스에 접근이 가능하도록 지원해준다.
클라이언트가 HTTP 요청을 인그레스에 보낼 때, 요청한 호스트와 경로에 따라 요청을 전달할 서비스가 결정된다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: kubia.example.com // 인그레스는 kubia.example.com 도메인 이름을 서비스에 매핑한다
http:
paths:
- path: / // 모든 요청은 kubia-nodeport 서비스의 포트 80으로 전달된다
pathType: Prefix
backend:
service:
name: kubia-nodeport
port:
number: 80
kubia.example.com
으로 요청되는 인그레스 컨트롤러에 수신된 모든 HTTP 요청을 포트 80의 kubia-nodeport 서비스로 전송하도록 하는 인그레스 규칙을 정의했다.
생성하고 조회하여 나온 IP를 이용해 DNS 서버를 구성하거나, /etc/hosts에 추가할 수 있다.
하나의 인그레스로 여러 서비스 노출
동일한 호스트의 다른 경로로 여러 서비스 매핑
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: kubia.example.com
http:
paths:
- path: /kubia / kubia.example.com/kubia 으로의 요청은 kubia 서비스로 라우팅
pathType: Prefix
backend:
service:
name: kubia
port:
number: 80
- path: /bar / kubia.example.com/bar로의 요청은 bar 서비스로 라우팅
pathType: Prefix
backend:
service:
name: bar
port:
number: 80
서로 다른 호스트로 서로 다른 서비스 매핑
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: foo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: foo
port:
number: 80
- host: bar.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bar
port:
number: 80
이 때에 DNS에 foo.example.com과 bar.example.com 도메인 이름을 모두 인그레스 컨트롤러의 IP 주소로 지정해야 한다.
파드가 연결을 수락할 준비가 됐을 때 신호 보내기
서비스가 파드와 연결이 되었지만 파드는 준비가 되지 않았을 수도 있다. 이럴 떄 파드에 레디니스 프로브(readiness probe)를 정의할 수 있다.
3가지 유형의 레디니스 프로브가 있다.
- 프로세스를 실행하는 Exec 프로브는 컨테이너의 상태를 프로세스의 종료 상태 코드로 결정한다.
- HTTP GET 프로브는 HTTP GET 요청을 컨테이너로 보내고 응답의 HTTP 상태 코드를 보고 컨테이너가 준비됐는지 여부를 결정한다.
- TCP 소켓 프로브는 컨테이너의 지정된 포트로 TCP 연결을 연다. 소켓이 연결되면 컨테이너가 준비된 것으로 간주한다.
레디니스 프로브의 동작
컨테이너가 시작되고 주기적으로 프로브를 호출한다. 파드가 준비되지 않으면 서비스에서 제거되고 준비되면 서비스에 닷 ㅣ추가된다(서비스의 엔드 포인트에 추가/제거 된다는 것이다).
라이브니스 프로브와 달리 레디니스 프로브는 실패하더라도 컨테이너가 종료되거나 다시 시작되지 않는다.
파드에 레디니스 프로브 추가
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080
readinessProbe:
exec:
command:
- ls
- /var/ready
이제 k exec [pod name] -- touch /var/ready
로 /var/ready로 파일을 만들고 파드를 확인해보면 해당 파드가 READY 상태가 된 것을 확인할 수가 있다.
레디니스 프로브를 정의하지 않으면 파드가 시작하는 즉시 서비스 엔드포인트가 되므로 레디니스 프로브를 설정해주는 것이 좋다.
레디니스 프로브에 파드의 종료 코드를 포함시키면 안된다.
헤드리스 서비스로 개별 파드 찾기
클라이언트가 모든 파드에 연결해야 하는 경우에 파드 IP를 API 서버에 호출하여 사용하기에는 무리가 있다. 쿠버네티스는 DNS 조회로 파드 IP를 찾을 수 있도록 한다. 일반적으로 DNS 조회를 수행하면 DNS 서버는 서비스의 클러스터 IP를 반환하지만 서비스에 클러스터 IP가 필요하지 않다고 설정을 하면 DNS 서버는 서비스 IP 대신 파드의 IP들을 반환한다.
헤드리스 서비스 생성
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
spec:
clusterIP: None
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
이제 클러스터 내부에서 kubia-headless에 대해서 ip를 확인해보면
파드의 ip 정보를 반환해주는 것을 호가인할 수가 있다.
서비스 문제 해결
서비스로 파드에 액세스할 수 없는 경우 이것들을 확인해보자.
- 먼저 외부가 아닌 클러스터 내에서 서비스의 클러스터 IP에 연결되는지 확인한다.
- 서비스에 액세스할 수 있는지 확인하려고 서비스 IP로 핑을 할 필요 없다.
- 레디니스 프로브를 정의했다면 성공했는지 확인하자. 그렇지 않으면 파드는 서비스에 포함되지 않는다.
- 파드가 서비스의 일부인지 확인하려면 k get endpoints를 사용해 확인하자.
- FQDN이나 그 일부로 서비스에 액세스하려고 하는데 작동하지 않는 경우, FQDN 대신 클러스터 IP를 사용해 액세스할 수 있는지 확인한다.
- 대상 포트가 아닌 서비스로 노출된 포트에 연결하고 있는지 확인한다.
- 파드 IP에 직접 연결해 파드가 올바른 포트에 연결돼 있는지 확인한다.
- 파드 IP로 애플리케이션에 액세스할 수 없는 경우 애플리케이션이 로컬호스트에만 바인딩하고 있는지 확인한다.
볼륨: 컨테이너에 디스크 스토리지 연결
볼륨 소개
쿠버네티스 볼륨은 파드의 구성 요소로 컨테이너와 동일하게 파드 스펙에서 정의된다.
볼륨은 파드의 모든 컨테이너에서 사용 가능하지만 접귾나려는 컨테이너에서 각각 마운트돼야 한다.
아래와 같은 형태일 때 서로 분리된 컨테이너이기 때문에 webserver가 /var/logs에 로그를 써도 logrotator 에서는 활용할 수가 없다. 그러나 볼륨 2개를 파드에 추가하고 3개의 컨테이너 내부의 적절한 경로에 마운트한다면 해결이 된다.
볼륨이 파드의 라이프사이클에 바인딩되면 파드가 존재하는 동안 유지될 수 있지만 볼륨 유형에 따라 파드와 볼륨이 사라진 후에도 볼륨의 파일이 유지돼 새로운 볼륨으로 마운트될 수 있다.
사용 가능한 볼륨 유형 소개
- emptyDir: 일시적인 데이터를 저장하는 데 사용되는 간단한 빈 디렉터리
- hostPath: 워커 노드의 파일시스템을 파드의 디렉터리로 마운트하는데 사용
- gitRepo: 깃 리포지터리의 컨텐츠를 체크아웃해 초기화한 볼륨
- nfs: NFS 공유를 파드에 마운트
- gcePersistentDisk, awsElasticBlockStore, azureDisk: 클라우드 제공자의 전용 스토리지를 마운트하는데 사용
- cinder, cephfs, iscsi, flocker, glusterfs: 다른 유형의 네트워크 스토리지를 마운트하는데 사용
- configMap, secret, downwardAPI: 쿠버네티스 리소스나 클러스터 정보를 파드에 노출하는데 사용
- persistentVolumeClaim: 사전에 혹은 동적으로 프로비저닝된 퍼시스턴트 스토리지를 사용하는 방법
단일 파드는 여러 유형의 여러 볼륨을 사용할 수 있으며 파드의 각 컨테이너는 볼륨을 마운트할 수도 있고 하지 않을 수도 있다.
볼륨을 사용한 컨테이너 간 데이터 공유
emptyDir 볼륨 사용
emptyDir 볼륨은 동일 파드에서 실행 중인 컨테이너 간 파일을 공유할 때 유용하다. 단일 컨테이너에서도 가용한 메모리에 넣기에 큰 데이터 세트의 정렬 작업을 수행하는 것과 같이 임시 데이터를 디스크에 쓰는 목적인 경우 사용할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- image: luksa/fortune
name: html-generator
volumeMounts:
- name: html //html이란 이름의 볼륨을 컨테이너의 /var/htdocs에 마운트한다
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html //위와 동일한 볼륨을 /usr/share/nginx/html에 읽기전용으로 마운트한다
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes: //html이란 단일 emptyDir 볼륨을 위의 컨테이너 2개에 마운트한다
- name: html
emptyDir: {}
이미지 fortune은 10초마다 /var/htdocs에 index.html을 저장하는 이미지다.
볼륨이 /var/htdocs에 마운트 됐으므로 볼륨에 index.html이 쓰여지고 web-server는 /usr/share/nginx/html(nginx 서버가 서비스하는 기본 디렉터리)의 html 파일을 서비스하기 시작한다. 볼륨이 마운트 되었기 떄문에 fortune이 생성한 index.html을 서비스하게 된다.
이제 k port-forward fortune 8080:80
을 하여 로컬에서 8080 포트로 접근하면 연결이 되는 것을 확인할 수가 있다.
emptyDir은 워커 노드의 실제 디스크에 생성되므로 노드 디스크가 어떤 유형인지에 따라 성능이 결정된다. 이를 디스크가 아닌 메모리를 사용하는 tmpfs 파일시스템으로 생성하도록 요청할 수 있다.
volumes:
- name: html
emptyDir:
medium: Memory
위와 같이 설정하면 된다.
깃 리포지터리를 볼륨으로 사용하기
apiVersion: v1
kind: Pod
metadata:
name: gitrepo-volume-pod
spec:
containers:
- image: nginx:alpine
name: wweb-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
gitRepo: //gitRepo 볼륨 생성
repository: https://github.com/Be-poz/kubia-website-example.git //볼륨이 이 리포지터리 복제
revision: master //master 브랜치 체크아웃
directory: . //볼륨의 루트 디렉터리에 복제
복제하고 나서 해당 레포지터리에 커밋이 푸쉬되도 반영되지 않는다. 파드를 삭제하고 다시 만들면서 최신 커밋을 포함하게 된다.
이 상태로 호출을 해보면 Hello there이 찍힌다. 이 깃 레포지터리에 커밋을 해도 동기화가 되지않는 것을 확인할 수가 있다.
두 번째 컨테이너인 사이드카 컨테이너를 사용하여 이 동기화를 돕게끔 할 수도 있다. git sync 이미지를 사용하면된다.(요건 18장에서 해볼 예정)
private repository는 gitRepo를 이용할 수 없다. 이를 위해서는 사이드카나 다른 볼륨 방식을 이용해야 한다.
워커 노드 파일시스템의 파일 접근
대부분의 파드는 호스트 노드를 인식하지 못하므로 노드의 파일시스템에 있는 어떤 파일에도 접근하면 안 된다. 그러나 특정 시스템 레벨의 파드는 노드의 파일을 읽거나 피일시스템을 통해 노드 디바이스를 접근하기 위해 노드의 파일시스템을 사용해야 한다. 쿠버네티스는 hostPath 볼륨으로 가능케 한다.
hostPath 볼륨 소개
hostPath 볼륨은 노드 파일시스템의 특정 파일이나 디렉터리를 가리킨다. 동일 노드에 실행 중인 파드가 hostPath 볼륨의 동일 경로를 사용 중이면 동일한 파일이 표시된다.
hostPath는 emptyDir, gitRepo와 같이 파드가 삭제되면 볼륨의 컨텐츠가 모두 삭제되지 않는다.
hostPath 볼륨은 파드가 어떤 노드에 스케줄링되느냐에 따라 이전 데이터를 볼 수 있냐 아니냐가 갈리기 때문에 일반적인 파드에 사용하는 것은 좋은 생각이 아니다.
hostPath 볼륨을 사용하는 시스템 파드 검사하기
k get po -n kube-system
으로 나오는 파드를 describe 해서 살펴보면 hostPath 볼륨을 사용하는 파드를 많이 볼 수 있다.
데이터를 저장하기 위한 목적으로 사용하는 것은 안보이고 노드의 로그파일이나 kubeconfig, CA 인증서를 위해 사용하는 것을 확인 할 수가 있다.
컨피그맵과 시크릿: 애플리케이션 설정
컨테이너화된 애플리케이션 설정
설정 데이터를 저장하는 쿠버네티스 리소스를 컨피그맵이라고 한다. 컨피그맵을 사용하여 아래와 같은 방법으로 애플리케이션을 구성할 수 있다.
- 컨테이너에 명령줄 인수 전달
- 각 컨테이너를 위한 사용자 정의 환경변수 지정
- 특수한 유형의 볼륨을 통해 설정 파일을 컨테이너에 마운트
컨테이너에 명령줄 인자 전달
도커에서 명령어와 인자 정의
Dockerfile에서 ENTRYPOINT는 컨테이너가 시작될 때 호출될 명령어이고,
CMD는 ENTRYPOINT에 전달되는 인자를 정의한다.
CMD 명령어를 사용해 이미지가ㅣ 실행될 때 실행할 명령어를 지정할 수 있지만, 올바른 방법은 ENTRYPOINT 명령어로 실행하고 기본 인자로 정의하려는 경우에만 CMD를 지정하는 것이다.
- shell 형식 - ENTRYPOINT node app.js
- exec 형식 - ENTRYPOINT ["node", "app.js"]
exec 형식으로 하면 컨테이너 내부에서 node 프로세스를 직접 실행한다.
shell 형식으로 하게되면 쉘 프로세스를 거치게 된다. 이는 불필요하므로 exec 형식을 사용해 실행한다.
#!/bin/bash
trap "exit" SIGINT
INTERVAL=$1
echo Configurerd to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
FROM ubuntu:latest
RUN apt-get update; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh
ENTRYPOINT ["/bin/fortuneloop.sh"]
CMD ["10"]
이대로 이미지를 만들고 돌려보면
Configurerd to generate new fortune every 10 seconds
Mon May 29 10:27:51 UTC 2023 Writing fortune to /var/htdocs/index.html
위와 같이 나오게 된다. docker run -it [image name] 15
로 하면 10seconds가 아닌 15로 찍히는 것을 확인할 수가 있다.
쿠머네티스에서 명령과 인자 재정의
도커에서의 ENTRYPOINT는 쿠버네티스에서 command,
CMD는 args로 지정할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: fortune2
spec:
containers:
- image: luksa/fortune:args
args: ["2"]
...
위와 같이 인자를 넘겨줄 수 있다. 여러 개의 인자를 가진 경우에는
args:
- foo
- bar
- "15"
와 같이 표현할 수 있다.
컨테이너의 환경변수 설정
#!/bin/bash
trap "exit" SIGINT
echo Configurerd to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
apiVersion: v1
kind: Pod
metadata:
name: fortune2
spec:
containers:
- image: luksa/fortune:args
env:
- name: INTERVAL
value: "30"
...
기존의 sh에서 $INTERVAL을 초기화하던 행을 지우고, pod 매니페스트에서 env를 설정해준다.
env:
- name: FIRST_VAR
value: "foo"
- name: SECOND_VAR
value: "$(FIRST_VAR)bar"
이렇게 다른 환경변수를 참조할 수도 있다.
이렇게 하드코딩을 하면 프로덕션과 개발을 위해 서로 분리된 파드 정의가 필요하다는 단점이 있다.
컨피그맵으로 설정 분리
컨피그맵 소개
컨피그맵은 짧은 문자열에서 전체 설정 파일에 이르는 값을 가지는 키/값 쌍으로 구성된 맵이다.
맵의 내용은 컨테이너의 환경변수 또는 볼륨 파일로 전달된다.
컨피그맵 생성
k create configmap
으로 간단하게 생성이 가능하다.
k create configmap fortune-config --from-literal=sleep-interval=25
이렇게 생성을 하게되면,
sleep-interval이라는 키에 25이라는 값이 들어가게 된다.
여러 쌍을 추가하고 싶다면 --from-literal=foo=bar
와 같이 --from-literal
인자를 추가하면 된다.
k get configmap [configmap name] -o yaml
명령어로 생성된 컨피그맵 정의를 확인할 수 있다.
k create configmap my-config --from-file=config-file.conf
이 명령어를 이용하면 뒤에 적은 파일을 찾아 해당 내용을 config-file.confg 키 값으로 저장한다.
k create configmap my-config --from-file=customkey=config-file.conf
이렇게 입력하면 내가 원하는 key 값을 설정할 수가 있다.
이외에도 위와 같이 path를 줄 수도 있다.
컨피그맵 항목을 환경변수로 컨테이너에 전달
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config // 참조하는 컨피그맵 이름
key: sleep-interval // 컨피그맵에서 해당 키 아래에 저장된 값으로 변수 설정
...
valueFrom을 이용해서 컨피그맵 키에서 값을 가져와 환경변수를 세팅했다.
컨피그맵에 여러 항목이 포함돼 있을 때에는 env 속성 대신 envFrom을 사용한다.
spec:
containers:
- image: some-image
envFrom:
- prefix: CONFIG_
configMapRef:
name: my-config-map
prefix를 통해서 환경변수의 접두어를 붙여주었다. 내부에 FOO, BAR 키가 있다면 환경변수로 가지고 오면서 CONFIG_FOO, CONFIG_BAR가 된다. 접두사는 선택 사항이고 이를 생략하면 환경변수의 이름은 키와 동일한 이름을 갖게 된다.
만약 컨피그맵의 키가 올바른 형식이 아닌 경우는 항목을 건너뛴다(ex. '-'가 들어간 경우).
컨피그맵 항목을 명령줄 인자로 전달
컨피그맵 항목을 환경변수로 먼저 초기화하고 참조하게 된다.
apiVersion: v1
kind: Pod
metadata:
name: fortune-args-from-configmap
spec:
containers:
- image: luksa/fortune:args
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
args: ["$(INTERVAL)"]
...
컨피그맵 볼륨을 사용해 컨피그맵 항목을 파일로 노출
server {
listen 80;
server_name www.kubia-example.com;
gzip on;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
위의 conf와 sleep-interval 텍스트를 생성하고 컨피그맵을 이 2개의 파일이 있는 directory로 컨피그맵을 생성해보았다.
user@AD01977799 fortune-args % k get configmap fortune-config -o yaml
apiVersion: v1
data:
my-nginx-config.conf: |
server {
listen 80;
server_name www.kubia-example.com;
gzip on; // 일반 텍스트와 xml 파일에 대해 gzip 압축 활성화
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
sleep-interval: |
25
kind: ConfigMap
...
위와 같이 출력이 되었다.
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: config
mountPath: /tmp/whole-fortune-config-volume
readOnly: true
ports:
- containerPort: 80
name: http
protocol: TCP
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
fortune-config 이름의 configMap 을 참조해서 볼륨을 마운트 해주었다.
애플리케이션을 재시작하지 않고 애플리케이션 설정 업데이트
환경변수 또는 명령줄 인수를 사용할 경우에는 프로세스가 실행되고 있는 동안에 업데이트를 할 수 없다는 단점이 있다.
컨피그맵을 사용하면 파드를 다시 만들거나 컨테이너를 다시 시작할 필요 없이 설정을 업데이트 할 수 있다.
k edit configmap [configmap name]
컨피그맵이 변경 되었을 때 모든 파일을 한 번에 변경하기 때문에 일부만 변경이 되고 애플리케이션이 그 일부만 변경된 채로 로드되거나 하는 경우는 없다.
시크릿으로 민감한 데이터를 컨테이너에 전달
시크릿 소개
시크릿은 키-값 쌍을 가진 맵으로 컨피그맵과 매우 비슷하다.
- 환경변수로 시크릿 항목을 컨테이너에 전달
- 시크릿 항목을 볼륨 파일로 노출
위와 같은 상황에서 사용할 수 있다.
쿠버네티스는 시크릿에 접근해야 하는 파드가 실행되고 있는 노드에만 개별 시크릿을 배포해 시크릿을 안전하게 유지한다. 또한 노드 자체적으로 시크릿을 항상 메모리에만 저자오디게 하고 물리 저장소에 기록되지 않도록 한다. 물리 저장소는 시크릿을 삭제한 후에도 디스크를 완전히 삭제하는 작업이 필요하기 때문이다.
마스터 노드(구체적으로 etcd)에는 시크릿을 암호화되지 않은 형식으로 저장하므로, 시크릿에 저장한 민감한 데이터를 보호하려면 마스터 노드를 보호하는 것이 필요하다. etcd 저장소를 안전하게 하는 것뿐만 아니라 권한 없는 사용자가 API 서버를 이용하지 못하게 해야 한다.
시크릿 생성
인증키와 개인키를 만들고 bar라는 텍스트가 써진 foo를 가지고 generic 시크릿을 생성했다.
컨피그맵과 시크릿 비교
k get secret fortune-https -o yaml
로 조회를 해보면 시크릿의 값이 base64 인코딩 문자열로 표시됨을 알 수 있다.
이렇게 사용되는 까닭은 시크릿 항목에 텍스트뿐만 아니라 바이너리 값도 담을 수 있기 때문이다.
kind: Secret
apiVersion: v1
stringData:
foo: plain Text
data:
https.key: base64value...
stringData를 사용하면 일반 텍스트로 입력이 가능하다. 다만 쓰기 전용이다. 이 시크릿을 조회해보면 plain Text 값이 base64로 인코딩된 것을 확인할 수가 있다.
secret 볼륨을 통해 시크릿을 컨테이너에 노출하면, 시크릿 항목의 값이 일반 텍스트인지 바이너리 데이터인지에 관계없이 실제 형식으로 디코딩돼 파일에 기록된다. 환경변수로 시크릿 항목을 노출할 때도 마찬가지다. 두 경우 모두 애플리케이션에서 디코딩할 필요는 없이 파일 내용을 읽거나 환경변숫값을 찾아 직접 사용할 수 있다.
파드에서 시크릿 사용
volumes:
- name: certs
secret:
secretName: fortune-https
secret
과 secretName
을 이용하여 사용하면 된다.
env:
- name: FOO_SECRET
valueFrom:
secretKeyRef:
name: fortune-https // 키를 갖고 있는 시크릿의 이름
key: foo // 노출할 시크릿으이 키 이름
환경변수로 시크릿 항목 노출을 원한다면 위와 같이 하면 된다.
configMapKeyRef
을 사용하던 컨피그맵과 달리 secretKeyRef
을 사용한다.
환경변수로 시크릿을 사용하면 의도치 않게 노출될 수 있기 때문에 안전을 위해서는 secret 볼륨 사용이 낫다.
애플리케이션에서 파드 메타데이터와 그 외의 리소스에 액세스하기
Downward API로 메타데이터 전달
컨피그맵과 시크릿과 같이 파드가 노드에 스케줄링돼 실행되기 이전에 이미 알고 있는 데이터가 아닌,
파드의 IP, 호스트 노드 이름 또는 파드 자체의 이름과 같이 실행 시점까지 알려지지 않은 데이터의 경우에는 Downward AIP로 해결한다. 환경변수 또는 downwardAPI 볼륨 내에 있는 파일로 파드와 해당 환경의 메타데이터를 전달할 수 있다.
사용 가능한 메타데이터 이해
Downward API를 사용하면 아래의 정보를 컨테이너에 전달할 수 있다.
- 파드의 이름
- 파드의 IP 주소
- 파드가 속한 네임스페이스
- 파드가 실행 중인 노드의 이름
- 파드가 실행 중인 서비스 어카운트 이름
- 각 컨테이너의 CPU와 메모리 요청
- 각 컨테이너의 CPU와 메모리 제한
- 파드의 레이블
- 파드의 어노테이션
서비스 어카운트란 파드가 API 서버와 통신할 때 인증하는 계정이다.
환경변수로 메타데이터 노출하기
apiVersion: v1
kind: Pod
metadata:
name: downward
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: CONTAINER_CPU_REQUEST_MILLICORES
valueFrom:
resourceFieldRef://컨테이너 CPU/메모리 요청/제한은 fieldRef대신 resourceFieldRef를 사용해 참조함
resource: requests.cpu
divisor: 1m
- name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Ki
자원 제한 또는 요청을 노출시키는 환경변수의 경우 divisor를 지정한다.
제한 또는 요청의 실제 값은 제수로 나누고 결과값을 환경변수로 노출한다. 위 예제에서는 CPU 요청에 대한 divisor를 1m(1밀리코어 또는 1000분의 1 CPU 코어)로 설정했다. CPU 요청을 15m로 설정했기 떄문에 환경변수 CONTAINER_CPU_REQUEST_MILICORES는 15로 설정된다. 마찬가지로 메모리 제한을 4Mi(4메비바이트)로 설정하고 divisor를 1Ki(1키비바이트)로 설정했으므로 CONTAINER_MEMORY_LIMIT_KIBIBYTES 환경변수는 4096으로 설정된다.
downwardAPI 볼륨에 파일로 메타데이터 전달
apiVersion: v1
kind: Pod
metadata:
name: downward
labels:
foo: bar
annotations:
key1: value1
key2: |
multi
line
value
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
volumeMounts:
- name: downward
mountPath: /etc/downward
volumes:
- name: downward
downwardAPI:
items:
- path: "podName" //파드의 이름은 podName 파일에 기록된다
fieldRef:
fieldPath: metadata.name
- path: "podNamespace"
fieldRef:
fieldPath: metadata.namespace
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
- path: "containerCpuRequestMilliCores"
resourceFieldRef:
containerName: main
resource: requests.cpu
divisor: 1m
- path: "containerMemoryLimitBytes"
resourceFieldRef:
containerName: main
resource: limits.memory
divisor: 1
생성 후 파드의 해당 path를 확인해보니 파드의 정보들이 들어간 것을 확인할 수가 있다.
어노테이션과 레이블은 파드가 실행되는 동안 수정할 수 있다. 그렇기 때문에 환경변수로 이 값들을 노출시키면 나중에 업데이트 할 수 없다는 문제가 있기 때문에 볼륨을 사용한 것이다.
쿠버네티스 API 서버와 통신하기
애플리케이션에서 클러스터에 정의된 다른 파드나 리소스에 관한 정보가 필요한 경우에는 API 서버와 직접 통신해야 한다.
k proxy
명령은 프록시 서버를 실행해 로컬 컴퓨터에서 HTTP 연결을 수신하고, 이 연결을 인증을 관리하면서 API 서버로 전달하기 때문에, 요청할 때마다 인증토큰을 전달할 필요가 없다. 또한 각 요청마다 서버의 인증서를 확인해 중간자가 아닌 ㅅ ㅣㄹ제 API 서버와 통신한다는 것을 담보한다.
파드 내에서 API 서버와 통신
kubectl이 없는 파드 내에서 통신하는 방법을 하려면 3가지를 처리해야 한다.
- API 서버의 위치를 찾아야 한다
- API 서버와 통신하고 있는지 확인해야 한다
- API 서버로 인증해야 한다. 그렇지 않으면 볼 수도 없고 아무것도 할 수 없다
apiVersion: v1
kind: Pod
metadata:
name: curl
spec:
containers:
- name: main
image: curlimages/curl
command: ["sleep", "9999999"]
일단 아무것도 하지 않는 파드를 실행한 다음에 파드 내부에서 bash 셸을 실행해보겠다.
k exec -it curl /bin/sh
apiVersion: v1
kind: Pod
metadata:
name: curl-with-ambassador
spec:
containers:
- name: main
image: curlimages/curl
command: ["sleep", "9999999"]
- name: ambassador
image: luksa/kubectl-proxy:1.6.2
인증서, 토큰 등을 다루기가 너무 번거로우므로 proxy를 컨테이너에서 실행하고 이를 이용하여 API 서버와 통신을 할 수도 있다.
API 서버와 직접 통신하는 대신 메인 컨테이너의 애플리케이션은 HTTPS 대신 HTTP로 앰배서더에 연결하고 앰배서더 프록시가 API 서버에 대한 HTTPS 연결을 처리하도록해 보안을 투명하게 관리할 수 있다.
외부 서비스에 연결하는 복잡성을 숨기고 메인 컨테이너에서 실행되는 애플리케이션을 단순화하기 위해 앰버서더 컨테이너를 사용하는 좋은 예시이다. 앰배서더 컨테이너는 메인 애플리케이션의 언어에 관계없이 여러 애플리케이션에서 재사용할 수 있다. 단점은 추가 프로세스가 실행 중이고 추가 리소스를 소비한다는 것이다.
디플로이먼트: 선언적 애플리케이션 업데이트
파드에서 실행 중인 애플리케이션 업데이트
3개의 파드가 떠있는 상태고 해당 파드의 이미지가 v1태그에서 v2태그로 바뀐다고 가정하면, 새 파드로 교체하기 위해서는 한 번에 파드를 삭제하고 생성하거나 또는 순차적으로 파드를 삭제하고 생성하는 작업이 이루어져야 할 것이다.
오래된 파드를 삭제하고 새 파드로 교체
레플리케이션 컨트롤러는 레이블 셀렉터에 부합하는 파드가 없을 때 새로운 인스턴스를 시작한다.
새 파드 기동과 이전 파드 삭제
새 파드가 모두 실행되고 나서 서비스가 새 파드들을 바로보도록 한다. 이것을 블루-그린 디플로이먼트라고 한다.
위와 같이 롤링 업데이트 방식을 수행할 수도 있다.
애플리케이션을 선언적으로 업데이트하기 위한 디플로이먼트 사용하기
위의 내용은 사실 이제 사용이 되지 않고 실질적으로는 디플로이먼트를 사용한다.
디플로이먼트는 낮은 수준의 개념으로 간주되는 레플리케이션컨트롤러 또는 레플리카셋을 통해 수행하는 대신 애플리케이션을 배포하고 선언적으로 업데이트하기 위한 높은 수준의 리소스다.
디플로이먼트는 레플리카셋을 생성하고 레플리카셋이 파드를 생성하고 관리한다. 배포를 위해서는 새로운 레플리카셋이 필요하기 때문에 레플리카셋 위에 존재하는 다른 오브젝트를 만든 것이다.
디플로이먼트 생성
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v1
name: nodejs
selector:
matchLabels:
app: kubia
k rollout status deployment [kubia name]
을 통해 디플로이먼트 상태를 확인할 수 있다.
디플로이먼트로 생성된 파드와 레플리카셋은 이름 중간에 특정 값(해시값)이 있는데, describe해서 살펴보면 pod-template
이라는 레이블이 추가되어있고 그 값이 해당 레이블의 value로 들어간 것을 확인할 수 있다. 이를 통해 지정된 버전의 파드 템플릿에 관해 항상 동일한 레플리카셋을 사용할 수 있다.
디플로이먼트 업데이트
레플리케이션 컨트롤러에서 롤링 업데이트를 하듯이 디플로이먼트에서 그러한 기능을 사용하기 위한 디플로이먼트의 기본적인 전략으로 RollingUpdate
를 사용할 수 있다. 또는 Recreate
전략도 있는데 이것은 마찬가지로 한 번에 기존 모든 파드를 삭제한 뒤 새로운 파드를 만든다.
모든 파드를 삭제한 뒤 새로운 파드를 만드므로 서비스 다운 타임이 생긴다.
k set image deployment kubia nodejs=luksa/kubia:v2
를 통해 이미지를 변경해 주었고, while true; do curl http://{ip}; done
으로 지속적인 curl 요청을 날려보면 v1 응답에서 v2 응답으로 바뀌는 것을 확인할 수가 있다. (이것을 확인하기 위해 k patch deployment [deploy name] -p '{"spec": {"minReadySeconds": 10}}'
를 설정해 업데이트 프로세스를 늦춰주었다)
디플로이먼트 롤백
만약 새롭게 배포한 파드들이 문제가 있다면 롤백을 해야될 것이다. 사실 새로운 디플로이먼트를 배포한 이후에도 이전의 레플리카셋이 남아있는 것을 확인할 수가 있다. 이것이 바로 롤백을 위한 레플리카셋이다. k rollout undo deployment [deploy name]
을 사용하면 이전 버전으로 롤백한다.
k rollout history deployment [deploy name]
을 입력하면 개정 이력(revision history)을 표시할 수 있다.
그리고 k rollout undo deployment [deploy name] --to-revision=1
와 같이 특정 디플로이먼트 개정으로 롤백도 가능하다.
디플로이먼트의 revisionHistoryLimit 값만큼 레플리카셋을 저장하게되고 그 초과의 값은 자동으로 삭제된다.
롤아웃 속도 제어
디플로이먼트의 롤링 업데이트 중에 한 번에 몇 개의 파드를 교체할지 정할 수 있다. maxSurge와 maxUnavailable이 그 속성이다.
replicas가 1~3이고 따로 값을 설정하지 않은 경우 밑의 설명과 같이 25%로 처리하고 소수점을 반올림/내림 하기 때문에 maxSurge가 1, maxUnavailable이 0으로 설정될 것이다.
롤아웃 프로세스 일시 중지
새로운 이미지로 롤아웃을 시작하고 그 즉시 k rollout pause deployment [deploy name]
으로 롤아웃을 중지한다. 그러면 모든 파드가 아닌 일부 파드만 새로운 이미지로 올라와있을 것이다. 이렇게 카나리 릴리스를 활용할 수가 있다. 다시 재개하기 위해서는 k rollout resume deployment [deploy name]
을 통해 다시 재개한다.
잘못된 버전의 롤아웃 방지
앞에서 minReadySeconds 를 설정을 했는데 이 기능은 정확히는 배포 속도를 늦추는 것이 아니라 오작동 버전의 배포를 방지하기 위함이다.
이 속성은 파드를 사용 가능한 것으로 취급하기 전에 새로 만든 파드를 준비할 시간을 지정한다. 모든 파드의 레디니스 프로브가 성공하면 파드가 준비된다.
minReadySeconds가 지나기 전에 새 파드가 제대로 작동하지 않고 레디니스 프로브가 실패하기 시작하면 새 버전의 롤아웃이 효과적으로 차단된다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
minReadySeconds: 10
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v3
name: nodejs
readinessProbe:
periodSeconds: 1
httpGet:
path: /
port: 8080
v2 이미지에서 위의 v3 yaml로 업데이트를 해보자. k apply -f new.yaml
그 후 k rollout status deployment [deploy name]
을 해보면 1개의 파드가 새로 생성되었다고 뜨지만 서비스가 v3 파드로 접속이 안되는 것을 확인할 수 있다.
k get po를 해보면 새로운 파드가 READY 0/1 인 상태인 것을 확인할 수 있다. 그 이유는 v3는 5번째 요청부터 500 에러를 반환하는데 새 파드가 시작되자마자 레디니스 프로브가 매초 시작되는데 minReadySeconds 이전에 5번 이상이 호출이 되어 레디니스 프로브가 실패하고 결과적으로 파드가 서비스의 엔드포인트에서 제거되어식 때문이다.
maxSurge와 maxUnavailable이 각각 1, 0 이기 때문에 위 상태에서 멈추게 된 것이다. 즉 배포가 중단된 것이다.
만약 minReadySeconds가 10이 아니라 2였다면 v3 호출이 5번이 일어나기도 전에 시간이 지나게되고 레디니스 프로브가 호출에 성공하고 사용 가능한 파드로 간주되었을 것이다.
기본적으로 롤아웃이 10분 동안 진해오디지 않으면 실패한 것으로 간주된다. 이 시간은 progressDeadlineSeconds
를 통해 설정가능하다.
잘못된 롤아웃 중지는 k rollout undo deployment [deploy name]
을 통해 진행하자.
스테이트풀셋: 복제된 스테이트풀 애플리케이션 배포하기
스테이트풀 파드 복제하기
파드 템플릿에는 클레임에 대한 참조가 있기 때문에 파드들은 동일한 퍼시스턴트 볼륨을 바라보게 된다.
파드별로 각기 다른 퍼시스턴트 볼륨을 바라보고자 단일 파드를 가진 레플리카셋을 여러개 운용하는 방식은 굉장히 번거롭다.
동일한 볼륨을 바라보게 하되, 파드의 볼륨 내부에서 별도의 파일 디렉터리를 갖게끔 할 수 있는 방법도 있다.
하지만, 파드 템플릿으로부터 각 인스턴스에 어떤 디렉터리를 사용해야 하는지 전달할 수 없다. 그러나 각 인스턴스가 생성되는 시점에 다른 인스턴스가 사용하지 않는 데이터 디렉터리를 자동으로 선택(가능하다면 생성)하도록 할 수 있다. 이 방법은 인스턴스 간 조정이 필요하고 올바르게 수행하기 쉽지 않다. 또한 공유 스토리지 볼륨에 병목 현상이 발생한다.
파드는 종료되고 새로 생성되면 새로운 호스트이름과 IP를 가지기 때문에 애플리케이션에서 이 데이터를 가지고 가면 문제가 발생할 수 있다.
특정 애플리케이션은 관리자가 다른 모든 클러스터 멤버의 리스트와 멤버들의 IP 주소를 각 멤버의 설정 파일에 기재해야 한다. 하지만 쿠버네티스에서 파드가 재스케줄링되기 때문에 문제가 있다.
이 문제를 해결하기 위해 각 개별 멤버에게 전용 쿠버네티스 서비스를 생성해 클러스터 멤버에 안정적인 네트워크 주소를 제공하는 것이다. 서비스 IP는 안정적이므로 설정에서 각 멤버를 서비스 IP를 통해 가리킬 수 있다.
하지만 이런 방식은 어리석은 해결 방법이다. 이런 복잡한 해결책에 의존하지 않고 쿠버네티스에서는 스테이트풀셋 이라는 간단한 방법을 제공해준다.
스테이트풀셋 이해하기
스테이트풀셋은 애플리케이션의 인스턴스가 각각 안정적인 이름과 상태를 가지며 개별적으로 취급돼야 하는 애플리케이션에 알맞게 만들어졌다.
파드가 종료되고 새롭게 생성되는 파드는 동일한 이름, 네트워크 아이덴티티, 상태 그대로 생성된다. 스테이트풀셋으로 생성된 파드는 서수 인덱스(0부터 시작)가 할당되고 파드의 이름과 호스트 이름, 안정적인 스토리지를 붙이는 데 사용된다.
아무 파드나 사용해도 문제될 것이 없는 스테이트리스 파드와 다르게 스테이트풀 파드는 각각의 파드가 서로 다르기 떄문에 스테이트풀셋은 거버닝 헤드리스 서비스를 생성해서 각 파드에게 실제 네트워크 아이덴티티를 제공한다. 위의 그림에서 a-0.foo.default.svc.cluster.local
와 같이 파드의 이름과 함께 FQDN을 통해 접근 가능하다. 그리고 foo.default.svc.cluster.local
도메인의 SRV 레코드를 조회해 모든 스테이트풀셋의 파드 이름을 찾는 목적으로 DNS를 사용할 수 있다.
앞서 말했듯이 새롭게 생성되는 파드는 잃어버린 파드와 동일한 아이덴티티를 가진다. 스케일업 시에는 인덱스가 늘어나고(위의 그림에서는 A-2가 생성될 것임), 스케일 다운인 경우에는 인덱스가 높은 파드부터 삭제가 된다.
특정 스테이트풀 애플리케이션은 빠른 스케일 다운을 잘 처리하지 못하기 때문에 스테이트풀셋은 한 시점에 하나의 파드 인스턴스만 스케일 다운한다.
분산 데이터 스토어라면 여러 개 노드가 동시에 다운되면 데이터를 잃을 수 있다. 스케일 다운이 순차적으로 일어나면 분산 데이터 저장소는 손실된 복사본을 대체하기 위한 데이터 엔트리의 추가 복제본을 다른 곳에 생성할 시간을 갖게 된다.
이러한 이유로 스테이트풀셋은 인스턴스 하나라도 비정상인 경우 스케일 다운 작업을 허용하지 않는다.
스테이트풀셋은 파드와 같이 퍼시스턴트볼륨클레임 또한 생성한다. 이렇게 각 파드와 매칭되는 클레임을 얻게되는 것이다.
그러나 스케일 다운을 할 때에는 파드만 삭제하고 클레임은 남겨둔다. 이후 스케일업 될 때에 생성해두었던 클레임을 다시 이용하게 된다.
클레임이 삭제되면 콘텐츠 손실이 일어나기 때문에 치명적인 문제가 된다. 따라서 이렇게 보관을 하는 것이다.
그리고 스테이트풀셋은 파드를 생성하기 전에 파드가 더 이상 실행 중이지 않는다는 점을 확인을 하고 생성한다. 그렇지 않고 동일한 아이덴티티를 가진 파드가 생성이되면 결국 두 인스턴스는 동일한 스토리지에 바인딩되고 두 프로세스가 동일한 아이덴티티로 같은 파일을 쓰려고 할 것이기 때문이다.
스테이트풀셋 사용하기
스테이트풀셋을 통해 애플리케이션을 배포하려면 다른 유형의 오브젝트 2가지 또는 3가지를 생성해야 한다.
- 데이터 저장을 위한 퍼시스턴트볼륨(클러스터가 퍼시스턴트볼륨의 동적 프로비저닝을 지원하지 않는다면 직접 생성해야함)
- 스테이트풀셋에 필요한 거버닝 서비스
- 스테이트풀셋 자체
kind: List
apiVersion: v1
items:
- apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-a-poz
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle # 클레임에서 볼륨이 해제되면 다시 사용해 재사용된다
hostPath:
path: /tmp/pv-a-poz
- apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-b-poz
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-b-poz
- apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-c-poz
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-c-poz
먼저 퍼시스턴트볼륨을 3개 만들었다.
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None # 스테이트풀셋의 거버닝 서비스는 헤드리스여야 한다.
selector:
app: kubia
ports:
- name: http
port: 80
헤드리스 서비스를 생성하였다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kubia
spec:
serviceName: kubia
replicas: 2
selector:
matchLabels:
app: kubia # has to match .spec.template.metadata.labels
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data # 파드 내부의 컨테이너는 pvc 볼륨을 이 경로에 마운트한다
mountPath: /var/data
volumeClaimTemplates: # 이 템플릿으로 퍼시스턴트볼륨클레임이 생성된다
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
파드는 매니페스트 안에 퍼시스턴트볼륨클레임 볼륨을 포함시켜 클레임을 참조한다.
스테이트풀셋을 생성하고 파드를 살펴보면 1개씩 생성되는 것을 확인할 수 있다. 이것은 동시에 생성 시에 레이스 컨디션에 빠질 가능성이 있기 때문이다.
k get pvc
를 입력하면 퍼시스턴트볼륨클레임이 생성된 것을 확인할 수가 있다.
파드를 생성하는데 사용한 이미지는 post 요청으로 들어온 메세지를 저장해뒀다가 get 요청을 하면 응답을 하는 이미지다.
파드에 post 요청으로 메세지를 저장해두고 해당 파드를 삭제 후 재스케줄링된 파드가 동일한 스토리지에 연결되어서 이전의 파드가 저장해둔 메세지를 그대로 출력하는지 확인해보자.
우선 k exec -it kubia-0 /bin/sh
를 통해 파드에 접속해서 curl 요청으로 메세지를 저장하고 나왔다.
우선 파드를 삭제하고 재생성 시켰다.
파드에 들어가서 get 요청을 날려보니 저장했던 메세지를 그대로 출력하는 것을 확인할 수가 있다.
위와 같은 흐름으로 내부적으로 돌아간 것이다.
스테이트풀셋의 피어 디스커버리
스테이트풀셋의 각 멤버는 다른 멤버를 쉽게 찾을 수 있어야 한다. API 서버와 통신해 찾을 수 있지만 쿠버네티스의 목표 중 하나인 애플리케이션을 완전히 쿠버네티스에 독립적으로 유지하며 기능을 노출에 부합하지 않기 때문에 다른 멤버를 찾을 수 있어야 한다. 이를 SRV 레코드를 이용하여 해결한다.
SRV 레코드는 특정 서비스를 제공하는 서버의 호스트 이름과 포트를 가리키는 데 사용된다. 쿠버네티스는 헤드리스 서비스를 뒷받침하는 파드의 호스트 이름을 가리키도록 SRV 레코드를 생성한다.
k run -it srvlookup --image=tutum/dnsutils --rm --restart=Never -- dig SRV kubia.default.svc.cluster.local
명령어를 이용하여 srvlookup 이라는 1회용 파드를 실행하고 콘솔에 연결되며 종료되자마자 바로 삭제된다. 파드는 tutum/dnsutils 이미지의 단일 컨테이너를 실행하고 dig SRV kubia.default.svc.cluster.local
명령을 수행한다.
명령을 수행하면 ANSWER SECTION에는 헤드리스 서비스를 뒷받침하는 두 개의 파드를 가리키는 두 개의 SRV 레코드를 보여준다.
스테이트풀셋 업데이트
k edit statefulset [name]
을 통해 스테이트풀셋을 업데이트 할 수 있다. replicas를 늘리면 생성이 되지만 이미지를 변경해도 레플리카셋과 마찬가지로 파드가 재생성되어야만 새로운 이미지로 업데이트가 된다. 따라서 수동으로 파드를 삭제해야 적용이 된다.
스테이트풀셋이 노드 실패를 처리하는 과정 이해하기
위에서 한 번 언급을 했듯이 스테이트풀 파드는 더 이상 실행중이지 않음을 절대적으로 확신해야 한다고 말했다. 만약 노드가 갑자기 실패하면 쿠버네티스는 노드나 그 안의 파드의 상태를 알 수 없다. 스테이트풀셋은 파드가 더 이상 실행되지 않는다는 것을 확신할 때까지 대체 파드를 생성할 수 없으며, 생성해서도 안된다. 오직 클러스터 관리자가 알려줘야만 알 수 있다. 이를 위해 관리자는 파드를 삭제하거나 전체 노드를 삭제해야 한다.
노드가 실패하게되면 k get node
시에 노드의 status가 NotReady
상태가 되며, 해당 노드에 속한 파드는 Unknown
상태가 된다.
노드가 다시 돌아오면 파드는 Running
으로 표시되지만, 몇 분이 지나도 파드의 상태가 Unknown
으로 남아있으면 파드는 자동으로 노드에서 제거된다. 마스터가 이 동작을 수행한다. 파드 리소스를 삭제해 파드를 제거한다. 파드의 종료 이유에 NodeLost로 찍히고 Terminating으로 표시된다.
노드가 돌아오지 않았지만 정상적인 애플리케이션 실행을 위해서는 약속된 개수의 파드가 실행 중이어야 하므로 노드나 파드를 수동으로 삭제해야 한다.
k delete po [pod name]
을 해도 po 상태가 제대로 삭제가 되지 않는 경우가 있는데, 이 때에는 kubelet이 파드가 더 이상 실행 중이지 않음을 확인해주는 것을 기다리지 않고 API 서버에게 파드를 삭제하도록 알리는 것이다.
k delete po [pod name] --force --grace-period 0
이렇게 파드를 강제 삭제할 수 있다. 노드가 더 이상 실행 중이 아니거나 연결 불가함을 아는 경우가 아니라면, 스테이트풀 파드를 강제로 삭제하지 말아야 한다.
쿠버네티스 내부 이해
아키텍처 이해
컨트롤 플레인 구성 요소
- etcd 분산 저장 스토리지
- API 서버
- 스케줄러
- 컨트롤러 매니저
워커 노드에서 실행하는 구성 요소
- Kubelet
- 쿠버네티스 서비스 프록시
- 컨테이너 런타임
애드온 구성 요소
- 쿠버네티스 DNS 서버
- 대시보드
- 인그레스 컨트롤러
- 힙스터
- 컨테이너 네트워크 인터페이스 플러그인
k get componentstatuses
NAME STATUS MESSAGE ERROR
controller-manager Healthy ok
scheduler Healthy ok
etcd-0 Healthy {"health":"true","reason":""}
구성요소들은 오직 API 서버하고만 통신한다. API 서버는 etcd와 통신하는 유일한 구성 요소다. 다른 구성요소는 etcd와 직접 통신하지 않고, API 서버로 클러스터 상태를 변경한다.
위의 그림과 같이 API 서버와 구성 요소 사이의 통신은 대부분 구성 요소에서 시작하지만, kubectl을 이용해 로그를 가져오거나 kubectl attach 명령으로 실행 중인 컨테이너에 연결할 때 kubectl port-forward 명령을 실행할 때는 API 서버가 Kubelet에 접속한다.
컨트롤 플레인을 둘 이상 두어 병렬로 수행할 수 있다. 이때 etcd와 API 서버는 여러 인스턴스를 동시에 활성화해 작업을 병렬로 수행할 수 있지만, 스케줄러와 컨트롤러 매니저는 하나의 인스턴스만 활성화되고 나머지는 대기 상태에 있게 된다.
Kubelet은 유일하게 일반 시스템 구성 요소로 실행되며, Kubelet이 다른 구성 요소를 파드에 실행한다. 컨트롤 플레인 구성 요소를 파드로 실행하기 위해 Kubelet도 마스터 노드에 배포된다.
쿠버네티스가 etcd를 사용하는 방법
모든 오브젝트는 API 서버가 다시 시작되거나 실패하더라도 유지하기 위해 매니페스트가 영구적으로 저장될 필요가 있다. 이를 위해 쿠버네티스는 빠르고, 분산해서 저장되며, 일관된 키-값 저장소를 제공하는 etcd를 사용한다. 분산돼 있기 때문에 둘 이상의 etcd 인스턴스를 실행해 고가용성과 우수한 성능을 제공할 수 있다.
저장된 오브젝트의 일관성과 유효성 보장
쿠버네티스는 etcd 접근을 API 서버로만 하고 다른 구성 요소가 API서버를 통하게 함으로써 API 서버 한곳에서 낙관적 잠금 메커니즘을 구현해 구성 요소의 데이터 불일치를 개선시켰다. 또한 API 서버는 저장소에 기록된 데이터가 항상 유효하고 데이터의 변경이 올바른 권한을 가진 클라이언트에 의해서만 수행되도록 한다.
클러스터링된 etcd의 일관성 보장
두 개 이상의 etcd 인스턴스를 사용하면서 etcd는 RAFT 합의 알고리즘을 사용해 어느 순간이든 각 노드 상태가 대다수의 노드가 동의하는 현재 상태이거나 이전에 동의된 상태 중에 하나임을 보장한다.
합의 알고리즘은 클러스터가 다음 상태로 진행하기 위해 과반수가 필요하다. etcd 인스턴스 수가 홀수인 이유이기도 하다.
API 서버의 기능
다른 구성요소로부터 클러스터 상태를 조회하고 변경하기 위해 RESTFul API로 CRUD 인터페이스를 제공하고 상태는 etcd안에 저장한다.
오브젝트를 etcd에 저장하는 일관된 방법을 제공하는 것뿐만 아니라, 오브젝트 유효성 검사 작업도 수행하기 때문에 잘못 설정된 오브젝트를 저장할 수 없다.
낙관적 잠금도 처리하기 때문에 동시에 업데이터가 발생하더라도 다른 클라이언트에 의해 오브젝트의 변경 사항이 재정의되지 않는다.
API 서버에 구성된 하나 이상의 플러그인을 이용해 요청을 보낸 클라이언트를 인증한다. 인가 또한 마찬가지다.
리소스를 생성, 수정, 삭제하려는 요청인 경우에 해당 요청은 어드미션 컨트롤로 보내진다.
요청이 모든 어드미션 컨트롤 플러그인을 통과하면, API 서버는 오브젝트의 유효성을 검증하고 etcd에 저장한다.
API 서버는 레플리카셋 리소스를 만들 때 파드를 만들지 않고 서비스의 엔드포인트를 관리하지 않는다. 이것들은 컨트롤러 매니저의 컨트롤러들이 하는 일이다. API 서버는 컨트롤러한테 무엇을 해야하는지 알려주지 않고 컨트롤러와 다른 구성요소가 배포된 리소스의 변경 사항을 관찰할 수 있게끔 한다.
오브젝트가 갱신될 때마다, 서버는 오브젝트를 감시하고 있는 연결된 모든 클라이언트에게 오브젝트의 새로운 버전을 보낸다.
k get pods --watch
로 생성되고 삭제되는 파드를 감시할 수 있다.
스케줄러 이해
파드가 어떤 노드에 올라갈 것인지는 스케줄러가 담당핳ㄴ다. API 서버의 감시 메커니즘을 통해 새로운 파드를 기다리고 있다가 할당된 노드가 없는 새로운 파드를 노드에 할당하기만 한다.
선택된 노드에 파드를 실행하도록 지시하지 않고 API 서버로 파드 정의를 갱신한다. API 서버는 Kubelet에 파드가 스케줄링된 것을 통보한다(앞에서 설명한 감시 메커니즘을 통해).
수용 가능한 노드를 찾을 때에 하드웨어 리소스에 대한 파드 요청을 충족시킬 수 있는지, 노드가 파드 정의 안에 있는 노드 셀렉터와 일치하는 레이블을 가지고 있는지 등등의 모든 검사를 통과해야만 한다. 이에 부합하는 노드가 여러대라면 이중 적합한 노드를 선택한다.
파드의 레플리카가 여러개라면 가능한 많은 노드에 분산시키는 것이 이상적이다. 동일한 서비스또는 레플리카셋에 속한 파드는 기본적으로 여러 노드에 분산된다. 항상 그런 것은 아니지만, 어피니티와 안티 어피니티 규칙을 정의해 클러스터 전체에 퍼지거나 가깝게 유지되도록 강제할 수 있다.
클러스터에서 여러 개의 스케줄러를 실행할 수도 있다. 파드 정의에 schedulerName을 두어서 스케줄러를 선택할 수도 있다. 이 속성을 설정하지 않은 파드는 기본적으로 기본 스케줄러를 사용한다. 스케줄러명을 default-scheduler로 설정한 파드 또한 마찬가지다.
컨트롤러 매니저에서 실행되는 컨트롤러 소개
API 서버에 배포된 리소스에 지정된 대로 시스템을 원하는 상태로 수렴되도록 하는 활성 구성 요소로 컨트롤러 매니저 안에서 실행되는 컨트롤러에 의해 수행된다.
- 레플리케이션 매니저(레플리케이션컨트롤러 리소스의 컨트롤러)
- 레플리카셋, 데몬셋, 잡 컨트롤러
- 디플로이먼트 컨트롤러
- 서비스 컨트롤러
등등 각 컨트롤러들은 API 서버에서 리소스가 변경되는 것을 감지하고 각 변경작업을 수행한다. 대부분 이러한 작업은 다른 리소스 생성, 감시 중인 리로스 자체를 갱신하는 것이 포함된다. 컨트롤러는 서로 대화하지 않고 다른 컨트롤러가 존재하는지도 모른다. 각 컨트롤러는 API 서버에 연결하고 감시 메커니즘을 통해 컨트롤러가 담당하는 리소스 유형에서 변경이 발생하면 통보해줄 것을 요청한다.
Kubelet이 하는 일
Kubelet이 실행 중인 노드를 노드 리소스로 만들어서 API 서버에 등록하는 것이 첫 번째 일이다. 그런 다음 API 서버를 지속적으로 모니터링해 해당 노드에 파드가 스케줄링되면, 파드의 컨테이너를 시작한다. 설정된 컨테이너 런타임에 지정된 컨테이너 이미지로 컨테이너를 실행하도록 지시함으로써 이 작업을 수행한다. 그런 다음 Kubelet은 실행 중인 컨테이너를 계속 모니터링하면서 상태, 이벤트, 리소스 사용량을 API 서버에 보고한다.
Kubelet은 컨테이너 라이브니스 프로브를 실행하는 구성 요소이기도 하며, 프로브가 실패할 경우 컨테이너를 다시 시작한다. 마지막으로 API 서버에서 파드가 삭제되면 컨테이너를 정지하고 파드가 종료된 것을 서버에 통보한다.
API 서버를 이용하여 파드 매니페스트를 가져오는 방법 외에 그림과 같이 특정 로컬 디렉터리 안에 있는 파일을 기반으로 파드를 실행할 수도 있다.
쿠버네티스 시스템 구성 요소가 기본적으로 실행되도록 하는 대신, 시스템 구성 요소 파드 매니페스트를 Kubelet의 매니페스트 디렉터리 안에 넣어서 Kubelet이 실행하고 관리하도록 할 수 있다.
쿠버네티스 서비스 프록시의 역할
kube-proxy의 초기 구현은 사용자 공간에서 동작하는 프록시였다. 실제 서버가 프로세스가 연결을 수락하고 이를 파드로 전달했다. 서비스 IP로 향하는 연결을 가로채기 위해 프록시는 iptables 규칙을 설정해 이를 프록시 서버로 전송했다.
현재는 iptables 규칙만 사용해 프록시 서버를 거치지 않고 패킷을 무작위로 선택한 백엔드 파드로 전달한다.
컨트롤러가 협업하는 방법
컨트롤러와 스케줄러 그리고 Kubelet은 API 서버에서 각 리소스 유형이 변경되는 것을 감시한다.
kubectl 명령으로 디플로이먼트 yaml을 쿠버네티스에 게시하게 되면 위와 같이 이벤트들이 연계된다.
클러스터 이벤트 관찰
구성요소와 Kubelet은 위와 같은 작업을 수행할 때 API 서버로 이벤트를 발송하는데 다른 리소스들과 마찬가지로 이벤트 리소스를 만들어 이를 수행한다.
kubectl get events --watch
를 통해 컨트롤러가 생성한 이벤트를 관찰할 수 있다.
실행 중인 파드에 관한 이해
퍼즈 컨테이너는 파드의 모든 컨테이너를 함께 담고 있는 컨테이너다. 퍼즈 컨테이너는 동일한 네트워크와 리눅스 네임스페이스를 모두 보유하는게 유일한 목적인 인프라스트럭처 컨테이너다.
파드 간 네트워킹
파드A가 바라보는 파드B의 IP주소는 파드B의 IP 주소와 일치해야 한다. 파드 사이에 NAT가 없으면 내부에서 실행 중인 애플리케이션이 다른 파드에 자동으로 등록되도록 할 수 있다. 파드 간에 NAT 없이 통신해야 한다는 요구 사항은 파드와 노드 그리고 노드와 파드 간에 통신할 때 동일하게 요구된다. 그러나 파드가 인터넷에 있는 서버스와 통신할 때는 패킷의 출발지 IP를 변경하는 것이 필요하다. 파드의 IP는 사설이기 때문이다. 외부로 나가는 패킷의 출발지 IP는 호스트 워커 노드의 IP로 변경된다.
서비스 구현 방식
kube-proxy 소개
서비스와 관련된 모든 것은 각 노드에서 동작하는 kube-proxy 프로세스에 의해 처리된다. 초기에는 kube-proxy가 실제 프록시로서 연결을 기다리다가, 들어온 연결을 위해 해당 파드로 가는 새로운 연결을 생성했다. 이것을 userspace 프록시 모드라고 한다. 나중에는 성능이 더 우수한 iptables 프록시 모드가 이를 대체했다.
각 서비스는 안정적인 IP 주소와 포트를 얻는다. 클라이언트는 이를 이용해 서비스에 접속해 사용한다. 이 IP 주소는 가상이다. 어떠한 네트워크 인터페이스에도 할당되지 않고 패킷이 노드를 떠날 때 네트워크 패킷안에 출발지 혹은 도착지 IP 주소로 표시되지 않는다. 서비스의 주요 핵심 사항은 서비스가 IP와 포트의 쌍으로 구성된다는 것으로, 서비스 IP만으로는 아무것도 나타내지 않는다. 이게 서비스에 핑을 보낼 수 없는 이유다.
kube-proxy가 iptables를 사용하는 방법
API 서버에서 서비스를 생성하면, 가상 IP 주소가 바로 할당된다. 곧이어 API 서버는 워커 노드에서 실행 중인 모든 kube-proxy 에이전트엥 새로운 서비스가 생성됐음을 통보한다. 각 kube-proxy는 실행 중인 노드에 해당 서비스 주소로 접근할 수 있도록 만든다. 이것은 서비스의 IP/포트 쌍으로 향하는 패킷을 가로채서, 목적지 주소를 변경해 패킷이 서비스를 지원하는 여러 파드 중 하나로 리디렉션되도록 하는 몇 개의 iptables 규칙을 설정함으로써 이뤄진다.
kube-proxy는 API 서버에서 서비스가 변경되는 것을 감지하는 것 외에도, 엔드포인트 오브젝트가 변경되는 것을 같이 감시한다.
처음에 패킷의 목적지는 서비스의 IP와 포트로 지정된다. 패킷이 네트워크로 전송되기 전에 노드 A의 커널이 노드에 설정된 iptables 규칙에 따라 먼저 처리한다. 커널은 패킷이 iptables 규칙 중에 일치하는게 있는지 검사한다. 그 규칙 중 하나에서 패킷 중에 목적지 IP가 172.30.0.1이고 목적지 포트가 80인 포트가 있다면, 임의로 선택된 파드의 IP와 포트로 교체돼야 한다고 알려준다.
고가용성 클러스터 실행
애플리케이션 가용성 높이기
디플로이먼트 리소스로 애플리케이션을 실행하고 적절한 수의 레플리카를 설정하면 애플리케이션의 가용성이 높아진다.
가동 중단 시간을 줄이기 위해 다중 인스턴스로 실행한다. 수평 스케일링이 불가능한 애플리케이션을 위해 리더 선출 매커니즘을 사용한다.
쿠버네티스 컨트롤 플레인 구성 요소의 가용성 향상
- etcd 클러스터 실행
- etcd는 분산 시스템으로 설계됐으므로 그 주요 기능은 여러 etcd 인스턴스를 실행하는 기능이라, 가용성을 높이는 것은 큰 문제가 되지 않는다.
- 여러 API 서버 인스턴스 실행
- API 서버는 상태를 저장하지 않기 때문에, 필요한 만큼 많은 API 서버를 실행할 수 있고 서로 인지할 필요도 없다.
- 일반적으로 모든 etcd 인스턴스에 API 서버를 함께 띄운다.
- 컨트롤러와 스케줄러 고가용성 확보
- 컨트롤러와 스케줄러는 클러스터 상태를 감시하고 상태가 변경될 때 반응해야 하는데 이런 구성 요소의 여러 인스턴스가 동시에 실행돼 같은 동작을 수행하면, 클러스터 상태가 예상보다 더 많이 변경될 가능성이 있다. 따라서 하나의 인스턴스만 활성화하고 나머지는 현재 활성화된 리더가 실패할 경우 새로운 리더 선출을 통해 동작하게 된다.
리더 선출 메커니즘은 API 서버에 오브젝트를 생성하는 것만으로 완전히 동작한다.
쿠버네티스 API 서버 보안
인증 이해
API 서버를 하나 이상의 인증 플러그인으로 구성할 수 있다고 11장에서 언급을 했는데, API 서버가 요청을 받으면 인증 플러그인 목록을 거치면서 요청이 전달되고, 각각의 인증 플러그인이 요청을 검사해서 보낸 사람이 누구인가를 밝혀내려 시도한다. 요청에서 해당 정보를 처음으로 추출해낸 플러그인은 사용자 이름, 사용자 ID와 클라이언트가 속한 그룹을 API 서버 코어에 반환한다. API 서버는 나머지 인증 플러그인의 호출을 중지하고, 계속해서 인가 단계를 진행한다.
사용자와 그룹
인증 플러그인은 인증된 사용자의 사용자 이름과 그룹을 반환한다. 쿠버네티스는 이 정보를 저장하지 않는다.
사용자
쿠버네티스는 API 서버에 접속하는 두 종류의 클라이언트를 구분한다.
- 실제 사람(사용자)
- 파드(파드 내부에서 실행되는 애플리케이션)
사용자는 SSO와 같은 외부 시스템에 의해 관리돼야 하지만 파드는 서비스 어카운트라는 메커니즘을 사용하며, 클러스터에 서비스어카운트 리소스로 생성되고 저장된다. 사용자 계정을 나타내는 자원은 없다. 이는 API 서버를 통해 사용자를 생성, 업데이트 또는 삭제할 수 없다는 뜻이다.
그룹
휴먼 사용자와 서비스어카운트는 하나 이상의 그룹에 속할 수 있다. 인증 플러그인은 사용자 이름 및 사용자 ID와 함께 그룹을 반환한다.
그룹은 개별 사용자에게 권한을 부여하지 않고 한 번에 여러 사용자에게 권한을 부여하는 데 사용된다.
인증 플러그인이 반환하는 그룹은 임의의 그룹ㄹ 이름을 나타내는 문자열일 뿐이지만, 내장된 그룹은 특별한 의미를 갖는다.
- system:unauthenticated 그룹은 어떤 인증 플러그인에서도 클라이언트를 인증할 수 없는 요청에 사용된다.
- system:authenticated 그룹은 성공적으로 인증된 사용자에게 자동으로 할당된다.
- system:serviceaccounts 그룹은 시스템의 모든 서비스어카운트를 포함한다.
- system:serviceaccounts:는 특정 네임스페이스의 모든 서비스어카운트를 포함한다.
서비스어카운트 소개
모든 파드는 파드에서 실행 중인 애플리케이션의 아이덴티티를 나타내는 서비스어카운트와 연계돼 있다. 이 토큰 파일은 서비스어카운트의 인증 토큰을 갖고 있다. 애플리케이션이 이 토큰을 사용해 API 서버에 접속하면 인증 플러그인이 서비스어카운트를 인증하고 서비스어카운트의 사용자 이름을 API 서버 코어로 전달한다. 서비스 어카운트의 사용자 이름은 다음과 같은 형식이다.
system:serviceaccount:<namespace>:<service account name>
API 서버는 설정된 인가 플러그인에 이 사용자 이름을 전달하며, 이 인가 플러그인은 애플리케이션이 수행하려는 작업을 서비스어카운트에서 수행할 수 있는지를 결정한다. 서비스어카운트는 파드 내부에서 실행되는 애플리케이션이 API 서버에 자신을 인증하는 방법에 지나지 않는다. 애플리케이션은 요청에 서비스어카운트의 토큰을 전달해서 이 과정을 수행한다.
서비스어카운트 리소스
서비스어카운트는 파드, 시크릿, 컨피그맵 등과 같은 리소스이며 개별 네임스페이스로 범위가 지정된다. 각 네임스페이스마다 default 서비스어카운트가 자동으로 생성된다. kubectl get sa
로 서비스어카운트를 나열할 수 있다.
각 파드는 딱 하나의 서비스와 연계되지만 여러 파드가 같은 서비스어카운트를 사용할 수 있다. 그러나 다른 네임스페이스에서 다른 네임스페이스의 서비스어카운트를 사용할 수는 없다.
서비스어카운트가 인가와 어떻게 밀접하게 연계돼 있는지 이해하기
파드에 서로 다른 서비스어카운트를 할당하면 각 파드가 액세스할 수 있는 리소스를 제어할 수 있다. API 서버가 인증 토큰이 있는 요청을 수신하면, API 서버는 토큰을 사용해 요청을 보낸 클라이언트를 인증한 다음 관련 서비스어카운트가 요청된 작업을 수행할 수 있는지 여부를 결정한다. API 서버는 클러스터 관리자가 구성한 시스템 전체의 인가 플러그인에서 이 정보를 얻는다.
서비스어카운트 생성
default 서비스어카운트를 사용하지 않고 새롭게 생성하는 이유는 클러스터 보안 때문이다. 클러스터의 메타데이터를 읽을 필요가 없는 파드는 클러스터에 배포된 리소스를 검색하거나 수정할 수 없는 제한된 계정으로 실행해야 한다. 리소스의 메타데이터를 검색해야 하는 파드는 해당 오브젝트의 메타데이터만 읽을 수 있는 서비스어카운트로 실행해야 하며, 오브젝트를 수정해야 하는 파드는 API 오브젝트를 수정할 수 잇는 고유한 서비스어카운트로 실행해야 한다.
서비스어카운트 생성
k create serviceaccount <name>
으로 손쉽게 생성 가능하다.
kubectl describe를 사용해 서비스어카운트를 검사하면 토큰이 Mountable secrets 목록에 표시된다.
파드에 서비스어카운트 할당
apiVersion: v1
kind: Pod
metadata:
name: curl-custom-sa
spec:
serviceAccountName: foo
containers:
- name: main
image: curlimages/curl
command: ["sleep", "9999999"]
- name: ambassador
image: luksa/kubectl-proxy:1.6.2
추가 서비스어카운트를 만든 후에는 파드에 할당해야한다. spec.serviceAccountName에 이름을 설정하면 된다.
k exec -it curl-custom-sa -c main cat /var/run/secrets/kubernetes.io/serviceaccount/token
으로 토큰이 두 개의 컨테이너에 할당됐는지 확인한다.
역할 기반 액세스 제어로 클러스터 보안
RBAC 인가 플러그인 소개
쿠버네티스 API 서버는 인가 플러그인을 사용해 액션을 요청하는 사용자가 액션을 수행할 수 있는지 점검하도록 설정할 수 있다. API 서버가 REST 인터페이스를 제공하므로 사용자는 서버에 HTTP 요청을 보내 액션을 수행한다. 사용자는 요청에 자격증명을 포함시켜 자신을 인증한다.
API 서버 내에서 실행되는 RBAC와 같은 인가 플러그인은 클라이언트가 요청한 자원에서 요청한 동사를 수행할 수 있는지를 판별한다.
HTTP 메서드 | 단일 리소스에 대한 동사 | 컬렉션에 관한 동사 |
---|---|---|
GET, HEAD | get | list (and watch) |
POST | create | n/a |
PUT | update | n/a |
PATCH | patch | n/a |
DELETE | delete | deleteCollection |
전체 리소스 유형에 보안 권한을 적용하는 것 외에도 RBAC 규칙은 특정 리소스 인스턴스에도 적용할 수 있다.
RBAC 인가 플러그인은 사용자가 액션을 수행할 수 있는지 여부를 결정하는 핵심 요소로 사용자의 role을 사용한다.
RBAC 리소스 소개
RBAC 인가 규칙은 4개의 리소스로 구성되며 두 개의 그룹으로 분류할 수 있다.
- 롤과 클러스터롤: 리소스에 수행할 수 있는 동사를 지정한다.
- 롤바인딩과 클러스터롤바인딩: 위의 롤을 특정 사용자, 그룹 또는 서비스어카운트에 바인딩한다.
롤은 수행할 수 있는 작업을 정의하고, 바인딩을 누가 이를 수행할 수 있는지를 정의한다.
롤과 롤바인딩은 네임스페이스가 지정된 리소스이고 클러스터롤과 클러스터롤바인딩은 네임스페이스를 지정하지 않는 클러스터 수준의 시소스다.
롤과 롤바인딩 사용
a와 b 네임스페이스에 파드를 각각 만들고 둘 중 아무 파드에 들어가서 curl localhost:8001/api/v1/namespaces/a/services
를 하면 403 에러가 나온다. 이걸 롤과 롤바인딩을 사용해서 볼 수 있게끔 한다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: bepoz # 롤은 네임스페이스가 지정된다. (생략 시 현재의 네임스페이스가 된다)
name: service-reader
rules:
- apiGroups: [""] # 서비스는 이름이 없는 core apiGroup의 리소스다. 따라서 "" 이다.
verbs: ["get", "list"] # 개별 서비스를 가져오고(이름으로), 모든 항목의 나열이 허용된다.
resources: ["services"] # 이 규칙(rule)은 서비스와 관련있다.
bepoz namespace에 role을 등록한다.
k create rolebinding <name> --role=service-reader --serviceaccount=bepoz:default
명령어를 사용하여 롤을 바인딩한다.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
creationTimestamp: "2023-06-25T13:20:31Z"
name: test
namespace: bepoz
resourceVersion: "25345152"
uid: 231d484c-ae8a-4546-9aa6-87634d982059
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: service-reader # 롤바인딩은 service-reader 롤을 참조한다.
subjects:
- kind: ServiceAccount
name: default
namespace: bepoz # bepoz 네임스페이스에 있는 default 서비스어카운트에 바인드한다.
k get rolebinding test -o yaml
로 확인을 해보면 위와 같다.
위의 namespace를 변경해서 다른 네임스페이스에서 현재 위치한 네임스페이스의 서비스어카운트에 바인딩 할 수 있다.
클러스터롤과 클러스터바인딩 사용하기
롤과 롤바인딩은 네임스페이스가 지정된 리소스다. 따라서 특정 네임스페이스가 아닌 모든 네임스페이스에 필요한 경우 각 네임스페이스마다 생성을 해줘야 한다. 어떤 리소스는 전혀 네임스페이스를 지정하지 않는다(노드, 퍼시스턴트볼륨, 네임스페이스 등). 그리고 API 서버는 리소스를 나타내지 않는 일부 URL 경로를 노출한다. 일반적인 롤로는 이런 리소스나 리로스가 아닌 URL에 관한 액세스 권한을 부여할 수 없지만 클러스터롤은 가능하다.
클러스터롤은 네임스페이스가 지정되지 않은 리소스나 리소스가 아닌 URL에 액세스를 허용하는 클러스터 수준의 리소스로 각각의 네임스페이스에 동일한 롤을 재정의할 필요 없이 개별 네임스페이스에 바인드해서 공통적인 롤로 사용할 수 있다.
클러스터 수준 리소스에 액세스 허용
k create clusterrole pv-reader --verb=get,list --resource=persistentvolumes
클러스터롤을 만든다.
클러스터롤에 일반 롤바인딩을 사용하면 제대로 되지 않는다.
k create clusterrolebinding pv-test --clusterrole=pv-reader --serviceaccount=bepoz:default
로 클러스터롤바인딩을 해주었다. 이제는 정상적으로 접근이 가능해진다.
클러스터 노드와 네트워크 보안
파드에서 호스트 노드의 네임스페이스 사용
파드의 컨테이너는 일반적으로 별도의 리눅스 네임스페이스에서 실행되므로 프로세스가 다른 컨테이너 또는 노드의 기본 네임스페이스에서 실행 중인 프로세스와 분리된다. 예를 들어 각 파드는 고유한 네트워크 네임스페이스를 사용하기 때문에 고유한 IP와 포트 공간을 얻는다. 마찬가지로 각 파드는 고유한 PID 네임스페이스가 있기 때문에 고유한 프로세스 트리가 있으며 고유한 IPC 네임스페이스도 사용하므로 동일한 파드의 프로세스 간 통신 메커니즘으로 서로 통신할 수 있다.
파드에서 노드의 네트워크 네임스페이스 사용
특정 파드는 호스트의 기본 네임스페이스에서 작동해야 노드의 리소스와 장치를 읽고 조작할 수 있따. 예를 들어 파드는 가상 네트워크 어댑터 대신 노드의 실제 네트워크 어댑터를 사용해야 할 수도 있다. 이는 파드 스펙에서 hostNetwork 속성을 true로 설정하면 된다.
위와 같이 파드의 네트워크 인터페이스가 아니라 노드의 네트워크 인터페이스를 사용하게 된다. 이는 파드가 자체 IP 주소를 갖는 것이 아니라, 포트를 바인드하는 프로세스를 실행할 경우 해당 프로세스는 노드의 포트에 직접 바인드된다는 의미다.
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-network
spec:
hostNetwork: true
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
spec.hostNetwork
를 true로 주고 파드를 생성 후 k exec pod-with-host-network ifconfig
를 입력해보면 실제 호스트 네트워크 네임스페이스를 사용하는지 확인할 수 있다.
쿠버네티스 컨트롤 플레인 구성 요소가 파드로 배포되면 해당 파드는 hostNetwork 옵션을 사용하므로 파드 안에서 실행되지 않는 것처럼 동작할 수 있다.
호스트 네트워크 네임스페이스를 사용하지 않고 호스트 포트에 바인딩
파드는 노드의 기본 네임스페이스의 포트에 바인딩할 수 있지만 여전히 고유한 네트워크 네임스페이스를 갖는다. 이는 컨테이너의 포트를 정의하는 spec.containers.ports
필드 안에 hostPort 속성을 사용해 할 수 있다.
파드가 hostPort를 사용하는 경우 노드포트에 대한 연결은 해당 노드에서 실행 중인 파드로 직접 전달되는 반면 NodePort 서비스의 경우 노드포트의 연결은 임의의 파드로 전달된다. 또 다른 차이점은 hostPort를 사용하는 파드의 경우 노드포트는 해당 파드를 실행하는 노드에만 바인딩되는 반면 NodePort 서비스는 이런 파드를 실행하지 않는 노드에서도 모든 노드의 포트를 바인딩한다는 것이다.
파드가 특정 호스트 포트를 사용하는 경우 두 프로세스가 동일한 호스트 포트에 바인딩될 수 없으므로 파드 인스턴스 하나만 노드에 스케줄링될 수 있다는 점을 이해해야 한다. 스케줄러는 파드를 스케줄할 때 이를 고려하므롤 밑의 그림과 같이 여러 파드를 동일한 노드에 스케줄링하지 않는다. 노드 3개가 있고 파드 레플리카 4개를 배포하려는 경우 3개만 스케줄링된다.
apiVersion: v1
kind: Pod
metadata:
name: kubia-hostport
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
hostPort: 9000
protocol: TCP
kubia 파드를 실행하고 노드포트 9000에 바인딩하는 yaml이다.
이 파드를 생성한 후 스케줄링된 노드포트 9000으로 액세스할 수 있다. 노드가 여러개인 경우 다른 노드의 해당 포트로 파드에 액세스할 수는 없다.
hostPort 기능은 기본적으로 데몬셋을 사용해 모든 노드에 배포되는 시스템 서비스를 노출하는 데 사용된다. 처음에는 사람들이 동일한 파드의 레플리카 2개가 동일한 노드에 스케줄링되지 않았는지 확인하려고 이 도구를 사용했지만 이제는 이를 확인하는 더 좋은 방법이 있다.
노드의 PID와 IPC 네임스페이스 사용
hostNetwork 옵션과 유사한 파드 스펙 속성으로 hostPID와 hostIPC가 있다. 이를 true로 설정하면 파드의 컨테이너는 노드의 PID와 IPC 네임스페이스를 사용해 컨테이너에서 실행 중인 프로세스가 노드의 다른 프로세스를 보거나 IPC로 이들과 통신할 수 있도록 한다.
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-pid-and-ipc
spec:
hostPID: true # 파드가 호스트의 PID 네임스페이스를 사용하도록 한다.
hostIPC: true # 파드가 호스트의 IPC 네임스페이스를 사용하도록 한다.
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
파드는 일반적으로 자체 프로세스만 표시하지만 이 파드를 실행한 후 컨테이너의 프로세스를 조회하면 컨테이너에서 실행 중인 프로세스뿐만 아니라 호스트 노드에서 실행 중인 모든 프로세스가 조회된다.
k exec pod-with-host-pid-and-ipc ps aux
로 호스트 노드에서 실행 중인 모든 프로세스가 조회된다.
hostIPC 속성을 true로 설정하면 파드 컨테이너의 프로세스는 노드에서 실행 중인 다른 모든 프로세스와 IPC로 통신할 수도 있다.
컨테이너의 보안 컨텍스트 구성
파드가 호스트의 리눅스 네임스페이스를 사용하도록 허용하는 것 외에도, 파드 컨텍스트 아래의 개별 컨테이너 스펙에서 직접 지정할 수 있는 securityContext 속성으로 다른 보안 관렬ㄴ 기능을 파드와 파드의 컨테이너에 구성할 수 있다.
보안 컨텍스트에서 설정할 수 있는 사항
- 컨테이너의 프로세스를 실행할 사용자 지정하기
- 컨테이너가 루트로 실행되는 것 방지하기
- 컨테이너를 특권 모드에서 실행해 노드의 커널에 관한 모든 접근 권한을 가짐
- 특권 모드에서 컨테이너를 실행해 컨테이너에 가능한 모든 권한을 부여하는 것과 달리 기능을 추가하거나 삭제해 세분화된 권한 구성하기
- 컨테이너의 권한 확인을 강력하게 하기 위해 SELinux 옵션 설정하기
- 프로세스가 컨테이너의 파일시스템에 쓰기 방지하기
보안 컨텍스트를 지정하지 않고 파드 실행
보안 컨텍스트가 설정된 파드와 그렇지 않은 파드의 동작 방식을 비교해보자.
k run pod-with-defaults --image alpine --restart Never -- /bin/sleep 999999
로 파드를 만들고,k exec pod-with-defaults id
를 입력해서 확인해보면 uid 0과 gid 0인 루트 사용자로 실행하고 있음을 확인할 수가 있다.
컨테이너를 특정 사용자로 실행
apiVersion: v1
kind: Pod
metadata:
name: pod-as-user-guest
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 405
컨테이너 이미지에 설정한 것과 다른 사용자 ID로 파드를 실행하려면 파드의 securityContext.runAsUser
속성을 설정해야 한다.
위의 파일에 나와있듯 alpine 컨테이너 이미지의 사용자 ID가 405인 게스트 사용자로 컨테이너를 실행한다.
k exec pod-as-user-guest id
를 입력해보면 uid 405에 gid 100인 것을 확인할 수가 있다.
컨테이너가 루트로 실행되는 것 방지
대부분의 컨테이너는 호스트 시스템과 분리돼 있지만 프로세스를 루트로 실행하는 것은 여전히 나쁜 관행이다. 예를 들어 호스트 디렉터리가 컨테이너에 마운트될 때 컨테이너에서 실행 중인 프로세스가 루트로 실행 중인 경우 마운트된 디렉터리에 관한 모든 액세스 권한이 있지만 루트가 아닌 경우에는 권한이 없다.
apiVersion: v1
kind: Pod
metadata:
name: pod-run-as-non-root
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsNonRoot: true # 이 컨테이너는 루트가 아닌 사용자로만 실행할 수 있다.
위의 파드는 배포 후 스케줄링되지만 실행되지는 않는다. 이제 누군가 컨테이너 이미지를 무단 변경해도 실행되지 않는다.
특권 모드에서 파드 실행
때때로 파드는 일반 컨테이너에서는 접근할 수 없는 보호된 시스템 장치나 커널의 다른 기능을 사용하는 것과 같이 그들이 실행 중인 노드가 할 수 있는 모든 것을 해야 할 수도 있다. 이런 파드의 예는 kube-proxy 파드가 있으며 서비스를 작동시키려 노드의 iptables 규칙을 수정한다.
노드 커널의 모든 액세스 권한을 얻기 위해 파드의 컨테이너는 특권 모드로 실행된다. 컨테이너의 securityContext 속성에서 privileged 속성을 true로 설정하면 된다.
apiVersion: v1
kind: Pod
metadata:
name: pod-privileged
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
privileged: true # 이 컨테이너는 특권 모드에서 실행될 것이다.
이 파드를 배포하면 이전에 실행한 권한이 없는 파드와 비교할 수 있다.
pod-with-defaults 파드와 위의 파드의 /dev 를 확인해보면(k exec -it <pod name> ls /dev
) 표시되는 장치의 수가 압도적으로 다르다.
권한이 있는 컨테이너는 모든 호스트 노드의 장치를 볼 수 있다. 즉, 모든 장치를 자유롭게 사용할 수 있다.
컨테이너에 개별 커널 기능 추가
권한 있는 컨테이너를 만들고 무제한 권한을 부여하는 대신 보안 관점에서 훨씬 안전한 방법은 실제로 필요한 커널 기능만 액세스하도록 하는 것이다.
쿠버네티스를 사용하면 각 컨테이너에 커널 기능을 추가하거나 일부를 삭제할 수 있으므로 컨테이너 권한을 미세 조정하고 공격자의 잠재적인 침입의 영향을 제한할 수 있다.
예를 들어 컨테이너는 일반적으로 시스템 시간을 변경할 수 없다.
apiVersion: v1
kind: Pod
metadata:
name: pod-add-settime-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
capabilities: # securityContext 속성 아래에 기능을 추가하거나 삭제할 수 있다.
add:
- SYS_TIME
위와 같이 SYS_TIME 기능이 추가된 컨테이너에서는 시스템 시간을 변경할 수 있다.
컨테이너에서 기능 제거
컨테이너에서 사용할 수 있는 기능을 제거할 수도 있다. 예를 들어 컨테이너에 제공되는 기본 기능에는 프로세스가 파일시스템에서 파일의 소유권을 변경할 수 있는 CAP_CHOWN 기능이 포함된다.
현재 pod-with-defaults에서 chown 을 사용할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: pod-drop-chown-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
capabilities:
drop: # 위 컨테이너에서는 파일 소유권을 변경할 수 없다.
- CHOWN
CHOWN 기능을 삭제하면 이 파드에서 /tmp 디렉터리의 소유자를 변경할 수 없다.
프로세스가 컨테이너의 파일시스템에 쓰는 것 방지
컨테이너에서 실행 중인 프로세스가 컨테이너의 파일시스템에 쓰지 못하게 하고 마운트된 볼륨에만 쓰도록 할 수 있다. 대부분 보안상의 이유로 이 작업이 필요하다. 공격자가 파일시스템에 쓸 수 있도록 숨겨진 취약점이 있는 PHP 애플리케이션을 실행한다고 가정해보자. PHP 파일은 빌드 시 컨테이너 이미지에 추가되고 컨테이너의 파일시스템에서 제공된다. 이 취약점으로 인해 공격자는 파일을 수정해 악성 코드를 삽입할 수 있다.
이런 유형의 공격은 컨테이너가 파일시스템을 쓰지 못하게 함으로써 방지할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: pod-with-readonly-filesystem
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
readOnlyRootFilesystem: true # 이 컨테이너의 파일시스템에 쓰기를 할 수 없다.
volumeMounts:
- name: my-volume
mountPath: /volume
readOnly: false # 하지만 마운트된 볼륨인 /volume에는 쓸 수 있다.
volumes:
- name: my-volume
emptyDir:
k exec -it pod-with-readonly-filesystem -- touch /new-file
를 실행하면 read-only file system 이라는 문구가 출력된다.k exec -it pod-with-readonly-filesystem -- touch /volume/newfile
,k exec -it pod-with-readonly-filesystem -- ls -la /volume/newfile
는 정상적으로 touch 되는 것을 확인할 수가 있다.
파드 수준의 보안 컨텍스트 옵션 설정
위의 예제들에서 개별 컨테이너의 보안 컨텍스트를 설정했다. 이런 옵션 중 몇몇은 pod.spec.securityContext
속성으로 파드 수준에서 설정할 수도 있다. 모든 파드의 컨테이너에 대한 기본값으로 사용되지만 컨테이너 수준에서 재정의할 수 있다. 파드 수준 보안 컨텍스트를 사용하면 다음에 설명할 추가 속성을 설정할 수 있다.
컨테이너가 다른 사용자로 실행될 때 볼륨 공유
6장에서 파드의 컨테이너 간 데이터 공유 시 한 컨테이너에서 파일을 쓰고 다른 컨테이너에서 읽는 데 어려움이 없었지만 이것은 둘 다 루트로 실행돼 볼륨의 모든 파일에 대한 전체 액세스 권한이 부여되었기 때문이다. 위의 예제중 runAsUser 옵션을 사용한다고 했을 때 두 개의 컨테이너를 두 명의 다른 사용자로 실행해야 할 수도 있다. 쿠버네티스를 사용하면 컨테이너에서 실행 중인 모든 파드에 supplementalGroups
속성을 지정해 실행 중인 사용자 ID에 상관없이 파일을 공유할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: pod-with-shared-volume-fsgroup
spec:
securityContext:
fsGroup: 555 # fsGroup과 supplementalGroups는 파드 레벨의 보안 컨텍스트에서 정의된다.
supplementalGroups: [666, 777]
containers:
- name: first
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 1111 # 첫 번째 컨테이너는 사용자 ID 1111로 실행된다.
volumeMounts:
- name: shared-volume
mountPath: /volume
readOnly: false
- name: second
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 2222 # 두 번째 컨테이너는 사용자 ID 2222
volumeMounts:
- name: shared-volume # 두 컨테이너 모두 같은 볼륨을 사용한다.
mountPath: /volume
readOnly: false
volumes:
- name: shared-volume
emptyDir:
k exec -it pod-with-shared-volume-fsgroup -c first -- id
을 입력하면 컨테이너가 1111로 돌아가는 것을 확인할 수가 있다.
gid는 0이지만, 그룹은 555, 666, 777 사용자오 ㅏ연관이 되어 있다.
파드 정의에서 fsGroup을 555로 설정했기에 마운트된 볼륨은 그룹 id 555가 소유한다.
k exec -it pod-with-shared-volume-fsgroup -c first -- ls -l / | grep volume
을 입력해보면 나온다.
마운트된 볼륨의 디렉터리에 파일을 작성하면 파일은 사용자 ID 1111과 그룹 ID 555가 소유하게된다.
fsGroup 보안 컨텍스트 속성은 프로세스가 볼륨에 파일을 생성할 때 사용되지만 supplementalGroups 속성은 사용자와 관련된 추가 그룹 ID 목록을 정의하는 데 사용된다.
파드의 보안 관련 기능 사용 제한
PodSecurityPolicy 리소스 소개
PodSecurityPolicy는 클러스터 수준 리소스로, 사용자가 파드에서 사용할 수 있거나 사용할 수 없는 보안 관련 기능을 정의한다.
PodSecurityPolicy 리소스에 구성된 정책을 유지하는 작업은 API 서버에서 실행되는 PodSecurityPolicy 어드미션 컨트롤 플러그인으로 수행된다.
누군가 파드 서버 리소스를 API 서버에 게시하면 PodSecurityPolicy 어드미션 컨트롤 플러그인은 구성된 PodSecurityPolicies로 파드 정의의 유효성을 검사한다. 파드가 클러스터의 정책을 준수하면 승인되고, etcd에 저장된다. 그렇지 않으면 즉시 거부된다. 플러그인은 정책에 구성된 기본값에 따라 파드 리소스를 수정할 수도 있다.
PodSecurityPolicy 리소스는 다음을 정의한다.
- 파드가 호스트의 IPC, PID 또는 네트워크 네임스페이스를 사용할 수 있는지 여부
- 파드가 바인딩할 수 있는 호스트 포트
- 컨테이너가 실행할 수 있는 사용자 ID
- 특권을 갖는 컨테이너가 있는 파드를 만들 수 있는지 여부
- 어떤 커널 기능이 허용되는지, 어떤 기능이 기본으로 추가되거나 혹은 항상 삭제되는지 여부
- 컨테이너가 사용할 수 있는 SELinux 레이블
- 컨테이너가 쓰기 가능한 루트 파일시스템을 사용할 수 있는지 여부
- 컨테이너가 실행할 수 있는 파일시스템 그룹
- 파드가 사용할 수 있는 볼륨 유형
PodSecurityPolicy 예제 살펴보기
밑의 yaml 파일은 파드의 호스트 IPC, PID, 네트워크 네임스페이스 사용을 방지하고 권한 있는 컨테이너 실행과 대부분의 호스트 포트를 사용하지 못하게 하는 PodSecurityPolicy 예를 보여준다. 이 정책은 컨테이너가 실행할 수 있는 사용자, 그룹 또는 SELinux 그룹에 대한 제약은 설정하지 않는다.
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
hostIPC: false # 컨테이너는 호스트의 IPC, PID 또는 네트워크 네임스페이스를 사용할 수 없다.
hostPID: false
hostNetwork: false
hostPorts:
- min: 10000 # 컨테이너는 호스트 포트 10k~11k, 13k~14k에만 바인딩할 수 있다.
max: 11000
- min: 13000
max: 14000
privileged: false # 컨테이너는 특구너 모드에서 실행할 수 없다.
readOnlyRootFilesystem: true # 컨테이너는 읽기 전용 루트 파일시스템으로 강제 실행된다.
runAsUser:
rule: RunAsAny # 컨테이너는 모든 사용자와 그룹으로 실행할 수 있다.
fsGroup:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
seLinux: # 원하는 SELinux 그룹을 사용할 수 있다.
rule: RunAsAny
volumes: # 모든 볼륨 유형을 파드에 사용할 수 있다.
- '*'
이 PodSecurityPolicy 리소스가 클러스터에 게시되면 API 서버에서 더 이상 이전에 사용된 권한 있는 파드를 배포할 수 없다.
마찬가지로 호스트의 PID, IPC 또는 네트워크 네임스페이스를 사용하려는 파드를 더 이상 배포할 수 없다. 또한 정책에서 readOnlyRootFilesystem을 true로 설정했기 때문에 모든 파드의 컨테이너 파일시스템은 읽기 전용이다.
runAsUser, fsGroup, supplementalGroups 정책
RunAsAny 규칙으로 인해 컨테이너가 실행할 수 있는 사용자와 그룹의 제한이 없다. 허용된 사용자 또는 그룹 ID 목록을 제한하려면 MustRunAs 규칙을 변경해 허용할 그룹 ID를 지정할 수 있다.
MustRunAs
runAsUser:
rule: MustRunAs
ranges:
- min: 2 # 하나의 특정 ID를 설정하려면 min과 max를 동일한 값으로 추가한다.
max: 2
fsGroup:
rule: MustRunAs
ranges: # 여러 개 범위를 지원한다.
- min: 2
max: 10
- min: 20
max: 30
supplementalGroups:
rule: MustRunAs
ranges:
- min: 2
max: 10
- min: 20
max: 30
seLinux:
rule: RunAsAny
volumes:
- '*'
파드 스펙에 해당 필드를 이 범위를 벗어난 값으로 설정하려고 하면 API 서버에서 파드를 허용하지 않는다. 이를 확인해보려면 이전 PodSecurityPolicy를 삭제하고 위의 내용으로 수정된 yaml 파일로 새 PodSecurityPolicy를 만들면 된다.
RunAsUser 필드의 MustRunAsNonRoot 규칙 사용
runAsUser 필드의 경우 MustRunAsNonRoot라는 추가 규칙을 사용할 수 있다. 사용자는 루트로 실행되는 컨테이너를 배포할 수 없다. 컨테이너 스펙은 runAsUser 필드를 지정해야 하며, 0이 아니거나 컨테이너 이미지 자체는 0이 아닌 사용자 ID로 실행해야 한다.
allowed, default, disallowed 기능 구성
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
allowedCapabilities:
- SYS_TIME # 컨테이너가 SYS_TIME 기능을 사용하도록 허용한다.
defaultAddCapabilities:
- CHOWN # 컨테이너에 CHOWN 기능을 자동으로 추가한다.
requiredDropCapabilities:
- SYS_ADMIN # 컨테이너에 SYS_ADMIN과 SYS_MODULE 기능을 삭제하도록 요구한다.
- SYS_MODULE
- allowedCapabilities 필드는 파드 작성자가 컨테이너 스펙의 securityContext.capabilities 필드에 추가할 수 있는 기능을 지정하는 데 사용된다.
- defaultAddCapabilities 필드 아래에 나열된 모든 기능은 배포되는 모든 파드 컨테이너에 추가된다. 특ㅈ겅 컨테이너의 사용자가 이런 기능을 갖기를 원하지 않는 경우 해당 컨테이너의 사양에서 명시적으로 삭제해애ㅑ 한다.
- requiredDropCapabilities 필드에 나열된 기능은 모든 컨테이너에서 자동으로 삭제된다.
파드가 사용할 수 있는 볼륨 유형 제한
사용자가 파드에 추가할 수 있는 볼륨 유형을 정의하는 것이 가능하다. 최소한 PodSecurityPolicy는 적어도 emptyDir, 컨피그맵, 시크릿, 다운워드 API, 퍼시스턴트볼륨클레임 볼륨 사용을 허용해야 한다.
kind: PodSecurityPolicy
spec:
volumes:
- emptyDir
- configMap
- secret
- downwardAPI
- persistentVolumeClaim
PodSecurityPolicy 리소스가 여러 개 있는 경우 파드는 모든 정책에 정의된 모든 볼륨 유형을 사용할 수 있다.
각각의 사용자와 그룹에 다른 PodSecuruityPolicies 할당
PodSecurityPolicy는 클러스터 수준의 리소스이므로 모든 네임스페이스에 적용되는 것을 의미하지는 않다. 상황에 따라 사용할 수 없기 때문이다. 결국 시스템 파드는 종종 일반 파드가 해서는 안 되는 일을 하도록 허용해야 한다.
특권을 가진 컨테이너를 배포할 수 있는 PodSecurityPolicy 만들기
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: privileged
spec:
privileged: true # 특권을 갖는 컨테이너를 실행할 수 있다.
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
seLinux:
rule: RunAsAny
volumes:
- '*'
위의 정책을 API 서버에 게시하면 클러스터에는 2가지 정책이 있게 된다. default와 privileged.
default 정책은 권한 있는 컨테이너를 실행할 수 없지만 privileged 정책은 실행할 수 있다. 파드를 생성할 때 특정 기능을 사용해 파드를 배포할 수 있는 정책이 있으면 API 서버가 해당 파드를 수락한다.
이제 사용자 A가 제한된 파드만 배포하려고 하고 사용자 B는 권한 있는 파드를 배포하려고 할 때에 A는 PodSecurityPolicy만 사용할 수 있고 B는 둘 다 사용할 수 있도록 해야 한다. 이하 생략...
파드 네트워크 격리
네임스페이스에서 네트워크 격리 사용
기본적으로 지정된 네임스페이스의 파드는 누구나 액세스할 수 있다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: # 빈 파드 셀렉터는 동일한 네임스페이스의 모든 파드와 매치된다.
위의 NetworkPolicy는 모든 클라이언트가 네임스페이스의 모든 파드에 연결할 수 없다.
특정 네임스페이스에서 위의 NetworkPolicy를 만들면 아무도 해당 네임스페이스의 파드에 연결할 수 없다.
네임스페이스의 일부 클라이언트 파드만 서버 파드에 연결하도록 허용
클라이언트가 네임스페이스의 파드에 연결할 수 있게 하려면 파드에 연결할 수 있는 대상을 명시적으로 지정해야 한다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-netpolicy
spec:
podSelector:
matchLabels: # 이 정책은 app=database 레이블을 사용해 파드에 대한 액세스를 보호한다.
app: database
ingress: # app=webserver 레이블이 있는 파드에서 들어오는 연결만 허용한다.
- from:
- podSelector:
matchLabels:
app: webserver
ports:
- port: 5432 # 이 포트에 연결할 수 있다.
위의 정책을 사용하면 app=webserver 레이블이 있는 파드가 app=database 레이블이 있는 파드의 포트 5432에만 연결될 수 있다.
다른 파드는 데이터베이스 파드에 연결할 수 없으며 웹 서버 파드조차도 연결할 수 없으며 데이터베이스 파드의 포트 5432 이외의 어떤 것도 연결할 수 없다.
쿠버네티스 네임스페이스 간 네트워크 격리
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: shoppingcart-netpolicy
spec:
podSelector: # 이 정책은 app=shopping-cart로 표시된 파드에 적용된다.
matchLabels:
app: shopping-cart
ingress:
- from:
- namespaceSelector: # tenant=manning의 레이블이 지정된 네임스페이스에서 실행 중인 파드만 마이크로서비스에 액세스할 수 있다.
matchLabels:
tenant: manning
ports:
- port: 80
위 정책은 tenant:manning 레이블이 지정된 네임스페이스에서 실행 중인 파드만 shopping-cart 마이크로서비스에 액세스하도록 보장한다.
CIDR 표기법으로 격리
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ipblock-netpolicy
spec:
podSelector:
matchLabels:
app: shopping-cart
ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24 # 이 인그레스 규칙은 192.168.1.0/24 IP 대역의 클라이언트 트래픽만 허용한다.
파드의 아웃바운드 트래픽 제한
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-net-policy
spec:
podSelector:
matchLabels:
app: webserver
egress: # 파드의 아웃바운드 트래픽을 제한한다.
- to:
- podSelector: # 웹 서버 파드는 app=database 레이블이 있는 파드만 연결할 수 있다.
matchLabels:
app: database
ports:
- port: 5432
파드의 컴퓨팅 리소스 관리
파드 컨테이너의 리소스 요청
파드를 생성할 때 컨테이너가 필요로 하는 CPU와 메모리 양과 사용할 수 있는 엄격한 제한을 지정할 수 있다.
리소스 요청을 갖는 파드 생성하기
apiVersion: v1
kind: Pod
metadata:
name: requests-pod
spec:
containers:
- image: busybox
command: ["dd", "if=/dev/zero", "of=/dev/null"]
name: main # 주 컨테이너에 리소스 요청을 지정한다.
resources:
requests:
cpu: 200m # 컨테이너는 200밀리코어를 요청한다. (하나의 CPU 코어 시간의 1/5)
memory: 10Mi # 컨테이너는 10Mi(Mebibyte)의 메모리를 요청한다.
1/5 CPU 코어를 필요로 한다고 적어놨는데 이런 파드/컨테이너 5개를 CPU 코어 하나에서 충분히 빠르게 실행할 수 있다.
CPU 요청을 지정하지 않으면 컨테이너에서 실행 중인 프로세스에 할당되는 CPU 시간에 신경 쓰지 않는다는 것과 같다. 최악의 경우 CPU 시간을 전혀 할당받지 못할 수 있다. 시간이 중요하지 않은 우선순위가 낮은 배치 작업은 괜찮지만 사용자 요청을 처리하는 컨테이너에는 분명 적합하지 않다.
10Mi의 메모리를 요청함으로써 컨테이너 내부에 실행 중인 프로세스가 최대 10Mi의 메모리를 사용할 것을 예상할 수 있다.
파드 생성 후 k exec -it requests-pod top
을 실행하여 프로세스의 CPU 소비량을 확인할 수 있다.
리소스 요청이 스케줄링에 미치는 영향
리소스 요청을 하면 스케줄러는 충분한 리소스를 가진 노드만을 고려한다. 부합하지 않은 노드에는 스케줄링 하지 않는다.
파드가 특정 노드에 실행할 수 있는지 스케줄러가 결정하는 방법
스케줄러는 스케줄링하는 시점에 각 개별 리소스가 얼마나 사용되는지 보지 않고, 노드에 배포된 파드들의 리소스 요청량의 전체 합만을 본다는 것이다.
파드가 요청한 것보다 적게 사용할지라도 실제 리소스 사용량에 기반해 다른 파드를 스케줄링한다는 것은 이미 배포된 파드에 대한 보장을 깨뜨릴 수 있다.
스케줄러가 파드를 위해 최적의 노드를 선택할 때 파드의 요청을 사용하는 방법
11장에서 스케줄러는 파드에 맞지 않은 노드를 제거하기 위해 노드의 목록을 필터링한 다음 설정된 우선순위 함수에 따라 남은 노드의 우선순위를 지정한다고 했다. 다른 여러 우선순위 함수 중에서, 두 개의 우선순위 함수가 요청된 리소스 양에 기반해 노드의 순위를 정한다.
LeastRequestedPriority와 MostRequestPriority이다. 첫 번째 함수는 요청된 리소스가 낮은 노드를 선호하는 반면, 두 번째 함수는 그와 정반대로 요청된 리소스가 가장 많은 노드를 선호한다.
스케줄러는 이들 함수 중 하나만을 이용하도록 설정된다. 일반적으로 노드들의 세트가 있는 경우 CPU 부하를 전체 노드에 고르게 분산하기를 원할 것이다.
하지만 필요하면 언제든 노드를 추가하거나 제거할 수 있는 클라우드 인프라에서 실행하는 경우는 다르다. 스케줄러가 MostRequestedPriority 함수를 사용하도록 설정하면 쿠버네티스는 파드가 요청한 CPU와 메모리 양을 제공하면서도 가장 적은 수의 노드를 사용하도록 보장한다. 파드를 일부 노드에 많이 스케줄링해 특정 노드를 비울 수 있고 제거할 수 있다. 각 노드별로 비용을 지불하므로 이렇게 비용을 절감할 수 있다.
노드의 용량 검사
k describe nodes
로 노드 리소스를 확인한다. Capacity.memory
에서 노드의 전체 용량을 확인할 수 있고, Allocatable.memory
를 통해 파드에 할당 가능한 리소스를 확인할 수 있다. 이제 cpu 800m, memory 20Mi의 파드를 하나 더 생성해보자.
어느 노드에도 실행할 수 없는 파드 생성
현재 2개의 파드가 배포됐고 총 1000 밀리코어, 정확히 1코어를 요청했다. 따라서 추가적인 파드에 1,000 밀리코어가 사용 가능해야 한다.
이제 cpu 1, memory 20Mi인 3번째 파드를 배포해보자. k get po를 해보면 Pending 상태인 것을 확인할 수 있을 것이고 describe 해보면 cpu가 부족해 스케줄링이 실패했다는 문구를 확인할 수가 있다.
파드가 스케줄링되지 않은 이유 확인
요청한 CPU는 1000m인데 1275m으로 275밀리코어가 더 많다. 확인해보면 kube-system 네임스페이스의 3개의 파드 때문인 것을 알 수가 있다.
파드가 스케줄링될 수 있도록 리소스 해제
파드는 적절한 양의 CPU가 남아 있는 경우에만 스케줄링이 된다. 2번째 파드를 삭제하면 스케줄러는 삭제를 통지받고 두 번째 파드가 종료되자마자 세 번째 파드를 스케줄링할 것이다.
CPU 요청이 CPU 시간 공유에 미치는 영향
위와 같이 동작한다. 그리고 한 컨테이너가 CPU를 최대로 사용하려는 순간 나머지 파드가 유휴 상태에 있다면 첫 번째 컨테이너가 전체 CPU 시간을 사용할 수 있다. 결국 아무도 사용하지 않는다면 사용 가능한 모든 CPU를 사용하는 것이 상식이다.
사용자 정의 리소스의 정의와 요청
쿠버네티스를 사용하면 사용자 정의 리소스를 노드에 추가하고 파드의 리소스 요청으로 사용자 정의 리소스를 요청할 수 있다.
먼저 노드 오브젝트의 capacitty 필드에 값을 추가해 쿠버네티스가 사용자 정의 리소스를 인식하도록 해야 한다. 이는 PATCH HTTP 요청을 수행해 이뤄진다. 리소스 이름은 kubernetes.io 도메인으로 시작하지 않는 이상 example.org/my-resource와 같이 무엇이든 될 수 있다. 수량은 반드시 정수여야 한다. 이 값은 capacity 필드에서 allocatable 필드로 자동으로 복사된다.
그런 다음 파드를 생성할 때 동일한 리소스 이름과 수량을 컨테이너 스펙의 resources.requests 필드로 지정하거나 이전 예제와 같이 kubectl run에 --requests를 사용해 지정한다.
사용자 정의 리소스의 예로는 노드에 사용 가능한 GPU 단위 수가 있다. GPU 사용을 요구하는 파드는 요청에 이를 지정한다. 스케줄러는 할당되지 않은 GPU가 적어도 하나가 있는 노드에만 파드가 스케줄링되도록 보장한다.
컨테이너에 사용 가능한 리소스 제한
컨테이너가 사용 가능한 리소스 양을 엄격한 제한으로 설정
다른 모든 프로세스가 유휴 상태일 때 컨테이너가 남은 모든 CPU를 사용하는 방법을 살펴봤다. 그러나 특정 컨테이너가 지정한 CPU 양보다 많은 CPU를 사용하는 것을 막고 싶을 수 있다. 그리고 컨테이너가 사용하는 메모리 양을 제한하고 싶을 수도 있다.
CPU는 압축 가능한 리소스다. 즉, 컨테이너에서 실행 중인 프로세스에 부정적인 영향을 주지 않고 컨테이너가 사용하는 CPU 양을 조절할 수 있따. 메모리는 압축이 불가능하다. 프로세스에 메모리가 주어지면 프로세스가 메모리를 해제하지 않는 한 가져갈 수 없다. 그것이 컨테이너에 할당되는 메모리의 최대량을 제한해야 하는 이유다. 메모리를 제한하지 않으면 워커 노드에 실행중인 컨테이너는 사용 가능한 모든 메모리를 사용해서 노드에 있는 다른 모든 파드와 노드에 스케줄링되는 새 파드에 영향을 미칠 수 있다. 오작동하거나 악의적인 파드 하나가 실제 전체 노드를 사용할 수 없게 만들 수 있다.
리소스 제한을 갖는 파드 생성
apiVersion: v1
kind: Pod
metadata:
name: limited-pod
spec:
containers:
- image: busybox
command: ["dd", "if=/dev/zero", "of=/dev/null"]
name: main
resources:
limits: # 컨테이너의 리소스 제한을 지정한다.
cpu: 1 # 이 컨테이너는 최대 CPU 1코어를 사용할 수 있다.
memory: 20Mi # 컨테이너는 최대 메모리 20Mi를 사용할 수 있다.
리소스 제한 오버커밋
리소스 요청과는 달리 리소스 제한은 노드의 할당 가능한 리소스 양으로 제한되지 않는다.
노드에 있는 모든 파드의 리소스 제한 합계는 노드 용량의 100%를 초과할 수 있다. 다시 말하면 리소스 제한은 오버커밋 될 수 있다.
리소스 제한 초과
컨테이너에서 실행 중인 프로세스가 허용된 양보다 많은 리소스를 사용하려고 하면 어떤일이 발생할까?
프로세스의 CPU 사용률은 조절되므로 컨테이너에 CPU 제한이 설정돼 있으면 프로세스는 설정된 제한보다 많은 CPU 시간을 할당받을 수 없다.
메모리는 CPU와는 다르다. 프로세스가 제한보다 많은 메모리를 할당받으려 시도하면 프로세스는 종료된다. 파드의 재시작 정책이 Always 또는 OnFailure로 설정된 경우 프로세스는 즉시 다시 시작하므로 종료됐음을 알아차리지 못할 수 있다, 하지만 메모리 제한 초과와 종료가 지속되면 쿠버네티스는 재시작 사이의 지연 시간을 증가시키면서 재시작시킨다. 이런 경우 CrashLoopBackOff
상태가 표시된다.
이 상태는 kubelet이 포기한 것이 아니라 각 크래시 후 kubelet이 컨테이너를 다시 시작하기 전에 간격을 늘리는 것을 의미한다.
컨테이너의 애플리케이션이 제한을 바라보는 방법
위의 limited-pod를 실행하고 k exec -it limited-pod top
으로 내부 지표를 살펴보면 used 메모리와 free 메모리 양이 20Mi와는 거리가 멀다는 것을 확인할 수가 있다. 비슷하게 CPU 또한 다르다. 왜 이런 것일까?
컨테이너는 항상 컨테이너 메모리가 아닌 노드 메모리를 본다
top 명령은 컨테이너가 실행 중인 전체 노드의 메모리 양을 표시한다. 컨테이너에 사용 가능한 메모리의 제한을 설정하더라도 컨테이너는 이 제한을 인식하지 못한다. 이는 시스템에서 사용 가능한 메모리 양을 조회하고 해당 정보를 사용해 예약하려는 메모리 양을 결정하는 모든 애플리케이션에 좋지 않은 영향을 미친다.
자바 애플리케이션을 실행할 때 특히 -Xms 옵션으로 JVM의 최대 힙 크기를 지정하지 않은 경우 문제가 발생한다. 이런 경우 JVM은 컨테이너에 사용 가능한 메모리 대신 호스트의 총 메모리를 기준으로 최대 힙 크기를 설정할 것이다. 많은 물리 메모리를 갖는 프로덕션 환경에 파드를 배포하면 JVM이 컨테이너에 설정된 메모리 제한을 초과해 OOMKilled가 될 수 있다.
적절한 -Xms 옵션을 설정해도 해결되지 않는다. -Xms 옵션은 힙 크기를 제한하지만 JVM의 오프 힙 메모리에는 영향을 미치지 않는다. 다행히도 새 버전의 자바는 컨테이너 제한을 설정한 것을 고려해 문제를 완화한다.
컨테이너는 또한 노드의 모든 CPU 코어를 본다
메모리와 마찬가지로 컨테이너는 컨테이너에 결정된 CPU 제한과 상관없이 노드의 모든 CPU를 본다. CPU 제한을 1코어로 설정하는 것은 마법과 같이 컨테이너에 CPU 1코어만을 노출하지 않는다. CPU 제한이 하는 일은 컨테이너가 사용할 수 있는 CPU 시간의 양을 제한하는 것이다.
어떤 애플리케이션은 시스템의 CPU 수를 검색해 실행해야 할 작업 스레드 수를 결정한다. 이런 애플리케이션은 개발 노트북에서는 정상적으로 돌아가지만 많은 수의 코어를 갖는 노드에 배포하면 너무 많은 스레드가 기동돼 제한된 CPU 시간을 두고 모두 경합하게 된다. 또한 각 스레드는 추가적인 메모리를 요구하게 돼 애플리케이션의 메모리 사용량이 급증한다.
파드 QoS 클래스 이해
리소스 제한은 오버커밋될 수 있으므로 노드가 모든 파드의 리소스 제한에 지정된 양의 리소스를 반드시 제공할 수는 없다고 언급했다.
2개의 파드가 있다고 가정할 때, 파드 A는 노드 메모리의 90%를 사용하고 있는 상황에서 파드 B가 갑자기 그 시점까지 사용하던 메모리보다 많은 메모리를 요구해 노드가 필요한 양의 메모리를 제공할 수 없다면 어떤 컨테이너를 종료해야 할까? 상황에 따라 다르다. 쿠버네티스는 파드를 세 가지 서비스 품질클래스로 분류한다.
- BestEffort (최하위 우선순위)
- Burstable
- Guaranteed (최상위 우선순위)
파드의 QoS 클래스 정의
BestEffort 클래스에 파드를 할당하기
우선순위가 가장 낮은 클래스다. 아무런 리소스 요청과 제한이 없는 파드에 할당된다. 이런 파드에 실행 중인 컨테이너는 리소스 보장을 받지 못한다.
최악의 경우 CPU 시간을 전혀 받지 못할 수 있고 다른 파드를 위해 메모리가 해제돼야 할 때 가장 먼저 종료된다. 그러나 BestEffort 파드는 설정된 메모리 제한이 없으므로 메모리가 충분한다면 컨테이너는 원하는 만큼 메모리를 사용할 수 있다.
Guaranteed 클래스에 파드를 할당하기
이 클래스는 모든 리소스를 컨테이너의 리소스 요청이 리소스 제한과 동일한 파드에게 주어진다. 해당 클래스이기 위해서는 3가지를 충족해야 한다.
- CPU와 메모리에 리소스 요청과 제한이 모두 설정돼야 한다.
- 각 컨테이너에 설정돼야 한다.
- 리소스 요청과 제한이 동일해야 한다.
컨테이너의 리소스 요청이 명시적으로 설정되지 않은 경우 기본적으로 리소스 제한과 동일하게 설정되므로 모든 리소스에 대한 제한을 지정하는 것으로 파드가 Guaranteed가 되기 충분하다. 이런 파드의 컨테이너는 요청된 리소스의 양을 얻지만 추가 리소스를 사용할 수 없다.
Burstable QoS 클래스에 파드를 할당하기
BestEffort와 Guaranteed 사이가 Burstable QoS 클래스다. 그 밖의 다른 파드는 이 클래스에 해당한다. 컨테이너의 리소스 제한이 리소스 요청과 일치하지 않은 단일 컨테이너 파드와 적어도 한 개의 컨테이너가 리소스 요청을 지정했지만 리소스 제한을 설정하지 않은 모든 파드가 여기에 속한다. 또한 컨테이너 하나의 리소스 요청과 제한은 일치하지만 다른 컨테이너의 리소스 요청과 제한을 지정하지 않는 파드도 포함된다. Burstable 파드는 요청한 양 만큼의 리소스를 얻지만 필요하면 추가 리소스를 사용할 수 있다.
메모리가 부족할 때 어떤 프로세스가 종료되는지 이해
QoS 클래스는 어떤 컨테이너를 먼저 종료할지 결정하고 해제된 리소스를 높은 우선순위의 파드에 줄 수 있다. BestEffort 클래스가 가장 먼저 종료되고 다음은 Burstable 파드가 종료되며, 마지막으로 Guaranteed 파드는 시스템 프로세스가 메모리를 필요로 하는 경우에만 종료된다.
동일한 QoS 클래스인 경우에는 시스템은 요청된 메모리의 비율이 달느 것보다 높은 컨테이너를 종료한다. 위 그림에서 요청된 메모리의 90%를 사용하는 파드 B가 70ㅖ%를 사용하는 파드 C 보다 먼저 종료된 이유다.
네임스페이스별 파드에 대한 기본 요청과 제한 설정
LimitRange 리소스 소개
모든 컨테이너에 리소스 요청과 제한을 설정하는 대신 LimitRange 리소스를 생성해 이를 수행할 수 있다. LimitRange 리소스는 컨테이너의 각 리소스에 최소/최대 제한을 지정할 뿐만 아니라 리소스 요청을 명시적으로 지정하지 않은 컨테이너의 기본 리소스 요청을 지정한다.
LimitRange 리소스는 LimitRanger 어드미션 컨트롤 플러그인에서 사용된다. 파드 매니페스트가 API 서버에 게시되면 LimitRanger 플러그인이 파드 스펙을 검증한다. 검증이 실패하면 매니페스트는 즉시 거부된다. 이 때문에 LimitRanger 오브젝트의 좋은 사용 사례는 클러스터의 어느 노드보다 큰 파드를 생성하려는 사용자를 막는 것이다. 이런 LimitRange가 없으면 API 서버는 절대 스케줄링되지 않는 파드라도 기꺼이 받아들인다.
LimitRange 오브젝트 생성하기
apiVersion: v1
kind: LimitRange
metadata:
name: example
spec:
limits:
- type: Pod # 파드 전체에 리소스 제한을 지정
min:
cpu: 50m # 모든 파드의 컨테이너가 전체적으로 요청할 수 있는 최소 CPU 및 메모리
memory: 5Mi
max:
cpu: 1 # 모든 파드의 컨테이너가 요청하는 최대 CPU 및 메모리
memory: 1Gi
- type: Container # 컨테이너 제한은 이 줄의 아래에 지정된다
defaultRequest: # 명시적으로 요청을 지정하지 않은 컨테이너에 적용되는 CPU 및 메모리의 요청
cpu: 100m
memory: 10Mi
default: # 리소스 제한을 지정하지 않은 컨테이너의 기본 제한
cpu: 200m
memory: 100Mi
min: # 컨테이너가 가질 수 있는 최소 및 최대 요청/제한
cpu: 50m
memory: 5Mi
max:
cpu: 1
memory: 1Gi
maxLimitRequestRatio: # 각 리소스의 제한과 요청 간의 최대 비율
cpu: 4
memory: 10
- type: PersistentVolumeClaim # LimitRange는 PVC가 요청할 수 있는 스토리지의 최소 및 최대량을 설정할 수 있다.
min:
storage: 1Gi
max:
storage: 10Gi
강제 리소스 제한
제한이 설정되면 이제 LimitRange에서 허용하는 것보다 더 많은 CPU를 요청하는 파드를 만들 수 있따.
apiVersion: v1
kind: Pod
metadata:
name: too-big
spec:
containers:
- image: busybox
args: ["sleep", "9999999"]
name: main
resources:
requests:
cpu: 2
LimitRange의 최대보다 큰 2개의 CPU를 요청하는 파드를 생성하려고 하면
The Pod "too-big" is invalid: spec.containers[0].resources.requests: Invalid value: "2": must be less than or equal to cpu limit
이러한 오류가 발생한다. 파드가 거부된 것이다.
기본 리소스 요청과 제한 적용
리소스 요청과 제한이 지정되지 않은 파드를 생성 후 describe 해보면 LimitRange에 설정한 값이 들어간 것을 확인할 수가 있다.
네임스페이스의 사용 가능한 총 리소스 제한하기
LimitRange를 이용해서 개별 파드에 대한 적용을 했지만 네임스페이스에서 사용 가능한 총 리소스 양을 제한할 수 있는 방법이 필요하다.
이것을 ResourceQuota, 리소스쿼터 오브젝트를 생성해 달성할 수 있다.
리소스쿼터 오브젝트 소개
리소스쿼터는 네임스페이스에서 파드가 사용할 수 있는 컴퓨팅 리소스 양과 퍼시스턴트볼륨클레임이 사용할 수 있는 스토리지 양을 제한한다. 또한 네임스페이스 안에서 사용자가 만들 수 있는 파드, 클레임, 기타 API 오브젝트의 수를 제한할 수 있다.
CPU 및 메모리에 관한 리소스쿼터 생성
apiVersion: v1
kind: ResourceQuota
metadata:
name: cpu-and-mem
spec:
hard:
requests.cpu: 400m
requests.memory: 200Mi
limits.cpu: 600m
limits.memory: 500Mi
각 리소스의 합계를 정의하는 대신 CPU 및 메모리에 대한 요청과 제한에 대한 별도의 합계를 정의한다. LimitRange의 구조와 비교해 구조가 약간 다르다. 여기에서 모든 리소스에 관한 요청과 제한을 한 곳에서 정의한다.
쿼터와 쿼터 사용량 검사
k describe quota
로 리소스쿼터를 검사해보면 상태가 나온다. Used는 현재 사용된 리소스 요청과 제한이다.
리소스쿼터와 함께 LimitRange 생성
리소스쿼터를 생성할 때 주의할 점은 LimitRange 오브젝트도 함께 생성해야 한다는 것이다. 특히 리소스에 대한 쿼터가 설정된 경우, 파드에는 동일한 리소스에 대한 요청 또는 제한이 설정돼야 한다. 그렇지 않으면 API 서버가 파드를 허용하지 않는다. 그렇기 때문에 이러한 리소스에 대한 기본값이 있는 LimitRange를 갖는 것이 파드를 만드는 것을 조금 더 쉽게 만든다.
퍼시스턴트 스토리지에 관한 쿼터 지정하기
apiVersion: v1
kind: ResourceQuota
metadata:
name: storage
spec:
hard:
requests.storage: 500Gi
ssd.storageclass.storage.k8s.io/requests.storage: 300Gi
standard.storageclass.storage.k8s.io/requests.storage: 1Ti
생성 가능한 오브젝트 수 제한
apiVersion: v1
kind: ResourceQuota
metadata:
name: objects
spec:
hard:
pods: 10
replicationcontrollers: 5
secrets: 10
configmaps: 10
persistentvolumeclaims: 5
services: 5
services.loadbalancers: 1
services.nodeports: 2
ssd.storageclass.storage.k8s.io/persistentvolumeclaims: 2
리소스쿼터는 네임스페이스 내의 파드, 레플리케이션컨트롤러, 서비스 및 그 외의 오브젝트 수를 제한하도록 구성할 수 있다.
특정 파드 상태나 QoS 클래스에 대한 쿼터 지정
쿼터는 쿼터 범위를 BestEffort, NotBestEffort, Terminating, NotTerminating의 네 가지 범위를 사용할 수 있다.
Terminating, NotTerminating은 종료 과정의(또는 종료되지 않은)파드에는 적용되지 않는다. 아직 다루지는 않았지만, 각 파드가 종료되고 실패로 표시되기 전에 얼마나 오래 실행할 수 있는지 지정할 수 있다. 파드 스펙의 activeDeadlineSeconds 필드는 파드가 실패로 표시된 후 종료되기 전 시작 시간을 기준으로 노드에서 활성화되도록 허용하는 시간을 정의한다. Terminating 쿼터 범위는 activeDeadlineSeconds가 설정된 파드에 적용되는 반면 NotTerminating은 그렇지 않은 파드에 적용된다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: besteffort-notterminating-pods
spec:
scopes: # 이 쿼터는 BestEffort QoS를 갖고 유효 데드라인이 없는 파드에만 적용된다.
- BestEffort
- NotTerminating
hard:
pods: 4 # 그러한 파드는 네 개만 존재할 수 있다.
이 쿼터는 유효 데드라인(activeDeadline)이 없는 BestEffort QoS 클래스를 갖는 파드가 최대 4개 존재하도록 보장한다. 그 대신 쿼터가 NotBestEffort 파드를 대상으로 하는 경우 request.cpu, request.memory, limit.cpu, limit.memory도 지정할 수 있다.
파드 리소스 사용량 모니터링
실제 리소스 사용량 수집과 검색
쿠버네티스에서 실행 중인 애플리케이션 모니터링은 Kubelet 자체에 cAdvisor 라는 에이전트가 포함돼 있고 이 에이전트가 노드에서 실행되는 개별 컨테이너와 노드 전체의 리소스 사용 데이터를 수집한다. 전체 클러스터를 이러한 통계를 중앙에서 수집하려면 힙스터라는 추가 구성 요소를 실행해야 한다(최근 쿠버네티스에서는 힙스터 대신 메트릭 서버를 사용한다).
노드 중 하나에서 파드로 실행되는 힙스터는 일반적인 쿠버네티스 서비스를 통해 노출돼 안정된 IP 주소로 접속할 수 있다.
파드는 cAdvisor를 전혀 모르고 cAdvisor는 힙스터를 전혀 모른다. 힙스터가 모든 cAdvisor에 연결하며, cAdvisor는 파드의 컨테이너 내부에서 실행 중인 프로세스와 통신하지 않고 컨테이너와 노드의 사용량 데이터를 수집한다.
힙스터가 실행되면 k top node
와 같은 명령어로 노드에서 사용 중인 CPU 및 메모리 양을 확인 가능하다.
파드와 클러스터 노드의 오토스케일링
수평적 파드 오토스케일링
수평적 파드 오토스케일링은 컨트롤러가 관리 파드의 레플리카 수를 자동으로 조정하는 것을 말한다. 이것은 Horizontal 컨트롤러에 의해 수행되며, HorizontalPodAutoScaler(HPA) 리소스를 작성해 활성화시키고 원하는 대로 설정한다. 컨트롤러는 주기적으로 파드 메트릭을 확인해, HorizontalPodAutoScaler 리소스에 설정돼 있는 대상 메트릭 값을 만족하는 레플리카 수를 계산한다. 그리고 대상 리소스 안에 있는 replicas 필드 값을 조절한다.
오토스케일링 프로세스 이해
- 확장 가능한 리소스 오브젝트에서 관리하는 모든 파드의 메트릭을 가져온다.
- 메트릭을 지정한 목표 값과 같거나 가깝도록 하기 위해 필요한 파드 수를 계산한다.
- 확장 가능한 리소스의 replicas 필드를 갱신한다.
파드 메트릭 얻기
오토스케일러는 파드 메트릭을 수집하지 않고, 다른 소스에서 메트릭을 가져온다. 파드와 노드 메트릭은 모든 노드에서 실행되는 kubelet에서 실행되는 cAdvisor 에이전트에 의해 수집된다. 수집한 메트릭은 클러스터 전역 구성 요소인 힙스터에 의해 집계된다. 수평적 파드 오토스케일러 컨트롤러는 힙스터에 REST를 통해 질의해 모든 파드의 메트릭을 가져온다.
이 흐름은 오토스케일링이 동작하기 위해서는 힙스터가 동작해야 한다는 것을 의미한다.
이것은 쿠버네티스 버전 1.6 이전인 상황이고 1.8에서는 오토스케일러가 리소스 메트릭의 집계된 버전 API를 통해 얻을 수 있다.
필요한 파드 수 계산
오토스케일러의 스케일링 대상이 되는 리소스에 속해 있는 파드의 모든 메트릭을 가지고 있으면, 이 메트릭을 사용해 필요한 레플리카 수를 파악할 수 있다.
모든 레플리카에서 메트릭의 평균 값을 이용해 지정한 목표 값과 가능한 가깝게 하는 숫자를 찾아야 한다. 이 계산의 입력은 파드 메트릭 세트이고, 출력은 하나의 정수이다.
오토스케일러가 단일 메트릭만을 고려하도록 설정돼 있다면 필요한 레플리카 수를 계산하는 것은 간단한다. 모든 파드의 메트릭 값을 더한 뒤 HPA 리소스에 정의된 목표 값으로 나눈 값을 그 다음으로 큰 정수로 반올림해서 구한다.
오토스케일링이 여러 파드 메트릭을 기반으로 하는 경우 계싼이 그렇게 복잡하지는 않다. 오토스케일러는 각 메트릭의 레플리카 수를 개별적으로 계산한 뒤 가장 높은 값을 취한다.
스케일링된 리소스의 레플리카 수 갱신
오토스케일링 작업의 마지막 단계는 스케일링된 리소스 오브젝트의 레플리카 개수 필드를 원하는 값으로 갱신해, 레플리카셋 컨트롤러가 추가 파드를 시작하거나 초과한 파드를 삭제하도록 하는 것이다.
오토스케일러 컨트롤러는 스케일 대상 리소스의 replicas 필드를 스케일 서브 리소스를 통해 변경한다. 이는 스케일 서브 리소스를 통해 노출되는 것을 제외하고, 오토스케일러가 리소스의 세부 사항을 알 필요 없이 수행할 수 있게 해준다.
API 서버가 스케일 서브 리소스를 노출하는 한, 오토스케일러는 모든 확장 가능한 리소스를 대상으로 동작할 수 있따. 현재 노출되는 리소스는 다음과 같다.
- 디플로이먼트
- 레플리카셋
- 레플리케이션 컨트롤러
- 스테이트풀셋
전체 오토스케일링 과정 이해
메트릭 데이터가 전파돼 재조정 작업이 수해오디기까지는 시간이 걸린다. 즉각적으로 이루어지지 않는다.
CPU 사용률 기반 스케일링
오토스케일러에 한해서는 파드의 CPU 사용률을 결정할 때 파드가 보장받은 CPU 사용량만이 중요하다. 오토스케일러는 파드의 실제 CPU 사용량과 CPU 요청을 비교하는데, 이는 오토스케일링이 필요한 파드는 오토스케일러가 CPU 사용률을 결정하기 위해서 오토스케일링이 필요한 파드는 직접 또는 간접적으로 LimitRange 오브젝트를 통해 CPU 요청을 설정해야 한다는 것을 의미한다.
CPU 사용량을 기반으로 HorizontalPodAutoscaler 생성
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v1
name: nodejs
resources: # 파드당 100밀리코어의 CPU 요청
requests:
cpu: 100m
HPA 리소스를 매니페스트를 준비하여 생성하는 방법도 있지만, k autoscale deployment kubia --cpu-percent=30 --min=1 --max=5
와 같이 명령어로도 수행 가능하다.
위 명령은 파드의 목표 CPU 사용률으 30%로 지정하고 최소 및 최대 레플리카 수를 지정한다. 오토스케일러는 CPU 사용률을 30%대로 유지하기 위해 레플리카 수를 조정하지만 1개 미만으로 줄이거나 5개를 초과하는 레플리카를 만들지는 않는다.
apiVersion: autoscaling/v2 # HPA 리소스는 오토스케일링 API 그룹에 속해있다
kind: HorizontalPodAutoscaler
metadata:
creationTimestamp: "2023-07-02T05:41:42Z"
name: kubia # 각 HPA는 이름을 갖고 deployment와 일치하지 않아도 된다.
namespace: bepoz
resourceVersion: ...
uid: ...
spec:
maxReplicas: 5
metrics: # 오토스케일러가 파드 수를 조정해 각 파드가 요청 CPU의 30%를 사용하도록 한다.
- resource:
name: cpu
target:
averageUtilization: 30
type: Utilization
type: Resource
minReplicas: 1
scaleTargetRef: # 오토스케일러가 제어할 목표 리소스
apiVersion: apps/v1
kind: Deployment
name: kubia
status: # 오토스케일러의 현재 상태
conditions:
...
currentMetrics:
- resource:
current:
averageUtilization: 0
averageValue: "0"
name: cpu
type: Resource
currentReplicas: 3
desiredReplicas: 3
k get deploy
를 해보면 replicas가 1개로 준 것을 확인할 수가 있을 것이다.
k describe hpa
를 해보면 아래와 같이 나온다.
Name: kubia
Namespace: bepoz
Labels: <none>
Annotations: <none>
CreationTimestamp: Sun, 02 Jul 2023 14:41:42 +0900
Reference: Deployment/kubia
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 0% (0) / 30%
Min replicas: 1
Max replicas: 5
Deployment pods: 1 current / 1 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
ScalingLimited True TooFewReplicas the desired replica count is less than the minimum replica count
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 36s horizontal-pod-autoscaler New size: 1; reason: All metrics below target
스케일 업 일으키기
이제 CPU 사용량을 증가시켜 오토스케일러가 이를 감지해 추가 파드를 시작하게끔 해보자.
k expose deploy kubia --port=80 --target-port=80
로 간단하게 서비스를 생성해두고,
k run -it --rm --restart=Never loadgenerator --image=busybox -- sh -c "while true; do wget -O - -q http://kubia.<namespace>; done"
를 이용하여 계속 호출하게하고 describe 해보면 replicas가 늘어난 것을 확인할 수가 있다.
기존 HPA 오브젝트에서 목표 메트릭 값 변경
k edit
을 이용해 메트릭 값 변경을 할 수 있다.
spec:
maxReplicas: 5
metrics:
- resource:
name: cpu
target:
averageUtilization: 30
type: Utilization
type: Resource
위의 averageUtilization
항목을 변경하여 CPU 사용률 목표를 변경할 수 있다. 대부분의 다른 리소스와 마찬가지로 리소스를 수정한 후에 변경 사항은 오토스케일러 컨트롤러에 의해 감지돼 동작한다.
기타 그리고 사용자 정의 메트릭 기반 스케일링
spec:
maxReplicas: 5
metrics:
- type: Resource # 메트릭 유형을 지정
resource:
name: cpu # 사용률을 모니터링할 리소스
target:
averageUtilization: 30 # 목표 사용률
metric 필드에는 사용할 하나 이상의 메트릭을 정의할 수 있다.
- 리소스
- 파드
- 오브젝트
이렇게 3 가지 유형의 메트릭이 있다. 위의 예시에서는 타입을 지정해줬는데 1.6 버전 이후로는 생략할 수 있다고 한다. 그런데 이게 파드와 오브젝트의 경우에도 생략가능한지는 자세히 모르겠다.
파드 메트릭의 이해
POds 유형은 파드와 관련된 다른 메트릭을 직접 참조하는 데 사용된다. 이런 메트릭의 예로는 이미 언급했던 초당 질의 수(QPS) 또는 메세지 브로커의 큐 메세지 수 등이 있다.
spec:
maxReplicas: 5
metrics:
- type: Pods # 메트릭 유형을 지정
resource:
metricName: qps # 사용률을 모니터링할 리소스
targetAverageValue: 100 # 모든 대상 파드의 목표 평균 값
모든 파드에서 평균 QPS가 HPA 리소스에 정의한 목표 값이 100을 유지하도록 설정한다.
오브젝트 메트릭 유형 이해
spec:
metrics:
- type: Object
resource:
metricName: latencyMillis
target:
apiVersion: extensions/v1beta1
kind: Ingress
name: frontend
targetValue: 20
위 예제에서 HPA는 frontend 인그레스 오브젝트의 latencyMillis 메트릭을 사용하도록 설정돼 있다. 수평적 파드 오토스케일러는 인그레스의 메트릭을 모니터링하고 목표 값보다 높이 올라갈 경우 오토스케일러가 kubia 디플로이먼트 리소스를 확장한다.
오토스케일링에 적합한 메트릭 결정
오토스케일러 애플리케이션 정의 메트릭을 오토스케일러의 기반 항목으로 하기 전에, 파드 수가 증가하고 감소할 때 메트릭 값이 어떻게 변화하는지 고려해야 한다.
레플리카를 0으로 감소
수평적 파드 오토스케일러는 minReplicas 필드를 0으로 설정할 수 없기 때문에, 파드가 아무것도 하지 않더라도 오토스케일러는 파드 수를 0으로 감소시키지 않는다. 파드 수를 0으로 축소할 수 있게 만들면 하드웨어 사용률이 크게 높아질 수 있다. 이를 유휴(idling), 유휴 해제(un-idling)라고 한다. 아직 쿠버네티스에서 기능 제공을 하고 있지는 않다.
수평적 클러스터 노드 확장
수평적 파드 오토스케일러는 필요할 때 추가 파드 인스턴스를 생성한다. 그러나 모든 노드가 한계에 도달해 더 이상 파드를 추가할 수 없을 때는 어떻게 될까?
이 경우에는 기존 파드 중 몇 개를 삭제하거나 파드가 사용하는 자원을 줄이거나 새로운 노드를 추가해야 한다.
클러스터 오토스케일러 소개
클러스터 오토스케일러는 노드에 리소스가 부족해서 스케줄링할 수 없는 파드를 발견하면 추가 노드를 자동으로 공급한다. 또한 오랜 시간 동안 사용률이 낮으면 노드를 줄인다.
클라우드 인프라스트럭처에 추가 노드 요청
새 노드가 시작되면, 해당 노드의 kubelet이 API 서버에 접속해 노드 리소스를 만들어 노드를 등록한다.
노드 종료
클러스터 오토스케일러는 모든 노드에 요청된 CPU와 메모리를 모니터링해 이를 수행한다. 특정 노드에서 실행 중인 모든 파드의 CPU와 메모리 요청이 50% 미만이면 해당 노드는 필요하지 않은 것으로 간주한다. 오토스케일러는 해당 노드에서만 실행 중인 시스템 파드가 있는지 검사한다(데몬셋 등으로 모든 노드에 배포되는 시스템 파드는 제외한다). 만약 시스템 파드가 노드에서 실행 중이라면 해당 노드는 종료될 수 없다. 관리되지 않는 파드나 로컬 저장소를 가진 파드가 실행되는 경우에도 마찬가지다. 파드가 제공하는 서비스가 중단될 수 있기 때문이다. 다르게 말하면 클러스터 오토스케일러가 노드에서 실행 중인 파드가 다른 노드로 다시 스케줄링될 수 있다는 것을 알고 있는 경우에만 해당 노드가 클라우드 제공자에게 반환될 수 있다.
종료할 노드로 선택되면, 해당 노드는 먼저 스케줄링할 수 없다는 표시를 하고 노드에서 실행 중인 모든 파드를 제거한다. 제거하는 모든 파드는 레플리카셋이나 다른 컨트롤러에 속해 있기 때문에, 교체할 파드가 생성되고 남아 있는 나머지 노드에 스케줄링된다.
### 클러스터 스케일 다운 동안에 서비스 중단 제한
노드가 예기치 않게 실패할 경우 파드가 사용 불가 상태가 되는 것을 막을 수 있는 방법이 없다. 하지만 노드 종료가 클러스터 오토스케일러나 시스템 관리자로 인해 이뤄지는 상황이라면, 추가 기능을 통해 해당 노드에서 실행되는 파드가 제공하는 서비스를 중단되지 않도록 할 수 있다.
Pod DisruptionBudget 리소스를 만들어 이를 수행할 수 있다. 파드 레이블 셀렉터와 항상 사용 가능해야 하는 파드의 최소 개수 혹은 파드의 최대 개수를 정의할 수 있다.
k create pdb kubia-pdb --selector=app=kubia --min-available=3
명령어로 생성하고 yaml을 확인해보겠다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
creationTimestamp: "2023-07-02T06:56:10Z"
generation: 1
name: kubia-pdb
namespace: bepoz
resourceVersion: ...
uid: ...
spec:
minAvailable: 3 # 얼마나 많은 파드를 항상 사용 가능하게 할 것인가
selector: # 이 budget을 적용할 파드를 결정하는 레이블 셀렉터
matchLabels:
app: kubia
status:
conditions:
- lastTransitionTime: "2023-07-02T06:56:10Z"
message: ""
observedGeneration: 1
reason: InsufficientPods
status: "False"
type: DisruptionAllowed
currentHealthy: 1
desiredHealthy: 3
disruptionsAllowed: 0
expectedPods: 1
observedGeneration: 1
minAvailable
필드에 절댓값이 아닌 백분율을 사용할 수도 있다. 예를 들어 app=kubia 레이블을 가진 모든 파드 중 60%의 파드가 항상 실행되는 상태를 지정할 수 있다. 리소스가 존재하는 동안 클러스터 오토스케일러와 와 kubectl drain 명령 모두 이를 준수해 app-kubia 레이블을 가진 파드가 정의해둔 수 이하로 줄어들지 안ㅇㅎ는다.
예를 들어 4개의 파드가 있고 minAvailable
을 3개로 설정한 경우 파드 제거 프로세스는 파드를 하나씩 제거하는데, 이때 다른 파드를 제거하기 전에 레플리카셋 컨트롤러가 제거된 파드를 새 파드로 교체하기를 기다린다.
고급 스케줄링
테인트와 톨러레이션을 사용해 특정 노드에서 파드 실행 제한
고급 스케줄링과 관련된 기능 중 가장 먼저 살펴볼 두 가지는 노드 테인트와 이 테인트에 대한 파드 톨러레이션이다.
테인트와 톨러레이션은 어떤 파드가 특정 노드를 사용할 수 있는지를 제한하고자 사용된다.
노드 셀렉터와 노드 어피니티 규칙을 사용하면 특정 정보를 파드에 추가해 파드가 스케줄링되거나 스케줄링될 수 없는 노드를 선택할 수 있다. 반면 테인트는 기존의 파드를 수정하지 않고, 노드에 테인트를 추가하는 것만으로도 파드가 특정 노드에 배포되지 않도록 한다. 테인트된 노드에 배포할 파드는 테인트된 노드를 사용하게 선택할 필요가 있는 반면, 노드 셀렉터를 사용하면 파드를 배포할 노드를 명시적으로 지정한다.
테인트와 톨러레이션 소개
k describe node <node-name>
으로 테인트를 먼저 확인할 수 있다.
Taints: node-role.kubernetes.io/control-plane:NoSchedule
이렇게 나오는데 테인트에는 키, 값, 효과가 있고 <key>=<value>:<effect>
형태로 표시된다.
위의 예시에서 키는 node-role.kubernetes.io/control-plane
, 값은 null(테인트에 표시되지 않음), 효과는 NoSchedule을 갖는다.
이 테인트는 파드가 이 테인트를 허용하지 않는 한 마스터 노드에 스케줄링되지 못하게 막는다. 이 테인트를 허용하는 파드는 주로 시스템 파드다.
파드의 톨러레이션 표시하기
k describe po kube-proxy-cpwzp -n kube-system
프록시 파드를 조회해보면 톨러레이션을 확인할 수가 있다.
Tolerations: op=Exists
node.kubernetes.io/disk-pressure:NoSchedule op=Exists
node.kubernetes.io/memory-pressure:NoSchedule op=Exists
node.kubernetes.io/network-unavailable:NoSchedule op=Exists
node.kubernetes.io/not-ready:NoExecute op=Exists
node.kubernetes.io/pid-pressure:NoSchedule op=Exists
node.kubernetes.io/unreachable:NoExecute op=Exists
node.kubernetes.io/unschedulable:NoSchedule op=Exists
만약 node-role.kubernetes.io/control-plane:NoSchedule
이라면 해당 테인트를 가지고 있는 노드에 접근이 가능해진다.
테인트 효과 이해하기
각 테인트는 그와 관련된 효과를 갖고 있다.
- NoSchedule: 파드가 테인트를 허용하지 않는 경우 파드가 노드에 스케줄링되지 않는다.
- PreferNoSchedule: NoSchedule의 소프트한 버전이다. 즉, 스케줄러가 파드를 노드에 스케줄링하지 않으려 하지만 다른 곳에 스케줄링할 수 없으면 해당 노드에 스케줄링된다.
- NoExecute: 스케줄링에만 영향을 주는 NoSchedule이나 PreferNoSchedule과 달리, NoExecute는 노드에서 이미 실행 중인 파드에도 영향을 준다. NoExecute 테인트를 노드에 추가하면 해당 노드에서 이미 실행 중이지만 NoExecute 테인트를 허용하지 않은 파드는 노드에서 제거된다.
노드에 사용자 정의 테인트 추가하기
k taint node <node-name> node-type=production:NoSchedule
으로 노드에 테인트를 추가했다.
키는 node-type, 값은 production, 효과는 NoSchedule을 갖는 테인트가 추가되었을 것이다.
이제 파드를 생성해보면 해당 테인트가 있는 노드에는 스케줄링되지 않는 것을 확인할 수가 있다.
파드에 톨러레이션 추가
piVersion: apps/v1
kind: Deployment
metadata:
name: prod
spec:
replicas: 5
selector:
matchLabels:
app: prod
template:
metadata:
labels:
app: prod
spec:
containers:
- args:
- sleep
- "99999"
image: busybox
name: main
tolerations:
- key: node-type
operator: Equal
value: production
effect: NoSchedule
위와 같이 톨러레이션을 추가하고 돌려보면 모든 노드에 배포된 것을 확인할 수가 있다. 이 파드가 특정 노드에 배포되길 원치 않는다면 none-type=non-production:NoSchedule
과 같은 테인트를 추가하면 될 것이다.
테이트와 톨러레이션의 활용 방안 이해
테인트는 키와 효과만 갖고 있고, 값을 꼭 필요로 하지 않는다. 톨러레이션은 Equals 연산자를 지정해 특정한 값을 허용하거나, Exists 연산자를 사용해 특정 테인트 키에 여러 값을 허용할 수 있다.
스케줄링 테인트와 톨러레이션 사용하기
테인트는 새 파드의 스케줄링을 방지하고, 선호하지 않는 노드를 정의하고, 노드에서 기존 파드를 제거하는 데에도 사용할 수 있다.
노드 실패 후 파드를 재스케줄링하기까지의 시간 설정
파드를 실행 중인 노드가 준비되지 않거나 도달할 수 없는 경우 톨러레이션을 사용해 쿠버네티스가 다른 노드로 파드를 다시 스케줄링하기 전에 대기해야 하는 시간을 지정할 수 있다. 톨러레이션을 추가한 파드를 describe 해보면 이 정보를 알 수 있다.
tolerations:
- effect: NoSchedule
key: node-type
operator: Equal
value: production
- effect: NoExecute # 노드가 준비되지 않은 상태에서 파드는 재스케줄링되기 전에 300초 동안 기다린다.
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute # 도달할 수 없는 노드에도 동일하게 적용된다.
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
두 톨러레이션은 이 파드가 notReady 또는 unreachable 상태를 300초 동안 허용한다는 것을 의미한다. 쿠버네티스 컨트롤 플레인은 노드가 더 이상 준비되지 않았거나 더 이상 도달할 수 없다는 것을 감지함현, 파드를 삭제하고 다른 노드로 다시 스케줄링하기까지 300초 동안 기다린다.
톨러레이션을 별도로 정의하지 않은 파드는 이 두 톨러레이션이 자동으로 추가된다. 지연 시간 5분이 너무 길다고 생각되면 파드 스펙에 이 두 개의 톨러레이션을 추가한 뒤 지연 시간을 짧게 설정할 수 있다.
노드 어피니티를 사용해 파드를 특정 노드로 유인하기
노드 어피니티는 쿠버네티스가 특정 노드 집합에만 파드를 스케줄링하도록 지시할 수 있다.
노드 어피니티와 노드 셀렉터 비교
노드 셀렉터와 유사하게 각 파드는 고유한 노드 어피니티 규칙을 정의할 수 있다. 이를 통해 꼭 지켜야 하는 필수 요구 사항이나 선호도를 지정할 수 있다. 선호도를 지정하는 방식으로 쿠버네티스에게 어떤 노드가 특정한 파드를 선호한다는 것을 알려주면, 쿠버네티스는 해당 노드 중 하나에 파드를 스케줄링하려고 시도하게 된다. 해당 노드에 스케줄링이 불가능하다면 다른 노드를 선택한다.
하드 노드 어피니티 규칙 지정
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
nodeSelector:
gpu: "true" # 파드는 gpu=true라는 레이블이 있는 노드에만 스케줄링된다.
...
위의 파드를 노드 어피니티 규칙을 사용하는 파드로 변경하면 아래와 같게된다.
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"
...
긴 nodeAffinity 속성 이름 이해
- requiredDuringScheduling... : 이 필드 아래에 정의된 규칙은 파드가 노드로 스케줄링되고자 가져야 하는 레이블을 지정한다
- ...IgnoredDuringExecution: 이 필드 아래에 정의된 규칙은 노드에서 이미 실행 중인 파드에는 영향을 미치지 않는다
현재 시점에는 어피니티가 파드 스케줄링에만 영향을 미치며, 파드가 노드에서 제거되지 않는다.
requiredDuringExecution...는 아직 쿠버네티스에서 지원하지 않는다.
nodeSelectorTerm 이해
파드의 스케줄링 시점에 노드 우선순위 지정
새로 도입된 노드 어피니티 기능의 가장 큰 장점은 특정 파드를 스케줄링할 때 스케줄러가 선호할 노드를 지정할 수 있다는 것이다.
이는 preferredDuringSchedulingIgnoringDuringExecution
필드를 통해 수행된다.
k label node <node-name> zone=zone1
이렇게 노드에 레이블을 각각주고 k get node -L zone -L share-type
으로 아래와 같이 조회했다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pref
spec:
replicas: 5
selector:
matchLabels:
app: pref
template:
metadata:
labels:
app: pref
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # 필수 요구사항이 아닌 선호도를 명시하고 있다
- weight: 80 # 파드가 zone1에 스케줄링되는 것을 더 선호한다.
preference:
matchExpressions:
- key: availability-zone
operator: In
values:
- zone1
- weight: 20 # 파드가 dedicated 노드로 스케줄링하는 것을 선호하지만 zone1보다 4배 덜 중요하다.
preference:
matchExpressions:
- key: share-type
operator: In
values:
- dedicated
containers:
- args:
- sleep
- "99999"
image: busybox
name: main
노드 선호도 작동 방법 이해하기
파드 어피니티와 안티-어피니티를 이용해 파드 함께 배치하기
노드 어피니티 규칙을 사용해 파드가 어느 노드에 스케줄링될지에 영향을 주는 방법을 살펴봤다. 그러나 이 규칙은 파드와 노드 간의 어피니티에만 영향을 미치는 반면, 때때로 파드 간의 어피니티를 지정할 필요가 있는 경우가 있다.
파드 간 어피니티를 사용해 같은 노드에 파드 배포하기
k create deployment backend --image=busybox -- sleep 999999
후, k label deployment <deploy name> app=backend
로 레이블을 달아주었다. 그 후 아래의 deployment를 배포하였다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 5
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAffinity: # podAffinity 규칙을 정의
requiredDuringSchedulingIgnoredDuringExecution: # 선호도가 아닌 필수 요구 사항을 정의한다.
- topologyKey: kubernetes.io/hostname # 이 deployment 파드는 셀렉터와 일치하는 노드에 배포돼야 한다.
labelSelector:
matchLabels:
app: backend
containers:
- name: main
image: busybox
args:
- sleep
- "99999"
파드를 확인해보면 프론트엔드 파드와 백엔드 파드가 동일한 노드에 배포된 것을 확인할 수가 있다.
흥미로운 점은 이제 파드 어피니티 규칙을 정의하지 않은 백엔드 파드를 삭제하더라도 스케줄러가 백엔드 파드를 node2에 스케줄링한다는 것이다.
백엔드 파드가 실수로 삭제돼서 다른 노드로 다시 스케줄링된다면, 프론트엔드 파드의 어피니티 규칙이 깨지기 때문에 같은 노드에 스케줄링되는 것이다.
동일한 랙, 가용 영역 또는 리전에 파드 배포
topologyKey 작동 방법 이해
필수 요구 사항 대신 파드 어피니티 선호도 표현핳기
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 5
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: backend
containers:
- name: main
image: busybox
args:
- sleep
- "99999"
노드 어피니티와 마찬가지로 파드의 경우에도 동일하게 선호하는 파드를 설정할 수 있다.
파드 안티-어피니티를 사용해 파드들이 서로 떨어지게 스케줄링하기
파드끼리 떨어뜨려 놓을 수도 있다. podAffinity 대신 podAntiAffinity 속성을 사용하면 된다.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 5
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 파드의 안티-어피니티를 위한 필수 요구 사항을 정의한다
- topologyKey: kubernetes.io/hostname # 프론트엔드 파드는 app=frontend 레이블이 있는 파드와 동일한 노드에
labelSelector: # 스케줄링돼서는 안된다
matchLabels:
app: frontend
containers:
- name: main
image: busybox
args:
- sleep
- "99999"
애플리케이션 개발을 위한 모범 사례
모든 것을 하나로 모아 보기
일반적인 애플리케이션 매니페스트에는 하나 이상의 디플로이먼트나 스테이트풀셋 오브젝트가 포함된다. 여기에는 하나 이상의 컨테이너가 포함된 파드 템플릿, 각 컨테이너에 대한 라이브니스 프로브와(있는 경우) 컨테이너가 제공하는 서비스에 대한 레디니스 프로브가 포함된다. 다른 사람에게 서비스를 제공하는 파드는 하나 이상의 서비스로 노출된다. 클러스터 외부로 통신할 수 있어야 하는 경우 서비스는 로드밸런서나 노드포트 유형 서비스로 구성되거나 인그레스 리소스로 노출된다.
파드 템플릿은 일반적으로 프라이빗 이미지 레지스트리에서 컨테이너 이미지를 가져오는 데 사용되는 시크릿과 파드 내에 실행되는 프로세스에서 직접 사용되는 시크릿을 참조한다. 시크릿은 일반적으로 애플리케이션 개발자가 아니라 운영팀이 구성하기 때문에 애플리케이션 매니페스트의 일부는 아니다. 시크릿은 일반적으로 개별 파드에 할당된 서비스어카운트에 할당한다.
애플리케이션에는 환경변수를 초기화하거나 파드에 컨피그맵 볼륨으로 마운트되는 하나 이상의 컨피그맵이 포함돼 있다. 특정 파드는 emptyDir 또는 girRepo 볼륨과 같은 추가 볼륨을 사용하는 반면, 퍼시스턴트 스토리지가 있어야 하는 파드는 퍼시스턴트 볼륨 클레임 볼륨을 사용한다. 퍼시스턴트 볼륨 클레임은 애플리케이션 매니페스트에 속하며, 이에 의해 참조된 스토리지클래스는 시스템 관리자가 미리 생성한다.
경우에 따라 애플리케이션에서 잡이나 크론잡을 사용해야 한다. 데몬셋은 일반적으로 애플리케이션 디플로이먼트의 일부는 아니지만 일반적으로 시스템 운영자가 노드 전체 또는 일부에 시스템 서비스를 실행하려고 생성한다. 수평 파드 오토 스케일러는 개발자가 매니페스트에 포함시키거나 나중에 운영팀이 시스템에 추가한다. 또한 크러스터 관리자는 제한 범위와 리소스쿼터 오브젝트를 생성해 개별 파드와 모든 파드의 리소스 사용량을 제어할 수 있다.
애플리케이션이 배포되면 다양한 쿠버네티스 컨트롤러에 의해 오브젝트가 추가적으로 자동 생성된다. 여기에는 엔드포인트 컨트롤러로 생성된 서비스 엔드포인트 오브젝트, 디플로이먼트 컨트롤러로 생성된 레플리카셋과 레플리카셋, 잡, 크론잡, 스테이트풀셋, 데몬셋 컨트롤러로 생성된 실제 파드가 포함된다.
리소스는 종종 체계적으로 유지되기 위해 하나 이상의 레이블을 지정한다. 이는 파드에만 적용되는 것이 아니라 다른 모든 리소스에도 적용된다. 레이블 외에도 대부분의 리소스에는 각 리소스를 설명하거나 해당 담당자 또는 팀의 연락처 정보를 나열하거나 관리와 기타 도구에 관한 추가 메타데이터를 제공하는 어노테이션이 포함돼 있다.
이 중심에는 파드가 있다. 가장 중요한 쿠버네티스 리소스다. 결국 각 애플리케이션은 파드 내부에서 실행된다. 환경을 최대한 활용하는 애플리케이션을 개발하는 방법을 알려면, 애플리케이션 관점에서 파드를 자세히 살펴봐야 한다.
파드 라이프사이클 이해
애플리케이션을 종료하고 파드 재배치 예상하기
쿠버네티스 환경이 아닌 경우 가상머신에서 실행되는 애플리케이션은 한 시스템에서 다른 시스템으로 이동하는 경우가 드물다.
쿠버네티스를 사용하면 애플리케이션이 훨씬 더 자주 자동으로 재배치된다.
로컬 IP와 호스트 이름 변경 예상하기
파드가 종료되고 다른 곳에서 실행되면 새로운 IP 주소뿐만 아니라 새로운 이름과 호스트 이름을 갖는다. 대부분의 스테이트리스 애플리케이션은 일반적으로 문제없이 처리할 수 있지만 스테이트풀 애플리케이션은 그렇지 않다. 스테이트풀 애플리케이션을 스테이트풀셋으로 실행할 수 있다는 사실을 알고 있으므로 스케줄링을 조정한 후 새 노드에서 애플리케이션을 시작할 때도 여전히 이전과 동일한 호스트 이름과 퍼시스턴트 상태를 볼 수 있다. 그럼에도 파드의 IP는 변경될 것이다. 이를 위해서 애플리케이션이 미리 준비돼 있어야 한다. 따라서 애플리케이션 개발자는 클러스터된 애플리케이션의 구성원을 IP 주소 기반으로 하면 안 되며 호스트 이름을 기반으로 할 때에도 항상 스테이트풀셋을 사용해야 한다.
디스크에 기록된 데이터가 사라지는 경우 예상하기
애플리케이션이 디스크에 데이터를 쓰는 경우 애플리케이션이 쓰는 위치에 퍼시스턴트 스토리지를 마운트하지 않으면 애플리케이션이 새 파드로 시작된 후에 해당 데이터를 사용하지 못할 수 있다는 것이다. 파드를 다시 스케줄링하면 이러한 상황이 발생하는 게 명확하지만 스케줄링과 관련되지 않는 경우에도 디스크에 기록된 파일은 사라질 수 있다. 파드의 라이프사이클 동안에도 파드에서 실행되는 애플리케이션이 디스크에 쓴 파일은 사라질 수 있다.
예로들면, 애플리케이션이 재시작할 때 더 빨리 시작되도록 개발자는 디스크에 초기 시작 결과를 애플리케이션 캐시로 만든다. 쿠버네티스의 패를리케이션은 기본적으로 컨테이너에서 실행되므로 이러한 파일은 컨테이너의 파일시스템에 만든다. 컨테이너가 다시 시작되면 새 컨테이너는 완전히 새로운 쓰기 가능한 레이어로 시작하기 때문에 이전 파일은 모두 손실된다.
프로세스 크래시나 라이브니스 프로브가 실패를 반환했거나 노드의 메모리 부족이 시작돼 프로세스가 OOMKiller에 의해 종료된 경우와 같이 여러 가지 이유로 컨테이너가 다시 시작될 수 있다. 이 경우 파드는 여전히 동일하지만 컨테이너 자체는 완전히 새로운 것이다. Kubelet은 동일한 컨테이너를 다시 실행하지 않는다. 항상 새 컨테이너를 만든다.
컨테이너를 다시 사용하더라도 데이터를 보존하기 위해 볼륨 사용하기
컨테이너를 재시작할 때 파일을 보존하려고 볼륨을 사용하는 것은 좋은 생각이지만 항상 그런 것은 아니다. 데이터가 손상돼 새로 생성된 프로세스가 다시 크래시되면 어떻게 할까? 이로 인해 연속 크래시 루프가 발생한다(파드에 CrashLoopBackOff
상태가 표시됨). 볼륨을 사용하지 않은 경우 새 컨테이너가 처음부터 시작돼 크래시하지 않을 가능성이 높다. 컨테이너를 재시작할 때 파일을 보존하려고 볼륨을 사용하는 것은 양날의 검이다.
종료된 파드 또는 부분적으로 종료된 파드를 다시 스케줄링하기
파드의 컨테이너가 계속 크래시되면 Kubelet은 계속 파드를 재시작한다. 재시작 간격은 5분이 될 때까지 간격이 증가한다.
멀티 컨테이너 파드인 경우 엄밀하게 말해 특정 컨테이너가 정상적으로 작동할 수 있으므로 파드는 일부만 종료된 것으로 볼 수 있다. 그러나 파드에 컨테이너가 하나만 포함돼 있으면 더 이상 프로세스가 실행되지 않기 때문에 파드가 실제로 종료된 것이고 완전히 쓸모가 없어진다.
의도하는 레플리카수가 3인 레플리카셋을 만든 다음 해당 파드 중 하나에서 컨테이너가 크래시하기 시작해도 쿠버네티스는 해당 파드를 삭제하고 교체하지 않는다. 결론적으로 의도하는 세 개의 레플리카가 아닌 제대로 실행되는 두 개의 레플리카가 있는 레플리카셋을 가진다.
파드가 삭제되고 다른 노드에서 성공적으로 실행될 수 있는 다른 파드 인스턴스로 교체되기 원할 것이다. 결국 다른 노드에서는 나타나지 않은 노드 관련 문제로 인해 컨테이너가 중단될 수 있다. 슬프게도, 레플리카셋 컨트롤러는 파드가 죽은 상태가 됐는지 상관하지 않는다. 관심 있는 것은 파드 수가 의도하는 레플리카 수와 일치하느냐하는 것이다.
원하는 순서로 파드 시작
파드에서 실행되는 애플리케이션과 수동으로 관리되는 애플리케이션의 또 다른 차이점은 애플리케이션을 배포하는 담당자가 애플리케이션 간의 의존성을 알고 있다는 것이다. 이것은 애플리케이션을 순서대로 시작할 수 있게 해준다.
파드 시작 방법
쿠버네티스로 파드 애플리케이션 여러 개를 실행할 때 쿠버네티스가 특정 파드를 먼저 실행하고 첫 번째 파드가 서비스할 준비가 됐을 때 나머지 파드를 실행하도록 지시할 방법이 기본적으로 없다. 쿠버네티스 API 서버는 YAML/JSON의 오브젝트를 나열된 순서대로 처리하지만 이는 etcd에 순서대로 기록됨을 의미한다. 파드가 그 순서대로 시작된다는 보장은 없다. 그러나 전체 조건이 충족될 때까지 파드의 주 컨테이니ㅓ가 시작되지 않도록 할 수 있다. 이것은 파드에 초기화 컨테이너를 포함시켜 수행된다.
초기화 컨테이너 소개
일반 컨테이너 외에도 파드는 초기화 컨테이너를 포함할 수 있다. 이름에서 알 수 있듯이 파드를 초기화하는 데 사용한다. 이는 주 파드의 볼륨에 데이터를 쓴 다음 주 컨테이너에 마운트하는 것을 의미한다.
파드는 여러 개의 초기화 컨테이너를 가질 수 있다. 순차적으로 실행되며 마지막 컨테이너가 완료된 후에 파드의 주 컨테이너가 시작된다. 즉, 초기화 컨테이너를 사용해 파드의 주 컨테이너 시작을 지연시킬 수 있다. 초기화 컨테이너로 파드의 주 컨테이너에 필요한 서비스가 준비될 때까지 기다릴 수 있다. 주 컨테이너가 시작되면, 초기화 컨테이너는 종료되고 주 컨테이너가 시작될 수 있게 한다. 이렇게 하면 주 컨테이너가 준비되기 전까지 서비스를 사용하지 않게 된다.
주 컨테이너가 시작되기 전에 fortune 서비스를 시작하고나서 실행해야 하는 fortune 클라이언트 파드가 있다고 가정해보자. 서비스가 요청에 응답하는지 여부를 확인하는 초기화 컨테이너를 추가할 수 있다. 이때까지 초기화 컨테이너는 계속 재시도한다. 응답을 받으면 초기화 컨테이너가 종료되고 주 컨테이너가 시작된다.
파드에 초기화 컨테이너 추가
초기화 컨테이너는 주 컨테이너와 같이 파드 스펙에 정의될 수 있지만 spec.initContainers
필드에 정의할 수도 있다.
apiVersion: v1
kind: Pod
metadata:
name: fortune-client
spec:
initContainers:
- name: init
image: busybox
command:
- sh
- -c
- 'while true; do echo "Waiting for fortune service to come up..."; wget http://fortune -q -T 1 -O /dev/null >/dev/null 2>/dev/null && break; sleep 1; done; echo "Service is up! Starting main container."'
# 초기화 컨테이너는 fortune 서비스가 가동될 때까지 루프를 실행한다
containers:
- image: busybox
name: main
command:
- sh
- -c
- 'echo "Main container started. Reading fortune very 10 seconds."; while true; do echo "-------------"; wget -q -O - http://fortune; sleep 10; done'
위 파드를 생성하고 확인해보면 STATUS가 조금 특이한 것을 알 수 있다. 초기화 컨테이너 중 0개가 완료됐음을 나타내고 있다.
파드 간 의존성 처리를 위한 모범 사례
애플리케이션이 시작되기 전 준비 상태가 되기 위해 의존해야 할 서비스를 필요로 하지 않도록 애플리케이션을 만드는 것이 좋은 방법이다. 결국 애플리케이션이 실행 상태가 되더라도 서비스는 나중에 오프라인이 될 수 있다. 애플리케이션은 이러한 의존성이 준비되지 않았을 가능성을 내부적으로 처리해야 한다. 의존성이 누락돼 애플리케이션이 작업을 수행할 수 없는 경우 쿠버네티스가 레디니스 프로브로 이를 인식하고 준비가 되지 않았다는 신호를 보내야 한다. 애플리케이션을 서비스 엔드포인트에 추가되는 것을 방지할 뿐만 아니라, 롤링 업데이트를 수행할 때 디플로이먼트 컨트롤러에서 애플리케이션의 레디니스 상태를 사용해 잘못된 버전의 롤아웃을 방지하기 때문에 이 방법을 사용한다.
라이프사이클 훅 추가
초기화 컨테이너를 사용해 파드 시작 시 사용하는 방법을 설명했지만, 파드는 라이프사이클 훅 두 가지를 정의할 수 있다.
- 시작 후(post-start) 훅
- 종료 전(pre-stop) 훅
이런 라이프사이클 훅은 전체 파드에 적용되는 초기화 컨테이너와 달리 컨테이너 별로 지정된다.
라이프사이클, 라이브니스 프로브, 레디니스 프로브와 유사하게 다음을 수행할 수 있다.
- 컨테이너 내부에서 명령 실행
- URL로 HTTP GET 요청 수행
컨테이너 시작 후 라이프사이클 훅 사용
시작 후 훅은 컨테이너의 주 프로세스가 시작된 직후에 실행된다. 애플리케이션이 시작될 떄 추가 작업을 수행하는 데 사용된다. 물론 컨테이너에서 실행 중인 애플리케이션 개발자라면 항상 애플리케이션 코드 내에서 해당 작업을 수행할 수 있지만 대부분 소스 코드를 수정하고 싶어 하지 않는다. 시작 후 훅을 사용하면 애플리케이션을 건드리지 않고도 추가 명령을 실행할 수 있다. 애플리케이션이 시작되고 있는 외부 리스너에게 시그널을 보내거나 애플리케이션을 초기화하는 작업을 시작할 수 있다.
훅이 완료될 때까지 컨테이너는 ContainerCreating인 채로 Waiting 상태가 유지된다. 이 때문에 파드 상태는 Running 중이 아니라 Pending 상태다. 훅이 실행되지 않거나 0이 아닌 종료 코드를 반환하면 주 컨테이너가 종료된다.
파드 셧다운 이해하기
파드의 종료는 API 서버로 파드 오브젝트를 삭제하면 시작된다. HTTP DELETE 요청을 수신하면 API 서버는 아직 오브젝트를 삭제하지 않고 그 안에 deleteTimeStamp 필드만 설정한다. 그리고 deletedTimeStamp 필드가 설정된 파드가 종료된다.
Kubelet은 파드를 종료해야 함을 확인하면 각 파드의 컨테이너를 종료하기 시작한다. 각 컨테이너에 정상적으로 종료하는 데 시간이 걸리지만 시간은 제한돼 있다. 이 시간을 종료 유예 기간이라고 하며 파드별로 구성할 수 있다. 종료 프로세스가 시작되자마자 타이머가 시작된다.
- 종료 전 훅(구성된 경우)을 실행하고 완료될 때까지 기다린다.
- SIGTERM 신호를 컨테이너의 주 프로세스로 보낸다.
- 컨테이너가 완전히 종료될 때까지 또는 종료 유예 기간이 끝날 때까지 기다린다.
- 아직 정상적으로 종료되지 않은 경우 SIGKILL로 프로세스를 강제 종료한다.
종료 유예 기간 지정
종료 유예 기간은 파드 스펙에서 terminateGracePeriodSeconds
필드로 설정할 수 있다.
기본값은 30이고 파드의 컨테이너는 강제 종료되기 전에 정상 종료할 수 있도록 30초가 주어진다.
k delete po mypod --grace-period=5
이렇게 파드를 삭제할 때 파드 스펙에 지정된 종료 유예 기간을 재정의할 수 있다.
그러면 파드가 깨끗하게 종료될 때까지 kubelet이 5초 동안 기다린다. 모든 파드의 컨테이너가 중지되면 kubelet은 API 서버에 알리고 파드 리소스가 삭제된다. 유예 기간을 0으로 설정하고 다음과 같이 --force 옵션을 추가해 확인을 기다리지 않고 API 서버가 리소스를 즉시 삭제하도록 할 수 있다.
k delete po mypod --grace-period=0 --force
이 옵션을 사용할 때는 특히 스테이트풀셋 파드에 주의한다. 스테이트풀셋 컨트롤러는 동일한 파드의 두 인스턴스를 동시에 실행하지 않도록 유의한다. 파드를 강제 삭제하면 삭제된 파드의 컨테이너가 종료될 때까지 기다리지 않고 컨트롤러가 교체 파드를 생성하게 된다. 즉, 동일한 파드의 두 인스턴스가 동시에 실행돼 스테이트풀 클러스터가 오작동할 수 있다. 파드가 더 이상 실행되고 있지 않거나 클러스터의 다른 멤버와 대화할 수 없는 경우 스테이트풀 파드를 강제로 삭제한다.
애플리케이션에서 적절한 셧다운 핸들러 구현
애플리케이션은 SIGTERM 신호에 대응해 셧다운 절차를 시작하고 완료되면 종료해야 한다. SIGTERM 신호를 처리하는 대신, 종료 전 훅으로 애플리케이션을 종료하도록 알릴 수 있다. 두 경우 모두 애플리케이션이 성공적으로 완료하기 위한 일정 시간밖에 없다.
애플리케이션이 분산 데이터 저장소라고 가정해보자. 스케일을 축소하면 파드 인스턴스가 제거되므로 종료된다. 종료 단계에서 파드는 모든 데이터를 나머지 파드로 마이그레이션해 데이터가 손실되지 않도록 해야 한다. 파드는 종료 신호를 수신하면 데이터 마이그레이션을 시작해야 할까? 아니다.
- 컨테이너가 종료해도 파드 전체가 종료되는 것은 아니다
- 프로세스가 종료되기 전에 종료 절차가 끝난 것이라는 보장이 없다
두 번째 시나리오는 애플리케이션의 정상적인 종료가 수행되기 전에 종료 유예 기간이 만료될 때뿐만 아니라 컨테이너의 셧다운 단계 중간에 파드를 실행하는 노드가 실패하는 경우에도 발생한다. 그 후, 노드가 다시 시작하는 경우에도 kubelet은 셧다운 절차를 다시 시작하지 않는다. 파드가 전체 셧다운 단계 전체를 완료할 수 있다는 보장은 전혀 없다.
전용 셧다운 절차 파드를 사용해 중요한 셧다운 절차 대체
반드시 실행해야 하는 중요한 셧다운 절차가 완료될 때까지 셧다운 절차가 실행되도록 보장하는 방법이 있을까?
한 가지 해결책은 애플리케이션이(종료 신호가 수신될 때) 새 파드를 실행하는 새로운 잡 리소스를 만드는 것이며, 그 역할은 삭제된 파드의 데이터를 나머지 파드로 옮기는 것이다. 그러나 주의해야 할 것은 애플리케이션이 잡 오브젝트를 매번 만들 수 있다는 보장이 없다는 것이다. 애플리케이션을 실행할 때 노드가 실패하면 어떻게 될까? 이 문제를 처리하는 적절한 방법은 분산된 데이터의 존재를 확인하는 전용 파드를 항상 실행하는 것이다. 이 파드가 분산된 데이터를 찾아 나머지 파드로 마이그레이션할 수 있다. 항상 실행되는 파드가 아니라 크론잡 리소스를 사용해 정기적으로 파드를 수행할 수 있다.
모든 클라이언트 요청의 적절한 처리 보장
파드가 시작될 때 클라이언트 연결 끊기 방지
파드가 시작되면 레이블 셀렉터가 파드의 레이블과 일치하는 모든 서비스의 엔드포인트에 추가된다. 파드는 쿠버네티스에 준비가 됐다는 신호를 보내야 한다. 그렇지 않으면 서비스 엔드포인트가 추가되지 않으므로 클라이언트로부터 요청을 받지 못한다.
파드 스펙에 레디니스 프로브를 지정하지 않으면 파드는 항상 준비된 것으로 간주된다. 첫 번째 kube-proxy가 노드에서 iptables 규칙을 업데이트하고 첫 클라이언트 파드가 서비스에 연결을 시도하자마자 거의 즉시 요청을 수신하기 시작한다. 그때까지 애플리케이션이 연결을 수락할 준비가 되지 않으면 클라이언트에게 'connection refused'와 같은 오류가 나타난다.
애플리케이션이 들어오는 요청을 올바르게 처리할 준비가 됐을 때만 레디니스 프로브가 성공을 반환하도록 해야 한다. 첫 번째 단계는 HTTP GET 레디니스 프로브를 추가하고 애플리케이션의 기본 URL을 가리키는 것이다. 대부분 정상 동작하며 애플리케이션에서 특별한 레디니스 엔드포인트를 구현하지 않아도 된다.
파드 셧다운 동안 연결 끊어짐 방지
파드 삭제 시 발생하는 이벤트의 순서 이해
API 서버가 파드 삭제 요청을 받으면 먼저 etcd의 상태를 수정한 다음 감시자에게 삭제를 알린다. 이러한 감시자 중에는 kubelet과 엔드포인트 컨트롤러가 있다.
A 이벤트 순서에서 kubelet이 파드를 종료해야 한다는 알림을 받으면 앞에서 설명한대로 셧다운 순서를 시작한다. 애플리케이션이 클라이언트 요청을 즉시 받지 않음으로써 SIGTERM에 응답하는 경우 애플리케이션에 연결하려는 모든 클라이언트는 connection refused 오류를 수신한다.
API 서버에서 kubelet까지 바로 파드가 삭제되는 시점까지 이 작업이 수행되는 데 걸리는 시간은 비교적 짧다.
B의 흐름이 A보다 오래 걸린다. 결과적으로 파드는 종료 신호를 보낸 후에도 클라이언트 요청을 받을 수 있다.
애플리케이션이 서버 소켓을 닫고 연결 수락을 즉시 중지하면 클라이언트가 Connection Refused의 오류를 수신하게 된다.
문제해결
- 몇 초를 기다린 후, 새 연결을 받는 것을 막는다
- 요청 중이 아닌 모든 연결 유지 연결(keep-alive connections)을 닫는다
- 모든 활성 요청이 완료될 때까지 기다린다
- 그런 다음 완전히 셧다운한다
'Infra' 카테고리의 다른 글
OpenSearch Sink Connector 등록 설정 (0) | 2024.04.03 |
---|---|
여러 파드 로그 조회 명령어 Stern (0) | 2023.07.24 |
[ES] rollup 간략 정리 (1) | 2022.09.24 |
[Redis] RedisTemplate, RedisCacheManager 설정에 대해 (2) | 2022.06.15 |
[Docker] 계속 잊어버려서 작성하는 도커 사용 간단 정리 (0) | 2022.06.12 |