Terragrunt で始めるマルチアカウント Snowflake 環境構築
はじめに
以前より Snowflake の Terraform Provider (Snowflake-Labs/snowflake) が公式にサポートされることが発表されており、2024 年に入って Roadmap も公開されました。現状抱えている課題も徐々に解消され、今後より使いやすくなっていくものと思います。
当社においても先日、技術検証のため Snowflake のトライアルアカウントを開設してみました。せっかく何もリソースがない綺麗な環境を手に入れたので、最初から可能な限り Terraform を利用して、ある程度統制の利いた環境構築をしていきたいと考えています。
ということで今回は、Terraform と Terragrunt を使用してマルチアカウントの Snowflake 環境を構築する方法について検討してみたので、その内容についてご紹介したいと思います。
Terragrunt とは?
Terragrunt とは、Gruntwork 社が開発する Terraform の Wrapper ツールです。tfstate 管理に関するバックエンド定義の記述を一箇所に集約できたり、同一モジュールの環境間の複製も簡単にできたりと、当社でもとても便利に利用しています。
Terragrunt については以前の記事で詳しく取り扱っているため、良ければ併せてご覧ください。
前提
ソフトウェアバージョン
本記事では、以下のソフトウェアバージョンを使用しています。
- Terraform ...
1.8.5
- Terrgrunt ...
0.57.1
- Snowflake Provider ...
0.92.0
ディレクトリ構成
筆者環境では、Git リポジトリの上位構造を以下のようにしていますが、少し冗長になってしまうため、以降に示すディレクトリ構造では terraform/snowflake/
を起点にするものとします。
(Root)
├── README.md
└── terraform/
├── aws/
├── ...
└── snowflake/ ★
Snowflake アカウント登録
サインアップ
サインアップページ より、$400 相当のクレジットが付与されたトライアルアカウントを登録できます。(2024/7/1 現在)
詳細は公式ドキュメント [1] をご確認ください。
組織アカウントの有効化
組織内で複数の Snowflake アカウントを運用する場合、ORGADMIN ロールが有効になっているアカウントで、その他のアカウントを作成していくことになります。
サインアップによって作成されたアカウントでは、デフォルトで ORGADMIN ロールは有効化されています。これは [Admin]
- [Accounts]
で表示されるアカウント一覧で、該当アカウントの ORGADMIN ENABLED
にチェックが入っているかどうかで確認できます。
アカウント一覧画面
ORGADMIN ロールは複数のアカウントで有効化でき、後から無効化することもできます。
詳細は公式ドキュメント [2] をご確認ください。
その他、最初にやっておくと良さそうなこと
- MFA を設定する [3]
- Trust Center を有効にする [4]
- コスト節約のため、Default Warehouse の Auto-Suspend を 60 秒にする
- Snowflake サポートに依頼して、組織名・組織管理アカウント名を変更してもらう [5]
実装
準備が整ったので、Terraform + Terragrunt の実装について解説していきます。
terraform/snowflake/
配下に、以下のようなディレクトリ・ファイル構成を作成します。
リポジトリ内のディレクトリ・ファイル構成
図の構成の概要は以下の通りです。
- 各種 Terraform モジュールは
modules/
に、各アカウントからモジュールを呼び出すためのterragrunt.hcl
(子) はenvs/
に配置することを想定した構成になっています。 - Terraform バックエンド定義などに関する共通的な設定は
terraform/snowflake/
直下のterragrunt.hcl
(親) に記述します。 - 全アカウントで共通の設定は
common_vars.yaml
に、アカウント毎に固有の設定は各アカウントディレクトリ直下のaccount_vars.yaml
に記述します。
各種ファイルの実装例
図に記載している各種ファイルについて、実装例を以下に示します。
(terragrunt.hcl
については次節でも詳しく扱います)
terraform.tf (例)
terraform {
required_version = "1.8.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.55.0"
}
snowflake = {
source = "Snowflake-Labs/snowflake"
version = "0.92.0"
}
}
}
terragrunt.hcl (例)
locals {
common_vars = yamldecode(file(find_in_parent_folders("common_vars.yaml")))
account_vars = yamldecode(file(find_in_parent_folders("account_vars.yaml")))
profile = can(find_in_parent_folders("profile.yaml")) ? (
yamldecode(file(find_in_parent_folders("profile.yaml")))
) : (
yamldecode(file(find_in_parent_folders("default_profile.yaml")))
)
aws_account_id = local.account_vars.backend_aws.account_id
snowflake = {
profile = join("-", [
local.account_vars.account.name,
local.profile.snowflake.user,
local.profile.snowflake.role,
])
}
}
remote_state {
backend = "s3"
config = {
region = "ap-northeast-1"
bucket = "tfstate-example-bucket-${local.aws_account_id}"
key = "snowflake/${path_relative_to_include()}/terraform.tfstate"
dynamodb_table = "tfstate-example-table"
encrypt = true
}
generate = {
path = "_backend.tf"
if_exists = "overwrite_terragrunt"
}
}
generate "provider" {
path = "_provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
// Snowflake Provider
provider "snowflake" {
profile = "${local.snowflake.profile}"
}
EOF
}
generate "terraform" {
path = "_terraform.tf"
if_exists = "overwrite_terragrunt"
contents = file(find_in_parent_folders("terraform.tf"))
}
common_vars.yaml (例)
organization_name: mycompany
account_vars.yaml (例)
backend_aws:
account_id: "000011112222"
region: ap-northeast-1
account:
name: analytics_stg
type: analytics
env: stg
default_profile.yaml (例)
snowflake:
user: "testuser01"
role: "terraform_administrators"
使用するプロファイル決定の仕組み
Terraform から Snowflake 環境を操作するにあたり、認証情報を渡すが必要になります。いくつか方法がありそうですが、今回は profile を指定する方法を試したいと思います。
Snowflake のプロファイル設定ファイルを ~/.snowflake/config
として作成し、必要なプロファイルを複数設定しておくことができます。
[{profile_name}]
account='{organization_name}-{account_name}'
user='{user}'
password='{password}'
role='{role}'
Provider 定義においてプロファイル名を指定することで、対象アカウントに対して操作ができるようになります。
provider "snowflake" {
profile = "${profile_name}"
}
「アカウント名」「ユーザー」「ロール」の組合せが定まればプロファイルを一意に特定できそうなので、プロファイル名を {account_name}-{user}-{role}
の形式で持たせる想定で terragrunt.hcl
(親) を以下のように記述してみます。
locals {
account_vars = yamldecode(file(find_in_parent_folders("account_vars.yaml")))
// 上位ディレクトリを辿って "profile.yaml" が存在すれば "profile.yaml" を、
// 存在しなければ "default_profile.yaml" をプロファイル情報ソースとして使用する
profile = can(find_in_parent_folders("profile.yaml")) ? (
yamldecode(file(find_in_parent_folders("profile.yaml")))
) : (
yamldecode(file(find_in_parent_folders("default_profile.yaml")))
)
// プロファイル名を構成する ("{account_name}-{user}-{role}")
snowflake = {
profile = join("-", [
local.account_vars.account.name,
local.profile.snowflake.user,
local.profile.snowflake.role,
])
}
}
remote_state {
# 略
}
generate "provider" {
path = "_provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
// 構成したプロファイル名で Provider を定義する
provider "snowflake" {
profile = "${local.snowflake.profile}"
}
EOF
}
Snowflake プロファイル名を構成する要素のうち、{account_name}
についてはユーザー・ロールに関係なく環境毎に一意であるため、アカウント毎の設定を記述する account_vars.yaml
から情報を取得します。
backend_aws:
account_id: "000011112222"
region: ap-northeast-1
account:
name: analytics_stg # ここからアカウント名を取得
type: analytics
env: stg
ユーザー・ロールについては、ユーザー毎に設定すべき値が異なるため、default_profile.yaml
などに自身がデフォルトで使用するユーザー・ロールを記述しておきます。
snowflake:
user: "testuser01"
role: "terraform_administrators"
基本的には default_profile.yaml
のユーザー・ロールを使用すれば問題ありませんが、そもそも terraform_administrators
ロールを Terraform で作成するときなど、一時的に別のプロファイルを使用したいケースも出てくるのではないかと思います。
このようなケースに対応できるよう、必要に応じて profile.yaml
を作成することにしました。Terragrunt の find_in_parent_folders() 関数を使用して、profile.yaml
が見つかればこれをプロファイル情報ソースとして使用し、見つからなければ default_profile.yaml
を使用します。
snowflake:
user: "adminuser"
role: "useradmin"
モジュール分割方法
モジュールの分割方法やディレクトリの切り方については絶対的な正解があるわけではないと思いますが、筆者環境では以下のようなディレクトリ構成にしてみました。(あくまで一実装例として見て頂ければと思います)
snowflake
├── envs/
│ ├── analytics/
│ │ └── {env}/
│ │ └── {resource_type}/
│ │ └── {usage}/
│ │ └── terragrunt.hcl
│ └── orgadmin/
└── modules/
├── analytics/
│ └── {resource_type}/
│ └── {usage}/
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── orgadmin/
{resource_type}
には、例えば snowflake_warehouse であれば warehouse
、snowflake_grant_account_role であれば grant_account_role
が入るようなイメージです。
{usage}
には、用途に応じて任意の名称を設定します。
Account の実装例
サインアップ時に最初に作成される ORGADMIN アカウントでは、主にアカウント作成・管理用途での利用を想定しています。account
モジュールと envs 側定義の実装例を以下に示します。
account
モジュールを構成するファイル群の実装例は以下の通りです。
variables.tf
variable "accounts" {
type = map(object({
admin_name = string
admin_initial_password = string
email = string
first_name = string
last_name = string
must_change_password = optional(bool, true)
edition = string
comment = string
region = string
}))
}
main.tf
resource "snowflake_account" "default" {
for_each = var.accounts
name = each.key
admin_name = each.value.admin_name
admin_password = each.value.admin_initial_password
email = each.value.email
first_name = each.value.first_name
last_name = each.value.last_name
must_change_password = each.value.must_change_password
edition = each.value.edition
comment = each.value.comment
region = each.value.region
}
outputs.tf
output "accounts" {
value = {
for key, value in snowflake_account.default : key => {
id = value.id
name = value.name
}
}
}
envs 側の terragrunt.hcl
(子) では、以下のようにモジュールを呼び出せます。
これを terragrunt apply
すると ANALYTICS_STG
アカウントが作成され、初回サインイン時にパスワード変更が求められます。
include "root" {
path = find_in_parent_folders()
}
locals {
common_vars = yamldecode(file(find_in_parent_folders("common_vars.yaml")))
account_vars = yamldecode(file(find_in_parent_folders("account_vars.yaml")))
account = local.account_vars.account
}
terraform {
source = "${dirname(find_in_parent_folders())}/modules/${local.account.type}//account/"
}
inputs = {
accounts = {
"ANALYTICS_STG" = {
admin_name = "adminuser"
admin_initial_password = "PtgzQNAF?t@9bCs8ZRxB;mC" # initial
email = "first.last@example.com"
first_name = "First"
last_name = "Last"
edition = "ENTERPRISE"
comment = "analytics-stg"
region = "AWS_AP_NORTHEAST_1"
}
}
}
Warehouse の実装例
ORGADMIN ロールが有効ではない分析用アカウントでの実装例として、Warehouse (用途名: default
) を作成してみます。
モジュールを構成するファイル群の定義は、例えば以下のようになります。
variables.tf
variable "warehouses" {
type = map(object({
name = string
comment = optional(string, null)
warehouse_type = optional(string, "STANDARD")
warehouse_size = optional(string, "XSMALL")
min_cluster_count = optional(number, 1)
max_cluster_count = optional(number, 1)
max_concurrency_level = optional(number, 8)
scaling_policy = optional(string, "STANDARD")
resource_monitor = optional(string, null)
# Snowflake default for "auto_suspend" is 600 (10-min),
# but setting default to 60 (1-min) for cost savings.
auto_suspend = optional(number, 60)
auto_resume = optional(bool, true)
# Timeouts
statement_timeout_in_seconds = optional(number, 172800)
statement_queued_timeout_in_seconds = optional(number, 0)
}))
}
main.tf
resource "snowflake_warehouse" "default" {
for_each = var.warehouses
name = each.value.name
comment = each.value.comment
warehouse_type = each.value.warehouse_type
warehouse_size = each.value.warehouse_size
max_cluster_count = each.value.max_cluster_count
min_cluster_count = each.value.min_cluster_count
scaling_policy = each.value.scaling_policy
resource_monitor = each.value.resource_monitor
auto_suspend = each.value.auto_suspend
auto_resume = each.value.auto_resume
# Statement Timeout
statement_timeout_in_seconds = each.value.statement_timeout_in_seconds
statement_queued_timeout_in_seconds = each.value.statement_queued_timeout_in_seconds
}
outputs.tf
output "warehouses" {
value = {
for key, value in snowflake_warehouse.default : key => {
id = value.id
name = value.name
}
}
}
envs 側の terragrunt.hcl
(子) では、以下のようにモジュールを呼び出せます。
これを terragrunt apply
すると、default_wh_xsmall
, default_wh_small_snowpark
の 2 つの Warehouse が作成されます。
include "root" {
path = find_in_parent_folders()
}
locals {
common_vars = yamldecode(file(find_in_parent_folders("common_vars.yaml")))
account_vars = yamldecode(file(find_in_parent_folders("account_vars.yaml")))
account = local.account_vars.account
usage = "default"
}
terraform {
source = "${dirname(find_in_parent_folders())}/modules/${local.account.type}/warehouse/${local.usage}/"
}
inputs = {
warehouses = {
xsmall = {
name = "${local.use}_wh_xsmall"
}
small_snowpark = {
name = "${local.use}_wh_small_snowpark"
warehouse_type = "SNOWPARK-OPTIMIZED"
}
}
}
実装に関する説明は以上です。
さいごに
Terraform + Terragrunt を使用した Snowflake 環境構築の始め方について書いてみました。
アカウントを開設して色々とリソースを作ってしまった後に、途中から Terraform 管理に移行するというのはなかなか大変な作業ではないかと思います。(筆者は AWS 環境でこれを経験しましたが、リソース量が膨大でかなりしんどかった記憶があります)
本記事がこれから Snowflake を始める方の参考になれば幸いです。
触りたてということもあり、「どこまでを Terraform で管理して、どこから dbt などの他ツールで管理すべきか」といった勘所をまだあまり掴めていないため、この辺りは機会があればまたアウトプットしてみたいと思います。
最後まで読んで頂き、ありがとうございました。
参考
- SnowflakeとTerraformで作るデータ基盤入門 - Zenn (Book)
- 2024年、Snowflake Terraformがこう生まれ変わる! - Zenn
- SnowflakeでFunctional Role+Access Roleのロール設計を実現するTerraformのModule構成を考えてみた - DevelopersIO
- Terraform無しでSnowflakeを始めちゃった人へのTerraform導入ガイド - Zenn
- redashからSnowflakeを参照したらクレジット消費が異常に増えたお話 - Zenn
-
トライアルアカウント - Snowflake Documentation ↩︎
-
アカウントの ORGADMIN ロールの有効化 - Snowflake Documentation ↩︎
-
多要素認証(MFA) - Snowflake Documentation ↩︎
-
Trust Center - Snowflake Documentation ↩︎
リアルタイム法人調査システム「SimpleCheck」を開発・運営するシンプルフォーム株式会社の開発チームのメンバーが、日々の開発で得た知見や試してみた技術などについて発信していきます。 Publication 運用への移行前の記事は zenn.dev/simpleform からご覧ください。
Discussion