Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ out/
### VS Code ###
.vscode/

### deploy
/letsencrypt/acme.json

### Rest Docs
/src/main/resources/static/docs/openapi3.yaml

Expand Down
54 changes: 54 additions & 0 deletions docker/application/docker-compose.application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
services:
application:
image: debatetimer/debate_timer:${ENV:-dev}
container_name: application
environment:
- SERVER_FORWARD_HEADERS_STRATEGY=framework
- SPRING_PROFILES_ACTIVE=${ENV:-dev},monitor
- TZ=Asia/Seoul

- DD_AGENT_HOST=datadog-agent
- DD_SERVICE=debate-timer
- DD_ENV=${ENV:-dev}
- DD_VERSION=${APP_VERSION:-1.0.0}
- DD_LOGS_INJECTION=true
- DD_PROFILING_ENABLED=true
- DD_PROFILING_ALLOCATION_ENABLED=true
- DD_PROFILING_HEAP_ENABLED=true

- MANAGEMENT_STATSD_METRICS_EXPORT_ENABLED=true
- MANAGEMENT_STATSD_METRICS_EXPORT_FLAVOR=datadog
- MANAGEMENT_STATSD_METRICS_EXPORT_HOST=datadog-agent
- MANAGEMENT_STATSD_METRICS_EXPORT_PORT=8125
- MANAGEMENT_STATSD_METRICS_EXPORT_PROTOCOL=UDP

networks:
- debate-timer-net

depends_on:
traefik:
condition: service_healthy

healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8083/monitoring/health" ]
interval: 60s
retries: 10
start_period: 450s
Comment on lines +35 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

[사소한 의견]
오... 거의 7분 30초동안은 실패 카운트 안하는 것 같은데 혹시 기준점이 따로 있엇나요?

10분 동안 애플리케이션 healthcheck 시도할 수도 있을 것 같아서 period와 retries 횟수 조금 줄이는 건 어떨까... 하는 사견입니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

메모리 1GB, 스왑 메모리 2GB 기준 평균 Spring Boot 로딩 시간이 5분입니다. (H2 내장 DB 사용 기준)
그래서 일단 7.5분으로 진행하고, 나중에 DEV, PROD 부팅 평균 시간을 측정해서 개선해 나가는게 좋을 것 같아요!


labels:
- "traefik.enable=true"
- "traefik.http.routers.application.rule=Host(`api.${ENV:-dev}.debate-timer.com`)"
- "traefik.http.routers.application.entrypoints=websecure"
- "traefik.http.routers.application.tls=true"
- "traefik.http.routers.application.tls.certresolver=myresolver"
- "traefik.http.routers.application.service=application"
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 뭔가 했는데 Nginx에서 편집해줬던 443 라우팅 정책이네요 ㄷㄷ traefik이 미래다..

- "traefik.http.services.application.loadbalancer.server.port=8080"

- "traefik.http.routers.application-monitor.rule=PathPrefix(`/`)"
- "traefik.http.routers.application-monitor.entrypoints=monitoring"
- "traefik.http.routers.application-monitor.service=application-monitor-svc"
- "traefik.http.services.application-monitor-svc.loadbalancer.server.port=8083"

- "com.datadoghq.ad.logs=[{\"source\": \"java\", \"service\": \"debate-timer\"}]"

restart: unless-stopped
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
environment:
- DD_API_KEY=${DD_API_KEY}
- DD_SITE=us5.datadoghq.com
- DD_HOSTNAME=debate-timer-${ENV:-dev}-server
- DD_APM_ENABLED=true
- DD_APM_NON_LOCAL_TRAFFIC=true
- DD_APM_IGNORE_RESOURCES=/monitoring/health
Expand All @@ -25,6 +26,6 @@ services:
healthcheck:
test: ["CMD", "agent", "health"]
interval: 60s
timeout: 10s
timeout: 30s
retries: 5
start_period: 120s
75 changes: 0 additions & 75 deletions docker/docker-compose.spring.yml

This file was deleted.

7 changes: 4 additions & 3 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: debate-timer-server

