Provisioning Heroku apps with Terraform

This article assumes that you have installed Terraform, have setup an account with Heroku, and are familiar with the Heroku platform and the command line.

The aim of this article is to guide you through the process of creating a Terraform configuration for Heroku. The configuration will provision two apps, linked via a pipeline, as well as add-ons from the Marketplace. The final configuration can then be used as a template, which you can then tailor to your requirements - automating the process of provisioning an Heroku infrastructure.

Getting started

Create a file called heroku.tf in a directory of your choice and add the following block.

provider "heroku" {
    version = "~>2.0"
}

This block specifies Heroku as a provider and sets the required version number to ~>2.0. Providers are plugins for Terraform that offer a set of named resources. Named resources are how Terraform interacts with the provider’s API. A configuration can have multiple providers, for example, you may also want to use the Amazon Web Services provider.

Install the provider plugin

On the command line, change directory in the same location as your heroku.tf file and run the command below.

$ terraform init

Running terraform init will create a .terraform directory inside your present working directory and install the plugin to it. You should see something similar to the output below.

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "heroku" (terraform-providers/heroku) 2.3.0...

Terraform has been successfully initialized!

Heroku authentication

In order to interact with the Platform API we need to provide our Heroku account email address and API key. We can supply these details in one of a few ways:

Inside the configuration file

The provider block for Heroku can be given the name and password parameters. The name parameter is your email address and the password is your API key.

provider "heroku" {
    version = "~>2.0"
    name = "$HEROKU_ACCOUNT_EMAIL"
    password = "$HEROKU_API_KEY"
}

Hardcoding these values in the configuration isn’t best practise and should be avoided for security reasons. If these details were to become publicly available, someone could delete all your apps and add-ons 😬.

Using a .netrc file

Create a file called .netrc in your home directory and add the following code into it, replacing the placeholders with the actual values.

# ~/.netrc
machine api.heroku.com
    login <heroku_account_email>
    password <heroku_api_key>

Note: if you have previously logged into the Heroku Toolbelt a .netrc file will already be present. In this case simply append your credentials to it instead.

Using environment variables

The variables can be set on the command line prior to running Terraform commands.

$ export HEROKU_EMAIL=<heroku_account_email>
$ export HEROKU_API_KEY=<heroku_api_key>

These variable will only last for the current session, so you may want to add them to your ~/.profile, ~/.bashrc, or ~/.zshrc file in order for them to persist.

Resources

A resource is a block that describes one or more infrastructure objects. The resource keyword is followed by a string for the resource type (“heroku_app”) and a string for the given local name (“staging”). The local name is given by you and it’s what you use to refer to the block from elsewhere within the configuration.

resource "heroku_app" "staging" {
    name   = "my-staging-app"
    region = "eu"
}

The resource block above specifies a Heroku app called staging, with the required name and region arguments.

Staging and production apps

Add a second resource block to define the production app. Multiple heroku_app resources can be defined within the same configuration file, as long as the given name for each block (of the same type) is different.

# Create a staging app
resource "heroku_app" "staging" {
    name   = "my-app-staging"
    region = "eu"
}
# Create a production app
resource "heroku_app" "production" {
    name   = "my-app-production"
    region = "eu"
}

Provisioning two apps linked by a pipeline

Add the following three blocks to your configuration file to create a pipeline and “couple” the two apps to it.

# Create the pipeline
resource "heroku_pipeline" "pipeline" {
    name = "my-app-pipeline"
}
# Couple the staging app to the pipeline
resource "heroku_pipeline_coupling" "staging" {
    app      = heroku_app.staging.name
    pipeline = heroku_pipeline.pipeline.id
    stage    = "staging"
}
# Couple the production app to the pipeline
resource "heroku_pipeline_coupling" "production" {
    app      = heroku_app.production.name
    pipeline = heroku_pipeline.pipeline.id
    stage    = "production"
}

A resource heroku_pipeline block is used first to create a pipeline with the name “my-app-pipeline”. Two heroku_pipeline_coupling resources are used next to connect the two apps to the pipeline. A heroku_pipeline_coupling has both an app and pipeline parameter, the values of which refer to other resources using dot notation. The dot notation is made up of three parts <resource_type>.<given_name>.<parameter>.

Using Terraform

In this section we are going to use the plan, apply and destroy commands to interact with the Heroku API.

Plan

The plan command creates an execution plan. Execution plans are used to determine what actions are needed to achieve the configuration set out in the .tf file. Use plan to ensure the changes that will be made match what you expect.

$ terraform plan

Running terraform plan will produce a diff, of which there is a snippet below. The presence of a + indicates an addition, a ~ represents a change, and - represents a removal. At this stage you should see a + for all resources.

# heroku_pipeline.pipeline will be created
+ resource "heroku_pipeline" "pipeline" {
    + id   = (known after apply)
    + name = "my-app-pipeline"
}

Apply

The apply command is used to make the changes set out in the configuration file.

$ terraform apply

An output similar to the one plan generates will be displayed in your terminal. This time however, Terraform will pause for input.

Plan: 5 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
    Terraform will perform the actions described above.
    Only 'yes' will be accepted to approve.

Answering yes at this stage will cause the Heroku apps and pipeline to be provisioned. You should see an output like the following.

heroku_app.production: Creating...
heroku_app.staging: Creating...
heroku_pipeline.pipeline: Creating...
heroku_pipeline.pipeline: Creating... complete after 1s
heroku_app.staging: Creation complete after 7s
heroku_pipeline_coupling.staging: Creating...
heroku_app.production: Creation complete after 7s
heroku_pipeline_coupling.production: Creating...
heroku_pipeline_coupling.staging: Creation complete after 2s
heroku_pipeline_coupling.production: Creation complete after 2s

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

