playdata/daily

12주차 : Day 1 (9/23)

soojin1 2024. 9. 24. 09:30

지지난주, 로컬에 csv 파일로 저장하는 부분까지 끝마쳤다. 그 다음 단계이다.

 

csv 영구 저장

docker volume

내가 만들고 있는 FastAPI  프로그램은 Docker image기반으로 제공되는데, 그 특성상 프로그램이 종료되면 내부에 저장했던 데이터가 휘발된다. 그 문제를 해결하기 위해 -v 옵션을 사용하는 방법이 있다.

로컬 경로와 컨테이너 내부 경로가 연동되어 한 쪽에서 데이터가 생성되거나 삭제되는 경우 반대쪽에서도 같은 이벤트가 발생한다. 도커 이미지가 종료되어 내부 데이터가 휘발되어도, 로컬의 데이터는 삭제되지 않기 때문에 도커 내부 데이터도 휘발되지 않는 구조로 동작할 수 있다.

# AWS 서버에서 수행
# 연결 경로 생성
$ mkdir /home/ubuntu/data/n23

# 도커 볼륨이 적용된 컨테이너로 다시 RUN
$ sudo docker run -d -p 8023:8080 \
--name food23
-v /home/ubuntu/data/n23:/code/data \
sooj1n/food:0.2.11


DB 데이터 저장

MariaDB를 사용하기 위해 docker hub에 올라와있는 것 을 pull 받아와 실행했다.

(서버가 닫혀서 그런건지(?) 데이터베이스까지는 확인이 되는데.. 테이블이 없어서 수강생 분들 블로그 사진 좀 사용했슴니다...감사)

$ docker exec -it localdb bash
root@13d1fea32321:/# mariadb -u food -p1234
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 11.5.2-MariaDB-ubu2404 mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| fooddb             |
| information_schema |
+--------------------+
2 rows in set (0.001 sec)

MariaDB [(none)]> use fooddb;
MariaDB [(none)]> show databases;

 

 

 

main.py에 아래의 내용을 추가한다.

# Connect to the database
    connection = pymysql.connect(host="localhost",
                             user='food',
                             password='<비밀번호 입력>',
                             database='fooddb',
                             port ='33306',
                             cursorclass=pymysql.cursors.DictCursor)

    sql = "INSERT INTO foodhistory(`username`, `foodname`, `dt`) VALUES(%s,%s,%s)"

    with connection:
        with connection.cursor() as cursor:
            cursor.execute(sql, ('n01', name, datetime.now()))

        connection.commit()

 

여기까지 수행하면 maria DB에는 잘 저장이 된다.

하지만 AWS 서버에서 위의 내용을 도커로 저장하여 실행했을 때 제대로 작동되지 않을 것 이다.

localhost 라던지.. 로컬 대상으로 db 설정을 했기 때문이다.

그래서 다른 환경에 맞게 수정을 해줘야하는데 일일히 번거롭게 수정하지 않는 방법은 환경변수 getenv를 사용하는 것 이다.

 

아래와 같이 os.getenv를 사용해주면 된다.

DB_IP, DB_PORT가 있다면 그걸로 실행시키고, 없으면 localhost, port 33306으로 실행시킨다는 의미이다.

DB_IP, DB_PORT는 DOCKER RUN 수행 시 지정할 값 이다.

import os

# Connect to the database
    connection = pymysql.connect(host=os.getenv('DB_IP', 'localhost'),
                             user='food',
                             password='<비밀번호 입력>',
                             database='fooddb',
                             port = int(os.getenv('DB_PORT', '33306')),
                             cursorclass=pymysql.cursors.DictCursor)

    sql = "INSERT INTO foodhistory(`username`, `foodname`, `dt`) VALUES(%s,%s,%s)"

    with connection:
        with connection.cursor() as cursor:
            cursor.execute(sql, ('n01', name, datetime.now()))

        connection.commit()

 

AWS 서버에서 도커를 실행시켜보자.

-e 옵션으로 DB_IP, DB_PORT 를 설정할 수 있다.

