Skip to main content

Sample Common Module

Overview

Sample Common is a reference implementation of a *_Common module in the RAD Modules ecosystem. It deploys a minimal Flask web application backed by PostgreSQL to demonstrate the correct structure, patterns, and conventions that all *_Common modules follow.

Use this module as a starting point when building a new application module, or as a working example to understand how Layer 1 configuration modules integrate with App CloudRun and App GKE.

The module provisions one GCP Secret Manager secret (the Flask SECRET_KEY), defines no GCS buckets, and optionally enables a Redis sidecar for server-side session storage.


Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│ Sample_Common (Layer 1) │
│ │
│ Inputs: project_id, tenant_deployment_id, deployment_id, │
│ enable_redis, resource_prefix, ... │
│ │
│ ┌──────────────────────┐ ┌─────────────────────────────────────────┐ │
│ │ GCP Resources │ │ Config Output (consumed by Layer 2) │ │
│ │ │ │ │ │
│ │ Secret Manager API │ │ container_image: "" (derived from │ │
│ │ │ │ application_name by App_GKE/Run) │ │
│ │ secret-key │ │ container_port: 8080 │ │
│ │ (32-char, no │ │ secret_env_vars: {SECRET_KEY: ...} │ │
│ │ special chars) │ │ database_type: POSTGRES_15 │ │
│ │ 30s propagation │ │ initialization_jobs: [db-init] │ │
│ │ wait │ │ additional_services: [redis] (opt.) │ │
│ │ │ │ startup_probe: HTTP /healthz 10s │ │
│ │ storage_buckets: [] │ │ liveness_probe: HTTP /healthz 15s │ │
│ │ (no GCS buckets) │ │ │ │
│ └──────────────────────┘ └─────────────────────────────────────────┘ │
│ │
│ secret_prefix = var.resource_prefix OR │
│ "app{application_name}{tenant_deployment_id}{deployment_id}"│
└──────────────────────────────────────────────────────────────────────────────┘


App_CloudRun / App_GKE (Layer 2)

GCP Resources Created

ResourceName PatternDescription
google_project_servicesecretmanager.googleapis.comEnsures Secret Manager API is active
random_password32-char alphanumeric Flask secret key
google_secret_manager_secret{secret_prefix}-secret-keyStores Flask SECRET_KEY
google_secret_manager_secret_versionPopulates the secret key
time_sleep30s wait after secret creation for IAM propagation

