Skip to content

Commit

Permalink
Merge pull request #21 from MOKY4/develop
Browse files Browse the repository at this point in the history
[FEATURE] 취미 테이블에 컨텐츠 URL 컬럼 추가 및 인프라 설명 수정

resolved #14
  • Loading branch information
CokeLee777 committed Jun 26, 2023
2 parents 05821a2 + 17fde14 commit 08d5e49
Show file tree
Hide file tree
Showing 57 changed files with 103 additions and 63 deletions.
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hollang-api-server

### 추론 서버: [Hollang-inferring-server](https://github.com/swyg-goorm/inferring_server)
### 추론 서버: [hollang-weeks52-inferring-server](https://github.com/MOKY4/hollang-weeks52-inferring-server)

## ⚙️ 개발 환경(Development Environment)

Expand Down Expand Up @@ -40,7 +40,7 @@
- **MySQL**
- **AWS RDS**

## 인프라 구조 및 CI/CD Pipeline
## 인프라 구조

### 전체 구조

Expand All @@ -62,38 +62,38 @@
- MySQL 서버는 MySQL Docker 공식 이미지를 받아와서 띄웠고, 데이터베이스의 데이터들은 컨테이너가 종료되어도 유지하도록 Docker Named Volume을 사용하여 인스턴스 종료시까지 유지할 수 있도록 하였다.
- EC2로의 인바운드 트래픽은 관리자가 인스턴스로 접속하여 관리할 수 있도록 SSH Port(22)를 열어서 Private Key를 보유한 관리자가 접속할 수 있도록 하였고, 또한 EC2 인스턴스로 직접 접속할 수 없도록 로드밸런서로부터 들어오는 클라이언트 트래픽을 받을 수 있도록 EC2 시큐리티 그룹을 로드밸런서와 공유하였다.

### 운영 환경
### 운영 및 스테이지 환경

![Production Infrastructure](asset/images/infrastructure_prod.png)

- EC2 인스턴스 두개에 각각 도커 컨테이너 1개씩(API Server, Inferring Server)를 띄워서 관리할 수 있도록 구성하였다.
- API Server가 띄워져있는 EC2를 제외한 Flask Server EC2 인스턴스는 Public으로 노출하지 않아서 외부로부터 트래픽을 받을 수 없게 설계하였다. 또한 API Server는 인스턴스 자체를 외부로 노출시키는 것이 아니라 로드밸런서로부터의 트래픽만 받을 수 있도록 설계하였다.
- EC2 인스턴스 한개에 각각 도커 컨테이너 1개씩(API Server, Inferring Server)를 띄워서 관리할 수 있도록 구성하였다.
- API Server가 띄워져있는 컨테이너를 제외한 Flask Server 컨테이너는 Public으로 노출하지 않아서 외부로부터 트래픽을 받을 수 없게 설계하였다. 또한 API Server가 띄워져 있는 컨테이너는 인스턴스 자체를 외부로 노출시키는 것이 아니라 로드밸런서로부터의 트래픽만 받을 수 있도록 설계하였다.
- MySQL 서버를 컨테이너로 띄우지 않는 이유는 AWS RDS를 사용하면 모니터링과 데이터 관리 등 운영하는데 필요한 유용한 기능들을 제공해주고, 인스턴스와 분리하여 메모리를 효율적으로 관리하기 위해서이다.
- 또한 AWS S3 버킷을 생성하여 애플리케이션 전반에 존재하는 이미지들을 저장하고, 배포할때 배포 버전을 관리할 수 있도록 하였다.
- 각각의 파일들은 버전별로 관리되게 구성하였다.
- EC2로의 인바운드 트래픽은 관리자가 인스턴스로 접속하여 관리할 수 있도록 SSH Port(22)를 열어서 Private Key를 보유한 관리자가 접속할 수 있도록 하였고, 또한 EC2 인스턴스로 직접 접속할 수 없도록 로드밸런서로부터 들어오는 클라이언트 트래픽을 받을 수 있도록 EC2 시큐리티 그룹을 로드밸런서와 공유하였다.

### CI/CD Pipeline
# CI/CD Pipeline

![CI/CD Pipeline](asset/images/ci_cd_pipeline.png)

