Skip to main content

Certification track: Associate Cloud Engineer (ACE)

InvoiceNinja Common Shared Configuration Module

The InvoiceNinja Common module defines the Invoice Ninja application configuration for the RAD Modules ecosystem. It is a pure configuration module — it creates no GCP resources directly and produces a config output consumed by platform-specific wrapper modules (InvoiceNinja CloudRun and InvoiceNinja GKE).

1. Overview

Purpose: To centralise all Invoice Ninja-specific configuration (container image, MySQL 8.0 database setup, environment variable mapping, health probes, storage bucket, auto-generated secrets, and initialisation job definitions) in a single module shared by both Cloud Run and GKE deployments.

Architecture:

Layer 3: Application Wrappers
├── InvoiceNinja_CloudRun ──┐
└── InvoiceNinja_GKE ──┤── instantiate InvoiceNinja_Common

InvoiceNinja_Common (this module)
Creates: APP_KEY secret in Secret Manager
Produces: config, secret_ids, storage_buckets, path

Layer 2: Platform Modules
├── App_CloudRun (serverless deployment)
└── App_GKE (Kubernetes deployment)

Layer 1: App_Common (networking, database, storage, secrets, IAM)

Key characteristics:

  • One of the few modules in the ecosystem that uses MySQL 8.0 instead of PostgreSQL. Invoice Ninja's Laravel application only supports MySQL.
  • Auto-generates the APP_KEY secret — a base64-encoded 32-byte random Laravel encryption key, stored in Secret Manager as base64:<value>. This is generated once on first apply and is not regenerated on subsequent applies.
  • Defines two initialisation jobs (db-init and artisan-migrate) that run sequentially on deployment.
  • Configures snappdf PDF generation with Chromium bundled in the invoiceninja/invoiceninja:5 container.
  • Injects TRUSTED_PROXIES=* to correctly handle Cloud Run and GKE reverse proxy headers in Laravel.

2. Outputs

config

The application configuration object passed to the platform module via application_config.

FieldValue / Description
app_name"invoiceninja"
application_versionVersion tag (default: "5")
container_image"invoiceninja/invoiceninja:<application_version>" — the public Docker Hub image, used as the Dockerfile FROM base for the Common module's own custom build
image_source"custom" in InvoiceNinja_Common's own config (a thin Cloud Build wrapper adding nginx + a Cloud SQL Unix-socket patch to config/database.php — see container_build_config below). Both InvoiceNinja_CloudRun and InvoiceNinja_GKE default their own container_image_source variable to "prebuilt", which overrides this in the final merged config unless the wrapper caller explicitly passes "custom"
container_build_config{ enabled = true, dockerfile_path = "Dockerfile", context_path = "<module>/scripts", build_args = { APP_VERSION = application_version } } — only takes effect when the wrapper's image_source resolves to "custom"
container_port80 — Invoice Ninja uses nginx on port 80
database_type"MYSQL_8_0" — Invoice Ninja requires MySQL 8.0+
db_nameDatabase name (default: "invoiceninja")
db_userDatabase user (default: "invoiceninja")
enable_cloudsql_volumeWhether to mount the Cloud SQL Auth Proxy sidecar (default: true)
cloudsql_volume_mount_path"/cloudsql"
gcs_volumesList of GCS Fuse volume mounts (empty by default)
container_resourcesCPU: 2000m, Memory: 2Gi — Chromium PDF generation requires significant resources
environment_variablesPassed through from var.environment_variables merged with Invoice Ninja-specific defaults (see §4)
secret_environment_variablesContains APP_KEY reference plus any additional secrets from var.secret_environment_variables
initialization_jobsDefault db-init + artisan-migrate jobs or custom override (see §5)
startup_probeHTTP GET /, 90s initial delay, 10s timeout, 15s period, 30 failure threshold
liveness_probeHTTP GET /, 120s initial delay, 10s timeout, 30s period, 3 failure threshold

secret_ids

A map containing the Secret Manager secret IDs for secrets created by this module:

KeyDescription
APP_KEYLaravel application encryption key. Secret ID in the format projects/PROJECT_ID/secrets/APP_KEY_SECRET_NAME.

Note: Unlike most modules where secret_ids is passed through via module_secret_env_vars in the wrapper's main.tf, the APP_KEY for Invoice Ninja is wired directly inside InvoiceNinja Common into the config.secret_environment_variables field. The wrapper does not need to separately handle this secret.

storage_buckets

A list of GCS bucket configurations for provisioning by the platform module:

FieldValue
name_suffix"storage"
locationDeployment region
storage_class"STANDARD"
versioning_enabledfalse
lifecycle_rules[]
public_access_prevention"enforced"

path

The absolute path to the module directory, used by wrapper modules to locate the scripts/ directory.


3. Input Variables

Application

