본문 바로가기

도커

NGINX + HTTPS + NodeJS + PM2 도커로 만들기

github repository

최근에 정부과제(예비창업패키지)로 윈도우용 특허 검색 프로그램인 CCPatents를 만들었습니다.

CCPatents의 인터페이스는 기획할 때부터 단순함을 추구했기 때문에 매우 간단하지만, 내부적으로는 가용성과 보안에 많이 신경썼습니다.

그 중에 사용자가 키워드별로 동의어를 입력하면 자동으로 수집해서 중앙 서버로 전송하는 기능이 있는데요.

최초 구성도는 다음과 같습니다.

docker 사용전

여담

처음에 AWS EC2와 RDS를 사용한 것은 자동으로 Scale-out이 가능하고, Scale-up도 손쉽게 할 수 있었기 때문입니다.

그리고 NGINX와 아파치 중에서 NGINX를 선택한 것은 성능때문이었고, NodeJS를 선택한 것은 최근에 자바스크립트를 많이 다뤄왔기 때문입니다. NodeJS의 멀티 코어 문제는 PM2를 사용하여 극복할 수 있었습니다.

만약에 사용자가 몰리면 바로 확장할 수 있도록 말이죠! 그런 일은 일어나지 않았습니다만...

외부에서 패킷을 읽지 못하도록 하기 위해서 HTTPS를 사용했습니다.

  • NGINX가 외부와 HTTPS 통신을 담당합니다. 그리고 HTTPS 전용 포트인 443번 포트로 들어오는 요청을 내부적으로 localhost 3000번 포트로 라우팅시킵니다.
  • Collector라는 NodeJS 어플리케이션이 localhost 3000번 포트를 수신하고 있습니다.
    그리고 Collector가 동일한 네트워크 안에 있는 Amazon RDS 인스턴스(MySQL)에 키워드별 동의어를 누적 카운팅합니다.
  • Collector 앱은 PM2에 의해서 관리되고 있습니다.
    PM2를 사용하면 기본적으로 단일 코어만 사용하는 NodeJS 어플리케이션도 멀티 코어를 사용할 수 있고, NodeJS 어플리케이션이 알 수 없는 이유로 다운되더라도 자동으로 재시작시켜주기 때문입니다.

그런데 HTTPS 통신을 하기 위해서는 SSL 인증서를 발급받아야 합니다.

유료 인증서가 있지만 연간 5-10만원정도? 하는 것 같더라구요.

무료 인증서를 발급해 주는 Let's Encrypt가 있지만 3개월마다 주기적으로 갱신해 주어야 합니다.

그런데 일정 주기마다 Let's Encrypt에서 인증서를 자동으로 갱신해 주는 certbot이라는 프로그램도 있습니다.

따라서 let's Encrypt와 certbot을 사용하면 평생 무료로 SSL 인증서를 발급받을 수 있습니다.

그런데 초기 설정이 너무 귀찮다.

EC2에 콘솔로 접속해서 NGINX 설치하고 설정하고, certbot 설치하고 설정하고, NodeJS 환경 만들고, NGINX와 NodeJS 연결하고.. 이러한 과정이 필요한데요.

특히 NGINX에 SSL 인증서를 적용할 때, certbot을 사용해서 인증서 자동 갱신을 설정할 때, NGINX와 NodeJS 어플리케이션을 연결할 때 너무 많은 오류가 발생해서 힘들었습니다...

그리고 위의 구성을 AWS 프리티어를 이용해서 구현하였습니다.

이전부터 사용해오던 계정을 사용했었는데 몇 달전에 프리티어 기한(1년)이 만료되면서 AWS 계정을 새로 만들고, 다시 처음부터 설정을 해야 하는 일이 발생했습니다.

이전에 만들면서 만드는 과정을 개인적인 노트에 기록해 두긴 했지만, 낙서하듯이 기록해 두어서 몇 달 후에 다시 만들려고 하니 쉽지 않았습니다.

도커라는 것을 알게 되었습니다. 그런데 더 힘들어졌다...

처음엔 리눅스의 현재 상태를 이미지로 만들어서 VM처럼 구동시키는 것인줄 알았는데 아니더라구요.

Dockerfile 파일에 설치될 프로그램과 실행될 명령어를 미리 지정해 놓으면, Dockerfile 파일에 설정한 내용대로 Docker 이미지(≒설치 파일)가 생성되고, Docker 이미지를 컨테이너(≒VM) 단위로 구동시키는 것이었습니다.

즉, 일반적인 리눅스 환경에서는 콘솔 명령어로 프로그램을 설치하거나 어떤 설정을하기가 쉬운데, Docker를 사용하면 Dockerfile 파일에 이러한 작업을 미리 지정해 놔야 합니다.

