Azure Policy カスタムポリシーことはじめ

クラウドサービスのガバナンス

Azure を組織内の複数の人で共同利用するときに、リソースグループやリソースに Owner タグは必ず付与してね、みたいなルールを課すことがあると思います。少人数組織であればルールを守らせることはできるでしょうが、利用者が多かったり、人の入れ替わりが激しかったりすると、ルールを守らせることが途端に難しくなります。環境を管理する専用の人を立てても、利用者の数に応じて管理コストは増えますし、また将来その人がいなくなった時点で統制が効かなくなるため、将来に渡って有効な手段にはなり得ません。

Azure には幸い Azure Policy という無料のサービスがあり、リソースグループやリソースが取れる「状態」(設定値)に制限をかけることが可能です。 Azure Policy は、リソースの状態に制限をかけるという点で、利用者の「操作」に制限をかける RBAC を補完するものと言えるでしょう。

ポリシー定義と割り当て

Azure Policy には、「ポリシー定義」と「割り当て」という二つの概念があります。

ポリシー定義

ポリシー定義は、その名のとおり、ポリシーの中身を定義するものです。例えば、「〇〇タグが付与されていないリソースグループを拒否する」といった「条件」と「効果」を定義します。「拒否」という言葉は少しわかりにくいですが、要するにユーザーのリクエストを拒否するということなので、リソースやリソーググループの作成および更新が失敗することを意味します。

割り当て

一方の割り当ては、ポリシー定義をどのスコープ(管理グループやサブスクリプション、リソースグループ)に割り当てるかを決めるものです。割り当ての際に、ポリシー定義で変数としていた部分に値を与えることが可能です。例えば、前述の例の「〇〇」という変数に Owner という値を与えて、「リソースグループに Owner タグが付与されていないリソースを拒否する」という割り当てを行うことが可能です。ポリシー定義は複数の異なるスコープに対して使い回せるので、例えばリソースグループ A では Owner タグが必須となる割り当てを行い、リソースグループ B では CreatedOn タグが必須となる割り当てを行う、といったことが可能です。

割り当ての際に指定するスコープには、「除外」の概念があります。サブスクリプション全体に割り当てるけども、その中のリソースグループ A は除外する、という制御ができます。何かをシステム的に制限する場合は、例外的に許可してほしいというユーザー要望は当然のように想定すべきなので、この点は安心ですね。

ビルトインポリシーとカスタムポリシー

ポリシー定義には、ビルトインポリシーとカスタムポリシーの二種類があります。ビルトインポリシーは Azure に予め用意されているもので、特定のタグを必須にするポリシーもビルトインポリシーとして用意されています。カスタムポリシーはユーザーが自分で定義できるものです。カスタムポリシーは自分で一から定義するほか、ビルトインポリシーを複製してカスタマイズすることでも作れるので、まずは複製から始めてみると比較的楽にカスタムポリシーを作ることができます。

カスタムポリシーを作成する際には、ポリシー定義を作成する場所に注意が必要です。例えば、サブスクリプション A に作成したカスタムポリシーは、サブスクリプション B には割り当てられません。複数のサブスクリプションでポリシー定義を使い回したい場合は、二つの方法があります。一つは、ポリシー定義の二重管理を覚悟の上で各々のサブスクリプションにポリシー定義を作成する方法です。もう一つは、複数のサブスクリプションを一つの管理グループ配下にまとめて、その管理グループにポリシー定義を作成する方法です。既定では、すべてのサブスクリプションTenant Root Group という管理グループ直下に存在するので、Tenant Root Group にポリシー定義を作成するのが楽でしょう。

ビルトインポリシーの例

それでは、ビルトインポリシーの「リソース グループのタグを追加または置換する」を見てみましょう。

