442 lines
13 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.

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