\chapter{PaaS - Heroku}

\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} \index{PaaS}, où vous choisissez le \emph{service ou le composant} dont vous avez besoin (une base de données, un service de cache, un service applicatif, \ldots, vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. \footnote{Nous prenons ici Heroku, mais il existe d'autres plateformes similaires, notamment \href{https://www.pythonanywhere.com/details/django_hosting}{PythonAnywhere}.}
Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas possible de modifier un fichier une fois qu'elle aura démarré : si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres pour déployer une nouvelle instance.

\begin{figure}[H]
    \centering
    \scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/deployment/heroku.png}}
    \caption{Invest in apps, not ops. Heroku handles the hard stuff ---
    patching and upgrading, 24/7 ops and security, build systems, failovers,
    and more --- so your developers can stay focused on building great
    apps.}
\end{figure}

Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place.
La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH.
Par la suite, il conviendra d'investir un minimum: des bases de données sont disponibles, mais montrent rapidement leurs limites \footnote{\ldots et 10 000 enregistrements sont une limite \textbf{très} rapidement atteinte}, tandis qu'aucun mécanisme de migration n'est prévu.
Ceci signifie qu'après avoir démarré un projet en mode "hobby", il vous sera nécessaire de gérer vous-même la migration de vos données, au travers d'un mode opératoire, dans la mesure où aucun processus automatique n'existe entre instances de deux niveaux différents.

Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé.
Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition.
Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe.
Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests unitaires et d'intégration auront été réalisés.

Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous :
\begin{verbatim}
    $ heroku create
    Creating app... done, -> young-temple-86098
    https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-
    temple-86098.git

    $ cat .git/config
    [core]
        repositoryformatversion = 0
        filemode = false
        bare = false
        logallrefupdates = true
        symlinks = false
        ignorecase = true
    [remote "heroku"]
        url = https://git.heroku.com/still-thicket-66406.git
        fetch = +refs/heads/*:refs/remotes/heroku/*
\end{verbatim}


\begin{verbatim}
    Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration.
    Celle-ci se limite aux deux fichiers suivants :

    * Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI
    * Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`)
\end{verbatim}

Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande \texttt{git push heroku main}. \footnote{Puisque nous avons précedemment proposé d'utiliser Poetry, la commande \texttt{export} pourra être utilisée pour générer le fichier \texttt{requirements.txt}, comme exigé par Heroku.}

Heroku propose des espaces de déploiements, mais pas d'espace de stockage.
Il est possible d'y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container.
Il est donc primordial de configurer correctement l'hébergement des fichiers média, de préférences sur un stockage compatible S3.
s3

Prêt à vous lancer ? Commencez par créer un compte : \url{https://signup.heroku.com/python}.

\section{Configuration du compte}

+ Récupération des valeurs d'environnement pour les réutiliser ci-dessous.

Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite:

\begin{figure}
    \centering
    \includegraphics{images/deployment/heroku-new-app.png}
    \caption{Heroku: Commencer à travailler avec un langage}
\end{figure}

Installez ensuite la CLI (\emph{Command Line Interface}) en suivant \href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation suivante}.

Au besoin, cette CLI existe pour :
\begin{enumerate}
    \item
        macOS, \emph{via} `brew `
    \item
        Windows, grâce à un \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin)
    \item
        GNU/Linux, via un script Shell \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} ou sur \href{https://snapcraft.io/heroku}{SnapCraft}.
\end{enumerate}

Une fois installée, connectez-vous :
\begin{verbatim}
    $ heroku login
\end{verbatim}

Et créer votre application :
\begin{verbatim}
    $ heroku create
    Creating app... done, -> young-temple-86098
    https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-
    temple-86098.git
\end{verbatim}

\begin{figure}
    \centering
    \includegraphics{images/deployment/heroku-app-created.png}
    \caption{Notre application est à présent configurée!}
\end{figure}


Ajoutons lui une base de données, que nous sauvegarderons à intervalle
régulier :
\begin{verbatim}
    $ heroku addons:create heroku-postgresql:hobby-dev
    Creating heroku-postgresql:hobby-dev on -> still-thicket-66406... free
    Database has been created and is available
    ! This database is empty. If upgrading, you can transfer
    ! data from another database with pg:copy
    Created postgresql-clear-39693 as DATABASE_URL
    Use heroku addons:docs heroku-postgresql to view documentation

    $ heroku pg:backups schedule --at '14:00 Europe/Brussels' DATABASE_URL
    Scheduling automatic daily backups of postgresql-clear-39693 at 14:00
    Europe/Brussels... done
\end{verbatim}

(+ voir comment récupérer les backups)

\begin{verbatim}
    # Copié/collé de https://cookiecutter- django.readthedocs.io/en/latest/deployment-on-heroku.html
    heroku create --buildpack https://github.com/heroku/heroku-buildpack-python
    heroku addons:create heroku-redis:hobby-dev
    heroku addons:create mailgun:starter
    heroku config:set PYTHONHASHSEED=random
    heroku config:set WEB_CONCURRENCY=4
    heroku config:set DJANGO_DEBUG=False
    heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
    heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)"

    # Generating a 32 character-long random string without any of the visually
    similar characters "IOl01":
    heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-
    HJ-NP-Za-km-z2-9' | head -c 32)/"

    # Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
    heroku config:set DJANGO_ALLOWED_HOSTS=

    # Assign with AWS_ACCESS_KEY_ID
    heroku config:set DJANGO_AWS_ACCESS_KEY_ID=

    # Assign with AWS_SECRET_ACCESS_KEY
    heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY=

    # Assign with AWS_STORAGE_BUCKET_NAME
    heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME=
    git push heroku main
    heroku run python manage.py createsuperuser
    heroku run python manage.py check --deploy
    heroku open
\end{verbatim}

\section{Type d'application}

Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement.
Pour un projet Django, cela comprend, à placer à la racine de votre projet :
\begin{enumerate}
    \item
        Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r})
    \item
        Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), qui expliquera la commande pour le protocole WSGI.
\end{enumerate}

\begin{verbatim}
    # Procfile
    release: python3 manage.py migrate
    web: gunicorn gwift.wsg
\end{verbatim}

\section{Hébergement S3}

\begin{verbatim}
    # requirements.txt
    django==3.2.8
    gunicorn
    boto3
    django-storages
\end{verbatim}

Pour cette partie, nous allons nous baser sur l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de Scaleway}.
Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d'espace pour jouer un peu.

\includegraphics{images/deployment/scaleway-object-storage-bucket.png}

L'idée est qu'au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3.
La complexité va être de configurer correctement les différents points de terminaison.
Pour héberger nos fichiers sur notre \textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes dans l'ordre :

\begin{enumerate}
    \def\labelenumi{\arabic{enumi}.}
    \item
    Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y
    en a - \textbf{littéralement} - des dizaines.
    \item
    Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec
    ce type de protocole
    \item
    Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de
    faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et
    Django, qui s'attend à ce qu'on lui donne un moteur de gestion
    \textbf{via} la clé
    {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}).
\end{enumerate}

La première étape consiste à se rendre dans {[}la console Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), pour gérer ses identifiants et créer un jeton.

\includegraphics{images/deployment/scaleway-api-key.png}

Selon la documentation de \href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, de \href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} et de \href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, vous aurez besoin des clés suivantes au niveau du fichier \texttt{settings.py} :

\begin{minted}{python}
    AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = os.getenv('SECRET_ACCESS_KEY')
    AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
    AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME')
    AWS_DEFAULT_ACL = 'public-read'
    AWS_LOCATION = 'static'
    AWS_S3_SIGNATURE_VERSION = 's3v4'
    AWS_S3_HOST = 's3.%s.scw.cloud' % (AWS_S3_REGION_NAME,)
    AWS_S3_ENDPOINT_URL = 'https://%s' % (AWS_S3_HOST, )

    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
    STATICFILES_STORAGE = 'storages.backends.s3boto3.S3ManifestStaticStorage'
    STATIC_URL = '%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)

    # General optimization for faster delivery
    AWS_IS_GZIPPED = True
    AWS_S3_OBJECT_PARAMETERS = {
        'CacheControl': 'max-age=86400',
    }
\end{minted}

Cette partie pourrait éventuellement se trouver dans un fichier \texttt{heroku.py}, que vous pourriez placer à côté de votre configuration applicative.
Le paramètre \texttt{--settings=} pourrait alors pointer vers ce nouveau fichier, pour ne pas polluer l'environnement par défaut.

Configurez-les dans la console d'administration d'Heroku:

\includegraphics{images/deployment/heroku-vars-reveal.png}

Lors de la publication, vous devriez à présent avoir la sortie suivante,
qui sera confirmée par le \textbf{bucket}:

\begin{verbatim}
    remote: -----> $ python manage.py collectstatic --noinput
    remote: 128 static files copied, 156 post-processed
\end{verbatim}

\includegraphics{images/deployment/gwift-cloud-s3.png}

Sources complémentaires:

\begin{itemize}
    \item
        {[}How to store Django static and media files on S3 in production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/})
    \item
        {[}Using Django and Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/})
\end{itemize}