ABEJA Tech Blog

中の人の興味のある情報を発信していきます

Azure OpenAI Service で設定ミスって1,000万円請求されたくない!

この記事は ABEJA アドベントカレンダー 2024 の19日目の記事です。

こんにちは。システム開発部の鈴木(@szpshota)です。 3年くらい前にエンジニアとして入社して、去年の暮れくらいからマネージャーをやっています。 今でも業務含めてコードは書いてますが、組織力や生産性を高めるための仕組みづくりや組織運営が主な仕事になってきました。

今回は組織マネジメントの話・・・ではなく、怖ーい Azure OpenAI Service の高額請求の話と Terraform の話をしようと思います! 目次は以下の通りです。

Azure OpenAI Service の高額請求について

LLM の登場から数年が経ち、様々な導入事例が見られるようになりました。利用する LLM のプロバイダーとして Azure OpenAI Service を選定するケースも多いのではないでしょうか。 今年の8月から Model のデプロイの種類に関する仕様にアップデートが入り、それに関連した高額請求の事例をチラホラ見かけました。

Provisioned-Managed なデプロイ

Azure OpenAI Service ではいくつかのデプロイの種類を選択できますが、パフォーマンスと課金体系の観点で以下の様に整理できます。

デプロイの種類 パフォーマンス 課金体系
GlobalProvisionedManaged / DataZoneProvisionedManaged / ProvisionedManaged リアルタイムでの呼び出し 時間単位の課金(月または年単位の予約オプションあり)
GlobalStandard / DataZoneStandard / Standard リアルタイムでの呼び出し(大量に利用する場合、レイテンシーが増加) トークン単位の課金
GlobalBatch ファイルベースの非同期プロセス(24時間以内に処理完了する) トークン単位の課金(Global-Standard の 50% の料金)

GlobalProvisionedManaged / DataZoneProvisionedManaged / ProvisionedManagedPTU (Provisioned Throughput Units) と呼ばれる単位でスループットを指定して API のパフォーマンスを保証するデプロイ方式です。

GlobalStandard / DataZoneStandard / Standard では、モデルごとに指定されている利用頻度の閾値を超えたときにレイテンシーが高くなることがあるのですが、こちらでは定常的に安定したパフォーマンスを得ることができます。

元々は Microsoft 側の営業を通して契約できるものでしたが、今年の8月から Standard デプロイと同様に利用できるアップデートがかかりました。

Provisioned-Managed の利用料

スループットが確約されるメリットもある Provisioned-Managed なデプロイですが、PTU 時間ごとの請求になり、その利用料は Standard デプロイと比べて非常に高額*1になっています。

「そんなヘマはしないでしょ!」と思うのですが、なんと、8月のアップデート直後は GPT-4o や GPT-4o-mini などの一部のモデルでは GUI 上のデフォルト値が ProvisionedManaged になっていたのです。 そのため、アップデート直後に手なりでデプロイメントを新規作成した結果、以下の記事の様に意図しない高額請求につながってしまったという事例もあったようです。

zenn.dev

この記事では数時間で気づくことができ、請求は5万円ぐらいだったようですが、50PTU の設定*2でうっかり1ヶ月使い続けると、利用料はなんと $73,000 ! 今の為替レートだと1,095万円*3の請求*4が発生します。数ヶ月の利用で家が建ちますね!

50PTU で1ヶ月間使い続けると・・・

高額請求の防止策

実は現在の東日本リージョンで提供されているモデルではデフォルトの設定が「グローバル標準」になっています。 そのため、設定をミスって高額請求されてしまうケースもあまり発生しなくなっています。

大体伝えたいことは言えたので終わりでも良いのですが、設定ミスの懸念がゼロにはなっていないので対策を考えたいと思います。 パッと思いつく対策は以下でしょうか。

  1. 予算アラートを設定する
  2. ペア体制で設定値を指差し確認する
  3. IaC で設定値を管理し、コードレビュー時に気付ける様にする

