|
| 1 | +# Terraform AWS Postgres Bootstrap |
| 2 | + |
| 3 | +This module is designed to simplify and automate the provisioning of PostgreSQL databases on Amazon Web Services (AWS) with IaC capabilities. As is most often the case, you will have applications and systems deploying managing their own migrations. Those applications and systems however do not always address the initial configuration that needs to be done before those migrations can run, or before the software can use the database. This is where this module comes in handy. |
| 4 | + |
| 5 | +Note that if you only want to bootstrap your database, and do not need an RDS instance, you can use the [postgres_init submodule](modules/postgres_init/). |
| 6 | + |
| 7 | +## Key Features |
| 8 | + |
| 9 | +- Automated PostgreSQL Setup: Provision a PostgreSQL instance on AWS with minimal configuration. |
| 10 | +- Credential Management: Securely manage database and user credentials without credentials being stored in the state. |
| 11 | +- Dynamic SQL generation: Dynamically generate SQL using variables and secrets |
| 12 | +- Security Configurations: Automatically set up security groups and other necessary configurations to ensure secure access to the database. |
| 13 | +- Modular Design: Utilize sub-modules for managing databases, users and custom scripts. |
| 14 | + |
| 15 | +## Usage |
| 16 | + |
| 17 | +### Minimal example |
| 18 | + |
| 19 | +```hcl |
| 20 | +module "postgres_databases" { |
| 21 | + source = "terraform-aws-postgres-bootstrap" |
| 22 | +
|
| 23 | + environment = "dev" |
| 24 | + database_configs = [ |
| 25 | + { |
| 26 | + rds = { |
| 27 | + identifier = "my-db" |
| 28 | + server_name = "my-database" |
| 29 | + vpc_id = "vpc-xxxxxxxx" |
| 30 | + db_subnet_ids = ["subnet-1234abcd", "subnet-5678efgh"] |
| 31 | + postgres_version = "16" |
| 32 | + instance_class = "db.t3.small" |
| 33 | + allocated_storage = 50 |
| 34 | + } |
| 35 | + init = { |
| 36 | + users = [{ name = "myuser" }] |
| 37 | + databases = [{ |
| 38 | + name = "mydb", |
| 39 | + owner = "myuser", |
| 40 | + schemas = ["myschema1"] |
| 41 | + extensions = [{ |
| 42 | + name = "myextension", |
| 43 | + schema = "myschema1" |
| 44 | + }] |
| 45 | + }] |
| 46 | + scripts = [{ |
| 47 | + id = "create_foo_config_table" |
| 48 | + script = "../path/to/script.sql", |
| 49 | + database = "mydb", |
| 50 | + variables = { "FOO" : "foo" }, |
| 51 | + secrets = { |
| 52 | + "BAR" = { path = "/aws/secret/path", key = "mysecret" } |
| 53 | + } |
| 54 | + shell_script = true |
| 55 | + }] |
| 56 | + } |
| 57 | + } |
| 58 | + ] |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### Full example |
| 63 | + |
| 64 | +```hcl |
| 65 | +module "terraform-aws-postgres-bootstrap" { |
| 66 | + source = "terraform-aws-postgres-bootstrap" |
| 67 | +
|
| 68 | + environment = "dev" |
| 69 | + database_configs = [ |
| 70 | + { |
| 71 | + rds = { |
| 72 | + identifier = "my-db" |
| 73 | + server_name = "my-database-server" |
| 74 | + vpc_id = "vpc-xxxxxxxx" |
| 75 | + db_subnet_ids = ["subnet-1234abcd", "subnet-5678efgh"] |
| 76 | + postgres_version = "16.3" |
| 77 | + auto_minor_version_upgrade = false # false because we specify minor version |
| 78 | + instance_class = "db.t3.small" |
| 79 | + allocated_storage = 50 # Gb |
| 80 | + storage_type = "gp3" |
| 81 | + storage_encrypted = true |
| 82 | + maintenance_username = "postgres" |
| 83 | + maintenance_database = "postgres" |
| 84 | + subnet_group = null # null because of db_subnet_ids |
| 85 | + max_allocated_storage = 500 # Gb |
| 86 | + deletion_protection = true |
| 87 | + skip_final_snapshot = false |
| 88 | + existing_user_credentials = null # use when importing existing db |
| 89 | + allowed_cidrs = [ |
| 90 | + { cidr_blocks = ["10.150.0.0/24"], description = "private-subnet-1"}, |
| 91 | + { cidr_blocks = ["10.150.1.0/24"], description = "private-subnet-2"} |
| 92 | + ] |
| 93 | + parameter_group = [ |
| 94 | + { |
| 95 | + name = "shared_preload_libraries" |
| 96 | + value = "pg_stat_statements,pglogical,pg_cron" |
| 97 | + apply_method = "pending-reboot" |
| 98 | + } |
| 99 | + ] |
| 100 | + } |
| 101 | + init = { |
| 102 | + users = [{ name = "user_1", password = "foobar" }, { name = "user_2", regenerate_password = true }] |
| 103 | + databases = [ |
| 104 | + { |
| 105 | + name = "db_1", |
| 106 | + owner = "user_1", |
| 107 | + schemas = ["schema_1_db_1"] |
| 108 | + extensions = [ |
| 109 | + { |
| 110 | + name = "pg_search", |
| 111 | + schema = "schema_1_db_1" |
| 112 | + } |
| 113 | + ] |
| 114 | + }, |
| 115 | + { |
| 116 | + name = "db_2", |
| 117 | + owner = "user_2", |
| 118 | + } |
| 119 | + ] |
| 120 | + scripts = [ |
| 121 | + { |
| 122 | + id = "create_foo_config_table" |
| 123 | + script = "../path/to/script.sql", |
| 124 | + database = "db_1", |
| 125 | + variables = { "FOO" : "foo" }, |
| 126 | + secrets = { |
| 127 | + "BAR" = { path = "/aws/secret/path", key = "mysecret" } |
| 128 | + } |
| 129 | + shell_script = false |
| 130 | + }, |
| 131 | + { |
| 132 | + id = "update_user_1_role" |
| 133 | + script = "../path/to/script.sh", |
| 134 | + database = "db_2", |
| 135 | + user = "user_2", |
| 136 | + variables = { "OTHER_USER" : "user_1" }, |
| 137 | + secrets = { |
| 138 | + "OTHER_USER_PASSWORD" = { |
| 139 | + path = "/dev/database-server/my-database-server/user/user_1", |
| 140 | + key = "password" |
| 141 | + } |
| 142 | + } |
| 143 | + shell_script = true |
| 144 | + rerun_on_user_change = true |
| 145 | + rerun_on_variable_change = true |
| 146 | + }] |
| 147 | + } |
| 148 | + } |
| 149 | + ] |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +This example creates the following: |
| 154 | + |
| 155 | +1. An RDS instance with the given specifications. |
| 156 | +2. Two users: `user_1` and `user_2`. |
| 157 | +3. Two databases: |
| 158 | + - `db_1`: |
| 159 | + - Owned by `user_1`. |
| 160 | + - Contains the schema `schema_1_db_1`. |
| 161 | + - Has the extension `pg_search` in schema `schema_1_db_1`. |
| 162 | + - `db_2`: |
| 163 | + - Owned by `user_2`. |
| 164 | +4. Executes the following scripts: |
| 165 | + - `../path/to/script.sql`: |
| 166 | + - An SQL script executed on database `db_1`. |
| 167 | + - Executed by the database server maintenance user (superuser). |
| 168 | + - Receives the variable `FOO`. |
| 169 | + - Receives the variable `BAR`, extracted from AWS Secrets Manager. |
| 170 | + - `../path/to/script.sh`: |
| 171 | + - A shell script executed on database `db_2`. |
| 172 | + - Executed by user `user_2`. |
| 173 | + - Receives the environment variable `OTHER_USER`. |
| 174 | + - Receives the environment variable `OTHER_USER_PASSWORD`, extracted from AWS Secrets Manager. |
| 175 | + - Configured to rerun when the script execution user's credentials change. |
| 176 | + - Configured to rerun when the variables and secrets passed to the script change. |
| 177 | + |
| 178 | +### Good to know |
| 179 | + |
| 180 | +#### Using additional user credentials in scripts |
| 181 | + |
| 182 | +The module creates the credentials for your database users which you can use in your `scripts` for database initialization, (f.x creating fdw's or servers). You can retrieve them from AWS secrets manager. The credentials are always created before the scripts execute. See the [user credentials module](modules/credentials/user/) and the [database credentials moduel](modules/credentials/database/) for the format of the secret name. |
| 183 | + |
| 184 | +Please review the [script submodule](modules/postgres_init/modules/script) for a detailed description on how to implement the bootstrapping scripts. There are also examples in the [examples directory](examples/) |
| 185 | + |
| 186 | +## Inputs |
| 187 | + |
| 188 | +| Name | Description | Type | Default | Required | |
| 189 | +| ---------------- | ------------------------------------------------------------------------------- | --------------------------------------------- | ------- | :------: | |
| 190 | +| environment | A unique identifier for the environment. Used for tagging and naming resources. | `string` | n/a | yes | |
| 191 | +| database_configs | The configurations of the databases to create. | `list(object({rds = object, init = object}))` | n/a | yes | |
| 192 | + |
| 193 | +## Database Configurations |
| 194 | + |
| 195 | +The `database_configs` variable is a list of objects, each representing a database to be created along with bootstrapping configuration. |
| 196 | + |
| 197 | +A database configuration object consists of two keys, namely `rds` and `init`. |
| 198 | + |
| 199 | +- The `rds` key configures the input variables for the postgres_rds submodule which creates the desired RDS instance to specification. For details on the configuration options see the [postgres_rds submodule](modules/postgres_rds/) |
| 200 | + |
| 201 | +- The `init` key configures the input variables for the postgres_init submodule, which bootstraps the RDS instance to the required specification. This includes adding users, creating databases, schemas, extensions, and executing arbitrary scripts using variables and secrets from AWS Secrets Manager. For details on the configuration options, see the [postgres_init submodule](modules/postgres_init/) |
| 202 | + |
| 203 | +In the configuration, unspecified optional keys default to null. The submodules then use this null value to apply default values when the fields are specified as non-nullable. |
| 204 | + |
| 205 | +## Outputs |
| 206 | + |
| 207 | +| Name | Description | |
| 208 | +| ------------------ | ------------------------------------------------------------------------------------------------------------- | |
| 209 | +| postgres_instances | Map of postgres_rds module output with each key being the identifier of the RDS instance created. | |
| 210 | +| postgres_inits | Map of postgres_init module output with each key being the identifier of the RDS instance it was executed on. | |
| 211 | + |
| 212 | +## Requirements |
| 213 | + |
| 214 | +| Name | Version | |
| 215 | +| --------------- | ------- | |
| 216 | +| awscli | >= 2 | |
| 217 | +| postgres-client | >= 12 | |
| 218 | +| openssl | >= 3 | |
| 219 | +| linux/mac | | |
0 commit comments