Головна / Статті

Розгортання NestJS на Bun та Prisma 7 у Cloud Run без помилок під час збірки

Робочий пайплайн GitHub Actions для розгортання додатку NestJS на Bun із Prisma 7 та Neon у Cloud Run, а також виправлення проблем з Docker та підключеннями, які створюють труднощі у командах.

Видавець: REACTAPP.TOP5 хв читання1259 слів

Джерела: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run

Робота NestJS на Bun із підтримкою Prisma 7 та безсерверного Postgres на кшталт Neon, а також його розгортання на Google Cloud Run через GitHub Actions — це швидкий процес у всіх відношеннях. Проблема полягає у тому, що ця комбінація ще достатньо нова, тож проблеми, які виникають, є незнайомими, і більшість команд витрачає день на помилки під час створення Docker-образу або проблеми з підключенням до Prisma перед першим успішним розгортанням. Цей посібник надає вам функціональну схему обробки та пояснює секрети її роботи, щоб ви могли пропустити цей день.

Як формується схема обробки

Метою є автоматизований процес без участі людини: достатньо опублікувати код у відповідну гілку для розгортання, і автоматизація виконає міграції бази даних, створить легкий Docker-образ, завантажить його до Artifact Registry та розгорне на безсерверній службі. Порядок виконання кроків має значення, а деякі з них потребують особливого підходу через особливості роботи Prisma та Bun усередині контейнера.

Ефективна робота генератора Prisma під час збірки в Docker

Створення контейнерів для Prisma має одну постійну проблему. Коли виконується команда prisma generate, програма потребує рядка підключення до бази даних, щоб проаналізувати схему та створити клієнт. Під час збірки немає причин надавати їй справжні облікові дані для продакшену, а вбудовування секретів у шар зображення — це те, чого слід уникати, адже все, що знаходиться в розповсюджуваному зображенні, можна згодом витягнути.

Рішенням є синтаксично коректний, але фальшивий URL, який існує лише під час збірки. Наведений нижче багатоступеневий Dockerfile встановлює залежності на основі зафіксованого файлу lockfile, встановлює тимчасовий параметр DB__URL, який вказує на неіснуючу локальну базу даних, лише для того, щоб генератор працював коректно, створює додаток та копіює лише скомпільований результат у легке зображення для виконання.

# Stage 1: Builder
FROM oven/bun:1 AS builder
WORKDIR /app

COPY package.json bun.lock ./
COPY prisma ./prisma
COPY prisma.config.ts ./

RUN bun install --frozen-lockfile

# The "Prisma Satisfaction" Trick:
# Prisma 7 needs to see a URL format during the build phase to generate the client.
ENV DB__URL="postgresql://dummy:dummy@localhost:5432/dummy"

RUN bunx prisma generate

COPY . .

RUN bun run build

# Stage 2: Runner (Slim)
FROM oven/bun:1-slim AS runner
WORKDIR /app

# Only copy what is strictly necessary to keep the image small
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./

ENV NODE_ENV=production
ENV PORT=8080

CMD ["bun", "run", "dist/main.js"]

Важливі два фактори. Багатоетапний процес створення використовує повне зображення Bun та працює на bun:1-slim, що дозволяє зберегти кінцеве зображення компактним та скоротити можливості для атакувача. Крім того, фіктивний рядок задовольняє вимоги генератора, оскільки справжні облікові дані ніколи не потрапляють у шари системи — генератору на етапі створення потрібна лише структура URL до Postgres, а не доступний сервер.

Забезпечення безпеки облікового запису сервісу

Пайплайн автентифікується в Google Cloud за допомогою облікового запису сервісу, і буває спокуса надати йому широкі права, щоб усунути помилки. Протистояйте цьому. Дотримуючись принципу мінімальних привілеїв, надайте йому лише ті ролі, які необхідні для розгортання, щоб витекла ключ не дозволила отримати контроль над усім проектом: лише завантаження зображень до Artifact Registry та розгортання в Cloud Run, більше нічого.

Повний процес GitHub Actions

Перед першим запуском зберігайте значення, на які посилається робочий процес — такі як ключ облікового запису сервісу та URL бази даних — у зашифрованих секретах репозиторію. Завдання завантажує код, налаштовує Bun, автентифікується у Google Cloud, виконує міграцію Prisma, автентифікує Docker у Artifact Registry, створює та відправляє образ, розгортає його у Cloud Run та видаляє старі образи. Джерело позначає цей фрагмент як SQL; це YAML-робочий процес GitHub Actions, а підказка нижче правильно його класифікує.

