Skip to main content

쿠버네티스 환경 구성하기

쿠버네티스 환경을 구성하기 전 먼저 아래와 같은 조건들을 살펴봐야 합니다.

kubeadm 설치 조건

  • 2GB 이상의 램을 장착한 머신 (이보다 작으면 사용자의 앱을 위한 공간이 거의 남지 않음)
  • 2코어 이상의 CPU
  • 클러스터의 모든 머신에 걸친 전체 네트워크 연결
  • 모든 노드에 대해 고유한 호스트 이름
  • 컴퓨터의 특정 포트 개방
  • kubelet이 제대로 작동하게 하려면 반드시 스왑을 사용하지 않도록 설정한다.

따라서 저는 우분투 22.04 이미지 인스턴스의 2코어 램 4기가인 마스터 인스턴스 1개와 노드 인스턴스 2개를 생성했습니다. 각 보안 그룹은 마스터 인스턴스의 보안그룹과 노드 인스턴스의 보안그룹으로 나누어 적용하였습니다.

패키지 설치

sudo apt-get install xclip -y

.bashrc 수정

# 자동완성 기능
source <(kubectl completion bash) # set up autocomplete in bash into the current shell, bash-completion package should be installed first.
echo "source <(kubectl completion bash)" >> ~/.bashrc # add autocomplete permanently to your bash shell.

# 단축키 설정
alias k=kubectl
complete -o default -F __start_kubectl k

# 복사, 붙여넣기
alias pbcopy='xclip -selection clipboard'
alias pbpaste='xclip -selection clipboard -o'

.vimrc 수정

# vi .vimrc
au BufNewFile,BufReadPost *.{yaml,yml} set filetype=yaml
autocmd FileType yaml setlocal ai ts=2 sts=2 sw=2 expandtab
if has("syntax")
syntax on
endif
set shiftwidth=2
set tabstop=2
set laststatus=2
set encoding=utf-8
set number
set showmatch
set path+=**
set hlsearch
set clipboard=unnamedplus
set mouse=a
set termguicolors
set viewoptions-=options
set autowrite
set statusline=\ %<%l:%v\ [%P]%=%a\ %h%m%r\ %F\
# 표준시간대 변경 / master, node1, node2
rm -f /etc/localtime
ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
date

# 각 인스턴스별로 호스트를 설정합니다.
hostnamectl set-hostname master
hostnamectl set-hostname node1
hostnamectl set-hostname node2

# vi /etc/hosts 프라이빗 IP로 각 노드의 호스트네임 매핑하여 저장

# os 정보, 방화벽, cpu 및 램 용량, 스왑 비활성화 확인
cat /etc/os-release
ufw status
lscpu
free -h
swapon -s
# br_netfilter 모듈을 로드 / 모든 노드
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

# bridge taffic 보게 커널 파라메터 수정
# 필요한 sysctl 파라미터를 /etc/sysctl.d/conf 파일에 설정하면, 재부팅 후에도 값이 유지된다.
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

# 재부팅하지 않고 sysctl 파라미터 적용하기
sysctl --system
note

기본 방화벽으로 사용되는 iptables의 경우 Linux 커널의 네트워크 스택에 있는 패킷 필터링 Hook과 연동되어 동작한다. 이 Hook을 netfilter framework라고 한다. 모든 패킷은 networking 시스템으로 들어오는데 들어올 때 이러한 Hook(Netfilter)를 Trigger하게 되고 패킷들은 그 때 스택을 통해 통과된다. 애플리케이션들은 이러한 Hook에 등록되어 있으면 허용되어 트래픽이 흘러가게 되고 iptables는 이러한 netfilter hook들을 등록한다. 트래픽은 state를 따라 흘러가는데, 그 state들은 이러한 방화벽 규칙에 의해 구성된다.

# 각 노드에 도커 설치
apt update
apt install -y docker.io
systemctl enable --now docker
systemctl status docker

# cri-dockerd 설치
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.0/cri-dockerd_0.3.0.3-0.ubuntu-bionic_amd64.deb
dpkg -i cri-dockerd_0.3.0.3-0.ubuntu-bionic_amd64.deb
systemctl status cri-docker
ls /var/run/cri-dockerd.sock
cri-dockerd

