feat: recovery of kubernetes in action part 3 (#45)

* [PUBLISHER] upload files #43

* PUSH NOTE : 18. Extending Kubernetes.md

* PUSH ATTACHMENT : k8s-18.jpeg

* PUSH NOTE : 17. Best Practices for Developing Apps.md

* PUSH ATTACHMENT : k8s-17.jpeg

* PUSH NOTE : 16. Advanced Scheduling.md

* PUSH ATTACHMENT : k8s-16.jpeg

* PUSH NOTE : 15. Automatic Scaling of Pods and Cluster Nodes.md

* PUSH ATTACHMENT : k8s-15.jpeg

* PUSH NOTE : 14. Managing Pods' Computational Resources.md

* PUSH ATTACHMENT : k8s-14.jpeg

* PUSH NOTE : 13. Securing Cluster Nodes and the Network.md

* PUSH ATTACHMENT : k8s-13.jpeg

* PUSH NOTE : 12. Securing the Kubernetes API Server.md

* PUSH ATTACHMENT : k8s-12.jpeg

* PUSH NOTE : 11. Understanding Kubernetes Internals.md

* PUSH ATTACHMENT : k8s-11 1.jpeg

* fix: delete invalid file

* chore: Pull-Request [blog-7-13-2023] from Obsidian (#44)

* PUSH NOTE : 11. Understanding Kubernetes Internals.md

* PUSH ATTACHMENT : k8s-11.jpeg

* PUSH NOTE : 11. Understanding Kubernetes Internals.md

* PUSH ATTACHMENT : k8s-11.jpeg
This commit is contained in:
2023-07-14 00:12:09 +09:00
committed by GitHub
parent 6be7f42f68
commit 281149f50f
16 changed files with 3270 additions and 0 deletions

View File

@@ -0,0 +1,519 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "11. Understanding Kubernetes Internals"
date: "2021-05-30"
github_title: "2021-05-30-11-k8s-internals"
image:
path: /assets/img/posts/k8s-11.jpeg
---
![k8s-11.jpeg](../../../assets/img/posts/k8s-11.jpeg) _The chain of events that unfolds when a Deployment resource is posted to the API server (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-11)_
### 주요 내용
- Kubernetes 클러스터를 구성하는 컴포넌트의 기능과 역할에 대한 이해
- Pod 생성시 일어나는 일에 대한 이해
- Kubernetes 내부 네트워크와 Service 의 동작 방식 이해
Kubernetes 리소스들이 어떻게 구현되었는지 살펴보자!
## 11.1 Understanding the architecture
---
Kubernetes 클러스터는 다음과 같이 구성되어 있음을 1장에서 배웠다!
- Kubernetes Control Plane: 클러스터의 상태를 저장하고 관리한다
- etcd distributed persistent storage
- API server
- Scheduler
- Controller Manager
- (Worker) Node(s): 실제로 컨테이너를 동작하게 한다
- Kubelet
- Kubernetes Service Proxy (kube-proxy)
- Container Runtime (Docker 등)
이와 별개로도 리소스들을 관리하거나 사용하기 위해서는 이하의 것들이 추가로 필요하다.
- Kubernetes DNS server
- Dashboard
- Ingress controller
- Heapster (14장)
- Container Network Interface network plugin
### 11.1.1 The distributed nature of Kubernetes components
위에서 언급한 컴포넌트들은 각각 별도의 프로세스로 실행된다!
#### 컴포넌트의 통신 방식
컴포넌트들은 반드시 API server 와만 통신하고, 서로 직접 통신하지 않는다. 그리고 etcd 와 통신하는 유일한 컴포넌트는 API server 이다. 클러스터의 상태를 변경하기 위해서는 무조건 API server 를 거쳐야 한다.
#### 컴포넌트의 인스턴스 여러 개 만들기
Control Plane 의 컴포넌트들은 여러 개의 서버 사이에 분리될 수 있으며, Control Plane 을 여러 개 만들어 주면 high availability (HA/고가용성) 을 달성할 수 있다.
또한 etcd 와 API server 의 경우 병렬적인 작업 처리가 가능하지만, Scheduler 와 Controller Manager 의 경우 작업을 한 번에 한 인스턴스만 실행할 수 있다. 나머지는 standby 상태가 된다.
#### 컴포넌트들의 실행 방식
Kubelet 만 시스템에서 실행하고 나머지는 Kubelet 이 pod 의 형태로 실행한다. (물론 Control Plane 의 컴포넌트들은 시스템에서 직접 실행 할수도 있다)
### 11.1.2 How Kubernetes uses etcd
Kubernetes 를 사용하면서 리소스를 생성하고 수정하게 되면 이러한 정보가 persistent 하게 어딘가에 저장되어야 한다. 그래야만 API server 가 재시작하는 등의 경우에도 리소스들이 유지될 수 있기 때문이다. Kubernetes 에서는 클러스터의 정보와 메타데이터를 저장하는 *유일한* 저장소가 etcd 이다. etcd 는 fast, distributed, consistent key-value store 이다. (Distributed 이므로 HA 를 위해 여러 개 생성 가능)
etcd 와 통신하는 유일한 컴포넌트는 API server 이고, 나머지 컴포넌트들은 API server 를 통해 간접적으로 etcd 에 접근하게 된다. 이렇게 구현된 이유는 validation 과 more robust optimistic locking system, 그리고 저장소와의 통신을 API server 에게 맡겨서 abstraction 의 효과를 얻기 위해서이다.
> **Optimistic Concurrency Control** (Optimistic Locking): 데이터에 lock 을 걸어서 read/write 를 제한하지 않고, 데이터에 버전을 추가하는 방식이다. 대신 데이터가 수정될 때마다 버전이 증가하게 되며, 클라이언트가 데이터를 수정하려 할 때 데이터를 읽을 때와 쓰려고 할 때 버전을 비교하여 만약 다르다면 업데이트가 기각당하는 방식이다. 이 때 클라이언트는 데이터를 다시 읽어와서 업데이트를 다시 시도해야 한다.
>
> 모든 Kubernetes 리소스에는 `metadata.resourceVersion` field 가 있어 클라이언트 쪽에서 API server 에 업데이트 요청을 보낼 때 반드시 함께 전달해야 한다. 만약 etcd 에 저장된 버전과 다르다면, 업데이트가 기각된다.
#### etcd 에 리소스가 저장되는 방식
etcd v2 에서는 key 를 계층적으로 저장해서 (hierarchical key space) key-value pair 가 파일 시스템처럼 관리 되었다. 그래서 key 는 다른 key 를 포함하는 폴더이거나, 그냥 value 를 가졌다.
etcd v3 에서는 폴더를 지원하지 않는데, 대신 key 의 형태는 변하지 않고 유지되었다. `/` 를 포함할 수 있는 것이므로 폴더처럼 계층적으로 나뉘어 있다고 봐도 괜찮다.
Kubernetes 에서는 etcd 의 `/registry` 안에 정보를 저장한다.
> etcd 설치하고 etcdctl 로 확인해보려 했으나 실패... 아래 내용은 책의 내용이다. May be outdated.
```
$ etcdctl ls /registry
/registry/configmaps
/registry/daemonsets
/registry/deployments
/registry/events
/registry/namespaces
/registry/pods
...
```
각 리소스 별로 저장되어 있음을 알 수 있다. 만약 pod 의 정보를 보고 싶다면
```
$ etcdctl ls /registry/pods
/registry/pods/default
/registry/pods/kube-system
```
Namespace 별로 구분이 되어있는 것을 확인할 수 있고, 더욱 자세히 확인하면
```
$ etcdctl ls /registry/pods/default
/registry/pods/default/kubia-159041347-xk0vc
/registry/pods/default/kubia-159041347-wt6ga
/registry/pods/default/kubia-159041347-hp2o5
```
Pod 마다 key 가 하나씩 존재하고 있음을 알 수 있다. Value 를 가져와 보면 pod definition 이 JSON 형태로 저장되어 있는 것을 확인할 수 있다.
#### Consistency and validity of stored objects
Kubernetes 에서는 다른 Control Plane 컴포넌트들이 무조건 API server 를 거쳐서 etcd 와 통신하기 때문에, optimistic locking 을 이용하여 항상 consistent 한 업데이트를 할 수 있게 된다. 또한 API server 가 validation 을 해주기 때문에 etcd 에 저장되는 정보는 항상 valid 하며, 업데이트 요청이 인증된 클라이언트로부터만 이뤄지도록 하고 있다.
#### Consistency when etcd is clustered
HA 를 위해 etcd 가 여러 개인 경우, 동기화가 이뤄져야 한다. etcd 에서는 RAFT 알고리즘을 사용하여 etcd 간의 상태를 동기화한다. 그래서 항상 노드의 상태는 다수의 노드가 동의한 현재 상태이거나, 과거에 동의했던 상태가 된다.
이 알고리즘은 consensus 알고리즘이기 때문에 '다수'가 동의해야 다음 상태로 나아갈 수 있게 된다. 그래서 만약 클러스터가 분리되어 두 그룹이 생긴다고 해도, 둘 중 한 그룹에는 분명 '다수'의 노드가 포함되어 있을 것이므로 해당 그룹만 상태를 변경할 수 있고, 다른 그룹은 상태를 변경할 수 없게 된다. 즉, 각 그룹의 상태는 diverge 할 수 없다.
나중에 클러스터가 다시 합쳐지게 되면 '다수'가 아니었던 노드들은 '다수'의 노드 상태를 확인하고 그에 맞게 자신의 상태를 변경하면 된다.
따라서 etcd 인스턴스의 개수는 홀수개로 정하는 것이 좋다!
### 11.1.3 What the API server does
API server 는 기본적인 CRUD interface 를 제공하여 RESTful API 로 클러스터 상태를 조회하거나 수정할 수 있도록 한다. 그리고 그 정보를 etcd 에 저장하며, 저장할 때 validation 을 수행하여 잘못된 데이터가 저장되지 않도록 한다. 또한 optimistic locking 을 이용해 동시성도 관리하고 있다.
`kubectl` 을 사용하게 되면 요청이 API server 로 오게 되는데, 이 과정을 상세하게 살펴본다.
우선 `kubectl` 을 이용해 리소스를 생성하게 되면 HTTP POST 요청이 API server 에 가게 된다.
#### Authenticating the client with authentication plugins
API server 입장에서는, 요청을 보낸 사람이 누구인지 먼저 확인할 필요가 있다. 내부에 authentication plugin 이 존재하므로, 이 plugin 을 사용해서 누가 요청을 보낸 것인지 판단한다. (HTTP 요청을 분석하는 것)
#### Authorizing the client with authorization plugins
요청을 어떤 사용자가 보냈는지 판단했다면, 해당 사용자가 요청의 내용을 실제로 수행할 수 있는지 확인한다. 이 때는 authorization plugin 을 사용하게 된다.
#### Validating and/or modifying the resource in the request with admission control plugins
요청이 리소스 생성/수정/삭제라면, Admission Control 로 요청이 전달된다. 이 또한 plugin 을 사용하게 되는데, 요청의 리소스 spec 에서 누락된 field 값을 채워주거나 (기본값으로 설정하거나) 다른 관련된 리소스의 값을 수정하기도 하며, 요청을 기각할 수도 있다.
> 리소스 읽기는 Admission Control 에 전달되지 않는다!
> Docs: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
#### Validating the resource and storing it persistently
위 과정을 모두 거친 뒤에는 validation 을 수행하고 etcd 에 저장하여 클라이언트에게 응답을 돌려준다.
### 11.1.4 Understanding how the API server notifies clients of resource changes
Controller Manager 의 경우 ReplicaSet 을 관리하거나 service 의 endpoint 를 관리해야 하는데, 이를 위해서는 리소스의 정보가 필요하고, 리소스가 변경되었다면 변경 사실을 알 수 있어야 한다.
그래서 API server 는 리소스 변경사항을 다른 컴포넌트가 확인할 수 있도록 해준다. Control Plane 의 컴포넌트들은 리소스가 생성/수정/삭제될 때 알림을 받도록 요청할 수 있다. 이를 통해 다른 컴포넌트들은 클러스터 상태 변경에 대응할 수 있게 되는 것이다.
클라이언트(변경사항을 지켜보는 다른 컴포넌트)는 API server 에 HTTP 연결을 하게 된다. 이 연결을 통해 요청한 리소스의 수정 내역이 전달된다. 그래서 리소스 변경이 일어나면, 이 리소스의 변경 내역을 요청한 (watch 하고 있는) 모든 클라이언트에게 수정 내역이 전달된다.
### 11.1.5 Understanding the Scheduler
Scheduler 의 동작은 비교적 간단한 편이다. API server 의 watch 를 이용해 새로운 pod 가 생기면 pod 가 띄워질 노드를 하나 골라주면 된다.
Scheduler 는 노드에게 pod 를 띄우라고 절대 알려주지 않는다. Scheduler 가 하는 일은 API server 에 요청을 보내 pod definition 을 수정하는 것이다. 그러면 해당 노드의 Kubelet 이 watch 를 이용해 보고있다가 pod 가 schedule 되었음을 알게 되는 것이다. 이제 Kubelet 은 띄워야 할 pod 가 생긴 것이므로 pod 를 실행하게 된다.
큰 그림은 간단하지만, 노드를 고르는 일은 생각보다 복잡하다.
#### Default scheduling algorithm
- Scheduling 이 가능한 노드(acceptable nodes)의 리스트를 만든다.
- 만든 리스트의 노드들 중에서 가장 적합한 노드를 고르고, 만약 가장 적합한 노드가 여러 개라면, round-robin 을 이용하여 균등하게 scheduling 되도록 한다.
#### Finding acceptable nodes
Acceptable 인지 아닌지 판단하기 위해서는 미리 정의된 조건들을 확인한다. 몇 가지만 살펴보면..
- Pod 가 요구하는 하드웨어 리소스가 노드에 충분한가?
- 노드에 리소스가 거의 바닥나지는 않았는지?
- Pod 에서 특정한 노드로의 scheduling 을 요청하지는 않았는지?
- Node selector label 이 맞는지?
- Pod 가 특정 포트로 요청을 받는다면 해당 포트가 노드에서 이미 bind 되어 있지는 않은지?
- Pod 가 요구하는 volume 이 이 노드에서 mount 될 수 있는지?
- Pod 에 node affinity/anti-affinity 설정이 있는지?
이러한 조건들을 모두 만족해야 acceptable node 가 된다.
#### Selecting the best node for the pod
위에서 다양한 조건으로 검사를 했지만, 그럼에도 불구하고 어떤 노드는 더 나은 선택일 수도 있다. 2-노드 클러스터가 있다고 하면, 둘다 acceptable 인데 한 노드가 10개의 pod 를 이미 실행하고 있는 반면 나머지 하나는 실행 중인 pod 가 없다고 하자. 그러면 자연스럽게 실행 중인 pod 가 없는 쪽으로 scheduling 을 할 것이다.
하지만 클라우드에서 실행하는 경우라면, 그냥 10개 pod 를 실행 중인 노드에 scheduling 하고 나머지 node 는 없애서 cloud provider 에게 메모리를 돌려주는 것이 나을 수도 있다.
#### Multiple schedulers
여러 개의 scheduler 를 사용할 수도 있으며, `schedulerName` field 를 사용해 어떤 scheduler 를 사용할지 정할 수 있다.
### 11.1.6 Introducing the controllers running in the Controller Manager
앞에서 언급한 것처럼 API server 는 etcd 에 저장하고 클라이언트에게 알리며, scheduler 는 노드를 선택하기만 하기 때문에, 변경 사항이 생겼을 때 실제로 변경된 클러스터 상태에 다가가도록 일할 컴포넌트가 필요하다. 이 일은 Controller Manager 안의 controller 들이 수행한다.
Controller 종류는 거의 모든 리소스 별로 하나씩 있는 느낌이다.
- Replication Manager (ReplicationController)
- ReplicaSet, DaemonSet, Job controllers
- Deployment, StatefulSet controllers
- Node controller
- Service, Endpoints controller
- Namespace controller
- PersistentVolume controller
- Others
즉 리소스는 클러스터 내부에서 돌아가야 하는 것들의 정보/명세이며 controller 는 그 정보를 바탕으로 실제로 실행하는 역할을 한다고 생각하면 된다.
#### Understanding what controllers do and how they do it
결국에는 API server 를 watch 하고 있다가 변경 내역이 생기면 일을 한다고 생각하면 된다. Controller 들은 서로의 존재를 모르며 각자 API server 와 통신할 뿐이다.
추가로 controller 들은 reconciliation loop 을 사용하여 목표 상태와 현재 상태를 비교한다. 또 `status` 에 현재 상태를 기록하며, API server 의 watch 기능이 모든 변경 이벤트를 준다는 보장은 없으므로 주기적으로 re-list operation 을 수행하여 놓친 변경 내역이 없는지 확인한다.
#### Replication Manager
ReplicationController 를 관리한다.
API server 에 ReplicationController 리소스를 watch 하고 있으며, replica count 에 변경 사항이 생기면 알림을 받게 된다. 또한 pod 리소스도 watch 하고 있어서 실제로 몇 개의 replica 가 실행 중인지 확인할 수 있다.
만약 replica count 와 실제 replica 수가 다르다면, POST/DELETE 요청을 API server 에게 보내서 pod 를 생성/삭제하도록 한다. 실제 생성과 삭제는 Scheduler 와 노드의 Kubelet 이 담당하게 된다.
#### ReplicaSet, DaemonSet, Job controllers
ReplicaSet 의 경우 Replication Manager 와 비슷하다.
한편 DaemonSet 과 Job controller 는 비슷한데, 자신들이 watch 하고 있는 리소스의 pod template 를 참고하여 API server 에게 pod 생성 요청을 보낸다. 마찬가지로 실제 pod 생성은 Kubelet 이 하게 된다.
#### Deployment controller
실행 중인 Deployment 의 상태가 리소스 정보와 동일하도록 계속 동기화하는 역할을 담당한다.
Deployment 리소스의 정보가 수정될 때마다 controller 는 새 버전을 rollout 을 하게 되는데, 이는 ReplicaSet 을 이용해서 한다. 그리고 deployment strategy 에 따라 각 ReplicaSet 의 replica 수를 적절히 조절하여 모든 pod 이 새로운 pod 로 교체되도록 한다.
9장에서 언급했듯이 Deployment 가 pod 를 직접 만들지 않는다.
#### StatefulSet Controller
(Deployment 에 state 가 추가된 것이니...) ReplicaSet controller 와 유사하다. 하지만 ReplicaSet controller 는 pod 만 관리하는 반면 StatefulSet controller 는 pod 의 PVC 까지 같이 관리하게 된다.
#### Node controller
클러스터의 worker node 정보를 담고있는 Node 리소스를 관리한다. 또한 노드의 health 를 확인하고 도달할 수 없는 (unreachable) 노드의 pod 는 제거한다.
#### Service controller
`LoadBalancer` service 를 관리하는 controller 이다.
#### Endpoints controller
Service object 들은 endpoints 를 확인하여 요청을 분산하는데, 이 endpoint 를 관리한다. Service 를 watch 하고 있고, label selector 에 맞는 pod 들을 watch 하고 있다가 pod 가 `READY` 상태가 되면 Endpoints 리소스에 pod 의 IP 와 포트를 추가한다.
Endpoints 는 standalone object 이므로 controller 가 직접 생성하고 삭제한다.
#### Namespace controller
Namespace 가 삭제될 때, 해당 namespace 안의 모든 리소스를 삭제하는 역할을 담당한다.
#### PersistentVolume controller
사용자가 PVC 를 생성했을 때, 적절한 PV 를 찾아 bind 해주는 역할을 담당한다.
PVC 가 생성됐을 때 access mode 를 만족하는 가장 작은 PV 를 찾아준다. 내부적으로 용량으로 정렬된 PV 목록을 가지고 있다.
### 11.1.7 What the Kubelet does
처음에 노드가 생성될 때는 API server 에 Node 리소스를 생성한다. 그리고 APi server 에 watch 하여 자신의 노드로 scheduling 된 pod 가 있으면 pod 의 컨테이너를 실행한다. Kubelet 은 실행중인 컨테이너들을 지속적으로 모니터링하여 상태와 이벤트 그리고 (하드웨어) 리소스 사용량을 API server 에 보고한다.
Liveness probe 를 실행하는 것도 Kubelet 이다. 실패하면 Kubelet 이 재시작 명령을 내린다.
#### Running static pods without the API server
일반적으로는 API server 에 pod definition 을 요청하여 pod 를 생성하겠지만, 로컬 저장소에 있는 definition 으로부터 pod 를 생성할 수도 있다. Pod manifest 를 Kubelet 의 manifest 폴더에 넣어주면 Kubelet 이 그것을 실행하고 관리해준다.
이 방식을 이용해서 Control Plane 의 컴포넌트들을 pod 형태로 실행할 수 있는 것이다.
### 11.1.8 The role of the Kubernetes service proxy
kube-proxy 는 클라이언트들이 생성된 service 에 연결될 수 있도록 해준다. Service IP 와 port 로 들어오는 요청이 service endpoints 중 하나의 pod 로 연결되도록 보장하며, 만약 endpoint 가 여러 개라면 로드 밸런싱도 해준다.
#### Why it's called a proxy
예전에는 리눅스 `iptables` 를 수정하여 kube-proxy 서버로 요청이 오도록 한 다음 처리하여 pod 에게 전달했었기 때문이다. 현재는 `iptables` 규칙을 수정하여 proxy 서버를 거치지 않고 바로 endpoint pod 중 랜덤한 하나에게 요청이 가도록 하고 있다. (packet redirection) 후자가 성능이 더 좋다.
### 11.1.9 Introducing Kubernetes add-ons
#### How add-ons are deployed
Pod 로 띄워지기도 하고 Deployment 나 ReplicationController 로 띄워지기도 한다. 결국 YAML 파일을 API server 에 POST 하는 점은 동일하다.
> `minikube` 에서는 Ingress controller 와 coredns 가 deployment 로 띄워져있는 것을 확인했다.
#### How the DNS server works
DNS server pod 는 `kube-dns` service 를 통해 expose 되어 있으며, service 의 IP 주소는 pod 의 모든 컨테이너 내부의 `/etc/resolv.conf` 파일 안에 `nameserver` 로 들어가 있다.
`kube-dns` pod 는 Service 와 Endpoints 리소스를 watch 하고 있어서 변경 사항이 생기면 DNS record 를 업데이트한다. 이 업데이트에는 약간의 지연이 있을 수 있다.
#### How (most) Ingress controllers work
구현체마다 조금씩 다를 수 있지만 대부분 reverse proxy server 를 둔다. 그리고 reverse proxy server 의 세팅을 Ingress, Service, Endpoints 에 맞게 해준다. (API server 의 watch 기능 이용)
Ingress 를 사용하게 되면 service 를 거치지 않고 endpoint 로 바로 요청이 전달되는 것도 이 때문이다. 더불어 client IP 가 유지되는 장점도 있다.
### 11.1.10 Bringing it all together
Kubernetes 시스템이 각 역할과 책임을 가진 개별적인 컴포턴트들로 잘 분리되어있음을 알게 되었다.
사용자가 정의한 목표 상태에 클러스터가 도달할 수 있도록 컴포넌트들이 협동한다.
## 11.2 How controllers cooperate
---
Pod 를 생성할 때 어떤 일이 일어나는지 자세히 살펴본다. Pod 를 직접 생성하는 경우는 잘 없으므로, Deployment 를 생성하는 경우를 살펴볼 것이다.
### 11.2.1 Understanding which components are involved
시작하기 전, controllers, Scheduler, Kubelet 이 API server 를 watch 하고 있다는 사실을 기억해야 한다.
### 11.2.2 The chain of events
`kubectl` 을 이용해서 Deployment 를 생성하게 되면, Kubernetes API server 는 POST 요청을 받게 된다.
API server 는 spec 을 validation 하고, etcd 에 저장하며, `kubectl` 에 응답을 돌려준다.
그 다음부터는 연쇄적으로 반응이 일어난다.
#### Deployment controller creates the ReplicaSet
Deployment 리소스를 watch 하고 있던 모든 클라이언트들은 새롭게 생성된 deployment 에 대해 알게 된다. 이 클라이언트 중에 Deployment controller 가 있으므로, controller 는 새로운 deployment 를 감지하고 현재 specification 에 맞는 ReplicaSet 을 생성한다. 이 또한 API server 에 요청하는 것이다.
#### ReplicaSet controller creates the pod resources
ReplicaSet controller 가 새로운 ReplicaSet 을 감지하고, pod selector 의 값을 이용해서 replica count 와 같은 pod 개수가 존재하고 있는지 확인한다.
(새로 생성하면 당연히 개수가 부족하므로) 이제 controller 는 ReplicaSet spec 의 pod template 을 참고하여 API server 에 pod 생성을 요청한다.
#### Scheduler assigns a node to the newly created pods
새롭게 생성된 pod 는 etcd 에 저장되어 있지만 아직 노드가 결정되지 않은 상태라 `nodeName` field 가 없다. Scheduler 는 이렇게 `nodeName` field 가 없는 pod 들을 watch 하고 있다가 발견하면 scheduling 을 해준다. 이제 pod 정보에 `nodeName` 이 존재하게 된다.
여기까지는 Control Plane 에서 일어나는 일이다. Controller 들은 단지 API server 와 통신하여 리소스를 업데이트하기만 했다.
#### Kubelet runs the pod's containers
이제 worker node 들의 차례이다. Kubelet 은 자신의 노드에 schedule 된 pod 를 watch 하고 있으므로 pod definition 을 가져와 Docker (혹은 container runtime) 에게 특정 이미지를 기반으로 컨테이너를 실행하라고 명령한다. 이제 container runtime 이 컨테이너를 실행하게 된다.
### 11.2.3 Observing cluster events
위 작업이 수행되는 과정에서 Control Plane 컴포넌트와 Kubelet 은 API server 에 event 가 발생했다고 알려준다. Event 리소스를 생성하여 알려주며, 이는 `kubectl get events` 로 확인할 수 있다.
> 참고로 `kubectl describe` 하면 밑에 event 가 나왔었다.
명령어를 입력해 보면 `SOURCE` 에 어떤 컴포넌트/controller 가 event 를 발생시켰는지, `KIND`, `NAME` 에서 event 가 영향을 미친 리소스의 종류와 이름을 확인할 수 있다. `REASON``MESSAGE` 에서는 상세한 설명을 확인할 수 있게 된다.
## 11.3 Understanding what a running pod is
---
Pod 를 실행했으므로, *실행중인 pod* 가 무엇인지 살펴보자. Kubelet 이 컨테이너를 실행하는 것은 알았는데, 더 할 일이 있을까?
Pod 를 실행한 뒤, *노드*에 `ssh` 연결하여 `docker ps` 를 입력해 보면 `COMMAND``/pause` 인 컨테이너가 하나 있는데, 이 컨테이너가 한 pod 내의 컨테이너를 하나로 묶는 역할을 해준다.
Pod 내의 컨테이너는 linux namespace 와 네트워크를 공유하기 때문에, `/pause` 컨테이너는 이 linux namespace 를 붙잡고 있는 infrastructure 컨테이너인 것이다.
Pod 내의 컨테이너는 재시작 되는 경우가 많다. 이 때 같은 linux namespace 로 재시작 되어야 하는데, 위와 같이 infrastructure 컨테이너가 존재하기 때문에 같은 linux namespace 를 사용할 수 있게 된다. 이 infrastructure 컨테이너는 pod 와 lifecycle 이 같기 때문에 pod 가 삭제될 때 같이 지워진다. 만약 infrastructure 컨테이너를 삭제하면 Kubelet 이 다시 만들어 주고, pod 의 컨테이너도 전부 다시 만든다.
## 11.4 Inter-pod networking
---
Pod 들은 고유 IP 를 가지며, NAT 없이 flat network 구조를 갖는다. 작동 원리를 살펴본다.
### 11.4.1 What the network must be like
Kubernetes 는 특정 네트워크 기술을 요구하지는 않지만, 컨테이너들끼리는 어떤 노드에 있을지라도 서로 통신이 가능해야 한다.
또한 통신에 사용하는 IP 는 NAT 가 적용되지 않아서, 어디서 바라보더라도 자신의 IP 가 동일해야 한다. 이렇게 되야 하는 이유는 마치 같은 네트워크에 연결된 머신에서 동작하는 것처럼 보이고 또 간단해지기 때문이다.
이러한 요구사항을 만족하는 네트워크 기술은 많지만 구현체마다 다르고 상황에 따라 장단점이 다르기 때문에, 일반적인 방법에 대해 다룰 것이다.
### 11.4.2 Diving deeper into how networking works
Infrastructure 컨테이너에 IP 주소와 network namespace 가 설정된다는 것을 11.3 에서 확인했다. Pod 의 컨테이너는 해당 network namespace 를 사용하게 된다.
#### Enabling communication between pods on the same node
Infrastructure 컨테이너가 시작되기 전에 virtual Ethernet (veth) 인터페이스 pair 가 컨테이너에 생성된다. 한쪽은 노드의 namespace 에 남아있고, 다른 쪽은 컨테이너 안의 namespace 가 되어 `eth0` 으로 이름이 변경된다.
이제 노드의 namespace 에 남아있는 인터페이스는 container runtime 이 사용하는 network bridge 에 연결된다. 이제 `eth0` 인터페이스는 bridge 의 IP 대역 중 하나를 할당받아 IP 로 사용하게 된다.
컨테이너 내부에서 요청이 나가는 경우 `eth0` -> `vethXXX` -> Bridge 의 순서로 가게 된다.
만약 이 요청의 destination 이 노드 내의 다른 pod 라면 Bridge -> `vethYYY` -> `eth0` 의 경로로 다른 pod 에 요청이 전달된다.
#### Enabling communication between pods on different nodes
다른 노드의 pod 와 통신하는 방법은 다양하지만, layer 3 routing 방법을 살펴볼 것이다.
우선 Pod IP 는 클러스터 내에서 유일해야 하므로, 노드의 network bridge 에 할당된 IP 대역은 서로 겹치지 않아야 한다.
각 노드에 layer 3 networking 을 적용하여 노드의 physical network interface 가 bridge 에 연결되도록 해주면 된다. 그리고 각 노드에서 routing table 을 설정해주면 된다.
이제 다른 노드로 통신하려는 경우 패킷의 이동 경로는 `eth0` -> `vethXXX` -> Bridge -> Node's physical adapter -> Network Wire -> Other node's physical adapter -> Bridge -> `vethYYY` -> `eth0` 가 된다.
이 방법은 물론 노드가 같은 네트워크 스위치에 연결된 경우, 사이에 라우터가 없을 때만 유효하다. 물론 라우터를 사용하여 노드 사이에 오고가는 패킷을 routing 할 수 있겠지만, 노드 개수가 많아지거나 노드 사이의 라우터 개수가 많아지면 어렵고 오류가 발생하기 쉽다. 이러한 이유로 Software Defined Network (SDN) 을 사용하는 것이 편하다.
SDN 을 사용하게 되면 노드들이 같은 네트워크 스위치에 연결된 것처럼 보이게 되며, pod 에서 나가는 패킷은 캡슐화되어서 다른 pod 에 전달되고 un-캡슐화된다.
### 11.4.3 Introducing the Container Network Interface
컨테이너가 네트워크에 연결하는 것을 쉽게 하기 위해 Container Network Interface (CNI) 프로젝트가 시작되었다. Kubernetes 에서 해당 CNI 플러그인을 사용하도록 할 수 있다.
종류에는 Calico, Flannel, Romana, Weave Net 등이 있다.
플러그인 설치는DaemonSet 과 기타 리소스를 포함한 YAML 파일을 생성하면 된다. 참고로 Kubelet 실행시 `--network-plugin=cni` 로 옵션을 줘야 한다.
## 11.5 How services are implemented
---
복습!
- 각 service 는 stable IP 주소와 포트를 갖고, 클라이언트는 이 주소로 연결하여 service 를 사용한다.
- IP 주소는 가상 IP 주소로, 네트워크 인터페이스에 할당되어있지 않고, 노드 밖으로 나가는 패킷에 source/destination IP 로 들어가지 않는다.
### 11.5.1 Introducing the kube-proxy
Service 와 관련된 모든 것들은 각 노드에서 실행중인 kube-proxy 프로세스에 의해 관리된다.
예전에는 `userspace` proxy mode 를 통해 진짜 pod 로 연결을 proxy 해주는 역할을 했으나, 현재는 `iptables` 를 사용하고 있다.
### 11.5.2 How kube-proxy uses iptables
API server 가 service 생성 요청을 받으면, service 에 가상 IP 주소가 바로 할당된다. 그 다음, API server 는 worker node 의 kube-proxy 에게 새로운 service 가 생성됐음을 알린다. 그 다음 kube-proxy 는 해당 service 가 참조 가능하도록 자신의 노드의 `iptables` 규칙을 수정한다.
`iptables` 규칙을 수정하게 되면 service 를 목적지로 갖는 패킷을 가로채 목적지 주소가 변경되고 endpoint 중 한 곳으로 패킷이 연결된다.
kube-proxy 는 service 의 변화만 watch 하고 있는 것은 아니고, Endpoints 의 변화도 지켜보고 있다. 그래서 pod 가 생성/삭제되거나 readiness 상태가 바뀌거나 label 이 수정되었는지 확인한다.
#### 예시
예를 들어, pod A 에서 172.30.0.1:80 에 있는 service 로 패킷을 보낸다고 하면, 패킷의 목적지 주소는 172.30.0.1:80 일 것이다. 네트워크로 패킷이 보내지기 전에, 노드의 `iptables` 규칙을 먼저 적용하게 된다.
규칙 중에서 적용할 규칙이 있는지 확인하게 되는데, service 가 생성된 상태이므로 '목적지가 172.30.0.1:80 인 패킷의 목적지는 임의로 선택된 pod 의 IP 로 교체되어야 한다'는 규칙이 있을 것이다.
그러므로 이 패킷의 목적지는 service 의 endpoint 중 한 pod 의 주소와 포트로 변경된다. 이때부터는 마치 클라이언트에서 선택된 pod 로 직접 (directly) 요청을 보낸 것처럼 보이게 된다.
## 11.6 Running highly available clusters
---
Kubernetes 를 사용하는 이유 중 하나로 인프라에 문제가 생겨도 중단 없이 서비스를 유지할 수 있다는 점이 있다. 중단 없이 서비스를 유지하기 위해서는 앱이 계속 실행되고 있어야 하기도 하지만, 노드에 있는 Control Plane 컴포넌트들이 잘 작동해 줘야 한다. 고가용성 (HA) 를 달성하기 위해 어떤 것들이 관련되어 있는지 살펴볼 것이다.
### 11.6.1 Making your apps highly available
노드에 문제가 생기더라도 Kubernetes 의 다양한 controller 덕분에 우리의 애플리케이션은 안정적으로 돌아갈 수 있게 된다. 애플리케이션의 HA 를 위해서는 Deployment 를 사용해서 충분한 replica 수를 정해주면, 나머지는 Kubernetes 가 알아서 해줄 것이다.
#### Running multiple instances to reduce the likelihood of downtime
당연히 여러 인스턴스를 만들어두면 downtime 이 줄어들 것이다. 물론 애플리케이션이 horizontally scalable 해야한다는 조건이 있다. 만약 scalable 하지 않더라도 Deployment 를 이용해 replica count 를 1로 설정해서 배포하는 것이 좋다. 문제가 생겼을 때 어쩔 수 없이 약간의 지연이 있긴 하겠지만 알아서 생성해주긴 할 것이다.
#### Using leader-election for non-horizontally scalable apps
Downtime 을 회피하기 위해서는 비활성 상태의 replica 를 몇 개 더 만들어두고, leader-election 방법을 이용해 replica 들 중 반드시 하나만 작동하도록 해주면 된다.
만약 나머지도 전부 동작한다면, 하나의 replica 만 write 가 가능하고 나머지는 read 만 가능하게 해도 괜찮을 것이다. 중요한 것은 인스턴스끼리 동기화 문제가 발생하지 않도록 하면 된다는 점이다.
그리고 이 leader-election 을 애플리케이션에 구현할 필요는 없고, sidecar 컨테이너에 담을 수 있도록 이미 구현체가 존재한다.
### 11.6.2 Making Kubernetes Control Plane components highly available
만약 Kubernetes 에 문제가 생기면 진짜 답이 없어지는데, 이런 경우를 위해서는 Control Plane 컴포넌트들에 HA 를 달성해야 한다. 이를 위해서는 마스터 노드를 여러개 띄워야 하는데, 각 노드들은 API server, etcd, Controller Manager, Scheduler 를 각각 가지고 있어야 한다.
#### Running an etcd cluster
etcd 는 애초에 분산 시스템을 위해 설계되었기 때문에 여러 개의 etcd 를 띄워도 괜찮다. 홀수 개의 etcd 만 유지하고, 각 etcd 인스턴스가 다른 노드에 있는 etcd 인스턴스의 존재를 알고 있으면 된다. 이는 각 인스턴스의 설정에 다른 인스턴스들의 리스트를 넘겨줘서 설정한다. (다른 etcd 의 IP/포트 정보를 넘겨준다)
etcd 는 앞에서 설명한 RAFT 로 데이터를 모든 인스턴스에 복제할 것이기 때문에, 인스턴스 몇 개에 문제가 생겨도 괜찮다. HA 를 위해서는 5~7개의 etcd 를 띄워서 2~3개 정도의 etcd failure 를 감당할 수 있도록 하면 된다.
#### Running multiple instances of the API server
API server 를 복제하는 것은 더 쉽다! API server 는 거의 stateless 하다. 물론 약간의 caching 이 있지만 데이터는 전부 etcd 에 저장되기 때문에 여러 인스턴스를 띄울 수 있게 된다. 인스턴스끼리 서로의 존재를 알 필요도 없다.
다만 API server 의 앞단에 로드 밸런서를 두어 API server 중 healthy 한 쪽으로만 클라이언트의 요청이 가도록 조절할 필요가 있다.
#### Ensuring high availability of the controllers and the Scheduler
얘네 둘은 간단하지 않다. Controller 와 Scheduler 는 클러스터의 상태를 계속 모니터링하고 상태가 변할 때 일하기 때문에 클러스터 상태가 변한 것을 감지한 다수의 controller 가 한꺼번에 반응하게 될 수도 있다. (예를 들면 replica count 가 변경된 것을 보고 5개의 controller 가 한꺼번에 pod 생성 요청을 보내면 pod 가 5개 추가될 것이다)
위와 같은 이유로 controller 와 Scheduler 는 한 번에 한 인스턴스씩 일을 할수 있도록 이미 설계되어 있다. 내부적으로 leader-election 알고리즘을 가지고 있어서, 자신이 leader 일 때만 작업을 수행한다. 나머지 인스턴스들은 leader 에게 문제가 생길 때까지 가만히 있는다.
#### Understanding the leader election mechanism used in Control Plane components
여기서 흥미로운 점은 leader election 을 위해 컴포넌트들이 서로 통신하지 않아도 된다는 점이다. Leader election 의 동작 과정을 보면 API server 에 Endpoints 리소스를 만들어서 한다. (저자는 Endpoints 리소스를 그렇게 쓰는거 아니라는 의미에서 *abused* 라고 표현했다. 다른 리소스를 사용했어도 괜찮았을 것이라고 한다.)
`kube-scheduler` 라는 이름을 가진 Endpoints 리소스를 확인해 보면 annotation 으로 `control-plane.alpha.kubernetes.io/leader` 를 가지고 있는 것을 확인할 수 있다. 여기에 `holderIdentity` 라는 필드가 있는데, 여기에 현재 leader 의 이름이 들어간다.
Optimistic locking 때문에 여러 개의 Scheduler 가 자신을 `holderIdentity` 에 넣으려고 하겠지만, 오직 한 인스턴스만 성공하며, 그 인스턴스가 leader 가 된다. 또한 자신이 계속 살아있음을 증명해야 하기 때문에 주기적으로(2초마다) 이 Endpoints 리소스를 업데이트하여 다른 Scheduler 에게 알린다. Leader 가 실패했음을 알게된다면 다른 인스턴스들은 모두 자신의 이름을 `holderIdentity` 에 넣고 leader 가 되기 위해 또다시 경쟁한다.
---
## Discussion & Additional Topics
### RAFT
- https://en.wikipedia.org/wiki/Raft_(algorithm)
- https://raft.github.io/
### Authentication vs Authorization
### Leader Election Algorithm
- https://en.wikipedia.org/wiki/Leader_election

View File

@@ -0,0 +1,525 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "12. Securing the Kubernetes API Server"
date: "2021-06-06"
github_title: "2021-06-06-12-securing-k8s-api-server"
image:
path: /assets/img/posts/k8s-12.jpeg
---
![k8s-12.jpeg](../../../assets/img/posts/k8s-12.jpeg) _Roles grant permissions, whereas RoleBindings bind Roles to subjects (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-12)_
### 주요 내용
- Authentication 과 ServiceAccount 에 대한 이해
- RBAC plugin, Role/RoleBinding, ClusterRole/ClusterRoleBinding 에 대한 이해
## 12.1 Understanding authentication
---
API server 가 요청을 받게 되면, authentication plugin 을 거치며 요청을 보낸 주체가 누구인지 분석한다.
Plugin 을 거치게 되면 username, user ID, 클라이언트가 속하는 group 등의 정보가 API server 에게 전달된다.
### 12.1.1 Users and groups
#### Understanding users
Kubernetes 에서는 API server 에 연결하는 주체를 2가지로 분류한다.
- 사람 (users)
- Pods (정확히는 pod 내에 실행 중인 애플리케이션)
실제 사람과 같은 경우 외부 시스템 (SSO 등) 에 의해 관리된다. (관심 대상이 아니다) 실제 사용자의 계정을 관리하는 리소스는 존재하지 않는다.
한편 pod 의 경우 *service accounts* 를 사용하는데, 이는 클러스터에 ServiceAccount 리소스로 저장된다.
#### Understanding groups
모든 사용자는 하나 혹은 그 이상의 group 에 속할 수 있다. Group 을 사용하게 되면 여러 명의 user 에게 동시에 권한을 줄 수 있다.
Authentication plugin 에서 group 을 알려준다고 했는데, plugin 은 group 을 string 으로 알려준다. 몇 가지의 내장된 group 이 있다.
- `system:unauthenticated`: 모든 authentication plugin 이 요청을 authenticate 할 수 없는 경우
- `system:authenticated`: Authentication 이 성공한 user 에게 자동으로 설정된다
- `system:serviceaccounts`: 시스템(클러스터)에 존재하는 모든 ServiceAccounts 를 포함한다
- `system:serviceaccounts:<namespace>`: 특정 namespace 안의 모든 ServiceAccounts 를 포함한다
### 12.1.2 Introducing ServiceAccounts
Pod 가 API server 와 통신할 때는 `/var/run/secrets/kubernetes.io/serviceaccount/token` 에 있는 token 을 이용해서 authentication 한다는 것을 배웠다. (이 파일은 `secret` volume 을 통해 mount 된다) 이제 이 파일이 정확히 무엇을 의미하는지 알아본다.
모든 pod 는 ServiceAccount 와 연결되게 되는데, 이 token 파일에는 ServiceAccount 의 authentication token 이 들어있다. 그래서 애플리케이션이 token 을 이용해서 API server 와 통신하게 되면, authentication plugin 에서는 해당 ServiceAccount 를 authenticate 하고, API server 에게 ServiceAccount username 을 알려준다.
ServiceAccount username 은 다음과 같은 형식으로 구성되어 있다.
```
system:serviceaccount:<namespace>:<service account name>
```
이제 API server 는 이 ServiceAccount username 을 authorization plugin 에게 넘기고, 요청의 action 을 수행할 권한이 있는지 확인한다.
결국 ServiceAccount 는 pod 내의 애플리케이션이 API server 와의 통신에서 자신을 authenticate 하는 방법이다.
#### Understanding the ServiceAccount resource
ServiceAccount 도 namespace 에 귀속된다.
각 namespace 에는 default ServiceAccount 가 기본적으로 생성되며, pod 들은 이 default ServiceAccount 를 기본적으로 사용한다.
```
$ kubectl get sa
NAME SECRETS AGE
default 1 79d
```
각 pod 는 오직 하나의 ServiceAccount 와 연결될 수 있지만, 같은 ServiceAccount 를 여러 pod 에서 재사용 할 수 있다. 단 제약 조건이 있는데, 같은 namespace 의 ServiceAccount 만 사용할 수 있다.
#### Understanding how ServiceAccounts tie into authorization
Pod manifest 에 ServiceAccount 를 지정하게 되는데, (지정하지 않으면 default 사용) ServiceAccount 마다 역할이 정해져 있기 때문에 pod 의 권한 또한 제어가 가능하다.
API server 가 token 을 받으면 어떤 ServiceAccount 인지 확인하고, 해당 ServiceAccount 의 권한을 확인하여 요청을 수행할 권한이 있는지 확인하게 되는 방식이다.
### 12.1.3 Creating ServiceAccounts
권한 부여는 최소로 하는 것이 원칙이다. 그래서 pod 의 용도에 맞게 ServiceAccount 를 직접 설정하고 pod 에 연결해야한다.
생성은 아래와 같이 하면 된다.
```bash
$ kubectl create serviceaccount <NAME>
```
이제 `describe` 로 확인해 보면,
```
$ kubectl describe sa foo
Name: foo
Namespace: default
Labels: <none>
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: foo-token-hnx6x
Tokens: foo-token-hnx6x
Events: <none>
```
`Tokens: ...` 에 새로운 token이 생성된 것을 확인할 수 있고, 이는 Secret 으로 관리된다.
#### Mountable secrets
원래 pod 는 원하는 Secret 을 임의로 mount 할 수 있다. 하지만 pod 의 ServiceAccount 에서, 이 ServiceAccount 를 사용하는 pod 이 mount 할 수 있는 Secret 을 제한할 수 있다.
Mountable secrets 목록은 이 pod 가 mount 할 수 있는 Secret 의 목록이다. 다른 Secret 은 사용할 수 없다.
#### Image pull secrets
Image pull secret 은 private image repository 에서 image 를 pull 받을 때 사용하는 credential 이다. Mountable secrets 처럼 사용 가능한 secret 을 제한하는 것은 아니고, image pull secret 에 있는 secret 은 해당 ServiceAccount 를 사용하는 모든 pod 에 자동으로 mount 된다.
### 12.1.4 Assigning a ServiceAccount to a pod
Pod definition 에서 `.spec.serviceAccountName` 필드에 적어주면 된다. Pod 생성할 때 미리 정해야 하며, pod 가 생성된 뒤에는 수정할 수 없다.
## 12.2 Securing the cluster with role-based access control
---
Role-based access control (RBAC) plugin 을 사용하게 되면, unauthorized user 가 클러스터의 상태를 조회하거나 수정하는 것을 막을 수 있다. RBAC 를 사용하여 클러스터의 authorization 을 관리하는 방법을 살펴본다.
### 12.2.1 Introducing the RBAC authorization plugin
Kubernetes API server 는 authorization plugin 을 사용해서 요청을 보낸 주체가 작업(action)을 수행할 수 있는지 판단한다고 했다.
#### Understanding actions
작업의 종류에는 다음과 같은 것들이 있다. 기본적으로는 HTTP 요청을 사용하기 때문에 GET, POST, PUT, DELETE 등이 있고, 위 요청을 하면서 특정 리소스의 정보를 요구하게 된다. 예를 들면 pod 의 목록 가져오기 (GET), Service 생성하기 (POST), Secret 업데이트 (PUT) 등과 같은 요청이 있을 것이다.
이제 RBAC 와 같은 authorization plugin 을 사용하게 되면, 클라이언트가 리소스에 대한 요청을 할 수 있는지 없는지 판단하게 된다. (리소스의 종류와 작업의 종류 별도로 관리 가능)
추가로 RBAC 의 경우, 리소스의 특정 인스턴스 (특정 이름을 가진 service)에 대해서도 권한을 관리할 수 있게 된다.
#### Understanding the RBAC plugin
RBAC plugin 은 user 의 role 을 기반으로 authorization 을 하게 된다. User (사용자/ServiceAccount) 는 하나 이상의 role 이 부여되며, 각 role 마다 어떤 리소스에 어떤 작업을 할 수 있는지 정해져 있다.
만약 user 가 다수의 role 을 부여받았다면, role 에서 부여한 권한들의 합집합이 user 의 권한이 된다.
### 12.2.2 Introducing RBAC resources
RBAC authorization rules 은 4개의 리소스를 통해 설정된다. 크게 2가지 분류로 나뉜다.
- Role 과 ClusterRole: 어떤 리소스에 어떤 작업을 수행할 수 있는지 관리한다
- RoleBinding 과 ClusterRoleBinding: 위 role 을 적용할 user, group, ServiceAccounts 를 관리한다
> Roles 는 무엇(*what*)을 할 수 있는지 관리하는 것이고, bindings 는 누가(*who*)할 수 있는지 관리한다.
그리고 Cluster 가 붙은 리소스의 차이점은 Role/RoleBinding 는 namespace 가 존재하는 리소스를 관리하는 반면, ClusterRole/ClusterRoleBinding 의 경우 namespace 가 없는 클러스터 레벨의 리소스를 관리한다는 점이다.
#### Creating the namespaces and running to pods
Namespace 별로 동작이 달라지는 것을 확인하기 위해 namespace 를 만든다.
```bash
$ kubectl create ns foo
$ kubectl run test --image=luksa/kubectl-proxy -n foo
$ kubectl create ns bar
$ kubectl run test --image=luksa/kubectl-proxy -n bar
```
이제 터미널을 2개 열고 각 pod 안으로 들어간다.
```bash
$ kubectl exec -it test -n foo -- sh
$ kubectl exec -it test -n bar -- sh
```
#### Listing services from your pods
RBAC 가 동작하고 있음을 확인하기 위해 `curl` 을 사용해 `foo` namespace 의 Service 목록을 가져올 것이다.
```
$ curl localhost:8001/api/v1/namespaces/foo/services
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "services is forbidden: User \"system:serviceaccount:foo:default\" cannot list resource \"services\" in API group \"\" in the namespace \"foo\"",
"reason": "Forbidden",
"details": {
"kind": "services"
},
"code": 403
}
```
Default ServiceAccount 에는 아무 권한이 주어져 있지 않으므로 실패해야 한다. RBAC 가 잘 동작하고 있다.
### 12.2.3 Using Roles and RoleBindings
#### Creating Roles
이제 Role 을 생성해보자. Service 목록을 조회할 수 있도록 할 것이다.
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: foo # Role 은 namespaced resource 이다
name: service-reader
rules:
- apiGroups: [""] # Service 는 core apiGroup 에 있는 리소스이다
verbs: ["get", "list"] # 특정 리소스를 get 하는 요청, 전체를 list 하는 요청 허용
resources: ["services"] # 대상 리소스는 service 이고, 반드시 복수형으로 적어야 한다
```
> 리소스를 명시할 때 반드시 복수형으로 적어야 한다. 그리고 리소스의 특정 인스턴스에만 접근을 허용하고 싶을 경우 `resourceNames` field 를 적어주면 된다.
생성은 마찬가지로 YAML 파일을 POST 하면 되고, namespace 에 주의해야 한다.
```bash
$ kubectl create -f service-reader.yaml -n foo
```
Role 을 생성하는 또 다른 방법으로 CLI 에 직접 입력하는 방법이 있다. `bar` namespace 에는 CLI 로 생성한다.
```bash
$ kubectl create role service-reader --verb=get --verb=list \
--resource=services -n bar
```
#### Binding a role to a ServiceAccount
이제 이 Role 을 ServiceAccount 에 bind 해줘야 Role 이 실제로 적용된다. RoleBinding 리소스를 생성한다.
```bash
$ kubectl create rolebinding test --role=service-reader \
--serviceaccount=foo:default -n foo
```
> User 에게 bind 하려면 `--user`, group 에게 bind 하려면 `--group` 옵션을 주면 된다.
> RoleBinding 은 하나의 Role 만 reference 할 수 있다. 다만 Role 자체는 재사용이 가능하므로 여러 RoleBinding 을 생성할 수 있다.
이제 `foo``default` ServiceAccount 에 `service-reader` Role 이 bind 되었다. Service 의 목록을 다시 조회해 본다.
```
$ curl localhost:8001/api/v1/namespaces/foo/services
{
"kind": "ServiceList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "812515"
},
"items": []
}
```
조회가 되는 것을 확인할 수 있다.
#### Including ServiceAccounts from other namespaces in a RoleBinding
현재 namespace `bar` 에 생성한 pod 내에서는 Service 목록을 조회할 수 없다. RoleBinding 을 수정해보자.
```bash
$ kubectl edit rolebinding test -n foo
```
그리고 아래와 같이 수정한다.
```yaml
subjects:
- kind: ServiceAccount
name: default
namespace: foo
- kind: ServiceAccount
name: default
namespace: bar
```
그러면 이 RoleBinding 이 `bar` namespace 의 default ServiceAccount 에도 적용되고, `bar` 안의 pod 에서 `foo` namespace 의 Service 목록을 조회할 수 있다. 다만, 여전히 `bar` namespace 의 Service 목록은 조회가 불가능하다.
### 12.2.4 Using ClusterRoles and ClusterRoleBindings
ClusterRole 과 ClusterRoleBinding 을 사용하면 클러스터 레벨의 리소스에 대한 권한을 관리할 수 있게 된다.
Role 의 경우 Role 이 존재하고 있는 namespace 안의 리소스에 대한 권한만 부여할 수 있다. (위 예시에서 `bar` namespace 의 pod 가 `foo` namespace 의 Service 를 조회했지만, 이는 부여된 Role 이 `foo` namespace 에 속해 있기 때문) 그래서 만약 여러 namespace 에 접근해야 한다면, 각 namespace 마다 Role 과 RoleBinding 을 하나씩 전부 만들어줘야 한다.
추가로 Node, PV, Namespace 리소스의 경우 애초에 namespace 가 존재하지 않기도 하고, API server 의 endpoint 중 리소스를 나타내고 있지 않은 URL (`/healthz`) 도 있기 때문에 Role 만 사용해서는 위와 같은 정보를 API server 에 요청할 수 없게 된다.
ClusterRole 은 클러스터 레벨의 리소스로, namespace 가 없는 리소스나 리소스가 아닌 URL 에 대한 접근 권한을 관리할 수 있게 해준다. 또는 여러 namespace 에서 사용할 공통적인 권한을 정할 때도 사용한다.
#### Allowing access to cluster-level resources
우선 ClusterRole 을 생성해 본다. Namespace 가 없음에 유의한다.
```bash
$ kubectl create clusterrole pv-reader --verb=get,list \
--resource=persistentvolumes
```
ClusterRole 을 bind 하기 전에 권한이 있는지 확인해보자.
```
$ curl localhost:8001/api/v1/persistentvolumes
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "persistentvolumes is forbidden: User \"system:serviceaccount:foo:default\" cannot list resource \"persistentvolumes\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "persistentvolumes"
},
"code": 403
}
```
요청이 실패한다. 이제 ClusterRoleBinding 을 생성한다. 마찬가지로 namespace 가 없음에 유의한다.
> 클러스터 레벨의 리소스에 대해 권한을 관리하려면 반드시 ClusterRoleBinding 을 이용해야 한다.
```bash
$ kubectl create clusterrolebinding pv-test --clusterrole=pv-reader \
--serviceaccount=foo:default
```
이제 다시 요청해 보면 성공하는 것이 확인된다.
#### Allowing access to non-resource URLs
리소스를 reference 하지 않는 URL 에 대한 요청 권한도 직접 허용해야 한다. 대부분의 경우 `system:discovery` 라는 이름의 ClusterRole/ClusterRoleBinding 이 존재하여 이를 해결해준다.
다음은 `kubectl get clusterrole system:discovery -o yaml` 의 output 이다.
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
creationTimestamp: "2021-03-17T15:34:10Z"
labels:
kubernetes.io/bootstrapping: rbac-defaults
managedFields:
- apiVersion: rbac.authorization.k8s.io/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.: {}
f:rbac.authorization.kubernetes.io/autoupdate: {}
f:labels:
.: {}
f:kubernetes.io/bootstrapping: {}
f:rules: {}
manager: kube-apiserver
operation: Update
time: "2021-03-17T15:34:10Z"
name: system:discovery
resourceVersion: "76"
uid: a752c732-0544-455c-8c63-968ab2da0351
rules:
- nonResourceURLs: # 리소스를 참조하지 않는 URL 목록
- /api
- /api/*
- /apis
- /apis/*
- /healthz
- /livez
- /openapi
- /openapi/*
- /readyz
- /version
- /version/
verbs: # GET 만 허용된다
- get
```
마찬가지로 ClusterRoleBinding 도 확인해본다. `kubectl get clusterrolebinding system:discovery -o yaml` 의 output 이다.
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
creationTimestamp: "2021-03-17T15:34:10Z"
labels:
kubernetes.io/bootstrapping: rbac-defaults
managedFields:
- apiVersion: rbac.authorization.k8s.io/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.: {}
f:rbac.authorization.kubernetes.io/autoupdate: {}
f:labels:
.: {}
f:kubernetes.io/bootstrapping: {}
f:roleRef:
f:apiGroup: {}
f:kind: {}
f:name: {}
f:subjects: {}
manager: kube-apiserver
operation: Update
time: "2021-03-17T15:34:10Z"
name: system:discovery
resourceVersion: "138"
uid: 2942e213-29be-4ecb-8439-be3167c1177b
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:discovery
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:authenticated
```
마지막 `subjects` 를 보면 `system:authenticated` group 에 bind 되어있는 것을 확인할 수 있다. Authenticated user 라면 ClusterRole 의 적용을 받는다.
그래서 로컬 머신에서 `curl` 요청을 보내면 실패하게 된다.
```
$ curl https://$(minikube ip):8443/api -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
"reason": "Forbidden",
"details": {
},
"code": 403
}
```
#### Using ClusterRoles to grant access to resources in specific namespaces
ClusterRole 이 반드시 ClusterRoleBinding 과 연결된 필요는 없다. Namespace 가 존재하는 RoleBinding 과도 연결될 수 있다.
`view` 라는 ClusterRole 을 살펴본다. `kubectl get clusterrole view -o yaml` 중 일부이다.
```yaml
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- persistentvolumeclaims
- persistentvolumeclaims/status
- pods
- replicationcontrollers
- replicationcontrollers/scale
- serviceaccounts
- services
- services/status
verbs:
- get
- list
- watch
...
```
자세히 보면 ConfigMap, Endpoints, PVC, Pods 와 같이 namespace 된 리소스들이 적혀있다.
이러한 경우 ClusterRole 이 RoleBinding/ClusterRoleBinding 중 어느 쪽과 연결되느냐에 따라 권한이 달라진다. 만약 RoleBinding 에 연결하게 되면, RoleBinding 은 namespace 가 있으므로 RoleBinding 의 namespace 안에 있는 리소스에 권한이 부여된다. 반면 ClusterRoleBinding 을 사용하게 되면 임의의 namespace 안에 있는 리소스에 대해 권한이 부여된다.
#### Summary
| 접근 | Role Type | Binding Type |
|:----:|:---------:|:------------:|
|클러스터 레벨 리소스|`ClusterRole`|`ClusterRoleBinding`|
|리소스를 참조하지 않는 URL|`ClusterRole`|`ClusterRoleBinding`|
|임의의 namespace 에서 namespace 가 있는 리소스 접근|`ClusterRole`|`ClusterRoleBinding`|
|특정 namespace 에서 namespace 가 있는 리소스 접근 (ClusterRole 을 재사용하는 경우)|`ClusterRole`|`RoleBinding`|
|특정 namespace 에서 namespace 가 있는 리소스 접근 (각 namespace 에 Role 생성)|`Role`|`RoleBinding`|
### 12.2.5 Understanding default ClusterRoles and ClusterRoleBindings
Kubernetes 에는 기본적으로 설정되어 있는 ClusterRole 과 ClusterRoleBinding 이 있다. API server 가 시작될 때 매 번 업데이트 되기 때문에 실수로 삭제하는 경우 role 이 꼬이는 것을 방지하고, Kubernetes 가 업데이트 될 때 같이 업데이트 된다.
중요한 ClusterRole 몇 가지만 살펴본다.
#### Allowing read-only access to resources with `view` ClusterRole
이 ClusterRole 은 namespace 가 존재하는 대부분의 리소스를 조회 (view) 할 수 있다. 다만 Role, RoleBinding, Secret 은 제외되는데, 이는 privilege escalation 을 막기 위함이다. 어떤 secret 는 더 많은 권한을 가지고 있을 수도 있다.
#### Allowing modifying resources with `edit` ClusterRole
말 그대로 수정을 하게 해준다. 이 경우 Secret 도 접근하여 수정할 수 있지만, Role, RoleBinding 은 여전히 제외된다. 이 또한 privilege escalation 을 방지하기 위함이다.
#### Full control of a namespace with `admin` ClusterRole
Namespace 에 속한 모든 리소스를 읽고 변경할 수 있다. 다만 ResourceQuota 와 Namespace 자체는 건드릴 수 없다. 앞의 `edit` ClusterRole 과의 차이점은 Role, RoleBindings 를 읽고 쓸 수 있다는 점이다.
> Privilege escalation 을 막기 위해서 한 사용자가 Role 을 업데이트 하는 경우, 그 사용자가 업데이트 하려는 Role 에 정의된 모든 권한을 가지고 있는지 확인한다. 당연히 권한의 범위 (namespace) 도 동일해야 한다. 가지고 있어야만 수정할 수 있다.
#### Complete control with `cluster-admin` ClusterRole
모든 권한이 주어진다!
#### Other ClusterRoles
`system:` 으로 시작하는 ClusterRole 들은 Kubernetes 컴포넌트들이 사용한다. `kube-scheduler` (Scheduler 가 사용), `node` (Kubelet 이 사용), `controller:` 등등이 있다.
### 12.2.6 Granting authorization permissions wisely
- Principle of least privilege 만 기억하면 된다. 권한은 최소한으로 준다.
- 각 pod 마다 ServiceAccount 를 생성하는 것이 바람직하다.

View File

@@ -0,0 +1,497 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "13. Securing Cluster Nodes and the Network"
date: "2021-06-29"
github_title: "2021-06-29-13-securing-nodes-and-network"
image:
path: /assets/img/posts/k8s-13.jpeg
---
![k8s-13.jpeg](../../../assets/img/posts/k8s-13.jpeg) _A pod with hostNetwork: true uses the node's network interfaces instead of its own. (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-13)_
### 주요 내용
- 노드의 default Linux namespace 사용하기
- 컨테이너/Pod 단위로 권한 및 네트워크를 제어하여 보안 수준 높이기
컨테이너는 독립적인 환경을 제공한다고 하긴 했지만, 공격자가 API server 에 접근하게 되면 컨테이너에 무엇이든 집어넣고 악의적인 코드를 실행할 수 있고, 이는 실행 중인 다른 컨테이너에 영향을 줄 수도 있다!
## 13.1 Using the host node's namespaces in a pod
---
컨테이너는 별도의 linux namespace 에서 실행된다고 했었다.
### 13.1.1 Using the node's network namespace in a pod
시스템과 관련된 작업 (노드 레벨의 자원을 확인/수정하는 등) 을 하는 pod 의 경우 노드의 default namespace 에서 실행되어야 한다.
예를 들어, 별도의 네트워크 namespace 를 갖지 않고 (가상 네트워크 어댑터를 사용하지 않고), 호스트의 네트워크 어댑터를 사용하고 싶다면 `hostNetwork` 의 값을 `true` 로 해서 pod 를 실행하면 된다.
그러면 pod 는 노드의 네트워크 인터페이스에 접근할 수 있게 되고, pod 에는 별도의 IP 주소가 부여되지 않게 된다. Pod 내부에서 특정 포트에 bind 된 프로세스가 있다면, pod 의 포트가 곧 노드의 포트이므로 노트의 포트에 bind 되게 된다.
참고로 Kubernetes Control Plane 에 있는 컴포넌트들은 `hostNetwork` 옵션을 사용하여 pod 를 실행한다.
### 13.1.2 Binding to a host port without using the host's network namespace
위 경우에서는 노드의 네트워크 어댑터에 붙었지만, `hostPort` 값을 설정하면 노드의 특정 포트에 bind 하면서도 자신만의 네트워크 namespace 를 가질 수 있게 된다.
이렇게 했을 때 NodePort service 와의 차이점은, hostPort 의 경우 노드로 들어오는 요청을 직접 포워딩 해주는 반면, NodePort service 는 요청을 받아서 endpoint 중 임의의 (같은 노드가 아닐 수 있음) pod 로 포워딩 해준다는 점이다. 또 hostPort 의 경우 해당 노드에서만 포워딩이 일어나지만, NodePort service 의 경우 모든 노드에서 포워딩이 일어난다.
여러 프로세스가 하나의 포트에 bind 될 수 없기 때문에, Scheduler 도 이를 반영하여 scheduling 을 해준다. 만약 모든 노드의 포트가 사용 중이어서 bind 가 불가능하면 한 pod 는 pending 상태로 남아있게 된다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: kubia-hostport
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080 # 컨테이너의 8080 포트를
hostPort: 9000 # 노드의 9000번 포트와 bind
protocol: TCP
```
이 기능은 주로 시스템과 관련된 pod 를 expose 할 때 사용한다. (DaemonSet)
### 13.1.3 Using the node's PID and IPC namespaces
호스트의 네트워크 namespace 를 사용할 수 있었던 것처럼 `hostPID`, `hostIPC` 값을 `true` 로 설정해 주면 노드의 PID 와 IPC namespace 를 사용하게 된다. `spec` 아래에 넣어주면 된다.
## 13.2 Configuring the container's security context
---
`securityContext` property 를 이용하면 보안과 관련된 기능들을 pod 과 내부 컨테이너에 설정할 수 있다.
#### Security Context
Security context 를 설정하면 다양한 것들이 가능하다.
- 컨테이너 안의 프로세스가 어떤 user 로 실행할지 명시하기
- 컨테이너가 root 로 실행되는 것을 막기
- 컨테이너가 privileged mode 로 실행되도록 하기 (노드의 커널에 접근 가능)
- 권한을 상세하게 조정하기
- 프로세스가 컨테이너의 파일시스템에 write 하는 것을 막기
#### Running a pod without specifying a security context
Security context 를 기본값으로 하고 pod 를 실행해본다.
```bash
$ kubectl run pod-with-defaults --image alpine --restart Never -- /bin/sleep 999999
```
이제 컨테이너가 실행 중인 user 와 group 을 살펴보면,
```
$ kubectl exec pod-with-defaults -- id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
```
모두 root 로 실행 중인 것을 확인할 수 있다.
### 13.2.1 Running a container as a specific user
다른 user 로 pod 를 실행하려면, `securityContext.runAsUser` property 값을 설정하면 된다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-as-user-guest
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 405 # guest
```
Pod 를 생성한 뒤 `id` 명령을 실행해 보면 `guest` user 로 실행된 것을 확인할 수 있다.
```
$ kubectl exec pod-as-user-guest -- id
uid=405(guest) gid=100(users)
```
### 13.2.2 Preventing a container from running as root
root 가 아닌 임의의 사용자로 실행되더라도 무관하다면, root 로 실행하지 못하게 막을 수 있다.
> Scheduler 가 새롭게 pod 를 띄울 때는 registry 에서 image 를 pull 받을 것이다. 만약 공격자가 image registry 에 접근 권한을 얻어서 같은 tag 를 가졌지만 root 로 실행하는 image 를 push 하게 되면 악의적인 목적을 가진 컨테이너가 그대로 실행될 위험이 있다.
컨테이너는 물론 호스트의 시스템과 분리되어 있지만, 프로세스를 root 권한으로 실행하는 것은 권장되지 않는다. 대표적으로 폴더를 mount 하는 경우, root 로 실행하게 되면 모든 권한을 다 갖게 된다.
root 로 실행을 막기 위해서는 `securityContext.runAsNonRoot``true` 로 설정하면 된다.
### 13.2.3 Running pods in privileged mode
어떤 경우에는 pod 가 모든 권한을 부여받아야 할 때도 있다. 예를 들어 kube-proxy pod 의 경우 노드의 `iptables` 를 변경해야 service 를 동작시킬 수 있게 된다.
이 경우 `securityContext.privileged` 의 값을 `true` 로 설정하면 된다. 그러면 노드의 커널에 모든 접근 권한을 갖게 된다.
### 13.2.4 Adding individual kernel capabilities to a container
당연히, 모든 권한을 주는 것 보다는 필요한 권한만 주는 것이 훨씬 안전할 것이다. Linux 에서는 kernel *capability* 로 권한을 관리한다.
예를 들어, 컨테이너에서는 보통 시간을 설정할 수 없다.
```
$ kubectl exec -it pod-with-defaults -- date +%T -s "12:00:00"
date: can't set date: Operation not permitted
```
만약 이 권한을 주고 싶다면 `SYS_TIME` 을 설정해 주면 된다. `securityContext.capabilities.add` 아래에 추가해준다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-add-settime-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
capabilities:
add: # 권한을 추가한다
- SYS_TIME
```
> 시간이 변경 가능한지 확인하고 싶었으나, minikube 내부에서 NTP daemon 이 시간을 원래대로 돌려줬다. 컨테이너 내의 `sh` 에서 직접 `date +%T -s "12:00:00"; date` 를 하고 나니 변경된 것을 확인할 수 있었다.
### 13.2.5 Dropping capabilities from a container
만약 특정 권한을 빼앗고 싶다면 `securityContext.capabilities.drop` 아래에 추가해주면 된다.
### 13.2.6 Preventing processes from writing to the container's filesystem
보안상 프로세스가 컨테이너의 파일시스템보다는 mounted volume 에 write 하도록 하는 것이 좋다. 파일시스템을 read only 로 설정하려면 `securityContext.readOnlyRootFilesystem``true` 로 설정하면 된다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-readonly-filesystem
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
readOnlyRootFilesystem: true # write 는 불가능
volumeMounts:
- name: my-volume
mountPath: /volume
readOnly: false # volume 에는 write 가능
volumes:
- name: my-volume
emptyDir:
```
Pod 를 생성해보면, root 로 실행되었음에도 `/` 디렉터리에 write 가 안된다.
#### Setting options at the pod level
지금까지는 각 컨테이너마다 security context 를 지정했지만, pod 레벨에서도 `pod.spec.securityContext` property 를 이용해 설정할 수 있다. 모든 컨테이너가 적용 대상이 되고, 컨테이너 레벨에서 또 설정하게 되면 overriding 할 수 있다.
또한 pod 레벨에서는 추가로 사용할 수 있는 보안 기능이 있다.
### 13.2.7 Sharing volumes when containers run as different users
한 pod 내에서 volume 을 사용하게 되면 컨테이너 간에 데이터를 공유할 수 있다고 했다. 이게 가능했던 이유는 컨테이너가 모두 root 로 실행되어 읽기/쓰기 권한을 모두 갖고 있었기 때문이다. 한편 `runAsUser` 옵션을 사용하게 되면 volume 을 사용했을 때 둘 다 읽기/쓰기 권한이 없을 수도 있다.
Kubernetes 에서는 supplemental groups 를 제공하여 데이터 공유를 가능하게 해준다. `fsGroup`, `supplementalGroups` 옵션을 사용하면 된다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-shared-volume-fsgroup
spec:
securityContext: # 이 두 옵션은 pod 레벨에서 정의된다
fsGroup: 555
supplementalGroups: [666, 777]
containers:
- name: first
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 1111 # 첫 번째 컨테이너는 user ID 1111
volumeMounts:
- name: shared-volume
mountPath: /volume
readOnly: false
- name: second
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 2222 # 두 번째 컨테이너는 user ID 2222
volumeMounts:
- name: shared-volume
mountPath: /volume
readOnly: false
volumes:
- name: shared-volume
emptyDir:
```
Pod 를 생성하고 `id` 명령을 실행해본다.
```
$ id
uid=1111 gid=0(root) groups=555,666,777
```
user ID 는 `1111` 이고, group ID 는 `0` (root) 이지만 `555,666,777` 도 이 사용자와 엮여있는 것을 확인할 수 있다. `fsGroup``555` 로 설정했으므로, mount 된 volume 을 소유하고 있는 group ID 는 `555` 이다.
```
$ ls -l / | grep volume
drwxrwsrwx 2 root 555 4096 Jun 13 14:59 volume
```
Volume 안에 들어가서 파일을 생성하면 파일을 소유하고 있는 user ID 는 `1111` 이고 group ID 는 `555` 가 된다.
```
$ echo foo > /volume/foo
$ ls -l /volume
total 4
-rw-r--r-- 1 1111 555 4 Jun 13 15:03 foo
```
보통 사용자가 파일을 만들게 되면 effective group ID 로 설정되는데, `fsGroup` 옵션을 이용하게 되면 volume 안에 파일을 만들 때 설정할 group ID 를 지정할 수 있다.
> `supplementalGroups` 에 대한 설명이 좀 부족하다. 단순히 user 와 엮인 추가 group ID 를 설정할 수 있다고만 적혀있다.
## 13.3 Restricting the use of security-related features in pods
---
클러스터 관리자는 PodSecurityPolicy 리소스를 이용해서 pod 의 보안과 관련된 기능들을 제한할 수 있다.
### 13.3.1 Introducing the PodSecurityPolicy resource
PodSecurityPolicy 리소스는 클러스터 레벨의 리소스로, 사용자들이 pod 를 생성할 때 사용할 수 있는 보안 관련 기능을 정의하기 위해 사용한다. PodSecurityPolicy 안의 규칙(policy)은 API server 에서 실행 중인 PodSecurityPolicy admission control plugin 에서 관리된다.
사용자가 pod 생성을 요청하게 되면, PodSecurityPolicy admission control plugin 이 pod 의 정의를 보고 validation 을 해준다. 만약 pod 의 정의가 PodSecurityPolicy 에 부합하면, etcd 에 저장되고, 그렇지 않으면 생성 요청이 거절된다. 추가로, 해당 플러그인이 직접 pod 리소스 정보를 변경할 수도 있다. (기본값 세팅 등)
#### Understanding what a PodSecurityPolicy can do
다음과 같은 작업을 제어할 수 있다.
- Pod 의 호스트 IPC/PID/네트워크 namespace 를 사용 제어
- Pod 가 bind 할 수 있는 호스트의 포트 제한
- 컨테이너를 실행할 user ID 제한
- Privileged 컨테이너 실행 가능 여부
- 커널 관련 작업 제어
- 컨테이너의 root 파일시스템 쓰기 제어
- 컨테이너를 실행할 파일시스템 group 제한
- Pod 가 사용할 수 있는 volume 종류 제한
앞에서 소개한 내용과 거의 비슷하다.
#### Examining a sample PodSecurityPolicy
```yaml
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
hostIPC: false
hostPID: false
hostNetwork: false # 호스트의 IPC, PID, 네트워크 namespace 사용 불가
hostPorts:
- min: 10000
max: 11000 # bind 가능한 포트는 10000~11000
- min: 13000
max: 14000 # 13000~14000 포트도 허용
privileged: false # privileged 컨테이너 실행 불가능
readOnlyRootFilesystem: true # root 파일시스템은 읽기 전용
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
seLinux:
rule: RunAsAny # 실행할 user 와 group 은 제한 없음
volumes:
- '*' # 모든 종류의 volume 사용 가능
```
### 13.3.2 Understanding `runAsUser`, `fsGroup` and `supplementalGroups` policies
앞 예제에서는 `RunAsAny` 를 사용했기 때문에 제약 조건이 없었지만, 제한하고 싶다면 `MustRunAs` 를 이용해서 ID 의 범위를 제한할 수 있다.
```yaml
runAsUser:
rule: MustRunAs
ranges:
- min: 2
max: 2
```
> 참고로 PodSecurityPolicy 리소스를 업데이트 하더라도 기존에 생성된 pod 에는 영향을 주지 않는다. Pod 를 생성하거나 수정할 때만 플러그인이 확인한다.
또한 root user 로의 실행을 막고 싶을 때는 `MustRunAsNonRoot` 를 사용하면 된다.
### 13.3.3 Configuring allowed, default, and disallowed capabilities
Linux 커널과 관련된 권한을 통제하고 싶을 때 capabilities 를 조작하면 됐었다. PodSecurityPolicy 에서는 `allowedCapabilities`, `defaultAddCapabilities`, `requiredDropCapabilities` 를 이용해 권한을 통제한다.
```yaml
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
allowedCapabilities:
- SYS_TIME
defaultAddCapabilities:
- CHOWN
requiredDropCapabilities:
- SYS_ADMIN
- SYS_MODULE
```
`allowedCapabilities` 를 사용하면 pod 의 `securityContext.capabilities` 에 어떤 값들이 포함될 수 있는지 제한할 수 있게 된다.
`defaultAddCapabilities` 를 사용하면 pod 에 해당 capability 가 자동으로 추가된다.
`requiredDropCapabilities` 를 사용하면 pod 가 어떤 capability 를 가지지 않아야 하는지 제한할 수 있다. (해당 capability 를 drop 하는 것을 require 하는 것이다)
### 13.3.4 Constraining the types of volumes pods can use
최소한 `emptyDir`, `configMap`, `secret`, `downwardAPI`, `persistentVolumeClaim` 은 사용할 수 있게 해줘야 한다.
PodSecurityPolicy 가 여러 개 있으면, 각각에서 허용한 volume 종류의 합집합이 사용 가능한 volume 종류가 된다.
### 13.3.5 Assigning different PodSecurityPolicies to different users and groups
PodSecurityPolicy 를 만들었는데 해당 policy 가 전역에 영향을 준다면 이를 사용하기 어려울 것이다. 그러므로 RBAC 를 이용해 사용자마다 어떤 policy 가 할당되어 적용되는지 관리할 수 있다.
방법은 간단하다. PodSecurityPolicy 를 필요한 만큼 만들어 두고, ClusterRole 을 만들어 PodSecurityPolicy 를 reference 하도록 하는 것이다. 이제 ClusterRoleBinding 을 이용해 사용자나 group 에게 ClusterRole 을 bind 하면 적용된다.
```bash
$ kubectl create clusterrole <CLUSTER_ROLE_NAME> --verb=use \
--resource=podsecuritypolicies --resource-name=<POD_SECURITY_POLICY_NAME>
$ kubectl create clusterrolebinding <CLUSTER_ROLE_BINDING_NAME> \
--clusterrole=<CLUSTER_ROLE_NAME> --group=<GROUP_NAME>
```
> `kubectl` 에서 사용자를 추가하려면 `kubectl config set-credentials <NAME> --username=<USERNAME> --password=<PASSWORD>` 를 입력하면 된다.
> 다른 사용자의 이름으로 리소스를 생성하려면 `kubectl --user <USERNAME> create` 를 하면 된다.
## 13.4 Isolating the pod network
---
앞서 살펴본 방법들은 pod 와 컨테이너 단에서 적용되는 보안 관련 설정을 살펴봤다. 이번에는 pod 사이의 네트워크 통신 측면에서 보안을 적용하는 방법을 알아본다.
네트워크 보안을 설정하기 위해서는 클러스터에서 사용하는 networking plugin 이 이를 지원해야한다. 만약 지원한다면, NetworkPolicy 리소스를 생성하여 네트워크를 분리시킬 수 있다.
NetworkPolicy 리소스를 사용하게 되면 ingress 와 egress 규칙을 설정할 수 있어 어떤 source 에서만 트래픽을 받을지, 어떤 destination 으로만 트래픽을 보낼지 제한할 수 있다.
### 13.4.1 Enabling network isolation in a namespace
원래 한 namespace 안의 pod 로는 아무나 접근할 수 있으므로, 이것부터 변경해야 한다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: # 모든 pod 가 match 된다
```
이제 NetworkPolicy 를 특정 namespace 에 생성하면 그 누구도 pod 에 접근할 수 없게 된다.
### 13.4.2 Allowing only some pods in the namespace to connect to a server pod
클라이언트의 연결을 허용하려면 어떤 pod 가 연결할 수 있는지 명시적으로(explicitly) 적어야 한다.
예를 들어 DB 를 갖고있는 pod 가 실행 중인데, 이를 사용하는 웹 서버 이외의 접근은 막으려고 한다. 이런 경우 NetworkPolicy 에서 ingress 규칙을 설정하면 된다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-netpolicy
spec:
podSelector: # app=database label 이 있는 pod 에 적용되는 규칙
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels: # app=webserver label 이 있는 pod 로부터 들어오는 트래픽을 허용
app: webserver
ports:
- port: 5432 # 개방된 포트
```
이렇게 설정하면 `app=webserver` label 을 가진 pod 이외에는 DB pod 에 접속이 불가능하며, 심지어 웹 서버 조차도 5432 포트 외의 포트에는 접속할 수 없게 된다.
> 실제로는 pod 에 직접 접속하지 않고 service 를 거칠 것이다. 이 경우에도 NetworkPolicy 의 적용을 받게 된다.
### 13.4.3 Isolating the network between Kubernetes namespaces
만약 다양한 namespace 로부터 트래픽을 받고 싶다면 namespace 에 label 을 붙여서 사용할 수도 있다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: shoppingcart-netpolicy
spec:
podSelector:
matchLabels:
app: shopping-cart
ingress:
- from:
- namespaceSelector: # tenant=manning 을 가진 namespace 에서 오는 트래픽을 허용
matchLabels:
tenant: manning
ports:
- port: 80
```
### 13.4.4 Isolating using CIDR notation
Label selector 를 사용하는 대신 CIDR block 으로 제어할 수도 있다.
```yaml
ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24 # 해당 block 의 트래픽만 허용
```
### 13.4.5 Limiting the outbound traffic of a set of pods
앞에서는 들어오는 트래픽 (inbound/ingress) 에 대한 제한이었지만, 나가는 트래픽 (outbound/egress) 도 제어할 수 있다.
```yaml
spec:
podSelector:
matchLabels:
app: webserver
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 5432
```
위와 같이 하면 `app=webserver` label 을 가진 pod 는 `app=database` 의 5432 포트로만 요청을 보낼 수 있게 된다.

View File

@@ -0,0 +1,459 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "14. Managing Pods' Computational Resources"
date: "2021-07-11"
github_title: "2021-07-11-14-managing-computation-resources"
image:
path: /assets/img/posts/k8s-14.jpeg
---
![k8s-14.jpeg](../../../assets/img/posts/k8s-14.jpeg) _The Scheduler only cares about requests, not actual usage. (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-14)_
### 주요 내용
- Pod requests/limits 설정, 동작 방식 및 scheduling 에 미치는 영향
- Pod requests/limits 과 QoS class
- LimitRange resource, ResourceQuota object
각 pod 가 어느 정도의 자원(CPU/메모리)을 소모할지 파악하고, 이를 적절히 제한하는 것은 pod 정의에서 굉장히 중요한 부분이다.
## 14.1 Requesting resources for a pod's containers
---
Pod 를 생성할 때 **requests****limits** 를 정할 수 있다.
- requests: 컨테이너가 필요로 하는 CPU와 메모리 양을 설정
- limits: 컨테이너가 최대로 사용할 수 있는 CPU와 메모리를 설정
이는 각 컨테이너 별로 설정할 수 있으며, pod 의 requests/limits 는 각 컨테이너들의 requests/limits 의 합이 된다.
### 14.1.1 Creating pods with resource requests
리소스 requests 를 가진 pod 를 생성하는 것은 무척 간단하다.
```yaml
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 millicore
memory: 10Mi # 메모리 10 MiB
```
> 200 millicore 라는 것은 한 CPU 코어의 1/5를 사용한다는 것이고 이는 곧 CPU 타임의 1/5 만 사용한다는 의미이다.
만약 requests 를 정하지 않으면 이 컨테이너가 자원을 임의로 할당받아도 무관하다는 뜻이 되기 때문에, 최악의 경우에는 자원을 사용하지 못하게 될 수도 있음에 유의해야 한다.
Pod 를 실행하고 `top` 을 쳐보면 자원을 얼마나 소비하고 있는지 확인할 수 있다.
```
Mem: 10819360K used, 5510064K free, 211100K shrd, 173304K buff, 5530868K cached
CPU: 4.0% usr 9.6% sys 0.0% nic 86.1% idle 0.1% io 0.0% irq 0.0% sirq
Load average: 1.21 1.08 0.79 3/2346 46
PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
1 0 root R 1316 0.0 5 12.4 dd if /dev/zero of /dev/null
41 0 root R 1324 0.0 1 0.0 top
```
막상 확인해 보면 CPU는 12.5%를 사용하고 있는데, 이는 현재 실행 환경의 CPU 코어 수가 8개여서 그런 것이고, `top` 을 실행한 뒤 키보드 `1` 을 눌러보면 각 코어별 사용률을 볼 수 있다.
```
CPU5: 33.3% usr 66.6% sys 0.0% nic 0.0% idle 0.0% io 0.0% irq 0.0% sirq
```
한편 pod 정의에서는 200 millicore 로 설정했었는데, 정작 확인해보니 한 코어를 full 로 사용하고 있는 것을 확인할 수 있었다.
이처럼 requests 만 정의하는 경우에는 제한이 없으므로 컨테이너는 원하는 만큼 자원을 사용할 수 있게 된다.
### 14.1.2 Understanding how resource requests affect scheduling
즉, requests 를 설정하게 되면 **pod 가 요구하는 최소 자원**을 설정하는 셈이 된다. 그렇기 때문에 이는 pod scheduling 에도 영향을 주게 된다. 최소 자원을 만족시키지 못하는 노드에 pod 를 schedule 해서는 안 되기 때문이다.
Scheduling 에서 유의할 점은 Scheduler 가 pod 를 띄울 노드를 선택할 때, scheduling 당시에 소비되고 있는 자원들의 상태를 확인하는 것이 아니라, **노드에 존재하는 pod 들의 자원 requests 총합**을 확인한다는 점이다.
> 이렇게 해야 각 pod 의 requests 를 모두 만족시킬 수 있기 때문이다. Scheduling 당시에 자원이 적게 소모된다고 해서 괜찮다고 생각하고 scheduling 해버리면 나중에 자원이 부족해진다.
Scheduler 가 pod 의 requests 를 만족하는 노드를 찾았다면, 이제 실제로 할당을 하면 되는데, 이 때 조건에 맞는 노드가 여러 개 일수도 있다. 이 경우 우선순위를 두는 방법이 2가지 있다.
- `LeastRequestedPriority`: 여유 자원이 많은 노드를 선호하는 방법
- `MostRequestedPriority`: 여유 자원이 적은 노드를 선호하는 방법
> `MostRequestedPriority` 의 경우 클라우드에서 사용하는 노드 수를 줄이기 위해 사용할 수 있다. 물론 여러 노드를 띄워서 각 노드가 여유롭게 작업하도록 하면 좋겠지만, 비용을 절감하기 위해 노드의 자원을 최대한 전부 사용하도록 선택하는 경우도 있다.
#### 노드의 자원 양 확인하는 방법
`kubectl describe nodes` 를 하면 중간에 `Capacity`, `Allocatable` 부분이 있다.
```
Capacity:
cpu: 8
ephemeral-storage: 102686648Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 16329424Ki
pods: 110
Allocatable:
cpu: 8
ephemeral-storage: 102686648Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 16329424Ki
pods: 110
```
참고로 `Non-terminated Pods` 부분을 보면 `CPU Requests` column 이 있는데, 직접 생성한 pod 이외에도 `kube-system` 과 관련된 pod 들이 실행 중이며, 이 pod 들도 requests 가 있다는 점을 기억해 두어야 한다. 시스템 pod 들이 일부 리소스를 사용 중이므로 사용자가 생성한 컨테이너 만으로 CPU 전체를 쓸 수는 없다.
### 14.1.3 Understanding how CPU requests affect CPU time sharing
앞에서 requests 만 설정하고 limits 를 두지 않았기 때문에 CPU 사용에 제한이 없다고 했는데, 만약 여러 개 pod 를 띄우게 되면 여분의 CPU time 은 각 pod 의 request 양의 비율에 맞게 배분된다.
예를 들어 2개의 pod 이 각각 200m, 1000m 의 CPU 를 요청했다면, 남는 CPU 는 1:5 로 비례배분되어 각 pod 이 사용하게 된다.
하지만 항상 이렇게 되는 것은 아니고, 한 pod 가 idle 상태일 때 다른 pod 에서 CPU 를 더 많이 가져가서 쓰려고 하면 사용할 수 있으며, idle 상태에서 벗어나 CPU 를 요청하게 되면 더 많이 쓰던 pod 에는 throttling 이 걸리게 된다.
### 14.1.4 Defining and requesting custom resources
Kubernetes 에서는 사용자 지정 resource 를 정의해서 requests 에 포함할 수 있다. (Extended Resources since version 1.8)
## 14.2 Limiting resources available to a container
---
이번에는 자원의 최대 사용량을 제한해 본다.
### 14.2.1 Setting a hard limit for the amount of resources a container can use
CPU는 compressible 이므로, pod 들의 사용량 총합이 100% 를 넘어가도 throttling 이 걸려서 계산이 느려지지만 돌아가기는 한다. 반면 메모리는 incompressible 이기 때문에 절대적인 양이 부족하면 할당이 되지 않고 프로세스가 실행되지 않는다.
특히 메모리의 경우 limit 을 걸지 않으면 한 pod 가 메모리를 지나치게 사용하여 다른 pod 에게 영향을 줄 수 있다. Scheduler 는 실제 사용량이 아니라 requests 만 보기 때문에 실제 사용량이 requests 보다 많을 경우 schedule 은 됐지만 pod 이 시작되지 않을 수 있다.
#### Limits 를 이용해서 pod 생성하기
requests 때와 유사하다.
```yaml
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
memory: 20Mi
```
> 참고로 requests 를 명시하지 않으면 limits 와 동일한 값으로 설정된다.
#### Limits 가 노드 capacity 의 100% 를 초과하는 경우
requests 의 경우 노드 allocatable 양보다 무조건 같거나 작아야 하지만, limits 의 경우 별도의 제약이 없다. 노드 capacity 의 100% 를 초과할 수 있다.
단, 이 경우 자원이 모두 사용되었기 때문에 pod/container 가 kill 될 수 있다.
### 14.2.2 Exceeding the limits
만약 컨테이너가 limits 값보다 더 많은 리소스를 사용하려고 하는 경우,
- CPU의 경우 throttling 이 걸리게 된다.
- 메모리의 경우 `OOMKilled` 상태가 되고 프로세스가 kill 된다.
만약 restart policy 가 `Always`/`OnFailure` 이면 바로 재시작되기는 하는데, 또 메모리 초과로 kill 되며 이 상황이 반복적으로 나타나는 경우 restart 를 지연하게 되며 `CrashLoopBackOff` 상태로 들어가게 된다.
각 restart 마다 delay 를 2배씩 증가시켜 최대 300초까지 늘리며, 이후로부터는 300초마다 재시작을 시도한다.
`OOMKilled` 상태는 `kubectl describe pod` 에서 볼 수 있다.
따라서 메모리 limit 은 신중하게 설정해야 한다.
### 14.2.3 Understanding how apps in containers see limits
주의할 점은 컨테이너는 limit 값을 모른다는 점이다! Limit 을 설정했지만, 컨테이너 안의 프로세스가 메모리를 확인할 때 자신이 실행 중인 노드의 전체 메모리를 보게 된다.
그래서 만약 애플리케이션이 메모리를 확인하고 여유 메모리 값에 따라 메모리를 할당하게 되면 limit 과 관계없이 더 많은 양의 메모리를 할당하려고 할 수 있다.
> JVM `-Xmx` 옵션을 사용하지 않으면 JVM 은 호스트(노드)의 메모리를 고려하여 heap size 를 잡기 때문에 프로덕션 환경에서 heap size 가 크게 잡혀 `OOMKilled` 가 발생할 수 있다.
마찬가지로 CPU 도 노드의 모든 CPU를 보게 된다. 단지 CPU limit 을 걸면 사용 시간 제한이 걸리는 것이다.
CPU도 문제가 되는 경우가 있는데, 코어 수를 참고하여 worker thread 를 생성하는 경우, CPU limit 의 제한을 받아 threading 의 효과를 전혀 못 받게 될 수도 있다.
이러한 경우 Kubernetes Downward API 를 이용해서 CPU limit 값을 애플리케이션에 넘겨주는 식으로 해결할 수 있다.
## 14.3 Understanding pod QoS classes
---
위에서 limits 의 경우 100% 를 초과할 수 있다고 했는데, 초과하면 어떤 컨테이너나 pod 를 kill 해야 한다고 했다. 어떤 pod 가 kill 되는지는 내부적으로 정해져 있다.
Kubernetes 에서는 pod 를 3개의 QoS 클래스로 나눠서 관리하고 이에 따라 kill 우선순위가 정해진다.
### 14.3.1 Defining the QoS class for a pod
QoS 클래스는 따로 pod 정의에 설정하는 것이 아니고, requests/limits 값으로부터 자동으로 도출된다.
> QoS Class 는 `kubectl describe pod` 에서도 확인할 수 있다.
#### `BestEffort` class
- **requests/limits 가 설정되지 않은 컨테이너가 하나라도 존재**하는 경우 pod 에게 부여된다.
- Pod 가 사용 가능한 리소스에 대해 어떠한 보장도 되지 않는다. Starvation 이 일어날 수도 있으며, 노드 capacity 를 초과하는 경우 먼저 kill 된다.
#### `Guaranteed` class
- **모든 컨테이너의 requests 와 limits 값이 일치**하는 pod 에게 부여된다.
- CPU/메모리 모두 requests/limits 가 모든 컨테이너에 부여되어야 하며 그 값이 같아야 한다.
- 이 pod 내의 컨테이너는 request 한 만큼 자원을 받지만 limits 때문에 그 이상 받지는 못한다.
#### `Burstable` class
`BestEffort`, `Guaranteed` 가 아닌 pod 는 전부 이 클래스에 속한다.
#### kill 우선순위
`BestEffort` > `Burstable` > `Guaranteed` 순으로 kill 된다.
> `BestEffort` 가 먼저 kill 되는 이유는 requests/limits 를 explicit 하게 set 한 것에 대한 존중(?)이라고 생각할 수 있다.
### 14.3.2 Understanding which process gets killed when memory is low
QoS 클래스 순서대로 kill 이 일어나지만, QoS 클래스가 같은 pod 가 여러 개 있으면 어떤 pod 를 kill 할지 결정해야 한다.
이 경우 OutOfMemory (OOM) score 를 계산한다. 시스템에서는 OOM score 가 가장 높은 프로세스를 죽인다.
OOM score 의 계산에는 2가지 요인이 들어간다.
- 프로세스가 잡고 있는 메모리 중 사용 가능한 메모리의 비율
- Fixed OOM score adjustment
잡은 메모리 중 사용 비율이 높을수록 먼저 kill 된다.
## 14.4 Setting default requests and limits for pods per namespace
---
requests/limits 를 설정하지 않으면 kill 의 대상이 될 수 있으므로 설정하는 것이 좋다. 각 컨테이너에 이를 설정하는 것은 번거로우므로, Kubernetes 에서는 LimitRange 리소스를 제공한다.
### 14.4.1 Introducing the LimitRange resource
각 namespace 별로 자원의 최솟값/최댓값을 설정할 수 있으며, requests 를 설정하지 않은 컨테이너에게는 기본값을 제공해준다.
LimitRange 리소스는 LimitRanger Admission Control plugin 에서 사용하는데, pod 이 새롭게 생성될 때 pod manifest validation 과정에서 사용된다. 주로 지나치게 큰 자원의 생성을 막기 위해 사용한다.
또 기억할 점은 LimitRange 는 각 pod 에 적용되는 것이기 때문에 pod 전체에서 사용하는 리소스의 합을 제한하지는 않는다.
> Pod 전체에서 사용하는 리소스 제한은 ResourceQuota 로 한다.
### 14.4.2 Creating a LimitRange object
```yaml
apiVersion: v1
kind: LimitRange
metadata:
name: example
spec:
limits:
- type: Pod # Pod 전체에 걸리는 range
min:
cpu: 50m
memory: 5Mi
max:
cpu: 1
memory: 1Gi
- type: Container # 컨테이너에 걸리는 range
defaultRequest: # requests 기본값
cpu: 100m
memory: 10Mi
default: # limits 기본값
cpu: 200m
memory: 100Mi
min:
cpu: 50m
memory: 5Mi
max:
cpu: 1
memory: 1Gi
maxLimitRequestRatio: # limit / request (비율)의 범위
cpu: 4
memory: 10
- type: PersistentVolumeClaim # PVC 용량 제한
min:
storage: 1Gi
max:
storage: 10Gi
```
### 14.4.3 Enforcing the limits
LimitRange 리소스를 생성한 뒤 range 를 벗어난 pod 를 생성하려고 하면 reject 된다.
### 14.4.4 Applying default resource requests and limits
LimitRange 는 namespaced resource 이므로 한 namespace 에만 적용된다. 따라서 namespace 별로 LimitRange 를 만들어 두면 제한을 다르게 할 수 있다.
## 14.5 Limiting the total resources available in a namespace
---
LimitRange 는 pod 전체의 리소스 총합을 제한하지는 못한다. 하지만 클러스터 관리자 입장에서는 namespace 별로 리소스 총량을 제한할 필요가 있기 때문에, Kubernetes 에서는 ResourceQuota object 가 제공된다.
### 14.5.1 Introducing the ResourceQuota object
ResourceQuota object 를 생성해 두면, pod 이 생성되었을 때 최대 사용 가능한 자원 양을 초과하는지 ResourceQuota Admission Control plugin 이 확인한다.
ResourceQuota 에서는 CPU/메모리 뿐만 아니라 namespace 내의 각종 Kubernetes resource 의 개수를 제한할 수도 있다.
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: cpu-and-mem
spec:
hard:
requests.cpu: 400m
requests.memory: 200Mi
limits.cpu: 600m
limits.memory: 500Mi
```
즉 LimitRange 는 각 pod 의 자원을 제한하지만, ResourceQuota 는 namespace 전체의 pod 자원 총합을 제한한다.
ResourceQuota 를 생성한 뒤에는 `kubectl describe` 로 얼마나 사용 중이고, 제한이 얼마인지 자세한 정보를 확인할 수 있다.
> ResourceQuota 를 생성하게 되면 모든 pod 에 CPU/메모리 requests/limits 가 명시적으로 설정되어 있어야 한다. 이를 위해 LimitRange 를 만들어 기본값을 주도록 설정 해두는 것이 좋다.
### 14.5.2 Specifiying a quota for persistent storage
```yaml
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
```
PVC 용량을 제한할 수도 있다. 또한 dynamic provisioning 의 경우 storage class 를 사용하는데, 각 class 마다 quota 를 제한할 수도 있다.
### 14.5.3 Limiting the number of objects that can be created
오브젝트 개수를 제한할 수도 있다.
```yaml
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
```
### 14.5.4 Specifiying quotas for specific pod states and/or QoS classes
Quota 의 경우 namespace 내의 모든 pod 에 적용됐지만, **quota scope** 를 이용해 적용 대상을 더욱 좁힐 수 있다. 현재 4가지의 scope 가 존재한다.
`BestEffort`, `NotBestEffort` scope 의 경우 QoS 가 `BestEffort` 인지 아닌지 확인하는 것이다.
`Terminating`, `NotTerminating` scope 의 경우 `activeDeadlineSeconds` 필드가 정의되어 있는지 아닌지에 따라 달라진다. (Pod 가 terminating 인지와는 무관하다)
> `activeDeadlineSeconds` 필드는 pod 가 terminate 되고 `Failed` 로 마킹된 이후 얼마나 더 실행해도 괜찮은지 설정하는 값이다.
Scope 를 정의한 뒤 ResourceQuota 를 만들면 해당 scope 에 해당되는 pod 만 적용된다.
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: besteffort-notterminating-pods
spec:
scopes:
- BestEffort
- NotTerminating
hard:
pods: 4
```
위 ResourceQuota 의 경우 `BestEffort` QoS 이면서 `activeDeadlineSeconds` 필드가 세팅 되지 않은 pod 에만 적용된다.
`BestEffort` 의 경우 pod 의 개수만 제한할 수 있다. (애초에 requests/limits 가 세팅되지 않음) 나머지 3개 클래스의 경우 pod 개수 뿐만 아니라 CPU/메모리 requests/limits 모두 제한할 수 있다.
## 14.6 Monitoring pod resource usage
---
requests/limits 를 적절하게 설정하려면 pod 에서 자원이 얼마나 사용되고 있는지 확인해야 하고 이를 모니터링해야 한다.
### 14.6.1 Collecting and retrieving actual resource usages
Kubelet 에 **cAdvisor** 라는 애가 내장되어 있는데, 컨테이너와 노드의 자원 사용량을 모은다. 이를 중앙으로 모르기 위해서는 **Heapster** 라는 추가 컴포넌트를 실행해야 한다.
**Heapster 는 현재 deprecated!**
Heapster 는 Service 로 실행되어 IP 주소로 접근이 가능하고, 각 노드의 cAdvisor 로부터 정보를 모은다.
> minikube 에서는 `minikube addons enable metrics-server` 로 실행한다.
실행 후 data aggregation 을 위해 조금 기다리면 `kubectl top node` 커맨드를 실행하여 실제 현재의 CPU/메모리 사용량을 볼 수 있다.
```
$ kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
minikube 2003m 25% 1128Mi 7%
```
`kubectl top pod` 를 하면 pod 의 상태도 확인할 수 있다.
```
$ kubectl top pod --all-namespaces
NAMESPACE NAME CPU(cores) MEMORY(bytes)
bar test 0m 9Mi
default requests-pod 954m 2Mi
default requests-pod2 949m 1Mi
foo test 0m 11Mi
kube-system coredns-74ff55c5b-4f8ft 1m 11Mi
kube-system etcd-minikube 10m 98Mi
kube-system ingress-nginx-controller-65cf89dc4f-zhztv 1m 168Mi
kube-system kube-apiserver-minikube 30m 266Mi
kube-system kube-controller-manager-minikube 9m 46Mi
kube-system kube-proxy-vz9qq 0m 16Mi
kube-system kube-scheduler-minikube 1m 15Mi
kube-system metrics-server-56c4f8c9d6-94fsb 0m 10Mi
kube-system storage-provisioner 0m 9Mi
```
> `--containers` 옵션을 주면 각 컨테이너 별로 확인할 수 있다.
### 14.6.2 Storing and analyzing historical resource consumption statistics
cAdvisor 와 Heapster 는 단기간 동안만 정보를 보관하기 때문에, 장기간 분석을 위해서는 **InfluxDB****Grafana** 를 이용한다.
**InfluxDB** 는 시계열 데이터베이스로 애플리케이션 metric 과 모니터링 데이터를 저장하기에 좋다. **Grafana** 는 분석 및 시각화 툴로 브라우저에서 접근할 수 있다. 이 둘 모두 pod 으로 실행할 수 있다.
Grafana 에서는 클러스터, Pod 단위로 requests/limits 뿐만 아니라 리소스 사용량도 확인할 수 있다. 또한 긴 기간에 대해서도 확인 가능하다.
---
## Discussion & Additional Topics
### Linux OOM Killer

View File

@@ -0,0 +1,222 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "15. Automatic Scaling of Pods and Cluster Nodes"
date: "2021-07-18"
github_title: "2021-07-18-15-autoscaling"
image:
path: /assets/img/posts/k8s-15.jpeg
---
![k8s-15.jpeg](../../../assets/img/posts/k8s-15.jpeg) _How the autoscaler obtains metrics and rescales the target deployment (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-15)_
### 주요 내용
- HPA 리소스 이해하고 사용하기
- 클러스터 노드의 automatic scaling
수동으로 scaling (수평) 을 하는 경우 ReplicationController, ReplicaSet, Deployment 등에서 `replicas` field 를 고쳐줘야 했다.
이번 장에서는 pod 나 노드의 상태에 따라 자동으로 scaling 하는 방법을 알아본다.
## 15.1 Horizontal pod autoscaling
---
Horizontal pod autoscaling 은 **Horizontal controller** 가 pod 의 replica 수를 알아서 조절하는 것이다.
이 controller 는 **HorizontalPodAutoscaler**(HPA) 리소스를 생성하면 활성화 되며, 주기적으로 pod 의 metric 을 확인하고, metric 이 원하는 값에 도달할 수 있도록 replica 수를 조절한다.
### 15.1.1 Understanding the autoscaling process
Autoscaling 은 3단계로 일어난다.
1. Scaled resource object 가 관리하는 모든 pod 의 metric 수집
- Scaled resource object 라 함은 ReplicationController, ReplicaSet, Deployment 등 scaling 을 관리하는 오브젝트
2. Metric 값이 원하는 값(target value)에 최대한 가깝게 하기 위해 필요한 pod replica 수 계산
- HPA 에 어떤 metric 값이 얼마에 도달하기를 원하는지 정의할 수 있다
3. Scaled resource 의 `replicas` field 를 업데이트
#### Pod metric 수집
14장에서 살펴본 것처럼 *cAdvisor* 에서 metric 을 수집하고 이를 *Heapster* 로 보낸다. 그리고 HPA 는 Heapster 를 REST API 로 호출하여 정보를 얻어온다.
> Heapster 는 deprecated 되었으므로, 아마 metrics-server 에서 가져오지 않을까?
한 가지 유의할 점은 여러 단계를 거쳐 정보를 받아오므로, 정보가 전파되는데 시간이 조금 걸리기 때문에 scaling 이 실제로 시작되기까지는 delay 가 있을 수 있다.
#### 필요한 pod 개수 계산
Metric 을 하나만 사용하는 경우에는 모든 pod 의 metric 값을 합해서 이를 target value (HPA 에 지정한 원하는 값) 로 나누고 정수로 올림한다.
예를 들어 CPU의 target value 가 50% 인데, 3개의 pod 의 CPU metric 합이 270% 라면, 270 / 50 = 5.4 이고 정수로 올려서 6개의 pod 가 필요하게 된다.
> 물론 실제 계산은 이보다 복잡하다. 순간적으로 CPU 사용률이 요동을 치는 경우 HPA 가 이에 민감하게 반응하지 않도록 처리한다.
만약 metric 을 여러 개 사용하는 경우에는 각 metric 별로 요구하는 pod 개수 중 최댓값을 택한다.
#### `replicas` field 업데이트
HPA 와 scaled resource object 사이에 한 단계의 추상화 interface layer 를 두어 HPA 는 scaled resource object 의 세부 스펙을 몰라도 되게 해두었다.
**Scale** sub-resource 가 해당 layer 역할을 하고, HPA 는 Scale 의 replica count 만 조작하고, 실제고 scaled resource object 의 `replicas` field 를 업데이트 하는 것은 Scale 이다.
Scale 가 지원하는 scaled resource object 에는 Deployments, ReplicaSets, ReplicationControllers, StatefulSets 등이 있다.
### 15.1.2 Scaling based on CPU utilization
Autoscaler 는 pod CPU requests 에 설정한 값에 비해 실제로 얼마나 쓰고있는지를 계산하여 pod CPU utilization 을 계산한다.
따라서 autoscaling 을 위해서는 CPU requests 값을 설정해둬야 한다.
HPA 를 실제로 생성하기 위해서는 다음 명령어를 입력하면 된다.
```bash
$ kubectl autoscale deployment <DEPLOYMENT_NAME> --cpu-percent=<PERCENT> --min=<MIN> --max=<MAX>
```
- `PERCENT` 는 target value 로 지정할 CPU utilization 이다.
- `MIN`, `MAX` 는 scaling 으로 조절할 수 있는 pod 개수의 최솟값/최댓값이다.
> Deployment 를 autoscale 하는 것이 권장된다. ReplicaSet 을 직접 autoscale 하는 경우 프로덕션 환경에서 pod 이 업데이트 할 때 Deployment 가 새로운 ReplicaSet 을 생성하면 HPA 를 다시 붙여야 한다.
YAML manifest 는 이렇게 생겼다.
```yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
...
name: kubia
spec:
maxReplicas: 5
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kubia
targetCPUUtilizationPercentage: 30
status:
currentReplicas: 3
desiredReplicas: 0
```
HPA 를 생성한 뒤 `kubectl get hpa` 를 해보면 아직 CPU 정보가 없어 `<unknown>` 이라고 나오는 것을 볼 수 있다.
```
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
kubia Deployment/kubia <unknown>/30% 1 5 3 47s
```
#### Maximum scaling rate
Autoscaler 가 scaling 을 할 때 한 번에 생성할 수 있는 pod 개수에 제한이 있다.
만약 현재 replica 수가 1~2 이면 최대 replica count 는 4로 제한된다. 만약 3 이상인 경우에는 replica count 는 최대 현재의 2배로 제한된다.
추가로 autoscale event 를 시도할 수 있는 시간에 대한 제약도 있다. Scale-up 의 경우 지난 3분간 rescaling 이 없었어야 하고, scale-down 은 5분마다 시도한다. Metric 값이 갑작스럽게 변경되었지만 최근에 scaling 이 일어났다면 HPA 가 바로 반응하지 않을 수도 있다.
### 15.1.3 Scaling based on memory consumption
메모리의 경우는 CPU와 달리 scaling 하기가 어렵다. Pod 를 더 띄웠다고 해서 기존 pod 가 물고 있던 메모리를 놓지 않을 수도 있기 때문이다.
시스템 측에서 할 수 있는 것이라고는 그저 컨테이너를 죽이고 재시작 한 다음, 메모리를 예전보다 적게 쓰길 바라는 수밖에 없다. 그래서 메모리 해제는 pod 내부의 애플리케이션이 처리해야 하는 부분이다.
> Kubernetes 1.8 에서 memory-based scaling 이 나왔다고 한다!
### 15.1.4 Scaling based on other and custom metrics
HPA 의 `.spec.metrics` field 는 list 이기 때문에 여러 metric 을 한꺼번에 사용할 수 있다. 각 entry 는 metric `type` 를 정의한다. HPA 에는 3가지 종류의 metric `type` 이 있는데, `Resource`, `Pods`, `Object` 가 있다.
#### `Resource` metric type
CPU utilization 과 같이 컨테이너의 resource requests 와 관련된 값들이다.
### `Pods` metric type
Pod 와 관련된 metric 들이 여기에 속한다. 예를 들어 queries per second (QPS) 등이 있다.
### `Object` metric type
Scaling 을 결정하는 metric 이 pod 과 직접적인 연관이 없을 때 사용한다. 예를 들면 Ingress object 의 QPS 나 request latency 등을 기반으로 scaling 을 할 수 있다.
`Object` type 을 쓰게 되면 하나의 object 에서 하나의 metric 을 가져오기 때문에 target object (이름으로) 정해줘야 한다.
https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics
### 15.1.5 Determining which metrics are appropriate for autoscaling
모든 metric 이 autoscaling 에 적합한 것은 아니다. Replica 수를 늘렸을 때 metric 이 감소해야 한다. 그러므로 pod 개수가 증가/감소할 때 metric 이 어떻게 변할지 예측해보고 사용해야 한다.
### 15.1.6 Scaling down to zero replicas
책이 쓰였을 당시 HPA 에서는 `minReplicas` 값을 0 으로 설정하는 것을 허용하지 않았다고 한다.
> Documentation 에서는 특별한 언급을 찾지는 못했다. 일단 `autoscale` 명령어로 `--min=0` 을 주고 생성해도 1 로 설정하는 것이 확인되었다.
## 15.2 Vertical pod autoscaling
---
만약 vertical scaling 이 지원됐다면, pod manifest 에서 resource requests 를 수정하는 방식으로 지원했을 것이라고 한다. 하지만 이미 생성된 pod 의 resource requests 는 수정이 불가능하다.
책이 쓰였을 당시에는 vertical pod autoscaling 은 지원이 되지 않았다고 한다.
> 검색해보니 AWS, GCP 등 클라우드에서 vertical scaling 이야기는 나오는데, 어째서인지 검색 결과 중에 공식 문서는 없다...?
### 15.2.1 Automatically configuring resource requests
Experimental feature 중에, 새로 생성된 pod 의 CPU/메모리 requests 를 자동으로 설정해주는 기능이 있다. InitialResources 라는 Admission Control plugin 에 의해 동작한다.
자동으로 설정할 때는 과거 같은 컨테이너를 실행한 기록을 참고하여 CPU/메모리 requests 를 설정한다. 어떻게 보면 vertical scaling 을 하고 있는 것인데, 컨테이너가 OOM이 자주 발생했다면, InitialResources 는 그 컨테이너를 실행할 때 메모리를 더 요청한다.
### 15.2.2 Modifying resource requests while a pod is running
책이 쓰여질 당시 vertical pod autoscaling proposal 이 finalize 되고 있었다고 한다. 공식 문서를 참고해 달라고 한다.
## 15.3 Horizontal scaling of cluster nodes
---
Scaling 을 하다 보면 존재하는 노드의 자원을 다 쓰거나 기타 이유로 scheduling 이 불가능해지는 상황이 올 수도 있다.
On-premise 환경이라면 어렵겠지만, 클라우드의 경우 자동으로 노드를 생성해주는데 이를 **Cluster Autoscaler** 라고 부른다.
### 15.3.1 Introducing the Cluster Autoscaler
Cluster Autoscaler 는 pod 가 생성되었지만 scheduling 에 실패한 경우 새로운 노드를 생성해주며, 만약 노드의 사용률이 낮으면 (under-utilized) 노드를 알아서 처분해주기도 한다.
#### 클라우드에 새로운 노드 요청
Scheduler 가 scheduling 에 실패했을 때, Cluster Autoscaler 가 동작하며 새로운 노드를 클라우드에 요청한다. 우선 새로운 pod 가 새로운 노드에 띄워질 수 있는지 검사하고, 내부의 알고리즘에 따라 노드 타입을 선택하여 요청을 보낸다.
새로운 노드가 띄워지면 새 노드의 Kubelet 이 API server 에 Node 리소스 생성을 요청하며, 해당 노드는 클러스터의 일부가 되어 동작하게 된다.
> 책에 내부 알고리즘이 정확하게 나와있지는 않다. 클라우드가 제공하는 노드 종류가 여러 개 있는데, 이 중 어떤 노드가 'best' 인지는 configurable 하다고 적혀있다. 최악의 경우 랜덤으로 고른다고 한다. 근데 비용이 많이 나갈 수도 있는데 설마 이럴 것 같지는 않다.
#### 노드 삭제
만약 한 노드에 존재하는 모든 pod 의 CPU/메모리 requests 가 50% 이하이고, 해당 노드에서만 돌아가는 system pod 가 없으며, (DaemonSet 의 경우 모든 노드에서 돌아가므로 제외) unmanaged pod (Replication 에 의해 관리되지 않음) 나 pod with local storage 가 존재하지 않으면 불필요하다고 판단한다.
조건이 복잡하지만 결국 노드에서 실행 중인 pod 들이 다른 노드로 scheduling 될 수 있고, requests 가 적으면 불필요하다고 판단한다.
노드가 삭제될 것이라고 마킹되면, unschedulable 이라고 마킹되며, pod 들은 전부 삭제된다. 삭제되기 위해서는 애초에 모든 pod 들이 Replication 에 의해 관리되고 있었을 것이므로, 삭제된 pod 대신 다른 노드에 pod 가 자동으로 띄워진다.
> `kubectl cordon <node>` 를 하면 노드가 unschedulable 상태가 된다. 하지만 pod 들은 남아있는다. `kubectl drain <node>` 를 하면 unschedulable 상태가 되고 pod 가 전부 삭제된다.
### 15.3.2 Enabling the Cluster Autoscaler
각 클라우드 공식 문서를 참고하면 된다.
### 15.3.3 Limiting service disruption during cluster scale-down
몇몇 서비스는 항상 실행되고 있어야 하는 최소 pod 개수가 존재한다. Kubernetes 에서는 **PodDisruptionBudget** 리소스를 사용하여 최솟값을 설정할 수 있다.
Pod label selector 와 최솟값을 설정해서 생성하면 selector 에 매칭된 pod 의 개수가 최솟값 이상이 되도록 해준다.
```bash
$ kubectl create pdb <NAME> --selector=<SELECTOR> --min-available=<MIN>
```
- `SELECTOR` 는 pod label selector (`key=value`)
- `MIN` 은 사용 가능한 pod 개수의 최솟값
> Kubernetes 1.7 부터는 사용 가능한 pod 의 최솟값 대신 사용 불가능한 pod 의 최댓값을 `maxUnavailable` 으로 설정할 수도 있다.

View File

@@ -0,0 +1,274 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "16. Advanced Scheduling"
date: "2021-08-15"
github_title: "2021-08-15-16-advanced-scheduling"
image:
path: /assets/img/posts/k8s-16.jpeg
---
![k8s-16.jpeg](../../../assets/img/posts/k8s-16.jpeg) _A pod is only scheduled to a node if it tolerates the nodes taints. (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-16)_
### 주요 내용
- 노드의 taint 와 pod tolerations
- Node affinity rules 사용
- Pod affinity, anti-affinity 사용
## 16.1 Using taints and tolerations to repel pods from certain nodes
---
Pod 가 특정 노드에 schedule 되기 위해서는 그 노드의 taint 를 tolerate 할 수 있어야 한다.
### 16.1.1 Introducing taints and tolerations
Taint 는 **key, value, effect** 로 이루어져 있고, `<key>=<value>:<effect>` 로 표현된다. `kubectl describe node [NODE_NAME]` 을 해보면 `Taints` 항목에서 확인할 수 있다.
예를 들어 `node-role.kubernetes.io/master:NoSchedule` 이라는 taint 가 노드에 설정되어 있으면, 이 값을 tolerate 에 가지고 있지 않은 pod 는 이 노드에서 실행될 수 없게 된다.
`kubectl describe pod` 를 이용해 pod 설명을 보면 `Tolerations` 항목에서 확인할 수 있다.
Taint 의 effect 종류에는 3가지가 있다.
- `NoSchedule`: taint 를 tolerate 하지 않는 pod 들은 schedule 될 수 없다.
- `PreferNoSchedule`: taint 를 tolerate 하지 않더라도 만약 scheduling 될 수 있는 다른 노드가 없을 때는 이 노드에 scheduling 가능하다.
- `NoExecute`: 이 taint 를 노드에 추가하면, 이를 tolerate 하지 않는 pod 들은 전부 삭제된다.
### 16.1.2 Adding custom taints to a node
```bash
$ kubectl taint node <NODE> <key>=<value>:<effect>
```
### 16.1.3 Adding toleration to pods
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: prod
spec:
replicas: 5
template:
metadata:
labels:
app: prod
spec:
containers:
- args:
- sleep
- "99999"
image: busybox
name: main
tolerations: # tolerations
- key: node-type
operator: Equal
value: production
effect: NoSchedule
```
`key``value` 가 같을 때 (`operator: Equal`) taint 를 tolerate 할 수 있게 된다.
### 16.1.4 Understanding what taints and tolerations can be used for
기본적으로 taint 와 toleration 은 모두 여러 개 가질 수 있다.
Taint 의 경우 key 만 있고 value 는 없어도 된다.
Toleration 의 경우 `operator: Equal` 을 이용해서 특정 value 만 처리할 수 있으며, value 가 없거나 상관 없는 경우에는 key 가 존재하는지만 확인하기 위해 `operator: Exists` 를 사용할 수 있다.
#### Scheduling 을 위해 사용
Taint 를 사용하면 `NoSchedule` effect 를 이용해 새로운 pod 들이 노드에 schedule 되는 것을 막을 수 있고, `PreferNoSchedule` effect 로 선호하지 않는 pod 을 정의할 수 있으며, `NoExecute` 를 이용해 존재하는 pod 도 삭제할 수 있게 된다.
이외에도 taint/toleration 을 이용해서 클러스터를 분할해서 여러 팀이 사용하게 할 수 있다.
## 16.2 Using node affinity to attract pods to certain nodes
---
Taint 를 이용하면 특정 노드에 pod 이 scheduling 되지 않도록 할 수 있었다. 반면 **node affinity** 를 이용하면 pod 가 schedule 될 수 있는 노드를 정할 수 있다.
우선 node selector 는 결국 deprecated 될 것이라는 점을 유의하고, node affinity 를 사용하는 것이 권장된다.
Node selector 와 유사하게 pod 마다 node affinity rule 을 가질 수 있다. 이 rule 들은 hard requirement 나 preference 를 정할 수 있게 해준다.
### 16.2.1 Specifiying hard node affinity rules
Chapter 3 의 예제에서 GPU 가 필요한 pod 를 GPU 노드에만 scheduling 되도록 했었다. 그 때는 `nodeSelector` field 를 지정해 줬었다.
```yaml
...
spec:
nodeSelector:
gpu: "true"
...
```
반면 node affinity rule 을 사용하여 다음과 같아진다.
```yaml
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"
```
`requiredDuringSchedulingIgnoredDuringExecution` attribute 는 사실
- `requiredDuringScheduling`: Scheduling 에 필요한 rule 들을 정의
- `IgnoredDuringExecution`: 노드에서 이미 실행 중인 pod 에는 rule 이 영향을 주지 않는다
는 의미이다.
이외에 `nodeSelectorTerms``matchExpressions` 는 노드의 label 에서 일치하는 것을 찾으라는 의미이다.
따라서 이 pod 는 `gpu=true` label 이 있는 노드에만 scheduling 된다.
### 16.2.2 Prioritizing nodes when scheduling a pod
Node affinity 를 이용했을 때의 가장 큰 장점은 노드에 우선순위 (선호도)를 두어 Scheduler 가 scheduling 할 때 이를 반영할 수 있다는 점이다. 이는 `preferredDuringSchedulingIgnoredDuringExecution` field 를 이용해서 할 수 있다.
우선 노드에 label 이 되어있어야 하고, pod 를 생성할 때 다음과 같이 생성하면 된다.
```yaml
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # 가중치 설정 가능
preference:
matchExpressions:
- key: availability-zone
operator: In
values:
- zone1
- weight: 20
preference:
matchExpressions:
- key: share-type
operator: In
values:
- dedicated
```
`preferredDuringSchedulingIgnoredDuringExecution` 으로 선호도 규칙을 설정할 수 있고, `weight` 를 주어 가중치를 설정할 수 있다. 80:20 이므로 첫 번째 규칙이 4배 더 중요한 것이다.
위 규칙에 의하면 노드의 우선순위 순서는 다음과 같아진다.
1. `availability-zone=zone1`, `share-type=dedicated`
2. `availability-zone=zone1`, `share-type``dedicated` 가 아님
3. `availability-zone``zone1` 이 아니고, `share-type=dedicated`
4. 이외의 노드
## 16.3 Co-locating pods with pod affinity and anti-affinity
---
앞서 살펴본 node affinity 는 pod 과 노드 사이의 affinity 를 정한 것이었는데, 때로는 pod 사이의 affinity 가 필요할 때도 있다. 예를 들어 프론트엔드/백엔드 pod 를 같은 노드에 띄우면 latency 가 줄어들게 될 것이다. 그렇다고 두 pod 를 특정 노드에 띄우라고 지시하기 보다는 Kubernetes 가 알아서 하라고 하는 것이 좋을 것이다.
### 16.3.1 Using inter-pod affinity to deploy pods on the same node
예시로 백엔드 pod 하나와 프론트엔드 pod 5개를 띄우는 상황을 고려해 보자.
```
$ kubectl run backend -l app=backend --image busybox -- sleep 999999
```
여기서 확인할 부분은 `app=backend` label 이 부여되었다는 점이다. 이 label 을 이용해 `podAffinity` 를 설정하기 때문이다.
프론트엔드 pod 에서 다음과 같이 설정한다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 5
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: backend
containers:
- name: main
image: busybox
args:
- sleep
- "99999"
```
`requiredDuringSchedulingIgnoredDuringExecution` 으로부터 hard requirement 이고, `topologyKey` field 로 인해 `app=backend` label 을 가진 pod 와 같은 노드에 띄워지게 된다.
> 여기서 백엔드 pod 를 지우고 다시 띄우면, 백엔드 pod 에는 affinity 설정이 없음에도 불구하고 프론트엔드 pod 과 같은 노드로 scheduling 된다. Scheduler 가 노드를 고를 때 프론트엔드 pod 에 있는 affinity rule 를 반영하게 된다.
### 16.3.2 Deploying pods in the same rack, availability zone, or geographic region
#### Same availability zone
`topologyKey` 의 값을 `failure-domain.beta.kubernetes.io/zone` 으로 설정하면 된다.
#### Same geographical region
`topologyKey` 의 값을 `failure-domain.beta.kubernetes.io/region` 으로 설정하면 된다.
#### `topologyKey` 의 작동 방식
Scheduler 가 scheduling 을 수행할 때 pod 의 `podAffinity` 설정을 확인하는데, 우선 label selector 를 이용해 pod 의 목록을 조회한다. 그 다음 pod 들이 띄워져 있는 노드를 모두 조사하여 노드의 label 들 중에서 `topologyKey` 에 대한 value 가 같은 노드에 scheduling 한다.
예를 들어 `topologyKey``rack`, label selector 는 `app=backend` 라고 하자. `app=backend` label 을 가진 pod 가 만약 노드 1에 띄워져 있다면, 노드 1의 label 을 조회하여 `rack` 이라는 key 가 존재하는지 확인하고, 그 key 에 대한 value 가 `rack2` 라면 새로운 pod 는 `rack=rack2` label 을 가진 노드로만 scheduling 이 일어난다.
> label 을 잘못 설정하거나 그러면 분명 꼬이는 경우가 있을 것 같은데 그런 경우에는 어떻게 에러 핸들링이 되는지 궁금하다. `minikube` 는 single node cluster 라 테스트가 불가능...
> 추가로 label selector 가 동작할 때 같은 namespace 에서만 조회한다.
### 16.3.3 Expressing pod affinity preferences instead of hard requirements
Node affinity 에서와 마찬가지로 hard requirement 대신 preference 를 설정할 수 있고, 필드 명도 `preferredDuringSchedulingIgnoredDuringExecution` 이다. 마찬가지로 선호한다는 의미이고, 반드시 지켜지지 않을 수도 있다.
```yaml
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: backend
```
### 16.3.4 Scheduling pods away from each other with pod anti-affinity
이번에는 반대로 pod 가 서로 같은 노드에 scheduling 되지 않도록 하고 싶을 때, anti-affinity 를 사용하면 된다.
대표적으로 만약 pod 에서 실행 중인 애플리케이션이 서로의 performance 에 영향을 준다면 다른 노드에 배치하고 싶을 수 있으며, 또 고가용성(HA)를 위해 여러 availability zone/region 에 pod 를 띄우고 싶은 경우 anti-affinity 가 필요하다.
```yaml
spec:
affinity:
podAntiAffinity: # podAntiAffinity 이다
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: frontend
```
전체적인 규칙은 `podAffinity` 와 비슷하다. 마찬가지로 `preferredDuringSchedulingIgnoredDuringExecution` 를 이용하면 hard requirement 가 아닌 preference 를 지정할 수 있다.

View File

@@ -0,0 +1,332 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "17. Best Practices for Developing Apps"
date: "2021-08-15"
github_title: "2021-08-15-17-best-practices"
image:
path: /assets/img/posts/k8s-17.jpeg
---
![k8s-17.jpeg](../../../assets/img/posts/k8s-17.jpeg) _Resources in a typical application (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-17)_
### 주요 내용
- Kubernetes 리소스에 대한 전반적인 이해
- Pod lifecycle hooks and init containers
## 17.1 Bringing everything together
---
일반적인 애플리케이션이 Kubernetes 위에서 배포될 때 어떤 형태로 하는지, 지금까지 살펴본 것들을 종합하여 알아본다.
일반적으로 애플리케이션을 배포할 때는 Deployment 또는 StatefulSet 을 반드시 사용하게 된다. 이들은 pod template 를 들고 있으며, 거기에는 liveness/readiness probe 가 모두 정의되어 있다.
Service 를 이용해서 외부에서 pod 로 요청을 보낼 수 있도록 하고, 이 Service 는 `LoadBalancer` 또는 `NodePort` 타입이나 Ingress 를 이용해 외부로 노출된다.
그리고 pod template 들은 주로 두 종류의 secret 을 '참조'하는데, 첫째는 private image registry 에서 image 를 pull 받기 위해 사용하는 secret 이고, 둘째는 컨테이너 내부에서 사용하는 secret 이다. '참조'만 하는 이유는 secret 의 경우 manifest 에 포함되지 않기 때문에 개발자 생성하는 것이 아니기 때문이다. 주로 클러스터 관리자/Operation team 에서 secret 을 생성하며 ServiceAccount 에 할당되고 pod 가 ServiceAccount 를 이용하는 형태이다.
그 밖에도 환경 변수를 위해 ConfigMap 이 사용되며, 저장 공간이 필요한 경우 PVC 가 사용된다. 주기적인 작업이 필요한 경우 Jobs 나 CronJobs 를 사용하고, DaemonSet 의 경우에는 개발자가 직접 생성하지는 않고 시스템 관리자들이 실행해 둔다. HorizontalPodAutoscaler 는 scaling 을 위해 개발자가 직접 포함할 수도 있고, 나중에 Ops 팀에서 추가할 수도 있다. 클러스터 관리자는 LimitRange, ResourceQuota 를 이용해 pod 들의 리소스 사용량을 관리할 수 있다.
애플리케이션이 배포되면, Service 와 함께 Endpoints 가 생성되고, Deployment/StatefulSet 와 함께 ReplicaSet 이 생성된다.
Kubernetes resource 에는 주로 label 을 붙여서 관리하고, 또 대부분 annotation 을 가지고 있어 메타데이터를 저장하여 운영 및 관리를 할 수 있게 해준다.
## 17.2 Understanding the pod's lifecycle
---
Pod 와 VM 의 가장 큰 차이점 중 하나는 pod 는 얼마든지 삭제되었다 다시 생성할 수 있다는 점이다. Pod 를 다른 노드로 옮기거나, scale down 이 일어났을 때 pod 를 삭제하곤 한다.
### 17.2.1 Applications must expect to be killed and relocated
Kubernetes 를 사용하게 되면 pod 들이 (VM 을 사용할 때와 비교했을 때) 훨씬 많이 움직인다. 그리고 이 작업은 자동으로 일어나기 때문에, 개발자는 애플리케이션이 자주 이동되더라도 잘 작동하도록 개발해야 한다.
Pod 이 새롭게 생성되는 경우 변경되는 것들에 의존성이 생기지 않도록 주의해야 한다. 예를 들어, pod 이 죽었다가 다시 생성되면 IP 주소와 hostname 이 변경된다. Stateless 애플리케이션이라면 상관이 없겠지만, stateful 하다면 난감해진다. 물론 StatefulSet 을 사용하면 hostname 은 변하지 않지만 IP 는 여전히 변경된다.
또한 디스크에 쓴 데이터가 유실될 수 있음에 유의해야 한다. Persistent storage 를 사용하지 않으면 컨테이너가 재시작될 때 컨테이너의 파일시스템에 쓴 데이터는 모두 사라진다. 특별히 이러한 상황은 단순히 pod 가 삭제될 때 뿐만 아니라, liveness probe 가 실패하거나, 컨테이너의 프로세스에 오류가 생기거나, OOM 이 발생하는 등 다른 이유로 컨테이너가 재시작될 때 생길 수 있는 문제이므로 조심해야 한다.
위와 같은 데이터 유실을 막으려면 pod-scope 에서 volume 을 사용하면 될 것이다. 새로운 컨테이너가 생기더라도 volume 은 바뀌지 않았을 것이다. 다만 이 해결책도 완벽하지는 않은데, volume 내의 데이터에 문제가 생겨 컨테이너가 반복적으로 죽는 상황이 발생할 수도 있다. 양날의 검이므로 주의해야 한다.
### 17.2.2 Rescheduling of dead or partially dead pods
만약 컨테이너가 여러 개인 pod 에서 일부 컨테이너가 죽었거나 (partially dead), 모든 컨테이너가 죽었다고 가정해보자. (dead - 컨테이너가 1개인 경우도 포함)
여기서 주의할 점은 만약 이 pod 가 ReplicaSet 등의 관리를 받고 있다고 하더라도, pod 의 rescheduling 이 일어나지 않는다는 점이다. 그래서 만약 desired replica count 가 3 인데 한 pod 에 이상이 생겨 실제로 정상인 pod 는 2개이더라도 Kubernetes 에서는 이에 대해 특별한 조치를 취하지 않고 내버려 둔다. 즉, ReplicaSet controller 는 pod 가 죽어있는지에 대해서 관심이 없다. 단지 존재하는 pod 의 개수가 desired replica count 와 일치하는지만 확인한다.
이렇게 동작하는 이유는, 만약 rescheduling 이 일어난다고 하더라도 컨테이너가 죽은 원인이 해결되지 않을 것이라고 가정하기 때문이다. 어차피 pod 라는 격리된 환경에서 돌아가고 있으므로 rescheduling 하더라도 같은 환경일 확률이 높을 것이다. (항상 그렇지는 않겠지만 대부분의 경우)
### 17.2.3 Starting pods in a specific order
애플리케이션을 배포할 때 pod 간의 의존성이 있어 특정 순서로 pod 이 생성될 필요가 있을 때가 있다. 하지만 Kubernetes 에게 생성하는 pod 의 순서를 직접 알려줄 방법은 없다. 그렇다고 순서를 맞추기 위해 YAML 파일을 여러 번 POST 하는 방법은 절대 좋은 방법이 아닐 것이다.
물론 하나의 YAML 파일 안에 여러 리소스가 있다면, Kubernetes 는 각 리소스를 파일에 입력된 순서대로 처리하기는 한다. 하지만 이것도 `etcd` 에 그 순서로 write 가 된다는 것이지, 생성된 리소스가 정상적으로 '실행'되는 순서는 보장하지 않는다.
그래서 Kubernetes 에서는 init container 를 사용하여, 특정 조건이 만족되기 전까지 pod 의 main container 를 실행하지 않도록 한다.
#### Init containers
이름에서 알 수 있듯이, pod 를 초기화할 때 (시작할 때) 사용하는 컨테이너이다.
Pod 가 가질 수 있는 init container 의 개수에는 제한이 없으며, YAML 에 입력한 순서대로 실행되고 마지막 init container 가 종료되어야 main container 가 실행된다. 그러므로 init container 를 사용하면 main container 의 실행을 지연시킬 수 있으며, init container 에 main container 가 실행될 조건을 걸 수 있다는 것이다.
예를 들어, pod A 가 실행된 다음에 B 가 실행되어야 한다면, pod B 의 init container 에 pod A 가 실행 중인지 확인 (HTTP 요청을 보내는 등) 하는 실행 중이면 종료하는 코드를 넣어 B 의 실행을 A 의 실행 이후가 되도록 할 수 있다.
Pod 에 init container 를 추가하려면 `spec.initContainers` field 에 적어주면 된다.
```yaml
spec:
initContainers:
- name: init
image: busybox
command:
- sh
- -c
- 'while true; do ...;'
```
이 pod 를 실행하면 init container 만 실행되게 되고, `kubectl get` 으로 확인해보면 `STATUS` 에도 이것이 반영된다.
```
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
fortune-client 0/1 Init:0/1 0 1m
```
그리고 `kubectl logs fortune-client -c init` 으로 init container 의 로그도 가져올 수 있다.
#### Best practices for handling inter-pod dependencies
기본적으로 애플리케이션 내부에서 pod 간의 의존성이 해결되지 않았을 경우를 handling 해야 한다. 추가로 readiness probe 를 적절하게 활용하여 만약 의존성 문제로 pod 이 실행되지 않고 있는 경우 readiness probe 가 실패하도록 설계해야 한다.
### 17.2.4 Adding lifecycle hooks
Init containers 와 별도로 pod 에는 2가지의 lifecycle hooks 가 있다.
- Post-start hook (시작 후 실행)
- Pre-stop hook (종료 전 실행)
Hook 은 컨테이너 당 만들 수 있으며, 컨테이너 내에서 특정 command 를 실행하거나 HTTP GET 요청을 보낼 수 있게 해준다.
#### Post-start hook
Post-start hook 는 컨테이너의 프로세스가 시작되자마자 실행된다. 컨테이너 프로세스와 별개로 추가 작업을 할 수 있게 해주며, 컨테이너 안의 프로세스가 3rd-party 앱이라 코드를 직접 건드리지 못하는 경우 post-start hook 이 도움이 될 수 있다.
Hook 는 컨테이너 프로세스와 병렬로 실행되어, 컨테이너 프로세스가 실행되는 것을 기다리지 않는다. (애초에 실행 되었는지 알 방법이 없기도 하다)
그리고 병렬로 실행되지만, hook 이 완료될 때까지 컨테이너는 `Waiting` 상태에 있게 되고 reason 은 `ContainerCreating` 이 된다. 그래서 pod 의 상태가 `Running` 이 아닌 `Pending` 이 된다.
Hook 의 실행이 실패하거나 non-zero exit code 를 돌려주면 컨테이너가 종료된다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-poststart-hook
spec:
containers:
- image: luksa/kubia
name: kubia
lifecycle:
postStart:
exec:
command:
- sh
- -c
- "echo 'hook will fail with exit code 15'; sleep 5 ; exit 15"
```
한 가지 아쉬운 점이 있다면, hook 이 `stdout` 으로 출력하면 출력물을 볼 수 없어 디버깅이 어렵다는 점이다. Hook 이 실패하면 `kubectl describe pod` 의 events 에서 `FailedPostStartHook` 만 확인할 수 있다. 만약 출력물을 보고 싶다면 파일에 쓰도록 해야할 것이다. 이 또한 컨테이너가 재시작 되면 날아갈 수 있으므로 `emptyDir` volume 을 붙여서 그 곳에 쓰도록 하는 것이 좋다.
#### Pre-stop hook
Pre-stop hook 는 컨테이너가 죽기 전에 실행된다. 컨테이너가 종료될 때 Kubelet 이 pre-stop hook 을 실행하고 컨테이너 프로세스에게 `SIGTERM` 을 보낸다.
Pre-stop hook 를 사용하면 컨테이너의 graceful shutdown 을 구현할 수 있다. 이외에도 컨테이너 프로세스 종료 전에 해야하는 정리 작업을 할 수도 있다.
Post-start hook 와는 달리 실행 결과와 관계없이 컨테이너 프로세스가 종료된다. 물론 pre-stop hook 이 실패하면 event 에 `FailedPreStopHook` 이 발생하지만, 컨테이너가 곧 지워지므로 눈치채지 못할 수 있다.
#### Lifecycle hooks target containers, not pods
Lifecycle hook 은 컨테이너 레벨에서 실행해야 하는 것이지, pod 이 종료될 때 실행하는 것이 아님을 명심해야 한다. Pod 는 그대로이지만, 컨테이너는 중간에 여러 번 교체될 수 있기 때문에 lifecycle hook 에는 container 레벨의 작업이 들어가야 한다. 컨테이너 교체가 일어날 때 pod 전체의 데이터를 정리하는 pre-stop hook 을 사용해서는 안될 것이다.
### 17.2.5 Understanding pod shutdown
Pod 를 깔끔하게 종료하는 것은 중요하다!
API server 가 HTTP DELETE 요청을 받으면, `deletionTimestamp` field 를 붙인다. 이 field 값이 존재하는 pod 들은 `Terminating` 상태가 된다.
이제 Kubelet 이 pod 가 종료되어야 한다는 것을 확인하면, 컨테이너를 하나씩 종료한다. Graceful shutdown 을 위한 시간을 주지만, 기본적으로는 30초를 주며, 이는 수정 가능하다. 이 시점에서 grace period 타이머가 시작되고, 다음 과정이 일어난다.
1. Pre-stop hook 이 존재한다면 실행하고, 완료되기를 기다린다.
2. 컨테이너에 `SIGTERM` 을 보낸다.
3. Grace period 안에 종료되지 않았다면 `SIGKILL` 로 강제 종료한다.
Grace period 는 `spec.terminationGracePeriodSeconds` 로 설정할 수 있다. Pod 를 생성할 때 설정하지 않았더라도, 종료할 때 값을 설정할 수 있다.
```bash
$ kubectl delete pod <POD> --grace-period=5
```
그래서 만약 강제로 종료하고 리소스를 삭제하고 싶다면 `--grace-period=0` 을 주고 `--force` 옵션을 부여한다.
#### Shutdown handler
컨테이너에서 실행 중인 애플리케이션은 `SIGTERM` 에 적절하게 반응하여 종료를 위한 준비를 시작해야 한다.
다만 문제가 있다면 이게 얼마나 걸릴지 예측하기 어렵다는 점이다. 만약 애플리케이션이 stateless 해서 그냥 종료되도 상관이 없다면 문제가 되지 않지만, stateful 하거나, 데이터를 담고 있어 종료 전에 데이터를 옮겨야 한다면 상황이 복잡해진다.
Shutdown procedure 가 반드시 성공하게 하기 위해서, 애플리케이션이 `SIGTERM` 을 받았을 때 Job 리소스를 생성하고, 그 Job 은 애플리케이션의 데이터를 다른 곳으로 옮겨줄 pod 를 생성하도록 하면 된다. 다만 노드에 문제가 생기는 경우 Job 이 생성되지 않거나, pod 가 생성되지 않을 수 있다. 이런 경우에는 pod 하나를 데이터 이전 용도로 항상 띄워두는 방법을 사용한다.
StatefulSet 을 사용하면 PVC 를 다시 사용할 수 있기는 하지만, scale up 이 오랜 시간 동안 일어나지 않으면 PVC 의 데이터가 그냥 남아있으므로, 이 데이터를 가져와야 하기 때문에 데이터 이전 용 pod 가 필요하다.
> 얘도 문제가 생기는 경우는 어떻게 하나요...
## 17.3 Ensuring all client requests are handled properly
---
당연히 client 의 요청은 잘 처리해야 한다.
### 17.3.1 Preventing broken client connections when a pod is starting up
Service 의 Endpoint 리소스에 pod 의 IP 가 들어가려면 pod 가 `READY` 상태가 되어야 하므로 readiness probe 만 잘 사용하면 pod 가 시작될 때 요청을 처리하지 못할 걱정은 하지 않아도 된다.
### 17.3.2 Preventing broken connections during pod shutdown
크게 두 가지 문제가 있다. 받았는데 처리하지 못한 요청이 있을 수 있고, persistent connection 이 있을 수도 있다.
우선 shutdown 과정이 어떻게 일어나는지 자세히 살펴본다.
API server 가 pod 삭제 요청을 받으면, `etcd` 에서 상태를 변경한 다음 watch 하고 있는 프로세스에게 알려준다. Kubelet 과 Endpoints controller 가 그 알림을 받게 되고 각자 할 일을 진행한다.
- Kubelet 의 경우 앞서 17.2.5 에서 설명한 shutdown procedure 를 실행한다.
- Endpoints controller 는 우선 해당 pod 를 모든 Service 의 endpoint 에서 제거한다. 제거를 위해서 API server 에 요청을 하기 때문에 API server 는 요청을 받고 처리한 뒤 Endpoints object 를 watch 하고 있는 각 노드의 `kube-proxy` 에게 변경 사실을 알려준다. 그러면 `kube-proxy` 는 노드의 `iptables` rule 을 변경하여 종료 중인 pod 로는 요청이 오지 않도록 한다.
여기서 문제가 되는 부분은 `iptables` rule 이 변경된다고 해서 이미 맺은 요청이 변경되지는 않는다는 점과, shutdown procedure 보다 `iptables` rule 수정이 더 오래 걸린다는 점이다.
결국 해결책은 요청을 처리하기 위해 컨테이너의 종료를 미루는 수밖에 없다. `kube-proxy` 뿐만 아니라 Ingress controller 나 load balancer 의 경우 `iptables` 를 거치지 않고 바로 pod 에게 요청을 주기도 한다. 그리고 client 측의 load balancing 도 고려하면 결국에는 기다리는 수밖에 없다. 5~10초 지연 만으로도 UX 를 크게 향상시킬 수 있다.
> 책에서는 완벽한 해결책이 없는 것처럼 서술되어 있는데 진짜 없을까?
#### 요약
Shutdown 을 위해서 다음 절차를 밟아야 한다.
- 몇 초간 대기하고, 새로운 요청을 더 이상 받지 않는다.
- Keep-alive connection 을 모두 종료하는데, 단 요청 중에 중단하지 않도록 유의한다.
- 처리 중인 요청이 모두 끝나기를 기다린다.
- 종료한다.
## 17.4 Making your apps easy to run and manage in Kubernetes
---
### 17.4.1 Making manageable container images
생각해보면 이미지 내에 OS 의 모든 파일이 필요하지는 않다. 불필요한 파일은 다운로드를 늦춰 pod 의 scaling 을 늦출 뿐이다. 그러므로 이미지는 가볍게 유지하는 것이 좋기는 하다.
하지만 이미지 안에 애플리케이션 실행에 필요한 파일만 남겨두게 되면 추후 디버깅이 매우 어려워진다. `curl` 과 같은 명령은 자주 사용되는데 이미지 안에 없으면 매번 설치해야 한다. 따라서 적절한 도구는 남겨두는 것이 좋다.
### 17.4.2 Properly tagging your images and using imagePullPolicy wisely
`latest` 로 이미지를 tag 하면 어떤 버전인지 알 수 없으므로 매우 불편해진다. 또한 `latest` 를 사용하면 이전 버전의 이미지로 롤백이 불가능해진다. 버전 번호를 잘 붙여서 이미지를 사용해야 한다.
또한 `imagePullPolicy``Always` 면 레지스트리에 접속해서 이미지를 다운받기 때문에 pod 의 시작이 느려질 수 있음에 유의해야 한다. 최악의 경우 레지스트리 접속에 실패하면 pod 이 시작되지 않을 수도 있다.
### 17.4.3 Using multi-dimensional instead of single-dimensional labels
Pod 를 비롯한 모든 리소스는 label 을 붙이는 것이 좋다. 또, 각 리소스에 label 은 여러개 붙여서 각 key 별로 selection 이 가능하도록 해야 한다. Label 에 자주 사용되는 key 들은 다음과 같은 것들이 있다.
- 이름
- Tier (프론트엔드/백엔드 등)
- Environment (dev/staging/prod/qa)
- 버전
- 릴리스 타입 (stable, canary 등)
### 17.4.4 Describing each resource through annotations
추가 정보를 기입하기 위해 annotation 을 사용하면 된다. 리소스를 설명하는 annotation 이 있으면 좋고, 담당자 연락처가 들어가는 것도 좋다. 혹은 MSA 에서 이 pod 를 사용하는 다른 마이크로서비스의 목록을 기입하는 것도 좋다.
### 17.4.5 Providing information on why the process terminated
로그 파일에 디버깅을 위한 정보를 남겨두는 것이 권장된다. 로그 파일로 남기는 방법도 있으며, Kubernetes 기능 중에 termination log 를 남기는 방법이 있다.
컨테이너 정의에서 `terminationMessagePath` 를 설정해 주면 (기본값은 `/dev/termination-log`) 해당 파일로 컨테이너가 죽은 이유를 기록할 수 있게 된다. 그리고 이는 `kubectl describe pod` 에서 `Last State``Message` 에서 확인할 수 있게 된다.
### 17.4.6 Handling application logs
기본적으로 애플리케이션 로그는 `stdout` 으로 출력하면 `kubectl logs` 로 읽을 수 있지만, 파일로 저장할 수도 있다. 이 경우,
```bash
$ kubectl exec <POD> cat <LOG_FILE_PATH>
```
로 내용을 읽을 수 있다. 만약 밖으로 로그를 복사하고 싶으면 `kubectl cp` 명령을 사용한다.
```bash
$ kubectl cp <POD>:<LOG_FILE_PATH> <PATH_IN_LOCAL_MACHINE>
```
참고로 반대 방향도 된다.
#### Centralized Logging
Production 환경에서는 클러스터 전체에 적용되는 중앙화된 로깅 솔루션을 적용하는 것이 좋다. Pod 이 죽으면 로그가 같이 날아가기 때문에, 로그를 중앙으로 모아주고 관리하고 분석해주는 것이 필요하다.
Kubernetes 에 내장된 기능은 없지만 FlunetD 가 주로 사용된다. (주로 DaemonSet 으로 실행된다)
FluentD 를 사용하게 되면 컨테이너의 로그를 수집하고, pod-specific information 을 태그하여 중앙으로 모아준다. EFK (Elasticsearch, FluentD, Kibana) 를 사용하는 방법도 있다.
Sidecar 컨테이너에 로그를 처리해주는 프로세스를 띄워도 된다.
#### Multi-line logs
로그가 만약 여러 줄에 걸쳐서 찍힌다면 (Java Exception stack trace 처럼) 로그 output 을 JSON 형태로 고치는 것이 좋다. 다만 이렇게 하면 `kubectl logs` 에서 보이는 내용이 인간 친화적이지 않다.
해결 방법으로는 JSON 로그는 파일로 내보내고, `stdout` 에는 원본을 찍어 사람이 보기 편하도록 하면 된다.
## 17.5 Best practices for development and testing
---
정해진 정답은 없지만, 권장 사항을 몇 가지 살펴본다. 각자 환경에 알맞은 방법을 택하면 된다.
### 17.5.1 Running apps outside of Kubernetes during development
앱 자체는 로컬에서 실행하고, 환경 변수만 적절하게 조절하여 애플리케이션의 다른 부분에 접속하면 된다. Service 를 사용해야 하는 경우 Service type 을 NodePort 등으로 둘 수도 있을 것이다.
만약 Kubernetes API server 에 접속해야 한다면 secret 만 복사해서 가져오면 된다. 클러스터 내부인지 검사하지 않는다.
또한 컨테이너 내부에서 작업을 수행해야 한다면, 로컬 스토리지를 mount 해서 개발하면 된다. 그러면 수정할 때 매번 다시 이미지를 빌드하지 않아도 된다.
### 17.5.2 Using Minikube in development
- minikube 를 사용해서 로컬의 파일을 minikube VM 에 mount 하고 이를 다시 `hostPath` 를 이용해 컨테이너 안에 mount 할 수 있다.
- minikube 의 Docker daemon 을 활용하면 매번 이미지를 레지스트리에 push 할 필요가 없어진다. `eval $(minikube docker-env)` 를 이용하면 `DOCKER_HOST` 환경 변수가 minikube 의 Docker daemon 으로 변경된다. 이미지를 새롭게 빌드한 뒤에는 pod 을 재시작 하면 된다.
### 17.5.3 Versioning and auto-deploying resource manifests
Git 등의 version control system 을 활용해 manifest 들을 저장해 두면 versioning 이나 변경 사항을 기록하기 편리하다. 또한 `kube-applier` 와 같은 툴을 사용하면 version control system 에 등록된 manifest 의 상태를 자동으로 적용해 주기도 한다.
### 17.5.4 Introducing Ksonnet as an alternative to writing YAML/JSON manifests
YAML 이 불편하면 Ksonnet 을 사용할 수 있다. JSON 을 만들기 위한 template language 인 Jsonnet 을 바탕으로 만들어졌고, Ksonnet 을 사용하면 resource manifest 를 손쉽게 작성할 수 있다.
변수에 값을 저장하는 기능도 지원한다.
```
local k = import "../ksonnet-lib/ksonnet.beta.1/k.libsonnet";
local container = k.core.v1.container;
local deployment = k.apps.v1beta1.deployment;
local kubiaContainer =
container.default("kubia", "luksa/kubia:v1") +
container.helpers.namedPort("http", 8080);
deployment.default("kubia", kubiaContainer) +
deployment.mixin.spec.replicas(3)
```
### 17.5.5 Employing Continuous Integration and Continuous Delivery
Fabric8 project 를 참고.

