├── assets └── overview.png ├── modules ├── bulk_roles │ ├── outputs.tf │ ├── main.tf │ ├── variables.tf │ └── README.md ├── bulk_users │ ├── outputs.tf │ ├── README.md │ ├── main.tf │ └── variables.tf ├── bulk_role_grants │ ├── outputs.tf │ ├── main.tf │ ├── variables.tf │ └── README.md ├── bulk_warehouse_grants │ ├── outputs.tf │ ├── main.tf │ ├── variables.tf │ └── README.md ├── bulk_warehouses │ ├── outputs.tf │ ├── variables.tf │ ├── README.md │ └── main.tf └── application_database │ ├── user.tf │ ├── outputs.tf │ ├── locals.tf │ ├── README.md │ ├── main.tf │ ├── warehouse.tf │ ├── variables.tf │ └── privileges.tf ├── versions.tf ├── examples └── dbt-quickstart │ ├── provider.tf │ ├── outputs.tf │ ├── locals.tf │ ├── variables.tf │ ├── README.md │ └── main.tf ├── outputs.tf ├── variables.tf ├── .gitignore ├── LICENSE ├── main.tf └── README.md /assets/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immuta/terraform-snowflake-fast-data-warehouse/HEAD/assets/overview.png -------------------------------------------------------------------------------- /modules/bulk_roles/outputs.tf: -------------------------------------------------------------------------------- 1 | output "roles" { 2 | description = "Map of all roles created." 3 | value = snowflake_role.main 4 | } 5 | -------------------------------------------------------------------------------- /modules/bulk_users/outputs.tf: -------------------------------------------------------------------------------- 1 | output "users" { 2 | description = "Map of user resources created." 3 | value = snowflake_user.main 4 | } 5 | -------------------------------------------------------------------------------- /modules/bulk_role_grants/outputs.tf: -------------------------------------------------------------------------------- 1 | output "grants" { 2 | description = "Map of all grants created." 3 | value = snowflake_role_grants.main 4 | } 5 | -------------------------------------------------------------------------------- /modules/bulk_warehouse_grants/outputs.tf: -------------------------------------------------------------------------------- 1 | output "grants" { 2 | description = "Map of all grants created." 3 | value = snowflake_warehouse_grant.main 4 | } 5 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.23.2" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/dbt-quickstart/provider.tf: -------------------------------------------------------------------------------- 1 | provider "snowflake" { 2 | account = var.snowflake_account 3 | region = var.snowflake_region 4 | username = var.snowflake_username 5 | password = var.snowflake_user_password 6 | role = var.snowflake_user_role 7 | } 8 | -------------------------------------------------------------------------------- /modules/bulk_warehouses/outputs.tf: -------------------------------------------------------------------------------- 1 | output "warehouses" { 2 | description = "Map of all warehouses created." 3 | value = snowflake_warehouse.main 4 | } 5 | 6 | output "resource_monitors" { 7 | description = "Map of all resource monitors created." 8 | value = snowflake_resource_monitor.main 9 | } 10 | -------------------------------------------------------------------------------- /examples/dbt-quickstart/outputs.tf: -------------------------------------------------------------------------------- 1 | output "system_users" { 2 | description = "System users generated by the core module." 3 | sensitive = true 4 | value = module.systems.users 5 | } 6 | 7 | output "application_users" { 8 | description = "Manually selected applilcation users generated by each application_database module." 9 | sensitive = true 10 | value = { 11 | stitch = module.stitch_db.user 12 | fivetran = module.fivetran_db.user 13 | meltano = module.meltano_db.user 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/bulk_roles/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.18.1" 6 | } 7 | random = { 8 | version = ">=2.2.0" 9 | } 10 | } 11 | experiments = [module_variable_optional_attrs] 12 | } 13 | 14 | resource "snowflake_role" "main" { 15 | for_each = var.roles 16 | 17 | name = coalesce(each.value["name"], each.key) 18 | comment = try(coalesce(each.value["comment"], var.default_comment), null) 19 | } 20 | -------------------------------------------------------------------------------- /modules/bulk_roles/variables.tf: -------------------------------------------------------------------------------- 1 | variable "roles" { 2 | default = {} 3 | description = "Map of roles to create. 'name' required. Values from the 'snowflake_user' resource will be applied. 'name' is required." 4 | type = map(object({ 5 | name = optional(string) 6 | comment = optional(string) 7 | }) 8 | ) 9 | } 10 | 11 | variable "default_comment" { 12 | type = string 13 | description = "Comment to be added to each warehouse, when no other comment specified." 14 | default = "Role managed by Terraform." 15 | } 16 | -------------------------------------------------------------------------------- /modules/bulk_role_grants/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.23.1" 6 | } 7 | random = { 8 | version = ">=2.2.0" 9 | } 10 | } 11 | experiments = [module_variable_optional_attrs] 12 | } 13 | 14 | resource "snowflake_role_grants" "main" { 15 | for_each = var.grants 16 | 17 | role_name = coalesce(each.value["role_name"], each.key) 18 | roles = coalesce(each.value["roles"], var.default_roles) 19 | users = coalesce(each.value["users"], var.default_users) 20 | } 21 | -------------------------------------------------------------------------------- /examples/dbt-quickstart/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | public_role = "PUBLIC" 3 | sysadmin_role = "SYSADMIN" 4 | employees = { 5 | "EMPLOYEE_A" = { 6 | name = "employee.a@immuta.com" 7 | login_name = "employee.a@immuta.com" 8 | } 9 | "EMPLOYEE_B" = { 10 | email = "employee.b@immuta.com" 11 | } 12 | } 13 | system_users = { 14 | "LOOKER_USER" = {} 15 | "SUPERSET_USER" = {} 16 | "IMMUTA_USER" = {} 17 | "HIGHTOUCH_USER" = {} 18 | "DBT_CLOUD_USER" = { 19 | default_role = module.bulk_roles.roles["DBT_CLOUD"].name 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "users" { 2 | description = "System users generated by the core module." 3 | sensitive = true 4 | value = module.employees.users 5 | } 6 | 7 | output "roles" { 8 | description = "System users generated by the core module." 9 | value = module.bulk_roles.roles 10 | } 11 | 12 | output "warehouses" { 13 | description = "System users generated by the core module." 14 | value = module.bulk_warehouses.warehouses 15 | } 16 | 17 | output "analytics_application" { 18 | description = "Manually selected applilcation users generated by each application_database module." 19 | sensitive = true 20 | value = module.example_db 21 | } 22 | -------------------------------------------------------------------------------- /modules/application_database/user.tf: -------------------------------------------------------------------------------- 1 | // database user (optional) 2 | resource "snowflake_user" "app" { 3 | count = var.create_application_user ? 1 : 0 4 | 5 | name = local.user_name 6 | login_name = local.user_name 7 | password = random_password.app_user[0].result 8 | default_role = snowflake_role.admin.name 9 | default_namespace = snowflake_database.app.name 10 | default_warehouse = local.user_default_warehouse 11 | comment = var.description 12 | must_change_password = false 13 | } 14 | 15 | resource "random_password" "app_user" { 16 | count = var.create_application_user ? 1 : 0 17 | 18 | length = 16 19 | special = false 20 | } -------------------------------------------------------------------------------- /modules/bulk_warehouse_grants/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.18.1" 6 | } 7 | random = { 8 | version = ">=2.2.0" 9 | } 10 | } 11 | experiments = [module_variable_optional_attrs] 12 | } 13 | 14 | resource "snowflake_warehouse_grant" "main" { 15 | for_each = var.grants 16 | 17 | warehouse_name = coalesce(each.value["warehouse_name"], each.key) 18 | privilege = coalesce(each.value["privilege"], var.default_privilege) 19 | roles = coalesce(each.value["roles"], var.default_roles) 20 | with_grant_option = coalesce(each.value["with_grant_option"], var.default_with_grant_option) 21 | } 22 | -------------------------------------------------------------------------------- /modules/bulk_role_grants/variables.tf: -------------------------------------------------------------------------------- 1 | variable "grants" { 2 | default = {} 3 | description = "Map of role grants to create. The module will look for default grant arguments, otherwise apply null." 4 | type = map(object({ 5 | role_name = optional(string) 6 | users = optional(list(string)) 7 | roles = optional(list(string)) 8 | }) 9 | ) 10 | } 11 | 12 | variable "default_roles" { 13 | type = list(string) 14 | description = "Roles to be granted access to the resource, if not specified." 15 | default = [] 16 | } 17 | 18 | variable "default_users" { 19 | type = list(string) 20 | description = "Users to be granted access to the resource, if not specified." 21 | default = [] 22 | } 23 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "snowflake_account" { 2 | description = "The Snowflake account for resources to be loaded into." 3 | type = string 4 | } 5 | 6 | variable "snowflake_region" { 7 | description = "The AWS region of the Snowflake account." 8 | type = string 9 | } 10 | 11 | variable "snowflake_username" { 12 | description = "The username for the Snowflake Terraform user" 13 | sensitive = true 14 | type = string 15 | } 16 | 17 | variable "snowflake_user_password" { 18 | description = "The password for the Snowflake Terraform user" 19 | sensitive = true 20 | type = string 21 | } 22 | 23 | variable "snowflake_user_role" { 24 | description = "The role of the Terraform user." 25 | type = string 26 | } 27 | -------------------------------------------------------------------------------- /examples/dbt-quickstart/variables.tf: -------------------------------------------------------------------------------- 1 | variable "snowflake_account" { 2 | description = "The Snowflake account for resources to be loaded into." 3 | type = string 4 | } 5 | 6 | variable "snowflake_region" { 7 | description = "The AWS region of the Snowflake account." 8 | type = string 9 | } 10 | 11 | variable "snowflake_username" { 12 | description = "The username for the Snowflake Terraform user" 13 | type = string 14 | } 15 | 16 | variable "snowflake_user_password" { 17 | description = "The password for the Snowflake Terraform user" 18 | type = string 19 | } 20 | 21 | variable "snowflake_user_role" { 22 | default = "TERRAFORM" 23 | description = "The role of the Terraform user." 24 | type = string 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | .terraform.lock.hcl 8 | 9 | # Crash log files 10 | crash.log 11 | 12 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 13 | # .tfvars files are managed as part of configuration and so should be included in 14 | # version control. 15 | # 16 | # example.tfvars 17 | secrets.tfvars 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # 28 | # !example_override.tf 29 | 30 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 31 | # example: *tfplan* 32 | -------------------------------------------------------------------------------- /modules/application_database/outputs.tf: -------------------------------------------------------------------------------- 1 | output "database" { 2 | description = "Database resource created for the app." 3 | value = snowflake_database.app 4 | } 5 | 6 | output "admin_role" { 7 | description = "Admin resource created for the app. Should not be granted to roles and users outside of the module." 8 | value = snowflake_role.admin 9 | } 10 | 11 | output "reader_role" { 12 | description = "Role resource created for the app. Should not be granted to roles and resources outside of the module." 13 | value = snowflake_role.reader 14 | } 15 | 16 | output "user" { 17 | description = "User resource created for the app. May be null." 18 | value = var.create_application_user ? snowflake_user.app[0] : null 19 | } 20 | 21 | output "warehouse" { 22 | description = "Warehouse resource created for the app. May be null." 23 | value = var.create_application_warehouse ? snowflake_warehouse.app[0] : null 24 | } -------------------------------------------------------------------------------- /modules/bulk_warehouse_grants/variables.tf: -------------------------------------------------------------------------------- 1 | variable "grants" { 2 | default = {} 3 | description = "Map of warehouse grants to create. The module will look for default grant arguments, otherwise apply null." 4 | type = map(object({ 5 | warehouse_name = optional(string) 6 | privilege = optional(string) 7 | roles = optional(list(string)) 8 | with_grant_option = optional(bool) 9 | }) 10 | ) 11 | } 12 | 13 | variable "default_privilege" { 14 | type = string 15 | description = "Privilege to be granted if not specified. Additional privileges require an additional block." 16 | default = "USAGE" 17 | } 18 | 19 | variable "default_roles" { 20 | type = list(string) 21 | description = "Roles to be granted access to the resource, if not specified." 22 | default = [] 23 | } 24 | 25 | variable "default_with_grant_option" { 26 | type = bool 27 | description = "When this is set to true, allows the recipient role to grant the privileges to other roles.." 28 | default = false 29 | } 30 | -------------------------------------------------------------------------------- /modules/application_database/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | admin_role_name = upper("${snowflake_database.app.name}${var.admin_role_name_suffix}") 3 | create_reader_role_grants = length(var.grant_read_to_roles) > 0 || length(var.grant_read_to_users) > 0 ? 1 : 0 4 | create_warehouse_monitor = var.create_application_warehouse && var.create_application_warehouse_monitor 5 | database_name = upper(var.database_name) 6 | grant_admin_to_users = ( 7 | var.create_application_user ? 8 | concat(var.grant_admin_to_users, [snowflake_user.app[0].name]) : 9 | var.grant_admin_to_users 10 | ) 11 | reader_role_name = upper("${snowflake_database.app.name}${var.reader_role_name_suffix}") 12 | user_default_warehouse = ( 13 | var.create_application_warehouse ? 14 | coalesce(var.application_user_default_warehouse, local.warehouse_name) : 15 | var.application_user_default_warehouse 16 | ) 17 | 18 | user_name = upper("${var.database_name}_USER") 19 | warehouse_name = upper("${var.database_name}_WH") 20 | warehouse_monitor_name = upper("${local.warehouse_name}_MONITOR") 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Immuta Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/application_database/README.md: -------------------------------------------------------------------------------- 1 | # Application Database 2 | 3 | The `application_database` module creates a new database, optionally with a user and waerhouse. 4 | For example, if creating a `MY_APP` database, the module would stand up the following resources: 5 | 6 | - `MY_APP` database 7 | - `MY_APP` role 8 | - `MY_APP_WH` warehouse (optional) 9 | - `MY_APP_USER` system user (optional) 10 | 11 | The `MY_APP` role is granted all privileges on the resources in this application. 12 | This role can then be granted to other roles or users in your platform to 13 | simplify derivative resource reation and management. 14 | 15 | ## Example 16 | 17 | A perfect use case for the `application_database` module is for developer databases. 18 | 19 | ```{terraform} 20 | locals { 21 | developer_list = ["USER1", "USER2"] 22 | } 23 | 24 | module developer_dbs { 25 | for_each = local.developer_list 26 | source = "./modules/application_database" 27 | 28 | database_name = each.value 29 | grant_admin_to_users = [each.value] 30 | } 31 | ``` 32 | 33 | This would produce 2 separate databases, each only accessible to the respective developer. -------------------------------------------------------------------------------- /modules/bulk_roles/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Roles 2 | 3 | The `bulk_roles` module simplifies the creation of multiple Snowflake roles. 4 | 5 | Please note that this module does not handle any resource grants. 6 | 7 | ## Inputs 8 | 9 | The `bulk_roles` module accepts a map of resource configurations, along with 10 | default attribute values for those resources that are not more specific. 11 | 12 | For each resource, resource attributes are decided in the following order: 13 | 14 | 1. Attributes specified in the input resource map: e.g., `var.roles[key][attribute]` 15 | 2. Attribues specified as module inputs: e.g., `var.default_size 16 | 3. Default attribute values for the resource. (For the `name` attribute, the map `key` will be used.) 17 | 18 | For example, a set of roles could be created by passing the following map: 19 | 20 | ```{terraform} 21 | module roles { 22 | source = "./modules/bulk_roles 23 | 24 | roles = { 25 | marketing = {} 26 | product = {} 27 | sales = {} 28 | data_team = { 29 | comment = "They're just so cool. 30 | } 31 | } 32 | 33 | default_comment = "Organization roles managed by Terraform" 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /modules/application_database/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">=0.14" 3 | required_providers { 4 | snowflake = { 5 | source = "chanzuckerberg/snowflake" 6 | version = ">=0.23.1" 7 | } 8 | time = { 9 | source = "hashicorp/time" 10 | version = ">=0.7.0" 11 | } 12 | } 13 | } 14 | 15 | // application database 16 | resource "snowflake_database" "app" { 17 | name = local.database_name 18 | comment = var.description 19 | } 20 | 21 | // application admin and reader roles 22 | resource "snowflake_role" "admin" { 23 | name = local.admin_role_name 24 | comment = var.description 25 | } 26 | 27 | resource "snowflake_role_grants" "admin" { 28 | role_name = snowflake_role.admin.name 29 | roles = var.grant_admin_to_roles 30 | users = local.grant_admin_to_users 31 | } 32 | 33 | resource "snowflake_role" "reader" { 34 | name = local.reader_role_name 35 | comment = var.description 36 | } 37 | 38 | resource "snowflake_role_grants" "reader" { 39 | count = local.create_reader_role_grants 40 | 41 | role_name = snowflake_role.reader.name 42 | roles = var.grant_read_to_roles 43 | users = var.grant_read_to_users 44 | } 45 | -------------------------------------------------------------------------------- /modules/bulk_warehouse_grants/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Warehouse Grants 2 | 3 | The `bulk_warehouse_grants` module simplifies the creation of multiple Snowflake warehouse grants. 4 | 5 | ## Inputs 6 | 7 | The `bulk_warehouse_grants` module accepts a map of resource configurations, along with 8 | default attribute values for those resources that are not more specific. 9 | 10 | For each resource, resource attributes are decided in the following order: 11 | 12 | 1. Attributes specified in the input resource map: e.g., `var.warehouse_grants[key][attribute]` 13 | 2. Attribues specified as module inputs: e.g., `var.default_size 14 | 3. Default attribute values for the resource. (For the `warehouse_name` attribute, the map `key` will be used.) 15 | 16 | For example, a set of grants could be issued by passing the following map: 17 | 18 | ```{terraform} 19 | module warehouse_grants { 20 | source = "./modules/bulk_warehouse_grants 21 | 22 | grants = { 23 | loading_wh = {} 24 | transforming_wh = {} 25 | reporting_wh = { 26 | roles = ["LOOKER_ROLE", "PUBLIC"] 27 | } 28 | } 29 | default_privilege = "USAGE" 30 | default_roles = ["DATA_TEAM_ROLE"] 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /modules/bulk_warehouses/variables.tf: -------------------------------------------------------------------------------- 1 | variable "warehouses" { 2 | default = {} 3 | description = "Optional config for each warehouse to be created. Key will be used as 'name' if not specified." 4 | type = map(object({ 5 | name = optional(string) 6 | comment = optional(string) 7 | auto_suspend = optional(string) 8 | auto_resume = optional(bool) 9 | warehouse_size = optional(string) 10 | create_resource_monitor = optional(bool) 11 | }) 12 | ) 13 | } 14 | 15 | variable "default_create_resource_monitor" { 16 | type = bool 17 | default = false 18 | description = "If true, each warehouse created will have a resource monitor created." 19 | } 20 | 21 | variable "default_auto_resume" { 22 | type = bool 23 | default = true 24 | } 25 | 26 | variable "default_auto_suspend" { 27 | type = string 28 | default = 60 29 | } 30 | 31 | variable "default_comment" { 32 | type = string 33 | description = "Comment to be added to each warehouse, when no other comment specified." 34 | default = "Warehouse managed by Terraform." 35 | } 36 | 37 | variable "default_size" { 38 | type = string 39 | default = "x-small" 40 | } 41 | -------------------------------------------------------------------------------- /modules/bulk_warehouses/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Warehouses 2 | 3 | The `bulk_warehouses` module simplifies the creation of multiple Snowflake warehouses. 4 | 5 | Please note that this module does not handle any resource grants. 6 | 7 | ## Inputs 8 | 9 | The `bulk_warehouses` module accepts a map of resource configurations, along with 10 | default attribute values for those resources that are not more specific. 11 | 12 | For each resource, resource attributes are decided in the following order: 13 | 14 | 1. Attributes specified in the input resource map: e.g., `var.warehouses[key][attribute]` 15 | 2. Attribues specified as module inputs: e.g., `var.default_size 16 | 3. Default attribute values for the resource. (For the `name` attribute, the map `key` will be used.) 17 | 18 | For example, a set of warehouses could be created by passing the following map: 19 | 20 | ```{terraform} 21 | module warehouses { 22 | source = "./modules/bulk_warehouses 23 | 24 | warehouses = { 25 | loading_wh = {} 26 | reporting = { 27 | name = "REPORT_WH" 28 | comment = "Warehouse for end user reporting." 29 | size = "x-small" 30 | auto_suspend = 180 31 | } 32 | } 33 | default_size = "medium" 34 | default_comment = "Managed by Terraform" 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /modules/bulk_role_grants/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Role Grants 2 | 3 | The `bulk_role_grants` module simplifies the creation of multiple Snowflake role grants. 4 | 5 | ## Inputs 6 | 7 | The `bulk_role_grants` module accepts a map of resource configurations, along with 8 | default attribute values for those resources that are not more specific. 9 | 10 | For each resource, resource attributes are decided in the following order: 11 | 12 | 1. Attributes specified in the input resource map: e.g., `var.role_grants[key][attribute]` 13 | 2. Attribues specified as module inputs: e.g., `var.default_size 14 | 3. Default attribute values for the resource. (For the `role_name` attribute, the map `key` will be used.) 15 | 16 | For example, a set of grants could be issued by passing the following map: 17 | 18 | ```{terraform} 19 | module role_grants { 20 | source = "./modules/bulk_role_grants 21 | 22 | grants = { 23 | marketing = {} # Grant "marketing" role to defaults 24 | product = {} 25 | sales = {} 26 | data_team = { 27 | role_name = "DATA_TEAM_ROLE 28 | roles = [] # override default grant 29 | users = ["HARRY"] # override default user 30 | } 31 | } 32 | default_roles = ["DATA_TEAM] 33 | default_users = ["HARRY", "RON", "HERMIONE"] 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /modules/bulk_users/README.md: -------------------------------------------------------------------------------- 1 | # Bulk Users 2 | 3 | The `bulk_users` module simplifies the creation of multiple Snowflake users. 4 | 5 | Please note that this module does not handle any resource grants. 6 | 7 | ## Inputs 8 | 9 | The `bulk_users` module accepts a map of resource configurations, along with 10 | default attribute values for those resources that are not more specific. 11 | 12 | For each resource, resource attributes are decided in the following order: 13 | 14 | 1. Attributes specified in the input resource map: e.g., `var.users[key][attribute]` 15 | 2. Attribues specified as module inputs: e.g., `var.default_comment` 16 | 3. Default attribute values for the resource. (For the `name` attribute, the map `key` will be used.) 17 | 18 | For example, a set of warehouses could be created by passing the following map: 19 | 20 | ```{terraform} 21 | module employees { 22 | source = "./modules/bulk_users 23 | 24 | warehouses = { 25 | harry = {} 26 | hermione = {} 27 | ron = { 28 | first_name = "Ronald" 29 | login_name = "Ron" 30 | } 31 | } 32 | 33 | default_must_change_password = true 34 | default_comment = "Employee user managed by Terraform" 35 | } 36 | ``` 37 | 38 | This module can simplify the creation of generic system users 39 | that may need read-only access to parts of your system. Note that need privileged 40 | access to an isolated database may be best handled by the `application_database` 41 | module. 42 | -------------------------------------------------------------------------------- /modules/application_database/warehouse.tf: -------------------------------------------------------------------------------- 1 | // database warehouse (optional) 2 | resource "snowflake_warehouse" "app" { 3 | count = var.create_application_warehouse ? 1 : 0 4 | 5 | name = local.warehouse_name 6 | auto_suspend = var.application_warehouse_auto_suspend_time 7 | auto_resume = var.application_warehouse_auto_resume_time 8 | comment = var.description 9 | warehouse_size = var.application_warehouse_size 10 | } 11 | 12 | resource "snowflake_warehouse_grant" "app_role" { 13 | count = var.create_application_warehouse ? 1 : 0 14 | 15 | warehouse_name = local.warehouse_name 16 | privilege = "USAGE" 17 | roles = [snowflake_role.admin.name] 18 | } 19 | 20 | resource "time_offset" "monitor_start_times" { 21 | count = local.create_warehouse_monitor ? 1 : 0 22 | 23 | offset_days = 1 24 | } 25 | 26 | resource "snowflake_resource_monitor" "app" { 27 | count = local.create_warehouse_monitor ? 1 : 0 28 | 29 | name = local.warehouse_monitor_name 30 | credit_quota = 24 31 | frequency = "DAILY" 32 | start_timestamp = formatdate("YYYY-MM-DD 00:00", time_offset.monitor_start_times[0].rfc3339) 33 | end_timestamp = null 34 | 35 | notify_triggers = [100] 36 | suspend_triggers = [] 37 | suspend_immediate_triggers = [] 38 | 39 | // Snowflake will convert the timestamp provided into a 40 | // localized format, causing continual errors if not ignored 41 | lifecycle { 42 | ignore_changes = [ 43 | start_timestamp 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/dbt-quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Fishtown Analyltics Quick-Start 2 | 3 | Instantiating the above module will create a set of resources mostly aligned 4 | with those outined in [this great Fishtown Analytics blog post](https://blog.getdbt.com/how-we-configure-snowflake/). 5 | 6 | 7 | Below is a summary of the resources created: 8 | 9 | 1. Two "employee users", each with their own isolated development databases and access to _only_ read-only assets in the _ANALYTICS_ database. 10 | 11 | 2. Three "raw" storage application databases - STITCH, FIVETRAN, and MELTANO - with system users that have administrative access. 12 | 13 | 3. Two databases accessible only by the `DBT_CLOUD` role: `ANALYTICS_STAGING` and `ANALYTICS_PROD`. 14 | 15 | 4. A couple of general roles: 16 | - `ANALYST` should be able to read _only_ from the production `ANALYTICS_PROD` database. 17 | - The `READER` role with read-only access to all databases. 18 | - `SYSADMIN` should have admin access to all the created database resources. 19 | - `DBT_CLOUD` should have admin access to the dev and staging analytics databases, and read-only access to other databases. 20 | 21 | 22 | ## Additional resources 23 | 24 | Some great content from dbt Labs, whether you use Terraform or not. 25 | 26 | - dbt Blog: [How we configure Snowflake](https://blog.getdbt.com/how-we-configure-snowflake/) 27 | - dbt Discourse: [Setting up Snowflake - the exact grant statements we run](https://discourse.getdbt.com/t/setting-up-snowflake-the-exact-grant-statements-we-run/439) 28 | - dbt Blog: [Five principles that will keep your data warehouse organized](https://blog.getdbt.com/five-principles-that-will-keep-your-data-warehouse-organized/) 29 | -------------------------------------------------------------------------------- /modules/bulk_users/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.25.0" 6 | } 7 | random = { 8 | version = ">=2.2.0" 9 | } 10 | } 11 | experiments = [module_variable_optional_attrs] 12 | } 13 | 14 | locals { 15 | users_requiring_password = [ 16 | for k, v in var.users : k if coalesce(v.generate_user_password, var.default_generate_user_password) 17 | ] 18 | } 19 | 20 | 21 | resource "snowflake_user" "main" { 22 | for_each = var.users 23 | 24 | name = coalesce(each.value["name"], each.key) 25 | email = each.value["email"] 26 | first_name = each.value["first_name"] 27 | last_name = each.value["last_name"] 28 | login_name = each.value["login_name"] 29 | display_name = each.value["display_name"] 30 | 31 | password = coalesce(each.value["generate_user_password"], var.default_generate_user_password) ? random_password.users[each.key].result : null 32 | must_change_password = coalesce(each.value["must_change_password"], var.default_must_change_password) 33 | 34 | comment = try(coalesce(each.value["comment"], var.default_comment), null) 35 | default_role = try(coalesce(each.value["default_role"], var.default_role), null) 36 | default_namespace = try(coalesce(each.value["default_namespace"], var.default_namespace), null) 37 | default_warehouse = try(coalesce(each.value["default_warehouse"], var.default_warehouse), null) 38 | 39 | depends_on = [random_password.users] 40 | lifecycle { 41 | ignore_changes = [ 42 | password, 43 | must_change_password 44 | ] 45 | } 46 | } 47 | 48 | resource "random_password" "users" { 49 | for_each = toset(local.users_requiring_password) 50 | 51 | length = 16 52 | special = false 53 | } 54 | -------------------------------------------------------------------------------- /modules/bulk_warehouses/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.23.1" 6 | } 7 | time = { 8 | version = ">=0.7.2" 9 | } 10 | } 11 | experiments = [module_variable_optional_attrs] 12 | } 13 | 14 | locals { 15 | monitored_warehouses = [ 16 | for k, v in var.warehouses : k if coalesce(v.create_resource_monitor, var.default_create_resource_monitor) 17 | ] 18 | } 19 | 20 | resource "snowflake_warehouse" "main" { 21 | for_each = var.warehouses 22 | 23 | name = coalesce(each.value["name"], each.key) 24 | auto_suspend = try(coalesce(each.value["auto_suspend"], var.default_auto_suspend), null) 25 | auto_resume = try(coalesce(each.value["auto_resume"], var.default_auto_resume), null) 26 | comment = try(coalesce(each.value["comment"], var.default_comment), null) 27 | warehouse_size = try(coalesce(each.value["warehouse_size"], var.default_size), null) 28 | } 29 | 30 | resource "time_offset" "monitor_start_times" { 31 | for_each = toset(local.monitored_warehouses) 32 | 33 | offset_days = 1 34 | } 35 | 36 | resource "snowflake_resource_monitor" "main" { 37 | for_each = toset(local.monitored_warehouses) 38 | 39 | name = "${each.key}_monitor" 40 | credit_quota = 24 41 | 42 | frequency = "DAILY" 43 | start_timestamp = formatdate( 44 | "YYYY-MM-DD 00:00", 45 | time_offset.monitor_start_times[each.key].rfc3339 46 | ) 47 | end_timestamp = null 48 | 49 | notify_triggers = [100] 50 | suspend_triggers = [] 51 | suspend_immediate_triggers = [] 52 | 53 | // Snowflake will convert the timestamp provided into a 54 | // localized format, causing continual errors if not ignored 55 | lifecycle { 56 | ignore_changes = [ 57 | start_timestamp 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /modules/bulk_users/variables.tf: -------------------------------------------------------------------------------- 1 | variable "users" { 2 | default = {} 3 | description = "Map of users to be created. Values from the 'snowflake_user' resource will be applied. 'name' is required." 4 | type = map(object({ 5 | name = optional(string) 6 | comment = optional(string) 7 | display_name = optional(string) 8 | email = optional(string) 9 | first_name = optional(string) 10 | last_name = optional(string) 11 | login_name = optional(string) 12 | default_namespace = optional(string) 13 | default_role = optional(string) 14 | default_warehouse = optional(string) 15 | must_change_password = optional(bool) 16 | generate_user_password = optional(bool) 17 | }) 18 | ) 19 | } 20 | 21 | variable "default_comment" { 22 | type = string 23 | description = "Comment to be added to each warehouse, when no other comment specified." 24 | default = "User managed by Terraform." 25 | } 26 | 27 | variable "default_namespace" { 28 | type = string 29 | description = "The default user namespace, applied when no user namespace provided." 30 | default = null 31 | } 32 | 33 | variable "default_role" { 34 | type = string 35 | description = "The default user role, applied when no user role provided." 36 | default = null 37 | } 38 | 39 | variable "default_warehouse" { 40 | type = string 41 | description = "The default user warehouse, applied when no user warehouse provided." 42 | default = null 43 | } 44 | 45 | variable "default_must_change_password" { 46 | type = bool 47 | description = "The default setting for whether a newly created user must change their password, applied when not individually provided." 48 | default = false 49 | } 50 | 51 | variable "default_generate_user_password" { 52 | type = bool 53 | description = "If true, Terraform will generate a random password for newly created users." 54 | default = false 55 | } 56 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | provider "snowflake" { 2 | account = var.snowflake_account 3 | region = var.snowflake_region 4 | username = var.snowflake_username 5 | password = var.snowflake_user_password 6 | role = var.snowflake_user_role 7 | } 8 | 9 | locals { 10 | developer_list = ["harry", "hermione"] 11 | } 12 | 13 | module "employees" { 14 | source = "./modules/bulk_users" 15 | users = { 16 | "harry" = {} 17 | "ron" = { 18 | first_name = "Ronald" 19 | } 20 | "hermione" = {} 21 | "fred" = { 22 | login_name = "fred" 23 | } 24 | } 25 | 26 | default_role = "PUBLIC" 27 | default_generate_user_password = true 28 | } 29 | 30 | module "bulk_roles" { 31 | source = "./modules/bulk_roles" 32 | roles = { 33 | analyst = { name = "ANALYST_ROLE" } 34 | } 35 | } 36 | 37 | module "bulk_warehouses" { 38 | source = "./modules/bulk_warehouses" 39 | warehouses = { 40 | transform = { 41 | name = "TRANSFORM_WH" 42 | create_resource_monitor = true 43 | } 44 | report = { 45 | name = "REPORTING_WH" 46 | size = "medium" 47 | } 48 | } 49 | default_size = "x-small" 50 | default_comment = "This is my warehouse comment." 51 | } 52 | 53 | // role and warehouse grants 54 | module "bulk_role_grants" { 55 | source = "./modules/bulk_role_grants" 56 | grants = { 57 | analyst = { 58 | role_name = module.bulk_roles.roles["analyst"].name 59 | users = [module.employees.users["harry"].name] 60 | } 61 | } 62 | } 63 | 64 | module "bulk_warehouse_grants" { 65 | source = "./modules/bulk_warehouse_grants" 66 | grants = { 67 | transform = { 68 | warehouse_name = module.bulk_warehouses.warehouses["transform"].name 69 | roles = [module.bulk_roles.roles["analyst"].name] 70 | } 71 | report = { 72 | warehouse_name = module.bulk_warehouses.warehouses["report"].name 73 | roles = [module.bulk_roles.roles["analyst"].name] 74 | } 75 | } 76 | } 77 | 78 | // databases 79 | module "example_db" { 80 | source = "./modules/application_database" 81 | 82 | database_name = "ANALYTICS" 83 | grant_admin_to_roles = [] 84 | grant_admin_to_users = [module.employees.users["ron"].name] 85 | grant_read_to_roles = [module.bulk_roles.roles["analyst"].name] 86 | } 87 | 88 | module "developer_dbs" { 89 | for_each = toset(local.developer_list) 90 | source = "./modules/application_database" 91 | 92 | database_name = "DEV_${module.employees.users[each.key].name}" 93 | admin_role_name_suffix = "" 94 | create_application_user = false 95 | create_application_warehouse = false 96 | grant_admin_to_users = [module.employees.users[each.key].name] 97 | } 98 | -------------------------------------------------------------------------------- /modules/application_database/variables.tf: -------------------------------------------------------------------------------- 1 | variable "create_application_user" { 2 | default = false 3 | description = "If true, creates a user with access to the database role." 4 | type = bool 5 | } 6 | 7 | variable "create_application_warehouse" { 8 | default = false 9 | description = "If true, creates a warehouse and grants it to the database role." 10 | type = bool 11 | } 12 | 13 | variable "create_application_warehouse_monitor" { 14 | default = false 15 | description = "If true, creates a warehouse monitor for the application warehouse. Requires ACCOUNTADMIN privileges." 16 | type = bool 17 | } 18 | 19 | variable "admin_role_name_suffix" { 20 | default = "_ADMIN" 21 | description = "The suffix appended to the database name to determine the admin role (e.g. APP_ADMIN)." 22 | type = string 23 | } 24 | 25 | variable "application_user_default_warehouse" { 26 | default = null 27 | description = "The name of the default warehouse to be used by the database_user. Only used when creating a user but not creating a warehouse." 28 | type = string 29 | } 30 | 31 | variable "application_warehouse_size" { 32 | default = "x-small" 33 | description = "The size of the warehouse to be created, if applicable." 34 | type = string 35 | } 36 | 37 | variable "application_warehouse_auto_suspend_time" { 38 | default = 60 39 | description = "The suspension time of the warehouse to be created, if applicable." 40 | type = number 41 | } 42 | 43 | variable "application_warehouse_auto_resume_time" { 44 | default = true 45 | description = "The suspension time of the warehouse to be created, if applicable." 46 | type = bool 47 | } 48 | 49 | variable "database_name" { 50 | description = "The database name will be used to drive the names of all application resources." 51 | type = string 52 | } 53 | 54 | variable "description" { 55 | default = "Application resources managed by Terraform." 56 | description = "Description to be applied to database resources." 57 | type = string 58 | } 59 | 60 | variable "grant_admin_to_roles" { 61 | default = [] 62 | description = "Additional roles that will have full access to the module resources." 63 | type = list(string) 64 | } 65 | 66 | variable "grant_admin_to_users" { 67 | default = [] 68 | description = "Additional users that will have full access to the module resources." 69 | type = list(string) 70 | } 71 | 72 | variable "grant_read_to_roles" { 73 | default = [] 74 | description = "Additional roles that should have read access to module resources." 75 | type = list(string) 76 | } 77 | 78 | variable "grant_read_to_users" { 79 | default = [] 80 | description = "Additional users that should have read access to module resources." 81 | type = list(string) 82 | } 83 | 84 | variable "grant_database_usage_to_roles" { 85 | default = [] 86 | description = "Additional roles that should have only the USAGE privilege on the module database. This allows sub-resources to be granted individually by the admin role." 87 | type = list(string) 88 | } 89 | 90 | variable "reader_role_name_suffix" { 91 | default = "_READER" 92 | description = "The suffix appended to the database name to determine the reader role (e.g. APP_READER)." 93 | type = string 94 | } -------------------------------------------------------------------------------- /examples/dbt-quickstart/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | snowflake = { 4 | source = "chanzuckerberg/snowflake" 5 | version = ">=0.23.2" 6 | } 7 | } 8 | } 9 | 10 | // CORE RESOURCES 11 | // This section generates base roles, warehouses and users 12 | // It does NOT create grants between these resources 13 | module "employees" { 14 | source = "../../modules/bulk_users" 15 | 16 | users = local.employees 17 | default_role = local.public_role 18 | default_must_change_password = true 19 | default_generate_user_password = true 20 | } 21 | 22 | module "systems" { 23 | source = "../../modules/bulk_users" 24 | 25 | users = local.system_users 26 | default_role = module.bulk_roles.roles["ANALYST"].name 27 | default_generate_user_password = true 28 | } 29 | 30 | module "bulk_roles" { 31 | source = "../../modules/bulk_roles" 32 | 33 | roles = { 34 | READER = {} 35 | ANALYST = {} 36 | DBT_CLOUD = {} 37 | } 38 | } 39 | 40 | module "bulk_warehouses" { 41 | source = "../../modules/bulk_warehouses" 42 | 43 | warehouses = { 44 | PROCESSING_WH = { size = "medium" } 45 | REPORTING_WH = {} 46 | } 47 | } 48 | 49 | // APPLICATION DATABASES 50 | // databases (and system users) to be leveraged for a single purpose 51 | module "analytics_db" { 52 | for_each = toset(["STAGING", "PROD"]) 53 | source = "../../modules/application_database" 54 | 55 | database_name = "ANALYTICS_${each.value}" 56 | grant_admin_to_roles = [local.sysadmin_role] 57 | grant_admin_to_users = [module.systems.users["DBT_CLOUD_USER"].name] 58 | grant_read_to_roles = [ 59 | module.bulk_roles.roles["READER"].name, 60 | module.bulk_roles.roles["ANALYST"].name, 61 | ] 62 | } 63 | 64 | module "stitch_db" { 65 | source = "../../modules/application_database" 66 | 67 | database_name = "STITCH" 68 | create_application_user = true 69 | create_application_warehouse = true 70 | grant_admin_to_roles = [local.sysadmin_role] 71 | grant_read_to_roles = [ 72 | module.bulk_roles.roles["READER"].name, 73 | ] 74 | } 75 | 76 | module "fivetran_db" { 77 | source = "../../modules/application_database" 78 | 79 | database_name = "FIVETRAN" 80 | create_application_user = true 81 | create_application_warehouse = true 82 | grant_admin_to_roles = [local.sysadmin_role] 83 | grant_read_to_roles = [ 84 | module.bulk_roles.roles["READER"].name, 85 | ] 86 | } 87 | 88 | module "meltano_db" { 89 | source = "../../modules/application_database" 90 | 91 | database_name = "MELTANO" 92 | create_application_user = true 93 | create_application_warehouse = true 94 | grant_admin_to_roles = [local.sysadmin_role] 95 | grant_read_to_roles = [ 96 | module.bulk_roles.roles["READER"].name, 97 | ] 98 | } 99 | 100 | module "developer_dbs" { 101 | for_each = module.employees.users 102 | source = "../../modules/application_database" 103 | 104 | database_name = "DEV_${each.key}" 105 | create_application_user = false 106 | create_application_warehouse = false 107 | grant_admin_to_users = [each.value.name] 108 | } 109 | 110 | // GRANTS 111 | // Grants on core roles and warehouses need to be performed 112 | // after all resources are defined and created. 113 | module "bulk_role_grants" { 114 | source = "../../modules/bulk_role_grants" 115 | grants = { 116 | READER = { 117 | roles = [module.bulk_roles.roles["ANALYST"].name] 118 | users = [module.employees.users["EMPLOYEE_A"].name] 119 | } 120 | } 121 | depends_on = [module.bulk_roles] 122 | } 123 | 124 | module "bulk_warehouse_grants" { 125 | source = "../../modules/bulk_warehouse_grants" 126 | grants = { 127 | PROCESSING_WH = { 128 | roles = concat( 129 | [for m in module.analytics_db : m.admin_role.name], 130 | [for m in module.developer_dbs : m.admin_role.name], 131 | [module.bulk_roles.roles["ANALYST"].name] 132 | ) 133 | } 134 | REPORTING_WH = { 135 | roles = [ 136 | module.bulk_roles.roles["ANALYST"].name, 137 | local.public_role 138 | ] 139 | } 140 | } 141 | depends_on = [module.bulk_warehouses] 142 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-snowflake-fast-data-warehouse 2 | 3 | Set up your data warehouse with speed and style using [Terraform's Snowflake provider](https://github.com/chanzuckerberg/terraform-provider-snowflake). 4 | 5 | Much of the work done in creating a data warehouse is the same old story: 6 | 7 | 1. Create some roles, warehouses, and users. 8 | 2. Create a database for some limited purpose, such as a developer database or application database. 9 | 3. Create a highly privileged role for administering that database, and give read access for to other roles. 10 | 4. Repeat, again and again, for each new application you add. 11 | 12 | This set of modules simplifies many of these common operations and provides easy 13 | handles for managing change over time, without managing a massive number of distinct 14 | Terraform resources. 15 | 16 | ![Overview of warehouse organization with this module.](./assets/overview.png) 17 | 18 | By modularizing these operations, we also avoid some common "gotchas", especially 19 | around role grants, and are able to include some convenient extras, like automatically 20 | creating resource monitors for new Snowflake warehouses. 21 | 22 | 23 | ## Strategy 24 | 25 | This Terraform module here has two types of resources: 26 | 27 | ### Bulk Resources 28 | 29 | The `bulk_` modules are wrappers around a Snowflake resource, such as 30 | `snowflake_users`, and accept a map of attributes for each resource specified, 31 | along with configurable defaults for unspecified attributes. 32 | 33 | For example, the code below creates 4 warehouses with default settings, except 34 | for the last one: 35 | 36 | ``` 37 | module my_warehouses { 38 | source = "./modules/bulk_warehouses" 39 | 40 | warehouses = { 41 | "marketing": {} 42 | "sales": {} 43 | "product": {} 44 | "data_team": { 45 | "size": "x-large" 46 | "comment": "Such a great team." 47 | } 48 | } 49 | default_size = "medium" 50 | default_auto_suspend = 60 51 | } 52 | ``` 53 | 54 | An individual warehouse can then be accessed downstream by referencing the 55 | output: `module.my_warehouses.warehouses["data_team"]`. 56 | 57 | ### Application Databases 58 | 59 | Most SaaS applications that have a non-trivial Snowflake integration ask the user 60 | to set up a database, user, warehouse, and privileged role for creating resources 61 | in the database. The `application_database` module abstracts the resource creation 62 | and dependency management of these resources and creates an easy interface for 63 | granting either full access or read-only access to the database resources. 64 | 65 | To create a new application database, for example, for a production `dbt` database: 66 | 67 | ``` 68 | module "dbt_database" { 69 | source = "./modules/application_database" 70 | 71 | db_name = "PROD" 72 | description = "Production resources for dbt transformation runs" 73 | create_user = true 74 | create_warehouse = true 75 | warehouse_size = "medium" 76 | 77 | grant_admin_to_roles = [] # Grants privileged role direftly 78 | grant_admin_to_users = [] # Grants privileged role directly 79 | grant_read_to_roles = ["READER"] # Grants read-only privileges to role 80 | } 81 | ``` 82 | 83 | This module is particularly useful for creating isolated sandboxes or multiple 84 | environments for a single application or process. Using the `for_each` argument, 85 | you can pass in a list of databases to be created and granted out. For example: 86 | 87 | ``` 88 | module "developer_databases" { 89 | for_each = local.developer_list 90 | source = "./modules/application_database" 91 | 92 | db_name = "DEV_${each.key}" 93 | grant_admin_to_users = [each.key] 94 | } 95 | ``` 96 | 97 | Standardizing the creation of these resources allows teams to worry much less 98 | about change management: when a user leaves, they can update (e.g.) the variable 99 | `local.developer_list` to tear down the associated user's resources. 100 | 101 | ### A word on GRANT resources 102 | 103 | *The Snowflake Terraform provider only allows a single `snowflake_role_grants` 104 | resource to be declared for each role, across the entire account.* 105 | 106 | The reason for this is simple: to manage the _removal_ of role grants, the resource 107 | must be able to know the entire set of roles and users that should be granted. 108 | 109 | The upshot is that it is easy for users who are new to Terraform to get into 110 | a situation where two `role_grant` resources are overwriting each other on every 111 | application, particularly because the user _also_ must loop through privileges 112 | to assign each individual one. 113 | 114 | To address this, the `application_database` module handles granting privileges 115 | within the module itself. *Users should _not_ create additional grants to module 116 | resources on top of those handled by the module.* 117 | 118 | The expected flow is: 119 | 120 | 1. Create a general-purpose role, such as `READER`. 121 | 2. Create an application database, passing in `READER` to the `grant_read_to_roles` variable. 122 | 3. Grant the `READER` role to users with the `bulk_role_grants` (or regular role grants) resources. 123 | 124 | ## Setting up the TERRAFORM role 125 | 126 | The user running Terraform will need elevated permissions to create users, 127 | databases, and other resources. Here is a snippet you can run to grant the 128 | resources required for this module. 129 | 130 | ```{terraform} 131 | // set up user and role 132 | create user terraform_user; 133 | create role terraform; 134 | grant role terraform to user terraform_user; 135 | 136 | // grant account privileges 137 | grant create user on account to role terraform; 138 | grant create database on account to role terraform; 139 | grant create integration on account to role terraform; 140 | grant create role on account to role terraform; 141 | grant create warehouse on account to role terraform; 142 | grant manage grants on account to role terraform; 143 | 144 | // Note that, currently, `CREATE RESOURCE MONITOR` cannot be granted to another user 145 | ``` 146 | 147 | *Note that some resources, such as `snowflake_resource_monitor` must be executed 148 | as the `ACCOUNTADMIN` role!* 149 | 150 | As with any highly privileged role, you should protect these permissions very 151 | carefully, as they allow the user to set up your warehouse -- and tear it down! 152 | Snowflake's new Organizations features make it much easier to stand up development 153 | environments for implementing a staging and production workflow. 154 | -------------------------------------------------------------------------------- /modules/application_database/privileges.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | all_admin_roles = [snowflake_role.admin.name] 3 | all_read_roles = [snowflake_role.admin.name, snowflake_role.reader.name] 4 | // https://docs.snowflake.com/en/sql-reference/sql/grant-privilege.html 5 | admin_privileges = { 6 | database = ["CREATE SCHEMA"] 7 | schema = [ 8 | "CREATE EXTERNAL TABLE", 9 | "CREATE FILE FORMAT", 10 | "CREATE FUNCTION", 11 | "CREATE MASKING POLICY", 12 | # "CREATE MATERIALIZED VIEW", # Not GA yet 13 | "CREATE PIPE", 14 | "CREATE PROCEDURE", 15 | # "CREATE ROW ACCESS POLICY", # Not GA yet 16 | "CREATE SEQUENCE", 17 | "CREATE STAGE", 18 | "CREATE STREAM", 19 | "CREATE TABLE", 20 | # "CREATE TAG", # Not GA yet 21 | "CREATE TASK", 22 | "CREATE VIEW", 23 | "MODIFY", 24 | "MONITOR", 25 | ] 26 | table = [ 27 | "DELETE", 28 | "INSERT", 29 | "TRUNCATE", 30 | "UPDATE", 31 | ] 32 | view = [] 33 | // Public schema only 34 | external_table = [] 35 | file_format = [] 36 | function = [] 37 | # materialized_view = [] 38 | masking_policy = ["APPLY"] 39 | pipe = ["OPERATE"] 40 | procedure = [] 41 | sequence = [] 42 | stage = [] # Adding ["WRITE", "READ"] throws a 'Privilege Order' violation 43 | stream = [] 44 | task = ["OPERATE"] 45 | } 46 | reader_privileges = { 47 | // Full application database 48 | database = ["USAGE"] 49 | schema = ["USAGE"] 50 | table = ["SELECT", "REFERENCES"] 51 | view = ["SELECT", "REFERENCES"] 52 | // Public schema only 53 | external_table = ["SELECT"] 54 | file_format = ["USAGE"] 55 | function = ["USAGE"] 56 | # materialized_view = ["SELECT"] 57 | masking_policy = [] 58 | pipe = ["MONITOR"] 59 | procedure = ["USAGE"] 60 | sequence = ["USAGE"] 61 | stage = ["USAGE"] 62 | stream = ["SELECT"] 63 | task = ["MONITOR"] 64 | } 65 | public_schema_name = "PUBLIC" 66 | } 67 | 68 | 69 | // read privileges on database + child objects 70 | resource "snowflake_database_grant" "read" { 71 | for_each = toset(local.reader_privileges["database"]) 72 | 73 | database_name = snowflake_database.app.name 74 | privilege = each.key 75 | roles = concat( 76 | local.all_read_roles, 77 | var.grant_database_usage_to_roles 78 | ) 79 | } 80 | 81 | resource "snowflake_schema_grant" "read" { 82 | for_each = toset(local.reader_privileges["schema"]) 83 | 84 | database_name = snowflake_database.app.name 85 | on_future = true 86 | privilege = each.key 87 | roles = local.all_read_roles 88 | } 89 | 90 | resource "snowflake_schema_grant" "public_read" { 91 | for_each = toset(local.reader_privileges["schema"]) 92 | 93 | database_name = snowflake_database.app.name 94 | schema_name = local.public_schema_name 95 | privilege = each.key 96 | roles = concat( 97 | local.all_read_roles, 98 | var.grant_database_usage_to_roles 99 | ) 100 | } 101 | 102 | resource "snowflake_table_grant" "read" { 103 | for_each = toset(local.reader_privileges["table"]) 104 | 105 | database_name = snowflake_database.app.name 106 | on_future = true 107 | privilege = each.key 108 | roles = local.all_read_roles 109 | } 110 | 111 | resource "snowflake_view_grant" "read" { 112 | for_each = toset(local.reader_privileges["view"]) 113 | 114 | database_name = snowflake_database.app.name 115 | on_future = true 116 | privilege = each.key 117 | roles = local.all_read_roles 118 | } 119 | 120 | // elevated privileges on database + child objects 121 | 122 | resource "snowflake_database_grant" "admin" { 123 | for_each = toset(local.admin_privileges["database"]) 124 | 125 | database_name = snowflake_database.app.name 126 | privilege = each.key 127 | roles = local.all_admin_roles 128 | } 129 | 130 | resource "snowflake_schema_grant" "admin" { 131 | for_each = toset(local.admin_privileges["schema"]) 132 | 133 | database_name = snowflake_database.app.name 134 | on_future = true 135 | privilege = each.key 136 | roles = local.all_admin_roles 137 | } 138 | 139 | resource "snowflake_schema_grant" "public_admin" { 140 | for_each = toset(local.admin_privileges["schema"]) 141 | 142 | database_name = snowflake_database.app.name 143 | schema_name = local.public_schema_name 144 | privilege = each.key 145 | roles = local.all_admin_roles 146 | } 147 | 148 | resource "snowflake_table_grant" "admin" { 149 | for_each = toset(local.admin_privileges["table"]) 150 | 151 | database_name = snowflake_database.app.name 152 | on_future = true 153 | privilege = each.key 154 | roles = local.all_admin_roles 155 | } 156 | 157 | resource "snowflake_view_grant" "admin" { 158 | for_each = toset(local.admin_privileges["view"]) 159 | 160 | database_name = snowflake_database.app.name 161 | on_future = true 162 | privilege = each.key 163 | roles = local.all_admin_roles 164 | } 165 | 166 | // schema level grants 167 | // only applied to the PUBLIC schema 168 | resource "snowflake_external_table_grant" "read" { 169 | for_each = toset(local.reader_privileges["external_table"]) 170 | 171 | database_name = snowflake_database.app.name 172 | schema_name = local.public_schema_name 173 | privilege = each.key 174 | roles = local.all_read_roles 175 | on_future = true 176 | } 177 | 178 | resource "snowflake_file_format_grant" "read" { 179 | for_each = toset(local.reader_privileges["file_format"]) 180 | 181 | database_name = snowflake_database.app.name 182 | schema_name = local.public_schema_name 183 | privilege = each.key 184 | roles = local.all_read_roles 185 | on_future = true 186 | } 187 | 188 | resource "snowflake_pipe_grant" "read" { 189 | for_each = toset(local.reader_privileges["pipe"]) 190 | 191 | database_name = snowflake_database.app.name 192 | schema_name = local.public_schema_name 193 | privilege = each.key 194 | roles = local.all_read_roles 195 | on_future = true 196 | } 197 | 198 | resource "snowflake_procedure_grant" "read" { 199 | for_each = toset(local.reader_privileges["procedure"]) 200 | 201 | database_name = snowflake_database.app.name 202 | schema_name = local.public_schema_name 203 | privilege = each.key 204 | roles = local.all_read_roles 205 | on_future = true 206 | } 207 | 208 | resource "snowflake_sequence_grant" "read" { 209 | for_each = toset(local.reader_privileges["sequence"]) 210 | 211 | database_name = snowflake_database.app.name 212 | schema_name = local.public_schema_name 213 | privilege = each.key 214 | roles = local.all_read_roles 215 | on_future = true 216 | } 217 | 218 | resource "snowflake_stage_grant" "read" { 219 | for_each = toset(local.reader_privileges["stage"]) 220 | 221 | database_name = snowflake_database.app.name 222 | schema_name = local.public_schema_name 223 | privilege = each.key 224 | roles = local.all_read_roles 225 | on_future = true 226 | } 227 | 228 | resource "snowflake_stream_grant" "read" { 229 | for_each = toset(local.reader_privileges["stream"]) 230 | 231 | database_name = snowflake_database.app.name 232 | schema_name = local.public_schema_name 233 | privilege = each.key 234 | roles = local.all_read_roles 235 | on_future = true 236 | } 237 | 238 | 239 | // schema level admin grants 240 | // only applied to the PUBLIC schema 241 | resource "snowflake_external_table_grant" "admin" { 242 | for_each = toset(local.admin_privileges["external_table"]) 243 | 244 | database_name = snowflake_database.app.name 245 | schema_name = local.public_schema_name 246 | privilege = each.key 247 | roles = local.all_admin_roles 248 | on_future = true 249 | } 250 | 251 | resource "snowflake_file_format_grant" "admin" { 252 | for_each = toset(local.admin_privileges["file_format"]) 253 | 254 | database_name = snowflake_database.app.name 255 | schema_name = local.public_schema_name 256 | privilege = each.key 257 | roles = local.all_admin_roles 258 | on_future = true 259 | } 260 | 261 | resource "snowflake_pipe_grant" "admin" { 262 | for_each = toset(local.admin_privileges["pipe"]) 263 | 264 | database_name = snowflake_database.app.name 265 | schema_name = local.public_schema_name 266 | privilege = each.key 267 | roles = local.all_admin_roles 268 | on_future = true 269 | } 270 | 271 | resource "snowflake_procedure_grant" "admin" { 272 | for_each = toset(local.admin_privileges["procedure"]) 273 | 274 | database_name = snowflake_database.app.name 275 | schema_name = local.public_schema_name 276 | privilege = each.key 277 | roles = local.all_admin_roles 278 | on_future = true 279 | } 280 | 281 | resource "snowflake_sequence_grant" "admin" { 282 | for_each = toset(local.admin_privileges["sequence"]) 283 | 284 | database_name = snowflake_database.app.name 285 | schema_name = local.public_schema_name 286 | privilege = each.key 287 | roles = local.all_admin_roles 288 | on_future = true 289 | } 290 | 291 | resource "snowflake_stage_grant" "admin" { 292 | for_each = toset(local.admin_privileges["stage"]) 293 | 294 | database_name = snowflake_database.app.name 295 | schema_name = local.public_schema_name 296 | privilege = each.key 297 | roles = local.all_admin_roles 298 | on_future = true 299 | } 300 | 301 | resource "snowflake_stream_grant" "admin" { 302 | for_each = toset(local.admin_privileges["stream"]) 303 | 304 | database_name = snowflake_database.app.name 305 | schema_name = local.public_schema_name 306 | privilege = each.key 307 | roles = local.all_admin_roles 308 | on_future = true 309 | } --------------------------------------------------------------------------------