1. 개발자가 GitHub 원격 저장소의 develop 또는 main 브랜치로 Push & Merge를 하면 이벤트 트리거가 작동한다.
2. 이벤트 트리거는 저장소에 존재하는 애플리케이션 소스코드를 다운로드받고 빌드하고 캐시하는 작업을 수행한다.
3. Github의 Open ID Connector(OIDC)는 AWS로부터 액세스 토큰을 요청하고, 받은 토큰을 사용해서 AWS 사용자 자격 증명을 수행하여 자원에 접근할 수 있게된다.
1. 개발자가 GitHub 원격 저장소의 develop, stage, main 브랜치로 Push & Merge를 하면 이벤트 트리거가 작동한다.
2. 이벤트 트리거는 저장소에 존재하는 애플리케이션 소스코드를 다운로드받고 빌드 및 테스트하고 캐시하는 작업을 수행한다.
3. AWS Access key id와 Secret Access Key를 사용하여 AWS로부터 액세스 토큰을 요청하고, 받은 토큰을 사용해서 AWS 사용자 자격 증명을 수행하여 자원에 접근할 수 있게된다.
4. 다운로드 받은 애플리케이션을 도커 이미지로 빌드해서 AWS ECR(Elastic Container Registry)에 푸쉬하고, AWS CodeDeploy를 위한 스크립트 파일들을 작성한다. 그리고 다운로드 받은 스크립트 파일들을 하나로 압축해서 AWS S3 버킷에 넣는다.
5. AWS CodeDeploy에서 새로운 배포를 생성한다.
6. 이 과정에서는 develop과 production이 다른 배포 전략으로 배포를 수행한다.
6. 이 과정에서는 develop과 stage & production이 다른 배포 전략으로 배포를 수행한다.
- develop 인스턴스는 무중단 배포가 필요없기 때문에 다음과 같이 한번에 새로운 배포를 하는 전략을 사용하여 구축하였다. 일단 CodeDeploy가 실행중이면 해당 인스턴스는 중지상태가 된다. 따라서 배포중에는 어떤 트래픽도 들어오지 못한다. 배포중에는 이전의 도커 컨테이너를 중지시키고 새로운 버전의 이미지로 도커 컨테이너를 띄우는 전략을 사용했다.
![AllAtOnce Strategy](asset/images/codedeploy_allatonce_strategy.png)

- production 인스턴스는 오토스케일링과 로드 밸런서를 이용해서 배포를 수행한다. 배포가 성공적으로 완료되면 오토 스케일링 그룹이 새로운 배포를 위한 새로운 인스턴스를 생성한다. 로드 밸런서는 새로운 버전의 인스턴스가 모든게 성공적으로 완료되면 이전의 인스턴스로 가던 트래픽을 끊고 새로운 인스턴스로의 트래픽을 전달하게 된다. 또한 새로운 버전을 배포하고 서버가 안정화가 되면 오토 스케일링 그룹이 이것을 인식하고 이전 버전의 인스턴스를 종료시킨다.
- stage & production 인스턴스는 오토스케일링과 로드 밸런서를 이용해서 배포를 수행한다. 배포가 성공적으로 완료되면 오토 스케일링 그룹이 새로운 배포를 위한 새로운 인스턴스를 생성한다. 로드 밸런서는 새로운 버전의 인스턴스가 모든게 성공적으로 완료되면 이전의 인스턴스로 가던 트래픽을 끊고 새로운 인스턴스로의 트래픽을 전달하게 된다. 또한 새로운 버전을 배포하고 서버가 안정화가 되면 오토 스케일링 그룹이 이것을 인식하고 이전 버전의 인스턴스를 종료시킨다.
![Blue/Green Strategy](asset/images/codedeploy_blue_green_strategy.png)
7. 배포하는 과정에서 이전에 S3에 저장했던 스크립트 파일들을 가져온다.

## 📝 테이블 정의서(Entity Details)
### 📝 테이블 정의서(Entity Details)

[Table Description](asset/images/Hollang_table_desc.pdf)
[Table Description](asset/images/HollangWeeks52_table_desc.xlsx)

## 🔗 엔티티-관계 모델(Entity Relationship Diagram)
### 🔗 엔티티-관계 모델(Entity Relationship Diagram)

