Category Archives: Heroku

How I wrote a command-line Ruby program to manage EC2 instances for me

Why I did this

Heroku is great, but not in 100% of cases

When I want to quickly deploy a Rails application, my go-to choice is Heroku. I’m a big fan of the idea that I can just run heroku create and have a production application online in just a matter of seconds.

Unfortunately, Heroku isn’t always a desirable option. If I’m just messing around, I don’t usually want to pay for Heroku features, but I also don’t always want my dynos to fall asleep after 30 minutes like on the free tier. (I’m aware that there are ways around this but I don’t necessarily want to deal with the hassle of all that.)

Also, sometimes I want finer control than what Heroku provides. I want to be “closer to the metal” with the ability to directly manage my EC2 instances, RDS instances, and other AWS services. Sometimes I desire this for cost reasons. Sometimes I just want to learn what I think is the valuable developer skill of knowing how to manage AWS infrastructure.

Unfortunately, using AWS by itself isn’t very easy.

Setting up Rails on bare EC2 is a time-consuming and brain-consuming hassle

Getting a Rails app standing up on AWS is pretty hard and time-consuming. I’m actually not even going to get into Rails-related stuff in this post because even the small task of getting an EC2 instance up and running—without no additional software installed on that instance—is a lot harder than I think it should be, and there’s a lot to discuss and improve just inside that step.

Just to briefly illustrate what a pain in the ass it is to get an EC2 instance launched and to SSH into it, here are the steps. The steps that follow are the command-line steps. I find the AWS GUI console steps roughly equally painful.

1. Use the AWS CLI create-key-pair command to create a key pair. This step is necessary for later when I want to SSH into my instance.

