satococoa's blog

主にサーバーサイド、Web 系エンジニアのブログです。Go, Ruby, React, GCP, ...etc.

CloudFoundryでNode+MongoDBを動かした

先週末の開発コンテスト24の合間にチャレンジしていたのですがうまく動かず、さきほどようやく動かせました。

コツは3つ。

  1. npm bundleで必要なライブラリをインストールする
  2. 環境変数からMongoDBへの接続情報をとり出す
  3. vmc envでNODE_ENVを設定する

npm bundleで必要なライブラリをインストールする

今回動かしてみたのはexpressのアプリです。(NodeでMongoDBをいじってみた参照)
ちなみに"$ express -s -c sass"のコマンドで生成しています。

Nodeアプリを動かすには動作に必要なライブラリも一緒にCloudfoundryにアップする必要があり、その方法がnpm bundleです。
ちょっと手間がかかるのですが、まずはpackage.jsonというファイルを用意します。

{
  "name": "instanode",
  "version": "0.0.1",
  "dependencies": {
    "express":"",
    "jade":"",
    "less":"",
    "mongodb":"",
    "socket.io":"",
    "instagram":""
  }
}

dependenciesの中に必要なライブラリを列挙しました。バージョンの指定などもできますが、今回はしていません。 このファイルをapp.jsと同じディレクトリに置いて、$ npm bundle と打てば、node_modules/内にライブラリがインストールされます。

これが終わると以下のような構成になります。

$ tree
.
├── app.js
├── logs
├── node_modules
│   ├── connect -> ./connect@1.3.0
│   ├── connect@1.3.0 -> ./.npm/connect/1.3.0/package
│   ├── express -> ./express@2.2.2
│   ├── express@2.2.2 -> ./.npm/express/2.2.2/package
│   ├── instagram -> ./instagram@0.0.4
│   ├── instagram@0.0.4 -> ./.npm/instagram/0.0.4/package
│   ├── jade -> ./jade@0.10.4
│   ├── jade@0.10.4 -> ./.npm/jade/0.10.4/package
│   ├── less -> ./less@1.0.41
│   ├── less@1.0.41 -> ./.npm/less/1.0.41/package
│   ├── mime -> ./mime@1.2.1
│   ├── mime@1.2.1 -> ./.npm/mime/1.2.1/package
│   ├── mongodb -> ./mongodb@0.9.3
│   ├── mongodb@0.9.3 -> ./.npm/mongodb/0.9.3/package
│   ├── qs -> ./qs@0.1.0
│   ├── qs@0.1.0 -> ./.npm/qs/0.1.0/package
│   ├── socket.io -> ./socket.io@0.6.17
│   └── socket.io@0.6.17 -> ./.npm/socket.io/0.6.17/package
├── package.json
├── pids
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       ├── style.css
│       └── style.sass
├── test
│   └── app.test.js
└── views
    ├── index.jade
    └── layout.jade

27 directories, 7 files

あとはapp.jsに以下を書き足して、requireできるようにします。

require.paths.unshift('./node_modules');

環境変数からMongoDBへの接続情報をとり出す

vmc pushとか、MongoDBを有効にする方法とか、そのへんは他にも書いてある記事がありますし指示に従えばできるので省略しちゃいます。

MongoDBへの接続情報は環境変数で渡されます。 以下のようなコードでその値を取り出してMongoDBに接続します。

app.configure('production', function(){
  app.use(express.errorHandler()); 
  var env = JSON.parse(process.env.VCAP_SERVICES);
  var mongo = env['mongodb-1.8'][0]['credentials'];
  client = new Db('db', new Server(mongo.hostname, mongo.port, {}));
  client.open(function(err, p_client){
    client.authenticate(mongo.username, mongo.password, function(){});
  });
});

vmc envでNODE_ENVを設定する

先ほど挙げたコードを見ると、sinatraに慣れた人なら'production'の指定に気づくと思います。

これがまたちょっと曲者で、自分でNODE_ENVを設定しておかないと'production'の方のコードが実行されませんでした。

$ vmc env-add instanode env-add NODE_ENV=production

heroku configと似ていますね。これでOK。

app.jsの全体

最後にapp.jsの全体を載せます。ご参考になれば幸いです。

require.paths.unshift('./node_modules');

/**
 * Module dependencies.
 */

var express = require('express'),
    instagram = require('instagram').createClient(
      'YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET'),
    io = require('socket.io'),
    json = JSON.stringify,
    Db = require('mongodb').Db,
    Server = require('mongodb').Server;

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({ secret: 'YOUR_SECRET' }));
  app.use(express.compiler({ src: __dirname + '/public', enable: ['sass'] }));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

var client = {};
app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
  client = new Db('instanode', new Server('127.0.0.1', 27017, {}));
  client.open(function(err, p_client){});
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
  var env = JSON.parse(process.env.VCAP_SERVICES);
  var mongo = env['mongodb-1.8'][0]['credentials'];
  client = new Db('db', new Server(mongo.hostname, mongo.port, {}));
  client.open(function(err, p_client){
    client.authenticate(mongo.username, mongo.password, function(){});
  });
});

// Routes

app.get('/', function(req, res){
  client.collection('images', function(err, collection){
    collection.find({}, {limit: 10}).toArray(function(err, images){
      res.render('index', {
        title: 'index',
        images: images
      });
    });
  });
});

app.get('/popular', function(req, res){
  instagram.media.popular(function(images, error) {
    client.collection('images', function(err, collection){
      collection.remove();
      for (var i=0, l=images.length; i < l; i++) {
        collection.insert(images[i]);
      }
      res.redirect('/');
    });
  });
});

// Only listen on $ node app.js

if (!module.parent) {
  app.listen(process.env.VMC_APP_PORT || 3000);
}

http://instanode.cloudfoundry.com/popularにアクセスすると、

  1. instagramからpopularの写真を取ってきて
  2. MongoDBにそのまま格納し
  3. http://instanode.cloudfoundry.com/にリダイレクトし、
  4. MongoDBから写真のデータを取り出し
  5. 画面に表示します。

次はSocket.ioを試そうと思います。(ライブラリはインストールしていますが、上記コードではまだ使っていません。)