서버와 클라이언트 배포를 위한 AWS EC2 설정
✒️ 2025-05-26 14:24 내용 수정
- 팀 프로젝트의 서버와 클라이언트 배포를 위해 AWS EC2를 사용하였다.
- DB는 팀 프로젝트에서 사용한 AWS RDS를 사용하였고, DB 설정은 팀원이 맡았기에 이 부분은 제외하고 기록했다.
참고 자료 : Geeksforgeeks What is Elastic Compute Could(EC2), AWS EC2 Node.js 서버 배포, Iheunoia's AWS 웹 어플리케이션 배포하기(EC2), 오호's AWS EC2 프론트 배포 이렇게만 해보자, hakawati's AWS에 EC2 생성하기
1. EC2 가입 및 인스턴스 생성
- AWS EC2(Elastic Compute Cloud)는 아마존 웹 서비스에서 제공하며, 가상 환경에서 컴퓨터를 임대해 사용할 수 있다. 이를 이용하여 어플리케이션 배포 및 확장 등을 할 수 있다.
- 개발 환경 설정 메모를 위해 EC2 인스턴스 생성 방법을 정리했다.
2. EC2와 RDS 연결
- 프로젝트의 DB는 AWS RDS를 사용하였고, 이번 배포 설정을 진행하면서 EC2와 RDS를 연결했다.
- AWS RDS 생성 참고자료 : 이지IT's AWS Cloud RDS 생성하기
- AWS EC2와 RDS 연결 참고 자료 : 배규리's AWS EC2 + RDS 연동하기
3. AWS EC2 인스턴스에서 ubuntu 설정
- 배포할 프로젝트의 서버와 클라이언트 파일을 모두 github에 올려두었기에 git을 사용해서 파일을 내려받을 예정이다.
- ubuntu 환경에서의 동작은 Linux 명령어 참고.
- 파일 및 디렉토리 관리 명령어, Telnet과 OpenSSH 등 참고.
- 인스턴스에 연결을 성공했다면 배포 설정을 진행하기 전에 파일 편집용 vim을 설치한다.
# 시스템 패키지 목록을 업데이트하고, 필요한 패키지를 업그레이드
sudo apt update -y
# vim 설치
sudo apt-get install vim
- EC2와 RDS를 연결했다면 인스턴스에 사용하려는 DB 서버 CLI를 설치한다. 설치가 완료 되었다면 DB에 접속하여 잘 연결되는지 확인한다.
- 프로젝트에선 MySQL을 사용해서
mysql-server를 설치했다.
- 프로젝트에선 MySQL을 사용해서
# mysql 설치
sudo apt install mysql-server
# 접속
mysql -u 계정이름 -p -h host주소(RDS엔드포인트)
- ubuntu 홈 경로
/home/ubuntu에서 배포할 서버의 root 폴더로 이동한다.- 만약 폴더를 새로 만들어서 작업하고 싶다면
mkdir 폴더명으로 새 폴더를 생성한다.
- 만약 폴더를 새로 만들어서 작업하고 싶다면
cd server
- nodejs, npm을 설치한다.
- npm 설치 과정에서 에러가 발생하면 aptitude를 설치하여 aptitude를 통해 npm을 설치하는 방법도 있다.
- 참고 자료 : stackoverflow How do I resolve 'The following packages have umet dependencies'
# 패키지 업데이트 확인
sudo apt update
# nodejs npm 설치
sudo apt-get install nodejs npm
- 설치된 node와 npm의 버전도 확인해본다.
- 설치가 안되어 있다면 버전 확인이 안된다.
# node 버전 확인
node -v
# npm 버전 확인
npm -v
- 의존성 패키지를 설치한다.
- node_modules 폴더가 생성되고 파일들을 다운 받는다.
Node 수업에서 패키지 관련 설명을 제대로 들었으면 node_modules를 로컬에서 인스턴스로 파일전송하는 뻘짓을 하지 않았을 것이다
- node_modules 폴더가 생성되고 파일들을 다운 받는다.
npm install
- github에 올려둔 서버와 클라이언트 파일을 받기 위해
git clone git주소를 실행한다.
git clone https://github.com/이름/리포지토리
- 만약 github에 올리지 않은 파일을 로컬에서 원격으로 전송해야 한다면 로컬에서
scp명령어로 파일을 전송할 수 있다.- ssh-keygen으로 생성한 키 페어 파일이 있는 위치에서 수행하거나 다른 위치일 경우
ssh키파일에 경로까지 작성한다. - Telnet과 OpenSSH#파일 전송 참고.
- ssh-keygen으로 생성한 키 페어 파일이 있는 위치에서 수행하거나 다른 위치일 경우
# local VSC terminal
scp -i ssh키파일 전송할파일 ubuntu@host:/파일저장경로
- 이제 server와 client의 host 설정을 변경한다.
.env파일에 HOST를 환경 변수로 저장했다면 HOST를HOST=0.0.0.0으로 변경한다.- 이렇게 적용한 후에 서버나 클라이언트를 실행하고, 로컬 브라우저에서 EC2의 동적 ip(
17.114.69.1같은 것)와 개방 허용된 포트를 입력하면 접근할 수 있다.
- 이렇게 적용한 후에 서버나 클라이언트를 실행하고, 로컬 브라우저에서 EC2의 동적 ip(
# client .env, server .env 둘 다 있다면 모두 적용
HOST=0.0.0.0
# EC2에서 개방 설정한 PORT로 변경
PORT=5000
// server.js
require("dotenv").config();
const PORT = process.env.PORT || 5000;
const HOSTNAME = process.env.HOSTNAME || 'localhost';
server.listen(PORT, HOSTNAME, () => {
console.log(`server on : http://${HOSTNAME}:${PORT}/`);
});

4. 메모리 부족 현상 해결
- EC2에서 서버는 잘 가동되었지만 이상하게 클라이언트를 실행할 때마다 인스턴스가 동작을 멈췄다.
- 인스턴스를 다시 껐다가 킨 후에도 클라이언트 실행엔 비슷한 문제가 생겨 이번엔 실행을 해두고 다른 터미널로 ssh 연결을 해서 아래 명령어들을 입력해보니 생각보다 메모리에 여유 공간이 없었다.
- 사진은 서버만
pm2로 가동한 상태로 찍은 사진들이다. - 클라이언트는 가동하면 이 메모리 사용량 명령어들의 결과 조차도 늦게 떴다.
- 사진은 서버만
# 메모리 사용량 확인
free
# 읽기 쉬운 형태로 출력
free -h
- Swap 메모리가 0이다.
# 메모리 사용량 모니터링
# -r : 메모리 사용량 보고
# 1초 간격
sar -r 1
- 서버와 기타 프로그램들 가동 상태로도 메모리 사용률이 56% 정도 된다.
# 실시간 프로세스 및 자원 상태 모니터링
top
-
mysql이 메모리를 많이 사용하고 있다.
-
2024-06-18 추가 : 앱이 사용한 CPU 사용률을 확인하기 위해
sysstat을 설치해서 확인했다.
참고 자료 : 귀찮Lee's Linux에서 CPU/메모리 상태 확인하는 방법
sudo apt install sysstat
mpstat
-
참고자료에선 서버의 경우
%usr(앱이 사용한 CPU 사용률)이 20이 넘으면 위험하다고 한다.
-
비슷한 문제를 겪은 사람이 있는지 확인해보니 프리티어 OS를 사용해서 RAM이 부족해서 메모리를 추가해줘야 한다는 글이 많았다.
-
따라서 메모리 스왑을 통해 메모리의 부족한 부분을 디스크의 일부로 사용하는 방법을 적용하기로 했다.
- 참고 자료 설명에 따르면 메모리에 비해 디스크의 속도가 느려 스왑 시 속도가 느려진다고 한다.
sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=2048
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
sudo chmod 600 /mnt/swapfile실행 전에sudo mkswap /mnt/swapfile과sudo swapon /mnt/swapfile를 먼저 실행했더니 파일 권한을 변경해야 한다는 메시지가 떠서 뒤늦게 수정했는데도 잘 적용되었다.
// client package.json
"scripts": {
"start": "react-scripts --max_old_space_size=4096 start",
"build": "react-scripts GENERATE_SOURCEMAP=false --max_old_space_size=4096 build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
# heap 메모리 부족 현상 해결을 위한 client .env 설정
GENERATE_SOURCEMAP=false
NODE_OPTIONS="--max-old-space-size=4096"
-
설정을 모두 적용하고
npm run client(클라이언트 가동 스크립트)를 실행했더니 이번엔 잘 작동되었다. -
다만 작동은 되는데 로컬(집에서 작업중인 컴퓨터)에서 EC2 ip를 입력하고 접근하려고 하면 접근이 거부되어서 원인을 찾아보고 있다.
- 접속 불가 및 메모리 문제 때문에 클라이언트를 Vercel로 배포하기로 결정했다.
- React Vercel 배포 참고.
-
스왑 메모리 해제는 아래 명령어로 적용할 수 있다.
- 참고 자료에선 가상 메모리를 사용할 경우 퍼포먼스 문제가 발생할 수 있어 임시 방편으로 사용하고 사양을 올릴 것을 추천했다.
sudo swapoff -v /mnt/swapfile
sudo rm /mnt/swapfile
5. Nginx 설치 및 설정
- 아직 수정 중인 자료이므로 불확실한 정보가 포함되어 있을 가능성이 높다.
- Vercel로 배포한 클라이언트에서 도메인 없이 ip 주소로 EC2에 배포한 서버로 요청을 했더니 서버로 보내는 요청이 HTTP 요청이라서 차단되었다.
- HTTPS로 로드된 페이지에서 HTTP로 요청을 보내려고 하는 경우에 발생하는 문제다.
Mixed Content: The page at '[https://test.vercel.app/api](https://test.vercel.app/api)' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '[http://server_domain/api/](http://server_domain/api/)'. This request has been blocked; the content must be served over HTTPS.
- 이를 해결하기 위해
nginx를 사용하여 reverse proxy를 설정하였다.- HTTP 서버에 HTTPS 요청이 들어오면 이를
nginx가 HTTP 요청으로 전달하고, 서버에서 클라이언트로 응답을 보낼 때 HTTP 응답을 HTTPS 응답으로 다시 보낸다.
- HTTP 서버에 HTTPS 요청이 들어오면 이를
- 먼저 패키지 업데이트를 진행한 후,
nginx패키지를 다운 받는다.
# 패키지 업데이트
sudo apt update
# Nginx 설치
sudo apt install nginx
- 선택적 옵션으로 ubuntu 방화벽인
ufw에nginx포트 허용 설정을 진행한다.
# 방화벽에서 Nginx의 기본 포트인 80(HTTP)와 443(HTTPS) 허용
sudo ufw allow "Nginx Full"
- 이번엔 https 인증서를 위한
certbot을 설치한다.
# https 인증서를 위한 certbot 설치
sudo snap install --classic certbot
certbot에nginx자동 설정을 진행한다.
# nginx 설정
sudo certbot --nginx
-
순차적으로 나오는 설명을 보고 본인 이메일과 도메인 이름을 입력해서 설정을 완료한다.
-
서버에 https 설정이 되어 있지 않아
nginx에서 https 요청이 들어오면 리버스 프록시를 하도록 설정했다.vi나nano로/etc/nginx/nginx.conf파일을 열고, Virtual Host Configs 설정에server설정을 추가한다.
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
# http 포트(80)으로 들어오는 요청을 수신
# 모든 http 요청을 301 코드(moved permanently)와 함께 연결할 주소로 리디렉션(http->https)
server {
listen 80;
listen [::]:80;
# 서버 도메인 설정
server_name 서버도메인;
return 301 연결할주소;
}
# https 포트(443)으로 들어오는 요청을 수신
server {
listen 443 ssl; # managed by Certbot
listen [::]:443 ssl;
# 서버 도메인 설정
server_name 서버도메인;
# SSL 인증서 경로
ssl_certificate "인증서주소"; # managed by Certbot
ssl_certificate_key "인증서주소"; # managed by Certbot
# 기타 SSL 설정
include "/etc/letsencrypt/options-ssl-nginx.conf"; # managed by Certbot
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem"; # managed by Certbot
# 서버 도메인의 루트 경로에 대한 요청을 처리
location / {
# 로컬 host의 10000번 포트로 전달함
proxy_pass http://localhost:10000;
# 원본 요청의 호스트 헤더를 전달
proxy_set_header HOST $host;
# 원본 요청의 클라이언트 ip 주소를 전달
proxy_set_header X-Real-IP $remote_addr;
# 원본 요청의 X-Forwarded-For 헤더를 전달
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 원본 요청ㅇ의 프로토콜을 https로 설정
proxy_set_header X-Forwarded-Proto https;
# proxy 응답의 리디렉션을 비활성화
proxy_redirect off;
}
}
- 변경된 설정을 적용하기 위해
nginx를 재시작한다.
# nginx 재시작
sudo systemctl restart nginx
- 이제 클라이언트와 서버 모두 새로 지정한 도메인이 있다면 도메인으로 연결시킨다.
- 클라이언트는 Vercel로 배포해서 Vercel에서 구매한 도메인을 설정해줬다.
- 서버는 EC2 인스턴스에서 구매한 도메인과 IP를 연결시켰다.
- 클라이언트와 서버의
.env파일에서 도메인 주소를 환경변수로 저장하고,axios와 서버 설정 등에서도 도메인으로 주소를 바꿔준다.
# client/.env
# 서버 주소
REACT_APP_SERVER_IP = 구매한 서버 도메인
// src/lib/axios.js
// 이제 클라이언트에서 axios를 요청할 때 서버 도메인으로 가게끔 설정한다.
import axios from 'axios';
axios.defaults.withCredentials = true; // 쿠키 공유 허용
const server = process.env.REACT_APP_SERVER_IP
const instance = axios.create({
baseURL : `https://${server}/api/`
});
export default instance;
# server/.env
# 도메인
SERVER_DOMAIN = 서버 도메인
CLIENT_DOMAIN = 클라이언트 도메인
- 서버에선 CORS 대비 whiteList도 편집했다.
// server.js
const whiteList = [
`http://${HOSTNAME}:${PORT}`, `https://${HOSTNAME}:${PORT}`,
`http://0.0.0.0:3000`, `https://0.0.0.0:3000`,
`https://${SERVER_DOMAIN}`,
`https://${CLIENT_DOMAIN}`, `http://${CLIENT_DOMAIN}:3000`
]