Terraform無しでSnowflakeを始めちゃった人へのTerraform導入ガイド

2022/12/17に公開

この記事は何

TerraformでSnowflakeを管理する環境を構築することなしに、Snowflakeの環境を構築し始めちゃった人、いませんか?
この記事ではそんな人に向けて、Snowflake Terraform Providerの導入の仕方について書きます。
以下のような方に向いている記事です。

  • Terraform初学者だが、SnowflakeをTerraformで管理していきたい人
  • 現在のSnowflake環境をCICDしていきたい人

Terraformとは

改めて言うまでも無いかもしれませんが、Terraform(以下tf)とは、IaC(Infrastructure as Code)のオープンソースライブラリです。雑に言えば、tfのコードを書くことでインフラ環境のリソースの作成・削除が可能になります。
主にAWSなどのパブリッククラウドの環境構築に使われていますが、Snowflakeの各種リソース(ウェアハウスやユーザーなど)もtfを用いて記述することができます。tfではリソース管理のために宣言的な言語を採用しているため、tfのコードは常に現在のインフラ状態と一致するようになっています。

日本語の素晴らしい解説本です:
https://zenn.dev/mnagaa/books/3d668d2dfc657e

公式のチュートリアルです:
https://quickstarts.snowflake.com/guide/terraforming_snowflake/index.html

この記事では、tf初学者を対象に書いているので、tfの使い方についても解説しながらSnowflake環境の構築を進めていきます。

とりあえずウェアハウスを作ってみよう

百聞は一見にしかずということで、tfを使ってSnowflakeのウェアハウスを作成してみようと思います。

Terraformの導入

まず、tfをローカル環境にセットアップしましょう。
インストールする方法には、以下の記事にわかりやすくまとまっています。
https://zenn.dev/mnagaa/books/3d668d2dfc657e/viewer/22642e

ちなみに、tf自体のバージョン管理ツールtfenvを利用すると、より管理しやすいです。
https://github.com/tfutils/tfenv
tfenvの使い方はこちらの記事も詳しいです。

Snowflakeに接続する設定を記述

まず、tfのファイルを置くためのディレクトリを作成します。

mkdir terraform
cd terraform

作成したディレクトリ配下に、最初のtfファイルを作成します。.tfという拡張子をつける必要があります。

touch provider.tf

ちなみに、お使いのエディタにtfのLanguage Serverや拡張が入っていない場合は入れましょう。

作成したファイルに以下のように記述します。

provider.tf
terraform {
  required_version = "~> 0.14.11"
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "~> 0.53"
    }
  }
}

provider "snowflake" {
  region    = "ap-northeast-1.aws"
}

さらに、環境変数に、以下のようなキーで値を設定してください。

.envrc
export SNOWFLAKE_ACCOUNT=<お使いのSnowflakeアカウントロケータ>
export SNOWFLAKE_USER=<tfからSnowflakeに使うのに使うユーザ>
export SNOWFLAKE_PASSWORD=<上記のユーザのパスワード>
export SNOWFLAKE_ROLE=<Snowflake上の各リソースを作成するのに使うロール>

アカウントロケータはSnowSight上で、select select lower(current_account());の実行結果を設定してください。なお、上記の例ではSnowflakeをホストしているリージョンはAWS東京リージョンap-northeast-1.awsを指定していますが、もしそれ以外のクラウド・地域の場合は適宜変更してください。
また、ロールについては、tfで各種リソースを作成する権限があるロールでないといけないので、とりあえず試す場合、SYSADMINがおすすめです。

では、この状態でtfの初期化をします。

terraform init

成功した場合、いくつかのフォルダ・ファイルが作成されたと思います。
もしtfのバージョンが適合しないエラーが出る場合は、provider.tf内のrequired_version = "~> 0.14.11"という箇所を、使っているtfのバージョンと一致するように修正するか、tfのバージョンを上げてください。

provider.tf内の各ブロックについて説明します。まず、terraformブロック内はtfの基本設定などを記述します。1階層目のrequired_versionはtf自体のバージョンを指定します。required_providersブロックでは、tf内で必要なプロバイダの指定をします。

