❄️

【Quickstart】Terraforming Snowflake

2024/06/24に公開

https://quickstarts.snowflake.com/guide/terraforming_snowflake/index.html#0

1. 概要(Overview)

Terraformとは:

宣言型のInfrastructure as Codeのプロダクト。YAMLに似た構文を使用して必要なものを宣言し、インフラを構築する。また、Terraformはステートフル(状態保持)であり、現在の状況を追跡し、それを改修後の状態と比較します。Terraformは、新しいリソースを作成したり、既存リソースの更新/削除をするプランを生成します。Terraform を使用すると、ウェアハウス、データベース、スキーマ、テーブル、ロール/許可などのアカウントレベルの Snowflake リソースを、他の多くのユースケースの中でも特に管理しやすくなります。

https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs
2024/06/13現在、Latestは、0.92、のため今回は バージョン:0.92 で行う

terraformの使用例・利用シーン

  • クラウドプロバイダにストレージを設定し、それを外部ステージとしてSnowflakeに設定
  • ストレージを追加してSnowpipeでデータ取り込み
  • サービスユーザを作成し、選択したシークレットマネージャにキーをプッシュ、かローテートする

このチュートリアルでは、terraformを使用して、ソース管理を自動化によってSnowflakeの構成管理を行う。
データベース、スキーマ、ウェアハウス、複数のロール、サービス ユーザーの作成方法など、Terraform をインストールして使用し、Snowflake 環境を作成および管理する方法を説明する

前提条件

  • Git,Snowflake、Snowflakeのオブジェクトに関する知識
  • ACCOUNTADMIN ロールを持っていること

学習内容

  • Terraformの基本的な使い方
  • Terraform でユーザー、ロール、データベース、スキーマ、ウェアハウスを作成する方法
  • コード/ソース管理からオブジェクトを管理する方法

必要なもの

  • GitHubアカウント​
  • Git コマンドラインクライアント
  • お好みのテキストエディタ
  • Terraformのインストール

何を構築するか

  • コードを通じてSnowflakeアカウントオブジェクトを管理するTerraformプロジェクトを含むリポジトリ

2. 新しいレポジトリを作る

$ mkdir sfguide-terraform-sample && cd sfguide-terraform-sample
$ echo "# sfguide-terraform-sample" >> README.md
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git branch -M main
$ git remote add origin git@github.com:YOUR_GITHUB_USERNAME/sfguide-terraform-sample.git
$ git push -u origin main

と書いていたのですが、私自身は、

https://github.com/gakut12?tab=repositories

にて、sfguide-terraform-sample を作り

$ cd $GIT_HOME
$ git clone git@github.com:gakut12/sfguide-terraform-sample.git

を実施して、Git Repository を作成

3. Terraformのサービスユーザーを作成する

terraform用にACCOUNTADMIN権限を持つサービスユーザを別途作ります。このサービスユーザの認証はキーペア認証で行います。
なぜ、キーペア認証にするかというと、認証情報のキャッシュにかんするプロバイダの制限と2FAのサポートがないため。
TerraformでCI/CDパイプラインを実行する際は、大抵キーペア認証かなと思います。※主にセキュリティ観点で

認証のためのRSA Keyを作成

Terraformでのキーペア認証のために、秘密鍵と公開鍵を作ります。

$ cd ~/.ssh
$ openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out snowflake_tf_snow_key.p8 -nocrypt
$ openssl rsa -in snowflake_tf_snow_key.p8 -pubout -out snowflake_tf_snow_key.publ

秘密鍵と公開鍵を作りました。

SnowflakeでTerraform用サービスアカウントを作ります

Snowflakeのコンソール(Snowsight)にログインし、以下のSQLを実行します。
実行するロールは、ACCOUNTADMIN です。

作成したtf-snowユーザに、SYSADMINSECURITYADMINを付与します。

$ cat ~/.ssh/snowflake_tf_snow_key.pub | tr -d "\n" | sed "s/-----BEGIN PUBLIC KEY-----//g" | sed "s/-----END PUBLIC KEY-----//g"
MIIBIjANBgkqhkiG....... 

を実行して、MIIBIjAN.....の文字列を控えておく
控えた文字列を次のCREATE USER にいれる RSA_PUBLIC_KEY の文字列へ埋め込む

use role ACCOUNTADMIN;
CREATE USER "tf-snow" RSA_PUBLIC_KEY='MIIBIjANBgkqhkiG.......' DEFAULT_ROLE=PUBLIC MUST_CHANGE_PASSWORD=FALSE;

GRANT ROLE SYSADMIN TO USER "tf-snow";
GRANT ROLE SECURITYADMIN TO USER "tf-snow";

4. Terraformの認証周りのセットアップ(Setup Terraform Authentication)

Terraformがユーザとして認証できるようにしましょう。環境変数などの設定を行う
まずは、Snowflakeのアカウント識別子を調べましょう。
※Snowsigthから取得することも可能

https://docs.snowflake.com/ja/user-guide/admin-account-identifier

こちらにアカウント識別子についての説明があります。

  • 形式1(推奨): 組織内のアカウント名
  • 形式2(レガシー): リージョン内のアカウントロケーター
    の2タイプがあります。
SELECT 
    current_organization_name() || '-' || current_account_name() as ACCOUNT_LOCATOR
    , current_account() as YOUR_LEGACY_ACCOUNT_LOCATOR
    , current_region() as YOUR_SNOWFLAKE_REGION_ID
;

