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.

Updated 22 Oct 20266 min read

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

FieldTypeExampleDescription
handlestring"orders-db"Permanent identifier. Cannot be reused after deletion.
namestring"Orders DB"Display name.
tierstring"single"single, replicated or ha. Changing it forces recreation.
cpumillicoresnumber1000CPU per node in millicores. Chosen as a pair with memorymb.
memorymbnumber2048Memory per node in MB. Chosen as a pair with cpumillicores.
storagegbnumber20Data storage per node in GB, 5 to 250. Grow-only.
databaseslist of objects[{ name = "orders", username = "orders-app" }]At least one database, each with its user.
activeregionslist["falkenstein-1"]One or more regions to deploy to.
projectstringbahriya_project.x.idThe 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-app

Connecting 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

AttributeDescription
idMySQL instance UUID.
statusCurrent status (running, provisioning, terminated, etc.).
nodesDerived node count per region for the current tier and replicas.
externalportThe allocated edge port, once external access is enabled.
defaulthostnameThe generated hostname label used in the regional hostnames.

Notes

  • cpumillicores, memorymb, storagegb, replicas and clientttlhours are integers, not strings.
  • tier, encryptionenabled, volumeencryptionenabled and project force recreation when changed.
  • rootsecret, databasesecrets, lukskey and keyringkey are read-only: the handles of the instance's platform-managed vault items.
  • storagegb can only grow.
  • Handles cannot be reused after deletion.
  • CPU, memory and storage are per node; the nodes output 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.