559 lines
16 KiB
Plaintext
559 lines
16 KiB
Plaintext
|
|
#!/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
|
|||
|
|
|
|||
|
|
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..."
|
|||
|
|
try {
|
|||
|
|
nickel export workspace.ncl | from json | null
|
|||
|
|
print " ✓ Nickel configuration is valid"
|
|||
|
|
} catch {|err|
|
|||
|
|
error make {msg: $"Nickel validation failed: ($err)"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Validate config.toml
|
|||
|
|
print " Validating config.toml..."
|
|||
|
|
try {
|
|||
|
|
let config = (open config.toml)
|
|||
|
|
print " ✓ config.toml is valid"
|
|||
|
|
} catch {|err|
|
|||
|
|
error make {msg: $"config.toml validation failed: ($err)"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Test provider connectivity
|
|||
|
|
print " Testing provider connectivity..."
|
|||
|
|
try {
|
|||
|
|
doctl account get | null
|
|||
|
|
print " ✓ DigitalOcean connectivity verified"
|
|||
|
|
} catch {|err|
|
|||
|
|
error make {msg: $"DigitalOcean connectivity failed: ($err)"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
hcloud server list | null
|
|||
|
|
print " ✓ Hetzner connectivity verified"
|
|||
|
|
} catch {|err|
|
|||
|
|
error make {msg: $"Hetzner connectivity failed: ($err)"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
aws sts get-caller-identity | null
|
|||
|
|
print " ✓ AWS connectivity verified"
|
|||
|
|
} catch {|err|
|
|||
|
|
error make {msg: $"AWS connectivity failed: ($err)"}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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)..."
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
doctl databases create \
|
|||
|
|
--engine pg \
|
|||
|
|
--version 14 \
|
|||
|
|
--region "nyc3" \
|
|||
|
|
--num-nodes 3 \
|
|||
|
|
--size "db-s-2vcpu-4gb" \
|
|||
|
|
--name "us-db-primary" | null
|
|||
|
|
|
|||
|
|
print " ✓ Database creation initiated (may take 10-15 minutes)"
|
|||
|
|
} catch {|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..."
|
|||
|
|
try {
|
|||
|
|
aws rds create-db-instance-read-replica \
|
|||
|
|
--region ap-southeast-1 \
|
|||
|
|
--db-instance-identifier "asia-db-replica" \
|
|||
|
|
--source-db-instance-identifier "us-db-primary" | null
|
|||
|
|
|
|||
|
|
print " ✓ Read replica creation initiated"
|
|||
|
|
} catch {|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..."
|
|||
|
|
try {
|
|||
|
|
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}]" | null
|
|||
|
|
|
|||
|
|
print " ✓ VPN gateway created (manual completion required)"
|
|||
|
|
} catch {|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..."
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
let hosted_zones = (aws route53 list-hosted-zones | 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)"
|
|||
|
|
}
|
|||
|
|
} catch {|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 {
|
|||
|
|
try {
|
|||
|
|
let db = (doctl databases get us-db-primary --format Status --no-header)
|
|||
|
|
if $db == "active" {
|
|||
|
|
print " ✓ Primary database is active"
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
# Database not ready yet
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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..."
|
|||
|
|
try {
|
|||
|
|
let do_droplets = (doctl compute droplet list --format Name,Status --no-header)
|
|||
|
|
print $" ✓ Found ($do_droplets | split row "\n" | length) droplets"
|
|||
|
|
|
|||
|
|
let do_lbs = (doctl compute load-balancer list --format Name --no-header)
|
|||
|
|
print $" ✓ Found load balancer"
|
|||
|
|
} catch {|err|
|
|||
|
|
print $" ⚠ Error checking DigitalOcean: ($err)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print " Verifying Hetzner resources..."
|
|||
|
|
try {
|
|||
|
|
let hz_servers = (hcloud server list --format Name,Status)
|
|||
|
|
print " ✓ Hetzner servers verified"
|
|||
|
|
|
|||
|
|
let hz_lbs = (hcloud load-balancer list --format Name)
|
|||
|
|
print " ✓ Hetzner load balancer verified"
|
|||
|
|
} catch {|err|
|
|||
|
|
print $" ⚠ Error checking Hetzner: ($err)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print " Verifying AWS resources..."
|
|||
|
|
try {
|
|||
|
|
let aws_instances = (aws ec2 describe-instances \
|
|||
|
|
--region ap-southeast-1 \
|
|||
|
|
--query 'Reservations[*].Instances[*].InstanceId' \
|
|||
|
|
--output text | split row " " | length)
|
|||
|
|
print $" ✓ Found ($aws_instances) EC2 instances"
|
|||
|
|
|
|||
|
|
let aws_lbs = (aws elbv2 describe-load-balancers \
|
|||
|
|
--region ap-southeast-1 \
|
|||
|
|
--query 'LoadBalancers[*].LoadBalancerName' \
|
|||
|
|
--output text)
|
|||
|
|
print " ✓ Application Load Balancer verified"
|
|||
|
|
} catch {|err|
|
|||
|
|
print $" ⚠ Error checking AWS: ($err)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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?
|