例) YOUR_SNOWFLAKE_REGION_ID が 'aws_us_west_2' の場合は、SNOWFLAKE_REGIONは 'us-west-2.aws' になります

YOUR_LEGACY_ACCOUNT_LOCATORの方を使う場合は、SNOWFLAKE_REGIONにリージョンを設定する必要があります

ACCOUNT_LOCATOR YOUR_LEGACY_ACCOUNT_LOCATOR YOUR_SNOWFLAKE_REGION_ID
UMX****-JZ0**** BL2***** AWS_AP_NORTHEAST_1

※注意 Snowsightで値を取得すると UMX****.JZ0**** となるので、「.」を「-」に書き換える必要があります

必要な値は、

  • SNOWFLAKE_USER : アカウント識別子とはを参照
  • SNOWFLAKE_AUTHENTICATOR doc
    • Snowflake
    • OAuth
    • ExternalBrowser
    • Okta
    • JWT
      • 秘密鍵認証の場合は、JWTを指定するべし
    • TokenAccessor
    • UsernamePasswordMFA
  • SNOWFLAKE_PRIVATE_KEY
  • SNOWFLAKE_ACCOUNTで、

zshとかbashの場合はこちらで環境変数をセット

$ export SNOWFLAKE_USER="tf-snow"
$ export SNOWFLAKE_AUTHENTICATOR=JWT
$ export SNOWFLAKE_PRIVATE_KEY=`cat ~/.ssh/snowflake_tf_snow_key.p8`
$ export SNOWFLAKE_ACCOUNT="YOUR_ACCOUNT_LOCATOR"

fishの場合はこちらでセット →

$ set -Ux SNOWFLAKE_USER "tf-snow"
-- set: successfully set universal 'SNOWFLAKE_USER'; but a global by that name shadows it
$ set -Ux SNOWFLAKE_AUTHENTICATOR JWT
set: successfully set universal 'SNOWFLAKE_AUTHENTICATOR'; but a global by that name shadows it
$ set -Ux SNOWFLAKE_PRIVATE_KEY (cat ~/.ssh/snowflake_tf_snow_key.p8)
$ set -Ux SNOWFLAKE_ACCOUNT "UMX****-JZ0****"

※fishでcatコマンド(出力複数)が、環境変数に改行状態でいれる方法がわかる方おしえてくださいぃぃぃ。どうしてもできなくて、zshを使うことにしました・・・・

5. リソースの定義(Declaring Resources)

ベースディレクトリのプロジェクトにmain.tfという名前のファイルを追加します。main.tfではプロバイダを設定し、Terraformに作成させたいデータベースとウェアハウスの設定を定義します。

以下のブロックの内容をmain.tfにコピーします。

main.tf
terraform {
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "~> 0.87"
    }
  }
}

provider "snowflake" {
  role = "SYSADMIN"
}

resource "snowflake_database" "db" {
  name = "TF_DEMO"
}

resource "snowflake_warehouse" "warehouse" {
  name           = "TF_DEMO"
  warehouse_size = "xsmall"
  auto_suspend   = 60
}

6. Terrformプロジェクを動かすための準備(Preparing the Project to Run)

Terraformを実行するためにプロジェクトをセットアップするには、初期化する必要があります

プロジェクトフォルダ内のシェルから以下を実行します

$ cd $(プロジェクトフォルダ)
$ terraform init
terraform init log

Initializing the backend...

Initializing provider plugins...

  • Finding snowflake-labs/snowflake versions matching "~> 0.87"...
  • Installing snowflake-labs/snowflake v0.92.0...
  • Installed snowflake-labs/snowflake v0.92.0 (signed by a HashiCorp partner, key ID 5166D7352E69A585)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, ot

このデモではTerraformのローカルバックエンドを使用しており、状態ファイルはTerraformを実行しているマシン上にローカルに保存されています。Terraformの状態ファイルはすべての変更を計算するために必要なので、このファイル(.tfstate、古い状態ファイルでは.tfstate.*)を失わないことが重要です。マルチテナントや自動化ワークフローを利用する場合は、リモートバックエンドを利用することを強く推奨します。

.terraformフォルダは、すべてのプロバイダとモジュールの依存関係をダウンロードする場所です。このフォルダには重要なものはないので、削除しても問題ありません。このフォルダとTerraformの状態ファイル(VCSに機密情報をチェックインしたくないため)を.gitignoreに追加してください。

プロジェクトルートに.gitignoreという名前のファイルを作成し、そのファイルに以下のテキストを追加して保存します:

7. ソース管理、変更管理(Commit changes to source control)

$ git checkout -b dbwh
$ git add main.tf
$ git add .gitignore
$ git commit -m "Add Database and Warehouse"
$ git push origin main

ってやってあげた

次に、GitHubにログインしてPull Requestを作成します。

多くの本番システムでは、GitHub のコミットが CI/CD ジョブのトリガーとなり、エンジニアがレビューできるプランが作成されます。変更が望ましいものであれば、Pull Request はマージされます。マージ後、CI/CD ジョブが起動し、メインブランチで行われた変更が適用されます。

異なる環境(dev/test/prod)を管理するために、異なる入力変数でTerraformコードを構成し、同じSnowflakeアカウント、または異なるSnowflakeアカウントにデプロイすることができます。

具体的なワークフローは、コンプライアンスのニーズ、他のワークフロー、環境とアカウントのトポロジーなどの要件によって異なります。

