잊지 않겠습니다.

nodejs + pm2 application을 구성한 dockfile을 구성하는 방법에 대해서 알아보도록 하겠습니다.

먼저 code를 어떤 방법으로 배포할지에 대한 process를 정하는 것이 중요합니다. 이는 Dockerfile을 어떻게 만들지에 대한 방향이 결정나게 됩니다.

배포 Process

제가 구성한 Docker를 이용한 배포 process는 다음과 같습니다.

  1. nvm 설치
  2. nvm을 이용한 node 설치
  3. pm2 설치
  4. ssh 인증 + git clone 을 통한 code download
  5. npm install
  6. .pm2/logs 위치에 따른 host 파일 공유
  7. docker run의 parameter pass를 통한 pm2 start json 파일 전송

1. nvm 설치 & node 설치 & pm2 설치

ubuntu image를 기본으로 하여 구성을 하도록 하겠습니다. nodejs application은 npm install을 통해서 node_modules을 다운로드 받아서 compile되는 환경이기 때문에 build환경역시 갖추는 것이 중요합니다.

FROM ubuntu # 빌드 환경 구성 RUN apt-get update RUN apt-get install -y wget build-essential keychain git python # NVM_DIR, NODE_VERSION에 따른 ENV 설정 ENV NVM_DIR /usr/local/nvm ENV NODE_VERSION 7.3.0 # NVM 다운로드 & 설치 RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash # Shell 변경 (sh -> bash) SHELL ["/bin/bash", "-c"] # nvm 구성 & node install RUN source $NVM_DIR/nvm.sh; \ nvm install $NODE_VERSION; \ nvm use --delete-prefix $NODE_VERSION; ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH # PM2 설치 RUN npm install -g pm2

2. ssh 인증 구성

ssh 인증을 위해서 private key를 image안에 COPY할 필요가 있습니다. 그리고 $(ssh-agent)값을 지정해줄 필요가 있습니다. 그런데, eval을 이용한 값 설정의 경우, Dockerfile에서 할 수 없습니다. 따라서 do-ssh.sh 실행 파일을 구성해서 처리할 image 내부에서 실행할 필요가 있습니다.

다음은 do-ssh.sh파일의 내용입니다.

eval "$(ssh-agent)" && ssh-agent -s ssh-add /root/.ssh/id_rsa
# .ssh Folder작성 & private key Copy RUN mkdir /root/.ssh COPY [ "id_rsa", "/root/.ssh/id_rsa" ] # SSH Key 권한 설정 - 권한이 600이 아닌경우, id_rsa key를 얻어내지 못합니다. RUN chmod 600 /root/.ssh/id_rsa # do-ssh.sh 파일 COPY COPY [ "do-ssh.sh", "/root/do-ssh.sh" ] # 실행권한 설정 RUN chmod 777 /root/do-ssh.sh # SHELL 변경 (bash -> sh) SHELL ["/bin/sh", "-c"] # do-ssh.sh RUN RUN /root/do-ssh.sh # SSH 접근 check. -o StrictHostKeyChecking=no option이 없으면 docker container에서 실행되지 않습니다. RUN ssh -T -v git@10.1.95.184 -o StrictHostKeyChecking=no

3. git code download & npm install

nodejs Application이 위치될 folder를 만들어주고, git clone을 통해서 code를 받고, npm install을 하는 구성입니다.

# apps folder 작성 RUN mkdir /apps # Shell 변경 (sh -> bash) SHELL ["/bin/bash", "-c"] WORKDIR /apps RUN git clone git@10.1.95.184:/home/git/repo/fms-api-v2.git -o StrictHostKeyChecking=no WORKDIR /apps/fms-api-v2 # npm 설치 RUN npm install

npm install을 통해서 필요한 node module들을 모두 받아주고 build과정을 거치게 됩니다.

4. PORT 공유 & PM2 Log volume 구성

제가 만든 application은 5000, 8000번 port를 open시켜서 사용합니다. 사용되는 PORT를 EXPOSE 시켜주는 과정이 필요하게 됩니다. 그리고 pm2의 log 파일을 host에 기록하도록 volume을 추가합니다.

# expose 5000, 8000 port EXPOSE 8000 EXPOSE 5000 # VOLUME 설정 pm2 log folder VOLUME /root/.pm2/logs

5. 뒷처리과정

매우매우 위험한 파일을 container에 넣어둔것을 기억하고 있어야지됩니다. private key가 바로 그것이지요. 꼭 이 파일을 지워주는 과정이 필요합니다. 마지막으로pm2 실행에 대한 ENTRYPOINT와 CMD 설정이 필요합니다. 제가 만든 application은 설정 json파일에 따라서 동작환경이 변경되게 됩니다. 설정 json파일을 외부에서 parameter로 넣어줄 수 있도록 설정해주는 것이 필요합니다.

# Private Key 삭제 RUN rm /root/.ssh/id_rsa # ENTRYPOINT 설정 docker container에서 pm2를 실행시킬때는 pm2-docker를 통해서 실행합니다. ENTRYPOINT [ "pm2-docker" ] # CMD 설정 - parameter가 없는 경우, default 값을 지정하게 됩니다. CMD ["pm2/dev-system.json"]

