The challenge
Provisioning four VMs is only half the job. Getting the same application running identically on all of them — despite four different cloud APIs, auth models, package managers, and firewall systems — is where most multi-cloud efforts fall apart.
Our approach
One root Terraform module composes four per-cloud child modules. A single identical bootstrap script is read once into a Terraform local and passed as instance startup data to all four clouds; each child wires it into its own mechanism — plain user-data on AWS and GCP, base64 custom-data on Azure and OCI. The result is one source of truth for how the app comes up, everywhere.
The bootstrap script is deliberately portable: it detects the package manager, installs the runtime, creates a virtual environment, initialises data within a bounded memory budget, generates a self-signed certificate, and installs a systemd unit that serves the app directly over HTTPS. Firewall rules are applied with whichever tool the OS uses.
Technical specifics
- A single cloud-init script (90+ lines) that branches on dnf vs apt-get and installs Python, pip, git, and openssl.
- A Python virtual environment, dependencies plus gunicorn, and a bounded data-initialisation step sized to fit 1 GB of RAM.
- A self-signed RSA-2048 TLS certificate (10-year validity) and a systemd unit running gunicorn bound to 0.0.0.0:443.
- Firewall configured per OS — iptables on Ubuntu/OCI, firewall-cmd on the RHEL family — for ports 80 and 443.
- Independent SQLite per VM (no shared state); outputs include app_urls, public_ips, and ssh_commands.
Example configuration
One bootstrap script is read once and fed to every cloud; a systemd unit serves the app over HTTPS.
locals { app_bootstrap = file("${path.module}/cloud-init.sh") }
module "aws" { source = "./modules/aws" user_data = local.app_bootstrap }
module "gcp" { source = "./modules/gcp" user_data = local.app_bootstrap }
# Azure & OCI expect base64:
module "azure" { source = "./modules/azure" user_data = base64encode(local.app_bootstrap) }
module "oci" { source = "./modules/oci" user_data = base64encode(local.app_bootstrap) }#!/usr/bin/env bash
set -euo pipefail
if command -v dnf >/dev/null; then PKG=dnf; else PKG="apt-get"; fi
"${PKG}" install -y python3 python3-pip git openssl
git clone --depth 1 https://github.com/beknar/poli-tracker.git /opt/poli-tracker
cd /opt/poli-tracker && python3 -m venv .venv
.venv/bin/pip install -r requirements.txt gunicorn
# self-signed TLS, then serve over 443 via systemd
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout tls.key -out tls.crt -subj "/CN=${APP_DOMAIN:-app.example.com}"
cat >/etc/systemd/system/app.service <<'UNIT'
[Service]
ExecStart=/opt/poli-tracker/.venv/bin/gunicorn -b 0.0.0.0:443 -w 1 \
--certfile /opt/poli-tracker/tls.crt --keyfile /opt/poli-tracker/tls.key app:app
[Install]
WantedBy=multi-user.target
UNIT
systemctl enable --now app.serviceOutcome
One apply provisions four identical app servers across four clouds, each serving the application over HTTPS on port 443 within minutes — reproducible, reviewable, and with no secrets in version control. Teardown is equally simple.