Snowflake × Terraform × GitHub Actions で Workload Identity Federation を構築してみた
はじめに
データ事業本部の kasama です。
今回は Snowflake × Terraform で Workload Identity Federation (WIF) を使った認証基盤を構築してみました。
従来のKey Pair認証では秘密鍵(private_key)を管理する必要がありましたが、WIFを使用することで秘密鍵不要でセキュアな認証を実現できます。GitHub ActionsとAWS IAM Roleを組み合わせることで、一時認証情報のみを使用した安全な運用が可能になります。
前提
前提として、オープンソース版のTerraformの話であり、HCP Terraform(クラウド版)は考慮していません。
以下のブログで記載した構成を元に実装していきたいと思います。moduleをユーザータイプ単位で作成し、dev/prdごとのmain.tfで呼び出す実装です。terraformのinitやplan,applyコマンドの実行はGitHub Actions経由で行います。
また、前回のブログでSnowflake と AWS S3 を連携させる Storage Integration を Terraform で構築しました。今回はその構成にWorkload Identity Federation認証を適用していきます。
snowflake-terraform-sample % tree
.
├── .github
│ └── workflows
│ └── dev-snowflake-terraform-cicd.yml
├── cfn
│ └── create_state_gha_resources.yml
├── environments
│ └── dev
│ ├── backend.tf
│ ├── main.tf
│ ├── variable.tf
│ └── versions.tf
└── modules
└── data_engineer
├── main.tf
├── outputs.tf
├── variables.tf
└── versions.tf
今回使用するSnowflake Provider は 2.11.0 です。
従来の認証方式(Key Pair認証)
まず、従来のKey Pair認証について説明します。
Key Pair認証では、RSA公開鍵/秘密鍵のペアを使ってSnowflakeに認証します。Snowflakeユーザーに公開鍵を登録し、秘密鍵(private_key)をGitHub Secretsに保存してTerraform Providerに渡すことで認証していました。
provider "snowflake" {
role = "SYSADMIN"
organization_name = var.snowflake_organization_name
account_name = var.snowflake_account_name
user = "TERRAFORM_USER"
authenticator = "SNOWFLAKE_JWT"
private_key = var.snowflake_private_key # 秘密鍵が必要
warehouse = "TERRAFORM_WH"
}
この方式では、秘密鍵の管理には定期的なローテーションや安全な保存が必要であり、運用負荷がかかります。
Workload Identity Federationとは
Workload Identity Federation(WIF)は、各クラウドプロバイダーが提供する認証機能を使ってSnowflakeに認証する仕組みです。具体的には、AWS IAM Role、Microsoft Entra ID、Google Cloud service accountsなどに対応しています。
従来のKey Pair認証との大きな違いは、秘密鍵を使用せず一時的な認証情報のみを使用する点です。
Snowflake Provider 2.10.0から導入された認証方式です。
今回のブログでは、AWS IAM Roleを使用したWIFの実装例を紹介します。
WIF認証の仕組み
WIF認証のフローは以下のようになります。
- GitHub ActionsがOIDCトークンを要求し、GitHub OIDC Providerが発行
- GitHub ActionsがOIDCトークンを使ってAWS IAM RoleをAssumeRole
- AWS STSが一時認証情報を発行し、環境変数に設定
- Terraform Snowflake Providerが環境変数から認証情報を自動検出
- Terraform ProviderがAWS IAM Roleの身元証明をSnowflakeに送信
- Snowflakeが身元証明を検証し、事前に許可されたIAM Roleであることを確認して認証
詳細な仕組みはSnowflake公式ドキュメントと以下の記事を参考に、私の理解に基づいて記載しているので、誤りがある可能性もあります。Snowflake公式ドキュメントに明確な記載は見つけられませんでした。。
Key Pair認証と比較して、WIFには以下のメリットがあります。
- 秘密鍵の管理不要: 秘密鍵を生成・保存・ローテーションする必要がありません
- 自動的な認証情報ローテーション: 一時認証情報は短時間で自動的に無効化されます
- セキュリティ向上: 認証情報の漏洩リスクが大幅に低減されます
- 運用負荷の軽減: GitHub Secretsに秘密鍵を保存する必要がなく、設定もシンプルになります
- アクティビティログの記録: AWS CloudTrailなどのクラウドプロバイダーサービスを利用して、アクティビティをログ記録および監視できます。
事前準備
WIF認証を使用するため、以下の事前準備が必要です。
AWS IAM Roleの作成
まず、GitHub ActionsがAssumeRoleできるIAM Roleを作成します。CloudFormationで作成する場合、以下のようになります。
cfn/create_state_gha_resources.yml
AWSTemplateFormatVersion: 2010-09-09
Description: |-
Terraform S3 & GitHub Actions Setup - Creates S3 bucket for Terraform state
and IAM role for GitHub Actions with comprehensive configuration
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Project Configuration"
Parameters:
- ProjectName
- EnvName
- Label:
default: "GitHub Configuration"
Parameters:
- GitHubAccountName
- GitHubRemoteRepoName1
- GitHubRemoteRepoName2
- GitHubOIDCProviderArn
Parameters:
ProjectName:
Type: String
Description: Project name used for resource naming
EnvName:
Type: String
Default: dev
AllowedValues:
- dev
- stg
- prd
Description: Environment name
GitHubAccountName:
Type: String
Description: GitHub account name of the repository
GitHubRemoteRepoName1:
Type: String
Description: First GitHub repository name
GitHubRemoteRepoName2:
Type: String
Description: Second GitHub repository name
GitHubOIDCProviderArn:
Type: String
Description: ARN of the existing GitHub OIDC Provider
Resources:
# S3 Bucket for Terraform state
TerraformStateBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${ProjectName}-${EnvName}-s3-terraform-state"
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Tags:
- Key: project
Value: !Ref ProjectName
- Key: environment
Value: !Ref EnvName
# IAM Role for GitHub Actions
GitHubActionsRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ProjectName}-${EnvName}-iamrole-gha"
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Sid: TrustGitHubActionsOIDCProvider
Effect: Allow
Principal:
Federated: !Ref GitHubOIDCProviderArn
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringLike:
token.actions.githubusercontent.com:sub:
- !Sub "repo:${GitHubAccountName}/${GitHubRemoteRepoName1}:*"
- !Sub "repo:${GitHubAccountName}/${GitHubRemoteRepoName2}:*"
Policies:
- PolicyName: !Sub "${ProjectName}-${EnvName}-iampolicy-gha"
PolicyDocument:
Version: 2012-10-17
Statement:
# Terraform state S3 bucket permissions
- Sid: TerraformS3StateBucketPolicy
Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
- s3:ListBucketMultipartUploads
- s3:ListBucketVersions
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
- s3:DeleteObject
Resource:
- !Sub "arn:aws:s3:::${TerraformStateBucket}"
- !Sub "arn:aws:s3:::${TerraformStateBucket}/*"
# S3 bucket management for Terraform modules
- Sid: ModuleS3BucketManagement
Effect: Allow
Action:
- s3:CreateBucket
- s3:DeleteBucket
- s3:PutBucketVersioning
- s3:GetBucketTagging
- s3:PutEncryptionConfiguration
- s3:GetEncryptionConfiguration
- s3:PutBucketTagging
- s3:PutBucketPolicy
- s3:GetBucketPolicy
- s3:DeleteBucketPolicy
- s3:GetBucketLocation
- s3:GetBucketAcl
- s3:GetBucketCORS
- s3:ListBucket
- s3:GetBucketWebsite
- s3:GetBucketVersioning
- s3:GetAccelerateConfiguration
- s3:GetBucketRequestPayment
- s3:GetBucketLogging
- s3:GetLifecycleConfiguration
- s3:GetReplicationConfiguration
- s3:GetBucketObjectLockConfiguration
- s3:PutBucketNotification
- s3:GetBucketNotification
Resource:
- !Sub "arn:aws:s3:::${ProjectName}-${EnvName}-s3-*"
- !Sub "arn:aws:s3:::${ProjectName}-*"
# IAM role management for Terraform modules
- Sid: ModuleIamRoleManagement
Effect: Allow
Action:
- iam:CreateRole
- iam:DeleteRole
- iam:GetRole
- iam:TagRole
- iam:PutRolePolicy
- iam:GetRolePolicy
- iam:DeleteRolePolicy
- iam:ListRolePolicies
- iam:ListAttachedRolePolicies
- iam:ListInstanceProfilesForRole
- iam:UpdateAssumeRolePolicy
- iam:UntagRole
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/${ProjectName}-${EnvName}-iamrole-*"
- !Sub "arn:aws:iam::${AWS::AccountId}:role/${ProjectName}-*"
Trust PolicyでGitHub OIDC Providerを信頼し、特定のGitHubリポジトリからのみAssumeRoleを許可しています。このIAM Roleはあくまで「GitHub ActionsがTerraformを実行するため」のものであり、Snowflakeに関する設定は不要です。この実装は、Key-pairの時から特に変更はありません。
CloudFormationスタックをAWS Management Consoleから作成します。
Snowflakeユーザーの作成
次に、Snowflake側でWIF対応のサービスアカウントを作成します。上記で取得したIAM Role ARNを使用します。
-- 1. Terraformサービスアカウント作成(WIF対応)
USE ROLE SECURITYADMIN;
CREATE USER TERRAFORM_BLOG_USER
TYPE = SERVICE
WORKLOAD_IDENTITY = (
TYPE = AWS
ARN = 'arn:aws:iam::<aws_account_id>:role/<your-iam-role-name>'
)
COMMENT = 'Terraform automation service account with Workload Identity Federation';
-- 2. 最小限のロール付与(3つのみ)
-- SYSADMIN: Database/Warehouse/Stage作成
-- ACCOUNTADMIN: Storage Integration作成
-- SECURITYADMIN: Grant操作
USE ROLE ACCOUNTADMIN;
GRANT ROLE SYSADMIN TO USER TERRAFORM_BLOG_USER;
GRANT ROLE ACCOUNTADMIN TO USER TERRAFORM_BLOG_USER;
GRANT ROLE SECURITYADMIN TO USER TERRAFORM_BLOG_USER;
-- 3. 設定確認
SHOW USER WORKLOAD IDENTITY AUTHENTICATION METHODS FOR USER TERRAFORM_BLOG_USER;
DESC USER TERRAFORM_BLOG_USER;
-- 4. ウェアハウス作成
USE ROLE SYSADMIN;
CREATE OR REPLACE WAREHOUSE TERRAFORM_BLOG_WH
WAREHOUSE_SIZE = XSMALL
AUTO_RESUME = TRUE
AUTO_SUSPEND = 60
INITIALLY_SUSPENDED = TRUE
STATEMENT_TIMEOUT_IN_SECONDS = 300
COMMENT = 'For terraform automation only';
TYPE = SERVICEで作成し、WORKLOAD_IDENTITYにAWS IAM Role ARNを指定します。これにより、このユーザーはWIF認証でのみログイン可能になります。
GitHub Secretsの設定
GitHub Secretsに以下の値を設定します。従来必要だったSNOWFLAKE_PRIVATE_KEYは不要です。
SNOWFLAKE_ORG_NAME- Snowflake組織名SNOWFLAKE_ACCOUNT_NAME- Snowflakeアカウント名AWS_ROLE_ARN- AWS IAMロールARN(CloudFormationで作成したもの)
実装
それでは実際のTerraform実装を見ていきます。
terraform {
required_version = ">= 1.5.0"
required_providers {
snowflake = {
source = "snowflakedb/snowflake"
version = "~> 2.11.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 = "WORKLOAD_IDENTITY"
workload_identity_provider = "AWS"
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 = "WORKLOAD_IDENTITY"
workload_identity_provider = "AWS"
warehouse = "TERRAFORM_BLOG_WH"
preview_features_enabled = [
"snowflake_storage_integration_resource"
]
}
provider "aws" {
region = "ap-northeast-1"
}
Snowflake Provider 2.11.0では、authenticator = "WORKLOAD_IDENTITY"を設定すると、環境変数に設定されたAWS一時認証情報(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKEN)を自動的に検出して使用します。
variable "snowflake_organization_name" {
type = string
description = "Snowflake organization name"
}
variable "snowflake_account_name" {
type = string
description = "Snowflake account name"
}
JWT認証で必要だったsnowflake_private_key変数を削除します。
シンプルに組織名とアカウント名のみになりました。
name: "Snowflake Terraform CI/CD - DEV"
on:
push:
branches:
- develop
paths:
- "environments/dev/**"
- "modules/**"
- ".github/workflows/dev-snowflake-terraform-cicd.yml"
pull_request:
branches:
- develop
paths:
- "environments/dev/**"
- "modules/**"
- ".github/workflows/dev-snowflake-terraform-cicd.yml"
jobs:
plan-dev:
runs-on: ubuntu-latest
timeout-minutes: 30
environment: dev
env:
# Terraform
TF_VERSION: "1.11.0"
# Snowflake - TF_VAR_プレフィックスでTerraform変数にマッピング
TF_VAR_snowflake_organization_name: ${{ secrets.SNOWFLAKE_ORG_NAME }}
TF_VAR_snowflake_account_name: ${{ secrets.SNOWFLAKE_ACCOUNT_NAME }}
permissions:
id-token: write # OIDCを利用する際に必須
contents: read # actions/checkout のために必要
pull-requests: write # PRコメント投稿用
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Set up AWS credentials
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
audience: sts.amazonaws.com
- name: Terraform format
run: terraform fmt -check -recursive
working-directory: environments/dev
- name: Terraform Init
run: terraform init -upgrade -no-color
working-directory: environments/dev
- name: Terraform validate
run: terraform validate -no-color
working-directory: environments/dev
- name: Setup TFLint
uses: terraform-linters/setup-tflint@acd1575d3c037258ce5b2dd01379dc49ce24c6b7 # v6.2.0
with:
tflint_version: v0.58.0
- name: Run TFLint
run: tflint --init && tflint -f compact --minimum-failure-severity=error
working-directory: environments/dev
- name: Run Trivy Security Scan
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
scan-type: "config"
scan-ref: "environments/dev"
format: "table"
exit-code: 1
severity: CRITICAL,HIGH
ignore-unfixed: true
- name: Terraform Plan
id: plan
if: github.event_name == 'pull_request'
run: |
terraform plan -no-color -out=tfplan.binary
terraform show -no-color tfplan.binary > tfplan.txt
working-directory: environments/dev
- name: Post Plan to PR
if: github.event_name == 'pull_request' && steps.plan.outcome == 'success'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('environments/dev/tfplan.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `### Terraform Plan - Dev
<details>
<summary>Show Plan</summary>
\`\`\`terraform
${plan.substring(0, 65000)}
\`\`\`
</details>`
});
- name: Terraform Apply
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
run: terraform apply -auto-approve
working-directory: environments/dev
GitHub ActionsでAWS OIDC認証を使用してIAM RoleをAssumeRoleします。
aws-actions/configure-aws-credentialsアクションがAWS一時認証情報を環境変数に設定してくれるため、Terraform Snowflake Providerは自動的にこれを検出して使用します。
Storage Integrationの実装における注意点
WIFと関係ないのですが、version upにより、storage integrationでエラーとなったので、注意書きとして残しておきます。Snowflake Provider 2.5.0以降では、snowflake_storage_integrationリソースのstorage_aws_external_id属性はdescribe_output経由でアクセスする必要があります。
// 誤った実装例
resource "aws_iam_role" "snowflake_access" {
assume_role_policy = jsonencode({
Statement = [{
Principal = {
AWS = snowflake_storage_integration.s3_integ.storage_aws_iam_user_arn
}
Condition = {
StringEquals = {
# 直接アクセス(Provider 2.5.0以降ではエラー)
"sts:ExternalId" = snowflake_storage_integration.s3_integ.storage_aws_external_id
}
}
}]
})
}
// 正しい実装例
resource "aws_iam_role" "snowflake_access" {
name = var.aws_iam_role_name
path = "/"
# storage_aws_iam_user_arn: 直接アクセス可能
# storage_aws_external_id: describe_output経由でアクセス(Provider 2.5.0以降の仕様)
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 = {
# describe_output経由でアクセス
"sts:ExternalId" = snowflake_storage_integration.s3_integ.describe_output[0].storage_aws_external_id[0].value
}
}
}
]
})
tags = var.aws_tags
}
describe_outputは、SnowflakeのDESCRIBE INTEGRATIONコマンドの結果を構造化したものです。Provider 2.5.0以降では、storage_aws_external_idはこの方法でアクセスする必要があります。公式ドキュメントにも以下の注意書きがありました。
Currently, describe_output field is not used in all the relevant fields (only storage_aws_external_id is supported). This will be addressed during the resource rework.
デプロイ
実装が完了したら、GitHub Actions 経由でデプロイします。
※ 今回は試しませんが、ローカルからterraformコマンドでデプロイする場合は、AWS_PROFILEを設定してTerraform変数(TF_VAR_snowflake_organization_name、TF_VAR_snowflake_account_name)を環境変数として設定すれば実行可能だと思います。
featureブランチで実装してpushします。
git add .
git commit -m "feat(dev): Add Workload Identity Federation authentication"
git push
GitHub 上で feature ブランチから develop ブランチへ Pull Request を作成します。

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

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

