Node.js and MongoDB on Ubuntu

2011-09-07 (cloud, juju, node, mongo)

I gave my first talk on IRC the other day on deploying Node.js & Mongo in Ubuntu… it was quite a new experience. Figured I’d post details of the talk here.

An example stack

We’ll use juju to deploy a basic node.js app along with a couple of typical surrounding services..

  • haproxy to catch inbound web traffic and route it to our node.js app cluster
  • mongodb for app storage

Along the way, we’ll see what it takes to connect and scale this particular stack of services. I’ll err on the side of too much detail over simplicity in this example, but I’ll try to make it clear when there’s a sidebar topic.

At the end of the day, the deployment for our application would look like the usual juju deployment

$ juju bootstrap

(with a pregnant pause to allow EC2 to catch up)

$ juju deploy --repository ~/charms local:mongodb
$ juju deploy --repository ~/charms local:node-app myapp
$ juju add-relation mongodb myapp

$ juju deploy --repository ~/charms local:haproxy
$ juju add-relation myapp haproxy
$ juju expose haproxy

(with another pregnant pause to allow EC2 to catch up)

We can get the service URLs from

$ juju status

and hit the head of the haproxy service to see the app in action.

We can scale it out with

$ for i in {1..4}; do
$   juju add-unit myapp
$ done

and we’ll soon have a cluster of one haproxy node balancing between five application nodes all talking to a single mongo node in the backend. Of course, we can scale mongo too, but that’s another post.

juju “Application” charms

There are two types of juju charms used in this example:

“Canned Charms”, like the haproxy charm and the mongodb charm, and “Application Charms”, like the node.js app charm.

Canned charms can be used as-is right off the shelf.

Application charms are used to manage your custom application as an juju service. We haven’t nailed down the language on this, but these charms create a contained environment, “framework”, or “wrapper” around your custom application and help it to play nicely with other services.

The node-app charm we use here is meant to be an example that you can fork/adapt and use to maintain custom components of your infrastructure.

The node-app charm

The node-app charm is the key feature we want to look at. It’s a charm that will pull your app from revision control and config/deploy/maintain it as a service within your infrastructure.

Setup and clone this charm

$ mkdir ~/charms
$ cd ~/charms
~/charms$ git clone http://github.com/charms/node-app

and we’ll walk through it.

README.markdown
config.yaml
copyright
metadata.yaml
revision
hooks/
  install
  mongodb-relation-changed
  mongodb-relation-departed
  mongodb-relation-joined
  start
  stop
  website-relation-joined

We can see the usual install, start, and stop hooks for the node.js service, along with a couple of other hooks for relating to other services.

Before we go into this in detail, let’s take a little sidebar on the Node.js app we’ll be deploying…

Example node.js app

The example app I’m using for this

http://github.com/mmm/testnode

just logs page hits in mongo and reports results.

As usual, I have absolutely no graphic design gifts so things look a little bare-bones. Don’t let that fool you… it’s quite easy to dress this up with some svg maps and some client-side js a la topfunky’s (peepcode.com) node examples.

This is a really basic node app that…

Reads config info

var config = require('./config/config'),
    mongo = require('mongodb'),
    http = require('http');

from a file config/config.js

module.exports = config = {
   "name" : "mynodeapp"
  ,"listen_port" : 8000
  ,"mongo_host" : "localhost"
  ,"mongo_port" : 27017
}

attaches to the mongo instance specified in the config file

var db = new mongo.Db('mynodeapp', new mongo.Server(config.mongo_host, config.mongo_port, {}), {});

spins up a webservice

var server = http.createServer(function (request, response) {

  var url = require('url').parse(request.url);

  if(url.pathname === '/hits') {
    show_log(request, response);
  } else {
    track_hit(request, response);
  }

});
server.listen(config.listen_port);

and handles requests.

The entire app would look something like

//require.paths.unshift(__dirname + '/lib');
//require.paths.unshift(__dirname);

