270 lines
7.9 KiB
Python
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]}
|
|
"""))
|