컨테이너에 대한 관심이 급격히 증가하면서 대부분의 주요 IT 벤더와 클라우드 공급자들은 컨테이너 기반의 솔루션을 발표했고 관련 스타트업 또한 급증해 컨테이너의 생태계를 넓혀왔습니다. 하지만 포맷과 런타임에 대한 특정한 규격이 없다 보니 컨테이너의 미래는 불안했던 것이 사실입니다. 일례로 2013년 출시된 도커(Docker)가 사실상의 컨테이너 표준 역할을 했지만 코어OS(CoreOS)는 도커와는 다른 규격으로 표준화를 추진하려 했습니다. 이러한 문제를 해결하기 위해 2015년 6월 도커, 코어OS, AWS, Google, Microsoft, IBM 등 주요 플랫폼 벤더들은 애플리케이션의 이식성 관점에서 컨테이너 포맷과 런타임에 대한 개방형 업계 표준을 만들기 위해 OCI(Open Container Initiative)를 구성하였습니다. 이후 컨테이너 시장은 OCI의 런타임 명세와 이미지 명세를 준수하는 방향으로 성장하였고 그 과정에서 2016년 12월 쿠버네티스의 컨테이너 런타임을 만들기 위한 CRI(Container Runtime Interface)가 등장했습니다.

CRI는 쿠버네티스에서 만든 컨테이너 런타임 인터페이스로 개발자들의 컨테이너 런타임 구축에 대한 진입 장벽을 낮추어 줍니다. 초기 쿠버네티스는 컨테이너를 실행하기 위해 도커를 사용하였는데 이는 쿠버네티스 클러스터 워커 노드의 에이전트인 Kubelet 소스코드 내부에 통합되어 있었습니다. 이처럼 통합된 프로세스는 Kubelet에 대한 깊은 이해를 필요로 하였고 쿠버네티스 커뮤니티에 상당한 유지보수 오버헤드를 발생시켰습니다. 이러한 문제를 해결하기 위해 쿠버네티스는 CRI를 만들어 명확하게 정의된 추상화 계층을 제공함으로써 개발자가 컨테이너 런타임 구축에 집중할 수 있게 하였습니다.

kubeadm, kubectl , kubelet 설치

# kubeadm: 클러스터를 부트스트랩하는 명령
# kubelet: 클러스터의 모든 머신에서 실행되는 파드와 컨테이너 시작과 같은 작업을 수행
# kubectl: 클러스터와 통신하기 위한 커맨드 라인 유틸리티
apt update
apt install -y apt-transport-https ca-certificates curl

#Download the Google Cloud public signing key:
curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg \
https://packages.cloud.google.com/apt/doc/apt-key.gpg

#Add the Kubernetes apt repository:
echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] \
https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

#Update apt package index, install kubelet, kubeadm and kubectl, and pin their version:
apt update
apt install -y kubelet=1.26.0-00 kubeadm=1.26.0-00 kubectl=1.26.0-00
apt-mark hold kubelet kubeadm kubectl
# control-plain 컴포넌트 구성
kubeadm init --pod-network-cidr=192.168.0.0/16 --cri-socket unix:///var/run/cri-dockerd.sock

# 위 명령어 실행 결과
# vi token.join 파일 저장
kubeadm join 10.0.1.103:6443 --token \
--discovery-token-ca-cert-hash sha256: \
--cri-socket unix:///var/run/cri-dockerd.sock # 추가


# Kubectl을 명령 실행 허용하려면 kubeadm init 명령의 실행결과 나온 내용을 동작해야 함
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
# CNI(컨테이너 네트워크 인터페이스) 기반 Pod 네트워크 추가 기능을 배포해야 Pod가 서로 통신할 수 있습니다. 네트워크를 설치하기 전에 클러스터 DNS(CoreDNS)가 시작되지 않습니다.
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady control-plane 4m v1.26.0

Calico 설치

# legacy
# kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/tigera-operator.yaml
# kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/custom-resources.yaml

# Calico 설치 / master
kubectl create -f https://docs.projectcalico.org/manifests/calico.yaml

# node 초기화 될때까지 기다림
# watch kubectl get pods -n calico-system
watch kubectl get pods -n kube-system

# Calico Pod Running 확인
# kubectl get pods -n calico-system
kubectl get pods -n kube-system

# node 상태 확인
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready control-plane 5m15s v1.24.8

워커노드 조인

# 각 워커노드에서 실행
kubeadm join 10.0.1.103:6443 --token \
--discovery-token-ca-cert-hash sha256: \
--cri-socket unix:///var/run/cri-dockerd.sock

# 조인된 워커노드 확인
# NotReady 상태에서 Ready 상태 전환 확인
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready control-plane 10m v1.26.0
node1 Ready <none> 61s v1.26.0
node2 Ready <none> 62s v1.26.0