var config = require('./config/config'),
    mongo = require('mongodb'),
    http = require('http');

var show_log = function(request, response){
  var db = new mongo.Db('mynodeapp', new mongo.Server(config.mongo_host, config.mongo_port, {}), {});
  db.addListener("error", function(error) { console.log("Error connecting to mongo"); });
  db.open(function(err, db){
    db.collection('addresses', function(err, collection){
      collection.find({}, {limit:10, sort:[['_id','desc']]}, function(err, cursor){
        cursor.toArray(function(err, items){
          response.writeHead(200, {'Content-Type': 'text/plain'});
          for(i=0; i<items.length;i++){
            response.write(JSON.stringify(items[i]) + "\n");
          }
          response.end();
        });
      });
    });
  });
}

var track_hit = function(request, response){
  var db = new mongo.Db('mynodeapp', new mongo.Server(config.mongo_host, config.mongo_port, {}), {});
  db.addListener("error", function(error) { console.log("Error connecting to mongo"); });
  db.open(function(err, db){
    db.collection('addresses', function(err, collection){
      var address = request.headers['x-forwarded-for'] || request.connection.remoteAddress;

      hit_record = { 'client': address,'ts': new Date() };

      collection.insert( hit_record, {safe:true}, function(err){
        if(err) { 
          console.log(err.stack);
        }
        response.writeHead(200, {'Content-Type': 'text/plain'});
        response.write(JSON.stringify(hit_record));
        response.end("Tracked hit from " + address + "\n");
      });
    });
  });
}

var server = http.createServer(function (request, response) {

  var url = require('url').parse(request.url);

  if(url.pathname === '/hits') {
    show_log(request, response);
  } else {
    track_hit(request, response);
  }

});
server.listen(config.listen_port);

console.log("Server running at http://0.0.0.0:" + config.listen_port + "/");

We won’t get into my node.js skillz at the moment… it’s a deployment example.

I’ve also got a package.json in there to let npm resolve some example dependencies upon install.

Now, there’s no standard way to handle configuration in node apps, so it’s quite likely your app’s config looks a bit different. No problem, it’s pretty straightforward to adapt this example charm to handle the way your app works… and use your own config file paths, and config parameter names.

End-of-sidebar… Back to the node-app charm.

Hooks

Let’s go through the hooks as they would be executing during deployment and service relation.

The install hook is kicked off upon deployment, reads its config from config.yaml and then will

  • install node/npm
  • clone your node app from the repo specified in app_repo
  • run npm if your app contains package.json
  • configure networking if your app contains config/config.js
  • create a startup service for your app
  • wait to startup once we’re joined to a mongodb service

start and stop are trivial in this charm because we want to wait for mongo to join before we actually run the app. If your app was simpler and didn’t depend on a backing store, then you could use these hooks to manage the service created during installation.

MongoDB

The key to almost every charm is in the relation hooks.

This particular app is written against mongodb so the app’s charm has hooks that get fired when the “app” service is related to the mongo service.

This relation was defined when we did

$ juju add-relation mongodb myapp

and the relation-joined/changed hooks get fired after the install and start hooks have successfully completed for both ends of the relationship.

The mongodb-relation-changed hook in this charm will read config from config.yaml

  • get relation info from the mongo service (i.e., hostname)
  • configure the app to use that host for mongo connections
  • start the node app service we created during install

That’s it really… our app is up and running at this point.

Note that the example here depends on mongo, but juju makes it easy to relate to some other backend db. Just like we have mongodb-relation-changed hooks, we could just as easily have cassandra-relation-changed hooks that would look strikingly similar. Of course, our app would have to be written in such a way that it could use either, but that’s another topic. The deployment tool supports the choice being made dynamically when relations are joined. I’d say “at deployment time” but it’s even better than that because I can remove relations and add other ones at any time throughout the lifetime of the service… and the correct hooks get called.

HAProxy

For this example, I’d like to use haproxy to load balance