このラボでは、提案された CI/CD ジョブをシミュレートし、Terraform が何を変更したいかを確認するために計画を実行できます。Terraform planで、Terraformはdiff計算を実行し、希望する状態と状態ファイルからの前/現在の状態を比較し、実行する必要があるアクションを決定します。

プロジェクトフォルダ内のシェル(Account Informationが環境)から実行します:

$ terraform plan
terraform plan

gaku.tashiro@[17:01:27]:~/git/gaku_t_private/sfguide-terraform-sample% terraform plan [main]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • create

Terraform will perform the following actions:

snowflake_database.db will be created

  • resource "snowflake_database" "db" {
    • data_retention_time_in_days = -1
    • id = (known after apply)
    • is_transient = false
    • name = "TF_DEMO"
      }

snowflake_warehouse.warehouse will be created

  • resource "snowflake_warehouse" "warehouse" {
    • auto_resume = (known after apply)
    • auto_suspend = 60
    • enable_query_acceleration = false
    • id = (known after apply)
    • max_cluster_count = (known after apply)
    • max_concurrency_level = 8
    • min_cluster_count = (known after apply)
    • name = "TF_DEMO"
    • resource_monitor = (known after apply)
    • scaling_policy = (known after apply)
    • statement_queued_timeout_in_seconds = 0
    • statement_timeout_in_seconds = 172800
    • warehouse_size = "xsmall"
    • warehouse_type = "STANDARD"
      }

Plan: 2 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

8. Terraformの実行(Running Terraform)

プランの確認が終わったところで、CI/CDジョブの次のステップである、アカウントに変更を適用するシミュレーションを行います。

  • プロジェクトフォルダ内のシェルから(アカウント情報が環境内にある状態で)実行する
  • $ terraform apply
    

プロンプトが出るので、yes を入力し、Enter

  • Terraformは実行計画を再生成し(plan.outファイルへのパスをオプションで指定しない限り)、確認後に必要な変更を適用します。この場合、Terraformは2つの新しいリソースを作成し、それ以外の変更はありません
  • Snowflakeアカウントにログインし、Terraformがデータベースとウェアハウスを作成したことを確認します。

Database : TF_DEMO
Warehouse:TF_DEMO
が作成

9. リソースの変更と追加(Changing and Adding Resources)

すべてのデータベースにはテーブルを格納するスキーマが必要なので、それを追加し、アプリケーション/クライアントがデータベースとスキーマに接続できるようにサービス・ユーザーを追加します。構文は、すでに作成したデータベースとウェアハウスとよく似ています。ここまでで、Snowflakeでリソースを作成および更新するために必要な知識はすべて習得しました。サービスロール/ユーザがデータベースとスキーマを使用できるように、権限も追加します。

この大部分は期待通りの内容であることがわかるでしょう。唯一の新しい部分は秘密鍵の作成です。Terraform TLS秘密鍵ジェネレータはTerraformプロバイダが利用できる形式で公開鍵をエクスポートしないため、いくつかの文字列操作が必要になります。

  • main.tfファイルの倉庫サイズをxsmallからsmallに変更する。
  • main.tfファイルに以下のリソースを追加します
provider "snowflake" {
  alias = "security_admin"
  role  = "SECURITYADMIN"
}

resource "snowflake_role" "role" {
  provider = snowflake.security_admin
  name     = "TF_DEMO_SVC_ROLE"
}

resource "snowflake_grant_privileges_to_account_role" "database_grant" {
  provider          = snowflake.security_admin
  privileges        = ["USAGE"]
  account_role_name = snowflake_role.role.name
  on_account_object {
    object_type = "DATABASE"
    object_name = snowflake_database.db.name
  }
}

resource "snowflake_schema" "schema" {
  database   = snowflake_database.db.name
  name       = "TF_DEMO"
  is_managed = false
}

resource "snowflake_grant_privileges_to_account_role" "schema_grant" {
  provider          = snowflake.security_admin
  privileges        = ["USAGE"]
  account_role_name = snowflake_role.role.name
  on_schema {
    schema_name = "\"${snowflake_database.db.name}\".\"${snowflake_schema.schema.name}\""
  }
}

resource "snowflake_grant_privileges_to_account_role" "warehouse_grant" {
  provider          = snowflake.security_admin
  privileges        = ["USAGE"]
  account_role_name = snowflake_role.role.name
  on_account_object {
    object_type = "WAREHOUSE"
    object_name = snowflake_warehouse.warehouse.name
  }
}

resource "tls_private_key" "svc_key" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

resource "snowflake_user" "user" {
    provider          = snowflake.security_admin
    name              = "tf_demo_user"
    default_warehouse = snowflake_warehouse.warehouse.name
    default_role      = snowflake_role.role.name
    default_namespace = "${snowflake_database.db.name}.${snowflake_schema.schema.name}"
    rsa_public_key    = substr(tls_private_key.svc_key.public_key_pem, 27, 398)
}

resource "snowflake_grant_privileges_to_account_role" "user_grant" {
  provider          = snowflake.security_admin
  privileges        = ["MONITOR"]
  account_role_name = snowflake_role.role.name  
  on_account_object {
    object_type = "USER"
    object_name = snowflake_user.user.name
  }
}

resource "snowflake_grant_account_role" "grants" {
  provider  = snowflake.security_admin
  role_name = snowflake_role.role.name
  user_name = snowflake_user.user.name
}

こちらをmain.tfについかして、terraform planをおこなったところ、Error

