AWSのコスト配分タグは便利ですが、新たにタグを設定してからリストにあがってくるまで最大24時間。そのあとActiveにしてからCost Explorer上で使えるようになるまで更に最大24時間。実際はそこまでかからなかったにしろ、できるだけ早く反映を知り設定を進めたいケースがあったので、反映を待ってる間に通知する仕組みを作ってみました。

この記事では、方法と仕組み一式、そのなかで得た知見を共有します。
アカウントタグ、リソースタグのAPI上の違いについても言及しています。何かの参考になれば幸いです。

全体像

コスト配分タグの反映はEventBridgeになかったのでポーリングして通知する仕組みにしました。
定期的にタグの状態を読み、進展があったときだけSlackにメンション付きで通知します。タグを有効化したら放っておくだけで、使えるようになった瞬間に通知が飛んできます。

gistにテンプレートをあげているのでCloudFormationスタックを作成すれば使えます。詳細は後述。

背景

私の所属部署は30以上の社内サービスの開発と運用、1つの商材の技術主幹を担当しています。
3グループ、12名の体制で、各グループに主として担当する社内サービスと商材が割り当てられています。

管理するAWSアカウントも多く、その中には他部署から引き継いだもの、1アカウントに複数ワークロードが詰め込まれたレガシーなものも含まれます。
これらのアカウントは会社の共通組織(AWS Organizations)に属していましたが
管理を強化したかったのと、部署内でOrganizations含めたコントロール/最適化の知見を貯めたかったので、部署で独立した1つの組織を持つことにしました。

5月も終わり頃、専用組織(≒管理アカウント)配下にメンバーアカウントを移動し、コストと予算管理の設定をするなかで「コスト配分タグの反映が最大24時間かかる」がもどかしく
反映されたらすぐに後続作業をして6月分から綺麗にスタートしたいという思いがありました。

張り付くのも忘れるのもイヤなので、待っている間にササっと「反映されたら通知する仕組み」をAIに作らせ無駄な時間をゼロにすることができました。

コスト配分タグの反映順

タグを設定してから実際に使えるようになるまで、いくつかの段階を踏みます。公式にも「タグキーがコスト配分タグページに表示されるまでに最大24時間」と明記されています。アクティブ化のステータス変更自体は即反映されますが、実際に使える状態になるまでは更に最大24時間かかる旨も記載されています。

https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/activating-tags.html

ユーザー定義のタグを作成してリソースに適用した後、アクティブ化のためにタグキーがコスト配分タグページに表示されるまでに最大で 24 時間かかる場合があります。タグキーがアクティブ化されるまでに最大 24 時間かかることがあります。

段階 速さ 内容
①コスト配分タグの候補に出る 数時間〜24h ListCostAllocationTagsStatus=Inactive で出現する
②コスト配分タグのアクティブ化 瞬時 有効化操作で Status=Active に状態はすぐ変わる
③アクティブ化したコスト配分タグがCost Explorerから使える 〜24h フィルタ/グループ化の選択肢にキーが現れる
値別に料金が反映 数時間〜24h 値ごとの金額が入る(有効化以降の利用分)
⑤バックフィル バッチ+データ更新24h周期(公式) 過去分に現在Activeなタグを遡及適用

今回作った通知が面倒を見るのは①〜③(=Cost Explorerで使えるようになるまで)です。④の値別反映や⑤のバックフィルは通知対象外で、本章の最後に補足だけ載せています。

「① コスト配分タグの候補に出る」をポーリングする

ListCostAllocationTagsで目当てのタグがあがってくるまでポーリングします。候補に出ると Status=Inactive で一覧に現れます。

2025年12月から、Organizationsのアカウントタグがコスト配分タグに使えるようになりました。弊部署では原則ワークロードとステージごとにアカウントを分けているので、アカウントタグでのコスト配分が非常にマッチします。ただワークロード相乗りアカウントも存在するのでリソースタグによるコスト配分も併用します。

Type では種別が分からない — プレフィックスで見分ける

AIに実装させるなかで最初に引っかかったのが、「アカウントタグかリソースタグか」は Type で判別できないことです。ListCostAllocationTagsType enumは AWSGenerated / UserDefined(AWS製かユーザー定義か)を示すもので、ソースの種別ではありませんでした。
タグ種別はTagKey のプレフィックスで表現されています。

プレフィックス(キー例) 種別
(なし)Project リソースタグ
accountTag/Project アカウントタグ(Organizations)
iamPrincipal/Project IAMプリンシパルタグ
userAttribute/Department IAM Identity Centerユーザー属性
costCategory/Project コストカテゴリ

