kubernetes 를 핸즈온으로 공부하려면 로컬 머신에서 멀티 노드 클러스터를 구축하는게 가장 편리하다는 판단을 했다. 그래서 여러 가이드를 찾아보면서 처음에는 multipass 라는 가상화 도구를 사용해서 시도해보고, 그 다음에는 kind 라는 docker 컨테이너에 올리는 kubernetes 도구도 사용해봤는데, 내공이 부족하여 실패하였다. 결국 docker desktop 에서 간단히 활성화 할 수 있는 kind 로의 프로비저닝 방식을 사용하여 구축에 성공했다.

하지만, ingress 를 실습할 차례가 왔고, kind 방식으로는 네트워크 구성이 실제 멀티 노드 클러스터 환경과 달라서 실습이 불가능함을 깨달았다. 그래서, 실제 완전한 리눅스 OS와 동일하게 구성할 수 있는 UTM 가상화 도구를 사용해서 멀티 노드 클러스터를 구축했고, 그 과정을 기술한다.

 

1. UTM 설치

UTM 이라는 가상 머신 구축 도구를 m1 맥북에 설치한다.

 

2. 가상머신 생성

가상머신 3대를 Ubuntu 24.04.3 LTS OS를 설치하며 생성한다.

1대는 control plane 이고, 나머지 2대는 worker node 용도이다.

시스템 자원은 본인이 사용하는 컴퓨팅 리소스에 맞게 할당한다.

 

3. 클립보드 붙여넣기 설정

개발 편의성을 위해 호스트 머신과 각 게스트 머신간 복붙을 가능하게 하여야 한다.

CLI 만 사용중이라면 아래 두가지 방법이 있다.(spice-vdagent 는 GUI 용 클립보드 공유 에이전트이므로 상관하지 않는다.)

  1) 리눅스 터미널에 openssh 를 설치하여 맥북에서 직접 ssh 접속

  2) 가상머신의 display 장치를 삭제하고 직렬 포트 장치로 사용

직렬 포트 장치가 간단하지만, CLI 가 이상하게 표시되는 현상이 종종 있으므로, ssh 접속하는 방법이 사용성이 더 낫다.

 

4. hostname 설정(모든 노드)

아래 커맨드를 통해서 각 노드에 hostname 을 지정해준다.

$ sudo hostnamectl set-hostname <새 호스트명>

 

아래 커맨드를 통해서 정상적으로 hostname 이 설정되었는지 확인한다.

$ cat /etc/hostname
# 또는
$ hostname
# 또는
$ hostnamectl

 

5. /etc/hosts 수정(모든 노드)

각 노드에서 아래 명령어를 실행하여 enp0s1 장치의 inet 에 기재된 ip 를 확인한다.

$ sudo ip addr

 

각 노드의 /etc/hosts 파일에 나머지 노드의 ip 를 hostname 과 매핑해준다.

# master01 노드의 /etc/hosts 예시
...
127.0.1.1 master01
192.168.64.6 worker01
192.168.64.7 worker02
...

 

6. swapoff 및 영구 swapoff 적용(모든 노드)

아래 커맨드를 사용하여 각 노드에서 swapoff 를 적용한다.

$ sudo swapoff -a

 

아래 커맨드를 사용해 /etc/fstab 파일을 수정하여 부팅시마다 swapoff 가 자동 적용되도록 설정한다.(swap 설정을 주석처리하여 영구 비활성화)

$ sudo sed -i '/swap/s/^\(.*\)$/#\1/g' /etc/fstab

 

7. 커널 네트워크에 브릿지 트래픽 허용(모든 노드)

아래 커맨드를 실행하여 브릿지 네트워크로부터 들어오는 트래픽을 허용한다.

$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

# 설정 적용
$ sudo sysctl --system

 

8. containerd 설치(모든 노드)

아래 커맨드를 순차적으로 실행하여 각 노드에 containerd 를 설치한다.

# 1) 필요한 의존성 패키지 설치
$ sudo apt update
$ sudo apt install -y ca-certificates curl gnupg lsb-release

# 2) Docker의 APT repo 대신 Ubuntu 기본 repo 에서 containerd 설치
$ sudo apt install -y containerd

