본문 바로가기

프로젝트/잔소리

NextJS 도커 배포하기(GCP Cloud Run + Cloud SQL+Postgresql + prisma)

반응형

 

[들어가기 전] Cloud Run 배포 

많은 배포 방식 중 이 방식을 선택한 이유는 특정 환경에 제한되지 않고 도커 컨테이너화애플리케이션을 어떤 환경에서든 동일한 조건으로 배포할 수 있다는 점,  Cloud Run 의 자체적인 인스턴스 확장 및 축소 기능을 통해 과도하게 몰리는 트래픽 문제나 그에 따른 유지보수 문제 등을 서버리스 방식으로 자동으로 완전 관리할 수 있다는 장점이 돋보였기 때문입니다.

 

따라서 이번 포스트는 Cloud Run 과 Cloud SQL 을 사용하여 NextJS 프로젝트를 배포하는 시간이 되겠습니다.

이미 많은 부분이 GCP 문서에서 자세하게 다루고 있기 때문에, 너무 과정이 생략되어 알아보기 힘들고, 찾아보기 힘든 부분만 별도로 정리하며, 나머지는 링크를 달아두는 형식으로 정리하였습니다.

우선 들어가기 전에 Cloud Run 이 무엇인지 간략하게 알아보면 좋을 듯하여 추가적으로 정리해보았습니다. 

 

Cloud Run

개념

구글의 클라우드 런(Google Cloud Run)은 도커 컨테이너로 애플리케이션을 배포하는 서비스입니다. 클라우드 런은 서버리스 컴퓨팅 플랫폼으로, 컨테이너 이미지를 사용하여 애플리케이션을 실행합니다. 이를 통해 사용자는 인프라를 관리할 필요 없이, 컨테이너 이미지 배포와 스케일링을 쉽게 할 수 있습니다.

 

또한, AWS 람다와 마찬가지로 특정 이벤트 트리거를 등록할 수 있어서 활용에 따라서 범용적으로 사용이 가능하다는 부분이 돋보입니다.

 

주요 기능과 특징

클라우드 런의 주요 기능과 특징은 다음과 같습니다

컨테이너 기반: 클라우드 런은 도커 컨테이너를 기반으로 하여, 다양한 언어와 런타임을 지원합니다. 사용자는 자신이 만든 도커 이미지를 클라우드 런에 배포할 수 있습니다.

서버리스: 인프라 관리 없이 코드에 집중할 수 있습니다. 구글 클라우드가 자동으로 인프라를 관리하고, 트래픽에 따라 자동으로 스케일링합니다.

빠른 배포: 컨테이너 이미지만 준비되면 클라우드 런에 빠르게 배포할 수 있습니다.

자동 스케일링: 트래픽이 증가하면 자동으로 컨테이너 인스턴스를 추가하고, 트래픽이 감소하면 인스턴스를 줄입니다. 최소 인스턴스 수와 최대 인스턴스 수를 설정할 수 있습니다.

통합: 다른 구글 클라우드 서비스와 쉽게 통합됩니다. 예를 들어, 클라우드 SQL, Pub/Sub, 비전 AI 등의 서비스와 통합할 수 있습니다.

 

 

요약하면, 구글 클라우드 런은 도커 컨테이너를 사용하여 애플리케이션을 배포하고 관리할 수 있는 서버리스 플랫폼입니다. 이를 통해 개발자는 인프라 걱정 없이 코드 작성과 배포에 집중할 수 있도록 해줍니다. 여러모로 장점이 많은 것 같긴 합니다.

 

단점보다는 주의점

다만, 문제는 별도의 설정이 없으면 자동 스케일링을 통해 사이트를 이용하는 유저가 없으면 인스턴스를 0으로 만들기 때문에, 최초 접속 시 콜드 스타드 문제가 발생할 수 있다고 합니다. 그러나, 애초에 필요하다면, 인스턴스의 최소 갯수를 1 개로 지정해두면 문제가 없기 때문에 단점은 아닌 것 같고, 24시간 호스팅이 필요한 서비스라면 고려해볼 필요가 있겠다 정도인 듯 합니다.

 

실제 배포해보며 느낀 개인이 생각하는 장점

Cloud Run 을 사용 시 제일 좋았던 점은 배포된 컨테이너를 버전 별로 관리할 수 있고, 특정 컨테이너에 문제가 발생하는 경우 이전 컨테이너로 빠르게 이전이 가능하다는 장점이 돋보였습니다.

 

또한 배포시 버전의 자동 확장 기능이 있어서, 사용자가 서비스를 이용하지 않는 경우에는 인스턴스를 자동으로 축소하여 0개의 경우 아에 인스턴스를 중지하기 때문에 불필요한 비용 낭비를 줄일 수 있다는 점입니다. 또한, 과도하게 트래픽이 몰리는 경우에도 개발자가 지정한 최대 인스턴스 수 에 따라 트래픽을 분산처리할 수 있다는 이점도 있습니다.

 

이 외에도 각 인스턴스 마다 CPU 나 메모리 등을 상황에 맞게 할당하여 빠른 재배포가 가능하므로 서비스의 규모에 따라서도 스케일 업이 유연하다는 것도 큰 장점으로 보입니다.

 

특히, 도커 컨테이너 방식이기 때문에 특정 플랫폼에 종속적이지 않다는 것이 제일 큰 장점이구요.

 

원래라면 콜드 스타트 문제로 인해 서버가 닫혔다가 열리는 시간에 대기시간이 발생하게 되는데, 이 부분은 최소 인스턴스 개수를 1개로 지정해두면 해결되는 부분이므로 큰 단점은 아니라는 생각도 듭니다. 오히려 확장과 축소가 자유롭기 때문에 비교적 적은 비용을 사용하여 서비스를 운영하고자 한다면, 이 보다 더 좋은 조건이 있을까 싶습니다.

 


그럼 이제부터 Cloud Run 을 기반으로 배포를 시작해보겠습니다.

참고로 GCP 의 경우 모든 서비스가 첫 입문자들도 배포하기 쉽게 문서화가 잘 되어 있습니다. 이번에 정리하는 내용들도 문서를 통해 작성된 것이므로 내용의 정리가 미비하여 이해하기 어려운 부분은 최하단에 첨부해둔 참고자료를 확인하시길 권장합니다.

 

배포 전 환경 설정

기초셋팅

이 부분은 기초셋팅 부분으로 링크를 달아두고 넘어가겠습니다. 문서화가 잘되어 있습니다.

1.  create a Google Cloud project. 을 방문하여 프로젝트를 등록 하거나 생성합니다. 
2.  https://cloud.google.com/sdk/docs/install?hl=ko 을 방문하여 qcloud SDK CLI 를 설치합니다. AWS SDK CLI 와 동일하게 커맨드 기반으로 모든 작업을 처리할 수 있도록 해줍니다.
3.  https://docs.docker.com/docker-hub/quickstart/을 방문하여 도커 데스크톱을 설치합니다. 도커 이미지 빌드 및 풀 등의 작업에 필요합니다.

 

그 외 npm, node 등의 기본 패키지의 설치 유무를 확인 합니다. 

 

프로젝트 도커 설정

앞으로 작성되는 파일은 모드 NextJS 프로젝트 루트 경로에 둡니다. 즉, src 폴더와 병렬적으로 배치합니다.

 

Dockerfile 작성

현재 만들어진 NextJS 프로젝트에 들어가서 루트 경로에서 도커 파일을 생성 후 이미지 빌드 및 컨테이너 생성을 위한 옵션을 설정해줍니다.

# 경량 Alpine Linux 기반의 Node.js 이미지를 사용(본인이 사용하는 환경에 맞게)
FROM node:20-alpine 

# 컨테이너 내에서 /app 디렉토리를 작업 디렉토리로 설정
WORKDIR /app

# 나머지 소스코드(.)를 모두 /app(.) 으로 복사
COPY . .

# package.json과 package-lock.json 파일을 작업 디렉토리(/app)로 복사
COPY package*.json ./

# 의존성 설치
RUN npm install

# 빌드
RUN npm run build

# 컨테이너를 노출할 포트 번호
EXPOSE 3000

# 앱 실행
CMD ["npm", "start"]

 

Docker Ignore 파일 작성

gitignore 와 동일하게 Docker 이미지 빌드 시 보안상 문제가 되거나 불필요한 이미지 크기를 차지할 수 있는 파일을 설정합니다.

# 여기에 포함된 파일을 제외한 모든 소스 코드는 도커 이미지 빌드 시 작업 디렉토리에 복사되게 됩니다.
Docker
.dockerignore
node_modules

 

도커 이미지 빌드와 도커 컨테이너 실행/중지

이미지 빌드

앞서 설정한 도커파일을 기반으로 이미지 빌드를 시도 합니다. 이미지 이름 이후에 (공백) . 을 꼭 넣어줍시다. 

# nextjs-nagging-project-gcr : 이것은 사용자 작명으로 사용할 이미지명을 지정합니다. 원하는 이미지 이름을 설정해주세요
docker build -t nextjs-nagging-project-gcr .

 

정상적으로 실행이 되면 아래 로그가 표시 됩니다. 이미지가 큰 만큼 몇 분 정도의 시간이 걸릴 수 있습니다.

 

이미지 빌드가 완료되고 나서 docker images 라고 입력하시면, 방금 생성된 따끈따끈한 이미지를 확인할 수 있습니다.

 

도커 컨테이너 실행

도커 이미지를 실행하면 도커 컨테이너가 됩니다. 즉, 컨테이너를 실행하기 전의 상태를 도커 이미지라 부르고, 이미지를 실행하여 실제 활성화(컨테이너화) 상태로 만든다면 이 때 부터 실행된 이미지를 도커 컨테이너라 부릅니다.

 

이미지를 실행하려면 아래와 같이 명령어를 입력해줍니다. 로컬의 3000 포트를 도커 컨테이너 내부의 로컬 3000 포트로 포워딩하며, 실행된 서버는 백그라운드에서 관리하겠다는 의미입니다.