If you login to Heroku now you should be able to see the pipeline with the two apps attached. As an alternative, if you have the Heroku Toolbelt installed, try heroku apps and the apps should be listed in the output.

Error name is already taken

You may get an error from the Heroku API informing you that the name of an app is already taken.

Error: Post https://api.heroku.com/apps: Name my-app-staging is already taken

The solution to this is to rename the app or apps within the resource block. Heroku requires all apps on it’s infrastructure to be unique.

Destroy

The destroy command is used to destroy the Terraform-managed infrastructure. In our case this will destroy the two apps and the pipeline that we have defined.

$ terraform destroy

After running terraform destroy you will be shown a diff again, to which you will have to answer yes to accept the changes.

...
# heroku_pipeline.pipeline will be destroyed
    - resource "heroku_pipeline" "pipeline" {
        - id   = "256afcc9-9ce3-4b5e-bb51-3ec92444cb17" -> null
        - name = "my-app-pipeline" -> null
    }
...
Plan: 0 to add, 0 to change, 5 to destroy.

Do you really want to destroy all resources?
    Terraform will destroy all your managed infrastructure, as shown above.
    There is no undo. Only 'yes' will be accepted to confirm.

    Enter a value: yes

Answering yes at this point will start the process of tearing down the infrastructure.

heroku_pipeline_coupling.production: Destroying...
heroku_pipeline_coupling.staging: Destroying...
heroku_pipeline_coupling.production: Destruction complete after 1s
heroku_app.production: Destroying...
heroku_pipeline_coupling.staging: Destruction complete after 1s
heroku_pipeline.pipeline: Destroying...
heroku_app.staging: Destroying...
heroku_pipeline.pipeline: Destruction complete after 0s
heroku_app.production: Destruction complete after 0s
heroku_app.staging: Destruction complete after 0s

Destroy complete! Resources: 5 destroyed.

Once complete the two apps and the pipeline have been destroyed and are no longer accessible via the Heroku website or the Heroku Toolbelt.

Provisioning add-ons

Add-ons from The Heroku Elements Marketplace can be provisioned with the heroku_addon resource. Below is an example of attaching a Heroku Redis instance to the staging app, on the hobby-dev (free) plan.

# Create and attach a Redis resource to the staging app
resource "heroku_addon" "redis-staging" {
    app  = heroku_app.staging.name
    plan = "heroku-redis:hobby-dev"
}

The app parameter is the app which you want to attach the resource to. The value follows the same principles mentioned earlier - resource type, followed by the given name for the resource (app), followed by the parameter to reference (name).

The plan parameter is made up of the add-ons slug and the plan, separated by a colon. These values are listed on each add-ons page on the market place.

Provisioning certain add-ons will require billing details to be attached to the account.

Variables

So far we have hardcoded certain values, such as the name of the apps and the region, that could be replaced with variables. Variables are defined using the variable block, the second parameter is the name of the variable. The description parameter is used for documentation and is displayed on the CLI.

variable "app_name" {
    description = "Name of the Heroku app provisioned"
}
variable "app_region" {
    description = "Region the app is provisioned in"
}

Variables are referenced throughout the configuration, like so

var.app_name
var.app_region

String interpolation can be done using the following syntax

"${var.app_name}-staging"

Using the variables

Update your heroku.tf file to use variables for both the app’s name and region by first declaring the variable blocks at the top of the file.

variable "app_name" {
    description = "Name of the Heroku app provisioned"
}
variable "app_region" {
    description = "Region the app is provisioned in"
}

Then update the heroku_app and heroku_pipeline blocks to use the variables like so.

resource "heroku_app" "staging" {
    name   = "${var.app_name}-staging"
    region = var.app_region
}
resource "heroku_app" "production" {
    name   = "${var.app_name}-production"
    region = var.app_region
}
resource "heroku_pipeline" "pipeline" {
    name = var.app_name
}

String interpolation is used for the app name to include staging and production for the staging and production apps respectively.

Specifying the values

Now when running terraform apply or terraform plan the CLI will stop and ask for the values for both app_name and app_region. Example below.

var.app_name
    Name of the Heroku app provisioned

    Enter a value:

The values can be provided on the command line using the -var option. The -var option can be set multiple times.

$ terraform plan -var "app_name=my-startup" -var "app_region=eu"

A vars file can be provided using the -var-file option. A vars file has the file type .tfvars or .tfvars.json and consists only of variable name assignments.

app_name = "my-app"
app_region = "eu"

If a .json file is used the variables have to be declared using the JSON format.

{
    "app_name": "my-app",
    "app_region": "eu"
}

If in the directory there is a file called .auto.tfvars or .auto.tfvars.json then it will automatically be loaded.

Defaults

The values can also be set in the block and they are defined using the default param. This allows us to set a “default” which can be overwritten via one of the methods mentioned above.

variable "app_region" {
    default = "eu"
    description = "Region the app is provisioned in"
}

Now that your file is using variables try running terraform plan or terraform apply to see the difference and make sure it works. Don’t forget to run terraform destroy if you don’t want the infrastructure to persist.

Conclusion

At this point you should have a Terraform configuration file that provisions two apps as part of a pipeline. If you added any Marketplace add-ons then those too will be provisioned as part of running terraform apply. We used variables to keep the file DRY and provide the flexibility to provision other pipelines using the same file. This file can provide the basis for your own requirements. You may want to specify the buildpack, dyno formation, and any environment variables you need.

Further reading

There is a lot more to Terraform than what was covered above. Below are some links to documentation that will be useful:

I’ve uploaded the full configuration file, with a few additions, to my GitHub profile. Please feel free to download it and configure it to your own needs.