Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Binary file added .github/.DS_Store
Binary file not shown.
83 changes: 83 additions & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: CD

on:
workflow_run:
workflows: ['CI']
types: [completed]

jobs:
# ============================================
# 서버 배포
# ============================================
deploy:
name: Deploy to Wisoft Server
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }}

steps:
- name: SSH 키 설정
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.RASPI_SSH_PRIVATE_KEY }}

- name: Known Hosts 등록
env:
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
JUMP_HOST: ${{ secrets.RASPI_HOST }}
run: |
mkdir -p ~/.ssh
ssh-keyscan -p $JUMP_PORT -H $JUMP_HOST >> ~/.ssh/known_hosts

- name: 서버에 배포
env:
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
JUMP_HOST: ${{ secrets.RASPI_HOST }}
JUMP_USER: ${{ secrets.RASPI_USER }}
TARGET_HOST: ${{ secrets.RASPI_TARGET_HOST }}
TARGET_USER: ${{ secrets.RASPI_TARGET_USER }}
DEPLOY_PATH: ${{ secrets.RASPI_DEPLOY_PATH_JAVA }}
GHCR_PAT: ${{ secrets.GHCR_PAT }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
ssh -o StrictHostKeyChecking=no -o ProxyJump=$JUMP_USER@$JUMP_HOST:$JUMP_PORT $TARGET_USER@$TARGET_HOST /bin/bash << ENDSSH
set -e

echo "=== 배포 디렉토리 이동 ==="
cd $DEPLOY_PATH

echo "=== GHCR 로그인 ==="
echo "$GHCR_PAT" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin

echo "=== 최신 이미지 Pull ==="
docker compose -f docker-compose-prod.yml --env-file .env pull app

echo "=== 앱 컨테이너 재시작 ==="
docker compose -f docker-compose-prod.yml --env-file .env up -d app

echo "=== Health Check (최대 60초) ==="
for i in \$(seq 1 12); do
if curl -sf http://localhost:7300/actuator/health > /dev/null; then
echo "Health check 통과"
exit 0
fi
echo "재시도 \$i/12"
sleep 5
done

echo "Health check 실패"
docker compose -f docker-compose-prod.yml logs --tail=50 app
exit 1
ENDSSH

- name: 오래된 Docker 이미지 정리
if: success()
env:
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
JUMP_HOST: ${{ secrets.RASPI_HOST }}
JUMP_USER: ${{ secrets.RASPI_USER }}
TARGET_HOST: ${{ secrets.RASPI_TARGET_HOST }}
TARGET_USER: ${{ secrets.RASPI_TARGET_USER }}
run: |
ssh -o StrictHostKeyChecking=no -o ProxyJump=$JUMP_USER@$JUMP_HOST:$JUMP_PORT $TARGET_USER@$TARGET_HOST /bin/bash << ENDSSH
docker image prune -af --filter "until=72h" || true
ENDSSH
106 changes: 106 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: CI

on:
push:
branches:
- "*/**"
- main
pull_request:
branches:
- "*/**"
- main

jobs:
# ============================================
# Job 1: 테스트
# ============================================
test:
name: Test
runs-on: ubuntu-latest

services:
postgres:
image: postgres:17
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: interview_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: 코드 체크아웃
uses: actions/checkout@v4

- name: JDK 21 설정
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Gradle 캐싱
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: 테스트 실행
env:
SPRING_PROFILES_ACTIVE: test
run: ./gradlew test --no-daemon

# ============================================
# Job 2: Docker 이미지 빌드 + GHCR 푸시
# ============================================
build:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

permissions:
contents: read
packages: write

steps:
- name: 코드 체크아웃
uses: actions/checkout@v4

- name: Docker Buildx 설정
uses: docker/setup-buildx-action@v3

- name: GHCR 로그인
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 이미지 메타데이터 설정
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/wisoft-prepair/backend-java
tags: |
type=raw,value=latest
type=sha,prefix=

- name: Docker 이미지 빌드 & 푸시
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ FROM mcr.microsoft.com/playwright/java:v1.49.0-noble

WORKDIR /app

RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y ffmpeg curl && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/build/libs/*.jar app.jar

# 포트 문서화
EXPOSE 7300

# 컨테이너 레벨 헬스체크 (Docker daemon의 status 표시용)
HEALTHCHECK --interval=24h --timeout=10s --start-period=40s --retries=3 \
CMD curl -sf http://localhost:7300/health || exit 1
Comment on lines +36 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

HEALTHCHECK 설정에 두 가지 개선 사항이 있습니다.

  1. 경로 불일치: spring-boot-starter-actuator를 의존성에 추가하셨는데, 기본 헬스체크 경로는 /actuator/health입니다. 현재 /health로 설정되어 있어 Docker daemon이 컨테이너를 unhealthy 상태로 판단할 수 있습니다.
  2. 주기(Interval) 설정: interval=24h는 장애 감지 주기가 너무 깁니다. 컨테이너 상태를 적절히 모니터링하기 위해 보통 30s 정도로 설정하는 것을 권장합니다.
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -sf http://localhost:7300/actuator/health || exit 1


# 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
ENTRYPOINT ["java", "-jar", "app.jar"]
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ dependencies {
implementation 'software.amazon.awssdk:s3:2.26.12'
runtimeOnly 'org.postgresql:postgresql'

implementation 'org.springframework.boot:spring-boot-starter-actuator'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml → docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ services:
app:
build:
context: .
dockerfile: Dockerfile
dockerfile: Dockerfile.prev
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

dockerfile 경로가 Dockerfile.prev로 지정되어 있습니다. 이는 보통 백업 파일로 사용되는 명칭입니다. 개발 환경에서 최신 설정을 반영하기 위해 Dockerfile을 사용하도록 수정하는 것이 적절해 보입니다.

      dockerfile: Dockerfile

container_name: interview-api
ports:
- "7300:7300"
Expand Down
70 changes: 70 additions & 0 deletions docker-compose-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
services:
fluent-bit:
image: fluent/fluent-bit:3.2
container_name: interview-fluent-bit
volumes:
- ./docker/fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
- ./docker/fluent-bit/parsers.conf:/fluent-bit/etc/parsers.conf:ro
ports:
- "24224:24224"
environment:
- ELASTIC_USERNAME=${ELASTIC_USERNAME}
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
networks:
- interview-network
- logging-network
restart: unless-stopped

postgres:
image: postgres:17
container_name: interview-postgres
ports:
- "${POSTGRES_PORT}:5432"
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- interview-network
restart: unless-stopped

app:
image: ghcr.io/wisoft-prepair/backend-java:latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

운영 환경 배포 시 latest 태그를 사용하는 것은 권장되지 않습니다. latest 태그는 시점에 따라 가리키는 이미지가 달라질 수 있어 배포의 재현성을 보장하기 어렵고 롤백 시 혼선을 줄 수 있습니다. 특정 버전 태그나 커밋 해시를 사용하는 것이 안전합니다.

container_name: interview-api
ports:
- "7300:7300"
env_file:
- .env
environment:
- SPRING_PROFILES_ACTIVE=prod
- TZ=Asia/Seoul
depends_on:
postgres:
condition: service_healthy
fluent-bit:
condition: service_started
logging:
driver: fluentd
options:
fluentd-address: localhost:24224
tag: interview-service
fluentd-async: "true"
networks:
- interview-network
restart: unless-stopped

volumes:
postgres-data:

networks:
interview-network:
driver: bridge
logging-network:
external: true
9 changes: 8 additions & 1 deletion docker/fluent-bit/fluent-bit.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[SERVICE]
Flush 1
Daemon Off
Log_Level info
Parsers_File parsers.conf

Expand All @@ -15,6 +16,12 @@
Parser json
Reserve_Data On

[FILTER]
Name modify
Match *
Add source fluent-bit
Add host ${HOSTNAME}

[OUTPUT]
Name es
Match *
Expand All @@ -24,4 +31,4 @@
HTTP_Passwd ${ELASTIC_PASSWORD}
Index interview-service
Retry_Limit 5
Suppress_Type_Name On
Suppress_Type_Name On
2 changes: 1 addition & 1 deletion src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ spring:

jpa:
hibernate:
ddl-auto: validate
ddl-auto: update
show-sql: true
open-in-view: false
properties:
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ spring:

jpa:
hibernate:
ddl-auto: validate
ddl-auto: update
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

운영 환경(prod)에서 hibernate.ddl-autoupdate로 설정하는 것은 매우 위험합니다. 의도치 않은 스키마 변경으로 인해 데이터가 유실되거나 서비스 장애가 발생할 수 있습니다. 운영 환경에서는 반드시 validate 또는 none을 사용해야 합니다.

      ddl-auto: validate

open-in-view: false

Loading