From c61fce7dea9a00796afad6c85ed905693d204d91 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 19:35:08 +0900 Subject: [PATCH 01/25] test: Test product-api --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bb051c2..67e5849 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [ main, fix/package-error ] + branches: [ main, fix/product-api ] pull_request: branches: [ main ] From 0a97cbee50dc35bc8134b4a739cc5eaccc931865 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 19:54:39 +0900 Subject: [PATCH 02/25] test: Add logging --- product/repository/recommendation_repository.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/product/repository/recommendation_repository.py b/product/repository/recommendation_repository.py index 82653a1..b3b6e26 100644 --- a/product/repository/recommendation_repository.py +++ b/product/repository/recommendation_repository.py @@ -43,6 +43,7 @@ def save_to_s3(self, s3_key: str, recommendation_data: Dict[str, Any]) -> bool: def get_recommendation_from_opensearch(self, user_id: str) -> Dict: """최근 14일 이내에 생성된 추천 결과 중 가장 최근 문서를 OpenSearch에서 조회""" + logger.info(f"user_id is {user_id}") try: query = { "query": { @@ -54,8 +55,10 @@ def get_recommendation_from_opensearch(self, user_id: str) -> Dict: "size": 1 } + logger.info(query) + result = self.opensearch.search(query) - print(result) + logger.info(result) hits = result.get("hits", {}).get("hits", []) if hits: @@ -68,6 +71,7 @@ def get_recommendation_from_opensearch(self, user_id: str) -> Dict: try: doc_date = datetime.strptime(date_str, "%Y%m%d") if doc_date >= datetime.now() - timedelta(days=14): + logger.info(source) return { "status": "success", "data": source, From 28ecd2074f942a84cac6e746301b01eb9602f1b4 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 20:33:33 +0900 Subject: [PATCH 03/25] refactor: Change doc_id logic --- product/service/recommendation_saver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/product/service/recommendation_saver.py b/product/service/recommendation_saver.py index 30c5156..cfecee2 100644 --- a/product/service/recommendation_saver.py +++ b/product/service/recommendation_saver.py @@ -78,9 +78,11 @@ def recommend_for_existing_user( # 추천 엔진 실행 recommendations = engine.recommend(user_id, top_k=top_k) rec_result = recommendations.to_dict(orient='records') + # S3 키 생성 - today = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + now = datetime.datetime.now() + today = now.strftime("%Y%m%d_%H%M%S") doc_id = f"user_{user_id}_{today}" s3_key = f"recommendations/user_{user_id}/product_{today}.json" experiment_id = 2 From 0c7eef3234dcd5bbdb8b45c48bd338939cb1551d Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 20:34:11 +0900 Subject: [PATCH 04/25] refactor: Delete created_at, updated_at --- config/opensearch_mappings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/opensearch_mappings.py b/config/opensearch_mappings.py index 9dd47ae..914a7d3 100644 --- a/config/opensearch_mappings.py +++ b/config/opensearch_mappings.py @@ -35,9 +35,7 @@ } }, "experiment_id": {"type": "keyword"}, - "run_id": {"type": "keyword"}, - "created_at": {"type": "date"}, - "updated_at": {"type": "date"} + "run_id": {"type": "keyword"} } } } From c2b2c6193611c4f0d3ee781576f120ba10e8f8f0 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 20:35:03 +0900 Subject: [PATCH 05/25] fix: Set Docker container timezone to Asia/Seoul --- Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index bc4aac7..c97a0f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,15 @@ FROM python:3.12-slim # 작업 디렉토리 설정 WORKDIR /app -# 시스템 의존성 최소화 및 locale 설정 +# 시스템 의존성 최소화 및 시간대(tzdata) 및 locale 설정 RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ curl \ - && apt-get clean && \ + tzdata && \ + ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone && \ + apt-get clean && \ rm -rf /var/lib/apt/lists/* # 필요 패키지 복사 및 설치 @@ -22,7 +25,8 @@ COPY . . # 환경변수 설정 ENV PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ - PORT=8000 + PORT=8000 \ + TZ=Asia/Seoul # 포트 노출 EXPOSE 8000 From a44d6e21d0cab3d6ee40b9e7a3b30b14c35055ad Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 21:51:41 +0900 Subject: [PATCH 06/25] chore: Delete prefect secret --- config/mysql_config.py | 19 ++++++++----------- config/opensearch_config.py | 16 +++++++--------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/config/mysql_config.py b/config/mysql_config.py index 4f5ee2b..76b129d 100644 --- a/config/mysql_config.py +++ b/config/mysql_config.py @@ -1,17 +1,14 @@ -from prefect.blocks.system import Secret +import os +from dotenv import load_dotenv -MYSQL_URL = Secret.load("mysql-url", _sync=True) -MYSQL_DATABASE = Secret.load("mysql-database", _sync=True) -MYSQL_PORT = Secret.load("mysql-port", _sync=True) -MYSQL_USERNAME = Secret.load("mysql-username", _sync=True) -MYSQL_PASSWORD = Secret.load("mysql-password", _sync=True) +load_dotenv() MYSQL_CONFIG = { - "url": MYSQL_URL.get(), - "database": MYSQL_DATABASE.get(), - "port": MYSQL_PORT.get(), - "username": MYSQL_USERNAME.get(), - "password": MYSQL_PASSWORD.get(), + "url": os.getenv("MYSQL_URL"), + "database": os.getenv("MYSQL_DATABASE"), + "port": int(os.getenv("MYSQL_PORT", 3306)), + "username": os.getenv("MYSQL_USERNAME"), + "password": os.getenv("MYSQL_PASSWORD"), "charset": "utf8mb4", "timezone": "Asia/Seoul", } \ No newline at end of file diff --git a/config/opensearch_config.py b/config/opensearch_config.py index 17d8933..34c8b5f 100644 --- a/config/opensearch_config.py +++ b/config/opensearch_config.py @@ -1,13 +1,11 @@ -from prefect.blocks.system import Secret +import os +from dotenv import load_dotenv -OPENSEARCH_HOST = Secret.load("opensearch-host", _sync=True) -OPENSEARCH_PORT = Secret.load("opensearch-port", _sync=True) -OPENSEARCH_USERNAME = Secret.load("opensearch-username", _sync=True) -OPENSEARCH_PASSWORD = Secret.load("opensearch-password", _sync=True) +load_dotenv() OPENSEARCH_CONFIG = { - "host": OPENSEARCH_HOST.get(), - "port": OPENSEARCH_PORT.get(), - "username": OPENSEARCH_USERNAME.get(), - "password": OPENSEARCH_PASSWORD.get() + "host": os.getenv("OPENSEARCH_HOST"), + "port": os.getenv("OPENSEARCH_PORT", 9200), + "username": os.getenv("OPENSEARCH_USERNAME", "admin"), + "password": os.getenv("OPENSEARCH_PASSWORD") } \ No newline at end of file From 67b2cc37659e98bd8279e2aeec2498814c3f188d Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 21:52:12 +0900 Subject: [PATCH 07/25] chore: Add mysql env in docker-compose.yaml --- docker-compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 7034806..ab27c81 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,12 @@ services: - OPENSEARCH_PORT=${OPENSEARCH_PORT} - OPENSEARCH_USERNAME=${OPENSEARCH_USERNAME} - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} + - MYSQL_URL=${MYSQL_URL} + - MYSQL_PORT=${MYSQL_PORT} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USERNAME=${MYSQL_USERNAME} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: - .env:/app/.env restart: always From 676af63c01c1968af080887c891c99e786ce87f9 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 22:40:44 +0900 Subject: [PATCH 08/25] refactor: Add logs --- utils/storage/opensearch_manager.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/utils/storage/opensearch_manager.py b/utils/storage/opensearch_manager.py index e0e042e..193d5ba 100644 --- a/utils/storage/opensearch_manager.py +++ b/utils/storage/opensearch_manager.py @@ -25,15 +25,15 @@ def create_index(self) -> bool: print(f"Creating index {self.index} if it does not exist...") # 인덱스 존재 여부 확인 if self.client.indices.exists(index=self.index): - logging.info(f"Index {self.index} already exists") + print(f"Index {self.index} already exists") return True response = self.client.indices.create(index=self.index, body=self.mapping) - logging.info(f"Created new index {self.index}") + print(f"Created new index {self.index}") return True except Exception as e: - logging.error(f"Failed to create index {self.index}: {str(e)}") + print(f"Failed to create index {self.index}: {str(e)}") return False # 인덱스 타입 자동 추론돼서 에러 발생 @@ -97,7 +97,13 @@ def delete_all(self) -> int: def delete_index(self) -> bool: """인덱스 전체 삭제""" try: - response = self.client.indices.delete(index=self.index) - return True + if self.client.indices.exists(index=self.index): + response = self.client.indices.delete(index=self.index) + print(f"[INFO] 인덱스 '{self.index}' 삭제 완료: {response}") + return response.get("acknowledged", False) + else: + print(f"[INFO] 인덱스 '{self.index}' 존재하지 않음.") + return True except Exception as e: + print(f"[ERROR] 인덱스 삭제 실패: {e}") return False \ No newline at end of file From fc2a6c2c91ab3b53472f7f9e908173b0a4a58556 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 22:41:25 +0900 Subject: [PATCH 09/25] fix: Modify PRODUCT_MAPPING --- config/opensearch_mappings.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/config/opensearch_mappings.py b/config/opensearch_mappings.py index 914a7d3..90f257f 100644 --- a/config/opensearch_mappings.py +++ b/config/opensearch_mappings.py @@ -27,16 +27,9 @@ "properties": { "doc_id": {"type": "keyword"}, "user_id": {"type": "keyword"}, - "recommended_item_ids": { - "type": "nested", - "properties": { - "item_id": {"type": "keyword"}, - "score": {"type": "float"} - } - }, + "recommended_item_ids": { "type": "keyword" }, "experiment_id": {"type": "keyword"}, "run_id": {"type": "keyword"} } } } - From 973dd4f48ced42e4694a522c5704b1760fb4811c Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 22:42:09 +0900 Subject: [PATCH 10/25] test: Add delete_index() in pipeline test --- test/product/test_product_service_pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/product/test_product_service_pipeline.py b/test/product/test_product_service_pipeline.py index 954106d..5509be6 100644 --- a/test/product/test_product_service_pipeline.py +++ b/test/product/test_product_service_pipeline.py @@ -13,6 +13,9 @@ def test_delete_storage_data(service, s3_prefix: str): deleted_os = service.repository.opensearch.delete_all() print(f"OpenSearch 삭제된 문서 수: {deleted_os}") + deleted_index = service.repository.opensearch.delete_index() + print(f"OpenSearch 인덱스 삭제 결과: {deleted_index}") + def test_run_full_pipeline(service): """추천 파이프라인 전체 실행 (업로드)""" From def1d178b143adcb31a30df7f5c4dd12f847f06c Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 22:55:06 +0900 Subject: [PATCH 11/25] chore: Delete tzdata --- Dockerfile | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index c97a0f7..bc4aac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,15 +4,12 @@ FROM python:3.12-slim # 작업 디렉토리 설정 WORKDIR /app -# 시스템 의존성 최소화 및 시간대(tzdata) 및 locale 설정 +# 시스템 의존성 최소화 및 locale 설정 RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ curl \ - tzdata && \ - ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ - echo "Asia/Seoul" > /etc/timezone && \ - apt-get clean && \ + && apt-get clean && \ rm -rf /var/lib/apt/lists/* # 필요 패키지 복사 및 설치 @@ -25,8 +22,7 @@ COPY . . # 환경변수 설정 ENV PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ - PORT=8000 \ - TZ=Asia/Seoul + PORT=8000 # 포트 노출 EXPOSE 8000 From 43f3611b770a912bd618e53c0011a322487af7e4 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 23:17:49 +0900 Subject: [PATCH 12/25] refactor: Change opensearch index --- api/routers/recommendation.py | 2 +- product/service/new_user_recommendation_service.py | 2 +- product/service/recommendation_service.py | 2 +- test/product/test_product_service_pipeline.py | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/routers/recommendation.py b/api/routers/recommendation.py index cce7e6e..1cb1e93 100644 --- a/api/routers/recommendation.py +++ b/api/routers/recommendation.py @@ -13,7 +13,7 @@ # RecommendationService 인스턴스 생성 service = RecommendationRepository( s3_bucket="team6-mlops-bucket", - opensearch_index="recommendations", + opensearch_index="recommendations-v2", mapping=PRODUCT_MAPPING ) diff --git a/product/service/new_user_recommendation_service.py b/product/service/new_user_recommendation_service.py index ba6c28f..9d4a4c7 100644 --- a/product/service/new_user_recommendation_service.py +++ b/product/service/new_user_recommendation_service.py @@ -37,7 +37,7 @@ def __init__(self): # 저장소 및 saver 준비 self.repository = RecommendationRepository( s3_bucket="team6-mlops-bucket", - opensearch_index="recommendations", + opensearch_index="recommendations-v2", mapping=PRODUCT_MAPPING ) self.saver = RecommendationSaver() diff --git a/product/service/recommendation_service.py b/product/service/recommendation_service.py index 5ab2186..05b0ef6 100644 --- a/product/service/recommendation_service.py +++ b/product/service/recommendation_service.py @@ -30,7 +30,7 @@ def __init__(self, bucket: str = "team6-mlops-bucket"): self.engine = None self.repository = RecommendationRepository( s3_bucket="team6-mlops-bucket", - opensearch_index="recommendations", + opensearch_index="recommendations-v2", mapping=PRODUCT_MAPPING ) diff --git a/test/product/test_product_service_pipeline.py b/test/product/test_product_service_pipeline.py index 5509be6..b222b2d 100644 --- a/test/product/test_product_service_pipeline.py +++ b/test/product/test_product_service_pipeline.py @@ -9,12 +9,12 @@ def test_delete_storage_data(service, s3_prefix: str): # S3 데이터 삭제 deleted_s3 = service.repository.s3.delete_prefix(s3_prefix) print(f"S3 삭제된 객체 수: {deleted_s3}") - # OpenSearch 데이터 삭제 - deleted_os = service.repository.opensearch.delete_all() - print(f"OpenSearch 삭제된 문서 수: {deleted_os}") + # # OpenSearch 데이터 삭제 + # deleted_os = service.repository.opensearch.delete_all() + # print(f"OpenSearch 삭제된 문서 수: {deleted_os}") - deleted_index = service.repository.opensearch.delete_index() - print(f"OpenSearch 인덱스 삭제 결과: {deleted_index}") + # deleted_index = service.repository.opensearch.delete_index() + # print(f"OpenSearch 인덱스 삭제 결과: {deleted_index}") def test_run_full_pipeline(service): """추천 파이프라인 전체 실행 (업로드)""" From 8f5abd3d2712b34c71ca6e864d7f9c45587bbaaa Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Wed, 25 Jun 2025 23:44:18 +0900 Subject: [PATCH 13/25] fix: Change repository to service --- api/routers/recommendation.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/api/routers/recommendation.py b/api/routers/recommendation.py index 1cb1e93..505ad90 100644 --- a/api/routers/recommendation.py +++ b/api/routers/recommendation.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, HTTPException -from product.repository.recommendation_repository import RecommendationRepository +from product.service.recommendation_service import RecommendationService import logging from config.opensearch_mappings import PRODUCT_MAPPING import datetime @@ -11,11 +11,7 @@ router = APIRouter() # RecommendationService 인스턴스 생성 -service = RecommendationRepository( - s3_bucket="team6-mlops-bucket", - opensearch_index="recommendations-v2", - mapping=PRODUCT_MAPPING - ) +service = RecommendationService() @router.get("/recommendations/{user_id}") def get_recommendations(user_id: int): @@ -23,7 +19,7 @@ def get_recommendations(user_id: int): 사용자별 추천 결과를 조회 """ try: - recommendations = service.get_recommendation_from_opensearch(str(user_id)) + recommendations = service.repository.get_recommendation_from_opensearch(str(user_id)) if not recommendations: raise HTTPException( From 278c1ae0134a76ef33a1831a5a80ee5e220a8c1c Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 00:04:03 +0900 Subject: [PATCH 14/25] fix: Delete docker build cash --- .github/workflows/ci-cd.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 67e5849..407ec71 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,14 +38,13 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Build and push Docker image + - name: Build and push Docker image (No Cache) uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest - cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest - cache-to: type=inline + no-cache: true platforms: linux/amd64 - name: Deploy to EC2 From b8ce49c2a6b6f867910cb006b838a72afefdb51e Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 00:04:45 +0900 Subject: [PATCH 15/25] refactor: Change config again --- config/mysql_config.py | 19 +++++++++++-------- config/opensearch_config.py | 16 +++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/config/mysql_config.py b/config/mysql_config.py index 76b129d..4f5ee2b 100644 --- a/config/mysql_config.py +++ b/config/mysql_config.py @@ -1,14 +1,17 @@ -import os -from dotenv import load_dotenv +from prefect.blocks.system import Secret -load_dotenv() +MYSQL_URL = Secret.load("mysql-url", _sync=True) +MYSQL_DATABASE = Secret.load("mysql-database", _sync=True) +MYSQL_PORT = Secret.load("mysql-port", _sync=True) +MYSQL_USERNAME = Secret.load("mysql-username", _sync=True) +MYSQL_PASSWORD = Secret.load("mysql-password", _sync=True) MYSQL_CONFIG = { - "url": os.getenv("MYSQL_URL"), - "database": os.getenv("MYSQL_DATABASE"), - "port": int(os.getenv("MYSQL_PORT", 3306)), - "username": os.getenv("MYSQL_USERNAME"), - "password": os.getenv("MYSQL_PASSWORD"), + "url": MYSQL_URL.get(), + "database": MYSQL_DATABASE.get(), + "port": MYSQL_PORT.get(), + "username": MYSQL_USERNAME.get(), + "password": MYSQL_PASSWORD.get(), "charset": "utf8mb4", "timezone": "Asia/Seoul", } \ No newline at end of file diff --git a/config/opensearch_config.py b/config/opensearch_config.py index 34c8b5f..17d8933 100644 --- a/config/opensearch_config.py +++ b/config/opensearch_config.py @@ -1,11 +1,13 @@ -import os -from dotenv import load_dotenv +from prefect.blocks.system import Secret -load_dotenv() +OPENSEARCH_HOST = Secret.load("opensearch-host", _sync=True) +OPENSEARCH_PORT = Secret.load("opensearch-port", _sync=True) +OPENSEARCH_USERNAME = Secret.load("opensearch-username", _sync=True) +OPENSEARCH_PASSWORD = Secret.load("opensearch-password", _sync=True) OPENSEARCH_CONFIG = { - "host": os.getenv("OPENSEARCH_HOST"), - "port": os.getenv("OPENSEARCH_PORT", 9200), - "username": os.getenv("OPENSEARCH_USERNAME", "admin"), - "password": os.getenv("OPENSEARCH_PASSWORD") + "host": OPENSEARCH_HOST.get(), + "port": OPENSEARCH_PORT.get(), + "username": OPENSEARCH_USERNAME.get(), + "password": OPENSEARCH_PASSWORD.get() } \ No newline at end of file From cf28976b4add3f0c8ec74ab2358717d3a7ef470f Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 00:28:42 +0900 Subject: [PATCH 16/25] fix: Set Dockeer image tag --- .github/workflows/ci-cd.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 407ec71..e2a9200 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -17,6 +17,11 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + # 고유 태그(커밋 SHA) 생성 + - name: Set Docker image tag + id: vars + run: echo "TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV + - name: Create .env file run: | echo "📝 Creating .env file..." @@ -38,22 +43,24 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + # 고유 태그로 이미지 빌드 및 푸시 - name: Build and push Docker image (No Cache) uses: docker/build-push-action@v5 with: context: . push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest + tags: ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${{ env.TAG }} no-cache: true platforms: linux/amd64 + # EC2에 고유 태그로 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} - envs: DOCKERHUB_USERNAME + envs: DOCKERHUB_USERNAME, TAG script: | echo "🚀 Starting deployment process..." @@ -61,7 +68,7 @@ jobs: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} echo "📦 Pulling latest image..." - docker pull ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest + docker pull ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${TAG} echo "🛑 Checking if container exists..." if docker container inspect ml-api >/dev/null 2>&1; then @@ -88,7 +95,7 @@ jobs: -p 8000:8000 \ --restart unless-stopped \ --env-file /home/ubuntu/machine-learning/.env \ - ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest + ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${TAG} echo "📋 Container status:" docker ps -a | grep ml-api || echo "❌ ml-api container not found" From d54ff66fa03a7aeb2af2c2b6cdd3e5e7644bc938 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 01:14:23 +0900 Subject: [PATCH 17/25] chore: Add prefect key in ci-cd.yml --- .github/workflows/ci-cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e2a9200..34df296 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -36,6 +36,8 @@ jobs: echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> .env echo "MYSQL_USERNAME=${{ secrets.MYSQL_USERNAME }}" >> .env echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> .env + echo "PREFECT_API_URL=${{ secrets.PREFECT_API_URL }}" >> .env + echo "PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }}" >> .env - name: Login to Docker Hub uses: docker/login-action@v3 From efa92de4e63398afc91cbb57663e8c0a7c66cdbc Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 14:00:36 +0900 Subject: [PATCH 18/25] chore: Modify Dockerfile --- Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index bc4aac7..1dff194 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM python:3.12-slim # 작업 디렉토리 설정 WORKDIR /app -# 시스템 의존성 최소화 및 locale 설정 +# 시스템 의존성 설치 및 정리 RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ @@ -12,11 +12,13 @@ RUN apt-get update && \ && apt-get clean && \ rm -rf /var/lib/apt/lists/* -# 필요 패키지 복사 및 설치 -COPY requirements.txt ./ +# requirements.txt만 먼저 복사 - 캐시 최적화 +COPY requirements.txt . + +# 패키지 설치 - requirements가 변경되지 않으면 캐시됨 RUN pip install --no-cache-dir -r requirements.txt -# 소스 코드 복사 +# 이후 전체 소스 코드 복사 - 변경된 코드만 캐시 무효화 COPY . . # 환경변수 설정 @@ -27,5 +29,5 @@ ENV PYTHONUNBUFFERED=1 \ # 포트 노출 EXPOSE 8000 -# 실행 명령어 +# 컨테이너 시작 시 실행할 명령어 CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] From 69a30d267b9637aa9b36ee1f8ef6def2e8a2e42f Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 14:01:15 +0900 Subject: [PATCH 19/25] chore: Add Prefect Auth in ci-cd.yaml --- .github/workflows/ci-cd.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 34df296..5e6880b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -45,6 +45,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Prefect Auth + uses: PrefectHQ/actions-prefect-auth@v1 + with: + prefect-api-key: ${{ secrets.PREFECT_API_KEY }} + prefect-workspace: ${{ secrets.PREFECT_WORKSPACE }} + # 고유 태그로 이미지 빌드 및 푸시 - name: Build and push Docker image (No Cache) uses: docker/build-push-action@v5 From 52b9420f0df7a6eb9cf67e1366273c26b1fa293a Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 14:24:45 +0900 Subject: [PATCH 20/25] chore: Add multistage build --- Dockerfile | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1dff194..840a405 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,39 @@ -# 경량 베이스 이미지 사용 -FROM python:3.12-slim +# 1단계: 의존성만 설치하는 빌더 스테이지 +FROM python:3.12-slim AS builder -# 작업 디렉토리 설정 WORKDIR /app -# 시스템 의존성 설치 및 정리 +# 시스템 패키지 최소화, 빌드 툴만 설치 RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - && apt-get clean && \ + apt-get install -y --no-install-recommends build-essential && \ rm -rf /var/lib/apt/lists/* -# requirements.txt만 먼저 복사 - 캐시 최적화 +# requirements.txt만 복사 (캐시 최적화) COPY requirements.txt . -# 패키지 설치 - requirements가 변경되지 않으면 캐시됨 -RUN pip install --no-cache-dir -r requirements.txt +# 패키지 설치 (캐시 활용) +RUN pip install --user --no-cache-dir -r requirements.txt -# 이후 전체 소스 코드 복사 - 변경된 코드만 캐시 무효화 -COPY . . +# 2단계: 실행용 베이스 이미지 +FROM python:3.12-slim + +WORKDIR /app -# 환경변수 설정 -ENV PYTHONUNBUFFERED=1 \ +# 빌더에서 설치한 패키지 복사 +COPY --from=builder /root/.local /root/.local + +# 환경변수로 PATH 추가 +ENV PATH=/root/.local/bin:$PATH \ + PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ PORT=8000 -# 포트 노출 +# 소스코드 복사 +COPY . . + +# 불필요한 파일 복사 방지 (반드시 .dockerignore 사용) +# 예: .git, __pycache__, *.log 등 + EXPOSE 8000 -# 컨테이너 시작 시 실행할 명령어 -CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file From 5099f02ea12c59efec6e0f09dce89aa2037639aa Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 14:42:33 +0900 Subject: [PATCH 21/25] chore: Add cache docker layers --- .github/workflows/ci-cd.yml | 77 ++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5e6880b..fe87762 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -10,21 +10,41 @@ jobs: deploy: runs-on: ubuntu-latest + env: + DOCKER_BUILDKIT: 1 # BuildKit 활성화 + steps: - name: Checkout code uses: actions/checkout@v4 + - name: Print start log + run: echo "🚀 [START] CI/CD Workflow started!" + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Print cache status + run: echo "🗄️ [CACHE] Docker layer cache step completed." + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Print buildx status + run: echo "🔧 [BUILDX] Docker Buildx set up completed." - # 고유 태그(커밋 SHA) 생성 - name: Set Docker image tag id: vars - run: echo "TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV + run: | + echo "🏷️ [TAG] Setting Docker image tag to first 7 chars of GITHUB_SHA." + echo "TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV + echo "TAG=${GITHUB_SHA::7}" - name: Create .env file run: | - echo "📝 Creating .env file..." + echo "📝 [ENV] Creating .env file with secrets." echo "OPENSEARCH_USERNAME=${{ secrets.OPENSEARCH_USERNAME }}" >> .env echo "OPENSEARCH_HOST=${{ secrets.OPENSEARCH_HOST }}" >> .env echo "OPENSEARCH_PORT=${{ secrets.OPENSEARCH_PORT }}" >> .env @@ -39,29 +59,37 @@ jobs: echo "PREFECT_API_URL=${{ secrets.PREFECT_API_URL }}" >> .env echo "PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }}" >> .env + - name: Print .env status + run: echo "✅ [.ENV] .env file created." + - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Print Docker login status + run: echo "🔑 [DOCKER] Docker Hub login successful." - name: Prefect Auth uses: PrefectHQ/actions-prefect-auth@v1 with: prefect-api-key: ${{ secrets.PREFECT_API_KEY }} prefect-workspace: ${{ secrets.PREFECT_WORKSPACE }} - - # 고유 태그로 이미지 빌드 및 푸시 - - name: Build and push Docker image (No Cache) + - name: Print Prefect auth status + run: echo "🔗 [PREFECT] Prefect authentication completed." + + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${{ env.TAG }} - no-cache: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${{ env.TAG }} + ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:latest platforms: linux/amd64 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache - # EC2에 고유 태그로 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@master with: @@ -70,34 +98,34 @@ jobs: key: ${{ secrets.EC2_SSH_KEY }} envs: DOCKERHUB_USERNAME, TAG script: | - echo "🚀 Starting deployment process..." + echo "🚀 [DEPLOY] Starting deployment process on EC2..." - echo "🔐 Logging into Docker..." + echo "🔐 [DEPLOY] Logging into Docker Hub on EC2..." docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} - echo "📦 Pulling latest image..." + echo "📦 [DEPLOY] Pulling latest image..." docker pull ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${TAG} - echo "🛑 Checking if container exists..." + echo "🛑 [DEPLOY] Checking if old container exists..." if docker container inspect ml-api >/dev/null 2>&1; then - echo "🧼 Stopping and removing old container..." + echo "🧼 [DEPLOY] Stopping and removing old container..." docker stop -t 30 ml-api || true docker rm ml-api || true else - echo "✅ No existing ml-api container." + echo "✅ [DEPLOY] No existing ml-api container." fi - echo "🧹 Docker system cleanup (optional)..." + echo "🧹 [DEPLOY] Docker system cleanup (optional)..." docker system prune -f - echo "📝 Validating .env file..." + echo "📝 [DEPLOY] Validating .env file on EC2..." if [ ! -f "/home/ubuntu/machine-learning/.env" ]; then - echo "⚠️ /home/ubuntu/machine-learning/.env not found. Creating empty .env..." + echo "⚠️ [DEPLOY] .env file not found. Creating empty .env..." mkdir -p /home/ubuntu/machine-learning touch /home/ubuntu/machine-learning/.env fi - echo "🚀 Starting new container..." + echo "🚀 [DEPLOY] Starting new container..." docker run -d \ --name ml-api \ -p 8000:8000 \ @@ -105,8 +133,11 @@ jobs: --env-file /home/ubuntu/machine-learning/.env \ ${{ secrets.DOCKERHUB_USERNAME }}/ml-api:${TAG} - echo "📋 Container status:" - docker ps -a | grep ml-api || echo "❌ ml-api container not found" + echo "📋 [DEPLOY] Container status:" + docker ps -a | grep ml-api || echo "❌ [DEPLOY] ml-api container not found" + + echo "📄 [DEPLOY] Container logs:" + docker logs ml-api || echo "⚠️ [DEPLOY] Failed to retrieve logs. Container may have exited early." - echo "📄 Container logs:" - docker logs ml-api || echo "⚠️ Failed to retrieve logs. Container may have exited early." + - name: Print finish log + run: echo "✅ [FINISH] CI/CD Workflow completed!" From 88230e52a6dabdf814f69150e649ffbda91313cd Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 15:04:58 +0900 Subject: [PATCH 22/25] refactor: Delete pip user, env path --- Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 840a405..5b7842c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && \ COPY requirements.txt . # 패키지 설치 (캐시 활용) -RUN pip install --user --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt # 2단계: 실행용 베이스 이미지 FROM python:3.12-slim @@ -22,9 +22,8 @@ WORKDIR /app # 빌더에서 설치한 패키지 복사 COPY --from=builder /root/.local /root/.local -# 환경변수로 PATH 추가 -ENV PATH=/root/.local/bin:$PATH \ - PYTHONUNBUFFERED=1 \ +# 환경변수 +ENV PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ PORT=8000 From 34fc14c65161999da89c5fc57109ed1143fb195c Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 15:06:00 +0900 Subject: [PATCH 23/25] chore: Modify prefect branch to main --- prefect.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefect.yaml b/prefect.yaml index 6b3355b..15d4bce 100644 --- a/prefect.yaml +++ b/prefect.yaml @@ -2,7 +2,7 @@ pull: - prefect.deployments.steps.git_clone: id: clone-step repository: https://github.com/Team-MoongChi/machine-learning.git - branch: prefect-flows + branch: main - prefect.deployments.steps.pip_install_requirements: requirements_file: requirements.txt From 31d0d5625e446519ebbc23b1e341eea61c32c8d6 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 15:13:50 +0900 Subject: [PATCH 24/25] fix: Modify builder path --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5b7842c..2977c2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM python:3.12-slim WORKDIR /app # 빌더에서 설치한 패키지 복사 -COPY --from=builder /root/.local /root/.local +COPY --from=builder /app /app # 환경변수 ENV PYTHONUNBUFFERED=1 \ From b902cc96760cd007b18683c9d2161ee31018dc86 Mon Sep 17 00:00:00 2001 From: suri-pu-bi Date: Thu, 26 Jun 2025 15:32:37 +0900 Subject: [PATCH 25/25] fix: Add COPY builder path --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2977c2e..8a84d40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,10 @@ FROM python:3.12-slim WORKDIR /app -# 빌더에서 설치한 패키지 복사 -COPY --from=builder /app /app +# 빌더에서 패키지와 바이너리 복사 +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + # 환경변수 ENV PYTHONUNBUFFERED=1 \