soleprint init commit
This commit is contained in:
269
station/tools/infra/digitalocean/__main__.py
Normal file
269
station/tools/infra/digitalocean/__main__.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
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]}
|
||||
"""))
|
||||
Reference in New Issue
Block a user