Article - 깊게 탐구하기/kubernetes, k8s 적용기

[쿠버네티스 튜토리얼] 8. 쿠버네티스 클러스터 구축부터 CNI, Ingress 설정까지 직접 해보기

조금씩 차근차근 2026. 3. 9. 19:00

앞서, k3s로 쿠버네티스에 대한 정보를 대략적으로 이해했다.

 

'Article - 깊게 탐구하기/kubernetes, k8s 적용기' 카테고리의 글 목록

목표: 핀잇 실사용자 1명 이상 달성하기 글은 비정기적으로 7시에 올라옵니다.

go-gradually.tistory.com

 

  • 클러스터 내에 노드 등록
  • 각 노드의 적절한 리소스를 할당하는 방식을, "마스터 노드(컨트롤 플레인)"에서 관리
    • 쿠버네티스가 어렵다고 하는 가장 큰 이유
    • 선점형/비선점형 리소스 할당을 이용해, 직접 관리하는 것

이제 k3s 가 자동으로 진행해주던 부분직접 해보며, k8s를 좀 더 깊게 써보자.

  1. 기본 개념 1 - kubeadm, kubectl, kubelet의 구분
  2. 기본 개념 2 - 목표 상태 설정
  3. 모든 노드: k8s 설치 준비
  4. 모든 노드: k8s 설치
  5. 컨트롤 플레인: 클러스터 구축
  6. 컨트롤 플레인: 네트워크 플러그인 구성 - Flannel, Calico
  7. 워커 노드: 클러스터에 join하기
  8. 컨트롤 플레인: Ingress Controller 설정하기
이러한 과정을 통해, sh 파일로 자동화 코드를 작성할 때 사용할 수 있을 수준까지 이해해 볼 것이다.

1. kubeadm, kubectl, kubelet의 구분

기존 k3s를 사용할 땐, kubectl / kubelet에 대해서만 이해하면 됐다.

 

하지만, k8s를 초기부터 구축하는 순간, kubeadm을 이해해야 한다.

 

이 세가지의 차이점을 이해해보자.

  1. kubeadm - 클러스터 부트스트랩 생성 도구
    • kubeadm init : 컨트롤 플레인 구성(인증서, kubeconfig, static pod 매니페스트 등 생성)
    • kubeadm join : 워커/추가 컨트롤플레인 노드를 클러스터에 편입
    • kubeadm upgrade : kubeadm 워크플로우 기반 업그레이드
  2. kubectl - 클러스터 "운영/제어" CLI
    • 클러스터가 아직 없음kubeadm init/join 같은 “부트스트랩”이 필요
    • 클러스터가 이미 있음kubectl로 “리소스 운영”을 함
  3. kubelet: 각 노드의 에이전트(데몬)
    • 해당 노드에서 파드를 실제로 실행·감시(컨테이너 런타임과 연동)
    • kubectl이 아니라 컨트롤 플레인이 kubelet에 지시하는 구조

 


2. 목표 상태 설정

새로운 프로젝트를 시작할 땐, 프로젝트의 완료(DoD)를 가정해야 한다.
우리는 이번 글에서, 다음 구성을 완성할 것이다.

  • 구성: 컨트롤 플레인 노드 1대 + 워커 노드 N대(가정)
  • 방화벽/보안그룹: 최소 아래 포트가 통과해야 컨트롤 플레인-노드 간 통신이 된다.
    • Control plane: TCP 6443, 2379-2380, 10250, 10257, 10259
    • Worker: TCP 10250, 10256, (NodePort 사용 시) 30000-32767

3. 모든 노드: k8s 설치 준비

1) 모든 노드에서: 커널 모듈 + sysctl

Flannel 포함 대부분의 CNI에서 브리지 트래픽/포워딩 설정이 필요하다.

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf  
overlay  
br_netfilter  
EOF
sudo modprobe overlay  
sudo modprobe br_netfilter  
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

