#017: azコマンドで取得したNSGの受信ルール情報をJMESPathでフィルターした話

※本記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません

これまでの処理で、KeyVaultに保管したVMのパスワードを使って、システムオペレータがVMにログインできるようにしてきた。

KeyVaultの参照は正常にできるようになったものの、クライアントPCのIPがBastionSubnetのNSGで許可されていないため、今の状態ではBastionからVMへアクセスする時にNSGでブロックされてしまう。

今回やりたいのは以下の処理。

- BastionSubnetのNSG受信ルールに自分のPCのパブリックIPを追加する処理をスクリプト化し、必要な時に必要最低限の情報のみで受信ルールの登録を手動実行したい。

毎回実行するというよりは、クライアントPCの接続環境により登録するかどうかが変わるので、必要な時に手動でスクリプトを実行できるようにしておきたいと思った。

スクリプトを作成する時、「NSGの既存受信ルールの一番優先度の低いレコードを取得する」というJMESPathの定義に少し調査時間がかかったが、やってみると面白かったのでここにまとめる。

JMESPathについては以下を参照。

https://jmespath.org/examples.html

# 1. 新しい受信ルールを追加するスクリプトの定義

まずは作成したスクリプトはこちらなので、コメントから大体の流れを確認。

```bash
#!/bin/bash

# This script adds an inbound rule to a specified Network Security Group (NSG) 
# to allow traffic from the client's public IP address.
# The rule is added with a priority that is 10 lower than the current lowest 
# priority inbound rule with a priority less than 4096.
# If the calculated priority exceeds 4096, the script exits with an error.
#
# Usage:
# ./addInboundRule.sh <resource-group> <nsg-name>
#
# Parameters:
# <resource-group> : The name of the resource group containing the NSG.
# <nsg-name>       : The name of the Network Security Group to which the rule
#                    will be added.
#
# Example:
# ./addInboundRule.sh myResourceGroup myNsg
#
# Recommendations:
# - Ensure that you have the necessary permissions to modify the NSG.
# - Run this script from a secure environment to avoid exposing your public IP
#   address.
# - Verify the existing rules and priorities in the NSG to avoid conflicts.

# Check parameters.
if [[ "$#" -ne 2 ]]; then
  echo "Usage: $0 <resource-group> <nsg-name>"
  exit 1
fi

# Set parameters to variables that are easier to understand.
RESOURCE_GROUP=$1
NSG_NAME=$2

# Get public IP address of the client.
IP_ADDRESS=$(curl -s ifconfig.me)
echo "  Your Public IP Address: $IP_ADDRESS"

# Check if the NSG rule already exists.
SAME_RULE_NAME=$(az network nsg rule list --resource-group $RESOURCE_GROUP --nsg-name $NSG_NAME --query "[?sourceAddressPrefix=='$IP_ADDRESS'&&direction=='Inbound'] | [0].name" -o tsv)
if [[ -n "$SAME_RULE_NAME" ]]; then
  echo "Ignored: The IP address '$IP_ADDRESS' already exists in Inbound rule '$SAME_RULE_NAME' of NSG '$NSG_NAME' in resource group '$RESOURCE_GROUP'."
  exit 1
fi

# Build rule name.
# Example: AllowInboundFrom_1.2.3.4
RULE_NAME="AllowInboundFrom_$IP_ADDRESS"

# Retrieve the lowest priority value among the Inbound rules with a priority less than 4096.
# If the lowest priority value cannot be obtained, set the default priority.
LOWEST_PRIORITY=$(az network nsg rule list --resource-group $RESOURCE_GROUP --nsg-name $NSG_NAME --query "[?priority < to_number('4096') && direction == 'Inbound'] | reverse(sort_by([], &priority)) | [0].priority" -o tsv)
echo "  Current lowest Inbound rule priority: $LOWEST_PRIORITY"
if [[ -z "$LOWEST_PRIORITY" ]]; then
  LOWEST_PRIORITY=100
fi

# Calculate the new priority.
# If the new priority exceeds the maximum allowed value of 4096 then exit with an error.
NEW_PRIORITY=$((LOWEST_PRIORITY + 10))
echo "  New rule priority: $NEW_PRIORITY"
if [[ "$NEW_PRIORITY" -gt 4096 ]]; then
  echo "Error: The calculated priority ($NEW_PRIORITY) exceeds the maximum allowed value of 4096."
  exit 1
fi

# Add the new NSG rule.
az network nsg rule create --resource-group "$RESOURCE_GROUP" --nsg-name "$NSG_NAME" --name "$RULE_NAME" --priority "$NEW_PRIORITY" --direction Inbound --access Allow --protocol TCP --source-address-prefixes "$IP_ADDRESS" --source-port-ranges "*" --destination-address-prefixes "*" --destination-port-ranges "443"
if [[ $? -eq 0 ]]; then
  echo "Success: Added NSG rule '$RULE_NAME' with priority '$NEW_PRIORITY' to NSG '$NSG_NAME' in resource group '$RESOURCE_GROUP'."
else
  echo "Failed to add NSG rule."
  exit 1
fi
```

実物はGithubリポジトリを参照。

https://github.com/masatoshisato/RagSystem/blob/v1.0.8/infra/scripts/addInboundRule.sh

# 2. スクリプト作成時に調査した事の整理

スクリプト内の要点を以下にまとめる。

## 2-1. 自分のパブリックIPの取得

37行目、自分のPCのパブリックIPを取得するコマンド。

