Skip to content

Commit 770ac61

Browse files
authored
chore: Github Actions CI/CD 파이프라인 구축 (#53)
1 parent cf6a2fb commit 770ac61

12 files changed

Lines changed: 324 additions & 67 deletions

File tree

.github/.DS_Store

6 KB
Binary file not shown.

.github/workflows/cd.yaml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: CD
2+
3+
on:
4+
workflow_run:
5+
workflows: ['CI']
6+
types: [completed]
7+
8+
jobs:
9+
# ============================================
10+
# 서버 배포
11+
# ============================================
12+
deploy:
13+
name: Deploy to Wisoft Server
14+
runs-on: ubuntu-latest
15+
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }}
16+
17+
steps:
18+
- name: SSH 키 설정
19+
uses: webfactory/ssh-agent@v0.9.0
20+
with:
21+
ssh-private-key: ${{ secrets.RASPI_SSH_PRIVATE_KEY }}
22+
23+
- name: Known Hosts 등록
24+
env:
25+
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
26+
JUMP_HOST: ${{ secrets.RASPI_HOST }}
27+
run: |
28+
mkdir -p ~/.ssh
29+
ssh-keyscan -p $JUMP_PORT -H $JUMP_HOST >> ~/.ssh/known_hosts
30+
31+
- name: 서버에 배포
32+
env:
33+
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
34+
JUMP_HOST: ${{ secrets.RASPI_HOST }}
35+
JUMP_USER: ${{ secrets.RASPI_USER }}
36+
TARGET_HOST: ${{ secrets.RASPI_TARGET_HOST }}
37+
TARGET_USER: ${{ secrets.RASPI_TARGET_USER }}
38+
DEPLOY_PATH: ${{ secrets.RASPI_DEPLOY_PATH_JAVA }}
39+
GHCR_PAT: ${{ secrets.GHCR_PAT }}
40+
GITHUB_ACTOR: ${{ github.actor }}
41+
run: |
42+
ssh -o StrictHostKeyChecking=no -o ProxyJump=$JUMP_USER@$JUMP_HOST:$JUMP_PORT $TARGET_USER@$TARGET_HOST /bin/bash << ENDSSH
43+
set -e
44+
45+
echo "=== 배포 디렉토리 이동 ==="
46+
cd $DEPLOY_PATH
47+
48+
echo "=== GHCR 로그인 ==="
49+
echo "$GHCR_PAT" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
50+
51+
echo "=== 최신 이미지 Pull ==="
52+
docker compose -f docker/docker-compose.prod.yml --env-file .env pull app
53+
54+
echo "=== 앱 컨테이너 재시작 ==="
55+
docker compose -f docker/docker-compose.prod.yml --env-file .env up -d app
56+
57+
echo "=== Health Check (최대 60초) ==="
58+
for i in \$(seq 1 12); do
59+
if curl -sf http://localhost:7300/health > /dev/null; then
60+
echo "Health check 통과"
61+
exit 0
62+
fi
63+
echo "재시도 \$i/12"
64+
sleep 5
65+
done
66+
67+
echo "Health check 실패"
68+
docker compose -f docker/docker-compose.prod.yml logs --tail=50 app
69+
exit 1
70+
ENDSSH
71+
72+
- name: 오래된 Docker 이미지 정리
73+
if: success()
74+
env:
75+
JUMP_PORT: ${{ secrets.RASPI_SSH_PORT }}
76+
JUMP_HOST: ${{ secrets.RASPI_HOST }}
77+
JUMP_USER: ${{ secrets.RASPI_USER }}
78+
TARGET_HOST: ${{ secrets.RASPI_TARGET_HOST }}
79+
TARGET_USER: ${{ secrets.RASPI_TARGET_USER }}
80+
run: |
81+
ssh -o StrictHostKeyChecking=no -o ProxyJump=$JUMP_USER@$JUMP_HOST:$JUMP_PORT $TARGET_USER@$TARGET_HOST /bin/bash << ENDSSH
82+
docker image prune -af --filter "until=72h" || true
83+
ENDSSH

.github/workflows/ci.yaml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- "*/**"
7+
- main
8+
pull_request:
9+
branches:
10+
- "*/**"
11+
- main
12+
13+
jobs:
14+
# ============================================
15+
# Job 1: 테스트
16+
# ============================================
17+
test:
18+
name: Test
19+
runs-on: ubuntu-latest
20+
21+
services:
22+
postgres:
23+
image: postgres:17
24+
env:
25+
POSTGRES_USER: test
26+
POSTGRES_PASSWORD: test
27+
POSTGRES_DB: interview_test
28+
ports:
29+
- 5432:5432
30+
options: >-
31+
--health-cmd pg_isready
32+
--health-interval 10s
33+
--health-timeout 5s
34+
--health-retries 5
35+
36+
steps:
37+
- name: 코드 체크아웃
38+
uses: actions/checkout@v4
39+
40+
- name: JDK 21 설정
41+
uses: actions/setup-java@v4
42+
with:
43+
java-version: '21'
44+
distribution: 'temurin'
45+
46+
- name: Gradle 캐싱
47+
uses: actions/cache@v4
48+
with:
49+
path: |
50+
~/.gradle/caches
51+
~/.gradle/wrapper
52+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
53+
restore-keys: |
54+
${{ runner.os }}-gradle-
55+
56+
- name: 테스트 실행
57+
env:
58+
SPRING_PROFILES_ACTIVE: test
59+
run: ./gradlew test --no-daemon
60+
61+
# ============================================
62+
# Job 2: Docker 이미지 빌드 + GHCR 푸시
63+
# ============================================
64+
build:
65+
name: Build & Push Docker Image
66+
runs-on: ubuntu-latest
67+
needs: test
68+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
69+
70+
permissions:
71+
contents: read
72+
packages: write
73+
74+
steps:
75+
- name: 코드 체크아웃
76+
uses: actions/checkout@v4
77+
78+
- name: Docker Buildx 설정
79+
uses: docker/setup-buildx-action@v3
80+
81+
- name: GHCR 로그인
82+
uses: docker/login-action@v3
83+
with:
84+
registry: ghcr.io
85+
username: ${{ github.actor }}
86+
password: ${{ secrets.GITHUB_TOKEN }}
87+
88+
- name: 이미지 메타데이터 설정
89+
id: meta
90+
uses: docker/metadata-action@v5
91+
with:
92+
images: ghcr.io/${{ github.repository }}
93+
tags: |
94+
type=raw,value=latest
95+
type=sha,prefix=
96+
97+
- name: Docker 이미지 빌드 & 푸시
98+
uses: docker/build-push-action@v6
99+
with:
100+
context: .
101+
platforms: linux/arm64
102+
push: true
103+
tags: ${{ steps.meta.outputs.tags }}
104+
labels: ${{ steps.meta.outputs.labels }}
105+
cache-from: type=gha
106+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ FROM mcr.microsoft.com/playwright/java:v1.49.0-noble
2525

