SnowflakeのリソースをTerraform×GitHub Actionsで管理するための手順をまとめてみた

2024.04.09

さがらです。

2024年1月にSnowflakeのTerraform Providerに関する2024年のロードマップが公開されています。

このロードマップについてわかりやすくまとめて頂いているのが下記の記事です。内容としては、GRANTの再設計、GAしている全機能のサポート、既存Issueの解決、などに取り組んでいくとのことで、破壊的な変更を含む一方で良い方向に進んでいることが感じ取れます。

そこでこのロードマップの内容を受け、今後SnowflakeとTerraformを組み合わせたリソース管理がより普及していくことを見越し、SnowflakeのリソースをTerraform×GitHub Actionsで管理するための手順を本記事でまとめてみます。

前提条件

今回TerraformをGitHub Actionsで実行するにあたり、前提条件は下記の内容となります。

  • TerraformはOSS版を使用
  • OSは、ローカル・GitHub ActionsともにUbuntu
  • ローカル及びGitHub ActionsからSnowflakeへの認証にはキーペア認証を使用する
  • ローカルからAWSへの認証にはaws-cliを使用する
  • GitHub ActionsからAWSへの認証にはOpenID Connectを使用する(Access Keyを払い出したくないため)
  • Remote StateはAWSのS3で管理する。state lockのためにDynamoDBを使用する
  • Remote StateやGitHub Actionsの認証に用いるAWSのリソースは、すべてCloudFormationで管理する
    • 公式Docsに「Terraform is an administrative tool that manages your infrastructure, and so ideally the infrastructure that is used by Terraform should exist outside of the infrastructure that Terraform manages.」とあるので、Terraformのためのリソースは別管理が望ましいと思い、CloudFormationを使用しています。

また、検証時の環境は以下となっております。

  • Ubuntu 20.04 LTS(WSL2)
  • Terraform:1.7.5
  • Python:3.11.8
  • pre-commit:3.5.0
  • aws-cli:2.15.36
  • aws-vault:7.2.0
  • git:2.25.1

各種インストール

非常に簡単にですが、必要な各ツールのインストール手順を記しておきます。

Pythonのインストール

UbuntuにおけるPythonインストールに関しては、こちらが参考になると思います。

今回Pythonはpre-commitで使用しているだけなのではありますが、リポジトリごとにPythonのバージョンやパッケージを切り分けて管理したい場合にはpyenv+poetryを使う方法もあります。

pre-commitのインストール

pre-commitを用いてTerraform fmtterraform validateコマンドをcommit前に実行することで、コードの体裁を整えて統一することができます。

細かな設定は後述するため、ここではpre-commitをインストールするコマンドだけ載せておきます。

pip install pre-commit

pre-commitとTerraformの組み合わせについては、下記の記事も参考になります。

gitのインストールとGitHubとのSSH接続

UbuntuにおけるGitインストールは、基本的には下記コマンドを実行すればOKです。

sudo apt-get update # パッケージリストの更新
sudo apt-get install git

他の環境でのインストールはこちらも参考にしてください。

また、GitHubとSSH接続しておくと都度パスワードを入力しなくて済むので便利です。インターネット上で検索すると多くの記事がヒットすると思いますが、以下のような記事を参考に設定しましょう。

Terraformのインストール

Terraformのインストールについては下記の記事を参考にしてください。

Ubuntuの場合は下記になります。

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

また、今回は使用していませんがTerraformもバージョンを切り替えて開発したい場合にはtfenvもあります。

aws-cliとaws-vault

Remote StateにS3を使う関係上、ローカルからも対象のS3やDynamoDBにアクセスできるようにしておく必要があります。そのためにaws-cliを入れます。(また、TerraformでAWSのリソースを管理する場合にも、aws-cliを入れておくと便利です。)

Ubuntuでのaws-cliのインストールは下記のコマンドで可能です。

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# インストールされたか確認
aws --version