This example stack uses haproxy to handle initial web requests from outside. haproxy will load balance across multiple instances of our app. That way we could just attach an elastic ip to haproxy, configure dns, and we’re cruising (of course we’re leaving out plenty of infrastructure aspects like monitoring/logging/backups/etc that are pretty important for a production deployment in the cloud).

The app charm has hooks that get fired when the “app” service is related to the haproxy service. Just as above, this relation was defined when we did

$ juju add-relation haproxy myapp

and the relation-joined/changed hooks get fired after the install and start hooks have successfully completed for both ends of the relationship.

The website-relation-changed hook in this charm in its entirety:

#!/bin/sh

app_port=`config-get app_port`
relation-set port=$app_port hostname=`hostname -f`

simply tells the haproxy service which address and port our application uses to handle requests.

We could of course configure our app to listen on port 80, tell the charm to open port 80 in its firewall, and then expose port 80 for our app service to the outside world. That’d be fine if we never needed to scale or we were planning to load balance multiple units of our app using dns, elastic load balancer instances, or something else external.

Again, note that the example here uses haproxy, but we could easily swap that out with any other service that consumed the juju http interface.

Charm configuration

Ok, so I lied a little up above when I said that the hooks read config info from config.yaml. Yes, they do read config information from there, but that’s not the whole story. The values of the configurable parameters can be set/overidden in a number of different ways throughout the lifecycle of the service.

You can pass in dynamic configuration during deployment or later at runtime using the cli

`juju set <service_name> <config_param>=<value>`

or configure the charm at deployment time via a yaml file passed to the juju deploy --config command.

Scaling tips

Scaling with juju works really well. The key to this lies in the boundaries between configuration for the service itself, versus configuration for the service in the context of a relation with another service.

When these two types of configuration are well isolated, scaling with juju just works. I’ve caught myself several times working on just getting a service charm working, with no real thought to scalability, and being pleasantly surprised to find out that the service pretty much scales as written.

The best way to grok this is to walk through the process of joining your relations as single unit services…

In our example,

haproxy <-> myapp <-> mongodb

containers for each service get instantiated, then the install and start hooks are run for each service. Once both sides of relations are started then the relation hooks get called: joined and then usually several rounds of changed depending on the relation parameters being set. Once these are complete, the services are up, related, and running.

Ok, now comes scaling. juju add-unit myapp adds a new myapp service node and goes through the whole cycle above. The “services” are already related, so the relation hooks are automatically fired as each new unit is started. Since we divided up the installation/configuration/setup/startup of the service into the parts that are specific to the service and parts that are specific to the relation with another service, then each new unit runs “just enough” configuration to join it to the cluster.

Not all tools can be configured like that, but that’s the key to strive for when writing relation hooks. Identify the components of your application configuration that really depend on another service, and isolate them as much as possible. Only configure relation-specific things in the relation hooks. The more minimal the relation hooks, the more scalable the service.

Monitoring Hadoop Benchmarks TeraGen/TeraSort with Ganglia

2011-09-03 (cloud, hadoop, juju)

#########################################################
NOTE: This is outdated... 

The internal project “ensemble” is now publicly known as “juju”. Please see the repost Monitoring Hadoop Benchmarks TeraGen/TeraSort with Ganglia of this article with new names and updates to the api.

#########################################################

Here I’m using new features of Ubuntu Server (namely Ubuntu juju) to easily deploy Ganglia alongside a small Hadoop cluster to play around with monitoring some benchmarks like Terasort.

Short Story

Deploy hadoop and ganglia using juju:

$ juju bootstrap
$ juju deploy --repository "~/charms"  hadoop-master namenode
$ juju deploy --repository "~/charms"  ganglia jobmonitor
$ juju deploy --repository "~/charms"  hadoop-slave datacluster
$ juju add-relation namenode datacluster
$ juju add-relation jobmonitor datacluster
$ for i in {1..6}; do
$   juju add-unit datacluster
$ done
$ juju expose jobmonitor

