Merge branch 'cicd'

add testcode
This commit is contained in:
ondal 2025-03-15 02:43:34 +09:00
commit 6095e1ac25
224 changed files with 4877 additions and 40 deletions

274
.github/workflows/cicd.yaml vendored Normal file
View File

@ -0,0 +1,274 @@
name: Backend Services CI/CD
on:
push:
#branches: [ cicd ]
paths:
- 'member/**'
- 'mysub/**'
- 'recommend/**'
- 'common/**'
- 'deployment/**'
- '.github/workflows/**'
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_outputs.outputs.image_tag }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
cache: 'gradle'
- name: Load environment variables
id: env_vars
run: |
# Read environment variables from file
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
# Extract key-value pairs
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
# Set GitHub environment variables
echo "$key=$value" >> $GITHUB_ENV
done < deployment/deploy_env_vars
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: |
./gradlew :member:build :mysub-infra:build :recommend:build -x test
- name: Test with Gradle
run: |
./gradlew :member:test :member:jacocoTestReport
./gradlew :mysub-infra:test :mysub-infra:jacocoTestReport
./gradlew :recommend:test :recommend:jacocoTestReport
- name: SonarQube Analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: |
./gradlew :member:sonar \
-Dsonar.projectKey=lifesub-member \
-Dsonar.projectName=lifesub-member \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.java.binaries=build/classes/java/main \
-Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
./gradlew :mysub-infra:sonar \
-Dsonar.projectKey=lifesub-mysub \
-Dsonar.projectName=lifesub-mysub \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.java.binaries=build/classes/java/main \
-Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
./gradlew :recommend:sonar \
-Dsonar.projectKey=lifesub-recommend \
-Dsonar.projectName=lifesub-recommend \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.java.binaries=build/classes/java/main \
-Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: app-builds
path: |
member/build/libs/*.jar
mysub-infra/build/libs/*.jar
recommend/build/libs/*.jar
- name: Set outputs
id: set_outputs
run: |
# Generate timestamp for image tag
IMAGE_TAG=$(date +%Y%m%d%H%M%S)
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
release:
name: Build and Push Docker Images
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: app-builds
- name: Load environment variables
run: |
# Read environment variables from file
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
# Extract key-value pairs
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
# Set GitHub environment variables
echo "$key=$value" >> $GITHUB_ENV
done < deployment/deploy_env_vars
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.registry }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Member service image
uses: docker/build-push-action@v5
with:
context: .
file: deployment/Dockerfile
push: true
tags: ${{ env.registry }}/${{ env.image_org }}/member:${{ needs.build.outputs.image_tag }}
build-args: |
BUILD_LIB_DIR=member/build/libs
ARTIFACTORY_FILE=member.jar
- name: Build and push MySub service image
uses: docker/build-push-action@v5
with:
context: .
file: deployment/Dockerfile
push: true
tags: ${{ env.registry }}/${{ env.image_org }}/mysub:${{ needs.build.outputs.image_tag }}
build-args: |
BUILD_LIB_DIR=mysub-infra/build/libs
ARTIFACTORY_FILE=mysub.jar
- name: Build and push Recommend service image
uses: docker/build-push-action@v5
with:
context: .
file: deployment/Dockerfile
push: true
tags: ${{ env.registry }}/${{ env.image_org }}/recommend:${{ needs.build.outputs.image_tag }}
build-args: |
BUILD_LIB_DIR=recommend/build/libs
ARTIFACTORY_FILE=recommend.jar
deploy:
name: Deploy to Kubernetes
needs: [build, release]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Load environment variables
run: |
# Read environment variables from file
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
# Extract key-value pairs
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
# Set GitHub environment variables
echo "$key=$value" >> $GITHUB_ENV
done < deployment/deploy_env_vars
- name: Set image tag environment variable
run: |
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
# Azure CLI 설치 단계 수정
- name: Install Azure CLI
run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Setup kubectl
uses: azure/setup-kubectl@v3
- name: Get AKS Credentials
run: |
az aks get-credentials --resource-group ictcoe-edu --name ${{ env.teamid }}-aks --overwrite-existing
- name: Create namespace
run: |
kubectl create namespace ${{ env.namespace }} --dry-run=client -o yaml | kubectl apply -f -
- name: Install envsubst
run: |
sudo apt-get update
sudo apt-get install -y gettext-base
- name: Generate Kubernetes manifest
run: |
# Set environment variables for the deployment template
export namespace=${{ env.namespace }}
export allowed_origins=${{ env.allowed_origins }}
export jwt_secret_key=${{ env.jwt_secret_key }}
export postgres_user=${{ env.postgres_user }}
export postgres_password=${{ env.postgres_password }}
export replicas=${{ env.replicas }}
export resources_requests_cpu=${{ env.resources_requests_cpu }}
export resources_requests_memory=${{ env.resources_requests_memory }}
export resources_limits_cpu=${{ env.resources_limits_cpu }}
export resources_limits_memory=${{ env.resources_limits_memory }}
# Set image paths with the dynamic tag
export member_image_path=${{ env.registry }}/${{ env.image_org }}/member:${{ env.IMAGE_TAG }}
export mysub_image_path=${{ env.registry }}/${{ env.image_org }}/mysub:${{ env.IMAGE_TAG }}
export recommend_image_path=${{ env.registry }}/${{ env.image_org }}/recommend:${{ env.IMAGE_TAG }}
# Generate the manifest file using envsubst
envsubst < deployment/deploy.yaml.template > deployment/deploy.yaml
# Print manifest for debugging
echo "Generated Kubernetes manifest:"
cat deployment/deploy.yaml
- name: Apply Kubernetes manifest
run: |
kubectl apply -f deployment/deploy.yaml
- name: Wait for deployments to be ready
run: |
kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/member --timeout=300s
kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/mysub --timeout=300s
kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/recommend --timeout=300s
- name: Get service information
run: |
echo "Ingress IP: $(kubectl -n ${{ env.namespace }} get ingress lifesub -o jsonpath='{.status.loadBalancer.ingress[0].ip}')"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,2 +1,2 @@
#Wed Feb 12 15:21:47 KST 2025 #Sat Feb 15 20:31:45 KST 2025
gradle.version=8.4 gradle.version=8.10

Binary file not shown.

9
.idea/compiler.xml generated
View File

@ -8,13 +8,18 @@
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" /> <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" />
</processorPath> </processorPath>
<module name="lifesub.recommend.test" />
<module name="lifesub.common.main" />
<module name="lifesub.mysub-infra.test" />
<module name="lifesub.mysub-infra.main" /> <module name="lifesub.mysub-infra.main" />
<module name="lifesub.common.test" />
<module name="lifesub.member.test" />
<module name="lifesub.mysub-biz.main" /> <module name="lifesub.mysub-biz.main" />
<module name="lifesub.recommend.main" /> <module name="lifesub.recommend.main" />
<module name="lifesub.common.main" />
<module name="lifesub.member.main" /> <module name="lifesub.member.main" />
<module name="lifesub.mysub-biz.test" />
</profile> </profile>
</annotationProcessing> </annotationProcessing>
<bytecodeTargetLevel target="17" /> <bytecodeTargetLevel target="21" />
</component> </component>
</project> </project>

3
.idea/dictionaries/hiond.xml generated Normal file
View File

@ -0,0 +1,3 @@
<component name="ProjectDictionaryState">
<dictionary name="hiond" />
</component>

1
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

2
.idea/misc.xml generated
View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="23" project-jdk-type="JavaSDK" />
</project> </project>

View File

@ -2,6 +2,8 @@ plugins {
id 'org.springframework.boot' version '3.4.0' apply false id 'org.springframework.boot' version '3.4.0' apply false
//id 'io.spring.dependency-management' version '1.1.6' apply false //id 'io.spring.dependency-management' version '1.1.6' apply false
id 'java' id 'java'
id "org.sonarqube" version "5.0.0.4638" apply false //apply false
} }
allprojects { allprojects {
@ -15,25 +17,82 @@ subprojects {
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.sonarqube'
apply plugin: 'jacoco' // JaCoCo
jacoco {
toolVersion = "0.8.11" // JaCoCo
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
// Spring Boot Starters
implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop' // AOP:
// Utils
implementation 'com.google.code.gson:gson'
// Lombok // Lombok
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
// Test // Test Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
// Lombok for Tests
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
// Test Configuration
sourceSets {
test {
java {
srcDirs = ['src/test/java']
}
}
} }
test { test {
useJUnitPlatform() useJUnitPlatform()
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/**" //
])
}))
}
}
} }
//-- Biz와 common //-- Biz와 common
@ -42,6 +101,10 @@ configure(subprojects.findAll { !it.name.endsWith('-biz') && it.name != 'common'
// Spring Boot // Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// data // data
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// JWT // JWT
@ -50,10 +113,19 @@ configure(subprojects.findAll { !it.name.endsWith('-biz') && it.name != 'common'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// AOP:
implementation 'org.springframework.boot:spring-boot-starter-aop'
// Swagger // Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
//-- spring security test
testImplementation 'org.springframework.security:spring-security-test'
// Test Containers
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:junit-jupiter'
// WebFlux for WebMvc Testing
implementation 'org.springframework.boot:spring-boot-starter-webflux'
} }
} }

25
deployment/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# Build stage
FROM openjdk:23-oraclelinux8 AS builder
ARG BUILD_LIB_DIR
ARG ARTIFACTORY_FILE
COPY ${BUILD_LIB_DIR}/${ARTIFACTORY_FILE} app.jar
# Run stage
FROM openjdk:23-slim
ENV USERNAME k8s
ENV ARTIFACTORY_HOME /home/${USERNAME}
ENV JAVA_OPTS=""
# Add a non-root user
RUN adduser --system --group ${USERNAME} && \
mkdir -p ${ARTIFACTORY_HOME} && \
chown ${USERNAME}:${USERNAME} ${ARTIFACTORY_HOME}
WORKDIR ${ARTIFACTORY_HOME}
COPY --from=builder app.jar app.jar
RUN chown ${USERNAME}:${USERNAME} app.jar
USER ${USERNAME}
ENTRYPOINT [ "sh", "-c" ]
CMD ["java ${JAVA_OPTS} -jar app.jar"]

187
deployment/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,187 @@
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 ictcoe-edu --name ${props.teamid}-aks --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
"""
// Member 서비스 빌드 및 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 \
-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 \
-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 \
-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/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}
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
"""
}
}
}
}

View File

@ -0,0 +1,182 @@
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'),
containerTemplate(name: 'git', image: 'alpine/git:latest', command: 'cat', ttyEnabled: true)
],
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']
def MANIFEST_REPO = "https://github.com/cna-bootcamp/lifesub-manifest.git"
def MANIFEST_BRANCH = "main"
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 ictcoe-edu --name ${props.teamid}-aks --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
"""
// Member 서비스 빌드 및 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 \
-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 \
-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 \
-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/Dockerfile \
-t ${props.registry}/${props.image_org}/${service}:${imageTag} .
podman push ${props.registry}/${props.image_org}/${service}:${imageTag}
"""
}
}
}
}
stage('Update Manifest Repository') {
container('git') {
// 임시 디렉토리 생성
sh "mkdir -p /tmp/manifests"
// Clone manifest repository
withCredentials([usernamePassword(credentialsId: 'github-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
sh """
git config --global user.email "jenkins@example.com"
git config --global user.name "Jenkins Pipeline"
git clone https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/cna-bootcamp/lifesub-manifest.git /tmp/manifests
cd /tmp/manifests
# Update image tags in the appropriate YAML files
for service in ${services.join(' ')}; do
# 백엔드 서비스 이미지 태그 업데이트
if [ -f lifesub/deployments/\${service}-deployment.yaml ]; then
sed -i "s|image: ${props.registry}/${props.image_org}/\${service}:.*|image: ${props.registry}/${props.image_org}/\${service}:${imageTag}|g" lifesub/deployments/\${service}-deployment.yaml
fi
done
# Commit and push changes
git add .
git commit -m "Update backend service image tags to ${imageTag}" || true
git push
"""
}
}
}
}
}

View File

@ -0,0 +1,68 @@
#!/bin/bash
# 사용법 함수 정의
usage() {
echo "Usage: $0 <namespace>"
echo "Example: $0 myapp-ns"
echo "This script creates PostgreSQL databases for member, mysub, and recommend services in the specified namespace."
exit 1
}
# 파라미터 체크
if [ $# -ne 1 ]; then
usage
fi
NAMESPACE=$1
# Namespace 존재 여부 확인 후 생성
if ! kubectl get namespace ${NAMESPACE} &> /dev/null; then
echo "Creating namespace: ${NAMESPACE}"
kubectl create namespace ${NAMESPACE}
fi
# Namespace 전환
echo "Switching to namespace: ${NAMESPACE}"
kubens ${NAMESPACE}
# 각 서비스별 설치
for service in member mysub recommend; do
echo "Installing PostgreSQL for ${service} service..."
# Helm으로 PostgreSQL 설치 - heredoc으로 직접 values 전달
helm upgrade -i ${service} bitnami/postgresql --version 14.3.2 --values - <<EOF
architecture: standalone
global:
postgresql:
auth:
postgresPassword: "Passw0rd"
replicationPassword: "Passw0rd"
database: "${service}"
username: "admin"
password: "Passw0rd"
storageClass: "managed"
primary:
persistence:
enabled: true
storageClass: "managed"
size: 10Gi
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "0.5Gi"
cpu: "0.5"
service:
type: ClusterIP
ports:
postgresql: 5432
securityContext:
enabled: true
fsGroup: 1001
runAsUser: 1001
EOF
done
echo "Installation completed successfully in namespace: ${NAMESPACE}"

View File

@ -0,0 +1,346 @@
# 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:
POSTGRES_DB: member
POSTGRES_HOST: member-postgresql
POSTGRES_PORT: '5432'
SERVER_PORT: '8081'
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mysub-config
namespace: ${namespace}
data:
FEE_LEVEL_ADDICT: '100000'
FEE_LEVEL_COLLECTOR: '50000'
POSTGRES_DB: mysub
POSTGRES_HOST: mysub-postgresql
POSTGRES_PORT: '5432'
SERVER_PORT: '8082'
---
apiVersion: v1
kind: ConfigMap
metadata:
name: recommend-config
namespace: ${namespace}
data:
POSTGRES_DB: recommend
POSTGRES_HOST: recommend-postgresql
POSTGRES_PORT: '5432'
SERVER_PORT: '8083'
---
# Secrets
apiVersion: v1
kind: Secret
metadata:
name: common-secret
namespace: ${namespace}
stringData:
JWT_SECRET_KEY: ${jwt_secret_key}
type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
name: member-secret
namespace: ${namespace}
stringData:
JWT_ACCESS_TOKEN_VALIDITY: '3600000'
JWT_REFRESH_TOKEN_VALIDITY: '86400000'
POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
name: mysub-secret
namespace: ${namespace}
stringData:
POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
name: recommend-secret
namespace: ${namespace}
stringData:
POSTGRES_PASSWORD: ${postgres_password}
POSTGRES_USER: ${postgres_user}
type: Opaque
---
# Deployments
apiVersion: apps/v1
kind: Deployment
metadata:
name: member
namespace: ${namespace}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: member
template:
metadata:
labels:
app: member
spec:
containers:
- name: member
image: ${member_image_path}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: member-config
- secretRef:
name: common-secret
- secretRef:
name: member-secret
resources:
requests:
cpu: ${resources_requests_cpu}
memory: ${resources_requests_memory}
limits:
cpu: ${resources_limits_cpu}
memory: ${resources_limits_memory}
ports:
- containerPort: 8081
startupProbe:
httpGet:
path: /actuator/health
port: 8081
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 60
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysub
namespace: ${namespace}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: mysub
template:
metadata:
labels:
app: mysub
spec:
containers:
- name: mysub
image: ${mysub_image_path}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: mysub-config
- secretRef:
name: common-secret
- secretRef:
name: mysub-secret
resources:
requests:
cpu: ${resources_requests_cpu}
memory: ${resources_requests_memory}
limits:
cpu: ${resources_limits_cpu}
memory: ${resources_limits_memory}
ports:
- containerPort: 8082
startupProbe:
httpGet:
path: /actuator/health
port: 8082
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8082
initialDelaySeconds: 60
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: recommend
namespace: ${namespace}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: recommend
template:
metadata:
labels:
app: recommend
spec:
containers:
- name: recommend
image: ${recommend_image_path}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: recommend-config
- secretRef:
name: common-secret
- secretRef:
name: recommend-secret
resources:
requests:
cpu: ${resources_requests_cpu}
memory: ${resources_requests_memory}
limits:
cpu: ${resources_limits_cpu}
memory: ${resources_limits_memory}
ports:
- containerPort: 8083
startupProbe:
httpGet:
path: /actuator/health
port: 8083
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8083
initialDelaySeconds: 60
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8083
initialDelaySeconds: 10
periodSeconds: 5
---
# Services
apiVersion: v1
kind: Service
metadata:
name: member
namespace: ${namespace}
spec:
selector:
app: member
ports:
- port: 80
targetPort: 8081
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: mysub
namespace: ${namespace}
spec:
selector:
app: mysub
ports:
- port: 80
targetPort: 8082
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: recommend
namespace: ${namespace}
spec:
selector:
app: recommend
ports:
- port: 80
targetPort: 8083
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lifesub
namespace: ${namespace}
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /member(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: member
port:
number: 80
- path: /mysub(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: mysub
port:
number: 80
- path: /recommend(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: recommend
port:
number: 80

View File

@ -0,0 +1,23 @@
# Team Settings
teamid=unicorn
root_project=lifesub
namespace=unicorn-lifesub-ns
# Container Registry Settings
registry=unicorncr.azurecr.io
image_org=lifesub
# Application Settings
replicas=1
allowed_origins=http://4.230.147.248
# Security Settings
jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ
postgres_user=admin
postgres_password=Passw0rd
# Resource Settings
resources_requests_cpu=256m
resources_requests_memory=256Mi
resources_limits_cpu=1024m
resources_limits_memory=1024Mi

View File

@ -0,0 +1,9 @@
# lifesub/deployment/manifest/configmaps/common-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: common-config
data:
JPA_DDL_AUTO: update
JPA_SHOW_SQL: "true"
ALLOWED_ORIGINS: "http://localhost:18080,http://localhost:18081,http://20.214.113.12"

View File

@ -0,0 +1,10 @@
# lifesub/deployment/manifest/configmaps/member-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: member-config
data:
SERVER_PORT: "8081"
POSTGRES_HOST: "member-postgresql"
POSTGRES_PORT: "5432"
POSTGRES_DB: "member"

View File

@ -0,0 +1,13 @@
# lifesub/deployment/manifest/configmaps/mysub-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysub-config
data:
SERVER_PORT: "8082"
POSTGRES_HOST: "mysub-postgresql"
POSTGRES_PORT: "5432"
POSTGRES_DB: "mysub"
FEE_LEVEL_COLLECTOR: "50000"
FEE_LEVEL_ADDICT: "100000"

View File

@ -0,0 +1,10 @@
# lifesub/deployment/manifest/configmaps/recommend-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: recommend-config
data:
SERVER_PORT: "8083"
POSTGRES_HOST: "recommend-postgresql"
POSTGRES_PORT: "5432"
POSTGRES_DB: "recommend"

View File

@ -0,0 +1,56 @@
# lifesub/deployment/manifest/deployments/member-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: member
spec:
replicas: 2
selector:
matchLabels:
app: member
template:
metadata:
labels:
app: member
spec:
containers:
- name: member
image: dg0200cr.azurecr.io/lifesub/member:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8081
resources:
requests:
cpu: 256m
memory: 256Mi
limits:
cpu: 1024m
memory: 1024Mi
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: member-config
- secretRef:
name: common-secret
- secretRef:
name: member-secret
startupProbe:
httpGet:
path: /actuator/health
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 60
periodSeconds: 15

View File

@ -0,0 +1,56 @@
# lifesub/deployment/manifest/deployments/mysub-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysub
spec:
replicas: 1
selector:
matchLabels:
app: mysub
template:
metadata:
labels:
app: mysub
spec:
containers:
- name: mysub
image: dg0200cr.azurecr.io/lifesub/mysub:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8082
resources:
requests:
cpu: 256m
memory: 256Mi
limits:
cpu: 1024m
memory: 1024Mi
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: mysub-config
- secretRef:
name: common-secret
- secretRef:
name: mysub-secret
startupProbe:
httpGet:
path: /actuator/health
port: 8082
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
initialDelaySeconds: 60
periodSeconds: 15

View File

@ -0,0 +1,56 @@
# lifesub/deployment/manifest/deployments/recommend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: recommend
spec:
replicas: 1
selector:
matchLabels:
app: recommend
template:
metadata:
labels:
app: recommend
spec:
containers:
- name: recommend
image: dg0200cr.azurecr.io/lifesub/recommend:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8083
resources:
requests:
cpu: 256m
memory: 256Mi
limits:
cpu: 1024m
memory: 1024Mi
envFrom:
- configMapRef:
name: common-config
- configMapRef:
name: recommend-config
- secretRef:
name: common-secret
- secretRef:
name: recommend-secret
startupProbe:
httpGet:
path: /actuator/health
port: 8083
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8083
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8083
initialDelaySeconds: 60
periodSeconds: 15

View File

@ -0,0 +1,34 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backend-ingress
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /member(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: member
port:
number: 80
- path: /mysub(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: mysub
port:
number: 80
- path: /recommend(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: recommend
port:
number: 80

View File

@ -0,0 +1,8 @@
# lifesub/deployment/manifest/secrets/common-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: common-secret
type: Opaque
stringData:
JWT_SECRET_KEY: "8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ"

View File

@ -0,0 +1,11 @@
# lifesub/deployment/manifest/secrets/member-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: member-secret
type: Opaque
stringData:
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "Passw0rd"
JWT_ACCESS_TOKEN_VALIDITY: "3600000"
JWT_REFRESH_TOKEN_VALIDITY: "86400000"

View File

@ -0,0 +1,9 @@
# lifesub/deployment/manifest/secrets/mysub-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysub-secret
type: Opaque
stringData:
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "Passw0rd"

View File

@ -0,0 +1,9 @@
# lifesub/deployment/manifest/secrets/recommend-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: recommend-secret
type: Opaque
stringData:
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "Passw0rd"

View File

@ -0,0 +1,12 @@
# lifesub/deployment/manifest/services/member-service.yaml
apiVersion: v1
kind: Service
metadata:
name: member
spec:
selector:
app: member
ports:
- port: 80
targetPort: 8081
type: ClusterIP

View File

@ -0,0 +1,12 @@
# lifesub/deployment/manifest/services/mysub-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysub
spec:
selector:
app: mysub
ports:
- port: 80
targetPort: 8082
type: ClusterIP

View File

@ -0,0 +1,12 @@
# lifesub/deployment/manifest/services/recommend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: recommend
spec:
selector:
app: recommend
ports:
- port: 80
targetPort: 8083
type: ClusterIP

View File

@ -0,0 +1,82 @@
!theme mono
title Common Module - Class Diagram
package "com.unicorn.lifesub.common" {
package "dto" {
class ApiResponse<T> {
-status: int
-message: String
-data: T
-timestamp: LocalDateTime
+ApiResponse(status: int, message: String, data: T)
+{static} success(data: T): ApiResponse<T>
+{static} error(errorCode: ErrorCode): ApiResponse<T>
}
class JwtTokenDTO {
-accessToken: String
-refreshToken: String
}
}
package "exception" {
class BusinessException {
-errorCode: ErrorCode
+BusinessException(errorCode: ErrorCode)
+getErrorCode(): ErrorCode
}
class InfraException {
-errorCode: ErrorCode
+InfraException(errorCode: ErrorCode)
+getErrorCode(): ErrorCode
}
enum ErrorCode {
INVALID_INPUT_VALUE(100, "Invalid input value")
INTERNAL_SERVER_ERROR(110, "Internal server error")
MEMBER_NOT_FOUND(200, "Member not found")
INVALID_CREDENTIALS(210, "Invalid credentials")
TOKEN_EXPIRED(220, "Token expired")
SIGNATURE_VERIFICATION_EXCEPTION(230, "서명 검증 실패")
ALGORITHM_MISMATCH_EXCEPTION(240, "알고리즘 불일치")
INVALID_CLAIM_EXCEPTION(250, "유효하지 않은 클레임")
SUBSCRIPTION_NOT_FOUND(300, "Subscription not found")
ALREADY_SUBSCRIBED(310, "Already subscribed to this service")
NO_SPENDING_DATA(400, "No spending data found")
NO_RECOMMENDATION_DATA(410, "추천 구독 카테고리 없음")
UNDIFINED_ERROR(0, "정의되지 않은 에러")
--
-status: int
-message: String
}
}
package "entity" {
abstract class BaseTimeEntity {
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
}
}
package "aop" {
class LoggingAspect {
-gson: Gson
+logMethodStart(joinPoint: JoinPoint): void
+logMethodEnd(joinPoint: JoinPoint, result: Object): void
+logMethodException(joinPoint: JoinPoint, exception: Exception): void
-getArgumentString(args: Object[]): String
-getResultString(result: Object): String
}
}
package "config" {
class JpaConfig {
}
}
}
' Relationships
BusinessException --> ErrorCode
InfraException --> ErrorCode
LoggingAspect ..> ApiResponse : uses

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

7
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

Some files were not shown because too many files have changed in this diff Show More