Blog

[NestJS] 21. AWS 클라우드 서비스와 배포

Category
Author
citeFred
citeFred
PinOnMain
1 more property
NestJS, TypeORM, Angular, AWS 배포 이해하기
Table of Content

1. AWS란?

Amazon Web Service
아마존에서 제공하는 클라우드 컴퓨팅 서비스 플랫폼
AWS는 다양한 IT 리소스를 인터넷을 통해 제공하며, 사용자는 필요에 따라 서버, 스토리지, 데이터베이스, 네트워킹, 머신 러닝, 분석 도구 등 다양한 서비스를 이용할 수 있음
AWS는 스타트업부터 대기업까지 다양한 규모의 기업에서 사용되며, 클라우드 기반의 애플리케이션 개발, 데이터 저장, 웹 호스팅 등 여러 용도로 활용
AWS의 강점
확장성
스케일링 : 사용자는 필요에 따라 리소스를 쉽게 확장하거나 축소
Scale Up/Down : 수직 확장, 간단히 컴퓨터 스펙자체를 올리는 행위라 이해
Scale Out/In : 수평 확장, 간단히 컴퓨터를 병렬로 여러대를 배치하는 행위로 이해
비용 효율성
사용한 만큼만 비용을 지불하는 Pay-as-you-go 모델을 채택하고 있어 초기 투자 비용이 낮음
다양한 서비스
컴퓨팅, 스토리지, 데이터베이스, 네트워킹, 보안 등 웹 서비스를 위해 필요한 부분을 전반적으로 관리
통합 서비스
서버와 각종 서비스간의 연결 설정을 간편화하여 기존 개발자가 직접 설정해야하는 여러 연결 설정 부분을 최소화 시켜 개발에 집중 할 수 있는 환경을 제공
글로벌 인프라
전 세계 여러 지역에 데이터 센터를 운영하여 높은 가용성과 낮은 지연 시간을 제공
보안
강력하고 검증된 보안 기능과 인증 절차를 통해 데이터 보호를 강화
AWS를 선택하는 이유?
AWS는 아래와 같은 이유로 인해 클라우드 배포에서 가장 인기 있는 선택지로 클라우드 서비스의 성장과 함께 AWS의 사용률은 계속 증가하고 있음
클라우드 서비스 시장 점유율
AWS는 클라우드 인프라 서비스(IaaS) 및 플랫폼 서비스(PaaS) 시장에서 약 30% 이상의 점유율을 보유
주요 클라우드 제공업체 중 가장 높은 수치
광범위한 고객 기반
AWS는 수백만 개의 활성 고객을 보유하고 있으며, 스타트업, 중소기업, 대기업, 공공 기관 등 다양한 산업에서 사용
예를 들어, Netflix, Airbnb, NASA 등 유명 기업들이 AWS를 활용
다양한 서비스
AWS는 200개 이상의 서비스와 기능을 제공하여 배포, 데이터 저장, 분석, 머신 러닝 등 다양한 요구를 충족
강력한 개발자 생태계
AWS는 활발한 개발자 커뮤니티와 방대한 문서, 튜토리얼, 교육 자료를 제공하여 사용자가 쉽게 배포하고 관리할 수 있도록 돕고있음
참고할 레퍼런스가 많다는 것은 그만큼 빠르게 배포 관련 설정 할 수 있고 비지니스 로직 개발에 더 집중 할 수 있는 장점

2. AWS 사용 준비

2.1 회원 등록(최초 1회)

AWS 회원 가입
링크를 통해서 회원 가입 페이지로 이동
아래와 같이 root 계정(앞으로 사용할 관리 메인 계정) email 주소를 입력
계정 이름(account name = nick name 변경가능)을 입력
Verify email address 버튼으로 이메일 인증
이메일을 통해 인증 코드 획득
이메일을 인증 확인
비밀번호 설정
회원 기본 정보 입력
결재 정보 입력
카드 정보 인증
회원 SMS 인증
이용 플랜 선택(Free Tier)
회원 가입 완료

2.2 AWS 콘솔 사용 준비하기

AWS 콘솔 들어가기
우측 상단의 Console로 들어가면 다음과 같은 링크로 이동하게 된다.
우측 상단의 네비게이션을 살펴보면 Ohio로 미국 동부가 기본적으로 선택되어 있다.
→ 이것을 Asia Pacific(Seoul) 로 변경해준다. 서비스의 기본 서버 위치이다.
즐겨찾기 설정해두기
콘솔의 좌측 상단을 살펴보면 Services 탭으로 모든 AWS 서비스를 살펴 볼 수 있으며
Search 탭을 통해 원하는 Service를 검색 할 수 있다.
하지만, AWS는 200개 이상의 서비스가 있으므로 일일히 찾아서 사용하기엔 번거롭기 때문에 주요 서비스들을 미리 즐겨찾기로 등록해두고자 한다.
자주 사용되며 이 샘플에서 사용될 서비스들을 아래처럼 검색 필드에 각 이름을 입력하면서 동적으로 나타나는 결과를 즐겨찾기 해둔다.
IAM
RDS
S3
CloudFront
Route 53
EC2
Certificate Manager
이제 아래와 같이 각 서비스를 즐겨찾기를 통해서 빠르게 접근 할 수 있다.

3. AWS 사용해보기

현재 AWS를 통한 목표는 다음과 같다.
로컬 → 클라우드 환경으로 전환 부분
AWS 회원 가입 및 IAM 권한 설정
로컬 MySQL 데이터베이스 데이터베이스 RDS로 변경
로컬 파일 시스템 업로드 경로 → 저장소 S3로 변경
Cloudfront CDN 파일 제공 설정
로컬 테스트 서버 실행 환경 → 실제 서비스 EC2 인스턴스로 변경
nest-js-board-frontend 프론트엔드 Client 서버
로컬 테스트 서버 실행 환경 → 실제 서비스 프론트 정적배포 실행 환경 S3로 변경
클라이언트 테스트 진입 경로 → 도메인 연결(Route 53, CloudFront)실제 클라이언트 진입 경로
보안
HTTP → HTTPS (ACM, SSL, Cloudfront) (SSR인경우는 ELB인데 CSR이라 Cloudfront가 책임짐)
대용량 파일 스트리밍 → Cloudfront(CDN)

3.1 AWS IAM 접근 권한 설정

3.1.1 IAM(Amazon Identity and Access Management)

Amazon IAM이란?
아마존 웹 서비스(AWS)에서 제공하는 사용자가 AWS 리소스에 대한 접근을 안전하게 관리할 수 있도록 돕는 기능
IAM을 사용하면 사용자, 그룹, 역할 및 정책을 설정하여 AWS 리소스에 대한 권한을 세밀하게 조정
사용자 및 그룹 관리
IAM을 통해 여러 사용자 계정을 생성하고, 이들을 그룹으로 묶어 권한을 관리
정책 기반 접근 제어
JSON 형식의 정책을 사용하여 특정 AWS 서비스에 대한 접근 권한을 정의하여 사용자 또는 그룹이 어떤 리소스에 접근할 수 있는지 세부적으로 설정
역할(Role) 및 임시 자격 증명
IAM 역할을 사용하여 특정 서비스나 애플리케이션이 다른 AWS 리소스에 접근할 수 있도록 임시 자격 증명을 부여
예를 들어, EC2 인스턴스가 S3에 접근할 때 IAM 역할을 사용
다단계 인증(MFA)
IAM에서는 다단계 인증(MFA)을 설정하여 추가적인 인증 수단을 요구하여 계정을 보호
로그 및 모니터링
IAM과 AWS CloudTrail을 연동하여 사용자 활동을 로그하고 모니터링

3.1.2 MFA(Multi-Factor Authentication) 설정

