Deploy MySQL with Terraform
Provision managed MySQL on Bahriya with Terraform: single, replicated or highly available tiers, plans, databases and users, encryption, external access and phpMyAdmin.
Bahriya provides managed MySQL instances — a relational database that runs alongside your containers, with one or more logical databases inside, each owned by its own user. This guide covers the bahriya_mysql resource. For the concepts behind it, see the MySQL overview.
Required fields
| Field | Type | Example | Description |
|---|---|---|---|
handle | string | "orders-db" | Permanent identifier. Cannot be reused after deletion. |
name | string | "Orders DB" | Display name. |
tier | string | "single" | single, replicated or ha. Changing it forces recreation. |
cpumillicores | number | 1000 | CPU per node in millicores. Chosen as a pair with memorymb. |
memorymb | number | 2048 | Memory per node in MB. Chosen as a pair with cpumillicores. |
storagegb | number | 20 | Data storage per node in GB, 5 to 250. Grow-only. |
databases | list of objects | [{ name = "orders", username = "orders-app" }] | At least one database, each with its user. |
activeregions | list | ["falkenstein-1"] | One or more regions to deploy to. |
project | string | bahriya_project.x.id | The project UUID (not handle). |
cpumillicores and memorymb must be one of the published plans: 500/1024, 1000/2048, 2000/4096, 2000/8192, 4000/8192, 4000/16384, 8000/16384 or 8000/32768. Any other pair is rejected.
Minimal example
resource "bahriya_project" "app" {
handle = "my-app"
name = "My Application"
regions = ["falkenstein-1"]
}
resource "bahriya_mysql" "orders" {
handle = "orders-db"
name = "Orders DB"
tier = "single"
cpumillicores = 1000
memorymb = 2048
storagegb = 20
activeregions = ["falkenstein-1"]
project = bahriya_project.app.id
databases = [
{ name = "orders", username = "orders-app" },
]
}databases is an attributes list — write databases = [{ ... }], not block syntax.
Read replicas and tuning
The replicated tier runs a primary plus one or two asynchronous read replicas (replicas = 1 or 2). config takes the supported mysqld settings, with values as strings.
resource "bahriya_mysql" "shop" {
handle = "shop-db"
name = "Shop DB"
tier = "replicated"
replicas = 2
cpumillicores = 2000
memorymb = 8192
storagegb = 100
activeregions = ["falkenstein-1"]
project = bahriya_project.app.id
databases = [
{ name = "catalog", username = "catalog-app" },
{ name = "reporting", username = "reporting-ro" },
]
config = {
"max_connections" = "500"
"slow_query_log" = "true"
}
backupschedule = "0 3 * * *"
}Supported config keys: max_connections, wait_timeout, interactive_timeout, max_allowed_packet, group_concat_max_len, innodb_flush_log_at_trx_commit, character_set_server, collation_server, sql_mode, slow_query_log, long_query_time.
Backups are always on; backupschedule only changes when the daily database dumps run.
Highly available, encrypted and externally reachable
The ha tier runs MySQL Group Replication across three or five members (replicas = 3 or 5) with automatic failover. encryptionenabled turns on MySQL's own encryption at rest, and volumeencryptionenabled adds the separately billed volume encryption layer; both can only be set at creation, and changing either forces recreation. The keys are platform-managed; see Encryption at rest.
External access requires TLS and a non-empty IP allowlist. The port is allocated by the platform and exposed as the externalport attribute.
resource "bahriya_mysql" "ledger" {
handle = "ledger-db"
name = "Ledger DB"
tier = "ha"
replicas = 3
cpumillicores = 4000
memorymb = 16384
storagegb = 250
encryptionenabled = true
activeregions = ["falkenstein-1"]
project = bahriya_project.app.id
databases = [
{ name = "ledger", username = "ledger-app" },
]
tlsenabled = true
tlsvalidityyears = 1 # 1, 3, 5 or 10; renewed 30 days before expiry
externalenabled = true
allowedips = ["198.51.100.0/24"]
clientenabled = true # phpMyAdmin
clientttlhours = 8
}
output "ledger_endpoint" {
value = "${bahriya_mysql.ledger.defaulthostname}.falkenstein-1.mysql.on.bahriya.app:${bahriya_mysql.ledger.externalport}"
}Getting the CA certificate
Clients that verify the server (--ssl-mode=VERIFY_IDENTITY) need the CA certificate of the instance's TLS bundle. With tlsenabled = true and no tlsbundle of your own, the platform manages that bundle in your vault as <handle>-tls, and tlsbundle holds its handle. Read the CA with the bahriya_tls_bundle data source:
data "bahriya_tls_bundle" "ledger" {
handle = bahriya_mysql.ledger.tlsbundle
}
output "ledger_ca" {
value = data.bahriya_tls_bundle.ledger.ca
}terraform output -raw ledger_ca > ledger-ca.pem writes it to a file for --ssl-ca. Renewals keep the CA, so the file keeps working. It changes only if you replace the CA; the next terraform apply then picks up the new one.
Changing the databases list
Adding an entry to databases creates the database and its user on the running instance. Removing an entry drops that database and its user, with all their data, in every region. Terraform shows the removal in the plan — read it before you apply. Databases you created yourself over SQL are never touched.
Credentials
Passwords are never part of the resource or the state. The platform generates the root password and one password per database user, and keeps them in your vault as platform-managed secrets attached to the instance's project: rootsecret and databasesecrets hold their handles (<handle>-mysql-root, <handle>-mysql-<database>). They are billed like any secret. Reveal or rotate a password from the console or with Reis:
reis mysql:credentials <instance-id> --username orders-appConnecting from a container
Containers in the same project reach the instance on port 3306 at its handle:
resource "bahriya_container" "api" {
# ... other required fields ...
newenvvar = [
{ key = "DB_HOST", value = "orders-db:3306" },
{ key = "DB_NAME", value = "orders" },
{ key = "DB_USER", value = "orders-app" },
]
# The database user's password, from the instance's managed vault secret.
secretsenvvar = [
{ name = "DB_PASSWORD", secret = "orders-db-mysql-orders" },
]
}The password is injected from its managed secret and never appears in your configuration; the value is decrypted when the container deploys, so after a rotation redeploy it (the Redeploy button on the secret's Consumers panel) to pick up the new password. Each region runs an independent copy of the instance with its own data.
Outputs
| Attribute | Description |
|---|---|
id | MySQL instance UUID. |
status | Current status (running, provisioning, terminated, etc.). |
nodes | Derived node count per region for the current tier and replicas. |
externalport | The allocated edge port, once external access is enabled. |
defaulthostname | The generated hostname label used in the regional hostnames. |
Notes
cpumillicores,memorymb,storagegb,replicasandclientttlhoursare integers, not strings.tier,encryptionenabled,volumeencryptionenabledandprojectforce recreation when changed.rootsecret,databasesecrets,lukskeyandkeyringkeyare read-only: the handles of the instance's platform-managed vault items.storagegbcan only grow.- Handles cannot be reused after deletion.
- CPU, memory and storage are per node; the
nodesoutput is the multiplier. - Point-in-time restore is not a Terraform resource: it is an action that creates a new instance. Start it from the console or with
reis mysql:restore— see Restoring to a point in time. A restored instance you keep can be imported into Terraform like any other.