6. build image & docker run

image를 만들기 위해서, docker build를 다음과 같이 구성합니다.

docker build -t nodejs-pm2-application .

이제 process가 진행되면 nodejs-pm2-application으로 image가 만들어지게 됩니다.

만들어진 image를 이용해서 container를 배포하는 명령어는 다음과 같습니다.

docker run -it --name node-app -p 5000:5000 -p 8000:8000 -v /logs/pm2:/root/.pm2/logs -t nodejs-pm2-application pm2/api-system1.json
  • -p option으로 5000, 8000번 port를 host에 연결합니다.
  • -v options으로 /root/.pm2/logs folder를 host의 /logs/pm2로 mount 시킵니다.
  • 마지막에 command로 /pm2/api-system1.json을 넣어 container의 COMMAND를 pm2-docker pm2/api-system1.json으로 변경시킵니다.

SUMMARY

Dockerfile을 구성하는 것은 Application의 형태에 따라, 개발환경에 따라 많은 차이를 가지고 오게 됩니다. nodejspythonruby와 같이 외부 library를 받아 compile하는 과정을 가지게 된다면 image내부에서 code를 직접 git으로 받아올 필요가 있습니다. 그러나, JAVAC#GO와 같이 universal compile이 되는 경우, Docker image를 만들때, compile시켜 ADD를 통해서 실행파일을 image안에 넣는것이 더 좋을것 같습니다.

다음은 전체 Dockerfile입니다.

FROM ubuntu # 빌드 환경 구성 RUN apt-get update RUN apt-get install -y wget build-essential keychain git python # NVM_DIR, NODE_VERSION에 따른 ENV 설정 ENV NVM_DIR /usr/local/nvm ENV NODE_VERSION 7.3.0 # NVM 다운로드 & 설치 RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash # Shell 변경 (sh -> bash) SHELL ["/bin/bash", "-c"] # nvm 구성 & node install RUN source $NVM_DIR/nvm.sh; \ nvm install $NODE_VERSION; \ nvm use --delete-prefix $NODE_VERSION; ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH # PM2 설치 RUN npm install -g pm2 # .ssh Folder작성 & private key Copy RUN mkdir /root/.ssh COPY [ "id_rsa", "/root/.ssh/id_rsa" ] # SSH Key 권한 설정 - 권한이 600이 아닌경우, id_rsa key를 얻어내지 못합니다. RUN chmod 600 /root/.ssh/id_rsa # do-ssh.sh 파일 COPY COPY [ "do-ssh.sh", "/root/do-ssh.sh" ] # 실행권한 설정 RUN chmod 777 /root/do-ssh.sh # SHELL 변경 (bash -> sh) SHELL ["/bin/sh", "-c"] # do-ssh.sh RUN RUN /root/do-ssh.sh # SSH 접근 check. -o StrictHostKeyChecking=no option이 없으면 docker container에서 실행되지 않습니다. RUN ssh -T -v git@10.1.95.184 -o StrictHostKeyChecking=no # apps folder 작성 RUN mkdir /apps # Shell 변경 (sh -> bash) SHELL ["/bin/bash", "-c"] WORKDIR /apps RUN git clone git@10.1.95.184:/home/git/repo/fms-api-v2.git -o StrictHostKeyChecking=no WORKDIR /apps/fms-api-v2 # npm 설치 RUN npm install # expose 5000, 8000 port EXPOSE 8000 EXPOSE 5000 # VOLUME 설정 pm2 log folder VOLUME /root/.pm2/logs # Private Key 삭제 RUN rm /root/.ssh/id_rsa # ENTRYPOINT 설정 docker container에서 pm2를 실행시킬때는 pm2-docker를 통해서 실행합니다. ENTRYPOINT [ "pm2-docker" ] # CMD 설정 - parameter가 없는 경우, default 값을 지정하게 됩니다. CMD ["pm2/dev-system.json"]

Happy Coding~

Posted by Y2K
,

Express에서의 Session 사용법

기초중의 기초이지만, 간혹 사용법이 헛갈릴때가 많아서 정리하였습니다.

요구사항

  1. ES6 문법을 기초로 이용
  2. Session을 이용한 로그인 관리
  3. Cluster를 이용가능 (pm2 등을 이용)
  4. login을 통해 얻어진 사용자 이름을 session에 저장.
  5. view는 모두 json을 이용

개발 환경 구성

yo generator중에 generator-express 설치

npm install -g generator-express

특정 directory에서 yo express 실행하면 기본적인 express application이 구성됩니다. 구성된 폴더안에서 gulp default를 실행하면 기본 port 3000번으로 application이 구동됩니다.

Application이 구동되지 않을때.

port 35729가 사용중일때 : gulp.liveload를 이용한 다른 application이 구동되고 있을 때, 발생되는 error. 두개의 application을 동시에 실행하고 싶은 경우에는 gulpfile.js에서 liveload method에 port 번호를 명시. livereload.listen(60000);

config.root undefined가 될때. config/config.js가 정상적으로 로드되지 않은 경우. NODE_ENVdevelopment, production 등으로 지정하지 않은 경우에 발생. 자신의 NODE_ENV를 확인후, NODE_ENV에 맞게 config.js를 수정

