609 lines
18 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env nu
# Multi-Region HA Workspace Deployment Script
# Orchestrates deployment across US East (DigitalOcean), EU Central (Hetzner), Asia Pacific (AWS)
# Features: Regional health checks, VPN tunnels, global DNS, failover configuration
# Error handling: Result pattern (hybrid, no inline try-catch)
use lib_provisioning/result.nu *
def main [--debug: bool = false, --region: string = "all"] {
print "🌍 Multi-Region High Availability Deployment"
print "──────────────────────────────────────────────────"
if $debug {
print "✓ Debug mode enabled"
}
# Determine which regions to deploy
let regions = if $region == "all" {
["us-east", "eu-central", "asia-southeast"]
} else {
[$region]
}
print $"\n📋 Deploying to regions: ($regions | str join ', ')"
# Step 1: Validate configuration
print "\n📋 Step 1: Validating configuration..."
validate_environment
# Step 2: Deploy US East (Primary)
if ("us-east" in $regions) {
print "\n☁ Step 2a: Deploying US East (DigitalOcean - Primary)..."
deploy_us_east_digitalocean
}
# Step 3: Deploy EU Central (Secondary)
if ("eu-central" in $regions) {
print "\n☁ Step 2b: Deploying EU Central (Hetzner - Secondary)..."
deploy_eu_central_hetzner
}
# Step 4: Deploy Asia Pacific (Tertiary)
if ("asia-southeast" in $regions) {
print "\n☁ Step 2c: Deploying Asia Pacific (AWS - Tertiary)..."
deploy_asia_pacific_aws
}
# Step 5: Setup VPN tunnels (only if deploying multiple regions)
if (($regions | length) > 1) {
print "\n🔐 Step 3: Setting up VPN tunnels for inter-region communication..."
setup_vpn_tunnels
}
# Step 6: Configure global DNS
if (($regions | length) == 3) {
print "\n🌐 Step 4: Configuring global DNS and failover policies..."
setup_global_dns
}
# Step 7: Configure database replication
if (($regions | length) > 1) {
print "\n🗄 Step 5: Configuring database replication..."
setup_database_replication
}
# Step 8: Verify deployment
print "\n✅ Step 6: Verifying deployment across regions..."
verify_multi_region_deployment
print "\n🎉 Multi-region HA deployment complete!"
print "✓ Application is now live across 3 geographic regions with automatic failover"
print ""
print "Next steps:"
print "1. Configure SSL/TLS certificates for all regional endpoints"
print "2. Deploy application to web servers in each region"
print "3. Test failover by stopping a region and verifying automatic failover"
print "4. Monitor replication lag and regional health status"
}
def validate_environment [] {
# Check required environment variables
let required = [
"DIGITALOCEAN_TOKEN",
"HCLOUD_TOKEN",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY"
]
print " Checking required environment variables..."
$required | each {|var|
if ($env | has $var) {
print $" ✓ ($var) is set"
} else {
print $" ✗ ($var) is not set"
error make {msg: $"Missing required environment variable: ($var)"}
}
}
# Verify CLI tools
let tools = ["doctl", "hcloud", "aws", "nickel"]
print " Verifying CLI tools..."
$tools | each {|tool|
if (which $tool | is-not-empty) {
print $" ✓ ($tool) is installed"
} else {
print $" ✗ ($tool) is not installed"
error make {msg: $"Missing required tool: ($tool)"}
}
}
# Validate Nickel configuration
print " Validating Nickel configuration..."
let nickel_result = (try-wrap { nickel export workspace.ncl | from json | null })
if (is-err $nickel_result) {
error make {msg: $"Nickel validation failed: ($nickel_result.err)"}
}
print " ✓ Nickel configuration is valid"
# Validate config.toml
print " Validating config.toml..."
if not ("config.toml" | path exists) {
error make {msg: "config.toml not found"}
}
let config_result = (try-wrap { open config.toml })
if (is-err $config_result) {
error make {msg: $"config.toml validation failed: ($config_result.err)"}
}
print " ✓ config.toml is valid"
# Test provider connectivity using bash-wrap helper (no inline try-catch)
print " Testing provider connectivity..."
# DigitalOcean connectivity
let do_result = (bash-wrap "doctl account get")
if (is-err $do_result) {
error make {msg: $"DigitalOcean connectivity failed: ($do_result.err)"}
}
print " ✓ DigitalOcean connectivity verified"
# Hetzner connectivity
let hz_result = (bash-wrap "hcloud server list")
if (is-err $hz_result) {
error make {msg: $"Hetzner connectivity failed: ($hz_result.err)"}
}
print " ✓ Hetzner connectivity verified"
# AWS connectivity
let aws_result = (bash-wrap "aws sts get-caller-identity")
if (is-err $aws_result) {
error make {msg: $"AWS connectivity failed: ($aws_result.err)"}
}
print " ✓ AWS connectivity verified"
}
def deploy_us_east_digitalocean [] {
print " Creating DigitalOcean VPC (10.0.0.0/16)..."
let vpc = (doctl compute vpc create \
--name "us-east-vpc" \
--region "nyc3" \
--ip-range "10.0.0.0/16" \
--format ID \
--no-header | into string)
print $" ✓ Created VPC: ($vpc)"
print " Creating DigitalOcean droplets (3x s-2vcpu-4gb)..."
let ssh_keys = (doctl compute ssh-key list --no-header --format ID)
if ($ssh_keys | is-empty) {
error make {msg: "No SSH keys found in DigitalOcean. Please upload one first."}
}
let ssh_key_id = ($ssh_keys | first)
# Create 3 web server droplets
let droplet_ids = (
1..3 | each {|i|
let response = (doctl compute droplet create \
$"us-app-($i)" \
--region "nyc3" \
--size "s-2vcpu-4gb" \
--image "ubuntu-22-04-x64" \
--ssh-keys $ssh_key_id \
--enable-monitoring \
--enable-backups \
--format ID \
--no-header | into string)
print $" ✓ Created droplet: us-app-($i)"
$response
}
)
# Wait for droplets to be ready
print " Waiting for droplets to be active..."
sleep 30sec
# Verify droplets are running
$droplet_ids | each {|id|
let droplet = (doctl compute droplet get $id --format Status --no-header)
if $droplet != "active" {
error make {msg: $"Droplet ($id) failed to start"}
}
}
print " ✓ All droplets are active"
print " Creating DigitalOcean load balancer..."
let lb = (doctl compute load-balancer create \
--name "us-lb" \
--region "nyc3" \
--forwarding-rules "entry_protocol:http,entry_port:80,target_protocol:http,target_port:80" \
--format ID \
--no-header | into string)
print $" ✓ Created load balancer: ($lb)"
print " Creating DigitalOcean PostgreSQL database (3-node Multi-AZ)..."
# Create database using bash-wrap helper (no inline try-catch)
let db_result = (bash-wrap "doctl databases create --engine pg --version 14 --region nyc3 --num-nodes 3 --size db-s-2vcpu-4gb --name us-db-primary")
(match-result $db_result
{|_| print " ✓ Database creation initiated (may take 10-15 minutes)"}
{|err| print $" ⚠ Database creation error \(may already exist\): ($err)"}
)
}
def deploy_eu_central_hetzner [] {
print " Creating Hetzner private network (10.1.0.0/16)..."
let network = (hcloud network create \
--name "eu-central-network" \
--ip-range "10.1.0.0/16" \
--format json | from json)
print $" ✓ Created network: ($network.network.id)"
print " Creating Hetzner subnet..."
hcloud network add-subnet eu-central-network \
--ip-range "10.1.1.0/24" \
--network-zone "eu-central"
print " ✓ Created subnet: 10.1.1.0/24"
print " Creating Hetzner servers (3x CPX21)..."
let ssh_keys = (hcloud ssh-key list --format ID --no-header)
if ($ssh_keys | is-empty) {
error make {msg: "No SSH keys found in Hetzner. Please upload one first."}
}
let ssh_key_id = ($ssh_keys | first)
# Create 3 servers
let server_ids = (
1..3 | each {|i|
let response = (hcloud server create \
--name $"eu-app-($i)" \
--type cpx21 \
--image ubuntu-22.04 \
--location nbg1 \
--ssh-key $ssh_key_id \
--network eu-central-network \
--format json | from json)
print $" ✓ Created server: eu-app-($i) \(ID: ($response.server.id)\)"
$response.server.id
}
)
print " Waiting for servers to be running..."
sleep 30sec
$server_ids | each {|id|
let server = (hcloud server list --format ID,Status | where {|row| $row =~ $id} | get Status.0)
if $server != "running" {
error make {msg: $"Server ($id) failed to start"}
}
}
print " ✓ All servers are running"
print " Creating Hetzner load balancer..."
let lb = (hcloud load-balancer create \
--name "eu-lb" \
--type lb21 \
--location nbg1 \
--format json | from json)
print $" ✓ Created load balancer: ($lb.load_balancer.id)"
print " Creating Hetzner backup volume (500GB)..."
let volume = (hcloud volume create \
--name "eu-backups" \
--size 500 \
--location nbg1 \
--format json | from json)
print $" ✓ Created backup volume: ($volume.volume.id)"
# Wait for volume to be ready
print " Waiting for volume to be available..."
let max_wait = 60
mut attempts = 0
while $attempts < $max_wait {
let status = (hcloud volume list --format ID,Status | where {|row| $row =~ $volume.volume.id} | get Status.0)
if $status == "available" {
print " ✓ Volume is available"
break
}
sleep 1sec
$attempts = ($attempts + 1)
}
if $attempts >= $max_wait {
error make {msg: "Hetzner volume failed to become available"}
}
}
def deploy_asia_pacific_aws [] {
print " Creating AWS VPC (10.2.0.0/16)..."
let vpc = (aws ec2 create-vpc \
--region ap-southeast-1 \
--cidr-block "10.2.0.0/16" \
--tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=asia-vpc}]" | from json)
print $" ✓ Created VPC: ($vpc.Vpc.VpcId)"
print " Creating AWS private subnet..."
let subnet = (aws ec2 create-subnet \
--region ap-southeast-1 \
--vpc-id $vpc.Vpc.VpcId \
--cidr-block "10.2.1.0/24" \
--availability-zone "ap-southeast-1a" | from json)
print $" ✓ Created subnet: ($subnet.Subnet.SubnetId)"
print " Creating AWS security group..."
let sg = (aws ec2 create-security-group \
--region ap-southeast-1 \
--group-name "asia-db-sg" \
--description "Security group for Asia Pacific database access" \
--vpc-id $vpc.Vpc.VpcId | from json)
print $" ✓ Created security group: ($sg.GroupId)"
# Allow inbound traffic from all regions
aws ec2 authorize-security-group-ingress \
--region ap-southeast-1 \
--group-id $sg.GroupId \
--protocol tcp \
--port 5432 \
--cidr 10.0.0.0/8
print " ✓ Configured database access rules"
print " Creating AWS EC2 instances (3x t3.medium)..."
let ami_id = "ami-09d56f8956ab235b7"
# Create 3 EC2 instances
let instance_ids = (
1..3 | each {|i|
let response = (aws ec2 run-instances \
--region ap-southeast-1 \
--image-id $ami_id \
--instance-type t3.medium \
--subnet-id $subnet.Subnet.SubnetId \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=asia-app-($i)}]" | from json)
let instance_id = $response.Instances.0.InstanceId
print $" ✓ Created instance: asia-app-($i) \(ID: ($instance_id)\)"
$instance_id
}
)
print " Waiting for instances to be running..."
sleep 30sec
$instance_ids | each {|id|
let status = (aws ec2 describe-instances \
--region ap-southeast-1 \
--instance-ids $id \
--query 'Reservations[0].Instances[0].State.Name' \
--output text)
if $status != "running" {
error make {msg: $"Instance ($id) failed to start"}
}
}
print " ✓ All instances are running"
print " Creating AWS Application Load Balancer..."
let lb = (aws elbv2 create-load-balancer \
--region ap-southeast-1 \
--name "asia-lb" \
--subnets $subnet.Subnet.SubnetId \
--scheme internet-facing \
--type application | from json)
print $" ✓ Created ALB: ($lb.LoadBalancers.0.LoadBalancerArn)"
print " Creating AWS RDS read replica..."
# Create read replica using bash-wrap helper (no inline try-catch)
let replica_result = (bash-wrap "aws rds create-db-instance-read-replica --region ap-southeast-1 --db-instance-identifier asia-db-replica --source-db-instance-identifier us-db-primary")
(match-result $replica_result
{|_| print " ✓ Read replica creation initiated"}
{|err| print $" ⚠ Read replica creation error \(may already exist\): ($err)"}
)
}
def setup_vpn_tunnels [] {
print " Setting up IPSec VPN tunnels between regions..."
# US to EU VPN
print " Creating US East → EU Central VPN tunnel..."
# Create VPN gateway using bash-wrap helper (no inline try-catch)
let vpn_result = (bash-wrap "aws ec2 create-vpn-gateway --region us-east-1 --type ipsec.1 --tag-specifications ResourceType=vpn-gateway,Tags=[{Key=Name,Value=us-eu-vpn-gw}]")
(match-result $vpn_result
{|_| print " ✓ VPN gateway created (manual completion required)"}
{|err| print $" VPN setup note: ($err)"}
)
# EU to APAC VPN
print " Creating EU Central → Asia Pacific VPN tunnel..."
print " Note: VPN configuration between Hetzner and AWS requires manual setup"
print " See multi-provider-networking.md for StrongSwan configuration steps"
print " ✓ VPN tunnel configuration documented"
}
def setup_global_dns [] {
print " Setting up Route53 geolocation routing..."
# List hosted zones using bash-wrap helper (no inline try-catch)
let zones_result = (bash-wrap "aws route53 list-hosted-zones")
(match-result $zones_result
{|output|
# Parse JSON
let hosted_zones = ($output | from json)
if (($hosted_zones.HostedZones | length) > 0) {
let zone_id = $hosted_zones.HostedZones.0.Id
print $" ✓ Using hosted zone: ($zone_id)"
print " Creating regional DNS records with health checks..."
print " Note: DNS record creation requires actual endpoint IPs"
print " Run after regional deployment to get endpoint IPs"
print " US East endpoint: us.api.example.com"
print " EU Central endpoint: eu.api.example.com"
print " Asia Pacific endpoint: asia.api.example.com"
} else {
print " No hosted zones found. Create one with:"
print " aws route53 create-hosted-zone --name api.example.com --caller-reference \$(date +%s)"
}
}
{|err|
print $" ⚠ Route53 setup note: ($err)"
}
)
}
def setup_database_replication [] {
print " Configuring multi-region database replication..."
print " Waiting for primary database to be ready..."
print " This may take 10-15 minutes on first deployment"
# Check if primary database is ready
let max_attempts = 30
mut attempts = 0
while $attempts < $max_attempts {
# Guard: Check database status (silently retry on error)
let db_result = (bash-wrap "doctl databases get us-db-primary --format Status --no-header")
if (is-ok $db_result) {
let status = $db_result.ok
if $status == "active" {
print " ✓ Primary database is active"
break
}
}
sleep 30sec
$attempts = ($attempts + 1)
}
print " Configuring read replicas..."
print " EU Central read replica: replication lag < 300s"
print " Asia Pacific read replica: replication lag < 300s"
print " ✓ Replication configuration complete"
}
def verify_multi_region_deployment [] {
print " Verifying DigitalOcean resources..."
# Guard: Verify DigitalOcean droplets
let do_droplets_result = (bash-wrap "doctl compute droplet list --format Name,Status --no-header")
(match-result $do_droplets_result
{|output|
print $" ✓ Found \(($output | split row \"\\n\" | length)\) droplets"
ok $output
}
{|err|
print $" ⚠ Error checking DigitalOcean: ($err)"
err $err
}
) | null
# Guard: Verify DigitalOcean load balancer
let do_lbs_result = (bash-wrap "doctl compute load-balancer list --format Name --no-header")
(match-result $do_lbs_result
{|output|
print $" ✓ Found load balancer"
ok $output
}
{|err|
print $" ⚠ Error checking DigitalOcean load balancer: ($err)"
err $err
}
) | null
print " Verifying Hetzner resources..."
# Guard: Verify Hetzner servers
let hz_servers_result = (bash-wrap "hcloud server list --format Name,Status")
(match-result $hz_servers_result
{|output|
print " ✓ Hetzner servers verified"
ok $output
}
{|err|
print $" ⚠ Error checking Hetzner: ($err)"
err $err
}
) | null
# Guard: Verify Hetzner load balancer
let hz_lbs_result = (bash-wrap "hcloud load-balancer list --format Name")
(match-result $hz_lbs_result
{|output|
print " ✓ Hetzner load balancer verified"
ok $output
}
{|err|
print $" ⚠ Error checking Hetzner load balancer: ($err)"
err $err
}
) | null
print " Verifying AWS resources..."
# Guard: Verify AWS EC2 instances
let aws_instances_result = (bash-wrap "aws ec2 describe-instances --region ap-southeast-1 --query 'Reservations[*].Instances[*].InstanceId' --output text | split row \" \" | length")
(match-result $aws_instances_result
{|output|
print $" ✓ Found ($output) EC2 instances"
ok $output
}
{|err|
print $" ⚠ Error checking AWS: ($err)"
err $err
}
) | null
# Guard: Verify AWS load balancers
let aws_lbs_result = (bash-wrap "aws elbv2 describe-load-balancers --region ap-southeast-1 --query 'LoadBalancers[*].LoadBalancerName' --output text")
(match-result $aws_lbs_result
{|output|
print " ✓ Application Load Balancer verified"
ok $output
}
{|err|
print $" ⚠ Error checking AWS load balancers: ($err)"
err $err
}
) | null
print ""
print " Summary:"
print " ✓ US East (DigitalOcean): Primary region, 3 droplets + LB + database"
print " ✓ EU Central (Hetzner): Secondary region, 3 servers + LB + read replica"
print " ✓ Asia Pacific (AWS): Tertiary region, 3 EC2 + ALB + read replica"
print " ✓ Multi-region deployment successful"
}
# Run main function
main --debug=$nu.env.DEBUG? --region=$nu.env.REGION?