Automation of LAMP (Linux, Apache, MySQL, PHP) stack deployment using Bash Script in Ansible
Tags: ansible, lamp, bashscript, laravel
Published: 2024-04-27 16:30:30

##Introduction

 A Bash Script is a text file with several commands written in the Bash shell language. The Bash shell carries out operations one by one when you run the script. This allows you to automate repetitive tasks, manage files, and configure systems.

Ansible is an open-source automation tool for configuration and
management of IT infrastructures. It allows for the easy
management of numerous servers using YAML language to describe
automated tasks. Ansible operates agentlessly, meaning there is no
need to install additional software on the nodes it manages.

----
You will appreciate the use of Ansible as a tool if you find out about things it does to make your IT job easy. Here are some advantages:

- Ease of use and simplicity: YAML is a human-readable language used to write playbooks (configuration files).
- Strong and adaptable: Ansible is capable of managing challenging jobs and changing with the times.
- Agentless: Management and security are made simpler by not requiring software to be installed on the controlled nodes.
- Idempotent: Playbooks can be run repeatedly without producing unpleasant or unexpected results.
- Huge support and community: Ansible has a large and active community, as well as a wealth of modules and resources.


##Context

Just so we understand what we are going to be doing in this project, I will explain all that it entails.

- We are going to automate the provisioning of two Ubuntu-based servers, named “master” and “slave”, using Vagrant. 
- On the Master node, we will create a bash script to automate the deployment of a LAMP (Linux, Apache, MySQL, PHP) stack.
- The bash script we create above should clone a PHP application from [GitHub](https://github.com/laravel/laravel), install all necessary packages, and configure Apache web server and MySQL.
- We will ensure the bash script is reusable and readable.
- Using Ansible Playbook, we will execute the bash script on the Slave node and verify that the PHP application is accessible through the Virtual Machine’s IP address.
- We will also create a cron job to check the server’s uptime every 12 am.


##Steps

###Setting Up Virtual Machines

- To create a Virtual Machine, Check these resources for [Ubuntu](https://ubuntu.com/tutorials/how-to-run-ubuntu-desktop-on-a-virtual-machine-using-virtualbox#1-overview).

- You should have a Vagrantfile for the Virtual Machines saved in your local file folder just like mine below after you start your machine. But if you don't I can walk you through it. Remember you need to create 2 Ubuntu servers named "master" and "slave". Open your Virtual box down as you will need it open to start your Virtual Machines. Create two folders and name them "master" and "slave".


![folders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rkxmjnhwvvao9v7wq8ap.png)



- Open the folders with Gitbash. Let us start with the "master" folder.


![gitbash](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tqcrc4j5liiwrg03m8a.png)




- Next is to Initialize your master Ubuntu machine. After opening the "master" folder with git bash, run:

```
vagrant init Ubuntu/focal64
```


![vagrant init master](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19r7n91tj69yvikga1iz.png)



- Open the `Vagrantfile` and make some edits to the configuration file using the command `nano vagrantfile`. It would help if you had something like this.

![vagrantfile](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yfor67itsif16ys07y9v.png)

- Go ahead to paste the configuration below just under `Vagrant.configure("2") do |config|` like the image below. Also, add `config.ssh.insert_key = false` under `config.vm.box = ubuntu/focal64`. Press "crtl+O" to save, then "enter" and "ctrl+x" to exit the vagrantfile.

```
# Provision master Node
config.vm.define "master" do |master|
master.vm.box = "ubuntu/focal64"
master.vm.hostname = "master"
master.vm.network "private_network", ip: "192.168.33.10", type: "dhcp"
end

```
NOTE: I have circled the aspects you should the various commands under. Pay attention to them.


![vagrantfile](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tw2jry0v1pw6xzvux13o.png)






Then run the following command to start the machine. It would be best if you had something like the image below towards the end after running the command.

```
vagrant up
```


![vagrant up master](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rob6te8xu2l6649o5d0t.png)

- Connect the virtual machine by running:

```
vagrant ssh
```

![vagrant ssh](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cch48p1ullz2xc80r16k.png)

- Now that you have logged into your "masters" virtual machine, follow the same procedure for the "slave" virtual machine. You should have something like the screenshot below by the time you ssh into your "slave" virtual machine.

![slave machine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgy620qc9b38l2xcxfao.png)



NOTE: Every other thing is the same for the procedure except for the configuration in the vagrant file which should be under `Vagrant.configure("2") do |config|`. Use the command below under the `Vagrant.configure("2") do |config|`.


```
# Provision Slave Node
config.vm.define "slave" do |slave|
slave.vm.box = "ubuntu/focal64"
slave.vm.hostname = "slave"
slave.vm.network "private_network", ip: "192.168.33.11", type: "dhcp"
end
```


##Connect the "master" and "slave" machine by _ssh-ing _into each of them

- Use the command `ip a` to check for the IP address of each of the virtual machines. Below is that of the "master's" virtual machine. The circled one is the IP address for the "master" virtual machine. Do the same for the "slave" virtual machine. I have underlined each one for you to know what virtual machine owns each IP address. Also, note that your IP address will be different from mine.


![IP address for master](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fmvqj5sl1nlq1p6uqpne.png)

![IP address for slave](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gpz6zl0hlqhqlaj8ilyf.png)

- On your "master" Virtual Machine, write `ssh-keygen` to generate a private & public key. When you run the command you will see 
`Enter file in which to save the key (/home/vagrant/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:`
Keep pressing the "enter" key when those 3 lines appear as seen below.  

![master key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1sy4xf095bedmeiu0i6l.png)



- We will repeat the above step for the "slave" virtual machine too and should look like what is below.


![slave key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsdu0x6jghqwpri6b4e1.png)



- In the "master" virtual machine, change the directory into ssh with the command `cd .ssh` and then `ls` to list all the files in the ssh directory. You will see something like what is below.

![cd .ssh](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azsemm3tqm0kriyh3jhg.png)

- Open the content of  id_rsa.pub and copy it to save it somewhere for later use. That is where your public key is and that is what you are copying. Use this command to open it `cat id_rsa.pub`. The key would be long and look like something below.


![public key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zga3e9pjjvv8wx1vbyo0.png)


- Change the directory to .ssh with the command cd .ssh just as we did in the "master" virtual machine. Create and open a file in the .ssh directory of the "slave" machine at once so that the public key you copied from the "masters" machine and saved somewhere, you will paste it in the created file. I named my file 'public'. Run the command nano public and paste the public key from the "master" virtual machine here. After pasting all the public keys correctly, press 'crtl+O' to save, press 'enter', and then press 'ctrl+X' to exit.

- Then run the command `cat public >> authorized_keys` to copy the public keys into the authorized_keys file from the file named public in the "slave" machine. Run the command `cat authorized_keys` to see the content of the copied public keys of the "masters" virtual machine and the "slave" virtual machine. Below is an image of both the "masters" and "slave" public keys.



![public keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1k2xwo345aagpgn040yn.png)


- Connect the "master" virtual machine to the "slave" virtual machine by using ssh to connect the IP address of the "slave" machine to the "master" machine. First of all, run the command `ip a` to know the IP addresses in each machine. Then run the command ssh vagrant@slave ip in the "master" virtual machine. It should look like this `ssh vagrant@192.168.33.9` Make sure to replace the IP address with your own "slave" IP address. 
Whatever it prompts you, type 'yes'. From the screenshot, you can see that we can connect to our "slave" machine from the "master" machine.

![master sshing into slave](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h313l3hb5mt19cnvql61.png)


---------
## Creating the Bash Script and Ansible to automate the provisioning of the two Ubuntu-based Virtual Machines we've created.

- In your "master" virtual machine, create your script that will automate the deployment of LAMP (Linux, Apache, MySQL, PHP) stack.

- Name your script anything of your choice with the .sh extension. I will be naming mine LAMP.sh. Use the command `touch LAMP.sh` to create your script. run `ls` to see if the script file you ran is showing.

![create script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8ybz9t94pauwzz8h2vy.png)
- Run `nano LAMP.sh` to open the file script and write your script. Please copy and paste this script inside it. Each line of the command has a comment with a brief explanation with a '#' symbol in front of it. I made sure to pick each line of commands one by one to run before compiling it into a full script below.

```
#!/bin/bash

# Define variables for paths and IP address
LARAVEL_DIR="/var/www/html/laravel"
LARAVEL_CONF="/etc/apache2/sites-available/laravel.conf"
VIRTUAL_HOST="192.168.33.9"

# Update all package index and upgrade packages
sudo apt update
sudo apt upgrade -y
echo "Done with upgrade package"

# Update the PHP repository
sudo add-apt-repository -y ppa:ondrej/php
echo "Done with php repo update"

# Install Apache
sudo apt install -y apache2
sudo systemctl enable apache2
echo "Done with Apache installation"

# Install MySQL
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password'
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password'
sudo apt install -y mysql-server
echo "Done with MySQL installation"

# Install PHP 8.2 and 8.3 with necessary extensions
sudo apt install -y php libapache2-mod-php php-mysql php8.2 php8.2-curl php8.2-dom php8.2-xml php8.2-mysql php8.2-sqlite3 php8.3 php8.3-curl php8.3-dom php8.3-xml php8.3-mysql php8.3-sqlite3
echo "Done with PHP 8.2 and 8.3 installation"

# Run MySQL secure installation
expect <<EOF
spawn sudo mysql_secure_installation
expect "Would you like to setup VALIDATE PASSWORD component?"
send "y\r"
expect {
    "Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG" {
        send "1\r"
        exp_continue
    }
    "Remove anonymous users?" {
        send "y\r"
        exp_continue
    }
    "Disallow root login remotely?" {
        send "n\r"
        exp_continue
    }
    "Remove test database and access to it?" {
        send "y\r"
        exp_continue
    }
    "Reload privilege tables now?" {
        send "y\r"
        exp_continue
    }
}
EOF
echo "Done with MySQL secure installation"

# Restart Apache
sudo systemctl restart apache2
echo "Done with Apache restart"

# Install Git
sudo apt install -y git
echo "Done with Git installation"

# Clone Laravel repository
sudo git clone https://github.com/laravel/laravel $LARAVEL_DIR
echo "Done with cloning Laravel repository"

# Change directory to Laravel folder
cd $LARAVEL_DIR
echo "Changed directory to $LARAVEL_DIR"

# Install Composer
sudo apt install -y composer
echo "Done with installing Composer"

# Upgrade Composer to version 2
sudo php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
sudo php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
sudo php composer-setup.php --install-dir /usr/bin --filename composer
echo "Done with upgrading Composer to version 2"

# Use Composer to install dependencies
yes | sudo composer install
echo "Done installing Composer dependencies"

# Copy Laravel configuration file and set permissions
sudo cp .env.example .env
sudo chown www-data:www-data .env
sudo chmod 640 .env
echo "Done copying Laravel configuration file and setting permissions"

# Create virtual host in /etc/apache2/sites-available
sudo tee $LARAVEL_CONF >/dev/null <<EOF
<VirtualHost *:80>
    ServerName $VIRTUAL_HOST
    ServerAlias *
    DocumentRoot $LARAVEL_DIR/public

    <Directory $LARAVEL_DIR>
        AllowOverride All
    </Directory>
</VirtualHost>
EOF
echo "Done creating virtual host in /etc/apache2"

# Generate application key
sudo php artisan key:generate
echo "Done generating application key"

# Run migrations
sudo php artisan migrate --force
echo "Done running migrations"

# Change ownership permissions
sudo chown -R www-data:www-data $LARAVEL_DIR/database/ $LARAVEL_DIR/storage/logs/ $LARAVEL_DIR/storage $LARAVEL_DIR/bootstrap/cache
echo "Done changing ownership permissions"

# Set file permissions
sudo chmod -R 775 $LARAVEL_DIR/database/ $LARAVEL_DIR/storage/logs/ $LARAVEL_DIR/storage
echo "Done setting file permissions"

# Disable default configuration file
sudo a2dissite 000-default.conf
echo "Done disabling default configuration file"

# Enable Laravel configuration file
sudo a2ensite laravel.conf
echo "Done enabling Laravel configuration file"

# Restart Apache
sudo systemctl restart apache2
echo "Done restarting Apache"


uptime > /var/log/uptime.log

```


- Press 'crtl+O' to save, press 'enter' and then 'crtl+X' to exit the script.

_**The variables created in the first three lines of the script allow for its reusability, as mentioned in our context.**_

**_Leave the repository as it is in the script, as it is the same one linked earlier in this article._**


> Here is a snippet of my script

![script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x55f9lok6zcl0jm6aruj.png)


> Below are detailed explanations of what each line of command does:

i. Defining variables for paths and IP address:

-LARAVEL_DIR="/var/www/html/laravel": This variable specifies the directory where the Laravel application will be located.

-LARAVEL_CONF="/etc/apache2/sites-available/laravel.conf": This variable specifies the path for the Apache virtual host configuration file for Laravel.

-VIRTUAL_HOST="192.168.33.9": This variable specifies the IP address for the virtual host.
ii. Updating package index and upgrading packages:

-sudo apt update: Updates the list of available packages and their versions.
-sudo apt upgrade -y: Upgrades all installed packages to the latest versions. The -y option automatically answers "yes" to prompts.
iii. Updating the PHP repository:

-sudo add-apt-repository -y ppa:ondrej/php: Adds the PHP PPA (Personal Package Archive) maintained by Ondřej Surý, which provides the latest PHP versions.

iv. Installing Apache:

- sudo apt install -y apache2: Installs the Apache web server.
- sudo systemctl enable apache2: Enables Apache to start automatically on system boot.

v. Installing MySQL:

- sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password': Configures the root password for MySQL server installation.
- sudo apt install -y mysql-server: Installs the MySQL server.

vi. Installing PHP 8.2 and 8.3 with necessary extensions:

sudo apt install -y php libapache2-mod-php php-mysql php8.2 ...: Installs PHP 8.2 and 8.3 with necessary extensions such as curl, dom, xml, mysql, and sqlite3.

vii. Running MySQL secure installation:

- The expect block automates the mysql_secure_installation process, answering prompts automatically to set up MySQL securely.

viii. Restarting Apache:

- sudo systemctl restart apache2: Restarts the Apache web server.

ix. Installing Git:

- sudo apt install -y git: Installs Git, a version control system.

x. Cloning Laravel repository:

- sudo git clone https://github.com/laravel/laravel $LARAVEL_DIR: Clones the Laravel repository from GitHub to the specified directory.

xi. Changing directory to Laravel folder:

- cd $LARAVEL_DIR: Changes the current working directory to the Laravel folder.

xii. Installing Composer:

- sudo apt install -y composer: Installs Composer, a dependency management tool for PHP.

xiii. Upgrading Composer to version 2:

- These commands download and verify the Composer installer, then install Composer version 2.

xiv. Installing Composer dependencies:

- yes | sudo composer install: Installs Composer dependencies for the Laravel project, automatically answering "yes" to prompts.

xv. Copying Laravel configuration file and setting permissions:

- Copies the Laravel .env.example configuration file to .env and sets appropriate ownership and permissions.

xvi. Creating virtual host in /etc/apache2/sites-available:

- sudo tee $LARAVEL_CONF >/dev/null <<EOF: Creates a new virtual host configuration file for Laravel.
- This block of code sets up the virtual host with the specified server name, alias, and document root.

xvii. Generating application key:

- sudo php artisan key:generate: Generates a unique application key for the Laravel project.

xviii. Running migrations:

- sudo php artisan migrate --force: Runs database migrations for the Laravel project.

xix. Changing ownership permissions:

- sudo chown -R www-data:www-data $LARAVEL_DIR/...: Changes ownership of specified directories to the www-data user and group.

xx. Setting file permissions:

- sudo chmod -R 775 $LARAVEL_DIR/...: Sets file permissions to 775 (read, write, execute for owner and group; read and execute for others).

xxi. Disabling default configuration file:

- sudo a2dissite 000-default.conf: Disables the default Apache configuration file.

xxii. Enabling Laravel configuration file:

- sudo a2ensite laravel.conf: Enables the Laravel virtual host configuration file.

xxiii. Restarting Apache:

- sudo systemctl restart apache2: Restarts the Apache web server to apply changes.
xxiv. Logging system uptime:

- uptime > /var/log/uptime.log: Records the current system uptime to a log file.


- Test run the script to see if it works by running the command `./LAMP.sh`

-----

- Still in your "master" machine create an Ansible directory with the command 

```
mkdir Ansible
```
 
- Change into the `Ansible` directory so it becomes your present working directory.

```
cd Ansible
```
- Run this command to update any dependencies. From the image below, you should have something like the image below at the end after the update.
 

```
sudo apt-get update
```

![apt-get update](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nnkifk05rvmbwkg2r6ga.png)


- Then to install Ansible run the command below. It would be best if you had something like the image at the end of the installation.

```
sudo apt install ansible
```

![install ansibe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a4wbnt6dkvdyuwjkmqjv.png)

----

- Still, in your `Ansible` directory, create your inventory. The inventory file is used to store the address of the slave nodes to be configured which will enable the playbook to run in the "slave" machine even if it is created in the "master's" machine.

```
touch inventory 
```

- Open your inventory file to put in the IP address of your "slave" machine. It should look like the image below, but use your IP address. Press 'crtl+O' to save, press 'enter', and then 'crtl+X' to exit the text editor.



![IP address](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jk303ss3mq1xca0ld0d9.png)

----


- Create your playbook with the `.yml` extension.

```
touch playbook.yml
```

- To ping your inventory run the command in your "master" machine. It should look like the screenshot below. Press 'crtl+C' to cancel it afterwards.

```
ansible all -m ping -i inventory
```

![pinging](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h1phsxozii0eg0d78559.png)

- Change the directory back to Ansible with the command `cd Ansible`. Then open the playbook with the command `nano playbook.yml`. Paste the command below inside your playbook but change the IP address to your "slave" machine IP. Press 'crtl+O' to save, press 'enter', and then 'crtl+X' to exit the playbook.

```
---

- hosts: slave
  become: yes
  tasks:

    # Copy LAMP.sh script to the slave node
    - name: Copy LAMP.sh script to slave node
      copy:
        src: /home/vagrant/Ansible/LAMP.sh
        dest: /home/vagrant/LAMP.sh
        mode: 0755  
        
      # Make the script executable

    # Execute the LAMP.sh script on the slave node
    - name: Execute LAMP.sh script on slave node
      shell: /home/vagrant/LAMP.sh


- name: Create a cron job to check server's uptime every 12 am
  hosts: slave
  become: yes
  tasks:

    # Create an uptime script that logs the server's uptime to a file
    - name: Create the uptime script
      copy:
        content: |

          #!/bin/bash

          # Log the server's uptime to a file
          uptime > /var/log/uptime.log
        dest: /usr/local/bin/check_uptime.sh
        mode: '0755'  # Make the script executable

    # Create a cron job that runs the uptime script every day at 12 am (midnight)

    - name: Create a cron job to check uptime every 12 am
      cron:
        name: Check uptime every 12 am
        job: "/usr/local/bin/check_uptime.sh"
        
        # Run at 12:00 am
        minute: '0'  
        hour: '0'
        state: present

    # Display the contents of the uptime log file
    - name: Display uptime log file
      shell: cat /var/log/uptime.log
      register: uptime_output

    # Display the server's uptime as captured in the uptime log file
    - name: Display the server's uptime
      debug:
        msg: "{{ uptime_output.stdout }}"

    # Task to fetch the content of the PHP application using curl
    - name: Fetch PHP application content using curl
      command: "curl -s http://192.168.33.9"  # Perform a silent HTTP GET request to the target IP address
      register: php_application  # Register the output of the curl command
      ignore_errors: true  # Continue playbook execution even if this task fails

    # Task to display the content fetched from the PHP application
    - name: Display content of PHP application
      debug:
        msg: "PHP Application Content:\n{{ php_application.stdout }}"  # Display the output of the curl command

```
 Here is a snippet of my playbook



![playbook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5ez1lghozgynlva2zoq.png)




- Created a cron job to check the server’s uptime every 12 am in my playbook. You can find the task in the playbook above but this is a snippet of it.

![cron job](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0x7jyzz24mds1owbd9ra.png)

> This is the content of my log file where the server's uptime check is stored. Since my master and my slave are connected, I was able to check this from my "slave" machine. I used the command `sudo crontab -l` to check.

![uptime check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d22mlshgk9kmz4wnb5eo.png)

- Run the command below to execute the playbook:

```
ansible-playbook playbook.yml -i inventory
```

- I could execute the bash script on the Slave node and verify that the PHP application is accessible through the VM’s IP address. I have circled the slave IP address in the screenshot.


![execution 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i95wpr6ib4umtw00p57x.png)
![Execution 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7qiwl6d60yk8l4ilvlms.png)


- Paste your "slave" IP address on your web browser. This is the output with the slave IP address. A Laravel PHP application.


![laravel page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsynxdlwkzk909g7hk67.png)

---
###Conclusion

In summary, one of the effective and dependable methods for provisioning and controlling servers has been demonstrated in this project by the automation of the LAMP (Linux, Apache, MySQL, PHP) stack deployment utilizing a Bash script in Ansible. The deployment of the LAMP stack on Ubuntu-based servers is quite fast and less labor-intensive than with manual setup because of the script automation and it is even reusable.

The Bash script can be easily adapted to many environments and use cases due to its reusability, which is attained through defining variables that can be passed as parameters and error handling.

The deployment process was further automated and made uniform and repeatable by integrating the script into an Ansible playbook. In addition to establishing cron jobs to track server uptime and confirming that PHP applications could be accessed, the playbook carried out actions on both the master and slave nodes, giving important information about server performance.

Even though there were a few difficulties along the road, like script errors and log file problems, these could be fixed with thorough debugging and code modifications. The result showed that the PHP application was successfully deployed and verified on the slave node's IP address.

All things considered, the project demonstrates how Ansible and Bash scripting can be used to automate intricate deployment procedures. This detailed documentation of the stages and solutions makes it useful for future deployments and projects utilizing the LAMP stack.