$ docker run -d -p 8023:8080 --name food23 -v /home/ubuntu/data/n23:/code/data -e FILE_PATH=/code/data/food.csv -e DB_PORT=13306 -e DB_IP=172.17.0.1 sooj1n/food01:0.3.0

 

여기까지 한가위 숙제 끝@

 


□ 데이터베이스에 파일 업로드 하기

오늘은 강사님 깃헙을 클론해서 사용했지만 지금은 복습 겸 다른 수강생분 블로그를 보고 다시 해보겠다 !!

 

$ cat src/mnist/main.py

from typing import Annotated
import os
from fastapi import FastAPI, File, UploadFile
from datetime import datetime
import pymysql.cursors

app = FastAPI()

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    #파일 저장
    img = await file.read()
    file_name = file.filename
    upload_dir = "/Users/sujinya/code/mnist/img"
    file_full_path = os.path.join(upload_dir, file_name)

    with open(file_full_path, 'wb') as f:
        f.write(img)

    return {
            "filename": file.filename,
            "content_type": file.content_type,
            "file_full_path": file_full_path,
            "time": datetime.now()
            }
$ uvicorn src.mnist.main:app --reload

 

지정한 경로에 파일이 잘 저장되었다.

 

□ Maria DB 준비

$ sudo docker run -d \
        --name mnist-mariadb \
        -e MARIADB_USER=mnist \
        --env MARIADB_PASSWORD=1234 \
        --env MARIADB_DATABASE=mnistdb \
        --env MARIADB_ROOT_PASSWORD=my-secret-pw \
        -p 53306:3306 \
        mariadb:latest
  • -e MARIADB_USER=mnist: MariaDB에서 사용할 기본 사용자 이름을 mnist로 설정합니다.
  • --env MARIADB_PASSWORD=1234: 위에서 설정한 mnist 사용자의 비밀번호를 1234로 설정합니다.
  • --env MARIADB_DATABASE=mnistdb: MariaDB 내에서 생성할 초기 데이터베이스의 이름을 mnistdb로 설정합니다.
  • --env MARIADB_ROOT_PASSWORD=my-secret-pw: MariaDB의 루트(root) 사용자 비밀번호를 my-secret-pw로 설정합니다.
  • -p 53306:3306: 호스트의 53306 포트와 컨테이너의 3306 포트를 연결합니다. 이는 MariaDB가 사용하는 기본 포트인 3306을 컨테이너 안에서 호스트의 53306 포트로 포워딩하여 외부에서 접근할 수 있게 합니다.
  • mariadb:latest: 사용할 MariaDB 이미지의 버전을 latest로 지정합니다. 이는 Docker Hub에서 가장 최신 버전의 MariaDB 이미지를 가져오라는 의미입니다.

maria DB 컨테이너를 실행하여 image_processing 테이블을 만든다.

$ docker exec -it mnist-mariadb bash
root@af04cf7da56d:/# mariadb -u mnist -p1234
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 46
Server version: 11.5.2-MariaDB-ubu2404 mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mnistdb            |
+--------------------+
2 rows in set (0.003 sec)

MariaDB [(none)]> use mnistdb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [(none)]> CREATE TABLE image_processing (
    -> num INT AUTO_INCREMENT PRIMARY KEY COMMENT '자동 증가 숫자',
    -> file_name VARCHAR(100) NOT NULL COMMENT '원본 파일명',
    -> file_path VARCHAR(255) NOT NULL COMMENT '저장 전체 경로 및 변환 파일명',
    -> request_time VARCHAR(50) NOT NULL COMMENT '요청시간',
    -> request_user VARCHAR(50) NOT NULL COMMENT '요청 사용자',
    -> prediction_model VARCHAR(100) COMMENT '예측 사용 모델',
    -> prediction_result VARCHAR(50) COMMENT '예측 결과',
    -> prediction_time VARCHAR(50) COMMENT '예측 시간'
    -> )
    -> ;
    
 MariaDB [mnistdb]> SHOW TABLES;