# master에서 실행
# Calico Mode 변경
# 클러스터 버전에 맞게 설치
# curl -L https://github.com/projectcalico/calico/releases/download/v3.24.1/calicoctl-linux-amd64 -o calicoctl
curl -L https://github.com/projectcalico/calico/releases/download/v3.25.0/calicoctl-linux-amd64 -o calicoctl
chmod +x calicoctl
mv calicoctl /usr/bin

# IPIPMODE Never 확인
calicoctl get ippool -o wide

# yaml 파일 설정
# 컨트롤 플레인 Pod 네트워크 대역으로 설정
cat << END > ipipmode.yaml
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-ipv4-ippool
spec:
blockSize: 26
cidr: 192.168.0.0/16
ipipMode: Always
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
END

# 적용
calicoctl apply -f ipipmode.yaml

# IPIPMODE Always 확인
calicoctl get ippool -o wide
# 루트 권한이 아닌 우분투 사용자도 kubectl 명령 실행 설정
mkdir -p ~ubuntu/.kube
cp -i /etc/kubernetes/admin.conf ~ubuntu/.kube/config
chown -R ubuntu:ubuntu ~ubuntu/.kube

# kubectl 명령어 사용 가능 확인
ubuntu@mater:~$: kubectl get nodes

인증서 적용

sudo -i
vi /etc/ssh/sshd_config
...
57 PasswordAuthentication yes
systemctl restart sshd

passwd ubuntu

# 마스터 노드
# 세 인스턴스 모두 동일한 키페어 사용하므로 사용중인 키페어 RSA PRIVATE KEY를 id_rsa에 저장
vi ~/.ssh/id_rsa

# 퍼블릭 키 추출
ssh-keygen -f id_rsa -y > id_rsa.pub

# 권한 변경
chmod 400 id_rsa

# 접속 테스트
ssh node1
ssh node2

sudo -i
vi /etc/ssh/sshd_config
...
57 PasswordAuthentication no
systemctl restart sshd
# master node
sudo mkdir -p /data/cka /var/CKA2022/
sudo apt update
sudo apt install wget curl tree -y

# 워커 노드 라벨 설정
kubectl label node node1 disktype=ssd gpu=true
kubectl label node node2 disktype=std
kubectl get node -L disktype,gpu

# 각 워커노드에 폴더 생성
mkdir -p /data/{app-data,volume,storage,cka} /app/storage/storage{1,2,3}

k8s 환경구성

cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: Namespace
metadata:
name: migops
labels:
team: migops

---
apiVersion: v1
kind: Namespace
metadata:
name: devops
labels:
team: devops
---
apiVersion: v1
kind: Namespace
metadata:
name: presales
labels:
team: presales
---

## namespcae customera
apiVersion: v1
kind: Namespace
metadata:
name: customera
labels:
partition: customera

---
## namespcae customera
apiVersion: v1
kind: Namespace
metadata:
name: customerb
labels:
partition: customerb
---

## deploy and service-port추가해서
## k8s
apiVersion: apps/v1
kind: Deployment
metadata:
name: front-end
spec:
selector:
matchLabels:
run: nginx
replicas: 2
template:
metadata:
labels:
run: nginx
spec:
containers:
- name: http
image: nginx

---
## storage class를 가진 PV 생성준비
## 문제 : pvc생성 - pod 마운트 - pvc size 확장
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv1
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
- ReadOnlyMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: app-hostpath-sc
hostPath:
path: /data/storage

---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv2
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
- ReadOnlyMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: app-data-sc
hostPath:
path: /data/volume

---
## sidecar container
apiVersion: v1
kind: Pod
metadata:
name: eshop-cart-app
spec:
containers:
- image: busybox
name: cart-app
command: ['/bin/sh', '-c', 'i=1;while :;do echo -e "$i: Price: $((RANDOM % 10000 + 1))" >> /var/log/cart-app.log; i=$((i+1)); sleep 2; done']
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- emptyDir: {}
name: varlog
---
## rolling update
## k8s
## replicas 수를 5개로 확장
apiVersion: apps/v1
kind: Deployment
metadata:
name: eshop-order
namespace: devops
spec:
replicas: 2
selector:
matchLabels:
name: order
template:
metadata:
name: order
labels:
name: order
spec:
containers:
- name: nginx-container
image: nginx:1.14

---
# NetworkPolicy
kind: Pod
apiVersion: v1
metadata:
name: web
namespace: migops
labels:
app: webwas
tier: frontend
spec:
containers:
- name: web
image: smlinux/cent-mysql:v1
command: ["/bin/bash"]
args: ["-c", "while true; do echo hello; sleep 10;done"]

