JavaScriptを有効にしてください

Azure Policy を GitHub で管理し隊

 ·   18 分で読めます  ·   [Kento GitHub Copilot]

はじめに

Azure Policy はリソースの構成にルールを適用し、組織のガバナンスを維持するためのサービスです。
しかし、ポリシーの数が増えてくると Azure Portal 上での手動管理は大変になってきます。

Policy as Code は、ポリシー定義を JSON ファイルとして GitHub で管理し、GitHub Actions で自動デプロイする手法です。
これにより、ポリシーの変更履歴をコードとして追跡でき、レビュープロセスも組み込めるようになります。

本記事では「特定のリージョン以外での Storage Account の作成を拒否する」というシナリオを例に、
GitHub リポジトリにポリシーを配置し、GitHub Actions で Azure にデプロイする一連の流れを解説します。

想定読者

  • Azure Policy の基本的な概念を知っている方
  • GitHub / GitHub Actions の操作に慣れている方
  • Azure のガバナンスをコードで管理したい方

背景

手動管理の課題

Azure Portal でポリシーを管理していると、以下のような課題が生じます:

  • 変更履歴が追えない – 誰がいつ何を変更したかが分かりにくい
  • レビューの仕組みがない – ポリシー変更を他のメンバーが確認するフローがない
  • 環境間の差分が生まれやすい – 本番環境とテスト環境でポリシーが異なる状態になりがち

コード管理のメリット

Policy as Code のアプローチを取ることで、以下のメリットが得られます:

  • Git による変更履歴の管理 – すべての変更がコミットとして記録される
  • Pull Request によるレビュー – ポリシー変更に対して承認フローを組み込める
  • CI/CD による自動デプロイmain ブランチへのマージで自動的に Azure へ反映される
  • 環境の一貫性 – 同じコードから複数環境にデプロイできる

全体像

まず、Policy as Code のワークフロー全体像を確認しましょう。

graph TB
    A[ポリシー定義を<br>JSON で作成] --> B[feature ブランチに<br>Push]
    B --> C[GitHub Actions<br>がトリガー]
    C --> D[検証環境に<br>DoNotEnforce で<br>デプロイ]
    D --> E[コンプライアンス<br>結果を確認]
    E --> F[PR レビュー・承認]
    F --> G[main ブランチに<br>マージ]
    G --> H[GitHub Actions<br>がトリガー]
    H --> I[本番環境に<br>Default で<br>デプロイ]

    classDef code fill:#f9f0ff,stroke:#6f42c1,color:#333;
    classDef github fill:#f0f7ff,stroke:#0366d6,color:#333;
    classDef staging fill:#fff8e1,stroke:#f9a825,color:#333;
    classDef prod fill:#e6f3ff,stroke:#0078d4,color:#333;

    class A code;
    class B,F,G github;
    class C,D,E staging;
    class H,I prod;

ポイントは以下の通りです:

  • ポリシーの変更はすべて GitHub リポジトリ を経由する
  • feature ブランチへの push で検証環境enforcementMode: DoNotEnforce でデプロイされる(リソース作成はブロックしないが、コンプライアンス評価は実行される)
  • PR レビュー・承認後に main ブランチへマージすると、本番環境enforcementMode: Default でデプロイされる
  • policies/ フォルダ配下のファイルが変更されたときだけ GitHub Actions が実行される
  • Azure へのデプロイは Azure CLI を使い、認証には Workload Identity Federation(OIDC) を使用する

ざっくり手順

本記事では、以下の手順でポリシーの GitHub 管理を構築します:

  1. GitHub リポジトリにポリシー定義を作成
    • フォルダ構成を決めて、ポリシールールとパラメータを JSON で定義
  2. サービスプリンシパルを作成してシークレットに登録
    • Microsoft Entra ID でアプリ登録を行い、OIDC 用のフェデレーション資格情報を設定
    • 検証環境用・本番環境用のサブスクリプション情報をそれぞれ登録
  3. GitHub Actions ワークフローを作成
    • feature ブランチ → 検証環境(DoNotEnforce)、main ブランチ → 本番環境(Default)の 2 段階デプロイを定義
  4. 検証環境でポリシーのデプロイを確認
    • feature ブランチへの push でワークフローを実行し、検証環境にポリシーが作成されたことを確認
  5. 本番環境への反映と動作確認(Storage Account の作成テスト)
    • main ブランチへのマージで本番環境にデプロイし、リソース作成が拒否されることを確認