# 3) 기본 config 생성
$ sudo mkdir -p /etc/containerd
$ sudo containerd config default | sudo tee /etc/containerd/config.toml

# 4) 시스템에 맞춰 cgroup 설정(기본적으로 systemd cgroup 사용 권장)
# /etc/containerd/config.toml 에서 아래를 확인/적용:
# SystemdCgroup = true
$ sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml || true

# 5) pause:3.10 버전으로 수정(선택)
# (이건 kubernetes 1.31.3 버전과의 호환성을 위함이며, 사용하는 kubernetes 버전의 호환성을 확인하여 맞춰주시길 바랍니다.)
$ sudo sed -i 's|sandbox_image = "registry.k8s.io/pause:3.8"|sandbox_image = "registry.k8s.io/pause:3.10"|' \
  /etc/containerd/config.toml

# 6) containerd 재시작/활성화
$ sudo systemctl restart containerd
$ sudo systemctl enable containerd

 

9. kubeadm, kubelet, kubectl 설치(모든 노드)

아래 커맨드를 순차적으로 실행하여 각 노드에 kubeadm, kubelet, kubectl 를 설치한다.

# 1) 필요한 의존성 패키지 설치
$ sudo apt-get update
$ sudo apt-get install -y apt-transport-https ca-certificates curl

# 2) APT 저장소의 서명 키(GPG key)를 보관할 디렉터리를 생성
$ sudo mkdir -p /etc/apt/keyrings

# 3) Kubernetes APT 저장소의 GPG 서명 키 다운로드 및 변환하여 저장(v1.31)
$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
  sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# 4) Kubernetes APT 저장소 등록
$ echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
  https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /" | \
  sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update

# 5) 설치 (버전 고정 원하면 =<버전> 지정)
$ sudo apt-get install -y \
  kubelet=1.31.3-1.1 \
  kubeadm=1.31.3-1.1 \
  kubectl=1.31.3-1.1

# 6) 자동 업그레이드 되지 않도록 버전 고정
$ sudo apt-mark hold kubelet kubeadm kubectl

# 7) kubelet 서비스 재시작
$ sudo systemctl enable kubelet && sudo systemctl restart kubelet

 

10. VM에 브릿지 네트워크 설정(모든 노드)

UTM 에서 네트워크 장치 추가 > 브릿지 네트워크 > 실제 접속중인 네트워크 인터페이스 선택 하고 저장한다.

노드 접속 후 아래 커맨드를 실행하여 enp0s2 장치에 inet 이 잡혔는지 확인한다.

$ sudo ip addr

 

