From 6ca90098f75b356c1790fef6cbdc864ef84e32f5 Mon Sep 17 00:00:00 2001 From: Chhun-Lee Date: Sun, 25 Oct 2026 01:20:29 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20actuator=20=ED=97=AC=EC=8A=A4?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배포 확인용으로 /actuator/health만 노출 - JWT 적용 후에도 헬스체크 경로는 인증 없이 허용 --- build.gradle | 1 + src/main/java/com/itssum/global/config/SecurityConfig.java | 7 +++++-- src/main/resources/application.yml | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fa0ac4d..46c769d 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/src/main/java/com/itssum/global/config/SecurityConfig.java b/src/main/java/com/itssum/global/config/SecurityConfig.java index f989bf2..df37e4b 100644 --- a/src/main/java/com/itssum/global/config/SecurityConfig.java +++ b/src/main/java/com/itssum/global/config/SecurityConfig.java @@ -20,8 +20,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // TODO: JWT 적용 시 인증이 필요한 경로를 제한한다. 지금은 전체 허용. - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); + .authorizeHttpRequests(auth -> auth + // CD 배포 헬스체크. JWT 적용 후에도 인증 없이 열어 둔다. + .requestMatchers("/actuator/health").permitAll() + // TODO: JWT 적용 시 인증이 필요한 경로를 제한한다. 지금은 전체 허용. + .anyRequest().permitAll()); return http.build(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d9f087a..fd08b2e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,3 +17,10 @@ spring: hibernate: jdbc: time_zone: Asia/Seoul + +management: + endpoints: + web: + exposure: + # CD 배포 확인용. health 외 actuator 엔드포인트는 노출하지 않는다. + include: health From c8cf898a9ec0ebcdd86f07f30673d708c2ffc3bf Mon Sep 17 00:00:00 2001 From: Chhun-Lee Date: Sun, 25 Oct 2026 01:20:37 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20Docker=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20EC2=20=EC=8B=A4=ED=96=89=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile 멀티 스테이지 빌드, .dockerignore로 비밀값 파일 제외 - plain jar 비활성화로 실행 가능한 jar 하나만 생성 - EC2용 docker-compose.yml (127.0.0.1:8080 바인딩, KST, 로그 순환) - application-prod.yml 분리 (Nginx 프록시 헤더 처리) --- .dockerignore | 28 +++++++++++++++++++++++++ Dockerfile | 25 ++++++++++++++++++++++ build.gradle | 5 +++++ docker-compose.yml | 22 +++++++++++++++++++ src/main/resources/application-prod.yml | 5 +++++ 5 files changed, 85 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/main/resources/application-prod.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e5de657 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# 빌드 산출물 / 로컬 캐시 (이미지 빌드 컨텍스트에서 제외 → 전송량·빌드시간 절감) +build/ +.gradle/ + +# VCS / IDE +.git/ +.github/ +.idea/ +*.iml +.vscode/ + +# 시크릿 / 로컬 설정 — 이미지에 비밀값이 들어가지 않게 한다 +.env +.env.* +.secrets/ +*.pem +*.key +*.p12 +*.jks +src/main/resources/application-local.* +src/main/resources/application-secret.* + +# 기타 +logs/ +*.log +HELP.md +CLAUDE.md +docs/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e8affd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# ===== Build stage ===== +FROM eclipse-temurin:21-jdk AS builder +WORKDIR /app + +# 빌드 스크립트 먼저 복사 → 의존성 레이어 캐싱 (소스만 바뀔 땐 재다운로드 안 함) +COPY gradlew . +COPY gradle gradle +COPY build.gradle settings.gradle ./ +RUN chmod +x gradlew && ./gradlew dependencies --no-daemon > /dev/null 2>&1 || true + +# 소스 복사 후 실행 가능한 jar 빌드 (테스트는 CI에서 이미 통과한 상태) +COPY src src +RUN ./gradlew clean bootJar --no-daemon + +# ===== Runtime stage ===== +FROM eclipse-temurin:21-jre +WORKDIR /app + +# plain jar를 껐으므로 build/libs 에는 실행 가능한 jar 하나만 존재 +COPY --from=builder /app/build/libs/*.jar app.jar + +EXPOSE 8080 + +# 컨테이너에 할당된 메모리에 맞춰 힙 자동 조정 (t3.micro 등 소형 인스턴스 대비) +ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 46c769d..b450505 100644 --- a/build.gradle +++ b/build.gradle @@ -42,3 +42,8 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +// plain jar를 끈다 → build/libs 에 실행 가능한 jar 하나만 남아 Dockerfile 이 그대로 복사한다 +tasks.named('jar') { + enabled = false +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..63d49b1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +# EC2에서 앱 컨테이너를 기동하는 구성. +# 실제 값(이미지 태그, DB 접속정보)은 같은 폴더의 .env 를 CD 워크플로우가 생성해 주입한다. +services: + app: + image: ${DOCKER_IMAGE} # 예: /itssum-be:<커밋 SHA> (.env 에서 주입) + container_name: itssum-be + ports: + # 외부 트래픽은 호스트 Nginx(80)만 받고, 앱은 Nginx를 통해서만 접근 → 127.0.0.1 바인딩으로 8080 직접노출 차단 + - "127.0.0.1:8080:8080" + env_file: + - .env # DB_* 주입 + environment: + SPRING_PROFILES_ACTIVE: prod + # 로그 시각을 KST로 맞춘다 (DB 저장 기준은 application.yml 의 hibernate time_zone) + TZ: Asia/Seoul + # 로그 파일이 디스크를 채우지 않도록 10MB x 5개로 순환 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + restart: unless-stopped diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..38f8872 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,5 @@ +# 운영(EC2) 전용 설정. CD가 SPRING_PROFILES_ACTIVE=prod 로 띄운다. +# 공통 값은 application.yml, 비밀값(DB_* 등)은 CD가 만든 .env 로 주입된다. +server: + # 호스트 Nginx 뒤에서 X-Forwarded-* 헤더로 원래 요청의 IP·프로토콜을 인식한다 + forward-headers-strategy: native From b05f008d176ccecf1ddbc78f43ca472d5bc5d377 Mon Sep 17 00:00:00 2001 From: Chhun-Lee Date: Sun, 25 Oct 2026 01:20:37 +0900 Subject: [PATCH 3/5] =?UTF-8?q?ci:=20CI/CD=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI: develop/main 대상 PR에서 MySQL 8.4 서비스 컨테이너로 빌드·테스트 - CD: main push 시 Docker Hub push 후 EC2에 SSH로 배포, 커밋 SHA 태그 고정 - 배포 후 /actuator/health로 기동 확인 (최대 120초) --- .github/workflows/cd.yml | 99 ++++++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 48 +++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..2fe4760 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,99 @@ +name: CD + +# main 에 push(= PR 머지) 되면 이미지 빌드 → Docker Hub push → EC2 배포 +on: + push: + branches: [main] + workflow_dispatch: # Actions 탭에서 수동 실행도 허용 (재배포용) + +# 배포가 겹치면 EC2 컨테이너 교체가 꼬인다 → 한 번에 하나씩, 진행 중인 배포는 끊지 않는다 +concurrency: + group: cd + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: 소스 체크아웃 + uses: actions/checkout@v5 + + - name: Docker Hub 로그인 + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Buildx 설정 + uses: docker/setup-buildx-action@v3 + + - name: 이미지 빌드 & 푸시 + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/itssum-be:latest + ${{ secrets.DOCKERHUB_USERNAME }}/itssum-be:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: docker-compose.yml 전송 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + source: docker-compose.yml + target: ~/itssum-app + + - name: EC2 배포 + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + set -e + cd ~/itssum-app + + # 시크릿으로 .env 생성 (compose 가 이미지 태그·DB 설정을 여기서 읽음) + # 이미지는 latest 가 아니라 커밋 SHA 로 고정한다 → 이전 CD 실행을 Re-run 하면 그 버전으로 롤백된다. + # EC2 의 .env 를 직접 고치면 다음 배포에 이 블록이 덮어써서 되돌아온다. + cat > .env <<'EOF' + DOCKER_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/itssum-be:${{ github.sha }} + DB_URL=${{ secrets.DB_URL }} + DB_USERNAME=${{ secrets.DB_USERNAME }} + DB_PASSWORD=${{ secrets.DB_PASSWORD }} + EOF + chmod 600 .env + + # 시크릿 누락은 빈 값으로 조용히 통과한다 → 컨테이너 띄우기 전에 여기서 끊는다. + for required in DB_URL DB_USERNAME DB_PASSWORD; do + if ! grep -q "^${required}=." .env; then + echo "❌ 필수 시크릿이 비어 있습니다: ${required}" + exit 1 + fi + done + + # private 이미지 pull 위해 EC2 도 Docker Hub 로그인 + echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin + + docker compose pull + docker compose config --quiet + docker compose up -d + # SHA 태그라 이전 이미지가 dangling 이 되지 않는다 → 컨테이너가 안 쓰는 이미지를 모두 정리 + docker image prune -af + + # 기동 확인 (최대 120초 대기). health 는 DB 연결까지 확인한다. + for i in $(seq 1 24); do + if curl -sf http://localhost:8080/actuator/health > /dev/null; then + echo "✅ 배포 성공: 앱이 응답합니다." + exit 0 + fi + echo "기동 대기 중... ($i/24)" + sleep 5 + done + echo "❌ 앱이 120초 내 응답하지 않음. 로그 확인:" + docker compose logs --tail=100 + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7c0a33 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +# develop / main 으로의 PR 시 테스트 + 빌드 검증만 수행 (배포 X) +on: + pull_request: + branches: [develop, main] + +jobs: + build: + runs-on: ubuntu-latest + + # @SpringBootTest 가 실제 DB 연결을 요구하므로 운영과 같은 MySQL 8.4 를 띄운다 + services: + mysql: + image: mysql:8.4 + env: + MYSQL_ROOT_PASSWORD: test + MYSQL_DATABASE: itssum + ports: + - 3306:3306 + # -h 127.0.0.1 로 TCP 확인 → 초기화용 임시 서버(소켓 전용)가 아닌 실제 서버가 뜬 뒤에 통과한다 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ptest" + --health-interval=5s + --health-timeout=5s + --health-retries=20 + + env: + DB_URL: jdbc:mysql://127.0.0.1:3306/itssum?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + DB_USERNAME: root + DB_PASSWORD: test + + steps: + - name: 소스 체크아웃 + uses: actions/checkout@v5 + + - name: JDK 21 설정 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + cache: gradle + + - name: gradlew 실행 권한 + run: chmod +x gradlew + + - name: 빌드 & 테스트 + run: ./gradlew clean build From 3ed7b869084814c6e06c87f9efc05f251100ea53 Mon Sep 17 00:00:00 2001 From: Chhun-Lee Date: Sun, 25 Oct 2026 01:20:37 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=EB=B0=B0=ED=8F=AC=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EB=B0=8F=20EC2=20=EC=B4=88=EA=B8=B0=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README에 배포 흐름과 필요한 GitHub Secrets 정리 - MySQL 버전 8.4로 수정 - EC2 사용자 데이터 스크립트 (Docker, Nginx, 스왑, 시간대) --- README.md | 23 ++++++++++++++-- docs/infra/ec2-user-data.sh | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 docs/infra/ec2-user-data.sh diff --git a/README.md b/README.md index 39d5997..0cfea67 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,12 @@ |---|---| | Language | Java 21 | | Framework | Spring Boot 4.1.1, Spring MVC | -| DB | MySQL 8.0, Spring Data JPA (Hibernate) | +| DB | MySQL 8.4, Spring Data JPA (Hibernate) | | 인증 | Spring Security (JWT 적용 예정) | | 실시간 | Spring WebSocket + STOMP | | 기타 | Validation, Java Mail Sender, Lombok | | Build | Gradle (Groovy) | +| 배포 | GitHub Actions, Docker, AWS EC2 · RDS | | 예정 | AWS S3 (이미지), FCM (푸시), springdoc (Swagger) |
@@ -129,7 +130,7 @@ src/main/java/com/itssum ## 로컬 실행 -MySQL 8.0에 `itssum` 스키마를 먼저 만들어 두고, DB 접속 정보는 환경변수로 넘깁니다. 접속 정보는 커밋하지 않습니다. +MySQL 8.4에 `itssum` 스키마를 먼저 만들어 두고, DB 접속 정보는 환경변수로 넘깁니다. 접속 정보는 커밋하지 않습니다. ```bash export DB_URL="jdbc:mysql://localhost:3306/itssum?serverTimezone=Asia/Seoul" @@ -143,6 +144,24 @@ IntelliJ에서 띄울 때는 Run Configuration의 Environment variables에 같
+## 배포 + +수동 배포는 하지 않습니다. `main`에 머지되면 GitHub Actions가 EC2에 배포합니다. + +| 워크플로우 | 실행 시점 | 하는 일 | +|---|---|---| +| CI (`.github/workflows/ci.yml`) | `develop`, `main` 대상 PR | MySQL 8.4 컨테이너를 띄워 빌드·테스트 | +| CD (`.github/workflows/cd.yml`) | `main` push, Actions 탭 수동 실행 | 이미지 빌드 → Docker Hub push → EC2에서 컨테이너 교체 → `/actuator/health` 확인 | + +- 인프라는 EC2(Ubuntu 24.04) 한 대와 RDS MySQL 8.4입니다. EC2의 호스트 Nginx가 80 → `127.0.0.1:8080`으로 프록시하고, 초기 세팅 스크립트는 `docs/infra/ec2-user-data.sh`에 있습니다. +- 비밀값은 GitHub Secrets에만 둡니다. CD가 배포할 때마다 EC2의 `.env`를 새로 만들기 때문에 EC2에서 `.env`를 직접 고치면 다음 배포에서 덮어써집니다. +- 환경변수를 새로 추가할 때는 GitHub Secrets 등록과 `cd.yml`의 `.env` 블록 수정을 같이 해야 합니다. +- 이미지 태그는 커밋 SHA입니다. 롤백은 Actions에서 이전 CD 실행을 Re-run 하면 됩니다. + +필요한 GitHub Secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`, `EC2_HOST`, `EC2_USER`, `EC2_SSH_KEY`, `DB_URL`, `DB_USERNAME`, `DB_PASSWORD` + +
+ ## API 명세 API 명세는 노션에서 관리하고 있습니다. springdoc을 붙이면 Swagger 주소를 여기에 추가할 예정입니다. diff --git a/docs/infra/ec2-user-data.sh b/docs/infra/ec2-user-data.sh new file mode 100644 index 0000000..3ae9695 --- /dev/null +++ b/docs/infra/ec2-user-data.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# EC2(Ubuntu 24.04) 최초 부팅 시 실행하는 사용자 데이터. Docker, Nginx(80 -> 8080), 2GB 스왑, KST 시간대를 세팅한다. +# 이미 만든 인스턴스에는 한 번만 sudo bash 로 실행한다 (두 번 돌리면 스왑 단계에서 실패). +# 콘솔 사용자 데이터에 파일로 올리면 한글 주석이 깨져 보이지만 주석이라 실행에는 영향 없다. +set -euxo pipefail +export DEBIAN_FRONTEND=noninteractive + +timedatectl set-timezone Asia/Seoul + +# 스왑 2GB — Spring Boot 기동 시 메모리 여유 +fallocate -l 2G /swapfile +chmod 600 /swapfile +mkswap /swapfile +swapon /swapfile +echo '/swapfile none swap sw 0 0' >> /etc/fstab + +# Docker + compose 플러그인. ubuntu 사용자를 docker 그룹에 넣어 CD가 sudo 없이 docker 를 쓴다. +curl -fsSL https://get.docker.com | sh +usermod -aG docker ubuntu + +# Nginx: 80 -> 127.0.0.1:8080 (STOMP WebSocket 업그레이드 포함) +apt-get update -y +apt-get install -y nginx +cat > /etc/nginx/sites-available/itssum <<'EOF' +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } +} +EOF +ln -sf /etc/nginx/sites-available/itssum /etc/nginx/sites-enabled/itssum +rm -f /etc/nginx/sites-enabled/default +nginx -t +systemctl restart nginx + +# CD가 docker-compose.yml과 .env를 올릴 폴더 +mkdir -p /home/ubuntu/itssum-app +chown ubuntu:ubuntu /home/ubuntu/itssum-app From 7a83b63df5109ea5bd616bc1bd955d0eee896c11 Mon Sep 17 00:00:00 2001 From: Chhun-Lee Date: Sun, 25 Oct 2026 01:20:51 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=EC=89=98=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20LF=20=EC=A4=84=EB=B0=94=EA=BF=88=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Windows에서 받은 ec2-user-data.sh가 CRLF로 바뀌어 EC2에서 실패하지 않도록 고정 --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 8af972c..abdc36e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /gradlew text eol=lf *.bat text eol=crlf *.jar binary +*.sh text eol=lf