---

kind: Pod
apiVersion: v1
metadata:
name: was
namespace: migops
labels:
app: webwas
tier: application
spec:
containers:
- name: was
image: smlinux/cent-mysql:v1
command: ["/bin/bash"]
args: ["-c", "while true; do echo hello; sleep 10;done"]

---

kind: Pod
apiVersion: v1
metadata:
name: db
namespace: migops
labels:
app: webwas
tier: database
spec:
containers:
- name: db
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: pass

---
## init container
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: nginx
image: nginx
command: ['sh', '-c', 'if [ ! -e "/opt/test" ];then exit;fi;']
volumeMounts:
- name: workdir
mountPath: /opt
volumes:
- name: workdir
emptyDir: {}

---
## log exam
apiVersion: v1
kind: Pod
metadata:
name: custom-app
namespace: default
spec:
containers:
- name: app
image: busybox
command: ['/bin/sh', '-c', 'while :;do echo -e "find files\nerror: file not found\nToday: $(date)\nHostname: $(hostname)"; sleep 60; done']

---
## sidecar exam
apiVersion: v1
kind: Pod
metadata:
name: cka-webserver
namespace: default
spec:
containers:
- image: nginx:1.14
name: webserver
volumeMounts:
- mountPath: /var/log/nginx
name: log
volumes:
- name: log
emptyDir: {}

---
## cpu load :
apiVersion: v1
kind: Pod
metadata:
labels:
name: overloaded-cpu
name: campus-01
spec:
containers:
- name: campus
image: smlinux/vish-stress
resources:
limits:
cpu: "0.4"
memory: "300Mi"
requests:
cpu: "0.4"
memory: "250Mi"
args:
- -cpus
- "1"
- -mem-total
- "150Mi"
- -mem-alloc-size
- "100Mi"
- -mem-alloc-sleep
- "1s"
---
apiVersion: v1
kind: Pod
metadata:
labels:
name: overloaded-cpu
name: fast-01
spec:
containers:
- name: fast
image: smlinux/vish-stress
resources:
limits:
cpu: "0.2"
memory: "300Mi"
requests:
cpu: "0.2"
memory: "250Mi"
args:
- -cpus
- "1"
- -mem-total
- "250Mi"
- -mem-alloc-size
- "100Mi"
- -mem-alloc-sleep
- "1s"

---
#multi-container
apiVersion: v1
kind: Pod
metadata:
name: busybox-sleep
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sh"]
args: ["-c", "while true; do sleep 1000; done"]

EOF

etcd 설치

