pm2로 무중단배포를 구현하는 것의 기본원리 : pm2가 제공하고 있는 클러스터 모드
클러스터 모드로 실행 시, pm2는 인스턴스가 가지고 있는 코어 수 만큼 프로세스를 새로 생성 가능
이 프로세스를 이용해서 롤링방식의 무중단 배포를 구현
pm2로 여러 프로세스를 생성하고, instance에 새로운 내용이 배포가 되면, pm2 reload 명령어를 실행하여, 0번 프로세스 -> 1번 프로세스 순으로 순차적으로 배포를 실행시킬 수 있다.
즉, 0번 프로세스가 배포를 하면 1번 프로세스를 이용하고 이런 방식
nestjs ecosystem 구성
{
"apps": [
{
"name": "example", // 생성할 프로세스의 이름
"script": "dist/main.js", // 프로세스 생성 시 pm2 start를 할 경로(보통 서버 파일)
"instances": 0, // 생성할 인스턴스의 개수, 0, -1 => 최대 코어 수
"exec_mode": "cluster" // 클러스터 모드
}
]
}
script에 dist/main.js가 되어있는 이유 : 사양이 낮은 Instance의 경우, NestJS의 typescript 파일들을 컴파일/빌드하는데 굉장히 많은 시간들이 할애되기 때문 -> ec2 강제종료 또는 먹통 가능성 있음
즉, pm2를 실행하기 전에 빌드 작업은 사전에 마무리해야하는 것이고, pm2로는 컴파일이 완료된 js 파일을 실행하는 것이다.
pm2 start ecosystem.json => pm2 start dist/main.js
$ sudo npm i -g @nestjs/cli
$ sudo npm install pm2@latest -g
$ git clone http://coderepo.com/projectname.git
$ cd projectname
$ npm install
$ npm run build
$ pm2 start dist/main.js --name <application_name>
There are some PM2 commands to make your application auto restart after system reboot
$ pm2 startup systemd
$ pm2 save
pm2로 실행하는 것은 컴파일이 완료된 js 파일이어야 함을 위에서 설명했다.
이에, appspec의 afterInstall hook과 연계되는 scripts를 이용하면 CD로 자동적으로 코드가 배포된 이후에 빌드 -> pm2 reload가 연속해서 자동으로 실행될 수 있도록 할 수 있다.
1. appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/build/
hooks:
AfterInstall:
- location: scripts/after.install.sh
timeout: 300
2. after.install.sh
#!/bin/bash
sudo chmod -R 777 /home/ubuntu/build // sudo 권한 부여
#navigate into our working directory
cd /home/ubuntu/build 프로젝트 루트 경로 접근
#install node modules & update swagger & pm2 reload
sudo npm ci 패키지 설치
sudo npm run build ts 컴파일 실행
sudo pm2 reload 프로세스 이름 pm2 reload
ec2 ssh 접속
install pm2
$ sudo npm install pm2@latest -g
Project Clone
$ git clone <project-Url>
$ cd projectName
$ npm install
Project build
$ npm run build
Project run
$ pm2 start dist/main.js --name <applicationName> // ecosystem 실행
변경사항 적용
git pull origin main
npm run build
pm2 reload <applicationName>
pm2 list : 실행중인 프로세스 목록 확인
pm2 status : 실행중인 프로세스 상태 확인
pm2 kill : 모든 프로세스 없애기
pm2 start [applicationName]: 프로세스 실행
pm2 stop [applicationName] : 실행중인 프로세스 중지
pm2 delete [applicationName] : 프로젝트 삭제
pm2 restart [applicationName] : 실행중인 프로세스 재시작 ( 프로세스 kill 후 재실행 )
pm2 reload [applicationName]: 실행중인 프로세스 리로드 ( 프로세스 kill 하지 않고 적용 )
pm2 log : 작업중인 로그 실시간 확인
pm2 monit: 메모리 / CPU 사용량 확인
pm2의 cluster mode
nodejs의 cluster mode
child_process.fork()로 생성, 부모 자식 간의 통신을 위한 IPC 채널을 가지며, 생성된 각 프로세스틑 자체 V8 가짐운영체제의 문제
fork()된 프로세스가 주로 담당artillery 라이브러리를 이용해서 많은 트래픽을 주면 적어도 2개이상의 프로세스가 일하는 것을 볼 수 있음70%이상의 일을 처리불평등 분배로 인한 메모리 누수
이렇게 분배하는 것을 로드 밸런싱
// main.ts
...
import { SetResponseHeader } from "./middleware/zero-downtime-deploy/set-response-header.middleware";
import { GlobalService } from "./middleware/zero-downtime-deploy/is-disable-keep-alive.global";
async function bootstrap() {
...
GlobalService.isDisableKeepAlive = false;
app.use(SetResponseHeader);
// Starts listening to shutdown hooks
app.enableShutdownHooks();
await app.listen(PORT as string, () => {
process.send("ready");
console.log(`application is listening on port ${PORT}`);
});
}
bootstrap();
// set-response-header.middleware.ts
import { NextFunction, Request, Response } from "express";
import { GlobalService } from "./is-disable-keep-alive.global";
export function SetResponseHeader(req: Request, res: Response, next: NextFunction) {
if (GlobalService.isDisableKeepAlive) {
res.set("Connection", "close");
}
next();
}
// graceful-shutdown.ts
import { Injectable, OnApplicationShutdown } from "@nestjs/common";
import { GlobalService } from "./is-disable-keep-alive.global";
@Injectable()
export class GracefulShutdown implements OnApplicationShutdown {
onApplicationShutdown(signal: string) {
if (signal === "SIGINT") {
GlobalService.isDisableKeepAlive = true;
console.log("server closed");
}
}
}
// ecosystem.config.js
module.exports = {
apps: [{
name: "server", // 실행되는 프로세스 이름
script: "dist/main.js", // 스크립트는 main.js 사용
cwd: "/root/Web04-Fitory/server",
instances: 0, // max와 동일
exec_mode: "cluster", // 실행모드
wait_ready: true,
listen_timeout: 50000,
kill_timeout: 5000
}]
}
.env 설정 또한 pm2로 관리 가능 하지만 어려움
// .env 설정 부분
module.exports = {
apps: [{
name: "server",
script: "dist/main.js",
cwd: "/root/Web04-Fitory/server",
env: {
PORT: "8080",
}
}]
};
# www.fitory.ga 또는 fitory.ga로 들어올 경우 강제로 https로 redirect 해준다.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name fitory.ga www.fitory.ga;
if ($host = www.fitory.ga) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = fitory.ga) {
return 301 https://$host$request_uri;
} # managed by Certbot
return 404;
}
# https 설정 부분이다. letsencrypt와 certbot을 이용했다.
server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/fitory.ga/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/fitory.ga/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
root /var/www/html;
charset utf-8;
underscores_in_headers on;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://localhost:3000/;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
location /api {
proxy_pass http://localhost:8080/api;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
gzip on;
gzip_comp_level 2;
gzip_proxied any;
gzip_min_length 1000;
gzip_disable "MSIE [1-6]\."
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
}
/api로 올 경우 pm2로 클러스트링된 서버들로 이동롤링 배포
docker를 이용해서 해결가능해당 서버를 업데이트서버를 전부 업데이트클라우드 환경뿐만 아니라 물리적인 서버로 서비스를 운영하는 상황에서도 가능따라서 다른 각각의 서버가 부담하는 트래픽 양이 증가호환성 문제