중간에 디버깅이 불가능합니다. Dockerfile이 제대로 만들어 졌는지 알기 위해서는 이미지로 만들고 컨테이너로 띄워봐야 합니다. 만약 컨테이너가 제대로 실행되지 않거나 내부적으로 문제가 발생하면 컨테이너와 이미지를 지우고 Dockerfile을 다시 만들어야 합니다. 

실행중인 컨테이너에 접속해서 콘솔 명령어로 프로그램을 설치하거나 설정을 하고, 현재 상태를 이미지로 만들어서 도커 허브에 push할 수도 있지만, 이렇게 구성하면 관리하기가 상당히 까다로워 질 수 있을 것 같습니다.

Dockerfile에서 이미지를 만들면, 나중에 대규모 수정을 할 때, Dockerfile을 보면 서버가 어떻게 구성되었는지 정확하게 알 수 있지만, 현재 상태를 push하는 방법은 컨테이너를 띄우고 접속해서 일일히 확인한 다음에 변경해야 할 것입니다.

그런데 Docker 파일을 한 번 만들고 나면 매우 편리합니다.

docker run <이미지 이름> 명령어 한 줄로 기존의 리눅스 환경을 그대로 재현할 수 있습니다.

그리고 docker-compose를 사용하여 다수의 컨테이너를 동일한 네트워크로 묶어서 한 번에 띄울 수도 있습니다.
즉, 여러 가상환경의 연결 관계까지도 명령어 한 줄로 재현할 수 있습니다.

이것이 도커의 가장 큰 장점이라고 생각합니다.

도커는 어떻게 사용하나?

이 포스트에서는 제가 사용한 docker-compose 기본 구조와 링크만 제공하고, 도커 사용법에 대한 설명은 따로 하지 않겠습니다. 

나동빈님의 도커(Docker) 활용 및 배포 자동화 실전 초급 유튜브 강의에서 아주 잘 설명되어 있고, 인터넷 검색을 조금 하다보면, 인터넷이 떠도는 도커 셋팅을 조합해서 원하는 docer-compose를 설정할 수 있을 것입니다.

그래서 어떻게 바뀌었나?

NGINX와 certbot을 담당하는 컨테이너와 NodeJS와 PM2를 담당하는 컨테이너를 docker-compose로 구성했습니다.

더 복잡해 보이지만... 이렇게 구성해 놓고, 다음 작업만 하면 어디에서든지 기존과 동일한 기능을 구현하는 웹 서버를 쉽게 생성할 수 있습니다.

  • nginx.conf 파일에 인증서에 사용할 도메인을 입력
  • docker-compose.yml 파일에 인증서 발급시 사용할 이메일을 입력
  • docker-compose up -d 명령어 실행

docker 사용후

github에서 다운로드받을 수 있습니다.

하나씩 알아볼까요?

docker-compose.yml

사실 제가 한 것은 거의 없습니다. 제가 한건 staticfloat/docker-nginx-certbot 이미지와 keymetrics/docker-pm2 도커파일을 docker-compose로 묶은 것 뿐입니다.

staticfloat/docker-nginx-certbot는 호스트에서 설정한 nginx 설정파일을 공유하고, certbot을 사용해 SSL 인증서를 일정 주기마다 자동으로 갱신해 주는 이미지입니다.

keymetrics/docker-pm2는 컨테이너가 (재)시작될 때 PM2를 사용해서 미리 지정된 NodeJS 어플리케이션을 자동으로 실행시켜 주는 이미지입니다. 결론적으로 NodeJS 어플리케이션이 다운 상태에서 멈춰있을 일은 없습니다.

services에 포함된 proxy와 pm2 컨테이너는 기본적으로 동일한 네트워크로 묶입니다.

proxy 컨테이너에서 pm2 컨테이너의 심볼인 pm2를 사용하여 pm2 컨테이너에 접근할 수 있습니다.

예를 들어, 해당 파일에 의해 생성될 proxy 컨테이너에서 http://pm2:3000를 사용하여 pm2 컨테이너에서 실행중인 3000번 포트에 별다른 설정없이도 접속할 수 있습니다. 

 

 

 

 

 

 

나머지도 별거 없습니다.

  • staticfloat/nginx-certbot는 도커 허브에서 제공되는 이미지를 사용했습니다. 해당 이미지를 만들 때 사용된 Docker 파일은 위에 있는 개발자 github에서 다운로드받을 수 있습니다. 
  • 호스트의 80번 포트(HTTP)와 443번 포트(HTTPS)를 proxy 컨테이너와 연결
  • 환경변수로 CERTBOT_EMAIL 설정
    staticfloat/nginx-certbot 이미지가 컨테이너로 설치될 때 CERTBOT_EMAIL 환경변수를 사용합니다.
  • 호스트의 conf.d 폴더(nginx.conf 파일이 존재)와 proxy 컨테이너의 /etc/nginx/user.conf.d 폴더(proxy 컨테이너의 NGINX 기본 설정파일 경로)를 연결
  • pm2는 개발자가 도커 허브에 배포하지 않고 Dockerfile만 제공했기 때문에 Dockerfile을 사용해서 이미지를 직접 빌드합니다.
  • pm2 Dockerfile 파일을 보면 알 수 있지만, 호스트의 src 폴더에 있는 NodeJS 어플리케이션을 pm2 컨테이너에 복사해서 실행시키는 명령어가 포함되어 있습니다.

conf.d/nginx.conf

NGINX 설정 파일입니다.

위의 docker-compose.yml 설정에 의해 호스트와 proxy 컨테이너가 공유하는 파일입니다.

443 포트를 수신해서 http://pm2:3000 주소로 넘겨준다는 내용입니다.

그리고 ssl_certificatessl_certificate_key는 443 포트(HTTPS)를 사용해서 통신할 때 사용할 인증서 경로입니다.

3개의 <YOUR DOMAIN>을 사용할 도메인으로 바꿔주면 됩니다.

해당 파일은 도메인만 수정하는 것을 추천드립니다. 잘못 건드리면 NGINX에서 연결을 거부할 수도 있거든요. 이 경우 로그에도 안나오더라구요.

 

 

 

 

 

 

NodeJS 어플리케이션

pm2/package.json

docker-compose up -d 명령어를 실행하면, pm2 이미지가 컨테이너에 설치되면서 자동으로 npm install을 실행하는데 그때 NodeJS 어플리케이션들이 사용할 모듈을 입력해야 합니다.

json에는 주석이 없기 때문에 실제 사용할 때는 주석을 제거해야 합니다.

{
  "name": "<프로젝트 이름>",
  "version": "1.0.0",
  "description": "<알아서 입력>",
  "main": "index.js", // 없어도 될듯
  "private": "true",
  "scripts": {
    "test": "xo&& bash ./test/test.sh"
  },
  "author": "<알아서 입력>",
  "license": "MIT",
  "dependencies": {
    // 여기에 입력
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "express": "^4.16.4",
    "greenlock-express": "^2.7.8",
    "mysql": "^2.17.1"
  },
  "devDependencies": {
    "xo": "^0.18.2"
  },
  "xo": {
    "space": true
  }
}

pm2/pm2.json

PM2에서 사용될 NodeJS 어플리케이션을 지정합니다.

apps 속성을 보면 알 수 있지만, 다수의 어플리케이션을 지정할 수 있습니다.

json에는 주석이 없기 때문에 실제 사용할 때는 주석을 제거해야 합니다.

{
  "apps": [{
    "name": "<앱 이름>",
    "script": "src/app.js", // 실행할 어플리케이션 파일
    "instances": 0,
    "exec_mode": "cluster",
    "env": {
      "NODE_ENV": "development",
      // 아래에 사용할 환경변수 입력
      // 아래는 예시
      "NODE_API_KEY": "...",
      "MYSQL_HOST": "...",
      "MYSQL_USER": "...",
      "MYSQL_PWD": "...",
      "MYSQL_DB": "...",
      "APP_KEY": "..."
    },
    "env_production" : {
      "NODE_ENV": "production"
    }
  }]
}

pm2/src/app.js

실제로 실행될 어플리케이션 파일입니다.

제가 사용중인 것은 아니고, 개발자가 기본으로 제공하는 예제파일입니다.

웹 브라우저에 hey를 반환할 것입니다.

'use strict';

const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200);
  res.end('hey');
}).listen(process.env.PORT || 3000, () => {
  console.log('App listening on port 3000');
});

어떻게 실행하나요?

  1. 클라우드 컴퓨터에 git clone
  2. 호스트 80번 포트와 443번 포트 열기
  3. docker, docker-compose 설치
  4. docker-compose.yml 파일에 이메일, 도메인 입력
  5. pm2 폴더에 있는 json 파일의 주석 제거
  6. pm2/src/app.js 작성
  7. 프로젝트의 루트 경로에서 sudo docker-compose up -d 명령어 실행

docker-compose는 docker와 별도로 설치해야합니다.

 

저는 스크립트를 사용해서 위의 과정을 자동화했습니다.

아래 스크립트만 실행시키면 위의 모든 과정이 자동으로 진행됩니다.

아니면 젠킨스로 완전 자동화해도 됩니다.

#!/bin/bash

cd news_keeper_server
git remote set-url origin https://<token>@github.com/<account>/<repository name>.git
git pull

sudo docker rm -f `docker ps -a -q`
sudo docker rmi -f `docker images`

sudo docker-compose up -d

github repository