Files
soleprint/station/tools/infra/digitalocean/__main__.py
2025-12-24 05:38:37 -03:00

270 lines
7.9 KiB
Python

"""
DigitalOcean Infrastructure for Amar Mascotas
Deploys:
- VPC for network isolation
- Droplet for Django app + Celery
- Managed PostgreSQL (with PostGIS via extension)
- Managed Redis
- Firewall rules
- (Optional) Load Balancer, Domain records
Estimated cost: ~$66/month
"""
import pulumi
import pulumi_digitalocean as do
import sys
sys.path.append("..")
from shared.config import get_config, APP_SERVER_INIT_SCRIPT
# Load configuration
cfg = get_config()
# =============================================================================
# NETWORKING
# =============================================================================
# VPC for private networking between resources
vpc = do.Vpc(
f"{cfg.resource_prefix}-vpc",
name=f"{cfg.resource_prefix}-vpc",
region="nyc1",
ip_range="10.10.10.0/24",
)
# =============================================================================
# DATABASE - Managed PostgreSQL
# =============================================================================
# DigitalOcean managed Postgres (PostGIS available as extension)
db_cluster = do.DatabaseCluster(
f"{cfg.resource_prefix}-db",
name=f"{cfg.resource_prefix}-db",
engine="pg",
version=cfg.db_version,
size="db-s-1vcpu-1gb", # $15/mo - smallest managed DB
region="nyc1",
node_count=1, # Single node (use 2+ for HA)
private_network_uuid=vpc.id,
tags=[cfg.environment],
)
# Create application database
db = do.DatabaseDb(
f"{cfg.resource_prefix}-database",
cluster_id=db_cluster.id,
name=cfg.db_name,
)
# Create database user
db_user = do.DatabaseUser(
f"{cfg.resource_prefix}-db-user",
cluster_id=db_cluster.id,
name=cfg.db_user,
)
# =============================================================================
# CACHE - Managed Redis
# =============================================================================
redis_cluster = do.DatabaseCluster(
f"{cfg.resource_prefix}-redis",
name=f"{cfg.resource_prefix}-redis",
engine="redis",
version=cfg.redis_version,
size="db-s-1vcpu-1gb", # $15/mo
region="nyc1",
node_count=1,
private_network_uuid=vpc.id,
tags=[cfg.environment],
)
# =============================================================================
# COMPUTE - Droplet
# =============================================================================
# SSH key (you should create this beforehand or import existing)
# ssh_key = do.SshKey(
# f"{cfg.resource_prefix}-ssh-key",
# name=f"{cfg.resource_prefix}-key",
# public_key=open("~/.ssh/id_rsa.pub").read(),
# )
# Use existing SSH keys (fetch by name or fingerprint)
ssh_keys = do.get_ssh_keys()
# App server droplet
droplet = do.Droplet(
f"{cfg.resource_prefix}-app",
name=f"{cfg.resource_prefix}-app",
image="ubuntu-22-04-x64",
size="s-2vcpu-4gb", # $24/mo - 4GB RAM, 2 vCPU
region="nyc1",
vpc_uuid=vpc.id,
ssh_keys=[k.id for k in ssh_keys.ssh_keys[:1]] if ssh_keys.ssh_keys else [],
user_data=APP_SERVER_INIT_SCRIPT,
tags=[cfg.environment, "app"],
opts=pulumi.ResourceOptions(depends_on=[db_cluster, redis_cluster]),
)
# =============================================================================
# FIREWALL
# =============================================================================
firewall = do.Firewall(
f"{cfg.resource_prefix}-firewall",
name=f"{cfg.resource_prefix}-firewall",
droplet_ids=[droplet.id],
# Inbound rules
inbound_rules=[
# SSH (restrict to specific IPs in production)
do.FirewallInboundRuleArgs(
protocol="tcp",
port_range="22",
source_addresses=cfg.allowed_ssh_ips or ["0.0.0.0/0", "::/0"],
),
# HTTP
do.FirewallInboundRuleArgs(
protocol="tcp",
port_range="80",
source_addresses=["0.0.0.0/0", "::/0"],
),
# HTTPS
do.FirewallInboundRuleArgs(
protocol="tcp",
port_range="443",
source_addresses=["0.0.0.0/0", "::/0"],
),
],
# Outbound rules (allow all outbound)
outbound_rules=[
do.FirewallOutboundRuleArgs(
protocol="tcp",
port_range="1-65535",
destination_addresses=["0.0.0.0/0", "::/0"],
),
do.FirewallOutboundRuleArgs(
protocol="udp",
port_range="1-65535",
destination_addresses=["0.0.0.0/0", "::/0"],
),
do.FirewallOutboundRuleArgs(
protocol="icmp",
destination_addresses=["0.0.0.0/0", "::/0"],
),
],
)
# =============================================================================
# DATABASE FIREWALL - Only allow app server
# =============================================================================
db_firewall = do.DatabaseFirewall(
f"{cfg.resource_prefix}-db-firewall",
cluster_id=db_cluster.id,
rules=[
do.DatabaseFirewallRuleArgs(
type="droplet",
value=droplet.id,
),
],
)
redis_firewall = do.DatabaseFirewall(
f"{cfg.resource_prefix}-redis-firewall",
cluster_id=redis_cluster.id,
rules=[
do.DatabaseFirewallRuleArgs(
type="droplet",
value=droplet.id,
),
],
)
# =============================================================================
# OPTIONAL: Load Balancer (uncomment if needed)
# =============================================================================
# load_balancer = do.LoadBalancer(
# f"{cfg.resource_prefix}-lb",
# name=f"{cfg.resource_prefix}-lb",
# region="nyc1",
# vpc_uuid=vpc.id,
# droplet_ids=[droplet.id],
# forwarding_rules=[
# do.LoadBalancerForwardingRuleArgs(
# entry_port=443,
# entry_protocol="https",
# target_port=80,
# target_protocol="http",
# certificate_name=f"{cfg.resource_prefix}-cert",
# ),
# do.LoadBalancerForwardingRuleArgs(
# entry_port=80,
# entry_protocol="http",
# target_port=80,
# target_protocol="http",
# ),
# ],
# healthcheck=do.LoadBalancerHealthcheckArgs(
# port=80,
# protocol="http",
# path="/health/",
# ),
# )
# =============================================================================
# OPTIONAL: DNS Records (uncomment if managing domain in DO)
# =============================================================================
# domain = do.Domain(
# f"{cfg.resource_prefix}-domain",
# name=cfg.domain,
# )
#
# api_record = do.DnsRecord(
# f"{cfg.resource_prefix}-api-dns",
# domain=domain.name,
# type="A",
# name="backoffice",
# value=droplet.ipv4_address,
# ttl=300,
# )
# =============================================================================
# OUTPUTS
# =============================================================================
pulumi.export("droplet_ip", droplet.ipv4_address)
pulumi.export("droplet_private_ip", droplet.ipv4_address_private)
pulumi.export("db_host", db_cluster.private_host)
pulumi.export("db_port", db_cluster.port)
pulumi.export("db_name", cfg.db_name)
pulumi.export("db_user", cfg.db_user)
pulumi.export("db_password", db_user.password)
pulumi.export("redis_host", redis_cluster.private_host)
pulumi.export("redis_port", redis_cluster.port)
pulumi.export("redis_password", redis_cluster.password)
# Generate .env content for easy deployment
pulumi.export("env_file", pulumi.Output.all(
db_cluster.private_host,
db_cluster.port,
db_user.password,
redis_cluster.private_host,
redis_cluster.port,
redis_cluster.password,
).apply(lambda args: f"""
# Generated by Pulumi - DigitalOcean
DB_HOST={args[0]}
DB_PORT={args[1]}
DB_NAME={cfg.db_name}
DB_USER={cfg.db_user}
DB_PASSWORD={args[2]}
CELERY_BROKER_URL=rediss://default:{args[5]}@{args[3]}:{args[4]}
CELERY_RESULT_BACKEND=rediss://default:{args[5]}@{args[3]}:{args[4]}
"""))