ARM 서버 Kubernetes Part 4: 외부 접근 환경 구성

오늘은 KVM 가상머신 위에 구성된 K8s 클러스터에 외부 접근 환경을 만드는 작업을 했다.
WiFi 환경이라 브리지 네트워킹이 안 되는 제약이 있었고, OpenVPN + MetalLB + Ingress 조합으로 해결했다.


환경

호스트 서버 (ARM64, Ubuntu 24.04)
  ├── WiFi (192.168.200.100) — 외부 통신
  ├── virbr0 (192.168.122.1/24) — KVM NAT 브리지
  │     ├── k8s-master  (192.168.122.10)
  │     ├── k8s-worker1 (192.168.122.11)
  │     └── k8s-worker2 (192.168.122.12)
  └── OpenVPN 서버 (Docker, UDP 51194)
        VPN 대역: 192.168.255.0/24

K8s: v1.29.15, 3노드 구성


문제: WiFi는 브리지가 안 된다

WiFi 어댑터는 AP가 단일 MAC 주소만 허용하기 때문에, VM들이 외부 IP를 직접 가질 수 없다.
이더넷이었다면 virbr0를 브리지로 연결해 VM이 직접 외부 IP를 받을 수 있지만, WiFi에서는 불가능하다.

대안으로 선택한 방법: OpenVPN

이미 호스트에 OpenVPN 서버가 Docker로 구성되어 있었다. VM마다 클라이언트를 발급해서 VPN 망을 통해 고정 IP를 부여하는 방식을 선택했다.


1단계: K8s VM에 VPN 고정 IP 부여

클라이언트 발급

1
2
3
bash scripts/create-client.sh k8s-master
bash scripts/create-client.sh k8s-worker1
bash scripts/create-client.sh k8s-worker2

CCD로 고정 IP 설정

OpenVPN의 CCD(Client Config Directory)를 사용하면 클라이언트별로 고정 IP를 지정할 수 있다.

1
2
3
4
5
6
7
8
# /etc/openvpn/ccd/k8s-master
ifconfig-push 192.168.255.10 255.255.255.0

# /etc/openvpn/ccd/k8s-worker1
ifconfig-push 192.168.255.11 255.255.255.0

# /etc/openvpn/ccd/k8s-worker2
ifconfig-push 192.168.255.12 255.255.255.0

openvpn.conf에 다음 두 줄 추가:

client-config-dir /etc/openvpn/ccd
topology subnet

VM에 openvpn 설치 및 자동 시작

1
2
3
4
sudo apt-get install -y openvpn
sudo cp k8s-master.ovpn /etc/openvpn/client/k8s-master.conf
sudo systemctl enable openvpn-client@k8s-master
sudo systemctl start openvpn-client@k8s-master

결과적으로 K8s 노드들이 tun0 인터페이스에 VPN IP를 가지게 되었고,
K8s 자체도 이 VPN IP를 INTERNAL-IP로 인식했다.

NAME          INTERNAL-IP      ...
k8s-master    192.168.255.10
k8s-worker1   192.168.255.11
k8s-worker2   192.168.255.12

2단계: MetalLB 설치 (L4 로드밸런서)

클라우드 환경에서는 type: LoadBalancer 서비스에 자동으로 외부 IP가 붙지만, 베어메탈에서는 그게 없다.
MetalLB가 이 역할을 해준다.

설치

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

IP 풀 설정

VPN 대역에서 K8s 노드 IP와 겹치지 않는 범위를 할당한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
cat << 'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: vpn-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.255.100-192.168.255.150
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: vpn-l2advert
  namespace: metallb-system
spec:
  ipAddressPools:
  - vpn-pool
  interfaces:
  - tun0
EOF

L2 모드의 한계: tun 인터페이스에서 ARP가 안 된다

MetalLB L2 모드는 ARP로 IP를 광고하는 방식인데, tun 인터페이스는 L3 전용이라 ARP(L2 브로드캐스트)를 지원하지 않는다.
VPN 클라이언트에서 MetalLB IP로 접근해도 ARP 응답이 없어서 패킷이 버려진다.

해결책: OpenVPN iroute

OpenVPN 서버의 CCD에 iroute를 추가하면, VPN 서버가 해당 IP 대역을 특정 클라이언트(k8s-master)를 통해 라우팅하도록 설정할 수 있다.
ARP 없이 L3 라우팅으로 해결된다.

1
2
3
# /etc/openvpn/ccd/k8s-master
ifconfig-push 192.168.255.10 255.255.255.0
iroute 192.168.255.96 255.255.255.224  # MetalLB 풀 전체 커버 (100~126)

openvpn.conf에도 서버 측 route 추가:

route 192.168.255.96 255.255.255.224

