Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
# ===============================
# 공통 AWS 설정
# GCS (Google Cloud Storage)
# ===============================
AWS_REGION=ap-northeast-2

# ===============================
# AWS S3 (PDF 저장)
# ===============================
# IAM 사용자의 S3 전용 Access Key
AWS_S3_ACCESS_KEY=
AWS_S3_SECRET_KEY=

# S3 버킷 이름
S3_BUCKET=
GCS_PROJECT_ID=
# 서비스 계정 JSON 키 전체를 한 줄 문자열로 입력 (로컬 개발용)
# Cloud Run에서는 서비스 계정을 직접 연결하므로 불필요
GCS_CREDENTIALS_JSON=
GCS_BUCKET=

# ===============================
# Database (PostgreSQL)
Expand Down
74 changes: 27 additions & 47 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy Backend (SSM + S3)
name: Deploy to Cloud Run

on:
workflow_run:
Expand All @@ -7,6 +7,7 @@ on:

permissions:
contents: read
id-token: write

concurrency:
group: backend-deploy-${{ github.event.workflow_run.head_branch }}
Expand Down Expand Up @@ -36,55 +37,34 @@ jobs:
- name: Build (bootJar, skip tests)
run: ./gradlew clean bootJar -x test

- name: Find bootJar (exclude plain)
run: |
ls -al build/libs
JAR_PATH="$(ls -1 build/libs/*.jar | grep -v plain | head -n 1)"
if [ -z "$JAR_PATH" ]; then
echo "ERROR: No boot jar found in build/libs"
exit 1
fi
echo "JAR_PATH=$JAR_PATH" >> $GITHUB_ENV
echo "JAR_NAME=$(basename "$JAR_PATH")" >> $GITHUB_ENV

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
service_account: ${{ secrets.GCP_DEPLOYER_SA }}

- name: Upload artifact to S3
run: |
SHA="${{ github.event.workflow_run.head_sha }}"
KEY="${{ secrets.DEPLOY_KEY_PREFIX }}/${SHA}/${{ env.JAR_NAME }}"
echo "S3_KEY=$KEY" >> $GITHUB_ENV
aws s3 cp "${{ env.JAR_PATH }}" "s3://${{ secrets.DEPLOY_BUCKET }}/$KEY"
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Deploy on EC2 via SSM
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet

- name: Build and push Docker image
run: |
DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}"
SERVICE_NAME="${{ secrets.SERVICE_NAME }}"
BUCKET="${{ secrets.DEPLOY_BUCKET }}"
KEY="${{ env.S3_KEY }}"
SHA="${{ github.event.workflow_run.head_sha }}"
IMAGE="${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.AR_REPO }}/${{ secrets.SERVICE_NAME }}:${SHA}"
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
docker build -t "$IMAGE" .
docker push "$IMAGE"

# Flyway runs automatically on Spring Boot startup.
# Do not run `java -jar app.jar migrate` in this pipeline.

