Skip to main content

Overview

This article walks the installer through migrating the LILT application database from an in-cluster MySQL deployment to an Amazon RDS for MySQL instance. It applies when moving an existing self-managed installation onto a fully managed cluster, such as the environment described in Install System (AWS EKS), where RDS replaces the in-cluster database. The database is exported through a proxied connection to a staging host and restored into RDS from there. Nothing is written inside the database pod at any point, so the procedure works even when the existing storage volume has no free space for a dump file. The export stage is read-only against the source cluster, leaving the original environment available as a fallback until the migration is accepted. Object storage migration is a separate procedure; see Migrate Object Storage (MinIO to S3).

Tools you will need

  • AWS user with permissions to describe the RDS instance, modify its parameter group, and create snapshots.
  • Required utilities for the staging host:
    • AWS CLI
    • kubectl, with access to the source cluster
    • MySQL client tools matching the source server version
  • A staging host with:
    • Network access to the RDS endpoint, or the ability to transfer the dump file to a host that has it
    • Free disk space for the compressed dump

Structure of this Article

The first section explains where the database migration belongs in the overall install sequence, because restoring at the wrong point produces a schema that cannot be repaired automatically. The remaining sections cover assessing the source database, preparing the RDS instance, exporting and restoring the data, reconciling the schema migration metadata, applying pending schema migrations, rebuilding the search index, and verifying the result.

Installer privileges

All commands are run from a staging host by a user with the permissions stated in the “Tools” section above. Commands that target the source cluster assume the LILT namespace is lilt and the database pod is mysql-0; adjust if your installation differs.

Migration Order

Restore before installing LILT

The application’s schema migrations are the mechanism that brings an older database up to the current schema. They must run against the restored data, which means the restore has to complete before LILT is installed on the new cluster. Installing first and restoring afterwards does not work, and fails in a way that is difficult to detect. On an empty database the installer applies every migration and produces a complete current-version schema. Restoring an older dump on top of that does not reset it: a dump drops and recreates only the tables it contains, so every table introduced after the dump was taken survives untouched, while the migration metadata table is replaced wholesale with the older list. The result is a mixture of old and new tables whose metadata claims the newer migrations never ran. Nothing reports an error at that point; the inconsistency surfaces later as failures during a subsequent upgrade. The correct sequence is:
  1. Provision the RDS instance and prepare the parameter group
  2. Create the application database user and grants
  3. Restore the dump
  4. Reconcile the schema migration metadata
  5. Take an RDS snapshot
  6. Install LILT, which applies the pending schema migrations
  7. Reset the search index cursor and restart the indexer
  8. Verify, then return the environment to service

Migrate first, upgrade separately

If the new environment also runs a newer LILT release than the source, treat the storage move and the version upgrade as two operations. Restore the data and confirm it is intact at the original version first, then perform the version upgrade against RDS. Combined, a failure partway through several hundred schema migrations cannot be attributed to either cause.

Assess the Source Database

Set the user environment variables

lilt_dev is the default application database name. Confirm it against your installation:

Read the database credentials

If that secret does not exist in an older installation, use the root password configured for the database chart.

Record the server configuration

These values determine the export options and the RDS parameter group. Record all of them before continuing:
Record the database-level character set and collation separately. These must be reproduced on RDS:

Measure the database

This sizes the disk requirement on the staging host. A logical dump is smaller than the reported total because it contains no index data, and compresses further:

Reclaim space on the storage volume

When binary logging is enabled, the log files are written into the data directory and can occupy a significant share of a small volume. Reclaiming that space reduces the risk of the volume filling during the export:

Prepare the RDS Instance

Engine version

Create the instance with an engine version matching the source server recorded above. Restoring a dump taken from a newer server into an older engine version can fail on syntax or features the older version does not support.

Parameter group

Set the following in the instance’s parameter group:
log_bin_trust_function_creators is required because the restoring user does not hold the SUPER privilege on RDS, and stored routines or triggers in the dump fail without it. If the application’s database client cannot negotiate the caching_sha2_password authentication plugin, also enable mysql_native_password in the parameter group. Recent MySQL versions disable that plugin by default, and the failure only appears when the application connects — testing as the administrative user succeeds regardless.

Correct the database character set and collation

