[Toy Project - Omuk] 13. 배포 환경 설정
부가적인 기능은 나중에 작성해보기로 하고, 일단 배포를 실습해보려고 합니다.
배포 환경 설정
일단 Express에 사용한 일부 미들웨어들을 배포용으로 사용하기 위해서 설정을 변경합니다.
1. morgan
morgan을 노드 환경에 따라 개발용, 배포용으로 나눠서 사용하겠습니다. 아래와 같이 morgan 사용 코드를 변경해줍니다.
if (process.env.NODE_ENV === "production") {
app.use(morgan("combined"));
} else {
app.use(morgan("dev"));
}
2. express-session
쿠키에 대한 설정도 일부 수정합니다. 일단 세션을 레디스에 저장하기 때문에 레디스에 대해서도 수정하겠습니다. 매번 온프레미스에서 레디스를 사용하는 것보다 원격으로 레디스를 사용하는 게 더 좋을 것 같습니다. 그래서 레디스 클라우드를 통해서 세션 저장소를 구축하겠습니다. 일단 redis.com에 접속합니다.
그후 회원가입을 마친 후에 새롭게 구독을 생성하겠습니다.
좌측의 Fixed plans로 가입합니다.
스크롤을 내려서 클라우드를 지정합니다. 가장 가까운 곳이 도쿄여서 도쿄로 설정했고 Free 요금제로 설정했습니다.
이제 데이터베이스를 생성할 차례입니다.
Database name에는 원하는 이름을 지정합니다. 저는 토이프로젝트 이름을 따서 작성했습니다.
데이터베이스를 생성했다면, 우리가 중요하게 볼 것은 Default User, Default Password, Public endpoint입니다. .env 파일에 해당 정보를 사용하여 url을 저장해줍니다.
REDIS_URL=redis://[Default User(기본값은 'default'):[Default Password]@[Public endpoint]
이제 메인 서버 파일로 가서 redisClient 설정을 바꿔줍니다.
const redisClient = createClient({
url: process.env.REDIS_URL,
legacyMode: true
});
...
app.use(
expressSession({
resave: false,
secret: process.env.COOKIE_SECRET,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
},
store: new RedisStore({
client: redisClient,
logErrors: true,
}),
})
);
배포 환경일 때는 session 설정에서 secure도 true로 해주고 proxy 설정도 true로 바꿉니다. 일단 https 적용하진 않을 예정이라서, proxy만 true로 변경했습니다. proxy는 https를 설정하기 위해 node앞에 서버를 두었을 때 설정합니다. 조건 삼항 연산자로 설정하려고 했는데 가독성이 떨어지는 것 같아서 변수로 옵션을 설정하고 조건문을 통해 한 번에 바꿀 예정입니다.
const sessionOption = {
resave: false,
secret: process.env.COOKIE_SECRET,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
},
store: new RedisStore({
client: redisClient,
logErrors: true,
}),
};
if (process.env.NODE_ENV === "production") {
sessionOption.proxy = true;
// sessionOption.cookie.secure = true;
}
app.use(expressSession(sessionOption));
3. 데이터베이스 Sequelize 설정
process.env.POSTGRES_PASSWORD를 통해서 데이터베이스를 연결하려고 합니다. 그런데 실무와 개발 환경에 따라 데이터베이스를 다르게 연결하고자 기존 config.json을 js파일로 바꿔서 process.env로 접근하도록 바꾸는 것이 좋아보입니다.
import dotenv from "dotenv";
dotenv.config();
export default {
development: {
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "omuk",
host: "127.0.0.1",
dialect: "postgres",
port: 5432,
},
test: {
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "omuk_test",
host: "127.0.0.1",
dialect: "postgres",
},
production: {
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "omuk",
host: "127.0.0.1",
dialect: "postgres",
logging: false,
},
};
4. pm2
pm2를 사용하면 배포된 서버가 만약 종료되었을 때 자동으로 서버를 다시 켜주는 관리 도구입니다. 관리자가 예측하지 못한 에러로 인해서 서버가 종료될 수 있으니 실무 환경에서는 pm2 패키지가 꼭 필요합니다.
또한, 싱글 스레드 싱글 프로세스에서 벗어나 멀티 프로세스로 노드 프로세스를 구동할 수 있다는 점입니다. 단점으로는 각 프로세스는 서버 자원을 공유하지 못합니다. 그렇기 때문에 미리 세션 저장소를 레디스로 구축해놓았습니다.
바로 pm2를 설치해보겠습니다.
npm i pm2
package.json 파일을 수정해서 pm2를 실행하는 스크립트를 작성하겠습니다.
"scripts": {
"test": "jest",
"start": "NODE_ENV=production PORT=80 pm2 start app.js",
"dev": "NODE_ENV=development PORT=3000 nodemon app"
}
이제 npm start를 통해서 서버를 실행시켜보면,
이처럼 백그라운드에서 노드 프로세스를 작동 시키고 있습니다. 여기서 cpu의 모든 코어를 사용하기 위해서 클러스터링 모드로 서버를 실행시키려고 합니다. 이를 위해서 package.json에서 start의 스크립트에서 마지막에 -i 0
을 입력합니다.
그 이후에 npm start로 서버를 실행시키면 아래와 같은 결과가 나옵니다.
제 CPU는 8코어라서 8개의 프로세스를 사용하고 있습니다.
6. winston
console.log와 console.error를 언제 출력했는지 정확히 알 수 없어서 로그를 다른 파일로 저장하거나 데이터베이스에 저장하는 과정이 필요합니다. 그래서 winston을 사용합니다. 배포는 자세히 공부를 안 해서 책 내용을 많이 참고했습니다.
import { createLogger, format, transports } from "winston";
const logger = createLogger({
level: "info",
format: format.json(),
transports: [
new transports.File({ filename: 'combined.log' }),
new transports.File({ filename: 'error.log', level: 'error' }),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(new transports.Console({ format: format.simple() }));
}
export default logger;
level은 로그의 심각도, format은 로그의 형식, transports는 어떻게 저장할지를 지정하는 것입니다.
이제 메인 서버 파일에서 해당 js파일을 불러오고 log를 작성할 수 있도록 원하는 라우터에 로거를 실행하도록 하였습니다.
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
logger.info("404 Error");
logger.error(error.message);
res.status(404).send("404 Error");
});
app.use((err, req, res, next) => {
const error = new Error(
`${req.method} ${req.url} 에서 심각한 에러가 발생했습니다.`
);
logger.info("500 Error");
logger.error(error.message);
res.status(500).send("Server Error");
});
여기서 winston을 조금 더 세팅하겠습니다. 먼저 조금 더 빠르게 로그를 확인할 수 있도록 날짜, 사이즈별로 로그를 확인하는 winston-daily-rotate-file 패키지를 설치했습니다.
import winston from "winston";
import "winston-daily-rotate-file";
import path from "path";
import { fileURLToPath } from "url";
const { createLogger, format, transports } = winston;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
const colors = {
error: "red",
warn: "yellow",
info: "green",
http: "magenta",
debug: "blue",
};
winston.addColors(colors);
const logger = createLogger({
level: "http" ? process.env.NODE_ENV === "production" : "debug",
levels,
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
format.printf((info) => {
return `${info.timestamp} ${info.level}: ${info.message}`;
})
),
transports: [
new transports.DailyRotateFile({
level: "debug",
dirname: path.join(__dirname, "/logs", "/combined"),
filename: "combined-%DATE%.log",
datePattern: "YYYY-MM-DD-HH",
zippedArchive: true,
maxSize: 7,
}),
new transports.DailyRotateFile({
level: "error",
dirname: path.join(__dirname, "/logs", "/error"),
filename: "error-%DATE%.log",
datePattern: "YYYY-MM-DD-HH",
zippedArchive: true,
maxSize: 30,
}),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(new transports.Console({ format: format.simple() }));
}
export default logger;
로그가 찍힌 시간과 정보를 찍기 위해서 combine을 통해 로그를 찍도록 했습니다.
7. helmet, hpp
보안에 도움이 되는 패키지들입니다.
npm i helmet hpp
if (process.env.NODE_ENV === "production") {
app.use(morgan("combined"));
app.use(helmet());
app.use(hpp());
} else {
app.use(morgan("dev"));
}
이후 깃을 설정하겠습니다.