加えて、個人的にはaws-vaultもインストールすることを推奨しています。aws-vaultを入れることで、AWSのAccess Keyの平文保存を避けたり、MFAを設定している場合のコード入力もセッション開始時に1度だけ入力すればよくなります。

aws-vaultについては下記の記事を参考にしてください。(実は私はaws-vaultをWSL2のUbuntuでインストール後セットアップする時にうまくいかなかったのですが、こちらのコメントの手順を参考にして進め、こちらのコメントの環境変数も設定することで対応できました。)

GitHubリポジトリの作成

最終的にGitHub Actions経由でTerraformを実行するため、GitHubでコードを管理するリポジトリを作成しておきます。

リポジトリの設定については、下記のリンク先が参考になります。

ローカルでの開発環境の準備

続いて、ローカルのUbuntuでの開発用ディレクトリを作成し、先ほど作成したGitHubリポジトリと紐づけておきます。ディレクトリ名はterraform-snowflake-practiceとしていますが、必要に応じて変更してください。

mkdir terraform-snowflake-practice && cd terraform-snowflake-practice
echo "# terraform-snowflake-practice" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:<GitHubアカウント名>/<GitHub上のリモートリポジトリ名>.git
git push -u origin main

また、.gitignoreもこのタイミングで追加しておくとよいと思います。Terraform関係でいうと、stateファイルなどを.gitignoreに追加しておきましょう。

echo "*.terraform*" >> .gitignore
echo "*.tfstate" >> .gitignore
echo "*.tfstate.*" >> .gitignore
echo "*.env" >> .gitignore
git add .gitignore
git commit -m "add gitignore"
git push

Snowflakeの準備

次に、Snowflakeの設定を行います。

まず、Snowflakeとの認証にはキーペア認証を使用するため、ローカルでRSA Keyを作成しておきます。

cd ~/.ssh
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out snowflake_tf_snow_key.p8 -nocrypt
openssl rsa -in snowflake_tf_snow_key.p8 -pubout -out snowflake_tf_snow_key.pub

cat ~/.ssh/snowflake_tf_snow_key.pubを実行し、公開鍵のキー部分だけをコピーしておきます。(後でSnowflakeでTerraform用のユーザーを作成するときに使用します。)

続いてSnowflakeでワークシートを立ち上げ、下記のクエリを実行します。やっていることは、Terraform用のユーザー、ロール、ウェアハウスを定義しているだけです。

RSA_PUBLIC_KEY_HEREのところに、先程ローカルで作成しコピーした公開鍵の内容を貼り付けるようにしてください。

use role securityadmin;

create user terraform_user
    RSA_PUBLIC_KEY='RSA_PUBLIC_KEY_HERE' 
    DEFAULT_ROLE=PUBLIC 
    MUST_CHANGE_PASSWORD=FALSE;
   
create role terraform;

grant role terraform to user terraform_user;
grant role securityadmin to role terraform;
grant role sysadmin to role terraform;
grant role terraform to role accountadmin;

use role sysadmin;

create or replace warehouse terraform_wh 
    warehouse_size=XSMALL
    auto_resume=TRUE
    auto_suspend=60
    initially_suspended=TRUE
    statement_timeout_in_seconds=300  -- 60min
    comment='For terraform.'
;

-- SECURITYADMINにもウェアハウスの操作権限を付与しないと、TerarformでSECURITYADMIN経由でクエリ実行できずエラーになるので付与
grant usage on warehouse terraform_wh to role securityadmin;

-- MANAGE GRANTS権限をSYSADMINに付与しないと、future grantができないため付与
use role securityadmin;
grant manage grants on account to role sysadmin;

最後に、ローカルでTerraformを実行するために環境変数を定義しておきます。exportコマンドは一度実行しただけではそのセッション内で有効となるため、永続化したい場合は.profile.bashrcへ追記してください。