When all is said and done (and EC2 has caught up), run the jobs

$ juju ssh namenode/0
ubuntu@<ec2-url> $ sudo -su hdfs
hdfs@<ec2-url> $ time hadoop jar hadoop-*-examples.jar teragen -Dmapred.map.tasks=1000 1000000000 in_dir
hdfs@<ec2-url> $ hadoop job -history all in_dir
hdfs@<ec2-url> $ time hadoop jar hadoop-*-examples.jar terasort -Dmapred.reduce.tasks=1000 in_dir out_dir
hdfs@<ec2-url> $ hadoop job -history all in_dir

While these are running, we can run

$ juju status

to get the URL for the jobmonitor ganglia web frontend

http://<jobmonitor-instance-ec2-url>/ganglia/

and see…

and a little later as the jobs run…

Of course, I’m just playing around with ganglia at the moment… For real performance, I’d change my juju config file to choose larger (and ephemeral) EC2 instances instead of the defaults.

A Few Details…

Let’s grab the charms necessary to reproduce this.

First, let’s install juju and set up a repo for charms.

$ sudo add-apt-repository ppa:juju/pkgs
$ sudo apt-get install juju

Note that I’m describing all this using an Ubuntu laptop to run the juju cli because that’s how I roll, but you can certainly use a Mac to drive your Ubuntu services in the cloud. The juju CLI is already available in ports, but I’m not sure the version. Note to myself to add it to homebrew and do more testing with that setup. Windows should work too, but I don’t have a clue.

~$ mkdir ~/charms
~$ cd ~/charms
~/charms$ git clone http://github.com/charms/ganglia
~/charms$ git clone http://github.com/charms/hadoop-master
~/charms$ git clone http://github.com/charms/hadoop-slave

That’s about all that’s really necessary to get you up and benchmarking/monitoring.

I’ll do another post on how to adapt your own charms to use monitoring and the monitor juju interface as part of the “Core Infrastructure” series I’m writing for charm developers. Go over the process of what I had to do to get the hadoop-slave service talking to the ganglia service.

Until then, clone/test/enjoy… or better yet, fork/adapt/use!

Core Infrastructure Part 1 - Adding NFS to juju charms

2011-08-25 (cloud, juju)

Several features have recently landed in juju that make it easier to convert some common infrastructure-level components over and use them within your stack of services. I figured it’s worth a series of posts about core services, storage, logging, monitoring, messaging, etc… and how they fit into the juju ecosystem.

We’ll start with NFS. I’d consider that vanilla workhorse to be core infrastructure technology… it’s still pretty much everywhere you look. Many friends keep ancient boxes around as dedicated NFS fileserver appliances that are usually in fairly critical roles. Well, how would this fit into a sexy new juju stack of services?

The Goal

In this post I’ll walk through adapting a basic mediawiki charm to enable using nfs mounts for shared resource directories. The steps here should work pretty closely for a lot of different service charms that might benefit from a NFS hooks.

At the end of the day, an example stack might look something like

$ juju bootstrap
$ juju deploy --repository=~/charms local:nfs myimages
$ juju deploy --repository=~/charms local:mysql
$ juju deploy --repository=~/charms local:mediawiki mywiki
$ juju add-relation mysql:db mywiki:db
$ juju add-relation myimages mywiki
$ juju expose mywiki

which will result in a mediawiki setup with images stored on an nfs server.

As expected,

$ juju add-unit mywiki
$ juju add-unit mywiki

will scale mediawiki instances.

Since the mywiki service is related to the myimages service, then all mywiki instances will share the same resource directory on the nfs server. This works months into the deployment… we add another unit and the relation hooks perform the mount.

The nfs server charm is pretty canned, it’ll take config options, but has sane defaults. There’s little need to adjust anything in there.

To adapt your charm to use a juju-managed NFS service, we copy two hooks from an nfs-client example charm and adapt to suit the needs of the charm we’re working on (mediawiki in this example).