include:
- docker-compose.traefik.yml
- docker-compose.spring.yml
- docker-compose.datadog.yml
- ./application/docker-compose.application.yml
- ./datadog/docker-compose.datadog.yml
- ./traefik/docker-compose.traefik.yml
- ./webhook/docker-compose.webhook.yml

networks:
debate-timer-net:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ services:
start_period: 120s
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ../letsencrypt:/letsencrypt
- ../../letsencrypt:/letsencrypt
networks:
- debate-timer-net
35 changes: 35 additions & 0 deletions docker/webhook/docker-compose.webhook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
webhook:
build:
context: ..
dockerfile: ./webhook/webhook.Dockerfile
container_name: webhook
user: root

Choose a reason for hiding this comment

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

security-high high

webhook 컨테이너를 root 사용자로 실행하는 것은 보안상 위험합니다. 컨테이너가 탈취될 경우 호스트 시스템에 대한 root 권한을 갖게 될 수 있습니다. Docker 소켓에 접근해야 하더라도, root가 아닌 별도의 사용자를 생성하고 docker 그룹에 추가하여 최소한의 권한만 부여하는 것이 안전합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

후순위긴 하지만 이거 나중에 시도해봐도 좋을 듯 👍

command: ["-verbose", "-hooks=/etc/webhook/hooks.json", "-hotreload"]
environment:
- ENV=${ENV:-dev}
- SECRET_TOKEN=${WEBHOOK_KEY}
labels:
- "traefik.enable=true"
- "traefik.http.routers.webhook.rule=Host(`webhook.${ENV:-dev}.debate-timer.com`)"
- "traefik.http.routers.webhook.entrypoints=websecure"
- "traefik.http.routers.webhook.tls=true"
- "traefik.http.routers.webhook.tls.certresolver=myresolver"
- "traefik.http.services.webhook.loadbalancer.server.port=9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/ubuntu/.docker:/root/.docker:ro

Choose a reason for hiding this comment

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

security-high high

호스트의 .docker 디렉토리를 컨테이너에 마운트하면 private registry의 인증 정보가 노출될 수 있어 보안상 위험합니다. read-only로 마운트하더라도 컨테이너 내부에서 인증 정보를 읽을 수 있습니다. docker login을 CI/CD 파이프라인에서 수행하거나, DOCKER_AUTH_CONFIG 환경 변수를 통해 인증 정보를 안전하게 전달하는 방법을 고려해 보세요.

- ./hooks.json:/etc/webhook/hooks.json
- ../..:/home/ubuntu/debate-timer
networks:
- debate-timer-net
depends_on:
traefik:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
interval: 120s
timeout: 30s
retries: 5
start_period: 30s
restart: always
38 changes: 38 additions & 0 deletions docker/webhook/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"id": "deploy",
"http-methods": ["POST"],
"execute-command": "/home/ubuntu/debate-timer/scripts/deploy/deploy-docker.sh",
"command-working-directory": "/home/ubuntu/debate-timer",
"pass-environment-to-command": [
{
"source": "header",
"name": "X-Deploy-Token",
"envname": "REQUEST_TOKEN"
}
],
"pass-arguments-to-command": [
{ "source": "entire-payload" }
]
},
{
"id": "status",
"http-methods": ["GET"],
"execute-command": "/home/ubuntu/debate-timer/scripts/deploy/check-deploy-status.sh",
"command-working-directory": "/home/ubuntu/debate-timer",
"include-command-output-in-response": true,
"response-headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"pass-environment-to-command": [
{
"source": "header",
"name": "X-Deploy-Token",
"envname": "REQUEST_TOKEN"
}
]
}
]
7 changes: 7 additions & 0 deletions docker/webhook/webhook.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM almir/webhook:2.8.3
USER root
RUN apk add --no-cache docker-cli-compose curl bash util-linux git

WORKDIR /etc/webhook
Comment on lines +2 to +5
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify USER declarations, docker.sock mounts, and socket access configuration
rg -n "^\s*USER\s+|^\s*user:\s*|/var/run/docker.sock|group_add" docker/webhook/webhook.Dockerfile docker/webhook/docker-compose.webhook.yml

