{"id":195,"date":"2013-12-09T19:08:47","date_gmt":"2013-12-09T19:08:47","guid":{"rendered":"http:\/\/blogs.nd.edu\/devops\/?p=195"},"modified":"2013-12-09T21:00:26","modified_gmt":"2013-12-09T21:00:26","slug":"a-public-rails-instance-from-a-single-command","status":"publish","type":"post","link":"https:\/\/sites.nd.edu\/devops\/2013\/12\/09\/a-public-rails-instance-from-a-single-command\/","title":{"rendered":"A Public Rails App From Scratch in a Single Command"},"content":{"rendered":"<p>This weekend, I tweeted the public URL of an AWS instance. \u00a0Like most instances during this experimentation phase, it was not meant to live forever, so I&#8217;ll reproduce it here:<\/p>\n<p>_________________________________________________________________________________________________________<br \/>\n<a href=\"http:\/\/blogs.nd.edu\/devops\/files\/2013\/12\/devops_intensifies1.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-198 aligncenter\" alt=\"devops_intensifies\" src=\"http:\/\/blogs.nd.edu\/devops\/files\/2013\/12\/devops_intensifies1.gif\" width=\"480\" height=\"280\" \/><\/a><\/p>\n<p><em>This rails application was deployed to AWS with a single command. What happens when it runs?\u00a0<\/em><\/p>\n<ol>\n<li><em>A shell script passes a CloudFormation template containing the instance and security group definitions to Boto.<\/em><\/li>\n<li><em>Boto kicks off the stack creation on AWS.<\/em><\/li>\n<li><em>A CloudInit script in the instance definition bootstraps puppet and git, then downloads repos from Github.<\/em><\/li>\n<li><em>Puppet provisions rvm, ruby, and rails.<\/em><\/li>\n<li><em>Finally, CloudInit runs Webrick as a daemon.<\/em><\/li>\n<\/ol>\n<p><em>To do:\u00a0<\/em><\/p>\n<ol>\n<li><em>Reduce CloudInit script to merely bootstrap Puppet.<\/em><\/li>\n<li><em>Let Puppet Master and Capistrano handle instance and app provisioning, respectively.<\/em><\/li>\n<li><em>Get better feedback on errors that may occur during this process.<\/em><\/li>\n<li><em>Introduce an Elastic Load Balancer and set autoscale to 2.<\/em><\/li>\n<li><em>Get a VPN tunnel to campus and access ND data<\/em><\/li>\n<li><em>Work toward automatic app redeployments triggered by git pushes.<\/em><\/li>\n<\/ol>\n<p><em>Onward!\u00a0<\/em><\/p>\n<p><em>Brandon<\/em><\/p>\n<p>_________________________________________________________________________________________________________<\/p>\n<p>A single command, eh? It looks a bit like this:<\/p>\n<pre class=\"code\">.\/run_cloudformation.py us-east-1 MyTestStack cf_template.json brich tagfile<\/pre>\n<p>Alright! \u00a0So let&#8217;s dig into exactly how it works. \u00a0All code can be found <a href=\"https:\/\/oit-svn.cc.nd.edu\/es\/master\/trunk\/devops\/\">here in SVN<\/a>. \u00a0The puppet scripts and app are in my Github account, which I&#8217;ll link as necessary.<\/p>\n<p>It starts with CloudFormation, as described in <a href=\"http:\/\/blogs.nd.edu\/devops\/2013\/12\/02\/cloudformation-and-the-challenge-of-iaas-governance\/\">my post on that topic<\/a>. \u00a0The following template creates a security group, the instance, and a public IP. \u00a0<a href=\"https:\/\/oit-svn.cc.nd.edu\/es\/master\/trunk\/devops\/cloudformation\/rails_instance_no_wait.json\">This template<\/a> is called rails_instance_no_wait.json. \u00a0That&#8217;s because the <a href=\"http:\/\/docs.aws.amazon.com\/AWSCloudFormation\/latest\/UserGuide\/cloudformation-waitcondition-article.html\">Cloud Init tutorial<\/a> has you create this &#8220;wait handle&#8221; that prevents the CF console from showing &#8220;complete&#8221; until the provisioning part is done. \u00a0I&#8217;m doing so much in this step that I removed the wait handle to prevent a timeout. \u00a0As I mention below, this step could\/should be much more streamlined, so later we should be able to reintroduce this.<\/p>\n<pre class=\"code\">{\r\n  \"AWSTemplateFormatVersion\" : \"2010-09-09\",\r\n\r\n  \"Description\" : \"Creates security groups, an Amazon Linux instance, and a public IP for that instance.\",\r\n\r\n  \"Resources\" : {\r\n\r\n    \"SGOpenRailsToWorld\" : {\r\n      \"Type\" : \"AWS::EC2::SecurityGroup\",\r\n      \"Properties\" : {\r\n        \"GroupDescription\" : \"Rails web server access from SA-VPN\",\r\n        \"VpcId\" : \"vpc-1f47507d\",\r\n        \"SecurityGroupIngress\" : [ { \r\n        \t\"IpProtocol\" : \"tcp\", \r\n  \t\t\"CidrIp\" : \"0.0.0.0\/0\",\r\n\t\t\"FromPort\" : \"3000\", \r\n  \t\t\"ToPort\" : \"3000\"\r\n    \t} ]\r\n      }\r\n    },\r\n\r\n    \"BrandonTestInstance\" : {\r\n        \"Type\" : \"AWS::EC2::Instance\",\r\n        \"Properties\" : {\r\n            \"ImageId\" : \"ami-83e4bcea\",\r\n            \"InstanceType\" : \"t1.micro\",\r\n            \"KeyName\" : \"brich_test_key\",\r\n            \"SecurityGroupIds\" : [ \r\n                { \"Ref\" : \"SGWebTrafficInFromCampus\" },\r\n                { \"Ref\" : \"SGSSHInFromSAVPN\" },\r\n                { \"Ref\" : \"SGOpenRailsToWorld\" }\r\n            ],\r\n            \"SubnetId\" : \"subnet-4a73423e\",\r\n            \"Tags\" : [\r\n              {\"Key\" : \"Name\", \"Value\" : \"Brandon Test Instance\" },\r\n              {\"Key\" : \"Application\", \"Value\" : { \"Ref\" : \"AWS::StackId\"} },\r\n              {\"Key\" : \"Network\", \"Value\" : \"Private\" }\r\n            ],\r\n            \"UserData\" : \r\n            { \"Fn::Base64\" : { \"Fn::Join\" : [\"\",[\r\n            \"#!\/bin\/bash -ex\",\"\\n\",\r\n            \"yum -y update\",\"\\n\",\r\n            \"yum -y install puppet\",\"\\n\",\r\n            \"yum -y install subversion\",\"\\n\",\r\n            \"yum -y install git\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/puppet-rvm.git \/usr\/share\/puppet\/modules\/rvm\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/websvc-puppet.git \/tmp\/websvc-puppet\",\"\\n\",\r\n            \"puppet apply \/tmp\/websvc-puppet\/rvm.pp\",\"\\n\",\r\n            \"source \/usr\/local\/rvm\/scripts\/rvm\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/cap-test.git \/home\/ec2-user\/cap-test\",\"\\n\",\r\n            \"cd \/home\/ec2-user\/cap-test\",\"\\n\",\r\n            \"bundle install\",\"\\n\",\r\n            \"rails s -d\",\"\\n\"\r\n ]]}}\r\n       }\r\n    },\r\n\r\n    \"PublicIPForTestInstance\" : {\r\n        \"Type\" : \"AWS::EC2::EIP\",\r\n        \"Properties\" : {\r\n            \"InstanceId\" : { \"Ref\" : \"BrandonTestInstance\" },\r\n            \"Domain\" : \"vpc\"\r\n        }\r\n    }\r\n\r\n  },\r\n\r\n\"Outputs\" : {\r\n    \"BrandonPublicIPAddress\" : {\r\n        \"Value\" : { \"Ref\" : \"PublicIPForTestInstance\" }\r\n    },\r\n\r\n    \"BrandonInstanceId\" : {\r\n        \"Value\" : { \"Ref\" : \"BrandonTestInstance\" }\r\n    }\r\n}\r\n\r\n}<\/pre>\n<p>So we start with a security group. \u00a0This opens port 3000 (the default Rails port) to the world. \u00a0It could just have easily been opened to the campus IP range, the ES-VPN, or something else. \u00a0You&#8217;ll note that I am making reference to an already-existing VPC. \u00a0This is one of those governance things: VPCs and subnets are relatively permanent constructs, so we will just have to use IDs for static architecture like that. \u00a0Note that I have altered the ID for publication.<\/p>\n<p><em>Please also notice that I have omitted an SSH security group! \u00a0Look ma, no hands!<\/em><\/p>\n<pre class=\"code\">    \"SGOpenRailsToWorld\" : {\r\n      \"Type\" : \"AWS::EC2::SecurityGroup\",\r\n      \"Properties\" : {\r\n        \"GroupDescription\" : \"Rails web server access from SA-VPN\",\r\n        \"VpcId\" : \"vpc-1f37ab7d\",\r\n        \"SecurityGroupIngress\" : [ { \r\n        \t\"IpProtocol\" : \"tcp\", \r\n  \t\t\"CidrIp\" : \"0.0.0.0\/0\",\r\n\t\t\"FromPort\" : \"3000\", \r\n  \t\t\"ToPort\" : \"3000\"\r\n    \t} ]\r\n      }\r\n    },<\/pre>\n<p>Next up is the instance itself. \u00a0Its parameters are pretty straightforward:<\/p>\n<ul>\n<li>the ID of the base image we want to use: in this case a 64-bit Amazon Linux box.<\/li>\n<li>the image sizing: t1.micro, one of the least powerful (and therefore cheapest!) instance types<\/li>\n<li>the subnet which will house the instance (again obscured).<\/li>\n<li>the instance key (previously generated and stored on my machine as a pem file).\n<ul>\n<li><em>note that we will never use this key in this demo! we can&#8217;t &#8212; no ssh access!<\/em><\/li>\n<\/ul>\n<\/li>\n<li><em><\/em>tags for the instance: metadata like who created the thing. \u00a0Cloudformation will also add some tags to every resource in the stack.<\/li>\n<li>user data. \u00a0This is the &#8220;Cloud Init&#8221; part, which I will describe in more detail, below.<\/li>\n<\/ul>\n<h3>Bootstrapping with Cloud Init (User Data)<\/h3>\n<p>Much of what I&#8217;m doing with Cloud Init comes from <a href=\"http:\/\/docs.aws.amazon.com\/AWSCloudFormation\/latest\/UserGuide\/cloudformation-waitcondition-article.html\">this Amazon documentation<\/a>. There is a more powerful version of this called <a href=\"http:\/\/docs.aws.amazon.com\/AWSCloudFormation\/latest\/UserGuide\/aws-resource-init.html\">cfn-init<\/a>, as <a href=\"http:\/\/docs.aws.amazon.com\/AWSCloudFormation\/latest\/UserGuide\/aws-resource-init.html\">demonstrated here<\/a>, but in my opinion it&#8217;s overkill. \u00a0Cfn-init looks like it&#8217;s trying to be Puppet-lite, but that&#8217;s why we have actual Puppet. \u00a0The &#8220;user data&#8221; approach is basically just a shell script, and though I have just under a dozen lines here, ideally you&#8217;d have under five: just enough to bootstrap the instance and let better automation tools handle the rest. \u00a0This also lets the instance resource JSON be more reusable and less tied to the app you&#8217;ll deploy on it. \u00a0Anyway, here it is:<\/p>\n<pre class=\"code\">\"UserData\" : \r\n            { \"Fn::Base64\" : { \"Fn::Join\" : [\"\",[\r\n            \"#!\/bin\/bash -ex\",\"\\n\",\r\n            \"yum -y update\",\"\\n\",\r\n            \"yum -y install puppet\",\"\\n\",\r\n            \"yum -y install git\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/puppet-rvm.git \/usr\/share\/puppet\/modules\/rvm\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/websvc-puppet.git \/tmp\/websvc-puppet\",\"\\n\",\r\n            \"puppet apply \/tmp\/websvc-puppet\/rvm.pp\",\"\\n\",\r\n            \"source \/usr\/local\/rvm\/scripts\/rvm\",\"\\n\",\r\n            \"git clone https:\/\/github.com\/catapultsoftworks\/cap-test.git \/home\/ec2-user\/cap-test\",\"\\n\",\r\n            \"cd \/home\/ec2-user\/cap-test\",\"\\n\",\r\n            \"bundle install\",\"\\n\",\r\n            \"rails s -d\",\"\\n\"\r\n ]]}}<\/pre>\n<p>So you can see what&#8217;s happening here. \u00a0We use yum to update itself, then install puppet and git. \u00a0I first clone a <a href=\"https:\/\/github.com\/catapultsoftworks\/puppet-rvm.git \/usr\/share\/puppet\/modules\/rvm\">git repo that installs rvm<\/a>.<\/p>\n<h3>A note about Amazon Linux version numbering.<\/h3>\n<p>Why did I fork this manifest into my own account? \u00a0Well, there is a dependency in there for a curl library, which apparently changed names on centos as some point. \u00a0So there is conditional code in the original manifest that chooses which name to use based on version number. Unfortunately, even though Amazon Linux is rightly recognized as a centos variant, this part fails, because Amazon uses their own version numbering. \u00a0I fixed it, but without being sure how the authors would want to handle this, I avoided a pull request and submitted an issue to them. \u00a0We&#8217;ll see.<\/p>\n<h3>A note about github<\/h3>\n<p>I went to github for two reasons:<\/p>\n<ol>\n<li>It&#8217;s public. \u00a0our SVN server is locked down to campus, and without a VPN tunnel, I can&#8217;t create a security rule to get there<\/li>\n<li>I could easily fork that repo.<\/li>\n<\/ol>\n<p>Let&#8217;s add these to the pile of good reasons to use Github Organizations.<\/p>\n<h3>More Puppet: Set up Rails<\/h3>\n<p>Anyway, with the rvm module installed, we use <a href=\"https:\/\/github.com\/catapultsoftworks\/websvc-puppet.git\">another puppet manifest<\/a> to invoke \/ install it with <strong>puppet apply<\/strong>, the standalone client-side version of puppet. It installs my ruby\/rails versions of choice: 1.93 \/ 2.3.14. \u00a0I also set up bundler and sqlite (so that I can run a default rails app).<\/p>\n<h3>The application<\/h3>\n<p>The next git clone downloads the application. \u00a0<a href=\"https:\/\/github.com\/catapultsoftworks\/cap-test.git\">It&#8217;s a very simple app with one root route<\/a>. It&#8217;s called cap-test because the next thing I want to do is deploy it with capistrano. The only thing to note here is that the <a href=\"https:\/\/github.com\/catapultsoftworks\/cap-test\/blob\/master\/Gemfile\">Gemfile<\/a>\u00a0contains the gem &#8220;therubyracer,&#8221; a javascript runtime. \u00a0I could have satisfied this requirement by installing nodejs, but it looks like a bit of pain since there&#8217;s no yum repo for that. \u00a0This was the simplest solution.<\/p>\n<h3>Starting the server<\/h3>\n<p>No magic here&#8230; I just let the root user that&#8217;s running the provisioning also start up the server. \u00a0It&#8217;s running on port 3000, which is already open to the world, so it&#8217;s now publicly available.<\/p>\n<h3>That public IP<\/h3>\n<p>The CloudFormation template also creates an &#8220;elastic IP&#8221; and assigns it to the instance. \u00a0This is just a public IP generated in AWS&#8217;s space. \u00a0Not sure why it has to be labeled &#8220;elastic.&#8221;<\/p>\n<pre class=\"code\">    \"PublicIPForTestInstance\" : {\r\n        \"Type\" : \"AWS::EC2::EIP\",\r\n        \"Properties\" : {\r\n            \"InstanceId\" : { \"Ref\" : \"BrandonTestInstance\" },\r\n            \"Domain\" : \"vpc\"\r\n        }\r\n    }<\/pre>\n<p>You&#8217;ll also notice the output section of the CF template includes this IP reference. \u00a0This causes the IP to show up in the CloudFormation console under &#8220;output&#8221; and should be something I can return from the command line. \u00a0Speaking of which&#8230;<\/p>\n<h3>Oh yeah, that boto script<\/h3>\n<p>So this thing doesn&#8217;t just run itself. \u00a0You can run it manually through the CloudFormation GUI (nah), use the AWS CLI tools, or use Boto. \u00a0Here&#8217;s the usage on that script:<\/p>\n<pre class=\"code\">\/Users\/brich\/devops\/boto&gt; .\/run_cloudformation.py -h\r\nusage: run_cloudformation.py [-h]\r\n                             region stackName stackFileName creator tagFile\r\n\r\nRun a cloudformation script. Make sure your script contains a detailed description! This script does not currently accept stack input params.\r\n\r\nCreator: Brandon Rich Last Updated: 5 Dec 2013\r\n\r\npositional arguments:\r\n  region         e.g. us-east-1\r\n  stackName      e.g. TeamName_AppName_TEST (must be unique)\r\n  stackFileName  JSON file for your stack. Make sure it validates!\r\n  creator        Your netID. Will be attached to stack as \"creator\" tag.\r\n  tagFile        optional. additional tags for the stack. newline delimited\r\n                 key-value pairs separated by colons. ie team:sfis\r\n                 functional:registrar\r\n\r\noptional arguments:\r\n  -h, --help     show this help message and exit<\/pre>\n<p>You can see what arguments it takes. \u00a0The tags file is optional, but it will always put your netid down as a &#8220;creator&#8221; tag. \u00a0Some key items are not yet implemented:<\/p>\n<ul>\n<li>input arguments. \u00a0supported by CloudFormation, these would let us re-use templates for multiple apps. \u00a0critical for things like passing in datasource passwords to the environment (to keep them out of source control)<\/li>\n<li>event tracking (feedback for status changes as the stack builds)<\/li>\n<li>json \/ aws validation of the template<\/li>\n<\/ul>\n<p>Anyway, here&#8217;s the implementation.<\/p>\n<pre class=\"code\"># read tagfile\r\nlines = [line.strip() for line in open(args.tagFile)]\r\n\r\nprint \"tags: \" \r\nprint \"(creator: \" + args.creator + \")\"\r\n\r\ntagdict = { \"creator\" : args.creator }\r\nfor line in lines:\r\n    keyval = line.split(':')\r\n    key = keyval[0]\r\n    val = keyval[1]\r\n    tagdict[key] = val\r\n    print \"(\" + key + \": \" + val + \")\"\r\n\r\n# aws cloudformation validate-template --template-body file:\/\/path-to-file.json\r\n#result = conn.validate_template( template_body=args.stackFileName, template_url=None )\r\n# example of bad validation (aside from invalid json): referencing a security group that doesn't exist in the file\r\n\r\nwith open (args.stackFileName, \"r\") as stackfile:\r\n    json=stackfile.read().replace('\\n', '')\r\n#print json\r\n\r\ntry:\r\n    stackID = conn.create_stack( \r\n\t\t   stack_name=args.stackName, \r\n\t\t   template_body=json, \r\n\t\t   template_url=\"\", \r\n\t\t   parameters=\"\",\r\n\t\t   notification_arns=\"\",\r\n\t\t   disable_rollback=False,\r\n\t\t   timeout_in_minutes=10,\r\n\t\t   capabilities=None,\r\n\t\t   tags=tagdict\r\n\t\t) \r\n    events = conn.describe_stack_events( stackID, None )\r\n    print events\r\nexcept Exception,e:\r\n    print str(e)<\/pre>\n<p>That&#8217;s it. \u00a0<a href=\"https:\/\/oit-svn.cc.nd.edu\/es\/master\/trunk\/devops\/boto\/run_cloudformation.py\">Here&#8217;s the script for OIT SVN users.<\/a> \u00a0Again, run it like so:<\/p>\n<pre class=\"code\">.\/run_cloudformation.py us-east-1 MyTestStack cf_template.json brich tagfile<\/pre>\n<p>So this took a while to write up in a blog post, but there&#8217;s not much magic here. \u00a0It&#8217;s just connecting pieces together. \u00a0Hopefully, as I tackle those outstanding items from the page replica above, we&#8217;ll start to see some impressive levels of automation!<\/p>\n<p>Onward!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This weekend, I tweeted the public URL of an AWS instance. \u00a0Like most instances during this experimentation phase, it was not meant to live forever, so I&#8217;ll reproduce it here: _________________________________________________________________________________________________________ This rails application was deployed to AWS with a &hellip; <a href=\"https:\/\/sites.nd.edu\/devops\/2013\/12\/09\/a-public-rails-instance-from-a-single-command\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1550,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[65098,65105,65099,65106,65127,65107,65129,29105],"class_list":["post-195","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-aws-2","tag-boto","tag-cloudformation","tag-cloudinit","tag-devops","tag-puppet","tag-rails","tag-ruby"],"_links":{"self":[{"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/posts\/195","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/users\/1550"}],"replies":[{"embeddable":true,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/comments?post=195"}],"version-history":[{"count":16,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/posts\/195\/revisions"}],"predecessor-version":[{"id":217,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/posts\/195\/revisions\/217"}],"wp:attachment":[{"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/media?parent=195"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/categories?post=195"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sites.nd.edu\/devops\/wp-json\/wp\/v2\/tags?post=195"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}