diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..2b012b9 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,85 @@ +name: CD + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 이미지 태그 생성 + id: vars + run: echo "tag=${GITHUB_SHA}" >> $GITHUB_OUTPUT + + - name: 이미지 경로 생성 + run: | + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + echo "IMAGE_NAME=ghcr.io/${REPO_LOWER}/backend" >> $GITHUB_ENV + + - name: GHCR 로그인 + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: QEMU 설정 + uses: docker/setup-qemu-action@v3 + + - name: Buildx 설정 + uses: docker/setup-buildx-action@v3 + + - name: 멀티 아키텍처 이미지 빌드 및 푸시 + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f Dockerfile \ + -t $IMAGE_NAME:${{ steps.vars.outputs.tag }} \ + -t $IMAGE_NAME:latest \ + --push \ + . + + - name: 서버에 배포 디렉토리 생성 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + port: ${{ secrets.DEPLOY_PORT }} + script: | + mkdir -p ~/teampling + + - name: 배포 파일 전송 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + port: ${{ secrets.DEPLOY_PORT }} + source: "docker-compose.prod.yaml,nginx.conf" + target: "~/teampling" + + - name: 서버 배포 실행 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + port: ${{ secrets.DEPLOY_PORT }} + script: | + cd /home/${{ secrets.DEPLOY_USER }}/teampling + + export IMAGE_TAG=${{ steps.vars.outputs.tag }} + + docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GHCR_PAT }} + + docker compose -f docker-compose.prod.yaml pull app + docker compose -f docker-compose.prod.yaml run --rm app alembic upgrade head + docker compose -f docker-compose.prod.yaml up -d app nginx + + docker image prune -f \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f5db58f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + pull_request: + push: + branches: [develop, main] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: testdb + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U testuser -d testdb" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + PYTHONPATH: ${{ github.workspace }} + APP_ENV: test + APP_NAME: teampling-test + POSTGRES_DB: testdb + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + POSTGRES_HOST: localhost + POSTGRES_PORT: "5432" + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 파이썬 환경 설정 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: 의존성 설치 + run: | + pip install poetry + poetry config virtualenvs.create false + poetry install --no-interaction --no-ansi + + - name: 테스트 실행 + env: + DATABASE_URL: postgresql+psycopg://testuser:testpass@localhost:5432/testdb + run: pytest -q + + - name: Alembic 마이그레이션 테스트 + env: + DATABASE_URL: postgresql+psycopg://testuser:testpass@localhost:5432/testdb + run: alembic upgrade head + + - name: 도커 빌드 테스트 + run: docker build -f Dockerfile -t backend:test . \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7d884c5..727ec88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,22 @@ FROM python:3.12-slim -RUN pip install poetry +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 -WORKDIR /teampling +WORKDIR /app -RUN pip install --no-cache-dir --upgrade pip +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* -COPY pyproject.toml poetry.lock ./ +COPY pyproject.toml poetry.lock* /app/ -RUN poetry config virtualenvs.create false +RUN pip install --no-cache-dir poetry && \ + poetry config virtualenvs.create false && \ + poetry install --only main --no-interaction --no-ansi -RUN poetry install --no-root --only main +COPY . /app -COPY . . - -EXPOSE 8000 \ No newline at end of file +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 41714d5..c44536e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -37,7 +37,7 @@ def LOCAL_DATABASE_URL(self) -> str: ) # JWT / AUTH - JWT_SECRET: str + JWT_SECRET: str = "" JWT_ALG: str = "HS256" ACCESS_TOKEN_MINUTES: int = 30 REFRESH_TOKEN_DAYS: int = 14 @@ -52,13 +52,12 @@ def LOCAL_DATABASE_URL(self) -> str: LOG_LEVEL: str = "INFO" # Oracle Cloud Storage - OCI_USER_OCID: str - OCI_API_KEY_PATH: str - OCI_FINGERPRINT: str - OCI_TENANCY_OCID: str - OCI_REGION: str - - OCI_OBJECT_STORAGE_NAMESPACE: str - OCI_OBJECT_STORAGE_BUCKET: str + OCI_USER_OCID: str = "" + OCI_API_KEY_PATH: str = "" + OCI_FINGERPRINT: str = "" + OCI_TENANCY_OCID: str = "" + OCI_REGION: str = "" + OCI_OBJECT_STORAGE_NAMESPACE: str = "" + OCI_OBJECT_STORAGE_BUCKET: str = "" settings = Settings() diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 0000000..ee5f05b --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,45 @@ +services: + db: + image: postgres:16 + container_name: teampling-db + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + restart: always + + app: + image: ghcr.io/teampling/backend_main/backend:${IMAGE_TAG} + container_name: teampling-app + env_file: + - .env + depends_on: + db: + condition: service_healthy + expose: + - "8000" + restart: always + + nginx: + image: nginx:stable-alpine + container_name: teampling-nginx + depends_on: + - app + ports: + - "80:80" + # - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + # - ./certbot/conf:/etc/letsencrypt + # - ./certbot/www:/var/www/certbot + restart: always + +volumes: + postgres_data: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 6c97f74..af26851 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,7 +29,7 @@ services: ports: - "8000:8000" volumes: - - ./app:/teampling/app # 코드 바뀌면 바로 반영(개발용) + - ./app:/app/app command: > uvicorn app.main:app --host 0.0.0.0 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..f83bedf --- /dev/null +++ b/nginx.conf @@ -0,0 +1,24 @@ +events {} + +http { + upstream backend { + server app:8000; + } + + server { + listen 80; + server_name _; + + client_max_body_size 20M; + + location / { + proxy_pass http://backend; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file