https://www.orbitale.io/2018/01/05/deploy-a-symfony-flex-project-on-heroku.html
https://github.com/Pierstoval/FlexAndHeroku.git
Nous allons aborder trois sujets ici :
Flex, parfois appelé Symfony Flex, est un plugin Composer que vous pouvez installer dans tout projet PHP, et qui va permettre de standardiser la structure et la composition d'un projet.
D'autres articles existent à ce propos, notamment dans le cadre de ce calendrier de l'avent, et je ne vais donc pas trop m'étaler sur le sujet.
En quelques phrases, je peux vous dire que Flex :
cli pour symfony/console, ou orm pour plusieurs packages relatifs à l'ORM Doctrine).recipe) pouvant être définies dans les repositories publics symfony/recipes-contrib et symfony/recipesC'est une plateforme permettant de faire du cloud-computing.
On peut l'utiliser pour héberger des bases de données, des applications (web, workers, etc.), et les orchestrer.
Sa configuration peut être faite via l'utilisation d'une ligne de commande, ou par un tableau de bord en ligne, les deux étant facilement accessibles.
Heroku est payant mais propose un accès gratuit jusqu'à un certain nombre d'heures d'utilisation. Dans le cas d'une application web, celle-ci rentre dans un état de "sommeil" après 30 minutes d'activité, permettant d'économiser le temps disponible. La formule gratuite est par conséquent très pratique pour les prototypes ou les environnements d'intégration continue.
Pour le reste, le prix dépend des performances que vous souhaitez, des add-ons que vous utilisez, et surtout du temps d'utilisation.
Par exemple, un abonnement Hobby à $7 par mois vous coûtera seulement $3.50 si vous l'utilisez 15 jours et le désactivez ensuite. C'est très important à savoir, car la facture sera calculée en fonction du temps d'exécution de vos Dynos (voir plus loin).
Les machines utilisent une distribution nommée Cedar, qui est basée sur Ubuntu, et on peut configurer une application pour utiliser Cedar 14.04 ou Cedar 16.04 (les dernières LTS de Ubuntu).
Les applications sont exécutées dans des containers Linux appelés Dynos.
Il existe trois types de Dynos : web, worker et one-off et ils sont configurés par un fichier nommé Procfile.
web sont exécutés de façon persistante et sont configurés par votre Procfile et sont les seuls à recevoir les requêtes HTTP envoyés sur votre application.worker sont des scripts configurés dans votre Procfile et sont majoritairement utilisés pour des tâches de fond, comme des Queues.one-off sont des dynos temporaires que vous pouvez créer, par exemple en exécutant manuellement des scripts en ligne de commande avec heroku run {script...}. Ils sont utilisés également au déploiement d'une release (cela permet d'éviter qu'un déploiement de 10 minutes soit décompté de votre temps de gratuité...), mais aussi par le Heroku Scheduler pour orchestrer des tâches de fond (similaire à crontab).web, worker ou autre, l'exécution sera toujours effectuée dans un Dyno.
En tant qu'utilisateur, nous n'avons pas accès à la distribution. Nous ne pouvons donc pas modifier les packages de la machine.
En revanche, pour pallier à cela, Heroku utilise un système de buildpacks et d'add-ons qui, eux, vont pouvoir exécuter des commandes dans la machine afin d'installer certains packages.
Les buildpacks comprennent un ensemble de scripts ayant plusieurs responsabilités :
composer.json est présent à la racine de votre projet.
Il est également capable d'installer des extensions PHP si celles-ci sont présentes dans la section require de votre composer.json, comme "require": { "ext-intl": "*" } par exemple.
Les buildpacks sont indispensables à la configuration de base d'une application. À moins que vous n'exécutiez que des scripts en bash...
Les add-ons, eux, sont généralement là pour intégrer des services externes à votre projet, comme des bases de données, des rapports de logs, du profiling ou un système d'envoi de mails.
La plupart des add-ons sont payants mais offrent une option gratuite avec des services et perfomances limités.
Ils ont plusieurs avantages :
Allez, maintenant qu'on sait en quoi consistent les outils que nous allons utiliser, servons-nous-en !
Tout d'abord, on crée le projet quelque part sur notre machine :
$ composer create-project symfony/skeleton:^4.0 my_project
Le package symfony/skeleton ne contient qu'une seule chose : un fichier composer.json déterminant quelques dépendances pour créer un projet web avec Symfony, dont Flex.
Les dépendances principales qui nous permettent de faciliter tout ça sont les suivantes :
symfony/flex : Le plugin Composer dont on parlait au début de cet article.symfony/lts : Un simple package composer permettant de définir quelle version majeure de Symfony nous allons utiliser. Ce package définit simplement des conflits de version avec la version majeure supérieure.symfony/framework-bundle : Le package principal qui nous permet de créer un projet web avec Symfony.config/.public/.src/Kernel.php..env et .env.dist pour configurer notre projet..gitignore déjà prêt à l'usage.composer install/updatesymfony/console est désormais une dépendance de base de symfony/skeleton, Flex va également suivre la recette de ce package et installer un fichier bin/console comme nous l'adorons dans Symfony !
Toutes ces actions sont définies dans les différentes recettes des packages en question, et l'avantage c'est que grâce à Flex, si nous supprimons un package, tout ce qui a été préalablement installé et configuré par cette recette sera supprimé ! Plus besoin de se prendre la tête avec des suppressions manuelles si on désire supprimer un package !
gitC'est aujourd'hui indispensable à tout projet !
$ git init
Nous utiliserons Git plus tard, mais il fallait au moins préparer le terrain.
Pour tester notre projet nous allons utiliser le bundle WebServerBundle de Symfony, qui nous permet d'exécuter des commandes utilisant le serveur PHP intégré afin de pouvoir lancer notre projet en dev :
$ composer require --dev server
server est simplement un alias du package symfony/web-server-bundle, encore une fois, merci Flex !
Ce contrôleur sera nécessaire, car désormais il n'y a plus de contrôleur par défaut dans Symfony :
<?php
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController
{
/**
* @Route("/", name="homepage")
*/
public function index(): Response
{
return new Response('It works! ☺');
}
}
On exécute la commande du WebServerBundle pour voir notre site :
$ php bin/console server:run
[OK] Server listening on http://127.0.0.1:8000
// Quit the server with CONTROL-C.
Cela nous donne une page de ce genre :
Super ! Ça fonctionne, donc on peut partir du principe qu'on a un projet Symfony opérationnel !
Maintenant nous allons préparer le déploiement sur Heroku.
Dans un premier temps il faut télécharger l'application en ligne de commande fournie par Heroku : https://devcenter.heroku.com/articles/heroku-cli#download-and-install
Une fois fait, on peut l'exécuter pour vérifier qu'elle est bien installée et fonctionnelle :
$ heroku --version
heroku-cli/6.14.36-15f8a25 (linux-x64) node-v8.7.0
Évidemment, il faut d'abord créer un compte sur Heroku, et une fois fait, il faut indiquer à la CLI de Heroku quel compte nous utilisons :
$ heroku login
Enter your Heroku credentials:
Email: me@domain.com
Password: *************█
Logged in as me@domain.com
Cela va permettre à Heroku CLI de nous donner des détails sur nos projets, leur configuration, etc.
À partir de maintenant, toutes les commandes heroku seront exécutées depuis le dossier du projet.
Heroku permet de tout faire depuis la ligne de commande, alors profitons-en :
$ heroku create
Creating app... done, stark-escarpment-87840
https://stark-escarpment-87840.herokuapp.com/ | https://git.heroku.com/stark-escarpment-87840.git
Celui-ci nous donne l'URL finale du projet (utilisant le nom de domaine herokuapp.com) ainsi que l'URL de la remote git à utiliser.
Nous allons installer le buildpack PHP pour être sûrs de pouvoir automatiser tout ce dont nous avons besoin :
$ heroku buildpacks:set heroku/php
$ heroku buildpacks:set heroku/php
Buildpack set. Next release on stark-escarpment-87840 will use heroku/php.
Run git push heroku master to create a new release using this buildpack.
Note : En réalité nous n'avons pas vraiment besoin d'installer ce buildpack, puisqu'il est détecté automatiquement grâce à la présence d'un fichier composer.json à la racine de notre projet. Mais nous l'ajoutons manuellement histoire de faire les choses proprement.
Heroku nous propose de déployer mais nous ferons ça plus tard, quand le projet sera prêt :)
remoteL'intérêt de cette remote est de pouvoir déployer avec un simple git push.
Et l'url vient de nous être donnée, alors un simple copier/coller suffit :
$ git remote add heroku https://git.heroku.com/stark-escarpment-87840.git
Note : nommer la remote heroku permet à Heroku CLI de détecter automatiquement le projet en cours sans avoir à le spécifier en tant qu'argument à chaque commande.
Pour accéder directement à l'url de notre projet, on peut exécuter cette commande :
$ heroku open
Vous devriez voir quelque chose de ce genre :
Évidemment, pour l'instant il n'y a rien, mais au moins nous savons que Heroku a entendu nos demandes.
En premier lieu, il faut rajouter les variables d'environnement que Symfony nous dit de spécifier.
Les références sont dans .env.dist, et à chaque package que nous ajouterons, si des variables sont ajoutées, il faudra les rajouter manuellement à Heroku.
Pour l'instant, seules 2 variables sont demandées par Symfony :
$ heroku config:set APP_ENV=prod APP_SECRET=Wh4t3v3r
Nous allons maintenant configurer le projet pour l'envoyer sur Heroku.
Comme toute application web, nous avons besoin d'un serveur web : ça tombe bien, le buildpack PHP nous permet d'utiliser Apache ou Nginx directement !
Dans un premier temps, configurons Nginx.
Vous pouvez créer un fichier heroku/nginx_host.conf et y mettre ceci :
# Try to serve file directly, fallback to rewrite.
location / {
try_files $uri @rewriteapp;
}
# Rewrite all to index.php. This will trigger next location.
location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}
# Redirect everything to Heroku.
# In development, replace this with your php-fpm/php-cgi proxy.
location ~ ^/index\.php(/|$) {
try_files @heroku-fcgi @heroku-fcgi;
internal;
}
# Return 404 for all other php files not matching the front controller.
# This prevents access to other php files you don't want to be accessible.
location ~ \.php$ {
return 404;
}
Cette configuration fait plusieurs choses :
index.php, qui sera délégué au processus FCGI configuréphp-fpm).
Le Procfile est un fichier qui décrit les différents dynos que vous allez posséder dans votre projet.
Chaque dyno sera comptabilisé dans le temps de consommation relatif à votre abonnement.
Ici nous n'avons besoin que d'un seul dyno, en l'occurence un dyno de type web (pour rappel, le nom du dyno est unique, donc nous ne pouvons avoir qu'un seul dyno nommé web).
Chaque ligne du fichier se compose de deux informations : le type de dyno et le script à exécuter.
Le script correspondra ici à celui documenté dans le buildpack PHP, en l'occurrence une instance nginx suivie du nom du dossier servant de point d'entrée au vhost.
Nous devons également rajouter la configuration de nginx précédemment créée, nécessaire pour utiliser Symfony (sinon, seule la page d'accueil sera utilisable!).
web: vendor/bin/heroku-php-nginx -C heroku/nginx.conf public/
Cela suffira à Heroku pour qu'il puisse exécuter notre code.
Il est possible de personnaliser également la configuration de php-fpm, ou même de complètement surcharger toute la configuration de nginx, mais c'est juste l'affaire d'un argument spécifiant le fichier utilisé, et nous n'en avons pas besoin pour l'instant.
Dans le doute, la documentation sur Heroku vous sera utile pour savoir comment personnaliser vos paramètres.
Du coup, déployons notre projet !
$ git add .
$ git commit -m "Initial files"
$ git push heroku master
(...)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> PHP app detected
remote: -----> Bootstrapping...
remote: -----> Installing platform packages...
remote: - php (7.1.11)
remote: - nginx (1.8.1)
remote: - apache (2.4.29)
remote: -----> Installing dependencies...
remote: Composer version 1.5.2 2017-09-11 16:59:25
remote: Loading composer repositories with package information
remote: Installing dependencies from lock file
remote: Package operations: 40 installs, 0 updates, 0 removals
remote: (...)
remote: Generating optimized autoload files
remote: (...)
remote: Executing script cache:clear [OK]
remote: Executing script assets:install --symlink --relative public [OK]
remote:
remote: -----> Preparing runtime environment...
remote: -----> Checking for additional extensions to install...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 16.7M
remote: -----> Launching...
remote: Released v13
remote: https://stark-escarpment-87840.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/stark-escarpment-87840.git
Le projet est maintenant déployé, nous pouvons l'ouvrir et le tester :
$ heroku open
Et nous devrions voir ceci :
Nos projets sont bien plus complexes et utilisent d'autres composants, alors continuons sur notre lancée.
D'abord, installer Doctrine ORM :
$ composer require orm-pack
Using version ^1.0 for symfony/orm-pack
(...)
Symfony operations: 3 recipes (7d946f30d2601a4530d4c10790aefad1)
- Configuring doctrine/doctrine-cache-bundle (1.3.2): From auto-generated recipe
- Configuring doctrine/doctrine-bundle (1.6): From github.com/symfony/recipes:master
- Configuring doctrine/doctrine-migrations-bundle (1.2): From github.com/symfony/recipes:master
(...)
Les différentes recettes Flex vont rajouter ceci :
DoctrineBundle :DATABASE_URL à notre fichier .env.DoctrineMigrationsBundle :src/Migrations.Ensuite, il faut installer l'addon Heroku nécessaire à l'utilisation de notre base de données :
$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on stark-escarpment-87840... 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-flexible-83322 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
Du coup, Heroku va utiliser un autre serveur (qui ne nous concerne pas) pour gérer la base de données, ce qui facilite grandement la gestion & migration de l'application tout en laissant la BDD de son côté.
Note : Par défaut j'utilise PostgreSQL ici, tout simplement parce qu'Heroku dispose de facilités d'utilisations et de monitoring avec ce SGBD, mais il existe aussi de très bons add-ons pour MySQL ou MariaDB, comme ClearDB ou JawsDB, qui sont eux aussi des services cloud externes, et qui peuvent être intégrés à Heroku tout comme heroku-postgresql.
Il faut donc obligatoirement modifier nos fichiers .env et .env.dist pour changer le driver PDO de mysql (utilisé par défaut) à pgsql.
L'installation de l'addon aura automatiquement rajouté une variable d'environnement à la configuration du projet :
`` bash
$ heroku config
=== stark-escarpment-87840 Config Vars
APP_ENV: prod
APP_SECRET: Wh4t3v3r
DATABASE_URL: postgres://... <--- Cette variable vient d'être rajoutée par l'addon heroku-postgresql
%%CODEBLOCK18%%
$ php bin/console doctrine:database:create
$ php bin/console doctrine:migrations:diff
%%CODEBLOCK19%%
$ php bin/console doctrine:database:drop --force
$ php bin/console doctrine:database:create
$ php bin/console doctrine:migrations:diff
%%CODEBLOCK20%%php
<?php declare(strict_types = 1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20171106100053 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SEQUENCE Post_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE Post (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE Post_id_seq CASCADE');
$this->addSql('DROP TABLE Post');
}
}
%%CODEBLOCK21%%
$ php bin/console doctrine:migrations:migrate
Application Migrations
WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20171106100053 from 0
++ migrating 20171106100053
-> CREATE SEQUENCE Post_id_seq INCREMENT BY 1 MINVALUE 1 START 1
-> CREATE TABLE Post (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, PRIMARY KEY(id))
++ migrated (2.7s)
------------------------
++ finished in 2.7s
++ 1 migrations executed
++ 2 sql queries
%%CODEBLOCK22%%json
{
"scripts": {
"compile": [
"php bin/console doctrine:migrations:migrate"
]
}
}
%%CODEBLOCK23%%
$ git add . && git commit -m "Setup migrations"
$ git push heroku master
(...)
remote: -----> Running 'composer compile'...
remote: > php bin/console doctrine:migrations:migrate
remote:
remote: Application Migrations
remote:
remote:
remote: Migrating up to 20171106100053 from 0
remote:
remote: ++ migrating 20171106100053
remote:
remote: -> CREATE SEQUENCE Post_id_seq INCREMENT BY 1 MINVALUE 1 START 1
remote: -> CREATE TABLE Post (id INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, PRIMARY KEY(id))
remote:
remote: ++ migrated (0.06s)
remote:
remote: ------------------------
remote:
remote: ++ finished in 0.06s
remote: ++ 1 migrations executed
remote: ++ 2 sql queries
(...)
%%CODEBLOCK24%%php
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SimpleMessageCommand extends Command
{
protected static $defaultName = 'app:simple-message';
protected function configure()
{
$this->setDescription('Simply sends a message to stdout and stderr.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$now = date('Y-m-d H:i:s');
$output->writeln("[$now] Stdout message");
fwrite(STDERR, "[$now] Stderr message");
}
}
`
L'idée c'est de pouvoir consulter les logs de Heroku pour voir ces messages.
**Pro tip:** Depuis Symfony 3.4, on peut utiliser la propriété statique Command::$defaultName. Si notre commande est définie en tant que service, cela permettra à Symfony d'optimiser le chargement de la console et la compilation du container en n'instanciant pas la commande.
#### Installer Heroku Scheduler
Heroku Scheduler est l'add-on qui va nous permettre d'exécuter des tâches à des intervalles réguliers personnalisables.
Installons-le dans notre projet :
%%CODEBLOCK25%%
Et maintenant on va ouvrir cet add-on pour le personnaliser :
%%CODEBLOCK26%%
Vous devriez voir ceci:

Le bouton Add new job va nous permettre de faire exactement ce qu'il nous faut !

Alors la fréquence est clairement moins flexible qu'une _vraie_ tâche cron, mais pour les usages les plus simples, ça reste la meilleure solution. Sinon, il faudra un worker, ce qui est plus complexe à mettre en place (et est plus cher).
On peut en tout cas exécuter notre tâche :
* Une fois par jour à une heure/demi-heure donnée.
* Toutes les heures, à la dizaine de minutes donnée.
* Toutes les 10mn à partir du moment où la tâche est créée / mise à jour.
Une fois votre commande configurée, vous pouvez attendre quelques minutes que celle-ci s'exécute.
Lorsque le temps est passé, vous pouvez voir les logs:
%%CODEBLOCK27%%
On voit bien nos messages Stdout et Stderr s'afficher !
Et voilà, nous avons une routine correctement configurée !
**Note :** Attention au temps d'exécution de vos commandes, car celui-ci sera décompté du temps consommé de votre dyno, qui peut vous être facturé selon votre abonnement. Ceci dit, une commande qui dure 5 secondes, exécutée 144 fois par jour, cela fait 720 secondes de consommées. Ce n'est pas grand chose comparé aux 2592000 secondes pour un serveur web allumé 24h/24...
### Améliorer son environnement Heroku
Heroku étant plein d'addons, pour la plupart gratuits, je vous en recommande quelques-uns :
* [Autobus](https://elements.heroku.com/addons/autobus), un système de backups pour votre base de données, très pratique et dont le plan gratuit est idéal pour les projets simples.
* [Blackfire](https://elements.heroku.com/addons/blackfire) (beta), l'indémodable outil de profilage pour tous nos projets PHP !
* [Mailgun](https://elements.heroku.com/addons/mailgun), excellent outil d'envoi d'emails, qui peut être directement branché à Swiftmailer grâce à la variable d'environnement MAILER_URL, et dont le plan gratuit avec 400 mails par jour (soit 12000 par mois) est largement suffisant pour la plupart des projets (le plan suivant étant à 50000 mails par mois...).
* [Papertrail](https://elements.heroku.com/addons/papertrail), outil de monitoring des logs de tous vos dynos, très utile pour garder un œil sur vos erreurs PHP. Il peut vous envoyer un mail lorsqu'il y a des erreurs à intervalles réguliers, permet de créer des filtres pour les types d'erreurs, de commandes, etc.. Le gros avantage c'est que nous n'avons même pas besoin de configurer monolog autrement qu'en lui disant de tout envoyer vers php://stderr !
* [Deploy Hooks](https://devcenter.heroku.com/articles/deploy-hooks), un bon moyen d'envoyer une petite notification de succès d'un déploiement sur Slack, IRC, par email ou même avec une requête HTTP à n'importe quelle URL.
### Conclusion
Heroku est un PaaS très simple à utiliser, mais surtout, il est excellent pour le test, car il suffit d'utilier des [review apps](https://devcenter.heroku.com/articles/github-integration-review-apps) pour la preprod, et étant donné que la preprod n'est pas utilisée non-stop, on peut largement utiliser l'abonnement gratuit pour ça !
Pour l'upload et le stockage de fichiers, il vous faudra utiliser Amazon S3 et vous référer à la documentation en [suivant ce lien](https://devcenter.heroku.com/articles/s3) et utiliser les références à S3 dans votre code.
---
Chez [Agate Éditions](https://www.studio-agate.com/fr/), nous avons fait le choix d'utiliser Heroku pour notre projet, une application monolithique multi-domaines qui héberge des portails et des sites relatifs aux jeux de rôle du studio, notamment un gestionnaire de personnages et une application de cartographie interactive.
Merci de cette lecture ! Vous pouvez me retrouver un peu partout sur le web avec le pseudo @pierstoval` !
🌑 🌘 🌗 🌖 🌕 🌔 🌓 🌒 🌑