2025. 3. 9. 22:07ㆍNodejs
AWS에서 EC2에 HTTPS를 적용하려면 보통 Elastic Load Balancer(ELB)를 사용하는 것이 일반적입니다. ELB는 AWS에서 제공하는 관리형 로드 밸런서로, 자동 SSL 인증서 관리 및 트래픽 분산 기능을 제공합니다. 하지만, ELB는 거의 t3.small~t3.medium급 EC2 인스턴스 하나를 더 운영하는 것과 비슷한 추가 비용이 발생하고, 트래픽이 많지 않은 작은 프로젝트나 개인 프로젝트에서는 불필요한 경우가 많습니다.
그래서 작은 프로젝트에서는 ELB 없이 EC2에 직접 HTTPS를 적용하여 사용하는 경우가 많습니다. 이 글에서는 NestJS 앱을 EC2에 배포하고, Nginx와 Certbot을 활용해 HTTPS를 적용하는 과정을 설명하려고 합니다.
1. NestJS 프로젝트 생성
간단하게 EC2에 배포해서 실행할 프로젝트를 생성하겠습니다. 여기서는 NestJS 프로젝트를 생성해서 테스트 하겠습니다.
NestJS 프로젝트를 생성하려면 NestJS CLI가 필요합니다. 아래의 명령어를 실행하여 설치합니다.
$ pnpm install -g @nestjs/cli
NestJS CLI 설치가 완료되었다면, 아래의 명령어를 실행하여 프로젝트를 생성합니다.
$ nest new my-app
프로젝트가 생성되었다면, 실행한 후 기본으로 작성되어 있는 API를 호출해 봅니다.
$ pnpm run start
$ curl localhost:3000
Hello World! # 출력
이 글은 HTTPS 적용이 목표이기 때문에 별도의 API를 추가하지 않겠습니다.
2. EC2 생성
이제 프로젝트를 배포할 EC2를 생성합니다. AWS 콘솔에 접속하여 EC2 페이지로 이동합니다. 인스턴스 메뉴로 이동하여 "인스턴스 시작"을 클릭하면 EC2 생성 페이지로 이동합니다.
인스턴스 생성
"이름 및 태그"는 원하는 이름을 입력합니다. 저는 my-app으로 하겠습니다.
"애플리케이션 및 OS 이미지(Amazon Machine Image)"는 Ubuntu를 선택하고 버전은 최신 버전을 선택합니다.
"인스턴스 유형"은 t2.micro를 선택합니다. 저는 간단한 테스트를 위해서 낮은 사양의 인스턴스를 선택했습니다.
"키 페어(로그인)"는 새로운 키 페어를 생성하여 선택합니다. 키 페어 생성 시, “키 페어 유형”은 RSA, 프라이빗 키 파일 형식은 “.pem”을 선택합니다.
"네트워크 설정"의 보안 그룹 부분에 SSH 트래픽 허용을 내 IP로만 제한하고, "인터넷에서 HTTPS 트래픽 허용", "인터넷에서 HTTP 트래픽 허용"을 체크합니다. 이 설정을 활성화하면 외부에서 80(HTTP) 포트와 443(HTTPS) 포트로 접근할 수 있습니다.
나머지 설정은 모두 기본값을 사용하기 때문에 별도로 변경하지 않겠습니다. "인스턴스 시작"을 클릭하면 인스턴스가 생성됩니다.
인스턴스 확인
인스턴스의 "상태 검사"가 "2/2개 검사 통과"로 변경되면 SSH를 통해 인스턴스에 접속해 봅니다. 키 페어를 생성하면서 다운로드한 my-app.pem 키 파일의 권한을 변경하고 ssh를 사용하여 접속합니다.
SSH 접속을 하기 전에, .pem 키 파일의 권한을 제한해야 합니다. AWS에서는 보안 정책상 프라이빗 키의 권한이 너무 개방적이면 SSH 접속이 차단 됩니다. 따라서, chmod를 통해 .pem 파일의 권한을 400(읽기만 가능, 다른 사용자 접근 차단)으로 설정합니다.
$ chmod 400 my-app.pem
$ ssh -i my-app.pem ubuntu@<인스턴스 IP>
3. 프로젝트 배포
인스턴스 생성이 완료되었으면, 앞에서 생성한 NestJS 프로젝트를 배포합니다. 이번 글의 핵심은 HTTPS 적용이므로, 프로젝트 배포 과정은 SCP를 사용해 인스턴스로 직접 업로드한 후, 수동으로 실행하는 방식으로 배포하겠습니다.
앱을 빌드한 후, 빌드 결과물과 package.json을 같이 압축하여 인스턴스에 업로드합니다.
# 프로젝트 빌드
$ pnpm run build
# dist, package.json을 같이 압축
$ tar -czvf my-app.tar.gz dist package.json
# 인스턴스에 업로드
$ scp -i my-app.pem my-app.tar.gz ubuntu@<인스턴스 IP>
인스턴스에 접속하여 업로드 되었는지 확인합니다.
$ ssh -i my-app.pem ubuntu@<인스턴스 IP>:~/
$ ubuntu@ip-xxx-xx-x-xx:~$ ls
my-app.tar.gz
앱을 실행하기 위해서 Node.js와 pnpm을 설치합니다. 설치 방법은 공식 문서에서 자세히 안내하고 있으므로, 이를 참고하면 쉽게 설치할 수 있습니다. 참고로, nvm은 Node Version Manager로 여러 Node.js 버전을 설치하고 관리할 수 있는 도구입니다.
# nvm 다운로드 및 설치:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# Node.js 다운로드 및 설치:
nvm install 22
# Node.js 버전 확인:
node -v # "v22.14.0"가 출력되어야 합니다.
nvm current # "v22.14.0"가 출력되어야 합니다.
# pnpm 다운로드 및 설치:
corepack enable pnpm
# pnpm 버전 확인:
pnpm -v
pnpm setup | bash
이제 앱을 실행합니다. node로 실행하면 SSH 세션이 종료되면 애플리케이션도 같이 종료되기 때문에 PM2를 사용하여 앱을 백그라운드에서 실행하도록 하겠습니다.
# pm2 설치
$ pnpm install -g pm2
# 압축 해제
$ mkdir my-app
$ tar -xvzf my-app.tar.gz -C my-app
# 의존성 설치
$ cd my-app
$ pnpm install
# pm2로 앱 실행
$ pm2 start dist/main.js --name my-app
# 실행 확인 (Ctrl + C를 눌러 빠져나옵니다.)
$ pm2 log my-app
# 테스트
$ curl localhost:3000
Hello World!
4. 도메인 설정
도메인 구매
도메인은 가비아, AWS, Cloudflare 등 다양한 곳에서 구매할 수 있으며, Freenom, 도메인.한국 같은 서비스에서 무료로 받을 수도 있습니다. 저는 가비아에서 구매한 도메인이 있기 때문에 이를 사용하겠습니다.
Route 53 설정
구매한 도메인을 AWS에서 사용하려면, 먼저 Route 53에 도메인을 등록하고 네임서버를 설정해야 합니다. 네임서버는 도메인 이름과 실제 서버 IP 주소를 연결해주는 역할을 하는 서버입니다. 우리가 example.com 같은 도메인을 입력하면, 네임서버가 해당 도메인에 대한 IP 주소를 찾아 웹사이트에 접속할 수 있도록 도와줍니다.
AWS 콘솔에서 Route 53에 접속합니다. 호스팅 영역으로 이동해서 “호스팅 영역 생성”을 클릭합니다.
"도메인 이름"에 구매한 도메인을 입력하고 호스팅 영역을 생성합니다.
호스팅 영역이 생성되면 2개의 레코드가 표시됩니다. 이 중에서 "유형"이 NS인 레코드의 "값/트래픽 라우팅 대상"에 있는 네임서버 값을, 도메인을 구매한 사이트의 네임서버 설정에 입력해야 합니다.
도메인의 네임 서버를 변경해야 Route 53에서 설정한 DNS 레코드(A 레코드, CNAME 등)가 정상적으로 적용됩니다. 도메인의 네임서버를 변경하지 않으면, Route 53에서 설정한 값이 적용되지 않고, 기존 도메인 등록 업체의 네임서버를 계속 사용하게 됩니다.
EC2에 적용
이제 이 도메인을 EC2에 연결합니다. 호스팅 영역에서 레코드를 생성하여 도메인과 EC2 인스턴스의 IP를 연결합니다.
호스팅 영역에서 "레코드 생성"을 클릭합니다. 서브 도메인이 필요하다면 "도메인 이름"에 원하는 이름을 입력합니다. "레코드 유형"은 A 레코드를 선택합니다. "값"에는 EC2 인스턴스의 IP를 입력합니다.
모든 입력을 완료했다면 "레코드 생성"을 클릭합니다. 레코드가 적용되기까지 최대 1분 정도 소요될 수 있습니다.
도메인이 정상적으로 연결되었는지 확인하려면, 브라우저에서 도메인을 입력해 호출해봅니다. 하지만, NestJS 프로젝트는 현재 3000번 포트에서 실행되고 있으며, 보안 그룹에는 3000번 포트의 외부 접속을 허용하지 않기 때문에 요청을 보내도 응답이 없습니다.
임시로 3000번 포트의 외부 접속을 허용하겠습니다. EC2로 이동하여 인스턴스에 연결된 보안 그룹을 클릭합니다. "인바운드 규칙 편집"을 클릭합니다.
“규칙 추가”를 클릭하고 “유형”은 사용자 지정 TCP, “포트 범위”는 3000, “소스”는 임시 설정이기 때문에 내 IP를 선택합니다. 이제 “규칙 저장”를 클릭하여 설정을 적용합니다.
이제 브라우저에서 도메인과 포트 번호(http://iammin.shop:3000)로 접속하면, "Hello World!"가 보입니다.
! DNS_PROBE_FINISHED_NXDOMAIN 이슈
AWS가 아닌 다른 사이트에서 구매한 도메인은, 해당 사이트에서 네임 서버를 AWS 네임 서버로 변경해야 합니다. 이 과정에서 변경 사항이 전파 되기까지 최대 24~48시간 정도 소요될 수 있습니다. DNS_PROBE_FINISHED_NXDOMAIN 오류는 네임 서버 변경이 아직 전파되지 않았을 때 발생할 수 있습니다. 네임 서버 변경이 완료 되었는지 확인하려면, 아래 명령어를 실행하여 도메인의 네임 서버 정보를 조회해 보시기 바랍니다.
1. whois : whois 명령어는 도메인의 등록 정보와 네임 서버 정보를 확인하는 명령어입니다. 변경 사항이 전파 되었다면 출력되는 내용 중 “Name Server” 부분에 Route 53의 네임 서버가 표시되어야 합니다.
$ whois <YOUR_DOMAIN> | grep 'Name Server'
2. dig : dig 명령어는 도메인의 DNS 정보를 상세히 조회하는 명령어입니다. 변경 사항이 전파 되었다면 “ANSWER_SECTION”에 Route 53의 네임 서버가 표시되어야 합니다. (전파 되지 않았다면, “ANSWER_SECTION”이 아예 출력되지 않을 수 있습니다.)
$ dig <YOUR_DOMAIN> NS
3. nslookup : nslookup 명령어는 간단하게 도메인의 DNS 정보를 조회하는 명령어입니다. 변경 사항이 전파되었다면 “Non-authoritative answer”에 Route 53의 네임 서버가 표시되어야 합니다.
$ nslookup -type=NS <YOUR_DOMAIN>
5. HTTPS 적용
Nginx 설치
Https를 적용하기 위해서 Nginx를 설치합니다. NestJS에서도 Https를 직접 적용할 수 있지만, 운영 환경에서는 Nginx를 활용하는 것이 더 효율적입니다. Nginx를 사용하면 SSL 인증서 관리가 용이하고, 리버스 프록시를 통한 보안 강화 및 로드 밸런싱 등의 이점을 얻을 수 있습니다.
아래의 명령어를 통해 Nginx를 설치합니다.
$ sudo apt update
$ sudo apt install -y nginx
Nginx 설치가 완료 되었다면, 정상적으로 실행 중인지 확인합니다.
# 실행 상태를 확인합니다.
$ sudo systemctl status nginx
# 실행 상태가 아니라면, 실행합니다.
$ sudo systemctl start nginx
브라우저에서 도메인으로 접속했을 때, Nginx 기본 페이지가 표시되면 정상적으로 실행된 것입니다. 이때, 포트 번호는 생략해도 됩니다. Nginx는 기본적으로 80번 포트를 사용하면, 80번 포트는 생략이 가능합니다.
Certbot 설치
certbot은 Let’s Encrypt에서 제공하는 Https 인증서를 자동으로 발급하고 갱신해주는 도구 입니다. Let’s Encrypt는 무료로 Https 인증서를 발급해주는 기관입니다.
아래의 명령어를 통해 certbot과 certbot과 nginx를 연동하기 위한 플러그인(python3-certbot-nginx)를 설치합니다.
$ sudo apt install -y certbot python3-certbot-nginx
인증서 발급
인증서를 발급하려면 해당 도메인의 소유자임을 인증하는 과정이 필요한데요. (제가 아는 바로는...) 인증 방식에는 두 가지 방법이 있습니다. 첫 번째 방법은 수동 인증 방식으로 Certbot이 출력하는 값을 TXT 레코드로 등록하여 확인하는 방법입니다. 두 번째 방법은 자동 인증 방식으로, 해당 인스턴스에 도메인을 연결하여 인증하는 방법입니다.
위에서 이미 인스턴스에 도메인을 연결했기 때문에 여기서는 자동 인증 방식을 사용하겠습니다. 아래의 명령어를 실행하여 인증서를 발급받습니다. —-nginx 옵션은 인증서 발급과 동시에 Nginx 설정을 자동으로 수정합니다. -d 옵션은 Route 53의 A 레코드에 등록한 도메인을 입력합니다.
$ sudo certbot --nginx -d iammin.shop
인증서 발급이 완료 되었다면, 브라우저에서 HTTPS로 접속이 가능한 것을 확인할 수 있습니다.
Let's Encrypt에서 발급하는 SSL 인증서는 유효기간이 90일입니다. 따러서 만료 전에 갱신해야 하며, certbot을 사용하면 자동으로 갱신할 수 있습니다. 아래의 명령어를 실행하여 인증서의 유효기간을 확인할 수 있습니다.
$ sudo certbot certificates
인증서는 유효기간이 30일 이하로 남은 경우에만 갱신할 수 있습니다. 갱신은 아래의 명령어로 간단하게 수행할 수 있습니다.
$ sudo certbot renew
# --dry-run 옵션을 사용하면 실제로 갱신하지 않고, 갱신에 문제가 없는지 확인할 수 있습니다.
$ sudo certbot renew --dry-run
Nginx에서 리버스 프록시 설정
리버스 프록시 설정을 위해서 nginx 설정 파일을 수정해야 합니다. /etc/nginx/sites-availabe에 있는 default 파일을 vim으로 편집합니다. 꼭 sudo를 붙여주세요. 루트 권한이 있어야만 편집이 가능합니다.
$ vim /etc/nginx/sites-available/default
HTTPS와 관련된 설정이 포함된 Server 블록에 다음의 내용을 추가합니다. 또한, HTTP 요청을 HTTPS로 리다이렉트하는 설정도 함께 추가합니다. 만약 위에서 인증서를 발급할 때 --nginx 옵션을 추가했다면, 해당 설정이 이미 포함되어 있을 수 있습니다.
Server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
...생략
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
if ($host = iammin.shop) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80 ;
listen [::]:80 ;
server_name iammin.shop;
}
이제 nginx를 재시작하여 변경된 설정을 적용합니다. 먼저, nginx -t 명령어를 실행하여 설정 파일에 오류가 없는지 확인한 후, nginx를 재시작합니다.
$ sudo nginx -t
$ sudo systemctl restart nginx
이제 브라우저에서 도메인으로 접속하면, 3000번 포트에서 실행 중인 앱의 응답이 오는 것을 확인할 수 있습니다. HTTP를 사용하여 접속했을 때 HTTPS로 리다이렉트 되는지도 확인해 보시기 바랍니다.
마치며
이번 글에서는 NestJS 프로젝트를 EC2에 배포하고, Nginx를 활용하여 HTTPS를 적용하는 과정을 다뤘습니다. 최근에 백오피스 프로젝트를 진행하면서 개발용 서버를 배포해야 했습니다. 개발용 서버는 복잡한 관리형 서비스를 사용할 필요 없이 EC2에 간단히 배포하는 것이 더 적합하다고 판단하여, EC2 배포를 진행했습니다. 이후, HTTPS 적용이 필요하여 Certbot과 Nginx를 활용해 SSL을 적용하게 되었습니다.
이전에 이 과정을 간단히 노션에 정리해 두었는데, 다시 진행하려고 보니 생략된 부분이 참 많더라구요. 그래서 이번 기회에 시간을 내어 더 자세하게 정리하자는 생각으로 이 글을 작성하게 되었습니다. 이제는 쉽게 잊어버릴 일은 없을 것 같네요.
'Nodejs' 카테고리의 다른 글
비동기 작업 처리를 위한 메시지 큐 연동 - BullMQ 이해하기 (0) | 2025.06.03 |
---|---|
NestJS + Swagger 사용 중 겪은 DTO 네이밍 충돌 이슈 (0) | 2025.05.29 |
NestJS - @Res() 데코레이터와 Interceptor를 함께 사용하면서 겪었던 이슈 (0) | 2025.03.05 |
슬랙으로 RSS 알림 보내기 (6) - 배포 (0) | 2025.03.03 |
슬랙으로 RSS 알림 보내기 (5) - 중복 알림 방지하기 (0) | 2025.03.01 |