予算アラートを設定する

閾値を超えた場合に通知を飛ばす対策です。 万が一、設定ミスした場合に速やかに事態に気づくことができ、事後対応に移ることができます。

ただ、今回のケースでは時間単位の請求金額も大きいため、アラート発生から速やかに気づけても、「なんとか致命傷で済んだぜ…」となってしまうこともあるかと思います。 そのため、2番目や3番目の予防策とセットで利用することをオススメします。

ペア体制で設定値を指差し確認する

実施者と確認者で時間をとり、指差し確認する方法です。ややナイーブですが、即効性のある対策です。実施時間も1デプロイメントあたり、5分もあればよいので、Quality と Cost のバランスを考えたときにこの方法も選択肢に入るかと思います。

モデルのアップデートの都度、デプロイメントの数だけ同期的なコミュニケーションが発生するのと、技術者っぽくない対策なのが難点です。 管理するデプロイメントが多いケースでは、中長期的にはある程度、自動化した仕組みを考えていきたいですよね。

IaC で設定値を管理し、コードレビュー時に気付ける様にする

設定値をコードで管理し、他のソースコード同様にレビューしていく対策になります。 前述の2つの対策と比べ、以下のメリットがあります。

  • 設定反映前にレビュープロセスを設けることで未然に事態を防げる(2番目の対策と同様)
  • 設定ミスを検知するテスト書いておくことで明確に気づくことができる
  • テンプレ化して共有することで容易に横展開できる

今回は Terraform を使ってリソース管理をしながら、設定ミスを検知するテストを書いていこうと思います。

Terraform を使って設定を記述する

コード全体は以下のリポジトリに置いています。

github.com

今回は以下のディレクトリ構成で作っていきます。

.
├── README.md
├── backend.tf
├── main.tf
├── modules
│   └── aoai
│       ├── main.tf
│       ├── output.tf
│       └── variables.tf
├── providers.tf
├── terraform.tfvars
├── tests
│   └── aioi.tftest.hcl
└── variables.tf

modules/aoai/main.tf に Azure OpenAI Service の設定を記述していきます。 Azure OpenAI Service のリソースは azurerm_cognitive_account 、デプロイメントのリソースは azurerm_cognitive_deployment を使って定義します。 今回はgpt-4ogpt-4o-miniの二つのデプロイメントを作成します!

# modules/aoai/main.tf

resource "azurerm_cognitive_account" "main" {
  name                   = "${var.app_name}-cognitive-account"
  resource_group_name    = var.resource_group_name
  location               = var.location
  sku_name               = "S0"
  kind                   = "OpenAI"
  custom_sub_domain_name = "var.sub_domain_name"
}

resource "azurerm_cognitive_deployment" "gpt_4o" {
  name                 = "${var.app_name}-cognitive-deployment-gpt-4o"
  cognitive_account_id = azurerm_cognitive_account.main.id
  model {
    format  = "OpenAI"
    name    = "gpt-4o"
    version = "2024-08-06"
  }
  sku {
    name     = "Standard"
    capacity = 10
  }
}

resource "azurerm_cognitive_deployment" "gpt_4o_mini" {
  name                 = "${var.app_name}-cognitive-deployment-gpt-4o_mini"
  cognitive_account_id = azurerm_cognitive_account.main.id
  model {
    format  = "OpenAI"
    name    = "gpt-4o-mini"
    version = "2024-07-18"
  }
  sku {
    name     = "Standard"
    capacity = 10
  }
}

次に tfstate を保管するためのリソースグループ、 Azure Blob Storage を作成します。

az group create -n <resource-group-name> -l <location>
az storage account create -n <storage-account-name> -g <resource-group-name> -l <location>
az storage container create -n <storage-container-name> --account-name <storage-account-name>

作成した Blob Strorage を backend-configに設定して初期化します。