{
  "properties": {
    "displayName": "リソース グループのタグを追加または置換する",
    "policyType": "BuiltIn",
    "mode": "All",
    "description": "リソース グループが作成または更新された場合に、指定されたタグと値を追加または置換します。既存のリソース グループは、修復タスクをトリガーすることによって修復できます。",
    "metadata": {
      "category": "Tags"
    },
    "parameters": {
      "tagName": {
        "type": "String",
        "metadata": {
          "displayName": "タグ名",
          "description": "タグの名前 (例: environment)"
        }
      },
      "tagValue": {
        "type": "String",
        "metadata": {
          "displayName": "タグ値",
          "description": "タグの値 (例: production)"
        }
      }
    },
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Resources/subscriptions/resourceGroups"
          },
          {
            "field": "[concat('tags[', parameters('tagName'), ']')]",
            "notEquals": "[parameters('tagValue')]"
          }
        ]
      },
      "then": {
        "effect": "modify",
        "details": {
          "roleDefinitionIds": [
            "/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
          ],
          "operations": [
            {
              "operation": "addOrReplace",
              "field": "[concat('tags[', parameters('tagName'), ']')]",
              "value": "[parameters('tagValue')]"
            }
          ]
        }
      }
    }
  },
  "id": "/providers/Microsoft.Authorization/policyDefinitions/d157c373-a6c4-483d-aaad-570756956268",
  "type": "Microsoft.Authorization/policyDefinitions",
  "name": "d157c373-a6c4-483d-aaad-570756956268"
}

第一階層のキー群

第一階層には id, type, name, properties の4つのキーがあります。簡単なものから説明します。

type

固定値なので、特に説明は不要と思います。

name

ポリシー定義の名前です。名前とは言っても、 Auzre Portal に表示されるのは、これとは別の表示名 (properties.displayName) なので、目にする機会はあまりありません。ちなみに、ビルトインポリシーではご覧の通り GUID 形式になっていますが、カスタムポリシーを CLI で作成する場合は GUID 形式以外の文字列を自分で指定することもできます。

id

ポリシー定義をグローバルに一意に特定するための識別子です。 id の値が "/providers/Microsoft.Authorization/policyDefinitions/d157c373-a6c4-483d-aaad-570756956268" となっていることからもわかるように、このポリシー定義のリソースは Microsoft が管理するリソースプロバイダー /providers/Microsoft.Authorization の配下に存在します(つまり、ユーザーがリソースを作成できない場所に存在します)。

一方で、例えばユーザーが自身のサブスクリプション {subscriptionId} にカスタムポリシーを作成すると、 id の値は "/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/policyDefinitions/{name}" といった値になります。ここで、 {name} の値は前述の name キーの値と同一になっています。

properties

properties がポリシー定義の本体とも言える部分です。 その中でも特に policyRule がコア部分なので、以下で説明します。

第二階層の policyRule キー

properties に属する第二階層のキーの中で特に重要なのが、 policyRule キーです。第三階層に if キーと then キーがありますが、それぞれがポリシーの「条件」と「効果」に対応しています。抽象化して構造だけ抜き出すと以下のようになります。 allOf はいわゆる AND 条件で、以下の例では条件 A も条件 B も満たされる場合に、 if の評価が true となります。なお、 allOf の代わりに anyOf と書くと、 OR 条件になります。 then 内の effect には、効果の種類を書きます。 "modify" という値は、はタグの付与や更新をするときに使う値です。似たようなものに "append" がありますが、こちらはタグ以外のリソースのプロパティを更新する際に指定します。

"policyRule": {
    "if": {
        "allOf": [
            {
                // 条件A
            },
            {
                // 条件B
            }
        ]
    },
    "then": {
        "effect": "modify", //適用する効果の種類
        "details": {
            // 効果の詳細("effect" の値によっては "details" 自体不要)
        }
    }
}

カスタムポリシーの作成

それでは、リソースグループの最終更新日をタグとして保持することを目的として、「タグ〇〇の値を日付 YYYY-mm-dd (UTC 基準) に書き換える」カスタムポリシーを作成してみます。タグ名となる「〇〇」の部分は UpdatedOn とか LastUpdate とかなんでもよいのですが、変数化したままにします。

完成したカスタムポリシーの properties の一部だけを以下に抜粋します。なお // のコメント部分は説明用に記載しているので実際のポリシー定義には含めないでください。

{
    "parameters": {
      "tagName": {
        "type": "String",
        "metadata": {
          "displayName": "タグ名",
          "description": "タグの名前 (例: environment)"
        }
      }
      // 変更1: ここにあった tagValue を削除
    },
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Resources/subscriptions/resourceGroups"
          }
        ]
      },
      "then": {
        "effect": "modify",
        "details": {
          "roleDefinitionIds": [
            "/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
          ],
          "operations": [
            {
              "operation": "addOrReplace",
              "field": "[concat('tags[', parameters('tagName'), ']')]",
              "value": "[substring(utcNow(), 0, 10)]" // 変更2: 値を書き換え
            }
          ]
        }
      }
    }
}