docker run -dp 3000:3000 [이미지 이름]

 

이미지를 실행하고 나면, docker ps 명령어를 입력하여 활성화된 컨테이너 목록을 확인할 수 있습니다. 아래와 같이 앞서 실행된 이미지명이 표시되면 성공입니다.

 

현재 컨테이너가 3000 포트로 실행중이므로, localhost:3000 을 브라우저 URL 창에 입력 후 들어가면, 서버가 정상적으로 실행됨에 따라 마치 로컬에서 구동한 것과 같이 정상적으로 사이트가 렌더링됩니다.

 

도커 컨테이너 중지

도커 컨테이너를 중지 하려면 docker stop [컨테이너 ID] 형식으로 적어줍니다. ID 확인은 dokcer ps 를 입력하면 바로 확인할 수 있습니다.

이 때 GCP 와 연동하기 위해서는 도커 컨테이너가 실행된 상태이어야 하므로 실행을 유지한 상태로 다음 단계로 넘어갑니다.

 

본격적인 배포 직전 밑작업

 gcloud CLI  를 통한 셀 다루기 |  프로젝트에 접근하기

배포 준비 단계에서 gcloud CLI 를 설치하라는 언급이 있었습니다. 해당 cli 를 이용해서 cloud build 를 통해 로컬에서 실행중인 도커 이미지를 빌드 하여 도커 파일을 저장하는 GCP의 아티팩트 레지스트리에 올릴 것입니다.

 

우선 로컬 터미널(bash) 에서 gcloud config set project [projectId] 형식으로 입력 해줍니다.  여기서 projectId 는 GCP 에서 프로젝트를 생성 후 시작하기 화면에서 볼 수 있습니다.

 gcloud config set project [projectId]

 

 

실행 후 아래와 같이  Updated 부분이 뜬다면 성공입니다.

 

 

Artifacts 레지스트리 내  프로젝트 배포를 위한 리포지토리 생성

아래 형식에 맞춰  터미널에 입력하여 리포지토리를 생성해줍니다. 이는 향후 GCP 서비스 중 하나인 Cloud Build 와 연계하여 도커 이미지를 빌드 후 배포하는 데 사용할 것입니다. 참고로 --location 은 리전 이름입니다. 배포할 리전과 동일하게 입력해줍니다. 예시는 서울 리전을 의미합니다.

gcloud artifacts repositories create [생성할 리포지토리 이름] --repository-format=docker --location=asia-northeast3 --description="Docker respository"

 

❗ 주의
서울 리전의 경우에는 커스텀 도메인을 설정할 수 없으므로 asia-northeast1(도쿄) 를 선택하면 커스텀 도메인 설정이 가능합니다. 향후 서울 리전에 커스텀 도메인이 나오길 기대해야 겠네요.

 

 

