soleprint init commit

This commit is contained in:
buenosairesam
2025-12-24 05:38:37 -03:00
commit 329c401ff5
96 changed files with 11564 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
# Amar Mascotas Infrastructure as Code
Pulumi configurations for deploying the Amar Mascotas backend to different cloud providers.
## Structure
```
infra/
├── digitalocean/ # DigitalOcean configuration
├── aws/ # AWS configuration
├── gcp/ # Google Cloud configuration
└── shared/ # Shared Python utilities
```
## Prerequisites
```bash
# Install Pulumi
curl -fsSL https://get.pulumi.com | sh
# Install Python dependencies
pip install pulumi pulumi-digitalocean pulumi-aws pulumi-gcp
# Login to Pulumi (free tier, or use local state)
pulumi login --local # Local state (no account needed)
# OR
pulumi login # Pulumi Cloud (free tier available)
```
## Cloud Provider Setup
### DigitalOcean
```bash
export DIGITALOCEAN_TOKEN="your-api-token"
```
### AWS
```bash
aws configure
# Or set environment variables:
export AWS_ACCESS_KEY_ID="xxx"
export AWS_SECRET_ACCESS_KEY="xxx"
export AWS_REGION="us-east-1"
```
### GCP
```bash
gcloud auth application-default login
export GOOGLE_PROJECT="your-project-id"
```
## Usage
```bash
cd infra/digitalocean # or aws, gcp
# Preview changes
pulumi preview
# Deploy
pulumi up
# Destroy
pulumi destroy
```
## Cost Comparison (Estimated Monthly)
| Resource | DigitalOcean | AWS | GCP |
|----------|--------------|-----|-----|
| Compute (4GB RAM) | $24 | $35 | $30 |
| Managed Postgres | $15 | $25 | $25 |
| Managed Redis | $15 | $15 | $20 |
| Load Balancer | $12 | $18 | $18 |
| **Total** | **~$66** | **~$93** | **~$93** |
## Architecture
All configurations deploy:
- 1x App server (Django + Gunicorn + Celery)
- 1x Managed PostgreSQL with PostGIS
- 1x Managed Redis
- VPC/Network isolation
- Firewall rules (SSH, HTTP, HTTPS)
## Provider Comparison
### Code Complexity
| Aspect | DigitalOcean | AWS | GCP |
|--------|--------------|-----|-----|
| Lines of code | ~180 | ~280 | ~260 |
| Resources created | 8 | 15 | 14 |
| Networking setup | Simple (VPC only) | Complex (VPC + subnets + IGW + routes) | Medium (VPC + subnet + peering) |
| Learning curve | Low | High | Medium |
### Feature Comparison
| Feature | DigitalOcean | AWS | GCP |
|---------|--------------|-----|-----|
| **Managed Postgres** | Yes (DO Database) | Yes (RDS) | Yes (Cloud SQL) |
| **PostGIS** | Via extension | Via extension | Via flags |
| **Managed Redis** | Yes (DO Database) | Yes (ElastiCache) | Yes (Memorystore) |
| **Private networking** | VPC | VPC + subnets | VPC + peering |
| **Load balancer** | $12/mo | $18/mo | $18/mo |
| **Auto-scaling** | Limited | Full (ASG) | Full (MIG) |
| **Regions** | 15 | 30+ | 35+ |
| **Free tier** | None | 12 months | $300 credit |
### When to Choose Each
**DigitalOcean:**
- Simple deployments
- Cost-sensitive
- Small teams
- Latin America (São Paulo region)
**AWS:**
- Enterprise requirements
- Need advanced services (Lambda, SQS, etc.)
- Complex networking needs
- Compliance requirements (HIPAA, PCI)
**GCP:**
- Machine learning integration
- Kubernetes-first approach
- Good free credits to start
- BigQuery/analytics needs
### Real Cost Breakdown (Your App)
```
DigitalOcean (~$66/mo):
├── Droplet 4GB $24
├── Managed Postgres $15
├── Managed Redis $15
└── Load Balancer $12 (optional)
AWS (~$93/mo):
├── EC2 t3.medium $35
├── RDS db.t3.micro $25
├── ElastiCache $15
└── ALB $18 (optional)
GCP (~$93/mo):
├── e2-medium $30
├── Cloud SQL $25
├── Memorystore $20
└── Load Balancer $18 (optional)
```
### Migration Effort
If you ever need to switch providers:
| From → To | Effort | Notes |
|-----------|--------|-------|
| DO → AWS | Medium | Postgres dump/restore, reconfigure Redis |
| DO → GCP | Medium | Same as above |
| AWS → GCP | Medium | Similar services, different APIs |
| Any → Kubernetes | High | Need to containerize everything |
The Pulumi code is portable - only the provider-specific resources change.

