Home Hyper-v 환경에서 k8s 클러스터 구축
Post
Cancel

Hyper-v 환경에서 k8s 클러스터 구축

본 글은 ‘쿠버네티스 클러스터 구성을 위한 Hyper-v Provisioning’ 이후 진행함

0. 개요


  • 1개의 Master와 3개의 Worker로 구성된 쿠버네티스 클러스터를 구축하는 것이 목표
  • 환경
    • Host OS: Window11
    • Hyper-v OS: Rocky-9.1-x86_64-minimal.iso

Customize vim / promptline (if nessesary)

1
sudo dnf install -y vim git ctags ShellCheck
1
curl -fLo "${HOME}/.vim/autoload/plug.vim" --create-dirs 'https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'
1
cat << EOF > "${HOME}/.vimrc"
  • .vimrc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    
      set term=xterm-256color
      set nu
      set ai
      set si
      set cindent
      set shiftwidth=4
      set tabstop=4
      set hlsearch
      set ruler
      set title
      set showmatch
      set wmnu
      set expandtab
      set background=dark
      set nocompatible
      set fileencodings=utf-8,euc-kr
      set bs=indent,eol,start
      set history=1000
      set laststatus=2
    
      set foldenable
      set foldlevelstart=10
      set foldnestmax=10
      set foldmethod=indent
    
      set autoread
      set cursorline
    
      filetype plugin on
      filetype indent on
    
      call plug#begin('~/.vim/plugged')
    
      Plug 'morhetz/gruvbox'
      Plug 'vim-airline/vim-airline'
      Plug 'vim-airline/vim-airline-themes'
      Plug 'nathanaelkane/vim-indent-guides'
      Plug 'itchyny/vim-cursorword'
      Plug 'tpope/vim-surround'
      Plug 'vim-scripts/WhiteWash'
      Plug 'roxma/vim-paste-easy'
      Plug 'bitc/vim-bad-whitespace'
      Plug 'mtdl9/vim-log-highlighting'
      Plug 'scrooloose/nerdtree', { 'on':  'NERDTreeToggle' }
      Plug 'edkolev/promptline.vim'
      Plug 'godlygeek/tabular'
      Plug 'preservim/tagbar'
      Plug 'preservim/nerdcommenter'
      Plug 'dense-analysis/ale'
      Plug 'vim-scripts/AutoComplPop'
      Plug 'mg979/vim-visual-multi'
    
      call plug#end()
    
      syntax on
      colorscheme gruvbox
    
      map <C-e> :NERDTreeToggle <CR>
      map <C-t> :Tagbar         <CR>
    
      inoremap <S-Tab> <C-d>
      nnoremap <S-Tab> <<
    
      nnoremap <space> za
      nnoremap <tab> zM
      nnoremap \ zR
    
      nnoremap <F1>   :noh                   <CR>
      nnoremap <F2>   :set nonumber!         <CR>
      nnoremap <F3>   :set mouse=a           <CR>
      nnoremap <F4>   :set mouse-=a          <CR>
      nnoremap <F5>   :set list!             <CR>
      nnoremap <F6>   :%!python -m json.tool <CR>
      nnoremap <F7>   :!vi -u NONE %         <CR>
      nnoremap <F12>  :!cat %                <CR>
    
      let g:gruvbox_contrast_dark                   = 'hard'
    
      let g:airline#extensions#tabline#enabled      = 1
      let g:airline#extensions#tabline#left_sep     = ' '
      let g:airline#extensions#tabline#left_alt_sep = '|'
      let g:airline#extensions#tabline#formatter    = 'unique_tail_improved'
    
      let g:airline_theme                           = 'hybrid'
      let g:airline_powerline_fonts                 = 1
    
      let g:indent_guides_enable_on_vim_startup     = 1
      let g:indent_guides_start_level               = 2
      let g:indent_guides_guide_size                = 1
    
      let g:promptline_theme                        = 'airline'
      let git_sha_slice                             = {
          \'function_name': 'git_sha',
          \'function_body': [
              \'function git_sha {',
              \'  local sha',
              \'  sha=$(git rev-parse --short HEAD 2>/dev/null) || return 1',
              \'  printf "%s" "$sha"',
          \'}']}
      let g:promptline_preset                       = {
          \'a' : [ '\h' ],
          \'b' : [ '\u' ],
          \'c' : [ '\w' ],
          \'warn' : [ promptline#slices#vcs_branch(), git_sha_slice ]
          \}
      let g:promptline_powerline_symbols            = 1
    
      command! -nargs=0 WriteWithSudo :w !sudo tee % > /dev/null
      cnoreabbrev w!! WriteWithSudo
    
1
EOF
1
printf '\n' | vim +PlugInstall +qall &> /dev/null
1
printf '\n' | vim +"PromptlineSnapshot ${HOME}/.promptline airline" +qall &> /dev/null
1
printf 'source "${HOME}/.promptline.sh" &> /dev/null\nfunction vi { vim "${@}"; }\n' >> "${HOME}/.bashrc" && source "${HOME}/.bashrc"

1. 클러스터 설정


Host file (모든 노드에서 설정)

1
2
3
4
5
cat << EOF | sudo tee /etc/hosts
192.168.25.220  woobin-vm1
192.168.25.221  woobin-vm2
192.168.25.222  woobin-vm3
EOF

환경변수 설정

1
2
export CLUSTER_HOSTS=(woobin-vm1 woobin-vm2 woobin-vm3)
export HOME_DIR_PATH=/home/$USER

SSH key sharing

각 서버에 접근할 때 비밀번호 없이 로그인하기 위해 host 계정 ssh key를 생성하고 각 서버에 배포

1
2
3
4
5
6
7
8
9
10
11
# sshd_config 변경
for i in ${!CLUSTER_HOSTS[*]}; do ssh woobin@${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo dnf install -y sshpass && sudo sed -i -e 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config && sudo systemctl restart sshd" &> /dev/null; done

# ssh key 생성
echo -e ">> Enter Password:"; read -s PASSWD; for i in ${!CLUSTER_HOSTS[*]}; do echo "i[$i]: [${CLUSTER_HOSTS[$i]}]"; sshpass -p $PASSWD ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no woobin@${CLUSTER_HOSTS[$i]} "mkdir -p $HOME_DIR_PATH/.ssh; echo -e 'n\n' | ssh-keygen -t rsa -f $HOME_DIR_PATH/.ssh/id_rsa -q -N ''; cp $HOME_DIR_PATH/.ssh/id_rsa.pub $HOME_DIR_PATH/.ssh/authorized_keys; chmod 700 $HOME_DIR_PATH/.ssh" &> /dev/null; done

# ssh key 공유
echo -e ">> Enter Password:"; read -s PASSWD; for i in ${!CLUSTER_HOSTS[*]}; do echo "i[$i]: [${CLUSTER_HOSTS[$i]}]"; for j in ${!CLUSTER_HOSTS[*]}; do echo "j[$j]: [${CLUSTER_HOSTS[$i]}] -> [${CLUSTER_HOSTS[$j]}]"; sshpass -p $PASSWD ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no woobin@${CLUSTER_HOSTS[$i]} "sshpass -p $PASSWD ssh-copy-id -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no woobin@${CLUSTER_HOSTS[$j]} &> /dev/null" &> /dev/null; done; done

# 체크
for i in ${!CLUSTER_HOSTS[*]}; do ssh ${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "ls -al .ssh"; done

Disable Firewall

1
for i in ${!CLUSTER_HOSTS[*]}; do ssh woobin@${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo systemctl stop firewalld && sudo systemctl disable firewalld && sudo systemctl mask --now firewalld"; done
  • systemctl stop firewalld: 현재 기동중인 방화벽 중지
  • systemctl disable firewalld: 시스템 기동시 방화벽 기동 비활성화
  • systemctl mask --now firewalld: 다른 서비스에서 방화벽 시작 방지 (Created symlink /etc/systemd/system/firewalld.service → /dev/null.)
    1. mask: mask는 서비스를 “마스크”하거나 비활성화하는 기능을 합니다. 마스크된 서비스는 수동으로 시작할 수 없으며, 다른 서비스나 사용자의 요청에 의해서도 자동으로 시작되지 않습니다.
    2. -now: -now 옵션은 서비스 상태를 변경하는 동시에 즉시 실행하도록 지시하는 옵션입니다.

Install initial Package

1
for i in ${!CLUSTER_HOSTS[*]}; do ssh woobin@${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo dnf install -y epel-release; sudo dnf update -y"; done

2. Docker

Install docker

현재 구성하는 클러스터에서 도커는 컨테이너 빌드 도구로 사용할 예정. 컨테이너 런타임으로는 containerd를 사용

  • 참고) 도커 엔진은 컨테이너 런타임이 쿠버네티스와 호환되기 위한 요구 사항인 CRI를 만족하지 않는다. 쿠버네티스 v1.24 이전 릴리스는 도커심이라는 구성 요소를 사용하여 도커 엔진과의 직접 통합을 지원했다. 이는 더 이상 쿠버네티스에 포함되지 않는다.
1
for i in ${!CLUSTER_HOSTS[*]}; do ssh woobin@${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"; done

Bash command completion for the docker client

쉘 세션 재시작 시 적용됨

1
for h in ${CLUSTER_HOSTS[*]}; do ssh ${h} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo dnf -y install bash-completion && sudo curl -L https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker"; done

Ensure docker for Operation User

docker 그룹에 사용자를 추가하여 docker 명령어를 실행할 수 있도록 권한을 부여

1
for h in ${CLUSTER_HOSTS[@]}; do ssh ${h} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo usermod -aG docker $USER"; done

Configure docker(d)

kubelet과 컨테이너 런타임이 같은 cgroup 드라이버를 사용해야 하며 구성도 동일해야 함. 본 포스팅에서는 cgroup 드라이버로 systemd 사용

https://kubernetes.io/ko/docs/setup/production-environment/container-runtimes/#cgroup-드라이버

1
2
3
4
5
6
7
8
for h in ${CLUSTER_HOSTS[@]}; do ssh ${h} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no 'cat <<EOF | sudo tee /etc/docker/daemon.json
{
    "exec-opts": ["native.cgroupdriver=systemd"],
    "hosts": ["fd://","tcp://0.0.0.0:2375"],
    "containerd": "/run/containerd/containerd.sock",
    "storage-driver": "overlay2"
}
EOF'; done
  • "exec-opts": ["native.cgroupdriver=systemd"]: 컨테이너 런타임의 cgroup 드라이버로 cgroupfs 대신 systemd를 사용
  • "hosts": ["fd://","tcp://0.0.0.0:2375"]: Docker 데몬이 수신 대기할 호스트와 포트를 지정. "fd://"는 유닉스 도메인 소켓을 통한 연결, "tcp://0.0.0.0:2375"는 모든 IP 주소에서 2375 포트를 통한 TCP 연결을 허용함
  • "containerd": "/run/containerd/containerd.sock": 컨테이너 런타임 “containerd”의 소켓 경로를 지정
  • "storage-driver": "overlay2": overlay2"는 컨테이너 레이어를 효율적으로 관리하기 위해 사용되는 드라이버

Configure docker systemd unit

1
2
3
for h in ${CLUSTER_HOSTS[@]}; do ssh ${h} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo sed -i '/ExecStart/s/^/# /' /usr/lib/systemd/system/docker.service && sudo sed -i '/ExecStart/a ExecStart=/usr/bin/dockerd' /usr/lib/systemd/system/docker.service"; done

# AS-IS: ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Run Docker(d)

1
for h in ${CLUSTER_HOSTS[@]}; do ssh ${h} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo systemctl daemon-reload && sudo systemctl start docker && sudo systemctl enable docker"; done

3. Kubernetes


Disable SELinux (Security-Enhanced Linux)

setenforce 0 및 sed ... 를 실행하여 permissive 모드로 SELinux를 설정하면 효과적으로 비활성화된다. 컨테이너가 호스트 파일시스템(예를 들어, 파드 네트워크에 필요한)에 접근하도록 허용하는 데 필요하다. kubelet에서 SELinux 지원이 개선될 때까지 이 작업을 수행해야 한다.

1
for i in ${!CLUSTER_HOSTS[*]}; do ssh woobin@${CLUSTER_HOSTS[$i]} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo setenforce 0 && sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config"; done

Disable Memory Swap

메모리 스왑이 활성화돼 있으면 컨테이너의 성능이 일관되지 않을 수 있기 때문

1
2
# 현재 활성화된 swap 정보 확인
cat /proc/swaps
1
for host in ${CLUSTER_HOSTS[@]}; do ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo swapoff -a && sudo sed -i '/ swap / s/^/#/' /etc/fstab && sudo mount -a"; done

Configure k8s repository

1
2
3
4
5
6
7
8
9
10
11
12
13
for host in ${CLUSTER_HOSTS[@]}; do
    ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
        cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
    "
done

Install kubelet, kubeadm, kubectl

1
for host in ${CLUSTER_HOSTS[@]}; do ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes && sudo systemctl enable --now kubelet"; done
  • kubeadm: Kubernetes 클러스터를 부트스트랩(시작)하는 데 사용
  • kubelet: Kubernetes 클러스터의 모든 노드에서 실행되는 구성 요소. pod 및 컨테이너를 시작하는 등 클러스터 내의 모든 노드에서 필요한 작업을 수행
  • kubectl: Kubernetes 클러스터의 모든 노드에서 실행되는 구성 요소. 이는 pod 및 컨테이너를 시작하는 등 클러스터 내의 모든 노드에서 필요한 작업을 수행
  • -disableexcludes=kubernetes: Kubernetes 관련 패키지를 설치할 때 다른 패키지 제외 설정을 무시하고 모든 관련 패키지를 설치하도록 지시하는 것

Configure containerd

1
2
3
4
5
6
7
for host in ${CLUSTER_HOSTS[@]}; do
    ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no '
        sudo containerd config default | sudo tee /etc/containerd/config.toml &&
        sudo sed -i "s/SystemdCgroup = false/SystemdCgroup = true/g" /etc/containerd/config.toml &&
        sudo systemctl restart containerd
    '
done

참고) error: The connection to the server 192.168.x.x:6443 was refused - did you specify the right host or port?

  • kubeadm init 이후에 kubectl get node를 했을 때 처음에는 결과가 잘 나타나다가 시간이 조금 지나면 위와 같은 에러가 나타나기 시작한다.
  • 6443은 쿠버네티스 API에서 사용하는 포트이고 192.168.x.x는 localhost니깐 위의 에러는 자기 자신에 연결할 수 없다는 이야기가 된다. 6443 포트를 통해서 자기 내부에서 돌고 있는 쿠버네티스 시스템과 통신을 하게 되는데 그게 불가능하다는 것은 쿠버네티스가 제대로 돌고 있지 않다는 것이다
  • 컨테이너 런타임(containerd)의 cgroup 드라이버를 설정해줘서 해결

Initializing control-plane node

세팅할 클러스터에서 Pod가 서로 통신할 수 있도록 Pod 네트워크 애드온을 설치 해야 한다. kubeadm을 통해 만들어진 클러스터는 CNI(Container Network Interface) 기반의 애드온이 필요하다. 본 글에서는 Calico를 설치하여 사용할 것이다.

1
sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --apiserver-advertise-address=192.168.25.220
  • —-pod-network-cidr : Pod 네트워크를 설정할 때 사용
    • 192.168.0.0/16 네트워크는 Calico에서 기본적으로 권장하는 네트워크 대역
  • -apiserver-advertise-address : 특정 마스터 노드의 API Server 주소를 설정할 때 사용
    • ifconfig 명령를 통해 마스터 노드의 IPv4 주소를 확인
  • 만약 고가용성을 위해 마스터 노드를 다중으로 구성했다면 --control-plane-endpoint 옵션을 사용하여 모든 마스터 노드에 대한 공유 엔드포인트를 설정할 수 있다.

참고) [ERROR CRI]: container runtime is not running

  • /etc/containerd/config.toml에서 disabled_plugins 라인을 비활성화하여 CRI 인터페이스를 활성화
1
for host in ${CLUSTER_HOSTS[@]}; do ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "sudo sed -i '/^disabled_plugins/s/^/#/' /etc/containerd/config.toml && sudo systemctl restart containerd"; done

Additional

1
2
# maser node
mkdir -p $HOME/.kube && sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config && sudo chown $(id -u):$(id -g) $HOME/.kube/config
1
2
3
4
5
6
# worker node
for host in ${CLUSTER_HOSTS[@]}; do
    ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \
        "mkdir -p \$HOME/.kube && scp woobin@woobin-vm1:${HOME}/.kube/config \$HOME/.kube/"
done
  • root 계정이 아닌 다른 사용자 계정에서 kubectl 명령어를 사용하여 클러스터를 제어하기 위함

Install calico

https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart

1
2
3
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/tigera-operator.yaml

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/custom-resources.yaml

Join nodes

1
2
3
sudo kubeadm join 192.168.25.220:6443 \
    --token ielh36.8k2eiq8m5rk0u12d \
    --discovery-token-ca-cert-hash sha256:6a4e73b78661ecbe9d7a43694ae5809c2601309461a14fed6f595fdb13796edc
  • 명령어에서 확인할 수 있는 것과 같이 워커 노드가 마스터 노드에 결합(join)하기 위해서는 아래 인자들이 필요하다.
    • 마스터 노드의 IP:6443 (6443 포트는 kubernetes api server 프로세스의 기본 포트이다. 만약 마스터 노드의 api server 포트가 6443이 아니라면 해당 마스터 노드의 api server 포트 번호를 기입해주면 된다.)
    • -token (token은 기본적으로 24시간 뒤 만료된다.)
    • -discovery-token-ca-cert-hash
  • 마스터 노드의 IP와 포트는 쉽게 알 수 있지만, 만약 --token 값과 --discovery-token-ca-cert-hash 값을 잊어버렸다면 아래와 같이 다시 찾을 수 있다.
    • -token 찾는 명령어

      1
      
        kubeadm token list
      
    • -discovery-token-ca-cert-hash 찾는 명령어

      1
      
        openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
      
  • 만약 token이 24시간이 지나서 만료가 되었을 때에는 아래의 명령어를 통해 새로 생성이 가능하다.

    1
    
      kubeadm token create
    

Unset

1
for host in ${CLUSTER_HOSTS[@]}; do ssh ${host} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "unset CLUSTER_HOSTS"; done

Reference


  1. https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/
This post is licensed under CC BY 4.0 by the author.