통과하면, 다음과 같은 값을 볼 수 있다.

 

 

2) 모든 노드에서: swap off

쿠버네티스는 기본적으로 swap 메모리 사용 시 kubelet이 동작하지 않는다.

 

Installing kubeadm

This page shows how to install the kubeadm toolbox. For information on how to create a cluster with kubeadm once you have performed this installation process, see the Creating a cluster with kubeadm page. This installation guide is for Kubernetes v1.35. If

kubernetes.io

 

따라서, 다음과 같이 스왑 메모리 사용을 꺼두자.

sudo swapoff -a

재부팅 시에도 스왑 메모리를 사용하지 않도록, /swap.~~~라인을 주석 처리했다.

 

 

3) 모든 노드에서: containerd 설치 + systemd cgroup 설정

containerd란?

containerd는 Kubernetes가 컨테이너를 실행하기 위해 사용하는 “컨테이너 런타임(runtime)"이다.
즉, 도커 컨테이너를 쿠버네티스에 최적화하여 사용하기 위한 구현체 중 하나이다.

예전에는 kubernetes가 Docker 엔진을 직접 런타임으로 쓰는 구성이 흔했다.
현재는 k8s가 표준 인터페이스(CRI)로 런타임과 통신하고, containerd는 그 CRI를 제공하는 대표 런타임이다.
Docker를 쓰더라도 내부적으로는 containerd + runc가 동작하는 구조인 경우가 많다.

그럼 이제 containerd를 설치해보자.

sudo apt update
sudo apt install containerd

이후, 기본 설정을 지정해줘야 한다.

sudo mkdir -p /etc/containerd  
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null

이후, config.toml 에서 kubelet과 container runtime의 cgroup driver를 systemd로 맞추는 것이 좋다.

 

용어 설명

  • kubelet: 노드 에이전트
  • containerd: 실제 파드를 띄우고, 관리하는 런타임 주체
  • cgroups: 리소스 제어

즉, kubelet과 containerd가 “리소스 제어(cgroups)”를 같은 방식(systemd)으로 관리하게 맞추는 작업이다.

 

쿠버네티스 공식 문서에는 이를 권장한다.

 

Container Runtimes

Note: Dockershim has been removed from the Kubernetes project as of release 1.24. Read the Dockershim Removal FAQ for further details. You need to install a container runtime into each node in the cluster so that Pods can run there. This page outlines what

kubernetes.io

 

/etc/containerd 폴더에 들어가서, 다음 옵션을 true로 맞춰주자.

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]  
SystemdCgroup = true

 

주의: containerd 2.x는 섹션 경로가 다름에 유의하자.

이후, containerd를 항상 시작하게 해두고, 재시작해주자.

sudo systemctl enable --now containerd  
sudo systemctl restart containerd

참고로, containerd는 Kubernetes 전용이 아니라, systemd가 없는/다른 init을 쓰는 환경에서도 쓰인다.
그래서 기본 값으론 false로 되어 있고, 필요할 때(쿠버네티스 환경)에만 true로 설정하는 방식이 흔하다.


4. 모든 노드: k8s 설치

먼저, 다음 패키지들을 설치한다.

sudo apt-get update  
sudo apt-get install -y apt-transport-https ca-certificates curl gpg

이후, k8s 패키지 저장소를 apt가 신뢰하고 사용할 수 있도록, 저장소 서명 키 등록 후 url을 추가하자.

sudo mkdir -p -m 755 /etc/apt/keyrings  
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key \  
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /' \  
| sudo tee /etc/apt/sources.list.d/kubernetes.list

이후, 위에서 언급한 kubelet, kubeadm, kubectl을 설치해주자.

sudo apt-get update  
sudo apt-get install -y kubelet kubeadm kubectl  
sudo apt-mark hold kubelet kubeadm kubectl

참고로, 버전 호환은 다음과 같이 붙여주는 것이 좋다.

  • kubectl클러스터와 마이너 버전 ±1 범위 권장
  • kubeletAPI server 버전보다 높으면 안 됨

