Patroni Cluster im Container

Im Blog Postgres Cluster ohne LB aber mit Patroni habe ich einen Patroni Cluster in drei VMs händisch aufgesetzt, um zu verstehen wie so ein Setup überhaupt funktioniert. Das Ziel ist aber natürlich, so ein Cluster automatisiert ausrollen zu können. Und dies idealerweise auf Performancegründen nicht in VMs sondern in Containern, die idealerweise auf bare metal laufen.

Leider ist es so, dass Linux-Container in VMs laufen müssen, weil sie, wenn sie auf bare metal laufen, nicht multi-mandantenfähig sind. Für Datenbanken, die oft hohe Leistung benötigen, ist bare metal ideal. Um mehrere Mandanten auf derselben Hardware betreiben zu können, bieten sich also andere Container an - z. B. SmartOS-Zonen.

Dankenswerterweise gibt es schon Repositories auf Github (s. unten), die man benutzen kann, um ein Patroni Cluster in Containern automatisiert auszubringen.

Das Vorgehen ist wie üblich: Mit Packer wird zunächst ein Image beschrieben und konfiguriert, welches mit einer Pipeline hergestellt werden kann. Danach wird mit Hilfe von Terraform die Infrastruktur beschrieben und konfiguriert, die z. B. mit einer Pipeline ausgerollt werden kann.

Image mit Packer erstellen

Das Image bekommt i. W. drei Softwarekomponenten installiert:

  • Consul
  • Patroni
  • PostgreSQL

Da die PostgreSQL-Version, die mit dem Packagemanagement (pkgsrc) installiert werden kann, Kerberos-Support einkompiliert hat, was bei Patroni zu Problemen führt, ziehen wir uns den passenden PostgreSQL-Sourcecode, übersetzen diesen im Image (ohne Kerberos-Support) und installieren die übersetzte Version über die Dateien, die wir per Paketmanagement installiert hatten.

Als Service Discovery Komponente (im Patroni-Sprech "dcs" genannt), wird Consul verwendet. Dementsprechend wird es auch im Image installiert:

# Patroni
"pip3 install patroni[consul]",

Wichtig ist, daß die Installation fehlerfrei durchläuft, damit Packer das Image erstellen kann. Im konkreten Fall hat das Packagemanagement bei der Installation des Pakets py38-psycopg2

# Psycopg2
"pkgin -y install py38-psycopg2",

als Abhängigkeit noch den postgresql-client in der Version 13 installiert. Das hat dazu geführt, dass im Moment nur die PostgreSQL-Version 13 funktioniert. Bei allen anderen PostgreSQL-Versionen führt dies zu Konflikten.

In der Packerkonfiguration wird auch schon das pgbackrest-Repo geklont, um in der Lage zu sein, die laufende Postgres-Instanz sichern zu können.

Infrastruktur mit Terraform ausrollen

In der Terraformkonfiguration wird das oben erstellte Image verwendet. Der Ersteller der genannten Repositories hat für die Parameterisierung einen etwas eigenwilligen Weg gewählt. Seine variables.tf sieht so aus:

variable "config" {
}

Er benutzt aber eine Horde Variablen:

11:57:04 toens@NB-bla triton_postgresql_patroni_consul ±|trunk ✗|→ grep var.config main.tf
#    count = var.config.vm.replicas
#    count = var.config.vm.replicas
#        organization = var.config.organization.name
#    count = var.config.vm.replicas
#    ca_key_algorithm   = var.config.certificate_authority.algorithm
#    ca_private_key_pem = var.config.certificate_authority.private_key_pem
#    ca_cert_pem        = var.config.certificate_authority.certificate_pem
version = var.config.image_version
count = var.config.vm.replicas
networks = var.config.machine_networks
#        content = var.config.certificate_authority.certificate_pem
        datacenter_name = var.config.consul_datacenter_name,
        consul_addr = var.config.consul_addr,
        encryption_key = var.config.consul_encryption_key,
        consul_addr = var.config.consul_addr
        consul_scope = var.config.consul_scope
        consul_namespace = var.config.consul_namespace

Nachdem ich mit der Erstellung einer passenden variables.tf an der Meckerei von Terraform gescheitert war, habe ich es dann mit folgender terraform.tfvars versucht:

11:57:10 toens@NB-bla triton_postgresql_patroni_consul ±|trunk ✗|→ cat terraform.tfvars
config = {
  vm = {
    replicas = "3"
  }
  organization = {
    name = "Plusserver"
  }
  certificate_authority = {
    algorithm = "aes256"
    private_key_pem = "pem"
    certificate_pem = "pem"
  }
  image_version = 2022071101
  machine_networks = ["2fa4d4ed-fd46-4469-9b95-f7a61d4f9089"]
  consul_datacenter_name = "my-dc-2"
  consul_addr = "consul.svc.a5ba23f5-8237-6879-e7e1-ea6f574fbde9.my-dc-2.snc.mydc.xyz"
  consul_encryption_key = "supergeheim"
  consul_scope = "Patroni"
  consul_namespace = "terra"
}

Damit hat Terraform aufgehört zu meckern (und ich gelernt, dass man auch auf diese Weise Terraform parameterisieren kann). Wichtig ist für den Cluster natürlich noch die Anti-Affinity:

affinity = ["role!=~postgresql"]

Damit alle Container auf unterschiedlichen Hardwareknoten ausgerollt werden. Vorher wird die Instanz natürlich noch mit einem passenden Tag ausgestattet:

tags = {
     role = "postgresql"
}

Die meisten Änderungen am oben erstellten Image müssen jetzt noch zum Startzeitpunkt in Konfigurationsdateien durchgeführt werden, die z. B. die IP-Adressen der hochfahrenden Instanz enthalten müssen. Das sind konkret die Consul-Konfigurationsdatei (consul.hcl.tpl)(:

{
    "datacenter": "${datacenter_name}",
    "data_dir": "/opt/local/consul",
    "log_level": "INFO",
    "node_name": "${node_name}",
    "server": false,
    "leave_on_terminate": true,
    "bind_addr": "{{ GetInterfaceIP \"net0\" }}"
}

(wobei die zwar "hcl" heißt aber hier fälschlicherweise in JSON abgefasst ist). Und die Patroni-Konfigurationsdatei (patroni.yml.tpl):

name: ${hostname}
scope: inortap
namespace: /${consul_namespace}/
consul:
  url: http://127.0.0.1:8500
  register_service: true
postgresql:
  connect_address: ${listen_ip}:5432
  bin_dir: /opt/local/bin
  data_dir: /var/pgsql/data
  listen: "*:5432"
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: rep-pass
    superuser:
      username: postgres
      password: ${admin_password}
  parameters:
    unix_socket_directories: '.'
restapi:
  listen: ${listen_ip}:8008
  connect_address: ${listen_ip}:8008
bootstrap:
  dcs:
    postgresql:
      use_pg_rewind: true
  # some desired options for 'initdb'
  initdb:  # Note: It needs to be a list (some options need values, others are switches)
  - encoding: UTF8
  - data-checksums
  pg_hba:  # Add following lines to pg_hba.conf after running 'initdb'
  - host replication replicator 127.0.0.1/32 md5
  - host replication replicator 192.168.129.0/22 md5
  - host all all 0.0.0.0/0 md5
 
  # Additional script to be launched after initial cluster creation (will be passed the connection URL as parameter)
# post_init: /usr/local/bin/setup_cluster.sh
  # Some additional users users which needs to be created after initializing new cluster
  users:
    admin:
      password: ${admin_password}
      options:
        - createrole
        - createdb
tags:
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false

$(listen_ip) wird in der main.tf aus der Ressource primaryip, die der Terraform Triton-Provider automatisch beim Erstellen einer Instanz befüllt, gesetzt.

Auch nicht ganz uninteressant: Die Instanzen bekommen nur ein Netzwerkinterface mit einer privaten IP-Adresse und sind deshalb nur über einen Bastion-Host erreichbar. Auch auf dafür hat Terraform vorgesorgt. Die Connection-Konfiguration sieht entsprechend aus:

connection {
   type = "ssh"
   user = "root"
   private_key = "${file("~/.ssh/sdc-docker-hbloed.id_rsa")}"
   agent = "true"
   bastion_host = "10.65.69.143"
   bastion_user = "root"
   bastion_private_key = "${file("~/.ssh/sdc-docker-hbloed.id_rsa")}"
}

Auch ganz nett: Wir können uns von Terraform ein zufälliges Paßwort für Patroni/PostgreSQL generieren lassen:

resource "random_password" "admin_password" {
    length = 32
    special = false
}

welches wir dann im Template-File für Patroni (s. o.) verwenden können.

Ganz wichtig: Da so eine Konfiguration natürlich nicht im ersten Versuch funktioniert ist es wichtig, nach jedem Test im Consul unter "Key/Value" nachzusehen ob ggfs. noch irgendwelche "Rückstände" vorhanden sind, die den Ausgang des nächsten Tests beeinflussen könnten.

Hat alles funktioniert, sieht das Ergebnis genauso aus wie im Versuch mit den VMs:

[root@postgresql-2 ~]# patronictl -c /var/pgsql/patroni.yml list
+--------------+-----------------+---------+---------+----+-----------+
| Member       | Host            | Role    | State   | TL | Lag in MB |
+ Cluster: inortap (7119153973474255889) --+---------+----+-----------+
| postgresql-0 | 192.168.129.218 | Replica | running |  1 |         0 |
| postgresql-1 | 192.168.129.217 | Replica | running |  1 |         0 |
| postgresql-2 | 192.168.129.216 | Leader  | running |  1 |           |
+--------------+-----------------+---------+---------+----+-----------+

Dabei belegt die Zone 161 MB Arbeitsspeicher:

[root@hh24-gts2-de22 (my-dc-2) ~]# zonememstat -a |grep postgresql          a19fde05-4b8d-48c6-a43a-e56eff380b5c           postgresql-2      161   2048        0         0 4,25081