#!/usr/bin/env nu # Multi-Provider Web App Deployment Script # Orchestrates deployment across DigitalOcean, AWS, and Hetzner def main [--debug: bool = false] { print "šŸš€ Multi-Provider Web App Deployment" print "────────────────────────────────────────" # Enable debug output if requested if $debug { print "āœ“ Debug mode enabled" } # Step 1: Validate configuration print "\nšŸ“‹ Step 1: Validating configuration..." validate_environment # Step 2: Create networks first print "\n🌐 Step 2: Creating private networks..." create_digitalocean_vpc create_aws_vpc create_hetzner_network # Step 3: Create provider resources print "\nā˜ļø Step 3: Creating compute and database resources..." create_hetzner_resources create_aws_resources create_digitalocean_resources # Step 4: Setup VPN tunnels print "\nšŸ” Step 4: Setting up VPN tunnels for secure communication..." setup_vpn_do_to_aws setup_vpn_aws_to_hetzner # Step 5: Configure security print "\nšŸ”’ Step 5: Configuring security..." setup_firewall_rules setup_security_groups # Step 6: Deploy application print "\nšŸ“¦ Step 6: Deploying application..." deploy_application # Step 7: Verify deployment print "\nāœ… Step 7: Verifying deployment..." verify_deployment print "\nšŸŽ‰ Deployment complete!" print "Your application is now live on three cloud providers." } def validate_environment [] { # Check required environment variables let required = [ "DIGITALOCEAN_TOKEN", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "HCLOUD_TOKEN" ] $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", "aws", "hcloud", "nickel"] $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)"} } } def create_hetzner_network [] { print " Creating Hetzner private network..." let network = (hcloud network create \ --name "backup-network" \ --ip-range "10.2.0.0/16" \ --format "json" | from json) print $" āœ“ Created network: ($network.network.id)" # Create subnet hcloud network add-subnet backup-network \ --ip-range "10.2.1.0/24" \ --network-zone "eu-central" print " āœ“ Created subnet: 10.2.1.0/24" } def create_hetzner_resources [] { print " Creating Hetzner backup volume..." # Create volume attached to network let volume = (hcloud volume create \ --name "webapp-backups" \ --size 500 \ --location "nbg1" \ --format "ext4" | from json) print $" āœ“ Created volume: ($volume.id)" # Wait for volume to be ready let max_wait = 60 mut attempts = 0 while $attempts < $max_wait { let status = (hcloud volume describe "webapp-backups" --output json | from json) if $status.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 create_aws_vpc [] { print " Creating AWS VPC..." let vpc = (aws ec2 create-vpc \ --cidr-block "10.1.0.0/16" \ --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=webapp-vpc}]" | from json) print $" āœ“ Created VPC: ($vpc.Vpc.VpcId)" # Create private subnet for database let subnet = (aws ec2 create-subnet \ --vpc-id $vpc.Vpc.VpcId \ --cidr-block "10.1.1.0/24" \ --availability-zone "us-east-1a" | from json) print $" āœ“ Created private subnet: ($subnet.Subnet.SubnetId)" # Create security group for RDS let sg = (aws ec2 create-security-group \ --group-name "webapp-db-sg" \ --description "Security group for RDS database" \ --vpc-id $vpc.Vpc.VpcId | from json) print $" āœ“ Created security group: ($sg.GroupId)" # Allow PostgreSQL from DO VPC (10.0.0.0/16) aws ec2 authorize-security-group-ingress \ --group-id $sg.GroupId \ --protocol tcp \ --port 5432 \ --cidr "10.0.0.0/16" print " āœ“ Allowed PostgreSQL from DO network (10.0.0.0/16)" } def create_aws_resources [] { print " Creating AWS RDS PostgreSQL database..." # Create RDS instance in private subnet let db_response = (aws rds create-db-instance \ --db-instance-identifier "webapp-db" \ --db-instance-class "db.t3.medium" \ --engine "postgres" \ --engine-version "14.6" \ --master-username "admin" \ --master-user-password "YourSecurePassword123!" \ --allocated-storage 100 \ --storage-type "gp3" \ --multi-az \ --db-subnet-group-name "default" | from json) print $" āœ“ Created RDS instance: ($db_response.DBInstance.DBInstanceIdentifier)" # Wait for database to be available print " Waiting for database to be available (this may take 10-15 minutes)..." let max_wait = 900 # 15 minutes mut attempts = 0 while $attempts < $max_wait { let db_check = (do { aws rds describe-db-instances \ --db-instance-identifier "webapp-db" | from json } | complete) if $db_check.exit_code == 0 { let db_info = ($db_check.stdout | from json) if ($db_info.DBInstances | first).DBInstanceStatus == "available" { print " āœ“ Database is available" let endpoint = ($db_info.DBInstances | first).Endpoint.Address print $" āœ“ Database endpoint: ($endpoint)" break } } sleep 10sec $attempts = ($attempts + 10) } if $attempts >= $max_wait { error make {msg: "AWS RDS failed to become available"} } } def create_digitalocean_vpc [] { print " Creating DigitalOcean VPC..." let vpc = (doctl compute vpc create \ --name "web-app-vpc" \ --region "nyc3" \ --ip-range "10.0.0.0/16" \ --format ID \ --no-header) print $" āœ“ Created VPC: ($vpc)" } def create_digitalocean_resources [] { print " Creating DigitalOcean droplets and load balancer..." # Create droplets 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 \ $"web-server-($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: web-server-($i)" $response } ) # Wait for droplets to be ready print " Waiting for droplets to be ready..." 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" # Create load balancer print " Creating DigitalOcean load balancer..." let lb = (doctl compute load-balancer create \ --name "web-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)" } def setup_vpn_do_to_aws [] { print " Setting up VPN tunnel: DigitalOcean → AWS..." # Create VPN Gateway in AWS let vgw = (aws ec2 create-vpn-gateway \ --type "ipsec.1" \ --tag-specifications "ResourceType=vpn-gateway,Tags=[{Key=Name,Value=webapp-vpn-gw}]" | from json) print $" āœ“ Created VPN Gateway: ($vgw.VpnGateway.VpnGatewayId)" # Create Customer Gateway (DigitalOcean endpoint - use placeholder) let cgw = (aws ec2 create-customer-gateway \ --type "ipsec.1" \ --public-ip "203.0.113.12" \ --tag-specifications "ResourceType=customer-gateway,Tags=[{Key=Name,Value=do-vpn-endpoint}]" | from json) print $" āœ“ Created Customer Gateway: ($cgw.CustomerGateway.CustomerGatewayId)" # Create VPN Connection let vpn = (aws ec2 create-vpn-connection \ --type "ipsec.1" \ --customer-gateway-id $cgw.CustomerGateway.CustomerGatewayId \ --vpn-gateway-id $vgw.VpnGateway.VpnGatewayId | from json) print $" āœ“ Created VPN Connection: ($vpn.VpnConnection.VpnConnectionId)" print " Note: Download VPN configuration and apply to DigitalOcean endpoint manually" } def setup_vpn_aws_to_hetzner [] { print " Setting up VPN tunnel: AWS → Hetzner..." # For Hetzner, VPN is typically set up via StrongSwan or Wireguard on their servers print " VPN tunnel to Hetzner requires manual configuration on Hetzner server" print " Use SSH to configure StrongSwan or Wireguard based on network requirements" print " Example configuration steps:" print " 1. SSH into Hetzner server" print " 2. Install StrongSwan: apt-get install strongswan" print " 3. Configure IPSec with AWS VPN gateway" print " 4. Enable IP forwarding: sysctl -w net.ipv4.ip_forward=1" print " 5. Verify connectivity: ping 10.1.1.10 (AWS RDS)" } def setup_firewall_rules [] { print " Configuring firewall rules..." # Allow SSH, HTTP, HTTPS on DigitalOcean droplets print " Configuring DigitalOcean firewall..." let fw = (doctl compute firewall create \ --name "web-firewall" \ --inbound-rules "protocol:tcp,ports:22,sources:addresses:0.0.0.0/0;sources:addresses::/0" \ --inbound-rules "protocol:tcp,ports:80,sources:addresses:0.0.0.0/0;sources:addresses::/0" \ --inbound-rules "protocol:tcp,ports:443,sources:addresses:0.0.0.0/0;sources:addresses::/0" \ --format ID \ --no-header | into string) print $" āœ“ Created firewall: ($fw)" } def setup_security_groups [] { print " Configuring AWS security groups..." # Create security group for RDS let sg = (aws ec2 create-security-group \ --group-name "webapp-db-sg" \ --description "Security group for webapp RDS database" | from json) print $" āœ“ Created security group: ($sg.GroupId)" # Allow PostgreSQL from web tier CIDR aws ec2 authorize-security-group-ingress \ --group-id $sg.GroupId \ --protocol tcp \ --port 5432 \ --cidr 10.0.0.0/8 print " āœ“ Configured PostgreSQL access" } def deploy_application [] { # Get web server IPs let droplets = (doctl compute droplet list --format Name,PublicIPv4 --no-header) $droplets | lines | each {|line| if ($line | is-not-empty) { let parts = ($line | split column --max-columns 2 " " | get column1) let ip = ($parts | last) let name = ($parts | first) print $" Deploying to ($name) at ($ip)..." # Note: In production, you would: # 1. SSH into the server # 2. Clone application repository # 3. Install dependencies # 4. Configure environment variables # 5. Start application print $" āœ“ Ready for application deployment at ssh://root@($ip)" } } } def verify_deployment [] { print " Verifying DigitalOcean droplets..." let droplets = (doctl compute droplet list --no-header --format ID,Name,Status) print $" Found ($droplets | lines | length) droplets" print " Verifying AWS RDS database..." let db_check = (do { aws rds describe-db-instances \ --db-instance-identifier "webapp-db" | from json } | complete) if $db_check.exit_code == 0 { let db = ($db_check.stdout | from json) let endpoint = ($db.DBInstances | first).Endpoint.Address print $" Database endpoint: ($endpoint)" } else { print " Note: Database may still be initializing" } print " Verifying Hetzner volumes..." let volumes = (hcloud volume list --format "ID Name Status" --no-header) print $" Found ($volumes | lines | length) volumes" print "\n Summary:" print " āœ“ DigitalOcean: 3 web servers + load balancer" print " āœ“ AWS: PostgreSQL database (Multi-AZ)" print " āœ“ Hetzner: Backup storage volume" } # Run main function main --debug=$nu.env.DEBUG?