Featured image of post Azure DatabricksワークスペースAPIをAPI Management経由で利用する

Azure DatabricksワークスペースAPIをAPI Management経由で利用する

Databricksきつい 触りたくない

まとめ

どうしてもDatabricksのワークスペースAPIをAPI Management経由で叩きたかったのだが、
設定が結構大変な上になぞりやすい公式のドキュメントが見つからなかったので
自分が設定できたときの手順を残しておく。

  • DatabricksワークスペースAPIをAPI Managementのバックエンドとして利用する際はカスタムURLが使える
  • API Managementに持たせる資格情報はDatabricksワークスペースのPATも利用可能だが、
    よりセキュアにやるならAPI Managementのauthentication-managed-identityポリシーでresource="2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"を指定する

前提条件

Databricks, API Managementは以下の設定で作成済みとする。

手順

バックエンドAPIを作成

まずはAPI ManagementからDatabricksへ接続するためのバックエンドAPI情報を設定する。

API Managementの管理画面から「APIs」→「Backends」→「+追加」と進み、
バックエンド(例: dbw-01)を作成する。
ランタイムURLにDatabricksワークスペースURLをぺたっと貼るだけでOK。
(資格情報は後ほど設定する)

次にAPI定義を作成する。
API Managementの管理画面から「APIs」→「API」→「+Add API」と進み、
API定義(例: dbw-01)を作成する。
名前とAPI URL suffix(例: dbw-01)を設定するだけでよい。
(Web service URLは設定せず、後ほどポリシー式で先ほど作成したバックエンドを指定する)

API定義が作成できたら、「Design」→「All operations」→「Design」から全操作(メソッド)共通のポリシー編集画面を開き、
先ほど作成したバックエンドを指定するset-backend-serviceポリシーを追加する。

<policies>
<inbound>
<!-- 作成済みのバックエンド「dbw-01」を指定する -->
<set-backend-service backend-id="dbw-01" />
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
view raw policy.xml hosted with ❤ by GitHub

また、リクエストを受信しバックエンドに流せるようにoperationを追加する。
Azure Databricks REST API reference等を参考に必要なAPI操作を定義するのがよさげ。
(OpenAPIで定義されたものがあればインポートできるんだけど、自分が探した限りだと見つけられなかった)

今回は簡単に確認したいだけなので、全てのGETリクエストをそのままのパスで流すワイルドカードを設定する。
(ドキュメントの注意事項にもあるように、本来は必要な操作だけをホワイトリスト的に定義した方が安全)

これでAPIを利用する準備ができたように見えるが(実際ほぼ終わり)、
資格情報を設定していないので試しにAPI Management越しにリクエストを送っても失敗する。

# 変数の設定
$ apimUrl="<API Managementの管理画面「概要」→「基本」→「ゲートウェイのURL」>" # 例: https://apim-name.azure-api.net
$ subscriptionKey="<API Managementの管理画面「APIs」→「サブスクリプション」から取得した主キー>"

# API Management越しにクラスター一覧APIを呼び出す(Databricks用のキーは付与せず、API Management用のキーのみ設定)
$ curl -H "Ocp-Apim-Subscription-Key: ${subscriptionKey}" ${apimUrl}/dbw-01/api/2.1/clusters/list

{"error_code":"401","message":"Unauthorized"}

API ManagementのマネージドIDに権限を付与

次はこの資格情報を設定していく。

参考: https://learn.microsoft.com/ja-jp/azure/databricks/dev-tools/azure-mi-auth

Databricks側でPAT発行してそれをAPI ManagementバックエンドのAuthorizationヘッダーに設定、でもいいんだけど、
PATってあんまり推奨されるものでもない(個人の権限ですべての操作が行われてしまう)のでできればマネージドIDでやりたい。

このため、まずはAPI Managementのシステム割り当てマネージドIDをDatabricksにサービスプリンシパルとして登録(ワークスペースへのアクセス権限を付与)する。
(ドキュメントだとユーザー割り当てマネージドIDが推奨されてるけど、やってみたらシステム割り当てでもできちゃった)

