SnowflakeでFunctional Role+Access Roleのロール設計を実現するTerraformのModule構成を考えてみた

2024.05.01

さがらです。

2024年1月にSnowflakeのTerraform Providerに関する2024年のロードマップが公開されています。

このロードマップについてわかりやすくまとめて頂いているのが下記の記事です。内容としては、GRANTの再設計、GAしている全機能のサポート、既存Issueの解決、などに取り組んでいくとのことで、破壊的な変更を含む一方で良い方向に進んでいることが感じ取れます。

そしてこのロードマップのうちの「GRANTの再設計」ですが、「v0.88.0でGRANTの再設計は完了」「以前の形式のGRANT関係のリソースは2024年6月26日に削除」というDiscussionが投稿されていました。着実に開発が進んでいますね。

そこで「新しいGRANTのリソースを使っていくのはいいけど、どのように使えばいいのだろう」と私自身も感じたため、この新しいGRANTのリソースを用いたSnowflakeでのFunctional Role + Access Roleのロール設計を実現するTerraformの構成を考えてみましたので、本記事でまとめてみます。

前置き:Functional Role + Access Roleとは

まず前置きとして、Functional Role + Access Roleのロール設計について簡単に触れておきます。

公式Docでも言及されているロール設計の考え方で、まとめるとこのような特徴を持ったロール設計です。

  • Functional Roleは実際にビジネスを進める上での部門や役割に応じたロール
  • Access Roleは各Snowflakeオブジェクトへのアクセス権だけを付与したロール
    • 例:あるスキーマ内のテーブルをSELECTだけできる権限を持つAccess Role、あるスキーマ内のテーブルに対して全ての操作ができる権限を持つAccess Role
  • 複数のAccess RoleをFunctional RoleにGRANTし、Snowflake上の権限を付与する
  • Functional Roleを各ユーザーにGRANTし、ユーザーはFunctional Roleを使ってSnowflake上の操作を行う

図は下記のリンク先からの引用ですが、Functional Role + Access Roleのロール設計のイメージとしてはこのようになります。

このロール設計のメリットについては、主に以下の点があげられます。

  • FUTURE GRANTを行うのは1回だけでよい
    • あるデータベース・スキーマに対するAccess Roleに一度FUTURE GRANTすればよい
    • これがAccess RoleとFunctional Roleに分かれていないと、何回もFUTURE GRANTをしないといけないので運用が大変です
  • Functional RoleにGRANTされている権限がわかりやすい
    • これはロールの命名規則にも気をつける必要がありますが、例えばAccess Roleの命名規則を_SCHEMA_<スキーマ名>_RO_ARのようにしておくと、SHOW GRANTSコマンドでGRANTされている権限の一覧を見たときに「Functional RoleはこのスキーマのRead Only権限があるんだな」ということがすぐにわかります。
    • これがAccess RoleとFunctional Roleに分かれていないと、SHOW GRANTSコマンドでGRANTされている権限の一覧を見たときに大量のUSAGEやSELECT権限が表示され、付与されている権限の理解に時間がかかってしまいます。

デメリットとしては、作成するロールの数がどうしても多くなってしまいます。しかし、このロールの数が多くなる点はTerraformを使うことで、一度実装さえしてしまえばすぐに新しいオブジェクトを追加できるため問題なくなります。

参考:これまでのGRANT関係のリソースを用いたFunctional Role + Access Roleを実現する方法

ちなみに、TerraformのこれまでのGRANT関係のリソースでFunctional Role + Access Roleを実現する方法は下記の記事が非常に参考になります。この記事で使用しているGRANT関係のリソースはもうすぐ使えなくなってしまいますが、YAMLで仕様をまとめたTerraform × Snowlflakeの構成の参考になると思います。

(実は、私自身もこの記事を元に最新のGRANTに置き換えるところからTerraformの勉強を始めました。この記事には本当に感謝しかないです…)

Terraformを使用するための事前準備

Terraformを実運用環境で使うためには事前にちゃんとRemote Stateや実行環境を用意しておく必要があります。

私の記事で恐縮ですが、Remote Stateに必要なAWSのリソース定義し、GitHub Actions上でTerraformを実行するための準備をまとめた記事が下記になります。今回の私の検証もこの記事の内容に沿って事前準備を行っています。

ディレクトリ構成

ということで、ここからは本題のTerraformを用いたFunctional Role + Access Roleのロール設計について話していきます!

いきなりですが、下記が今回構築したディレクトリ構成です。実際の各ファイルの内容については次章以降で触れます。※注意事項としては、最低限のオブジェクトのみModuleを定義しています。実際に使う際には他のオブジェクトのModule定義も忘れないようにしてください(ステージ、各種ポリシー、リソースモニター、etc)

.
├── README.md
├── backend.tf
├── main.tf
├── modules
│   ├── access_role_and_database
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_schema
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_warehouse
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── functional_role
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   └── user
│       ├── main.tf
│       ├── outputs.tf
│       ├── variables.tf
│       └── versions.tf
├── outputs.tf
├── variables.tf
└── versions.tf

