Skip to content

Commit 96c8797

Browse files
✨ 배포 및 CI/CD 구축 (#2)
* 💬 프로젝트 정상 실행 문구 출력 * ✨ 테스트용 컨트롤러 작성 * ✨ 로컬 빌드 테스트를 위해 Dockerfile 작성 * ♻️Refactor: 코드 포맷 적용 * 🎨 코드 포맷팅 적용 * ✨ 도커 허브로 이미지 받고 로컬에서 컨테이너 띄우기 * ✨ 워크플로 생성 * 💚 배포 job 추가 --------- Co-authored-by: uni-j-uni <[email protected]>
1 parent b96df6d commit 96c8797

File tree

17 files changed

+400
-259
lines changed

17 files changed

+400
-259
lines changed

.github/workflows/cicd.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: CI/CD to EC2 (GHCR via PAT)
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read # 패키지 푸시는 PAT로 처리
10+
11+
env:
12+
IMAGE: ghcr.io/ai-gongmo/app
13+
14+
jobs:
15+
build-and-push:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout (with submodules)
19+
uses: actions/checkout@v4
20+
with:
21+
submodules: recursive
22+
fetch-depth: 0
23+
24+
- name: Set up JDK 17
25+
uses: actions/setup-java@v4
26+
with:
27+
distribution: temurin
28+
java-version: "17"
29+
30+
- name: Build JAR
31+
run: ./gradlew --no-daemon clean build -x test
32+
33+
# PAT로 GHCR 로그인 (GITHUB_TOKEN 아님)
34+
- name: Log in to GHCR (PAT)
35+
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
36+
37+
- name: Build & Tag Docker image
38+
run: |
39+
docker build -f docker/Dockerfile -t $IMAGE:latest -t $IMAGE:${{ github.sha }} .
40+
41+
- name: Push Docker image
42+
run: |
43+
docker push $IMAGE:${{ github.sha }}
44+
docker push $IMAGE:latest
45+
46+
deploy:
47+
needs: build-and-push
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Deploy over SSH
51+
uses: appleboy/[email protected]
52+
with:
53+
host: ${{ secrets.EC2_HOST }}
54+
username: ${{ secrets.EC2_USER }}
55+
key: ${{ secrets.EC2_SSH_KEY }}
56+
script: |
57+
set -e
58+
cd ~/app
59+
# 최신 소스(서브모듈 포함) 갱신
60+
git pull --recurse-submodules
61+
62+
# 이번 커밋으로 배포 태그 고정
63+
sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=${{ github.sha }}/" docker/.env || true
64+
65+
# 컨테이너 갱신
66+
cd docker
67+
docker compose --env-file ./.env -f docker-compose.yml pull
68+
docker compose --env-file ./.env -f docker-compose.yml up -d --remove-orphans
69+
70+
# 사용 안 하는 이미지 정리
71+
docker image prune -af

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ src/main/resources/*.yml
4040
.vscode/
4141

4242
*/.env
43+
*.env

build.gradle

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,32 @@ tasks.named('test') {
4848
useJUnitPlatform()
4949
}
5050

51-
spotless {
52-
java {
53-
// Google JAVA Format 적용
54-
googleJavaFormat()
55-
// 아래 순서로 import 문 정렬
56-
importOrder('java', 'javax', 'jakarta', 'org', 'com')
57-
// 사용하지 않는 import 제거
58-
removeUnusedImports()
59-
// 각 라인 끝에 있는 공백을 제거
60-
trimTrailingWhitespace()
61-
// 파일 끝에 새로운 라인 추가
62-
endWithNewline()
63-
// 라이선스 헤더 추가
64-
licenseHeader '/* \n * Copyright (c) 팀명 \n */'
65-
// 어노테이션 정렬을 일관성 있게 유지
66-
formatAnnotations()
67-
// 들여쓰기를 공백 2칸으로 고정
68-
indentWithSpaces(2)
69-
// 파일 인코딩을 UTF-8로 강제
70-
encoding 'UTF-8'
71-
}
72-
}
51+
//spotless {
52+
// java {
53+
// // Google JAVA Format 적용
54+
// googleJavaFormat()
55+
// // 아래 순서로 import 문 정렬
56+
// importOrder('java', 'javax', 'jakarta', 'org', 'com')
57+
// // 사용하지 않는 import 제거
58+
// removeUnusedImports()
59+
// // 각 라인 끝에 있는 공백을 제거
60+
// trimTrailingWhitespace()
61+
// // 파일 끝에 새로운 라인 추가
62+
// endWithNewline()
63+
// // 라이선스 헤더 추가
64+
// licenseHeader '/* Copyright (c) 팀명 */'
65+
// // 어노테이션 정렬을 일관성 있게 유지
66+
// formatAnnotations()
67+
// // 들여쓰기를 공백 2칸으로 고정
68+
// indentWithSpaces(2)
69+
// // 파일 인코딩을 UTF-8로 강제
70+
// encoding 'UTF-8'
71+
// }
72+
//}
7373

74-
tasks.named('check') {
75-
dependsOn 'spotlessCheck'
76-
}
74+
//tasks.named('check') {
75+
// dependsOn 'spotlessCheck'
76+
//}
7777

7878
def profile = project.findProperty("profile") ?: "dev"
7979

@@ -85,4 +85,4 @@ tasks.named('bootJar') {
8585
doFirst {
8686
println ">>> Building with Spring profile: $profile"
8787
}
88-
}
88+
}

