Deploy your system with Slack

Our two-person team at Penny deploys over five times a day. Because we’re so small, we lack most of the safeguards that more mature systems have, like extensive tests and feature branches. In fact, we commit directly to master, and we run our few end to end tests against master with CI. Our goal isn’t to catch all the bugs or test all of our code, but to keep stability high with the minimum possible effort. For where our product is, it’s counterproductive to invest in complex toolchains or insist on 100% test coverage.

You don't have to catch all bugs or test all code—keep stability high with minimum possible effort.

Therefore, it’s even more critical than usual to deploy quickly and easily. It may sound nonintuitive to compensate for fewer safeguards by deploying more aggressively, but it’s actually much better than deploying infrequently because it accelerates the feedback loop between writing and testing code—after all, your code will always be running in production. If an error occurs, you’ll know about it right after you’ve written the code rather than after a later deploy when you’ve moved on to other work. It also reduces the set of commits to search through when you encounter a bug, because any bug is likely to be caused by the one or two commits that were most recently deployed. Though the specific workflows might differ between small and large teams, having an easy deploy is always a plus because it removes development friction.

We’ve implemented our deploy system using Slack’s custom commands integration. Slack is a brilliant channel for making deploys easy because it’s always open, easy to use, and obviates building a dashboard with a button on it. You can even post results back to Slack, where they’re automatically logged. Here’s how we did it:

1. Configure a slash command

Head here to configure a slash command for /deploy. You’ll need an publicly accessible HTTP(S) endpoint to hit, which will depend on how your system is configured. You’ll also receive a token, which Slack uses to authenticate itself to you so that random people can’t call your deploy endpoint.

2. Create HTTP endpoint

This will depend on your system and setup. We’re running Sinatra on Rack, so we mounted a listener on our deploy route.

class DeployListener
  def call(env)
    begin
      req = Rack::Request.new(env)
      if req.params['token'] != '<Slack token from configuration>'
        raise 'Not allowed.'
      end

      # Deploy code goes here.
      # For a Ruby deployment, it may be as easy as system('git pull && bundle install && <restart webserver>')

      [200, {}, 'Deploy started.']
    rescue StandardError => e
      [500, {}, e.inspect]
    end
  end
end

map '/api/deploy' do
  run DeployListener.new
end

So our endpoint for Slack is https://<server>/api/deploy.

3. Run your deploy

At this point, you’ll add the deploy code to your listener, or if you have a deploy script already written, call it from the listener. There’s one gotcha here: if you run the listener in the same process as the rest of your system’s code and restart the system during your deploy, you may accidentally have the listener kill its own process, thus preventing it from bringing itself back up. We solve that by forking the process and running the deploy script in the child process. When the child restarts the system, the parent will be killed and restarted, which usually won’t affect the child. Then the child will exit quietly.

Our final integration:

class DeployListener
  def call(env)
    begin
      req = Rack::Request.new(env)
      if req.params['token'] != '<Slack token from configuration>'
        raise 'Not allowed.'
      end

      fork do
        system('(cd /home/ubuntu/penny && ruby deploy.rb) >> /home/ubuntu/slack-deploy.log 2>&1')
      end

      [200, {}, 'Deploy started.']
    rescue StandardError => e
      [500, {}, e.inspect]
    end
  end
end

map '/api/deploy' do
  run DeployListener.new
end

where deploy.rb is our custom deploy script. Note that your deploy script might work if run via the shell, but might barf when run in the context of the listener process because it has a different PATH and environment. Piping output to a file is useful for debugging this issue.

4. Profit

This integration is primitive and a little hacky, but it does what we need it to do. As a two-person team, we’re hungry for effort-amplifying tools like this one that ease our work and move more quickly. Not to mention that you can deploy from your phone, which has saved us a few times already. If you’re interested in what we’re deploying, pop over to https://www.pennyapp.io to take a look :)