๋ฌด์ค‘๋‹จ ๋ฐฐํฌ


์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•  ๋•Œ ์ƒˆ๋กœ์šด ๋ฒ„์ „์„ ๋ฐฐํฌํ•˜๋Š” ๋™์•ˆ ์šด์˜์ค‘์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ข…๋ฃŒ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

์ด๋Ÿฐ ๋ฌธ์ œ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ ์ข‹์€ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค๊ฐ€ ์ •์ง€๋˜์ง€ ์•Š๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์—ฌ๋Ÿฌ ์ „๋žต์ด ์กด์žฌํ•œ๋‹ค.

๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ์ „๋žต

  • Rolling Update
  • Blue-Green Deployment
  • Canary Release

Rolling Update


๋กค๋ง ์ „๋žต์€ ์‚ฌ์šฉ์ค‘์ธ ์ธ์Šคํ„ด์Šค ๋‚ด์—์„œ ์ƒˆ ๋ฒ„์ „์„ ๊ต์ฒดํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์ „๋žต์ด๋‹ค. ์„œ๋น„์Šค์ค‘์ธ ์ธ์Šคํ„ด์Šค ํ•˜๋‚˜๋ฅผ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ์—์„œ ๋ผ์šฐํŒ…ํ•˜์ง€ ์•Š๋„๋ก ํ•œ ๋’ค ์ƒˆ ๋ฒ„์ „์„ ์ ์šฉํ•˜์—ฌ ๋‹ค์‹œ ๋ผ์šฐํŒ…ํ•˜๋Š” ์ „๋žต์œผ๋กœ ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ƒˆ๋กœ์šด ๋ฒ„์ „์œผ๋กœ ๊ต์ฒดํ•˜๋Š” ์ „๋žต์ด๋‹ค.

์žฅ์ 

  • ๋กค๋ง ์ „๋žต์€ ๊ตฌ์„ฑ๋œ ์ž์›์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ ์ฑ„๋กœ ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ธฐ์— ๊ด€๋ฆฌ๊ฐ€ ํŽธํ•˜๋‹ค.
  • ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ์ฐจ๋ก€๋กœ ๋ฐฐํฌ๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒํ™ฉ์— ๋”ฐ๋ผ ์†์‰ฝ๊ฒŒ ๋กค๋ฐฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋‹จ์ 

  • ๋กค๋ง ์ „๋žต์€ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ œํ•œ์ ์ผ ๊ฒฝ์šฐ์— ์‚ฌ์šฉ๋˜๋ฉฐ ์ƒˆ ๋ฒ„์ „์„ ๋ฐฐํฌํ•  ๋•Œ ๊ธฐ์กด ํŠธ๋ž˜ํ”ฝ์„ ๊ฐ๋‹นํ•˜๋˜ ์ธ์Šคํ„ด์Šค ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค ์ฒ˜๋ฆฌ ์šฉ๋Ÿ‰์„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ฐฐํฌ๊ฐ€ ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ ๊ตฌ๋ฒ„์ „๊ณผ ์‹ ๋ฒ„์ „์ด ๊ณต์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

Blue-Green Deployment


์šด์˜ ํ™˜๊ฒฝ์—์„œ ๊ตฌ ๋ฒ„์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ์‹  ๋ฒ„์ „์„ ๋ฐฐํฌํ•˜๊ณ  ์ผ์ œํžˆ ์ „ํ™˜ํ•˜์—ฌ ๋ชจ๋“  ํŠธ๋ž˜ํ”ฝ์„ ์‹  ๋ฒ„์ „์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ์ „๋žต์ด๋‹ค. ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต์€ ๋ฌผ๋ฆฌ์ ์ธ ์„œ๋ฒ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ๋น„์šฉ์ƒ ๋ฒ„๊ฒ๋‹ค. ๊ทธ๋ž˜์„œ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ ์‰ฝ๊ฒŒ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์—†์•จ ์ˆ˜ ์žˆ๋Š” AWS ๋‚˜ Docker ์™€ ๊ฐ™์€ ๊ฐ€์ƒ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํšจ๊ณผ์ ์ด๋‹ค.

