Instances for the keycloak upgrade.

This commit is contained in:
Andrea Dell'Amico 2026-08-11 18:35:46 +02:00
parent 46e62720ec
commit e22d4e61a8
Signed by: adellam
GPG Key ID: 147ABE6CEB9E20FF
10 changed files with 591 additions and 0 deletions

View File

@ -0,0 +1,202 @@
locals {
# Addresses actually used, one per instance
keycloak_ip = slice(var.keycloak_ip, 0, var.keycloak_data.vm_count)
}
#
# Traffic between the cluster nodes (Infinispan/JGroups discovery and
# replication). The addresses are static, so no rule depends on the instances
#
resource "openstack_networking_secgroup_v2" "keycloak_cluster_traffic" {
name = "keycloak_cluster_traffic"
delete_default_rules = "true"
description = "Traffic between the nodes of the keycloak cluster"
}
resource "openstack_networking_secgroup_rule_v2" "cluster_tcp" {
for_each = toset(local.keycloak_ip)
security_group_id = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
description = "TCP traffic from the keycloak node ${each.value}"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
remote_ip_prefix = "${each.value}/32"
}
resource "openstack_networking_secgroup_rule_v2" "cluster_udp" {
for_each = toset(local.keycloak_ip)
security_group_id = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
description = "UDP traffic from the keycloak node ${each.value}"
direction = "ingress"
ethertype = "IPv4"
protocol = "udp"
remote_ip_prefix = "${each.value}/32"
}
resource "openstack_networking_secgroup_rule_v2" "cluster_egress" {
for_each = toset(local.keycloak_ip)
security_group_id = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
description = "Traffic to the keycloak node ${each.value}"
direction = "egress"
ethertype = "IPv4"
remote_ip_prefix = "${each.value}/32"
}
# JGroups can use multicast for the discovery
resource "openstack_networking_secgroup_rule_v2" "cluster_igmp_ingress" {
security_group_id = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
description = "Ingress IGMP traffic between the keycloak nodes"
direction = "ingress"
ethertype = "IPv4"
protocol = "igmp"
remote_ip_prefix = "0.0.0.0/0"
}
resource "openstack_networking_secgroup_rule_v2" "cluster_igmp_egress" {
security_group_id = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
description = "Egress IGMP traffic between the keycloak nodes"
direction = "egress"
ethertype = "IPv4"
protocol = "igmp"
remote_ip_prefix = "0.0.0.0/0"
}
#
# Traffic from the main L7 load balancers and from Prometheus
#
resource "openstack_networking_secgroup_v2" "traffic_to_keycloak" {
name = "traffic_to_keycloak_from_the_main_load_balancers"
delete_default_rules = "true"
description = "Traffic from the main L7 HAPROXY load balancers to keycloak"
}
resource "openstack_networking_secgroup_rule_v2" "haproxy_to_keycloak_https" {
for_each = toset(var.haproxy_l7_ip)
security_group_id = openstack_networking_secgroup_v2.traffic_to_keycloak.id
description = "HTTPS traffic from the HAPROXY L7 ${each.value}"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.keycloak_data.https_port
port_range_max = var.keycloak_data.https_port
remote_ip_prefix = "${each.value}/32"
}
resource "openstack_networking_secgroup_rule_v2" "haproxy_to_keycloak_management" {
for_each = toset(var.haproxy_l7_ip)
security_group_id = openstack_networking_secgroup_v2.traffic_to_keycloak.id
description = "Traffic from the HAPROXY L7 ${each.value} to the management port"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.keycloak_data.management_port
port_range_max = var.keycloak_data.management_port
remote_ip_prefix = "${each.value}/32"
}
resource "openstack_networking_secgroup_rule_v2" "prometheus_to_keycloak_metrics" {
security_group_id = openstack_networking_secgroup_v2.traffic_to_keycloak.id
description = "Requests from Prometheus to the management port, that serves the metrics"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.keycloak_data.management_port
port_range_max = var.keycloak_data.management_port
remote_ip_prefix = var.prometheus_cidr
}
#
# Hard anti affinity: the two instances never share a hypervisor
#
resource "openstack_compute_servergroup_v2" "keycloak" {
name = var.keycloak_data.srv_name
policies = [var.keycloak_data.affinity_policy]
}
#
# Ports. One on the main private network, one on the dedicated network of the
# database: keycloak reaches PostgreSQL only through the latter
#
resource "openstack_networking_port_v2" "keycloak_main_port" {
count = var.keycloak_data.vm_count
name = format("%s-%02d-main-port", var.keycloak_data.srv_name, count.index + 1)
description = "Port of the keycloak instance on the main private network"
admin_state_up = true
network_id = var.main_private_network_id
security_group_ids = [
var.default_security_group_id,
openstack_networking_secgroup_v2.keycloak_cluster_traffic.id,
openstack_networking_secgroup_v2.traffic_to_keycloak.id,
]
fixed_ip {
subnet_id = var.main_private_subnet_id
ip_address = local.keycloak_ip[count.index]
}
}
resource "openstack_networking_port_v2" "keycloak_postgresql_port" {
count = var.keycloak_data.vm_count
name = format("%s-%02d-postgresql-port", var.keycloak_data.srv_name, count.index + 1)
description = "Port of the keycloak instance on the dedicated network of the database"
admin_state_up = true
network_id = var.postgresql_network_id
security_group_ids = [var.postgresql_client_security_group_id]
# The address comes from the allocation pool of the dedicated subnet
fixed_ip {
subnet_id = var.postgresql_subnet_id
}
}
#
# Instances
#
resource "openstack_compute_instance_v2" "keycloak" {
count = var.keycloak_data.vm_count
name = format("%s-%02d", var.keycloak_data.srv_name, count.index + 1)
availability_zone_hints = var.availability_zone
flavor_name = var.keycloak_data.flavor
key_pair = var.ssh_key_name
scheduler_hints {
group = openstack_compute_servergroup_v2.keycloak.id
}
block_device {
uuid = var.image.uuid
source_type = "image"
volume_size = var.keycloak_data.boot_vol_size
boot_index = 0
destination_type = "volume"
delete_on_termination = false
}
network {
port = openstack_networking_port_v2.keycloak_main_port[count.index].id
}
network {
port = openstack_networking_port_v2.keycloak_postgresql_port[count.index].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
]
}
}
#
# Optional CNAMEs, pointing to the load balancer that publishes the service
#
resource "openstack_dns_recordset_v2" "keycloak_recordset" {
for_each = var.keycloak_recordsets
zone_id = var.dns_zone_id
name = each.value.name
description = each.value.description
ttl = 8600
type = "CNAME"
records = [var.keycloak_cname_target]
}

