Skip to content
Merged
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
139 changes: 83 additions & 56 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,77 +6,104 @@ cd "$APP_DIR"
JAR_PATH=$(ls $APP_DIR/*.jar | grep -v plain | head -1)
echo "> JAR 파일: $JAR_PATH"

echo "> 실행 중인 애플리케이션 종료"
CURRENT_PID=$(pgrep -f '\.jar' || true)
if [ -n "$CURRENT_PID" ]; then
echo "> 종료: $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
else
echo "> 실행 중인 애플리케이션 없음"
fi

echo "> 애플리케이션 시작"
OTEL_AGENT_JAR=$APP_DIR/grafana-opentelemetry-java.jar
OTEL_ENV_FILE=$APP_DIR/otel.env
JAVA_AGENT_OPTS=""
if [ -f "$OTEL_AGENT_JAR" ] && [ -f "$OTEL_ENV_FILE" ]; then
echo "> Grafana OTel agent 감지, 모니터링 활성화"
set -a
source "$OTEL_ENV_FILE"
set +a
JAVA_AGENT_OPTS="-javaagent:$OTEL_AGENT_JAR"
else
echo "> Grafana OTel agent 미설정, 모니터링 없이 실행"
fi

LOG_FILE=/home/ec2-user/app/nohup.out
START_LINE=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)

nohup java $JAVA_AGENT_OPTS -jar \
-Duser.timezone=Asia/Seoul \
$JAR_PATH \
>> "$LOG_FILE" 2>&1 &
APP_PID=$!
echo "> 프로세스 시작됨 (PID: $APP_PID)"

echo "> 앱 준비 신호 대기 중 (최대 240초, 프로세스 생존 여부로 실패 판단)"
MAX_WAIT=240
ELAPSED=0
READY=false

while [ $ELAPSED -lt $MAX_WAIT ]; do
if ! kill -0 $APP_PID 2>/dev/null; then
echo "> 프로세스가 예기치 않게 종료됨 (크래시)"
echo "> 최근 로그:"
# 포트 하나씩 순서대로 종료→재기동한다 (롤링 재시작).
# nginx가 두 포트를 upstream으로 물고 있어서, 한쪽이 재시작되는 동안에도
# 다른 한쪽이 계속 트래픽을 받아 무중단으로 배포된다.
deploy_process() {
local PORT=$1
local USE_OTEL=$2
local LOG_FILE=$APP_DIR/app-$PORT.log

echo "=== [$PORT] 배포 시작 ==="

echo "> [$PORT] 실행 중인 프로세스 종료"
OLD_PID=$(pgrep -f "server.port=$PORT" || true)
if [ -n "$OLD_PID" ]; then
echo "> [$PORT] 종료: $OLD_PID"
kill -15 $OLD_PID
sleep 5
else
echo "> [$PORT] 실행 중인 프로세스 없음"
fi
Comment on lines +23 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fixed 5s sleep after SIGTERM doesn't guarantee the port is free.

After kill -15, the script blindly sleeps 5 seconds with no check that the old process actually exited before starting a new process bound to the same port. If graceful shutdown (e.g., in-flight connection draining) takes longer than 5s, the new java process will fail to bind the port; this is only caught indirectly via the crash-detection loop, causing an avoidable deploy failure for that port.

♻️ Proposed fix — poll for exit with an escalation fallback
   if [ -n "$OLD_PID" ]; then
     echo "> [$PORT] 종료: $OLD_PID"
     kill -15 $OLD_PID
-    sleep 5
+    for i in $(seq 1 10); do
+      kill -0 $OLD_PID 2>/dev/null || break
+      sleep 1
+    done
+    kill -0 $OLD_PID 2>/dev/null && kill -9 $OLD_PID
   else
📝 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
OLD_PID=$(pgrep -f "server.port=$PORT" || true)
if [ -n "$OLD_PID" ]; then
echo "> [$PORT] 종료: $OLD_PID"
kill -15 $OLD_PID
sleep 5
else
echo "> [$PORT] 실행 중인 프로세스 없음"
fi
OLD_PID=$(pgrep -f "server.port=$PORT" || true)
if [ -n "$OLD_PID" ]; then
echo "> [$PORT] 종료: $OLD_PID"
kill -15 $OLD_PID
for i in $(seq 1 10); do
kill -0 $OLD_PID 2>/dev/null || break
sleep 1
done
kill -0 $OLD_PID 2>/dev/null && kill -9 $OLD_PID
else
echo "> [$PORT] 실행 중인 프로세스 없음"
fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 26-26: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy.sh` around lines 23 - 30, Update the shutdown flow around
OLD_PID in scripts/deploy.sh to poll until the identified process exits instead
of relying only on a fixed 5-second sleep. After sending SIGTERM with kill -15,
wait for the process to disappear, and add an escalation fallback such as
SIGKILL when the graceful-shutdown timeout is exceeded, before starting the
replacement process.


JAVA_AGENT_OPTS=""
if [ "$USE_OTEL" = "true" ] && [ -f "$OTEL_AGENT_JAR" ] && [ -f "$OTEL_ENV_FILE" ]; then
echo "> [$PORT] Grafana OTel agent 활성화"
set -a
source "$OTEL_ENV_FILE"
set +a
JAVA_AGENT_OPTS="-javaagent:$OTEL_AGENT_JAR"
else
echo "> [$PORT] Grafana OTel agent 미적용"
fi

START_LINE=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)

nohup java $JAVA_AGENT_OPTS -jar \
-Duser.timezone=Asia/Seoul \
$JAR_PATH \
--server.port=$PORT \
>> "$LOG_FILE" 2>&1 &
APP_PID=$!
echo "> [$PORT] 프로세스 시작됨 (PID: $APP_PID)"
Comment on lines +45 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deploy.sh excerpt =="
cat -n scripts/deploy.sh | sed -n '1,130p'

echo
echo "== available java version =="
if command -v java >/dev/null 2>&1; then
  java -version 2>&1 || true
else
  echo "java not installed"
fi

echo
echo "== Java -jar option semantics probe =="
if command -v java >/dev/null 2>&1; then
  mkdir -p /tmp/java-jar-probe
  echo 'public class App { public static void main(String... args) { for(String a: args) System.out.println(a); } }' > /tmp/java-jar-probe/App.java
  javac /tmp/java-jar-probe/App.java
  java -jar -Duser.timezone=Asia/Seoul /tmp/java-jar-probe/App.jar 2>&1 || true
  java -cp /tmp/java-jar-probe -Duser.timezone=Asia/Seoul App 2>&1 || true
  rm -f /tmp/java-jar-probe/App.java /tmp/java-jar-probe/App.class
fi

Repository: Runnect/Runnect-Spring-Boot-Server

Length of output: 1957


🌐 Web query:

Java -jar option JVM options must precede -jar man page

💡 Result:

When launching a Java application using the java command, options intended for the Java Virtual Machine (JVM)—such as -Xmx or -Dproperty=value—must precede the name of the class or the JAR file [1]. The general syntax for the java command is: java [options] classname [args] or java [options] -jar jarfile [args] Everything provided before the class name or JAR file is treated as an option for the JVM, while everything provided after is treated as an argument for the application's main method [1][2]. Regarding specific tools or legacy commands: 1. javaws (Java Web Start): Documentation has explicitly stated that all run-options must precede the JNLP file argument [3]. 2. javadoc: Certain options, such as -locale, must be placed before other options to function correctly [4]. 3. jcmd: Options and arguments for this diagnostic tool have specific syntax requirements, often requiring key or key=value syntax [5]. For most standard Java invocations, the requirement that JVM options precede the application entry point (class or JAR) is a fundamental rule of the Java launcher's command-line parsing [1].

Citations:


🌐 Web query:

Oracle Java man java "If a JAR file is being used, the options must precede the -jar"

💡 Result:

Yes, when using the -jar option with the java command, all command-line options intended for the Java Virtual Machine (JVM)—such as system properties defined with -D—must precede the -jar flag [1][2]. The standard syntax for the java command is [3][4][5]: java [options] -jar jarfile [args...] In this structure: 1. [options]: These are JVM options (e.g., -Dproperty=value, -Xmx, etc.) that must be placed before the -jar argument to be recognized as configuration for the JVM [1][2]. 2. -jar jarfile: This specifies the JAR file to execute. When this flag is used, the JAR file becomes the source of all user classes, and other classpath settings are ignored [3][4][5]. 3. [args...]: Any arguments placed after the JAR file name are treated as application arguments and are passed directly to the main method of the application's entry point class, not to the JVM [3][4][5]. If JVM options (like -D) are placed after the -jar flag, the JVM will typically interpret them as application arguments rather than system settings, which often causes the application to fail to receive the expected configuration [1][2].

Citations:


Critical: Move -Duser.timezone before -jar.

JVM options must precede -jar; with the current order, -jar consumes -Duser.timezone=Asia/Seoul as the jar file name, java fails to launch, and $JAR_PATH becomes an application argument.

Proposed fix
   nohup java $JAVA_AGENT_OPTS -jar \
-    -Duser.timezone=Asia/Seoul \
-    $JAR_PATH \
+    -Duser.timezone=Asia/Seoul \
+    -jar $JAR_PATH \
     --server.port=$PORT \
     >> "$LOG_FILE" 2>&1 &
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 47-47: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 48-48: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy.sh` around lines 45 - 51, Reorder the Java command in the
deployment startup block so -Duser.timezone=Asia/Seoul appears before -jar,
while keeping $JAR_PATH and the existing application arguments unchanged.


local MAX_WAIT=240
local ELAPSED=0
local READY=false

while [ $ELAPSED -lt $MAX_WAIT ]; do
if ! kill -0 $APP_PID 2>/dev/null; then
echo "> [$PORT] 프로세스가 예기치 않게 종료됨 (크래시)"
echo "> [$PORT] 최근 로그:"
tail -n 40 "$LOG_FILE"
return 1
fi

if tail -n +"$((START_LINE + 1))" "$LOG_FILE" | grep -q "Started ServerApplication"; then
echo "> [$PORT] 준비 완료 신호 감지 (${ELAPSED}초 소요)"
READY=true
break
fi

sleep 2
ELAPSED=$((ELAPSED + 2))
done

if [ "$READY" != "true" ]; then
echo "> [$PORT] ${MAX_WAIT}초 내에 준비 신호 없음 — 안전장치 발동"
echo "> [$PORT] 최근 로그:"
tail -n 40 "$LOG_FILE"
echo "> 배포 실패"
exit 1
return 1
fi

if tail -n +"$((START_LINE + 1))" "$LOG_FILE" | grep -q "Started ServerApplication"; then
echo "> 앱 준비 완료 신호 감지 (${ELAPSED}초 소요)"
READY=true
break
RESPONSE=$(curl -s --max-time 10 http://localhost:$PORT/actuator/health || true)
echo "> [$PORT] 헬스체크 확인: $RESPONSE"

if ! echo "$RESPONSE" | grep -q '"status":"UP"'; then
echo "> [$PORT] 헬스체크 실패"
return 1
fi

sleep 2
ELAPSED=$((ELAPSED + 2))
done
return 0
}

if [ "$READY" != "true" ]; then
echo "> ${MAX_WAIT}초 내에 준비 신호 없음 — 안전장치 발동"
echo "> 최근 로그:"
tail -n 40 "$LOG_FILE"
if ! deploy_process 8080 true; then
echo "> 8080 배포 실패 — 8081은 계속 서비스 중이므로 전체 다운은 아니지만 배포는 실패 처리"
echo "> 배포 실패"
exit 1
fi

RESPONSE=$(curl -s --max-time 10 http://localhost:8080/actuator/health || true)
echo "> 헬스체크 확인: $RESPONSE"
if ! deploy_process 8081 false; then
echo "> 8081 배포 실패 — 8080은 정상 서비스 중이므로 전체 다운은 아니지만 배포는 실패 처리"
echo "> 배포 실패"
exit 1
fi

echo "> Nginx 시작"
sudo systemctl start nginx || true
sudo systemctl enable nginx || true

echo "> 배포 완료"
echo "> 배포 완료 (8080, 8081 모두 정상)"
Loading