5. 클러스터 구축 - 첫 클러스터 생성

이제 클러스터를 생성해보자.

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

여기서 사이더를 10.244.0.0/16을 사용했는데, 이는 단순히 Flannel 이 기본 예시로 해당 사이더를 사용하기 때문이다.
만약 다른 cidr를 사용하고 싶다면, Flannel 매니페스트를 수정하면 된다.

 

이후, kubectl을 위한 기본 config파일을 만들어주자.

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

참고: 만약 싱글 노드로 사용한다면

만약 자신이 단일 노드로 k8s를 구축했다면, 다음 명령어를 추가해주자.

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

기본적으로 컨트롤 플레인 노드는 워커 파드 배치를 할당받지 않는다.
하지만, 단일 노드로 사용한다면 컨트롤 플레인 노드가 워커 노드로도 사용되어야 하기 때문에, 위 명령어를 통해 제한을 풀어두는 것이다.


6. 네트워크 플러그인 구성 - Flannel, Calico (컨트롤 플레인 노드에서)

이제 네트워크 설정만 마무리 지으면, 기본적인 클러스터 구축은 완료할 수 있다.
이제 CNI 설정을 진행해보자.

 

CNI 설정으로는 크게 다음 세 가지를 들 수 있다.

  • Calico
  • Flannel
  • Cilium(잘 모름)

이중에서 어느정도 아는 두가지인 CalicoFlannel을 구분하자면,

  • NetworkPolicy (쿠버네티스 Pod 간 네트워크 정책) 기능이 필요하다면, Calico 사용
  • NetworkPolicy 기능이 굳이 필요 없고, 직접 애플리케이션끼리 비교하면 된다? Flannel 사용

나의 경우, k3s 에서 사용하던 Flannel이 좀 더 익숙하기도 하고, 해당 NetworkPolicy 기능이 필요 없기에, Flannel을 사용하기로 결정했다.

 

우선, 네트워크 포트 방화벽 설정을 진행해두자.

NODE_CIDR="10.244.0.0/16" # 본인의 파드가 연결될 네트워크 CIDR를 지정해두자.
# 이는 방굼 위에서 설정한 Flannel 기본 사이더이다.

# iptables을 사용한다면 그에 맞춰 환경설정을 진행해주자.
sudo ufw allow from $NODE_CIDR to any port 6443 proto tcp

sudo ufw allow from $NODE_CIDR to any port 2379:2380 proto tcp

sudo ufw allow from $NODE_CIDR to any port 10250 proto tcp
sudo ufw allow from $NODE_CIDR to any port 10257 proto tcp
sudo ufw allow from $NODE_CIDR to any port 10259 proto tcp

잠깐, 이 방화벽은 왜 여는거지? - CNI의 역할

아무리 CIDR이라지만, 방화벽을 이렇게 시키는대로 여는게 불안할 수 있다.
나 또한 이게 불안해서, 이걸 왜 여는지 찾아보았다.

클러스터 내 IP 통신을 굳이 노드가 알아야 하나?

 

라는 의문이 들었기 때문이다.

 

이에 대한 답을 내려보자면, 다음과 같다.

  • Pod CIDR은 클러스터 내 라우팅 전용 서비스 파드가 존재해서 이 파드가 라우팅 하는 게 아니다.
  • 클러스터 내 pod간 통신은, 기본적으로 각 노드의 Linux 커널 라우팅 테이블 + CNI의 협동으로 동작한다.

이게 무슨 소린지, 다음 두 예시를 살펴보며 이해해보자.

단일 노드

  • Pod 간/Pod→노드 트래픽은 노드 내부에서만 움직인다.
  • 커널 라우팅이 보통 이렇게 진행된다.
    • 10.244.0.0/16cni0(브리지) 또는 CNI가 만든 인터페이스로 로컬 라우팅
  • 노드 커널이 라우터 역할을 하고, CNI가 cni0, veth, route를 만들어준다.

멀티 노드

  • 각 노드는 자기만의 PodCIDR(예: 노드A=10.244.1.0/24, 노드B=10.244.2.0/24)을 가진다.
  • flannel은 기본 옵션으로, 오버레이(VXLAN)로 동작한다.
    • 다른 노드 PodCIDR로 가는 패킷을 flannel.1 같은 터널로 캡슐화해서 노드 간에 전달을 수행한다.
    • 쉽게 말해, 수신지 IP가 CNI에 의해 노드 IP로 감싸져 전송되는 것이다.
    • 이후 해당 노드에 도착하는 순간, 노드 IP가 벗겨지고 그 안의 PodCIDR을 이용해 내부 Pod로 전달된다.

참고) ClusterIP는 누가 처리하지?

이때, 한 가지 궁금증이 떠오를 수 있다.

  • ClusterIP는 누가 처리하지?

ClusterIP는 Pod가 바뀌어도 동일한 IP/이름으로 접근 가능하게 해주는데, 어떻게 해결되는걸까?

 

ClusterIP를 해결해주는 것은 kube-proxy이다.

  • Pod IP(10.244.x.x)는 Pod 재생성 시 바뀔 수 있다.
  • Service는 고정된 ClusterIP(예: 10.96.0.1) 와 DNS 이름을 제공한다.
    • 예: kubernetes.default.svc10.96.0.1
  • Service는 라벨 셀렉터로 여러 Pod를 묶는다.
  • 클러스터 내부에서 Service IP로 트래픽을 보내면, 실제로는 여러 Endpoint Pod 중 하나로 분산된다.
    • 예: kube-dns 서비스 → 여러 CoreDNS Pod로 분산

즉, ClusterIP는 커널 라우팅의 영역이 아니라, NAT/DNAT를 담당하는 kube-proxy의 역할인 것이다.
따라서, ufw에 해당 10.96.0.1 과 같은 IP를 allow/deny할 필요는 없다.


이제 kubectl에서 Flannel을 적용해보자.

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

이후 coredns를 한번 재시작해주고,

kubectl -n kube-system rollout restart deploy/coredns

다음 명령어를 통해 Flannel이 잘 적용되었는지 확인하면 된다.

kubectl get pods -n kube-flannel  
kubectl get ds -n kube-flannel  
kubectl get pods -n kube-system

 


7. 워커 노드에서 - 클러스터에 조인하기

이제 다른 워커 노드를 클러스터에 조인할 수 있다.

 

이 조인 명령어는 컨트롤 플레인에서 다음 명령어로 확인할 수 있다.

kubeadm token create --print-join-command

이를 수행하면, 다음과 같은 명령어가 나온다.

 

현재 필자의 경우, 네트워크가 공유기에 물려있기 때문에, 사설 IP 대역을 쓰고 있다.
따라서 이 경우, 공유기에서 포트포워딩을 해주고,
명령어의 IP를 공유기 IP로 바꿔준 뒤 해당 명령어를 입력해주어야 한다.

만약 컨트롤 플레인이 여러 대(고가용성 확보)라면, 공용 엔드포인트를 작성해주면 된다.


8. Ingress Controller 설정하기

이제 Ingress Controller를 설정할 차례다.
나의 경우 k3s 를 먼저 썼기 때문에, 자연스레 nginx보다 traefik 쪽에 더 손이 갔다.

 

따라서, Traefik Ingress Controller + cert-manager + Let's Encrypt HTTP-01 구성을 사용하기로 결정했다.
여기에, 핀잇을 해당 클러스터로 옮길 것이기에, HTTPS 설정까지 진행할 것이다.

 

이는 크게 다음과 같은 과정으로 진행된다.

1) Traefik 설치
2) cert-manager 설치
3) le ClusterIssuer 생성
4) MetalLB 설치
5) 테스트를 위한 예시 애플리케이션 배포
    - 생략 가능, 하지만 정상 작동을 위해 테스트 수행해보는 것을 추천함
6) Ingress 생성 + HTTPS 연결
7) DNS 연결
8) 확인 명령

그대로 따라해보고 싶다면, 개인 소유 도메인을 가지고 진행할 것을 권장한다.
만약 개인 도메인이 없다면, 1, 4, 5번만 진행하자.

 

 

1) Traefik 설치

Traefik를 설치할 때는 다른 controller와 충돌하지 않게 ingressClassName: traefik를 명시해서 쓰는 편이 안전하다.

 

특히 chart 기본값은 IngressClass를 default로 만들 수 있으므로, 기존 NGINX Ingress 등이 있으면 isDefaultClass: false로 두는 편이 낫다.

 

HTTP→HTTPS 리다이렉트는 ports.web.http.redirections.entryPoint로 설정할 수 있고, 이를 통해 websecure 엔트리포인트에 TLS를 활성화할 수 있다.

values-traefik.yaml

deployment:
  replicas: 2 # Traefik 레플리카 수

ingressClass:
  enabled: true
  isDefaultClass: false
  name: traefik

providers:
  kubernetesIngress:
    enabled: true
  kubernetesCRD:
    enabled: true
  kubernetesGateway:
    enabled: false

ports:
  web:
    exposedPort: 80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    exposedPort: 443

설치

helm repo add traefik https://traefik.github.io/charts
helm repo update

kubectl create namespace traefik

helm upgrade --install traefik traefik/traefik \
  --namespace traefik \
  -f values-traefik.yaml

확인

kubectl get pods -n traefik
kubectl get svc -n traefik
kubectl get ingressclass

지금까지 설정한 내용 + 기본 설정 내용까지 엮어서, 동작 원리를 조금만 이해해보자.

쿠버네티스의 요청-응답 처리 흐름은 크게 두 가지 방식으로 시선으로 볼 수 있다.

  • 물리적 관점
    • 요청-응답이 어느 노드를 통과하는가?
  • 논리적 관점
    • 요청-응답이 어느 Ingress Controller를 통과하는가?
    • 요청-응답이 어느 Service를 확인하는가?
    • 요청-응답이 어느 Pod를 통해 처리되는가?

즉, 노드에 도착한 요청이 특청 IngressController에 도착할 수단이 필요하다.

 

따라서, 보통은 외부 LB에서 "Ingress Controller가 존재하는 노드"로 로드 밸런싱을 수행한다.
이때, 각 Ingress Controller는 적절한 서비스를 찾아 추가적으로 로드 밸런싱을 수행하는 것이다.

 

곧, 이를 연결하는 외부 LB의 간단한 구현체인 MetalLB의 설치를 진행할 것이다.

 

 

 

2) cert-manager 설치

cert-manager를 helm을 통해 설치해주자.

helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --version v1.19.4 \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true

다음 명령어로, 설치가 잘 되었는지 확인해주자.

kubectl get pods -n cert-manager

 

 

3) Let’s Encrypt ClusterIssuer 생성

Let’s Encrypt는 staging으로 먼저 검증한 뒤 production으로 전환하는 것이 안전하다.
항상 먼저 잘 돌아가는지 확인하고, 그 다음에 실제 배포판으로 전환하자.

cluster-issuer.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: <YOUR_EMAIL> # 당신의 이메일로 바꿔주세요.
    server: zm # 스테이징 링크
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: <YOUR_EMAIL> # 당신의 이메일로 바꿔주세요.
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik

이를 적용한 후, 잘 적용되었는지 확인해보자.

kubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer

잘 적용되었는지 확인이 완료되었다면, letsencrypt.org링크를 참고해, 위의 스테이징 링크를 production 링크로 바꿔주자.

참고로, 현재 글 작성 시점 기준 production 링크는 다음과 같았다.
https://acme-v02.api.letsencrypt.org/directory

 

 