% terraform plan                     [main]
╷
│ Error: Inconsistent dependency lock file
│
│ The following dependency selections recorded in the lock file are inconsistent with the current
│ configuration:
│   - provider registry.terraform.io/hashicorp/tls: required by this configuration but no version is selected
│
│ To update the locked dependency selections to match a changed configuration, run:
│   terraform init -upgrade

以下のコマンドを実行

$ terraform init -upgrade
terraform init -upgrade

Initializing the backend...

Initializing provider plugins...

  • Finding snowflake-labs/snowflake versions matching "~> 0.87"...
  • Finding latest version of hashicorp/tls...
  • Using previously-installed snowflake-labs/snowflake v0.92.0
  • Installing hashicorp/tls v4.0.5...
  • Installed hashicorp/tls v4.0.5 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

※ライブラリ?を追加した際には、都度initが必要なのかな

あらためて

$ terraform plan

を実行。正常に通りました

terraform plan

$ terraform plan [

snowflake_database.db: Refreshing state... [id=TF_DEMO]
snowflake_warehouse.warehouse: Refreshing state... [id=TF_DEMO]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • create

Terraform will perform the following actions:

snowflake_grant_account_role.grants will be created

  • resource "snowflake_grant_account_role" "grants" {
    • id = (known after apply)
    • role_name = "TF_DEMO_SVC_ROLE"
    • user_name = (sensitive value)
      }

snowflake_grant_privileges_to_account_role.database_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "database_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = "TF_DEMO"
      • object_type = "DATABASE"
        }
        }

snowflake_grant_privileges_to_account_role.schema_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "schema_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_schema {

      • schema_name = ""TF_DEMO"."TF_DEMO""
        }
        }

snowflake_grant_privileges_to_account_role.user_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "user_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "MONITOR",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = (sensitive value)
      • object_type = "USER"
        }
        }

snowflake_grant_privileges_to_account_role.warehouse_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "warehouse_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = "TF_DEMO"
      • object_type = "WAREHOUSE"
        }
        }

snowflake_role.role will be created

  • resource "snowflake_role" "role" {
    • id = (known after apply)
    • name = "TF_DEMO_SVC_ROLE"
      }

snowflake_schema.schema will be created

  • resource "snowflake_schema" "schema" {
    • data_retention_days = -1
    • database = "TF_DEMO"
    • id = (known after apply)
    • is_managed = false
    • is_transient = false
    • name = "TF_DEMO"
      }

snowflake_user.user will be created

  • resource "snowflake_user" "user" {
    • default_namespace = "TF_DEMO.TF_DEMO"
    • default_role = "TF_DEMO_SVC_ROLE"
    • default_warehouse = "TF_DEMO"
    • disabled = (known after apply)
    • display_name = (sensitive value)
    • has_rsa_public_key = (known after apply)
    • id = (known after apply)
    • login_name = (known after apply)
    • name = (sensitive value)
    • rsa_public_key = (known after apply)
      }

tls_private_key.svc_key will be created

  • resource "tls_private_key" "svc_key" {
    • algorithm = "RSA"
    • ecdsa_curve = "P224"
    • id = (known after apply)
    • private_key_openssh = (sensitive value)
    • private_key_pem = (sensitive value)
    • private_key_pem_pkcs8 = (sensitive value)
    • public_key_fingerprint_md5 = (known after apply)
    • public_key_fingerprint_sha256 = (known after apply)
    • public_key_openssh = (known after apply)
    • public_key_pem = (known after apply)
    • rsa_bits = 2048
      }

Plan: 9 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply"
now.

アプリケーションの公開鍵と秘密鍵の情報を得るには、Terraformの出力値を使う。

outputs.tfという新しいファイルに以下のリソースを追加する。

output "snowflake_svc_public_key" {
  value = tls_private_key.svc_key.public_key_pem
}

output "snowflake_svc_private_key" {
  value     = tls_private_key.svc_key.private_key_pem
  sensitive = true
}

10. 変更をコミット(Commit Changes to Source Control)

変更をgitへcommit & push します

$ git checkout -b svcuser
$ git add main.tf
$ git add outputs.tf
$ git commit -m "Add Service User, Schema, Grants"
$ git push origin HEAD

※git checkout は、現在は、git switch が提供されていて、動作は一緒だけど、直感的にわかりやすいので、switchを使うほうがいいかも

11. 変更を適用(Apply and Verify the Changes)

CI/CDパイプラインをシミュレートするために、変更を適用して、望ましい状態を保存されている状態に適合させることができる。

  1. プロジェクトフォルダ内のシェルから(アカウント情報が環境内にある状態で)実行する
    2. $ terraform apply
  2. 変更が適切であれば、それを受け入れる。
  3. Snowflakeコンソール(Snowsight)にログインして、すべての変更が完了したことを確認する。
terraform apply log

snowflake_database.db: Refreshing state... [id=TF_DEMO]
snowflake_warehouse.warehouse: Refreshing state... [id=TF_DEMO]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • create

Terraform will perform the following actions:

snowflake_grant_account_role.grants will be created

  • resource "snowflake_grant_account_role" "grants" {
    • id = (known after apply)
    • role_name = "TF_DEMO_SVC_ROLE"
    • user_name = (sensitive value)
      }

snowflake_grant_privileges_to_account_role.database_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "database_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = "TF_DEMO"
      • object_type = "DATABASE"
        }
        }

snowflake_grant_privileges_to_account_role.schema_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "schema_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_schema {

      • schema_name = ""TF_DEMO"."TF_DEMO""
        }
        }

