GitHub ActionsとAzure Blueprintでリソースグループを自動デプロイする

こんにちは。

最近Azureに入門&運用について考える機会があり、**Azure Blueprint**という便利機能を知りました。

Azure Blueprintはリソースグループのテンプレートを作っておいて、テンプレートに従ったリソースグループをデプロイできるサービスです。実態としてはARMテンプレートの集まりですが、
- ブループリント定義のバージョン管理
- 割り当てリソースグループの追跡
- リソースグループの削除防止

などの便利な機能が使えます。

通常Azure BluePrintからリソースグループをデプロイするには、Azure PortalやCLIで実施する必要がありますが、**GitHub Actionsから必要なパラメータを指定して自動実行する方法を実装してみました。**

作成したブループリント定義とGitHub Actionsワークフローは以下のリポジトリにあります。

https://github.com/quotto/azure-blueprint-workflow

## 想定するユースケースと構成

以下のようなユースケースを想定して実装してみます。

1. リソースグループがほしい開発者は、必要な事項を入力してGitHub Actionsのワークフローを起動する。
1. GitHub Actionsワークフローの中で以下の処理を実行する。
    - リソースグループの所有者グループを作成する。
    - Azure BluePrintを使ってリソースグループをデプロイする。この時、動的なパラメータは1.の入力値を利用する。またリソースグループの所有者はこのワークフローの中で生成したグループを指定する。

![GitHubActionsとAzureBlurpintによるデプロイ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/0d550faf-e6f7-7277-217a-d80356e4022c.png)


## 作成するブループリント
今回は以下のブループリントを作成します。

- サブスクリプション
    - **「リソースグループにタグ`Department`の設定を必須とする」** ポリシーを設定する。
- リソースグループ： sandbox-rg
    - 動的パラメータを受け取って **「sandbox-<パラメータ値>-rg」** という名前で作成する。
    - **「リソースの作成可能な場所を制限する」** ポリシーを設定する。
    - 動的パラメータを受け取って所有者を設定する。

Azureポータルから見ると完成系はこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/65281a1f-e33b-36f2-2bc1-c0eaccaf770e.png)


## 実装手順
それではフローを実装してみます。

### ブループリント定義の作成
まずデプロイに必要なブループリントを作成します。

#### ブループリント定義作成時の留意点
最初に注意点として、今回のようにAzure CLIなどからブループリントの割り当てを行う場合、**定義の作成はAzureポータルからではなく、JSONファイルを作成してCLI経由で作成することをお勧めします。**

理由としてAzureポータルからブループリント定義を作成した場合、**動的パラメータやリソースグループのキー名が勝手につけられて非常に扱いにくくなります。**

試しにAzureポータルでブループリント定義を作って、作成後に「JSONビュー」から定義を確認を確認してみます。（後述するblueprint.jsonに相当するものです。）

`parameters`や`resourceGroups`内でリソースグループを識別するキーが**日本語を使った値になっています。**

![parametersのキーが日本語になる.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/21ee60e0-bee2-c4bc-09bc-c87333ed2c17.png)
![リソースグループのキーも自動で付与される.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/488e1fc9-caf8-7b34-567b-b029eb41e587.png)


仮に英数字のみにできたとしても、あくまで自動で付与される名前ですので**編集の度にJSONビューで確認する必要があり手間になります。**

#### JSONファイルでブループリント定義を作成する
ということで、まずはJSONファイルで定義を作成します。

今回は`blueprint`ディレクトリの中に以下の構成で作成します。
```
blueprint
├── artifacts
│   ├── policy-allowed-resource-location.json
│   ├── policy-set-department-tag.json
│   └── role-sandbox-rg-owner.json
└── blueprint.json
```


- `artifacts`: ポリシーやリソースの定義（JSONファイル）を格納するディレクトリ。
- `blueprint.json`: ブループリント全体の定義。パラメーターとリソースグループを定義する。

##### blueprint.json

ブループリント全体の定義を以下に示します。

```json:blueprint.json
{
    "properties": {
        "targetScope": "subscription",
        "parameters": {
            "resourceGroupName": {
                "type": "string",
                "allowedValues": [],
                "defaultValue": "",
                "metadata": {
                    "displayName": "リソースグループの名前",
                    "description": "resourceGroupName"
                }
            },
            "tagValueOfDepartment": {
                "type": "string",
                "allowedValues": [],
                "metadata": {
                    "displayName": "departmentタグの値"
                }
            },
            "listOfAllowedLocations": {
                "type": "Array",
                "metadata": {
                    "description": "リソースのデプロイ時に指定できる場所の一覧。",
                    "strongType": "location",
                    "displayName": "許可されている場所"
                },
                "defaultValue": [
                    "japaneast",
                    "japanwest"
                ]
            },
            "ownerPrincipalId": {
                "type": "array",
                "metadata": {
                    "displayName": "作成するリソースグループの所有者のプリンシパルID",
                    "strongType": "PrincipalId"
                }
            }
        },
        "resourceGroups": {
            "sandbox-rg": {
                "name": "[concat(concat('sandbox-',parameters('resourceGroupName')),'-rg')]",
                "dependsOn": [],
                "metadata": {
                    "displayName": "sandbox-rg"
                }
            }
        }
    }
}
```

