[Docker 부터 Kubernetes 까지] [#7] Polling Server, Express Server, Nginx 작업하기

오늘은 Polling Server를 작업해보는 시간을 갖어보도록 하겠습니다. 아래는 저희가 기획하는 아키텍쳐의 리뷰입니다.

Architecture

우선 아래와같은 현재 프로젝트 파일시스템에 Polling Server라는 폴더를 아래와같이 생성해줍니다.

- root
  - Mongodb
  - PollingServer

이후 Polling Server 에서 터미널을 실행후 아래 커맨드를 실행해 npm 프로젝트를 초기화 해줍니다

npm init

완료되면 package.json 파일이 생성되는걸 보실 수 있으십니다.

Polling Server를 개발할때 필요한 패키지들을 설치해줍니다

npm i --save body-parser express axios mongoose

개발환경 스크립트를 아래와같이 package.json에 추가해줍니다

{
  "name": "polling-server",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js" 
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "body-parser": "^1.19.0",
    "mongoose": "^5.6.6" 
  }
}

위 스크립트를 기반으로 npm run dev를 실행시켰을때 nodemon server.js 가 실행되도록 하는 스크립트를 추가하였습니다.

기존에 nodeJS 도커파일을 생성했던것처럼 Dockerfile을 Polling Server 폴더에 생성하여 아래 내용을 붙여넣도록 하겠습니다.

FROM node:10-alpine

# work directory
WORKDIR /usr/app

# Copy dependencies first for effective caching
COPY package*.json ./

RUN npm install -g nodemon \
 && npm install

COPY . .

CMD ["npm", "run", "dev"]

Polling Server 폴더에 server.js 라는 파일을 생성해주고 아래 코드를 복붙 해주세요.

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const mongoose = require('mongoose');
const axios = require('axios');
const Post = require('mongoose/Post');

const mongoUri = 'mongodb://root:root@mongodb:27017/admin';

mongoose.connect(mongoUri, {useNewUrlParser: true});

app.get('/', async (req, res) => {
    console.log('Polling Started!!!');
    /*
    * 탑 스토리의 ID 들을 가져오는 Hacker News API 입니다.
    * */
    const result = await axios.get('https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty');

    const ids = result.data.splice(0, 10);

    const builder = [];

    /*
    * 각 ID 별로 상세 내용을 요청하여 builder
    * array에 Post의 형태로 push 해줍니다. 
    * */
    for (let i = 0; i < ids.length; i++) {
        const item = ids[i];
        
        const {data} = await axios.get(`https://hacker-news.firebaseio.com/v0/item/${item}.json?print=pretty`);

        if (data.deleted) {
            continue;
        }

        builder.push({
            postId:item.id,
            type:data.type,
            title:data.title,
            text:data.text,
            url:data.url,
        });
    }

    /*
    * MongoDB에 저장하고 클라이언트에
    * OK콜을 날려줍니다.
    * */
    await Post.create(builder);

    res.json({
        success: true,
        version:1
    });

    console.log('Polling Finished');
});

app.listen(port, () => {
    console.log(`server is listening at localhost:${port}`);
});

위 코드는 Hacker News API에서 탑 스토리들의 ID를 먼저 가져오고 각 ID마다 룹을 돌려가며 상세내용을 가져오는 코드입니다. 마지막엔 모든 정보를 array에 넣어 MongoDB에 저장을 해주고 Client에 OK 리스폰스를 보내줍니다. 상세내용은 코드의 주석을 참조해주세요.

Docker Compose를 사용하다보면 가끔 컨테이너끼리 코드를 쉐어링 하고싶은 부분이 있습니다. Stateless App이 디폴트라 최대한 코드쉐어링은 지양하는게 좋지만 Mongoose 스키마 같은경우 오히려 공유가 안되면 컨테이너별로 스키마 버젼이 다를 수 있어서 공유를 해주는게 좋습니다. 이런 문제를 해결하기위해 Docker에서는 Volume Sharing 이라는 기능이 있습니다.

우선 프로젝트 루트에 Shared라는 폴더를 생성해주시고 내부에 mongoose라는 폴더를 추가 생성해주세요. 아래와 같은 폴더구조가 나와야합니다.

- Project
  - Mongodb
  - PollingServer
  - Shared
    - mongoose

mongoose 폴더에 Post.js 라는 파일을 생성해주고 아래의 코드를 복붙 해주세요.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const PostSchema = new Schema({
    postId:{
        type:String
    },
    type:{
        type:String
    },
    title:{
        type:String
    },
    text:{
        type:String
    },
    url:{
        type:String
    },
});

