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 の性質上、実現できないことも当然ありますが、その有用性はご理解いただけたのではないでしょうか。

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