Deploying Laravel application on a remote server using ansible for configuration management

Jubril Adeyi
14 min readApr 18, 2023

--

Introduction

Automation is a key aspect of cloud computing and DevOps, allowing cloud engineers to make necessary installations and configurations on servers without having to work on them directly. With the help of tools like Ansible, the entire process of installing dependencies and setting up applications can be done remotely, streamlining the configuration process and making it more efficient.

WHAT IS ANSIBLE?

Ansible is a powerful automation tool that is widely used in cloud computing and DevOps. It is Developed in Python and can be used for a variety of purposes, including configuration management, application deployment, and infrastructure as code. In this article, we’ll be exploring Ansible’s use as a configuration management tool, which allows management and automation of configuration of servers and applications using YAML files.

Ansible also allows you to limit access to the target server by only granting the necessary privileges to perform specific tasks, such as installing dependencies or modifying configuration files. This is important as a security consideration, as it allows you to control who can access and modify the server, reducing the risk of unauthorized access and unwanted system changes.

You may be wondering how Ansible is able to perform all these configurations completely remotely. Well, for an Ansible configuration, the working server just needs to have established SSH access to the remote server, and this is the channel through which all the configurations are made.

Overview

In this article, we’ll explore how to use Ansible to configure a remote server for hosting Laravel.

Laravel is a PHP web application framework with powerful features such as routing, ORM, and templating engine. It is widely used to build scalable and maintainable web applications.

We will be using Apache as the web server and MySQL as the database for this application.

For this setup, Linux Ubuntu instances on AWS EC2 will be used as master and slave nodes/servers.

Prerequisites:

  • An AWS account.
  • Two EC2 instances:
  1. A slave (target) instance where the Laravel application and packages will be deployed.
  2. A master instance, which will have Ansible installed on it and be granted SSH access to the slave instance.
  • Ansible Installed on the master instance.

Setting up AWS EC2 Instances

Step 1: Create a Security Group for the instances.

  • Locate the EC2 dashboard on the AWS Console and select “Security Groups” under the Network & Security section.
  • Click on “Create Security Group”
  • Proceed to adding name, description (optional) for the security group and select the VPC this security group will be located in.
  • Modify the Inbound rules to allow Ports 22, 80 and 443 which will allow SSH access, HTTP connection and HTTPS connections respectively.
  • Set outbound rules to allow all traffic on all ports and finally click on “Create security group” and the security group has been created.

Step 2: Create an AWS SSH key pair.

  • Locate “Key Pairs” in the Network and Security section on the EC2 dashboard and click on “Create key pair.”
  • Proceed to Naming the key pair, select key pair type to be RSA and .pem Private key file format. Finally, click on “Create key pair.”

Note: .pem key pair file can be downloaded and used to ssh into the instance they’re attached to from your pc

Step 3: Create The AWS EC2 Instances

  • Locate “Instances” on the EC2 dashboard. and click on “Launch instances.”
  • Select the number of Instances, two since we’re creating just master and slave nodes for the ansible set up.
  • We will be using the t2.micro instance type with an Ubuntu Linux 22.04 server.
  • Select the key pair created in the previous step.
  • Enable Auto-assign public IP and select the security group created in the previous steps as well.
  • Now click on “Launch Instance” to finalize the creation of the instances.

Note: Both instances will be created inside the same VPC, will share the same security group and SSH key pair

Configuring the Master Node for Ansible

Step 1: SSH into the master node, Update package list and Install Ansible with the following commands.

sudo apt update 

sudo apt install ansible

Step 2: Create a working directory. This directory will house the ansible playbook and other necessary files. Also all the configuration and deployment will be done in this directory.

mkdir Ansible 

cd Ansible

Step 3: Create Host-inventory file and add slave node(s) IP address(es) in this file.

touch host-inventory
[slave-nodes]
< slave-node-ip-address >

The [slave-nodes] header is used to define a group of multiple hosts. “< slave-ip-address >” should be replaced with the public ip address of the slave node.

