442 lines
13 KiB
Plaintext
442 lines
13 KiB
Plaintext
#!/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?
|