パラメータで指定しても良いが、自動で取得できるならその方が良いと考えた。

もし、会社のPIPがある程度の範囲で定義されているのであれば、CIDR形式で範囲を指定したIPアドレスをパラメータ指定するように変えても良いかな。

```bash
curl -s ifconfig.me
```

## 2-2. 既存ルールの同じパブリックIPのルール名を取得

41行目、クライアントPCのパブリックIPが既に登録されているかどうかチェックするために、既存ルールのルール名を取得。

同じパブリックIPが登録されている場合は、そのルール名をstdoutして処理を中断する。

```bash
az network nsg rule list --resource-group $RESOURCE_GROUP --nsg-name $NSG_NAME --query "[?sourceAddressPrefix=='$IP_ADDRESS'&&direction=='Inbound'] | [].name" -o tsv
```

## 2-3. NSGの既存受信ルールの取得とフィルター

53行目、NSGの既存受信ルールの一番優先度が低いレコードを取得。

JMESPathは、MSドキュメントのサンプルを見てわかったようなフリをしていたが、自分のやりたいこと決めて試行錯誤しながら定義したのは初めて。このルールを定義するのに苦労したので後ほど詳解する。

ここで取得したレコードから”priority”属性値だけを取得して+10すれば今回付与したい優先度になる。

```bash
az network nsg rule list --resource-group $RESOURCE_GROUP --nsg-name $NSG_NAME --query "[?priority < to_number('4096') && direction == 'Inbound'] | reverse(sort_by([], &priority)) | [0].priority" -o tsv
```

## 2-4. NSGへの新しい受信ルールの追加

69行目、自分のPCのパブリックIPを受信ルールに追加。

IPアドレスと優先度は上記で取得・計算した値を指定する。

名前は、自分のPCのIPを許可することがわかるような名前にする。

```bash
az network nsg rule create --resource-group "RagSystem-dev" --nsg-name "RagSystem-BastionNsg-dev" --name "AllowFromClientIP220" --priority "220" --direction Inbound --access Allow --protocol TCP --source-address-prefixes "103.5.140.141" --source-port-ranges "*" --destination-address-prefixes "*" --destination-port-ranges "443"
```

# 3. 既存受信ルールを取得するためのフィルター詳解

調査結果として残したかったのはここ、53行目。

一旦フィルター処理している箇所を分解。

```bash
az network nsg rule list 
	--resource-group "RagSystem-dev" 
	--nsg-name "RagSystem-BastionNsg-dev" 
	--query "
		[?                               # 1-1
			priority < to_number('4096') # 1-2
			&&                           # 1-3
			direction == 'Inbound'       # 1-4
		] 
		|
		reverse(                         # 2-1
			sort_by(                     # 2-2
				[],                      # 2-3
				&priority                # 2-4
			)
		)
		|
		[0].priority"                    # 3-1
	-o tsv
```

以下３つのクエリーをパイプ（｜）で繋いで実行する。

1. 優先度が最大値（4096）より小さい受信ルールのレコード配列を抽出
2. 抽出したレコード配列を優先度降順で並び替え
3. 配列の先頭レコード（一番優先度が低い）のpriority値を取得

## 3-1. 優先度が最大より小さい受信ルールのレコード配列を抽出

- 1-1. 配列要素に対するクエリの実行を宣言
- 1-2. 配列内の”priority”という数値が、4096以下のレコード

:::note
この時、以下のようにすることができると考えたのだが、JMESPathでは数値をそのまま指定してもinvalid valueエラーとなる。

```bash
priority < 4096
```

数値の比較をどのように定義すれば良いのか、JMESPathのドキュメントには見つけることができず、あてずっぽうで「こうやればいけんじゃね？」的に探し当てたのが、この「文字列として定義しto_number関数で数値化して比較する」だった。ドキュメントとして明記されているサイトがわかる人、是非URLを教えてほしい。。
:::

- 1-3. 配列要素に対するクエリーをAND条件で複数クエリーを組み合わせる
- 1-4. 配列内の”direction”に”Inbound”が指定されているレコード

ここまでで、NSGのルール配列の中から、優先度が4096未満、かつ受信ルールのレコードだけが抽出される。

## 3-2. 出したレコード配列を優先度降順で並び替え

- 2-1. reverse関数を用いて、レコードを逆順にする
- 2-2. sort_by関数を用いて、レコード全体をpriorityの昇順に並び替える
- 2-3. sort_by関数の処理対象は、配列全体
- 2-4. sort_by関数の並び替えのキーは”priority”の値

sort_by関数の中で、配列要素全体を指定しているが、これは元のJson全体ではなく、フィルター後にパイプで引き継がれた情報が対象。

## 3-3. 配列の先頭レコード（最も優先度が低いレコード）のpriority値を取得

- 3-1. 配列要素0番目（先頭＝これまでの処理で一番優先度が低いレコードになっている）の”priority”値（優先度）のみを抽出

以上で、追加したいIPの優先度のベースとなる値を取得できた。

あとはスクリプトに記載した通り、この値に+10した値を、新たに追加する許可IPの優先度として、NSGの受信ルールに追加した。

# 4. JMESPathを使ったjsonデータのフィルタリングのまとめ

今回JMESPathについて少し詳細に調査しながら使ってみた。
言語仕様や関数の使い方は独特なものがあるものの、SQLもしくはKQLに慣れている人にはとっつきやすいかもしれないと感じた。

- 必要な条件でデータをフィルタリング
- （必要に応じて）必要な順番に並び替え
- 必要な情報を取得