Agent

Task 5: Create terragrunt.hcl for tools-database in cpi-new

Prompt:
Create the file `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/tools-database/terragrunt.hcl` with this exact content:

```hcl
# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

generate "backup_provider" {
  path      = "backup-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider aws {
  alias   = "backup"
  profile = "${local.accounts["platform_management"]["profile"]}"
  region  = "us-east-2"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  gs_cidr_blocks = local.common_vars.locals.global_segment_cidrs

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/tools-database"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency vpc {
  config_path = "../vpc-cloudwan"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  name       = "tools-database"
  pg_version = "17.4"
  env        = "prod"

  private_subnet_ids = [for subnet in dependency.vpc.outputs.vpc.private_subnet_map : subnet.id]
  vpc_id             = dependency.vpc.outputs.vpc.vpc_id

  subnet_group_name_override       = "tools-db"
  dba_security_group_name_override = "tools-db-access"
  security_group_name_override     = "tools-db-trusted-access"

  rds_instance_size            = "db.t4g.small"
  enhanced_monitoring_enabled  = false
  performance_insights_enabled = false
  multi_az                     = false

  allocated_storage = 20
  storage_encrypted = true

  backup_retention_period  = 5
  replica_retention_period = 5
  final_snapshot           = false
  encrypt_replicas         = true

  vault_secret_path = "platform/tools-database"

  trusted_cidr_blocks = [
    local.gs_cidr_blocks["management"]["mgmt"]
  ]

  tags = local.tags
}
```

Do NOT run any git commands. Just write the file.

After writing, verify it exists with the correct content by reading it back.

End with `STATUS: DONE` or `STATUS: BLOCKED`.


Set up RDS Postgres database for churn-renewal-forecast


Perfect. The file has been created successfully at `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/tools-database/terragrunt.hcl` with all the exact content you specified. The verification confirms that all lines match the provided configuration, including:

- Terraform source configuration
- Include directive
- Backup provider generation
- Locals section with variable loading and tags
- VPC dependency
- Input variables for the RDS database module

STATUS: DONE

agentId: a8adf25a519902aa4 (use SendMessage with to: 'a8adf25a519902aa4' to continue this agent)
<usage>total_tokens: 31238
tool_uses: 2
duration_ms: 9903</usage>

Prompt:
Create the file `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/tools-database/terragrunt.hcl` with this exact content:

```hcl
# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

generate "backup_provider" {
  path      = "backup-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider aws {
  alias   = "backup"
  profile = "${local.accounts["platform_management"]["profile"]}"
  region  = "us-east-2"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  gs_cidr_blocks = local.common_vars.locals.global_segment_cidrs

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/tools-database"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency vpc {
  config_path = "../vpc-cloudwan"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  name       = "tools-database"
  pg_version = "17.4"
  env        = "prod"

  private_subnet_ids = [for subnet in dependency.vpc.outputs.vpc.private_subnet_map : subnet.id]
  vpc_id             = dependency.vpc.outputs.vpc.vpc_id

  subnet_group_name_override       = "tools-db"
  dba_security_group_name_override = "tools-db-access"
  security_group_name_override     = "tools-db-trusted-access"

  rds_instance_size            = "db.t4g.small"
  enhanced_monitoring_enabled  = false
  performance_insights_enabled = false
  multi_az                     = false

  allocated_storage = 20
  storage_encrypted = true

  backup_retention_period  = 5
  replica_retention_period = 5
  final_snapshot           = false
  encrypt_replicas         = true

  vault_secret_path = "platform/tools-database"

  trusted_cidr_blocks = [
    local.gs_cidr_blocks["management"]["mgmt"]
  ]

  tags = local.tags
}
```

Do NOT run any git commands. Just write the file.

After writing, verify it exists with the correct content by reading it back.

End with `STATUS: DONE` or `STATUS: BLOCKED`.


Spec + quality review: terragrunt.hcl

Prompt:
Read and review `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/tools-database/terragrunt.hcl`.

Also read the existing reference for comparison: `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/retool-database/terragrunt.hcl`

**Spec compliance** — verify all of the following:
1. `terraform.source` = `"tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0"`
2. `include` block with `find_in_parent_folders()`
3. `generate "backup_provider"` block generating `backup-provider.tf` with `aws.backup` alias pointing to `us-east-2`
4. `locals` loading `common_vars`, `account_vars`, `region_vars`; extracting `aws_region`, `env`, `accounts`, `gs_cidr_blocks`; `tags` with `tfpath = "/platform-management/${local.aws_region}/tools-database"`
5. `dependency vpc` with `config_path = "../vpc-cloudwan"`
6. `inputs` containing: `name = "tools-database"`, `pg_version = "17.4"`, `env = "prod"`, `private_subnet_ids` from vpc dependency, `vpc_id` from vpc dependency, security group name overrides (`tools-db`, `tools-db-access`, `tools-db-trusted-access`), `rds_instance_size = "db.t4g.small"`, `enhanced_monitoring_enabled = false`, `performance_insights_enabled = false`, `multi_az = false`, `allocated_storage = 20`, `storage_encrypted = true`, `backup_retention_period = 5`, `replica_retention_period = 5`, `final_snapshot = false`, `encrypt_replicas = true`, `vault_secret_path = "platform/tools-database"`, `trusted_cidr_blocks` using `gs_cidr_blocks["management"]["mgmt"]`, `tags = local.tags`

**Quality** — consistent style with the reference `retool-database/terragrunt.hcl`.

End with `REVIEW: ✅ APPROVED` or `REVIEW: ❌ ISSUES` with details.


Now let me verify each specification item:

**Spec Compliance Review:**

1. ✅ **terraform.source** — Line 6: `tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0` — CORRECT

2. ✅ **include block** — Lines 10-12: Include with `find_in_parent_folders()` — CORRECT

3. ✅ **generate "backup_provider" block** — Lines 14-24: Generates `backup-provider.tf` with `aws.backup` alias pointing to `us-east-2` — CORRECT

4. ✅ **locals section** — Lines 29-51:
   - `common_vars` loaded ✅
   - `account_vars` loaded ✅
   - `region_vars` loaded ✅
   - `aws_region` extracted ✅
   - `env` extracted ✅
   - `accounts` extracted ✅
   - `gs_cidr_blocks` extracted ✅
   - `tags` with correct `tfpath` ✅

5. ✅ **dependency vpc** — Lines 57-59: `config_path = "../vpc-cloudwan"` — CORRECT

6. ✅ **inputs block** — Lines 65-97:
   - `name = "tools-database"` ✅
   - `pg_version = "17.4"` ✅
   - `env = "prod"` ✅
   - `private_subnet_ids` from vpc dependency ✅
   - `vpc_id` from vpc dependency ✅
   - Security group name overrides: `tools-db`, `tools-db-access`, `tools-db-trusted-access` ✅
   - `rds_instance_size = "db.t4g.small"` ✅
   - `enhanced_monitoring_enabled = false` ✅
   - `performance_insights_enabled = false` ✅
   - `multi_az = false` ✅
   - `allocated_storage = 20` ✅
   - `storage_encrypted = true` ✅
   - `backup_retention_period = 5` ✅
   - `replica_retention_period = 5` ✅
   - `final_snapshot = false` ✅
   - `encrypt_replicas = true` ✅
   - `vault_secret_path = "platform/tools-database"` ✅
   - `trusted_cidr_blocks` using `gs_cidr_blocks["management"]["mgmt"]` ✅
   - `tags = local.tags` ✅