MFA란?
MFA(다단계 인증, Multi-Factor Authentication)는 사용자 계정의 보안을 강화하기 위한 인증 방식
MFA는 특히 중요한 정보나 자산을 다루는 서비스에서 필수적인 보안 수단으로 자리 잡고 있음
간단히 다단 인증 설정이라고 볼 수 있다.
예로 우리는 평소에 어떤 서비스에서 로그인 시 이메일 인증 또는 SMS인증 등 추가 인증을 설정하던 것을 생각 하면 된다.
IAM에서 루트 계정의 MFA 설정하기
AWS 콘솔 →  IAM (즐겨찾기 또는 검색)
IAM Dashboard 콘솔 →  Add MFA
Select MFA device
Device name →  MFA 이름 구분
MFA Device Authenticator app
본인의 스마트폰에서 Google Authenticator 다운로드
Google Authenticator →  코드 추가(
Set up device →  스마트폰으로 QR코드 인식
Google Authenticator →  OTP 임시번호가 자동 로딩됨
Set up Device →  OTP 인증 번호 연속 입력 2회(30초마다 갱신기다리고 2차 입력)
MFA - Google Authenticator 등록 완료
이후 로그인마다 해당 앱을 실행시켜 인증 번호 입력을 해야 함

3.1.3 Access Key 발급

Access Key란?
AWS API에 접근할 때 사용되는 중요한 인증 정보
프로젝트 등 에서 AWS 각종 서비스를 사용 할 때(ex: NestJS에서 AWS SDK 모듈을 통해 AWS 서비스를 연결 할 때 등) 필요한 정보이다.
AWS 계정 사용 권장사항
Root 계정은 모든 권한을 가지고 있기 때문에 보안에 취약한 상태라면 해커의 공격타겟이 될 가능성이 높다.
실제로 수많은 사례들이 나타나고 있으며, 작성자 본인도 공격에 수십만원의 서비스 이용료가 청구된 적이 있었다.
하지만 외부 공격이라는 것을 입증만 하면 해당 청구금액은 환불 시킬 수 있으나, 과정 자체가 Amazon과 직접 컨택해야 하는 과정, 요구 조치들을 이해해야 하는 과정 등 시간이 소요되므로 사전에 MFA, Group/User 설정을 하는 기본 조치들에 신경써나가야 한다.
따라서 Root 계정을 직접 사용하지 않고 않는 것은 AWS 공식 문서에서도 강력히 권장되고 있다.
어떠한 팀 프로젝트를 진행한다고 가정하고 아래와 같이 권한 그룹을 만들고 유저를 생성하는 것이 좋다.
싱글 프로젝트인 경우에도 프로젝트별로 나누는것이 좋음
Group 생성
IAM 콘솔 → Access management →  User Group  Create group
Create user group
Name the group → User group name →  그룹 이름 예제에선 “boardapp-aws-group”
Add users to the group - 그룹 생성 후 진행
Attach permissions policies
해당 프로젝트에서 사용되는 모든  AWS 서비스를 검색 서비스명FullAccess 체크
 IAM
 RDS
 RDS Data
 S3
 CloudFront
 EC2
 Route 53
 Certificate Manager 각각 Full Access 정책 등록
→ 이후  Create user group
 Group 생성 완료
User 생성
IAM 콘솔 → Access management →  Users  Create user
Specify user details
User name → boardapp-fred처럼 해당 어플리케이션-이름 으로 구분해두면 프로젝트별 인원을 관리하기 편리해진다.
다른 팀원들은 boardapp-alice, boardapp-tom 처럼 추가해나가면 된다.
→ 이후  Next
Set permissions
Permission options →  Add user to group
User groups →  직전에 만든 프로젝트 그룹 예제에선 boardapp-aws-group선택
→ 이후  Next
Review and create →  확인 후 Create user
 User 생성 완료 → 해당 User name 을 눌러서 상세 정보 확인
생성된 User 의 상세 정보 →  Security credentials 탭 이동
Security credentials → Multi-factor authentication(MFA) →  Assign MFA device
Root 계정의 MFA 설정과 마찬가지로 해당 User의 MFA 설정
생성 User인  boardapp-fred의 MFA 설정 완료
Security credentials → Access keys →  Create access key
Access key best practices & alternatives
Use case →  Local code
Confirmation →  I understood..
→ 이후  Next
Set description tag(생략)  Create access key
Retrieve access keys →  Access key, Secret access key 개인 저장
절대 공유 금지( github에 코드상으로도 공유되지 않도록 은닉화(.env) 신경 써야 함)
개인 PC 메모장, 개인 카톡 등에 기록해 둘 것
→ 이후  Done
생성 User의  MFA + Secret Key 까지 생성 완료
Security credentials → Console sign-in →  Enable console access
이 부분은 현재 AWS 웹 콘솔에 로그인하는 설정임
Enable console access → Console password
Root 계정 주인 본인인 경우 Custom password로 해당 User의 패스워드 지정해도 됨
Root 계정의 팀원이 초대되는 경우
 Autogenerated password로 자동 비밀번호로 생성해서 공유해주고
→ 이후 해당 팀원이 첫 로그인하면 비밀번호를 본인이 변경 할 수 있도록 하단  User must create new password at next sign-in 을 켜줌
 Enable console access
팀원인 경우 이런 페이지를 공유해줘서 첫 로그인 할 수 있도록 함
위 sign-in URL에 표기된 숫자가 ID
Username 이 사용자 이름
패스워드는 초기 랜덤 패스워드(이후 변경됨)
다음 처럼 Root 계정을 로그아웃하고
Root 계정 로그인이 아닌, IAM User 로그인 을 가정하면 위 정보대로 입력
User 생성시 추가한  User용 MFA 인증번호 입력 (Root 계정용 아님)
초기 비밀번호 확인 및 신규 비밀번호 설정 화면
로그인 이후 IAM 서비스로 들어가보면 다음과 같이 Root과 동일하게 페이지가 나타난다.
이는 권한이 있기 때문
해당 User group에 권한을 부여하지 않은 다른 서비스로 이동해보면 다음과 같이 권한 없음이 나타남

3.2 AWS RDS 사용하기

3.2.1 RDS(Amazon Relational Database Service)

Amazon RDS란?
아마존 웹 서비스(AWS)에서 제공하는 관리형 관계형 데이터베이스 서비스
RDS는 데이터베이스의 설정, 운영, 유지보수를 자동화하여 사용자가 데이터베이스에만 집중할 수 있도록 돕는 서비스
관리형 서비스
RDS는 데이터베이스 인스턴스의 프로비저닝, 패치, 백업, 복구 등의 관리 작업을 자동화, 개발자는 이러한 작업에 시간을 할애할 필요가 없음
다양한 데이터베이스 엔진 지원
RDS는 여러 데이터베이스 엔진을 지원
대표적으로 MySQL, PostgreSQL, MariaDB, Oracle, SQL Server 등 대중적인 DB선택 폭을 가지고 있음
확장성
RDS는 필요에 따라 쉽게 인스턴스를 확장하거나 축소할 수 있음(Auto Scaling)
데이터베이스의 성능 요구에 따라 CPU, 메모리, 스토리지 용량을 조정
고가용성
Multi-AZ 배포를 통해 고가용성을 제공
장애 발생 시 자동으로 대체 인스턴스로 전환하는 기술로 서비스의 연속성을 보장
보안
AWS Identity and Access Management(IAM)를 통해 데이터베이스에 대한 세밀한 접근 제어가 가능하며, 데이터 암호화 기능도 지원
자동 백업
RDS는 자동으로 데이터베이스 백업을 수행하여 데이터 손실을 방지하고, 필요 시 특정 시점으로 복원할 수 있는 기능을 제공

3.2.2 RDS로 MySQL Database 인스턴스 생성하기

RDS로 클라우드 데이터 베이스 만들기 따라하기
AWS 콘솔 →  RDS (즐겨찾기 또는 검색)
Amazon RDS 콘솔 → Region 선택(지역 선택) →  Asia Pacific(Seoul)
Databases →  Create database
Choose a database creation method →  Standard create
개발환경에서의 MySQL 버전 확인 → 터미널 → mysql —version
Engine options
Engine Type →  MySQL
Edition →  MySQL Community
Engine version →  MySQL 8.0.39 (*개발환경과 왠만하면 맞출것)
Templates →  Free tier
Availability and durability (Free tier 자동 비활성화)
Settings
DB instance identifier → rds-mysql-boardapp 과 유사하게 본인 프로젝트명으로 변경
Credentials Settings
Master username →  admin 과 유사하게 팀에 공유될 아이디이므로 개인아이디 X
Credentials management → Self managed
Master password →  팀에 공유될 패스워드이므로 개인비밀번호 X
Confirm master password → 비밀번호 재입력확인
Instance configuration → 클라우드 PC 사양 선택 프리티어로 가장 작은  db.t3.micro
이는 대여할 PC의 CPU, RAM을 선택하는것과 같음
Storage
Storage type →  SSD(GP2)
이는 대여할 PC의 SSD 하드디스크를 선택하는것과 같음
Allocated storage →  20 GiB(Giga bytes)
Storage autoscaling →  Enable storage autoscaling 체크 해제
자동 용량 확장 옵션 해제
Connectivity
Compute resource →  Don’t connect to an EC2 compute resource
VPC →  Default VPC
DB subnet group →  default
Public access →  YES
New VPC security group name
Availablity Zone →  No preference
Certificate authority →  default
Database port →  3306
Tags
Database authentication
Mornitoring
Additional configuration
Database options
initial database name →  “boardapp”처럼 DB이름
 Create Database
생성 후 몇분 정도 초기화 및 생성 과정 대기
 RDS DB 생성 완료
해당 인스턴스 이름을 클릭하면 세부 정보를 확인 할 수 있음
AWS에서는 이와 같이 특정 서비스들이 동작하는 렌탈 클라우드 PC를 인스턴스라 함

3.2.3 RDS 연결 설정

보안그룹 설정
RDS 인스턴스 상세정보 → Security →  VPC security groups
Security Groups →  Security group ID 클릭
sg-073dc77…. - default 상세 정보 →  Edit Inbound rules
Add rule
Type →  MySQL/Aurora
Source →  My IP → ip주소 자동입력됨
 Save rules
Inbound 규칙 생성 확인
DBeaver 및 MySQL 터미널 등으로 접속 시도
new Connection
host name = RDS 인스턴스 상세정보에서 Endpoint 복사&붙여넣기
port = 3306 그대로
database = RDS 인스턴스 생성시 설정한 DB name (이샘플에선 boardapp)
username = RDS 인스턴스 생성시 설정한 User name (이샘플에선 admin)
password = RDS 인스턴스 생성시 설정한 Password

3.2.4 백엔드 API 서버와 RDS의 연결

위 처럼 GUI 툴에서 접속 테스트가 완료된다면, 실제로 접속이 가능하다는 상태
NestJS 백엔드 프로젝트를 열어보자
.env
우리는 DB 접속 정보를 .env 파일로 환경변수로 다루며 은닉화 시켜 왔다.
민감 정보 은닉화를 필요한 이유가 이와 같은 AWS 서비스를 사용 할 때 외부인의 접속을 제한하기 위함이다.(해커의 공격 위험성 = 무자비한 데이터 공격가능성 = 비용$$)
이는 실제 수도 없이 요금 폭탄 사고가 발생하는 매우 흔한일이다. (본인실제경험포함)
다행히 AWS는 1회에 이상현상에 대해서 비용을 제외해주지만 처리를 모두 AWS 직원(영어)와 통화 또는 이메일로 수도없이 주고 받고 보안 조치를 확인하고 비용제외해주는 번거로움이 있음
.env에 다음과 같이 DB 접속 정보를 본인의 인스턴스에 맞게 수정한다.
# DATABASE 설정 정보 DB_HOST=RDS인스턴스의Endpoint복사붙여넣기 DB_PORT=3306 DB_USERNAME=admin DB_PASSWORD=설정한비밀번호입력 DB_NAME=boardapp # JWT Secret Key ...
Shell
복사
서버를 실행시키면 TypeORM의 테이블 자동 생성 기능에 따라 엔터티에 맞추어 RDS에 테이블을 생성하는 것을 볼 수 있다.
DBeaver말고도 터미널로 들어가는것이 편한사람은 아래같은 명령어를 입력하는데 Endpoint 부분만 본인것으로 수정하면 된다.
mysql -h {본인의RDS Endpoint} -P 3306 -u {본인의RDS Username} -p
Shell
복사

3.3 AWS S3 사용하기(CDN)

3.3.1 S3(Amazon Simple Storage Service)

Amazon S3란?
아마존 웹 서비스(AWS)에서 제공하는 객체 스토리지(저장소) 서비스
S3는 인터넷을 통해 대량의 데이터를 안전하게 저장하고 관리할 수 있는 기능을 제공, 사용자는 파일을 업로드하고 다운로드하며, 데이터를 효율적으로 관리
무제한 스토리지
S3는 사용자가 필요에 따라 용량을 조정할 수 있어, 데이터의 양에 제한이 없음
고가용성 및 내구성
S3는 99.999999999% (11 9s) 내구성을 제공하며, 데이터의 손실 없이 안전하게 저장. 또한, 여러 지역에 분산 저장되어 높은 가용성을 유지.
다양한 데이터 관리 옵션
객체에 대한 메타데이터를 설정할 수 있으며, 버전 관리, 라이프사이클 정책 등을 통해 데이터를 효율적으로 관리
보안
AWS IAM을 통해 접근 권한을 관리하고, 데이터 암호화 기능을 제공하여 데이터를 안전하게 보호
비용 효율성
사용한 만큼만 요금을 지불하는 요금 체계로, 데이터 저장 및 전송에 대한 비용을 효율적으로 관리

3.3.2 S3 인스턴스 생성하기

S3로 클라우드 저장소(Bucket) 생성하기
AWS 콘솔 →  S3 (즐겨찾기 또는 검색)
Amazon RDS 콘솔 → Region 선택(지역 선택) →  Asia Pacific(Seoul)
Buckets Create bucket
General configurationBucket name s3-bucket-boardapp처럼 Unique한 이름
먼저 이름을 유니크하게 설정하여 버킷을 생성한 후, 필요에 따라 모든 설정을 자유롭게 변경할 수 있음
Object Ownership
Block Public Access settings for this bucket
Bucket Versioning
Tags -optional
Availability and durability (Free tier 자동 비활성화)
Default encryption
Advanced settings
 Create bucket
 S3 Bucket 생성 완료
해당 인스턴스 이름을 클릭하면 첫 Object 탭을 볼 수 있으며 저장소 파일들을 볼 수 있음
샘플로 파일을 업로드 하기 위해 Upload 버튼으로 이동 → 이미지1개와 영상1개를 업로드 테스트
업로드 성공 상태

3.3.3 백엔드 API 서버와 S3의 연결

위 처럼 웹 AWS 콘솔에서 파일 업로드 테스트가 완료된다면, 실제로 S3 저장소 인스턴스는 생성된 것이다.
NestJS 백엔드 프로젝트를 열어보자
.env
우리는 DB 접속 정보를 .env 파일로 환경변수로 다루며 은닉화 시켜 왔다.
이 정보는 은닉화에 신경써야 의도하지 않은 공격으로부터 보호 할 수 있다.
아래와 같이 발급받은 키를 입력하여 환경변수로 받아서 사용 할 수있도록 준비 해둔다.
# DATABASE 설정 정보 ... # JWT Secret Key ... # File Upload PATH (Local Test용) # STORAGE_PATH=/Users/inyongkim/Documents/Projects/localStorage # AWS Keys AWS_ACCESS_KEY_ID=발급받은본인의ACCESSKEY AWS_SECRET_ACCESS_KEY=발급받은본인의SECRETKEY # 외부 API관련 Key 등 필요 시 추가
Shell
복사
AWS-SDK 의존성 설치
NestJS 프로젝트에서 AWS 서비스와 쉽게 연결하기 위해서는 aws-sdk를 설치해야 한다.
아래 명령어를 통해 aws-sdk 모듈들을 설치해준다.
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Shell
복사
s3.service.ts
기존 파일 업로드 서비스를 S3로 처리해 줄 서비스 계층을 하나 생성한다.
생성자를 통해 S3 객체를 생성하면서 이 때 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 를 통해 해당 백엔드 API 서버가 권한이 있는 관리자임을 인증하게 된다.
PutObjectCommand 클래스를 통해 S3에 파일을 전송 할 수 있는 객체로 만들어내고 S3send메서드를 통해 해당 객체를 S3 저장소로 전송하여 파일을 업로드하게 된다.
import { Injectable } from '@nestjs/common'; import { S3 } from '@aws-sdk/client-s3'; import { PutObjectCommand } from '@aws-sdk/client-s3'; @Injectable() export class S3Service { private s3: S3; constructor() { this.s3 = new S3({ region: 'ap-northeast-2', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, }); } async uploadFile(file: Express.Multer.File, bucketName: string): Promise<string> { const encodedFileName = encodeURIComponent(`${Date.now()}-${file.originalname}`); const command = new PutObjectCommand({ Bucket: bucketName, Key: encodedFileName, Body: file.buffer, ContentType: file.mimetype, }); await this.s3.send(command); return `https://${bucketName}.s3.amazonaws.com/${encodedFileName}`; // 반환 URL } }
TypeScript
복사
attachment-upload.service.ts
위 작성된 S3 Service를 호출하는 첨부파일 업로드 비지니스 로직 부분이다.
크게 변경된 점은 없지만 변경된 것은 파일을 S3에 저장하는 로직이 작성된 s3Service를 호출한다는 것
추가로 S3 생성 시 설정한 저장소의 이름 ‘s3-bucket-boardapp’을 인수로 전달하는 점이 변경되었다.
@Injectable() export class AttachmentUploadService { constructor( @InjectRepository(Attachment) private readonly attachmentRepository: Repository<Attachment>, private readonly s3Service: S3Service, ) {} // 파일 업로드 async uploadFile(file: Express.Multer.File) { try { const fileUrl = await this.s3Service.uploadFile(file, 's3-bucket-boardapp'); return { message: 'File uploaded successfully', url: fileUrl, }; } catch (err) { console.error('S3 Upload Error:', err); throw new HttpException('Failed to upload file', HttpStatus.INTERNAL_SERVER_ERROR); } } // 파일 엔터티 데이터베이스에 저장 async save(file: Attachment) { try { return await this.attachmentRepository.save(file); } catch (err) { throw new HttpException('Failed to save file', HttpStatus.INTERNAL_SERVER_ERROR); } } }
TypeScript
복사
이와 동일하게 프로필 사진 업로드 로직이 작성된 profile-picture-upload.service.ts도 변경되었다.
모듈화를 위해서 분리해 둔 것일 뿐 위 게시글의 첨부파일 코드와 거의 동일한 코드 구조이다.
S3 Service를 호출하는 것으로 변경된 것도 똑같다.
@Injectable() export class ProfilePictureUploadService { constructor( @InjectRepository(ProfilePicture) private readonly profilePictureRepository: Repository<ProfilePicture>, private readonly s3Service: S3Service, ) {} // 프로필 사진 파일 업로드 async uploadProfilePicture(file: Express.Multer.File) { try { const fileUrl = await this.s3Service.uploadFile(file, 's3-bucket-boardapp'); return { message: 'File uploaded successfully', url: fileUrl, }; } catch (err) { console.error('S3 Upload Error:', err); throw new HttpException('Failed to upload file', HttpStatus.INTERNAL_SERVER_ERROR); } } // 프로필 사진 엔터티 데이터베이스에 저장 async save(file: ProfilePicture) { try { return await this.profilePictureRepository.save(file); } catch (err) { throw new HttpException('Failed to save file', HttpStatus.INTERNAL_SERVER_ERROR); } } }
TypeScript
복사

3.4 AWS CloudFront 사용하기(CDN 역할)

3.4.1 Amazon CloudFront Service

Amazon CloudFront란?
아마존 웹 서비스의 .html, .css, .js이미지 파일과 같은 정적 및 동적 웹 콘텐츠를 사용자에게 더 빨리 배포하도록 지원하는 웹 서비스
CDN(Content Delivery Network)의 기능
CloudFront는 전 세계 여러 지역에 분산된 서버(엣지 로케이션)를 통해 사용자에게 콘텐츠를 빠르게 제공
이를 통해 웹사이트와 애플리케이션의 성능을 향상시키고, 지리적 거리와 관계없이 빠른 응답 속도를 제공
CloudFront는 정적 및 동적 콘텐츠를 모두 지원하며, 원본 서버를 보호하고 트래픽을 감소시키는 데 도움을 줌
Cache(캐싱 서버)의 기능
CloudFront는 요청된 콘텐츠를 엣지 로케이션에 저장하여, 이후의 요청에 대해 빠르게 응답할 수 있는 캐싱 기능을 제공
이를 통해 원본 서버에 대한 요청 수를 줄이고, 데이터 전송 비용을 절감하며, 사용자에게 빠른 로딩 속도를 제공
캐시된 콘텐츠의 유효 기간(TTL)을 설정할 수 있어, 콘텐츠가 갱신될 때까지 기존 캐시를 활용할 수 있음
CloudFront를 사용하는 이유의 이해 위한 현재까지의 상황 정리
AWS RDS에 데이터베이스가 연결되어 정보(문자열)들이 저장되고 있다.
AWS S3에 실제 파일이 업로드 되고 있다.
→ S3에 저장된 파일의 경로(URL) 문자열은 RDS의 파일 관련 테이블 저장되며 S3의 주소값이다.
public access를 막는 이유?
백엔드 서버는 쓰기/읽기 권한이 있는 Access Key로 접근하기 떄문에 업로드가 가능한 상태이다.
그럼 S3 저장소의 파일을 public으로 접근 할 수 있는 중간 매개체가 필요하며 이것이 CDN(Content Delivery Network)의 역할이다.
AWS에서는 이런 CDN기능을 CloudFront 서비스로 구현하게 된다.
이후 CloudFront를 통해 실제 외부 접근 가능한 파일의 경로를 URL 값으로 DB에 입력하는 것이 목표이다.

3.4.2 CloudFront 설정하기

CloudFront로 CDN 구축 따라하기
CloudFront Dashboard →  Create distribution
Create distribution
Origin
Origin domain →  생성했던 연결할 S3 버킷 선택
Name →  자동 입력됨
Origin access →  Origin access control settings(recommanded)
S3 버킷은 public access를 비공개로 했던 설정이기 때문에 CF는 S3에 인증해서 접근 할 수 있도록 설정하는 부분
 Create new OAC 인증 설정 생성 후 자동 입력
Default cache behavior
View protocol policy →  HTTP and HTTPS
Allowed HTTP methods →  GET, HEAD
Restrict view access →  YES
이를 통해 회원 전용 영상, 이미지와 같이 제한을 설정 할 수 있음
Add key groups → 아래 토글 내용 전체 진행 생성한 키 그룹 선택
 그 외 기본 설정 선택
Function associations - optional
Web Application Firewall (WAF) →  Do not enable security protections
Settings
Price class →  Use NA,Euro, Asia, M/E, Africa 선택(과금 비용 최소화)
Alternate domain name(CNAME) 기본값 → 이후 프론트엔드 배포 시 추가 설정
Custom SSL certificate - optional 빈값 → 이후 프론트엔드 배포 시 추가 설정
Supported HTTP versions →  HTTP/2  HTTP/3
Default root object - optional 빈값 → 이후 프론트엔드 배포 시 추가 설정
Standard logging →  On + S3변경 팝업 활성화 → Log prefix →  CloudFrontLogs/ 입력(s3의 해당폴더에 쌓임)
그 외 기본값 그대로 →  Create distribution
 CloudFront 설정 생성 완료
CloudFront에서 S3 폴더 경로에 따른 권한 변경이 필요 →  Behavior탭  Create behavior
첫번째 모두 접근 가능한 경로
Create behavior
Path pattern →  public/* 입력
Origin and origin groups →  생성했던 S3 선택
Cache policy →  S3 선택시 자동 선택 됨(CachingOptimized for S3)
그 외 기본 설정 →  Create behavior
두번째 Access Key가 필요한 경로
Create behavior
Path pattern →  private/* 입력(이부분은 정하기 나름)
Origin and origin groups →  생성했던 S3 선택
Restrict view access →  YES → Add key groups →  생성한 키 그룹 선택
Cache policy →  S3 선택시 자동 선택 됨(CachingOptimized for S3)
그 외 기본 설정 →  Create behavior
경로에 따른 권한 설정 완료(프로젝트에 맞게 수정할 것)
S3에 public, private 폴더 만들기
위 두가지 접근 권한(공개 및 AccessKey 필요비공개)경로를 설정했기 때문에 실제로 S3에 해당 폴더들이 위와 같은 공개 상태를 유지 할 수 있다.
AWS S3 콘솔 → 나의 S3 버킷 클릭 → Object 탭 →  Create folder
Create folder → Folder name →  private 입력  Create folder
그 외 특별한 추가 설정은 필요 없다.
Create folder → Folder name →  public 입력  Create folder
이후 아래와 같이 폴더로 정리되면 된다. 외부에 파일이 있으면 각 경로로 이동 시켜줄 것
예제에서는 프로필 사진은 private, 게시글 첨부 사진은 public으로 이동시켰다.
접근 권한의 테스트
우선 권한을 정리하자면 다음과 같다.
S3에서 설정된 부분
Block public access → Block All(Private)
버킷 정책
S3는 연결된 CloudFront에 권한을 주고 있다.
CloudFront에서 설정된 부분
OAC(Origin Access Control) - Sign Requests 설정 상태
CloudFront는 Behavior 설정으로 S3의 폴더마다 접근 권한을 설정하고 있다.
CloudFront는 연결된 S3의 Default(/* 루트경로)경로에 대해서 비공개 상태이다. (AccessKey를 필요)
CloudFront는 연결된 S3의 private/*경로에 대해서 비공개 상태이다. (AccessKey를 필요)
CloudFront는 연결된 S3의 public/*경로에 대해서 전체 공개 상태이다.
이제 CloudFront에서 제공해주는 파일의 각 경로에 대한 권한 상태를 확인하고자 한다.
CloudFront에서도 연결된 S3로 접근 할 수 있는 URL이 제공된다.

3.4.3 백엔드 API 서버와 CloudFront의 연결

s3.service.ts
백엔드 서버 코드에서는 S3에 파일을 업로드하는 것은 동일하기 때문에 큰 변경이 없다.
반환하던 파일 접근 URL주소가 S3에서 CloudFront로 변경된 것 외에 차이점이 없다.
일부 하드코딩 되어 있던 AWS 관련 정보들은 모두 환경 변수로 넣어서 관리하도록 리팩토링했다.
기존 S3 URL만 반환하던 것을 CloudFront 접근 URL로 변경했으며, 파일 데이터베이스에 필요한 메타데이터들을 추가로 반환하도록 설정했다.
일부 한글에 대한 인코딩 문제가 있었다.
한글 파일이 S3에 업로드될 때 UTF8인코딩을 하더라도 자모가분리되거나 특수문자에 대한 처리가 문제가 발생했다.
여러 방법을 시도했지만 파일명에서 한글을 배제시키는 것만이 효과적이었다.
따라서 nomalize 메서드를 추가했으며 영문숫자 등 허용된 파일명으로 변경되어 업로드 된다.
import { Injectable } from '@nestjs/common'; import { S3 } from '@aws-sdk/client-s3'; import { PutObjectCommand } from '@aws-sdk/client-s3'; @Injectable() export class S3Service { private s3: S3; constructor() { this.s3 = new S3({ region: process.env.AWS_S3_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, }); } async uploadFile(file: Express.Multer.File, prefix: string): Promise<any> { const normalizeFileName = this.normalizeFileName(file.originalname); const filePath = `${prefix}/${normalizeFileName}`; const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_BUCKET_NAME, Key: filePath, Body: file.buffer, ContentType: file.mimetype, }); await this.s3.send(command); // CloudFront URL 반환 및 메타데이터 return { url: `https://${process.env.AWS_CLOUDFRONT_DOMAIN}/${filePath}`, filename: normalizeFileName, mimetype: file.mimetype, size: file.size, }; } private normalizeFileName(fileName: string): string { return fileName .replace(/[^a-zA-Z0-9.-]/g, '_') // 알파벳, 숫자, 점, 하이픈만 허용 .replace(/_{2,}/g, '_') // 연속된 언더바를 하나로 .replace(/^_|_$/g, ''); // 언더바로 시작하거나 끝나는 경우 제거 } }
Shell
복사

3.5  AWS 프론트엔드 기본 HTTP 배포(Static Deployment)

3.5.1 프론트엔드 프로젝트 빌드

Frontend 프로젝트 빌드
프론트엔드 프로젝트를 열고 터미널을 통해 아래 명령어를 입력한다.
ng build
Shell
복사
빌드 후 www 라는 폴더가 생성되며 컴파일된 프로젝트의 내용이 스냅샷으로 저장된다.
이후 본 프로젝트 원본 소스코드를 수정하면 다시 빌드해야 업데이트 내용이 반영된다.
코드 수정과 업데이트, 배포 과정을 자동화 하는 것을 CI/CD 파이프라인 구축이라 한다.
아직 CI/CD는 신경쓰지 않고 수동 배포부터 진행하고 추후 해당 내용을 다루려고 한다.
www대신 dist라는 폴더가 생성되는데요?
빌드를 통해서 생성되는 스냅샷 산출물을 정의하는 것은 angular.json에서 정의되어 있다.
www는 IONIC 프레임워크에서 주로 사용되는 빌드 산출물 폴더명이며 IONIC 명령어를 통해서 해당 프로젝트 구조가 초기 생성 되었으면 www로 설정되어있다.
dist는 Angular 프레임워크에서 주로 사용되는 폴더명이며 Angular/CLI 명령어로 프로젝트가 생성된 경우 dist로 설정되어 있는 것이다.
결론적으로 둘다 Angular 프레임워크를 혼용하는 상태이기도 하고 폴더명은 크게 문제되지 않기 때문에 기억하기만 하면 된다.
또한 angular.json 에서 직접 원하는 폴더 명칭으로 변경도 가능하다는 점

3.5.2 배포용 S3 저장소 생성과 설정(첫번째 버킷)

AWS S3에 프론트엔드 페이지 전용 버킷 생성
오리진 도메인 이름과 동일한 버킷, www가 포함된 버킷 두개가 필요하다.
첫번째 버킷(origin)
AWS S3 콘솔 → Create Bucket → General configuration
Bucket name →  boardapp.site 와 같이 본인이 구매한 도메인 그대로 입력
Block all public access →  체크 해제
그외 기본값 →  Create bucket →  배포용 첫번째 버킷 생성 완료
버킷 생성 후 버킷 설정
해당 버킷 상세정보 → Properties → Edit static website hosting
Static website hosting →  Enable
Hosting type →  Host a static website
Index document →  index.html 입력 →  Save changes
 첫번째 버킷 정적페이지 호스팅 설정 완료
하단 웹페이지 링크로 접속되면 정상, 하지만 아직 도메인 적용, HTTPS도 안되어있다.
프론트엔드 빌드 결과인 www 폴더 업로드
AWS S3 콘솔 → 내가 생성한 S3 Bucket →  Upload
Upload →  Add folder → www 폴더 찾아서 선택
그 외 설정 기본값 →  Upload
 프로젝트 업로드 완료
업로드 후 www폴더 내부 파일들을  버킷의 루트 경로로 이동시켜준다.
 버킷에 다음 처럼 index.html이 나타나면 된다.

3.5.3 www 리다이렉션 용 S3 저장소 생성과 설정(두번째 버킷)

AWS S3에 프론트엔드 페이지 www 리다이렉션 전용 버킷 생성
오리진 도메인 이름과 동일한 버킷, www가 포함된 버킷 두개가 필요하다.
이 설명은 그 중 두번째 버킷(www. 프리픽스에 대한 리다이렉션 처리용)이다.
AWS S3 콘솔 → Create Bucket → General configuration
Bucket name →  www.boardapp.site 와 같이 www.를 포함한 도메인 주소 입력
Block all public access →  체크 해제
그외 기본값 →  Create bucket →  배포용 두번째 버킷 생성 완료
버킷 생성 후 버킷 설정
해당 버킷 상세정보 → Properties → Edit static website hosting
Static website hosting →  Enable
Hosting type →  Redirect requests for an object
Host name →  boardapp.site 와 같이 첫번째 버킷의 이름(도메인)
Protocol - Optional →  http →  Save changes
 두번째 버킷 www. 리다이렉션 설정 완료
첫번째 버킷과 다르게 www. 프리픽스가 붙은 URL이 확인될 것이다.

3.5.4 도메인 구매

도메인 구매 과정
도메인 구매는 AWS의 Route 53에서도 할 수 있지만, 그 외 많은 업체에서도 구매 할 수 있다.
AWS에서 구매하는 경우(비추천)
AWS의 Route 53 콘솔 → 좌측 메뉴 Domains →  Registered domains
검색하면 도메인이 나타나지만 가격대가 높은 편이다. 14~30 USD 수준
도메인 및 호스팅 업체에서 구매하는 경우(Cafe24, 가비아 등)
국내 도메인 관리 업체들도 많이 있으며 저렴하게 할인하는 경우도 있다.
하지만 갱신 시 최초 투자한 가격보다 매우 높게 갱신되는 경우가 있기 때문에 실제 장기적인 이용을 위한 구매 시 잘 선택할 필요가 있다.
현재는 1,900원까지 할인된 도메인이지만 본래 가격이 60,000원이기 때문에 1년 후에도 계속 사용해야 한다면 비싼 금액으로 도메인을 유지해야 할 수도 있다.
보통 유명한 명사가 아닌 경우엔 10,000~20,000원/1년 정도로 가격대가 분포되어있다.
따라서 위 AWS에서 구매하는 가격이 일반적인 가격이라는 것.
하지만 현재는 테스트 용도가 크기 때문에 최대한 저렴하게 사용 할 수 있는 도메인을 구매해서 사용하고자 한다.
가비아 업체를 통해 1,900원 할인가로 구매 할 수 있는 boardapp.site 를 구매하게 되었다. 본인들도 원하는 서비스 이름과 유사한 도메인을 구매하는 것을 추천한다.
 도메인 구매 완료

3.5.5 구매한 도메인을 AWS에서 관리하도록 Route 53 설정

도메인 구매가 완료되면 AWS Route 53에서 도메인을 등록해야 한다.
AWS Route 53 콘솔 →  Hosted zones Create hosted zone
Create hosted zone
Hosted zone configuration → Domain name →  본인의 도메인 주소 입력 boardapp.site
그 외 기본 설정 →  Create hosted zone
Hosted zone 생성 완료
이 목록에서 TypeNS 인 레코드의 Value들을 기억해두자.
AWS에서 Hosted zone을 생성했으면 도메인 구매 업체의 설정 화면으로 돌아가자
바로 직전에 NS(Name Server)의 값들로 변경해야 되는 부분이 있다.
구매 업체별로 메뉴의 구성은 다르지만 해당 도메인의 네임서버 설정할 수 있는 부분을 찾으면 된다.
가비아 → 서비스관리 → 구매한 도메인의 관리 메뉴 →  네임서버 설정
이제 AWS Hosted zone에서 보았던 4개(일반적으론 4개) 값들을 입력해주고 적용시킨다.
AWS에서 복사하는 경우 …org. 처럼 복사되는데 뒤에 .을 빼야 한다
 AWS Hosted zone으로 NS 변경 완료

3.5.6 도메인과의 연결 HTTP 배포

구매한 도메인으로 주소를 변경하자.
현재까지 정상적으로 진행되었다면 아래와 같은 2개의 복잡한 주소지만 본인의 웹 페이지가 브라우저에 나타날 것이다.
아직 boardap.site이외에 s3-website.ap…amazonaws.com 과 같은 복잡한 도메인 주소가 따라 붙는 것을 제거해야 한다.
이 과정을 위해서는 우선 웹 호스팅과 도메인에 관련된 DNS 관리에 대해서 가볍게 알아야 한다. 하지만 네트워크 관련 이론이기 때문에 우리는 실무적으로 바로 사용할 수 있는 정도로 가이드를 따라 설정하는 방법을 익히는 것에 집중하고 해당 CS지식이 필요한 경우 추가로 스터디를 추천하는 편이다.
하지만 이 복잡한 과정을 우리는 AWS 인프라 내에서 처리하고 있기 때문에 최대한 간편하게 설정하고 도메인을 사용 할 수 있게 된다.
DNS관리의 A레코드와 CNAME
도메인을 구매하는 과정에서 NS(Name Server)를 설정하던 부분을 찾아가야 한다.
Route 53 → Hosted zones →  본인의 도메인 boardapp.site 클릭
Records →  Create Record
첫번째 레코드 ( A )
Record name →  빈칸으로 그대로 둔다(boardapp.site가 됨)
Record type →  A (레코드 선택)
Alias →  On (스위치 선택)
Route traffic to →  Alias to S3 web site endpoint
 Asia Pacific (Seoul)
 s3.website.ap-northeast… (본인의 S3 오리진 버킷 선택)
 Add another record
두번째 레코드 ( CNAME)
Record name →  www 입력 (www.boardapp.site가 됨)
Record type →  CNAME (레코드 선택)
Alias → 사용안한다.
Value →  boardapp.site.s3-website.ap-northeast-2.amazonaws.com 처럼 본인의 S3 오리진 버킷의 Web hosting 설정 후 제공해준 도메인을 입력
 Create records
 도메인 S3 버킷 연결 완료
주소에 할당 될 때 까지 잠시 기다린 후 접속해보면 http://boardapp.site 와 같은 도메인으로 정적 페이지가 나타나는 것을 볼 수 있다.
해당 A레코드, CNAME 주소 부분은 직후 CloudFront로 변경 시 CloudFront가 제공해주는 URL로 다시 변경 해야 HTTPS를 적용 할 수 있음을 기억해두자.

3.6  HTTPS 배포(CloudFront, ACM→SSL)

3.6.1 Amazon Route 53 Service

Amazon Route 53이란?
아마존 웹 서비스(AWS)에서 제공하는 클라우드 기반의 DNS(Domain Name System) 서비스
Route 53을 사용하여 도메인 등록, DNS 라우팅, 상태 확인 등 주요 기능을 제공
도메인 등록
Route 53을 사용하여 새로운 도메인을 등록할 수 있음
직접 도메인 구매 가능, 또는 타사 구매 도메인 등록 가능
DNS 관리
도메인에 대한 DNS 레코드를 쉽게 생성하고 관리
A 레코드, CNAME 레코드, MX 레코드 등 다양한 유형의 레코드를 지원
트래픽 라우팅
사용자의 요청을 리소스(예: 웹 서버, S3 버킷 등)로 라우팅할 수 있는 기능을 제공
Route 53은 다양한 라우팅 정책(예: 지리적 라우팅, 가용성 기반 라우팅 등)을 지원
헬스 체크 및 관리
리소스의 상태를 모니터링하고, 문제가 발생할 경우 트래픽을 다른 건강한 리소스로 자동으로 전환
AWS의 다른 서비스와 쉽게 통합되어, AWS 리소스를 효율적으로 관리하고 배포

3.6.2 ACM(Amazon Certificate Manager Service)

ACM이란?
아마존 웹 서비스(AWS)에서 제공하는 SSL/TLS 인증서를 쉽게 관리하고 배포할 수 있는 서비스
인증서 생성
ACM을 사용하여 무료로 SSL/TLS 인증서를 생성할 수 있음
이를 통해 웹사이트와 애플리케이션의 HTTPS 보안을 강화
인증서 관리
인증서를 쉽게 관리하고, 만료 전에 자동으로 갱신할 수 있는 기능을 제공
다양한 서비스와 통합
ACM 인증서는 Amazon CloudFront, Elastic Load Balancing, Amazon API Gateway 등 여러 AWS 서비스와 통합되어 쉽게 연결하여 사용 가능
도메인 검증
도메인 소유권을 검증하는 DNS 검증 또는 이메일 검증 방법을 통해 소유권을 확인하는 기능을 제공

3.6.3 도메인 등록과 보안 설정 흐름 정리

위 서비스들을 활용하기 위한 상황 정리
CloudFront가 기본적으로 제공해주는 도메인은 실제로 사용하기 어렵다.
d1i0cqrftvetp.cloudfront.net 이름 다음과 같이 나타난다.
도메인은 그 서비스, 기업의 이름이자 첫 인상이기 때문에 특정 이름의 도메인으로 변경할 필요가 있다.
내가 원하는 문자의 도메인 구매가 필요하다.(HTTP에서 진행했다.)
AWS Route 53을 통해 도메인을 관리, 해당 주소와 연결하게 될 것이다.
CloudFront가 기본적으로 제공해주는 도메인은 HTTPS 보안이 적용되어 있다.
커스텀 도메인으로 변경하면 보안 인증서 SSL를 해당 도메인과 연결해야 한다.
SSL은 유료로 발급 받기도 하며 발급 과정이 번거롭고 설정 또한 복잡하다.
AWS Certificate Manager에서는 무료로 SSL을 제공해준다.
또한 Route 53에 등록할 도메인, CloudFront로 배포되는 프로젝트는 동일한 AWS 인프라에 포함되어 있어 쉽게 HTTPS 환경을 적용 할 수있다.
아래 순서대로 진행하게 된다.
도메인 구매 - 도메인구매 및 등록(이미 진행 됨)
Route 53 설정 - DNS 레코드를 관리(S3→CloudFront 변경 필요)
ACM에서 SSL 인증서 생성 - HTTPS 보안을 위한 인증서를 발급
CloudFront 또는 ELB 설정 - SSL 인증서를 연결하여 HTTPS를 활성화
웹사이트 배포 - 사이트를 사용자에게 제공

3.6.4 CloudFront에서 배포용 Distribution 생성

CloudFront를 활용한 HTTPS 배포
CloudFront 콘솔 → Distributions →  Create distribution
Origin domain →  boardapp.site.s3.ap-northeast-2.amazonaws.com 과 같이 S3에서 생성했던 오리진 버킷을 선택
그 외 기본설정
Default cache behavior
Viewer →  Redirect HTTP to HTTPS
Allowed HTTP methods →  GET, HEAD, OPTIONS
그 외 기본 설정
Web Application Firewall (WAF) →  Do not.. 선택
Settings
Alternate domain name (CNAME) →  boardapp.site ,  www.boardapp.site 두개 모두 입력
Custom SSL certificate →  ACM에서 구성한 인증서 선택 ( 없는경우 아래 Request certificate를 통해 발급 - Virginia region것만 됨)
ACM에서 SSL(HTTPS 보안을 위한 인증서)받기
ACM(Amazon Certificate Manager)에서 SSL 발급 받기
AWS 콘솔 → 검색 및 즐겾차기로 이동 →  Certificate Manager Request a certificate
Certificate type →  Request a public certificate  Next
Domain names →  boardapp.site /  www.boardapp.site 둘다 입력
Validation method →  DNS validation 기본 설정
Key algorithm →  RSA 2048 기본 설정
 Request
 SSL 인증서 신청 완료 → Status - Pending
이는 해당 도메인 소유자 확인을 해야 된다. →  Create records in Route 53 버튼 클릭
Create DNS records in Amazon Route 53 에서 자동으로 입력된다.
 Create records
Route 53의 Hosted zone Records에 자동으로 입력된 키들이 보인다.
몇분 이후 다시 ACM 콘솔을 살펴보면 Issued, Success로 인증서가 발급된 것을 볼 수 있다.
Supported HTTP versions →  HTTP/2,  HTTP/3 둘다 선택
그 외 기본설정
 Create distribution
 Distribution 생성 완료

3.6.5 CloudFront 제공 주소를 구매한 커스텀 도메인으로 연결

도메인 변경
각자 클라우드 프론트 대쉬보드를 보면 d1i0cqrftvetp.cloudfront.net 과 같은 주소로 S3에서 배포하는 것이 HTTPS로 우선 접속 될 것이다.
이제 다시 구매한 도메인 주소로 깔끔하게 변경할 필요가 있다.
이제 저 cloudfront.net으로 끝나는 주소를 복사하여 기존 A레코드, CNAME을 수정하여 변경해주면 된다.
Route 53 →  기존 S3와 연결된 boardapp.site, www.boardapp.site 각각  cloudfront배포 주소를 입력 수정하여 아래와 같이 만들면 된다.
일정 시간 이후에 접속하면 커스텀 도메인으로 커스텀 SSL이 적용된 웹 페이지를 볼 수 있다.
10분 정도 이후 Region 별로 연결이 추가되는 모습
HTTPS로 접속 되는 모습

3.7  AWS EC2 사용하기

3.7.1 EC2(Amazon Elastic Compute Cloud Service)

Amazon EC2란?
아마존 웹 서비스(AWS)에서 제공하는 클라우드에서 가상 서버(인스턴스) 컴퓨팅 서비스
EC2는 간단하게 AWS에서 컴퓨터 자체를 렌탈한다고 생각하면 된다.
실제로 각종 프로그램을 설치하고 실행 할 수 있다.
RDS로 데이터베이스를 사용하고있지만 EC2 로컬 내부에 MySQL을 설치하고 데이터베이스 서버로 사용해도 된다.
하지만 RDS가 데이터베이스 관련 기능만 경제적으로 사용 할 수 있는 서버기 때문에 해당 기술을 사용하는 것이다.
EC2의 강점은 다음과 같다.
가상 서버
EC2는 사용자가 필요에 따라 다양한 구성의 가상 서버를 생성
CPU, 메모리, 스토리지 용량 등을 선택, 확장, 축소 가능
유연성
인스턴스를 필요에 따라 쉽게 시작하고 종료
필요에 따라 스케일 업(성능 증가) 또는 스케일 다운(성능 감소)
다양한 인스턴스 유형 제공
특정 워크로드에 최적화된 인스턴스를 선택할 수 있음
예를 들어, CPU 집약적인 작업에는 C 시리즈, 메모리 집약적인 작업에는 R 시리즈를 사용
안전성
EC2는 가용 영역(AZ)과 리전(region) 내에서 인스턴스를 배포할 수 있어, 고가용성을 유지
스토리지 옵션
EBS(Elastic Block Store)를 통해 영구 저장소를 제공하며, S3와 같은 다른 스토리지 옵션과도 통합하여 사용 가능
보안
AWS IAM(Identity and Access Management)을 통해 인스턴스에 대한 접근 권한을 관리할 수 있으며, 보안 그룹을 사용하여 네트워크 트래픽을 제어
비용 효율성
사용한 만큼만 비용을 지불하는 요금제(종량제)를 제공하여, 필요할 때만 리소스를 사용

3.7.2 EC2로 백엔드 서버 인스턴스 생성하기

EC2로 클라우드 서버 환경 만들기 따라하기
AWS 콘솔 →  EC2 (즐겨찾기 또는 검색)
Amazon EC2 콘솔 → Region 선택(지역 선택) →  Asia Pacific(Seoul)
Instances →  Launch instances
Name and tags →  ec2-server-boardapp 처럼 알아 볼 수 있게 입력
Application and OS Images
Quick Start →  Ubuntu
Ubuntu는 타 클라우드 미래 호환성, 마이그레이션을 고려한 선호되는 관례적 선택임
Amazon Machine Image (AMI) →  Ubuntu Server 24.04 LTS (Free tier eligible)
Description → Architecture →  64-bit
Instance type →  t2.micro
Key pair (login)
EC2에 접근 할 때 필요한 인증 키 →  Create new key pair
Key pair name →  ec2-boardapp-key 처럼 구분할 수 있는 이름
Key pair type →  RSA
Private key file format →  .pem
 Create key pair
자동으로 다운로드 된 key를 알 수 있는 위치로 이동 →  PC 루트 경로(예로 c: 같은 위치로 옮겨둠)
Key pair name →  위 생성한 key 선택
Network settings
Firewall(security groups) →  Create security group (새로운 보안 그룹 생성)
Allow SSH traffic from → Anywhere에서  My IP로 변경
Configure storage →  20GiB (*일반적인 서버 역할로는 기본 8GiB도 충분, 추후 고려)
 Launch Instance
 EC2 Instance 생성 완료
해당 인스턴스 이름을 클릭하면 세부 정보를 확인 할 수 있음
AWS에서는 이와 같이 특정 서비스들이 동작하는 렌탈 클라우드 PC를 인스턴스라 함

3.7.3 EC2 서버 초기 설정

Elastic IP address 설정
EC2 컴퓨터는 AWS 인터넷에 연결되어 있어서 외부 액세스가 가능하다.
제공되는 기본 IP들은 유동 IP로 서버 재부팅 및 일정 시간마다 주소가 변경된다.
하지만 프론트엔드 서버와의 호출 등 고정된 IP 주소가 필요하기 때문에 이를 탄력적 IP라고 불리우는 Elastic IP를 설정해야 한다.
EC2 콘솔 →  Elastic IPs
Elastic IP adresses →  Allocate Elastic IP address
기본 설정 →  Allocate
 Elastic IP 생성 완료
이제 고정 IP 주소를 생성한 것이지 아직 EC2 인스턴스와 연결되지 않았다.
 해당 IP 주소를 클릭하여 정보 페이지로 진입
IP 정보 페이지 →  Associate Elastic IP address
Instance →  생성했던 EC2 선택
Private IP address →  드롭박스에 나타나는것 선택
 Associate
 Elastic IP - EC2 연결 완료
EC2 Instance 세부 정보에서도 고정 IP를 확인 할 수 있다.
이 주소가 실제 요청, 터미널 접근 등에 사용되는 IP주소가 된다.

3.7.4 EC2 API 서버 실행 환경 구축

이제 실제로 EC2에 접속하여 프로젝트를 실행 시키기 위한 단계이다.
많은 학습자들이 생소해서 어렵게 느끼기도 하지만 쉽게 아래처럼 생각하자.
우리는 다른곳에 있는 컴퓨터를 빌렸을 뿐인데, 하필이면 DOS 환경의 옛날 컴퓨터를 빌린 상태이다.
Zoom에서 흔하게 사용하던 원격 제어처럼 해당 컴퓨터를 내 컴퓨터에서 원격으로 제어하는 것 뿐이다.
그런데, 아무것도 설치 안되어 있어서 하나하나 DOS(정확히는 Ubuntu, Linux) 명령어로 설치해줘야 한다.
다행히 인터넷은 연결되어 있다.
우선 EC2에 터미널로 접속해야 한다.
EC2 인스턴스 상세정보 페이지 →  Connect
Connect to instance →  SSH client 탭 Example Copy
본인 컴퓨터의 터미널을 연다 → Key Pair 생성 과정에서 .pem 으로 다운로드 받은 키의 저장 위치가 기억나는가? 해당 위치로 이동한다. cd.. 등 명령어 이용 →  복사한 ssh 접속 명령어 붙여넣고 실행
최초 접속에서 SSH 클라이언트가 연결하려는 호스트의 신원을 확인할 수 없기 때문에 다음 처럼 나타난다.  yes
bad permissions 같은 오류가 나타날 수도 있다.
이 경우에는 key파일에 접근 권한의 문제이다.
 아래 권한 명령어를 먼저 입력(본인의 키 파일 이름으로 변경)
chmod 400 "ec2-boardapp-key.pem"
Shell
복사
 EC2 SSH 접속 시도
ssh -i "ec2-boardapp-key.pem" ubuntu@ec2-43-200-247-144.ap-northeast-2.compute.amazonaws.com
Shell
복사
 로컬(내 PC) 터미널에서 원격 EC2 접속 완료
AWS EC2에 터미널을 통해 접속했다 무엇부터 해야 할까?
목표를 생각하고 점진적으로 접근해야 한다. 아래처럼 자연스러운 생각의 흐름을 따라가자.
우리는 백엔드 API 서버를 이 EC2 컴퓨터에서 돌리고 싶은 목표가 있다.
프로젝트는 어디있지? → Github Repository
Github에서 가져오려면? → git clone …
git이 있나?
git 설치확인은? → 버전확인을 하자 → git -v
EC2는 아무것도 설치 안된 컴퓨터라 설명한 것 처럼..
우리가 개발을 시작할 때 아무것도 설치되지 않았던 내 컴퓨터에서 개발을 위한 환경 구축 부분들을 리마인드해야 한다.
일부 위 처럼 기본적인 일부 git과 같은 프로그램은 기본적으로 설치되어 있지만 확실하다고 보장 할 수 없다. 본인이 직접 설치한 기억이 없으면 불확실한 것이다. 없다고 생각하고 각종 프로그램 설치 상태 확인을 해줘야 한다.
개발 PC와는 다르게 실행 환경(Runtime environment)만 구축되도 된다.
하지만 예전 자바에서 JDKJRE를 포함하고 있는 것 처럼, 기본적으로 우리가 개발 환경을 구축하는 것은 실행환경을 포함하고 있어서 크게 다르지 않다.
프로젝트 클론하기
 현재 폴더 내용 조회
ls -a
Shell
복사
 git clone(본인 Remote Repository 주소로 변경)
git clone https://github.com/citeFred/nestjs-board-app
Shell
복사
 프로젝트 폴더 이동
cd nestjs-board-app
Shell
복사
 npm run start
NestJS는 Angular기반이며 Node.js 기반 웹 프레임워크이다.
따라서 npm run start로 실행하기 위해서는 Node.js가 설치되어 있어야 한다.
Node.js가 설치되기 위해서는 NVM이 설치되어야 하며 그에 따라 NPM이 설치된다.
이에 대한 자세한 내용은 [NestJS] 1. NestJS 소개 에서 [4], [5] 환경 구축 부분을 살펴 보길 바란다.
 NVM 설치
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
Shell
복사
 NVM 설치완료
 터미널 환경 변수 확인(자동 등록, 혹시 없으면 작성)
vim /home/ubuntu/.bashrc
Shell
복사
환경 변수 등록 코드
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
Shell
복사
VI에디터로 문서 편집 = I / 저장 후 종료 esc:wq
 환경 변수 터미널 즉시 적용
source /home/ubuntu/.bashrc
Shell
복사
 NVM 버전 확인
nvm -v
Shell
복사
nvm 버전 확인이 안되는사람은 설치, 환경 변수 설정 재확인
 Node.js 설치
nvm install --lts
Shell
복사
 Node.js 버전 확인
node -v
Shell
복사
 NPM 버전 확인
npm -v
Shell
복사
 현재 프로젝트의 모든 의존성 설치
 경로 프로젝트 루트 경로여야 함(package.json이 보이는 위치)
npm install
Shell
복사
 npm run start
서버는 실행되지만 Database(RDS)를 연결하지 못하는 상황
현재 프로젝트는 DB 접속 정보 등이 .env 파일로 은닉되어 있다.
github에는 .gitignore로 추적이 제외되어 해당 파일이 존재하지 않기 때문에 git clone으로 받아온 현재 프로젝트에 존재하지 않는다.
따라서 해당 파일을 직접 작성해주면 된다.
 .env 파일 생성 및 작성
vim .env
Shell
복사
VI에디터로 문서 편집 = I / 저장 후 종료 esc:wq
 npm run start
.env까지 작성했는데 접속이 안된다.
그 이유는 접근 권한에 대한 것이다.
EC2 인스턴스에서 RDS에 접근하려면 RDS는 EC2 IP주소에 대한 접근이 허용되어야 한다.
EC2의 보안 그룹 확인
EC2 대시보드 → Security →  Security groups 이름 확인
sg-08d2c9adfd58876c9 (launch-wizard-1)
RDS의 보안 그룹 확인
RDS 대시보드 → Security group rules →  CIDR/IP - Inbound 인 부분 선택
해당 보안그룹 정보로 들어가서  Edit inboud rules
 Add rule
Type →  MYSQL/Aurora
Source → 우측 드롭다운에서 위에서 기억한 EC2의 보안 그룹 선택
 Save rules
 RDS EC2 권한 설정 완료
 npm run start
TypeORM이 정상적으로 작동한다면 DB 연결까지 완료된 상태
이제 EC2의 3000번 포트에서 백엔드 API 서버가 실행되고 있다.

3.8 AWS Frontend와 Backend 연결

현재 까지의 상황 정리
프론트엔드는 S3 버킷에 빌드되어 업로드되고 CloudFront가 S3의 페이지를 배포하고 있다.
백엔드는 EC2 서버에서 실행되고 있으며 RDS와 연결이 되었다.
이제 프론트엔드가 어떤 버튼을 눌렀을때(HTTP 요청시) 회원 가입이나 게시글 페이지가 나타나는 등 API 요청의 응답이 나타나야 한다.
로컬에서는 이미 작동하던 것이지만 AWS로 옮겨지면서 요청 URL이 변경되어야 하고
EC2와 CloudFront IP에서 보내는 요청을 허용해야 한다.

3.8.1 백엔드의 설정 변경 CORS

EC2는 현재 로컬(나의 PC)의 접근만 허용되고 있다.
EC2 Dashboard → Security → Security groups 선택
Inbound rules
Type →  HTTP
Source →  Anywhere 0.0.0.0
백엔드 코드 CORS 설정 변경
main.ts
dotenv.config(); async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // cookie parser 미들웨어 추가 app.use(cookieParser()); app.enableCors({ origin: [ 'http://localhost:8100','http://localhost:4200', // 로컬 개발용 'https://d2r1i81lny2w8r.cloudfront.net', // CloudFront 도메인 'https://boardapp.site', // 커스텀 도메인 'https://www.boardapp.site', // www 커스텀 도메인 ], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); app.useStaticAssets(path.join(process.cwd(), 'public'), { prefix: '/files/' }); await app.listen(process.env.SERVER_PORT); Logger.log(`Application Running on Port : ${process.env.SERVER_PORT}`) } bootstrap();
TypeScript
복사
EC2에는 해당 내용이 반영되어야 하기 떄문에 clone을 다시 받아와야 한다.
임시로 직접 ubuntu 터미널로 파일에 접근해 vim으로 수정하긴 했다.

3.8.2 백엔드 API의 HTTP→HTTPS 요청이 가능하도록 변경

현재 프론트 엔드 배포 이후 요청이 안되는 문제가 있을 것이다.
위 문서처럼 프론트엔드는 HTTPS로 보안 적용이 되어 있지만 백엔드 API는 여전히 HTTP를 사용하고 있기 때문이다.
따라서 백엔드 서버 또한 HTTPS 요청을 주고 받을 수 있도록 변경하고자 한다.
SSL(Secure Socket Layer)이란?
서버와 사용자(브라우저) 간의 통신을 할 경우 정보를 암호화 하고 도중에 해킹을 통해 정보가 유출이 된다고 하더라도 정보의 내용을 보호할 수 있는 보안 인증 솔루션 기술
대표적으로 해킹 기법 중 '스니핑(sniffing)' 기법을 막기 위한 도구
Sniffing?
해킹기법의 하나로 네트워크상에서 전송되는 패킷을 캡쳐하여 이의 내용을 엿보는 행위
패킷을 가로채는 것으로 도청과 비슷한 개념으로 이해
Let’s encrypt 로 SSL 인증서 생성
Let's Encrypt는 HTTPS 의 확산이 늦어지는 것은 SSL 인증서에 있다고 보고 무료 인증서 보급을 통해 HTTPS 의 확산을 늘리겠다는 취지로 시작된 비영리 프로젝트
2016 년 4월 정식 버전을 출시 했으며, 2020년 12월 기준 2.34 억개의 웹서버 인증이 진행
인증 기간이 90일로 제한되어 재발행을 권장하고 있음
그 외 타기관 인증서는 유료로 관리되는 점에서 Let’s encrypt를 사용하여 무료로 개발 할 수 있다.

3.8.2.1 API서버 A레코드 생성

EC2 IP주소를 가리키는 A레코드가 필요하다
EC2 대시보드 → API 서버 인스턴스 상세정보 →  Elastic IP address 복사
Route 53 대시보드 → 생성한 Hosted zones 선택 →  Create record
Record name →  api 입력 ( api.boardapp.site 형태가 됨 )
Record type →  A 선택
Value →  복사한 EC2의 IP 주소 붙여넣기
 EC2 를 가리키는 A 레코드 생성 완료

3.8.2.2 EC2에서의 작업 - Certbot 설치 및 인증서 발급

EC2 콘솔에서 작업(SSH로 접근)
우선 NestJS 백엔드 API 서버가 배포되는 EC2 서버 터미널로 접근한다.
EC2 대시보드에서 인스턴스를 선택하여 Connect to instance에서 SSH 접근 명령어를 확인 할 수 있다.
SSH 접근을 위해서 pem키를 생성했던 위치에서 아래 명령어를 입력한다.(잊은 사람은 이 링크에서 재확인 3.7.4 EC2 API 서버 실행 환경 구축 )
나의 사례에서는 다음과 같은 형태이다.
ssh -i "ec2-boardapp-key.pem" ubuntu@ec2-43-200-247-144.ap-northeast-2.compute.amazonaws.com
Shell
복사
접속하면 다음과 같이 ubuntu~ 로 현재 위치가 변경되어야 한다.
Certbot 설치 및 인증서 설치
패키지 매니저 업데이트
sudo apt update
Shell
복사
Certbot 설치
sudo apt install certbot
Shell
복사
도메인에 대한 인증서 발급
sudo certbot certonly --standalone -d api.boardapp.site
Shell
복사
email 주소 입력과 약관 등이 나타나며 각 항목을 동의한다.
인증서 생성 완료
.pem 확장자의 저장 경로를 안내해준다.
/etc/letsencrypt/live/api.boardapp.site/ 경로 내에 두개의 키가 저장된 것을 확인 할 수 있다.

3.8.2.3 EC2에서의 작업 - NestJS HTTPS 설정

프로젝트의 main.ts 수정
EC2 → 프로젝트 폴더로 이동
cd nestjs-board-app/src
Shell
복사
VI에디터로 코드 수정
vim main.ts
Shell
복사
main.ts 아래 코드 추가
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as cookieParser from 'cookie-parser' import { Logger } from '@nestjs/common'; import * as dotenv from 'dotenv'; import { NestExpressApplication } from '@nestjs/platform-express'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; // https 모듈 추가 dotenv.config(); async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // cookie parser 미들웨어 추가 app.use(cookieParser()); app.enableCors({ origin: [ 'http://localhost:8100','http://localhost:4200', // 로컬 개발용 'https://d2r1i81lny2w8r.cloudfront.net', // CloudFront 도메인 'https://boardapp.site', // 커스텀 도메인 'https://www.boardapp.site', // www 커스텀 도메인 'http://s3-bucket-boardapp.s3-website.ap-northeast-2.amazonaws.com' ], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, exposedHeaders: ['Authorization'], // 이 부분 추가 }); app.useStaticAssets(path.join(process.cwd(), 'public'), { prefix: '/files/' }); // HTTPS 옵션 설정 const httpsOptions = { key: fs.readFileSync('/etc/letsencrypt/live/api.boardapp.site/privkey.pem'), cert: fs.readFileSync('/etc/letsencrypt/live/api.boardapp.site/fullchain.pem'), }; // HTTPS 서버 생성 const server = https.createServer(httpsOptions, app.getHttpAdapter().getInstance()); // NestJS 애플리케이션을 HTTPS 서버에 연결 (HTTPS 적용) server.listen(process.env.HTTPS_SERVER_PORT, ()=> { Logger.log(`Application Running on [SECURED] https://api.boardapp.site:${process.env.HTTPS_SERVER_PORT}`); }); // NestJS 애플리케이션 시작 (HTTPS 기존) await app.listen(process.env.HTTP_SERVER_PORT, ()=> { Logger.log(`Application Running on [BASIC] http://localhost:${process.env.HTTP_SERVER_PORT}`); }); } bootstrap();
TypeScript
복사
ESC:wq 로 저장하고 나가기
프로젝트를 실행해보면 npm run start 아래와 같이 권한 관련 문제가 나타난다.
ubuntu 유저는 EC2의 root 권한이 없기 때문
아래 명령어로 프로젝트를 실행하도록한다. 슈퍼유저 권한으로 실행하는 방법이다.
sudo npm run start
Shell
복사
super user(root) 에 npm이 없다면 설치해주자
sudo apt update
Shell
복사
sudo apt install nodejs npm
Shell
복사
.env 포트 관련 환경변수 편집
ls -a.env 파일이 있는 곳을 확인(src 폴더 내 이동 cd src
기존 SERVER_PORT인 부분에 앞에 HTTPS_ , HTTP_ 처럼 구분하여 포트번호를 다르게 설정
3001번 포트가 HTTPS 키를 사용하는 서버로 설정했다. 클라이언트는 이곳으로 요청을 보내야 한다.
sudo npm run start 인증서를 사용하는 HTTPS API서버 URL 확인
EC2의 express 서버가 실행중인 현재 상태에서 HTTPS 요청이 정확히 되는지 테스트
중요한점은 이전 개발 환경과 다르게 localhost가 아니라 현재 AWS EC2의 구매한 도메인의 API 서버인 서브 도메인 api.boardapp.site에 SSL이 적용된 요청이 되는지 확인하는 것이다.
간단하게 터미널을 통해서도 테스트 할 수 있다.
curl -X GET https://api.boardapp.site:3001/api/articles
Shell
복사
POSTMAN을 통해서 가장 간단한 API인 게시글 목록을 조회하는 요청을 시도
정상적으로 HTTPS 경로로 요청이 성공되는 모습

3.8.2.4 프론트엔드의 설정 변경

API 요청 URL의 변경이 필요하다.
EC2의 Elastic IP 주소를 설정했었다. 해당 주소는 고정 IP주소이기 떄문에 요청 주소로 사용하기 위해 api.boardapp.site로 Route 53을 통해 도메인을 연결했다.
위 과정을 완료했기 때문에 프론트엔드에서 HttpClient를 통해(또는 fetch api)요청을 보내는 각 Service 계층에서 요청 주소를 HTTPS를 포함하는 주소로 변경해야 한다.
article.service.ts
@Injectable({ providedIn: 'root' }) export class ArticleService { // private apiUrl = 'http://localhost:3000/api/articles'; // 로컬 테스트용 // private apiUrl = 'http://43.200.247.144:3000/api/articles'; // EC2 연결용 private apiUrl = 'https://api.boardapp.site:3001/api/articles'; // HTTPS 주소로 변경 constructor(private http: HttpClient) { } // 생략... }
TypeScript
복사
auth.service.ts
@Injectable({ providedIn: 'root' }) export class AuthService { // private apiUrl = 'http://localhost:3000/api/auth'; // 로컬 테스트용 // private apiUrl = 'http://43.200.247.144:3000/api/auth'; // EC2 연결용 private apiUrl = 'https://api.boardapp.site:3001/api/auth'; // HTTPS 주소로 변경 constructor(private http: HttpClient) { } // 생략... }
TypeScript
복사
user.service.ts
@Injectable({ providedIn: 'root' }) export class UserService { // private apiUrl = 'http://localhost:3000/api/users'; // 로컬 테스트용 // private apiUrl = 'http://43.200.247.144:3000/api/users'; // EC2 연결용 private apiUrl = 'https://api.boardapp.site:3001/api/users'; // HTTPS 주소로 변경 constructor(private http: HttpClient) { } // 생략... }
TypeScript
복사
위 처럼 코드가 변경된 부분이 적용되어야 하기 떄문에 재빌드, S3에 재업로드 해야 한다.
이런 불편함을 해결하는것이 CI/CD 파이프라인 구축인데 아직은 안내하지 않았음
ng build 재빌드
S3 → boardapp.site 버킷에 재업로드
HTTPS 요청 성공 테스트
구현 상태 정리
백엔드 API서버에서는 Let’s Encrypt를 통해 EC2내부에서 pem 키페어를 관리, HTTPS 요청에 대해 처리
사용자가 구매한 커스텀 도메인의 서브 도메인(api.domain.com) 을 Route 53을 통해 EC2 IP주소와 연결
프론트 클라이언트 서버에서는 S3 버킷에 기본 HTTP 정적 페이지를 제공하면서 CloudFront를 연결하여 배포
사용자가 구매한 커스텀 도메인을 Route 53을 통해 AWS Certificate Manager의 SSL 키페어를 등록, 연결하여 HTTPS 접근 URL을 제공

4. 기타 변경 사항 및 정리 과정에서의 트러블슈팅

4.1 트러블슈팅 - ”ERR_TOO_MANY_REDIRECTS”, “ChunkLoadError”

예상과 다르게 백엔드 프론트엔드 서버 구축은 끝났지만 정상작동이 안되고 있다.
도메인 https://www.boardapp.site 에 접근해서 특정 페이지를 접근 할 때
다음과 같이 이상한 경로로 나타나고 있다. 뭔가 이상한 prefix가 중복해서 붙고 있는 상황
어디엔가 Redirection 관련 설정 또는 코드의 결함일 수 있다.
EC2 로컬에서의 테스트
NestJS 서버 자체가 작동은 잘되는지부터 확인해보았다.
둘다 EC2의 터미널이며 왼쪽에는 서버를 실행시켜두고
오른쪽 아래 커맨드를 통해 요청을 보내봤다.
curl -X POST http://localhost:3000/api/auth/signin -H "Content-Type: application/json" -d '{"email": "test@test.com", "password": "Test1234!"}'
TypeScript
복사
결과 서버는 정상적으로 작동하며 RDS와의 연결도 정상이다.
이제 localhost라는 부분을 IP주소로 EC2의 대체 해본다.
curl -X POST http://172.31.41.28:3000/api/auth/signin -H "Content-Type: application/json" -d '{"email": "test@test.com", "password": "Test1234!"}'
TypeScript
복사
마찬가지로 정상 작동하고 Elastic IP로 변경해본다.
curl -X POST http://43.200.247.144:3000/api/auth/signin -H "Content-Type: application/json" -d '{"email": "test@test.com", "password": "Test1234!"}'
TypeScript
복사
역시나 정상적으로 쿼리까지 작동한다.
이말은 EC2의 서버는 정상적으로 작동하고 있으며 유동 IP, 고정 IP 모두 정상적으로 API 요청을 받고 있다.
그럼 문제는 외부로부터의 요청에 대한 문제일 가능성이 높아졌다.
EC2 자체가 외부 권한을 어떻게 허용하고 있는지 체크할 필요가 있다.
인바운드 규칙은 테스트를 위해서 로컬PC, FrontCloud 등 최대한 오픈했지만 작동하지 않았다.
다시한번 깔끔하게 인바운드 규칙을 모두 정리하고 모든 권한을 열어보았다.
이후 이번엔 나의 로컬 PC가 원격으로 EC2 서버에 요청해보려고한다.
curl -X POST http://43.200.247.144:3000/api/auth/signin -H "Content-Type: application/json" -d '{"email": "test@test.com", "password": "Test1234!"}'
TypeScript
복사
결과 정상적으로 작동한다. 이말은 인바운드 규칙 방화벽의 문제가 확실해졌다.
이번엔 내부 IP주소로 요청해보았다.
curl -X POST http://172.31.41.28:3000/api/auth/signin -H "Content-Type: application/json" -d '{"email": "test@test.com", "password": "Test1234!"}'
TypeScript
복사
응답이 없다. Elastic IP로 요청을 보내야 정상적으로 처리된다는 것이다.
로컬의 프론트엔드를 실행하여 다시 테스트했다.
정상적으로 로그인이 처리되었으며, EC2 서버에서도 로그가 나타나는 것을 볼 수 있었다.
하지만 쿠키가 로컬때 테스트와 다르게 노란색으로 표기된다.
EC2에서 src/auth/auth.controller.ts쿠키가 생성되는 로그인 컨트롤러로 접근하여 httponly 속성을 true로 변경
요청과 관련된 문제는 체크되었으나 CloudFront의 Redirection 문제는 해결되지 않았다.
현재 정상 작동되는 배포 파일을 S3에 업로드해서 오류를 좁혀나가려고 한다.
문제 해결 과정
하나의 버킷을 사용하면서 폴더별로 Behavior를 설정하는데서 가장 큰 문제가 있는것 같다.
우선 과감하게 S3 버킷을 모두 삭제하고 다시 재생성해보았다. 이럴수록 설정을 부분적으로 수정하면서 더 문제가 커지는 경우가 많았다.
S3에서의 작업
CloudFront에서의 폴더별 리다이렉션을 설정하는 방법 대신, S3 버킷에서 boardapp.site 오리진 버킷을 만들고 호스팅한다.
www.boardapp.site 와 같은 프리픽스 더미 버킷을 만들어서 오리진 버킷 자체로 리다이렉션 하도록 구성을 바꿨다.
CloudFront에서의 작업
Distribution을 생성 할 때 해당 배포와 관련된 URL을 CNAME으로 오리진인 boardapp.site, www.boardapp.site 두개 모두 추가한다.
CloudFront가 직접 바라보는 오리진 버킷은 boardapp.site의 S3 버킷만을 지정한다.
ACM을 통해 SSL(HTTPS인증서)를 발급받아야 하므로 (SSL 생성은 Virginia 지역에서만 처리된다는 점 기억) 안내에 따라 설정한다.
Route 53에서의 작업
그리고 Route 53에서 boardapp.site로 A레코드를 생성
www.boardapp.site로는 CNAME을 생성
이후 배포와 DNS 연결까지 잠시 대기 후 접속하여 해결
HTTPS 연결 테스트 모습
우선 Redirection 문제는 해결되었으며 이제 HTTPS 요청에 대한 문제로 변경되었다.

4.2 트러블 슈팅 - Mixed Content: The page at 'https://boardapp.site/auth/signin' was loaded over HTTPS, but requested an insecure

해당 클라이언트 오류 로그를 통해서도 알 수 있듯이 HTTPS 로 배포되는 서비스에서 HTTP 요청이 혼재된 경우에 발생하는 오류이다.
해결 방법은 두가지 정도로 조사되었다.
첫번쨰는 요청 자체를 HTTPHTTPS로 바꾸는것
프론트엔드의 HttpClient 요청 부분을 살펴보면 API 호출 URL이 모두 http로 시작한다.
@Injectable({ providedIn: 'root' }) export class AuthService { // private apiUrl = 'http://localhost:3000/api/auth'; private apiUrl = 'https://43.200.247.144:3000/api/auth'; constructor(private http: HttpClient) { } ... }
TypeScript
복사
이 부분을 https로 변경한다. 해당 EC2 IP주소가 https 인증과 Security group에서 inbound 규칙이 설정이 되어 있어야 한다.
우선 inbound 규칙은 모든 요청을 허용하고 있다. 정상 실행되는 모습이 확인되면 HTTP, HTTPS를 별도로 그리고 정확한 IP 소스로부터 제한하도록 세부 설정하게 될 것이다.
역시 https로 요청을 보내도 EC2의 백엔드 서버 자체에 https 설정이 없기 때문에 해당 요청을 받아낼 수 없는 상태이다.
이 방법을 위해서 EC2에 Loadbalancer를 통해서 ACM의 인증서를 적용시키려 했으나 EC2는 AsiaPacific Region이며 SSL은 Virginia Region이기 때문에 AWS내에서 적용하기 어려움이 있다.
두번째는 HTML의 head안에 meta태그를 추가로 설정하는 것이다.
프론트엔드의 index.htmlhead에 다음 코드를 작성한다.
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"/>
HTML
복사
두번째 방법도 결국 EC2에서 https를 받을 수 없기 때문에 동일한 오류가 나타나고있다.
근본적인 원인 해결 필요
현재 백엔드 API 서버에서 HTTPS를 위한 인증서 관련 작업을 진행한 적이 없다.
SSL을 발급 받고 HTTPS 요청을 받을 수 있도록 조치가 필요하다.

4.3 JWT 토큰 저장 방식 변경

현 HTTPS 문제, Redirection 문제, 쿠키 저장 문제 등 보안 설정 및 연결에 다양한 이상이 있다.
FrontCloud에 CDN과 프론트엔드 배포를 하나의 버킷에서 처리하고 싶었지만 생각보다 보안과 DNS 연결 구성이 복잡하게 구성되어 있었다.
따라서 각 AWS 기술의 역할을 명확하게 정리하고 서비스를 다시 연결해보고자 한다.
JWT 쿠키 방식을 JWT Header 로컬 스토리지 저장 방식으로 변경
우선 HTTPS 보안을 적용하기 전에 현재 인증과정을 좀더 깔끔하게 처리하고 싶어서 쿠키 생성보다 헤더로 전송하여 로컬스토리지에 저장하는 방식으로 변경하고자 한다.
백엔드의 auth.controller.ts
기존 쿠키를 생성하여 Response에 추가하던 부분을 res.setHeader를 통해 헤더에 담아서 응답을 반환하도록 설정했다.
@Controller('api/auth') export class AuthController { private readonly logger = new Logger(AuthController.name); constructor(private authService: AuthService){} // 회원 가입 기능 @Post('signup') @UseInterceptors(FileInterceptor('profilePicture')) async signUp( ... } // 로그인 기능 @Post('signin') async signIn( @Body() signInRequestDto: SignInRequestDto, @Res() res: Response ): Promise<void> { this.logger.verbose(`Attempting to sign in user with email: ${signInRequestDto.email}`); const { jwtToken, user } = await this.authService.signIn(signInRequestDto); const userResponseDto = new UserResponseDto(user); this.logger.verbose(`User signed in successfully: ${JSON.stringify(userResponseDto)}`); // JWT를 응답 헤더에 설정 res.setHeader('Authorization', `Bearer ${jwtToken}`); res.status(200).json(new ApiResponse(true, 200, 'Sign in successful', { user: userResponseDto })); } ... }
TypeScript
복사
프론트엔드의 로그인 요청 부분 auth.service.ts
기존 쿠키를 특별히 저장하거나 하는 로직은 없었고 이는 쿠키는 자동으로 클라이언트에서 저장되기 때문이었다.
하지만 헤더로 JWT토큰을 전송하게 되면 로컬스토리지에 저장하는 로직이 프론트엔드에 구성되어야 한다.
signIn(signInRequestData: SignInRequestData): Observable<AuthResponse> { const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); return this.http.post<AuthResponse>(`${this.apiUrl}/signin`, signInRequestData, { headers, withCredentials: true, observe: 'response' }).pipe( tap(response => { // JWT를 Authorization 헤더에서 추출하여 로컬 스토리지에 저장 const jwtToken = response.headers.get('Authorization')?.split(' ')[1]; console.log("get"+jwtToken) if (jwtToken) { localStorage.setItem('jwtToken', jwtToken); console.log("saved"+jwtToken) } }), map(response => { // response.body가 null이 아닐 때만 반환 if (response.body) { return response.body; // AuthResponse 반환 } throw new Error('Response body is null'); // null인 경우 예외 처리 }) ); }
TypeScript
복사
클라이언트 테스트
이제 안정적으로 JWT토큰을 클라이언트에 저장하는 모습이다. 해당 클라이언트는 로컬 테스트 환경이 아니라 AWS 배포환경(EC2 + S3) 에서 실행된 모습이다.
이는 쿠키 JWT를 보관했을때 배포환경에서 문제가 발생하였기 때문에 이와 같이 JWT 토큰 관리 방식 로직을 수정하게 되었다.

PS. Github

리팩토링 완료 된 결과 코드 묶음은 Github를 참고
Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio