""" 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= CELERY_BROKER_URL=redis://{args[1]}:{args[2]}/0 CELERY_RESULT_BACKEND=redis://{args[1]}:{args[2]}/0 """))