ARMテンプレートを使って Databricks + Unity Catalog を一括準備する

# はじめに
Microsoft Azure Databricks で、アクセス制御の為に Unity Catalog を使用する際、ワークスペースに加えてテーブルを保存するためのストレージなどが必要になります。ワークスペース間での分離が必要な場合、ワークスペースを作成するたびにこのセットをポータル上でその都度デプロイするのは手間がかかります。

そこで、今回は ARM (Azure Resource Management) テンプレートを作成し、 Unity Catalog を利用するために必要なリソースを一括作成する方法について調べました。

# 構築したい環境
![arm_resourcegroup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/ed8156c6-8780-83a0-4a7d-3c143a612f99.png)

1つのリソースグループ下に Databricks ワークスペースを1つ作成する。Unity Catalog を使用したいので、カタログの保存に必要なストレージと、ストレージに接続するためのアクセスコネクタも作成する。

作成するリソースは以下の通り。
- Databricks ワークスペース
- Databricks 用のアクセスコネクタ
- ストレージアカウント
  - Data Lake Storage Gen2 として設定する
  - Unity Catalog用に使うコンテナを一つ作成

また、Databricksをストレージに接続するために、アクセスコネクタにストレージに対する「BLOB データ共同作成者」のロールを与える必要がある。

# ARM テンプレートの構造
今回は、3つのリソースの構成をそれぞれ定義したテンプレートファイルを作成してから、リソースグループ全体を示す親テンプレート内で参照し、さらに親テンプレート内で権限の付与を行うという構成でテンプレートを作成します。

複数のリソースを同時にデプロイする場合、1つのファイルにそれぞれのリソースを記述する方法と別々のファイルに記述する方法がありますが、今回は後者を選びました。こちらはモジュール化によるテストのしやすさや再利用性の高さが魅力ですが、権限の設定などファイルを跨いだ依存関係を含む場合に少し記述する物が多くなります。

![arm_template.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/4eed3645-f82d-0dd0-c412-f0f643c5809b.png)

# 各リソースのテンプレートを作る
テンプレートを作る際には、すでにデプロイされているリソースからテンプレートをエクスポートして、それの必要な部分を書き換えるという方法がやりやすいと思いました。ただ、エクスポートしたデフォルトの状態では、リソース名など設定に融通を効かせたいプロパティがハードコーディングされているため、まずはそれらをパラメータや変数の形に修正する作業が必要になります。
![2023-08-30_11h58_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/a02e7805-6203-131a-9cab-393ebfa3bfff.png)

![2023-08-23_18h41_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/7607a3b7-3eb4-dda3-ed3e-5b37dfa16514.png)

## ワークスペース
必要なパラメータは名前とデプロイするリージョン、そしてプランの3つです。

ワークスペース名は親のテンプレートで指定します。

リージョンについては統一したいので`"[resourceGroup().location]"`として親のリソースグループを参照するようにします。他のリソースも同様です。

Unity Catalog を使用するため、プランはPremiumを指定します。

```json
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "workspaceName": {
            "type": "String",
            "minLength": 3,
            "maxLength": 24
        },
        "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]"
        },
        "sku": {
            "type": "string",
            "defaultValue": "premium"
        }
    },
    "variables": {
        "managedResourceGroupName": "[format('rg-{0}-{1}', parameters('workspaceName'), uniqueString(resourceGroup().id))]"
    },
    "resources": [
        {
            "type": "Microsoft.Databricks/workspaces",
            "apiVersion": "2023-02-01",
            "name": "[parameters('workspaceName')]",
            "location": "[parameters('location')]",
            "sku": {
                "name": "[parameters('sku')]"
            },
            "properties": {
                "managedResourceGroupId": "[concat(subscription().id, '/resourceGroups/', variables('managedResourceGroupName'))]"
            }
        }
    ]
}
```

## アクセスコネクタ
こちらは設定するパラメータは名前のみで、特筆すべき点は特にありません。

```json
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "connectorName": {
            "type": "String",
            "minLength": 3,
            "maxLength": 24
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Databricks/accessConnectors",
            "apiVersion": "2023-05-01",
            "name": "[parameters('connectorName')]",
            "location": "[resourceGroup().location]",
            "identity": {
                "type": "SystemAssigned"
            },
            "properties": {}
        }
    ]
}
```

ただし、この後で権限の付与を行う際に「出力」を追記することが必要となります。詳しくは後述。

## ストレージアカウント
ストレージアカウントの記述方法は少し複雑で、「ストレージアカウント＞BLOBサービス＞コンテナ」の3階層分を記述しなければいけません。

### Microsoft.Storage/storageAccounts
ここでは、ストレージの形式やプラン、その他ほとんどのプロパティを指定します。特に注意する点は Unity Catalog の要件である Data Lake Storage Gen2 を有効化するための設定をしなければならない点です。
- プラン:プレミアム
- 形式:ブロックBLOB
- 階層型名前空間:有効化

以上に関する部分が記述できているかを確認します。それ以外は、既存のものをエクスポートしたままでも問題ないかと思われます。

