Ansible Tutorial - Playbook How To Install From Scratch and Deploy LAMP + Wordpress on Remote Server

1. Let's work from an environment where we can install Ansible on.

If you are using an older version of Linux based on Mint 18 or Ubuntu 16, you may want to get the PPA and get the latest version of Ansible that way:

sudo apt install gpg

sudo add-apt-repository ppa:ansible/ansible
sudo apt update

How to get the add-apt-repository command.

Some bugs in older Ansible versions are where unarchive will retrieve the remote_src as local and in mysql_user where it cannot assign the privileges needed to a user (it can't parse some of the GRANT queries).  For the lineinline module it may say there is no parameter "path" found.

 

Requirements: A Linux machine (eg. VM whether in the Cloud or a local VM on Vbox/VMWare/Proxmox) that you can easily install Anisble on (eg. Debian/Ubuntu/Mint).  The VM requires proper/working internet between the Ansible Controller and to the internet.

This will be on our "controller" / source machine which is where we deploy the Ansible Playbooks (.yaml) files from.

Install Ansible

sudo apt install ansible

Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
  ieee-data python-jinja2 python-netaddr python-yaml
Suggested packages:
  python-jinja2-doc ipython python-netaddr-docs
Recommended packages:
  python-selinux
The following NEW packages will be installed:
  ansible ieee-data python-jinja2 python-netaddr python-yaml
0 upgraded, 5 newly installed, 0 to remove and 153 not upgraded.
Need to get 2,463 kB of archives.
After this operation, 15.7 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 python-jinja2 all 2.8-1ubuntu0.1 [106 kB]
Get:2 http://archive.ubuntu.com/ubuntu xenial/main amd64 python-yaml amd64 3.11-3build1 [105 kB]
Get:3 http://archive.ubuntu.com/ubuntu xenial/main amd64 ieee-data all 20150531.1 [830 kB]
Get:4 http://archive.ubuntu.com/ubuntu xenial/main amd64 python-netaddr all 0.7.18-1 [174 kB]
Get:5 http://archive.ubuntu.com/ubuntu xenial-backports/universe amd64 ansible all 2.1.1.0-1~ubuntu16.04.1 [1,249 kB]
Fetched 2,463 kB in 1s (1,474 kB/s)
Selecting previously unselected package python-jinja2.
(Reading database ... 434465 files and directories currently installed.)
Preparing to unpack .../python-jinja2_2.8-1ubuntu0.1_all.deb ...
Unpacking python-jinja2 (2.8-1ubuntu0.1) ...
Selecting previously unselected package python-yaml.
Preparing to unpack .../python-yaml_3.11-3build1_amd64.deb ...
Unpacking python-yaml (3.11-3build1) ...
Selecting previously unselected package ieee-data.
Preparing to unpack .../ieee-data_20150531.1_all.deb ...
Unpacking ieee-data (20150531.1) ...
Selecting previously unselected package python-netaddr.
Preparing to unpack .../python-netaddr_0.7.18-1_all.deb ...
Unpacking python-netaddr (0.7.18-1) ...
Selecting previously unselected package ansible.
Preparing to unpack .../ansible_2.1.1.0-1~ubuntu16.04.1_all.deb ...
Unpacking ansible (2.1.1.0-1~ubuntu16.04.1) ...
Processing triggers for man-db (2.7.5-1) ...
Setting up python-jinja2 (2.8-1ubuntu0.1) ...
Setting up python-yaml (3.11-3build1) ...
Setting up ieee-data (20150531.1) ...
Setting up python-netaddr (0.7.18-1) ...
Setting up ansible (2.1.1.0-1~ubuntu16.04.1) ...

Setup Ansible Hosts File

#some distros don't create /etc/ansible or hosts, but we can do that ourselves:

mkdir /etc/ansible

touch /etc/ansible/hosts

vi /etc/ansible/hosts

Let's make a new section/group called "lamp"

Change the IP 10.0.2.16 to the IP of your destination Linux VM

[lamp]
host1 ansible_ssh_host=10.0.2.16  #you could add host2,host3 and as many extra hosts as you want

Setup ssh root Username for "lamp" group

sudo mkdir -p /etc/ansible/group_vars

vi /etc/ansible/group_vars/lamp

#note that the file name is lamp, if the group was called "abcgroup" then the filename would be "abcgroup" instead otherwise it has no impact if the filename does not match the group name.

ansible_ssh_user: root

#note that we can put other variables in this same file by adding more lines like above
#you could create another variable like this:

rtt_random_var: woot!

Let's make sure things work, let's just ping all hosts (we only have 1 so far)

ansible -m ping all

#We also could have specified ansible -m ping lamp to just check connectivity to the lamp group

Oops it didn't work!? But I can ping and ssh to it manually


host1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh.",
    "unreachable": true
}

 

If you need to login as root, make sure you enable root login in /etc/ssh/sshd_config.

1.) You need an SSH Key on your Ansible controller (ssh-keygen)

Example below