์žฅ์ 

  • ๋กค๋ง ์ „๋žต๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋น ๋ฅธ ๋กค๋ฐฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๊ตฌ๋ฒ„์ „๊ณผ ๋™์ผํ•œ ํ™˜๊ฒฝ์—์„œ ์‹ ๋ฒ„์ „ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ์‹ ๋ฒ„์ „์„ ๋ฏธ๋ฆฌ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฐฐํฌ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ๋‚จ์•„์žˆ๋Š” ๊ตฌ๋ฒ„์ „ ํ™˜๊ฒฝ์„ ๋‹ค์Œ ๋ฐฐํฌ์— ์žฌ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์‹ ๋ฒ„์ „ ๋ฐฐํฌ๊ฐ€ ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ ์„œ๋ฒ„ ๊ณผ๋ถ€ํ™”๊ฐ€ ์ผ์–ด๋‚  ํ™•๋ฅ ์ด ์ ๋‹ค.

Canary Release


์นด๋‚˜๋ฆฌ์•„ ๋ผ๋Š” ์ƒˆ๋Š” ์œ ๋…๊ฐ€์Šค์— ๊ต‰์žฅํžˆ ๋ฏผ๊ฐํ•œ ๋™๋ฌผ์ด๋‹ค. ๊ณผ๊ฑฐ ๊ด‘๋ถ€๋“ค์€ ์œ ๋…๊ฐ€์Šค์— ๋ฏผ๊ฐํ•œ ์นด๋‚˜๋ฆฌ์•„ ์ƒˆ๋ฅผ ์œ ๋…๊ฐ€์Šค ๋ˆ„์ถœ์˜ ์œ„ํ—˜์„ ๊ฐ์ง€ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

์นด๋‚˜๋ฆฌ ์ „๋žต์€ ์œ„ํ—˜์„ ๋น ๋ฅด๊ฒŒ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฐํฌ ์ „๋žต์ด๋‹ค. ์‹ ๋ฒ„์ „์˜ ์ œ๊ณต ๋ฒ”์œ„๋ฅผ ๋Š˜๋ ค๊ฐ€๋ฉด์„œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ํ”ผ๋“œ๋ฐฑ ๊ณผ์ •์„ ๊ฑฐ์น  ์ˆ˜ ์žˆ๋‹ค. ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ๋ฅผ ํ†ตํ•ด ์‹ ๋ฒ„์ „์˜ ์ œํ’ˆ์„ ๊ฒฝํ—˜ํ•˜๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ํŠน์ง•์œผ๋กœ ์‹ ๋ฒ„์ „์„ ํŠน์ • ์‚ฌ์šฉ์ž ํ˜น์€ ๋‹จ์ˆœ ๋น„์œจ์— ๋”ฐ๋ผ ๊ตฌ๋ถ„ํ•ด ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์„œ๋ฒ„์˜ ํŠธ๋ž˜ํ”ฝ ์ผ๋ถ€๋ฅผ ์‹ ๋ฒ„์ „์œผ๋กœ ๋ถ„์‚ฐํ•˜์—ฌ ์˜ค๋ฅ˜ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์žฅ์ 

  • ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‹ ๋ฐฐํฌ ์ „์— ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ๋ฏธ๋ฆฌ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์นด๋‚˜๋ฆฌ ๋ฐฐํฌ๋Š” ๋‹จ๊ณ„์ ์ธ ์ „ํ™˜ ๋ฐฉ์‹์„ ํ†ตํ•ด ๋ถ€์ •์  ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•˜๊ณ  ์ƒํ™ฉ์— ๋”ฐ๋ผ ํŠธ๋ž˜ํ”ฝ ์–‘์„ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜ ๋กค๋ฐฑํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹จ์ 

  • ๋กค๋ง ์ „๋žต๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๊ตฌ๋ฒ„์ „๊ณผ ์‹ ๋ฒ„์ „์ด ๋ชจ๋‘ ์šด์˜๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฒ„์ „ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์‚ฌ์ „ ์ค€๋น„


์ „๋žต ๊ตฌ์„ฑ