手順 1: GitHub リポジトリにポリシー定義を作成

フォルダ構成

GitHub リポジトリに以下のフォルダ構成でポリシー定義を配置します。
公式ドキュメントの推奨構成 を参考にしつつ、シンプルにまとめています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.
├── .github/
│   └── workflows/
│       └── deploy-policy.yml        # GitHub Actions ワークフロー
├── policies/
│   └── deny-storage-location/
│       ├── metadata.json            # ポリシーメタデータ(名前・割り当て情報)
│       ├── policy.rules.json        # ポリシールール
│       ├── policy.parameters.json   # パラメータ定義
│       └── assign.subscription.json # 割り当て定義(参考用)
└── README.md

ポイントは以下の 4 点です:

  • policies/ フォルダにポリシー定義をまとめる
  • ポリシーごとに サブフォルダdeny-storage-location/)を切る
  • ポリシー定義は ルール (policy.rules.json) と パラメータ (policy.parameters.json) に分割する
  • 各ポリシーに メタデータ (metadata.json) を配置し、ワークフローが動的に読み取る

メタデータの作成 (metadata.json)

ワークフローが各ポリシーの定義名や割り当て情報を動的に読み取るための設定ファイルです。
このファイルがあることで、ポリシーを追加してもワークフローの修正が不要になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "name": "deny-storage-location",
  "displayName": "Storage Account の作成を許可されたリージョンに制限する",
  "description": "指定されたリージョン以外での Storage Account の作成を拒否します",
  "mode": "All",
  "assignment": {
    "name": "deny-storage-location-assignment",
    "displayName": "Storage Account リージョン制限",
    "parameters": {
      "allowedLocations": {
        "value": [
          "japaneast",
          "japanwest"
        ]
      }
    }
  }
}

各フィールドの役割は以下の通りです:

  • name – ポリシー定義の一意な名前(az policy definition create --name に使用)
  • displayName / description – Azure Portal 上での表示名と説明
  • mode – 評価モード(All ですべてのリソースタイプを対象にする)
  • assignment – ポリシー割り当ての名前・表示名・パラメータ値

新しいポリシーを追加する場合は、policies/ 配下にサブフォルダを作成し、このファイルと policy.rules.jsonpolicy.parameters.json を配置するだけで OK です。

ポリシールールの作成 (policy.rules.json)

「指定されたリージョン以外で Storage Account を作成しようとした場合に拒否する」ルールです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "if": {
    "allOf": [
      {
        "field": "type",
        "equals": "Microsoft.Storage/storageAccounts"
      },
      {
        "field": "location",
        "notIn": "[parameters('allowedLocations')]"
      }
    ]
  },
  "then": {
    "effect": "deny"
  }
}

ルール部分だけを抜き出しているため、非常にシンプルです。
allOf の条件で「リソースタイプが Storage Account かつ リージョンが許可リストに含まれない」場合に deny(拒否)します。

パラメータ定義の作成 (policy.parameters.json)

許可するリージョンをパラメータとして定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "allowedLocations": {
    "type": "Array",
    "metadata": {
      "displayName": "許可するリージョン",
      "description": "Storage Account の作成を許可するリージョンのリスト"
    },
    "defaultValue": [
      "japaneast",
      "japanwest"
    ]
  }
}

デフォルト値として japaneast(東日本)と japanwest(西日本)を指定しています。
割り当て時にこのパラメータを上書きすることも可能です。

割り当て定義(参考用)(assign.subscription.json)