このディレクトリ構成に至った理由は主に以下です。

  • なるべくシンプルな構成にして、可読性を高めたかった
    • リソースの定義はmain.tfに集約
  • Module構成は「各Snowflakeオブジェクト + 関連するAccess Roleの作成」をベースに分けた
    • Access Roleは各オブジェクトが作成されると必ず必要になることに加え、作成するAccess Roleの権限も同じため、各オブジェクトごとにAccess Roleもまとめてしまおうと考えました

このディレクトリ構成を考えるにあたって、下記のリンク先を参考にさせて頂きました。

modules/access_role_and_databaseについて

modules/access_role_and_databaseでは、「データベースを作成するリソース」と「作成するデータベースに関連するAccess Roleのリソース」をModuleとして定義しています。

main.tf

各種リソースをまとめて定義しています。ポイントはこのあたりです。

  • Database Roleを使うことで、ユーザーがSnowsight上でAccess Roleに切り替えができない(ユーザーに表示されるロールが無駄に増えないメリットがある)
  • このAccess Roleを付与したいFunctional Roleをgrant_readonly_ar_to_fr_setgrant_readwrite_ar_to_fr_setでModule利用時に受け取ることで、作成したAccess RoleのFunctional RoleへのGRANTもまとめて行う
  • 作成したAccess RoleはSYSADMINにもGRANTすることで、SYSADMINが全てのオブジェクトにアクセスできるようにする
# データベースの作成
resource "snowflake_database" "this" {
  name                        = var.database_name
  comment                     = var.comment
  data_retention_time_in_days = var.data_retention_time_in_days

  # replicationやshare周りのoptionは割愛
}

# 対象のデータベースに対するRead OnlyのAccess Roleを作成
resource "snowflake_database_role" "read_only_ar" {
  database = snowflake_database.this.name
  name     = "_DATABASE_${snowflake_database.this.name}_RO_AR"
  comment  = "Read only role of ${snowflake_database.this.name}"

  depends_on = [snowflake_database.this]
}

# Read OnlyのAccess Roleへの権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_only" {
  privileges         = ["USAGE", "MONITOR"]
  database_role_name = "\"${snowflake_database.this.name}\".\"${snowflake_database_role.read_only_ar.name}\""
  on_database        = snowflake_database.this.name

  depends_on = [snowflake_database_role.read_only_ar]
}

# Functional RoleにRead OnlyのAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_readonly_ar_to_fr" {
  for_each = var.grant_readonly_ar_to_fr_set

  database_role_name = "\"${snowflake_database.this.name}\".\"${snowflake_database_role.read_only_ar.name}\""
  parent_role_name   = each.value

  depends_on = [snowflake_database_role.read_only_ar]
}

# 対象のデータベースに対するRead/WriteのAccess Roleを作成
resource "snowflake_database_role" "read_write_ar" {
  database = snowflake_database.this.name
  name     = "_DATABASE_${snowflake_database.this.name}_RW_AR"
  comment  = "Read/Write role of ${snowflake_database.this.name}"

  depends_on = [snowflake_database.this]
}

# Read WriteのAccess Roleへの権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_write" {
  all_privileges     = true
  database_role_name = "\"${snowflake_database.this.name}\".\"${snowflake_database_role.read_write_ar.name}\""
  on_database        = snowflake_database.this.name

  depends_on = [snowflake_database_role.read_write_ar]
}

# Functional RoleにRead/WriteのAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_readwrite_ar_to_fr" {
  for_each = var.grant_readwrite_ar_to_fr_set

  database_role_name = "\"${snowflake_database.this.name}\".\"${snowflake_database_role.read_write_ar.name}\""
  parent_role_name   = each.value

  depends_on = [snowflake_database_role.read_write_ar]
}

# SYSADMINにAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_to_sysadmin" {
  for_each           = toset([snowflake_database_role.read_only_ar.name, snowflake_database_role.read_write_ar.name])
  database_role_name = "\"${snowflake_database.this.name}\".\"${each.value}\""
  parent_role_name   = "SYSADMIN"

  depends_on = [snowflake_database_role.read_only_ar, snowflake_database_role.read_write_ar]
}

outputs.tf

現状は、他のModuleから作成したデータベース名を参照できるように、nameだけoutputとして定義しています。

output "name" {
  description = "Name of the database."
  value       = snowflake_database.this.name
}

variables.tf

Moduleを使用するときに必要な各変数を定義しています。基本的にsnowflake_databaseリソースで使う値を定義しています。

また、Access Roleを付与したいFunctional RoleをModule使用時に受け取るためにgrant_readonly_ar_to_fr_setgrant_readwrite_ar_to_fr_setも定義しています。

variable "database_name" {
  description = "Name of the database"
  type        = string
  default     = null
}

variable "comment" {
  description = "Write description for the database"
  type        = string
  default     = null
}

variable "data_retention_time_in_days" {
  description = "Time travelable period to be set for the entire database."
  type        = number
  default     = null
}

variable "grant_readonly_ar_to_fr_set" {
  description = "Set of functional role for grant read only access role"
  type        = set(string)
  default     = []
}