성공적으로 생성이 완료 되었다면(https://console.cloud.google.com/artifacts), 아티팩트 레지스트리에 접속하면 도커를 담을 수 있는 빈 폴더가 생성된 것을 볼 수 있어야 합니다. 앞서 입력한 명령어와 매칭해보시고, 설정한 옵션 중 누락된 것이 없는지 꼭 확인해봅시다.

 

커맨드로 확인하고 싶다면,  gcloud artifacts repositories list  라고 커맨드 창에서 입력해도 동일하게 표시되는 것을 볼 수 있습니다.

# 저장된 아티팩트 목록을 나열합니다.
gcloud artifacts repositories list

참고로 위에서 로케이션(리전)이 asia-northeast3(대한민국 서울)로 되어 있는데, 커스텀 도메인을 연결하고자 한다면, 파이어베이스 호스팅, 외부 리전 로드밸런서를 사용하거나 아에 커스텀 도메인 자체 맵핑이 가능한 리전을 선택해서 해야합니다. 대한민국과 가장 가까운 도메인 맵핑 지원 리전은 24.08.13 기준으로 asia-northeast1(일본의 도쿄) 입니다.

 

Cloud Build 를 통한 도커 이미지 업로드

앞서 폴더 생성이 끝났다면, 이제 해당 폴더에 앞서 로컬 프로젝트에서 생성한 이미지를 도커 허브에서 pull 하여 받아오는 작업을 시도할 것입니다.

 

다음 명령어 형식에 맞춰 터미널에 잘 입력해줍니다. 여기서 --tag도커 이미지를 보관할 경로를 설정하는 것 입니다.

#ex) gcloud builds submit --region=asia-east1 --tag asia-northeast3-docker.pkg.dev/nagging/nextjs-nagging-project-gcr/nextjs-docker-gcr-image:tag1
gcloud builds submit --region=asia-east1 --tag [레포지토리 태그]/[이미지를 보관할 폴더 이름]:[태그 이름]

 

 

레포지토리에 대한 태그(--tag)를 확인하려면 . 생성된 레포지토리 폴더를 들어가 줍니다.

 

여기서 태그를 쉽게 식별할 수 있도록 아래 경로를 복사하여 --tag 뒤에 붙여넣기 해주시면 좋습니다.

 

 

성공적으로 실행이 되었다면, 현재 로컬 디렉토리의 Dockerfile 을 기반으로 빌드 프로세스가 진행되는 것을 확인할 수 있습니다.

 

마지막 까지 문제가 없었다면, STATUS: SUCCESS 인 것을 확인할 수 있습니다.

 

 

실제로 지정한 --tag 에 이미지가 정상적으로 들어왔는지 확인해봅시다. 아까 전 까지는 빈 폴더 였는데 지금은 도커 파일이 들어와 있습니다. 성공입니다

 

배포 실행

이제 실제 Cloud Run 에 배포를 진행해봅니다. 현재 제 프로젝트의 경우에는 데이터베이스를 Postgres 를 사용하고 있기 때문에, 이에 대한 설정도 필요한데, 일단은 NextJS 자체만을 배포하는 것에 집중하도록 하겠습니다.

Cloud Deploy 를 통해 Cloud Run 에 배포하기

로컬 프로젝트의 터미널에서 아래와 같은 형식으로 입력해줍니다.

 # ex) gcloud run deploy --iamge=asia-northeast3-docker.pkg.dev/nagging/nextjs-nagging-project-gcr/nextjs-docker-gcr-image@sha256:bb2ee17fsdfsdfsfdsfsdfsdbbtert:tag1
 gcloud run deploy --image=[이미지이름]:[태그명]

 

 

이때 이미지 이름의 경우에는 앞서 설치된 도커파일 내부로 들어가시면 상단에 엄청 긴 sha 까지 나온 레포지토리가 보입니다. 이를 복사하여 붙여넣어주면 됩니다.

 

환경변수 설정

앞서 단계 까지 수행했다면, 컨테이너 배포 자체는 성공적으로 마무리 되었습니다. 이제 부터는 별도의 환경변수를 설정해서 배포 시 적용할 수 있도록 해보겠습니다.

 

우선 Cloud run 대시보드에서 생성된 서비스를 클릭하여 들어가줍니다.

 

상단에 보시면 [새 버전 수정 및 배포] 가 보이는데 이를 클릭해줍니다.

 

아래로 스크롤 하다면 [변수 및 보안 비밀] 이 보이는데 이를 클릭하면, 환경 변수를 설정할 수 있습니다. 이후 하단의 [배포] 를 클릭하면 이미지가 새로 빌드된 컨테이너가 실행되어 반영되는 것을 확인할 수 있습니다.

 

 

인스턴스 생성 외 설정(공개IP)

공식 문서 자체가 워낙 잘 되어 있어서 이 부분은 많이 생략하도록 하겠습니다.

 

참고로, 저의 경우 데이터베이스는 PostgreSQL 을 사용하고 있습니다. 모든 과정에 대한 정보는(https://cloud.google.com/sql/docs/postgres/connect-instance-cloud-run?hl=ko#create-instance) 문서를 참고하면 됩니다. 참고로 저는 공개 IP 를 기반으로 Cloud SQL 설정을 따라갔습니다. 비공개 IP 의 경우에는 보안적으로나 Cloud Run과 SQL 사이의 통신에 있어서 적합한 방식이라 생각되지만, 문제는 두 서비스 간에 커넥터 전용 VPC 의 유지비용이 적지 않으며, 최근에는 공개IP 라고 하더라도 SQL 인증 프록시를 통해서 SSL 등의 조치 없이도 보안을 높게 유지하고, 필요에 따라 외부 접근 자체를 차단할수도 있기 때문에 굳이 VPC 를 쓸 필요는 없어 보입니다.

 

따라서, 24시간 인스턴스를 구동할 필요가 없는 제 서비스의 특성상 굳이 이러한 비용부담을 가지는 것은 불필요하기도 했습니다.

(참고) 최소 비용을 유지하기 위해 데이터베이스의 모든 옵션은 샌드박스 기준으로 최소 설정을 유지합니다. 즉, 공유 CPU : 0.6G,  스토로지: HDD, 스토로지 저장 공간:  10G, 리전: Cloud Run 배포 지역 으로 설정합니다. 이 경우 24시간 1달을 인스턴스 구동 시 최소 달러로 10~13USD 정도 나올 것입니다.  12시간이나 필요한 상황에서만 구동한다면 2달러 미만으로도 가능합니다.

 


 

앞서 언급했듯이 기본적인 설정 과정은 위 공식문서를 통해서 단계별로 알 수 있으니 생략합니다. 다만, 위 과정을 따라가다보면, Cloud Run 과 Cloud SQL 을 연결하기 위해 커넥터 라이브러리를 사용해야 하는데, 이 때 nodejs 를 사용하면서 prisma 로 맵핑된 PostgreSQL 의 경우 예제에 나와있지 않습니다. 따라서 이 부분을 처리하는 과정을 별도로 정리할 것입니다.

 

Cloud SQL 과 Cloud Run 연결을 위한 Cloud SQL 인증 프록시 설정과 연결

진행할 항목의 개요

앞서 예제 링크를 클릭하여 따라 가셨다면, 두 서비스를 서로 연결하기 위해 사용하는 방식인 SQL Auth Proxy 을 적용해야 함을 알 수 있습니다. 또한, 현재 진행하고자 하는 부분 아래 부분 입니다(다만 아래와 같이 샘플 앱을 사용하지 않을 겁니다).

 

이때, 추가적으로 유닉스 소켓 및 커텍터 라이브러리를 이용하여 인증 프록시를 적용할 것입니다. 해당 형태의 인증방식을 적용하면, 다음 장점이 있습니다.

- 안전한 통신: 인증된 네트워크 설정 및 SSL 설정 없이 Cloud Run 인스턴스와 SQL 인스턴스 간에 안정적인 통신이 가능합니다.
- 커텍터: Prisma 와 같은 지원되지 않는 외부라이브러리도 Unix 소켓을 통한 인증 프록시를 적용할 수 있도록 해줍니다(원래라면 불가능하거나 까다롭지만, 구글에서 해당 커텍터 라이브러리를 별도로 지원해주고 있습니다)

 

보통 공개 IP 를 사용하여 접근하게 되면 전송되는 데이터가 외부에 노출되어 보안상 위험에 노출될 가능성이 크지만,  SQL Auth Proxy 를 설정하게 되면 그럴 걱정이 사라집니다. 

 


그럼 이제 설정해보도록 하겠습니다.

커텍터 패키지 설치

SQL Auth Proxy 를 연결하기 위해서 Unix 소켓을 사용해야 합니다. 또한, 프리즈마와 같이 데이터베이스 맵핑 라이브러리를 같이 사용하는 경우 이를 지원하는 방식인 커텍터를 사용해야 합니다. 따라서 이를 위한 NodeJS 전용 커텍터인 구라이브러리를 설치 합니다.

( https://www.npmjs.com/package/@google-cloud/cloud-sql-connector/v/0.2.0 )

npm install @google-cloud/cloud-sql-connector

 

참고로 해당 부분은
여기 구글 클라우드 공식문서(https://cloud.google.com/static/sql/docs/postgres/connect-connectors?hl=ko#node.js) 도 언급하고 있습니다.

 

client.ts 파일 및 기타 prisma 클라이언트 import 구문 수정

client.ts 파일 수정

아마 해당 패키지 예시에서는 간단한 예시만 보일 겁니다. 여기서 프리즈마 연결과 연동하여 커넥션을 설정하려면 아래와 같은 형식으로 파일을 수정해주세요(https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/blob/0dfbb524d736ef5f28ab7c87fd7a95223a989cb6/examples/prisma/postgresql/connect.ts).

참고) client.ts 파일과 동일 경로에 있는 schema.prisma 는 변경 사항이 없습니다.
// prisma/client.ts

import { resolve } from 'node:path';
import {
    AuthTypes,
    Connector,
    IpAddressTypes,
} from '@google-cloud/cloud-sql-connector';
import { PrismaClient } from '@prisma/client';

// reference: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/blob/0dfbb524d736ef5f28ab7c87fd7a95223a989cb6/examples/prisma/postgresql/connect.ts
export async function connect({ instanceConnectionName, user, database }: { instanceConnectionName: string, user: string, database: string }) {
    const path = resolve('.s.PGSQL.5432'); // postgres-required socket filename
    const connector = new Connector();
    await connector.startLocalProxy({
        instanceConnectionName, // DB 연결용 인스턴스 이름
        ipType: IpAddressTypes.PUBLIC, // 공개 IP 접근 명시
        authType: AuthTypes.IAM, // DB 인스턴스 접근 시 사용자 IAM 을 사용하여 접근토록 설정
        listenOptions: { path }, // postgres 소켓 경로 설정(포트 5432로 연결)
    });

    const datasourceUrl = `postgresql://${user}@localhost/${database}?host=${process.cwd()}`;
    const prisma = new PrismaClient({ datasourceUrl });

    // 커텍터 연결이 완료된 후(인증이 완료 되었으므로) 연결을 종료합니다. 이후 부터는 prisma 클라이언트 사용 시 자동 온/오프 됩니다.
    return {
        prisma,
        async close() {
            await prisma.$disconnect();
            connector.close();
        },
    };
}

 

 

참고로 저는 아래와 같이 환경변수를 직접 바인딩 하였습니다. 또한, GCP Cloud Run 의 경우 여러 개의 인스턴스가 동시에 5432포트와 같이 동일한 포트 연결을 시도할 수 있으므로, 이에 대한 중복을 미연에 방지하는 로직을 별도로 추가하였습니다. 그리고 로컬에서는 Cloud SQL 이 아닌 로컬 데이터베이스를 사용하도록 분기처리하는 부분도 추가되었습니다.

import { resolve } from 'node:path';
import {
    AuthTypes,
    Connector,
    IpAddressTypes,
} from '@google-cloud/cloud-sql-connector';
import { PrismaClient } from '@prisma/client';


// 중복 인스턴스 생성을 방지하기 위해 전역변수로 관리합니다.
let prisma: PrismaClient | null = null;
let connector: Connector | null = null;

// reference: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/blob/0dfbb524d736ef5f28ab7c87fd7a95223a989cb6/examples/prisma/postgresql/connect.ts
export async function connect() {
	
    // 만일 이미 연결된 인스턴스가 있으면 해당 인스턴스를 반환합니다(매우 중요).
    if(prisma) {
    	return {prisma, close}
    }

   // 프로덕션인 경우 Cloud SQL 인증을 시도합니다.
   if(process.env.NODE_ENV === 'production') {
    const path = resolve('.s.PGSQL.5432'); // postgres-required socket filename
    connector = new Connector();
    await connector.startLocalProxy({
        instanceConnectionName: process.env.INSTANCE_CONNECTION_NAME||'', // DB 연결용 인스턴스 이름
        ipType: IpAddressTypes.PUBLIC, // 공개 IP 접근 명시
        authType: AuthTypes.IAM, // DB 인스턴스 접근 시 사용자 IAM 을 사용하여 접근토록 설정
        listenOptions: { path }, // postgres 소켓 경로 설정(포트 5432로 연결)
    });

    const datasourceUrl = `postgresql://${process.env.DB_USER}:${process.env.DB_PASS}@localhost/${process.env.DB_NAME}?host=${process.cwd()}`;
    prisma = new PrismaClient({ datasourceUrl });


	} else {
   // 개발 환경인 경우 기존 로컬 방식대로 디비 인증을 시도합니다.
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
    prisma = globalForPrisma.prisma || new PrismaClient();

    globalForPrisma.prisma = prisma;
    
    }
    
  async function close() {
    if (prisma) {
      await prisma.$disconnect();
    }
    if (connector) {
      connector.close();
    }
  }

// 생성된 디비 인스턴스 내보내기
  return { prisma, close };
}

 

참고로, postgres 말고도 다양한 데이터베이스 커넥션에 대한 예제는 

->  https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/blob/HEAD/examples/README.md#prisma

 여기를 방문하여 확인하시면 됩니다.

 

[참고] 그 외 import 구문 수정

이 부분은 각자 파일에서 prisma 클라이언트를 import 하는 방식을 수정하는 것입니다. 저의 경우에는 기존 App 라우터 기반으로 api route 를 사용 시 아래와 같이 prisma 클라이언트 인스턴스를 가져와서 사용하였습니다.

import prisma from '../../../../../prisma/client'; // -> 바꿔야 하는 부분

 

하지만 client.ts 파일에서 구글 SQL 커넥터를 연결하면서, 해당 프리즈마 인스턴스를 내보내는 방식이 변경되었기 때문에 이에 호환되도록 변경해주어야 합니다(물론 기존 구문이 유지되도록 설정했다면 불필요한 과정). 아래 connect 함수가 return 하는 변수 중에 prisma 가 있습니다.

import { connect } from '../../../../../prisma/client';

 

따라서 해당 인스턴스를 사용하는 모든 파일에서 수정하는 작업을 해주어야 합니다

const {prisma, clouse}= await connect();

// 그 외 데이터베이스 접근 및 연산 작업

 

Dockerfile 및 package.json 스크립트 수정

프리즈마를 사용하는 경우 schema.prisma 에 생성된 테이블을 실제 연결된 Cloud SQL 에 반영하도록 해주어야 합니다. 그러기 위해서는 프로덕션 서버가 실행되기 직전 이면서 환경변수가 모두 적용된 시점에 마이그레이션 작업을 시도해주어야 하므로 아래와 같이 각 파일들을 수정해주도록 합니다.

 

참고로 이 방식은 ( https://notiz.dev/blog/prisma-migrate-deploy-with-docker ) 기술 블로그를 참고하였습니다.

 

 

도커파일

# 경량 Alpine Linux 기반의 Node.js 이미지를 사용(본인이 사용하는 환경에 맞게)
FROM node:20-alpine AS builder

# 컨테이너 내에서 /app 디렉토리를 작업 디렉토리로 설정
WORKDIR /app

# package.json과 package-lock.json 파일을 작업 디렉토리(/app)로 복사
COPY package*.json ./
#  Docker 이미지에 복사할 뿐만 아니라 디렉토리도 포함. 이것은 마이그레이션 기록 파일 만 실행하기 때문에 필요
COPY prisma ./prisma/ 

# 의존성 설치
RUN npm install

# 나머지 소스코드(.)를 모두 /app(.) 으로 복사
COPY . .

# 빌드
RUN npm run build

# 컨테이너를 노출할 포트 번호
EXPOSE 3000

# 앱 실행
CMD ["npm", "run", "start:migrate"]

 

패키지

  "scripts": {
    "start": "next start",
    "start:migrate":"prisma migrate deploy && npm run start",
  },

 

재배포하기

이제 콘솔 터미널에서 gcloud run deploy 를 입력하여 변경한 컨테이너를 재배포해줍니다.  사실 해당 명령어는 gcloud 인증 후 즉시 소스코드가 있는 프로젝트 루트 경로에서 실행해도 배포를 자동화 해줍니다. 이 때 루트 경로에 Dockerfile 이 있으면 해당 파일에 적용된 옵션으로 빌드해주며, 도커파일이 없다면 GCP 클라우드에 저장된 기본 도커파일을 불러와서 배포해줍니다.

명령어 입력 시 보이는 첫 줄과 두 줄은 기본값을 적용하면 됩니다. 
PS C:\Users\Administrator\Desktop\개발관련\넥스트 프로젝트\nagging> gcloud run deploy

Deploying from source. To deploy a container use [--image]. See https://cloud.google.com/run/docs/deploying-source-code for more details.
Source code location (C:\Users\Administrator\Desktop\개발관련\넥스트 프로젝트\nagging):  
Next time, use `gcloud run deploy --source .` to deploy the current directory.

Service name (nagging): # 서비스 이름(기본값)
Please specify a region: # 배포할 지역을 선택합니다.
 [1] africa-south1
 [2] asia-east1
 [3] asia-east2
 [4] asia-northeast1 # 도쿄 -> 커스텀 도메인 지원
 [5] asia-northeast2 # 오사카 -> 커스텀 X
 [6] asia-northeast3 # 서울   -> 커스텀X
 [7] asia-south1
 [8] asia-south2
 [9] asia-southeast1
 [10] asia-southeast2
 [11] australia-southeast1
 [12] australia-southeast2 
 [13] europe-central2
 [14] europe-north1
 [15] europe-southwest1
 [16] europe-west1
 [17] europe-west10
 [18] europe-west12
 [19] europe-west2
 [20] europe-west3
 [21] europe-west4
 [22] europe-west6
 [23] europe-west8
 [24] europe-west9
 [25] me-central1
 [26] me-central2
 [27] me-west1
 [28] northamerica-northeast1
 [29] northamerica-northeast2
 [30] southamerica-east1
 [31] southamerica-west1
 [32] us-central1
 [33] us-east1
 [34] us-east4
 [35] us-east5
 [36] us-south1
 [37] us-west1
 [38] us-west2
 [39] us-west3
 [40] us-west4
 [41] cancel
 
Please enter numeric choice or text value (must exactly match list item):  4

# 이후 나머지는 자동으로 Cloud Build 후 Cloud Run 에 배포됩니다.
To make this the default region, run `gcloud config set run/region asia-northeast1`.

Building using Buildpacks and deploying container to Cloud Run service [nagging] in project [nagging] region [asia-northeast1]
OK Building and deploying... Done.
  OK Uploading sources...
  OK Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/buil
  ds/9dea7c-89-4587-af30-8b4b432?project=764442153].
  OK Creating Revision...
  OK Routing traffic...
Done.
Service [nagging] revision [ngging-00-2h9] has been deployed and is serving 100 percent of traffic.
Service URL: https://nagging-62gjdjawha-an.a.run.app
PS C:\Users\Administrator\Desktop\개발관련\넥스트 프로젝트\nagging>

 

결과 이미지

정상적으로 마이그레이션이 실행되었다면, Cloud SQL 의 스튜디오를 통해 테이블이 정상적으로 생성된 것을 볼 수 있어야 합니다.

 

 

[참고] 커스텀 도메인 매핑

혹시 본인이 구매한 커스텀 도메인을 적용하고자 한다면, 아래 문서를 참고해주시면, 됩니다. 

 

https://cloud.google.com/run/docs/mapping-custom-domains?hl=ko&_gl=1*1o3uhy7*_ga*MTM1MzU2OTI3Mi4xNzIwNTgyMTgy*_ga_WH2QY8WWF5*MTcyMzUxMzMwMC4yMi4xLjE3MjM1MTc2MDQuNjAuMC4w#run

 

커스텀 도메인 매핑  |  Cloud Run Documentation  |  Google Cloud

의견 보내기 커스텀 도메인 매핑 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 사용자는 Cloud Run이 배포된 서비스에 제공하는 기본 주소가 아닌 커스텀 도

cloud.google.com

 

[참고] Cloud Run 커스텀 도메인 지원 관련 부분

서울 리전에서 컨테이너를 배포하면 커스텀 도메인을 지원하지 않기 때문에, 불가피하게 로드밸런서를 사용해야 할 수 있습니다 . 로드 밸런서를 사용하지 않는 경우에는, 파이어베이스 호스팅을 사용할 수 있고, 즉시 맵핑하고자 한다면, 아래 리전에 대해서만 즉시 맵핑이 가능합니다(2024.08.13 기준)(https://cloud.google.com/run/docs/mapping-custom-domains?hl=ko&_gl=1*1o3uhy7*_ga*MTM1MzU2OTI3Mi4xNzIwNTgyMTgy*_ga_WH2QY8WWF5*MTcyMzUxMzMwMC4yMi4xLjE3MjM1MTc2MDQuNjAuMC4w#run)

asia-east1
asia-northeast1
asia-southeast1
europe-north1
europe-west1
europe-west4
us-central1
us-east1
us-east4
us-west1

 

트러블 슈팅

Error: listen EADDRINUSE: address already in use /workspace/.s.PGSQL.5432

해당 문제 개선 과정을 정리하였습니다. 아래 포스트를 참고해주세요

 

[잔소리 ] 트러블 슈팅

해당 포스트는..해당 포스트는 프로젝트를 진행하면서 경험한 다양한 문제들과 이를 개선하는 과정를 정리한 포스트 입니다. 모든 이슈를 정리하지는 않으며, 사이사이에 해결하기가 까다롭고

duklook.tistory.com

 

ERROR: (gcloud.builds.submit) FAILED_PRECONDITION: invalid bucket "764500442153.cloudbuild-logs.googleusercontent.com";

해당 에러는 현재 클라우드에 접근하려는 유저의 계정 권한이 없기 때문에 발생하는 문제입니다. 이 경우에는 터미널에서 gcloud auth login 을 입력하면, 구글 로그인 화면이 뜨고, 안내에 따라 처리하게 되면 GCP SDK 사용 권한에 대한 부분이 나올 겁니다. 여기서 누락된 사용권한을 부여받을 수 있도록 해주기만 하면 대부분 해결이 됩니다.

 

ERROR: (gcloud.builds.submit) FAILED_PRECONDITION: failed precondition: due to quota restrictions, cannot run builds in this region

해당 문제는 현재 이용하고자 하는 region 에서는 build 가 제한되어서 발생하는 에러 입니다.

 

이 경우( https://cloud.google.com/build/docs/locations?hl=ko#restricted_regions_for_some_projects ) 페이지의 하단에 제한되는 경우 사용할 수 있는 몇 가지의 대체 리전 중에서 현재 서비스를 배포하고자 하는 위치와 가까운 곳을 대신 선택하면 됩니다.

 

참고하면 좋은 자료

애드센스 당 최대 동시 요청: https://cloud.google.com/run/docs/about-concurrency?hl=ko&_gl=1*co1rj3*_ga*MTM1MzU2OTI3Mi4xNzIwNTgyMTgy*_ga_WH2QY8WWF5*MTcyMzc3NTM3NS4zMy4wLjE3MjM3NzUzNzUuNjAuMC4w

8 가지 JS 프레임워크 Cloud Run 배포: https://www.youtube.com/watch?v=eemS-UTjdb0&t=9s

nodejs 전용 cloud sql 커텍터 라이브러리: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector?tab=readme-ov-file#using-a-local-proxy-tunnel-unix-domain-socketcloud run 전체 가이드 : https://cloud.google.com/run/docs?hl=ko

비공개 IP 를 통한 VPC 커텍터 활용: https://mkdev.me/posts/how-to-connect-cloud-run-and-cloud-sql-internally

리전 제한 오류 시 : https://cloud.google.com/build/docs/locations?hl=ko#restricted_regions_for_some_projects

샘플 GCP RUN 컨테이너 배포: https://cloud.google.com/run/docs/quickstarts/deploy-container?hl=ko&_gl=1*1njx1o0*_ga*MTM1MzU2OTI3Mi4xNzIwNTgyMTgy*_ga_WH2QY8WWF5*MTcyMzA4NzQwMS4xOC4xLjE3MjMwODc0NTkuMi4wLjA.

qcloud cli 사용법: https://cloud.google.com/sdk/docs/initializing?hl=ko

qcloud cli 설치: https://cloud.google.com/sdk/docs/install?hl=ko

cloud run nextjs 배포: https://cloud.google.com/run/docs/develop/frameworks/deploy-nextjs-service?hl=ko

https://cloud.google.com/run?hl=ko

cloud run nextjs 배포2: https://cloud.google.com/run/docs/develop/frameworks/deploy-nextjs-service?hl=ko

 


 

나가는 말

여기까지가 배포의 끝 입니다. 전반적으로 Cloud Run 에 nextjs 프로젝트를 도커 컨테이너로 배포하였고, 추가적으로 PostgreSQL 과 Prisma 가 맵핑된 환경에서 Cloud SQL 과 Cloud Run 을 Unix 소켓을 이용한 SQL Auth Proxy를 사용하여 연동 후 기존 프리즈마 스키마로 정의된 테이블을 실제 Cloud SQL 에 반영하는 마이그레이션 작업까지 시도해보았습니다. 아무리 배포의 대부분을 쉽게 도와주는 도구를 이용하더라도 낯선 환경에서 지원하지 않는 혹은 문서화가 미흡한 부분을 프로젝트 환경에 맞게 수정해 나가는 것은 쉬운 일은 아니었던 것 같습니다. 그래도 끝까지 한다면, 해결하지 못할 문제는 거의 없는 것 같다는 생각이 듭니다. 

 

이 글이 저와 비슷한 상황에 직면한 분들에게 도움이 되길 바라며 글을 줄여 봅니다.

 

 

반응형