How to set up wordpress on ec2 using puppet and git

Daily Standup
Creative Commons License Greg Walters
Having started out on a Joyent appliance, migrating to Linode, and, finally, to Amazon with a Bitnami stack, we noticed the common pain of manually configuring each of these environments. Bitnami caused us an even bigger headache by being very difficult to update (apt-get doesn’t update the bitnami wrapped AMP stack). We decided to get full control of our box by setting up a stock Debian LAMP stack on AWS using Puppet and git to manage our sites. Here’s a gentle introduction on how we did it.

AWS and unfuddle

Setup a t1.micro instance with aws.amazon.com for your blog. Remember, if you don’t have an AWS account yet, this micro instance is free for 12 months! We get ~20KPIs per month so the micro instance is just enough to cover this (with significant tuning).

Sign up with unfuddle.com for 200MB worth of free, private git repositories.

Install & configure puppet standalone

$ apt-get install puppetmaster puppet
$ vi /etc/hosts
127.0.0.1 master.successfulengineering.com
...
$ vi /etc/puppet/puppet.conf
...
[agent]
server = master.successfulengineering.com

Setup your git repo

unfuddle has a nice help document for this, but the gist is basically to add the base puppet installation directory on your instance to unfuddle’s git repo:

$ cd /etc/puppet
$ git remote add unfuddle git@successfulengineering.unfuddle.com:successfulengineering/puppetmaster.git
$ git config remote.unfuddle.push refs/heads/master:refs/heads/master
$ git add *
$ git commit -am "initial commit"
$ git push unfuddle master

Now, you can clone this to your local dev env and start creating manifests! After you push your local changes to git, do a git pull on the remote instance. To apply manifest updates while building up your site, use the --test option (otherwise puppet will start in daemon mode).

$ puppet agent --test

Congratulations! Now you’ve got a running puppet standalone environment backed with a git repository. Time for the real work.

Base environment (L)

Here’s what our nodes.pp file looks like:

node default {
  include setenv
  include ntp
  include users
  
  include mysql
  include apache
  include php
  
  include blogs
}

The setenv module gets my default packages installed (htop, unzip, wget, git-core, vim, fail2ban), sets up vi as the default editor, and installs a default locale at /etc/default/locale as:

LANG="en_US.UTF-8"
LC_ALL="en_US.UTF8"

NTP should be obvious, and the users module ensures Matthias and my users’ exist with the appropriate SSH keys and admin group roles.

Base applications (AMP)

Now that our base Linux environment is setup, let’s get to the ‘AMP’ section.
We setup our base MySQL server with the following manifest:

class mysql {
  package { "mysql-server":
    ensure => present,
  }
  service { "mysql":
    ensure => running,
    enable => true,
    hasstatus => true,
    require => Package["mysql-server"], 
  }
  file { "/etc/mysql/my.cnf":
    ensure => present,
    content => template("mysql/my.cnf.erb"),
    notify => Service["mysql"],
    require => Package["mysql-server"],
  }
  exec { "set mysql root password":
    path => "/usr/bin",
    unless => "mysql -uroot -p${root_mysql_password}",
    command => "mysqladmin -u root password ${root_mysql_password}",
    require => Service['mysql'],
  }
}

The $root_mysql_password variable is declared in our site.pp file.

Apache is a bit more complex:

class apache {
  include apache::install
  include apache::conf
  include apache::sites
  include apache::mods
}

For sites and mods, I define a custom method to allow us to easily enable/disable new sites and required modules. Here’s a snippet from the apache::mods class:

  define mods_stats ( $ensure = 'present') {
    case $ensure {
      'present' : {
        exec { "/usr/sbin/a2enmod $name":
          unless => "/bin/sh -c '[ -L /etc/apache2/mods-enabled/$name.load ] \
            && [ /etc/apache2/mods-enabled/${name}.load -ef /etc/apache2/mods-available/${name}.load ]'",
            notify => Exec["force-reload-apache"],
 	    require => Package["apache"],
        }
      }
      'absent': {
        exec { "/usr/sbin/a2dismod $name":
          onlyif => "/bin/sh -c '[ -L /etc/apache2/mods-enabled/${name}.load ] \
            && [ /etc/apache2/mods-enabled/${name}.load -ef /etc/apache2/mods-available/${name}.load ]'",
            notify => Exec["force-reload-apache"],
            require => Package["apache"],
        }
      }
      default: { err ( "Unknown ensure value: '$ensure'" ) }
    }
  }
  apache::mods::mods_stats { 
    "rewrite" : ensure => present, 
    notify => Service["apache"], 
  }
  apache::mods::mods_stats { 
    "deflate" : ensure => present, 
    notify => Service["apache"], 
  }

Although the definition of mods_stats may seem a bit lengthy, it pays itself back pretty quickly when you can easily enable/disable any mods throughout your manifests.

The PHP module is more of the same, using puppet templates to control the memory allocation for apc.ini and php.ini. Email me for more details.

WordPress sites

So we have our default LAMP stack setup and initialized with our passwords, modules, and memory constraints. Let’s get down to business – namely, the blogs module.

class blogs {
  include blogs::agileweboperations
  include blogs::successfulengineering
}

Let’s have a look at this blog’s manifest:

class blogs::agileweboperations {
  # Apache website
  file { "/etc/apache2/sites-available/agileweboperations.com":
      ensure => present,
      owner   => root,
      group   => root,
      mode    => 644,
      source => "puppet:///modules/blogs/etc/apache2/sites-available/agileweboperations.com.conf",
      require => Package["apache"],
      notify => Service["apache"],
  }
  apache::sites::site { "agileweboperations.com" : ensure => present }  

  exec { "setup awo db":
    path => "/usr/bin",
    unless => "mysql -uroot -p${root_mysql_password} ${awo_mysql_db}",
    command => "mysql -uroot -p${root_mysql_password} -e "
CREATE DATABASE ${awo_mysql_db} DEFAULT CHARACTER SET UTF8;"",
    require => Exec["set mysql root password"],
  }  
  
  exec { "setup awo user":
    path => "/usr/bin",
    unless => "mysql -u${awo_mysql_user} -p${awo_mysql_pass}",
    command => "mysql -uroot -p${root_mysql_password} -e "
CREATE USER '${awo_mysql_user}'@'localhost' IDENTIFIED BY '${awo_mysql_pass}'; 
GRANT ALL PRIVILEGES ON ${awo_mysql_db}.* TO '${awo_mysql_user}'@'localhost' WITH GRANT OPTION;"",
    require => Exec["setup awo db"],
  }
}

This puts all the pieces of the puzzle together: our custom apache configuration, enabling the site with an easy one-liner, and bootstrapping the WordPress database and user (only if they don’t exist).

We don’t do a WordPress install per-se. We’ve been running this site for almost four years, and we have it in a git repository (conveniently hosted next to the puppet repo with unfuddle). All the migrations we’ve performed in the past have shown us the “famous 5-minute installation” can’t beat the ease of a git clone.

Crafting and finding the above manifests on the Internet took me about 12 hours total work effort, but I have a few years of puppet experience under my belt as well. Hopefully, this guide gives you some ideas and pointers about how to get control of your WordPress environment with puppet. I’ve published an “anonymized” version of the entire configuration over at github – hope this helps you get a jump start on your server setup. Let me know if you have any questions!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.