Step 3: Create ansible.cfg file and specify the defaults and privilege escalations. This file controls how ansible will function, setting parameters such as path of the inventory file, default user and privileges.

mkdir ansible.cfg
[defaults]
inventory = host-inventory
remote_user = ubuntu
private_key_file = key.pem

[privilege_escalation]
become = yes
become_method = sudo

Preparing Ansible Environment for Laravel deployment

Step 1: Create a variable file that will be used during the deployment process. To do this, we will be using the Group_vars system.

In the Group_vars directory, subdirectories will be created to represent specific groups of hosts as defined in the host-inventory file. These variables will apply only to the ip-addresses/hosts in this group. Each subdirectory will contain a YAML file (main.yaml) that declares variables and their values.

mkdir Group_vars/slave-nodes/

cd Group_vars/slave-nodes/

touch main.yaml

Contents of the variable YAML file will include all the variables that will be used during the course of this deployment.

laravel_app: < name-for-laravel-app >
mysql_username: < mysql-username >
servername: < server-name or ip-address >
sever_email: < email-address >
mysql_database_name: < mysql-database-name >
sever_admin: < server-admin-email >
server_name: < server-name or ip-address >
server_alias: < server-alias >

Step 2: Create an Ansible Vault;

Ansible Vault is used to securely store sensitive information such as passwords, API keys, and other secrets, we’ll create an Ansible Vault directory. This directory will contain a YAML that contains variables defined by encrypted values that we can reference in our playbooks.

mkdir vault 

After creating a directory, vault in the working directory, run the following command to create the encrypted YAML file for your passwords:

ansible-vault create /vault/db.yaml

A new file db.yaml will be created in the vault directory and you’ll prompted to type a new vault password. Once password has been set, variables and values can be added to the encrypted YAML file.

db_password : “ your-db-password ”

Now, these values can be referenced in our playbook and our sensitive information can be called without exposing it in plain text.

Encrypted variables can be edited using this command:

ansible-vault edit db.yml 

To run your ansible playbook commands whilst using the values defined in the encrypted ansible-vault file, the “ — ask-vault-pass” flag can be added like this:

ansible-playbook -i host-inventory playbook.yaml —ask-vault-pass

You will prompted for the vault password to run this command.

Deploying Laravel using Ansible Roles

Ansible roles is a feature in Ansible that allows users to organize tasks in a playbook into reusable units. A directory is created for each role, which contains subdirectories for tasks, templates, files, and handlers specific to that role. The main task playbook for each role is located in the tasks directory. This makes it easier to modify and maintain the playbook, especially in larger and more complex deployments.

The roles can then be called in the main playbook YAML file which will be located in the root working directory.

Step 1: Create a roles directory in the root working directory and create a directory for each ansible role within the roles directory.

mkdir roles

For this deployment, we will section the tasks into eleven ansible roles:

  1. Install-packages
  2. Install-mysql
  3. Configure-mysql
  4. Configure-mysql-user-database
  5. Configure-firewall
  6. Install-php
  7. Install-setup-composer
  8. Clone-laravel-repo
  9. Configure-apache
  10. Migrate-database
  11. Install-setup-SSL
  • Install-packages: This role Updates system packages on the slave node and installs the necessary packages required for this deployment using the apt module.
/roles/install-packages/tasks/main.yaml



- name: update "apt"
apt:
update_cache: yes
autoclean: yes
autoremove: yes
- name: install (git, wget, apache, ufw, unzip, curl)
apt:
pkg:
- git
- wget
- apache2
- ufw
- unzip
- curl
  • Install-mysql: This role fully installs the MySQL package and all its dependencies.
/roles/Install-mysql/tasks/main.yaml

---



- name: install mysql-server
apt:
name: mysql-server
state: present
- name: install mysql-client
apt:
name: mysql-client
state: present
- name: install python3-pymysql
apt:
name: python3-pymysql
state: present
- name: install python3-mysqldb
apt:
name: python3-mysqldb
state: present
- name: install libmysqlclient-dev
apt:
name: libmysqlclient-dev
state: present

  • Configure-mysql: This role starts and enables the MySQL package on the slave node, flushes privileges, sets the root password and removes all anonymous user accounts.
