Building a Multi-Account CI/CD Environment for Snowflake × Terraform Using GitHub Actions

Building a Multi-Account CI/CD Environment for Snowflake × Terraform Using GitHub Actions

2025.10.11

This page has been translated by machine translation. View original

Introduction

I'm kasama from the Data Business Division.
In this article, I'll share how I built a multi-account CI/CD pipeline using Terraform and GitHub Actions for Snowflake environments with separate development and production accounts.

Premise

This article is based on the open-source version of Terraform, not considering HCP Terraform (cloud version).
I'll reference the configurations from the following blogs, with some customizations. While the OIDC setup and general workflow are similar, the multi-account configuration differs slightly, which I'll focus on.
https://dev.classmethod.jp/articles/snowflake-terraform-design-with-functional-and-access-role/
https://dev.classmethod.jp/articles/snowflake-how-to-terraform-with-github-actions/

I'll base my approach on the following structure. The implementation creates modules by access role + resource type, called from dev/prd main.tf files. Terraform init, plan, and apply commands are executed through GitHub Actions.

snowflake-terraform-sample % tree
.
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── dev-snowflake-terraform-cicd.yml
│       └── prd-snowflake-terraform-cicd.yml
├── cfn
│   └── create_state_gha_resources.yml
├── environments
│   ├── dev
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   └── prd
│       ├── backend.tf
│       ├── main.tf
│       ├── outputs.tf
│       ├── variables.tf
│       └── versions.tf
├── modules
│   ├── access_role_and_database
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_file_format
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_schema
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_stage
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_storage_integration
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── access_role_and_warehouse
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── aws_storage_integration
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── functional_role
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
└── README.md

GitHub Actions Workflow

This project adopts a Git Flow strategy, centered around a main branch (production) and a develop branch (development). Feature branches are used for development, with integration and testing on the develop branch before merging to main for production releases. Each branch corresponds to a specific environment, with automatic deployments triggered by merges to branches.

https://qiita.com/homhom44/items/9f13c646fa2619ae63d0

  • Development Environment (feature → develop): Automatic deployment to the development environment when PR merges from feature to develop branch
  • Production Environment (develop → main): After validation in development, automatic deployment to production when PR merges from develop to main branch

For each environment, terraform plan is executed when creating a PR for preview, and terraform apply is executed when merging the PR for actual deployment.

Development Environment Deployment Flow (feature → develop)

  1. When PR is created (terraform plan)

    • Developer creates PR from feature branch to develop branch
    • GitHub Actions workflow is automatically triggered
    • Authenticate to AWS via OIDC and get state from S3 for terraform init
    • Run code quality checks (fmt/validate/TFLint/Trivy)
    • Execute terraform plan on Snowflake environment with Key-pair authentication
    • Comment plan results on PR
  2. When PR is merged (terraform apply)

    • PR is approved and merged to develop branch
    • Workflow is triggered again by push event
    • Authenticate to AWS and run terraform init
    • Rerun code quality checks
    • Automatically deploy to development Snowflake with terraform apply -auto-approve

Production Environment Deployment Flow (develop → main)

  1. When PR is created (terraform plan)

    • After verification in development, create PR from develop branch to main branch
    • GitHub Actions workflow is automatically triggered
    • Authenticate to AWS via OIDC and get state from S3 for terraform init
    • Run code quality checks (fmt/validate/TFLint/Trivy)
    • Execute terraform plan on production Snowflake environment with Key-pair authentication
    • Comment plan results on PR
  2. When PR is merged (terraform apply)

    • After review and approval, merge to main branch
    • Workflow is triggered again by push event
    • Authenticate to AWS and run terraform init
    • Rerun code quality checks
    • Automatically deploy to production Snowflake with terraform apply -auto-approve

The workflow for the production environment is similar to development, but branch protection settings can be used to create differences.
Typically, PR merges to the main branch (production deployment) require at least one approval. For the develop branch (development environment), settings can be more flexible based on project policy. For example, if local deployments are prohibited due to Terraform State Lock considerations, making approvals for develop branch merges optional can accelerate CI/CD validation cycles. Alternatively, a staged approval process can be implemented, requiring one approver for develop and two (admin + approver) for main to emphasize code quality.

GitHub's Rulesets can be used for branch protection settings like merge approvals. The following article was helpful:
https://zenn.dev/kuritify/articles/github-rulesets

I tried it, but while I could create rulesets, they couldn't be applied to Private repositories on the Free Plan.
Screenshot 2025-10-11 at 10.20.58

Implementation