4) MetalLB 설치

MetalLB 를 쓰는 이유 - 작동과정

온프레미스, 홈랩, kubeadm 기반 Kubernetes 환경에서는 Service type=LoadBalancer를 생성해도 EXTERNAL-IP<pending> 상태로 남는다.

우리의 노드는 분명 IP를 갖는데, EXTERNAL-IP는 왜 <pending> 상태로 남아있는 걸까?

 

그 원인은 WAN IP가 없어서가 아니라, Kubernetes에 LoadBalancer 구현체가 없어서이다.

 

이를 이해하기 위해선 네트워크의 구성 방식에 대한 간단한 이해가 필요하다.

  • 일반적으로 네트워크를 사용할 땐, WAN IP를 직접 사용하지 않고 내부적으로 CIDR를 만들어 네트워크를 만들어 사용한다. (LAN을 만든다.)
  • 만약, 노드의 LAN IP를 직접 사용하게 되면, 해당 노드를 직접 사용하는데 불편함이 발생하는 등, 여러모로 번거로워진다.
  • 그래서, k8s는 클러스터 내로 요청을 받을 때, 일반적으로 LAN 내 사설 IP를 하나 추가로 할당받아 사용한다.

그러면, 어떻게 자신의 IP를 다른 L3 장비에 알릴 수 있을까?
바로 이 때 필요한게, MetalLB이다.

 

 

 

MetalLB는 보통 LAN/L3 네트워크에서 LoadBalancer IP를 제공하는 도구로, bare metal Kubernetes 환경에서 LoadBalancer 타입 서비스를 실제로 동작하게 만들어주는 구현체이다.

 

설치와 설정이 끝나면 spec.type: LoadBalancer 서비스에 대해 MetalLB가 외부에 노출할 IP를 할당하고, 해당 IP를 로컬 네트워크에 광고한다.

 

ARP - 주소 결정 프로토콜

본 게시글은 그림으로 공부하는 TCP/IP 구조 도서를 참고하였습니다. IP주소는 OS상에서 설정한 논리적인 주소인데, MAC는 NIC 자체에 내장되어 있는 물리적인 주소이다.IP 주소는 DNS, 다른 전달 방식

dev.go-gradually.me

 

 

만약 Traefik만 설치한 상태라면, Traefik Service가 이미 LoadBalancer 타입이지만 외부 IP를 줄 컴포넌트가 없어서 <pending> 상태가 된다.
이때 MetalLB를 추가하면, Traefik 앞단에 사용할 IP를 직접 정의한 IP 풀에서 할당할 수 있고, 그 결과 http://도메인 또는 https://도메인 형태의 진입점을 만들 수 있게 된다.

여기서 “IP를 준다”는 뜻은, MetalLB가 LoadBalancer 타입 Service에 대해 클러스터 외부에서 접속할 수 있는 가상 IP 하나를 할당한다는 의미이다.

  • 노드 실제 IP: 172.30.1.35
  • MetalLB가 Service에 할당한 IP: 예를 들어 172.30.1.240

 

172.30.1.240이 노드의 ip addr에 항상 직접 올라가 있는 건 아닐 수 있지만, 같은 L2 네트워크에서 누군가 172.30.1.240의 MAC 주소를 물으면 MetalLB speaker가 ARP 응답을 해줄 수 있게 된다.
그러면 클라이언트는 “아, 172.30.1.240 패킷은 이 노드 MAC으로 보내면 되는구나”라고 인식한다.

 

이를 작동 흐름으로 구성해보면 다음과 같다.

[Public/Private IP Pool] -> [MetalLB] -> [Service type=LoadBalancer] -> [Traefik]

 

 

 

설치

설치 방법은 크게 세 단계로 이루어진다.

  • MetalLB 설치
    • Manifest
    • Kustomize
    • Helm
  • IPAddressPool 생성
  • L2Advertisement 생성