2. Think of a name for the key pair and save it somewhere. Thinking of a name might seem like a trivially small hurdle, but every tiny bit of mental friction adds up. I don’t want to have to think of a name, and I don’t want to have to think about where to put the file (even if that means just remembering that I want to put the key in ~/.ssh, which is the most likely case.

3. Use the run-instances command, using an AMI ID (AMI == Amazon Machine Image) and passing in my key name. Now I have to go look up the run-instances (because I sure as hell don’t remember it) and, look up my AMI ID, and remember what my key name is. (If you don’t know what an AMI ID is, that’s what determines whether the instance will be Ubuntu, Amazon Linux, Windows, etc.)

4. Use the describe-instances command to find out the public DNS name of the instance I just launched. This means I either have to search the JSON response of describe-instances for the PublicDnsName entry or apply a filter. Just like with every AWS CLI command, I’d have to go look up the exact syntax for this.

5. Run the ssh command, passing in my instance’s DNS and the path to my key. This step is probably the easiest, although it took me a long time to commit the exact ssh -i syntax to memory. For the record, the command is ssh -i ~/.ssh/my_key.pem ubuntu@mypublicdns.com. It’s a small pain in the ass to have to look up the public DNS for my instance again and remember whether my EC2 user is going to be ubuntu or ec2-user (it depends on what AMI I used).

My goals for my AWS command-line tool

All this fuckery was a big hassle so I decided to write my own command-line tool to manage EC2 instances. I call the tool Exosuit. You can actually try it out yourself by following these instructions.

There were four specific capabilities I wanted Exosuit to have.

Launch an instance

By running bin/exo launch, it should launch an EC2 instance for me. It should assume I want Ubuntu. It should let me know when the instance is ready, and what its instance ID and public DNS are.

SSH into an instance

I should be able to run bin/exo ssh, get prompted for which instance I want to SSH into, and then get SSH’d into that instance.

List all running instances

I should be able to run bin/exo instances to see all my running instances. It should show the instance ID and public DNS for each.

Terminate instances

I should be able to run bin/exo terminate which will show me all my instance IDs and allow me to select one or more of them for termination.

How I did it

Side note: when I first wrote this, I forgot that the AWS SDK for Ruby existed, so I reinvented some wheels. Whoops. After I wrote this I refactored the project to use AWS SDK instead of shell out to AWS CLI.

For brevity I’ll focus on the bin/exo launch command.

Using the AW CLI run-instances command

The AWS CLI command for launching an instance looks like this:

aws ec2 run-instances \
  --count 1 \
  --image-id ami-05c1fa8df71875112 \
  --instance-type t2.micro \
  --key-name normal-quiet-carrot \
  --profile personal

Hopefully most of these flags are self-explanatory. You might wonder where the key name of normal-quiet-carrot came from. When the bin/exo launch command is run, Exosuit asks “Is there a file defined at .exosuit/config.yml that contains a key pair name and path? If not, create that file, create a new key pair with a random phrase for a name, and save the name and path to that file.”

Here’s what my .exosuit/config.yml looks like:

---
aws_profile_name: personal
key_pair:
  name: normal-quiet-carrot
  path: "~/.ssh/normal-quiet-carrot.pem"

The aws_profile_name is something that I imagine most users aren’t likely to need. I personally happen to have multiple AWS accounts, so it’s necessary for me to send a --profile flag when using AWS CLI commands so AWS knows which account of mine to use. If a profile isn’t specified in .exosuit/config.yml, Exosuit will just leave the --profile flag off and everything will still work fine.

Abstracting the run-instances command

Once I had coded Exosuit to construct a few different AWS CLI commands (e.g. run-instances, terminate-instances), I noticed that things were getting a little repetitive. Most troubling, I had to always remember to include the --profile flag (just as I would if I were typing all this on the command line manually), and I didn’t always remember to do so. In those cases my command would get sent to the wrong account. That’s bad.

So I created an abstraction called AWSCommand. Here’s what a usage of it looks like:

command = AWSCommand.new(
  :run_instances,
  count: 1,
  image_id: IMAGE_ID,
  instance_type: INSTANCE_TYPE,
  key_name: key_pair.name
)

JSON.parse(command.run)

You can probably see the resemblance it bears to the bare run-instances usage. Note the conspicuous absence of the profile flag, which is now automatically included every single time.

Listening for launch success

One of my least favorite things about manually launching EC2 instances is having to check periodically to see when they’ve started running. So I wanted Exosuit to tell me when my EC2 instance was running.

I achieved this by writing a loop that hits AWS once per second, checking the state of my new instance each time.

module Exosuit
  def self.launch_instance
    response = Instance.launch(self.key_pair)
    instance_id = response['Instances'][0]['InstanceId']
    print "Launching instance #{instance_id}..."

    while true
      sleep(1)
      print '.'
      instance = Instance.find(instance_id)

      if instance && instance.running?
        puts
        break
      end
    end

    puts 'Instance is now running'
    puts "Public DNS: #{instance.public_dns_name}"
  end
end

You might wonder what Instance.find and instance.running? do.

The Instance.find method will run the aws ec2 describe-instances command, parse the JSON response, then grab the relevant JSON data for whatever instance_id I passed to it. The return value is an instance of the Instance class.

When an instance of Instance is instantiated, an instance variable gets set (pardon all the “instances”) with all the JSON data for that instance that was returned by the AWS CLI. The instance.running? method simply looks at that JSON data (which has since been converted to a Ruby hash) and checks to see what the value of ['State']['Name'] is.

Here’s an abbreviated version of the Instance class for reference.

module Exosuit
  class Instance
    def initialize(info)
      @info = info
    end

    def state
      @info['State']['Name']
    end

    def running?
      state == 'running'
    end
  end
end

(By the way, all the Exosuit code is available on GitHub if you’d like to take a look.)

Success notification

As you can see from the code a couple snippets above, Exosuit lets me know once my instances has entered a running state. At this point I can run bin/exo ssh, bin/exo instances or bin/exo terminate to mess with my instance(s) as I please.

Demo video

Here’s a small sample of Exosuit in action:

Try it out yourself

If you’d like to try out Exosuit, just visit the Getting Started with Exosuit guide.

If you think this idea is cool and useful, please let me know by opening a GitHub issue for a feature you’d like to see, or tweeting at me, or simply starring the project on GitHub so I can gage interest.

I hope you enjoyed this explanation and I look forward to sharing the next steps I take with this project.

How to Deploy an Angular 2/Rails 5 App to Heroku

Here’s how to deploy an Angular 2 and Rails 5 app to Heroku. There’s more than one possible way this sort of thing could be structured. Before I describe how to set up the deployment I’ll explain how I chose to structure my app and why.

First, my app is a single-page application where the front-end code is completely separate from the back-end API code. The alternative would be to use Rails views and include the JavaScript and CSS in the asset pipeline. I don’t think either way is necessarily better than the other. In this case I chose the SPA way because a) I understand a lot of people are structuring their Angular/Rails apps this way and b) between the two structures, it’s probably the trickier of the two to deploy, and so it’s probably the one that could benefit most from being documented.