/roles/Configure-mysql/tasks/main.yaml


- name: start and enable mysql service
service:
name: mysql
state: started
enabled: yes
- name: Change the auhentication plugin of mysql root user to mysql_native_password
shell: mysql -u root -e 'UPDATE mysql.user SET plugin="mysql_native_password" WHERE user="root" AND host="localhost"'
- name: Flush Privileges
shell: mysql -u root -e 'FLUSH PRIVILEGES'
- name: Sets the root password
mysql_user:
login_host: 'localhost'
login_user: 'root'
login_password: ''
name: root
password: "{{ mysql_root_password }}"
state: present
- name: Removes all anonymous user accounts
mysql_user:
name: ''
host_all: yes
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
  • Configure-mysql-user-database: This role is responsible for Creating a MySQL user, creating a database and granting user all access and privileges to the database. Variables defined using our Group_vars system created earlier are used here, also some of encrypted values stored in our Ansible-vault are used called in the playbook as variables.
/roles/Configure-mysql-user-database/tasks/main.yaml


- name: Creates new user
mysql_user:
login_host: 'localhost'
name: "{{ mysql_username }}"
password: "{{ mysql_user_password }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Creates new database
mysql_db:
name: "{{ mysql_database_name }}"
login_host: 'localhost'
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Grants user ALL access/privileges to database
mysql_user:
name: "{{ mysql_database_name }}"
login_host: 'localhost'
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
priv: '{{ mysql_database_name }}.*:ALL,GRANT'
  • Configure-firewall: This role configures the UFW firewall on the slave node to allow the necessary network traffic in and out of the server. This is so that the Firewall rules allow HTTP, HTTPS and MySQL traffic on the server level. This role does this by Creating UFW rules to allow traffic on ports 443, 80 and 3306.
/roles/Configure-firewall/tasks/main.yaml

---

- name: UFW - Allow HTTP on port 80
ufw:
rule: allow
port: "80”
- name: UFW - Allow HTTPS
ufw:
rule: allow
port: "443"

- name: UFW - Allow Mysql
ufw:
rule: allow
port: 3306
  • Install-php: This role installs the PHP packages on the slave node.
/roles/Install-php/tasks/main.yaml

- name: Add php repository
apt_repository:
repo: 'ppa:ondrej/php'
- name: Updating the repo
apt: update_cache=yes
- name: Install PHP
apt: name=php8.1 state=latest
- name: Install PHP MB
apt: name=php8.1-mbstring state=latest
- name: Install PHP XML
apt: name=php8.1-xml state=latest
- name: Install PHP ZIP
apt: name=php8.1-zip state=latest
- name: Install PHP CURL
apt: name=php8.1-curl state=latest
- name: Install libapache2-mod-php
apt: name=libapache2-mod-php state=latest
- name: PHP DEV
apt: name=php8.1-dev state=latest
- name: PHP mysql
apt: name=php8.1-mysql state=latest
- name: PHP XMLRPC
apt: name=php8.1-xmlrpc state=latest
- name: PHP DEV
apt: name=php8.1-dev state=latest
- name: PHP common
apt: name=php8.1-common state=latest
- name: PHP gd
apt: name=php8.1-gd state=latest
- name: PHP imagick
apt: name=php8.1-imagick state=latest
- name: PHP CLI
apt: name=php8.1-cli state=latest
- name: PHP imap
apt: name=php8.1-imap state=latest
- name: PHP intl
apt: name=php8.1-intl state=latest
- name: Install some required softwares for php functionality
apt:
name:
- ca-certificates
- apt-transport-https
- software-properties-common
- lsb-release
state: latest
update_cache: true
  • Install-setup-composer: This role installs the Composer package. It does this by downloading the installer file, installing it and making it executable. Composer is a dependency manager for PHP that is required to install and manage Laravel dependencies.
/roles/install-setup-composer/tasks/main.yaml



