From cd17320b83f653d0e6483e16f40ce39b77332396 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sun, 19 Oct 2025 00:56:31 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[BOOK-350]=20refactor:=20apis,=20admin,?= =?UTF-8?q?=20batch=20-=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80=EA=B0=80?= =?UTF-8?q?=20=EB=90=98=EC=96=B4=EB=8F=84=20component=20=EC=8A=A4=EC=BA=94?= =?UTF-8?q?=20=EB=8C=80=EC=9D=91=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/org/yapp/admin/AdminApplication.kt | 9 +++++++-- .../src/main/kotlin/org/yapp/apis/ApisApplication.kt | 12 ++---------- .../main/kotlin/org/yapp/batch/BatchApplication.kt | 7 ++++++- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/admin/src/main/kotlin/org/yapp/admin/AdminApplication.kt b/admin/src/main/kotlin/org/yapp/admin/AdminApplication.kt index 513706a8..6c029fc8 100644 --- a/admin/src/main/kotlin/org/yapp/admin/AdminApplication.kt +++ b/admin/src/main/kotlin/org/yapp/admin/AdminApplication.kt @@ -1,11 +1,16 @@ package org.yapp.admin import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan -@SpringBootApplication +@SpringBootApplication( + exclude = [JpaRepositoriesAutoConfiguration::class] +) +@ComponentScan(basePackages = ["org.yapp"]) class AdminApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/apis/src/main/kotlin/org/yapp/apis/ApisApplication.kt b/apis/src/main/kotlin/org/yapp/apis/ApisApplication.kt index fb5c2683..a9a1d661 100644 --- a/apis/src/main/kotlin/org/yapp/apis/ApisApplication.kt +++ b/apis/src/main/kotlin/org/yapp/apis/ApisApplication.kt @@ -3,20 +3,12 @@ package org.yapp.apis import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan -/** - * Main application class for the apis module. - */ @SpringBootApplication( - scanBasePackages = [ - "org.yapp.apis", - "org.yapp.infra", - "org.yapp.domain", - "org.yapp.gateway", - "org.yapp.globalutils" - ], exclude = [JpaRepositoriesAutoConfiguration::class] ) +@ComponentScan(basePackages = ["org.yapp"]) class ApisApplication fun main(args: Array) { diff --git a/batch/src/main/kotlin/org/yapp/batch/BatchApplication.kt b/batch/src/main/kotlin/org/yapp/batch/BatchApplication.kt index 554d731a..67dd7f7c 100644 --- a/batch/src/main/kotlin/org/yapp/batch/BatchApplication.kt +++ b/batch/src/main/kotlin/org/yapp/batch/BatchApplication.kt @@ -1,9 +1,14 @@ package org.yapp.batch import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan -@SpringBootApplication +@SpringBootApplication( + exclude = [JpaRepositoriesAutoConfiguration::class] +) +@ComponentScan(basePackages = ["org.yapp"]) class BatchApplication fun main(args: Array) { From d19bd11af617389c720270f16aa794375c9b8816 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sun, 19 Oct 2025 01:22:37 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[BOOK-350]=20feat:=20observability=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20MDC,=20Ac?= =?UTF-8?q?tuator=20=EC=84=A4=EC=A0=95=EC=9D=84=20gateway=EC=97=90?= =?UTF-8?q?=EC=84=9C=20observability=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/main/resources/application.yml | 8 ++ apis/src/main/resources/application.yml | 6 +- batch/src/main/resources/application.yml | 13 ++- .../filter/SecurityMdcLoggingFilter.kt | 30 ++++++ .../logging/filter/BaseMdcLoggingFilter.kt | 95 +++++++++++++++++++ .../logging/filter/SimpleMdcLoggingFilter.kt | 18 ++++ .../metrics}/config/ActuatorProperties.kt | 2 +- .../resources/application-observability.yml | 2 +- 8 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 gateway/src/main/kotlin/org/yapp/gateway/filter/SecurityMdcLoggingFilter.kt create mode 100644 observability/src/main/kotlin/org/yapp/observability/logging/filter/BaseMdcLoggingFilter.kt create mode 100644 observability/src/main/kotlin/org/yapp/observability/logging/filter/SimpleMdcLoggingFilter.kt rename {gateway/src/main/kotlin/org/yapp/gateway => observability/src/main/kotlin/org/yapp/observability/metrics}/config/ActuatorProperties.kt (81%) rename gateway/src/main/resources/application-web.yml => observability/src/main/resources/application-observability.yml (96%) diff --git a/admin/src/main/resources/application.yml b/admin/src/main/resources/application.yml index b0b7ec80..a9371496 100644 --- a/admin/src/main/resources/application.yml +++ b/admin/src/main/resources/application.yml @@ -9,17 +9,25 @@ spring: group: dev: - persistence + - crosscutting - jwt - redis - external + - observability prod: - persistence + - crosscutting - jwt - redis - external + - observability test: - persistence + - crosscutting - jwt + - redis + - external + - observability servlet: multipart: max-file-size: 10MB diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index 235eeb20..dcd73de6 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -11,23 +11,23 @@ spring: - persistence - crosscutting - jwt - - web - redis - external + - observability prod: - persistence - crosscutting - jwt - - web - redis - external + - observability test: - persistence - crosscutting - jwt - - web - redis - external + - observability servlet: multipart: max-file-size: 10MB diff --git a/batch/src/main/resources/application.yml b/batch/src/main/resources/application.yml index 35307531..cf427e6e 100644 --- a/batch/src/main/resources/application.yml +++ b/batch/src/main/resources/application.yml @@ -9,15 +9,22 @@ spring: group: dev: - persistence - - jwt + - crosscutting - redis + - external + - observability prod: - persistence - - jwt + - crosscutting - redis + - external + - observability test: - persistence - - jwt + - crosscutting + - redis + - external + - observability servlet: multipart: max-file-size: 10MB diff --git a/gateway/src/main/kotlin/org/yapp/gateway/filter/SecurityMdcLoggingFilter.kt b/gateway/src/main/kotlin/org/yapp/gateway/filter/SecurityMdcLoggingFilter.kt new file mode 100644 index 00000000..b86c8552 --- /dev/null +++ b/gateway/src/main/kotlin/org/yapp/gateway/filter/SecurityMdcLoggingFilter.kt @@ -0,0 +1,30 @@ +package org.yapp.gateway.filter + +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Component +import org.yapp.observability.logging.filter.BaseMdcLoggingFilter + +/** + * Spring Security와 JWT 인증이 있는 환경에서 사용하는 MDC 로깅 필터 + * + * SecurityContext에서 JWT 토큰을 읽어 사용자 ID를 MDC에 추가합니다. + * API 서버(apis), 관리자 서버(admin) 등 인증이 필요한 서비스에 사용됩니다. + */ +@Component +class SecurityMdcLoggingFilter : BaseMdcLoggingFilter() { + /** + * SecurityContext에서 JWT principal을 읽어 사용자 ID를 추출합니다. + * + * @return JWT subject (사용자 ID) 또는 null + */ + override fun resolveUserId(): String? { + val authentication = SecurityContextHolder.getContext().authentication ?: return null + + return when (val principal = authentication.principal) { + is Jwt -> principal.subject + else -> principal?.toString() + } + } +} + diff --git a/observability/src/main/kotlin/org/yapp/observability/logging/filter/BaseMdcLoggingFilter.kt b/observability/src/main/kotlin/org/yapp/observability/logging/filter/BaseMdcLoggingFilter.kt new file mode 100644 index 00000000..d0469581 --- /dev/null +++ b/observability/src/main/kotlin/org/yapp/observability/logging/filter/BaseMdcLoggingFilter.kt @@ -0,0 +1,95 @@ +package org.yapp.observability.logging.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.MDC +import org.springframework.web.filter.OncePerRequestFilter +import java.util.* + +/** + * MDC (Mapped Diagnostic Context) 기반 로깅 필터의 기본 구현 + * + * 이 필터는 모든 HTTP 요청에 대해 다음 정보를 MDC에 추가합니다: + * - traceId: 요청 추적 ID (X-Request-ID 헤더에서 가져오거나 자동 생성) + * - clientIp: 클라이언트 IP (X-Forwarded-For, X-Real-IP 헤더 고려) + * - requestInfo: HTTP 메서드와 URI + * - userId: 사용자 ID (서브클래스에서 구현) + * + * 서브클래스는 resolveUserId()를 오버라이드하여 사용자 ID 추출 로직을 제공할 수 있습니다. + */ +abstract class BaseMdcLoggingFilter : OncePerRequestFilter() { + companion object { + const val TRACE_ID_HEADER = "X-Request-ID" + const val XFF_HEADER = "X-Forwarded-For" + const val X_REAL_IP_HEADER = "X-Real-IP" + const val TRACE_ID_KEY = "traceId" + const val USER_ID_KEY = "userId" + const val CLIENT_IP_KEY = "clientIp" + const val REQUEST_INFO_KEY = "requestInfo" + const val DEFAULT_GUEST_USER = "GUEST" + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val traceId = resolveTraceId(request) + populateMdc(request, traceId) + + try { + filterChain.doFilter(request, response) + } finally { + MDC.clear() + } + } + + /** + * 요청에서 TraceId를 추출하거나 생성합니다. + * X-Request-ID 헤더가 있으면 사용하고, 없으면 새로 생성합니다. + */ + private fun resolveTraceId(request: HttpServletRequest): String { + val incomingTraceId = request.getHeader(TRACE_ID_HEADER) + return incomingTraceId?.takeIf { it.isNotBlank() } + ?: UUID.randomUUID().toString().replace("-", "") + } + + /** + * MDC에 로깅 컨텍스트 정보를 추가합니다. + */ + private fun populateMdc(request: HttpServletRequest, traceId: String) { + MDC.put(TRACE_ID_KEY, traceId) + MDC.put(CLIENT_IP_KEY, extractClientIp(request)) + MDC.put(REQUEST_INFO_KEY, "${request.method} ${request.requestURI}") + + val userId = resolveUserId() + MDC.put(USER_ID_KEY, userId ?: DEFAULT_GUEST_USER) + } + + /** + * 클라이언트의 실제 IP 주소를 추출합니다. + * X-Forwarded-For, X-Real-IP 헤더를 우선 확인하고, 없으면 remoteAddr 사용합니다. + */ + private fun extractClientIp(request: HttpServletRequest): String { + val xffHeader = request.getHeader(XFF_HEADER) + if (!xffHeader.isNullOrBlank()) { + return xffHeader.split(",").first().trim() + } + + val xRealIp = request.getHeader(X_REAL_IP_HEADER) + if (!xRealIp.isNullOrBlank()) { + return xRealIp.trim() + } + + return request.remoteAddr + } + + /** + * 사용자 ID를 추출합니다. + * 서브클래스에서 오버라이드하여 Security Context, JWT 등에서 사용자 정보를 추출할 수 있습니다. + * + * @return 사용자 ID (null인 경우 GUEST로 처리됨) + */ + protected abstract fun resolveUserId(): String? +} diff --git a/observability/src/main/kotlin/org/yapp/observability/logging/filter/SimpleMdcLoggingFilter.kt b/observability/src/main/kotlin/org/yapp/observability/logging/filter/SimpleMdcLoggingFilter.kt new file mode 100644 index 00000000..c984fa95 --- /dev/null +++ b/observability/src/main/kotlin/org/yapp/observability/logging/filter/SimpleMdcLoggingFilter.kt @@ -0,0 +1,18 @@ +package org.yapp.observability.logging.filter + +import org.springframework.stereotype.Component + +/** + * 인증이 필요 없는 환경에서 사용하는 기본 MDC 로깅 필터 + * + * 이 필터는 사용자 ID를 추출하지 않고 모든 요청을 GUEST로 처리합니다. + * Batch 애플리케이션이나 인증이 없는 내부 서비스에 사용됩니다. + */ +@Component +class SimpleMdcLoggingFilter : BaseMdcLoggingFilter() { + /** + * 인증 정보가 없으므로 null을 반환합니다. + * MDC에는 GUEST로 기록됩니다. + */ + override fun resolveUserId(): String? = null +} diff --git a/gateway/src/main/kotlin/org/yapp/gateway/config/ActuatorProperties.kt b/observability/src/main/kotlin/org/yapp/observability/metrics/config/ActuatorProperties.kt similarity index 81% rename from gateway/src/main/kotlin/org/yapp/gateway/config/ActuatorProperties.kt rename to observability/src/main/kotlin/org/yapp/observability/metrics/config/ActuatorProperties.kt index 0a381e73..41728f80 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/config/ActuatorProperties.kt +++ b/observability/src/main/kotlin/org/yapp/observability/metrics/config/ActuatorProperties.kt @@ -1,4 +1,4 @@ -package org.yapp.gateway.config +package org.yapp.observability.metrics.config import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/gateway/src/main/resources/application-web.yml b/observability/src/main/resources/application-observability.yml similarity index 96% rename from gateway/src/main/resources/application-web.yml rename to observability/src/main/resources/application-observability.yml index 3923ec3b..7e659010 100644 --- a/gateway/src/main/resources/application-web.yml +++ b/observability/src/main/resources/application-observability.yml @@ -18,7 +18,7 @@ spring: management: server: - port: 8081 + port: 1234 endpoints: jmx: exposure: From 378b663129168c2e364b8df38c36c3c1d7563659 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sun, 19 Oct 2025 01:25:40 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[BOOK-350]=20chore:=20observability=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20=EA=B0=81?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/build.gradle.kts | 1 + apis/build.gradle.kts | 1 + batch/build.gradle.kts | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 1 + gateway/build.gradle.kts | 5 +- .../yapp/gateway/filter/MdcLoggingFilter.kt | 79 ------------------- .../yapp/gateway/security/SecurityConfig.kt | 8 +- settings.gradle.kts | 3 +- 8 files changed, 12 insertions(+), 87 deletions(-) delete mode 100644 gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt diff --git a/admin/build.gradle.kts b/admin/build.gradle.kts index 9b84b1db..05319495 100644 --- a/admin/build.gradle.kts +++ b/admin/build.gradle.kts @@ -4,6 +4,7 @@ dependencies { implementation(project(Dependencies.Projects.INFRA)) implementation(project(Dependencies.Projects.DOMAIN)) implementation(project(Dependencies.Projects.GLOBAL_UTILS)) + implementation(project(Dependencies.Projects.OBSERVABILITY)) implementation(Dependencies.Spring.BOOT_STARTER_WEB) implementation(Dependencies.Spring.BOOT_STARTER_SECURITY) diff --git a/apis/build.gradle.kts b/apis/build.gradle.kts index 4d04c486..c6ab8ff1 100644 --- a/apis/build.gradle.kts +++ b/apis/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { implementation(project(Dependencies.Projects.DOMAIN)) implementation(project(Dependencies.Projects.GLOBAL_UTILS)) implementation(project(Dependencies.Projects.GATEWAY)) + implementation(project(Dependencies.Projects.OBSERVABILITY)) implementation(Dependencies.Spring.BOOT_STARTER_WEB) implementation(Dependencies.Spring.BOOT_STARTER_DATA_JPA) diff --git a/batch/build.gradle.kts b/batch/build.gradle.kts index 51836ce9..4c056659 100644 --- a/batch/build.gradle.kts +++ b/batch/build.gradle.kts @@ -4,6 +4,7 @@ dependencies { implementation(project(Dependencies.Projects.DOMAIN)) implementation(project(Dependencies.Projects.GLOBAL_UTILS)) implementation(project(Dependencies.Projects.INFRA)) + implementation(project(Dependencies.Projects.OBSERVABILITY)) implementation(Dependencies.Spring.BOOT_STARTER_WEB) implementation(Dependencies.Spring.BOOT_STARTER_SECURITY) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index d5e946b6..cd00e90b 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -30,6 +30,7 @@ object Dependencies { const val DOMAIN = ":domain" const val GLOBAL_UTILS = ":global-utils" const val GATEWAY = ":gateway" + const val OBSERVABILITY = ":observability" } object Logging { diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index 52c2c904..c280b284 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -2,12 +2,11 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar dependencies { implementation(project(Dependencies.Projects.GLOBAL_UTILS)) + implementation(project(Dependencies.Projects.OBSERVABILITY)) + implementation(Dependencies.Spring.BOOT_STARTER_WEB) implementation(Dependencies.Spring.BOOT_STARTER_SECURITY) implementation(Dependencies.Spring.BOOT_STARTER_OAUTH2_RESOURCE_SERVER) - implementation(Dependencies.Spring.BOOT_STARTER_ACTUATOR) - - implementation(Dependencies.Prometheus.MICROMETER_PROMETHEUS_REGISTRY) testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) } diff --git a/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt b/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt deleted file mode 100644 index b2277191..00000000 --- a/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.yapp.gateway.filter - -import jakarta.servlet.FilterChain -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.slf4j.MDC -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.oauth2.jwt.Jwt -import org.springframework.stereotype.Component -import org.springframework.web.filter.OncePerRequestFilter -import java.util.* - -@Component -class MdcLoggingFilter : OncePerRequestFilter() { - companion object { - private const val TRACE_ID_HEADER = "X-Request-ID" - private const val XFF_HEADER = "X-Forwarded-For" - private const val X_REAL_IP_HEADER = "X-Real-IP" - private const val TRACE_ID_KEY = "traceId" - private const val USER_ID_KEY = "userId" - private const val CLIENT_IP_KEY = "clientIp" - private const val REQUEST_INFO_KEY = "requestInfo" - private const val DEFAULT_GUEST_USER = "GUEST" - } - - override fun doFilterInternal( - request: HttpServletRequest, - response: HttpServletResponse, - filterChain: FilterChain - ) { - val traceId = resolveTraceId(request) - populateMdc(request, traceId) - - try { - filterChain.doFilter(request, response) - } finally { - MDC.clear() - } - } - - private fun resolveTraceId(request: HttpServletRequest): String { - val incomingTraceId = request.getHeader(TRACE_ID_HEADER) - return incomingTraceId?.takeIf { it.isNotBlank() } - ?: UUID.randomUUID().toString().replace("-", "") - } - - private fun populateMdc(request: HttpServletRequest, traceId: String) { - MDC.put(TRACE_ID_KEY, traceId) - MDC.put(CLIENT_IP_KEY, extractClientIp(request)) - MDC.put(REQUEST_INFO_KEY, "${request.method} ${request.requestURI}") - - val userId = resolveUserId() - MDC.put(USER_ID_KEY, userId ?: DEFAULT_GUEST_USER) - } - - private fun extractClientIp(request: HttpServletRequest): String { - val xffHeader = request.getHeader(XFF_HEADER) - if (!xffHeader.isNullOrBlank()) { - return xffHeader.split(",").first().trim() - } - - val xRealIp = request.getHeader(X_REAL_IP_HEADER) - if (!xRealIp.isNullOrBlank()) { - return xRealIp.trim() - } - - return request.remoteAddr - } - - private fun resolveUserId(): String? { - val authentication = SecurityContextHolder.getContext().authentication ?: return null - - return when (val principal = authentication.principal) { - is Jwt -> principal.subject - else -> principal?.toString() - } - } -} - diff --git a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt index 2fa33de4..81b179df 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt +++ b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt @@ -11,8 +11,8 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter import org.springframework.security.web.SecurityFilterChain -import org.yapp.gateway.config.ActuatorProperties -import org.yapp.gateway.filter.MdcLoggingFilter +import org.yapp.observability.metrics.config.ActuatorProperties +import org.yapp.gateway.filter.SecurityMdcLoggingFilter @Configuration @EnableWebSecurity @@ -21,7 +21,7 @@ class SecurityConfig( private val jwtAuthenticationConverter: Converter, private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, private val customAccessDeniedHandler: CustomAccessDeniedHandler, - private val mdcLoggingFilter: MdcLoggingFilter, + private val securityMdcLoggingFilter: SecurityMdcLoggingFilter, actuatorProperties: ActuatorProperties ) { companion object { @@ -61,7 +61,7 @@ class SecurityConfig( it.requestMatchers(ADMIN_PATTERN).hasRole("ADMIN") it.anyRequest().authenticated() } - .addFilterAfter(mdcLoggingFilter, BearerTokenAuthenticationFilter::class.java) + .addFilterAfter(securityMdcLoggingFilter, BearerTokenAuthenticationFilter::class.java) .build() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index c7847254..95b6b7ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,5 +7,6 @@ include( "batch", "domain", "infra", - "global-utils" + "global-utils", + "observability" ) From 377a329dd9a7d8ded14731e1e2382794928fd488 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sun, 19 Oct 2025 01:26:19 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[BOOK-350]=20chore:=20observability=20-?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- observability/build.gradle.kts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 observability/build.gradle.kts diff --git a/observability/build.gradle.kts b/observability/build.gradle.kts new file mode 100644 index 00000000..3437d36a --- /dev/null +++ b/observability/build.gradle.kts @@ -0,0 +1,21 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +dependencies { + // Web & Filter + implementation(Dependencies.Spring.BOOT_STARTER_WEB) + + // Metrics & Monitoring + implementation(Dependencies.Spring.BOOT_STARTER_ACTUATOR) + implementation(Dependencies.Prometheus.MICROMETER_PROMETHEUS_REGISTRY) + + // Logging + implementation(Dependencies.Logging.KOTLIN_LOGGING) + + // Test + testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) +} + +tasks { + withType { enabled = true } + withType { enabled = false } +} From b7c598e786444acd3752e0db79043a88448a77a9 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 00:54:32 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[BOOK-350]=20refactor:=20admin,=20batch?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=A7=80=EC=9B=90=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Dockerfile=20=EB=B0=8F=20ci=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pr.yml | 6 +++++- Dockerfile | 9 +++++--- Dockerfile-dev | 42 ------------------------------------- 3 files changed, 11 insertions(+), 46 deletions(-) delete mode 100644 Dockerfile-dev diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index eeceb7a5..5975ab1b 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -58,4 +58,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew fullCheck --parallel --build-cache --info --stacktrace + run: | + # fullCheck: 모든 모듈 (apis, admin, batch, gateway 등)의 빌드, 테스트, 정적분석 수행 + # --parallel: 모듈별 병렬 빌드로 시간 단축 + # --build-cache: Gradle 빌드 캐시 사용 + ./gradlew fullCheck --parallel --build-cache --info --stacktrace diff --git a/Dockerfile b/Dockerfile index b2f69cf6..9d6f5ddd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ # Build stage FROM gradle:8.7-jdk21 AS build -ARG MODULE=apis +# MODULE: 빌드할 모듈 (apis, admin, batch) - 빌드 시 --build-arg MODULE=xxx 필수 +ARG MODULE WORKDIR /app # 의존성 캐싱 최적화를 위한 단계별 복사 @@ -24,7 +25,8 @@ RUN ./gradlew :${MODULE}:bootJar --parallel --no-daemon # Run stage FROM openjdk:21-slim -ARG MODULE=apis +# MODULE: 빌드할 모듈 (apis, admin, batch) - 빌드 시 --build-arg MODULE=xxx 필수 +ARG MODULE WORKDIR /app # 멀티스테이지 빌드로 최종 이미지 크기 최소화 @@ -39,4 +41,5 @@ ENV TZ=Asia/Seoul # JVM 실행 설정 # - Xms512m: 초기 힙 메모리 512MB # - Xmx1g: 최대 힙 메모리 1GB -ENTRYPOINT ["java", "-Xms512m", "-Xmx1g", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] +# - server.port: Spring Boot 서버 포트 (컨테이너 실행 시 -e SERVER_PORT=xxxx로 주입 필수) +ENTRYPOINT ["sh", "-c", "java -Xms512m -Xmx1g -Duser.timezone=Asia/Seoul -Dserver.port=${SERVER_PORT} -jar app.jar"] diff --git a/Dockerfile-dev b/Dockerfile-dev deleted file mode 100644 index b2f69cf6..00000000 --- a/Dockerfile-dev +++ /dev/null @@ -1,42 +0,0 @@ -# Build stage -FROM gradle:8.7-jdk21 AS build -ARG MODULE=apis -WORKDIR /app - -# 의존성 캐싱 최적화를 위한 단계별 복사 -# 1. 의존성 캐싱 최적화를 위해 Gradle Wrapper 및 의존성 관련 파일만 먼저 복사 -COPY build.gradle.kts settings.gradle.kts gradlew gradlew.bat ./ -COPY gradle/wrapper/ ./gradle/wrapper/ -COPY buildSrc/ ./buildSrc/ -COPY ${MODULE}/build.gradle.kts ./${MODULE}/ - -# 2. Gradle Wrapper 실행 권한 부여 -RUN chmod +x gradlew - -# 3. 소스코드 없이 의존성만 다운로드 -RUN ./gradlew :${MODULE}:dependencies --no-daemon - -# 4. 소스코드 전체 복사 -COPY . . - -# 5. 실제 애플리케이션 빌드 -RUN ./gradlew :${MODULE}:bootJar --parallel --no-daemon - -# Run stage -FROM openjdk:21-slim -ARG MODULE=apis -WORKDIR /app - -# 멀티스테이지 빌드로 최종 이미지 크기 최소화 -COPY --from=build /app/${MODULE}/build/libs/${MODULE}-*.jar app.jar - -# 런타임에 필요한 secret 폴더 복사 -COPY --from=build /app/secret ./secret/ - -# TimeZone KST 설정 -ENV TZ=Asia/Seoul - -# JVM 실행 설정 -# - Xms512m: 초기 힙 메모리 512MB -# - Xmx1g: 최대 힙 메모리 1GB -ENTRYPOINT ["java", "-Xms512m", "-Xmx1g", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] From ad2e1b0b8540afb40109661a9ddb731c5e9b9a94 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 01:58:03 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[BOOK-350]=20refactor:=20Matrix=20Strateg?= =?UTF-8?q?y=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EB=B3=91=EB=A0=AC=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EA=B8=B0=EB=8A=A5=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-ci-cd.yml | 161 ++++++++++++++++---------------- 1 file changed, 82 insertions(+), 79 deletions(-) diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 68e85ef2..65e73d8e 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -11,96 +11,99 @@ concurrency: env: REGISTRY: docker.io - IMAGE_NAME: ninecraft0523/ninecraft-server - MODULE: apis + IMAGE_PREFIX: ninecraft0523/ninecraft jobs: - build-push-and-deploy: + detect-changes: runs-on: ubuntu-24.04 - timeout-minutes: 20 - environment: development - + outputs: + apis: ${{ steps.filter.outputs.apis }} + # admin: ${{ steps.filter.outputs.admin }} # TODO: Uncomment when admin module is ready + batch: ${{ steps.filter.outputs.batch }} + any: ${{ steps.filter.outputs.any }} steps: - name: Checkout code uses: actions/checkout@v4 - - name: Inject application-secret.properties from Secrets - run: | - mkdir ./secret - echo "${{ secrets.DEV_SECRET_PROPERTIES }}" > ./secret/application-dev-secret.properties - echo "${{ secrets.APPLE_AUTH_KEY }}" > ./secret/AuthKey.p8 - chmod 600 ./secret/* - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - name: Check changed files + uses: dorny/paths-filter@v3 + id: filter with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + filters: | + apis: + - 'apis/**' + - 'domain/**' + - 'infra/**' + - 'global-utils/**' + - 'observability/**' + # admin: # TODO: Uncomment when admin module is ready + # - 'admin/**' + # - 'domain/**' + # - 'infra/**' + # - 'global-utils/**' + # - 'observability/**' + batch: + - 'batch/**' + - 'domain/**' + - 'infra/**' + - 'global-utils/**' + - 'observability/**' + any: + - 'apis/**' + # - 'admin/**' # TODO: Uncomment when admin module is ready + - 'batch/**' + - 'domain/**' + - 'infra/**' + - 'global-utils/**' + - 'observability/**' - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=development-latest + build-push-and-deploy: + needs: detect-changes + if: needs.detect-changes.outputs.any == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: development + strategy: + fail-fast: false + matrix: + include: + - module: apis + port: 8080 + changed: ${{ needs.detect-changes.outputs.apis }} + # - module: admin # TODO: Uncomment when admin module is ready + # port: 8081 + # changed: ${{ needs.detect-changes.outputs.admin }} + - module: batch + port: 8082 + changed: ${{ needs.detect-changes.outputs.batch }} - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - MODULE=${{ env.MODULE }} + steps: + - name: Skip if no changes + if: matrix.changed != 'true' + run: | + echo "No changes detected for ${{ matrix.module }}, skipping deployment" + exit 0 - - name: Deploy to Development Server - uses: appleboy/ssh-action@v1.2.2 + - name: Checkout code + if: matrix.changed == 'true' + uses: actions/checkout@v4 + + - name: Deploy module + if: matrix.changed == 'true' + uses: ./.github/actions/deploy-module with: + environment: dev + module: ${{ matrix.module }} + port: ${{ matrix.port }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + secret-properties: ${{ secrets.DEV_SECRET_PROPERTIES }} + apple-auth-key: ${{ secrets.APPLE_AUTH_KEY }} host: ${{ secrets.DEV_HOST }} username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_SSH_KEY }} - port: ${{ secrets.DEV_PORT }} - script: | - export DOCKERHUB_USERNAME="${{ secrets.DOCKERHUB_USERNAME }}" - export DOCKERHUB_TOKEN="${{ secrets.DOCKERHUB_TOKEN }}" - export IMAGE_TAG="$(echo "${{ steps.meta.outputs.tags }}" | head -n1)" - cd ~/deploy - chmod +x ./deploy.sh - ./deploy.sh - - - name: Send Discord notification on success - uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 - if: success() - continue-on-error: true - with: - webhook-url: ${{ secrets.DEV_DEPLOY_DISCORD_WEBHOOK_URL }} - embed-title: "✅ [${{ github.repository }}] Development Deploy Succeeded" - embed-description: | - **Commit**: `${{ github.sha }}` - **Author**: `${{ github.actor }}` - **Message**: `${{ github.event.head_commit.message }}` - [View Committed Changes](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) - embed-color: 65280 - - - name: Send Discord notification on failure - uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 - if: failure() - continue-on-error: true - with: - webhook-url: ${{ secrets.DEV_DEPLOY_DISCORD_WEBHOOK_URL }} - embed-title: "❌ [${{ github.repository }}] Development Deploy Failed" - embed-description: | - **Commit**: `${{ github.sha }}` - **Author**: `${{ github.actor }}` - An error occurred during the workflow execution. - [View Failed Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - embed-color: 16711680 + ssh-key: ${{ secrets.DEV_SSH_KEY }} + ssh-port: ${{ secrets.DEV_PORT }} + discord-webhook-url: ${{ secrets.DEV_DEPLOY_DISCORD_WEBHOOK_URL }} + image-prefix: ${{ env.IMAGE_PREFIX }} + image-tag-type: type=raw,value=development-latest + deploy-script: deploy-dev.sh From 7d7dad9225285e11d1ab3c1aae9b5773d2b59315 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 01:58:55 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[BOOK-350]=20refactor:=20batch=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=B0=B0=ED=8F=AC=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/deploy-module/action.yml | 178 +++++++++++++++++++++++ .github/workflows/prod-ci-cd.yml | 108 ++++---------- 2 files changed, 206 insertions(+), 80 deletions(-) create mode 100644 .github/actions/deploy-module/action.yml diff --git a/.github/actions/deploy-module/action.yml b/.github/actions/deploy-module/action.yml new file mode 100644 index 00000000..f7aad6f0 --- /dev/null +++ b/.github/actions/deploy-module/action.yml @@ -0,0 +1,178 @@ +name: 'Deploy Module' +description: 'Build, push and deploy a module to server (dev/prod)' + +inputs: + environment: + description: 'Environment (dev or prod)' + required: true + module: + description: 'Module name (apis, admin, batch)' + required: true + port: + description: 'Server port' + required: true + dockerhub-username: + description: 'Docker Hub username' + required: true + dockerhub-token: + description: 'Docker Hub token' + required: true + secret-properties: + description: 'Secret properties (dev or prod)' + required: true + apple-auth-key: + description: 'Apple Auth Key' + required: true + host: + description: 'Server host' + required: true + username: + description: 'Server username' + required: true + ssh-key: + description: 'Server SSH key' + required: true + ssh-port: + description: 'Server SSH port' + required: true + discord-webhook-url: + description: 'Discord webhook URL' + required: true + image-prefix: + description: 'Docker image prefix' + required: true + image-tag-type: + description: 'Image tag type (development-latest or semver)' + required: true + deploy-script: + description: 'Deploy script name (deploy-dev.sh or deploy.sh)' + required: true + default: 'deploy-dev.sh' + +runs: + using: 'composite' + steps: + - name: Inject application-secret.properties from Secrets + shell: bash + run: | + mkdir -p ./secret + echo "$SECRET_CONTENT" > ./secret/application-${{ inputs.environment }}-secret.properties + echo "$APPLE_KEY_CONTENT" > ./secret/AuthKey.p8 + chmod 600 ./secret/* + env: + SECRET_CONTENT: ${{ inputs.secret-properties }} + APPLE_KEY_CONTENT: ${{ inputs.apple-auth-key }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ inputs.dockerhub-username }} + password: ${{ inputs.dockerhub-token }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: docker.io/${{ inputs.image-prefix }}-${{ inputs.module }} + tags: ${{ inputs.image-tag-type }} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha,scope=${{ inputs.module }} + cache-to: type=gha,mode=max,scope=${{ inputs.module }} + build-args: | + MODULE=${{ inputs.module }} + + - name: Deploy to Server + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ inputs.host }} + username: ${{ inputs.username }} + key: ${{ inputs.ssh-key }} + port: ${{ inputs.ssh-port }} + script: | + export DOCKERHUB_USERNAME="${{ inputs.dockerhub-username }}" + export DOCKERHUB_TOKEN="${{ inputs.dockerhub-token }}" + export MODULE="${{ inputs.module }}" + export SERVER_PORT="${{ inputs.port }}" + export SPRING_PROFILE="${{ inputs.environment }}" + export IMAGE_TAG="$(echo "${{ steps.meta.outputs.tags }}" | head -n1)" + cd ~/deploy + chmod +x ./${{ inputs.deploy-script }} + ./${{ inputs.deploy-script }} + + - name: Send Discord notification on success (Development) + uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 + if: success() && inputs.environment == 'dev' + continue-on-error: true + shell: bash + with: + webhook-url: ${{ inputs.discord-webhook-url }} + embed-title: "✅ [${{ github.repository }}] Development Deploy Succeeded - ${{ inputs.module }}" + embed-description: | + **Module**: `${{ inputs.module }}` + **Port**: `${{ inputs.port }}` + **Commit**: `${{ github.sha }}` + **Author**: `${{ github.actor }}` + **Message**: `${{ github.event.head_commit.message }}` + [View Committed Changes](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) + embed-color: 65280 + + - name: Send Discord notification on success (Production) + uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 + if: success() && inputs.environment == 'prod' + continue-on-error: true + shell: bash + with: + webhook-url: ${{ inputs.discord-webhook-url }} + content: "🚀 **Production Deploy Succeeded!**" + embed-title: "✅ [${{ github.repository }}] Production Deploy Succeeded - ${{ inputs.module }}" + embed-description: | + **Module**: `${{ inputs.module }}` + **Port**: `${{ inputs.port }}` + **Deployed by**: `${{ github.actor }}` + The new version has been successfully deployed to production. + [View Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + embed-color: 65280 + + - name: Send Discord notification on failure (Development) + uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 + if: failure() && inputs.environment == 'dev' + continue-on-error: true + shell: bash + with: + webhook-url: ${{ inputs.discord-webhook-url }} + embed-title: "❌ [${{ github.repository }}] Development Deploy Failed - ${{ inputs.module }}" + embed-description: | + **Module**: `${{ inputs.module }}` + **Commit**: `${{ github.sha }}` + **Author**: `${{ github.actor }}` + An error occurred during the workflow execution. + [View Failed Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + embed-color: 16711680 + + - name: Send Discord notification on failure (Production) + uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 + if: failure() && inputs.environment == 'prod' + continue-on-error: true + shell: bash + with: + webhook-url: ${{ inputs.discord-webhook-url }} + content: "🚨 **Production Deploy Failed!**" + embed-title: "❌ [${{ github.repository }}] Production Deploy Failed - ${{ inputs.module }}" + embed-description: | + **Module**: `${{ inputs.module }}` + **Deployed by**: `${{ github.actor }}` + An error occurred during the production deployment workflow. + [View Failed Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + embed-color: 16711680 diff --git a/.github/workflows/prod-ci-cd.yml b/.github/workflows/prod-ci-cd.yml index 9c89683e..27e676d7 100644 --- a/.github/workflows/prod-ci-cd.yml +++ b/.github/workflows/prod-ci-cd.yml @@ -11,97 +11,45 @@ concurrency: env: REGISTRY: docker.io - IMAGE_NAME: ninecraft0523/ninecraft-server - MODULE: apis + IMAGE_PREFIX: ninecraft0523/ninecraft jobs: build-push-and-deploy: runs-on: ubuntu-24.04 timeout-minutes: 25 environment: production + strategy: + fail-fast: false + matrix: + include: + - module: apis + port: 8080 + # - module: admin # TODO: Uncomment when admin module is ready + # port: 8081 + - module: batch + port: 8082 steps: - name: Checkout code uses: actions/checkout@v4 - - name: Inject application-secret.properties from Secrets - run: | - mkdir ./secret - echo "${{ secrets.PROD_SECRET_PROPERTIES }}" > ./secret/application-prod-secret.properties - echo "${{ secrets.APPLE_AUTH_KEY }}" > ./secret/AuthKey.p8 - chmod 600 ./secret/* - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - type=raw,value=production-latest - - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - MODULE=${{ env.MODULE }} - - - name: Deploy to Production Server - uses: appleboy/ssh-action@v1.2.2 + - name: Deploy module + uses: ./.github/actions/deploy-module with: + environment: prod + module: ${{ matrix.module }} + port: ${{ matrix.port }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + secret-properties: ${{ secrets.PROD_SECRET_PROPERTIES }} + apple-auth-key: ${{ secrets.APPLE_AUTH_KEY }} host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USERNAME }} - key: ${{ secrets.PROD_SSH_KEY }} - port: ${{ secrets.PROD_PORT }} - script: | - export DOCKERHUB_USERNAME="${{ secrets.DOCKERHUB_USERNAME }}" - export DOCKERHUB_TOKEN="${{ secrets.DOCKERHUB_TOKEN }}" - export IMAGE_TAG="$(echo "${{ steps.meta.outputs.tags }}" | head -n1)" - export VERSION_TAG="${{ steps.meta.outputs.version }}" - export RELEASE_VERSION="${{ github.event.release.tag_name }}" - cd ~/deploy - chmod +x ./deploy.sh - ./deploy.sh - - - name: Send Discord notification on success - if: success() - uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_DISCORD_WEBHOOK_URL }} - content: "🚀 **Production Deploy Succeeded!**" - embed-title: "✅ [${{ github.repository }}] Release **${{ github.event.release.tag_name }}**" - embed-description: | - **Released by**: `${{ github.actor }}` - The new version has been successfully deployed to production. - [View Release Notes](https://github.com/${{ github.repository }}/releases/tag/${{ github.event.release.tag_name }}) - embed-color: 65280 # Green - - - name: Send Discord notification on failure - if: failure() - uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_DISCORD_WEBHOOK_URL }} - content: "🚨 **Production Deploy Failed!**" - embed-title: "❌ [${{ github.repository }}] Release **${{ github.event.release.tag_name }}**" - embed-description: | - **Released by**: `${{ github.actor }}` - An error occurred during the production deployment workflow. - [View Failed Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - embed-color: 16711680 # Red + ssh-key: ${{ secrets.PROD_SSH_KEY }} + ssh-port: ${{ secrets.PROD_PORT }} + discord-webhook-url: ${{ secrets.PROD_DEPLOY_DISCORD_WEBHOOK_URL }} + image-prefix: ${{ env.IMAGE_PREFIX }} + image-tag-type: | + type=semver,pattern={{version}} + type=raw,value=production-latest + deploy-script: deploy.sh From 9690272d85bd1a6dbc4c723d3fee630fa043931e Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 23:39:13 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[BOOK-350]=20fix:=20batch=20-=20batch=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=8F=AC=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- batch/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/batch/src/main/resources/application.yml b/batch/src/main/resources/application.yml index cf427e6e..ae7e42b9 100644 --- a/batch/src/main/resources/application.yml +++ b/batch/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8080 + port: 8082 shutdown: graceful spring: From 27f305915e2f6b5882e6db40f0a5d0e3f6ed6cf7 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 23:49:58 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[BOOK-350]=20chore:=20Server=20port=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=EB=8A=94=20docker=20compose=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/deploy-module/action.yml | 8 +++----- .github/workflows/dev-ci-cd.yml | 10 ---------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/actions/deploy-module/action.yml b/.github/actions/deploy-module/action.yml index f7aad6f0..ea9839ba 100644 --- a/.github/actions/deploy-module/action.yml +++ b/.github/actions/deploy-module/action.yml @@ -9,8 +9,9 @@ inputs: description: 'Module name (apis, admin, batch)' required: true port: - description: 'Server port' - required: true + description: 'Server port (for logging only - actual port defined in docker-compose.yml)' + required: false + default: 'N/A' dockerhub-username: description: 'Docker Hub username' required: true @@ -104,7 +105,6 @@ runs: export DOCKERHUB_USERNAME="${{ inputs.dockerhub-username }}" export DOCKERHUB_TOKEN="${{ inputs.dockerhub-token }}" export MODULE="${{ inputs.module }}" - export SERVER_PORT="${{ inputs.port }}" export SPRING_PROFILE="${{ inputs.environment }}" export IMAGE_TAG="$(echo "${{ steps.meta.outputs.tags }}" | head -n1)" cd ~/deploy @@ -121,7 +121,6 @@ runs: embed-title: "✅ [${{ github.repository }}] Development Deploy Succeeded - ${{ inputs.module }}" embed-description: | **Module**: `${{ inputs.module }}` - **Port**: `${{ inputs.port }}` **Commit**: `${{ github.sha }}` **Author**: `${{ github.actor }}` **Message**: `${{ github.event.head_commit.message }}` @@ -139,7 +138,6 @@ runs: embed-title: "✅ [${{ github.repository }}] Production Deploy Succeeded - ${{ inputs.module }}" embed-description: | **Module**: `${{ inputs.module }}` - **Port**: `${{ inputs.port }}` **Deployed by**: `${{ github.actor }}` The new version has been successfully deployed to production. [View Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 65e73d8e..e6956101 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -68,22 +68,13 @@ jobs: matrix: include: - module: apis - port: 8080 changed: ${{ needs.detect-changes.outputs.apis }} # - module: admin # TODO: Uncomment when admin module is ready - # port: 8081 # changed: ${{ needs.detect-changes.outputs.admin }} - module: batch - port: 8082 changed: ${{ needs.detect-changes.outputs.batch }} steps: - - name: Skip if no changes - if: matrix.changed != 'true' - run: | - echo "No changes detected for ${{ matrix.module }}, skipping deployment" - exit 0 - - name: Checkout code if: matrix.changed == 'true' uses: actions/checkout@v4 @@ -94,7 +85,6 @@ jobs: with: environment: dev module: ${{ matrix.module }} - port: ${{ matrix.port }} dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} secret-properties: ${{ secrets.DEV_SECRET_PROPERTIES }} From 8a6072ee1f13f5c8bf75f58590f6b249250ebdf2 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 23:50:11 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[BOOK-350]=20chore:=20shell=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=84=B8=EC=8A=A4=EB=A5=BC=20java=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=84=B8=EC=8A=A4=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= =?UTF-8?q?=ED=95=98=EC=97=AC=20graceful=20shutdown=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9d6f5ddd..5609903d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,5 +41,6 @@ ENV TZ=Asia/Seoul # JVM 실행 설정 # - Xms512m: 초기 힙 메모리 512MB # - Xmx1g: 최대 힙 메모리 1GB -# - server.port: Spring Boot 서버 포트 (컨테이너 실행 시 -e SERVER_PORT=xxxx로 주입 필수) -ENTRYPOINT ["sh", "-c", "java -Xms512m -Xmx1g -Duser.timezone=Asia/Seoul -Dserver.port=${SERVER_PORT} -jar app.jar"] +# - server.port: Spring Boot 서버 포트 (컨테이너 실행 시 -e SERVER_PORT=xxxx로 주입, 기본값: 8080) +# - exec: shell 프로세스를 java 프로세스로 대체하여 graceful shutdown 지원 +ENTRYPOINT ["sh", "-c", "exec java -Xms512m -Xmx1g -Duser.timezone=Asia/Seoul -Dserver.port=${SERVER_PORT:-8080} -jar app.jar"] From 89f3164fa9f9358130a64ed5fd98e7fdce532d58 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 23:50:40 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[BOOK-350]=20chore:=20prod=EB=8F=84=20Ser?= =?UTF-8?q?ver=20port=20=EC=A0=95=EC=9D=98=EB=A5=BC=20docker=20compose?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod-ci-cd.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/prod-ci-cd.yml b/.github/workflows/prod-ci-cd.yml index 27e676d7..7558b00e 100644 --- a/.github/workflows/prod-ci-cd.yml +++ b/.github/workflows/prod-ci-cd.yml @@ -23,11 +23,8 @@ jobs: matrix: include: - module: apis - port: 8080 # - module: admin # TODO: Uncomment when admin module is ready - # port: 8081 - module: batch - port: 8082 steps: - name: Checkout code @@ -38,7 +35,6 @@ jobs: with: environment: prod module: ${{ matrix.module }} - port: ${{ matrix.port }} dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} secret-properties: ${{ secrets.PROD_SECRET_PROPERTIES }} From 9a2bc7734e60a2feff32de47999894c29d7fa1e4 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Mon, 20 Oct 2025 23:55:35 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[BOOK-350]=20chore:=20prod=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20deploy=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=84?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/deploy-module/action.yml | 2 +- .github/workflows/prod-ci-cd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/deploy-module/action.yml b/.github/actions/deploy-module/action.yml index ea9839ba..d8b0a924 100644 --- a/.github/actions/deploy-module/action.yml +++ b/.github/actions/deploy-module/action.yml @@ -46,7 +46,7 @@ inputs: description: 'Image tag type (development-latest or semver)' required: true deploy-script: - description: 'Deploy script name (deploy-dev.sh or deploy.sh)' + description: 'Deploy script name (deploy-dev.sh or deploy-prod.sh)' required: true default: 'deploy-dev.sh' diff --git a/.github/workflows/prod-ci-cd.yml b/.github/workflows/prod-ci-cd.yml index 7558b00e..a98c238b 100644 --- a/.github/workflows/prod-ci-cd.yml +++ b/.github/workflows/prod-ci-cd.yml @@ -48,4 +48,4 @@ jobs: image-tag-type: | type=semver,pattern={{version}} type=raw,value=production-latest - deploy-script: deploy.sh + deploy-script: deploy-prod.sh