[SKKUDING] ‘codedang’ 시스템 아키텍쳐 설명서 (v0.2)
SKKUDING 멤버 모두가 현재 진행되고 있는 프로젝트 시스템의 구성 및 배포에 대해 충분히 공유되었으면 좋겠다는 바람을 담아 제작하였습니다. 모르는 것이 있거나, 제안해주실 것이 있다면 부담없이 같이 이야기하며 공부해보아요~!
1. VPC (Virtual Private Cloud)
2. IGW (Internet Gateway)
3. ECS (Elastic Container Service)
4. S3 (Simple Storage Service)
5. RDS (Relational Database System)
6. Amazon ElastiCache
7. MQ (Rabbit Message Queue)
8. On-Premise Server
분리된 가상 사설 네트워크 환경입니다. Private IP를 사용해야 하므로, 10.X.X.X, 172.16~31.X.X, 192.168.X.X 영역을 사용합니다. 외부 네트워크로부터 보안을 유지합니다.
설정한 VPC 안에서는, VPC네트워크로 설정한 IP대역 안에서, 배포를 하고자 하는 여러 어플리케이션의 IP대역을 따로 설정해주어야 합니다. 이 과정을 Subnetting 이라고 합니다. 예를들어, 10.0.0.0/24 로 VPC 네트워크 IP대역을 설정했다면, 10.0.0.0/26, 10.0.0.64/26, 10.0.0.128/26, 10.0.0.192/26 의 4개의 IP대역으로 Subnetting 할 수 있을 것입니다.
하지만 VPC안에 배포해야 하는 요소들은 외부에서 자유롭게 사용할 수 있는 것들(웹 서버) , 외부에서 접근하지 못하게 해야하는 민감한 것들 (DB)가 있습니다. 그래서 이러한 것들을 고려하여 분리하는 것이 Subnet 영역입니다.
Subnet 영역은 Public, Private 두가지로 나뉩니다. 외부에 접근을 허용하는 것을 Public Subnet으로, 허용하지 못하게 하는 것은 Private Subnet으로 나누어야 합니다. 한가지 유의해야할 점은 하나의 Subnet영역은 하나의 Availability Zone (이하 AZ)에 위치해야 한다는 점입니다.
그렇다면 VPC의 사설 네트워크를 사용하고 있는데, Public Subnet으로 배포를 한다고 한들, 어떻게 외부에서 허용이 가능할까요?
이는 바로 Internet Gateway의 역할이 있기 때문입니다.
인터넷 게이트웨이는 VPC와 외부 인터넷과의 통신을 담당합니다.
저희가 Public Subnet에 포함시킨 어플리케이션들은 모두 IGW와 연결되어야함은 당연합니다. 외부와 통신할 수 있는 통로가 있어야 하기 때문입니다.
그래서, 라우팅 테이블을 만들 때, IGW도 반드시 포함을 시켜야 합니다. IGW는 외부에서 VPC에 접속할 수 있는 Public IP와 내부 Public Subnet의 Private IP (외부에서 접속을 허용해야 하는 어플리케이션의 Private IP를 말함) 를 Translate하기 때문입니다. 라우팅 테이블에 IGW가 없으면, 어떤 Private IP가 VPC IP와 연관이 되어있는지 모르겠죠?
NAT도 추후에 살펴보겠습니다.
저희 서비스의 주가 되는 배포 환경이라고 할 수 있습니다. ECS는 도커 이미지를 활용하여 만들어진 컨테이너화된 어플리케이션을 보다 쉽게 배포하고, 관리하는 오케스트레이션 AWS 서비스입니다. 오케트스트레이션이라는 낯선 용어를 이해하기보다, 그냥 복잡한 배포 환경을 관리하기 쉽게 해주는 서비스라고 가볍게 생각하시면 됩니다.
저희가 배포해야할 것은 세가지라고 할 수 있습니다.
Nginx (웹서버)
Nest.js (백엔드)
Iris (채점서버)
각각의 개발사항을 담은 도커 이미지를 빌드하여, 컨테이너를 격리시켜야 합니다.
본격적인 설명에 앞서 ECS에 대해 먼저 알아봅시다.
위 그림은, ECS의 기본구조에 대한 그림인데요. 기본적으로 ECS는 Cluster를 정의하는 것 부터 시작합니다. 이는 도커 컨테이너를 실행할 수 있는 논리적인 공간입니다. 도커 컨테이너를 띄우기 위해서는 도커가 설치된 인스턴스가 반드시 필요합니다. 백엔드 개발환경(Nest.js이든, Spring Boot이든)을 만들기 위해 EC2 인스턴스에 그와 관련한 패키지, JDK를 다운로드해야 개발할 수 있는 것처럼, 도커 컨테이너를 띄우기 위해서는 도커가 설치된 인스턴스가 필요합니다. 이러한 인스턴스를 클러스터라고 부릅니다.
저희는 이 인스턴스로 Fargate(VM)를 사용합니다. EC2를 사용할 수도 있겠지만, EC2는 CPU,RAM,메모리에서 한계가 정해져 있으므로 트래픽이 몰리면, EC2가 병렬적으로 생성되면서 여러 신경써야할 점이 많습니다. 그러므로 저희는 Fargate를 사용하여, 이러한 관리의 부담을 덜기로 했습니다. (지갑은 아파요)
다음으로는 Task를 정의해야 합니다. Task란 컨테이너를 실행하는 최소 단위를 말합니다.애플리케이션을 구성하는 파라미터와 하나 이상의 컨테이너를 설명하는 JSON 형식의 텍스트 파일입니다.
태스크에서 사용할 도커 이미지나, 태스크에서 사용하는 CPU의 양과 같은 인프라적인 측면에 관한 정보, 컨테이너 죽었을 때 재시작 여부와 같은 정보들을 직접 정의할 수 있습니다.
마지막으로는 Service입니다. Task를 실행하기 위해 Service를 이용합니다. ALB같은 AWS 서비스를 사용하여, 원하는 태스크의 수를 유지하는 일들을 설정할 수 있습니다.
그럼 우리 서비스의 ECS구조에 대해 알아봅시다. 맨 처음에는, ECS 클러스터 인스턴스를 3개를 정의하려고 하였습니다. 배포하고자 하는 3가지 (Nginx, API-Server, Judge Server) 를 각각 다른 클러스터 인스턴스마다 하나의 Task Definition를 정의함으로써 말이죠.
그리고 Nginx를 2개의 AZ와 Public Subnet(10.0.1.0/24 - northeast2-a, 10.0.2.0/24 - northeast2-c)에 포함시키고, 외부 접속가능한 Public IP - Nginx의 Private IP를 ALB로 연동합니다. 사용자들의 트래픽은 로드밸런서를 통해 각 Nginx 서버 인스턴스에 접근하는 것이기 때문에, 도메인 및 HTTPS 인증은 모두 로드밸런서를 통해 한다는 사실을 알아둡시다. 그리고 Nginx가 포함된 인스턴스와 API-Server가 포함된 인스턴스가 통신하는 구조로 구성하려고 하였습니다. 아래 그림과 같습니다.
저희는 두가지를 염두해 두고 있었습니다. Nginx와 Backend(API-Server)를 하나의 클러스터 인스턴스에 같이 두는 것 (최종 결정된 Architecture), 아니면 위 그림처럼 클러스터 인스턴스를 서비스마다 다 따로 두고 Task를 각각 정의하는 것.
처음엔, 하나의 클러스터 인스턴스에 정의된 Task에 Nginx와 NestJS를 묶는 것은 낭비라고 생각했습니다. NestJS 컨테이너에 비해 Nginx 컨테이너는 굉장히 적은 리소스가 필요했기 때문입니다. 또한, Public Subnet과 Private Subnet이 완벽히 분리된다는 보안적인 측면에서도 따로 두는 것이 맞다고 생각했습니다.
하지만 실상은 그렇지 않았습니다. 구현 난이도도 난이도지만, 의미가 없는 행동임을 알게 되었습니다. Nginx와 NestJS를 하나의 클러스터 인스턴스에 Task로 묶을 때, Task Definition으로 직접 각 컨테이너들의 리소스를 제어할 수 있었고, 보안적인 측면도, Public Subnet에 클러스터 인스턴스가 포함되어 있어도, 로드밸런서와 연결된 Task 속 Port Mapping을 Nginx 서버만 해주면 되므로, 무리가 없습니다. 또한, 컨테이너가 많아지면서 생기는 Port 충돌 문제도 Dynamic Port Mapping으로 해결할 수 있습니다.
Frontend 의 빌드 파일을 저장할 용도로 S3를 사용합니다.
DB를 위한 AWS 서비스로 RDS를 사용합니다. 그림에 표시된 3번,5번,6번을 보시면 같은 Private Subnet에 묶여있는 것을 볼 수 있습니다. VPC에 대한 설명을 다시 읽어보시면 아시겠지만, 하나의 Subnet에는 반드시 하나의 AZ만 존재해야 합니다. 그리고, 특히 DB같은 경우에는 고가용성이 중요합니다. 만약 DB가 하나의 AZ에만 할당이 되어있었고, AZ를 담당하는 서버가 죽으면 많이 곤란해질 것입니다.
따라서 저희는 ECS,RDS,Cache 관련 서비스를 여러 AZ에 포함시키기 위해 AZ의 개수만큼 Private Subnet을 설정하여 해당 서비스들을 하나의 Subnet에 포함하여 배포할 예정입니다.
Redis 사용예정
Judge 서버 연동을 위하여 MQ를 사용합니다. 트래픽도 트래픽이지만, On-Premise 서버를 이용하기에는 MQ가 좋다고 합니다.
저희 물리 서버에요.
nginx 리버스 프록시
-> 내부 서버에서의 통로 역할 (포워드 프록시는 사용자로 가는 길의 통로역할)