상세 컨텐츠

본문 제목

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

23-24/Node.js 1

by YUZ 유즈 2024. 1. 19. 10:00

본문

728x90

익스프레스 미들웨어 배포용으로 수정

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에 대한 설정을 넣어줄 수 있다 -> 설정으로는 levelformattransports 등이 있다.
 
 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은서

 

728x90

관련글 더보기