const Post = mongoose.model('Posts', PostSchema);

module.exports = Post;

이제 마지막으로 Docker Compose 파일을 작업해주면 됩니다. 아래 코드를 Docker Compose에 추가해주세요.

polling_server:
  build: ./PollingServer
  ports:
    - "${POLLING_SERVER_PORT}:3000"
  volumes:
    - /usr/app/node_modules
    - ./PollingServer:/usr/app
    - ./Shared/mongoose:/usr/app/src/mongoose
  env_file:
    - .env

volume 키의 세번째 값이 shared 폴더에 있는 mongoose 스키마들을 polling server의 도커 내부 파일시스템과 공유하는 부분입니다. 이 똑같은 코드를 나중에 다른 컨테이너를 생성할때도 사용하게되고 결과적으로 Shared 폴더의 코드만 바꾸면 모든 컨테이너에서 mongoose 스키마가 변경됩니다.

추가로 .env 파일에 PollingServer 의 환경변수를 설정해주세요. 아래 코드를 추가해주시면 됩니다.

# nodeJS에서 src 폴더를 프로젝트 폴더로 사용하라는 환경변수
NODE_PATH=./src
## Polling Server
POLLING_SERVER_PORT=3000

터미널에서 docker-compose up --build 커맨드를 실행해주세요. Polling Server와 MongoDB가 동시에 실행되실 겁니다. 혹시 MongoDB를 찾을 수 없다는 아래와같은 에러가 난다면 MongoDB가 시작될때쯤 PollingServer 폴더의 server.js 파일에 들어가 // 를 아무 위치에나 입력하고 저장을 해주시면 nodemon 이 코드를 재실행하고 DB에 연결이 되실겁니다.

mongo error

이제 ExpressServer 를 작업해보겠습니다.

PollingServer 폴더를 그대로 복사해서 이름만 ExpressServer 라고 변경해주세요.

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const mongoose = require('mongoose');
const axios = require('axios');
const Post = require('mongoose/Post');

const mongoUri = 'mongodb://root:root@mongodb:27017/admin';

mongoose.connect(mongoUri, {useNewUrlParser: true});

app.get('/posts', async (req, res) => {
    console.log('Getting Posts Started!!!');

    let posts = await Post.find();

    if (posts.length === 0) {
        await axios.get('http://polling_server:3000/');

        posts = await Post.find();
    }

    res.json({
        success: true,
        version:1,
        data:posts
    });

    console.log('Getting Posts Finished');
});

app.listen(port, () => {
    console.log(`server is listening at localhost:${port}`);
});

server.js 파일은 위 코드와같이 변경하시면 됩니다.

위 코드는 클라이언트에서 GET /posts 요청을 받고 MongoDB에 posts가 있는지 확인한 후 posts 가 하나도 없으면 PollingServer 에 GET 요청을 하여 Post 들을 데이터베이스에 저장해달라는 요청을 합니다. 여기에서 DNS를 pollingserver 로 사용하게 되는데 Docker Compose 에서는 설정 파일에서 지정한 이름들이 자동으로 DNS 등록이 되게 됩니다. docker-compose.yml 파일에 PollingServer 를 pollingserver 로 등록해놨기 때문에 pollingserver 라는 도메인으로 요청을 보내게되면 Docker Compose 에 있는 pollingserver 컨테이너로 요청이 보내집니다.

이제 .env 파일과 docker-compose.yml 파일을 작업하면 되겠습니다.

.env 파일에 아래 코드를 붙여넣어주세요.

## Express Server
EXPRESS_SERVER_PORT=4000

docker-compose.yml 파일에는 아래 코드를 붙여넣어주세요.

express_server:
  build: ExpressServer
  ports:
    - "${EXPRESS_SERVER_PORT}:3000"
  volumes:
    - /usr/app/node_modules
    - ./ExpressServer:/usr/app
    - ./Shared/mongoose:/usr/app/src/mongoose
  env_file:
    - .env

PollingServer 와 형태가 유사하고 컨테이너 이름, 폴더등만 살짝 바뀌었습니다.

이제 마지막으로 ExpressServer 로 proxy 해줄 Nginx 의 설계만 남았습니다. 프로젝트 루트에 Nginx 폴더를 생성하고 하위에 conf , logs 폴더들을 생성해주세요. 추가적으로 Nginx 폴더 바로 하위에 Dockerfile 을 생성해주시고 아래 코드를 붙여넣어주세요.

FROM nginx