docker/Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# --- Build stage ---
2+
FROM eclipse-temurin:17-jdk AS build
3+
WORKDIR /workspace
4+
5+
# 캐시 최적화: Gradle 캐시 폴더 미리 준비
6+
COPY gradlew .
7+
COPY gradle ./gradle
8+
RUN chmod +x gradlew
9+
10+
# 소스 복사
11+
COPY build.gradle settings.gradle ./
12+
COPY src ./src
13+
14+
# 테스트 & 빌드 (테스트 생략하고 싶으면 test -x)
15+
RUN ./gradlew --no-daemon clean build -x test
16+
17+
# --- Run stage ---
18+
FROM eclipse-temurin:17-jre
19+
WORKDIR /app
20+
21+
# 빌드 산출물 복사 (jar 경로는 프로젝트에 맞게)
22+
COPY --from=build /workspace/build/libs/*-SNAPSHOT.jar /app/app.jar
23+
24+
# 외부에서 바인드 마운트로 주입할 설정 폴더
25+
# /app/config 에 prod properties 들어올 예정
26+
VOLUME ["/app/config", "/app/logs"]
27+
28+
EXPOSE 8080
29+
ENTRYPOINT ["java","-jar","/app/app.jar"]

docker/docker-compose.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: "3.9"
2+
3+
services:
4+
app:
5+
# 로컬
6+
# image: ${REGISTRY:-ghcr.io/aigongmo/app}:${IMAGE_TAG:-local}
7+
image: ${REGISTRY}:${IMAGE_TAG}
8+
container_name: app
9+
restart: always
10+
ports:
11+
- "${SERVER_PORT:-8080}:8080"
12+
environment:
13+
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
14+
SPRING_DATASOURCE_URL: ${DB_URL}
15+
SPRING_DATASOURCE_USERNAME: ${DB_USERNAME}
16+
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
17+
# 추가 환경변수가 있으면 여기에
18+
# SPRING_CONFIG_ADDITIONAL_LOCATION 로도 지정 가능
19+
SPRING_CONFIG_ADDITIONAL_LOCATION: "file:/app/config/"
20+
volumes:
21+
# 서브모듈에 있는 prod 설정을 컨테이너에 마운트
22+
- ../src/main/resources/application-prod.properties:/app/config/application-prod.properties:ro
23+
# 로그 폴더
24+
- ./logs:/app/logs

src/main/java/com/sku/aigongmo/AiGongmoApplication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ public class AiGongmoApplication {
88

99
public static void main(String[] args) {
1010
SpringApplication.run(AiGongmoApplication.class, args);
11+
System.out.println("프로젝트 정상 실행 완료");
1112
}
12-
1313
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
/*
2-
* Copyright (c) SKU K-IO-SK
3-
*/
41
package com.sku.aigongmo.global.common;
52

63
import jakarta.persistence.EntityListeners;
74
import jakarta.persistence.MappedSuperclass;
8-
import java.time.LocalDateTime;
95
import lombok.Getter;
106
import org.springframework.data.annotation.CreatedDate;
117
import org.springframework.data.annotation.LastModifiedDate;
128
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
139

10+
import java.time.LocalDateTime;
11+
1412
@Getter
1513
@MappedSuperclass
1614
@EntityListeners(AuditingEntityListener.class)
1715
public abstract class BaseTimeEntity {
1816

19-
@CreatedDate private LocalDateTime createdAt;
17+
@CreatedDate
18+
private LocalDateTime createdAt;
2019

21-
@LastModifiedDate private LocalDateTime modifiedAt;
20+
@LastModifiedDate
21+
private LocalDateTime modifiedAt;
2222
}
Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
1-
/*
2-
* Copyright (c) SKU K-IO-SK
3-
*/
41
package com.sku.aigongmo.global.config;
52

6-
import java.util.Arrays;
73
import org.springframework.beans.factory.annotation.Value;
84
import org.springframework.context.annotation.Bean;
95
import org.springframework.context.annotation.Configuration;
106
import org.springframework.web.cors.CorsConfiguration;
117
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
128

9+
import java.util.Arrays;
10+
1311
@Configuration
1412
public class CorsConfig {
1513

16-
@Value("${cors.allowed-origins}")
17-
private String[] allowedOrigins;
14+
@Value("${cors.allowed-origins}")
15+
private String[] allowedOrigins;
1816

19-
@Bean
20-
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
21-
CorsConfiguration configuration = new CorsConfiguration();
17+
@Bean
18+
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
19+
CorsConfiguration configuration = new CorsConfiguration();
2220

23-
// 환경 변수에 정의된 출처만 허용
24-
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins));
25-
// 리스트에 작성한 HTTP 메소드 요청만 허용
26-
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH"));
27-
// 리스트에 작성한 헤더들이 포함된 요청만 허용
28-
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "X-Requested-With"));
29-
// 모든 경로에 대해 위의 CORS 설정을 적용
30-
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
31-
source.registerCorsConfiguration("/**", configuration);
32-
return source;
33-
}
21+
// 환경 변수에 정의된 출처만 허용
22+
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins));
23+
// 리스트에 작성한 HTTP 메소드 요청만 허용
24+
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH"));
25+
// 리스트에 작성한 헤더들이 포함된 요청만 허용
26+
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "X-Requested-With"));
27+
// 모든 경로에 대해 위의 CORS 설정을 적용
28+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
29+
source.registerCorsConfiguration("/**", configuration);
30+
return source;
31+
}
3432
}

0 commit comments

Comments
 (0)