Skip to content

Commit 2a04775

Browse files
authored
Merge: CI/CD 및 모니터링 구축 (#169)
Feat: CI/CD 및 모니터링 구축
2 parents 57021c1 + 4aa94a5 commit 2a04775

File tree

20 files changed

+757
-24
lines changed

20 files changed

+757
-24
lines changed

.github/workflows/cd.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 추후 dev에 머지 되면 cd.yml로 배포 워크플로우 분리
2+
3+
name: CD - Deploy Backend
4+
5+
on:
6+
workflow_run:
7+
workflows: ["CI - Build & Push Backend"]
8+
types: [completed]
9+
branches: [release]
10+
11+
workflow_dispatch:
12+
inputs:
13+
tag:
14+
description: "Deploy image tag (default: release)"
15+
required: false
16+
default: "release"
17+
18+
permissions:
19+
contents: read
20+
21+
concurrency:
22+
group: deploy-docsa
23+
cancel-in-progress: false
24+
25+
jobs:
26+
deploy:
27+
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
28+
runs-on: ubuntu-latest
29+
environment: production
30+
steps:
31+
- name: Decide tag
32+
id: tag
33+
run: |
34+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
35+
echo "value=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
36+
else
37+
echo "value=release" >> $GITHUB_OUTPUT
38+
fi
39+
40+
- name: Deploy via SSH
41+
uses: appleboy/ssh-action@v1.0.3
42+
with:
43+
host: ${{ secrets.CD_HOST }}
44+
username: ${{ secrets.CD_USER }}
45+
key: ${{ secrets.CD_SSH_KEY }}
46+
port: ${{ secrets.CD_PORT }}
47+
script: |
48+
export DEPLOY_TAG='${{ steps.tag.outputs.value }}'
49+
bash -lc '/srv/docsa/infra/deploy.sh'

.github/workflows/ci-cd.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: CI/CD — Backend (Build → Push → Deploy)
2+
3+
on:
4+
# dev에 머지 전 ci push 대상 브랜치 중첩 적용 (release, dev)
5+
push:
6+
branches: [ release, dev ]
7+
pull_request:
8+
branches: [ dev ]
9+
workflow_dispatch:
10+
inputs:
11+
tag:
12+
description: "배포할 이미지 태그 (ex. release or <GIT_SHA>)"
13+
required: false
14+
default: "release"
15+
16+
concurrency:
17+
group: cicd-${{ github.ref_name }}
18+
cancel-in-progress: false
19+
20+
jobs:
21+
build-and-push:
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
packages: write
26+
env:
27+
IMAGE: ghcr.io/prgrms-web-devcourse-final-project/docsa-backend
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Set up JDK
32+
uses: actions/setup-java@v4
33+
with:
34+
java-version: '21'
35+
distribution: 'temurin'
36+
37+
- name: Cache Gradle
38+
uses: gradle/actions/setup-gradle@v3
39+
40+
- name: Detect project dir
41+
id: detect
42+
shell: bash
43+
run: |
44+
if [ -f "./gradlew" ] || [ -f "./build.gradle" ] || [ -f "./build.gradle.kts" ]; then
45+
echo "dir=." >> $GITHUB_OUTPUT
46+
elif [ -d "./backend" ]; then
47+
echo "dir=backend" >> $GITHUB_OUTPUT
48+
else
49+
echo "No Gradle project found"; exit 1
50+
fi
51+
52+
- name: Test
53+
env:
54+
MAIL_PASSWORD: ${{ secrets.CI_MAIL_PASSWORD }}
55+
MAIL_USERNAME: ${{ secrets.CI_MAIL_USERNAME }}
56+
MONGO_URI: ${{ secrets.CI_MONGO_URI }}
57+
run: |
58+
cd "${{ steps.detect.outputs.dir }}"
59+
chmod +x ./gradlew || true
60+
./gradlew clean test --no-daemon
61+
62+
- name: Find Dockerfile
63+
id: df
64+
run: |
65+
if [ -f "infra/backend/Dockerfile" ]; then
66+
echo "path=infra/backend/Dockerfile" >> $GITHUB_OUTPUT
67+
echo "ctx=." >> $GITHUB_OUTPUT
68+
else
69+
echo "No Dockerfile found"; exit 1
70+
fi
71+
72+
- name: Build image (PR only)
73+
if: github.event_name == 'pull_request'
74+
run: |
75+
docker build -f "${{ steps.df.outputs.path }}" -t sanity-check:pr "${{ steps.df.outputs.ctx }}"
76+
77+
- name: Log in to GHCR
78+
if: github.event_name == 'push'
79+
uses: docker/login-action@v3
80+
with:
81+
registry: ghcr.io
82+
username: ${{ github.actor }}
83+
password: ${{ secrets.GITHUB_TOKEN }}
84+
85+
- name: Build & Push image
86+
if: github.event_name == 'push'
87+
run: |
88+
GIT_SHA=${{ github.sha }}
89+
docker build -f "${{ steps.df.outputs.path }}" -t $IMAGE:release -t $IMAGE:$GIT_SHA "${{ steps.df.outputs.ctx }}"
90+
docker push $IMAGE:release
91+
docker push $IMAGE:$GIT_SHA
92+
93+
deploy:
94+
needs: build-and-push
95+
# dev에 머지 전 cd 대상 브랜치 중첩 적용 (release, dev)
96+
if: github.event_name == 'push' && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/dev') &&
97+
(needs.build-and-push.result == 'success')
98+
|| (github.event_name == 'workflow_dispatch')
99+
runs-on: ubuntu-latest
100+
101+
steps:
102+
- name: Decide tag
103+
id: tag
104+
run: |
105+
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.tag }}" ]; then
106+
echo "value=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
107+
else
108+
echo "value=release" >> $GITHUB_OUTPUT
109+
fi
110+
111+
- name: Deploy
112+
uses: appleboy/ssh-action@v1.0.3
113+
with:
114+
host: ${{ secrets.CD_HOST }}
115+
username: ${{ secrets.CD_USER }}
116+
key: ${{ secrets.CD_SSH_KEY }}
117+
port: ${{ secrets.CD_PORT }}
118+
script: |
119+
export DEPLOY_TAG='${{ steps.tag.outputs.value }}'
120+
bash -lc '/srv/docsa/infra/deploy.sh'

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build/
44
!gradle/wrapper/gradle-wrapper.jar
55
!**/src/main/**/build/
66
!**/src/test/**/build/
7+
infra/backend/*.jar
78

89
### STS ###
910
.apt_generated
@@ -35,3 +36,13 @@ out/
3536

3637
### VS Code ###
3738
.vscode/
39+
40+
### Secrets & Runtime
41+
infra/.env
42+
infra/**/*.env
43+
infra/certbot
44+
infra/mysql/exporter.cnf
45+
infra/mysql/logs/
46+
*.log
47+
*.pid
48+
.*.swp

