Engine Yard Blog RSS Feed

I've been experimenting with a variety of cloud Platform as a Service (PaaS) offerings lately, and naturally was interested in seeing what Engine Yard brought to the table for PHP developers with its Engine Yard Cloud offering.

Deployment of my application includes several non-trivial aspects, and I wanted to see how easy, or difficult, it was to accomplish each. These include:

  • Deployment from a Git repository.
  • Usage of Git submodule in my project repository.
  • Usage of Composer for primary dependency management of third-party code.
  • Several private configuration files containing things like API keys; these are currently stored in a private GitHub repository.
  • Several "pre-run" tasks for gathering information via web services.
  • Several cronjobs.
  • DNS for resolving the website when ready.

So, how does Engine Yard stack up? Can it accomplish all these tasks?

The answer to the latter question is "yes," and the former is "quite well."

Initial deployment: Git and Composer

When first creating an application on Engine Yard, you are asked to provide the URL to an existing Git repository. Engine Yard deploys your application by cloning this repository. You can optionally specify a branch, tag, or specific revision for Engine Yard to use.

Furthermore, it clones the repositoryrecursivelyby default (i.e., git clone --recursive), which means if you have any submodules, these will automatically be cloned as well! This was a feature I'd not seen in other offerings I'd tried (in other PaaS solutions, I've often had to initialize submodules manually as part of my deployment), and made the initial setup a breeze.

For PHP projects, if Engine Yard detects a composer.lock file in the project root, it will immediately run composer install, installing your Composer-managed dependencies. This greatly simplifies deployment as you do not need to install the dependencies, package them, and push them to your environment -- or, alternately, create a deployment task to install them. Engine Yard takes care of the heavy lifting for you.

Note: At this time, I have a query in to Engine Yard to determine if Composer is run using the --no-dev flag, or if it is run in the default "development" mode. As such, you should be wary about what is in your composer.lock file, and only check in versions run with the --no-dev flag.

Private Configuration

Next on my agenda was determining how to get my private configuration files to the environment.

In Zend Framework 2, we merge configuration using a glob path, allowing us to have "global" configuration that's available on all environments, and "local" configuration, which is never checked into version control, and which may contain sensitive data such as API keys, database credentials, etc. The runtime configuration is the merged product of global and local configuration, with local having precedence over global.

I've traditionally deployed in roughly this fashion:

  • Create a private git repository for the sensitive local settings.
  • During deployment:
  • clone the private repository in a temp directory.
  • copy the configuration files into my app.

The problem with this approach, of course, is that you need to setup SSH keys on your server, and add them to your keyring on GitHub or other Git hosting platform. On cloud platforms, this can be problematic, as the server is often different from launch to launch, leading SSH to falsely identify potential man-in-the-middle (MitM) attacks, often locking the server out of the repository.

Additionally, a new question is raised: how do you get the SSH key onto the server in the first place?

Engine Yard uses Chef to manage and configure application containers. When you create a new application, it uses a default configuration. However, you can check out that configuration locally, add or modify recipes, and then push the new configuration!

If we can do that, we don't even need to bother with SSH keys; we can simply push our files into the container so that they are there waiting for us when the application is deployed.

To get started, you need to first install the Engine Yard ruby gem:

    $ gem install engineyard

For PHP, the base recipes are available here:

https://github.com/engineyard/ey-cloud-recipes-chef-10.git

Clone that repository locally (git clone https://github.com/engineyard/ey-cloud-recipes-chef-10.git), and then you can start modifying it. For my ZF2-based application, I created the directory cookbooks/php/files/default/, and copied my various *local.php files into it. I then created a Chef recipe in cookbooks/php/recipes/production_config.rb that copies those files into the shared/appconfig directory of my container, and changes them to be world-readable:

    ey_cloud_report "production_config" do
       message "Production config pushed"
    end

    directory "/data/myappname/shared/appconfig" do
        owner node[:owner_name]
        group node[:owner_name]
        mode 0755
        action :create
    end

    [
        'database.local.php',
        'local.php',
        'module.application.local.php',
        'module.blog.local.php',
        'module.github-feed.local.php',
        'module.phly-contact.local.php',
        'scn-social-auth.local.php'
    ].each do |config|
        cookbook_file "/data/myappname/shared/appconfig/#{config}" do
            owner node[:owner_name]
            group node[:owner_name]
            mode 0644
            source "#{config}"
            backup false
            action :create
        end
    end

Note: Chef recipes are written in Ruby, which is not a language with which I have much experience. If there's an easier way to maintain the files, please let me know!

Now that the recipe is created, I have to tell Chef to use it. I do this by editing the cookbooks/main/recipes/default.rb file to add the following line:

    include_recipe "php::production_config"

From the root of the Chef recipes repository you've cloned, you deploy the recipes using the Engine Yard gem:

    $ ey recipes upload -e ENV

where ENV is your the target environment for your application; in my Chef recipe example above, it would correspond to "production". Whenever you make changes to any of these files, you will re-issue that command in order to deploy the changes to your container.

If you look in your applications screen on the web interface, you should now see a message for the container indicating "custom recipes uploaded."

chef-uploaded

To run the recipes, you can either click "Apply" in the environment screen, or using the engineyard gem:

    $ ey recipes apply -e ENV

Deployment Tasks

You may have noticed that I put the configuration files in a shared directory, and not the application. This is because at the time of container creation, the application may not yet be deployed.

So, how will the application know about those files?

The answer is to add some deployment scripts.

Engine Yard supports a variety of deployment hooks, and will fire off specific scripts in the deploy/ directory of your project for each hook, if they exist. All hook scripts are written in Ruby. To simplify common tasks, several syntax additions exist:

  • The commands run and sudo will allow you to execute arbitrary shell commands. Using an exclamation point ("!") after either will cause them to abort deployment if an error occurs; e.g., run! someCommand, sudo! someCommand.
  • The above commands allow you to inject a number of environment variables, so that you do not need to know specific details of the filesystem layout. In particular, the #{shared_path} and #{release_path} variables are useful.
  • The PHP executable is available for use, at the path /usr/bin/php.

Combined, this means I can create the file deploy/before_symlink.rb in my project with the following contents in order to copy over my local configuration files:

    run "cp #{shared_path}/appconfig/*.php #{release_path}/config/autoload/"

If I wanted to abort execution if the above command failed, I'd write it as follows:

    run! "cp #{shared_path}/appconfig/*.php #{release_path}/config/autoload/"

At this point, I now have my production settings in place, and my application is ready to go!

Except I have a few more tasks!

I have a few tasks I like to run when I deploy, and these are written using ZF2's console tooling. So, I added a few more lines to the above script:

    run "cd #{release_path} & /usr/bin/php public/index.php githubfeed fetch"
    run "cd #{release_path} & /usr/bin/php public/index.php phlysimplepage cache clear all"

Each of the above descends into the release path, and then executes a ZF2 console command using the PHP binary.

Things are looking good!

Note: Not all available hooks are available on all platforms Engine Yard supports. For PHP applications, the following hooks willnottrigger: before_compile_assets.rb, after_compile_assets.rb, before_migrate.rb, and after_migrate.rb. Otherwise, all other hooks are available. I have personally found the before_symlink.rb hook to be sufficient for all of my deployment tasks.

Cronjobs

I have a few tasks that run on a periodic basis. For instance, the site needs to update some GitHub stats hourly. These are perfect for cronjobs.

Engine Yard provides a web interface for configuring cronjobs. This interface basically mimics crontab entries, allowing you to specify the various intervals, and then a task to run.

cronjob

The one potential issue is if you want to run a task inside the application itself. If you recall from the previous section, during deployment, we have access to environment variables that resolve to the locations. But how do we do this for cronjobs?

Engine Yard decided to use a symlink when deploying applications. This is a smart practice, as it makes rolling back trivial -- you simply re-point the symlink to a previously known-good deployment. A side benefit is that it means that the current application is also always available at a known location: /data/<your app name>/current.

So, as an example, one of my cronjobs executes the following:

    $ cd /data/myappname/current &amp; /usr/bin/php public/index.php githubfeed fetch

In all, I had my cronjobs going within minutes.

DNS

Now that I had an application up and running, how do I get to it?

Engine Yard uses Amazon AWS Elastic IP addresses, and associates an Elastic IP with each application. These addresses are associated with your AWS account, and not a particular machine - which means that if your Engine Yard container moves to a different machine, AWS will still be able to find it.

Chances are, when you create your container, you will either already have one setup, or Engine Yard will prompt you to register with AWS in order to create and associate one with your account.

On some other PaaS platforms, you cannot have what are called "naked domains"; you must use a subdomain. In fact, services such as wwwizer exist to solve this very problem, redirecting the naked domain to a "www" subdomain managed by your PaaS platform. This of course presents new problems, in that you are now introducing a redirect into each and every call to the naked domain.

What's interesting about using Elastic IP is that you can in fact use a naked domain with your cloud application, as you have a specific IP address you can utilize with a DNS A record.

Conclusions

Engine Yard provides a highly flexible PaaS solution that nails several common deployment issues very capably. The default deployment using Git and Composer is magical when you first encounter it, as for most applications, it resolves a majority of deployment-related tasks for you. The usage of Chef means that more complex or sensitive portions of deployment can be managed, using a tool that has a wide amount of documentation and knowledge available for you. Deployment hook scripts allow you to further tailor your deployment, ensuring your application can be properly configured, seeded, cached, or otherwise prior to going live. Having cronjobs available by default gives asynchronous capabilities from the outset. Finally, usage of Elastic IP means you can keep your naked domain, and host it in the cloud.

All in all, I was quite pleased with my experiments, and hope that my experience can aid other PHP developers as they test cloud platforms for their application deployment.

Acknowledgements

Davey Shafik fielded my early questions about deployment, though, ultimately, he was unable to answer them -- and then prompted me to write this post once I figured it all out! Kevin Holler was able to dig up the information on the hook scripts available for deployment, and, more importantly, which ones PHP applications could hook into. Finally, Engine Yard support pointed me to Chef as a solution for my private files, as well as the default PHP recipes Engine Yard uses.


Tagged:

comments powered by Disqus