추가 npm - express-session, filestore

Session을 이용하기 위해서 express-session 설치합니다. 구 버젼을 기반으로한 책이나 blog에서는 이 과정이 나오지 않습니다. express의 middleware들은 대부분 npm module로 따로 분리가 되어 사용됩니다. express에 기본으로 들어가 있는 경우는 얼마 없습니다.

npm install express-session --save
babel-express-app@0.0.1 /home/ykyoon/dev/code/babel-express-app
└─┬ express-session@1.13.0 
  ├── crc@3.4.0 
  └─┬ uid-safe@2.0.0 
    └── base64-url@1.2.1 

npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.7

express.js파일에 express-session을 middleware로 등록합니다.

const session = require('express-session');
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false }
}));

지정되는 parameter는 다음 의미를 갖습니다.

  • secret: session의 암호화에 사용되는 key값입니다.
  • resave: requrest가 요청되었을때, 기존의 session이 존재하는 경우 다시 저장할 필요가 있는지를 확인하는 option입니다. express-session에서 제공되는 touch를 구현하고 있는지를 확인해보면 됩니다. 기본적인 MemoryStore의 경우, touch가 구현되어 있기 때문에 설정할 필요가 없습니다.
  • saveUninitialized: 설정하지 않는 경우, Session이 저장되지 않습니다. 일반적인 로그인을 구성할 때, false로 지정해줘야지 됩니다. 기본값은 true이며 모든 초기화되지 않은 session은 저장되게 됩니다.
  • cookie.secure: https로 호출되는 경우에만 session cookie를 생성하는 option입니다. 실 production에 유용한 option으로 테스트나 개인 공부를 할 때는 false로 지정해주시면 됩니다.
  • cookie.maxAge: cookie가 만료되는 시간을 설정합니다.

login url 구현

DB를 사용하지 않는 최소한의 application이기 때문에 사용자가 입력한 username을 그대로 session의 username으로 넣어주는 code를 구성합니다.

'use strict';

const express = require('express'),
  router = express.Router();

router.get('/login', (req, res) => {
  const session = req.session;
  session.username = req.query.username;
  return res.json(session);
});

module.exports = (app) => {
  app.use('/user', router);
};

원래는 login의 경우, post로 구현해야지 되는 것이 원칙이지만, 테스트를 위한 application이고, browser에서 그대로 구현하기 위해서이니 그냥 get으로 구현하였습니다. 이제 저장된 username을 얻어내야지 됩니다. 이제 browser에서는 저장된 username이 계속 유지가 되어야지 됩니다.

router.get('/info', (req, res) => {
  return res.json(req.session);
});

이제 url을 두개를 연달아 browser에서 실행하면 다음과 같은 결과를 볼 수 있습니다.

{
    "cookie":{"originalMaxAge":null,"expires":null,"secure":false,"httpOnly":true,"path":"/"},
    "username":"abcdef"
}

Session store의 변경

고성능, 고가용성을 구현하기 위해서는 기본적으로 nodejs는 cpu의 core 숫자와 동일한 process숫자로 실행되어야지 됩니다. 이 사실은 지금 application의 경우 심각한 문제를 가지고 있는 것을 의미합니다. process간의 memory 공유는 원칙적으로 되지 않는거니까요. (hacking이나 memory editor의 경우 다르겠지만, web application의 기본 기능과는 거리가 있는 상황이니.)

그래서, nodejs application에서는 공용으로 사용되어야지 되는 정보는 file이나 redis, db와 같은 공용 IO에 저장해서 모든 process가 공유하는 형식으로 사용하게 됩니다. 또한, nodejs application은 application을 종료했다가 다시 올리는 경우가 왕왕 있습니다. express-session의 기본값인 MemoryStore는 기본적으로 production에 올릴수 없는 설정입니다.

성능은 안좋고, multiple instance에서는 절대로 사용할 수 없는 방법이지만, File에 session정보를 저장하기 위해서 FileStore를 이용해보도록 하겠습니다. FileStore와 Session간의 interface를 하는 session-file-store를 설치하면 FileStore가 같이 설치됩니다.

npm install session-file-store --save
babel-express-app@0.0.1 /home/ykyoon/dev/code/babel-express-app
├── filestore@0.1.3 
└─┬ session-file-store@0.0.24 
  ├── bagpipe@0.3.5 
  ├─┬ fs-extra@0.26.5 
  │ ├── jsonfile@2.2.3 
  │ ├── klaw@1.1.3 
  │ └─┬ rimraf@2.5.2 
  │   └── glob@7.0.0 
  └── retry@0.8.0

FileStore를 Session에 설정합니다. 코드는 다음과 같습니다.

const session = require('express-session');
const FileStore = require('session-file-store')(session);
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: false },
  store: new FileStore()
}));

설치후, folder를 보면 session라는 folder가 생성되어 있는 것을 볼 수 있습니다. 이제 위에서 실행했던 URL을 다시 실행해보면 File이 하나 생성되어 있는 것을 볼 수 있습니다. 파일을 열어보면, session의 정보가 그대로 저장되어 있는 것을 볼 수 있습니다.

정리