I could have chosen to keep my Angular app and my Rails app in two separate repos. In that case the Rails code could have been deployed to Heroku and the Angular code could have been deployed to, say, a static AWS site. What I instead chose to do was to include my Angular app in a subdirectory of the Rails project and host it all on the same Heroku app. The reason I did it this way is because a) front-end and back-end changes are often made at the same time by the same people and b) although it might have been less work initially to host the Angular app and the Rails app in two different places, it’s arguably simpler from a day-to-day workflow standpoint just to have a single way to deploy everything to one place.

The deployment steps below begin with the creation of an Angular/Rails app, so you don’t have to worry about having your own app to deploy. These steps will get you all the way there but you can also clone my example repo if you don’t want to follow the steps manually.

Initializing the App

The first step will be to initialize a new Rails 5 project. I’m calling mine rails_5_angular_2_deployment_example.

For the front-end we’ll use angular2-seed. Clone the repo into a subdirectory called client. (The name client, in this case, is important. If you call it something else, the deployment won’t work.)

git clone git@github.com:mgechev/angular2-seed.git client

We’re just interested in the angular2-seed code. If we keep it as a repository inside our project’s repository, there will be problems. Kill client/.git.

rm -rf client/.git

Now install the Node packages.

cd client
npm install

Before we try to deploy our app we’ll want to make sure it works locally. Run the following command to do a build.

npm run build.prod

This command will create a directory called client/dist/prod. If we want Rails to be able to serve something out of there, we’ll have to tell Rails about it somehow. We can do this by simply symlinking public/ to client/dist/prod/. Rails will look for public/index.html and find client/dist/prod/index.html and we’ll be in business.

cd ..
rm -rf public
ln -s client/dist/prod public

Start the Rails server. If you navigate to localhost:3000 you should see “Howdy! Here’s a list of awesome computer scientists…”

rails server

Creating the Heroku App

The first step is somewhat self-explanatory:

heroku create

We’ll need to use two buildpacks for this deployment. If we were deploying a plain old Rails app, Heroku would auto-detect Ruby and use the heroku/ruby buildpack. We also need a Node buildpack because in order to build our front-end app we need to run a Gulp command and in order to run Gulp commands we need to have Node packages installed.

We can tell Heroku about our two buildpacks like this:

heroku buildpacks:add https://github.com/jasonswett/heroku-buildpack-nodejs
heroku buildpacks:add heroku/ruby

The order matters. If we were to put the Ruby buildpack first and the Node buildpack second, Heroku would give us a single Node dyno which wouldn’t know how to run Ruby. By putting the Ruby buildpack second we get two dynos: a web dyno and a worker dyno, both of which know how to run Ruby.

The reason we’re using my https://github.com/jasonswett/heroku-buildpack-nodejs buildpack instead of the Heroku version is that I needed to modify the buildpack to look for package.json inside of the client directory instead of at the project root.

We need to do one last thing before we can push our code. Modify client/package.json to look like this:

{
  "name": "angular2-seed",
  "version": "0.0.0",
  "description": "Seed for Angular 2 apps",
  "repository": {
    "url": "https://github.com/mgechev/angular2-seed"
  },
  "scripts": {
    "build.dev": "gulp build.dev --color",
    "build.dev.watch": "gulp build.dev.watch --color",
    "build.e2e": "gulp build.e2e --color",
    "build.prod": "gulp build.prod --color",
    "build.test": "gulp build.test --color",
    "build.test.watch": "gulp build.test.watch --color",
    "docs": "npm run gulp -- build.docs --color && npm run gulp -- serve.docs --color",
    "e2e": "protractor",
    "e2e.live": "protractor --elementExplorer",
    "gulp": "gulp",
    "karma": "karma",
    "karma.start": "karma start",
    "postinstall": "typings install && gulp check.versions && npm prune && gulp build.prod",
    "reinstall": "npm cache clean && npm install",
    "serve.coverage": "remap-istanbul -b src/ -i coverage/coverage-final.json -o coverage -t html && npm run gulp -- serve.coverage --color",
    "serve.dev": "gulp serve.dev --color",
    "serve.e2e": "gulp serve.e2e --color",
    "serve.prod": "gulp serve.prod --color",
    "start": "gulp serve.dev --color",
    "tasks.list": "gulp --tasks-simple --color",
    "test": "gulp test --color",
    "webdriver-start": "webdriver-manager start",
    "webdriver-update": "webdriver-manager update"
  },
  "author": "Minko Gechev <mgechev>",
  "license": "MIT",
  "devDependencies": {
  },
  "dependencies": {
    "angular2": "2.0.0-beta.15",
    "es6-module-loader": "^0.17.8",
    "es6-promise": "^3.1.2",
    "es6-shim": "0.35.0",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.2",
    "systemjs": "~0.19.25",
    "zone.js": "^0.6.10",
    "async": "^1.4.2",
    "autoprefixer": "^6.3.3",
    "browser-sync": "^2.11.2",
    "chalk": "^1.1.3",
    "codelyzer": "0.0.12",
    "colorguard": "^1.1.1",
    "connect": "^3.4.1",
    "connect-history-api-fallback": "^1.1.0",
    "connect-livereload": "^0.5.3",
    "cssnano": "^3.5.2",
    "doiuse": "^2.3.0",
    "event-stream": "^3.3.2",
    "express": "~4.13.1",
    "express-history-api-fallback": "^2.0.0",
    "extend": "^3.0.0",
    "gulp": "^3.9.1",
    "gulp-cached": "^1.1.0",
    "gulp-concat": "^2.6.0",
    "gulp-filter": "^4.0.0",
    "gulp-inject": "^4.0.0",
    "gulp-inline-ng2-template": "^1.1.2",
    "gulp-load-plugins": "^1.2.0",
    "gulp-plumber": "~1.1.0",
    "gulp-postcss": "^6.1.0",
    "gulp-shell": "~0.5.2",
    "gulp-sourcemaps": "git+https://github.com/floridoo/gulp-sourcemaps.git#master",
    "gulp-template": "^3.1.0",
    "gulp-tslint": "^4.3.3",
    "gulp-typedoc": "^1.2.1",
    "gulp-typescript": "~2.12.1",
    "gulp-uglify": "^1.5.3",
    "gulp-util": "^3.0.7",
    "gulp-watch": "^4.3.5",
    "is-ci": "^1.0.8",
    "isstream": "^0.1.2",
    "jasmine-core": "~2.4.1",
    "jasmine-spec-reporter": "^2.4.0",
    "karma": "~0.13.22",
    "karma-chrome-launcher": "~0.2.2",
    "karma-coverage": "^0.5.5",
    "karma-ie-launcher": "^0.2.0",
    "karma-jasmine": "~0.3.8",
    "karma-mocha-reporter": "^2.0.0",
    "karma-phantomjs-launcher": "^1.0.0",
    "merge-stream": "^1.0.0",
    "open": "0.0.5",
    "phantomjs-prebuilt": "^2.1.4",
    "postcss-reporter": "^1.3.3",
    "protractor": "^3.0.0",
    "remap-istanbul": "git+https://github.com/SitePen/remap-istanbul.git#master",
    "rimraf": "^2.5.2",
    "run-sequence": "^1.1.0",
    "semver": "^5.1.0",
    "serve-static": "^1.10.2",
    "slash": "~1.0.0",
    "stream-series": "^0.1.1",
    "stylelint": "^5.3.0",
    "stylelint-config-standard": "^5.0.0",
    "systemjs-builder": "^0.15.14",
    "tiny-lr": "^0.2.1",
    "traceur": "^0.0.91",
    "ts-node": "^0.7.1",
    "tslint": "^3.7.0-dev.2",
    "tslint-stylish": "2.1.0-beta",
    "typedoc": "^0.3.12",
    "typescript": "~1.8.10",
    "typings": "^0.7.12",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "yargs": "^4.2.0"
  }
}

