-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] : FastAPI로 마이그레이션 & AWS Lambda로 배포한다 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughFlask 앱을 제거하고 FastAPI(Mangum)로 전환했으며 Selenium WebDriver 풀을 도입하고 Discord 알림·로깅·보안·예외 핸들러 모듈을 추가했습니다. CI/CD는 DockerHub/EC2에서 Amazon ECR→AWS Lambda 이미지 배포로 변경되었습니다. (50단어 이내) Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client as 클라이언트
participant APIGW as API Gateway
participant Lambda as AWS Lambda (Mangum)
participant FastAPI as FastAPI 앱
participant Sec as API Key 검증
participant Pool as WebDriverPool
participant Crawler as crawl_schedule
participant Discord as Discord Webhook
Client->>APIGW: GET /api/v1/schedule?url=...
APIGW->>Lambda: Invoke
Lambda->>FastAPI: ASGI 요청
FastAPI->>Sec: X-API-Key 검증
Sec-->>FastAPI: OK / 401
alt 유효한 키
FastAPI->>Pool: get_driver()
FastAPI->>Crawler: crawl_schedule(url, request, pool) [스레드]
Crawler-->>Pool: release_driver()
alt 성공
Crawler-->>FastAPI: 결과 (200)
else 도메인 오류 (CRAWLING-003)
Crawler-->>FastAPI: 오류 응답 (400)
end
else 키 누락/불일치
FastAPI-->>Client: 401 JSON
end
opt 예외 발생
Crawler->>Discord: send_error_to_discord(...)
Crawler-->>FastAPI: 예외 -> 500 JSON
end
FastAPI-->>Lambda: 응답
Lambda-->>APIGW: Response
APIGW-->>Client: HTTP 응답
sequenceDiagram
autonumber
actor GH as GitHub Actions
participant ECR as Amazon ECR
participant Lambda as AWS Lambda
GH->>ECR: Login (amazon-ecr-login)
GH->>ECR: Build & Push image (:<git-sha>)
GH->>Lambda: UpdateFunctionCode (imageUri)
GH->>Lambda: Wait for update
GH->>Lambda: UpdateFunctionConfiguration (env vars)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.github/workflows/crawler-cicd.yaml (1)
51-64: 환경변수 업데이트 시 기존 변수 손실 위험
update-function-configuration --environment Variables={...}는 기존 변수를 덮어씁니다.적용 제안(diff):
- - name: Update Lambda function configuration + - name: Update Lambda function configuration (merge env) env: API_KEY_SECRET: ${{ secrets.API_KEY }} DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | - # Lambda가 환경 변수를 업데이트할 때까지 잠시 대기 - aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} - - # GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정 - aws lambda update-function-configuration \ - --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ - --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}" + set -euo pipefail + aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} + EXISTING=$(aws lambda get-function-configuration --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} --query 'Environment.Variables' --output json) + echo "$EXISTING" | jq --arg api "$API_KEY_SECRET" --arg wh "$DISCORD_URL_SECRET" '. + {API_KEY:$api, DISCORD_WEBHOOK_URL:$wh}' > merged_env.json + aws lambda update-function-configuration \ + --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ + --environment "Variables=$(cat merged_env.json)" + aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}app/services/crawler.py (1)
37-43: 오류 응답 형태 일관성: 서비스 레이어에서 dict 반환 대신 예외를 던지세요전역 예외 핸들러를 도입했으므로 여기서는 400 예외를 던지는 편이 일관적입니다. 현재는
payload누락으로 스키마도 달라집니다.적용 제안(diff):
- if not days: - return { - "code": "CRAWLING-003", - "message": "공개되지 않은 시간표입니다.", - "is_success": False - } + if not days: + from fastapi import HTTPException + raise HTTPException( + status_code=400, + detail={"code": "CRAWLING-003", "message": "공개되지 않은 시간표입니다.", "is_success": False} + )
🧹 Nitpick comments (19)
.github/PULL_REQUEST_TEMPLATE.md (1)
31-31: LGTM, 표기 명확성 소폭 개선 제안(옵션)문구 자체는 자연스럽고 의도(연관만 표시, 자동 종료 안 함)도 적절합니다. 혼동 방지를 위해 “이슈를 닫지 않음”을 괄호에 명시하면 좋겠습니다.
- - Related to : #00 #01 (해당 커밋과 관련됨) + - Related to : #00 #01 (연관 — 이슈를 닫지 않음)README.md (1)
8-8: 아키텍처 이미지: HTML대신 마크다운 이미지로 전환 + 대체 텍스트 구체화 권장
GitHub 렌더링/반응형 대응을 위해
<img width="983">고정보다형식을 권장합니다. 대체 텍스트도 “FastAPI→API Gateway→Lambda(ECR 이미지) 아키텍처”처럼 구체화해 주세요.app/core/logging_config.py (1)
20-23: 로그 레벨/포맷 환경변수화로 운영 편의성 개선CloudWatch에서 단계별 레벨 제어가 필요합니다. LOG_LEVEL(기본 INFO), JSON 포맷 선택 토글을 지원해 주세요.
적용 예시(diff):
+import os LOGGING_CONFIG = { @@ - "root": { # 모든 로거의 기본 설정 - "level": "INFO", # INFO 레벨 이상의 로그만 출력 + "root": { + "level": os.getenv("LOG_LEVEL", "INFO"), "handlers": ["console"], }, }app/utils/discord_logger.py (2)
57-63: 예외 로깅은 logging.exception 사용스택트레이스 자동 포함을 위해 error 대신 exception 사용.
적용 예시(diff):
- except requests.exceptions.RequestException as err: - logger.error(f"❌ Discord 전송 실패 (네트워크/HTTP 오류): {err}") + except requests.exceptions.RequestException as err: + logger.exception("❌ Discord 전송 실패 (네트워크/HTTP 오류)") if err.response: - logger.error(f"응답 코드: {err.response.status_code}, 응답 내용: {err.response.text}") + logger.exception(f"응답 코드: {err.response.status_code}, 응답 내용: {err.response.text}") - except Exception as err: - logger.error(f"❌ Discord 알림 처리 중 알 수 없는 오류 발생: {err}", exc_info=True) + except Exception: + logger.exception("❌ Discord 알림 처리 중 알 수 없는 오류 발생")
11-13: 환경변수 지연 로드 또는 재평가 권장모듈 임포트 시점에
.env를 로드하고 값을 고정합니다. 런타임에 변경될 수 있으니 함수 내부에서os.getenv재조회 또는 초기화 함수를 두는 방식을 권장합니다.app/api/v1/schedule.py (2)
17-26: 파라미터 검증을 Pydantic/Query로 위임하고 도메인 검사를 보완
url은 필수로 선언해 422를 활용하세요.www.서브도메인 허용(현재는 거부).적용 예시(diff):
-from fastapi import APIRouter, Request, HTTPException, Query, Depends +from fastapi import APIRouter, Request, HTTPException, Query, Depends +import re @@ -async def get_schedule(request: Request, url: str = Query(None)): +async def get_schedule( + request: Request, + url: str = Query(..., min_length=10, regex=r"^https://(www\.)?everytime\.kr/.*"), +): @@ - if not url: - raise HTTPException(status_code=400, - detail={"code": "CRAWLING-001", "message": "URL이 제공되지 않았습니다.", "is_success": False}) - if not url.startswith("https://everytime.kr/"): - raise HTTPException(status_code=400, - detail={"code": "CRAWLING-002", "message": "잘못된 URL 형식입니다.", "is_success": False}) + # 위의 Query 검증으로 미/오입력은 422 처리됨
39-45: 예외 재던지기/연쇄 개선
raise e대신raise사용, 미지정 예외는 원인 체인과 함께 래핑하세요.적용 예시(diff):
- except HTTPException as e: - # 핸들링된 HTTP 예외는 그대로 다시 발생시킴 - raise e - except Exception: - # 핸들링되지 않은 모든 예외는 500 에러로 처리 - raise HTTPException(status_code=500, - detail={"code": "INTERNAL_SERVER_ERROR", "message": "서버 내부 오류가 발생했습니다.", "is_success": False}) + except HTTPException: + raise + except Exception as err: + raise HTTPException( + status_code=500, + detail={"code": "INTERNAL_SERVER_ERROR", "message": "서버 내부 오류가 발생했습니다.", "is_success": False}, + ) from errapp/main.py (1)
44-59: 미들웨어에서 예외 시에도 처리시간 헤더 기록하려면 finally 사용현재
call_next에서 예외가 나면 이후 코드가 실행되지 않습니다.적용 예시(diff):
@app.middleware("http") async def add_process_time_header(request: Request, call_next): - start_time = time.time() - response = await call_next(request) - process_time = time.time() - start_time - response.headers["X-Process-Time"] = str(process_time) - logger.info(f'"{request.method} {request.url.path}" {response.status_code} | Process Time: {process_time:.4f}s') - return response + start_time = time.time() + try: + response = await call_next(request) + return response + finally: + process_time = time.time() - start_time + try: + response.headers["X-Process-Time"] = str(process_time) # 실패 응답에도 가능하면 부여 + except Exception: + pass + logger.info(f'"{request.method} {request.url.path}" | Process Time: {process_time:.4f}s')app/core/exception_handlers.py (1)
28-44: 검증 예외: 빈 에러 리스트/필드 경로 처리 및 ARG001 제거
exc.errors()가 빈 경우 IndexError 가능. 또한request미사용 경고(ruff ARG001).적용 제안(diff):
-async def validation_exception_handler(request: Request, exc: RequestValidationError): +async def validation_exception_handler(_request: Request, exc: RequestValidationError): """RequestValidationError를 공통 포맷으로 변환합니다.""" - - # 유효성 검사 에러에 대한 상세 내용을 메시지로 조합할 수 있습니다. - # 여기서는 간단하게 첫 번째 에러 메시지만 사용합니다. - first_error = exc.errors()[0] - error_message = f"Validation Error: '{first_error['loc'][-1]}' - {first_error['msg']}" + errors = exc.errors() + if errors: + first = errors[0] + loc = first.get("loc", []) + # 'body' 등 루트 토큰 제거하고 '.'로 연결 + path = ".".join(str(p) for p in loc if p not in {"body"}) + msg = first.get("msg", "Invalid input") + error_message = f"Validation Error: '{path or 'unknown'}' - {msg}" + else: + error_message = "Validation Error" return JSONResponse( status_code=422, content={ "code": "VALIDATION_ERROR", "message": error_message, "is_success": False, "payload": None, }, )app/services/driver_pool.py (4)
12-22: Chrome 옵션 Lambda 친화성 개선 필요
--headless구버전 플래그,/tmp사용자 데이터 미사용, zygote 비활성화 누락 등으로 런타임 이슈 가능.적용 제안(diff):
def _create_driver(self): """새로운 WebDriver 인스턴스를 생성합니다.""" chrome_options = Options() - chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("--window-size=1920x1080") chrome_options.add_argument("--no-sandbox") - chrome_options.add_argument("--disable-dev-shm-usage") - chrome_options.add_argument("--headless") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--headless=new") + chrome_options.add_argument("--no-zygote") + chrome_options.add_argument("--single-process") + chrome_options.add_argument("--disable-software-rasterizer") + chrome_options.add_argument("--user-data-dir=/tmp/chrome-user-data") + chrome_options.add_argument("--data-path=/tmp/chrome-data") + chrome_options.add_argument("--disk-cache-dir=/tmp/chrome-cache") chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") return webdriver.Chrome(options=chrome_options)최종 이미지의 Chrome/Chromedriver 버전에 따라
--headless=new지원 여부 확인이 필요합니다.
24-27: 대여 시 무한 대기 방지 및 게으른 생성현재
Queue.get()은 드라이버 고갈 시 영구 블록. 타임아웃/즉시 생성으로 전환 권장.적용 제안(diff):
- def get_driver(self): - """Pool에서 WebDriver를 가져옵니다.""" - return self._pool.get() + def get_driver(self, timeout: float | None = 30): + """Pool에서 WebDriver를 가져오며, 없으면 새로 생성합니다.""" + try: + return self._pool.get(timeout=timeout) if timeout else self._pool.get_nowait() + except queue.Empty: + return self._create_driver()
28-31: 반납 시 데드락 방지 및 과잉 드라이버 정리풀 가득 찬 상태에서
put()은 블록합니다. 즉시 실패 시quit()권장.적용 제안(diff):
- def release_driver(self, driver): - """사용한 WebDriver를 Pool에 반납합니다.""" - self._pool.put(driver) + def release_driver(self, driver): + """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 종료.""" + try: + self._pool.put_nowait(driver) + except queue.Full: + try: + driver.quit() + except Exception: + pass
32-36: 종료 루틴 강화큐에 없는 ‘사용 중’ 드라이버는 종료되지 않습니다. 추적/강제 종료 보완을 고려하세요(예: 약한참조 집합으로 all_drivers 추적).
원하시면 최소 침습으로
all_drivers추적과close_all()보강 패치를 제안 드리겠습니다..github/workflows/crawler-cicd.yaml (2)
32-41: 아키텍처/플랫폼 불일치 가능성 및 안정성 개선
--platform linux/amd64고정은 Lambda 함수가 arm64일 경우 실패합니다.- 스크립트에
set -euo pipefail미설정.적용 제안(diff):
- run: | - docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + run: | + set -euo pipefail + ARCH="${{ secrets.LAMBDA_ARCHITECTURE || 'x86_64' }}" + PLATFORM=$([ "$ARCH" = "arm64" ] && echo "linux/arm64" || echo "linux/amd64") + docker build --platform "$PLATFORM" -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "image_uri=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT함수의 실제 아키텍처 확인이 필요합니다(콘솔/CLI로 확인 부탁).
11-14: 동시 실행 배포 충돌 방지동일 브랜치에서 중복 실행 시 레이스가 발생할 수 있습니다.
concurrency추가 권장.적용 제안(diff):
jobs: + concurrency: + group: deploy-develop + cancel-in-progress: true deploy:app/services/crawler.py (4)
66-75: 매직 넘버 상수화 및 경계값 검증
450,25,30, 시작 시각9등이 하드코딩되어 유지보수/이식성이 낮습니다. 음수(이른 top) 처리도 필요.적용 제안(diff):
- start_total_minutes = ((top - 450) // 25) * 30 - start_hour = 9 + (start_total_minutes // 60) + PIXELS_PER_30M = 25 + START_OFFSET_PX = 450 + START_HOUR_BASE = 9 + start_total_minutes = max(0, ((top - START_OFFSET_PX) // PIXELS_PER_30M) * 30) + start_hour = START_HOUR_BASE + (start_total_minutes // 60) @@ - duration_total_minutes = math.ceil((height - 1) / 25) * 30 + duration_total_minutes = math.ceil((height - 1) / PIXELS_PER_30M) * 30 - end_hour = 9 + (end_total_minutes // 60) + end_hour = START_HOUR_BASE + (end_total_minutes // 60)
122-127: 원래 traceback 유지:raise e대신 맨몸raise
raise e는 traceback을 재설정합니다. 린트(TRY201) 지적과 동일.적용 제안(diff):
- raise e + raise
129-132: 드라이버 반납 로깅 안전성 및 정리 보강
driver.session_id접근이 실패할 수 있습니다(세션 종료/크래시). 반납 전 가벼운 헬스체크/쿠키 정리도 권장.적용 제안(diff):
- logger.info(f"Driver 반납: {driver.session_id}") - pool.release_driver(driver) + sid = getattr(driver, "session_id", "unknown") + logger.info(f"Driver 반납: {sid}") + try: + driver.delete_all_cookies() + except Exception: + pass + pool.release_driver(driver)
1-20: 불필요한 import 정리 및 페이지 로드 타임아웃 고려
webdriver,Options,Service는 이 파일에서 미사용입니다. 또한driver.get()의 무한 대기를 방지하려면 생성 시page_load_timeout을 설정하는 편이 안전합니다(풀 생성부에서 처리 추천).원하시면
driver_pool._create_driver()에driver.set_page_load_timeout(15)또는page_load_strategy="none"적용 패치를 함께 제안하겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
.github/PULL_REQUEST_TEMPLATE.md(1 hunks).github/workflows/crawler-cicd.yaml(1 hunks).gitignore(1 hunks)Dockerfile(1 hunks)README.md(1 hunks)app.py(0 hunks)app/api/v1/schedule.py(1 hunks)app/core/exception_handlers.py(1 hunks)app/core/logging_config.py(1 hunks)app/core/security.py(1 hunks)app/main.py(1 hunks)app/services/crawler.py(3 hunks)app/services/driver_pool.py(1 hunks)app/utils/discord_logger.py(1 hunks)requirements.txt(1 hunks)utils/discord_logger.py(0 hunks)
💤 Files with no reviewable changes (2)
- utils/discord_logger.py
- app.py
🧰 Additional context used
🧬 Code graph analysis (3)
app/api/v1/schedule.py (2)
app/services/crawler.py (1)
crawl_schedule(21-131)app/core/security.py (1)
get_api_key(11-34)
app/services/crawler.py (2)
app/services/driver_pool.py (3)
WebDriverPool(5-36)get_driver(24-26)release_driver(28-30)app/utils/discord_logger.py (1)
send_error_to_discord(15-63)
app/main.py (3)
app/core/exception_handlers.py (2)
http_exception_handler(7-25)validation_exception_handler(28-44)app/core/logging_config.py (1)
setup_logging(26-28)app/services/driver_pool.py (2)
WebDriverPool(5-36)close_all(32-36)
🪛 Ruff (0.13.1)
app/core/exception_handlers.py
7-7: Unused function argument: request
(ARG001)
28-28: Unused function argument: request
(ARG001)
app/api/v1/schedule.py
35-35: Abstract raise to an inner function
(TRY301)
37-37: Consider moving this statement to an else block
(TRY300)
41-41: Use raise without specifying exception name
Remove exception name
(TRY201)
42-42: Do not catch blind exception: Exception
(BLE001)
44-45: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
app/utils/discord_logger.py
58-58: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
60-60: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
app/services/crawler.py
126-126: Use raise without specifying exception name
Remove exception name
(TRY201)
🔇 Additional comments (2)
.gitignore (1)
7-7: .gitignore JetBrains IDE 무시 패턴 수정 (*.idea→.idea/,*.iml추가`)
*.idea를 폴더 무시용.idea/로 변경- 모듈 파일 무시용
*.iml추가
현재 커밋된.idea또는.iml파일이 없으므로 추가 제거 작업은 불필요합니다.app/main.py (1)
83-83: Mangum lifespan 처리 확인 필요 (app/main.py:83)requirements.txt에
mangum가 있으나 버전이 고정되어 있지 않아(로컬/sandbox엔 설치돼 있지 않음) 실제 설치된 버전을 확인할 수 없습니다.최신 Mangum은 문자열 파라미터
lifespan("auto" | "on" | "off")를 사용하며 기본값은 "auto"입니다. (mangum.fastapiexpert.com)
과거 버전은enable_lifespan(bool)을 사용했고 해당 옵션은 제거(0.11.0)되어 버전 차이로 에러가 발생할 수 있습니다. (newreleases.io)권장 조치: requirements.txt에 Mangum 버전을 명시(pin)하거나, 최신 API 사용 시 명시적으로 handler = Mangum(app, lifespan="auto")로 변경하십시오. 레거시 호환성이 필요하면 버전 고정 또는
try/except로lifespan→enable_lifespan폴백을 구현하세요.
| name: Deploy Onetime Crawler to AWS Lambda | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [ "develop" ] | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR 이벤트에서 실배포 수행은 위험합니다
pull_request 트리거로 Lambda를 업데이트하면 미승인 변경이 배포될 수 있고, 포크 PR은 시크릿 접근도 불가하여 실패합니다.
적용 제안(diff):
-on:
- pull_request:
- branches: [ "develop" ]
+on:
+ push:
+ branches: [ "develop" ]
+ workflow_dispatch:또는 if: github.event_name == 'push'로 job 레벨 가드 추가.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| name: Deploy Onetime Crawler to AWS Lambda | |
| on: | |
| pull_request: | |
| branches: [ "develop" ] | |
| name: Deploy Onetime Crawler to AWS Lambda | |
| on: | |
| push: | |
| branches: [ "develop" ] | |
| workflow_dispatch: |
🤖 Prompt for AI Agents
.github/workflows/crawler-cicd.yaml lines 1-6: the workflow currently triggers
on pull_request which can unintentionally deploy unapproved changes and fails
for forked PRs due to secret access; change the workflow trigger to a safer
event (e.g., use on: push with branches: [ "develop" ] or keep pull_request for
checks only) and/or add a job-level guard so deploy jobs run only on pushes (add
if: github.event_name == 'push' or a condition checking github.ref ==
'refs/heads/develop'), ensuring Lambda-update jobs never run on pull_request
events from forks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
궁금한 게 그러면 무조건 머지를 해야지만 배포 후 테스트를 할 수 있는 거 아니야?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bbbang105 좋은 지적이에요! 머지 전에 테스트할 수 있는 몇 가지 방법이 있습니다:
권장 방법들:
1. Staging 환경 분리
on:
pull_request:
branches: [ "develop" ]
push:
branches: [ "develop" ]
jobs:
deploy-staging:
if: github.event_name == 'pull_request'
# staging Lambda 함수로 배포
deploy-production:
if: github.event_name == 'push'
# production Lambda 함수로 배포2. 수동 배포 트리거
on:
pull_request:
branches: [ "develop" ]
workflow_dispatch: # 수동 실행 버튼
inputs:
environment:
description: 'Deploy environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production3. Preview 배포
- PR마다 임시 Lambda 함수 생성 후 테스트 완료시 삭제
- AWS Lambda Alias/Version 기능 활용
4. 로컬 테스트 강화
# Docker로 Lambda 환경 시뮬레이션
docker run --platform linux/amd64 -p 9000:8080 your-image
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'가장 실용적인 해결책은 staging 환경을 만드는 것입니다. PR에서는 staging으로, merge 후에는 production으로 배포하면 안전하게 테스트할 수 있어요.
🤔 어떤 방식이 가장 적합할 것 같으신가요?
.github/workflows/crawler-cicd.yaml
Outdated
| permissions: | ||
| id-token: write | ||
| contents: read |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OIDC 권한 선언과 정적 키 혼용
permissions.id-token: write를 선언했지만 정적 액세스 키를 사용 중입니다. OIDC 역할 연동으로 전환 권장.
적용 제안(diff):
- - name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: ${{ secrets.AWS_REGION }}
+ - name: Configure AWS Credentials (OIDC)
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ aws-region: ${{ secrets.AWS_REGION }}AWS IAM에 GitHub OIDC 신뢰 정책/role 구성 필요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
.github/workflows/crawler-cicd.yaml lines 7-9: 현재 워크플로우에 permissions.id-token:
write를 선언해 놓고도 정적 AWS 액세스 키(Secrets)를 병행 사용하고 있어 혼용 상태입니다; AWS OIDC로 전환하려면 정적 키를
제거하고 GitHub OIDC 신뢰 정책 및 역할(Role)을 AWS IAM에 설정한 뒤 워크플로우에서 OIDC를 통해 임시 자격증명을
획득하도록 수정하세요; 구체적으로는(1) AWS에 GitHub Actions용 신뢰 정책과 역할을 생성하고 필요한 최소 권한을 부여하고, (2)
워크플로우에서 aws-actions/configure-aws-credentials 같은 액션을 사용해 role-to-assume 설정으로
OIDC를 통한 인증을 하며, (3) 기존 정적 AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY 시크릿을 제거하거나 더
이상 사용하지 않도록 업데이트하세요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정적으로 갈거라서 이 부분은 제거할게
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bbbang105 알겠습니다! 정적 자격증명 방식으로 가시는군요. permissions 섹션을 제거하시면 일관성 있게 구성되겠습니다.
🐰✨
(\ /)
( ..)
o(")(")
thoughtful
✏️ Learnings added
Learnt from: bbbang105
PR: onetime-with-members/crawling-server#16
File: .github/workflows/crawler-cicd.yaml:7-9
Timestamp: 2025-09-21T20:00:59.927Z
Learning: User bbbang105 prefers using static AWS credentials over OIDC for GitHub Actions workflows in the crawling-server project.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| pool = request.app.state.driver_pool | ||
|
|
||
| # 서비스 계층의 크롤링 함수 호출 | ||
| result = await asyncio.to_thread(crawl_schedule, url, request, pool) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
driver_pool 존재 보장 필요(수명주기 미호출/초기화 실패 대비)
Lambda/Mangum 환경에서 lifespan 미호출 시 app.state.driver_pool 부재 가능. 안전가드 추가하세요.
적용 예시(diff):
- pool = request.app.state.driver_pool
+ pool = getattr(request.app.state, "driver_pool", None)
+ if pool is None:
+ # 지연 초기화(최소 1개). 대안: 503 반환.
+ from app.services.driver_pool import WebDriverPool
+ pool = request.app.state.driver_pool = WebDriverPool(size=1)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pool = request.app.state.driver_pool | |
| # 서비스 계층의 크롤링 함수 호출 | |
| result = await asyncio.to_thread(crawl_schedule, url, request, pool) | |
| pool = getattr(request.app.state, "driver_pool", None) | |
| if pool is None: | |
| # 지연 초기화(최소 1개). 대안: 503 반환. | |
| from app.services.driver_pool import WebDriverPool | |
| pool = request.app.state.driver_pool = WebDriverPool(size=1) | |
| # 서비스 계층의 크롤링 함수 호출 | |
| result = await asyncio.to_thread(crawl_schedule, url, request, pool) |
🤖 Prompt for AI Agents
In app/api/v1/schedule.py around lines 29 to 33, there is no guarantee that
request.app.state.driver_pool exists (lifespan may not have run in
Lambda/Mangum), so add a safety check before using it: verify driver_pool is
present and valid, and if missing either return/raise an HTTP 503/500 with a
clear message or lazily initialize a new pool (ensuring async-safety) depending
on your app policy; log the missing pool for diagnostics and avoid calling
crawl_schedule with a None pool.
| async def http_exception_handler(request: Request, exc: StarletteHTTPException): | ||
| """HTTPException을 공통 포맷으로 변환합니다.""" | ||
|
|
||
| # 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다. | ||
| if isinstance(exc.detail, dict): | ||
| content = {**exc.detail, "payload": None} | ||
| else: | ||
| # 아닐 경우, 표준 에러 포맷을 생성합니다. | ||
| content = { | ||
| "code": f"HTTP_{exc.status_code}", | ||
| "message": exc.detail, | ||
| "is_success": False, | ||
| "payload": None, | ||
| } | ||
|
|
||
| return JSONResponse( | ||
| status_code=exc.status_code, | ||
| content=content, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP 예외 응답 포맷 불일치 및 인증 헤더 손실 위험
exc.detail이 dict일 때is_success/code가 누락될 수 있어 응답 스키마가 흔들립니다.WWW-Authenticate등exc.headers가 빠져 401/429 등에서 클라이언트 호환성 문제가 납니다.
적용 제안(diff):
-async def http_exception_handler(request: Request, exc: StarletteHTTPException):
+async def http_exception_handler(_request: Request, exc: StarletteHTTPException):
"""HTTPException을 공통 포맷으로 변환합니다."""
-
- # 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다.
- if isinstance(exc.detail, dict):
- content = {**exc.detail, "payload": None}
- else:
- # 아닐 경우, 표준 에러 포맷을 생성합니다.
- content = {
- "code": f"HTTP_{exc.status_code}",
- "message": exc.detail,
- "is_success": False,
- "payload": None,
- }
+ # 표준 에러 포맷을 기본으로, dict detail이면 overlay
+ detail = exc.detail
+ base = {
+ "code": f"HTTP_{exc.status_code}",
+ "message": str(detail) if not isinstance(detail, dict) else str(detail.get("message", "")) or f"HTTP_{exc.status_code}",
+ "is_success": False,
+ "payload": None,
+ }
+ content = {**base, **detail} if isinstance(detail, dict) else base
- return JSONResponse(
- status_code=exc.status_code,
- content=content,
- )
+ return JSONResponse(
+ status_code=exc.status_code,
+ content=content,
+ headers=getattr(exc, "headers", None) or {}
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def http_exception_handler(request: Request, exc: StarletteHTTPException): | |
| """HTTPException을 공통 포맷으로 변환합니다.""" | |
| # 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다. | |
| if isinstance(exc.detail, dict): | |
| content = {**exc.detail, "payload": None} | |
| else: | |
| # 아닐 경우, 표준 에러 포맷을 생성합니다. | |
| content = { | |
| "code": f"HTTP_{exc.status_code}", | |
| "message": exc.detail, | |
| "is_success": False, | |
| "payload": None, | |
| } | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| content=content, | |
| ) | |
| async def http_exception_handler(_request: Request, exc: StarletteHTTPException): | |
| """HTTPException을 공통 포맷으로 변환합니다.""" | |
| # 표준 에러 포맷을 기본으로, dict detail이면 overlay | |
| detail = exc.detail | |
| base = { | |
| "code": f"HTTP_{exc.status_code}", | |
| "message": str(detail) if not isinstance(detail, dict) else str(detail.get("message", "")) or f"HTTP_{exc.status_code}", | |
| "is_success": False, | |
| "payload": None, | |
| } | |
| content = {**base, **detail} if isinstance(detail, dict) else base | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| content=content, | |
| headers=getattr(exc, "headers", None) or {}, | |
| ) |
🧰 Tools
🪛 Ruff (0.13.1)
7-7: Unused function argument: request
(ARG001)
🤖 Prompt for AI Agents
In app/core/exception_handlers.py around lines 7 to 25, the handler currently
returns inconsistent response shapes when exc.detail is a dict and drops
exception headers (e.g., WWW-Authenticate), so change the logic to normalize and
merge defaults into the detail dict (ensure keys: "code" defaulting to
f"HTTP_{exc.status_code}", "message" using exc.detail.get("message") or
str(exc.detail), "is_success": False, and "payload": None) without deleting
existing fields, and pass exc.headers (if any) into the JSONResponse via the
headers parameter so authentication/rate-limit headers are preserved.
| API_KEY = os.getenv("API_KEY") | ||
| API_KEY_NAME = "X-API-Key" | ||
|
|
||
| api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) | ||
|
|
||
| async def get_api_key(api_key_header: str = Security(api_key_header)): | ||
| """헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API_KEY 미설정 시 TypeError 가능성 → 500으로 튀는 버그
secrets.compare_digest(api_key_header, API_KEY)에서 API_KEY가 None이면 TypeError가 발생합니다. 미설정 시 명시적으로 500/설정오류를 반환하거나 안전 실패하도록 처리하세요.
적용 예시(diff):
-import os
+import os
+import logging
@@
API_KEY = os.getenv("API_KEY")
@@
async def get_api_key(api_key_header: str = Security(api_key_header)):
@@
- if secrets.compare_digest(api_key_header, API_KEY):
+ if API_KEY is None:
+ logging.getLogger(__name__).critical("API_KEY 환경변수가 설정되어 있지 않습니다.")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail={"code": "AUTH-000", "message": "서버 보안 설정 오류(API_KEY 미설정).", "is_success": False},
+ )
+
+ if secrets.compare_digest(api_key_header, API_KEY):
return api_key_header📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| API_KEY = os.getenv("API_KEY") | |
| API_KEY_NAME = "X-API-Key" | |
| api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) | |
| async def get_api_key(api_key_header: str = Security(api_key_header)): | |
| """헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수""" | |
| import os | |
| import logging | |
| API_KEY = os.getenv("API_KEY") | |
| API_KEY_NAME = "X-API-Key" | |
| api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) | |
| async def get_api_key(api_key_header: str = Security(api_key_header)): | |
| """헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수""" | |
| if API_KEY is None: | |
| logging.getLogger(__name__).critical("API_KEY 환경변수가 설정되어 있지 않습니다.") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail={"code": "AUTH-000", "message": "서버 보안 설정 오류(API_KEY 미설정).", "is_success": False}, | |
| ) | |
| if secrets.compare_digest(api_key_header, API_KEY): | |
| return api_key_header |
🤖 Prompt for AI Agents
In app/core/security.py around lines 6 to 12, the dependency uses
secrets.compare_digest(api_key_header, API_KEY) but API_KEY can be None which
causes a TypeError; update get_api_key to first check if API_KEY is None and, if
so, raise an HTTPException with a 500 status (e.g., "API key not configured") or
otherwise fail-safe by rejecting the request; only call secrets.compare_digest
when API_KEY is a non-None string (and ensure api_key_header is a string) and
return/raise a 401 Unauthorized when the comparison fails.
| @asynccontextmanager | ||
| async def lifespan(app: FastAPI): | ||
| # 앱 시작 시 실행 | ||
| logger.info("🚀 WebDriver Pool을 생성합니다...") | ||
| app.state.driver_pool = WebDriverPool(size=5) | ||
| yield | ||
| # 앱 종료 시 실행 | ||
| logger.info("💀 WebDriver Pool을 종료합니다...") | ||
| app.state.driver_pool.close_all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lambda에서 WebDriverPool 사전 생성은 콜드스타트/실행 실패 유발(브라우저 미존재 문제와 결합)
- Lambda 1-동시성 인스턴스 특성상 풀(5개) 사전 생성은 비효율/메모리 증가.
- 현재 이미지엔 Chromium이 없어 초기화 단계에서 즉시 실패 가능.
풀 크기 축소 및 지연 초기화/환경기반 토글이 필요합니다.
적용 예시(diff):
+import os
@@
async def lifespan(app: FastAPI):
- logger.info("🚀 WebDriver Pool을 생성합니다...")
- app.state.driver_pool = WebDriverPool(size=5)
+ pool_size = int(os.getenv("POOL_SIZE", "0")) # 기본 지연초기화
+ if pool_size > 0:
+ logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})")
+ app.state.driver_pool = WebDriverPool(size=pool_size)
+ else:
+ logger.info("🚀 WebDriver Pool 지연초기화 모드")
+ app.state.driver_pool = None
yield
# 앱 종료 시 실행
- logger.info("💀 WebDriver Pool을 종료합니다...")
- app.state.driver_pool.close_all()
+ if getattr(app.state, "driver_pool", None):
+ logger.info("💀 WebDriver Pool을 종료합니다...")
+ app.state.driver_pool.close_all()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @asynccontextmanager | |
| async def lifespan(app: FastAPI): | |
| # 앱 시작 시 실행 | |
| logger.info("🚀 WebDriver Pool을 생성합니다...") | |
| app.state.driver_pool = WebDriverPool(size=5) | |
| yield | |
| # 앱 종료 시 실행 | |
| logger.info("💀 WebDriver Pool을 종료합니다...") | |
| app.state.driver_pool.close_all() | |
| import os | |
| @asynccontextmanager | |
| async def lifespan(app: FastAPI): | |
| # 앱 시작 시 실행 | |
| pool_size = int(os.getenv("POOL_SIZE", "0")) # 기본 지연초기화 | |
| if pool_size > 0: | |
| logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})") | |
| app.state.driver_pool = WebDriverPool(size=pool_size) | |
| else: | |
| logger.info("🚀 WebDriver Pool 지연초기화 모드") | |
| app.state.driver_pool = None | |
| yield | |
| # 앱 종료 시 실행 | |
| if getattr(app.state, "driver_pool", None): | |
| logger.info("💀 WebDriver Pool을 종료합니다...") | |
| app.state.driver_pool.close_all() |
🤖 Prompt for AI Agents
In app/main.py around lines 23 to 31, the lifespan currently eagerly constructs
a WebDriverPool of size 5 at app startup which can cause cold-start failures and
increased memory usage in AWS Lambda (and will fail if Chromium is not present);
change this to create a much smaller default pool (e.g., 1) or defer creation
entirely by storing a lazy factory on app.state and instantiating the pool on
first use, add an environment-based toggle (e.g., DISABLE_DRIVER_POOL or
DRIVER_POOL_LAZY=true) to skip pre-creation in Lambda, and ensure the shutdown
path still closes the pool if it exists and handles absence of a browser by
catching initialization errors and logging them without crashing the app.
app/services/driver_pool.py
Outdated
| def __init__(self, size=5): | ||
| self._pool = queue.Queue(maxsize=size) | ||
| for _ in range(size): | ||
| driver = self._create_driver() | ||
| self._pool.put(driver) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lambda에서의 초기 드라이버 일괄 생성은 메모리/콜드스타트 리스크가 큽니다
Lambda 컨테이너에서 Chrome 5개 선생성은 OOM/콜드스타트 지연을 유발할 수 있습니다. 실패 시 앱 초기화 전체가 터집니다.
게으른 생성+비동기 대여로 전환 권장(diff):
class WebDriverPool:
- def __init__(self, size=5):
- self._pool = queue.Queue(maxsize=size)
- for _ in range(size):
- driver = self._create_driver()
- self._pool.put(driver)
+ def __init__(self, size=2):
+ # Lambda 기본 메모리(예: 512~1024MB) 고려해 기본값 축소
+ self._pool = queue.Queue(maxsize=size)이후 get_driver/release_driver를 lazy/논블로킹으로 조정(아래 코멘트 참고).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def __init__(self, size=5): | |
| self._pool = queue.Queue(maxsize=size) | |
| for _ in range(size): | |
| driver = self._create_driver() | |
| self._pool.put(driver) | |
| def __init__(self, size=2): | |
| # Lambda 기본 메모리(예: 512~1024MB) 고려해 기본값 축소 | |
| self._pool = queue.Queue(maxsize=size) |
| fastapi | ||
| uvicorn[standard] | ||
| python-multipart | ||
| mangum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 재현성과 보안 위해 버전 고정 및 런타임/개발 분리 필요
현재 전부 미핀(최신 부수 변경 위험). Lambda 컨테이너는 재현성 중요합니다. 운영 필수 deps만 고정하고, 로컬 전용(uvicorn 등)은 dev로 분리해 주세요.
적용 예시(diff):
-selenium
-psutil
-python-dotenv
-requests
-fastapi
-uvicorn[standard]
-python-multipart
-mangum
+selenium>=4.21,<5.0
+psutil>=5.9,<6.0
+python-dotenv>=1.0,<2.0
+requests>=2.32,<3.0
+fastapi>=0.110,<1.0
+python-multipart>=0.0.9,<1.0
+mangum>=0.17,<1.0
+# 로컬 개발 전용은 별도 requirements-dev.txt로 분리:
+# uvicorn[standard]>=0.30,<1.0또는 constraints/lock 파일(pip-tools) 사용을 권장합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fastapi | |
| uvicorn[standard] | |
| python-multipart | |
| mangum | |
| fastapi>=0.110,<1.0 | |
| # 로컬 개발 전용은 별도 requirements-dev.txt로 분리: | |
| # uvicorn[standard]>=0.30,<1.0 | |
| python-multipart>=0.0.9,<1.0 | |
| mangum>=0.17,<1.0 |
🤖 Prompt for AI Agents
requirements.txt lines 5-8: currently all deps are unpinned and include runtime
and local-only packages; pin production runtime packages and split dev/runtime
deps. Update by locking exact versions for production-required packages (e.g.,
fastapi==X.Y.Z, mangum==A.B.C, python-multipart==M.N.O if used at runtime) in
requirements.txt, move local/dev-only packages such as uvicorn[standard] into a
requirements-dev.txt (also pinned), and optionally adopt a constraints or
lockfile workflow (pip-tools/requirements.txt + requirements-dev.txt generated
from requirements.in) to ensure reproducible Lambda containers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (4)
app/services/driver_pool.py (4)
39-43: close_all는 race에 취약하고 블로킹 가능 → get_nowait 루프와 예외 방어
empty()체크 후get()은 경합 시 블로킹될 수 있습니다. 논블로킹으로 소거하고 종료 예외는 무시하세요.def close_all(self): """애플리케이션 종료 시 모든 WebDriver를 종료합니다.""" - while not self._pool.empty(): - driver = self._pool.get() - driver.quit() + while True: + try: + driver = self._pool.get_nowait() + except queue.Empty: + break + try: + driver.quit() + except Exception: + pass
1-1: 락/ENV 사용을 위한 import 보강위 제안(diff) 적용 시 필요합니다.
-import queue +import os +import queue +import threading
5-11: 운영 가이드: 풀 메트릭/로깅/백오프
- 생성/종료/반납/풀고갈 이벤트에 카운터·게이지(예: Prometheus)와 구조화 로그 추가.
_create_driver실패 시 지수 백오프+재시도(최대 N회) 및 Discord/Slack 경보 연동.
31-37: 사용성 개선: 컨텍스트 매니저 제공
with pool.borrow() as driver:형태로 누수·이중반납 방지에 도움됩니다.아래 보조 API를 동일 파일 하단에 추가 제안(참고용):
from contextlib import contextmanager @contextmanager def borrow(self, timeout: float = 10.0): driver = self.get_driver(timeout=timeout) try: yield driver finally: self.release_driver(driver)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/services/driver_pool.py(1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py
23-23: f-string without any placeholders
Remove extraneous f prefix
(F541)
24-24: f-string without any placeholders
Remove extraneous f prefix
(F541)
25-25: f-string without any placeholders
Remove extraneous f prefix
(F541)
🔇 Additional comments (2)
app/services/driver_pool.py (2)
6-10: Lambda에서 드라이버 ‘선생성’은 콜드스타트·메모리 리스크 큼 → 게으른 생성으로 전환 권장컨테이너 기동 시 Chrome를 size개 동시 생성하면 콜드스타트 지연·OOM·초기화 전체 실패 전파 위험이 큽니다. 요청 시 부족분만 생성하고, 최대 개수만 추적하세요.
아래처럼 초기 선생성을 제거하고, 최대치/생성수/락을 두세요:
-def __init__(self, size=5): - self._pool = queue.Queue(maxsize=size) - for _ in range(size): - driver = self._create_driver() - self._pool.put(driver) +def __init__(self, size: int = int(os.getenv("WEBDRIVER_POOL_SIZE", "2"))): + self._pool = queue.Queue(maxsize=size) + self._size = size + self._created = 0 + self._lock = threading.Lock()(상단 import 추가는 별도 코멘트 참조)
12-29: 드라이버 배포 방식 확인 필요 — 현재 Selenium Manager 의존 가능성, 번들링 권장
- 검증 결과: Dockerfile에서 Chromium/ChromeDriver 번들 관련 참조 없음.
- 코드: app/services/driver_pool.py (약 라인 28–29)에서 _create_driver가 webdriver.Chrome(options=...)만 호출하여 실행 바이너리/드라이버 경로(ENV)를 사용하지 않음 → 런타임에 Selenium Manager가 드라이버를 다운로드할 가능성 높음.
- 반납 보장: app/services/crawler.py (라인 128–131)에서 finally로 드라이버 반납이 안전하게 처리되고 있음.
- 권장 조치: Lambda 이미지에 Chromium + ChromeDriver를 고정 번들링하고 CHROME_BINARY_PATH / CHROMEDRIVER_PATH 환경변수로 경로를 주입하세요. 드라이버 생성 시 options.binary_location 또는 selenium.webdriver.chrome.service.Service(executable_path=...)로 바이너리/드라이버 경로를 명시해 Selenium Manager 다운로드(콜드스타트)를 회피하세요. 변경 후 Dockerfile·ENV·driver_pool 코드 재검증 필요.
app/services/driver_pool.py
Outdated
| def _create_driver(self): | ||
| """새로운 WebDriver 인스턴스를 생성합니다.""" | ||
| chrome_options = Options() | ||
| chrome_options.add_argument("--headless=new") | ||
| chrome_options.add_argument("--no-sandbox") | ||
| chrome_options.add_argument("--disable-gpu") | ||
| chrome_options.add_argument("--window-size=1280x1696") | ||
| chrome_options.add_argument("--single-process") | ||
| chrome_options.add_argument("--disable-dev-shm-usage") | ||
| chrome_options.add_argument("--disable-dev-tools") | ||
| chrome_options.add_argument("--no-zygote") | ||
| chrome_options.add_argument(f"--user-data-dir=/tmp/user-data") | ||
| chrome_options.add_argument(f"--data-path=/tmp/data-path") | ||
| chrome_options.add_argument(f"--disk-cache-dir=/tmp/cache-dir") | ||
| chrome_options.add_argument("--remote-debugging-port=9222") | ||
| chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") | ||
|
|
||
| return webdriver.Chrome(options=chrome_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chrome 플래그/바이너리 설정 개선: --single-process 제거, 환경변수 기반 경로/UA, F541 정리
--single-process는 크래시·불안정 원인으로 알려져 제거 권장.- 바이너리/드라이버 경로는 Lambda 이미지/레이어에 포함 후 ENV로 주입 권장.
- 하드코딩 UA는 유지보수 부담. ENV로 옵셔널 처리.
- f-string 남용(F541)도 함께 정리.
- def _create_driver(self):
- """새로운 WebDriver 인스턴스를 생성합니다."""
- chrome_options = Options()
- chrome_options.add_argument("--headless=new")
- chrome_options.add_argument("--no-sandbox")
- chrome_options.add_argument("--disable-gpu")
- chrome_options.add_argument("--window-size=1280x1696")
- chrome_options.add_argument("--single-process")
- chrome_options.add_argument("--disable-dev-shm-usage")
- chrome_options.add_argument("--disable-dev-tools")
- chrome_options.add_argument("--no-zygote")
- chrome_options.add_argument(f"--user-data-dir=/tmp/user-data")
- chrome_options.add_argument(f"--data-path=/tmp/data-path")
- chrome_options.add_argument(f"--disk-cache-dir=/tmp/cache-dir")
- chrome_options.add_argument("--remote-debugging-port=9222")
- chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
-
- return webdriver.Chrome(options=chrome_options)
+ def _create_driver(self):
+ """새로운 WebDriver 인스턴스를 생성합니다."""
+ chrome_options = Options()
+ chrome_options.add_argument("--headless=new")
+ chrome_options.add_argument("--no-sandbox")
+ chrome_options.add_argument("--disable-setuid-sandbox")
+ chrome_options.add_argument("--disable-gpu")
+ chrome_options.add_argument("--disable-extensions")
+ chrome_options.add_argument("--disable-dev-shm-usage")
+ chrome_options.add_argument("--no-zygote")
+ chrome_options.add_argument("--window-size=1280x1696")
+ chrome_options.add_argument("--remote-debugging-port=9222")
+ chrome_options.add_argument("--user-data-dir=/tmp/user-data")
+ chrome_options.add_argument("--data-path=/tmp/data-path")
+ chrome_options.add_argument("--disk-cache-dir=/tmp/cache-dir")
+ ua = os.getenv("CHROME_USER_AGENT")
+ if ua:
+ chrome_options.add_argument(f"user-agent={ua}")
+
+ binary = os.getenv("CHROME_BINARY_PATH")
+ if binary:
+ chrome_options.binary_location = binary
+ driver_path = os.getenv("CHROMEDRIVER_PATH")
+ if driver_path:
+ from selenium.webdriver.chrome.service import Service
+ service = Service(executable_path=driver_path)
+ return webdriver.Chrome(options=chrome_options, service=service)
+ return webdriver.Chrome(options=chrome_options)Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Ruff (0.13.1)
23-23: f-string without any placeholders
Remove extraneous f prefix
(F541)
24-24: f-string without any placeholders
Remove extraneous f prefix
(F541)
25-25: f-string without any placeholders
Remove extraneous f prefix
(F541)
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 12 to 29, remove the unstable
"--single-process" flag, stop hardcoding binary/driver paths and
user-data/data/cache dirs and user-agent; instead read Chrome binary path,
chromedriver path, user-data-dir, data-path, disk-cache-dir and optional
USER_AGENT from environment variables and pass them to Options/binary_location
and add_argument only when set, and replace unnecessary f-strings used with
constant strings to plain literals to satisfy F541; ensure any missing env vars
fall back to sensible defaults or are omitted.
| def get_driver(self): | ||
| """Pool에서 WebDriver를 가져옵니다.""" | ||
| return self._pool.get() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
풀 고갈 시 무기한 대기 위험 → timeout 부여 + 부족분 즉시 생성
현재 get()은 끝없이 블로킹합니다. 요청 스레드가 고립되지 않도록 timeout과 lazy-create를 적용하세요.
-def get_driver(self):
- """Pool에서 WebDriver를 가져옵니다."""
- return self._pool.get()
+def get_driver(self, timeout: float = 10.0):
+ """Pool에서 WebDriver를 가져오되, 비어있으면 부족분을 생성하고 마지막엔 timeout으로 대기합니다."""
+ try:
+ return self._pool.get_nowait()
+ except queue.Empty:
+ pass
+ with self._lock:
+ if self._created < self._size:
+ driver = self._create_driver()
+ self._created += 1
+ return driver
+ return self._pool.get(timeout=timeout)Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 31-34, get_driver currently blocks
forever on self._pool.get(); change it to call
self._pool.get(timeout=SHORT_TIMEOUT) and catch queue.Empty, then under a lock
check if current total drivers < max_allowed and if so lazily create a new
driver (e.g. self._create_driver()), increment the internal counter and return
it; if at max, raise a clear TimeoutError or custom exception to the caller.
Ensure the creation path is thread-safe (use the same lock used for counters)
and choose a sensible SHORT_TIMEOUT value (seconds) configurable from the pool
settings.
| def release_driver(self, driver): | ||
| """사용한 WebDriver를 Pool에 반납합니다.""" | ||
| self._pool.put(driver) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
release 시 큐가 가득 차면 영구 블로킹 가능 → nowait/timeout + 초과분/불량 드라이버 폐기
이중 반납 등 실수 시 put()이 무기한 대기할 수 있습니다. 초과분은 종료하고, 카운터를 줄여 다음 요청에서 재생성되게 하세요.
-def release_driver(self, driver):
- """사용한 WebDriver를 Pool에 반납합니다."""
- self._pool.put(driver)
+def release_driver(self, driver):
+ """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 드라이버를 종료해 데드락/누수를 방지합니다."""
+ # 간단한 생존 체크(세션이 없으면 버림)
+ if getattr(driver, "session_id", None) is None:
+ try:
+ driver.quit()
+ finally:
+ with self._lock:
+ self._created = max(0, self._created - 1)
+ return
+ try:
+ self._pool.put(driver, timeout=1)
+ except queue.Full:
+ try:
+ driver.quit()
+ finally:
+ with self._lock:
+ self._created = max(0, self._created - 1)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def release_driver(self, driver): | |
| """사용한 WebDriver를 Pool에 반납합니다.""" | |
| self._pool.put(driver) | |
| def release_driver(self, driver): | |
| """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 드라이버를 종료해 데드락/누수를 방지합니다.""" | |
| # 간단한 생존 체크(세션이 없으면 버림) | |
| if getattr(driver, "session_id", None) is None: | |
| try: | |
| driver.quit() | |
| finally: | |
| with self._lock: | |
| self._created = max(0, self._created - 1) | |
| return | |
| try: | |
| self._pool.put(driver, timeout=1) | |
| except queue.Full: | |
| try: | |
| driver.quit() | |
| finally: | |
| with self._lock: | |
| self._created = max(0, self._created - 1) |
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 35 to 37, release_driver currently
calls self._pool.put(driver) which can block forever if the queue is full;
change it to use a non-blocking or timed put (e.g., put_nowait or put with
timeout) and handle queue.Full by disposing/closing the incoming driver and
updating the pool's active/available counter so the slot is considered free and
a new driver can be created on demand; also catch exceptions from driver
disposal and ensure the counter is decremented only once to avoid leaking state
on double-release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (6)
app/services/driver_pool.py (3)
1-6: 환경설정/동시성 대비: os·logging·threading import 및 로거 추가 권장ENV 기반 설정, 락, 로깅 적용을 위한 선행 준비가 필요합니다.
import queue +import os +import logging +import threading from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service + +logger = logging.getLogger(__name__)
53-58: close_all는 대여 중 드라이버를 놓칩니다 → 카운터 리셋 및 non-blocking drain
대여 중인 드라이버는 남고 카운터도 초기화되지 않습니다. 최소한 큐 drain 후 카운터를 리셋하세요.def close_all(self): """애플리케이션 종료 시 모든 WebDriver를 종료합니다.""" - while not self._pool.empty(): - driver = self._pool.get() - driver.quit() + try: + while True: + driver = self._pool.get_nowait() + try: + driver.quit() + except Exception: + logger.debug("driver.quit() 실패 무시", exc_info=True) + except queue.Empty: + pass + finally: + if hasattr(self, "_created"): + self._created = 0
40-43: broad exception + print 사용 금지(BLE001)
로깅으로 대체하고 예외를 좁히세요. 위 제안(diff)에서 함께 수정되었습니다.Dockerfile (3)
44-44: Chrome/Driver 경로 환경변수 추가 설정 권장PATH 추가만으로도 동작할 수 있으나, 드라이버가 바이너리를 확실히 찾도록 아래 환경변수도 설정하세요.
-ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}" +ENV CHROME_BIN=/opt/chrome-linux64/chrome +ENV CHROMEDRIVER=/opt/chromedriver-linux64/chromedriver +ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"
48-52: 레이어 캐시 최적화는 적절함 · 추가 제안(선택)
requirements.txt만 먼저 복사하는 순서는 좋습니다. 이미지 크기/재현성을 위해pip install후 pip 캐시 제거를 추가하는 소소한 최적화 제안을 남깁니다.-RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt && rm -rf ~/.cache/pip
6-41: 대안(선택): OS 패키지 사용 또는 레이어로 브라우저 분리
- 대안 1: AL2023 리포에
chromium/chromedriver가 제공된다면 dnf로 직접 설치하여 외부 다운로드를 제거(재현성↑, 속도↑). 버전 조합은 반드시 일치 검증 필요.- 대안 2: Lambda Layer에 브라우저/드라이버를 올리고 함수 설정에서 레이어로 주입(이미지 슬림화, 보안/업데이트 관리 용이).
운영 요구사항에 따라 선택하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Dockerfile(1 hunks)app/services/driver_pool.py(1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py
40-40: Do not catch blind exception: Exception
(BLE001)
🪛 GitHub Actions: Deploy Onetime Crawler to AWS Lambda
Dockerfile
[error] 6-6: Docker build failed during RUN yum update -y ...: 'yum' command not found in the base image.
🔇 Additional comments (6)
app/services/driver_pool.py (4)
8-14: Lambda에서 드라이버 선(先)생성(size=5) → 콜드스타트/메모리 리스크. Lazy 생성으로 전환하세요.
초기 일괄 생성은 OOM/콜드스타트 지연과 실패 시 전체 초기화 실패를 유발합니다. Lazy-create(+락)로 변경하세요. 과거 코멘트와 동일 맥락입니다.-class WebDriverPool: - def __init__(self, size=5): - self._pool = queue.Queue(maxsize=size) - for _ in range(size): - driver = self._create_driver() - if driver: - self._pool.put(driver) +class WebDriverPool: + def __init__(self, size: int = 2): + self._size = max(1, size) + self._pool = queue.Queue(maxsize=self._size) + self._lock = threading.Lock() + self._created = 0 # 현재 생성된 드라이버 수 (대여/반납과 무관)
15-44: 하드코딩 경로/UA 제거 및 예외 처리 개선(ENV 기반, 안전 플래그, 로깅)
경로/UA 하드코딩은 이식성과 운영 편의성을 저해합니다. broad exception+print도 지양하세요. 과거 코멘트와 동일 맥락입니다.def _create_driver(self): """ 새로운 WebDriver 인스턴스를 생성합니다. """ - try: - chrome_options = Options() - - # Chrome 바이너리 위치를 지정합니다. - chrome_options.binary_location = "/opt/chrome-linux64/chrome" - - # Headless 실행을 위한 표준 옵션들을 설정합니다. - chrome_options.add_argument("--headless=new") - chrome_options.add_argument("--no-sandbox") - chrome_options.add_argument("--disable-dev-shm-usage") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--window-size=1280x1696") - - # User-Agent 설정 - chrome_options.add_argument( - "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") - - # Chromedriver 경로를 지정하여 Service 객체를 생성합니다. - service = Service(executable_path="/opt/chromedriver-linux64/chromedriver") - - return webdriver.Chrome(service=service, options=chrome_options) - except Exception as e: - # 드라이버 생성 실패 시 로그를 남기고 None을 반환하여 Pool에 추가되지 않도록 함 - print(f"WebDriver 생성 실패: {e}") - return None + try: + chrome_options = Options() + # 안전 플래그 + chrome_options.add_argument("--headless=new") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-setuid-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--disable-extensions") + chrome_options.add_argument("--no-zygote") + chrome_options.add_argument("--window-size=1280x1696") + + # 바이너리/경로/UA/임시 디렉터리: ENV 우선, 없으면 합리적 기본값(/tmp) + binary = os.getenv("CHROME_BINARY_PATH") + if binary: + chrome_options.binary_location = binary + + ua = os.getenv("CHROME_USER_AGENT") + if ua: + chrome_options.add_argument(f"user-agent={ua}") + + user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data") + data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path") + cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir") + for d in (user_data_dir, data_path, cache_dir): + try: + os.makedirs(d, exist_ok=True) + except OSError: + pass + chrome_options.add_argument(f"--user-data-dir={user_data_dir}") + chrome_options.add_argument(f"--data-path={data_path}") + chrome_options.add_argument(f"--disk-cache-dir={cache_dir}") + + # chromedriver 경로: ENV 설정 시에만 지정 + driver_path = os.getenv("CHROMEDRIVER_PATH") + if driver_path: + service = Service(executable_path=driver_path) + return webdriver.Chrome(service=service, options=chrome_options) + return webdriver.Chrome(options=chrome_options) + except Exception: # noqa: BLE001 + # 구체 예외로 좁히는 것을 권장: selenium.common.exceptions.WebDriverException 등 + logger.exception("WebDriver 생성 실패") + return None
45-47: 무기한 블로킹 위험(get) → timeout + lazy-create
풀 고갈 시 요청 스레드가 영구 대기합니다. 과거 코멘트와 동일 맥락입니다.- def get_driver(self): - """Pool에서 WebDriver를 가져옵니다.""" - return self._pool.get() + def get_driver(self, timeout: float = 10.0): + """Pool에서 WebDriver를 가져오되, 부족하면 즉시 생성 후 반환하고, 끝에는 timeout으로 대기합니다.""" + try: + return self._pool.get_nowait() + except queue.Empty: + pass + with self._lock: + if self._created < self._size: + driver = self._create_driver() + if driver: + self._created += 1 + return driver + # 여기까지 왔다면 생성 불가 또는 이미 최대치 + try: + return self._pool.get(timeout=timeout) + except queue.Empty: + raise TimeoutError("WebDriver 풀에서 드라이버를 획득하지 못했습니다(timeout).")
49-51: release 시 큐 가득/불량 드라이버 처리 → nowait/timeout + 폐기
이중 반납/불량 세션에서 영구 대기/누수 위험. 과거 코멘트와 동일 맥락입니다.- def release_driver(self, driver): - """사용한 WebDriver를 Pool에 반납합니다.""" - self._pool.put(driver) + def release_driver(self, driver): + """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 종료하여 데드락/누수를 방지합니다.""" + # 간단한 생존 체크(세션이 없거나 크래시 시 폐기) + if getattr(driver, "session_id", None) is None: + try: + driver.quit() + finally: + with self._lock: + self._created = max(0, self._created - 1) + return + try: + self._pool.put(driver, timeout=1) + except queue.Full: + try: + driver.quit() + finally: + with self._lock: + self._created = max(0, self._created - 1)Dockerfile (2)
55-55: Lambda 핸들러 지정은 적합
CMD ["app.main.handler"]구성은 Lambda Python 베이스 이미지와 호환됩니다.app/main.py에handler심볼이 실제로 존재하는지만 한번 더 확인 부탁드립니다.
32-45: 과거 리뷰 대응 현황: 크롬/드라이버 번들링 시도는 좋으나 아직 불안정이전 지적 사항(브라우저/드라이버 미포함) 대비, 이번 커밋에서 번들링을 시도한 점은 긍정적입니다. 다만 AL2023 패키지/도구 호환성과 다운로드 출처/권한/무결성 이슈가 남아 있어 런타임 실패 가능성이 큽니다. 위 수정안 반영을 권고합니다.
Dockerfile
Outdated
| RUN yum update -y && \ | ||
| yum install -y \ | ||
| gzip \ | ||
| tar \ | ||
| unzip \ | ||
| alsa-lib \ | ||
| atk \ | ||
| cups-libs \ | ||
| gtk3 \ | ||
| ipa-gothic-fonts \ | ||
| libXcomposite \ | ||
| libXcursor \ | ||
| libXdamage \ | ||
| libXext \ | ||
| libXi \ | ||
| libXrandr \ | ||
| libXScrnSaver \ | ||
| libXtst \ | ||
| pango \ | ||
| xorg-x11-fonts-100dpi \ | ||
| xorg-x11-fonts-75dpi \ | ||
| xorg-x11-fonts-cyrillic \ | ||
| xorg-x11-fonts-misc \ | ||
| xorg-x11-fonts-Type1 \ | ||
| && rm -rf /var/cache/yum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AL2023 기반 Lambda 이미지에서 yum 미존재 → 빌드 실패. dnf로 교체하고 패키지 세트 수정 필요
public.ecr.aws/lambda/python:3.12는 Amazon Linux 2023 기반이라 yum이 없고, 현재 패키지 목록 중 다수(예: xorg-x11-fonts-*)는 AL2023 리포에 존재하지 않습니다. 또한 curl을 설치하지 않아 이후 단계에서 curl 호출도 실패합니다. 아래처럼 dnf로 교체하고, AL2023에서 사용 가능한 최소 의존성 세트 및 클린업을 적용하세요.
-RUN yum update -y && \
- yum install -y \
- gzip \
- tar \
- unzip \
- alsa-lib \
- atk \
- cups-libs \
- gtk3 \
- ipa-gothic-fonts \
- libXcomposite \
- libXcursor \
- libXdamage \
- libXext \
- libXi \
- libXrandr \
- libXScrnSaver \
- libXtst \
- pango \
- xorg-x11-fonts-100dpi \
- xorg-x11-fonts-75dpi \
- xorg-x11-fonts-cyrillic \
- xorg-x11-fonts-misc \
- xorg-x11-fonts-Type1 \
- && rm -rf /var/cache/yum
+RUN set -eux; \
+ dnf -y update && \
+ dnf -y install \
+ curl \
+ unzip \
+ tar \
+ gzip \
+ ca-certificates \
+ nss \
+ alsa-lib \
+ atk \
+ at-spi2-atk \
+ at-spi2-core \
+ cups-libs \
+ dbus-libs \
+ gtk3 \
+ libX11 \
+ libXcomposite \
+ libXcursor \
+ libXdamage \
+ libXext \
+ libXi \
+ libXrandr \
+ libXScrnSaver \
+ libXtst \
+ pango \
+ freetype \
+ libdrm \
+ mesa-libgbm \
+ --setopt=install_weak_deps=False && \
+ dnf clean all && rm -rf /var/cache/dnf🧰 Tools
🪛 GitHub Actions: Deploy Onetime Crawler to AWS Lambda
[error] 6-6: Docker build failed during RUN yum update -y ...: 'yum' command not found in the base image.
🤖 Prompt for AI Agents
In Dockerfile around lines 6–30, the Dockerfile uses yum and installs packages
unavailable on Amazon Linux 2023 causing build failures; replace yum with dnf,
update the package list to only include AL2023-available dependencies (remove
xorg-x11-fonts-* and any other unavailable font packages), add curl to the
install list, use non-interactive flags (e.g., -y) and minimize weak deps if
desired, and perform proper cleanup with dnf clean all and removing
metadata/cache directories to keep the image small.
| ENV CHROME_VERSION=125.0.6422.78 | ||
| RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \ | ||
| unzip /tmp/chrome-linux64.zip -d /opt/ && \ | ||
| rm /tmp/chrome-linux64.zip | ||
|
|
||
| # 4️⃣ 필요한 패키지 설치 | ||
| RUN apt-get update && apt-get install -y \ | ||
| curl unzip \ | ||
| chromium chromium-driver \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \ | ||
| unzip /tmp/chromedriver-linux64.zip -d /opt/ && \ | ||
| rm /tmp/chromedriver-linux64.zip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chrome/Driver 다운로드 소스/무결성/실행권한 보강
edgedl.me.gvt1.com대신 공식 GCS 경로(storage.googleapis.com/chrome-for-testing-public/...) 사용을 권장.curl에-fSL추가, unzip에-q로 로그 최소화.- chromedriver에 실행권한 부여.
- 가능하면 SHA256 검증을 추가(아래는 기본형).
-ENV CHROME_VERSION=125.0.6422.78
-RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \
- unzip /tmp/chrome-linux64.zip -d /opt/ && \
- rm /tmp/chrome-linux64.zip
-
-RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \
- unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
- rm /tmp/chromedriver-linux64.zip
+ARG CHROME_VERSION=125.0.6422.78
+ENV CHROME_VERSION=${CHROME_VERSION}
+RUN set -eux; \
+ curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \
+ unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip
+RUN set -eux; \
+ curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \
+ unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \
+ chmod +x /opt/chromedriver-linux64/chromedriver
+# 선택: 무결성 검증 (SHA256SUMS 파일 활용)도 추가 가능📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ENV CHROME_VERSION=125.0.6422.78 | |
| RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \ | |
| unzip /tmp/chrome-linux64.zip -d /opt/ && \ | |
| rm /tmp/chrome-linux64.zip | |
| # 4️⃣ 필요한 패키지 설치 | |
| RUN apt-get update && apt-get install -y \ | |
| curl unzip \ | |
| chromium chromium-driver \ | |
| && rm -rf /var/lib/apt/lists/* | |
| RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \ | |
| unzip /tmp/chromedriver-linux64.zip -d /opt/ && \ | |
| rm /tmp/chromedriver-linux64.zip | |
| ARG CHROME_VERSION=125.0.6422.78 | |
| ENV CHROME_VERSION=${CHROME_VERSION} | |
| RUN set -eux; \ | |
| curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \ | |
| unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip | |
| RUN set -eux; \ | |
| curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \ | |
| unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \ | |
| chmod +x /opt/chromedriver-linux64/chromedriver | |
| # Optional: add SHA256 checksum verification for downloads |
🤖 Prompt for AI Agents
In Dockerfile around lines 34 to 41, the download/unpack steps should be
hardened: switch the download URLs from edgedl.me.gvt1.com to the official GCS
path (storage.googleapis.com/chrome-for-testing-public/.../linux64/...), add
curl flags -fSL to fail on HTTP errors and follow redirects, use unzip -q to
quiet output, and after extracting give the chromedriver binary executable
permission (chmod +x /opt/chromedriver*/chromedriver). Also add an optional
SHA256 verification step: download the .sha256 or compute sha256sum of the zip,
compare it against a trusted checksum before extracting and fail the build if it
doesn’t match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (8)
.github/workflows/crawler-cicd.yaml (6)
8-9: 동시 실행 취소로 레이스/중복 배포 방지같은 브랜치에 여러 실행이 겹치면 최신만 남기고 이전 실행은 취소하세요.
jobs: deploy: + concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true
16-21: 정적 자격증명 사용 시 최소권한·회전 정책 확인사용자 선호에 따라 OIDC 미사용은 OK. 대신 IAM 사용자에 ECR push, Lambda UpdateFunctionCode/Configuration 최소 권한만 부여하고 키 주기적 회전(예: 90일), 필요 시 IP 조건을 적용하세요. Region은 시크릿이 아니므로 상수/env로 빼도 됩니다.
29-38: 빌드 캐시/보안 강화를 위해 build-push 액션 사용 고려로컬 docker 명령은 캐시 미활용·로그인 상태 의존이 큽니다. buildx+캐시로 시간/요금 절감하고 액션 버전을 핀으로 고정하세요.
- - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }} - IMAGE_TAG: ${{ github.sha }} # Git 커밋 ID를 이미지 태그로 사용하여 버전 관리 - run: | - docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "image_uri=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + - name: Set image meta + id: meta + run: echo "tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Build and push (with cache) + id: build-image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ steps.meta.outputs.tag }} + cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:buildcache + cache-to: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:buildcache,mode=max + + - name: Export image uri + run: echo "image_uri=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ steps.meta.outputs.tag }}" >> "$GITHUB_OUTPUT"
41-46: $LATEST 직접 갱신 대신 버전+별칭(alias) 배포로 롤백/안정성 확보버전 발행(--publish) 후 prod/staging 별칭을 이동시키면 점진적 전환·즉시 롤백이 가능합니다.
- aws lambda update-function-code \ - --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ - --image-uri ${{ steps.build-image.outputs.image_uri }} + VERSION=$(aws lambda update-function-code \ + --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ + --image-uri ${{ steps.build-image.outputs.image_uri }} \ + --publish \ + --query 'Version' --output text) + # prod/staging 중 운영 별칭을 이동 (사전 생성 가정) + aws lambda update-alias \ + --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ + --name prod \ + --function-version "$VERSION"
11-21: 액션 버전 고정(pinning) 및 최소 GITHUB_TOKEN 권한 설정서드파티/공식 액션은 커밋 SHA로 핀 고정, GITHUB_TOKEN 권한은 최소화(contents: read 등) 권장.
- - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 + # 예: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 +permissions: + contents: read
28-34: ECR 리포지토리 존재 보장(첫 배포 실패 방지)리포지토리가 없으면 빌드 후 push에서 실패합니다. 사전 생성 또는 조건부 생성 스텝을 추가하세요.
- name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 + + - name: Ensure ECR repository exists + run: | + aws ecr describe-repositories --repository-names "${{ secrets.ECR_REPOSITORY_NAME }}" >/dev/null 2>&1 || \ + aws ecr create-repository --repository-name "${{ secrets.ECR_REPOSITORY_NAME }}"Dockerfile (2)
11-18: OS 패키지 설치 슬림화 및 재현성 개선 제안chromium/chromium-driver 설치는 OK이나, 이미지 비대화/콜드스타트 비용이 큽니다. 다음을 권장합니다.
--no-install-recommends로 추천 패키지 제외.- 가능한 경우 버전 고정(동일 메이저 매칭 유지) 또는 Chrome-for-Testing 번들로 명시 버전 관리.
- 불필요한 도구 미설치 유지.
패치 예시:
-RUN apt-get update && apt-get install -y \ - chromium chromium-driver \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium chromium-driver \ + && rm -rf /var/lib/apt/lists/*
22-23: pip --user + PATH 구성은 동작하나, 운영 근거지로는 venv가 더 안전현재
--user설치와 PATH 추가로 런타임 해상은 됩니다. 다만, 재현성과 의존성 격리를 위해 프로젝트 고정 venv 경로(예:/opt/venv+ENV PATH="/opt/venv/bin:$PATH"+pip install -t또는python -m venv) 사용을 고려하세요.Also applies to: 29-31
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
.github/workflows/crawler-cicd.yaml(1 hunks)Dockerfile(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-21T20:00:59.927Z
Learnt from: bbbang105
PR: onetime-with-members/crawling-server#16
File: .github/workflows/crawler-cicd.yaml:7-9
Timestamp: 2025-09-21T20:00:59.927Z
Learning: User bbbang105 prefers using static AWS credentials over OIDC for GitHub Actions workflows in the crawling-server project.
Applied to files:
.github/workflows/crawler-cicd.yaml
🔇 Additional comments (1)
.github/workflows/crawler-cicd.yaml (1)
3-6: PR 이벤트에서 실배포 방지 + 수동/푸시 배포로 분리하세요pull_request 트리거에서 바로 Lambda 배포가 일어나면 미승인 코드가 배포될 수 있고, 포크 PR에선 시크릿 미제공으로 실패합니다. push/workflow_dispatch만 배포로 가드하세요.
적용 예시(둘 다 적용 권장):
on: - pull_request: - branches: [ "develop" ] + pull_request: + branches: [ "develop" ] # PR에서는 빌드/테스트까지만 + push: + branches: [ "develop" ] # 머지 시 자동 배포 + workflow_dispatch: # 필요 시 수동 배포 jobs: deploy: + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
| - name: Update Lambda function configuration | ||
| env: | ||
| API_KEY_SECRET: ${{ secrets.API_KEY }} | ||
| DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }} | ||
| run: | | ||
| # Lambda가 환경 변수를 업데이트할 때까지 잠시 대기 | ||
| aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} | ||
| # GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정 | ||
| aws lambda update-function-configuration \ | ||
| --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ | ||
| --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
환경 변수 전체 덮어쓰기 위험(기존 키 소실 가능)
현재 Variables=…는 기존 변수를 모두 대체합니다. get-function-configuration로 병합 후 업데이트하세요.
- aws lambda update-function-configuration \
- --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
- --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}"
+ # 기존 변수와 병합
+ EXISTING=$(aws lambda get-function-configuration \
+ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+ --query 'Environment.Variables' --output json)
+ MERGED=$(jq -n --argjson a "$EXISTING" \
+ --arg API_KEY "$API_KEY_SECRET" \
+ --arg DISCORD "$DISCORD_URL_SECRET" \
+ '$a + {API_KEY:$API_KEY, DISCORD_WEBHOOK_URL:$DISCORD}')
+ aws lambda update-function-configuration \
+ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+ --environment "Variables=${MERGED}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Update Lambda function configuration | |
| env: | |
| API_KEY_SECRET: ${{ secrets.API_KEY }} | |
| DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| # Lambda가 환경 변수를 업데이트할 때까지 잠시 대기 | |
| aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} | |
| # GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정 | |
| aws lambda update-function-configuration \ | |
| --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ | |
| --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}" | |
| - name: Update Lambda function configuration | |
| env: | |
| API_KEY_SECRET: ${{ secrets.API_KEY }} | |
| DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| # Lambda가 환경 변수를 업데이트할 때까지 잠시 대기 | |
| aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} | |
| # 기존 변수와 병합 (존재하지 않으면 빈 객체로 처리) | |
| EXISTING=$(aws lambda get-function-configuration \ | |
| --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ | |
| --query 'Environment.Variables' --output json || echo 'null') | |
| MERGED=$(jq -c -n --argjson a "$EXISTING" \ | |
| --arg API_KEY "$API_KEY_SECRET" \ | |
| --arg DISCORD "$DISCORD_URL_SECRET" \ | |
| '($a // {}) + {API_KEY:$API_KEY, DISCORD_WEBHOOK_URL:$DISCORD}') | |
| ENV_JSON=$(jq -c -n --argjson vars "$MERGED" '{"Variables": $vars}') | |
| aws lambda update-function-configuration \ | |
| --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ | |
| --environment "$ENV_JSON" |
🤖 Prompt for AI Agents
In .github/workflows/crawler-cicd.yaml around lines 48 to 59, the workflow
currently overwrites the Lambda function’s entire environment (Variables=...)
which can erase existing keys; change it to fetch the current environment with
aws lambda get-function-configuration, parse/merge the existing Variables with
the new API_KEY and DISCORD_WEBHOOK_URL entries, then call aws lambda
update-function-configuration with the merged Variables so existing keys are
preserved; ensure you handle empty/missing Variables, proper quoting/escaping of
values, and fail fast if merging or parsing the JSON configuration fails.
Dockerfile
Outdated
|
|
||
| # 8️⃣ 실행 명령어 설정 | ||
| CMD ["python", "app.py"] | ||
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
비루트 사용자 + 포트 80 바인딩 실패
현재 USER가 appuser이므로 80/tcp 바인딩은 권한 오류로 실패합니다. 8000/8080 등 비특권 포트로 변경하세요.
적용 패치 예시:
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] | |
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] |
🤖 Prompt for AI Agents
In Dockerfile around line 36, the container command binds to privileged port 80
while the image switches to non-root user (appuser), causing permission errors;
change the uvicorn bind port to a non-privileged port (e.g., 8000 or 8080) in
the CMD, update any EXPOSE line to match the new port, and ensure any external
documentation or docker-compose ports mapping is updated accordingly so the app
runs without root privileges.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
Dockerfile (2)
22-23: PATH만으로는 애플리케이션 설정과 불일치 가능 — 표준 ENV도 함께 노출 권장코드가 CHROME_BIN/CHROMEDRIVER_PATH를 참조한다면 현재 Dockerfile에는 해당 ENV가 없습니다. PATH 탐색에만 의존하면 런타임 설정 변경 시 디버깅이 어려워집니다. 아래처럼 명시적 ENV를 추가하는 것을 권장합니다.
-ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}" +ENV CHROME_BIN=/opt/chrome-linux64/chrome +ENV CHROMEDRIVER_PATH=/opt/chromedriver-linux64/chromedriver +ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"추가로, 애플리케이션에서 위 ENV를 실제로 읽는지 확인 부탁드립니다.
31-31: pip 설치 단계 무난함 (추가 최적화 여지 소소)--no-cache-dir 사용은 적절합니다. 필요 시 안전한 범위에서 pip 버전 고정 또는 constraints 파일 도입을 고려할 수 있습니다(선택).
app/services/driver_pool.py (2)
42-44: 예외 처리를 구체적으로 개선하세요현재 모든 예외를 포괄적으로 잡고 있어 디버깅이 어렵습니다.
구체적인 예외 처리로 개선하세요:
- except Exception as e: - print(f"WebDriver 생성 실패: {e}") + except (WebDriverException, OSError) as e: + print(f"WebDriver 생성 실패: {e}") return Noneselenium의 예외를 import하는 것도 잊지 마세요:
from selenium import webdriver +from selenium.common.exceptions import WebDriverException from selenium.webdriver.chrome.options import Options
54-58: 종료 시 드라이버 정리 로직에 예외 처리 추가드라이버 종료 중 예외가 발생하면 나머지 드라이버들이 정리되지 않을 수 있습니다.
안전한 종료를 위해 예외 처리를 추가하세요:
def close_all(self): """애플리케이션 종료 시 모든 WebDriver를 종료합니다.""" while not self._pool.empty(): - driver = self._pool.get() - driver.quit() + try: + driver = self._pool.get_nowait() + driver.quit() + except queue.Empty: + break + except Exception as e: + print(f"드라이버 종료 중 오류 발생: {e}")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Dockerfile(1 hunks)app/services/driver_pool.py(1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py
42-42: Do not catch blind exception: Exception
(BLE001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy
🔇 Additional comments (9)
Dockerfile (5)
1-2: Lambda 베이스 이미지 전환 잘했습니다AWS Lambda Python 3.12 공식 이미지를 사용하고 CMD를 핸들러로 지정하는 방향과 일치합니다. CI/CD와 함수 핸들러 설정도 이 전제에 맞춰 유지해 주세요.
26-28: 레이어 캐싱 활용 구조 적절requirements → pip 설치 → 앱 코드 복사 순서가 적절해 빌드 캐시 재활용에 유리합니다.
33-34: Lambda 핸들러 진입점 지정 OKMangum 핸들러 노출(CMD ["app.main.handler"])이 베이스 이미지와 일치합니다.
Lambda 함수 설정(이미지 기반)에서도 핸들러가 빈 값이어야 하고(이미지 엔트리포인트 사용), 필요한 환경변수(API 키 등)가 세팅되어 있는지 확인해 주세요.
13-20: Chrome/Driver 다운로드 소스/무결성/실행권한 강화 필요이전 리뷰와 동일한 이슈입니다: 공식 GCS 경로 사용, curl 실패 시 빌드 중단, unzip 조용 모드, chromedriver 실행권한, 버전 ARG화, 선택적 SHA256 검증을 권장합니다.
-ENV CHROME_VERSION=125.0.6422.78 -RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \ - unzip /tmp/chrome-linux64.zip -d /opt/ && \ - rm /tmp/chrome-linux64.zip - -RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \ - unzip /tmp/chromedriver-linux64.zip -d /opt/ && \ - rm /tmp/chromedriver-linux64.zip +ARG CHROME_VERSION=125.0.6422.78 +ENV CHROME_VERSION=${CHROME_VERSION} +RUN set -eux; \ + curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \ + unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip +RUN set -eux; \ + curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \ + unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \ + chmod +x /opt/chromedriver-linux64/chromedriver +# (선택) SHA256 검증 추가 가능 +# RUN curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/sha256sum.txt" -o /tmp/SHA256SUMS && \ +# (cd /tmp && sha256sum -c SHA256SUMS --ignore-missing)
5-10: 필수 패키지 누락으로 빌드/런타임 실패 위험 (curl, libgbm 계열 등 추가 필요)
- curl 미설치 상태에서 아래 단계에서 curl 호출(라인 14, 18)로 인해 빌드가 즉시 실패합니다.
- Headless Chromium 구동에 자주 필요한 mesa-libgbm/libdrm, at-spi2-core, dbus-libs, freetype가 빠져 크롬 실행이 실패할 수 있습니다(“error while loading shared libraries: libgbm.so.1” 등).
아래 패치를 적용해 주세요.
-RUN microdnf install -y \ - unzip gzip tar \ - glib2 nss atk at-spi2-atk cups-libs gtk3 pango \ - libX11 libXcomposite libXcursor libXdamage libXext libXi \ - libXrandr libXScrnSaver libXtst alsa-lib fontconfig \ - && microdnf clean all +RUN microdnf install -y \ + unzip gzip tar curl ca-certificates \ + glib2 nss atk at-spi2-atk at-spi2-core cups-libs gtk3 pango freetype \ + dbus-libs \ + libX11 libXcomposite libXcursor libXdamage libXext libXi \ + libXrandr libXScrnSaver libXtst alsa-lib fontconfig \ + libdrm mesa-libgbm \ + && microdnf clean allapp/services/driver_pool.py (4)
8-14: Lambda에서의 초기 드라이버 일괄 생성은 메모리/콜드스타트 리스크가 큽니다Lambda 컨테이너에서 Chrome 5개 선생성은 OOM/콜드스타트 지연을 유발할 수 있습니다. 실패 시 앱 초기화 전체가 터집니다.
게으른 생성+비동기 대여로 전환을 권장합니다.
46-48: 풀 고갈 시 무기한 대기 위험 → timeout 부여 필요현재
get()은 끝없이 블로킹하여 요청 스레드가 고립될 위험이 있습니다.
50-52: release 시 큐가 가득 차면 영구 블로킹 위험이중 반납 등 실수 시
put()이 무기한 대기할 수 있습니다.
19-41: Chrome 설정을 환경변수 기반으로 개선하고 불안정한 플래그를 제거하세요현재 구현에는 여러 개선점이 필요합니다:
--single-process플래그는 크래시와 불안정의 원인이므로 제거 권장- 바이너리 경로와 드라이버 경로가 하드코딩되어 있어 환경별 배포에 취약
- User-Agent가 하드코딩되어 유지보수가 어려움
환경변수 기반 설정으로 개선하세요:
def _create_driver(self): try: chrome_options = Options() - - chrome_options.binary_location = "/opt/chrome-linux64/chrome" + + binary = os.getenv("CHROME_BINARY_PATH", "/opt/chrome-linux64/chrome") + chrome_options.binary_location = binary # Headless & Lambda 옵션 추가 chrome_options.add_argument("--headless") chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-setuid-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--disable-extensions") chrome_options.add_argument("--window-size=1280x1696") - chrome_options.add_argument("--single-process") chrome_options.add_argument("--disable-software-rasterizer") chrome_options.add_argument("--no-zygote") chrome_options.add_argument("--remote-debugging-port=9222") - chrome_options.add_argument('--user-data-dir=/tmp/user-data') - chrome_options.add_argument('--data-path=/tmp/data-path') - chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir') + + user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data") + data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path") + cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir") + chrome_options.add_argument(f'--user-data-dir={user_data_dir}') + chrome_options.add_argument(f'--data-path={data_path}') + chrome_options.add_argument(f'--disk-cache-dir={cache_dir}') # User-Agent 설정 - chrome_options.add_argument( - "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + ua = os.getenv("CHROME_USER_AGENT") + if ua: + chrome_options.add_argument(f"user-agent={ua}") - service = Service(executable_path="/opt/chromedriver-linux64/chromedriver") + driver_path = os.getenv("CHROMEDRIVER_PATH", "/opt/chromedriver-linux64/chromedriver") + service = Service(executable_path=driver_path) return webdriver.Chrome(service=service, options=chrome_options)이 변경사항을 위해 파일 상단에
import os를 추가해야 합니다:import queue +import os from selenium import webdriver
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (6)
app/main.py (2)
44-59: 프로세스 시간 측정에 time.perf_counter 사용 권장고해상도 단조 타이머로 전환하면 더 정확합니다. 헤더 값은 소수점 고정으로 전달하세요.
async def add_process_time_header(request: Request, call_next): - # 1. 요청이 들어올 때 시작 시간 기록 - start_time = time.time() + # 1. 요청이 들어올 때 시작 시간 기록 + start_time = time.perf_counter() @@ - process_time = time.time() - start_time + process_time = time.perf_counter() - start_time @@ - response.headers["X-Process-Time"] = str(process_time) + response.headers["X-Process-Time"] = f"{process_time:.4f}"
62-79: CORS: 테스트 도메인 정리 및 와일드카드 대체로 정규식 허용 추가
- vercel.app는 www 서브도메인을 사용하지 않습니다. 목록에서 제거하세요.
- 제품 도메인은 allow_origin_regex로 서브도메인 전체 허용이 깔끔합니다.
origins = [ - "http://localhost:5173", - "http://localhost:3000", - "https://onetime-test.vercel.app", - "https://www.onetime-test.vercel.app", - "https://onetime-with-members.com", - "https://www.onetime-with-members.com", - "https://1-ti.me", + "http://localhost:5173", + "http://localhost:3000", + "https://onetime-test.vercel.app", + "https://onetime-with-members.com", + "https://www.onetime-with-members.com", + "https://1-ti.me", ] app.add_middleware( CORSMiddleware, allow_origins=origins, + allow_origin_regex=r"^https://([a-z0-9-]+\.)?onetime-with-members\.com$", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )app/services/driver_pool.py (4)
1-4: 환경 변수 사용을 위한 os 임포트 추가하드코드된 경로/플래그를 ENV로 치환할 준비가 필요합니다.
import logging import queue import threading +import os
5-8: 구체 예외 임포트로 블라인드 캐치 제거 준비WebDriverException을 명시적으로 다루기 위해 임포트합니다.
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service +from selenium.common.exceptions import WebDriverException
70-75: close_all: None 가드 및 카운터 정합성 회복큐에서 나온 항목이 None일 수 있으므로 가드하고, 종료 시 created_count를 감소시켜 정합성을 맞춥니다.
def close_all(self): """애플리케이션 종료 시 모든 WebDriver를 종료합니다.""" - while not self._pool.empty(): - driver = self._pool.get() - driver.quit() + while not self._pool.empty(): + driver = self._pool.get() + try: + if driver: + driver.quit() + finally: + with self.lock: + self.created_count = max(0, self.created_count - 1)
11-17: 문맥 관리자 제공으로 대여/반납 보일러플레이트 제거 제안with pool.borrow(): 형태를 제공하면 누수 방지에 효과적입니다.
# (추가 제안) 클래스 내부에 컨텍스트 매니저 제공 from contextlib import contextmanager @contextmanager def borrow(self, timeout: float = 10.0): driver = self.get_driver(timeout=timeout) try: yield driver finally: self.release_driver(driver)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/main.py(1 hunks)app/services/driver_pool.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/main.py (3)
app/core/exception_handlers.py (2)
http_exception_handler(7-25)validation_exception_handler(28-44)app/core/logging_config.py (1)
setup_logging(26-28)app/services/driver_pool.py (2)
WebDriverPool(11-74)close_all(70-74)
🪛 Ruff (0.13.1)
app/services/driver_pool.py
45-45: Do not catch blind exception: Exception
(BLE001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy
🔇 Additional comments (3)
app/main.py (1)
23-31: Lambda에서 드라이버 풀 초기화 지연·환경 토글 적용 필요 (이전 코멘트와 동일 이슈)콜드스타트/의존 바이너리 부재 시 실패를 피하려면 POOL_SIZE(기본 0)로 제어하고, 0이면 생성 생략+지연 초기화가 안전합니다. 종료 시 존재 여부를 확인해 안전 종료하세요.
+import os @@ @asynccontextmanager async def lifespan(app: FastAPI): - # 앱 시작 시 실행 - logger.info("🚀 WebDriver Pool을 생성합니다...") - app.state.driver_pool = WebDriverPool(size=1) + # 앱 시작 시 실행 + pool_size = int(os.getenv("POOL_SIZE", "0")) + if pool_size > 0: + logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})") + app.state.driver_pool = WebDriverPool(size=pool_size) + else: + logger.info("🚀 WebDriver Pool 지연초기화 모드") + app.state.driver_pool = None yield # 앱 종료 시 실행 - logger.info("💀 WebDriver Pool을 종료합니다...") - app.state.driver_pool.close_all() + if getattr(app.state, "driver_pool", None): + logger.info("💀 WebDriver Pool을 종료합니다...") + app.state.driver_pool.close_all()app/services/driver_pool.py (2)
66-69: release_driver: None/불량 드라이버 처리 및 put 무한대기 방지
- None 또는 세션 없는 드라이버는 폐기하고 카운터 감소.
- 큐가 가득 차면 timeout 후 폐기.
def release_driver(self, driver): """사용한 WebDriver를 Pool에 반납합니다.""" - self._pool.put(driver) + # None 또는 비정상 드라이버는 폐기 + if driver is None or getattr(driver, "session_id", None) is None: + try: + if driver: + driver.quit() + finally: + with self.lock: + self.created_count = max(0, self.created_count - 1) + return + try: + self._pool.put(driver, timeout=1) + except queue.Full: + try: + driver.quit() + finally: + with self.lock: + self.created_count = max(0, self.created_count - 1)
49-65: get_driver: 실패 시 None 반환·무기한 대기·카운터 불일치 가능성 → 타임아웃·성공 시에만 카운트 증가
- 생성 실패 시 None 반환은 호출측 NPE 유발.
- 무한 대기 대신 timeout 후 명확히 예외.
- created_count는 성공 생성 후에만 증가.
- def get_driver(self): - """Pool에서 드라이버를 가져오거나, 없으면 새로 생성합니다.""" - try: - # 1. 일단 Pool에 드라이버가 있는지 확인 (기다리지 않음) - return self._pool.get_nowait() - except queue.Empty: - # 2. Pool이 비어있으면, 새로 만들어도 되는지 확인 - with self.lock: - if self.created_count < self.max_size: - self.created_count += 1 - logger.info(f"새로운 WebDriver 생성 (현재 {self.created_count}/{self.max_size}개)") - return self._create_driver() - - # 3. 만들 수 없다면, 다른 요청이 드라이버를 반납할 때까지 기다림 - logger.info("사용 가능한 WebDriver가 없어 대기합니다...") - return self._pool.get() + def get_driver(self, timeout: float = 10.0): + """Pool에서 드라이버를 가져오거나, 없으면 새로 생성합니다.""" + try: + return self._pool.get_nowait() + except queue.Empty: + pass + # 2) 여유가 있으면 생성 시도(성공 시에만 카운트 증가) + with self.lock: + if self.created_count < self.max_size: + driver = self._create_driver() + if driver is not None: + self.created_count += 1 + logger.info(f"새로운 WebDriver 생성 (현재 {self.created_count}/{self.max_size}개)") + return driver + logger.error("WebDriver 생성 실패로 풀 확장을 건너뜁니다.") + # 3) 반납 대기 (타임아웃) + logger.info("사용 가능한 WebDriver가 없어 대기합니다...") + try: + return self._pool.get(timeout=timeout) + except queue.Empty: + raise TimeoutError("WebDriverPool: 드라이버 대여가 제한 시간 내에 불가했습니다.")
| def _create_driver(self): | ||
| try: | ||
| chrome_options = Options() | ||
|
|
||
| chrome_options.binary_location = "/opt/chrome-linux64/chrome" | ||
|
|
||
| # Headless & Lambda 옵션 추가 | ||
| chrome_options.add_argument("--headless") | ||
| chrome_options.add_argument("--no-sandbox") | ||
| chrome_options.add_argument("--disable-dev-shm-usage") | ||
| chrome_options.add_argument("--disable-gpu") | ||
| chrome_options.add_argument("--window-size=1280x1696") | ||
| chrome_options.add_argument("--single-process") | ||
| chrome_options.add_argument("--disable-software-rasterizer") | ||
| chrome_options.add_argument("--no-zygote") | ||
| chrome_options.add_argument("--remote-debugging-port=9222") | ||
| chrome_options.add_argument('--user-data-dir=/tmp/user-data') | ||
| chrome_options.add_argument('--data-path=/tmp/data-path') | ||
| chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir') | ||
|
|
||
| # User-Agent 설정 | ||
| chrome_options.add_argument( | ||
| "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") | ||
|
|
||
| service = Service(executable_path="/opt/chromedriver-linux64/chromedriver") | ||
|
|
||
| return webdriver.Chrome(service=service, options=chrome_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
드라이버 생성: 불안정 플래그 제거, 경로/UA 하드코드 제거, ENV 기반 설정으로 전환
- --single-process 제거(불안정).
- 바이너리/드라이버/UA/캐시 경로를 ENV로 지정 가능하게.
- Lambda에 맞게 headless/new·옵션 정리.
def _create_driver(self):
try:
chrome_options = Options()
-
- chrome_options.binary_location = "/opt/chrome-linux64/chrome"
+ # ENV 기반 경로/설정
+ binary = os.getenv("CHROME_BINARY_PATH", "/opt/chrome-linux64/chrome")
+ driver_path = os.getenv("CHROMEDRIVER_PATH", "/opt/chromedriver-linux64/chromedriver")
+ user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data")
+ data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path")
+ cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir")
+ user_agent = os.getenv("CHROME_USER_AGENT")
+ if binary:
+ chrome_options.binary_location = binary
# Headless & Lambda 옵션 추가
- chrome_options.add_argument("--headless")
+ chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1280x1696")
- chrome_options.add_argument("--single-process")
chrome_options.add_argument("--disable-software-rasterizer")
chrome_options.add_argument("--no-zygote")
chrome_options.add_argument("--remote-debugging-port=9222")
- chrome_options.add_argument('--user-data-dir=/tmp/user-data')
- chrome_options.add_argument('--data-path=/tmp/data-path')
- chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir')
+ chrome_options.add_argument(f'--user-data-dir={user_data_dir}')
+ chrome_options.add_argument(f'--data-path={data_path}')
+ chrome_options.add_argument(f'--disk-cache-dir={cache_dir}')
# User-Agent 설정
- chrome_options.add_argument(
- "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+ if user_agent:
+ chrome_options.add_argument(f"user-agent={user_agent}")
- service = Service(executable_path="/opt/chromedriver-linux64/chromedriver")
+ service = Service(executable_path=driver_path)
return webdriver.Chrome(service=service, options=chrome_options)
- except Exception as e:
- logger.info(f"WebDriver 생성 실패: {e}")
+ except (WebDriverException, OSError) as e:
+ logger.error(f"WebDriver 생성 실패: {e}")
return None🏁 Script executed:
#!/bin/bash
# driver_pool.py 의 상단 import 및 예외 처리용 타입 확인
sed -n '1,50p' app/services/driver_pool.py
rg -n "import os" -g "app/services/driver_pool.py"
rg -n "WebDriverException" -g "app/services/driver_pool.py"
rg -n "Service" -g "app/services/driver_pool.py"
rg -n "Options" -g "app/services/driver_pool.py"Length of output: 2318
드라이버 생성: ENV 기반 설정 적용 및 불안정 플래그 제거 (필수 리팩터)
- 위치: app/services/driver_pool.py (약 18–44행)
- 변경 요구:
- CHROME_BINARY_PATH, CHROMEDRIVER_PATH, CHROME_USER_DATA_DIR, CHROME_DATA_PATH, CHROME_CACHE_DIR, CHROME_USER_AGENT 등으로 하드코드된 바이너리/드라이버/디렉토리/UA 대체(기본값은 기존 값 유지).
- chrome 옵션에서 --single-process 제거, --headless → --headless=new로 교체; 나머지 Lambda 친화 옵션은 필요 시 유지 검토.
- 파일 상단에 import os 및 from selenium.common.exceptions import WebDriverException 추가. except Exception → except (WebDriverException, OSError)로 제한하고 logger.info → logger.error로 변경.
- Service(executable_path=...)를 환경변수(CHROMEDRIVER_PATH) 사용으로 대체.
- 검증 요구: 변경 후 Lambda/컨테이너 환경에서 Chrome/Chromedriver 경로·권한과 headless 옵션 호환성 확인.
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 18 to 44, the driver creation
currently uses many hardcoded paths and unstable flags; update it to read
CHROME_BINARY_PATH, CHROMEDRIVER_PATH, CHROME_USER_DATA_DIR, CHROME_DATA_PATH,
CHROME_CACHE_DIR, and CHROME_USER_AGENT from environment variables with the
current hardcoded values as defaults, replace
chrome_options.add_argument("--headless") with "--headless=new", remove the
"--single-process" flag, keep or review other Lambda-friendly flags as needed,
use os.environ.get(...) for each path/UA, replace Service(executable_path="...")
with Service(executable_path=CHROMEDRIVER_PATH), add imports "import os" and
"from selenium.common.exceptions import WebDriverException" at the top, change
the bare except Exception to except (WebDriverException, OSError) and switch the
logger.info call in the except block to logger.error, and then validate
Chrome/Chromedriver paths, permissions and headless compatibility in
Lambda/container environments after making these changes.
✅ PR 유형
🚀 작업 내용
기존 문제점 (Problem)
t2.micro인스턴스를 유지해야 했습니다. 이는 유휴 시간 대부분에 불필요한 비용이 발생하는 비효율적인 구조였습니다.t2.micro단일 인스턴스 구조는 갑작스러운 트래픽 증가에 대응할 수 없어, 동시 요청이 몰릴 경우 서버가 다운될 수 있는 잠재적인 불안정성을 내포하고 있었습니다.변경 사항 (Solution)
상시 실행되던 EC2 인스턴스 기반의 서버를, 요청이 있을 때만 실행되는 AWS Lambda 기반의 서버리스(Serverless) 아키텍처로 전환했습니다. 배포 단위는 Docker 컨테이너 이미지를 사용하여, 로컬 개발 환경과 실제 운영 환경의 일관성을 확보했습니다.
기존 Flask로 작성되었던 API 서버를, 현대적인 고성능 웹 프레임워크인 FastAPI로 마이그레이션했습니다. 이를 통해 다음과 같은 개선을 이루었습니다.
async/await를 활용하여 I/O 병목을 줄일 수 있는 기반을 마련했습니다.GitHub Actions, Docker, Amazon ECR, AWS Lambda를 연동하여 CI/CD 파이프라인을 구축했습니다. 이제
develop브랜치에 코드를 푸시하기만 하면, 아래의 모든 과정이 자동으로 실행됩니다.배포 초기, Lambda의 콜드 스타트로 인해 응답 속도가 5초 이상 소요되는 문제가 있었습니다. 다음과 같은 최적화를 통해 문제를 해결했습니다.
기대 효과 (Expected Effects)
git push만으로 배포가 자동화되어 배포에 소요되는 시간이 사라졌으며, OS 업데이트나 보안 패치 등 서버를 직접 관리해야 하는 부담이 완전히 없어졌습니다.t2.micro환경에서 발생할 수 있었던 서버 다운의 위험 없이 안정적인 서비스 운영이 가능해졌습니다.📝️ 관련 이슈
💬 기타 사항 or 추가 코멘트
Summary by CodeRabbit
New Features
Refactor
Documentation
Chores