No More Seat Costs: Semaphore Plans Just Got Better!

    17 Feb 2020 · Software Engineering

    Continuous Deployment of an Elixir Phoenix Application to Heroku with Semaphore

    6 min read
    Contents

    This tutorial was initially published on DailyDrip. Read the original post here.

    Continuous Deployment to Heroku

    In the previous tutorial, we set up continuous integration for Firestorm via Semaphore. In this tutorial, we’ll deploy our application to Heroku, and then set up Semaphore to deploy each time the tests pass on the master branch. Let’s get started.

    Project

    We’re starting with the dailydrip/firestorm repo tagged before this episode.

    We’ve already signed up for Heroku and Semaphore and installed the Heroku Toolbelt, which is Heroku’s CLI.

    Heroku created the concept of buildpacks, which are a way to tell Heroku how to set up and run your application. We’ll be using the Heroku Elixir Buildpack.

    We’ll create an app named firestorm-forum, specifying the buildpack:

    heroku create firestorm-forum --buildpack "https://github.com/HashNuke/heroku-buildpack-elixir.git"
    

    We need to add another buildpack, to handle the static compilation for our frontend assets.

    heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.git
    

    We need to tweak our configuration a little to fetch some environment variables from Heroku:

    vim config/prod.exs
    
    # We'll enable SSL and force its use, and fetch the SECRET_KEY_BASE environment
    # variable.
    config :firestorm_web, FirestormWeb.Web.Endpoint,
      # ...
      url: [scheme: "https", host: "firestorm-forum.herokuapp.com", port: 443],
      force_ssl: [rewrite_on: [:x_forwarded_proto]],
      secret_key_base: System.get_env("SECRET_KEY_BASE")
    # ...
    # We'll use the POOL_SIZE var and let Heroku tell us how to talk to our database
    config :firestorm_web, FirestormWeb.Repo,
      adapter: Ecto.Adapters.Postgres,
      url: System.get_env("DATABASE_URL"),
      pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
      ssl: true
    # and we don't need a prod secrets file so we can remove that
    

    We’re not using Phoenix Channels yet, but we will in the future. We need to reduce the timeout time for the websocket transport so that connections are closed before Heroku’s 55 second timeout:

    vim lib/firestorm_web/web/channels/user_socket.ex
    
    defmodule FirestormWeb.Web.UserSocket do
      # ...
      transport :websocket, Phoenix.Transports.WebSocket,
        timeout: 45_000
      # ...
    end
    

    Finally, we’ll create a Procfile. This is what Heroku uses to determine how to start your application:

    vim Procfile
    
    web: MIX_ENV=prod mix phx.server
    

    Now, let’s create our database on Heroku:

    heroku addons:create heroku-postgresql:hobby-dev
    

    And we’ll configure its pool size:

    heroku config:set POOL_SIZE=18
    

    We get 20 connections to our database on the hobby tier, so we’ll configure it to be slightly below that number so we have a couple of spare connections to use for things like migrations and mix tasks.

    When we do run a mix task, we can limit the number of connections it attempts to create to make sure we don’t exceed the headroom:

    # example:
    # heroku run "POOL_SIZE=2 mix ecto.migrate"
    

    Let’s create the secret key and set our Heroku environment variable:

    mix phoenix.gen.secret
    # copy its output
    
    heroku config:set SECRET_KEY_BASE="the_previous_output"
    

    We have a few more environment variables that we need to make available to our application as well. We can configure this by creating an elixir_buildpack.config file:

    vim elixir_buildpack.config
    
    config_vars_to_export=(AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_S3_BUCKET AWS_S3_REGION GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET)
    

    Some of these env vars are secret, so we’ll set them off-screen. Now, we’re ready to deploy. Let’s add the files we changed and push it up to our repo:

    git checkout -b feature/episode_010.3
    git add config/prod.exs lib/firestorm_web/web/channels/user_socket.ex elixir_buildpack.config
    git commit -m"Add Heroku configuration"
    git push origin feature/episode_010.3
    

    Now we can deploy to Heroku. Normally you would just push master up to Heroku, but we’ll push this feature branch to Heroku’s master branch instead:

    git push heroku feature/episode_010.3:master
    

    On a Phoenix application that uses Brunch, this would be sufficient. We’re using webpack though, so there’s a little more work that has to go into it. To get Heroku to build our assets with webpack, we can create a compile script that the buildpack will use:

    vim compile
    
    #!/bin/bash
    
    NODE_ENV=production ./node_modules/.bin/webpack -p
    cd ..
    mix phx.digest
    
    chmod +x compile
    git add .
    git commit -m"Add custom compile script to handle webpack"
    git push origin feature/episode_010.3
    git push heroku feature/episode_010.3:master
    

    If we push this, it’s still not quite enough – there’s an error when uglify tries to run. Let’s add a .babelrc for our assets:

    vim assets/.babelrc
    
    {
      "presets": [
        ["es2015", { "modules": false }]
      ]
    }
    
    git add .
    git commit -m"Add babelrc for assets"
    git push origin feature/episode_010.3
    git push heroku feature/episode_010.3:master
    

    With this, everything pushes up and builds successfully. If we try to visit the site, it won’t work though. We can check the logs to find out why:

    heroku logs
    

    We haven’t run our migrations. Let’s do that:

    heroku run "POOL_SIZE=2 mix ecto.migrate"
    heroku ps:restart
    

    Now if we visit the app, it works. Mostly. We still haven’t setup the frontend to hit the deployed URL, and we’d really like to set up continuous deployment so it gets deployed each time we push.

    First, we’ll configure the frontend to hit this API:

    vim assets/config/production.js
    
    module.exports = {
      apiBaseUrl: "https://firestorm-forum.herokuapp.com/api/v1/"
    };
    

    Before we push this, we’ll set up semaphore for continuous deployment.

    First, we’ll click Set up deployment on semaphore for the project:

    Set Up Deployment on Semaphore

    We’ll click ‘Heroku’:

    Select Deploy with Heroku on Semaphore

    We’ll pick automatic deployment:

    Deploy Automatically using Semaphore

    Then we’ll pick the branch we want to deploy from.

    Next, we have to paste my Heroku API key. Then, we pick the application to deploy and complete setup.

    We can also modify the deployment steps to add a migration step after each deploy:

    heroku run --exit-code -- MIX_ENV=prod POOL_SIZE=2 mix ecto.migrate
    

    Now we can push the js config change up, just to our github this time – we’ll let Semaphore handle the Heroku deployment.

    git add .
    git commit -m"Set up js production url"
    git push origin feature/episode_010.3
    

    Once the tests pass, it will deploy to Heroku for us. And that’s it!

    Summary

    Today we deployed Firestorm to Heroku and set up Continuous Deployment via Semaphore.

    If you’d like to sign up for Semaphore, you can use the coupon code DAILYDRIP30 for a 30% discount for three months. The coupon expires on August 31, 2017.

    Feel free to leave any comments or questions you may have in the comment section below.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Josh was a CTO for over a decade, building software for small businesses, startups, and government agencies. In the last four years, he realized that functional programming was in fact amazing, and has been eager to help people build software better.