.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_ prefix maps to Terraform variables
      TF_VAR_snowflake_organization_name: ${{ secrets.SNOWFLAKE_ORG_NAME }}
      TF_VAR_snowflake_account_name: ${{ secrets.SNOWFLAKE_ACCOUNT_NAME }}
      TF_VAR_snowflake_private_key: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}

    permissions:
      id-token: write # Required for OIDC
      contents: read # Required for actions/checkout
      pull-requests: write # For PR comments

    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@ae78205cfffec9e8d93fd2b3115c7e9d3166d4b6 # v5.0.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  # v8.0.0
        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

This is the GitHub Actions workflow configuration for the development environment.

  • on: Limits triggers to the develop branch, with paths filters to run only when relevant files are changed
  • env: References values from pre-configured GitHub Secrets with TF_VAR_ prefix, automatically mapping to Terraform variables
  • permissions: Following the principle of least privilege, specifies only id-token: write (OIDC authentication), contents: read (checkout), and pull-requests: write (PR comments)
  • Third-party Actions: Fixed with commit SHA like actions/checkout@08c6903... to prevent tag tampering attacks
  • AWS authentication: Gets temporary credentials via OIDC, reducing risks of leaked long-term access keys
  • Code quality checks: Runs terraform fmt/validate, TFLint, and Trivy sequentially, stopping the workflow if issues are found
  • terraform plan: Runs only when creating a PR with the condition github.event_name == 'pull_request'
  • Posting plan results to PR: Posts a comment with actions/github-script only when plan succeeds with the condition steps.plan.outcome == 'success'
  • terraform apply: Runs only when merging to develop branch with the condition github.ref == 'refs/heads/develop' && github.event_name == 'push'

There are several security-focused points. In permissions, rather than granting default write permissions, only id-token: write, contents: read, and pull-requests: write are explicitly specified, minimizing the attack surface by granting only the necessary minimum permissions. Third-party Actions are fixed with commit SHA (actions/checkout@08c6..) rather than version tags (v5.0.0). Since tags can be rewritten later, there's a risk they could be replaced with malicious code, but commit SHAs are immutable, preventing this risk. For secret management, sensitive information like Snowflake private keys is managed in GitHub Secrets to prevent output in workflow logs. GitHub Actions automatically masks Secret values, reducing the risk of exposing sensitive information in logs.

These practices are based on the following references:
https://docs.github.com/en/actions/reference/security/secure-use
https://zenn.dev/azu/articles/ad168118524135
https://zenn.dev/farstep/books/learn-github-actions/viewer/security-and-operations
https://zenn.dev/cybozu_ept/articles/573c706ec08b48

.github/workflows/prd-snowflake-terraform-cicd.yml
name: "Snowflake Terraform CI/CD - PRD"

on:
  push:
    branches:
      - main
    paths:
      - "environments/prd/**"
      - "modules/**"
      - ".github/workflows/prd-snowflake-terraform-cicd.yml"
  pull_request:
    branches:
      - main
    paths:
      - "environments/prd/**"
      - "modules/**"
      - ".github/workflows/prd-snowflake-terraform-cicd.yml"

jobs:
  plan-prd:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: prd

    env:
      # Terraform
      TF_VERSION: "1.11.0"
      # Snowflake - TF_VAR_ prefix maps to Terraform variables
      TF_VAR_snowflake_organization_name: ${{ secrets.SNOWFLAKE_ORG_NAME }}
      TF_VAR_snowflake_account_name: ${{ secrets.SNOWFLAKE_ACCOUNT_NAME }}
      TF_VAR_snowflake_private_key: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}

    permissions:
      id-token: write # Required for OIDC
      contents: read # Required for actions/checkout
      pull-requests: write # For PR comments

    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/prd

      - name: Terraform Init
        run: terraform init -upgrade -no-color
        working-directory: environments/prd

      - name: Terraform validate
        run: terraform validate -no-color
        working-directory: environments/prd

      - name: Setup TFLint
        uses: terraform-linters/setup-tflint@ae78205cfffec9e8d93fd2b3115c7e9d3166d4b6 # v5.0.0
        with:
          tflint_version: v0.58.0

      - name: Run TFLint
        run: tflint --init && tflint -f compact --minimum-failure-severity=error
        working-directory: environments/prd

      - name: Run Trivy Security Scan
        uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
        with:
          scan-type: "config"
          scan-ref: "environments/prd"
          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/prd

      - name: Post Plan to PR
        if: github.event_name == 'pull_request' && steps.plan.outcome == 'success'
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  # v8.0.0
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('environments/prd/tfplan.txt', 'utf8');

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `### Terraform Plan - Production

            <details>
            <summary>Show Plan</summary>

            \`\`\`terraform
            ${plan.substring(0, 65000)}
            \`\`\`

            </details>`
            });

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve
        working-directory: environments/prd

This is the GitHub Actions workflow configuration for the production environment.
It has the same structure as the development environment, but with the trigger branch changed to main, the working directory set to environments/prd, and the apply condition set to github.ref == 'refs/heads/main'.