同名タグはソース別に別キーとして共存しますProject(リソースタグ)と accountTag/Project(アカウントタグ)は名前が同じでも完全に別物で値も独立なので、監視や重複判定はプレフィックス込みの完全キーで行います。

なお、リソースタグだけは Cost Explorer API では無印(Project)で返りaccountTag/ などのプレフィックスは付きません。本記事のコードは Cost Explorer API(ListCostAllocationTags / GetTags)を相手にするので、無印=リソースタグとして扱っています。

実際に ListCostAllocationTags をCLIで叩くと、TypeStatus が返ります。Type はどれも UserDefined で、ソース種別の判別には使えないことが分かります。

aws ce list-cost-allocation-tags --region us-east-1
{
  "CostAllocationTags": [
    { "TagKey": "Project",                "Type": "UserDefined", "Status": "Active" },
    { "TagKey": "accountTag/Project",     "Type": "UserDefined", "Status": "Inactive" }
  ]
}

「② コスト配分タグがアクティブか?」

これは候補に出たタグをマネジメントコンソールなどでActiveにすると即反映されます。
ListCostAllocationTagsのStatusがActiveかどうかで判定できます。

「③ 反映されたか」は GetTags で判定する

反映されたかは ce:GetTags で機械判定できます。--tag-key を付けずに呼ぶと「いまCost Explorerがフィルタ/グループ化に使えるキーの一覧」が返るので、この一覧に対象キーが含まれていれば反映済み(=料金データ反映済み)です。

aws ce get-tags \
  --time-period Start=2025-06-02,End=2026-06-02 \
  --region us-east-1

有効化した直後はまだ反映されておらず、対象キー(例 accountTag/Project)は一覧に出てきません。

{ "Tags": [ "Project" ], "ReturnSize": 1, "TotalSize": 1 }

しばらくすると一覧に対象キーが現れます。こうなれば反映済みです。

{
  "Tags": [ "Project", "accountTag/Project" ],
  "ReturnSize": 2,
  "TotalSize": 2
}

補足: ④の値別反映と⑤のバックフィル(通知対象外)

ここからは今回の通知の対象外ですが、関連情報として補足。

④の値別反映や⑤のバックフィルは、Cost Explorer 等のデータ更新が24時間周期である点が効いてきます。

https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/cost-allocation-backfill.html

これらのサービスは 24 時間に 1 回データを更新するため、バックフィルは成功してもすぐには更新されません。

「遅いならバックフィルで速くなる?」——なりません。バックフィル自体がバッチ処理で、役割は速度でなく遡及でカバー範囲を広げることです。月の途中で有効化すると有効化前(月初〜)の利用分は前向き処理では付かないので、その穴を埋めます。

さらに注意は、バックフィルしても「タグを設定する前の料金」は反映されないこと。対象は現在Activeなタグなので、タグが無かった期間も遡って料金が仕分けされるわけではなく、「今Activeなタグを過去の利用に当てはめ直す」だけです。これは公式にも明記されています。
https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/cost-allocation-backfill.html

ただし、2023 年 1 月から 2023 年 5 月の間は、タグが AWS リソースに存在しなかったため、Projectタグ値は関連付けられません。

作った仕組み

Lambda定期実行でポーリングし、Amazon Q Developer in chat applications(旧 AWS Chatbot)でメンションをつけて通知します。
反映されたよ!の通知は一度だけでいいのでSSM Parameter Storeで簡易的に管理しています。
延々と通知されるのを避けたいだけで、稀に重複通知されても問題ないので厳格な重複予防は実装しません。デプロイはCloudFormation1枚に収めています。

再掲:gist::aws-cost-allocation-tag-notifier

流れ

  1. コスト配分タグに使いたいタグを、アカウントタグ(またはリソースタグ)に設定する
  2. 今回の仕組みをデプロイ
  3. 定期実行で各タグの状態をポーリングする
  4. 前回状態(SSM)と比べて段階が進んでいたら、Slackに通知される
  5. 全タグが反映済みになったら「反映完了」サマリを一度だけ通知し、以降は空振りになる

今回のケースは昼過ぎにタグをアカウントタグに設定して、この仕組みをデプロイ。別の仕事をすすめ、退勤して夕飯を食べ終わってゆっくりしているときに「候補にあがってきたよ」の通知が来たので、アクティブにだけしておきました(ここも自動化しても良かったかも)。深夜にCost Explorerから見えるようになった通知もきており、翌朝からディメンションやフィルターとしてスムーズに使い始めることができました。

状態判定とlatch

各タグキーは NOT_DETECTED → CANDIDATE → ACTIVE → REFLECTED と一方向に状態遷移します。「進展があったときだけ通知」を実現する肝で、一度進んだ状態は降格させません(latch)。

判定材料は前述のとおりで、ListCostAllocationTags のStatusで①候補出現(Inactive)/②有効化(Active)を、GetTags(TagKey無し)のキー一覧で③反映済み(REFLECTED)を見ます。各キーをランク化し、前回より進んだときだけ通知してSSMに保存します(詳細なコードはgist参照)。

RANK = {NOT_DETECTED: 0, CANDIDATE: 1, ACTIVE: 2, REFLECTED: 3}

# 各キーについて、ランクが上がったときだけ通知してSSMに保存する
cur = observed if RANK[observed] > RANK.get(old, 0) else old  # 降格させない(latch)
if RANK[cur] <= RANK.get(old, 0):
    continue  # 進展が無ければ通知しない(差分時のみ通知)

デプロイ(パラメータ)

コスト配分タグやCost ExplorerはOrganizationsの管理(ペイヤー)アカウント単位なので、このスタックも管理(ペイヤー)アカウントにデプロイします。以下をパラメータで指定できるようにしています。

パラメータ 既定値 内容
SlackWorkspaceId (必須) Amazon Q Developer in chat applicationsのSlack Workspace ID(例 Txxxxxxxx)
SlackChannelId (必須) 通知先チャンネルID(例 Cxxxxxxxx)
TagKeys accountTag/Group,accountTag/Project,accountTag/Environment 監視するコスト配分タグキー(プレフィックス込み・カンマ区切り)
SlackMention (空) 任意。通知に前置するメンション。Slack記法(<@U…> / / / )
ScheduleExpression rate(30 minutes) ポーリング間隔(cron/rate式)
ReflectWindowDays 365 GetTags の反映チェックで遡る日数。狭いと古い利用のタグを取りこぼす

疎通テスト

一番手軽なのは、Lambdaを {"test": true} ペイロードで手動invokeする方法です。この場合はタグの状態に関係なく、監視対象タグの一覧を添えたテスト通知が即座にSlackへ飛ぶので、Slack連携が通っているかだけをサッと確認できます。

aws lambda invoke \
  --function-name <スタック名>-notifier \
  --payload '{"test":true}' \
  --cli-binary-format raw-in-base64-out \
  /dev/stdout

ポーリング〜通知までの一連の流れまで含めて確かめたいときは、既にコスト配分タグとして有効なものを監視対象の TagKeys に指定してみると、定期実行時に通知がくるはずなので、エンドツーエンドの疎通テストになります。

コスト

rate(30 minutes)(≒1,460回/月)前提でざっくり見積もり

要素 月額 根拠
Lambda 実質$0(〜$0.02) 128MB×実行数秒。無料枠400,000 GB-s内。枠外でも約912 GB-s≒$0.015
SSM Parameter Store $0 標準パラメータ(Type: String)の get/put は無料
EventBridge(スケジュール起動) $0 スケジュール起動は無料
SNS 実質$0 状態変化時のみpublish。無料枠100万件内
Cost Explorer GetTags $0.01/req これだけが有意なコスト

つまり、GetTags 以外はほぼタダです(SSMのgetも無料)。問題は GetTags だけで、毎回無条件に呼ぶと 1,460回 × $0.01 = 約$14.6/月 がずっと掛かり続けます。

使い終わったら定期実行を止めればいいんですが、それすら忘れてもコスト的に問題ないようにしておきたいです。
ということで唯一気にするべきGetTagsは必要時にしか呼ばないようにしています。

# Activeだが未REFLECTEDのキーが無ければ、課金されるGetTagsは呼ばない
need_reflect_check = any(
    listed.get(k) == "Active" and prev.get(k) != REFLECTED
    for k in TAG_KEYS
)
ce_keys = get_ce_filterable_keys() if need_reflect_check else set()

全キーがREFLECTEDに達した後は need_reflect_check が常に False になり、課金対象の GetTags は二度と呼ばれません。残るのは無料の ListCostAllocationTags だけなので、ポーリングを止めずに走らせ続けても定常コストは実質ゼロです。

まとめ

コスト配分タグまわりで得た学び

  1. 有効化 ≠ 使える。Active表示とCost Explorer反映には最大24時間の時間差があります。「キーは見えるのに値が空」という中途半端な状態もあります。
  2. 種別はAPIの Type ではなくキーのプレフィックスで決まる。同名タグは別キーとして共存します(ProjectaccountTag/Project は別物)。
  3. 「反映されたか」は GetTags で機械判定できる

この仕組みのおかげで、コンソールへ張り付くことなく「6月から使える状態」を無駄なく準備できました。
今後も、部署でのマルチアカウント管理で得た知見や気づきがあれば、また記事にしたいと思います。

参考リンク