terraformとStripeを使って、SaaSのプランごとの料金や機能・権限をコードで管理する


この記事では、SaaSの料金や機能リストの管理をコードで管理する方法を紹介します。[terraform](https://www.terraform.io/)とコミュニティプラグインである[Stripe Provider](https://registry.terraform.io/providers/lukasaron/stripe/latest/docs)を使った料金や機能リストの設定方法とデプロイの仕方、そしてエラーへの対処方法について簡単に解説します。テスト環境と本番環境での設定を統一させたい方や、料金や機能に関する設定についてもCI / CDを利用したフローでデプロイされたい方は、ぜひお読みください。

:::note
[terraform Advent Calendar 2024](https://qiita.com/advent-calendar/2024) 11日目のポストです
:::

## 料金や機能リストをコードで管理する

Stripeには、開発やテストに利用するサンドボックス環境と実際の決済処理を行うLive環境があります。アプリケーションがどの環境を利用するかは、Stripe APIを呼び出す際のAPIキーを利用したい環境のものに差し替えるだけで対応できます。

商品や料金情報、Webhook APIの設定などを複数の環境間でデータを同期したい場合、一般的にはダッシュボードで都度操作を行うか、Stripe CLIまたはAPIを利用して設定を反映するスクリプトを作成します。しかし手動操作による設定ミスや同期漏れ、スクリプトの保守管理コストなどを考えると、既存のコード管理ツールを利用できるのが理想的です。

今回はコードでStripe上のデータをコードで管理するためのツールとして、コミュニティプラグインである[Stripe Provider](https://registry.terraform.io/providers/lukasaron/stripe/latest/docs)を紹介します。これは[Lukas Aron氏](https://lukasaron.com/)によって作成・保守されているコミュニティプラグインで、terraformで定義した設定をStripe API経由で反映するためのterraform Providerです。

https://github.com/lukasaron/terraform-provider-stripe

:::note
今回の記事で紹介した定義や設定方法をまとめたサンプルをGitHubにて公開しています。動かしながら試されたい方は、こちらも一緒にご覧ください。
https://github.com/hideokamoto-stripe/example-stripe-terraform-plan-management
:::

## terraformとStripe Providerをセットアップする

terraformのStripe Providerを利用するには、まずterraformのセットアップが必要です。[terraformのドキュメント](https://developer.hashicorp.com/terraform/install)を参考にterraformコマンドを利用する準備を行いましょう。macOSでHomebrewを利用している場合は、次のコマンドでインストールできます。

https://developer.hashicorp.com/terraform/install

```bash
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
```

### Stripe Providerのセットアップ方法

Stripe Providerを利用するには、`tf`ファイルにProviderの設定を行います。`main.tf`を作成して定義を行いましょう。[Providerのドキュメント](https://registry.terraform.io/providers/lukasaron/stripe/latest/docs)を参考に定義を行いましょう。

```tf:main.tf
terraform {
  required_providers {
    stripe = {
      source = "lukasaron/stripe"
    }
  }
}

provider "stripe" {
  # Configuration options
}
```

`terraform`と`provider "stripe"`の定義が終われば、`terraform init`を実行します。これでProviderのインストールなどが実行され、Stripe Providerを使ったリソース定義ができるようになります。`terraform init`が成功すれば、次のようなメッセージが表示されます。

```
Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should 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, other
commands will detect it and remind you to do so if necessary.
```

### Stripe APIキーは環境変数にて設定しよう


terraformのStripe Providerは、Stripe APIを利用してリソース管理を行います。そのためこのProviderを利用するにはStripeのシークレットAPIキーまたは制限付きAPIキーが必要です。StripeのシークレットAPIキーや制限付きAPIキーは、顧客データやビジネス情報などにアクセスできるため、ソースコードに直接記述することはおすすめできません。また、利用する環境（サンドボックス・Liveなど）によってAPIキーを変更する必要もあるため、環境変数として設定しましょう。terraformでは、`TF_VAR`からはじまる環境変数として設定すると、簡単にAPIキーをインポートできます。

```
export TF_VAR_stripe_api_key=sk_test_xxx
```

インポートしたい変数は、`variable`で変数定義しましょう。ここで定義する変数名は、`TF_VAR_`を除外した名前にします。そのため`TF_VAR_stripe_api_key`で`export`した場合は、`variable "stripe_api_key"`と定義しましょう。

```diff_terraform
+variable "stripe_api_key" {
+  type        = string
+}
```

最後に定義した変数をStripe Providerの`api_key`に設定しましょう。これでStripe Prodiverが任意のStripeアカウントのリソースを管理する準備ができました。

```diff_terraform
provider "stripe" {
+  api_key = var.stripe_api_key
}
```

## SaaSの料金プランをコードで管理しよう

セットアップができましたので、早速コードで料金プランの定義をはじめましょう。[Providerのドキュメント](https://registry.terraform.io/providers/lukasaron/stripe/latest/docs/resources/stripe_price)に対応しているリソースのリストと、設定できるパラメータのリストがまとまっていますので、これを参考に値をカスタマイズしてください。

```tf
resource "stripe_product" "standard_product" {
  name        = "Standard license"
  unit_label  = "seat"
  description = "Awesome SaaS license"
  active      = true
}

resource "stripe_price" "standard_monthly_price" {
    product     = stripe_product.standard_product.id
    currency    = "jpy"
    unit_amount = 9800
    lookup_key  = "standard_monthly"
    recurring {
        interval      = "month"
        interval_count = 1
    }
}
```

:::note
### 価格改定に備えて、`lookup_key`を設定しよう

必須の値ではありませんが、`lookup_key`を料金へ設定することをお勧めします。これは料金データをAPIから取得する場合に利用できる検索用キーで、プランの料金を変更する場合などに、Price APIを利用しているソースコードの変更箇所を減らすことができます。

```node
const result = await stripe.prices.list({
  lookup_keys: ['membership_free', 'membership_bronze', 'membership_silver']
})
```

もし`lookup_key`を設定・利用しない場合、サンドボックス環境やLive環境それぞれで作成されたPrice IDを取得する処理を実装するか、別途DBを用意する必要があります。この方法では、新しい料金へ変更する際にDBやソースコードの変更が必要となるため、価格改訂に必要な工数が肥大化してしまいます。
:::


### `terraform apply`でStripeアカウントへ反映する

設定した内容がどのように反映されるかは、`terraform plan`でプレビューできます。ここで変更・追加削除されるリソースの一覧を確認し、意図しない変更が起きないかをチェックします。

```bash
$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # stripe_price.standard_monthly_price will be created
  + resource "stripe_price" "standard_monthly_price" {
      + active              = true
      + billing_scheme      = (known after apply)
      + currency            = "jpy"
      + id                  = (known after apply)
      + product             = (known after apply)
      + tax_behavior        = "unspecified"
      + transfer_lookup_key = false
      + type                = (known after apply)
      + unit_amount         = 9800
      + unit_amount_decimal = (known after apply)

      + recurring {
          + interval       = "month"
          + interval_count = 1
          + usage_type     = "licensed"
        }
    }

  # stripe_product.standard_product will be created
  + resource "stripe_product" "standard_product" {
      + active      = true
      + description = "Awesome SaaS license"
      + id          = (known after apply)
      + name        = "Standard license"
      + product_id  = (known after apply)
      + unit_label  = "seat"
    }
```

その後問題がなければ、`terraform apply`でデプロイします。このアクションは本当に実行するかの確認ステップがあります。プレビュー内容を確認してデプロイしても問題なさそうであれば、`yes`と回答してデプロイを実行します。

```bash
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
```

あとはリソースの作成状況が順次出力されますので、作業が完了するのを待ちましょう。

```bash
stripe_product.standard_product: Creating...
stripe_product.standard_product: Creation complete after 1s [id=prod_RMidOVSz3Q38vA]
stripe_price.standard_monthly_price: Creating...
stripe_price.standard_monthly_price: Creation complete after 1s [id=price_1QTzFVQ9mxJNCzY5GRhU9mIV]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

デプロイが完了すれば、Stripeのダッシュボードで実際にリソースが作成されていることを確認しましょう。

![スクリーンショット 2024-12-09 14.12.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d483e276-bcc7-0b11-bb0a-d1538fa410e7.png)

このようにterraformを利用して簡単にStripe上にあるリソースのコード管理が実現できます。

### 変更やプランの追加もterraformで

コードで設定を管理するため、新しい料金プランや商品を追加する場合のワークフローもterraformベースで行えます。たとえば下位プランの料金を追加するには、対応する商品と料金をそれぞれ追加するだけです。

```tf
resource "stripe_product" "hobby_product" {
  name        = "Hobby"
  unit_label  = "seat"
  description = "Hobby and personal usage plan"
  active      = true
}

resource "stripe_price" "hobby_price" {
    product     = stripe_product.hobby_product.id
    currency    = "jpy"
    unit_amount = 980
    recurring {
        interval      = "month"
        interval_count = 1
    }
}

```

追加後に`terraform apply`を実行すると、新規に追加したリソースのみ追加されます。

```bash
stripe_product.hobby_product: Creating...
stripe_product.hobby_product: Creation complete after 0s [id=prod_RMimUAtHdrL64t]
stripe_price.hobby_price: Creating...
stripe_price.hobby_price: Creation complete after 1s [id=price_1QTzLNQ9mxJNCzY5T3cvoOZd]

Apply complete! Resources: 2 added, 0 changed, 1 destroyed.
```

これで下位プランであるHobbyプランが追加できました。

![スクリーンショット 2024-12-09 14.18.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d1e5f392-9d8d-85e9-4228-5ddee65c6d08.png)

コードベースで料金プランを管理することで、`terraform plan`の内容を共有したりCIワークフローでレビューできるようにしたりが可能となります。それによって新プランのローンチやテストの進め方などの管理をより簡単にできます。

## SaaSの機能リストをEntitlements APIで管理する
terraformで管理できるのは料金だけではありません。SaaSではほぼ必須の機能である、プランごとの権限・機能管理もコード化できます。たとえばダッシュボードへのアクセスとREST APIへのアクセスの2つをユーザーに提供するケースを考えてみましょう。この場合、それぞれの機能を`stripe_entitlements_feature`リソースとして定義します。ここでもAPIからの検索などをしやすくするため、`lookup_key`の設定をお勧めします。

```tf

resource "stripe_entitlements_feature" "feature_dashboard_access" {
    name       = "Dashboard Access"
    lookup_key = "dashboard_access"
}

resource "stripe_entitlements_feature" "feature_rest_api_access" {
    name       = "REST API Access"
    lookup_key = "rest_api_access"
}
```

続いて登録したリソースを商品に紐付けしましょう。`stripe_product_feature`リソースを追加し、対象の商品IDと提供したい機能のリソースIDの2つを定義します。もし複数の機能を1つの料金プラン（商品）で提供したい場合は、その組み合わせの数だけリソースを定義しましょう。

。下の例では、Hobbyプランはダッシュボードへのアクセスのみ提供し、StandardプランではREST APIも利用できる設定をおこなっています

```terraform
resource "stripe_product_feature" "standard_product_feature_rest_api_access" {
    entitlements_feature = stripe_entitlements_feature.feature_rest_api_access.id
    product              = stripe_product.standard_product.id
}
resource "stripe_product_feature" "hobby_product_feature_dashboard_access" {
    entitlements_feature = stripe_entitlements_feature.feature_dashboard_access.id
    product              = stripe_product.hobby_product.id
}
resource "stripe_product_feature" "standard_product_feature_dashboard_access" {
    entitlements_feature = stripe_entitlements_feature.feature_dashboard_access.id
    product              = stripe_product.standard_product.id
}
```

この設定を`terraform apply`でデプロイしましょう。それぞれのリソースにIDが割り振られますので、社内Wikiなどに記録しておきたい場合は、実行結果を控えておくとよいでしょう。

```bash
stripe_entitlements_feature.feature_dashboard_access: Creating...
stripe_entitlements_feature.feature_rest_api_access: Creating...
stripe_entitlements_feature.feature_dashboard_access: Creation complete after 0s [id=feat_test_61RdNK3uaykEjKMIF41Q9mxJNCzY5NpA]
stripe_entitlements_feature.feature_rest_api_access: Creation complete after 0s [id=feat_test_61RdNK3ElxDCVoZjF41Q9mxJNCzY5TSy]
stripe_product_feature.standard_product_feature_dashboard_access: Creating...
stripe_product_feature.hobby_product_feature_dashboard_access: Creating...
stripe_product_feature.standard_product_feature_rest_api_access: Creating...
stripe_product_feature.standard_product_feature_rest_api_access: Creation complete after 1s [id=prodft_1QTzMiQ9mxJNCzY5WYYoE4OS]
stripe_product_feature.hobby_product_feature_dashboard_access: Creation complete after 1s [id=prodft_1QTzMiQ9mxJNCzY57PhuX12n]
stripe_product_feature.standard_product_feature_dashboard_access: Creation complete after 1s [id=prodft_1QTzMiQ9mxJNCzY5lJovSY2f]

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

デプロイが完了すると、ダッシュボードの各商品ページにて更新された機能リストが確認できます。

Standardプランは、Dashboard / REST API両方の機能が設定されています。

![スクリーンショット 2024-12-09 14.19.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/cbef3c73-95ac-a992-8f84-277d81ac2683.png)

HobyプランはDashboardのみが設定されています。

![スクリーンショット 2024-12-09 14.20.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e264bdbc-f0e6-c812-cce0-815fc6f758e7.png)

提供する機能・権限についてもterraformで管理することによって、新機能リリース前の設定チェックを`terraform plan`の出力に集約することができます。

## トラブルシューティング

`terraform apply`を実行すると、エラーが発生することもあります。Stripe ProviderはStripe APIのエラーレスポンスを表示しますので、この内容をみて設定値の間違いがないかをチェックしましょう。

![スクリーンショット 2024-12-09 14.08.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f05d4240-ad90-469d-0552-fe678ee2b544.png)

また、レスポンスに含まれる`request_log_url`のURLへもぜひアクセスしてください。Stripeダッシュボードが提供する開発者向けのAPIログ調査機能を使って、「どんなAPIリクエストが送信されたか」や「このエラーは何を意味しているか」などを簡単にチェックできます。

![スクリーンショット 2024-12-09 14.09.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/6c8fbc15-4bec-889c-b4c6-86ee9c151130.png)


## まとめ

価格改訂や継続的な機能追加による変更タスクが定期的に発生するのがSaaSの運用保守です。なかでも決済に関する機能はミスが顧客からのクレームにつながりやすく、リリース前に内容の慎重なチェックを入れたり、できるだけ環境間の差分を起こしにくい仕組みが必要とされます。

terraformのStripe Providerを活用することで、料金プランだけでなく、顧客に提供するプランごとの権限・機能リストについてもコードで管理し、CI / CDパイプラインに載せることが期待できます。

決済・サブスクリプションサービスをより効率的に、そしてトラブルの起きにくい形で運用するためにも、ぜひこのプラグインを使った新しい料金・機能管理のOpsを検討してください。

## 関連記事

https://creators-note.chatwork.com/entry/terraform-meets-stripe

https://github.com/lukasaron/terraform-provider-stripe

https://registry.terraform.io/providers/lukasaron/stripe/latest/docs