variable "grant_readwrite_ar_to_fr_set" {
  description = "Set of functional role for grant read/write access role"
  type        = set(string)
  default     = []
}

versions.tf

使用するSnowflakeのterraform providerのバージョン指定を行っています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

modules/access_role_and_schemaについて

modules/access_role_and_schemaでは、「スキーマを作成するリソース」と「作成するスキーマに関連するAccess Roleのリソース」をModuleとして定義しています。

main.tf

各種リソースをまとめて定義しています。ポイントはこのあたりです。

  • 今回は「テーブルごとにアクセス権を与えず、スキーマレベルでアクセス権を管理する」考えでModuleを定義したので、スキーマ内の全テーブルへのGRANTと今後増えるテーブルに対するFUTURE GRANTを行っている
  • Database Roleを使うことで、ユーザーがSnowsight上でAccess Roleに切り替えができない(ユーザーに表示されるロールが無駄に増えないメリットがある)
  • このAccess Roleを付与したいFunctional Roleをgrant_readonly_ar_to_fr_setgrant_readwrite_ar_to_fr_setでModule利用時に受け取ることで、作成したAccess RoleのFunctional RoleへのGRANTもまとめて行う
  • 作成したAccess RoleはSYSADMINにもGRANTすることで、SYSADMINが全てのオブジェクトにアクセスできるようにする
# スキーマの作成
resource "snowflake_schema" "this" {
  database            = var.database_name
  name                = var.schema_name
  comment             = var.comment
  data_retention_days = var.data_retention_days

  is_managed   = var.is_managed
  is_transient = var.is_transient
}

# 対象のスキーマに対するRead OnlyのAccess Roleを作成
resource "snowflake_database_role" "read_only_ar" {
  database = snowflake_schema.this.database
  name     = "_SCHEMA_${snowflake_schema.this.name}_RO_AR"
  comment  = "Read only role of ${snowflake_schema.this.name} schema"

  depends_on = [snowflake_schema.this]
}

# Read OnlyのAccess Roleへのスキーマ権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_only_schema" {
  privileges         = ["USAGE", "MONITOR"]
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_only_ar.name}\""
  on_schema {
    schema_name = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
  }

  depends_on = [snowflake_database_role.read_only_ar]
}

# Read OnlyのAccess Roleへのスキーマ内すべてのテーブル権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_only_all_tables" {
  privileges         = ["SELECT"]
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_only_ar.name}\""
  on_schema_object {
    all {
      object_type_plural = "TABLES"
      in_schema          = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
    }
  }

  depends_on = [snowflake_database_role.read_only_ar]
}

# Read OnlyのAccess Roleへのスキーマ内すべてのテーブル権限のfuture grant
resource "snowflake_grant_privileges_to_database_role" "grant_read_only_future_tables" {
  privileges         = ["SELECT"]
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_only_ar.name}\""
  on_schema_object {
    future {
      object_type_plural = "TABLES"
      in_schema          = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
    }
  }

  depends_on = [snowflake_database_role.read_only_ar]
}


# Functional RoleにRead OnlyのAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_readonly_ar_to_fr" {
  for_each = var.grant_readonly_ar_to_fr_set

  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_only_ar.name}\""
  parent_role_name   = each.value

  depends_on = [snowflake_database_role.read_only_ar]
}

# 対象のデータベースに対するRead/WriteのAccess Roleを作成
resource "snowflake_database_role" "read_write_ar" {
  database = snowflake_schema.this.database
  name     = "_SCHEMA_${snowflake_schema.this.name}_RW_AR"
  comment  = "Read/Write role of ${snowflake_schema.this.name} schema"

  depends_on = [snowflake_schema.this]
}

# Read WriteのAccess Roleへのスキーマ権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_write_schema" {
  all_privileges     = true
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_write_ar.name}\""
  on_schema {
    schema_name = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
  }

  depends_on = [snowflake_database_role.read_write_ar]
}

# Read WriteのAccess Roleへのスキーマ内すべてのテーブル権限のgrant
resource "snowflake_grant_privileges_to_database_role" "grant_read_write_all_tables" {
  all_privileges     = true
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_write_ar.name}\""
  on_schema_object {
    all {
      object_type_plural = "TABLES"
      in_schema          = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
    }
  }

  depends_on = [snowflake_database_role.read_write_ar]
}

# Read WriteのAccess Roleへのスキーマ内すべてのテーブル権限のfuture grant
resource "snowflake_grant_privileges_to_database_role" "grant_read_write_future_tables" {
  all_privileges     = true
  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_write_ar.name}\""
  on_schema_object {
    future {
      object_type_plural = "TABLES"
      in_schema          = "\"${snowflake_schema.this.database}\".\"${snowflake_schema.this.name}\""
    }
  }

  depends_on = [snowflake_database_role.read_write_ar]
}

# Functional RoleにRead/WriteのAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_readwrite_ar_to_fr" {
  for_each = var.grant_readwrite_ar_to_fr_set

  database_role_name = "\"${snowflake_schema.this.database}\".\"${snowflake_database_role.read_write_ar.name}\""
  parent_role_name   = each.value

  depends_on = [snowflake_database_role.read_write_ar]
}