ssh-keygen


Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:Nnb95Vj6mlS/xmhqPdx/DvWp6/BOf+B0SSA8ileygtM root@AnsibleTarget
The key's randomart image is:
+---[RSA 3072]----+
|          .      |
|         . = .   |
|      o . = o .  |
|     o E + .   . |
|      . S . . . *|
|       o o   .oX=|
|           .o+X++|
|           .+B+B+|
|          ..=**=B|
+----[SHA256]-----+

2.) You need to copy the SSH key to the target (ssh-copy-id user@remotehost)

Do this on your Ansible Controller.

ssh-copy-id root@YourTargetIP

 

3.) If you need to login as root, you'll need to enable root login

 

Other Ansible Ping Errors


areebhost01 | FAILED! => {
    "changed": false,
    "module_stderr": "Shared connection to 192.168.1.208 closed.\r\n",
    "module_stdout": "/bin/sh: 1: /usr/bin/python: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}


The error above means the target does not have python which is required for Ansible to work.

#on the target
apt install python python3

Now our ping works

areebhost01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}


 

*I strongly recommend not using become or using any method that uses a manual password or password saved in a variable for both security reasons and convenience.

I especially don't recommend using -K or --ask-become-pass because it uses the same password for all hosts (all hosts should not have the same password).  it is also inefficient and insecure to rely on typing the password each time when prompted and defeats the purposes of automation with Ansible.

 

More on become from the Ansible documentation:

https://docs.ansible.com/ansible/latest/user_guide/become.html#risks-of-becoming-an-unprivileged-user

Try again now that you have your key auth working (if it works you should be able to ssh as root to the server without any password)

host1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

 

Check the uptime or run other shell commands from host1:

ansible -m shell -a 'free -m' host1
host1 | SUCCESS | rc=0 >>
              total        used        free      shared  buff/cache   available
Mem:           3946          84        3779           5          82        3700
Swap:           974           0         974

*Note we could swap host1 for "all" to do all servers or specify "lamp" for just the lamp group to execute that command on.

 

What Is An Ansible Play/PlayBook And How Does It Work?

The sports analogy or possibly theatre inspired terms are really just slang for "it's a YAML" config file that Ansible then translates into specific commands and operations to achieve the automated task on the destination hosts.

Essentially the YAML you create is the equivalent of a script, think of YAML as a high-end language that is then translated into more complex, high-end commands to the destination server.

The difference between the Play and Playbook, is that a Play is more like a single chapter book (a single play, possibly something like just starting Apache).  A Playbook is made up of "multiple Plays" or chapters, that essentially execute a number of ordered plays, usually to achieve a larger and more complex task (eg. install LAMP, then create a DB for Wordpress, then install and configure Wordpress etc.. would be done as a Playbook normally).

What Does A Valid .YAML Play Look Like?

1.) It has a list of hosts (eg. a group like lamp that we created earlier).

2.) A list of task(s) to execute on the remote host(s)

*A good rule is that each section/block is indented by two spaces as you can see in the playbook below here.

Notice there are two spaces in front of become, tasks and then each task name is indented another 2 spaces and then adds " - name:" and then the actions below are inline with name (eg. you will see name and apt line up).

*Note that it is indenation sensitive and spacing sensitive, as in the real syntax is based on spacing and the dashes -

---
- hosts: lamp
  become: yes
  tasks:
    - name: install apache2
      apt: name=apache2 update_cache=yes state=latest

How do we execute a playbook? (use ansible-playbook)

ansible-playbook areebapache.yaml

 


 _____________
< PLAY [lamp] >
 -------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

 ______________
< TASK [setup] >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [host1]
 ________________________
< TASK [install apache2] >
 ------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||



changed: [host1]
 ____________
< PLAY RECAP >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

host1                      : ok=2    changed=1    unreachable=0    failed=0   
 

 You should be able to visit the IP of each host in the lamp group and see the default Apache2 Debian index

 

Format Quiz

Which playbook works and why, what is different about the two?  (Feel free to run each one).

#book 1

---
- hosts: lamp
  become: root
  tasks:
    - name: Install apache2
      apt: name=apache2 state=latest

 

#book 2

 ---
 - hosts: lamp
  become: root
  tasks:
     - name: Install apache2
       apt: name=apache2 state=latest

 

Stick To The Facts

Facts are like default, builtin environment variables that we can use to access information about the target:

Get facts by using "ansible NAME -m setup"

You can replace NAME with a specific host, all or a group name.

 