+-------------------+
| Tables_in_mnistdb |
+-------------------+
| image_processing  |
+-------------------+
1 row in set (0.001 sec)

MariaDB [mnistdb]> desc image_processing;
+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| num               | int(11)      | NO   | PRI | NULL    | auto_increment |
| file_name         | varchar(100) | NO   |     | NULL    |                |
| file_path         | varchar(255) | NO   |     | NULL    |                |
| request_time      | varchar(50)  | NO   |     | NULL    |                |
| request_user      | varchar(50)  | NO   |     | NULL    |                |
| prediction_model  | varchar(100) | YES  |     | NULL    |                |
| prediction_result | varchar(50)  | YES  |     | NULL    |                |
| prediction_time   | varchar(50)  | YES  |     | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+
8 rows in set (0.022 sec)

 

□ API - DB 연결

main.py를 아래와 같이 수정한다.

from typing import Annotated
import os
from fastapi import FastAPI, File, UploadFile
from datetime import datetime
import pymysql.cursors

app = FastAPI()

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    img = await file.read()
    file_name = file.filename
    file_ext = file.content_type.split('/')[-1]

    upload_dir = "/Users/sujinya/code/mnist/img"
    if not os.path.exists(upload_dir):
        os.makedirs(upload_dir)

    import uuid
    file_full_path = os.path.join(upload_dir, f'{uuid.uuid4()}.{file_ext}')

    with open(file_full_path, 'wb') as f:
        f.write(img)


    connection = pymysql.connect(host="127.0.0.1",
                                 user='mnist',
                                 password='<비밀번호 입력>',
                                 database='mnistdb',
                                 port=int(53306),
                                 cursorclass=pymysql.cursors.DictCursor)
    sql = "INSERT INTO image_processing(`file_name`, `file_path`, `request_time`, `request_user`) VALUES(%s,%s,%s,%s)"

    import jigeum.seoul #시간모듈
    with connection:
        with connection.cursor() as cursor:
            cursor.execute(sql, (file.filename, file_full_path, jigeum.seoul.now(), 'n01'))

        connection.commit()

    return {
            "filename": file.filename,
            "content_type": file.content_type,
            "file_full_path": file_full_path,
            "time": jigeum.seoul.now()
            }
  • uuid.uuid4()를 사용하여 파일명에 고유 식별자를 추가함으로써 파일명 충돌을 방지합니다.
  • pymysql 라이브러리를 사용하여 MariaDB 데이터베이스에 연결합니다.
$ pdm add pymysql
$ pdm install
  • with connection 블록 안에서 데이터베이스 작업을 수행하고, 블록을 벗어날 때 자동으로 연결을 종료한다.
  • cursor : 데이터베이스와 상호작용하기 위해 커서를 생성한다. 이 커서는 SQL 쿼리를 실행하는 데 사용된다.
  • cursor.execute(): sql 쿼리를 실행하고, 두 번째 인자로 전달된 튜플의 값들을 쿼리에 삽입한다.

□ 중복 데이터 처리

같은 이름의 파일을 여러 번 업로드하면 한 개의 파일만 DB에 업로드된다.

같은 파일이라도 file_path를 고유값으로 주어 이 문제를 해결한다.

위의 코드 설명에서 uuid에 해당되는 내용이다.

 

import uuid
file_full_path = os.path.join(upload_dir, f'{uuid.uuid4()}.{file_ext}')

 

동일한 파일을 여러 번 업로드 한 결과이다.

파일이름이 고유한 형식으로 저장되는 것을 확인할 수 있다.

 

 

** 가독성을 위한 아키텍처 설계 -> 시간 나면 해보기 ㅠㅠ

db.py 만들어서 하는것임

import pymysql.cursors
import os

def get_conn():
  db_host = os.getenv("DB_IP", "localhost")
  db_port = os.getenv("DB_PORT", "53306")
  conn = pymysql.connect(   host=db_host, 
                            port=int(db_port),
                            user='mnist', password = '1234',
                            database='mnistdb',
                            cursorclass=pymysql.cursors.DictCursor)
  return conn


