├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── databases.tf ├── examples ├── 1-single-node │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf └── 2-multi-node │ ├── main.tf │ ├── network.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── main.tf ├── outputs.tf ├── pgpass.tf ├── users.tf ├── variables.tf └── versions.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # Terraform lockfile 5 | .terraform.lock.hcl 6 | 7 | # .tfstate files 8 | *.tfstate 9 | *.tfstate.* 10 | 11 | **/*.log 12 | 13 | # sh 14 | *.sh 15 | **/*.sh 16 | 17 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 18 | # password, private keys, and other secrets. These should not be part of version 19 | # control as they are data points which are potentially sensitive and subject 20 | # to change depending on the environment. 21 | #*.tfvars 22 | 23 | # Ignore override files as they are usually used to override resources locally and so 24 | # are not checked in 25 | override.tf 26 | override.tf.json 27 | *_override.tf 28 | *_override.tf.json 29 | 30 | # Ignore CLI configuration files 31 | .terraformrc 32 | terraform.rc 33 | 34 | # Zip archive 35 | *.zip 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/antonbabenko/pre-commit-terraform.git 3 | rev: v1.77.1 4 | hooks: 5 | - id: terraform_fmt 6 | - id: terraform_validate 7 | - id: terraform_docs 8 | - id: terraform_tflint 9 | args: 10 | - '--args=--only=terraform_deprecated_interpolation' 11 | - '--args=--only=terraform_deprecated_index' 12 | - '--args=--only=terraform_unused_declarations' 13 | - '--args=--only=terraform_comment_syntax' 14 | - '--args=--only=terraform_documented_outputs' 15 | - '--args=--only=terraform_documented_variables' 16 | - '--args=--only=terraform_typed_variables' 17 | - '--args=--only=terraform_module_pinned_source' 18 | - '--args=--only=terraform_naming_convention' 19 | - '--args=--only=terraform_required_version' 20 | - '--args=--only=terraform_required_providers' 21 | - '--args=--only=terraform_standard_module_structure' 22 | - '--args=--only=terraform_workspace_remote' 23 | - repo: https://github.com/pre-commit/pre-commit-hooks 24 | rev: v4.4.0 25 | hooks: 26 | - id: check-merge-conflict 27 | - id: end-of-file-fixer 28 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have created the source code of "Terraform Yandex Cloud Modules" 2 | published and distributed by YANDEX LLC as the owner: 3 | Alexander Dushein adushein@yandex-team.ru 4 | Roman Timofeev romantimofeev@yandex-team.ru 5 | Pavel Selivanov poselivanov@yandex-team.ru 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Notice to external contributors 2 | 3 | 4 | ## General info 5 | 6 | Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the **CLA**). The current version of the CLA can be found here: 7 | 1) https://yandex.ru/legal/cla/?lang=en (in English) and 8 | 2) https://yandex.ru/legal/cla/?lang=ru (in Russian). 9 | 10 | By adopting the CLA, you state the following: 11 | 12 | * You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, 13 | * You have read the terms and conditions of the CLA and agree with them in full, 14 | * You are legally able to provide and license your contributions as stated, 15 | * We may use your contributions for our open source projects and for any other our project too, 16 | * We rely on your assurances concerning the rights of third parties in relation to your contributions. 17 | 18 | If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. 19 | 20 | ## Provide contributions 21 | 22 | If you have already adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: 23 | 24 | ``` 25 | I hereby agree to the terms of the CLA available at: [link]. 26 | ``` 27 | 28 | Replace the bracketed text as follows: 29 | * [link] is the link to the current version of the CLA: https://yandex.ru/legal/cla/?lang=en (in English) or https://yandex.ru/legal/cla/?lang=ru (in Russian). 30 | 31 | It is enough to provide us such notification once. 32 | 33 | ## Pull Request Process 34 | 35 | When contributing to this repository, please first discuss the change you wish to make via issue. 36 | 37 | 1. Update the README.md with details of changes including example hcl blocks and [example files](./examples) if appropriate. 38 | 2. Run pre-commit hooks `pre-commit run -a`. 39 | 3. Once all outstanding comments and checklist items have been addressed, your contribution will be merged! Merged PRs will be included in the next release. 40 | 41 | ## Checklists for contributions 42 | 43 | - [ ] You have updated at least one of the examples/* to demonstrate and validate change(s) 44 | - [ ] You have tested and validated changes using one or more of the provided [examples/*](./examples) projects 45 | - [ ] README.md has been updated after any changes to variables and outputs. 46 | - [ ] Run pre-commit hooks `pre-commit run -a` 47 | - [ ] Done steps from [Provide contributions](#1.0) section 48 | 49 | ## Other questions 50 | 51 | If you have any questions, please mail us at opensource@yandex-team.ru. 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 YANDEX LLC 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yandex Cloud Managed PostgreSQL Cluster 2 | 3 | ## Features 4 | 5 | - Create a managed PostgreSQL cluster with a predefined number of DB hosts 6 | - Create a list of users and databases with permissions 7 | - Easy to use in other resources via outputs 8 | 9 | ## PostgreSQL cluster definition 10 | 11 | First, you need to create a VPC network with three subnets. 12 | 13 | PostgreSQL module requires the following input variables: 14 | - VPC network ID. 15 | - VPC network subnet IDs. 16 | - PostgreSQL host definitions: List of maps with DB host name, zone name, and subnet ID. 17 | - Databases: List of databases with database names, owners, and other parameters. 18 | - Owners: List of database owners. 19 | - Users: All other users with a list of permissions to databases. 20 | 21 | Notes: 22 | 1. The `owners` variable defines a list of databases owners. It does not support the `permissions` list because these users will be linked with `databases` via the `owner` parameter. This means each database must have an owner. 23 | 2. The `users` variable defines a list of separate DB users with the `permissions` list, which points to a list of databases. This means each user will have access to each database from the `permissions` list. 24 | 3. The `settings_options` parameter may be null, in which case the default values will be used. 25 | 26 | ### Example 27 | 28 | See [examples section](./examples/) 29 | 30 | ### How to configure Terraform for Yandex Cloud 31 | 32 | - Install [YC CLI](https://cloud.yandex.com/docs/cli/quickstart) 33 | - Add environment variables for terraform auth in Yandex.Cloud 34 | 35 | ``` 36 | export YC_TOKEN=$(yc iam create-token) 37 | export YC_CLOUD_ID=$(yc config get cloud-id) 38 | export YC_FOLDER_ID=$(yc config get folder-id) 39 | export TF_VAR_network_id=_vpc id here_ 40 | ``` 41 | 42 | 43 | ## Requirements 44 | 45 | | Name | Version | 46 | |------|---------| 47 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 48 | | [random](#requirement\_random) | > 3.3 | 49 | | [yandex](#requirement\_yandex) | >= 0.89.0 | 50 | 51 | ## Providers 52 | 53 | | Name | Version | 54 | |------|---------| 55 | | [local](#provider\_local) | 2.4.0 | 56 | | [random](#provider\_random) | 3.5.1 | 57 | | [yandex](#provider\_yandex) | 0.98.0 | 58 | 59 | ## Modules 60 | 61 | No modules. 62 | 63 | ## Resources 64 | 65 | | Name | Type | 66 | |------|------| 67 | | [local_file.pgpass_file](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | 68 | | [random_password.password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | 69 | | [yandex_dns_recordset.rw](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/dns_recordset) | resource | 70 | | [yandex_mdb_postgresql_cluster.this](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/mdb_postgresql_cluster) | resource | 71 | | [yandex_mdb_postgresql_database.database](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/mdb_postgresql_database) | resource | 72 | | [yandex_mdb_postgresql_user.owner](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/mdb_postgresql_user) | resource | 73 | | [yandex_mdb_postgresql_user.user](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/mdb_postgresql_user) | resource | 74 | | [yandex_client_config.client](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/data-sources/client_config) | data source | 75 | 76 | ## Inputs 77 | 78 | | Name | Description | Type | Default | Required | 79 | |------|-------------|------|---------|:--------:| 80 | | [access\_policy](#input\_access\_policy) | Access policy from other services to the PostgreSQL cluster. |
object({
data_lens = optional(bool, null)
web_sql = optional(bool, null)
serverless = optional(bool, null)
data_transfer = optional(bool, null)
})
| `{}` | no | 81 | | [autofailover](#input\_autofailover) | (Optional) Configuration setting which enables and disables auto failover in the cluster. | `bool` | `true` | no | 82 | | [backup\_retain\_period\_days](#input\_backup\_retain\_period\_days) | (Optional) The period in days during which backups are stored. | `number` | `null` | no | 83 | | [backup\_window\_start](#input\_backup\_window\_start) | (Optional) Time to start the daily backup, in the UTC timezone. |
object({
hours = string
minutes = optional(string, "00")
})
| `null` | no | 84 | | [databases](#input\_databases) | List of PostgreSQL databases.

Required values:
- name - (Required) The name of the database.
- owner - (Required) Name of the user assigned as the owner of the database. Forbidden to change in an existing database.
- extension - (Optional) Set of database extensions.
- lc\_collate - (Optional) POSIX locale for string sorting order. Forbidden to change in an existing database.
- lc\_type - (Optional) POSIX locale for character classification. Forbidden to change in an existing database.
- template\_db - (Optional) Name of the template database.
- deletion\_protection - (Optional) A deletion protection. |
list(object({
name = string
owner = string
lc_collate = optional(string, null)
lc_type = optional(string, null)
template_db = optional(string, null)
deletion_protection = optional(bool, null)
extensions = optional(list(string), [])
}))
| n/a | yes | 85 | | [default\_user\_settings](#input\_default\_user\_settings) | The default user settings. These settings are overridden by the user's settings.
Full description https://cloud.yandex.com/en-ru/docs/managed-postgresql/api-ref/grpc/user_service#UserSettings1 | `map(any)` | `{}` | no | 86 | | [deletion\_protection](#input\_deletion\_protection) | Protects the cluster from deletion | `bool` | `false` | no | 87 | | [description](#input\_description) | PostgreSQL cluster description | `string` | `"Managed PostgreSQL cluster"` | no | 88 | | [disk\_size](#input\_disk\_size) | Disk size for every cluster host | `number` | `20` | no | 89 | | [disk\_type](#input\_disk\_type) | Disk type for all cluster hosts | `string` | `"network-ssd"` | no | 90 | | [dns\_cname\_config](#input\_dns\_cname\_config) | Creates a CNAME record of connection host in the specified DNS zone.

Object values:
- name - (Required) The DNS name this record set will apply to.
- zone\_id - (Required) The id of the zone in which this record set will reside
- ttl - (Optional) The time-to-live of this record set (seconds). |
object({
name = string
zone_id = string
ttl = optional(number, 360)
})
| `null` | no | 91 | | [environment](#input\_environment) | Environment type: PRODUCTION or PRESTABLE | `string` | `"PRODUCTION"` | no | 92 | | [folder\_id](#input\_folder\_id) | Yandex Cloud Folder ID where the cluster resides | `string` | `null` | no | 93 | | [host\_master\_name](#input\_host\_master\_name) | Name of the master host. | `string` | `null` | no | 94 | | [hosts\_definition](#input\_hosts\_definition) | A list of PostgreSQL hosts. |
list(object({
name = optional(string, null)
zone = string
subnet_id = string
assign_public_ip = optional(bool, false)
replication_source_name = optional(string, null)
priority = optional(number, null)
}))
| n/a | yes | 95 | | [labels](#input\_labels) | A set of label pairs to assing to the PostgreSQL cluster. | `map(any)` | `{}` | no | 96 | | [maintenance\_window](#input\_maintenance\_window) | (Optional) Maintenance policy of the PostgreSQL cluster.
- type - (Required) Type of maintenance window. Can be either ANYTIME or WEEKLY. A day and hour of window need to be specified with weekly window.
- day - (Optional) Day of the week (in DDD format). Allowed values: "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"
- hour - (Optional) Hour of the day in UTC (in HH format). Allowed value is between 0 and 23. |
object({
type = string
day = optional(string, null)
hour = optional(string, null)
})
|
{
"type": "ANYTIME"
}
| no | 97 | | [name](#input\_name) | PostgreSQL cluster name | `string` | `"pgsql-cluster"` | no | 98 | | [network\_id](#input\_network\_id) | Network id of the PostgreSQL cluster | `string` | n/a | yes | 99 | | [owners](#input\_owners) | List of special PostgreSQL DB users - database owners. These users are created first and assigned to database as owner.
There is also an aditional list for other users with own permissions.

Required values:
- name - (Required) The name of the user.
- password - (Optional) The user's password. If it's omitted a random password will be generated.
- grants - (Optional) List of the user's grants.
- login - (Optional) The user's ability to login.
- conn\_limit - (Optional) The maximum number of connections per user.
- settings - (Optional) A user setting options.
- deletion\_protection - (Optional) A deletion protection. |
list(object({
name = string
password = optional(string, null)
grants = optional(list(string), [])
login = optional(bool, null)
conn_limit = optional(number, null)
settings = optional(map(any), {})
deletion_protection = optional(bool, null)
}))
| n/a | yes | 100 | | [performance\_diagnostics](#input\_performance\_diagnostics) | (Optional) PostgreSQL cluster performance diagnostics settings. |
object({
enabled = optional(bool, null)
sessions_sampling_interval = optional(number, 60)
statements_sampling_interval = optional(number, 600)
})
| `{}` | no | 101 | | [pg\_version](#input\_pg\_version) | PostgreSQL version | `string` | `"15"` | no | 102 | | [pgpass\_path](#input\_pgpass\_path) | Location of the .pgpass file. If it's omitted the file will not be created | `string` | `null` | no | 103 | | [pooler\_config](#input\_pooler\_config) | Configuration of the connection pooler.
- pool\_discard - Setting pool\_discard parameter in Odyssey. Values: yes \| no
- pooling\_mode - Mode that the connection pooler is working in. Values: `POOLING_MODE_UNSPECIFIED`, `SESSION`, `TRANSACTION`, `STATEMENT` |
object({
pool_discard = optional(bool, null)
pooling_mode = optional(string, null)
})
| `null` | no | 104 | | [postgresql\_config](#input\_postgresql\_config) | Map of PostgreSQL cluster configuration.
Details info in a 'PostgreSQL cluster settings' of official documentation.
Link: https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/mdb_postgresql_cluster#postgresql-cluster-settings | `map(any)` | `null` | no | 105 | | [resource\_preset\_id](#input\_resource\_preset\_id) | Preset for hosts | `string` | `"s2.micro"` | no | 106 | | [restore\_parameters](#input\_restore\_parameters) | The cluster will be created from the specified backup.
NOTES:
- backup\_id must be specified to create a new PostgreSQL cluster from a backup.
- time format is 'yyy-mm-ddThh:mi:ss', where T is a delimeter, e.g. "2023-04-05T11:22:33".
- time\_inclusive indicates recovery to nearest recovery point just before (false) or right after (true) the time. |
object({
backup_id = string
time = optional(string, null)
time_inclusive = optional(bool, null)
})
| `null` | no | 107 | | [security\_groups\_ids\_list](#input\_security\_groups\_ids\_list) | List of security group IDs to which the PostgreSQL cluster belongs | `list(string)` | `[]` | no | 108 | | [users](#input\_users) | List of additional PostgreSQL users with own permissions. They are created at the end.

Required values:
- name - (Required) The name of the user.
- password - (Optional) The user's password. If it's omitted a random password will be generated.
- grants - (Optional) List of the user's grants.
- login - (Optional) The user's ability to login.
- conn\_limit - (Optional) The maximum number of connections per user.
- permissions - (Optional) List of databases names for an access
- settings - (Optional) A user setting options.
- deletion\_protection - (Optional) A deletion protection. |
list(object({
name = string
password = optional(string, null)
grants = optional(list(string), [])
login = optional(bool, null)
conn_limit = optional(number, null)
permissions = optional(list(string), [])
settings = optional(map(any), {})
deletion_protection = optional(bool, null)
}))
| `[]` | no | 109 | 110 | ## Outputs 111 | 112 | | Name | Description | 113 | |------|-------------| 114 | | [cluster\_fqdns\_list](#output\_cluster\_fqdns\_list) | PostgreSQL cluster nodes FQDN list | 115 | | [cluster\_host\_names\_list](#output\_cluster\_host\_names\_list) | PostgreSQL cluster host name | 116 | | [cluster\_id](#output\_cluster\_id) | PostgreSQL cluster ID | 117 | | [cluster\_name](#output\_cluster\_name) | PostgreSQL cluster name | 118 | | [connection\_host\_cname](#output\_connection\_host\_cname) | PostgreSQL cluster connection host CNAME. | 119 | | [connection\_step\_1](#output\_connection\_step\_1) | 1 step - Install certificate | 120 | | [connection\_step\_2](#output\_connection\_step\_2) | How connect to PostgreSQL cluster?

1. Install certificate

mkdir --parents \~/.postgresql && \\
curl -sfL "https://storage.yandexcloud.net/cloud-certs/CA.pem" -o \~/.postgresql/root.crt && \\
chmod 0600 \~/.postgresql/root.crt

2. Run connection string from the output value, for example

psql "host=rc1a-g2em5m3zc9dxxasn.mdb.yandexcloud.net \\
port=6432 \\
sslmode=verify-full \\
dbname=db-b \\
user=owner-b \\
target\_session\_attrs=read-write" | 121 | | [databases](#output\_databases) | List of databases names. | 122 | | [owners\_data](#output\_owners\_data) | List of owners with passwords. | 123 | | [users\_data](#output\_users\_data) | List of users with passwords. | 124 | 125 | -------------------------------------------------------------------------------- /databases.tf: -------------------------------------------------------------------------------- 1 | # PostgreSQL databases 2 | resource "yandex_mdb_postgresql_database" "database" { 3 | for_each = length(var.databases) > 0 ? { for db in var.databases : db.name => db } : {} 4 | 5 | cluster_id = yandex_mdb_postgresql_cluster.this.id 6 | name = each.value.name 7 | owner = yandex_mdb_postgresql_user.owner[each.value.owner].name 8 | lc_collate = each.value.lc_collate 9 | lc_type = each.value.lc_type 10 | deletion_protection = each.value.deletion_protection 11 | template_db = each.value.template_db 12 | 13 | dynamic "extension" { 14 | for_each = each.value.extensions 15 | content { 16 | name = extension.value 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/1-single-node/main.tf: -------------------------------------------------------------------------------- 1 | # main.tf 2 | 3 | module "db" { 4 | source = "../../" 5 | 6 | network_id = var.network_id 7 | name = "alone-in-the-dark" 8 | description = "Single-node PostgreSQL cluster for test purposes" 9 | 10 | maintenance_window = { 11 | type = "WEEKLY" 12 | day = "SUN" 13 | hour = "02" 14 | } 15 | 16 | access_policy = { 17 | web_sql = true 18 | } 19 | 20 | performance_diagnostics = { 21 | enabled = true 22 | } 23 | 24 | hosts_definition = [ 25 | { 26 | zone = "ru-central1-a" 27 | assign_public_ip = true 28 | subnet_id = var.subnet_id 29 | } 30 | ] 31 | 32 | postgresql_config = { 33 | max_connections = 395 34 | enable_parallel_hash = true 35 | autovacuum_vacuum_scale_factor = 0.34 36 | default_transaction_isolation = "TRANSACTION_ISOLATION_READ_COMMITTED" 37 | shared_preload_libraries = "SHARED_PRELOAD_LIBRARIES_AUTO_EXPLAIN,SHARED_PRELOAD_LIBRARIES_PG_HINT_PLAN" 38 | } 39 | 40 | default_user_settings = { 41 | default_transaction_isolation = "read committed" 42 | log_min_duration_statement = 5000 43 | } 44 | 45 | databases = [ 46 | { 47 | name = "test1" 48 | owner = "test1" 49 | lc_collate = "ru_RU.UTF-8" 50 | lc_type = "ru_RU.UTF-8" 51 | extensions = ["uuid-ossp", "xml2"] 52 | } 53 | ] 54 | 55 | owners = [ 56 | { 57 | name = "test1" 58 | conn_limit = 15 59 | } 60 | ] 61 | 62 | users = [ 63 | { 64 | name = "test1-guest" 65 | conn_limit = 30 66 | permissions = ["test1"] 67 | settings = { 68 | pool_mode = "transaction" 69 | prepared_statements_pooling = true 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /examples/1-single-node/outputs.tf: -------------------------------------------------------------------------------- 1 | output "postgresql_cluster_id" { 2 | description = "PostgreSQL cluster ID" 3 | value = try(module.db.cluster_id, null) 4 | } 5 | 6 | output "postgresql_cluster_name" { 7 | description = "PostgreSQL cluster name" 8 | value = try(module.db.cluster_name, null) 9 | } 10 | 11 | output "postgresql_cluster_host_names_list" { 12 | description = "PostgreSQL cluster host name list" 13 | value = try(module.db.cluster_host_names_list, null) 14 | } 15 | 16 | output "postgresql_cluster_fqdns_list" { 17 | description = "PostgreSQL cluster FQDNs list" 18 | value = try(module.db.cluster_fqdns_list, null) 19 | } 20 | 21 | output "db_owners" { 22 | description = "A list of DB owners users with password." 23 | sensitive = true 24 | value = try(module.db.owners_data, null) 25 | } 26 | 27 | output "db_users" { 28 | description = "A list of separate DB users with passwords." 29 | sensitive = true 30 | value = try(module.db.users_data, null) 31 | } 32 | 33 | output "postgresql_databases" { 34 | description = "A list of database names." 35 | value = try(module.db.databases, null) 36 | } 37 | 38 | output "postgresql_connection_step_1" { 39 | description = "1 step - Install certificate" 40 | value = try(module.db.connection_step_1, null) 41 | } 42 | output "postgresql_connection_step_2" { 43 | description = "2 step - Execute psql command for a connection to the cluster" 44 | value = try(module.db.connection_step_2, null) 45 | } 46 | -------------------------------------------------------------------------------- /examples/1-single-node/variables.tf: -------------------------------------------------------------------------------- 1 | variable "network_id" { 2 | description = "PostgreSQL cluster network id" 3 | type = string 4 | } 5 | 6 | variable "subnet_id" { 7 | description = "PostgreSQL cluster subnet id" 8 | type = string 9 | # Isn't necessarily to set the subnet_id if there's the only subnet in the availability zone 10 | default = null 11 | } 12 | -------------------------------------------------------------------------------- /examples/1-single-node/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.0" 3 | 4 | required_providers { 5 | yandex = { 6 | source = "yandex-cloud/yandex" 7 | version = "> 0.8" 8 | } 9 | local = { 10 | source = "hashicorp/local" 11 | version = "2.2.3" 12 | } 13 | random = { 14 | source = "hashicorp/random" 15 | version = "> 3.3" 16 | } 17 | } 18 | } 19 | 20 | provider "yandex" {} 21 | 22 | provider "local" {} 23 | 24 | provider "random" {} 25 | -------------------------------------------------------------------------------- /examples/2-multi-node/main.tf: -------------------------------------------------------------------------------- 1 | # main.tf 2 | 3 | module "db" { 4 | source = "../../" 5 | 6 | network_id = var.network_id 7 | name = "one-two-tree" 8 | description = "Multi-node PostgreSQL cluster for test purposes" 9 | 10 | maintenance_window = { 11 | type = "WEEKLY" 12 | day = "SUN" 13 | hour = "02" 14 | } 15 | 16 | access_policy = { 17 | web_sql = true 18 | } 19 | 20 | performance_diagnostics = { 21 | enabled = true 22 | } 23 | 24 | hosts_definition = [ 25 | { 26 | name = "one" 27 | priority = 0 28 | zone = "ru-central1-a" 29 | assign_public_ip = true 30 | subnet_id = yandex_vpc_subnet.foo.id 31 | }, 32 | { 33 | name = "two" 34 | priority = 10 35 | zone = "ru-central1-b" 36 | assign_public_ip = true 37 | subnet_id = yandex_vpc_subnet.bar.id 38 | }, 39 | { 40 | name = "suntree" 41 | zone = "ru-central1-b" 42 | assign_public_ip = true 43 | subnet_id = yandex_vpc_subnet.bar.id 44 | replication_source_name = "two" 45 | } 46 | ] 47 | 48 | postgresql_config = { 49 | max_connections = 395 50 | enable_parallel_hash = true 51 | autovacuum_vacuum_scale_factor = 0.34 52 | default_transaction_isolation = "TRANSACTION_ISOLATION_READ_COMMITTED" 53 | shared_preload_libraries = "SHARED_PRELOAD_LIBRARIES_AUTO_EXPLAIN,SHARED_PRELOAD_LIBRARIES_PG_HINT_PLAN" 54 | } 55 | 56 | default_user_settings = { 57 | default_transaction_isolation = "read committed" 58 | log_min_duration_statement = 5000 59 | } 60 | 61 | databases = [ 62 | { 63 | name = "test1" 64 | owner = "test1" 65 | lc_collate = "ru_RU.UTF-8" 66 | lc_type = "ru_RU.UTF-8" 67 | extensions = ["uuid-ossp", "xml2"] 68 | } 69 | ] 70 | 71 | owners = [ 72 | { 73 | name = "test1" 74 | conn_limit = 15 75 | } 76 | ] 77 | 78 | users = [ 79 | { 80 | name = "test1-guest" 81 | conn_limit = 30 82 | permissions = ["test1"] 83 | settings = { 84 | pool_mode = "transaction" 85 | prepared_statements_pooling = true 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /examples/2-multi-node/network.tf: -------------------------------------------------------------------------------- 1 | resource "yandex_vpc_subnet" "foo" { 2 | zone = "ru-central1-a" 3 | network_id = var.network_id 4 | v4_cidr_blocks = ["10.1.0.0/24"] 5 | } 6 | 7 | resource "yandex_vpc_subnet" "bar" { 8 | zone = "ru-central1-b" 9 | network_id = var.network_id 10 | v4_cidr_blocks = ["10.2.0.0/24"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/2-multi-node/outputs.tf: -------------------------------------------------------------------------------- 1 | output "postgresql_cluster_id" { 2 | description = "PostgreSQL cluster ID" 3 | value = try(module.db.cluster_id, null) 4 | } 5 | 6 | output "postgresql_cluster_name" { 7 | description = "PostgreSQL cluster name" 8 | value = try(module.db.cluster_name, null) 9 | } 10 | 11 | output "postgresql_cluster_host_names_list" { 12 | description = "PostgreSQL cluster host name list" 13 | value = try(module.db.cluster_host_names_list, null) 14 | } 15 | 16 | output "postgresql_cluster_fqdns_list" { 17 | description = "PostgreSQL cluster FQDNs list" 18 | value = try(module.db.cluster_fqdns_list, null) 19 | } 20 | 21 | output "db_owners" { 22 | description = "A list of DB owners users with password." 23 | sensitive = true 24 | value = try(module.db.owners_data, null) 25 | } 26 | 27 | output "db_users" { 28 | description = "A list of separate DB users with passwords." 29 | sensitive = true 30 | value = try(module.db.users_data, null) 31 | } 32 | 33 | output "postgresql_databases" { 34 | description = "A list of database names." 35 | value = try(module.db.databases, null) 36 | } 37 | 38 | output "postgresql_connection_step_1" { 39 | description = "1 step - Install certificate" 40 | value = try(module.db.connection_step_1, null) 41 | } 42 | 43 | output "postgresql_connection_step_2" { 44 | description = "2 step - Execute psql command for a connection to the cluster" 45 | value = try(module.db.connection_step_2, null) 46 | } 47 | -------------------------------------------------------------------------------- /examples/2-multi-node/variables.tf: -------------------------------------------------------------------------------- 1 | variable "network_id" { 2 | description = "PostgreSQL cluster network id" 3 | type = string 4 | } 5 | -------------------------------------------------------------------------------- /examples/2-multi-node/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.0" 3 | 4 | required_providers { 5 | yandex = { 6 | source = "yandex-cloud/yandex" 7 | version = "> 0.8" 8 | } 9 | local = { 10 | source = "hashicorp/local" 11 | version = "2.2.3" 12 | } 13 | random = { 14 | source = "hashicorp/random" 15 | version = "> 3.3" 16 | } 17 | } 18 | } 19 | 20 | provider "yandex" {} 21 | 22 | provider "local" {} 23 | 24 | provider "random" {} 25 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | data "yandex_client_config" "client" {} 2 | 3 | locals { 4 | folder_id = var.folder_id == null ? data.yandex_client_config.client.folder_id : var.folder_id 5 | } 6 | 7 | # PostgreSQL cluster 8 | resource "yandex_mdb_postgresql_cluster" "this" { 9 | name = var.name 10 | description = var.description 11 | environment = var.environment 12 | network_id = var.network_id 13 | folder_id = local.folder_id 14 | labels = var.labels 15 | host_master_name = var.host_master_name 16 | deletion_protection = var.deletion_protection 17 | security_group_ids = var.security_groups_ids_list 18 | 19 | config { 20 | version = var.pg_version 21 | postgresql_config = var.postgresql_config 22 | autofailover = var.autofailover 23 | backup_retain_period_days = var.backup_retain_period_days 24 | 25 | resources { 26 | disk_size = var.disk_size 27 | disk_type_id = var.disk_type 28 | resource_preset_id = var.resource_preset_id 29 | } 30 | 31 | dynamic "access" { 32 | for_each = range(var.access_policy == null ? 0 : 1) 33 | content { 34 | data_lens = var.access_policy.data_lens 35 | web_sql = var.access_policy.web_sql 36 | serverless = var.access_policy.serverless 37 | data_transfer = var.access_policy.data_transfer 38 | } 39 | } 40 | 41 | dynamic "performance_diagnostics" { 42 | for_each = range(var.performance_diagnostics == null ? 0 : 1) 43 | content { 44 | enabled = var.performance_diagnostics.enabled 45 | sessions_sampling_interval = var.performance_diagnostics.sessions_sampling_interval 46 | statements_sampling_interval = var.performance_diagnostics.statements_sampling_interval 47 | } 48 | } 49 | 50 | dynamic "backup_window_start" { 51 | for_each = range(var.backup_window_start == null ? 0 : 1) 52 | content { 53 | hours = var.backup_window_start.hours 54 | minutes = var.backup_window_start.minutes 55 | } 56 | } 57 | 58 | dynamic "pooler_config" { 59 | for_each = range(var.pooler_config == null ? 0 : 1) 60 | content { 61 | pool_discard = var.pooler_config.pool_discard 62 | pooling_mode = var.pooler_config.pooling_mode 63 | } 64 | } 65 | } 66 | 67 | dynamic "host" { 68 | for_each = var.hosts_definition 69 | content { 70 | name = host.value.name 71 | zone = host.value.zone 72 | subnet_id = host.value.subnet_id 73 | assign_public_ip = host.value.assign_public_ip 74 | priority = host.value.priority 75 | replication_source_name = host.value.replication_source_name 76 | } 77 | } 78 | 79 | dynamic "restore" { 80 | for_each = range(var.restore_parameters == null ? 0 : 1) 81 | content { 82 | backup_id = var.restore_parameters.backup_id 83 | time = var.restore_parameters.time 84 | time_inclusive = var.restore_parameters.time_inclusive 85 | } 86 | } 87 | 88 | dynamic "maintenance_window" { 89 | for_each = range(var.maintenance_window == null ? 0 : 1) 90 | content { 91 | type = var.maintenance_window.type 92 | day = var.maintenance_window.day 93 | hour = var.maintenance_window.hour 94 | } 95 | } 96 | 97 | } 98 | 99 | resource "yandex_dns_recordset" "rw" { 100 | count = var.dns_cname_config != null ? 1 : 0 101 | 102 | zone_id = var.dns_cname_config["zone_id"] 103 | name = "${var.dns_cname_config["name"]}." 104 | type = "CNAME" 105 | ttl = var.dns_cname_config["ttl"] 106 | data = ["c-${yandex_mdb_postgresql_cluster.this.id}.rw.mdb.yandexcloud.net"] 107 | } 108 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_id" { 2 | description = "PostgreSQL cluster ID" 3 | value = yandex_mdb_postgresql_cluster.this.id 4 | } 5 | 6 | output "cluster_name" { 7 | description = "PostgreSQL cluster name" 8 | value = yandex_mdb_postgresql_cluster.this.name 9 | } 10 | 11 | output "cluster_host_names_list" { 12 | description = "PostgreSQL cluster host name" 13 | value = [yandex_mdb_postgresql_cluster.this.host[*].name] 14 | } 15 | 16 | output "cluster_fqdns_list" { 17 | description = "PostgreSQL cluster nodes FQDN list" 18 | value = [yandex_mdb_postgresql_cluster.this.host[*].fqdn] 19 | } 20 | 21 | output "owners_data" { 22 | description = "List of owners with passwords." 23 | sensitive = true 24 | value = [ 25 | for u in yandex_mdb_postgresql_user.owner : { 26 | user = u.name 27 | password = u.password 28 | } 29 | ] 30 | } 31 | 32 | output "users_data" { 33 | description = "List of users with passwords." 34 | sensitive = true 35 | value = [ 36 | for u in yandex_mdb_postgresql_user.user : { 37 | user = u.name 38 | password = u.password 39 | } 40 | ] 41 | } 42 | 43 | output "databases" { 44 | description = "List of databases names." 45 | value = [for db in var.databases : db.name] 46 | } 47 | 48 | output "connection_step_1" { 49 | description = "1 step - Install certificate" 50 | value = "mkdir --parents ~/.postgresql && curl -sfL 'https://storage.yandexcloud.net/cloud-certs/CA.pem' -o ~/.postgresql/root.crt && chmod 0600 ~/.postgresql/root.crt" 51 | } 52 | 53 | output "connection_step_2" { 54 | description = < v if v.password == null } 3 | length = 16 4 | special = true 5 | min_lower = 1 6 | min_numeric = 1 7 | min_special = 1 8 | min_upper = 1 9 | override_special = "-_()[]{}!%^" 10 | } 11 | 12 | # PostgreSQL databases owners 13 | resource "yandex_mdb_postgresql_user" "owner" { 14 | for_each = length(var.owners) > 0 ? { for owner in var.owners : owner.name => owner } : {} 15 | 16 | cluster_id = yandex_mdb_postgresql_cluster.this.id 17 | name = each.value.name 18 | password = each.value.password == null ? random_password.password[each.value.name].result : each.value.password 19 | grants = each.value.grants 20 | login = each.value.login 21 | conn_limit = each.value.conn_limit 22 | deletion_protection = each.value.deletion_protection 23 | settings = merge(var.default_user_settings, each.value.settings) 24 | } 25 | 26 | # PostgreSQL users with own permissions 27 | resource "yandex_mdb_postgresql_user" "user" { 28 | for_each = length(var.users) > 0 ? { for user in var.users : user.name => user } : {} 29 | 30 | cluster_id = yandex_mdb_postgresql_cluster.this.id 31 | name = each.value.name 32 | password = each.value.password == null ? random_password.password[each.value.name].result : each.value.password 33 | grants = each.value.grants 34 | login = each.value.login 35 | conn_limit = each.value.conn_limit 36 | deletion_protection = each.value.deletion_protection 37 | settings = merge(var.default_user_settings, each.value.settings) 38 | 39 | dynamic "permission" { 40 | for_each = each.value.permissions 41 | content { 42 | database_name = yandex_mdb_postgresql_database.database[permission.value].name 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | # Variables 2 | variable "name" { 3 | description = "PostgreSQL cluster name" 4 | type = string 5 | default = "pgsql-cluster" 6 | } 7 | 8 | variable "environment" { 9 | description = "Environment type: PRODUCTION or PRESTABLE" 10 | type = string 11 | default = "PRODUCTION" 12 | validation { 13 | condition = contains(["PRODUCTION", "PRESTABLE"], var.environment) 14 | error_message = "Release channel should be PRODUCTION (stable feature set) or PRESTABLE (early bird feature access)." 15 | } 16 | } 17 | 18 | variable "network_id" { 19 | description = "Network id of the PostgreSQL cluster" 20 | type = string 21 | } 22 | 23 | variable "description" { 24 | description = "PostgreSQL cluster description" 25 | type = string 26 | default = "Managed PostgreSQL cluster" 27 | } 28 | 29 | variable "folder_id" { 30 | description = "Yandex Cloud Folder ID where the cluster resides" 31 | type = string 32 | default = null 33 | } 34 | 35 | variable "labels" { 36 | description = "Set of label pairs to assing to the PostgreSQL cluster" 37 | type = map(any) 38 | default = {} 39 | } 40 | 41 | variable "host_master_name" { 42 | description = "Name of the master host." 43 | type = string 44 | default = null 45 | } 46 | 47 | variable "security_groups_ids_list" { 48 | description = "List of security group IDs to which the PostgreSQL cluster belongs" 49 | type = list(string) 50 | default = [] 51 | nullable = true 52 | } 53 | 54 | variable "deletion_protection" { 55 | description = "Protects the cluster from deletion" 56 | type = bool 57 | default = false 58 | } 59 | 60 | variable "pg_version" { 61 | description = "PostgreSQL version" 62 | type = string 63 | default = "15" 64 | validation { 65 | condition = contains(["13", "13-1c", "14", "14-1c", "15", "15-1c", "16", "16-1c", "17"], var.pg_version) 66 | error_message = "Allowed PostgreSQL versions are 13, 13-1c, 14, 14-1c, 15, 15-1c, 16, 16-1c, 17." 67 | } 68 | } 69 | 70 | variable "disk_size" { 71 | description = "Disk size for every cluster host" 72 | type = number 73 | default = 20 74 | } 75 | 76 | variable "disk_type" { 77 | description = "Disk type for all cluster hosts" 78 | type = string 79 | default = "network-ssd" 80 | } 81 | 82 | variable "resource_preset_id" { 83 | description = "Preset for hosts" 84 | type = string 85 | default = "s2.micro" 86 | } 87 | 88 | variable "access_policy" { 89 | description = "Access policy from other services to the PostgreSQL cluster." 90 | type = object({ 91 | data_lens = optional(bool, null) 92 | web_sql = optional(bool, null) 93 | serverless = optional(bool, null) 94 | data_transfer = optional(bool, null) 95 | }) 96 | default = {} 97 | } 98 | 99 | variable "restore_parameters" { 100 | description = <