snowflake_grant_privileges_to_account_role.user_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "user_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "MONITOR",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = (sensitive value)
      • object_type = "USER"
        }
        }

snowflake_grant_privileges_to_account_role.warehouse_grant will be created

  • resource "snowflake_grant_privileges_to_account_role" "warehouse_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE"

    • all_privileges = false

    • always_apply = false

    • id = (known after apply)

    • on_account = false

    • privileges = [

      • "USAGE",
        ]
    • with_grant_option = false

    • on_account_object {

      • object_name = "TF_DEMO"
      • object_type = "WAREHOUSE"
        }
        }

snowflake_role.role will be created

  • resource "snowflake_role" "role" {
    • id = (known after apply)
    • name = "TF_DEMO_SVC_ROLE"
      }

snowflake_schema.schema will be created

  • resource "snowflake_schema" "schema" {
    • data_retention_days = -1
    • database = "TF_DEMO"
    • id = (known after apply)
    • is_managed = false
    • is_transient = false
    • name = "TF_DEMO"
      }

snowflake_user.user will be created

  • resource "snowflake_user" "user" {
    • default_namespace = "TF_DEMO.TF_DEMO"
    • default_role = "TF_DEMO_SVC_ROLE"
    • default_warehouse = "TF_DEMO"
    • disabled = (known after apply)
    • display_name = (sensitive value)
    • has_rsa_public_key = (known after apply)
    • id = (known after apply)
    • login_name = (known after apply)
    • name = (sensitive value)
    • rsa_public_key = (known after apply)
      }

tls_private_key.svc_key will be created

  • resource "tls_private_key" "svc_key" {
    • algorithm = "RSA"
    • ecdsa_curve = "P224"
    • id = (known after apply)
    • private_key_openssh = (sensitive value)
    • private_key_pem = (sensitive value)
    • private_key_pem_pkcs8 = (sensitive value)
    • public_key_fingerprint_md5 = (known after apply)
    • public_key_fingerprint_sha256 = (known after apply)
    • public_key_openssh = (known after apply)
    • public_key_pem = (known after apply)
    • rsa_bits = 2048
      }

Plan: 9 to add, 0 to change, 0 to destroy.

Changes to Outputs:

  • snowflake_svc_private_key = (sensitive value)
  • snowflake_svc_public_key = (known after apply)

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

tls_private_key.svc_key: Creating...
tls_private_key.svc_key: Creation complete after 0s [id=492f4854f1502a368bb2c0dcd5c0e300d9abde0a]
snowflake_schema.schema: Creating...
snowflake_role.role: Creating...
snowflake_role.role: Creation complete after 1s [id=TF_DEMO_SVC_ROLE]
snowflake_grant_privileges_to_account_role.database_grant: Creating...
snowflake_grant_privileges_to_account_role.warehouse_grant: Creating...
snowflake_schema.schema: Creation complete after 1s [id=TF_DEMO|TF_DEMO]
snowflake_grant_privileges_to_account_role.schema_grant: Creating...
snowflake_user.user: Creating...
snowflake_grant_privileges_to_account_role.database_grant: Creation complete after 0s [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|DATABASE|"TF_DEMO"]
snowflake_grant_privileges_to_account_role.warehouse_grant: Creation complete after 0s [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|WAREHOUSE|"TF_DEMO"]
snowflake_user.user: Creation complete after 0s [id=tf_demo_user]
snowflake_grant_account_role.grants: Creating...
-----BEGIN PUBLIC KEY-----
snowflake_grant_privileges_to_account_role.user_grant: Creating...
snowflake_grant_account_role.grants: Creation complete after 1s [id="TF_DEMO_SVC_ROLE"|USER|"tf_demo_user"]
snowflake_grant_privileges_to_account_role.user_grant: Creation complete after 1s [id="TF_DEMO_SVC_ROLE"|false|false|MONITOR|OnAccountObject|USER|"tf_demo_user"]
snowflake_grant_privileges_to_account_role.schema_grant: Creation complete after 1s [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnSchema|OnSchema|"TF_DEMO"."TF_DEMO"]

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

snowflake_svc_private_key = <sensitive>
snowflake_svc_public_key = <<EOT
-----BEGIN PUBLIC KEY-----
MIIBI........................................
.............................................
QwIDAQAB
-----END PUBLIC KEY-----

EOT

※PUBLIC KEYは保存しておきます。新たに作ったユーザ(tf_demo_user)のキーペア認証で必要です

すべての変更はソース・コントロールに保存され、CI/CDによって適用されるため、すべての環境変更の履歴を得ることができる。コンプライアンス・コントロールを導入し、環境を直接管理する権限を少数のユーザーに制限することができる。また、SQLスクリプトを管理することなく、他と同じ新しい環境をタイムリーに立ち上げることも簡単にできる。

12. お掃除(Cleanup)

デモはほぼ終了です。あとは、あなたのアカウントをクリーンアップするだけ。

すべてのTerraform管理リソースを破棄する

1.プロジェクトフォルダ内のシェルから(アカウント情報を環境にして)実行します:
$ terraform destroy
2. 適切であれば変更を受け入れる。
3. コンソールにログインして、すべてのオブジェクトが破棄されたことを確認します。Terraformが作成したデータベース、スキーマ、ウェアハウス、ロール、ユーザーオブジェクトは自動的に削除されます。

terraform destroy log