express에서 session을 구성하는 방법에 대해서 간단히 알아봤습니다. 또한 MemoryStore가 아닌 다른 Store를 이용하는 방법에 대해서도 간단히 알아 보았습니다. 실무에서는 대부분 redis를 이용하는 경우가 일반적이긴 하지만, 개발단에서 편하게 알아보기 위해서, File을 사용하는 경우도 꼭 나쁜 것은 아닙니다.


Posted by Y2K
,

mocha를 이용한 express application test 방법

yo-express를 이용한 application을 test 하는 방법을 한번 알아보고자합니다.

npm module 설치

web에 대한 test는 supertest를 사용하는 것이 좋습니다. 직관적인 사용에도 좋고, mocha에서 사용하기에도 매우 편합니다.

npm install supertest --save-dev

root에 test folder 생성

app.test.js 파일 생성

express application을 먼저 구동하는 것이 필요합니다. 이는 원 app.js에서 구성되는 express start와 sequelize model의 초기화 과정에 대한 process를 실행해야지 되는것을 의미합니다. test라는 상대경로로 옮겨왔기 때문에 경로의 depth가 달라지는 것에 주의할 필요가 있습니다. 그리고 test의 시작전에 항시 실행되어야지 되는 global before이기 때문에 before로 구성되어야지 됩니다.

'use strict';

before(function () {
  var express = require('express'),
    config = require('../config/config'),
    db = require('../app/models');
  var agent = require('supertest');

  var app = express();

  require('../config/express')(app, config);

  db.sequelize
    .sync()
    .then(function () {
      app.listen(config.port, function () {
        console.log('Express server listening on port ' + config.port);
      });
    }).catch(function (e) {
      throw new Error(e);
    });
  console.log('before');
  global.app = app;
  global.agent = agent(app);
});

mocha로 test running

이제 mocha를 이용해서 다음 command로 실행이 가능합니다.

mocha test/app.test.js test/**/*.test.js

mocha.opts 설정

좀더 편한 test 환경을 만들기 위해서 mocha.optsapp.test.js를 넣어두면 편합니다.

--timeout 5000
--full-trace
test/app.test.js

이 때, mocha.opts파일의 위치는 다음과 같이 구성합니다.

└── test
    ├── app.test.js
    ├── controllers
    ├── mocha.opts
    └── models

controller test code 작성

controller test code는 다음과 같이 구성될 수 있습니다. app.test.js에서 supertest의 request를 global.agent로 지정했기 때문에, test에서는 언제나 agent를 접근 가능해서 test code를 작성하기 좀 더 편해집니다.

'use strict';

var assert = require('assert');

describe('func01', function () {
  it('test', function () {
    console.log('abc');
  });

  it('call /', function (done) {
    agent
      .get('/')
      .expect(200)
      .end(function (err, res) {
        // console.log(res);
        done(err);
      });
  });

  it('call / - 2', function (done) {
    agent
      .get('/')
      .expect(500)
      .end(function (err, res) {
        done();
      });
  });
});

Summary

nodejs는 compile 언어가 아니기 때문에, 실행되기 전까지 정상적인 code를 작성했는지를 헛갈릴 수 있습니다. 개인적으로는 원론적으로 test를 거치지 않으면 compile 언어 역시 마찬가지로 문제가 있다고 생각합니다만. test를 잘 만들어주는 것과 test를 잘 할 수 있도록 해주는 것은 자신 뿐 아니라 다른 개발자들에게도 매우 좋은 일들입니다. 이건 정말 잘 할 필요가 있다고 생각합니다.

mocha를 이용한 test는 매우 유용합니다. 만약에 nodejs를 이용한 개발을 하고 있다면, test를 어떻게 할지에 대한 고민을 꼭 해보시길 바랍니다.

Posted by Y2K
,

jenkins를 이용해서 nodejs project의 CI를 구성하는 방법은 다음과 같다.
기본적으로 mocha와 istanbul을 이용해서 xunit.xml과 codecoverage.xml 파일을 생성한다.

Test 결과의 저장 - xunit.xml 생성

junit의 결과값과 동일한 xml 파일을 생성한다. mocha에 다음과 같은 plugin을 설치한다.

npm install spec-xunit-file@0.0.1-3

위 plugin을 설치하면 mocha에서 xunit.xml 파일을 생성해줄 수 있다. unit test와 동시에 xunit.xml 생성을 위해서는 다음과 같은 command를 실행한다.

mocha test/**/*.test.js -R spec-xunit-file

이제 root directory에 xunit.xml 파일이 생성된 것을 볼 수 있다.

CodeCoverage 테스트 실행

istanbul을 먼저 설치한다.

npm install -g istanbul

istanbul에서 mocha를 이용한 test coverage를 해준다.

istanbul _mocha test/**/*.test.js -R node_modules/spec-xunit-file

위 command는 기본적으로 mocha를 실행시키는것이기 때문에, Code Coverage를 수행하는 경우에는 Test결과는 같이 생기게 된다. 주의점은 package dependency로 spec-xunit-file을 넣어주는 것이다. 이는 istanbul에서는 기본적으로 mocha의 기본적인 reporter만을 인식하게 되고 나머지는 직접 경로를 통해서 얻어야지 되는 단점이 있기 때문이다.