ポイントとしてリソースグループ名（`sandbox-rg.name`）はユーザーの入力値を受け取ってプリフィックス、サフィックスを付けたいので **[concat(concat('sandbox-',parameters('resourceGroupName')),'-rg')]** という感じで、ARMテンプレートの関数を利用しています。


##### アーティファクトの定義
各アーティファクトはディレクトリ`artifacts`内に個別に作成します。

Departmentタグを自動でリソースグループに追加するポリシーはサブスクリプションに対して設定するため、`resourceGroup`を指定しません。

タグの値は動的に設定するため **[parameters('tagValueOfDepartment')]** としています。
```json:policy-set-department-tag.json
{
    "kind": "policyAssignment",
    "properties": {
        "displayName": "departmentタグの設定",
        "dependsOn": [],
        "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71",
        "parameters": {
            "tagName": {
                "value": "Department"
            },
            "tagValue": {
                "value": "[parameters('tagValueOfDepartment')]"
            }
        }
    }
}
```

リソースの作成場所を制限するポリシーはリソースグループに対して設定するため、`resourceGroup`を指定します。

許可されるリソースの場所は動的に指定するため **[parameters('listOfAllowedLocations')]** としています。
```json:policy-allowed-resource-location.json
{
    "kind": "policyAssignment",
    "properties": {
        "displayName": "許可されるリソースの場所",
        "dependsOn": [],
        "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c",
        "parameters": {
            "listOfAllowedLocations": {
                "value": "[parameters('listOfAllowedLocations')]"
            }
        },
        "resourceGroup": "sandbox-rg"
    }
}
```

リソースグループに対する所有者を指定します。`principalIds`は動的に指定するため **[parameters('ownerPrincipalId')]** としています。
```json:role-sandbox-rg-owner.json
{
    "kind": "roleAssignment",
    "properties": {
        "displayName": "リソースグループ所有者",
        "dependsOn": [],
        "roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
        "principalIds": "[parameters('ownerPrincipalId')]",
        "resourceGroup": "sandbox-rg"
    }
}
```

#### 作成したブループリント定義をアップロードする

定義ファイルを作成したら、以下のコマンドでAzure上に定義をアップロードします。

すでに同じ名前の定義がある場合はドラフト版が上書きされます。

```bash
az blueprint import -y --name SandboxBluePrint --input-path ./blueprint --subscription <サブスクリプションID>
```

- `--name`: ブループリント定義の名前
- `--input-path`: blueprint.json,artifactが格納されたディレクトリパス

このコマンド実行後にAzureポータルを見ると、ブループリント定義が作成されています。
![ブループリント定義が作成されている.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/2478eb1f-2259-c280-0833-49fcb044255c.png)


### ブループリントの発行

ブループリント定義を作成しただけでは、割り当て（リソースグループの作成）をすることはできません。

バージョンを付けて発行する必要があります。

これはコマンド一発で実施します。

```bash
az blueprint publish --blueprint-name SandboxBluePrint --version 1.0 --subscription <サブスクリプションID>
```

- `--blueprint-name`: 対象のブループリント定義。
- `--version`: バージョン番号。1.0やv1など、わかりやすい形にする。

Aazureポータルで確認すると**「最新のバージョン」**が更新され、割り当て可能になりました。
![ブループリントの最新バージョンが更新された.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/9f1584fd-0c0a-a8ed-df85-1c9066c293ba.png)


これでブループリント定義の下準備は完了です。

### OIDCの設定

GitHub Actions経由でAzure ADグループの作成、ブループリント割り当てを実行するには権限設定が必要です。

まず[Azure での OpenID Connect の構成](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure)に従って、Azure上にエンタープライズアプリケーションを登録します。

追加する資格情報は、`エンティティ型`を**「ブランチ」**、`GitHub ブランチ名`を**「main」**としました。これでmainブランチからのワークフローに対してのみ、Azure上の権限が利用可能になります。
![資格情報の追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/14e432b4-e390-6154-741c-4b60b4a1a39b.png)