def select(query: str, size = -1):
  conn = get_conn()
  with conn:
      with conn.cursor() as cursor:
          cursor.execute(query)
          result = cursor.fetchmany(size)

  return result


def dml(sql, *values):
  conn = get_conn()

  with conn:
    with conn.cursor() as cursor:
        cursor.execute(sql, values)
        conn.commit()
        return cursor.rowcount

 

 

□ worker.py

우리는 숫자 이미지를 업로드하여 MNIST 딥러닝 모델을 사용하여 손글씨 숫자를 인식하려고 한다.

하지만 아직 모델을 만들지 않았기 때문에 아래 코드에서 prediction_model / prediction_result / prediction_time 세 개의 컬럼이 NULL 값인 상태이다. 따라서 랜덤한 숫자를 넣을 것 이다. 추가적으로 라인 알람을 설정해주었다.

 

import jigeum.seoul
import requests
import os

def run():
    """image_processing 테이블을 읽어서 가장 오래된 요청 하나씩을 처리"""

    # STEP 1
    # image_processing 테이블의 prediction_result IS NULL 인 ROW 1 개 조회 - num 갖여오기

    from mnist.db import get_conn
    from random import randrange
    conn = get_conn()

    with conn:
        with conn.cursor() as cursor:
            sql = "SELECT * FROM image_processing WHERE prediction_result IS NULL ORDER BY num"
            cursor.execute(sql)
            result = cursor.fetchall() #모든 행을 가져옴

    # STEP 2
    # RANDOM 으로 0 ~ 9 중 하나 값을 prediction_result 컬럼에 업데이트
    # 동시에 prediction_model, prediction_time 도 업데이트

            for i in result:
                number = randrange(10)
                num_id = i["num"] #key값
                sql = f"""
                        UPDATE image_processing
                        SET prediction_result = {number},
                            prediction_model = {number},
                            prediction_time = '{jigeum.seoul.now()}'
                        WHERE num = {num_id}
                        """
                cursor.execute(sql)

            conn.commit()


    # STEP 3
    # LINE 으로 처리 결과 전송
    KEY = os.environ.get('API_TOKEN')
    url = "https://notify-api.line.me/api/notify"
    data = {"message": "성공적으로 저장했습니다!"}
    headers = {"Authorization": f"Bearer {KEY}"}
    response = requests.post(url, data=data, headers=headers)

    print(response.text)

    return True
$ cat pyproject.toml

[project.scripts]
ml-worker = 'mnist.worker:run'

$ cat ~/.zshrc
export LINE_API_TOKEN= <비밀>

 

fastapi에서 이미지를 업로드하면 아래와 같이 DB에 저장되고,

의도했던대로 prediction_model / prediction_result 가 null이 아닌 랜던 값인 것을 확인할 수 있다.

 

그리고 아래와 같은 방식으로 run 함수를 실행시키면 라인 메시지가 오는 것을 확인할 수 있다.

$ ml-worker
{"status":200,"message":"ok"}

 

**크론탭 시간나면 해보기

 


학원에서 했던 내용에 이어서 하지 않고 집에서 쓰는 노트북에 새로 디렉토리를 만들어서 작업했다.

이 내용을 기존에 존재하던 mnist 레포에 연결하고 싶었다.

 

방법)

1. 원격 레포지토리 설정

$ git remote add origin <깃헙주소(나는 ssh로 했음)>

 

2. 브랜치 체크아웃

$ git checkout -b 0.1

 

3. 파일 추가 및 커밋

$ git add .
$ git commit -m "home"
$ git push --set-upstream origin 0.1

 

checkout -b 옵션을 새로 알게 되었다. \^0^/

'playdata > daily' 카테고리의 다른 글

12주차 : Day 3 (9/25)  (1) 2024.09.30
12주차 : Day 2 (9/24)  (0) 2024.09.30
10주차 : Day 4,5 (9/12,13)  (3) 2024.09.23
10주차 : Day 2,3 (9/10,11)  (0) 2024.09.23
10주차 - Day 1(9/9)  (0) 2024.09.23