Detailed walk-through

Let’s go through this process of adapting our charms to use NFS mounts.

The Problem

Start with a simple mediawiki deploy,

$ juju bootstrap
$ juju deploy --repository=~/charms local:mysql
$ juju deploy --repository=~/charms local:mediawiki mywiki
$ juju add-relation mysql:db mywiki:db
$ juju expose mywiki

With juju, it’s easy to spin up multiple mediawiki instances (or “service units” in juju-speak).

$ juju add-unit mywiki
$ juju add-unit mywiki

Now, since the mywiki service is related to the mysql service, then all these mywiki units share the same database and hence content. Groovy.

Ok, well there’s a problem. Each wiki instance has its own filesystem-store for various things… images, uploads, etc. This is pretty common practice across different CMSes that results in scaling pain. The content in the database will point to resources that only exist on one of the instances.

The Solution

A common solution to this is to just put these resource directories on a fileserver and share them out to each of the wiki instances using NFS.

This, of course, isn’t limited to content management systems. Just about every infrastructure has some sort of need for shared storage.

Ubuntu packages for mediawiki install mediawiki’s resource directories with respect to /var/lib/mediawiki/. So let’s just set adapt the basic mediawiki charm to use an nfs share for /var/lib/mediawiki/images.

Each mediawiki service unit has a /var/lib/mediawiki/images directory that points to up multiple mediawiki instances, they’ll all share the same images directory.

Copy code examples over as a template

Let’s grab some code and get cranking…

First, pull mediawiki as an example (or any charm you’re currently working on that might need shared storage)

$ mkdir ~/charms
$ cd ~/charms
$ bzr branch lp:~mark-mims/+junk/juju-mediawiki-nfsdemo mediawiki

This gives us

/home/mmm/charms/mediawiki
|-- copyright
|-- hooks
|   |-- ...
|   |-- db-relation-changed
|   |-- install
|   |-- start
|   |-- stop
|   |-- website-relation-changed
|   |-- website-relation-joined
|   `-- ...
|-- metadata.yaml
`-- revision

a bare mediawiki charm that we can use as a starting point to add our nfs client hooks. Note that even though this is an ‘nfsdemo’ branch just for this demo, the real mediawiki charm should now have nfs support in the trunk.

Now, we can copy sample nfs client hooks from

http://bazaar.launchpad.net/~mark-mims/+junk/principia-nfs-client/files/head:/hooks/

Grab storage-relation-joined and storage-relation-changed, and save them as mediawiki hooks… I’d rename them to something that makes sense to future readers of our mediawiki charm. Let’s call them nfs-imagestore-relation-joined and nfs-imagestore-relation-changed.

Adapt the example code to our charm

Let’s look at the nfs-imagestore-relation-joined hook:

#!/bin/bash
set -ue

apt-get install -y nfs-common

sed -i -e "s/NEED_IDMAPD.*/NEED_IDMAPD=yes/" /etc/default/nfs-common
service idmapd restart || service idmapd start

relation-set client=`hostname -f`

This looks pretty normal. It installs some stuff nfs stuff when the relation is joined… this is good it doesn’t really install it until it’s needed. Then it tells the nfs server who we are for access control. Ok, so no changes need to be made to get this working on our mediawiki service unit… we can leave it as-is.

Next, what about the nfs-imagestore-relation-changed hook?

#!/bin/bash
set -ue

remote_host=`relation-get hostname`
if [ -z "$remote_host" ] ; then
    juju-log "remote host not set yet."
    exit 0
fi
export_path=`relation-get mountpoint`
fstype=`relation-get fstype`

local_mountpoint=`config-get mountpoint`
local_owner=`config-get owner`
mount_options=""

create_local_mountpoint() {
  juju-log "creating local mountpoint"
  umask 002
  mkdir -p $local_mountpoint
  # create owner if necessary?
  chown -f $local_owner.$local_owner $local_mountpoint
}
[ -d $local_mountpoint ] || create_local_mountpoint 

