BLOG

ブログ

Snowflakeにterraformを導入する方法

背景

 今回データ基盤構築のお手伝いをさせていただいているクライアント様の中で、 新規にSnowflakeをご利用される方がいらっしゃり備考録の意味もこめ調査した内容をまとめさせていただいています。 SnowflakeはGCPやAWSといった特定クラウドサービスに縛られないサードパーティツールで、 SQLのみで全ての管理を行うことができるという特徴を持っています。 そのため今回terraformはGCPやAWSなどにおけるインフラ管理というより、DB管理ツール的な意味合いが強くなっています。

Snowflakeにterraformを導入すべきか

 インフラではなくDB管理としてterraform有効なのかでいうと、 個人的にはterraform等で管理することによりインフラと同様「コスト・スピード・リスク」などの面で優れていると考えています。

  • コスト:デプロイ時にかかる工数が大幅に減少
  • スピード:デプロイスピードが向上する
  • リスク:デプロイフローがシステム的に担保され、またDBの最新状態がコードで表現されている。

ただSnowflakeのSQLで全て管理できると行った特性から、 AWS・GCPなどのクラウドサービスと比較すると上記メリットはそこまで大きくないように感じました。 例えばCI/CDがなくてもSQLをGitHubなどで管理することで、 デプロイは手動実行になりますが「DBの最新状態がコードで表現されている」状態は保つことができ、 DBの状況がわからないといったリスクは回避できるのではないかと思われます。
 ただ一方でroleの権限などを細かく設定する必要がある点においては、 moduleなどでテンプレート化できる点が大きくterraform側のメリットとしてあるかと思われます。

全体構成

CI/CDの構成

 今回特定クラウドサービスに依存したサービスではなく、 またコード自体はGitHubにて管理していたためGitHub Actionsを用いました。 GitHub Actionsにはシークレット情報を格納する機能があるので、 ユーザー名などは基本的にそちらに格納して用いています。

 
name: snowflake_deploy
on: pull_request

env:
  TERRAFORM_VERSION: '1.3.0'
  TERRAFORM_FOLDER: 'terraform' 
  SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
  SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
  SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
  SNOWFLAKE_REGION: "ap-northeast-1.aws"

jobs:
  test_job:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Configure aws credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_TERRAFORM_ANALYTICS_USER_ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.AWS_TERRAFORM_ANALYTICS_USER_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
          
      - name: setup terraform
        uses: hashicorp/setup-terraform@v1.3.2
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}

      - name: terraform init
        id: init
        working-directory: ${{ env.TERRAFORM_FOLDER }}
        run: |
          terraform init
          
      - name: terraform apply
        id: apply
        working-directory: ${{ env.TERRAFORM_FOLDER }}
        run: |
          terraform apply -no-color -auto-approve -parallelism=50

サンプルにおけるシステム構成

 今回サンプルにおいてはSnowflake内部にdabtabase・schema・tableを構築し、 SnowflakeのtaskによってAmazon S3からtableにデータ連携を行う構成で考えています。

terraformのフォルダ構成

 フォルダ構成としては以下のようなものを考えており、 module化するかどうかは主に以下の観点で考えています。

  • 複数のresourceを作成するかどうか
  • 他のデータベースでも汎用的に利用するかどうか

また基本的にリソースの作成と権限付与を同時に行っていますが、 snowflakeではfutureという「特定リソース配下の今後作成するリソースへの権限付与」を行うことができ、 そのためtaskなどでは権限付与を行わずに利用できる状態となっています。

.
├── module
   ├── database        databaseの作成及び権限の設定
      ├── main.tf
      └── variables.tf
   ├── stage           stage・integration・file formatの作成及び権限の設定
      ├── main.tf
      └── variables.tf
   └── task            procedure・taskの作成
       ├── main.tf
       └── variables.tf
├── backend.tf          terraformの設定ファイル(stateファイルの置き場など)
├── main.tf             各種モジュール呼び出し+schema・tableの作成
└── variables.tf

module・tfファイルについて

  フォルダ内部のtfファイルの概要をご説明しようと思います。

main.tf

 こちらは各種モジュール呼び出しとschema・tableの作成を行っています。 システム構成図と照らし合わせるとそれぞれ大きな枠から作成していっていることがわかります。

###########################################
# database
###########################################
module database {
  source = "./module/database"
  ...
}

