Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 51 additions & 13 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,33 @@ jobs:

echo "VPC CIDRs and Firewall rules configured for ${ENV}"

- name: Get dev database info for preview
if: startsWith(needs.setup.outputs.environment, 'preview-')
id: dev_db
working-directory: ./terraform
run: |
# Save current backend config
PREVIEW_ENV="${{ needs.setup.outputs.environment }}"

# Temporarily init with dev state to get DB info
terraform init \
-backend-config="prefix=terraform/state/dev" \
-reconfigure \
-input=false

# Get dev database outputs
DB_IP=$(terraform output -raw database_private_ip 2>/dev/null || echo "")
DB_CONNECTION=$(terraform output -raw database_connection_name 2>/dev/null || echo "")

echo "database_ip=${DB_IP}" >> $GITHUB_OUTPUT
echo "database_connection=${DB_CONNECTION}" >> $GITHUB_OUTPUT

# Re-init with preview state
terraform init \
-backend-config="prefix=terraform/state/${PREVIEW_ENV}" \
-reconfigure \
-input=false

- name: Terraform Plan
working-directory: ./terraform
run: |
Expand All @@ -271,19 +298,30 @@ jobs:
ENABLE_DOMAIN="true"
fi

terraform plan \
-var="environment=${{ needs.setup.outputs.environment }}" \
-var="browser_image_url=${{ needs.build.outputs.browser_image }}" \
-var="mastra_image_url=${{ needs.build.outputs.mastra_image }}" \
-var="chatbot_image_url=${{ needs.build.outputs.chatbot_image }}" \
-var="browser_ws_proxy_image_url=${{ needs.build.outputs.browser_ws_proxy_image }}" \
-var="enable_custom_domain=${ENABLE_DOMAIN}" \
-var="vpc_cidr_public=${{ steps.vpc_cidrs.outputs.vpc_cidr_public }}" \
-var="vpc_cidr_private=${{ steps.vpc_cidrs.outputs.vpc_cidr_private }}" \
-var="vpc_cidr_db=${{ steps.vpc_cidrs.outputs.vpc_cidr_db }}" \
-var="vpc_connector_cidr=${{ steps.vpc_cidrs.outputs.vpc_connector_cidr }}" \
-var='firewall_rules=${{ steps.vpc_cidrs.outputs.firewall_rules }}' \
-out=tfplan
# Build terraform plan command
PLAN_ARGS=(
-var="environment=${{ needs.setup.outputs.environment }}"
-var="browser_image_url=${{ needs.build.outputs.browser_image }}"
-var="mastra_image_url=${{ needs.build.outputs.mastra_image }}"
-var="chatbot_image_url=${{ needs.build.outputs.chatbot_image }}"
-var="browser_ws_proxy_image_url=${{ needs.build.outputs.browser_ws_proxy_image }}"
-var="enable_custom_domain=${ENABLE_DOMAIN}"
-var="vpc_cidr_public=${{ steps.vpc_cidrs.outputs.vpc_cidr_public }}"
-var="vpc_cidr_private=${{ steps.vpc_cidrs.outputs.vpc_cidr_private }}"
-var="vpc_cidr_db=${{ steps.vpc_cidrs.outputs.vpc_cidr_db }}"
-var="vpc_connector_cidr=${{ steps.vpc_cidrs.outputs.vpc_connector_cidr }}"
-var='firewall_rules=${{ steps.vpc_cidrs.outputs.firewall_rules }}'
)

# Add dev database info for preview environments
if [[ "${{ needs.setup.outputs.environment }}" == preview-* ]]; then
PLAN_ARGS+=(
-var="dev_database_private_ip=${{ steps.dev_db.outputs.database_ip }}"
-var="dev_database_connection_name=${{ steps.dev_db.outputs.database_connection }}"
)
fi

terraform plan "${PLAN_ARGS[@]}" -out=tfplan

