Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/workflows/Dev_CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- "develop"
- "chore/#224"

jobs:
build:
Expand Down Expand Up @@ -59,19 +60,25 @@ jobs:
uses: actions/download-artifact@v4
with:
name: app-artifact
path: ~/app
path: ~/app/staging

- name: Download deploy scripts
uses: actions/download-artifact@v4
with:
name: deploy-scripts
path: ~/app/scripts/

- name: Replace application to latest
run: sudo sh ~/app/scripts/replace-new-version.sh
- name: Setup log directory
run: |
sudo mkdir -p /home/ubuntu/logs
sudo chown -R ubuntu:ubuntu /home/ubuntu/logs
chmod 755 /home/ubuntu/logs

- name: Make deploy script executable
run: chmod +x ~/app/scripts/zero-downtime-deploy.sh

- name: Health Check
run: sh ~/app/scripts/health-check.sh
- name: Zero Downtime Deployment
run: sh ~/app/scripts/zero-downtime-deploy.sh

- name: Send Discord Alert on Failure
if: failure()
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/Prod_CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,25 @@ jobs:
uses: actions/download-artifact@v4
with:
name: app-artifact
path: ~/app
path: ~/app/staging

- name: Download deploy scripts
uses: actions/download-artifact@v4
with:
name: deploy-scripts
path: ~/app/scripts/

- name: Replace application to latest
run: sudo sh ~/app/scripts/replace-new-version.sh
- name: Setup log directory
run: |
sudo mkdir -p /home/ubuntu/logs
sudo chown -R ubuntu:ubuntu /home/ubuntu/logs
chmod 755 /home/ubuntu/logs

- name: Make deploy script executable
run: chmod +x ~/app/scripts/zero-downtime-deploy.sh

- name: Health Check
run: sh ~/app/scripts/health-check.sh
- name: Zero Downtime Deployment
run: sh ~/app/scripts/zero-downtime-deploy.sh

- name: Send Discord Alert on Failure
if: failure()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ out/

### application-local.yml
/src/main/resources/application-local.yml
.serena
34 changes: 34 additions & 0 deletions nginx/api.dev.debate-timer.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
upstream debate_timer_backend {
server 127.0.0.1:8080;
keepalive 32;
}

server {
server_name api.dev.debate-timer.com;

location / {
proxy_pass http://debate_timer_backend;
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;
}

listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/api.dev.debate-timer.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/api.dev.debate-timer.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
if ($host = api.dev.debate-timer.com) {
return 308 https://$host$request_uri;
} # managed by Certbot

listen 80;
listen [::]:80;
server_name api.dev.debate-timer.com;
return 404; # managed by Certbot
}
34 changes: 34 additions & 0 deletions nginx/api.prod.debate-timer.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
upstream debate_timer_backend {
server 127.0.0.1:8080;
keepalive 32;
}

server {
server_name api.prod.debate-timer.com;

location / {
proxy_pass http://debate_timer_backend;
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;
}

listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/api.prod.debate-timer.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/api.prod.debate-timer.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
if ($host = api.prod.debate-timer.com) {
return 308 https://$host$request_uri;
} # managed by Certbot

listen 80;
listen [::]:80;
server_name api.prod.debate-timer.com;
return 404; # managed by Certbot
}
231 changes: 231 additions & 0 deletions scripts/dev/zero-downtime-deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#!/bin/bash

set -e

APP_DIR="/home/ubuntu/app"
PORT_FILE="$APP_DIR/current_port.txt"
LOG_FILE="$APP_DIR/deploy.log"
BLUE_PORT=8080
GREEN_PORT=8081
BLUE_MONITOR_PORT=8083
GREEN_MONITOR_PORT=8084
MAX_HEALTH_CHECK_RETRIES=60
HEALTH_CHECK_INTERVAL=2
PROFILE="dev"
TIMEZONE="Asia/Seoul"

log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "${timestamp} $@" | tee -a "$LOG_FILE"
}
Comment on lines +17 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Shellcheck 경고를 수정해주세요.

로그 함수에 SC2155와 SC2145 경고가 있습니다.