아래 두 가지 작업이 존재하는데, 이는 다음과 같은 역할을 수행한다.

  1. 관리자가 IPAddressPool로 사용할 IP 대역을 정의한다.
  2. 그 다음 MetalLB가 LoadBalancer 타입 서비스에 이 IP 중 하나를 할당한다.
  3. 우리가 사용하게 될 L2 모드에서는 L2Advertisement가 연결된 IP 풀의 주소를 로컬 네트워크에 광고하며, 이때 별도로 노드 NIC에 IP를 직접 바인딩하는 방식이 아니라 ARP 응답을 통해 해당 IP가 어떤 노드의 MAC 주소로 도달해야 하는지 알리는 방식으로 동작한다.

 

즉, L2Advertisement를 만들지 않으면 IP를 할당하더라도 네트워크에 광고되지 않아서 외부 접근이 성립하지 않는다.

공식 문서 기준으로 L2 모드는 가장 단순한 설정 방식이다.
선출된 1개 노드가 해당 IP를 광고하는 방식으로 동작한다.

 

Traefik Service가 type: LoadBalancer인 상태에서 MetalLB가 예를 들어 172.30.1.240을 할당하면, 클라이언트는 그 IP로 접속하고, MetalLB가 광고한 노드를 통해 최종적으로 Traefik Pod까지 트래픽을 전달할 수 있게 된다.

 

이제 이 구성대로 설치를 진행해보자.

 

 

 

 

(1) 가장 단순한 설치 - Manifest 이용

https://metallb.io/installation/

 

Installation :: MetalLB, bare metal load-balancer for Kubernetes

Before starting with installation, make sure you meet all the requirements. In particular, you should pay attention to network addon compatibility. If you’re trying to run MetalLB on a cloud platform, you should also look at the cloud compatibility page

metallb.io

 

공식 설치 문서의 manifest 예시는 작성 시점 기준 v0.15.3 이다.
적용하면 metallb-system 네임스페이스에 controller와 speaker 등이 배포된다.

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.3/config/manifests/metallb-native.yaml

설치 확인을 진행해보자.

kubectl get pods -n metallb-system

 

 

 

(2) IP 풀과 L2 광고 설정

이제 IP을 설정해야 한다.
현재 노드 IP가 172.30.1.35이므로, 같은 대역의 미사용 주소를 IP 풀로 잡는 것이 일반적이다.

 

현재 글에서는 172.30.1.240-172.30.1.250을 사용했는데, 이는 사용자의 환경에 따라 달라져야 할 수 있다.

  • 반드시 DHCP 또는 다른 장비가 사용하지 않는 범위여야 한다.

L2 모드에서는 IPAddressPoolL2Advertisement를 함께 생성해야 하니, 이대로 진행해주자.

 

 

metallb-config.yaml

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: traefik-pool
  namespace: metallb-system
spec:
  addresses:
    - 172.30.1.240-172.30.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: traefik-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - traefik-pool
kubectl apply -f metallb-config.yaml

 

 

 

(3) Traefik Service에 EXTERNAL-IP 할당 확인

 

MetalLB 설정이 끝나면 type: LoadBalancer 서비스에 대해 MetalLB가 IP를 할당하게 된다.
Traefik Service는 이미 LoadBalancer 타입이므로 별도 변경 없이도 EXTERNAL-IP가 채워지는지 확인하면 된다.

 

MetalLB는 자신이 제어하는 서비스에 대해 이벤트도 남기므로, 이상이 있으면 kubectl describe service로 확인할 수 있다.
문제가 생기면, 이 부분을 자세히 분석해보면 된다.

kubectl get svc -n traefik
kubectl describe svc traefik -n traefik

5) 예시 애플리케이션 배포

app.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  namespace: demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: demo-svc
  namespace: demo
spec:
  selector:
    app: demo-app
  ports:
    - port: 80
      targetPort: 80
kubectl apply -f app.yaml

테스트

