Postgresql instance in the s2i2s project.

This commit is contained in:
Andrea Dell'Amico 2026-08-11 18:34:29 +02:00
parent df574fdd31
commit 9f1a34a266
Signed by: adellam
GPG Key ID: 147ABE6CEB9E20FF
9 changed files with 662 additions and 0 deletions

View File

@ -0,0 +1,59 @@
output "postgresql_data" {
description = "The input data, re-exported for the dependent workspaces"
value = var.postgresql_data
}
output "postgresql_server_id" {
value = openstack_compute_instance_v2.postgresql_server.id
}
output "postgresql_server_name" {
value = openstack_compute_instance_v2.postgresql_server.name
}
# Addresses
output "postgresql_main_ip" {
description = "Address on the main private network, used by ansible and by the monitoring"
value = var.postgresql_main_ip
}
output "postgresql_server_ip" {
description = "Address the service listens on"
value = var.postgresql_data.server_ip
}
output "postgresql_port" {
value = var.postgresql_data.port
}
# Dedicated network, needed by every workspace that runs a client of the service
output "postgresql_network" {
value = openstack_networking_network_v2.postgresql_net
}
output "postgresql_network_id" {
value = openstack_networking_network_v2.postgresql_net.id
}
output "postgresql_subnet" {
value = openstack_networking_subnet_v2.postgresql_subnet
}
output "postgresql_subnet_id" {
value = openstack_networking_subnet_v2.postgresql_subnet.id
}
# Security groups
output "postgresql_access_security_group_id" {
description = "Security group of the server port"
value = openstack_networking_secgroup_v2.postgresql_access.id
}
output "postgresql_client_access_security_group_id" {
description = "Security group to put on the port that a client has in the dedicated network"
value = openstack_networking_secgroup_v2.postgresql_client_access.id
}
output "postgresql_client_access_security_group_name" {
value = openstack_networking_secgroup_v2.postgresql_client_access.name
}

View File