Databricksワークスペースで「設定」→「IDとアクセス」→「サービスプリンシパル」→「Service Principalを追加」からシステム割り当てマネージドIDを新規追加する。
記載するIDはMicrosoft Entra IDの管理画面から「管理」→「すべてのアプリケーション」から対象のマネージドIDの情報を開き、「アプリケーションID」の値を利用する。
(マネージドIDが見つけづらい場合はAPI Management管理画面(portal)の「セキュリティ」→「マネージドID」→「オブジェクト(プリンシパル)ID」の値を取得し、アプリケーション一覧画面で検索する。)

必要であれば特定の権限を持つグループに対象のサービスプリンシパルを追加しておくこと。
(今回は雑にadminに追加してしまっているが、本番運用の際はちゃんと専用のグループと権限を設定した方が良い)

これでAPI ManagementのマネージドIDでDatabricksワークスペースを操作する権限が設定できた。

API Managementのポリシー設定

マネージドIDに権限を付与できたので、
次はAPI Managementがアクセストークンを取得するためのauthentication-managed-identityポリシーを定義する。

resourceに何を指定すればよいのか迷うところだが、
curlでアクセストークンを取得する例を見る感じ2ff814a6-3304-4ab8-85cb-cd0e6f879c1d(DatabricksのプログラムID)でよさそう。
(正直ここに気付けなくて1日くらい溶かした)

<policies>
<inbound>
<!-- 作成済みのバックエンド「dbw-01」を指定する -->
<set-backend-service backend-id="dbw-01" />
<!-- Databricksに対してシステム割り当てマネージドIDで認証し、アクセストークンを取得 -->
<authentication-managed-identity resource="2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"/>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
view raw policy.xml hosted with ❤ by GitHub

ここまでできたら接続設定は完了。
バックエンドAPIの作成後に試した手順で再度リクエストを送ると、
今度は権限が付与されているので成功するようになっている。

$ curl -H "Ocp-Apim-Subscription-Key: ${subscriptionKey}" ${apimUrl}/dbw-01/api/2.1/clusters/list

{"next_page_token":"","prev_page_token":""} 
# クラスターを作成していないのでほぼ空っぽだが、APIリファレンス通りのレスポンスが返っているので動作確認は成功
# https://docs.databricks.com/api/azure/workspace/clusters/list

やったぜ。
これでAPI Managementにさえ疎通できればDatabricksワークスペースAPIが使えるようになった。

後は必要に応じてネットワークを閉域化したり、お好みで…

bicep

今回ブラウザでポチポチした操作をコード(bicep)化するとこんな感じ。↓↓↓

ネットワークまわりの設定で追加したところがあり、
API Managementは外部モードでデプロイして、
Databricksにはプライベートエンドポイント経由で接続するようにした。

また、Databricksワークスペースの細かい設定(IPアクセスリストだったりサービスプリンシパル)はbicepでは書けないっぽいので(Terraformだといけるかも?)、
手動で実施する必要がある。

