みち草

Azure中心にまとめる技術情報ブログ

Azure Monitor のブックでリソース グループ毎に先月と今月の料金差額を可視化する

はじめに

今回はコスト可視化の試みとして、リソース グループ毎に先月との差額はどれくらいか?を表示できるようにしてみました。

Cost Management でも予測やグルーピングなどそれなりに充実しているんですが、 "先月"、"過去〇日" という選択しかできず、 先月と比較する、というような表示の仕方ができなかったので、それっぽいものを Azure Monitor のブックで作ってみました。

細かい見栄えとかまで考えると完璧ではないのですが、目的は果たせていると思います。

作るのめんどい場合は以下の GitHub から Deploy to Azure をポチーでどうぞ。

github.com

目次

作りたいもの

作りたいのは前述のとおり、リソース グループ毎に、先月と今月の利用料金を求め、差額を算出することです。

最終的には 1 レコードを "リソース グループ名、RG へのリンク、先月の料金、今月の料金、差額" としてテーブルにしたいと思います。

見た目としてはこんな感じです。

最終結果テーブルの見栄えについて補足

  • リソース グループ名が空欄の箇所がある?

料金を取得したときに、リソース グループに属さない判定となっている料金や、リソース グループは存在するけど料金が 0 の場合にそうなるようです。

それはそれで情報となるので、これ以上時間はかけずこれで OK としました。

  • リソース グループへのリンクが空欄の箇所がある?

Resource Graph で動的に、ブックを見たその時存在しているリソース グループの情報を取得しているので、既に削除されているリソース グループはリンクがなしになります。

  • 先月または今月の利用料金が空欄の箇所がある?

先月は料金が発生したが先月の内に削除された、今月新しく作られ料金が発生した、などはそのようになります。

  • 差額がマイナスだったら 0 にできない?

したいと思ったけど列の設定に該当しそうなものを見つけられなかったので諦めました。

ブック

Azure Monitor の機能の 1 であり、メトリックやログなどいろいろなデータ ソースとパーツを使って、対話型の分析・可視化を行うことができる機能です。

learn.microsoft.com

ビューを提供する、という点では Azure ダッシュボードも存在していますが、ブックは対話型である点が大きく違います。

例えばサブスクリプションを選ぶパラメーターを作り、それを変更することで表示結果を変える、ということなどができます。

今回はこれを使います。

ブックの作成

全体像

初めに、完成系を作るために必要なものをまとめます。

API もしくは Resource Graph から作ったテーブルが計 3 つ、それをベースにマージして中間テーブル (3-1 から 3-3) を作り、再度マージして目的のテーブルを作成しました。

最適化できる余地があるのかもしれませんが、思いつかなかったのでこの方法で記載します。

(1) から順に作り方をまとめます。

パラメーター作成

(1) から順番にとは言ったのですが細かいところで、パラメーターを先に作成します。

可視化対象のサブスクリプションをドロップダウン リストから選択できるようにしています。

作り方

空のブックを作成し、"パラメーター" を追加します。

"パラメーターの追加" から以下のように Subscription 選択用のパラメーターを追加します

これでドロップダウン リストが完成です。

(1) 今月のリソース グループ毎の利用料金

利用料金のテーブルは、Azure Resource Manager の API を叩いて取得します。

ここにやりたいことは大体書いてあるので、こちらも読んでみてください。

techcommunity.microsoft.com

以下の Usage API があるので、これをブックから実行し、データを取得します。

learn.microsoft.com

作り方

"クエリ" を追加します。

Usage の API に従い、以下のように設定します。

  • データ ソース : Azure Resource Manager
  • HTTP メソッド : POST
  • パス : /subscriptions/{Subscription:id}/providers/Microsoft.CostManagement/query

パスの {Subscription:id} は、1 つ前の手順で作成した Subscription パラメーターを指す記法です。 パラメータの選択にあわせてデータの取得先が変更されるように、このとおり記載します。

URL パラメーターとしては、api-version を 2019-11-01 に指定します。

"結果の設定" では、JSON パス テーブルを $.properties に指定します。

ボディには以下の JSON を指定します。

{
    "type": "Usage",
    "timeframe": "MonthToDate",
    "dataset": {
      "granularity": "None",
      "aggregation": {
       "totalCost": {
          "name": "PreTaxCost",
          "function": "Sum"
        }
      },
     "grouping": [
        {
         "type": "Dimension",
          "name": "resourceGroup"
        }
      ]
    }
  }

timeframe にて、データの取得範囲を指定します。
今月のデータを取りたいので、MonthToDate を指定します。

granularity を None にすると、ひと月分がまとめて集計されます。
1 日毎のデータが欲しい場合は、Daily を指定します。
1 日毎に取ろうかと思ったのですが、整理が難しかったのでやめました。

aggregation が集計部分で、PreTaxCost の合計を算出しています。

今回はリソース グループ毎に集計したいので、grouping 部分の記述が必要です。
これを入れない場合、全リソース グループ合算した結果が返されます。

ここまで指定したら 1 回クエリを実行してみて、リソース グループ毎に結果が得られれば OK です。

通過の単位は環境により異なります。私の環境だと USD になってしまいますが、変化量の大小はわかるのでよしとします。

以降も USD の想定で設定しているところはありますが、環境に応じて ¥ (JPY) などに変更してください。

また、リソース グループ名が空欄の課金情報があるようでそれも出力されてしまいますが、課金情報には変わりないのでよしとしました。

続いて、後のために見栄えを整えておきます。

"詳細設定" タブの "ステップ名" に、後で参照するとき用に名前を付けておきます。
今月分の集計なので This Month にしました。

また、最終的にはこのテーブルは参照しないので、"この項目を条件付きで表示する" 設定を使用して非表示にします。
パラメーターにあわせて表示非表示を切り替えることもできますが、ずっと非表示にしたいので適当に指定して大丈夫です。

最後に、非表示にはしますが後でわかるよう、グラフのタイトルを付けました。

透かしのような斜線が入れば非表示設定が有効です。

これで (1) は完成です。

ここでは列名をそのままにしましたが、今月の料金だとわかる列名にした方が最後のマージの際にわかりやすいかも。

(2) 先月のリソース グループ毎の利用料金

続いて先月の利用料金を取得します。

作り方

これは (1) のテーブルと同じ方法で取得しますが、1 点だけ、ボディの timeframe を "TheLastMonth" に変更する必要があります。 他の設定は先ほどと同様です。

Timeframe の TheLastMonth は非サポートになってしまったようで、エラーが表示されるようになりました。

現在同様のことを行うには TheLastBillingMonth を指定してください。EA 契約などであれば、TheLastMonth と同様の値を取得することができます。

一方で、従量課金や Visual Studio サブスクリプションなどの場合は一致しない場合があるため注意してください。

入力したら実行し、先ほどと異なる結果であることを確認しましょう。

(1) と同様に、ステップ名、非表示設定、タイトルをつけて完成です。

ここでは列名をそのままにしましたが、先月の料金だとわかる列名にした方が最後のマージの際にわかりやすいかも。

(3-1) 今月のみ、または先月と今月の両方に存在したリソース グループ

ここからは少し変わります。

最終的には先月と今月の月額料金を比較したいため、"先月または今月に存在したリソースグループ" の一覧 (列) が必要になります。

ブックでは取得したテーブルをマージすることもできるのですが、(1) と (2) のテーブルを直接マージするのみでは、 "先月または今月に存在したリソース グループ名" をすべて重複せずに保持したテーブル (3-3) を生成する方法がないように思われました。

そのため、一旦中間テーブルを作り (3-1, 3-2)、それを組み合わせて望む結果 (3-3) を作りました。

順に解説します。

作り方

まずは、今月のみ、または先月と今月の両方に存在したリソース グループのテーブルを作成します。

"クエリ" を追加した後にデータ ソースとして "マージ" を選択、"マージを追加します" からマージを行うテーブルとその方法、およびマージするキー列を指定します。

左右のどちらにテーブルを置くかが重要なため注意してください。
左が今月のデータ (前の手順でつけたステップ名 : This Month) です。

マージにはいろいろ種類があります。

learn.microsoft.com

全ての説明は省きますが、ここでは "Left Outer" を使用します。

Left Outer は指定した列を基に、以下の条件でマージされます。

  • 指定した列の値が、左右どちらのテーブルにも存在する場合は出力される
  • 指定した列の値が、左のテーブルにのみ存在する場合は出力される
  • 指定した列の値が、右のテーブルにのみ存在する場合は出力されない

ここでいう左は今月のデータ、右は先月のデータ、列はリソース グループ名 (resourceGroup) ですので、結果 "今月のみ、または先月と今月の両方に存在したリソース グループ" が取得できます。

保存してマージを実行すると得られるテーブルにて、resourceGroup 列がそのようになっているはずです。

このままでもいいのですが、ここでほしいのは resourceGroup 列だけなので、他は消してしまいます。 (消さなくてもできるけど列が増えて見づらいので個人的にはいらない)

列の名前にチェックをつけて削除ボタンを押すと消せます。ステップ名を付けておくと、こういう時に判別しやすいです。[This Month].resourceGroup だけ残します。

列の名前は後のマージに影響がでるため、ここでは変えません。

削除だけでは表が更新されないので、マージの実行をしておきます。

後述の理由によりこのテーブルを非表示にできないので、ステップ名とタイトルだけ、こんな感じで付けておきます。

これで 3-1 は完成です。

中間テーブルの場合の注意点

ここで、今回の 3-1 のようにマージにより生成されたテーブルにおける注意点を紹介します。

それは、マージにより生成されたテーブルを非表示にすると自動的に更新されず、結果が得られない (テーブルが生成されない) 状態になる、ということです。
中間テーブルが生成されない結果、当然ながら最終結果も生成されなくなってしまいます。

これはどうも現状のブックの仕様らしく、今のところは "非表示にしない" くらいしか回避方法が見つかっていません。

そのため見栄えが悪くなってしまうのですが、マージにより生成する中間テーブル (3-1 から 3-3) は非表示にしません、できません。

ちなみに、最終結果を一番上に持ってくれば (中間テーブルを下に置けば) いいのでは?という考えもありましたが、実はブック内ではパーツの順番に意味があり、参照されるよりも先にテーブルが生成されている必要があります。

そのため、一番上で (5) を生成し、(5) より後ろにおいた (3-1 から 3-3) を参照、ということはできませんでした。

こんなエラーになります。

何かいい方法があればアップデートしますが、現状は難しそうです。 (クエリ内で join で頑張るくらい?)

苦肉の策を最後に載せています。

少しわかりづらいところがありそうだったので、補足を追記します。ここで "非表示にできない" と表現しているのは、マージ元のテーブルすべてが非表示にできないわけではなく、今回のように、クエリで取得したテーブルをマージし中間テーブルを生成、中間テーブルどうしをさらにマージする、という際に、中間テーブルが非表示にできないということです。

例として、今回実施しているような以下の場合を考えます。

  • クエリで取得したテーブル A と B をマージ ⇒ テーブル C
  • クエリで取得したテーブル D と E をマージ ⇒ テーブル F
  • マージで取得したテーブル C と F をマージ ⇒ テーブル G

この場合、テーブル A, B, D, E を非表示にしても、テーブル C, F, G は正しく表示されます。 しかし、テーブル C, F を非表示にしてしまうと、テーブル G は生成されなくなってしまいます。

マージにより生成されたテーブルを非表示にすると再利用できない、という仕様のようです。

クエリでのデータ取得は非表示でも関係なく実行するが、マージは非表示だと実行されない、ということなのかと勝手に想像しています。

(3-2) 先月にのみ存在したリソース グループ

作り方

中間テーブルその 2 を作りますが、先ほどと同様にマージを、オプションを変えて行います。

今度は Right Anti を使います。

Right Anti は指定した列を基に、以下の条件でマージされます。

  • 指定した列の値が、左右どちらのテーブルにも存在する場合は出力されない
  • 指定した列の値が、左のテーブルにのみ存在する場合は出力されない
  • 指定した列の値が、右のテーブルにのみ存在する場合は出力される

端的に言えば右のテーブルにあるもののみ取得、です。

これで "先月にのみ存在したリソース グループ" が取得できます。
※ここでも左右の指定が重要なため注意

3-1 と同様に、必要な列だけ残します。列名はここでは変えません。

3-1 と同様に非表示にできませんので、ステップ名とタイトル名だけ付けておきます。

これで 3-2 は完成です。

(3-3) 先月または今月に存在したリソース グループ

中間テーブルの最後として、先月または今月の両方に存在したリソース グループのリストを生成します。

作り方

再度マージを使用します。

ここでは "共有体" (Union) を選択します。

これは和集合であるため、左右テーブルにある値が合算されたテーブルが生成されます。

対象として "(3-1) 今月のみ、または先月と今月の両方に存在したリソース グループ" と "(3-2) 先月にのみ存在したリソース グループ" を選択することで、 "先月または今月に存在したリソース グループ" の列が得られます。

3-1 、3-2 で列名を変えてしまうと別の列として扱われるのか、ここでうまく 1 つの列にマージされませんでしたので注意です。

不要な列を消します。rg_LeftOuter が前のテーブルなのでそちらを消します。

中間テーブルのため例によって非表示は不可です。

これで、ほしいテーブルができました。

(4) 現在のリソース グループ名とリンク

ここまでのテーブルがあれば目的は達成できるのですが、単にテキストが並んだだけの味気ない表になってしまいますし、表中から差額の大きいリソース グループを見つけた後、概要画面に簡単に遷移できたら便利だと思うので、リソース グループへのリンクをつけます。

そのためのテーブルが (4) です。

作り方

リソース グループへのリンク付きのテーブルは、Resource Graph で取得します。

learn.microsoft.com

"クエリ" を追加し、データ ソースで "Azure Resource Graph" を選択します。

"サブスクリプション" の項目にて、"リソース パラメーター" の "Subscription" を選択します。

これで、Resource Graph によるクエリの対象が、パラメーターの選択と連動します。

クエリとしては以下を指定します。

resourcecontainers
| where type =~ "Microsoft.resources/subscriptions/resourcegroups"
| project id, name

Resource Graph でサブスクリプション内のリソース グループの一覧を取得し、リソース グループへのリンクになっている列と、この後のマージ用にテキストのリソース グループ名を取得しています。

リンクになっている id 列は、実態は "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}" という文字列になっています。

これだけだとマージの際に純粋なテキストのリソース グループ名と比較ができないので、リソース グループ名のみの列も必要、という理由です。

いつものように、ステップ名とタイトルを設定します。

(4) はマージで作られたテーブルではないので、非表示設定も可能です。

(4) はこれで完成です。

(5) リソース グループ毎の前月との差額

長かった作成もあと少し、最終結果のテーブルを生成し、見栄えを整えます。

作り方
テーブルの生成

最後はこれまでに生成した (1), (2), (3-3), (4) を 3 パターン マージして作ります。

1 つ目は、(3-3) と (1) の Left Outer です。

これで、先月または今月に存在したリソース グループリストの内、今月のリソース グループに利用料金の列がマージできます。

2 つ目は、(3-3) と (2) の Left Outer です。

これで、先月または今月に存在したリソース グループリストの内、先月のリソース グループに利用料金の列がマージできます。

3 つ目は、(3-3) と (4) の Full Outer です。

Full Outer は指定した列を基に、以下の条件でマージされます。

  • 指定した列の値が、左右どちらのテーブルにも存在する場合は出力される
  • 指定した列の値が、左のテーブルにのみ存在する場合は出力される
  • 指定した列の値が、右のテーブルにのみ存在する場合は出力される

これで、resourceGroup 列に先月と今月のリソース グループ名、PreTaxCost と PreTaxCost1 に先月と今月の料金 (事前に列名変えておけばよかった…)、id に該当リソース グループへのリンクを持つテーブルが生成できました。

上記の 4 列以外は不要なため削除し、順番を並び替えます。

また、必要に応じて列名も任意のものに変更します。

列の設定

このままでも料金の違いは判断できますが、もっとわかりやすくするため、列に設定を加えていきます。

まずは "新しい項目の追加" から新しい列を追加し、先月と今月の料金の差額を表示するようにします。

編集から、"式" を選択し、以下のように列名を使って計算式を記載します。

["今月の利用料金"] - ["先月の利用料金"]

これで再びマージすると、差額を計算した列が追加されます。名前は任意につけておきます。

続いて、料金の桁数が多すぎて見づらいので、設定していきます。"列の設定" を選択します。

"先月の利用料金" を選択し、カスタムの書式設定で以下のように設定するのがいいかと思います。この辺は見やすさなど好みです。

※通貨の単位は環境により異なります。利用している環境に応じて $ (USD) や ¥ (JPY) などに変更してください。

同様の設定を "今月の利用料金" と "差額 (今月 - 先月)" にも設定します。

この設定だとこんな感じになり、だいぶ見やすくなります。

"差額" の列には、追加でもう少し設定を加えます。

"列レンダラー" をヒートマップにして、"カラー パレット" を赤 (明るい)、"最小値" を 0 、"最大値" を任意 (ここでは 100) にします。

こうすることで差額の列が 0 より大きい (先月より利用料金が増えている) 場合、赤で色付けがされ、指定した値にどれくらい近いかで濃淡が変わります。

最大値を指定しなくともいいのですが、その場合は増加量が $5 であろうと $100 であろうと、列中のすべての値と比較して遠ければ薄く近ければ濃く色がついてしまい、どれくらいの重要度なのかわかりづらいように思います。

なので、五千円前後の増額を気にしたいから $40 、一万円前後の増額を気にしたいから $80、という具合に最大値を決めるといいと思います。注意点は USD なことです。

色がつくとこんな感じです。 (最大値 100 の場合)

これで、リソース グループ毎に月額料金、前月との差額を表示してくれる目的のブックは完成です!

上部の保存を忘れないようにしましょう。

ここまで読んでくれた方、試してくれた方はお疲れ様でした!

中間テーブルの非表示問題に対する苦肉の策

マージしたテーブルは非表示にできない、という話を書きましたが、見づらいので何かないか、というところで苦肉の策ですがこんな方法がありました。

(3-1) から (3-3) のテーブルに対して、サイズを "最小" とし、スタイルを以下のようにします。

何が起こるかというと、消せはしないんですが、めっちゃ小さくなります。

格好良くはないのですが、実用には足るんじゃないかと…

現状はこれくらいしかできることがなさそう…

終わりに

ということで、とてもとても長くなってしまったんですが、ブックでの料金可視化をしてみました。

ブックはテキストやメトリック、Log Analytics、Resource Graph、JSON、ARM API と実はいろいろなものが扱え、 それで得たテーブルを今回のようにマージして加工することができ、ヒートマップのような列レンダラーの種類もたくさんあってかなり多くのことが行えます。

一方で、最終的に可視したいものに対して、どこから何を取ってきて、どう組み合わせたらいいか、作り方を考えるのが少し難しい、という点はあると思います。

今回は、どうやってマージすればリソース グループの列を取得できるか?というところで苦労しました。
最初は Union でマージしてたけど、これじゃ同じ名前が 2 レコードあるじゃん!とかとか…

簡単にペタペタ貼って使えるダッシュボードに対して、簡単なものだけでなくやり込めばより複雑なものも作れるのがブック、という位置づけだと思いますが、 簡単なところからでもなかなか面白いので、こんなこともできるならやってみよーと感じてもらえたら嬉しいです。

Azure Monitor Agent + Log Analytics でプロセス毎の CPU 使用率を可視化する

はじめに

Azure Monitor Agent で Windows VM のプロセス毎の CPU 使用率を示すパフォーマンス カウンターを Log Analytics に収集して、クエリで可視化してみました。

目次

Azure Monitor エージェント構成

事前に Windows VM 1 台と Log Analytics ワークスペースをデプロイしている状態です。

データ コレクション ルールの作成

Azure Monitor エージェントで Log Analytics にデータを収集する際は、データ コレクション ルールを作成します。

どのイベント ログからどんなイベントを収集する、どのパフォーマンス カウンターを収集する、そしてそれをどこのワークスペースに送信する、というルールを定義し、それを VM に関連付けます。

これにより、以前の Azure Monitoring Agent ではできなかった、VM 毎に収集するデータを変えつつ、1 つの Log Analytics ワークスペースにデータを収集する、ということができるようになっています。

Security Center Standard が必要だったセキュリティ イベントも、特に制限なく収集できるようになったのもポイントです。

最初ちょっとわかりづらいのは、Log Analytics ワークスペースではなく、Azure Monitor から作成する、ということでしょうか。

前置きはこのくらいにして、ルールを作成します。

  • プラットフォームの種類 : ルールの対象とする OS の種類を指定できます。Custom の場合は Windows と Linux の両方を対象とします。
  • エンドポイント ID : Log Analytics ワークスペースに接続するためのエンドポイントを指定できます。カスタム ログやアクセス制限など、一部のシナリオで必要ですが、今回は不要なので で OK です。

続いては、ルールの対象とする VM の選択です。

事前にデプロイしておいた VM を指定します。

Azure Monitor Agent でのデータ収集を行う場合、対象 VM にはマネージド ID の有効化とエージェントのインストールが必要ですが、ここで選択したマシンは自動でシステム割り当てマネージド ID の有効化、エージェント インストールが行われます。

続いて、収集するデータを指定します。

今回は、パフォーマンス カウンターを指定します。

"基本" にすればよくあるパフォーマンス カウンターは収集してくれるのですが、今回の目的であるプロセス毎の CPU は収集してくれないので、 "カスタム" にします

任意のパフォーマンス カウンターを指定できるため、以下の値を追加します。

\Process(*)\% Processor Time

必要なカウンターは、OS の Performance Monitor から探しましょう。今回はこれ。

全てのプロセスについて取得したいので、インスタンス名を表すカッコ内を、* にします。

ここでもう 1 つ、以下のカウンターを追加します。

\Process(*)\ID Process

これは、インスタンス毎のプロセス ID を表すパフォーマンス カウンターです。

\Process(*)\% Processor Time はインスタンス毎の CPU 使用率を表しますが、インスタンス名は再利用されます。

そのため、インスタンス名は同じだけどプロセス ID が変わっており、実際は別プロセスになっていることがあります。

プロセス ID の変化を確認できるようにするため、こちらも取得しておきます。

こんな感じで一番最後に追加されていれば OKです。

最後に、収集したデータを送信するターゲットを指定します。

作成しておいた Log Analytics ワークスペースを選択します。

Azure Monitor メトリックにも送ることができるようですが、現在はプレビューです。

データソースを追加して、データ コレクション ルールを作成します。

作成が完了したら、データが収集されるまで、数分待ちます。

クエリでの CPU 使用率の可視化

データが収集できたら可視化しましょう。

Log Analytics ワークスペースの "ログ" から KQL を実行します。

まずはとりあえず、取得したカウンターを使って、直近 1 時間以内のプロセス毎の CPU 使用率を可視化してみます。クエリはこちら。

Perf
| where TimeGenerated > ago(1h)
| where CounterName == "% Processor Time"
| summarize avg(CounterValue) by InstanceName, bin(TimeGenerated, 1min)
| render timechart 

可視化できましたが、なんだか飛びぬけたのが 2 つありました。

Idle と _Total 、未使用値と合計値っぽく、プロセス毎の CPU を見る上では不要そうなので除外するよう、以下のクエリに変更。

Perf
| where TimeGenerated > ago(1h)
| where CounterName == "% Processor Time"
| where InstanceName != "Idle"
| where InstanceName != "_Total"
| summarize avg(CounterValue) by InstanceName, bin(TimeGenerated, 1min)
| render timechart 

これでプロセス毎の CPU 使用率を可視化できました。

クエリでのプロセス ID の可視化

同様に、プロセス ID のカウンターも可視化してみます。

さっき除外した 2 つを同様に除くと、こんな感じのクエリに。

Perf
| where TimeGenerated > ago(1h)
| where CounterName == "ID Process"
| where InstanceName != "Idle"
| where InstanceName != "_Total"
| summarize max(CounterValue) by InstanceName, bin(TimeGenerated, 1min)
| render timechart  

とても見づらいですが CPU 使用率のグラフがメインで、そこで特定プロセスの使用率の変化を確認した際にこっちの変化も見る、という感じになると思うので、 プロセス ID が変わっているかいないかがわかれば十分かと思います。

どちらのグラフも、特定のプロセスだけ見たくなったら、where 文を追加すればいいですし。

ということで、プロセス ID 変化も可視化できました。

これらのクエリは保存しておくことができるので保存していつでも呼び出せるようにするか、可視化したグラフ自体をダッシュボードや Azure ブックにピン留めして使うのがいいと思います。

終わりに

プロセス毎の CPU 使用率を可視化したい、という都合があり試したことをまとめました。

対象のカウンターさえわかれば、Azure Monitor エージェントの設定自体は非常に簡単です。

カスタム ログにも対応したので、別途そちらもまとめたいと思います。

<テスト編> Azure VM の Scheduled Event をポーリングする

はじめに

以前、Python スクリプトを daemon 化して、Azure VM の Schedule Event をポーリングしました。

今回は、そのスクリプトを実装した後のテストをしてみる、そして Scheduled Event で検知される値がどのように変化するのか、という内容です。

実装編の投稿はこちら

www.michikusayan.com

目次

Scheduled Event のテスト方法

そもそもの Scheduled Event については、実装編で記載したため省略します。そちらを参照ください。

まずは Scheduled Event で検知される以下のイベントをどうテストするか、というところです。

  • freeze: VM の一時停止
  • reboot: VM の再起動
  • redeploy: ホストの移動
  • preempt: スポット VM の削除
  • terminate: VM の削除

と言っても非常に簡単で、Azure ポータルから VM の停止、再起動を実行すれば、それが Scheduled Event として検知されます。

現状メンテナンスを疑似的に起こすことはできませんし、削除だと繰り返しテストするのが面倒なので、これが一番簡単です。

Azure ポータル以外にも、API やコマンドでの実施も OK です。これについては、Docs 上にも記載があります。

learn.microsoft.com

ということで、Azure ポータルから VM を再起動し、挙動と出力されたログの内容を確認します。

テスト 1 : 予定時刻まで放置してみる

最初は、素直にポータルから再起動をして、待機時間いっぱいまで放置してみました。

先に全体の流れをまとめると、こんな感じでした。

同じ矢印の時間帯は、Scheduled Event から同じ結果が返っている、と解釈してください。

つまりは、以下の 4 種類のレスポンスがありました。

  • ログ 1 : 再起動実行以前からイベント検知開始まで
  • ログ 2 : イベント検知開始から、イベント開始まで
  • ログ 3 : イベント開始から、ログ出力停止 (再起動) まで
  • ログ 4 : ログ出力再開以降

これらを順に以下に紹介します。

ログ 1 : 再起動実行以前からイベント検知開始まで

ポータルで再起動を教えてから、イベントとして検知されるまで、つまりは何もイベントがない平常時のレスポンスです。

この時は以下のような結果が返ります。

当然、イベントは空っぽです。

{
    "DocumentIncarnation": 4,
    "Events": []
}

ログ 2 : イベント検知開始から、イベント開始まで

イベントが検知されはじめ、実行時間まで待機している状態 (EventStatus が Scheduled 状態) のレスポンスです。

{
    "DocumentIncarnation": 5,
    "Events": [
        {
            "EventId": "6B951C83-99B4-4551-8A5E-CB0BA9A6F82B",
            "EventStatus": "Scheduled",
            "EventType": "Reboot",
            "ResourceType": "VirtualMachine",
            "Resources": [
                "IMDS-East1"
            ],
            "NotBefore": "Wed, 07 Sep 2022 05:51:15 GMT",
            "Description": "Virtual machine is going to be restarted as requested by authorized user.",
            "EventSource": "User",
            "DurationInSeconds": -1
        }
    ]
}

イベントが 1 件検知されており、イベントの実行者がユーザーであることや、Reboot であること、対象がどのマシンかなどがわかります。 (実際のメンテナンスだった場合には、EventSource が Platform になります)

NotBefore がイベント実行を待機できる最大の時間です。これよりも長く待機することはできず、この時間になるとイベントが実行されます。

DocumentIncarnation はレスポンスに変化があるたびに値が +1 されるため、先ほどと異なる値になっています。

Azure ポータルで実施した再起動は、NotBefore の時間まで保留になります。

ちなみに、このときポータルでは再起動が終わるまでずっとぐるぐるしてます。

ログ 3 : イベント開始から、ログ出力停止 (再起動) まで

NotBefore の時間まで待機した結果のイベント開始から、再起動の実行によりログの出力が停止される直前までのレスポンスです。

{
    "DocumentIncarnation": 6,
    "Events": [
        {
            "EventId": "6B951C83-99B4-4551-8A5E-CB0BA9A6F82B",
            "EventStatus": "Started",
            "EventType": "Reboot",
            "ResourceType": "VirtualMachine",
            "Resources": [
                "IMDS-East1"
            ],
            "NotBefore": "",
            "Description": "Virtual machine is going to be restarted as requested by authorized user.",
            "EventSource": "User",
            "DurationInSeconds": -1
        }
    ]
}

DocumentIncarnation 以外で先ほどと異なるのは、EventStatus が Started になっていること、NotBefore が空欄になっていることです。

イベントの開始時刻になると即実行ではなく、一旦上記のようなイベント実行状態になったあと、開始されるようです。

この後は再起動が行われるため、一時的にログの出力が停止します。

ログ 4 : ログ出力再開以降

再起動から復帰し、ログ出力が再開された以降のレスポンスです。

{
    "DocumentIncarnation": 7,
    "Events": []
}

再起動により検知していたイベントは完了したため、再びイベントなしに戻っています。

例によってイベントの変化の度に、DocumentIncarnation は変化しています。

Scheduled Event で検知しても放置しておくと、イベントなし ⇒ イベント検知 ⇒ イベント開始 ⇒ イベントなし、となることがわかりました。

テスト 2 : 待機中に自分でイベント開始を要求してみる

Scheduled Event が有効な場合、所定の時間までイベントの実行が保留になりますが必ずその時間まで待つ必要はなく、自分で開始を要求することができます。

待機中に準備が整ったので早く済ませたいから開始を要求、というようなイメージでしょうか。

ということで 2 つ目は、再起動の待機中に自分で開始を要求してみたら何か変わるのか?を確認してみます。

こちらも先に全体の流れをまとめると、こんな感じでした。

今回は以下の 3 種類のレスポンスがありました。

  • ログ 1 : 再起動実行以前からイベント検知開始まで
  • ログ 2 : イベント検知開始から、イベントの開始要求、ログ出力停止 (再起動) まで
  • ログ 3 : ログ出力再開以降

これらを順に以下に紹介します。

ログ 1 : 再起動実行以前からイベント検知開始まで

これについては、1 つ目のテストと変わりはありません。

イベントを検知する前なので、空っぽの状態です。

{
    "DocumentIncarnation": 7,
    "Events": []
}

ログ 2 : イベント検知開始から、イベントの開始要求、ログ出力停止 (再起動) まで

イベントの検知も、1 つ目と同様です。

ただ、何で DocumentIncarnation が +2 されてるのかよくわからない…

{
    "DocumentIncarnation": 9,
    "Events": [
        {
            "EventId": "B7D02EBD-FB9F-430B-B3B6-1845EF2D5328",
            "EventStatus": "Scheduled",
            "EventType": "Reboot",
            "ResourceType": "VirtualMachine",
            "Resources": [
                "IMDS-East1"
            ],
            "NotBefore": "Wed, 07 Sep 2022 06:32:35 GMT",
            "Description": "Virtual machine is going to be restarted as requested by authorized user.",
            "EventSource": "User",
            "DurationInSeconds": -1
        }
    ]
}

ここで、今回は NotBefore まで待たずに、自分でイベントの開始を要求します。

方法は、メタデータ サービスに対して、イベント ID を添えて POST します。

このテストでは、以下のコマンドを実行しました。

curl -H Metadata:true -X POST -d '{"StartRequests": [{"EventId": "B7D02EBD-FB9F-430B-B3B6-1845EF2D5328"}]}' http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01

ここで、1 つ目と同じように EventStatus が Started になってから再起動かなーと思っていたのですが、違いました。

先ほどのレスポンスに特に変化がないまま、再起動に入りログの出力が停止しました。

そういうもの…?

ログ 3 : ログ出力再開以降

再起動から復帰し、ログ出力が再開された以降のレスポンスです。

これも 1 つ目のときと変わりません。

ただ、ここでも DocumentIncarnation が +2 されています。

変化の度に +1 ずつされていくはずなのでもしかしたら、ログ出力にできなかったけど一瞬だけ Status が Started のレスポンス (DocumentIncarnation: 10) があったのかもしれません。

{
    "DocumentIncarnation": 11,
    "Events": []
}

確認できたログからは イベントなし ⇒ イベント検知 (イベント開始要求) ⇒ イベントなし という変化をしましたが、実際は放置したときと同じように Started のタイミングがあるけれど、毎秒のポーリングでは検知できなかった、という可能性も考えられます。

ということで、若干の謎が残るところではありますが、今回のやり方でテストしたところ、このような結果になりました。

終わりに

今回は Scheduled Event の検知をテストしてみました。

ポーリングを実装する場合には、検知した (Status: Scheduled の) 段階でイベント ログに残すとか、備えるための処理をしてからイベントの開始を要求、という感じになるのかなと思います。

そのため開始した後の Started 状態はあまり気にすることはないように思いますが、こんな動作なんだなーと把握する際の参考になればいいかなと!

Azure File Sync を構築する

はじめに

今までほとんど触ってなかった Azure File Sync を構築してみたので、備忘としてのまとめです。

目次

Azure File Sync

ザックリ言えば、Windows Server に Azure Files Storage をマウントして、ファイルサーバー的に使えます。

アクセス頻度の高いファイルはローカルに保存し、アクセス頻度の低いファイルはデータを Azure Files に保存しつつ サーバー側に Files 内のデータを指すポインターだけ残すことで、ローカルの容量を節約しつつ利用できます。

learn.microsoft.com

File Sync 構築

File Storage 準備

Azure File Storage でファイル共有を作成する

同期用にフォルダを作成

OS 準備

IE セキュリティ強化の構成を無効化する(エージェントをサーバーから直接ダウンロードする場合)

作成したファイル共有をマウントする

接続コマンドはポータルから作れる

ストレージ同期サービスの構成

ポータルで "ストレージ同期サービス" を検索して作成する

パブリック 経由でも、プライベート エンドポイント経由でも構築可能

エージェントのインストール、設定

デプロイしたら、"作業の開始" のリンクからエージェントの ダウンロード ページ

サーバー OS に適したものをダウンロード

ダウンロードしたらインストール開始

インストール パスの指定

プロキシの指定も可能

Update で一緒に更新するかの選択

エージェントのアップデートスケジュールの指定

エージェントがインストールできたら、Azure サブスクリプションにサインイン

サインインしたら、さっき作成した同期グループを選択し、接続

接続したサーバーは、ポータル上では "登録済みサーバー" として確認できる

同期グループ作成、設定

ポータルから同期グループを作成

ここで、グループ内で同期する Azure Files を指定

同期グループを作成したら、"サーバー エンドポイントを追加" から同期対象のサーバーを追加していく

learn.microsoft.com

追加するサーバーと、同期対象のフォルダ パスを指定

階層化オプションの選択

  • Files にデータがあり、サーバー側にもコピー データを置く場合には階層化の無効化
  • Files にデータがあり、アクセス頻度に応じてサーバー側にコピー データを置くかポインターを置くかコントロールする場合には階層化の有効化

ここでは有効化

ポリシーは以下の 2 つ

  • 容量ポリシー : 指定した割合 (%) の容量をボリュームに必ず残す (100GB のボリュームで 20 を指定すると、File Sync で使用する容量を 80GB に抑える)
  • 日付ポリシー : 指定した日数以上アクセスがないファイルは階層化され、ローカルにはポインターのみ配置される (データは Files 内)

同期時の初期アップロード/ ダウンロードの選択 (いろいろスクショしてたら初期値がどれかわからなくなってしまった…)

初期アップロード

  • Azure ファイル共有のコンテンツと結合 : Files のデータをメインにするイメージだが、競合する場合は両方保持する
  • サーバーのパスのコンテンツで優先して上書き : Files の内容を、サーバー側にあるデータで上書き

初期ダウンロード

  • 最初に名前空間をダウンロード : Files からポインターをダウンロードした後、ディスクに収まるようにデータをダウンロード
  • 名前空間のみをダウンロード : Files へのポインターのみダウンロード (データはファイルへのアクセス時にダウンロード)
  • 階層化されたファイルを避ける :階層化ファイルを使わず、 各ファイルのポインターとデータの両方をダウンロード

これで設定は終わり、指定したパスにて File Sync を利用可能

Files の内容が同期グループ内の各サーバーの指定したフォルダに同期されます。

サーバー側で編集すれば、変更内容が Files 経由で他のサーバーにも同期されます。

終わりに

今回は簡単に、Azure File Sync の構築メモ

やること自体はとても簡単

Azure Resource Mover で 600 リソースを別 RG に移動する

はじめに

Azure Resource Mover で大量のリソースを移動する場合、どれくらいかかるのか試してみました。

実環境に則した様々な種類のリソース群ではないし、正直やってみないとわからない、というのが実際のところだと思うので、参考値にすらなるのかわかりませんが試した結果まとめ

learn.microsoft.com

目次

移動元の環境

1 つのリソース グループに、以下のリソースを展開

  • VNet * 2
  • NSG * 6
  • VM * 200
  • NIC * 200
  • DIsk *200

コア数の上限の関係で 1 つのリージョン (VNet) に 100 台を配置して、2 リージョン分展開

全部で 608 リソース、これを Resource Mover で別のリソース グループに移動します。

デプロイ用 Bicep

200 台のデプロイはこの Bicep ファイル (DeployVms_Repeat.bicep) を 2 回使って 100 台ずつデプロイしました。

github.com

たまに更新してたりするので、テスト時に使ったものを貼っておくとこんな感じです。

module も別の階層に置いてあります。

param location string = resourceGroup().location
param adminUserName string

@secure()
param adminUserPassword string
param numberOfWindows int
param numberOfUbuntu int

resource vnet 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: 'vnet'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '172.16.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'Subnet-1'
        properties: {
          addressPrefix: '172.16.0.0/24'
          networkSecurityGroup: {
            id: NSG1.id
          }
        }
      }
      {
        name: 'Subnet-2'
        properties: {
          addressPrefix: '172.16.1.0/24'
          networkSecurityGroup: {
            id: NSG2.id
          }
        }
      }
      {
        name: 'Subnet-3'
        properties: {
          addressPrefix: '172.16.3.0/24'
          networkSecurityGroup: {
            id: NSG3.id
          }
        }
      }
    ]
  }
  resource Subnet1 'subnets' existing = {
    name: 'Subnet-1'
  }
  resource Subnet2 'subnets' existing = {
    name: 'Subnet-2'
  }
  resource Subnet3 'subnets' existing = {
    name: 'Subnet-3'
  }
}

resource NSG1 'Microsoft.Network/networkSecurityGroups@2022-01-01' = {
  name: 'NSG-1'
  location: location
  properties: {
    securityRules: [

    ]
  }
}

resource NSG2 'Microsoft.Network/networkSecurityGroups@2022-01-01' = {
  name: 'NSG-2'
  location: location
  properties: {
    securityRules: [

    ]
  }
}

resource NSG3 'Microsoft.Network/networkSecurityGroups@2022-01-01' = {
  name: 'NSG-3'
  location: location
  properties: {
    securityRules: [

    ]
  }
}

module WinVmModule '../module/deploy-windows.bicep' = [for i in range(0, numberOfWindows): {
  name: 'winVmDeploy${padLeft(i+1, 3, '0')}'
  params: {
    adminPassword: adminUserPassword
    adminUserName: adminUserName
    location: location
    vmName: 'WinVM${padLeft(i+1, 3, '0')}'
    subnetId: vnet::Subnet1.id
    vmSize: 'Standard_B2ms'
  }
}]

module UbuVmModule '../module/deploy-ubuntu.bicep' = [for i in range(0, numberOfUbuntu): {
  name: 'ubuVmDeploy${padLeft(i+1, 3, '0')}'
  params: {
    adminPassword: adminUserPassword
    adminUserName: adminUserName
    location: location
    vmName: 'UbuVM${padLeft(i+1, 3, '0')}'
    subnetId: vnet::Subnet2.id
    vmSize: 'Standard_B1ms'
  }
}]

リソースの移動

Azure Resource Mover からリソース グループの移動を選択

ソースとターゲットのリソース グループを選択

608 個のリソースを全選択

まずは対象リソースの検証が動きます。

依存関係のあるリソースが含まれているか、とかがチェックされている様子

608 リソースありましたが、チェックは 3 分ほどで終わりました。

準備ができたら移動開始

移動開始後、3 分ほどでソース リソース グループ内のリソースが減り始める

減り始めたら、あっという間にソース側は 0 個に

ポータル上の通知が完了になるには、開始から 7 分かかりました。

ターゲットのリソース グループに、608 個のリソースが移動しています。

これで移動は完了です。

終わりに

今回はリソース グループ間のリソース移動を試しました。

最大 4 時間かかる可能性があり、どのくらいかかるか気になっていたのですが、思ったよりも全然時間がかかりませんでした。

しかし最初にも書いたとおり、実際の利用環境に則した状況を再現しているわけではありません。

LB や AppGW、SQL DB、Recovery Services コンテナーなどは一切なくシングル VM のみでテストしており、この 7 分という結果がどのくらい参考になるのか…というところではあるので、やってみたらこんな結果だった、くらいの感じで見ていただければと。

実際の環境でも、とりあえず移行前のチェックが完了するまででどのくらい時間がかかるか、を試してみるのはアリかと思います。

Azure Advisor の推奨事項に関する詳細リンクまとめ

はじめに

Azure Advisor の推奨事項の詳細を公式 Docs で探そうとしたとき、セキュリティだけ "Microsoft Defender for Cloud" の記事に飛ばされ見つけづらかったのでまとめ。

他のページもあったので 2023/5/16 に追記

目次

信頼性

5/16 追記 learn.microsoft.com

learn.microsoft.com

セキュリティ

learn.microsoft.com

パフォーマンス

5/16 追記 learn.microsoft.com

learn.microsoft.com

コスト

5/16 追記 learn.microsoft.com

learn.microsoft.com

オペレーショナル エクセレンス

5/16 追記 learn.microsoft.com

learn.microsoft.com

終わりに

セキュリティだけ全然見つからなかったのでメモ!

Azure ポリシーのカスタム ポリシーで NSG の設定値をもっと制限する

はじめに

前に似たようなタイトルで記事を書きましたが、そのときは送信元が * または Internet タグだったら、というシンプルなものでした。

今回はそれよりもう少し細かく制御するポリシーを作りました。

ロジックがなかなか面倒だったのでその辺の紹介も兼ねて。

ちなみに前回のはこれ

www.michikusayan.com

2022/10/28 x.x.x.x/x でのレンジ指定にも対応できるよう改修しました。

目次

やりたいこと

条件を満たした NSG ルールを Deny する、カスタム ポリシーを作ります。

ポリシーで記述する条件は以下。これらを満たしたら deny する

  1. NSG の受信許可ルールであること
  2. 送信元 IP アドレスに、"許可されている IP アドレス リスト" 以外の IP アドレスが含まれていること
  3. 宛先ポートに、3389, 22, * のいずれかが含まれていること

"許可された IP からしか RDP/SSH を許可できないようにする"、というのが想定しているところです。

"許可されている IP アドレス" の部分は、パラメータにして任意に指定できるようにします。

これをカスタム ポリシーにします。

カスタム ポリシー

作成したポリシーがこれです。

github.com

ロジック部分を順に解説します。

条件 1 : NSG の受信許可ルールであること

これを記述しているのは、ポリシーのこの部分です。

type が NSG のルールであるかどうか、許可ルールかどうか、受信ルールかどうかを判定しています。

ここは通常のポリシーでもあるようなシンプルな記述なのでわかりやすいと思います。

条件 2 : 送信元 IP アドレスに、許可されている IP アドレス以外が含まれていること

ここについては、まず抑えるべきポイントがあります。

ポイント 1 : 送信元アドレスが格納されるプロパティ

NSG のルールを作成したとき、送信元 IP アドレスがカンマ区切りで入力されているかどうかにより、sourceAddressPrefix と sourceAddressPrefixes のどちらのプロパティに格納されるかが異なります。

  • 送信元 IP アドレスがカンマ区切りではない場合

この場合、sourceAddressPrefix に値が格納され、もう一方の sourceAddressPrefixes は空になります。

  • 送信元 IP アドレスがカンマ区切りの場合

この場合、sourceAddressPrexies に値が格納され、もう一方の sourceAddressPrefix は消えます。 (get しても空すら見えない)

このように、入力の仕方でプロパティが異なるため、それぞれに対して判定の条件が必要です。

また、プロパティが空の場合があるため、空だった場合は評価しないように処理することも必要です。

ポイント 2 : 配列の要素の参照と比較

sourceAddressPrefixes の場合は配列のため、"許可されている IP アドレス以外が含まれているか" を評価するには各要素の比較が必要です。

その場合はここにあるように、sourceAddressPrefixes[*] という書き方をします。

learn.microsoft.com

これを比較に用いる場合、"配列のすべての要素が式を満たしたら True, 1 つでも満たさなければ False" という評価が行われます。

例として以下の場合、objectArray 配列内のすべてのプロパティが value と等しければ True、そうでなければ False という評価です。

{
  "field": "Microsoft.Test/resourceType/objectArray[*].property",
  "equals": "value"
}

この 2 つを抑えたうえで実際の記述についてです。

条件 2 を記述しているのはこの部分です。

レンジ指定にも対応しようといろいろ考えた結果こうなりました。

送信元アドレスが 1 つの場合

1 つの場合、この部分が該当します。

ここでは "A かつ (B または C)" という 3 ブロックの構成になっており、それぞれ以下のように処理しています。

1 つ目のブロックでは、sourceAddressPrefix が空かどうかを評価します。

2 つ目のブロックでは、送信元として指定された値が、Any または Internet タグかどうかを評価します。

3 つ目のブロックでは、送信元として指定された値が、allowedIpAddressesList に含まれる回数を数え、0 より大きければ許可されている IP アドレスである、という評価をしています。

この時、文字列として比較するのではなくレンジで含まれるかどうか比較するため、ipRangeContains という関数を使います。

learn.microsoft.com

これは、ipRangeContains(range, targetRange) とすることで、range の範囲に targetRange のアドレスが含まれるかどうかを判定してくれる関数です。

range = 1.1.1.1, targetRange = 1.1.1.1 と同じ値になったときも判定してくれるので、これでレンジ指定でも、そうでなくても (/32 でも) 処理することができます。

ただし注意点として、Any 指定の * がパラメータとして与えられると処理できずにエラーになってしまいます。

そのため、sourceAddressPrefix が * の場合は即 False を返す (0 回になる) よう、if 分で処理しています。

送信元アドレスが複数の場合

複数の場合、この部分が該当します。

ここは A かつ B の 2 ブロック構成です。

上のブロックで、sourceAddressPrefixes が空かどうか評価します。

下のブロックで、sourceAddressPrefixes 配列に格納されている各 IP アドレスが、allowedIpAddressesList に含まれているかどうかを評価します。

ここでは、count を 2 回使って、2 重 for 文のような処理をしています。

58 行目のコードで、sourceAddressPrefixes 配列から 1 つ値を取り出します。

60 から 68 行目のコードで、取り出した値が、allowedIpAddressesList に含まれるかどうかを、回数をカウントすることで評価します。

ここは、前述のアドレスが 1 つの場合と同様に、ipRangeContains 関数でレンジ指定も含めて評価を行い、Any 指定 (*) の場合の処理を if 文で行います。

1 つでも含まれる場合があれば、68 行目の条件に当てはまり、外側の count の回数が増加します。

ここで、"どうやって sourceAddressPrefixes[*] のうち 1 つでも allowedIpAddressesList に含まれない IP アドレスがあるか評価するか" ですが、souceAddressPrefixes に指定した値が何個であれ、各要素に対して "allowedIpAddressesList のうち 1 つにでも当てはまれば (含まれていれば) 外側のカウントを +1" という処理が行われます。

そのため、souceAddressPrefixes の要素全てが許可されるの値なのであれば、"外側の count 数 = sourceAddressPrefixes 配列の要素数" となるはずです。

それを用いて、外側の count (71 行目) で "カウントした回数が送信元 IP アドレスに指定した要素より少なかったら監査対象" という条件にしました。

これで、sourceAddressPrefixes のうち 1 つでも許可されない IP があれば監査、という評価を実現できていると思います。

文字ばかりになってしまい非常にわかりづらいと思うので、できればフローチャート的なのいれたい…

ここを考えるのが一番難しかったです。これでうまくできていると思うのですが、そうならないパターンがあれば、ぜひ教えてください。

条件 3 : 宛先ポートに、3389, 22, * のいずれかが含まれていること

宛先ポートについても送信元アドレスと同様に、カンマ区切りで入力されているかどうかにより、destinationPortRange と destinationPortRanges のどちらのプロパティに格納されるかが異なります。

そのため、この前半部分は送信元 IP アドレスの場合と同じ考え方で OK です。

一方で、ポートの場合は 20 - 100 のようなハイフンでの連番指定があるため、それを処理する必要があります。

連番指定の場合

連番指定の場合、"a - b" のように入力されます。

ここに 3389 または 22 が含まれるか判定するには、"-" が含まれていたらハイフンの両側にある a, b を個別に取得し、a ≦ 3389 (or 22) ≦ b を満たすかどうかを評価します。

カンマ区切りでない場合のハイフンの評価と、カンマ区切りの場合のハイフンの評価をし、どこかで 3389 または 22 の許可を検知したら True としています。

それを実装しているのが後半のこの辺りです。

これで宛先ポートに 3389 or 22 が含まれるかどうか評価することができます。

未実装な部分 (実装済み)

ここまででもだいぶ手がかかったのですが、実はこれでも不十分です。

それは、x.x.x.x/x のように指定しても文字列として比較しているため、レンジ的には許可 IP アドレスの範囲に含まれる、というのが評価できません。

例として、パラメーターの許可 IP アドレス リストに 192.168.0.0/16 を指定した状態で、NSG のルール作成時に送信元アドレスに 192.168.1.10 を入力したルールを作成した場合、 レンジには含まれるので 3389/22 宛てでも許可してほしいところですが、本ポリシーだと deny されます。

192.168.0.0/16 と 192.168.1.10 を文字列として比較し、一致していないためです。

本来はそこまでできて完成なのですが、ここまでのポリシー作成のためのロジックと検証で力つきました。

レンジに含まれるかどうか判定する ipRangeContains という関数があるので、/ を含んでいる場合はそれを使って判定、ということをする感じになると思います。

余裕があれば足したいとは思っていますが、現状はこういう状態なので参照する場合は注意してください。

今回の改修でレンジ指定に対応できるようになったので、全部対応できるはず!

