Kubernetes에 배포된 pod들끼리 통신을 할 때는 클러스터 도메인 주소를 사용한다. Web app 개발 시 로컬에서는 localhost 주소를 주로 사용하기 때문에 다른 pod들과의 통합 테스트를 하기가 어렵다. Telepresence로 로컬에서 kubernetes의 traffic을 훔치는 방법을 알아보자.
Telepresence로 kubernetes 도메인 localhost에서 훔쳐쓰기
Kubernetes web app 개발을 편하게 할 수 없을까?
고통스러운 kubernetes 환경에서의 web app 개발
보통 로컬에서 web app을 개발한다면 localhost 주소로 app을 띄운 뒤에 browser에서 해당 주소에 접근하여 테스트를 하는 것이 가장 기본적이다. 그런데, 만약 web app이 다음과 같은 조건을 만족한다면 개발이 매우 불편해진다.
- Kubernetes에 pod 형태로 배포해야 한다.
- Kubernetes에 배포된 다른 서비스들과 통신해야 한다.
이유는 localhost와 kubernetes 클러스터가 격리되어 있기 때문이다. Kubernetes 클러스터 내부적으로는 클러스터 DNS에 매핑된 도메인 주소를 사용한다. 예를 들어, "littlemobs"라는 namespace에 배포된 "test-app"이라는 서비스는 http://test-app.littlemobs
주소로 접근할 수 있다. 로컬 환경에서는 이 kubernetes 클러스터 DNS 정보가 없기 때문에 다른 서비스와 통신을 하는 부분에서 오류가 발생하게 된다.
로컬 환경의 DNS 설정을 변경하거나 /etc/hosts
를 수정하면 로컬에 띄운 web app에서 kubernetes에 배포된 서비스에 접근할 순 있지만 역방향은 불가능하다. 결국 제대로된 양방향 통신 테스트를 하려면 web app을 도커 이미지로 빌드한 뒤에 kubernetes pod에 배포해야 한다. 코드 한 줄만 바뀌어도 매번 docker build와 kubernetes 배포를 반복해야 한다는 것은 매우 비효율적이다. 더 좋은 방법은 없을까?
Telepresence로 로컬에서 kubernetes traffic 훔치기
로컬 개발 환경에 localhost 주소로 띄운 web app을 마치 kubernetes에 배포된 pod인 것처럼 속일 수 있다면 모든 문제는 해결된다. Telepresence라는 녀석이 이 역할을 해준다. 로컬의 web app과 kubernetes에 배포된 workload 사이에 연결 통로를 만들어서 해당 workload에 드나드는 HTTP 요청을 로컬의 web app으로 라우팅해준다.
이 방식을 활용하면 docker build, push, 그리고 kubernetes 배포를 하지 않더라도 로컬 web app의 최신 코드를 마치 kubernetes cluster 안에서 동작하는 것처럼 만들 수 있다. Kubernetes에 배포된 다른 서비스들과 통신이 가능하면서도 로컬 개발 환경의 hot reloading을 활용할 수 있기 때문에 개발 속도가 매우 빨라진다.
Telepresence 테스트
Telepresence 설치하기
MacOS에서 telepresence를 설치하는 방법을 알아보자. Kubernetes 클러스터는 이미 준비되어 있다고 가정하겠다. 우선 telepresence 실행 파일을 설치하고 관련 helm chart를 kubernetes에 배포해야 한다.
$ brew install telepresenceio/telepresence/telepresence-oss
$ telepresence helm install
잘 설치되었는지 확인하려면 telepresence의 version 정보를 출력해보고 "ambassador"라는 namespace에 traffic-manager라는 deployment가 잘 생성되어 있는지 확인해보면 된다. 다음과 같이 결과가 정상적으로 출력된다면 telepresence를 사용할 준비가 끝난 것이다.
$ telepresence version
OSS Client : v2.22.4
OSS Root Daemon: v2.22.4
OSS User Daemon: v2.22.4
Traffic Manager: not connected
$ kubectl get deployments -n ambassador
NAME READY UP-TO-DATE AVAILABLE AGE
traffic-manager 1/1 1 1 2m37s
Web app 개발하기
FastAPI를 통해 간단한 web app을 만들어서 telepresence를 테스트해보자. FastAPI 개발 환경 구성은 다른 글에서 다루었으므로 생략하겠다. 다음 코드는 기본적으로 "Hello, World!"라는 문자열을 반환하고, GET /attack
요청의 경우 같은 namespace에 배포된 "test2"라는 이름의 서비스에 get 요청을 날리는 간단한 예제이다.
from fastapi import FastAPI
import requests
app = FastAPI()
@app.get("/")
def hello():
return "Hello, World!"
@app.get("/attack")
def attack():
try:
response = requests.get("http://test2:9999")
return {
"status_code": response.status_code,
"message": response.text,
}
except Exception as e:
return {
"status_code": 500,
"message": str(e),
}
$ uvicorn main:app --host 0.0.0.0 --port 9999 --reload
INFO: Will watch for changes in these directories: ['/Users/littlemobs/workspace/telepresence']
INFO: Uvicorn running on http://0.0.0.0:9999 (Press CTRL+C to quit)
INFO: Started reloader process [62453] using WatchFiles
INFO: Started server process [62455]
INFO: Waiting for application startup.
INFO: Application startup complete.
로컬에서 web app을 실행한 뒤 browser로 접근해보면 다음과 같이 GET /
에 대해서는 "Hello, World"가 잘 출력되지만 GET /attack
의 경우 http://test2:9999
를 DNS에서 찾을 수 없어서 통신 오류가 발생하는 것을 확인할 수 있다.
GET /
"Hello, World!"
GET /attack
{"status_code":500,"message":"HTTPConnectionPool(host='test2', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10582f670>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))"}
Kubernetes에 배포해두기
http://test2:9999
로 요청을 보내는 web app을 kubernetes에 배포해서 테스트해보겠다. 작성한 코드를 http://test1:9999
로 접근할 수 있도록 설정해서 배포해보자. Dockerfile과 kubernetes yaml 파일은 다음과 같이 간단하게 작성했다. Docker image 이름은 상황에 맞게 적절한 것으로 변경하여 사용하면 된다.
FROM python:3.10-alpine
ARG WORKSPACE=/opt/telepresence
WORKDIR ${WORKSPACE}
RUN mkdir -p ${WORKSPACE}
RUN pip install --upgrade pip
RUN pip install --no-cache-dir requests fastapi "uvicorn[standard]"
COPY main.py ${WORKSPACE}
ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9999"]
# test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: test1
namespace: test
spec:
replicas: 1
selector:
matchLabels:
app: test1
template:
metadata:
labels:
app: test1
spec:
containers:
- name: web-app
image: littlemobs/telepresence:test
imagePullPolicy: Always
ports:
- containerPort: 9999
---
apiVersion: v1
kind: Service
metadata:
name: test1
namespace: test
spec:
type: ClusterIP
selector:
app: test1
ports:
- protocol: TCP
port: 9999
targetPort: 9999
편의상 "test" namespace에 배포하도록 하였다. 네임스페이스가 없다면 미리 만들어두어야 한다. 다음과 같이 docker build, push, 그리고 kubernetes 배포까지 진행하면 deployment와 service가 각각 한 개씩 떠있는 것을 확인할 수 있다.
$ docker build -t littlemobs/telepresence:test .
$ dokcer push littlemobs/telepresence:test
$ kubectl create namespace test
$ kubectl apply -f test.yaml
$ kubectl get deployments -n test
NAME READY UP-TO-DATE AVAILABLE AGE
test1 1/1 1 1 22m
$ kubectl get services -n test
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test1 ClusterIP 10.20.30.40 <none> 9999/TCP 22m
Traffic 훔치기 테스트
앞서 배포한 앱에 포트 포워딩을 한 뒤에 API 호출을 해보면, 로컬에서 테스트했을 때와 동일한 결과를 얻게 된다. 아직 http://test2:9999
주소로 접근할 수 있는 web app이 kubernetes cluster에 배포되어 있지 않기 때문이다.
Telepresence를 통해 코드 변경없이 kubernetes에 배포되어 있는 "test1" 앱에서 http://test2:9999
대신 로컬 개발 환경에 요청을 보내도록해보자. 우선 "test1" 앱이 겨냥할 수 있도록 kubernetes에 허수아비를 하나 세워둬야 한다. "test2"라는 이름의 서비스를 만들기 위해 다음과 같은 명령어를 사용하자. 이때 사용되는 image나 실행 명령어는 아무거나 상관없다. 매우 가벼운 busybox를 추천한다.
$ kubectl create deployment test2 -n test --image=busybox --port=9999 -- sleep 3600
$ kubectl expose deployment test2 -n test --name=test2 --port=9999 --target-port=9999
$ kubectl get deployments -n test
NAME READY UP-TO-DATE AVAILABLE AGE
test1 1/1 1 1 61s
test2 1/1 1 1 9s
$ kubectl get services -n test
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test1 ClusterIP 10.20.30.40 <none> 9999/TCP 47s
test2 ClusterIP 10.20.30.41 <none> 9999/TCP 15s
이제 kubernetes에 배포된 "test1" 앱에 포트 포워딩을 해두고, "test2"로 들어오는 요청을 대신 받을 web app을 로컬에 다시 띄워보자. 편의상 포트 포워딩은 10000번 포트로 설정해서 로컬에 배포된 web app과 구분되도록 하였다.
$ kubectl port-forward svc/test1 10000:9999 -n test
$ uvicorn main:app --host 0.0.0.0 --port 9999 --reload
두 명령어 모두 정상적으로 실행된 상태에서 다음과 같이 telepresence 명령어를 통해 "test" namespace에 연결한 뒤 "test2" app으로 들어오는 요청이 http://localhost:9999
로 라우팅되도록 intercept해주자.
$ telepresence connect -n test
Connected to context littlemobs, namespace test (https://0.0.0.0:59842)
$ telepresence intercept test2 --port 9999
Using Deployment test2
Intercept name : test2
State : ACTIVE
Workload kind : Deployment
Intercepting : 10.42.0.222 -> 127.0.0.1
9999 -> 9999 TCP
Volume Mount Error: remote volume mounts are disabled: sshfs is not installed on your local machine
연결이 잘 되었다면, kubernetes에 허수아비로 배포해두었던 "test2" pod 안에 "tel-agent-init"과 "traffic-agent"라는 이름의 container가 추가된 것을 확인할 수 있을 것이다. Telepresence가 "test2" pod 안에 스파이를 심어둔 것이다.
이 상태에서 브라우저 주소창에 http://localhost:10000/attack
입력하면 다음과 같이 정상적으로 "Hello, World"라는 문자열을 확인할 수 있다. 그리고, local에 배포해둔 web app의 로그를 보면 GET /
요청이 전달되었음을 확인할 수 있다.
{"status_code":200,"message":"\"Hello, World!\""}
INFO: 127.0.0.1:56756 - "GET / HTTP/1.1" 200 OK
그렇다면 반대로 local에 배포한 web app에서 http://test1:9999
주소로 요청을 보내면 정상적으로 동작할까? FastAPI의 attack
메서드에서 요청 URL을 http://test1:9999
로 바꾼 뒤 저장하고 브라우저에서 주소창에 http://localhost:9999/attack
을 입력해보자. 마찬가지로 통신이 잘 이루어지는 것을 확인할 수 있다.
{"status_code":200,"message":"\"Hello, World!\""}
INFO: 10.20.0.222:33624 - "GET / HTTP/1.1" 200 OK
맺음말
Telepresence를 통해 로컬에서 변경되는 코드를 실시간으로 반영하여 kubernetes 환경에서 동작하는 것처럼 만들 수 있게 되었다. 앞서 "test1" 앱을 배포하기 위해 수행했던 docker build, push, kubernetes 배포 과정을 생략하고 코드 저장 후 바로 테스트가 가능하게 된 것이다.
Telepresence가 간단한 코드 수정과 빠른 테스트를 하기에 편리한 것은 분명하지만 너무 의존해서는 안된다. Kubernetes 배포 환경과 로컬 개발 환경은 엄연히 다르기 때문에 실제 배포 시 개발 과정에서는 예상하지 못했던 문제들이 발생할 수 있다. Telepresence는 어디까지나 개발 효율성 향상용 도구라고 생각하자. 실제 중요한 테스트는 빌드와 배포 과정을 거치도록 해야 한다.
More Posts
Python으로 kubernetes 노드 선택 기능을 개발하는 방법
클러스터를 구성하면 목적에 따라 노드의 역할을 구분하는 경우가 많다. 따라서, kubernetes에서 pod를 특정 노드에만 배포해야 한다는 제약사항은 당연히 나올 수 밖에 없다. Python을 통해 이를 구현하는 방법과 고려해야 할 점들에 대해 알아보자.
Argo workflow에 kubernetes resource request와 limit을 설정하는 방법
Kubernetes 환경에서 argo workflow를 통해 파이프라인을 실행할 때는 자원 할당에 대한 고민이 필요하다. 그런데, 일반적인 방식으로는 의도대로 자원 할당이 되지 않는다. Kubernetes 환경의 안정적인 운영을 위해 argo workflow에 자원 설정을 하는 방법을 알아보자.
Python FastAPI로 파일 업로드 및 다운로드 가능한 web server 개발하기
FastAPI를 활용하면 파일을 업로드하거나 다운로드할 수 있는 web server를 매우 간단하게 구현할 수 있다. 예제 코드와 함께 최대한 간단하게 파일 업로드 및 다운로드 API를 구현하고 테스트하는 방법을 알아보자.
Comments