Repository: debate-timer/debate-timer-be

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Read the full Dockerfile to understand the complete context
cat -n docker/webhook/webhook.Dockerfile

Repository: debate-timer/debate-timer-be

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check if docker-compose file exists and review its contents
if [ -f docker/webhook/docker-compose.webhook.yml ]; then
  cat -n docker/webhook/docker-compose.webhook.yml
else
  # Try other common naming patterns
  find docker/webhook -name "*compose*" -type f 2>/dev/null | head -10
fi

Repository: debate-timer/debate-timer-be

Length of output: 1516


🏁 Script executed:

#!/bin/bash
# Check for any entrypoint or command specifications
rg -n "ENTRYPOINT|CMD|entrypoint|command:" docker/webhook/ -A 2

Repository: debate-timer/debate-timer-be

Length of output: 871


웹훅 프로세스가 루트로 실행되는 문제를 모두 해결해야 합니다.

Dockerfile의 USER root (Line 2)뿐만 아니라 docker-compose.yml의 user: root (Line 7)도 제거해야 합니다. Dockerfile에서만 사용자를 변경해도 Compose 파일의 user 설정이 이를 무시합니다.

웹훅이 docker.sock에 접근해야 하므로, 비루트 사용자를 docker 그룹에 추가하는 방식으로 권한을 분리하세요:

권장 수정

docker/webhook/webhook.Dockerfile:

 FROM almir/webhook:2.8.3
 USER root
 RUN apk add --no-cache docker-cli docker-cli-compose curl bash util-linux
+RUN addgroup -S webhook && adduser -S -G webhook webhook && addgroup webhook docker
 WORKDIR /etc/webhook
+USER webhook

docker/webhook/docker-compose.webhook.yml:

 webhook:
     build:
       context: ..
       dockerfile: ./webhook/webhook.Dockerfile
     container_name: webhook
-    user: root
     command: ["-verbose", "-hooks=/etc/webhook/hooks.json", "-hotreload"]
📝 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
USER root
RUN apk add --no-cache docker-cli docker-cli-compose curl bash util-linux
WORKDIR /etc/webhook
FROM almir/webhook:2.8.3
USER root
RUN apk add --no-cache docker-cli docker-cli-compose curl bash util-linux
RUN addgroup -S webhook && adduser -S -G webhook webhook && addgroup webhook docker
WORKDIR /etc/webhook
USER webhook
🧰 Tools
🪛 Trivy (0.69.1)

[error] 2-2: Image user should not be 'root'