が、すべてのケースを整理してテストしているわけではないので、そのまま使用する場合は必ずテストしてください。

終わりに

NSG 向けのカスタム ポリシーを頑張って作ってみました。結果、かなり大変でした。

NSG 以外のリソースならカンマ区切りくらいはあれど、プロパティが変わったりハイフンや CIDR のレンジ指定はほとんどないだろうと思うので、NSG のルールだからこうなのだろうと思います…

FW のルールとかくらいかな…

少しずつ変えながら何度も検証して、いろんな Docs やサンプルとか回ってやっとできたので、ガバナンスのためにいろんなリソースをポリシーで制御しよう、でもビルトインにないからカスタムで作っていこう、となったらなかなか大変そう。

Azure VM の Scheduled Event をポーリングする

はじめに

Azure VM で発生するメンテナンス イベントの情報を取得し、メンテナンスに備えるための猶予を与える Azure Metadata Service をポーリングするスクリプトを、Python で作成してみました。

(polling のスペルミスに気付かないまま作ってしまった…)

2022/10/27 : ファイルの出力を yyyy/MM/dd/hh/mm の階層構造になるように修正しました

設定したスクリプトの動作テスト編を記事にしたのでよかったらこちらも参照ください。

www.michikusayan.com

目次

やりたいこと

Scheduled Event

docs.microsoft.com

Azure VM では、メンテナンスやハードウェア障害、ポータルからの操作などを様々なことをきっかけに、以下のようなイベントが起こりえます。

  • freeze: VM の一時停止
  • reboot: VM の再起動
  • redeploy: ホストの移動
  • preempt: スポット VM の削除
  • terminate: VM の削除

Azure VM 内から叩ける API である Azure Metadata Service を使用することで、これらのイベントの予定をある程度事前に検知することができます。

それにより、必要に応じてアプリケーションが正常に中断するための準備処理を行うことや、イベントが発生したことを記録するためにイベント ログを残す、などの対処を行う時間的猶予を得ることができます。

それが Scheduled Event です。

Azure VM の中から以下のエンドポイントにリクエストを投げることで、レスポンスとして Scheduled Event の情報を返します。

http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01

どれくらい早く検知できるかはイベントの種類にもよりますが、Docs によれば一時停止メンテと再起動で、最大 15 分程度前には検知できます。

前置きが長くなりましたが、前述のとおり、このエンドポイントは Azure VM 内から API を叩く必要があります。

アプリケーションの重要度によるところもありますが、少しでも早くイベントを検知したいのであればポーリング頻度として 1 秒がお勧めされているため、今回は Python スクリプトで Scheduled Event を毎秒ポーリングしてみます。

実現イメージ

東日本リージョンに RHEL をデプロイし、必要なものをインストールして、Python スクリプトを動かします。

停止、起動してもスクリプトが止まらないよう、デーモン化させて動かすことにしました。

ログ ファイルは、マウントして使える Azure Files に出力しています。

環境構築

Azure Files マウント

デプロイしたのは RHEL 8.2 です。

SMB でマウントするため、cifs-utils をインストールします。

sudo yum update -y
sudo yum install -y cifs-utils

ストレージ アカウントと、ファイル共有をデプロイ (容量はデフォルトの 5TiB)

ファイル共有の接続ボタンから、Linux 用の接続スクリプトをコピーして実行

マウント、fstab 設定までやってくれるので便利です。

"/mnt/マウント ポイント" にマウントされるので適当にファイルを作成して、ポータルから見えるかチェックしてもいいと思います。

Python 設定

インストール

今回は Python 3.8 をインストールしました。

sudo yum install -y python38
alternatives 設定

このままだと、"python" コマンドで動かず、"python3" とすると python3.6 が動いてしまうため、"python" で 3.8 を指すよう alternatives で変更してしまいます。

sudo alternatives --config python

変更前の確認

変更 & 変更後の確認

拡張機能インストール

今回のスクリプトで必要な request 拡張機能をインストールします。

sudo python -m pip install requests

root 使って入れない方がいい、と警告されたので sudo つけないで --user にするのがよいかも。

インストール後、script normalizer が PATH に入ってないと言われたので PATH に追加しときます。

export PATH=$PATH:/usr/local/bin

スクリプト設定

1 秒毎に Scheduled Event のエンドポイントを叩いて、結果を json ファイルとして Azure Files に出力しています。

毎秒実行については、こちらの記事を参考にしました。

qiita.com

スクリプト配置

作成したスクリプトはここにあるので、任意の場所にダウンロードします。

今回は home ディレクトリでやってしまいます。

github.com

curl -O https://raw.githubusercontent.com/kzk839/Polling_ScheduledEvent/main/Polling_ScheduledEvent.py

basepath 変数に Azure Files のマウントパスが書かれているため、変更しておきます。
(今回の場合は /mnt/scheduledevent/)

デーモン化して実行するので、システムに実行権限を付与します。

sudo chmod o+x Polling_ScheduledEvent.py

テスト

ここで一度、スクリプトを実行しておきます。

Scheduled Event のエンドポイントは初回のリクエストを投げた際に初めて有効化され、その際応答には 1, 2 分を要します。

デーモン化してから初めて実行すると待ち時間の間にエラーが出て、止まってしまったりするので、動作確認も兼ねてここで動かしておくのがいいと思います。

こんな感じで 1 秒ごとにファイルが出力されていれば OK

※2022/10/27 に、ファイルの出力を yyyy/MM/dd/hh/mm の階層構造になるように修正したため、現在は日時の変化に従いディレクトリが自動で追加されていきます。

例として、2022/10/27 18:24:35 のログだったら、 /2022/10/27/18/24/20221027-182435.json が出力されます。

ちなみに、Scheduled Event のエンドポイントを 24 時間叩かないと、無効化されます。(次叩いたときにまた 1 分くらい待たされる)

デーモン化

ログオフしたり、再起動したりしても実行し続けるよう、スクリプトをデーモン化します。

デーモン化については参考にした qiita 記事があったのですが、なくなってしまいました…

デーモン化するには、以下のファイルを作成します。

名前 (xxx.service) は任意です。

sudo vi /etc/systemd/system/polling_scheduledevent.service

中身はこれを書きます。

[Unit]
Description = polling scheduled event daemon

[Service]
Type = simple
Restart = on-failure
ExecStart = Python3までの絶対パス スクリプトまでの絶対パス

[Install]
WantedBy = multi-user.target

今回だとこんな感じ

これで準備はできたので起動して、動作を確認します。

sudo systemctl start polling_scheduledevent
sudo systemctl status polling_scheduledevent

ステータスを確認して、active (running) になれば OK です。

Files にバンバン出力されていると思います。

問題なければ最後に、自動起動を有効化します。

sudo systemctl enable polling_scheduledevent

これで実装は完了です!

終わりに

以上、Scheduled Event を毎秒ポーリングする Python スクリプトの実装でした。

このスクリプトではサボっていますが、ファイルが大量に出力されるので、日時に応じて出力フォルダを分けるとか、前回と内容が変化していなかったら出力しない、などした方が後から見やすいです。 サボらずディレクトリを作るよう更新しました。

今回実装しているのはイベント情報の出力だけなので、イベントへの対処をするのであれば、取得したイベントの種類に応じて更に処理を書き加えていく感じです。

最初は Windows でタスク スケジューラでやろうとしたんですがなかなかうまくいかず、デーモン化の記事を見つけて試したらあっさりできました…