vague memory

うろ覚えを無くしていこうともがき苦しむ人の備忘録

Terraform の集合演算

殆ど使う機会は無いですが、関数により違いがあったのでメモ。



題材

list 型の変数の要素に対して演算する例です。

variable "list" {
  default = [56, 27, 84, 13, 42]
}

output "count_value" {
  value = length(var.list)
}

output "sum_value" {
  value = sum(var.list)
}

output "min_value" {
  value = min(var.list...)
}

output "max_value" {
  value = max(var.list...)
}

結果

Outputs:

count_value = 5
max_value = 84
min_value = 13
sum_value = 222

Count

length 関数で取得します。

これは利用頻度がそれなりにある気がします。

length(var.list)

別の使い方として string を渡して文字数を取得することもできます。 ちなみに Terraform では count という関数は無く、別の意味を持ちます。 複数リソースを作成するための構文になります。参考: The count Meta-Argument

Sum

sum 関数で取得します。

sum(var.list)

Min / Max

min / max 関数で取得します。

Nurmeric Functions に分類され、1つ以上の number を渡して使うようになっています。
list を渡す場合は ... を末尾に付与する必要があります。

min(var.list...)
max(var.list...)

Average

avg や average 関数はありません。 使うケースも思い浮かばないです。

どうしても必要となったら自前関数を無理やり作ることになるのではと思います。

  • external 例
data "external" "avg" {
  program = ["python", "./calc_avg.py"]
  query = {
    values = join(",", var.list)
  }
}

output "avg_value" {
  value = tonumber(data.external.avg.result.avg)
}
import sys
import json
import numpy

input = sys.stdin.read()
input_json = json.loads(input)

values = input_json.get('values', "").split(",")
avg = numpy.average([int(i) for i in values])

output = {
    'avg': str(avg),
}

print(json.dumps(output, indent=2))
  • 結果
Outputs:

avg_value = 44.4

OpenAI Usage を深堀りする

2023/07/21 追記 現在この手法は利用できません。
APIキーでの認証は不可となったようです。 StatusCode:403、session key が必要というメッセージが返却されます。

Your request to GET /dashboard/billing/usage must be made with a session key (that is, it can only be made from the browser). You made it with the following key type: secret.

Usage Dashboard から model 別の利用状況をグラフ化します。



前置き

前回 "OpenAI Usage を取得する" で合計料金を取得した方法で、 グラフを細分化します。

標準のダッシュボードでは Daily usage では合計量のみしか出力されません。

データとしてモデル別の cost を持っているのでそれを利用してモデル別の利用量を出力します。

Daily usage breakdown では日付、ユーザを選択するとタイムラインが表示されます。 目的の日付、ユーザが決まっているのであればこの形でも問題ないですが、その日誰が一番利用したかなどの把握が為難いのでユーザ別の利用量を可視化します。

取得方法

Daily usage は前回に続き Usage Dashboard から取得します。

https://api.openai.com/dashboard/billing/usage?end_date=2023-06-01&start_date=2023-05-01

ユーザのリストと、Daily usage breakdown は API で取得します。 API Reference にはいずれも記載が無いようです。見つけられませんでした。 管理系のドキュメントは用意されていないのかもしれません。

https://api.openai.com/v1/organizations/org-XXXXX/users
https://api.openai.com/v1/usage?date=2023-05-01

利用例

htnosm/openai-usage-details: Visualize the details of OpenAI usage.

html ファイルをブラウザで開き、 Organization ID と API Key を入力して、期間のボタンを押下します。

usage-summary.html

指定月の合計料金、モデル別の使用量割合、日毎と累積利用量を出力します。UTCです。

usage-per-user.html

ユーザ単位の利用量を出力します。

APIキー毎に集計できるとプログラムや処理毎に分けられるので良いのですが、現状はキーの情報はデータとして持っていません。

データを持っては来ているので、Usage Dashboard 標準でもう少し詳細を入れてくれると嬉しいです。

Terraform "count = 1" の追加/削除は暗黙的に move される

Terraformのコードをリファクタリングしている際に気付いた点です。 moved ブロックを記述していないのに move の動作となったので不思議に思いました。

