The previous two lessons designed new systems with complete freedom. This one is different, and that is why it looks far more like what you will actually meet on a real project. Contoso Airlines' content portal and corporate blog — airline news, destination guides, operational notices and the press room — is a WordPress that has been running for eight years on a MySQL 5.7 installed on a server in the basement of the Barcelona office. It holds 14 GB of data, 40,000 articles, third-party plugins, backups on a USB drive nobody has ever tried to restore, and, above all, one clear instruction from management: nothing gets rewritten.

The goal is to take it to Azure exactly as it is, gaining automated backups, high availability and patching, without touching a single line of PHP. That is precisely what Azure Database for MySQL - Flexible Server does, and this lesson deploys it, connects it to the private network and runs the migration within its agreed maintenance window.

Cost warning: a General Purpose flexible server with 2 vCores runs at around €120–150 a month plus storage, and zone-redundant high availability doubles the compute. The good news compared with Azure SQL Database is that here you can stop the server (for up to 30 days in a row), which completely changes the bill in development. Even so, delete the lab resource group when you finish.

Contents

  1. The starting point and what has to be preserved
  2. Flexible Server: what Azure manages and what you still manage
  3. Compute and storage tiers
  4. High availability and its cost
  5. Deployment with Azure CLI and private access
  6. Server parameters
  7. Read replicas for the blog's traffic
  8. Backups, retention and restore
  9. The migration from the Barcelona server
  10. Stopping the server to save money in development
  11. Secure TLS connections and what changes in the application
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. The starting point and what has to be preserved

Element Current situation Target in Azure
Engine MySQL 5.7 on Ubuntu, in Barcelona Managed MySQL 8.0 in West Europe
Data 14 GB, 40,000 articles, 180,000 comments The same data, intact, with accents and emoji correct
Application WordPress with 23 plugins No changes, apart from the connection string
Backups A daily dump to a USB drive Automated, with 14-day retention
Availability A single server; if it goes down, there is no portal Zone-redundant high availability
Acceptable outage — 4 hours maximum, in the small hours of a Sunday

That last item governs the whole lesson: a four-hour window allows an offline migration, which is far simpler than an online one. If the acceptable outage were measured in minutes, the strategy would change.

  1. Flexible Server: what Azure manages and what you still manage

Flexible Server is the service's current deployment model (the old "Single Server" is being retired and must not be used on new projects). Each server is a MySQL instance on a managed virtual machine, with redundant remote storage, directly integrable into your virtual network and with fine-grained control of parameters and maintenance windows.

Responsibility Azure You
Operating system, engine patches, minor versions Yes No
High availability and failover Yes, if you enable it You enable it and you test it
Automated backups and restore Yes You define retention and verify that they restore
Encryption at rest and in transit Yes, by default You enforce TLS on the client
Schema, index and query design No Yes
Server parameters (my.cnf) Exposed as configuration Yes, you decide the values
MySQL users and permissions No Yes
Major version (5.7 → 8.0) It offers the upgrade You decide when

The boundary is sharp: Azure looks after the machine and the process; you remain responsible for everything inside the database. Migrating to PaaS does not turn a bad schema into a good one.

  1. Compute and storage tiers

Tier CPU profile Memory per vCore What it is for
Burstable A fraction of a CPU with accumulated credits ~2 GB Development, testing and very light workloads; never sustained production
General Purpose Dedicated CPU ~4 GB Most production workloads
Memory Optimized Dedicated CPU ~8 GB Large working sets, many connections, big caches

The trap with the Burstable tier is that it works perfectly until the CPU credits run out, and then performance collapses exactly when there is traffic. It works for the portal's development environment, not for the portal.

Storage has one detail hardly anyone looks at: IOPS are tied to the provisioned size. A 32 GB disk delivers very few operations per second, and enlarging it is the cheapest way to fix an I/O problem. You can enable auto-grow, which is highly recommended because running out of space leaves the server read-only, and on the higher tiers you can provision IOPS beyond what the size would give you. Storage can only be increased, never reduced.

