From 542718f64411dc85a7b505490fb8e695a3180de2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 28 May 2025 13:51:17 +0000 Subject: [PATCH] release --- build.gradle | 35 +++ deployment/Jenkinsfile | 186 +++++++++++ deployment/deploy.yaml.template | 290 ++++++++++++++++++ deployment/deploy_env_vars | 23 ++ .../lifesub/member/config/SecurityConfig.java | 2 +- member/src/main/resources/application.yml | 16 + .../mysub/infra/config/SecurityConfig.java | 2 +- .../src/main/resources/application.yml | 16 + .../recommend/config/SecurityConfig.java | 2 +- recommend/src/main/resources/application.yml | 16 + 10 files changed, 585 insertions(+), 3 deletions(-) create mode 100644 deployment/Jenkinsfile create mode 100644 deployment/deploy.yaml.template create mode 100644 deployment/deploy_env_vars diff --git a/build.gradle b/build.gradle index 1cc249c..dbfa32d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ plugins { id 'org.springframework.boot' version '3.4.0' apply false //id 'io.spring.dependency-management' version '1.1.6' apply false id 'java' + + id "org.sonarqube" version "5.0.0.4638" apply false } allprojects { @@ -15,6 +17,13 @@ subprojects { apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' + apply plugin: 'org.sonarqube' + apply plugin: 'jacoco' // 서브 프로젝트에 JaCoCo 플러그인 적용 + + jacoco { + toolVersion = "0.8.11" // JaCoCo 최신 버전 사용 + } + repositories { mavenCentral() } @@ -42,6 +51,9 @@ subprojects { // Lombok for Tests testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + + // Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' } // Test Configuration @@ -58,6 +70,29 @@ subprojects { include '**/*Test.class' testLogging { events "passed", "skipped", "failed" + } + finalizedBy jacocoTestReport // 테스트 후 JaCoCo 리포트 생성 + } + + jacocoTestReport { + dependsOn test + reports { + xml.required = true // SonarQube 분석을 위해 XML 형식 필요 + csv.required = false + html.required = true + html.outputLocation = layout.buildDirectory.dir("jacocoHtml").get().asFile + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "**/config/**", // 설정 클래스 제외 + "**/entity/**", // 엔티티 클래스 제외 + "**/dto/**", // DTO 클래스 제외 + "**/*Application.class", // 메인 애플리케이션 클래스 제외 + "**/exception/**" // 예외 클래스 제외 + ]) + })) } } } diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile new file mode 100644 index 0000000..99a2f83 --- /dev/null +++ b/deployment/Jenkinsfile @@ -0,0 +1,186 @@ +def PIPELINE_ID = "${env.BUILD_NUMBER}" + +def getImageTag() { + def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss') + def currentDate = new Date() + return dateFormat.format(currentDate) +} + +podTemplate( + label: "${PIPELINE_ID}", + serviceAccount: 'jenkins', + containers: [ + containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true), + containerTemplate(name: 'gradle', + image: 'gradle:jdk17', + ttyEnabled: true, + command: 'cat', + envVars: [ + envVar(key: 'DOCKER_HOST', value: 'unix:///run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true') + ]), + containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true), + containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h') + ], + volumes: [ + emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false), + emptyDirVolume(mountPath: '/root/.azure', memory: false), + emptyDirVolume(mountPath: '/run/podman', memory: false) + ] +) { + node(PIPELINE_ID) { + def props + def imageTag = getImageTag() + def manifest = "deploy.yaml" + def namespace + def services = ['member', 'mysub', 'recommend'] + + stage("Get Source") { + checkout scm + props = readProperties file: "deployment/deploy_env_vars" + namespace = "${props.namespace}" + } + + stage("Setup AKS") { + container('azure-cli') { + withCredentials([azureServicePrincipal('azure-credentials')]) { + sh """ + az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + az aks get-credentials --resource-group rg-digitalgarage-01 --name aks-digitalgarage-01 --overwrite-existing + kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f - + """ + } + } + } + + stage('Build Applications & SonarQube Analysis') { + container('podman') { + sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2' + } + + container('gradle') { + def testContainersConfig = '''docker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy +docker.host=unix:///run/podman/podman.sock +ryuk.container.privileged=true +testcontainers.reuse.enable=true''' + + sh """ + # TestContainers 설정 + mkdir -p member/src/test/resources mysub-infra/src/test/resources recommend/src/test/resources + echo '${testContainersConfig}' > member/src/test/resources/testcontainers.properties + echo '${testContainersConfig}' > mysub-infra/src/test/resources/testcontainers.properties + echo '${testContainersConfig}' > recommend/src/test/resources/testcontainers.properties + """ + + // 빌드 및 SonarQube 분석 + withSonarQubeEnv('SonarQube') { + sh """ + chmod +x gradlew + + # 빌드 실행 + ./gradlew :member:build :mysub-infra:build :recommend:build -x test + + # Member 서비스 + ./gradlew :member:test :member:jacocoTestReport :member:sonar \ + -Dsonar.projectKey=lifesub-member-dg0400 \ + -Dsonar.projectName=lifesub-member \ + -Dsonar.java.binaries=build/classes/java/main \ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ + -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** + + # Recommend 서비스 + ./gradlew :recommend:test :recommend:jacocoTestReport :recommend:sonar \ + -Dsonar.projectKey=lifesub-recommend-dg0400 \ + -Dsonar.projectName=lifesub-recommend \ + -Dsonar.java.binaries=build/classes/java/main \ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ + -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** + + # Mysub 서비스 (biz & infra 구조) + ./gradlew :mysub-infra:test :mysub-infra:jacocoTestReport :mysub-infra:sonar \ + -Dsonar.projectKey=lifesub-mysub-dg0400 \ + -Dsonar.projectName=lifesub-mysub \ + -Dsonar.java.binaries=build/classes/java/main \ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ + -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** + """ + } + } + } + + stage('Quality Gate') { + timeout(time: 10, unit: 'MINUTES') { + def qg = waitForQualityGate() + if (qg.status != 'OK') { + error "Pipeline aborted due to quality gate failure: ${qg.status}" + } + } + } + + stage('Build & Push Images') { + container('podman') { + withCredentials([usernamePassword( + credentialsId: 'acr-credentials', + usernameVariable: 'USERNAME', + passwordVariable: 'PASSWORD' + )]) { + sh "podman login ${props.registry} --username \$USERNAME --password \$PASSWORD" + + services.each { service -> + def buildDir = service == 'mysub' ? 'mysub-infra' : service + def jarFile = service == 'mysub' ? 'mysub.jar' : "${service}.jar" + + sh """ + podman build \ + --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \ + --build-arg ARTIFACTORY_FILE="${jarFile}" \ + -f deployment/container/Dockerfile \ + -t ${props.registry}/${props.image_org}/${service}:${imageTag} . + + podman push ${props.registry}/${props.image_org}/${service}:${imageTag} + """ + } + } + } + } + + stage('Generate & Apply Manifest') { + container('envsubst') { + sh """ + export namespace=${namespace} + export allowed_origins=${props.allowed_origins} + export jwt_secret_key=${props.jwt_secret_key} + export postgres_user=${props.postgres_user} + export postgres_password=${props.postgres_password} + export replicas=${props.replicas} + export resources_requests_cpu=${props.resources_requests_cpu} + export resources_requests_memory=${props.resources_requests_memory} + export resources_limits_cpu=${props.resources_limits_cpu} + export resources_limits_memory=${props.resources_limits_memory} + + # 이미지 경로 환경변수 설정 + export member_image_path=${props.registry}/${props.image_org}/member:${imageTag} + export mysub_image_path=${props.registry}/${props.image_org}/mysub:${imageTag} + export recommend_image_path=${props.registry}/${props.image_org}/recommend:${imageTag} + + # manifest 생성 + envsubst < deployment/${manifest}.template > deployment/${manifest} + echo "Generated manifest file:" + cat deployment/${manifest} + """ + } + + container('azure-cli') { + sh """ + kubectl apply -f deployment/${manifest} + + echo "Waiting for deployments to be ready..." + kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/mysub --timeout=300s + kubectl -n ${namespace} wait --for=condition=available deployment/recommend --timeout=300s + """ + } + } + } +} diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template new file mode 100644 index 0000000..5e2b2c1 --- /dev/null +++ b/deployment/deploy.yaml.template @@ -0,0 +1,290 @@ +# ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-config + namespace: ${namespace} +data: + ALLOWED_ORIGINS: ${allowed_origins} + JPA_DDL_AUTO: "update" + JPA_SHOW_SQL: "true" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: member-config + namespace: ${namespace} +data: + SERVER_PORT: "8081" + POSTGRES_HOST: "postgres-member-postgresql" + POSTGRES_PORT: "5432" + POSTGRES_DB: "member" + JWT_ACCESS_TOKEN_VALIDITY: "3600000" + JWT_REFRESH_TOKEN_VALIDITY: "86400000" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysub-config + namespace: ${namespace} +data: + SERVER_PORT: "8082" + POSTGRES_HOST: "postgres-mysub-postgresql" + POSTGRES_PORT: "5432" + POSTGRES_DB: "mysub" + FEE_LEVEL_COLLECTOR: "50000" + FEE_LEVEL_ADDICT: "100000" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: recommend-config + namespace: ${namespace} +data: + SERVER_PORT: "8083" + POSTGRES_HOST: "postgres-recommend-postgresql" + POSTGRES_PORT: "5432" + POSTGRES_DB: "recommend" + +--- +# Secrets +apiVersion: v1 +kind: Secret +metadata: + name: common-secret + namespace: ${namespace} +type: Opaque +stringData: + JWT_SECRET_KEY: ${jwt_secret_key} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: database-secret + namespace: ${namespace} +type: Opaque +stringData: + POSTGRES_USER: ${postgres_user} + POSTGRES_PASSWORD: ${postgres_password} + +--- +# Deployments +apiVersion: apps/v1 +kind: Deployment +metadata: + name: member + namespace: ${namespace} + labels: + app: member +spec: + replicas: ${replicas} + selector: + matchLabels: + app: member + template: + metadata: + labels: + app: member + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: member + image: ${member_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8081 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: member-config + - secretRef: + name: common-secret + - secretRef: + name: database-secret + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysub + namespace: ${namespace} + labels: + app: mysub +spec: + replicas: ${replicas} + selector: + matchLabels: + app: mysub + template: + metadata: + labels: + app: mysub + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: mysub + image: ${mysub_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8082 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: mysub-config + - secretRef: + name: common-secret + - secretRef: + name: database-secret + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: recommend + namespace: ${namespace} + labels: + app: recommend +spec: + replicas: ${replicas} + selector: + matchLabels: + app: recommend + template: + metadata: + labels: + app: recommend + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: recommend + image: ${recommend_image_path} + imagePullPolicy: Always + ports: + - containerPort: 8083 + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + envFrom: + - configMapRef: + name: common-config + - configMapRef: + name: recommend-config + - secretRef: + name: common-secret + - secretRef: + name: database-secret + +--- +# Services +apiVersion: v1 +kind: Service +metadata: + name: member-service + namespace: ${namespace} + labels: + app: member +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8081 + protocol: TCP + selector: + app: member + +--- +apiVersion: v1 +kind: Service +metadata: + name: mysub-service + namespace: ${namespace} + labels: + app: mysub +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8082 + protocol: TCP + selector: + app: mysub + +--- +apiVersion: v1 +kind: Service +metadata: + name: recommend-service + namespace: ${namespace} + labels: + app: recommend +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8083 + protocol: TCP + selector: + app: recommend + +--- +# Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backend-ingress + namespace: ${namespace} + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: dg0400.20.214.196.128.nip.io + http: + paths: + - path: /api/auth + pathType: Prefix + backend: + service: + name: member-service + port: + number: 80 + - path: /api/mysub + pathType: Prefix + backend: + service: + name: mysub-service + port: + number: 80 + - path: /api/recommend + pathType: Prefix + backend: + service: + name: recommend-service + port: + number: 80 diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars new file mode 100644 index 0000000..7229000 --- /dev/null +++ b/deployment/deploy_env_vars @@ -0,0 +1,23 @@ +# Team Settings +teamid=dg0400 +root_project=lifesub +namespace=dg0400-lifesub-ns + +# Container Registry Settings +registry=acrdigitalgarage01.azurecr.io +image_org=dg0400 + +# Application Settings +replicas=1 +allowed_origins=http://20.214.195.54 + +# Security Settings +jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ +postgres_user=dg0400 +postgres_password=Hi5Jessica! + +# Resource Settings +resources_requests_cpu=256m +resources_requests_memory=256Mi +resources_limits_cpu=1024m +resources_limits_memory=1024Mi diff --git a/member/src/main/java/com/unicorn/lifesub/member/config/SecurityConfig.java b/member/src/main/java/com/unicorn/lifesub/member/config/SecurityConfig.java index a3c68e7..5a43ef7 100644 --- a/member/src/main/java/com/unicorn/lifesub/member/config/SecurityConfig.java +++ b/member/src/main/java/com/unicorn/lifesub/member/config/SecurityConfig.java @@ -62,7 +62,7 @@ public class SecurityConfig { ) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers(HttpMethod.GET, "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers(HttpMethod.GET, "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll() .anyRequest().authenticated() ) diff --git a/member/src/main/resources/application.yml b/member/src/main/resources/application.yml index 2d122ef..b3f2e20 100644 --- a/member/src/main/resources/application.yml +++ b/member/src/main/resources/application.yml @@ -35,3 +35,19 @@ logging: level: com.unicorn: DEBUG org.hibernate.SQL: TRACE +# Actuator 설정 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + probes: + enabled: true + health: + livenessState: + enabled: true + readinessState: + enabled: true \ No newline at end of file diff --git a/mysub-infra/src/main/java/com/unicorn/lifesub/mysub/infra/config/SecurityConfig.java b/mysub-infra/src/main/java/com/unicorn/lifesub/mysub/infra/config/SecurityConfig.java index 7cc3af5..31199b6 100644 --- a/mysub-infra/src/main/java/com/unicorn/lifesub/mysub/infra/config/SecurityConfig.java +++ b/mysub-infra/src/main/java/com/unicorn/lifesub/mysub/infra/config/SecurityConfig.java @@ -48,7 +48,7 @@ public class SecurityConfig { ) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**", "/actuator/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session diff --git a/mysub-infra/src/main/resources/application.yml b/mysub-infra/src/main/resources/application.yml index 5fded7b..aca8836 100644 --- a/mysub-infra/src/main/resources/application.yml +++ b/mysub-infra/src/main/resources/application.yml @@ -39,3 +39,19 @@ logging: level: com.unicorn: DEBUG org.hibernate.SQL: TRACE +# Actuator 설정 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + probes: + enabled: true + health: + livenessState: + enabled: true + readinessState: + enabled: true \ No newline at end of file diff --git a/recommend/src/main/java/com/unicorn/lifesub/recommend/config/SecurityConfig.java b/recommend/src/main/java/com/unicorn/lifesub/recommend/config/SecurityConfig.java index bbb87a1..5b38bae 100644 --- a/recommend/src/main/java/com/unicorn/lifesub/recommend/config/SecurityConfig.java +++ b/recommend/src/main/java/com/unicorn/lifesub/recommend/config/SecurityConfig.java @@ -39,7 +39,7 @@ public class SecurityConfig { ) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**", "/actuator/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session diff --git a/recommend/src/main/resources/application.yml b/recommend/src/main/resources/application.yml index d65dcd2..2ec9e12 100644 --- a/recommend/src/main/resources/application.yml +++ b/recommend/src/main/resources/application.yml @@ -33,3 +33,19 @@ logging: com.unicorn: DEBUG org.hibernate.SQL: TRACE +# Actuator 설정 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + probes: + enabled: true + health: + livenessState: + enabled: true + readinessState: + enabled: true \ No newline at end of file