We’ve done two things here. First, we added gulp build.prod to the postinstall script. This will force Heroku to do a build as part of the deployment process. Second, we move everything from devDependencies to dependencies. Since it’s a production environment, Node won’t pick up the devDependencies, but we need those.

By the way, why not just build locally, commit the generated code, and push that? You could do that and it would work. The reason I didn’t want to is that it’s not a good idea to commit build artifacts to version control. You end up with a bunch of “changed” files every time you do a build, which not only doesn’t make sense but serves as a distraction. I’ve worked on projects before that commit build artifacts to version control and it has been painful.

After all our changes are committed we can do a push:

git push heroku master

Now open the app in Heroku.

heroku open

You should see the same Angular 2 Seed app in the production environment. Obviously, the Angular app isn’t talking to Rails but it is coexisting with it, and that’s the hard part.

How to Deploy an Angular CLI Webpack Project to Heroku

Angular CLI recently switched from SystemJS to Webpack. This affects the Heroku deployment process.

Here I’ll show you how to initiate an Angular CLI/Webpack project and deploy it to Heroku. (By the way, this is an Angular-only post. I won’t be talking about Rails at all here. Even if you use the Angular/Rails combo I’d recommend deploying an Angular-only app first just so you understand how the process works. If you’re ready for the Rails version, that can be found here.)

First, make sure you have the following versions of the following things installed:

Angular CLI: 1.0.0-beta.11-webpack.8
NPM: 3.10.6
Node: 6.5.0

First let’s initiate the project, which I’ll give the random name of bananas:

$ ng new bananas

When we deploy this, we’ll want to invoke the ng build command after installation. When you run ng build it creates a dist directory with your TypeScript transpiled to JavaScript and all that stuff. It’s a “static” set of files that can be served up as-is.

Because the error-free operation of ng build is critical to our success, let’s make sure we can run it locally before we try to invoke it on Heroku:

$ cd bananas
$ ng build

In all likelihood this did not run error-free for you. You probably got Cannot find global type 'Array'. and a bunch of other similar stuff like these people did.

The way I fixed that was to first have the right Node and NPM versions installed. We’ve already taken care of that at the beginning. The second step is to change your typescript version in package.json from ^2.0.0 to 2.0.0.

Here’s what my package.json looks like after the change:

{
  "name": "bananas",
  "version": "0.0.0",
  "license": "MIT",
  "angular-cli": {},
  "scripts": {
    "start": "ng serve",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "2.0.0-rc.5",
    "@angular/compiler": "2.0.0-rc.5",
    "@angular/core": "2.0.0-rc.5",
    "@angular/forms": "0.3.0",
    "@angular/http": "2.0.0-rc.5",
    "@angular/platform-browser": "2.0.0-rc.5",
    "@angular/platform-browser-dynamic": "2.0.0-rc.5",
    "@angular/router": "3.0.0-rc.1",
    "core-js": "^2.4.0",
    "rxjs": "5.0.0-beta.11",
    "ts-helpers": "^1.1.1",
    "zone.js": "0.6.12"
  },
  "devDependencies": {
    "@types/jasmine": "^2.2.30",
    "angular-cli": "1.0.0-beta.11-webpack.8",
    "codelyzer": "~0.0.26",
    "jasmine-core": "2.4.1",
    "jasmine-spec-reporter": "2.5.0",
    "karma": "0.13.22",
    "karma-chrome-launcher": "0.2.3",
    "karma-jasmine": "0.3.8",
    "karma-remap-istanbul": "^0.2.1",
    "protractor": "4.0.3",
    "ts-node": "1.2.1",
    "tslint": "3.13.0",
    "typescript": "2.0.0"
  }
}