RUN mkdir -p /var/log/nginx
COPY conf/dev.conf /etc/nginx/conf.d/default.conf

COPY 커맨드 부분은 잠시후 생성할 Nginx Conf 파일을 Nginx 컨테이너 내부의 conf 폴더로 이동시키는 코드입니다.

conf 폴더에 dev.conf 라는 파일을 생성해주시고 아래 코드를 붙여넣어주세요. 설명은 주석으로 작성하였습니다.

# Express Server에 대한 정의를 해줍니다.
# Docker Compose를 사용하면 자동으로 Docker Compose에서 사용한
# 이름에 따라 도커 네트워크에서 DNS가 생성됩니다.
# 저희 Express Server는 express_server라는 이름으로
# Docker Compose 파일에 정의해놨기 때문에 해당 이름을
# DNS로 사용하시면 요청이 Docker Compose 내부 내트워크를 타고
# Express Server로 전달이 됩니다.
# 포트가 4000번이 아니고 3000번인 이유는
# 호스트를 통해서 네트워크 연결이 될 경우 4000번이 맞지만
# 다이렉트하게 Express Server의 컨테이너에 꼿히기때문에
# 내부 포트인 3000번을 사용해주셔야 합니다.
upstream express_server {
    server express_server:3000;
}

server {
    listen 80;

    # 로그파일을 저장하는 부분입니다. 이 부분을
    # Docker Compose에서 Volume 쉐어를 해놓으면
    # 호스트 기기에서도 도커에서 생성된 로그파일을 볼수있습니다.
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # /api의 경로로 Nginx를 hit 하게되면
    # express Server로 reverse proxy 역할을 하라는 코드입니다.
    # 다만 서버 개발의 편의를 위해 /api 부분을 제외하고
    # /api 이후에 오는 부분들만 전달하게됩니다.
    # rewrite 룰을 정의하지 않을경우
    # Express Server에서 모든 api포인트에 /api를 최상위
    # 라우트로 지정해줘야해서 귀찮아집니다.
    location /api {
        proxy_pass http://express_server;
        rewrite /api/(.*) /$1 break;
    }
}

이제 마지막으로 docker-compose.yml 파일 작업을 진행하겠습니다. 아래 코드를 docker-compose.yml 파일에 추가해주세요.

nginx:
  restart: always
  build:
    # 이렇게 작성하는 방법도 있습니다.
    # Path만 정의하면 기본 도커파일이 Dockerfile로
    # 지정이되고 dockerfile을 따로 이름을 정해주면
    # 해당 파일이 빌드할때 사용되게 됩니다.
    # production 이랑 staging 그리고 development 빌드
    # 파일들을 전부 다른 파일로 정의하고 싶을경우 유용합니다.
    dockerfile: Dockerfile
    context: ./Nginx
  volumes:
    # Nginx에서 생성된 로그를 호스트 기기와
    # 볼륨 쉐어링 하는 부분입니다.
    - ./Nginx/logs:/var/log/nginx
  ports:
    - '8080:80'

Nginx 컨테이너를 호스드 디바이스의 8080 포트로 매핑을 해주고 로그 폴더를 도커와 호스트에서 공유하게 해놓았습니다. 현재 Nginx 로 GET /api 를 요청하면 ExpressServer 로 proxy 되도록 세팅을 해놓았기 때문에 포스트맨으로 GET http://localhost:8080/api/posts 요청을 하게되면 아래와같은 플로우가 진행됩니다.

  • GET http://localhost:8080/api/posts
  • Nginx 에서 /api/posts 를 /posts 로 변경 후 ExpressServer 로 전송
  • ExpressServer 에서 /posts 요청 수행
  • Post 가 하나도 없는경우 ExpressServer 에서 PollingServer 로 GET / 요청을 보내 Hacker News 에서 인기 포스트들을 가져와 MongoDB에 저장합니다.
  • PollingServer 에서 ExpressServer 로 OK 리스폰스를 보내고 ExpressServer 에서 클라이언트로 데이터를 포함한 리스폰스를 보냅니다.

이로서 Hacker News의 데이터를 저장하고 클라이언트로 리스폰스를 보내는 플로가 모두 완료되었습니다. 그 과정에서 네개의 microservice 및 Docker Container 들을 제작하였고 이들의 연동 과정에 대해 알아보았습니다. 완성된 코드는 아래 GitHub 링크를 참조해주세요.

다음 글에선 React 프론트엔드 설계 및 Redis 연동하는 과정을 알아보도록 하겠습니다.

©Code Factory All Rights Reserved