Databricks Terraform Provider

TerraformによるDatabricksのアクセス制御管理

Terraformの概要

  • Databricks Providerを利用する前にTerraformのアーキテクチャについて簡単に触れておく
    • パフォーマンスやリソース間の依存関係などの観点から、Terraform自体が実態のリソースの状態を把握するために状態を管理するためのファイル(tfstate)を作成する
    • tfstateは、デフォルトではterraform.tfstateというファイル名でローカルに保存する。s3などにリモートデータストアで管理することで、他ユーザーと共有することができる
    • terraform plan を実行すると、tfstateとソースコードで記述した定義との差分を表示する
    • terraform apply を実行すると実際にリソースに対してplanで表示した差分を適用し、リソースに関連する情報が tfstate に保存される
    • terraform destroy を実行するとリソースを削除し、 tfstate の内容も削除される
    • terraform import を実行すると既に構築済みの既存のリソースをTerraformの管理にするためtfstateへリソースに関する情報が保存される

本稿では、DatabricksのCatalogは既に作成済として、terraform importコマンドで取り込んで管理することにする。CatalogをTerraformで作成することで、自動的にtfstateで管理できるようになるが、CatalogをTerraformで作成するには、その上位のUnityCatalogとメタストアもTerraformの管理下に置く必要がある

Databricks Providerのセットアップ

次のいずれかの方法でDatabricksのリソースにアクセスするためのセットアップを行う

  • Databricks CLI資格情報によるセットアップ
    • databricks configure --tokenで作成された資格情報ファイル(デフォルトは~/.databrickscfg)を利用する
provider "databricks" {
  alias       = "mws"
  config_file = "C:/Users/sugah/.databrickscfg"
}
  • ホスト名とパーソナルアクセストークン(PAT)によるセットアップ
    • ワークスペースのURLとPATを利用する
provider "databricks" {
  host  = "http://abc-cdef-ghi.cloud.databricks.com"
  token = "dapitokenhere"
}
  • ホスト名、ユーザー名、パスワードによるセットアップ
    • アカウントレベルのユーザーの資格情報を利用するため、ワークスペースの作成などのアカウントレベルの操作を行う場合に利用する
provider "databricks" {
  alias    = "mws"
  host     = "https://accounts.cloud.databricks.com"
  username = var.databricks_account_username
  password = var.databricks_account_password
}

Databricks ワークスペースの作成

  • Databricksのワークスペースを作成するためには、VPCなどのネットワーク設定、Databricks社とのクロスアカウントロール資格情報、ルートバケットとストレージ資格情報を構築する必要がある
  • まず最初にDatabricks Providerをアカウントレベルのユーザーの資格情報を利用してセットアップする
    • 次のような機密情報を管理するファイル(ここではデフォルトの名前でterraform.tfvars作成する)
databricks_account_id="a4xxxxxx-xxxx-xxxx-xxxx-xx9999931x3"
databricks_account_username="xxxx@xxxxxxxxxx"
databricks_account_password="xxxxxxxxxx"
  • 外部パラメータとデフォルトのパラメータ情報を管理するvariables.tfファイルを作成する
variable "databricks_account_id" {}
variable "databricks_account_username" {}
variable "databricks_account_password" {}

variable "cidr_block" {
  default = "10.4.0.0/16"
}

variable "region" {
  default = "ap-northeast-1"
}

locals {
    prefix = "data-domain"
}
  • providers.tfファイルを作成する
    • tfstateを管理するs3バケットは事前に作成済とする(cluetechnologies-terraform-state
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.64.0"
    }
    databricks = {
      source  = "databricks/databricks"
    }
  }

  backend "s3" {
    bucket = "cluetechnologies-terraform-state"
    key    = "databricks/terraform.tfstate"
    encrypt = true
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
        env = "databricks-terraform-poc"
    }
  }
}

provider "databricks" {
  alias      = "mws"
  host       = "https://accounts.cloud.databricks.com"
  account_id = var.databricks_account_id
  username   = var.databricks_account_username
  password   = var.databricks_account_password
}
data "aws_availability_zones" "available" {}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.70.0"

  name = local.prefix
  cidr = var.cidr_block
  azs  = data.aws_availability_zones.available.names

  enable_dns_hostnames = true
  enable_nat_gateway   = true
  create_igw           = true

  public_subnets  = [cidrsubnet(var.cidr_block, 3, 0)]
  private_subnets = [cidrsubnet(var.cidr_block, 3, 1),
                     cidrsubnet(var.cidr_block, 3, 2)]

  default_security_group_egress = [{
    cidr_blocks = "0.0.0.0/0"
  }]

  default_security_group_ingress = [{
    description = "Allow all internal TCP and UDP"
    self        = true
  }]
}