Now do an npm install and ng build again. You should be error-free this time.

$ npm install
$ ng build

Next you’ll need to change the scripts section of package.json as follows:

  "scripts": {
    "start": "http-server",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor",
    "preinstall": "npm install -g http-server",
    "postinstall": "ng build && mv dist/* ."
  },

We did three things: First, we told Heroku to install http-server globally, which we’ll use to serve our application. Second, we told Heroku to run ng build after installation as well as to move everything in the dist directory to the project root. Third, we told Heroku to serve the app by running http-server. (Heroku automatically runs npm start for Node projects.)

We also have to move our devDependencies into dependencies. Heroku won’t pick up anything in devDependencies but we need some of it. Here’s my final package.json:

{
  "name": "bananas",
  "version": "0.0.0",
  "license": "MIT",
  "angular-cli": {},
  "scripts": {
    "start": "http-server",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor",
    "preinstall": "npm install -g http-server",
    "postinstall": "ng build && mv dist/* ."
  },
  "private": true,
  "dependencies": {
    "@angular/common": "2.0.0-rc.5",
    "@angular/compiler": "2.0.0-rc.5",
    "@angular/core": "2.0.0-rc.5",
    "@angular/forms": "0.3.0",
    "@angular/http": "2.0.0-rc.5",
    "@angular/platform-browser": "2.0.0-rc.5",
    "@angular/platform-browser-dynamic": "2.0.0-rc.5",
    "@angular/router": "3.0.0-rc.1",
    "core-js": "^2.4.0",
    "rxjs": "5.0.0-beta.11",
    "ts-helpers": "^1.1.1",
    "zone.js": "0.6.12",
    "@types/jasmine": "^2.2.30",
    "angular-cli": "1.0.0-beta.11-webpack.8",
    "codelyzer": "~0.0.26",
    "jasmine-core": "2.4.1",
    "jasmine-spec-reporter": "2.5.0",
    "karma": "0.13.22",
    "karma-chrome-launcher": "0.2.3",
    "karma-jasmine": "0.3.8",
    "karma-remap-istanbul": "^0.2.1",
    "protractor": "4.0.3",
    "ts-node": "1.2.1",
    "tslint": "3.13.0",
    "typescript": "2.0.0"
  },
  "devDependencies": {
  }
}

Now let’s create the Heroku project. (I’m assuming you already have a Heroku account and Heroku toolbelt set up.)

$ heroku create

Make sure you have all your changes commit and push your code.

$ git push heroku master

When that has finished, run:

$ heroku open

You should see “app works!”.

Page reload problem

There is one problem, though. If your Angular/Rails application has any routes defined, you’ll notice that if you navigate to a route by clicking a link, then refresh the page, the page will no longer work. The reasons for this are nuanced but I explain them in depth in my HTML5 pushState post.

The fix to the problem is pretty easy.

First, add the rack-rewrite gem to your Gemfile.

# Gemfile

gem 'rack-rewrite'

Then bundle install, of course.

$ bundle install

Then you’ll add a redirect rule to your config.ru that looks like this:

# config.ru

# This file is used by Rack-based servers to start the application.

use Rack::Rewrite do
  rewrite %r{^(?!.*(api|\.)).*$}, '/index.html'
end

require ::File.expand_path('../config/environment',  __FILE__)
run Rails.application

After you commit these changes and deploy again you should be good.