๐ ๋ค์ด๊ฐ๋ฉฐ
์๋ ํ์ธ์! ์ด๋ฒ ๊ธ์์๋ ์๋น์ค์ ๋จ์ผ Oracle Cloud ์ธ์คํด์ค์์ ๋ค์ดํ์ ์์ด ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ๊ณต์ ํฉ๋๋ค.
ํด๋ผ์ฐ๋ ๋น์ฉ ์ต์ ํ๋ฅผ ์ํด ๋จ์ผ ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๊ณ ์์๋๋ฐ, ๋ฐฐํฌํ ๋๋ง๋ค ์๋น์ค๊ฐ ์ ์ ์ค๋จ๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 8080 / 8081 ํฌํธ๋ฅผ ๋ฒ๊ฐ์ ์ฌ์ฉํ๋ Blue-Green ์ ๋ต๊ณผ Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ์ ํ ๋ฐฉ์์ ๋์ ํ์ต๋๋ค.
์ด ๊ธ์์ ๋ค๋ฃฐ ๋ด์ฉ์ ์๋์ ๊ฐ์ต๋๋ค.
- ๋จ์ผ ์๋ฒ Blue-Green ๋ฐฐํฌ์ ์๋ฆฌ
- Jenkins Multibranch Pipeline ์ค์ ๋ฐ ๋ธ๋์น๋ณ ์๋ ๋ฐฐํฌ
- Jenkins Pipeline ์ ์ฒด ํ๋ฆ
- Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ์ ํ ๋ฐฉ๋ฒ
- ํฌ์ค์ฒดํฌ ๊ธฐ๋ฐ ์๋ ๋กค๋ฐฑ
๐๏ธ 1. ์ ์ฒด ๋ฐฐํฌ ํ๋ฆ

GitHub Push (develop / main ๋ธ๋์น)
→ GitHub Webhook
→ Jenkins Multibranch Pipeline ์๋ ํธ๋ฆฌ๊ฑฐ
→ Gradle bootJar ๋น๋
→ JAR ํ์ผ ์๊ฒฉ ์๋ฒ(SSH)๋ก ์ ์ก
→ ์๊ฒฉ ์๋ฒ์์ Docker Image ๋น๋
→ ์ ํฌํธ(8080 or 8081)์ ์ปจํ
์ด๋ ์คํ
→ ํฌ์ค์ฒดํฌ ํต๊ณผ
→ Nginx ์ค์ ์ ํ (์ฌ๋ณผ๋ฆญ ๋งํฌ)
→ ์ด์ ์ปจํ
์ด๋ ์ข
๋ฃ
์ ์ฒด ๋ฐฐํฌ๊ฐ ์๋ํ๋์ด ์์ผ๋ฉฐ, ํฌ์ค์ฒดํฌ ์คํจ ์ ์๋์ผ๋ก ๋กค๋ฐฑ๋ฉ๋๋ค.
๐ต๐ข 2. Blue-Green ๋ฐฐํฌ ์๋ฆฌ