@ -0,0 +1,252 @@
#
# Dedicated network of the PostgreSQL service.
# No gateway: it carries database traffic only, the instances reach the rest of
# the world through their port on the main private network.
#
resource "openstack_networking_network_v2" "postgresql_net" {
name = var.postgresql_data.network_name
description = var.postgresql_data.network_description
admin_state_up = "true"
external = "false"
dns_domain = var.dns_zone_name
mtu = var.mtu_size
port_security_enabled = true
shared = false
region = var.main_region
}
resource "openstack_networking_subnet_v2" "postgresql_subnet" {
name = var.postgresql_data.subnet_name
description = var.postgresql_data.subnet_description
network_id = openstack_networking_network_v2.postgresql_net.id
cidr = var.postgresql_data.network_cidr
dns_nameservers = var.resolvers_ip
ip_version = 4
enable_dhcp = true
no_gateway = true
allocation_pool {
start = var.postgresql_data.allocation_pool_start
end = var.postgresql_data.allocation_pool_end
}
}
#
# Security group of the server: the service is reachable only from the
# dedicated network, and only on its port
#
resource "openstack_networking_secgroup_v2" "postgresql_access" {
name = "access_to_the_postgresql_service"
delete_default_rules = "true"
description = "Access to the PostgreSQL service through the dedicated network"
}
resource "openstack_networking_secgroup_rule_v2" "postgresql_ingress" {
security_group_id = openstack_networking_secgroup_v2.postgresql_access.id
description = "Connections to port ${var.postgresql_data.port} from the ${var.postgresql_data.network_cidr} network"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.postgresql_data.port
port_range_max = var.postgresql_data.port
remote_ip_prefix = var.postgresql_data.network_cidr
}
resource "openstack_networking_secgroup_rule_v2" "postgresql_ingress_icmp" {
security_group_id = openstack_networking_secgroup_v2.postgresql_access.id
description = "ICMP from the dedicated network"
direction = "ingress"
ethertype = "IPv4"
protocol = "icmp"
remote_ip_prefix = var.postgresql_data.network_cidr
}
# The port of the server has no default egress rule (delete_default_rules), so
# the answers and DHCP have to be allowed explicitly
resource "openstack_networking_secgroup_rule_v2" "postgresql_egress" {
security_group_id = openstack_networking_secgroup_v2.postgresql_access.id
description = "Egress traffic on the dedicated network"
direction = "egress"
ethertype = "IPv4"
remote_ip_prefix = var.postgresql_data.network_cidr
}
resource "openstack_networking_secgroup_rule_v2" "postgresql_egress_bootps" {
security_group_id = openstack_networking_secgroup_v2.postgresql_access.id
description = "DHCP requests on the dedicated network"
direction = "egress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = 67
port_range_max = 67
}
resource "openstack_networking_secgroup_rule_v2" "postgresql_ingress_bootpc" {
security_group_id = openstack_networking_secgroup_v2.postgresql_access.id
description = "DHCP answers on the dedicated network"
direction = "ingress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = 68
port_range_max = 68
}
#
# Security group of the clients: it goes on the port that every client of the
# database has on the dedicated network
#
resource "openstack_networking_secgroup_v2" "postgresql_client_access" {
name = "vm_access_to_the_postgresql_service"
delete_default_rules = "true"
description = "Access to the PostgreSQL service from the port of a VM in the dedicated network"
}
resource "openstack_networking_secgroup_rule_v2" "client_egress_postgresql" {
security_group_id = openstack_networking_secgroup_v2.postgresql_client_access.id
description = "Connections to port ${var.postgresql_data.port} of the PostgreSQL server"
direction = "egress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.postgresql_data.port
port_range_max = var.postgresql_data.port
remote_ip_prefix = var.postgresql_data.server_cidr
}
resource "openstack_networking_secgroup_rule_v2" "client_egress_icmp" {
security_group_id = openstack_networking_secgroup_v2.postgresql_client_access.id
description = "ICMP to the PostgreSQL server"
direction = "egress"
ethertype = "IPv4"
protocol = "icmp"
remote_ip_prefix = var.postgresql_data.server_cidr
}
resource "openstack_networking_secgroup_rule_v2" "client_egress_bootps" {
security_group_id = openstack_networking_secgroup_v2.postgresql_client_access.id
description = "DHCP requests on the dedicated network"
direction = "egress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = 67
port_range_max = 67
}
resource "openstack_networking_secgroup_rule_v2" "client_ingress_bootpc" {
security_group_id = openstack_networking_secgroup_v2.postgresql_client_access.id
description = "DHCP answers on the dedicated network"
direction = "ingress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = 68
port_range_max = 68
}
#
# Volumes: data and WAL on separate SSD backed volumes
#
resource "openstack_blockstorage_volume_v3" "postgresql_data_vol" {
name = var.postgresql_data.vol_data_name
description = "PostgreSQL data directory"
size = var.postgresql_data.vol_data_size
volume_type = var.postgresql_data.volume_type
enable_online_resize = true
}
resource "openstack_blockstorage_volume_v3" "postgresql_wal_vol" {
name = var.postgresql_data.vol_wal_name
description = "PostgreSQL write ahead log"
size = var.postgresql_data.vol_wal_size
volume_type = var.postgresql_data.volume_type
enable_online_resize = true
}
#
# Ports. They are defined outside the instance so that the addresses and the
# security groups do not depend on the life cycle of the VM
#
resource "openstack_networking_port_v2" "postgresql_main_port" {
name = "${var.postgresql_data.name}-main-port"
description = "Administration port of the PostgreSQL server on the main private network"
admin_state_up = true
network_id = var.main_private_network_id
security_group_ids = [var.default_security_group_id]
fixed_ip {
subnet_id = var.main_private_subnet_id
ip_address = var.postgresql_main_ip
}
}
resource "openstack_networking_port_v2" "postgresql_service_port" {
name = "${var.postgresql_data.name}-service-port"
description = "Service port of the PostgreSQL server on the dedicated network"
admin_state_up = true
network_id = openstack_networking_network_v2.postgresql_net.id
security_group_ids = [openstack_networking_secgroup_v2.postgresql_access.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.postgresql_subnet.id
ip_address = var.postgresql_data.server_ip
}
}
#
# Instance
#
resource "openstack_compute_instance_v2" "postgresql_server" {
name = var.postgresql_data.name
availability_zone_hints = var.availability_zone
flavor_name = var.postgresql_data.flavor
key_pair = var.ssh_key_name
block_device {
uuid = var.image.uuid
source_type = "image"
volume_size = var.postgresql_data.boot_vol_size
boot_index = 0
destination_type = "volume"
delete_on_termination = false
}
network {
port = openstack_networking_port_v2.postgresql_main_port.id
}
network {
port = openstack_networking_port_v2.postgresql_service_port.id
}
user_data = file(var.image.user_data_file)
# Do not replace the instance when the ssh key or the user data change
lifecycle {
ignore_changes = [
key_pair, user_data, network
]
}
}
resource "openstack_compute_volume_attach_v2" "postgresql_data_attach" {
instance_id = openstack_compute_instance_v2.postgresql_server.id
volume_id = openstack_blockstorage_volume_v3.postgresql_data_vol.id
device = var.postgresql_data.vol_data_device
}
resource "openstack_compute_volume_attach_v2" "postgresql_wal_attach" {
instance_id = openstack_compute_instance_v2.postgresql_server.id
volume_id = openstack_blockstorage_volume_v3.postgresql_wal_vol.id
device = var.postgresql_data.vol_wal_device
# The devices are assigned in order, so the WAL volume is attached after the
# data one
depends_on = [openstack_compute_volume_attach_v2.postgresql_data_attach]
}
#
# Optional A record on the main network address
#
resource "openstack_dns_recordset_v2" "postgresql_recordset" {
count = length(var.postgresql_recordset_name) > 0 ? 1 : 0
zone_id = var.dns_zone_id
name = var.postgresql_recordset_name
description = "Address of the PostgreSQL server on the main private network"
ttl = 8600
type = "A"
records = [var.postgresql_main_ip]
}