share_already_mounted() {
  `mount | grep -q $local_mountpoint`
}
mount_share() {
  for try in {1..3}; do

    juju-log "mounting nfs share"
    [ ! -z $mount_options ] && options="-o ${mount_options}" || options=""
    mount -t $fstype $options $remote_host:$export_path $local_mountpoint \
      && break

    juju-log "mount failed: $local_mountpoint"
    sleep 10

  done
}
share_already_mounted || mount_share 

# ownership
chown -f $local_owner.$local_owner $local_mountpoint

Ok, when parameters used within charm hooks are likely to change from one service deployment to the next, they can be externalized into a config.yaml for the charm. This also allows you to pass them in at deploy-time or change them throughout the lifetime of the service using juju cli --config options and set commands. See the juju docs for how to do all of this.

There are two parameters in the nfs-imagestore-relation-changed hook that are set using config-get:

local_mountpoint=`config-get mountpoint`
local_owner=`config-get owner`

Well, for mediawiki these parameters aren’t really going to change. It’s reasonable to just hard-code them here as:

mw_root="/var/lib/mediawiki"
local_mountpoint="$mw_root/images"
local_owner="www-data"

We could certainly just add these parameters to a config.yaml for the mediawiki charm, but in this case, they’re not really interesting “tweakable” aspects of the mediawiki charm, so let’s keep the mediawiki config simple. For other charms this might not be the case, but make that call in context.

Now, at this point, our version of the nfs-imagestore-relation-changed hook:

#!/bin/bash
set -ue

remote_host=`relation-get hostname`
if [ -z "$remote_host" ] ; then
    juju-log "remote host not set yet."
    exit 0
fi
export_path=`relation-get mountpoint`
fstype=`relation-get fstype`

mw_root="/var/lib/mediawiki"
local_mountpoint="$mw_root/images"
local_owner="www-data"
mount_options=""

create_local_mountpoint() {
  juju-log "creating local mountpoint"
  umask 002
  mkdir -p $local_mountpoint
  # create owner if necessary?
  chown -f $local_owner.$local_owner $local_mountpoint
}
[ -d $local_mountpoint ] || create_local_mountpoint 

share_already_mounted() {
  `mount | grep -q $local_mountpoint`
}
mount_share() {
  for try in {1..3}; do

    juju-log "mounting nfs share"
    [ ! -z $mount_options ] && options="-o ${mount_options}" || options=""
    mount -t $fstype $options $remote_host:$export_path $local_mountpoint \
      && break

    juju-log "mount failed: $local_mountpoint"
    sleep 10

  done
}
share_already_mounted || mount_share 

# insure ownership
chown -f $local_owner.$local_owner $local_mountpoint

will run fine. It’ll mount the images share and mediawiki will run normally.

Additional configuration

This next step depends greatly on your particular charm/service. With mediawiki, the images directory isn’t really used until you tell mediawiki to turn on uploads. This is true regardless of whether images is an nfs mount or you’re just using the directory on the local filesystem.

The time to do this though is after the images share is mounted, so putting it in the nfs-imagestore-relation-changed hook after the mount is a good place for it.

juju-log "updating mediawiki upload config"

cat > /etc/mediawiki/upload_settings.php <<'EOS'
$wgEnableUploads = true;
EOS

service apache2 status && service apache2 restart

So we end up with

#!/bin/bash
set -ue

remote_host=`relation-get hostname`
if [ -z "$remote_host" ] ; then
    juju-log "remote host not set yet."
    exit 0
fi
export_path=`relation-get mountpoint`
fstype=`relation-get fstype`

mw_root="/var/lib/mediawiki"
local_mountpoint="$mw_root/images"
local_owner="www-data"
mount_options=""

