minikube에서 파드로 서버 띄우고, 컨테이너 접근하기(MacOS docker driver 이슈)
최근에 쿠버네티스를 통해 간단히 파드로 서버를 띄워야 하는 일이 있었다. 오랜만에 쿠버네티스를 사용해보는 것이라 서버 자체는 빠르게 띄웠는데, 외부에서 접속하는 방법을 잊어버려서 조금 헤맸다. 그래도 minikube를 사용하니까 쉽게 웹 브라우저나 curl 명령어로 요청/응답이 가능했다.
하지만, LoadBalancer 타입의 Service 오브젝트를 실행하면서 의문이 들었었다. 분명 External IP, targetPort도 잘 지정이 되었는데 막상 로컬 환경에서는 접근이 불가했던 것이다. 구글링하면서 문제를 확인하고 나름 해결했는데 회고하는 겸 서버 띄우는 과정들 및 원인, 이를 해결하는 방법들을 공유해보고자 한다. 혹시 이 게시물이 도움이 될 수 있는 사람이 한 사람이라도 있지 않을까?
Minikube로 서버를 띄워보자
minikube는 Kubernetes Cluster 환경을 만들어주는 도구다. minikube 말고도 kubeadm, kops, kubespray 등의 솔루션이 있는데 가장 많이 사용하는 것은 kubeadm 이다. Kubernetes 커뮤니티에서도 kubeadm을 권장한다.
kubeadm은 커스터마이징이 원활하다는 장점이 있지만, 그만큼 설정이 많이 필요하기 때문에 단순하게 학습용으로는 minikube도 적절한 솔루션이 된다.
1️⃣ minikube 설치
MacOS(arm64) 기준으로 minikube는 바이너리 설치 혹은 homebrew로 설치할 수 있다.
https://minikube.sigs.k8s.io/docs/start/
바이너리 파일 다운로드
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64
sudo install minikube-darwin-arm64 /usr/local/bin/minikube
홈 브루 설치
brew install minikube
2️⃣ minikube 실행
minikube를 실행하여 쿠버네티스 환경을 구성한다.
minikube start
😄 Darwin 14.0 (arm64) 의 minikube v1.31.2
✨ 자동적으로 docker 드라이버가 선택되었습니다. 다른 드라이버 목록: qemu2, ssh
📌 Using Docker Desktop driver with root privileges
👍 minikube 클러스터의 minikube 컨트롤 플레인 노드를 시작하는 중
🚜 베이스 이미지를 다운받는 중 ...
🔥 Creating docker container (CPUs=2, Memory=3885MB) ...
🐳 쿠버네티스 v1.27.4 을 Docker 24.0.4 런타임으로 설치하는 중
▪ 인증서 및 키를 생성하는 중 ...
▪ 컨트롤 플레인이 부팅...
▪ RBAC 규칙을 구성하는 중 ...
🔗 Configuring bridge CNI (Container Networking Interface) ...
🔎 Kubernetes 구성 요소를 확인...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 애드온 활성화 : storage-provisioner, default-storageclass
🏄 끝났습니다! kubectl이 "minikube" 클러스터와 "default" 네임스페이스를 기본적으로 사용하도록 구성되었습니다.
생각보다 출력이 귀엽다...
--vm-driver= 옵션으로 원하는 드라이버를 선택할 수 있다. virtualbox도 물론 지원한다. m1의 경우에는 virtualbox가 아직 beta라 사용이 어렵다. 그리고 minikube에서도 MacOS는 기본값인 Docker를 권장한다.
https://minikube.sigs.k8s.io/docs/drivers/
minikube 대시보드 실행
minikube는 대시보드를 제공해준다! 그래서 웹 브라우저로 편하게 네임스페이스 간 오브젝트를 확인할 수 있다.
굳이 사용하지 않더라도 큰 지장은 없다.
minikube dashboard
터미널 실행 후 바로 웹브라우저가 열리므로 이를 원하지 않은 경우 --url 옵션을 사용하여 url만 받을 수도 있다.
참고로 dashboard도 minikube가 기본적으로 제공하는 addon 중 하나다.
3️⃣ 서버 생성
간단하게 사용할 서버를 생성한다. 혹은, nginx 등으로 테스트해도 상관 없을 것 같다.
먼저 nginx로 간단하게 확인할 수 있도록 하면 다음과 같다.
kubectl create deploy nginx --image=nginx
kubectl export deploy nginx --port 80 --target-port=80 --type=NodePort
LoadBalancer 오브젝트가 있다면 LoadBalacner로 타입을 설정해도 된다. NodePort는 직접 노드를 노출시키는 서비스로 Node나 VM의 IP가 바뀐다면 서비스에도 적용을 해줘야 하고, 사용할 수 있는 포트 대역이 정해져 있는 등의 문제가 있어서 권장하는 방법은 아니다.
NestJS 서버 생성
NestJS로 서버를 생성했다.
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
import * as os from 'os';
@Controller()
export class AppController {
@Get()
healthCheck(@Req() request: Request) {
return {
request: {
method: request.method,
url: request.url,
ip: request.ip,
userAgent: request.headers['user-agent'],
},
server: {
host: os.hostname(),
arch: os.arch(),
ethernet0: os
.networkInterfaces()
['en0'].find((ip) => ip.family === 'IPv4'),
},
};
}
}
다른 설정은 안 건드리고 컨트롤러만 수정했다.
4️⃣ 이미지 생성
쿠버네티스속 워커 노드들은 컨테이너로 동작하므로, 이 컨테이너를 위한 이미지가 필요하다.
FROM node:20.9.0
WORKDIR /home/server
COPY . .
RUN yarn install
RUN yarn build
EXPOSE 3000
CMD ["yarn", "start:prod"]
5️⃣ 이미지 Build & Push
해당 이미지를 pull 받아 사용하기 위해서 build 및 push가 필요하다. 또한 내부 레지스트리도 필요하다. 이를 위해서 minikube의 addon 중 하나인 registry를 사용한다.
minikube addons enable registry
registry는 5000번 포트로 열려 있다(만약 다르다면 터미널에 출력된 값들을 확인하자). 이제 이 레지스트리에서 사용할 이미지 이름을 지정해준다.
docker build --tag $(minikube ip):5000/test-img .
레지스트리에 이미지를 push 해준다.
docker push $(minikube ip):5000/test-img
6️⃣ Deployment, Service 오브젝트 파일 작성
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-api
spec:
replicas: 3
selector:
matchLabels:
app: simple-api
tier: back
template:
metadata:
labels:
app: simple-api
tier: back
spec:
containers:
- name: test-api
image: kubernetes-server
imagePullPolicy: Never
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: simple-api
spec:
type: LoadBalancer
selector:
app: simple-api
tier: back
ports:
- port: 3000
targetPort: 3000
디플로이먼트와 서비스를 만들어줬다. 서비스는 디플로이먼트를 외부에 노출시키기 위한 오브젝트 중 하나다. 서비스가 어떤 디플로이먼트를 노출할지는 selector를 통해서 선택하도록 해야 하는데, Deployment에서 matchLabels -> template.metadata.labels로 설정한 labels와 일치하도록 한다.
여기서 이미지는 Push 했던 이미지를 넣어주고, 로컬 레지스트리를 사용하기에 imagePullPolicy는 Never로 설정한다.
7️⃣ 오브젝트 생성
쿠버네티스에 내가 작성했던 파일을 적용시킨다.
kubectl apply -f 오브젝트.yml
이렇게 적용시킨 후에, 파드와 서비스가 정상적으로 생성되었는지 확인한다.
kubectl get pod,svc # 혹은 pods
pod/simple-api-675c54c789-22vhg 1/1 Running 0 14m
pod/simple-api-675c54c789-28f44 1/1 Running 0 14m
pod/simple-api-675c54c789-79ft6 1/1 Running 0 14m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3h11m
service/simple-api LoadBalancer 10.105.113.175 <pending> 3000:32648/TCP 14m
아마 EXTERNAL-IP는 pending 상태로 되어있을 것이다.
8️⃣ minikube tunneling
minikube의 tunnel은 LoadBalancer 유형으로 배포된 서비스에 대한 경로를 생성하고 해당 인그레스를 클러스터IP로 설정한다.(https://minikube.sigs.k8s.io/docs/commands/tunnel/)
터널을 생성해주고, 그 다음 컨테이너를 빌드하면서 사용하는데 그냥 service 명령어를 사용하면 된다.
minikube service 서비스_이름
그러면 아래와같이 tunnel을 설정해준다.
|-----------|------------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------------|-------------|---------------------------|
| default | simple-api | 3000 | http://192.168.49.2:32648 |
|-----------|------------|-------------|---------------------------|
🏃 simple-api 서비스의 터널을 시작하는 중
|-----------|------------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------------|-------------|------------------------|
| default | simple-api | | http://127.0.0.1:59205 |
|-----------|------------|-------------|------------------------|
🎉 Opening service default/simple-api in default browser...
❗ Because you are using a Docker driver on darwin, the terminal needs to be open to run it.
이제 minikube가 개방한 포트로 접속하여 확인할 수 있다.
여기서 들었던 궁금증이 LoadBalancer를 사용하여 tunneling 없이 접속할 수 있지 않을까? 하는 생각이었다. 그때로 돌아가고 싶다.
MetalLB 로드밸런서 구축
로드밸런서는 클라우드 업체에서 제공하는 로드 밸런서를 사용하여 쉽게 로드 밸런싱을 할 수 있는데, 온프레미스의 경우에는 쉽지 않다. 이때, 온프레미스 환경에서도 로드밸런서를 사용할 수 있게 만들어진 프로젝트다. L2(ARP/NDP)나 L3(BGP)로 로드밸런서를 구현할 수 있다.
가장 설정이 간단한 L2로 로드밸런서를 구축한다. 노드 하나가 로드 밸런싱을 모두 수행하기 때문에 트래픽이 한 번에 몰리면 문제가 될 수 있지만, 학습 환경이기 때문에 가장 간단한 방법을 사용하기로 했다.
1️⃣ MetalLB 설치
https://metallb.universe.tf/installation/
Manifest, Kustomize, Helm 등의 방법이 있으나 가장 간단한 menifest 설치로 설치한다.
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
namespace부터 controller, speaker, secret까지 모두 알아서 등록된다.
2️⃣ IPAddressPool 설정
metalLB의 설정이 하나 남았는데 어떤 IP를 사용할지 설정이 필요하다.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
IP Pool을 생성한 다음, L2에서 로드밸런싱할 IP Address Pool을 설정해준다.
아니면 configmap으로 구성하는 방법도 있다.
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.49.2-192.168.49.5
이때, address는 모두 minikube의 IP 범위로 지정해주어야 한다!
3️⃣ Object 생성
역시 apply 로 kubernetes 오브젝트로 적용한다.
kubectl apply -f metallb.yml
💡 Minikube addon 사용
아니면 minikube의 addon을 사용해도 된다.
metallb addon 활성화
minikube addons enable metallb
metallb 설정
minikube configure metallb
minikube addons configure metallb
-- Enter Load Balancer Start IP: 192.168.49.2
-- Enter Load Balancer End IP: 192.168.49.12
▪ Using image quay.io/metallb/speaker:v0.9.6
▪ Using image quay.io/metallb/controller:v0.9.6
✅ metallb 이 성공적으로 설정되었습니다
4️⃣ 서비스 노출
이 상태에서 LoadBalancer 타입의 서비스를 사용하면 끝이다. 그렇게 하면 EXTERNAL-IP가 설정이 된다.
NAME READY STATUS RESTARTS AGE
pod/simple-api-675c54c789-22vhg 1/1 Running 0 27m
pod/simple-api-675c54c789-28f44 1/1 Running 0 27m
pod/simple-api-675c54c789-79ft6 1/1 Running 0 27m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3h24m
service/simple-api LoadBalancer 10.105.113.175 192.168.49.2 3000:32648/TCP 27m
하지만 역시 해당 IP의 포트로 접속을 시도해도 실패했다. ^^...
configmap에서 공식 홈페이지의 오브젝트로도 시도해보고 여러 가지 방법을 사용했지만 잘 안 되더라. 그러다 문득 옛날에 책에서 읽었던 도커 네트워크 내용이 떠올랐다.
흔히 발생하는 도커 포트 포워딩이 문제인 것으로 보인다. 가상환경의 호스트 및 포트가 있고, 가상환경 내 컨테이너의 호스트와 포트가 존재하는데, 이 가상환경의 포트로 요청을 보내면 이에 대한 응답은 컨테이너가 해야 하는데, 가상환경과 컨테이너가 이어지지 않아서 답할 주체가 존재하지 않는 것이다. 우체국에 편지를 부치는데 수신인을 적지 않은 것과 같다.
실제로, minikube가 노출하는 ip가 존재하므로 이를 사용하여 확인을 해봤다. minikube가 열어 놓은 포트 번호를 그대로 사용해서 <minikube ip>:<dashboard port> 로 요청을 보냈을 때 대시보드를 확인할 수 없었다.
그래서 이것과 로드밸런서가 연관이 있는 것으로 예측했다.
실제 stackoverflow에서도 MacOS에서 동작하는 Docker driver에 약간의 제약이 있다고 한다.
This is a known issue, Docker Desktop networking doesn't support ports. You will have to use minikube tunnel.
그래서 해결방법은 세 개 정도가 있는데, Kubernetes port-forwarding, minikube tunnel, minikube VM driver 변경이 있다.
세 가지 모두 다 간단해서 시도해봄직 하다. 나머지는 위에 이미 기술했으니까 kubernetes port-forwarding에 관해서만 언급하고 마무리 하고자 한다.
Kubernetes port-forwarding
kubectl port-forward service/simple-api 3000:3000
// 출력
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000
이렇게 하면 로컬의 3000번 포트로의 요청이 minikube의 3000번 포트로 연결 되어 접속이 가능하다.
docker의 컨테이너 브릿지 인터페이스인 docker0의 IP는 172.17.0.1/16 범위를 사용하므로, 192.168.0.0/16 범위에서 임의의 IP 범위를 가지는 minikube ip로 요청을 보내는 것은 포워딩 하고 있지 않은 가상환경 포트로 연결을 요청하는 것과 유사한 맥락이라고 생각한다. 그래서 tunnel을 열거나 port-forwarding을 해줘야 하지 않나 싶다.
참고
https://metallb.universe.tf/configuration/
https://docs.docker.com/desktop/networking/#known-limitations-use-cases-and-workarounds
https://github.com/kubernetes/minikube/issues/11193#issuecomment-826331511
https://stackoverflow.com/questions/75193305/cannot-access-a-kubernetes-nodeport-service-in-minikube