terraform init --backend-config="resource_group_name=<resource-group-name>" \
               --backend-config="storage_account_name=<storage-account-name>" \
               --backend-config="container_name=<storage-container-name>" \
               --backend-config="key=terraform.tfstate"

plan を実行すると、以下のような結果が表示されます。 問題なければ、apply してリソース作成!・・・と行きたいところですが、テストを書いてから実行します。

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:

  # module.aoai.azurerm_cognitive_account.main will be created
  + resource "azurerm_cognitive_account" "main" {
      + custom_subdomain_name              = "terraform-aoai-testing"
      + endpoint                           = (known after apply)
      + id                                 = (known after apply)
      + kind                               = "OpenAI"
      + local_auth_enabled                 = true
      + location                           = "eastus"
      + name                               = "terraform-aoai-testing-cognitive-account"
      + outbound_network_access_restricted = false
      + primary_access_key                 = (sensitive value)
      + public_network_access_enabled      = true
      + resource_group_name                = "hogehoge"
      + secondary_access_key               = (sensitive value)
      + sku_name                           = "S0"
    }

  # module.aoai.azurerm_cognitive_deployment.gpt_4o will be created
  + resource "azurerm_cognitive_deployment" "gpt_4o" {
      + cognitive_account_id   = (known after apply)
      + id                     = (known after apply)
      + name                   = "terraform-aoai-testing-cognitive-deployment-gpt-4o"
      + version_upgrade_option = "OnceNewDefaultVersionAvailable"

      + model {
          + format  = "OpenAI"
          + name    = "gpt-4o"
          + version = "2024-08-06"
        }

      + sku {
          + capacity = 10
          + name     = "Standard"
        }
    }

  # module.aoai.azurerm_cognitive_deployment.gpt_4o_mini will be created
  + resource "azurerm_cognitive_deployment" "gpt_4o_mini" {
      + cognitive_account_id   = (known after apply)
      + id                     = (known after apply)
      + name                   = "terraform-aoai-testing-cognitive-deployment-gpt-4o_mini"
      + version_upgrade_option = "OnceNewDefaultVersionAvailable"

      + model {
          + format  = "OpenAI"
          + name    = "gpt-4o-mini"
          + version = "2024-07-18"
        }

      + sku {
          + capacity = 10
          + name     = "Standard"
        }
    }

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

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

terraform test を使って、設定ミスを防ぐテストを書く

今回の本丸となる部分です。 v1.6.0からリリースされた test 機能を使って、設定ミスを防ぐテストを書いていきます。

この機能では module 内で管理しているリソースは検証できないため、今回は output を定義し、こちらを検証する形をとります。

# modules/aoai/output.tf

output "gpt_4o_sku_name" {
  value = azurerm_cognitive_deployment.gpt_4o.sku[0].name
  description = "The SKU name of the GPT-4o OpenAI model"
}

output "gpt_4o_mini_sku_name" {
  value = azurerm_cognitive_deployment.gpt_4o_mini.sku[0].name
  description = "The SKU name of the GPT-4o-mini OpenAI model"
}

続いて、テストを書いていきます。 今回は azurerm_cognitive_deploymentリソースに定義した skunameプロパティに GlobalProvisionedManagedDataZoneProvisionedManagedProvisionedManaged を指定した場合に失敗するテストを書きます。

# tests/aioi.tftest.hcl

run "check_not_provisioned_managed_gpt_4o" {
  command = plan

  assert {
    condition = !contains(
      ["GlobalProvisionedManaged", "DataZoneProvisionedManaged", "ProvisionedManaged"],
      module.aoai.gpt_4o_sku_name
    )
    error_message = "The sku name must not be ProvisionedManaged"
  }
}

run "check_not_provisioned_managed_gpt_4o_mini" {
  command = plan

  assert {
    condition = !contains(
      ["GlobalProvisionedManaged", "DataZoneProvisionedManaged", "ProvisionedManaged"],
      module.aoai.gpt_4o_mini_sku_name
    )
    error_message = "The sku name must not be ProvisionedManaged"
  }
}

