diff --git a/.gitignore b/.gitignore index 6ddcb8da..561da450 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ /docker/localstack/ /grafana-data/** + +/sample_data/** +!/sample_data/.gitkeep diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..1bb641cb --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,13 @@ +# 베이스 이미지 +FROM openjdk:17-jdk-alpine + +# 작업 디렉토리 설정 +WORKDIR /app + +EXPOSE 8080 + +# JAR 파일 복사 +COPY /build/libs/mywork-be-0.0.1-SNAPSHOT.jar app.jar + +# 프로파일 설정하여 실행 +ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=local"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b501be42..3f4415c5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,24 @@ version: '3.8' services: + mywork-application: + build: + context: ../ + dockerfile: docker/Dockerfile + container_name: mywork-application + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=local + deploy: + resources: + limits: + cpus: "1.00" + memory: 1GB + networks: + mywork-network: + ipv4_address: 172.10.0.14 + depends_on: + - mysql mysql: container_name: 'mywork_rdb' image: mysql:8.0 @@ -44,13 +63,12 @@ services: - "/var/run/docker.sock:/var/run/docker.sock" - "./init-scripts:/etc/localstack/init/ready." grafana: - image: grafana/grafana:latest + image: grafana/grafana:11.5.6 container_name: grafana ports: - "3000:3000" volumes: - ../grafana-data:/var/lib/grafana - - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin @@ -75,6 +93,7 @@ services: ipv4_address: 172.10.0.7 depends_on: - mysqld-exporter + - mywork-application mysqld-exporter: image: prom/mysqld-exporter:latest ports: diff --git a/docker/prometheus/config/prometheus.yml b/docker/prometheus/config/prometheus.yml index 9a80c140..c2ee33a0 100644 --- a/docker/prometheus/config/prometheus.yml +++ b/docker/prometheus/config/prometheus.yml @@ -24,3 +24,11 @@ scrape_configs: static_configs: - targets: - 172.10.0.5:9104 + - job_name: mywork-instance01 + honor_timestamps: true + scrape_interval: 10s + scrape_timeout: 10s + metrics_path: /actuator/prometheus + scheme: http + static_configs: + - targets: ['172.10.0.14:8080'] diff --git a/k6/api/MemberApi.js b/k6/api/MemberApi.js new file mode 100644 index 00000000..fd2a8859 --- /dev/null +++ b/k6/api/MemberApi.js @@ -0,0 +1,11 @@ +import http from 'k6/http'; +import baseUrl from '../base/baseUrl.js'; + +export function loginMember(member) { + let headers = { + 'Content-Type': 'application/json' + }; + + const url = `${baseUrl}/api/login`; + return http.post(url, JSON.stringify(member), { headers: headers }); +} diff --git a/k6/base/BaseUrl.js b/k6/base/BaseUrl.js new file mode 100644 index 00000000..49744054 --- /dev/null +++ b/k6/base/BaseUrl.js @@ -0,0 +1,3 @@ +const baseUrl = "http://localhost:8080"; + +export default baseUrl; diff --git a/k6/base/UserSampleReader.js b/k6/base/UserSampleReader.js new file mode 100644 index 00000000..8ac210a9 --- /dev/null +++ b/k6/base/UserSampleReader.js @@ -0,0 +1,20 @@ +// CSV 파일을 읽고 파싱하는 함수 +export function loadMemberFromCSV() { + const file = open('../../sample_data/member.csv'); // CSV 파일을 읽기 + const lines = file.split('\n'); // 각 줄을 구분자로 나누기 + const data = []; + + // 첫 번째 줄은 헤더이므로 건너뛰고 나머지 데이터 처리 + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); // 빈 줄을 처리하기 위해 trim() 사용 + if (line) { + const fields = line.split(','); // 쉼표로 구분된 항목을 배열로 변환 + data.push({ + id: fields[0], + email: fields[3], + password: fields[8] + }); + } + } + return data; // 데이터를 반환 +} diff --git a/k6/member/MemberLoadTest.js b/k6/member/MemberLoadTest.js new file mode 100644 index 00000000..fad7ee1a --- /dev/null +++ b/k6/member/MemberLoadTest.js @@ -0,0 +1,29 @@ +import { check, group, sleep } from 'k6'; +import { loginMember } from '../api/MemberApi.js'; +import {loadMemberFromCSV} from "../base/UserSampleReader.js"; + +export let options = { + rps: 10, + // iterations: 100, + vus: 100, + duration: '3m', +} + +const memberLoginData = loadMemberFromCSV() + +export default function() { + + group('Member Login Load Test', function() { + const userIndex = __VU - 1; + const user = memberLoginData[userIndex]; + + let response = loginMember({ + email: user.email, + password: user.password + }); + + check(response, { + '[member login] status code 200': (r) => r.status === 200 + }) + }) +} diff --git a/k6/member/MemberSmokeTest.js b/k6/member/MemberSmokeTest.js new file mode 100644 index 00000000..71f2cb15 --- /dev/null +++ b/k6/member/MemberSmokeTest.js @@ -0,0 +1,14 @@ +import http from 'k6/http'; +import {check} from 'k6'; +import { loginMember } from '../api/MemberApi.js'; + +const member = { + email: 'admin@example.com', + password: 'password1234' +} + +export default function () { + let response = loginMember(member); + check(response, { 'status code 200': (r) => r.status === 200 }); + +} diff --git a/sample_data/.gitkeep b/sample_data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/kr/mywork/common/aws/S3Config.java b/src/main/java/kr/mywork/common/aws/S3Config.java index f50e7722..7f64044a 100644 --- a/src/main/java/kr/mywork/common/aws/S3Config.java +++ b/src/main/java/kr/mywork/common/aws/S3Config.java @@ -18,7 +18,7 @@ import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.presigner.S3Presigner; -@Profile("default") +@Profile("default | local") @Configuration public class S3Config { diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..04724364 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,85 @@ +spring: + application: + name: mywork-be + jpa: + show-sql: true + open-in-view: false + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true + sql: + init: + mode: always + datasource: + url: jdbc:mysql://mywork_rdb:3306/mywork_db + username: root + password: mywork2503 + driver-class-name: com.mysql.cj.jdbc.Driver + messages: + basename: messages/messages + fallback-to-system-locale: false + cloud: + aws: + s3: + region: us-east-1 # TODO 운영용은 별도로 선언 + credentials: + access-key: test # TODO 운영용은 별도로 선언 + secret-key: test # TODO 운영용은 별도로 선언 + output: + ansi: + enabled: never +management: + endpoints: + web: + exposure: + include: "metrics,health,prometheus" # 혹은 "*"로 모든 엔드포인트 오픈 가능 +company: + page: + size: 10 + upload: + duration: 3m +member: + page: + size: 10 +post: + page: + size: 10 + upload: + bucket-name: mywork-upload-bucket + duration: 3m + endpoint: http://localstack:4566 + attachment: + max-count: 3 + issued-id: + created-limit-hour: 24 + delete: + cron: 0 0 4 * * * +project: + page: + size: 10 +review: + page: + size: 10 +dashboard: + page: + size: 5 +activityLog: + page: + size: 10 +notification: + page: + size: 10 +jwt: + access-token: + private-key: 126cab8a5d9087a27be4e2a0599682e2bab38e80201d1befa2dd9d55ecbdeac58bab0c84301ad9f9f8a71836825e5a7214e7a9fb17a8578418de6cfe9a15cbc7 + expiration: 3600000 + refresh-token: + private-key: 408a96ae4affd8bce03d11385d9a8dfbd4c0cbc9c5a83675864192dbb50eaa965acf2ef84e60bb1a38798f612caed4f383466a4be14957bf87370b62be71c2e1 + expiration: 86400000 + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true