![ERD](asset/images/Hollang_ERD.png)
[바로가기](https://www.erdcloud.com/d/5hkKS8x6Hr4KmBEFL)
Binary file added asset/images/HollangWeeks52_table_desc.xlsx
Binary file not shown.
Binary file removed asset/images/Hollang_ERD.png
Binary file not shown.
Binary file removed asset/images/Hollang_table_desc.pdf
Binary file not shown.
Binary file modified asset/images/infrastructure_prod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions src/main/kotlin/swyg/hollang/InitDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class InitService(
private lateinit var em: EntityManager

private final val INIT_DATA_PATH = "static/initData.xlsx"
private final val IMG_URL = "https://test.com"
private final val IMG_URL = "localhost:8080/static"

fun initFile(): Workbook {
val inputStream: InputStream = Thread.currentThread().contextClassLoader.getResourceAsStream(INIT_DATA_PATH)
Expand Down Expand Up @@ -83,8 +83,9 @@ class InitService(
val summary = row.getCell(2).stringCellValue
val description = row.getCell(3).stringCellValue
val imageName = row.getCell(4).stringCellValue
val imageUrl = "${IMG_URL}/images/hobby/$imageName.png"
val hobby = Hobby(originalName, shortName, summary, description, imageUrl)
val contentUrl = row.getCell(5).stringCellValue
val imageUrl = "${IMG_URL}/hobby/$imageName.png"
val hobby = Hobby(originalName, shortName, summary, description, imageUrl, contentUrl)
em.persist(hobby)
}
}
Expand All @@ -100,7 +101,7 @@ class InitService(
val name = row.getCell(0).stringCellValue
val description = row.getCell(1).stringCellValue
val mbtiType = row.getCell(2).stringCellValue
val imageUrl = "${IMG_URL}/images/hobby_type/${mbtiType}.png"
val imageUrl = "${IMG_URL}/hobby-type/${mbtiType}.png"
val fitHobbyTypes = mutableSetOf(
FitHobbyType(row.getCell(3).stringCellValue, 1),
FitHobbyType(row.getCell(4).stringCellValue, 2),
Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/swyg/hollang/config/WebConfig.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package swyg.hollang.config

import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import swyg.hollang.interceptor.LogInterceptor

Expand All @@ -18,4 +20,14 @@ class WebConfig: WebMvcConfigurer {
.addPathPatterns(LOG_INCLUDE_PATH)
.excludePathPatterns("/css/**", "/*.ico")
}
}

@Profile(value = ["local", "dev"])
@Configuration
class StaticResourceConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/image/**")
.addResourceLocations("classpath:/static/image/")
.setCachePeriod(60 * 60 * 24 * 365)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ data class GetRecommendationResponse(
val summary: String = hobbyEntity.summary
val description: String = hobbyEntity.description
val imageUrl: String = hobbyEntity.imageUrl
val contentUrl: String = hobbyEntity.contentUrl
val ranking: Int = hobbyRanking
}

Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/swyg/hollang/entity/Hobby.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Hobby (
@Column(name = "img_url", nullable = false)
val imageUrl: String,

@Column(name = "content_url", nullable = false)
val contentUrl: String,

) : BaseTimeEntity() {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/static/image/hobby/nft-art.png
Binary file modified src/main/resources/static/initData.xlsx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class HobbyControllerTest(
"홀랑 $i 요약",
"홀랑 $i 상세정보",
"default",
"https://example.com/hollang$i.png"
"https://example.com/hollang$i.png",
"https://weeks52.me"
)
hobby.recommendCount = 40L - i
em.persist(hobby)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ internal class RecommendationControllerTest(
.andExpect(jsonPath("$.data.recommendation.hobbies[0].summary").exists())
.andExpect(jsonPath("$.data.recommendation.hobbies[0].description").exists())
.andExpect(jsonPath("$.data.recommendation.hobbies[0].imageUrl").exists())
.andExpect(jsonPath("$.data.recommendation.hobbies[0].contentUrl").exists())
.andExpect(jsonPath("$.data.recommendation.hobbies[0].ranking").exists())
.andExpect(jsonPath("$.data.recommendation.fitHobbyTypes.length()", 3).exists())
.andExpect(jsonPath("$.data.recommendation.fitHobbyTypes[0].id").exists())
Expand Down Expand Up @@ -234,9 +235,9 @@ internal class RecommendationControllerTest(
em.persist(hobbyType2)
em.persist(hobbyType3)

val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트", "취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
em.persist(hobby1)
em.persist(hobby2)
em.persist(hobby3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,12 @@ class TestResponseControllerTest(
)
em.persist(hobbyType)

val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
em.persist(hobby1)
em.persist(hobby2)
em.persist(hobby3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,12 @@ internal class RecommendationManagerTest(
em.persist(hobbyType2)
em.persist(hobbyType3)

val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
em.persist(hobby1)
em.persist(hobby2)
em.persist(hobby3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ internal class TestResponseManagerTest(
)
em.persist(hobbyType)

val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기", "스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기", "어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com")
val hobby1 = Hobby("Adobe illustrator로 나만의 굿즈 만들기", "어도비 일러스트레이트",
"취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby2 = Hobby("사람들을 사로잡는 스토리텔링 시작하기",
"스토리텔링", "취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
val hobby3 = Hobby("Adobe InDesign으로 편집 디자인 시작하기",
"어도비 인디자인", "취미 요약정보", "취미 상세정보", "https://example.com", "https://weeks52.me")
em.persist(hobby1)
em.persist(hobby2)
em.persist(hobby3)
Expand Down
Loading

0 comments on commit 08d5e49

Please sign in to comment.