For example if we wanted the ipv4 address we would use this notation to get the nested "address" we added a dot after ansible_default_ipv4

        "ansible_default_ipv4": {
            "address": "10.0.2.16",

{{ansible_default_ipv4.address}}


host1 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "10.0.2.16"
        ],
        "ansible_all_ipv6_addresses": [
            "fec0::dcad:beff:feef:682",
            "fe80::dcad:beff:feef:682"
        ],
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "04/01/2014",
        "ansible_bios_version": "Ubuntu-1.8.2-1ubuntu1",
        "ansible_cmdline": {
            "BOOT_IMAGE": "/boot/vmlinuz-4.19.0-18-amd64",
            "quiet": true,
            "ro": true,
            "root": "UUID=78481d95-1470-42f0-bf4f-2dd841e4412a"
        },
        "ansible_date_time": {
            "date": "2022-01-25",
            "day": "25",
            "epoch": "1643136984",
            "hour": "13",
            "iso8601": "2022-01-25T18:56:24Z",
            "iso8601_basic": "20220125T135624284357",
            "iso8601_basic_short": "20220125T135624",
            "iso8601_micro": "2022-01-25T18:56:24.284585Z",
            "minute": "56",
            "month": "01",
            "second": "24",
            "time": "13:56:24",
            "tz": "EST",
            "tz_offset": "-0500",
            "weekday": "Tuesday",
            "weekday_number": "2",
            "weeknumber": "04",
            "year": "2022"
        },
        "ansible_default_ipv4": {
            "address": "10.0.2.16",
            "alias": "ens3",
            "broadcast": "10.0.2.255",
            "gateway": "10.0.2.2",
            "interface": "ens3",
            "macaddress": "de:ad:be:ef:06:82",
            "mtu": 1500,
            "netmask": "255.255.255.0",
            "network": "10.0.2.0",
            "type": "ether"
        },
        "ansible_default_ipv6": {
            "address": "fec0::dcad:beff:feef:682",
            "gateway": "fe80::2",
            "interface": "ens3",
            "macaddress": "de:ad:be:ef:06:82",
            "mtu": 1500,
            "prefix": "64",
            "scope": "site",
            "type": "ether"
        },
        "ansible_devices": {
            "fd0": {
                "holders": [],
                "host": "",
                "model": null,
                "partitions": {},
                "removable": "1",
                "rotational": "1",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "cfq",
                "sectors": "8",
                "sectorsize": "512",
                "size": "4.00 KB",
                "support_discard": "0",
                "vendor": null
            },
            "sr0": {
                "holders": [],
                "host": "IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]",
                "model": "QEMU DVD-ROM",
                "partitions": {},
                "removable": "1",
                "rotational": "1",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "mq-deadline",
                "sectors": "688128",
                "sectorsize": "2048",
                "size": "1.31 GB",
                "support_discard": "0",
                "vendor": "QEMU"
            },
            "vda": {
                "holders": [],
                "host": "SCSI storage controller: Red Hat, Inc Virtio block device",
                "model": null,
                "partitions": {
                    "vda1": {
                        "sectors": "18968576",
                        "sectorsize": 512,
                        "size": "9.04 GB",
                        "start": "2048"
                    },
                    "vda2": {
                        "sectors": "2",
                        "sectorsize": 512,
                        "size": "1.00 KB",
                        "start": "18972670"
                    },
                    "vda5": {
                        "sectors": "1996800",
                        "sectorsize": 512,
                        "size": "975.00 MB",
                        "start": "18972672"
                    }
                },
                "removable": "0",
                "rotational": "1",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "mq-deadline",
                "sectors": "20971520",
                "sectorsize": "512",
                "size": "10.00 GB",
                "support_discard": "0",
                "vendor": "0x1af4"
            }
        },
        "ansible_distribution": "Debian",
        "ansible_distribution_major_version": "10",
        "ansible_distribution_release": "buster",
        "ansible_distribution_version": "10.11",
        "ansible_dns": {
            "nameservers": [
                "10.0.2.3"
            ]
        },
        "ansible_domain": "ca",
        "ansible_ens3": {
            "active": true,
            "device": "ens3",
            "ipv4": {
                "address": "10.0.2.16",
                "broadcast": "10.0.2.255",
                "netmask": "255.255.255.0",
                "network": "10.0.2.0"
            },
            "ipv6": [
                {
                    "address": "fec0::dcad:beff:feef:682",
                    "prefix": "64",
                    "scope": "site"
                },
                {
                    "address": "fe80::dcad:beff:feef:682",
                    "prefix": "64",
                    "scope": "link"
                }
            ],
            "macaddress": "de:ad:be:ef:06:82",
            "module": "virtio_net",
            "mtu": 1500,
            "pciid": "virtio0",
            "promisc": false,
            "type": "ether"
        },
        "ansible_env": {
            "HOME": "/root",
            "LANG": "C",
            "LC_ALL": "C",
            "LC_MESSAGES": "C",
            "LOGNAME": "root",
            "MAIL": "/var/mail/root",
            "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "PWD": "/root",
            "SHELL": "/bin/bash",
            "SHLVL": "0",
            "SSH_CLIENT": "10.0.2.15 34260 22",
            "SSH_CONNECTION": "10.0.2.15 34260 10.0.2.16 22",
            "SSH_TTY": "/dev/pts/0",
            "TERM": "xterm",
            "USER": "root",
            "XDG_RUNTIME_DIR": "/run/user/0",
            "XDG_SESSION_CLASS": "user",
            "XDG_SESSION_ID": "79",
            "XDG_SESSION_TYPE": "tty",
            "_": "/bin/sh"
        },
        "ansible_fips": false,
        "ansible_form_factor": "Other",
        "ansible_fqdn": "areeb-ansible.ca",
        "ansible_gather_subset": [
            "hardware",
            "network",
            "virtual"
        ],
        "ansible_hostname": "areeb-ansible",
        "ansible_interfaces": [
            "lo",
            "ens3"
        ],
        "ansible_kernel": "4.19.0-18-amd64",
        "ansible_lo": {
            "active": true,
            "device": "lo",
            "ipv4": {
                "address": "127.0.0.1",
                "broadcast": "host",
                "netmask": "255.0.0.0",
                "network": "127.0.0.0"
            },
            "ipv6": [
                {
                    "address": "::1",
                    "prefix": "128",
                    "scope": "host"
                }
            ],
            "mtu": 65536,
            "promisc": false,
            "type": "loopback"
        },
        "ansible_lsb": {
            "codename": "buster",
            "description": "Debian GNU/Linux 10 (buster)",
            "id": "Debian",
            "major_release": "10",
            "release": "10"
        },
        "ansible_machine": "x86_64",
        "ansible_machine_id": "3c9b9946e31e46d39d7fc12c28fcf2c7",
        "ansible_memfree_mb": 3567,
        "ansible_memory_mb": {
            "nocache": {
                "free": 3738,
                "used": 208
            },
            "real": {
                "free": 3567,
                "total": 3946,
                "used": 379
            },
            "swap": {
                "cached": 0,
                "free": 974,
                "total": 974,
                "used": 0
            }
        },
        "ansible_memtotal_mb": 3946,
        "ansible_mounts": [
            {
                "device": "/dev/vda1",
                "fstype": "ext4",
                "mount": "/",
                "options": "rw,relatime,errors=remount-ro",
                "size_available": 7193808896,
                "size_total": 9492197376,
                "uuid": "78481d95-1470-42f0-bf4f-2dd841e4412a"
            }
        ],
        "ansible_nodename": "areeb-ansible",
        "ansible_os_family": "Debian",
        "ansible_pkg_mgr": "apt",
        "ansible_processor": [
            "GenuineIntel",
            "KVM @ 2.00GHz",
            "GenuineIntel",
            "KVM @ 2.00GHz",
            "GenuineIntel",
            "KVM @ 2.00GHz",
            "GenuineIntel",
            "KVM @ 2.00GHz",
            "GenuineIntel",
            "KVM @ 2.00GHz",
            "GenuineIntel",
            "KVM @ 2.00GHz"
        ],
        "ansible_processor_cores": 1,
        "ansible_processor_count": 6,
        "ansible_processor_threads_per_core": 1,
        "ansible_processor_vcpus": 6,
        "ansible_product_name": "Standard PC (i440FX + PIIX, 1996)",
        "ansible_product_serial": "NA",
        "ansible_product_uuid": "NA",
        "ansible_product_version": "pc-i440fx-xenial",
        "ansible_python": {
            "executable": "/usr/bin/python",
            "has_sslcontext": true,
            "type": "CPython",
            "version": {
                "major": 2,
                "micro": 16,
                "minor": 7,
                "releaselevel": "final",
                "serial": 0
            },
            "version_info": [
                2,
                7,
                16,
                "final",
                0
            ]
        },
        "ansible_python_version": "2.7.16",
        "ansible_selinux": false,
        "ansible_service_mgr": "systemd",
        "ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAeJa04CWRa6N2zV+hKt+utDxOVI/23Zntb815bXz+qqK/XZsFoIEL7jYUZFlifJFAxmWgE9CJ6Vtn/4DzHnDx4=",
        "ansible_ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAIBhlJVY9PgACISzzqwviVOgeosQBWAKULGY4UsSRzbKJ",
        "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQC3rT0zeZS8TS7+XmYIt2aTAK1L/RHAhbJ54+UqpyXRJ0CmlQZySdh6ug65lK6VYMQMrmxC8niKVQ/1pSia2swJjb/qSyRlEUGnGYR8xGmVG1I99OcH1301E3nzvmJw44bcRKx/zf5CYf16X8KAPoNg9EsagvjGB5CYz3b5/x4fJmwJ2Qp7rPgNvDYp2GIqRcCXvtfui1vhf2eSqzDLFeK0nFfGqMj8mrBZn2UPRtJNKd3aFWyTqEePKT3Mm1B1cBgdh3St76X7kw0dKuY1BUqtZAOOGEUw84c/vLAeRmQx5yh78COf6ys5jltj6MBwCZ2iSTLAapRxxh13LQ7oAgIh",
        "ansible_swapfree_mb": 974,
        "ansible_swaptotal_mb": 974,
        "ansible_system": "Linux",
        "ansible_system_capabilities": [
            "cap_chown",
            "cap_dac_override",
            "cap_dac_read_search",
            "cap_fowner",
            "cap_fsetid",
            "cap_kill",
            "cap_setgid",
            "cap_setuid",
            "cap_setpcap",
            "cap_linux_immutable",
            "cap_net_bind_service",
            "cap_net_broadcast",
            "cap_net_admin",
            "cap_net_raw",
            "cap_ipc_lock",
            "cap_ipc_owner",
            "cap_sys_module",
            "cap_sys_rawio",
            "cap_sys_chroot",
            "cap_sys_ptrace",
            "cap_sys_pacct",
            "cap_sys_admin",
            "cap_sys_boot",
            "cap_sys_nice",
            "cap_sys_resource",
            "cap_sys_time",
            "cap_sys_tty_config",
            "cap_mknod",
            "cap_lease",
            "cap_audit_write",
            "cap_audit_control",
            "cap_setfcap",
            "cap_mac_override",
            "cap_mac_admin",
            "cap_syslog",
            "cap_wake_alarm",
            "cap_block_suspend",
            "cap_audit_read+ep"
        ],
        "ansible_system_capabilities_enforced": "True",
        "ansible_system_vendor": "QEMU",
        "ansible_uptime_seconds": 77628,
        "ansible_user_dir": "/root",
        "ansible_user_gecos": "root",
        "ansible_user_gid": 0,
        "ansible_user_id": "root",
        "ansible_user_shell": "/bin/bash",
        "ansible_user_uid": 0,
        "ansible_userspace_architecture": "x86_64",
        "ansible_userspace_bits": "64",
        "ansible_virtualization_role": "guest",
        "ansible_virtualization_type": "kvm",
        "module_setup": true
    },
    "changed": false
}
 