- name: Dowload php-composer
get_url:
url: https://getcomposer.org/installer
dest: /tmp/installer
- name: verify installatiion script is safe
command: php -r "if (hash_file('sha384', 'composer-setup.php') === '55ce33d7678c5a611085589f1f3ddf8b3c52d662cd01d4ba75c0ee0459970c2200a51f492d557530c71c15d8dba01eae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"

- name: Install composer
command: php /tmp/installer --install-dir=/usr/local/bin/ --filename=composer
args:
creates: /usr/local/bin/composer

- name: rename composer.phar to composer
shell: mv /usr/local/bin/composer.phar /usr/local/bin/composer
args:
creates: /usr/local/bin/composer

- name: Make composer executable
file:
path: /usr/local/bin/composer
mode: a+x
state: file
  • Clone-laravel-repo: This role simply clones a github repository that contains the Laravel application onto the slave node.
/roles/Clone-laravel-repo/tasks/main.yaml

- name: Clone github repository
git:
repo: https://github.com/f1amy/laravel-realworld-example-app.git
dest: /var/www/{{ laravel_app }}
clone: yes
update: no
  • Configure-apache: This role is responsible for Configuring Apache webserver on the slave node. This role consists of several directories which include files, handlers, templates and the task directory. The files directory contains all the necessary files for this role, the handlers directory contains defined handlers, and the templates directory contains templates referenced in the main task YAML. This role performs tasks that includes moving the Laravel app files into the /var/www/html directory for Apache, setting up virtual hosts, enabling necessary modules, and restarting Apache service.
roles/Configure-apache/files/.htaccess

<IfModule mod_rewrite.c>
RewriteEngine On

RewriteCond %{REQUEST_URI} !^/public/
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^(.*)$ /public/$1
RewriteRule ^(.*)?$ public/index.php [L]
</IfModule>
roles/Configure-apache/templates/.web.php

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
return view('welcome');
});

/roles/Configure-apache/templates/web.conf.j2

<VirtualHost *:80>
ServerAdmin {{ sever_admin }}
ServerName {{ server_name }}
ServerAlias {{ server_alias }}

DocumentRoot /var/www/html/{{ laravel_app }}/

<Directory /var/www/html/{{ laravel_app }}>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
roles/Configure-apache/tasks/main.yaml

- name: Copy laravel project
shell: sudo mv /var/www/{{ laravel_app }} /var/www/html

- name: Change permission
shell: sudo chgrp -R www-data /var/www/html/{{ laravel_app }}

- name: Change permission
shell: sudo chmod -R 775 /var/www/html/{{ laravel_app }}/storage

- name: Change permission
shell: sudo chmod -R ug+rwx /var/www/html/{{ laravel_app }}/bootstrap/cache

- name: Copy .env file
copy:
src: /var/www/html/{{ laravel_app }}/.env.example
dest: /var/www/html/{{ laravel_app }}/.env
remote_src: true

- name: Modify .env file
template: src=.env dest="/var/www/html/{{ laravel_app }}/.env" owner=www-data mode=0644

- name: Modify /routes/web.php file (Add frontend to laravel route)
template: src=.web.php dest="/var/www/html/{{ laravel_app }}/routes/web.php" owner=www-data mode=0644

- name: Copy .htacces file
copy:
src: .htaccess
dest: /var/www/html/{{ laravel_app }}/.htaccess

- name: Remove default apache vhost config from sites-enabled
file: name=/etc/apache2/sites-enabled/000-default.conf state=absent

- name: create apache vhosts for domain
template: src=web.conf.j2 dest="/etc/apache2/sites-available/{{ laravel_app }}.conf" owner=www-data mode=0644

- name: Update a2ensite
command: a2ensite {{ laravel_app }}

- name: Update enable apache rewrite module
shell: a2enmod rewrite

notify:
- Restart apache
  • Migrate-database: This role is responsible for automating the process of migrating the MySQL database for the Laravel application. It first makes sure that the Composer package is installed and updated. Then, it creates a new Laravel project on the slave node. Finally, it runs the “php artisan migrate” command to migrate the database, which applies any outstanding database migrations to the MySQL database on the slave node. The importance of this role is that it makes sure the Laravel app has the appropriate database schema.