이렇게 하면 MetalLB 풀 전체가 k8s-master를 통해 라우팅되고, kube-proxy가 나머지를 처리한다.
새 서비스가 추가되어도 IP 풀 범위 안이면 별도 설정이 필요 없다.


3단계: Ingress Controller (L7 로드밸런서)

MetalLB는 IP + 포트 수준의 L4만 담당한다. HTTP 경로나 도메인 기반 분기는 Ingress Controller가 필요하다.

nginx-ingress가 이미 설치되어 있었다. NodePort → LoadBalancer로 변경하면 MetalLB가 외부 IP를 자동 할당한다.

1
2
kubectl patch svc ingress-nginx-controller -n ingress-nginx \
  -p '{"spec":{"type":"LoadBalancer"}}'

결과: 192.168.255.101 할당


4단계: nginx + apache 서비스 + 경로 기반 라우팅

서비스 배포

 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
cat << 'EOF' | kubectl apply -f -
# nginx (기존)
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo-svc
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
---
# apache (신규)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apache-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: apache-demo
  template:
    metadata:
      labels:
        app: apache-demo
    spec:
      containers:
      - name: apache
        image: httpd:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: apache-demo-svc
spec:
  selector:
    app: apache-demo
  ports:
  - port: 80
EOF

Ingress 규칙

 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
cat << 'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-demo-svc
            port:
              number: 80
      - path: /apache
        pathType: Prefix
        backend:
          service:
            name: apache-demo-svc
            port:
              number: 80
EOF

배포 검증

1. nginx-demo Deployment 배포

nginx 서비스가 없으면 / 경로가 동작하지 않으므로, nginx-demo를 추가로 배포해야 합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
cat << 'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
EOF

2. 배포 상태 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 모든 Pod 상태 확인
kubectl get pods -A | grep -E "apache|nginx"

# 예상 출력
# default  pod/nginx-demo-75b947f799-5srr9   1/1 Running
# default  pod/apache-demo-86668c88df-jbd6b  1/1 Running
# default  pod/apache-demo-86668c88df-tdmjr  1/1 Running

# Ingress 상태 확인
kubectl get ingress
# 예상 출력
# NAME            CLASS   HOSTS   ADDRESS         PORTS   AGE
# demo-ingress    nginx   *       192.168.255.100 80      2m

3. 외부 접근 테스트

VPN 클라이언트에서 아래 명령어로 테스트:

1
2
3
4
5
6
7
8
9
# nginx 서비스 (/ 경로)
curl -v http://192.168.255.100/

# 예상 결과: HTTP/1.1 200 OK (Welcome to nginx!)

# apache 서비스 (/apache 경로)
curl -v http://192.168.255.100/apache

# 예상 결과: HTTP/1.1 200 OK (It works! Apache httpd)

4. 실제 테스트 결과

/ 경로 (nginx):

HTTP/1.1 200 OK
Content-Type: text/html
...
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working.

/ apache 경로 (apache):

HTTP/1.1 200 OK
Content-Type: text/html
...
It works!
Apache httpd

경로 기반 라우팅 성공!


최종 구조

VPN 클라이언트 (핸드폰, PC 등)
  ↓ OpenVPN 연결 (192.168.255.x)
  │
  └── 192.168.255.100 (Ingress Controller - LoadBalancer)
          │
          ├── GET /        → nginx-demo (1개 Pod)
          │
          └── GET /apache  → apache-demo (2개 Pod)

실제 구성:

  • Ingress External IP: 192.168.255.100 (MetalLB 할당)
  • 경로 분기:
    • / → nginx-demo-svc → nginx-demo Pod (nginx:alpine)
    • /apache → apache-demo-svc → apache-demo Pods (httpd:alpine × 2)

정리

레이어역할담당
VPN외부 클라이언트 → K8s 접근 터널OpenVPN (192.168.255.0/24)
L4외부 IP 할당, 포트 라우팅MetalLB (192.168.255.100-150)
L7경로 기반 HTTP 분기Nginx Ingress Controller

핵심 포인트

  1. WiFi 환경에서 외부 접근: OpenVPN + VPN 고정 IP
  2. 베어메탈 K8s에서 LoadBalancer: MetalLB (클라우드의 LB 대체)
  3. HTTP 경로 분기: Ingress Controller (L7 라우팅)
  4. 실제 서비스: nginx + apache 배포 완료

WiFi 브리지 제약이 있는 환경에서도 OpenVPN iroute를 활용하면 외부에서 K8s 서비스에 깔끔하게 접근할 수 있다.
tap 모드 VPN이나 BGP 라우팅 없이도 L3 라우팅만으로 충분히 해결 가능하다.


작성일: 2026-02-18
검증일: 2026-02-20
최종 상태: ✅ 모든 테스트 성공