あまり遭遇しないと思いますし、 便利なので利用しましょうというより、 仕様なので意図しない出力があっても落ち着きましょうという内容です。


公式ドキュメント

Refactoring | Terraform | HashiCorp Developer

Note: When you add count to an existing resource that didn't use it, Terraform automatically proposes to move the original object to instance zero, unless you write an moved block explicitly mentioning that resource. However, we recommend still writing out the corresponding moved block explicitly, to make the change clearer to future readers of the module.

"moved ブロックが明示的に指定されていない場合、count を追加すると インスタンス0に移動するよう提案する" とあります。

変更を明確にするため、movedブロックを明示的に記述することを推奨 ともありますので、基本的には明示しましょう。

moved ブロックについては公式ドキュメント参照。 Use Configuration to Move Resources | Terraform | HashiCorp Developer

どういうことか

Terraform で変数によりリソース作成有無を切り替える手法として以下の様に記述することがあります。

resource foo bar {
  count = condition ? 1 : 0
  etc
}

この count = の部分を追加・削除した際に、 リソース削除・追加(destroyed/created) ではなく、moved の動作になるという話です。

検証

  • 事前作成リソース
# 既存リソースに count を追加する例
resource "terraform_data" "this" {
  provisioner "local-exec" {
    command = "echo this."
  }
}

# 既存リソースから count を削除する例
variable "create" {
  type    = bool
  default = true
}
resource "terraform_data" "this_count" {
  count = var.create ? 1 : 0
  provisioner "local-exec" {
    command = "echo this count."
  }
}

apply します。

 % terraform apply
terraform_data.this: Refreshing state... [id=6e9d88ea-41ac-169e-2626-9cb4af26c2ea]
terraform_data.this_count[0]: Refreshing state... [id=bde4e6a0-d339-1a63-e41a-120ed59d30e9]

以下の変更を加えます。変更箇所は count の部分です。

# 既存リソースに count を追加する例
resource "terraform_data" "this" {
  count = 1  # count を追加する
  provisioner "local-exec" {
    command = "echo this."
  }
}

# 既存リソースから count を削除する例
variable "create" {
  type    = bool
  default = true
}
resource "terraform_data" "this_count" {
  #count = var.create ? 1 : 0  # count 削除する
  provisioner "local-exec" {
    command = "echo this count."
  }
}

この状態で plan を叩くと、 move の動作になります。

Terraform will perform the following actions:

  # terraform_data.this has moved to terraform_data.this[0]
    resource "terraform_data" "this" {
        id = "6e9d88ea-41ac-169e-2626-9cb4af26c2ea"
    }

  # terraform_data.this_count[0] has moved to terraform_data.this_count
    resource "terraform_data" "this_count" {
        id = "bde4e6a0-d339-1a63-e41a-120ed59d30e9"
    }

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

moved を明示指定する場合でも同様の動作になります。

moved {
  from = terraform_data.this
  to   = terraform_data.this[0]
}

moved {
  from = terraform_data.this_count[0]
  to   = terraform_data.this_count
}

検証(2)

count に2以上の値が指定されている場合、 [0] のみ前述の動作 になります。
混乱の元なのでやはり moved を明示指定すべきだと思います。

Terraform will perform the following actions:

  # terraform_data.this2 has moved to terraform_data.this2[0]
    resource "terraform_data" "this2" {
        id = "fd03ec07-12cf-2347-fe0b-c1aa6b088f0c"
    }

  # terraform_data.this2[1] will be created
  + resource "terraform_data" "this2" {
      + id = (known after apply)
    }

  # terraform_data.this2_count[0] has moved to terraform_data.this2_count
    resource "terraform_data" "this2_count" {
        id = "d44f89cb-442c-501f-ca87-192b44dcc4a6"
    }

  # terraform_data.this2_count[1] will be destroyed
  # (because resource does not use count)
  - resource "terraform_data" "this2_count" {
      - id = "567b7f34-47e4-e164-2442-1a808845b0d7" -> null
    }

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

検証(3)

module に対する count では効きません。