# SYSADMINにAccess Roleをgrant
resource "snowflake_grant_database_role" "grant_to_sysadmin" {
  for_each           = toset([snowflake_database_role.read_only_ar.name, snowflake_database_role.read_write_ar.name])
  database_role_name = "\"${snowflake_schema.this.database}\".\"${each.value}\""
  parent_role_name   = "SYSADMIN"

  depends_on = [snowflake_database_role.read_only_ar, snowflake_database_role.read_write_ar]
}

outputs.tf

現状は、他のModuleから作成したスキーマ名を参照できるように、nameだけoutputとして定義しています。

output "name" {
  description = "Name of the schema."
  value       = snowflake_schema.this.name
}

variables.tf

Moduleを使用するときに必要な各変数を定義しています。基本的にsnowflake_schemaリソースで使う値を定義しています。

また、Access Roleを付与したいFunctional RoleをModule使用時に受け取るためにgrant_readonly_ar_to_fr_setgrant_readwrite_ar_to_fr_setも定義しています。

variable "schema_name" {
  description = "Name of the schema"
  type        = string
  default     = null
}

variable "database_name" {
  description = "Name of the database to which the Schema belongs"
  type        = string
  default     = null
}

variable "comment" {
  description = "Write description for the schema"
  type        = string
  default     = null
}

variable "data_retention_days" {
  description = "Time travelable period to be set for the entire schema."
  type        = number
  default     = null
}

variable "is_managed" {
  description = "Specifies a managed schema."
  type        = bool
  default     = false
}

variable "is_transient" {
  description = "Specifies a schema as transient."
  type        = bool
  default     = false
}

variable "grant_readonly_ar_to_fr_set" {
  description = "Set of functional role for grant read only access role"
  type        = set(string)
  default     = []
}

variable "grant_readwrite_ar_to_fr_set" {
  description = "Set of functional role for grant read/write access role"
  type        = set(string)
  default     = []
}

versions.tf

使用するSnowflakeのterraform providerのバージョン指定を行っています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

modules/access_role_and_warehouseについて

modules/access_role_and_warehouseでは、「ウェアハウスを作成するリソース」と「作成するウェアハウスに関連するAccess Roleのリソース」をModuleとして定義しています。

main.tf

各種リソースをまとめて定義しています。ポイントはこのあたりです。

  • ウェアハウスの権限はDatabase RoleにGRANTできないため、普通のロールを使っている。ただ、Access Roleであることがひと目でわかるように_WAREHOUSE_<ウェアハウス名>_USAGE_ARのように、先頭に_をつけている
  • このAccess Roleを付与したいFunctional Roleをgrant_usage_ar_to_fr_setgrant_admin_ar_to_fr_setでModule利用時に受け取ることで、作成したAccess RoleのFunctional RoleへのGRANTもまとめて行う
  • 作成したAccess RoleはSYSADMINにもGRANTすることで、SYSADMINが全てのオブジェクトにアクセスできるようにする
# ウェアハウスの作成
resource "snowflake_warehouse" "this" {
  name           = var.warehouse_name
  comment        = var.comment
  warehouse_size = var.warehouse_size

  auto_resume                         = var.auto_resume
  auto_suspend                        = var.auto_suspend
  enable_query_acceleration           = var.enable_query_acceleration
  initially_suspended                 = var.initially_suspended
  max_cluster_count                   = var.max_cluster_count
  max_concurrency_level               = var.max_concurrency_level
  min_cluster_count                   = var.min_cluster_count
  query_acceleration_max_scale_factor = var.query_acceleration_max_scale_factor
  resource_monitor                    = var.resource_monitor
  scaling_policy                      = var.scaling_policy
  statement_queued_timeout_in_seconds = var.statement_queued_timeout_in_seconds
  statement_timeout_in_seconds        = var.statement_timeout_in_seconds
  warehouse_type                      = var.warehouse_type
}

# 対象のウェアハウスに対するUSAGEのAccess Roleを作成
resource "snowflake_role" "usage_ar" {
  name    = "_WAREHOUSE_${snowflake_warehouse.this.name}_USAGE_AR"
  comment = "USAGE role of ${snowflake_warehouse.this.name}"

  depends_on = [snowflake_warehouse.this]
}

# USAGEのAccess Roleへの権限のgrant
resource "snowflake_grant_privileges_to_account_role" "grant_usage" {
  privileges        = ["USAGE", "MONITOR"]
  account_role_name = snowflake_role.usage_ar.name
  on_account_object {
    object_type = "WAREHOUSE"
    object_name = snowflake_warehouse.this.name
  }

  depends_on = [snowflake_role.usage_ar]
}

# Functional RoleにUSAGEのAccess Roleをgrant
resource "snowflake_grant_account_role" "grant_usage_ar_to_fr" {
  for_each = var.grant_usage_ar_to_fr_set

  role_name        = snowflake_role.usage_ar.name
  parent_role_name = each.value

  depends_on = [snowflake_role.usage_ar]
}