export SNOWFLAKE_ACCOUNT="<組織名>-<アカウント名>" 
export SNOWFLAKE_USER="TERRAFORM_USER"
export SNOWFLAKE_AUTHENTICATOR=JWT
export SNOWFLAKE_PRIVATE_KEY=`cat ~/.ssh/snowflake_tf_snow_key.p8`
export SNOWFLAKE_ROLE="TERRAFORM"
export SNOWFLAKE_WAREHOUSE="TERRAFORM_WH"

pre-commitのセットアップ

ここで、pre-commitのセットアップを行います。

Terraform用に作成したディレクトリのルートで、.pre-commit-config.yamlを新規作成して以下の内容を記述します。

default_stages: [commit]
repos:
    - repo: https://github.com/antonbabenko/pre-commit-terraform
      rev: v1.88.4
      hooks:
          - id: terraform_fmt
          - id: terraform_validate

続いて、以下のコマンドを実行してスクリプトをインストールします。

pre-commit install

ここまで設定すれば、下図のように本ディレクトリでcommitを行ったときにterraform fmtterraform validateコマンドが実行されます。

作成した.pre-commit-config.yamlもGitHubにプッシュしておきます。

git add .pre-commit-config.yaml
git commit -m "add precommit"
git push

Remote StateとGitHub ActionsからのOpenID Connect認証に使うリソース準備

次に、Remote StateとGitHub ActionsからのOpenID Connect認証に使うリソースをCloudFormationでセットアップします。

定義するリソースは、下記の4つとなります。

  • stateファイルを管理するS3
  • state lockを実現するためのDynamoDB
  • GitHub Actionsから認証させるためのOpenID Connectのプロバイダ
  • OpenID Connectプロバイダが認証後に使用するIAMロール

実際のコードはこちらになります。GitHubのリポジトリ名や、作られるリソース名を修正したうえでご利用ください。

## 実行する際に変更するところ
# S3のバケット名:sagara-terraform-state-bucket-name
# DynamoDBのテーブル名:sagara-terraform-state-lock-table
# GitHubリポジトリ名:"repo:<GitHubアカウント名>/<GitHubリポジトリ名>:*"
# Policy名:SagaraGitHubActionsPolicy

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Setup for GitHub Actions with OIDC, including S3, DynamoDB, and IAM Role'