@description('リージョン')
param region string = 'japaneast'
@description('VNet(1つめ)の名前')
param vnet01name string = 'vnet-01'
@description('VNet(2つめ)の名前')
param vnet02name string = 'vnet-02'
@description('VNet(1つめ)のアドレス空間')
param vnet01addressPrefix string = '10.1.0.0/16'
@description('VNet(2つめ)のアドレス空間')
param vnet02addressPrefix string = '10.2.0.0/16'
@description('VNet(1つめ)のサブネット(Databricks workspaceのプライベートエンドポイント)のアドレス空間')
param vnet01snetPepDbwAddressPrefix string = '10.1.1.0/24'
@description('VNet(2つめ)のサブネット(API ManagementのVNetデプロイ)のアドレス空間')
param vnet02snetApimAddressPrefix string = '10.2.1.0/24'
@description('Databricks workspaceの名前')
param dbw01name string = 'dbw-01'
@description('Databricks workspaceのカスタムサブネット名(public)')
param dbw01PublicSubnetName string = 'snet-dbw-01-public'
@description('Databricks workspaceのカスタムサブネット名(private)')
param dbw01PrivateSubnetName string = 'snet-dbw-01-private'
@description('Databricks workspaceのプライベートエンドポイントサブネット名')
param dbw01pepSubnetName string = 'snet-pep-dbw-01'
@description('API ManagementのVNetデプロイ用サブネット名')
param snetApim01name string = 'snet-apim'
@description('Databricks workspaceのカスタムサブネット(public)のアドレス空間')
param dbw01PublicSubnetAddressPrefix string = '10.1.2.0/24'
@description('Databricks workspaceのカスタムサブネット(private)のアドレス空間')
param dbw01PrivateSubnetAddressPrefix string = '10.1.3.0/24'
@description('Databricks workspaceのカスタムサブネットに割り当てるNSG名')
param dbw01subnetNsgName string = 'nsg-dbw-01'
@description('API Managementの名前')
param apim01name string = 'apim-uzimihsr-01'
// Databricks workspace用VNet
resource vnet01 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: vnet01name
location: region
properties: {
addressSpace: {
addressPrefixes: [
vnet01addressPrefix
]
}
subnets: [
{
// Databricks workspaceのプライベートエンドポイント用サブネット
name: dbw01pepSubnetName
properties: {
addressPrefix: vnet01snetPepDbwAddressPrefix
}
}
{
// Databricks workspaceのパブリックサブネット
name: dbw01PublicSubnetName
properties: {
addressPrefix: dbw01PublicSubnetAddressPrefix
networkSecurityGroup: {
id: dbw01subnetNsg.id
}
delegations: [
{
name: 'databricks-del-public'
properties: {
serviceName: 'Microsoft.Databricks/workspaces'
}
}
]
}
}
{
// Databricks workspaceのプライベートサブネット
name: dbw01PrivateSubnetName
properties: {
addressPrefix: dbw01PrivateSubnetAddressPrefix
networkSecurityGroup: {
id: dbw01subnetNsg.id
}
delegations: [
{
name: 'databricks-del-private'
properties: {
serviceName: 'Microsoft.Databricks/workspaces'
}
}
]
}
}
]
virtualNetworkPeerings: []
}
}
// API Management用VNet
resource vnet02 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: vnet02name
location: region
properties: {
addressSpace: {
addressPrefixes: [
vnet02addressPrefix
]
}
subnets: [
{
// API Managementの外部モード用サブネット
name: snetApim01name
properties: {
addressPrefix: vnet02snetApimAddressPrefix
networkSecurityGroup: {
id: nsgApim01.id
}
}
}
]
virtualNetworkPeerings: []
}
}
// VNet01からVNet02に疎通するためのピアリング設定
resource vnetPeering01 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-03-01' = {
parent: vnet01
name: 'vnet01-vnet02'
properties: {
allowVirtualNetworkAccess: true
remoteVirtualNetwork: {
id: vnet02.id
}
}
}
// VNet02からVNet01に疎通するためのピアリング設定
resource vnetPeering02 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-03-01' = {
parent: vnet02
name: 'vnet02-vnet01'
properties: {
allowVirtualNetworkAccess: true
remoteVirtualNetwork: {
id: vnet01.id
}
}
}
// Databricks workspaceのプライベート/パブリックサブネット用のNSG
// https://learn.microsoft.com/ja-jp/azure/databricks/security/network/classic/vnet-inject#network-security-group-rules-for-workspaces
resource dbw01subnetNsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: dbw01subnetNsgName
location: region
properties: {
securityRules: [
{
name: 'databricks-worker-to-worker-inbound'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 100
direction: 'Inbound'
}
}
{
name: 'databricks-control-plane-to-worker-ssh'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '22'
sourceAddressPrefix: 'AzureDatabricks'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 101
direction: 'Inbound'
}
}
{
name: 'databricks-control-plane-to-worker-proxy'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '5557'
sourceAddressPrefix: 'AzureDatabricks'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 102
direction: 'Inbound'
}
}
{
name: 'databricks-worker-to-worker-outbound'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 100
direction: 'Outbound'
description: 'Required for worker nodes communication within a cluster.'
}
}
{
name: 'databricks-worker-to-databricks-cp'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureDatabricks'
access: 'Allow'
priority: 101
direction: 'Outbound'
description: 'Required for workers communication with Databricks control plane.'
}
}
{
name: 'databricks-worker-to-sql'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3306'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'Sql'
access: 'Allow'
priority: 102
direction: 'Outbound'
description: 'Required for workers communication with Azure SQL services.'
}
}
{
name: 'databricks-worker-to-storage'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'Storage'
access: 'Allow'
priority: 103
direction: 'Outbound'
description: 'Required for workers communication with Azure Storage services.'
}
}
{
name: 'databricks-worker-to-eventhub'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '9093'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'EventHub'
access: 'Allow'
priority: 104
direction: 'Outbound'
description: 'Required for worker communication with Azure Eventhub services.'
}
}
]
}
}
// API Managementの外部モードで必要なNSG
// https://learn.microsoft.com/ja-jp/azure/api-management/api-management-using-with-vnet?tabs=stv2#configure-nsg-rules
resource nsgApim01 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: 'nsg-apim-01'
location: region
properties: {
securityRules: [
{
name: 'Client_communication_to_API_Management'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'Internet'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 100
direction: 'Inbound'
}
}
{
name: 'Management_endpoint_for_Azure_portal_and_Powershell'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3443'
sourceAddressPrefix: 'ApiManagement'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 110
direction: 'Inbound'
}
}
{
name: 'Azure_Infrastructure_Load_Balancer'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '6390'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 120
direction: 'Inbound'
}
}
{
name: 'Azure_Traffic_Manager_routing_for_multi-region_deployment'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'AzureTrafficManager'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 130
direction: 'Inbound'
}
}
{
name: 'Dependency_on_Azure_Storage_for_core_service_functionality'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'Storage'
access: 'Allow'
priority: 100
direction: 'Outbound'
}
}
{
name: 'Access_to_Azure_SQL_endpoints_for_core_service_functionality'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '1433'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'Sql'
access: 'Allow'
priority: 110
direction: 'Outbound'
}
}
{
name: 'Access_to_Azure_Key_Vault_for_core_service_functionality'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureKeyVault'
access: 'Allow'
priority: 120
direction: 'Outbound'
}
}
{
name: 'Publish_Diagnostics_Logs_and_Metrics_Resource_Health_and_Application_Insights'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRanges: [
'1886'
'443'
]
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureMonitor'
access: 'Allow'
priority: 130
direction: 'Outbound'
}
}
]
}
}
// Databricks workspace
resource dbw01 'Microsoft.Databricks/workspaces@2024-05-01' = {
name: dbw01name
location: region
sku: {
name: 'premium'
}
properties: {
managedResourceGroupId: '${subscription().id}/resourceGroups/managed-rg-${dbw01name}'
parameters: {
customVirtualNetworkId: {
value: vnet01.id
}
customPublicSubnetName: {
value: dbw01PublicSubnetName
}
customPrivateSubnetName: {
value: dbw01PrivateSubnetName
}
enableNoPublicIp: {
value: false
}
}
publicNetworkAccess: 'Enabled'
requiredNsgRules: 'AllRules'
}
// ワークスペース設定(IDとアクセスまわり)は現状bicepで設定できないため、以下の操作(サービスプリンシパルの追加)は手動で実施する:sob:
// https://learn.microsoft.com/ja-jp/azure/databricks/admin/users-groups/service-principals#add-a-service-principal-to-a-workspace-using-the-workspace-admin-settings
// サービスプリンシパルは"Microsoft Entra ID で管理",
// Microsoft EntraアプリケーションIDはAPI ManagementのマネージドIDの"アプリケーションID"(≠オブジェクトID or プリンシパルID)を指定すること
// (アプリケーションIDはMicrosoft Entra IDの「エンタープライズアプリケーション」の中から対象API ManagementのIDを選択して確認可能)
}
// privatelink.azuredatabricks.netのプライベートDNSゾーン
resource privateDnsZoneDbw 'Microsoft.Network/privateDnsZones@2024-06-01' = {
name: 'privatelink.azuredatabricks.net'
location: 'global'
resource privateNetworkLink01 'virtualNetworkLinks@2024-06-01' = {
name: 'vnet01'
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: {
id: vnet01.id
}
}
}
resource privateNetworkLink02 'virtualNetworkLinks@2024-06-01' = {
name: 'vnet02'
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: {
id: vnet02.id
}
}
}
}
// dbw-01のプライベートエンドポイント
resource privateEndpointDbw01 'Microsoft.Network/privateEndpoints@2024-03-01' = {
name: 'pep-dbw-01'
location: region
properties: {
privateLinkServiceConnections: [
{
name: 'pep-dbw-01'
properties: {
privateLinkServiceId: dbw01.id
groupIds: [
'databricks_ui_api'
]
}
}
]
subnet: {
id: '${vnet01.id}/subnets/${dbw01pepSubnetName}'
}
}
// IPアドレスを動的に決めるため、プライベートDNSゾーングループを作る
// https://learn.microsoft.com/ja-jp/azure/templates/microsoft.network/privateendpoints/privatednszonegroups?pivots=deployment-language-bicep
resource privateDnsZoneGroup 'privateDnsZoneGroups@2024-03-01' = {
name: 'privatednszonegroupdbw'
properties: {
privateDnsZoneConfigs: [
{
name: 'string'
properties: {
privateDnsZoneId: privateDnsZoneDbw.id
}
}
]
}
}
}
// API Managementで使用するポリシー式XML
// '''で囲めば複数行文字列を記載できるが、変数の評価ができないので...
// https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/data-types#multi-line-strings
// var policyXml = '''
// <policies>
// <inbound>
// <!-- 作成済みのバックエンド「${dbw01.name}」を指定する -->
// <set-backend-service backend-id="${dbw01.name}" />
// <!-- Databricksに対してシステム割り当てマネージドIDで認証し、アクセストークンを取得 -->
// <authentication-managed-identity resource="2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"/>
// <base />
// </inbound>
// <backend>
// <base />
// </backend>
// <outbound>
// <base />
// </outbound>
// <on-error>
// <base />
// </on-error>
// </policies>
// '''
var policyXml = '<policies><inbound><!-- 作成済みのバックエンド「${dbw01.name}」を指定する --><set-backend-service backend-id="${dbw01.name}" /><!-- Databricksに対してシステム割り当てマネージドIDで認証し、アクセストークンを取得 --><authentication-managed-identity resource="2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"/><base /></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'
// API Management
resource apim01 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
name: apim01name
location: region
sku: {
capacity: 1
name: 'Developer'
}
identity: {
type: 'SystemAssigned'
}
properties: {
publisherEmail: 'hogehoge@gmail.com'
publisherName: 'hogehoge'
virtualNetworkType: 'External'
virtualNetworkConfiguration: {
subnetResourceId: '${vnet02.id}/subnets/${snetApim01name}'
}
}
// Databricks workspace URLをBackendとして登録
resource backendDbw01 'backends@2023-09-01-preview' = {
name: dbw01.name
properties: {
protocol: 'http'
type: 'Single'
url: 'https://${dbw01.properties.workspaceUrl}/'
}
}
// API定義
resource apiDbw01 'apis@2023-09-01-preview' = {
name: dbw01.name
properties: {
displayName: dbw01.name
apiType: 'http'
protocols: [
'https'
]
path: dbw01.name
}
// ポリシー式
resource policyDbw01 'policies@2023-09-01-preview' = {
name: 'policy'
properties: {
format: 'xml'
value: policyXml
}
}
// GET /*
resource operationWildcardGet 'operations@2023-09-01-preview' = {
name: 'WildcardGet'
properties: {
displayName: 'WildcardGet'
method: 'GET'
urlTemplate: '/*'
}
}
}
}
view raw main.bicep hosted with ❤ by GitHub

おわり

Azure Databricksが難しすぎて日々泣いている。😭
ファーストパーティサービスと言いつつRBACはAzureロールと別物だし、サービスエンドポイントはないし…
同じように泣いている人の助けになれば幸い。

おまけ