aws ssm send-command \
--instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \
--comment "Deploy backend ${SHA:0:7}" \
--document-name "AWS-RunShellScript" \
--parameters commands="[
\"set -e\",
\"echo Deploying $KEY\",
\"sudo mkdir -p ${DEPLOY_PATH}\",
\"sudo chown -R ec2-user:ec2-user ${DEPLOY_PATH}\",
\"sudo aws s3 cp s3://${BUCKET}/${KEY} ${DEPLOY_PATH}/app.jar\",
\"sudo chmod 644 ${DEPLOY_PATH}/app.jar\",
\"sudo systemctl daemon-reload\",
\"sudo systemctl restart ${SERVICE_NAME}\",
\"sudo systemctl is-active ${SERVICE_NAME}\"
]" \
--output text
- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ secrets.SERVICE_NAME }} \
--image "${{ env.IMAGE }}" \
--region ${{ secrets.GCP_REGION }} \
--platform managed \
--allow-unauthenticated \
--min-instances 1 \
--set-env-vars "SPRING_PROFILES_ACTIVE=prod" \
--set-secrets "DB_HOST=db-host:latest,DB_PORT=db-port:latest,DB_NAME=db-name:latest,DB_USERNAME=db-username:latest,DB_PASSWORD=db-password:latest,REDIS_HOST=redis-host:latest,REDIS_PORT=redis-port:latest,JWT_SECRET=jwt-secret:latest,KAKAO_CLIENT_ID=kakao-client-id:latest,KAKAO_CLIENT_SECRET=kakao-client-secret:latest,NAVER_CLIENT_ID=naver-client-id:latest,NAVER_CLIENT_SECRET=naver-client-secret:latest,GOOGLE_CLIENT_ID=google-client-id:latest,GOOGLE_CLIENT_SECRET=google-client-secret:latest,GCS_PROJECT_ID=gcs-project-id:latest,GCS_BUCKET=gcs-bucket:latest,GCS_CREDENTIALS_JSON=gcs-credentials-json:latest,INTERNAL_API_TOKEN=internal-api-token:latest,OPENROUTER_API_KEY=openrouter-api-key:latest" \
--project ${{ secrets.GCP_PROJECT_ID }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S -G app app
COPY --chown=app:app build/libs/app.jar app.jar
EXPOSE 8080
USER app
ENTRYPOINT ["java", "-jar", "app.jar"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
14 changes: 11 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ dependencies {
// Swagger (Springdoc OpenAPI)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15'

// AWS S3 SDK
implementation platform('software.amazon.awssdk:bom:2.20.26')
implementation 'software.amazon.awssdk:s3'
// Google Cloud Storage SDK
implementation platform('com.google.cloud:libraries-bom:26.34.0')
implementation 'com.google.cloud:google-cloud-storage'

// PDF/이미지 처리 (썸네일 생성용)
implementation 'org.apache.pdfbox:pdfbox:3.0.6'
Expand All @@ -79,3 +79,11 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
}

tasks.named('bootJar') {
archiveFileName = 'app.jar'
}

tasks.named('jar') {
enabled = false
}
18 changes: 7 additions & 11 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
version: '3.8'

services:
localstack:
image: localstack/localstack:latest
container_name: proovy-localstack
# GCS 로컬 에뮬레이터 (fake-gcs-server)
# 접속: http://localhost:4443/storage/v1/
fake-gcs:
image: fsouza/fake-gcs-server:latest
container_name: proovy-fake-gcs
ports:
- "4566:4566"
environment:
- SERVICES=s3
- DEBUG=1
- TMPDIR=/var/lib/localstack/tmp
volumes:
- "./localstack-data:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
- "4443:4443"
command: -scheme http -port 4443 -public-host localhost:4443
Comment on lines +4 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로컬 개발 환경에서 fake-gcs-server에 연결할 수 없습니다.

fake-gcs-serverlocalhost:4443에서 실행되지만, GcsConfig.java에서 GCS 클라이언트의 endpoint URL을 설정하는 코드가 없습니다. 또한 GcsServiceImpl.javagetFileUrl() 메서드는 https://storage.googleapis.com을 하드코딩하고 있습니다.

현재 상태로는 로컬에서 Spring 애플리케이션이 실제 GCS에 연결을 시도하게 됩니다.

🛠️ 로컬 환경 지원을 위한 수정 제안
  1. application.yaml에 endpoint 설정 추가:
gcs:
  project-id: ${GCS_PROJECT_ID}
  bucket: ${GCS_BUCKET:proovy-assets-dev}
  credentials-json: ${GCS_CREDENTIALS_JSON:}
  endpoint: ${GCS_ENDPOINT:}  # 로컬: http://localhost:4443
  1. GcsConfig.java에서 endpoint 설정:
+    `@Value`("${gcs.endpoint:}")
+    private String endpoint;

     `@Bean`
     public Storage gcsStorage() throws IOException {
         StorageOptions.Builder builder = StorageOptions.newBuilder()
                 .setProjectId(projectId);

+        if (endpoint != null && !endpoint.isBlank()) {
+            builder.setHost(endpoint);
+        }

         if (credentialsJson != null && !credentialsJson.isBlank()) {
             // ...
         }
         return builder.build().getService();
     }
  1. GcsServiceImpl.javagetFileUrl()도 endpoint 기반으로 변경 필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yaml` around lines 4 - 11, Add a configurable GCS endpoint and
use it when building the client and composing public file URLs: add a
gcs.endpoint property to application.yaml (e.g., gcs.endpoint=${GCS_ENDPOINT:})
and in GcsConfig.java read that property and, when non-empty, configure the
Storage/StorageOptions client to use that endpoint (override
host/transport/options so local fake-gcs-server at http://localhost:4443 is
used); then update GcsServiceImpl.getFileUrl() to derive the base URL from the
same configured endpoint (fall back to https://storage.googleapis.com if
endpoint is blank) so URLs point to the local emulator in dev and to real GCS in
prod.


postgres:
image: pgvector/pgvector:pg15
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import com.proovy.domain.user.entity.PlanType;
import com.proovy.domain.user.repository.UserPlanRepository;
import com.proovy.global.exception.BusinessException;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;
import com.proovy.global.infra.thumbnail.ThumbnailService;
import com.proovy.global.response.ErrorCode;
import lombok.RequiredArgsConstructor;
Expand All @@ -40,7 +40,7 @@ public class AssetsServiceImpl implements AssetsService {

private final AssetRepository assetRepository;
private final NoteRepository noteRepository;
private final S3Service s3Service;
private final GcsService s3Service;
private final UserPlanRepository userPlanRepository;
private final ThumbnailService thumbnailService;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import com.proovy.domain.user.entity.User;
import com.proovy.domain.user.repository.UserRepository;
import com.proovy.global.exception.BusinessException;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;
import com.proovy.global.response.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -60,7 +60,7 @@ public class ChatServiceImpl implements ChatService {
private final UserRepository userRepository;
private final AssetRepository assetRepository;
private final NoteRepository noteRepository;
private final S3Service s3Service;
private final GcsService s3Service;
private final WebClient webClient;
private final ObjectMapper objectMapper;
private final CreditUseService creditUseService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import com.proovy.domain.note.entity.Note;
import com.proovy.domain.note.repository.NoteRepository;
import com.proovy.global.exception.BusinessException;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;
import com.proovy.global.response.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -36,7 +36,7 @@ public class ConversationQueryServiceImpl implements ConversationQueryService {
private final ChatMessageRepository chatMessageRepository;
private final AssetRepository assetRepository;
private final NoteRepository noteRepository;
private final S3Service s3Service;
private final GcsService s3Service;

private static final int PRESIGNED_URL_DURATION_MINUTES = 15;
private static final Set<String> ALLOWED_CANVAS_MIME_TYPES = Set.of("image/png", "image/jpeg", "image/webp");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;

import java.time.LocalDateTime;
import java.time.ZoneId;
Expand All @@ -52,7 +52,7 @@ public class NoteServiceImpl implements NoteService {
private final MessageToolRepository messageToolRepository;
private final AssetRepository assetRepository;
private final com.proovy.domain.user.repository.UserPlanRepository userPlanRepository;
private final S3Service s3Service;
private final GcsService s3Service;
private final EmbeddingJobPublisher embeddingJobPublisher;
private final GeminiClient geminiClient;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import com.proovy.domain.user.repository.UserPlanRepository;
import com.proovy.domain.user.repository.UserRepository;
import com.proovy.global.exception.BusinessException;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;
import com.proovy.global.response.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -36,7 +36,7 @@ public class StorageService {
private final UserRepository userRepository;
private final NoteRepository noteRepository;
private final UserPlanRepository userPlanRepository;
private final S3Service s3Service;
private final GcsService s3Service;

/**
* 자산 일괄 삭제
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/proovy/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import com.proovy.domain.user.repository.UserPlanRepository;
import com.proovy.domain.user.repository.UserRepository;
import com.proovy.global.exception.BusinessException;
import com.proovy.global.infra.s3.S3Service;
import com.proovy.global.infra.gcs.GcsService;
import com.proovy.global.response.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -55,7 +55,7 @@ public class UserService {
private final MessageAttachmentRepository messageAttachmentRepository;
private final ChatMessageRepository chatMessageRepository;
private final ChatSessionRepository chatSessionRepository;
private final S3Service s3Service;
private final GcsService s3Service;
private final RefreshTokenRepository refreshTokenRepository;
private final AccessTokenBlacklistService accessTokenBlacklistService;

Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/proovy/global/config/GcsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.proovy.global.config;

import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Configuration
public class GcsConfig {

@Value("${gcs.project-id}")
private String projectId;

@Value("${gcs.credentials-json:}")
private String credentialsJson;

@Value("${gcs.endpoint:}")
private String endpoint;

@Bean
public Storage gcsStorage() throws IOException {
StorageOptions.Builder builder = StorageOptions.newBuilder()
.setProjectId(projectId);

if (credentialsJson != null && !credentialsJson.isBlank()) {
ServiceAccountCredentials credentials = ServiceAccountCredentials
.fromStream(new ByteArrayInputStream(
credentialsJson.getBytes(StandardCharsets.UTF_8)));
builder.setCredentials(credentials);
}
// credentialsJson 미설정 시 ADC(Application Default Credentials) 사용
// Cloud Run에서는 서비스 계정이 자동 적용됨

if (endpoint != null && !endpoint.isBlank()) {
builder.setHost(endpoint);
}

return builder.build().getService();
}
}
43 changes: 0 additions & 43 deletions src/main/java/com/proovy/global/config/S3Config.java

This file was deleted.

Loading
Loading