To automate updates for third-party Actions and Terraform providers, we've also included Dependabot configuration.

.github/dependabot.yml
version: 2
updates:
  # Update GitHub Actions dependencies
  - package-ecosystem: "github-actions"
    directory: "/"
    target-branch: "develop"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
      timezone: "Asia/Tokyo"
    labels:
      - "dependencies"
      - "github-actions"
    commit-message:
      prefix: "chore(deps)"
      include: "scope"
    open-pull-requests-limit: 10

  # Update Terraform dependencies
  - package-ecosystem: "terraform"
    directory: "/environments/dev"
    target-branch: "develop"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
      timezone: "Asia/Tokyo"
    labels:
      - "dependencies"
      - "terraform"
    commit-message:
      prefix: "chore(deps)"
      include: "scope"
    open-pull-requests-limit: 10

  - package-ecosystem: "terraform"
    directory: "/environments/prd"
    target-branch: "develop"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
      timezone: "Asia/Tokyo"
    labels:
      - "dependencies"
      - "terraform"
    commit-message:
      prefix: "chore(deps)"
      include: "scope"
    open-pull-requests-limit: 10

This configuration automatically checks for updates to GitHub Actions and Terraform (dev/prd environments) dependencies every Monday at 09:00 (Japan time), creating up to 10 PRs for new versions. Terraform provider updates (especially for the Snowflake provider) may include breaking changes due to preview features being made GA or specification changes, so it's important to check the CHANGELOG and thoroughly test in the development environment before merging PRs.

Preparation

Creating Snowflake Deployment User

As of October 2025, while Snowflake CLI supports OIDC, Snowflake Terraform does not yet support it, so we'll connect using key pair authentication with a Snowflake deployment user.
https://qiita.com/bgcanary/items/432f12744478e4165395

Run the following commands locally or in AWS CloudShell to create a key pair. Since GitHub Secrets cannot be referenced after setting, we're storing the values in AWS SSM Parameter.

# Generate RSA key pair
openssl genrsa -out snowflake_private_key.pem 2048
openssl rsa -in snowflake_private_key.pem -pubout -out snowflake_public_key.pem

# Format the public key (remove line breaks)
PUBLIC_KEY=$(cat snowflake_public_key.pem | grep -v "BEGIN PUBLIC KEY" | grep -v "END PUBLIC KEY" | tr -d '\n')
echo $PUBLIC_KEY

# Save private key to AWS SSM Parameter Store
aws ssm put-parameter \
    --name "/<your-unique-name>/dev/snowflake/terraform-user/private-key" \
    --value "$(cat snowflake_private_key.pem)" \
    --type "SecureString" \
    --overwrite

# Save formatted public key
aws ssm put-parameter \
    --name "/<your-unique-name>/dev/snowflake/terraform-user/public-key" \
    --value "$PUBLIC_KEY" \
    --type "String" \
    --overwrite

Run the following SQL in your Snowflake account to create a deployment user:

USE ROLE SECURITYADMIN;

CREATE USER TERRAFORM_BLOG_USER
    TYPE = SERVICE
    RSA_PUBLIC_KEY='<YOUR_RSA_PUBLIC_KEY>'  -- Paste your formatted public key
    DEFAULT_ROLE=PUBLIC;

CREATE ROLE TERRAFORM;

GRANT ROLE TERRAFORM TO USER TERRAFORM_BLOG_USER;
GRANT ROLE SECURITYADMIN TO ROLE TERRAFORM;
GRANT ROLE SYSADMIN TO ROLE TERRAFORM;
GRANT ROLE TERRAFORM TO ROLE ACCOUNTADMIN;

-- Grant CREATE INTEGRATION permission (run as ACCOUNTADMIN)
USE ROLE ACCOUNTADMIN;
GRANT CREATE INTEGRATION ON ACCOUNT TO ROLE TERRAFORM;

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  -- 5min
    COMMENT='For terraform.';

-- Grant warehouse usage to SECURITYADMIN (needed for Terraform execution)
GRANT USAGE ON WAREHOUSE TERRAFORM_BLOG_WH TO ROLE SECURITYADMIN;

-- Grant MANAGE GRANTS to SYSADMIN (needed for future grants)
USE ROLE SECURITYADMIN;
GRANT MANAGE GRANTS ON ACCOUNT TO ROLE SYSADMIN;

AWS GitHub Actions Resource Deployment

Below is an example of IAM Role and S3 resources used for Terraform deployment. Please feel free to change resource names according to your project. These were deployed using CloudFormation.

cfn/create_state_gha_resources.yml
AWSTemplateFormatVersion: 2010-09-09
Description: |-
  Snowflake-Terraform S3 & GitHub Actions Setup - Creates S3 bucket for Terraform state
  and IAM role for GitHub Actions with comprehensive configuration