Terraform will perform the following actions:

  # module.example.terraform_data.this will be destroyed
  # (because module.example is not in configuration)
  - resource "terraform_data" "this" {
      - id = "8dada03a-1f12-93b1-84c9-f516e94335c4" -> null
    }

  # module.example[0].terraform_data.this will be created
  + resource "terraform_data" "this" {
      + id = (known after apply)
    }

  # module.example_count.terraform_data.this will be created
  + resource "terraform_data" "this" {
      + id = (known after apply)
    }

  # module.example_count[0].terraform_data.this will be destroyed
  # (because module.example_count[0] is not in configuration)
  - resource "terraform_data" "this" {
      - id = "8a6bdeb1-9c26-7b40-b578-869ea6da4a5e" -> null
    }

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

Issue が挙がっているので、将来的に挙動が変わるかもしれません。

おまけ

(0,1 のために count を使用する用途とは違いますが念の為、 ) count を利用する際によく併用される count.index が定義に含まれている場合は既存通りの動作です。

count を削除するので count.index は Error になります。

Error: Reference to "count" in non-counted context

OpenAI Usage を取得する

2023/07/21 追記 現在この手法は利用できません。
APIキーでの認証は不可となったようです。 StatusCode:403、session key が必要というメッセージが返却されます。

Your request to GET /dashboard/billing/usage must be made with a session key (that is, it can only be made from the browser). You made it with the following key type: secret.


前置き

OpenAI の使用量を取得したくなりました。 日次と当月の使用量は Usage dashboard で確認できます。

How will I know how many tokens I’ve used each month?

Log in to your account to view your usage tracking dashboard. This page will show you how many tokens you’ve used during the current and past billing cycles.

ドキュメント上の URL リンクは beta.openai.com となっていますが、現在は platform.openai.com にリダイレクトされます。

この値を定期的に取得して通知してダッシュボードに行かずとも日々確認できるようにしたいと思いました。 詳細はダッシュボードを見れば良いので、今週使いすぎたな位のざっくりとした物で十分です。

取得方法

Usage の API は用意されていないようなので、以下を参考に Usage dashboard から値を取得します。

curl で total_usage を取得する例です。

  • total_usage は 各 cost の合計
  • total_usage, cost は /100 で $ (ドル)
_ORGANIZATION="Your Organization"
_OPENAI_APIKEY="Your API Key"

_START_DATE="$(date -u +'%Y-%m')-01"
_END_DATE="$(date -u -v+1m +'%Y-%m')-01"
# for GNU
#_START_DATE="$(date -u +'%Y-%m')-01"
#_END_DATE="$(date -u -d '1 month' +'%Y-%m')-01"

curl -sSf "https://api.openai.com/dashboard/billing/usage?end_date=${_END_DATE}&start_date=${_START_DATE}" \
-H "openai-organization: ${_ORGANIZATION}" \
-H "authorization: Bearer ${_OPENAI_APIKEY}" \
| jq -r '"$" + (.total_usage | round / 100 | tostring)'

利用例

取得した値を日次で Slack に流してます。

余談

公式ドキュメントに記載が無いので使えなくなる or 方法が変わる可能性はありますが、当面は大丈夫なようです。 何も指定せず実行すると API key を指定せよと怒られます。