This step is required when the database was created for you, either by the instance’s initial database name setting or by an infrastructure-as-code definition. A dump taken with the --databases option includes a CREATE DATABASE ... IF NOT EXISTS statement carrying the source character set and collation. When the database already exists, that statement does nothing and its collation clause is ignored, leaving the database on the engine’s default collation instead of the source’s. The restored tables are unaffected, because every CREATE TABLE statement in the dump carries its own character set and collation. The problem appears later: tables created by subsequent schema migrations without an explicit collation inherit the database default, and queries joining them against restored tables fail with Illegal mix of collations. That surfaces days afterwards as intermittent errors rather than as a restore failure. Create a credentials file for the administrative user and correct the collation to match the source:
Substitute the character set and collation recorded from the source. ALTER DATABASE changes only the default applied to newly created objects; it does not rewrite existing data.

Create the application database user

User accounts and grants are not carried by the dump and must be created directly. Skip this step if your infrastructure definition already provisions the user:
A database-level grant is valid even before the database exists, and the CREATE privilege it includes is what allows the dump to create the database if it is not already present. Do not run any database bootstrap or schema initialization job at this stage. Creating the database and user is required; applying schema migrations before the restore produces the inconsistent schema described in “Migration Order”.

Export the Database

Stop application writes

A dump is only consistent if nothing is writing to the database. This also protects the storage volume: a consistent dump requires the server to retain previous row versions for the duration of the export, and on a volume with little free space a long export against a live database can exhaust it. Record the current replica counts first, so the environment can be restored if the migration is abandoned:
Scale down the application, excluding the data services:
Do not scale down every deployment in the namespace with a single command. Doing so stops the database, which the export reads from, and discards the replica counts needed to restore the environment.

Open a connection to the database

Forward the database service port to the staging host. Port 13306 is used locally to avoid conflicting with any MySQL instance already running there:
Write the credentials to a protected file rather than passing them on the command line, so the password does not appear in the process list:
A default installation runs the in-cluster database without TLS, which is what the connection above assumes. If custom certificates have been applied to the database outside of the service mesh, add an explicit mode to the credentials file. REQUIRED encrypts the connection without validating the server certificate, which is normally what is wanted for an export running through a local tunnel:
Certificate validation fails even with a valid certificate when connecting through a forwarded port, because the certificate is issued for the in-cluster service name and does not match 127.0.0.1. Use ssl-mode=VERIFY_CA with ssl-ca pointing at the issuing authority if validation is required, and connect by a name the certificate covers rather than by address:
If the source server does not accept encrypted connections at all, set ssl-mode=DISABLED instead. Do not carry any of these settings over to the RDS credentials file: RDS requires encrypted connections and presents certificates from a public authority, so it keeps ssl-mode=REQUIRED as shown later in this article. Port forwarding tunnels through the Kubernetes API server and can drop during a long transfer. If the connection proves unstable, capture the service definition, expose it as a NodePort for the duration of the migration, connect directly to a node, and restore the original definition afterwards:

Export to the staging host

The export runs on the staging host and writes only there. No dump file is created inside the database pod:
Each option addresses a specific requirement:
  • --defaults-extra-file must be the first argument on the command line, or it is ignored without warning.
  • --set-gtid-purged=OFF is required when the source has GTID mode enabled. Without it the dump contains a statement that sets a global GTID variable, which RDS rejects because the restoring user does not hold SUPER.
  • --no-tablespaces avoids a statement requiring the PROCESS privilege, which the restoring user does not hold.
  • --single-transaction produces a consistent snapshot without locking tables.
  • --databases includes the CREATE DATABASE and USE statements, making the dump self-contained.
  • The sed filter removes DEFINER clauses, which reference user accounts that do not exist on the new instance. Applying it in the same pipeline avoids writing the data to disk twice.

Validate the export

A truncated dump that appears to restore successfully is the worst outcome, so validate before restoring:
The first count must be 1 and the other two must be 0. The Dump completed marker is written only when the export finishes cleanly, so its absence means the connection dropped partway and the export must be repeated.

Record the row-count baseline

Capture this while writes are still stopped. It is the reference for verification after the restore:

Close the connection

Restore into RDS

If the staging host cannot reach the RDS endpoint, transfer the dump to a host that can before continuing. Create a credentials file for the application user and restore:
RDS requires encrypted connections, hence ssl-mode=REQUIRED. Expect the restore to take considerably longer than the export, because it rebuilds every index. If the restore fails partway, do not retry over the partial result. Recreate the database with the correct collation and start again:

Reconcile the Schema Migration Metadata

The application records each applied schema migration by filename in a metadata table named SequelizeMeta, and matches them by exact filename. Some migrations are authored with a .ts extension and shipped in the application image compiled to .js. If a database recorded them under their .ts names, the installed application sees those migrations as never applied, attempts to run them again, and fails on objects that already exist — typically reported as a duplicate key or duplicate column error. Check whether the restored database is affected:
If ts is 0, no action is needed. Otherwise normalize the recorded names to the compiled extension. This affects only the metadata table — no schema and no application data are modified — and running it a second time has no effect:
The DELETE handles the case where both extensions are recorded for the same migration, which would otherwise collide on the table’s primary key during the rename.

Take a Snapshot

Create a snapshot before any schema migration runs. This is the only inexpensive way to recover from a migration that applies partially:

Apply Schema Migrations

Install LILT on the new cluster as described in Install System (AWS EKS), with the database connection pointing at the RDS endpoint. During installation the application applies every schema migration recorded as unapplied, bringing the restored database up to the current schema. When migrating from an older release, expect a large number of migrations to run. This is normal and represents the accumulated schema changes between the two versions.

Confirm the migrations succeeded

Do not infer success from pod status. A migration failure does not necessarily cause the pod to report a failure, so the application can start against a partially migrated schema. Inspect the initialization container’s logs directly:
Confirm the metadata table grew as expected. Record the count before installation and compare it afterwards; the difference is the number of migrations applied:
unreconciled must be 0. If it is not, the reconcile step was skipped or new entries were recorded under the wrong extension, and the next upgrade will attempt to reapply them. Installation is idempotent with respect to migrations: any that did not run are applied on the next install or upgrade. If the logs show a failure, resolve the cause and run the installer again rather than editing the metadata table by hand.

Rebuild the Search Index

The search index on the new cluster is empty, and restored content is not indexed automatically. The indexer tracks its position in a table named IndexerCursor, which holds a timestamp; only records modified after that timestamp are indexed. The restored row carries the position reached on the original cluster, and every restored record predates it, so without this step search returns no results for migrated content. Perform this after the schema migrations have completed. The cursor is read once when the indexer starts, so it must be paired with a restart. CURSOR is a reserved word in MySQL and must be quoted with backticks:
Restart the indexer so it reads the new position:
The column is a DATETIME, so a date before 1970 is also accepted; the value above is simply a conventional starting point. The search indices themselves are created automatically when the indexer starts and require no manual step. Monitor progress by watching the index document counts grow:

Verify the Migration

Compare row counts

Row counts reported by information_schema are estimates for InnoDB tables, so use this comparison to identify substantial differences and confirm those with a direct COUNT(*) on the largest tables. Counts will legitimately differ for tables that the schema migrations altered.

Confirm the character set and collation

Application checks

Perform these in order, because each one exercises a different part of the migration:
  1. Sign in, which confirms the application can authenticate against the restored user records.
  2. Confirm the jobs list is populated, which confirms general read access to restored data.
  3. Open a previously existing document, which confirms the restored records resolve correctly.
  4. Search for a phrase from restored content, which confirms the index was rebuilt.
  5. Create a new job and run a translation, which confirms writes succeed against the new instance.

Post-Migration Configuration

  • Connection settings. The database host, port, user and password must point at the RDS endpoint. Confirm every location where the deployment configures them.
  • Application user. Confirm the application authenticates as the dedicated database user rather than the instance’s administrative user.
  • In-cluster database. Once the migration is accepted, the in-cluster MySQL deployment is no longer used. Leave it in place until then, since it is the fallback.
  • Encrypted connections. RDS requires TLS. Any component configured for an unencrypted in-cluster connection must be updated.

Cleanup

Restore the application to its previous replica counts using the file recorded before the export, then remove the credentials files and the staged dump:
Restore the database service definition if it was exposed as a NodePort. Retain the row-count baseline and the restore and migration logs as a record of the migration. Keep the in-cluster database volume intact until the migration has been accepted. No step in the export modifies it, so the original environment remains a complete fallback.

Debugging

Access denied when the application connects, while administrative access works: the application’s client cannot negotiate the instance’s default authentication plugin. Enable mysql_native_password in the parameter group and recreate the application user with that plugin. Restore fails on a stored routine or trigger: log_bin_trust_function_creators is not set to 1 in the parameter group. Restore fails on a statement setting a global GTID variable: the export was taken without --set-gtid-purged=OFF. Repeat the export with that option. Illegal mix of collations after an upgrade: the database default collation does not match the restored tables, because the database was created before the restore and its collation was not corrected. Compare the database default against a restored table’s collation and correct it, then recreate the affected tables. Duplicate key or duplicate column errors during schema migration: migration metadata records entries under a different file extension than the installed image uses. See “Reconcile the Schema Migration Metadata”. Search returns no results for migrated content: the index cursor was not reset, or the indexer was not restarted after resetting it. The export stops partway with a connection error: the port-forward tunnel dropped. The dump cannot be resumed; repeat the export, and use the NodePort approach if the tunnel proves unreliable.