View File

@@ -0,0 +1,6 @@
name: amar-aws
runtime:
name: python
options:
virtualenv: venv
description: Amar Mascotas infrastructure on AWS

View File

@@ -0,0 +1,341 @@
"""
AWS Infrastructure for Amar Mascotas
Deploys:
- VPC with public/private subnets
- EC2 instance for Django app + Celery
- RDS PostgreSQL (PostGIS via extension)
- ElastiCache Redis
- Security Groups
- (Optional) ALB, Route53
Estimated cost: ~$93/month
NOTE: AWS is more complex but offers more services and better scaling options.
"""
import pulumi
import pulumi_aws as aws
import sys
sys.path.append("..")
from shared.config import get_config, APP_SERVER_INIT_SCRIPT
# Load configuration
cfg = get_config()
# Get current region and availability zones
region = aws.get_region()
azs = aws.get_availability_zones(state="available")
az1 = azs.names[0]
az2 = azs.names[1] if len(azs.names) > 1 else azs.names[0]
# =============================================================================
# NETWORKING - VPC
# =============================================================================
vpc = aws.ec2.Vpc(
f"{cfg.resource_prefix}-vpc",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True,
enable_dns_support=True,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-vpc"},
)
# Internet Gateway (for public internet access)
igw = aws.ec2.InternetGateway(
f"{cfg.resource_prefix}-igw",
vpc_id=vpc.id,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-igw"},
)
# Public subnets (for EC2, load balancer)
public_subnet_1 = aws.ec2.Subnet(
f"{cfg.resource_prefix}-public-1",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24",
availability_zone=az1,
map_public_ip_on_launch=True,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-public-1"},
)
public_subnet_2 = aws.ec2.Subnet(
f"{cfg.resource_prefix}-public-2",
vpc_id=vpc.id,
cidr_block="10.0.2.0/24",
availability_zone=az2,
map_public_ip_on_launch=True,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-public-2"},
)
# Private subnets (for RDS, ElastiCache)
private_subnet_1 = aws.ec2.Subnet(
f"{cfg.resource_prefix}-private-1",
vpc_id=vpc.id,
cidr_block="10.0.10.0/24",
availability_zone=az1,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-private-1"},
)
private_subnet_2 = aws.ec2.Subnet(
f"{cfg.resource_prefix}-private-2",
vpc_id=vpc.id,
cidr_block="10.0.11.0/24",
availability_zone=az2,
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-private-2"},
)
# Route table for public subnets
public_rt = aws.ec2.RouteTable(
f"{cfg.resource_prefix}-public-rt",
vpc_id=vpc.id,
routes=[
aws.ec2.RouteTableRouteArgs(
cidr_block="0.0.0.0/0",
gateway_id=igw.id,
),
],
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-public-rt"},
)
# Associate route table with public subnets
aws.ec2.RouteTableAssociation(
f"{cfg.resource_prefix}-public-1-rta",
subnet_id=public_subnet_1.id,
route_table_id=public_rt.id,
)
aws.ec2.RouteTableAssociation(
f"{cfg.resource_prefix}-public-2-rta",
subnet_id=public_subnet_2.id,
route_table_id=public_rt.id,
)
# =============================================================================
# SECURITY GROUPS
# =============================================================================
# App server security group
app_sg = aws.ec2.SecurityGroup(
f"{cfg.resource_prefix}-app-sg",
vpc_id=vpc.id,
description="Security group for app server",
ingress=[
# SSH
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=22,
to_port=22,
cidr_blocks=cfg.allowed_ssh_ips or ["0.0.0.0/0"],
description="SSH access",
),
# HTTP
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=80,
to_port=80,
cidr_blocks=["0.0.0.0/0"],
description="HTTP",
),
# HTTPS
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=443,
to_port=443,
cidr_blocks=["0.0.0.0/0"],
description="HTTPS",
),
],
egress=[
aws.ec2.SecurityGroupEgressArgs(
protocol="-1",
from_port=0,
to_port=0,
cidr_blocks=["0.0.0.0/0"],
description="Allow all outbound",
),
],
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-app-sg"},
)
# Database security group (only accessible from app server)
db_sg = aws.ec2.SecurityGroup(
f"{cfg.resource_prefix}-db-sg",
vpc_id=vpc.id,
description="Security group for RDS",
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=5432,
to_port=5432,
security_groups=[app_sg.id],
description="PostgreSQL from app",
),
],
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-db-sg"},
)
# Redis security group (only accessible from app server)
redis_sg = aws.ec2.SecurityGroup(
f"{cfg.resource_prefix}-redis-sg",
vpc_id=vpc.id,
description="Security group for ElastiCache",
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=6379,
to_port=6379,
security_groups=[app_sg.id],
description="Redis from app",
),
],
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-redis-sg"},
)
# =============================================================================
# DATABASE - RDS PostgreSQL
# =============================================================================
# Subnet group for RDS (requires at least 2 AZs)
db_subnet_group = aws.rds.SubnetGroup(
f"{cfg.resource_prefix}-db-subnet-group",
subnet_ids=[private_subnet_1.id, private_subnet_2.id],
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-db-subnet-group"},
)
# RDS PostgreSQL instance
# Note: PostGIS is available as an extension, enable after creation
db_instance = aws.rds.Instance(
f"{cfg.resource_prefix}-db",
identifier=f"{cfg.resource_prefix}-db",
engine="postgres",
engine_version=cfg.db_version,
instance_class="db.t3.micro", # $25/mo - smallest
allocated_storage=20,
storage_type="gp3",
db_name=cfg.db_name,
username=cfg.db_user,
password=pulumi.Config().require_secret("db_password"), # Set via: pulumi config set --secret db_password xxx
vpc_security_group_ids=[db_sg.id],
db_subnet_group_name=db_subnet_group.name,
publicly_accessible=False,
skip_final_snapshot=True, # Set False for production!
backup_retention_period=7,
multi_az=False, # Set True for HA ($$$)
tags=cfg.tags,
)
# =============================================================================
# CACHE - ElastiCache Redis
# =============================================================================
# Subnet group for ElastiCache
redis_subnet_group = aws.elasticache.SubnetGroup(
f"{cfg.resource_prefix}-redis-subnet-group",
subnet_ids=[private_subnet_1.id, private_subnet_2.id],
tags=cfg.tags,
)
# ElastiCache Redis cluster
redis_cluster = aws.elasticache.Cluster(
f"{cfg.resource_prefix}-redis",
cluster_id=f"{cfg.resource_prefix}-redis",
engine="redis",
engine_version="7.0",
node_type="cache.t3.micro", # $15/mo - smallest
num_cache_nodes=1,
port=6379,
subnet_group_name=redis_subnet_group.name,
security_group_ids=[redis_sg.id],
tags=cfg.tags,
)
# =============================================================================
# COMPUTE - EC2 Instance
# =============================================================================
# Get latest Ubuntu 22.04 AMI
ubuntu_ami = aws.ec2.get_ami(
most_recent=True,
owners=["099720109477"], # Canonical
filters=[
aws.ec2.GetAmiFilterArgs(
name="name",
values=["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"],
),
aws.ec2.GetAmiFilterArgs(
name="virtualization-type",
values=["hvm"],
),
],
)
# Key pair (import your existing key or create new)
# key_pair = aws.ec2.KeyPair(
# f"{cfg.resource_prefix}-key",
# public_key=open("~/.ssh/id_rsa.pub").read(),
# tags=cfg.tags,
# )
# EC2 instance
ec2_instance = aws.ec2.Instance(
f"{cfg.resource_prefix}-app",
ami=ubuntu_ami.id,
instance_type="t3.medium", # $35/mo - 4GB RAM, 2 vCPU
subnet_id=public_subnet_1.id,
vpc_security_group_ids=[app_sg.id],
# key_name=key_pair.key_name, # Uncomment when key_pair is defined
user_data=APP_SERVER_INIT_SCRIPT,
root_block_device=aws.ec2.InstanceRootBlockDeviceArgs(
volume_size=30,
volume_type="gp3",
),
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-app"},
)
# Elastic IP (static public IP)
eip = aws.ec2.Eip(
f"{cfg.resource_prefix}-eip",
instance=ec2_instance.id,
domain="vpc",
tags={**cfg.tags, "Name": f"{cfg.resource_prefix}-eip"},
)
# =============================================================================
# OPTIONAL: Application Load Balancer (uncomment if needed)
# =============================================================================
# alb = aws.lb.LoadBalancer(
# f"{cfg.resource_prefix}-alb",
# load_balancer_type="application",
# security_groups=[app_sg.id],
# subnets=[public_subnet_1.id, public_subnet_2.id],
# tags=cfg.tags,
# )
# =============================================================================
# OUTPUTS
# =============================================================================
pulumi.export("ec2_public_ip", eip.public_ip)
pulumi.export("ec2_private_ip", ec2_instance.private_ip)
pulumi.export("db_endpoint", db_instance.endpoint)
pulumi.export("db_name", cfg.db_name)
pulumi.export("db_user", cfg.db_user)
pulumi.export("redis_endpoint", redis_cluster.cache_nodes[0].address)
pulumi.export("redis_port", redis_cluster.port)
# Generate .env content
pulumi.export("env_file", pulumi.Output.all(
db_instance.endpoint,
redis_cluster.cache_nodes[0].address,
redis_cluster.port,
).apply(lambda args: f"""
# Generated by Pulumi - AWS
DB_HOST={args[0].split(':')[0]}
DB_PORT=5432
DB_NAME={cfg.db_name}
DB_USER={cfg.db_user}
DB_PASSWORD=<set via pulumi config>
CELERY_BROKER_URL=redis://{args[1]}:{args[2]}/0
CELERY_RESULT_BACKEND=redis://{args[1]}:{args[2]}/0
"""))

