How-tos and Support

Continuous Security for Rails apps with Pipeline and Brakeman

15 minutes to read

When the Ruby on Rails framework debuted itchanged the industry in two noteworthy ways: it created a trend of opinionated webapplication frameworks (Django,Play, Grails) and italso strongly encouraged thousands of developers to embrace test-drivendevelopment along with many other modern best practices (source control, dependencymanagement, etc). Because Ruby, the language underneath Rails, is interpretedinstead of compiled there isn't a "build" per se but rather tens, if nothundreds, of tests, linters and scans which are run to ensure the application'squality. With the rise in popularity of Rails, the popularity of applicationhosting services with easy-to-use deployment tools like Heroku orEngine Yard rose too.

This combination of good test coverage and easily automated deploymentsmakes Rails easy to continuously deliver with Jenkins. In this post we'll covertesting non-trivial Rails applications withJenkins Pipeline and, as an added bonus, we will add security scanning viaBrakeman and theBrakeman plugin.

Stage View

Setup

For this demonstration, I used Ruby Central'scfp-app:

A Ruby on Rails application that lets you manage your conference's call for proposal (CFP), program and schedule. It was written by Ruby Central to run the CFPs for RailsConf and RubyConf.

I chose this Rails app, not only because it's a sizable application with lotsof tests, but it's actually the application we used to collect talk proposalsfor the "[Community Tracks]" at thisyear's Jenkins World. For the most part,cfp-app is a standard Rails application. It usesPostgreSQL for its database,RSpec for its tests andRuby 2.3.x as its runtime.

TIP: If you prefer to just to look at the code, skip straight to the Jenkinsfile.

Preparing the app

For most Rails applications there are few, if any, changes needed to enablecontinuous delivery with Jenkins. In the case ofcfp-app, I added two gems to getthe most optimal integration into Jenkins:

Adding these was simple, I just needed to update the Gemfile and theRakefile in the root of the repository to contain:

Gemfile

LANG:code
# .. snip ..
group :test do
# RSpec, etc
gem 'ci_reporter'
gem 'ci_reporter_rspec'
gem "brakeman", :require => false
end

Rakefile

LANG:code
# .. snip ..
require 'ci/reporter/rake/rspec'
# Make sure we setup ci_reporter before executing our RSpec examples
task :spec => 'ci:setup:rspec'

Preparing Jenkins

With the cfp-app project set up, next on the list is to ensure that Jenkins itselfis ready. Generally I suggest running the [latest LTS] ofJenkins; for this demonstration I used Jenkins 2.7.1 with the followingplugins:

I also used theGitHub Organization Folder pluginto automatically create pipeline items in myJenkins instance; that isn't required for the demo, but it's pretty cool to seerepositories and branches with a Jenkinsfile automatically show up inJenkins, so I recommend it!

In addition to the plugins listed above, I also needed at least oneJenkins agent with the Docker daemon installed andrunning on it. I label these agents with "docker" to make it easier to assignDocker-based workloads to them in the future.

Any Linux-based machine with Docker installed will work, in my case I wasprovisioning on-demand agents with theAzureplugin which, like theEC2 plugin,helps keep my test costs down.

TIP: If you're using Amazon Web Services, you might also be interested in this blog post from earlier this year unveiling the EC2 Fleet plugin for working with EC2 Spot Fleets.

Writing the Pipeline

To make sense of the various things that the Jenkinsfile needs to do, I findit easier to start by simply defining the stages of my pipeline. This helps methink of, in broad terms, what order of operations my pipeline should have.For example:

LANG:code
/* Assign our work to an agent labelled 'docker' */
node('docker') {
stage 'Prepare Container'
stage 'Install Gems'
stage 'Prepare Database'
stage 'Invoke Rake'
stage 'Security scan'
stage 'Deploy'
}

As mentioned previously, this Jenkinsfile is going to rely heavily on the[CloudBees Docker Pipeline plugin]The plugin provides two very important features:

  • Ability to execute steps inside of a running Docker container
  • Ability to run a container in the "background."

Like most Rails applications, one can effectively test the application with twocommands: bundle install followed by bundle exec rake. I already had someDocker images prepared with RVM and Ruby 2.3.0 installed,which ensures a common and consistent starting point:

LANG:code
node('docker') {
// .. 'stage' steps removed
docker.image('rtyler/rvm:2.3.0').inside { // <1>
rvm 'bundle install' // <2>
rvm 'bundle exec rake'
} // <3>
}

  • 1: Run the named container. The inside method can take optional additional flags for the docker run command.
  • 2: Execute our shell commands using our tiny sh step wrapper rvm. This ensures that the shell code is executed in the correct RVM environment.
  • 3: When the closure completes, the container will be destroyed.

