VM for the forgejo installation in the s2i2s project.

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

166
modules/forgejo/forgejo.tf Normal file
View File

@ -0,0 +1,166 @@
#
# Forgejo (git service).
#
# One VM with two interfaces: the main private network, where the L7 load
# balancers reach it, and the dedicated network of the database. The
# repositories live on a volume of their own.
#
# forgejo terminates the TLS connection itself, with the certificate of the
# internal CA of the project: there is no nginx in front of it.
#
locals {
# One rule per (load balancer, service port) pair
forgejo_service_rules = {
for pair in setproduct(var.haproxy_l7_ip, var.forgejo_data.service_ports) :
"${pair[0]}-${pair[1]}" => { address = pair[0], port = pair[1] }
}
}
#
# Traffic from the main L7 load balancers
#
resource "openstack_networking_secgroup_v2" "traffic_to_forgejo" {
name = "traffic_to_forgejo_from_the_main_load_balancers"
delete_default_rules = "true"
description = "Traffic from the main L7 HAPROXY load balancers to the forgejo service"
}
resource "openstack_networking_secgroup_rule_v2" "haproxy_to_forgejo" {
for_each = local.forgejo_service_rules
security_group_id = openstack_networking_secgroup_v2.traffic_to_forgejo.id
description = "Traffic from the HAPROXY L7 ${each.value.address} to the port ${each.value.port}"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = each.value.port
port_range_max = each.value.port
remote_ip_prefix = "${each.value.address}/32"
}
# Git over SSH. The public port 22 is a listener of the main load balancer, that
# forwards to the HAPROXY L7 instances; they in turn reach the builtin SSH server
# of forgejo on this port. The VM port 22 stays with sshd
resource "openstack_networking_secgroup_rule_v2" "haproxy_to_forgejo_ssh" {
for_each = toset(var.haproxy_l7_ip)
security_group_id = openstack_networking_secgroup_v2.traffic_to_forgejo.id
description = "Git over SSH from the HAPROXY L7 ${each.value}"
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = var.forgejo_data.ssh_port
port_range_max = var.forgejo_data.ssh_port
remote_ip_prefix = "${each.value}/32"
}
#
# Data volume: the repositories and the attachments. Online resize enabled, like
# every other additional volume of the project
#
resource "openstack_blockstorage_volume_v3" "forgejo_data_vol" {
name = var.forgejo_data.vol_data_name
description = "Forgejo repositories and attachments"
size = var.forgejo_data.vol_data_size
volume_type = var.forgejo_data.volume_type
enable_online_resize = true
}
#
# Ports, declared outside the instance
#
resource "openstack_networking_port_v2" "forgejo_main_port" {
name = "${var.forgejo_data.name}-main-port"
description = "Port of the forgejo service 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.traffic_to_forgejo.id,
]
fixed_ip {
subnet_id = var.main_private_subnet_id
ip_address = var.forgejo_main_ip
}
}
resource "openstack_networking_port_v2" "forgejo_postgresql_port" {
name = "${var.forgejo_data.name}-postgresql-port"
description = "Port of the forgejo service 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
}
}
#
# Instance
#
resource "openstack_compute_instance_v2" "forgejo" {
name = var.forgejo_data.name
availability_zone_hints = var.availability_zone
flavor_name = var.forgejo_data.flavor
key_pair = var.ssh_key_name
block_device {
uuid = var.image.uuid
source_type = "image"
volume_size = var.forgejo_data.boot_vol_size
boot_index = 0
destination_type = "volume"
delete_on_termination = false
}
network {
port = openstack_networking_port_v2.forgejo_main_port.id
}
network {
port = openstack_networking_port_v2.forgejo_postgresql_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" "forgejo_data_attach" {
instance_id = openstack_compute_instance_v2.forgejo.id
volume_id = openstack_blockstorage_volume_v3.forgejo_data_vol.id
device = var.forgejo_data.vol_data_device
}
#
# A record on the main network address, so that the name can be used by the
# playbooks and by the load balancer configuration
#
resource "openstack_dns_recordset_v2" "forgejo_recordset" {
zone_id = var.dns_zone_id
name = "${var.forgejo_data.name}.${var.dns_zone_name}"
description = "Address of the forgejo service on the main private network"
ttl = 8600
type = "A"
records = [var.forgejo_main_ip]
}
#
# Public name of the service, a CNAME of the main load balancer that publishes
# it. The other name, gitea-s2i2s.isti.cnr.it, belongs to the isti.cnr.it zone
# and is moved here by hand at the migration: it is not managed by tofu
#
resource "openstack_dns_recordset_v2" "git_recordset" {
count = length(var.forgejo_public_name) > 0 ? 1 : 0
zone_id = var.dns_zone_id
name = "${var.forgejo_public_name}.${var.dns_zone_name}"
description = "Forgejo git service, published by the main load balancer"
ttl = 8600
type = "CNAME"
records = [var.forgejo_cname_target]
}

View File

@ -0,0 +1,45 @@
output "forgejo_data" {
description = "The input data, re-exported for the dependent workspaces"
value = var.forgejo_data
}
output "forgejo_instance_id" {
value = openstack_compute_instance_v2.forgejo.id
}
output "forgejo_server_name" {
value = openstack_compute_instance_v2.forgejo.name
}
output "forgejo_main_ip" {
description = "Address on the main private network, used by the ansible inventory"
value = var.forgejo_main_ip
}
output "forgejo_postgresql_ip" {
description = "Address on the dedicated network of the database"
value = openstack_networking_port_v2.forgejo_postgresql_port.all_fixed_ips
}
output "forgejo_ssh_port" {
description = "Port of the builtin SSH server, reached through the load balancers"
value = var.forgejo_data.ssh_port
}
output "forgejo_data_volume_id" {
value = openstack_blockstorage_volume_v3.forgejo_data_vol.id
}
output "traffic_to_forgejo_security_group_id" {
value = openstack_networking_secgroup_v2.traffic_to_forgejo.id
}
output "forgejo_hostname" {
description = "Internal name, on the main private network address"
value = openstack_dns_recordset_v2.forgejo_recordset.name
}
output "forgejo_public_hostname" {
description = "Public name, a CNAME of the main load balancer"
value = length(var.forgejo_public_name) > 0 ? openstack_dns_recordset_v2.git_recordset[0].name : ""
}

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,116 @@
#
# Forgejo (git service).
#
# Sizing and service ports 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 "forgejo_data" {
description = "Instance, volume and ports of the forgejo service. m1.xlarge is RAM 16 - VCPUs 8"
type = object({
name = optional(string, "forgejo")
description = optional(string, "Forgejo git service")
flavor = optional(string, "m1.xlarge")
boot_vol_size = optional(number, 20)
# Repositories and attachments
vol_data_name = optional(string, "forgejo-data")
vol_data_size = optional(number, 200)
vol_data_device = optional(string, "/dev/vdb")
volume_type = optional(string, "CephSSD")
# forgejo terminates the TLS connection itself, with the certificate that
# os-bootstrap gets from the internal CA: no nginx in front of it. 3000 is
# its default port, so it does not need to bind a privileged one
service_ports = optional(list(number), [3000])
# Builtin SSH server. It cannot be the port 22 of the VM, which belongs to
# sshd, so git over SSH is published on the port 22 of the load balancer and
# forwarded here
ssh_port = optional(number, 2222)
})
default = {}
}
# Part of the address plan of the project: no default on purpose
variable "forgejo_main_ip" {
type = string
description = "Address of the instance on the main private network"
}
# 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, the only ones allowed to reach the service"
}
# 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 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"
}
# DNS. The A record on the main network address is always created; the public
# name is a CNAME of the load balancer that publishes the service
variable "dns_zone_id" {
type = string
description = "ID of the DNS zone of the project"
}
variable "dns_zone_name" {
type = string
description = "Name of the DNS zone of the project, with the trailing dot"
}
variable "forgejo_public_name" {
type = string
default = "git"
description = "Left part of the public name, a CNAME of the load balancer. Empty means no record"
}
variable "forgejo_cname_target" {
type = string
default = ""
description = "Target of the CNAME, usually the name of the main load balancer, with the trailing dot"
}

122
s2i2s/forgejo/README.md Normal file
View File

@ -0,0 +1,122 @@
# Forgejo (git service) of the S2I2S project
One VM, `m1.xlarge` (RAM 16 - VCPUs 8), Ubuntu 24.04, 20 GB of root disk, with
**two interfaces**:
| Interface | Address | Use |
|---|---|---|
| main private network | `10.10.0.165` | traffic from the L7 HAPROXY load balancers, administration, monitoring |
| `postgresql-srv-net` | from the DHCP pool | the only way to the database |
and **one 200 GB SSD volume** (`CephSSD`, `enable_online_resize`) on `/dev/vdb`,
for the repositories and the attachments.
The resources live in [`../../modules/forgejo`](../../modules/forgejo), which
also carries the sizing and the service ports as defaults, so another project can
instantiate it with a handful of lines. (The first version of this workspace
declared the resources inline, like `s2i2s/mailbackup-relay` does: they were
moved into a module when the sizing of the services was moved out of
`../variables`.)
Only what belongs to this project is set in `main.tf`: the address on the main
private network, which comes from the address plan in
[`../variables`](../variables) (`basic_services_ip.forgejo`), and the IDs read
from the other workspaces.
Security groups on the ports:
* `default_for_all` and `traffic_to_forgejo_from_the_main_load_balancers` on the
main network port. The second one opens
`forgejo_server_data.service_ports` (**3000**, see below) from each L7 load
balancer: change that list if the service listens elsewhere.
* `vm_access_to_the_postgresql_service`, taken from the `postgresql` workspace,
on the port in the dedicated network of the database.
## Names
| Name | Type | Managed here |
|---|---|---|
| `forgejo.s2i2s.cloud.isti.cnr.it` | A → `10.10.0.165` | yes, internal name used by the playbooks and by the load balancer |
| `git.s2i2s.cloud.isti.cnr.it` | CNAME → `main-lb.s2i2s.cloud.isti.cnr.it.` | yes |
| `gitea-s2i2s.isti.cnr.it` | the name of the service being migrated | no: it lives in the `isti.cnr.it` zone and is moved by hand at the migration |
Both public names are served by the L7 load balancers: the `forgejo` entry of
`haproxy_l7_services` in
`group_vars/main_haproxy_l7/main_haproxy_l7.yml` of `infrastructure-playbooks`
sends them to `10.10.0.165:3000`.
## TLS: no nginx here
Only the L7 load balancers talk to this VM, so forgejo terminates the TLS
connection itself with the certificate that `os-bootstrap` requests from the
[internal CA](../../../infrastructure-playbooks/docs/internal_ca.md) of the
project — there is no nginx in front of it, and the unix socket setup of the
on-premise host (`group_vars/git_server`) does not apply here.
In `app.ini`:
```ini
[server]
PROTOCOL = https
HTTP_PORT = 3000
CERT_FILE = /etc/pki/certs/forgejo.s2i2s.cloud.isti.cnr.it.pem
KEY_FILE = /etc/pki/keys/forgejo.s2i2s.cloud.isti.cnr.it-key.pem
```
Port 3000 keeps forgejo away from the privileged ports, so
`forgejo_bind_privileged_ports` stays false.
**Mind the permissions of the key.** `os-bootstrap` installs it as
`root:root 0440`, and forgejo runs as `git`: as it is, the service cannot read
it. Either give the key a group that `git` belongs to, or set an ACL on
`/etc/pki/keys` — the `user_services_perms` role of the playbooks already does
this kind of thing for other services.
The load balancer verifies the certificate against the internal CA **and** its
name (`verify_host` in the service definition), which the certificate satisfies
because `os-bootstrap` puts the FQDN and every address of the VM among the SANs.
## Git over SSH
No floating IP on this VM: the traffic goes through the load balancers.
```
client :22 -> Octavia TCP listener -> HAPROXY L7 :2222 -> forgejo :2222
```
The public port stays 22, so the clone URLs need no port. The port 22 of this VM
belongs to sshd (that is how ansible gets in), so forgejo has to use its
**builtin SSH server on 2222** (`forgejo_server_data.ssh_port`): in `app.ini`,
`START_SSH_SERVER = true` and `SSH_LISTEN_PORT = 2222`, with `SSH_PORT = 22`
advertised in the clone URLs. The `app.ini` file is not managed by the forgejo
role.
The listener, its pool and its health monitor are in
`../project-setup/octavia.tf`; the ingress rule on the load balancers is in
`../project-setup/haproxy.tf`; the rule that lets the load balancers reach 2222
here is in this workspace.
## Order of the applies
```
main_net_dns_router -> project-setup -> postgresql -> forgejo
```
This workspace reads the state of all three.
```bash
tofu init
tofu plan -out=forgejo.plan
tofu apply forgejo.plan
```
After the apply, regenerate the ansible inventory in
`infrastructure-playbooks`, which reads this state:
```bash
ansible-playbook tofu-inventory.yml --diff
```
The playbook that configures the service is `git-server.yml` (role `forgejo`),
which currently targets the `git_server` group: the generated inventory puts
this VM in `forgejo_cloud`, under `openstack_s2i2s`.

98
s2i2s/forgejo/main.tf Normal file
View File

@ -0,0 +1,98 @@
# Forgejo (git service) of the S2I2S OpenStack project.
#
# The resources are in ../../modules/forgejo, which also carries the sizing
# (m1.xlarge: RAM 16 - VCPUs 8, 200 GB of SSD for the repositories) and the
# service ports. Only what belongs to this project is set here: the address on
# the main private network, taken from the address plan in ../variables, and the
# IDs that come from the other workspaces.
#
# 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
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
basic_services_ip = module.project_variables.basic_services_ip
}
module "forgejo" {
source = "../../modules/forgejo"
# Address plan of the project. The sizing comes from the module defaults
forgejo_main_ip = local.basic_services_ip.forgejo
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
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
# git.s2i2s.cloud.isti.cnr.it, a CNAME of the main load balancer. The other
# name of the service, gitea-s2i2s.isti.cnr.it, is in the isti.cnr.it zone and
# is moved by hand at the migration
dns_zone_id = local.dns_zone_id
dns_zone_name = local.dns_zone.name
forgejo_public_name = "git"
forgejo_cname_target = local.main_loadbalancer_name
}

49
s2i2s/forgejo/outputs.tf Normal file
View File

@ -0,0 +1,49 @@
output "forgejo_instance_id" {
value = module.forgejo.forgejo_instance_id
}
output "forgejo_server_name" {
value = module.forgejo.forgejo_server_name
}
output "forgejo_server_data" {
value = module.forgejo.forgejo_data
}
output "forgejo_main_ip" {
description = "Address on the main private network. Used by the ansible inventory"
value = module.forgejo.forgejo_main_ip
}
output "forgejo_postgresql_ip" {
description = "Address on the dedicated network of the database"
value = module.forgejo.forgejo_postgresql_ip
}
output "forgejo_ssh_port" {
description = "Port of the builtin SSH server, reached through the load balancers"
value = module.forgejo.forgejo_ssh_port
}
output "forgejo_data_volume_id" {
value = module.forgejo.forgejo_data_volume_id
}
output "traffic_to_forgejo_security_group_id" {
value = module.forgejo.traffic_to_forgejo_security_group_id
}
output "forgejo_hostname" {
description = "Internal name, on the main private network address"
value = module.forgejo.forgejo_hostname
}
output "forgejo_public_hostname" {
description = "Public name, a CNAME of the main load balancer"
value = module.forgejo.forgejo_public_hostname
}
# Re-exported for the ansible inventory generator
output "dns_zone" {
value = local.dns_zone
}

14
s2i2s/forgejo/provider.tf Normal file
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