576 lines
17 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
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 = (do { nickel export workspace.ncl | from json } | complete)
if $nickel_result.exit_code == 0 {
print " ✓ Nickel configuration is valid"
} else {
error make {msg: $"Nickel validation failed: ($nickel_result.stderr)"}
}
# Validate config.toml
print " Validating config.toml..."
let config_result = (do { open config.toml } | complete)
if $config_result.exit_code == 0 {
print " ✓ config.toml is valid"
} else {
error make {msg: $"config.toml validation failed: ($config_result.stderr)"}
}
# Test provider connectivity
print " Testing provider connectivity..."
let do_connect = (do { doctl account get } | complete)
if $do_connect.exit_code == 0 {
print " ✓ DigitalOcean connectivity verified"
} else {
error make {msg: $"DigitalOcean connectivity failed: ($do_connect.stderr)"}
}
let hz_connect = (do { hcloud server list } | complete)
if $hz_connect.exit_code == 0 {
print " ✓ Hetzner connectivity verified"
} else {
error make {msg: $"Hetzner connectivity failed: ($hz_connect.stderr)"}
}
let aws_connect = (do { aws sts get-caller-identity } | complete)
if $aws_connect.exit_code == 0 {
print " ✓ AWS connectivity verified"
} else {
error make {msg: $"AWS connectivity failed: ($aws_connect.stderr)"}
}
}
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)..."
let db_result = (do {
doctl databases create \
--engine pg \
--version 14 \
--region "nyc3" \
--num-nodes 3 \
--size "db-s-2vcpu-4gb" \
--name "us-db-primary"
} | complete)
if $db_result.exit_code == 0 {
print " ✓ Database creation initiated (may take 10-15 minutes)"
} else {
print $" ⚠ Database creation error (may already exist): ($db_result.stderr)"
}
}
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..."
let replica_result = (do {
aws rds create-db-instance-read-replica \
--region ap-southeast-1 \
--db-instance-identifier "asia-db-replica" \
--source-db-instance-identifier "us-db-primary"
} | complete)
if $replica_result.exit_code == 0 {
print " ✓ Read replica creation initiated"
} else {
print $" ⚠ Read replica creation error (may already exist): ($replica_result.stderr)"
}
}
def setup_vpn_tunnels [] {
print " Setting up IPSec VPN tunnels between regions..."
# US to EU VPN
print " Creating US East → EU Central VPN tunnel..."
let vpn_result = (do {
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}]"
} | complete)
if $vpn_result.exit_code == 0 {
print " ✓ VPN gateway created (manual completion required)"
} else {
print $" VPN setup note: ($vpn_result.stderr)"
}
# 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..."
let dns_result = (do {
aws route53 list-hosted-zones | from json
} | complete)
if $dns_result.exit_code == 0 {
let hosted_zones = ($dns_result.stdout | 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)"
}
} else {
print $" ⚠ Route53 setup note: ($dns_result.stderr)"
}
}
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 {
let db_check = (do { doctl databases get us-db-primary --format Status --no-header } | complete)
if $db_check.exit_code == 0 {
let db = ($db_check.stdout | str trim)
if $db == "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..."
let do_result = (do {
let do_droplets = (doctl compute droplet list --format Name,Status --no-header)
let do_lbs = (doctl compute load-balancer list --format Name --no-header)
} | complete)
if $do_result.exit_code == 0 {
let do_droplets = ($do_result.stdout | lines | length)
print $" ✓ Found ($do_droplets) droplets"
print " ✓ Found load balancer"
} else {
print $" ⚠ Error checking DigitalOcean: ($do_result.stderr)"
}
print " Verifying Hetzner resources..."
let hz_result = (do {
let hz_servers = (hcloud server list --format Name,Status)
let hz_lbs = (hcloud load-balancer list --format Name)
} | complete)
if $hz_result.exit_code == 0 {
print " ✓ Hetzner servers verified"
print " ✓ Hetzner load balancer verified"
} else {
print $" ⚠ Error checking Hetzner: ($hz_result.stderr)"
}
print " Verifying AWS resources..."
let aws_result = (do {
let aws_instances = (aws ec2 describe-instances \
--region ap-southeast-1 \
--query 'Reservations[*].Instances[*].InstanceId' \
--output text | split row " " | length)
let aws_lbs = (aws elbv2 describe-load-balancers \
--region ap-southeast-1 \
--query 'LoadBalancers[*].LoadBalancerName' \
--output text)
} | complete)
if $aws_result.exit_code == 0 {
print " ✓ Found EC2 instances"
print " ✓ Application Load Balancer verified"
} else {
print $" ⚠ Error checking AWS: ($aws_result.stderr)"
}
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?