View File

@ -0,0 +1,10 @@
# Define required providers
terraform {
required_version = ">= 0.14.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = ">= 2.0.0"
}
}
}

View File

@ -0,0 +1,117 @@
#
# PostgreSQL server on a dedicated network.
#
# The module creates the dedicated network and subnet, the security groups, the
# data and WAL volumes, the two ports (main network and dedicated network) and
# the instance. Nothing is read from another state here: the caller passes in
# everything that comes from the other workspaces.
#
# Sizing and dedicated network belong to the service, so they have defaults
# here. The address on the main private network does not: it is part of the
# address plan of the project, and it is passed in by the caller.
#
variable "postgresql_data" {
description = "Instance, volumes and dedicated network of the PostgreSQL server"
type = object({
name = optional(string, "postgresql")
description = optional(string, "PostgreSQL server")
flavor = optional(string, "m1.large")
boot_vol_size = optional(number, 20)
# Address on the dedicated network, the only one the service listens on
server_ip = optional(string, "192.168.0.5")
server_cidr = optional(string, "192.168.0.5/32")
# Dedicated network and subnet
network_name = optional(string, "postgresql-srv-net")
network_description = optional(string, "Network used to communicate with the postgresql service")
subnet_name = optional(string, "postgresql-srv-subnet")
subnet_description = optional(string, "Subnet used to connect to the postgresql service")
network_cidr = optional(string, "192.168.0.0/22")
allocation_pool_start = optional(string, "192.168.0.100")
allocation_pool_end = optional(string, "192.168.3.254")
# Port the service listens on
port = optional(number, 5432)
# Data and WAL volumes. m1.large is RAM 8 - VCPUs 4
vol_data_name = optional(string, "postgresql-data")
vol_data_size = optional(number, 100)
vol_data_device = optional(string, "/dev/vdb")
vol_wal_name = optional(string, "postgresql-wal")
vol_wal_size = optional(number, 100)
vol_wal_device = optional(string, "/dev/vdc")
volume_type = optional(string, "CephSSD")
})
default = {}
}
# Part of the address plan of the project: no default on purpose
variable "postgresql_main_ip" {
type = string
description = "Address of the server on the main private network, used for the administration (ansible, monitoring, backups)"
}
# Data that comes from the network/DNS and project setup workspaces
variable "main_private_network_id" {
type = string
description = "ID of the main private network of the project"
}
variable "main_private_subnet_id" {
type = string
description = "ID of the main private subnet of the project"
}
variable "default_security_group_id" {
type = string
description = "ID of the 'default_for_all' security group of the project"
}
variable "dns_zone_name" {
type = string
description = "Name of the DNS zone of the project, with the trailing dot"
}
variable "resolvers_ip" {
type = list(string)
description = "DNS resolvers configured on the dedicated subnet"
}
variable "mtu_size" {
type = number
description = "MTU of the dedicated network"
}
variable "main_region" {
type = string
description = "OpenStack region of the dedicated network"
}
variable "availability_zone" {
type = string
description = "Availability zone hint of the instance"
}
variable "image" {
description = "Image of the instance: uuid and cloud-init user data file"
type = object({
uuid = string
user_data_file = string
})
}
variable "ssh_key_name" {
type = string
description = "Name of the SSH key pair injected by cloud-init"
}
# Optional DNS record, useful when the service has to be reached by name
variable "dns_zone_id" {
type = string
default = ""
description = "ID of the DNS zone. Required when postgresql_recordset_name is set"
}
variable "postgresql_recordset_name" {
type = string
default = ""
description = "Name of an A record pointing to the main network address, with the trailing dot. Empty means no record"
}

