From 6b4743a5cf2e4e8e8682668ddb6408f94ce04788 Mon Sep 17 00:00:00 2001 From: jungdonggyu <1013as@naver.com> Date: Fri, 17 Nov 2023 21:25:41 +0900 Subject: [PATCH] Comment: "Update CI/CD" --- appspec.yml | 35 +++++++ build.gradle | 1 + docker/Dockerfile | 8 ++ docker/docker-compose.blue.yml | 12 +++ docker/docker-compose.green.yml | 8 ++ scripts/deploy.sh | 58 +++++++++++ .../global/config/SwaggerConfig.kt | 4 +- .../global/config/WebConfig.kt | 2 + .../global/resolver/authinfo/Auth.kt | 3 + .../yourssuAssigmnet/ArticleServiceTest.kt | 95 ------------------- 10 files changed, 129 insertions(+), 97 deletions(-) create mode 100644 appspec.yml create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.blue.yml create mode 100644 docker/docker-compose.green.yml create mode 100644 scripts/deploy.sh delete mode 100644 src/test/kotlin/yourssu/yourssuAssigmnet/ArticleServiceTest.kt diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..e67fa6f --- /dev/null +++ b/appspec.yml @@ -0,0 +1,35 @@ +version: 0.0 + +# Deploy 대상 서버의 운영체제를 표시 +os: linux + +# 코드 파일 전송과 관련된 설정 +files: + # 코드 파일의 소스 경로 + - source: / + # 코드 파일의 대상 경로 -> /home/ec2-user/app 디렉토리로 파일을 복사한다. + destination: /home/ec2-user/app + # 대상 경로에 이미 파일이 존재하는 경우, 덮어쓰기를 허용할지 여부 + overwrite: yes + +# 파일 및 디렉토리 권한에 관련된 설정 +permissions: + # 권한을 설정할 대상 경로 + - object: / + # 모든 파일 및 디렉토리를 의미 + pattern: "**" + # 파일 및 디렉토리의 소유자를 ec2-user로 설정 + owner: ec2-user + # 파일 및 디렉토리의 그룹을 ec2-user로 설정 + group: ec2-user + +# Deploy 전후에 실행할 스크립트 또는 명령에 관련된 설정 +hooks: + # 애플리케이션 시작시 실행할 스크립트 또는 명령에 관련된 설정 + ApplicationStart: + # 실행할 스크립트 또는 명령의 위치 + - location: deploy.sh + # 스크립트 또는 명령 실행의 제한 시간을 설정 + timeout: 60 + # CodeDeploy 중 실행되는 스크립트 또는 명령을 실행할 사용자를 지정 + runas: ec2-user \ No newline at end of file diff --git a/build.gradle b/build.gradle index ebb543b..ba55b92 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'io.springfox:springfox-boot-starter:3.0.0' //swagger implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation "org.mockito.kotlin:mockito-kotlin:3.2.0" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..173df23 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,8 @@ +### Docker 이미지를 생성할 때 기반이 되는 베이스 이미지를 설정한다. +FROM openjdk:11-jre-slim +### Dockerfile 내에서 사용할 변수 JAR_FILE을 정의한다. +ARG JAR_FILE=build/libs/*.jar +### JAR_FILE 경로에 해당하는 파일을 Docker 이미지 내부로 복사한다. +COPY ${JAR_FILE} yourssuAssignment.jar +### Docker 컨테이너가 시작될 때 실행할 명령을 지정한다. +ENTRYPOINT ["java","-jar","/yourssuAssignment.jar"] \ No newline at end of file diff --git a/docker/docker-compose.blue.yml b/docker/docker-compose.blue.yml new file mode 100644 index 0000000..7a2b78c --- /dev/null +++ b/docker/docker-compose.blue.yml @@ -0,0 +1,12 @@ +#blue +version: '3' +services: + # 서비스의 이름 + backend: + # 현재 디렉토리에서의 Dockerfile을 사용하여 Docker 이미지를 빌드 + build: .. + # 호스트의 8081 포트와 컨테이너의 80 포트를 매핑 + ports: + - "8081:80" + # 컨테이너의 이름 + container_name: spring-blue \ No newline at end of file diff --git a/docker/docker-compose.green.yml b/docker/docker-compose.green.yml new file mode 100644 index 0000000..e552b1f --- /dev/null +++ b/docker/docker-compose.green.yml @@ -0,0 +1,8 @@ +#green +version: '3' +services: + backend: + build: .. + ports: + - "8082:80" + container_name: spring-green \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..25d8896 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# 작업 디렉토리를 /home/ec2-user/app으로 변경 +cd /home/ec2-user/app + +# 환경변수 DOCKER_APP_NAME을 spring으로 설정 +DOCKER_APP_NAME=spring + + +# 실행중인 blue가 있는지 확인 +# 프로젝트의 실행 중인 컨테이너를 확인하고, 해당 컨테이너가 실행 중인지 여부를 EXIST_BLUE 변수에 저장 +EXIST_BLUE=$(sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up) + +# 배포 시작한 날짜와 시간을 기록 +echo "배포 시작일자 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + +# green이 실행중이면 blue up +# EXIST_BLUE 변수가 비어있는지 확인 +if [ -z "$EXIST_BLUE" ]; then + + # 로그 파일(/home/ec2-user/deploy.log)에 "blue up - blue 배포 : port:8081"이라는 내용을 추가 + echo "blue 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + + # docker-compose.blue.yml 파일을 사용하여 spring-blue 프로젝트의 컨테이너를 빌드하고 실행 + sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build + + # 30초 동안 대기 + sleep 30 + + # /home/ec2-user/deploy.log: 로그 파일에 "green 중단 시작"이라는 내용을 추가 + echo "green 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + + # docker-compose.green.yml 파일을 사용하여 spring-green 프로젝트의 컨테이너를 중지 + sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down + + # 사용하지 않는 이미지 삭제 + sudo docker image prune -af + + echo "green 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + +# blue가 실행중이면 green up +else + echo "green 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build + + sleep 30 + + echo "blue 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down + sudo docker image prune -af + + echo "blue 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + +fi + echo "배포 종료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log + + echo "===================== 배포 완료 =====================" >> /home/ec2-user/deploy.log + echo >> home/ec2-user/deploy.log \ No newline at end of file diff --git a/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/SwaggerConfig.kt b/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/SwaggerConfig.kt index cee6c9e..c9bf101 100644 --- a/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/SwaggerConfig.kt +++ b/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/SwaggerConfig.kt @@ -38,8 +38,8 @@ class SwaggerConfig{ //현재 @AuthInfo는 직접 입력하는게 아니라 토큰에서 가져오는 값이기에 무시처리 private fun apiInfo(): ApiInfo = ApiInfoBuilder() - .title("API 문서") - .description("API에 대해서 설명해주는 문서입니다.") + .title("유어슈 인큐베이팅 API 문서") + .description("유어슈 인큐베이팅 2주차까지의 API 문서입니다.") .version("1.0") .build() diff --git a/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/WebConfig.kt b/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/WebConfig.kt index 17ef5c4..4aa79ba 100644 --- a/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/WebConfig.kt +++ b/src/main/kotlin/yourssu/yourssuAssigmnet/global/config/WebConfig.kt @@ -10,7 +10,9 @@ class WebConfig( private val authInfoArgumentResolver: AuthInfoArgumentResolver ) : WebMvcConfigurer { + // 커스텀 argument resolver를 등록 override fun addArgumentResolvers(resolvers: MutableList) { + // 주입받은 `authInfoArgumentResolver`를 resolver 목록에 추가 resolvers.add(authInfoArgumentResolver) } } \ No newline at end of file diff --git a/src/main/kotlin/yourssu/yourssuAssigmnet/global/resolver/authinfo/Auth.kt b/src/main/kotlin/yourssu/yourssuAssigmnet/global/resolver/authinfo/Auth.kt index 4cae78a..7ca8b85 100644 --- a/src/main/kotlin/yourssu/yourssuAssigmnet/global/resolver/authinfo/Auth.kt +++ b/src/main/kotlin/yourssu/yourssuAssigmnet/global/resolver/authinfo/Auth.kt @@ -1,7 +1,10 @@ package yourssu.yourssuAssigmnet.global.resolver.authinfo +// 어노테이션의 적용 대상을 지정(파라미터) @Target(AnnotationTarget.VALUE_PARAMETER) +// 어노테이션이 유지될 기간을 정의(런타임에도 유지) @Retention(AnnotationRetention.RUNTIME) +// 문서포함여부 @MustBeDocumented annotation class Auth diff --git a/src/test/kotlin/yourssu/yourssuAssigmnet/ArticleServiceTest.kt b/src/test/kotlin/yourssu/yourssuAssigmnet/ArticleServiceTest.kt deleted file mode 100644 index b7a85e7..0000000 --- a/src/test/kotlin/yourssu/yourssuAssigmnet/ArticleServiceTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -package yourssu.yourssuAssigmnet - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.jupiter.MockitoExtension -import org.springframework.boot.test.context.SpringBootTest -import yourssu.yourssuAssigmnet.domain.article.entity.Article -import yourssu.yourssuAssigmnet.domain.article.repository.ArticleRepository -import yourssu.yourssuAssigmnet.domain.article.service.ArticleService -import yourssu.yourssuAssigmnet.domain.user.entity.User -import yourssu.yourssuAssigmnet.domain.user.service.UserService -import java.util.* -import javax.persistence.EntityNotFoundException - -@ExtendWith(MockitoExtension::class) -class ArticleServiceTest { - - @Mock - lateinit var articleRepository: ArticleRepository - - @Mock - lateinit var userService: UserService - - @InjectMocks - lateinit var articleService: ArticleService - - @Test - fun `test createArticle with valid user`() { - val email = "email@urssu.com" - val password = "password" - val article = Article(content = "content", title = "title") - val user = User(email = email, password = password, username = "username") - - `when`(userService.validateRegisterUser(email, password)).thenReturn(user) - `when`(articleRepository.save(any())).thenReturn(article) - - val result = articleService.createArticle(email, password, article) - - assertEquals(result, article) - assertEquals(result.user, user) - } - - @Test - fun `test updateArticle with validate author`() { - val articleId = 1L - val email = "email@urssu.com" - val password = "password" - val article = Article(content = "updated content", title = "updated title") - val preArticle = Article(content = "content", title = "title", user = User(email = email, password = password, username = "username")) - - `when`(articleRepository.findById(articleId)).thenReturn(Optional.of(preArticle)) - `when`(userService.validateUserAndAuthor(email, password, preArticle.user)).thenReturn(preArticle.user!!) - `when`(articleRepository.save(any())).thenReturn(preArticle) - - val result = articleService.updateArticle(articleId, email, password, article) - - assertEquals(result.title, article.title) - assertEquals(result.content, article.content) - } - - @Test - fun `test updateArticle with non-matching author`() { - val articleId = 1L - val email = "email@urssu.com" - val password = "password" - val article = Article(content = "updated content", title = "updated title") - val preArticle = Article(content = "content", title = "title", user = User(email = "email2@yourssu.com", password = password, username = "username2")) - - `when`(articleRepository.findById(articleId)).thenReturn(Optional.of(preArticle)) - - assertThrows { - articleService.updateArticle(articleId, email, password, article) - } - } - - @Test - fun `test deleteArticle with valid user`() { - val articleId = 1L - val email = "email@urssu.com" - val password = "password" - val article = Article(content = "content", title = "title", user = User(email = email, password = password, username = "username")) - - `when`(articleRepository.findById(articleId)).thenReturn(Optional.of(article)) - `when`(userService.validateUserAndAuthor(email, password, article.user)).thenReturn(article.user!!) - - articleService.deleteArticle(articleId, email, password) - - verify(articleRepository).deleteById(articleId) - } -} \ No newline at end of file