$ curl "https://api.openai.com/dashboard/billing/usage"
{
  "error": {
    "message": "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accesing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}

返却されるJSON例です。model毎のcost算出も行えます。

{
  "object": "list",
  "daily_costs": [
    {
      "timestamp": 0.0,
      "line_items": [
        {
          "name": "Instruct models",
          "cost": 0.0
        },
        {
          "name": "Chat models",
          "cost": 0.0
        },
        {
          "name": "GPT-4",
          "cost": 0.0
        },
        {
          "name": "Fine-tuned models",
          "cost": 0.0
        },
        {
          "name": "Embedding models",
          "cost": 0.0
        },
        {
          "name": "Image models",
          "cost": 0.0
        },
        {
          "name": "Audio models",
          "cost": 0.0
        }
      ]
    },
    ...
  ],
  "total_usage": 0.0
}

AWS Security Hub の状態遷移を整理してみる

AWS Security Hub の中に 〜Status や 〜State というような状態を表す属性が複数存在します。
ドキュメントの文章では関連性が解りにくかったので、それぞれがどのように関連・遷移するのかを整理してみました。



状態遷移図

属性

(AWS Config) Compliance Type

Security Hub を利用する前提条件となっている AWS Config の属性の一つ。

コンプライアンス - AWS Config

  • Compliant(準拠)
    • リソースを評価するすべての Config ルールに準拠している
  • Non Compliant(非準拠)
    • リソースを評価する Config ルールの1つ以上に準拠していない
  • Not Applicable
    • リソースが削除されたか、ルールのスコープから削除された場合に発生する
  • Insufficient Data
    • Config ルールの評価結果が利用できない

Record State

オプションの最上位属性 (RecordState)
用語と概念 - AWS Security Hub - アーカイブ済みの結果

結果を送信するサービスから入ってきた段階での状態。

  • Active
    • サービスによって最初に生成されたときの結果
    • Archived から Active に変更され、 Workflow Status が Suppressed でない場合は、Workflow Status が自動的に New に設定される
  • Archived
    • 結果がビューで非表示になるべきであることを示す
    • リソースが存在しない、コントロールが無効、または、3〜5日間結果が更新されていない(ベストエフォート)場合、自動的に設定される

Compliance Status

結果からのコントロールの全体的なステータスの特定 - AWS Security Hub

Security Hub コントロールでの評価結果。各リソース毎に持つ。

  • Passed (成功)
    • チェックに合格
    • Workflow Status が自動的に Resolved に設定される
  • Failed (失敗)
    • チェックに不合格
  • Warning
    • チェックを完了したが、Security Hub が結果を判断できないことを示す
  • Non Available
    • チェックを完了できない
      • サーバ障害
      • リソースが存在しない
      • AWS Config の評価結果が "Not Applicable"
        • Record State が自動的に Archived に設定される

Control Status

Security Hub コントロールでのリソース全体の評価結果。 Workflow Status が Suppressed の結果は無視される。
Disabled > Failed > Unknown > Passed の順で優先される。

  • Passed (成功)
  • Failed (失敗)
  • Unknown (不明)
    • Failed が 0 且つ、Compliance Status が1つ以上 Warning または Non Available
  • No Data (データなし)
    • 結果がないことを示す
      • 結果が未生成
      • 全ての Workflow Status が Suppressed
  • Disabled (無効)
    • コントロールが無効になっており、チェックが実行されていない

Workflow Status

結果のワークフローステータスを設定する - AWS Security Hub

結果に対する進行状況を追跡するために(運用者が)設定する。 個々の結果に固有のもので、新しい結果の生成には影響しない。

  • New (新規)
    • 初期状態。調査が必要であることを示す
  • Notified (通知済み)
    • リソース所有者に通知したことを示す
    • 判別に利用するのみであり、自動的に遷移することはない。
  • Suppressed (抑制済み)
    • アクションが必要と判断しなかった事を示す
    • 自動的な状態遷移の対象から除外される
  • Resolved (解決済み)
    • レビューおよび修正のアクションが実施され、現在は解決済みと見なされていることを示す
    • Compliance Status が Passed の場合に自動的に設定される

AWS Notification Message のメール内容を Step Functions で変更する

EventBridge -> SNS Topic -> Email とした場合に、メールの件名が全て "AWS Notification Message" になります。本文は Event の JSON です。

これを、AWS Chatbot のように通知設定を一箇所にして、そこに投げればイベント毎の細かな整形は行わずともどのイベントも良い感じに整形して通知できる。と良いなと思いました。

(Eメールで受け取る是非は今回は置いておきます。)



方式検討

要望は多いようで、既に様々なパターンが紹介されています。 AWS公式では Lambda Function を介した方法が挙げられています。

Lambda Function は柔軟な変更が可能ですが、コード・ライブラリバージョンの管理が必要です。 EventBridge の Input transformer は各Event毎に設定していく必要が出てきます。

また、EventBridge であまりにも緩いルールを使用すると無限ループの可能性があり、トリガーとなるイベントは適切にフィルタすべきなので、必然的にEventBridge のルールは複数使うことになります。
参考: Amazon EventBridge イベントパターンでのコンテンツのフィルタリング

EventBridge -> Step Functions

イメージです。

Input transformer を使わず、EventBridgeはただ投げるだけ、変換処理は Step Functions で行うようにすれば通知設定を一箇所にできそうです。
前段に Custom Event Bus を持たせればクロスリージョン・クロスアカウントでも利用できます。

Definition

State Machine の定義例です。
Pass で件名と本文を組み込み関数 States.Format を利用して変換、TaskでSNSへ送信します。

{
  "Comment": "A description of my state machine",
  "StartAt": "Formatting",
  "States": {
    "Formatting": {
      "Type": "Pass",
      "Parameters": {
        "subject.$": "States.Format('{} | {} | Account: {}', $['detail-type'], $.region, $.account)",
        "message.$": "States.Format('source:\n  {}\nresources:\n  {}\ndetail:\n  {}\n\nraw json:\n{}', $.source, $.resources, $.detail, $)"
      },
      "ResultPath": "$.generate",
      "Next": "SNS Publish"
    },
    "SNS Publish": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "Subject.$": "$.generate.subject",
        "Message.$": "States.Format('# {}\n\n{}', $.generate.subject, $.generate.message)",
        "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:test-topic"
      },
      "End": true
    }
  }
}