For production Contoso chooses General Purpose, 2 vCores, 128 GB (with headroom over the current 14 GB both to grow and to have enough IOPS) and for development Burstable B1ms with 32 GB.

  1. High availability and its cost

Two modes, plus the option of having none:

Mode Where the replica lives Protects against Cost Failover time
No high availability There is none Nothing; maintenance means a restart Base Minutes of unavailability
Same zone Another node in the same zone Node failure Double compute ~60–120 seconds
Zone redundant Another zone in the region Node failure and a whole zone failing Double compute ~60–120 seconds

If you are going to pay double, choose zone redundant: it costs the same as same-zone and protects against more. Same-zone only makes sense in regions with no availability zones, or when the latency between zones is critical.

Contoso enables zone redundancy in production — the portal is the airline's public face and it also publishes the disruption notices, exactly when it is consulted most — and no high availability in development.

  1. Deployment with Azure CLI and private access

There are two connectivity methods and they are chosen when the server is created, with no possibility of changing them afterwards:

  • Public access with firewall rules: the server has a public name and is filtered by IP. Convenient to start with, and a permanent attack surface.
  • Private access with virtual network integration: the server is placed in a delegated subnet and is only reachable from the network. It has no entry point from the internet.

Contoso chooses private access, for consistency with what was done with pe-sql-reservas and because WordPress runs on App Service with virtual network integration: nothing needs to reach the database from outside. The delegated subnet is snet-integracion-app, which was already reserved in module 2 for this kind of service.

RG="rg-contoso-reservas-pro"
SERVER="mysql-contoso-portal-pro"
read -s -p "MySQL administrator password: " ADMIN_PWD; echo

az mysql flexible-server create --name $SERVER --resource-group $RG \
  --location westeurope --version 8.0 \
  --admin-user adminportal --admin-password "$ADMIN_PWD" \
  --tier GeneralPurpose --sku-name Standard_D2ds_v4 \
  --storage-size 128 --storage-auto-grow Enabled \
  --high-availability ZoneRedundant \
  --vnet vnet-contoso-pro --subnet snet-integracion-app \
  --private-dns-zone "interno.contosoairlines.example" \
  --backup-retention 14 --geo-redundant-backup Enabled \
  --tags entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042 [email protected]

# The portal database, with a character set that handles accents and emoji
az mysql flexible-server db create -g $RG -s $SERVER -d portal_contenidos \
  --charset utf8mb4 --collation utf8mb4_unicode_ci

Two parameters deserve special attention. --private-dns-zone reuses the private zone interno.contosoairlines.example from module 2, so that the server resolves by internal name from anywhere on the network. And --charset utf8mb4: MySQL carries around a historical character set called utf8 that is not full UTF-8 and does not support emoji or certain characters; using utf8mb4 from the start avoids the classic blog article that shows up full of broken symbols after the migration.

  1. Server parameters

There is no access to the my.cnf file, but all of its parameters are exposed as server configuration. The ones you actually touch in practice:

# Concurrent connections: WordPress with 23 plugins opens a lot of short connections
az mysql flexible-server parameter set -g $RG -s $SERVER \
  --name max_connections --value 300

# Slow query log, essential for diagnosing the portal
az mysql flexible-server parameter set -g $RG -s $SERVER \
  --name slow_query_log --value ON
az mysql flexible-server parameter set -g $RG -s $SERVER \
  --name long_query_time --value 2      # seconds beyond which a query is logged

# The server's default character set: accents and emoji
az mysql flexible-server parameter set -g $RG -s $SERVER \
  --name character_set_server --value utf8mb4

# Time zone: everything runs in UTC and is converted in the application
az mysql flexible-server parameter set -g $RG -s $SERVER --name time_zone --value "+00:00"

About max_connections: simply raising it is a poor fix, because every connection consumes memory and the allowed maximum depends on the size of the server. If the application opens a lot of short connections, the right answer is connection pooling on the client. Some parameters are static and require restarting the server; the CLI warns you, and that restart has to be planned.

  1. Read replicas for the blog's traffic