- name: Terraform Apply
if: needs.setup.outputs.should_deploy == 'true'
Expand Down
219 changes: 219 additions & 0 deletions terraform/cloud_sql.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Cloud SQL Database Configuration
#
# Strategy:
# - Dev: Creates and manages app-dev Cloud SQL instance
# - Preview: Does NOT create database, uses VPC peering to connect to dev database
# - Prod: Creates and manages app-prod Cloud SQL instance (completely isolated from dev/preview)

# Cloud SQL Instance for DEV environment
resource "google_sql_database_instance" "dev" {
count = var.environment == "dev" ? 1 : 0
name = "app-dev"
database_version = "POSTGRES_15"
region = local.region

settings {
tier = "db-custom-2-3840" # Dev: 2 vCPUs, 3.75GB RAM
availability_type = "ZONAL"
deletion_protection_enabled = false # Allow deletion for dev environment

# Storage configuration
disk_type = "PD_SSD"
disk_size = 50 # 50GB storage for dev
disk_autoresize = true # Enable autoresize

# Backup configuration
backup_configuration {
enabled = true
start_time = "03:00"
point_in_time_recovery_enabled = true
transaction_log_retention_days = 7
backup_retention_settings {
retained_backups = 7
retention_unit = "COUNT"
}
}

# IP configuration - Private IP only via VPC peering
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
enable_private_path_for_google_cloud_services = true
}

# Database flags
database_flags {
name = "max_connections"
value = "100" # Appropriate for dev tier
}
}

depends_on = [
google_service_networking_connection.private_vpc_connection,
google_compute_network.main
]

deletion_protection = false
}

# Cloud SQL Database for DEV
resource "google_sql_database" "dev" {
count = var.environment == "dev" ? 1 : 0
name = "app_db"
instance = google_sql_database_instance.dev[0].name
}

# Generate random password for DEV database
resource "random_password" "dev_password" {
count = var.environment == "dev" ? 1 : 0
length = 32
special = true
upper = true
lower = true
numeric = true
}

# Create secret in Secret Manager for DEV database password
resource "google_secret_manager_secret" "database_password_dev" {
count = var.environment == "dev" ? 1 : 0
secret_id = "database-password-dev"
project = local.project_id

replication {
user_managed {
replicas {
location = local.region
}
}
}

labels = merge(local.common_labels, {
environment = "dev"
purpose = "database-password"
})
}

# Store password in Secret Manager
resource "google_secret_manager_secret_version" "database_password_dev" {
count = var.environment == "dev" ? 1 : 0
secret = google_secret_manager_secret.database_password_dev[0].id
secret_data = random_password.dev_password[0].result
}

# Cloud SQL User for DEV
resource "google_sql_user" "dev" {
count = var.environment == "dev" ? 1 : 0
name = "app_user"
instance = google_sql_database_instance.dev[0].name
password = random_password.dev_password[0].result
type = "BUILT_IN"
}


# Cloud SQL Instance for PROD environment (completely isolated)
resource "google_sql_database_instance" "prod" {
count = var.environment == "prod" ? 1 : 0
name = "app-prod"
database_version = "POSTGRES_15"
region = local.region

settings {
tier = "db-custom-4-7680" # Prod: 4 vCPUs, 7.5GB RAM
availability_type = "ZONAL"
deletion_protection_enabled = true # Protect production database

# Storage configuration
disk_type = "PD_SSD"
disk_size = 100 # 100GB storage for prod
disk_autoresize = true # Enable autoresize

# Backup configuration
backup_configuration {
enabled = true
start_time = "03:00"
point_in_time_recovery_enabled = true
transaction_log_retention_days = 7
backup_retention_settings {
retained_backups = 7
retention_unit = "COUNT"
}
}

# IP configuration - Private IP only via VPC peering
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
enable_private_path_for_google_cloud_services = true
}

# Database flags
database_flags {
name = "max_connections"
value = "200" # Higher for production tier
}
}

depends_on = [
google_service_networking_connection.private_vpc_connection,
google_compute_network.main
]

