nextjs pm2으로 무중단 배포해보기(로컬환경)

악음·2022년 3월 4일
11
post-thumbnail

pm2를 실행하기에 앞서 ecosystem을 설정해주자

ecosystem.config.json이란 pm2의 설정을 json형식으로 관리할 수 있고 pm2 에서 제공해주는 option을 보다 쉽게 관리할수있도록 도와준다.
참고한싸이트 : 라인 엔지니어링
참고한싸이트 : 로스템 블로그
참고한 싸이트 : 로스템 git

정해주기전에 무중단 배포가 진행되는 과정을 보자면..

  1. pm2으로 cluster 모드로 프로젝트를 멀티 스레트로 띄운다
    (node는 기본적으로 싱글스레드이나 pm2를 사용해 멀티 스레드로 프로젝트를 관리 할 수있다.)
  2. 그렇게 멀티 스래드로 진행하다가 프로젝트의 변경점이 생기는경우 pm2 reload를(프로레스를 죽이지 않고 다시 시작시켜준다)통해 프로세스가 죽지않고 무중단 배포를 진행시킨다.
  3. 이러는 과정에서 서버와 pm2가 서로 유기적으로 작동해야하는데 이러기 위해선 순수한 nextjs 보단 express를 next에 감싸서 설정하는것이 편하다.
  4. 유기적으로 작동하는 방식은 다음과 같다
    • 프로세스가 실행되고 있는 상황에서
    • pm2가 reload를 통해 프로젝트를 제시작 함
    • 그렇게되면 pm2는 자동으로 기존에있던 0번 프로세스를 _old_0 프로세스로 옮겨두고 새로운 0번 프로세스를 생성한다
    • 여기서 새로운 0번 프로세스는 앱이 켜진후뒤에 보낸 "ready"라는 요청을 받게되면
    • _old_0 프로세스를 죽인뒤에 해당 프로세스가 켜고있는 앱에 "sigint" 시그널을 보네어 애당 앱을 끄게 만든다. 하지만 여기서 일정시간이 지난후 (1600ms) 종료되지 않으면 SIGKILL 시그널을 보내 프로세스를 강제 종료시킨다.
    • 이과정이 0번 프로세스가 재시작되는 과정이며 이과정을 현재 실행중인 프로세스 만큼 진행함
  5. 이러한 유기적인 과정속에서 정확한 타이밍에 pm2에게 ready를 보내고 pm2는 정확한 타이밍에 Sigint 시그널을 보내야한다 때문에 ecosystem.config.json이 중요하며 노드앱은 이러한 과정을 진행하기위해 express를 next위에 감싸서 사용하게된다.

자이제 코드들을 살펴보자

우선 ecosystem.config.json 을 살펴보자

module.exports = {
  apps: [
    {
      name: "deploytest",
      script: "server.js",
      // args: "startServer",
      // cwd: "C:UsersjmyooDesktop코드연습\nextjsDeployTest",
      autorestart: true,
      // watch: true, // 프로젝트가 리스타트되거나 파일이 체인지 될경우를 와칭시켜줌
      instances: 4, // 인스턴스를 일단 4개정도 띄운다.
      exec_mode: "cluster", // 실행모드 cluster로 명시하지 않으면 기본 fork모드로 실행됨
      wait_ready: true, // Node앱으로 부터 앱이 실행되었다는 신호를 받기위해 기다리겠다는 것 "ready"
      listen_timeout: 50000, // 앱 실행신호까지 기다릴 최대시간 ms단위 50초
      kill_timeout: 5000, //새로운 프로세스 실행이 완료된 후 예전 프로세스를 교체하기까지 기다릴 시간  5초
      max_memory_restart: "2G", // 프로그램의 메모리 크기가 일정 크기 이상이 되면 재시작시킴
      // 개발환경 설정
      env: {
        NODE_ENV: "development",
      },
      // 운영환경 설정 실행시 --env production 옵션으로 지정할 수 있다.
      env_production: {
        NODE_ENV: "production",
        PORT: "8080",
      },
    },
  ],
};