The portal is a textbook case: 98% of the traffic is reads of articles and only 2% is writes from the communications team. A read replica copies the server asynchronously and absorbs that traffic:

az mysql flexible-server replica create \
  --replica-name mysql-contoso-portal-pro-r1 \
  --source-server $SERVER --resource-group $RG --location westeurope

Three essential warnings: the replica is asynchronous, so it can be a few seconds behind and it is no good for reading something that has just been written; the application has to send reads explicitly to its name, which in WordPress is solved with a database-splitting plugin; and the replica bills as a full server, so it is justified when the traffic calls for it, not by default. If it ever needed promoting to a standalone server, you stop replication with az mysql flexible-server replica stop, an irreversible operation.

  1. Backups, retention and restore

Backups are automated: a full backup daily and transaction log backups every few minutes, with configurable retention from 1 to 35 days and the option of geo-redundancy to North Europe, which is what was enabled with --geo-redundant-backup Enabled. Contoso sets 14 days, enough to catch the portal's typical mistake — a plugin that corrupts tables or a mass deletion of comments — without inflating the bill.

Restore works exactly as in Azure SQL Database: it creates a new server, it never overwrites the original.

az mysql flexible-server restore --name mysql-contoso-portal-rec \
  --source-server $SERVER --resource-group $RG \
  --restore-time "2026-08-11T02:15:00Z"   # UTC

And the rule that repeats throughout the module: that restored server bills from the first minute. You recover the data, verify it and delete the server the same day. One tested backup is worth infinitely more than three untested ones, so it is worth rehearsing a restore once a quarter.

  1. The migration from the Barcelona server

Two routes, and the choice depends on the acceptable outage:

mysqldump / mydumper Azure Database Migration Service
Mode Offline Offline or online
Outage The whole dump and load time Minutes, in online mode
Complexity Low: two commands Medium: it requires configuring the service and the connectivity
Reasonable volume Up to tens of GB Hundreds of GB or more
When There is a maintenance window The service cannot be stopped

With 14 GB and a four-hour window in the small hours of a Sunday, Contoso chooses mysqldump in offline mode. It is simpler, and simple things fail less at three in the morning.

Pre-migration checklist (the week before, not on the day):

  1. Compatible source and target versions; test the migration first on mysql-contoso-portal-dev.
  2. Inventory the character sets and collations of every table: SELECT table_name, table_collation FROM information_schema.tables WHERE table_schema='portal_contenidos';
  3. Check that no unsupported features are in use (the MyISAM engine on critical tables, SUPER, procedures that depend on the file system).
  4. Note the row counts of the big tables: wp_posts, wp_postmeta, wp_comments.
  5. Review users and permissions: MySQL users do not travel in a normal dump.
  6. Freeze content publishing and warn the communications team.

Execution, with connectivity already in place through module 2's site-to-site VPN:

# 1. Dump from the Barcelona server (--single-transaction does not lock the InnoDB tables)
mysqldump --host=10.100.4.20 --user=root --password \
  --single-transaction --routines --triggers --events \
  --default-character-set=utf8mb4 \
  --databases portal_contenidos > portal_contenidos.sql

# 2. Load into Azure, from a VM in snet-gestion with private access
mysql --host=mysql-contoso-portal-pro.interno.contosoairlines.example \
  --user=adminportal --password --ssl-mode=REQUIRED \
  < portal_contenidos.sql

Post-migration verification, before reopening the portal:

  1. Row counts per table, compared against the ones noted beforehand.
  2. Check accents and emoji in recent articles; if they show up broken, the problem is in the character set and the dump has to be repeated, not patched by hand.
  3. Recreate the application users and permissions with minimal CREATE USER and GRANT (never the administrator for the application).
  4. Change WordPress's connection string and test the whole site: home page, article, search, comment, admin dashboard.
  5. Leave the Barcelona server powered off but intact for two weeks, as the rollback plan.

  1. Stopping the server to save money in development