name: Deploy Server to Cloud Run

on:
  push:
    branches: ['main']

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  REGION: asia-south1
  REPO_NAME: cloud-run-source-deploy
  SERVICE_NAME: server

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2

      # STEP: Database Migration
      # Run migrations BEFORE deployment to ensure the DB is ready for new code.
      - name: Run Prisma Migrations
        run: |
          bun install --frozen-lockfile
          bunx prisma generate
          bunx prisma migrate deploy
        env:
          DB__URL: ${{ secrets.DB__URL }}

      - name: Docker Auth
        run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev

      - name: Build and Push
        run: |
          IMAGE_URI=${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.SERVICE_NAME }}:latest
          docker build -t $IMAGE_URI .
          docker push $IMAGE_URI
          echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV

      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: ${{ env.SERVICE_NAME }}
          region: ${{ env.REGION }}
          image: ${{ env.IMAGE_URI }}
          flags: '--port=8080 --allow-unauthenticated'
          env_vars: |
            DB__URL=${{ secrets.DB__URL }}
            # Add placeholders for other secrets below:
            # SMTP__HOST=${{ secrets.SMTP__HOST }}
            # AUTH__TOKEN_SECRET=${{ secrets.AUTH__TOKEN_SECRET }}

      # STEP: Housekeeping (Optional)
      # Deletes old image digests to save on storage costs.
      - name: Delete Old Images
        shell: bash
        run: |
          IMAGE_PATH="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.SERVICE_NAME }}"
          gcloud artifacts docker images list "$IMAGE_PATH" --sort-by=~create_time > all_images.txt
          awk 'NR>2 {print $2}' all_images.txt > digests_to_delete.txt

          if [ -s digests_to_delete.txt ]; then
            while read digest; do
              if [[ "$digest" == sha256:* ]]; then
                echo "Deleting old image: $digest"
                gcloud artifacts docker images delete "$IMAGE_PATH@$digest" --delete-tags --quiet || true
              fi
            done < digests_to_delete.txt
          fi

Чому складні кроки мають своє місце

Виконання команди prisma migrate deploy як окремого кроку перед тим, як новий образ почне працювати, значно безпечніше, ніж міграція зсередини контейнера під час запуску. Якщо міграція зазнає невдачі, процес розгортання зупиняється, а поточна версія продовжує обробляти запити; якщо ж міграція виконується під час запуску контейнера, невдала міграція може призвести до зупинки сервісу, поки вже починає працювати новий код.

Останній крок очищення полягає у видаленні старих хешів образів, адже у активному середовищі накопичуються збережені дані, що призводить до зростання витрат на зберігання. Це справді необов’язково: якщо ви покладаєтесь на зберігання кожної історичної версії для можливості повернення, можете пропустити цей крок та оплачувати лише необхідне зберігання.

Останнім способом тихої несправності є вичерпання кількості з’єднань. Cloud Run швидко масштабує інстанції, і кожна з них відкриває власні з’єднання, тому вказування прямої адреси бази даних може призвести до перевищення її ліміту з’єднань під час сплеску навантаження. У Neon використовуйте строк з’єднань із пулом у полях DB__URL, щоб багато короткочасних інстанцій ділилися обмеженим пулом. Це стосується будь-якої конфігурації від серверлесс-сервісів до Postgres.

Ключові висновки

  • Під час створення проекту надайте генератору Prisma фіктивну, правильно сформовану адресу бази даних, щоб жодні справжні конфіденційні дані не потрапляли до шару зображень.
  • Створюйте проект із повним образом Bun та запускайте його у легковазній версії, щоб зберегти розмір зображень мінімальним.
  • Виконуйте міграції як окремий крок перед розгортанням, щоб несправність зупиняла процес релізу, а не пошкоджувала діючий сервіс.
  • Використовуйте строк з’єднань із пулом для серверлесс-цілей та обмежте права облікової запису сервісу розгортання мінімальними привілеями.
  • Bun у стадії розробки та Prisma 7 на момент написання залишаються найсучаснішими інструментами; перевірте поведінку інструменту за актуальною документацією.