ポリシーの割り当て定義も JSON で管理しておくと、設定内容を記録できて便利です。
本記事では GitHub Actions 内で az policy assignment create を直接実行する方式を採用していますが、参考として配置しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "properties": {
    "displayName": "Storage Account リージョン制限",
    "description": "許可されたリージョン以外での Storage Account の作成を拒否します",
    "policyDefinitionId": "/subscriptions/{subscription-id}/providers/Microsoft.Authorization/policyDefinitions/deny-storage-location",
    "parameters": {
      "allowedLocations": {
        "value": [
          "japaneast",
          "japanwest"
        ]
      }
    },
    "enforcementMode": "Default"
  }
}

手順 2: サービスプリンシパルを作成してシークレットに登録

GitHub Actions から Azure にデプロイするために、Workload Identity Federation(OIDC) を使った認証を設定します。
従来のクライアントシークレット方式と比べて、シークレットの有効期限管理が不要で、よりセキュアです。

2-1. Microsoft Entra ID でアプリ登録を作成

まず、Azure CLI でアプリ登録とサービスプリンシパルを作成します。

1
2
3
4
5
6
7
8
# アプリ登録の作成
az ad app create --display-name "github-policy-deployer"

# 作成されたアプリの ID を取得
APP_ID=$(az ad app list --display-name "github-policy-deployer" --query "[0].appId" -o tsv)

# サービスプリンシパルの作成
az ad sp create --id $APP_ID

2-2. サービスプリンシパルにロールを割り当て

ポリシーの作成と割り当てに必要な権限を付与します。
サブスクリプションスコープで Resource Policy Contributor ロールを割り当てます。

検証環境と本番環境で異なるサブスクリプションを使用する場合は、両方のサブスクリプションに対してロールを割り当てる必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 検証環境のサブスクリプションにロールを割り当て
SUBSCRIPTION_ID_STAGING="<検証環境のサブスクリプション ID>"
az role assignment create \
  --assignee $APP_ID \
  --role "Resource Policy Contributor" \
  --scope "/subscriptions/$SUBSCRIPTION_ID_STAGING"

# 本番環境のサブスクリプションにロールを割り当て
SUBSCRIPTION_ID_PROD="<本番環境のサブスクリプション ID>"
az role assignment create \
  --assignee $APP_ID \
  --role "Resource Policy Contributor" \
  --scope "/subscriptions/$SUBSCRIPTION_ID_PROD"

2-3. フェデレーション資格情報(OIDC)の設定

GitHub Actions が OIDC トークンを使って Azure に認証できるようにするため、フェデレーション資格情報を作成します。
本ワークフローでは GitHub の Environments 機能(staging / production)を使用しているため、環境ごとにフェデレーション資格情報を登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# アプリのオブジェクト ID を取得
APP_OBJECT_ID=$(az ad app list --display-name "github-policy-deployer" --query "[0].id" -o tsv)

# staging 環境用(検証環境デプロイ用)
az ad app federated-credential create \
  --id $APP_OBJECT_ID \
  --parameters '{
    "name": "github-actions-staging",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<your-org>/<your-repo>:environment:staging",
    "audiences": ["api://AzureADTokenExchange"],
    "description": "GitHub Actions - staging 環境からの検証環境デプロイ用"
  }'

# production 環境用(本番環境デプロイ用)
az ad app federated-credential create \
  --id $APP_OBJECT_ID \
  --parameters '{
    "name": "github-actions-production",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<your-org>/<your-repo>:environment:production",
    "audiences": ["api://AzureADTokenExchange"],
    "description": "GitHub Actions - production 環境からの本番環境デプロイ用"
  }'

この設定により、GitHub Actions が実行時に発行する OIDC トークンと Azure 側のアプリ登録が信頼関係で結ばれ、クライアントシークレットなしで認証が行えるようになります。

2-4. GitHub リポジトリにシークレットを登録

GitHub リポジトリの Settings > Secrets and variables > Actions で、シークレットを登録します。
本ワークフローでは GitHub の Environments 機能(staging / production)を使い分けているため、シークレットも Repository secretsEnvironment secrets を適切に使い分けます。

