[Node.js 1팀] 15장 AWS와 GCP로 배포하기

익스프레스 미들웨어 배포용으로 수정
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}
- process.env.NODE_ENV는 배포 환경인지 개발 환경인지를 판단할 수 있는 환경 변수이다
- 배포 환경일 때는 morgan을 combined 모드로 사용하고, 개발 환경일 때는 dev 모드로 사용한다.
.env 파일은 정적 파일이기 때문에 NODE_ENV를 동적으로 바꾸는 방법이 필요하다. (뒤에 cross-env 설명 참고)
const sessionOption = {
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
};
if (process.env.NODE_ENV === 'production') {
sessionOption.proxy = true;
// sessionOption.cookie.secure = true;
}
app.use(session(sessionOption));
- proxy와 cookie.secure를 true로 바꾼다.
- 무조건 이렇게 적용해야 하는 것은 아니며 https 를 적용할 때만 true로 바꾸면 된다.
데이터베이스 배포용으로 수정
시퀄라이즈에서의 문제점
1. 비밀번호가 하드 코딩되어 있다.
2. JSON 파일이므로 변수를 사용할 수 없다. -> 시퀄라이즈는 JSON 대신 JS 파일을 설정 파일로 쓸 수 있게 지원한다.
config 폴더에서 config.json을 지우고 대신 config.js를 생성한다.
require('dotenv').config(); // dotenv 모듈 사용
module.exports = {
development: { // process.env가 development일 때 해당 설정내용 적용
username: 'root',
password: process.env.SEQUELIZE_PASSWORD,
database: 'nodebird',
host: '127.0.0.1',
dialect: 'mysql',
},
test: {
username: "root",
password: process.env.SEQUELIZE_PASSWORD,
database: "nodebird_test",
host: "127.0.0.1",
dialect: "mysql"
},
production: { // process.env가 production일 때 해당 내용 적용
username: 'root',
password: process.env.SEQUELIZE_PASSWORD,
database: 'nodebird',
host: '127.0.0.1',
dialect: 'mysql',
logging: false, // 배포환경일 때 쿼리 명령어를 숨김
},
};
- JS 파일이므로 dotenv 모듈을 사용할 수 있다.
- password 외에도 process.env로 변경해도 된다. -> username 속성이나 host 속성은 각각 아이디와 DB 서버 주소 역할을 하므로 숨기는 게 좋다.
- process.env가 development일 떼, production일 때 각각의 설정 내용이 적용된다.
- 쿼리를 수행할 때마다 콘솔에 SQL문이 노출된다. 배포 환경에서는 어떤 쿼리가 수행되는지 숨기는 것이 좋다. production일 경우에는 logging에 false를 줘서 쿼리 명령어를 숨긴다.
.env 파일에 데이터베이스 비밀번호를 기록해 둔다.
SEQUELIZE_PASSWORD=데이터베이스 비밀번호
cross-env
cross-env 패키지를 사용하면 동적으로 process.env(환경변수)를 변경할 수 있다.
기존 package.json의 scripts 부분을 아래 코드로 변경한다.
"scripts": {
"start": "NODE_ENV=production PORT=80 node server",
"dev": "nodemon server",
"test": "jest"
},
- 서버 실행을 위한 npm 스크립트를 두 개로 나눴다.
- npm start는 배포 환경에서 사용하는 스크립트이다.
- npm start 시 process.env 동적으로 production이 되고, process.env.PORT가 80이 된다. - npm run dev는 개발 환경에서 사용하는 스크립트이다.
그러나 리눅스 환경에서는 process.env를 이 방식으로 설정할 수 없다. -> cross-env가 사용된다.
corss-env 설치
npm i cross-env
package.json 수정
{
...
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 node server", // 앞에 cross-env를 붙임
"dev": "nodemon server",
"test": "jest"
},
- 앞에 cross-env를 붙임으로써 윈도우에서도 실행된다.
sanitize-html, csurf
sanitize-html과 csurf 패키지는 각각 XSS(Cross Site Scripting), CSRF(Cross Site Request Forgery) 공격을 막기 위한 패키지이다.
npm i sanitize-html
npm i csurf
XSS는 악의적인 사용자가 사이트에 스크립트를 삽입하는 공격이다.
- 악성 사용자가 게시글이나 댓글 등을 업로드할 때 자바스크립트가 포함된 태그를 올리면, 나중에 다른 사용자가 그 게시글이나 댓글을 볼 때 해당 스크립트가 실행되어 예기치 못한 동작을 하게 된다.
const sanitizeHtml = require('sanitize-html');
const html = "location.href = '<a href=https://gilbut.co.kr'>https://gilbut.co.kr'</a>"; console.log(sanitizeHtml(html)); // ''
- 사용자가 업로드한 HTML을 sanitize-html 함수로 감싸면 허용하지 않는 태그나 스크립트는 제거된다.
- 두 번째 인수로 허용할 부분에 대한 옵션을 넣을 수 있다.
CSRF는 사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격이다.
- 내가 한 행동이 내가 한 것이 맞다는 사실을 인증해야 한다.
- 이때 CSRF 토큰이 사용되고, csurf 패키지는 이 토큰을 쉽게 발급하거나 검증할 수 있도록 돕는다.
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => { // form을 렌더링하는 라우터
res.render('csrf', { csrfToken: req.csrfToken() });
});
app.post('/form', csrfProtection, (req, res) => { // form에서 보낸 데이터를 처리하는 라우터
res.send('ok');
});
- 익스프레스의 미들웨어 형식으로 동작한다.
- 렌더링할 때 CSRF 토큰을 같이 제공한다.
- 토큰은 req.csrfToken()으로 가져올 수 있다.
- 프런트엔드에 렌더링된 CSRF 토큰을 나중에 form을 제출할 때 데이터와 함께 제출하면 된다.
pm2
pm2는 원활한 서버 운영을 위한 패키지이다.
장점 )
서버가 에러로 인해 꺼졌을 때 서버를 다시 켜준다.
또한 멀티 프로세싱을 지원해 노드 프로세스 개수를 한 개 이상으로 늘릴 수 있다.
- pm2를 사용해서 프로세스를 여러 개 만들면 다른 코어들까지 사용할 수 있다.
- 클라이언트로부터 요청이 올 때 알아서 요청을 여러 노드 프로세스에 고르게 분배하여 하나의 프로세스가 받는 부하가 적어지므로 서비스를 더 원활하게 운영할 수 있다.
단점 )
멀티 스레딩이 아니므로 서버의 메모리 같은 자원을 공유하지 못한다.
- 세션을 공유할 수 있게 해주기 위해 주로 멤캐시드(Memcached)나 레디스(Redis) 같은 서비스를 사용한다.
- 최대한 프로세스 간에 공유하는 것(세션 등)이 없도록 설계해야 하며 공유해야 하는 데이터가 있다면 데이터베이스를 사용해야 한다.
npm i pm2
pm2는 nodemon처럼 콘솔에 입력하는 명령어이다.
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 pm2 start server.js",
"dev": "nodemon server",
"test": "jest"
},
start 스크립트에 node server 대신 pm2 start server.js를 입력한다.
- pm2로 스크립트를 실행하는 명령어이다.
npm start로 pm2를 실행하면 노드 프로세스가 실행된 후 콘솔에 다른 명령어를 입력할 수 있다.
- pm2가 노드 프로세스를 백그라운드로 돌리므로 가능해진다.
백그라운드에서 돌고 있는 노드 프로세스를 확인할 방법이 필요한데, npx pm2 list 명령어를 사용한다.
- 현재 프로세스 정보가 표시된다.
pm2 프로세스를 종료하고 싶다면 콘솔에 npx pm2 kill을 입력하면 된다.
서버를 재시작하고 싶다면 npx pm2 reload all 명령어를 사용한다.
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 pm2 start server.js -i 0",
"dev": "nodemon server",
"test": "jest"
},
- 0은 현재 CPU 코어 개수만큼 프로세스를 생성한다는 뜻이다.
- -1은 프로세스를 CPU 코어 개수보다 한 개 덜 생성하겠다는 뜻이다.
npx pm2 monit
현재 프로세스를 모니터링할 수 있다.
winston
실제 서버를 운영할 때 console.log와 console.error를 대체하기 위한 모듈이다.
npm i winston
logger.js 작성
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.json(),
transports: [
new transports.File({ filename: 'combined.log' }), // combined.log 파일에 info 이상 단계의 모든 로그를 기록
new transports.File({ filename: 'error.log', level: 'error' }), // error.log 파일에 error 단계의 로그만 기록
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;
- winston 패키지의 createLogger 메서드로 logger를 만든다.
- 인수로 logger에 대한 설정을 넣어줄 수 있다 -> 설정으로는 level, format, transports 등이 있다.
• level은 로그의 심각도를 의미
• format은 로그의 형식
• transports는 로그 저장 방식을 의미
logger 객체를 만들어 다른 파일에서 사용하면 된다.
const logger = require('./logger');
logger.info('hello');
logger.error(error.message);
- 로그를 콘솔에만 출력하는 것이 아니라, 파일로도 저장할 수 있어 실제 서비스를 운영할 때 유용하다.
helmet, hpp
서버의 각종 취약점을 보완해주는 패키지들이며, 익스프레스 미들웨어로서 사용할 수 있다.
npm i helmet hpp
개발 환경에서는 사용할 필요가 없으므로 배포 환경일 때만 적용하면 된다.
const helmet = require('helmet');
const hpp = require('hpp');
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
app.use(
helmet({
contentSecurityPolicy: false, // 필요 없는 옵션은 해제
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: false,
}),
);
app.use(hpp());
} else {
app.use(morgan('dev'));
}
connect-redis
멀티 프로세스 간 세션 공유를 위해 레디스와 익스프레스를 연결해주는 패키지이다.
세션 아이디와 실제 사용자 정보를 데이터베이스에 저장할 때 사용하는 데이터베이스가 레디스이다.
npm i redis connect-redis
레디스를 호스팅해주는 redislabs를 쓰는 것이 편리하다.
REDIS_HOST=redis-16721.c14.us-east-1-2.ec2.cloud.redislabs.com
REDIS_PORT=16721
REDIS_PASSWORD=41lL904Z153mw6YkZZd1TDCQkcoZrJXG
.env 파일에 Public endpoint와 password를 넣는다.
const hpp = require('hpp');
const redis = require('redis');
const RedisStore = require('connect-redis')(session); // session을 인수로 넣어서 호출
dotenv.config(); // .env 파일에 적힌 process.env 객체의 값들은 dotenv.config() 이후에 생성됨
const redisClient = redis.createClient({ // redisClient 객체를 생성
url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
password: process.env.REDIS_PASSWORD,
legacyMode: true,
});
redisClient.connect().catch(console.error);
const pageRouter = require('./routes/page');
...
const sessionOption = {
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
store: new RedisStore({ client: redisClient }), // client 속성에 redisClient 객체를 연결
};
connect-redis 패키지로부터 RedisStore 객체를 require한다.
- connect-redis는 express-session에 의존성이 있기 때문에 session을 인수로 넣어서 호출해야 한다.
- redis 패키지의 createClient 메서드로 redisClient 객체를 생성한 후 url과 password 속성에 접속 정보를 입력한다.
express-session 미들웨어에는 store 옵션을 추가하여 RedisStore에 저장한다.
RedisStore의 옵션으로 client 속성에 redisClient 객체를 연결하면 된다.
nvm, n
노드 버전을 업데이트하기 위한 패키지이다.
설치된 노드 버전을 확인하는 명령어는 nvm list이다.
nvm list
새로운 버전을 설치하고 싶다면 nvm install [버전]을 입력한다.
nvm install 18.7.0
설치된 버전을 사용하려면 nvm use [버전명]을 입력한다.
nvm use 18.7.0
업그레이드 후 npm 충돌 시
노드 버전을 업그레이드한 후 기존 npm 패키지들이 동작하지 않는 경우 npm rebuild 명령어로 해결하면 된다.
깃과 깃허브 사용하기
.gitignore 파일 생성
node_modules
uploads
*.log
coverage
// 실제 서버에서는 .env도 깃에 추가하면 안됨
AWS 시작하기
- 인스턴스를 생성한다.
- SSH를 사용하여 연결한다.
- 데이터베이스를 설치한다.
$ sudo apt-get update
$ sudo apt-get install -y gnupg
$ sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.23-1_all.deb
$ sudo dpkg -i mysql-apt-config_0.8.23-1>_all.deb
$ sudo apt update
$ sudo apt-get install -y mysql-server
- git clone 명령어를 사용해 깃허브에 올렸던 소스 코드를 내려받는다.
- 아파치 서버를 종료하는 명령어를 입력한다.
$ cd /opt/bitnami
$ sudo ./ctlscript.sh stop apache
- npm 패키지를 설치하고 서버를 실행한다.
$ cd ~/node-deploy
$ npm ci
$ npx sequelize db:create --env production
$ sudo npm i -g pm2
$ sudo NODE_ENV=production PORT=80 pm2 start server.js -i 0
서버가 실행되지 않는다면 sudo pm2 logs --err 명령어를 입력해 어떤 에러가 발생했는지 확인할 수 있다.
- sudo pm2 reload all로 서버를 재시작하면 된다.
GCP 시작하기
- 프로젝트를 생성한다.
- 인스턴스를 생성한다.
- 외부 IP 옆 SSH를 눌 해당 인스턴스의 콘솔로 접근한다.
GCP에 배포하기
- 루트계정으로 접속한다.
sudo su
- 우분투에 노드와 MySQL를 설치한다.
$ cd node-deploy
$ npm ci
$ npx sequelize db:create --env production
$ npm i -g pm2
$ NODE_ENV=production PORT=80 pm2 start server.js -i 0
- npm 패키지들을 설치하고 서버를 실행한다.
문제
1. ( process.env.NODE_ENV )는 배포 환경인지 개발 환경인지를 판단할 수 있는 환경 변수이다
2. ( cross-env ) 패키지를 사용하면 정적 파일인 .env를 동적으로 변경할 수 있다.
3. 시퀄라이즈는 JSON 대신 ( JS ) 파일을 설정 파일로 쓸 수 있게 지원한다.
4. 쿼리를 수행할때마다 콘솔에 SQL문이 노출되지 않기 위해 production일 경우 ( logging )에 ( false )를 줘서 쿼리 명령어를 숨긴다.
5. ( CSRF )는 사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격이다.
6. ( connect-redis ) 패키지는 멀티 프로세스 간 세션 공유를 위해 레디스와 익스프레스를 연결해주는 패키지이다.
7. ( pm2 )는 멀티 프로세싱을 지원하지만 서버의 메모리 같은 자원을 공유하지 못한다.
코드문제
1. 주석 부분의 코드를 채우시오.
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.json(),
transports: [
// combined.log 파일에 info 이상 단계의 모든 로그를 기록
// error.log 파일에 error 단계의 로그만 기록
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;
답)
transports: [
new transports.File({ filename: 'combined.log' }),
new transports.File({ filename: 'error.log', level: 'error' }),
]
2. 3개의 주석에 알맞은 코드를 작성하시오.
if (// 배포환경일 경우) {
// morgan을 combinded 모드로 사용
} else {
// morgan을 dev 모드로 사용
}
답)
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}
출처: 조현영 , 『Node.js 교과서』 개정판 3판, 길벗, 15장
Node.js #1
Editor : 7b은서