変更1

ベースにしたビルトインポリシーでは parameters 内で tagNametagValue という二つの変数が定義されていましたが、後述の変更2に伴って変数 tagValue が不要となったので削除しました。

変更2

また、 policyRule.then.details.operations.value の値を "[substring(utcNow(), 0, 10)]" に書き換えています。 utcNow() は現在のタイムスタンプを ISO8601 形式の文字列として取得する関数で、それを substrng() して日付部分を取得しています。

カスタムポリシーの動作確認

最後に、上記のポリシー定義をサブスクリプションに割り当てて動作確認してみます。割り当ての際に、パラメータ tagName には "UpdatedOn" を指定しました。

期待する挙動は以下の通りです。

  • リソースグループの作成または更新時に UpdatedOn タグが存在しなければ作成し、その値をその時点の日付 (YYYY-mm-dd) で上書きする。

それでは、リソーグループを作成してみます。

リソースグループの作成画面で Owner タグのみ指定します。

f:id:umanohoneo:20191209052846p:plain

この状態でリソースグループを作成すると、確かに UpdatedOn タグが付与されました。

f:id:umanohoneo:20191209052931p:plain

なお、画像は載せませんが、この後にタグの値を更新しても、強制的に日付で上書きされます。

まとめ

この記事では、 Azure Policy のビルトインポリシーをカスタマイズしてカスタムポリシーを作成する方法を説明しました。

この記事で扱ったのはリソースグループのタグ付与という単純な例でしたが、カスタムポリシーではもっと色々なことができます。例えば、 NSG がアタッチされていないサブネットの作成を未然に防いだり、予め用意しておいた NSG を、サブネットが新たに作成された際に強制的にアタッチしたりといったことも可能です。もちろん、リソースの状態に制限を掛けるという Azure Policy の性質上、実現できないことも当然ありますが、その有用性はご理解いただけたのではないでしょうか。

個人的には、守られないルールを作るより、こういった方法でルールの逸脱を未然に防ぐといった考え方にシフトしていったほうが、皆幸せになるのではと思っています。

Azure Functions (Python) でマネージド ID を使ってトークンを取得する

Azure Function のマネージド ID を有効化して、ローカルのエンドポイント (MSI_ENDPOINT) からトークンを取得してみます。

func newHttpTrigger を選択したときに生成されるコードを編集して、以下のコードを作成しました。

# __init__.py
import os
import logging

import azure.functions as func
from requests import get

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    # The two lines below are available only when managed ID enabled 
    msi_endpoint = os.getenv('MSI_ENDPOINT')
    msi_secret = os.getenv('MSI_SECRET')

    headers = {
        'secret': msi_secret
    }
    params = {
        'resource': 'https://vault.azure.net',
        'api-version': '2017-09-01'
    }
    response = get(url=msi_endpoint, headers=headers, params=params)

    logging.info(response.text)
    return func.HttpResponse('function was called')

resource の部分には、 Azure AD 認証をサポートするサービスを指定する必要があります。 今回は Key Vault を指定しました。 docs.microsoft.com

マネージド ID をローカルでテストする方法は今のところなさそうなので、 Function App に発行 (publish) して動作確認します。

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する (Python)

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する Python スクリプトです。

import requests

def get_tenantid_from_subscriptionid(subscriptionid):
  uri = 'https://management.azure.com/subscriptions/' + \
    subscriptionid + '?api-version=2015-01-01'
  headers = {
    'Authentication': 'Bearer xxx' 
  }
  resp = requests.get(uri, headers=headers)
  www_authenticate_header = resp.headers['WWW-Authenticate']
  tenantid = www_authenticate_header.split(',')[0].split('"')[1].split('/')[-1]
  return tenantid

参考

WWW-Authenticate の応答ヘッダーの値からテナント ID を取得します。 docs.microsoft.com

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する PowerShell 関数です。

function Get-AzureADTenantIDFromSubscriptionID {
  param([parameter(Mandatory=$true)][String]$subscriptionId)

  $uri = "https://management.azure.com/subscriptions/${subscriptionId}?api-version=2015-01-01"
  $headers = @{
      'Authorization' = 'Bearer xxx'
  }

  try {
    Invoke-RestMethod `
      -Uri ${uri} `
      -headers ${headers}
  } catch {
    $wwwAuthenticateHeader = $_.Exception.Response.Headers["WWW-Authenticate"]
    $tenantId = ${wwwAuthenticateHeader}.split(',')[0].split('"')[1].split('/')[-1]
  }

  ${tenantId}
}

参考

WWW-Authenticate の応答ヘッダーの値からテナント ID を取得します。 docs.microsoft.com

PowerShell で ISO 8601 拡張形式の現在時刻を取得する

Get-Date の Format オプションに "o" または "O" を指定する。

Get-Date -Format "o"
# 2018-01-28T17:34:35.7418503+09:00

小数点以下が不要なら、 -replace 演算子で "." (ピリオド)から数字が連続する部分を削除する。

(Get-Date -Format "o") -replace "\.[0-9]+"
# 2018-01-28T17:34:41+09:00

SharePoint Online のサーバーリソースクォータに意味はない

Disclaimer: 本エントリは2018年1月5日現在の Office 365 の仕様に基づいています。

SharePoint Online のサイトコレクション単位で設定するサーバーリソースクォータ (Server Resource Quota) は、サンドボックスソリューションのために用意されている仕組みである。サンドボックスソリューションがサポートされなくなった今では、サーバーリソースクォータを設定することに意味はない。 SharePoint 管理センターのサイトコレクション作成の画面で既定で 300 という値が指定されているが、値を 0 にして(=クォータを設定せずに)サイトコレクションを作成しても問題ない。

f:id:umanohoneo:20180105015842p:plain Fig: サーバーリソースクォータ

不要なパラメータは一刻も早く無効化してほしいものだ。

Office 365 E3 ライセンスのうち Exchange Online (Plan 2) だけをユーザーに割り当てる

Disclaimer: 本エントリの内容は2018年1月3日時点の Office 365 の仕様に基づいています。

honeotech.hatenablog.com

上記のエントリでユーザーに Office 365 のライセンスを割り当てる方法について言及したが、組織によっては「 E1 を購入したが Exchange Online しか使わせたくない」といった場合もあると思うので、そのような場合のライセンス割り当ての方法を検証してみた。

検証したこと

Office 365 Enterprise E3 ライセンスのうち Exchange Online (Plan 2) だけをユーザーに割り当てる。

はじめに

Azure AD PowerShell v1 では Set-MsolUserLicense コマンドレットによってユーザーにライセンスを割り当てることができる。 E1 や E3 といった SKU の単位でユーザーにライセンスを割り当てることが可能であるが、それらに含まれるサービスごとのライセンスを個別に割り当てることも可能である。本エントリでは、 E3 に含まれる Exchange Online (Plan 2) だけをユーザーに割り当てる方法を示す。

SKU に含まれる特定のライセンスのみを割り当てるためには、割り当て対象のライセンスではなく、除外するライセンスを明示的に列挙する必要がある。例えば、今回のケースでは E3 に含まれる Exchange Online (Plan 2) 以外のすべてのライセンスを列挙する必要がある。この制約は、 New-MsolLicenseOption コマンドレットの仕様によるものである。

E3 に含まれるライセンスは将来的に変わる可能性があるため、はじめに E3 に含まれるライセンスの一覧を Office 365 から取得し、そこから Exchange Online (Plan 2) を除いて、「除外するライセンス」の一覧を作成することとする。

ライセンス割り当ての大まかな流れは次のとおり。

  1. E3 に含まれるライセンスの一覧を取得する
  2. Exchange Online (Plan 2) を除外したライセンスのセットを定義する
  3. ライセンスを割り当てる
  4. ライセンスの割り当て状況を確認する

E3 に含まれるライセンスの一覧を取得する

Get-MsolAccountSku コマンドレットで E3 (ENTERPRISEPACK)*1 のオブジェクトを取得し、そこからライセンスの情報を抽出する。

# Connect-MsolService が完了した状態で

