Groovy DevOps in the Cloud

Andrey Adamovich, Aestas/IT

About me

Andrey Adamovich

What's this presentation about?

Our take on:

  • DevOps
  • Intrastructure Provisioning
  • Continuous Integration
  • Continuous Delivery

Technologies

Developers + Operations = ?

Silos

Silos

Conflicts

Risk

Traditional

Agile

Agile

What is DevOps?

DevOps

C.A.M.S.

  • Culture : People over processes and tools. Software is made by and for people.
  • Automation : Automation is essential for DevOps to gain quick feedback.
  • Measurement : DevOps finds a specific path to measurement. Quality and shared (or at least aligned) incentives are critical.
  • Sharing : Creates a culture where people share ideas, processes, and tools.

It's not about tools!

It's about culture and process!

But without tools...

...it's definitely harder!

DevOps imply automation!

DevOps imply structure!

Infrastructure as code

Infrastructure as code

Automate the provisioning and maintenance of servers:

  • Build from source control
  • Utilize existing tools
  • Ensure testability

Configuration propagation

Automation 1

Configuration propagation

Automation 1

Changes

Imagine uploading *.class files and repackaging JAR directly on production servers when you have an urgent code change.

Deployment is automatic!

And, so, should be...

infrastructure configuration changes!

No manual changes!

Building an automation toolkit

  • Automation is key
  • We are JVM hackers
  • Fragmented ecosystem

Initial toolset

  • Gradle
  • Groovy
  • Ant
  • Python/WLST
  • Shell scripts

Required tooling

  • Infrastructure connectivity
  • Infrastructure provisioning
  • Infrastructure virtualization
  • Infrastructure testing

First Blood

Ant + Gradle

ant.taskdef(
  name: 'scp', 
  classname: 'o.a.t.a.t.o.ssh.Scp', 
  classpath: configurations.secureShell.asPath) 

ant.taskdef(
  name: 'sshexec', 
  classname: 'o.a.t.a.t.o.ssh.SSHExec', 
  classpath: configurations.secureShell.asPath) 

Simple call

ant.sshexec(
  host: host, 
  username: user, 
  password: password, 
  command: command, 
  trust: 'true', 
  failonerror: failOnError)

Next step: wrapper function

def ssh(String command, 
        Properties props, 
        boolean failOnError = false, 
        String suCommandQuoteChar = "'", 
        String outputProperty = null) {
  ... 
}

Next step: wrapper function

def scp(String file, 
        String remoteDir, 
        Properties props) {
  ...
}

Task example I

task installFonts << {
  forAllServers { props ->
    ssh('yes | yum install *font*', props)
  }
}

Task example II

task uninstallNginx  << {
  forAllServers { props ->
    ssh('/etc/init.d/nginx stop', props)
    ssh('yes | yum remove nginx', props, true)
    ssh('rm -rf /etc/yum.repos.d/nginx.repo', props)
    ssh('rm -rf /var/log/nginx', props)
    ssh('rm -rf /etc/nginx /var/nginx', props)
  }
}

Drawbacks

  • New connection each time
  • Excplicit repeating parameters
  • Complex scripts are hard to maintain
  • Tasks are not idempotent

Sshoogr

Sshoogr logo

Sshoogr features

Groovy-based SSH DSL for:

  • Remote command execution
  • File uploading/downloading
  • Tunneling

Why Groovy?

  • Groovy is perfect choice for scripting
  • Gradle build scripts are Groovy
  • Very mature, concise syntax
  • Extremely easy to produce DSL
  • We wrote a book about it!

Shameless plug

Book Cover

Sshoogr usage (import)

@Grab(
  group='com.aestasit.infrastructure.sshoogr',
  module='sshoogr',
  version='0.9.16')
import static com.aestasit.ssh.DefaultSsh.*

Sshoogr usage (defaults)

defaultUser    = 'root'
defaultKeyFile = new File('secret.pem')
execOptions {
  verbose      = true
  showCommand  = true
}

Sshoogr usage (connection)

remoteSession {
  url = 'user2:654321@localhost:2222'
  exec 'rm -rf /tmp/*'
  exec 'touch /var/lock/my.pid'
  remoteFile('/var/my.conf').text = "enabled=true"
}

Sshoogr usage (multi-line content)

remoteFile('/etc/yum.repos.d/puppet.repo').text = '''
  [puppet]
  name=Puppet Labs Packages
  baseurl=http://yum.puppetlabs.com/el/
  enabled=0
  gpgcheck=0
'''