Resources:
  # S3 Bucket for Terraform state
  TerraformStateBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: sagara-terraform-state-bucket

  # DynamoDB Table for Terraform state lock
  TerraformStateLockTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: sagara-terraform-state-lock-table
      AttributeDefinitions:
        - AttributeName: LockID
          AttributeType: S
      KeySchema:
        - AttributeName: LockID
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  # OIDC Provider for GitHub Actions
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: 'https://token.actions.githubusercontent.com'
      ClientIdList:
        - 'sts.amazonaws.com'
      ThumbprintList:
        - '6938fd4d98bab03faadb97b34396831e3780aea1' # https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/より
        - '1c58a3a8518e8759bf075b76b750d4f2df264fcd'

  # IAM Role for GitHub Actions
  GitHubActionsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: !GetAtt GitHubOIDCProvider.Arn
            Action: 'sts:AssumeRoleWithWebIdentity'
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: "sts.amazonaws.com"
              StringLike: # 使用するリポジトリからのみ、このIAMロールの使用を許可させる
                token.actions.githubusercontent.com:sub: "repo:<GitHubアカウント名>/<GitHubリポジトリ名>:*"
      Policies:
        - PolicyName: SagaraTerraformGitHubActionsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - 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}/*'
              - Effect: Allow
                Action: 
                  - 'dynamodb:GetItem'
                  - 'dynamodb:PutItem'
                  - 'dynamodb:DeleteItem'
                  - 'dynamodb:Query'
                  - 'dynamodb:Scan'
                  - 'dynamodb:UpdateItem'
                Resource: !GetAtt TerraformStateLockTable.Arn

Outputs:
  TerraformStateBucketName:
    Description: "Terraform state bucket name"
    Value: !Ref TerraformStateBucket

  TerraformStateLockTableName:
    Description: "Terraform state lock DynamoDB table name"
    Value: !Ref TerraformStateLockTable

  GitHubActionsRoleArn:
    Description: "IAM Role ARN for GitHub Actions"
    Value: !GetAtt GitHubActionsRole.Arn

  GitHubOIDCProviderArn:
    Description: "OIDC Provider ARN"
    Value: !GetAtt GitHubOIDCProvider.Arn

CloudFormationの実行はAWSコンソールでもaws-cliからでもどちらでもOKです。

aws-vaultを使用している場合は、以下のようなコマンドを実行すればOKです。

aws-vault exec <使用するprofile名> -- aws cloudformation create-stack --stack-name <作られるスタック名> --template-body file:<上述のコードをyaml定義した上でのファイルパス>

無事にリソースが作られると、下図のように4つのリソースが出来ているはずです。

ローカルでTerraformを記述し動かしてみる

Remote State用のリソース準備が出来たら、ローカルでのTerraformの実行準備が出来たので、簡単なリソースを定義します。

main.tfを作成し、以下のコードを入れます。非常に簡単な例ですが、backendでRemote Stateを管理するS3を指定し、各resourceでSnowflakeのデータベースやウェアハウスを定義しています。(必要に応じて名称は変更してください。)

terraform {
  required_providers {
    snowflake = {
      source  = "Snowflake-Labs/snowflake"
      version = "~> 0.87"
    }
  }

  backend "s3" {
    bucket         = "sagara-terraform-state-bucket"
    key            = "snowflake-state/snowflake.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "sagara-terraform-state-lock-table"
    encrypt        = true
  }
}

provider "snowflake" {
  alias = "sys_admin"
  role  = "SYSADMIN"
}

resource "snowflake_database" "db" {
  provider = snowflake.sys_admin
  name     = "TF_DEMO"
}

resource "snowflake_warehouse" "warehouse" {
  provider       = snowflake.sys_admin
  name           = "TF_DEMO"
  warehouse_size = "xsmall"
  auto_suspend   = 60
}

この上で、terraform initを実行し必要なプラグインのインストールやRemote Stateの初期化を行います。

aws-vault exec <使用するprofile名> -- terraform init

次に、terraform planを実行し、どのリソースが作られるかを確認します。

aws-vault exec <使用するprofile名> -- terraform plan

terraform planの結果が問題なさそうならば、terraform applyを実行してSnowflake上にリソースを作成します。

aws-vault exec <使用するprofile名> -- terraform apply

実際には後述するGitHub Actions上でterraform planterraform applyを動かすことをベースとしますが、取り急ぎローカルで動かす場合にはこの手順でOKです。

動作確認まで終えたら、リモートリポジトリにプッシュしておきましょう。

git add main.tf
git commit -m "add main.tf"
git push

GitHub Actions用のワークフローの定義

GitHub Actions用のワークフローの定義を行います。

まず、コード上にSnowflakeのアカウント名や秘密鍵を載せることは避けたいため、GitHubのリポジトリ上でSecretを定義しておきます。

対象リポジトリのSettings→Secrets and variables→Actionsに、以下の3つを定義します。

  • SNOWFLAKE_ACCOUNT組織名_アカウント名の形式でSnowflakeのアカウントを定義
  • SNOWFLAKE_PRIVATE_KEY.ssh/snowflake_tf_snow_key.p8の内容をコピーして定義
    • -----BEGIN PRIVATE KEY-----から-----END PRIVATE KEY-----まで含める必要があるため注意
  • AWS_ROLE_ARN:OpenID Connectの認証後に使用するIAMロールのARNを定義
    • AWSコンソールで、CloudFormationのリソースから対象のIAMロールへリンクし、ARNを確認すればOKです。

Secretの定義後はこのような画面となっているはずです。

次に、.github/workflows/snowflake-terraform-cicd.ymlというファイルを作成し、以下のコードを記述してワークフローの内容を定義します。

ポイントとしては、プルリクエスト時にはterraform planが動き、mainブランチにプッシュされたときにはterraform applyが動くように条件分岐しています。

name: "Snowflake Terraform CI/CD"

on:
  push:
    branches:
      - main
  pull_request:

env:
  # Terraform
  TF_VERSION: "1.7.5"

  # Snowflake
  SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
  SNOWFLAKE_USER: "TERRAFORM_USER"
  SNOWFLAKE_AUTHENTICATOR: "JWT"
  SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}
  SNOWFLAKE_ROLE: "TERRAFORM"
  SNOWFLAKE_WAREHOUSE: "TERRAFORM_WH"

jobs:
  plan-common:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    permissions:
      id-token: write # OIDCを利用する際に必須
      contents: read # actions/checkout のために必要

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Set up AWS credentials
        uses: aws-actions/configure-aws-credentials@v4.0.2
        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

      - name: Terraform Init
        run: terraform init -upgrade -no-color

      - name: Terraform validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: terraform plan -no-color

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

ファイルの作成を終えたら、リモートリポジトリにプッシュしておきましょう。

git add .github/workflows/snowflake-terraform-cicd.yml
git commit -m "add gha workflow"
git push

実際にGitHub Actions上でTerraformを動かしてみる

ということで、実際の開発の手順に沿って、GitHub Actions上でTerraformを動かすところまでやってみます。

ローカルでの開発

ブランチを切って、Snowflakeのウェアハウスの仕様を変更してみます。

まずブランチを切ります。

git checkout -b feature-revise-wh

次に、main.tfのウェアハウスに関わるリソースを以下の内容に変更します。具体的にはauto_suspend = 60からauto_suspend = 120にしています。

resource "snowflake_warehouse" "warehouse" {
  provider = snowflake.sys_admin
  name           = "TF_DEMO"
  warehouse_size = "xsmall"
  auto_suspend   = 120
}

この状態で、terraform planを実行してみます。下図のように変更が検知されていればOKです。

aws-vault exec [使用するprofile名] -- terraform plan

問題なくtrerraform planが出来ているので、コミットしてリモートリポジトリにプッシュします。

git add main.tf
git commit -m "revise warehouse"
git push -u origin feature-revise-wh

プルリクエストの作成

対象のリポジトリを開くと、下図のようになっているはずなのでCompare & pull requestを押し、画面に沿ってプルリクエストを作成します。

プルリクエストを作成すると、下図のようにGitHub Actionsの処理が走ります。

詳細を見てみると、事前にyamlファイルで定義した内容に沿って処理が行われていることがわかります。

問題ないことを確認したので、Merge pull requestを押します。すると、マージを行ったときに行われるGitHub Actionsが動き、terraform applyを実行しているのがわかります。

実際にSnowflake上でウェアハウスの内容を見ると、ちゃんと変更した内容に変わっていました!これで新しいリソースの追加が滞りなく行えました。

おまけ:state lockがちゃんと起きているかを確認

今回、複数人でチーム開発した場合にstateの競合が起きないようにDynamoDBを用いてstate lockを実現しています。

実際に、ローカル環境でterraform planを動かしているときにGitHub Actions上でterraform planが動くと、下図のようにstate lockが検知されエラーとなりました。

最後に

SnowflakeのリソースをTerraform×GitHub Actionsで管理するための手順をまとめてみました。 開発時の好みによって少し別のツールを入れたりはあるかもしれませんが、参考になると幸いです。

参考

本記事の執筆にあたり、SnowflakeとTerraformに関しては以下の記事を参考にさせて頂きました。

GitHub ActionsからAWSへOpenID Connectで認証を行う手順とロジックについては、以下の記事を参考にさせて頂きました。