├── .gitignore
├── README.md
├── create-database
├── README.md
├── db_objects.tf
├── extensions.tf
├── outputs.tf
├── variables.tf
└── versions.tf
├── create-users
├── README.md
├── db_users.tf
├── outputs.tf
├── variables.tf
└── versions.tf
├── docker-compose.yml
├── examples
├── README.md
├── all-in-one
│ ├── .envrc
│ ├── README.md
│ ├── gen-password-in-ps.sh
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── terraform.tfvars
│ └── variables.tf
├── create-users-on-existent-database
│ ├── .envrc
│ ├── .json
│ ├── README.md
│ ├── gen-password.sh
│ ├── main.tf
│ ├── providers.tf
│ ├── terraform.tfvars
│ └── variables.tf
├── full-rds-example
│ ├── README.md
│ ├── create-procedure-statistiques.sql
│ ├── create-tables.sql
│ ├── elasticsearch.tf
│ ├── gen-password-in-secretsmanager.py
│ ├── locals.tf
│ ├── outputs.tf
│ ├── policies
│ │ ├── lambda_policy.tpl
│ │ └── lambda_role.json
│ ├── postgresql.tf
│ ├── providers.tf
│ ├── rds.tf
│ ├── retrieve-audit-logs.sh
│ ├── terraform.tfvars
│ ├── terraform.tfvars.step5
│ ├── variables.tf
│ └── vpc.tf
└── simple-database
│ ├── .envrc
│ ├── README.md
│ ├── main.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── terraform.tfvars
│ └── variables.tf
└── schemas
├── Diagram-Relations.png
├── Diagram.excalidraw
├── ELK1.png
└── FakeApplication.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local .terraform directories
2 | **/.terraform/*
3 |
4 | # .tfstate files
5 | *.tfstate
6 | *.tfstate.*
7 | *.terraform.lock.hcl
8 |
9 | .idea
10 | .env
11 | /.bin
12 |
13 | .project
14 |
15 | *.zip
16 |
17 | **/trace.txt
18 |
19 | **/*Zone.Identifier
20 |
21 | gen-docs.sh
22 |
23 | examples/full-rds-example/.envrc
24 |
25 | **/builds/*
--------------------------------------------------------------------------------
/create-database/README.md:
--------------------------------------------------------------------------------
1 | ## Requirements
2 |
3 | | Name | Version |
4 | |------|---------|
5 | | [terraform](#requirement\_terraform) | >= 1.0.4 |
6 | | [null](#requirement\_null) | >= 3.0.0 |
7 | | [postgresql](#requirement\_postgresql) | >= 1.15.0 |
8 | | [random](#requirement\_random) | >= 3.0.0 |
9 |
10 | ## Providers
11 |
12 | | Name | Version |
13 | |------|---------|
14 | | [postgresql](#provider\_postgresql) | >= 1.15.0 |
15 |
16 | ## Modules
17 |
18 | No modules.
19 |
20 | ## Resources
21 |
22 | | Name | Type |
23 | |------|------|
24 | | [postgresql_database.db](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/database) | resource |
25 | | [postgresql_default_privileges.alter_defaults_privs](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/default_privileges) | resource |
26 | | [postgresql_extension.psql_extension](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/extension) | resource |
27 | | [postgresql_grant.grant_roles_schema](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
28 | | [postgresql_grant.privileges](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
29 | | [postgresql_grant.revoke_create_public](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
30 | | [postgresql_role.app_role_admin](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
31 | | [postgresql_role.app_roles](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
32 | | [postgresql_schema.schema](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/schema) | resource |
33 |
34 | ## Inputs
35 |
36 | | Name | Description | Type | Default | Required |
37 | |------|-------------|------|---------|:--------:|
38 | | [create\_database](#input\_create\_database) | Enable/Disable the creation of the database. Except for local tests or Cloud environment, the database creation is not possible. Disabled by default | `bool` | `false` | no |
39 | | [dbhost](#input\_dbhost) | The Postgresql Database Hostname | `string` | n/a | yes |
40 | | [dbport](#input\_dbport) | The Postgresql Database Port | `string` | n/a | yes |
41 | | [default\_superusers\_list](#input\_default\_superusers\_list) | List the super-users. By default, it's the postgres user. | `list(string)` |
[
"postgres"
]
| no |
42 | | [inputs](#input\_inputs) | The Inputs parameters for objects to create inside the database | object({
db_schema_name = string
db_name = string
db_admin = string
extensions = list(string)
db_roles = list(object({
id = string
role = string
inherit = bool
login = bool
validity = string
privileges = list(string)
createrole = bool
}))
db_grants = list(object({
object_type = string
privileges = list(string)
role = string
owner_role = string
objects = list(string)
grant_option = bool
}))
})
| `null` | no |
43 | | [pgadmin\_user](#input\_pgadmin\_user) | The Postgresql username | `string` | n/a | yes |
44 | | [revoke\_create\_public](#input\_revoke\_create\_public) | Enable/Disable the revoke command for create table in schema public | `bool` | `true` | no |
45 |
46 | ## Outputs
47 |
48 | No outputs.
49 |
--------------------------------------------------------------------------------
/create-database/db_objects.tf:
--------------------------------------------------------------------------------
1 |
2 | ########################################
3 | # Database Creation
4 | ########################################
5 | resource "postgresql_database" "db" {
6 | for_each = var.create_database ? toset([var.inputs["db_name"]]) : []
7 |
8 | name = var.inputs["db_name"]
9 | owner = var.inputs["db_admin"]
10 | template = "template0"
11 | encoding = "UTF8"
12 | lc_collate = "en_US.UTF-8"
13 | lc_ctype = "en_US.UTF-8"
14 | connection_limit = -1
15 | allow_connections = true
16 |
17 | depends_on = [
18 | postgresql_role.app_role_admin,
19 | ]
20 | }
21 |
22 | ########################################
23 | # Roles Creation
24 | ########################################
25 | # the adminsitration role
26 | resource "postgresql_role" "app_role_admin" {
27 | # because there is a dependency between the admin role used to be the owner of the objects (var.inputs["db_admin"]) and the database and the other roles,
28 | # we need to create this role first. Except when the role is a user that already exists, like when var.inputs["db_admin"] == 'postgres" by example.
29 | for_each = { for tuple in var.inputs["db_roles"] : tuple.role => tuple if tuple.role == var.inputs["db_admin"] && !contains(var.default_superusers_list, var.inputs["db_admin"]) }
30 |
31 |
32 | name = each.value.role
33 | login = each.value.login
34 | inherit = each.value.inherit
35 | valid_until = each.value.validity
36 | create_role = lookup(each.value, "createrole", false)
37 | roles = lookup(each.value, "membership", null)
38 | search_path = lookup(each.value, "search_path", null)
39 | }
40 |
41 | # other roles
42 | resource "postgresql_role" "app_roles" {
43 | # because there is a dependency between the admin role used to be the owner of the objects (var.inputs["db_admin"]) and the database and the other roles,
44 | # we need to create other roles in a second step, after the creation of the var.inputs["db_admin"]
45 | for_each = { for tuple in var.inputs["db_roles"] : tuple.role => tuple if tuple.role != var.inputs["db_admin"] }
46 |
47 | name = each.value.role
48 | login = each.value.login
49 | inherit = each.value.inherit
50 | valid_until = each.value.validity
51 | create_role = lookup(each.value, "createrole", false)
52 | roles = lookup(each.value, "membership", null)
53 | search_path = lookup(each.value, "search_path", null)
54 |
55 |
56 | provisioner "local-exec" {
57 | when = create
58 | environment = {
59 | PGHOST = var.dbhost
60 | PGPORT = var.dbport
61 | PGUSER = var.pgadmin_user
62 | PGAPPNAME = "terraform-psql"
63 | PGDATABASE = var.inputs["db_name"]
64 | }
65 | command = < tuple.privileges }
97 |
98 | database = var.inputs["db_name"]
99 | schema = var.inputs["db_schema_name"]
100 | role = each.key
101 | object_type = "schema"
102 | privileges = try(each.value, null)
103 |
104 | depends_on = [
105 | postgresql_role.app_roles,
106 | postgresql_database.db,
107 | postgresql_schema.schema,
108 | ]
109 | }
110 |
111 |
112 |
113 |
114 | ########################################
115 | # Creation of grants for each role
116 | ########################################
117 | resource "postgresql_grant" "privileges" {
118 |
119 | for_each = { for tuple in var.inputs["db_grants"] :
120 | join("_", [tuple.role, tuple.object_type, "privs", join(",",tuple.objects)]) => tuple if tuple.object_type != "type" }
121 |
122 | database = var.inputs["db_name"]
123 | schema = var.inputs["db_schema_name"]
124 | role = each.value.role
125 | objects = try(each.value.objects, [])
126 | object_type = each.value.object_type
127 | privileges = each.value.privileges
128 | with_grant_option = each.value.grant_option
129 |
130 | depends_on = [
131 | postgresql_role.app_roles,
132 | postgresql_database.db,
133 | postgresql_schema.schema,
134 | ]
135 | }
136 |
137 |
138 | ########################################
139 | # Update default privileges according to parameters setted in var.inputs
140 | ########################################
141 | resource "postgresql_default_privileges" "alter_defaults_privs" {
142 |
143 | for_each = { for tuple in var.inputs["db_grants"] :
144 | join("_", [tuple.role, tuple.object_type, "defaults", "privs", join(",",tuple.objects)]) => tuple if tuple.object_type != "database"
145 | }
146 |
147 | database = var.inputs["db_name"]
148 | schema = var.inputs["db_schema_name"]
149 | owner = each.value.owner_role
150 | role = each.value.role
151 | object_type = each.value.object_type
152 | privileges = each.value.privileges
153 |
154 | depends_on = [
155 | postgresql_grant.privileges,
156 | postgresql_role.app_roles,
157 | postgresql_schema.schema,
158 | postgresql_database.db,
159 | postgresql_grant.revoke_create_public
160 | ]
161 | }
162 |
163 | ########################################
164 | # REVOKE CREATE ON SCHEMA public FROM PUBLIC;
165 | # Because by default, the default privileges allow any user ("public")
166 | # to create table inside "public" schema
167 | ########################################
168 | resource "postgresql_grant" "revoke_create_public" {
169 |
170 | count = var.revoke_create_public ? 1 : 0
171 | database = var.inputs["db_name"]
172 | schema = "public"
173 | role = "public"
174 | object_type = "schema"
175 | privileges = []
176 |
177 | depends_on = [
178 | postgresql_schema.schema,
179 | postgresql_database.db,
180 | postgresql_grant.privileges
181 | ]
182 | }
--------------------------------------------------------------------------------
/create-database/extensions.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Extras Schemas Creation
3 | # + creation of grants for the role "app_releng_role" inside schema
4 | # here, we assume that the role "app_releng_role" is defined
5 | ########################################
6 | resource "postgresql_extension" "psql_extension" {
7 |
8 | for_each = toset(var.inputs["extensions"])
9 | name = each.key
10 |
11 | depends_on = [
12 | postgresql_role.app_roles,
13 | postgresql_database.db,
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/create-database/outputs.tf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jparnaudeau/terraform-postgresql-database-admin/ef34b2d22741907712c405a555013ff71da3fbab/create-database/outputs.tf
--------------------------------------------------------------------------------
/create-database/variables.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Provider vars
3 | ########################################
4 | variable "pgadmin_user" {
5 | type = string
6 | description = "The Postgresql username"
7 | }
8 |
9 | variable "dbhost" {
10 | type = string
11 | description = "The Postgresql Database Hostname"
12 | }
13 |
14 | variable "dbport" {
15 | type = string
16 | description = "The Postgresql Database Port"
17 | }
18 |
19 | ########################################
20 | # Input vars for Creating Objects inside Database
21 | ########################################
22 | variable "revoke_create_public" {
23 | type = bool
24 | description = "Enable/Disable the revoke command for create table in schema public"
25 | default = true
26 | }
27 |
28 | variable "create_database" {
29 | type = bool
30 | description = "Enable/Disable the creation of the database. Except for local tests or Cloud environment, the database creation is not possible. Disabled by default"
31 | default = false
32 | }
33 |
34 | variable "inputs" {
35 | type = object({
36 | db_schema_name = string
37 | db_name = string
38 | db_admin = string
39 | extensions = list(string)
40 | db_roles = list(object({
41 | id = string
42 | role = string
43 | inherit = bool
44 | login = bool
45 | validity = string
46 | privileges = list(string)
47 | createrole = bool
48 | }))
49 | db_grants = list(object({
50 | object_type = string
51 | privileges = list(string)
52 | role = string
53 | owner_role = string
54 | objects = list(string)
55 | grant_option = bool
56 | }))
57 | })
58 | description = "The Inputs parameters for objects to create inside the database"
59 | default = null
60 | }
61 |
62 | variable "default_superusers_list" {
63 | type = list(string)
64 | description = "List the super-users. By default, it's the postgres user."
65 | default = ["postgres"]
66 | }
67 |
--------------------------------------------------------------------------------
/create-database/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.0.4"
3 | required_providers {
4 | null = {
5 | source = "hashicorp/null"
6 | version = ">= 3.0.0"
7 | }
8 | postgresql = {
9 | source = "cyrilgdn/postgresql"
10 | version = ">= 1.15.0"
11 | }
12 | random = {
13 | source = "hashicorp/random"
14 | version = ">= 3.0.0"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/create-users/README.md:
--------------------------------------------------------------------------------
1 | ## Requirements
2 |
3 | | Name | Version |
4 | |------|---------|
5 | | [terraform](#requirement\_terraform) | >= 1.0.4 |
6 | | [null](#requirement\_null) | >= 3.0.0 |
7 | | [postgresql](#requirement\_postgresql) | >= 1.15.0 |
8 | | [random](#requirement\_random) | >= 3.0.0 |
9 |
10 | ## Providers
11 |
12 | | Name | Version |
13 | |------|---------|
14 | | [null](#provider\_null) | >= 3.0.0 |
15 | | [postgresql](#provider\_postgresql) | >= 1.15.0 |
16 |
17 | ## Modules
18 |
19 | No modules.
20 |
21 | ## Resources
22 |
23 | | Name | Type |
24 | |------|------|
25 | | [null_resource.pgusers_postprocessing_playbook](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
26 | | [postgresql_role.app_users](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
27 |
28 | ## Inputs
29 |
30 | | Name | Description | Type | Default | Required |
31 | |------|-------------|------|---------|:--------:|
32 | | [db\_users](#input\_db\_users) | The Inputs parameters for objects to create inside the database | list(object({
name = string
inherit = bool
login = bool
membership = list(string)
validity = string
connection_limit = number
createrole = bool
})
)
| `null` | no |
33 | | [dbhost](#input\_dbhost) | The RDS DB Hostname | `string` | n/a | yes |
34 | | [dbport](#input\_dbport) | The RDS DB Port | `string` | n/a | yes |
35 | | [passwords](#input\_passwords) | Map of credentials, = | `map(string)` | `{}` | no |
36 | | [pgadmin\_user](#input\_pgadmin\_user) | The RDS Master username | `string` | n/a | yes |
37 | | [postprocessing\_playbook\_params](#input\_postprocessing\_playbook\_params) | params for postprocessing playbook | object({
enable = bool
db_name = string
extra_envs = map(string)
shell_name = string
refresh_passwords = list(string)
})
| {
"db_name": "",
"enable": false,
"extra_envs": {},
"refresh_passwords": [],
"shell_name": ""
}
| no |
38 |
39 | ## Outputs
40 |
41 | | Name | Description |
42 | |------|-------------|
43 | | [db\_users](#output\_db\_users) | The list of users created by the module |
44 |
--------------------------------------------------------------------------------
/create-users/db_users.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Creation of users. a user is a role with
3 | # a permission to log in.
4 | ########################################
5 | resource "postgresql_role" "app_users" {
6 | for_each = { for tuple in var.db_users : tuple.name => tuple }
7 |
8 | name = each.value.name
9 | login = each.value.login
10 | roles = each.value.membership
11 | inherit = each.value.inherit
12 | valid_until = each.value.validity
13 | encrypted_password = true
14 | password = var.passwords[each.key]
15 | skip_drop_role = false
16 | skip_reassign_owned = false
17 | create_role = lookup(each.value, "createrole", false)
18 | connection_limit = each.value.connection_limit
19 | search_path = lookup(each.value, "search_path", null)
20 | }
21 |
22 | #######################################
23 | # modify postgres app_users (previously created) password
24 | # and update the corresponding parameter store value
25 | ########################################
26 | locals {
27 | postprocessing_users = var.postprocessing_playbook_params["enable"] ? var.db_users : []
28 | }
29 |
30 | resource "null_resource" "pgusers_postprocessing_playbook" {
31 | depends_on = [postgresql_role.app_users]
32 |
33 | for_each = { for tuple in local.postprocessing_users : tuple.name => tuple }
34 |
35 | triggers = {
36 | appuser_to_update = postgresql_role.app_users[each.key].name
37 | refresh_password = timestamp()
38 | }
39 |
40 | provisioner "local-exec" {
41 | when = create
42 | environment = merge({
43 | DBUSER = self.triggers.appuser_to_update
44 | PGHOST = var.dbhost
45 | PGPORT = var.dbport
46 | PGUSER = var.pgadmin_user
47 | PGDATABASE = var.postprocessing_playbook_params["db_name"]
48 | SHELL_TO_EXECUTE = var.postprocessing_playbook_params["shell_name"]
49 | REFRESH_PASSWORD = contains(var.postprocessing_playbook_params["refresh_passwords"], each.key) || try(var.postprocessing_playbook_params["refresh_passwords"][0], "") == "all"
50 | },
51 | var.postprocessing_playbook_params["extra_envs"]
52 | )
53 |
54 | command = < merge(tuple, { "password" = var.passwords[tuple.name] }) }
4 | sensitive = true
5 | }
6 |
--------------------------------------------------------------------------------
/create-users/variables.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Provider vars
3 | ########################################
4 | variable "pgadmin_user" {
5 | type = string
6 | description = "The RDS Master username"
7 | }
8 |
9 | variable "dbhost" {
10 | type = string
11 | description = "The RDS DB Hostname"
12 | }
13 |
14 | variable "dbport" {
15 | type = string
16 | description = "The RDS DB Port"
17 | }
18 |
19 | ########################################
20 | # passwords vars
21 | ########################################
22 | variable "passwords" {
23 | type = map(string)
24 | description = "Map of credentials, = "
25 | default = {}
26 | }
27 |
28 | ########################################
29 | # Input vars for creating users inside database
30 | ########################################
31 | variable "db_users" {
32 | type = list(object({
33 | name = string
34 | inherit = bool
35 | login = bool
36 | membership = list(string)
37 | validity = string
38 | connection_limit = number
39 | createrole = bool
40 | })
41 | )
42 | description = "The Inputs parameters for objects to create inside the database"
43 | default = null
44 | }
45 |
46 |
47 | ########################################
48 | # params used inside postprocessing playbook.
49 | # this playbook allows you to update in-fly the password and store it inside the secrets vault of your choice
50 | # for doing this, you need to :
51 | # enable : enable the postprocessing playbook. disable (false) by default.
52 | # db_name : the database name on which the user is created
53 | # shell_name : provide a shell that will be executed by the playbook. The playbook set environment variables :
54 | # - postgresql native environment variables : DBUSER, PGHOST, PGPORT, PGUSER, PGDATABASE
55 | # - any extra environment variables setted in extra_envs
56 | # extra_envs : a map containing extra environments variables that you want manipulate inside your shell.
57 | ########################################
58 | variable "postprocessing_playbook_params" {
59 | description = "params for postprocessing playbook"
60 | type = object({
61 | enable = bool
62 | db_name = string
63 | extra_envs = map(string)
64 | shell_name = string
65 | refresh_passwords = list(string)
66 | })
67 | default = {
68 | enable = false
69 | db_name = ""
70 | extra_envs = {}
71 | shell_name = ""
72 | refresh_passwords = []
73 | }
74 | }
--------------------------------------------------------------------------------
/create-users/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.0.4"
3 | required_providers {
4 | null = {
5 | source = "hashicorp/null"
6 | version = ">= 3.0.0"
7 | }
8 | postgresql = {
9 | source = "cyrilgdn/postgresql"
10 | #source = "terraform-providers/postgresql"
11 | version = ">= 1.15.0"
12 | }
13 | random = {
14 | source = "hashicorp/random"
15 | version = ">= 3.0.0"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Use postgres/example user/password credentials
2 | version: '3.1'
3 |
4 | networks:
5 | unNetwork:
6 | driver: bridge
7 |
8 |
9 | services:
10 |
11 | db:
12 | image: postgres:13.4
13 | restart: on-failure
14 | networks:
15 | - unNetwork
16 | ports:
17 | - 5432:5432
18 | # volumes:
19 | # - ./postgres-data:/var/lib/postgresql/data
20 | environment:
21 | POSTGRES_PASSWORD: password
22 |
23 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | |Example|UseCase|
4 | |-------|--------|
5 | |[simple-database](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/simple-database/README.md)|Demonstration How to create Database, Roles, and Grants objects.|
6 | |[create-users-on-existent-database](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/create-users-on-existent-database/README.md)|From an existent database, you can create several users. This usecase use a trivial postprocessing playbook for example. **DO NOT USE THIS PLAYBOOK IN PRODUCTION, IT's NOT SAFE.**|
7 | |[all-in-one](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/all-in-one/README.md)|Demonstration How to create Database, Roles, Users in one phase. This usecase use a postprocessing playbook that generate passwords, set password for each user, and store the password in the parameterStore into an AWS Account.|
8 | |[full-rds-example](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/full-rds-example/README.md)|Demonstration for other features covered by the module : Demonstrate an another postprocessing playbook that generate passwords into AWS SecretsManager, deploy the `pgaudit` extension for real-time monitoring, illustrates the `least privileges access` with deployment of roles & users, simulates a SOC with an `elasticsearch` that indexes rds logs.|
9 |
--------------------------------------------------------------------------------
/examples/all-in-one/.envrc:
--------------------------------------------------------------------------------
1 | export PGPASSWORD=password
2 | export AWS_PROFILE=ippon-sandbox
3 | export AWS_DEFAULT_REGION=eu-west-3
4 |
--------------------------------------------------------------------------------
/examples/all-in-one/README.md:
--------------------------------------------------------------------------------
1 | # all-in-one
2 |
3 | This example shows a complete real case. In this example, we will :
4 |
5 | * create the database, create the admin, write and readOnly roles.
6 |
7 | * create 3 users
8 |
9 | * generate passwords, update the password for each user, and store it into AWS ParameterStore.
10 |
11 |
12 | ## Prepare you postgresql provider
13 |
14 | ```hcl
15 |
16 | #######################################
17 | # Define Providers pgadm & pgmgm for postgresql
18 | #######################################
19 | provider "postgresql" {
20 | alias = "pgadm"
21 | host = var.dbhost
22 | port = var.dbport
23 | username = var.pgadmin_user
24 | sslmode = var.sslmode
25 | connect_timeout = var.connect_timeout
26 | superuser = var.superuser
27 | expected_version = var.expected_version
28 | }
29 |
30 | provider "postgresql" {
31 | alias = "pgmgm"
32 | host = var.dbhost
33 | port = var.dbport
34 | database = var.inputs["db_name"]
35 | username = var.pgadmin_user
36 | sslmode = var.sslmode
37 | connect_timeout = var.connect_timeout
38 | superuser = var.superuser
39 | expected_version = var.expected_version
40 | }
41 |
42 | ```
43 |
44 | Note : the password of the `var.pgadmin_user` are stored in the environment variable **PGPASSWORD** that you must setted before the terraform plan or apply.
45 |
46 | ## Prepare fake passwords in ParameterStore
47 |
48 | ```hcl
49 |
50 | ####################################################################
51 | # for each users defined in var.inputs, create
52 | # - a parameter in parameterStore for storing the user (path : /_user)
53 | # - create a fake password for this user and
54 | # - save it into parameterStore at /_password
55 | #
56 | # we do this for having only one case to manage in the postprocessing shell :
57 | # we update systematically the value of the parameter
58 | ####################################################################
59 | locals {
60 | namespace = format("/%s/%s",var.environment,var.inputs["db_name"])
61 | tags = merge(var.tags,{"environment" = var.environment})
62 | }
63 |
64 | # the ssm parameters for storing username
65 | module "ssm_db_users" {
66 | source = "jparnaudeau/ssm-parameter/aws"
67 | version = "1.0.0"
68 |
69 | for_each = { for user in var.inputs["db_users"] : user.name => user }
70 |
71 | namespace = local.namespace
72 | tags = local.tags
73 |
74 | parameters = {
75 | format("%s_user", each.key) = {
76 | description = "db user param value rds database"
77 | value = each.key
78 | overwrite = false
79 | },
80 | }
81 | }
82 |
83 | # the random passwords for each user
84 | resource "random_password" "passwords" {
85 | for_each = { for user in var.inputs["db_users"] : user.name => user }
86 |
87 | length = 16
88 | special = true
89 | upper = true
90 | lower = true
91 | min_upper = 1
92 | number = true
93 | min_numeric = 1
94 | min_special = 3
95 | override_special = "@#%&?"
96 | }
97 |
98 | # the ssm parameters for storing password of each user
99 | module "fake_user_password" {
100 | source = "jparnaudeau/ssm-parameter/aws"
101 | version = "1.0.0"
102 |
103 | for_each = { for user in var.inputs["db_users"] : user.name => user }
104 |
105 | namespace = local.namespace
106 | tags = local.tags
107 |
108 | parameters = {
109 | format("%s_password", each.key) = {
110 | description = "db user param value rds database"
111 | value = random_password.passwords[each.key].result
112 | type = "SecureString"
113 | overwrite = false
114 | },
115 | }
116 | }
117 |
118 | ```
119 |
120 | Notes :
121 |
122 | * here, we use an another submodule `ssm-parameter` that creates parameter in the parameterStore. Don't forget to set yours AWS Credentials by setting the variable **AWS_PROFILE**.
123 | * for each user, we create 2 parameters in the parameterStore : `/_user` and `/_password`
124 | * by creating the parameters before the postprocessing playbook, it simplifies the shell executed by the playbook.
125 |
126 |
127 | ## call the module to initialize the database and all objects (roles,grants)
128 |
129 | ```hcl
130 |
131 | ########################################
132 | # Initialize the database and the objects
133 | # (roles & grants), the default privileges
134 | ########################################
135 | module "initdb" {
136 |
137 | source = "jparnaudeau/database-admin/postgresql//create-database"
138 | version = "2.0.2"
139 |
140 |
141 | # set the provider
142 | providers = {
143 | postgresql = postgresql.pgadm
144 | }
145 |
146 | # targetted rds
147 | pgadmin_user = var.pgadmin_user
148 | dbhost = var.dbhost
149 | dbport = var.dbport
150 |
151 | # input parameters for creating database & objects inside database
152 | create_database = true
153 | inputs = var.inputs
154 | }
155 |
156 |
157 | ```
158 |
159 |
160 | ## call the module to create the users and use the postprocessing playbook to store passwords in parameterStore.
161 |
162 | ```hcl
163 |
164 | #########################################
165 | # Create the users inside the database
166 | #########################################
167 | # AWS Region
168 | data "aws_region" "current" {}
169 |
170 | module "create_users" {
171 | source = "jparnaudeau/database-admin/postgresql//create-users"
172 | version = "2.0.2"
173 |
174 | # need that all objects, managed inside the module "initdb", are created
175 | depends_on = [module.initdb]
176 |
177 | # set the provider
178 | providers = {
179 | postgresql = postgresql.pgadm
180 | }
181 |
182 | # targetted rds
183 | pgadmin_user = var.pgadmin_user
184 | dbhost = var.dbhost
185 | dbport = var.dbport
186 |
187 | # input parameters for creating users inside database
188 | db_users = var.inputs["db_users"]
189 |
190 | # set passwords
191 | passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
192 |
193 | # set postprocessing playbook
194 | postprocessing_playbook_params = {
195 | enable = true
196 | db_name = var.inputs["db_name"]
197 | extra_envs = {
198 | REGION = data.aws_region.current.name
199 | ENVIRONMENT = var.environment
200 | }
201 | refresh_passwords = ["all"]
202 | shell_name = "./gen-password-in-ps.sh"
203 | }
204 |
205 | }
206 |
207 | ```
208 |
209 | Note : note the "depends_on" on this module : the initialization of the database need to be done before creating users.
210 |
211 |
212 | ## Define the inputs
213 |
214 | in the `terraform.tfvars`, you could find :
215 |
216 | ```hcl
217 |
218 | # database and objects creation
219 | inputs = {
220 |
221 | # parameters used for creating database
222 | db_schema_name = "public"
223 | db_name = "mydatabase"
224 | db_admin = "app_admin_role" #owner of the database
225 |
226 | # install extensions if needed
227 | extensions = []
228 |
229 | # https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles/
230 | # 1) create Roles that are a set of permissions (named grant inside postgresql)
231 | # 2) set grants on role
232 | # 3) create User (these users have username/password) that inherits their permissions from the role.
233 | # You can retrieve the password from the parameterStore. cf shell gen-password-in-ps.sh
234 |
235 | # ---------------------------------- ROLES ------------------------------------------------------------------------------------
236 | # In this example, we create 3 roles
237 | # - "app_admin_role" will be the role used for creation, deletion, grant operations on objects, especially for tables.
238 | # - "app_write_role" for write operations. If you have a backend that insert lines into tables, it will used a user that inherits permissions from it.
239 | # - "app_readonly_role" for readonly operations.
240 | # Notes :
241 | # - "write" role does not have the permissions to create table.
242 | # - the 'createrole' field is a boolean that provides a way to create other roles and put grants on it. Be carefull when you give this permission.
243 | db_roles = [
244 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
245 | { id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
246 | { id = "write", role = "app_write_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
247 | ],
248 |
249 | # ---------------------------------- GRANT PERMISSIONS ON ROLES ------------------------------------------------------------------------------------
250 | # you could find the available privileges on official postgresql doc : https://www.postgresql.org/docs/13/ddl-priv.html
251 | # Notes :
252 | # - "role" corresponds to the role on which the grants will be applied.
253 | # - "owner_role" is the role used to create grants on "role".
254 | # - object_type = "type" is used only for default privileges
255 | # - objects = [] means "all". Use this attribut if you want to allow permissions on specific tables, functions, procedures, sequences. Concept of Least privileges
256 | # - object_type = "type" is used only for default privileges
257 |
258 | db_grants = [
259 | # role app_admin_role : define grants to apply on db 'mydatabase', schema 'public'
260 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
261 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
262 |
263 | # role app_readonly_role : define grant to apply on db 'mydatabase', schema 'public'
264 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
265 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = true },
266 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
267 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
268 |
269 | # role app_write_role : define grant to apply on db 'mydatabase', schema 'public'
270 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
271 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = true },
272 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
273 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
274 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
275 |
276 | ],
277 |
278 | db_users = [
279 | { name = "readonly", inherit = true, login = true, membership = ["app_readonly_role"], validity = "infinity", connection_limit = -1, createrole = false },
280 | { name = "backend", inherit = true, login = true, membership = ["app_write_role"], validity = "infinity", connection_limit = -1, createrole = false },
281 | { name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = false },
282 | ]
283 | }
284 |
285 | # set tags & environment
286 | environment = "test"
287 | tags = {
288 | createdBy = "terraform"
289 | }
290 |
291 | ```
292 |
293 | ## Allowed UseCase Matrix
294 |
295 | Based on those inputs, this is the matrix providing permissions for the different users defined in this example :
296 |
297 | |DDB User|Login on database|Create/Drop Database|Create/Drop Schema|Create/Drop Role|Create/Drop Table|Insert/Delete items in Table|Select on table|
298 | |--------|-----------------|--------------------|------------------|----------------|-----------------|----------------------------|---------------|
299 | |postgres|OK | OK| OK| OK| OK| OK| OK|
300 | |admin | OK| OK| OK|OK (By default can't create role)|OK|OK|OK|
301 | |backend |OK |OK (Permission denied)|OK (Permission denied)|OK (Permission denied)|OK (Permission denied)|OK|OK|
302 | |readonly|OK |OK (Permission denied)|OK (Permission denied)|OK (Permission denied)|OK (Permission denied)|OK (Permission denied)|OK|
303 |
304 | Note : you can allow the user `admin` to create role, by using the field **createrole** in the **db_users** declaration.
305 |
306 |
307 |
308 | ## script used by the postprocessing playbook
309 |
310 | The postprocessing playbook generates a set of environments variables :
311 |
312 | * Native postgresql environment variables : PGHOST, PGPORT, PGUSER, PGDATABASE. So you can use it inside your shell.
313 | * a variable `DBUSER` representing the user that we want update his password.
314 | * a variable `REFRESH_PASSWORD` if you want control the execution of the update.
315 | * all extra variables defined in `extra_envs`.
316 |
317 | ```
318 |
319 | #!/bin/bash
320 |
321 | if [ "${REFRESH_PASSWORD}" == "true" ]
322 | then
323 |
324 | # generate a random password
325 | USERPWD=$(openssl rand -base64 16 |tr -d '[;+%$!/]');
326 |
327 | # generate the parameterStore path
328 | USER_PWD_PATH="/${ENVIRONMENT}/${PGDATABASE}/${DBUSER}_password"
329 |
330 | # Alter user inside postgresql database
331 | psql -c "ALTER USER $DBUSER WITH PASSWORD '$USERPWD'";
332 |
333 | # Alter Secret Storage
334 | aws ssm put-parameter --name $USER_PWD_PATH --type SecureString --overwrite --value $USERPWD --region $REGION;
335 |
336 | fi
337 |
338 | exit 0
339 |
340 | ```
341 |
342 | Notes :
343 |
344 | * By using a direct call on the api aws ssm put-parameter (and not using the terraform resource), we assure that the password is not stored into clear text in the tfstate.
345 | * note the use of the variable `REGION`, setted in the map extra_envs in the main.tf.
346 |
347 | ## To summarize
348 |
349 | launch `terraform apply --auto-approve`
350 |
351 | ```
352 |
353 | Outputs:
354 |
355 | affected_schema = "public"
356 | connect_string = "psql -h localhost -p 5432 -U app_admin_role -d mydatabase"
357 | created_database = "mydatabase"
358 | created_roles = [
359 | "app_admin_role",
360 | "app_readonly_role",
361 | "app_write_role",
362 | ]
363 | db_users = {
364 | "admin" = {
365 | "connect_command" = "psql -h localhost -p 5432 -U admin -d mydatabase -W"
366 | "parameter_store_user" = "/test/mydatabase/admin_user"
367 | "parameter_store_user_password" = "/test/mydatabase/admin_password"
368 | }
369 | "backend" = {
370 | "connect_command" = "psql -h localhost -p 5432 -U backend -d mydatabase -W"
371 | "parameter_store_user" = "/test/mydatabase/backend_user"
372 | "parameter_store_user_password" = "/test/mydatabase/backend_password"
373 | }
374 | "readonly" = {
375 | "connect_command" = "psql -h localhost -p 5432 -U readonly -d mydatabase -W"
376 | "parameter_store_user" = "/test/mydatabase/readonly_user"
377 | "parameter_store_user_password" = "/test/mydatabase/readonly_password"
378 | }
379 | }
380 |
381 | ```
382 |
383 |
384 | Connect with the admin user to create table
385 |
386 | ```
387 |
388 | psql -h localhost -p 5432 -U admin -d mydatabase -W
389 | Password:
390 |
391 | psql (12.8 (Ubuntu 12.8-0ubuntu0.20.04.1), server 13.4 (Debian 13.4-4.pgdg110+1))
392 | WARNING: psql major version 12, server major version 13.
393 | Some psql features might not work.
394 | Type "help" for help.
395 |
396 | mydatabase=> create table table1(col1 TEXT);
397 | CREATE TABLE
398 | mydatabase=> \q
399 |
400 | ```
401 |
402 | Connect with the backend user to insert line into this table
403 |
404 | ```
405 |
406 | psql -h localhost -p 5432 -U backend -d mydatabase -W
407 | Password:
408 |
409 | psql (12.8 (Ubuntu 12.8-0ubuntu0.20.04.1), server 13.4 (Debian 13.4-4.pgdg110+1))
410 | WARNING: psql major version 12, server major version 13.
411 | Some psql features might not work.
412 | Type "help" for help.
413 |
414 | mydatabase=> insert into table1 values ('first line');
415 | ERROR: permission denied for table table1
416 |
417 | ```
418 |
419 | * It's normal, we need to re-execute the terraform apply to propage permissions on this new table
420 | * be carefull to pass the refresh_passwords to [""] if you don't want regenerate new password.
421 |
422 |
423 | ```
424 |
425 | terraform apply --auto-approve
426 |
427 | ...
428 |
429 | # Test with backend user
430 | psql -h localhost -p 5432 -U backend -d mydatabase -W
431 |
432 | Password:
433 | psql (12.8 (Ubuntu 12.8-0ubuntu0.20.04.1), server 13.4 (Debian 13.4-4.pgdg110+1))
434 | WARNING: psql major version 12, server major version 13.
435 | Some psql features might not work.
436 | Type "help" for help.
437 |
438 | mydatabase=> insert into table1 values ('first line');
439 | INSERT 0 1
440 |
441 | ```
442 |
443 | Test the permissions for readonly user :
444 |
445 | ```
446 |
447 | psql -h localhost -p 5432 -U readonly -d mydatabase -W
448 | Password:
449 | psql (12.8 (Ubuntu 12.8-0ubuntu0.20.04.1), server 13.4 (Debian 13.4-4.pgdg110+1))
450 | WARNING: psql major version 12, server major version 13.
451 | Some psql features might not work.
452 | Type "help" for help.
453 |
454 | mydatabase=> select * from table1;
455 | col1
456 | ------------
457 | first line
458 | (1 row)
459 |
460 | mydatabase=> create table table2(col1 TEXT);
461 | ERROR: permission denied for schema public
462 | LINE 1: create table table2(col1 TEXT);
463 |
464 | ```
465 |
--------------------------------------------------------------------------------
/examples/all-in-one/gen-password-in-ps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "${REFRESH_PASSWORD}" == "true" ]
4 | then
5 |
6 | # generate a random password
7 | USERPWD=$(openssl rand -base64 16 |tr -d '[;+%$!/]');
8 |
9 | # generate the parameterStore path
10 | USER_PWD_PATH="/${ENVIRONMENT}/${PGDATABASE}/${DBUSER}_password"
11 |
12 | # Alter user inside postgresql database
13 | psql -c "ALTER USER $DBUSER WITH PASSWORD '$USERPWD'";
14 |
15 | # Alter Secret Storage
16 | aws ssm put-parameter --name $USER_PWD_PATH --type SecureString --overwrite --value $USERPWD --region $REGION;
17 |
18 | fi
19 |
20 | exit 0
21 |
--------------------------------------------------------------------------------
/examples/all-in-one/main.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Initialize the database and the objects
3 | # (roles & grants), the default privileges
4 | ########################################
5 | module "initdb" {
6 |
7 | source = "../../create-database"
8 |
9 | # set the provider
10 | providers = {
11 | postgresql = postgresql.pgadm
12 | }
13 |
14 | # targetted rds
15 | pgadmin_user = var.pgadmin_user
16 | dbhost = var.dbhost
17 | dbport = var.dbport
18 |
19 | # input parameters for creating database & objects inside database
20 | create_database = true
21 | inputs = var.inputs
22 | }
23 |
24 | ####################################################################
25 | # for each users defined in var.inputs, create
26 | # - a parameter in parameterStore for storing the user (path : /_user)
27 | # - create a fake password for this user and
28 | # - save it into parameterStore at /_password
29 | #
30 | # we do this for having only one case to manage in the postprocessing shell :
31 | # we update systematically the value of the parameter
32 | ####################################################################
33 | locals {
34 | namespace = format("/%s/%s", var.environment, var.inputs["db_name"])
35 | tags = merge(var.tags, { "environment" = var.environment })
36 | }
37 |
38 | # the ssm parameters for storing username
39 | module "ssm_db_users" {
40 | source = "jparnaudeau/ssm-parameter/aws"
41 | version = "1.0.0"
42 |
43 | for_each = { for user in var.inputs["db_users"] : user.name => user }
44 |
45 | namespace = local.namespace
46 | tags = local.tags
47 |
48 | parameters = {
49 | format("%s_user", each.key) = {
50 | description = "db user param value rds database"
51 | value = each.key
52 | overwrite = false
53 | },
54 | }
55 | }
56 |
57 | # the random passwords for each user
58 | resource "random_password" "passwords" {
59 | for_each = { for user in var.inputs["db_users"] : user.name => user }
60 |
61 | length = 16
62 | special = true
63 | upper = true
64 | lower = true
65 | min_upper = 1
66 | number = true
67 | min_numeric = 1
68 | min_special = 3
69 | override_special = "@#%&?"
70 | }
71 |
72 | # the ssm parameters for storing password of each user
73 | module "fake_user_password" {
74 | source = "jparnaudeau/ssm-parameter/aws"
75 | version = "1.0.0"
76 |
77 | for_each = { for user in var.inputs["db_users"] : user.name => user }
78 |
79 | namespace = local.namespace
80 | tags = local.tags
81 |
82 | parameters = {
83 | format("%s_password", each.key) = {
84 | description = "db user param value rds database"
85 | value = random_password.passwords[each.key].result
86 | type = "SecureString"
87 | overwrite = false
88 | },
89 | }
90 | }
91 |
92 | #########################################
93 | # Create the users inside the database
94 | #########################################
95 | # AWS Region
96 | data "aws_region" "current" {}
97 |
98 | module "create_users" {
99 | source = "../../create-users"
100 |
101 | # need that all objects, managed inside the module "initdb", are created
102 | depends_on = [module.initdb]
103 |
104 | # set the provider
105 | providers = {
106 | postgresql = postgresql.pgadm
107 | }
108 |
109 | # targetted rds
110 | pgadmin_user = var.pgadmin_user
111 | dbhost = var.dbhost
112 | dbport = var.dbport
113 |
114 | # input parameters for creating users inside database
115 | db_users = var.inputs["db_users"]
116 |
117 | # set passwords
118 | passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
119 |
120 | # set postprocessing playbook
121 | postprocessing_playbook_params = {
122 | enable = true
123 | db_name = var.inputs["db_name"]
124 | extra_envs = {
125 | REGION = data.aws_region.current.name
126 | ENVIRONMENT = var.environment
127 | }
128 | refresh_passwords = ["all"]
129 | shell_name = "./gen-password-in-ps.sh"
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/examples/all-in-one/outputs.tf:
--------------------------------------------------------------------------------
1 | output "created_database" {
2 | description = "the name of the database created by the module"
3 | value = var.inputs["db_name"]
4 | }
5 |
6 | output "affected_schema" {
7 | description = "the name of the schema in which the db objects have been created by the module"
8 | value = var.inputs["db_schema_name"]
9 | }
10 |
11 | output "created_roles" {
12 | description = "The list of roles created by the module"
13 | value = [for obj_role in var.inputs["db_roles"] : obj_role["role"]]
14 | }
15 |
16 | output "connect_string" {
17 | description = "The connect string to use to connect on the database"
18 | value = format("psql -h %s -p %s -U %s -d %s", var.dbhost, var.dbport, var.inputs["db_admin"], var.inputs["db_name"])
19 | }
20 |
21 | output "db_users" {
22 | description = "The list of users created by the module"
23 | value = { for user in var.inputs["db_users"] :
24 | user.name => {
25 | "parameter_store_user" = format("%s/%s_user", local.namespace, user.name),
26 | "parameter_store_user_password" = format("%s/%s_password", local.namespace, user.name),
27 | "connect_command" = format("psql -h %s -p %s -U %s -d %s -W", var.dbhost, var.dbport, user.name, var.inputs["db_name"])
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/all-in-one/providers.tf:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Define Providers pgadm & pgmgm for postgresql
3 | #######################################
4 | provider "postgresql" {
5 | alias = "pgadm"
6 | host = var.dbhost
7 | port = var.dbport
8 | username = var.pgadmin_user
9 | sslmode = var.sslmode
10 | connect_timeout = var.connect_timeout
11 | superuser = var.superuser
12 | expected_version = var.expected_version
13 | }
14 |
15 | provider "postgresql" {
16 | alias = "pgmgm"
17 | host = var.dbhost
18 | port = var.dbport
19 | database = var.inputs["db_name"]
20 | username = var.pgadmin_user
21 | sslmode = var.sslmode
22 | connect_timeout = var.connect_timeout
23 | superuser = var.superuser
24 | expected_version = var.expected_version
25 | }
26 |
27 |
28 | #######################################
29 | # Manage version of providers
30 | #######################################
31 | terraform {
32 | required_version = ">= 1.0.4"
33 | required_providers {
34 | postgresql = {
35 | source = "cyrilgdn/postgresql"
36 | version = ">= 1.15.0"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/all-in-one/terraform.tfvars:
--------------------------------------------------------------------------------
1 | # provider connection infos
2 | pgadmin_user = "postgres"
3 | dbhost = "localhost"
4 | expected_version = "12.0.0"
5 | sslmode = "disable"
6 |
7 | # database and objects creation
8 | inputs = {
9 |
10 | # parameters used for creating database
11 | db_schema_name = "public"
12 | db_name = "mydatabase"
13 | db_admin = "app_admin_role" #owner of the database
14 |
15 | # install extensions if needed
16 | extensions = []
17 |
18 | # https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles/
19 | # 1) create Roles that are a set of permissions (named grant inside postgresql)
20 | # 2) set grants on role
21 | # 3) create User (these users have username/password) that inherits their permissions from the role.
22 | # You can retrieve the password from the parameterStore. cf shell gen-password-in-ps.sh
23 |
24 | # ---------------------------------- ROLES ------------------------------------------------------------------------------------
25 | # In this example, we create 3 roles
26 | # - "app_admin_role" will be the role used for creation, deletion, grant operations on objects, especially for tables.
27 | # - "app_write_role" for write operations. If you have a backend that insert lines into tables, it will used a user that inherits permissions from it.
28 | # - "app_readonly_role" for readonly operations.
29 | # Notes :
30 | # - "write" role does not have the permissions to create table.
31 | # - the 'createrole' field is a boolean that provides a way to create other roles and put grants on it. Be carefull when you give this permission.
32 | db_roles = [
33 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
34 | { id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
35 | { id = "write", role = "app_write_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
36 | ],
37 |
38 | # ---------------------------------- GRANT PERMISSIONS ON ROLES ------------------------------------------------------------------------------------
39 | # you could find the available privileges on official postgresql doc : https://www.postgresql.org/docs/13/ddl-priv.html
40 | # Notes :
41 | # - "role" corresponds to the role on which the grants will be applied.
42 | # - "owner_role" is the role used to create grants on "role".
43 | # - object_type = "type" is used only for default privileges
44 | # - objects = [] means "all". Use this attribut if you want to allow permissions on specific tables, functions, procedures, sequences. Concept of Least privileges
45 | # - object_type = "type" is used only for default privileges
46 |
47 | db_grants = [
48 | # role app_admin_role : define grants to apply on db 'mydatabase', schema 'public'
49 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
50 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
51 |
52 | # role app_readonly_role : define grant to apply on db 'mydatabase', schema 'public'
53 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
54 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = true },
55 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
56 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
57 |
58 | # role app_write_role : define grant to apply on db 'mydatabase', schema 'public'
59 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
60 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = true },
61 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
62 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
63 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
64 |
65 | ],
66 |
67 | db_users = [
68 | { name = "readonly", inherit = true, login = true, membership = ["app_readonly_role"], validity = "infinity", connection_limit = -1, createrole = false },
69 | { name = "backend", inherit = true, login = true, membership = ["app_write_role"], validity = "infinity", connection_limit = -1, createrole = false },
70 | { name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = false },
71 | ]
72 |
73 | }
74 |
75 | # set tags & environment
76 | environment = "test"
77 | tags = {
78 | createdBy = "terraform"
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/examples/all-in-one/variables.tf:
--------------------------------------------------------------------------------
1 | variable "dbhost" {
2 | type = string
3 | default = "localhost"
4 | description = "The database host"
5 | }
6 |
7 | variable "dbport" {
8 | type = number
9 | default = 5432
10 | description = "The database port"
11 | }
12 |
13 | variable "pgadmin_user" {
14 | type = string
15 | description = "The RDS user to used for creating/managing other user in the database."
16 | }
17 |
18 | variable "sslmode" {
19 | type = string
20 | description = "Set the priority for an SSL connection to the server. Valid values are [disable,require,verify-ca,verify-full]"
21 | default = "require"
22 | }
23 |
24 | variable "connect_timeout" {
25 | type = number
26 | description = "Maximum wait for connection, in seconds. The default is 180s. Zero or not specified means wait indefinitely."
27 | default = 180
28 | }
29 |
30 | variable "superuser" {
31 | type = bool
32 | description = "Should be set to false if the user to connect is not a PostgreSQL superuser"
33 | default = false
34 | }
35 |
36 | variable "expected_version" {
37 | type = string
38 | description = "Specify a hint to Terraform regarding the expected version that the provider will be talking with. This is a required hint in order for Terraform to talk with an ancient version of PostgreSQL. This parameter is expected to be a PostgreSQL Version or current. Once a connection has been established, Terraform will fingerprint the actual version. Default: 9.0.0"
39 | default = "9.0.0"
40 | }
41 |
42 | variable "inputs" {
43 | type = any
44 | description = "The map containing all elements for creating objects inside database"
45 | default = null
46 | }
47 |
48 | variable "tags" {
49 | type = map(string)
50 | description = "a map of string used to tag entries in AWS Secrets Manager"
51 | default = {}
52 | }
53 |
54 | variable "environment" {
55 | type = string
56 | description = "environment name"
57 | default = "sta"
58 | }
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/.envrc:
--------------------------------------------------------------------------------
1 | export PGPASSWORD=password
2 | export AWS_PROFILE=ippon-sandbox
3 | export AWS_DEFAULT_REGION=eu-west-3
4 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/.json:
--------------------------------------------------------------------------------
1 | {password: MMWQ4brgDxslppbVL0Sclw==}
2 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/README.md:
--------------------------------------------------------------------------------
1 | # create-users-on-existent-database
2 |
3 | This example shows you how to create users after a clean initialisation of a database i.e, with roles created in the example [simple-database](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/simple-database).
4 |
5 | You can find a complete example for creating database, roles and users in the example [all-in-one](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/all-in-one).
6 |
7 | This example provide a first illustration to "How to set password" with the postprocessing playbook.
8 |
9 | ## Prepare you postgresql provider
10 |
11 | ```hcl
12 |
13 | #######################################
14 | # Define Providers pgadm & pgmgm for postgresql
15 | #######################################
16 | provider "postgresql" {
17 | alias = "pgadm"
18 | host = var.dbhost
19 | port = var.dbport
20 | username = var.pgadmin_user
21 | sslmode = var.sslmode
22 | connect_timeout = var.connect_timeout
23 | superuser = var.superuser
24 | expected_version = var.expected_version
25 | }
26 |
27 | provider "postgresql" {
28 | alias = "pgmgm"
29 | host = var.dbhost
30 | port = var.dbport
31 | database = var.inputs["db_name"]
32 | username = var.pgadmin_user
33 | sslmode = var.sslmode
34 | connect_timeout = var.connect_timeout
35 | superuser = var.superuser
36 | expected_version = var.expected_version
37 | }
38 |
39 | ```
40 |
41 | Note : the password of the `var.pgadmin_user` are stored in the environment variable **PGPASSWORD** that you must setted before the terraform plan or apply.
42 |
43 | ## Call the module
44 |
45 | ```hcl
46 |
47 | #######################################
48 | # Create Random Passwords for each user
49 | #######################################
50 | resource "random_password" "passwords" {
51 | for_each = { for user in var.inputs["db_users"] : user.name => user }
52 |
53 | length = 16
54 | special = true
55 | upper = true
56 | lower = true
57 | min_upper = 1
58 | number = true
59 | min_numeric = 1
60 | min_special = 3
61 | override_special = "@#%&?"
62 | }
63 |
64 |
65 | #########################################
66 | # Create the users inside the database
67 | #########################################
68 | module "create_users" {
69 |
70 | source = "jparnaudeau/database-admin/postgresql//create-users"
71 | version = "2.0.0"
72 |
73 | # set the provider
74 | providers = {
75 | postgresql = postgresql.pgadm
76 | }
77 |
78 | # targetted rds
79 | pgadmin_user = var.pgadmin_user
80 | dbhost = var.dbhost
81 | dbport = var.dbport
82 |
83 | # input parameters for creating users inside database
84 | db_users = var.inputs["db_users"]
85 |
86 | # set passwords
87 | passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
88 |
89 | # set postprocessing playbook
90 | postprocessing_playbook_params = var.postprocessing_playbook_params
91 |
92 | }
93 |
94 |
95 | ```
96 |
97 | Note : we use terraform resource `random_password` to initialize passwords, but the real passwords are setted by the postprocessing playbook. So even if the value of random_password are in clear text in the tfstate, the real passwords are not stored in the tfstate.
98 |
99 |
100 | ## Define the inputs
101 |
102 | in the `terraform.tfvars`, you could find :
103 |
104 | ```hcl
105 |
106 | inputs = {
107 |
108 | # ---------------------------------- USER ------------------------------------------------------------------------------------
109 | # finally, we create :
110 | # - a human user with the readonly permission and an expiration date (for troubelshooting by example)
111 | # - a user for a reporting application that requires only readonly permissions
112 | # - a user for a backend application that requires write permissions
113 | #
114 | # Regarding passwords, it's the shell "gen-password.sh" executed in the postprocessing playbook that in charge to set password for each user.
115 | db_users = [
116 | { name = "audejavel", inherit = true, login = true, membership = ["app_readonly_role"], validity = "2021-12-31 00:00:00+00", connection_limit = -1, createrole = false },
117 | { name = "reporting", inherit = true, login = true, membership = ["app_readonly_role"], validity = "infinity", connection_limit = -1, createrole = false },
118 | { name = "backend", inherit = true, login = true, membership = ["app_write_role"], validity = "infinity", connection_limit = -1, createrole = false },
119 | ]
120 |
121 | }
122 |
123 | ```
124 |
125 | # Define the passwords with the postprocessing playbook
126 |
127 | in the `terraform.tfvars`, you could find :
128 |
129 | ```hcl
130 |
131 | # for post processing
132 | postprocessing_playbook_params = {
133 | enable = true
134 | db_name = "mydatabase"
135 | extra_envs = {
136 | REGION="paris"
137 | }
138 | refresh_passwords = ["all"]
139 | shell_name = "./gen-password.sh"
140 | }
141 |
142 | ```
143 |
144 | The different parameters available in the object `postprocessing_playbook_params` are :
145 |
146 | * **enable** : you need to enable the postprocessing playbook execution. If by example, you prepare passwords in a secure way, by example in an encrypted file, you can use a terraform datasource to read this file (see this [post](https://blog.gruntwork.io/a-comprehensive-guide-to-managing-secrets-in-your-terraform-code-1d586955ace1) ), you can pass directly the passwords into the module without the need to execute the postprocessing playbook. Otherwise, enable it.
147 | * **db_name** : set the name of the database in which the users are related.
148 | * **extra_envs** : you can pass extra environment variables that are available inside your script.
149 | * **refresh_passwords** : you can force the execution of the postprocessing playbook for particular passwords. Just set in this field, the list of users for which you want a new password. In this case, a variable **REFRESH_PASSWORD** will be setted to `true`. Keep `all` if you want systematically regenerate new password for each user.
150 | * **shell_name** : it's your responsability to write a shell that generate passwords, update the user in the postgresql database, and store it in a safe place.
151 |
152 |
153 | # a dummy script used by the postprocessing playbook
154 |
155 | The postprocessing playbook put the native postgresql environment variables : DBUSER, PGHOST, PGPORT, PGUSER, PGDATABASE. So you can use it inside your shell.
156 |
157 | ```
158 |
159 | #!/bin/bash
160 |
161 |
162 | if [ "${REFRESH_PASSWORD}" == "true" ]
163 | then
164 |
165 | # generate a random password
166 | USERPWD=$(openssl rand -base64 16 |tr -d '[;+%$!/]');
167 |
168 | # Alter user inside postgresql database
169 | psql -c "ALTER USER $DBUSER WITH PASSWORD '$USERPWD'";
170 |
171 | # Alter Secret Storage
172 | echo "{password: $USERPWD}" > ./$DBUSER.json
173 |
174 | fi
175 |
176 | exit 0
177 |
178 | ```
179 |
180 | As you can see, we generate a random password and store the password in a file !! DO NOT DO THIS IN PRODUCTION !!. You can find a real secure script in the [all-in-one](https://github.com/jparnaudeau/terraform-postgresql-database-admin/tree/master/examples/all-in-one) example.
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/gen-password.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | if [ "${REFRESH_PASSWORD}" == "true" ]
5 | then
6 |
7 | # generate a random password
8 | USERPWD=$(openssl rand -base64 16 |tr -d '[;+%$!/]');
9 |
10 | # Alter user inside postgresql database
11 | psql -c "ALTER USER $DBUSER WITH PASSWORD '$USERPWD'";
12 |
13 | # Alter Secret Storage
14 | echo "{password: $USERPWD}" > ./$DBUSER.json
15 |
16 | fi
17 |
18 | exit 0
19 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/main.tf:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Create Random Passwords for each user
3 | #######################################
4 | resource "random_password" "passwords" {
5 | for_each = { for user in var.inputs["db_users"] : user.name => user }
6 |
7 | length = 16
8 | special = true
9 | upper = true
10 | lower = true
11 | min_upper = 1
12 | number = true
13 | min_numeric = 1
14 | min_special = 3
15 | override_special = "@#%&?"
16 | }
17 |
18 |
19 | #########################################
20 | # Create the users inside the database
21 | #########################################
22 | module "create_users" {
23 | source = "../../create-users"
24 |
25 |
26 | # set the provider
27 | providers = {
28 | postgresql = postgresql.pgadm
29 | }
30 |
31 | # targetted rds
32 | pgadmin_user = var.pgadmin_user
33 | dbhost = var.dbhost
34 | dbport = var.dbport
35 |
36 | # input parameters for creating users inside database
37 | db_users = var.inputs["db_users"]
38 |
39 | # set passwords
40 | passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
41 |
42 | # set postprocessing playbook
43 | postprocessing_playbook_params = var.postprocessing_playbook_params
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/providers.tf:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Define Providers pgadm & pgmgm for postgresql
3 | #######################################
4 | provider "postgresql" {
5 | alias = "pgadm"
6 | host = var.dbhost
7 | port = var.dbport
8 | username = var.pgadmin_user
9 | sslmode = var.sslmode
10 | connect_timeout = var.connect_timeout
11 | superuser = var.superuser
12 | expected_version = var.expected_version
13 | }
14 |
15 | provider "postgresql" {
16 | alias = "pgmgm"
17 | host = var.dbhost
18 | port = var.dbport
19 | database = var.inputs["db_name"]
20 | username = var.pgadmin_user
21 | sslmode = var.sslmode
22 | connect_timeout = var.connect_timeout
23 | superuser = var.superuser
24 | expected_version = var.expected_version
25 | }
26 |
27 |
28 | #######################################
29 | # Manage version of providers
30 | #######################################
31 | terraform {
32 | required_version = ">= 1.0.4"
33 | required_providers {
34 | postgresql = {
35 | source = "cyrilgdn/postgresql"
36 | version = ">= 1.15.0"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/terraform.tfvars:
--------------------------------------------------------------------------------
1 | # provider connection infos
2 | pgadmin_user = "postgres"
3 | dbhost = "localhost"
4 | sslmode = "disable"
5 |
6 | # for post processing
7 | postprocessing_playbook_params = {
8 | enable = true
9 | db_name = "mydatabase"
10 | extra_envs = {
11 | REGION = "paris"
12 | }
13 | refresh_passwords = ["all"]
14 | shell_name = "./gen-password.sh"
15 | }
16 |
17 | inputs = {
18 |
19 | # ---------------------------------- USER ------------------------------------------------------------------------------------
20 | # finally, we create :
21 | # - a human user with the readonly permission and an expiration date (for troubelshooting by example)
22 | # - a user for a reporting application that requires only readonly permissions
23 | # - a user for a backend application that requires write permissions
24 | #
25 | # Regarding passwords, it's the shell "gen-password.sh" executed in the postprocessing playbook that in charge to set password for each user.
26 | db_users = [
27 | { name = "audejavel", inherit = true, login = true, membership = ["app_readonly_role"], validity = "2021-12-31 00:00:00+00", connection_limit = -1, createrole = false },
28 | { name = "reporting", inherit = true, login = true, membership = ["app_readonly_role"], validity = "infinity", connection_limit = -1, createrole = false },
29 | { name = "backend", inherit = true, login = true, membership = ["app_write_role"], validity = "infinity", connection_limit = -1, createrole = false },
30 | ]
31 |
32 | }
33 |
34 |
35 |
--------------------------------------------------------------------------------
/examples/create-users-on-existent-database/variables.tf:
--------------------------------------------------------------------------------
1 | variable "dbhost" {
2 | type = string
3 | default = "localhost"
4 | description = "The database host"
5 | }
6 |
7 | variable "dbport" {
8 | type = number
9 | default = 5432
10 | description = "The database port"
11 | }
12 |
13 | variable "pgadmin_user" {
14 | type = string
15 | description = "The RDS user to used for creating/managing other user in the database."
16 | }
17 |
18 | variable "sslmode" {
19 | type = string
20 | description = "Set the priority for an SSL connection to the server. Valid values are [disable,require,verify-ca,verify-full]"
21 | default = "require"
22 | }
23 |
24 | variable "connect_timeout" {
25 | type = number
26 | description = "Maximum wait for connection, in seconds. The default is 180s. Zero or not specified means wait indefinitely."
27 | default = 180
28 | }
29 |
30 | variable "superuser" {
31 | type = bool
32 | description = "Should be set to false if the user to connect is not a PostgreSQL superuser"
33 | default = false
34 | }
35 |
36 | variable "expected_version" {
37 | type = string
38 | description = "Specify a hint to Terraform regarding the expected version that the provider will be talking with. This is a required hint in order for Terraform to talk with an ancient version of PostgreSQL. This parameter is expected to be a PostgreSQL Version or current. Once a connection has been established, Terraform will fingerprint the actual version. Default: 9.0.0"
39 | default = "12.0.0"
40 | }
41 |
42 | variable "inputs" {
43 | type = any
44 | description = "The map containing all elements for creating objects inside database"
45 | default = null
46 | }
47 |
48 | variable "postprocessing_playbook_params" {
49 | description = "params for postprocessing playbook"
50 | type = any
51 | default = null
52 | }
--------------------------------------------------------------------------------
/examples/full-rds-example/create-procedure-statistiques.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE PROCEDURE FEED_STATS(ProductId INTEGER) LANGUAGE plpgsql AS $$
2 | DECLARE
3 |
4 | infos record;
5 |
6 | BEGIN
7 |
8 | -- retrieve the total amount for a specific product
9 | for infos in (
10 | select product.label as ProductLabel, sum(quantity * cost) as totalAmount
11 | from customer,product,basket
12 | where basket.customer_id = customer.id
13 | and basket.product_id = product.id
14 | and product.id = ProductId
15 | group by product.label)
16 | loop
17 | -- insert into stats table
18 | insert into stats (product,value) values (infos.ProductLabel,infos.totalAmount);
19 | end loop;
20 |
21 | END;
22 | $$;
23 |
--------------------------------------------------------------------------------
/examples/full-rds-example/create-tables.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE product (
2 | id SERIAL PRIMARY KEY,
3 | label TEXT NOT NULL,
4 | owner TEXT NOT NULL,
5 | cost NUMERIC(5,2) NOT NULL,
6 | time DATE NOT NULL DEFAULT current_timestamp
7 | );
8 |
9 | INSERT INTO product(label,owner,cost) VALUES ('Tee-shirt','textile-team',2.5);
10 |
11 |
12 | CREATE TABLE Customer (
13 | id SERIAL PRIMARY KEY,
14 | firstname TEXT NOT NULL,
15 | lastname TEXT NULL,
16 | address TEXT NULL,
17 | time DATE NOT NULL DEFAULT current_timestamp
18 | );
19 |
20 | CREATE INDEX idx_Customer_lastname ON Customer(lastname);
21 |
22 |
23 | CREATE TABLE Basket (
24 | id SERIAL PRIMARY KEY,
25 | customer_id INT NOT NULL,
26 | product_id INT NOT NULL,
27 | quantity INT NOT NULL,
28 | CONSTRAINT fk_customer FOREIGN KEY(customer_id) REFERENCES Customer(id),
29 | CONSTRAINT fk_product FOREIGN KEY(product_id) REFERENCES Product(id)
30 | );
31 |
32 | CREATE TABLE Stats(
33 | id SERIAL PRIMARY KEY,
34 | product TEXT NOT NULL,
35 | value NUMERIC(8,2)
36 | );
37 |
--------------------------------------------------------------------------------
/examples/full-rds-example/elasticsearch.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Retrieve infos on AWS STS Caller
3 | ########################################
4 | data "aws_caller_identity" "current" {}
5 |
6 | #########################################
7 | # Because of a cyclic dependency, we need to
8 | # create the role of the lambda.
9 | #########################################
10 | resource "aws_iam_role" "lambda-role" {
11 | name = format("role-%s-%s", var.environment, local.lambda_function_name)
12 | assume_role_policy = file("${path.module}/policies/lambda_role.json")
13 | }
14 |
15 | resource "aws_iam_role_policy" "lambda-policy" {
16 | name = format("policy-%s-%s", var.environment, local.lambda_function_name)
17 | role = aws_iam_role.lambda-role.id
18 | policy = templatefile("${path.module}/policies/lambda_policy.tpl", {
19 | account_id = data.aws_caller_identity.current.account_id,
20 | region = var.region
21 | })
22 | }
23 |
24 |
25 | ###########################################
26 | # Deploy an ElasticSearch Cluster
27 | ###########################################
28 | module "elasticsearch" {
29 | source = "cloudposse/elasticsearch/aws"
30 | version = "0.35.0"
31 |
32 | # create or not all related resources inside the module
33 | enabled = var.create_elasticsearch
34 |
35 | #naming
36 | namespace = "soc"
37 | stage = var.environment
38 | name = "es"
39 |
40 | # config
41 | vpc_enabled = false
42 | zone_awareness_enabled = false
43 | elasticsearch_version = "7.4"
44 | instance_type = var.es_instance_type
45 | instance_count = var.es_instance_count
46 | ebs_volume_size = var.es_ebs_volume_size
47 | # because of a cyclic dependencies, create in a first step the elasticsearch without allowing the role of the lambda streaming
48 | iam_role_arns = [aws_iam_role.lambda-role.arn]
49 | iam_actions = ["es:ESHttpGet", "es:ESHttpPut", "es:ESHttpPost"]
50 | encrypt_at_rest_enabled = "true"
51 | kibana_subdomain_name = "kibana-soc"
52 | create_iam_service_linked_role = false
53 | allowed_cidr_blocks = var.allowed_ip_addresses
54 |
55 | advanced_options = {
56 | "rest.action.multi.allow_explicit_index" = "true"
57 | }
58 | }
59 |
60 | ###########################################
61 | # Deploy a subscription filter on RDS CloudWatch Logs
62 | # to stream logs on an ElasticSearch domain endpoint
63 | ###########################################
64 | module "stream2es" {
65 | source = "jparnaudeau/cloudwatch-subscription-elasticsearch/aws"
66 | version = "1.0.0"
67 |
68 | for_each = var.create_elasticsearch ? toset(["1"]) : []
69 |
70 | # gloval variables
71 | region = var.region
72 | environment = var.environment
73 | tags = local.tags
74 |
75 | # other variables
76 | function_name = local.lambda_function_name
77 | rds_name = var.rds_name
78 | rds_cloudwatch_log_name = format("/aws/rds/instance/%s/postgresql", var.rds_name)
79 | es_domain_endpoint = try(module.elasticsearch.domain_endpoint, "")
80 | source_account_id = data.aws_caller_identity.current.account_id
81 | lambda_role_arn = aws_iam_role.lambda-role.arn
82 | }
--------------------------------------------------------------------------------
/examples/full-rds-example/gen-password-in-secretsmanager.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import traceback
5 | import boto3
6 | import os
7 | from botocore.config import Config
8 | import subprocess
9 | import shlex
10 |
11 |
12 | ###########################
13 | # MAIN
14 | ###########################
15 | if __name__ == '__main__':
16 |
17 | try:
18 |
19 | # Retrieve environment variables
20 | region = os.getenv('REGION')
21 | refresh_password = os.getenv('REFRESH_PASSWORD')
22 | rds_name = os.getenv('RDS_NAME')
23 | database_user = os.getenv('DBUSER')
24 |
25 | if refresh_password == "true":
26 |
27 | my_config = Config(
28 | region_name = region,
29 | # signature_version = 'v4',
30 | # retries = {
31 | # 'max_attempts': 10,
32 | # 'mode': 'standard'
33 | # }
34 | )
35 | client = boto3.client('secretsmanager',config=my_config)
36 |
37 | # generate a random password
38 | response = client.get_random_password(
39 | PasswordLength=32,
40 | ExcludeNumbers=False,
41 | ExcludePunctuation=True,
42 | ExcludeUppercase=False,
43 | ExcludeLowercase=False,
44 | IncludeSpace=False,
45 | RequireEachIncludedType=True
46 | )
47 | secret_value = response['RandomPassword']
48 |
49 | # retrieve secret-id
50 | secret_name = "secret-kv-{rdsName}-{userName}".format(rdsName=rds_name,userName=database_user)
51 | response = client.list_secrets(Filters=[
52 | {
53 | 'Key': 'name',
54 | 'Values': [
55 | secret_name,
56 | ]
57 | },
58 | ]
59 | )
60 |
61 | secret_id = response['SecretList'][0]['ARN']
62 |
63 | # update password in database : psql -c "ALTER USER $DBUSER WITH PASSWORD '$USERPWD'"
64 | postgresql_ddl = shlex.split("psql -c \"ALTER USER {userName} WITH PASSWORD '{secretValue}'\"".format(userName=database_user,secretValue=secret_value))
65 | process = subprocess.Popen(postgresql_ddl,
66 | stdout=subprocess.PIPE,
67 | stderr=subprocess.PIPE,
68 | universal_newlines=True)
69 | while True:
70 | output = process.stdout.readline()
71 | print(output.strip())
72 | # Do something else
73 | return_code = process.poll()
74 | if return_code is not None:
75 | print('RETURN CODE', return_code)
76 | # Process has finished, read rest of the output
77 | for output in process.stdout.readlines():
78 | print(output.strip())
79 | break
80 | else:
81 |
82 | # alter secret value
83 | response = client.put_secret_value(SecretId=secret_id,
84 | SecretString=secret_value,
85 | )
86 |
87 | print("Succesfully alter secret {}".format(secret_name))
88 | break
89 |
90 | except Exception as err:
91 | print("Exception during processing: {0}".format(err))
92 | traceback.print_exc()
93 |
--------------------------------------------------------------------------------
/examples/full-rds-example/locals.tf:
--------------------------------------------------------------------------------
1 | ##################################################################
2 | # Define locals
3 | ##################################################################
4 | locals {
5 | name = var.rds_name
6 | subnet_grp_name = format("subnetsgrp-%s", local.name)
7 | tags = merge(var.tags, { "environment" = var.environment })
8 | namespace = format("/%s/%s", var.environment, var.inputs["db_name"])
9 | lambda_function_name = format("streamLogsToEsFor-%s", var.rds_name)
10 | }
11 |
--------------------------------------------------------------------------------
/examples/full-rds-example/outputs.tf:
--------------------------------------------------------------------------------
1 | ##########################################
2 | # Outputs for VPC
3 | ##########################################
4 | output "vpc_infos" {
5 | description = "map of vpc informations"
6 | value = {
7 | vpc_id = module.vpc.vpc_id,
8 | vpc_name = module.vpc.name,
9 | public_subnets = module.vpc.public_subnets,
10 | private_subnets = module.vpc.private_subnets,
11 | database_subnets = module.vpc.database_subnets
12 | }
13 | }
14 |
15 | ################################################
16 | # Outputs for RDS
17 | ################################################
18 | output "rds_infos" {
19 | description = "map of rds informations"
20 | value = {
21 | db_instance_address = module.rds.db_instance_address,
22 | db_instance_arn = module.rds.db_instance_arn,
23 | db_instance_endpoint = module.rds.db_instance_endpoint,
24 | db_instance_id = module.rds.db_instance_id,
25 | db_instance_name = module.rds.db_instance_name,
26 | "connect_command" = format("psql -h %s -p %s -U %s -d %s -W", module.rds.db_instance_address, var.dbport, var.rds_superuser_name, var.inputs["db_name"])
27 | }
28 | }
29 |
30 | output "affected_schema" {
31 | description = "the name of the schema in which the db objects have been created by the module"
32 | value = var.inputs["db_schema_name"]
33 | }
34 |
35 | output "created_roles" {
36 | description = "The list of roles created by the module"
37 | value = [for obj_role in var.inputs["db_roles"] : obj_role["role"]]
38 | }
39 |
40 | output "db_users" {
41 | description = "The list of users created by the module"
42 | value = { for user in var.inputs["db_users"] :
43 | user.name => {
44 | "secret_name" = join(",", keys(module.secrets-manager[user.name].secret_arns)),
45 | "secret_arn" = join(",", values(module.secrets-manager[user.name].secret_arns)),
46 | "connect_command" = format("psql -h %s -p %s -U %s -d %s -W", module.rds.db_instance_address, var.dbport, user.name, var.inputs["db_name"])
47 | }
48 | }
49 | }
50 |
51 | ################################################
52 | # Outputs for elasticSearch
53 | ################################################
54 | output "domain_arn" {
55 | description = "The ElasticSearch Domain ARN"
56 | value = try(module.elasticsearch.domain_arn, "")
57 | }
58 |
59 | output "domain_endpoint" {
60 | description = "The ElasticSearch Domain Endpoint"
61 | value = try(module.elasticsearch.domain_endpoint, "")
62 | }
63 | output "domain_hostname" {
64 | description = "The ElasticSearch Domain Hostname"
65 | value = try(module.elasticsearch.domain_hostname, "")
66 | }
67 | output "domain_id" {
68 | description = "The ElasticSearch Domain Id"
69 | value = try(module.elasticsearch.domain_id, "")
70 | }
71 | output "domain_name" {
72 | description = "The ElasticSearch Domain Name"
73 | value = try(module.elasticsearch.domain_name, "")
74 | }
75 | output "elasticsearch_user_iam_role_arn" {
76 | description = "The ElasticSearch User IAM Role ARN"
77 | value = try(module.elasticsearch.elasticsearch_user_iam_role_arn, "")
78 | }
79 | output "elasticsearch_user_iam_role_name" {
80 | description = "The ElasticSearch User IAM Role Name"
81 | value = try(module.elasticsearch.elasticsearch_user_iam_role_name, "")
82 | }
83 | output "kibana_endpoint" {
84 | description = "The ElasticSearch Kibana Endpoint"
85 | value = try(module.elasticsearch.kibana_endpoint, "")
86 | }
87 |
88 | ################################################
89 | # Outputs for streaming lambda
90 | ################################################
91 | output "streaming_lambda_arn" {
92 | description = "The Lambda ARN responsible of streaming RDS Logs to ElasticSearch"
93 | value = try(module.stream2es["1"].lambda_function_arn, "Not Deploy")
94 | }
95 |
96 | output "streamed_cloudwatch_log_arn" {
97 | description = "The CloudWatch Log ARN being streamed by the lambda"
98 | value = try(module.stream2es["1"].streamed_cloudwatch_log_arn, "Not Deploy")
99 | }
100 |
101 | output "streaming_lambda_role_arn" {
102 | description = "The Role ARN of the streaming lambda"
103 | value = aws_iam_role.lambda-role.arn
104 | }
105 |
--------------------------------------------------------------------------------
/examples/full-rds-example/policies/lambda_policy.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "CopiedFromTemplateAWSLambdaVPCAccessExecutionRole1",
6 | "Effect": "Allow",
7 | "Action": [
8 | "logs:Create*",
9 | "logs:Describe*",
10 | "es:ESHttpPost"
11 | ],
12 | "Resource": "*"
13 | },
14 | {
15 | "Sid": "CopiedFromTemplateAWSLambdaBasicExecutionRole2",
16 | "Effect": "Allow",
17 | "Action": [
18 | "logs:CreateLogStream",
19 | "logs:Put*",
20 | "logs:FilterLogEvents"
21 | ],
22 | "Resource": [
23 | "arn:aws:logs:${region}:${account_id}:log-group:*"
24 | ]
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/examples/full-rds-example/policies/lambda_role.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Action": "sts:AssumeRole",
6 | "Principal": {
7 | "Service": "lambda.amazonaws.com"
8 | },
9 | "Effect": "Allow"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/examples/full-rds-example/postgresql.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # Initialize the database and the objects
3 | # (roles & grants), the default privileges
4 | ########################################
5 | module "initdb" {
6 |
7 | source = "../../create-database"
8 |
9 | depends_on = [module.rds]
10 |
11 | # set the provider
12 | providers = {
13 | postgresql = postgresql.pgadm
14 | }
15 |
16 | # targetted rds
17 | pgadmin_user = var.rds_superuser_name
18 | dbhost = module.rds.db_instance_address
19 | dbport = var.dbport
20 |
21 | # input parameters for creating database & objects inside database
22 | create_database = false
23 | inputs = var.inputs
24 |
25 | # because the superuser is not "postgres", need to set it in the module
26 | default_superusers_list = [var.rds_superuser_name]
27 | }
28 |
29 | ####################################################################
30 | # for each users defined in var.inputs, create
31 | # - create a fake password for this user
32 | # - save it into secretsManager with key = "secret-kv-${rds_name}-${username}"
33 | #
34 | # we do this for having only one case to manage in the postprocessing shell :
35 | # we update systematically the value of the secret.
36 | ####################################################################
37 |
38 | # the random passwords for each user
39 | resource "random_password" "passwords" {
40 | for_each = { for user in var.inputs["db_users"] : user.name => user }
41 |
42 | length = 16
43 | special = true
44 | upper = true
45 | lower = true
46 | min_upper = 1
47 | number = true
48 | min_numeric = 1
49 | min_special = 3
50 | override_special = "@#%&?"
51 | }
52 |
53 | #########################################
54 | # Store key/value username/password in AWS SecretsManager
55 | #########################################
56 | module "secrets-manager" {
57 | for_each = { for user in var.inputs["db_users"] : user.name => user }
58 | source = "lgallard/secrets-manager/aws"
59 | version = "0.5.1"
60 |
61 | secrets = {
62 | "secret-kv-${local.name}-${each.key}" = {
63 | description = format("Password for username %s for database %s", each.key, local.name)
64 | secret_key_value = {
65 | username = each.key
66 | password = random_password.passwords[each.key].result
67 | }
68 | recovery_window_in_days = var.recovery_window_in_days
69 | },
70 | }
71 |
72 | tags = local.tags
73 | }
74 |
75 | #########################################
76 | # Create the users inside the database
77 | #########################################
78 | # AWS Region
79 | data "aws_region" "current" {}
80 |
81 | module "create_users" {
82 | source = "../../create-users"
83 |
84 | # need that all objects, managed inside the module "initdb", are created
85 | depends_on = [module.initdb]
86 |
87 | # set the provider
88 | providers = {
89 | postgresql = postgresql.pgadm
90 | }
91 |
92 | # targetted rds
93 | pgadmin_user = var.rds_superuser_name
94 | dbhost = module.rds.db_instance_address
95 | dbport = var.dbport
96 |
97 | # input parameters for creating users inside database
98 | db_users = var.inputs["db_users"]
99 |
100 | # set passwords
101 | passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
102 |
103 | # set postprocessing playbook
104 | postprocessing_playbook_params = {
105 | enable = true
106 | db_name = var.inputs["db_name"]
107 | extra_envs = {
108 | REGION = data.aws_region.current.name
109 | RDS_NAME = var.rds_name
110 | }
111 | refresh_passwords = var.refresh_passwords
112 | shell_name = "./gen-password-in-secretsmanager.py"
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/examples/full-rds-example/providers.tf:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Define Providers pgadm & pgmgm for postgresql
3 | #######################################
4 | provider "postgresql" {
5 | alias = "pgadm"
6 | host = module.rds.db_instance_address
7 | port = var.dbport
8 | username = var.rds_superuser_name
9 | sslmode = var.sslmode
10 | connect_timeout = var.connect_timeout
11 | superuser = var.superuser
12 | expected_version = var.expected_version
13 | }
14 |
15 | provider "postgresql" {
16 | alias = "pgmgm"
17 | host = module.rds.db_instance_address
18 | port = var.dbport
19 | database = var.inputs["db_name"]
20 | username = var.rds_superuser_name
21 | sslmode = var.sslmode
22 | connect_timeout = var.connect_timeout
23 | superuser = var.superuser
24 | expected_version = var.expected_version
25 | }
26 |
27 | #######################################
28 | # Define Provider for aws
29 | #######################################
30 | provider "aws" {
31 | region = var.region
32 | }
33 |
34 | #######################################
35 | # Manage version of providers
36 | #######################################
37 | terraform {
38 | required_version = ">= 1.0.4"
39 |
40 | required_providers {
41 | aws = {
42 | source = "hashicorp/aws"
43 | version = ">= 3.15"
44 | }
45 | postgresql = {
46 | source = "cyrilgdn/postgresql"
47 | version = ">= 1.11.0"
48 | }
49 | random = {
50 | source = "hashicorp/random"
51 | version = ">= 3.0.0"
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/full-rds-example/rds.tf:
--------------------------------------------------------------------------------
1 | ######################################
2 | # Deploy RDS Instance
3 | ######################################
4 | module "rds" {
5 | source = "terraform-aws-modules/rds/aws"
6 | version = "3.5.0"
7 |
8 | identifier = local.name
9 |
10 | # All available versions: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts
11 | engine = "postgres"
12 | engine_version = var.rds_engine_version
13 | family = var.rds_family
14 | major_engine_version = var.rds_major_engine_version
15 | instance_class = var.rds_instance_class
16 |
17 | allocated_storage = var.rds_allocated_storage
18 | max_allocated_storage = var.rds_max_allocated_storage
19 | storage_encrypted = var.rds_storage_encrypted
20 |
21 | # NOTE: Do NOT use 'user' as the value for 'username' as it throws:
22 | # "Error creating DB Instance: InvalidParameterValue: MasterUsername
23 | # user cannot be used as it is a reserved word used by the engine"
24 | name = var.inputs["db_name"]
25 | username = var.rds_superuser_name
26 | # password is setted inside environment variable TF_VAR_rds_root_password
27 | password = var.rds_root_password
28 | port = 5432
29 |
30 | multi_az = true
31 |
32 | # because we want reach the database from our local workstation, we need to deploy our RDS in the public subnets
33 | # DO NOT DO THAT IN PRODUCTION
34 | # to reduce the attack surface, limit the access of the RDS Instance to our personal IP addresses
35 | publicly_accessible = true
36 | subnet_ids = module.vpc.public_subnets
37 | vpc_security_group_ids = [module.security_group.security_group_id]
38 |
39 | maintenance_window = "Mon:00:00-Mon:03:00"
40 | backup_window = "03:00-06:00"
41 | enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
42 |
43 | backup_retention_period = 0
44 | skip_final_snapshot = true
45 | deletion_protection = false # for test purpose !!
46 |
47 | create_db_parameter_group = false
48 | parameter_group_name = aws_db_parameter_group.postgres.id
49 |
50 | create_db_option_group = false
51 |
52 | create_db_subnet_group = false
53 | db_subnet_group_name = aws_db_subnet_group.main_db_subnet_group.id
54 |
55 | tags = local.tags
56 | }
57 |
58 | resource "random_id" "val" {
59 | byte_length = 4
60 | }
61 |
62 | resource "aws_db_parameter_group" "postgres" {
63 | name = format("param-%s-%s", local.name, random_id.val.hex)
64 | description = "Parameter group for our postgresql rds instance"
65 | family = var.rds_family
66 |
67 | dynamic "parameter" {
68 | for_each = var.parameter_group_params["immediate"]
69 | content {
70 | name = parameter.key
71 | value = parameter.value
72 | }
73 | }
74 | dynamic "parameter" {
75 | for_each = var.parameter_group_params["pending-reboot"]
76 | content {
77 | name = parameter.key
78 | value = parameter.value
79 | apply_method = "pending-reboot"
80 | }
81 | }
82 |
83 | tags = local.tags
84 | }
85 |
86 |
87 | resource "aws_db_subnet_group" "main_db_subnet_group" {
88 | name = local.subnet_grp_name
89 | description = format("%s db subnet group", local.name)
90 | subnet_ids = module.vpc.public_subnets
91 |
92 | tags = local.tags
93 | }
94 |
--------------------------------------------------------------------------------
/examples/full-rds-example/retrieve-audit-logs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export INSTANCE_IDENTIFIER=`terraform output|grep db_instance_id|awk -F '=' '{print $2}'|sed 's/^ *//g'|sed 's/"//g'`
4 |
5 | LOGFILE=$(aws rds describe-db-log-files --db-instance-identifier ${INSTANCE_IDENTIFIER} --query 'DescribeDBLogFiles[-1].[LogFileName]' --output text)
6 |
7 | echo "LOGFILE = $LOGFILE"
8 |
9 | aws rds download-db-log-file-portion \
10 | --db-instance-identifier ${INSTANCE_IDENTIFIER} \
11 | --starting-token 0 \
12 | --log-file-name "${LOGFILE}" \
13 | --output text | grep AUDIT
14 |
--------------------------------------------------------------------------------
/examples/full-rds-example/terraform.tfvars:
--------------------------------------------------------------------------------
1 | # database and objects creation
2 | inputs = {
3 |
4 | # parameters used for creating database
5 | db_schema_name = "public"
6 | db_name = "mydatabase" # should be the same as var.rds_name. if not, a new database will be created
7 | db_admin = "app_admin_role" #owner of the database
8 |
9 | # install extensions if needed
10 | extensions = ["pgaudit"]
11 |
12 | # CREATE ROLE
13 | db_roles = [
14 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
15 | ],
16 |
17 | # GRANT PERMISSIONS ON ROLES
18 | db_grants = [
19 | # define grants for app_admin_role :
20 | # - access to all objects on database
21 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
22 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
23 |
24 | ],
25 |
26 | # CREATE USER
27 | db_users = [
28 | { name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = true },
29 | ]
30 |
31 | }
32 |
33 | # Refresh or not refresh passwords
34 | refresh_passwords = ["all"]
35 |
36 | # set tags & environment
37 | environment = "test"
38 | tags = {
39 | createdBy = "terraform"
40 | "ippon:owner" = "jparnaudeau"
41 | }
42 |
43 | ################################################
44 | # VPC & RDS Customization
45 | ################################################
46 |
47 | # a standard vpc
48 | vpc_cidr = "10.66.0.0/18"
49 |
50 | vpc_public_subnets = ["10.66.0.0/24", "10.66.1.0/24", "10.66.2.0/24"]
51 | vpc_private_subnets = ["10.66.3.0/24", "10.66.4.0/24", "10.66.5.0/24"]
52 | vpc_database_subnets = ["10.66.6.0/24", "10.66.7.0/24", "10.66.8.0/24"]
53 |
54 | # rds settings
55 | rds_name = "myfullrdsexample"
56 | rds_engine_version = "13.5"
57 | rds_major_engine_version = "13"
58 | rds_family = "postgres13"
59 | rds_instance_class = "db.t3.micro"
60 | rds_allocated_storage = 10
61 | rds_max_allocated_storage = 20
62 | allowed_ip_addresses = ["X.X.X.X/32"] # your personal Outbound IP Address
63 | rds_superuser_name = "root"
64 |
65 | # define parameter groups for our RDS, apply_method = "immediate"
66 | # for setting pg_extension parameters, the apply_method need to be "pending-reboot"
67 | # reboot required if the database already exsits : aws rds reboot-db-instance --db-instance-identifier xxx
68 | # extension pg_stat_statements : https://pganalyze.com/docs/install/amazon_rds/01_configure_rds_instance
69 | # extension pg_audit : https://aws.amazon.com/premiumsupport/knowledge-center/rds-postgresql-pgaudit/?nc1=h_ls
70 | parameter_group_params = {
71 | immediate = {
72 | autovacuum = 1
73 | client_encoding = "utf8"
74 | log_connections = "1"
75 | log_disconnections = "1"
76 | log_statement = "all"
77 | }
78 | pending-reboot = {
79 | shared_preload_libraries = "pgaudit",
80 | track_activity_query_size = "2048",
81 | "pgaudit.log" = "ALL",
82 | "pgaudit.log_level" = "info",
83 | "pgaudit.log_statement_once" = "1"
84 | }
85 | }
86 |
87 | ################################################
88 | # ElasticSearch
89 | ################################################
90 | create_elasticsearch = false
91 | es_instance_type = "t3.small.elasticsearch"
92 | es_instance_count = 1
93 | es_ebs_volume_size = 10
94 |
--------------------------------------------------------------------------------
/examples/full-rds-example/terraform.tfvars.step5:
--------------------------------------------------------------------------------
1 | # database and objects creation
2 | inputs = {
3 |
4 | # parameters used for creating database
5 | db_schema_name = "public"
6 | db_name = "mydatabase" # should be the same as var.rds_name. if not, a new database will be created
7 | db_admin = "app_admin_role" #owner of the database
8 |
9 | # install extensions if needed
10 | extensions = ["pgaudit"]
11 |
12 | # ---------------------------------- ROLES ------------------------------------------------------------------------------------
13 | # In this example, we want illustrate the "least privilege pattern". We have a schema in which 4 tables have been created : Customer, Product, Basket, Stats
14 | # We will create 4 roles :
15 | # - "app_admin_role" will be the role used for creation, deletion, grant operations on objects etc .. It's the "admin" role used for managed objects inside db 'mydatabase' (var.inputs['db_name']), schema 'public'
16 | # - "app_readonly_role" for readonly operations.
17 | # - "app_writeweb_role" for operations allowed from "web" application.
18 | # - "app_writebo_role" for operations allowed from "backoffice" application.
19 | db_roles = [
20 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
21 | { id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
22 | { id = "web", role = "app_writeweb_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
23 | { id = "backoffice", role = "app_writebo_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
24 | { id = "batch", role = "app_writebatch_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
25 | ],
26 |
27 | # ---------------------------------- GRANT PERMISSIONS ON ROLES ------------------------------------------------------------------------------------
28 | # you could find the available privileges on official postgresql doc : https://www.postgresql.org/docs/13/ddl-priv.html
29 | # Notes :
30 | # - "role" corresponds to the role on which the grants will be applied.
31 | # - "owner_role" is the role used to create grants on "role".
32 | # - object_type = "type" is used only for default privileges
33 | # - objects = [] means "all"
34 | # all these grants are related to db 'mydatabase' (var.inputs['db_name']), schema 'public' (var.inputs['db_schema_name'])
35 | db_grants = [
36 | # define grants for app_admin_role :
37 | # - access to all objects on database
38 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
39 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
40 |
41 | # define grants for app_readonly_role
42 | # - access to 'SELECT' on all tables
43 | # - access to 'SELECT' on all sequences
44 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
45 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
46 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
47 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
48 |
49 | # define grants for app_writeweb_role
50 | # - access in Read/Write on tables "customer" & "basket"
51 | # - access in Read on table "Product"
52 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
53 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
54 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["customer", "basket"], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
55 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["product"], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
56 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
57 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
58 |
59 | # define grants for app_writebo_role
60 | # - access in Read/Write on table "product"
61 | # - access in Read on table "customer", "basket", "stats"
62 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
63 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
64 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["product"], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
65 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["customer", "basket", "stats"], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
66 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
67 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
68 |
69 | # define grants for app_writebatch_role
70 | # - access in Read/Write on table "stats"
71 | # - access in Read on table "customer", "basket", "product"
72 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
73 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
74 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["stats"], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
75 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["customer", "basket", "product"], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
76 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
77 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
78 |
79 | ],
80 |
81 | db_users = [
82 | { name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = true },
83 | { name = "web", inherit = true, login = true, membership = ["app_writeweb_role"], validity = "infinity", connection_limit = -1, createrole = false },
84 | { name = "backoffice", inherit = true, login = true, membership = ["app_writebo_role"], validity = "infinity", connection_limit = -1, createrole = false },
85 | { name = "batch", inherit = true, login = true, membership = ["app_writebatch_role"], validity = "infinity", connection_limit = -1, createrole = false },
86 | { name = "lemmy", inherit = true, login = true, membership = ["app_readonly_role"], validity = "2022-07-31 00:00:00+00", connection_limit = -1, createrole = false },
87 | ]
88 |
89 | }
90 |
91 | # Refresh or not refresh passwords
92 | refresh_passwords = ["web","backoffice","batch","lemmy"]
93 |
94 | # set tags & environment
95 | environment = "test"
96 | tags = {
97 | createdBy = "terraform"
98 | "ippon:owner" = "jparnaudeau"
99 | }
100 |
101 | ################################################
102 | # VPC & RDS Customization
103 | ################################################
104 |
105 | # a standard vpc
106 | vpc_cidr = "10.66.0.0/18"
107 |
108 | vpc_public_subnets = ["10.66.0.0/24", "10.66.1.0/24", "10.66.2.0/24"]
109 | vpc_private_subnets = ["10.66.3.0/24", "10.66.4.0/24", "10.66.5.0/24"]
110 | vpc_database_subnets = ["10.66.6.0/24", "10.66.7.0/24", "10.66.8.0/24"]
111 |
112 | # rds settings
113 | rds_name = "myfullrdsexample"
114 | rds_engine_version = "13.5"
115 | rds_major_engine_version = "13"
116 | rds_family = "postgres13"
117 | rds_instance_class = "db.t3.micro"
118 | rds_allocated_storage = 10
119 | rds_max_allocated_storage = 20
120 | allowed_ip_addresses = ["X.X.X.X/32"] # your personal Outbound IP Address
121 | rds_superuser_name = "root"
122 |
123 | # define parameter groups for our RDS, apply_method = "immediate"
124 | # for setting pg_extension parameters, the apply_method need to be "pending-reboot"
125 | # reboot required if the database already exsits : aws rds reboot-db-instance --db-instance-identifier xxx
126 | # extension pg_stat_statements : https://pganalyze.com/docs/install/amazon_rds/01_configure_rds_instance
127 | # extension pg_audit : https://aws.amazon.com/premiumsupport/knowledge-center/rds-postgresql-pgaudit/?nc1=h_ls
128 | parameter_group_params = {
129 | immediate = {
130 | autovacuum = 1
131 | client_encoding = "utf8"
132 | log_connections = "1"
133 | log_disconnections = "1"
134 | log_statement = "all"
135 | }
136 | pending-reboot = {
137 | shared_preload_libraries = "pgaudit",
138 | track_activity_query_size = "2048",
139 | "pgaudit.log" = "ALL",
140 | "pgaudit.log_level" = "info",
141 | "pgaudit.log_statement_once" = "1"
142 | }
143 | }
144 |
145 | ################################################
146 | # ElasticSearch
147 | ################################################
148 | create_elasticsearch = false
149 | es_instance_type = "t3.small.elasticsearch"
150 | es_instance_count = 1
151 | es_ebs_volume_size = 10
152 |
--------------------------------------------------------------------------------
/examples/full-rds-example/variables.tf:
--------------------------------------------------------------------------------
1 | ########################################
2 | # define variables for postgresql database connectivity
3 | ########################################
4 | variable "dbport" {
5 | type = number
6 | default = 5432
7 | description = "The database port"
8 | }
9 |
10 | variable "sslmode" {
11 | type = string
12 | description = "Set the priority for an SSL connection to the server. Valid values are [disable,require,verify-ca,verify-full]"
13 | default = "require"
14 | }
15 |
16 | variable "connect_timeout" {
17 | type = number
18 | description = "Maximum wait for connection, in seconds. The default is 180s. Zero or not specified means wait indefinitely."
19 | default = 180
20 | }
21 |
22 | variable "superuser" {
23 | type = bool
24 | description = "Should be set to false if the user to connect is not a PostgreSQL superuser"
25 | default = false
26 | }
27 |
28 | variable "expected_version" {
29 | type = string
30 | description = "Specify a hint to Terraform regarding the expected version that the provider will be talking with. This is a required hint in order for Terraform to talk with an ancient version of PostgreSQL. This parameter is expected to be a PostgreSQL Version or current. Once a connection has been established, Terraform will fingerprint the actual version. Default: 9.0.0"
31 | default = "12.0.0"
32 | }
33 |
34 | ########################################
35 | # define variables for postgresql database creation
36 | ########################################
37 | variable "inputs" {
38 | type = any
39 | description = "The map containing all elements for creating objects inside database"
40 | default = null
41 | }
42 |
43 | ########################################
44 | # define global variables tags, env, ...
45 | ########################################
46 | variable "tags" {
47 | type = map(string)
48 | description = "a map of string used to tag entries in AWS Secrets Manager"
49 | default = {}
50 | }
51 |
52 | variable "environment" {
53 | type = string
54 | description = "environment name"
55 | default = "sta"
56 | }
57 |
58 | ########################################
59 | # define variables for vpc
60 | ########################################
61 | variable "vpc_cidr" {
62 | type = string
63 | description = "VPC CIDR"
64 | default = "10.0.0.0/16"
65 | }
66 |
67 | variable "vpc_public_subnets" {
68 | type = list(string)
69 | description = "list of public subnets range"
70 | default = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
71 | }
72 |
73 | variable "vpc_private_subnets" {
74 | type = list(string)
75 | description = "list of private subnets range"
76 | default = ["10.0.3.0/24", "10.0.4.0/24", "10.0.5.0/24"]
77 | }
78 |
79 | variable "vpc_database_subnets" {
80 | type = list(string)
81 | description = "list of database subnets range"
82 | default = ["10.0.6.0/24", "10.0.7.0/24", "10.0.8.0/24"]
83 | }
84 |
85 | ########################################
86 | # define variables for rds
87 | ########################################
88 | variable "region" {
89 | type = string
90 | description = "AWS Region name"
91 | default = "eu-west-3"
92 | }
93 |
94 | variable "rds_name" {
95 | type = string
96 | description = "RDS Database Name"
97 | default = "mydatabase"
98 | }
99 |
100 | variable "allowed_ip_addresses" {
101 | type = list(string)
102 | description = "List of allowed IP addresses"
103 | default = []
104 | }
105 |
106 | variable "rds_major_engine_version" {
107 | type = string
108 | description = "RDS Major Engine Version"
109 | default = "13"
110 | }
111 |
112 | variable "rds_engine_version" {
113 | type = string
114 | description = "RDS Engine Version"
115 | default = "13.5"
116 | }
117 |
118 | variable "rds_family" {
119 | type = string
120 | description = "RDS Family"
121 | default = "postgres13"
122 | }
123 |
124 | variable "rds_instance_class" {
125 | type = string
126 | description = "RDS Instance class"
127 | default = "db.t3.micro"
128 | }
129 |
130 | variable "rds_allocated_storage" {
131 | type = number
132 | description = "RDS Inital Allocated Storage"
133 | default = 10
134 | }
135 |
136 | variable "rds_max_allocated_storage" {
137 | type = number
138 | description = "RDS Max Allocated Storage"
139 | default = 20
140 | }
141 |
142 | variable "rds_storage_encrypted" {
143 | type = bool
144 | description = "Enable encryption at rest"
145 | default = true
146 | }
147 |
148 | variable "rds_superuser_name" {
149 | type = string
150 | description = "The default super-user name"
151 | default = "root"
152 | }
153 |
154 | variable "rds_root_password" {
155 | type = string
156 | description = "Password for RDS super-user"
157 | sensitive = true
158 | }
159 |
160 | variable "parameter_group_params" {
161 | type = map(any)
162 | description = "custom parameter group instance params"
163 | default = {}
164 | }
165 |
166 |
167 | ########################################
168 | # define variables for AWS SecretsManager
169 | ########################################
170 | variable "recovery_window_in_days" {
171 | type = number
172 | description = "delay in days during a secret can be recoverd"
173 | default = 7
174 | }
175 |
176 | variable "refresh_passwords" {
177 | type = list(string)
178 | description = "The list of users that we want refresh its password. Default '[all]'"
179 | default = ["all"]
180 | }
181 |
182 | ########################################
183 | # define variables for ElasticSearch
184 | ########################################
185 | variable "create_elasticsearch" {
186 | type = bool
187 | description = "Enable or Not the creation of an elasticSearch to simulate a SOC Tool"
188 | default = false
189 | }
190 |
191 | variable "es_instance_type" {
192 | type = string
193 | description = "InstanceType for ElasticSearch Node"
194 | default = "t3.small.elasticsearch"
195 | }
196 |
197 | variable "es_instance_count" {
198 | type = number
199 | description = "Number of instances in the ElasticSearch Domain"
200 | default = 1
201 | }
202 |
203 | variable "es_ebs_volume_size" {
204 | type = number
205 | description = "EBS Size associated to each node in the ElasticSearch Domain"
206 | default = 10
207 | }
208 |
--------------------------------------------------------------------------------
/examples/full-rds-example/vpc.tf:
--------------------------------------------------------------------------------
1 | ######################################
2 | # Create our playground - VPC
3 | ######################################
4 | module "vpc" {
5 | source = "terraform-aws-modules/vpc/aws"
6 | version = "~> 2"
7 |
8 | name = format("vpc-%s-%s", var.environment, local.name)
9 | cidr = var.vpc_cidr
10 |
11 | azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
12 | public_subnets = var.vpc_public_subnets
13 | private_subnets = var.vpc_private_subnets
14 | database_subnets = var.vpc_database_subnets
15 |
16 | create_database_subnet_group = false
17 |
18 | enable_dns_hostnames = true
19 | enable_dns_support = true
20 |
21 | tags = local.tags
22 | }
23 |
24 |
25 | ######################################
26 | # Deploy Security Group for our RDS Instance
27 | # allow access from personal IP Address
28 | ######################################
29 | module "security_group" {
30 | source = "terraform-aws-modules/security-group/aws"
31 | version = "~> 4"
32 |
33 | name = "${local.name}-postgresql"
34 | description = "PostgreSQL RDS security group"
35 | vpc_id = module.vpc.vpc_id
36 |
37 | tags = local.tags
38 | }
39 |
40 | resource "aws_security_group_rule" "allowed_ip_on_rds" {
41 | description = "Expose Postgresql Listener to Allowed IP Addresses"
42 | type = "ingress"
43 | from_port = 5432
44 | to_port = 5432
45 | protocol = "TCP"
46 | cidr_blocks = var.allowed_ip_addresses
47 | security_group_id = module.security_group.security_group_id
48 | }
49 |
50 | resource "aws_security_group_rule" "rds_outbound" {
51 | description = "Outbound access for ${local.name}"
52 | type = "egress"
53 | from_port = 0
54 | to_port = 0
55 | protocol = "-1"
56 | cidr_blocks = ["0.0.0.0/0"]
57 | security_group_id = module.security_group.security_group_id
58 | }
59 |
--------------------------------------------------------------------------------
/examples/simple-database/.envrc:
--------------------------------------------------------------------------------
1 | export PGPASSWORD=password
2 | export AWS_PROFILE=ippon-sandbox
3 | export AWS_DEFAULT_REGION=eu-west-3
4 |
--------------------------------------------------------------------------------
/examples/simple-database/README.md:
--------------------------------------------------------------------------------
1 | # simple-database
2 |
3 | This example shows you how to use the module to create a database and all roles and permissions. It is usefull for :
4 |
5 | * create a database locally. It's the case with the use of the docker-compose
6 | * in a cloud environment : After you have created an postgresql instance, you have a super-user and you want to create the database and prepare the database with roles and permissions.
7 |
8 | ## Prepare you postgresql provider
9 |
10 | ```hcl
11 |
12 | #######################################
13 | # Define Providers pgadm & pgmgm for postgresql
14 | #######################################
15 | provider "postgresql" {
16 | alias = "pgadm"
17 | host = var.dbhost
18 | port = var.dbport
19 | username = var.pgadmin_user
20 | sslmode = var.sslmode
21 | connect_timeout = var.connect_timeout
22 | superuser = var.superuser
23 | expected_version = var.expected_version
24 | }
25 |
26 | provider "postgresql" {
27 | alias = "pgmgm"
28 | host = var.dbhost
29 | port = var.dbport
30 | database = var.inputs["db_name"]
31 | username = var.pgadmin_user
32 | sslmode = var.sslmode
33 | connect_timeout = var.connect_timeout
34 | superuser = var.superuser
35 | expected_version = var.expected_version
36 | }
37 |
38 | ```
39 |
40 | Note : the password of the `var.pgadmin_user` are stored in the environment variable **PGPASSWORD** that you must setted before the terraform plan or apply.
41 |
42 | ## Call the module
43 |
44 | ```hcl
45 |
46 | module "initdb" {
47 |
48 | source = "jparnaudeau/database-admin/postgresql//create-database"
49 | version = "2.0.0"
50 |
51 | # set the provider
52 | providers = {
53 | postgresql = postgresql.pgadm
54 | }
55 |
56 | # targetted rds
57 | pgadmin_user = var.pgadmin_user
58 | dbhost = var.dbhost
59 | dbport = var.dbport
60 |
61 | # input parameters for creating database & objects inside database
62 | create_database = true
63 | inputs = var.inputs
64 | }
65 |
66 |
67 | ```
68 |
69 |
70 | ## Define the inputs
71 |
72 | in the `terraform.tfvars`, you could find :
73 |
74 | ```hcl
75 |
76 | inputs = {
77 |
78 | # parameters used for creating a database named 'mydatabase' and for creating objects in the public schema
79 | db_schema_name = "public"
80 | db_name = "mydatabase"
81 | db_admin = "app_admin_role" # owner of the database
82 | extensions = []
83 |
84 | # ---------------------------------- ROLES ------------------------------------------------------------------------------------
85 | # In this example, we create 3 roles
86 | # - "app_admin_role" will be the role used for creation, deletion, grant operations on objects, especially for tables.
87 | # - "app_write_role" for write operations. If you have a backend that insert lines into tables, it will used a user that inherits permissions from it.
88 | # - "app_readonly_role" for readonly operations.
89 | # Note : "write" role does not have the permissions to create table.
90 | # Note : the 'createrole' field is a boolean that provides a way to create other roles and put grants on it. Be carefull when you give this permission (privilege escalation).
91 | db_roles = [
92 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
93 | { id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
94 | { id = "write", role = "app_write_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
95 |
96 | ],
97 |
98 | # ---------------------------------- GRANT PERMISSIONS ON ROLES ------------------------------------------------------------------------------------
99 | # Notes :
100 | # the concept of "Least privilege" need to be applied here.
101 | # in the structure of a grant, there is the "role" and "owner_role"
102 | # "role" corresponds to the role on which the grants will be applied
103 | # "owner_role" is the role used to create grants on "role".
104 | # you could find the available privileges on official postgresql doc : https://www.postgresql.org/docs/13/ddl-priv.html
105 | # Note object_type = "type" is used only for default privileges
106 | db_grants = [
107 | # role app_admin_role : define grants to apply on db 'mydatabase', schema 'public'
108 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
109 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
110 |
111 | # role app_readonly_role : define grant to apply on db 'mydatabase', schema 'public'
112 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
113 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = true },
114 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
115 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
116 |
117 | # role app_write_role : define grant to apply on db 'mydatabase', schema 'public'
118 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
119 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = true },
120 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
121 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
122 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
123 |
124 | ],
125 |
126 | }
127 |
128 | ```
129 |
--------------------------------------------------------------------------------
/examples/simple-database/main.tf:
--------------------------------------------------------------------------------
1 | module "initdb" {
2 |
3 | source = "../../create-database"
4 |
5 | # set the provider
6 | providers = {
7 | postgresql = postgresql.pgadm
8 | }
9 |
10 | # targetted rds
11 | pgadmin_user = var.pgadmin_user
12 | dbhost = var.dbhost
13 | dbport = var.dbport
14 |
15 | # input parameters for creating database & objects inside database
16 | create_database = true
17 | inputs = var.inputs
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/examples/simple-database/outputs.tf:
--------------------------------------------------------------------------------
1 | output "created_database" {
2 | description = "the name of the database created by the module"
3 | value = var.inputs["db_name"]
4 | }
5 |
6 | output "affected_schema" {
7 | description = "the name of the schema in which the db objects have been created by the module"
8 | value = var.inputs["db_schema_name"]
9 | }
10 |
11 | output "created_roles" {
12 | description = "The list of roles created by the module"
13 | value = [for obj_role in var.inputs["db_roles"] : obj_role["role"]]
14 | }
15 |
16 | output "connect_string" {
17 | description = "The connect string to use to connect on the database"
18 | value = format("psql -h %s -p %s -U %s -d %s", var.dbhost, var.dbport, var.inputs["db_admin"], var.inputs["db_name"])
19 | }
20 |
--------------------------------------------------------------------------------
/examples/simple-database/providers.tf:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Define Providers pgadm & pgmgm for postgresql
3 | #######################################
4 | provider "postgresql" {
5 | alias = "pgadm"
6 | host = var.dbhost
7 | port = var.dbport
8 | username = var.pgadmin_user
9 | sslmode = var.sslmode
10 | connect_timeout = var.connect_timeout
11 | superuser = var.superuser
12 | expected_version = var.expected_version
13 | }
14 |
15 | provider "postgresql" {
16 | alias = "pgmgm"
17 | host = var.dbhost
18 | port = var.dbport
19 | database = var.inputs["db_name"]
20 | username = var.pgadmin_user
21 | sslmode = var.sslmode
22 | connect_timeout = var.connect_timeout
23 | superuser = var.superuser
24 | expected_version = var.expected_version
25 | }
26 |
27 | #######################################
28 | # Manage version of providers
29 | #######################################
30 | terraform {
31 | required_version = ">= 1.0.4"
32 |
33 | required_providers {
34 | postgresql = {
35 | source = "cyrilgdn/postgresql"
36 | version = ">= 1.15.0"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/simple-database/terraform.tfvars:
--------------------------------------------------------------------------------
1 | # provider connection infos
2 | pgadmin_user = "postgres"
3 | dbhost = "localhost"
4 | expected_version = "12.0.0"
5 | sslmode = "disable"
6 |
7 | inputs = {
8 |
9 | # parameters used for creating a database named 'mydatabase' and for creating objects in the public schema
10 | db_schema_name = "public"
11 | db_name = "mydatabase"
12 | db_admin = "app_admin_role" # owner of the database
13 | extensions = []
14 |
15 | # ---------------------------------- ROLES ------------------------------------------------------------------------------------
16 | # In this example, we create 3 roles
17 | # - "app_admin_role" will be the role used for creation, deletion, grant operations on objects, especially for tables.
18 | # - "app_write_role" for write operations. If you have a backend that insert lines into tables, it will used a user that inherits permissions from it.
19 | # - "app_readonly_role" for readonly operations.
20 | # Note : "write" role does not have the permissions to create table.
21 | # Note : the 'createrole' field is a boolean that provides a way to create other roles and put grants on it. Be carefull when you give this permission (privilege escalation).
22 | db_roles = [
23 | { id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
24 | { id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
25 | { id = "write", role = "app_write_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
26 |
27 | ],
28 |
29 | # ---------------------------------- GRANT PERMISSIONS ON ROLES ------------------------------------------------------------------------------------
30 | # Notes :
31 | # the concept of "Least privilege" need to be applied here.
32 | # in the structure of a grant, there is the "role" and "owner_role"
33 | # "role" corresponds to the role on which the grants will be applied
34 | # "owner_role" is the role used to create grants on "role".
35 | # you could find the available privileges on official postgresql doc : https://www.postgresql.org/docs/13/ddl-priv.html
36 | # Note object_type = "type" is used only for default privileges
37 | db_grants = [
38 | # role app_admin_role : define grants to apply on db 'mydatabase', schema 'public'
39 | { object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
40 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "postgres", grant_option = true },
41 |
42 | # role app_readonly_role : define grant to apply on db 'mydatabase', schema 'public'
43 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
44 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = true },
45 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
46 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
47 |
48 | # role app_write_role : define grant to apply on db 'mydatabase', schema 'public'
49 | { object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
50 | { object_type = "type", privileges = ["USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = true },
51 | { object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
52 | { object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
53 | { object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_write_role", owner_role = "app_admin_role", grant_option = false },
54 |
55 | ],
56 |
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/examples/simple-database/variables.tf:
--------------------------------------------------------------------------------
1 | variable "dbhost" {
2 | type = string
3 | default = "localhost"
4 | description = "The database host"
5 | }
6 |
7 | variable "dbport" {
8 | type = number
9 | default = 5432
10 | description = "The database port"
11 | }
12 |
13 | variable "pgadmin_user" {
14 | type = string
15 | description = "The RDS user to used for creating/managing other user in the database."
16 | }
17 |
18 | variable "sslmode" {
19 | type = string
20 | description = "Set the priority for an SSL connection to the server. Valid values are [disable,require,verify-ca,verify-full]"
21 | default = "require"
22 | }
23 |
24 | variable "connect_timeout" {
25 | type = number
26 | description = "Maximum wait for connection, in seconds. The default is 180s. Zero or not specified means wait indefinitely."
27 | default = 180
28 | }
29 |
30 | variable "superuser" {
31 | type = bool
32 | description = "Should be set to false if the user to connect is not a PostgreSQL superuser"
33 | default = false
34 | }
35 |
36 | variable "expected_version" {
37 | type = string
38 | description = "Specify a hint to Terraform regarding the expected version that the provider will be talking with. This is a required hint in order for Terraform to talk with an ancient version of PostgreSQL. This parameter is expected to be a PostgreSQL Version or current. Once a connection has been established, Terraform will fingerprint the actual version. Default: 9.0.0"
39 | default = "9.0.0"
40 | }
41 |
42 | variable "inputs" {
43 | type = any
44 | description = "The map containing all elements for creating objects inside database"
45 | default = null
46 | }
47 |
--------------------------------------------------------------------------------
/schemas/Diagram-Relations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jparnaudeau/terraform-postgresql-database-admin/ef34b2d22741907712c405a555013ff71da3fbab/schemas/Diagram-Relations.png
--------------------------------------------------------------------------------
/schemas/Diagram.excalidraw:
--------------------------------------------------------------------------------
1 | {
2 | "type": "excalidraw",
3 | "version": 2,
4 | "source": "https://excalidraw.com",
5 | "elements": [
6 | {
7 | "type": "image",
8 | "version": 48,
9 | "versionNonce": 2003440017,
10 | "isDeleted": false,
11 | "id": "ZGNQIqdmRrmVsow0oe5C8",
12 | "fillStyle": "hachure",
13 | "strokeWidth": 1,
14 | "strokeStyle": "solid",
15 | "roughness": 1,
16 | "opacity": 100,
17 | "angle": 0,
18 | "x": 580,
19 | "y": 260,
20 | "strokeColor": "transparent",
21 | "backgroundColor": "transparent",
22 | "width": 73.99999999999999,
23 | "height": 73.99999999999999,
24 | "seed": 442442239,
25 | "groupIds": [],
26 | "strokeSharpness": "round",
27 | "boundElements": [
28 | {
29 | "id": "JsQee1CZsxMflDtnJYmoG",
30 | "type": "arrow"
31 | }
32 | ],
33 | "updated": 1640116595542,
34 | "status": "saved",
35 | "fileId": "d1564f04fc39917fb6ab5b1bbcc0930445457501",
36 | "scale": [
37 | 1,
38 | 1
39 | ]
40 | },
41 | {
42 | "type": "image",
43 | "version": 144,
44 | "versionNonce": 1307701247,
45 | "isDeleted": false,
46 | "id": "vpmExZwo5PzL-bNlogjqI",
47 | "fillStyle": "hachure",
48 | "strokeWidth": 1,
49 | "strokeStyle": "solid",
50 | "roughness": 1,
51 | "opacity": 100,
52 | "angle": 0,
53 | "x": 1180,
54 | "y": 740,
55 | "strokeColor": "transparent",
56 | "backgroundColor": "transparent",
57 | "width": 73.99999999999999,
58 | "height": 73.99999999999999,
59 | "seed": 2075623889,
60 | "groupIds": [],
61 | "strokeSharpness": "round",
62 | "boundElements": [
63 | {
64 | "id": "U-7HJBwjCMpEF_qZoEA0o",
65 | "type": "arrow"
66 | }
67 | ],
68 | "updated": 1640117298309,
69 | "status": "saved",
70 | "fileId": "d1564f04fc39917fb6ab5b1bbcc0930445457501",
71 | "scale": [
72 | 1,
73 | 1
74 | ]
75 | },
76 | {
77 | "type": "image",
78 | "version": 228,
79 | "versionNonce": 228892511,
80 | "isDeleted": false,
81 | "id": "pKRUl3Ql5U-p0RvDTUHjv",
82 | "fillStyle": "hachure",
83 | "strokeWidth": 1,
84 | "strokeStyle": "solid",
85 | "roughness": 1,
86 | "opacity": 100,
87 | "angle": 0,
88 | "x": 900,
89 | "y": 740,
90 | "strokeColor": "transparent",
91 | "backgroundColor": "transparent",
92 | "width": 73.99999999999999,
93 | "height": 73.99999999999999,
94 | "seed": 1941852671,
95 | "groupIds": [],
96 | "strokeSharpness": "round",
97 | "boundElements": [
98 | {
99 | "id": "U-7HJBwjCMpEF_qZoEA0o",
100 | "type": "arrow"
101 | }
102 | ],
103 | "updated": 1640117221015,
104 | "status": "saved",
105 | "fileId": "d1564f04fc39917fb6ab5b1bbcc0930445457501",
106 | "scale": [
107 | 1,
108 | 1
109 | ]
110 | },
111 | {
112 | "type": "text",
113 | "version": 39,
114 | "versionNonce": 1604450207,
115 | "isDeleted": false,
116 | "id": "3bIzk1JFjmFkc105E70B-",
117 | "fillStyle": "hachure",
118 | "strokeWidth": 1,
119 | "strokeStyle": "solid",
120 | "roughness": 1,
121 | "opacity": 100,
122 | "angle": 0,
123 | "x": 537,
124 | "y": 340,
125 | "strokeColor": "#000000",
126 | "backgroundColor": "transparent",
127 | "width": 146,
128 | "height": 75,
129 | "seed": 1775664753,
130 | "groupIds": [],
131 | "strokeSharpness": "sharp",
132 | "boundElements": [],
133 | "updated": 1640116546658,
134 | "fontSize": 20,
135 | "fontFamily": 1,
136 | "text": "user 'postgres'\nor\nsuper-user",
137 | "baseline": 68,
138 | "textAlign": "left",
139 | "verticalAlign": "top",
140 | "containerId": null,
141 | "originalText": "user 'postgres'\nor\nsuper-user"
142 | },
143 | {
144 | "type": "text",
145 | "version": 45,
146 | "versionNonce": 440911313,
147 | "isDeleted": false,
148 | "id": "swNg3khuQzQiAcvlhBLQt",
149 | "fillStyle": "hachure",
150 | "strokeWidth": 1,
151 | "strokeStyle": "solid",
152 | "roughness": 1,
153 | "opacity": 100,
154 | "angle": 0,
155 | "x": 800,
156 | "y": 360,
157 | "strokeColor": "#000000",
158 | "backgroundColor": "transparent",
159 | "width": 215,
160 | "height": 25,
161 | "seed": 911333631,
162 | "groupIds": [],
163 | "strokeSharpness": "sharp",
164 | "boundElements": [
165 | {
166 | "id": "fs3uA4kk_LJzvj6WsE1eR",
167 | "type": "arrow"
168 | }
169 | ],
170 | "updated": 1640117175339,
171 | "fontSize": 20,
172 | "fontFamily": 1,
173 | "text": "Application Admin Role",
174 | "baseline": 18,
175 | "textAlign": "left",
176 | "verticalAlign": "top",
177 | "containerId": null,
178 | "originalText": "Application Admin Role"
179 | },
180 | {
181 | "type": "arrow",
182 | "version": 99,
183 | "versionNonce": 1829309368,
184 | "isDeleted": false,
185 | "id": "JsQee1CZsxMflDtnJYmoG",
186 | "fillStyle": "hachure",
187 | "strokeWidth": 1,
188 | "strokeStyle": "solid",
189 | "roughness": 1,
190 | "opacity": 100,
191 | "angle": 0,
192 | "x": 660,
193 | "y": 300.00000000177386,
194 | "strokeColor": "#000000",
195 | "backgroundColor": "transparent",
196 | "width": 179,
197 | "height": 1.2913687896798365e-9,
198 | "seed": 2084684369,
199 | "groupIds": [],
200 | "strokeSharpness": "round",
201 | "boundElements": [],
202 | "updated": 1641594417619,
203 | "startBinding": {
204 | "elementId": "ZGNQIqdmRrmVsow0oe5C8",
205 | "focus": 0.0810810810810811,
206 | "gap": 6
207 | },
208 | "endBinding": {
209 | "elementId": "PgXUgmO92DhydHPO4c4RD",
210 | "focus": 0.058823529411764705,
211 | "gap": 1
212 | },
213 | "lastCommittedPoint": null,
214 | "startArrowhead": null,
215 | "endArrowhead": "arrow",
216 | "points": [
217 | [
218 | 0,
219 | 0
220 | ],
221 | [
222 | 179,
223 | -1.2913687896798365e-9
224 | ]
225 | ]
226 | },
227 | {
228 | "type": "text",
229 | "version": 14,
230 | "versionNonce": 609823039,
231 | "isDeleted": false,
232 | "id": "5XZrBM0G-ueZeuq1LGxKK",
233 | "fillStyle": "hachure",
234 | "strokeWidth": 1,
235 | "strokeStyle": "solid",
236 | "roughness": 1,
237 | "opacity": 100,
238 | "angle": 0,
239 | "x": 700,
240 | "y": 260,
241 | "strokeColor": "#000000",
242 | "backgroundColor": "transparent",
243 | "width": 66,
244 | "height": 25,
245 | "seed": 160733247,
246 | "groupIds": [],
247 | "strokeSharpness": "sharp",
248 | "boundElements": [],
249 | "updated": 1640116604199,
250 | "fontSize": 20,
251 | "fontFamily": 1,
252 | "text": "create",
253 | "baseline": 18,
254 | "textAlign": "left",
255 | "verticalAlign": "top",
256 | "containerId": null,
257 | "originalText": "create"
258 | },
259 | {
260 | "type": "text",
261 | "version": 20,
262 | "versionNonce": 1688246591,
263 | "isDeleted": false,
264 | "id": "rwqR1rImrR1KXidtiL21W",
265 | "fillStyle": "hachure",
266 | "strokeWidth": 1,
267 | "strokeStyle": "solid",
268 | "roughness": 1,
269 | "opacity": 100,
270 | "angle": 0,
271 | "x": 1136,
272 | "y": 157,
273 | "strokeColor": "#000000",
274 | "backgroundColor": "transparent",
275 | "width": 200,
276 | "height": 25,
277 | "seed": 1843186577,
278 | "groupIds": [],
279 | "strokeSharpness": "sharp",
280 | "boundElements": [
281 | {
282 | "id": "UnGhNOcSF_FoSQi1oGdvf",
283 | "type": "arrow"
284 | }
285 | ],
286 | "updated": 1640117643540,
287 | "fontSize": 20,
288 | "fontFamily": 1,
289 | "text": "Database + schema",
290 | "baseline": 18,
291 | "textAlign": "left",
292 | "verticalAlign": "top",
293 | "containerId": null,
294 | "originalText": "Database + schema"
295 | },
296 | {
297 | "type": "arrow",
298 | "version": 121,
299 | "versionNonce": 1885263032,
300 | "isDeleted": false,
301 | "id": "UnGhNOcSF_FoSQi1oGdvf",
302 | "fillStyle": "hachure",
303 | "strokeWidth": 1,
304 | "strokeStyle": "solid",
305 | "roughness": 1,
306 | "opacity": 100,
307 | "angle": 0,
308 | "x": 981,
309 | "y": 298.18101032459657,
310 | "strokeColor": "#000000",
311 | "backgroundColor": "transparent",
312 | "width": 140.34388991312835,
313 | "height": 109.76240945513163,
314 | "seed": 945427103,
315 | "groupIds": [],
316 | "strokeSharpness": "round",
317 | "boundElements": [],
318 | "updated": 1641594417619,
319 | "startBinding": {
320 | "elementId": "PgXUgmO92DhydHPO4c4RD",
321 | "focus": 0.5263157894736842,
322 | "gap": 1
323 | },
324 | "endBinding": {
325 | "elementId": "rwqR1rImrR1KXidtiL21W",
326 | "focus": 0.78,
327 | "gap": 16
328 | },
329 | "lastCommittedPoint": null,
330 | "startArrowhead": null,
331 | "endArrowhead": "arrow",
332 | "points": [
333 | [
334 | 0,
335 | 0
336 | ],
337 | [
338 | 140.34388991312835,
339 | -109.76240945513163
340 | ]
341 | ]
342 | },
343 | {
344 | "type": "text",
345 | "version": 65,
346 | "versionNonce": 511525553,
347 | "isDeleted": false,
348 | "id": "cAjc0FHCCImV0Pf_GVsyk",
349 | "fillStyle": "hachure",
350 | "strokeWidth": 1,
351 | "strokeStyle": "solid",
352 | "roughness": 1,
353 | "opacity": 100,
354 | "angle": 0,
355 | "x": 1180,
356 | "y": 300,
357 | "strokeColor": "#000000",
358 | "backgroundColor": "transparent",
359 | "width": 238,
360 | "height": 25,
361 | "seed": 1146587327,
362 | "groupIds": [],
363 | "strokeSharpness": "sharp",
364 | "boundElements": [
365 | {
366 | "id": "U-7HJBwjCMpEF_qZoEA0o",
367 | "type": "arrow"
368 | },
369 | {
370 | "id": "ycChXnhlXWkTK9vlPvaRf",
371 | "type": "arrow"
372 | },
373 | {
374 | "id": "fIVQAtg5TlTuuwKzGkZ00",
375 | "type": "arrow"
376 | }
377 | ],
378 | "updated": 1640117352140,
379 | "fontSize": 20,
380 | "fontFamily": 1,
381 | "text": "Tables inside Database",
382 | "baseline": 18,
383 | "textAlign": "left",
384 | "verticalAlign": "top",
385 | "containerId": null,
386 | "originalText": "Tables inside Database"
387 | },
388 | {
389 | "type": "text",
390 | "version": 9,
391 | "versionNonce": 162855985,
392 | "isDeleted": false,
393 | "id": "nUMH0ER8Y-gc__9XhE1bC",
394 | "fillStyle": "hachure",
395 | "strokeWidth": 1,
396 | "strokeStyle": "solid",
397 | "roughness": 1,
398 | "opacity": 100,
399 | "angle": 0,
400 | "x": 1020,
401 | "y": 214,
402 | "strokeColor": "#000000",
403 | "backgroundColor": "transparent",
404 | "width": 66,
405 | "height": 25,
406 | "seed": 1942279839,
407 | "groupIds": [],
408 | "strokeSharpness": "sharp",
409 | "boundElements": [
410 | {
411 | "id": "fs3uA4kk_LJzvj6WsE1eR",
412 | "type": "arrow"
413 | }
414 | ],
415 | "updated": 1640117197030,
416 | "fontSize": 20,
417 | "fontFamily": 1,
418 | "text": "create",
419 | "baseline": 18,
420 | "textAlign": "left",
421 | "verticalAlign": "top",
422 | "containerId": null,
423 | "originalText": "create"
424 | },
425 | {
426 | "type": "arrow",
427 | "version": 57,
428 | "versionNonce": 1764750024,
429 | "isDeleted": false,
430 | "id": "gRg2SFar3ISdP0r2NvDz2",
431 | "fillStyle": "hachure",
432 | "strokeWidth": 1,
433 | "strokeStyle": "solid",
434 | "roughness": 1,
435 | "opacity": 100,
436 | "angle": 0,
437 | "x": 981,
438 | "y": 320,
439 | "strokeColor": "#000000",
440 | "backgroundColor": "transparent",
441 | "width": 179,
442 | "height": 0,
443 | "seed": 1901521151,
444 | "groupIds": [],
445 | "strokeSharpness": "round",
446 | "boundElements": [],
447 | "updated": 1641594417619,
448 | "startBinding": {
449 | "elementId": "PgXUgmO92DhydHPO4c4RD",
450 | "focus": 0.4117647058823529,
451 | "gap": 1
452 | },
453 | "endBinding": null,
454 | "lastCommittedPoint": null,
455 | "startArrowhead": null,
456 | "endArrowhead": "arrow",
457 | "points": [
458 | [
459 | 0,
460 | 0
461 | ],
462 | [
463 | 179,
464 | 0
465 | ]
466 | ]
467 | },
468 | {
469 | "type": "text",
470 | "version": 8,
471 | "versionNonce": 1913699281,
472 | "isDeleted": false,
473 | "id": "YJi7lK5c__MtqubsW8Q3H",
474 | "fillStyle": "hachure",
475 | "strokeWidth": 1,
476 | "strokeStyle": "solid",
477 | "roughness": 1,
478 | "opacity": 100,
479 | "angle": 0,
480 | "x": 1049,
481 | "y": 299,
482 | "strokeColor": "#000000",
483 | "backgroundColor": "transparent",
484 | "width": 66,
485 | "height": 25,
486 | "seed": 1243774271,
487 | "groupIds": [],
488 | "strokeSharpness": "sharp",
489 | "boundElements": [],
490 | "updated": 1640116927996,
491 | "fontSize": 20,
492 | "fontFamily": 1,
493 | "text": "create",
494 | "baseline": 18,
495 | "textAlign": "left",
496 | "verticalAlign": "top",
497 | "containerId": null,
498 | "originalText": "create"
499 | },
500 | {
501 | "type": "text",
502 | "version": 12,
503 | "versionNonce": 1113967048,
504 | "isDeleted": false,
505 | "id": "wCoXM4m4KhQ4kIRvjz4EE",
506 | "fillStyle": "hachure",
507 | "strokeWidth": 1,
508 | "strokeStyle": "solid",
509 | "roughness": 1,
510 | "opacity": 100,
511 | "angle": 0,
512 | "x": 1040,
513 | "y": 360,
514 | "strokeColor": "#000000",
515 | "backgroundColor": "transparent",
516 | "width": 66,
517 | "height": 25,
518 | "seed": 798923704,
519 | "groupIds": [],
520 | "strokeSharpness": "sharp",
521 | "boundElements": [],
522 | "updated": 1641594452945,
523 | "fontSize": 20,
524 | "fontFamily": 1,
525 | "text": "create",
526 | "baseline": 18,
527 | "textAlign": "left",
528 | "verticalAlign": "top",
529 | "containerId": null,
530 | "originalText": "create"
531 | },
532 | {
533 | "type": "text",
534 | "version": 45,
535 | "versionNonce": 1878978353,
536 | "isDeleted": false,
537 | "id": "Fy4XTlFPh1iW-ZYOYrDxF",
538 | "fillStyle": "hachure",
539 | "strokeWidth": 1,
540 | "strokeStyle": "solid",
541 | "roughness": 1,
542 | "opacity": 100,
543 | "angle": 0,
544 | "x": 1160,
545 | "y": 620,
546 | "strokeColor": "#000000",
547 | "backgroundColor": "transparent",
548 | "width": 102,
549 | "height": 25,
550 | "seed": 1410422239,
551 | "groupIds": [],
552 | "strokeSharpness": "sharp",
553 | "boundElements": [],
554 | "updated": 1640117260992,
555 | "fontSize": 20,
556 | "fontFamily": 1,
557 | "text": "Write Role",
558 | "baseline": 18,
559 | "textAlign": "left",
560 | "verticalAlign": "top",
561 | "containerId": null,
562 | "originalText": "Write Role"
563 | },
564 | {
565 | "type": "text",
566 | "version": 42,
567 | "versionNonce": 2042882815,
568 | "isDeleted": false,
569 | "id": "CnTPlYt0ojAFoKInH7fOA",
570 | "fillStyle": "hachure",
571 | "strokeWidth": 1,
572 | "strokeStyle": "solid",
573 | "roughness": 1,
574 | "opacity": 100,
575 | "angle": 0,
576 | "x": 1180,
577 | "y": 420,
578 | "strokeColor": "#000000",
579 | "backgroundColor": "transparent",
580 | "width": 285,
581 | "height": 25,
582 | "seed": 1138479199,
583 | "groupIds": [],
584 | "strokeSharpness": "sharp",
585 | "boundElements": [],
586 | "updated": 1640117371637,
587 | "fontSize": 20,
588 | "fontFamily": 1,
589 | "text": "select /insert/update/delete",
590 | "baseline": 18,
591 | "textAlign": "left",
592 | "verticalAlign": "top",
593 | "containerId": null,
594 | "originalText": "select /insert/update/delete"
595 | },
596 | {
597 | "type": "arrow",
598 | "version": 222,
599 | "versionNonce": 2014543288,
600 | "isDeleted": false,
601 | "id": "fs3uA4kk_LJzvj6WsE1eR",
602 | "fillStyle": "hachure",
603 | "strokeWidth": 1,
604 | "strokeStyle": "solid",
605 | "roughness": 0,
606 | "opacity": 100,
607 | "angle": 0,
608 | "x": 915.0084127874368,
609 | "y": 360,
610 | "strokeColor": "#000000",
611 | "backgroundColor": "transparent",
612 | "width": 4.991587212563331,
613 | "height": 178.9999999999999,
614 | "seed": 1385079903,
615 | "groupIds": [],
616 | "strokeSharpness": "round",
617 | "boundElements": [],
618 | "updated": 1641594417620,
619 | "startBinding": {
620 | "elementId": "PgXUgmO92DhydHPO4c4RD",
621 | "focus": -0.047832585949177324,
622 | "gap": 15
623 | },
624 | "endBinding": null,
625 | "lastCommittedPoint": null,
626 | "startArrowhead": null,
627 | "endArrowhead": "arrow",
628 | "points": [
629 | [
630 | 0,
631 | 0
632 | ],
633 | [
634 | 4.991587212563331,
635 | 178.9999999999999
636 | ]
637 | ]
638 | },
639 | {
640 | "type": "text",
641 | "version": 23,
642 | "versionNonce": 1960933553,
643 | "isDeleted": false,
644 | "id": "yxL_mrZgmuCbzB0EHh0Jg",
645 | "fillStyle": "hachure",
646 | "strokeWidth": 1,
647 | "strokeStyle": "solid",
648 | "roughness": 0,
649 | "opacity": 100,
650 | "angle": 0,
651 | "x": 880,
652 | "y": 620,
653 | "strokeColor": "#000000",
654 | "backgroundColor": "transparent",
655 | "width": 140,
656 | "height": 25,
657 | "seed": 1609439793,
658 | "groupIds": [],
659 | "strokeSharpness": "sharp",
660 | "boundElements": [],
661 | "updated": 1640117115054,
662 | "fontSize": 20,
663 | "fontFamily": 1,
664 | "text": "ReadOnly Role",
665 | "baseline": 18,
666 | "textAlign": "left",
667 | "verticalAlign": "top",
668 | "containerId": null,
669 | "originalText": "ReadOnly Role"
670 | },
671 | {
672 | "type": "rectangle",
673 | "version": 258,
674 | "versionNonce": 916662472,
675 | "isDeleted": false,
676 | "id": "PgXUgmO92DhydHPO4c4RD",
677 | "fillStyle": "hachure",
678 | "strokeWidth": 1,
679 | "strokeStyle": "solid",
680 | "roughness": 0,
681 | "opacity": 100,
682 | "angle": 0,
683 | "x": 840,
684 | "y": 260,
685 | "strokeColor": "#000000",
686 | "backgroundColor": "transparent",
687 | "width": 140,
688 | "height": 85,
689 | "seed": 1544923071,
690 | "groupIds": [],
691 | "strokeSharpness": "sharp",
692 | "boundElements": [
693 | {
694 | "type": "text",
695 | "id": "w0N7f3ezaS8vifFH-8yLd"
696 | },
697 | {
698 | "id": "JsQee1CZsxMflDtnJYmoG",
699 | "type": "arrow"
700 | },
701 | {
702 | "id": "UnGhNOcSF_FoSQi1oGdvf",
703 | "type": "arrow"
704 | },
705 | {
706 | "id": "gRg2SFar3ISdP0r2NvDz2",
707 | "type": "arrow"
708 | },
709 | {
710 | "id": "fs3uA4kk_LJzvj6WsE1eR",
711 | "type": "arrow"
712 | }
713 | ],
714 | "updated": 1641594417571
715 | },
716 | {
717 | "type": "rectangle",
718 | "version": 266,
719 | "versionNonce": 1568548383,
720 | "isDeleted": false,
721 | "id": "YOvj5fIbctNgV7xG9-OLK",
722 | "fillStyle": "hachure",
723 | "strokeWidth": 1,
724 | "strokeStyle": "solid",
725 | "roughness": 0,
726 | "opacity": 100,
727 | "angle": 0,
728 | "x": 860,
729 | "y": 520,
730 | "strokeColor": "#000000",
731 | "backgroundColor": "transparent",
732 | "width": 140,
733 | "height": 85,
734 | "seed": 2117228543,
735 | "groupIds": [],
736 | "strokeSharpness": "sharp",
737 | "boundElements": [
738 | {
739 | "id": "lG-7UiokO-k2wza2oFDK7",
740 | "type": "text"
741 | },
742 | {
743 | "id": "JsQee1CZsxMflDtnJYmoG",
744 | "type": "arrow"
745 | },
746 | {
747 | "id": "UnGhNOcSF_FoSQi1oGdvf",
748 | "type": "arrow"
749 | },
750 | {
751 | "id": "gRg2SFar3ISdP0r2NvDz2",
752 | "type": "arrow"
753 | },
754 | {
755 | "type": "text",
756 | "id": "lG-7UiokO-k2wza2oFDK7"
757 | }
758 | ],
759 | "updated": 1640117193216
760 | },
761 | {
762 | "type": "rectangle",
763 | "version": 307,
764 | "versionNonce": 1632554680,
765 | "isDeleted": false,
766 | "id": "UbxFLoEWt_kCcLLtlzF7g",
767 | "fillStyle": "hachure",
768 | "strokeWidth": 1,
769 | "strokeStyle": "solid",
770 | "roughness": 0,
771 | "opacity": 100,
772 | "angle": 0,
773 | "x": 1140,
774 | "y": 520,
775 | "strokeColor": "#000000",
776 | "backgroundColor": "transparent",
777 | "width": 140,
778 | "height": 85,
779 | "seed": 629013791,
780 | "groupIds": [],
781 | "strokeSharpness": "sharp",
782 | "boundElements": [
783 | {
784 | "id": "Vp64flTlg6cJPCKSp94ZU",
785 | "type": "text"
786 | },
787 | {
788 | "id": "JsQee1CZsxMflDtnJYmoG",
789 | "type": "arrow"
790 | },
791 | {
792 | "id": "UnGhNOcSF_FoSQi1oGdvf",
793 | "type": "arrow"
794 | },
795 | {
796 | "id": "gRg2SFar3ISdP0r2NvDz2",
797 | "type": "arrow"
798 | },
799 | {
800 | "id": "Vp64flTlg6cJPCKSp94ZU",
801 | "type": "text"
802 | },
803 | {
804 | "type": "text",
805 | "id": "Vp64flTlg6cJPCKSp94ZU"
806 | },
807 | {
808 | "id": "FBgc4K4AZxRT4MM7JlBoo",
809 | "type": "arrow"
810 | }
811 | ],
812 | "updated": 1641594427015
813 | },
814 | {
815 | "type": "text",
816 | "version": 53,
817 | "versionNonce": 1461102520,
818 | "isDeleted": false,
819 | "id": "w0N7f3ezaS8vifFH-8yLd",
820 | "fillStyle": "hachure",
821 | "strokeWidth": 1,
822 | "strokeStyle": "solid",
823 | "roughness": 0,
824 | "opacity": 100,
825 | "angle": 0,
826 | "x": 870,
827 | "y": 290,
828 | "strokeColor": "#000000",
829 | "backgroundColor": "transparent",
830 | "width": 80,
831 | "height": 25,
832 | "seed": 44154609,
833 | "groupIds": [],
834 | "strokeSharpness": "sharp",
835 | "boundElements": [],
836 | "updated": 1641594417571,
837 | "fontSize": 20,
838 | "fontFamily": 1,
839 | "text": "Role",
840 | "baseline": 18,
841 | "textAlign": "center",
842 | "verticalAlign": "middle",
843 | "containerId": "PgXUgmO92DhydHPO4c4RD",
844 | "originalText": "Role"
845 | },
846 | {
847 | "type": "text",
848 | "version": 61,
849 | "versionNonce": 1568018257,
850 | "isDeleted": false,
851 | "id": "lG-7UiokO-k2wza2oFDK7",
852 | "fillStyle": "hachure",
853 | "strokeWidth": 1,
854 | "strokeStyle": "solid",
855 | "roughness": 0,
856 | "opacity": 100,
857 | "angle": 0,
858 | "x": 890,
859 | "y": 550,
860 | "strokeColor": "#000000",
861 | "backgroundColor": "transparent",
862 | "width": 80,
863 | "height": 25,
864 | "seed": 1992190833,
865 | "groupIds": [],
866 | "strokeSharpness": "sharp",
867 | "boundElements": [],
868 | "updated": 1640117193216,
869 | "fontSize": 20,
870 | "fontFamily": 1,
871 | "text": "Role",
872 | "baseline": 18,
873 | "textAlign": "center",
874 | "verticalAlign": "middle",
875 | "containerId": "YOvj5fIbctNgV7xG9-OLK",
876 | "originalText": "Role"
877 | },
878 | {
879 | "type": "text",
880 | "version": 99,
881 | "versionNonce": 861624305,
882 | "isDeleted": false,
883 | "id": "Vp64flTlg6cJPCKSp94ZU",
884 | "fillStyle": "hachure",
885 | "strokeWidth": 1,
886 | "strokeStyle": "solid",
887 | "roughness": 0,
888 | "opacity": 100,
889 | "angle": 0,
890 | "x": 1150,
891 | "y": 550,
892 | "strokeColor": "#000000",
893 | "backgroundColor": "transparent",
894 | "width": 80,
895 | "height": 25,
896 | "seed": 2000889937,
897 | "groupIds": [],
898 | "strokeSharpness": "sharp",
899 | "boundElements": [],
900 | "updated": 1640117255687,
901 | "fontSize": 20,
902 | "fontFamily": 1,
903 | "text": "Role",
904 | "baseline": 18,
905 | "textAlign": "center",
906 | "verticalAlign": "middle",
907 | "containerId": "UbxFLoEWt_kCcLLtlzF7g",
908 | "originalText": "Role"
909 | },
910 | {
911 | "type": "text",
912 | "version": 13,
913 | "versionNonce": 1188531025,
914 | "isDeleted": false,
915 | "id": "b-Zl82ctVYdfyi8YinFTl",
916 | "fillStyle": "hachure",
917 | "strokeWidth": 1,
918 | "strokeStyle": "solid",
919 | "roughness": 0,
920 | "opacity": 100,
921 | "angle": 0,
922 | "x": 880,
923 | "y": 440,
924 | "strokeColor": "#000000",
925 | "backgroundColor": "transparent",
926 | "width": 66,
927 | "height": 25,
928 | "seed": 1646705983,
929 | "groupIds": [],
930 | "strokeSharpness": "sharp",
931 | "boundElements": [],
932 | "updated": 1640117212833,
933 | "fontSize": 20,
934 | "fontFamily": 1,
935 | "text": "create",
936 | "baseline": 18,
937 | "textAlign": "left",
938 | "verticalAlign": "top",
939 | "containerId": null,
940 | "originalText": "create"
941 | },
942 | {
943 | "type": "line",
944 | "version": 7,
945 | "versionNonce": 189323185,
946 | "isDeleted": false,
947 | "id": "IgG6BTDDsVRIxhiaS9y5l",
948 | "fillStyle": "hachure",
949 | "strokeWidth": 1,
950 | "strokeStyle": "solid",
951 | "roughness": 0,
952 | "opacity": 100,
953 | "angle": 0,
954 | "x": 940,
955 | "y": 720,
956 | "strokeColor": "#000000",
957 | "backgroundColor": "transparent",
958 | "width": 0,
959 | "height": 60,
960 | "seed": 67029521,
961 | "groupIds": [],
962 | "strokeSharpness": "round",
963 | "boundElements": [],
964 | "updated": 1640117225728,
965 | "startBinding": null,
966 | "endBinding": null,
967 | "lastCommittedPoint": null,
968 | "startArrowhead": null,
969 | "endArrowhead": null,
970 | "points": [
971 | [
972 | 0,
973 | 0
974 | ],
975 | [
976 | 0,
977 | -60
978 | ]
979 | ]
980 | },
981 | {
982 | "type": "line",
983 | "version": 23,
984 | "versionNonce": 1145228785,
985 | "isDeleted": false,
986 | "id": "D8KLYhSYDkbIURybKKZjO",
987 | "fillStyle": "hachure",
988 | "strokeWidth": 1,
989 | "strokeStyle": "solid",
990 | "roughness": 0,
991 | "opacity": 100,
992 | "angle": 0,
993 | "x": 1220,
994 | "y": 720,
995 | "strokeColor": "#000000",
996 | "backgroundColor": "transparent",
997 | "width": 0,
998 | "height": 60,
999 | "seed": 1175743359,
1000 | "groupIds": [],
1001 | "strokeSharpness": "round",
1002 | "boundElements": [],
1003 | "updated": 1640117321555,
1004 | "startBinding": null,
1005 | "endBinding": null,
1006 | "lastCommittedPoint": null,
1007 | "startArrowhead": null,
1008 | "endArrowhead": null,
1009 | "points": [
1010 | [
1011 | 0,
1012 | 0
1013 | ],
1014 | [
1015 | 0,
1016 | -60
1017 | ]
1018 | ]
1019 | },
1020 | {
1021 | "type": "text",
1022 | "version": 13,
1023 | "versionNonce": 1560631441,
1024 | "isDeleted": false,
1025 | "id": "iyUF9jwzV4n4AbARbeLJL",
1026 | "fillStyle": "hachure",
1027 | "strokeWidth": 1,
1028 | "strokeStyle": "solid",
1029 | "roughness": 0,
1030 | "opacity": 100,
1031 | "angle": 0,
1032 | "x": 920,
1033 | "y": 680,
1034 | "strokeColor": "#000000",
1035 | "backgroundColor": "transparent",
1036 | "width": 71,
1037 | "height": 25,
1038 | "seed": 190456831,
1039 | "groupIds": [],
1040 | "strokeSharpness": "sharp",
1041 | "boundElements": [],
1042 | "updated": 1640117232884,
1043 | "fontSize": 20,
1044 | "fontFamily": 1,
1045 | "text": "inherits",
1046 | "baseline": 18,
1047 | "textAlign": "left",
1048 | "verticalAlign": "top",
1049 | "containerId": null,
1050 | "originalText": "inherits"
1051 | },
1052 | {
1053 | "type": "text",
1054 | "version": 29,
1055 | "versionNonce": 814613919,
1056 | "isDeleted": false,
1057 | "id": "4CADzREncA4NFdCeJEYRj",
1058 | "fillStyle": "hachure",
1059 | "strokeWidth": 1,
1060 | "strokeStyle": "solid",
1061 | "roughness": 0,
1062 | "opacity": 100,
1063 | "angle": 0,
1064 | "x": 1200,
1065 | "y": 680,
1066 | "strokeColor": "#000000",
1067 | "backgroundColor": "transparent",
1068 | "width": 71,
1069 | "height": 25,
1070 | "seed": 609832945,
1071 | "groupIds": [],
1072 | "strokeSharpness": "sharp",
1073 | "boundElements": [],
1074 | "updated": 1640117321555,
1075 | "fontSize": 20,
1076 | "fontFamily": 1,
1077 | "text": "inherits",
1078 | "baseline": 18,
1079 | "textAlign": "left",
1080 | "verticalAlign": "top",
1081 | "containerId": null,
1082 | "originalText": "inherits"
1083 | },
1084 | {
1085 | "type": "arrow",
1086 | "version": 16,
1087 | "versionNonce": 347736415,
1088 | "isDeleted": false,
1089 | "id": "ycChXnhlXWkTK9vlPvaRf",
1090 | "fillStyle": "hachure",
1091 | "strokeWidth": 1,
1092 | "strokeStyle": "solid",
1093 | "roughness": 0,
1094 | "opacity": 100,
1095 | "angle": 0,
1096 | "x": 1220,
1097 | "y": 540,
1098 | "strokeColor": "#000000",
1099 | "backgroundColor": "transparent",
1100 | "width": 80,
1101 | "height": 200,
1102 | "seed": 630972689,
1103 | "groupIds": [],
1104 | "strokeSharpness": "round",
1105 | "boundElements": [],
1106 | "updated": 1640117266845,
1107 | "startBinding": null,
1108 | "endBinding": {
1109 | "elementId": "cAjc0FHCCImV0Pf_GVsyk",
1110 | "focus": -0.0967741935483871,
1111 | "gap": 15
1112 | },
1113 | "lastCommittedPoint": null,
1114 | "startArrowhead": null,
1115 | "endArrowhead": "arrow",
1116 | "points": [
1117 | [
1118 | 0,
1119 | 0
1120 | ],
1121 | [
1122 | 80,
1123 | -200
1124 | ]
1125 | ]
1126 | },
1127 | {
1128 | "type": "text",
1129 | "version": 32,
1130 | "versionNonce": 414696415,
1131 | "isDeleted": false,
1132 | "id": "tnXbNn2wQEKOaK6DPrXzk",
1133 | "fillStyle": "hachure",
1134 | "strokeWidth": 1,
1135 | "strokeStyle": "solid",
1136 | "roughness": 0,
1137 | "opacity": 100,
1138 | "angle": 0,
1139 | "x": 840,
1140 | "y": 820,
1141 | "strokeColor": "#000000",
1142 | "backgroundColor": "transparent",
1143 | "width": 197,
1144 | "height": 25,
1145 | "seed": 874344895,
1146 | "groupIds": [],
1147 | "strokeSharpness": "sharp",
1148 | "boundElements": [],
1149 | "updated": 1640117295773,
1150 | "fontSize": 20,
1151 | "fontFamily": 1,
1152 | "text": "Application reporting",
1153 | "baseline": 18,
1154 | "textAlign": "left",
1155 | "verticalAlign": "top",
1156 | "containerId": null,
1157 | "originalText": "Application reporting"
1158 | },
1159 | {
1160 | "type": "text",
1161 | "version": 50,
1162 | "versionNonce": 819162911,
1163 | "isDeleted": false,
1164 | "id": "nF6-ddPR7E8BljeYKWeyu",
1165 | "fillStyle": "hachure",
1166 | "strokeWidth": 1,
1167 | "strokeStyle": "solid",
1168 | "roughness": 0,
1169 | "opacity": 100,
1170 | "angle": 0,
1171 | "x": 1120,
1172 | "y": 820,
1173 | "strokeColor": "#000000",
1174 | "backgroundColor": "transparent",
1175 | "width": 188,
1176 | "height": 25,
1177 | "seed": 1424672177,
1178 | "groupIds": [],
1179 | "strokeSharpness": "sharp",
1180 | "boundElements": [],
1181 | "updated": 1640117330689,
1182 | "fontSize": 20,
1183 | "fontFamily": 1,
1184 | "text": "Application backend",
1185 | "baseline": 18,
1186 | "textAlign": "left",
1187 | "verticalAlign": "top",
1188 | "containerId": null,
1189 | "originalText": "Application backend"
1190 | },
1191 | {
1192 | "type": "arrow",
1193 | "version": 25,
1194 | "versionNonce": 2113416383,
1195 | "isDeleted": false,
1196 | "id": "fIVQAtg5TlTuuwKzGkZ00",
1197 | "fillStyle": "hachure",
1198 | "strokeWidth": 1,
1199 | "strokeStyle": "solid",
1200 | "roughness": 0,
1201 | "opacity": 100,
1202 | "angle": 0,
1203 | "x": 980,
1204 | "y": 540,
1205 | "strokeColor": "#000000",
1206 | "backgroundColor": "transparent",
1207 | "width": 260,
1208 | "height": 200,
1209 | "seed": 334902079,
1210 | "groupIds": [],
1211 | "strokeSharpness": "round",
1212 | "boundElements": [],
1213 | "updated": 1640117352140,
1214 | "startBinding": null,
1215 | "endBinding": {
1216 | "elementId": "cAjc0FHCCImV0Pf_GVsyk",
1217 | "focus": 0.17190388170055454,
1218 | "gap": 15
1219 | },
1220 | "lastCommittedPoint": null,
1221 | "startArrowhead": null,
1222 | "endArrowhead": "arrow",
1223 | "points": [
1224 | [
1225 | 0,
1226 | 0
1227 | ],
1228 | [
1229 | 260,
1230 | -200
1231 | ]
1232 | ]
1233 | },
1234 | {
1235 | "type": "text",
1236 | "version": 14,
1237 | "versionNonce": 820179839,
1238 | "isDeleted": false,
1239 | "id": "iL5hZRjYRSJrP32fSce4U",
1240 | "fillStyle": "hachure",
1241 | "strokeWidth": 1,
1242 | "strokeStyle": "solid",
1243 | "roughness": 0,
1244 | "opacity": 100,
1245 | "angle": 0,
1246 | "x": 1020,
1247 | "y": 460,
1248 | "strokeColor": "#000000",
1249 | "backgroundColor": "transparent",
1250 | "width": 60,
1251 | "height": 25,
1252 | "seed": 1318491217,
1253 | "groupIds": [],
1254 | "strokeSharpness": "sharp",
1255 | "boundElements": [],
1256 | "updated": 1640117375393,
1257 | "fontSize": 20,
1258 | "fontFamily": 1,
1259 | "text": "select",
1260 | "baseline": 18,
1261 | "textAlign": "left",
1262 | "verticalAlign": "top",
1263 | "containerId": null,
1264 | "originalText": "select"
1265 | },
1266 | {
1267 | "id": "FBgc4K4AZxRT4MM7JlBoo",
1268 | "type": "arrow",
1269 | "x": 980,
1270 | "y": 320,
1271 | "width": 200,
1272 | "height": 180,
1273 | "angle": 0,
1274 | "strokeColor": "#000000",
1275 | "backgroundColor": "transparent",
1276 | "fillStyle": "hachure",
1277 | "strokeWidth": 1,
1278 | "strokeStyle": "solid",
1279 | "roughness": 1,
1280 | "opacity": 100,
1281 | "groupIds": [],
1282 | "strokeSharpness": "round",
1283 | "seed": 1664506312,
1284 | "version": 17,
1285 | "versionNonce": 795480520,
1286 | "isDeleted": false,
1287 | "boundElements": null,
1288 | "updated": 1641594427015,
1289 | "points": [
1290 | [
1291 | 0,
1292 | 0
1293 | ],
1294 | [
1295 | 200,
1296 | 180
1297 | ]
1298 | ],
1299 | "lastCommittedPoint": null,
1300 | "startBinding": null,
1301 | "endBinding": {
1302 | "elementId": "UbxFLoEWt_kCcLLtlzF7g",
1303 | "focus": 0.3364928909952607,
1304 | "gap": 20
1305 | },
1306 | "startArrowhead": null,
1307 | "endArrowhead": "arrow"
1308 | }
1309 | ],
1310 | "appState": {
1311 | "gridSize": 20,
1312 | "viewBackgroundColor": "#ffffff"
1313 | },
1314 | "files": {
1315 | "d1564f04fc39917fb6ab5b1bbcc0930445457501": {
1316 | "mimeType": "image/png",
1317 | "id": "d1564f04fc39917fb6ab5b1bbcc0930445457501",
1318 | "dataURL": "data:image/png;base64,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",
1319 | "created": 1640116504462
1320 | }
1321 | }
1322 | }
--------------------------------------------------------------------------------
/schemas/ELK1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jparnaudeau/terraform-postgresql-database-admin/ef34b2d22741907712c405a555013ff71da3fbab/schemas/ELK1.png
--------------------------------------------------------------------------------
/schemas/FakeApplication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jparnaudeau/terraform-postgresql-database-admin/ef34b2d22741907712c405a555013ff71da3fbab/schemas/FakeApplication.png
--------------------------------------------------------------------------------