๐Ÿค– Jenkins Pipeline์„ ํ™œ์šฉํ•œ CI/CD ๊ตฌํ˜„ ๋ฐ Webhook

Dev96ยท2025๋…„ 3์›” 26์ผ
post-thumbnail

๐Ÿ“Œ ์›Œํฌ๋กœ๋“œ ์ƒํ™ฉ ์ •๋ฆฌ

1. ํŠน์ • EC2 ์ธ์Šคํ„ด์Šค ๋‚ด Docker๋กœ Jenkins ์ปจํ…Œ์ด๋„ˆ ์šด์˜
2. Jenkins Pipeline ๋‚ด์—์„œ Git Branch Checkout ์ •์˜
3. Jenkins Pipeline ๋‚ด์—์„œ AWS CLI ์‚ฌ์šฉ์ž ์ •์˜
4. Jenkins Pipeline ๋‚ด์—์„œ Dockerfile ์ •์˜
5. Jenkins Pipeline ๋‚ด์—์„œ Slack Notification ์ •์˜
6. Jenkins Pipeline ๋‚ด์—์„œ Build Gradle ์ •์˜
7. Jenkins Pipeline ๋‚ด์—์„œ ECR Registry & ECS Deploy ์ •์˜
8. Jenkins Pipeline ๋‚ด์—์„œ ์ปค๋ฐ‹ ๋กœ๊ทธ ์ถ”์ถœ Slack WebWook ์ •์˜


๐Ÿ”น ํŒŒ์ดํ”„๋ผ์ธ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์•ž์„œ ํ•„์š”ํ•œ Plugin

1๏ธโƒฃ AWS Credentials Plugin

2๏ธโƒฃ Pipeline: AWS Steps Plugin

3๏ธโƒฃ Slack Notification Plugin

4๏ธโƒฃ Git plugin


๐Ÿ”ง ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ ์š”์†Œ ์„ค๋ช…

  • agent ๐Ÿ‘‰ ์–ด๋””์„œ ์‹คํ–‰ํ• ์ง€๋ฅผ ์ •์˜ (any, label, docker, ๋“ฑ)
  • environment ๐Ÿ‘‰ ๊ณตํ†ต ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •
  • stages ๐Ÿ‘‰ ๋‹จ๊ณ„๋“ค์„ ๊ทธ๋ฃนํ•‘
  • stage ๐Ÿ‘‰ ๊ฐœ๋ณ„ ์ž‘์—… ๋‹จ๊ณ„ (์˜ˆ: ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ๋ฐฐํฌ ๋“ฑ)
  • steps ๐Ÿ‘‰ ํ•ด๋‹น ๋‹จ๊ณ„์—์„œ ์ˆ˜ํ–‰ํ•  ์ž‘์—…๋“ค
  • post ๐Ÿ‘‰ ์„ฑ๊ณต/์‹คํŒจ ํ›„์˜ ํ›„์ฒ˜๋ฆฌ ์ž‘์—… ์ •์˜

๐Ÿšจ AWS Credentials ์ •์˜

1) Jenkins ๊ด€๋ฆฌ -> Credentials

2) AWS Credentials ์ƒ์„ฑ

  • AWS Access Key Id : ๊ณต๊ฐœ์ ์œผ๋กœ ์‹๋ณ„ ๊ฐ€๋Šฅํ•œ ํ‚ค
  • AWS Secret Access Key : ํ•ด๋‹น Access Key ID์— ๋Œ€์‘ํ•˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ™์€ ๋น„๋ฐ€ ํ‚ค

โ€ป ์  ํ‚จ์Šค ํŒŒ์ดํ”„๋ผ์ธ ECR Registry & ECS Deploy ํ•ด๋‹น Stage ์—์„œ ์‚ฌ์šฉ ์˜ˆ์ •