View File

@ -0,0 +1,39 @@
output "keycloak_data" {
description = "The input data, re-exported for the dependent workspaces"
value = var.keycloak_data
}
output "keycloak_instance_ids" {
value = openstack_compute_instance_v2.keycloak[*].id
}
output "keycloak_instance_names" {
value = openstack_compute_instance_v2.keycloak[*].name
}
output "keycloak_ip" {
description = "Addresses of the instances on the main private network"
value = local.keycloak_ip
}
output "keycloak_postgresql_ip" {
description = "Addresses of the instances on the dedicated network of the database"
value = openstack_networking_port_v2.keycloak_postgresql_port[*].all_fixed_ips
}
output "keycloak_server_group_id" {
value = openstack_compute_servergroup_v2.keycloak.id
}
output "keycloak_cluster_security_group_id" {
value = openstack_networking_secgroup_v2.keycloak_cluster_traffic.id
}
output "traffic_to_keycloak_security_group_id" {
description = "Security group that allows the traffic from the load balancers, to be added to the ports of new clients"
value = openstack_networking_secgroup_v2.traffic_to_keycloak.id
}
output "keycloak_recordsets" {
value = var.keycloak_recordsets
}

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 @@
#
# Keycloak cluster.
#
# Derived from the d4science 'keycloak' module, rewritten for this repository:
# the ports are separate resources, the security groups go on the ports, and
# nothing is read from another state inside the module. The addresses of the
# instances are known in advance (keycloak_ip), so the rules of the cluster
# security group do not depend on the instances being created first.
#
variable "keycloak_data" {
description = "Instances of the keycloak cluster. m1.medium is RAM 4 - VCPUs 2"
type = object({
srv_name = optional(string, "keycloak")
vm_count = optional(number, 2)
flavor = optional(string, "m1.medium")
boot_vol_size = optional(number, 30)
# 'anti-affinity' is hard: the scheduler fails instead of co-locating the
# instances. 'soft-anti-affinity' only expresses a preference
affinity_policy = optional(string, "anti-affinity")
# Ports the service listens on
https_port = optional(number, 9443)
management_port = optional(number, 9000)
})
default = {}
}
variable "keycloak_ip" {
type = list(string)
description = "Addresses of the instances on the main private network, one per instance"
validation {
condition = length(var.keycloak_ip) >= var.keycloak_data.vm_count
error_message = "keycloak_ip must contain at least vm_count addresses."
}
}
# 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 "haproxy_l7_ip" {
type = list(string)
description = "Addresses of the L7 HAPROXY load balancers, allowed to reach the service"
}
variable "prometheus_cidr" {
type = string
description = "Address of the Prometheus server, allowed to scrape the management port"
}
# Data that comes from the postgresql workspace
variable "postgresql_network_id" {
type = string
description = "ID of the dedicated network of the PostgreSQL service"
}
variable "postgresql_subnet_id" {
type = string
description = "ID of the dedicated subnet of the PostgreSQL service"
}
variable "postgresql_client_security_group_id" {
type = string
description = "Security group that allows the connections to the PostgreSQL service"
}
variable "availability_zone" {
type = string
description = "Availability zone hint of the instances"
}
variable "image" {
description = "Image of the instances: 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 CNAMEs pointing to the load balancer that publishes the service
variable "dns_zone_id" {
type = string
default = ""
description = "ID of the DNS zone. Required when keycloak_recordsets is not empty"
}
variable "keycloak_cname_target" {
type = string
default = ""
description = "Target of the CNAMEs, usually the name of the main load balancer, with the trailing dot"
}
variable "keycloak_recordsets" {
description = "CNAMEs that publish the service through the load balancer"
type = map(object({
name = string
description = string
}))
default = {}
}

59
s2i2s/keycloak/README.md Normal file
View File

@ -0,0 +1,59 @@
# Keycloak cluster of the S2I2S project
Two VMs, `m1.medium` (RAM 4 - VCPUs 2), Ubuntu 24.04, 30 GB of root disk, no
data volume, in a server group with the **hard** anti affinity policy
(`anti-affinity`: the scheduler fails instead of putting the two instances on
the same hypervisor).
Each instance has **two interfaces**:
| Interface | Address | Use |
|---|---|---|
| main private network | `10.10.0.163`, `10.10.0.164` | traffic from the L7 HAPROXY load balancers and from Prometheus, administration |
| `postgresql-srv-net` | from the DHCP pool | the only way to the database |
Security groups:
* `keycloak_cluster_traffic` — TCP, UDP and IGMP between the two nodes
(Infinispan/JGroups). The rules use the static addresses of the instances, so
they do not depend on the instances being created first;
* `traffic_to_keycloak_from_the_main_load_balancers` — ingress on 9443 from each
HAPROXY L7, and on 9000 (management and metrics) from the load balancers and
from Prometheus;
* `vm_access_to_the_postgresql_service` — taken from the `postgresql` workspace,
on the port in the dedicated network of the database.
A CNAME `accounts.s2i2s.cloud.isti.cnr.it` pointing to the main load balancer is
created; the service itself is published by the HAPROXY L7 configuration (see
`docs/main_load_balancer.md` in `infrastructure-playbooks`, variable
`haproxy_l7_services`).
The resources and the sizing are in
[`../../modules/keycloak`](../../modules/keycloak), as defaults of
`keycloak_data`: override them in the module call to change them. The addresses
of the instances come from the address plan in [`../variables`](../variables)
(`basic_services_ip.keycloak_1` and `_2`, exported as the `keycloak_ip` list).
## Order of the applies
```
main_net_dns_router -> project-setup -> postgresql -> keycloak
```
This workspace reads the state of all three: the network and the DNS zone, the
default security group and the addresses of the load balancers and of
Prometheus, and the dedicated network of the database with its client security
group.
```bash
tofu init
tofu plan -out=keycloak.plan
tofu apply keycloak.plan
```
After the apply, regenerate the ansible inventory in
`infrastructure-playbooks`, which reads this state:
```bash
ansible-playbook tofu-inventory.yml --diff
```

Binary file not shown.

107
s2i2s/keycloak/main.tf Normal file
View File

@ -0,0 +1,107 @@
# Keycloak cluster of the S2I2S OpenStack project.
#
# Two VMs in hard anti affinity ('anti-affinity' server group policy: the
# scheduler fails instead of putting them on the same hypervisor), 30 GB of root
# disk, m1.medium (RAM 4 - VCPUs 2), no data volume.
#
# Each instance has two interfaces:
# - the main private network (10.10.0.163, 10.10.0.164), where the L7 HAPROXY
# load balancers and Prometheus reach it;
# - the dedicated network of the database, the only way to PostgreSQL. The
# address comes from the allocation pool of that subnet.
#
# Apply order: main_net_dns_router -> project-setup -> postgresql -> this one.
data "terraform_remote_state" "privnet_dns_router" {
backend = "local"
config = {
path = "../main_net_dns_router/terraform.tfstate"
}
}
data "terraform_remote_state" "project_setup" {
backend = "local"
config = {
path = "../project-setup/terraform.tfstate"
}
}
# Dedicated network and client security group of the database
data "terraform_remote_state" "postgresql" {
backend = "local"
config = {
path = "../postgresql/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
basic_services_ip = data.terraform_remote_state.project_setup.outputs.basic_services_ip
main_haproxy_l7_ip = data.terraform_remote_state.project_setup.outputs.main_haproxy_l7_ip
main_loadbalancer_name = data.terraform_remote_state.project_setup.outputs.main_loadbalancer_hostname
# From the postgresql state
postgresql_network_id = data.terraform_remote_state.postgresql.outputs.postgresql_network_id
postgresql_subnet_id = data.terraform_remote_state.postgresql.outputs.postgresql_subnet_id
postgresql_client_security_group_id = data.terraform_remote_state.postgresql.outputs.postgresql_client_access_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
keycloak_ip = module.project_variables.keycloak_ip
}
module "keycloak" {
source = "../../modules/keycloak"
# Address plan of the project. The sizing comes from the module defaults
keycloak_ip = local.keycloak_ip
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
haproxy_l7_ip = local.main_haproxy_l7_ip
prometheus_cidr = local.basic_services_ip.prometheus_cidr
postgresql_network_id = local.postgresql_network_id
postgresql_subnet_id = local.postgresql_subnet_id
postgresql_client_security_group_id = local.postgresql_client_security_group_id
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
# The service is published by the main load balancer, so the public name is a
# CNAME to it
dns_zone_id = local.dns_zone_id
keycloak_cname_target = local.main_loadbalancer_name
keycloak_recordsets = {
accounts = {
name = "accounts.${local.dns_zone.name}"
description = "Keycloak of the S2I2S project, published by the main load balancer"
}
}
}

42
s2i2s/keycloak/outputs.tf Normal file
View File

@ -0,0 +1,42 @@
output "keycloak_instance_ids" {
value = module.keycloak.keycloak_instance_ids
}
output "keycloak_instance_names" {
value = module.keycloak.keycloak_instance_names
}
output "keycloak_data" {
value = module.keycloak.keycloak_data
}
output "keycloak_ip" {
description = "Addresses on the main private network. Used by the ansible inventory"
value = module.keycloak.keycloak_ip
}
output "keycloak_postgresql_ip" {
description = "Addresses on the dedicated network of the database"
value = module.keycloak.keycloak_postgresql_ip
}
output "keycloak_server_group_id" {
value = module.keycloak.keycloak_server_group_id
}
output "keycloak_cluster_security_group_id" {
value = module.keycloak.keycloak_cluster_security_group_id
}
output "traffic_to_keycloak_security_group_id" {
value = module.keycloak.traffic_to_keycloak_security_group_id
}
output "keycloak_recordsets" {
value = module.keycloak.keycloak_recordsets
}
# Re-exported for 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