roles/Configure-apache/tasks/main.yaml


- name: Composer update
shell: composer update --no-interaction
args:
chdir: /var/www/html/{{ laravel_app }}

- name: Composer Install
shell: composer update --no-interaction
args:
chdir: /var/www/html/{{ laravel_app }}

- name: Composer create-project
shell: composer create-project --no-interaction
args:
chdir: /var/www/html/{{ laravel_app }}

- name: Run the php -artisan migrate --force --seed command
shell: php artisan migrate --force --seed
args:
chdir: /var/www/html/{{ laravel_app }}
  • Install-SSL: This role installs and configures the Let’s Encrypt SSL certificate package and its dependencies on the slave node. The certificate helps to secure the Laravel application on the web, making it accessible via HTTPS. By setting up HTTPS, this role ensures that communication between a user’s web browser and the web server that houses the Laravel application is secured and encrypted.
roles/Install-setup-SSL/tasks/main.yaml

- name: Install Python Package
apt: name=python3 update_cache=yes state=latest

- name: Install Let's Encrypt Package
apt: name=python3-certbot-apache update_cache=yes state=latest

- name: Create and Install Cert Using apache Plugin
command: "certbot --apache -d {{ servername }} -m {{ sever_email }} --agree-tos --noninteractive --redirect"

Now that the roles have been defined, an Ansible playbook YAML file needs to be created in the root working directory. In this YAML file, the roles will be referenced in the order in which we want the tasks to be executed. this file will be named “playbook.yaml”.

- name: deploy laravel app on remote server
hosts: slave-nodes
vars_files:
- group_vars/slave-nodes/main.yml
- vault/db.yml
become: yes
roles:
- Install-packages
- Install-mysql
- Configure-mysql
- Configure-mysql-user-database
- Configure-firewall
- Install-php
- Install-setup-composer
- Clone-laravel-repo
- apache-config
- migrate-database
- SSL-install-setup that the

We can now proceed to running the ansible-playbook command to deploy the application into the slave node. To do this, navigate to the root working directory in your terminal and run the following command:

ansible-playbook -i host-inventory playbook.yaml -ask-vault-pass

Note: You will be prompted to input your vault password because of the -ask-vault-pass flag. the deployment should run right after the correct password is inputted.

We have now successfully Deployed the Laravel application on a remote server using ansible.

Summary of the Process

  1. To begin the deployment process, we first created two AWS EC2 instances which will be used as the master and slave nodes for our Ansible tasks. We made sure that both instances were created within the same VPC network, shared a security group that allows inbound traffic for HTTP, HTTPS, and SSH protocols. Additionally, we ensured that they shared an SSH key pair to facilitate SSH connection between both instances.
  2. After creating the instances, we proceeded to install the Ansible package on the master node. We then created a working directory where we created and modified the host-inventory and ansible.cfg files to define the host nodes and set some configurations respectively.
  3. We proceeded to create a group_vars directory within the working directory on the master node. Inside this directory, we created a subdirectory to house the variables used in this deployment. We also created an Ansible vault to keep sensitive information such as passwords and keys secure.
  4. We created a roles directory in our working directory, and inside this directory, we created subdirectories for each task in this deployment. Each of these subdirectories contains additional subdirectories, such as files, templates, and tasks, that house the files, templates, and YAML files for each task respectively. This organizational structure made it easy to manage and modify the tasks in our playbook.
  5. Finally, we summarized the process by creating an Ansible playbook YAML in the working directory. This YAML file contains each of our defined roles in the order in which we want the tasks to be executed. We then ran the ansible-playbook command in the working directory with the ask-vault-pass flag to commence with our deployment.

Overall, this process allowed us to efficiently automate the deployment of our Laravel application on a remote server using Ansible. By breaking down the deployment tasks into separate roles and using Ansible’s automation capabilities, we were able to simplify the process and ensure that each step was executed correctly.

--

--