LEMP stands for Python
Using Python instead of PHP in LEMP
In a previous post we learned how to setup a LEMP guest machine with Vagrant and Ansible to test PHP applications. However, in this day and age, there are other programming languages that can be used for web development other than PHP.
Python is a very popular beginner-friendly programming language that will allow us to create web applications with ease due to its multiple third party modules and packages.
Unlike PHP, Python was not born as a server-side scripting language designed for web development and it’s not as intertwined with web servers (such as Apache). For this reason, it requires some kind of module or gateway to work.
For this post, we will be installing uWSGI, which is a Web Server Gateway Interface, inside our Vagrant guest machine and connect Nginx to it.
Creating a test application
We are going to create a very simple test application using Flask.
Flask is a micro web framework written in Python and based on the Werkzeug toolkit and Jinja2 template engine.
A web framework is a great tool that makes developing Python web applications much easier.
We will save the configuration files for our application in the subdirectory
vagrant/www/test
:
vagrant/www/test
├── requirements.txt
├── test.ini
└── test.py
Requirements file
A virtual environment is a self-contained directory tree that contains a Python installation for a particular version of Python, plus a number of additional packages.
Virtual environments give us the possibility to isolate each Python application. This way, we can use PIP to install different versions of packages without conflicting with versions of the same package for other applications.
And how do we tell PIP which packages and which versions to install? Well, by using a requirements file for each application.
Since our test application only needs Flask, we will add it to
requirements.txt
:
Flask>=0.12
uWSGI’s configuration file
To tell uWSGI how to launch our application, we will set some parameters in the
file test.ini
that will be loaded by uWSGI on boot:
[uwsgi]
plugins = python3
socket = /tmp/test.sock
venv = /opt/virtualenvs/test
chdir = /vagrant/www/test
wsgi-file = test.py
callable = app
Here we specify the version of Python to use, the socket file, the path of the virtual environment, the directory for our application’s files, the file that contains the application, and what object to call.
Python application
The web application per se will be in test.py
:
#!/usr/bin/env python3
from flask import Flask
app = Flask(__name__)
@app.route('/test')
def test():
return '<p style="background: aliceblue;">Hello Wold</p>\n'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
This just will send the text Hello World (on a blue background) to the web browser when we connect to the server.
Configuration of Ansible
Now, we have to modify our previous configuration for Ansible so it installs all the necessary packages and loads the appropriate configuration files.
Global variables
First of all, since we will be using some variables that are common to more than
one role, let’s create a file to store these global variables. In
the subdirectory vagrant/cfg/group_vars
we create a file called all.yml
for
all roles:
---
base_dir: '/vagrant/www'
venv_dir: '/opt/virtualenvs'
We specify the directory that Nginx will use as root and the directory where Python’s virtual environments will be created.
In this same file, we will include the name/directory of our applications:
apps:
- name: test
Python role
Now, let’s add a new role to our configuration so all the appropriate Python
dependencies are met. We will save the files in the subdirectory
vagrant/cfg/roles/python
:
vagrant/cfg/roles/python
├── handlers
│ └── main.yml
└── tasks
└── main.yml
Tasks are saved in tasks/main.yml
:
---
- name: Install Python
package: name={{ item }} state=present
with_items:
- python3-pip
- python3-venv
- uwsgi
- uwsgi-plugin-python3
notify:
- start uwsgi
- name: Install PIP packages
pip:
requirements: '{{ base_dir }}/{{ item.name }}/requirements.txt'
virtualenv: '{{ venv_dir }}/{{ item.name }}'
virtualenv_command: pyvenv
with_items:
- '{{ apps }}'
- name: Link uWSGI file
file:
src: '{{ base_dir }}/{{ item.name }}/{{ item.name }}.ini'
dest: '/etc/uwsgi/apps-enabled/{{ item.name }}.ini'
force: yes
state: link
with_items:
- '{{ apps }}'
notify:
- restart uwsgi
The steps are:
- Using the package module, we install the Python 3 packages for PIP and venv along with uWSGI and its Python 3 plugin.
- Then, using the pip module, we read the file
requirements.txt
for each application and install its packages in a virtual environment. - We then enable the application by adding a symbolic link to the
application’s INI file in the directory
/etc/uwsgi/apps-enabled
using the file module.
And let’s not forget the handlers to enable and restart the service when needed.
Add these to the file handlers/main.yml
:
---
- name: start uwsgi
service: name=uwsgi enabled=yes state=started
- name: restart uwsgi
service: name=uwsgi state=restarted
Finally, we modify vagrant/cfg/site.yml
to add this new role:
---
- name: Configure LEMP server
hosts: lemp
roles:
- mariadb
- php
- python
- nginx
Nginx role
We also need to modify the template for our Nginx role in
vagrant/cfg/roles/nginx
so it loads our applications.
For this, we edit templates/default
and add the following inside the server
directive:
{% for item in apps %}
location /{{ item.name }} {
include uwsgi_params;
uwsgi_pass unix:/tmp/{{ item.name }}.sock;
}
{% endfor %}
This will loop through each of our applications and configure Nginx to use uWSGI for its corresponding subpath.
Running the test application
We can now start the guest machine with Vagrant using the command vagrant up
.
After the machine has finished booting, we will be able to open our test
application by pointing our web browser to http://172.28.128.10/test:
Conclusion
Using Python as the backend for our web applications is not as straightforward
as throwing some PHP code in an index.php
file but, with some tweaking, we can
have a working application and have access to the power of Python
packages.