이제 생성된 결과를 보면 coverage라는 폴더가 생겨있다. 이 폴더를 지정해서 Cobertura xml을 생성해줘야지 된다. xml 생성은 다음 cmd를 통해서 생성가능하다.

istanbul report cobertura --root coverage --dir coverage

done이 나오면 cobertura-coverage.xml이 있는 것을 확인할 수 있다.

jenkins에 project의 등록

jenkins에 등록하기 위해서는 위 2가지 process를 순차적으로 실행시키면 된다. 그렇지만 문제가 두가지가 있다.

  1. spec-xunit-file를 상대경로를 사용할 수 없다. - jenkins에서 bash를 따로 실행시키는 것은 tomcat의 절대경로에 따라서 변경이 되게 된다. 따라서 tomcat의 경로를 넣어줘야지 되는 귀찮음이 생긴다.
  2. xunit.xml 파일의 절대경로를 얻어내야지 된다. - 역시 위와 같은 이유이다. spec-xunit-file의 경우 process path/xunit.xml파일로 생성되기 때문에 이에 대한 절대 경로를 얻어서 넣어줘야지 된다.

위와 같은 문제를 해결하는 npm package인 jenkins-mocha를 이용하는 것이 더 좋다고 생각된다.
jenkins-mochaistanbulmocha에 대한 wrapper기능만을 제공하지만, unit.xml파일과 coverage를 jenkins prorject folder에 생성해주는 역활을 담당하게 된다.

npm을 이용해서 설치후, package.json 파일에 다음 내용을 추가한다.

"scripts": {
    "test": "jenkins-mocha test/**/*.test.js"
},

등록 절차

freestyle project로 신규 project로 등록을 한다.

feeStyleProject

Build Process 등록

Execute Shell을 선택하고, 다음 Step들을 추가한다.

  1. npm install : npm module을 다운 받는 process
  2. npm test : istanbul, mocha를 이용해서 test를 구동하고 coverage를 계산하는 process
  3. istanbul report cobertura --root artifacts/coverage --dir artifacts/coverage : coverage.json을 cobertura.xml로 변경하는 process
Report file 등록

JUnit test report와 Code Coverage를 위한 Cobertura Coverage Report를 등록시키면 된다.
jenkins-mocha에서 생성되는 기본 report 파일들의 위치는 다음과 같다.

  1. xunit.xml : artifacts/xunit.xml
  2. Cobertura xml report: artifacts/coverage/cobertura-coverage.xml

위 두 파일을 Post Build Process에 다음과 같이 등록하면 된다.

Completed

이제 build를 하면 다음과 같은 결과를 볼 수 있다.

nodejs application은 test가 일반 java application보다 빠르게 끝나는 것이 특징이고, 무엇보다 TDD나 BDD를 사용하기가 좀 더 용의한 언어구조를 가지고 있다. 좀 더 즐거운 개발을 할 수 있지 않을까 생각된다.

Posted by Y2K
,

nodejs (express) + mongodb in Heroku

전부터 알고 있던 cloud base 개발 환경인 heroku를 한번 사용해보기로 했습니다. 일단 nodejs로 만들어진 application 이고, express와 mongodb를 사용하고 있는데. 이와 같은 환경을 무료로 제공해주는 곳 중에서 가장 유명하고, 한번 써보고자 하는 욕구가 강한 서비스여서 접근해보기로 했습니다.

먼저, 배포할 application의 구성입니다.

  • nodejs
  • express
  • mongodb
  • google OAuth 이용

nodejs + mongodb application deploy

Heroku service 가입

email을 등록시켜주면, 가입신청 확인 mail이 날라옵니다. 클릭후, password 설정만 하면 완료됩니다.
완료 후, https://dashboard-next.heroku.com/ 에서 Management Account -> Billing에서 신용카드 정보를 넣어두는 것이 좋습니다. 추후 설치할 addon에서 신용카드 정보가 없으면 진행할 수 없습니다.

Heroku toolbelt 설치

https://toolbelt.heroku.com/ 에서 환경에 맞는 toolbelt를 설치합니다. 저는 지금 linux를 설치하고 있기 때문에, linux 용 toolbelt를 설치한 기준으로 아래 글을 계속 이어가도록 하겠습니다.

설치후, heroku cmd를 사용할 수 있습니다.

Heroku toolbelt 인증 정보 + ssh public key 전송

heroku toolbelt를 설치하면 heroku cmd를 사용해서 인증정보를 입력합니다. heroku service에 가입한 email과 password를 넣어주면 됩니다. 그리고 ssh public key를 heroku에 등록시켜 이제는 heroku email/password가 아닌 ssh key인증을 이용해서 heroku cmd를 사용할 수 있습니다.

$> heroku login
Enter your Heroku credentials.
Email: 
Password:
Could not find an existing public key.
Would you like to generate one? [Yn]
Generating new SSH public key.
Uploading ssh public key /Users/adam/.ssh/id_rsa.pub

Application의 준비

기본적으로 heroku toolbelt를 이용한 application의 배포는 git를 이용합니다. local 또는 http://github.com 에서 제공되는 git repository에 project가 등록되어 있어야지 됩니다.