```json
{
  "sku": {
    "name": "Premium_ZRS",
    "tier": "Premium"
  },
  "kind": "BlockBlobStorage",
  "properties": {
    "isHnsEnabled": true
  }
}
```

### Microsoft.Storage/storageAccounts/blobServices
こちらで設定する項目はデータの削除期間（7日後まで復元可能など）に関するポリシーくらいで、特筆すべき点はありません。

```json
{
    "type": "Microsoft.Storage/storageAccounts/blobServices",
    "apiVersion": "2022-09-01",
    "name": "[concat(parameters('storageAccountName'), '/default')]",
    "dependsOn": [
        "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
    ],
    "sku": {
        "name": "[parameters('skuName')]",
        "tier": "[parameters('skuTire')]"
    },
    "properties": {
        "containerDeleteRetentionPolicy": {
            "enabled": true,
            "days": 7
        },
        "cors": {
            "corsRules": []
        },
        "deleteRetentionPolicy": {
            "allowPermanentDelete": false,
            "enabled": true,
            "days": 7
        }
    }
}
```

### Microsoft.Storage/storageAccounts/blobServices/containers
ここではコンテナ名を指定します。名前を指定する際、blobserviceのものに付け加えるように設定しなければいけません。ここでは"/unitycatalog"という名称を直接繋げていますが、この部分をパラメータ化するのもありかもしれません。

アクセスコネクタ以外からの接続を避けたいのでパブリックアクセスは無効化します。

```json
{
    "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
    "apiVersion": "2022-09-01",
    "name": "[concat(parameters('storageAccountName'), '/default/unitycatalog')]",
    "dependsOn": [
        "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), 'default')]",
        "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
    ],
    "properties": {
        "publicAccess": "None"
    }
}
```
# 親テンプレートの作成
1つのテンプレートファイルに、作成した3つのリソースに関するテンプレートファイルをリンクしてまとめてデプロイできるようにします。各リソースのデプロイを`Microsoft.Resources/deployments`というリソースの形式で記述し、テンプレートのリンクを記述します。

リンクはインターネットからアクセス可能なURLの形式で指定する必要があるため、あらかじめ BLOB ストレージなどにアップロードしておく必要があります。

また、各リソースの名前もここでパラメータとして設定できるようにします。

```json
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageAccountName": {
            "type": "string",
            "defaultValue": "[concat('catalog', uniqueString(subscription().id))]"
        },
        "workspaceName": {
            "type": "string"
        },
        "accessConnectorName": {
            "type": "string"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2021-04-01",
            "name": "[concat('Deploy-',parameters('workspaceName'))]",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "https://<storageurl>/template_databricks.json",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "workspaceName": {
                        "value": "[parameters('workspaceName')]"
                    }
                }
            }
        },
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2021-04-01",
            "name": "[concat('Deploy-',parameters('storageAccountName'))]",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri":"https://<storageurl>/template_blobstorage.json",
                    "contentVersion":"1.0.0.0"
                },
                "parameters": {
                    "storageAccountName": {
                        "value": "[parameters('storageAccountName')]"
                    }
                }
            }
        },
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2021-04-01",
            "name": "[concat('Deploy-',parameters('accessConnectorName'))]",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri":"https://<storageurl>/template_accessconnector.json",
                    "contentVersion":"1.0.0.0"
                },
                "parameters": {
                    "connectorName": {
                        "value": "[parameters('accessConnectorName')]"
                    }
                }
            }
        }
    ]
}
```

注意すべき点として、ストレージアカウントの名前は Azure 全体で一意である必要があるため、文字列をハッシュ化する`uniqueString()`関数で、リソースグループ固有の名前を生成します。

# 権限の設定を行う
次に、アクセスコネクタをストレージに接続するために必要な権限を付与するために、ロールの割り当てをデプロイするテンプレートを作成し、親テンプレートのファイルにリソースとして加えます。

必要なロールを整理すると、
- ロール：BLOBデータ共同作成者
- スコープ：ストレージアカウント
- プリンシパル：アクセスコネクタ

## デプロイ名
ロール付与のデプロイ名はGUIDと呼ばれる形式のIDを付ける必要があり、これ以外で独自に付けようとした場合エラーになります。ここはあまり難しいことを考えず、`newGuid()`関数を使ってランダムに生成するのが確実です。

## スコープの設定
スコープには、アクセス権限を与えるリソースの名前を記述します。注意したいのが、リソースのタイプを **デプロイ名に** 繋げて記述する必要があるという点です。

リソースのファイルではストレージの名前の先頭に"Deploy-"をつなげる形で命名しましたが、この事を考えると素直にデプロイ名とリソース名は同じでも良かったのではないかと後で思いました。

```json
{
    "scope": "[concat('Microsoft.Storage/storageAccounts/', parameters('storageAccountName'))]"
}
```