メール件名部分

Chatbot のメッセージを参考に "件名 | リージョン | アカウント" の形式に変換します。 件名は detail-type で何のイベントが発生したのかが判別できるようにしています。

"subject.$": "States.Format('{} | {} | Account: {}', $['detail-type'], $.region, $.account)"

メール本文部分

全てのイベントに当てはまる(キーが存在する)のかまでは確認していませんが、 件名に入れた情報に加えて、resources があるとイベント発生の対象となったリソースがわかります。

detail 部分の JSON は Pretty(整形) 出力したかったのですが、Step Function 単体では行えないようだったのでそのままの形にしています。 ここは各 Event によりキーが変わるので抽出などは行っていません。

raw json 部分は無くとも良いのですが、利用していないキーの値なども見れるよう変換前の値を末尾に持たせています。

"message.$": "States.Format('source:\n  {}\nresources:\n  {}\ndetail:\n  {}\n\nraw json:\n{}', $.source, $.resources, $.detail, $)"

件名に利用した値も本文に載せたいため、送信時に変換後の件名を結合しています。

"Message.$": "States.Format('# {}\n\n{}', $.generate.subject, $.generate.message)",

受信結果

件名と本文前半でどのリソースで何が起きたかがある程度判別できるようになったと思います。

応用例

Step Functions に処理を持たせておくことで、柔軟に変更が可能かと思います。 最終的にコードに落とすとしても、ビジュアルエディタ (Workflow Studio) 上で変更を簡単に行えるのは利点ではないでしょうか。

以下はエラー処理やイベント毎の整形を変更するなどを追加した例です。

以下端折りますが各タスクの補足です。

Verify

キーが存在しないとランタイムエラーとなってしまうため処理に使用するキーの存在を事前にチェックしておきます。

    "Verify": {
      "Type": "Choice",
      "Choices": [
        {
          "And": [
            {
              "Variable": "$.detail-type",
              "IsPresent": true
            },
            {
              "Variable": "$.region",
              "IsPresent": true
            },
...
          ],
          "Next": "Formatting"
        }
      ],
      "Default": "Raw"
    },

Formatting

イベント毎の整形に対応させています。

何かしらのエラーになった場合は、後述 Raw の異常系処理に流れるようにしています。

    "Formatting": {
      "Type": "Parallel",
      "Next": "Slice",
      "Branches": [
...
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Comment": "Formatting Error",
          "Next": "Raw",
          "ResultPath": "$.errors"
        }
      ]
Choice detail-type

detail-type での分岐を行います。

            "Choice detail-type": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.detail-type",
                  "StringEquals": "Trusted Advisor Check Item Refresh Notification",
                  "Next": "Trusted Advisor Check Item Refresh Notification"
                }
              ],
              "Default": "General"
            },
Trusted Advisor Check Item Refresh Notification