java의 pom.xml이나 build.gradle과 같은 기능을 하는 것이 package.json 입니다. 여기에 모든 dependency가 기록되어있어야지 됩니다.

그리고, scripts 항목의 start에 반드시 application의 실행 node command를 넣어줘야지 됩니다. 다음은 scripts 항목의 예시입니다.

  "scripts": {
    "start": "node app.js",
    "test": "grunt jasmine"
  }

개인적으로 이부분에서 실수를 한것이, 제 개발환경이 express가 global로 설치가 되어있습니다. 그래서 express가 package.json에 등록되어있지 않았습니다. 이렇게 되는 경우, dependency 문제로 인하여 application이 정상적으로 동작하지 않습니다. 반드시 package.json 정보만으로 application이 구동될 수 있어야지 됩니다.

git repository와 heroku repository 간의 연결

git repository directory안에서 다음 command를 실행합니다.

$>  heroku create --http-git
Creating sharp-rain-871... done, stack is cedar-14
http://sharp-rain-871.herokuapp.com/ | https://git.heroku.com/sharp-rain-871.git
Git remote heroku added

위 command의 결과, git는 remote repository를 하나 더 만들게 됩니다. 이 remote repository에 우리가 appliation을 배포하면 heroku에서 running되는 application을 구성하게 되는 것입니다.

위 console 창의 결과는 sharp-rain-871이라는 application의 이름을 갖게 됩니다. 이 이름은 unique 한 결과이며, 변경을 원하는 원하는 경우, heroku apps:rename 명령어를 통해 변경할 수 있습니다. 모든 action은 위 git repository directory 안에서 실행되어야지 됩니다.

git repository에서 heroku repository로 deploy

git repository directory안에서 다음 command를 실행합니다.

$> git push heroku master
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 386 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Node.js app detected
remote: -----> Requested node range:  0.10.33
remote: -----> Resolved node version: 0.10.33
remote: -----> Downloading and installing node
remote: -----> Restoring node_modules directory from cache
remote: -----> Pruning cached dependencies not specified in package.json
remote: -----> Exporting config vars to environment
remote: -----> Installing dependencies
remote:        
remote:        > sadari-app@1.0.0 postinstall /tmp/build_efb71b073a7c6b8520ad4613bd4ca2a3

deploy가 마치면 heroku open command를 통해서 browser를 이용해서 application이 구동되고 있는 것을 확인할 수 있습니다.

heroku는 cloud platform입니다. instance의 갯수를 제어할 수 있습니다. instance를 1개 이상 사용하는 경우, 비용이 발생하기 때문에 주의가 필요합니다.

$> heroku ps:scale web=1

bower를 이용한 javascript module dependency 처리

bower를 이용하는 경우, javascript를 각각의 repository에서 다운 받아 처리하게 됩니다. 따라서 bower components의 경우, repository에 올라가지 않는 것이 원칙입니다. 따라서, heroku에 배포된 application은 bower components가 하나도 없게 됩니다.

개발환경에서 bower install을 통해서 bower components들을 얻어주는 것과 같이 heroku에서도 이와 같은 action이 필요하게 됩니다. heroku의 nodejs 환경에는 아무런 환경이 없다고 생각해야지 됩니다. 이를 해결하기 위해서 bower 자체를 dependency에 추가해야지 됩니다.

npm install bower --save

bower를 추가후, package.json 파일의 scripts 항목에 다음을 추가한 후, heroku에 배포를 다시 진행합니다.

  "scripts": {
    "start": "node app.js",
    "test": "grunt jasmine",
    "postinstall": "bower install"
  },

이제 application을 정상적으로 설치하는 과정이 모두 마쳐졌습니다.

mongodb 설치

제가 만든 application은 mongodb를 사용하고 있습니다. mongodb는 heroku에서 addon 형태로 제공하고 있습니다. 500M 까지는 무료로 사용할 수 있으니, 아주 작은 application의 경우에는 언제나 무료로 사용할 수 있습니다. compose mongoDb에 대한 소개는 다음 URL에서 확인 가능합니다. https://devcenter.heroku.com/articles/mongohq

mongodb addon의 설치는 다음과 같습니다.

$> heroku addons:add mongohq

여기서, billing information이 설정되어 있지 않으면 addon의 설치는 진행되지 않습니다. 주의가 필요합니다.

mongodb 연결

compose mongodb를 설치하면, ENV에서 db connection url을 확인할 수 있습니다. config 값의 확인은 다음과 같습니다.

$> heroku config
=== my-sadari Config Vars
MONGOHQ_URL: mongodb://heroku:Xucs9HmJrBKBr6bPkO6CM044_G6oVv5gV0bN0dxcXS-LYHWmKdWXaPnqcYwa7MQn1iVyOmbrP4BRT8QkdJcxlQ@dogen.mongohq.com:10058/app31973614

이제 이 값을 이용하는 nodejs code는 다음과 같습니다.

db: process.env.MONGOHQ_URL

mondodb data import/export

개발환경에서의 mongodb의 값을 heroku에 옮겨야지 되는 경우는 자주 발생합니다. 기본적인 key값이라던지, 초기값들을 넣어주는 작업들은 반드시 필요한 작업들입니다. 이러한 작업을 application에서 해주는 것도 좋지만, 개발중인 mongodb의 값을 eport 시킨 후, import 시켜주는 것이 보다 더 편합니다.

