Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/src/test
.env
discodeit.env
/.logs
/.discodeit
/logs
docker-compose.yml
Dockerfile
.github
*.md
101 changes: 101 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
on:
push:
branches:
- release
env:
ECR_REPOSITORY_NAME: public.ecr.aws/h3g7d6d9
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
# 1) 소스 체크아웃
- name: Checkout
uses: actions/checkout@v3

# 2) AWS 자격 증명 설정 (Secrets 사용)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ vars.AWS_REGION }}

# 3) ECR 로그인 (계정 ID를 Secret에서 참조)
- name: ECR Login
run: |
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin ${{env.ECR_REPOSITORY_NAME}}

# 5) 이미지 태그 구성
- name: Prepare tags
id: prep
run: |
echo "IMAGE_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV

- name: Docker 이미지 빌드 및 푸시 태그:커밋해시
id: build-image
run: |
docker buildx build \
-t "${{vars.ECR_REPOSITORY_URI}}:latest" \
-t "${{vars.ECR_REPOSITORY_URI}}:$IMAGE_TAG" \
--push .

# 9) 작업 정의 템플릿 렌더링
- name: Render task definition
run: |
sudo apt-get update -y && sudo apt-get install -y gettext-base
export IMAGE_TAG=${{ env.IMAGE_TAG }}
envsubst < ecs-task-def.json > task-def.rendered.json
echo "=== Rendered task def ==="
cat task-def.rendered.json

# 🔧 10) jq 설치 (JSON 파싱용)
- name: Install jq
run: sudo apt-get install -y jq

