さがらです。
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 fmt
とterraform 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 fmt
とterraform 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 plan
やterraform 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で認証を行う手順とロジックについては、以下の記事を参考にさせて頂きました。