Monday, January 19, 2015

Vagrant for Local Development

When I was hired at my current company about a year and a half ago, it was immediately obvious that this company took dev-ops much more seriously than any other place I had worked before. In fact, a lot of the dev-ops culture is pushed by the ops team, which is a dramatic, and very welcome, departure from my previous experience. This team follows a lot of the principles espoused in the Continuous Delivery book. In this post, I'm going to focus on the principle of making all pre-prod environments as production-like as possible, and in particular, what has been done in the local development environment.

The Ops team uses puppet for server provisioning in all environments. This facilitates the separation of the vast majority of configuration that is the same across environments and servers within an environment from the relatively small proportion that that needs to be different.

The Problem

Our project currently comprises 7 microservices and one CLI app, all based on Spring Boot. Most of the services are RESTful; a couple are message driven. They all expose a management port, primarily for doing health checks and gathering metrics. Deployable artifacts are built by Jenkins and stored in Nexus. Deployment is handled by a standardized shell script which is placed on the target servers by Puppet during provisioning.

We also use several 3rd-party applications, including MySQL, Mongo, Rabbit MQ, Splunk, and a CLI ETL application. These are also provisioned by Puppet.

The provisioning and deployment infrastructure is almost identical in production and the non-local pre-prod environments. However, until several months ago, provisioning and deploying in the local development environment (i.e. developers' laptops) was still a manual affair. This was achieved by following meticulously crafted documentation about how to set up a new workstation. Invariably, differences crept in, e.g. different versions of 3rd party apps, different configuration, even different package managers. Mostly these differences were benign, but sometimes they did cause problems. And that type of problem is much more difficult to diagnose and fix than a bug in the application layer.

Enter Vagrant

Vagrant enables you to "Create and configure lightweight, reproducible, and portable development environments." Vagrant VM images are called "boxes". You can produce your own box, or find one at https://atlas.hashicorp.com/boxes/search. Once you find one (e.g. hashicorp/precise32), getting it up and running is simple:
vagrant init hashicorp/precise32
vagrant up

Then you can ssh to it with:
vagrant ssh

And to stop it with various levels of severity:
vagrant suspend/halt/destroy

When you run init, vagrant creates a basic Vagrantfile in your current directory which has a little bit of configuration in it, and a lot of commented out stuff so you can see how to do common things.

How we use Vagrant

Our gradle build keeps updated a vagrant.config file which contains a simple ruby structure with information about our deployable services and cli apps:
PROJECTS = {
        'core-foo' => {
                :version           => '3.211',
                :project_root      => '/Users/ryan.mckay/projects/shared-services/core-foo',
                :use_puppet_config => true,
                :use_puppet_deploy => true,
                :im_just_a_jar => false,
        },
...

Our Vagrantfile

Load configuration about our deployables
load 'vagrant.config'

Configure some vm settings like memory and number of cpus
config.vm.provider :virtualbox do |vb|
    # Use VBoxManage to customize the VM. For example to change memory:
    vb.customize ["modifyvm", :id, "--memory", "5120"]
    vb.customize ["modifyvm", :id, "--cpus", "4"]
end 

Run external provisioning script
config.vm.provision :shell, :path => "../scripts/05-vagrant.sh"

Deployment (calls deployment script landed by puppet for each deployable)
 PROJECTS.each { |artifact_name, artifact_config|
    args = [
      artifact_name,
      artifact_config[:version],
      artifact_config[:im_just_a_jar] ? 'jar' : 'service'
    ]
    config.vm.provision :shell, :path => "../scripts/10-deploy.sh", :args => args
  }

Make sure everything came up
config.vm.provision :shell, :path => "../scripts/99-runtests.sh"

We use a private custom base image
  # The url from where the 'config.vm.box' box will be fetched if it
  # doesn't already exist on the user's system.
   config.vm.box_url = "http://foo.com/images/debian7-base.box"

Forward application ports to host system with 10,000 offset
   # Application ports
  (8000..9999).step(10).each do |port|
    config.vm.network :forwarded_port, :host => 10000 + port, :guest => port
    config.vm.network :forwarded_port, :host => 10001 + port, :guest => 1 + port
  end

3rd party service port forwarding
  # default intellij debugging port
  config.vm.network :forwarded_port, :host => 5005, :guest => 5005

  # mysql port
  config.vm.network :forwarded_port, :host => 3306, :guest => 3306

  # mongodb port
  config.vm.network :forwarded_port, :host => 27017, :guest => 27017

  #rabbitmq 
  config.vm.network :forwarded_port, :host => 5672, :guest => 5672
  config.vm.network :forwarded_port, :host => 15672, :guest => 15672

  #splunk mangagement port
  config.vm.network :forwarded_port, :host => 18089, :guest => 8089

Host/VM synced folder
  # create sync folder for integration test data
  local_sync_folder = "/tmp/foo_integration"
  FileUtils.mkdir_p local_sync_folder
  File.chmod(0775, local_sync_folder)
  config.vm.synced_folder local_sync_folder, "/mnt/filer/foo/foodev"

Deploy locally built artifacts if available
  PROJECTS.each { |service_name, service_config|
    if (service_config.key?(:project_root) && service_config[:use_puppet_config] != true)
        host_config_dir = service_config[:project_root] + "/src/config"
        if (File.directory?(host_config_dir))
          config.vm.synced_folder host_config_dir, "/var/bv/conf/#{service_name}/local"
          config.vm.provision :shell, :inline => "ln -sf /var/bv/conf/#{service_name}/local/dev-local.properties /var/bv/conf/#{service_name}/properties"
          config.vm.provision :shell, :inline => "ln -sf /var/bv/conf/#{service_name}/local/dev-migrate.properties /var/bv/conf/#{service_name}/migrate.properties"
        else
          puts "Project root found for #{service_name}, but src/config not detected. Local config directory was not mounted"
        end
    end
    if (service_config.key?(:project_root) && service_config[:use_puppet_deploy] != true)
      guest_folder = "/var/bv/apps/#{service_name}/local"
      local_artifact = localArtifactNameFor(service_name)
      host_lib_dir = service_config[:project_root] + "/build/libs"
      if (File.directory?(host_lib_dir))
        config.vm.synced_folder host_lib_dir, guest_folder
        command_to_run = "ln -sf #{guest_folder}/#{local_artifact} /var/bv/apps/#{service_name}/current.jar"
        if (!service_config[:im_just_a_jar])
           command_to_run += " && /etc/init.d/#{service_name} stop && /etc/init.d/#{service_name} start"
        end
        config.vm.provision :shell, :inline => command_to_run
      else
        puts "Project root found for #{service_name}, but build/libs not detected. Local build directory was not mounted."
      end
    end
  }

end

def localArtifactNameFor(service_name)
  version = PROJECTS[service_name][:version]
  major_version = version.split('.')[0]
  return "#{service_name}-#{major_version}.99999.jar"
end

Conclusion

Now that we have started using Vagrant, spinning up a new developer workstation takes a matter of minutes, and you know you got it exactly right. We can run integration and manual tests, and be confident in the result. And we are regularly exercising a good portion of the provisioning, deployment, and monitoring infrastructure that is used in production.

Friday, June 20, 2014

Pair Programming Benefits

For the last year and a bit, I've been pair programming about 90% of my dev time (i.e. outside of meetings).  I've seen several major benefits compared to the previous 15 years of solo programming.  But I've seen some issues as well.  I think they are addressable, but they are issues to stay aware of.  I'm going to use this post to talk about the benefits, then follow up with another to discuss the issues I have identified.

For the most part, I've been practicing what I'm going to refer to as Long Form Pairing.  Other than meetings and lunch, pair all day long.  Not a lot of attention paid to taking breaks or role switching.  However, we have dabbled in Ping-Pong PairingApplying the Pomodoro Technique to Pairing, and even Ping-Pong Pomodoro Pair-Programming, or PPPPP.  In fact, at our most recent hackathon, one of my team mates wrote an IntelliJ port of the Pair Hero PPPPP Eclipse plugin.

One person is doing the typing and mousing (sometimes referred to as the driver), while the other is watching and providing verbal input (the navigator).  We are typically working on a user story that has been pretty well broken down into tasks, so when code is being written, there is not a lot of discussion required about what needs to be done.  The conversation at that point is about how things should be done - things like how to factor functionality into classes and methods, which test cases we need, what to name things, third party libraries, etc.  

Increased Focus

One of the biggest benefits that I have experienced from pair programming is increased focus on the task at hand.  When you are pairing, you don't have time to check email, do research, go off on tangents in the code, etc.  You are hyper-focused on getting the current task done and moving on to the next one.  Emails come in - I ignore them.  Conversations happen around me - I don't even hear them.  I'm definitely not taking this opportunity to take care of that big refactoring that's been nagging at the back of my mind.

Faster Context (Re)loading

Some interruptions are unavoidable, for example, meetings, lunch, nights and weekends :)  Sometimes the other person has the same interruption; sometimes they don't.  Either way, it is much easier and faster to reload a context shared with another person than one you had by yourself.  Similarly, if you are just coming into a context for the first time, someone who already has it loaded can spin you up much faster than you can on your own.

Teaching/Learning Opportunity

My current team is composed of all senior engineers (for better or worse), so there is not the same teaching opportunity I have had with junior developers in the past.  However, software engineering is a large field, and we all have varying experience in both quality and quantity, so there is still plenty of opportunity for learning from each other.  

We have identified several major technical areas of interest in our project, too many for any individual to specialize in all of them at the same time.  Some examples are Angular JS (and associated ecosystem), MongoDB, Spring (boot, data, IOC, etc), REST, and Testing.  Team members have selected their top three areas to focus on for this release.  Next release, we will shuffle them up, and encourage pairing between the SMEs and members who are new to the area.

Alternative Paths

Having another pair of eyes helps you see alternatives you wouldn't otherwise see.  Period.  Some of these can save you time right now, for example:
  • Remembering where some particular configuration properties are
  • Informing you of a library that does what you were about to implement
  • Explaining how the hell that variable is getting a null value
  • Containing scope creep
  • Suggesting an easier way to do something, so you can get it done now instead of pulling it out of the story for tech debt
Others can save you a lot of time later, for example:
  • Identifying missing test cases
  • Suggesting a more maintainable design
This is the area where I see the strongest relation with the driver/navigator analogy.  The driver has a very tactical view of the problem.  When the pairs' skill levels are pretty evenly matched, if both partners are going full speed, the driver simply cannot consider the same breadth and depth of strategic concerns as the navigator can.  In fact, sometimes the driver will have to stop typing in order to catch up mentally.  

Even in the highly unlikely case that neither of the partners learns a thing, they are producing better code, faster, right now.  Happily, both partners will be learning and getting better.

Adherence to Team Norms

Over the past year, several team norms have emerged.  Some of them are explicit, and recorded in Confluence.  For example, we have discussed and decided on several policies about how we test our software - what should be tested at various levels, how to set up tests, wording of test names, etc.  Others are more implicit, like patterns in the codebase for how we address certain design elements.  Individuals tend to deviate from these standards from time to time for various reasons.  Sometimes you simply forget or were not aware.  Other times, you're just feeling a bit lackadaisical.  And yes, sometimes its a standard you didn't agree with in the first place, and you don't feel like following it right now.  

As an analogy, I've been doing a lot of swimming lately, trying to improve my technique.  For argument's sake, let's just say I know exactly what I need to do to have a really efficient body position and stroke.  But when I'm in the water actually doing it, trying to focus on all the different elements, and get enough oxygen, its really difficult.  I'll notice that my head position is a bit off, and shift my focus there for a while.  Then I'll notice myself slacking off on rotating my body with each stroke and try to address that.  And so on.  My point is, its easy to say, "Everyone should follow the standards 100% of the time", but even for mature, experienced software developers, that is not realistic.

Pair programming increases adherence to team standards in a couple ways.  Sometimes your pairing partner will see you starting to go down a path that is out of line with the standards, and remind you.  But a lot of times, they don't have to say anything - just having another person there watching what you type applies pressure to do it "right".

Norming between pairing partners

Obviously, there isn't a team standard for every situation or even most situations.  There is plenty of room for individuality.  There are many facets to software engineering, and individual developers tend to be more dogmatic on some and more pragmatic on others.  I have a couple of favorites, like making domain objects immutable and always constructed by fluent builders, or replacing conditionals with polymorphism.  I also have a few pet peeves I specifically look out for, like overuse of generic interfaces.

Pair programming tends to bend each developer's tendencies toward the pragmatic.  For example, if a domain object only has one or two data members, do you really need to use a builder?  Or if there are only one or two cases in the conditional, is it really worth using separate strategy implementations?  Do we really need to make every class with a single arg non-void return method implement the Handler interface?  No.

public interface Handler<T,R> {
    R handle( T t );
} 

Conclusion

When a pair is really clicking, there is no doubt in my mind, they are going significantly faster, writing significantly better code, and learning more than an individual programmer.  I have experienced the difference first hand, and it is awesome.  But pairs don't always fire on all cylinders.  I'll talk about that in another post.

Sunday, January 6, 2013

Continous wc -l

When you want to see how fast lines are being appended to a file, try this:
tail -f $filename | awk 'BEGIN {nl=0; format="%F %T"}{nl += 1; if (nl % 1000 == 0) print strftime(format) " " nl}'
Just adjust the mod operand to how often you want a printout.

Saturday, January 14, 2012

Resizing Root ext4 filesystem on LVM

Resizing an ext4 filesystem on a lvm partition is pretty straightforward - you just extend the logical volume first, then resize the filesystem.  It gets a little more complicated if its the root filesystem that you want to resize, because resizing an online filesystem is riskier and probably not even supported for ext4 filesystems (see resize2fs man page).  In this case, you can boot from a live cd to resize the unmounted root partition.  However, most live cds do not include lvm support.  Here is how to resize your root partition with a Ubuntu 10.04 live cd.

First, you can resize the logical volume while you're still running off the root partition.  You can look in /etc/fstab to help figure out the logical volume name.  Look for the filesystem mounted on /.  My entry looked like:

/dev/mapper/lvmvolume-lucid64root /   ext4    errors=remount-ro 0 1
The corresponding logical volume name is /dev/lvmvolume/lucid64root.  You can use lvdisplay to verify:
ubuntu@ubuntu:~$ sudo lvdisplay
  --- Logical volume ---
  LV Name                /dev/lvmvolume/lucid64root
  VG Name                lvmvolume
  LV UUID                xbW7iN-x9Ri-gGHG-rwpp-iLu1-gIsf-ycT6dc
  LV Write Access        read/write
  LV Status              NOT available
  LV Size                12.00 GiB
  Current LE             3072
  Segments               2
  Allocation             inherit
  Read ahead sectors     auto
You can extend the logical volume without unmounting (see lvm howto):
ubuntu@ubuntu:~$ sudo lvextend -L12G /dev/lvmvolume/lucid64root

You'll need to use the same logical volume name later when you resize the filesystem.  Now boot into the live cd.  Since the Ubuntu 10.04 live cd I used doesn't have lvm support, the first step is to intall it:
ubuntu@ubuntu:~$ sudo apt-get install lvm2
Then make your logical volumes available:
ubuntu@ubuntu:~$ sudo vgchange -a y
Then resize the filesystem.  The default size is to just fill the partition.
ubuntu@ubuntu:~$ sudo resize2fs /dev/lvmvolume/lucid64root
It might ask you to run e2fck first:
ubuntu@ubuntu:~$ sudo e2fsck -f /dev/lvmvolume/lucid64root
Done.  Now just reboot to your newly resized root partition.

Wednesday, December 21, 2011

Encrypted Flash Swap Partition

Flash swap can improve performance on systems with low memory, and its pretty cheap.  My laptop has 4GB of memory, but I run a lot of applications and browser tabs, so I still end up swapping.  Using flash for swap doesn't make much if any difference while you're using a single application, but I do notice a significant speedup when switching to other applications that have been swapped out.  I'm running Ubuntu 10.04 with encrypted home directory, which also encrypts the swap partition, so I want my new flash swap encrypted as well.  I'm using a Verbatim Stay 'n Store 4GB drive ($9 at amazon) , which has a very small physical footprint, so I can just leave it in all the time.

The first step is to set up a swap partition on your flash drive.  Try to pick the USB port where the drive is least likely to get dislodged.  Insert the drive, and linux should automatically recognize it and mount it.  You can use
user@laptop:~$ mount
to see what drives are mounted.  The newly added usb drive should be the last one.  For example, mine looked like this:
/dev/sdc1 on /media/VERBATIM type vfat (rw,nosuid,nodev,uhelper=udisks,uid=1000,gid=1000,shortname=mixed,dmask=0077,utf8=1,flush)
In order to create a swap partition, you need to unmount it first:
user@laptop:~$ umount /dev/sdc1
Then create the swap partition:
user@laptop:~$ sudo mkswap /dev/sdc1
Then enable it with a high priority (so it gets used ahead of the hard disk swap partition):
user@laptop:~$ sudo swapon -p 32767 /dev/sdc1
You can see that it has been added to the list of available swap partitions (cryptswap1 is the pre-existing hard disk encrypted swap partition):
user@laptop:~$ cat /proc/swaps
Filename Type Size Used Priority
/dev/mapper/cryptswap1                  partition 1949688 0 -1
/dev/sdc1                               partition 3875832 0 32767
Note, if you did not install with the encrypted home directory option, you might need to install the crypto utilities:
user@laptop:~$ sudo apt-get install cryptsetup ecryptfs-utils
Now to encrypt the new swap partition:
user@laptop:~$ sudo ecryptfs-setup-swap
WARNING: [/dev/mapper/cryptswap1] already appears to be encrypted, skipping.
WARNING:
An encrypted swap is required to help ensure that encrypted files are not leaked to disk in an unencrypted format.
HOWEVER, THE SWAP ENCRYPTION CONFIGURATION PRODUCED BY THIS PROGRAM WILL BREAK HIBERNATE/RESUME ON THIS SYSTEM!
NOTE: Your suspend/resume capabilities will not be affected.
Do you want to proceed with encrypting your swap? [y/N]: y
INFO: Setting up swap: [/dev/sdc1]
* Stopping remaining crypto disks...                                          
* cryptswap1 (busy)...                                                        
* cryptswap2 (stopped)...                                               [ OK ]
* Starting remaining crypto disks...                                          
* cryptswap1 (running)...                                                    
* cryptswap2 (starting)..
* cryptswap2 (started)...                                               [ OK ] 
Now your /proc/swaps looks different:
laptop:~$ cat /proc/swaps
Filename Type Size Used Priority
/dev/mapper/cryptswap1                  partition 1949688 0 -1
/dev/mapper/cryptswap2                  partition 3875832 0 -2
Also an entry has been added to fstab:
user@laptop:~$ grep cryptswap /etc/fstab
/dev/mapper/cryptswap1 none swap sw 0 0
/dev/mapper/cryptswap2 none swap sw 0 0
Notice that it did not preserve the priority setting. You can fix this for your current session by doing:
user@laptop:~$ sudo swapoff /dev/mapper/cryptswap2
user@laptop:~$ sudo swapon -p 32767 /dev/mapper/cryptswap2
user@laptop:~$ cat /proc/swaps
Filename Type Size Used Priority
/dev/mapper/cryptswap1                  partition 1949688 0 -1
/dev/mapper/cryptswap2                  partition 3875832 0 32767
And fix it for your next reboot forward by changing the entry in /etc/fstab to:
/dev/mapper/cryptswap2 none swap sw,pri=32767 0 0


Sources:
http://www.arsgeek.com/2008/07/24/readyboost-for-linux-a-quick-how-to/
http://www.logilab.org/29155
http://www.brighthub.com/computing/linux/articles/37236.aspx

Sunday, November 27, 2011

Inexpensive Remote Backup with Synology DS110j

I wanted an offsite backup of my important data (~200GB), with more control and lower recurring cost than a service like dropbox. The Synology DS110j is an inexpensive, but full-featured, network-attached storage (NAS) device. It runs a Linux kernel with BusyBox utilities. I used it to set up a low-power remote rdiff-backup target. I like rdiff-backup because it provides the best features of a mirror and an incremental backup.

Hardware
Synology DiskStation DS110j 1-Bay NAS - $149.99 at Amazon.com
Seagate Barracuda Green ST1500DL003 1.5TB 5900 RPM internal hd - $129.99 at Newegg.com

Power Profile
I measured the power usage in various modes with a KillAWatt.
ModePower (Watts)
Standby (entered automatically after ~10 minutes of no activity)5.1
Active w/o disk activity10.1
During backup10.9

Initial Setup
Follow included instructions to install hd and DiskStation Manager software.

Enable SSH
Use the web interface to enable ssh. I like to change the ssh port from the default. I've found that in practice, this cuts down almost all automated ssh-based hack attempts. To change this setting, ssh to the box as root (same pw as admin in the web interface), then edit DS:/etc/ssh/sshd_config to add the setting:
Port ####
(whatever port you want to use). Also add to SOURCE:/root/.ssh/config
Host DS # whatever the DS hostname is
Port ####
After any change to sshd settings, you'll need to restart the ssh server. You can do this through the web interface by disabling and then re-enabling ssh.

Enable Automated Login
My backup runs as a cron job in the middle of the night, so it needs to be able to ssh login to the DS without prompting for a password. Public key authentication is the way to do this.

In my setup, I am backing up one source to multiple destinations (one local rdiff-backup and the DS for off-site rdiff-backup). Therefore, I prefer to put the rdiff-backup config on the source and push rather than pull my backups. Follow these instructions, where A is the SOURCE side (initiating the rdiff-backup session), and B is the DS side. I used the root account on both sides.

Suppose there are multiple remote hosts for which you want automated logins. You can reuse the same key pair, or set up multiple pairs. Assuming the latter case, rename the private key to something like id_rsa_synology (should also rename public key similarly so you remember which keys are part of the same pair). Also add to SOURCE:/root/.ssh/config (under the DS host section):
IdentityFile /root/.ssh/id_rsa_synology
You may also need to explicitly enable public key authentication on the DS. Again in DS:/etc/ssh/sshd_config:
PubkeyAuthentication yes           
AuthorizedKeysFile .ssh/authorized_keys
Install Package Manager
Synology uses a package manager called ipkg. Install it by following these instructions.

Install rdiff-backup

Setup Non-interactive Environment
When rdiff-backup connects to the DS over ssh, it is a non-interactive session. This mode omits a lot of the environment setup that normally happens when you log in interactively. In particular, the PATH is different:
Interactive:
DiskStation1> echo $PATH
/opt/bin:/opt/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/syno/bin:/usr/syno/sbin:/usr/local/bin:/usr/local/sbin

Non-interactive:
root@local:~# ssh nas1 'echo $PATH'
/usr/bin:/bin:/usr/sbin:/sbin:/usr/syno/bin
The critical difference in this case is that /opt/bin is missing in the non-interactive path. rdiff-backup requires the rdiff-backup executable to be in the path on the remote side. To set the path for non-interactive ssh sessions, add to DS:/etc/ssh/sshd_config
PermitUserEnvironment yes
and create DS:~/.ssh/environment containing
PATH=/opt/bin:/opt/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/syno/bin
DiskStation Hostname
Assuming you don't have a static IP to assign to your DS, you can use dynamic dns to maintain a consistent hostname by which you can reach it. This is functionality is built in to the DS web interface.

Conclusion
Now just park the DS at a friend/family member's house, and viola, 1.5TB of online, offsite, secure storage, with no recurring costs for several years. You'll probably have to set up port forwarding on the router that the DS is connected to, so you can connect to it from the outside. Just use the same port you set up for sshd.