View File

@@ -0,0 +1,2 @@
pulumi>=3.0.0
pulumi-aws>=6.0.0

View File

@@ -0,0 +1,6 @@
name: amar-digitalocean
runtime:
name: python
options:
virtualenv: venv
description: Amar Mascotas infrastructure on DigitalOcean

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

View File

@@ -0,0 +1,2 @@
pulumi>=3.0.0
pulumi-digitalocean>=4.0.0

View File

@@ -0,0 +1,6 @@
name: amar-gcp
runtime:
name: python
options:
virtualenv: venv
description: Amar Mascotas infrastructure on Google Cloud Platform

View File

@@ -0,0 +1,286 @@
"""
Google Cloud Platform Infrastructure for Amar Mascotas
Deploys:
- VPC with subnets
- Compute Engine instance for Django app + Celery
- Cloud SQL PostgreSQL (PostGIS via flags)
- Memorystore Redis
- Firewall rules
- (Optional) Cloud Load Balancer, Cloud DNS
Estimated cost: ~$93/month
NOTE: GCP has good free tier credits and competitive pricing.
PostGIS requires enabling the `cloudsql.enable_pgaudit` flag.
"""
import pulumi
import pulumi_gcp as gcp
import sys
sys.path.append("..")
from shared.config import get_config, APP_SERVER_INIT_SCRIPT
# Load configuration
cfg = get_config()
# Get project
project = gcp.organizations.get_project()
# =============================================================================
# NETWORKING - VPC
# =============================================================================
# VPC Network
vpc = gcp.compute.Network(
f"{cfg.resource_prefix}-vpc",
name=f"{cfg.resource_prefix}-vpc",
auto_create_subnetworks=False,
description="VPC for Amar Mascotas",
)
# Subnet for compute resources
subnet = gcp.compute.Subnetwork(
f"{cfg.resource_prefix}-subnet",
name=f"{cfg.resource_prefix}-subnet",
ip_cidr_range="10.0.1.0/24",
region="us-east1",
network=vpc.id,
private_ip_google_access=True, # Access Google APIs without public IP
)
# =============================================================================
# FIREWALL RULES
# =============================================================================
# Allow SSH
firewall_ssh = gcp.compute.Firewall(
f"{cfg.resource_prefix}-allow-ssh",
name=f"{cfg.resource_prefix}-allow-ssh",
network=vpc.name,
allows=[
gcp.compute.FirewallAllowArgs(
protocol="tcp",
ports=["22"],
),
],
source_ranges=cfg.allowed_ssh_ips or ["0.0.0.0/0"],
target_tags=["app-server"],
)
# Allow HTTP/HTTPS
firewall_http = gcp.compute.Firewall(
f"{cfg.resource_prefix}-allow-http",
name=f"{cfg.resource_prefix}-allow-http",
network=vpc.name,
allows=[
gcp.compute.FirewallAllowArgs(
protocol="tcp",
ports=["80", "443"],
),
],
source_ranges=["0.0.0.0/0"],
target_tags=["app-server"],
)
# Allow internal traffic (for DB/Redis access)
firewall_internal = gcp.compute.Firewall(
f"{cfg.resource_prefix}-allow-internal",
name=f"{cfg.resource_prefix}-allow-internal",
network=vpc.name,
allows=[
gcp.compute.FirewallAllowArgs(
protocol="tcp",
ports=["0-65535"],
),
gcp.compute.FirewallAllowArgs(
protocol="udp",
ports=["0-65535"],
),
gcp.compute.FirewallAllowArgs(
protocol="icmp",
),
],
source_ranges=["10.0.0.0/8"],
)
# =============================================================================
# DATABASE - Cloud SQL PostgreSQL
# =============================================================================
# Cloud SQL instance
# Note: PostGIS available via database flags
db_instance = gcp.sql.DatabaseInstance(
f"{cfg.resource_prefix}-db",
name=f"{cfg.resource_prefix}-db",
database_version="POSTGRES_15",
region="us-east1",
deletion_protection=False, # Set True for production!
settings=gcp.sql.DatabaseInstanceSettingsArgs(
tier="db-f1-micro", # $25/mo - smallest
disk_size=10,
disk_type="PD_SSD",
ip_configuration=gcp.sql.DatabaseInstanceSettingsIpConfigurationArgs(
ipv4_enabled=False,
private_network=vpc.id,
enable_private_path_for_google_cloud_services=True,
),
backup_configuration=gcp.sql.DatabaseInstanceSettingsBackupConfigurationArgs(
enabled=True,
start_time="03:00",
),
database_flags=[
# Enable PostGIS extensions
gcp.sql.DatabaseInstanceSettingsDatabaseFlagArgs(
name="cloudsql.enable_pg_cron",
value="on",
),
],
user_labels=cfg.tags,
),
opts=pulumi.ResourceOptions(depends_on=[vpc]),
)
# Database
db = gcp.sql.Database(
f"{cfg.resource_prefix}-database",
name=cfg.db_name,
instance=db_instance.name,
)
# Database user
db_user = gcp.sql.User(
f"{cfg.resource_prefix}-db-user",
name=cfg.db_user,
instance=db_instance.name,
password=pulumi.Config().require_secret("db_password"),
)
# Private IP for Cloud SQL
private_ip_address = gcp.compute.GlobalAddress(
f"{cfg.resource_prefix}-db-private-ip",
name=f"{cfg.resource_prefix}-db-private-ip",
purpose="VPC_PEERING",
address_type="INTERNAL",
prefix_length=16,
network=vpc.id,
)
# VPC peering for Cloud SQL
private_vpc_connection = gcp.servicenetworking.Connection(
f"{cfg.resource_prefix}-private-vpc-connection",
network=vpc.id,
service="servicenetworking.googleapis.com",
reserved_peering_ranges=[private_ip_address.name],
)
# =============================================================================
# CACHE - Memorystore Redis
# =============================================================================
redis_instance = gcp.redis.Instance(
f"{cfg.resource_prefix}-redis",
name=f"{cfg.resource_prefix}-redis",
tier="BASIC", # $20/mo - no HA
memory_size_gb=1,
region="us-east1",
redis_version="REDIS_7_0",
authorized_network=vpc.id,
connect_mode="PRIVATE_SERVICE_ACCESS",
labels=cfg.tags,
opts=pulumi.ResourceOptions(depends_on=[private_vpc_connection]),
)
# =============================================================================
# COMPUTE - Compute Engine Instance
# =============================================================================
# Service account for the instance
service_account = gcp.serviceaccount.Account(
f"{cfg.resource_prefix}-sa",
account_id=f"{cfg.resource_prefix}-app-sa",
display_name="Amar App Service Account",
)
# Compute instance
instance = gcp.compute.Instance(
f"{cfg.resource_prefix}-app",
name=f"{cfg.resource_prefix}-app",
machine_type="e2-medium", # $30/mo - 4GB RAM, 2 vCPU
zone="us-east1-b",
tags=["app-server"],
boot_disk=gcp.compute.InstanceBootDiskArgs(
initialize_params=gcp.compute.InstanceBootDiskInitializeParamsArgs(
image="ubuntu-os-cloud/ubuntu-2204-lts",
size=30,
type="pd-ssd",
),
),
network_interfaces=[
gcp.compute.InstanceNetworkInterfaceArgs(
network=vpc.id,
subnetwork=subnet.id,
access_configs=[
gcp.compute.InstanceNetworkInterfaceAccessConfigArgs(
# Ephemeral public IP
),
],
),
],
service_account=gcp.compute.InstanceServiceAccountArgs(
email=service_account.email,
scopes=["cloud-platform"],
),
metadata_startup_script=APP_SERVER_INIT_SCRIPT,
labels=cfg.tags,
)
# Static external IP (optional, costs extra)
static_ip = gcp.compute.Address(
f"{cfg.resource_prefix}-static-ip",
name=f"{cfg.resource_prefix}-static-ip",
region="us-east1",
)
# =============================================================================
# OPTIONAL: Cloud Load Balancer (uncomment if needed)
# =============================================================================
# health_check = gcp.compute.HealthCheck(
# f"{cfg.resource_prefix}-health-check",
# name=f"{cfg.resource_prefix}-health-check",
# http_health_check=gcp.compute.HealthCheckHttpHealthCheckArgs(
# port=80,
# request_path="/health/",
# ),
# )
# =============================================================================
# OUTPUTS
# =============================================================================
pulumi.export("instance_public_ip", instance.network_interfaces[0].access_configs[0].nat_ip)
pulumi.export("instance_private_ip", instance.network_interfaces[0].network_ip)
pulumi.export("static_ip", static_ip.address)
pulumi.export("db_private_ip", db_instance.private_ip_address)
pulumi.export("db_connection_name", db_instance.connection_name)
pulumi.export("db_name", cfg.db_name)
pulumi.export("db_user", cfg.db_user)
pulumi.export("redis_host", redis_instance.host)
pulumi.export("redis_port", redis_instance.port)
# Generate .env content
pulumi.export("env_file", pulumi.Output.all(
db_instance.private_ip_address,
redis_instance.host,
redis_instance.port,
).apply(lambda args: f"""
# Generated by Pulumi - GCP
DB_HOST={args[0]}
DB_PORT=5432
DB_NAME={cfg.db_name}
DB_USER={cfg.db_user}
DB_PASSWORD=<set via pulumi config>
CELERY_BROKER_URL=redis://{args[1]}:{args[2]}/0
CELERY_RESULT_BACKEND=redis://{args[1]}:{args[2]}/0
"""))