build.gradle

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = 'io.EJangs'
8-
version = '0.0.1-SNAPSHOT'
8+
version = '0.0.1'
99

1010
java {
1111
toolchain {
@@ -23,6 +23,16 @@ repositories {
2323
mavenCentral()
2424
}
2525

26+
test {
27+
testLogging {
28+
events "passed", "skipped", "failed", "standardOut", "standardError"
29+
exceptionFormat "full"
30+
showCauses true
31+
showExceptions true
32+
showStackTraces true
33+
}
34+
}
35+
2636
dependencies {
2737
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2838
implementation 'org.springframework.boot:spring-boot-starter-security'
@@ -55,3 +65,7 @@ dependencies {
5565
tasks.named('test') {
5666
useJUnitPlatform()
5767
}
68+
69+
tasks.named('jar') {
70+
enabled = false
71+
}

infra/.env.example

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ------------ APP ------------
2+
SPRING_DATA_MONGODB_URI=
3+
SPRING_MAIL_USERNAME=
4+
SPRING_MAIL_PASSWORD=
5+
SPRING_DATASOURCE_URL=
6+
SPRING_DATASOURCE_USERNAME=
7+
SPRING_DATASOURCE_PASSWORD=
8+
9+
# ------------ SESSION/COOKIE ------------
10+
SESSION_COOKIE_NAME=
11+
SESSION_COOKIE_DOMAIN=
12+
13+
# ------------ SWAGGER ------------
14+
SWAGGER_SERVER_URL=
15+
16+
# ------------ SPRING ------------
17+
SPRING_PROFILES_ACTIVE=
18+
SERVER_PORT=
19+
20+
# ------------ DOMAIN ------------
21+
DOMAIN=
22+
23+
# ------------ MYSQL ------------
24+
MYSQL_DATABASE=
25+
MYSQL_USER=
26+
MYSQL_PASSWORD=
27+
MYSQL_ROOT_PASSWORD=
28+
29+
# ------------ GRAFANA ------------
30+
GRAFANA_ADMIN=
31+
GRAFANA_PASSWORD=

infra/backend/Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ---- Build Stage ----
2+
FROM eclipse-temurin:21-jdk-alpine AS builder
3+
4+
WORKDIR /workspace
5+
6+
COPY gradlew ./gradlew
7+
COPY gradle ./gradle
8+
COPY build.gradle* settings.gradle* ./
9+
RUN chmod +x ./gradlew
10+
11+
RUN ./gradlew --no-daemon dependencies || true
12+
13+
COPY . .
14+
15+
RUN ./gradlew clean bootJar -x test --no-daemon
16+
17+
# ---- Runtime Stage ----
18+
FROM eclipse-temurin:21-jre-alpine
19+
20+
RUN addgroup -S app && adduser -S app -G app
21+
RUN apk add --no-cache wget
22+
23+
WORKDIR /app
24+
25+
COPY --from=builder --chown=app:app /workspace/build/libs/*.jar /app/app.jar
26+
27+
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseG1GC -Duser.timezone=Asia/Seoul"
28+
29+
EXPOSE 8080
30+
USER app
31+
32+
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

infra/deploy.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
cd /srv/docsa/infra
5+
6+
# 특정 태그로 배포시 DEPLOY_TAG=...
7+
IMAGE_BASE="ghcr.io/prgrms-web-devcourse-final-project/docsa-backend"
8+
TAG="${DEPLOY_TAG:-}" # 비어있으면 compose에 적힌 태그 사용
9+
OVR=""
10+
if [[ -n "$TAG" ]]; then
11+
OVR="docker-compose.override.deploy.yml"
12+
cat > "$OVR" <<EOF
13+
services:
14+
app:
15+
image: ${IMAGE_BASE}:${TAG}
16+
EOF
17+
fi
18+
19+
FILES=(-f docker-compose.yml)
20+
[[ -n "$OVR" ]] && FILES+=(-f "$OVR")
21+
22+
# 최신 이미지 받고 교체
23+
docker compose "${FILES[@]}" pull app
24+
docker compose "${FILES[@]}" up -d app
25+
26+
# 헬스체크 대기 (최대 120s)
27+
echo -n "Waiting for app (docsa-app) to be healthy"
28+
ok=0
29+
for i in {1..60}; do
30+
status="$(docker inspect -f '{{.State.Health.Status}}' docsa-app 2>/dev/null || echo none)"
31+
if [[ "$status" == "healthy" ]]; then ok=1; echo -e "\nApp healthy"; break; fi
32+
sleep 2; echo -n "."
33+
done
34+
35+
# 임시 override 제거 & 청소
36+
[[ -n "$OVR" ]] && rm -f "$OVR"
37+
docker image prune -f >/dev/null 2>&1 || true
38+
39+
# 실패 처리
40+
if [[ $ok -eq 0 ]]; then
41+
echo -e "\nApp failed to become healthy (status=$status)"
42+
docker logs --tail=200 docsa-app || true
43+
exit 1
44+
fi

0 commit comments

Comments
 (0)