๋ฌด์ค‘๋‹จ ๋ฐฐํฌ๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์œ„ ์ „๋žต ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์•ผ ํ–ˆ๊ณ  ๊ณต์ฑ… ํ”„๋กœ์ ํŠธ์—์„  ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต์„ ์„ ํƒํ–ˆ๋Š”๋ฐ ๊ทธ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ์—ฌ์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์—์„  ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต์ด ์ ํ•ฉํ•  ๊ฒƒ์ด๋ผ ํŒ๋‹จ
  2. ์‚ฌ์šฉ์ค‘์ธ EC2 ์ธ์Šคํ„ด์Šค๊ฐ€ ๋‘ ๊ฐœ์˜ ์Šคํ”„๋ง ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ๊ตฌ๋™ํ•˜๊ธฐ์—” ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ํด ๊ฒƒ์ด๋ผ ํŒ๋‹จ

๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต ์ค‘ ํ•˜๋‚˜์˜ EC2 ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€์—์„œ ๋‘ ๊ฐœ์˜ ์Šคํ”„๋ง ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ๋™ํ•˜์—ฌ ํฌํŠธ ์Šค์œ„์นญ์„ ํ†ตํ•ด ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ 2๋ฒˆ ์ด์œ  ๋•Œ๋ฌธ์— EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ๋Š˜๋ ค IP ์Šค์œ„์นญ์„ ํ†ตํ•œ ๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต์„ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

๋ธ”๋ฃจ ๊ทธ๋ฆฐ ์ „๋žต ์‹œ๋‚˜๋ฆฌ์˜ค

  1. ์ธ์Šคํ„ด์Šค๋“ค์„ group_a ์™€ group_b ๋กœ ๊ตฌ์„ฑ
  2. ๊ตฌ๋™์ค‘์ธ group ์„ ํ™•์ธ
  3. group_a ๊ฐ€ ๊ตฌ๋™์ค‘์ด๋ผ๋ฉด group_a ๋Š” blue ๋กœ group_b ๋Š” green ์œผ๋กœ ์„ค์ •
  4. green ๊ทธ๋ฃน์— ์‹ ๋ฒ„์ „ ๋ฐฐํฌ ํ›„ ์‹คํ–‰
  5. green ๊ทธ๋ฃน์— ๋Œ€ํ•œ Health Check ์ง„ํ–‰
  6. Nginx Reverse Proxy ์„ค์ •์„ green ๊ทธ๋ฃน์œผ๋กœ ์ „ํ™˜
  7. blue ๊ทธ๋ฃน ์„œ๋ฒ„๋ฅผ ๋ชจ๋‘ ์ข…๋ฃŒ

Global properties ์„ค์ •

group_a ์™€ group_b ๋กœ ๊ตฌ๋ถ„ํ•  WAS ์˜ IP ๋ฅผ ๋ช…์‹œํ•ด์ฃผ๊ธฐ ์œ„ํ•ด Global properties ๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

Manage Jenkins โ†’ Configure System โ†’ Global properties ์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ช…์‹œ๋œ IP ๋“ค๊ณผ SSH ํ†ต์‹ ์ด ์ด๋ฃจ์–ด์ ธ์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— Publish over SSH ํ•ญ๋ชฉ์— ๋ชจ๋‘ ๋“ฑ๋ก๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.

Pipeline ์ฝ”๋“œ