Mappings:
  # Switch values based on AWS AccountId
  EnvMapping:
    "123456789012":
      EnvName: dev

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "GitHub Configuration"
        Parameters:
          - GitHubAccountName
          - GitHubRemoteRepoName
          - GitHubOIDCProviderArn

Parameters:
  GitHubAccountName:
    Type: String
    Default: your-github-account
    Description: GitHub account name of the repository
  GitHubRemoteRepoName:
    Type: String
    Default: "terraform-snowflake-sample"
  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
        - "your-project-${EnvName}-s3-snf-state"
        - EnvName: !FindInMap
            - EnvMapping
            - !Ref "AWS::AccountId"
            - EnvName
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      Tags:
        - Key: service-id
          Value: your-project
        - Key: module
          Value: terraform-state
        - Key: project
          Value: integrated-analysis-env

  # IAM Role for GitHub Actions
  GitHubActionsRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - "your-project-${EnvName}-iamrole-snf-gha"
        - EnvName: !FindInMap
            - EnvMapping
            - !Ref "AWS::AccountId"
            - EnvName
      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}/${GitHubRemoteRepoName}:*"
      Policies:
        - PolicyName: !Sub
            - "your-project-${EnvName}-iampolicy-snf-gha"
            - EnvName: !FindInMap
                - EnvMapping
                - !Ref "AWS::AccountId"
                - EnvName
          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}/*"
              # Permissions to create and configure S3 buckets used by 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
                Resource:
                  - !Sub
                    - "arn:aws:s3:::your-project-${EnvName}-s3-*"
                    - EnvName: !FindInMap
                        - EnvMapping
                        - !Ref "AWS::AccountId"
                        - EnvName
                  - "arn:aws:s3:::your-project-snowflake-*"
              # IAM permissions to create roles and attach inline policies for 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/your-project-${EnvName}-iamrole-*"
                    - EnvName: !FindInMap
                        - EnvMapping
                        - !Ref "AWS::AccountId"
                        - EnvName
                  - !Sub "arn:aws:iam::${AWS::AccountId}:role/your-project-snowflake-*"
              # SSM Parameter Store permissions
              - Sid: SSMParameterStorePolicy
                Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:GetParameters
                Resource:
                  - !Sub "arn:aws:ssm:ap-northeast-1:${AWS::AccountId}:parameter/your-project/snowflake/*"
Outputs:
  TerraformStateBucketName:
    Description: Terraform state bucket name
    Value: !Ref TerraformStateBucket
  GitHubActionsRoleArn:
    Description: IAM Role ARN for GitHub Actions
    Value: !GetAtt GitHubActionsRole.Arn

GitHub Secrets Configuration

Manually configure secrets for your GitHub repository.
Open Settings > Environments in your GitHub repository, and create dev and prd from New environment. Define secrets like PRIVATE_KEY in the created Environment to reference them in GitHub Actions.

Screenshot 2025-10-11 at 15.31.49
Screenshot 2025-10-11 at 15.32.41

Deployment

Now let's deploy Snowflake Terraform. The actual implementation of Snowflake Terraform is similar to the content in the blog below, so we'll skip those details. The structure consists of defining configuration values in main.tf and calling modules.
https://dev.classmethod.jp/articles/snowflake-terraform-design-with-functional-and-access-role/

Screenshot 2025-10-11 at 16.20.22

Push the implemented content from your local feature branch.

@ snowflake-terraform-sample % git add .
@ snowflake-terraform-sample % git commit -m "feat(dev): Enable all Snowflake infrastructure modules"
@ snowflake-terraform-sample % git push origin

Create a Pull Request from the feature branch to the develop branch on GitHub.
Screenshot 2025-10-11 at 16.26.50

When a Pull Request to the develop branch is created, GitHub Actions will run.
Checking the Actions log, we can confirm it completed successfully. The terraform apply is skipped as expected.
Screenshot 2025-10-11 at 16.28.29

Returning to the Pull Request, we can see the bot has output the results of the terraform plan command. If your project has Claude AI review integration, you might want to have it summarize the terraform plan results.
Screenshot 2025-10-11 at 16.29.58
Screenshot 2025-10-11 at 16.30.14

After determining there are no issues, I merged it. GitHub Actions runs again, this time skipping the terraform plan command and executing the terraform apply command.
Screenshot 2025-10-11 at 16.35.24

The process completed successfully, and we confirmed the resources exist in the Snowflake account.
Screenshot 2025-10-11 at 16.38.41

For applying to the Snowflake production environment, create a PR from develop to main branch and follow the same steps, so we'll omit those details.

Conclusion

There are many considerations when configuring GitHub Actions, so I hope this serves as a helpful reference.

Share this article

FacebookHatena blogX

Related articles