View File

@@ -0,0 +1,2 @@
pulumi>=3.0.0
pulumi-gcp>=7.0.0

View File

@@ -0,0 +1,4 @@
# Shared configuration module
from .config import get_config, AppConfig, APP_SERVER_INIT_SCRIPT
__all__ = ["get_config", "AppConfig", "APP_SERVER_INIT_SCRIPT"]

View File

@@ -0,0 +1,99 @@
"""
Shared configuration for all cloud deployments.
Centralizes app-specific settings that are cloud-agnostic.
"""
from dataclasses import dataclass
from typing import Optional
import pulumi
@dataclass
class AppConfig:
"""Application configuration shared across all cloud providers."""
# Naming
project_name: str = "amar"
environment: str = "production" # production, staging, dev
# Compute sizing
app_cpu: int = 2 # vCPUs
app_memory_gb: int = 4 # GB RAM
# Database
db_name: str = "amarback"
db_user: str = "amaruser"
db_version: str = "15" # PostgreSQL version
db_size_gb: int = 10 # Storage
# Redis
redis_version: str = "7"
redis_memory_mb: int = 1024
# Networking
allowed_ssh_ips: list = None # IPs allowed to SSH (None = your IP only)
domain: Optional[str] = "amarmascotas.ar"
def __post_init__(self):
if self.allowed_ssh_ips is None:
self.allowed_ssh_ips = []
@property
def resource_prefix(self) -> str:
"""Prefix for all resource names."""
return f"{self.project_name}-{self.environment}"
@property
def tags(self) -> dict:
"""Common tags for all resources."""
return {
"Project": self.project_name,
"Environment": self.environment,
"ManagedBy": "Pulumi",
}
def get_config() -> AppConfig:
"""Load configuration from Pulumi config or use defaults."""
config = pulumi.Config()
return AppConfig(
project_name=config.get("project_name") or "amar",
environment=config.get("environment") or "production",
app_memory_gb=config.get_int("app_memory_gb") or 4,
db_name=config.get("db_name") or "amarback",
db_user=config.get("db_user") or "amaruser",
domain=config.get("domain") or "amarmascotas.ar",
)
# Cloud-init script for app server setup
APP_SERVER_INIT_SCRIPT = """#!/bin/bash
set -e
# Update system
apt-get update
apt-get upgrade -y
# Install dependencies
apt-get install -y \\
python3-pip python3-venv \\
postgresql-client \\
gdal-bin libgdal-dev libgeos-dev libproj-dev \\
nginx certbot python3-certbot-nginx \\
supervisor \\
git
# Create app user
useradd -m -s /bin/bash amarapp || true
# Create directories
mkdir -p /var/www/amarmascotas/media
mkdir -p /var/etc/static
mkdir -p /home/amarapp/app
chown -R amarapp:amarapp /var/www/amarmascotas
chown -R amarapp:amarapp /var/etc/static
chown -R amarapp:amarapp /home/amarapp
echo "Base setup complete. Deploy application code separately."
"""