This is the most pleasant practical difference compared with Azure SQL Database: a flexible server can be stopped, and while it is stopped no compute is billed (storage still is).

az mysql flexible-server stop  -g rg-contoso-reservas-dev -n mysql-contoso-portal-dev
az mysql flexible-server start -g rg-contoso-reservas-dev -n mysql-contoso-portal-dev

It restarts itself after 30 days stopped, so it is not a way of "archiving" a server: for that, you take a dump and delete it. Combined with an Azure Automation runbook (07-04) that stops it at 19:00 and starts it at 8:00 Monday to Friday, the development environment ends up costing roughly a third.

  1. Secure TLS connections and what changes in the application

Azure Database for MySQL requires TLS by default, and that is how it should stay. For the application this means two small but mandatory changes in the connection string: specifying the TLS mode and, if the client requires it, the root certificate.

// wp-config.php: these are the only lines that change on migration
define('DB_NAME',     'portal_contenidos');
define('DB_USER',     'wp_portal');                    // application user, not the admin
define('DB_HOST',     'mysql-contoso-portal-pro.interno.contosoairlines.example');
define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);       // enforces TLS

Note that the host name is the private one, resolvable only from the virtual network; that the user is an application user with minimal permissions on portal_contenidos; and that the password should not live in the file but in Key Vault, referenced from the App Service application settings (lesson 04-03). If an old client does not support TLS 1.2, the answer is to update the client, not to downgrade the server.

Common Mistakes and Tips

  • Choosing public access "just to try it" and leaving it that way. The connectivity method cannot be changed after the server is created: it forces you to recreate it and migrate again.
  • Using utf8 instead of utf8mb4. MySQL's utf8 is not full UTF-8. You find out when the articles show up with broken characters and there is already traffic on top of them.
  • Putting the Burstable tier in production. It works fine until the CPU credits run out, and that happens precisely on the busiest day.
  • Raising max_connections as the answer to any connection error. Every connection consumes memory; the fix is usually connection pooling in the application.
  • Trusting the read replica for data that has just been written. It is asynchronous: the editor would publish an article and not see it on the site.
  • Migrating without counting rows before and after. Without that count there is no objective way of knowing whether the migration was complete.
  • Forgetting the users and permissions. A mysqldump of a database does not include the MySQL accounts: if nobody recreates them, the application does not start during the cutover window.
  • Tip: rehearse the full migration in the development environment with a real copy of the data. Something always turns up the first time, and it is better that it turns up on a Wednesday afternoon.
  • Tip: keep the source server powered off and intact for two weeks. It is the cheapest rollback plan there is.

Exercises

Exercise 1: sizing and configuring

The portal receives 120,000 visits a month, with peaks on Monday mornings when the operational notices are published. The database occupies 14 GB and grows by about 3 GB a year.

  1. Choose the compute tier, storage size and high availability mode for production and for development, justifying each choice.
  2. Which connectivity method do you choose, and why is it a decision that leaves no room for regret?
  3. What backup retention would you set in each environment?

Exercise 2: planning the migration

The team has a four-hour window on Sunday from 02:00 to 06:00.

  1. Choose between mysqldump and Azure Database Migration Service and justify it.
  2. Write three pre-migration checks and three post-migration checks, saying what you would do if any of them failed.
  3. Describe the rollback plan if at 05:30 the portal is not working properly.

Exercise 3: diagnosing a performance problem

After the migration, the portal responds well except on Mondays at 09:00, when some pages take more than 10 seconds and "too many connections" errors appear.

  1. Which server parameter would you turn on first to diagnose it, and how?
  2. List three possible causes, with their respective fixes.
  3. Would a read replica solve the problem? And would simply increasing the storage?

Solutions

