Snowflake × Terraform × GitHub Actions で Workload Identity Federation を構築してみた

Snowflake × Terraform × GitHub Actions で Workload Identity Federation を構築してみた

2025.12.10

はじめに

データ事業本部の 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経由で行います。

https://dev.classmethod.jp/articles/comparing-snowflake-terraform-directory-patterns/

また、前回のブログでSnowflake と AWS S3 を連携させる Storage Integration を Terraform で構築しました。今回はその構成にWorkload Identity Federation認証を適用していきます。

https://dev.classmethod.jp/articles/snowflake-terraform-s3-storage-integration/

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に渡すことで認証していました。

Key Pair認証の実装例
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"
}

この方式では、秘密鍵の管理には定期的なローテーションや安全な保存が必要であり、運用負荷がかかります。

https://docs.snowflake.com/en/user-guide/key-pair-auth

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の実装例を紹介します。

https://docs.snowflake.com/user-guide/workload-identity-federation

https://registry.terraform.io/providers/snowflakedb/snowflake/2.10.0/docs/guides/authentication_methods#workload-identity-federation-wif-authenticator-flow

WIF認証の仕組み

WIF認証のフローは以下のようになります。

  1. GitHub ActionsがOIDCトークンを要求し、GitHub OIDC Providerが発行
  2. GitHub ActionsがOIDCトークンを使ってAWS IAM RoleをAssumeRole
  3. AWS STSが一時認証情報を発行し、環境変数に設定
  4. Terraform Snowflake Providerが環境変数から認証情報を自動検出
  5. Terraform ProviderがAWS IAM Roleの身元証明をSnowflakeに送信
  6. Snowflakeが身元証明を検証し、事前に許可されたIAM Roleであることを確認して認証

詳細な仕組みはSnowflake公式ドキュメントと以下の記事を参考に、私の理解に基づいて記載しているので、誤りがある可能性もあります。Snowflake公式ドキュメントに明確な記載は見つけられませんでした。。
https://tech.layerx.co.jp/entry/snowflake-wif-for-aws-bash

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実装を見ていきます。

environments/dev/versions.tf
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_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN)を自動的に検出して使用します。

environments/dev/variable.tf
variable "snowflake_organization_name" {
  type        = string
  description = "Snowflake organization name"
}

variable "snowflake_account_name" {
  type        = string
  description = "Snowflake account name"
}

JWT認証で必要だったsnowflake_private_key変数を削除します。
シンプルに組織名とアカウント名のみになりました。

.github/workflows/dev-snowflake-terraform-cicd.yml
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経由でアクセスする必要があります。

modules/data_engineer/main.tf
// 誤った実装例
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
        }
      }
    }]
  })
}
modules/data_engineer/main.tf
// 正しい実装例
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.

https://registry.terraform.io/providers/snowflakedb/snowflake/2.5.0/docs/resources/storage_integration

デプロイ

実装が完了したら、GitHub Actions 経由でデプロイします。

※ 今回は試しませんが、ローカルからterraformコマンドでデプロイする場合は、AWS_PROFILEを設定してTerraform変数(TF_VAR_snowflake_organization_nameTF_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 を作成します。
Screenshot 2025-12-10 at 20.56.44

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

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

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

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_factorWORKLOAD_IDENTITYとなっているので、想定通り認証できています。
Screenshot 2025-12-10 at 21.08.53

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等のクエリを確認できます。
Screenshot 2025-12-10 at 21.13.36

最後に

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

この記事をシェアする

FacebookHatena blogX

関連記事