์๋ฒ ๋ด๋ถ์๋ ํญ์ ๋ ๊ฐ์ ํฌํธ๊ฐ ์ค๋น๋์ด ์์ต๋๋ค.
- Blue (์: 8080) : ํ์ฌ ํธ๋ํฝ์ ๋ฐ๊ณ ์๋ ์ปจํ ์ด๋
- Green (์: 8081) : ์ ๋ฒ์ ์ ๋ฐฐํฌํ ๋๊ธฐ ํฌํธ
๋ฐฐํฌ ์ Green์ ์ ๋ฒ์ ์ ์ฌ๋ฆฌ๊ณ , ํฌ์ค์ฒดํฌ๊ฐ ํต๊ณผํ๋ฉด Nginx๊ฐ Green์ผ๋ก ํธ๋ํฝ์ ์ ํํฉ๋๋ค. ์ด์ Blue ์ปจํ ์ด๋๋ ๊ทธ ํ์ ์ข ๋ฃ๋ฉ๋๋ค.
[์ฌ์ฉ์ ์์ฒญ]
↓
Nginx (80)
↓ (์ฌ๋ณผ๋ฆญ ๋งํฌ๋ก ํ์ฌ ํ์ฑ ํฌํธ ๊ฒฐ์ )
app_8080 ๋๋ app_8081 (Docker ์ปจํ
์ด๋)
↓
Redis (๋ด๋ถ ๋คํธ์ํฌ)
Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ์ ํ
app_8080.conf์ app_8081.conf ๋ ๊ฐ์ ์ค์ ํ์ผ์ ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋๊ณ , ํ์ฑํํ ํ์ผ์ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ก ์ง์ ํฉ๋๋ค.
# /etc/nginx/sites-available/app_8080.conf
server {
listen 80;
server_name _;
# API ์์ฒญ
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
app_8081.conf๋ ํฌํธ ๋ฒํธ๋ง 8081๋ก ๋ฐ๊ฟ ๋์ผํ๊ฒ ๊ตฌ์ฑํฉ๋๋ค.
๋ฐฐํฌ ์ ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ๊ต์ฒดํ๊ณ Nginx๋ฅผ reloadํ๋ฉด ํธ๋ํฝ์ด ์ ํ๋ฉ๋๋ค.
sudo ln -sf /etc/nginx/sites-available/app_8081.conf /etc/nginx/sites-enabled/app.conf
sudo nginx -t && sudo systemctl reload nginx
systemctl reload nginx๋ ๊ธฐ์กด ์ปค๋ฅ์ ์ ๋์ง ์๊ณ ์ ์ค์ ์ ์ ์ฉํฉ๋๋ค. ์งํ ์ค์ธ ์์ฒญ์ ์ด์ ์ปจํ ์ด๋๊ฐ ์ฒ๋ฆฌ๋ฅผ ์๋ฃํ๊ณ , ์ ์์ฒญ๋ถํฐ ์ ์ปจํ ์ด๋๋ก ํฅํฉ๋๋ค.
๐ ๏ธ 3. Jenkins ์ค์
Multibranch Pipeline ์์ฑ
Jenkins์์ New Item → Multibranch Pipeline์ ์ ํํฉ๋๋ค.

์ผ๋ฐ Pipeline๊ณผ ๋ฌ๋ฆฌ Multibranch Pipeline์ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ค์บํ์ฌ ๋ธ๋์น๋ง๋ค ๋ ๋ฆฝ์ ์ธ ํ์ดํ๋ผ์ธ์ ์์ฑํฉ๋๋ค.
- develop ๋ธ๋์น Push/Merge → dev ์๋ฒ ์๋ ๋ฐฐํฌ
- main ๋ธ๋์น Push/Merge → prod ์๋ฒ ์๋ ๋ฐฐํฌ
Branch Sources์์ GitHub ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ์ฐ๊ฒฐํ๊ณ , Scan Multibranch Pipeline Triggers์์ GitHub hook trigger๋ฅผ ํ์ฑํํ๋ฉด Push/Merge ์ด๋ฒคํธ ๋ฐ์ ์ ํ์ดํ๋ผ์ธ์ด ์๋์ผ๋ก ์คํ๋ฉ๋๋ค.
Credentials ๋ฑ๋ก
Jenkins > Manage Jenkins > Credentials์์ ์๋ ํญ๋ชฉ๋ค์ ๋ฑ๋กํฉ๋๋ค.
| Credential ID | ํ์ | ์ฉ๋ |
| goody_server_key | SSH Username with private key | ๋ฐฐํฌ ์๋ฒ SSH ์ ์ ํค |
| jenkins_github_key | SSH Username with private key | GitHub ๋ ํฌ์งํ ๋ฆฌ ํด๋ก ํค |
| fcm-private-key | Secret text | FCM ์๋น์ค ๊ณ์ ํค |
| SENTRY_AUTH_TOKEN | Secret text | Sentry ๋ฐฐํฌ ์๋ฆผ ํ ํฐ |
โ๏ธ 4. Jenkins Pipeline ์ ์ฒด ํ๋ฆ
ํ์ดํ๋ผ์ธ์ ์ด 4๋จ๊ณ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.

| ๋จ๊ณ | ์ค๋ช |
| Git Clone | ๋ธ๋์น ๊ธฐ์ค์ผ๋ก ์ฝ๋ ํด๋ก |
| Jar Build | Gradle๋ก bootJar ์์ฑ |
| Find Jar File | ๋น๋๋ JAR ํ์ผ ๊ฒฝ๋ก ํ์ธ |
| Remote Deploy | ์๊ฒฉ ์๋ฒ์ JAR ์ ์ก → ํ์ฑ ํฌํธ ๊ฐ์ง ํ ๋ค๋ฅธ ํฌํธ์์ Docker ์ด๋ฏธ์ง ๋น๋ ๋ฐ ์คํ → ํฌ์ค์ฒดํฌ → Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ๋ณ๊ฒฝ → ์ด์ ์ปจํ ์ด๋ ์ ๋ฆฌ |
def jarFile
pipeline {
agent any
environment {
IMAGE_NAME = "goody-api"
SERVER = env.BRANCH_NAME == 'main' ? '<prod_server_ip>' : '<dev_server_ip>'
SSH_USER = "ubuntu"
SSH_KEY = credentials('goody_server_key')
GIT_CREDENTIALS_ID = 'jenkins_github_key'
SENTRY_AUTH_TOKEN = credentials('SENTRY_AUTH_TOKEN')
}
stages {
stage('Git Clone') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: env.BRANCH_NAME]],
userRemoteConfigs: [[
url: 'git@github.com:<org>/<repo>.git',
credentialsId: "${GIT_CREDENTIALS_ID}"
]]
])
}
}
stage('Jar Build') {
steps {
sh 'chmod +x ./gradlew'
sh './gradlew --no-daemon clean :goody-server-api:bootJar'
}
}
stage('Find Jar File') {
steps {
script {
def jarPath = "./goody-server-api/build/libs/"
jarFile = sh(script: "find ${jarPath} -name '*.jar' -print -quit", returnStdout: true).trim()
if (jarFile.isEmpty()) {
error "Jar file not found in ${jarPath}"
}
echo "Found Jar: ${jarFile}"
}
}
}
stage('Remote Deploy') {
steps {
script {
withCredentials([string(credentialsId: 'fcm-private-key', variable: 'FCM_KEY_JSON')]) {
// JAR ํ์ผ์ ์๊ฒฉ ์๋ฒ๋ก ์ ์ก
sh "scp -i $SSH_KEY ${jarFile} $SSH_USER@$SERVER:/home/$SSH_USER/app.jar"
// ์๊ฒฉ ์๋ฒ์์ ๋ฐฐํฌ ์คํฌ๋ฆฝํธ ์คํ
sh """
ssh -i $SSH_KEY $SSH_USER@$SERVER /bin/bash -s << 'EOF'
# ํ์ฌ ํ์ฑ ํฌํธ ๊ฐ์ง
activePort=\\$(docker ps --format "{{.Names}}" | grep -oE "app_(8080|8081)" | cut -d'_' -f2)
if [ "\\$activePort" = "8080" ]; then
nextPort="8081"
prevContainer="app_8080"
else
nextPort="8080"
prevContainer="app_8081"
fi
nextContainer="app_\\${nextPort}"
# ์๊ฒฉ ์๋ฒ์์ Docker ์ด๋ฏธ์ง ๋น๋
docker build -t ${IMAGE_NAME} -f /home/${SSH_USER}/Dockerfile /home/${SSH_USER}/
# ๊ธฐ์กด ๋๊ธฐ ์ปจํ
์ด๋ ์ ๋ฆฌ ํ ์ ๋ฒ์ ์คํ
docker stop \\$nextContainer || true
docker rm \\$nextContainer || true
docker run -d --name \\$nextContainer \\\\
--env-file /home/${SSH_USER}/.env \\\\
--network ubuntu_goody-net \\\\
-p \\$nextPort:8080 \\\\
${IMAGE_NAME}
# ํฌ์ค์ฒดํฌ — 90์ด(18ํ × 5์ด) ์๋
for i in {1..18}; do
status_code=\\$(curl -s -o /dev/null -w "%{http_code}" <http://localhost>:\\${nextPort}/actuator/health)
if [ "\$status_code" -eq 200 ]; then
echo "ํฌ์ค์ฒดํฌ ์ฑ๊ณต — Nginx ์ ํ"
sudo ln -sf /etc/nginx/sites-available/app_\${nextPort}.conf \\
/etc/nginx/sites-enabled/app.conf
sudo nginx -t && sudo systemctl reload nginx
docker stop \${prevContainer} || true
docker rm \${prevContainer} || true
exit 0
fi
echo "ํฌ์ค์ฒดํฌ ์คํจ (\\${i}/18) — 5์ด ํ ์ฌ์๋"
sleep 5
done
# 90์ด ํ์๋ ์คํจ → ๋กค๋ฐฑ
echo "๋ฐฐํฌ ์คํจ — ๋กค๋ฐฑ"
docker stop \\${nextContainer} || true
docker rm \\${nextContainer} || true
exit 1
'EOF'
"""
}
}
}
}
}
}
4-1. Remote Deploy ๋จ๊ณ ํ๋ฆ
Remote Deploy ์คํ ์ด์ง๋ ์๊ฒฉ ์๋ฒ์์ ์๋ ์์๋ก ๋์ํฉ๋๋ค.
โ ํ์ฑ ํฌํธ ๊ฐ์ง
docker ps๋ก ํ์ฌ ์คํ ์ค์ธ ์ปจํ ์ด๋ ์ด๋ฆ์ ํ์ธํด ํ์ฑ ํฌํธ(8080 ๋๋ 8081)๋ฅผ ๊ฐ์งํฉ๋๋ค. ๊ฐ์ง๋ ํฌํธ์ ๋ฐ๋ ํฌํธ๊ฐ ๋ค์ ๋ฐฐํฌ ๋์์ด ๋ฉ๋๋ค.
activePort=$(docker ps --format "{{.Names}}" | grep -oE "app_(8080|8081)" | cut -d'_' -f2)
# activePort=8080 ์ด๋ฉด → nextPort=8081
# activePort=์์(์ด๊ธฐ ๋ฐฐํฌ) ์ด๋ฉด → nextPort=8080
โก Docker ์ด๋ฏธ์ง ๋น๋ ๋ฐ ์ปจํ ์ด๋ ์คํ
Jenkins์์ ์ ์ก๋ฐ์ JAR ํ์ผ๋ก ์๊ฒฉ ์๋ฒ์ Dockerfile์ ์ด์ฉํด ์ด๋ฏธ์ง๋ฅผ ๋น๋ํ๊ณ , ๋นํ์ฑ ํฌํธ์ ์ ์ปจํ ์ด๋๋ฅผ ์คํํฉ๋๋ค. ์ด ์์ ์๋ ์์ง Nginx๊ฐ ์ด์ ์ปจํ ์ด๋๋ฅผ ๋ฐ๋ผ๋ณด๊ณ ์์ด ์๋น์ค๋ ์ ์ ์ด์ ์ค์ ๋๋ค.
โข ํฌ์ค์ฒดํฌ ๋ฐ ์๋ ๋กค๋ฐฑ (์ต๋ 90์ด)
์ ์ปจํ ์ด๋๊ฐ ์ ์์ ์ผ๋ก ๋ฐ ๋๊น์ง 5์ด ๊ฐ๊ฒฉ์ผ๋ก ์ต๋ 18ํ /actuator/health ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํฉ๋๋ค.
๊ฒฐ๊ณผ ๋์
| HTTP 200 ์๋ต | Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ์ ํ → ์ด์ ์ปจํ ์ด๋ ์ข ๋ฃ (์ ์ ๋ฐฐํฌ) |
| 90์ด ์ด๊ณผ ์คํจ | ์ ์ปจํ ์ด๋ ์ฆ์ ์ ๊ฑฐ → ์ด์ ์ปจํ ์ด๋ ์ ์ง (์๋ ๋กค๋ฐฑ) |
์ด์ ์ปจํ ์ด๋๋ฅผ ๋ฏธ๋ฆฌ ์ข ๋ฃํ์ง ์๊ธฐ ๋๋ฌธ์, ๋กค๋ฐฑ ์์๋ ์๋น์ค ์ค๋จ์ด ์์ต๋๋ค.
Docker ๋น๋๋ฅผ ์๊ฒฉ ์๋ฒ์์ ํ๋ ์ด์
์ด๊ธฐ์๋ Jenkins ์๋ฒ์์ Docker ์ด๋ฏธ์ง๋ฅผ ๋น๋ํด .tar๋ก ์ ์ฅํ ๋ค ์๊ฒฉ ์๋ฒ๋ก ์ ์กํ๋ ๋ฐฉ์์ ์ฌ์ฉํ์ต๋๋ค.
[์ด๊ธฐ ๋ฐฉ์]
Jenkins: Gradle ๋น๋ → Docker ๋น๋ → docker save(.tar) → scp → docker load
๊ทธ๋ฐ๋ฐ dev ์๋ฒ์ prod ์๋ฒ๋ Dockerfile ๋ด์ฉ์ด ์๋ก ๋ฌ๋ผ, Jenkins ์๋ฒ ํ ๊ณณ์์ Dockerfile์ ๊ด๋ฆฌํ๋ฉด ํ๊ฒฝ๋ณ ์ฐจ์ด๋ฅผ ์ ์ดํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์, ๊ฐ ์๋ฒ๊ฐ ์์ ์ Dockerfile์ ์ง์ ์ ์งํ๊ณ ๋น๋ํ๋๋ก ๋ฐฉ์์ ๋ณ๊ฒฝํ์ต๋๋ค.
[๊ฐ์ ๋ฐฉ์]
Jenkins: Gradle ๋น๋ → scp(JAR๋ง ์ ์ก) → ์๊ฒฉ ์๋ฒ์์ Docker ๋น๋ ๋ฐ ์คํ (์๋ฒ๋ณ Dockerfile ์ฌ์ฉ)
๐ 5. ์๋ฒ ๋ด๋ถ ๊ตฌ์กฐ ์ ๋ฆฌ
goody-server-001
โโโ Nginx (Host:80)
โ โโโ ๋ฆฌ๋ฒ์ค ํ๋ก์ → app_8080 ๋๋ app_8081
โ
โโโ Docker ๋คํธ์ํฌ (ubuntu_goody-net)
โโโ app_8080 (Spring Boot)
โโโ app_8081 (Spring Boot) ← ๋ฐฐํฌ ์ ์ ๋ฒ์
โโโ redis (๋ด๋ถ ํต์ ์ ์ฉ, ์ธ๋ถ ๋ฏธ๋
ธ์ถ)
โ ๏ธ 6. ํธ๋ฌ๋ธ์ํ
activePort ๊ฐ์ง ์ค๋ฅ
์ฒ์์ ์๋์ฒ๋ผ ์์ฑํ๋ค๊ฐ app_8081์ด ์คํ ์ค์ผ ๋ app_8080๋ ๋งค์นญ๋๋ ๋ฒ๊ทธ๊ฐ ์์์ต๋๋ค.
# ์๋ชป๋ ์ฝ๋ — app_8081 ์คํ ์ค์๋ app_8080์ด ๋งค์นญ๋ ์ ์์
activePort=$(docker ps --format "{{.Names}}" | grep app_8080 || true | cut -d'_' -f2)
# ์์ ๋ ์ฝ๋ — ์ ํํ ํจํด ๋งค์นญ
activePort=$(docker ps --format "{{.Names}}" | grep -oE "app_(8080|8081)" | cut -d'_' -f2)
๐ 7. ๋ง๋ฌด๋ฆฌ
๋จ์ผ ์ธ์คํด์ค๋ผ๋ ๋ ํฌํธ + Nginx ์ฌ๋ณผ๋ฆญ ๋งํฌ ์กฐํฉ์ผ๋ก ์ถฉ๋ถํ ๋ฌด์ค๋จ ๋ฐฐํฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค. ํต์ฌ์ ํฌ์ค์ฒดํฌ ๊ธฐ๋ฐ ์๋ ๋กค๋ฐฑ์ผ๋ก ๋ฐฐํฌ ์คํจ ์ ์์ ํ๊ฒ ์ด์ ๋ฒ์ ์ ์ ์งํ๋ ๊ฒ์ ๋๋ค.
๋ฐฐํฌ ๊ฒฐ๊ณผ — ๋ค์ดํ์ ๋น๊ต


| ๋ฐฐํฌ ๋ฐฉ์ | ๋ค์ดํ์ |
| ๊ธฐ์กด (ํ๋ก์ธ์ค ์ข ๋ฃ → ์ฌ๊ธฐ๋) | ์ฝ 32~45์ด |
| ๊ฐ์ (Blue-Green + Nginx ์ ํ) | ์ฝ 1์ด |
→ ๋จ์ํ ํฌํธ ์ ํ๋ง์ผ๋ก ๋ค์ดํ์ ์ฝ 97% ๋จ์ถ์ ๋ฌ์ฑํ์ต๋๋ค.