# 対象のウェアハウスに対するADMINのAccess Roleを作成
resource "snowflake_role" "admin_ar" {
  name    = "_WAREHOUSE_${snowflake_warehouse.this.name}_ADMIN_AR"
  comment = "ADMIN role of ${snowflake_warehouse.this.name}"

  depends_on = [snowflake_warehouse.this]
}

# ADMINのAccess Roleへの権限のgrant
resource "snowflake_grant_privileges_to_account_role" "grant_admin" {
  all_privileges    = true
  account_role_name = snowflake_role.admin_ar.name
  on_account_object {
    object_type = "WAREHOUSE"
    object_name = snowflake_warehouse.this.name
  }

  depends_on = [snowflake_role.admin_ar]
}

# Functional RoleにADMINのAccess Roleをgrant
resource "snowflake_grant_account_role" "grant_admin_ar_to_fr" {
  for_each = var.grant_admin_ar_to_fr_set

  role_name        = snowflake_role.admin_ar.name
  parent_role_name = each.value

  depends_on = [snowflake_role.admin_ar]
}

# SYSADMINにAccess Roleをgrant
resource "snowflake_grant_account_role" "grant_to_sysadmin" {
  for_each         = toset([snowflake_role.usage_ar.name, snowflake_role.admin_ar.name])
  role_name        = each.value
  parent_role_name = "SYSADMIN"

  depends_on = [snowflake_role.usage_ar, snowflake_role.admin_ar]
}

outputs.tf

現状は、他のModuleから作成したウェアハウス名を参照できるように、nameだけoutputとして定義しています。

output "name" {
  description = "Name of the warehouse."
  value       = snowflake_warehouse.this.name
}

variables.tf

Moduleを使用するときに必要な各変数を定義しています。基本的にsnowflake_warehouseリソースで使う値を定義しています。

また、Access Roleを付与したいFunctional RoleをModule使用時に受け取るためにgrant_usage_ar_to_fr_setgrant_admin_ar_to_fr_setも定義しています。

variable "warehouse_name" {
  description = "Name of the warehouse"
  type        = string
  default     = null
}

variable "auto_resume" {
  description = "Specifies whether to automatically resume a warehouse when a SQL statement (e.g. query) is submitted to it."
  type        = bool
  default     = true
}

variable "auto_suspend" {
  description = "Specifies the number of seconds of inactivity after which a warehouse is automatically suspended."
  type        = number
  default     = 60
}

variable "comment" {
  description = "Write description for the schema"
  type        = string
  default     = null
}

variable "enable_query_acceleration" {
  description = "Specifies whether to enable the query acceleration service for queries that rely on this warehouse for compute resources."
  type        = bool
  default     = null
}

variable "initially_suspended" {
  description = "Specifies whether the warehouse is created initially in the Suspended state."
  type        = bool
  default     = true
}

variable "max_cluster_count" {
  description = "Specifies the maximum number of server clusters for the warehouse."
  type        = number
  default     = 1
}

variable "max_concurrency_level" {
  description = "Object parameter that specifies the concurrency level for SQL statements (i.e. queries and DML) executed by a warehouse."
  type        = number
  default     = null
}

variable "min_cluster_count" {
  description = "Specifies the minimum number of server clusters for the warehouse (only applies to multi-cluster warehouses)."
  type        = number
  default     = 1
}

variable "query_acceleration_max_scale_factor" {
  description = "Specifies the maximum scale factor for leasing compute resources for query acceleration. The scale factor is used as a multiplier based on warehouse size."
  type        = number
  default     = null
}

variable "resource_monitor" {
  description = "Specifies the name of a resource monitor that is explicitly assigned to the warehouse."
  type        = string
  default     = null
}

variable "scaling_policy" {
  description = "Specifies the policy for automatically starting and shutting down clusters in a multi-cluster warehouse running in Auto-scale mode."
  type        = string
  default     = null
}

variable "statement_queued_timeout_in_seconds" {
  description = "Object parameter that specifies the time, in seconds, a SQL statement (query, DDL, DML, etc.) can be queued on a warehouse before it is canceled by the system."
  type        = number
  default     = null
}

variable "statement_timeout_in_seconds" {
  description = "Specifies the time, in seconds, after which a running SQL statement (query, DDL, DML, etc.) is canceled by the system"
  type        = number
  default     = null
}

variable "warehouse_size" {
  description = "Specifies the size of the virtual warehouse. "
  type        = string
  default     = "XSMALL"
}

variable "warehouse_type" {
  description = "Specifies a STANDARD or SNOWPARK-OPTIMIZED warehouse"
  type        = string
  default     = "STANDARD"
}

variable "grant_usage_ar_to_fr_set" {
  description = "Set of functional role for grant usage access role"
  type        = set(string)
  default     = []
}

variable "grant_admin_ar_to_fr_set" {
  description = "Set of functional role for grant admin access role"
  type        = set(string)
  default     = []
}

versions.tf

使用するSnowflakeのterraform providerのバージョン指定を行っています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

modules/functional_roleについて

modules/functional_roleでは、「Functional Roleを作成するリソース」と「作成したFunctional Roleを各ユーザーにGRANTするリソース」をModuleとして定義しています。

main.tf