terraform {
  required_version = "~> 0.14.11"
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "~> 0.53"
    }
  }
}

terraformブロックの詳細は以下を参照してください。
https://developer.hashicorp.com/terraform/language/settings

providerブロックでは、プロバイダごとの必要な設定項目を指定しています。

provider "snowflake" {
  region    = "ap-northeast-1.aws"
}

プロバイダとは、接続先との接続・リソースの生成削除を行なってくれる存在と思っていればとりあえずOKです。AWSやGCPなどのパブリッククラウドだけでなくさまざまなプロバイダが存在します。

Snowflake用のプロバイダの設定は非常にシンプルに見えますが、本来はより多くの設定項目を設定可能です。ここでリージョンのみを指定しているのは、他の設定項目は環境変数から自動セットしてくれているためです。公式ドキュメントにも記載があるように、SNOWFLAKE_XXXという環境変数を設定しておけばプロバイダ設定で利用してくれます。

なお、上の例では、User/Password形式で接続していますが、それ以外の接続形式を利用する方が一般的には望ましいでしょう。前述のチュートリアルでは、Key形式で設定しています。

複数人での開発に耐えるようにする

プロバイダの設定は基本的に出来ましたが、このままでは複数人での開発時に懸念が生じます。tfでは、状態管理のためのファイルtfstateを作成します。tfstateファイルには、前回terraform applyされた後のインフラの状態が保管されており、terraform planをすると、tfstateファイルと、tfファイルの差分をチェックします。
デフォルトでは、tfstateファイルはローカルに作成されます。おそらく、ここまでの手順を辿ってきた方であれば、ローカルにterraform.tfstateというファイルがあると思います。
複数人が同時にterraformの開発をおこなっていた場合、誰かが本番環境にterraform applyした場合、実行者のtfstateファイルのみが最新のインフラの状態に一致しますが、それ以外の人のtfstateファイルは古い状態のままになります。このままterraform planした場合、古いインフラ状態と比較することになり、意図しない結果を生じる可能性があります。

そこで、tfではtfstateファイルで複数人で共通化するためにRemote Backendという機能を提供しています。

https://developer.hashicorp.com/terraform/language/settings/backends/configuration

この記事では、AWS S3に共通のtfstateファイルを置くようにprovider.tfに設定を追記します。

provider.tf
terraform {
  required_version = "~> 0.14.11"
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "~> 0.53"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
    # このブロックを追加
  backend "s3" {
    bucket         = "snowflake-terraform-tfstate"
    region         = "ap-northeast-1"
    key            = "terraform.tfstate"
    encrypt        = true
    dynamodb_table = "terraform_state_lock"
  }
}

provider "snowflake" {
  region    = "ap-northeast-1.aws"
}

この設定例では、snowflake-terraform-tfstateバケットに、tfstateファイルを保管するように指定しています。tfstateファイルを置きたいバケットに適宜書き換えてください。
また、リモートのtfstateファイルが同時に参照・編集されないように、dynamoDBを用いたロックもあると望ましいため、dynamodb_tableプロパティでロックに利用するDynamoDBテーブルを指定しています。
S3とDynamoDBの詳しい設定方法は以下などを参考にしました。
https://qiita.com/tsukakei/items/2751e245e38c814225f1

S3とDynamoDBが用意出来たら、もう一度

terraform init

を実行します。うまくいけば、ローカルからtfstateファイルが消え、S3のバケット内にtfstateファイルが生成されていると思います。

なお、backendは内部的にはAWS CLIを利用して接続しています。そのため、AWS CLIを利用する際にMFAが必要だったりプロファイルを変えたい場合には、上記の初期化が失敗してしまいます。そういった場合には、AWSVaultを利用した解決法がシンプルです。以下の記事を参考に設定してみてください。
https://dev.classmethod.jp/articles/terraform-assumerole/

ウェアハウスを作ってみよう

さて、プロバイダの設定が終わったので、ついに、リソースを作成することができる準備が整いました。最初にウェアハウスを作るため、ファイルを作成します。

