Strona główna / Artykuły

Wdrażanie NestJS na Bun i Prisma 7 do Cloud Run bez błędów kompilacji

Działający pipeline GitHub Actions do wypuszczania aplikacji NestJS na platformie Bun z Prisma 7 i Neon do Cloud Run, wraz z poprawkami dotyczącymi Dockera oraz połączeń, które sprawiają problemy zespołom.

Wydawca: REACTAPP.TOP5 min czytania1259 słów

Źródła: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run

Zapусk NestJS na Bun, wspierany przez Prisma 7 oraz serwerless Postgres takiego jak Neon, a następnie jego rozprowadzanie na Google Cloud Run za pomocą GitHub Actions jest szybkie pod każdym względem. Problem polega na tym, że ta kombinacja jest na tyle nowa, iż jej błędy są mało znane, więc większość zespołów traci dzień na błędy kompilacji Dockera lub problemy z połączeniem z Prismą przed pierwszym udanym wdrożeniem. Ten przewodnik przedstawia działający pipeline oraz wyjaśnia zasady jego funkcjonowania, dzięki czemu można ominąć ten dzień.

Jak kształtuje się pipeline

Celem jest proces bez udziału człowieka: wystarczy zrobić push do gałęzi przeznaczonej do wdrożenia, a automatyzacja wykona migracje bazy danych, stworzy lekką obraz Dockera, prześle go do Artifact Registry i wdroży go w usłudze typu serverless. Kolejność kroków jest istotna, a kilka z nich wymaga specjalnego potraktowania ze względu na zachowanie Prismy i Bun wewnątrz kontenera.

Zaspokojenie generatora Prisma podczas budowania w Dockerze

Containerizacja Prismy ma jeden trwały problem. Gdy uruchamia się polecenie prisma generate, wymaga ono łańcucha połączenia do bazy danych, aby móc przetworzyć schemat i stworzyć klienta. Podczas procesu budowania nie ma powodu, by przekazywać mu prawdziwe dane dostępu do produkcji, a umieszczanie tajemnic w warstwie obrazu to coś, czego należy unikać, ponieważ wszystko, co znajduje się w obrazie przeznaczonym do dystrybucji, może zostać później wydobyte.

Rozwiązaniem jest składniowo poprawny, ale fałszywy adres URL, który istnieje tylko podczas budowania. Poniższy wielostopniowy plik Dockerfile instaluje zależności na podstawie zamrożonego pliku lockfile, ustawia tymczasowy parametr DB__URL wskazujący na nieistniejącą lokalną bazę danych wyłącznie po to, by generator działał poprawnie, buduje aplikację i kopiuje tylko skompilowany wynik do lekkiego obrazu runtime.

# 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"]

Dwa tamte decyzje są istotne. Proces budowania w kilku etapach wykorzystuje pełny obraz Bun i działa na bun:1-slim, co sprawia, że końcowy obraz pozostaje mały i ogranicza powierzchnię, którą może atakować napastnik. Ponadto sztuczna strona spełnia wymagania generatora, bez konieczności wprowadzania prawdziwych danych uwierzytelniających do żadnej warstwy, ponieważ generator potrzebuje jedynie struktury adresu URL do Postgresa w momencie budowania, a nie dostępnego serwera.

Zabezpieczanie konta usługowego

Pipeline autoryzuje się w Google Cloud za pomocą konta usługowego, i jest kuszące przyznać mu szerokie uprawnienia, aby błędy zniknęły. Odpieraj to pragnienie. Zgodnie z zasadą najmniejszych uprawnień, przyznaj mu tylko te role, których potrzebuje proces implementacji, aby wyciek klucza nie mógł zostać wykorzystany do przejęcia kontroli nad całym projektem: tylko wysyłanie obrazów do Artifact Registry i ich wdrażanie w Cloud Run, nic więcej.

Całkowity proces GitHub Actions

Zanim rozpocznie się pierwsze uruchomienie, należy zapisać wartości, do których odwołuje się proces pracy – takie jak klucz konta usługowego i adres URL bazy danych – jako szyfrowane tajemnice w repozytorium. Zadanie pobiera kod, konfiguruje narzędzie Bun, autoryzuje się w Google Cloud, uruchamia migrację Prisma, autoryzuje Docker wobec Artifact Registry, buduje i wysyła obraz, implementuje go w Cloud Run oraz usuwa stare obrazy. Źródło oznaczyło ten fragment jako SQL; jest to workflow w formacie YAML z GitHub Actions, a poniższa wskazówka poprawnie go sklasyfikowała.

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

Dlaczego te skomplikowane kroki są konieczne

Wykonywanie polecenia prisma migrate deploy jako odrębnego kroku przed uruchomieniem nowego obrazu jest znacznie bezpieczniejsze niż migrowanie z wnętrza kontenera podczas jego uruchamiania. Jeśli migracja zawiedzie w procesie, działanie deploy zostaje zatrzymane, a obecna wersja nadal obsługuje ruch; migrując natomiast podczas startu kontenera, błędna migracja może wyłączyć usługę w momencie, gdy nowy kod już się uruchamia.

Ostatni krok porządkujący polega na usuwaniu starych hashów obrazów, ponieważ w intensywnie wykorzystywanym środowisku gromadzą się przechowywane pliki, co powoduje wzrost rachunku za przechowywanie. Jest to rzeczywiście opcjonalne: jeśli polegasz na przechowywaniu każdej historycznej wersji kodu do możliwości cofnięcia się, możesz to pominąć i po prostu płacić za przechowywanie.

Ostatnim spokojnym modelem awarii jest wyczerpanie połączeń. Cloud Run szybko skaluje instancje, a każda z nich otwiera własne połączenia, więc podanie bezpośredniej adresacji bazy danych może doprowadzić do przekroczenia jej limitu połączeń podczas nagłego wzrostu obciążenia. W Neon należy używać łącznika połączeń z pulą w polu DB__URL, aby wiele krótkotrwałych instancji dzieliło się ograniczoną pulą połączeń. Dotyczy to każdego rozwiązania typu serverless-to-Postgres.

Główne wnioski

  • Podczas budowania podaj generatorowi Prisma fałszywy, poprawnie sformatowany adres bazy danych, aby żaden prawdziwy sekret nie trafił do warstwy obrazu.
  • Buduj z pełnym obrazem Bun i uruchamiaj w wersji lżejszej, aby utrzymać małe rozmiary obrazów.
  • Wykonuj migracje jako odrębny krok przed wdrożeniem, aby awaria zatrzymała proces publikacji, a nie uszkodziła działający serwis.
  • Używaj łącznika połączeń z pulą dla celów serverless i ogranicz uprawnienia konta służbowego używanego do wdrożeń do minimum koniecznego.
  • Bun w produkcji oraz Prisma 7 są nadal najnowocześniejszymi rozwiązaniami w momencie pisania tego tekstu; sprawdź ponownie zachowanie narzędzia na podstawie aktualnych dokumentacji.