次にこのアプリケーションに対してAzureBluePrintの操作とAzure ADグループ作成が可能なロールを割り当てます。AzureロールとAzure ADロールそれぞれで登録が必要です。

#### Azure ADロール
Azure Active Directoryの「ロールと管理者」メニューから、先ほど作成したアプリケーションに対して以下のロールを割り当てます。

- ユーザー管理者

#### Azureロール
ブループリント定義を作成したサブスクリプションの「アクセス制御（IAM）」メニューから、先ほど作成したアプリケーションに対して以下のロールを割り当てます。

- ブループリントオペレーター
- ユーザーアクセス管理者



注意点としてロールの割り当て方法はユーザーやグループに割り当てるのと同じなのですが、画面上で割り当て対象を検索する際に**エンタープライズアプリケーションは初期表示されません。検索ボックスに名前を入力すると現れます。**
![初期表示ではエンタープライズアプリケーションは表示されない.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/507f04f4-7eab-61bb-2779-daf9a15e0bf6.png)![名前を入力すると表示される.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/c34c11e3-68d1-b6fd-0394-21381bf27b1a.png)

### GitHub Actionsの作成

最後にGitHub Actionsのワークフローを作成します。

今回は手動トリガーとするので`on:`で**workflow_dispatch**を指定して、入力パラメータを定義します。
```yaml
on:
  workflow_dispatch:
    inputs:
      rg-name:
        type: string
        description: 'Resource Group Name'
        required: true
      rg-region:
        type: choice
        description: 'Resource Group Region'
        required: true
        default: 'japaneast'
        options:
          - 'japaneast'
          - 'japanwest'
      owner:
        type: string
        description: 'Owner UPN'
        required: true
      department:
        type: string
        description: 'Department'
        required: true
```

Azureへのログインは[azure/login]()を利用しますが、先ほどのOIDCでエンティティをブランチとしたため、`branch`属性を指定しています。

```yaml
      - name: 'Az CLI login'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          branch: ${{ github.ref_name }}
```

Azure CLIで`az blueprint`を使うためにはエクステンションの設定が必要なので、あらかじめインストールしておきます。

```yaml
      - name: 'install az cli extension for blueprint'
        run: |
          az extension add --name blueprint
```

リソースグループの所有者指定のため先にAzureのセキュリティグループを作成し、入力パラメータで受け取ったUPNをアサインします。次のステップでグループのUPNが必要になるので環境変数に吐き出しておきます。

```bash
          GROUP_ID=$(az ad group create --display-name ${GROUP_NAME} --mail-nickname ${GROUP_NAME} --query id -o tsv)
          OWNER_ID=$(az ad user show --id ${{ github.event.inputs.owner }} --query id -o tsv)
          az ad group member add --group $GROUP_ID --member-id $OWNER_ID
          echo "GROUP_ID=$GROUP_ID" >> $GITHUB_ENV
```

ブループリント定義に動的パラメータがある場合には割り当て実行時にパラメーターファイルを指定する必要があるため、作成します。具体的にはARMテンプレートで**parameters()** 関数を利用している場合は設定が必須です。

ただし許可されるリソースの場所を示す`listOfAllowedlocations`はデフォルト値があるため指定を省略しています。


フォーマットはJSON形式で、
```json
"ARM内のパラメータ名": {
    "value": "設定値"
}
```
となります。

GitHub Actionsのワークフロー内ではechoコマンドを使って一時的なファイルに出力します。
```bash
          echo "{
            \"resourceGroupName\": {
              \"value\": \"${{ github.event.inputs.rg-name }}\"
            },
            \"ownerPrincipalId\": {
              \"value\": [
                \"${GROUP_ID}\"
              ]
            },
            \"tagValueOfDepartment\": {
              \"value\": \"${{ github.event.inputs.department }}\"
            }
          }" > ./parameters.json

```

最後に`az blueprint assignment`で割り当てを実行します。

```bash
az blueprint assignment create --name assignment-${{ github.event.inputs.rg-name }} --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} --identity-type SystemAssigned --location ${{ github.event.inputs.rg-region }} --resource-group-value artifact_name=sandbox-rg location=${{ github.event.inputs.rg-region }} --parameters ./parameters.json --blueprint-version /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/providers/Microsoft.Blueprint/blueprints/SandboxBluePrint

```

- `--name`: 割り当て名。ブループリント定義ごとに一意とする必要がある。
- `--resource-group-value`: リソースグループごとに作成場所や動的パラメータの値を指定する。
- `--bluprint-version`: ブループリント定義のバージョンを示すID。バージョンを省略した場合は最新のバージョンが利用される。