touch warehouses.tf
warehouses.tf
resource "snowflake_warehouse" "tf_demo" {
  name           = "TF_DEMO"
  warehouse_size = "large"
  auto_suspend = 60
}

tfでは、resourceブロックを用いてリソースを宣言します。基本的な構文は以下の通りです。<resouce type>には、各プロバイダが提供するリソースタイプの名前が入ります。<resouce name>には、そのリソースにつけたい名前を指定します。この名前は識別子として使用されるのため、同一モジュール内で同じ名前のリソースを複数作成することはできません。詳しい使い方は公式ドキュメントを確認してください。

resource <resouce type>.<resouce name> {
  # resouce typeごとに設定できるプロパティが異なる
}

Snowflakeプロバイダで設定できるリソースタイプの一覧は、公式ドキュメントを参照してください。エンティティ(データベース、スキーマ、ウェアハウスなど)と、権限の二つに大別できます。

以下のコマンドを実行して、実行計画をチェックします。新しいリソースを1つ追加するという旨のメッセージが出ると思います。このコマンドを実行した時点ではウェアハウスは作成されていません。あくまで確認のみになります。

terraform plan

問題がなければ、実際に作成するために、以下のコマンドを実行します。

terraform apply

このコマンドが成功したら、SnowSightでshow warehousesをして、作成したウェアハウスが存在しているか確認してください。(もし出てこない場合は、tfでリソース生成に使うのに指定したロールを利用しているか確認してください。作成直後は、作成したロールとその上位ロールからしかリソースを参照できません。)

作成したウェアハウスの権限をつけてみよう

ウェアハウスを作成できたので、次はそのウェアハウスを使えるよう権限をつけてみます。

touch warehouses_grant.tf
warehouses_grant.tf
resource "snowflake_warehouse_grant" "tf_demo_exist_role_grant" {
  warehouse_name = snowflake_warehouse.tf_demo.name
  privilege = "USAGE"
  roles = ["EXIST_ROLE"] # 存在するロールに適宜変更してください
  with_grant_option = false
}

ここで注目したいのは、warehouse_name = snowflake_warehouse.tf_demo.nameです。実際のウェアハウス名を指定することもできるのですが、リソースのプロパティを参照することができます。このようにすることで、ウェアハウス名が変更されたとしても、自動的に権限が再アタッチされるようになっています。

terraform planで問題なければ、terraform applyして、SnowSight上から、ロールにUSAGE権限がついているか確認しましょう。

show grants on warehouse tf_demo;

権限の剥奪

さて、いずれこの権限を剥奪したいときがくるかもしれません。その場合、上記のtf_demo_your_role_grantリソースの記述を削除することで、自動的に権限を剥奪してくれます。便利ですね。

Terraform管理をしていない部分との競合

Snowflake環境が既に存在しており、これからtf管理へ移行しようと考えている場合、一時的にtf管理している部分とそうでない部分が混在することになります。リソースを作成・変更・削除する際に、どういった挙動になるでしょうか。
まず、warehouses.tfでウェアハウス名を既に存在しているウェアハウスに変更してみます。

warehouses.tf
resource "snowflake_warehouse" "tf_demo" {
  name           = "EXIST_WH" # 適宜、既に存在するウェアハウスを指定
  warehouse_size = "large"

  auto_suspend = 60
}

権限も付け直されるため、terraform planでは追加1,変更1,削除1になると思います。この場合、実際にterraform applyをしてみると、Object 'EXIST_WH' already exists.というエラーメッセージが出て失敗します。
エンティティのようなリソース(データベース、スキーマ、ロール、ユーザーなど)もおそらく同様の挙動となり、tf管理外のリソースをtfで上書きすることはできません。applyしてみるまで本当に成功するか分からない、という問題はあるとしても、tf管理外のリソースを意図せず変更してしまう危険性はなく安心ですね。

一方、権限については注意が必要です。まず、SnowSight上で、既存のウェアハウスへの権限をつけてみます。

grant usage on warehouse EXIST_WH to role EXIST_ROLE;

一方、warehouses_grant.tfで同様の権限を追加したとします。