Expanding To A Playbook, Let's install the full LAMP stack with a custom index.html!

---
- hosts: lamp
  become: root

#note we can put variables here under vars: and we can override variables from group_vars or elsewhere by redefining existing variables with new values (eg. ansible_ssh_user: somefakeuser).  You can also even use variable placeholders within the .yml file later on eg. to specify a file path like src: "/some/path/{{thevarname}}"

  vars:
     avarhere: hellothere


  tasks:
   - name: Install apache2
     apt: name=apache2 state=latest

   - name: Install MySQL (really MariaDB now)
     apt: name=mariadb-server state=latest

   - name: Install php
     apt: name=php state=latest

   - name: Install php-cgi
     apt: name=php-cgi state=latest

   - name: Install php-cli
     apt: name=php-cli state=latest

   - name: Install apache2 php module
     apt: name=libapache2-mod-php state=latest

   - name: Install php-mysql
     apt: name=php-mysql state=latest

 

Expand Our Playbook To Install Wordpress

Simply add on more tasks to your existing playbook above.

Wordpress requires a database like MariaDB and PHP (installed in our original playbook). 

But what else is needed?

  1. A database and user with privileges to create tables and insert records.
  2. The wordpress install files downloaded/extracted to /var/www/html (or whatever our vhost path is)
  3. A valid wp-config.php file which has our database info from #1.
  4. Define the following variables in your Playbook (modify for your needs):
  5.   wpdbname: rttdbname
      wpdbuser: rttdbuser
      wpdbpass: rttinsecurepass
      wpdbhost: localhost
      wppath: "/var/www/html"
     

#MySQL config
   - name: Create MySQL Database
     mysql_db:
       name: "{{wpdbname}}"
#     ignore_errors: yes

   - name: Create DB user/pass and give the user all privileges
     mysql_user:
       name: "{{wpdbuser}}"
       password: "{{wpdbpass}}"
       priv: '{{wpdbname}}.*:ALL'
       state: present
#     ignore_errors: yes

 

#Wordpress stuff
   - name: Download and tar -zxvf wordpress
     unarchive:
        src: https://wordpress.org/latest.tar.gz
        remote_src: yes
        dest: "{{ wppath }}"
        extra_opts: [--strip-components=1]
        #creates: "{{ wppath }}"

   - name: Set permissions
     file:
        path: "{{wppath}}"
        state: directory
        recurse: yes
        owner: www-data
        group: www-data
 
   - name: copy the config file wp-config-sample.php to wp-config.php so we can edit it
     command: mv {{wppath}}/wp-config-sample.php {{wppath}}/wp-config.php #creates={{wppath}}/wp-config.php

 
   - name: Update WordPress config file
     lineinfile:
        path: "{{wppath}}/wp-config.php"
        regexp: "{{item.regexp}}"
        line: "{{item.line}}"
     with_items:
       - {'regexp': "define\\( 'DB_NAME', '(.)+' \\);", 'line': "define( 'DB_NAME', '{{wpdbname}}' );"}
       - {'regexp': "define\\( 'DB_USER', '(.)+' \\);", 'line': "define( 'DB_USER', '{{wpdbuser}}' );"}
       - {'regexp': "define\\( 'DB_PASSWORD', '(.)+' \\);", 'line': "define( 'DB_PASSWORD', '{{wpdbpass}}' );"}

 