全体は以下のとおりです。
```yaml:.github/workflows/main.yaml
permissions:
  id-token: write
  contents: read
on:
  workflow_dispatch:
    inputs:
      rg-name:
        type: string
        description: 'Resource Group Name'
        required: true
      rg-region:
        type: choice
        description: 'Resource Group Region'
        required: true
        default: 'japaneast'
        options:
          - 'japaneast'
          - 'japanwest'
      owner:
        type: string
        description: 'Owner UPN'
        required: true
      department:
        type: string
        description: 'Department'
        required: true
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 'Az CLI login'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          branch: ${{ github.ref_name }}
      - name: 'install az cli extension for blueprint'
        run: |
          az extension add --name blueprint
      - name: 'create security gorup of resource group owner'
        run: |
          # オーナーグループの作成
          GROUP_NAME=sandbox-${{ github.event.inputs.rg-name }}-rg-owner
          GROUP_ID=$(az ad group create --display-name ${GROUP_NAME} --mail-nickname ${GROUP_NAME} --query id -o tsv)
          # オーナーユーザーのObjectIDを取得
          OWNER_ID=$(az ad user show --id ${{ github.event.inputs.owner }} --query id -o tsv)
          # オーナーグループにオーナーユーザーを追加
          az ad group member add --group $GROUP_ID --member-id $OWNER_ID
          # GROUP_IDを環境変数にセット
          echo "GROUP_ID=$GROUP_ID" >> $GITHUB_ENV

      - name: 'create parameters file'
        run: |
          # ブループリントのパラメータファイルを作成
          echo "{
            \"resourceGroupName\": {
              \"value\": \"${{ github.event.inputs.rg-name }}\"
            },
            \"ownerPrincipalId\": {
              \"value\": [
                \"${GROUP_ID}\"
              ]
            },
            \"tagValueOfDepartment\": {
              \"value\": \"${{ github.event.inputs.department }}\"
            }
          }" > ./parameters.json

      - name: 'create blueprint assignment'
        run: |
          # ブループリントの割り当て
          az blueprint assignment create --name assignment-${{ github.event.inputs.rg-name }} --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} --identity-type SystemAssigned --location ${{ github.event.inputs.rg-region }} --resource-group-value artifact_name=sandbox-rg location=${{ github.event.inputs.rg-region }} --parameters ./parameters.json --blueprint-version /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/providers/Microsoft.Blueprint/blueprints/SandboxBluePrint

```

## 実行
では実行してみます。

GitHub Actionsのmainブランチからワークフローを実行するとパラメータが入力できます。
![GitHubActionsの実行.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/5b879da6-ac4a-b1cb-cc52-b47a9188cbc9.png)


成功しました。
![GitHubActionsの実行結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/6b6c2c18-f809-4611-74b1-fed755251775.png)


Azureポータルから確認すると、新しくブループリントの割り当てが作成されています。
![割り当てられたブループリント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/6fc49feb-2e72-d661-5b7d-808d9838cf8c.png)


リソースグループ`sandbox-demo-rg`とその所有者グループ`sandbox-demo-rg-owner`が作成されています。
![作成されたリソースグループとセキュリティグループ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/d46ad371-74c7-2a04-b274-025c9fa7b96b.png)

![リソースグループの役割.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/86357b5c-1490-7804-aded-2664b961ffe1.png)


ポリシー周りの設定も見てみます。

リソースグループのタグ`Department`にはワークフロー実行時に入力した**Finance**が設定されています。
![リソースグループのタグ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/90e82fac-c090-8cf5-b711-c42a542cb314.png)

リソースを作成する場所はデフォルトで許可している**Japan East、Japan West**以外はエラーになります。
![スクリーンショット 2023-07-16 0.23.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/29c4780b-85d1-1985-f87d-ca7832362766.png)



以上、GitHub Actionsのパラメータを利用してAzure BluePrintによるリソースグループデプロイまでの仕組みを実装してみました。

管理者がAzureポータルからポチポチやることもできますが、この方法なら利用者に権限移譲したうえで安全に制限されたリソースグループを提供することができるのではないでしょうか。

もちろんワークフローはGitHubActions以外でも良いですし、ブループリント割り当て前に管理者レビューを入れるなどの仕組みを取り入れるのも有りだと思います。

Azure Blueprintは **「永遠のプレビュー版」[^1]** というのもあり若干心配な部分もありますが、非常に有用な機能だと思うので活用していきたいですね。

## 参考
https://learn.microsoft.com/ja-jp/azure/governance/blueprints/create-blueprint-azurecli

https://learn.microsoft.com/ja-jp/cli/azure/blueprint?view=azure-cli-latest

https://github.com/Azure/azure-blueprints/blob/master/README.md

[^1]:https://licensecounter.jp/azure/blog/topics/azure-blueprints.html