Heroku で Terraform を使用する
最終更新日 2024年05月30日(木)
Table of Contents
Heroku アプリのコレクションの数が増え、複雑さが増すにつれて、インフラストラクチャ全体 (アプリ、アドオン、ドメイン、Private Space など) のデプロイを自動化する機能が益々重要になります。
Hashicorp の Terraform は、幅広い統合化クラウドリソース (Heroku のリソースを含む) を HCL と呼ばれるシンプルな宣言型言語で設定およびデプロイできるようにするツールです。
この記事では、Heroku で Terraform を使用する方法を紹介します。一般的な Heroku のリソースを一般的な Terraform のベストプラクティスと併せて設定するための設定例も用意しています。
Terraform を設定する
最初に、Terraform をダウンロードしてインストールします。
Terraform を使用する前に、設定の現在の状態を保存するバックエンドを検討します。この状態には、既存のすべてのリソースと、リソース間の関係の識別子が含まれます。Terraform は、この状態を使用して、特定のアクション (通常、作成または破棄) を完了するために行う必要がある作業を把握します
Terraform を初めて使用する場合は、デフォルトのローカルバックエンドでファイルに状態が保管されます。実際のプロジェクト用に Terraform を使い始めたら、Heroku Postgres などのリモートのバックエンドを使用して状態の情報を保存する必要が出てきます。
ローカルバックエンド
ローカルバックエンドは、Terraform を立ち上げる最も簡単な方法です。Terraform の設定でバックエンドを指定していない場合は、ローカルバックエンドが使用されます。
このバックエンドにより、ローカルマシン上の terraform.tfstate
ファイルにデプロイの状態が保管されます。terraform.tfstate
ファイルをバージョン管理にチェックインしないでください。このファイルには、秘密鍵などの機密情報が入っています。
ローカルバックエンドは、長期使用や共同作業にはお勧めしません。状態が個人のマシンにのみ保存されるので、他のチームメンバーはアクセスできず、不慮の損失を防ぐためのバックアップも行われません。この問題を解決するために、代わりに Postgres データベースなどのリモートバックエンドを使用できます。
リモートの Postgres バックエンド
Heroku Postgres アドオンと一緒に pg バックエンド を使用して、長期使用や共同作業のためのプロジェクトを設定します。pg バックエンドは、Terraform バージョン 0.12 以降に含まれています。このリモートバックエンドを使用すると、(ローカルバックエンドのように) Terraform を個人貢献者のマシンで実行することも、Heroku アプリとして実行することもできます。Heroku の自動化されたデータベースバックアップとフェイルオーバーにより、Terraform の状態が使用可能になり、ハードウェア障害が発生した場合に復元できます。
pg バックエンドを使用するには、main.tf
(またはその他の) 設定ファイルでそのバックエンドを指定します。
terraform {
backend "pg" {
}
}
次に、Heroku Postgres アドオンを使用して Heroku アプリを作成し、そのアプリの DATABASE_URL
環境設定を pg バックエンドの conn_str
として使用します。
# Pick a unique app name
$ export APP_NAME=my-terraform-backend
# Create the database
$ heroku create $APP_NAME
$ heroku addons:create heroku-postgresql:essential-0 --app $APP_NAME
# On each machine where it's used, initialize Terraform
# with the database credentials
$ export DATABASE_URL=`heroku config:get DATABASE_URL --app $APP_NAME`
$ terraform init -backend-config="conn_str=$DATABASE_URL"
前の例では、Essential-0 Heroku Postgres データベースを使用しています。より重要なユースケースでの最高のパフォーマンスと可用性を確保するには、standard-0
や premium-0
などの Standard 層以上のプランを設定することを検討してください。
詳細な設定のオプションと例については、Terraform の pg バックエンドのドキュメントを参照してください。
その他のリモートバックエンド
この記事では説明しませんが、さまざまなその他のリモートバックエンドを使用できます。
Hashicorp も Remote State Management サービス とプレミアム Terraform Enterprise 製品を提供しています。
Heroku プロバイダーを設定する
完全なドキュメントは、Heroku プロバイダーのドキュメントで参照できます。
設定
Terraform をインストールした後で、主要設定ファイル (main.tf
) を作成して、Heroku プロバイダーを指定します。リモートの pg バックエンドを使用している場合は、このファイルにすでにバックエンドの設定が入っている可能性があります。
terraform {
required_providers {
heroku = {
source = "heroku/heroku"
version = "~> 5.0"
}
}
}
アップグレード
Heroku プロバイダーの最新のリリースバージョンを確認し、バージョンが古い場合は設定を更新できます。
セマンティックバージョニングの手法に従うと、マイナーおよびパッチバージョンのアップグレードには下位互換性があります。新しいメジャーバージョンに移行するには、アップグレードガイドを参照してください。
認証
Terraform で Heroku プロバイダーを使用すると、アプリ、アドオン、およびその他のリソースを作成するために Platform API へのリクエストが作成されます。Platform API へのリクエストには、認証トークンが必要です。
Terraform に使用される認証トークンには、必要なさまざまなアクションを Heroku API で実行するためにグローバルスコープが必要です。Terraform の機能を既存の Heroku アカウントから分離する必要がある場合は、Terraform で使用する専用の新しい Heroku アカウントを作成できます。
認証トークンを取得する
最初に、Heroku CLI を使用して、Terraform で使用する Heroku アカウントにログインしていることを確認します。
$ heroku whoami
ID を切り替える必要がある場合は、次のように、ログアウトしてからログインします。
$ heroku logout
$ heroku login
次に、Heroku CLI を使用して、認証トークンを生成します。--description
パラメータは、それぞれの認証の目的や識別子を示す、ユーザーが判読可能な名前です。
$ heroku authorizations:create --description terraform-my-app
返された Token 値と Heroku アカウントのメールアドレスを Terraform のローカル環境変数として設定します。
$ export HEROKU_API_KEY=<TOKEN> HEROKU_EMAIL=<EMAIL>
環境変数は、使用するそれぞれの新しいターミナル / シェルで、もう一度エクスポートする必要があります。
Heroku CLI を使用して、今後もう一度使用する認証トークンを探します。すべての認証のリストを表示してから、目的の認証の ID のトークンを取得します。
$ heroku authorizations
$ heroku authorizations:info <ID>
初期化
Terraform に有効な認証トークンができたので、プロバイダーを初期化できます。
$ terraform init
pg バックエンドを使用している場合も、初期化中にデータベースの資格情報を設定します(「リモートの Postgres バックエンドを設定する)」を参照してください)。
$ terraform init -backend-config="conn_str=$DATABASE_URL"
init
が正常に完了したら、provider
が変わった場合 (資格情報のローテーションなど)、または新しいコンピュータに Terraform を設定する場合以外は、再度この処理を実行する必要はありません。
空のアプリをプロビジョニングする
Terraform のリソースに関する完全なリファレンスは、Heroku プロバイダーのドキュメントで参照できます。
アプリのリソースを追加する
Heroku アプリのリソースを main.tf
ファイルに追加します。
variable "example_app_name" {
description = "Name of the Heroku app provisioned as an example"
}
resource "heroku_app" "example" {
name = var.example_app_name
region = "us"
}
このファイルにより、terraform apply
コマンドに指定する名前を使用して空の Heroku アプリをプロビジョニングするよう、Terraform に指示します。
アプリの Heroku 名とは別に、この特定のリソース用の Terraform の識別子は heroku_app.example
、リソースタイプ、およびリソースの名前です。この識別子は、設定や、インポートまたは状態の表示などの操作で変数として使用される可能性があります。
プランと適用
terraform apply
コマンドを使用して、main.tf
に保存した設定を適用します。
sushi
をアプリの一意の名前に置き換えます。
$ terraform apply -var example_app_name=sushi
上記のコマンドを 2 つのコマンドに分割して、プランを確認してから正確に適用できるようにします。
$ terraform plan -var example_app_name=sushi -out=current.tfplan
$ terraform apply current.tfplan
Terraform のコアワークフローについての詳細を参照してください。
apply
が正常に完了すると、Terraform によって作成されたリソースが Terraform の認証トークンに関連付けられている Heroku アカウントに提示されます。
Terraform の現在の状態を表示して、作成されているものを確認します。
$ terraform show
ローカルバックエンドを使用する場合、terraform show
の出力は terraform.tfstate
ファイルの内容に基づきます。リモートバックエンドを使用する場合、この出力はバックエンドの状態ストアの内容に基づきます。
コードをアプリにデプロイする
Terraform を使用して「空のアプリをプロビジョニングする」でプロビジョニングした空のアプリケーションにコードをデプロイするには、次のコードに合わせて main.tf
を更新します。
terraform {
required_providers {
heroku = {
source = "heroku/heroku"
version = "~> 5.0"
}
}
}
variable "example_app_name" {
description = "Name of the Heroku app provisioned as an example"
}
resource "heroku_app" "example" {
name = var.example_app_name
region = "us"
}
# Build code & release to the app
resource "heroku_build" "example" {
app_id = heroku_app.example.id
buildpacks = ["https://github.com/mars/create-react-app-buildpack.git"]
source {
url = "https://github.com/mars/cra-example-app/archive/v2.1.1.tar.gz"
version = "2.1.1"
}
}
# Launch the app's web process by scaling-up
resource "heroku_formation" "example" {
app_id = heroku_app.example.id
type = "web"
quantity = 1
size = "Standard-1x"
depends_on = [heroku_build.example]
}
output "example_app_url" {
value = heroku_app.example.web_url
}
設定を適用し、再びアプリ名を入力変数として渡します。
sushi
を自分のアプリ名に置き換えます。
$ terraform apply -var example_app_name=sushi
terraform apply
が正常に完了したら、設定の出力として入手できる Heroku アプリの URL にアクセスします。
$ terraform output example_app_url
サンプルを残しておく必要がない場合は、Terraform を使用してすべて消すことができます。
$ terraform destroy -var example_app_name=sushi
理解を深める
下の「例」のセクションで、複雑なアーキテクチャのプロビジョニングに使用できる本格的な設定を示しています。このセクションでは、Terraform で行うことができるその他の作業を紹介するために、よりシンプルな一部分を示しています。
アプリとアドオンを作成する
この部分では、特定の Heroku チームと Heroku リージョンにアプリリソースとアドオンリソースを作成しています。
variable "heroku_team" {
description = "Name of the Team (must already exist)"
}
resource "heroku_app" "example" {
name = "${var.heroku_team}-example"
region = "us"
organization {
name = var.heroku_team
}
}
resource "heroku_addon" "papertrail_example" {
app_id = heroku_app.example.id
plan = "papertrail:choklad"
}
この例のアプリの名前には、接頭辞としてチームの名前が使用されています。一貫してリソース名に接頭辞を付けると、Terraform によってプロビジョニングされたリソースの追跡がより簡単になります。
アプリをスケールする
この部分のスケールでは、Formation リソースを使用して、既存のアプリをスケールしています。アプリが正常に起動するまで待機するために、ローカルコマンド (Provisioner のヘルスチェック) も実行しています。
resource "heroku_formation" "example" {
app_id = heroku_app.example.id
type = "web"
quantity = 2
size = "Standard-1x"
depends_on = [heroku_app_release.example]
provisioner "local-exec" {
command = "./bin/health-check ${heroku_app.example.web_url}"
}
}
Private Space を作成する
この部分では、Private Space リソースを作成して、AWS リソースと同じリージョンに配置されるようにしています。
variable "heroku_enterprise_team" {
description = "Name of the Enterprise Team (must already exist)"
}
variable "heroku_private_space" {
description = "Name of the Private Space"
}
variable "aws_region" {
description = "Amazon Web Services region"
default = "us-east-1"
}
variable "aws_to_heroku_private_region" {
default = {
"eu-west-1" = "dublin"
"eu-central-1" = "frankfurt"
"eu-west-2" = "london"
"ca-central-1" = "montreal"
"ap-south-1" = "mumbai"
"us-west-2" = "oregon"
"ap-southeast-1" = "singapore"
"ap-southeast-2" = "sydney"
"ap-northeast-1" = "tokyo"
"us-east-1" = "virginia"
}
}
resource "heroku_space" "example" {
name = var.heroku_private_space
organization = var.heroku_enterprise_team
region = lookup(var.aws_to_heroku_private_region, var.aws_region)
}
Google Cloud Platform への VPN 接続を作成する
この設定の一部では、Google Cloud VPC ネットワークを使用した Heroku Private Space の VPN 接続 を設定します (完全な Terraform の例)を参照してください)。
variable "heroku_vpn" {
description = "Name of the Heroku VPN connection"
}
module "heroku_vpn_gcp" {
source = "github.com/heroku-examples/terraform-heroku-vpn-gcp"
// (config details omitted)
}
resource "heroku_space_vpn_connection" "google" {
name = var.heroku_vpn
space = heroku_space.example.id
public_ip = module.heroku_vpn_gcp.google_vpn_ip
routable_cidrs = ["${module.heroku_vpn_gcp.google_cidr_block}"]
}
例
次の例では、Heroku のリソースに加え、Amazon AWS や Google Cloud Platform のリソースを設定する、詳細な Terraform 設定を示しています。
ベストプラクティス
設定がずれないように注意する
Terraform で管理されているリソースを Heroku Dashboard または Heroku CLI を使用して変更しないでください。Terraform の設定が適用された後に Terraform の外部からリソースを変更すると、Terraform が自身の設定と同期されていない状態になります。
dyno のスケーリング、環境設定の設定、およびアドオンの変更は、すべて Terraform の設定をアップデートして再適用することによって行う必要があります。そうしないと、設定の違いにより、ずれてしまった値を (新しいリソースに) インポートするか手動で状態をアップデートするまで、それ以上の適用または破棄ができなくなります。
詳細は、「Detecting and Managing Drift with Terraform」(Terraform を使用したずれの検出と管理) を参照してください。
接頭辞に一貫した名前を使用する
Terraform で管理されているリソースを Heroku Dashboard または Heroku CLI で表示すると、リソースの相互関係がわかりにくくなる可能性があります。各設定内でリソース名に一貫した接頭辞を付けると、この関係性がかなり把握しやすくなります。
たとえば、次のように prefix
入力変数を使用します。
variable "prefix" {
description = "High-level name of this configuration, used as a resource name prefix"
type = "string"
}
resource "heroku_app" "example-1" {
name = "${var.prefix}-example-1"
region = "us"
}
resource "heroku_app" "example-2" {
name = "${var.prefix}-example-2"
region = "us"
}
Terraform の設定ごとに 1 つの Heroku チームを使用する
Terraform 化されたリソースの管理の操作性を改善するために、Terraform の設定ごとに専用の Heroku チームを使用します。そうすると、専用の Heroku チームに Terraform でプロビジョニングされたものがすべて格納されます。入力変数としてチーム名を設定してから、その変数を使用して、設定内で各リソースにチームを設定します。
variable "heroku_team_name" {
description = "Name of the Heroku Team owning this complete deployment."
type = "string"
}
resource "heroku_app" "example" {
name = "example"
region = "us"
organization = {
name = var.heroku_team_name
}
}
Provisioner のヘルスチェックを使用する
Terraform で作成されたリソースが、プロバイダーでは準備完了とされているのに、実際には「準備完了」ではないことがあります。このよい例が、HTTP ステータス 200 でリクエストに応答するまで準備完了とは見なされない Web アプリです。
Terraform では、この問題を Provisioner で解決します。Provisioner は、コマンドが失敗した場合に、Terraform でリソースの作成を待機し、デフォルトで失敗させる任意のコマンドです。
resource "heroku_formation" "sushi" {
app_id = heroku_app.sushi.id
type = "web"
quantity = 2
size = "Standard-1x"
depends_on = [heroku_app_release.sushi]
provisioner "local-exec" {
command = "./bin/health-check ${heroku_app.sushi.web_url}"
}
}
bin/health-check
スクリプトの例を次に示します。
#!/bin/bash
# Check the health of the web service every thirty-seconds
# for up to ten minutes, until it responds with HTTP status 200.
fail_count=1
while true
do
http_status=$(curl --write-out %{http_code} --silent --output /dev/null $1)
if [ "$http_status" -eq "200" ]; then
echo "$(date -u) health check succeeded to $1"
exit 0
else
if [ "$fail_count" -eq "21" ]; then
echo "$(date -u) health check failed (status $http_status) to $1"
exit 2
else
echo "$(date -u) health check ${fail_count}/20 to $1"
sleep 30
fail_count=$[$fail_count +1]
fi
fi
done
インフラストラクチャと運用のリソースを設定する
Terraform を使用して、インフラストラクチャと運用のリソースの組み合わせを設定できます。
インフラストラクチャリソースには、次のようなものがあります。
- アプリ
- アドオン
- ドメイン
- パイプライン
- Private Space
これらのリソースがプロビジョニングされるときには、コードはまだ Heroku で稼働していません。コードは、引き続き、開発者がプラットフォームを通じて、またはTerraform で運用リソースをプロビジョニングすることで、デプロイして起動する必要があります。
運用リソースには、次のようなものがあります。
- ビルド
- Slug
- アプリのリリース
- アプリの formation (つまり、dyno のスケール)
これらのリソースをプロビジョニングすると、Terraform でアプリのコードを直接デプロイして、完全なシステムを運用できます。これにより、複数のアプリを同時にリリースしたり、問題が発生した場合に共通のリリースロールバックを取りまとめたりすることができるようになります。
運用の設定に Terraform を使用している場合は、設定のずれが生じやすくなりますので注意してください。