""" 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]} """))