VariableTypeDefaultDescription
project_idstringGCP project ID. Required for Secret Manager resource creation.
resource_prefixstringPrefix used for naming Secret Manager resources.
application_namestring"invoiceninja"Application name. Used as base for resource naming.
display_namestring"Invoice Ninja"Human-readable application name.
descriptionstring"Invoice Ninja open-source invoicing platform"Description passed to init job definitions.
application_versionstring"5"Invoice Ninja Docker image tag. Increment to deploy a new release.
tenant_deployment_idstring"demo"Deployment identifier appended to resource names.
regionstring"us-central1"GCP region for the storage bucket location.
db_namestring"invoiceninja"MySQL database name. Do not change after initial deployment.
db_userstring"invoiceninja"MySQL application user.
cpu_limitstring"2000m"Container CPU limit passed into config.container_resources.
memory_limitstring"2Gi"Container memory limit. Minimum 2 Gi for Chromium PDF generation.
min_instance_countnumber1Minimum instance count, passed into config.min_instance_count.
max_instance_countnumber3Maximum instance count, passed into config.max_instance_count.
environment_variablesmap(string){}Additional plain-text environment variables. Merged with Invoice Ninja defaults.
secret_environment_variablesmap(string){}Additional Secret Manager references. Merged with the auto-generated APP_KEY reference.
initialization_jobslist(object)[]Custom init jobs. Empty triggers the default db-init + artisan-migrate pair.
startup_probeobjectsee §6Startup health probe configuration.
liveness_probeobjectsee §6Liveness health probe configuration.
invoiceninja_admin_emailstring"admin@example.com"Declared and forwarded by both wrappers, but not currently referenced in InvoiceNinja_Common's local.config — has no effect on the deployed environment. Invoice Ninja's first admin account is created through its own in-app /setup wizard instead.
mail_from_namestring"Invoice Ninja"Display name for outgoing emails.
mail_from_addressstring"ninja@example.com"Sender email address for outgoing emails.

Storage & Volumes

VariableTypeDefaultDescription
enable_cloudsql_volumebooltrueMount Cloud SQL Auth Proxy sidecar socket into the container.
gcs_volumeslist(object)[]GCS Fuse volume mounts: name, bucket_name, mount_path, readonly, mount_options.
labelsmap(string){}Labels to apply to all created resources.

4. Auto-Injected Environment Variables

InvoiceNinja Common injects the following environment variables into the application config automatically. They are merged with lower precedence than var.environment_variables, so a wrapper caller (or an end user's environment_variables input) can override any of them by supplying the same key.

VariableValuePurpose
APP_ENV"production"Laravel environment mode.
APP_DEBUG"false"Disables Laravel debug output in production.
DB_CONNECTION"mysql"Laravel database driver selection.
MAIL_MAILER"smtp"Selects the SMTP mail transport.
TRUSTED_PROXIES"*"Required for Cloud Run and GKE load balancer reverse proxy headers (X-Forwarded-For, X-Forwarded-Proto). Without this, Laravel generates HTTP links even when the client accesses via HTTPS.
PDF_GENERATOR"snappdf"Selects the snappdf Chromium-based PDF renderer.
SNAPPDF_EXECUTABLE_PATH"/usr/local/bin/chrome"Path to the bundled Chromium executable inside the invoiceninja/invoiceninja:5 container.
IN_USER_AGENT_SETTING"1"Invoice Ninja app setting passed through as an env var.
MAIL_FROM_NAMEvar.mail_from_nameDisplay name for outgoing Invoice Ninja emails.
MAIL_FROM_ADDRESSvar.mail_from_addressSender email address for outgoing emails.

The APP_KEY secret reference is injected via secret_environment_variables (not environment_variables) — it is resolved at runtime by Cloud Run or Kubernetes from Secret Manager.


5. Initialization Jobs

Two jobs are provisioned by default when initialization_jobs = []:

Job 1: db-init

FieldValue
Imagemysql:8.0-debian
Scriptscripts/db-init.sh
Secrets requiredROOT_PASSWORD (MySQL root), DB_PASSWORD (app user)
execute_on_applytrue
Timeout600s, 1 retry
CPU / Memory1000m / 512Mi

db-init.sh behaviour:

  1. Connects to Cloud SQL MySQL over whichever transport DB_HOST resolves to: a Unix socket path (Cloud Run, enable_cloudsql_volume = true) or TCP via the Cloud SQL Auth Proxy sidecar (GKE, falls back to DB_IP if DB_HOST is unset).
  2. Polls MySQL until available (up to 30 retries, 2s apart).
  3. Creates the invoiceninja database with utf8mb4 charset and utf8mb4_0900_ai_ci collation (required for MySQL 8.0 compatibility with Laravel).
  4. Creates (or updates) the invoiceninja user with a plain IDENTIFIED BY clause, which uses the server's default auth plugin (caching_sha2_password on MySQL 8.4+, mysql_native_password on 8.0). The script deliberately avoids pinning mysql_native_password explicitly, since that plugin was removed in MySQL 8.4 (ERROR 4052).
  5. Grants ALL PRIVILEGES on the invoiceninja database.
  6. Verifies the application user can connect.
  7. Signals Cloud SQL Proxy shutdown.

Job 2: artisan-migrate

FieldValue
Imagenull — defaults to the application's own container image (the Common config's container_image/build output)
Scriptscripts/migrate.sh, which runs php artisan migrate --force
execute_on_applytrue
depends_on_jobs["db-init"]
Timeout600s, 1 retry
CPU / Memory1000m / 1Gi

artisan-migrate runs Laravel's database migration system. On first deployment it creates all Invoice Ninja tables. On subsequent deployments it applies any new migrations introduced by Invoice Ninja version upgrades. The --force flag suppresses the interactive confirmation prompt in production mode. There is no --seed flag — the job does not seed demo/reference data; Invoice Ninja's own first-run /setup wizard handles initial account creation.

Override initialization_jobs with a non-empty list to replace both default jobs with custom jobs. When initialization_jobs is non-empty, InvoiceNinja Common does not inject either default job.


6. Health Probes

All probes target GET / (the Invoice Ninja login page or dashboard, which returns HTTP 200 when the application is fully ready). Invoice Ninja does not expose a dedicated /healthz endpoint.

ProbeInitial DelayTimeoutPeriodFailure ThresholdPurpose
Startup90s10s15s30Allows up to 540s total for Invoice Ninja to complete PHP-FPM initialisation, configuration caching, and first-boot database migrations
Liveness120s10s30s3Restarts the container if Invoice Ninja becomes unresponsive after a full start sequence

The generous startup probe thresholds accommodate Invoice Ninja's artisan-migrate process, which runs synchronously on the first boot when APP_ENV=production and no existing schema is detected.

Compared to App CloudRun/App GKE defaults:

FieldApp CloudRunInvoiceNinja CommonReason
path/healthz/Invoice Ninja has no /healthz endpoint.
Startup initial_delay_seconds1090PHP-FPM + Laravel bootstrap + optional migration takes 30–90 seconds.
Startup failure_threshold33030 × 15s = 450s of additional tolerance after the 90s delay.
Liveness initial_delay_seconds15120Prevents premature liveness failures before the startup sequence is complete.

7. APP_KEY Secret Management

The APP_KEY is Laravel's application encryption key. It is used to encrypt cookies, session data, and other sensitive values. All encrypted data becomes unreadable if the key is changed or lost.

Generation: InvoiceNinja Common generates a 32-byte cryptographically random value, base64-encodes it, and stores it in Secret Manager as base64:<value>. This format is required by Laravel's Crypt facade.

Injection: The APP_KEY secret is referenced in config.secret_environment_variables as:

{ APP_KEY = "<secret-manager-secret-id>" }

Cloud Run and GKE resolve this reference at container start, injecting the plaintext value as an environment variable. The plaintext is never written to state.

Rotation: The APP_KEY is a one-time generated secret — it is created on the first apply and never rotated automatically. Changing the APP_KEY after initial deployment invalidates all existing user sessions, encrypted cookies, and any data encrypted with the old key. Do not rotate this key unless you have a migration plan for re-encrypting existing data.


8. Platform-Specific Differences

AspectInvoiceNinja CloudRunInvoiceNinja GKE
container_image_source"prebuilt" default"prebuilt" default
min_instance_count1 (configurable)1 (configurable)
max_instance_count3 (configurable)5 (configurable)
enable_cloudsql_volumeOptional (default true)Optional (default true)
DB_HOST at runtimeCloud SQL Auth Proxy socket path under /cloudsql127.0.0.1 — the GKE pod's Cloud SQL Auth Proxy sidecar listens on loopback
Redis variablesGroup 21Group 15
Session affinityNot applicable (Cloud Run manages routing)"ClientIP" default — prevents admin session drops across pod replicas
NFSEnabled by default (enable_nfs = true)Enabled by default (enable_nfs = true)

9. Implementation Pattern

# Example: how InvoiceNinja_CloudRun instantiates InvoiceNinja_Common

module "invoiceninja_app" {
source = "../InvoiceNinja_Common"

project_id = var.project_id
resource_prefix = local.resource_prefix
application_version = var.application_version
db_name = var.application_database_name
db_user = var.application_database_user
cpu_limit = var.cpu_limit
memory_limit = var.memory_limit
description = var.application_description
startup_probe = var.startup_probe
liveness_probe = var.liveness_probe
enable_cloudsql_volume = var.enable_cloudsql_volume
invoiceninja_admin_email = var.invoiceninja_admin_email
mail_from_name = var.mail_from_name
mail_from_address = var.mail_from_address
environment_variables = var.environment_variables
}

locals {
application_modules = { invoiceninja = module.invoiceninja_app.config }
module_secret_env_vars = module.invoiceninja_app.secret_ids
module_storage_buckets = module.invoiceninja_app.storage_buckets
scripts_dir = abspath("${module.invoiceninja_app.path}/scripts")
}

module "app_cloudrun" {
source = "../App_CloudRun"

application_config = local.application_modules
module_secret_env_vars = local.module_secret_env_vars
module_storage_buckets = local.module_storage_buckets
scripts_dir = local.scripts_dir
# ... other inputs
}