This commit is contained in:
hiondal 2025-04-09 12:32:48 +09:00
commit 6e36841dbc
47 changed files with 430 additions and 233 deletions

View File

@ -1,143 +1,274 @@
name: Backend CI/CD Pipeline name: Backend Services CI/CD
# Temporarily disabled on:
# on: push:
# push: #branches: [ cicd ]
# branches: [ k8s ] paths:
- 'member/**'
env: - 'mysub/**'
JAVA_VERSION: '21' - 'recommend/**'
GRADLE_VERSION: '8.5' - 'common/**'
- 'deployment/**'
- '.github/workflows/**'
jobs: jobs:
build: build:
name: Build Applications name: Build and Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_outputs.outputs.image_tag }}
steps: steps:
- uses: actions/checkout@v4 - name: Check out code
uses: actions/checkout@v4
- name: Set up JDK - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: ${{ env.JAVA_VERSION }} java-version: '21'
distribution: 'temurin' distribution: 'temurin'
cache: 'gradle'
- name: Setup Gradle - name: Load environment variables
uses: gradle/gradle-build-action@v2 id: env_vars
with: run: |
gradle-version: ${{ env.GRADLE_VERSION }} # 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 - name: Build with Gradle
run: | run: |
chmod +x gradlew ./gradlew :member:build :mysub-infra:build :recommend:build -x test
./gradlew clean :member:build :mysub-infra:build :recommend:build
- name: Upload Build Artifacts - 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 uses: actions/upload-artifact@v4
with: with:
name: build-artifacts name: app-builds
path: | path: |
member/build/libs/*.jar member/build/libs/*.jar
mysub-infra/build/libs/*.jar mysub-infra/build/libs/*.jar
recommend/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: release:
name: Build and Push Images name: Build and Push Docker Images
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_tag.outputs.tag }}
steps: steps:
- uses: actions/checkout@v4 - name: Check out code
uses: actions/checkout@v4
- name: Download Build Artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: build-artifacts name: app-builds
- name: Load Environment Variables - name: Load environment variables
run: source deployment/deploy_env_vars
- name: Generate Image Tag
id: set_tag
run: | run: |
tag=$(date +'%Y%m%d%H%M%S') # Read environment variables from file
echo "tag=${tag}" >> $GITHUB_OUTPUT 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 - name: Login to Azure Container Registry
uses: azure/docker-login@v1 uses: docker/login-action@v3
with: with:
login-server: ${{ env.registry }} registry: ${{ env.registry }}
username: ${{ secrets.ACR_USERNAME }} username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }} password: ${{ secrets.ACR_PASSWORD }}
- name: Build and Push Images - name: Build and push Member service image
run: | uses: docker/build-push-action@v5
for service in member mysub recommend; do with:
build_dir=$([[ "$service" == "mysub" ]] && echo "mysub-infra" || echo "$service") context: .
jar_file=$([[ "$service" == "mysub" ]] && echo "mysub.jar" || echo "${service}.jar") 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
docker build \ - name: Build and push MySub service image
--build-arg BUILD_LIB_DIR="${build_dir}/build/libs" \ uses: docker/build-push-action@v5
--build-arg ARTIFACTORY_FILE="${jar_file}" \ with:
-f deployment/Dockerfile \ context: .
-t ${registry}/${image_org}/${service}:${{ steps.set_tag.outputs.tag }} . 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
docker push ${registry}/${image_org}/${service}:${{ steps.set_tag.outputs.tag }} - name: Build and push Recommend service image
done 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: deploy:
name: Deploy to AKS name: Deploy to Kubernetes
needs: release needs: [build, release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Check out code
uses: actions/checkout@v4
- name: Load Environment Variables - name: Load environment variables
run: source deployment/deploy_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: 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 - name: Azure Login
uses: azure/login@v1 uses: azure/login@v1
with: with:
creds: ${{ secrets.AZURE_CREDENTIALS }} creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Set AKS Context - name: Setup kubectl
uses: azure/aks-set-context@v1 uses: azure/setup-kubectl@v3
with:
resource-group: ictcoe-edu
cluster-name: ${{ env.teamid }}-aks
- name: Create Namespace - name: Get AKS Credentials
run: | run: |
kubectl create namespace ${teamid}-${root_project}-ns --dry-run=client -o yaml | kubectl apply -f - az aks get-credentials --resource-group ictcoe-edu --name ${{ env.teamid }}-aks --overwrite-existing
- name: Generate Manifest - name: Create namespace
run: | run: |
export namespace=${teamid}-${root_project}-ns kubectl create namespace ${{ env.namespace }} --dry-run=client -o yaml | kubectl apply -f -
export allowed_origins=${allowed_origins}
export jwt_secret_key=${jwt_secret_key}
export postgres_user=${postgres_user}
export postgres_password=${postgres_password}
export replicas=${replicas}
export resources_requests_cpu=${resources_requests_cpu}
export resources_requests_memory=${resources_requests_memory}
export resources_limits_cpu=${resources_limits_cpu}
export resources_limits_memory=${resources_limits_memory}
export member_image_path=${registry}/${image_org}/member:${{ needs.release.outputs.image_tag }}
export mysub_image_path=${registry}/${image_org}/mysub:${{ needs.release.outputs.image_tag }}
export recommend_image_path=${registry}/${image_org}/recommend:${{ needs.release.outputs.image_tag }}
- 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 envsubst < deployment/deploy.yaml.template > deployment/deploy.yaml
# Print manifest for debugging
echo "Generated Kubernetes manifest:"
cat deployment/deploy.yaml cat deployment/deploy.yaml
- name: Deploy to AKS - name: Apply Kubernetes manifest
run: | run: |
kubectl apply -f deployment/deploy.yaml kubectl apply -f deployment/deploy.yaml
echo "Waiting for deployments to be ready..." - name: Wait for deployments to be ready
kubectl -n ${namespace} wait --for=condition=available deployment/member --timeout=300s run: |
kubectl -n ${namespace} wait --for=condition=available deployment/mysub --timeout=300s kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/member --timeout=300s
kubectl -n ${namespace} wait --for=condition=available deployment/recommend --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}')"

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.gradle
build/
### IntelliJ IDEA ###
.idea/
### Mac OS ###
.DS_Store

Binary file not shown.

12
.idea/modules.xml generated
View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/common/lifesub.common.main.iml" filepath="$PROJECT_DIR$/.idea/modules/common/lifesub.common.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/member/lifesub.member.main.iml" filepath="$PROJECT_DIR$/.idea/modules/member/lifesub.member.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/mysub-biz/lifesub.mysub-biz.main.iml" filepath="$PROJECT_DIR$/.idea/modules/mysub-biz/lifesub.mysub-biz.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/mysub-infra/lifesub.mysub-infra.main.iml" filepath="$PROJECT_DIR$/.idea/modules/mysub-infra/lifesub.mysub-infra.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/recommend/lifesub.recommend.main.iml" filepath="$PROJECT_DIR$/.idea/modules/recommend/lifesub.recommend.main.iml" />
</modules>
</component>
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../common/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../common/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../member/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../member/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../mysub-biz/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../mysub-biz/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../mysub-infra/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../mysub-infra/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../recommend/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../recommend/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

2
.idea/vcs.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>
</project> </project>

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# 생활구독추천 서비스

View File

@ -92,6 +92,7 @@ subprojects {
})) }))
} }
} }
} }
//-- Biz와 common //-- Biz와 common
@ -133,3 +134,4 @@ configure(subprojects.findAll { it.name.endsWith('-biz') || it.name == 'common'
bootJar.enabled = false bootJar.enabled = false
jar.enabled = true jar.enabled = true
} }

View File

@ -54,7 +54,7 @@ podTemplate(
} }
} }
stage('Build Applications') { stage('Build Applications & SonarQube Analysis') {
container('podman') { container('podman') {
sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2' sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
} }
@ -71,12 +71,52 @@ podTemplate(
echo '${testContainersConfig}' > member/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > member/src/test/resources/testcontainers.properties
echo '${testContainersConfig}' > mysub-infra/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > mysub-infra/src/test/resources/testcontainers.properties
echo '${testContainersConfig}' > recommend/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > recommend/src/test/resources/testcontainers.properties
"""
// Member 서비스 빌드 및 SonarQube 분석
withSonarQubeEnv('SonarQube') {
sh """
chmod +x gradlew
# 빌드 실행 # 빌드 실행
chmod +x gradlew ./gradlew :member:build :mysub-infra:build :recommend:build -x test
./gradlew clean :member:build :mysub-infra:build :recommend:build
# 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') { stage('Build & Push Images') {
@ -144,3 +184,4 @@ podTemplate(
} }
} }
} }

View File

@ -10,11 +10,7 @@ podTemplate(
label: "${PIPELINE_ID}", label: "${PIPELINE_ID}",
serviceAccount: 'jenkins', serviceAccount: 'jenkins',
containers: [ containers: [
containerTemplate(name: 'podman', containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true),
image: "mgoltzsche/podman",
ttyEnabled: true,
command: 'cat',
privileged: true),
containerTemplate(name: 'gradle', containerTemplate(name: 'gradle',
image: 'gradle:jdk17', image: 'gradle:jdk17',
ttyEnabled: true, ttyEnabled: true,
@ -24,29 +20,44 @@ podTemplate(
envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'), envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'),
envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true') envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true')
]), ]),
containerTemplate(name: 'git', containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true),
image: 'alpine:3.19', containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h'),
command: 'cat', containerTemplate(name: 'git', image: 'alpine/git:latest', command: 'cat', ttyEnabled: true)
ttyEnabled: true)
], ],
volumes: [ volumes: [
emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false), emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false),
emptyDirVolume(mountPath: '/root/.azure', memory: false),
emptyDirVolume(mountPath: '/run/podman', memory: false) emptyDirVolume(mountPath: '/run/podman', memory: false)
] ]
) { ) {
node(PIPELINE_ID) { node(PIPELINE_ID) {
def props def props
def imageTag = getImageTag() def imageTag = getImageTag()
def manifest = "deploy.yaml"
def namespace
def services = ['member', 'mysub', 'recommend'] def services = ['member', 'mysub', 'recommend']
def manifestRepo = 'cna-bootcamp/lifesub-manifest' def MANIFEST_REPO = "https://github.com/cna-bootcamp/lifesub-manifest.git"
def manifestBranch = 'main' def MANIFEST_BRANCH = "main"
stage("Get Source") { stage("Get Source") {
checkout scm checkout scm
props = readProperties file: "deployment/deploy_env_vars" props = readProperties file: "deployment/deploy_env_vars"
namespace = "${props.namespace}"
} }
stage('Build Applications') { 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') { container('podman') {
sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2' sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
} }
@ -63,13 +74,52 @@ testcontainers.reuse.enable=true'''
echo '${testContainersConfig}' > member/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > member/src/test/resources/testcontainers.properties
echo '${testContainersConfig}' > mysub-infra/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > mysub-infra/src/test/resources/testcontainers.properties
echo '${testContainersConfig}' > recommend/src/test/resources/testcontainers.properties echo '${testContainersConfig}' > recommend/src/test/resources/testcontainers.properties
"""
// Member 서비스 빌드 및 SonarQube 분석
withSonarQubeEnv('SonarQube') {
sh """
chmod +x gradlew
# 빌드 실행 # 빌드 실행
chmod +x gradlew ./gradlew :member:build :mysub-infra:build :recommend:build -x test
./gradlew clean :member:build :mysub-infra:build :recommend:build
# 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') { stage('Build & Push Images') {
container('podman') { container('podman') {
@ -85,10 +135,10 @@ testcontainers.reuse.enable=true'''
def jarFile = service == 'mysub' ? 'mysub.jar' : "${service}.jar" def jarFile = service == 'mysub' ? 'mysub.jar' : "${service}.jar"
sh """ sh """
podman build \\ podman build \
--build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \\ --build-arg BUILD_LIB_DIR="${buildDir}/build/libs" \
--build-arg ARTIFACTORY_FILE="${jarFile}" \\ --build-arg ARTIFACTORY_FILE="${jarFile}" \
-f deployment/Dockerfile \\ -f deployment/Dockerfile \
-t ${props.registry}/${props.image_org}/${service}:${imageTag} . -t ${props.registry}/${props.image_org}/${service}:${imageTag} .
podman push ${props.registry}/${props.image_org}/${service}:${imageTag} podman push ${props.registry}/${props.image_org}/${service}:${imageTag}
@ -98,47 +148,35 @@ testcontainers.reuse.enable=true'''
} }
} }
stage('Update Manifests') { stage('Update Manifest Repository') {
container('git') { container('git') {
// git과 yq 설치 // 임시 디렉토리 생성
sh ''' sh "mkdir -p /tmp/manifests"
apk add --no-cache git curl
curl -L https://github.com/mikefarah/yq/releases/download/v4.40.5/yq_linux_amd64 -o /usr/local/bin/yq
chmod +x /usr/local/bin/yq
'''
withCredentials([usernamePassword( // Clone manifest repository
credentialsId: 'github-credentials', withCredentials([usernamePassword(credentialsId: 'github-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
usernameVariable: 'GIT_USERNAME', sh """
passwordVariable: 'GIT_PASSWORD'
)]) {
sh '''
git config --global user.email "jenkins@example.com" git config --global user.email "jenkins@example.com"
git config --global user.name "Jenkins" git config --global user.name "Jenkins Pipeline"
'''
sh """ git clone https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/cna-bootcamp/lifesub-manifest.git /tmp/manifests
rm -rf lifesub-manifest cd /tmp/manifests
#git clone https://oauth2:${GIT_PASSWORD}@github.com/${manifestRepo}.git
git clone https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/${manifestRepo}.git
"""
dir('lifesub-manifest') { # Update image tags in the appropriate YAML files
services.each { service -> for service in ${services.join(' ')}; do
def imagePath = "${props.registry}/${props.image_org}/${service}:${imageTag}" # 백엔드 서비스 이미지 태그 업데이트
sh """ if [ -f lifesub/deployments/\${service}-deployment.yaml ]; then
yq -i '.spec.template.spec.containers[0].image = "${imagePath}"' lifesub/deployments/${service}-deployment.yaml 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
sh """ # Commit and push changes
git add . git add .
git diff-index --quiet HEAD || git commit -m "Update backend services images to ${imageTag}" git commit -m "Update backend service image tags to ${imageTag}" || true
git push origin ${manifestBranch} git push
""" """
} }
} }
} }
} }
} }
}

View File

@ -307,3 +307,40 @@ spec:
- port: 80 - port: 80
targetPort: 8083 targetPort: 8083
type: ClusterIP 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

@ -8,8 +8,8 @@ registry=unicorncr.azurecr.io
image_org=lifesub image_org=lifesub
# Application Settings # Application Settings
replicas=2 replicas=1
allowed_origins=http://4.230.156.229 allowed_origins=http://20.214.113.12
# Security Settings # Security Settings
jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ jwt_secret_key=8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ

View File

@ -5,4 +5,8 @@ metadata:
data: data:
JPA_DDL_AUTO: update JPA_DDL_AUTO: update
JPA_SHOW_SQL: "true" JPA_SHOW_SQL: "true"
<<<<<<< HEAD
ALLOWED_ORIGINS: "http://localhost:18080,http://localhost:18081,http://20.249.184.6" ALLOWED_ORIGINS: "http://localhost:18080,http://localhost:18081,http://20.249.184.6"
=======
ALLOWED_ORIGINS: "http://localhost:18080,http://localhost:18081,http://20.214.113.12"
>>>>>>> 1ea06faa5d92eae36ec9d161158eb1314eaff732

View File

@ -18,7 +18,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -40,7 +39,7 @@ public class MemberServiceImpl implements MemberService {
// 사용자의 권한 정보 생성 // 사용자의 권한 정보 생성
Collection<? extends GrantedAuthority> authorities = member.getRoles().stream() Collection<? extends GrantedAuthority> authorities = member.getRoles().stream()
.map(SimpleGrantedAuthority::new) .map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()); .toList();
return jwtTokenProvider.createToken(member, authorities); return jwtTokenProvider.createToken(member, authorities);
} }

View File

@ -7,7 +7,7 @@ spring:
datasource: datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:member} url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:member}
username: ${POSTGRES_USER:admin} username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:Passw0rd} password: ${POSTGRES_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
# JPA 설정 # JPA 설정

View File

@ -7,7 +7,7 @@ spring:
datasource: datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:mysub} url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:mysub}
username: ${POSTGRES_USER:admin} username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:Passw0rd} password: ${POSTGRES_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
# JPA 설정 # JPA 설정
jpa: jpa:

View File

@ -1 +0,0 @@
com.unicorn.lifesub.recommend.RecommendApplication

View File

@ -1,12 +0,0 @@
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.unicorn.lifesub.recommend.RecommendApplication
Spring-Boot-Version: 3.4.0
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 21
Implementation-Title: recommend
Implementation-Version: 1.0.0

View File

@ -1,2 +0,0 @@
Manifest-Version: 1.0

View File

@ -7,7 +7,7 @@ spring:
datasource: datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommend} url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommend}
username: ${POSTGRES_USER:admin} username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:Passw0rd} password: ${POSTGRES_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
# JPA 설정 # JPA 설정
jpa: jpa: