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.

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

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
}

 

But since ansible is automated there is no way you could run this command and expect ansible to prompt for the password.  You'll need ssh key based authentication like this link.

You could also use ssh-copy-id to setup passwordless auth by key.

*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)

*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
     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}}' );"}

 

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

---
- hosts: all
  become: root
# we can put variables here too that work in addition to what is in group_vars
  ignore_errors: yes
  vars:
     auser: hellothere
     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
     notify:
       - restart apache2
   - name: Install MySQL (really MariaDB now)
     apt: name=mariadb-server state=latest

   - name: Install MySQL python module
     apt: name=python-mysqldb state=latest


   - 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

#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

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

   - 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}}' );"}
     

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

 

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

  • CentOS 7 / 8 cannot boot with with mdadm RAID array solution
  • How To Add Default Gateway in Linux using the ip route command routing
  • Error: Failed to download metadata for repo 'appstream': Cannot prepare internal mirrorlist: No URLs in mirrorlist Solution for Centos8 yum package install error
  • md mdadm array inactive how to start and activate the RAID array
  • Loaded: masked (Reason: Unit hostapd.service is masked.) Solution in Linux Debian Mint Ubuntu
  • Linux Mint Ubuntu Ubiquity Installer Bug EFI Installed To Wrong Partition Solution
  • Libreoffice Impress How To Change The Color of Links
  • ecryptfs How To Backup / Migrate Linux Mint Ubuntu Debian system ecryptfs properly and restore access
  • i915 nouveau Nvidia GPU not starting lightdm Xorg failing solution for Could not determine valid watermarks for inherited state
  • br0: received packet on bond0 with own address as source address Linux Solution Mint Debian Redhat CentOS bridge bridging
  • Debian Mint Ubuntu Howto Disable Network Manager
  • AMD GPU Xorg Won't Start [3576284.324] (EE) Segmentation fault at address 0x0 [3576284.325] (EE) Fatal server error: [3576284.325] (EE) Caught signal 11 (Segmentation fault). Server aborting
  • symbol 'grub_calloc' not found grub boot error solution / fix
  • /var/log/journal huge/too large solution in Debian Mint Ubuntu Linux Howot Fix
  • Libreoffice Calc Opens CSV Spreadsheet File as Asian Language/Chinese Characters Solution Fix
  • RTL8821AU Setup Configure Wifi Realtek 8821 in Linux Debian Mint Ubuntu Howto
  • How To Tell Which Repository a Package Comes From Debian Mint Ubuntu
  • How To Reload All Kernel Modules And List Required Moduels for Each Device - Linux Mint Debian Ubuntu Troubleshooting
  • Debian Ubuntu Mint How To Change Default Display Manager
  • Ubuntu Mint Debian Howto Execute Command / Script / Program Upon Wakeup From Sleep