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