Last USER command in Dockerfile should not be 'root'

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/webhook/webhook.Dockerfile` around lines 2 - 4, Remove the hardcoded
root execution: in webhook.Dockerfile stop using USER root and instead create a
non-root user (e.g., webhook), create or match a docker group (ensure group id
can be overridden at runtime to match host docker.sock gid), add the webhook
user to that docker group, chown /etc/webhook and any runtime-required dirs to
the webhook user, and keep package installation steps (RUN apk add ...) as root
during image build before switching to the non-root USER; also remove the user:
root setting from docker-compose.webhook.yml and instead rely on the container's
USER and mounting the docker socket with the host docker group GID (or pass GID
via environment/compose to set group at runtime). Ensure the image switches to
USER webhook at the end so the webhook process runs non-root but has access to
docker.sock via group membership.


RUN git config --global --add safe.directory /home/ubuntu/debate-timer
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: safe.directory 설정 경로와 실제 마운트 경로 정합성 점검(읽기 전용)
set -e

echo "[1] safe.directory 관련 설정 검색"
rg -n -C2 'safe\.directory|/home/ubuntu/debate-timer|GIT_SAFE_DIRECTORY' docker

echo
echo "[2] webhook compose의 볼륨/경로 설정 검색"
rg -n -C3 'webhook|volumes:|/home/ubuntu/debate-timer|hooks\.json' docker

Repository: debate-timer/debate-timer-be

Length of output: 6613


🏁 Script executed:

#!/bin/bash
# Check full Dockerfile content
echo "[1] Full webhook.Dockerfile"
cat -n docker/webhook/webhook.Dockerfile

echo
echo "[2] Full hooks.json content"
cat -n docker/webhook/hooks.json

echo
echo "[3] Full docker-compose.webhook.yml"
cat -n docker/webhook/docker-compose.webhook.yml

Repository: debate-timer/debate-timer-be

Length of output: 3040


배포 이식성을 깨뜨리는 하드코딩된 경로는 safe.directory 뿐만 아니라 hooks.json까지 광범위합니다.

Line 7의 /home/ubuntu/debate-timer 고정 경로가 safe.directory에만 있는 것이 아니라, hooks.json의 실행 명령과 작업 디렉토리(5, 6, 21, 22줄)에도 동일하게 하드코딩되어 있습니다. 서버 계정이나 디렉토리 구조가 바뀔 경우 Git 명령뿐만 아니라 배포 스크립트 실행 자체가 실패합니다. Dockerfile의 경로는 ARG로 주입하고 --system 범위로 설정하되, hooks.json도 동시에 템플릿화하거나 환경 변수로 매개변수화해야 합니다.

권장 수정안
 FROM almir/webhook:2.8.3
 USER root
 RUN apk add --no-cache docker-cli-compose curl bash util-linux git
+ARG GIT_SAFE_DIRECTORY=/home/ubuntu/debate-timer
 
 WORKDIR /etc/webhook
 
-RUN git config --global --add safe.directory /home/ubuntu/debate-timer
+RUN git config --system --add safe.directory "${GIT_SAFE_DIRECTORY}"

(추가로 hooks.json/home/ubuntu/debate-timer 참조도 환경 변수 치환이나 동적 생성으로 매개변수화 권장)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/webhook/webhook.Dockerfile` at line 7, Hardcoded deployment paths (the
RUN git config --global --add safe.directory /home/ubuntu/debate-timer and the
/home/ubuntu/debate-timer references inside hooks.json) break portability;
change the Dockerfile to accept a build ARG (e.g., APP_DIR) and use it when
setting git safe.directory (prefer --system scope if appropriate), and update
the Docker build step to template or generate hooks.json at build/run time using
that same ARG/ENV so all references to /home/ubuntu/debate-timer are
parameterized; ensure unique identifiers mentioned: the RUN git config command,
the safe.directory value, and the hooks.json file are updated to use the
injected ARG/ENV instead of a hardcoded path.

19 changes: 19 additions & 0 deletions scripts/deploy/check-deploy-status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

if [ -z "$SECRET_TOKEN" ] || [ "$REQUEST_TOKEN" != "$SECRET_TOKEN" ]; then
echo '{"status": "error", "message": "Unauthorized request. Invalid Token."}'
exit 0
fi

LOCK_FILE="/tmp/deploy-application.lock"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
echo '{"status": "running", "message": "배포 스크립트가 현재 실행 중입니다."}'
else
flock -u 200
echo '{"status": "idle", "message": "현재 진행 중인 배포가 없습니다. 대기 중입니다."}'
fi

exec 200>&-
82 changes: 82 additions & 0 deletions scripts/deploy/deploy-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash

if [ -z "$SECRET_TOKEN" ] || [ "$REQUEST_TOKEN" != "$SECRET_TOKEN" ]; then
echo "[$(date)] 🚨 보안 경고: 유효하지 않은 배포 요청입니다. (Unauthorized)"
exit 1
fi

echo "스크립트 실행 여부를 확인합니다."
LOCK_FILE="/tmp/deploy-application.lock"
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "⚠️ 이미 배포 스크립트가 실행 중입니다. 중복 실행을 차단합니다."; exit 1; }
echo "스크립트를 실행하고 있지 않습니다. 배포를 시작합니다."

TARGET_ENV=${ENV:-dev}
PROJECT_DIR="/home/ubuntu/debate-timer"
DOCKER_DIR="/home/ubuntu/debate-timer/docker"
TARGET_SERVICE="application"