各種リソースをまとめて定義しています。ポイントはこのあたりです。

  • Functional Roleの作成だけでなく、ユーザーへのGRANTも行う。GRANT先のユーザーリストはgrant_user_setで受け取る
  • 作成したFunctional RoleはSYSADMINにもGRANTすることで、SYSADMINが全てのFunctional Roleの親となるようにする
# Functional Roleの作成
resource "snowflake_role" "this" {
  name    = var.role_name
  comment = var.comment
}

# Functional Roleをユーザーにgrant
resource "snowflake_grant_account_role" "grant_to_user" {
  for_each = var.grant_user_set

  role_name = var.role_name
  user_name = each.value

  depends_on = [snowflake_role.this]
}

# SYSADMINにFunctional Roleをgrant
resource "snowflake_grant_account_role" "grant_to_sysadmin" {
  role_name        = var.role_name
  parent_role_name = "SYSADMIN"

  depends_on = [snowflake_role.this]
}

outputs.tf

現状は、他のModuleから作成したFunctional Role名を参照できるように、nameだけoutputとして定義しています。

output "name" {
  description = "Name of the functional_role."
  value       = snowflake_role.this.name
}

variables.tf

Moduleを使用するときに必要な各変数を定義しています。基本的にsnowflake_roleリソースで使う値を定義しています。

また、Functionalを付与したいユーザーをModule使用時に受け取るためにgrant_user_setも定義しています。

variable "role_name" {
  description = "Name of the functional role"
  type        = string
  default     = null
}

variable "comment" {
  description = "Write description for the functional role"
  type        = string
  default     = null
}

variable "grant_user_set" {
  description = "Set of user for grant functional role"
  type        = set(string)
  default     = []
}

versions.tf

使用するSnowflakeのterraform providerのバージョン指定を行っています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

modules/userについて

modules/userでは、「Snowflakeのユーザーを作成するリソース」をModuleとして定義しています。(現在の構成では無理にModule化する必要もないのが正直なところですが、将来ユーザーに関する別のリソースを使う可能性も考慮し、Module化しています。)

main.tf

Snowflakeのユーザー作成を行っています。

resource "snowflake_user" "this" {
  name         = var.name
  login_name   = var.login_name
  comment      = var.comment
  password     = var.password
  disabled     = var.disabled
  display_name = var.display_name
  email        = var.email
  first_name   = var.first_name
  last_name    = var.last_name

  default_warehouse       = var.default_warehouse
  default_secondary_roles = var.default_secondary_roles
  default_role            = var.default_role

  rsa_public_key   = var.rsa_public_key
  rsa_public_key_2 = var.rsa_public_key_2

  must_change_password = var.must_change_password
}

outputs.tf

module/functional_roleにユーザーのnameを渡したいため、それをoutputとしています。※nameがsensitiveのため、やむを得ずvariableの値を定義しています…

output "name" {
  description = "Name of the user."
  value       = var.name # snowflake_user.this.nameだとsensitiveで他moduleに渡せないため
}

variables.tf

Moduleを使用するときに必要な各変数を定義しています。基本的にsnowflake_userリソースで使う値を定義しています。

注意点としては、passwordのデフォルト値をdefaultにしています。must_change_passwordtrueにしているため初回ログイン時にパスワード変更を求められますが、必要に応じて変更ください。

variable "name" {
  description = "Name of the user. Note that if you do not supply login_name this will be used as login_name."
  type        = string
  default     = null
}

variable "login_name" {
  description = "The name users use to log in. If not supplied, snowflake will use name instead."
  type        = string
  default     = null
}

variable "comment" {
  description = "Write description for the resource"
  type        = string
  default     = null
}

variable "password" {
  description = "WARNING: this will put the password in the terraform state file. Use carefully."
  type        = string
  default     = "default" #必要に応じて変更する
}

variable "disabled" {
  description = "If true, the target user will not be deleted but deactivated"
  type        = string
  default     = null
}

variable "display_name" {
  description = "Name displayed for the user in the Snowflake web interface."
  type        = string
  default     = null
}

variable "email" {
  description = "Email address for the user."
  type        = string
  default     = null
}

variable "first_name" {
  description = "First name of the user."
  type        = string
  default     = null
}

variable "last_name" {
  description = "Last name of the user."
  type        = string
  default     = null
}

variable "default_warehouse" {
  description = "Specifies the namespace (database only or database and schema) that is active by default for the user’s session upon login."
  type        = string
  default     = null
}

variable "default_secondary_roles" {
  description = "Specifies the set of secondary roles that are active for the user’s session upon login. Currently only [ALL] value is supported"
  type        = set(string)
  default     = null
}

variable "default_role" {
  description = "Specifies the role that is active by default for the user’s session upon login."
  type        = string
  default     = "PUBLIC"
}

variable "rsa_public_key" {
  description = "Specifies the user’s RSA public key; used for key-pair authentication. Must be on 1 line without header and trailer."
  type        = string
  default     = null
}

variable "rsa_public_key_2" {
  description = "Specifies the user’s second RSA public key; used to rotate the public and private keys for key-pair authentication based on an expiration schedule set by your organization. Must be on 1 line without header and trailer."
  type        = string
  default     = null
}