create_local_mountpoint() {
  juju-log "creating local mountpoint"
  umask 002
  mkdir -p $local_mountpoint
  # create owner if necessary?
  chown -f $local_owner.$local_owner $local_mountpoint
}
[ -d $local_mountpoint ] || create_local_mountpoint 

share_already_mounted() {
  `mount | grep -q $local_mountpoint`
}
mount_share() {
  for try in {1..3}; do

    juju-log "mounting nfs share"
    [ ! -z $mount_options ] && options="-o ${mount_options}" || options=""
    mount -t $fstype $options $remote_host:$export_path $local_mountpoint \
      && break

    juju-log "mount failed: $local_mountpoint"
    sleep 10

  done
}
share_already_mounted || mount_share 

# insure ownership
chown -f $local_owner.$local_owner $local_mountpoint

juju-log "updating mediawiki upload config"

cat > /etc/mediawiki/upload_settings.php <<'EOS'
$wgEnableUploads = true;
EOS

service apache2 status && service apache2 restart

the same hook that’s in lp:~mark-mims/+junk/juju-mediawiki-nfs-imagestore.

Update charm metadata

So we have hooks that should do what we’d like… (I’ll developing and debugging hooks in another post). Next we need to update the charm metadata so juju knows when to fire them.

My mediawiki/metadata.yaml currently looks like

...
requires:
  db:
    interface: mysql
  slave:
    interface: mysql
  cache:
    interface: memcache
provides:
  website:
    interface: http
...

let’s add our nfs requirement (_requirement_ is a strong word… they’re all optional),

...
requires:
  db:
    interface: mysql
  slave:
    interface: mysql
  cache:
    interface: memcache
  nfs-imagestore:
    interface: mount
provides:
  website:
    interface: http
...

The relation name’s gotta match up with what we called the hooks. I got the interface name from the nfs server charm’s metadata:

...
provides:
  nfs:
    interface: mount
...

which provides the mount interface.

We can use the interface as a sanity check to insure we’re providing and using the right parameters when communicating with the nfs server service (using relation-get and relation-set) in our hooks.

Spin it up

Let’s deploy our new charm:

$ juju bootstrap
$ juju deploy --repository=~/charms local:nfs myimages
$ juju deploy --repository=~/charms local:mysql
$ juju deploy --repository=~/charms local:mediawiki mywiki
$ juju add-relation mysql:db mywiki:db
$ juju add-relation myimages mywiki
$ juju expose mywiki

Spread it out

Add a few more mediawiki instances…

$ for i in {1..10}; do
$ juju add-unit mywiki
$ done

When everything’s up, these should all share the same database and image store.

In real life you’d probably want to start adding mysql slaves to this. Of course, in real life you’ll also add remote logging, monitoring, and other core infrastructure components too. Keep an eye out for subsequent posts in this series.

NFS server config

Up until now we’ve only talked about the NFS client. The NFS server charm is pretty simple.

It lives at lp:principia/nfs and supports configuration options at deploy(or run)-time via

$ juju deploy --repository . --config ./mydata.yaml local:nfs mydata

where mydata.yaml looks like

mydata:
  initial_daemon_count: 43
  storage_root: /srv/mydata
  export_options: rw,sync,no_root_squash,no_all_squash

What’s left?

This might not be an ideal NFS setup for your particular service. What are some other ways we can tweak this?

Currently, the nfs server creates a separate share for each named service that attaches to it.

We deployed mediawiki as mywiki, so all mywiki service units would share an export. Well, you could deploy another mediawiki service unit called someotherwiki and all service units of someotherwiki would share a different export. The new export is created on the service when the first unit of a new service name joins… subsequent units of that named service are just connected to that new export.

Of course, this works for entirely separate services too. Relating a hadoop-master service named job27 to that same nfs service would result in the job27 units sharing a new nfs export.

This behavior is a reasonable default, but there may be a need to do this a little differently in your infrastructure. You could change the nfs server charm to provide different levels of export sharing… either by some additional config.yaml entries or by extending the mount interface. An nfs client might request a unique export, for backups say, or a named export for sharing.

