1. kubeadm 소개 & 인프라 구성
kubeadm은 쿠버네티스가 제공하는 클러스터를 빠르게 구축할수 있도록 도와주는 도구입니다. kubeadm 외에도 로컬환경에서 간단하게 쿠버네티스를 구축해볼수 있는 minikube, 클라우드 프로비저닝을 기본으로 제공해주는 kops 등이 있지만, 이번 실습에서는 클라우드를 사용하지 않을예정이어서 kubeadm을 선택했습니다.
쿠버네티스 클러스터 구성시 권장사항은 각 노드당 듀얼코어, 2GB 메모리를 갖추는것이지만 학습목적이므로 크게 신경쓰지 않았습니다. 마침 친구에게 받은 라즈베리파이가 있어서 이를 worker-node, 개인 노트북을 master-node로 사용하고 집에서 쓰던 공유기를 라우터로 활용하여 쿠버네티스 클러스터를 위한 내부망을 구성했습니다.
제 PC는 윈도우 운영체제를 사용하고 있지만 kubeadm는 macos나 windows를 공식적으로 지원하지 않으므로 hyper -v 가 지원하는 가상화기술을 사용하여 ubuntu 18.04 기반의 가상머신을 구축하여 master-node로 사용했습니다.
2. 컨테이너 계층 구성
서비스 운영을 위해 web server, web application server, database의 3계층으로 구성해보았습니다. web server replicaset의 경우 NodePort Service를 사용하여 사용자들로부터 request를 직접받아 static, media 파일을 처리할수 있도록 설계해 봤습니다.
web server가 dynamic한 처리가 필요할때는 request를 was replicaset으로 전달할수 있는 매개체와 web server replicaset 역시 Database에 접근하기 위한 매개체가 필요하므로 clusterIP service를 하나씩 생성해주었습니다.
사실 모든 pod들은 클러스터 내부에서 할당받은 IP로 접근하면 되므로 service 객체들이 필요없다고 생각할수 있겠지만, service 객체를 사용하면 내부 DNS의 도움을 받을수 있고 로드밸런싱 기능을 지원하기 때문에 사용하는것이 좋습니다.
3. master, worker 노드에 docker 설치하기
kubernetes 를 설치하는데 앞서서 docker-engine의 설치가 필요합니다. kubernetes는 OCI 조건을 만족하는 컨테이너 서비스를 만족한다면 어떤 서비스를 설치해도 상관없지만 제가 잘하는 docker를 쓰기로 했습니다. docker 설치를 위한 스크립트를 아래와 같이 정리했지만 공식 메뉴얼을 보고 설치하는것도 추천드립니다.
(링크: https://docs.docker.com/engine/install/ubuntu/ )
sudo apt-get update sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88
docker 설치하는 과정에서 라즈베리파이와 제 개인 PC의 필요한 명령어가 약간 다른 구간이있습니다. CPU의 아키텍쳐가 다르기때문으로 확인하고 아키텍쳐와 일치하는 명령어를 사용해야합니다.
# raspberry pi (armhf) sudo add-apt-repository \ "deb [arch=armhf] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" # PC (x86_64/amd64) sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable"
마지막으로 다음 명령어를 사용하여 도커 엔진과 클라이언트을 설치 해주고 docker group을 생성하여 권한을 설정해주면 끝납니다.
sudo groupadd docker sudo usermod -aG docker $USER
4. master, worker 노드에 쿠버네티스 환경 설치하기
쿠버네티스 환경을 위해서는 모든 노드의 swap 기능를 먼저 멈춰야 한다. swap기능을 모두 멈춘다음에 다음 스크립트를 실행하면 kubeadm을 포함한 kubernetes의 운영에 필요한 모듈들을 설치할수 있습니다. 설치에 필요한 스크립트를 정리해두긴 했지만 다음 공식문서를 보고 설치하는것을 권장합니다.
( 링크: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/ )
# swap 비활성화 swapoff -a # kubernetes 설치 sudo apt-get update curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb https://apt.kubernetes.io/ kubernetes-xenial main EOF sudo apt-get update sudo apt-get install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl
5. 쿠버네티스 클러스터 생성하기
master node 에서sudo kubeadm init --pod-network-cidr=10.244.0.0/16
명령어를 통해 클러스터를 생성할수 있습니다. pod network로 10.244.0.0/16
블록을 사용하는 이유는 이후 설치할 CNI인 Flannel의 기본 pod network 블록이기 때문입니다. 만약 아래의 화면이 뜨면 클러스터가 정상적으로 생성된것입니다.
Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.0.8:6443 --token nb7nfw.yaywk4lpag2klkxz \ --discovery-token-ca-cert-hash sha256:965d27bd44661965523cad524ccbb26f63a7b4cd58ec1203ab8b042ca6450eca
위 과정을 진행할때 라즈베리파이에서 cgroup-xxxxx missing 관련 에러가 뜨는 현상이 있어서 관련자료를 찾아보았는데 아래의 문서의 prep.sh를 보고 해결할수 있었습니다. 명령어로도 정리해보았고 실행해준다음 라즈베리 파이를 재시작 하고 kubeadm 초기화를 하면 해결됩니다.
( https://gist.github.com/alexellis/fdbc90de7691a1b9edb545c17da2d975 )
sudo cp /boot/cmdline.txt /boot/cmdline_backup.txt orig="$(head -n1 /boot/cmdline.txt) cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory" echo $orig | sudo tee /boot/cmdline.txt
kubeadm init에 성공하면 화면에 보이는 3줄짜리 스크립트를 실행시켜주고 pod 간에 통신을 위한 대표적인 네트워크 플러그인인 Flannel를 설치해줍니다. 그리고 성공화면의 맨 마지막줄에 있는 kubeadm join으로 시작하는 구문은 worker node에서 사용해야 하므로 메모장에 킵해둡시다.
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config # Flannel install kubectl apply -f https://github.com/coreos/flannel/raw/master/Documentation/kube-flannel.yml
Flannel 까지 정상적으로 설치가 되었으면 sudo kubectl get nodes -n kube-system
명령어를 통해 쿠버네티스 pod들이 전부 정상적으로 running되어 있는지 확인합니다. 모든 pod들의 status가 Running이고 Ready가 1/1 이면 클러스터가 정상적으로 구축되었고 master node에서 해야할일은 끝났습니다.
NAME READY STATUS RESTARTS AGE coredns-66bff467f8-4nw7q 1/1 Running 0 116s coredns-66bff467f8-6p8ck 1/1 Running 0 116s etcd-kube-master 1/1 Running 0 2m5s kube-apiserver-kube-master 1/1 Running 0 2m5s kube-controller-manager-kube-master 1/1 Running 0 2m5s kube-flannel-ds-amd64-l7lrv 1/1 Running 0 33s kube-proxy-7zw6t 1/1 Running 0 116s kube-scheduler-kube-master 1/1 Running 0 2m5s
이제 여러분의 worker node만 생성된 클러스터에 join시키면 끝납니다. 메모장에 keep 해두었던 명령어를 worker node에서 실행시켜줍니다. 그리고 master node에서 sudo kubectl get nodes
, sudo kubectl get pods -n kube-system
을 실행시켜서 두개의 노드와 pod들이 잘실행되는지 확인한후 우리가 사용할 네임스페이스 오브젝트를 하나 만들어주면 쿠버네티스 클러스터 구축이 완료됩니다.
# sudo kubectl get nodes NAME STATUS ROLES AGE VERSION kube-master Ready master 15m v1.18.3 ubuntu Ready <none> 12m v1.18.3 # sudo kubectl get pods -n kube-system coredns-66bff467f8-x5vfs 1/1 Running 0 72s coredns-66bff467f8-x9vdk 1/1 Running 0 72s etcd-kube-master 1/1 Running 0 14m kube-apiserver-kube-master 1/1 Running 0 14m kube-controller-manager-kube-master 1/1 Running 0 14m kube-flannel-ds-amd64-l7lrv 1/1 Running 0 13m kube-flannel-ds-arm-xbd55 1/1 Running 0 12m kube-proxy-7zw6t 1/1 Running 0 14m kube-proxy-cqrl2 1/1 Running 0 12m kube-scheduler-kube-master 1/1 Running 0 14m # namespace 생성 kubectl create namespace production
6. Database Layer
database layer는 postgres 기반으로 동작할수 있게 설계했습니다. 배포에 필요한 deployment와 clusterip service를 yaml 형식으로 다음과 같이 작성했습니다.
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: knufestival2019-db namespace: production spec: selector: matchLabels: app: knufestival2019-db template: metadata: name: knufestival2019-db labels: app: knufestival2019-db spec: containers: - name: knufestival2019-db image: postgres:12.2-alpine envFrom: - secretRef: name: knufestival2019-db-env
# service.yaml apiVersion: v1 kind: Service metadata: name: knufestival2019-db-service namespace: production spec: ports: - name: knufestival2019-db-port port: 5432 targetPort: 5432 selector: app: knufestival2019-db type: ClusterIP
was 계층은 내부 DNS의 도움으로 위의 service.yaml에 명시된 metadata.name인 knufestival2019-db-service를 host로 접근하여 데이터베이스에 접근할수 있게 됩니다. 우선 docker hub를 참고하여 필요한 환경변수를 secret으로 만들고 아래의 명령어로 deployment와 service를 배포합니다.
# 시크릿 생성 kubectl create secret generic knufestival2019-db-env --from-env-file=.env -n production # 데이터 베이스 레이어 배포 kubectl apply -f deployment.yaml kubectl apply -f service.yaml # 정상적으로 배포되었는지 확인 kubectl get pods -n production kubectl get services -n production
7. web application server Layer
was의 경우 쿠버네티스가 제공해주는 replicaset을 사용하여 두개의 컨테이너로 구성해 가용성을 높혀보았습니다. 처음 was를 구동시키면 database에 테이블이 아무것도 없는 상황이기 때문에 정상 작동이 불가합니다. 이를 해결하기 위해서 쿠버네티스가 제공하는 Job 오브젝트를 사용해 migrate를 진행하는 임시 컨테이너를 사용했습니다. Job 오브젝트의 경우 컨테이너에 정의된 명령이 끝나면 자동으로 종료되어 사용자가 힘들게 지울 수고를 덜어줍니다. database layer와 마찬가지로 배포에 필요한 deployment와 clusterip service를 yaml 형식으로 다음과 같이 작성했습니다.
apiVersion: apps/v1 kind: Deployment metadata: name: knu-festival-was namespace: production spec: replicas: 2 selector: matchLabels: app: knu-festival-was template: metadata: labels: app: knu-festival-was spec: containers: - name: web image: lunacircle4/knufestival2019:1.0.0 envFrom: - secretRef: name: knufestival2019-was-env --- apiVersion: batch/v1 kind: Job metadata: name: knu-festival-migrator namespace: production spec: template: spec: containers: - name: knu-festival-migrator image: lunacircle4/knufestival2019:1.0.0 command: ["/bin/sh"] args: ["-c", "python3 manage.py migrate --settings=config.environments.production;"] envFrom: - secretRef: name: knufestival2019-was-env restartPolicy: Never backoffLimit: 4
web server 계층은 내부 DNS의 도움으로 위의 service.yaml에 명시된 metadata.name인 knufestival2019-was-service를 host로 접근하여 was 계층에 접근할수 있게 됩니다. 장고 배포에 필요한 환경변수를 secret (데이터베이스에 접근하기 위핸 host, password, user 정보 등등이 필요합니다) 으로 만들고 아래의 명령어로 deployment와 service를 배포합니다.
# 시크릿 생성 kubectl create secret generic knufestival2019-was-env --from-env-file=.env -n production # 데이터 베이스 레이어 배포 kubectl apply -f deployment.yaml kubectl apply -f service.yaml # 정상적으로 배포되었는지 확인 kubectl get pods -n production kubectl get services -n production
8. web server Layer
web server layer도 was와 유사하게 쿠버네티스가 제공해주는 replicaset을 사용하여 두개의 컨테이너로 구성해 가용성을 높혔습니다. web server layer는 다른 계층과 다르게 NodePort Service의 생성이 필요합니다. 쿠버네티스 클러스터 내부에선 사용할수 있는 ClusterIP Service와 달리 NodePort Service를 활용하면 엔드 유저의 request를 직접 handle 할수 있습니다. database was layer와 마찬가지로 배포에 필요한 deployment와 clusterip service를 yaml 형식으로 다음과 같이 작성했습니다.
# deployment apiVersion: apps/v1 kind: Deployment metadata: name: knufestival2019-web namespace: production spec: replicas: 2 selector: matchLabels: app: knufestival2019-web template: metadata: name: knufestival2019-web labels: app: knufestival2019-web spec: containers: - name: knufestival2019-web image: lunacircle4/knufestival2019-nginx:latest volumeMounts: - name: knufestival2019-web-conf mountPath: /etc/nginx/nginx.conf subPath: nginx.conf volumes: - name: knufestival2019-web-conf configMap: name: knufestival2019-web-conf
# service apiVersion: v1 kind: Service metadata: name: knufestival2019-web-service namespace: production spec: ports: - name: knufestival2019-web-port port: 80 targetPort: 80 selector: app: knufestival2019-web type: NodePort
쿠버네티스는 configMap이라는 환경변수를 관리할수 있는 오브젝트를 제공합니다. 암호화 되어 저장되지 않는점에서 secret 오브젝트와 차이점이 있고 일반적인 환경변수 뿐만아니라 파일도 configMap으로 관리할수 있는것이 특징입니다. 저는 configMap을 활용하여 nginx.conf 파일을 configMap으로 만들고 web server에서 기본설정으로 사용할수 있도록 바인딩 시켰습니다. 제가 작성한 nginx.conf 파일과 configMap 생성과 web server 배포에 필요한 명령어를 아래에 정리해보았습니다.
# nginx.conf worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; server_name 192.168.0.5; location /static { root /; } location /media { root /; } location / { proxy_pass http://knufestival2019-was-service; } }
# 파일로부터 configMap 생성 kubectl create configmap knufestival2019-web-conf --from-file=nginx.conf -n production # 데이터 베이스 레이어 배포 kubectl apply -f deployment.yaml kubectl apply -f service.yaml # 정상적으로 배포되었는지 확인 kubectl get pods -n production kubectl get services -n production
아래의 명령어를 사용하여 NodePort가 라즈베리파이와 PC의 어떤 포트에 바인딩 되었는지 확인하고 웹브라우저로 접속해봅니다.
# port 확인 kubectl get services -n production # knufestival2019-web-service 는 32181번으로 NodePort가 설정되어있다. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE knufestival2019-db-service ClusterIP 10.110.95.252 <none> 5432/TCP 6h48m knufestival2019-was-service ClusterIP 10.98.124.18 <none> 80/TCP 80m knufestival2019-web-service NodePort 10.100.63.118 <none> 80:32181/TCP 76m
9. 회고
라즈베리파이와 개인PC를 연동하여 클러스터를 구성하면서 정신이 아찔했던 순간이 있었습니다. 두 컴퓨팅 자원의 CPU 아키텍쳐가 다르다는점을 간과한것입니다. PC에서 잘돌아가는 이미지가 라즈베리파이에서는 아키텍쳐가 달라 오류가 발생할 확률이 매우(?) 높으므로 클러스터를 구성할때는 동일한 CPU 아키텍처를 사용하는 컴퓨터들을 사용하는것을 권장드립니다.
현재 데이터베이스 레이어의 경우 컨테이너가 다운되면 자료가 모두 날라가는 결함이 있습니다. 다음번에는 퍼시스턴스 볼륨 오브젝트을 활용하여 해결해보고 싶고, 데이터베이스를 마스터, 슬레이브로 구성하여 read replica하는 작업도 다음번에 해보고 싶은 생각이 듭니다. 이번엔 야매로 쿠버네티스 클러스터를 구성해보았지만 다음번엔 kops 등을 활용하여 클라우드상에 클러스터를 구성해보고 오토스케일링 같은 재미있는 주제도 다뤄보고 싶습니다.
이번에 인생 최초로 쿠버네티스라는 서비스를 공부하고 클러스터를 구성해보았는데, 도커를 공부하고 활용하면서 부족하다고 생각했던 기능들이 쿠버네티스에 몽땅 들어가 있어서 놀라웠고, CI/CD 파이프라인을 구성할때 큰 도움을 받을수 있다고 생각했습니다. 다만 공부해야할 내용이 생각보다 많아서 소규모의 시스템을 운영할때는 배보다 배꼽이 더커지는 격이 될수 있을것 같습니다.
긴글 읽어주셔서 감사합니다!
10. 참고자료
<kubernetes>
https://kubernetes.io/docs/concepts/configuration/secret/
https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
https://kubernetes.io/docs/concepts/workloads/controllers/job/#writing-a-job-spec
<nginx>
http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header
https://hub.docker.com/_/nginx
https://stackoverflow.com/questions/34326704/how-do-i-delete-virtual-interface-in-linux/34326753
<gunicorn>
https://docs.gunicorn.org/en/stable/settings.html
<raspberry pi>
https://gist.github.com/alexellis/fdbc90de7691a1b9edb545c17da2d975
(아 힘들다…)