From 4d222c8bac2873f62fa8a5dc81c7680aaa4b5ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:54:24 +0900 Subject: [PATCH 01/16] =?UTF-8?q?environment:=20staging=EC=9A=A9=20Jenkins?= =?UTF-8?q?file=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/Jenkinsfile-stage | 472 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 jenkins/Jenkinsfile-stage diff --git a/jenkins/Jenkinsfile-stage b/jenkins/Jenkinsfile-stage new file mode 100644 index 0000000..bdcd181 --- /dev/null +++ b/jenkins/Jenkinsfile-stage @@ -0,0 +1,472 @@ +/* + * To use the following functionality: + * withCredentials([file(credentialsId: 'time_stage_config_yml', variable: 'CONFIG_FILE')]) { + * + * The 'Pipeline Utility Steps' plugin must be installed on your Jenkins instance. + * + * Installation Guide: + * 1. Log in to your Jenkins instance. + * 2. Navigate to 'Manage Jenkins' from the main dashboard. + * 3. Click on 'Manage Plugins'. + * 4. Go to the 'Available plugins' tab. + * 5. In the search box, type 'Pipeline Utility Steps'. + * 6. Select the 'Pipeline Utility Steps' plugin from the list. + * 7. Click on 'Install without restart' or 'Install and restart' to install the plugin. + * 8. Once the plugin is installed, you can use functions like 'readYaml' and other utility steps in your Jenkinsfile. + * + * Ensure the plugin is up-to-date to avoid compatibility issues. + */ + + +pipeline { + agent any + + triggers { + githubPush() + } + + stages { + stage('Load Environment Variables') { + steps { + withCredentials([file(credentialsId: 'time_stage_config_yml', variable: 'CONFIG_FILE')]) { + script { + def config = readYaml(file: env.CONFIG_FILE) + + env.JENKINS_DOMAIN = config.'jenkins-domain' + env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' + env.SLACK_COLOR_SUCCESS = config.slack.'color-success' + env.SLACK_COLOR_FAILURE = config.slack.'color-failure' + + env.MYSQL_USER = config.mysql.user + env.MYSQL_PASSWORD = config.mysql.password + env.BACKUP_DIR = config.mysql.'backup-dir' + + env.DOCKER_HUB_REPO = config.dockerhub.repo + env.DOCKER_HUB_USER = config.dockerhub.user + env.DOCKER_HUB_PASSWORD = config.dockerhub.password + + env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' + env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' + env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' + + env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' + env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' + env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' + + env.BLUE_CONTAINER = config.containers.blue + env.GREEN_CONTAINER = config.containers.green + env.BLUE_URL = config.containers.'blue-url' + env.GREEN_URL = config.containers.'green-url' + env.IMAGE_NAME = config.containers.'image-name' + + env.APPLICATION_NETWORK = config.networks.application + env.MONITORING_NETWORK = config.networks.monitoring + + env.PROFILE = config.spring.profile + env.PORT_A = config.spring.'port-a'.toString() + env.PORT_B = config.spring.'port-b'.toString() + + env.WHITELIST_ADMIN_USERNAME = config.admin.username + env.WHITELIST_ADMIN_PASSWORD = config.admin.password + + env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" + env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' + env.MYSQL_CONTAINER_NAME = config.docker.'mysql-container-name' + } + } + } + } + + stage('Check Java Version') { + steps { + sh 'java -version' + } + } + + stage('Slack Notification: Staging Server Testing') { + steps { + script { + sendSlackNotification(":test_tube: Testing the Staging server...", env.SLACK_COLOR_SUCCESS) + } + } + } + + stage('Get Git Change Log') { + steps { + script { + env.GIT_CHANGELOG = getChangeLog() + } + } + } + + stage('MYSQL Backup') { + steps { + script { + backupMysql() + } + } + } + + stage('Docker Hub Login') { + steps { + script { + dockerLogin() + } + } + } + + stage('Determine Containers') { + steps { + script { + determineContainers() + } + } + } + + stage('Build Application') { + steps { + script { + buildApplication() + } + } + } + + stage('Build and Push Docker Image') { + steps { + script { + buildAndPushDockerImage() + } + } + } + + stage('Deploy New Instance') { + steps { + script { + deployNewInstance() + } + } + } + + stage('Health Check') { + steps { + script { + performHealthCheck() + } + } + } + + stage('Switch Traffic and Cleanup') { + steps { + script { + switchTrafficAndCleanup() + } + } + } + } + + post { + failure { + script { + sendSlackBuildNotification(":scream_cat: Deployment failed.", env.SLACK_COLOR_FAILURE) + } + } + + success { + script { + sendSlackBuildNotification(":rocket: Deployment completed successfully", env.SLACK_COLOR_SUCCESS) + } + } + } +} + +def sendSlackNotification(String message, String color) { + def payload = [ + attachments: [ + [ + color: color, + text: message.replaceAll('"', '\\"').replaceAll('\n', '\\\\n') + ] + ] + ] + + withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { + def payloadJson = groovy.json.JsonOutput.toJson(payload) + sh """ + curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} + """ + } +} + + +def sendSlackBuildNotification(String message, String color) { + def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}" + def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console" + + def payload = [ + blocks: [ + [ + type: "section", + text: [ + type: "mrkdwn", + text: message + ] + ] + ], + attachments: [ + [ + color: color, + blocks: [ + [ + type: "section", + text: [ + type: "mrkdwn", + text: "*Change Log:*\n${env.GIT_CHANGELOG}" + ] + ], + [ + type: "actions", + elements: [ + [ + type: "button", + text: [ + type: "plain_text", + text: "Job", + emoji: true + ], + url: jobUrl, + value: "click_job" + ], + [ + type: "button", + text: [ + type: "plain_text", + text: "Console Output", + emoji: true + ], + url: consoleOutputUrl, + value: "click_console_output" + ] + ] + ] + ].findAll { it != null } + ] + ] + ] + + withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { + def payloadJson = groovy.json.JsonOutput.toJson(payload) + sh """ + curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} + """ + } +} + +def getChangeLog() { + def previousCommit = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: 'HEAD~1' + def currentCommit = env.GIT_COMMIT ?: 'HEAD' + + def changeLog = sh( + script: "git log ${previousCommit}..${currentCommit} --pretty=format:\"* %h - %s (%an)\" --abbrev-commit", + returnStdout: true + ).trim() + + def lines = changeLog.split('\n') + if (lines.size() > 10) { + changeLog = lines.take(10).join('\n') + '\n... (truncated)' + } + + return changeLog +} + +def backupMysql() { + def BACKUP_FILE = "mysql_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql" + withEnv([ + "BACKUP_DIR=${env.BACKUP_DIR}", + "MYSQL_CONTAINER_NAME=${env.MYSQL_CONTAINER_NAME}", + "MYSQL_PASSWORD=${env.MYSQL_PASSWORD}", + "MYSQL_USER=${env.MYSQL_USER}", + "MYSQL_DATABASE=${env.MYSQL_DATABASE}" + ]) { + sh """ + echo "Backing up MySQL database to ${BACKUP_DIR}/${BACKUP_FILE}..." + docker exec ${MYSQL_CONTAINER_NAME} sh -c 'mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} > ${BACKUP_DIR}/${BACKUP_FILE}' + """ + } + sendSlackNotification(":floppy_disk: MySQL backup completed successfully: ${BACKUP_FILE}", env.SLACK_COLOR_SUCCESS) +} + +def dockerLogin() { + withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) { + sh """ + echo "Logging in to Docker Hub..." + echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin + """ + } +} + +def determineContainers() { + script { + withEnv([ + "BLUE_CONTAINER=${env.BLUE_CONTAINER}", + "GREEN_CONTAINER=${env.GREEN_CONTAINER}", + "BLUE_URL=${env.BLUE_URL}", + "GREEN_URL=${env.GREEN_URL}", + "PORT_A=${env.PORT_A}", + "PORT_B=${env.PORT_B}" + ]) { + def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0 + if (blueRunning) { + env.CURRENT_CONTAINER = BLUE_CONTAINER + env.DEPLOY_CONTAINER = GREEN_CONTAINER + env.NEW_TARGET = GREEN_URL + env.NEW_PORT = PORT_B + env.OLD_PORT = PORT_A + } else { + env.CURRENT_CONTAINER = GREEN_CONTAINER + env.DEPLOY_CONTAINER = BLUE_CONTAINER + env.NEW_TARGET = BLUE_URL + env.NEW_PORT = PORT_A + env.OLD_PORT = PORT_B + } + echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + } + } +} + +def buildApplication() { + withEnv([ + "PROFILE=${env.PROFILE}" + ]) { + sh """ + echo "Building application with profile ${PROFILE}..." + ./gradlew clean build -Penv=${PROFILE} --stacktrace --info + """ + } +} + +def buildAndPushDockerImage() { + withEnv([ + "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}", + "IMAGE_NAME=${env.IMAGE_NAME}" + ]) { + sh """ + docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} . + docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} + docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} + """ + } +} + +def deployNewInstance() { + withEnv([ + "PROFILE=${env.PROFILE}", + "NEW_PORT=${env.NEW_PORT}", + "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}", + "MONITORING_NETWORK=${env.MONITORING_NETWORK}", + "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}", + "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}", + "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}", + "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}", + "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}", + "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "IMAGE_NAME=${env.IMAGE_NAME}" + ]) { + sh """ + echo "Stopping and removing existing container if it exists" + if docker ps | grep -q ${DEPLOY_CONTAINER}; then + docker stop ${DEPLOY_CONTAINER} + docker rm ${DEPLOY_CONTAINER} + fi + + echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" + docker run -d --name ${DEPLOY_CONTAINER} \\ + -p ${NEW_PORT}:8000 \\ + --network ${APPLICATION_NETWORK} \\ + -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ + -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ + -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\ + -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\ + -e SPRING_PROFILES_ACTIVE=${PROFILE} \\ + ${IMAGE_NAME}:${DEPLOY_CONTAINER} + + echo "Checking if monitoring network ${MONITORING_NETWORK} exists" + if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then + echo "Connecting to monitoring network ${MONITORING_NETWORK}" + docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER} + else + echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection." + fi + + echo "Listing all containers" + docker ps -a + """ + } + sendSlackNotification(":low_battery: Restarting the Staging server...", env.SLACK_COLOR_SUCCESS) +} + +def performHealthCheck() { + withEnv([ + "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}", + "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}" + ]) { + def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() + echo "Public IP address: ${PUBLIC_IP}" + + def start_time = System.currentTimeMillis() + def timeout = start_time + 150000 // 2.5 minutes + + while (System.currentTimeMillis() < timeout) { + def elapsed = (System.currentTimeMillis() - start_time) / 1000 + echo "Checking health... ${elapsed} seconds elapsed." + def status = sh( + script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \ + http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + returnStatus: true + ) + if (status == 0) { + echo "New application started successfully after ${elapsed} seconds." + return + } + sleep 5 + } + + if (System.currentTimeMillis() >= timeout) { + sendSlackNotification(":scream_cat: New Staging application did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) + sh "docker stop ${env.DEPLOY_CONTAINER}" + sh "docker rm ${env.DEPLOY_CONTAINER}" + error "Health check failed" + } + } +} + +def switchTrafficAndCleanup() { + withEnv([ + "NEW_PORT=${env.NEW_PORT}", + "OLD_PORT=${env.OLD_PORT}", + "NEW_TARGET=${env.NEW_TARGET}", + "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}" + ]) { + sh """ + echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}." + docker exec ${NGINX_CONTAINER_NAME} bash -c ' + export BACKEND_URL=${NEW_TARGET} + envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + ' + docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf + docker exec ${NGINX_CONTAINER_NAME} nginx -t + docker exec ${NGINX_CONTAINER_NAME} nginx -s reload + + echo "Checking if current container ${CURRENT_CONTAINER} is running..." + if docker ps | grep -q ${CURRENT_CONTAINER}; then + docker stop ${CURRENT_CONTAINER} + docker rm ${CURRENT_CONTAINER} + echo "Removed old container ${CURRENT_CONTAINER}." + fi + """ + } +} From 185eeaa60f7eff448cd7d6e5aa96f46c4df9def5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:58:18 +0900 Subject: [PATCH 02/16] =?UTF-8?q?environment:=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/{Jenkinsfile-stage => stage/Jenkinsfile} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jenkins/{Jenkinsfile-stage => stage/Jenkinsfile} (100%) diff --git a/jenkins/Jenkinsfile-stage b/jenkins/stage/Jenkinsfile similarity index 100% rename from jenkins/Jenkinsfile-stage rename to jenkins/stage/Jenkinsfile From 14349e44297ac6092119d8d20bd02ec15bebb81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:02:51 +0900 Subject: [PATCH 03/16] =?UTF-8?q?environment:=20staging=20Dockerfile=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 jenkins/stage/Dockerfile diff --git a/jenkins/stage/Dockerfile b/jenkins/stage/Dockerfile new file mode 100644 index 0000000..7ef8ea5 --- /dev/null +++ b/jenkins/stage/Dockerfile @@ -0,0 +1,12 @@ +# Use the official OpenJDK 21 image from the Docker Hub +FROM openjdk:21-jdk + +# Expose port 8080 to the outside world +EXPOSE 8000 + +# Copy the JAR file into the container +COPY build/libs/time.jar /time.jar + +# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment. +# For example, use '-Dspring.profiles.active=production' for production environment. +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=stage", "/time.jar"] From 1a82899c377b3c0f35bd1fe7e1ed97af726e8c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:23:44 +0900 Subject: [PATCH 04/16] =?UTF-8?q?environment:=20staging=EC=9A=A9=20Jenkins?= =?UTF-8?q?file=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index bdcd181..b951768 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -39,6 +39,7 @@ pipeline { env.MYSQL_USER = config.mysql.user env.MYSQL_PASSWORD = config.mysql.password + env.MYSQL_DAtABASE = config.mysql.database env.BACKUP_DIR = config.mysql.'backup-dir' env.DOCKER_HUB_REPO = config.dockerhub.repo From 82e33c1c583b1fe6a589b634518623b611c794b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:24:49 +0900 Subject: [PATCH 05/16] =?UTF-8?q?environment:=20staging=EC=9A=A9=20Jenkins?= =?UTF-8?q?file=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index b951768..b978226 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -39,7 +39,7 @@ pipeline { env.MYSQL_USER = config.mysql.user env.MYSQL_PASSWORD = config.mysql.password - env.MYSQL_DAtABASE = config.mysql.database + env.MYSQL_DATABASE = config.mysql.database env.BACKUP_DIR = config.mysql.'backup-dir' env.DOCKER_HUB_REPO = config.dockerhub.repo From 1317456c872fae9d4a6220717f037cac406c318d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:47:34 +0900 Subject: [PATCH 06/16] =?UTF-8?q?environment:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=AC=EB=9E=99=20=EC=95=8C=EB=A6=BC=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index b978226..25e7094 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -84,10 +84,10 @@ pipeline { } } - stage('Slack Notification: Staging Server Testing') { + stage('Slack Notification: Time Staging Server Testing') { steps { script { - sendSlackNotification(":test_tube: Testing the Staging server...", env.SLACK_COLOR_SUCCESS) + sendSlackNotification(":test_tube: Testing Time Staging server...", env.SLACK_COLOR_SUCCESS) } } } From 6fdaf7de2910cabd9ffd86679f5b82ee7dcc58bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:24:19 +0900 Subject: [PATCH 07/16] =?UTF-8?q?environment:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=8B=A8=EA=B3=84=EB=8A=94=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 25e7094..68d6718 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -340,7 +340,7 @@ def buildApplication() { ]) { sh """ echo "Building application with profile ${PROFILE}..." - ./gradlew clean build -Penv=${PROFILE} --stacktrace --info + ./gradlew clean build -Penv=${PROFILE} -x test --stacktrace --info """ } } From ed6e782aebaa9817386a06638c7d51ed0653abe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:32:25 +0900 Subject: [PATCH 08/16] =?UTF-8?q?environment:=20Time=20=EC=A0=84=EC=9A=A9?= =?UTF-8?q?=20=EC=8A=AC=EB=9E=99=20=EC=95=8C=EB=A6=BC=20=EB=AC=B8=EA=B5=AC?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 68d6718..7648470 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -87,7 +87,7 @@ pipeline { stage('Slack Notification: Time Staging Server Testing') { steps { script { - sendSlackNotification(":test_tube: Testing Time Staging server...", env.SLACK_COLOR_SUCCESS) + sendSlackNotification(":test_tube: Testing Staging server(Time)...", env.SLACK_COLOR_SUCCESS) } } } @@ -293,7 +293,7 @@ def backupMysql() { docker exec ${MYSQL_CONTAINER_NAME} sh -c 'mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} > ${BACKUP_DIR}/${BACKUP_FILE}' """ } - sendSlackNotification(":floppy_disk: MySQL backup completed successfully: ${BACKUP_FILE}", env.SLACK_COLOR_SUCCESS) + sendSlackNotification(":floppy_disk: Time MySQL backup completed successfully: ${BACKUP_FILE}", env.SLACK_COLOR_SUCCESS) } def dockerLogin() { @@ -405,7 +405,7 @@ def deployNewInstance() { docker ps -a """ } - sendSlackNotification(":low_battery: Restarting the Staging server...", env.SLACK_COLOR_SUCCESS) + sendSlackNotification(":low_battery: Restarting Staging server(Time)...", env.SLACK_COLOR_SUCCESS) } def performHealthCheck() { @@ -435,7 +435,7 @@ def performHealthCheck() { } if (System.currentTimeMillis() >= timeout) { - sendSlackNotification(":scream_cat: New Staging application did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) + sendSlackNotification(":scream_cat: New Staging application(Time) did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) sh "docker stop ${env.DEPLOY_CONTAINER}" sh "docker rm ${env.DEPLOY_CONTAINER}" error "Health check failed" From 17d1ccd98ee5cc0681f853994be24c7904a61e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:33:19 +0900 Subject: [PATCH 09/16] =?UTF-8?q?environment:=20Dockerfile=20Expose=208080?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/stage/Dockerfile b/jenkins/stage/Dockerfile index 7ef8ea5..ec2f1fb 100644 --- a/jenkins/stage/Dockerfile +++ b/jenkins/stage/Dockerfile @@ -2,7 +2,7 @@ FROM openjdk:21-jdk # Expose port 8080 to the outside world -EXPOSE 8000 +EXPOSE 8080 # Copy the JAR file into the container COPY build/libs/time.jar /time.jar From 9a0622cb426cb22aacb4eff31e6ac45bfb6052ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:37:27 +0900 Subject: [PATCH 10/16] =?UTF-8?q?environment:=20=ED=97=AC=EC=8A=A4?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=EC=97=90=EC=84=9C=20Basic=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 7648470..7192ae9 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -423,8 +423,7 @@ def performHealthCheck() { def elapsed = (System.currentTimeMillis() - start_time) / 1000 echo "Checking health... ${elapsed} seconds elapsed." def status = sh( - script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \ - http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + script: """curl -s http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", returnStatus: true ) if (status == 0) { From 1d95f57e6c78587ba652bffafa58e37852dc2303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:54:36 +0900 Subject: [PATCH 11/16] =?UTF-8?q?environment:=20=ED=97=AC=EC=8A=A4?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20Bas?= =?UTF-8?q?ic=20=EC=9D=B8=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 57 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 7192ae9..a633b3c 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -409,37 +409,32 @@ def deployNewInstance() { } def performHealthCheck() { - withEnv([ - "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}", - "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}" - ]) { - def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() - echo "Public IP address: ${PUBLIC_IP}" - - def start_time = System.currentTimeMillis() - def timeout = start_time + 150000 // 2.5 minutes - - while (System.currentTimeMillis() < timeout) { - def elapsed = (System.currentTimeMillis() - start_time) / 1000 - echo "Checking health... ${elapsed} seconds elapsed." - def status = sh( - script: """curl -s http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", - returnStatus: true + def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() + echo "Public IP address: ${PUBLIC_IP}" + + def start_time = System.currentTimeMillis() + def timeout = start_time + 150000 // 2.5 minutes + + while (System.currentTimeMillis() < timeout) { + def elapsed = (System.currentTimeMillis() - start_time) / 1000 + echo "Checking health... ${elapsed} seconds elapsed." + def status = sh( + script: """curl -s http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + returnStatus: true ) - if (status == 0) { - echo "New application started successfully after ${elapsed} seconds." - return - } - sleep 5 - } - - if (System.currentTimeMillis() >= timeout) { - sendSlackNotification(":scream_cat: New Staging application(Time) did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) - sh "docker stop ${env.DEPLOY_CONTAINER}" - sh "docker rm ${env.DEPLOY_CONTAINER}" - error "Health check failed" + if (status == 0) { + echo "New application started successfully after ${elapsed} seconds." + return } + sleep 5 } + + if (System.currentTimeMillis() >= timeout) { + sendSlackNotification(":scream_cat: New Staging application(Time) did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) + sh "docker stop ${env.DEPLOY_CONTAINER}" + sh "docker rm ${env.DEPLOY_CONTAINER}" + error "Health check failed" + } } def switchTrafficAndCleanup() { @@ -455,9 +450,9 @@ def switchTrafficAndCleanup() { echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}." docker exec ${NGINX_CONTAINER_NAME} bash -c ' export BACKEND_URL=${NEW_TARGET} - envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/time.conf.template > /etc/nginx/conf.d/time.conf ' - docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf + docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/time.conf docker exec ${NGINX_CONTAINER_NAME} nginx -t docker exec ${NGINX_CONTAINER_NAME} nginx -s reload @@ -469,4 +464,4 @@ def switchTrafficAndCleanup() { fi """ } -} +} \ No newline at end of file From 70db63f861e325411c7761654c1c1fc05649a4b1 Mon Sep 17 00:00:00 2001 From: JaeHoonSong <128021502+SongJaeHoonn@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:28:54 +0900 Subject: [PATCH 12/16] =?UTF-8?q?environment:=20actuator=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++ src/main/resources/logback-spring.xml | 57 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/resources/logback-spring.xml diff --git a/build.gradle b/build.gradle index 70dc49d..1e9edbb 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,12 @@ dependencies { implementation 'com.google.code.gson:gson:2.11.0' // JSON 라이브러리 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + // Monitoring + implementation 'org.springframework.boot:spring-boot-starter-actuator' // Spring Boot Actuator + implementation 'io.micrometer:micrometer-registry-prometheus' // Prometheus + implementation 'ch.qos.logback:logback-classic:1.5.6' // Logback + implementation 'ch.qos.logback:logback-core:1.5.6' // Logback + // DB implementation 'com.mysql:mysql-connector-j:9.0.0' // MySQL JDBC Driver implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..9dad698 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,57 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + ${LOG_PATH}/stage/application.%d{yyyy-MM-dd}.log + + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + ${LOG_PATH}/prod/application.%d{yyyy-MM-dd}.log + + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + From 3be03d5643c8cf29391997d81fa965c9f4614af3 Mon Sep 17 00:00:00 2001 From: JaeHoonSong <128021502+SongJaeHoonn@users.noreply.github.com> Date: Tue, 3 Sep 2024 01:06:25 +0900 Subject: [PATCH 13/16] =?UTF-8?q?environment:=20Jenkinsfile,=20Dockerfile?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/prod/Dockerfile | 12 + jenkins/prod/Jenkinsfile | 453 ++++++++++++++++++++++++++++++++++++++ jenkins/stage/Jenkinsfile | 50 ++--- 3 files changed, 483 insertions(+), 32 deletions(-) create mode 100644 jenkins/prod/Dockerfile create mode 100644 jenkins/prod/Jenkinsfile diff --git a/jenkins/prod/Dockerfile b/jenkins/prod/Dockerfile new file mode 100644 index 0000000..8887a17 --- /dev/null +++ b/jenkins/prod/Dockerfile @@ -0,0 +1,12 @@ +# Use the official OpenJDK 21 image from the Docker Hub +FROM openjdk:21-jdk + +# Expose port 8080 to the outside world +EXPOSE 8080 + +# Copy the JAR file into the container +COPY build/libs/time.jar /time.jar + +# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment. +# For example, use '-Dspring.profiles.active=production' for production environment. +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/time.jar"] \ No newline at end of file diff --git a/jenkins/prod/Jenkinsfile b/jenkins/prod/Jenkinsfile new file mode 100644 index 0000000..07db4dc --- /dev/null +++ b/jenkins/prod/Jenkinsfile @@ -0,0 +1,453 @@ +/* + * To use the following functionality: + * withCredentials([file(credentialsId: 'plus_prod_config_yml', variable: 'CONFIG_FILE')]) { + * + * The 'Pipeline Utility Steps' plugin must be installed on your Jenkins instance. + * + * Installation Guide: + * 1. Log in to your Jenkins instance. + * 2. Navigate to 'Manage Jenkins' from the main dashboard. + * 3. Click on 'Manage Plugins'. + * 4. Go to the 'Available plugins' tab. + * 5. In the search box, type 'Pipeline Utility Steps'. + * 6. Select the 'Pipeline Utility Steps' plugin from the list. + * 7. Click on 'Install without restart' or 'Install and restart' to install the plugin. + * 8. Once the plugin is installed, you can use functions like 'readYaml' and other utility steps in your Jenkinsfile. + * + * Ensure the plugin is up-to-date to avoid compatibility issues. + */ + +def FAILED_STAGE = "" + +pipeline { + agent any + + triggers { + githubPush() + } + + stages { + stage('Load Environment Variables') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + } + withCredentials([file(credentialsId: 'plus_prod_config_yml', variable: 'CONFIG_FILE')]) { + script { + def config = readYaml(file: env.CONFIG_FILE) + + env.JENKINS_DOMAIN = config.'jenkins-domain' + env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' + env.SLACK_COLOR_SUCCESS = config.slack.'color-success' + env.SLACK_COLOR_FAILURE = config.slack.'color-failure' + + env.MYSQL_USER = config.mysql.user + env.MYSQL_PASSWORD = config.mysql.password + env.MYSQL_DATABASE = config.mysql.database + env.BACKUP_DIR = config.mysql.'backup-dir' + + env.DOCKER_HUB_REPO = config.dockerhub.repo + env.DOCKER_HUB_USER = config.dockerhub.user + env.DOCKER_HUB_PASSWORD = config.dockerhub.password + + env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' + env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' + env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' + + env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' + env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' + env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' + + env.BLUE_CONTAINER = config.containers.blue + env.GREEN_CONTAINER = config.containers.green + env.BLUE_URL = config.containers.'blue-url' + env.GREEN_URL = config.containers.'green-url' + env.IMAGE_NAME = config.containers.'image-name' + + env.APPLICATION_NETWORK = config.networks.application + env.MONITORING_NETWORK = config.networks.monitoring + + env.PROFILE = config.spring.profile + env.PORT_A = config.spring.'port-a'.toString() + env.PORT_B = config.spring.'port-b'.toString() + + env.WHITELIST_ADMIN_USERNAME = config.admin.username + env.WHITELIST_ADMIN_PASSWORD = config.admin.password + + env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" + env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' + env.MYSQL_CONTAINER_NAME = config.docker.'mysql-container-name' + } + } + } + } + + stage('Check Java Version') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + } + sh 'java -version' + } + } + + stage('Get Git Change Log') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + env.GIT_CHANGELOG = getChangeLog() + } + } + } + + stage('MYSQL Backup') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + backupMysql() + } + } + } + + stage('Docker Hub Login') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + dockerLogin() + } + } + } + + stage('Determine Containers') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + determineContainers() + } + } + } + + stage('Build Application') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + buildApplication() + } + } + } + + stage('Build and Push Docker Image') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + buildAndPushDockerImage() + } + } + } + + stage('Deploy New Instance') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + deployNewInstance() + } + } + } + + stage('Health Check') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + performHealthCheck() + } + } + } + + stage('Switch Traffic and Cleanup') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + switchTrafficAndCleanup() + } + } + } + } + + post { + failure { + script { + sendSlackBuildNotification(":scream_cat: Stage *${FAILED_STAGE}* failed.", env.SLACK_COLOR_FAILURE) + } + } + + success { + script { + sendSlackBuildNotification(":rocket: Deployment completed successfully", env.SLACK_COLOR_SUCCESS) + } + } + } +} + +def sendSlackBuildNotification(String message, String color) { + def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}" + def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console" + + def payload = [ + blocks: [ + [ + type: "section", + text: [ + type: "mrkdwn", + text: message + ] + ] + ], + attachments: [ + [ + color: color, + blocks: [ + [ + type: "section", + text: [ + type: "mrkdwn", + text: "*Change Log:*\n${env.GIT_CHANGELOG}" + ] + ], + [ + type: "actions", + elements: [ + [ + type: "button", + text: [ + type: "plain_text", + text: "Job", + emoji: true + ], + url: jobUrl, + value: "click_job" + ], + [ + type: "button", + text: [ + type: "plain_text", + text: "Console Output", + emoji: true + ], + url: consoleOutputUrl, + value: "click_console_output" + ] + ] + ] + ].findAll { it != null } + ] + ] + ] + + withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { + def payloadJson = groovy.json.JsonOutput.toJson(payload) + sh """ + curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} + """ + } +} + +def getChangeLog() { + def previousCommit = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: 'HEAD~1' + def currentCommit = env.GIT_COMMIT ?: 'HEAD' + + def changeLog = sh( + script: "git log ${previousCommit}..${currentCommit} --pretty=format:\"* %h - %s (%an)\" --abbrev-commit", + returnStdout: true + ).trim() + + def lines = changeLog.split('\n') + if (lines.size() > 10) { + changeLog = lines.take(10).join('\n') + '\n... (truncated)' + } + + return changeLog +} + +def backupMysql() { + def BACKUP_FILE = "mysql_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql" + withEnv([ + "BACKUP_DIR=${env.BACKUP_DIR}", + "MYSQL_CONTAINER_NAME=${env.MYSQL_CONTAINER_NAME}", + "MYSQL_PASSWORD=${env.MYSQL_PASSWORD}", + "MYSQL_USER=${env.MYSQL_USER}", + "MYSQL_DATABASE=${env.MYSQL_DATABASE}" + ]) { + sh """ + echo "Backing up MySQL database to ${BACKUP_DIR}/${BACKUP_FILE}..." + docker exec ${MYSQL_CONTAINER_NAME} sh -c 'mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} > ${BACKUP_DIR}/${BACKUP_FILE}' + """ + } +} + +def dockerLogin() { + withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) { + sh """ + echo "Logging in to Docker Hub..." + echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin + """ + } +} + +def determineContainers() { + script { + withEnv([ + "BLUE_CONTAINER=${env.BLUE_CONTAINER}", + "GREEN_CONTAINER=${env.GREEN_CONTAINER}", + "BLUE_URL=${env.BLUE_URL}", + "GREEN_URL=${env.GREEN_URL}", + "PORT_A=${env.PORT_A}", + "PORT_B=${env.PORT_B}" + ]) { + def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0 + if (blueRunning) { + env.CURRENT_CONTAINER = BLUE_CONTAINER + env.DEPLOY_CONTAINER = GREEN_CONTAINER + env.NEW_TARGET = GREEN_URL + env.NEW_PORT = PORT_B + env.OLD_PORT = PORT_A + } else { + env.CURRENT_CONTAINER = GREEN_CONTAINER + env.DEPLOY_CONTAINER = BLUE_CONTAINER + env.NEW_TARGET = BLUE_URL + env.NEW_PORT = PORT_A + env.OLD_PORT = PORT_B + } + echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + } + } +} + +def buildApplication() { + withEnv([ + "PROFILE=${env.PROFILE}" + ]) { + sh """ + echo "Building application with profile ${PROFILE}..." + ./gradlew clean build -Penv=${PROFILE} -x test --stacktrace --info + """ + } +} + +def buildAndPushDockerImage() { + withEnv([ + "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}", + "IMAGE_NAME=${env.IMAGE_NAME}" + ]) { + sh """ + docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} . + docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} + docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} + """ + } +} + +def deployNewInstance() { + withEnv([ + "PROFILE=${env.PROFILE}", + "NEW_PORT=${env.NEW_PORT}", + "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}", + "MONITORING_NETWORK=${env.MONITORING_NETWORK}", + "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}", + "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}", + "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}", + "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}", + "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}", + "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "IMAGE_NAME=${env.IMAGE_NAME}" + ]) { + sh """ + echo "Stopping and removing existing container if it exists" + if docker ps | grep -q ${DEPLOY_CONTAINER}; then + docker stop ${DEPLOY_CONTAINER} + docker rm ${DEPLOY_CONTAINER} + fi + + echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" + docker run -d --name ${DEPLOY_CONTAINER} \\ + -p ${NEW_PORT}:8000 \\ + --network ${APPLICATION_NETWORK} \\ + -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ + -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ + -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\ + -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\ + -e SPRING_PROFILES_ACTIVE=${PROFILE} \\ + ${IMAGE_NAME}:${DEPLOY_CONTAINER} + + echo "Checking if monitoring network ${MONITORING_NETWORK} exists" + if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then + echo "Connecting to monitoring network ${MONITORING_NETWORK}" + docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER} + else + echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection." + fi + + echo "Listing all containers" + docker ps -a + """ + } +} + +def performHealthCheck() { + def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() + echo "Public IP address: ${PUBLIC_IP}" + + def start_time = System.currentTimeMillis() + def timeout = start_time + 150000 // 2.5 minutes + + while (System.currentTimeMillis() < timeout) { + def elapsed = (System.currentTimeMillis() - start_time) / 1000 + echo "Checking health... ${elapsed} seconds elapsed." + def status = sh( + script: """curl -s http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + returnStatus: true + ) + if (status == 0) { + echo "New application started successfully after ${elapsed} seconds." + return + } + sleep 5 + } + + if (System.currentTimeMillis() >= timeout) { + sh "docker stop ${env.DEPLOY_CONTAINER}" + sh "docker rm ${env.DEPLOY_CONTAINER}" + error "Health check failed" + } +} + +def switchTrafficAndCleanup() { + withEnv([ + "NEW_PORT=${env.NEW_PORT}", + "OLD_PORT=${env.OLD_PORT}", + "NEW_TARGET=${env.NEW_TARGET}", + "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}", + "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", + "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}" + ]) { + sh """ + echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}." + docker exec ${NGINX_CONTAINER_NAME} bash -c ' + export BACKEND_URL=${NEW_TARGET} + envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/time.conf.template > /etc/nginx/conf.d/time.conf + ' + docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/time.conf + docker exec ${NGINX_CONTAINER_NAME} nginx -t + docker exec ${NGINX_CONTAINER_NAME} nginx -s reload + + echo "Checking if current container ${CURRENT_CONTAINER} is running..." + if docker ps | grep -q ${CURRENT_CONTAINER}; then + docker stop ${CURRENT_CONTAINER} + docker rm ${CURRENT_CONTAINER} + echo "Removed old container ${CURRENT_CONTAINER}." + fi + """ + } +} \ No newline at end of file diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index a633b3c..52248be 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -1,6 +1,6 @@ /* * To use the following functionality: - * withCredentials([file(credentialsId: 'time_stage_config_yml', variable: 'CONFIG_FILE')]) { + * withCredentials([file(credentialsId: 'plus_stage_config_yml', variable: 'CONFIG_FILE')]) { * * The 'Pipeline Utility Steps' plugin must be installed on your Jenkins instance. * @@ -17,6 +17,7 @@ * Ensure the plugin is up-to-date to avoid compatibility issues. */ +def FAILED_STAGE = "" pipeline { agent any @@ -28,7 +29,10 @@ pipeline { stages { stage('Load Environment Variables') { steps { - withCredentials([file(credentialsId: 'time_stage_config_yml', variable: 'CONFIG_FILE')]) { + script { + FAILED_STAGE = env.STAGE_NAME + } + withCredentials([file(credentialsId: 'plus_stage_config_yml', variable: 'CONFIG_FILE')]) { script { def config = readYaml(file: env.CONFIG_FILE) @@ -79,22 +83,18 @@ pipeline { } stage('Check Java Version') { - steps { - sh 'java -version' - } - } - - stage('Slack Notification: Time Staging Server Testing') { steps { script { - sendSlackNotification(":test_tube: Testing Staging server(Time)...", env.SLACK_COLOR_SUCCESS) + FAILED_STAGE = env.STAGE_NAME } + sh 'java -version' } } stage('Get Git Change Log') { steps { script { + FAILED_STAGE = env.STAGE_NAME env.GIT_CHANGELOG = getChangeLog() } } @@ -103,6 +103,7 @@ pipeline { stage('MYSQL Backup') { steps { script { + FAILED_STAGE = env.STAGE_NAME backupMysql() } } @@ -111,6 +112,7 @@ pipeline { stage('Docker Hub Login') { steps { script { + FAILED_STAGE = env.STAGE_NAME dockerLogin() } } @@ -119,6 +121,7 @@ pipeline { stage('Determine Containers') { steps { script { + FAILED_STAGE = env.STAGE_NAME determineContainers() } } @@ -127,6 +130,7 @@ pipeline { stage('Build Application') { steps { script { + FAILED_STAGE = env.STAGE_NAME buildApplication() } } @@ -135,6 +139,7 @@ pipeline { stage('Build and Push Docker Image') { steps { script { + FAILED_STAGE = env.STAGE_NAME buildAndPushDockerImage() } } @@ -143,6 +148,7 @@ pipeline { stage('Deploy New Instance') { steps { script { + FAILED_STAGE = env.STAGE_NAME deployNewInstance() } } @@ -151,6 +157,7 @@ pipeline { stage('Health Check') { steps { script { + FAILED_STAGE = env.STAGE_NAME performHealthCheck() } } @@ -159,6 +166,7 @@ pipeline { stage('Switch Traffic and Cleanup') { steps { script { + FAILED_STAGE = env.STAGE_NAME switchTrafficAndCleanup() } } @@ -168,7 +176,7 @@ pipeline { post { failure { script { - sendSlackBuildNotification(":scream_cat: Deployment failed.", env.SLACK_COLOR_FAILURE) + sendSlackBuildNotification(":scream_cat: Stage *${FAILED_STAGE}* failed.", env.SLACK_COLOR_FAILURE) } } @@ -180,25 +188,6 @@ pipeline { } } -def sendSlackNotification(String message, String color) { - def payload = [ - attachments: [ - [ - color: color, - text: message.replaceAll('"', '\\"').replaceAll('\n', '\\\\n') - ] - ] - ] - - withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { - def payloadJson = groovy.json.JsonOutput.toJson(payload) - sh """ - curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} - """ - } -} - - def sendSlackBuildNotification(String message, String color) { def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}" def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console" @@ -293,7 +282,6 @@ def backupMysql() { docker exec ${MYSQL_CONTAINER_NAME} sh -c 'mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} > ${BACKUP_DIR}/${BACKUP_FILE}' """ } - sendSlackNotification(":floppy_disk: Time MySQL backup completed successfully: ${BACKUP_FILE}", env.SLACK_COLOR_SUCCESS) } def dockerLogin() { @@ -405,7 +393,6 @@ def deployNewInstance() { docker ps -a """ } - sendSlackNotification(":low_battery: Restarting Staging server(Time)...", env.SLACK_COLOR_SUCCESS) } def performHealthCheck() { @@ -430,7 +417,6 @@ def performHealthCheck() { } if (System.currentTimeMillis() >= timeout) { - sendSlackNotification(":scream_cat: New Staging application(Time) did not start successfully within 2.5 minutes.", env.SLACK_COLOR_FAILURE) sh "docker stop ${env.DEPLOY_CONTAINER}" sh "docker rm ${env.DEPLOY_CONTAINER}" error "Health check failed" From 8c4562b760a89fa34824228610e69b2f3bbd96fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Tue, 3 Sep 2024 02:05:17 +0900 Subject: [PATCH 14/16] =?UTF-8?q?environment:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=EA=B0=80=20=EC=97=86=EC=9D=8C?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../page/time/api/TimeServerApplicationTests.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/java/page/time/api/TimeServerApplicationTests.java diff --git a/src/test/java/page/time/api/TimeServerApplicationTests.java b/src/test/java/page/time/api/TimeServerApplicationTests.java deleted file mode 100644 index ec04db7..0000000 --- a/src/test/java/page/time/api/TimeServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package page.time.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TimeServerApplicationTests { - - @Test - void contextLoads() { - } - -} From ee81d1f374da4691517c86f3275291cebadd28b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Tue, 3 Sep 2024 02:57:00 +0900 Subject: [PATCH 15/16] =?UTF-8?q?environment:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EC=8B=9C=208080=20=ED=8F=AC=ED=8A=B8=EB=A1=9C=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=ED=8F=AC=EC=9B=8C=EB=94=A9=20=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/stage/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 52248be..cd2105d 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -372,7 +372,7 @@ def deployNewInstance() { echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" docker run -d --name ${DEPLOY_CONTAINER} \\ - -p ${NEW_PORT}:8000 \\ + -p ${NEW_PORT}:8080 \\ --network ${APPLICATION_NETWORK} \\ -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ @@ -450,4 +450,4 @@ def switchTrafficAndCleanup() { fi """ } -} \ No newline at end of file +} From dcf8a61f9d6e92739527a4bb40eccb00a976d258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=ED=9B=88?= <128021502+SongJaeHoonn@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:56:12 +0900 Subject: [PATCH 16/16] =?UTF-8?q?environment:=20=EC=BB=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=208080?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=EB=A1=9C=20=ED=8F=AC=ED=8A=B8=ED=8F=AC?= =?UTF-8?q?=EC=9B=8C=EB=94=A9=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(prod)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jenkins/prod/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/prod/Jenkinsfile b/jenkins/prod/Jenkinsfile index 07db4dc..1387c01 100644 --- a/jenkins/prod/Jenkinsfile +++ b/jenkins/prod/Jenkinsfile @@ -372,7 +372,7 @@ def deployNewInstance() { echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" docker run -d --name ${DEPLOY_CONTAINER} \\ - -p ${NEW_PORT}:8000 \\ + -p ${NEW_PORT}:8080 \\ --network ${APPLICATION_NETWORK} \\ -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ @@ -450,4 +450,4 @@ def switchTrafficAndCleanup() { fi """ } -} \ No newline at end of file +}