# E3 ライセンスの AccountSku を取得する
$e3AccountSku = Get-MsolAccountSku | where {$_.AccountSkuId -match "ENTERPRISEPACK"}
$e3AccountSku
# 
# AccountSkuId              ActiveUnits WarningUnits ConsumedUnits
# ------------              ----------- ------------ -------------
# xxxxxxxxxx:ENTERPRISEPACK 25          0            8
# 
#
# E3 ライセンスに含まれる各サービスの ServiceName を取得する
$e3ServiceName = ($e3AccountSku | Select-Object -ExpandProperty ServiceStatus | Select-Object -ExpandProperty ServicePlan).ServiceName
$e3ServiceName
# BPOS_S_TODO_2
# FORMS_PLAN_E3
# STREAM_O365_E3
# Deskless
# FLOW_O365_P2
# POWERAPPS_O365_P2
# TEAMS1
# PROJECTWORKMANAGEMENT
# SWAY
# INTUNE_O365
# YAMMER_ENTERPRISE
# RMS_S_ENTERPRISE
# OFFICESUBSCRIPTION
# MCOSTANDARD
# SHAREPOINTWAC
# SHAREPOINTENTERPRISE
# EXCHANGE_S_ENTERPRISE

ここで BPOS_S_TODO_2 (To-Do (Plan 2)) から EXCHANGE_S_ENTERPRISE (Exchange Online (Plan 2)) までの17行が、 E3 に含まれるサービス(ライセンス)を表す文字列である。

Exchange Online (Plan 2) を除外したライセンスのセットを定義する

前の手順で取得したライセンスの一覧から Exchange Online (Plan 2) を除外し、ライセンスの一覧を再生成する。

# EXCHANGE_S_ENTERPRISE を除外した ServiceName の一覧を作成する
$serviceName = $e3ServiceName | sls -Pattern "EXCHANGE" -NotMatch
$serviceName
# 
# BPOS_S_TODO_2
# FORMS_PLAN_E3
# STREAM_O365_E3
# Deskless
# FLOW_O365_P2
# POWERAPPS_O365_P2
# TEAMS1
# PROJECTWORKMANAGEMENT
# SWAY
# INTUNE_O365
# YAMMER_ENTERPRISE
# RMS_S_ENTERPRISE
# OFFICESUBSCRIPTION
# MCOSTANDARD
# SHAREPOINTWAC
# SHAREPOINTENTERPRISE
# 
# 

次に、 New-MsolLicenseOption コマンドレットで LicenseOption オブジェクトを生成する。その際、 -DisalbedPlans オプションに前述のライセンス一覧を指定する。これにより、 Exchange Online (Plan 2) 以外のライセンスが無効化された E3 の LicenseOption オブジェクトが生成される。

# LicenseOption オブジェクトを作成する
$licenseOption =  New-MsolLicenseOptions -AccountSkuId 'xxxxxxxxxx:ENTERPRISEPACK' -DisabledPlans $serviceName

ライセンスを割り当てる

Set-MsolUserLicense コマンドレットでユーザーにライセンスを割り当てる。この時、 -LicenseOption オプションで、前の手順で作成した LicenseOption オブジェクトを指定する。なお、ユーザーに E3 ライセンスを既に割り当てている場合は、 -AccountSkuId オプションは省略可能である。

# ライセンスを割り当てる
Set-MsolUserLicense -UserPrincipalName <upn> -LicenseOptions $licenseOption -AddLicenses "xxxxxxxxxx:ENTERPRISEPACK"
# 何も出力されなければ成功

ライセンスの割り当て状況を確認する

最後に、 Get-MsolUser コマンドレットでライセンスの割り当て状況を確認する。

# ライセンスの割り当て状況を確認する
Get-MsolUser -UserPrincipalName <upn> | Select-Object -ExpandProperty Licenses | Select-Object -ExpandProperty ServiceStatus
# 
# ServicePlan           ProvisioningStatus
# -----------           ------------------
# BPOS_S_TODO_2         Disabled
# FORMS_PLAN_E3         Disabled
# STREAM_O365_E3        Disabled
# Deskless              Disabled
# FLOW_O365_P2          Disabled
# POWERAPPS_O365_P2     Disabled
# TEAMS1                Disabled
# PROJECTWORKMANAGEMENT Disabled
# SWAY                  Disabled
# INTUNE_O365           PendingActivation
# YAMMER_ENTERPRISE     Disabled
# RMS_S_ENTERPRISE      Disabled
# OFFICESUBSCRIPTION    Disabled
# MCOSTANDARD           Disabled
# SHAREPOINTWAC         Disabled
# SHAREPOINTENTERPRISE  Disabled
# EXCHANGE_S_ENTERPRISE Success  # Exchange Online (Plan 2) のみが割り当てられている
# 

Exchange Online (Plan 2) のみが割り当てられていることが確認できた。

*1:E1 の場合は STANDARDPACK