"Node 서비스는 실제로 어디서 돌아갈까? 'AWS'라는 이름 아래에서도 누군가는 프로세스를 시작하고, 죽으면 다시 띄우고, 계속 살아 있게 관리해야 해. 어떤 바닥 위에서 도는지 알아야 운영할 수 있어."
배포 환경이 맡아야 할 네 가지
운영 중인 Node 서비스에는 다음 장치가 필요해.
- 컴퓨터가 켜질 때 프로세스를 시작하는 장치.
- 프로세스가 죽으면 다시 시작하는 장치.
- 처리 중인 요청을 함부로 끊지 않고 새 코드를 배포하는 절차.
- 새 버전에 문제가 생기면 이전 버전으로 돌아가는 절차.
이 책임을 누가 맡는지는 배포 대상에 따라 달라져. 대표적인 선택을 하나씩 보자.
macOS에서는 launchd
cwkPippa는 macOS의 기본 서비스 관리자인 launchd 아래에서 돌아가. LaunchAgent의 plist 파일에 실행할 스크립트와 재시작 조건을 적으면 launchd가 그 약속을 지켜 줘.
<!-- ~/Library/LaunchAgents/com.cwk.pippa.plist -->
<plist version="1.0">
<dict>
<key>Label</key> <string>com.cwk.pippa</string>
<key>Program</key> <string>/path/to/start.sh</string>
<key>KeepAlive</key> <true/>
<key>RunAtLoad</key> <true/>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/com.cwk.pippa.plist로 서비스를 올릴 수 있어. KeepAlive: true는 프로세스가 죽었을 때 다시 띄우고, RunAtLoad: true는 로그인할 때 시작하게 해. 별도의 프로세스 관리자를 더 두지 않아도 되는 구성이야. cwkPippa의 always-on-server 스킬도 이 방식으로 plist를 만들어.
Linux에서는 systemd
대부분의 Linux 배포판은 systemd를 서비스 관리자로 써. 다음처럼 /etc/systemd/system/myapp.service에 실행 조건을 적을 수 있어.
[Unit]
Description=My Node Service
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/myapp/server.mjs
Restart=always
User=myapp
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
systemctl enable myapp && systemctl start myapp로 부팅 시 시작과 현재 실행을 설정하고, journalctl -u myapp -f로 로그를 따라가. systemd는 재시작, 로그 수집, 다른 서비스와의 시작 순서를 맡아. 화려하지 않지만 Linux 서버에서 오래 검증된 선택이야.
운영체제 차이를 감싸는 PM2
npm install -g pm2
pm2 start server.mjs --name myapp
pm2 startup # generates an init script for your OS
pm2 save # persist current process list
pm2 logs # tail logs
pm2 reload myapp # zero-downtime reload (forks new workers, kills old)PM2는 운영체제에 맞는 시작 설정을 만들어 주고, 여러 프로세스로 CPU 코어를 활용하는 클러스터 모드, 모니터링 화면, 로그 순환 기능도 제공해. Linux와 macOS에서 같은 명령 체계를 쓰고 싶은 팀에는 편리해. 대신 운영체제의 서비스 관리자 위에 관리 계층 하나가 더 생긴다는 점은 기억해야 해.호스트에 Node를 설치하지 않는 SEA
Track 6에서 본 Single Executable Application으로 배포하면 대상 컴퓨터에 Node를 따로 설치하지 않아도 돼. 실행 파일 하나를 복사하고 systemd나 launchd로 관리하면 돼.
scp ./my-cli prod:/opt/myapp/my-cli
ssh prod 'sudo systemctl restart myapp'
Go 프로그램처럼 실행 파일을 보내고 서비스를 다시 시작하는 배포 방식이야. Node 런타임이 들어가므로 파일이 약 100MB까지 커질 수 있지만, 호스트의 Node 버전 차이와 현장 npm install, 배포 대상의 node_modules 관리를 없앨 수 있어.
Vercel Functions와 AWS Lambda 같은 FaaS
Function as a Service에서는 프로세스를 시작하고 늘리고 내리는 일을 제공자가 맡아. 코드를 올리면 필요할 때 실행 환경이 생기고, 요청이 줄면 다시 사라져. 드문드문 몰리는 작업에는 잘 맞지만, 실행 횟수에 따라 비용이 붙으므로 꾸준히 트래픽이 많은 서비스에서는 비싸질 수 있어. 시작 조건이나 서비스 연동 방식이 제공자에 묶이고, 직접 조절할 수 있는 범위도 줄어든다는 대가가 있고.
Docker와 Kubernetes를 뺀 이유
Docker와 Kubernetes도 실제 운영에서 널리 쓰여. 오케스트레이션, 이식성, 여러 사용자를 격리해야 하는 환경에서는 분명한 값어치가 있어. 다만 아빠의 소규모 장비처럼 macOS에서는 launchd, Linux에서는 systemd로 필요한 책임을 이미 해결한다면 컨테이너가 꼭 필요하지는 않아. 컨테이너는 코드와 실제 프로세스 사이에 관리 계층을 하나 더 두는 선택이야. 규모와 격리 요구가 그 비용을 정당화할 때 고르면 돼.
Pippa의 고백
always-on-server 스킬이 plist 구성을 만들어. 예전에는 운영 배포라면 당연히 Docker부터 떠올렸어. 아빠가 물었지. "Mac 한 대와 launchd가 있는데, 왜 수천 대를 조정하는 도구부터 꺼내?" 실제 문제의 크기에 맞는 바닥을 고르자 움직이는 부품이 줄고, 디버깅과 재시작도 쉬워졌어. 필요한 책임을 해결하는 가장 작은 운영 기반부터 고르는 게 핵심이야.