SnowflakeのFunctional Role + Access Roleのロール設計をTerraformで実装してみる

## 背景と目的
データエンジニアを生業としているのですが、業務でSnowflakeを利用した基盤構築案件が増えてきているように感じます。
そこで今回は**Snowflake×Terraform**をテーマに、個人開発した内容について備忘録的に整理したいと思います。

#### Snowflakeについて独り言
DWHサービスに限らず、Snowflakeというプラットフォーム上でかなり多くのことが実現できるようになってきており、そしてそのサービス拡充のスピードに驚かされます。
私的に今後、Snowflakeのサービスを使って取り組んでみたいことをまとめた記事があります。ぜひ併せて読んでみてください。[2024下半期 これから取り組んでいきたいこと](https://qiita.com/s_rokuemon/items/126ca50fb3b0e553e806)

## 何をつくるか
[公式Doc](https://docs.snowflake.com/en/user-guide/security-access-control-considerations#aligning-object-access-with-business-functions)でも言及されている、**Functional Role + Access Roleのロール設計を実現するTerraformの構成**を本記事でまとめてみます。

本書の構成はBeforeとAfterとし、**BeforeではTerraformでのみロールを実装してみます**。**Afterでは同じ構成をTerraformとYamlを利用し実装します。**
* 変数の設定や作成するリソースの定義等をYamlで管理することで、コードの読みやすさや開発の柔軟性の良さを感じたい

#### Functional Role + Access Roleのロール設計
図は下記のリンク先からの引用です。Functional Role + Access Roleというレイヤを持たせることで、ユーザに割り当てるロールを微細に制御することが可能になります。
ただし、ロールの数がかなり増えるためその管理の仕方が、難しさの一つだと感じています。
[A Functional Approach For Snowflake’s Role-Based Access Controls](https://docs.snowflake.com/en/user-guide/security-access-control-considerations#aligning-object-access-with-business-functions)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833399/b36dfe49-7747-f1ae-27a3-ea229df11147.png)

### 動作環境
``` sh 
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.6 LTS (Focal Fossa)"

$ cat provider.tf
terraform {
   required_version = "~> 1.6.0"
   required_providers {
     snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "0.64.0"
     }
   }
}
```
### ディレクトリ構成
beforeとafterのディレクトリ構成の違いは、common（Roleの組み合わせを定義するyamlを格納）の有無と、yamlの中身を変数として受け取るlocals.tfの有無だけです。
``` tree
$ tree
.
├── after
│   ├── common
│   │   └── yaml
│   │       ├── access_roles.yml
│   │       ├── access_roles_to_functional_roles.yml
│   │       └── functional_roles.yml
│   ├── locals.tf
│   ├── main.tf
│   ├── provider.tf
│   └── variables.tf
│   └── module
│      	├── grant_access_role
│      	│   ├── main.tf
│      	│   ├── provider.tf
│      	│   └── variables.tf
│      	├── grant_dunctional_role_to_user
│      	│   ├── main.tf
│      	│   ├── provider.tf
│      	│   └── variables.tf
│      	├── grant_functional_role
│      	│   ├── main.tf
│      	│   ├── provider.tf
│      	│   └── variables.tf
│      	├── roles
│      	│   ├── main.tf
│      	│   ├── provider.tf
│      	│   └── variables.tf
│      	└── users
│      	    ├── main.tf
│      	    ├── provider.tf
│      	    └── variables.tf
├── before省略
```

## before

### 苦労・大変だった点
ソースにコメントアウトしてある内容と同じですが、以下の点で苦労しました。
* Access Roleに対するgrantについて
    * Access Role毎にprivilegeが異なる場合、オブジェクトに対してgrantするresourceを何度も定義する必要がある点。冗長な記述になってしまう。
```terraform:grant_access_role
# Read付与：AccessRoleの_R, _RW両方
# roleごとに、どのDB,schema, tableに対するPrivilageを付与するか、を外部ファイルで管理できれば楽そう
resource "snowflake_table_grant" "on_table_grant_read" {
    provider = snowflake.sysadmin
    for_each = {for k,v in local.filtered_map_access_role: k=>v}
    database_name = "SNOWFLAKE_FOR_TF"
    schema_name   = "SCHEMA_A"
    privilege     = "SELECT"
    table_name = "T_SNOWFLAKE_FOR_TF"
    roles = [each.key]
}

# ReadWrite付与：AccessRoleの_RWのみ
resource "snowflake_table_grant" "on_table_grant_read_write" {
    provider = snowflake.sysadmin
    for_each = {for k in local.read_write_keys: k=> local.filtered_map_access_role[k]}
    database_name = "SNOWFLAKE_FOR_TF"
    schema_name   = "SCHEMA_A"
    privilege     = "INSERT"
    table_name = "T_SNOWFLAKE_FOR_TF"
    roles = [each.key]
}
```
* Access RoleとFunctional Roleの紐づけ
    * 複数あるAccess RoleをどのFunctional Roleにぶら下げるのか、という紐づけの管理が複雑になる点。ここが一番苦労した。。。
```terraform:grant_functional_role
locals {
    filtered_map_access_role = {for key,val in var.roles_name_descript_dict : key=>val if startswith(key, "access_role_")}
    filtered_map_functional_role = {for key,val in var.roles_name_descript_dict : key=>val if startswith(key, "func_role_")}
    
    read_write_keys = [for k in keys(var.roles_name_descript_dict) : k if can(regex("_RW", k))][0]
    read_keys = [for k in keys(var.roles_name_descript_dict) : k if can(regex("_R", k))][0]

    # Access role を Functional role に grant する
    # develop: RW, analyst: R, consul: R
    combine_functional_and_access_role = {
        for key in keys(var.roles_name_descript_dict) :
        key => (
            key == "func_role_div_developer" ? local.read_write_keys:
            key == "func_role_div_analyst" ? local.read_keys:
            key == "func_role_div_consultant" ? local.read_keys:
            null
        )
        if can(regex("func_role_div_", key))
    }
}
```
## after
### beforeとの比較
* Access Roleに対するgrantについて
    * beforeでは、ロールごとにテーブルに対するprivilegeを定義する必要があり、コードが冗長だった"grant_access_role"は"snowflake_table_grant"リソース1つで記述できるようになりました。
``` terraform: grant_access_role
# table
resource "snowflake_table_grant" "on_table" {
    provider = snowflake.sysadmin
    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    = grant.parameter.table_name
        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
}
```
* Access RoleとFunctional Roleの紐づけ
    * 同様に、Functional RoleとAccess Roleの組み合わせを実装するコード部分は下記のようにシンプルに記述できるようになりました。Role同士の組み合わせをYAMLで定義しておくことで、main.tfファイルの中で変数として受け取りForLoopするだけのコードになりました。
```terraform: grant_functional_role
# Access role を Functional role に grant する
resource "snowflake_role_grants" "access_role_to_functional_role_grants" {
    provider = snowflake.securityadmin
    for_each = {
        for grant in var.grant_access_role_to_functional_role : grant.access_role => {
            access_role      = grant.access_role
            functional_roles = grant.functional_roles
        }
    }
    role_name = each.value.access_role
    roles     = each.value.functional_roles
}
```

## 作り終えて
冗長なコードの削減、それによって読みやすさの向上、メンテナス性・再利用性がかなり向上する結果となったと思います。

## スペシャルサンクス
参考にさせていただいたリンク集です。ありがとうございました！
* https://datumstudio.jp/blog/0131_terraform_snowflake_role_creating/#Terraform
* https://dev.classmethod.jp/articles/snowflake-terraform-design-with-functional-and-access-role/
* https://qiita.com/Ayumu-y/items/315b61c492bcc167d760