commit 7d2bea832227831c941ae671816cf35ed277b0de
Author: root <root>
Date: Sat, 14 Mar 2026 15:56:51 +0100
initial commit
Diffstat:
18 files changed, 461 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,11 @@
+.terraform/
+terraform.tfstate
+terrafor,.tfstate.*
+*.tfstate
+*.tfstate.backup
+locals.tf.last
+locals.tf
+boundary.txt
+boundary.mk
+dump/00_dump.sql
+dump/gtid_pos.txt
diff --git a/Makefile b/Makefile
@@ -0,0 +1,62 @@
+-include config.mk
+-include boundary.mk
+OPERATION ?=
+VALID_OPERATIONS := APPLY DESTROY
+NUMBER ?=
+PORT ?=
+
+ifeq ($(NUMBER),)
+ NUMBER = 2
+endif
+
+ifeq ($(PORT),)
+ PORT = 3313
+endif
+
+ifneq ($(OPERATION),APPLY)
+ ifneq ($(OPERATION),DESTROY)
+ $(error Invalid value for OPERATION: "$(OPERATION)". Must be "APPLY" or "DESTROY")
+ endif
+endif
+
+IS_MAXSCALE_ACTIVE := $(shell systemctl is-active maxscale)
+
+ifneq ($(IS_MAXSCALE_ACTIVE),active)
+$(error Maxscale service is not running, can not do service discovery)
+endif
+
+ifeq ($(OPERATION),APPLY)
+ TARGETS := build apply
+else ifeq ($(OPERATION),DESTROY)
+ TARGETS := destroy
+endif
+
+operate: service_discovery
+ $(MAKE) execute_operation
+#we need a dynamically generated value from a file, so we must call make from make to refresh state
+
+execute_operation:
+ $(MAKE) $(TARGETS)
+
+#checks maxctrl, tells what's currently running gives the number of current servers, and builds a locals.tf file
+service_discovery:
+ rm -f boundary.mk
+ bash service_discovery.sh $(NUMBER) $(PORT) $(ROOT_SRC)
+ @echo "BOUNDARY=$$(cat boundary.txt)" > boundary.mk
+
+#builds and transports the docker images to the target server, along with a fresh mysqldump
+build:
+ bash init.sh $(TARGET_SRV) $(ROOT_SRC) $(REMOTE_DOCKER_DIR) $(REMOTE_DOCKER_SCRIPTS_DIR) $(DOCKER_BUILD)
+
+#runs the new docker containers, updates maxscale
+apply:
+ terraform init
+ terraform apply
+ bash create_maxscale.sh $(BOUNDARY) $(NUMBER) $(PORT) $(ROOT_SRC) $(TARGET_SRV)
+
+#destroys the docker containers, updates maxscale
+destroy:
+ bash destroy_maxscale.sh $(BOUNDARY) $(NUMBER) $(ROOT_SRC)
+ terraform destroy
+
+.PHONY: service_discovery execute_operation operate build apply destroy
diff --git a/README.md b/README.md
@@ -0,0 +1,25 @@
+# What is this?
+
+This repository contains a small project that does two things:
+
+* -> create terraform managed docker containers with mariadb, and add them as read-only to an existing maxscale cluster
+* -> remove terraform managed mariadb docker containers from maxscale, and destroy the containers
+
+## Assumptions:
+
+* -> Maxscale already exists on the machine where terraform is executed from
+* -> A Master server, and 2 Replica servers are already plugged into maxscale, users for maxscale and replication already set up
+* -> Terraform does not manage these 3 initial servers, it just needs to know how to add extra servers on top
+* -> Naming scheme of servers is just "server1, server2, server3" etc.
+* -> It is OK to just restart maxscale, in reality there would be a HA mechanism, but not in this homelab
+
+## Requirements:
+
+### HOST
+
+- MariaDB (or just mariadb-client with access to a master server), Maxscale, Make, Terraform, gnu awk, gnu sed, bash, ssh access to target server
+
+### TARGET SERVER
+
+- docker
+
diff --git a/awk_scripts/create_maxscale.awk b/awk_scripts/create_maxscale.awk
@@ -0,0 +1,44 @@
+#!/bin/awk -f
+
+BEGIN{
+ found = 0;
+ if(boundary == "") {boundary = 4;}
+ if(number == "") {number = 2;}
+ if(port == "") {port = 3313;}
+ if(address == "") {address="192.168.2.99";}
+ pat="\\[server"(boundary-1)"\\]";
+ pat2="";
+ repl="";
+ for(i=boundary-2; i<boundary; i++) {
+ pat2=pat2"server"i",";
+ }
+ pat2=substr(pat2, 1, length(pat2) - 1);
+ for(i=boundary; i<boundary+number; i++) {
+ repl=repl",server"i;
+ }
+}
+{
+ if($0 ~ pat2) {
+ sub(pat2, pat2""repl, $0);
+ print $0;
+ } else {
+ print $0;
+ }
+ if($0 ~ pat) {
+ found = 1;
+ }
+ if((found == 1) && ($0 ~ /protocol=MariaDBBackend/)) {
+ for(i = boundary; i < (boundary+number); i++) {
+ port++;
+ print "\n[server"i"]";
+ print "type=server";
+ print "address="address;
+ print "port="port;
+ print "protocol=MariaDBBackend\n";
+ }
+ found = 0;
+ next;
+ }
+}
+END{
+}
diff --git a/awk_scripts/destroy_maxscale.awk b/awk_scripts/destroy_maxscale.awk
@@ -0,0 +1,23 @@
+#!/bin/awk -f
+
+BEGIN{
+ if(number == "") {number = 2;}
+ if(boundary == "") {boundary = 6;}
+ new = boundary - number;
+ pat="\\[server"new"\\]";
+}
+{
+ if($0 ~ pat) {
+ flag = 1;
+ next;
+ }
+ if($0 ~ "#") {
+ print $0;
+ flag = 0;
+ next;
+ }
+ if(flag == 0) {
+ print $0;
+ }
+}
+
diff --git a/awk_scripts/service_discovery.awk b/awk_scripts/service_discovery.awk
@@ -0,0 +1,32 @@
+#!/bin/awk -f
+
+BEGIN{
+ FS="│";
+ max_id = 0;
+ if (port == ""){port = 3313;}
+ if (number == ""){number = 2;}
+}
+{
+ if($2 ~ /server[0-9]+/){
+ for (i=1; i<=NF; i++) {
+ gsub(/^[ \t]+|[ \t]+$/, "", $i);
+ }
+ match($2, /([0-9]+)$/, current_id);
+ if (current_id[1] > max_id) {
+ max_id = current_id[1];
+ }
+ }
+}
+END{
+ boundary = max_id+1;
+ print boundary;
+ print "locals {";
+ print " servers = {";
+ for(i = boundary; i < boundary+number; i++) {
+ port++;
+ print " \"server"i"\" = { port = "port", server_id = "i" },";
+ }
+ print " }"
+ print "}";
+}
+
diff --git a/boundary.txt.last b/boundary.txt.last
@@ -0,0 +1 @@
+4
diff --git a/config.mk b/config.mk
@@ -0,0 +1,5 @@
+ROOT_SRC=$(CURDIR)
+TARGET_SRV="192.168.2.99"
+REMOTE_DOCKER_DIR=/opt/docker/mariadb-pkg-tf
+REMOTE_DOCKER_SCRIPTS_DIR=$(REMOTE_DOCKER_DIR)/docker-entrypoint-initdb.d
+DOCKER_BUILD="mariadb-pkg-tf:dev"
diff --git a/config.mk.example b/config.mk.example
@@ -0,0 +1,5 @@
+ROOT_SRC=$(CURDIR)
+TARGET_SRV="{{IP OF SERVER WHERE DOCKER CONTAINERS SHOULD BE CREATED}}"
+REMOTE_DOCKER_DIR={{/path/to/directory/with/dockerfile}}
+REMOTE_DOCKER_SCRIPTS_DIR=$(REMOTE_DOCKER_DIR)/docker-entrypoint-initdb.d
+DOCKER_BUILD="{{docker_image:tag}}"
diff --git a/create_maxscale.sh b/create_maxscale.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+BOUNDARY=$1
+NUMBER=$2
+PORT=$3
+ROOT_SRC=$4
+TARGET_SRV=$5
+TMP=$(mktemp)
+
+awk -v boundary=$BOUNDARY -v number=$NUMBER -v port=$PORT -v address=$TARGET_SRV -f $ROOT_SRC/awk_scripts/create_maxscale.awk /etc/maxscale.cnf > $TMP
+cat /etc/maxscale.cnf > /etc/maxscale.cnf.last
+cat $TMP > /etc/maxscale.cnf
+
+systemctl restart maxscale
+
+rm $TMP
diff --git a/destroy_maxscale.sh b/destroy_maxscale.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+BOUNDARY=$1
+NUMBER=$2
+ROOT_SRC=$3
+NEW=$(echo "$BOUNDARY - $NUMBER" | bc)
+echo "DELETING SERVERS ${NEW}+ FROM maxscale.cnf"
+TMP=$(mktemp)
+
+awk -v boundary=$BOUNDARY -v number=$NUMBER -f $ROOT_SRC/awk_scripts/destroy_maxscale.awk /etc/maxscale.cnf > $TMP
+cat /etc/maxscale.cnf > /etc/maxscale.cnf.last
+cat $TMP > /etc/maxscale.cnf
+sed -E -i "s/,server${NEW}.*$//g" /etc/maxscale.cnf
+
+systemctl restart maxscale
+
+rm $TMP
diff --git a/docker/Dockerfile b/docker/Dockerfile
@@ -0,0 +1,19 @@
+FROM debian:12-slim
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ mariadb-server mariadb-client \
+ ca-certificates tzdata \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/
+
+COPY entrypoint.sh /usr/local/bin/entrypoint.sh
+RUN chmod +x /usr/local/bin/entrypoint.sh
+
+VOLUME ["/var/lib/mysql"]
+
+ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+CMD ["mariadbd"]
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+MARIADB_PORT=${MARIADB_PORT:-3311}
+MARIADB_SERVER_ID=${MARIADB_SERVER_ID:-4}
+DATADIR="/var/lib/mysql"
+RUNDIR="/run/mysqld"
+LOCK_FILE=/var/lib/mysql/setup.lock
+
+if ! [ -f "$LOCK_FILE" ]; then
+#Don't do all this in case of an accidental docker restart
+ sed -E -i "s/(server-id=)[0-9]+/\1${MARIADB_SERVER_ID}/" /docker-entrypoint-initdb.d/my.cnf
+ cat /docker-entrypoint-initdb.d/my.cnf > /etc/mysql/my.cnf
+ echo "lock" > $LOCK_FILE
+
+ mkdir -p "$RUNDIR"
+ chown -R mysql:mysql "$RUNDIR"
+ chown -R mysql:mysql "$DATADIR"
+
+ if [ ! -d "${DATADIR}/mysql" ]; then
+ echo "Initializing MariaDB data directory..."
+ mariadb-install-db --user=mysql --datadir="$DATADIR" >/dev/null
+ fi
+
+ #Initialize DB here for importing SQL files, don't allow connections
+ echo "Starting temporary MariaDB (no networking) for init..."
+ mariadbd --user=mysql --datadir="$DATADIR" --skip-networking --socket=/tmp/mysqld.sock &
+ pid="$!"
+
+ #Wait until mariadb server finished initializing before trying to import anything
+ for i in {1..60}; do
+ if mariadb-admin --protocol=socket --socket=/tmp/mysqld.sock ping >/dev/null 2>&1; then
+ break
+ fi
+ sleep 0.5
+ done
+
+ echo "Running init scripts in /docker-entrypoint-initdb.d ..."
+ shopt -s nullglob
+ for f in /docker-entrypoint-initdb.d/*; do
+ case "$f" in
+ *.sql)
+ echo " -> importing $f"
+ mariadb --protocol=socket --socket=/tmp/mysqld.sock < "$f"
+ ;;
+ *.sql.gz)
+ echo " -> importing $f"
+ gunzip -c "$f" | mariadb --protocol=socket --socket=/tmp/mysqld.sock
+ ;;
+ *.sh)
+ echo " -> running $f"
+ bash "$f"
+ ;;
+ *)
+ echo " -> ignoring $f"
+ ;;
+ esac
+ done
+
+ echo "Shutting down temporary MariaDB..."
+ mariadb-admin --protocol=socket --socket=/tmp/mysqld.sock shutdown
+ wait "$pid" || true
+
+fi
+
+echo "Starting MariaDB on port ${MARIADB_PORT}..."
+exec "$@" --user=mysql --datadir="$DATADIR" --bind-address=0.0.0.0 --port="$MARIADB_PORT"
diff --git a/docker/my.cnf b/docker/my.cnf
@@ -0,0 +1,17 @@
+[mysqld]
+server-id=2
+gtid_strict_mode=1
+log_bin=/var/lib/mysql/bin-mariadb.log
+expire_logs_days=8
+sync_binlog=1
+binlog_format = row
+log_slave_updates = 1
+
+[client-server]
+# Port or socket location where to connect
+# port = 3306
+socket = /run/mysqld/mysqld.sock
+
+# Import all .cnf files from configuration directory
+!includedir /etc/mysql/conf.d/
+!includedir /etc/mysql/mariadb.conf.d/
diff --git a/dump/01_queries.sql b/dump/01_queries.sql
@@ -0,0 +1,10 @@
+CHANGE MASTER TO
+MASTER_HOST='192.168.2.37',
+MASTER_PORT=3306,
+MASTER_USER='repl',
+MASTER_PASSWORD=[REDACTED]
+MASTER_USE_GTID=slave_pos;
+
+SET GLOBAL gtid_slave_pos='0-1-61043';
+
+START SLAVE;
diff --git a/init.sh b/init.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+TARGET_SRV=$1
+ROOT_SRC=$2
+REMOTE_DOCKER_DIR=$3
+REMOTE_DOCKER_SCRIPTS_DIR=$4
+DOCKER_BUILD=$5
+DUMP_DIR=$ROOT_SRC/dump
+DOCKER_DIR=$ROOT_SRC/docker
+mkdir -p $DUMP_DIR
+mkdir -p $DOCKER_DIR
+
+if [[ $(ps aux | grep -v grep | grep -c mariadbd) -lt 1 ]]; then
+ echo "Can not find active mariadb instance"
+ exit 1
+fi
+
+sed -E -i "s/(gtid_slave_pos=)'[0-9-]+'/\1'%SLAVE%'/" $DUMP_DIR/01_queries.sql
+
+mysqldump --all-databases --master-data=2 --single-transaction -u root > $DUMP_DIR/00_dump.sql
+mysql -u root --skip-column-names -e "SELECT @@gtid_binlog_pos;" > $DUMP_DIR/gtid_pos.txt
+GTID=$(cat $DUMP_DIR/gtid_pos.txt)
+sed -E -i "s/%SLAVE%/${GTID}/" $DUMP_DIR/01_queries.sql
+# The dump captures a consistent snapshot at a point in time, and the GTID position captured alongside it marks exactly where that snapshot ends. The replication can then pick up from this marker.
+
+scp $DUMP_DIR/00_dump.sql root@$TARGET_SRV:$REMOTE_DOCKER_SCRIPTS_DIR
+scp $DUMP_DIR/01_queries.sql root@$TARGET_SRV:$REMOTE_DOCKER_SCRIPTS_DIR
+
+scp $DOCKER_DIR/Dockerfile root@$TARGET_SRV:$REMOTE_DOCKER_DIR
+scp $DOCKER_DIR/entrypoint.sh root@$TARGET_SRV:$REMOTE_DOCKER_DIR
+
+scp $DOCKER_DIR/my.cnf root@$TARGET_SRV:$REMOTE_DOCKER_SCRIPTS_DIR
+
+ssh root@$TARGET_SRV "docker build -t $DOCKER_BUILD $REMOTE_DOCKER_DIR"
+
+
+
diff --git a/main.tf b/main.tf
@@ -0,0 +1,49 @@
+terraform {
+ required_version = ">= 1.5.0"
+
+ required_providers {
+ docker = {
+ source = "kreuzwerker/docker"
+ version = "~> 3.0"
+ }
+ local = {
+ source = "hashicorp/local"
+ version = "~> 2.0"
+ }
+ }
+}
+
+provider "docker" {
+ host = "ssh://root@192.168.2.99"
+}
+
+resource "docker_network" "lab" {
+ name = "tf-mariadb-lab-net"
+}
+
+resource "docker_container" "server" {
+ for_each = local.servers
+
+ name = each.key
+ image = "mariadb-pkg-tf:dev"
+ hostname = each.key
+
+ env = [
+ "MARIADB_PORT=${each.value.port}",
+ "MARIADB_SERVER_ID=${each.value.server_id}",
+ ]
+
+ ports {
+ internal = each.value.port
+ external = each.value.port
+ }
+
+ networks_advanced {
+ name = docker_network.lab.name
+ }
+
+ labels {
+ label = "deployed_by"
+ value = "terraform"
+ }
+}
diff --git a/service_discovery.sh b/service_discovery.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+NUMBER=$1
+PORT=$2
+ROOT_SRC=$3
+TMP=$(mktemp)
+
+maxctrl list servers | awk -v number=$NUMBER -v port=$PORT -f $ROOT_SRC/awk_scripts/service_discovery.awk | sed -e 'H;1h;$!d;x;s/\(.*\),/\1/' > $TMP
+
+if ! head -n1 "$TMP" | grep -Eq '^[0-9]+$'; then
+ echo "Failed to derive boundary from maxctrl output" >&2
+ exit 1
+fi
+
+cat $ROOT_SRC/boundary.txt > $ROOT_SRC/boundary.txt.last
+awk '(NR==1){print $0}' $TMP > $ROOT_SRC/boundary.txt
+sed -i '1d' $TMP
+
+cat $ROOT_SRC/locals.tf > $ROOT_SRC/locals.tf.last
+cat $TMP > $ROOT_SRC/locals.tf
+
+rm $TMP