2626
WORKDIR /app
2727

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

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

3232
# 포트 문서화
3333
EXPOSE 7300
3434

35+
# 컨테이너 레벨 헬스체크 (Docker daemon의 status 표시용)
36+
HEALTHCHECK --interval=24h --timeout=10s --start-period=40s --retries=3 \
37+
CMD curl -sf http://localhost:7300/health || exit 1
38+
3539
# 실행
36-
ENTRYPOINT ["java", "-jar", "app.jar"]
40+
ENTRYPOINT ["java", "-jar", "app.jar"]

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ dependencies {
4343
implementation 'software.amazon.awssdk:s3:2.26.12'
4444
runtimeOnly 'org.postgresql:postgresql'
4545

46+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
47+
4648
compileOnly 'org.projectlombok:lombok'
4749
annotationProcessor 'org.projectlombok:lombok'
4850

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ services:
3131
app:
3232
build:
3333
context: .
34-
dockerfile: Dockerfile
34+
dockerfile: Dockerfile.prev
3535
container_name: interview-api
3636
ports:
3737
- "7300:7300"

docker-compose-prod.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
services:
2+
fluent-bit:
3+
image: fluent/fluent-bit:3.2
4+
container_name: interview-fluent-bit
5+
volumes:
6+
- ./fluentbit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
7+
- ./fluentbit/parsers.conf:/fluent-bit/etc/parsers.conf:ro
8+
ports:
9+
- "24224:24224"
10+
environment:
11+
- ELASTIC_USERNAME=${ELASTIC_USERNAME}
12+
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
13+
networks:
14+
- interview-network
15+
- logging-network
16+
restart: unless-stopped
17+
18+
postgres:
19+
image: postgres:17
20+
container_name: interview-postgres
21+
ports:
22+
- "${POSTGRES_PORT}:5432"
23+
environment:
24+
- POSTGRES_DB=${POSTGRES_DB}
25+
- POSTGRES_USER=${POSTGRES_USER}
26+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
27+
volumes:
28+
- postgres-data:/var/lib/postgresql/data
29+
healthcheck:
30+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
31+
interval: 10s
32+
timeout: 5s
33+
retries: 5
34+
networks:
35+
- interview-network
36+
restart: unless-stopped
37+
38+
app:
39+
image: ghcr.io/wisoft-prepair/backend-java:latest
40+
container_name: interview-api
41+
ports:
42+
- "7300:7300"
43+
env_file:
44+
- .env
45+
environment:
46+
- SPRING_PROFILES_ACTIVE=prod
47+
- TZ=Asia/Seoul
48+
depends_on:
49+
postgres:
50+
condition: service_healthy
51+
fluent-bit:
52+
condition: service_started
53+
logging:
54+
driver: fluentd
55+
options:
56+
fluentd-address: localhost:24224
57+
tag: interview-service
58+
fluentd-async: "true"
59+
networks:
60+
- interview-network
61+
restart: unless-stopped
62+
63+
volumes:
64+
postgres-data:
65+
66+
networks:
67+
interview-network:
68+
driver: bridge
69+
logging-network:
70+
external: true

docker/fluent-bit/fluent-bit.conf

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[SERVICE]
22
Flush 1
3+
Daemon Off
34
Log_Level info
45
Parsers_File parsers.conf
56

@@ -15,6 +16,12 @@
1516
Parser json
1617
Reserve_Data On
1718

19+
[FILTER]
20+
Name modify
21+
Match *
22+
Add source fluent-bit
23+
Add host ${HOSTNAME}
24+
1825
[OUTPUT]
1926
Name es
2027
Match *
@@ -24,4 +31,4 @@
2431
HTTP_Passwd ${ELASTIC_PASSWORD}
2532
Index interview-service
2633
Retry_Limit 5
27-
Suppress_Type_Name On
34+
Suppress_Type_Name On

src/main/resources/application-local.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ spring:
77

88
jpa:
99
hibernate:
10-
ddl-auto: validate
10+
ddl-auto: update
1111
show-sql: true
1212
open-in-view: false
1313
properties:

src/main/resources/application-prod.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ spring:
77

88
jpa:
99
hibernate:
10-
ddl-auto: validate
10+
ddl-auto: update
1111
open-in-view: false
1212

0 commit comments

Comments
 (0)