Kubernetes Upgrade 는 고려해야 할 사항이 많지만 Kubespray 를 활용하면 의외로 간단할 수 도 있다.
Kubespray 는 Kubernetes 설치에 kubeadm 을 활용하기 때문에 업그레이드는 Kubernetes 의 바로 윗단계 마이너버전까지만 가능하다. 예를 들어 지금 Kubernetes 버전이 v1.14.x 이면 v1.16.x 로는 업그레이드를 못한다. v1.16.x 로 업그레이드를 하기 위해서는 v1.15.x 로 업그레이드를 하고, v1.16.x 로 다시 업그레이드를 해야 한다.
마침 업그레이드가 필요했고 현재 설치된 Kubernetes 버전이 v1.14.3 이므로 v1.15.6 으로 업그레이드를 먼저 진행하였다.
Every 2.0s: kubectl get nodes Thu Jan 9 16:15:35 2020
NAME STATUS ROLES AGE VERSION
k1-master01 Ready master 323d v1.15.6
k1-master02 Ready master 323d v1.15.6
k1-master03 Ready master 323d v1.15.6
k1-node01 Ready ingress,node 323d v1.14.3
k1-node02 Ready node 323d v1.14.3
k1-node03 Ready node 322d v1.14.3
k1-node04 Ready node 323d v1.14.3
k1-node05 Ready node 323d v1.14.3
이제 Worker 를 업그레이드 할 차례인데, Worker 를 업그레이드는 고려해야 할 사항이 있다. 노드를 업그레이드 하기 위해서는 drain 을 하여 Pod 를 eviction 하는데 Kubespray 는 evction 이 완료될 때 까지 기다린다. DaemonSet 은 무시하는 옵션이 있지만 PodDisruptionBudget 은 policy 에 걸리면 pod 가 terminating 상태에서 완료되지 않기 때문에 timeout 으로 업그레이드에 실패하게 된다. 여기서는 elasticsearch 가 문제가 있어 timeout 나서 실패하였다.
# kubectl get PodDisruptionBudget -A
NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
istio-system istio-egressgateway 1 N/A 0 76d
istio-system istio-galley 1 N/A 0 76d
istio-system istio-ingressgateway 1 N/A 0 76d
istio-system istio-pilot 1 N/A 0 76d
istio-system istio-policy 1 N/A 0 76d
istio-system istio-telemetry 1 N/A 0 76d
logging elasticsearch-k1-master-pdb N/A 1 0 71d
elasticsearch 문제를 해결하고 다시 k1-node01 이라는 Worker 노드 1대만 업그레이드를 진행한다.
CNCF 프로젝트 중에 하나로, RBAC 으로 권한에 제한을 두었다고 하더라도 해당 권한의 사용자 컨테이너가 탈취당한다면 컨테이너 안에서 Docker 명령이나 Kubectl 명령으로 새로운 자원을 띄우는 것이 가능하다. 이런 비정상 상황에서 자원 생성을 막을 수 있는 것이 OPA의 gatekeeper 이다.
Kubernetes 의 모든 metadata 는 etcd 에 저장됩니다. etcd 를 안정적으로 가져가기 위해 3 node 로 cluster 를 구성하는 방법을 사용합니다. 대부분의 문서는 일반적으로 etcd 를 백업 / 복구로 설명하고 있는데, 그 보다 서버 fault 가 나서 etcd 를 1 node 나 2 node 로 임시 운영해야 할 경우에 대해서 설명하고자 합니다.
[ etcd 노드 리스트 ]
k2-master01 : etcd1
k2-master02 : etcd2
k2-master03 : etcd3
먼저 etcdctl 명령어를 사용하기 쉽게 alias 로 다음과 같이 등록을 해줍니다. 각 노드 마다 endpoint ip 와 인증서 파일은 이름을 적절히 변경해 주어야 합니다.
# vi ~/.bash_aliases
alias etcdctl="export ETCDCTL_API=3; etcdctl --endpoints=[192.168.30.151:2379] --cacert=\"/etc/ssl/etcd/ssl/ca.pem\" --cert=\"/etc/ssl/etcd/ssl/admin-k2-master01.pem\" --key=\"/etc/ssl/etcd/ssl/admin-k2-master01-key.pem\" "
# . ~/.bash_aliases
멤버 조회를 하면 다음과 같이 3개 노드가 정상적으로 나타납니다.
# etcdctl --write-out=table member list
+------------------+---------+-------+-----------------------------+-----------------------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
+------------------+---------+-------+-----------------------------+-----------------------------+
| 683a3374b573e7c7 | started | etcd1 | https://192.168.30.151:2380 | https://192.168.30.151:2379 |
| 6f89416652d7e034 | started | etcd2 | https://192.168.30.152:2380 | https://192.168.30.152:2379 |
| d9c4159e72350017 | started | etcd3 | https://192.168.30.153:2380 | https://192.168.30.153:2379 |
+------------------+---------+-------+-----------------------------+-----------------------------+
모든 노드의 etcd 를 stop 하여 3대 모두 장애상황으로 만듭니다.
# systemctl stop etcd
3대 노드의 etcd 가 모두 장애로 다운되었고, 빠른 서비스를 위해 etcd1 만 띄운다고 하면, etcd db 에 이미 기존의 3대 cluster member 를 인식하고 있기 때문에 etcd1 만 실행시키는 것을 실패하게 됩니다. 이 때는 기존의 db (백업 db 가 아닌 운영되고 있었던 db) 를 가지고 restore 를 하여 member 정보를 새롭게 update 할 수 있습니다. 그리고 snapshot 으로 백업된 db 정보가 아니므로 hash check 도 skip 을 해야 합니다.
k2-master01 노드에서 작업
먼저, db 를 복사해 옵니다. (etcd 의 data dir 는 /var/lib/etcd 라고 가정합니다.) 그리고 /var/lib/etcd 디렉토리는 삭제합니다.
# etcdctl --write-out=table member list
+------------------+---------+-------+-----------------------------+-----------------------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
+------------------+---------+-------+-----------------------------+-----------------------------+
| 683a3374b573e7c7 | started | etcd1 | https://192.168.30.151:2380 | https://192.168.30.151:2379 |
+------------------+---------+-------+-----------------------------+-----------------------------+
etcd 3 node cluster 원상 복구
k2-master01 서버에서 작업
etcd cluster 를 다시 3 node 로 만들기 위해 etcd2 member 를 add 합니다.
# etcdctl member add etcd2 --peer-urls=https://192.168.30.152:2380
Member d8f69cbfe4c8a34f added to cluster a6552752c1542947
ETCD_NAME="etcd2"
ETCD_INITIAL_CLUSTER="etcd1=https://192.168.30.151:2380,etcd2=https://192.168.30.152:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
k2-master02 서버에서 작업
etcd.env 에서 initial cluster 값에 자기 자신인 etcd2 를 추가합니다. initial cluster state 는 existing 으로 그대로 진행합니다.
# vi /etc/etcd.env
ETCD_INITIAL_CLUSTER=etcd1=https://192.168.30.151:2380,etcd2=https://192.168.30.152:2380
etcd2 의 data dir 를 삭제하고 etcd2 ㅇ를 실행합니다.
# rm -rf /var/lib/etcd/
# systemctl start etcd
member 조회를 하면 정상적으로 etcd2 가 추가된 것을 알 수 있습니다.
# etcdctl --write-out=table member list
+------------------+---------+-------+-----------------------------+-----------------------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
+------------------+---------+-------+-----------------------------+-----------------------------+
| 683a3374b573e7c7 | started | etcd1 | https://192.168.30.151:2380 | https://192.168.30.151:2379 |
| d8f69cbfe4c8a34f | started | etcd2 | https://192.168.30.152:2380 | https://192.168.30.152:2379 |
+------------------+---------+-------+-----------------------------+-----------------------------+
k2-master01 서버에서 작업
etcd3 member 를 add 합니다.
# etcdctl member add etcd3 --peer-urls=https://192.168.30.153:2380
Member 63f37d54154a6e23 added to cluster a6552752c1542947
ETCD_NAME="etcd3"
ETCD_INITIAL_CLUSTER="etcd3=https://192.168.30.153:2380,etcd1=https://192.168.30.151:2380,etcd2=https://192.168.30.152:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
k2-master03 서버에서 작업
etcd3 의 data dir 삭제 및 etcd3 를 start 합니다.
# rm -rf /var/lib/etcd
# systemctl start etcd
정상적으로 member 가 추가되었음을 알 수 있습니다.
# etcdctl --write-out=table member list
+------------------+---------+-------+-----------------------------+-----------------------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
+------------------+---------+-------+-----------------------------+-----------------------------+
| 63f37d54154a6e23 | started | etcd3 | https://192.168.30.153:2380 | https://192.168.30.153:2379 |
| 683a3374b573e7c7 | started | etcd1 | https://192.168.30.151:2380 | https://192.168.30.151:2379 |
| d8f69cbfe4c8a34f | started | etcd2 | https://192.168.30.152:2380 | https://192.168.30.152:2379 |
+------------------+---------+-------+-----------------------------+-----------------------------+
마지막으로 설정을 변경해야 할 것이 있습니다. 언제든지 etcd 는 restart 를 할 수 있으므로 etcd1 과 etcd2 의 initial cluster state 를 existing 으로 변경하고 initial cluster 의 멤버를 3대로 모두 지정해야 합니다.
k2-master02 서버에서 작업
etcd2 옵션의 initial cluster 값에 etcd3 를 포함
# vi /etc/etcd.env
ETCD_INITIAL_CLUSTER=etcd1=https://192.168.30.151:2380,etcd2=https://192.168.30.152:2380,etcd3=https://192.168.30.153:2380
k2-master01 서버에서 작업
etcd1 옵션의 initial cluster state 를 existing 으로 변경하고 initial cluster 값에 etcd2 와 etcd3 를 포함
# vi /etc/etcd.env
ETCD_INITIAL_CLUSTER_STATE=existing
ETCD_INITIAL_CLUSTER=etcd1=https://192.168.30.151:2380,etcd2=https://192.168.30.152:2380,etcd3=https://192.168.30.153:2380
Ingress Controller 를 사용하여 같은 도메인에서 subpath 로 웹서비스를 분리하는 방법은 각 서비스마다 Ingress 를 만들고, '/' path 는 무조건 포함시키는 방법입니다. 한가지 단점은 root path '/' 는 처음 만든 Ingress 가 먼저 선점하게 됩니다.
물론 이 방법은 '/' 가 경로가 반드시 필요한 서비스가 2개 이상이면 적용할 수 없지만 그런 경우는 없다고 가정합니다.
예를 들어 Jupyterlab 은 '/' 경로를 반드시 필요로 합니다. '/jupyter' 를 호출할 때 jupyterlab 이 호출되게 Ingress 의 subpath 를 지정하려면 다음과 같이 작성하면 됩니다.
여기서 두번째에 들어가 '/' 는 소용이 없습니다. Nginx 에서는 처음 입력된 순서에 의해서 location 이 결정되기 때문에 'http://jupyterlab.myserver/' 를 호출하면 첫번째 생성한 jupyterlab ingress 가 적용되기 때문입니다.
웹서비스를 인터넷에 노출시키기 위해서는 Cloud Provider 가 제공하는 Load Balancer 나 On-Prem 에서 Load Balancer 나 Ingress Controller 를 사용해야 합니다. Ingress Controller 를 사용할 때 원래의 Client IP 를 확인하는 방법은 다음과 같습니다.
여기서 사용하는 Ingress Controller 와 Pod 의 Web Server 는 Nginx 를 사용했습니다.
1. [ 인터넷 ] ---> [ Ingress Controller ] ---> [ Pod (Web Server) ]
가장 기본적인 방법으로 추가 세팅없이도 바로 original client ip 를 알 수 있습니다.
set_real_ip_from 은 ingress controller 가 실행될 수 있는 서버의 cidr 입니다.
2. [ 인터넷 ] ---> [ Google or Azure LB ] ---> [ Pod (Web Server) ]
Cloud Provider (Google or Azure) 의 LB 를 이용하고 Service 의 LoadBalancer type 으로 웹서비스를 한다면 아래와 같이 Service 의 externalTrafficPolicy 값을 local 로 지정하면 됩니다. local 로 지정하면 LB 에서 바로 Web Server Pod 가 떠 있는 서버로만 접근되기 때문에 다른 서버를 통해서 연결될 때 client ip 가 해당 서버의 ip 로 들어가는 것을 방지해 줍니다.
---
kind: Service
apiVersion: v1
metadata:
name: example-service
spec:
ports:
- port: 8765
targetPort: 9376
selector:
app: example
type: LoadBalancer
externalTrafficPolicy: Local
Azure 부분은 한국 MS 의 박인혜 차장께서 알려주셨습니다.
3. [ 인터넷 ] ---> [ AWS ELB (classic lb) ] ---> [ Pod (Web Server) ]
Classic LB 에 proxy protocol 을 enable 하고, Service 의 annotations 에 다음과 같이 설정합니다.
AWS NLB 의 각 타겟 그룹에 Proxy Protocol V2 를 설정한 후, Ingress Controller 의 Service annotations 에 다음과 같이 설정합니다.
# by default the type is elb (classic load balancer).
service.beta.kubernetes.io/aws-load-balancer-type: nlb
# Enable PROXY protocol
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: 'tcp'
Ingress Controller 를 활용한 웹서비스에 TLS 를 적용하는 방법 중 가장 쉬운 방법은 Ingress Controller 에 TLS 를 적용하는 방법입니다. 이 방법은 client 와 Ingress Controller 까지 https 로 통신하고 Ingress Controller 와 web server 와는 http 로 통신하게 됩니다.
아래의 방법은 Ingress Controller 는 미리 설치되어 있다고 가정합니다.
먼저, letsencrypt 로 접속하여 3개월간 무료로 인증서를 받는 방법을 알아봅니다.
중요한 것은 인터넷에서 해당 도메인으로 로컬 서버에 접속이 되어야 하며 (Inbound 가능) standalone 을 사용할 때 로컬에 80 포트는 unbind 되어 있어야 합니다. 아래와 같은 경우는 외부에서 cloudnativeday.kr, www.cloudnativeday.kr, test.cloudnativeday.kr 도메인이 갖는 ip address 가 내 로컬 서버여야 한다는 의미입니다. 정확히는 해당 ip address 로 외부에서 내 서버로 연결이 가능해야 합니다.
인증서 발급이 완료되면 다음과 같은 디렉토리에 인증서가 생성되어 보입니다. archive 디렉토리와 심볼릭 링크가 있는 live 디렉토리가 있는데 live 디렉토리를 활용합니다. 그리고, crt 는 fullchain.pem 을, key 는 private.pem 을 사용합니다.
http 로 들어오면 자동으로 https 로 redirect 됩니다. 왜냐하면 기본 값이 nginx.ingress.kubernetes.io/ssl-redirect: "true" 이기 때문입니다. 일반 http 와 다른 부분은 tls 부분입니다. secret 과 인증서가 적용될 도메인명을 적으면 됩니다.
# kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kubernetes-admin@cluster.local cluster.local kubernetes-admin
seungkyua@k1 cluster.local seungkyua
# kubectl config use-context seungkyua@k1
# kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
kubernetes-admin@cluster.local cluster.local kubernetes-admin
* seungkyua@k1 cluster.local seungkyua seungkyua
Kubernetes 에 접속하여 해당 계정이 해당 네임스페이스에만 접속 권한이 있는지 확인
# kubectl get pods
No resources found.
# kubectl get pods -n defaults
Error from server (Forbidden): pods is forbidden: User "seungkyua" cannot list resource "pods" in API group "" in the namespace "defaults"
You can easily add your custom logic without modifying kubespray.
The my-kubespray has serveral parameters to override and can deploy ceph rbd provisioner into namespace.
Prerequisites
All servers must have the same user, and the user must set root permissions to NOPASSWD in sudo.
In the deployment node (or master 1 node), you need to enable ssh access to all other servers (including localhost) without a password to the corresponding account.
Register all cluster servers in the /etc/hosts file.
Time synchronization between servers with ntp.
Enable network restart with systemctl restart network.service .
After installation, you can find the artifacts which are kubectl and admin.conf in inventory/mycluster/artifacts directorin on deploy node(ansible runner).
$ ls -al inventory/mycluster/artifacts total 173620 drwxr-x--- 2 seungkyua seungkyua 4096 Feb 22 05:25 . drwxrwxr-x 4 seungkyua seungkyua 4096 Feb 22 02:38 .. -rw-r----- 1 seungkyua seungkyua 5449 Feb 22 05:25 admin.conf -rwxr-xr-x 1 seungkyua seungkyua 177766296 Feb 22 05:25 kubectl -rwxr-xr-x 1 seungkyua seungkyua 65 Feb 22 05:25 kubectl.sh
If desired, copy admin.conf to ~/.kube/config and kubectl to /usr/local/bin/kubectl
Kubernetes Node 가 NotReady 인 경우 해결을 위해서 제일 먼저 해당 노드의 syslog 를 확인 하는 것이 좋다.
## Node 의 syslog 확인 (ubuntu 인 경우)
# vi /etc/log/syslog
Feb 20 12:14:49 k1-node01 kubelet[31100]: E0220 12:14:49.401890 31100 raw.go:146] Failed to watch directory "/sys/fs/cgroup/devices/system.slice": inotify_add_watch /sys/fs/cgroup/devices/system.slice/run-ref5f878051f440b98dfd9bd843a01b58.scope: no space left on device
Feb 20 12:14:49 k1-node01 kubelet[31100]: F0220 12:14:49.401909 31100 kubelet.go:1369] Failed to start cAdvisor inotify_add_watch /sys/fs/cgroup/devices/system.slice/run-ref5f878051f440b98dfd9bd843a01b58.scope: no space left on device
Feb 20 12:14:49 k1-node01 kubelet[31100]: E0220 12:14:49.640153 31100 kubelet.go:2266] node "k1-node01" not found
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Main process exited, code=exited, status=255/n/a
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Unit entered failed state.
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Failed with result 'exit-code'.
위와 같이 inotify_add_watch 에서 no space left on device 로 에러가 날 경우에 sysctl 로 값을 늘려줘야 한다.
## fs.inotify.max_user_watches 값을 늘려줌
# vi /etc/sysctl.d/99-sysctl.conf
net.ipv4.ip_forward=1
net.ipv4.ip_local_reserved_ports=30000-32767
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-arptables=1
net.bridge.bridge-nf-call-ip6tables=1
fs.inotify.max_user_watches=1048576
해당 값을 적용하고 나서 kubelet 이 activating 상태이거나 failed 상태인 경우가 많으므로 아래와 같이 kubelet 을 재 시작해 준다.