pipeline {
    agent any
 
    stages {
        stage('git clone') {
            steps {
                checkout([$class: 'GitSCM', branches: [[name: '*/dev']], extensions: [[$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: true, recursiveSubmodules: false, reference: '', trackingSubmodules: true]], userRemoteConfigs: [[credentialsId: 'github-webhook-access-token', url: 'https://github.com/woowacourse-teams/2022-gong-check']]])
            }
        }
 
        stage('build') {
            steps {
                dir('backend') {
                    sh '''
                        echo '=== start application bootJar ==='
                        ./gradlew clean bootJar
                    '''
                }
            }
        }
        
        stage('publish ssh to server') {
			      environment {
                GROUP_GREEN = sh (returnStdout: true, script: '''#!/bin/bash
                    group_a_array=($group_a)
                    alive_was_count=0
                    for i in "${group_a_array[@]}"
	                      do
		                        if curl -I -X OPTIONS "http://${i}:8080"; then
			                          alive_was_count=$((alive_was_count+1))
		                        fi
	                      done
                    if [ ${alive_was_count} -eq 0 ];then
                        echo 'group_a'
                    elif [ ${alive_was_count} -eq ${#group_a_array[@]} ];then
                        echo 'group_b'
                    else
                        echo 'currently runnig was counts are not correct'
                        exit 100
                    fi''').trim()
            }
 
            steps {
                dir('backend') {
					          script {
		                    def green_ips;
		                    def blue_ips;
		                    def envVar = env.getEnvironment();
		                    if ("group_a" == GROUP_GREEN) {
		                        green_ips = envVar.group_a.split(' ');
		                        blue_ips = envVar.group_b.split(' ');
		                    } else {
		                        green_ips = envVar.group_b.split(' ');
		                        blue_ips = envVar.group_a.split(' ');
		                    }
		                    def green_ips_inline = green_ips.join(' ');
		                    def map = parseSSHServerConfiguration();
	
		                    for (item in green_ips) {
		                        sshPublisher(publishers: [sshPublisherDesc(configName: map.get(item), transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh /home/ubuntu/script/server_start.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/deploy', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
						            }
 
                        sh '''#!/bin/bash
												    array=(''' +green_ips_inline+ ''')
												
												    for ip in "${array[@]}"
																do
																    url=${array[0]}
																    attempts=10
																    timeout=10
																    online=false
																
																    echo "Checking status of $url."
																
																    for (( i=1; i<=$attempts; i++ ))
																		    do
																		        code=$(curl -sL --connect-timeout 20 --max-time 30 -w "%{http_code}\\n" "$url:8080" -o /dev/null)
																		
																		        if [ "$code" = "200" ]; then
																		            online=true
																		            echo "Connection successful."
																		            break
																		        else
																		            sleep $timeout
																		            echo "Connection failed."
																		        fi
																		    done
																
																    if $online; then
																        echo "Monitor finished, website is online."
																        exit 0 # Build Success
																    else
																        echo "Monitor failed, website seems to be down."
																        exit 1 # Build Failed
																    fi
																done
												'''
 
                        sshPublisher(publishers: [sshPublisherDesc(configName: 'gongcheck-reverse-proxy-dev', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''#!/bin/bash
												    function parse_was_address() {
												        array=(''' +green_ips_inline+ ''')
												        str=""
												    	  for i in "${array[@]}"
												            do
												    	          str+='set $service_url '
												                str+="http://${i}:8080;\n"
												            done
												        echo -e "$str"
												    }
												    echo -e "$(parse_was_address)" | sudo tee /etc/nginx/conf.d/service-url.inc
												    sudo service nginx restart
												    ''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
 
	                      for (item in blue_ips) {
                            sshPublisher(publishers: [sshPublisherDesc(configName: map.get(item), transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh /home/ubuntu/script/kill.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/deploy', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
                        }
					          }
                }
            }
        }
 
        stage('clean up workspace') {
            steps {
                cleanWs deleteDirs: true
            }
        }
    }
    
    post {
        success {
            slackSend (channel: 'jenkins', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        failure {
            slackSend (channel: 'jenkins', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
    }
}
 
@NonCPS
def parseSSHServerConfiguration() {
    def xml = new XmlSlurper().parse("${JENKINS_HOME}/jenkins.plugins.publish_over_ssh.BapSshPublisherPlugin.xml");
    def server_nick_names = xml.'**'.findAll{it.name() == 'name'};
    def server_ip_address = xml.'**'.findAll{it.name() == 'hostname'};
    def map = new HashMap();
    for (i = 0; i < server_nick_names.size(); i++) {
        def server_ip_str = (String) server_ip_address.get(i);
        def server_nick_name_str = (String) server_nick_names.get(i);
        map.put(server_ip_str, server_nick_name_str);
    }
    println(map);
    return map;
}

๋ธ”๋ฃจ ๊ทธ๋ฆฐ ๊ทธ๋ฃน ๊ตฌ๋ถ„

environment {
    GROUP_GREEN = sh (returnStdout: true, script: '''#!/bin/bash
        group_a_array=($group_a)
        alive_was_count=0
        for i in "${group_a_array[@]}"
            do
                if curl -I -X OPTIONS "http://${i}:8080"; then
                    alive_was_count=$((alive_was_count+1))
                fi
            done
        if [ ${alive_was_count} -eq 0 ];then
            echo 'group_a'
        elif [ ${alive_was_count} -eq ${#group_a_array[@]} ];then
            echo 'group_b'
        else
            echo 'currently runnig was counts are not correct'
            exit 100
        fi''').trim()
}

Global properties ์„ค์ •์„ ํ†ตํ•ด group_a group_b ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. group_a ๋ฐฐ์—ด์„ ์ˆœํšŒํ•˜๋ฉด์„œ OPTIONS ์š”์ฒญ์„ ๋ณด๋‚ด ํ•ด๋‹น ๊ทธ๋ฃน์— ์†ํ•œ ์„œ๋ฒ„๋“ค์ด ๊ตฌ๋™์ค‘์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

environment ๋ธ”๋Ÿญ์„ ํ†ตํ•ด ์‰˜ ์Šคํฌ๋ฆฝํŠธ์—์„œ ๋ฐ˜ํ™˜ํ•œ ๊ฐ’์„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ๋‹ด์•„์„œ ์™ธ๋ถ€ Groovy ์Šคํฌ๋ฆฝํŠธ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

group_a ์— ์†ํ•œ ์„œ๋ฒ„๋“ค์ด ๊ตฌ๋™์ค‘์ด๋ผ๋ฉด GROUP_GREEN ์—๋Š” group_a ๋ผ๋Š” ๋ฌธ์ž์—ด์ด ์ €์žฅ๋œ๋‹ค.

๊ทธ๋ฃน๋ณ„ IP ๋ฐฐ์—ด ์ƒ์„ฑ

def green_ips;
def blue_ips;
def envVar = env.getEnvironment();
if ("group_a" == GROUP_GREEN) {
    green_ips = envVar.group_a.split(' ');
    blue_ips = envVar.group_b.split(' ');
} else {
    green_ips = envVar.group_b.split(' ');
    blue_ips = envVar.group_a.split(' ');
}

env.getEnvironment() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด Global properties ์—์„œ ์„ ์–ธํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ Groovy ์Šคํฌ๋ฆฝํŠธ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

environment ๋ธ”๋Ÿญ์—์„œ ์„ ์–ธํ•œ GROUP_GREEN ์„ ํ†ตํ•ด green_ips ์™€ blue_ips ๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค.

group_a ๊ฐ€ green ์ด๋ผ๋ฉด green_ips ์—๋Š” 192.168.1.199 ๊ฐ€ ํ• ๋‹น๋˜์—ˆ์„ ๊ฒƒ์ด๋‹ค.

IP ์— ํ•ด๋‹นํ•˜๋Š” SSH Server Name Map ์ƒ์„ฑ

@NonCPS
def parseSSHServerConfiguration() {
    def xml = new XmlSlurper().parse("${JENKINS_HOME}/jenkins.plugins.publish_over_ssh.BapSshPublisherPlugin.xml");
    def server_nick_names = xml.'**'.findAll{it.name() == 'name'};
    def server_ip_address = xml.'**'.findAll{it.name() == 'hostname'};
    def map = new HashMap();
    for (i = 0; i < server_nick_names.size(); i++) {
        def server_ip_str = (String) server_ip_address.get(i);
        def server_nick_name_str = (String) server_nick_names.get(i);
        map.put(server_ip_str, server_nick_name_str);
    }
    println(map);
    return map;
}

Jenkins ์˜ sshPublisher ๋Š” IP ๊ฐ€ ์•„๋‹Œ SSH Server Name ์œผ๋กœ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— IP ์— ํ•ด๋‹นํ•˜๋Š” SSH Server Name ์„ ์•Œ์•„๋‚ด์•ผ ํ•œ๋‹ค.

Jenkins ๋Š” Publish over SSH ํ•ญ๋ชฉ์—์„œ ์„ค์ •ํ–ˆ๋˜ SSH Server Name ๊ณผ IP ๋“ฑ์˜ ์ •๋ณด๋ฅผ ${JENKINS_HOME}/jenkins.plugins.publish_over_ssh.BapSshPublisherPlugin.xml ์— ์ €์žฅํ•œ๋‹ค.

์œ„ ํŒŒ์ผ์„ ํŒŒ์‹ฑํ•˜์—ฌ IP ์™€ SSH Server Name ์ด key-value ํ˜•ํƒœ๋กœ ์ด๋ฃจ์–ด์ง„ Map ์„ ์ƒ์„ฑํ•ด ์Šคํฌ๋ฆฝํŠธ์—์„œ ํ™œ์šฉํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

Jenkins ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ž‘์—… ์ค‘์ง€ ๋ฐ ์žฌ๊ฐœ ๋“ฑ์„ ์œ„ํ•ด ์Šคํฌ๋ฆฝํŠธ์˜ step ๋ณ„ ์ƒํƒœ๋ฅผ ์ง๋ ฌํ™”ํ•˜์—ฌ ์ €์žฅํ•˜๋Š”๋ฐ, XML ํŒŒ์‹ฑ ๊ด€๋ จ ๊ฐ์ฒด๋“ค์€ ์ง๋ ฌํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜์—ฌ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

๋•Œ๋ฌธ์— XML ํŒŒ์‹ฑ ๊ด€๋ จ ๋กœ์ง์„ ๋ณ„๋„์˜ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ @NonCPS ์• ๋„ˆํ…Œ์ด์…˜์„ ํ†ตํ•ด ์ง๋ ฌํ™” ๋ฐ ์ €์žฅ ๋Œ€์ƒ์—์„œ ์ œ์™ธ์‹œํ‚จ๋‹ค.

{
		192.168.1.218=gongcheck-frontend-dev, 
		192.168.1.247=gongcheck-frontend-prod, 
		192.168.1.220=gongcheck-image-prod, 
		192.168.1.199=gongcheck-backend-dev-a, 
		192.168.1.234=gongcheck-backend-dev-b, 
		192.168.1.201=gongcheck-backend-prod, 
		192.168.1.212=gongcheck-image-dev, 
		192.168.1.240=gongcheck-reverse-proxy-dev
}

ํŒŒ์‹ฑ๋œ Map ์„ ํ™•์ธํ•ด๋ณด๋ฉด ์œ„์™€ ๊ฐ™์ด IP ์— ํ•ด๋‹นํ•˜๋Š” SSH Server Name ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์ƒ์„ฑ๋˜์–ด ์žˆ๋‹ค.

Green ๊ทธ๋ฃน ๋ฐฐํฌ ๋ฐ WAS ์‹คํ–‰

for (item in green_ips) {
    sshPublisher(publishers: [sshPublisherDesc(configName: map.get(item), transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh /home/ubuntu/script/server_start.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/deploy', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
}

green_ips ๋ฐฐ์—ด์„ ์ˆœํšŒํ•˜๋ฉด์„œ map.get(item) ์„ ํ†ตํ•ด IP ์— ํ•ด๋‹นํ•˜๋Š” SSH Server Name ์„ ํ†ตํ•ด sshPublisher ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

green_ips ์— 192.168.1.199 ๊ฐ€ ๋“ค์–ด์žˆ๋‹ค๋ฉด, map.get(item) ์„ ํ†ตํ•ด gongcheck-backend-dev-a ์— ํ•ด๋‹นํ•˜๋Š” WAS ๋กœ sshPublisher ๋ฅผ ์‹คํ–‰ํ•  ๊ฒƒ์ด๋‹ค.

echo "> ํ˜„์žฌ ์ง„ํ–‰์ค‘์ธ application pid ์กฐํšŒ"
CURRENT_PID=$(ps -ef | grep java | grep jar | grep -v nohup | grep gong-check | awk '{print $2}')
echo "> ํ˜„์žฌ ์ง„ํ–‰์ค‘์ธ application pid : $CURRENT_PID"
 
if [ -z ${CURRENT_PID} ]; then
echo "> ํ˜„์žฌ ๊ตฌ๋™์ค‘์ธ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—†์œผ๋ฏ€๋กœ ์ข…๋ฃŒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
else
echo "> sudo kill -9 $CURRENT_PID"
sudo kill -9 ${CURRENT_PID}
sleep 10
sudo lsof -i:8080
echo "> ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ •์ƒ ์ข…๋ฃŒ ์™„๋ฃŒ"
fi
 
echo "> ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ ์‹œ์ž‘"
JAR_PATH=$(ls -t /home/ubuntu/deploy/*.jar | head -1)
BUILD_ID=dontKillMe sudo nohup java -jar ${JAR_PATH} --spring.profiles.active=dev  2>> /dev/null >> /dev/null &
echo "> ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ ์ข…๋ฃŒ"

sshPublisher ๋Š” ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€์— ์žˆ๋Š” server_start.sh ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š”๋ฐ, ์ด๋Š” ์œ„์™€ ๊ฐ™๋‹ค.

Green ๊ทธ๋ฃน Health Check

sh '''#!/bin/bash
    array=(''' +green_ips_inline+ ''')
 
    for ip in "${array[@]}"
				do
				    url=${array[0]}
				    attempts=10
				    timeout=10
				    online=false
				
				    echo "Checking status of $url."
				
				    for (( i=1; i<=$attempts; i++ ))
						    do
						        code=$(curl -sL --connect-timeout 20 --max-time 30 -w "%{http_code}\\n" "$url:8080" -o /dev/null)
						
						        if [ "$code" = "200" ]; then
						            online=true
						            echo "Connection successful."
						            break
						        else
						            sleep $timeout
						            echo "Connection failed."
						        fi
						    done
				
				    if $online; then
				        echo "Monitor finished, website is online."
				        exit 0 # Build Success
				    else
				        echo "Monitor failed, website seems to be down."
				        exit 1 # Build Failed
				    fi
				done
'''

์‰˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด green ๊ทธ๋ฃน์— ๋Œ€ํ•œ Health Check ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค. curl ์š”์ฒญ์„ ํ†ตํ•ด ํ•ด๋‹น IP ์˜ 8080๋ฒˆ ํฌํŠธ๊ฐ€ ์—ด๋ ค์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. 10๋ฒˆ์˜ ์‹œ๋„ ๊ฐ„ ์„œ๋ฒ„๊ฐ€ ์‘๋‹ตํ•˜์ง€ ๋ชปํ•œ๋‹ค๋ฉด ์„œ๋ฒ„ ๋ฐฐํฌ์— ์‹คํŒจํ–ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜๊ณ  ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ข…๋ฃŒํ•œ๋‹ค.

Nginx ์„ค์ • ๋ณ€๊ฒฝ

green ์— ํ•ด๋‹นํ•˜๋Š” WAS ๊ฐ€ ์ค€๋น„๊ฐ€ ๋˜์–ด ์žˆ์œผ๋‹ˆ Nginx ์—์„œ ์š”์ฒญ์„ green ์œผ๋กœ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •์„ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

server {
	server_name dev.gongcheck.shop;
 
	include /etc/nginx/conf.d/service-url.inc;
 
	location / {
		proxy_pass $service_url/index.html;
	}
 
	# ...
}

ํ˜„์žฌ Nginx ์—์„  proxy_pass ์— service_url ์ด๋ผ๋Š” ๋ณ€์ˆ˜๋ฅผ ํ• ๋‹นํ•˜๊ณ  ์žˆ๋‹ค.

sshPublisher(publishers: [sshPublisherDesc(configName: 'gongcheck-reverse-proxy-dev', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''#!/bin/bash
    function parse_was_address() {
        array=(''' +green_ips_inline+ ''')
        str=""
    	  for i in "${array[@]}"
            do
    	          str+='set $service_url '
                str+="http://${i}:8080;\n"
            done
        echo -e "$str"
    }
    echo -e "$(parse_was_address)" | sudo tee /etc/nginx/conf.d/service-url.inc
    sudo service nginx restart
    ''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])

ํ•ด๋‹น ๋ณ€์ˆ˜๋Š” service-url.inc ๋ฅผ ํ†ตํ•ด ์„ ์–ธ๋˜๊ณ  ์žˆ๊ธฐ์— ์ด๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ์‰˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ sshPublisher ๋ฅผ ํ†ตํ•ด ์ˆ˜ํ–‰ํ•œ๋‹ค. tee ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ํŒŒ์ผ์„ ๋ฎ์–ด์”Œ์šฐ๊ฑฐ๋‚˜ ์ƒˆ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

set $service_url http://192.168.1.199:8080;

green_ips_inline ์— 192.168.1.199 ๋งŒ ์กด์žฌํ•œ๋‹ค๋ฉด, service-url.inc ํŒŒ์ผ์—๋Š” ์œ„์™€ ๊ฐ™์€ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž‘์„ฑ๋˜์–ด ์žˆ์„ ๊ฒƒ์ด๋‹ค.

Blue ๊ทธ๋ฃน WAS ์ข…๋ฃŒ

for (item in blue_ips) {
    sshPublisher(publishers: [sshPublisherDesc(configName: map.get(item), transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh /home/ubuntu/script/kill.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/deploy', remoteDirectorySDF: false, removePrefix: 'build/libs', sourceFiles: 'build/libs/*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
}

blue_ips ๋ฐฐ์—ด์„ ์ˆœํšŒํ•˜๋ฉด์„œ map.get(item) ์„ ํ†ตํ•ด IP ์— ํ•ด๋‹นํ•˜๋Š” SSH Server Name ์„ ํ†ตํ•ด sshPublisher ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

blue_ips ์— 192.168.1.234 ๊ฐ€ ๋“ค์–ด์žˆ๋‹ค๋ฉด, map.get(item) ์„ ํ†ตํ•ด gongcheck-backend-dev-b ์— ํ•ด๋‹นํ•˜๋Š” WAS ๋กœ sshPublisher ๋ฅผ ์‹คํ–‰ํ•  ๊ฒƒ์ด๋‹ค.

echo "> ํ˜„์žฌ ์ง„ํ–‰์ค‘์ธ application pid ์กฐํšŒ"
CURRENT_PID=$(ps -ef | grep java | grep jar | grep -v nohup | grep gong-check | awk '{print $2}')
echo "> ํ˜„์žฌ ์ง„ํ–‰์ค‘์ธ application pid : $CURRENT_PID"
 
if [ -z ${CURRENT_PID} ]; then
echo "> ํ˜„์žฌ ๊ตฌ๋™์ค‘์ธ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—†์œผ๋ฏ€๋กœ ์ข…๋ฃŒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
else
echo "> sudo kill -9 $CURRENT_PID"
sudo kill -9 ${CURRENT_PID}
sleep 10
sudo lsof -i:8080
echo "> ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ •์ƒ ์ข…๋ฃŒ ์™„๋ฃŒ"
fi

sshPublisher ๋Š” ์ธ์Šคํ„ด์Šค ๋‚ด๋ถ€์— ์žˆ๋Š” kill.sh ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š”๋ฐ, ์ด๋Š” ์œ„์™€ ๊ฐ™๋‹ค.

๊ฐœ์„  ์‚ฌํ•ญ


์ถ”ํ›„ WAS ๋ฅผ ๋‹ค์ค‘ํ™”ํ•œ๋‹ค๋ฉด ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ์„ ์ ์šฉํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— Nginx ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ˆ˜์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ ์„ค์ • ์ ์šฉ์€ ํฌ๊ฒŒ ์–ด๋ ต์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ถ”ํ›„์— ์„œ๋น„์Šค๊ฐ€ WAS ๋‹ค์ค‘ํ™”๋ฅผ ์š”๊ตฌํ•œ๋‹ค๋ฉด ๊ทธ๋•Œ ์ˆ˜์ •ํ•ด์ฃผ์–ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค.

์ถ”๊ฐ€๋กœ blue ๊ทธ๋ฃน์— ํ•ด๋‹นํ•˜๋Š” EC2 ์ธ์Šคํ„ด์Šค๊ฐ€ idle ์ƒํƒœ๋กœ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ถˆํ•„์š”ํ•œ ๋น„์šฉ์ด ๋ฐœ์ƒํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ๋•Œ๋ฌธ์— ๋ฐฐํฌ๊ฐ€ ์ด๋ฃจ์–ด์งˆ ๋•Œ ๋งˆ๋‹ค EC2 ์ธ์Šคํ„ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ ๋ฐ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด ๋ณผ ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

References