Profile picture

[k8s] hostname 출력하는 FastAPI 프로젝트 파드로 띄워보기

JaehyoJJAng2023년 04월 20일

▶︎ 개요

현재 파드의 이름을 출력해주는 매우 간단한 fastAPI 프로젝트를 만들고 쿠버네티스로 배포한 후 결과를 확인해보자.


▶︎ FastAPI

‣ 프로젝트 구조

$ tree -L 2 ./
./
├── Dockerfile
├── requirements
│   └── requirements.txt
└── server.py

‣ 소스코드

server.py

from fastapi import FastAPI
import socket


app : FastAPI = FastAPI()

@app.get('/hostname')
def get_hostname() -> dict[str,str]:
    hostname = socket.gethostname()
    return {'hostname': hostname}

requirements.txt

uvicorn
fastapi

‣ 이미지 빌드/푸시

위 프로젝트를 Dockerfile을 작성해 이미지화하여 도커 허브에 배포하자.

FROM python:3.10

WORKDIR /usr/src/app

COPY ./requirements/*.txt ./requirements.txt
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY ./ ./

CMD ["uvicorn", "server:app","--host","0.0.0.0","--port","8080"]
# 이미지 빌드
$ docker build --tag yshrim12/app .

# 도커 허브 로그인
$ echo "${DOCKERHUB_PASS}" | docker login -u "${DOCKERHUB_USER}" --password-stdin

# 도커 허브로 이미지 배포
$ docker push yshrim12/app

‣ 컨테이너 실행

이미지가 정상적으로 빌드되었는지 확인하기 위해 컨테이너로 실행해보자

$ docker run -d -it --name app-test -p 8080:8080 yshrim12/app
$ curl -X GET localhost:8080/hostname
{"hostname":"21791b9bad3c"}

▶︎ 쿠버네티스 배포

위에서 배포한 도커 이미지(yshrim12/app)을 기반으로 쿠버네티스 파드를 배포해보도록 하자!


디플로이먼트와 Service의 manifest는 아래와 같다.

# fastapi-deploy.yaml
apiVersion: v1
kind: Service
metadata:
  name: fastapi-svc
spec:
  selector:
    app: fastapi
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-deply
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fastapi
  template:
    metadata:
      labels:
        app: fastapi
    spec:
      containers:
        - name: fastapi-container
          image: yshrim12/app
          ports:
            - containerPort: 8080
              protocol: TCP

이 배포 파일은 FastAPI 애플리케이션을 쿠버네티스 클러스터에 배포하고 로드 밸런서 서비스를 노출시킨다.


작성한 manifest를 클러스터에 배포하자

$ kubectl apply -f ./fastapi-deploy.yaml

fastapi-svc 서비스의 클러스터 IP 주소 확인

$ kubectl get svc -n default -o wide | grep 'fastapi'
NAME          TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
fastapi-svc   LoadBalancer   10.106.207.6   <pending>     80:32416/TCP   36s   app=fastapi

EXTERNAL-IP가 <pending> 상태인데 현재 클러스터에 연결된 LoadBalance 플러그인이 없어서 그런 것이니 넘어가자.


Pod가 정상적으로 Running 상태로 넘어갔는지 확인해보자

$ kubectl get pods -o wide | tail -n 1
fastapi-deply-59c9f9d955-t7jqc   0/1     CrashLoopBackOff   5 (<invalid> ago)   3m51s   20.111.156.81   k8s-node1   <none>           <none>

응? Pod의 Status(CrashLoopBackOff)가 이상하다.


CrashLoopBackOff 상태인데 파드의 상세정보를 확인해보자.

$ kubectl describe pod/fastapi-deply-59c9f9d955-t7jqc

image
Events 항목의 Message를 보면 도커 이미지는 제대로 받아오는 것 같은데 왜 실행이 안되는지 역시 잘 모르겠다!


‣ 트러블슈팅

왜 안되는지 조금 더 자세하게 파고 들어가보자.


• 파드 로그 확인

정확한 진단을 위해 파드 컨테이너의 로그를 확인해보자

# kubectl logs pod/<pod-name> -c <container-name>
$ kubectl logs pod/fastapi-deply-59c9f9d955-t7jqc
exec /usr/local/bin/uvicorn: exec format error

"exec /usr/local/bin/uvicorn: exec format error" 이라는 에러가 발생하였다.


• exec foramt error

해당 에러는 일반적으로 호스트 운영 체제와 컨테이너 운영 체제가 서로 호환되지 않을 때 발생하는 오류이다!

이 오류는 보통 호스트 운영체제와 컨테이너 이미지가 서로 다른 아키텍처를 사용할 때 발생할 수 있다고 한다.

먼저 이미지 빌드/푸시편에서 도커 허브에 이미지를 배포했었지 않는가?

근데 나의 경우 M1 맥북 환경에서 해당 이미지를 빌드하고 배포했었다.

M1의 경우 자체적으로 ARM 아키텍처를 사용하여 이미지를 빌드하므로 현재 amd 아키텍처를 사용 중인 컨테이너와 이미지의 아키텍처가 맞지 않아 해당 에러(exec format error)가 발생한 것으로 추측이 된다.


• 해결 방법

그렇다면 해결 방법은 무엇일까? 도커 이미지를 빌드할 때 Multi-Platform 지원을 활성화하면 된다.

도커 이미지 빌드 시 Multi-Platform 지원을 활성화하여 여러 아키텍처를 지원하는 이미지를 빌드할 수 있다.
다음과 같이 --platform 옵션을 사용하여 여러 아키텍처를 지원하는 이미지를 빌드할 수 있다.

$ docker buildx build --platform linux/amd64 --push --tag yshrim12/app:latest .

자세한 내용은 [Docker] exec format error - Buildx (Docker Multi-Archtecture image)에서 확인 가능하다.


그럼 이제 도커 이미지에 Multi-Platform 지원 활성화 작업을 마쳤다고 가정하고 파드를 삭제하고 다시 만들어보자.

$ kubectl delete -f fastapi-deploy.yaml
$ kubectl apply -f fastapi-deploy.yaml

파드 상태를 확인해보자

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS        AGE
fastapi-deply-59c9f9d955-fd2mx   1/1     Running   9 (5m22s ago)   22m
fastapi-deply-59c9f9d955-hxlhn   1/1     Running   9 (4m54s ago)   22m
fastapi-deply-59c9f9d955-t7jqc   1/1     Running   9 (4m15s ago)   22m

파드가 정상적으로 동작하고 있는 것을 확인할 수 있었다!


‣ 테스트 결과

이제 curl 커맨드를 사용하여 Service의 클러스터 IP 주소의 /hostname path로 GET 요청을 보내 hostanme이 정상적으로 반환되는지 확인해보자!

# Service 확인
$ kubectl get svc | grep 'fastapi'
fastapi-svc   LoadBalancer   10.106.207.6   <pending>     80:32416/TCP   27m

# Node INTERNAL-IP 확인
$ kubectl get nodes -o wide
NAME         STATUS   ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                           KERNEL-VERSION                 CONTAINER-RUNTIME
k8s-master   Ready    control-plane   11h   v1.27.1   192.168.56.30    <none>        Rocky Linux 8.8 (Green Obsidian)   4.18.0-477.10.1.el8_8.x86_64   containerd://1.6.21
k8s-node1    Ready    <none>          11h   v1.27.1   192.168.56.101   <none>        Rocky Linux 8.8 (Green Obsidian)   4.18.0-477.10.1.el8_8.x86_64   containerd://1.6.21
k8s-node2    Ready    <none>          9h    v1.27.1   192.168.56.102   <none>        Rocky Linux 8.8 (Green Obsidian)   4.18.0-477.10.1.el8_8.x86_64   containerd://1.6.21

# GET 요청 보내기 (Service 주소)
$ curl -X GET 10.106.207.6/hostname
{"hostname":"fastapi-deply-59c9f9d955-hxlhn"}

# GET 요청 보내기 (Node 주소)
$ curl -X GET 192.168.56.101:32416/hostname
{"hostname":"fastapi-deply-59c9f9d955-hxlhn"}
$ curl -X GET 192.168.56.102:32416/hostname
{"hostname":"fastapi-deply-59c9f9d955-hxlhn"}

Loading script...