Snowflake 

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
    ├─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 ともっと仲良くなりましょう!

このページをシェアする:



DATUM STUDIOは、クライアントの事業成長と経営課題解決を最適な形でサポートする、データ・ビジネスパートナーです。
データ分析の分野でお客様に最適なソリューションをご提供します。まずはご相談ください。