이를 위해서는 먼저 원 데이터의 export가 필요합니다.
기본적인 export command는 다음과 같습니다.

mongoexport --collection <collection> --out <collection.json>

각 collection 당 json 파일을 만들어야지 되는 것을 까먹으면 안됩니다.
이제 heroku mongodb로 값을 export 시켜야지 됩니다. heroku deshboard로 가면 설치된 addon들을 모두 볼 수 있습니다. (아래의 무료 Plan은 이제 없습니다. 기존에 사용하고 있던 사람들은 계속 사용가능합니다.)

click 하면 Compose MongoDB configuration 화면을 볼 수 있습니다.

여기서 Admin으로 들어가서 새로운 사용자를 하나 만들어줍니다.

이제 만들어진 사용자로, import를 진행할겁니다. 이 관리자 console에서 remote url을 확인 할 수 있습니다.

위 정보를 이용해서 mongodb import는 다음 command를 이용해서 처리가 가능합니다.

mongoimport --host dogen.mongohq.com --port 10058 --username <user> --password <pass> --collection <collection> --db app31973614 --file <file.json>

Summary

heroku에 nodejs를 이용한 appliation 배포에 대해서 알아봤습니다. 제가 속한 팀에서의 점심후의 사다리타기 결과를 모아둔 heroku app을 공개해뒀습니다. ㅋㅋ (http://my-sadari.herokuapp.com) heroku는 300M까지는 무료입니다. 이 점에서 nodejs의 가벼운 code의 강점이 보이는 것 같습니다. 아무리 가벼운 웹이라도 은근히 용량이 되는 경우가 많으니까요.

그럼 모두 Happy Coding!


Posted by Y2K
,

nodejs + express를 이용한 Google OAuth 연동

OAuth는 근간에 거의 모든 WebSite에서 사용되고 있는 인증방법입니다. 회원 가입절차를 빠르게 할 수 있으며, 개인정보에 대한 관리 이슈를 피할 수 있어, 많이들 사용되는 방법입니다. nodejs를 이용한 OAuth 인증 방법에 대해서 알아보도록 하겠습니다.

npm package 설치

npm install passport
npm install passport-google-oauth
npm install cookie-session
  • passport: OAuth를 지원하기 위한 base package 입니다. twitter, facebook, google에서 지원하고 있는 OAuth를 모두 지원합니다.
  • passport-google-oauth: passport를 기반으로 하는 google-oauth 지원 package입니다.
  • cookie-session: express에서 session을 지원하기 위한 package입니다. express-session으로 대체해서 사용 가능합니다.

google auth api 설정

https://console.developers.google.com 에서 Google에서 제공하는 API를 이용하는 project를 생성할 수 있습니다. 새로운 project를 생성한 후, API 및 인증 > 사용자인증정보에서 새클라이언트 ID 만들기를 통해서 새로운 API를 만들어줍니다. 여기서 중요한 설정은 다음과 같습니다.

  • 승인된 javascript 원본 : 인증을 요청할 url을 넣습니다. 개발자 환경인 http://localhost:3000 과 같은 표현 역시 가능합니다.
  • 승인된 redirection URL : 인증이 완료된 후, redirect될 url을 넣어줍니다.

만들어진 웹어플리케이션용 Client ID에서 이제 3개의 정보는 우리가 작성할 application에서 사용해야지 됩니다.

  • 클라이언트 ID
  • 클라이언트 보안 비밀
  • URI 리디렉션

Login uri handling

사용자가 OAuth를 통해서 인증될 URL을 설정합니다. express controller code에 해당되는 내용입니다.

module.exports = OAuthController;

function OAuthController(app) {
  var passport = require('passport');
  app.use(passport.initialize());
  app.use(passport.session());
  // Google OAuth를 사용하는데 필요한 객체를 선언합니다.
  var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

  passport.serializeUser(function(user, done) {
    done(null, user);
  });
  passport.deserializeUser(function(obj, done) {
    done(null, obj);
  });

  passport.use(new GoogleStrategy({
    clientID: '클라이언트 ID',
    clientSecret: '클라이언트 보안 비밀',
    callbackURL: 'URI 리다렉션'
  },
  function(accessToken, refreshToken, profile, done) {
    //
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // req.session.passport 정보를 저장
    // done 메소드에 전달된 정보가 세션에 저장된다.
    // profile을 이용해서 사용자 정보를 DB에 넣는 등의 작업에 활용할 수 있다.
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    //
    return done(null, profile);
  }));

  // 인증 URI : uri link를 이곳에 넣어주면 됩니다.
  app.get('/auth/google', passport.authenticate('google', {
    scope: [
      'https://www.googleapis.com/auth/userinfo.profile',
      'https://www.googleapis.com/auth/userinfo.email'
    ]
  }));

  app.get('/oauth2callback', passport.authenticate('google', {
    successRedirect: '/auth/successed',
    failureRedirect: '/auth/failure'
  }));

  app.get('/logout', function(req, res) {
    //
    // passport 에서 지원하는 logout 메소드이다.
    // req.session.passport 의 정보를 삭제한다.
    //
    req.logout();
    req.session = null;
    res.redirect('/');
  });
};

Summary

Passport를 통해서 Google OAuth에 접근하는 방법은 많은 예시 code들이 있고, 거의 유사한 형태를 갖습니다. 위 controller code를 그대로 copy & paste로만도 간단히 Google OAuth를 구현 가능합니다.


Posted by Y2K
,

jasmine-node를 이용한 nodejs test

설치

다음 npm package를 설치해서 처리한다.

sudo npm install -g jasmine-node

Application의 구성

기본적으로 yo에서 제공되는 application 구조를 그대로 가지고 가는 것이 좋다. (express application을 기준)

.
├── Gruntfile.js
├── app
│   └── lib
├── app.js
├── npm-debug.log
├── package.json
└── test
    └── calculator-spec.js

test code의 실행

일반적으로 root directory에서 다음 명령어를 이용해서 처리하면 된다.

jasmine-node --test-dir test --autotest --watch app test --color

simple test code

target이 되는 file의 위치가 app/lib/testservice.js 라고 할때, export되는 객체는 다음과 같이 구성될 수 있다.

exports.method1 = function() {
  return true;
};

exports.method2 = function() {
  return false;
};

javascript는 method 단위의 구성을 주목할 필요가 있다.

이때, 이 method1, method2를 test하는 code는 다음과 같이 작성가능하다.

var testservice = require('../app/lib/testservice.js');
describe('test testservice', function() {
  it('method1 called', function() {
    expect(testservice.method1()).toBe(true);
  });

  it('method2 called', function() {
    expect(testservice.method2()).toBe(false);
  });
});

simple test code 2

만약에 db를 접속해서 만들어지는 BL이 존재한다면 다음과 같이 구성한다. yo express-generator에서는 db connection이 app.js에서 구성되는데, 이를 따로 뽑아서 사용하면 db에 대한 service code test가 쉽다.
mongoDb에 접근하는 코드는 다음과 같다.

var mongoose = require('mongoose'),
    glob = require('glob'),
    path = require('path'),
    rootPath = path.normalize(__dirname + '/..');

var init = function() {
  mongoose.connect('mongodb://localhost/sadari');
  var db = mongoose.connection;
  db.on('error', function() {
    throw new Error('unable connect to database');
  });

  //glob.sync는 absolute directory를 기준으로 검색하기 때문에 반드시 path에 대한 정보가 필요하다.
  var models = glob.sync(rootPath + '/models/*.js');
  models.forEach(function(model) {
    require(model);
  });
};

exports.init = init;

위와 같은 db connection을 이용하는 간단한 CRUD service를 작성할 때, code는 다음과 같이 구성될 수 있다.

var mongoose = require('mongoose');
var Player = mongoose.model('Player');
var ObjectId = mongoose.Types.ObjectId;

var findAll = function(func) {
  return Player.find(function(err, players) {
    if(func) {
      func(players);
    }
  });
};

var save = function(name, defaultAmount) {
  var player = new Player({
    name: name,
    defaultAmount: defaultAmount
  });
  return player.save(function(err) {
    throw new Exception(err);
  });
};

var findOne = function(id, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.findById(objectId, func);
};

var find = function(query, func) {
  Player.find(query, func);
};

var update = function(id, name, defaultAmount, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.update(
    {_id: objectId},
    {
      $set: {
        name: name,
        defaultAmount: defaultAmount
      }
    }, { upsert: false, multi: true }, func);
};

exports.update = update;
exports.findOne = findOne;
exports.findAll = findAll;
exports.save = save;
exports.find = find;

이에 대한 test code는 다음과 같다.

var dbConnection = require('../app/lib/db-connect.js');
dbConnection.init();
var playerService = require('../app/lib/playerservice.js');

describe('mongoDb test', function() {
  it('playerService.findAll', function(done) {
    var conn = playerService.findAll(function(players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(player) {
        expect(player.defaultAmount).not.toBe(0);
      });
      done();
    });
    expect(!!conn).toBe(true);
  });

  it('playerService.save', function() {
    var result = playerService.save('playerName', 10);
    expect(result).not.toBe(null);
  });

  it('playerService.find', function(done) {
    playerService.find({}, function(err, players) {
      expect(players.length).toBe(1);
      players.forEach(function(player) {
        console.log(player.name);
      });
      done();
    });
  });

  it('playerService.findOne', function(done) {
    var id = "5469751de83eaf6e34434c5e";
    playerService.findOne(id, function(err, player) {
      console.log(err);
      expect(!!err).toBe(false);
      expect(player).not.toBe(null);
      console.log(player);
      console.log(player.name);

      done();
    });
  });

  it('playerService.update', function(done) {
    var id = "5469751ae83eaf6e34434c55";
    playerService.update(id, "changedName2", 100, function(err, model) {
      expect(!!err).toBe(false);
      done();
    });
  });

  it('players list', function(done) {
    var mongoose = require('mongoose');
    var Player = mongoose.model('Player');
    var playersResult = null;
    Player.find(function(err, players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(p) {
        expect(p.defaultAmount).not.toBe(0);
      });
      done();
    });
  });
});


Posted by Y2K
,