Repository secrets(両環境共通)

リポジトリの Settings > Secrets and variables > Actions > Repository secrets に登録します。

シークレット名 説明
AZURE_CLIENT_ID アプリ登録のクライアント ID az ad app list --display-name "github-policy-deployer" --query "[0].appId" -o tsv
AZURE_TENANT_ID テナント ID az account show --query tenantId -o tsv

Environment secrets(環境ごとに異なる値)

リポジトリの Settings > Environmentsstagingproduction の各環境を作成し、それぞれに以下のシークレットを登録します。
同じシークレット名 AZURE_SUBSCRIPTION_ID を使い、環境ごとに異なる値を設定します。

環境 シークレット名
staging AZURE_SUBSCRIPTION_ID 検証環境のサブスクリプション ID
production AZURE_SUBSCRIPTION_ID 本番環境のサブスクリプション ID

ワークフローの environment: staging / environment: production の指定により、GitHub Actions が実行時に対応する環境のシークレット値を自動的に解決します。
このため、ワークフロー側では secrets.AZURE_SUBSCRIPTION_ID という単一の参照で済みます。

手順 3: GitHub Actions ワークフローを作成

いよいよ GitHub Actions のワークフローを作成します。
.github/workflows/deploy-policy.yml に以下の内容を配置します。

feature ブランチの push で検証環境にデプロイし、main ブランチへのマージで本番環境にデプロイする 2 段階構成です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
name: Deploy Azure Policy

on:
  push:
    branches:
      - main
    paths:
      - 'policies/**'
  pull_request:
    branches:
      - main
    paths:
      - 'policies/**'
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  # -----------------------------------------------
  # 検証環境へのデプロイ(PR 作成・更新時に実行)
  # -----------------------------------------------
  deploy-staging:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Azure Login (検証環境)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy Policies (検証環境 - DoNotEnforce)
        run: |
          for policy_dir in policies/*/; do
            [ ! -f "${policy_dir}metadata.json" ] && continue

            NAME=$(jq -r '.name' "${policy_dir}metadata.json")
            DISPLAY_NAME=$(jq -r '.displayName' "${policy_dir}metadata.json")
            DESCRIPTION=$(jq -r '.description' "${policy_dir}metadata.json")
            MODE=$(jq -r '.mode' "${policy_dir}metadata.json")

            echo "==> Deploying policy definition: ${NAME}"
            az policy definition create \
              --name "$NAME" \
              --display-name "$DISPLAY_NAME" \
              --description "$DESCRIPTION" \
              --rules "${policy_dir}policy.rules.json" \
              --params "${policy_dir}policy.parameters.json" \
              --mode "$MODE"

            ASSIGNMENT_NAME=$(jq -r '.assignment.name' "${policy_dir}metadata.json")
            ASSIGNMENT_DISPLAY_NAME=$(jq -r '.assignment.displayName' "${policy_dir}metadata.json")
            ASSIGNMENT_PARAMS=$(jq -c '.assignment.parameters' "${policy_dir}metadata.json")

            echo "==> Deploying policy assignment: ${ASSIGNMENT_NAME} (DoNotEnforce)"
            az policy assignment create \
              --name "$ASSIGNMENT_NAME" \
              --display-name "${ASSIGNMENT_DISPLAY_NAME}(検証)" \
              --policy "$NAME" \
              --params "$ASSIGNMENT_PARAMS" \
              --enforcement-mode DoNotEnforce
          done          

  # -----------------------------------------------
  # 本番環境へのデプロイ(main ブランチへのマージ時に実行)
  # -----------------------------------------------
  deploy-production:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Azure Login (本番環境)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy Policies (本番環境 - Default)
        run: |
          for policy_dir in policies/*/; do
            [ ! -f "${policy_dir}metadata.json" ] && continue

            NAME=$(jq -r '.name' "${policy_dir}metadata.json")
            DISPLAY_NAME=$(jq -r '.displayName' "${policy_dir}metadata.json")
            DESCRIPTION=$(jq -r '.description' "${policy_dir}metadata.json")
            MODE=$(jq -r '.mode' "${policy_dir}metadata.json")

            echo "==> Deploying policy definition: ${NAME}"
            az policy definition create \
              --name "$NAME" \
              --display-name "$DISPLAY_NAME" \
              --description "$DESCRIPTION" \
              --rules "${policy_dir}policy.rules.json" \
              --params "${policy_dir}policy.parameters.json" \
              --mode "$MODE"

            ASSIGNMENT_NAME=$(jq -r '.assignment.name' "${policy_dir}metadata.json")
            ASSIGNMENT_DISPLAY_NAME=$(jq -r '.assignment.displayName' "${policy_dir}metadata.json")
            ASSIGNMENT_PARAMS=$(jq -c '.assignment.parameters' "${policy_dir}metadata.json")

            echo "==> Deploying policy assignment: ${ASSIGNMENT_NAME} (Default)"
            az policy assignment create \
              --name "$ASSIGNMENT_NAME" \
              --display-name "$ASSIGNMENT_DISPLAY_NAME" \
              --policy "$NAME" \
              --params "$ASSIGNMENT_PARAMS"
          done          