terraform test コマンドを使ってテストを実行していきます。 各デプロイメントの skunameStandardになっているのでテストが通ります。

terraform test
tests/aioi.tftest.hcl... in progress
  run "check_not_provisioned_managed_gpt_4o"... pass
  run "check_not_provisioned_managed_gpt_4o_mini"... pass
tests/aioi.tftest.hcl... tearing down
tests/aioi.tftest.hcl... pass

Success! 2 passed, 0 failed.

続いて、gpt-4o のデプロイメントの sku に変更を加えます。 nameStandard から ProvisionedManaged に変更しています。 このまま、apply すると月額2,000万円のデプロイメントが爆誕します。

# modules/aoai/main.tf

resource "azurerm_cognitive_account" "main" {
  name                   = "${var.app_name}-cognitive-account"
  resource_group_name    = var.resource_group_name
  location               = var.location
  sku_name               = "S0"
  kind                   = "OpenAI"
  custom_sub_domain_name = "var.sub_domain_name"
}

resource "azurerm_cognitive_deployment" "gpt_4o" {
  name                 = "${var.app_name}-cognitive-deployment-gpt-4o"
  cognitive_account_id = azurerm_cognitive_account.main.id
  model {
    format  = "OpenAI"
    name    = "gpt-4o"
    version = "2024-08-06"
  }
  sku {
    name     = "ProvisionedManaged"
    capacity = 100
  }
}

resource "azurerm_cognitive_deployment" "gpt_4o_mini" {
  name                 = "${var.app_name}-cognitive-deployment-gpt-4o_mini"
  cognitive_account_id = azurerm_cognitive_account.main.id
  model {
    format  = "OpenAI"
    name    = "gpt-4o-mini"
    version = "2024-07-18"
  }
  sku {
    name     = "Standard"
    capacity = 10
  }
}

この状態でテストを実行すると失敗します。 これで設定ミスを明示的に検知する仕組みができたかと思います。

terraform test
tests/aioi.tftest.hcl... in progress
  run "check_not_provisioned_managed_gpt_4o"... fail
╷
│ Error: Test assertion failed
│ 
│   on tests/aioi.tftest.hcl line 5, in run "check_not_provisioned_managed_gpt_4o":
│    5:     condition = !contains(
│    6:       ["GlobalProvisionedManaged", "DataZoneProvisionedManaged", "ProvisionedManaged"],
│    7:       module.aoai.gpt_4o_sku_name
│    8:     )
│     ├────────────────
│     │ module.aoai.gpt_4o_sku_name is "ProvisionedManaged"
│ 
│ The sku name must not be ProvisionedManaged
╵
  run "check_not_provisioned_managed_gpt_4o_mini"... pass
tests/aioi.tftest.hcl... tearing down
tests/aioi.tftest.hcl... fail

Failure! 1 passed, 1 failed.

今回はめんどくさいので割愛しましたが、IP 制限の設定やモデルのスペックなど、柔軟性を持たせたい部分を variable として切り出したり、CI の設定を入れたりすることで広く横展開できるのではないでしょうか。

CI 周りは気が向いたら追記しようと思います!

We Are Hiring!

ABEJAは、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!)

careers.abejainc.com

特に下記ポジションの募集を強化しています!ぜひ御覧ください!

プラットフォームグループ:シニアソフトウェアエンジニア | 株式会社ABEJA

トランスフォーメーション領域:ソフトウェアエンジニア(リードクラス) | 株式会社ABEJA

トランスフォーメーション領域:データサイエンティスト(シニアクラス) | 株式会社ABEJA

*1:API の利用頻度が高い場合は利用料を安く抑えられるケースもあります。

*2:東日本リージョンの GPT-4o のデフォルト値

*3:$1 = 150円で計算

*4:予約オプションを指定することで大幅にディスカウントできます。このケースだと、月単位では $13,000、年単位では $11,050 にまで利用料を抑えることができます。