% terraform destroy
tls_private_key.svc_key: Refreshing state... [id=492f4854f1502a368bb2c0dcd5c0e300d9abde0a]
snowflake_database.db: Refreshing state... [id=TF_DEMO]
snowflake_role.role: Refreshing state... [id=TF_DEMO_SVC_ROLE]
snowflake_warehouse.warehouse: Refreshing state... [id=TF_DEMO]
snowflake_schema.schema: Refreshing state... [id=TF_DEMO|TF_DEMO]
snowflake_grant_privileges_to_account_role.database_grant: Refreshing state... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|DATABASE|"TF_DEMO"]
snowflake_grant_privileges_to_account_role.schema_grant: Refreshing state... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnSchema|OnSchema|"TF_DEMO"."TF_DEMO"]
snowflake_grant_privileges_to_account_role.warehouse_grant: Refreshing state... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|WAREHOUSE|"TF_DEMO"]
snowflake_user.user: Refreshing state... [id=tf_demo_user]
snowflake_grant_account_role.grants: Refreshing state... [id="TF_DEMO_SVC_ROLE"|USER|"tf_demo_user"]
snowflake_grant_privileges_to_account_role.user_grant: Refreshing state... [id="TF_DEMO_SVC_ROLE"|false|false|MONITOR|OnAccountObject|USER|"tf_demo_user"]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • destroy

Terraform will perform the following actions:

snowflake_database.db will be destroyed

  • resource "snowflake_database" "db" {
    • data_retention_time_in_days = -1 -> null
    • id = "TF_DEMO" -> null
    • is_transient = false -> null
    • name = "TF_DEMO" -> null
      }

snowflake_grant_account_role.grants will be destroyed

  • resource "snowflake_grant_account_role" "grants" {
    • id = ""TF_DEMO_SVC_ROLE"|USER|"tf_demo_user"" -> null
    • role_name = "TF_DEMO_SVC_ROLE" -> null
    • user_name = (sensitive value) -> null
      }

snowflake_grant_privileges_to_account_role.database_grant will be destroyed

  • resource "snowflake_grant_privileges_to_account_role" "database_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE" -> null

    • all_privileges = false -> null

    • always_apply = false -> null

    • id = ""TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|DATABASE|"TF_DEMO"" -> null

    • on_account = false -> null

    • privileges = [

      • "USAGE",
        ] -> null
    • with_grant_option = false -> null

    • on_account_object {

      • object_name = "TF_DEMO" -> null
      • object_type = "DATABASE" -> null
        }
        }

snowflake_grant_privileges_to_account_role.schema_grant will be destroyed

  • resource "snowflake_grant_privileges_to_account_role" "schema_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE" -> null

    • all_privileges = false -> null

    • always_apply = false -> null

    • id = ""TF_DEMO_SVC_ROLE"|false|false|USAGE|OnSchema|OnSchema|"TF_DEMO"."TF_DEMO"" -> null

    • on_account = false -> null

    • privileges = [

      • "USAGE",
        ] -> null
    • with_grant_option = false -> null

    • on_schema {

      • schema_name = ""TF_DEMO"."TF_DEMO"" -> null
        }
        }

snowflake_grant_privileges_to_account_role.user_grant will be destroyed

  • resource "snowflake_grant_privileges_to_account_role" "user_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE" -> null

    • all_privileges = false -> null

    • always_apply = false -> null

    • id = ""TF_DEMO_SVC_ROLE"|false|false|MONITOR|OnAccountObject|USER|"tf_demo_user"" -> null

    • on_account = false -> null

    • privileges = [

      • "MONITOR",
        ] -> null
    • with_grant_option = false -> null

    • on_account_object {

      • object_name = (sensitive value) -> null
      • object_type = "USER" -> null
        }
        }

snowflake_grant_privileges_to_account_role.warehouse_grant will be destroyed

  • resource "snowflake_grant_privileges_to_account_role" "warehouse_grant" {
    • account_role_name = "TF_DEMO_SVC_ROLE" -> null

    • all_privileges = false -> null

    • always_apply = false -> null

    • id = ""TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|WAREHOUSE|"TF_DEMO"" -> null

    • on_account = false -> null

    • privileges = [

      • "USAGE",
        ] -> null
    • with_grant_option = false -> null

    • on_account_object {

      • object_name = "TF_DEMO" -> null
      • object_type = "WAREHOUSE" -> null
        }
        }

snowflake_role.role will be destroyed

  • resource "snowflake_role" "role" {
    • id = "TF_DEMO_SVC_ROLE" -> null
    • name = "TF_DEMO_SVC_ROLE" -> null
      }

snowflake_schema.schema will be destroyed

  • resource "snowflake_schema" "schema" {
    • data_retention_days = -1 -> null
    • database = "TF_DEMO" -> null
    • id = "TF_DEMO|TF_DEMO" -> null
    • is_managed = false -> null
    • is_transient = false -> null
    • name = "TF_DEMO" -> null
      }

