🥳

Terraformを使った社内Snowflake Sandbox環境構築

2021/04/25に公開

記事の内容3行で

  1. Terraformを使って
  2. 社内のSnowflake環境に
  3. 楽にユーザ初期セットを構築したい

ソースコード:
https://github.com/yt1n4/terraform-snowflake-sandbox/blob/main/README.md
※諸々マスクしてからgitに移動させたので、どこかでちゃんと確認&メンテします。

最終的にvariables.tfへ、追加したいユーザのメールアドレスを追記し、消したいユーザをvariables.tfからコメントアウト/削除すれば良い感じになりました。

TODO

SSOを導入して更に管理しやすいように(いつか記事書きます)

やりたいこと

メールアドレスをベースに

  • Snowflakeユーザ作成
  • 専用DB作成
  • 専用WH作成
  • それらリソースを触れるような権限設定
    がしたい。
    ただ、人の出入りに応じて手動でやりたくない。ということで snowflake を terraforming したいと思います。

構成

構成はこんな感じにしています。

.
├── Dockerfile
├── README.md
├── docker-compose.yml
├── main.tf
├── modules
│   ├── aws
│   └── snowflake
│       ├── account
│       │   └── account.tf
│       ├── database
│       │   └── empty
│       ├── preset_resource
│       │   ├── main.tf
│       │   ├── output.tf
│       │   ├── provider.tf
│       │   └── variables.tf
│       ├── preset_user
│       │   ├── main.tf
│       │   ├── output.tf
│       │   ├── provider.tf
│       │   └── variables.tf
│       └── role
│           └── empty
├── output.tf
├── resource_monitor
│   ├── main.tf
│   ├── output.tf
│   ├── provider.tf
│   └── variables.tf
├── snowflake_provider.tf
├── system_profile.tf
├── terraform.tfstate.backup
├── variables.tf
└── .terraform_version

terraform入門したてなので、terraformのベストプラクティスは他を参考にしていきたい所存
やっていることは、 terraform-provider-snowslake の中から必要なものを実装していっているだけですが、かいつまんで残してみます。
折角terraformingするので、ユーザ分のDBやWHの定義を行わずfor文を用いて処理することで、ユーザの増減についてのオペレーションコストを低減するようにしています。
また、自動スクリプト用に ACCOUNTADMIN ロールを使用しないにもあるように、ユーザ作成という強めの権限が必要なパターンとそうでないパターンを、multi provider 機能を用いて使い分けています。
これは、SYSADMIN権限で作成されたDBやWH等のリソースは、リソースに対するアクセス権限やOWNERSHIP権限を譲渡されなくても自由に触れるという権限設定がされているため、運用的にもユーザ的にも楽になると思い、その方向性に倒しています。

自動スクリプト用に ACCOUNTADMIN ロールを使用しない

自動スクリプトには ACCOUNTADMIN 以外のロールを使用することをお勧めします。推奨されているように、SYSADMIN ロールの下にロール階層を作成する場合、SYSADMIN ロールまたは階層内の下位のロールを使用して、すべてのウェアハウスおよびデータベースオブジェクト操作を実行できます。発生する唯一の制限は、ユーザーまたはロールの作成または変更です。これらの操作は、SECURITYADMIN ロールまたは十分なオブジェクト権限がある別のロールがあるユーザーが実行する必要があります。
https://docs.snowflake.com/ja/user-guide/security-access-control-considerations.html#avoid-using-the-accountadmin-role-for-automated-scripts

なので、
ユーザ作成、権限付与、リソースモニター -> ACCOUNTADMIN
DBやWAREHOUSE作成 -> SYSADMIN
と provider を使い分けています。

provider

terraformのsnowflakeプラグインを使っています。
https://registry.terraform.io/providers/chanzuckerberg/snowflake/latest
(このchanzuckerbergさんってあのchan zuckerbergさんなんですかね)

また、権限(とかサービス)毎に provider を定義して使いたいので、terraformのalias機能を使用しています。
https://www.terraform.io/docs/language/providers/configuration.html

providerに設定している region ですが、
https://zenn.dev/u2/articles/def9fdcf79a6b2
ここでハマった内容を書いていて、純粋にSnowflakeをどのクラウドプロバイダーのどのリージョンで使っているかを記載するものではなく、WebコンソールのURLに合わせて記載してください。
もし https://XXXXX.ap-northeast-1.aws.snowflakecomputing.com というURLになっていた場合は、regionに ap-northeast-1.aws を設定してください。

snowflake_provider.tf
# terraform plugin
terraform {
  required_providers {
    snowflake = {
      source  = "chanzuckerberg/snowflake"
      version = "0.24.0"
    }
  }
}

# snowflake default sysadmin provider
provider "snowflake" {
  role = "SYSADMIN"
  region = ""
  private_key_path = var.snowlake_private_key_path
}

# snowflake sysadmin provider
# default providerにSYSADMINを設定しているが、可読性が上がったりしないかなと思い、sysadminのaliasを定義してみてます。
provider "snowflake" {
  alias  = "sysadmin"
  role   = "SYSADMIN"
  region = ""
  private_key_path = var.snowlake_private_key_path
}