Full Playbook To Install LAMP + Wordpress in Ansible on a Debian/Mint/Ubuntu Based Target

---
- hosts: all
  become: yes
#note this Playbook was made for Debian 10, adjustments for other distros will need to be made
# we can put variables here too that work in addition to what is in group_vars
  ignore_errors: yes
  vars:
     #If you are getting weird errors about Python modules missing that are really there, perhaps the wrong version of Python is executing (eg. python2.7 instead of python3).
     #You can fix it by setting the variable below to the python you need
     #ansible_python_interpreter: "/usr/bin/python3"
     ansible_ssh_user: root
     wpdbname: rttdbname
     wpdbuser: rttdbuser
     wpdbpass: rttinsecurepass
     wpdbhost: localhost
     wppath: "/var/www/html"

  tasks:
   - name: Install apache2
     apt: name=apache2 state=latest
     
   - name: Install MySQL (really MariaDB now)
     apt: name=mariadb-server state=latest
   #if you are executing as Python3 then you need python3- instead of python-
   - name: Install MySQL python module
     apt: name=python-mysqldb state=latest
   #if you are executing as Python3 then you need python3- instead of python-
   - name: Install PyMySQL
     apt: name=python-pymysql

   - name: Install php
     apt: name=php state=latest

   - name: Install apache2 php module
     apt: name=libapache2-mod-php state=latest

   - name: Install php-mysql
     apt: name=php-mysql state=latest
     #notify means that we call a handler after this tasks completes
     #in this case, remember that PHP in Apache WILL NOT work until apache2 is restarted after the installation of PHP
     notify:
       - restart apache2

#mysql_db on some distros acts funny and will throw errors about being unable to login or connect
#if this happens it could be that you need to set the login_unix_socket variable to the correct mysqld.sock location
#MySQL config
   - name: Create MySQL Database
     mysql_db:
       name: "{{wpdbname}}"
       #the below depends on the OS eg. some socks are in /var/lib/mysql
       login_unix_socket: /run/mysqld/mysqld.sock
#     ignore_errors: yes

   #mysql_user on versions as recent as 2.7 has a bug where it will hardcode trying to connect to the "mysql" database which breaks things of course
   - name: Create DB user/pass and give the user all privileges
     mysql_user:
       name: "{{wpdbuser}}"
       password: "{{wpdbpass}}"
       priv: '{{wpdbname}}.*:ALL'
       state: present
       #the below depends on the OS eg. some socks are in /var/lib/mysql
       login_unix_socket: /run/mysqld/mysqld.sock
#     ignore_errors: yes

   - name: enable Apache2 service
     service: name=apache2 enabled=yes

#Wordpress stuff
   - name: Download and tar -zxvf wordpress
     unarchive:
        src: https://wordpress.org/latest.tar.gz
        remote_src: yes
        dest: "{{ wppath }}"
        extra_opts: [--strip-components=1]
        #creates: "{{ wppath }}"

   - name: Set permissions
     file:
        path: "{{wppath}}"
        state: directory
        recurse: yes
        owner: www-data
        group: www-data
 
   - name: copy the config file wp-config-sample.php to wp-config.php so we can edit it
     command: mv {{wppath}}/wp-config-sample.php {{wppath}}/wp-config.php #creates={{wppath}}/wp-config.php
     become: yes
 
   - name: Update WordPress config file
     lineinfile:
        path: "{{wppath}}/wp-config.php"
        regexp: "{{item.regexp}}"
        line: "{{item.line}}"
     with_items:
       - {'regexp': "define\\( 'DB_NAME', '(.)+' \\);", 'line': "define( 'DB_NAME', '{{wpdbname}}' );"}
       - {'regexp': "define\\( 'DB_USER', '(.)+' \\);", 'line': "define( 'DB_USER', '{{wpdbuser}}' );"}
       - {'regexp': "define\\( 'DB_PASSWORD', '(.)+' \\);", 'line': "define( 'DB_PASSWORD', '{{wpdbpass}}' );"}
     
  #this is the handlers section, these are tasks that executed after the successful execution of a task that refers to the handler task below eg. "restart apache2" gets notified by the task near the top called "Install apache2"
  handlers:
   - name: restart apache2
     service: name=apache2 state=restarted 

 

Wordpress Playbook Errors

The above is fine on Debian 10 but on Mint 20/Ubuntu 20 you may get this:

TASK [Create MySQL Database] ***********************************************************************************************************************************************************************************

fatal: [areebhost01]: FAILED! => {"changed": false, "msg": "The PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) module is required."}
...ignoring

You'll need to install the "python3-pymysql" package or "python3-mysqldb" package to solve this:

   - name: Install PyMySQL
     apt: name=python3-pymysql

If the above is not an issue or it is still not working, you may be executing the wrong version of Python on your target.  The issue on a target like Mint 20 is that it may be running python2.7, yet there are no modules for python2.7 built-in to Mint/Ubuntu 20.  So what you need to do is set a variable in your playbook to use python3 like this:

#change the /usr/bin/python3 to the location of the target's python3 if different

  vars:
     ansible_python_interpreter: "/usr/bin/python3"

This is caused by the bug present until at least Ansible 2.7 where mysql_user module is hardcoded to only use the mysql database even though we specify a different one of course.

TASK [Create DB user/pass and give the user all privileges] **********************************************************************************************************************************
fatal: [areebhost01]: FAILED! => {"changed": false, "msg": "unable to connect to database, check login_user and login_password are correct or /root/.my.cnf has the credentials. Exception message: (1049, \"Unknown database 'mysql'\")"}


The solution is to upgrade to a newer version of Ansible (confirmed taht version 2.9 in Ubuntu/Mint 20 does not have the bug, nor does 2.10 in Debian 11).

 


This issue below, can be caused by not having the correct socket for MySQL/MariaDB (this assumes that you have successfully installed a default standard MySQL/MariaDB server and that it is running on the target):

TASK [Create MySQL Database] *****************************************************************************************************************************************************************
fatal: [debian10]: FAILED! => {"changed": false, "msg": "unable to find /root/.my.cnf. Exception message: (1698, u\"Access denied for user 'root'@'localhost'\")"}
...ignoring

Adding this variable as part of the task usually fixes it:

login_unix_socket: /run/mysqld/mysqld.sock

#MySQL config
   - name: Create MySQL Database
     mysql_db:
       name: "{{wpdbname}}"
       #the below depends on the OS eg. some socks are in /var/lib/mysql
       login_unix_socket: /run/mysqld/mysqld.sock
#     ignore_errors: yes

Make It 'More Fancy'

We can use conditionals (eg like an if statement equivalent) to change the behavior.  For example the playbook above installs python-mysqldb on the target, however it works on Debian 10 but not Debian 11 (since that package is deprecrated so we need to install python3-mysqldb instead).  How can we do it? 

   #install python-mysqldb only if we are Debian 10
   - name: Install MySQL python2 module Debian 10
     apt: name=python-mysqldb state=latest
     when: (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "10")

   - name: Install MySQL python3 module Debian 11
     apt: name=python3-mysqldb state=latest
     when: (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "11")

 

Seeing it in action, you will see that only one of the two tasks is executed which is the Debian 11 task since the conditional of when matched Debian 11.

More on Ansible conditionals from the documentation.

Could we be more efficient?

It would also be wise under the apt: module to add "update_cache=yes" to make sure the packages are up to date.

We could put all of the apt install tasks from the original example into a single task like this:

 

---
  - hosts: lamp
    become: yes
    tasks:
     - name: install LAMP
       apt: name={{item}} update_cache=yes state=latest
       with_items:
         - apache2
         - mariadb-server
         - php
         - php-cgi
         - php-cli
         - libapache2-mod-php
         - php-mysql

 


#note that the below won't work on older Ansible (eg. 2.1 and will throw a formatting error).  If that happens, use the above playbook.  I find the style above to be less prone to typos.

ERROR! The field 'loop' is supposed to be a string type, however the incoming data structure is a

The error appears to have been in '/home/markmenow/Ansible/lamp-fullloop.yaml': line 5, column 9, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

    tasks:
      - name: install LAMP
        ^ here
 

---
  - hosts: lamp
    become: yes
    tasks:
      - name: install LAMP
        apt: name={{item}} state=latest
        loop: [ 'apache2', 'mariadb-server', 'php', 'php-cgi', 'php-cli', 'libapache2-mod-php', 'php-mysql' ]

The only downside is that it can be harder to troubleshoot if something fails, since we are installing all of the items as a single apt command in a single task.

What Happens If There Is An Error On A Task?

By default, Ansible will stop executing the playbook and not move on to the next task.  For some reasons there are times where this is no the desirable or correct behavior.

https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html

You can tell an individual task to ignore errors and continue:

We just add ignore_errors at the same indentation level as our module.

   - name: Create DB user/pass and give the user all privileges
     mysql_user:
       name: "{{wpdbuser}}"
       password: "{{wpdbpass}}"
       priv: '{{wpdbname}}.*:ALL'
       state: present
     ignore_errors: yes

 

We could also  do a universal ignore_errors: yes which would apply to all tasks, but this is normally not what you'd want.

---
  - hosts: lamp
    become: yes
    ignore_errors: yes

But wait, don't we need to restart apache to make PHP work, how do we do that?

 

Handlers - Add this to the end of the above playbook.

  handlers:
   - name: restart apache2
     service: name=apache2 state=restarted

 

More on handlers from Ansible: https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html

How do we enable a service so it works upon boot?

   - name: enable Apache2 service
     service: name=apache2 enabled=yes

How can we copy a file?

   - name: Copy some file
     copy:
        src: "files/somefile.ext"
        dest: "/var/some/dest/path/"

 
How can we tell Apache to use a custom index.html?