ex) withAWS(credentials: '{AWS Credentials ID}', region: {AWS REGION}


โ€ป ์„ค๋ช…ํ•˜๊ธฐ ์•ž์„œ์„œ ํ•ด๋‹น ๊ธ€์— Stage View๋Š” ๊ทธ๋ฆผ๊ณผ ๊ฐ™๋‹ค.

pipeline {
  agent any

  environment {
    BITBUCKET_CREDENTIALS  = 'bc2fa053-e540-48bc-b7f7-b504430a63c9' 		     	 // Bitbucket ์ž๊ฒฉ ์ฆ๋ช… ID
    AWS_REGION             = 'ap-northeast-2'                        			    // AWS ๋ฆฌ์ „
    ECR_URI                = '998251115309.dkr.ecr.ap-northeast-2.amazonaws.com'   // ECR URI
    ECR_REPOSITORY_NAME    = 'test'                       						  // ECR ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ด๋ฆ„
    ECS_CLUSTER_NAME       = 'test-cluster'             						 // ECS ํด๋Ÿฌ์Šคํ„ฐ ์ด๋ฆ„
    ECS_SERVICE_NAME       = 'test-service'              						// ECS ์„œ๋น„์Šค ์ด๋ฆ„
    ECR_IMAGE_TAG          = 'latest'                               		   // ์ด๋ฏธ์ง€ ํƒœ๊ทธ
    SLACK_CREDENTIALS      = 'slack'                                		  // Slack ์ž๊ฒฉ ์ฆ๋ช… ID
    SLACK_CHANNEL          = '#test-notification'                			 // Slack ์ฑ„๋„
    COMMIT_FILE            = "previous_commit_hash.txt"            		    // ์ปค๋ฐ‹ ํ•ด์‹œ ์ €์žฅ ํŒŒ์ผ
    PREVIOUS_COMMIT        = ''                                    		   // ์ด์ „ ์ปค๋ฐ‹ ํ•ด์‹œ (๋™์  ํ• ๋‹น์šฉ)
    CURRENT_COMMIT         = ''                                     	  // ํ˜„์žฌ ์ปค๋ฐ‹ ํ•ด์‹œ (๋™์  ํ• ๋‹น์šฉ)
  }

  stages {

    stage('Checkout Bitbucket') {
      steps {
        git branch: 'test',
            credentialsId: BITBUCKET_CREDENTIALS,
            url: '{๋ธŒ๋žœ์น˜ ์ฃผ์†Œ}'
      }
    }

    stage('Write Dockerfile') {
      steps {
        script {
          // ๋™์ ์œผ๋กœ Dockerfile ์ž‘์„ฑ
          writeFile file: 'Dockerfile', text: '''
            FROM amazoncorretto:21
            ARG JAR_FILE=./build/libs/test-0.0.1-SNAPSHOT.jar
            COPY ${JAR_FILE} app.jar
            ENV SPRING_PROFILES_ACTIVE=dev
            ENTRYPOINT ["java", "-Xms3g", "-Xmx3g", "-XX:ActiveProcessorCount=2", "-XX:MaxRAMPercentage=75.0", "-XX:InitialRAMPercentage=75.0", "-jar", "app.jar"]
          '''
        }
      }
    }

    stage('Prepare Slack Notification') {
      steps {
        script {
          // ์ด์ „ ์ปค๋ฐ‹ ํ•ด์‹œ ํ™•์ธ (ํŒŒ์ผ์—์„œ ์ฝ๊ฑฐ๋‚˜ git์—์„œ ๊ฐ€์ ธ์˜ด)
          if (fileExists(COMMIT_FILE)) {
            PREVIOUS_COMMIT = readFile(COMMIT_FILE).trim()
          } else {
            PREVIOUS_COMMIT = sh(
              script: "git rev-parse HEAD~1",
              returnStdout: true
            ).trim()
          }

          // ํ˜„์žฌ ์ปค๋ฐ‹ ํ•ด์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ
          CURRENT_COMMIT = sh(
            script: "git rev-parse HEAD",
            returnStdout: true
          ).trim()
        }
      }
    }

    stage('Slack Build Notification') {
      steps {
        script {
          // ๋นŒ๋“œ ์‹œ์ž‘ ์•Œ๋ฆผ
          slackSend(
            color: "#439FE0",
            channel: SLACK_CHANNEL,
            message: "*ํ”„๋กœ์ ํŠธ ์ด๋ฆ„ : ${env.JOB_NAME}*\n*๋นŒ๋“œ๋ฒˆํ˜ธ : ${env.BUILD_NUMBER}*\n*๋นŒ๋“œ์‹คํ–‰ :jenkins_pepe_1:*",
            tokenCredentialId: SLACK_CREDENTIALS
          )
        }
      }
    }

    stage('Build Gradle') {
      steps {
        // Gradle ๋นŒ๋“œ ์‹คํ–‰
        sh 'chmod +x ./gradlew'
        sh './gradlew clean build -Dorg.gradle.java.home=/usr/lib/jvm/jdk-21'
      }
    }

    stage('ECR Registry & ECS Deploy') {
      steps {
        // AWS ์ธ์ฆ ๋ฐ ๋ฐฐํฌ ์ž‘์—…
        withAWS(credentials: 'aws-credentials-id', region: AWS_REGION) {
          sh '''
            aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_URI}

            docker build -t ${ECR_REPOSITORY_NAME} .
            docker tag ${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG} ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}
            docker push ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}

            docker rmi ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}
            docker rmi ${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}

            aws ecs update-service --cluster ${ECS_CLUSTER_NAME} --service ${ECS_SERVICE_NAME} --force-new-deployment
          '''
        }
      }
    }
  }

  post {
    success {
      script {
        // ๋ณ€๊ฒฝ๋œ ์ปค๋ฐ‹ ๋กœ๊ทธ ์ถ”์ถœ
        def newCommits = sh(
          script: "git log ${PREVIOUS_COMMIT}..${CURRENT_COMMIT} --pretty=format:'%s [%an]'",
          returnStdout: true
        ).trim()

        // ์ตœ์‹  ์ปค๋ฐ‹ ํ•ด์‹œ ์ €์žฅ
        writeFile file: COMMIT_FILE, text: CURRENT_COMMIT

        // Slack ์•Œ๋ฆผ - ๋นŒ๋“œ ์„ฑ๊ณต
        def message = newCommits ?
          newCommits.split('\n').collect { it + "  :jenkins_pepe_2:" }.join('\n') :
          "*๋ฐ˜์˜๋œ ์ปค๋ฐ‹ ์—†์Œ*"

        slackSend(
          color: "#36a64f",
          channel: SLACK_CHANNEL,
          message: "*๋นŒ๋“œ์„ฑ๊ณต : ${env.JOB_NAME}*\n*๋นŒ๋“œ๋ฒˆํ˜ธ : ${env.BUILD_NUMBER}*\n*๋ฐ˜์˜๋œ ์ปค๋ฐ‹ ๋ชฉ๋ก*\n${message}",
          tokenCredentialId: SLACK_CREDENTIALS
        )
      }
    }

    aborted {
      slackSend(
        color: "#ffcc00",
        channel: SLACK_CHANNEL,
        message: "*Build Aborted: ${env.JOB_NAME} - ${env.BUILD_NUMBER}*",
        tokenCredentialId: SLACK_CREDENTIALS
      )
    }

    unstable {
      slackSend(
        color: "#ffcc00",
        channel: SLACK_CHANNEL,
        message: "*Build Unstable: ${env.JOB_NAME} - ${env.BUILD_NUMBER}*",
        tokenCredentialId: SLACK_CREDENTIALS
      )
    }

    failure {
      script {
        // ์‹คํŒจํ•œ ๋นŒ๋“œ์˜ ์ปค๋ฐ‹ ๋กœ๊ทธ ์ถ”์ถœ
        def newCommits = sh(
          script: "git log ${PREVIOUS_COMMIT}..${CURRENT_COMMIT} --pretty=format:'%s [%an]'",
          returnStdout: true
        ).trim()

        // ์ปค๋ฐ‹ ํ•ด์‹œ ์ €์žฅ
        writeFile file: COMMIT_FILE, text: CURRENT_COMMIT

        // Slack ์•Œ๋ฆผ - ๋นŒ๋“œ ์‹คํŒจ
        def message = newCommits ?
          newCommits.split('\n').collect { it + "  :pepe2:" }.join('\n') :
          "*๋ฐ˜์˜๋œ ์ปค๋ฐ‹ ์—†์Œ*"

        slackSend(
          color: "#ff0000",
          channel: SLACK_CHANNEL,
          message: "*๋นŒ๋“œ์‹คํŒจ : ${env.JOB_NAME}*\n*๋นŒ๋“œ๋ฒˆํ˜ธ : ${env.BUILD_NUMBER}*\n*๋ฐ˜์˜๋œ ์ปค๋ฐ‹ ๋ชฉ๋ก*\n${message}",
          tokenCredentialId: SLACK_CREDENTIALS
        )
      }
    }
  }
}

profile
๋‹ค์–‘ํ•œ ๊ฒฝํ—˜๊ณผ ์‹ค๋ฌด์˜ ๊นŠ์ด๋กœ ํ‰๊ฐ€๋ฐ›๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ๋“ค์„ ์œ„ํ•ด ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด์—์„œ ๋ถ€๋”ชํžˆ๋ฉฐ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด ๊ฐ€์žฅ ์˜ค๋ž˜ ๋‚จ๋Š”๋‹ค๊ณ  ๋ฏฟ์Šต๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€