View File

@ -0,0 +1,55 @@
# PostgreSQL server of the S2I2S project
One VM, `m1.large` (RAM 8 - VCPUs 4), Ubuntu 24.04, 20 GB of root disk, with
**two interfaces**:
| Interface | Address | Use |
|---|---|---|
| main private network | `10.10.0.162` | administration: ansible, monitoring, backups |
| `postgresql-srv-net` (created here) | `192.168.0.5` | the only address the database listens on |
and **two SSD volumes of 100 GB** (`CephSSD`): `/dev/vdb` for the data
directory, `/dev/vdc` for the write ahead log.
The dedicated network is `192.168.0.0/22`, no gateway, DHCP pool
`192.168.0.100 - 192.168.3.254`. The addresses match what the ansible playbooks
already expect (`postgresql_production_host` and
`shared_postgresql_server_public` in `group_vars/openstack_s2i2s` of
`infrastructure-playbooks`).
Two security groups are created:
* `access_to_the_postgresql_service` — on the server port: ingress on 5432 and
ICMP from `192.168.0.0/22` only;
* `vm_access_to_the_postgresql_service` — to be put on the port that **every
client** has in the dedicated network: egress to `192.168.0.5/32:5432` plus
ICMP and DHCP. Its ID is an output of this workspace, used for instance by
`s2i2s/keycloak`.
The resources, the sizing and the dedicated network are in
[`../../modules/postgresql`](../../modules/postgresql), as defaults of
`postgresql_data`: override them in the module call to change them. The only
thing this workspace decides is the address on the main private network, which
comes from the address plan in [`../variables`](../variables)
(`basic_services_ip.postgresql`).
## Order of the applies
```
main_net_dns_router -> project-setup -> postgresql
```
This workspace reads the state of the first two.
```bash
tofu init
tofu plan -out=postgresql.plan
tofu apply postgresql.plan
```
After the apply, regenerate the ansible inventory in
`infrastructure-playbooks`, which reads this state:
```bash
ansible-playbook tofu-inventory.yml --diff
```

90
s2i2s/postgresql/main.tf Normal file
View File

