❄️

Terragrunt で始めるマルチアカウント Snowflake 環境構築

2024/07/01に公開

はじめに

以前より Snowflake の Terraform Provider (Snowflake-Labs/snowflake) が公式にサポートされることが発表されており、2024 年に入って Roadmap も公開されました。現状抱えている課題も徐々に解消され、今後より使いやすくなっていくものと思います。

当社においても先日、技術検証のため Snowflake のトライアルアカウントを開設してみました。せっかく何もリソースがない綺麗な環境を手に入れたので、最初から可能な限り Terraform を利用して、ある程度統制の利いた環境構築をしていきたいと考えています。

ということで今回は、Terraform と Terragrunt を使用してマルチアカウントの Snowflake 環境を構築する方法について検討してみたので、その内容についてご紹介したいと思います。

Terragrunt とは?

Terragrunt とは、Gruntwork 社が開発する Terraform の Wrapper ツールです。tfstate 管理に関するバックエンド定義の記述を一箇所に集約できたり、同一モジュールの環境間の複製も簡単にできたりと、当社でもとても便利に利用しています。

Terragrunt については以前の記事で詳しく取り扱っているため、良ければ併せてご覧ください。

https://zenn.dev/simpleform/articles/20221111-01-terraform-with-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 (例)
./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 (例)
./common_vars.yaml
organization_name: mycompany
account_vars.yaml (例)
./analytics/stg/account_vars.yaml
backend_aws:
  account_id: "000011112222"
  region: ap-northeast-1

account:
  name: analytics_stg
  type: analytics
  env: stg
default_profile.yaml (例)
./analytics/stg/default_profile.yaml
snowflake:
  user: "testuser01"
  role: "terraform_administrators"

使用するプロファイル決定の仕組み

Terraform から Snowflake 環境を操作するにあたり、認証情報を渡すが必要になります。いくつか方法がありそうですが、今回は profile を指定する方法を試したいと思います。

Snowflake のプロファイル設定ファイルを ~/.snowflake/config として作成し、必要なプロファイルを複数設定しておくことができます。

~/.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 (親) を以下のように記述してみます。

./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 から情報を取得します。

./analytics/stg/account_vars.yaml
backend_aws:
  account_id: "000011112222"
  region: ap-northeast-1

account:
  name: analytics_stg  # ここからアカウント名を取得
  type: analytics
  env: stg

ユーザー・ロールについては、ユーザー毎に設定すべき値が異なるため、default_profile.yaml などに自身がデフォルトで使用するユーザー・ロールを記述しておきます。

./analytics/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 を使用します。

./analytics/stg/role/terraform/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 であれば warehousesnowflake_grant_account_role であれば grant_account_role が入るようなイメージです。
{usage} には、用途に応じて任意の名称を設定します。

Account の実装例

サインアップ時に最初に作成される ORGADMIN アカウントでは、主にアカウント作成・管理用途での利用を想定しています。account モジュールと envs 側定義の実装例を以下に示します。

account モジュールを構成するファイル群の実装例は以下の通りです。

variables.tf
./modules/orgadmin/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
./modules/orgadmin/account/ 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
./modules/orgadmin/account/ 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 アカウントが作成され、初回サインイン時にパスワード変更が求められます。

./envs/orgadmin/account/ terragrunt.hcl (子)
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
./modules/analytics/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
./modules/analytics/warehouse/default/ 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
./modules/analytics/warehouse/default/ 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 が作成されます。

./envs/analytics/stg/warehouse/default/ terragrunt.hcl (子)
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 などの他ツールで管理すべきか」といった勘所をまだあまり掴めていないため、この辺りは機会があればまたアウトプットしてみたいと思います。

最後まで読んで頂き、ありがとうございました。

参考

脚注
  1. トライアルアカウント - Snowflake Documentation ↩︎

  2. アカウントの ORGADMIN ロールの有効化 - Snowflake Documentation ↩︎

  3. 多要素認証(MFA) - Snowflake Documentation ↩︎

  4. Trust Center - Snowflake Documentation ↩︎

  5. 組織の名前の変更 - Snowflake Documentation ↩︎

SimpleForm Tech Blog

Discussion