From fc987592aac5450362febce5c8adc703c8114a0c Mon Sep 17 00:00:00 2001 From: bbbang105 <2018111366@dgu.ac.kr> Date: Fri, 13 Mar 2026 03:19:40 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B4=87=20Docker/ECR=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=B6=95=20=EB=B0=8F=20Railway=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GitHub Actions CI/CD 워크플로우 추가 (CI Gate → ECR 빌드 → SSH 배포) - Dockerfile node:20 → node:22 업데이트 - Railway 설정 제거 (railway.json, bot/.dockerignore) - CLAUDE.md, ARCHITECTURE.md 배포 구조 최신화 Co-Authored-By: Claude --- .dockerignore | 2 +- .github/workflows/bot-deploy.yml | 102 +++++++++++++++++++++++++++++++ CLAUDE.md | 19 +++++- docs/ARCHITECTURE.md | 23 +++++-- packages/bot/.dockerignore | 36 ----------- packages/bot/Dockerfile | 18 ++---- packages/bot/railway.json | 18 ------ 7 files changed, 144 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/bot-deploy.yml delete mode 100644 packages/bot/.dockerignore delete mode 100644 packages/bot/railway.json diff --git a/.dockerignore b/.dockerignore index 63a3791..40c18a7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,7 +24,7 @@ Thumbs.db *.log npm-debug.log* -# Environment files (use Railway/Vercel environment variables) +# Environment files .env .env.* !.env.example diff --git a/.github/workflows/bot-deploy.yml b/.github/workflows/bot-deploy.yml new file mode 100644 index 0000000..88e2b02 --- /dev/null +++ b/.github/workflows/bot-deploy.yml @@ -0,0 +1,102 @@ +name: 🤖 Bot Build & Deploy + +on: + push: + branches: [dev] + paths: + - 'packages/bot/**' + - 'packages/shared/**' + - 'packages/bot/Dockerfile' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: bot-deploy-${{ github.ref }} + cancel-in-progress: true + +env: + ECR_REPOSITORY: study-admin-bot + AWS_REGION: ap-northeast-2 + +jobs: + ci: + name: CI Gate + runs-on: ubuntu-latest + steps: + - name: ✅ 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 📦 pnpm 설정 + uses: pnpm/action-setup@v4 + + - name: 🟢 Node.js 설정 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + + - name: 📥 의존성 설치 + run: pnpm install --frozen-lockfile + + - name: 🔨 shared 빌드 + run: pnpm --filter @blog-study/shared build + + - name: 🔍 린트 + run: pnpm --filter @blog-study/bot lint + + - name: 🔎 타입 체크 + run: pnpm --filter @blog-study/bot typecheck + + - name: 🧪 테스트 + run: pnpm --filter @blog-study/bot test + + build-and-push: + name: Build and Push to ECR + needs: ci + runs-on: ubuntu-latest + steps: + - name: ✅ 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 🏗️ Docker Buildx 설정 + uses: docker/setup-buildx-action@v3 + + - name: 🔐 AWS 자격 증명 설정 + 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: ${{ env.AWS_REGION }} + + - name: 🐳 ECR 로그인 + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: 🐳 Docker 이미지 빌드 및 푸시 (ARM64) + uses: docker/build-push-action@v6 + with: + context: . + file: ./packages/bot/Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy to EC2 + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: 🚀 SSH로 배포 스크립트 실행 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + ~/study-admin-bot/deploy.sh ${{ github.sha }} diff --git a/CLAUDE.md b/CLAUDE.md index 589019b..355ac77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,9 +6,11 @@ ``` packages/ -├── bot/ # Discord 봇 (스케줄러 + 이벤트 핸들러만, 슬래시 커맨드 없음) → AWS EC2 배포 +├── bot/ # Discord 봇 (스케줄러 + 이벤트 핸들러만, 슬래시 커맨드 없음) → AWS EC2 (Docker) ├── web/ # Next.js 16 대시보드 → Vercel 배포 └── shared/ # 공유 코드 (DB 스키마, 타입, 유틸) +deploy/ +└── bot/ # EC2 배포 스크립트 (deploy.sh) — 리포에 커밋하지 않음, EC2에 직접 배치 ``` **모노레포**: pnpm workspace (`pnpm-workspace.yaml`) @@ -22,7 +24,8 @@ packages/ | Web | Next.js 16 App Router, React 19, shadcn/ui, Tailwind CSS v4, Tiptap (리치 에디터), sonner (토스트), Framer Motion (랜딩 애니메이션) | | DB | Supabase PostgreSQL + Drizzle ORM (Transaction Pooler, `prepare: false`) | | Auth | Supabase Auth (Discord OAuth) + `@supabase/ssr` | -| 배포 | AWS EC2 (bot), Vercel (web), Supabase (DB + Auth) | +| 배포 | AWS EC2 Docker (bot), Vercel (web), Supabase (DB + Auth) | +| CI/CD | GitHub Actions → ECR → SSH deploy (bot), Vercel Git Integration (web) | ## 개발 명령어 @@ -102,6 +105,9 @@ pnpm --filter @blog-study/bot rss-collect # 수동 RSS 수집 (봇 없이) | `packages/web/src/components/landing/landing-client.tsx` | 랜딩 페이지 클라이언트 (7섹션: Hero, Stats, Bento, HowItWorks, Marquee, CTA, Footer) | | `packages/web/src/components/landing/motion.tsx` | 랜딩 애니메이션 컴포넌트 (FadeUp, StaggerContainer, CountUp, DrawLine) | | `packages/web/public/logo.svg` | 풀 로고 SVG (픽토그램 + 텍스트) | +| `packages/bot/Dockerfile` | 봇 Docker 이미지 (multi-stage, node:22-alpine) | +| `.github/workflows/bot-deploy.yml` | 봇 CI/CD (CI Gate → ECR 빌드/푸시 → SSH 배포) | +| `.github/workflows/ci.yml` | PR/push CI (lint, typecheck, test, build) | ## 인증 구조 @@ -219,6 +225,15 @@ npx drizzle-kit push --force | `docs/plans/26-03-08-landing-page-redesign-design.md` | 랜딩 페이지 리디자인 디자인 문서 | | `docs/plans/26-03-08-landing-page-redesign.md` | 랜딩 페이지 구현 플랜 | +## 봇 배포 (CI/CD) + +- **파이프라인**: `dev` push → CI Gate (lint+typecheck+test) → ECR 빌드(ARM64) → SSH 배포 +- **트리거**: `packages/bot/**`, `packages/shared/**` 변경 시 + `workflow_dispatch` +- **EC2**: illdan-mgmt (t4g ARM64), `~/study-admin-bot/deploy.sh` + `.env` +- **ECR**: `101548339709.dkr.ecr.ap-northeast-2.amazonaws.com/study-admin-bot` +- **deploy.sh**: ECR 로그인 → pull → 컨테이너 교체 → health check → Discord 웹훅 알림 +- **주의**: `deploy/bot/deploy.sh`는 커밋하지 않음 (EC2에 직접 배치) + ## docs 파일명 컨벤션 `yy-mm-dd-{설명}.md` — 예: `26-03-03-system-architecture.md` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d747ac4..e3f31e1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -31,7 +31,7 @@ graph TB end end - subgraph Bot["Discord Bot · AWS EC2"] + subgraph Bot["Discord Bot · AWS EC2 (Docker)"] SCH["Schedulers
pg-boss"] EVT["Event Handlers
discord.js v14"] SVC["Service Layer
RSS · Fine · Curation · Score"] @@ -126,7 +126,7 @@ graph LR | 패키지 | 설명 | 배포 | |--------|------|------| | `packages/shared` | Drizzle 스키마, 타입, 유틸 | npm (workspace 내부) | -| `packages/bot` | Discord 봇 (스케줄러 + 이벤트 핸들러, 슬래시 커맨드 없음) | AWS EC2 | +| `packages/bot` | Discord 봇 (스케줄러 + 이벤트 핸들러, 슬래시 커맨드 없음) | AWS EC2 (Docker) | | `packages/web` | Next.js 대시보드, API Routes | Vercel | ## 인증 아키텍처 @@ -435,12 +435,22 @@ erDiagram ```mermaid graph LR + subgraph GHA["GitHub Actions"] + CI["CI Gate
(lint+typecheck+test)"] + ECR_PUSH["Docker Build
→ ECR Push (ARM64)"] + SSH["SSH Deploy"] + end + subgraph Vercel["Vercel (ICN)"] WEB_DEPLOY["@blog-study/web
Next.js"] end - subgraph EC2["AWS EC2"] - BOT_DEPLOY["@blog-study/bot
discord.js + pm2"] + subgraph EC2["AWS EC2 (t4g)"] + BOT_DEPLOY["@blog-study/bot
Docker Container"] + end + + subgraph AWS["AWS"] + ECR["ECR
study-admin-bot"] end subgraph Supabase["Supabase (ap-northeast-2)"] @@ -448,6 +458,11 @@ graph LR SA["Supabase Auth"] end + CI --> ECR_PUSH + ECR_PUSH --> ECR + ECR_PUSH --> SSH + SSH --> BOT_DEPLOY + BOT_DEPLOY -->|pull| ECR WEB_DEPLOY --> PG WEB_DEPLOY --> SA BOT_DEPLOY --> PG diff --git a/packages/bot/.dockerignore b/packages/bot/.dockerignore deleted file mode 100644 index 392f954..0000000 --- a/packages/bot/.dockerignore +++ /dev/null @@ -1,36 +0,0 @@ -# Dependencies -node_modules - -# Build output (will be built in container) -dist - -# Development files -*.test.ts -*.spec.ts -vitest.config.ts -tsconfig.tsbuildinfo - -# IDE -.vscode -.idea - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log -npm-debug.log* - -# Environment files (use Railway environment variables) -.env -.env.* -!.env.example - -# Git -.git -.gitignore - -# Documentation -*.md -!README.md diff --git a/packages/bot/Dockerfile b/packages/bot/Dockerfile index b591372..6a0268b 100644 --- a/packages/bot/Dockerfile +++ b/packages/bot/Dockerfile @@ -1,7 +1,6 @@ # Build stage -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder -# Install pnpm RUN corepack enable && corepack prepare pnpm@9.0.0 --activate WORKDIR /app @@ -11,7 +10,6 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY packages/bot/package.json ./packages/bot/ -# Install dependencies RUN pnpm install --frozen-lockfile # Copy source code @@ -19,32 +17,26 @@ COPY tsconfig.json ./ COPY packages/shared ./packages/shared COPY packages/bot ./packages/bot -# Build shared package first, then bot -RUN pnpm --filter @blog-study/shared build -RUN pnpm --filter @blog-study/bot build +# Build shared → bot +RUN pnpm --filter @blog-study/shared build && \ + pnpm --filter @blog-study/bot build # Production stage -FROM node:20-alpine AS runner +FROM node:22-alpine AS runner -# Install pnpm RUN corepack enable && corepack prepare pnpm@9.0.0 --activate WORKDIR /app -# Copy workspace configuration COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY packages/bot/package.json ./packages/bot/ -# Install production dependencies only RUN pnpm install --frozen-lockfile --prod -# Copy built files COPY --from=builder /app/packages/shared/dist ./packages/shared/dist COPY --from=builder /app/packages/bot/dist ./packages/bot/dist -# Set environment ENV NODE_ENV=production -# Run the bot CMD ["node", "packages/bot/dist/index.js"] diff --git a/packages/bot/railway.json b/packages/bot/railway.json deleted file mode 100644 index d8af1cd..0000000 --- a/packages/bot/railway.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://railway.app/railway.schema.json", - "build": { - "builder": "DOCKERFILE", - "dockerfilePath": "packages/bot/Dockerfile", - "watchPatterns": [ - "packages/bot/**", - "packages/shared/**" - ] - }, - "deploy": { - "startCommand": "node packages/bot/dist/index.js", - "healthcheckPath": null, - "healthcheckTimeout": 300, - "restartPolicyType": "ON_FAILURE", - "restartPolicyMaxRetries": 10 - } -}