Profile picture

[k8s] 간단한 FastAPI 프로젝트에 NFS Volume 연결하기

JaehyoJJAng2023년 05월 16일

▶︎ 개요

FastAPI 파이썬 웹 프레임워크를 사용하여 간단한 프로젝트를 진행해보자.
/, /db 두 개의 라우팅을 만들거고 /db 라우팅은 접근 시 바로 db에 접근하도록 구현할 것이다.

그리고 파드가 비정상적으로 종료되어 데이터가 유실되는 것을 방지하기 위해 NFS Volume을 사용하여 마스터 노드 서버의 /mnt/shared/fastapi-db 경로를 db 컨테이너의 /var/lib/mysql 경로와 마운트 시켜 데이터 유실을 방지할 것이다.

또 프로젝트 소스코드 변경이 컨테이너에서도 실시간으로 이루어지게 하기 위해 /mnt/shared/fastapi-app 경로를 fastapi 컨테이너의 /apps 경로와 마운트해보자.


‣ 프로젝트 구조

$ tree .
.
|-- mysql-deployment.yaml
|-- mysql-volume.yaml
|-- was-deployment.yaml
|-- was-secret.yaml
`-- was-volume.yaml

‣ 이미지 빌드

main.py

from fastapi import FastAPI
from dataclasses import dataclass
import pymysql
import os

def get_env() -> tuple[str | int]:
    return (
        os.getenv('MYSQL_HOST'),
        os.getenv('MYSQL_PORT'),
        os.getenv('MYSQL_USER'),
        os.getenv('MYSQL_PASSWORD'),
        os.getenv('MYSQL_DATABASE'),
    )

@dataclass
class Data():
    host: str
    port: int
    user: str
    password: str
    database: str

data : Data = Data(*get_env())

def connect_db() -> pymysql.Connection:
    con = pymysql.connect(
        host=data.host,
        port=int(data.port),
        user=data.user,
        password=data.password,
        database=data.database,
        charset='utf8'
    )
    return con

app = FastAPI()

@app.get('/')
def say_hello() -> dict[str,str]:
    return {"Message": "안녕하세요"}

@app.get('/db')
def connect() -> None | dict[str,str]:
    try:
        con : pymysql.Connection = connect_db()
        cursor = con.cursor()

        query = "SELECT * FROM testTBL;"
        cursor.execute(query)
        result = cursor.fetchone()
        print(result)
        return {"Message": "DB connection complete!}
    except Exception as e:
        print(e)
        return {"Message": "DB connection failed!"}

Dockerfile

FROM python:3.10-slim-buster

WORKDIR /usr/src/app

COPY ./requirements/*.txt ./requirements.txt

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

COPY ./ ./

EXPOSE 8080

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

먼저 로컬에서 작업한 프로젝트를 도커 이미지화 시켜 docker hub에 업로드 시킬거다.

$ docker buildx build --platform linux/amd64 --push --tag yshrim12/was .

▶︎ 사전 준비

  • K8s 클러스터
  • nfs-server

K8s 클러스터 환경이 구축되어 있어야 하고 마스터 노드에 nfs-utils 패키지가 설치되어 있어야 한다.


MountPoint 생성

$ mkdir /mnt/shared/fastapi-app
$ mkdir /mnt/shared/fastapi-db

/etc/exportfs

# 아래 내용 추가
/mnt/shared 192.168.56.30/24(rw,sync,no_root_squash,no_subtree_check)

exportfs 적용

$ exportfs -a

nfs-server 재시작

$ systemctl restart nfs-server

▶︎ Manifest 작성

이미지 배포가 완료되었다면 오브젝트 스펙을 작성해보자.


‣ MySQL

• PV

mysql-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: db-storage
  labels:
    type: db-nfs
spec:
  capacity:
    storage: 10Gi
  accessModes: ["ReadWriteMany"]
  nfs:
    server: "192.168.56.30"
    path: "/mnt/shared/fastapi-db"
  • spec.capacity: 이 PV의 용량은 10 기가바이트(GiB)입니다.
  • spec.accessModes: ["ReadWriteMany"]: 이 PV는 여러 개의 Pod에서 동시에 읽고 쓸 수 있는 'ReadWriteMany' 액세스 모드를 지원합니다.
  • spec.nfs: PV가 NFS(Network File System) 프로토콜을 사용하는 것을 나타냅니다.
  • spec.nfs.server: "192.168.56.30": NFS 서버의 IP 주소는 "192.168.56.30"입니다.
  • spec.nfs.path: "/mnt/shared/wtt-db-volume": NFS 서버의 공유 디렉토리 경로는 "/mnt/shared/fastapi-db"입니다.

• PVC

mysql-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 4Gi
  storageClassName: ""
  selector:
    matchLabels:
      type: db-nfs
  • spec.accessModes: ["ReadWriteMany"]: 이 PVC는 여러 개의 Pod에서 동시에 읽고 쓸 수 있는 'ReadWriteMany' 액세스 모드를 요청합니다.
  • spec.resources: PVC에서 요청하는 리소스를 정의합니다.
  • spec.resources.requests: 이 PVC가 요청하는 스토리지 용량은 4 기가바이트(GiB)입니다.
  • spec.storageClassName: "": 이 PVC는 특정 storage class를 지정하지 않습니다.
  • spec.selector: PVC가 특정 PersistentVolume과 매칭되어야 할 때 선택적으로 사용됩니다.
  • spec.selector.matchLabels: 이 PVC는 type: db-nfs 라벨을 가진 PersistentVolume과 매칭되어야 합니다. 즉, type: db-nfs 라벨을 가진 PersistentVolume과 연결될 것을 요청합니다.

• Deployment

mysql-deployment.yaml

#================
# MySQL Service
#================
apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  ports:
    - name: db-port
      port: 3306
      targetPort: 3306
  selector:
    app: mysql
  type: ClusterIP
---
#================
# MySQL Deployment
#================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-pod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: docker.io/mysql:latest
          ports:
            - containerPort: 3306
              protocol: TCP
          volumeMounts:
            - name: db-volume
              mountPath: /var/lib/mysql
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: secret
            - name: MYSQL_DATABASE
              value: testDB
      volumes:
        - name: db-volume
          persistentVolumeClaim:
            claimName: db-claim
  • spec.template.spec.containers.volumeMounts: 컨테이너에 마운트할 볼륨을 정의합니다. 여기서는 이름이 "db-volume"인 - PersistentVolumeClaim을 "/var/lib/mysql" 경로에 마운트합니다.
  • spec.template.spec.containers.env: 컨테이너에서 사용할 환경 변수를 정의합니다. 여기서는 MySQL 루트 비밀번호와 데이터베이스 이름을 설정합니다.
  • spec.template.spec.volumes: Pod에 연결할 볼륨을 정의합니다.
  • spec.template.spec.volumes.name: db-volume: 이 볼륨의 이름은 "db-volume"입니다.
  • spec.template.spec.volumes.persistentVolumeClaim: 이 볼륨은 PVC인 "db-claim"과 연결됩니다.

‣ WAS

• PV

was-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: was-storage
  labels:
    type: was-nfs
spec:
  capacity:
    storage: 10Gi
  accessModes: ["ReadWriteMany"]
  nfs:
    server: "192.168.56.30"
    path: "/mnt/shared/fastapi-app"
  • spec.capacity: 이 PV의 용량은 10 기가바이트(GiB)입니다.
  • spec.accessModes: ["ReadWriteMany"]: 이 PV는 여러 개의 Pod에서 동시에 읽고 쓸 수 있는 'ReadWriteMany' 액세스 모드를 지원합니다.
  • spec.nfs: PV가 NFS(Network File System) 프로토콜을 사용하는 것을 나타냅니다.
  • spec.nfs.server: "192.168.56.30": NFS 서버의 IP 주소는 "192.168.56.30"입니다.
  • spec.nfs.path: "/mnt/shared/wtt-app": NFS 서버의 공유 디렉토리 경로는 "/mnt/shared/fastapi-app"입니다.

• PVC

was-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: was-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 4Gi
  storageClassName: ""
  selector:
    matchLabels:
      type: was-nfs
  • spec.accessModes: ["ReadWriteMany"]: 이 PVC는 여러 개의 Pod에서 동시에 읽고 쓸 수 있는 'ReadWriteMany' 액세스 모드를 요청합니다.
  • spec.resources: PVC에서 요청하는 리소스를 정의합니다.
  • spec.resources.requests: 이 PVC가 요청하는 스토리지 용량은 4 기가바이트(GiB)입니다.
  • spec.storageClassName: "": 이 PVC는 특정 storage class를 지정하지 않습니다.
  • spec.selector: PVC가 특정 PersistentVolume과 매칭되어야 할 때 선택적으로 사용됩니다.
  • spec.selector.matchLabels: 이 PVC는 type: was-nfs 라벨을 가진 PersistentVolume과 매칭되어야 합니다. 즉, type: was-nfs 라벨을 가진 PersistentVolume과 연결될 것을 요청합니다.

• WAS Deployment

was-deployment.yaml

#================
# WAS Service
#================
apiVersion: v1
kind: Service
metadata:
  name: was-service
spec:
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 30002
  selector:
    app: fastapi
  type: NodePort
---
#================
# WAS Deployment
#================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: was-fastapi
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fastapi
  template:
    metadata:
      labels:
        app: fastapi
    spec:
      containers:
        - name: fastapi
          image: docker.io/yshrim12/was:latest
          resources:
            limits:
              cpu: "0.5"
              memory: 1Gi
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: was-volume
              mountPath: /apps
          env:
            - name: MYSQL_HOST
              valueFrom:
                secretKeyRef:
                  name: was-secret
                  key: MYSQL_HOST
            - name: MYSQL_PORT
              valueFrom:
                secretKeyRef:
                  name: was-secret
                  key: MYSQL_PORT
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: was-secret
                  key: MYSQL_USER
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: was-secret
                  key: MYSQL_PASSWORD
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: was-secret
                  key: MYSQL_DATABASE
      volumes:
        - name: was-volume
          persistentVolumeClaim:
            claimName: was-claim
  • spec.containers.resources: 컨테이너가 사용할 수 있는 CPU 및 메모리 리소스의 제한을 정의합니다.
  • spec.containers.ports: 컨테이너가 노출할 포트를 정의합니다. 여기서는 8080번 포트를 사용합니다.
  • spec.containers.volumeMounts: 컨테이너에 마운트할 볼륨을 정의합니다. 여기서는 이름이 "was-volume"인 PersistentVolumeClaim을 "/apps" 경로에 마운트합니다.
  • spec.containers.env: 컨테이너에서 사용할 환경 변수를 정의합니다. 여기서는 MySQL 호스트, 포트, 사용자 이름, 암호, 데이터베이스 이름을 시크릿에서 가져옵니다.
  • spec.volumes: Pod에 연결할 볼륨을 정의합니다.
  • spec.volumes.name: was-volume: 이 볼륨의 이름은 "was-volume"입니다.
  • spec.volumes.persistentVolumeClaim: 이 볼륨은 PVC인 "was-claim"과 연결됩니다.

‣ 배포하기

$ kubectl apply -f .
service/mysql-service created
deployment.apps/mysql-pod created
persistentvolume/db-storage created
persistentvolumeclaim/db-claim created
service/was-service created
deployment.apps/was-fastapi created
secret/was-secret created
persistentvolume/was-storage created
persistentvolumeclaim/was-claim created

Loading script...