Software, Physics, Data, Mountains

...and other random associations

Core Infrastructure Part 1 - Adding NFS to Juju Charms

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.

Comments

If you have any questions or feedback, please feel free to share it with me on Twitter: @m_3