secret_prefix resolution: When var.resource_prefix is set (e.g., passed from App GKE's own resource_prefix output), it is used directly. This aligns the secret name with GKE cluster and deployment resources. When empty, falls back to "app{application_name}{tenant_deployment_id}{deployment_id}".


Module Outputs

OutputTypeDescription
configobjectFull application configuration for App_CloudRun/App_GKE
storage_bucketslistAlways [] — no GCS buckets provisioned
secret_idsmap(string){ FLASK_SECRET_KEY: "<secret_id>" } — depends on 30s sleep
secret_valuesmap(string) (sensitive){ FLASK_SECRET_KEY: "<plaintext>" }
pathstringAbsolute path to this module directory

The SECRET_KEY is also wired directly into the config.secret_env_vars map, so App CloudRun/App GKE automatically mount it as a secret environment variable in the container without any extra caller configuration.


Input Variables

Identity & Project

VariableTypeDefaultDescription
project_idstringGCP project ID (required)
tenant_deployment_idstring"demo"Tenant identifier used in secret naming
deployment_idstring""Deployment identifier
resource_prefixstring""Override secret prefix (pass App GKE.resource_prefix to align names)
resource_labelsmap(string){}Labels applied to all GCP resources

Application

VariableTypeDefaultDescription
application_namestring"sample-app"Used in auto-generated secret prefix
application_versionstring"1.0.0"Application version tag
display_namestring"Sample Application"Human-readable display name
descriptionstring"Sample Custom Application - Flask App with Database Connection"Description
db_namestring"sampledb"PostgreSQL database name
db_userstring"sampleuser"PostgreSQL database user

Resources

VariableTypeDefaultDescription
cpu_limitstring"1000m"CPU limit
memory_limitstring"512Mi"Memory limit
min_instance_countnumber0Minimum instances
max_instance_countnumber1Maximum instances
environment_variablesmap(string){ FLASK_ENV = "production" }Container environment variables
initialization_jobslist(any)[]Override default jobs (empty = use default db-init)

Health Probes

VariableDefaultDescription
startup_probeHTTP GET /healthz, 10s delay, 5s timeout, 10s period, 3 failuresStartup readiness check
liveness_probeHTTP GET /healthz, 15s delay, 5s timeout, 30s period, 3 failuresOngoing liveness check

Redis

VariableTypeDefaultDescription
enable_redisboolfalseDeploys a redis:alpine sidecar service
redis_hoststring""Redis host for the Flask app
redis_portnumber6379Redis port

Initialization Job: db-init

PropertyValue
Imagepostgres:15-alpine
Scriptscripts/db-init.sh
execute_on_applytrue
max_retries3
Timeout600s

db-init.sh flow:

  1. Detects Cloud SQL Unix socket under /cloudsql, symlinks to /tmp/.s.PGSQL.5432, sets DB_HOST=/tmp
  2. Resolves target host (DB_IPDB_HOST)
  3. Waits for PostgreSQL with pg_isready
  4. Creates/updates user via a DO $$ PL/pgSQL block (idempotent — CREATE or ALTER)
  5. Grants DB_USER role to postgres (required for Cloud SQL where postgres is not a true superuser)
  6. Creates database with CREATE DATABASE … OWNER "$DB_USER" or updates owner if it already exists
  7. Grants all privileges on the database
  8. Signals Cloud SQL Auth Proxy shutdown via POST http://127.0.0.1:9091/quitquitquit (10 retries)

Redis Sidecar (additional_services)

When enable_redis = true, a Redis sidecar is added to the config.additional_services list:

{
name = "redis"
image = "redis:alpine"
port = var.redis_port # number, default 6379
cpu_limit = "1000m"
memory_limit = "512Mi"
min_instance_count = 0
max_instance_count = 1
ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"
}

The Flask app reads ENABLE_REDIS, REDIS_HOST, and REDIS_PORT from environment variables at startup. When ENABLE_REDIS=true but REDIS_HOST is empty, it logs a warning and falls back to cookie-based sessions.


Container Image

Built from scripts/Dockerfile using python:3.11-slim.

Base: python:3.11-slim

User: appuser (non-root, system user/group)

Dependencies (requirements.txt):
- Flask 3.0.0
- psycopg2-binary 2.9.9
- gunicorn 21.2.0
- SQLAlchemy 2.0.25
- redis
- Flask-Session

Files copied:
- requirements.txt
- app.py

CMD: exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
ENV: PORT=8080

No ENTRYPOINT is defined — the image uses CMD directly. No tini.


Flask Application (app.py)

A minimal working application demonstrating all integration patterns:

Database

  • Connects to PostgreSQL via DB_HOST, DB_NAME, DB_USER, DB_PASSWORD, DB_PORT
  • URL-encodes user and password with urllib.parse.quote_plus to handle special characters in SQLAlchemy connection strings
  • Supports both Unix socket (DB_HOST starts with /) and TCP connections:
    # Unix socket
    postgresql://user:pass@/dbname?host=/cloudsql/...
    # TCP
    postgresql://user:pass@host:5432/dbname
  • SQLAlchemy connection pool: pool_size=5, max_overflow=10
  • Visitor ORM model with a persistent counter table
  • init_db() runs at startup to create the table and seed an initial row

Routes

RouteMethodDescription
/GETIncrements the DB visitor counter; optionally tracks per-session visits via Redis
/healthzGETReturns {"status": "healthy"} — used by both startup and liveness probes
/dbGETExecutes SELECT version() and returns the PostgreSQL version

Sessions

  • When ENABLE_REDIS=true and REDIS_HOST is set: uses Flask-Session with Redis backend (SESSION_TYPE=redis, signed sessions via SECRET_KEY)
  • Otherwise: falls back to Flask's default cookie-based sessions

Platform-Specific Differences

AspectSample CloudRunSample GKE
service_urlComputed Cloud Run service URLEmpty string (not known at plan time)
resource_prefixAuto-computed from app/tenant/deploymentExplicitly set (typically App GKE.resource_prefix)
min_instance_count0 (scale-to-zero)1 (minimum pod availability)
DB_HOSTCloud SQL Auth Proxy socket pathCloud SQL private IP
Redis hostExplicit redis_host requiredDefaults to 127.0.0.1 if enabled
Secret injectionmodule.sample_common.secret_ids mapSecret values injected directly
NFSNot usedNot used

Usage Example

module "sample_common" {
source = "./modules/Sample_Common"

project_id = var.project_id
tenant_deployment_id = "prod"
deployment_id = random_id.deployment.hex

enable_redis = true
redis_host = "localhost" # or set by App_CloudRun sidecar
}

module "sample_cloudrun" {
source = "./modules/App_CloudRun"

config = module.sample_common.config
storage_buckets = module.sample_common.storage_buckets
# SECRET_KEY is already wired via config.secret_env_vars — no extra config needed
}

Aligning Secret Names with App GKE

When deploying on GKE, pass App GKE's resource_prefix output so the secret name matches all other cluster resources:

module "sample_common" {
source = "./modules/Sample_Common"
project_id = var.project_id
resource_prefix = module.app_gke.resource_prefix
# ...
}