Deploy, manage, and scale an application on Heroku
Heroku is a Platform as a Service (PaaS) provider that enables developers to build, run, and operate applications in the cloud. While you can use Heroku's dashboard or CLI to manage your application resources, using Terraform provides you with several benefits:
Safety and consistency - Defining your infrastructure as code (IaC) reduces risk of human error. IaC enables you to manage similar infrastructure across your environments using a consistent workflow.
Full lifecycle management - Terraform can create, update, and delete tracked resources without requiring you to inspect the dashboard or API to identify those resources.
Graph of relationships - Terraform tracks dependency relationships between resources. For example, since a Heroku formation needs to be associated with an application and a build, Terraform will wait for the application and build to successfully deploy before attempting to create a formation.
In this tutorial, you will use Terraform to manage your Heroku application's lifecycle. First, you will deploy an application and database to Heroku. Then, you will scale and add logging to the application using Terraform.
Prerequisites
To complete this tutorial, you will need:
- the Terraform v0.14+ CLI installed locally
- a Heroku account
Note
This tutorial uses Heroku formation which requires you to use a Heroku account with payment information. If you destroy your resources by the end of the tutorial, you should not be charged. We are not responsible for any charges that may incur.
Generate Heroku authorization token
Terraform uses a Heroku authorization token to authenticate to Heroku and manage your resources.
After signing in to Heroku, go to the Application page of your Heroku Account.
Click on "Create authorization" under the authorization section. Then, enter "Terraform" in the description field and leave the expiration field blank. Click on the "Create" button to create the authorization token.
The dashboard will present the authorization token. Copy this token and export
it as an environment variable named HEROKU_API_KEY
.
$ export HEROKU_API_KEY=
In addition to the authorization token, Terraform needs your Heroku account's
email address. You can find this in your
account settings. Export your account's
email as an environment variable named HEROKU_EMAIL
.
$ export HEROKU_EMAIL=
Clone example repository
In your terminal, clone the example repository. This repository contains a demo application and a complete Terraform configuration that deploys the application and a database to Heroku.
$ git clone https://github.com/hashicorp-education/learn-terraform-heroku
Navigate to the cloned repository.
$ cd learn-terraform-heroku
Note
The example repository contains both the application code and Terraform configuration for demo purposes. In practice, you should store application code and Terraform configuration in separate repositories.
Review configuration
Open versions.tf
, which defines the required Terraform version and provider
versions.
A
terraform
block that specifies the providers required by this configuration and the compatible Terraform versions. This configuration uses the Terraform Heroku provider v4.6.x to interact with Heroku. It also specifies that you must use at least Terraform v0.14+.versions.tf
terraform { required_providers { heroku = { source = "heroku/heroku" version = "~> 4.6.0" } } required_version = ">= 0.14"}
Open main.tf
. This file contains the Terraform configuration that deploys an
application and a database. This file contains:
A
provider
block that configures the Heroku provider. Though you can configure your credentials in this block, it is safer to use theHEROKU_API_KEY
andHEROKU_EMAIL
environment variables. This prevents you from adding your sensitive credentials to the configuration and accidentally pushing them to version control.main.tf
provider "heroku" {}
The
heroku_app.example
resource defines a new Heroku application namedlearn-terraform-heroku
in the US region.main.tf
resource "heroku_app" "example" { name = "learn-terraform-heroku" region = "us"}
The
heroku_addon.postgres
resource defines a new Postgres instance. This resource references theheroku_app.example
resource so Heroku knows to deploy the database in the application.main.tf
resource "heroku_addon" "postgres" { app = heroku_app.example.id plan = "heroku-postgresql:hobby-dev"}
The
heroku_build.example
resource deploys source code to a Heroku application. Like the Postgres resource, this resource setsapp
toheroku_app.example.id
. The resource expects the source code to live in./app
.main.tf
resource "heroku_build" "example" { app = heroku_app.example.id source { path = "./app" }}
Tip
You can specify the source as a URL that responds to a
GET
request with a tarball containing source code. Review theheroku_build
resource documentation for more information.The
app_quantity
variable defines a Terraform variable you can use to set the number of dynos you want in your formation. By defining a variable, you're able to scale your application without changing your configuration.main.tf
variable "app_quantity" { default = 1 description = "Number of dynos in your Heroku formation"}
The
heroku_formation.example
resource defines a formation for your application. This enables you to scale your application over its lifecycle. Notice that this resource uses thedepends_on
meta-argument to define an explicit dependency between the formation and theheroku_build
resource. Terraform will wait for the build resource to complete before creating the formation.main.tf
resource "heroku_formation" "example" { app = heroku_app.example.id type = "web" quantity = var.app_quantity size = "Standard-1x" depends_on = [heroku_build.example]}
Open outputs.tf
, which defines an output value for the deployed application's URL.
outputs.tf
output "app_url" { value = heroku_app.example.web_url description = "Application URL"}
Review demo application
The demo NodeJS application lives in the /app
directory of the repository
you cloned. The application connects to the Postgres database and defines
endpoints that allow you to read and write values from a test table.
The demo application uses the PORT
and DATABASE_URL
environment variables.
The Heroku application adds these environment variables to the application on
deployment.
Apply configuration
Initialize the Terraform configuration.
$ terraform initInitializing the backend... Initializing provider plugins...- Reusing previous version of heroku/heroku from the dependency lock file- Installing heroku/heroku v4.6.0...- Installed heroku/heroku v4.6.0 (signed by a HashiCorp partner, key ID 49ACC74D80C7B012) Partner and community providers are signed by their developers.If you'd like to know more about provider signing, you can read about it here:https://www.terraform.io/docs/cli/plugins/signing.html Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to reviewany changes that are required for your infrastructure. All Terraform commandsshould now work. If you ever set or change modules or backend configuration for Terraform,rerun this command to reinitialize your working directory. If you forget, othercommands will detect it and remind you to do so if necessary.
Next, apply the configuration. Respond yes
to the prompt to confirm.
$ terraform applyTerraform used the selected providers to generate thefollowing execution plan. Resource actions are indicatedwith the following symbols: + create Terraform will perform the following actions: # heroku_addon.postgres will be created + resource "heroku_addon" "postgres" { + app = (known after apply) + config_vars = (known after apply) + id = (known after apply) + name = (known after apply) + plan = "heroku-postgresql:hobby-dev" + provider_id = (known after apply) } ## ... Plan: 4 to add, 0 to change, 0 to destroy. Changes to Outputs: + app_url = (known after apply) Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes heroku_app.example: Creating...heroku_app.example: Creation complete after 7s [id=learn-terraform-heroku]heroku_addon.postgres: Creating...heroku_build.example: Creating...heroku_addon.postgres: Creation complete after 1s [id=8a32cbe5-3b5d-497a-b54a-853367ffa5fe]heroku_build.example: Still creating... [10s elapsed]heroku_build.example: Still creating... [20s elapsed]heroku_build.example: Still creating... [30s elapsed]heroku_build.example: Creation complete after 34s [id=49ff566e-193a-453c-80f3-144da1996b57]heroku_formation.example: Creating...heroku_formation.example: Creation complete after 0s [id=af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe] Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: app_url = "https://learn-terraform-heroku.herokuapp.com/"
Verify provisioned resources
Use cURL to send a request to the app_url
output value to verify Terraform
provisioned the application and database correctly. The -raw
flag removes
surrounding quotes from the output value.
$ curl $(terraform output -raw app_url)Hello World
Now, send a request to the db/seed
path to create the test database table.
$ curl -X POST $(terraform output -raw app_url)db/seedSuccessfully seeded database
Insert items into the database.
$ curl -d '{"terraform":"rocks", "hashicorp":"learn"}' -H "Content-Type: application/json" -X POST $(terraform output -raw app_url)db/upsertAdded data to database
Read the value from the database. The db
endpoint returns all rows in the
test table. The db/query/<KEY>
endpoint returns a specific key from the table.
$ curl $(terraform output -raw app_url)db/query/terraform{"key":"terraform","value":"rocks"}
Update application
The heroku_build
resource uses a checksum of your source code to determine if
the contents of your application have changed. If the checksums are different,
it will trigger a build on the next apply operation.
In app/index.js
, update your application's response.
app/index.js
app.get('/', function (_, res) {- res.send('Hello World\n')+ res.send('Hello Terraform!\n') });
Then, add a lifecycle
block to your heroku_build
resource. This
block allows Terraform to create the new build before destroying
the old version. Lifecycle management allows you to make changes without
interrupting your application uptime.
main.tf
resource "heroku_build" "example" { app = heroku_app.example.id source { path = "./app" } lifecycle { create_before_destroy = true }}
Apply your changes. Enter yes
when prompted to accept your changes.
$ terraform applyheroku_app.example: Refreshing state... [id=learn-terraform-heroku]heroku_addon.postgres: Refreshing state... [id=e4512647-6182-4d67-a451-a56e9aad40db]heroku_build.example: Refreshing state... [id=5a0b0167-9e27-4ccb-8642-ce889224d0af]heroku_formation.example: Refreshing state... [id=d0666a58-41c6-4790-8553-5b07ecf4745d] ## ... ──────────────────────────────────────────────────────── Terraform used the selected providers to generate thefollowing execution plan. Resource actions are indicatedwith the following symbols:-/+ destroy and then create replacement Terraform will perform the following actions: # heroku_build.example must be replaced-/+ resource "heroku_build" "example" { ~ buildpacks = [ - "https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/nodejs.tgz", ] -> (known after apply) ~ id = "5a0b0167-9e27-4ccb-8642-ce889224d0af" -> (known after apply) ~ local_checksum = "SHA256:9c5550a5e3cc4726f075b423bea57ac963ae43a7a53eba4c56e7ba06d7b47c00" -> "SHA256:0739cab672bdac0844a412f16469ddfe09c5cf695853dcb3401a454bdc932b7f" # forces replacement ~ output_stream_url = "https://build-output.heroku.com/streams/ba/bafd93f4-1be4-4141-b23b-f49c0bf66e70/logs/5a/5a0b0167-9e27-4ccb-8642-ce889224d0af.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIQI6BAUWXGR4S77Q%2F20210721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210721T152100Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=dc2f5f00a627240f731bea494a0d82e31727ac67e302d977310d684a77859445" -> (known after apply) ~ release_id = "2ae31be0-ef36-470e-bf7f-911ffc7d947d" -> (known after apply) ~ slug_id = "6aaea84f-8b6e-4da9-aeb6-1d7074e5e5e7" -> (known after apply) ~ stack = "heroku-20" -> (known after apply) ~ status = "succeeded" -> (known after apply) ~ user = [ - { - email = "team-training@hashicorp.com" - id = "9be75c65-bcfe-4e6c-b1ea-29852c2b320c" }, ] -> (known after apply) ~ uuid = "5a0b0167-9e27-4ccb-8642-ce889224d0af" -> (known after apply) # (1 unchanged attribute hidden) ~ source { + checksum = (known after apply) # (1 unchanged attribute hidden) } } Plan: 1 to add, 0 to change, 1 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes heroku_build.example: Destroying... [id=5a0b0167-9e27-4ccb-8642-ce889224d0af]heroku_build.example: Destruction complete after 0sheroku_build.example: Creating...heroku_build.example: Still creating... [10s elapsed]heroku_build.example: Still creating... [20s elapsed]heroku_build.example: Still creating... [30s elapsed]heroku_build.example: Creation complete after 33s [id=f7a800bd-3c0d-43ac-94c8-a589b3cacc9d] Apply complete! Resources: 1 added, 0 changed, 1 destroyed. Outputs: app_url = "https://learn-terraform-heroku.herokuapp.com/"
Use cURL to send a request to the app_url
output value to verify Terraform
deployed the latest changes to your application.
$ curl $(terraform output -raw app_url)Hello Terraform!
Scale application
Scale your application by setting your app_quantity
variable to 2.
Create a new file named terraform.tfvars
with the following contents.
Terraform automatically uses values defined in this file to override any
variable defaults.
terraform.tfvars
app_quantity = 2
Apply your changes. Enter yes
when prompted to accept your changes.
$ terraform applyheroku_app.example: Refreshing state... [id=learn-terraform-heroku]heroku_addon.postgres: Refreshing state... [id=8a32cbe5-3b5d-497a-b54a-853367ffa5fe]heroku_build.example: Refreshing state... [id=49ff566e-193a-453c-80f3-144da1996b57]heroku_formation.example: Refreshing state... [id=af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe] ## ... ──────────────────────────────────────────────────────── Terraform used the selected providers to generate thefollowing execution plan. Resource actions are indicatedwith the following symbols: ~ update in-place Terraform will perform the following actions: # heroku_formation.example will be updated in-place ~ resource "heroku_formation" "example" { id = "af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe" ~ quantity = 1 -> 2 # (3 unchanged attributes hidden) } Plan: 0 to add, 1 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. Enter a value: yes heroku_formation.example: Modifying... [id=af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe]heroku_formation.example: Modifications complete after 1s [id=af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe] Apply complete! Resources: 0 added, 1 changed, 0 destroyed. Outputs: app_url = "https://learn-terraform-heroku.herokuapp.com/"
Open your Heroku dashboard then select the learn-terraform-heroku
application.
Select "Resources" in the top menu. Observe that your application now has two
dynos.
Add logging to application
Add the following resource to main.tf
. This resource adds the Papertrail
resource to your application.
main.tf
resource "heroku_addon" "logging" { app = heroku_app.example.id plan = "papertrail:choklad"}
Apply your changes to add logging to your application. Enter yes
when
prompted to accept your changes.
$ terraform apply ## ... Terraform used the selected providers to generate thefollowing execution plan. Resource actions are indicatedwith the following symbols: + create Terraform will perform the following actions: # heroku_addon.logging will be created + resource "heroku_addon" "logging" { + app = "learn-terraform-heroku" + config_vars = (known after apply) + id = (known after apply) + name = (known after apply) + plan = "papertrail:choklad" + provider_id = (known after apply) } Plan: 1 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. Enter a value: yes heroku_addon.logging: Creating...heroku_addon.logging: Creation complete after 3s [id=304c8928-179b-4a1e-a3f4-35642fe00e4d] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: app_url = "https://learn-terraform-heroku.herokuapp.com/"
Send multiple requests to your application URL to generate logs.
$ for i in `seq 1 5`; do curl $(terraform output -raw app_url); doneHello Terraform!Hello Terraform!Hello Terraform!Hello Terraform!Hello Terraform!
On your application overview page, click on "Papertrail" under the "Installed add-ons" section.
Agree to Papertrail's service agreement then click "Continue". You will find a set of logs for your application.
Clean up resources
Before moving on, destroy the infrastructure you created in this tutorial.
Enter yes
when prompted to destroy your resources.
$ terraform destroy## ... Plan: 0 to add, 0 to change, 5 to destroy. Changes to Outputs: - app_url = "https://learn-terraform-heroku.herokuapp.com/" -> null 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 heroku_addon.postgres: Destroying... [id=8a32cbe5-3b5d-497a-b54a-853367ffa5fe]heroku_formation.example: Destroying... [id=af8bb93e-be1b-4b5b-a5cb-9ae09c5a8ebe]heroku_addon.logging: Destroying... [id=304c8928-179b-4a1e-a3f4-35642fe00e4d]heroku_formation.example: Destruction complete after 0sheroku_build.example: Destroying... [id=49ff566e-193a-453c-80f3-144da1996b57]heroku_build.example: Destruction complete after 0sheroku_addon.postgres: Destruction complete after 1sheroku_addon.logging: Destruction complete after 1sheroku_app.example: Destroying... [id=learn-terraform-heroku]heroku_app.example: Destruction complete after 0s Destroy complete! Resources: 5 destroyed.
Next steps
Over the course of this tutorial, you deployed an application and database to Heroku. Then, you scaled and added logging to the application. Terraform enables you to manage your application's lifecycle, from creating new resources to managing existing ones to destroying ones you no longer need.
For more information on topics covered in this tutorial, check out the following resources.
- Complete the Reuse Configuration with Modules tutorials to learn how to create reusable modules to enable repeatable workflows.
- Visit the Heroku provider documentation to learn more about the Heroku resources and data sources you can manage using Terraform.
- Visit Heroku's getting started with Terraform tutorial for additional examples and Heroku-specific best practices.