variable "must_change_password" {
  description = "Specifies whether the user is forced to change their password on next login (including their first/initial login) into the system."
  type        = bool
  default     = true
}

versions.tf

使用するSnowflakeのterraform providerのバージョン指定を行っています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

ルートディレクトリの各ファイルについて

backend.tf

このファイルではRemote Stateを管理するS3とDynamoDBを定義しています。

terraform {
  backend "s3" {
    bucket         = "sagara-terraform-state-bucket"
    key            = "snowflake-state/snowflake.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "sagara-terraform-state-lock-table"
    encrypt        = true
  }
}

outputs.tf

今回検証した範疇では必須となるoutputがなかったため、特にコードを書いていません。いつか使うときのためにファイルだけ作成しています。

variables.tf

今回検証した範疇では必須となるvariableがなかったため、特にコードを書いていません。いつか使うときのためにファイルだけ作成しています。(環境を分けてterraformの管理をしていきたくなったときには、必要になると考えています。)

versions.tf

使用するSnowflakeのterraform providerのバージョン指定と、各aliasの定義をしています。

特徴としては、SYSADMINとSECURITYADMINをGRANTしたTERRAFORMというロールを作成し、基本的にはこのaliasを用いてTerraformを実行しています。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflake-labs/snowflake"
      version = "~> 0.88"
    }
  }
}

provider "snowflake" { # 事前にSYSADMINとSECURITYADMINをGRANTしたロール。
  alias = "terraform"
  role  = "TERRAFORM"
}

provider "snowflake" {
  alias = "sys_admin"
  role  = "SYSADMIN"
}

provider "snowflake" {
  alias = "security_admin"
  role  = "SECURITYADMIN"
}

main.tf

定義した各Moduleを使用し、Snowflakeでの各オブジェクトを定義していくファイルとなっています。

以下のコードはサンプルですが、各Moduleの使い方のポイントはこの辺りです。

  • 各Moduleで定義したオブジェクトのGRANT先のリストを格納するgrant_user_setgrant_usage_ar_to_fr_setgrant_admin_ar_to_fr_setgrant_readonly_ar_to_fr_setgrant_readwrite_ar_to_fr_setに対しては、関連するModuleのOutputを指定すること(このように記述しないと、Module間の依存関係がうまく構築できず、terraform apply時にエラーとなります)
    • 例:作成したFunctional RoleをGrantする先を指定する際は、grant_user_set = [module.analyst_sagara.name,module.developer_sagara.name]のように記述する
  • このコードではModule使用時に、各Moduleで定義したvariable全てに対して値を入れていません。各Moduleのvariables.tfで定義したvariableのdefaultの値を確認の上、必要な場合にはModule使用時に該当するvariableに値を入れてください
########################
# ユーザー
########################

module "analyst_sagara" {
  source = "./modules/user"
  providers = {
    snowflake = snowflake.security_admin
  }

  name    = "ANALYST_SAGARA"
  comment = "Analyst sagara"
}

module "developer_sagara" {
  source = "./modules/user"
  providers = {
    snowflake = snowflake.security_admin
  }

  name    = "DEVELOPER_SAGARA"
  comment = "Developer sagara"
}

########################
# Functional Role
########################

module "aaa_analyst_fr" {
  source = "./modules/functional_role"
  providers = {
    snowflake = snowflake.security_admin
  }

  role_name = "AAA_ANALYST_FR"
  grant_user_set = [
    module.analyst_sagara.name,
    module.developer_sagara.name
  ]
  comment = "Functional Role for analysis in Project AAA"
}

module "aaa_developer_fr" {
  source = "./modules/functional_role"
  providers = {
    snowflake = snowflake.security_admin
  }

  role_name = "AAA_DEVELOPER_FR"
  grant_user_set = [
    module.developer_sagara.name
  ]
  comment = "Functional Role for develop in Project AAA"
}

module "bbb_analyst_fr" {
  source = "./modules/functional_role"
  providers = {
    snowflake = snowflake.security_admin
  }

  role_name = "BBB_ANALYST_FR"
  grant_user_set = [
    module.analyst_sagara.name,
    module.developer_sagara.name
  ]
  comment = "Functional Role for analysis in Project BBB"
}

module "bbb_developer_fr" {
  source = "./modules/functional_role"
  providers = {
    snowflake = snowflake.security_admin
  }

  role_name = "BBB_DEVELOPER_FR"
  grant_user_set = [
    module.developer_sagara.name
  ]
  comment = "Functional Role for develop in Project BBB"
}

########################
# データベース
########################
module "raw_data_db" {
  source = "./modules/access_role_and_database"
  providers = {
    snowflake = snowflake.terraform
  }

  database_name               = "RAW_DATA"
  comment                     = "Database to store loaded raw data"
  data_retention_time_in_days = 3
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name,
    module.bbb_developer_fr.name
  ]
}

module "staging_db" {
  source = "./modules/access_role_and_database"
  providers = {
    snowflake = snowflake.terraform
  }