snowflake_user.user will be destroyed

  • resource "snowflake_user" "user" {
    • default_namespace = "TF_DEMO.TF_DEMO" -> null
    • default_role = "TF_DEMO_SVC_ROLE" -> null
    • default_secondary_roles = [] -> null
    • default_warehouse = "TF_DEMO" -> null
    • disabled = false -> null
    • display_name = (sensitive value) -> null
    • has_rsa_public_key = true -> null
    • id = "tf_demo_user" -> null
    • login_name = "TF_DEMO_USER" -> null
    • name = (sensitive value) -> null
    • rsa_public_key = <<-EOT
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+IBtUKTryUznRYFR2Gyz
      NpmBuu42C5LsqJsOH1dF73lPRnYmYZv5XDwOFEzssv42IhdRNrm4p94c9lj4S4sB
      HXTRDwNma4BLCQN+o/6ZfLmvz6YOFx96ebtk++Y31i3QUYRLKnl7r0EsqgUj13mC
      4yg70S6zg27kCgLCKJ98aq8pr9+t6dqtCHgxVrhzqw0TDrhHPH+ZamPFVO57nCTN
      gMgw1XTjFHuhYwlMXmb30AppWC6Mmn9fFMVV8qlib29tfORbV4orl0I6KdR/r5Ix
      0AVgWd6nLml/YTSEvItG3G6fdiCakgk58jMqJL6pS59YYmYUX4ZIViRCWiYTdI1O
      QwIDAQAB
      EOT -> null
      }

snowflake_warehouse.warehouse will be destroyed

  • resource "snowflake_warehouse" "warehouse" {
    • auto_resume = true -> null
    • auto_suspend = 60 -> null
    • enable_query_acceleration = false -> null
    • id = "TF_DEMO" -> null
    • max_cluster_count = 1 -> null
    • max_concurrency_level = 8 -> null
    • min_cluster_count = 1 -> null
    • name = "TF_DEMO" -> null
    • resource_monitor = "null" -> null
    • scaling_policy = "STANDARD" -> null
    • statement_queued_timeout_in_seconds = 0 -> null
    • statement_timeout_in_seconds = 172800 -> null
    • warehouse_size = "SMALL" -> null
    • warehouse_type = "STANDARD" -> null
      }

tls_private_key.svc_key will be destroyed

  • resource "tls_private_key" "svc_key" {
    • algorithm = "RSA" -> null
    • ecdsa_curve = "P224" -> null
    • id = "492f4854f1502a368bb2c0dcd5c0e300d9abde0a" -> null
    • private_key_openssh = (sensitive value) -> null
    • private_key_pem = (sensitive value) -> null
    • private_key_pem_pkcs8 = (sensitive value) -> null
    • public_key_fingerprint_md5 = "3d:7e:27:0b:ca:ee:52:4e:2f:3d:50:a0:aa:da:84:22" -> null
    • public_key_fingerprint_sha256 = "SHA256:U7nH8N6p2+3yi7/f1EtvvYL1roAxXHO6jOaapBWzOKU" -> null
    • public_key_openssh = <<-EOT
      ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD4gG1QpOvJTOdFgVHYbLM2mYG67jYLkuyomw4fV0XveU9GdiZhm/lcPA4UTOyy/jYiF1E2ubin3hz2WPhLiwEddNEPA2ZrgEsJA36j/pl8ua/Ppg4XH3p5u2T75jfWLdBRhEsqeXuvQSyqBSPXeYLjKDvRLrODbuQKAsIon3xqrymv363p2q0IeDFWuHOrDRMOuEc8f5lqY8VU7nucJM2AyDDVdOMUe6FjCUxeZvfQCmlYLoyaf18UxVXyqWJvb2185FtXiiuXQjop1H+vkjHQBWBZ3qcuaX9hNIS8i0bcbp92IJqSCTnyMyokvqlLn1hiZhRfhkhWJEJaJhN0jU5D
      EOT -> null
    • public_key_pem = <<-EOT
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+IBtUKTryUznRYFR2Gyz
      NpmBuu42C5LsqJsOH1dF73lPRnYmYZv5XDwOFEzssv42IhdRNrm4p94c9lj4S4sB
      HXTRDwNma4BLCQN+o/6ZfLmvz6YOFx96ebtk++Y31i3QUYRLKnl7r0EsqgUj13mC
      4yg70S6zg27kCgLCKJ98aq8pr9+t6dqtCHgxVrhzqw0TDrhHPH+ZamPFVO57nCTN
      gMgw1XTjFHuhYwlMXmb30AppWC6Mmn9fFMVV8qlib29tfORbV4orl0I6KdR/r5Ix
      0AVgWd6nLml/YTSEvItG3G6fdiCakgk58jMqJL6pS59YYmYUX4ZIViRCWiYTdI1O
      QwIDAQAB
      -----END PUBLIC KEY-----
      EOT -> null
    • rsa_bits = 2048 -> null
      }

Plan: 0 to add, 0 to change, 11 to destroy.

Changes to Outputs:

  • snowflake_svc_private_key = (sensitive value) -> null
  • snowflake_svc_public_key = <<-EOT
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+IBtUKTryUznRYFR2Gyz
    NpmBuu42C5LsqJsOH1dF73lPRnYmYZv5XDwOFEzssv42IhdRNrm4p94c9lj4S4sB
    HXTRDwNma4BLCQN+o/6ZfLmvz6YOFx96ebtk++Y31i3QUYRLKnl7r0EsqgUj13mC
    4yg70S6zg27kCgLCKJ98aq8pr9+t6dqtCHgxVrhzqw0TDrhHPH+ZamPFVO57nCTN
    gMgw1XTjFHuhYwlMXmb30AppWC6Mmn9fFMVV8qlib29tfORbV4orl0I6KdR/r5Ix
    0AVgWd6nLml/YTSEvItG3G6fdiCakgk58jMqJL6pS59YYmYUX4ZIViRCWiYTdI1O
    QwIDAQAB
    -----END PUBLIC KEY-----
    EOT -> null

Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.

Enter a value:

プロンプトに `yes`

snowflake_grant_account_role.grants: Destroying... [id="TF_DEMO_SVC_ROLE"|USER|"tf_demo_user"]
snowflake_grant_privileges_to_account_role.schema_grant: Destroying... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnSchema|OnSchema|"TF_DEMO"."TF_DEMO"]
snowflake_grant_privileges_to_account_role.user_grant: Destroying... [id="TF_DEMO_SVC_ROLE"|false|false|MONITOR|OnAccountObject|USER|"tf_demo_user"]
snowflake_grant_privileges_to_account_role.warehouse_grant: Destroying... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|WAREHOUSE|"TF_DEMO"]
snowflake_grant_privileges_to_account_role.database_grant: Destroying... [id="TF_DEMO_SVC_ROLE"|false|false|USAGE|OnAccountObject|DATABASE|"TF_DEMO"]
snowflake_grant_account_role.grants: Destruction complete after 0s
snowflake_grant_privileges_to_account_role.warehouse_grant: Destruction complete after 0s
snowflake_grant_privileges_to_account_role.database_grant: Destruction complete after 1s
snowflake_grant_privileges_to_account_role.schema_grant: Destruction complete after 1s
snowflake_grant_privileges_to_account_role.user_grant: Destruction complete after 1s
snowflake_user.user: Destroying... [id=tf_demo_user]
snowflake_user.user: Destruction complete after 0s
snowflake_schema.schema: Destroying... [id=TF_DEMO|TF_DEMO]
snowflake_warehouse.warehouse: Destroying... [id=TF_DEMO]
snowflake_role.role: Destroying... [id=TF_DEMO_SVC_ROLE]
tls_private_key.svc_key: Destroying... [id=492f4854f1502a368bb2c0dcd5c0e300d9abde0a]
tls_private_key.svc_key: Destruction complete after 0s
snowflake_schema.schema: Destruction complete after 0s
snowflake_database.db: Destroying... [id=TF_DEMO]
snowflake_role.role: Destruction complete after 0s
snowflake_database.db: Destruction complete after 0s
snowflake_warehouse.warehouse: Destruction complete after 0s

Destroy complete! Resources: 11 destroyed.

Snowflakeコンソールからリソースが消えていることを確認

Drop the User we added

terraform用に手動で作ったUSERを削除
※terraformで定義して作ったUSERは、terraform destroyで削除されている

$ DROP USER "tf-snow";

13. 結論(Conclusion)

Terraformを初めて使うのであれば、学ぶべきことはまだたくさんあります。リモートステート、入力変数、ビルドモジュールについて研究することをお勧めします。これにより、シンプルな宣言型言語を使ってSnowflake環境を構築・管理できるようになります。

Snowflake用のTerraformプロバイダはオープンソースのプロジェクトです。まだプロバイダに作成されていないリソースを管理するためにTerraformが必要であれば、貢献を歓迎します!また、Terraformプロバイダプロジェクトと全体的なエクスペリエンスを改善するために、Githubプロジェクトへの問題やフィードバックの投稿も歓迎します。

Next steps

  • Terraformの変更をどのように実行するかを決める必要がある。このクイックスタートでは、ローカルコンピュータですべてを実行したが、本番環境をローカル環境化のTerraformで行うことはほとんどない。ベストプラクティスでは、共有環境の変更をCI/CDパイプラインを作り自動化する。CI/CDパイプラインは、環境を変更するため、環境の履歴をレビューできるように、ソース管理にプルリクエストを作成する。
  • 環境とプロジェクトをどのように分離するかについては、名前空間/RBACを使うか、複数のアカウントを使うか、の選択がある。※ここはSnowflakeのアカウントの作成がどうできるか?等色々考慮点がある。ほとんどのモジュールとCI/CDインフラストラクチャは、別のアプローチ用に大きく変更する必要があるため、これを後で変更するのは難しい。
  • また、IaCもすべてTerraformで管理は難しい。他のツールも併用する方が良い。TerraformはSnowflakeの多くのリソースを管理するための強力なツールだが、スキーマやデータの変更を管理するには限界がある。

その他、データ移行やテーブルスキーマの変更などを管理するツールもある。Terraformは以下のいずれかと組み合わせることで、顧客のあらゆる要求を満たすことができる。

(補足)dbtは、データモデリング/データトランスファーでのデファクトになりつつある、今後のデータエンジニアリングでは必須技能になるでしょう
(補足2)snowchangeは、現在は、schemachange という名前に変わっている。tableの定義などをやりやすいらしい。私自身まだ使っていないが、table以外はterraform, tableはschemachangeという棲み分けが良さそう
(補足3)flywayは、schamachangeの着想の元?になったと仄聞している。なので、Schamachange と競合らしい
※使い勝手は後発のschemachangeかも・・・Snowflakeがつくってるので、Snowflakeに関しては特に優位性があるかも>schamachange

What we've covered

  • Terraformを始める
  • 新しいプロジェクトのセットアップ
  • 新しいデータベース、ウェアハウス、スキーマ、ユーザー、ロール、新しいグラントを追加する
  • 作成したリソースをすべて破棄してクリーンアップする

参考文献

https://zenn.dev/allllllllez/articles/f06e3a6e3d76aa

https://zenn.dev/dataheroes/articles/2024-terraform-snowflake-provider-roadmap

https://qiita.com/usakoyama/items/d3d173bde0c046ad2c03

https://select.dev/posts/snowflake-terraform

Discussion