주석으로 정리되어있으니 천천히 읽으면서 이해해보자

이제 express를 살펴보자.

const express = require("express");
const next = require("next");

// 데브 상태냐 아니냐?
const dev = process.env.NODE_ENV !== "production";

// 포트가 배포상태일 경우 3000 아닐경우 정해준대로 ㄱ
const port = parseInt(process.env.PORT, 10) || 3000;

// next 앱 데브냐 아니냐로 next가 실행시킬 방향을 정한다.
const nextApp = next({ dev });

// next의 리퀘스트 핸들러
const handle = nextApp.getRequestHandler();
// next js 가 ssr을 진행하기전에 준비과정
nextApp.prepare().then(() => {
  // 서버
  const server = express();

  // SIGNIT 시그널을 받았는지 여부, 앱이 곧 종료 될 것임을 의미함.
  let isNextAppGoingToBeClose = false;
  server.use((req, res, next) => {
    if (isNextAppGoingToBeClose) {
      // 앱이곧 종료될 경우
      // 커넥션을 끊어버린다
      res.set("Connection", "close");
    }
    // 그리고 넘김
    next();
  });
  // test
  server.get("/a", (req, res) => {
    return nextApp.render(req, res, "/a", req.query);
  });
  server.get("/b", (req, res) => {
    // return res.json({ asd: 123 });
    return nextApp.render(req, res, "/b", req.query);
  });
  server.get("/posts/:id", (req, res) => {
    console.log(req.params);
    return nextApp.render(req, res, "/posts", { id: req.params.id });
  });

  // 위에 요청을 제외한 요청들은 next requestHandler에서 관리한다.
  server.all("*", (req, res) => {
    return handle(req, res);
  });

  const listeningServer = server.listen(port, (err) => {
    if (err) throw err;
    console.log(`서버켜짐 port : ${port} state : ${process.env.NODE_ENV}`);

    // 서버가 켜지면 pm2에게 구동이 완료됨을 전달
    if (process.send) {
      process.send("ready");
      console.log(`sent to pm2 with ready message at ${new Date()}`);
    }
  });
  process.on("SIGINT", () => {
    console.log("앱이 곧 종료됩니다. received signit signal");

    // 앱이 곧 꺼지개 만들고 서버와의 커넥션도 닫게만든다
    isNextAppGoingToBeClose = true;

    // pm2에서 _old_N 프로세스에서 종료 신호가 들어오면 서버를 종료한다.
    listeningServer.close((err) => {
      console.log("server closed");
      process.exit(err ? 1 : 0);
    });
  });
});

이제 package.json에 srcript를 등록하여 무중단 배포를 진행하여보자

{
  "name": "deploytest",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p 8080",
    "lint": "next lint",
    "devServer": "node server.js",
    "start-eco": "npm run build && pm2 reload ecosystem.config.js --only deploytest --env production",
    "start-server": "npm run build && cross-env NODE_ENV=production node server.js"
  },
  "dependencies": {
    "axios": "^0.26.0",
    "express": "^4.17.3",
    "next": "12.1.0",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "17.0.21",
    "@types/react": "17.0.39",
    "cross-env": "^7.0.3",
    "eslint": "8.10.0",
    "eslint-config-next": "12.1.0",
    "ts-node": "^10.6.0",
    "typescript": "4.6.2"
  }
}
npm run start-eco

express를 사용하지 않고 무중단 배포

ecosystem

module.exports = {
  apps: [
    {
      name: 'blink-frontend-dev',
      cwd: './',
      script: 'node_modules/next/dist/bin/next',
      args: 'start',
      exec_mode: 'cluster',
      instances: 0,
      autorestart: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
  ],
};
pm2 reload {애코시스템 파일명}
profile
RN/react.js개발자이며 배운것들을 제가 보기위해서 정리하기 때문에 비속어 오타가 있을수있습니다.

0개의 댓글