다음과 같이 수정하세요:

 log() {
-    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
-    echo "${timestamp} $@" | tee -a "$LOG_FILE"
+    local timestamp
+    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
+    echo "${timestamp} $*" | tee -a "$LOG_FILE"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "${timestamp} $@" | tee -a "$LOG_FILE"
}
log() {
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "${timestamp} $*" | tee -a "$LOG_FILE"
}
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 18-18: Declare and assign separately to avoid masking return values.

(SC2155)


[error] 19-19: Argument mixes string and array. Use * or separate argument.

(SC2145)

🤖 Prompt for AI Agents
In scripts/dev/zero-downtime-deploy.sh around lines 17 to 20, fix ShellCheck
SC2155 and SC2145 by avoiding combined declaration+assignment and by handling
positional parameters safely: declare the timestamp variable first (local
timestamp), assign it on the next line (timestamp=$(date '+%Y-%m-%d %H:%M:%S')),
and use a robust print that preserves arguments (e.g., printf '%s %s\n'
"$timestamp" "$*" or similar) piped to tee -a "$LOG_FILE" so you don't trigger
the warnings.


error_exit() {
log "$1"
exit 1
}

get_current_port() {
if [ ! -f "$PORT_FILE" ]; then
log "Port file not found. Initializing with default port $BLUE_PORT"
echo "$BLUE_PORT" > "$PORT_FILE"
echo "$BLUE_PORT"
else
cat "$PORT_FILE"
fi
}

get_inactive_port() {
local current_port=$1
if [ "$current_port" -eq "$BLUE_PORT" ]; then
echo "$GREEN_PORT"
else
echo "$BLUE_PORT"
fi
}

get_monitor_port() {
local app_port=$1
if [ "$app_port" -eq "$BLUE_PORT" ]; then
echo "$BLUE_MONITOR_PORT"
else
echo "$GREEN_MONITOR_PORT"
fi
}

is_port_in_use() {
local port=$1
lsof -t -i:$port > /dev/null 2>&1
return $?
}

kill_process_on_port() {
local port=$1
local pid=$(lsof -t -i:$port 2>/dev/null)

if [ -z "$pid" ]; then
log "No process running on port $port"
return 0
fi

log "Sending graceful shutdown signal to process $pid on port $port"
kill -15 "$pid"

local wait_count=0
while [ $wait_count -lt 35 ] && is_port_in_use "$port"; do
sleep 1
wait_count=$((wait_count + 1))
done

if is_port_in_use "$port"; then
log "Process didn't stop gracefully, forcing shutdown"
kill -9 "$pid" 2>/dev/null || true
sleep 2
fi

log "Process on port $port stopped successfully"
}

health_check() {
local port=$1
local monitor_port=$2
local health_url="http://localhost:$monitor_port/monitoring/health"

log "Starting health check for port $port (monitor: $monitor_port)"

local retry=1
while [ $retry -le $MAX_HEALTH_CHECK_RETRIES ]; do
local status=$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null || echo "000")

log "Health check attempt $retry/$MAX_HEALTH_CHECK_RETRIES - Status: $status"

if [ "$status" = "200" ]; then
log "Health check passed!"
return 0
fi

sleep $HEALTH_CHECK_INTERVAL
retry=$((retry + 1))
done

log "Health check failed after $MAX_HEALTH_CHECK_RETRIES attempts"
return 1
}

start_application() {
local port=$1
local monitor_port=$2
local staging_jar="$APP_DIR/staging/app.jar"
local jar_file="$APP_DIR/app-$port.jar"

if [ ! -f "$staging_jar" ]; then
error_exit "No JAR file found in staging directory: $staging_jar"
fi

log "Copying JAR from staging to $jar_file"
cp "$staging_jar" "$jar_file"

log "Starting application on port $port with JAR: $jar_file"

if is_port_in_use "$port"; then
log "Port $port is in use, cleaning up..."
kill_process_on_port "$port"
fi

nohup java \
-Dspring.profiles.active=$PROFILE,monitor \
-Duser.timezone=$TIMEZONE \
-Dserver.port=$port \
-Dmanagement.server.port=$monitor_port \
-Ddd.service=debate-timer \
-Ddd.env=$PROFILE \
-jar "$jar_file" > "$APP_DIR/app-$port.log" 2>&1 &

local pid=$!
log "Application started with PID: $pid"

sleep 3

if ! kill -0 $pid 2>/dev/null; then
error_exit "Application process died immediately after start. Check logs at $APP_DIR/app-$port.log"
fi
}

