Database configuration#
Safir provides Pydantic models for configuring the database used by an application. Applications that use PostgreSQL with these models will be able to transparently support either an external PostgreSQL instance or a Cloud SQL instance. The engine management interface will use these models to connect to the external database using the appropriate protocol, and the rest of the application can ignore the underlying differences.
The simplest approach for most applications is to use these classes as part of the application configuration model, conventionally as a database attribute of the main application Config class.
config.database can then be passed into engine_manager, which will use those models to set up the database engine.
Using the configuration models#
The two models are DatabaseConfig, which is used for a conventional external database with an associated DSN and password, and CloudSqlConfig, which is used for a Cloud SQL PostgreSQL instance.
YAML-based configuration#
Applications that read their configuration from a YAML file (see Application configuration) will use a pattern such as the following:
from typing import Annotated
from pydantic import Field
from pydantic_settings import SettingsConfigDict
from safir.database import CloudSqlConfig, DatabaseConfig
from safir.pydantic import YamlBaseSettings
class Config(YamlBaseSettings):
model_config = SettingsConfigDict(
env_nested_delimiter="__", env_prefix="EXAMPLE_"
)
# ...
database: Annotated[
CloudSqlConfig | DatabaseConfig,
Field(title="Database configuration"),
]
Change EXAMPLE_ to something appropriate for your application.
Most fields will then be automatically parsed from the database key in the YAML file, which corresponds to the config.database key in the Helm values.yaml file.
When not using Cloud SQL, the database password will have to be injected via a secret.
For some applications, it may also be desirable to inject the database URL via an environment variable, such as when the application is configured to use Phalanx in-cluster PostgreSQL.
To do this, set the environment variables EXAMPLE_DATABASE__PASSWORD and EXAMPLE_DATABASE__URL (note the double underscores).
This will override anything set in the YAML file.
Environment-based configuration#
Configuration for applications that use environment variables for all settings instead of a YAML file is very similar:
from typing import Annotated
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from safir.database import CloudSqlConfig, DatabaseConfig
class Config(BaseSettings):
model_config = SettingsConfigDict(
env_nested_delimiter="__", env_prefix="EXAMPLE_"
)
# ...
database: Annotated[
CloudSqlConfig | DatabaseConfig,
Field(title="Database configuration"),
]
The only difference is using regular Pydantic BaseSettings as the base class for Config.
Change EXAMPLE_ to something appropriate for your application.
The database configuration can then be passed from the Helm values.yaml file in one of two ways:
Pass the entire
config.databaseportion ofvalues.yamlto the application in an environment variable, encoded in JSON. For this approach, put the following line in thedatasection of the applicationConfigMapwhere the other configuration environment variables are set:EXAMPLE_DATABASE: {{ toJson .Values.config.database | quote }}
The resulting JSON will then be parsed by Pydantic and interpreted as either
CloudSqlConfigorDatabaseConfig, depending on which schema it satisfies.Use individual environment variables to pass in all of the possible settings. This is more tedious, since you will have to replicate all the possible attributes for either
CloudSqlConfigorDatabaseConfig(including their shared base class) that you want to support in the Helmvalues.yamlfile in theConfigMap. The results will look something like:EXAMPLE_DATABASE__TYPE: {{ .Values.config.database.type | quote }} {{- if .Values.config.database.url }} EXAMPLE_DATABASE__URL: {{ .Values.config.database.url | quote }} {{- end }} {{- if eq .Values.config.database.type "cloudsql" }} EXAMPLE_DATABASE__PROJECT: {{ .Values.config.database.project | quote }} EXAMPLE_DATABASE__REGION: {{ .Values.config.database.region | quote }} EXAMPLE_DATABASE__INSTANCE: {{ .Values.config.database.database | quote }} EXAMPLE_DATABASE__DATABASE: {{ .Values.config.database.instance | quote }} EXAMPLE_DATABASE__SERVICE_ACCOUNT: {{ .Values.config.database.serviceAccount | quote }} {{- end }}
Any other supported settings will need their own lines. Note the double underscores.
If you use this approach, you will need to update
ConfigMapfor any additional settings that you want to support.
In both cases, change EXAMPLE_ as appropriate for your application.
Supported settings#
All of the example configurations in the following sections are excerpts from a hypothetical values.yaml file in Phalanx for an application.
External databases#
External databases must be configured with a URL (a DSN) and a password. Normally, the URL is provided directly in the application configuration and the password is injected via a secret. For example:
config:
database:
url: "postgresql://example@db01.example.com/example"
The password is then normally provided in the EXAMPLE_DATABASE__PASSWORD (note the double underscore) environment variable, set via a Kubernetes secret.
See Using the configuration models for more details.
Cloud SQL database#
To configure a Cloud SQL database backend, more parameters must be provided:
typeMust be set to
cloudsql. The default isexternal, which is used for an external database configured with a URL and password as above.projectThe Google Cloud Platform project in which the database is located.
regionThe Google Cloud Platform region in which the database is located.
instanceThe Cloud SQL database instance name.
databaseThe name of the Cloud SQL database to use within that instance.
serviceAccountThe Google Cloud Platform service account that will be used to access the database. This must be the service account to which the running application is mapped using Kubernetes workload identity. It must have full access, via IAM authentication, to read and write to the relevant database and to manage its schema.
The database configuration when the application is using Cloud SQL will look like this:
config:
database:
type: "cloudsql"
project: "project-7696"
region: "us-central1"
instance: "database-e9e11de2"
database: "example"
serviceAccount: "example@project-7696.iam.gserviceaccount.com"
Additional database settings#
Both CloudSqlConfig and DatabaseConfig support a variety of common database settings that an application may want to configure.
isolationLevelThe transaction isolation level to use for the database. This must be one of the isolation levels supported by the SQLAlchemy database driver in use.
maxOverflowMaximum connections above the pool size that can be created to handle surge traffic.
poolPrePingIf true, check that a connection is alive before returning it to a session. Applications with low traffic whose database connections often time out should consider setting this to true.
poolRecycleIf set, discard any idle connection that has been open for longer than this number of seconds. If the connection idle timeout for the database is known, set this value to something slightly shorter.
poolSizeThe database connection pool size; the number of connections to the database to keep open. Remember that if your application uses multiple replicas, this is the size of the connection pool for each replica, so the total number of open connections will be this number multiplied by the replica count.
poolTimeoutHow long to wait for a connection from the connection pool, in seconds.
All of these settings are optional. If not set, the SQLAlchemy default will be used.
Not all of these settings will make sense to document in the Helm chart for every application.
For example, if an application needs a custom isolationLevel, it will normally want to set that directly in the application, not require that it be set in a Helm chart.
The application can set the values of any parameter, either as a default or as the only supported setting, by subclassing the configuration models and setting that parameter.
For example:
from typing import Annotated
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from safir.database import CloudSqlConfig, DatabaseConfig
class ExampleCloudSqlConfig(CloudSqlConfig):
isolation_level: str = "REPEATABLE READ"
class ExampleDatabaseConfig(DatabaseConfig):
isolation_level: str = "REPEATABLE READ"
class Config(BaseSettings):
model_config = SettingsConfigDict(
env_nested_delimiter="__", env_prefix="EXAMPLE_"
)
# ...
database: Annotated[
ExampleCloudSqlConfig | ExampleDatabaseConfig,
Field(title="Database configuration"),
]
Technically, this sets a default that can still be overridden via a YAML configuration file or an environment variable. This generally does not matter in practice; if that setting is not documented, no one will have a reason to set it in the configuration in the Helm chart.