# snowflake accountadmin provider
provider "snowflake" {
  alias  = "accountadmin"
  role   = "ACCOUNTADMIN"
  region = ""
  private_key_path = var.snowlake_private_key_path
}

これでproviderへのアクセスが、例えばACCOUNTADMINであれば

provider = snowflake.accountadmin

とかでできるようになりました。

ユーザ関連

トップディレクトリのvariableに発行管理したいユーザのメールアドレスリストを定義します。

/variables.tf
variable "users" {
  default = [
    "aaa@yyy.zzz",
    "bbb@yyy.zzz",
    "ccc@yyy.zzz",

    # "terraform" # システムユーザ
    # "dedadmin" # adminユーザ
  ]
}

今回は、 module/ に諸々をいれていくような構成にしているため、トップディレクトリのmainは、それらリソースに対してvariableで定義されたユーザリストや適切な provider を渡す役割だけこなします。
ユーザ作成は強い権限が必要なため、ACCOUNTADMINのproviderを渡していきます。

/main.tf
module "preset_user" {
  # ここで読み込みたい source
  source = "./modules/snowflake/preset_user/"
  
  # 上で読み込んだ階層にいる variables.tf に渡すための変数
  users  = toset(var.users)

  # 上で読み込んだリソースで使用するsnowflake provider
  # ACCOUNTADMIN用のproviderを指定
  providers = {
    snowflake = snowflake.accountadmin
  }
}

mainでmoduleの読み込みと変数を用意したら、次はmodule側です。

/modules/snowflake/preset_user/variables.tf
# ルートのmainから呼ばれ、そこで定義された users の変数をここで受け取る
variable "users" {}

mainの中では、usersとして定義されたメールアドレスリストを for_each とかその他Functionを使って適切な形に加工してます。
https://www.terraform.io/docs/language/meta-arguments/for_each.html
terraformの7不思議の一つなんですが、var.usersがリストで、そのリストをfor_each変数に格納することで、以降その要素に対してはeachでアクセス制御することができます。
例えば値の場合は
each.value
キーの場合は
each.key
みたいな感じです。

弊社では(というかどこでもだとは思うのですが)メールアドレスが {名前の頭文字}.{苗字}@ドメイン となっているため、 . がメタ文字として用いられることが多いのもあって不都合でないように、データベース名やウェアハウス名には ._(ドットからアンダーバー)に変換し、@の手前まででユニークになっているハズなのでトリムしています。

${replace(element(split("@", upper(each.value)), 0), ".", "_")}_WH"

後はSnowflakeの慣習(?)からUPPER CASEにしたりしています。
少し気持ち悪いのですが、snowflake_role_grantsrole_name でステータスが解決されてしまうためか、手動で作成したSYSADMIN権限を持つterraformをここに入れておかないと消えてしまうため追加しています。(何か別の回避策があったら知りたい)
上からsetで引き回しているため、一旦リストにしてからconcatしています。

snowflake_role_grants
toset(concat(tolist(var.users), ["terraform"]))

最終的にmainはこんな感じになると思います。

/modules/snowflake/preset_user/main.tf
resource "snowflake_user" "preset_user" {
  for_each = var.users

  name                 = each.value
  email                = each.value
  password             = "Datum_1234" # SSOを用いる場合不要
  must_change_password = true
  default_namespace = "${replace(element(split("@", upper(each.value)), 0), ".", "_")}_DB"
  default_warehouse = "${replace(element(split("@", upper(each.value)), 0), ".", "_")}_WH"
  default_role = "SYSADMIN"
}

resource "snowflake_role_grants" "preset_role_grants" {
  role_name = "SYSADMIN"
  users     = toset(concat(tolist(var.users), ["terraform"]))
  roles = [
    "ACCOUNTADMIN"
  ]

  depends_on = [
    snowflake_user.preset_user
  ]
}

データベース、ウェアハウス関連

ユーザ関連よりも考えることが少ないので、あまり躓くところはないハズ
initially_suspended = trueを設定しておくと、ウェアハウス作成時に止まった状態で出来上がるので少しお得なハズです。

トップのメインの呼び出し側

/main.tf
module "preset_resource" {
  source = "./modules/snowflake/preset_resource/"
  users  = toset(var.users)

  providers = {
    snowflake = snowflake.sysadmin
  }
}

データベースやウェアハウスはアクセス制御のし易さからSYSADMIN権限のterraformで作成したいので
snowflake = snowflake.sysadmin でアクセスできるproviderを指定しています。

本体のモジュール側

/module/snowflake/preset_resource/main.tf
resource "snowflake_warehouse" "preset_wh" {
  for_each = var.users

  name = "${replace(element(split("@", upper(each.value)), 0), ".", "_")}_WH"
  warehouse_size      = "xsmall"
  auto_suspend        = 150
  initially_suspended = true
}

resource "snowflake_database" "preset_db" {
  for_each = var.users

  name = "${replace(element(split("@", upper(each.value)), 0), ".", "_")}_DB"
}

後はリソースモニターを適当に設定すれば、最低限自由に触れるSandbox環境の完成です。
本当はSSOを有効にして代わりにパスワードを廃止しセキュアにしていきたいのですが、SSO周りで調査が足りず、また後日機会があれば記事にしてみたいと思います。

Discussion