Chris Roberts
- Work at Heavy Water
- Developer / Toolsmith
- Pragmatic Open Source Zealot
- Tirelessly Striving to be Lazy
- Not Really a Believer of Magic
> whoami
Chris Roberts
Unicorns," I said. "Very dangerous. You go first.
What is a "unicorn"
What is Infrastructure as Code
If the road is easy, you're likely going the wrong way.
Yet Another Rails Job
Detangling the rat's nest
Found my way to greener grass.
I was told this was infrastructure as code!
... a true creator is necessity, which is the mother of our invention.
Building collections of instances one at a time is boring
Using Matt Ray's Spiceweasel tool for building clusters
Functional and solved the immediate problem. Some gripes persisted:
Straw that broke the camel's back:
Born from rage and frustration
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
CFN: "Configuration Management & Cloud Orchestration"
{ "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" } } }
Write Ruby, get JSON
Well, no, not really
Lets use
Jenkins
Jackal!
Jackal: Framework for creating simple services
It's a series of tubes.
(Why jackal? Come to the workshop and find out!)
Tools configured:
Collection of independent tools
Simplicity is prerequisite for reliability.
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.
Magic, perhaps, in its simplicity
More Information:
Tools: