http://store.glennz.com/products/autobacon

> whoami

Chris Roberts

  • Work at Heavy Water
  • Developer / Toolsmith
  • Pragmatic Open Source Zealot
  • Tirelessly Striving to be Lazy
  • Not Really a Believer of Magic
Unicorns," I said. "Very dangerous. You go first.
Jim Butcher, Summer Knight

Searching for Unicorns

What is a "unicorn"

  • Goat like animal
  • Horse like animal
  • Has a goat like beard
  • Is winged

"Infrastructure as Code" Unicorn

What is Infrastructure as Code

  • No correct answer
  • Many correct answers
  • Lots of people searching for different things
    • Multiple classifications
    • All paths lead forward
  • My unicorn
If the road is easy, you're likely going the wrong way.
Terry Goodkind

YARJ

Yet Another Rails Job

  • Really like Ruby
  • No operations team
  • Environment evolved
  • Don't touch anything
  • Search and pray
  • Any problem == panic

Introduction to Automation

Detangling the rat's nest

  • Complete overhaul using Chef
  • Extracting setup knowledge to code
  • Predictable reproducibility
  • Easy additions/replacements
  • No golden images
  • Amazing

OMG I'm Infrastructure as Coding!

Found my way to greener grass.

  • Digging into chef code base
  • Started submitting PRs for new features
    • Dynamic runlist modifications
    • Process forking
    • Lazy attributes
  • Writing cookbooks for all the things
    • This is the future people!

Not Really Infrastructure as Code

I was told this was infrastructure as code!

    • Infrastructure as Code
      • Term was used too loosely
      • Meant any of the things not all the things
    • Configuration as code
      • Nodes were faster (create/replace)
      • Configuration knowledge in code
    • Work to be done
      • Still snowflaking resources
      • Still too many bus factors

... a true creator is necessity, which is the mother of our invention.
Plato, The Republic

Need Drives Creation

Building collections of instances one at a time is boring

  • Slow and tedious
  • Testing required fresh builds
  • Development required existing builds
  • Wasting time waiting

Spiceweasel: More Than Seasoning

Using Matt Ray's Spiceweasel tool for building clusters

  • Could mostly build clusters
    • Serialized configuration meant copies
    • Needed more command support
  • Added required command and config support
  • Successful clusters with single command*
    • If everything runs successfully
    • If my connection stays stable

The `If` Too Big

Functional and solved the immediate problem. Some gripes persisted:

  • Heavy SSH usage
  • DRY but lots of shell
  • Walking access point

Straw that broke the camel's back:

  • Intermittent network failures

Vagabond Origin

Born from rage and frustration

    • Handful of scripts + LXC
    • DSL inspired from previous DRY
    • Large local cluster builds fast and reliable
    • Seeing a pattern in description
    • Implemented helpers for pattern
    • Turtles all the way down

From the DSL:

Configuration.new do
  defaults do
    template 'ubuntu_1404'
    attributes.creator ENV['USER']
  end
  nodes do
    database.run_list ['role[database]']
  end
end

Into primatives:

{
  "nodes" => {
    "database" => {
      "template" => "ubuntu_1404",
      "run_list" => [
        "role[database]"
      ],
      "attributes" => {
        "creator" => "spox"
      }
    }
  }
}

No one cares about Hashes

AWS CloudFormation

CFN: "Configuration Management & Cloud Orchestration"

  • Templates to describe infrastructure
  • Limited but powerful number of "runtime" functions
  • Reliably reproducable infrastructure
  • The most amazing API
  • The worst interface in the world