resource "databricks_mws_networks" "this" {
  provider           = databricks.mws
  account_id         = var.databricks_account_id
  network_name       = "${local.prefix}-network"
  security_group_ids = [module.vpc.default_security_group_id]
  subnet_ids         = module.vpc.private_subnets
  vpc_id             = module.vpc.vpc_id
}
  • ルートバケットの作成
    • DBFSワークスペースストレージ用のs3バケットを作成する。このプロバイダーには、必要な IAM ポリシー テンプレートを含むdatabricks_aws_bucket_policyがあります
    • strage_config.tf
resource "aws_s3_bucket" "root_storage_bucket" {
  bucket = "cluetechnologies-databirkcs-${local.prefix}-rootbucket"
  force_destroy = true
}

data "databricks_aws_bucket_policy" "this" {
  bucket = aws_s3_bucket.root_storage_bucket.bucket
}

resource "aws_s3_bucket_policy" "root_bucket_policy" {
  bucket = aws_s3_bucket.root_storage_bucket.id
  policy = data.databricks_aws_bucket_policy.this.json
}

resource "databricks_mws_storage_configurations" "this" {
  provider                   = databricks.mws
  account_id                 = var.databricks_account_id
  bucket_name                = aws_s3_bucket.root_storage_bucket.bucket
  storage_configuration_name = "${local.prefix}-storage"
}
  • クロスアカウントIAMロールを作成する
    • DatabricksへAWSアカウント内で必要なアクションを許可する
    • credentials.tf

data "databricks_aws_assume_role_policy" "this" {
  external_id = var.databricks_account_id
}

resource "aws_iam_role" "cross_account_role" {
  name               = "${local.prefix}-crossaccount"
  assume_role_policy = data.databricks_aws_assume_role_policy.this.json
  description        = "Grants Databricks full access to VPC resources"
}

data "databricks_aws_crossaccount_policy" "this" {}

resource "aws_iam_role_policy" "this" {
  name   = "${local.prefix}-policy"
  role   = aws_iam_role.cross_account_role.id
  policy = data.databricks_aws_crossaccount_policy.this.json
}

resource "databricks_mws_credentials" "this" {
  provider         = databricks.mws
  account_id       = var.databricks_account_id
  role_arn         = aws_iam_role.cross_account_role.arn
  credentials_name = "${local.prefix}-creds"
  depends_on       = [time_sleep.wait]
}

resource "time_sleep" "wait" {
  depends_on = [
    aws_iam_role.cross_account_role,
    aws_iam_role_policy.this
  ]
  create_duration = "10s"
}
  • E2 ワークスペースを作成する。作成したワークスペースURLを表示しています
    • workspace.tf
resource "databricks_mws_workspaces" "this" {
  depends_on      = [
    databricks_mws_credentials.this,
    databricks_mws_storage_configurations.this,
    databricks_mws_networks.this
  ]
  provider        = databricks.mws
  account_id      = var.databricks_account_id
  aws_region      = var.region
  workspace_name  = local.prefix
  deployment_name = local.prefix

  credentials_id           = databricks_mws_credentials.this.credentials_id
  storage_configuration_id = databricks_mws_storage_configurations.this.storage_configuration_id
  network_id               = databricks_mws_networks.this.network_id
}

// export host to be used by other modules
output "databricks_host" {
  value = databricks_mws_workspaces.this.workspace_url
}
  • Terraformの実行
terraform init
terraform plan
terraform apply
  • 作成したリソースの一覧を確認する
terraform state list

data.aws_availability_zones.available
data.databricks_aws_assume_role_policy.this
・・・省略
module.vpc.aws_subnet.public[0]
module.vpc.aws_vpc.this[0]

ワークスペースの管理

  • ワークスペースを作成するコードとワークスペースを管理するコードとの間で混同を避けるために、別の Terraform モジュールに配置する
  • 以下の手順ではワークスペースを管理するためのサービスプリンシパルの認証トークンで作業を行う

サービスプリンシパルと認証トークンの作成

  • Terraform用サービスプリンシパルを作成し、ワークスペースへ割り当てた後、認証トークンを作成する
    • service-principal.tf
provider "databricks" {
  alias = "created_workspace"
  host = databricks_mws_workspaces.this.workspace_url
  account_id = var.databricks_account_id
  username   = var.databricks_account_username
  password   = var.databricks_account_password
}

# アカウントレベルのサービスプリンシパルを作成する
resource "databricks_service_principal" "terraform" {
  provider     = databricks.mws
  display_name = "terraform"
}

# サービスプリンシパルをワークスペースに割り当てる
resource "databricks_mws_permission_assignment" "add_terraform" {
  provider     = databricks.mws
  workspace_id = databricks_mws_workspaces.this.workspace_id
  principal_id = databricks_service_principal.terraform.id
  permissions  = ["ADMIN"]
}

