Snowflake × Terraform で S3 Storage Integration を構築してみた
はじめに
データ事業本部の kasama です。
今回はSnowflake と AWS S3 を連携させる Storage Integration を Terraform で構築してみたいと思います。
前提
前提として、オープンソース版のTerraformの話であり、HCP Terraform(クラウド版)は考慮していません。
以下のブログで記載した構成を元に実装していきたいと思います。moduleをユーザータイプ単位で作成し、dev/prdごとのmain.tfで呼び出す実装です。terraformのinitやplan,applyコマンドの実行はGitHub Actions経由で行います。
snowflake-terraform-sample % tree
.
├── .github
│ └── workflows
│ ├── dev-snowflake-terraform-cicd.yml
│ └── prd-snowflake-terraform-cicd.yml
├── environments
│ ├── dev
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── output.tf
│ │ ├── variable.tf
│ │ └── versions.tf
│ └── prd
│ ├── backend.tf
│ ├── main.tf
│ ├── output.tf
│ ├── variable.tf
│ └── versions.tf
└── modules
└── data_engineer
├── main.tf
├── outputs.tf
├── variables.tf
└── versions.tf
S3 Storage Integration作成時の循環依存とその解決策
Terraformでの課題
Snowflake公式ドキュメントでは、以下の順序でリソースを作成します。
- S3バケット用のIAMポリシーを作成
- IAM Roleを作成(Trust Policyに自分のAWSアカウントIDを一時的に設定)
- SnowflakeでStorage Integrationを作成(Step 2のIAM Role ARNを指定)
DESC INTEGRATIONでSnowflakeが生成したSTORAGE_AWS_IAM_USER_ARNとSTORAGE_AWS_EXTERNAL_IDを取得- IAM RoleのTrust Policyを更新(Step 4で取得した値を設定)
- Snowflakeで外部Stageを作成
この手順は手動での構築には適していますが、Terraformで実装する場合は一度作成したIAM RoleのTrust Policyを後から変更する必要があるため、1回のterraform applyで完結しません。初回apply後に手動でStorage Integrationから値を確認し、IAM RoleのTrust Policyを更新して再度applyする必要があり、運用の手間が増えます。
解決策
- IAM Role ARNを事前計算
- Storage Integrationを作成
- S3バケットを作成
- IAM Roleを作成(正しいTrust Policy)
- S3バケット用のIAMポリシーを作成
- 外部Stageを作成
IAM RoleのARNは予測可能な形式(arn:aws:iam::{アカウントID}:role/{ロール名})のため、AWSアカウントIDとロール名から事前に文字列として構築できます。SnowflakeがStorage Integration作成時にIAM Roleの存在を確認しないため、まだ存在しないIAM RoleのARNを使ってStorage Integrationを先に作成し、その出力値を使ってIAM Roleを後から作成することで、循環依存を回避できます。
実装
それでは実際の実装を見ていきます。
dev環境とprd環境では細かい設定値以外は同様であるため、今回はdev環境のみの実装とします。
GitHub ActionsのworkflowやSnowflakeデプロイ用ユーザーの構築手順については以下のブログで記載しましたので今回は省略します。
terraform {
backend "s3" {
bucket = "<YOUR_TERRAFORM_STATE_BUCKET>" # 例: your-company-dev-terraform-state
key = "snowflake/snowflake.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
}
}
Terraform stateファイルの保存先を定義します。S3 bucket名は適切な値に置き換えてください。
# ========================================================================================
# S3 Storage Integration Configuration - Dev Environment
# ========================================================================================
# This configuration uses the data_engineer module for S3 storage integration
# ========================================================================================
module "data_engineer" {
source = "../../modules/data_engineer"
providers = {
snowflake.sysadmin = snowflake.sysadmin
snowflake.accountadmin = snowflake.accountadmin
}
# Basic configuration
database_name = "RAW_DATA_DEV"
warehouse_name = "RAW_DATA_DEV_ADMIN_WH"
# Warehouse configuration
warehouse_size = "XSMALL"
warehouse_auto_suspend = 60
warehouse_initially_suspended = true
warehouse_min_cluster_count = 1
warehouse_max_cluster_count = 1
# Storage Integration configuration
storage_integration_name = "S3_DEV_INTEGRATION"
storage_integration_enabled = true
stage_name = "DEV_STAGE"
stage_schema = "PUBLIC"
# AWS configuration
aws_s3_bucket_name = "<YOUR_S3_BUCKET_NAME>" # 例: your-company-snowflake-dev-data
aws_iam_role_name = "<YOUR_IAM_ROLE_NAME>" # 例: your-company-snowflake-dev-role
aws_iam_policy_name = "<YOUR_IAM_POLICY_NAME>" # 例: snowflake-s3-dev-access-policy
# AWS Tags
aws_tags = {
Environment = "dev"
ManagedBy = "Terraform"
Project = "snowflake-terraform-sample"
}
}
data_engineer moduleを呼び出し、Storage Integration、Database、Warehouse、Stageを一括作成します。ProvidersブロックでSnowflakeの複数role(sysadmin/accountadmin)を明示的に渡しています。AWS関連のリソース名(S3 Bucket、IAM Role、IAM Policy)は適切な値に置き換えてください。
########################
# Snowflake authentication
########################
variable "snowflake_organization_name" {
type = string
description = "Snowflake organization name"
}
variable "snowflake_account_name" {
type = string
description = "Snowflake account name"
}
variable "snowflake_private_key" {
description = "Snowflake private key content (for CI/CD)"
type = string
default = ""
sensitive = true
}
Snowflake接続に必要な変数を定義します。これらの値はGitHub ActionsのSecretsからTF_VAR_プレフィックスで渡されます。
terraform {
required_version = ">= 1.5.0"
required_providers {
snowflake = {
source = "snowflakedb/snowflake"
version = "~> 2.3.0"
}
aws = {
source = "hashicorp/aws"
version = "6.18.0"
}
}
}
# Snowflake provider with SYSADMIN role
provider "snowflake" {
alias = "sysadmin"
role = "SYSADMIN"
organization_name = var.snowflake_organization_name
account_name = var.snowflake_account_name
user = "TERRAFORM_BLOG_USER"
authenticator = "SNOWFLAKE_JWT"
private_key = var.snowflake_private_key
warehouse = "TERRAFORM_BLOG_WH"
preview_features_enabled = [
"snowflake_stage_resource"
]
}
# Snowflake provider with ACCOUNTADMIN role (for admin module)
provider "snowflake" {
alias = "accountadmin"
role = "ACCOUNTADMIN"
organization_name = var.snowflake_organization_name
account_name = var.snowflake_account_name
user = "TERRAFORM_BLOG_USER"
authenticator = "SNOWFLAKE_JWT"
private_key = var.snowflake_private_key
warehouse = "TERRAFORM_BLOG_WH"
preview_features_enabled = [
"snowflake_storage_integration_resource"
]
}
provider "aws" {
region = "ap-northeast-1"
}
TerraformとProviderのバージョンを固定し、2つのSnowflake Provider(sysadmin/accountadmin)を定義します。Storage Integrationの作成にはACCOUNTADMIN権限が必要なため、aliasを使用して複数のRoleを使い分けます。
# AWS caller identity for ARN construction
data "aws_caller_identity" "current" {}
# Local variables for computed values
locals {
s3_bucket_url = "s3://${var.aws_s3_bucket_name}/"
aws_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.aws_iam_role_name}"
}
# Database
resource "snowflake_database" "db" {
provider = snowflake.sysadmin
name = var.database_name
}
# Warehouse
resource "snowflake_warehouse" "wh" {
provider = snowflake.sysadmin
name = var.warehouse_name
warehouse_size = var.warehouse_size
auto_suspend = var.warehouse_auto_suspend
initially_suspended = var.warehouse_initially_suspended
min_cluster_count = var.warehouse_min_cluster_count
max_cluster_count = var.warehouse_max_cluster_count
}
# S3 Storage Integration
resource "snowflake_storage_integration" "s3_integ" {
provider = snowflake.accountadmin
name = var.storage_integration_name
type = "EXTERNAL_STAGE"
enabled = var.storage_integration_enabled
storage_provider = "S3"
storage_aws_role_arn = local.aws_role_arn
storage_allowed_locations = [local.s3_bucket_url]
}
# ===========================
# AWS Resources
# ===========================
# S3バケット、暗号化、バージョニング設定
resource "aws_s3_bucket" "snowflake_storage" {
bucket = var.aws_s3_bucket_name
tags = var.aws_tags
}
# S3バケット暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "snowflake_storage" {
bucket = aws_s3_bucket.snowflake_storage.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_versioning" "snowflake_storage" {
bucket = aws_s3_bucket.snowflake_storage.id
versioning_configuration {
status = "Enabled"
}
}
# IAMロール(Snowflake Storage Integrationの出力値を使用)
resource "aws_iam_role" "snowflake_access" {
name = var.aws_iam_role_name
path = "/"
# Snowflake Storage Integrationの出力を直接使用
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TrustIamUserOfSnowflakePolicy"
Effect = "Allow"
Principal = {
AWS = snowflake_storage_integration.s3_integ.storage_aws_iam_user_arn
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"sts:ExternalId" = snowflake_storage_integration.s3_integ.storage_aws_external_id
}
}
}
]
})
tags = var.aws_tags
}
resource "aws_iam_role_policy" "snowflake_s3_access" {
name = var.aws_iam_policy_name
role = aws_iam_role.snowflake_access.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadAndWriteObjectsInSpecificBucketPolicy"
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:DeleteObject",
"s3:DeleteObjectVersion"
]
Resource = "${aws_s3_bucket.snowflake_storage.arn}/*"
},
{
Sid = "ListBucketAndGetLocationAllowPolicy"
Effect = "Allow"
Action = [
"s3:ListBucket",
"s3:GetBucketLocation"
]
Resource = aws_s3_bucket.snowflake_storage.arn
Condition = {
StringLike = {
"s3:prefix" = "*"
}
}
}
]
})
}
resource "aws_s3_bucket_policy" "snowflake_access" {
bucket = aws_s3_bucket.snowflake_storage.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowSnowflakeAccess"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.snowflake_access.arn
}
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.snowflake_storage.arn,
"${aws_s3_bucket.snowflake_storage.arn}/*"
]
}
]
})
}
# ===========================
# Snowflake Stage (depends on AWS resources)
# ===========================
# External Stage
resource "snowflake_stage" "stage" {
provider = snowflake.sysadmin
database = snowflake_database.db.name
schema = var.stage_schema
name = var.stage_name
url = local.s3_bucket_url
storage_integration = snowflake_storage_integration.s3_integ.name
# IAMロール作成後にStageを作成
depends_on = [
aws_iam_role.snowflake_access,
aws_iam_role_policy.snowflake_s3_access
]
}
# Grant Storage Integration to SYSADMIN
resource "snowflake_grant_privileges_to_account_role" "integ_to_sysadmin" {
provider = snowflake.accountadmin
privileges = ["USAGE"]
account_role_name = "SYSADMIN"
on_account_object {
object_type = "INTEGRATION"
object_name = snowflake_storage_integration.s3_integ.name
}
}
moduleのmain実装です。ポイントは以下になります。
- ARN事前計算:
data.aws_caller_identity.currentでAWSアカウントIDを取得し、local.aws_role_arnでIAM Role ARNを事前に文字列として構築します。 - Storage Integration作成: 事前計算した
local.aws_role_arnを使用してStorage Integrationを作成します。この時点ではまだIAM Roleは存在していません。 - IAM Role作成:
snowflake_storage_integration.s3_integ.storage_aws_iam_user_arnとstorage_aws_external_idを使用してTrust Policyを構築します。これによりSnowflakeが正しくAssumeRoleできるようになります。 - Stage作成:
depends_onでaws_iam_role.snowflake_accessとaws_iam_role_policy.snowflake_s3_accessを明示的に指定し、IAMリソースの作成完了後にStageを作成します。
# Basic configuration
variable "database_name" {
description = "Database name"
type = string
}
variable "warehouse_name" {
description = "Warehouse name"
type = string
}
# Warehouse configuration
variable "warehouse_size" {
description = "Warehouse size"
type = string
}
variable "warehouse_auto_suspend" {
description = "Auto suspend time in seconds"
type = number
}
variable "warehouse_initially_suspended" {
description = "Initially suspended"
type = bool
}
variable "warehouse_min_cluster_count" {
description = "Minimum cluster count"
type = number
}
variable "warehouse_max_cluster_count" {
description = "Maximum cluster count"
type = number
}
variable "storage_integration_name" {
description = "Storage integration name"
type = string
}
variable "stage_name" {
description = "Stage name"
type = string
}
variable "stage_schema" {
description = "Schema for the stage"
type = string
}
variable "storage_integration_enabled" {
description = "Storage integration enabled"
type = bool
}
# AWS configuration
variable "aws_s3_bucket_name" {
description = "S3 bucket name for Snowflake storage"
type = string
}
variable "aws_iam_role_name" {
description = "IAM role name for Snowflake access"
type = string
}
variable "aws_iam_policy_name" {
description = "IAM policy name"
type = string
}
variable "aws_tags" {
description = "AWS resource tags"
type = map(string)
default = {}
}
moduleの入力変数を定義します。Snowflakeリソース(Database、Warehouse、Storage Integration、Stage)とAWSリソース(S3、IAM)の設定に必要なパラメータを受け取ります。
terraform {
required_providers {
snowflake = {
source = "snowflakedb/snowflake"
version = "~> 2.3.0"
configuration_aliases = [
snowflake.sysadmin,
snowflake.accountadmin
]
}
aws = {
source = "hashicorp/aws"
version = "6.18.0"
}
}
}
moduleのprovider要件を定義します。aliasでsysadminとaccountadminの2つのSnowflake provider aliasを受け取れるようにしています。呼び出し側(environments/dev/main.tf)でprovidersブロックを使用して実際のproviderを渡す必要があります。
デプロイ
実装が完了したら、GitHub Actions 経由でデプロイします。
featureブランチで実装してpushします。
git checkout -b feature/s3-storage-integration
git add .
git commit -m "feat(dev): Add S3 Storage Integration"
git push origin feature/s3-storage-integration
GitHub 上で feature ブランチから develop ブランチへ Pull Request を作成します。

