soleprint init commit
This commit is contained in:
163
station/tools/infra/README.md
Normal file
163
station/tools/infra/README.md
Normal 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.
|
||||
6
station/tools/infra/aws/Pulumi.yaml
Normal file
6
station/tools/infra/aws/Pulumi.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: amar-aws
|
||||
runtime:
|
||||
name: python
|
||||
options:
|
||||
virtualenv: venv
|
||||
description: Amar Mascotas infrastructure on AWS
|
||||
341
station/tools/infra/aws/__main__.py
Normal file
341
station/tools/infra/aws/__main__.py
Normal 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
|
||||
"""))
|
||||
2
station/tools/infra/aws/requirements.txt
Normal file
2
station/tools/infra/aws/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pulumi>=3.0.0
|
||||
pulumi-aws>=6.0.0
|
||||
6
station/tools/infra/digitalocean/Pulumi.yaml
Normal file
6
station/tools/infra/digitalocean/Pulumi.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: amar-digitalocean
|
||||
runtime:
|
||||
name: python
|
||||
options:
|
||||
virtualenv: venv
|
||||
description: Amar Mascotas infrastructure on DigitalOcean
|
||||
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]}
|
||||
"""))
|
||||
2
station/tools/infra/digitalocean/requirements.txt
Normal file
2
station/tools/infra/digitalocean/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pulumi>=3.0.0
|
||||
pulumi-digitalocean>=4.0.0
|
||||
6
station/tools/infra/gcp/Pulumi.yaml
Normal file
6
station/tools/infra/gcp/Pulumi.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: amar-gcp
|
||||
runtime:
|
||||
name: python
|
||||
options:
|
||||
virtualenv: venv
|
||||
description: Amar Mascotas infrastructure on Google Cloud Platform
|
||||
286
station/tools/infra/gcp/__main__.py
Normal file
286
station/tools/infra/gcp/__main__.py
Normal 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
|
||||
"""))
|
||||
2
station/tools/infra/gcp/requirements.txt
Normal file
2
station/tools/infra/gcp/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pulumi>=3.0.0
|
||||
pulumi-gcp>=7.0.0
|
||||
4
station/tools/infra/shared/__init__.py
Normal file
4
station/tools/infra/shared/__init__.py
Normal 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"]
|
||||
99
station/tools/infra/shared/config.py
Normal file
99
station/tools/infra/shared/config.py
Normal 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."
|
||||
"""
|
||||
Reference in New Issue
Block a user