# サービスプリンシパルにトークン利用権限を付与する
resource "databricks_permissions" "token_usage" {
  provider      = databricks.created_workspace
  authorization = "tokens"
  access_control {
    service_principal_name = databricks_service_principal.terraform.application_id
    permission_level       = "CAN_USE"
  }
}

# サービスプリンシパルに管理権限を追加
resource "databricks_entitlements" "terraform" {
  provider                   = databricks.created_workspace
  service_principal_id       = databricks_service_principal.terraform.id
  allow_cluster_create       = true
  allow_instance_pool_create = true
  databricks_sql_access      = true
  workspace_access           = true
}

# サービスプリンシパルの認証トークンを作成する
resource "databricks_obo_token" "this" {
  provider         = databricks.created_workspace
  depends_on       = [databricks_permissions.token_usage]
  application_id   = databricks_service_principal.terraform.application_id
  comment          = "PAT on behalf of ${databricks_service_principal.terraform.display_name}"
  lifetime_seconds = 3600
}

# 認証トークンを出力する
output "obo_token" {
  value     = databricks_obo_token.this.token_value
  sensitive = true
}

発行した認証トークンを確認する

  • Terraformでは機密情報(sensitive)は結果のJsonから非表示となるが、以下のコマンドで機密情報(認証トークン)を表示させることができる
terraform output -raw obo_token
dapi99999999999999999999999999999999

ワークスペース管理用のプロバイダー

  • 認証用にワークスペースURLとサービスプリンシパルのトークンを設定するためterraform.tfvarsファイルを作成する
databricks_account_host="xxx-xxx.cloud.databricks.com"
databricks_account_token="dapi99999999999999999999999999999999"
  • ワークスペース用のプロジェクトフォルダを作成し、providers.tfを作成する
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.64.0"
    }
    databricks = {
      source  = "databricks/databricks"
    }
  }

  backend "s3" {
    bucket = "cluetechnologies-terraform-state"
    key    = "databricks/terraform.tfstate"
    encrypt = true
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
        env = "databricks-terraform-poc"
    }
  }
}

# サービスプリンシパルでDatabricksプロバイダーを初期化する
provider "databricks" {
  alias      = "workspace"
  host       = var.databricks_host
  token      = var.databricks_token
}

# デフォルトパラメータ
data "databricks_current_user" "me" {}
data "databricks_spark_version" "latest" {}
data "databricks_node_type" "smallest" {
  local_disk = true
}

UnityCatalogをTerraformで管理する

  • 前回紹介したDatabricksのアクセス制御設定の管理者用グループと公開用グループに対してUnityCagalogのカタログとスキーマの権限設定を行う
  • UnityCatalogのカタログをTerraformで管理するためにいくつか選択肢がある
    • resource "databricks_catalog"でTerraformからカタログを新規作成する
    • terraform import databricks_catalog.<cagalog> <name>でTerraformに既存のカタログをインポートする
  • 前者はUnityCatalogのメタストアが管理されていないと作成できないため、UnityCatalogのメタストアがTerraformの管理下にない場合は後者を選択する必要がある。
  • 本手順では、既存のカタログをインポートする手順を示す。Databricksコンソールからカタログを作成する
  • インポート先の空のカタログ定義を作成しておく。catalog.tf
resource "databricks_catalog" "domain" {
}
  • 既存のカタログをTerraformにインポートする
terraform import databricks_catalog.domain domain
databricks_catalog.domain: Importing from ID "domain"...
・・・略
Import successful!
  • tfstateにリソース情報が反映されていることを確認する
terraform state show databricks_catalog.domain

# databricks_catalog.domain:
resource "databricks_catalog" "domain" {
    id           = "domain"
    metastore_id = "6634edc1-c4b5-4111-9b6a-9d8c4f564a96"
    name         = "domain"
    owner        = "68f36ce3-4810-4f0c-8f6f-00e954f439ae"
}
  • インポート後、カタログの定義を編集する
resource "databricks_catalog" "domain" {
    name         = "domain"
}
  • 定義内容に反映されていることを確認する。No changes.が出力されていれば反映されている
>terraform plan  
・・・略
No changes. Your infrastructure matches the configuration.
  • domainカタログのALL_PRIVILEGES権限をadmin_groupに付与する
    • catalog_grant.tf
data "databricks_group" "catalog_admins" {
  display_name = "admin_group"
}

resource "databricks_grants" "catalog" {
  depends_on = [ databricks_catalog.domain ]
  catalog    = databricks_catalog.domain.name
  grant {
    principal  = data.databricks_group.catalog_admins.display_name
    privileges = [ "ALL_PRIVILEGES" ]
  }
}

スキーマを作成

  • データ公開用のスキーマ(product_data)を作成する
    • schema.tf