Disclaimer

I work on the Ubuntu juju project for Canonical.

PreSonus FireBox in Ubuntu

2011-08-17 (howto, audio)

Just some notes to myself for later.

I want a little better sounding audio on screencasts so I dusted off the firewire soundcard I got a few years ago for recording bass tracks.

This model FireBox 24-bit/96kHz from PreSonus worked great for me on audio production stuff several years ago, but at the time I could give jackd and friends a realtime kernel to have their way with.

Now, I don’t really have the spare hardware to dedicate to a RT audio setup…
gotta run several ubuntu server VMs for work and can’t really hand the whole shebang over to jack every time I wanna record something.

Here’s my attempt to do it without realtime priorities… I’ll track my progress here.

The quick and the dirty…

Starting from a Natty desktop.

install jack

# apt-get install jackd

when the installer asks to do realtime by default, I said no.

Note that I installed ffado-mixer-qt4 ffado-tools ffado-dbus-server earlier trying to get this to work with pulse, without jack… but gave up. I don’t know if these packages effect the current setup, but they’re still installed

start the jack daemon…

$ jackd -r -dfirewire

Connect to jackd

$ qjackctl 

(in the foreground so I could watch messages)

Install something like Ardour

# apt-get install ardour

and wire stuff together with the qjackctl patchpanel, start jackd and go.

I used to use a better jack patch panel in the past… have to find it.

resources

http://wiki.jon.geek.nz/index.php/Presonus_Firebox
http://ubuntuforums.org/showthread.php?t=835477
http://rgrwkmn.hubpages.com/hub/Recording-in-Linux-aka-Free-and-Open-Source-Digital-Audio-Workstation

git-create new github repos

2011-07-27 (howto, github, api)

Ok, I’m too lazy to go through the web interface every time I want to add a new github repo. github api to the rescue…

The goal

I want

git create myrepo "my repo description"

to create a new github repo ‘myrepo’.

manually

Grab your API Token from your github account, “Account Settings” – “Account Admin”.

Use curl

curl -u "mmm/token:XXXXXXXX" https://github.com/api/v2/json/user/show/mmm | jsonpretty

curl -X POST -u "mmm/token:XXXXXXXX" https://github.com/api/v2/json/repos/create -d "name=junk2&description=morejunk2"

or the same thing using restclient (sudo gem install rest-client):

restclient get https://github.com/api/v2/json/user/show/mmm mmm token:XXXXXXXX | jsonpretty 

echo '{"name":"junk3","description":"junk3 description"}' | restclient post https://github.com/api/v2/json/repos/create mmm token:XXXXXXXX

Github docs say you should send at least name but it will take any of these as POST args:

  • name => name of the repository. ex: “my-repo” or “other-user/my-repo”
  • description => repo description
  • homepage => homepage url
  • public => 1 for public, 0 for private

monkeypatch git

I’ll use a script…

#!/usr/bin/ruby

require 'rubygems'
require 'json'
require 'rest-client'

USER = "mmm"
TOKEN = "XXXXXXXX"
URL = "https://github.com/api/v2/json/repos/create"

def usage 
  puts <<-EOS
    usage: #{$0} <name> [<description>] [<homepage>] [<public>]
    where:
      name => name of the repository
      description => repo description
      homepage => homepage url
      public => 1 for public, 0 for private
  EOS
  exit 1
end
ARGV.size > 0 || usage

params = {
  "login" => USER,
  "token" => TOKEN,
  "name" => ARGV[0],
  "description" => ARGV[1] || "",
  "homepage" => ARGV[2] || "",
  "public" => ARGV[3] || "1",
}

puts "creating repo: #{params.to_json}"

response = RestClient.post URL, params

puts response.body

I’ll call it git-create and put it in my path at ~/bin. Notice that git even picks it up as a builtin so I can

git create myrepo "my repo description"

and it just works…