## ロールID
割り当てたいロールをリソースIDの形で記述することになりますが、そのためにまずはロール固有のIDを[Azure組み込みロールの一覧](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles)から探します。「BLOBデータ共同作成者」は`ba92f5b4-2d11-453d-a403-e96b0029c9fe`です。

その後、`resourceId()`関数を用いてロールIDをリソースIDに変換します。

```json
"properties": {
    "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions','ba92f5b4-2d11-453d-a403-e96b0029c9fe')]",
}
```

## プリンシパルの設定と依存関係
ロールを割り当てる対象の決定には実際にデプロイされたアプリケーションの持つプリンシパルIDを指定する必要があります。よってアクセスコネクタがデプロイされるのを待ってからロール割り当てのデプロイを行う必要があります（複数リソースを記述した場合、通常は並行してデプロイ作業が行われます）。

デプロイの順番を指定するためには`dependsOn`属性に、依存するリソース（ここではアクセスコネクタ）のリソースIDを指定する必要があります。こちらはスコープと同じ記法になります。

```json
{
    "dependsOn": [
        "[resourceId('Microsoft.Resources/deployments', concat('Deploy-',parameters('accessConnectorName')))]"
    ],
}
```

次にプリンシパル ID の参照方法です。ID という用語がいくつも出てきてややこしいですが、ここで言うのは、実際にデプロイされた後に割り当てられる、オブジェクト ID とも表記されるものです。

プリンシパル ID を取得するためには、リソース ID を`reference()`関数に渡してリソースのプロパティオブジェクトを指定し、その中の`identity.principalId`の値を取得します。

```
reference(resourceId()).identity.principalId
```

パラメータとして、API バージョンと 'full' を追加できます。前者はテンプレートで記述したバージョンと一致してないとエラーになります（省略可）。後者は出力するパラメータを追加する設定であり、欲しい値が参照できない事を避けるために書いておいた方がいいと思います。

### 躓いた点
まず、プリンシパルIDを取得するために`reference(resourceId('Microsoft.Databricks/accessConnectors', parameters('accessConnectorName')),'2023-05-01', 'Full').identity.principalId`として記述しデプロイを実行してみたところ、

>{
>   "code":"ResourceNotFound",
>   "message":"The Resource 'Microsoft.Databricks/accessConnectors/<connectorname>' under resource group 'deploytest' was not found. For more details please go to https://aka.ms/ARMResourceNotFoundFix"
>}

と、なぜかデプロイ時にアクセスコネクタが存在しないという旨のエラーが発生しました。そこで、 dependsOn で設定したリソース ID との相違が原因と見て、リソースIDを`/deoloyments`に変更してみましたが、今度はリソースがテンプレートに定義されていないとのエラーが発生し、デプロイされたリソースを参照する必要があることには変わらないと分かりました。

ここでデプロイのログを見てみたところ、明らかに他のリソースのデプロイが完了する前にロール割り当てのデプロイを始めており、 **dependsOn の記述が機能していないのではないか？** と思ってしまいました。

https://bmoore-msft.blog/2020/07/26/resource-not-found-dependson-is-not-working/

いくつか情報を探してみたところ、上記の記事にて **refernce()関数はデプロイ開始時に評価されるため、依存関係による実行順を無視する場合がある** と記述されていました。また、この処理のタイミングを制御する方法として「`reference()`に API バージョンを記述しない」「`if()`文で囲む」ことでデプロイ側を強制的に呼び出す方法が紹介されていましたが、いずれも効果はありませんでした。

そこで別のアプローチとして、アクセスコネクタのデプロイに出力を設定してそれを参照すればデプロイまで処理を止められるのではないかと考え、テンプレートに出力を追加することにしました。

### 出力を追加
テンプレートに、アクセスコネクタのプリンシパルIDを出力するよう追記します。

```json
{
    "outputs": {
        "principalId": {
            "type": "string",
            "value": "[reference(resourceId('Microsoft.Databricks/accessConnectors', parameters('connectorName')),'2023-05-01', 'Full').identity.principalId]"
        }
    }
}
```

そして、親テンプレートで`principalId`の値をこの出力を参照する形式に書き換えます。

```json
"properties": {
    "principalId": "[reference(concat('Deploy-',parameters('accessConnectorName'))).outputs.principalId.value]"
}
```

出力を使う記法に変更するとデプロイが成功し、権限を付与することができました！

# おわりに
今回はARMテンプレートを利用して、Databricks のワークスペースと Unity Catalog を利用するために必要なリソースを一括でデプロイすることに挑戦しました。

ワークスペースは作成された状態ですが、まだクラスターなど環境の作成や、ワークスペース内で Unity Catalogを使用するための設定がまだできていません。今後はさらなる効率化のために Databricks API などを利用してワークスペース内の設定まで自動化する方法について調べたいと思います。

最後に、2023年9月14日に日本最大の Databricks イベント、 Data+AI World Tour Tokyo が開催されるようです。生成 AI にまつわる話題を中心に Databricks に関する最新情報が公開されるとのことなので、興味のある方はチェックしてみてください。

https://dbricks.co/3DFyQYB