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 add-apt-repository ppa:ansible/ansible
sudo apt update

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

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


   - 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

  • How To Upgrade Debian 8,9,10 to Debian 12 Bookworm
  • Linux dhcp dhclient Mint Redhat Ubuntu Debian How To Use Local Domain DNS Server Instead of ISPs
  • Docker dockerd swarm high CPU usage cause solution
  • Docker Minimum Requirements/How Efficient is Docker? How Much Memory Does Dockerd Use?
  • qemu-nbd: Failed to set NBD socket solution qemu-nbd: Disconnect client, due to: Failed to read request: Unexpected end-of-file before all bytes were read
  • apache2 httpd apache server will not start [pid 22449:tid 139972160445760] AH00052: child pid 23248 exit signal Aborted (6) solution Mint Debian Ubuntu Redhat
  • How to use the FTDI USB serial cable to RJ45 adapter to connect to the console on Cisco/Juniper Switch Router Firewall in Linux Ubuntu Debian Redhat
  • How To Setup Python3 in Ubuntu Docker Image for AI Deep Learning
  • How to Configure NVIDIA GPUs with Docker on Ubuntu: A Comprehensive Guide for AI Deep Learning CUDA Solution
  • Linux Ubuntu Mint how to check nameservers when /etc/resolv.conf disabled solution
  • Docker cannot work on other overlayfs filesystems such as ecryptfs won't start overlayfs: filesystem on '/home/docker/overlay2/check-overlayfs-support130645871/upper' not supported as upperdir
  • Linux How To Access Original Contents of Directory Mounted Debian Mint CentOS Redhat Solution
  • ecryptfs how to manually encrypt your existing home directory or other directory
  • How to Reset CIPC Cisco IP Communicator for CME CUCM CallManager
  • Internet Explorer Cannot Download File "Your security settings do not allow for this file to be downloaded." Security Settings Solution
  • Linux How To Upgrade To The Latest Kernel Debian Mint Ubuntu
  • Firefox how to restore and backup saved passwords and history which files/location
  • Linux How To echo as root solution to use tee permission denied solution Ubuntu Debian Mint Redhat CentOS
  • Linux how to keep command line bash process running if you are disconnected or need to logout of SSH remotely
  • Linux swapping too much? How to check the swappiness and stop swapping