안잡혔다면 아래 커맨드로 /etc/netplan/*.yaml 파일에 설정을 추가하고 적용한다.

# /etc/netplan/*.yaml 파일을 열어서 아래처럼 enp0s2 에 dhcp4 설정 추가
network:
  version: 2
  ethernets:
    enp0s1:
      dhcp4: true
    enp0s2:
      dhcp4: true

$ sudo netplan apply

 

sudo ip addr 을 실행하여 enp0s2 가 아직 DOWN 상태라면 아래 커맨드를 실행한다.

$ sudo ip link set enp0s2 up

 

11. 각 노드에 필요한 패키지들 설치

아래 커맨드를 실행하여 각 노드에 추가 패키지를 설치한다.

# k8s 구성 및 운영에 필요한 패키지들 설치
$ sudo apt-get update
$ sudo apt-get install -y \
  conntrack \
  ipset \
  iptables \
  ebtables \
  ethtool \
  socat \
  ipvsadm

 

12. control-plane 초기화(master01 노드)

master 노드에서 아래 커맨드를 순차적으로 실행하여 kubernetes control plane 초기화를 진행한다.

# 초기화 실행. <MASTER_NODE_BRIDGE_IP> 에는 master 노드의 브릿지 네트워크 ip를 넣는다.
$ sudo kubeadm init --kubernetes-version=1.31.3 \
  --pod-network-cidr=10.244.0.0/16 \
  --apiserver-advertise-address=<MASTER_NODE_BRIDGE_IP>

# 성공 후 출력되는 메시지 가이드에 따라 진행하면 됩니다.(아래는 예시)
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

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

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/

# flannel CNI 로 파드 네트워크 구성시 예시
$ kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

# flannel 파드 네트워크 제거 예시(CNI 파드 네트워크 구성중 문제 발생시 제거하고 다시 설치해야 할 때)
$ kubectl delete -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml --ignore-not-found=true

Then you can join any number of worker nodes by running the following on each as root:

# 아래 join 커맨드를 복사해놓고, 각 worker node 에서 실행해줍니다.
kubeadm join...

# 가이드에는 없는데, node join 후 control plane 에서 containerd 를 재시작 해줘야 함
$ sudo systemctl restart containerd

 

만약 중간에 실패하거나 다시 init 하려면 아래 커맨드로 reset 실행 후 다시 init 진행한다.

$ sudo kubeadm reset -f
$ sudo systemctl stop kubelet
$ sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X
$ sudo rm -rf /etc/cni/net.d & \
  sudo ipvsadm --clear & \
  sudo rm -rf $HOME/.kube/config & \
  sudo rm -rf $HOME/.kube/cache

 

만약 flannel pod(DaemonSet) 가 생성되지 않는다면 모든 노드(master/worker)에서 아래 순서대로 실행한다.

# 모듈 로드
$ sudo modprobe br_netfilter
$ sudo modprobe overlay

# 부팅시 유지
$ cat <<'EOF' | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# 필요한 sysctl 설정 적용
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
$ sudo sysctl --system

 

master 노드에서 아래 명령어로 flannel 재시작 후 파드를 확인한다.

# flannel 재시작
$ kubectl -n kube-flannel rollout restart ds/kube-flannel-ds

# 정상 확인
$ kubectl -n kube-flannel get pods -o wide
$ kubectl -n kube-system get pods

 

13. host 머신에서 가상머신으로 공유 폴더 설정하기(선택)

UTM 에서 가상머신 편집 > 공유에서 아래를 설정한다.

  • 디렉터리 공유 모드: VirtFS
  • 경로: 공유폴더 지정

가상머신 부팅 및 로그인 후 아래 커맨드를 실행한다.

$ sudo mkdir /media/<공유폴더이름>
$ sudo mount -t 9p -o trans=virtio share /media/<공유폴더이름> -oversion=9p2000.L
$ sudo vi /etc/fstab
# 최하단에 아래 내용 추가하고 저장
share /media/<공유폴더이름> 9p trans=virtio,version=9p2000.L,rw,_netdev,nofail 0 0

 

 

이제 가상머신을 재실행하면 아래 경로에서 공유폴더를 확인 가능하다.

  • /media/<공유폴더이름>

참고로, 공유폴더 외부에서 공유폴터 내부로 심볼릭 링크 된 파일은 가상머신에서 내용을 볼 수 없다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

None-Selector 서비스는 ClusterIP 서비스의 일종으로(일반적으로 클러스터 내부에서는 ClusterIP 서비스를 통해 로드 밸런싱을 하기 때문에 이를 기준으로 설명한다.), 말 그대로 ClusterIP 서비스의 selector 를 지정하지 않는 대신, Service와 동일한 이름의 Endpoints 를 생성하고 Endpoints 에 subsets[].address[] 를 배열로 지정하여 Service와 목적지를 1:N 으로 매핑하도록 구성해준다.(로드 밸런싱 포함) Endpoints 에서 지정하는 로드 밸런싱 대상 멤버에는 외부 서비스의 IP 들을 지정하면 된다. Endpoints 에 지정된 엔드포인트들을 타켓으로 해야 하기 때문에 당연히 spec.externalName 도 지정하면 안된다.

 

1. 생성

Service 와 Endpoints 리소스를 각각 생성한다. 중요한 것은, 서비스와 엔드포인트 리소스의 이름이 일치해야 한다는 것이다. 서비스에는 spec.selector 와 spec.externalName 을 지정하지 않아야 한다.

 

서비스

apiVersion: v1
kind: Service
metadata:
  name: none-selector
spec:
  type: ClusterIP
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80

 

엔드포인트

apiVersion: v1
kind: Endpoints
metadata:
  name: none-selector
subsets:
  - addresses:
      - ip: 192.168.1.1
      - ip: 192.168.1.2
    ports:
      - protocol: TCP
        port: 80

 

2. 로드 밸런싱 대상 변경

다음 커맨드로 서비스에 엔드포인트 대상 멤버가 등록되었는지 확인한다.

$ kubectl describe svc none-selector
Name:                     none-selector
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 <none>
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.201.140
IPs:                      10.96.201.140
Port:                     <unset>  8080/TCP
TargetPort:               80/TCP
Endpoints:                192.168.1.1:80,192.168.1.2:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

 

이제 엔드포인트 리소스에서 멤버만 추가/삭제하면 로드 밸런싱 대상이 변경된다.

참고로, ClusterIP 나 LoadBalancer 서비스를 생성하여도 자동으로 로드 밸런싱 대상인 엔드포인트 리소스가 생성된다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

일반적인 서비스 리소스의 경우 <서비스명>.<네임스페이스명>.svc.cluster.local 과 같은 규칙으로 도메인 이름 해석을 진행한다. FQDN 를 정방향 질의하면 서비스에서 해석된 내부의 ClusterIP(헤드리스 서비스의 경우 파드IP) 를 반환하는 것이다.

반면 ExternalName 는 클러스터 내부에서 클러스터 외부의 도메인을 CNAME으로 호출하기 위한 DNS 역할을 한다. 주로 다음과 같은 경우에 사용하게 된다.

  • 외부 도메인을 클러스터 내부에서 사용할 때 다른 도메인으로 사용하고 싶은 경우
  • 애플리케이션에서 호출하는 엔드포인트를 변경하지 않고도 내/외부 서비스를 전환하고 싶은 경우

 

1. 생성

다음과 같은 매니페스트로 생성한다. spec.externalName 항목에 지정한 값이 CNAME 으로 사용된다.

kind: Service
apiVersion: v1
metadata:
  name: sample-externalname
  namespace: default
spec:
  type: ExternalName
  externalName: external.example.com

 

다음 커맨드로 생성한 서비스를 조회해보면 EXTERNAL-IP 에 CNAME FQDN 이 출력된다.

$ kubectl get services sample-externalname
NAME                  TYPE           CLUSTER-IP   EXTERNAL-IP            PORT(S)   AGE
sample-externalname   ExternalName   <none>       external.example.com   <none>    2s

 

서비스에 <서비스명>.<네임스페이스명>.svc.cluster.local 로 질의해보면 CNAME 이 반환된다.

$ kubectl run --image=amsy810/tools:v2.0 --restart=Never --rm -i testpod --command -
- dig sample-externalname.default.svc.cluster.local CNAME
...
;; QUESTION SECTION:
;sample-externalname.default.svc.cluster.local. IN CNAME

;; ANSWER SECTION:
sample-externalname.default.svc.cluster.local. 30 IN CNAME external.example.com.
...
pod "testpod" deleted

 

2. 외부 서비스의 도메인과 느슨한 결합 확보

외부 서비스를 쿠버네티스 클러스터 내부에서 호출해야 하는 경우에, 애플리케이션에서 외부 서비스의 엔드포인트(도메인이나 IP)를 직접 호출하면, 추후 외부 서비스의 엔드포인트가 변경 되었을 때 애플리케이션의 변경사항이 많아지게 된다. 하지만 ExternalName 서비스를 사용해서 외부 서비스의 엔드포인트를 CNAME 으로 호출할 수 있게 해두면, 목적지 엔드포인트가 변경되더라도 각 애플리케이션의 수정 없이 쿠버네티스 ExternalName 서비스의 spec.externalName 수정만으로 대처할 수 있게 된다.

3. 내/외부 서비스 전환

내부 API 로 호출하던 것을 외부 API 로 호출하도록 전환할 때에도(또는 반대도) ExternalName 을 사용하면 손쉽게 전환 가능하다. 애플리케이션에서 호출하는 도메인의 변경 필요 없이 ClusterIP 서비스를 바라보던 것을 ExternalName 서비스를 바라보도록 서비스 설정만 변경하면 되기 때문이다. 내부 서비스로 호출할 때는 ClusterIP 가 반환하는 A 레코드로, 외부 서비스로 호출하도록 전환 된 후에는 ExternalName 이 반환하는 CNAME 레코드로 호출하게 된다. ExternalName 서비스의 spec.clusterIP 항목의 값을 빈 값("") 으로 설정하여 apply 하면 된다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,