한 달 정도 팀프로젝트를 진행했고 1차 개발이 끝나서 배포까지 도전!!
현재 프로젝트 구조가 Spring Boot + React + Node.js 로 되어 있기에
추후 CI/CD 까지 고려하여
서버리스 방식으로 관리가 가장 편리한 Google Cloud Run을 선택하게 되었당.
GCP Compute Engine VS Cloud Run 비교
Compute Engine | Cloud Run | |
서비스 종류 | 가상 머신(VM) | 컨테이너 기반 서버리스 |
내가 직접 관리? | OS, 보안 패치 등 직접 | GCP가 다 해줌 |
Docker 필요 여부 | 없어도 가능 | 필수 (컨테이너 기반) |
배포 방식 | 수동 (SSH 접속 등) | 자동 (gcloud run deploy) |
현재 프로젝트 구조
서비스 | 기능 | 배포 위치 |
React | 프론트, 정적 사이트, Nginx로 서빙 | Cloud Run |
Spring Boot | 백엔드, REST API, DB 연동 가능 | Cloud Run |
Node.js | Puppeteer, 크롤링 등 주기 작업 수행 | Cloud Run |
구현 목표
- React / Spring Boot / Node 각각 Dockerfile 작성
- Artifact Registry에 컨테이너 저장
- Cloud Run으로 서비스 배포
- GitHub + Cloud Build로 CI/CD 자동화 연결 (추후)
도커 허브(Docker Hub) 이미지 Repository 생성
Dockerfile 생성
각각 프로젝트 최상단 루트에 생성해주면 된다. 확장자는 따로 없고 파일명이 Dockerfile 임!!
-Spring Boot
FROM eclipse-temurin:17-jdk-alpine
# 2단계: jar 파일 복사
COPY build/libs/app.jar app.jar
# 3단계: 실행
ENTRYPOINT ["java", "-jar", "/app.jar"]
-React
npm run build --빌드하기
# Step 1: Build the React app
FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Step 2: Serve with nginx
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
}
-Node.js
FROM node:18
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "crawl.js"] # 또는 app.js 등 메인 진입점
2. Docker 이미지 빌드
cd 프로젝트 경로
./gradlew clean build
3. Artifact Registry 생성
gcloud artifacts repositories create 프로젝트이름 \
--repository-format=docker \
--location=asia-northeast3 \
--description="프로젝트 이름 Docker Repo"
배포용 deploy.sh 스크립트 작성
스프링부트, 리액트, node 따로 배포를 하다 보니 각각 명령어가 모두 다르고 번거로움.
각각 배포할 때마다 도커 이미지 푸시, 배포를 따로 해줘야 해서
한방에 자동으로 배포해주는 deploy.sh 파일을 작성하기로 했다.
springboot-deploy.sh
#!/bin/bash
# 설정값
PROJECT_ID="gcp 프로젝트 ID"
REGION="asia-northeast3"
REPO_NAME="Artifact Registry 이름"
IMAGE_NAME="도커 이미지파일 이름"
IMAGE_TAG="1.0"
SERVICE_NAME="서비스 이름"
FULL_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "Gradle 빌드 시작..."
./gradlew clean bootJar
echo "Docker 멀티 아키텍처 이미지 빌드 및 푸시 중..."
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t "$FULL_IMAGE" \
--push .
echo "Cloud Run 배포 중 (환경변수 SPRING_PROFILES_ACTIVE=dev 포함)..."
gcloud run deploy "$SERVICE_NAME" \
--image="$FULL_IMAGE" \
--platform managed \
--region "$REGION" \
--allow-unauthenticated \
--set-env-vars SPRING_PROFILES_ACTIVE=dev
if [ $? -eq 0 ]; then
echo "Spring Boot 서비스 배포 성공!"
else
echo "Spring Boot 배포 실패!"
exit 1
fi
react-deploy.sh
#!/bin/bash
# 설정값
PROJECT_ID="gcp 프로젝트 ID"
REGION="asia-northeast3"
REPO_NAME="Artifact Registry 이름"
IMAGE_NAME="도커 이미지파일 이름"
IMAGE_TAG="1.0"
SERVICE_NAME="서비스 이름"
FULL_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "기존 빌드 폴더 제거 중..."
rm -rf build
echo "React 빌드 중..."
npm run build
echo "Docker 이미지 빌드 및 푸시 (빌드 타임 환경변수 포함)..."
docker buildx build \
--platform=linux/amd64 \
--build-arg REACT_APP_API_BASE_URL="https://배포된 주소.asia-northeast3.run.app" \
-t "$FULL_IMAGE" \
--push .
echo "Cloud Run 배포 중..."
gcloud run deploy "$SERVICE_NAME" \
--image="$FULL_IMAGE" \
--platform=managed \
--region="$REGION" \
--allow-unauthenticated
if [ $? -eq 0 ]; then
echo "React 서비스 배포 성공!"
else
echo "chmod +x deploy.sh
React 배포 실패!"
exit 1
fi
node-deploy.sh
#!/bin/bash
# 설정
PROJECT_ID="gcp 프로젝트 ID"
REGION="asia-northeast3"
REPO_NAME="Artifact Registry 이름"
IMAGE_NAME="도커 이미지파일 이름"
IMAGE_TAG="1.0"
SERVICE_NAME="서비스 이름"
FULL_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
# .env → 환경변수 문자열 변환
if [ ! -f .env ]; then
echo ".env 파일이 없습니다. 중단합니다."
exit 1
fi
ENV_VARS=$(cat .env | grep -v '^#' | xargs | sed 's/ /,/g')
echo "이미지 빌드 및 배포 시작..."
echo "이미지: $FULL_IMAGE"
# Docker 이미지 빌드 (M1 대응)
docker buildx build \
--platform linux/amd64 \
-t "$FULL_IMAGE" \
--push .
# Cloud Run 배포
gcloud run deploy "$SERVICE_NAME" \
--image="$FULL_IMAGE" \
--region="$REGION" \
--platform=managed \
--set-env-vars="$ENV_VARS" \
--allow-unauthenticated \
--memory=1Gi
if [ $? -eq 0 ]; then
echo "배포 성공!"
else
echo "배포 실패"
exit 1
fi
터미널에 ./deploy.sh 명령어 입력하면 한방에 배포끝!
💣트러블 슈팅
리액트 보안 파일 (.env) 관련
.env 파일로 보안 관련 내용을 관리 중이었는데 배포 시에 개발, 운영 환경에 맞춰 .env 파일을 각각 관리해야 한다고 한다.
# .env.development
REACT_APP_API_BASE_URL=http://localhost:8080
# .env.production
REACT_APP_API_BASE_URL=https://배포된 주소.asia-northeast3.run.app
리액트 코드에는 이런 식으로
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const response = await axios.get(`${API_BASE_URL}/api/musical/getMonthMusical`, {
params: {
startDate: getDateString(),
endDate: getDateString('lastday'),
}
});
위와 같이 관리하면 빌드, 개발 시 따로 설정할 필요 없이 알맞은 .env 파일을 불러옴.
# 개발
npm start # → .env.development 자동 사용
# 배포용 정적 파일 빌드
npm run build # → .env.production 자동 사용
이를 위한 React Dockerfile 수정 (최종)
# React 앱 빌드
FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# REACT_APP_API_BASE_URL 환경변수로 React 앱 빌드
ARG REACT_APP_API_BASE_URL
ENV REACT_APP_API_BASE_URL=$REACT_APP_API_BASE_URL
RUN echo "REACT_APP_API_BASE_URL is $REACT_APP_API_BASE_URL"
RUN npm run build
# 2. nginx로 서비스
FROM nginx:alpine
# 빌드된 정적파일 복사
COPY --from=build /app/build /usr/share/nginx/html
# nginx 설정 템플릿 복사
COPY nginx.conf.template /etc/nginx/templates/nginx.conf.template
# 컨테이너 포트 노출 (Cloud Run 기본 8080)
EXPOSE 8080
# $PORT 환경변수 치환 후 nginx 실행
CMD ["/bin/sh", "-c", "envsubst '$PORT' < /etc/nginx/templates/nginx.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
스프링부트 애플리케이션 실행 가능한 JAR 파일 만들기 위한 build.gradle 설정
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.curty'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 생략
}
tasks.withType(Jar).configureEach {
enabled = false
}
// 핵심!
tasks.named('bootJar') {
enabled = true
mainClass.set('com.curty.muggle.MuggleApplication')
archiveFileName.set('app.jar')
}
빌드 시 build/libs/app.jar 파일이 잘 생성됨.
GCP 이미지 업로드 스토리지 통일
먼저 다른 팀원이 게시판 이미지 업로드 파일을 저장할 GCP 스토리지를 생성했으나 배포용 GCP에 맞춰
하나로 통일함.
+ 이외에도 수많은 삽질이 있었으나 대부분 스프링 시큐리티 관련이었다.
node, React를 따로 배포하다 보니 관련 주소를 SpringSecurityConfig CORS에 추가해주어야했고
기존 로컬 주소 뿐만 아니라 배포 주소도 추가해주어야했다.
이렇게 Service URL이 뜨면 드디어 정상으로 배포 성공한 것이다!!
스프링부트, 리액트, 노드 모두 잘 배포됐고
로그도 볼 수 있다.
'💻 my code archive > 🌈Project' 카테고리의 다른 글
[Python 프로젝트] 주제 선정, 개발 환경 구축, 프로젝트 구조 잡기 (0) | 2022.11.08 |
---|---|
[RN 프로젝트] #1 개발 환경 구축, 프로젝트 디렉토리 구조 잡기 (0) | 2022.10.05 |
[개인 프로젝트] 스프링 시큐리티(Spring Security), OAuth2 네이버 카카오 로그인 API 사용하기 (0) | 2022.06.28 |
[개인 프로젝트] OpenAPI 조회해온 뮤지컬 정보 화면 목록 뿌리기+Bootswatch 부트스트랩 (0) | 2022.06.24 |
[개인 프로젝트] KOPIS 공연 Open API 가져오기, XML JSON 파싱, DB 저장, 스프링 배치(Spring Batch) (4) | 2022.06.24 |