ワークフローの解説

このワークフローの各ポイントを詳しく見ていきましょう。

トリガー条件 (on)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
on:
  push:
    branches:
      - main
    paths:
      - 'policies/**'
  pull_request:
    branches:
      - main
    paths:
      - 'policies/**'
  workflow_dispatch:
  • push + branches: mainmain ブランチへの push(マージ含む)で 本番環境デプロイジョブがトリガーされます
  • pull_request + branches: mainmain ブランチ向けの PR が作成・更新されたときに 検証環境デプロイジョブがトリガーされます。これにより、feature ブランチでの変更が自動的に検証環境に反映されます
  • paths: 'policies/**'policies/ フォルダ配下のファイルが変更された場合のみ実行されます。ワークフローファイルや README の変更では実行されないため、無駄な実行を防げます
  • workflow_dispatch – GitHub の Actions タブから手動で実行することもできます。初回デプロイやトラブルシューティング時に便利です

パーミッション (permissions)

1
2
3
permissions:
  id-token: write
  contents: read
  • id-token: write – Workload Identity Federation(OIDC)認証に必須の設定です。GitHub Actions が OIDC トークンを発行するために必要です
  • contents: read – リポジトリのコードを checkout するための読み取り権限です

Azure ログイン (azure/login@v2)

1
2
3
4
5
6
- name: Azure Login (検証環境)
  uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

azure/login@v2 アクションに client-idtenant-idsubscription-id を指定することで、自動的に OIDC(OpenID Connect) による認証が行われます。
クライアントシークレットを渡す必要がないのが、Workload Identity Federation の大きなメリットです。

ワークフローの environment 指定により、同じ secrets.AZURE_SUBSCRIPTION_ID でも 環境ごとに異なる値が自動的に解決されます。

ポリシーの動的デプロイ

従来はポリシーごとにデプロイステップを個別に記述する必要がありましたが、本ワークフローでは policies/*/ ディレクトリを動的にループして、すべてのポリシーを自動的にデプロイします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- name: Deploy Policies (検証環境 - DoNotEnforce)
  run: |
    for policy_dir in policies/*/; do
      [ ! -f "${policy_dir}metadata.json" ] && continue

      NAME=$(jq -r '.name' "${policy_dir}metadata.json")
      DISPLAY_NAME=$(jq -r '.displayName' "${policy_dir}metadata.json")
      DESCRIPTION=$(jq -r '.description' "${policy_dir}metadata.json")
      MODE=$(jq -r '.mode' "${policy_dir}metadata.json")

      echo "==> Deploying policy definition: ${NAME}"
      az policy definition create \
        --name "$NAME" \
        --display-name "$DISPLAY_NAME" \
        --description "$DESCRIPTION" \
        --rules "${policy_dir}policy.rules.json" \
        --params "${policy_dir}policy.parameters.json" \
        --mode "$MODE"

      ASSIGNMENT_NAME=$(jq -r '.assignment.name' "${policy_dir}metadata.json")
      ASSIGNMENT_DISPLAY_NAME=$(jq -r '.assignment.displayName' "${policy_dir}metadata.json")
      ASSIGNMENT_PARAMS=$(jq -c '.assignment.parameters' "${policy_dir}metadata.json")

      echo "==> Deploying policy assignment: ${ASSIGNMENT_NAME} (DoNotEnforce)"
      az policy assignment create \
        --name "$ASSIGNMENT_NAME" \
        --display-name "${ASSIGNMENT_DISPLAY_NAME}(検証)" \
        --policy "$NAME" \
        --params "$ASSIGNMENT_PARAMS" \
        --enforcement-mode DoNotEnforce
    done    

