Back (Current repo: terraform-mariadb-replica-homelab)

A terraform homelab with mariadb and maxscale for my own understanding and learning.
To clone this repository:
git clone https://git.viktor1993.net/terraform-mariadb-replica-homelab.git
Log | Download | Files | Refs | README

commit 7d2bea832227831c941ae671816cf35ed277b0de
Author: root <root>
Date:   Sat, 14 Mar 2026 15:56:51 +0100

initial commit

Diffstat:
A.gitignore | 11+++++++++++
AMakefile | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 25+++++++++++++++++++++++++
Aawk_scripts/create_maxscale.awk | 44++++++++++++++++++++++++++++++++++++++++++++
Aawk_scripts/destroy_maxscale.awk | 23+++++++++++++++++++++++
Aawk_scripts/service_discovery.awk | 32++++++++++++++++++++++++++++++++
Aboundary.txt.last | 1+
Aconfig.mk | 5+++++
Aconfig.mk.example | 5+++++
Acreate_maxscale.sh | 16++++++++++++++++
Adestroy_maxscale.sh | 17+++++++++++++++++
Adocker/Dockerfile | 19+++++++++++++++++++
Adocker/entrypoint.sh | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocker/my.cnf | 17+++++++++++++++++
Adump/01_queries.sql | 10++++++++++
Ainit.sh | 37+++++++++++++++++++++++++++++++++++++
Amain.tf | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aservice_discovery.sh | 22++++++++++++++++++++++
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