Sshoogr usage (file copying)

remoteSession {
  scp {
    from { localDir "$buildDir/application" }
    into { remoteDir '/var/bea/domain/application' }
  }
}

Sshoogr usage (command result)

def result = exec(command: '/usr/bin/mycmd',
  failOnError: false, showOutput: false)
if (result.exitStatus == 1) {
  result.output.eachLine { line ->
    if (line.contains('WARNING')) {
      throw new RuntimeException("Warning!!!")
    }
  }
}

Sshoogr usage (shortcuts)

if (ok('/usr/bin/mycmd')) {
  ...
}
if (fail('/usr/bin/othercmd')) {
  ...
}

Sshoogr usage (tunnels)

tunnel('1.2.3.4', 8080) { int localPort ->
  def url = "http://localhost:${localPort}/flushCache"
  def result = new URL(url).text
  if (result == 'OK') {
    println "Cache is flushed!"
  } else {
    throw new RuntimeException(result)
  }
}

Sshoogr usage (prefix/suffix)

prefix('sudo ') {
  exec 'rm -rf /var/log/abc.log'
  exec 'service abc restart'
}
suffix(' >> output.log') {
  exec 'yum -y install nginx'
  exec 'yum -y install mc'
  exec 'yum -y install links'
}

Still problems

  • Complex scripts are still not easy to maintain
  • Scripts are usually not idempotent

Puppet

Why Puppet?

  • More mature than competition
  • Large community
  • Readable DSL
  • Good acceptance from DEVs and OPs
  • No need to learn Ruby ;)

Puppet example

Puppet code

Puppet provisioning

Provisioning 1

Puppet provisioning

Provisioning 2

Puppet provisioning

Provisioning 3

Puppet provisioning

Provisioning 4

Puppet state management

State 1

Puppet state management

State 2

Puppet state management

State 3

Puppet modules

Modules 1

Puppet modules

Modules 2

Puppet modules

Modules 3

Sshoogr + Gradle + Puppet

Upload modules

task uploadModules << {
  remoteSession {
    exec 'rm -rf /tmp/repo.zip'
    scp {
      from { localFile "${buildDir}/repo.zip" }
      into { remoteDir "/root" }
    }
    ...

Upload modules

    ...
    exec 'rm -rf /etc/puppet/modules'
    exec 'unzip /tmp/repo.zip -d /etc/puppet/modules'
  }
}

Apply manifests

task puppetApply(dependsOn: uploadModules) << {
  remoteSession {
    scp {
      from { localFile "${buildDir}/setup.pp" }
      into { remoteDir "/tmp" }
    }
    exec 'puppet apply /tmp/setup.pp'
  }
}

What we solved?

  • Separated infrastructure state description and operations tasks
  • Scripts became more maintainable and idempotent

In the meanwhile...

  • We started developing complex/generic Puppet modules
  • Modules need proper testing
  • ...on different platforms

Do you test, right?

  • How to test this stuff?
  • How to reuse a JUnit approach to testing?
  • We wanted things to be SIMPLE!

PUnit

PUnit logo

PUnit

  • Simple testing tool for verifying remote server state
  • Uses Sshoogr and JUnit
  • Reuse reporting features of JUnit
  • As simple as ...

PUnit example (derby)

class DerbyInstallTest 
    extends BasePuppetIntegrationTest {
  @Before
  void installDerby() {
    apply("include derby")
  }
  ...
}

PUnit example (derby)

@Test
void ensureDerbyRunning() {
  command('service derby status > derbystatus.log')
  assertTrue fileText("/root/derbystatus.log")
               .contains('Derby')
  assertTrue fileText("/root/derbystatus.log")
               .contains('is running.')
}

PUnit example (derby)

@Test
void ensureCanConnect() {
  Thread.sleep(10000)
  uploadScript()
  command('/opt/derby/db-derby-10.9.1.0-bin/bin/ij ' + 
          'testDataScript.sql > derbytest.log')
  ...

PUnit example (derby)

  ...
  // Check if the log of the insert 
  // operation contains the word ERROR.
  assertFalse(
    "The script should return at least one error",
    fileText("/root/derbytest.log")
      .contains('ERROR')
  )
  ...

PUnit example (derby)

  ...
  // Check on data that was inserted into a table.
  assertTrue(
    "The log should contain a SELECT result",
    fileText("/root/derbytest.log")
     .contains('Grand Ave.')
  )
}

PUnit example (jenkins)

