복사되었습니다.

Telepresence로 kubernetes 도메인 localhost에서 훔쳐쓰기

Cover Image for Telepresence로 kubernetes 도메인 localhost에서 훔쳐쓰기

Kubernetes에 배포된 pod들끼리 통신을 할 때는 클러스터 도메인 주소를 사용한다. Web app 개발 시 로컬에서는 localhost 주소를 주로 사용하기 때문에 다른 pod들과의 통합 테스트를 하기가 어렵다. Telepresence로 로컬에서 kubernetes의 traffic을 훔치는 방법을 알아보자.

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 정보가 없기 때문에 다른 서비스와 통신을 하는 부분에서 오류가 발생하게 된다.

kubernetes cluster network isolation

로컬 환경의 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으로 라우팅해준다.

connecting local to kubernetes with telepresence

이 방식을 활용하면 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에 배포해두기

deploying test1 to 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에 배포되어 있지 않기 때문이다.

intercepting requests with telepresence

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

using cluster domain with telepresence

그렇다면 반대로 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는 어디까지나 개발 효율성 향상용 도구라고 생각하자. 실제 중요한 테스트는 빌드와 배포 과정을 거치도록 해야 한다.

Comments

    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를 구현하고 테스트하는 방법을 알아보자.

    Font Size