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.
Ź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.