From b48c5d8cd6024ae6cbb276f1b5a3bba2fc0710b0 Mon Sep 17 00:00:00 2001 From: Dmori <83327857+Dimo-2562@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:25:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[chore/#470]=20GCP=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20Terraform=20=EA=B5=AC=EC=B6=95=20(#471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: GCP, Cloudflare Terraform provider 초기 설정 * chore: Terraform 변수 정의 추가 (GCP, Cloudflare, SSH) * chore: cloudflare IP만 접속 가능하도록 네트워크 셋팅(무료 인스턴스는 리전이 한정되어 있어 us-central1로 셋팅) * chore: 인스턴스 설정 구성 * chore: Cloudflare DNS A 레코드 설정 (prod, staging, SSH) * chore: IP 출력하도록 셋팅 * chore: provider 버전 고정 파일 * chore: Cloud SQL로 MySQL 띄우도록 Terraform 코드 작성 * chore: GCS Terraform 코드 작성 * chore: 서비스 계정 추가를 위해 업데이트 시 잠 시 멈추도록 설정 * chore: Cloud SQL 대신 Docker MySQL을 사용하기로 결정 --- .gitignore | 8 ++- terraform/.terraform.lock.hcl | 45 +++++++++++++++++ terraform/compute.tf | 93 +++++++++++++++++++++++++++++++++++ terraform/dns.tf | 31 ++++++++++++ terraform/main.tf | 23 +++++++++ terraform/network.tf | 62 +++++++++++++++++++++++ terraform/outputs.tf | 19 +++++++ terraform/storage.tf | 32 ++++++++++++ terraform/variables.tf | 26 ++++++++++ 9 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 terraform/.terraform.lock.hcl create mode 100644 terraform/compute.tf create mode 100644 terraform/dns.tf create mode 100644 terraform/main.tf create mode 100644 terraform/network.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/storage.tf create mode 100644 terraform/variables.tf diff --git a/.gitignore b/.gitignore index 21ca485b..61319394 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,10 @@ src/main/resources/application-dev.yml .env -application-dev.yml \ No newline at end of file +application-dev.yml + +### Terraform ### +terraform/.terraform/ +terraform/terraform.tfstate +terraform/terraform.tfstate.backup +terraform/terraform.tfvars \ No newline at end of file diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 00000000..f5fc3276 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/cloudflare/cloudflare" { + version = "4.52.5" + constraints = "~> 4.0" + hashes = [ + "h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", + "zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", + "zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", + "zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", + "zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", + "zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", + "zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072", + "zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661", + "zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", + "zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", + "zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", + "zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", + "zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", + "zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", + ] +} + +provider "registry.terraform.io/hashicorp/google" { + version = "5.45.2" + constraints = "~> 5.0" + hashes = [ + "h1:k8taQAdfHrv2F/AiGV5BZBZfI+1uaq8g6O8dWzjx42c=", + "zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a", + "zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804", + "zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e", + "zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392", + "zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca", + "zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf", + "zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9", + "zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4", + "zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3", + "zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8", + "zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform/compute.tf b/terraform/compute.tf new file mode 100644 index 00000000..7cace9f5 --- /dev/null +++ b/terraform/compute.tf @@ -0,0 +1,93 @@ +resource "google_compute_address" "prod" { + name = "cockple-prod-ip" + region = "asia-northeast3" +} + +resource "google_compute_address" "staging" { + name = "cockple-staging-ip" + region = "us-central1" +} + +resource "google_compute_instance" "prod" { + name = "cockple-prod" + machine_type = "e2-medium" # 4GB RAM + zone = "asia-northeast3-b" + tags = ["cockple-prod"] + allow_stopping_for_update = true + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + size = 20 + } + } + + network_interface { + subnetwork = google_compute_subnetwork.prod.id + access_config { + nat_ip = google_compute_address.prod.address + } + } + + metadata = { + ssh-keys = "ubuntu:${var.ssh_public_key}" + } + + service_account { + email = google_service_account.cockple_app.email + scopes = ["cloud-platform"] # GCS 등 GCP 서비스 접근 + } + + metadata_startup_script = <<-EOF + #!/bin/bash + apt-get update -y + apt-get install -y docker.io + curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + systemctl enable docker + systemctl start docker + usermod -aG docker ubuntu + EOF +} + +resource "google_compute_instance" "staging" { + name = "cockple-staging" + machine_type = "e2-micro" # 1GB RAM, 무료 티어 + zone = "us-central1-a" + tags = ["cockple-staging"] + allow_stopping_for_update = true + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + size = 30 # 무료 티어 최대 + } + } + + network_interface { + subnetwork = google_compute_subnetwork.staging.id + access_config { + nat_ip = google_compute_address.staging.address + } + } + + metadata = { + ssh-keys = "ubuntu:${var.ssh_public_key}" + } + + service_account { + email = google_service_account.cockple_app.email + scopes = ["cloud-platform"] + } + + metadata_startup_script = <<-EOF + #!/bin/bash + apt-get update -y + apt-get install -y docker.io + curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + systemctl enable docker + systemctl start docker + usermod -aG docker ubuntu + EOF +} diff --git a/terraform/dns.tf b/terraform/dns.tf new file mode 100644 index 00000000..95d6a4f7 --- /dev/null +++ b/terraform/dns.tf @@ -0,0 +1,31 @@ +resource "cloudflare_record" "prod" { + zone_id = var.cloudflare_zone_id + name = "@" + content = google_compute_address.prod.address + type = "A" + proxied = true +} + +resource "cloudflare_record" "prod_ssh" { + zone_id = var.cloudflare_zone_id + name = "ssh" + content = google_compute_address.prod.address + type = "A" + proxied = false +} + +resource "cloudflare_record" "staging" { + zone_id = var.cloudflare_zone_id + name = "staging" + content = google_compute_address.staging.address + type = "A" + proxied = true +} + +resource "cloudflare_record" "staging_ssh" { + zone_id = var.cloudflare_zone_id + name = "ssh-staging" + content = google_compute_address.staging.address + type = "A" + proxied = false +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 00000000..9fa928de --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,23 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4.0" + } + } +} + +provider "google" { + project = var.gcp_project_id + region = var.gcp_region +} + +provider "cloudflare" { + api_token = var.cloudflare_api_token +} \ No newline at end of file diff --git a/terraform/network.tf b/terraform/network.tf new file mode 100644 index 00000000..d65214f4 --- /dev/null +++ b/terraform/network.tf @@ -0,0 +1,62 @@ +resource "google_compute_network" "cockple_vpc" { + name = "cockple-vpc" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "prod" { + name = "cockple-subnet-prod" + ip_cidr_range = "10.0.1.0/24" + region = "asia-northeast3" + network = google_compute_network.cockple_vpc.id +} + +resource "google_compute_subnetwork" "staging" { + name = "cockple-subnet-staging" + ip_cidr_range = "10.0.2.0/24" + region = "us-central1" + network = google_compute_network.cockple_vpc.id +} + +# Cloudflare IP 대역에서만 80 포트 허용 (origin IP 보호) +resource "google_compute_firewall" "allow_http_cloudflare" { + name = "cockple-allow-http-cloudflare" + network = google_compute_network.cockple_vpc.name + + allow { + protocol = "tcp" + ports = ["80"] + } + + source_ranges = [ + "173.245.48.0/20", + "103.21.244.0/22", + "103.22.200.0/22", + "103.31.4.0/22", + "141.101.64.0/18", + "108.162.192.0/18", + "190.93.240.0/20", + "188.114.96.0/20", + "197.234.240.0/22", + "198.41.128.0/17", + "162.158.0.0/15", + "104.16.0.0/13", + "104.24.0.0/14", + "172.64.0.0/13", + "131.0.72.0/22", + ] + + target_tags = ["cockple-prod", "cockple-staging"] +} + +resource "google_compute_firewall" "allow_ssh" { + name = "cockple-allow-ssh" + network = google_compute_network.cockple_vpc.name + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["cockple-prod", "cockple-staging"] +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 00000000..aeb7a1a9 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,19 @@ +output "prod_ip" { + description = "Prod 서버 공인 IP" + value = google_compute_address.prod.address +} + +output "staging_ip" { + description = "Staging 서버 공인 IP" + value = google_compute_address.staging.address +} + +output "gcs_bucket_name" { + description = "GCS 버킷 이름" + value = google_storage_bucket.cockple_assets.name +} + +output "app_service_account_email" { + description = "앱 서비스 계정 이메일 (GCS 인증에 사용)" + value = google_service_account.cockple_app.email +} \ No newline at end of file diff --git a/terraform/storage.tf b/terraform/storage.tf new file mode 100644 index 00000000..63e9e4a4 --- /dev/null +++ b/terraform/storage.tf @@ -0,0 +1,32 @@ +resource "google_project_service" "storage" { + service = "storage.googleapis.com" + disable_on_destroy = false +} + +# 앱 인스턴스용 서비스 계정 (GCS 접근) +resource "google_service_account" "cockple_app" { + account_id = "cockple-app" + display_name = "Cockple App Service Account" +} + +resource "google_storage_bucket" "cockple_assets" { + name = "cockple-assets-${var.gcp_project_id}" + location = "ASIA-NORTHEAST3" + + uniform_bucket_level_access = true + + cors { + origin = ["https://cockple.shop", "https://staging.cockple.shop"] + method = ["GET", "PUT", "POST", "DELETE"] + response_header = ["Content-Type"] + max_age_seconds = 3600 + } + + depends_on = [google_project_service.storage] +} + +resource "google_storage_bucket_iam_member" "app_storage_admin" { + bucket = google_storage_bucket.cockple_assets.name + role = "roles/storage.objectAdmin" + member = "serviceAccount:${google_service_account.cockple_app.email}" +} \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..2f96e69b --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,26 @@ +variable "gcp_project_id" { + description = "GCP 프로젝트 ID" + type = string +} + +variable "gcp_region" { + description = "GCP 기본 리전 (prod)" + type = string + default = "asia-northeast3" +} + +variable "cloudflare_api_token" { + description = "Cloudflare API 토큰" + type = string + sensitive = true +} + +variable "cloudflare_zone_id" { + description = "cockple.shop Cloudflare Zone ID" + type = string +} + +variable "ssh_public_key" { + description = "인스턴스 접속용 SSH 공개키" + type = string +} From 0bc314ea3c834886a1ab4c18d66acb5b5c3b8dee Mon Sep 17 00:00:00 2001 From: Dmori <83327857+Dimo-2562@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:04:53 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[chore/#472]=20=EB=8B=A8=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EA=B8=B0=EB=B0=98=20Doc?= =?UTF-8?q?ker=20=EB=B0=B0=ED=8F=AC=20=ED=99=98=EA=B2=BD=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=9E=90=EB=8F=99=ED=99=94=20(#473)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 인스턴스 분리 대신 단일 인스턴스로 변경 * chore: 배포 스크립트 별도로 분리 * chore: application.yml로 통합 staging과 prod를 Docker로 띄우므로 url이 공개되어도 접근 불가 * chore: Cloudflare에서 SSL 인증을 진행하고, Nginx에서 컨테이너로 분기 * chore: docker compose 구성 * improve: redis에서 인덱스로 분리하기 위해 환경변수 추가 * chore: 데이터베이스 생성 스크립트 작성 * chore: jre만 있어도 jar 파일 실행이 가능하므로 jre로 이미지 크기를 줄이고, CMD 대신 ENTRYPOINT로 JAVA_OPTION이 제대로 적용되도록 변경 * chore: cd 스크립트에서 초기 파일들을 scp로 복사하여 자동화 * chore: DB 사용 시 별도의 유저 생성 대신 root 유저 사용 * deploy.sh에서 env 값 받아서 .env 파일 생성 * chore: 로컬 프로파일은 별도로 분리 * docs: DB 및 redis 터널링 스크립트 작성 * chore: nginx, mysql, redis도 deploy.sh로 자동으로 올리도록 변경 --- .github/workflows/cd.yml | 76 +++++------ Dockerfile | 4 +- docker-compose.yml | 119 ++++++++++++++++++ init-db.sql | 2 + nginx/conf.d/prod.conf | 12 ++ nginx/conf.d/staging.conf | 12 ++ nginx/nginx.conf | 25 ++++ scripts/deploy.sh | 42 +++++++ scripts/tunnel.bat | 20 +++ scripts/tunnel.sh | 24 ++++ .../demo/global/config/RedisConfig.java | 4 + src/main/resources/application-local.yml | 12 ++ src/main/resources/application-prod.yml | 12 ++ src/main/resources/application-staging.yml | 12 ++ src/main/resources/application.yml | 13 +- terraform/compute.tf | 49 +------- terraform/dns.tf | 12 +- terraform/network.tf | 13 +- terraform/outputs.tf | 7 +- 19 files changed, 340 insertions(+), 130 deletions(-) create mode 100644 docker-compose.yml create mode 100644 init-db.sql create mode 100644 nginx/conf.d/prod.conf create mode 100644 nginx/conf.d/staging.conf create mode 100644 nginx/nginx.conf create mode 100644 scripts/deploy.sh create mode 100644 scripts/tunnel.bat create mode 100644 scripts/tunnel.sh create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/main/resources/application-staging.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c663c3ae..7f8f4449 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -23,25 +23,9 @@ jobs: id: vars run: | if [ "${{ github.ref_name }}" == "main" ]; then - echo "APP_SECRET=APPLICATION" >> $GITHUB_OUTPUT echo "DOCKER_TAG=latest" >> $GITHUB_OUTPUT - echo "COMPOSE_FILE=docker-compose.prod.yml" >> $GITHUB_OUTPUT else - echo "APP_SECRET=APPLICATION_STAGING" >> $GITHUB_OUTPUT echo "DOCKER_TAG=staging" >> $GITHUB_OUTPUT - echo "COMPOSE_FILE=docker-compose.staging.yml" >> $GITHUB_OUTPUT - fi - - - name: Remove existing application.yml - run: rm -f src/main/resources/application.yml - - - name: Make application.yml - run: | - mkdir -p src/main/resources - if [ "${{ github.ref_name }}" == "main" ]; then - echo "${{ secrets.APPLICATION }}" > src/main/resources/application.yml - else - echo "${{ secrets.APPLICATION_STAGING }}" > src/main/resources/application.yml fi - name: Build with Gradle @@ -55,39 +39,39 @@ jobs: docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}:${{ steps.vars.outputs.DOCKER_TAG }} . docker push ${{ secrets.DOCKER_REPO }}:${{ steps.vars.outputs.DOCKER_TAG }} - - name: Deploy_EC2 + - name: Copy files to server + uses: appleboy/scp-action@master + with: + host: ${{ secrets.HOST }} + username: ubuntu + key: ${{ secrets.KEY }} + source: "docker-compose.yml,init-db.sql,nginx/,scripts/" + target: /home/ubuntu/cockple + + - name: Deploy uses: appleboy/ssh-action@master - id: deploy with: host: ${{ secrets.HOST }} username: ubuntu key: ${{ secrets.KEY }} + envs: >- + DB_PASSWORD, + S3_BUCKET,S3_ACCESS_KEY,S3_SECRET_KEY, + KAKAO_CLIENT_ID,KAKAO_CLIENT_SECRET,KAKAO_REDIRECT_URI_PROD,KAKAO_REDIRECT_URI_STAGING,KAKAO_ADMIN_KEY, + JWT_SECRET_KEY script: | - cd /home/ubuntu/home/monitor - echo "=== 배포 전 상태 ===" - sudo docker ps - sudo docker image prune -f - sudo docker pull ${{ secrets.DOCKER_REPO }}:${{ steps.vars.outputs.DOCKER_TAG }} - - if [ "${{ github.ref_name }}" == "main" ]; then - sudo docker stop cockple-app || true - sudo docker rm -f cockple-app || true - if ! sudo docker ps | grep -q cockple-redis; then - echo "Redis(prod)가 죽었음, 재시작 중..." - sudo docker compose -f docker-compose.prod.yml up -d redis - sleep 10 - fi - sudo docker compose -f docker-compose.prod.yml up -d cockple-app - else - sudo docker stop cockple-app-staging || true - sudo docker rm -f cockple-app-staging || true - if ! sudo docker ps | grep -q cockple-redis-staging; then - echo "Redis(staging)가 죽었음, 재시작 중..." - sudo docker compose -f docker-compose.staging.yml up -d redis-staging - sleep 10 - fi - sudo docker compose -f docker-compose.staging.yml up -d cockple-app-staging - fi - - echo "=== 배포 후 상태 ===" - sudo docker ps \ No newline at end of file + chmod +x /home/ubuntu/cockple/scripts/deploy.sh + bash /home/ubuntu/cockple/scripts/deploy.sh \ + ${{ secrets.DOCKER_REPO }} \ + ${{ github.ref_name }} + env: + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} + KAKAO_REDIRECT_URI_PROD: ${{ secrets.KAKAO_REDIRECT_URI_PROD }} + KAKAO_REDIRECT_URI_STAGING: ${{ secrets.KAKAO_REDIRECT_URI_STAGING }} + KAKAO_ADMIN_KEY: ${{ secrets.KAKAO_ADMIN_KEY }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} diff --git a/Dockerfile b/Dockerfile index 99a04c76..36c0d42a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM eclipse-temurin:17-jdk-jammy +FROM eclipse-temurin:17-jre-jammy COPY build/libs/cockple.demo-0.0.1-SNAPSHOT.jar app.jar -CMD ["java", "-Dspring.profiles.active=dev", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..09d702bd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +name: cockple + +services: + mysql: + image: mysql:8.0 + container_name: cockple-mysql + restart: always + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + TZ: Asia/Seoul + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --innodb-buffer-pool-size=256M + volumes: + - mysql-data:/var/lib/mysql + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + mem_limit: 512m + memswap_limit: 768m + + redis: + image: redis:7-alpine + container_name: cockple-redis + restart: always + command: + - redis-server + - --appendonly + - "yes" + - --maxmemory + - 200mb + - --maxmemory-policy + - allkeys-lru + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + mem_limit: 256m + memswap_limit: 384m + + cockple-app: + container_name: cockple-app + image: kanghana1/cockple:latest + restart: always + environment: + JAVA_TOOL_OPTIONS: "-Xms768m -Xmx768m" + SPRING_PROFILES_ACTIVE: prod + DB_PASSWORD: ${DB_PASSWORD} + S3_BUCKET: ${S3_BUCKET} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} + KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET} + KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI_PROD} + KAKAO_ADMIN_KEY: ${KAKAO_ADMIN_KEY} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + mem_limit: 1200m + memswap_limit: 1536m + + cockple-app-staging: + container_name: cockple-app-staging + image: kanghana1/cockple:staging + restart: always + environment: + JAVA_TOOL_OPTIONS: "-Xms128m -Xmx512m" + SPRING_PROFILES_ACTIVE: staging + DB_PASSWORD: ${DB_PASSWORD} + S3_BUCKET: ${S3_BUCKET} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} + KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET} + KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI_STAGING} + KAKAO_ADMIN_KEY: ${KAKAO_ADMIN_KEY} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + mem_limit: 1024m + memswap_limit: 1280m + + nginx: + image: nginx:stable-alpine + container_name: cockple-nginx + restart: always + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + depends_on: + - cockple-app + - cockple-app-staging + mem_limit: 64m + memswap_limit: 128m + +volumes: + mysql-data: + redis-data: + +networks: + default: + name: cockple_network diff --git a/init-db.sql b/init-db.sql new file mode 100644 index 00000000..a50a51c5 --- /dev/null +++ b/init-db.sql @@ -0,0 +1,2 @@ +CREATE DATABASE IF NOT EXISTS cockple CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS cockple_staging CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/nginx/conf.d/prod.conf b/nginx/conf.d/prod.conf new file mode 100644 index 00000000..4f812314 --- /dev/null +++ b/nginx/conf.d/prod.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name cockple.shop; + + location / { + proxy_pass http://cockple-app:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx/conf.d/staging.conf b/nginx/conf.d/staging.conf new file mode 100644 index 00000000..7fd60692 --- /dev/null +++ b/nginx/conf.d/staging.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name staging.cockple.shop; + + location / { + proxy_pass http://cockple-app-staging:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..2ce913f6 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,25 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + client_max_body_size 30M; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 00000000..132b5a13 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +DOCKER_REPO=$1 +BRANCH=$2 + +cd /home/ubuntu/cockple + +if [ "$BRANCH" == "main" ]; then + SERVICE="cockple-app" + TAG="latest" +else + SERVICE="cockple-app-staging" + TAG="staging" +fi + +cat > .env << EOF +DB_PASSWORD=${DB_PASSWORD} +S3_BUCKET=${S3_BUCKET} +S3_ACCESS_KEY=${S3_ACCESS_KEY} +S3_SECRET_KEY=${S3_SECRET_KEY} +KAKAO_CLIENT_ID=${KAKAO_CLIENT_ID} +KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET} +KAKAO_REDIRECT_URI_PROD=${KAKAO_REDIRECT_URI_PROD} +KAKAO_REDIRECT_URI_STAGING=${KAKAO_REDIRECT_URI_STAGING} +KAKAO_ADMIN_KEY=${KAKAO_ADMIN_KEY} +JWT_SECRET_KEY=${JWT_SECRET_KEY} +EOF + +echo "=== 배포 전 상태 ===" +sudo docker ps + +sudo docker compose up -d mysql redis nginx +sudo docker image prune -f +sudo docker pull $DOCKER_REPO:$TAG + +sudo docker stop $SERVICE || true +sudo docker rm -f $SERVICE || true + +sudo docker compose up -d $SERVICE + +echo "=== 배포 후 상태 ===" +sudo docker ps diff --git a/scripts/tunnel.bat b/scripts/tunnel.bat new file mode 100644 index 00000000..fec35329 --- /dev/null +++ b/scripts/tunnel.bat @@ -0,0 +1,20 @@ +@echo off +:: 사용법: scripts\tunnel.bat [GCP_IP] +:: 예시: scripts\tunnel.bat 34.64.xxx.xxx + +set GCP_IP=%1 + +if "%GCP_IP%"=="" ( + set /p GCP_IP=GCP IP 입력: +) + +echo 터널링 시작: %GCP_IP% +echo MySQL -^> localhost:3306 -^> cockple-mysql:3306 +echo Redis -^> localhost:6379 -^> cockple-redis:6379 +echo 종료: Ctrl+C + +ssh -N ^ + -L 3306:localhost:3306 ^ + -L 6379:localhost:6379 ^ + -i %USERPROFILE%\.ssh\cockple ^ + ubuntu@%GCP_IP% diff --git a/scripts/tunnel.sh b/scripts/tunnel.sh new file mode 100644 index 00000000..92895996 --- /dev/null +++ b/scripts/tunnel.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# GCP 서버를 통해 Docker MySQL/Redis 터널링 +# 사용법: ./scripts/tunnel.sh [GCP_IP] +# 예시: ./scripts/tunnel.sh 34.64.xxx.xxx + +GCP_IP=${1:-$(cat .tunnel-ip 2>/dev/null)} + +if [ -z "$GCP_IP" ]; then + echo "GCP IP를 인자로 전달하거나 .tunnel-ip 파일에 저장하세요." + echo "사용법: ./scripts/tunnel.sh [GCP_IP]" + exit 1 +fi + +echo "터널링 시작: $GCP_IP" +echo " MySQL → localhost:3306 → cockple-mysql:3306" +echo " Redis → localhost:6379 → cockple-redis:6379" +echo "종료: Ctrl+C" + +ssh -N \ + -L 3306:localhost:3306 \ + -L 6379:localhost:6379 \ + -i ~/.ssh/cockple \ + ubuntu@$GCP_IP diff --git a/src/main/java/umc/cockple/demo/global/config/RedisConfig.java b/src/main/java/umc/cockple/demo/global/config/RedisConfig.java index 1efe480a..d8de2b9e 100644 --- a/src/main/java/umc/cockple/demo/global/config/RedisConfig.java +++ b/src/main/java/umc/cockple/demo/global/config/RedisConfig.java @@ -32,9 +32,13 @@ public class RedisConfig { @Value("${spring.data.redis.port}") private int redisPort; + @Value("${spring.data.redis.database:0}") + private int redisDatabase; + @Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisHost, redisPort); + configuration.setDatabase(redisDatabase); return new LettuceConnectionFactory(configuration); } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..873cc611 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/cockple?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + + data: + redis: + host: localhost + database: 0 + + jpa: + hibernate: + ddl-auto: update diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..6a100ef4 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:mysql://cockple-mysql:3306/cockple?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + + jpa: + hibernate: + ddl-auto: validate + + data: + redis: + host: cockple-redis + database: 0 diff --git a/src/main/resources/application-staging.yml b/src/main/resources/application-staging.yml new file mode 100644 index 00000000..42edab0c --- /dev/null +++ b/src/main/resources/application-staging.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:mysql://cockple-mysql:3306/cockple_staging?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + + jpa: + hibernate: + ddl-auto: update + + data: + redis: + host: cockple-redis + database: 1 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fb6831ad..d5793df7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,26 +10,23 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ${SPRING_DATASOURCE_URL} - username: ${SPRING_DATASOURCE_USERNAME} - password: ${SPRING_DATASOURCE_PASSWORD} + username: root + password: ${DB_PASSWORD} sql: init: mode: never jpa: + show_sql: false hibernate: ddl-auto: update - show_sql: false properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect format_sql: false use_sql_comments: false default_batch_fetch_size: 1000 - show_sql: false - servlet: multipart: @@ -38,8 +35,8 @@ spring: data: redis: - host: localhost port: 6379 + database: 0 lettuce: pool: max-active: 8 @@ -52,6 +49,7 @@ spring: write-dates-as-timestamps: false deserialization: fail-on-unknown-properties: false + cache: type: redis @@ -85,4 +83,3 @@ jwt: logging: level: org.hibernate.SQL: WARN - diff --git a/terraform/compute.tf b/terraform/compute.tf index 7cace9f5..d10c794f 100644 --- a/terraform/compute.tf +++ b/terraform/compute.tf @@ -3,11 +3,6 @@ resource "google_compute_address" "prod" { region = "asia-northeast3" } -resource "google_compute_address" "staging" { - name = "cockple-staging-ip" - region = "us-central1" -} - resource "google_compute_instance" "prod" { name = "cockple-prod" machine_type = "e2-medium" # 4GB RAM @@ -18,7 +13,7 @@ resource "google_compute_instance" "prod" { boot_disk { initialize_params { image = "ubuntu-os-cloud/ubuntu-2204-lts" - size = 20 + size = 30 } } @@ -33,48 +28,6 @@ resource "google_compute_instance" "prod" { ssh-keys = "ubuntu:${var.ssh_public_key}" } - service_account { - email = google_service_account.cockple_app.email - scopes = ["cloud-platform"] # GCS 등 GCP 서비스 접근 - } - - metadata_startup_script = <<-EOF - #!/bin/bash - apt-get update -y - apt-get install -y docker.io - curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose - systemctl enable docker - systemctl start docker - usermod -aG docker ubuntu - EOF -} - -resource "google_compute_instance" "staging" { - name = "cockple-staging" - machine_type = "e2-micro" # 1GB RAM, 무료 티어 - zone = "us-central1-a" - tags = ["cockple-staging"] - allow_stopping_for_update = true - - boot_disk { - initialize_params { - image = "ubuntu-os-cloud/ubuntu-2204-lts" - size = 30 # 무료 티어 최대 - } - } - - network_interface { - subnetwork = google_compute_subnetwork.staging.id - access_config { - nat_ip = google_compute_address.staging.address - } - } - - metadata = { - ssh-keys = "ubuntu:${var.ssh_public_key}" - } - service_account { email = google_service_account.cockple_app.email scopes = ["cloud-platform"] diff --git a/terraform/dns.tf b/terraform/dns.tf index 95d6a4f7..55143ad9 100644 --- a/terraform/dns.tf +++ b/terraform/dns.tf @@ -6,7 +6,7 @@ resource "cloudflare_record" "prod" { proxied = true } -resource "cloudflare_record" "prod_ssh" { +resource "cloudflare_record" "ssh" { zone_id = var.cloudflare_zone_id name = "ssh" content = google_compute_address.prod.address @@ -17,15 +17,7 @@ resource "cloudflare_record" "prod_ssh" { resource "cloudflare_record" "staging" { zone_id = var.cloudflare_zone_id name = "staging" - content = google_compute_address.staging.address + content = google_compute_address.prod.address type = "A" proxied = true } - -resource "cloudflare_record" "staging_ssh" { - zone_id = var.cloudflare_zone_id - name = "ssh-staging" - content = google_compute_address.staging.address - type = "A" - proxied = false -} diff --git a/terraform/network.tf b/terraform/network.tf index d65214f4..2eeadd24 100644 --- a/terraform/network.tf +++ b/terraform/network.tf @@ -10,13 +10,6 @@ resource "google_compute_subnetwork" "prod" { network = google_compute_network.cockple_vpc.id } -resource "google_compute_subnetwork" "staging" { - name = "cockple-subnet-staging" - ip_cidr_range = "10.0.2.0/24" - region = "us-central1" - network = google_compute_network.cockple_vpc.id -} - # Cloudflare IP 대역에서만 80 포트 허용 (origin IP 보호) resource "google_compute_firewall" "allow_http_cloudflare" { name = "cockple-allow-http-cloudflare" @@ -45,7 +38,7 @@ resource "google_compute_firewall" "allow_http_cloudflare" { "131.0.72.0/22", ] - target_tags = ["cockple-prod", "cockple-staging"] + target_tags = ["cockple-prod"] } resource "google_compute_firewall" "allow_ssh" { @@ -58,5 +51,5 @@ resource "google_compute_firewall" "allow_ssh" { } source_ranges = ["0.0.0.0/0"] - target_tags = ["cockple-prod", "cockple-staging"] -} \ No newline at end of file + target_tags = ["cockple-prod"] +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index aeb7a1a9..717c5af4 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,13 +1,8 @@ output "prod_ip" { - description = "Prod 서버 공인 IP" + description = "서버 공인 IP (prod + staging 공용)" value = google_compute_address.prod.address } -output "staging_ip" { - description = "Staging 서버 공인 IP" - value = google_compute_address.staging.address -} - output "gcs_bucket_name" { description = "GCS 버킷 이름" value = google_storage_bucket.cockple_assets.name From 23590d205cf9771a5dbe9cb6e21f1a9677d42996 Mon Sep 17 00:00:00 2001 From: Dmori <83327857+Dimo-2562@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:20:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[debug/#474]=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20(#475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * debug: 잘못된 도메인 명 해결 * debug: 도커 설치 및 docker compose V1/V2 불일치 해결 * test: 테스트를 위해 임의로 pull_request 생성 시 배포되도록 변경 * chore: deploy.sh에서 헬스체크를 수행해 컨테이너 성공 유무에 따라 cd가 성공 실패하도록 변경 * chore: 배포가 성공했으므로 다시 배포 조건 변경 * debug: nginx의 depends-on 조건때문에 cockple-app도 같이 배포되는 문제 해결 * debug: cockple.shop으로 잘못 기재된 nginx server name 해결 * debug: https를 정상적으로 전달하기 위해 scheme대신 http_x_forwarded_proto로 그대로 전달 * chore: 테스트를 위해 변경했던 CD 조건 복구 --- docker-compose.yml | 3 -- nginx/conf.d/prod.conf | 4 +-- nginx/conf.d/staging.conf | 4 +-- scripts/deploy.sh | 30 +++++++++++++++++++ .../demo/global/config/SecurityConfig.java | 2 +- terraform/compute.tf | 10 +++++-- terraform/storage.tf | 2 +- terraform/variables.tf | 2 +- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 09d702bd..d96ecb64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,9 +104,6 @@ services: volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - depends_on: - - cockple-app - - cockple-app-staging mem_limit: 64m memswap_limit: 128m diff --git a/nginx/conf.d/prod.conf b/nginx/conf.d/prod.conf index 4f812314..314b3e38 100644 --- a/nginx/conf.d/prod.conf +++ b/nginx/conf.d/prod.conf @@ -1,12 +1,12 @@ server { listen 80; - server_name cockple.shop; + server_name cockple.store; location / { proxy_pass http://cockple-app:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; } } diff --git a/nginx/conf.d/staging.conf b/nginx/conf.d/staging.conf index 7fd60692..2a222c17 100644 --- a/nginx/conf.d/staging.conf +++ b/nginx/conf.d/staging.conf @@ -1,12 +1,12 @@ server { listen 80; - server_name staging.cockple.shop; + server_name staging.cockple.store; location / { proxy_pass http://cockple-app-staging:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; } } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 132b5a13..12f17ac2 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -40,3 +40,33 @@ sudo docker compose up -d $SERVICE echo "=== 배포 후 상태 ===" sudo docker ps + +echo "=== 헬스체크 ===" +for container in cockple-mysql cockple-redis $SERVICE; do + for i in $(seq 1 12); do + STATUS=$(sudo docker inspect --format='{{.State.Status}}' $container 2>/dev/null) + HEALTH=$(sudo docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $container 2>/dev/null) + + if [ "$STATUS" != "running" ]; then + echo "FAIL: $container 상태 이상 (status=$STATUS)" + sudo docker logs --tail 20 $container + exit 1 + fi + + if [ "$HEALTH" == "healthy" ] || [ "$HEALTH" == "none" ]; then + echo "OK: $container (status=$STATUS, health=$HEALTH)" + break + fi + + if [ $i -eq 12 ]; then + echo "FAIL: $container 헬스체크 타임아웃 (health=$HEALTH)" + sudo docker logs --tail 20 $container + exit 1 + fi + + echo "대기 중: $container ($i/12, health=$HEALTH)..." + sleep 5 + done +done + +echo "=== 배포 성공 ===" diff --git a/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java b/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java index 6aae4a97..52718bf0 100644 --- a/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java +++ b/src/main/java/umc/cockple/demo/global/config/SecurityConfig.java @@ -69,7 +69,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("http://localhost:5173", "https://cockple.store", "https://cockple-fe.vercel.app")); // 배포 시에는 도메인 지정 권장 + config.setAllowedOrigins(List.of("http://localhost:5173", "https://cockple.store", "https://staging.cockple.store", "https://cockple-fe.vercel.app")); // 배포 시에는 도메인 지정 권장 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); diff --git a/terraform/compute.tf b/terraform/compute.tf index d10c794f..8910e2cf 100644 --- a/terraform/compute.tf +++ b/terraform/compute.tf @@ -36,9 +36,13 @@ resource "google_compute_instance" "prod" { metadata_startup_script = <<-EOF #!/bin/bash apt-get update -y - apt-get install -y docker.io - curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose + apt-get install -y ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update -y + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable docker systemctl start docker usermod -aG docker ubuntu diff --git a/terraform/storage.tf b/terraform/storage.tf index 63e9e4a4..78ee5250 100644 --- a/terraform/storage.tf +++ b/terraform/storage.tf @@ -16,7 +16,7 @@ resource "google_storage_bucket" "cockple_assets" { uniform_bucket_level_access = true cors { - origin = ["https://cockple.shop", "https://staging.cockple.shop"] + origin = ["https://cockple.store", "https://staging.cockple.store"] method = ["GET", "PUT", "POST", "DELETE"] response_header = ["Content-Type"] max_age_seconds = 3600 diff --git a/terraform/variables.tf b/terraform/variables.tf index 2f96e69b..27b8c522 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -16,7 +16,7 @@ variable "cloudflare_api_token" { } variable "cloudflare_zone_id" { - description = "cockple.shop Cloudflare Zone ID" + description = "cockple.store Cloudflare Zone ID" type = string } From dd37284950e02030abb693861d2146714b7f2331 Mon Sep 17 00:00:00 2001 From: Dmori <83327857+Dimo-2562@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:11:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[feat/#476]=20AWS=20S3=20=E2=86=92=20GCP=20?= =?UTF-8?q?GCS=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20(#477)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 데이터베이스 터널링 사용법 수정 * chore: S3 의존성 대신 GCS 의존성으로 변경 * chore: S3 설정 대신 GCS 설정으로 변경 * chore: Config 파일 변경 * feat: S3에서 GCS로 변경 * improve: 터널링은 local 포트랑 겹치치 않도록 포트 변경 * chore: redis도 터널링이 가능하도록 포트 설정 추가 * chore: 환경변수 변화 적용 * test: 테스트를 위해 잠시 배포 조건 변경 * test: 임의로 바꿨던 cd 조건을 다시 복구 * chore: Q클래스 생성된 패키지는 gitignore로 관리 * chore: test yml 파일도 환경변수 변경 --- .github/workflows/cd.yml | 7 +- .gitignore | 1 + build.gradle | 4 +- docker-compose.yml | 10 ++- scripts/deploy.sh | 4 +- scripts/tunnel.bat | 20 ------ scripts/tunnel.sh | 22 +++---- .../chat/service/ChatFileServiceImpl.java | 20 +++--- .../chat/service/ChatImageServiceImpl.java | 20 +++--- .../image/controller/ImgController.java | 6 +- .../domain/image/service/ImageService.java | 66 ++++++++----------- .../cockple/demo/global/config/GcsConfig.java | 15 +++++ .../cockple/demo/global/config/S3Config.java | 40 ----------- src/main/resources/application-local.yml | 3 +- src/main/resources/application.yml | 14 +--- .../resources/application-integrationtest.yml | 13 +--- src/test/resources/application.yml | 13 +--- 17 files changed, 90 insertions(+), 188 deletions(-) delete mode 100644 scripts/tunnel.bat create mode 100644 src/main/java/umc/cockple/demo/global/config/GcsConfig.java delete mode 100644 src/main/java/umc/cockple/demo/global/config/S3Config.java diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7f8f4449..36bdafa9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -55,8 +55,7 @@ jobs: username: ubuntu key: ${{ secrets.KEY }} envs: >- - DB_PASSWORD, - S3_BUCKET,S3_ACCESS_KEY,S3_SECRET_KEY, + DB_PASSWORD,GCS_BUCKET, KAKAO_CLIENT_ID,KAKAO_CLIENT_SECRET,KAKAO_REDIRECT_URI_PROD,KAKAO_REDIRECT_URI_STAGING,KAKAO_ADMIN_KEY, JWT_SECRET_KEY script: | @@ -66,9 +65,7 @@ jobs: ${{ github.ref_name }} env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - S3_BUCKET: ${{ secrets.S3_BUCKET }} - S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} - S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + GCS_BUCKET: ${{ secrets.GCS_BUCKET }} KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} KAKAO_REDIRECT_URI_PROD: ${{ secrets.KAKAO_REDIRECT_URI_PROD }} diff --git a/.gitignore b/.gitignore index 61319394..7b042e06 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ bin/ ### IntelliJ IDEA ### .idea +src/main/generated/ *.iws *.iml *.ipr diff --git a/build.gradle b/build.gradle index d007bfd6..57828e98 100644 --- a/build.gradle +++ b/build.gradle @@ -72,8 +72,8 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' - // s3 - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // gcs + implementation 'com.google.cloud:google-cloud-storage:2.40.1' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/docker-compose.yml b/docker-compose.yml index d96ecb64..605c9893 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,8 @@ services: image: redis:7-alpine container_name: cockple-redis restart: always + ports: + - "6379:6379" command: - redis-server - --appendonly @@ -55,9 +57,7 @@ services: JAVA_TOOL_OPTIONS: "-Xms768m -Xmx768m" SPRING_PROFILES_ACTIVE: prod DB_PASSWORD: ${DB_PASSWORD} - S3_BUCKET: ${S3_BUCKET} - S3_ACCESS_KEY: ${S3_ACCESS_KEY} - S3_SECRET_KEY: ${S3_SECRET_KEY} + GCS_BUCKET: ${GCS_BUCKET} KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET} KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI_PROD} @@ -79,9 +79,7 @@ services: JAVA_TOOL_OPTIONS: "-Xms128m -Xmx512m" SPRING_PROFILES_ACTIVE: staging DB_PASSWORD: ${DB_PASSWORD} - S3_BUCKET: ${S3_BUCKET} - S3_ACCESS_KEY: ${S3_ACCESS_KEY} - S3_SECRET_KEY: ${S3_SECRET_KEY} + GCS_BUCKET: ${GCS_BUCKET} KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET} KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI_STAGING} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 12f17ac2..32fa0040 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -15,9 +15,7 @@ fi cat > .env << EOF DB_PASSWORD=${DB_PASSWORD} -S3_BUCKET=${S3_BUCKET} -S3_ACCESS_KEY=${S3_ACCESS_KEY} -S3_SECRET_KEY=${S3_SECRET_KEY} +GCS_BUCKET=${GCS_BUCKET} KAKAO_CLIENT_ID=${KAKAO_CLIENT_ID} KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET} KAKAO_REDIRECT_URI_PROD=${KAKAO_REDIRECT_URI_PROD} diff --git a/scripts/tunnel.bat b/scripts/tunnel.bat deleted file mode 100644 index fec35329..00000000 --- a/scripts/tunnel.bat +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -:: 사용법: scripts\tunnel.bat [GCP_IP] -:: 예시: scripts\tunnel.bat 34.64.xxx.xxx - -set GCP_IP=%1 - -if "%GCP_IP%"=="" ( - set /p GCP_IP=GCP IP 입력: -) - -echo 터널링 시작: %GCP_IP% -echo MySQL -^> localhost:3306 -^> cockple-mysql:3306 -echo Redis -^> localhost:6379 -^> cockple-redis:6379 -echo 종료: Ctrl+C - -ssh -N ^ - -L 3306:localhost:3306 ^ - -L 6379:localhost:6379 ^ - -i %USERPROFILE%\.ssh\cockple ^ - ubuntu@%GCP_IP% diff --git a/scripts/tunnel.sh b/scripts/tunnel.sh index 92895996..4786d106 100644 --- a/scripts/tunnel.sh +++ b/scripts/tunnel.sh @@ -1,24 +1,20 @@ #!/bin/bash +# 사용법: bash scripts/tunnel.sh [GCP_IP] +# 예시: bash scripts/tunnel.sh 34.64.xxx.xxx -# GCP 서버를 통해 Docker MySQL/Redis 터널링 -# 사용법: ./scripts/tunnel.sh [GCP_IP] -# 예시: ./scripts/tunnel.sh 34.64.xxx.xxx - -GCP_IP=${1:-$(cat .tunnel-ip 2>/dev/null)} +GCP_IP=${1} if [ -z "$GCP_IP" ]; then - echo "GCP IP를 인자로 전달하거나 .tunnel-ip 파일에 저장하세요." - echo "사용법: ./scripts/tunnel.sh [GCP_IP]" - exit 1 + read -p "GCP IP 입력: " GCP_IP fi echo "터널링 시작: $GCP_IP" -echo " MySQL → localhost:3306 → cockple-mysql:3306" -echo " Redis → localhost:6379 → cockple-redis:6379" +echo " MySQL -> localhost:3306 -> cockple-mysql:3306" +echo " Redis -> localhost:6379 -> cockple-redis:6379" echo "종료: Ctrl+C" ssh -N \ - -L 3306:localhost:3306 \ - -L 6379:localhost:6379 \ - -i ~/.ssh/cockple \ + -L 3307:localhost:3306 \ + -L 6380:localhost:6379 \ + -i ~/.ssh/cockple_gcp \ ubuntu@$GCP_IP diff --git a/src/main/java/umc/cockple/demo/domain/chat/service/ChatFileServiceImpl.java b/src/main/java/umc/cockple/demo/domain/chat/service/ChatFileServiceImpl.java index 24b3eb4a..0c013009 100644 --- a/src/main/java/umc/cockple/demo/domain/chat/service/ChatFileServiceImpl.java +++ b/src/main/java/umc/cockple/demo/domain/chat/service/ChatFileServiceImpl.java @@ -1,7 +1,6 @@ package umc.cockple.demo.domain.chat.service; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectInputStream; +import com.google.cloud.storage.Blob; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; @@ -66,9 +65,9 @@ public ResponseEntity downloadFile(Long fileId, String token) { //채팅 파일 조회 ChatMessageFile chatFile = findChatFileOrThrow(fileId); - //S3에서 파일 객체 직접 가져오기 - S3Object s3Object = imageService.downloadFile(chatFile.getFileKey()); - ResponseEntity responseEntity = createDownloadResponseEntity(chatFile, s3Object); + //GCS에서 파일 객체 직접 가져오기 + Blob blob = imageService.downloadFile(chatFile.getFileKey()); + ResponseEntity responseEntity = createDownloadResponseEntity(chatFile, blob); log.info("파일 다운로드 완료 - fileName: {}", chatFile.getOriginalFileName()); return responseEntity; @@ -96,12 +95,11 @@ private void validateToken(Long fileId, String tokenValue) { downloadTokenRepository.delete(token); } - private ResponseEntity createDownloadResponseEntity(ChatMessageFile chatFile, S3Object s3Object) { - //S3 객체에서 직접 메타데이터를 가져오기 - long contentLength = s3Object.getObjectMetadata().getContentLength(); - String contentType = s3Object.getObjectMetadata().getContentType(); - S3ObjectInputStream inputStream = s3Object.getObjectContent(); - Resource resource = new InputStreamResource(inputStream); + private ResponseEntity createDownloadResponseEntity(ChatMessageFile chatFile, Blob blob) { + //GCS 객체에서 직접 메타데이터를 가져오기 + long contentLength = blob.getSize(); + String contentType = blob.getContentType(); + Resource resource = new InputStreamResource(new java.io.ByteArrayInputStream(blob.getContent())); //헤더 생성 ContentDisposition contentDisposition = ContentDisposition.builder("attachment") diff --git a/src/main/java/umc/cockple/demo/domain/chat/service/ChatImageServiceImpl.java b/src/main/java/umc/cockple/demo/domain/chat/service/ChatImageServiceImpl.java index cd49e377..e7d778d0 100644 --- a/src/main/java/umc/cockple/demo/domain/chat/service/ChatImageServiceImpl.java +++ b/src/main/java/umc/cockple/demo/domain/chat/service/ChatImageServiceImpl.java @@ -1,7 +1,6 @@ package umc.cockple.demo.domain.chat.service; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectInputStream; +import com.google.cloud.storage.Blob; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; @@ -66,9 +65,9 @@ public ResponseEntity downloadImage(Long imageId, String token) { //채팅 파일 조회 ChatMessageImg chatImage = findChatImageOrThrow(imageId); - //S3에서 파일 객체 직접 가져오기 - S3Object s3Object = imageService.downloadFile(chatImage.getImgKey()); - ResponseEntity responseEntity = createDownloadResponseEntity(chatImage, s3Object); + //GCS에서 파일 객체 직접 가져오기 + Blob blob = imageService.downloadFile(chatImage.getImgKey()); + ResponseEntity responseEntity = createDownloadResponseEntity(chatImage, blob); log.info("이미지 다운로드 완료 - imageName: {}", chatImage.getOriginalFileName()); return responseEntity; @@ -96,12 +95,11 @@ private void validateToken(Long ImageId, String tokenValue) { downloadTokenRepository.delete(token); } - private ResponseEntity createDownloadResponseEntity(ChatMessageImg chatMessageImg, S3Object s3Object) { - //S3 객체에서 직접 메타데이터를 가져오기 - long contentLength = s3Object.getObjectMetadata().getContentLength(); - String contentType = s3Object.getObjectMetadata().getContentType(); - S3ObjectInputStream inputStream = s3Object.getObjectContent(); - Resource resource = new InputStreamResource(inputStream); + private ResponseEntity createDownloadResponseEntity(ChatMessageImg chatMessageImg, Blob blob) { + //GCS 객체에서 직접 메타데이터를 가져오기 + long contentLength = blob.getSize(); + String contentType = blob.getContentType(); + Resource resource = new InputStreamResource(new java.io.ByteArrayInputStream(blob.getContent())); //헤더 생성 ContentDisposition contentDisposition = ContentDisposition.builder("attachment") diff --git a/src/main/java/umc/cockple/demo/domain/image/controller/ImgController.java b/src/main/java/umc/cockple/demo/domain/image/controller/ImgController.java index e2ed69f0..aef8c61c 100644 --- a/src/main/java/umc/cockple/demo/domain/image/controller/ImgController.java +++ b/src/main/java/umc/cockple/demo/domain/image/controller/ImgController.java @@ -26,7 +26,7 @@ public class ImgController { private final ImageService imageService; @PostMapping(value = "/s3/upload/img", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @Operation(summary = "이미지 업로드", description = "S3에 이미지를 업로드하고 이미지 URL과 imgKey를 반환합니다.") + @Operation(summary = "이미지 업로드", description = "GCS에 이미지를 업로드하고 이미지 URL과 imgKey를 반환합니다.") public BaseResponse imgUpload(@RequestPart("image") MultipartFile image, @RequestParam("domainType") DomainType domainType) { @@ -35,7 +35,7 @@ public BaseResponse imgUpload(@RequestPart("image") Mul @PostMapping(value = "/s3/upload/imgs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @Operation(summary = "이미지 여러장 업로드", description = "S3에 이미지 여러장을 업로드하고 이미지 URL과 imgKey를 반환합니다.") + @Operation(summary = "이미지 여러장 업로드", description = "GCS에 이미지 여러장을 업로드하고 이미지 URL과 imgKey를 반환합니다.") public BaseResponse> imgUpload(@RequestPart("image") List images, @RequestParam("domainType") DomainType domainType) { @@ -43,7 +43,7 @@ public BaseResponse> imgUpload(@RequestPart("image } @PostMapping(value = "/s3/upload/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @Operation(summary = "파일 업로드", description = "S3에 파일을 업로드하고 파일정보를 반환합니다.") + @Operation(summary = "파일 업로드", description = "GCS에 파일을 업로드하고 파일정보를 반환합니다.") public BaseResponse fileUpload(@RequestPart("file") MultipartFile file, @RequestParam("domainType") DomainType domainType) { diff --git a/src/main/java/umc/cockple/demo/domain/image/service/ImageService.java b/src/main/java/umc/cockple/demo/domain/image/service/ImageService.java index bef5bbdc..ff74504c 100644 --- a/src/main/java/umc/cockple/demo/domain/image/service/ImageService.java +++ b/src/main/java/umc/cockple/demo/domain/image/service/ImageService.java @@ -1,10 +1,6 @@ package umc.cockple.demo.domain.image.service; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.S3Object; +import com.google.cloud.storage.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -27,11 +23,10 @@ @Slf4j public class ImageService { - @Value("${cloud.aws.s3.bucket}") + @Value("${gcs.bucket}") private String bucket; - private final AmazonS3 amazonS3; - + private final Storage storage; public ImageUploadDTO.Response uploadImage(MultipartFile image, DomainType domainType) { if (image == null || image.isEmpty()) { @@ -41,8 +36,8 @@ public ImageUploadDTO.Response uploadImage(MultipartFile image, DomainType domai log.info("[이미지 업로드 시작]"); String originalFileName = image.getOriginalFilename(); - String key = getFileKey(image, domainType); // 예: contest-images/uuid.jpg - String imgUrl = uploadToS3(image, key, false); + String key = getFileKey(image, domainType); + String imgUrl = uploadToGcs(image, key); log.info("[이미지 업로드 완료]"); return ImageUploadDTO.Response.builder() @@ -63,7 +58,7 @@ public FileUploadDTO.Response uploadFile(MultipartFile file, DomainType domainTy String originalFileName = file.getOriginalFilename(); String key = getFileKey(file, domainType); - String fileUrl = uploadToS3(file, key, false); + String fileUrl = uploadToGcs(file, key); log.info("[파일 업로드 완료]"); return FileUploadDTO.Response.builder() @@ -75,14 +70,9 @@ public FileUploadDTO.Response uploadFile(MultipartFile file, DomainType domainTy .build(); } - /** - * 다중 이미지 업로드 - * @param images MultipartFile 이미지 리스트 - * @return 업로드된 이미지 URL 리스트 - */ public List uploadImages(List images, DomainType domainType) { if (images == null || images.isEmpty()) { - return List.of(); // 빈 리스트 반환 + return List.of(); } return images.stream() @@ -92,31 +82,27 @@ public List uploadImages(List images, Do public void delete(String imgKey) { try { - amazonS3.deleteObject(bucket, imgKey); - log.info("[S3 삭제 성공] {}", imgKey); + storage.delete(BlobId.of(bucket, imgKey)); + log.info("[GCS 삭제 성공] {}", imgKey); } catch (Exception e) { - log.error("[S3 삭제 실패] {}", e.getMessage()); + log.error("[GCS 삭제 실패] {}", e.getMessage()); throw new S3Exception(S3ErrorCode.IMAGE_DELETE_EXCEPTION); } } - private String uploadToS3(MultipartFile file, String key, boolean useMetadata) { + private String uploadToGcs(MultipartFile file, String key) { try { - if (useMetadata) { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(file.getSize()); - metadata.setContentType(file.getContentType()); - amazonS3.putObject(new PutObjectRequest(bucket, key, file.getInputStream(), metadata)); - } else { - amazonS3.putObject(new PutObjectRequest(bucket, key, file.getInputStream(), null)); - } - return amazonS3.getUrl(bucket, key).toString(); - } catch (AmazonServiceException e) { - log.error("[S3 업로드 실패 - AWS 예외] {}", e.getMessage()); - throw new S3Exception(S3ErrorCode.FILE_UPLOAD_AMAZON_EXCEPTION); + BlobInfo blobInfo = BlobInfo.newBuilder(bucket, key) + .setContentType(file.getContentType()) + .build(); + storage.create(blobInfo, file.getBytes()); + return String.format("https://storage.googleapis.com/%s/%s", bucket, key); } catch (IOException e) { - log.error("[S3 업로드 실패 - IO 예외] {}", e.getMessage()); + log.error("[GCS 업로드 실패 - IO 예외] {}", e.getMessage()); throw new S3Exception(S3ErrorCode.FILE_UPLOAD_IO_EXCEPTION); + } catch (StorageException e) { + log.error("[GCS 업로드 실패 - Storage 예외] {}", e.getMessage()); + throw new S3Exception(S3ErrorCode.FILE_UPLOAD_AMAZON_EXCEPTION); } } @@ -125,20 +111,22 @@ public String getFileKey(MultipartFile file, DomainType domainType) { return null; } - // 원본 파일명에서 확장자 추출 String originalFilename = file.getOriginalFilename(); String extension = StringUtils.getFilenameExtension(originalFilename); - // UUID 기반 유니크 키 생성 String uuid = UUID.randomUUID().toString(); return domainType.getDirectory() + "/" + uuid + "." + extension; } public String getUrlFromKey(String key) { - return amazonS3.getUrl(bucket, key).toString(); + return String.format("https://storage.googleapis.com/%s/%s", bucket, key); } - public S3Object downloadFile(String fileKey) { - return amazonS3.getObject(bucket, fileKey); + public Blob downloadFile(String fileKey) { + Blob blob = storage.get(BlobId.of(bucket, fileKey)); + if (blob == null) { + throw new S3Exception(S3ErrorCode.IMAGE_DELETE_EXCEPTION); + } + return blob; } } \ No newline at end of file diff --git a/src/main/java/umc/cockple/demo/global/config/GcsConfig.java b/src/main/java/umc/cockple/demo/global/config/GcsConfig.java new file mode 100644 index 00000000..9fb20578 --- /dev/null +++ b/src/main/java/umc/cockple/demo/global/config/GcsConfig.java @@ -0,0 +1,15 @@ +package umc.cockple.demo.global.config; + +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GcsConfig { + + @Bean + public Storage storage() { + return StorageOptions.getDefaultInstance().getService(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/cockple/demo/global/config/S3Config.java b/src/main/java/umc/cockple/demo/global/config/S3Config.java deleted file mode 100644 index e0574df9..00000000 --- a/src/main/java/umc/cockple/demo/global/config/S3Config.java +++ /dev/null @@ -1,40 +0,0 @@ -package umc.cockple.demo.global.config; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -@Configuration -public class S3Config { - - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; - - @Value("${cloud.aws.region.static}") - private String region; - - @Bean - @Primary - public BasicAWSCredentials awsCredentialsProvider(){ - BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); - return basicAWSCredentials; - } - - @Bean - public AmazonS3 amazonS3Client() { - BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); - return AmazonS3ClientBuilder.standard() - .withRegion(region) - .enablePathStyleAccess() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); - } -} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 873cc611..d83c9adf 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,10 +1,11 @@ spring: datasource: - url: jdbc:mysql://localhost:3306/cockple?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + url: jdbc:mysql://localhost:3307/cockple?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul data: redis: host: localhost + port: 6380 database: 0 jpa: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d5793df7..dc17074b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -53,18 +53,8 @@ spring: cache: type: redis -cloud: - aws: - s3: - bucket: ${S3_BUCKET} - credentials: - access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} - region: - static: ap-northeast-2 - auto: false - stack: - auto: false +gcs: + bucket: ${GCS_BUCKET} kakao: client-id: ${KAKAO_CLIENT_ID} diff --git a/src/test/resources/application-integrationtest.yml b/src/test/resources/application-integrationtest.yml index ab4deefc..171e20b3 100644 --- a/src/test/resources/application-integrationtest.yml +++ b/src/test/resources/application-integrationtest.yml @@ -17,17 +17,8 @@ spring: cache: type: redis -cloud: - aws: - s3: - bucket: test-bucket - credentials: - access-key: test-access-key - secret-key: test-secret-key - region: - static: ap-northeast-2 - stack: - auto: false +gcs: + bucket: test-bucket kakao: client-id: test-client-id diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index fe01691f..0507865a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -20,14 +20,5 @@ spring: format_sql: true use_sql_comments: true -cloud: - aws: - s3: - bucket: test-bucket - credentials: - access-key: test-access-key - secret-key: test-secret-key - region: - static: ap-northeast-2 - stack: - auto: false \ No newline at end of file +gcs: + bucket: test-bucket \ No newline at end of file