###########################################
# raw schema
###########################################
resource snowflake_schema schema {
  database = module.database.database_name
  name     = "SCHEMA_NAME"
  comment  = "COMMENT"

  is_transient        = false
  is_managed          = false
  data_retention_days = 1
}

###########################################
# table
###########################################
resource snowflake_table table {
  database            = module.database.database_name
  schema              = snowflake_schema.schema.name
  name                = "TABLE_NAME"
  comment             = "COMMENT"
  change_tracking     = false

  column {
    name     = "body"
    type     = "VARIANT"
    nullable = false
  }
}

###########################################
# stage・integration・file format
###########################################
module "stage" {
  source = "./module/stage"
  ...
}

###########################################
# tasl・procedure
###########################################
module "task" {
  source = "./module/task"
  ...
}

database module

 データベースmoduleではdatabaseの作成及び権限の設定を行っています。 権限の設定は実際の利用用途によって変わってくると思いますが、 主に以下3つの権限を準備して権限付与しておけば最低限よいのではないかと思われます。

  • read権限のみを持つロール(各データベースごとに作成)
  • read・write権限を持つロール(各データベースごとに作成)
  • admin関連のロール
###########################################
# database
###########################################
resource snowflake_database database {
  name = var.name
  comment = var.description
  data_retention_time_in_days = 3
}

###########################################
# grant
###########################################
resource snowflake_database_grant grant {
  provider = snowflake.security_admin
  database_name = snowflake_database.database.name
  enable_multiple_grants = true

  privilege = "USAGE"
  roles     = [...]

  with_grant_option = false
}

...

###########################################
# output
###########################################
output "database_name" {
  value = snowflake_database.database.name
}

stage module

 ステージmoduleではstage・integration・file formatの作成及び権限の設定を行っています。 ファイルフォーマットなどは元データの形式によって変わってくるため、 variable化など行って動的に変更できるようにしても良いかもしれないです。

resource snowflake_storage_integration integration {
  provider = snowflake.account_admin
  name    = "${var.database_name}_INTEGRATION_S3"
  comment = "A storage integration for ${var.description}"
  type    = "EXTERNAL_STAGE"

  enabled = true
  storage_provider         = "S3"
  storage_aws_role_arn     = var.storage_aws_role_arn
  storage_allowed_locations = [var.storage_allowed_location]
}

resource snowflake_integration_grant grant {
  provider = snowflake.account_admin
  integration_name = snowflake_storage_integration.integration.name
  roles         = [...]
  privilege     = "USAGE"
  with_grant_option = false
}

resource snowflake_file_format json_format {
  name        = "JSON"
  database    = var.database_name
  schema      = "PUBLIC"
  format_type = "JSON"
  compression = "AUTO"
  binary_format = "UTF-8"
}

resource snowflake_stage stage {
  name        = "${var.database_name}_${var.schema_name}"
  url         = var.storage_allowed_location
  database    = var.database_name
  schema      = var.schema_name
  storage_integration = snowflake_storage_integration.integration.name
  file_format = "FORMAT_NAME = ${snowflake_file_format.json_format.database}.${snowflake_file_format.json_format.schema}.${snowflake_file_format.json_format.name}"
}

output "stage_name" {
  value = snowflake_stage.stage.name
}

output "file_format" {
  value = "${snowflake_file_format.json_format.database}.${snowflake_file_format.json_format.schema}.${snowflake_file_format.json_format.name}"
}

task module

 タスクmoduleではprocedure・taskの作成を行います。 データのロードはjavascriptのプログラムによって行うためprocedureとして定義しており、 taskの定期実行でprocedureを定期的に呼び出すことで定期更新を実現しています。

resource snowflake_procedure procedure {
  name     = "LOAD_${var.database_name}_${var.schema_name}_${var.table_name}"
  database = var.database_name
  schema   = var.schema_name
  language = "JAVASCRIPT"
  comment             = "load into ${var.database_name}.${var.schema_name}.${var.table_name}"
  return_type         = "VARCHAR"
  execute_as          = "CALLER"
  return_behavior     = "IMMUTABLE"
  statement           = <<EOT
    ...(javasvtiptでデータロードするプログラム)
EOT
}