Pull Request を作成すると GitHub Actions が自動実行され、terraform plan の結果が PR にコメントされます。

PR を承認してマージすると、GitHub Actions が terraform apply を自動実行し、リソースがデプロイされます。

デプロイ後の確認
デプロイが完了したら、サンプルデータで Storage Integration が正しく動作することを確認します。
サンプルCSVデータの準備
AWS CloudShell上で以下のコマンドを実行し、S3にテストデータをuploadします。
# サンプルデータを作成
cat > sample_data.csv << 'EOF'
id,name,age,email
1,Alice,30,alice@example.com
2,Bob,25,bob@example.com
3,Charlie,35,charlie@example.com
4,Diana,28,diana@example.com
5,Eve,32,eve@example.com
EOF
# S3にアップロード
aws s3 cp sample_data.csv s3://<YOUR_S3_BUCKET_NAME>/sample_data/
Snowflakeでデータを確認
デプロイしたStorage Integration、IAM Role、外部Stageが正しく連携し、S3からSnowflakeへデータをロードできることを確認します。
まずは、外部StageがS3 Bucketのファイルを正しく認識できるか(Storage IntegrationとIAM Roleの認証が成功しているか)を確認します。
-- Stageのファイルを確認
LIST @RAW_DATA_DEV.PUBLIC.DEV_STAGE/sample_data/;
S3に格納されているデータを参照できました。

次に、COPY INTOコマンドでS3からSnowflakeテーブルへデータをロードできるかを確認します。
-- テーブル作成
USE ROLE SYSADMIN;
USE DATABASE RAW_DATA_DEV;
USE WAREHOUSE RAW_DATA_DEV_ADMIN_WH;
CREATE OR REPLACE TABLE users (
id INTEGER,
name STRING,
age INTEGER,
email STRING
);
-- Stageからデータをロード
COPY INTO users
FROM @DEV_STAGE/sample_data/sample_data.csv
FILE_FORMAT = (
TYPE = 'CSV'
FIELD_DELIMITER = ','
SKIP_HEADER = 1
FIELD_OPTIONALLY_ENCLOSED_BY = '"'
);
-- データ確認
SELECT * FROM users;
S3からSnowflakeへのデータロードが正常に動作していることが確認できました。

クリーンアップの方法はS3 Bucketの中身を空にしてからenvironments/dev/main.tfをコメントアウトしてデプロイすればOKです。
最後に
SnowflakeのS3 Storage IntegrationをTerraformでデプロイする際に2回に分けてデプロイする必要があると思い悩んでいましたので、同じような方の参考になれば幸いです。