# master
export RELEASE=$(curl -s https://api.github.com/repos/etcd-io/etcd/releases/latest|grep tag_name | cut -d '"' -f 4)
wget https://github.com/etcd-io/etcd/releases/download/${RELEASE}/etcd-${RELEASE}-linux-amd64.tar.gz
tar xf etcd-${RELEASE}-linux-amd64.tar.gz
cd etcd-${RELEASE}-linux-amd64
sudo mv etcd etcdctl etcdutl /usr/local/bin
etcd --version
cd

sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /data/etcd-snapshot-previous.db

메트릭 서버 설치

# master
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# vi components.yaml 수정
...
129 metadata:
130 labels:
131 k8s-app: metrics-server
132 spec:
133 containers:
134 - args:
135 - --cert-dir=/tmp
136 - --secure-port=4443
137 - --kubelet-insecure-tls # 추가
...

...
175 dnsPolicy: ClusterFirst # 추가
176 hostNetwork: true # 추가
177 nodeSelector:
178 kubernetes.io/os: linux
...

# yaml 파일 실행
k apply -f component.yaml

# 메트릭 Pod 실행 확인
k get pods -A

# 메모리, CPU 사용량 확인
k top nodes

/etc/resolv.conf 설정

sudo apt-get install resolvconf

sudo vi /etc/resolvconf/resolv.conf.d/base
...
search default.svc.cluster.local svc.cluster.local cluster.local openstacklocal
options ndots:5

sudo /etc/init.d/resolvconf start

echo "nameserver 10.96.0.10" | sudo tee /etc/resolvconf/resolv.conf.d/head

여기까지 잘 따라와 주셨다면 메모리 및 CPU 사용량을 확인하였을 때 마스터 노드, 워커 노드의 메트릭을 모두 모니터링할 수 있게 됩니다. 하지만 저는 여기서 node2의 CPU와 메모리 사용량이 unknown 상태로 나타났었는데 메트릭 서버가 어떻게 실행되는지 알고 나서 문제의 실마리를 찾아 해결하였습니다. 문제는 보안 그룹 설정과 관련 있었습니다.

NAME     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
master 158m 7% 2386Mi 62%
node1 503m 25% 1032Mi 26%
node2 <unknown> <unknown> <unknown> <unknown>

Trouble Shooting

메트릭 서버의 파드가 마스터 노드에 위치한다고 생각하여 마스터 노드의 보안 그룹에 10250 포트로 워커 노드의 보안 그룹만을 참조하였던 것입니다. 즉 워커 노드 끼리의 10250 통신을 안해줬기에 아래 그림처럼 노드 1에 위치한 쿠버네티스 메트릭 서버 파드에 node2의 메트릭 정보를 보낼 수 없었던 것입니다. 즉 아래 그림과 같은 상황이었던 것입니다.

trouble1

마스터 노드 보안 그룹 trouble3

아래 사진처럼 워커 노드의 보안 그룹에 10250포트로 워커 노드가 속해 있는 서브넷 대역을 포함하여 노드 2의 메트릭 정보가 노드 1에 메트릭 서버 Pod에 전송될 수 있도록 해주었습니다.

워커 노드 보안 그룹 trouble4

보안 그룹 설정 후 제대로 통신이 된 아키텍처입니다.

trouble2

$ k top nodes
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
master 138m 6% 2433Mi 63%
node1 492m 24% 1113Mi 29%
node2 284m 14% 1423Mi 37%

쿠버네티스 메트릭 서버는 쿠버네티스 클러스터를 구성하는 Node와 Pod의 Metric 정보를 수집한 다음, 메트릭 정보가 필요한 쿠버네티스 컴포넌트들에게 수집한 메트릭 정보를 전달하는 역할을 수행한다.

kubelet은 cAdvisor라고 불리는 Linux의 Cgroup을 기반으로 하는 Node, Pod Metric Collector를 내장하고 있으며 cAdvisor가 수집하는 메트릭은 kubelet의 10250 Port의 /stat Path를 통해서 외부로 노출된다.

메트릭 서버는 쿠버네티스 API 서버로부터 Node에서 구동중인 kubelet의 접속 정보를 얻은 다음, kubelet으로부터 Node, Pod의 Metric을 수집한다. 수집된 메트릭은 메모리에 저장된다. 따라서 메트릭 서버가 재시작되면 수집된 모든 메트릭 정보는 사라진다. 메트릭 서버는 쿠버네티스의 API Aggregation 기능을 이용하여 메트릭 서버와 연결되어 있는 메트릭 서비스를 metric.k8s.io API로 등록한다. 따라서 메트릭 서버의 메트릭 정보가 필요한 쿠버네티스 컴포넌트들은 메트릭 서버 또는 메트릭 서비스로부터 직접 메트릭을 가져오지 않고, 쿠버네티스 API 서버를 통해서 가져온다.

현재 메트릭 서버의 메트릭 정보를 이용하는 쿠버네티스 컴포넌트에는 쿠버네티스 컨트롤러 매니저에 존재하는 Horizontal Pod Autoscaler Controllerkubectl top 명령어가 있다.

별도의 컨트롤러로 동작하는 Vertical Pod Autoscaler Controller도 메트릭 서버의 메트릭을 이용한다. 메트릭 서버는 쿠버네티스 컴포넌트들에게 메트릭을 제공하는 용도로 개발되었으며, 쿠버네티스 클러스터 외부로 메트릭 정보를 노출시키는 용도로 개발되지는 않았다. 쿠버네티스 클러스터의 메트릭을 외부로 노출하기 위해서는 프로메테우스와 같은 별도의 도구를 이용해야 한다.

메트릭 서버의 HA(High Availability)를 위해서 다수의 메트릭 서버를 구동하는 방법을 생각해볼 수 있다. 하지만 메트릭 서버의 구조상 메트릭 서버의 개수만큼 중복되어 메트릭을 수집하는 구조이기 때문에 다수의 메트릭 서버는 쿠버네티스 클러스터의 부하의 원인이 된다. 또한 다수의 메트릭 서버를 구동하여도 메트릭을 직접 가져가는 Kubernetes API는 하나의 메트릭 서버와 커넥션을 맺고 메트릭을 수집하기 때문에, 다수의 메트릭 서버를 구동하여도 부하 분산 효과는 얻을 수 없다.

이러한 특징들 때문에 현재도 고가용성을 위해서 다수의 메트릭 서버를 띄우는 방식이 올바른 방식인지 검토하고 있다.