Snowflakeの画面からも今回デプロイしたRAW_DATA_DEVというDatabaseがあることを確認できました。

login_historyテーブルで、Terraformユーザーが実際にWIF認証でログインしているかを確認します。
USE ROLE ACCOUNTADMIN;
-- ログイン履歴でWIF認証を確認
SELECT
event_timestamp,
user_name,
first_authentication_factor, -- "WORKLOAD_IDENTITY"であることを確認
is_success
FROM snowflake.account_usage.login_history
WHERE user_name = 'TERRAFORM_BLOG_USER'
AND event_timestamp > DATEADD(hour, -24, CURRENT_TIMESTAMP())
ORDER BY event_timestamp DESC
LIMIT 5;
first_authentication_factorがWORKLOAD_IDENTITYとなっているので、想定通り認証できています。

query_historyテーブルで、Terraform Provider 2.11.0が実行したクエリを確認できます。
USE ROLE ACCOUNTADMIN;
-- Terraform実行履歴を確認
SELECT
start_time,
user_name,
role_name,
query_text,
execution_status
FROM snowflake.account_usage.query_history
WHERE user_name = 'TERRAFORM_BLOG_USER'
AND start_time > DATEADD(hour, -24, CURRENT_TIMESTAMP())
ORDER BY start_time DESC
LIMIT 10;
Terraformが実行したDESCRIBE、CREATE、GRANT等のクエリを確認できます。

最後に
これからSnowflake × Terraformで構築する方や、既存のkey pairからWIFへの移行を検討している方の参考になれば幸いです。







