Skip to content

Commit

Permalink
[CICD] AWS 배포 CICD 구축 (#494)
Browse files Browse the repository at this point in the history
* refactor: ApiFolderErrorCode 오타 수정 (#468)

* [REFACTOR] 픽 리팩토링, 검색 테스트 구현 (#474)

* refactor: FolderType label 수정

* refactor: 파라미터 idList -> pickIdList 명시적으로 변경

* refactor: folder 내부에 있는 pick_order 리스트에 pickId가 중복 생성되는 문제 해결

* refactor: root folder 검색 못하도록 예외 처리

* refactor: pick controller 메서드명 수정

* refactor: DB fleid 함수를 이용하여 정렬된 픽 리스트 조회할 수 있는 메서드 구현

* refactor: 픽 검색 시 폴더 리스트가 null인 경우 검증하지 않도록 변경, tag 검증 추가

* refactor: 테스트 코드 작성

* refactor: 테스트 코드 ParameterizedTest로 리팩토링 (#476)

* [FIX] 픽 수정 리팩토링 및 검증로직 개선 (#477)

* refactor: 불필요한 초기값 제거

* fix: 엔티티를 list로 조회시 존재하지 않는 엔티티를 조회하려고 하면 NOT_FOUND 예외 발생

* fix: pick update시 부모폴더 id도 변경 가능하도록 수정, 누락된 검증 로직 추가

* fix: 기능 변경에 의한 테스트 코드 수정

* fix: idList로 폴더 조회시 존재하지 않는 폴더id로 조회하면 예외 발생

* refactor: 파라미터명 명확하게 수정 idList -> folderIdList

* feat: 폴더 응답에 생성 수정 일자 추가 (#485)

* cicd: aws 배포 action 추가

---------

Co-authored-by: Sangwon Yang <[email protected]>
  • Loading branch information
Gyaak and sangwonsheep authored Nov 19, 2024
1 parent abc3c9f commit d53ce07
Show file tree
Hide file tree
Showing 20 changed files with 856 additions and 255 deletions.
102 changes: 102 additions & 0 deletions .github/workflows/aws-api-module-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Tech-pick Api-Module CI/CD

on:
push:
branches:
- "release" # 배포 대상 브랜치
paths:
- 'backend/techpick-core/**'
- 'backend/techpick-api/**'
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
env:
api-version: 'v2'

steps:
# 저장소 Checkout
- name: Checkout source code
uses: actions/checkout@v4

# Gradle 실행 권한 부여
- name: Grant execute permission to gradlew
run: chmod +x ./backend/gradlew

# JDK 설치
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'

# Spring boot application, Docker image 빌드
- name: Build and Deploy techpick-api Module
run: |
echo "Building and deploying techpick-api..."
./backend/gradlew -p backend/techpick-api clean build -x test
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-api-${{ github.sha }} backend/techpick-api
- # Docker hub 로그인
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- # Docker hub 업로드
name: Publish to docker hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-api-${{ github.sha }}

- name: Deploy on AWS
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_API_SERVER_IP }}
port: 22
username: ${{ secrets.AWS_USERNAME }}
key: ${{ secrets.AWS_ACCESS_KEY }}
script: |
echo "Login to Docker Hub for private repository access on bastion..."
echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
echo "Pulling Docker image on bastion..."
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-api-${{ github.sha }}
echo "Tagging Docker image..."
docker tag minkyeki/techpick:${{ env.api-version }}-api-${{ github.sha }} techpick:${{ env.api-version }}-api-staging
cd /home/ubuntu
rm -rf .env
touch .env
echo "DOCKER_MYSQL_USERNAME=${{ secrets.DOCKER_MYSQL_USERNAME }}" >> .env
echo "DOCKER_MYSQL_PASSWORD=${{ secrets.DOCKER_MYSQL_PASSWORD }}" >> .env
echo "DOCKER_MYSQL_DATABASE=${{ secrets.DOCKER_MYSQL_DATABASE }}_${{ env.api-version }}" >> .env
echo "DOCKER_MYSQL_URL=jdbc:mysql://techpick-mysql:3306/${{ secrets.DOCKER_MYSQL_DATABASE }}_${{ env.api-version }}?createDatabaseIfNotExist=true" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
echo "JWT_ISSUER=${{ secrets.JWT_ISSUER }}" >> .env
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env
echo "Restarting techpick-api service..."
docker-compose stop techpick-api
docker-compose rm -f techpick-api
docker-compose up -d techpick-api
echo "Cleanup Images..."
docker image prune -af
- name: Discord Webhook Notification
uses: sarisia/[email protected]
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: ${{ job.status }}
title: "TechPick ${{ env.api-version }} - Api Deployment Result"
description: "AWS 배포가 완료되었습니다."
color: 0xff91a4
url: "https://github.com/sarisia/actions-status-discord"
username: GitHub Actions
102 changes: 102 additions & 0 deletions .github/workflows/aws-batch-module-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Tech-pick Batch-Module CI/CD

on:
push:
branches:
- "release" # 배포 대상 브랜치
paths:
- 'backend/techpick-core/**'
- 'backend/techpick-batch/**'
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
env:
api-version: 'v2'

steps:
# 저장소 Checkout
- name: Checkout source code
uses: actions/checkout@v4

# Gradle 실행 권한 부여
- name: Grant execute permission to gradlew
run: chmod +x ./backend/gradlew

# JDK 설치
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'

# Spring boot application, Docker image 빌드
- name: Build and Deploy techpick-batch Module
run: |
echo "Building and deploying techpick-batch..."
./backend/gradlew -p backend/techpick-batch clean build -x test
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-batch-${{ github.sha }} backend/techpick-batch
- # Docker hub 로그인
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- # Docker hub 업로드
name: Publish to docker hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-batch-${{ github.sha }}

- name: Deploy on AWS
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_API_SERVER_IP }}
port: 22
username: ${{ secrets.AWS_USERNAME }}
key: ${{ secrets.AWS_ACCESS_KEY }}
script: |
echo "Login to Docker Hub for private repository access on bastion..."
echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
echo "Pulling Docker image on bastion..."
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/techpick:${{ env.api-version }}-batch-${{ github.sha }}
echo "Tagging Docker image..."
docker tag minkyeki/techpick:${{ env.api-version }}-batch-${{ github.sha }} techpick:${{ env.api-version }}-batch-staging
cd /home/ubuntu
rm -rf .env
touch .env
echo "DOCKER_MYSQL_USERNAME=${{ secrets.DOCKER_MYSQL_USERNAME }}" >> .env
echo "DOCKER_MYSQL_PASSWORD=${{ secrets.DOCKER_MYSQL_PASSWORD }}" >> .env
echo "DOCKER_MYSQL_DATABASE=${{ secrets.DOCKER_MYSQL_DATABASE }}_${{ env.api-version }}" >> .env
echo "DOCKER_MYSQL_URL=jdbc:mysql://techpick-mysql:3306/${{ secrets.DOCKER_MYSQL_DATABASE }}_${{ env.api-version }}?createDatabaseIfNotExist=true" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
echo "JWT_ISSUER=${{ secrets.JWT_ISSUER }}" >> .env
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env
echo "Restarting techpick-batch service..."
docker-compose stop techpick-batch
docker-compose rm -f techpick-batch
docker-compose up -d techpick-batch
echo "Cleanup Images..."
docker image prune -af
- name: Discord Webhook Notification
uses: sarisia/[email protected]
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: ${{ job.status }}
title: "TechPick ${{ env.api-version }} - Batch Deployment Result"
description: "AWS 배포가 완료되었습니다."
color: 0xff91a4
url: "https://github.com/sarisia/actions-status-discord"
username: GitHub Actions
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package techpick.api.application.folder.dto;

import java.time.LocalDateTime;
import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -11,6 +12,8 @@ public record FolderApiResponse(
@Schema(example = "GENERAL")
FolderType folderType,
Long parentFolderId,
List<Long> childFolderIdOrderedList
List<Long> childFolderIdOrderedList,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public ResponseEntity<List<PickApiResponse.FolderPickList>> getFolderChildPickLi
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공")
})
public ResponseEntity<PickSliceResponse<PickApiResponse.Pick>> searchPickAndRssList(
public ResponseEntity<PickSliceResponse<PickApiResponse.Pick>> searchPick(
@LoginUserId Long userId,
@Parameter(description = "조회할 폴더 ID 목록", example = "1, 2, 3") @RequestParam(required = false, defaultValue = "") List<Long> folderIdList,
@Parameter(description = "검색 토큰 목록", example = "리액트, 쿼리, 서버") @RequestParam(required = false, defaultValue = "") List<String> searchTokenList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public record Read(
public record Update(
@Schema(example = "1") @NotNull Long id,
@Schema(example = "Record란 뭘까?") String title,
@Schema(example = "3") Long parentFolderId,
@Schema(example = "[4, 5, 2, 1]") List<Long> tagIdOrderedList
) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package techpick.api.domain.folder.dto;

import java.time.LocalDateTime;
import java.util.List;

import techpick.core.model.folder.FolderType;
Expand All @@ -11,6 +12,8 @@ public record FolderResult(
Long parentFolderId,
Long userId,
List<Long> childFolderIdOrderedList,
List<Long> childPickIdOrderedList
List<Long> childPickIdOrderedList,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ public enum ApiFolderErrorCode implements ApiErrorCode {
CANNOT_DELETE_FOLDER_NOT_IN_RECYCLE_BIN
("FO-004", HttpStatus.BAD_REQUEST, "휴지통 안에 있는 폴더만 삭제할 수 있음", ErrorLevel.MUST_NEVER_HAPPEN()),
INVALID_FOLDER_TYPE
("F0-005", HttpStatus.NOT_IMPLEMENTED, "미구현 폴더 타입에 대한 서비스 요청", ErrorLevel.MUST_NEVER_HAPPEN()),
("FO-005", HttpStatus.NOT_IMPLEMENTED, "미구현 폴더 타입에 대한 서비스 요청", ErrorLevel.MUST_NEVER_HAPPEN()),
BASIC_FOLDER_ALREADY_EXISTS
("F0-006", HttpStatus.NOT_ACCEPTABLE, "기본 폴더는 1개만 존재할 수 있음.", ErrorLevel.MUST_NEVER_HAPPEN()),
("FO-006", HttpStatus.NOT_ACCEPTABLE, "기본 폴더는 1개만 존재할 수 있음.", ErrorLevel.MUST_NEVER_HAPPEN()),
INVALID_MOVE_TARGET
("F0-007", HttpStatus.NOT_ACCEPTABLE, "이동하려는 폴더들의 범위가 올바르지 않음", ErrorLevel.SHOULD_NOT_HAPPEN()),
("FO-007", HttpStatus.NOT_ACCEPTABLE, "이동하려는 폴더들의 범위가 올바르지 않음", ErrorLevel.SHOULD_NOT_HAPPEN()),
INVALID_PARENT_FOLDER
("FO-008", HttpStatus.NOT_ACCEPTABLE, "부모 폴더가 올바르지 않음", ErrorLevel.SHOULD_NOT_HAPPEN()),
// TODO: folder depth 추가 시 예외 삭제 예정
ROOT_FOLDER_SEARCH_NOT_ALLOWED
("FO-009", HttpStatus.NOT_ACCEPTABLE, "루트 폴더에 대한 검색은 허용되지 않음.", ErrorLevel.SHOULD_NOT_HAPPEN()),
;

private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,9 @@ public static ApiFolderException INVALID_MOVE_TARGET() {
public static ApiFolderException INVALID_PARENT_FOLDER() {
return new ApiFolderException(ApiFolderErrorCode.INVALID_PARENT_FOLDER);
}

public static ApiFolderException ROOT_FOLDER_SEARCH_NOT_ALLOWED() {
return new ApiFolderException(ApiFolderErrorCode.ROOT_FOLDER_SEARCH_NOT_ALLOWED);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public record Create(Long userId, String title, List<Long> tagIdOrderedList, Lon
LinkInfo linkInfo) {
}

public record Update(Long userId, Long id, String title, List<Long> tagIdOrderedList) {
public record Update(Long userId, Long id, String title, Long parentFolderId, List<Long> tagIdOrderedList) {
}

public record Move(Long userId, List<Long> idList, Long destinationFolderId, int orderIdx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.apache.commons.lang3.ObjectUtils;
import org.springframework.data.domain.Slice;
Expand All @@ -12,22 +13,38 @@
import techpick.api.domain.folder.exception.ApiFolderException;
import techpick.api.domain.pick.dto.PickCommand;
import techpick.api.domain.pick.dto.PickResult;
import techpick.api.domain.tag.exception.ApiTagException;
import techpick.api.infrastructure.folder.FolderDataHandler;
import techpick.api.infrastructure.pick.PickQuery;
import techpick.api.infrastructure.tag.TagDataHandler;
import techpick.core.model.folder.Folder;
import techpick.core.model.folder.FolderType;
import techpick.core.model.tag.Tag;

@Service
@RequiredArgsConstructor
public class PickSearchService {

private final PickQuery pickQuery;
private final FolderDataHandler folderDataHandler;
private final TagDataHandler tagDataHandler;

@Transactional(readOnly = true)
public Slice<PickResult.Pick> searchPick(PickCommand.Search command) {
List<Long> folderIdList = command.folderIdList();
for (Long folderId : folderIdList) {
validateFolderAccess(command.userId(), folderId);
List<Long> tagIdList = command.tagIdList();

if (ObjectUtils.isNotEmpty(folderIdList)) {
for (Long folderId : folderIdList) {
validateFolderAccess(command.userId(), folderId);
validateFolderRootSearch(folderId);
}
}

if (ObjectUtils.isNotEmpty(tagIdList)) {
for (Long tagId : tagIdList) {
validateTagAccess(command.userId(), tagId);
}
}

return pickQuery.searchPick(command.userId(), folderIdList,
Expand All @@ -36,9 +53,23 @@ public Slice<PickResult.Pick> searchPick(PickCommand.Search command) {
}

private void validateFolderAccess(Long userId, Long folderId) {
Folder parentFolder = folderDataHandler.getFolder(folderId);
Folder parentFolder = folderDataHandler.getFolder(folderId); // 존재하지 않으면, FOLDER_NOT_FOUND
if (ObjectUtils.notEqual(userId, parentFolder.getUser().getId())) {
throw ApiFolderException.FOLDER_ACCESS_DENIED();
}
}

private void validateFolderRootSearch(Long folderId) {
Folder parentFolder = folderDataHandler.getFolder(folderId); // 존재하지 않으면, FOLDER_NOT_FOUND
if (Objects.equals(parentFolder.getFolderType(), FolderType.ROOT)) {
throw ApiFolderException.ROOT_FOLDER_SEARCH_NOT_ALLOWED();
}
}

private void validateTagAccess(Long userId, Long tagId) {
Tag tag = tagDataHandler.getTag(tagId); // 존재하지 않으면, TAG_NOT_FOUND
if (!userId.equals(tag.getUser().getId())) {
throw ApiTagException.UNAUTHORIZED_TAG_ACCESS();
}
}
}
Loading

0 comments on commit d53ce07

Please sign in to comment.