session {
  tunnel ('127.0.0.1', 8080) { int localPort ->
    def driver = new HtmlUnitDriver(false)
    driver.manage()
          .timeouts()
          .pageLoadTimeout(300, TimeUnit.SECONDS)
          .implicitlyWait(30, TimeUnit.SECONDS)
    driver.get("http://127.0.0.1:${localPort}/login")
    ...

PUnit example (jenkins)

    ...
    def input = driver.findElement(By.name('j_username'))
    input.sendKeys('john')
    input = driver.findElement(By.name('j_password'))
    input.sendKeys('123456')
    input.submit()
    ...

PUnit example (jenkins)

    ...
    def wait = new WebDriverWait(driver, 30)
    wait.until ExpectedConditions.
       presenceOfElementLocated (By.linkText('John Doe'))
    ...
  }
}

PUnit example (svn)

session {
  tunnel ('127.0.0.1', 80) { int localPort ->
    // Initilize repository connection data.
    DAVRepositoryFactory.setup()
    def url = SVNURL.create('http', null, '127.0.0.1', 
                localPort, 'repos/cafebabe', true)
    def repository = SVNRepositoryFactory.create(url)
    println "Verifying SVN repository at ${url}"
    ...

PUnit example (svn)

    ...
    // Setup credentials.
    def authManager = SVNWCUtil.
      createDefaultAuthenticationManager('joe', '123456')
    repository.setAuthenticationManager(authManager)
    
    // Verify repository is at revision 0.
    assertEquals 0, repository.getLatestRevision()
    ...

PUnit example (svn)

    ...
    // Commit first revision.
    ISVNEditor editor = repository.
      getCommitEditor("Initial commit.", null)
    editor.with {
      openRoot(-1)
      addFile('dummy.txt', null, -1)
      applyTextDelta('dummy.txt', null)
      def deltaGenerator = new SVNDeltaGenerator()

PUnit example (svn)

      ...
      def checksum = deltaGenerator.sendDelta('dummy.txt', 
        new ByteArrayInputStream("data".getBytes()), 
        editor, true) 
      closeFile('dummy.txt', checksum)
      def commitInfo = closeEdit()
      println commitInfo
    }
    ...

PUnit example (svn)

    ...
    // Verify repository is at revision 1 now.
    assertEquals 1, repository.getLatestRevision()    
  }
}

Continuous integration

Continous integration

Why Jenkins?

  • De-facto standard
  • Stable
  • There is a plugin for that!

Jenkins build

Puppet Build

Next problem?

Scalability

  • How do we test on different OS?
  • How do we run parallel tests on multiple architectures?
  • How do we avoid selling our houses?

Amazon Web Services

Elastic Compute Cloud

  • Mature
  • Great API
  • Virtual hardware variety
  • OS variety

Gramazon

Gramazon Logo

Gramazon

  • Groovy-based API for interacting with EC2
  • Integration with Gradle

Gramazon example I

task startInstance(type: StartInstance) {
  keyName       'cloud-do'
  securityGroup 'cloud-do'
  instanceName  'gramazon/cloud-do'
  stateFileName 'cloud-do.json'
  ami           'ami-6f07e418'       
  instanceType  't1.micro'
  waitForStart  true
}

Gramazon example II

task terminateInstance(type: TerminateInstance) {
  stateFileName 'cloud-do.json'
}

The flow

  1. Start instance(s)
  2. Upload manifests
  3. Run tests
  4. Generate report
  5. Terminate instance(s)

Next issue?

Imgr

Imgr Logo

Imgr

  • A tool for building images
  • Inspired by Packer

Supports

  • Shell
  • Puppet

Configuration example

Imgr

Summary

Images, manifests, tasks

Images

The big picture

Images

Aetomation

Images

Conclusions

  • Reuse your existing Java knowledge
  • ...to build a bridge between DEVs and OPs
  • Reuse development best practices for OPs
  • Don't be afraid to try new technologies
  • Automate!

Next steps?

  • Create more documentation and examples
  • Add more DSL convience methods
  • Extend integration with Gradle
  • Add Windows connectivity/scripting support
  • Define richer model for EC2 and potentially other clouds
  • Extend support for other provisioning tools

Reading material

The Phoenix Project

Phoenix Project

Continuous Delivery

Cd

Release It

Cd

Programming Amazon EC2

ec2

Gradle in Action

gradle

Groovy 2 Cookbook

Groovy book

Technologies to follow

One more thing...

It's all Open Source!

Source code

Seeking contributors!

Questions?

Thank you!

DevOps