deletion_protection = true # Critical: Protect production database
}

# Cloud SQL Database for PROD
resource "google_sql_database" "prod" {
count = var.environment == "prod" ? 1 : 0
name = "app_db"
instance = google_sql_database_instance.prod[0].name
}

# Generate random password for PROD database
resource "random_password" "prod_password" {
count = var.environment == "prod" ? 1 : 0
length = 32
special = true
upper = true
lower = true
numeric = true
}

# Create secret in Secret Manager for PROD database password
resource "google_secret_manager_secret" "database_password_prod" {
count = var.environment == "prod" ? 1 : 0
secret_id = "database-password-prod"
project = local.project_id

replication {
user_managed {
replicas {
location = local.region
}
}
}

labels = merge(local.common_labels, {
environment = "prod"
purpose = "database-password"
})
}

# Store password in Secret Manager
resource "google_secret_manager_secret_version" "database_password_prod" {
count = var.environment == "prod" ? 1 : 0
secret = google_secret_manager_secret.database_password_prod[0].id
secret_data = random_password.prod_password[0].result
}

# Cloud SQL User for PROD
resource "google_sql_user" "prod" {
count = var.environment == "prod" ? 1 : 0
name = "app_user"
instance = google_sql_database_instance.prod[0].name
password = random_password.prod_password[0].result
type = "BUILT_IN"
}

# Note: VPC Peering configuration has been moved to vpc.tf for better organization
# See vpc.tf for preview-to-dev and dev-to-preview peering resources

4 changes: 4 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ terraform {
source = "hashicorp/google-beta"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.1"
}
}

# Use existing GCS backend for state storage
Expand Down
44 changes: 44 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ output "vpc_connector_cidr" {
value = var.vpc_connector_cidr
}

# Database outputs
output "database_instance_name" {
description = "Cloud SQL instance name"
value = var.environment == "dev" ? (
length(google_sql_database_instance.dev) > 0 ? google_sql_database_instance.dev[0].name : null
) : (
var.environment == "prod" ? (
length(google_sql_database_instance.prod) > 0 ? google_sql_database_instance.prod[0].name : null
) : null
)
}

output "database_connection_name" {
description = "Cloud SQL connection name (for Cloud SQL Proxy)"
value = var.environment == "dev" ? (
length(google_sql_database_instance.dev) > 0 ? google_sql_database_instance.dev[0].connection_name : null
) : (
var.environment == "prod" ? (
length(google_sql_database_instance.prod) > 0 ? google_sql_database_instance.prod[0].connection_name : null
) : null
)
}

output "database_private_ip" {
description = "Cloud SQL private IP address"
value = var.environment == "dev" ? (
length(google_sql_database_instance.dev) > 0 ? google_sql_database_instance.dev[0].private_ip_address : null
) : (
var.environment == "prod" ? (
length(google_sql_database_instance.prod) > 0 ? google_sql_database_instance.prod[0].private_ip_address : null
) : null
)
}

output "database_name" {
description = "Database name"
value = "app_db"
}

output "database_user" {
description = "Database username"
value = "app_user"
}

# Architecture summary
output "architecture_summary" {
description = "Summary of the deployed architecture"
Expand Down
18 changes: 18 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,22 @@ variable "allow_public_access" {
description = "DEPRECATED: Use firewall_rules instead. Allow public access (0.0.0.0/0) to application services."
type = bool
default = true
}

# Database passwords are stored in Secret Manager:
# - database-password-dev (for dev environment)
# - database-password-prod (for prod environment)
# No variable needed - passwords are retrieved from Secret Manager

# Database connection info for preview environments (passed from dev state)
variable "dev_database_private_ip" {
description = "Private IP of the dev Cloud SQL instance (for preview environments to connect)"
type = string
default = ""
}

variable "dev_database_connection_name" {
description = "Connection name of the dev Cloud SQL instance (for Cloud SQL Proxy)"
type = string
default = ""
}