Trusted Advisor は us-esat-1 固定でイベントが作成されるため、件名から region を除外しています。

General

else(Default) に該当する場合の前述 "メール本文部分" の処理を行います。

Slice

SNS Publish 1タスクで正常系と異常系の両方を処理させるため、Parallel state の結果から値を抽出して形を合わせています。

    "Slice": {
      "Type": "Pass",
      "Next": "SNS Publish",
      "InputPath": "$[0]"
    },

Raw

異常系処理です。
整形処理を行わず、EventBridge から受け取った値をそのまま SNS Topic へ送信します。

    "Raw": {
      "Type": "Pass",
      "Parameters": {
        "subject": "AWS Notification Message",
        "message.$": "$"
      },
      "ResultPath": "$.generate",
      "Next": "SNS Publish"
    },

SNS Publish

SNS Topic への送信処理です。 今回の例では固定にしていますが、TopicArn を渡すようにすれば送信先の振り分けも行えます。

Datadog Cumulative Time Windows (累積タイムウィンドウ)

数億年振りに Datadog の Web UI からポチポチと monitor を新規作成していた所、見慣れない項目を見つけました。



Evaluation window

公式ドキュメント (Configure Monitors) 上は 2022/10/13 に追記されていたようです。([Monitors] Document cumulative evaluation windows by Dalje-et · Pull Request #15383 · DataDog/documentation Configure Monitors)

rolling_vs_expanding.eb5be76d2ac9dced08ec172d703c8062.png (1930×758)

これまで、ある一定期間でデータをまとめるような機能は、Rollup がありましたが、起点を変えることはできなかったので、 個人的には非常に嬉しい機能です。 何時台の件数とか、SaaSの月額利用料とか、色々と使えそうです。

Rolling time windows

図の右側です。時間の経過とともに開始点が移動します。
直近の N [分|時間|日] を対象に評価されます。
これまでの Datadog Monitor の動作です。

Cumulative time windows

図の左側です。固定の開始点を持ち、時間とともに拡大されます。
新たに加わった機能です。

現時点で時間枠としてサポートされているのは以下3つです。 (公式ドキュメントより)

  • Current hour: 構成可能な分単位で開始する最大1時間のタイムウィンドウです。 例えば、HTTP エンドポイントが 0 分から 1 時間の間に受けたコールの量を監視します。
  • Current day: 構成可能な 1 日の時分から始まる、最大 24 時間のタイムウィンドウです。例えば、1 日のログインデックスクォータを監視するには、current day タイムウィンドウを使い、UTC 2:00pm から開始するようにします。
  • Current month: 当月 1 日午前 0 時 (UTC) を起点に、当月を振り返ります。このオプションは、1 か月単位のタイムウィンドウを表し、メトリクスモニターでのみ利用可能です。

公式記載の例で、 "daily log index quota" についての記載がありますが、このためにこの機能を追加したのではないかと思える例です。

daily log index quota とは

Datadog Logs の機能として、ログインデックスの1日の取り込み量が指定値を超過した場合にインデックス化を停止する設定が行なえます。

この値は設定値脇の記載の通り、日次 02:00 UTC (23:00(JST)) にリセットされます。
超過した場合に daily quota reached Event が発生するのでそれを通知することで超過に気付くことができます。 実際には超過して停止される前に対処を行いたい所です。

公式ドキュメント内でも、割当の 80 % を超えたら通知することを推奨しています。

If an index has a daily quota, Datadog recommends that you set the monitor that notifies on that index’s volume to alert when 80% of this quota is reached within the past 24 hours.

ドキュメント内での monitor 例は概要の記載 (Alert when an indexed log volume passes a specified threshold) しか無く、具体的な monitor の設定例までは記載されていません。

monitor 例

公式の手順に沿って作成します。

  • "Log Explorer" に移動し、インデックス名で検索、"Create monitor" を押下
  • ※ Window 設定で current day を設定
    • Evaluate the query over the current day starting at 23:00
  • thredshold に任意の値( daily quota 設定値の 80% 等) を設定

設定後、 monitor status を見てみると、値が加算されて行き、指定した時間帯になるとリセットされていることが確認できます。データポイントは10分置きに作成されていました。