"Let me show you awesome CFN!"

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation Sample Template Rails_Multi_AZ",
  "Parameters": {
    "KeyName": {
      "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "64",
      "AllowedPattern": "[-_ a-zA-Z0-9]*",
      "ConstraintDescription": "can contain only alphanumeric characters, spaces, dashes and underscores."
    },
    "DBName": {
      "Default": "MyDatabase",
      "Description": "MySQL database name",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "64",
      "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters."
    },
    "DBUsername": {
      "NoEcho": "true",
      "Description": "Username for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "16",
      "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters."
    },
    "DBPassword": {
      "NoEcho": "true",
      "Description": "Password for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "41",
      "AllowedPattern": "[a-zA-Z0-9]*",
      "ConstraintDescription": "must contain only alphanumeric characters."
    },
    "DBAllocatedStorage": {
      "Default": "5",
      "Description": "The size of the database (Gb)",
      "Type": "Number",
      "MinValue": "5",
      "MaxValue": "1024",
      "ConstraintDescription": "must be between 5 and 1024Gb."
    },
    "DBInstanceClass": {
      "Default": "db.m1.small",
      "Description": "The database instance type",
      "Type": "String",
      "AllowedValues": [
        "db.m1.small",
        "db.m1.large",
        "db.m1.xlarge",
        "db.m2.xlarge",
        "db.m2.2xlarge",
        "db.m2.4xlarge"
      ],
      "ConstraintDescription": "must select a valid database instance type."
    },
    "MultiAZDatabase": {
      "Default": "true",
      "Description": "Create a multi-AZ MySQL Amazon RDS database instance",
      "Type": "String",
      "AllowedValues": [
        "true",
        "false"
      ],
      "ConstraintDescription": "must be either true or false."
    },
    "WebServerCapacity": {
      "Default": "2",
      "Description": "The initial number of WebServer instances",
      "Type": "Number",
      "MinValue": "1",
      "MaxValue": "5",
      "ConstraintDescription": "must be between 1 and 5 EC2 instances."
    },
    "InstanceType": {
      "Description": "WebServer EC2 instance type",
      "Type": "String",
      "Default": "m1.small",
      "AllowedValues": [
        "t1.micro",
        "m1.small",
        "m1.medium",
        "m1.large",
        "m1.xlarge",
        "m2.xlarge",
        "m2.2xlarge",
        "m2.4xlarge",
        "m3.xlarge",
        "m3.2xlarge",
        "c1.medium",
        "c1.xlarge",
        "cc1.4xlarge",
        "cc2.8xlarge",
        "cg1.4xlarge"
      ],
      "ConstraintDescription": "must be a valid EC2 instance type."
    },
    "SSHLocation": {
      "Description": " The IP address range that can be used to SSH to the EC2 instances",
      "Type": "String",
      "MinLength": "9",
      "MaxLength": "18",
      "Default": "0.0.0.0/0",
      "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
      "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
    }
  },
  "Mappings": {
    "AWSInstanceType2Arch": {
      "t1.micro": {
        "Arch": "64"
      },
      "m1.small": {
        "Arch": "64"
      },
      "m1.medium": {
        "Arch": "64"
      },
      "m1.large": {
        "Arch": "64"
      },
      "m1.xlarge": {
        "Arch": "64"
      },
      "m2.xlarge": {
        "Arch": "64"
      },
      "m2.2xlarge": {
        "Arch": "64"
      },
      "m2.4xlarge": {
        "Arch": "64"
      },
      "m3.xlarge": {
        "Arch": "64"
      },
      "m3.2xlarge": {
        "Arch": "64"
      },
      "c1.medium": {
        "Arch": "64"
      },
      "c1.xlarge": {
        "Arch": "64"
      },
      "cc1.4xlarge": {
        "Arch": "64HVM"
      },
      "cc2.8xlarge": {
        "Arch": "64HVM"
      },
      "cg1.4xlarge": {
        "Arch": "64HVM"
      }
    },
    "AWSRegionArch2AMI": {
      "us-east-1": {
        "32": "ami-31814f58",
        "64": "ami-1b814f72",
        "64HVM": "ami-0da96764"
      },
      "us-west-2": {
        "32": "ami-38fe7308",
        "64": "ami-30fe7300",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "us-west-1": {
        "32": "ami-11d68a54",
        "64": "ami-1bd68a5e",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "eu-west-1": {
        "32": "ami-973b06e3",
        "64": "ami-953b06e1",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "ap-southeast-1": {
        "32": "ami-b4b0cae6",
        "64": "ami-beb0caec",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "ap-southeast-2": {
        "32": "ami-b3990e89",
        "64": "ami-bd990e87",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "ap-northeast-1": {
        "32": "ami-0644f007",
        "64": "ami-0a44f00b",
        "64HVM": "NOT_YET_SUPPORTED"
      },
      "sa-east-1": {
        "32": "ami-3e3be423",
        "64": "ami-3c3be421",
        "64HVM": "NOT_YET_SUPPORTED"
      }
    }
  },
  "Resources": {
    "ElasticLoadBalancer": {
      "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
      "Metadata": {
        "Comment": "Configure the Load Balancer with a simple health check and cookie-based stickiness"
      },
      "Properties": {
        "AvailabilityZones": {
          "Fn::GetAZs": ""
        },
        "LBCookieStickinessPolicy": [
          {
            "PolicyName": "CookieBasedPolicy",
            "CookieExpirationPeriod": "30"
          }
        ],
        "Listeners": [
          {
            "LoadBalancerPort": "80",
            "InstancePort": "3000",
            "Protocol": "HTTP",
            "PolicyNames": [
              "CookieBasedPolicy"
            ]
          }
        ],
        "HealthCheck": {
          "Target": "HTTP:3000/",
          "HealthyThreshold": "2",
          "UnhealthyThreshold": "5",
          "Interval": "10",
          "Timeout": "5"
        }
      }
    },
    "WebServerGroup": {
      "Type": "AWS::AutoScaling::AutoScalingGroup",
      "Properties": {
        "AvailabilityZones": {
          "Fn::GetAZs": ""
        },
        "LaunchConfigurationName": {
          "Ref": "LaunchConfig"
        },
        "MinSize": "1",
        "MaxSize": "5",
        "DesiredCapacity": {
          "Ref": "WebServerCapacity"
        },
        "LoadBalancerNames": [
          {
            "Ref": "ElasticLoadBalancer"
          }
        ]
      }
    },
    "LaunchConfig": {
      "Type": "AWS::AutoScaling::LaunchConfiguration",
      "Metadata": {
        "Comment1": "Configure the bootstrap helpers to install the Rails",
        "Comment2": "The application is downloaded from the CloudFormationRailsSample.zip file",
        "AWS::CloudFormation::Init": {
          "config": {
            "packages": {
              "yum": {
                "gcc-c++": [ ],
                "make": [ ],
                "ruby-devel": [ ],
                "rubygems": [ ],
                "mysql": [ ],
                "mysql-devel": [ ],
                "mysql-libs": [ ]
              },
              "rubygems": {
                "rack": [
                  "1.3.6"
                ],
                "execjs": [ ],
                "therubyracer": [ ],
                "rails": [ "3.2.14" ]
              }
            },
            "sources": {
              "/home/ec2-user/sample": "https://s3.amazonaws.com/cloudformation-examples/CloudFormationRailsSample.zip"
            },
            "files": {
              "/home/ec2-user/sample/config/database.yml": {
                "content": {
                  "Fn::Join": [
                    "",
                    [
                      "development:\n",
                      " adapter: mysql2\n",
                      " encoding: utf8\n",
                      " reconnect: false\n",
                      " pool: 5\n",
                      " database: ",
                      {
                        "Ref": "DBName"
                      },
                      "\n",
                      " username: ",
                      {
                        "Ref": "DBUsername"
                      },
                      "\n",
                      " password: ",
                      {
                        "Ref": "DBPassword"
                      },
                      "\n",
                      " host: ",
                      {
                        "Fn::GetAtt": [
                          "MySQLDatabase",
                          "Endpoint.Address"
                        ]
                      },
                      "\n",
                      " port: ",
                      {
                        "Fn::GetAtt": [
                          "MySQLDatabase",
                          "Endpoint.Port"
                        ]
                      },
                      "\n"
                    ]
                  ]
                },
                "mode": "000644",
                "owner": "root",
                "group": "root"
              }
            }
          }
        }
      },
      "Properties": {
        "ImageId": {
          "Fn::FindInMap": [
            "AWSRegionArch2AMI",
            {
              "Ref": "AWS::Region"
            },
            {
              "Fn::FindInMap": [
                "AWSInstanceType2Arch",
                {
                  "Ref": "InstanceType"
                },
                "Arch"
              ]
            }
          ]
        },
        "InstanceType": {
          "Ref": "InstanceType"
        },
        "SecurityGroups": [
          {
            "Ref": "WebServerSecurityGroup"
          }
        ],
        "KeyName": {
          "Ref": "KeyName"
        },
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#!/bin/bash -v\n",
                "yum update -y aws-cfn-bootstrap\n",
                "# Helper function\n",
                "function error_exit\n",
                "{\n",
                " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '",
                {
                  "Ref": "WaitHandle"
                },
                "'\n",
                " exit 1\n",
                "}\n",
                "# Install Rails packages\n",
                "/opt/aws/bin/cfn-init -s ",
                {
                  "Ref": "AWS::StackId"
                },
                " -r LaunchConfig ",
                " --region ",
                {
                  "Ref": "AWS::Region"
                },
                " || error_exit 'Failed to run cfn-init'\n",
                "# Install anu other Gems, create the database and run a migration\n",
                "cd /home/ec2-user/sample\n",
                "bundle install || error_exit 'Failed to install bundle'\n",
                "rake db:migrate || error_exit 'Failed to execute database migration'\n",
                "# Startup the rails server\n",
                "rails server -d\n",
                "echo \"cd /home/ec2-user/sample\" >> /etc/rc.local\n",
                "echo \"rails server -d\" >> /etc/rc.local\n",
                "# All is well so signal success\n",
                "/opt/aws/bin/cfn-signal -e 0 -r \"Rails application setup complete\" '",
                {
                  "Ref": "WaitHandle"
                },
                "'\n"
              ]
            ]
          }
        }
      }
    },
    "WaitHandle": {
      "Type": "AWS::CloudFormation::WaitConditionHandle"
    },
    "WaitCondition": {
      "Type": "AWS::CloudFormation::WaitCondition",
      "DependsOn": "WebServerGroup",
      "Properties": {
        "Handle": {
          "Ref": "WaitHandle"
        },
        "Timeout": "1500",
        "Count": {
          "Ref": "WebServerCapacity"
        }
      }
    },
    "WebServerSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Enable HTTP access via port 3000 locked down to the load balancer + SSH access",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": "3000",
            "ToPort": "3000",
            "SourceSecurityGroupOwnerId": {
              "Fn::GetAtt": [
                "ElasticLoadBalancer",
                "SourceSecurityGroup.OwnerAlias"
              ]
            },
            "SourceSecurityGroupName": {
              "Fn::GetAtt": [
                "ElasticLoadBalancer",
                "SourceSecurityGroup.GroupName"
              ]
            }
          },
          {
            "IpProtocol": "tcp",
            "FromPort": "22",
            "ToPort": "22",
            "CidrIp": {
              "Ref": "SSHLocation"
            }
          }
        ]
      }
    },
    "DBSecurityGroup": {
      "Type": "AWS::RDS::DBSecurityGroup",
      "Properties": {
        "GroupDescription": "Grant database access to web server",
        "DBSecurityGroupIngress": {
          "EC2SecurityGroupName": {
            "Ref": "WebServerSecurityGroup"
          }
        }
      }
    },
    "MySQLDatabase": {
      "Type": "AWS::RDS::DBInstance",
      "Properties": {
        "Engine": "MySQL",
        "DBName": {
          "Ref": "DBName"
        },
        "MultiAZ": {
          "Ref": "MultiAZDatabase"
        },
        "MasterUsername": {
          "Ref": "DBUsername"
        },
        "MasterUserPassword": {
          "Ref": "DBPassword"
        },
        "DBInstanceClass": {
          "Ref": "DBInstanceClass"
        },
        "DBSecurityGroups": [
          {
            "Ref": "DBSecurityGroup"
          }
        ],
        "AllocatedStorage": {
          "Ref": "DBAllocatedStorage"
        }
      }
    }
  },
  "Outputs": {
    "WebsiteURL": {
      "Value": {
        "Fn::Join": [
          "",
          [
            "http://",
            {
              "Fn::GetAtt": [
                "ElasticLoadBalancer",
                "DNSName"
              ]
            }
          ]
        ]
      },
      "Description": "URL for newly created Rails application"
    }
  }
}