template means it is a jinja2 file which causes Ansible to replace variables based on the placeholder specified with double braces (eg {{varname}} ).  If the varname is not found Ansible will throw an error and not replace the undefined variable and cause the playbook to fail (from the point that the template is called):

fatal: [host1]: FAILED! => {"changed": false, "failed": true, "msg": "AnsibleUndefinedVariable: 'auserr' is undefined"}
 


   - name: Copy index test page
     template:
        src: "files/index.html.j2"
        dest: "/var/www/html/index.html"


To make this work you would need to define the variables in the index.html above within your group_vars file or within the .yml playbook file.

 

How can we enable an Apache module?

-name: Apache Module - mod_rewrite
  apache2_module:
    state: present
    name: rewrite

 

How can we enable htaccess?

Inside your files directory (based on the relative dir) place these contents into a file called "htaccess.conf"

*Note you would change the /var/www to another path such as /www/vhosts/ if your vhost directory was different than Apache's default /var/www

 

 

 

 

Create a new task to actually copy the htaccess enable file into Apache2's config directory on the target server:

     - name: Enable htaccess support in /var/www
       template:
         src: "files/htaccess.conf"
         dest: "/etc/apache2/sites-available/htaccess.conf"

Don't forget to symlink it to sites-enabled (which actually enables the htaccess
 

- name: Enable the htaccess.conf by copying to sites-enabled
  file:
    src: /etc/apache2/sites-available/htaccess.conf
    dest: /etc/apache2/sites-enabled/htaccess.conf
    state: link

 

Fun Stuff, Random ASCII art (cowsay cows):

Edit /etc/ansible/ansible.cfg

Set this line: cow_selection = random

 

References:

https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html

https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html

https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html

https://docs.ansible.com/ansible/2.3/playbooks_variables.html

https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html

https://github.com/ansible/ansible-examples

https://docs.ansible.com/ansible-core/devel/reference_appendices/YAMLSyntax.html

https://docs.ansible.com/ansible-core/devel/reference_appendices/playbooks_keywords.html

https://docs.ansible.com/

 

 

 

 

 

 

 

 

 


Tags:

ansible, tutorial, playbook, install, deploy, wordpress, server, requirements, linux, eg, vm, vbox, vmware, proxmox, anisble, debian, ubuntu, mint, requires, controller, quot, playbooks, yaml, sudo, apt, lists, dependency, additional, packages, installed, ieee, python, jinja, netaddr, ipython, docs, recommended, selinux, upgraded, newly, kb, archives, mb, disk, http, archive, xenial, updates, amd, backports, fetched, selecting, previously, unselected, database, directories, currently, preparing, unpack, _, _all, deb, unpacking, yaml_, _amd, data_, netaddr_, ansible_, processing, triggers, db, hosts, vi, etc, ip, destination, ansible_ssh_host, ssh, username, mkdir, group_vars, abcgroup, filename, ansible_ssh_user, ping, specified, connectivity, didn, manually, unreachable, msg, via, automated, prompt, password, ll, authentication, auth, pong, uptime, shell, commands, rc, buff, cache, mem, swap, analogy, theatre, inspired, slang, config, translates, operations, achieve, task, essentially, equivalent, translated, apache, multiple, chapters, execute, larger, configure, valid, indenation, spacing, syntax, dashes, tasks, update_cache, areebapache, _____________, __, oo, _______, ______________, ok, ________________________, ____________, recap, default, index, format, quiz, expanding, stack, custom, html, mysql, mariadb, php, cgi, cli, module, libapache, mod, restart, handlers, restarted, template, src, dest, var, www, references, https, playbooks_variables, collections, builtin, github, examples, devel, reference_appendices, yamlsyntax, playbooks_keywords,

Latest Articles

  • FreePBX 17 How To Add a Trunk
  • Docker Container Onboot Policy - How to make sure a container is always running
  • FreePBX 17 How To Add Phones / Extensions and Register
  • Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes. solution
  • Cisco How To Use a Third Party SIP Phone (eg. Avaya, 3CX)
  • Cisco Unified Communication Manager (CUCM) - How To Add Phones
  • pptp / pptpd not working in DD-WRT iptables / router
  • systemd-journald high memory usage solution
  • How to Install FreePBX 17 in Linux Debian Ubuntu Mint Guide
  • How To Install Cisco's CUCM (Cisco Unified Communication Manager) 12 Guide
  • Linux Ubuntu Redhat How To Extract Images from PDF
  • Linux and Windows Dual Boot Issue NIC Won't work After Booting Windows
  • Cisco CME How To Enable ACD hunt groups
  • How to install gns3 on Linux Ubuntu Mint
  • How to convert audio for Asterisk .wav format
  • Using Cisco CME Router with Asterisk as a dial-peer
  • Cisco CME How To Configure SIP Trunk VOIP
  • Virtualbox host Only Network Error Failed to save host network interface parameter - Cannot change gateway IP of host only network
  • Cisco CME and C7200 Router Testing and Learning Environment on Ubuntu 20+ Setup Tutorial Guide
  • Abusive IP ranges blacklist