switch_nginx_upstream() {
local new_port=$1
local nginx_conf="/etc/nginx/sites-available/api.dev.debate-timer.com"
local temp_conf="/tmp/api.dev.debate-timer.com.tmp"

if [ ! -f "$nginx_conf" ]; then
error_exit "nginx configuration not found at $nginx_conf"
fi

log "Switching nginx upstream to port $new_port"

sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf"

sudo cp "$temp_conf" "$nginx_conf"
if ! sudo nginx -t 2>/dev/null; then
log "nginx configuration test failed"
git checkout "$nginx_conf" 2>/dev/null || true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

git checkout을 롤백 메커니즘으로 사용하지 마세요.

nginx 설정 롤백에 git checkout을 사용하는 것은 부적절합니다:

  1. 배포 환경에 git 저장소가 없을 수 있습니다
  2. 백업 파일이 이미 Line 33에서 생성되었습니다

백업 파일을 사용하도록 수정하세요:

     if ! sudo nginx -t 2>/dev/null; then
         log "nginx configuration test failed"
-        git checkout "$nginx_conf" 2>/dev/null || true
+        sudo cp "$BACKUP_CONF" "$nginx_conf" 2>/dev/null || true
         return 1
     fi

그리고 스크립트 상단에 BACKUP_CONF 변수를 추가하세요:

 switch_nginx_upstream() {
     local new_port=$1
     local nginx_conf="/etc/nginx/sites-available/api.dev.debate-timer.com"
+    local backup_conf="/etc/nginx/sites-available/api.dev.debate-timer.com.backup"
     local temp_conf="/tmp/api.dev.debate-timer.com.tmp"

그리고 백업 생성 단계를 추가하세요:

     log "Switching nginx upstream to port $new_port"

+    sudo cp "$nginx_conf" "$backup_conf"
     sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf"

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In scripts/dev/zero-downtime-deploy.sh around line 169, don't use "git checkout"
as a rollback; instead use the backup file created earlier. Replace the git
checkout line with logic that restores $BACKUP_CONF to $nginx_conf (ensure
$BACKUP_CONF is defined at the top of the script), add a step near the backup
creation (around line 33) that sets BACKUP_CONF and creates the backup file
(e.g., cp "$nginx_conf" "$BACKUP_CONF" with a timestamp/unique name), and update
the rollback path to check that the backup file exists and restore from it
rather than calling git. Ensure any error handling/logging reflects backup
restore failures.

return 1
fi

sudo nginx -s reload
log "nginx reloaded successfully"

sleep 2
local response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null || echo "000")
if [ "$response" = "000" ] || [ "$response" = "502" ] || [ "$response" = "503" ]; then
log "nginx health check failed after reload (status: $response)"
return 1
fi

log "nginx is now routing traffic to port $new_port"
return 0
}

main() {
local current_port=$(get_current_port)
local new_port=$(get_inactive_port "$current_port")
local new_monitor_port=$(get_monitor_port "$new_port")

log "Current active port: $current_port"
log "Deploying to port: $new_port"
log "Monitor port: $new_monitor_port"

log "Step 1/4: Starting new version on port $new_port"
start_application "$new_port" "$new_monitor_port"

log "Step 2/4: Performing health check"
if ! health_check "$new_port" "$new_monitor_port"; then
log "Deployment failed: Health check did not pass"
log "Rolling back: Stopping new version on port $new_port"
kill_process_on_port "$new_port"
error_exit "Deployment aborted due to health check failure"
fi

log "Step 3/4: Switching nginx to new version"
if ! switch_nginx_upstream "$new_port"; then
log "nginx switch failed, rolling back"
kill_process_on_port "$new_port"
error_exit "Deployment aborted due to nginx switch failure"
fi

log "Step 4/4: Stopping old version on port $current_port"
kill_process_on_port "$current_port"

local old_jar="$APP_DIR/app-$current_port.jar"
if [ -f "$old_jar" ]; then
log "Removing old JAR file: $old_jar"
rm -f "$old_jar"
fi

echo "$new_port" > "$PORT_FILE"
log "Updated active port file to $new_port"

log "Deployment completed successfully!"
log "Active port: $new_port"
log "Inactive port: $current_port"
}

main "$@"
Loading