442 lines
13 KiB
Plaintext
Raw Normal View History

#!/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?