Ansible is an open-source software provisioning, configuration management, and application-deployment tool enabling infrastructure as code.
MySQL is an open-source relational database management system. Mysql is commonly installed as part of the popular LAMP or LEMP (Linux, Apache/Nginx, MySQL/MariaDB, PHP/Python/Perl) stack.
In this guide we are going to install and set up MySQL on Ubuntu 22.04. This guide also works on other Debian derivatives like Debian and on newer Ubuntu versions.
A note on versions
Which MySQL you get depends on the release you run, and this matters more than it used to:
| Ubuntu release | mysql-server ships | Status |
|---|---|---|
| 22.04 LTS (Jammy) | MySQL 8.0 | Standard support until May 2027 |
| 24.04 LTS (Noble) | MySQL 8.0 | Standard support until May 2029 |
| 26.04 LTS | MySQL 8.4 LTS | Current LTS |
MySQL 8.0 reached upstream end of life on 21 April 2026 — Oracle no longer ships community security fixes for it, and 8.4 LTS is the version to build on for anything new (supported into 2032). Canonical still backports fixes to the 8.0 packages in 22.04 and 24.04 for the life of those releases, so this playbook remains safe to run there. If you are starting fresh, prefer Ubuntu 26.04, where mysql-server is 8.4 and the playbook below works unchanged.
Also check:
- How to Install and Initialize MySQL 8 on Rocky Linux 8 using Ansible
- How to Install and Secure MariaDB 11 on Rocky Linux 9 using Ansible
- Using Ansible to Install and Initialize MySQL 8 on Rocky Linux/CentOS 8
- How to Install and Set Up MySQL 8 on Ubuntu 20.04
- Installing and Configuring MySQL Server 8 on CentOS 8
- How to Install and Configure MySQL Server 8 on Fedora 34/35
- MySQL Permissions – Create, Update, Delete Database Users
- Install and Setup Nginx, WordPress and MySQL 8 in CentOS 8
- Create an RDS instance in Terraform with a MariaDB Example
Before you begin
The MySQL modules are not part of ansible-core — they live in a collection that you install on the control node. The collection was renamed from community.mysql to ansible.mysql; the old community.mysql.* names still work as redirects, but they are deprecated and are removed in community.mysql 6.0.0 (Ansible 17), so use the new name:
| |
If you are on an older Ansible and stuck with
community.mysql, the playbook below works if you swap theansible.mysql.module prefixes forcommunity.mysql.. Everything else is identical.
Creating the Ansible playbook
Before we define our tasks, we have to tell ansible a couple of things:
| |
Explanation:
namegives the playbook a descriptive name of what it does, it is not compulsory to have this.hostsdefines the hosts to target as defined in thehostsorhosts.yamlfile defined above.gather_factsdefines whether we want ansible to gather os facts before processing the tasks. in our case we do not wantbecomedefines that we want to execute our tasks as rootvarsdefines the variables that we want to reuse in our tasks. We definedmysql_root_passwordandmysql_socketin our case
mysql_socket matters on Ubuntu. A fresh MySQL 8 install authenticates root@localhost with the auth_socket plugin — the account has no password, and it can only be used over the local Unix socket. If Ansible connects over TCP instead, that first login fails and the playbook cannot bootstrap. Pointing the MySQL modules at the socket is what lets them log in as root the first time.
Never keep a real password in plain text like this. In a real playbook, pull mysql_root_password from Ansible Vault or an external secret store.
The ansible tasks
After the section above, we now need to define our tasks. These tasks can either be added in a role or specified as tasks. In our case we will use them as tasks (check the end of this guide for the complete playbook).
Ensure required software is installed
Before proceeding, we want to install all the software that we would need. These includes mysql specific software mysql-server and supporting software like python related software that will be used by ansible to connect to and set up the mysql server instance. We use the ansible apt module to do this.
Please note that we are also doing an OS update and upgrade before proceeding to ensure that we are using the latest packages in our system.
| |
Two things worth calling out here:
python3-pymysql, notpython3-mysqldb. Theansible.mysqlmodules require PyMySQL as their database connector. Older versions of this guide (and plenty of others still online) installpython3-mysqldb; that driver is no longer what the modules expect, so install PyMySQL instead.python3-cryptographyis what PyMySQL uses forcaching_sha2_password, the default authentication plugin in MySQL 8. You can get away without it when connecting over the local Unix socket, but installing it avoids a confusing authentication failure the moment anything connects over TCP.
The unzip, vim and git packages are just conveniences — drop them if you keep your database hosts minimal.
Start mysql server
Because we want to connect and perform operations in the server, lets start it with this task. Since Ubuntu 22.04 uses systemd to manage long running processes, lets start and enable mysqld using the ansible systemd_service module:
| |
ansible.builtin.systemdwas renamed toansible.builtin.systemd_service. The old name still works as an alias, so existing playbooks are not broken, butsystemd_serviceis the canonical name to write today.
Initialize Mysql set up
With the server running, the remaining tasks harden the default install: set a root password, restrict root to local logins, remove the anonymous users and the test database. This is the same ground mysql_secure_installation covers, expressed as idempotent tasks so it can run repeatedly.
Disable root remote login
The mysql root user is the default admin user who has permissions to all resources in the server. A best practice would be to only enable access through this userroot in the local system when we are doing admin tasks otherwise create a dedicated user for each connection, i.e. for each app, have a user that has access to that db alone.
| |
In the above task definition:
- the
mysql_root_passwordvariable will be picked from thevarsdefined earlier - The
itemis a loop of the values defined in theloopsection. - The
check_implicit_admintells ansible to try logging in without password first, which works on a fresh install becauseroot@localhoststill usesauth_socket. As part of this, thepasswordprovided will be set for the root user. login_unix_socketforces that first login over the local socket, which is the only wayauth_socketauthentication succeeds.
Use
looprather thanwith_items. Both still work, butloopis the form Ansible recommends for simple lists, andwith_*is effectively legacy.
Once this task has run, root@localhost no longer uses auth_socket — it has a real password and authenticates with caching_sha2_password. That is the trade you are making by managing the root password from Ansible: you gain a scriptable credential, and you lose the “only a local root shell can become MySQL root” property that auth_socket gave you.
Add my.cnf to the home dir
Now that we have set the password in the above task, we would want to supply the password when doing more tasks as the root user. We can provide this in the ~/.my.cnf, a file that is checked for credentials every time we run mysql commands.
Create a file /root/.my.cnf by copying inline content
| |
Quote the
mode. Unquoted0600is parsed by YAML as the number 600, not as octal permissions — quoting it keeps the intent (-rw-------) unambiguous.
Remove anonymous users
It’s a good practice to remove anonymous users. Lets do it using this task:
| |
Disallow root from logging in remotely
We want the root user to be usable locally only, so drop any root account defined for a remote host:
| |
Earlier versions of this guide did this with a raw DELETE FROM mysql.user ... statement. Don’t. MySQL’s documentation is explicit that the grant tables should not be modified directly — a DELETE removes the row but leaves the account cached in memory until a FLUSH PRIVILEGES, so the account you think you deleted can keep working. mysql_user with state: absent issues a proper DROP USER, which updates the tables and the in-memory cache together. It is also idempotent, which the raw DELETE was not.
Remove test database and access to it
Since we do not need the test database, we can remove it with this task:
| |
The original guide paired this with a DELETE FROM mysql.db WHERE Db='test' query to strip the matching grants. That is the same direct-grant-table anti-pattern as above, and it is no longer needed: MySQL 8 does not create the test database, its grants, or the anonymous users in the first place. DROP DATABASE removes any database-level privileges along with the database, so the task above is sufficient. Keep it as a safety net for servers that were upgraded from MySQL 5.7 and may still carry those leftovers.
Reload privileges to apply changes
| |
FLUSH PRIVILEGES is only strictly required when you have edited the grant tables behind MySQL’s back. Now that every account change above goes through CREATE USER / DROP USER, MySQL keeps its in-memory cache in sync on its own, so this task is a belt-and-braces step rather than a necessity. It is harmless to keep.
Delete the .my.cnf that we copied
For security, lets remove the /root/.my.cnf file since it contains root access:
| |
Whole playbook
This is the whole playbook with all the tasks:
| |
To run the playbook, you need to create the file setup-mysql.yaml with the above content and hosts.yaml with the hosts file content then use the following command to execute:
| |
Run it a second time to confirm it is idempotent: every task should report ok rather than changed, which tells you the playbook describes a settled state instead of re-applying work on every run.
You can then confirm the result on the database host:
| |
The SELECT should show root only for localhost, 127.0.0.1 and ::1 — no % row, and no empty-username anonymous accounts.
Conclusion
In this guide, we were able to use ansible to install Mysql on a Ubuntu Linux host using ansible.
Ansible gives us a way to automate the process. This can be used to set up multiple instances in a predictable way using a single command.