View File

@@ -0,0 +1,442 @@
---
share: true
toc: true
categories: [Development, Kubernetes]
tags: [kubernetes, sre, devops]
title: "18. Extending Kubernetes"
date: "2021-09-04"
github_title: "2021-09-04-18-extending-k8s"
image:
path: /assets/img/posts/k8s-18.jpeg
---
![k8s-18.jpeg](../../../assets/img/posts/k8s-18.jpeg) _API Server Aggregation (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-18)_
### 주요 내용
- Kubernetes 에 custom resource/controller/API server 추가하기
- Kubernetes Service Catalog
- OpenShift, Deis Workflow, Helm
## 18.1 Defining custom API objects
---
지금까지 책에서 살펴본 Kubernetes object 들은 비교적으로 low-level 한 object 이고 일반적인 개념을 다룬다.
그런데 Kubernetes ecosystem 이 발전하게 되면서 high-level object 를 만나게 되는데, 이러한 object 들은 일반적인 개념보다는 특정한 개념을 다루고, 애플리케이션 전체나 소프트웨어 서비스를 나타낸다.
Custom controller 를 사용하게 되면 이런 high-level object 도 관리할 수 있게 되며, Kubernetes 에서는 이렇게 custom resource 를 추가할 수 있는 방법을 제공해 준다.
### 18.1.1 Introducing CustomResourceDefinitions
새로운 resource type 을 정의하기 위해서는 **CustomResourceDefinition** (CRD) object 를 생성해서 API server 에 POST 요청을 날려주면 된다. 이 object 는 custom resource type 에 대한 정보를 담고 있으며, CRD 가 POST 되면 사용자가 JSON, YAML manifest 를 활용하여 API server 에 요청을 보내 resource 를 만들 수 있게 된다.
다만 CRD 만들게 되면 이와 연관된 controller 도 함께 만들어줘야 한다. (그래야 resource 관리가 된다)
#### CRD 예시
예시로 사용자들에게 static website 를 빠르게 배포하기 위해서 Website 라는 custom resource 만들어 준다고 해보자.
사용자에게 parameter 로 받을 부분은 웹사이트의 이름과 웹사이트에서 제공할 파일을 담고있는 곳(git repo)일 것이며, Website resource 가 만들어지면 webserver pod 가 실행되고, Service 도 하나 생겨서 자동으로 endpoint 에 추가되어 IP 로 접속 가능하게 만들고 싶을 것이다.
그렇다면 사용자가 Website resource 를 생성하기 위해 생성할 YAML 은 다음과 같은 형식일 것이다.
```yaml
kind: Website # Custom object
metadata:
name: kubia # 웹사이트의 이름
spec:
gitRepo: https://github.com/luksa/kubia-website-example.git # 파일 경로
```
사실 여기에 `apiVersion` field 도 필요한데, 뒤에서 설명하기로 한다.
#### CRD object 만들기
이제 진짜로 CRD 를 만들어 보도록 하자.
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: websites.extensions.example.com # Custom object 의 full name
spec:
scope: Namespaced # Namespace 에 종속될 것인가?
group: extensions.example.com # API group 과 버전
version: v1
names: # Custom resource 의 이름
kind: Website
singular: website
plural: websites
shortNames:
- ws
```
위와 같이 CRD 를 정의하고 `kubectl create` 로 생성 요청을 보내면 CRD 가 생성된다.
> 이름이 `websites.extensions.example.com` 으로 굉장히 긴데, 이는 이름 충돌을 방지하기 위해서이다. 실제로 Website object 를 생성할 때는 `names.kind` 의 값을 이용해서 `kind: Website` 라고 YAML 에 적을 것이므로 괜찮다.
API group 과 버전은 여기서 정하는대로 `apiVersion` field 의 값이 정해진다. 위와 같이 설정하면 Website 의 YAML manifest 에는 `apiVersion: extensions.example.com/v1` 으로 입력하게 된다.
#### Custom Resource 생성
이제 `apiVersion` 도 정해졌으니 생성을 해본다.
```yaml
apiVersion: extensions.example.com/v1
kind: Website
metadata:
name: kubia
spec:
gitRepo: https://github.com/luksa/kubia-website-example.git
```
Object 를 생성하고, `kubectl get website` (`shortNames` 을 이용해 `kubectl get ws`) 를 해보면 아래와 같이 object 가 생성된 것을 확인할 수 있다.
```
$ kubectl get ws
NAME AGE
kubia 44s
```
마찬가지로 `kubectl describe` 도 가능하며, `kubectl delete` 도 똑같이 동작한다.
하지만 object 만 생성한 것이지 아직 이 object 는 어떠한 동작도 하지 않는데, 이는 controller 가 존재하지 않기 때문이다.
> 물론 object 생성 목적이 어떤 동작을 하기 위해서는 아닐 수도 있다. ConfigMap 과 같은 경우 데이터만 저장하고 별도의 추가 동작이 없다. 대신 pod 들이 API server 에게 요청해서 값을 받아올 수 있는 것이다.
### 18.1.2 Automating custom resources with custom controllers
Website object 가 생성되었을 때 원하는 동작을 하게 만드려면 Website controller 를 생성해야 한다. Website controller 는 API server 를 watch 하고 있다가 Website object 가 생성되면 정의한 동작을 실행한다.
Website object 는 다음과 같이 설계할 것이다.
- 노드에 문제가 생기는 것을 대비하여 Deployment 안에 pod 를 띄울 것이다.
- Service 를 생성하여 pod 가 외부로 expose 되도록 한다.
#### Website controller 의 동작 이해
Controller 가 실행되면, Kubernetes API server 에 요청을 보내서 Website object 를 watch 하기 시작한다.
```
http://localhost:8001/apis/extensions.example.com/v1/websites?watch=true
```
(요청지가 `localhost:8001` 인 이유는 `kubectl proxy` 를 sidecar 컨테이너로 이용할 것이기 때문이다)
이제 API server 는 Website object 에 변경사항이 생기면 controller 에게 알려준다.
새로운 Website object 가 생성되면 `ADDED` event 를 돌려주게 되는데, 이 때 controller 는 Website 의 이름과 git repo URL 을 event 로부터 얻고, 해당 정보를 바탕으로 Service 와 Deployment 를 생성해달라는 요청을 API server 에 하게 된다.
만약 object 가 삭제될 때는 `DELETED` event 를 받고, 생성했던 resource 를 삭제해 달라고 API server 에 요청하게 된다.
#### Pod 로 controller 실행하기
> Controller 의 구현은 어려우므로, 구현된 image 를 저자가 제공해 주고 있다.
Controller 가 준비되었다면 Kubernetes 안에서 실행하기 위해 pod 로 만들어주면 된다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: website-controller
spec:
replicas: 1
template:
metadata:
name: website-controller
labels:
app: website-controller
spec:
serviceAccountName: website-controller
containers:
- name: main
image: luksa/website-controller
- name: proxy
image: luksa/kubectl-proxy:1.6.2
selector:
matchLabels:
app: website-controller
```
생성하기 전에 `website-controller` 는 ServiceAccount 를 만들어야한다. 만약 RBAC 가 활성화 되어있다면, ClusterRoleBinding 을 이용해서 권한을 부여해야 한다.
```bash
$ kubectl create -f website-controller.yaml
```
위와 같이 하면 Website controller 가 실행되고, 이제 Website 를 실행해보면 event 를 받은 controller 가 Deployment 와 Service 를 실행한다.
> 알 수 없는 이유로 실습에 실패했다... controller 코드를 뜯어봐야 할 것 같은데...
이렇게 하면 Kubernetes 사용자들이 작은 노력으로, Deployment, Service, Pod 에 대한 이해 없이 웹사이트를 배포할 수 있게 된다.
### 18.1.3 Validating custom objects
눈치가 빠르다면 위에서 CRD 를 생성할 때 `gitRepo` 와 같은 field 에 대한 schema 를 정의하지 않았음을 깨달았을 것이다. 즉 validation 에 대한 로직이 현재 없으므로, `gitRepo` field 가 없는 Website object 도 생성이 가능하다. (기본적인 `apiVersion`, `kind`, `metadata` 정도에 대한 validation 만 진행한다)
그렇다면 controller 에 validation 로직을 추가해서 API server 가 validation 에 실패한 manifest 를 받지 않도록 할 수 있을까? 아쉽게도 이는 불가능하다. Controller 가 event 를 애초에 받으려면 object 가 생성되어야 하는데, validation 이 실패할 object 라면 애초에 생성이 되지 말아야 한다. 또 Controller 는 object 생성에 대해서 event 만 받기 때문에 validation 을 추가하더라도 사용자가 에러를 즉시 확인할 수 없다.
사용자가 오류에 대해 즉시 알게 하기 위해서는 API server 의 CustomResourceValidation 기능을 활성화해야 하고, CRD 에 JSON schema 를 제공해야 한다.
> 현재 Kubernetes 공식 문서에는 CRD 의 `apiVersion` 이 beta 가 아니고 `apiextensions.k8s.io/v1` 이다. 또한 여기에는 `versions` 가 string 이 아닌 array 형태이며, 각 버전별로 `schema` 를 정의할 수 있게 되어있다.
### 18.1.4 Providing a custom API server for your custom objects
Custom object 를 사용하는 좋은 방법 중 하나로 자체적으로 API server 를 두는 방법이 있다.
#### API server aggregation
Kubernetes 1.7 부터 custom API server 를 Kubernetes API server 와 통합할 수 있다!
원래 Kubernetes API server 는 monolithic component 였는데, 1.7 부터는 여러 개로 나뉘었고, client 의 요청을 적절한 곳으로 잘 포워딩 해주도록 변경되었다.
그러므로, Website object 만 따로 핸들링 해주는 API server 를 만들 수 있다. 그러면 API server 가 내부적으로 validation 을 담당할 수 있게 된다. 또한 CRD 를 생성할 필요도 없어지는데, API server 내부에 Website object 에 대한 정보를 담아버리면 되기 때문이다.
일반적으로 각 API server 는 자신의 resource 를 자신이 저장한다. 그래서 자체적으로 `etcd` 를 가지고 있거나, main API server 의 `etcd` 를 사용할 수 있다. 후자의 경우 CRD 를 미리 생성해줘야 object 생성이 가능해진다.
#### Custom API server 등록하기
Custom API server 를 추가하기 위해서는 pod 를 띄워서 Service 를 이용해 expose 해야한다. 그리고 API server 와 통합을 위해서 APIService resource 의 manifest 를 작성하여 등록해야 한다.
```yaml
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.extensions.example.com
spec:
group: extensions.example.com # 이 API server 가 담당할 API group 및 version
version: v1alpha1
priority: 150
service: # 실제로 요청을 처리할 service
name: website-api
namespace: default
```
APIService 를 등록하고 나면, `apiVersion` 값이 `extensions.example.com/v1alpha1` 인 요청들은 `website-api` Service 로 포워딩된다.
#### Custom clients
필요하다면 `kubectl` 과 같은 CLI tool 을 자체 개발하여 custom resource 에 대한 더욱 풍부한 제어를 할 수도 있다.
## 18.2 Extending Kubernetes with the Kubernetes Service Catalog
---
Kubernetes 에서 '서비스' (Service 리소스 아님) 를 만들고 싶다면, 사용자가 pod, Service, Secret 등을 직접 만들어서 배포하거나, 혹은 이를 배포해 달라고 Ops 팀에 요청해야 했다.
하지만 Kubernetes 의 목표는 이런 것들을 쉽게 할 수 있게 하는 것이기에, 이상적으로는 사용자가 '나 PostgreSQL DB가 필요해' 라고 요청 하면 모든 것들이 알아서 만들어져야 한다. 이런 것들을 가능하게 해주는 것이 Kubernetes Service Catalog 이다.
### 18.2.1 Introducing the Service Catalog
Service Catalog 는 말 그대로 catalog 라서, 사용자들이 catalog 에서 필요한 구조가 있다면 바로 배포가 가능하게 해준다. 마치 앞에서 봤던 Website custom resource 와 유사하다.
Catalog 에서 제공하는 각 서비스 종류마다 custom resource 를 API server 에 추가하지는 않고, 4개의 generic API resource 를 사용한다.
- ClusterServiceBroker: 서비스를 생성할 수 있는 시스템
- ClusterServiceClass: 생성이 가능한 서비스의 종류
- ServiceInstance: 생성된 서비스의 인스턴스
- ServiceBinding: 생성된 서비스와 clients (pods) 의 대응 관계
간단하게 살펴보면, 클러스터 관리자는 클러스터 내에서 사용하고자 하는 서비스에 대해 ClusterServiceBroker 를 생성한다. 그러면 Kubernetes 가 broker 에게 어떤 서비스를 제공하는지 확인하여 각 서비스 마다 ClusterServiceClass 를 생성한다. 이제 사용자가 서비스 생성을 요청하면 ServiceInstance 가 생성되는 것이고, 이 ServiceInstance 와 pod 가 ServiceBinding 으로 연결되며, pod 에는 필요하다면 Secret 이 주입된다. (ServiceInstance 와의 연결에도 필요할 수 있다)
### 18.2.2 Introducing the Service Catalog API server and Controller Manager
Service Catalog 도 분산 시스템으로, 3개의 component 로 구성되어 있다.
- Service Catalog API server
- etcd
- Controller Manager (controller 가 실행되는 곳)
앞서 살펴본 4개의 generic API resource 는 모두 Service Catalog API server 에 YAML/JSON 을 POST 해서 생성한다. 그러면 이제 해당 resource 정보를 etcd 에 저장한다. (혹은 CRD 를 main API server 에 만들어서 그 곳의 etcd 에 저장하던가)
Controller Manager 의 controller 는 실제 resource 가지고 뭔가 하는데, 당연히 Service Catalog API server 와 통신하지만 이들은 직접 서비스를 생성하지는 않고, ServiceBroker 에게 그 일을 맡긴다.
(??? 뭘 한다는 건지 안 나와있음)
### 18.2.3 Introducing Service Brokers and the OpenServiceBroker API
클러스터 관리자는 Service Catalog 에 Service Broker 를 등록해야 하는데, Service Broker 는 반드시 OpenServiceBroker API 를 구현해야 한다.
#### OpenServiceBroker API
Service Catalog 는 API 를 이용해서 broker 와 통신한다.
- `GET /v2/catalog`: 서비스 목록 가져오기
- `PUT /v2/service_instances/:id`: 서비스 인스턴스 생성하기
- `PATCH /v2/service_instances/:id`: 서비스 인스턴스 수정하기
- `PUT /v2/service_instances/:id/service_bindings/:binding_id`: 서비스 인스턴스 바인딩하기
- `DELETE /v2/service_instances/:id/service_bindings/:binding_id`: 서비스 인스턴스 바인딩 해제하기
- `DELETE /v2/service_instances/:id`: 서비스 인스턴스 삭제하기
#### Service Catalog 에 broker 등록
Service Catalog API server 에 ServiceBroker resource manifest 를 POST 하면 된다.
```yaml
apiVersion: servicecatalog.k8s.io/v1alpha1
kind: Broker
metadata:
name: database-broker
spec:
url: http://database-osbapi.myorganization.org # OpenServiceBroker API URL
```
위 ServiceBroker 는 여러 종류의 DB 를 생성할 수 있는 가상의 broker 이다.
ClusterServiceBroker 가 생성되면, Controller Manager 의 controller 가 URL 에 접속하여 생성 가능한 서비스 목록을 조회한다. 서비스 목록을 얻어오면, ClusterServiceClass 를 각 서비스마다 생성한다. 예를 들면 'PostgreSQL DB' 가 하나의 ClusterServiceClass 이다.
각 ClusterServiceClass 에는 service plan 이 부여되어 있는데, 이는 요금제라고 생각하면 된다. 해당 서비스를 무료 티어로 이용할지, 프리미엄 티어로 이용할지 정할 수 있다.
#### Listing the available services in a cluster
`kubectl get serviceclasses` 로 사용할 수 있는 서비스 목록을 조회할 수 있다.
마지 6장에서 살펴본 StorageClasses 와 비슷하다. StorageClass 는 어떤 종류의 저장소를 생성할 수 있는지 알려줬다면 ServiceClass 는 생성 가능한 서비스 종류를 알려준다.
### 18.2.4 Provisioning and using a service
이제 실제로 서비스 하나를 생성해보자.
```yaml
apiVersion: servicecatalog.k8s.io/v1alpha1
kind: Instance # ServiceInstance 생성
metadata:
name: my-postgres-db
spec:
serviceClassName: postgres-database # ServiceClass 와 plan 을 선택
planName: free
parameters:
init-db-args: --data-checksums
```
위와 같이 Service 이름과 사용하는 ServiceClass 와 plan 을 선택하여 manifest 를 작성하고, 이를 POST 하면 된다. 이렇게 하면 ServiceInstance 가 생성된다.
이제 Service Catalog 는 ServiceClass 가 속해이있는 broker 에 연결해서 서비스를 생성하라고 요청한다. 그러면 실제 Kubernetes resource 생성은 온전히 broker 의 몫이 된다.
여기서 신기한 점은 broker 가 PostgreSQL DB 인스턴스를 생성하긴 하는데, 어디서 하는지 모른다는 점이다. 클러스터 내부에 만들 수도 있고, Kubernetes 안에서 만들지 않을 수도 있다. Service Catalog 나 이를 요청한 사용자나, 전혀 신경쓰지 않는다. 단지 기능이 동작하면 된다.
그래서 생성이 되었다고 치면, pod 에서 사용할 수 있어야 하는데, 이를 위해서는 bind 를 해줘야 한다.
#### Binding a ServiceInstance
ServiceBinding 을 생성하여 pod 과 서비스를 연결한다. 그 과정에서 통신을 위해 secret 을 사용할 수도 있다. (Access Key)
```yaml
apiVersion: servicecatalog.k8s.io/v1alpha1
kind: Binding
metadata:
name: my-postgres-db-binding
spec:
instanceRef:
name: my-postgres-db
secretName: postgres-secret # 필요한 secret 주입
```
여기서 이상한 점은 실제로 연결할 pod 의 정보는 없다는 점이다.
책이 쓰여질 당시에는 ServiceInstance 의 secret 을 Pod 에 직접 주입하는 방법이 없어서, pod 에서 이를 직접 mount 해야 했다.
> 추후 PodPresets 라는 기능이 나왔다고 한다.
ServiceBinding 을 생성하게 되면 controller 가 broker 와 연결하고 broker 가 binding 을 실제로 만들어주고, broker 는 연결을 위한 정보 (credentials etc.) 를 돌려준다. 이 연결을 위한 정보는 manifest 에서 정한 secret 에 저장된다. (자동 생성되는 것)
#### Client pod 에서 secret 사용하기
연결을 위한 정보다 secret 에 저장되어 있으므로, pod 에서는 이를 mount 해서 사용하면 되고, secret 에 들어있는 대표적인 정보로는 host URL, username, password, access key, API token 등이 있을 것이다.
### 18.2.5 Unbinding and deprovisioning
ServiceBinding 이 불필요해지면 `kubectl delete servicebinding` 으로 삭제하면 된다. 그러면 secret 이 제거되고 broker 가 unbinding 을 진행한다. 이 상태에서는 다시 ServiceBinding 을 만들 수 있다.
서비스 자체가 불필요해지면 `kubectl delete serviceinstance` 로 삭제하면 된다.
### 18.2.6 Understanding what the Service Catalog brings
Service Catalog 는 서비스 제공자들이 Kubernetes 를 통해 서비스를 제공할 수 있게 해준다.
해당 서비스가 쉽게 생성되기 때문에 애플리케이션 배포에 더욱 유용하게 사용할 수 있다.
## 18.3 Platforms built on top of Kubernetes
---
이처럼 Kubernetes 는 굉장히 유연하고 확장 가능한 시스템이지만, 몇몇 회사들은 플랫폼을 자체 개발하여 Kubernetes 위에서 동작하게 하고, 이를 서비스로 제공하고 있다. (PaaS, Platform-as-a-Service)
Deis Workflow 와 Red Hat OpenShift 에 대해서 알아본다.
### 18.3.1 Red Hat OpenShift Container Platform
Red Hat OpenShift 는 개발자에게 초점이 맞춰진 PaaS dlek. 애플리케이션의 빠른 개발과 손쉬운 배포, 스케일링, 장기간 유지보수를 목표로 한다.
OpenShift 는 Kubernetes 이전에 나왔어서, 버전 1~2 는 전혀 Kubernetes 와 무관했는데, 버전 3 부터는 재개발에 들어가 Kubernetes 위에서 돌아가도록 개편했다. Red Hat 이 기존 버전을 버리고 Kubernetes 위에서 동작하도록 다시 개발을 할 정도라면, Kubernetes 가 그만큼 좋다는 뜻이기도 하다.
Kubernetes 에서 rollout 와 scaling 을 자동화 해준다면, OpenShift 에서는 애플리케이션 이미지 빌드와 배포를 자동화하여 CI 를 사용하지 않아도 되게 해준다. 그리고 user, group 관리가 가능하여 보안도 신경쓸 수 있다.
#### Additional Resources in OpenShift
OpenShift 에서 추가로 제공되는 API object 들이 몇 가지 있다.
- User & Group
- Project
- Template
- BuildConfig
- DeploymentConfig
- ImageStream
- Route
- 기타
#### Users, Groups, Projects
OpenShift 에서는 User 를 만들어서 권한 통제를 확실하게 할 수 있으며, 특정 Project 에 접근 권한을 부여하거나 말소할 수 있다. 이러한 동작은 모두 클러스터 관리자가 수행한다.
Project 는 Kubernetes namespaces 와 동일하고, 추가로 annotation 이 들어간 것이다.
#### Application Templates
Kubernetes 에서는 YAML/JSON 으로 리소스를 생성하는데, OpenShift 에서는 manifest 가 parameterizable 하다. 그래서 OpenShift 에서는 Template 을 만들어 placeholder (변수 처럼) 를 뒀다가, 나중에 실제로 Template 으로부터 리소스를 생성할 때 parameter 를 넘겨주고 그 정보를 바탕으로 리소스를 생성한다.
OpenShift 에서 미리 제공되는 Template 도 있어서 몇 개의 parameter 만으로 복잡한 아키텍쳐를 구성할 수 있다.
#### Building Images from source using BuildConfigs
OpenShift 의 장점 중 하나는 git repo 의 소스 코드로부터 바로 이미지를 빌드하고 배포할 수 있다는 점이다. 직접 컨테이너 이미지를 빌드할 필요가 없어진다.
이 작업은 BuildConfig 가 해주는데, git repo 에 commit 이 발생하면 빌드를 trigger 하게 할 수 있다.
Source To Image 라는 내장된 빌드 기능이 있어서, git repo 를 보고 어떤 종류의 애플리케이션인지 감지하여 적절한 빌드 과정을 수행해 준다. 만약 `pom.xml` 이 발견되면 Maven 빌드를 수행한다.
#### Automatically deploying newly built images with DeploymentConfigs
DeploymentConfig 를 활용하면 BuildConfig 를 이용해 빌드한 이미지를 자동으로 클러스터에 배포할 수 있게 된다.
DeploymentConfig 는 ImageStream 을 참조하도록 되어있는데, ImageStream 은 말 그대로 이미지들의 스트림이고, 이미지가 빌드될 때 ImageStream 에 추가되는 구조이다. 그래서 DeploymentConfig 가 새로운 이미지를 감지하여 rollout 을 자동으로 수행한다.
#### Exposing services externally using Routes
Kubernetes 에 Ingress object 가 없던 시절에는 `NodePort``LoadBalancer` 타입의 Service 를 사용해야 했다. 이 때에도 OpenShift 에서는 Route 라는 대안을 제공해줬다. Ingress 와 유사하지만, TLS termination, traffic splitting 과 관련된 추가 설정이 가능하다.
Route 에는 Router 가 필요한데 얘는 load balancer 또는 proxy 의 역할을 한다.
### 18.3.2 Deis Workflow and Helm
#### Deis Workflow
Deis Workflow 는 Kubernetes 클러스터에 직접 배포한다. 실행하면 Service, ReplicationController 를 여러 개 생성하고, 개발자에게 간단하고 친숙한 환경을 제공해준다.
새로운 버전을 배포할 때는 `git push deis master` 만 입력하면 나머지는 Workflow 가 해결해 준다. OpenShift 와 마찬가지로 source to image 빌드 기능을 제공하고, rollout/rollback, edge routing 이 제공되며, Kubernetes 내장 기능에는 없는 로그 적재, 통계치, 알림 기능이 있다.
#### Deploying resources through Helm
Helm 은 Kubernetes 용 package manager 이다. 2가지로 구성되어 있는데, 하나는 `helm` CLI 와 Tiller 라는 서버 component 로 Kubernetes 클러스터 내 pod 형태로 실행된다.
이 2개의 component 를 사용해서 package 를 관리한다. Helm 애플리케이션 package 를 Chart 라고 부른다.
Chart 는 애플리케이션 설정을 담고 있는 Config 와 합쳐져 Release 가 된다. Release 가 되면 실행 중인 애플리케이션 인스턴스가 된다. Release 는 `helm` CLI 를 이용해서 관리하는데, `helm` CLI 가 Tiller 와 통신하여 Chart 에서 요구하는 Kubernetes 리소스를 모두 생성하도록 해준다.
우리가 Docker Hub 등에서 이미지를 pull 받아 사용하는 것처럼, 만약 Kubernetes 상에서 어떤 아키텍쳐나 애플리케이션이 배포하고 싶다면 Helm Chart 가 있는지 확인해보고 있으면 그것을 그대로 가져와서 사용할 수 있게 된다.
예를 들어 MySQL 을 실행하고 싶으면 Chart repo 를 clone 받은 다음 `helm install --name my-dayabase stable/mysql` 명령을 입력하면 알아서 Deployment, Service, Secret, PVC 를 만들어 준다.
---
마지막 장이다. 끝!