쿠버네티스 입문 : 90가지 예제로 배우는 컨테이너 관리 자동화 표준
책을 읽으며 공부한 내용 필기
< 파드 스케줄링 >
쿠버네티스에선 파드를 어떤 노드에 실행할 것인지에 관한 다양한 옵션을 제공
옵션들을 조합하여 사용자가 원하는 구조대로 클러스터 내에 파드들을 배치할 수 있음
특정 파드들을 노드 하나에 모아두거나
특정 IP 대역의 노드들에서만 실행시키거나
같은 기능이 있는 파드들이 노드 하나에 몰리지 않게 골고루 분산하여 실행시키거나
관리가 필요한 노드가 있을 때 해당 노드에 있는 파드들을 다른 노드로 옮기거나
등등
가장 간단한 스케줄링 옵션으로 파드의 spec 필드에서 설정하는 nodeSelector 가 있음
말 그대로 노드를 선택하는 기능
파드가 클러스터 내 구체적으로 어떤 노드에서 실행될지를 키-값 쌍으로 직접 설정함!
노드 셀럭터는 노드의 레이블에 설정된 값으로 노드를 선택
노드의 레이블을 보는 명령어는
kubectl get nodes --show-labels
아래 명령어로 노드(hostname 이 docker-desktop인 노드)에 disktype=hdd 라는 키-값 쌍 레이블을 붙여봄
kubectl label nodes docker-desktop disktype=hdd
그리고 파드 템플릿 yaml 에서 아래와 같이 nodeSelector 로 해당 노드를 선택
그럼 파드가 해당 노드 위에서 실행됨.
만약 nodeSelector : disktype : sdd 같이
존재하지 않는 노드의 라벨을 넣게되면,
파드는 존재하지도 않는 노드를 기다리며 영원히 Pending 상태에 머물게 됨.
노드 어피니티는 (nodeSelector 와 비슷하게) 노드의 레이블을 기반으로 파드를 스케줄링함
노드 어피니티는 두 가지 필드를 갖음
- requitedDuringSchedulingIgnoredDuringExecution : 스케줄링하는 동안 반드시 필요한 조건
- preferredDuringSchedulingIgnoredDuringExecution : 스케줄링하는 동안 만족하면 좋은 조건
반드시 이 조건을 만족해야 하는 것은 아님. 하면 좋고 안 해도 괜춘함
위와 같은 어떠한 조건에 들어맞는 노드를 찾아 그 위에서 파드를 실행시킴
위에 두 조건은 '스케줄링' 하는 동안임.
스케줄링이 이미 완료되었고, 노드 위에서 파드가 동작중일 때는, 위의 조건이 바뀌어도
실행중인 파드는 그대로 실행. 다른 곳으로 옮겨가거나 재스케줄링 하지 않음
노드 어피니티 설정 yaml 파일을 살펴보자
nodeSelectorTerms[].matchExpressions[] 필드를 보자
key는 노드의 레이블 키 중 하나를 설정
operator 와 values 는 key가 만족할 조건
다음과 같이 설정 가능
requited 만 봤을 때 : 위의 예에서는
노드의 레이블 키가 beta.kubernetes.io/os 인 레이블의 값이 linux 혹은 window 두 값 중 일치해야 하고
노드의 레이블 키가 disktype 인 레이블의 값이 존재해야 함
CPU 코어가 4개 이상있는 노드에만 스케줄링한다는 조건을 추가해봄
아래 명령어로 키-값 레이블을 추가. 키 : core, 값 : 8
kubectl label nodes docker-desktop core=8
아래와 같이 key operator values 값을 추가
- key : core operator : Gt values : - "4" |
이런 식으로 key operator values 를 사용할 수 있음....
preferred 만 봤을 때 : 위에 예에는 weight 필드가 있고 nodeSelectorTerms 대신 preference 필드가 있음
preference 필드는 해당 조건에 맞는 걸 선호한다는 뜻
파드를 생성할 때 preference 필드의 조건에 맞는 노드를 우선해서 선택하지만,
없다면 없는 대로 조건에 맞지 않는 노드에 파드를 스케줄링함
weight 필드는 1부터 100까지 값으로 설정 가능
여러 matchExpressions 필드 내의 설정 각각이 노드의 설정과 맞을 때마다 weight 필드 값을 더함
그리고 모든 노드 중 weight 필드 값의 합계가 가장 큰 노드를 선택하여 스케줄링
예를 들어
노드 A 는 CPU 코어 숫자가 4, 메모리 용량이 8
노드 B 는 CPU 코어 숫자가 8, 메모리 용량이 8
yaml 이 다음과 같이 설정되었다면
matchExpressions : - key : core |
노드 A 는 cpu 는 만족하지 않고(6이상이어야 하는데 4밖에 없음) memory 는 만족함
따라서 weight 필드 값 10 한 번만 더하여 total 10
노드 B 는 cpu, memory 둘 다 만족함
따라서 weight 필드 값 10 두 번 더하여 total 20
이 결과를 토대로 나중에 파드가 스케줄링 할 때는 노드 B 를 고름
만약 맞는 노드가 없으면 없는대로 그냥 지나감
파드 사이(inter-pod) 어피니티와 안티 어피니티는 디플로이먼트나 스테이트풀세트로 파드를 배포했을 때
개별 파드 사이의 관계를 정의하는 용도로 사용
노드 어피니티 Node Affinity : 조건에 맞는 노드 위에서 파드를 실행하도록 설정
어피니티 Affinity : 파드들을 함께 묶어서 같은 노드에서 실행하도록 설정
안티 어피니티 Anti-Affinity : 파드들을 다른 노드에 나눠서 실행하도록 설정
서비스 A에 묶인 파드와 서비스 B에 묶인 파드가 서로 자주 통신을 한다면,
어피니티가 이 두 파드들을 같은 노드에 속하게 만들어 효율을 높일 수 잇음
안티 어피니티는 CPU 나 네트워크 같은 하드웨어 자원을 많이 사용하는 앱 컨테이너가 있을 때
여러 노드로 파드를 분산하는 것
안티 어피니티를 설정하지 않으면, 디플로이먼트로 배포한 파드가 노드 하나에서만 실행되어
자원을 많이 소모할 가능성이 있음
안티 어피니티가 설정되지 않으면, 파드 개수를 늘려 자원 경쟁을 해결하려고 해도
이미 시스템 사용률이 높은 노드에 다시 파드가 추가로 실행되어 문제를 더 키울 수 있음
Anti Affinity 설정 yaml 파일을 살펴보자
어피니티와 안티어피니티는 노드 어피니티와 다르게, podAffinity 와 podAntiAffinity 로 나뉘어져서 설정됨.
하지만 설정 방법은 노드 어피니티와 비슷함
key operator values 패턴도 노드 어피니티와 마찬가지로 해석하면 됨.
(topologyKey 는 아래서 다시 설명)
1) 파드 레이블의 key 는 app 이고
2) 파드 레이블 key:app 의 값은 store 로 설정
위 조건에 맞는 파드가 있는 노드를 피해, 없는 노드로 가서 파드를 실행시킴.
즉, spec.template.metadata.labels.app 필드 값이 store 인 파드가 속한 노드를 피해 파드를 추가한다는 설정!
만약 노드가 하나뿐인데 deployment replicas:2 로 실행시키면,
노드 하나에 첫번째 파드 하나가 올라가고,
조건에 맞는 파드가 이미 노드 위에 존재하게 되므로 두번째 파드는 Pending 상태가 됨
반대로 Affinity 설정 yaml 파일을 살펴보자
1, 2) 위에 설명과 동일
3) 파드 레이블의 키가 app
4) 파드 레이블의 키가 app 인 값이 store
이는 Anti Affinity 와 반대로
위 조건에 맞는 파드가 있는 노드를 찾고, 그 노드 위에서 파드를 실행시킴.
즉, spec.template.metadata.labels.app 필드 값이 store 인 파드가 속한 노드를 찾아서 함께 파드를 실행한다는 설정!
궁금했던 topologyKey 는 무엇일까?
노드의 레이블을 이용해 파드의 어피니티와 안티 어피니티를 설정할 수 있는 또 하나의 기준
쿠버네티스는 파드를 스케줄링할 때 먼저 파드의 레이블 기준으로 대상 노드를 찾음
그리고 나서 topologyKey 필드를 확인해서 해당 노드가 원하는 노드인지 확인
위에서는 topologyKey 가 kubernetes.io/hostname 임
hostname 을 기준으로 어피니티 설정을 만족하면 같은 노드에 파드를 실행하고
hostname 을 기준으로 안티어피니티 설정을 만족하면 다른 노드에 파드를 실행
??????????????????
쿠버네티스 클러스터의 특정 노드에 테인트 taint 를 설정할 수 있음.
테인트를 설정한 노드에서는 파드들을 스케줄링하지 않음!(파드 실행 자체가 되지 않음)
테인트 설정한 노드에서 파드 스케줄링을 진행하려면 toleration 을 설정해야 함
톨러레이션을 설정하면, 테인트는 톨러레이션에서 설정한 특정 파드들만 실행하고
다른 파드는 실행하지 못하게 함
테인트와 톨러레이션은 주로 노드를 특정 역할만 하도록 만들 때 사용함
예를 들어,
데이터베이스용 파드 실행 후, 노드 전체의 CPU 나 RAM 자원을 독점하여 사용할 수 있도록 설정하거나
GPU 가 있는 노드에는 GPU 를 사용하는 파드들만 실행되도록 설정
테인트는 키, 값, 효과 세 가지로 구성됨
아래 명령어를 사용
kubectl taint nodes [노드이름] [키]=[값]:[효과]
예) kubectl taint nodes docker-desktop key01=value01:NoSchedule
위 명령을 실행하면, docker-desktop 노드 자체가 taint 되어 파드 실행이 되지 않게됨.
억지로 파드를 생성하면 파드는 pending 이 되고 실행되진 않음
아래 명령으로 노드에 테인트가 설정되었는지 확인 가능
kubectl describe nodes [노드이름]
아래처럼 yaml 의 템플릿에 톨러레이션을 추가해주면,
테인트 적용된 노드에서도 파드가 실행 가능
key 와 value, effect는 노드를 테인트로 만들 때 사용했던 값들을 넣음
노드에 테인트를 적용할 때 아래의 기준으로 명령어를 작성하면 됨
명령어 : kubectl taint nodes docker-desktop key01=value01:NoSchedule
key, value 는 string 값이 옴
effect 는 다음 세 개의 값이 올 수 있음
- NoSchedule : 톨러레이션 설정이 없으면 파드를 스케줄링 하지 않음(기존에 실행중인 파드에는 적용되지 않음)
- PreferNoSchedule : 톨러레이션 설정이 없으면 파드를 스케줄링 하지 않음.
하지만 클러스터 내의 자원이 부족하면, 테인트를 설정한 노드에서도 파드를 스케줄링 가능
- NoExecute : 톨러레이션 설정이 없으면 새로운 파드를 스케줄링하지 않으며,
기존 파드 역시 (테인트 설정을 무시할 수 있는) 톨러레이션 설정이 없으면 종료시킴 ㅎㄷㄷ;;
operator 는 다음 두 개의 값이 올 수 있음
- Equal : key, value, effect 필드값이 원하는 테인트의 설정값과 모두 같은지 확인
- Exists : 이해가 안 되서 그냥 설명을 가져옴
쿠버네티스 클러스터를 사용하다 보면
특정 노드에 있는 파드들을 모두 다른 노드로 옮기거나
특정 노드에 파드들을 스케줄링하지 않도록 제한할 필요가 있음
이런 기능을 제공하는 cordon 과 drain 명령어가 있음
일단 스킵 나중에 다시 보겠음
< 데이터 저장 >
컨테이너 내의 저장된 데이터는 해당 컨테이너가 삭제되었을 때 함께 사라짐
이 데이터들을 보존하기 위해, 컨테이너 외부에 데이터를 저장하는 방법을 지금부터 살펴봄.
'볼륨'과 '퍼시스턴트 볼륨'을 알아보자.
볼륨을 사용하면 컨테이너를 재시작하더라도 데이터가 유지됨
퍼시스턴트 볼륨을 사용하면 데이터를 저장했던 노드가 아닌, 다른 노드에서 컨테이너를 재시작하더라도
데이터를 저장한 볼륨을 그대로 사용 가능
볼륨 관련 필드 중에는 spec.container.volumeMounts.mountPropagation 이 있는데
하나의 파드 내에 있는 컨테이너들끼리 or 같은 노드에 실행된 파드들끼리
볼륨을 공유해서 사용할지를 설정
다음 세 가지 값을 갖음. 무슨 말인지 몰라서 일단 가져옴
나중에는 이해할 수 있겠지......
여기선 로컬 서버에서 사용 가능한 볼륨 중, 내부 호스트의 디스크를 사용하는 emptyDir, hostPath 를 살펴봄
emptyDir 는 파드가 실행되는 호스트의 디스크를 임시로 컨테이너에 볼륨으로 할당하여 사용하는 방법
볼륨을 컨테이너에 붙이는(마운트 하는) 것
파드가 사라지면(컨테이너가 아니라) emptyDir 에 할당해서 사용했던 볼륨의 데이터도 함께 사라짐
연산 중 컨테이너에 문제가 발생해서 재시작되어도 파드가 살아있다면 emptyDir 에 데이터 계속 이용 가능
주로 메모리와 디스크를 함께 이용하는 대용량 데이터 계산에 사용하거나
과학 연산처럼 오랜 연산 시간이 걸리는 상황에서 중간 데이터 저장용으로 디스크가 필요할 때 사용
2) spec.volumes[] 하위 필드에 사용하려는 볼륨들을 먼저 선언
여기선 name필드 값을 emptydir-vol 로 설정
emptyDir 를 사용하기 위해 .emptyDir 필드 값으로 빈 값인 {} 을 설정
1) 이렇게 설정한 볼륨을 컨테이너 설정(spec.containers[].volumeMounts[] 하위 필드)에서 불러와서 사용
volumeMounts[].name 을 emptydir-vol 로 지정하여, 2에서 설정한 볼륨을 사용할 수 있도록 설정
volumeMounts[].mountPath 를 컨테이너의 /emptydir 디렉터리를 설정해 볼륨을 마운트......
(/emptydir 가 데이터 저장되는 위치인가?)
이런 식으로 컨테이너에 직접 볼륨을 마운트하여 사용
emptyDir 이 임시 디렉터리(이 디렉터리는 어디 있는거지??)를 컨테이너에 마운트하는 거라면,
hostPath 는 파드가 실행된 호스트에 있는 실제 파일이나 디렉터리를 파드에 마운트
emptyDir 가 단순히 컨테이너를 재시작했을 때 데이터를 보존하는 역할이라면
hostPath 는 파드를 재시작했을때도 호스트에 데이터가 남음
호스트의 중요 디렉터리를 컨테이너에 마운트하여 사용 가능!
/var/lib/docker 같은 도커 시스템용 디렉터리를 컨테이너에서 사용할때나
시스템용 디렉터리를 마운트하여 시스템을 모니터링하는 용도로 사용
위에서 /tmp 와 /test-volume 위치가 서로 다른데,
책을 보니 컨테이너 내부의 /test-volume 와 host 의 /tmp 가 동일한 위치로 사용됨
즉, 컨테이너 내부에서 /test-volume 에 test.txt 를 생성하면 호스트의 /tmp 에서 test.txt 를 찾을 수 있음.
파드가 delete 되어도 호스트의 /tmp 에서 test.txt 는 그대로 남아있음
1) mountPath 필드는 볼륨을 컨테이너의 /test-volume 이라는 디렉터리에 마운트하도록 값을 설정함
name 필드값으로는 hostpath-vol 을 설정해 볼륨의 이름을 정함
2) name 필드값은 볼륨의 이름인 hostpath-vol 로 설정
경로를 뜻하는 hostPath.path 필드 값은 호스트의 /tmp 디렉터리로 설정
설정한 경로가 어떤 타입인지 뜻하는 type 필드 값은 Directory 로 설정
type 은 다음과 같은 종류를 갖음
nfs 를 마운트하여 볼륨으로 사용하는 방법도 있는데 일단 스킵함.
쿠버네티스에서 볼륨을 사용하는 구조는
- PV 라고 하는 퍼시스턴트 볼륨(PersistentVolume)
- PVC 라고 하는 퍼시스턴트 볼륨 클레임(PersistentVolumeClaim)
2 개로 분리되어 있음
PV : 볼륨 자체를 의미함.
클러스터 안에서 자원으로 다룸
파드하고는 별개로 관리되고 별도의 생명 주기가 있음
PVC : 사용자가 PV 에 하는 요청
사용하고 싶은 용량은 얼마인지, 읽기/쓰기는 어떤 모드로 설정하고 싶은지 등을 요청
쿠버네티스는 볼륨을 파드에 직접 할당하는 방식이 아니라 중간에 PVC 를 두어
파드와, 파드가 사용할 스토리지를 분리하였음
이런 구조 덕분에 파드 각각의 상황에 맞는 다양한 스토리지를 사용할 수 있게 됨.
PVC 가 중간에 있기 때문에 파드는 어떤 스토리지를 사용하는지 신경 쓰지 않음
다음은 PV 와 PVC 의 생명 주기임
프로비저닝 : PV 를 만드는 단계
프로비저닝에는 두 가지 방법이 있음
- PV 를 미리 만들어두고 사용하는 정적 static 방법 :
클러스터 관리자가 미리 적정 용량의 PV 를 만들어두고
사용자 요청이 오면 미리 만들어둔 PV 를 할당함
스토리지 용량 제한이 있을 때 이런 식으로 만듦
근데 만들어둔 용량보다 더 큰 용량을 요구하는 요청들은 실패하게 됨
- 요청이 있을 때마다 PV 를 만드는 동적 dynamic 방법 :
사용자가 PVC 를 거쳐 PV 를 요청했을 때 생성
쿠버네티스 클러스터에서 사용 가능한 스토리지가 1TB 라면,
해당 용량 내에서 원하는 만큼 생성할 수 있음
바인딩 : 프로비저닝으로 만든 PV 를 PVC 와 연결하는 단계
PVC 에서 원하는 스토리지의 용량과 접근 방법을 명시하여 요청하면
거기에 맞는 PV 가 할당됨
PVC 에서 원하는 PV 가 없다면 요청은 실패하는데,
PVC 는 원하는 PV 가 생길 때까지(기존 사용하던 PV 가 반납되거나 새로운 PV 가 생성되거나)
대기하다가 PVC 에 바인딩됨
PV 와 PVC 는 1:1 매핑관계.
PVC 하나가 여러 PV 에 바인딩 되진 않음.
사용 : PVC 는 파드에 설정되고 파드는 PVC 를 볼륨으로 인식하여 사용
반환 : 사용이 끝난 PVC 는 삭제되고, PVC 를 사용하던 PV 를 초기화하는 과정을 거침
초기화 정책에는 다음과 같은 것들이 있음
- Retain : PV 를 그대로 보존.
PVC 삭제 후 PV 가 바로 초기화되지 않고 그냥 PVC 로부터 해제만 됨.
따라서 다른 PVC 가 재사용 가능(재사용하려면 수동으로 초기화 처리해야 함)
물론 데이터 역시 보존됨
- Delete : PVC 삭제 후 PV 삭제(만약 연결된 외부 스토리지 있다면 스토리지 쪽의 볼륨도 같이 삭제)
프로비저닝 동적 볼륨 할당으로 생성된 PV 들의 기본 반환 정책이 Delete 임
- Recycle : deprecated
3) 볼륨은 딱 하나의 accessModes 만 적용 가능. 아래 세 가지가 있음
- ReadWriteOnce : 노드 하나에만 볼륨을 읽기/쓰기 하도록 마운트 할 수 있음
- ReadOnlyMany : 여러 개 노드에서 읽기 전용으로 마운트 할 수 있음
- ReadWriteMany : 여러 개 노드에서 읽기/쓰기 가능하도록 마운트 할 수 있음
4) 스토리지 클래스(StorageClass) 를 설정하는 필드
이해하기로는, 이게 마치 암호문과 같아서
만약 PV 가 A라는 암호문을 들고 있다면 A 암호문을 들고있는 PVC 와만 연결이 성립됨
다른 암호문을 들고있는 PVC 와는 연결이 성립되지 않음
만약 PV 가 StorageClass를 들고있지 않다면, 마찬가지로 아무것도 들고있지 않은 PVC 와만 연결이 성립됨
5) PV 가 해제되었을 때의 초기화 옵션
위에서 설명한 Retain, Delete 중 하나
6) hostPath 로 PV 설정
데이터가 저장될 실제 로컬 서버 내의 path 는 /tmp/k8s-pv
위와 같이 yaml 파일을 만들어 apply 하면 PV 가 생성됨.
아직 PVC 와 연결되지 않았으므로 PVC 를 생성한 후 연결하여 사용하자
spec.resources.requests.storage 필드에 자원을 얼마나 사용할 것인지 적음
필드 값은 앞으로 연결할 PV 의 용량을 초과하면 안 됨
(만약 초과하면 연결에 만족하는 PV 가 없어 PVC 를 생성할 수 없는 Pending 상태가 됨)
apply 로 생성하면 자동으로 조건에 맞는 (똑같은 스토리지클래스, 스토리지 용량 등) PV 를 찾아서 bound 함
kubectl get pv pvc 명령으로 살펴보면
PV 와 PVC 의 status 가 bound (연결) 된 것을 볼 수 있음
PV 는 쿠버네티스 안에서 사용되는 자원
PVC 는 PV 를 사용하겠다고 요청하는 것
이 둘을 연결하기 위해, (파드와 서비스를 연결할 때처럼) 레이블을 사용할 수 있음
위의 yaml 을 보면
PV 에서 metadata.labels 에 키 location: 값 local 인 레이블을 추가함
PVC 에서 spec.selector.matchLabels 에 키 location: 값 local 인 레이블을 추가함
이 두 yaml 을 apply 하면 맞는 레이블끼리 연결이 되겠지
그럼 실제 파드에서 PVC 에 연결하여 PVC 를 볼륨처럼 사용하는 방법을 살펴봄
1) 디플로이먼트 파드에서 사용할 볼륨의 이름을 여기서 명명. 내가 마음대로 명명 가능
pvc-hostpath 라는 이름의 PVC를 사용하기 위해 claimName 을 pve-hostpath 로 두고
myvolume 이라는 이름으로 PVC 를 사용하고 싶기 때문에 name 을 myvolume 으로 둠
참고로 pvc-hostpath 는 위에서 만든 PVC 임
2) deployment 내의 컨테이너들은 myvolume 이라는 볼륨을 사용할 수 있음
컨테이너 내의 /tmp 에 myvolume 을 마운트.
위의 yaml 을 실행하고 /tmp 에 test.txt 파일을 넣으면
호스트의 /tmp/k8s-pv 디렉터리가 컨테이너의 내의 /tmp 와 연결되어있으므로
호스트 /tmp/k8s-pv 안을 살펴보면 test.txt 가 있는 걸 볼 수 있음
책에는 PV 크기를 늘리거나 노드 별 볼륨 개수 제한 에 대한 이야기도 함
노드 하나에 연결할 수 있는 볼륨 개수가 제한되어있다니!
< 클러스터 네트워킹 구성 >
파드마다 IP 가 할당되어 있음.
쿠버네티스는 여러 노드를 사용하여 클러스터를 구성한 후
노드별로 실행한 파드들이 IP를 이용해 서로 통신함
이때 파드 각각은 컨테이너 하나가 아니라 여러 개 컨테이너로 구성
그래서 일반적인 도커와는 네트워킹 구조가 다름
일반 도커의 네트워킹 구조는 다음과 같음
간단하게 말하자면,
호스트에는 '호스트와 도커 컨테이너들을 네트워크로 이어주는 브릿지'를 갖고 있음
docker0 라는 것이 도커가 자체적으로 만드는 기본 default 브릿지임.
컨테이너가 생성되면 각각의 컨테이너가 고유의 ip 를 할당받게 됨
이 ip 는 eth 에 연결되는데, 이 eth 은 virtual eth(veth) 을 통해 브릿지와 연결되고 브릿지로 호스트와 연결됨
따라서 컨테이너의 ip 는 (기본적으로) docker0 브릿지를 이용하여 host 와도 연결됨.
그리고 다른 컨테이너와도 연결이 됨
파드 하나의 네트워킹 구조는 다음과 같음
.....................뭔 말이람!
'Docker' 카테고리의 다른 글
[Kubernetes] 네트워크 설명 링크 (0) | 2020.11.16 |
---|---|
[Docker] Ubuntu 에 Docker 설치하는 방법 (0) | 2020.11.12 |
[Docker] Kubernetes 공부 Part 2 필기 (0) | 2020.11.04 |
[Docker] Container 내부에서 host ip 찾는 방법 (0) | 2020.11.03 |
[Docker] DC/OS vs Kubernetes 비교 (0) | 2020.10.29 |