resource snowflake_task task {
  provider = snowflake.task_admin
  comment = var.description

  database = var.database_name
  schema   = var.schema_name

  name          = "${var.database_name}_${var.schema_name}_${var.table_name}_TASK"
  schedule      = "USING CRON 45 9 * * * Asia/Tokyo"
  sql_statement = "CALL ${snowflake_procedure.procedure.database}.${snowflake_procedure.procedure.schema}.${snowflake_procedure.procedure.name}();"

  user_task_timeout_ms                     = 10000
  user_task_managed_initial_warehouse_size = "XSMALL"
  enabled                                  = true
}

output "task_name" {
  value = snowflake_task.task.name
}

terraformを利用する際の注意点

 以下terraformを利用する際にハマった注意点を載せておこうと思います。

実行roleが変わるためmodule内部にproviderが必要

 Snowflakeの特徴として作成リソースによって実行ロールを変えるケースが多い事があり、 そのためaliasを含むproviderを複数作る必要があるのですが、 これらがルートディレクトリで設定してもmodule側で利用できず各moduleにて設定が必要となってきます。 この事象はネット上でいくつか報告があり「ルートディレクトリだけでも問題ない」等の記述もありましたが、 terraformのバージョンなどの影響下僕の環境だと各moduleで設定が必要だったため、 ルートディレクトリでの設定でエラーが出る方は試してみてください。
 ただmodule側で設定した際の注意点としてmoduleの削除が少し困難になる点が有り、 例えばルートディレクトリのmoduleを読んでいる箇所をコメントアウトしてしまうと、 module配下のproviderが見つからずリソース削除時にエラーがでてしまいます。 そのためmodule内部のリソースを全てコメントアウトして削除してから、 再度ルートディレクトリのmoduleをコメントアウトすると行った手順が必要になります。

terraform {
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "0.44.0"
    }
  }
}

provider "snowflake" {
  role = "アドミン権限のロール名"
}

provider "snowflake" {
  alias = "account_admin"
  role  = "アカウントアドミン権限のロール名"
}

権限付与の際にallが利用できない

 SnowflakeではSQL利用時に以下のような書き方で全スキーマに権限付与等を行うことができますが、 terraformで記載する際にはそういったallやall schemasといった記載方法を用いることができません。

GRANT all ON all schemas IN DATABASE *** TO ROLE ***;
GRANT all ON all tables IN DATABASE *** TO ROLE ***;
GRANT all ON all procedures IN DATABASE *** TO ***;
GRANT all ON all stages IN DATABASE *** TO ROLE ***;
GRANT all ON all file formats IN DATABASE *** TO ROLE ***;

そのためそれぞれ以下のような記載方法で記述する必要があります

GRANT all:すべての権限

 以下のような全権限をlistで持たせておき、 for_each = var.all_table_privileges といった記載方法でgrant文を量産する

variable "all_table_privileges" {
  type    = list(string)
  default = [
    "INSERT",
    "DELETE",
    "SELECT",
    "UPDATE"
    ]
}

ON all schemas:全てのリソース(スキーマ・テーブルなど)

 こちらは今後作成するものに関してはSnowflakeのfuture機能を用い(grant系resourceにて on_future = true を設定)、 デフォルトで作成されるデータベースや既存のものにはそれぞれgrant系resourceを作る必要があります。

integrationはAWS側の設定が必要

 ステージmoduleの箇所にて var.storage_aws_role_arn などを指定していることからも分かる通り、 S3からデータを取得する事ができるAWS IAM roleを事前に作成しておく必要があります。 データ取得時の条件で詳細にアクセス権限が設定できるので、 用途に合わせて設定を行うようにしてください。

まとめ

 今回はSnowflakeをterraform化する際に調査した内容を記事としてまとめさせていただきました。 元々SQLで全リソースを管理できる特性から管理しやすくはなっていますが、 terraform化を進めることでより安全にリソース管理ができたように思えます。
 ただ今後手動で作成するSQLでの操作要望がでてくることが想定され、 その中で全てをterraform管理にするのか一部手動作成を許す運用にするかなどは検討が必要かと思いました。 そういった実際の運用におけるベストプラクティスは、 知見が溜まり次第こういった記事の形で発信していければと思います。

SinkCapitalではデータに関する支援を行っています

弊社はスペシャリスト人材が多く在籍するデータ組織です。 データ分析や分析基盤の設計などでお困りの方がいらっしゃれば、 まずは無料で、こちらから各分野のスペシャリストに直接相談出来ます。