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 ロールの信頼ポリシーにそれらを反映(以下の設定ファイルを編集)し、再適用する。
- 初回は以下の設定(envs/stg/config/integration.yaml)で最初は 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の場合は **VARIANT1列(既定名"DATA")**を自動作成。type=csvの場合は YAML のcolumnsを採用(STRINGをVARCHARに正規化など)
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でリソース作成ルールを強制。
