From 6a5f928648dee3732f90005ed7c9462fa041d7a6 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Tue, 10 Jun 2025 16:32:16 +0200 Subject: [PATCH] Initial commit --- .forgejo/workflows/default-workflow.yaml | 28 ++++++ .gitignore | 40 +++++++++ 00-provider.tf | 25 ++++++ 01-config.tf | 66 ++++++++++++++ 02-user_data | 73 ++++++++++++++++ 03-network.tf | 70 +++++++++++++++ 04-master.tf | 44 ++++++++++ 05-backup.tf | 44 ++++++++++ 06-ha.tf | 30 +++++++ README.md | 67 ++++++++++++++ docs/ha.d2 | 21 +++++ docs/ha.svg | 104 ++++++++++++++++++++++ docs/vip.d2 | 29 +++++++ docs/vip.svg | 106 +++++++++++++++++++++++ example.env | 4 + keepalive.conf | 30 +++++++ 16 files changed, 781 insertions(+) create mode 100644 .forgejo/workflows/default-workflow.yaml create mode 100644 .gitignore create mode 100644 00-provider.tf create mode 100644 01-config.tf create mode 100644 02-user_data create mode 100644 03-network.tf create mode 100644 04-master.tf create mode 100644 05-backup.tf create mode 100644 06-ha.tf create mode 100644 README.md create mode 100644 docs/ha.d2 create mode 100644 docs/ha.svg create mode 100644 docs/vip.d2 create mode 100644 docs/vip.svg create mode 100644 example.env create mode 100644 keepalive.conf diff --git a/.forgejo/workflows/default-workflow.yaml b/.forgejo/workflows/default-workflow.yaml new file mode 100644 index 0000000..6c85152 --- /dev/null +++ b/.forgejo/workflows/default-workflow.yaml @@ -0,0 +1,28 @@ +name: CI + +on: [push] + +jobs: + secrets-scan: + name: TruffleHog Secrets Scan + runs-on: docker + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: trufflehog-actions-scan + uses: https://github.com/edplato/trufflehog-actions-scan@master + + terraform: + name: Terraform Format & Validate + runs-on: docker + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: https://github.com/hashicorp/setup-terraform@v3 + with: + terraform_version: "1.5.7" + + - name: Format Terraform Code + run: terraform fmt -recursive -check \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4783ca5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc +.env +.terraform.lock.hcl +conf.img +.DS_Store + +.idea \ No newline at end of file diff --git a/00-provider.tf b/00-provider.tf new file mode 100644 index 0000000..723accf --- /dev/null +++ b/00-provider.tf @@ -0,0 +1,25 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# Define required providers +terraform { + required_version = ">= 0.14.0" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "0.47.0" + } + } +} + +# Configure the STACKIT Provider +provider "stackit" { + region = "eu01" + service_account_token = var.STACKIT_SERVICE_ACCOUNT_TOKEN +} diff --git a/01-config.tf b/01-config.tf new file mode 100644 index 0000000..77c0406 --- /dev/null +++ b/01-config.tf @@ -0,0 +1,66 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# +# Custom User Settings +# + +# STACKIT Availability Zone +variable "zone" { + type = string + description = "" + default = "eu01-m" +} + +# STACKIT VM Flavor +variable "flavor" { + type = string + description = "" + default = "c1.2" +} + +# Local VPC Subnet to create STACKIT Network +variable "LOCAL_SUBNET" { + type = string + description = "" + default = "10.0.0.0/24" +} + +############################################ + +# +# System Settings (do not edit) +# + +# STACKIT Service Account Token +variable "STACKIT_SERVICE_ACCOUNT_TOKEN" { + type = string + description = "" +} + +variable "STACKIT_PROJECT_ID" { + type = string + description = "" +} + +variable "debian_image" { + type = string + default = "b641bf53-4dd4-4ba9-a1ff-9739920fbe73" +} + +resource "random_shuffle" "az" { + input = ["eu01-1", "eu01-2", "eu01-3"] + result_count = 1 +} + +# SSH Key Pair +resource "stackit_key_pair" "admin-keypair" { + name = "brunsch-keypair" + public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDUswz82ZkG2G/2ZYZjo1/Q3srmqcB0zIwpO9GD1X7baH41QEKRY3FGb7KpD6gU1zEKcQYDI8cSmF9ICN51GgRD/+P+B4b59S/z5KUQ10H5QaeFGxBIKxgcWmSGFTMhamMmn/8nG+vI4HEHWSEgpiOQ1vjRDboncW2ac4EH+eZkk2dJAYPWcQorPTW89cRYzaBrhLO+3gpCEyK9Ke4Tf3emnCilYbS+g/qQqo67qR7tW3lYYpJbAEm/ARQhVeyP5mYhOYXwMkiaBOqQ7Wgip0Gc6p9KbGS5ZOe04bhomDJqgK88N03xgGo2st2mbVAKBDpE4kcwxNF08jIeA7UGQ8Si1YJ/vfchTE+dRWwTf5GuB2/Kh/CqaOM8Tb2yFGMPVobeg7EIJd+d5OteX4/9entwXIWkPLtQdWsTubTsSM+eVbgVJ5cv62LkhD8bpm2cEDrvuFWrCO0A2q3KEdlUa2alK8yhgez8mzIgEPDfOcB0SZoQMvilVYVGqnlkriwB9jKRb3WXhqksv8UaPyfE3rlIKAdXEmaWpTNOhsk4RrMjiivSm6L+p/oqwntRE6rqug+jHLqXwfbxwwMl0ouF0GPm2pFdPo4Ki6VE3JBEo71t//GugCHbFb7bRdHEHxKvfTcpIywz3MlDkHWsQKMCRDacpED/i3Snt9Mwxr2ApNli4Q==" +} diff --git a/02-user_data b/02-user_data new file mode 100644 index 0000000..98956d3 --- /dev/null +++ b/02-user_data @@ -0,0 +1,73 @@ +#cloud-config +# Update apt database on first boot (run 'apt-get update'). +# Note, if packages are given, or package_upgrade is true, then +# update will be done independent of this setting. +# +# Default: false +package_update: true + +# Upgrade the instance on first boot +# +# Default: false +package_upgrade: true + +# Install additional packages on first boot +# +# Default: none +# +# if packages are specified, then package_update will be set to true +# +# packages may be supplied as a single package name or as a list +# with the format [, ] wherein the specific +# package version will be installed. +packages: + - apache2 + - libapache2-mod-php + - keepalived + - htop + +# This is the configuration syntax that the write_files module +# will know how to understand. Encoding can be given b64 or gzip or (gz+b64). +# The content will be decoded accordingly and then written to the path that is +# provided. +# +# Note: Content strings here are truncated for example purposes. +write_files: +- content: | +

+ path: /var/www/html/index.php +- content: | + global_defs { + router_id 1337 + + # This 3 settings make sure that the inital garp request will be sended 5 times with 1 sec in between to make + # sure the garp will be processed also if one request was missed by openstack. + vrrp_garp_master_repeat 5 + vrrp_garp_interval 1 + vrrp_garp_lower_prio_repeat 1 + + # Send garp every 5 min again to make sure everything is set in openstack if something failes. + vrrp_garp_master_refresh 300 + vrrp_garp_master_refresh_repeat 1 + + # Make sure that in case 2 keepalived are in master state that the new master will send a garp again. + vrrp_higher_prio_send_advert true + } + vrrp_instance VI_1 { + state ${type} + interface enp3s0 + virtual_router_id 51 + priority ${priority} + advert_int 1 + authentication { + auth_type PASS + auth_pass 12345 + } + virtual_ipaddress { + 10.0.0.123/24 + } + } + path: /etc/keepalived/keepalived.conf + +runcmd: + - [ systemctl, restart, keepalived.service ] \ No newline at end of file diff --git a/03-network.tf b/03-network.tf new file mode 100644 index 0000000..06e05f8 --- /dev/null +++ b/03-network.tf @@ -0,0 +1,70 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# Create vNET Network +resource "stackit_network" "default" { + project_id = var.STACKIT_PROJECT_ID + ipv4_prefix = "10.1.2.0/24" + name = "default" + ipv4_nameservers = ["9.9.9.9", "1.1.1.1"] +} + +# Security Group +resource "stackit_security_group" "active-passive" { + project_id = var.STACKIT_PROJECT_ID + name = "ha-active-passive" +} + +resource "stackit_security_group_rule" "icmp" { + project_id = var.STACKIT_PROJECT_ID + security_group_id = stackit_security_group.active-passive.security_group_id + direction = "ingress" + icmp_parameters = { + code = 0 + type = 8 + } + protocol = { + name = "icmp" + } +} + +resource "stackit_security_group_rule" "ssh" { + project_id = var.STACKIT_PROJECT_ID + security_group_id = stackit_security_group.active-passive.security_group_id + direction = "ingress" + port_range = { + min = 22 + max = 22 + } + protocol = { + name = "tcp" + } +} + +resource "stackit_security_group_rule" "http" { + project_id = var.STACKIT_PROJECT_ID + security_group_id = stackit_security_group.active-passive.security_group_id + direction = "ingress" + port_range = { + min = 80 + max = 80 + } + protocol = { + name = "tcp" + } +} + +resource "stackit_security_group_rule" "vrrp" { + project_id = var.STACKIT_PROJECT_ID + security_group_id = stackit_security_group.active-passive.security_group_id + direction = "ingress" + protocol = { + name = "vrrp" + } +} diff --git a/04-master.tf b/04-master.tf new file mode 100644 index 0000000..3fbf1b6 --- /dev/null +++ b/04-master.tf @@ -0,0 +1,44 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# Create virtual Server +resource "stackit_server" "example01" { + project_id = var.STACKIT_PROJECT_ID + name = "example01" + boot_volume = { + size = 64 + source_type = "image" + source_id = var.debian_image + performance_class = "storage_premium_perf6" + delete_on_termination = true + } + machine_type = "c1.4" + availability_zone = "eu01-1" + keypair_name = stackit_key_pair.admin-keypair.name + depends_on = [stackit_network_interface.example01] +} + +resource "stackit_network_interface" "example01" { + project_id = var.STACKIT_PROJECT_ID + network_id = stackit_network.default.network_id + allowed_addresses = [format("%s/%s", stackit_network_interface.vip01.ipv4, "32")] + security_group_ids = [stackit_security_group.active-passive.security_group_id] +} + +resource "stackit_server_network_interface_attach" "example01-nic-attachment" { + project_id = var.STACKIT_PROJECT_ID + server_id = stackit_server.example01.server_id + network_interface_id = stackit_network_interface.example01.network_interface_id +} + +resource "stackit_public_ip" "example01-wan" { + project_id = var.STACKIT_PROJECT_ID + network_interface_id = stackit_network_interface.example01.network_interface_id + depends_on = [stackit_server_network_interface_attach.example01-nic-attachment] +} diff --git a/05-backup.tf b/05-backup.tf new file mode 100644 index 0000000..5fca48c --- /dev/null +++ b/05-backup.tf @@ -0,0 +1,44 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# Create virtual Server +resource "stackit_server" "example02" { + project_id = var.STACKIT_PROJECT_ID + name = "example02" + boot_volume = { + size = 64 + source_type = "image" + source_id = var.debian_image + performance_class = "storage_premium_perf6" + delete_on_termination = true + } + machine_type = "c1.4" + availability_zone = "eu01-2" + keypair_name = stackit_key_pair.admin-keypair.name + depends_on = [stackit_network_interface.example02] +} + +resource "stackit_network_interface" "example02" { + project_id = var.STACKIT_PROJECT_ID + network_id = stackit_network.default.network_id + allowed_addresses = [format("%s/%s", stackit_network_interface.vip01.ipv4, "32")] + security_group_ids = [stackit_security_group.active-passive.security_group_id] +} + +resource "stackit_server_network_interface_attach" "example02-nic-attachment" { + project_id = var.STACKIT_PROJECT_ID + server_id = stackit_server.example02.server_id + network_interface_id = stackit_network_interface.example02.network_interface_id +} + +resource "stackit_public_ip" "example02-wan" { + project_id = var.STACKIT_PROJECT_ID + network_interface_id = stackit_network_interface.example02.network_interface_id + depends_on = [stackit_server_network_interface_attach.example02-nic-attachment] +} diff --git a/06-ha.tf b/06-ha.tf new file mode 100644 index 0000000..11d0bc6 --- /dev/null +++ b/06-ha.tf @@ -0,0 +1,30 @@ +/* +Copyright 2023 Schwarz IT KG +Copyright 2024-2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT. +*/ + +# Create VIP Port +#resource "terraform_data" "spoke-local-1" { +# provisioner "local-exec" { +# command = "curl --location 'https://iaas.api.eu01.stackit.cloud/v1alpha1/projects/${var.STACKIT_PROJECT_ID}/networks/${stackit_network.default.network_id}/virtual-ips' --header 'Content-Type: application/json' --header 'Authorization: Bearer ${var.STACKIT_SERVICE_ACCOUNT_TOKEN}' --data '{\"ip\":\"10.1.2.10\",\"labels\":{\"key\":\"sap\"},\"members\":[\"${stackit_network_interface.example01.network_interface_id}\",\"${stackit_network_interface.example02.network_interface_id}\"],\"name\":\"vip01\"}'" +# } +# depends_on = [ +# stackit_network_interface.example01, +# stackit_network_interface.example02 +# ] +#} + +resource "stackit_network_interface" "vip01" { + project_id = var.STACKIT_PROJECT_ID + network_id = stackit_network.default.network_id + security = false +} + +resource "stackit_public_ip" "vip01-wan" { + project_id = var.STACKIT_PROJECT_ID + network_interface_id = stackit_network_interface.vip01.network_interface_id +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..73cc0cb --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Basic HA Setup (VRRP) + +Documentation on how to set up an active passive VRRP Cluster +All the needed Commands use the STACKIT Cli. + +Overview core components: + +VRRP Sync between two Virtual Machines including Security Groups and Port Security setup (additional adresses) +![](docs/ha.svg) + +![](docs/vip.svg) + +## Basic Network Config +Creation of a STACKIT Network where the VMs and NIC adapters will be placed. +```bash +NETWORKID=$(stackit network create --name demo --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" -y -o json | jq -r .networkId) +``` + +## Security Groups + +Basic Security Group to allow VRRP & ICMP Traffic for failover + +**Create the Security Group**: +```bash +SECGROUPID=$(stackit security-group create --name VRRP -y -o json | jq -r .id) +``` + +**Create the Security Rules**: +Allow VRRP & ICMP for testing only +```bash +stackit security-group rule create --security-group-id $SECGROUPID --direction ingress --protocol-name icmp +stackit security-group rule create --security-group-id $SECGROUPID --direction ingress --protocol-name vrrp +``` + +## Network Adapters + +We need three network interfaces. +One for each server an the third for registering the internal vip address. + +**Network Interface for the VIP**: +```bash +VIPNICID=$(stackit network-interface create --network-id $NETWORKID --name vipPort -y -o json | jq -r .id) +``` + +**Get the (v)IP of the NIC**: +```bash +VIPIP=$(stackit network-interface describe $VIPNICID --network-id $NETWORKID -o json | jq -r .ipv4) +``` + +**Network Interface for the VMs**: +```bash +NICID=$(stackit network-interface create --network-id $NETWORKID --allowed-addresses $VIPIP --name --security-groups $SECGROUPID, -y -o json | jq -r .id) +``` + +## Set up the virtual Machines + +Create two VMs with a Debian 12 as OS. + +```bash +stackit server create --boot-volume-performance-class storage_premium_perf4 --boot-volume-size 32 --boot-volume-source-type image --boot-volume-source-id 03e19c6a-d73a-4ba9-96af-4bd03cf905d3 --keypair-name --availability-zone eu01-1 --machine-type c1.2 --name --network-interface-ids $NICID +``` + +## External floating Addresses (HA) +To access the HA cluster from the Internet bind a Public IP to our vIP NIC adapter so the WAN ip is always pointed to the active replica. +```bash +stackit public-ip create --associated-resource-id $VIPNICID +``` \ No newline at end of file diff --git a/docs/ha.d2 b/docs/ha.d2 new file mode 100644 index 0000000..033ed23 --- /dev/null +++ b/docs/ha.d2 @@ -0,0 +1,21 @@ +direction: right + +NIC VM 1: { + style: { + stroke: black + font-color: "#004E5A" + fill: "#F8EC17" + opacity: 0.8 + } +} + +NIC VM 2: { + style: { + stroke: black + font-color: "#004E5A" + fill: "#F8EC17" + opacity: 0.8 + } +} + +NIC VM 1 -- NIC VM 2: VRRP HA Sync \ No newline at end of file diff --git a/docs/ha.svg b/docs/ha.svg new file mode 100644 index 0000000..b4d7884 --- /dev/null +++ b/docs/ha.svg @@ -0,0 +1,104 @@ +NIC VM 2NIC VM 1VRRP HA Sync + + + + + diff --git a/docs/vip.d2 b/docs/vip.d2 new file mode 100644 index 0000000..22df79d --- /dev/null +++ b/docs/vip.d2 @@ -0,0 +1,29 @@ +NIC vIP: { + style: { + stroke: black + font-color: "#004E5A" + fill: "#F8EC17" + opacity: 0.8 + } +} + +NIC VM 1: { + style: { + stroke: black + font-color: "#004E5A" + fill: "#F8EC17" + opacity: 0.8 + } +} + +NIC VM 2: { + style: { + stroke: black + font-color: "#004E5A" + fill: "#F8EC17" + opacity: 0.8 + } +} + +NIC vIP -> NIC VM 2: allowed address \n whitelist +NIC vIP -> NIC VM 1: allowed address \n whitelist \ No newline at end of file diff --git a/docs/vip.svg b/docs/vip.svg new file mode 100644 index 0000000..0f95875 --- /dev/null +++ b/docs/vip.svg @@ -0,0 +1,106 @@ +NIC vIPNIC VM 1NIC VM 2 allowed address whitelistallowed address whitelist + + + + + + + diff --git a/example.env b/example.env new file mode 100644 index 0000000..d707323 --- /dev/null +++ b/example.env @@ -0,0 +1,4 @@ +# STACKIT Service Account Token +export TF_VAR_STACKIT_SERVICE_ACCOUNT_TOKEN= +# STACKIT ProjectID +export TF_VAR_STACKIT_PROJECT_ID= \ No newline at end of file diff --git a/keepalive.conf b/keepalive.conf new file mode 100644 index 0000000..51eefbd --- /dev/null +++ b/keepalive.conf @@ -0,0 +1,30 @@ +global_defs { + router_id 1337 + + # This 3 settings make sure that the inital garp request will be sended 5 times with 1 sec in between to make + # sure the garp will be processed also if one request was missed by openstack. + vrrp_garp_master_repeat 5 + vrrp_garp_interval 1 + vrrp_garp_lower_prio_repeat 1 + + # Send garp every 5 min again to make sure everything is set in openstack if something failes. + vrrp_garp_master_refresh 300 + vrrp_garp_master_refresh_repeat 1 + + # Make sure that in case 2 keepalived are in master state that the new master will send a garp again. + vrrp_higher_prio_send_advert true +} +vrrp_instance VI_1 { + state BACKUP + interface enp6s0 + virtual_router_id 51 + priority 254 + advert_int 1 + authentication { + auth_type PASS + auth_pass 12345 + } + virtual_ipaddress { + 10.1.2.64/24 + } +} \ No newline at end of file