  database_name               = "STAGING"
  comment                     = "Database to store data with minimal transformation from raw data"
  data_retention_time_in_days = 1
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name,
    module.bbb_developer_fr.name
  ]
}

module "dwh_db" {
  source = "./modules/access_role_and_database"
  providers = {
    snowflake = snowflake.terraform
  }

  database_name               = "DWH"
  comment                     = "Database to store data on which various modeling has been done"
  data_retention_time_in_days = 1
  grant_readonly_ar_to_fr_set = [
    module.aaa_analyst_fr.name,
    module.bbb_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name,
    module.bbb_developer_fr.name
  ]
}

module "mart_db" {
  source = "./modules/access_role_and_database"
  providers = {
    snowflake = snowflake.terraform
  }

  database_name               = "MART"
  comment                     = "Database that stores data used for reporting and linkage to another tool"
  data_retention_time_in_days = 1
  grant_readonly_ar_to_fr_set = [
    module.aaa_analyst_fr.name,
    module.bbb_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name,
    module.bbb_developer_fr.name
  ]
}

########################
# スキーマ
########################
module "raw_data_db_aaa_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "AAA"
  database_name       = module.raw_data_db.name
  comment             = "Schema to store loaded raw data of AAA"
  data_retention_days = 3
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "raw_data_db_bbb_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "BBB"
  database_name       = module.raw_data_db.name
  comment             = "Schema to store loaded raw data of BBB"
  data_retention_days = 3
  grant_readwrite_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

module "staging_db_aaa_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "AAA"
  database_name       = module.staging_db.name
  comment             = "Schema to store data with minimal transformation from raw data of AAA"
  data_retention_days = 1
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "staging_db_bbb_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "BBB"
  database_name       = module.staging_db.name
  comment             = "Schema to store data with minimal transformation from raw data of BBB"
  data_retention_days = 1
  grant_readwrite_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

module "dwh_db_aaa_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "AAA"
  database_name       = module.dwh_db.name
  comment             = "Schema to store data on which various modeling has been done for AAA"
  data_retention_days = 1
  grant_readonly_ar_to_fr_set = [
    module.aaa_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "dwh_db_bbb_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "BBB"
  database_name       = module.dwh_db.name
  comment             = "Schema to store data on which various modeling has been done for BBB"
  data_retention_days = 1
  grant_readonly_ar_to_fr_set = [
    module.bbb_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

module "mart_db_aaa_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "AAA"
  database_name       = module.mart_db.name
  comment             = "Schema that stores data used for reporting and linkage to another tool for AAA"
  data_retention_days = 1
  grant_readonly_ar_to_fr_set = [
    module.aaa_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "mart_db_bbb_schema" {
  source = "./modules/access_role_and_schema"
  providers = {
    snowflake = snowflake.terraform
  }

  schema_name         = "BBB"
  database_name       = module.mart_db.name
  comment             = "Schema that stores data used for reporting and linkage to another tool for BBB"
  data_retention_days = 1
  grant_readonly_ar_to_fr_set = [
    module.bbb_analyst_fr.name
  ]
  grant_readwrite_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

########################
# ウェアハウス
########################
module "aaa_analyse_wh" {
  source = "./modules/access_role_and_warehouse"
  providers = {
    snowflake = snowflake.terraform
  }

  warehouse_name = "AAA_ANALYSE_WH"
  warehouse_size = "XSMALL"
  comment        = "Warehouse for analysis of AAA projects"

  grant_usage_ar_to_fr_set = [
    module.aaa_analyst_fr.name
  ]
  grant_admin_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "bbb_analyse_wh" {
  source = "./modules/access_role_and_warehouse"
  providers = {
    snowflake = snowflake.terraform
  }

  warehouse_name = "BBB_ANALYSE_WH"
  warehouse_size = "XSMALL"
  comment        = "Warehouse for analysis of BBB projects"

  grant_usage_ar_to_fr_set = [
    module.bbb_analyst_fr.name
  ]
  grant_admin_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

module "aaa_develop_wh" {
  source = "./modules/access_role_and_warehouse"
  providers = {
    snowflake = snowflake.terraform
  }

  warehouse_name = "AAA_DEVELOP_WH"
  warehouse_size = "XSMALL"
  comment        = "Warehouse for develop of AAA projects"

  grant_admin_ar_to_fr_set = [
    module.aaa_developer_fr.name
  ]
}

module "bbb_develop_wh" {
  source = "./modules/access_role_and_warehouse"
  providers = {
    snowflake = snowflake.terraform
  }

  warehouse_name = "BBB_DEVELOP_WH"
  warehouse_size = "XSMALL"
  comment        = "Warehouse for develop of BBB projects"

  grant_admin_ar_to_fr_set = [
    module.bbb_developer_fr.name
  ]
}

このコードで下図のオブジェクトが構築されます。

最後に

新しいGRANTリソースを用いたSnowflakeでのFunctional Role + Access Roleのロール設計を実現するTerraformの構成を考えてみました。

Moduleの分け方は色々な考え方があると思いますが、本記事で述べた内容は比較的シンプルなModuleの分け方だと思いますので、Functional Role + Access Roleのロール設計をTerraformで行う際の参考になると嬉しいです!