**Quality & Style:**
- Header comments match retool-database style ✅
- Consistent indentation and formatting ✅
- All required sections present ✅
- Clean, readable structure ✅

REVIEW: ✅ APPROVED

The `tools-database/terragrunt.hcl` file fully complies with all specifications. It follows the established pattern from `retool-database/terragrunt.hcl` with appropriate differences (tools-db vs retool-db naming, different PostgreSQL module source, and correct input parameter mappings). No issues detected.

agentId: ab9a5aab4bf3abda1 (use SendMessage with to: 'ab9a5aab4bf3abda1' to continue this agent)
<usage>total_tokens: 31690
tool_uses: 2
duration_ms: 41057</usage>

Prompt:
Read and review `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/tools-database/terragrunt.hcl`.

Also read the existing reference for comparison: `/Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/retool-database/terragrunt.hcl`

**Spec compliance** — verify all of the following:
1. `terraform.source` = `"tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0"`
2. `include` block with `find_in_parent_folders()`
3. `generate "backup_provider"` block generating `backup-provider.tf` with `aws.backup` alias pointing to `us-east-2`
4. `locals` loading `common_vars`, `account_vars`, `region_vars`; extracting `aws_region`, `env`, `accounts`, `gs_cidr_blocks`; `tags` with `tfpath = "/platform-management/${local.aws_region}/tools-database"`
5. `dependency vpc` with `config_path = "../vpc-cloudwan"`
6. `inputs` containing: `name = "tools-database"`, `pg_version = "17.4"`, `env = "prod"`, `private_subnet_ids` from vpc dependency, `vpc_id` from vpc dependency, security group name overrides (`tools-db`, `tools-db-access`, `tools-db-trusted-access`), `rds_instance_size = "db.t4g.small"`, `enhanced_monitoring_enabled = false`, `performance_insights_enabled = false`, `multi_az = false`, `allocated_storage = 20`, `storage_encrypted = true`, `backup_retention_period = 5`, `replica_retention_period = 5`, `final_snapshot = false`, `encrypt_replicas = true`, `vault_secret_path = "platform/tools-database"`, `trusted_cidr_blocks` using `gs_cidr_blocks["management"]["mgmt"]`, `tags = local.tags`

**Quality** — consistent style with the reference `retool-database/terragrunt.hcl`.

End with `REVIEW: ✅ APPROVED` or `REVIEW: ❌ ISSUES` with details.


Updated task #6 status


All 5 tasks done, Sir Charles. Here's the full rundown of what got written:

**`operations` repo** — `terraform/iac-library/aws/storage/database-tools-db/`:
- `main.tf` — KMS + `data-database-rds` v3.0.1 + `vault_generic_secret`, with `null` KMS fallbacks and `parameters` passthrough (bug caught in review and fixed)
- `variables.tf` — all required + optional vars including `parameters` and `vault_secret_path`
- `output.tf` — full passthrough outputs, `admin_password` marked `sensitive = true`, KMS splat outputs, `vault_secret_path`
- `VERSION` — `1.0.0`
- `README.md` — usage docs

**`cpi-new` repo** — `platform-management/us-east-1/tools-database/terragrunt.hcl`:
- Source `tfr://crow.infrastructure.immuta.io/iac/storage-database-tools-db/aws?version=1.0.0`
- Postgres 17.4, `db.t4g.small`, no multi-AZ, vault at `platform/tools-database`
- Depends on `vpc-cloudwan`, backup provider alias to `us-east-2`

Once the `database-tools-db` module is published to `crow.infrastructure.immuta.io` at v1.0.0, `terragrunt plan` in `tools-database/` should produce the full stack. Bling bling! 🦕


afplay ~/.claude/sounds/mario/stop/stomp.wav 2>/dev/null || true


ok, one more ask. I need to create a terragrunt module that will create database users for churn-renewal-forecast along with it's database and schema, (usernames churn_forecast_admin - owns the database/schema has full privileges in the database, churn_forecast is a normal user that can delete, insert, update, but not alter) they should have randomly generated passwords that get stored in vault under secrets/internal/churn-rewnewal-forecast-db, the database and schema both should be named churn_forecast and the public schema should also be able to be used in churn_forecast db, we already have an internal module for this at @/Users/steve.briggs/projects/operations/terraform/internal-modules/database/postgres-create-users


outputs.tf
main.tf
README.md
VERSION
variables.tf


Let me read the `postgres-create-users` module before anything else.


1	output "credentials" {
2	  description = "Map of created database users and their credentials."
3	  sensitive   = true
4
5	  value = {
6	    for user, _ in var.users : user => random_password.db_password[user].result
7	  }
8	}
9

output "credentials" {
  description = "Map of created database users and their credentials."
  sensitive   = true

  value = {
    for user, _ in var.users : user => random_password.db_password[user].result
  }
}


ok, one more ask. I need to create a terragrunt module that will create database users for churn-renewal-forecast along with it's database and schema, (usernames churn_forecast_admin - owns the databa…


1	/**
2	 * # Postgres Create Users
3	 * ## Module Name: postgres-create-users
4	 * ## This module is to be used to create multiple (or single) users in a postgres db WITHOUT any grants or roles attached
5	 */
6
7	# ---------------------------------------------------------------------------------------------------------------------
8	# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
9	# ---------------------------------------------------------------------------------------------------------------------
10
11	terraform {
12	  required_version = "~> 1.0"
13
14	  required_providers {
15	    postgresql = {
16	      source  = "cyrilgdn/postgresql"
17	      version = "~> 1.0"
18	    }
19	    random = {
20	      version = "~> 3.0"
21	    }
22	    vault = {
23	      version = "~> 5.0"
24	    }
25	  }
26	}
27
28	# ---------------------------------------------------------------------------------------------------------------------
29	# CREATE A TERRAFORM RESOURCE
30	# ---------------------------------------------------------------------------------------------------------------------
31	resource "postgresql_role" "database_user" {
32	  for_each = var.users
33
34	  name        = each.key
35	  password    = random_password.db_password[each.key].result
36	  login       = true
37	  create_role = false
38
39	  lifecycle {
40	    ignore_changes = [
41	      roles
42	    ]
43	  }
44
45	  depends_on = [random_password.db_password]
46	}
47
48	resource "random_password" "db_password" {
49	  for_each = var.users
50
51	  length      = var.password_settings.length
52	  lower       = var.password_settings.lowercase
53	  min_lower   = var.password_settings.min_lower
54	  min_numeric = var.password_settings.min_numeric
55	  min_special = var.password_settings.min_special
56	  min_upper   = var.password_settings.min_upper
57	  numeric          = var.password_settings.numeric
58	  override_special = var.password_settings.override_special
59	  special          = var.password_settings.special
60	  upper            = var.password_settings.upper
61
62	  lifecycle {
63	    ignore_changes = [
64	      length,
65	      lower,
66	      min_lower,
67	      min_numeric,
68	      min_special,
69	      min_upper,
70	      override_special,
71	      special,
72	      upper
73	    ]
74	  }
75	}
76
77	# ---------------------------------------------------------------------------------------------------------------------
78	# STORE DB CREDENTIALS IN VAULT
79	# ---------------------------------------------------------------------------------------------------------------------
80	resource "vault_kv_secret_v2" "db_users" {
81	  depends_on = [postgresql_role.database_user]
82
83	  for_each = {
84	    for user, val in var.users : user => val
85	    if val.vault
86	  }
87
88	  mount               = var.vault_mount
89	  name                = each.value.vault_path
90	  cas                 = 1
91	  delete_all_versions = false
92	  data_json = jsonencode(
93	    {
94	      user = each.key
95	      pass = random_password.db_password[each.key].result
96	    }
97	  )
98	  custom_metadata {
99	    data = {
100	      tags = jsonencode(var.tags)
101	    }
102	  }
103	}
104
105

/**
 * # Postgres Create Users
 * ## Module Name: postgres-create-users
 * ## This module is to be used to create multiple (or single) users in a postgres db WITHOUT any grants or roles attached
 */

# ---------------------------------------------------------------------------------------------------------------------
# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  required_version = "~> 1.0"

  required_providers {
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "~> 1.0"
    }
    random = {
      version = "~> 3.0"
    }
    vault = {
      version = "~> 5.0"
    }
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# CREATE A TERRAFORM RESOURCE
# ---------------------------------------------------------------------------------------------------------------------
resource "postgresql_role" "database_user" {
  for_each = var.users

  name        = each.key
  password    = random_password.db_password[each.key].result
  login       = true
  create_role = false

  lifecycle {
    ignore_changes = [
      roles
    ]
  }

  depends_on = [random_password.db_password]
}

resource "random_password" "db_password" {
  for_each = var.users

  length      = var.password_settings.length
  lower       = var.password_settings.lowercase
  min_lower   = var.password_settings.min_lower
  min_numeric = var.password_settings.min_numeric
  min_special = var.password_settings.min_special
  min_upper   = var.password_settings.min_upper
  numeric          = var.password_settings.numeric
  override_special = var.password_settings.override_special
  special          = var.password_settings.special
  upper            = var.password_settings.upper

  lifecycle {
    ignore_changes = [
      length,
      lower,
      min_lower,
      min_numeric,
      min_special,
      min_upper,
      override_special,
      special,
      upper
    ]
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# STORE DB CREDENTIALS IN VAULT
# ---------------------------------------------------------------------------------------------------------------------
resource "vault_kv_secret_v2" "db_users" {
  depends_on = [postgresql_role.database_user]

  for_each = {
    for user, val in var.users : user => val
    if val.vault
  }

  mount               = var.vault_mount
  name                = each.value.vault_path
  cas                 = 1
  delete_all_versions = false
  data_json = jsonencode(
    {
      user = each.key
      pass = random_password.db_password[each.key].result
    }
  )
  custom_metadata {
    data = {
      tags = jsonencode(var.tags)
    }
  }
}


1	# ---------------------------------------------------------------------------------------------------------------------
2	# REQUIRED PARAMETERS
3	# These variables must be set.
4	# ---------------------------------------------------------------------------------------------------------------------
5
6	variable "users" {
7	  description = "Map of default users (with random passwords to create) WITHOUT GRANTS/ROLES"
8	  type = map(object({
9	    vault      = bool
10	    vault_path = string
11	  }))
12	}
13
14	# ---------------------------------------------------------------------------------------------------------------------
15	# OPTIONAL PARAMETERS
16	# These variables are configurable, but have reasonable defaults set.
17	# ---------------------------------------------------------------------------------------------------------------------
18
19	variable "vault_mount" {
20	  description = "which vault mount path to use"
21	  type        = string
22	  default     = "secret"
23	}
24
25	variable "password_settings" {
26	  description = "Password settings to use when generating random passwords for ALL users created by this module"
27	  type = object({
28	    length           = number
29	    lowercase        = bool
30	    min_lower        = number
31	    min_numeric      = number
32	    min_special      = number
33	    min_upper        = number
34	    numeric          = bool
35	    override_special = string
36	    special          = bool
37	    upper            = bool
38	  })
39
40	  default = ({
41	    length           = 64
42	    lowercase        = true
43	    min_lower        = 4
44	    min_numeric      = 4
45	    min_special      = 4
46	    min_upper        = 4
47	    numeric          = true
48	    override_special = "!&#$^-"
49	    special          = true
50	    upper            = true
51	  })
52	}
53
54	variable "tags" {
55	  description = "Map of key-value pairs to tag created resources with."
56	  type        = map(any)
57	  default     = {}
58	}
59
60

# ---------------------------------------------------------------------------------------------------------------------
# REQUIRED PARAMETERS
# These variables must be set.
# ---------------------------------------------------------------------------------------------------------------------

variable "users" {
  description = "Map of default users (with random passwords to create) WITHOUT GRANTS/ROLES"
  type = map(object({
    vault      = bool
    vault_path = string
  }))
}

# ---------------------------------------------------------------------------------------------------------------------
# OPTIONAL PARAMETERS
# These variables are configurable, but have reasonable defaults set.
# ---------------------------------------------------------------------------------------------------------------------

variable "vault_mount" {
  description = "which vault mount path to use"
  type        = string
  default     = "secret"
}

variable "password_settings" {
  description = "Password settings to use when generating random passwords for ALL users created by this module"
  type = object({
    length           = number
    lowercase        = bool
    min_lower        = number
    min_numeric      = number
    min_special      = number
    min_upper        = number
    numeric          = bool
    override_special = string
    special          = bool
    upper            = bool
  })

  default = ({
    length           = 64
    lowercase        = true
    min_lower        = 4
    min_numeric      = 4
    min_special      = 4
    min_upper        = 4
    numeric          = true
    override_special = "!&#$^-"
    special          = true
    upper            = true
  })
}

variable "tags" {
  description = "Map of key-value pairs to tag created resources with."
  type        = map(any)
  default     = {}
}


Let me also check the `postgres-db-users` module since it might handle the grants side.


1	# ---------------------------------------------------------------------------------------------------------------------
2	# MODULE PARAMETERS
3	# These variables are expected to be passed in by the operator when calling this terraform module.
4	# ---------------------------------------------------------------------------------------------------------------------
5
6
7	# ---------------------------------------------------------------------------------------------------------------------
8	# REQUIRED PARAMETERS
9	# These variables must be set.
10	# ---------------------------------------------------------------------------------------------------------------------
11
12	variable "database_name" {
13	  description = "Name of the database to be created, will also be used as the prefix for user creation unless the user name override variables are set."
14	  type        = string
15	}
16
17	variable "vault_path" {
18	  description = "The vault path that the db_user secrets will be created in"
19	  type        = string
20	}
21
22	# ---------------------------------------------------------------------------------------------------------------------
23	# OPTIONAL PARAMETERS
24	# These variables are configurable, but have reasonable defaults set.
25	# ---------------------------------------------------------------------------------------------------------------------
26	variable "create_database" {
27	  description = "Create the database"
28	  type        = bool
29	  default     = true
30	}
31
32	variable "schema_names" {
33	  description = "Schema to add and create where to add user privileges"
34	  type        = list(string)
35	  default     = ["public"]
36	}
37
38	variable "password_settings" {
39	  description = "Password settings to use when generating random passwords for ALL users created by this module"
40	  type = object({
41	    length           = number
42	    lowercase        = bool
43	    min_lower        = number
44	    min_numeric      = number
45	    min_special      = number
46	    min_upper        = number
47	    numeric          = bool
48	    override_special = string
49	    special          = bool
50	    upper            = bool
51	  })
52
53	  default = ({
54	    length           = 64
55	    lowercase        = true
56	    min_lower        = 4
57	    min_numeric      = 4
58	    min_special      = 4
59	    min_upper        = 4
60	    numeric          = true
61	    override_special = "-_"
62	    special          = true
63	    upper            = true
64	  })
65	}
66
67	variable "admin_name_override" {
68	  description = "Admin name to use when creating the database (will be set as the database owner) instead of using the user_name_prefix"
69	  type        = string
70	  default     = ""
71	}
72
73	variable "user_name_override" {
74	  description = "User name to use when creating the database instead of the user_name_prefix"
75	  type        = string
76	  default     = ""
77	}
78
79	variable "admin_conn_limit" {
80	  description = "Connection limit for the admin role"
81	  type        = number
82	  default     = -1
83	}
84
85	variable "user_conn_limit" {
86	  description = "Connection limit for the user role"
87	  type        = number
88	  default     = -1
89	}
90
91	variable "admin_table_privs" {
92	  description = "Optional list of admin table privileges to assign to the admin user if it is being created"
93	  type        = list(string)
94	  default     = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"]
95	}
96
97	variable "admin_database_privs" {
98	  description = "Optional list of admin database privileges to assign to the admin user if it is being created"
99	  type        = list(string)
100	  default     = ["CREATE"]
101	}
102
103	variable "admin_exec_privs" {
104	  description = "Optional list of admin function/procdure privileges to assign to the admin user if it is being created"
105	  type        = list(string)
106	  default     = ["EXECUTE"]
107	}
108
109	variable "admin_create_role" {
110	  description = "Bool determing if the admin user should be granted CREATEROLE or not (default to false)"
111	  type = bool
112	  default = false
113	}
114
115	variable "user_privs" {
116	  description = "A list of string representing the privileges to give the user to the created database (SELECT, INSERT, etc..)"
117	  type        = list(string)
118	  default     = ["SELECT", "INSERT", "UPDATE", "DELETE"]
119	}
120
121	variable "db_encoding" {
122	  description = "Encoding to set at the database level"
123	  type        = string
124	  default     = "UTF8"
125	}
126
127	variable "vault_mount" {
128	  description = "mount point for vault engine"
129	  type        = string
130	  default     = "secret"
131	}
132
133	variable "create_vault_secret" {
134	  description = "toggle to create the vault secret"
135	  type        = bool
136	  default     = true
137	}
138
139	variable "admin_granted_roles" {
140	  description = "List of roles granted to the admin user"
141	  type        = list(string)
142	  default     = []
143	}
144
145	variable "user_granted_roles" {
146	  description = "List of roles granted to the standard user"
147	  type        = list(string)
148	  default     = []
149	}
150
151	variable "tags" {
152	  description = "Map of key-value pairs to tag created resources with."
153	  type        = map(any)
154	  default     = {}
155	}
156

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These variables are expected to be passed in by the operator when calling this terraform module.
# ---------------------------------------------------------------------------------------------------------------------

# ---------------------------------------------------------------------------------------------------------------------
# REQUIRED PARAMETERS
# These variables must be set.
# ---------------------------------------------------------------------------------------------------------------------

variable "database_name" {
  description = "Name of the database to be created, will also be used as the prefix for user creation unless the user name override variables are set."
  type        = string
}

variable "vault_path" {
  description = "The vault path that the db_user secrets will be created in"
  type        = string
}

# ---------------------------------------------------------------------------------------------------------------------
# OPTIONAL PARAMETERS
# These variables are configurable, but have reasonable defaults set.
# ---------------------------------------------------------------------------------------------------------------------
variable "create_database" {
  description = "Create the database"
  type        = bool
  default     = true
}

variable "schema_names" {
  description = "Schema to add and create where to add user privileges"
  type        = list(string)
  default     = ["public"]
}

variable "password_settings" {
  description = "Password settings to use when generating random passwords for ALL users created by this module"
  type = object({
    length           = number
    lowercase        = bool
    min_lower        = number
    min_numeric      = number
    min_special      = number
    min_upper        = number
    numeric          = bool
    override_special = string
    special          = bool
    upper            = bool
  })

  default = ({
    length           = 64
    lowercase        = true
    min_lower        = 4
    min_numeric      = 4
    min_special      = 4
    min_upper        = 4
    numeric          = true
    override_special = "-_"
    special          = true
    upper            = true
  })
}

variable "admin_name_override" {
  description = "Admin name to use when creating the database (will be set as the database owner) instead of using the user_name_prefix"
  type        = string
  default     = ""
}

variable "user_name_override" {
  description = "User name to use when creating the database instead of the user_name_prefix"
  type        = string
  default     = ""
}

variable "admin_conn_limit" {
  description = "Connection limit for the admin role"
  type        = number
  default     = -1
}

variable "user_conn_limit" {
  description = "Connection limit for the user role"
  type        = number
  default     = -1
}

variable "admin_table_privs" {
  description = "Optional list of admin table privileges to assign to the admin user if it is being created"
  type        = list(string)
  default     = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"]
}

variable "admin_database_privs" {
  description = "Optional list of admin database privileges to assign to the admin user if it is being created"
  type        = list(string)
  default     = ["CREATE"]
}

variable "admin_exec_privs" {
  description = "Optional list of admin function/procdure privileges to assign to the admin user if it is being created"
  type        = list(string)
  default     = ["EXECUTE"]
}

variable "admin_create_role" {
  description = "Bool determing if the admin user should be granted CREATEROLE or not (default to false)"
  type = bool
  default = false
}

variable "user_privs" {
  description = "A list of string representing the privileges to give the user to the created database (SELECT, INSERT, etc..)"
  type        = list(string)
  default     = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

variable "db_encoding" {
  description = "Encoding to set at the database level"
  type        = string
  default     = "UTF8"
}

variable "vault_mount" {
  description = "mount point for vault engine"
  type        = string
  default     = "secret"
}

variable "create_vault_secret" {
  description = "toggle to create the vault secret"
  type        = bool
  default     = true
}

variable "admin_granted_roles" {
  description = "List of roles granted to the admin user"
  type        = list(string)
  default     = []
}

variable "user_granted_roles" {
  description = "List of roles granted to the standard user"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Map of key-value pairs to tag created resources with."
  type        = map(any)
  default     = {}
}


1	/*
2	 * # Postgres DB & Users
3	 * ## Module Name: postgres-db-users
4	 * ## This module is to be used to create a single database and an admin and user role associated with said database
5	 * ## in a pre-existing RDS/postgresql instance
6	 */
7
8	# ---------------------------------------------------------------------------------------------------------------------
9	# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
10	# ---------------------------------------------------------------------------------------------------------------------
11
12	terraform {
13	  required_version = "~> 1.0"
14
15	  required_providers {
16	    postgresql = {
17	      source  = "cyrilgdn/postgresql"
18	      version = "~> 1.21"
19	    }
20	    random = {
21	      version = "~> 3.6.0"
22	    }
23	    vault = {
24	      version = "~> 4.0"
25	    }
26	  }
27	}
28
29	locals {
30	  admin_user    = var.admin_name_override != "" ? var.admin_name_override : "${var.database_name}_admin"
31	  database_user = var.user_name_override != "" ? var.user_name_override : var.database_name
32	}
33
34	# ---------------------------------------------------------------------------------------------------------------------
35	# CREATE DATABASE
36	# ---------------------------------------------------------------------------------------------------------------------
37	resource "postgresql_database" "database" {
38	  count = var.create_database ? 1 : 0
39
40	  name       = var.database_name
41	  owner      = postgresql_role.admin_user.name
42	  encoding   = var.db_encoding
43	  lc_collate = "C"
44	  lc_ctype   = "C"
45
46	  depends_on = [postgresql_role.admin_user]
47	}
48
49	resource "postgresql_schema" "schema" {
50	  for_each = toset(var.schema_names)
51
52	  name     = each.key
53	  owner    = postgresql_role.admin_user.name
54	  database = var.database_name
55
56	  if_not_exists = true
57
58	  depends_on = [postgresql_role.admin_user, postgresql_database.database]
59	}
60
61	# ---------------------------------------------------------------------------------------------------------------------
62	# CREATE ADMIN USER
63	# ---------------------------------------------------------------------------------------------------------------------
64	resource "postgresql_role" "admin_user" {
65	  name        = local.admin_user
66	  password    = random_password.db_password[local.admin_user].result
67	  login       = true
68	  create_role = var.admin_create_role
69	  roles       = length(var.admin_granted_roles) > 0 ? var.admin_granted_roles : null
70
71	  connection_limit = var.admin_conn_limit
72
73	  depends_on = [random_password.db_password]
74	}
75
76	resource "postgresql_grant" "admin_user_database" {
77	  database    = var.database_name
78	  role        = postgresql_role.admin_user.name
79	  object_type = "database"
80	  privileges  = concat(var.admin_database_privs)
81
82	  depends_on = [postgresql_role.admin_user, postgresql_database.database]
83	}
84
85	resource "postgresql_grant" "admin_user_table" {
86	  for_each = postgresql_schema.schema
87
88	  database    = var.database_name
89	  role        = postgresql_role.admin_user.name
90	  schema      = each.key
91	  object_type = "table"
92	  privileges  = concat(var.admin_table_privs)
93
94	  depends_on = [
95	    postgresql_role.admin_user,
96	    postgresql_database.database,
97	    postgresql_schema.schema
98	  ]
99	}
100
101	resource "postgresql_grant" "admin_user_function" {
102	  for_each = postgresql_schema.schema
103
104	  database    = var.database_name
105	  role        = postgresql_role.admin_user.name
106	  schema      = each.key
107	  object_type = "function"
108	  privileges  = concat(var.admin_exec_privs)
109
110	  depends_on = [
111	    postgresql_role.admin_user,
112	    postgresql_database.database,
113	    postgresql_schema.schema
114	  ]
115	}
116
117	resource "postgresql_grant" "admin_user_procedure" {
118	  for_each = postgresql_schema.schema
119
120	  database    = var.database_name
121	  role        = postgresql_role.admin_user.name
122	  schema      = each.key
123	  object_type = "procedure"
124	  privileges  = concat(var.admin_exec_privs)
125
126	  depends_on = [
127	    postgresql_role.admin_user,
128	    postgresql_database.database,
129	    postgresql_schema.schema
130	  ]
131	}
132
133	# ---------------------------------------------------------------------------------------------------------------------
134	# CREATE APPLICATION USER
135	# ---------------------------------------------------------------------------------------------------------------------
136	resource "postgresql_role" "database_user" {
137	  name        = local.database_user
138	  password    = random_password.db_password[local.database_user].result
139	  login       = true
140	  create_role = false
141	  roles       = length(var.user_granted_roles) > 0 ? var.user_granted_roles : null
142
143	  connection_limit = var.user_conn_limit
144
145	  depends_on = [random_password.db_password]
146	}
147
148	resource "postgresql_grant" "database_user_schema" {
149	  for_each = postgresql_schema.schema
150
151	  database = var.database_name
152	  role = postgresql_role.database_user.name
153	  schema = each.key
154	  object_type = "schema"
155	  privileges = ["USAGE"]
156
157	  depends_on = [
158	    postgresql_role.database_user,
159	    postgresql_database.database,
160	    postgresql_schema.schema
161	  ]
162	}
163
164	resource "postgresql_grant" "database_user" {
165	  for_each = postgresql_schema.schema
166
167	  database    = var.database_name
168	  role        = postgresql_role.database_user.name
169	  schema      = each.key
170	  object_type = "table"
171	  privileges  = concat(var.user_privs)
172
173	  depends_on = [
174	    postgresql_role.database_user,
175	    postgresql_database.database,
176	    postgresql_schema.schema
177	  ]
178	}
179
180	resource "postgresql_grant" "database_user_function" {
181	  for_each = postgresql_schema.schema
182
183	  database    = var.database_name
184	  role        = postgresql_role.database_user.name
185	  schema      = each.key
186	  object_type = "function"
187	  privileges  = ["EXECUTE"]
188
189	  depends_on = [
190	    postgresql_role.database_user,
191	    postgresql_database.database,
192	    postgresql_schema.schema
193	  ]
194	}
195
196	resource "postgresql_grant" "database_user_sequence" {
197	  for_each = postgresql_schema.schema
198
199	  database    = var.database_name
200	  role        = postgresql_role.database_user.name
201	  schema      = each.key
202	  object_type = "sequence"
203	  privileges  = ["USAGE"]
204
205	  depends_on = [
206	    postgresql_role.database_user,
207	    postgresql_database.database,
208	    postgresql_schema.schema
209	  ]
210	}
211
212	resource "postgresql_default_privileges" "database_user_table" {
213	  for_each = postgresql_schema.schema
214
215	  database = var.database_name
216	  role     = postgresql_role.database_user.name
217	  schema   = each.key
218
219	  owner       = postgresql_role.admin_user.name
220	  object_type = "table"
221	  privileges  = concat(var.user_privs)
222	  depends_on = [
223	    postgresql_database.database,
224	    postgresql_role.admin_user,
225	    postgresql_role.database_user,
226	    postgresql_schema.schema
227	  ]
228	}
229
230	resource "postgresql_default_privileges" "database_user_function" {
231	  for_each = postgresql_schema.schema
232
233	  database = var.database_name
234	  role     = postgresql_role.database_user.name
235	  schema   = each.key
236
237	  owner       = postgresql_role.admin_user.name
238	  object_type = "function"
239	  privileges  = ["EXECUTE"]
240	  depends_on = [
241	    postgresql_database.database,
242	    postgresql_role.admin_user,
243	    postgresql_role.database_user,
244	    postgresql_schema.schema
245	  ]
246	}
247
248	resource "postgresql_default_privileges" "database_user_sequence" {
249	  for_each = postgresql_schema.schema
250
251	  database = var.database_name
252	  role     = postgresql_role.database_user.name
253	  schema   = each.key
254
255	  owner       = postgresql_role.admin_user.name
256	  object_type = "sequence"
257	  privileges  = ["USAGE"]
258	  depends_on = [
259	    postgresql_database.database,
260	    postgresql_role.admin_user,
261	    postgresql_role.database_user,
262	    postgresql_schema.schema
263	  ]
264	}
265
266	# ---------------------------------------------------------------------------------------------------------------------
267	# GENERATE PASSWORDS
268	# ---------------------------------------------------------------------------------------------------------------------
269	resource "random_password" "db_password" {
270	  for_each = toset([local.admin_user, local.database_user])
271
272	  length      = var.password_settings.length
273	  lower       = var.password_settings.lowercase
274	  min_lower   = var.password_settings.min_lower
275	  min_numeric = var.password_settings.min_numeric
276	  min_special = var.password_settings.min_special
277	  min_upper   = var.password_settings.min_upper
278	  numeric          = var.password_settings.numeric
279	  override_special = var.password_settings.override_special
280	  special          = var.password_settings.special
281	  upper            = var.password_settings.upper
282
283	  lifecycle {
284	    ignore_changes = [
285	      length,
286	      lower,
287	      min_lower,
288	      min_numeric,
289	      min_special,
290	      min_upper,
291	      numeric,
292	      override_special,
293	      special,
294	      upper
295	    ]
296	  }
297	}
298
299	# ---------------------------------------------------------------------------------------------------------------------
300	# STORE DB CREDENTIALS IN VAULT
301	# ---------------------------------------------------------------------------------------------------------------------
302	resource "vault_kv_secret_v2" "db_users" {
303	  for_each = var.create_vault_secret ? random_password.db_password : {}
304
305	  mount               = var.vault_mount
306	  name                = "${var.vault_path}/${each.key}"
307	  cas                 = 1
308	  delete_all_versions = false
309	  data_json = jsonencode(
310	    {
311	      user = each.key
312	      pass = random_password.db_password[each.key].result
313	    }
314	  )
315	  custom_metadata {
316	    data = {
317	      tags = jsonencode(var.tags)
318	    }
319	  }
320	}
321
322

/*
 * # Postgres DB & Users
 * ## Module Name: postgres-db-users
 * ## This module is to be used to create a single database and an admin and user role associated with said database
 * ## in a pre-existing RDS/postgresql instance
 */

# ---------------------------------------------------------------------------------------------------------------------
# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  required_version = "~> 1.0"

  required_providers {
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "~> 1.21"
    }
    random = {
      version = "~> 3.6.0"
    }
    vault = {
      version = "~> 4.0"
    }
  }
}

locals {
  admin_user    = var.admin_name_override != "" ? var.admin_name_override : "${var.database_name}_admin"
  database_user = var.user_name_override != "" ? var.user_name_override : var.database_name
}

# ---------------------------------------------------------------------------------------------------------------------
# CREATE DATABASE
# ---------------------------------------------------------------------------------------------------------------------
resource "postgresql_database" "database" {
  count = var.create_database ? 1 : 0

  name       = var.database_name
  owner      = postgresql_role.admin_user.name
  encoding   = var.db_encoding
  lc_collate = "C"
  lc_ctype   = "C"

  depends_on = [postgresql_role.admin_user]
}

resource "postgresql_schema" "schema" {
  for_each = toset(var.schema_names)

  name     = each.key
  owner    = postgresql_role.admin_user.name
  database = var.database_name

  if_not_exists = true

  depends_on = [postgresql_role.admin_user, postgresql_database.database]
}

# ---------------------------------------------------------------------------------------------------------------------
# CREATE ADMIN USER
# ---------------------------------------------------------------------------------------------------------------------
resource "postgresql_role" "admin_user" {
  name        = local.admin_user
  password    = random_password.db_password[local.admin_user].result
  login       = true
  create_role = var.admin_create_role
  roles       = length(var.admin_granted_roles) > 0 ? var.admin_granted_roles : null

  connection_limit = var.admin_conn_limit

  depends_on = [random_password.db_password]
}

resource "postgresql_grant" "admin_user_database" {
  database    = var.database_name
  role        = postgresql_role.admin_user.name
  object_type = "database"
  privileges  = concat(var.admin_database_privs)

  depends_on = [postgresql_role.admin_user, postgresql_database.database]
}

resource "postgresql_grant" "admin_user_table" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.admin_user.name
  schema      = each.key
  object_type = "table"
  privileges  = concat(var.admin_table_privs)

  depends_on = [
    postgresql_role.admin_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_grant" "admin_user_function" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.admin_user.name
  schema      = each.key
  object_type = "function"
  privileges  = concat(var.admin_exec_privs)

  depends_on = [
    postgresql_role.admin_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_grant" "admin_user_procedure" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.admin_user.name
  schema      = each.key
  object_type = "procedure"
  privileges  = concat(var.admin_exec_privs)

  depends_on = [
    postgresql_role.admin_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

# ---------------------------------------------------------------------------------------------------------------------
# CREATE APPLICATION USER
# ---------------------------------------------------------------------------------------------------------------------
resource "postgresql_role" "database_user" {
  name        = local.database_user
  password    = random_password.db_password[local.database_user].result
  login       = true
  create_role = false
  roles       = length(var.user_granted_roles) > 0 ? var.user_granted_roles : null

  connection_limit = var.user_conn_limit

  depends_on = [random_password.db_password]
}

resource "postgresql_grant" "database_user_schema" {
  for_each = postgresql_schema.schema

  database = var.database_name
  role = postgresql_role.database_user.name
  schema = each.key
  object_type = "schema"
  privileges = ["USAGE"]

  depends_on = [
    postgresql_role.database_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_grant" "database_user" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.database_user.name
  schema      = each.key
  object_type = "table"
  privileges  = concat(var.user_privs)

  depends_on = [
    postgresql_role.database_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_grant" "database_user_function" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.database_user.name
  schema      = each.key
  object_type = "function"
  privileges  = ["EXECUTE"]

  depends_on = [
    postgresql_role.database_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_grant" "database_user_sequence" {
  for_each = postgresql_schema.schema

  database    = var.database_name
  role        = postgresql_role.database_user.name
  schema      = each.key
  object_type = "sequence"
  privileges  = ["USAGE"]

  depends_on = [
    postgresql_role.database_user,
    postgresql_database.database,
    postgresql_schema.schema
  ]
}

resource "postgresql_default_privileges" "database_user_table" {
  for_each = postgresql_schema.schema

  database = var.database_name
  role     = postgresql_role.database_user.name
  schema   = each.key

  owner       = postgresql_role.admin_user.name
  object_type = "table"
  privileges  = concat(var.user_privs)
  depends_on = [
    postgresql_database.database,
    postgresql_role.admin_user,
    postgresql_role.database_user,
    postgresql_schema.schema
  ]
}

resource "postgresql_default_privileges" "database_user_function" {
  for_each = postgresql_schema.schema

  database = var.database_name
  role     = postgresql_role.database_user.name
  schema   = each.key

  owner       = postgresql_role.admin_user.name
  object_type = "function"
  privileges  = ["EXECUTE"]
  depends_on = [
    postgresql_database.database,
    postgresql_role.admin_user,
    postgresql_role.database_user,
    postgresql_schema.schema
  ]
}

resource "postgresql_default_privileges" "database_user_sequence" {
  for_each = postgresql_schema.schema

  database = var.database_name
  role     = postgresql_role.database_user.name
  schema   = each.key

  owner       = postgresql_role.admin_user.name
  object_type = "sequence"
  privileges  = ["USAGE"]
  depends_on = [
    postgresql_database.database,
    postgresql_role.admin_user,
    postgresql_role.database_user,
    postgresql_schema.schema
  ]
}

# ---------------------------------------------------------------------------------------------------------------------
# GENERATE PASSWORDS
# ---------------------------------------------------------------------------------------------------------------------
resource "random_password" "db_password" {
  for_each = toset([local.admin_user, local.database_user])

  length      = var.password_settings.length
  lower       = var.password_settings.lowercase
  min_lower   = var.password_settings.min_lower
  min_numeric = var.password_settings.min_numeric
  min_special = var.password_settings.min_special
  min_upper   = var.password_settings.min_upper
  numeric          = var.password_settings.numeric
  override_special = var.password_settings.override_special
  special          = var.password_settings.special
  upper            = var.password_settings.upper

  lifecycle {
    ignore_changes = [
      length,
      lower,
      min_lower,
      min_numeric,
      min_special,
      min_upper,
      numeric,
      override_special,
      special,
      upper
    ]
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# STORE DB CREDENTIALS IN VAULT
# ---------------------------------------------------------------------------------------------------------------------
resource "vault_kv_secret_v2" "db_users" {
  for_each = var.create_vault_secret ? random_password.db_password : {}

  mount               = var.vault_mount
  name                = "${var.vault_path}/${each.key}"
  cas                 = 1
  delete_all_versions = false
  data_json = jsonencode(
    {
      user = each.key
      pass = random_password.db_password[each.key].result
    }
  )
  custom_metadata {
    data = {
      tags = jsonencode(var.tags)
    }
  }
}


`postgres-db-users` is exactly what we need. Let me check the existing roles module pattern.


1	# ---------------------------------------------------------------------------------------------------------------------
2	# TERRAGRUNT CONFIGURATION
3	# ---------------------------------------------------------------------------------------------------------------------
4
5	terraform {
6	  source = "tfr://crow.infrastructure.immuta.io/internal/security-database-roles-cps/aws?version=1.1.0"
7	}
8
9	# Include all settings from the root terragrunt.hcl file
10	include {
11	  path = find_in_parent_folders()
12	}
13
14	# To get access to IMC DB
15	# The Terraform required providers block is there because terragrunt is generating this indepenently from
16	# the module, and cannot access that required providers block.
17	generate "rds_postgres_provider" {
18	  path      = "postgres-provider.tf"
19	  if_exists = "overwrite_terragrunt"
20	  contents  = <<EOF
21	provider postgresql {
22	  connect_timeout = 30
23	  database        = "${dependency.database.outputs.name}"
24	  host            = "${dependency.database.outputs.address}"
25	  password        = "${dependency.database.outputs.admin_password}"
26	  port            = "${dependency.database.outputs.port}"
27	  sslmode         = "require"
28	  superuser       = false
29	  username        = "${dependency.database.outputs.admin_username}"
30	}
31	EOF
32	}
33
34	# ---------------------------------------------------------------------------------------------------------------------
35	# Locals are named constants that are reusable within the configuration.
36	# ---------------------------------------------------------------------------------------------------------------------
37	locals {
38	  # Automatically load common variables shared across all accounts
39	  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))
40
41	  # Automatically load account-level variables
42	  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
43
44	  # Automatically load region-level variables
45	  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
46
47	  # Extract the region and environment for easy access
48	  aws_region = local.region_vars.locals.aws_region
49	  env        = local.account_vars.locals.env
50
51	  # A local for more convenient access to the accounts map.
52	  accounts = local.common_vars.locals.accounts
53
54	  tags = merge(local.common_vars.locals.global_tags, {
55	    tfpath = "/platform-management/${local.aws_region}/mgmt-db-roles"
56	  })
57	}
58
59	# ---------------------------------------------------------------------------------------------------------------------
60	# MODULE DEPENDENCIES
61	# ---------------------------------------------------------------------------------------------------------------------
62
63	dependency database {
64	  config_path = "../mgmt-database"
65	}
66
67	# ---------------------------------------------------------------------------------------------------------------------
68	# MODULE PARAMETERS
69	# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
70	# ---------------------------------------------------------------------------------------------------------------------
71	inputs = {
72	  database = "registry"
73	  tags     = local.tags
74	}
75

# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/internal/security-database-roles-cps/aws?version=1.1.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

# To get access to IMC DB
# The Terraform required providers block is there because terragrunt is generating this indepenently from
# the module, and cannot access that required providers block.
generate "rds_postgres_provider" {
  path      = "postgres-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider postgresql {
  connect_timeout = 30
  database        = "${dependency.database.outputs.name}"
  host            = "${dependency.database.outputs.address}"
  password        = "${dependency.database.outputs.admin_password}"
  port            = "${dependency.database.outputs.port}"
  sslmode         = "require"
  superuser       = false
  username        = "${dependency.database.outputs.admin_username}"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/mgmt-db-roles"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency database {
  config_path = "../mgmt-database"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  database = "registry"
  tags     = local.tags
}


1	# ---------------------------------------------------------------------------------------------------------------------
2	# TERRAGRUNT CONFIGURATION
3	# ---------------------------------------------------------------------------------------------------------------------
4
5	terraform {
6	  source = "tfr://crow.infrastructure.immuta.io/internal/security-database-roles-cps/aws?version=1.1.0"
7	}
8
9	# Include all settings from the root terragrunt.hcl file
10	include {
11	  path = find_in_parent_folders()
12	}
13
14	# To get access to IMC DB
15	# The Terraform required providers block is there because terragrunt is generating this indepenently from
16	# the module, and cannot access that required providers block.
17	generate "rds_postgres_provider" {
18	  path      = "postgres-provider.tf"
19	  if_exists = "overwrite_terragrunt"
20	  contents  = <<EOF
21	provider postgresql {
22	  connect_timeout = 30
23	  database        = "${dependency.database.outputs.name}"
24	  host            = "${dependency.database.outputs.address}"
25	  password        = "${dependency.database.outputs.admin_password}"
26	  port            = "${dependency.database.outputs.port}"
27	  sslmode         = "require"
28	  superuser       = false
29	  username        = "${dependency.database.outputs.admin_username}"
30	}
31	EOF
32	}
33
34	# ---------------------------------------------------------------------------------------------------------------------
35	# Locals are named constants that are reusable within the configuration.
36	# ---------------------------------------------------------------------------------------------------------------------
37	locals {
38	  # Automatically load common variables shared across all accounts
39	  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))
40
41	  # Automatically load account-level variables
42	  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
43
44	  # Automatically load region-level variables
45	  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
46
47	  # Extract the region and environment for easy access
48	  aws_region = local.region_vars.locals.aws_region
49	  env        = local.account_vars.locals.env
50
51	  # A local for more convenient access to the accounts map.
52	  accounts = local.common_vars.locals.accounts
53
54	  tags = merge(local.common_vars.locals.global_tags, {
55	    tfpath = "/platform-management/${local.aws_region}/database-roles"
56	  })
57	}
58
59	# ---------------------------------------------------------------------------------------------------------------------
60	# MODULE DEPENDENCIES
61	# ---------------------------------------------------------------------------------------------------------------------
62
63	dependency database {
64	  config_path = "../retool-database"
65	}
66
67	# ---------------------------------------------------------------------------------------------------------------------
68	# MODULE PARAMETERS
69	# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
70	# ---------------------------------------------------------------------------------------------------------------------
71	inputs = {
72	  database = "retool_prod"
73	  tags     = local.tags
74	}
75

# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/internal/security-database-roles-cps/aws?version=1.1.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

# To get access to IMC DB
# The Terraform required providers block is there because terragrunt is generating this indepenently from
# the module, and cannot access that required providers block.
generate "rds_postgres_provider" {
  path      = "postgres-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider postgresql {
  connect_timeout = 30
  database        = "${dependency.database.outputs.name}"
  host            = "${dependency.database.outputs.address}"
  password        = "${dependency.database.outputs.admin_password}"
  port            = "${dependency.database.outputs.port}"
  sslmode         = "require"
  superuser       = false
  username        = "${dependency.database.outputs.admin_username}"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/database-roles"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency database {
  config_path = "../retool-database"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  database = "retool_prod"
  tags     = local.tags
}


1	4.3.0
2


Got everything I need. The `postgres-db-users` v4.3.0 module handles database + schema + admin/service user + vault storage in one shot. I'll create `churn-forecast-db-roles/terragrunt.hcl` directly.

Key mapping:
- `database_name = "churn_forecast"`, `schema_names = ["churn_forecast", "public"]`
- `admin_name_override = "churn_forecast_admin"`, `user_name_override = "churn_forecast"`
- `user_privs = ["DELETE", "INSERT", "UPDATE"]` — no SELECT, no DDL
- Vault at `secret/internal/churn-renewal-forecast-db/{username}` via `vault_mount = "secret"` + `vault_path = "internal/churn-renewal-forecast-db"`
- PostgreSQL provider connects to `postgres` default db using `tools-database` admin creds


Bash

Create churn-forecast-db-roles directory


(Bash completed with no output)


Write

# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/internal/postgres-db-users/database?version=4.3.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

generate "rds_postgres_provider" {
  path      = "postgres-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider postgresql {
  connect_timeout = 30
  database        = "postgres"
  host            = "${dependency.database.outputs.address}"
  password        = "${dependency.database.outputs.admin_password}"
  port            = "${dependency.database.outputs.port}"
  sslmode         = "require"
  superuser       = false
  username        = "${dependency.database.outputs.admin_username}"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/churn-forecast-db-roles"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency database {
  config_path = "../tools-database"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  database_name       = "churn_forecast"
  schema_names        = ["churn_forecast", "public"]
  admin_name_override = "churn_forecast_admin"
  user_name_override  = "churn_forecast"

  user_privs = ["DELETE", "INSERT", "UPDATE"]

  vault_mount = "secret"
  vault_path  = "internal/churn-renewal-forecast-db"

  tags = local.tags
}


File created successfully at: /Users/steve.briggs/projects/cpi-new/platform-management/us-east-1/churn-forecast-db-roles/terragrunt.hcl (file state is current in your context — no need to Read it back)

# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  source = "tfr://crow.infrastructure.immuta.io/internal/postgres-db-users/database?version=4.3.0"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

generate "rds_postgres_provider" {
  path      = "postgres-provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider postgresql {
  connect_timeout = 30
  database        = "postgres"
  host            = "${dependency.database.outputs.address}"
  password        = "${dependency.database.outputs.admin_password}"
  port            = "${dependency.database.outputs.port}"
  sslmode         = "require"
  superuser       = false
  username        = "${dependency.database.outputs.admin_username}"
}
EOF
}

# ---------------------------------------------------------------------------------------------------------------------
# Locals are named constants that are reusable within the configuration.
# ---------------------------------------------------------------------------------------------------------------------
locals {
  # Automatically load common variables shared across all accounts
  common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl"))

  # Automatically load account-level variables
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Extract the region and environment for easy access
  aws_region = local.region_vars.locals.aws_region
  env        = local.account_vars.locals.env

  # A local for more convenient access to the accounts map.
  accounts = local.common_vars.locals.accounts

  tags = merge(local.common_vars.locals.global_tags, {
    tfpath = "/platform-management/${local.aws_region}/churn-forecast-db-roles"
  })
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE DEPENDENCIES
# ---------------------------------------------------------------------------------------------------------------------

dependency database {
  config_path = "../tools-database"
}

# ---------------------------------------------------------------------------------------------------------------------
# MODULE PARAMETERS
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
# ---------------------------------------------------------------------------------------------------------------------
inputs = {
  database_name       = "churn_forecast"
  schema_names        = ["churn_forecast", "public"]
  admin_name_override = "churn_forecast_admin"
  user_name_override  = "churn_forecast"

  user_privs = ["DELETE", "INSERT", "UPDATE"]

  vault_mount = "secret"
  vault_path  = "internal/churn-renewal-forecast-db"

  tags = local.tags
}


Done fr fr, Sir Charles. File at `platform-management/us-east-1/churn-forecast-db-roles/terragrunt.hcl`.

Here's what it does when applied:

| Resource | Detail |
|---|---|
| Database | `churn_forecast`, owned by `churn_forecast_admin` |
| Schemas | `churn_forecast` + `public` (both created with `if_not_exists`, so public is safe) |
| `churn_forecast_admin` | Full table/function/procedure privs + `CREATE` on database |
| `churn_forecast` | `DELETE`, `INSERT`, `UPDATE` on all schemas — no SELECT, no DDL |
| Vault secrets | `secret/internal/churn-renewal-forecast-db/churn_forecast_admin` and `.../churn_forecast` (KV v2, each has `user` + `pass`) |

Apply order: `tools-database` first, then `churn-forecast-db-roles`. The postgresql provider connects as the RDS admin to the default `postgres` database to bootstrap everything. Bling bling! 🦕