処理の流れは以下の通りです:

  1. for policy_dir in policies/*/policies/ 配下のすべてのサブディレクトリをループします
  2. metadata.json の存在チェックmetadata.json が無いディレクトリはスキップします
  3. jq でメタデータを読み取り – ポリシー定義名・表示名・説明などを metadata.json から取得します
  4. az policy definition create – ポリシー定義を作成します。--rules--params は各ディレクトリ内の JSON ファイルを参照します
  5. az policy assignment create – ポリシー割り当てを作成します。検証環境では --enforcement-mode DoNotEnforce を付与し、本番環境では省略(Default)します

この仕組みにより、新しいポリシーを追加する際はワークフローの修正が不要です。policies/ 配下に新しいサブフォルダを作成し、metadata.jsonpolicy.rules.jsonpolicy.parameters.json を配置するだけで自動的にデプロイ対象になります。

検証環境と本番環境の違いは --enforcement-mode のみです:

  • 検証環境(DoNotEnforce – リソース作成をブロックせず、コンプライアンス評価のみ実行します。Azure Portal のダッシュボードで違反リソースを確認できます
  • 本番環境(Default – ポリシーに違反するリソースの作成を実際に拒否します

ジョブの使い分け (if 条件)

1
2
3
4
5
6
7
8
9
# 検証環境ジョブ
deploy-staging:
  if: github.event_name == 'pull_request'
  environment: staging

# 本番環境ジョブ
deploy-production:
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  environment: production
  • deploy-stagingpull_request イベント時のみ実行されます。feature ブランチから main ブランチへの PR を作成・更新すると、自動的に検証環境にポリシーがデプロイされます
  • deploy-productionmain ブランチへの push(PR マージ含む)時のみ実行されます
  • environment – GitHub の Environments 機能を利用しています。本番環境には承認者を設定することで、追加の保護レイヤーを設けることもできます

手順 4: 検証環境でポリシーのデプロイを確認

feature ブランチでの作業

まず、feature ブランチを作成してポリシーファイルを変更し、Pull Request を作成します。

1
2
3
4
5
git checkout -b feature/add-storage-location-policy
# policies/ 配下のファイルを追加・編集
git add policies/
git commit -m "Add deny-storage-location policy"
git push origin feature/add-storage-location-policy

GitHub 上で main ブランチ向けの Pull Request を作成すると、検証環境デプロイジョブ(deploy-staging)が自動的に実行されます。

image01
Pull Request 作成:

検証環境でのデプロイ確認

ワークフローが正常に完了したら、検証環境の Azure CLI でポリシーが作成されたことを確認します。

1
2
3
4
5
# 検証環境のサブスクリプションに切り替え
az account set --subscription "<検証環境のサブスクリプション ID>"

# ポリシー定義の確認
az policy definition show --name "deny-storage-location" --query "{name:name, displayName:displayName, policyType:policyType}" -o table

実行結果の例:

1
2
3
Name                     DisplayName                                                PolicyType
-----------------------  ---------------------------------------------------------  ----------
deny-storage-location    Storage Account の作成を許可されたリージョンに制限する      Custom
1
2
# ポリシー割り当ての確認(enforcementMode が DoNotEnforce であること)
az policy assignment show --name "deny-storage-location-assignment" --query "{name:name, displayName:displayName, enforcementMode:enforcementMode}" -o table

実行結果の例:

1
2
3
Name                                  DisplayName                          EnforcementMode
------------------------------------  -----------------------------------  ---------------
deny-storage-location-assignment      Storage Account リージョン制限(検証) DoNotEnforce

検証環境では enforcementModeDoNotEnforce なので、リソースの作成はブロックされません
ただし、コンプライアンス評価は実行されるため、Azure Portal の ポリシー > コンプライアンス ダッシュボードで、ポリシーに違反するリソースが存在するかどうかを確認できます。

image02
Azure Portal でのコンプライアンス評価結果:

手順 5: 本番環境への反映と動作確認(Storage Account の作成テスト)

PR のマージ

検証環境でのテストが問題なければ、Pull Request をレビュー・承認して main ブランチにマージします。
マージすると 本番環境デプロイジョブ(deploy-production)が自動的に実行され、enforcementMode: Default(強制)でポリシーが適用されます。

1
2
3
4
5
# 本番環境のサブスクリプションに切り替え
az account set --subscription "<本番環境のサブスクリプション ID>"

# ポリシー割り当ての確認(enforcementMode が Default であること)
az policy assignment show --name "deny-storage-location-assignment" --query "{name:name, displayName:displayName, enforcementMode:enforcementMode}" -o table

実行結果の例:

1
2
3
Name                                  DisplayName                    EnforcementMode
------------------------------------  -----------------------------  ---------------
deny-storage-location-assignment      Storage Account リージョン制限  Default

動作確認

本番環境でポリシーの enforcementModeDefault(強制)になったことを確認したら、実際にリソースの作成を試みて動作を確認します。

テスト用リソースグループの作成

1
az group create --name "rg-policy-test" --location japaneast

テスト 1: 許可されていないリージョンでの作成(拒否されること)

東日本・西日本以外のリージョン(eastus)で Storage Account を作成しようとします。

1
2
3
4
5
az storage account create \
  --name "testsa$(date +%s)" \
  --resource-group "rg-policy-test" \
  --location eastus \
  --sku Standard_LRS

ポリシーにより拒否され、以下のようなエラーが表示されるはずです:

image03
リソース作成の拒否エラー:

テスト 2: 許可されたリージョンでの作成(成功すること)

許可されたリージョン(japaneast)で Storage Account を作成します。

1
2
3
4
5
az storage account create \
  --name "testsa$(date +%s)" \
  --resource-group "rg-policy-test" \
  --location japaneast \
  --sku Standard_LRS

こちらは正常に作成できることを確認します。

テスト後のクリーンアップ

テストで作成したリソースは削除しておきましょう。

1
2
# リソースグループごと削除
az group delete --name "rg-policy-test" --yes --no-wait

まとめ

本記事では、Azure Policy を GitHub で管理する Policy as Code のアプローチを紹介しました。

  • ポリシー定義を JSON ファイルとして GitHub リポジトリで管理 することで、変更履歴の追跡と Pull Request によるレビューが可能になりました
  • Workload Identity Federation(OIDC) を使って、クライアントシークレットを保存せずにセキュアな認証を実現しました
  • GitHub Actions の 2 段階デプロイにより、feature ブランチの変更は検証環境DoNotEnforce で、main ブランチへのマージは本番環境Default でデプロイする安全なワークフローを構築しました
  • enforcementMode を活用して、検証環境ではコンプライアンス評価のみ行い、本番環境で強制するという段階的なアプローチを実現しました

Policy as Code を導入することで、Azure のガバナンスをチーム全体で管理・レビューできる体制が整います。
ポリシーの数が増えてきたら、イニシアティブ(ポリシーセット)を活用してグルーピングすることも検討してみてください。

参考

公式ドキュメント

GitHub Actions 関連

更新履歴

日付 内容
2026/03/16 初版作成
共有

Kento
著者
[Kento GitHub Copilot]
2020年に新卒で IT 企業に入社. インフラエンジニア(主にクラウド)として活動中