이제 앞서 등록한 MetalLB가 잘 동작하는지 테스트해보자.

EXTERNAL-IP172.30.1.240으로 잡혔다면, 우선 내부망에서 아래처럼 확인할 수 있다.

curl -I http://172.30.1.240
curl -k -I https://172.30.1.240

도메인까지 붙일 경우에는 임시로 이렇게 요청을 보내보자.

curl -k -I -H "Host: your_domain.com" https://172.30.1.240

그 다음 Traefik Ingress와 TLS 설정이 맞으면 다음 형태로 접근할 수 있다.

http://your_domain.com
https://your_domain.com

6) Ingress 생성 + HTTPS 연결

Traefik는 Ingress를 보고 라우터를 만들고, router.entrypointsrouter.tls annotation으로 HTTPS 라우터를 제어할 수 있다.
즉, 이를 바탕으로 https를 진행할 수 있다.

 

 

cert-manager는 Ingress에 cert-manager.io/cluster-issuer annotation이 있고 spec.tls[].secretName이 있으면 해당 이름의 Certificate/Secret를 생성한다.

 

Traefik 문서 기준으로 spec.tls.secretName은 어떤 인증서를 로드할지만 정하고, 그것만으로 TLS 라우터가 켜지지는 않으므로 websecure 또는 router.tls: "true"를 가드로 같이 주는 방식이 안전하다.

도메인이 Traefik의 공인 IP를 가리키고 80/443 포트가 외부에서 도달 가능해야 한다.
문제가 발생했다면, 다시 한 번 확인해보자.

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  namespace: demo
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - demo.example.com # 당신의 도메인을 적어주세요
      secretName: demo-example-com-tls
  rules:
    - host: demo.example.com # 당신의 도메인을 적어주세요
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: demo-svc
                port:
                  number: 80
kubectl apply -f ingress.yaml

여기서 네 가지 설정에 주목하자.

  • ingressClassName: traefik은 이 Ingress를 Traefik가 처리하게 만들고,
  • cert-manager.io/cluster-issuer는 발급자를 지정하며,
  • tls.secretName은 cert-manager가 발급한 인증서를 저장할 Secret 이다.
  • router.entrypoints: websecurerouter.tls: "true"는 앞서 이야기한 가드로, Traefik 쪽 HTTPS 라우터를 명시적으로 켠다.

7) DNS 연결

아래 절차가 모두 통과되어야 인증서 발급이 완료된다.

  1. kubectl get svc -n traefik로 external IP 또는 LB hostname 확인
  2. demo.example.com의 DNS A/AAAA 또는 CNAME을 그 주소로 연결
  3. 외부에서 80/443 접근 가능해야 함
  4. 그 다음 cert-manager가 HTTP-01 검증 후 인증서를 발급

8) 확인 명령

kubectl get ingress -n demo
kubectl get certificate -n demo
kubectl get secret -n demo
kubectl describe certificate demo-example-com-tls -n demo
kubectl describe challenge -n demo

정상 흐름이면

  1. Secret/demo-example-com-tls가 생성되고,
  2. 브라우저에서 https://demo.example.com 접속 시 유효한 인증서가 내려온다.
  3. cert-manager는 Ingress를 보고 Certificate를 만들고,
  4. 인증서는 tls.secretName에 지정한 Secret에 저장한다.

이를 바탕으로, 기본적인 k8s 클러스터 구축을 완료했다.

이 이후는, 다음 가이드를 참고해서 각자의 방식의 맞게 배포하면 된다.

 

[Pinit] 핀잇 백엔드 마이크로서비스를 배포할 k8s 클러스터 구축하기

목차인프라 세팅ARC 설치ARC가 사용할 ServiceAccount 정의pinit 네임스페이스에서 role 생성 & sa와 바인딩워크플로우 작성테스트/빌드도커파일 작성도커 이미지 업로드러너가 해당 서비스 어카운트를

dev.go-gradually.me