@ -0,0 +1,90 @@
# PostgreSQL server of the S2I2S OpenStack project.
#
# One VM with two interfaces:
# - the main private network (10.10.0.162), used by ansible, the monitoring
# and the backups;
# - a dedicated network (192.168.0.0/22, address 192.168.0.5) created here,
# which is the only one the database listens on. Every client needs a port
# in that network carrying the 'vm_access_to_the_postgresql_service'
# security group, exported by this workspace.
#
# Two SSD volumes, 100 GB each: the data directory and the write ahead log.
#
# Apply order: main_net_dns_router -> project-setup -> this one.
# Network, DNS zone and images
data "terraform_remote_state" "privnet_dns_router" {
backend = "local"
config = {
path = "../main_net_dns_router/terraform.tfstate"
}
}
# Security groups of the project (the 'default_for_all' one is needed here)
data "terraform_remote_state" "project_setup" {
backend = "local"
config = {
path = "../project-setup/terraform.tfstate"
}
}
module "labs_common_variables" {
source = "../../modules/labs_common_variables"
}
module "project_variables" {
source = "../variables"
}
module "ssh_settings" {
source = "../../modules/ssh-key-ref"
}
locals {
# From the network/DNS state
dns_zone = data.terraform_remote_state.privnet_dns_router.outputs.dns_zone
dns_zone_id = data.terraform_remote_state.privnet_dns_router.outputs.dns_zone_id
main_private_network_id = data.terraform_remote_state.privnet_dns_router.outputs.main_private_network_id
main_private_subnet_id = data.terraform_remote_state.privnet_dns_router.outputs.main_subnet_network_id
# From the project setup state
default_security_group_id = data.terraform_remote_state.project_setup.outputs.default_security_group_id
# From the common and project variables
availability_zone = module.labs_common_variables.availability_zones_names.availability_zone_no_gpu
ubuntu_2404 = module.labs_common_variables.ubuntu_2404
ubuntu2404_data_file = module.labs_common_variables.ubuntu2404_data_file
resolvers_ip = module.labs_common_variables.resolvers_ip
mtu_size = module.labs_common_variables.mtu_size
main_region = module.labs_common_variables.main_region
basic_services_ip = module.project_variables.basic_services_ip
}
module "postgresql" {
source = "../../modules/postgresql"
# Address plan of the project. The sizing and the dedicated network come from
# the module defaults
postgresql_main_ip = local.basic_services_ip.postgresql
main_private_network_id = local.main_private_network_id
main_private_subnet_id = local.main_private_subnet_id
default_security_group_id = local.default_security_group_id
dns_zone_name = local.dns_zone.name
resolvers_ip = local.resolvers_ip
mtu_size = local.mtu_size
main_region = local.main_region
availability_zone = local.availability_zone
image = {
uuid = local.ubuntu_2404.uuid
user_data_file = local.ubuntu2404_data_file
}
ssh_key_name = module.ssh_settings.ssh_key_name
# A record on the main network address, so that the clients and the playbooks
# can use the name instead of the address
dns_zone_id = local.dns_zone_id
postgresql_recordset_name = "postgresql.${local.dns_zone.name}"
}

View File

@ -0,0 +1,64 @@
# Instance
output "postgresql_server_id" {
value = module.postgresql.postgresql_server_id
}
output "postgresql_server_name" {
value = module.postgresql.postgresql_server_name
}
output "postgresql_server_data" {
value = module.postgresql.postgresql_data
}
# Addresses
output "postgresql_main_ip" {
description = "Address on the main private network. Used by the ansible inventory"
value = module.postgresql.postgresql_main_ip
}
output "postgresql_server_ip" {
description = "Address the service listens on"
value = module.postgresql.postgresql_server_ip
}
output "postgresql_port" {
value = module.postgresql.postgresql_port
}
# Dedicated network. Every workspace that deploys a client of the database
# needs these
output "postgresql_network" {
value = module.postgresql.postgresql_network
}
output "postgresql_network_id" {
value = module.postgresql.postgresql_network_id
}
output "postgresql_subnet" {
value = module.postgresql.postgresql_subnet
}
output "postgresql_subnet_id" {
value = module.postgresql.postgresql_subnet_id
}
output "postgresql_access_security_group_id" {
value = module.postgresql.postgresql_access_security_group_id
}
output "postgresql_client_access_security_group_id" {
description = "To be added to the port that a client has in the dedicated network"
value = module.postgresql.postgresql_client_access_security_group_id
}
output "postgresql_client_access_security_group_name" {
value = module.postgresql.postgresql_client_access_security_group_name
}
# Re-exported for convenience of the dependent workspaces and of the ansible
# inventory generator
output "dns_zone" {
value = local.dns_zone
}

View File

@ -0,0 +1,14 @@
# Define required providers
terraform {
required_version = ">= 0.14.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = ">= 2.0.0"
}
}
}
provider "openstack" {
cloud = "s2i2s"
}

File diff suppressed because one or more lines are too long