SFN for Awesome CFN

Write Ruby, get JSON

  • Hey look, Hashes!
  • SparkleFormation to build templates
  • sfn tool to leverage SparkleFormation
  • Includes features that will blow your mind
    • (covered in lighting talk)
  • Is not a serialization format

Now I'm Infrastructure as Coding!

Well, no, not really

  • My unicorn requires:
    • Push button
    • Receive infrastructure
  • What I have:
    • Configuration as code
    • Infrastructure as code
      • (but not my unicorn!)

Link the Code to the Infra

Lets use Jenkins Jackal!

Jackal: Framework for creating simple services

It's a series of tubes.
Ted Stevens

(Why jackal? Come to the workshop and find out!)

All the Parts

Tools configured:

    • Chef to code the configuration
    • SparkleFormation to code the infrastructure
    • GitHub to store the code
    • AWS CFN to provide the infrastructure
    • Jackal to glue GitHub and AWS together
  • Push tag, receive infrastructure

K.I.S.S.

Collection of independent tools

  • Disperate tools
  • Each performing a single job
  • Linked together for complex task
  • Easily swappable
  • Easily reconfigurable

Simplicity is prerequisite for reliability.
Edsger W. Dijkstra
A Chinese prose writer has observed that the unicorn, because of its own anomaly, will pass unnoticed. Our eyes see what they are accustomed to seeing.
Jorge Luis Borges

Not Really so Magical

Magic, perhaps, in its simplicity

  • Used existing tools
  • Provided better interfaces
  • Redefined existing workflows
  • Adjusted view for new perspective

Looks Like a Unicorn

Informations

More Information:

  • SparkleFormation lightning talk
  • SparkleFormation workshop
  • Jackal workshop
  • Accost me in the hallway

Tools:

  • sfn http://github.com/sparkleformation/sfn
  • SparkleFormation http://github.com/sparkleformation/sparkle_formation
  • Jackal http://github.com/carnivore-rb/jackal

Thank you!