Terraform、Snowflakeでs3連携・テーブルを一括管理する

Terraformで実現したいこと

  • S3 とのファイル連携に必要な Storage Integration / External Stage / File Format / Schema / Table を Terraform で管理。
  • Storage Integration は共通の IAM ロールを使い、バケット毎に Stage・Schema・Table を YAML 定義から作成。
  • stg / prd の2環境で展開。ただし スキーマ・ファイルフォーマット・テーブル定義は共通 YAMLとして持ち、各環境で必要な差分をオーバーライド可能にする 。
  • ソースコードはGitHubで公開しています。

プロジェクト構成

snowflake-terraform/
  README.md
  envs/
    _common/config/           # 環境共通の設定
      buckets.yaml
      formats.yaml
    stg/                      # 環境別のterraformプロジェクト
      config/                 # 環境固有の設定
        integration.yaml
      provider.tf
      variables.tf
      main.tf                 # modules/stack を実行
    prd/
      config/
        integration.yaml
      provider.tf
      variables.tf
      main.tf
  modules/
    stack/                    # 他モジュールを実行するメインのモジュール
      main.tf
      variables.tf
    aws_snowflake_role/
    snowflake_storage_integration/
    snowflake_stage/
    snowflake_file_format/
    ・・・
  • envs/_commonに共通設定を配置し、環境別にenvs/stg, envs/prdに差分の設定を配置することでDRYな環境にする。
  • モジュールstack/main.tfでYAML の探索パスlocals で定義し、共通 stack を呼ぶだけにする。

YAMLでリソース構成を定義

  • YAML は“共通+環境差分”でレイヤ化し、Terraform 側で 後方優先でマージします。
  • 環境差分envs/stg/config/*.yaml に必要なキーだけ 上書きできます。
  • 以下のようなYAML定義からS3バケット毎にIAMロールポリシー、外部ストレージ連携、外部ステージ、スキーマ、ソーステーブルを階層的に作成します。このようにすることでバケット毎のスキーマにテーブルを配置するルールを強制することができます。
# バケット毎に以下のSnowflakeリソースを作成する
#   外部ステージ(バケット単位)
#   スキーマ(バケット単位)
#   ソーステーブル
buckets:
  - s3_bucket: s3://poc-clue-tec.com/
    stage_name: POC_STAGE
    schema: POC
    comment: "PoC用スキーマ"
    tables:
      - name: PRODUCT
        type: csv
        comment: "製品マスタ"
        columns:
          - { name: ID, type: NUMBER, comment: "主キー" }
          - { name: TEXT_DATA, type: VARCHAR, comment: "製品名" }

  - s3_bucket: s3://demo-clue-tec.com/
    stage_name: DEMO_STAGE
    schema: DEMO
    tables:
      - name: RAW_EVENTS
        type: json
        comment: "イベントJSON"
  • 上記階層をlocalに展開するため、Terraform では locals.cfgs = [for p in yaml_files : yamldecode(file(p))]merge() / flatten() を使い 統合。for_each で各リソースへ流す。(後述)
## YAMLを展開
locals {
  yaml_files = flatten([
    for d in var.config_dir_list : [
      for f in fileset(d, "*.yaml") : "${d}/${f}"
    ]
  ])
  cfgs = [for p in local.yaml_files : yamldecode(file(p))]
  buckets_lists = [for c in local.cfgs : try(c.buckets, [])]
}

Storage Integration と IAMの循環依存の回避

  • 鶏と卵の問題。Storage Integration を作るには AWS Role ARN が必要。 一方 IAM ロールの信頼ポリシーには Snowflake 側の iam_user_arn / external_id が必要になります。
    • 初回は以下の設定(envs/stg/config/integration.yaml)で最初は IAM ロールの信頼ポリシーをデフォルト(arn="*" / external_id=0000)でIAMロールとStorage Integration を作成 する。
    • Integration の storage_aws_iam_user_arn / storage_aws_external_id を取得する。
    • IAM ロールの信頼ポリシーにそれらを反映(以下の設定ファイルを編集)し、再適用する。
integration:
  # 以下はSTG_STORAGE_INTEGRATION初回作成後に編集する
  snowflake_iam_user_arn: "*"
  snowflake_external_id: 0000
# Snowflake の IAM ユーザーにのみ引受けを許す信頼ポリシー(ExternalId 条件付き)
data "aws_iam_policy_document" "trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = [var.snowflake_iam_user_arn]
    }
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [var.snowflake_external_id]
    }
  }
}

Stage / File Format / Schema / Table をYAMLから自動生成

External Stage(バケット単位)

  • Stage は Storage Integration 参照で作成します。
module "stages" {
  source   = "../snowflake_stage"

  # buckets の stage_name をキーにする
  for_each = { for b in local.buckets : b.stage_name => b }

  name        = each.key
  database    = local.integration.admin_database
  schema      = local.integration.admin_schema
  url         = each.value.s3_bucket

  storage_integration_name = module.storage_integration.name

  comment = "Managed by Terraform"
}

File Format(共通 + 環境差分)

  • CSV/JSON などの 共通フォーマットを YAML から mergeします。
module "file_formats" {
  source   = "../snowflake_file_format"

  for_each = merge(local.default_file_formats, local.custom_file_formats)
  name        = each.key
  database    = local.integration.admin_database
  schema      = local.integration.admin_schema
  format_type = upper(each.value.format_type)

Schema(バケットの schema をユニークに作成)

  • バケットのリストからスキーマ名を取得し、展開。
module "schemas" {
  source   = "../snowflake_schema"

  for_each = {
    for b in local.buckets :
    b.schema => {
      database = local.integration.source_database
      name     = b.schema
      comment  = try(b.comment, null)
    }
  }
  database = each.value.database
  name     = each.value.name
  comment  = each.value.comment
}

Table(schema + tables[] で定義)

  • type=json の場合は **VARIANT 1列(既定名 "DATA")**を自動作成。
  • type=csv の場合は YAML の columns を採用(STRINGVARCHAR に正規化など)
module "tables" {
  source   = "../snowflake_table"

  for_each = merge([
    for b in local.buckets : {
      for t in try(b.tables, []) :
      "${b.schema}.${t.name}" => {
        schema  = b.schema
        name    = t.name
        comment = try(t.comment, null)
        type    = try(t.type, "csv")
        columns = try(t.columns, [])
      }
    }
  ]...)
  database = local.integration.source_database
  schema   = each.value.schema
  name     = each.value.name
  type     = each.value.type
  columns  = each.value.columns
  comment  = each.value.comment
}

環境別運用(backend 固定・認証の扱い)

  • Terraformはbackendの変数は定義できない仕様のため、backend(S3) は root の provider.tf に固定値で定義(bucket/key/region など)。
  • Terragruntを利用すればここら辺はDRYに書けるが、このためにTerraformのラッパーを使うほどのことでもないし、今回は環境2面のみなので固定で設定することにした。
  • AWS 認証AWS_PROFILE(共有クレデンシャル)を環境変数に設定。
  • Snowflake 認証鍵ペア(JWT)を使用し、TF_VAR_private_key_pem 経由で渡します。

まとめ

  • YAML 共通定義 + 環境別オーバーライドで、stg/prd の両環境に 同一パターンを適用することができました。
  • Storage Integration ↔ IAM ロールの循環依存は、Role ARN 先決 → Integration 作成 → IAM 信頼更新の2段階で解消。
  • Terraform の制約(for_each、backend、Preview 等)を踏まえた root+共通モジュール構成で、YAMLでリソース作成ルールを強制。