Unfortunately, with this application, the bundle exec rake command will failif PostgreSQL isn't available when the process starts. This is where thesecond important feature of the CloudBees Docker Pipeline plugin comesinto effect: the ability to run a container in the "background."

LANG:code
node('docker') {
// .. 'stage' steps removed
/* Pull the latest `postgres` container and run it in the background */
docker.image('postgres').withRun { container -> // <1>
echo "PostgreSQL running in container ${container.id}" // <2>
} // <3>
}

  • 1: Run the container, effectively docker run postgres
  • 2: Any number of steps can go inside the closure
  • 3: When the closure completes, the container will be destroyed.

Running the tests

Combining these two snippets of Jenkins Pipeline is, in my opinion, where thepower of the DSLshines:

LANG:code
node('docker') {
docker.image('postgres').withRun { container ->
docker.image('rtyler/rvm:2.3.0').inside("--link=${container.id}:postgres") { // <1>
stage 'Install Gems'
rvm "bundle install"

stage 'Invoke Rake'
withEnv(['DATABASE_URL=postgres://postgres@postgres:5432/']) { // <2>
rvm "bundle exec rake"
}
junit 'spec/reports/*.xml' // <3>
}
}
}

  • 1: By passing the --link argument, the Docker daemon will allow the RVM container to talk to the PostgreSQL container under the host name 'postgres'.
  • 2: Use the withEnv step to set environment variables for everything that is in the closure. In this case, the cfp-app DB scaffolding will look for the DATABASE_URL variable to override the DB host/user/dbname defaults.
  • 3: Archive the test reports generated by ci_reporter so that Jenkins can display test reports and trend analysis.
image

With this done, the basics are in place to consistently run the tests forcfp-app in fresh Docker containers for each execution of the pipeline.

Security scanning

Using Brakeman, the security scanner for Rubyon Rails, is almost trivially easy inside of Jenkins Pipeline, thanks to theBrakemanplugin which implements the publishBrakeman step.

Building off our example above, we can implement the "Security scan" stage:

LANG:code
node('docker') {
/* --8<--8<-- snipsnip --8<--8<-- */
stage 'Security scan'
rvm 'brakeman -o brakeman-output.tabs --no-progress --separate-models' // <1>
publishBrakeman 'brakeman-output.tabs' // <2>
/* --8<--8<-- snipsnip --8<--8<-- */
}

  • 1: Run the Brakeman security scanner for Rails and store the output for later in brakeman-output.tabs
  • 2: Archive the reports generated by Brakeman so that Jenkins can display detailed reports with trend analysis.
image
CAUTION: As of this writing, there is work in progress (JENKINS-31202) to render trend graphs from plugins like Brakeman on a pipeline project's main page.

Deploying the good stuff

Once the tests and security scanning are all working properly, we can start toset up the deployment stage. Jenkins Pipeline provides the variablecurrentBuild which we can use to determine whether our pipeline has beensuccessful thus far or not. This allows us to add the logic to only deploy wheneverything is passing, as we would expect:

LANG:code
node('docker') {
/* --8<--8<-- snipsnip --8<--8<-- */
stage 'Deploy'
if (currentBuild.result == 'SUCCESS') { // <1>
sh './deploy.sh' // <2>
}
else {
mail subject: "Something is wrong with ${env.JOB_NAME} ${env.BUILD_ID}",
to: 'nobody@example.com',
body: 'You should fix it'
}
/* --8<--8<-- snipsnip --8<--8<-- */
}

  • 1: currentBuild has the result property which would be 'SUCCESS', 'FAILED', 'UNSTABLE', 'ABORTED'
  • 2: Only if currentBuild.result is successful should we bother invoking our deployment script (e.g. git push heroku master)

Wrap up

I have gratuitously commented the fullJenkinsfilewhich I hope is a useful summation of the work outlined above. Having workedon a number of Rails applications in the past, the consistency provided byDocker and Jenkins Pipeline above would have definitely improved thoseprojects' delivery times. There is still room for improvement however, whichis left as an exercise for the reader. Such as: preparing new containers withall theirdependencies built-in instead of installing them at run-time. Or utilizing the parallelstep for executing RSpec across multiple Jenkins agents simultaneously.

The beautiful thing about defining your continuous delivery, and continuoussecurity, pipeline in code is that you can continue to iterate on it!

image

This is the fifth in a series of posts showing ways to use Jenkins Pipeline. Follow along with this entire series through the links below:

Subscription confirmed

You'll now receive our latest news, releases, and event updates by email.