# ✅ 11) 작업 정의 등록 & ARN 추출
- name: Register task definition and extract ARN
id: register_task
run: |
REVISION_JSON=$(aws ecs register-task-definition \
--cli-input-json file://task-def.rendered.json)
TASK_DEF_ARN=$(echo $REVISION_JSON | jq -r '.taskDefinition.taskDefinitionArn')
echo "TASK_DEF_ARN=$TASK_DEF_ARN" >> $GITHUB_ENV

- name: ECS 서비스 중단
run: |
aws ecs update-service \
--cluster ${{vars.ECS_CLUSTER}} \
--service ${{vars.ECS_SERVICE}} \
--desired-count 0
aws ecs wait services-stable \
--cluster ${{vars.ECS_CLUSTER}} \
--service ${{vars.ECS_SERVICE}}

# ✅ 12) ECS 서비스 업데이트 (정확한 리비전 사용)
- name: ECS 서비스 업데이트
run: |
aws ecs update-service \
--cluster ${{ vars.ECS_CLUSTER }} \
--service ${{ vars.ECS_SERVICE }} \
--task-definition $TASK_DEF_ARN \
--desired-count 1 \
--force-new-deployment

# 13) 배포 완료 대기
- name: ECS 배포 완료 대기
run: |
aws ecs wait services-stable \
--cluster ${{ vars.ECS_CLUSTER }} \
--services ${{ vars.ECS_SERVICE }}

# 14) 배포 상태 확인
- name: ECS 배포 상태 확인
run: |
aws ecs describe-services \
--cluster ${{ vars.ECS_CLUSTER }} \
--services ${{ vars.ECS_SERVICE }} \
--query "services[0].deployments"
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
on:
pull_request:
branches: [ "main" ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: JDK 17 설정
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'

- name: gradlew 실행 권한
run: chmod +x gradlew

- name: Jacoco Test
run: ./gradlew clean test jacocoTestReport

- name: CodeCov 커버리지
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: build/reports/jacoco/test/jacocoTestReport.
fail_ci_if_error: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.env
discodeit.env
.logs

### IntelliJ IDEA ###
.idea/modules.xml
Expand Down
55 changes: 55 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ====== build args는 반드시 FROM보다 위에 선언 ======
# 도커 빌드 시 사용할 빌드/런타임 이미지 이름을 변수로 지정
# ARG는 선언된 이후부터만 유효하므로 반드시 FROM보다 위에 있어야 함
ARG BUILDER_IMAGE=gradle:7.6.0-jdk17
ARG RUNTIME_IMAGE=amazoncorretto:17.0.7-alpine

# ============ (1) Builder ============
# 빌더 스테이지 시작: 지정한 Gradle + JDK 환경을 사용
FROM ${BUILDER_IMAGE} AS builder

# 루트 권한으로 변경 (권한 설정/폴더 생성 작업을 위해)
USER root
# 애플리케이션 작업 디렉토리 설정
WORKDIR /app
# Gradle 캐시 디렉토리 경로를 환경 변수로 설정 (빌드 속도 향상)
ENV GRADLE_USER_HOME=/home/gradle/.gradle
# Gradle 캐시 디렉토리와 앱 디렉토리 소유자를 gradle 유저로 변경
RUN mkdir -p $GRADLE_USER_HOME && chown -R gradle:gradle /home/gradle /app
# gradle 유저로 변경 (보안 및 권한 문제 방지)
USER gradle

# Gradle Wrapper 스크립트 복사 (빌드 실행에 필요)
COPY --chown=gradle:gradle gradlew ./
# gradle 폴더 복사 (wrapper 설정 및 실행 환경)
COPY --chown=gradle:gradle gradle ./gradle
# Gradle 설정 파일 복사 (빌드 스크립트)
COPY --chown=gradle:gradle build.gradle settings.gradle ./
# gradlew 실행 권한 부여
RUN chmod +x ./gradlew
# 의존성만 먼저 다운로드하여 캐시 활용 (코드 변경 없이 재사용 가능)
RUN ./gradlew --no-daemon --refresh-dependencies dependencies || true

# 실제 소스코드 복사 (이 시점 이후 변경 시 빌드 다시 수행됨)
COPY --chown=gradle:gradle src ./src
# 애플리케이션 빌드 (테스트 제외, 속도 향상)
RUN ./gradlew clean build --no-daemon --no-parallel -x test

# ============ (2) Runtime ============
# 런타임 스테이지: 빌드 결과 실행에 필요한 최소한의 경량 이미지 사용
FROM ${RUNTIME_IMAGE}
# 앱 실행 디렉토리 지정
WORKDIR /app

# 애플리케이션이 사용하는 포트 노출
EXPOSE 80
# Spring Boot 프로필을 운영(prod)으로 설정
ENV PROJECT_NAME="discodeit"
ENV PROJECT_VERSION="1.2-M8"
ENV JVM_OPTS=""

# 빌드 스테이지에서 생성한 JAR 파일만 복사
COPY --from=builder /app/build/libs/*.jar app.jar

# 컨테이너 시작 시 JAR 실행
ENTRYPOINT ["java", "-jar", "app.jar", "--server.port=80"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[![codecov](https://codecov.io/github/NanHangBok/4-sprint-mission/branch/sprint8/graph/badge.svg?token=KYTI3YEL3E)](https://codecov.io/github/NanHangBok/4-sprint-mission)
29 changes: 26 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.6'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.7'
id 'jacoco'
}

group = 'com.sprint.mission'
version = '0.0.1-SNAPSHOT'
version = '2.0-M9'

java {
toolchain {
Expand All @@ -28,7 +29,6 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand All @@ -38,8 +38,31 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.apache.httpcomponents.client5:httpclient5'
implementation 'software.amazon.awssdk:s3:2.31.7'

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

// test 소스
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
useJUnitPlatform()
}

test {
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
}
}
43 changes: 43 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
services:
postgres:
image: postgres:15
container_name: postgres-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "15432:5432"
volumes:
- discodeit-data:/var/lib/postgresql/data
- ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend

app:
build: .
container_name: spring-app
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILE}
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
AWS_S3_ACCESS_KEY: ${AWS_S3_ACCESS_KEY}
AWS_S3_SECRET_KEY: ${AWS_S3_SECRET_KEY}
AWS_S3_REGION: ${AWS_S3_REGION}
AWS_S3_BUCKET: ${AWS_S3_BUCKET}
PRESIGNED-URL-EXPIRATION: ${AWS_S3_PRESIGNED_URL_EXPIRATION}
ports:
- "8081:8080"
depends_on:
- postgres
volumes:
- ./${STORAGE_LOCAL_ROOT_PATH}:/${STORAGE_LOCAL_ROOT_PATH}
networks:
- backend

networks:
backend:

volumes:
discodeit-data: { }
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing

@SpringBootApplication
public class DiscodeitApplication {
public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.sprint.mission.discodeit.auth;

import com.sprint.mission.discodeit.dto.UserDto;
import com.sprint.mission.discodeit.entity.Role;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;

@Getter
@RequiredArgsConstructor
public class DiscodeitUserDetails implements UserDetails {
private final UserDto userDto;
private final String password;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return createAuthorities(userDto.role());
}

@Override
public String getUsername() {
return userDto.username();
}

@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}

@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}

@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}

@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}

@Override
public boolean equals(Object o) {
if (!(o instanceof DiscodeitUserDetails that)) return false;
return Objects.equals(getUserDto().id(), that.getUserDto().id());
}

@Override
public int hashCode() {
return Objects.hashCode(getUserDto().id());
}

public List<GrantedAuthority> createAuthorities(Role role) {
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
}
}
Loading