Terraform による Snowflake ロール作成
~ Functional role + Access role モデル ~
こんにちは
DATUM STUDIOの梶谷です。
Snowflake
では、Functional role と Access role を組み合わせたロール構成
が推奨されています。推奨されてはいるものの、組織やデータの規模が大きくなっていくと、クエリでロールをメンテナンスしていくのは、ちょっと大変ではないでしょうか。
そんな時こそ IaC ツールの出番!ということで、今回の記事では Terraform で Functional role と Access role を実装する例をご紹介したいと思います。
目次
Snowflake でのアクセス制御
Snowflake でのアクセス制御について、基本的な考え方はSnowflake ドキュメント をご参照ください。
Functional role + Access role モデル
現実世界で人に与えられる役割や組織でのポジションに基づくロール(Functional Role)と、スキーマやテーブル等のオブジェクトに対する読み書き・使用可否に基づくロール(Access Role)をそれぞれ作成し、役割ごとに必要な権限を紐づけていくモデルです。 Functional Role が上位ロールに、Access Role が下位のロールになります。ユーザーには Functional Role 経由で各オブジェクトへの権限が割り当てられます。
(https://docs.snowflake.com/ja/user-guide/security-access-control-considerations.html
より引用)
詳しくは、Snowflake ドキュメント
や、こちらの記事にて解説されておりますので、ご参照ください。
Terraform による実装
それでは、Terraform で実装していきたいと思います。Let’s Terraforming!
環境について
本記事の実装については、次の環境で動作を確認しております。
- ・Terraform v1.2.6
- ・Terraform Snowflake Provider :snowflake-labs/snowflake v0.40.0
ディレクトリ構成
次の構成になっています。
terraform
├─environments # 環境別作業ディレクトリ
│ ├─prd # 本番環境用(テーブル等の定義がありますが、今回は割愛)
│ ├─dev # 開発環境用(上と同じく今回は割愛)
│ └─common # アカウントレベルのオブジェクトなど、共通リソース用
│ ├─yaml # Functional role と Access role の設定ファイル
│ │ ├─access_roles_to_functional_roles.yml
│ │ ├─access_roles.yml
│ │ └─functional_roles.yml
│ ├─backend.tf
│ ├─locals.tf
│ ├─main.tf
│ ├─provider.tf
│ └─variable.tf
└─modules # モジュール置き場
└─snowflake
└─functional_and_access_roles
├─grant_access_roles.tf
├─grant_access_roles_to_functional_roles.tf
├─grant_functional_roles.tf
├─output.tf
├─provider.tf
├─roles.tf
└─variable.tf
作業ディレクトリ
まずは、作業ディレクトリにあるソースコードから見ていきましょう。
ロールに関する設定は全て yaml ファイルに記載しており、local でファイルから読み込んでモジュールに渡していきます。
access_roles.yml には、Access role の名前とコメント、付与する権限を記載します。
# Access Role 一覧
# 書式:
access_roles:
- name: <Access Role 1>
comment: <comment for Access Role 1>
- name: <Access Role 2>
comment: <comment for Access Role 2>
:
# オブジェクトに対する権限を付与する Access Role 一覧
# 書式:
grant_on_object_to_access_role:
- name: <object1>_<parameter1_1>_<parameter1_2>_... # 一意な名前にする
type: <object1>
roles:
- <Access Role 1_1>
- <Access Role 1_2>
:
- name: <object2>_<parameter2_1>_<parameter2_2>_...
type: <object2>
roles:
- <Access Role 2_1>
- <Access Role 2_2>
:
:
functional_roles.yml には、 Functional role の名前とコメント、ロールを付与するユーザーを記載します。
# Functional Role一覧
# 書式:
functional_roles:
- name: <User1>
comment: <Comment for User1>
- name: <User2>
comment: <Comment for User2>
:
# Functional role を付与するユーザー一覧
# 書式:
grant_functional_roles_to_user:
- role_name: <Functional Role 1>
users:
- <User 1_1>
- <User 2_1>
:
- role_name: <Functional RolE 2>
users:
- <User 1_2>
- <User 2_2>
:
:
access_roles_to_functional_roles.yml には、Access role をどの Functional role に付与するかを記載します。
# Access role を付与する Functional role 一覧
# 書式:
grant_access_roles_to_functional_roles:
- access_role: <Access Role 1>
functional_roles:
- <Functional Role 1_1>
- <Functional Role 1_2>
:
- access_role: <Access Role 2>
functional_roles:
- <Functional Role 2_1>
- <Functional Role 2_2>
:
:
locals.tf で yaml ファイルに記載したロール情報を読み込みます。
locals {
# 設定ファイルをロード
access_roles_yml = yamldecode(
file("${path.root}/yaml/access_roles.yml")
)
functional_roles_yml = yamldecode(
file("${path.root}/yaml/functional_roles.yml")
)
access_roles_to_functional_roles_yml = yamldecode(
file("${path.root}/yaml/access_roles_to_functional_roles.yml")
)
# Access role のリスト
access_roles = flatten(local.access_roles_yml["access_roles"])
# grant ... on objects to Access role のリスト
grant_on_object_to_access_role = flatten(local.access_roles_yml["grant_on_object_to_access_role"])
# Functional roleのリスト
functional_roles = local.functional_roles_yml["functional_roles"]
# grant Functional role to ユーザーのリスト
grant_functional_roles_to_user = flatten(local.functional_roles_yml["grant_functional_roles_to_user"])
# grant Access role to Functional role のリスト
grant_access_role_to_functional_role = flatten(local.access_roles_to_functional_roles_yml["grant_access_roles_to_functional_roles"])
}
main.tf はモジュールを呼び出して locals を渡すだけのものです。
なお 、snowflake.snowflake_securityadmin は SECURITYADMIN ロールを設定した snowflake プロバイダです。
# Functional roleとAccess roleを作成
module "functional_and_access_roles" {
source = "../../modules/snowflake/functional_and_access_roles_refactor"
providers = {
snowflake = snowflake.snowflake_securityadmin
}
access_roles = local.access_roles
grant_on_object_to_access_role = local.grant_on_object_to_access_role
functional_roles = local.functional_roles
grant_access_roles_to_functional_roles = local.grant_access_role_to_functional_role
grant_functional_roles_to_user = local.grant_functional_roles_to_user
}
モジュール
variables.tf の各 variable に渡されたロール情報を展開して、ロールを作ったり権限を付与したりしていきます。
variable "functional_roles" {
type = list(any)
description = "Functional roleリスト。[ {name: <role_name>, comment: <comment>},... ]"
}
variable "access_roles" {
type = list(any)
description = "Access roleリスト。[ {name: <role_name>, comment: <comment>},... ]"
}
variable "grant_on_object_to_access_role" {
type = list(any)
description = "grant on ○○ を付与する Access role のリスト。[ {name: <name>, roles: [<role_name>], type: SCHEMA/FUTURE_TABLE/WAREHOUSE/etc., parameter: <parameter>},... ]"
}
variable "grant_access_roles_to_functional_roles" {
type = list(any)
description = "Access role を付与する Functional role のリスト。[ {functional_roles: [<role_a>, <role_b>, ...], access_role: <role_name>},... ]"
}
variable "grant_functional_roles_to_user" {
type = list(any)
description = "Functional role を付与するユーザーのリスト。[ {users: [<user_1>, <user_2, ...], role_name: <role_name>},... ]"
}
roles.tf で Functional role と Access role を作成し、SYSADMIN に紐づけます。
# Roleを作成
resource "snowflake_role" "roles" {
for_each = {
for role in concat(var.functional_roles, var.access_roles) :
role.name => role.comment
}
name = each.key
comment = each.value
}
# SYSADMIN にぶら下げる
resource "snowflake_role_grants" "role_grants" {
for_each = toset([for role in snowflake_role.roles : role.name])
role_name = each.key
roles = ["SYSADMIN"]
}
grant_access_roles.tf で、Access role への権限付与を行います。
access_roles.yml で type に指定したオブジェクトに従って、呼び出すリソースを分けています。
# on Database
resource "snowflake_database_grant" "on_database_grant" {
for_each = {
for grant in var.grant_on_object_to_access_role : grant.name => {
database_name = grant.parameter.database_name
privilege = grant.parameter.privilege
roles = grant.roles
}
if grant.type == "DATABASE"
}
database_name = each.value.database_name
privilege = each.value.privilege
roles = each.value.roles
# 先に存在しなければならないが、参照していないので依存関係を付ける
depends_on = [snowflake_role.roles]
}
# on Schema
resource "snowflake_schema_grant" "on_schema_grant" {
for_each = {
for grant in var.grant_on_object_to_access_role : grant.name => {
schema_name = grant.parameter.schema_name
database_name = grant.parameter.database_name
privilege = grant.parameter.privilege
roles = grant.roles
}
if grant.type == "SCHEMA"
}
database_name = each.value.database_name
schema_name = each.value.schema_name
privilege = each.value.privilege
roles = each.value.roles
# 先に存在しなければならないが、参照していないので依存関係を付ける
depends_on = [snowflake_role.roles]
}
# on (future) table
resource "snowflake_table_grant" "on_table" {
for_each = {
for grant in var.grant_on_object_to_access_role : grant.name => {
database_name = grant.parameter.database_name
schema_name = grant.parameter.schema_name
table_name = lookup(grant.parameter, "table_name", null)
privilege = grant.parameter.privilege
on_future = lookup(grant.parameter, "on_future", null)
roles = grant.roles
}
if grant.type == "TABLE"
}
database_name = each.value.database_name
schema_name = each.value.schema_name
table_name = each.value.table_name
privilege = each.value.privilege
on_future = each.value.on_future
roles = each.value.roles
# 先に存在しなければならないが、参照していないので依存関係を付ける
depends_on = [snowflake_role.roles]
}
# on warehouse
resource "snowflake_warehouse_grant" "on_warehouse_grant" {
for_each = {
for grant in var.grant_on_object_to_access_role : grant.name => {
warehouse_name = grant.parameter.warehouse_name
privilege = grant.parameter.privilege
roles = grant.roles
}
if grant.type == "WAREHOUSE"
}
warehouse_name = each.value.warehouse_name
privilege = each.value.privilege
roles = each.value.roles
# 先に存在しなければならないが、参照していないので依存関係を付ける
depends_on = [snowflake_role.roles]
}
grant_access_roles_to_functional_roles.tf で Access role を Functional role に付与します。
# Access role を Functional role に grant する
resource "snowflake_role_grants" "access_role_to_functional_role_grants" {
for_each = {
for grant in var.grant_access_roles_to_functional_roles : grant.access_role => {
access_role = grant.access_role
functional_roles = grant.functional_roles
}
}
role_name = snowflake_role.roles[each.value.access_role].name
roles = each.value.functional_roles
}
grant_functional_roles.tf で、Functional role をユーザーに付与します。
# Access role を Functional role に grant する
resource "snowflake_role_grants" "access_role_to_functional_role_grants" {
for_each = {
for grant in var.grant_access_roles_to_functional_roles : grant.access_role => {
access_role = grant.access_role
functional_roles = grant.functional_roles
}
}
role_name = snowflake_role.roles[each.value.access_role].name
roles = each.value.functional_roles
}
これで、Functional role と Access role を作成する準備が整いました。
Terraform で作ってみよう
今回は、次の状況を仮定して、ロールを作ってみます。
- ・Snowflake 環境を利用するユーザーは「HOGE社」または「PIYO社」のいずれかに所属しています。
- ・HOGE社 所属のユーザーは開発担当者または分析担当者です。PIYO社 所属のユーザーは分析担当者のみです。
- ・分析担当者はテーブルをSELECTできればOKで、開発担当者は必要に応じてデータを更新・修正するために書き込み権限が必要です。
ビューやステージも作りたくなりそうですが、簡単のために省略させてください。 - ・SCHEMA_A は、PIYO社 からは閲覧不可です。
以上の前提で、ロールを Functional role と Access role で設計してみましょう。 こんな感じにします。
Functional role
Access role
それではさっそく作ってみましょう!上のロールを設定ファイルに記載していきます。
access_roles.yml
access_roles:
- name: PRD_SCHEMA_A_READWRITE
comment: 本番環境データベースで SCHEMA_A スキーマに対して読み書き可能
- name: PRD_SCHEMA_A_READONLY
comment: 本番環境データベースで SCHEMA_A スキーマに対して読み取り可能
- name: PRD_SCHEMA_B_READWRITE
comment: 本番環境データベースで SCHEMA_B スキーマに対して読み書き可能
- name: PRD_SCHEMA_B_READONLY
comment: 本番環境データベースで SCHEMA_B スキーマに対して読み取り可能
- name: PRD_WAREHOUSE_USAGE
comment: 本番環境用ウェアハウスを使用可能
grant_on_object_to_access_role:
- name: DATABASE_PRD_DB_USAGE
type: DATABASE
parameter:
database_name: PRD_DB
privilege: USAGE
roles:
- PRD_SCHEMA_A_READWRITE
- PRD_SCHEMA_A_READONLY
- PRD_SCHEMA_B_READWRITE
- PRD_SCHEMA_B_READONLY
- name: SCHEMA_PRD_DB_SCHEMA_A_USAGE
type: SCHEMA
parameter:
database_name: PRD_DB
schema_name: SCHEMA_A
privilege: USAGE
roles:
- PRD_SCHEMA_A_READWRITE
- PRD_SCHEMA_A_READONLY
- name: SCHEMA_PRD_DB_SCHEMA_B_USAGE
type: SCHEMA
parameter:
database_name: PRD_DB
schema_name: SCHEMA_B
privilege: USAGE
roles:
- PRD_SCHEMA_B_READWRITE
- PRD_SCHEMA_B_READONLY
- name: TABLE_PRD_DB_SCHEMA_A_SELECT_ON_FUTURE
type: TABLE
parameter:
database_name: PRD_DB
schema_name: SCHEMA_A
privilege: SELECT
on_future: true
roles:
- PRD_SCHEMA_A_READWRITE
- PRD_SCHEMA_A_READONLY
- name: TABLE_PRD_DB_SCHEMA_A_INSERT_ON_FUTURE
type: TABLE
parameter:
database_name: PRD_DB
schema_name: SCHEMA_A
privilege: INSERT
on_future: true
roles:
- PRD_SCHEMA_A_READWRITE
- name: TABLE_PRD_DB_SCHEMA_A_UPDATE_ON_FUTURE
type: TABLE
parameter:
database_name: PRD_DB
schema_name: SCHEMA_A
privilege: UPDATE
on_future: true
roles:
- PRD_SCHEMA_A_READWRITE
- name: TABLE_PRD_DB_SCHEMA_A_DELETE_ON_FUTURE
type: TABLE
parameter:
database_name: PRD_DB
schema_name: SCHEMA_A
privilege: DELETE
on_future: true
roles:
- PRD_SCHEMA_A_READWRITE
access_roles_to_functional_roles.yml
grant_access_roles_to_functional_roles:
- access_role: PRD_SCHEMA_A_READWRITE
functional_roles:
- DEVELOPER_HOGE
- access_role: PRD_SCHEMA_A_READONLY
functional_roles:
- ANALYST_HOGE
- access_role: PRD_SCHEMA_B_READWRITE
functional_roles:
- DEVELOPER_HOGE
- access_role: PRD_SCHEMA_B_READONLY
functional_roles:
- ANALYST_HOGE
- ANALYST_PIYO
- access_role: PRD_WAREHOUSE_USAGE
functional_roles:
- DEVELOPER_HOGE
- ANALYST_HOGE
- ANALYST_PIYO
あとは terraform apply すれば、あっという間に Functional role と Access role が作成されます。
$ terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: :(中略) Plan: 36 to add, 0 to change, 0 to destroy. :(中略) Apply complete! Resources: 36 added, 0 changed, 0 destroyed.
terraform コマンドの裏側では、ひたすら grant plivilage と create role が実行されています。これを手打ちするのは、ちょっと大変じゃないですか?
作成されたロールを Snowsight で確認してみましょう。
DEVELOPER_HOGE
ANALYST_HOGE
ANAPYST_PIYO
先に記載した図と同じ階層関係になっていますね。
では、アクセス制御ができているか、確認しましょう。SCHEMA_A と SCHEMA_B にテーブルを作成します。
create table "PRD_DB"."SCHEMA_A"."A_TABLE_1" ( id int, name string ); create table "PRD_DB"."SCHEMA_B"."B_TABLE_1" ( id int, name string );
DEVELOPER_HOGE では、SCHEMA_A も SCHEMA_B も書き込み可能です。
ANALYST_HOGE では、SCHEMA_A も SCHEMA_B も閲覧可能です。
書き込みはできません。
ANALYST_PIYO では、 SCHEMA_A を閲覧することはできません。SCHEMA_B は閲覧可能です。
修正もしてみよう
これで HOGE社 と PIYO社 のためのロールは作成できました。しかし、現実に Snowflake 環境の運用をしていると、ロールの追加や修正が必要な場面が出てくるのではないでしょうか。
例えば「FUGA社」から分析者が新たに参加し、SCHEMA_A と SCHEMA_B の両方を閲覧する必要があるとします。この場合、FUGA社 分析者用の Functional role を追加し、既存の Access role を
FUGA社 用ロールに紐づければOKです。
FUGA社 分析者用ロールは ANALYST_FUGA としましょう。
Functional role
設定ファイルを次のように修正します。
functional_roles.yml
functional_roles:
- name: DEVELOPER_HOGE
comment: HOGE社に所属する開発者が使用するロール。全ての環境でテーブル作成・データ処理フロー作成などを行う
- name: ANALYST_HOGE
comment: HOGE社に所属する分析者が使用するロール。本番環境で集計などを行う
- name: ANALYST_PIYO
comment: PIYO社に所属する分析者が使用するロール。本番環境で集計などを行う
+ - name: ANALYST_FUGA
+ comment: FUGA社に所属する分析者が使用するロール。本番環境で集計などを行う
grant_functional_roles_to_user:
- role_name: DEVELOPER_HOGE
users:
- HOGE_USER_1
- role_name: ANALYST_HOGE
users:
- HOGE_USER_2
- role_name: ANALYST_PIYO
users:
- PIYO_USER_1
+ - role_name: ANALYST_FUGA
+ users:
+ - FUGA_USER_1
access_roles_to_functional_roles.yml
grant_access_roles_to_functional_roles:
- access_role: PRD_SCHEMA_A_READWRITE
functional_roles:
- DEVELOPER_HOGE
- access_role: PRD_SCHEMA_A_READONLY
functional_roles:
- ANALYST_HOGE
+ - ANALYST_FUGA
- access_role: PRD_SCHEMA_B_READWRITE
functional_roles:
- DEVELOPER_HOGE
- access_role: PRD_SCHEMA_B_READONLY
functional_roles:
- ANALYST_HOGE
- ANALYST_PIYO
+ - ANALYST_FUGA
- access_role: PRD_WAREHOUSE_USAGE
functional_roles:
- DEVELOPER_HOGE
- ANALYST_HOGE
- ANALYST_PIYO
+ - ANALYST_FUGA
あとは terraform apply すれば、ANALYST_FUGA の追加は完了です。
SCHEMA_A と SCHEMA_B 、両方とも閲覧できていますね。
おわりに
Functional role + Access role モデルの Terraform 実装例についてご紹介しました。
ロールの追加・修正を行う際、設定ファイルの修正で済むため、クエリでロールを追加・修正するよりも作業量が少なくなり、そこが良いなと感じています。最初に作成するコード量が少々多くなってしまうのが弱点ですが、後々、メンテナンス作業が楽になる事を考えれば、十分にメリットがあるのではないかと思います。
Terraform で Snowflake ともっと仲良くなりましょう!