PulumiでSnowflakeのユーザーとデータベースを作成してみた

2022.06.20

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

Infrastructure as Code のツール「Pulumi」はSnowflakeにも対応しており、下記の通りSnowflake用のパッケージが提供されています。

先日、こちらのパッケージを利用して下記のエントリの通りSnowflakeの「仮想ウェアハウス」と「ロール」を作成してみました。

今回は、更にこちらに加えて「ユーザー」と「データベース」も作成してみたいと思います。

前提

Snowflakeのアカウントは既にセットアップ済みで利用できるものとします。

また、Pulumiについては下記のようにHomebrew経由でインストールされていることを前提としています。

% brew install pulumi
% pulumi version
v3.34.1

加えて、PulumiのプロジェクトとSnowflake接続用の設定は下記のエントリの通り設定済みであることを前提としています。

ユーザーを作成するコードを記述する

では、ドキュメントを参考にしながらコードを記述していきましょう。まずは「ユーザー」を作成するコードです。

今回作成する「ユーザー」は「ロール」に所属させたいので、併せてRoleGrantsAPIも利用します。

resources/user.ts

import * as snowflake from "@pulumi/snowflake";
import { UserArgs } from "@pulumi/snowflake";

export class User {
  name: string;
  role: string;
  resource: snowflake.User | undefined;

  constructor(name: string, role: string) {
    this.name = name;
    this.role = role;
  }

  public create(args: UserArgs) {
    const user = new snowflake.User(this.name, {
      name: this.name,
      ...args,
    });

    const grants = new snowflake.RoleGrants(this.name, {
      roleName: this.role,
      users: [user.name],
    });

    this.resource = user;
  }
}

指定された名前で「ユーザー」を作成し、また同時にユーザーが指定されたロールに所属するようにしています。

データベースを作成するコードを記述する

次に「データベース」を作成するコードです。

ここでは別途作成する「ユーザー」が所属する「ロール」に対して、「データベース」と「データベース内のPUBLICスキーマ」の「オーナー権限」を付与したいと思います。

そこで、今回は「データベース」の作成と同時に権限の付与もDatabaseGrantAPIとSchemaGrantAPIを利用して実施します。

resources/database.ts

import * as snowflake from "@pulumi/snowflake";
import { DatabaseArgs } from "@pulumi/snowflake";

export class Database {
  name: string;
  role: string;
  resource: snowflake.Database | undefined;

  constructor(name: string, role: string) {
    this.name = name;
    this.role = role;
  }

  public create(args: DatabaseArgs) {
    const database = new snowflake.Database(this.name, {
      name: this.name,
      ...args,
    });

    const databaseGrants = new snowflake.DatabaseGrant(this.name, {
      databaseName: database.name,
      privilege: "OWNERSHIP",
      roles: [this.role],
      withGrantOption: false,
    });

    const schemaGrants = new snowflake.SchemaGrant(this.name, {
      databaseName: database.name,
      privilege: "OWNERSHIP",
      roles: [this.role],
      schemaName: "PUBLIC",
      withGrantOption: false,
    });

    this.resource = database;
  }
}

また、詳細は割愛しますがWarehouseも同様に作成した「ロール」で利用できるようにコードを修正しています。

resources/warehouse.ts

import * as snowflake from "@pulumi/snowflake";
import { WarehouseArgs } from "@pulumi/snowflake";

export class Warehouse {
  name: string;
  role: string;
  resource: snowflake.Warehouse | undefined;

  constructor(name: string, role: string) {
    this.name = name;
    this.role = role;
  }

  public create(args: WarehouseArgs) {
    const warehouse = new snowflake.Warehouse(this.name, {
      name: this.name,
      autoSuspend: 60,
      ...args,
    });

    const grantUsage = new snowflake.WarehouseGrant(this.name + "Usage", {
      privilege: "USAGE",
      roles: [this.role],
      warehouseName: warehouse.name,
      withGrantOption: false,
    });

    const grantOperate = new snowflake.WarehouseGrant(this.name + "Operate", {
      privilege: "OPERATE",
      roles: [this.role],
      warehouseName: warehouse.name,
      withGrantOption: false,
    });

    this.resource = warehouse;
  }
}

あとはこれらをindex.tsから呼び出します。

index.ts

import { Database } from "./resources/database";
import { Role } from "./resources/role";
import { User } from "./resources/user";
import { Warehouse } from "./resources/warehouse";

// Role
const role = new Role("role-ootaka-pulumi");
role.create({});

// Warehouse
const warehouse = new Warehouse("vwh-ootaka-pulumi", role.name);
warehouse.create({ warehouseSize: "xsmall" });

// User
const user = new User("user-ootaka-pulumi", role.name);
user.create({
  password: "MustBeSecret!!!",
  loginName: user.name,
  displayName: user.name,
  firstName: "pulumi",
  lastName: "ootaka",
  defaultRole: role.name,
  defaultWarehouse: warehouse.name,
  defaultNamespace: "PUBLIC",
  mustChangePassword: true,
});

// Database
const database = new Database("db-ootaka-pulumi", role.name);
database.create({});

// Export Resource Info
export const roleInfo = role.resource;
export const warehouseInfo = warehouse.resource;
export const userInfo = user.resource;
export const databaseInfo = database.resource;

こちらは作成したクラスを単純に呼んでいるだけですが、1点注意点があります。

今回は検証のためuser.createpasswordに対して、直接初期パスワード値を記載していますが、これは本来コードに記載すべきではありません。

やり方は色々あると思いますが、例えばランダムなパスワードを生成してAWS Secrets Managerに保存し、その値を参照するなど、セキュリティに考慮した対応が必要になってくると思います。

デプロイしてみる

では、デプロイしてみます。

デプロイにはpulumi upコマンドを実行します。

% pulumi up     
Previewing update (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/pulumi-snowflake/dev/previews/2443e9d0-ccd0-44a6-9094-1a1a41d6700a

     Type                               Name                      Plan       
 +   pulumi:pulumi:Stack                pulumi-snowflake-dev      create     
 +   ├─ snowflake:index:Warehouse       vwh-ootaka-pulumi         create     
 +   ├─ snowflake:index:Role            role-ootaka-pulumi        create     
 +   ├─ snowflake:index:Database        db-ootaka-pulumi          create     
 +   ├─ snowflake:index:User            user-ootaka-pulumi        create     
 +   ├─ snowflake:index:WarehouseGrant  vwh-ootaka-pulumiUsage    create     
 +   ├─ snowflake:index:WarehouseGrant  vwh-ootaka-pulumiOperate  create     
 +   ├─ snowflake:index:DatabaseGrant   db-ootaka-pulumi          create     
 +   ├─ snowflake:index:SchemaGrant     db-ootaka-pulumi          create     
 +   └─ snowflake:index:RoleGrants      user-ootaka-pulumi        create     
 
Resources:
    + 10 to create

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

作成リソースのプレビュー後にyesを選択し、成功すると以下のように表示されます。

Do you want to perform this update? yes
Updating (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/pulumi-snowflake/dev/updates/17

     Type                               Name                      Status      
 +   pulumi:pulumi:Stack                pulumi-snowflake-dev      created     
 +   ├─ snowflake:index:Role            role-ootaka-pulumi        created     
 +   ├─ snowflake:index:Warehouse       vwh-ootaka-pulumi         created     
 +   ├─ snowflake:index:User            user-ootaka-pulumi        created     
 +   ├─ snowflake:index:Database        db-ootaka-pulumi          created     
 +   ├─ snowflake:index:RoleGrants      user-ootaka-pulumi        created     
 +   ├─ snowflake:index:SchemaGrant     db-ootaka-pulumi          created     
 +   ├─ snowflake:index:DatabaseGrant   db-ootaka-pulumi          created     
 +   ├─ snowflake:index:WarehouseGrant  vwh-ootaka-pulumiUsage    created     
 +   └─ snowflake:index:WarehouseGrant  vwh-ootaka-pulumiOperate  created     
 
Outputs:
    databaseInfo : {
        dataRetentionTimeInDays: 1
        id                     : "db-ootaka-pulumi"
        name                   : "db-ootaka-pulumi"
        urn                    : "urn:pulumi:dev::pulumi-snowflake::snowflake:index/database:Database::db-ootaka-pulumi"
    }
    roleInfo     : {
        id     : "role-ootaka-pulumi"
        name   : "role-ootaka-pulumi"
        urn    : "urn:pulumi:dev::pulumi-snowflake::snowflake:index/role:Role::role-ootaka-pulumi"
    }
    userInfo     : {
        defaultNamespace  : "PUBLIC"
        defaultRole       : "role-ootaka-pulumi"
        defaultWarehouse  : "vwh-ootaka-pulumi"
        disabled          : false
        displayName       : "user-ootaka-pulumi"
        firstName         : "pulumi"
        hasRsaPublicKey   : false
        id                : "user-ootaka-pulumi"
        lastName          : "ootaka"
        loginName         : "USER-OOTAKA-PULUMI"
        mustChangePassword: true
        name              : "user-ootaka-pulumi"
        password          : [secret]
        urn               : "urn:pulumi:dev::pulumi-snowflake::snowflake:index/user:User::user-ootaka-pulumi"
    }
    warehouseInfo: {
        autoResume                     : true
        autoSuspend                    : 60
        id                             : "vwh-ootaka-pulumi"
        maxClusterCount                : 1
        maxConcurrencyLevel            : 8
        minClusterCount                : 1
        name                           : "vwh-ootaka-pulumi"
        resourceMonitor                : "null"
        scalingPolicy                  : "STANDARD"
        statementQueuedTimeoutInSeconds: 0
        statementTimeoutInSeconds      : 172800
        urn                            : "urn:pulumi:dev::pulumi-snowflake::snowflake:index/warehouse:Warehouse::vwh-ootaka-pulumi"
        warehouseSize                  : "X-Small"
    }

Resources:
    + 10 created

Duration: 8s

実際にsnowsqlを利用してSQLでユーザーとデータベースの情報を確認してみます。

USE ROLE ACCOUNTADMIN;
+--------------------+-------------------------------+--------------------+--------------------+------------+-----------+-------+----------------+----------------+---------+----------+----------------------+----------------+-------------------+-------------------+--------------------+-------------------------+---------------+---------------+--------------------+--------------+--------------------+-----------------+-------------------+--------------+--------------------+
| name               | created_on                    | login_name         | display_name       | first_name | last_name | email | mins_to_unlock | days_to_expiry | comment | disabled | must_change_password | snowflake_lock | default_warehouse | default_namespace | default_role       | default_secondary_roles | ext_authn_duo | ext_authn_uid | mins_to_bypass_mfa | owner        | last_success_login | expires_at_time | locked_until_time | has_password | has_rsa_public_key |
|--------------------+-------------------------------+--------------------+--------------------+------------+-----------+-------+----------------+----------------+---------+----------+----------------------+----------------+-------------------+-------------------+--------------------+-------------------------+---------------+---------------+--------------------+--------------+--------------------+-----------------+-------------------+--------------+--------------------|
| user-ootaka-pulumi | 2022-06-20 00:44:13.116 -0700 | USER-OOTAKA-PULUMI | user-ootaka-pulumi | pulumi     | ootaka    |       |                |                |         | false    | true                 | false          | vwh-ootaka-pulumi | PUBLIC            | role-ootaka-pulumi |                         | false         |               |                    | ACCOUNTADMIN | NULL               | NULL            | NULL              | true         | false              |
+--------------------+-------------------------------+--------------------+--------------------+------------+-----------+-------+----------------+----------------+---------+----------+----------------------+----------------+-------------------+-------------------+--------------------+-------------------------+---------------+---------------+--------------------+--------------+--------------------+-----------------+-------------------+--------------+--------------------+
1 Row(s) produced. Time Elapsed: 0.496s



SHOW DATABASES LIKE 'db-ootaka-pulumi';
+-------------------------------+------------------+------------+------------+--------+--------------------+---------+---------+----------------+
| created_on                    | name             | is_default | is_current | origin | owner              | comment | options | retention_time |
|-------------------------------+------------------+------------+------------+--------+--------------------+---------+---------+----------------|
| 2022-06-20 00:44:13.255 -0700 | db-ootaka-pulumi | N          | N          |        | role-ootaka-pulumi |         |         | 1              |
+-------------------------------+------------------+------------+------------+--------+--------------------+---------+---------+----------------+
1 Row(s) produced. Time Elapsed: 0.164s

想定どおり作成できていますね!

最後に、pulumi destroyコマンドで後片付けをしておしまいです。

まとめ

以上、PulumiでSnowflakeのユーザーとデータベースを作成してみました。

今回作成したように、コードベースで処理を書いていくことで「開発で利用しているSnowflakeアカウントに新規メンバー用ユーザと、開発用データベースを追加したい」というようなケースで活用できるかなと思いました。

今回は細かいところまで実装していませんが、例えばよくある名字.名前@foobar.comのように会社用メールアドレスが付与されている場合「メールアドレスを元にユーザー、ユーザー専用データベースを作成して、作成したSnowflakeアカウント情報をメールで通知」なんてこともできるかなと思います。

それぞれの処理についてはSQLだけでも対応できますが、他のシステムとの連携(メールでアカウント作成通知)などをやろうとする場合には、こういったInfrastructure as Code のツールが選択肢に入ってくるのかなと思います。(Snowflakeの外部関数を使う、という手もありますね)

どなたかのお役に立てば幸いです。それでは!