SnowflakeのリソースをTerraform×GitHub Actionsで管理するための手順をまとめてみた
さがらです。
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のリポジトリ名や、作られるリソース名を修正したうえでご利用ください。※2024/5/5追記:Parameterを使うように変更しました。
AWSTemplateFormatVersion: '2010-09-09' Description: 'Setup for GitHub Actions with OIDC, including S3, DynamoDB, and IAM Role' Parameters: S3BucketName: Type: String Default: sagara-terraform-state-bucket Description: Bucket name for state file DynamoDBTableName: Type: String Default: sagara-terraform-state-lock-table Description: Table for state lock GitHubAccountName: Type: String Default: <GitHubのアカウント名> Description: GitHub account name of the repository GitHubRemoteRepoName: Type: String Default: <GitHubのリポジトリ名> Description: Repository name of terraform PolicyName: Type: String Default: SagaraGitHubActionsPolicy Description: Policy for GitHub Actions Resources: # S3 Bucket for Terraform state TerraformStateBucket: Type: AWS::S3::Bucket Properties: BucketName: !Ref S3BucketName # DynamoDB Table for Terraform state lock TerraformStateLockTable: Type: AWS::DynamoDB::Table Properties: TableName: !Ref DynamoDBTableName 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: token.actions.githubusercontent.com:sub: "repo:${GitHubAccountName}/${GitHubRemoteRepoName}:*" Policies: - PolicyName: !Ref PolicyName 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です。
下図はコンソールからの例ですが、各Parameterの名称を変更してご利用ください。(コード上のDefault値も変えたほうが良いと思います。)
無事にリソースが作られると、下図のように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で認証を行う手順とロジックについては、以下の記事を参考にさせて頂きました。