Deploying PHP applications: PEAR and composer resources for chef

If you enjoyed this article, please leave a comment, rss subscribe to my RSS feed and/or follow me on Twitter. Thank you very much!

This is something experimental I have been working on for our chef deployments. So the objective was/is to find a sane way to install PEAR packages and install dependencies with composer.

execute in chef recipes

In chef recipes, almost everything is a resource. In case you're just getting started with Chef, a list of current resources is available on the Opscode Wiki. It's a link I put in my browser bar since I frequently work on chef recipes.

Some examples for resources are:

  • package (to install software)
  • cron (setup a crontab)
  • directory (create directories)
  • template (install customized configuration files)
  • user and group (to create users and groups)
  • mdadm (to setup a RAID)

The above list are examples — so there is more. But if there isn't a designated resource, you can always use an execute block.

An example for an execute block is the following:

execute "discover a pear channel" do
  command "pear channel-discover easybib.github.com/pear"
end

This works pretty well, but it is also not very robust.

Fail hard

By default whenever a command fails, chef fails hard.

To illustrate what I'm talking about, let's test and execute the command from our execute block multiple times on the shell to see its exit status ($?):

till:~/ $ pear channel-discover easybib.github.com/pear
Adding Channel "easybib.github.com/pear" succeeded
Discovery of channel "easybib.github.com/pear" succeeded
till:~/ $ echo $?
0
till:~/ $ pear channel-discover easybib.github.com/pear
Channel "easybib.github.com/pear" is already initialized
till:~/ $ echo $?
1

So whenever a command returns not 0, chef will bail.

One solution is to brute-force your way through these things with ignore_failure true in your execute block. But that's usually not a great idea either because it hides other issues from you (and me) when we need to debug this later on.

For example, if this PEAR channel is unavailable during your next chef-run, it would be much, much harder to find the root cause as of why the install commands failed.

Another solution is using the not_if or only_if options with execute:

execute "discover a pear channel" do
  command "pear channel-discover easybib.github.com/pear"
  not_if do
    `pear channel-info easybib.github.com/pear`
  end
end

If the command wrapped in not_if succeeds (success is exit status), we would skip the execute block.

Optimize?

Since I discovered not_if and only_if, it allows me write recipes which work in most cases. More robust code, which allows me to re-execute recipes on already running instances. So for example when I update a recipe or configuration file which is distributed through a recipe I can re-run the entire recipe and it will not fail but instead complete successfully.

One problem remains with this approach I end up doing the same checks again and again.

Custom resources

Chef is written in ruby and it's opensource. This means I can extend chef not only by means of recipes but I can create my own resources as well. The advantage of writing resources is that we can wrap the tedious code to run checks into a nice DSL and re-use this whenever we have to.

Chef offers a thing called LWRP — Lightweight Resources and Providers.

LWRP

… are a DSL to write customized resources and providers.

To explain the difference — a resource is sometimes (or always? :)) powered by a provider.

A resources in general defines available actions and attributes and their defaults. (See examples below!)

As far as providers are concerned, they are mostly an abstraction on various backends. So for example the package resource has different providers available.

For example:

  • apt (Ubuntu/Debian)
  • ports/pkg (FreeBSD)
  • rubygem
  • portage

etc.. Each provider implements the specifics for the package manager on its designated distribution.

If I run my recipe on FreeBSD and put package "lang/php5" in it, Chef is smart enough to use the ports provider to install php5 from /usr/ports/lang/php5.

If I run my recipe on Ubuntu and put package "php5" in it, Chef will install the package "php5" with apt-get install php5.

Small caveat: in case you're writing recipes which have to run on different operating systems, you need to figure out what a package is called on the designated target. I think most (if not all) distributions of Linux and Unix have different names for the same package. The only abstraction in this case is using package.

This part is a little confusing — if you're just getting started, I'd recommend targetting one distribution and not all.

pear resource

Getting back to the initial subject of this blog entry, I created a resource to install packages from PEAR channels.

Here is an example:

# this is a test
php_pear "Rediska" do
  action :install_if_missing
  channel "easybib.github.com/pear"
  version "0.5.6"
end

php_pear "Rediska" do
  action :uninstall
end

The above piece of code could be put in your recipe and would install the Rediska library (which I mirrored on our PEAR Channel).

The second statement uninstalls the library again. This kind of behavior could be nice for undeploy actions with Chef.

Background

My php_pear resource is called with the following attributes:

  • action: :installed_if_missing, which will check the local PEAR registry if the package is already installed. If it is, we saved a roundtrip to a remote server, which means a faster deploy.
  • channel: This is the channel the library is located on. If the channel hasn't been initialized it will discover the channel for you.
  • version: The version of the library we want to install. Pretty handy with bleeding edge code since I don't want to sort out potential BC breaks during deployment.

The second piece is self-explainatory.

Available actions

The available actions are:

  • install
  • installifmissing
  • uninstall
  • upgrade

Available options are:

  • package (by default this is what you put in php_pear "foo")
  • channel, default: pear.php.net
  • version, default: null
  • force, default: false (execute pear with -f)

composer resource

The second resource (and provider) I created are to install dependencies with composer.

The resource works a little different which is why I decided not to create a php resource and abstract between PEAR and composer using a provider.

Here is an example for composer:

php_composer "/var/www/app/current" do
  action :install
end

This statement tells the php_composer to install dependencies of our application code in /var/www/app/current.

Background

Whenever this runs during a deployment, the following will happen:

  • go into the directory
  • execute php composer.phar --quiet --no-interaction install

In detail:

  • check if the directory exists
  • check if a composer.phar exists

The only action available is (currently) install.

Things I need to work on are:

  • check if a composer.json file exists
  • (maybe) check if PHP has the Phar extension loaded
  • figure out how to debug composer runs (if the fail)

Code

The code is BSD licensed and both resources are developed in our cookbooks on Github: https://github.com/till/easybib-cookbooks/tree/master/php/

FIN

If they turn out to be more than worthwhile, I'll see if I can contribute them to Opscode.

In the mean time, if you have questions, feel free to leave a comment or if you'd like to contribute, please fork. :)

| More