SnowflakeでFunctional Role+Access Roleのロール設計を実現するTerraformのModule構成を考えてみた
さがらです。
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権限が表示され、付与されている権限の理解に時間がかかってしまいます。
- これはロールの命名規則にも気をつける必要がありますが、例えばAccess Roleの命名規則を
デメリットとしては、作成するロールの数がどうしても多くなってしまいます。しかし、このロールの数が多くなる点は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_set
とgrant_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_set
とgrant_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_set
とgrant_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_set
とgrant_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_set
とgrant_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_set
とgrant_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_password
をtrue
にしているため初回ログイン時にパスワード変更を求められますが、必要に応じて変更ください。
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_set
、grant_usage_ar_to_fr_set
、grant_admin_ar_to_fr_set
、grant_readonly_ar_to_fr_set
、grant_readwrite_ar_to_fr_set
に対しては、関連するModuleのOutputを指定すること(このように記述しないと、Module間の依存関係がうまく構築できず、terraform apply
時にエラーとなります)- 例:作成したFunctional RoleをGrantする先を指定する際は、
grant_user_set = [module.analyst_sagara.name,module.developer_sagara.name]
のように記述する
- 例:作成したFunctional RoleをGrantする先を指定する際は、
- このコードでは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で行う際の参考になると嬉しいです!