warehouses_grant.tf
resource "snowflake_warehouse_grant" "tf_demo_exist_role_grant" {
  warehouse_name = snowflake_warehouse.tf_demo.name
  privilege = "USAGE"

  roles = ["EXIST_ROLE"]

  with_grant_option = false
}

# 以下を追記
resource "snowflake_warehouse_grant" "exist_wh_exist_role_grant" {
  warehouse_name = "EXIST_WH"
  privilege = "USAGE"

  roles = ["EXIST_ROLE"]

  with_grant_option = false
}

この状態のままterraform applyをすると、すんなりと実行できます。では、追記した部分を削除して再度applyしたあと、SnowSightで以下のSQLを実行してみると、USAGE権限が外れていることが確認できます。

show grants on warehouse EXIST_WH;

つまり、tf管理外で付与された権限であっても、tf内で削除することが出来てしまいます。この挙動は問題を引き起こす可能性があるため、snowflake_xxx_grantリソースでは、enable_multiple_grantsオプションを設定することができます。このオプションをtrueにすることで、tf管理外で付与された権限を意図せず剥奪してまうことを防ぐことが出来るようです。

ファイルからSQLを読み込んでタスクを作成する

さて、次は、タスクをtfで作成してみます。

touch task.tf

今回はサーバレスタスクを作ってみます。各プロパティについては公式ドキュメントを確認してください。

task.tf
resource "snowflake_task" "serverless_task" {
  comment = "my serverless task"

  database = "EXIST_DB" # 適宜変更してください。
  schema   = "EXIST_SCHEMA" # 適宜変更してください。

  name          = "SERVERLESS_TASK"
  schedule      = "10 MINUTE"
  sql_statement = file("./task.sql")
  
  enabled = false
}

sql_statementプロパティにはSQL文をそのまま記述することも出来ますが、別ファイルに切り出すことで、SQL文単体のlint/formattingがしやすくなったり、可読性が向上します。file()関数はtfの組み込み関数なので、tfファイル内のどこでも利用できます。
切り出した別ファイルに適当にSQL文を入れます。

task.sql
select 1
;

このままterraform applyすると、タスクがデプロイされていることが確認できます。tfでは、ファイルから読み込む機能が充実しています。今回のSQLは静的ですが、tfからSQL文に変数を入れ込むtemplatefile関数も便利かと思います。
https://developer.hashicorp.com/terraform/language/functions/templatefile

リソースが変更される場合と再生成される場合

tfで記述したリソースを変更した場合、terraform planすると、変更になる場合と作り替えになる場合があります。
たとえば、上のタスクのSQL文を変更した上でterraform planすると変更1になります。

task.sql
select 2
;

一方、タスク名を変更する場合は、追加1削除1(作り替え)となります。

task.tf
resource "snowflake_task" "serverless_task" {
  comment = "my serverless task"

  database = "EXIST_DB"
  schema   = "EXIST_SCHEMA"

  name          = "SERVERLESS_TASK_RENAME" # タスク名を変更してみる
  schedule      = "10 MINUTE"
  sql_statement = file("./task.sql")
  
  enabled = false
}

ALTER TASK文にタスクの名前を変更する構文が存在しないため、作り替えるしかないためです。tf上ではリソースの変更をしているだけに思ったとしても、実際には作り替えになる場合があるため注意が必要です。具体的には、権限管理がtf内で完結していない場合に意図せず権限が外れることがあります。

既存のリソースをTerraform内で参照する

既存のリソースをtf内で利用するには、既に見てきたようにリソース名を指定するだけで済みますが、実在しないリソースを指定していた場合、terraform planではエラーにならないのに、terraform applyしてみたらエラーになるという挙動になります。ちょっと悲しいですね。

tfにはData Sourceという機能があり、インフラ内のtf管理外のリソースを参照することができます。この機能を使って既存のリソースを参照できるようにしてみます。例としてウェアハウスを参照してみます。

touch data_sources.tf
data_sources.tf
data "snowflake_warehouses" "current" {
}
output "output" {
  value = data.snowflake_warehouses.current
}

