
Azure Advisor Report
- 11 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
Azure Advisor Report is a skill that generates a monthly Azure environment report in Markdown and PowerPoint from Azure Advisor and Cost Management API data.
About
A skill that generates a monthly Azure environment report in Markdown and PowerPoint from the Azure Advisor and Cost Management APIs. It analyzes cost trends, service breakdowns, and Advisor recommendations across Cost, Security, Reliability and Operational Excellence, with prioritized recommended actions. It is read-only and aimed at customer-facing reporting. Developers or consultants use it to produce a subscription report for a customer.
- Generates a monthly Azure Advisor and cost report as Markdown plus PowerPoint
- Analyzes cost trends and Advisor recommendations across four categories
- Read-only; covers Cost, Security, Reliability and Operational Excellence
Azure Advisor Report by the numbers
- 11 all-time installs (skills.sh)
- Ranked #841 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
azure-advisor-report capabilities & compatibility
Requires an Azure subscription and CLI credentials; uses Advisor and Cost Management APIs (read-only)
- Capabilities
- presentations · data analysis
- Works with
- azure
- Use cases
- presentations · data analysis · security audit
- Runs
- Runs locally
- Pricing
- Bring your own API key
What azure-advisor-report says it does
Generate Azure environment monthly report (Markdown + PowerPoint) from Azure Advisor and Cost Management API.
**全 4 カテゴリを必ず取得**: Cost, Security, HighAvailability, **OperationalExcellence**
npx skills add https://github.com/aktsmm/agent-skills --skill azure-advisor-reportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
What it does
Generate a monthly Azure Advisor and cost report for a customer subscription as Markdown and PowerPoint.
Who is it for?
Producing customer-facing monthly Advisor recommendation and cost-trend reports for an Azure subscription
Skip if: Building or changing an Azure environment, since this skill is read-only report generation
When should I use this skill?
Creating an Advisor recommendations report, cost trend analysis, or security/reliability assessment for a customer subscription
What you get
A structured monthly Markdown and PowerPoint report of Advisor recommendations and cost trends
- Markdown monthly report
- PowerPoint report
By the numbers
- 10-section required report structure
- 4 Advisor categories (Cost, Security, Reliability, Operational Excellence)
Files
Azure Advisor 簡易月次レポート生成
Azure Advisor 推奨事項・コスト推移を分析し、顧客向けの簡易月次レポート(Markdown + PowerPoint)を生成するスキル。
When to Use
- 顧客の Azure 環境の Advisor 推奨事項をレポートにまとめたいとき
- コスト推移・サービス別内訳を可視化したいとき
- セキュリティ・信頼性の推奨事項を優先順位付きで報告したいとき
When NOT to Use
- Azure 環境の構築・変更作業(このスキルは読み取り専用のレポート生成のみ)
- 社内用の詳細分析(このスキルは顧客向けレポートに特化)
入力パラメータ
| パラメータ | 必須 | 説明 |
|---|---|---|
| Subscription ID | ✅ | 対象サブスクリプション(複数可) |
| Tenant ID | ✅ | 各サブスクリプションのテナント ID |
| 対象月 | ❌ | YYYY-MM 形式。省略時は当月 |
| 顧客名 | ❌ | レポートタイトルに使用。省略時は「Azure 環境」 |
ガードレール
レポート品質ルール を適用すること。
手順
Phase 1: データ取得
データ取得手順 を参照。
Phase 2: Markdown レポート生成
Markdown テンプレート をベースに生成。
出力先: 任意のパス(例: {customer}/monthly-report_{YYYYMMDD}.md)
Phase 3: PowerPoint レポート生成
PPTX 生成スクリプト を使用。 複数サブスクリプションの場合は Subscription ID ごとに章を分ける。重要な推奨事項は個別ページを作り、英語原文、日本語訳、リスク、推奨アクション、メリット / デメリット、是正手順、Microsoft Learn URL を入れる。 Executive Summary と推奨アクションでは、必要に応じて WAF / CAF の観点も添える。
py -3 generate_pptx.py --title "顧客名" --data-dir ./output --output report.pptxPhase 4: 検証
1. PPTX を開いて表示確認(要素の重なりがないか) 2. 全コンテンツスライドに speaker notes があるか確認 3. 表・本文・参照 URL が 14pt 未満になっていないか確認 4. 数値の検算(コスト合計、変動率の基準月明示) 5. 英語推奨事項に日本語訳があり、「日本語訳未登録」のような placeholder が残っていないか確認 6. 社内情報・社内用語が含まれていないか最終チェック
品質基準(Lessons Learned)
レポート構造の最低要件
初回作成時に以下のセクションを この順序で 含めること。不足・順序違いは手戻りが発生する:
1. 📋 対象サブスクリプション(ID・テナント・用途推定) 2. 📈 コスト推移・分析(月別テーブル + 前月比% + サービス別月次推移 Top5-7 + 変動インサイト) 3. 🔴 エグゼクティブサマリー(優先度 🔴🟡⚪ 付き重要ポイント) 4. 💰 コスト最適化(Cost)(Advisor Cost 件数 + 推定削減ポテンシャル + 代表リソース名) 5. 🔒 セキュリティ(Security)(Advisor Security 件数 + 重要度分析) 6. ⚡ 信頼性・可用性(Reliability)(Advisor Reliability 件数 + Zone冗長/Backup 改善提案) 7. ⚙️ オペレーショナルエクセレンス(Advisor OpEx 件数。必ず取得すること。「未取得」「次回予定」は禁止。結果が0件なら「0件(推奨なし)」と記載) 8. 📊 全体サマリー(サブスク × カテゴリ一覧 + グループ別集計) 9. 🎯 推奨アクション(優先順)(優先度付き・具体的・actionable) 10. 📎 参考リンク(Azure Portal Advisor 直リンク)
データ処理の注意事項
- Azure CLI 出力は ファイルに直接保存 し、チャットに表示しない
az rest ... -o json 2>$null | Set-Content output/xxx.json- JSON の集計・ピボット処理は Python を優先(
json+collections.defaultdict) - PowerShell は DateTime 型の自動変換で
.SubString()エラーが頻発するため回避 - Cost Management API は JPY(日本リージョン)、顧客ダッシュボード CSV は USD — 通貨を必ず明示
- Cost Management Query API は 429 になりやすいため、半年分析では 月次 × ServiceName を先に取得し、合計は ServiceName 行の合算で検算する。ResourceGroup / ResourceId の深掘りはスパイク月に限定する
- コスト削減候補を提示する場合は、削除実行ではなく「要確認」「親リソースから確認」「削除候補」に分類する。
Do Not Deleteタグ、削除ロック、Backup vault、Activity Log Alerts、NetworkWatcher、managed resource group は安全側に倒して扱う - コスト上位リソース表では リソース種類・現在状態 を併記する。MTD コストには削除済み/削除中リソースも残るため、現存リソース一覧と join して誤解を避ける
- 利用中アプリに紐づくことが確認されたリソースは、コスト上位でも削除候補にしない。維持または SKU/容量見直しとして扱う
PPTX 生成のルール
汎用 PPTX 衛生ルール(スライドノート必須、フォント下限、テーブル列幅、.tmp.pptx で生成して PermissionError 回避、ヘルパー関数の扱いなど)は powerpoint-automation skill を SSOT とする。本レポート固有のルールのみ以下に残す。
- 既存フル構成の継承: 既存顧客に過去の完成度が高い PPTX / generator がある場合、別の薄い新規 deck を作らず、既存のフル構成へ最新データを差し替える。比較軸・カテゴリ別章・speaker notes が落ちると前回品質より劣化するため
- データソース取得日必須: 表紙または補足で各データソースの取得日を明示する(Cost=JPY / 顧客ダッシュボード=USD のように通貨も併記)
- スライドデータは新規記述: テンプレート流用はヘルパー関数のみ。スライドデータ部分のコピー&置換は旧顧客データが残存するため禁止
Advisor データ取得のルール
- 全 4 カテゴリを必ず取得: Cost, Security, HighAvailability, OperationalExcellence
- 「未取得」「次回予定」は禁止。結果が 0 件なら 「0件(推奨なし)」 と記載
{顧客名} Azure 環境 簡易月次レポート
レポート作成日: {YYYY年M月D日}
データ取得日: {YYYY年M月D日}(Azure Advisor / Cost Management API)
対象期間: {YYYY年M月}
対象サブスクリプション: {N} サブスクリプション
---
📋 対象サブスクリプション
| 環境 | サブスクリプション名 | Subscription ID | 年間利用額 (USD, 過去12ヶ月) |
|---|---|---|---|
| 本番系 | {name} | {id} | ${amount} |
| 評価系 | {name} | {id} | ${amount} |
---
📈 コスト推移・分析
本番系 月別コスト推移
出典: Cost Management API — {取得日} 取得。単位: JPY
| 月 | {M-5} | {M-4} | {M-3} | {M-2} | {M-1} | {M} |
|---|---|---|---|---|---|---|
| 合計 | ¥{} | ¥{} | ¥{} | ¥{} | ¥{} | ¥{} |
| 前月比 | - | {%} | {%} | {%} | {%} | {%} |
本番系 サービス別月次推移
| サービス | {M-5} | {M-4} | {M-3} | {M-2} | {M-1} | {M} | 変動({M-5}→{M}) |
|---|---|---|---|---|---|---|---|
| {Top1} | ¥{} | ¥{} | ¥{} | ¥{} | ¥{} | ¥{} | {%} |
💡 コスト変動インサイト
| 変動要因 | 分析 |
|---|---|
| {Service} {%} | {分析テキスト} |
---
🔴 エグゼクティブサマリー
| 優先度 | 項目 | 影響・インサイト |
|---|---|---|
| 🔴 高 | コスト最適化: 年間 ${amount} 削減ポテンシャル | {詳細} |
| 🔴 高 | セキュリティ: High Impact {N}件 | {詳細} |
---
💰 コスト最適化(Cost)
本番系 — 年間削減ポテンシャル: ${amount}
※ 値は Azure Portal Advisor 画面の表示値を採用しています。
| # | 推奨事項 | 対象件数 | 年間削減額 | 対象リソース例(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N}台 | ${amount} | {resource1}, {resource2}, {resource3} ほか{N}台 |
評価系 — 年間削減ポテンシャル: ${amount}
| # | 推奨事項 | 対象件数 | 年間削減額 | 対象リソース例(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N}台 | ${amount} | {resource1} ほか{N}台 |
---
🔒 セキュリティ(Security)
本番系 — Advisor Score: {N}% | Total: {N}件 / High: {N}件
| # | 推奨事項 | 件数 | 影響度 | 対象リソース(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N} | High | {resource1}, {resource2}, {resource3} ほか{N}台 |
評価系 — Advisor Score: {N}% | Total: {N}件 / High: {N}件
| # | 推奨事項 | 件数 | 影響度 | 対象リソース(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N} | High | {resource1} ほか{N}台 |
💡 セキュリティ 推奨ネクストアクション
| 対応期間 | アクション | 対象 |
|---|---|---|
| 即時 | {アクション} | {対象リソース} |
---
🛡️ 信頼性・可用性(Reliability)
本番系 — Advisor Score: {N}% | Total: {N}件 / High: {N}件
| # | 推奨事項 | 件数 | 影響度 | 対象リソース(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N} | High | {resource1} ほか{N}台 |
評価系 — Advisor Score: {N}% | Total: {N}件 / High: {N}件
| # | 推奨事項 | 件数 | 影響度 | 対象リソース(代表3件 + 残数) |
|---|---|---|---|---|
| 1 | {推奨} | {N} | High | {resource1} ほか{N}台 |
---
📊 全体サマリー
| カテゴリ | 本番系 Total | 本番系 High | 評価系 Total | 評価系 High | 合計 Total | 合計 High | 年間削減額 |
|---|---|---|---|---|---|---|---|
| Cost | {N} | {N} | {N} | {N} | {N} | {N} | ${amount} |
| Security | {N} | {N} | {N} | {N} | {N} | {N} | - |
| Reliability | {N} | {N} | {N} | {N} | {N} | {N} | - |
| OpEx | {N} | {N} | {N} | {N} | {N} | {N} | - |
---
🎯 推奨アクション(優先順)
| 優先度 | アクション | カテゴリ | 対象数 | 期待効果 | 対応期間 |
|---|---|---|---|---|---|
| 🔴 1 | {アクション} | Cost | {対象} | ${amount}/年 | 即時 |
---
📎 参考リンク
| リンク | URL |
|---|---|
| 本番系 Advisor | Azure Portal |
| 評価系 Advisor | Azure Portal |
---
❗ 免責事項: 本レポートのコスト削減見込額は Azure Advisor の推定値に基づく概算値です。実際の削減額は、利用状況・価格改定・為替変動等により異なる場合があります。推奨事項の件数・対象リソースは日々変動するため、最新の状況は Azure Portal からご確認ください。
データ取得日: {YYYY年M月D日}
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2025-2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2025-2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
データ取得手順
前提条件
- Azure CLI (
az) がインストール済み - 対象サブスクリプションへの Reader 以上の権限(Azure Copilot 経由含む)
Step 1: 日付確認・対象月設定
Get-Date -Format "yyyy-MM-dd (ddd)"引数で対象月が指定されていればその月を使用、省略時は当月。 コスト取得期間: 対象月の6ヶ月前〜対象月末。
Step 2: テナント/サブスクリプション切替
az account set --subscription {subscriptionId}
az account show --query "{name:name, id:id, tenantId:tenantId}" -o jsonaz account set が対象 subscription を見失う場合でも、既存認証で対象 tenant の ARM token が取れることがある。対話ログインへ進む前に、tenant-scoped token + REST 経路を確認する。
az account get-access-token `
--tenant {tenantId} `
--resource https://management.azure.com/ `
--query "{tenant:tenant,expiresOn:expiresOn,subscription:subscription}" `
-o json- token が取れる場合:
az account setに依存しない REST helper で Cost / Advisor を取得する - token が取れない場合:
az login --tenant {tenantId}を実行し、ログイン後に再確認する - token 本体は保存・表示しない。出力する場合は tenant / expiresOn などのメタデータだけにする
Step 2.5: 事前チェック(API 経路の確定)
コスト系 API は「サブスクリプションが見えている」だけでは足りない。対象テナントで見えているか / offer type は何か / どの API がその offer type をサポートするか を先に確定する。
2.5.1 サブスクリプション可視性チェック
az account list --all --query "[?id=='{subscriptionId}'].{name:name,id:id,tenantId:tenantId,state:state}" -o json- 空配列なら、まず tenant を見直す
az account set --subscription {subscriptionId}が失敗しても、対象 tenant の ARM token が取れる場合は REST 経路で続行できる
2.5.2 offer type / billing 情報チェック
az rest --method get `
--url "https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Billing/billingProperty/default?api-version=2024-04-01" `
-o json > billing-property.jsonbilling-property.jsonで billing 系の前提を保存しておく- 詳細レポート API 可否に迷う場合は、この時点で subscription type / offer type を確認する
2.5.3 API 選択の決定木
1. まず Microsoft.CostManagement/query を試す 2. 429 Too many requests なら短時間の多重再試行はせず、別経路へ切り替える 3. generateCostDetailsReport が 422 Unsupported request かつ offer type: WebDirect なら、Cost Details API は使えない前提で legacy Usage Details API に切り替える 4. WebDirect では、月次総額と TopN 集計は Microsoft.Consumption/usageDetails のページング取得を使う
実績ベースの知見: WebDirect ではCostManagement/queryが 429、generateCostDetailsReportが 422 で塞がるケースがある。その場合でも legacy Usage Details API から月次集計は取得できる。
Step 3: Advisor 推奨取得
Advisor 取得も、コスト取得と同様に subscription 可視性と取得経路の確定 を先に行う。
Step 3.1: Advisor preflight
az account list --all --query "[?id=='{subscriptionId}'].{name:name,id:id,tenantId:tenantId,state:state}" -o json
az account set --subscription {subscriptionId}
az account show --query "{name:name,id:id,tenantId:tenantId}" -o json- 空配列なら tenant を見直す
az account set --subscription {subscriptionId}が失敗しても、対象 tenant の ARM token が取れる場合は REST 経路で続行できる- CLI 経路で取得する場合は、取得前に subscription context がズレていないことを毎回確認する
Step 3.2: CLI と REST の役割分担
az advisor recommendation listは手軽だが、CLI help 上の--categoryはCost,HighAvailability,Performance,Securityしか出ない- 一方、REST の recommendation list は
OperationalExcellenceを正式サポートする - 4 カテゴリ完全取得が必要なこの skill では、REST を正本の取得経路とする
実績ベースの知見: CLI help に OperationalExcellence が出ないため、CLI だけに依存すると OpEx を取り漏らすリスクがある。各カテゴリごとに取得:
az advisor recommendation list --category Cost -o json > advisor-cost.json
az advisor recommendation list --category Security -o json > advisor-security.json
az advisor recommendation list --category HighAvailability -o json > advisor-reliability.json
az advisor recommendation list --category OperationalExcellence -o json > advisor-opex.jsonただし OperationalExcellence は CLI help と不整合があるため、実運用では下記 REST helper を優先する。Step 3.3: 推奨取得(推奨: REST helper)
.\scripts\Get-Advisor-Recommendations.ps1 -SubscriptionId {subscriptionId} -OutputDir output出力ファイル:
advisor-cost.jsonadvisor-security.jsonadvisor-reliability.jsonadvisor-opex.jsonadvisor-preflight.json
Step 3.4: REST fallback
REST endpoint は次を使う。
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Advisor/recommendations?api-version=2025-01-01&$filter=Category eq 'OperationalExcellence'- filter 対象は
Category - category 値は
Cost,Security,HighAvailability,OperationalExcellence OperationalExcellenceは REST の正式カテゴリとして扱う- ページングがある場合は
nextLinkをたどる
Step 3.5: 失敗時の切り分け
az advisor recommendation listが category 不正扱い、または help に category が出ない:- REST helper に切り替える
az account set --subscription ...失敗:- まず tenant-scoped ARM token が取れるか確認する。取れる場合は REST helper に切り替え、取れない場合だけ Step 2 の tenant login へ戻る
- 一部カテゴリだけ 0 件:
- 取得失敗と決め打ちせず、まず
0件(推奨なし)を候補にする - ただし OpEx だけ 0 件で CLI 経路を使っていた場合は REST で再確認する
カテゴリ別集計
$data = Get-Content "advisor-{category}.json" -Raw | ConvertFrom-Json
$data | Group-Object impact | ForEach-Object {
Write-Host "$($_.Name): $($_.Count)"
}High Impact の推奨別グルーピング
$data | Where-Object { $_.impact -eq 'High' } |
Group-Object { $_.shortDescription.problem } |
Sort-Object Count -Descending |
ForEach-Object {
Write-Host "[$($_.Count)件] $($_.Name)"
$_.Group | Select-Object -First 3 | ForEach-Object {
Write-Host " -> $($_.impactedValue)"
}
}RI 推奨の注意
CLI は lookback(7/30/60日) × term(P1Y/P3Y) の組み合わせ別にレコードを返す。 Portal の値を SSOT とし、CLI は参考値として使用する。
Step 4: Cost Management API(月別・サービス別)
まずはMicrosoft.CostManagement/queryを使う。Get-AzConsumptionUsageDetailは subscription scope でBadRequestやSubscription scope usage is not supported for current api versionになることがあり、その場合は legacy cmdlet で粘らず REST に切り替える。
Step 4.1: 半年コスト分析の推奨取得順
半年分のコスト分析では、最初から ResourceId / ResourceGroupName / ResourceType まで全月分を一括取得しない。Cost Management Query API は短時間に粒度の細かい query を連打すると 429 Too Many Requests になりやすいため、次の順序で段階的に取得する。
1. 月次 × ServiceName を各月 1 クエリで取得する
timeframe=CustomtimePeriod.from={YYYY-MM-01}timePeriod.to={翌月のYYYY-MM-01}(終了日は排他的に扱う)granularity=Nonegrouping=ServiceName
2. 月別合計は、total query が 429 になる場合でも ServiceName 行の合計を正本として算出する 3. サービス別推移でスパイク月・支配サービスを特定する 4. 深掘りはスパイク月だけ ResourceGroupName → ResourceId の順で取得する 5. ResourceId / ResourceGroupName の全月一括取得は最後の手段にし、実行する場合は月ごとに十分な待機時間を入れる
この順序にすると、まず「何が何割を占めるか」を安定して把握でき、詳細 API のスロットリングで分析全体が止まるリスクを下げられる。
実績ベースの注意:
Azure Firewall、VPN Gateway、Load Balancer、Microsoft Defender for Cloud、Virtual Network、Storage、Backupはコスト上位になりやすいため、月次推移に必ず含めるVPN GatewayやAzure Firewallは短期間でも大きな割合を占めるため、スパイク月では優先して RG / ResourceId 明細を確認するAzure Cognitive Search、Recovery Services vault、Container Registry、Cognitive Services S0は小規模環境でも上位に出ることがある。上位表では ServiceName だけでなく ResourceType / リソース種類 も併記する- 削除候補を出す場合でも、この skill は読み取り専用レポート生成用である。
Do Not Deleteタグ、削除ロック、Backup vault、Activity Log Alerts、NetworkWatcher、managed resource group(MC_*,ME_*, Databricks managed RG など)は「要確認」または「親リソースから確認」として扱い、削除実行はしない - ユーザーまたは運用担当が「アプリで使用中」と明言したリソースは、コスト上位でも削除候補にしない。レポートでは「利用中のため維持」「必要ならSKU/容量見直し」として扱う
$subId = "{subscriptionId}"
$bodyFile = Join-Path $env:TEMP "cost-query.json"
@{
type = "ActualCost"
dataset = @{
granularity = "None"
aggregation = @{ totalCost = @{ name = "PreTaxCost"; function = "Sum" } }
grouping = @(@{ type = "Dimension"; name = "ServiceName" })
}
timeframe = "Custom"
timePeriod = @{ from = "{YYYY-MM-01}"; to = "{翌月のYYYY-MM-01}" }
} | ConvertTo-Json -Depth 5 | Set-Content $bodyFile -Encoding utf8
az rest --method post `
--url "https://management.azure.com/subscriptions/$subId/providers/Microsoft.CostManagement/query?api-version=2025-03-01" `
--body "@$bodyFile" --headers "Content-Type=application/json" -o json > cost-monthly.json通貨は日本リージョンの場合 JPY で返る。
既存環境で2025-03-01が通らない場合は2023-11-01または2023-03-01を試す。
Step 4.2: 上位リソース分析(ResourceId + 種類の併記)
サービス別の支配要因が分かったら、対象月または当月 MTD だけ ResourceId 粒度で取得する。レポートの上位リソース表には、最低限 Cost, 割合, ResourceGroupName, ServiceName, ResourceName, ResourceType, 現在状態 を含める。
$bodyFile = Join-Path $env:TEMP "cost-resource-query.json"
@{
type = "ActualCost"
timeframe = "MonthToDate"
dataset = @{
granularity = "None"
aggregation = @{ totalCost = @{ name = "PreTaxCost"; function = "Sum" } }
grouping = @(
@{ type = "Dimension"; name = "ResourceId" },
@{ type = "Dimension"; name = "ResourceGroupName" },
@{ type = "Dimension"; name = "ServiceName" }
)
}
} | ConvertTo-Json -Depth 8 | Set-Content $bodyFile -Encoding utf8
az rest --method post `
--url "https://management.azure.com/subscriptions/$subId/providers/Microsoft.CostManagement/query?api-version=2025-03-01" `
--body "@$bodyFile" --headers "Content-Type=application/json" -o json > cost-resource-mtd.json
az resource list --subscription $subId `
--query "[].{id:id,type:type,name:name,resourceGroup:resourceGroup,sku:sku.name,kind:kind}" `
-o json > current-resources.json集計時は ResourceId を current-resources.json に join し、次を判定する。
current: 現在も存在するdeleting: Resource Group がDeletingdeletedOrNotFound: MTD コストには残るが、現在は存在しない
注意: Cost Management の MonthToDate / 過去月データは 発生済みコスト であり、削除済みリソースも当月明細に残る。削除後の効果を見る場合は、当月 MTD だけでなく翌日以降または翌月の発生額で確認する。
Step 4.3: Backup / RSV 系リソースの削除完了判定
Recovery Services vault / Backup vault / Data Protection Backup Vault は、削除要求が通っても即時に RG から消えないことがある。コスト削減レポートや削除後確認では、次の順で状態を分けて記録する。
1. Microsoft.RecoveryServices/vaults が残っているか確認する 2. Microsoft.DataProtection/backupVaults が残っているか確認する 3. Recovery Services vault は protected item / container / storage account registration が残っていないか確認する 4. Backup vault は deletedBackupInstances を確認し、SoftDeleted と scheduledPurgeTime を記録する 5. scheduledPurgeTime がある場合、削除は「完了」ではなく soft delete retention 待ち として扱う
# Recovery Services / Data Protection の残存確認
az resource list --subscription $subId `
--query "[?contains(type, 'RecoveryServices') || contains(type, 'DataProtection')].{RG:resourceGroup,Type:type,Name:name}" `
-o table
# Data Protection Backup Vault の soft-deleted backup instance 確認
$vaultId = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.DataProtection/backupVaults/{backupVaultName}"
az rest --method get `
--url "https://management.azure.com${vaultId}/deletedBackupInstances?api-version=2023-01-01" `
--query "value[].{name:name,state:properties.currentProtectionState,purge:properties.deletionInfo.scheduledPurgeTime,billingEnd:properties.deletionInfo.billingEndDate}" `
-o table実績ベースの注意:
- Recovery Services vault は、VM backup / Azure Files backup などの保護停止 + backup data 削除後も、container / storage account registration が残ると vault delete が拒否される
- Data Protection Backup Vault は、backup instance 削除後に
SoftDeletedとして残り、scheduledPurgeTimeまで vault delete が拒否されることがある - RG が
Deletingになっている間は vault の soft delete 設定変更がResourceGroupBeingDeletedで拒否されることがある - 最新 API では Backup Vault の soft delete が Always-On 前提になる場合があり、
DppAlwaysOnSoftDeleteStateMandatoryが出たら即時削除ではなく retention 待ちとして報告する - レポートでは「削除要求済み」「保護項目削除済み」「soft delete retention 待ち」「完全削除済み」を分けて書く
公式 Docs:
- Delete Azure Backup Recovery Services vault: https://learn.microsoft.com/en-us/azure/backup/backup-azure-delete-vault
- Azure Backup soft delete / secure by default: https://learn.microsoft.com/en-us/azure/backup/backup-azure-security-feature-cloud
query が 429 のとき
429 Too many requestsが返る環境では、同一ターン内での短時間連打を避ける- その場で必要な値が「月次総額」「サービス別 TopN」であれば、まず 月次 × ServiceName の単月 query に絞る
ResourceId/ResourceGroupName/ResourceTypeの細粒度 query は、スパイク月が特定できるまで実行しないServiceNamequery が 429 でも、時間を空けたResourceId + ResourceGroupName + ServiceNamequery が通る場合がある。失敗結果は保存し、API バージョン変更だけを連打しない- 単月 ServiceName query でも 429 が続く場合は、後述の legacy Usage Details 集計へ切り替える
- デバッグ証跡は JSON に保存し、チャットには値だけを出す
Step 4.5: Cost Details API(使える offer type の場合のみ)
Microsoft.CostManagement/generateCostDetailsReport は非同期で詳細 CSV を返す。 ただし offer type により非対応 があるため、422 Unsupported request を返したら粘らず fallback する。
$token = az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv
$headers = @{ Authorization = "Bearer $token"; 'Content-Type' = 'application/json' }
$body = '{"metric":"ActualCost","timePeriod":{"start":"{YYYY-MM-01}","end":"{YYYY-MM-DD}"}}'
$resp = Invoke-WebRequest -Method Post `
-Uri "https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2025-03-01" `
-Headers $headers -Body $body
$resp.Headers.Location
$resp.Headers.'Retry-After'Locationが返れば poll して CSV を取得する422でoffer type: WebDirectが含まれる場合は この経路を打ち切る
Step 4.6: legacy Usage Details API(WebDirect / fallback 用)
query が 429、Cost Details API が offer type 非対応のときは、legacy の Microsoft.Consumption/usageDetails で月次集計を作る。
1件サンプル取得
$token = az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv
$headers = @{ Authorization = "Bearer $token" }
$uri = "https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Consumption/usageDetails?metric=ActualCost&`$filter=properties%2FusageStart%20ge%20'{YYYY-MM-01}'%20and%20properties%2FusageEnd%20le%20'{YYYY-MM-DD}'&`$top=1&api-version=2019-10-01"
Invoke-RestMethod -Method Get -Uri $uri -Headers $headers |
ConvertTo-Json -Depth 20 |
Set-Content usage-details-top1.json -Encoding utf8確認ポイント:
properties.costproperties.billingCurrencyproperties.consumedServicenextLink
全ページ集計(推奨: Python helper)
$env:AZURE_ACCESS_TOKEN = az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv
py -3 .\scripts\get-legacy-usage-summary.py {subscriptionId} {YYYY-MM-01} {YYYY-MM-DD} usage-summary.json
# ページングやローカル接続枯渇が不安定な場合は、単月単発 + 大きめ page size でページングを避ける
py -3 .\scripts\get-legacy-usage-summary.py {subscriptionId} {YYYY-MM-01} {YYYY-MM-DD} usage-summary.json 5000- 出力は
usage-summary.jsonに保存する nextLinkをたどって全ページ集計する- 集計キーは
consumedServiceを優先し、欠損時のみproduct/resourceNameにフォールバックする - 通貨は
billingCurrencyを使う
実装上の注意
- page size は 100 程度から始める。ページングで
WinError 10048が出る場合は、単月単発で5000程度へ上げ、1ページ取得を優先する nextLinkは相対 URL を返すことがあるため、https://management.azure.comを補うnextLink内に空白が残る場合があるので%20へエンコードする- Windows / GSA 環境では
az restや Azure CLI Python module を月ごと・ページごとに連打するとWinError 10048になることがある。az account get-access-tokenはトークン取得だけに使い、Usage Details は Pythonhttp.clientで単月単発取得する - 複数 tenant を同時ログインできない環境では、tenant ごとに MFA → その tenant のサブスクだけ収集 → 次 tenant へ切り替える。未認証 tenant を混ぜて一括収集しない
- 件数が多いサブスクリプションではページ数と総件数も一緒に保存する
失敗時の切り分け
429 Too many requests:- 同じ月・同じ scope で query を連打しない
- 月次レポート用途なら legacy
usageDetailsへ切り替えて総額と TopN を先に確定する 422 Unsupported request. offer type: WebDirect ...:generateCostDetailsReportはこのサブスクでは使わない- legacy
usageDetailsを使う The subscription ... doesn't exist in cloud 'AzureCloud':- サブスク未可視。tenant 切り替えからやり直す
- Usage Details が 200 +
value: []/itemCount: 0: - 認証エラーではなく、subscription scope に billed usage 明細が無い可能性が高い
- Azure RBAC を確認し、Owner / Contributor / Reader があるなら resource/subscription 権限不足とは切り分ける
- MCA / Internal / DevTest / Sandbox などでは billing profile / invoice section scope 側に費用が寄る可能性があるため、
billingProperty.defaultのbillingProfileId/invoiceSectionIdを確認する - billing role assignments API が
403 Forbidden: - subscription RBAC ではなく billing scope の権限不足。Billing account / Billing profile / Invoice section の Reader 以上に相当する権限を持つアカウントで確認する
実務で使う集計列
CostまたはPreTaxCostのどちらが返っているかを確認するCurrencyを必ず併記する- サービス別集計は
ServiceName、月別集計はMonthまたはUsageStartTimeを使う
月別合計の集計
$raw = Get-Content "cost-monthly.json" -Raw | ConvertFrom-Json
$monthly = @{}
foreach ($row in $raw.properties.rows) {
$month = ($row[1] -split ' ')[0]
$cost = [double]$row[0]
if ($monthly.ContainsKey($month)) { $monthly[$month] += $cost }
else { $monthly[$month] = $cost }
}
$monthly.GetEnumerator() | Sort-Object Name | ForEach-Object {
Write-Host ("{0}: {1:N0}" -f $_.Name, $_.Value)
}Step 5: 全サブスクリプションで繰り返し
複数サブスクリプションがある場合、Step 2〜4 を各サブスクで実行する。
ただしサブスクごとに offer type と API の通り方が違う。Step 2.5 の事前チェックを各サブスクで毎回実施してから取得経路を選ぶこと。
レポート品質ルール
顧客に提示するレポート(Markdown / PowerPoint)生成時に適用するガードレール。
社内情報の除外(MANDATORY)
以下を顧客向けレポートに 含めてはならない:
| 禁止項目 | 例 |
|---|---|
| 社内ツール名・リンク | 社内専用の分析ツール・ダッシュボード等 |
| 社内用語 | ACR, TPID, TTM 等 |
| CLI コマンド名 | az advisor recommendation list 等 |
| データソース詳細 | 「Cost Management API (ActualCost)」等 |
| 社内 URL | 社内専用ポータルの URL 等 |
| 社内専用分析データ | 社内ツールの Focus Areas 等 |
代替表記:
- ACR → 「年間利用額」
- 取得方法 → 記載しない
理由: 社内ツールで取得したデータは顧客がアクセスできない。顧客自身が Azure Portal で確認可能な情報のみ をレポートに含めること。
数値の正確性
Azure Advisor コスト推奨
- SSOT は Azure Portal の Advisor 画面
- CLI は RI 推奨を lookback×term の組み合わせ別に返すため、単純合計すると過大評価になる
- Portal の値と差異がある場合は Portal を優先
- Portal 確認できない場合は CLI 値 + 「概算値」注記
通貨
- Cost Management API は JPY で返す(日本リージョン)
- レポート内で通貨を必ず明示し、混在させない
変動率
- テーブルの「変動」列には 比較基準を明記 する(例:「変動(10月→3月)」)
免責事項(MANDATORY)
表紙スライド
表紙に以下の注記をグレー文字で含める:
※値はあくまで参考とし、実際の値は Azure Portal でご確認お願いしますレポート末尾
レポート末尾(最終スライド / Markdown 末尾)に以下を含める:
コスト削減見込額は Azure Advisor の推定値に基づく概算値です。
実際の削減額は、利用状況・価格改定・為替変動等により異なる場合があります。
推奨事項の件数・対象リソースは日々変動するため、最新の状況は Azure Portal からご確認ください。インサイト・コメントの記載ルール(MANDATORY)
レポート内のインサイト・分析コメント・推奨アクションは、取得した実データに基づいて記載すること。
| ルール | 説明 |
|---|---|
| 推測で書かない | データで裏付けられない仮説・憶測を「事実」として記載しない |
| 根拠を明示 | 「XXが高い」ではなく「XXが N 件(Advisor 取得値)」のように数値・出典を付ける |
| 不確実な場合は明示 | 「可能性があります」「確認推奨」等の表現を使い、断定しない |
| 原因推定は仮説と明記 | 「○○が原因です」→「○○が原因と推定されます。確認を推奨します」 |
禁止例:
- ✘ 「Storage が減少したのはオンプレ回帰のためです」(証拠なしの断定)
- ✘ 「このリソースは不要です」(利用状況未確認の断定)
良い例:
- ○ 「Storage が 63% 減少しています。データ移行・削除・オンプレ回帰などの可能性があり、利用トレンドの確認を推奨します」
- ○ 「Advisor が 14 台の VM を低使用率と判定しています。利用率を確認のうえ、サイズダウンまたは停止スケジュールの設定をご検討ください」
コスト上位リソースの扱い
- 上位リソース表には リソース名だけでなく種類(例: Search service, Recovery Services vault, Container Registry, Cognitive Services) を併記する
- MonthToDate のコストは発生済みコストであり、削除済み・削除中リソースも表示される。現在状態が分かる場合は
Current / Deleting / Deletedを併記する - ユーザーまたは運用担当が「アプリで使用中」と確認したリソースは、コスト上位でも削除候補と断定しない。推奨は「維持」「SKU/容量見直し」「利用量監視」に留める
- Backup / Recovery Services vault は削除要求後も
Deletingが長く続くことがある。削除効果は即時ではなく、翌日以降または翌月のコストで確認する - Backup Vault / Data Protection Backup Vault は soft-deleted backup instance の保持により、
scheduledPurgeTimeまで完全削除できないことがある。この場合は「削除失敗」ではなく「soft delete retention 待ち」として、purge 予定時刻を明記する
WAF / CAF 観点の添え方
Executive Summary と推奨アクションでは、単に Advisor 件数を並べず、必要に応じて Microsoft Learn の公式フレームワークに沿って意味付けする。
- Well-Architected Framework: Reliability / Security / Cost Optimization / Operational Excellence / Performance Efficiency のどの柱に効くかを添える
- Cloud Adoption Framework: Govern / Secure / Manage / Ready など、運用・統制のどの活動に関係するかを添える
- WAF / CAF はコメントの補助軸であり、Advisor 取得値や Cost 実データを置き換える根拠にしない
PowerPoint テンプレート設定
| 項目 | 値 |
|---|---|
| フォント | BIZ UDPゴシック |
| 本文サイズ | 14pt 以上 |
| テーブルサイズ | 14pt 以上 |
| インサイトボックス | 14pt 以上 |
| ヘッダー | 24pt |
| サブヘッダー | 14pt 以上 |
| スライドサイズ | 13.333 x 7.5 inch(ワイドスクリーン) |
定例運用向け Azure Brief 構成
顧客定例で毎月見せる Azure Brief は、Advisor 件数の羅列ではなく「今月の変化 → 優先論点 → 次アクション」の順にする。
- 表紙で対象範囲、取得日、免責を明示し、環境ごとの Subscription ID / 用途 / 年間利用額を小さな表で置く
- 序盤に Executive Summary を置き、優先度付きで Security / Reliability / Cost / OpEx の要点を 3〜5 件に絞る
- Cost は月別合計だけでなく、主要サービス Top 5 と前月比を並べる。月途中データは実績値と月末投影を分けて表示する
- 複数環境がある場合は、本番系と評価系を同じ軸で並べ、差分や運用上の意味が分かるコメントを添える
- Advisor はカテゴリ別サマリー表を置いたうえで、件数が多いカテゴリは環境別に分ける。1 枚に詰めて小さい文字へ逃げない
- 各カテゴリスライドには、左側に取得値・上位推奨、右側または下部にインサイト / 推奨アクションのボックスを置く
- 推奨アクションは「確認」「計画」「実施」の粒度が分かる動詞で書き、削除・変更の断定は避ける
- 内部的な取得経路やコマンドの説明はスライド面に出さず、必要なら speaker notes に限定する
スライドノート(MANDATORY)
各カテゴリ(Cost / Security / Reliability)のスライドに、件数 TOP3 の推奨事項の説明をスライドノートに含める。
各推奨事項のノートには以下を含める:
- 何が問題か: 具体的に何が未設定/未対応なのか
- リスク: 放置した場合に何が起きるか
- 推奨アクション: 具体的に何をすればよいか
これにより発表者がスライドを説明する際の補足情報として活用できる。
# python-pptx でのノート追加例
notes = slide.notes_slide
notes.notes_text_frame.text = "ノートテキスト"複数サブスクリプションの PPTX 構成
複数サブスクリプションを扱う PowerPoint では、環境名だけでまとめず Subscription ID ごとに章を分ける。
推奨構成:
1. 表紙 2. エグゼクティブサマリー 3. 全体コスト推移 4. Subscription ID ごとのセクション区切り 5. 各サブスクリプションの Cost 状況 6. 各サブスクリプションの Advisor 推奨事項 7. 推奨アクション / 免責事項
章区切りスライドには Subscription ID と Tenant ID を明記し、発表時にどの環境の話か迷わないようにする。 Cost スライドには月別合計、明細件数、主要サービス別推移を入れる。Advisor Overview スライドにはカテゴリ別件数と件数上位の推奨事項を入れる。重要な推奨事項は個別ページを作り、英語原文、その直下の日本語訳、リスク、推奨アクション、メリット / デメリット、是正手順、Microsoft Learn URL を入れる。全体推奨アクションも別途まとめる。
是正手順は Microsoft Learn の公式 URL を根拠にする。コマンドや具体的な操作例を書く場合は microsoft_code_sample_search で公式サンプルを確認する。code sample search が不安定な場合でも、Docs search / fetch で取得できた公式ページを使い、根拠なしの手順やコマンド例を書かない。
英語の Advisor 推奨事項は、そのまま出さず必ず直下に日本語訳を添える。辞書未登録の場合でも「日本語訳未登録」と表示せず、最低限、自然な日本語要約へ置き換える。
PPTX 生成後チェック
生成後は以下を確認してから完了扱いにする:
- PPTX ファイルが存在し、PowerPoint で開けること
- 全コンテンツスライドに speaker notes があること
- テーブルや RefURL がスライド外へはみ出していないこと
- PowerPoint COM で
TextRange.BoundHeight/BoundWidthを確認し、実レンダリング上の overflow が 0 件であること - 表・本文・参照 URL の表示テキストに 14pt 未満がないこと
- 旧顧客名・旧 Subscription ID・旧 URL が残っていないこと
- 生成に使った Cost / Advisor の JSON と PPTX 上の主要数値が一致すること
ファイル命名規則
| ファイル | 命名規則 | 例 |
|---|---|---|
| PowerPoint | Azure-Brief-{YYYYMMDD}.pptx | Azure-Brief-20260331.pptx |
| Markdown | Azure-Brief-{YYYYMMDD}.md | Azure-Brief-20260331.md |
| PPTX生成スクリプト | _gen_monthly_report.py | 固定名 |
Azure Portal リンク
テナント指定付きリンクを使用:
https://portal.azure.com/#@{tenantId}/blade/Microsoft_Azure_Expert/AdvisorMenuBlade/{category}/subscriptionId/{subscriptionId}"""
Azure Advisor Monthly Report - PowerPoint Generator (Template)
Usage:
py -3 generate_pptx.py --output report.pptx
This script generates a widescreen PowerPoint report with Azure Advisor
recommendations and cost trend analysis.
Customize the DATA section below with actual values from Azure CLI / Cost Management API.
"""
import sys, io, os, argparse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
from pptx import Presentation
from pptx.util import Inches, Pt, Cm
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE
# ============================================================
# CONFIG - Customize these
# ============================================================
FONT_ASCII = 'Calibri'
FONT_EAST_ASIAN = 'BIZ UDPGothic'
MIN_CONTENT_FONT_SIZE = 14
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
# Layout constants (prevent overlap)
HEADER_H = Inches(1.1) # Header bar height
CONTENT_TOP = Inches(1.3) # Content starts below header
MARGIN = Inches(0.5) # Page margin
FULL_W = Inches(12.3) # Full-width content
LEFT_W = Inches(7.5) # Left column width (60%)
RIGHT_X = Inches(8.3) # Right column X position
RIGHT_W = Inches(4.5) # Right column width (40%)
GAP = Inches(0.3) # Vertical gap between blocks
# Colors
DARK_BLUE = RGBColor(0x00, 0x32, 0x6E)
MS_BLUE = RGBColor(0x00, 0x78, 0xD4)
RED = RGBColor(0xE7, 0x4C, 0x3C)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
LIGHT_GRAY = RGBColor(0xF0, 0xF0, 0xF0)
DARK_GRAY = RGBColor(0x33, 0x33, 0x33)
LIGHT_BLUE = RGBColor(0xD6, 0xEA, 0xF8)
LIGHT_RED = RGBColor(0xFA, 0xDB, 0xD8)
LIGHT_GREEN = RGBColor(0xD5, 0xF5, 0xE3)
# ============================================================
# HELPER FUNCTIONS
# ============================================================
def _set_font(font, size, bold=False, color=DARK_GRAY):
"""Apply consistent font settings."""
font.size = Pt(max(size, MIN_CONTENT_FONT_SIZE))
font.bold = bold
font.color.rgb = color
font.name = FONT_ASCII
font._element.attrib['{http://schemas.openxmlformats.org/drawingml/2006/main}altLang'] = 'ja-JP'
# Set East Asian font via XML for proper fallback
from pptx.oxml.ns import qn
rPr = font._element
ea = rPr.find(qn('a:ea'))
if ea is None:
from lxml import etree
ea = etree.SubElement(rPr, qn('a:ea'))
ea.set('typeface', FONT_EAST_ASIAN)
def add_bg(sl, l, t, w, h, c):
"""Add colored rectangle background."""
s = sl.shapes.add_shape(MSO_SHAPE.RECTANGLE, l, t, w, h)
s.fill.solid(); s.fill.fore_color.rgb = c; s.line.fill.background()
def tb(sl, l, t, w, h, txt, fs=12, b=False, c=DARK_GRAY, a=PP_ALIGN.LEFT):
"""Add text box with proper font settings."""
bx = sl.shapes.add_textbox(l, t, w, h); tf = bx.text_frame; tf.word_wrap = True
p = tf.paragraphs[0]; p.text = txt; _set_font(p.font, fs, b, c); p.alignment = a
return tf
def ap(tf, txt, fs=12, b=False, c=DARK_GRAY):
"""Add paragraph to text frame."""
p = tf.add_paragraph(); p.text = txt; _set_font(p.font, fs, b, c)
p.space_before = Pt(2)
def tbl(sl, l, t, w, h, data, hc=MS_BLUE, col_widths=None):
"""Add table with header coloring and optional column width ratios."""
r, co = len(data), len(data[0])
ts = sl.shapes.add_table(r, co, l, t, w, h).table
# Apply column widths if specified (list of relative proportions)
if col_widths and len(col_widths) == co:
total = sum(col_widths)
for j, cw in enumerate(col_widths):
ts.columns[j].width = int(w * cw / total)
for i, rd in enumerate(data):
for j, ct in enumerate(rd):
cell = ts.cell(i, j); cell.text = str(ct)
for p in cell.text_frame.paragraphs:
_set_font(p.font, MIN_CONTENT_FONT_SIZE, bold=(i == 0), color=WHITE if i == 0 else DARK_GRAY)
if i == 0:
p.alignment = PP_ALIGN.CENTER
if i == 0:
cell.fill.solid(); cell.fill.fore_color.rgb = hc
elif i % 2 == 0:
cell.fill.solid(); cell.fill.fore_color.rgb = LIGHT_GRAY
def hdr(sl, title, sub=None):
"""Slide header bar."""
add_bg(sl, Inches(0), Inches(0), SLIDE_W, HEADER_H, DARK_BLUE)
tb(sl, MARGIN, Inches(0.2), Inches(12), Inches(0.6), title, fs=24, b=True, c=WHITE)
if sub:
tb(sl, MARGIN, Inches(0.65), Inches(12), Inches(0.4), sub, fs=13,
c=RGBColor(0xA0, 0xC0, 0xE0))
def ibox(sl, l, t, w, h, title, lines, bg=LIGHT_BLUE, tc=DARK_BLUE):
"""Insight box with background."""
add_bg(sl, l, t, w, h, bg)
tf = tb(sl, l + Inches(0.1), t + Inches(0.05), w - Inches(0.2), h - Inches(0.1),
title, fs=12, b=True, c=tc)
for ln in lines:
ap(tf, ln, fs=11, c=DARK_GRAY)
def add_notes(sl, text):
"""Add speaker notes to a slide."""
notes_slide = sl.notes_slide
notes_slide.notes_text_frame.text = text
# ============================================================
# DATA - Replace with actual values
# ============================================================
REPORT_TITLE = "Azure 環境 簡易月次レポート"
REPORT_DATE = "2026年3月31日"
CUSTOMER_NAME = "" # e.g., "お客様名様"
# Subscription info: [(env_label, sub_name, sub_id_short, annual_usage)]
SUBSCRIPTIONS = [
("本番系", "従量課金", "xxxxxxxx-...", "$XXX,XXX"),
("評価系", "従量課金(評価)", "yyyyyyyy-...", "$XX,XXX"),
]
# Cost trend: monthly totals (JPY)
COST_MONTHS = ["2025/10", "2025/11", "2025/12", "2026/01", "2026/02", "2026/03"]
COST_PROD = ["0", "0", "0", "0", "0", "0"] # Production monthly
COST_PROD_MOM = ["-", "+0%", "+0%", "+0%", "+0%", "+0%"] # MoM change
COST_EVAL = ["0", "0", "0", "0", "0", "0"] # Evaluation monthly
# Service breakdown: [service, m1, m2, m3, m4, m5, m6, trend]
COST_SERVICES_PROD = [
["Service A", "0", "0", "0", "0", "0", "0", "横ばい"],
]
# Advisor recommendations: [recommendation, count, impact, resources]
ADVISOR_COST_PROD = [
["VM 適正サイズ化", "N台", "$XX,XXX", "vm-name-1, vm-name-2 ほかN台"],
]
ADVISOR_COST_EVAL = [
["VM 適正サイズ化", "N台", "$XX,XXX", "vm-name-1 ほかN台"],
]
ADVISOR_SEC_PROD = [
["推奨事項", "N", "High", "resource-1, resource-2 ほかN台"],
]
ADVISOR_SEC_EVAL = [
["推奨事項", "N", "High", "resource-1 ほかN台"],
]
ADVISOR_REL_PROD = [
["推奨事項", "N", "High", "resource-1 ほかN台"],
]
ADVISOR_REL_EVAL = [
["推奨事項", "N", "High", "resource-1 ほかN台"],
]
# Portal links (with tenant ID)
PORTAL_LINKS = [
("本番系 Advisor", "https://portal.azure.com/#@{tenantId}/blade/..."),
("評価系 Advisor", "https://portal.azure.com/#@{tenantId}/blade/..."),
]
# ============================================================
# SLIDE GENERATION
# ============================================================
prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
# ----- S1: Title -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(sl, Inches(0), Inches(0), SLIDE_W, Inches(4), DARK_BLUE)
title_text = f"{CUSTOMER_NAME} {REPORT_TITLE}" if CUSTOMER_NAME else REPORT_TITLE
tb(sl, Inches(1), Inches(0.8), Inches(11), Inches(1.2), title_text, fs=34, b=True, c=WHITE)
tb(sl, Inches(1), Inches(2.0), Inches(11), Inches(0.6),
"Azure Advisor 推奨事項 / コスト推移分析", fs=16, c=RGBColor(0xA0, 0xC0, 0xE0))
tb(sl, Inches(1), Inches(4.5), Inches(5), Inches(0.4),
f"レポート作成日: {REPORT_DATE}", fs=14, c=DARK_GRAY)
tb(sl, Inches(1), Inches(5.0), Inches(6), Inches(0.4),
"データ取得: Azure Advisor / Cost Management API", fs=11, c=DARK_GRAY)
# Subscription table on title slide
sub_data = [["環境", "サブスクリプション名", "Subscription ID", "年間利用額"]]
for env, name, sid, annual in SUBSCRIPTIONS:
sub_data.append([env, name, sid, annual])
tbl(sl, Inches(6.5), Inches(4.5), Inches(6.3), Inches(1.5), sub_data,
col_widths=[1, 3, 3, 2])
add_notes(sl, f"表紙スライド。対象: {len(SUBSCRIPTIONS)} サブスクリプション。データ取得日: {REPORT_DATE}")
# ----- S2: Cost Trend -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "コスト推移・分析")
tb(sl, MARGIN, CONTENT_TOP, Inches(6), Inches(0.3),
"本番系 月別コスト推移", fs=13, b=True, c=DARK_BLUE)
trend_data = [[""] + COST_MONTHS, ["合計"] + COST_PROD, ["前月比"] + COST_PROD_MOM]
tbl(sl, MARGIN, CONTENT_TOP + Inches(0.4), FULL_W, Inches(0.8), trend_data)
if COST_SERVICES_PROD:
svc_top = CONTENT_TOP + Inches(1.4)
tb(sl, MARGIN, svc_top, Inches(12), Inches(0.3),
"本番系 サービス別月次推移", fs=12, b=True, c=DARK_BLUE)
svc_data = [["サービス"] + COST_MONTHS + ["変動"]] + COST_SERVICES_PROD
svc_h = min(Inches(0.35) * len(svc_data), Inches(2.0))
tbl(sl, MARGIN, svc_top + Inches(0.4), FULL_W, svc_h, svc_data)
# ----- S3: Cost Optimization -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "コスト最適化")
# Production cost table
tb(sl, MARGIN, CONTENT_TOP, Inches(6), Inches(0.3),
"本番系", fs=13, b=True, c=DARK_BLUE)
cost_prod_data = [["推奨事項", "件数", "年間削減", "対象リソース例"]] + ADVISOR_COST_PROD
cost_h = min(Inches(0.35) * len(cost_prod_data), Inches(2.0))
tbl(sl, MARGIN, CONTENT_TOP + Inches(0.4), FULL_W, cost_h, cost_prod_data,
col_widths=[5, 1, 2, 5])
# Evaluation cost table
eval_top = CONTENT_TOP + Inches(0.4) + cost_h + GAP
tb(sl, MARGIN, eval_top, Inches(6), Inches(0.3),
"評価系", fs=13, b=True, c=DARK_BLUE)
cost_eval_data = [["推奨事項", "件数", "年間削減", "対象リソース"]] + ADVISOR_COST_EVAL
eval_h = min(Inches(0.35) * len(cost_eval_data), Inches(1.5))
tbl(sl, MARGIN, eval_top + Inches(0.4), FULL_W, eval_h, cost_eval_data,
col_widths=[5, 1, 2, 5])
add_notes(sl, "コスト最適化: Advisor Cost カテゴリの推奨事項。年間削減額は Advisor 推定値。")
# ----- S4: Security (Production) -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "セキュリティ — 本番系")
sec_data = [["推奨事項", "件数", "影響度", "対象リソース(代表3件 + 残数)"]] + ADVISOR_SEC_PROD
sec_h = min(Inches(0.35) * len(sec_data), Inches(3.5))
tbl(sl, MARGIN, CONTENT_TOP, FULL_W, sec_h, sec_data, hc=RGBColor(0xC0, 0x39, 0x2B),
col_widths=[5, 1, 1, 6])
add_notes(sl, "セキュリティ(本番系): Advisor Security カテゴリ。High 影響度を優先対応。")
# ----- S5: Security (Evaluation) -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "セキュリティ — 評価系")
sec2_data = [["推奨事項", "件数", "影響度", "対象リソース(代表3件 + 残数)"]] + ADVISOR_SEC_EVAL
sec2_h = min(Inches(0.35) * len(sec2_data), Inches(3.0))
tbl(sl, MARGIN, CONTENT_TOP, FULL_W, sec2_h, sec2_data, hc=RGBColor(0xC0, 0x39, 0x2B),
col_widths=[5, 1, 1, 6])
add_notes(sl, "セキュリティ(評価系): 本番と同様に High 影響度を中心に確認。")
# ----- S6: Reliability (Production) -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "信頼性・可用性 — 本番系")
rel_data = [["推奨事項", "件数", "影響度", "対象リソース"]] + ADVISOR_REL_PROD
rel_h = min(Inches(0.35) * len(rel_data), Inches(3.5))
tbl(sl, MARGIN, CONTENT_TOP, LEFT_W, rel_h, rel_data, hc=RGBColor(0x29, 0x80, 0xB9),
col_widths=[5, 1, 1, 5])
ibox(sl, RIGHT_X, CONTENT_TOP, RIGHT_W, Inches(2.5),
"💡 改善ポイント",
["Zone 冗長化やバックアップ設定を", "優先的に確認してください"],
bg=LIGHT_GREEN, tc=RGBColor(0x1A, 0x5E, 0x3A))
add_notes(sl, "信頼性(本番系): Advisor HighAvailability カテゴリ。Zone 冗長・Backup を優先。")
# ----- S7: Reliability (Evaluation) -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "信頼性・可用性 — 評価系")
rel2_data = [["推奨事項", "件数", "影響度", "対象リソース"]] + ADVISOR_REL_EVAL
rel2_h = min(Inches(0.35) * len(rel2_data), Inches(3.5))
tbl(sl, MARGIN, CONTENT_TOP, LEFT_W, rel2_h, rel2_data, hc=RGBColor(0x29, 0x80, 0xB9),
col_widths=[5, 1, 1, 5])
add_notes(sl, "信頼性(評価系): 評価環境の冗長化は必要性を判断の上対応。")
# ----- S8: Appendix -----
sl = prs.slides.add_slide(prs.slide_layouts[6])
hdr(sl, "補足情報")
tf = tb(sl, MARGIN, CONTENT_TOP + Inches(0.2), Inches(6), Inches(4),
"Azure Portal リンク", fs=14, b=True, c=DARK_BLUE)
for label, url in PORTAL_LINKS:
ap(tf, "")
ap(tf, label, fs=12, b=True)
ap(tf, url, fs=9, c=MS_BLUE)
ibox(sl, Inches(7), CONTENT_TOP + Inches(0.2), Inches(5.8), Inches(4),
"❗ 免責事項",
["本レポートのコスト削減見込額は Azure Advisor の推定値に基づく概算値です。",
"実際の削減額は利用状況・価格改定・為替変動等により異なる場合があります。",
"",
"注意事項:",
"・推奨事項の件数・対象リソースは日々変動します",
"・最新の状況は Azure Portal からご確認ください",
"",
f"データ取得日: {REPORT_DATE}"],
bg=LIGHT_RED, tc=RED)
add_notes(sl, "補足情報: Portal 直リンクと免責事項。データ取得日を明示。")
# ============================================================
# SAVE
# ============================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate Azure Advisor monthly report PPTX")
parser.add_argument("--output", "-o", default="report.pptx", help="Output file path")
args = parser.parse_args()
out = args.output
tmp = out + '.tmp.pptx'
prs.save(tmp)
if os.path.exists(out):
os.remove(out)
os.rename(tmp, out)
print(f"Saved: {out}")
param(
[Parameter(Mandatory = $true)]
[string]$SubscriptionId,
[string]$OutputDir = "output",
[string]$ApiVersion = "2025-01-01"
)
$ErrorActionPreference = "Stop"
$categories = @(
@{ Name = "Cost"; File = "advisor-cost.json" },
@{ Name = "Security"; File = "advisor-security.json" },
@{ Name = "HighAvailability"; File = "advisor-reliability.json" },
@{ Name = "OperationalExcellence"; File = "advisor-opex.json" }
)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
$visible = az account list --all --query "[?id=='$SubscriptionId'].{name:name,id:id,tenantId:tenantId,state:state}" -o json | ConvertFrom-Json
if (-not $visible -or @($visible).Count -eq 0) {
throw "Subscription '$SubscriptionId' is not visible in the current Azure CLI login context."
}
az account set --subscription $SubscriptionId | Out-Null
$account = az account show --query "{name:name,id:id,tenantId:tenantId}" -o json | ConvertFrom-Json
$account | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $OutputDir "advisor-preflight.json") -Encoding UTF8
$token = az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv
$httpClient = [System.Net.Http.HttpClient]::new()
$httpClient.Timeout = [TimeSpan]::FromSeconds(60)
$httpClient.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new("Bearer", $token)
function Get-RecommendationsByCategory {
param(
[Parameter(Mandatory = $true)]
[string]$Category
)
$encodedFilter = [System.Uri]::EscapeDataString("Category eq '$Category'")
$nextUrl = "https://management.azure.com/subscriptions/$SubscriptionId/providers/Microsoft.Advisor/recommendations?api-version=$ApiVersion&`$filter=$encodedFilter"
$items = New-Object System.Collections.Generic.List[object]
while ($nextUrl) {
$json = $httpClient.GetStringAsync($nextUrl).GetAwaiter().GetResult()
$response = $json | ConvertFrom-Json
foreach ($item in @($response.value)) {
$items.Add($item)
}
$nextUrl = $response.nextLink
}
return $items
}
try {
foreach ($category in $categories) {
$items = Get-RecommendationsByCategory -Category $category.Name
$outputPath = Join-Path $OutputDir $category.File
if (@($items).Count -eq 0) {
Set-Content -LiteralPath $outputPath -Value '[]' -Encoding UTF8
}
else {
$items | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $outputPath -Encoding UTF8
}
Write-Output ("{0}={1}" -f $category.Name, @($items).Count)
}
}
finally {
$httpClient.Dispose()
}import http.client
import json
import os
import shutil
import subprocess
import sys
import urllib.parse
from collections import defaultdict
def get_access_token() -> str:
token_from_env = os.environ.get("AZURE_ACCESS_TOKEN")
if token_from_env:
return token_from_env.strip()
az_executable = shutil.which("az.cmd") or shutil.which("az.exe") or shutil.which("az")
if not az_executable:
raise FileNotFoundError("Azure CLI executable was not found in PATH.")
result = subprocess.run(
[
az_executable,
"account",
"get-access-token",
"--resource",
"https://management.azure.com/",
"--query",
"accessToken",
"-o",
"tsv",
],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
return result.stdout.strip()
def fetch_json(connection: http.client.HTTPSConnection, url: str, token: str) -> dict:
if url.startswith("/"):
request_target = url
else:
parsed_url = urllib.parse.urlsplit(url)
request_target = parsed_url.path
if parsed_url.query:
request_target += "?" + parsed_url.query
request_target = request_target.replace(" ", "%20")
connection.request("GET", request_target, headers={"Authorization": f"Bearer {token}"})
response = connection.getresponse()
payload = response.read()
if response.status >= 400:
raise RuntimeError(payload.decode("utf-8", errors="replace"))
return json.loads(payload.decode("utf-8"))
def build_initial_url(subscription_id: str, start_date: str, end_date: str, page_size: int) -> str:
filter_value = f"properties/usageStart ge '{start_date}' and properties/usageEnd le '{end_date}'"
query = urllib.parse.urlencode(
{
"metric": "ActualCost",
"$filter": filter_value,
"$top": str(page_size),
"api-version": "2019-10-01",
}
)
return (
f"https://management.azure.com/subscriptions/{subscription_id}"
f"/providers/Microsoft.Consumption/usageDetails?{query}"
)
def main() -> int:
if len(sys.argv) not in (5, 6):
print(
"Usage: py -3 scripts/get-legacy-usage-summary.py <subscription_id> <start_date> <end_date> <output_path> [page_size]",
file=sys.stderr,
)
return 2
subscription_id, start_date, end_date, output_path = sys.argv[1:5]
page_size = int(sys.argv[5]) if len(sys.argv) == 6 else 100
token = get_access_token()
next_url = build_initial_url(subscription_id, start_date, end_date, page_size)
connection = http.client.HTTPSConnection("management.azure.com", timeout=60)
total_cost = 0.0
currency = None
item_count = 0
page_count = 0
service_totals = defaultdict(float)
try:
while next_url:
print(f"FETCH_PAGE={page_count + 1}", flush=True)
payload = fetch_json(connection, next_url, token)
page_count += 1
print(f"PAGE_ITEMS={len(payload.get('value', []))}", flush=True)
for item in payload.get("value", []):
properties = item.get("properties", {})
cost = float(properties.get("cost") or 0.0)
service_name = (
properties.get("consumedService")
or properties.get("product")
or properties.get("resourceName")
or "Unknown"
)
billing_currency = properties.get("billingCurrency")
total_cost += cost
service_totals[service_name] += cost
item_count += 1
if not currency and billing_currency:
currency = billing_currency
next_url = payload.get("nextLink")
finally:
connection.close()
top_services = [
{
"serviceName": service_name,
"cost": round(cost, 4),
}
for service_name, cost in sorted(service_totals.items(), key=lambda pair: pair[1], reverse=True)[:10]
]
summary = {
"subscriptionId": subscription_id,
"startDate": start_date,
"endDate": end_date,
"pageSize": page_size,
"currency": currency,
"totalCost": round(total_cost, 4),
"itemCount": item_count,
"pageCount": page_count,
"topServices": top_services,
}
with open(output_path, "w", encoding="utf-8") as file_handle:
json.dump(summary, file_handle, ensure_ascii=False, indent=2)
print(f"TOTAL={summary['totalCost']}")
print(f"CURRENCY={summary['currency']}")
print(f"ITEMS={summary['itemCount']}")
print(f"PAGES={summary['pageCount']}")
for index, item in enumerate(top_services, start=1):
print(f"TOP{index}={item['serviceName']}|{item['cost']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Related skills
FAQ
Does this skill change my Azure environment?
No. It is read-only and only generates a report; it does not build or modify resources.
Which Advisor categories are covered?
All four: Cost, Security, HighAvailability/Reliability and Operational Excellence, with 0 items recorded as no recommendations rather than skipped.