How to deploy your Django project on Platform.sh

11 Dec 2018
Thumbnail

Django —a high-level framework—makes web development in Python easy. Platform.sh—a second-generation Platform-as-a-Service—makes the deployment easy. Together, they’re unbeatable combination.

When starting your project on Platform.sh, you can deploy an arbitrarily complex cluster with a single git push. Clone a production cluster into staging environment to try out a new feature, and when you’re satisfied with the result, just merge your changes into the production (master) environment.

Platform.sh supports a broad range of application languages and frameworks, and provides starter kits for many popular applications, including Django. That makes getting started with Django on Platform.sh as simple as a push of a button.

Which Django version should I choose?

Before you decide which Django version will best meet your needs, you need to determine which version of Python you’ll use. The choice is relatively simple. The Python 2 end of life is approaching. The only rationales to use Python 2 in 2018? Any specific third-party dependencies that don’t have a Python 3-compatible release.

The release of Django 2.0 dropped Python 2 support. Django 1.11 will receive security and data loss fixes until April 2020. This is pretty much aligned with Python 2 end of life.

In general, if you’re going to have a hard dependency on Python 2, start with Django 1 template; otherwise go for version 2.

Project setup

Space is a mysterious place, where plenty of planets, asteroids, and moons are moving around different objects, creating huge systems. In this tutorial, we’re going to show you how to turn our Django 2 template into a simple service collecting data about the moons.

The first step is to clone the template repository locally by running:

$ git clone git@github.com:platformsh/template-django2.git space-app
$ cd space-app

Now you should re-initialize git to have a clean history:

$ rm -rf .git && git init
$ git add -A && git commit -m "Initial commit"

You need to set up a virtual environment to be able to run Django administration and database migration tools locally. Managing virtual environments for your applications, and installing and tracking Python dependencies can be a bit of a challenge. Pipenv, the higher-level Python packaging tool, was designed to take this burden off your shoulders. Follow instructions in the documentation to install it on your system.

The default Python version used in the template is 3.7. In case you want to use a different version, set type in .platform.app.yaml file and python_version in your Pipfile to reflect the version you’ll be using. When you’re sure all the versions matches, run:

$ pipenv install
$ pipenv shell

The virtual environment will be created and activated, and you can add a new Django application to the project. This Python package will be dedicated to the moons orbiting their planets:

(space-app) $ django-admin startapp moons

The new application has to be added into the list of installed applications, overwriting example hello application; it’s not needed at this moment. myapp/settings.py:

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'moons',
]

The next step? Deploy!

At this point, the template can’t really do anything useful, but it contains all the configuration to be deployed to Platform.sh.

First, install Platform.sh Command Line Interface following the CLI section of the official documentation. After CLI is installed and set up, you can create a new project through the Platform.sh user interface.

Sign in or create a new Platform.sh account. When you’re successfully logged in, click on the “ADD A PLATFORM” button on the top of the page, and choose the right Platform.sh plan and region to fulfill your needs. Creation of the new project will take a while. When it’s ready, and you get to the “Setting up your project” screen, select “Import your existing code,” which gives you the git commands to run from the template directory. Use the first command to add a git remote called “platform”, which points to your new Platform.sh project and run:

 $ git push -u platform master

The plain Django project is up and running in about a minute, and you can devote all your attention and energy to your code.

Database model

The basic setup is done, and you can start to build the pillars of your new service. The very first thing you need to define is a database model to store the data. The definition of Django models is pretty simple and straightforward.

moons/models.py:

from django.db import models


class Moon(models.Model):
    name = models.CharField(max_length=255, null=False)
    planet = models.CharField(max_length=255, null=False)
    discovered = models.DateField()
    volume = models.FloatField()

At this point, it’s the right time to create initial migration:

(space-app) $ python manage.py makemigrations moons

Commit the changes, including the generated migration file under ‘planets/migrations,’ and push the new code to your Platform.sh project:

$ git add -A
$ git commit -m "Add concept of database model"
$ git push platform master

You can see ongoing activity on your master environment. The application was rebuilt, and redeployment of the cluster has started. Your first impression might be that not much has changed since your first deployment, but let’s take more precise look at the master environment cluster.

When you open .platform.app.yaml file in the root directory of the project, you can see the following lines:

# The relationships of the application with services or other applications.
#
# The left-hand side is the name of the relationship as it will be exposed
# to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
# side is in the form `<service name>:<endpoint name>`.
relationships:
    database: "postgresqldb:postgresql"

This section creates a connection between the application and the PostgreSQL service. The PostgreSQL service is defined in the .platform/services.yaml file. See the configuration section in the documentation for a more detailed explanation.

Another interesting bit is at the very end of myapp/setting.py file:

relationships = os.getenv('PLATFORM_RELATIONSHIPS')
if relationships:
    relationships = json.loads(base64.b64decode(relationships).decode('utf-8'))
    db_settings = relationships['database'][0]
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': db_settings['path'],
            'USER': db_settings['username'],
            'PASSWORD': db_settings['password'],
            'HOST': db_settings['host'],
            'PORT': db_settings['port'],
        },
        ...
    }

The credentials of the PostgreSQL service running as part of the cluster are assigned to the default Django database. This means you can immediately start interacting with the database from your application.

To make the “just works” user experience complete, there’s a deploy hook running database migrations during each redeployment of the cluster in .platform.app.yaml file. This also happened after you added database model in the last commit and pushed it to the project.

Let’s ensure everything worked as intended. You can run psql directly on the remote database container using following command:

$ platform db:sql

You should see a psql shell prompt, and you can run a bunch of SQL commands to verify that moon table was created, and it’s ready to be filled by the data:

psql (11.1 (Debian 11.1-1.pgdg80+1), server 9.6.1)
Type "help" for help.

main=> SELECT * FROM moons_moon;
 id | name | planet | discovered | volume
----+------+--------+------------+--------
(0 rows)

Now, let’s insert a testing data sample manually:

main=> INSERT INTO moons_moon VALUES('1', 'Phobos', 'Mars', '1877-08-17', '5729');

RESTful API

With the growing popularity of microservices architecture, being able to make RESTful API for your application back end has become more important than ever. We’ll use Django REST Framework (DRF) to build an API on top of the database model. A nice thing about DRF is that it gives us a lot of functionality, like serializers and views, right out of the box.

First, create a dedicated branch for development of this feature:

$ git checkout -b rest

Now you need to install DRF and add it to the list of installed applications.

$ pipenv install djangorestframework

And in myapp/settings.py:

INSTALLED_APPS = [
           ...
    'django.contrib.staticfiles',
    'rest_framework',
    'moons',
]

The REST framework is now available, and you can create a serializer for your Moon model. In moons/serializers.py:

from rest_framework import serializers

from .models import Moon


class MoonSerializer(serializers.HyperlinkedModelSerializer):
    def create(self, validated_data):
        return Moon.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.planet = validated_data.get('planet', instance.planet)
        instance.discovered = validated_data.get('discovered', instance.discovered)
        instance.volume = validated_data.get('volume', instance.volume)

    class Meta:
        model = Moon
        fields = ('name', 'planet', 'discovered', 'volume')

This class will automatically serialize and deserialize native Python data types of Moon model into JSON format.

Now you can create views with only a few lines of code using DRF.

moons/views.py:

from rest_framework import generics

from .models import Moon
from .serializers import MoonSerializer


class MoonList(generics.ListCreateAPIView):
    queryset = Moon.objects.all()
    serializer_class = MoonSerializer


class MoonDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Moon.objects.all()
    serializer_class = MoonSerializer

The first view provides a list of stored moon resources; the second one provides a detailed view of a single moon.

You also need to create routes to expose the API endpoints.

moons/urls.py:

from django.urls import path
from django.conf.urls import url
from django.views.generic.base import RedirectView

from . import views

urlpatterns = [
    path('', RedirectView.as_view(pattern_name='moon-list', permanent=False), name='index'),
    url(r'^api/moons/$', views.MoonList.as_view(), name='moon-list'),
    url(r'^api/moons/(?P<pk>[0-9]+)$', views.MoonDetail.as_view(), name='moon-detail'),
]

Finally, remember to include moons.urls in the global myapp.urls patterns overwrite path, including hello.urls.

myapp/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("", include("moons.urls")),
    path('admin/', admin.site.urls),
]

Commit the latest changes, and push them to the dedicated branch of the project:

$ git add -A
$ git commit -m "Add REST API for moons"
$ git push platform rest

Now, activate the rest environment of your Platform.sh project by running:

$ platform environment:activate

In a few moments, the new rest environment cluster will be setup on Platform.sh, and you can start testing the API. Your service can now communicate with the outer world via HTTP requests. Copy the rest environment route serving the application from the CLI output and run:

$ curl -iL -H "Accept: application/json" \
   URL_OF_YOUR_REST_ENVIRONMENT/api/moons/1

HTTP/2 200
allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
content-type: application/json

{"name":"Phobos","planet":"Mars","discovered":"1877-08-17","volume":5729.0}

Once you’ve verified that everything is working, open the rest environment overview in the user interface and hit the “Merge” button. Doing so will merge your latest changes into the master environment and redeploy it with your latest changes.

That’s it! You can easily create more branches, implement new features, and see them running in a production-like environments almost immediately. You can make sure that only well-tested code will be merged into your master branch. Remember: you can always roll back to the latest stable revision whenever you feel your latest release isn’t quite ready for production.