echo "--- Git 저장소 최신화 시작 ---"
cd $PROJECT_DIR || { echo "프로젝트 디렉토리 이동 실패"; exit 1; }

if [ "$TARGET_ENV" = "prod" ]; then
echo "▶ [ENV: prod] main 브랜치로 이동하여 최신 코드를 가져옵니다."
git fetch origin main || { echo "❌ git fetch(main) 실패"; exit 1; }
git switch main || { echo "❌ git switch(main) 실패"; exit 1; }
git reset --hard origin/main || { echo "❌ git reset(main) 실패"; exit 1; }
elif [ "$TARGET_ENV" = "dev" ]; then
echo "▶ [ENV: dev] develop 브랜치로 이동하여 최신 코드를 가져옵니다."
git fetch origin develop || { echo "❌ git fetch(develop) 실패"; exit 1; }
git switch develop || { echo "❌ git switch(develop) 실패"; exit 1; }
git reset --hard origin/develop || { echo "❌ git reset(develop) 실패"; exit 1; }
else
CURRENT_BRANCH=$(git branch --show-current)
if [ -z "$CURRENT_BRANCH" ]; then
echo "❌ 현재 브랜치를 확인할 수 없습니다 (detached HEAD 상태일 수 있음)"
exit 1
fi
echo "▶ [ENV: $TARGET_ENV] 현재 브랜치($CURRENT_BRANCH)에서 최신 코드를 가져옵니다."
git fetch origin "$CURRENT_BRANCH" || { echo "❌ git fetch($CURRENT_BRANCH) 실패"; exit 1; }
git reset --hard origin/"$CURRENT_BRANCH" || { echo "❌ git reset($CURRENT_BRANCH) 실패"; exit 1; }
fi

cd $DOCKER_DIR || { echo "디렉토리 이동 실패"; exit 1; }
echo "--- 중단 배포 시작 ENV: $TARGET_ENV at $(date) ---"
export ENV=$TARGET_ENV

echo "최근 이미지 가져오는 중 (ENV: $TARGET_ENV)"
docker compose pull $TARGET_SERVICE || { echo "❌ 이미지 풀 실패"; exit 1; }
if docker ps --format '{{.Names}}' | grep -q "^${TARGET_SERVICE}$"; then
echo "기존 $TARGET_SERVICE 중지 및 제거 중..."
docker compose stop $TARGET_SERVICE
fi

echo "새 컨테이너 실행 중 - $TARGET_SERVICE..."
docker compose up -d --no-deps $TARGET_SERVICE || { echo "❌ 컨테이너 실행 실패"; exit 1; }

echo "헬스 체크 진행 중 - $TARGET_SERVICE"
MAX_RETRIES=50
SLEEP_SECOND=10
COUNT=0

while [ $COUNT -lt $MAX_RETRIES ]; do
HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$TARGET_SERVICE" 2>/dev/null)
if [ "$HEALTH_STATUS" = "healthy" ]; then
echo "헬스 체크 완료 - $TARGET_SERVICE"
break
fi

echo "헬스체크 진행 중 ($COUNT/$MAX_RETRIES) - 현재 상태: $HEALTH_STATUS"
sleep $SLEEP_SECOND
COUNT=$((COUNT + 1))
done

if [ $COUNT -eq $MAX_RETRIES ]; then
echo "배포 실패: $TARGET_SERVICE가 healthy 상태가 되지 않았습니다."
docker logs --tail 50 $TARGET_SERVICE
exit 1
fi

docker image prune -f

echo "--- $TARGET_ENV 배포 완료 at $(date) ---"
Empty file modified scripts/init/auto-swap.sh
100644 → 100755
Empty file.
Empty file modified scripts/init/init-docker.sh
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion scripts/init/init-letsencrypt.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ mkdir -p ../../letsencrypt
touch ../../letsencrypt/acme.json
chmod 600 ../../letsencrypt/acme.json

echo "✅ 스크립트 위치($(pwd))에 letsencrypt 폴더를 생성했습니다."
echo "letsencrypt 폴더를 생성했습니다."