この状態でterraform planすると、既存のウェアハウスの一覧が出力されます(outputブロックの中身が出力されています)。Data Sourceとして定義したものはdataオブジェクト経由でアクセス可能です。
"snowflake_warehouses"Data Sourceはアカウント内に存在する全てのウェアハウスを取得します。

出力された内容をよくみると、以下のようなフォーマットになっています。

Changes to Outputs:
  + output = {
      + id         = "xxxxxx.AWS_AP_NORTHEAST_1"
      + warehouses = [
          + {
              + comment        = "this warehouse is ..."
              + name           = "EXIST_WH"
              + scaling_policy = "STANDARD"
              + size           = "X-Small"
              + state          = "SUSPENDED"
              + type           = "STANDARD"
            }
	]
    }

公式ドキュメントに書かれているように、"snowflake_warehouses"Data Sourceはidwarehouseプロパティを持っています。今回はローカル変数経由でウェアハウスが存在するか検証できるようにしてみます。data_sources.tfを以下のように書き換えます。

data_sources.tf
data "snowflake_warehouses" "current" {
}

locals {
  warehouses = zipmap(
    data.snowflake_warehouses.current.warehouses[*].name,
    data.snowflake_warehouses.current.warehouses[*].name
    )
}

output "output" {
  value = local.warehouses.EXIST_WH
}

tfでは、localsブロック内でローカル変数を宣言することができます。このブロック内で宣言した変数はlocalオブジェクトを経由してアクセス可能です。今回はwarehouses変数に以下のような連想配列が入るようにzipmap関数を利用しています。

{"EXIST_WH": "EXIST_WH", ...}

tfには各種データタイプを操作する組み込み関数が多数用意されています。どんな関数があるかは公式ドキュメントで確認できます。

この状態で一度terraform planしてみてエラーが出なければ、このlocal変数をresource内で参照してみます。先ほど作成したサーバレスタスクを既存のウェアハウスで起動するように書き換えます。

task.tf
resource "snowflake_task" "task" {
  comment = "my task"

  database = "EXIST_DB"
  schema   = "EXIST_SCHEMA"
  warehouse = local.warehouses.EXIST_WH

  name          = "TASK"
  schedule      = "10 MINUTE"
  sql_statement = file("./task.sql")
  
  enabled = false
}

terraform planをすると正しく参照できていることが確認できます。

続けて、warehouse = local.warehouses.EXIST_WHEXIST_WHを実際に存在しないウェアハウス名に変えてみると、missing keyエラーで失敗するようになります。これで、terraform planは通るのに、terraform applyが失敗する、という事態を回避できるようになりました。

なお、この方法の欠点としては以下の点があります。

  • コード補完が効かない。local.warehouses変数がどんなキーを保持するかはterraform plan実行時にしか分からないため、コードを書いている際には補完してくれない。
  • 既存のリソースの一覧がコード内に記述されていないため、インフラ内にどんなリソースがあるかはoutputを利用したり、別途show warehousesをしなければ分からない。

この欠点を補うため、より明示的にウェアハウスを変数として宣言しておくのも悪くないと思います。

locals {
  warehouses = {"EXIST_WH": "EXIST_WH"}
}

こちらの方法の場合、本当にそのウェアハウスが存在するかをterraform plan時に別途チェックする必要があります。やり方はいろいろ考えられそうですが、あまり調べてはいないのでここでは踏み込まないことにします。

どうやって既存のリソースをTerraformに移行するか

既にSnowflake環境がしっかりできている状態から、tf管理に移行するのは非常に骨が折れる作業です。1日2日で移行できるものではないので、順次管理をtfに移していくことが現実的です。

必要となる作業としては、tf管理外のリソースを一度削除した上で、tf内で同名でリソースを作成することになると思います。

移行難易度の低い順に移行を考えるとしたら、以下の順ではないでしょうか。

  • アカウントレベルのオブジェクトへの権限
  • アカウントレベルのオブジェクト(ステートフルなリソース:データベースなど、及びロールを除く)
  • スキーマレベルのオブジェクトへの権限
  • スキーマレベルのオブジェクト(ステートフルなリソース:テーブルなどを除く)
  • ロール
  • ステートフルなリソース(データベース、テーブル、ストリーム、パイプ etc)

オブジェクト自体をtf管理にする前に権限を先に移行する理由は、オブジェクトをtf管理する際に既存のオブジェクトを削除する必要がある関係上、先に権限をtfでアタッチできるようにしておかないと、権限不足でシステムエラーを引き起こす可能性が高いためです。

アカウントレベルのオブジェクトは、ウェアハウス、ユーザー、ロール、リソースモニタ、統合、シェア、データベースあたりです。このうちロールとデータベースは、tf管理にする=作り直すと、既存の権限を全て付け直したりデータを入れ直す必要があるため後回しにするべきと考えます。

また、ステートフルなリソース、テーブルやビューに関してはtfで管理するより、dbtで管理する方が良いのではないかと思います。dbtのDevOpsが優れているのもありますが、terraformでテーブルを宣言する構文があまり見やすくないことも理由です。

また、移行する際には、

  • tf用のユーザー以外からリソースの作成権限を剥がしておく
  • tf用のユーザー以外から権限の付与権限(MANAGE GRANTS)を剥がしておく

ことで、tf管理外でこれ以上変化が起こることを防ぐのが良いと思います。

なお、このセクションについては完全に自分のイメージなので、実際に移行した方の体験談を聞いてみたいですね。

CICD環境の構築

tf管理に移行するにあたり、当然CICDを実現していきたいと思います。
ここに関しては、Snowflakeプロバイダ特有の問題はなく、tfにおけるCICDノウハウを導入すれば良いため、この記事では言及しません。以下の記事などを参考に構築することが可能です。
https://zenn.dev/honmarkhunt/articles/2f03cba1ffe966

ちなみに、tf公式もいろいろな開発者支援ツールを出しているので、眺めてみると面白いです。
https://developer.hashicorp.com/terraform/docs/terraform-tools

余談: CDKについて

今回の記事ではtfの標準的な記法でリソースを作成していきましたが、最近ではCDK for Terraformを採用するという手もあります。tfの独自DMLではなく、より汎用的な言語(typescript, pythonなど)で記述することができるため、柔軟度が格段に上がります。

SnowflakeプロバイダのCDK版も作られているようです。
https://github.com/cdktf/cdktf-provider-snowflake
使える状態なのかよくわかりませんが、触ってみようと思っています。

dbtやSnowparkとの統合

CDKが使えると、他のツールとの統合がしやすくなるという点も重要です。

昨今の状況を考えると、tfのみでSnowflakeの環境を全て管理するのは現実的ではなく、他にSnowflakeへのデプロイツールとしてdbtやSnowparkを使いたくなるのではないでしょうか。これらのツールで生成されたリソースとtfで生成されたリソースを一元的に管理することを考えてみると、Python CDKを利用することで、コード上での統合が非常にスムーズです。
たとえば、共通で利用したい変数などはpythonモジュールにまとめておき、CDKとSnowpark for Pythonとdbt(PythonModel)から参照させることができるようになります。他に、dbtでテーブルは作成したいがそのテーブルの権限はtfで管理したい、といった場合でも、言語が共通であることの恩恵は受けられるのではないかと思っています(具体的な方法はまだ考えてないです)。

まとめ

ここまで読んでいただき、ありがとうございました。
この記事では、既存の環境をtf管理に移行していく、という視点から現在のSnowflakeプロバイダについて解説をしました。

いろいろ書いたのですが、この記事で真に伝えたいメッセージはひとつです。

「もし今SnowflakeがTerraformで管理されていないなら、今すぐTerraformで管理できるように移行を始めてください」

この記事が、その第一歩の助けになれば幸いです。
この記事ではディレクトリ構成やファイル分割については触れていませんが、その辺については今後の運用の中で考えていきたいと思っているのと、先人の知恵がいろいろあるので、気になる方は調べてみてください。

twitterもやっているのでぜひ交流させていただければと思います。

記事内にて書いた内容について誤解や、より優れた方法があるかもしれません。もしそういった点を見つけたらご連絡いただけると幸いです。

Discussion