resource "databricks_schema" "schema_public" {
  provider     = databricks.workspace
  catalog_name = databricks_catalog.domain.id
  name         = "product_data"
  comment      = "this database is managed by terraform"
}
  • product_dataスキーマの参照権限(SELECT)をpublic_groupに付与する
    • schema_grant.tf
data "databricks_group" "schema_public" {
  display_name = "public_group"
}

resource "databricks_grants" "schema_public" {
  depends_on = [ databricks_schema.schema_public ]
  schema = "${databricks_catalog.domain.name}.${databricks_schema.schema_public.name}"
  grant {
    principal  = data.databricks_group.schema_public.display_name
    privileges = [ "SELECT" ]
  }
}

ストレージ資格情報と外部ロケーションを作成

  • ストレージ資格情報の作成に必要なIAMロールを作成して、ストレージ資格情報と外部ロケーションを作成する
variable "external_storage_label" {}
variable "external_storage_location_label" {}

data "aws_iam_policy_document" "passrole_for_unity_catalog" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      identifiers = ["arn:aws:iam::414351767826:role/unity-catalog-prod-UCMasterRole-14S5ZJVKOTYTL"]
      type        = "AWS"
    }
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [var.databricks_account_id]
    }
  }
}

resource "aws_s3_bucket" "external" {
  bucket = "${local.prefix}-${var.external_storage_label}"
  acl    = "private"
  versioning {
    enabled = false
  }
  force_destroy = true
  tags = merge(local.tags, {
    Name = "${local.prefix}-${var.external_storage_label}"
  })
}

resource "aws_s3_bucket_public_access_block" "external" {
  bucket             = aws_s3_bucket.external.id
  ignore_public_acls = true
  depends_on         = [aws_s3_bucket.external]
}

resource "aws_iam_policy" "external_data_access" {
  policy = jsonencode({
    Version = "2012-10-17"
    Id      = "${aws_s3_bucket.external.id}-access"
    Statement = [
      {
        "Action" : [
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:PutObject",
          "s3:PutObjectAcl",
          "s3:DeleteObject",
          "s3:ListBucket",
          "s3:GetBucketLocation"
        ],
        "Resource" : [
          aws_s3_bucket.external.arn,
          "${aws_s3_bucket.external.arn}/*"
        ],
        "Effect" : "Allow"
      }
    ]
  })
  tags = merge(local.tags, {
    Name = "${local.prefix}-unity-catalog ${var.external_storage_label} access IAM policy"
  })
}

resource "aws_iam_role" "external_data_access" {
  name                = "${local.prefix}-external-access"
  assume_role_policy  = data.aws_iam_policy_document.passrole_for_unity_catalog.json
  managed_policy_arns = [aws_iam_policy.external_data_access.arn]
  tags = merge(local.tags, {
    Name = "${local.prefix}-unity-catalog ${var.external_storage_label} access IAM role"
  })
}

# ストレージ資格情報を作成
resource "databricks_storage_credential" "external" {
  name     = aws_iam_role.external_data_access.name
  aws_iam_role {
    role_arn = aws_iam_role.external_data_access.arn
  }
}

# 外部ロケーションを作成
resource "databricks_external_location" "raw" {
  name            = "${var.external_storage_label}"
  url             = "s3://${local.prefix}-${var.external_storage_label}/${var.external_storage_location_label}"
  credential_name = databricks_storage_credential.external.id
}
  • パラメータファイル external-storage.auto.tfvarsを作成する
external_storage_label          = "external"
external_storage_location_label = "raw"
  • 作成したストレージ資格情報と外部ロケーションのALL_PRIVILEGES権限をadmin_groupに付与する
locals {
    external_storage_admins_display_name = "admin_group"
    external_storage_privileges   = "ALL_PRIVILEGES"
}

data "databricks_group" "external_storage_admins" {
  display_name = local.external_storage_admins_display_name
}

# 管理者グループにストレージ資格情報の権限を付与
resource "databricks_grants" "external_storage_credential" {
  storage_credential = databricks_storage_credential.external.id
  grant {
    principal  = local.external_storage_admins_display_name
    privileges = [local.external_storage_privileges]
  }
}

# 管理者グループに外部ロケーションの権限を付与
resource "databricks_grants" "external_storage" {
  external_location = databricks_external_location.raw.id
  grant {
    principal  = local.external_storage_admins_display_name
    privileges = [local.external_storage_privileges]
  }
}

リソースの削除

  • ワークスペース内のリソースを削除する(ワークスペース管理用プロジェクトで実行する)
terraform destroy
  • カタログを削除するには、関連する情報も一括で削除しておく必要がある
    • 以下のコマンドでDatabricksのカタログを削除する
drop catalog domain cascade;