Solution 1:

  1. Production: General Purpose with 2 vCores (the dedicated CPU avoids the Burstable tier's collapse during Monday's peak), 128 GB of storage — not for space, but for the IOPS tied to the size and for growth headroom — with auto-grow enabled, and zone-redundant high availability, since the portal publishes the disruption notices exactly when it is consulted most. Development: Burstable B1ms, 32 GB, no high availability.
  2. Private access with integration into snet-integracion-app. It leaves no room for regret because the connectivity method is fixed when the server is created: changing it forces you to create another server and repeat the whole migration.
  3. Production, 14 days with geo-redundancy; development, 7 days without it, since the data is a copy and is regenerable.

Solution 2:

  1. Offline mysqldump. With 14 GB, the dump and the load fit comfortably into four hours, and no continuous replication is needed. The migration service would add configuration complexity without solving any real problem; you would keep it for volumes in the hundreds of GB or for outages measured in minutes.
  2. Beforehand: (a) test the full migration in development — if it fails, the window is postponed, never improvised; (b) verify the character sets of all the tables — if they are mixed, normalize them first; (c) note the row counts — without them there can be no verification. Afterwards: (a) compare the counts — if they do not match, roll back; (b) check accents and emoji — if they are broken, repeat the dump with --default-character-set=utf8mb4; (c) test the whole journey through the site, including publishing a test article.
  3. Rollback: point WordPress's connection string back at the Barcelona server, which has been kept powered off but intact, start it, verify the portal and reschedule the window. Since content publishing was frozen, there are no new writes to lose.

Solution 3:

  1. slow_query_log set to ON with long_query_time at 2 seconds, and then review the log to identify the queries that repeat during the peak. It is the first measure because it turns a complaint into data.
  2. Causes and fixes: (a) slow queries from a plugin against wp_postmeta with no suitable index, fixed by creating the index or disabling the plugin; (b) too many short connections from the lack of pooling, fixed with client-side pooling and, secondarily, by adjusting max_connections; (c) insufficient IOPS from small storage, fixed by enlarging the disk or provisioning additional IOPS.
  3. A read replica would help, because 98% of the traffic is reads and spreading them relieves the primary server, but it requires the application to send reads to the replica. Increasing the storage helps only if the bottleneck is IOPS; if the problem is badly indexed queries or connections, it changes nothing and only raises the bill. That is why diagnosis comes first.

Conclusion

You have taken a legacy system to Azure without rewriting a line, which is the most frequent and least glamorous migration on any real project. You know what Azure Database for MySQL - Flexible Server is, where the boundary lies between what Azure manages and what remains yours, and why the Single Server model is no longer used. You can tell the Burstable, General Purpose and Memory Optimized tiers apart and you know the trap of the CPU credits, along with the relationship between storage size and IOPS and why auto-grow is worth having. You have compared same-zone high availability with zone-redundant — same price, more protection — and you have deployed mysql-contoso-portal-pro with private access in snet-integracion-app and its internal DNS zone, knowing that this connectivity method cannot be changed later.

You have tuned the parameters people actually touch — max_connections, slow_query_log, character_set_server with utf8mb4 so that accents and emoji do not break — added a read replica for the blog's 98% read traffic with its three warnings, configured backups with 14 days of retention and restored to a new server that has to be deleted the same day. And above all you have executed the migration: the comparison between mysqldump and Azure Database Migration Service, the choice of offline mode because of the four-hour window, the pre-migration checklist, the dump and load commands, the post-migration verification with row counts and the two-week rollback plan. You close with two very practical details: that this service does let you stop the server so you do not pay for compute in development, and exactly what changes in the connection string when TLS is enforced.

The next system on the data map also comes from outside, but for the opposite reason. Contoso's crew planning system does not use PostgreSQL by accident: it was chosen because it needs complex queries over shifts and rest periods, rich data types and extensions that do not exist in other engines, in particular geographic calculations of distances and routes between airports. In Azure Database for PostgreSQL you will see what it shares with what you have just learned — which is a lot, and we will mark it explicitly so as not to repeat it — and, above all, what is different: the extensions and their allowed list, with postgis for the routes and pgvector already looking ahead to module 6's AI services, performance tuning with EXPLAIN ANALYZE, the autovacuum problem on tables with a lot of churn, and connection pooling with PgBouncer.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved