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
67 changes: 39 additions & 28 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI/CD Pipeline

on:
push:
branches: [ develop, main ]
branches: [ develop, feat/98-load-test ]

jobs:
build:
Expand Down Expand Up @@ -46,12 +46,26 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: digitalTok
path: build/libs/

# 3. Docker Hub 로그인
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

# 4. 도커 이미지 빌드 및 푸시
- name: Build and Push Docker Image
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/digitaltok-server:latest .
docker push ${{ secrets.DOCKER_USERNAME }}/digitaltok-server:latest
# env 추가 및 run.sh 생성
- name: Deploy to EC2
env:
Expand All @@ -61,43 +75,40 @@ jobs:
run: |
echo "$EC2_SSH_KEY" > private_key.pem
chmod 600 private_key.pem
jar_file=$(find build/libs -name '*.jar' ! -name '*plain.jar' | head -n 1)

# 파일 전송
scp -i private_key.pem -o StrictHostKeyChecking=no "$jar_file" $EC2_USERNAME@$EC2_HOST:/home/$EC2_USERNAME/digitalTok.jar

# 실행 스크립트 생성 (run.sh)
# EC2에서 실행할 스크립트 (run.sh) 작성
cat <<EOF > run.sh
#!/bin/bash

# 1. 기존 자바 프로세스 강제 종료
pgrep java | xargs -r kill -9
sleep 5
# 1. 최신 이미지 다운로드
docker pull ${{ secrets.DOCKER_USERNAME }}/digitaltok-server:latest

# DB 및 AWS 환경변수 설정
export DB_URL='${{ secrets.DB_URL }}'
export DB_USERNAME='${{ secrets.DB_USERNAME }}'
export DB_PASSWORD='${{ secrets.DB_PASSWORD }}'
export AWS_ACCESS_KEY='${{ secrets.AWS_ACCESS_KEY }}'
export AWS_SECRET_KEY='${{ secrets.AWS_SECRET_KEY }}'
export SPRING_CLOUD_AWS_REGION_STATIC=ap-northeast-2
# 2. 기존 실행 중인 컨테이너가 있다면 중지 및 삭제
docker stop digitaltok-server || true
docker rm digitaltok-server || true

# 이메일 환경변수 설정
export MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}'
export MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}'
# 3. 새 컨테이너 백그라운드 실행 (-e 로 기존 Secret 주입)
# --network host 옵션: EC2 서버의 네트워크를 그대로 사용하여 로컬 DB(3306)와 통신하기 쉽게 만듭니다.
docker run -d --name digitaltok-server \
--network host \
-e DB_URL='${{ secrets.DB_URL }}' \
-e DB_USERNAME='${{ secrets.DB_USERNAME }}' \
-e DB_PASSWORD='${{ secrets.DB_PASSWORD }}' \
-e AWS_ACCESS_KEY='${{ secrets.AWS_ACCESS_KEY }}' \
-e AWS_SECRET_KEY='${{ secrets.AWS_SECRET_KEY }}' \
-e SPRING_CLOUD_AWS_REGION_STATIC='ap-northeast-2' \
-e MAIL_USERNAME='${{ secrets.MAIL_USERNAME }}' \
-e MAIL_PASSWORD='${{ secrets.MAIL_PASSWORD }}' \
-e JWT_SECRET_KEY='${{ secrets.JWT_SECRET_KEY }}' \
-e TZ='Asia/Seoul' \
${{ secrets.DOCKER_USERNAME }}/digitaltok-server:latest

# 서버 실행 명령어
# -Djwt.secret 옵션으로 스프링이 찾는 이름에 맞춰서 값을 직접 넣어줌
nohup java -Duser.timezone=Asia/Seoul \
-Dapp.server-url='https://diring.site' \
-Djwt.secret='${{ secrets.JWT_SECRET_KEY }}' \
-Djwt.access-token-validity-in-seconds=1800 \
-Djwt.refresh-token-validity-in-seconds=604800 \
-jar /home/$EC2_USERNAME/digitalTok.jar > app.log 2>&1 &
# 4. 쌓여있는 안 쓰는 옛날 도커 이미지 삭제 (EC2 용량 확보)
docker image prune -a -f
EOF

# 스크립트 전송 및 실행
scp -i private_key.pem -o StrictHostKeyChecking=no run.sh $EC2_USERNAME@$EC2_HOST:/home/$EC2_USERNAME/run.sh
ssh -i private_key.pem -o StrictHostKeyChecking=no $EC2_USERNAME@$EC2_HOST "chmod +x /home/$EC2_USERNAME/run.sh && sh /home/$EC2_USERNAME/run.sh"

rm -f private_key.pem run.sh
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 1. Java 17 환경을 베이스로 사용 (가볍고 많이 쓰는 alpine 버전)
FROM eclipse-temurin:17-jdk-alpine

# 2. 작업할 디렉토리 설정
WORKDIR /app

# 3. 로컬에서 빌드된 jar 파일을 도커 컨테이너 안의 /app.jar로 복사
COPY build/libs/digitalTok.jar app.jar

# 4. 서버가 사용할 8080 포트 개방
EXPOSE 8080

