Deploying PHP applications: PEAR and composer resources for chef
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
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.
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
only_if options with
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.
Since I discovered
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.
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.
… 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.
- apt (Ubuntu/Debian)
- ports/pkg (FreeBSD)
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
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
This part is a little confusing — if you're just getting started, I'd recommend targetting one distribution and not all.
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.
php_pear resource is called with the following attributes:
: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.
The available actions are:
Available options are:
- package (by default this is what you put in
- channel, default: pear.php.net
- version, default: null
- force, default: false (execute pear with
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
Whenever this runs during a deployment, the following will happen:
- go into the directory
php composer.phar --quiet --no-interaction install
- check if the directory exists
- check if a
The only action available is (currently)
Things I need to work on are:
- check if a
- (maybe) check if PHP has the Phar extension loaded
- figure out how to debug composer runs (if the fail)
The code is BSD licensed and both resources are developed in our cookbooks on Github: https://github.com/till/easybib-cookbooks/tree/master/php/
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. :)
The author does not allow comments to this entry