# 5. 도커 컨테이너가 켜질 때 실행할 명령어 (java -jar app.jar)
ENTRYPOINT ["java", "-jar", "app.jar"]
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ dependencies {

// 이메일 발송 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-mail'

// 부하 테스트 및 모니터링
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

tasks.named('test') {
Expand Down
37 changes: 37 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
version: '3.8'

services:
mysql:
image: mysql:8.0
container_name: digitaltok-mysql
ports:
- "3307:3306"
environment:
MYSQL_DATABASE: digitaltok
MYSQL_USER: localuser
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_PASSWORD: localpassword
volumes:
- ./local-mysql-data:/var/lib/mysql # 컨테이너를 지워도 데이터가 날아가지 않도록 내 컴퓨터 폴더와 연결 (영속성)

mysql-exporter:
image: prom/mysqld-exporter
container_name: mysql-exporter
environment:
- DATA_SOURCE_NAME=localuser:localpassword@(mysql:3306)/digitaltok
ports:
- "9104:9104"

prometheus:
image: prom/prometheus
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"

grafana:
image: grafana/grafana
container_name: grafana
ports:
- "3000:3000"
80 changes: 80 additions & 0 deletions load-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// import http from 'k6/http';
// import { check, sleep } from 'k6';
//
// export const options = {
// vus: 1000, // 동시에 접속하는 가상 유저
// duration: '1m', // 1분 동안 계속 요청
// };
//
// export default function () {
// // 현재 프로젝트에 구현된 HealthController의 경로를 테스트합니다.
// const res = http.get('http://localhost:8080/health');
//
// check(res, {
// 'status is 200': (r) => r.status === 200,
// });
//
// sleep(1); // 1초 쉬고 다시 요청
// }

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
vus: 100,
duration: '1m',
};

export function setup() {
const loginUrl = 'http://localhost:8080/api/v1/auth/login';
const payload = JSON.stringify({
email: 'user@example.com', // 1. 반드시 DB에 있는 계정이어야 함
password: 'string', // 2. 비밀번호가 맞는지 확인
});

const params = {
headers: { 'Content-Type': 'application/json' },
};

const res = http.post(loginUrl, payload, params);

// [디버깅 코드 추가] 응답이 비어있거나 에러가 났는지 확인
console.log(`[Setup] Status: ${res.status}`);
console.log(`[Setup] Body: ${res.body}`);

if (res.status !== 200 || !res.body) {
console.error('로그인 실패! 서버 로그(IntelliJ)를 확인하세요.');
return { token: null };
}

try {
const responseBody = res.json();
// ApiResponse 구조에 맞게 접근: responseBody.result.accessToken
if (responseBody && responseBody.result && responseBody.result.accessToken) {
return { token: responseBody.result.accessToken };
}
} catch (e) {
console.error(`JSON 파싱 실패: ${e.message}. 응답 내용: ${res.body}`);
}

return { token: null };
}

export default function (data) {
if (!data || !data.token) {
// 토큰이 없으면 테스트를 수행하지 않음
return;
}

const url = 'http://localhost:8080/api/v1/templates/subway';
const params = {
headers: {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
},
};

const res = http.get(url, params);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.5);
}
8 changes: 8 additions & 0 deletions prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global:
scrape_interval: 5s # 5초마다 데이터 수집

scrape_configs:
- job_name: 'digitaltok-server'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080', 'localhost:8080'] # Spring 서버 주소 (network host 모드 기준)
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

// ★ 1. Preflight(OPTIONS) 요청은 인증 없이 무조건 허용 (CORS 해결 핵심)
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/health").permitAll()
.requestMatchers("/health", "/actuator/prometheus").permitAll()

// 인증 없이 접근 가능한 경로 (로그인, 회원가입, 스웨거 등)
.requestMatchers("/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
Expand Down
45 changes: 17 additions & 28 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server:
forward-headers-strategy: framework # Nginx 헤더 신뢰 설정
forward-headers-strategy: framework

app:
server-url: ${SERVER_URL:http://localhost:8080}
Expand All @@ -15,17 +15,17 @@ spring:
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
ddl-auto: update # create는 매번 데이터를 날리므로 update 추천
ddl-auto: update
properties:
hibernate:
format_sql: true
show_sql: true
storage:
type: s3

# 최신 버전에서 cloud는 spring 밑에 있어야함
cloud:
aws:
s3:
Expand All @@ -35,45 +35,34 @@ spring:
stack:
auto: false
credentials:
# 기본값을 임시로 넣어 에러 방지
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
#profiles:
# active: prod
# ... ?? ?? ...
# security:
# oauth2:
# client:
# registration:
# kakao:
# client-authentication-method: client_secret_post
# client-id: { ?? client-id }
# client-secret: { ?? client-secret }
# redirect-uri: http://{ ?? ec2 public ip ?? }:8080/login/oauth2/code/kakao
# authorization-grant-type: authorization_code
# scope: profile_nickname
# client-name: Kakao
# ... ?? ?? ...

#이메일 서버 설정 (Gmail 예시)
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME} # 발송할 이메일 주소
password: ${MAIL_PASSWORD} # 구글 앱 비밀번호
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000

# JWT 설정
jwt:
# 시스템 환경 변수 중 JWT_SECRET_KEY 값을 가져오고, 없으면 기본값(뒷부분)을 씁니다.
# JWT 시크릿 키는 최소 32글자 이상의 긴 문자열이어야 합니다.
secret: ${JWT_SECRET_KEY}
access-token-validity-in-seconds: 3600
refresh-token-validity-in-seconds: 1209600
refresh-token-validity-in-seconds: 1209600

management:
endpoints:
web:
exposure:
include: prometheus, health, info, metrics
metrics:
tags:
application: digitaltok-server
Loading