Je me suis mis au défi d’idéer, de créer et de publier une application en seulement 7 jours.
On dit que lorsque le seul outil dont vous disposez est un marteau, chaque problème a tendance à ressembler à un clou. Les ingénieurs se répartissent généralement en deux catégories ici: soit ils créent chaque application avec leur pile «préférée» et s’égarent rarement, soit ils abordent chaque nouveau projet avec une technologie différente, afin d’apprendre et de grandir.
Malheureusement, les deux écoles de pensée manquent de sens.
Le choix de chaque composant de la pile technologique d’un produit doit être déterminé par les besoins de l’utilisateur final, ainsi que par les considérations d’évolutivité et les plates-formes de déploiement qui ont été identifiées lors de la phase de contraintes et d’hypothèses.
Pour ce projet, je m’attends à un ratio 10: 1 de lectures sur écritures. Voici ma réflexion sur chaque composant de la pile:
Backend API
Pour l’API, j’ai choisi Node.js exécutant le middleware Express. Cette combinaison me permet de tirer parti d’un générateur MVP que j’ai écrit précédemment.
Cela m’a permis de décrire la «forme» de mes données et d’avoir mon générateur MVP cracher les larges traits du code Node.js et Express, sans que je doive copier et coller des modèles, des points de terminaison et d’autres minuties.
Une API construite de cette façon prend en charge l’architecture d’API RESTful sans état, et elle est très facilement insérée dans un conteneur Docker pour prendre en charge un déploiement rapide, des correctifs et une évolutivité.
J’ai structuré les points de terminaison individuels autour de modèles dans la base de données et les ai séparés en tant que microservices.
J’ai également écrit un middleware personnalisé qui vérifie un jeton Web Javascript (JWT) valide qui corrèle la demande avec un utilisateur dans la base de données (ou gère le rejet). Cela me permet d’authentifier et d’identifier en toute sécurité les utilisateurs qui font des demandes sans transmettre d’informations d’identification utilisateur spécifiques en dehors du jeton (opaque).
Base de données
En raison de la tokenisation du schéma d’authentification dans l’API, j’ai pu simplifier les rôles d’utilisateur et les étendues de propriété, ce qui signifie que mon application pourrait choisir librement entre une base de données NoSQL (schéma au niveau logiciel) telle que Cassandra (colonne large ), MongoDB (document), Redis (valeur-clé) et Apache Giraffe (graphique), ou une base de données relationnelle comme MySQL, PostgreSQL (objet-relationnel) ou MariaDB.
J’ai décidé qu’en raison de la forme de mes données – en particulier les relations avec la clé étrangère UUIDv4 hasMany et appartiennent à des jointures – j’utiliserais l’une des bases de données relationnelles. J’ai éliminé MariaDB des concurrents dès le début, car il s’agit d’une fourchette de MySQL par Sun Microsystems, et sa petite (er) base installée et son empreinte communautaire rendent le dépannage et le support à long terme plus inquiétants que MySQL ou Postgres.
Cela m’a laissé un choix entre MySQL et Postgres, où j’étais principalement préoccupé par les facteurs suivants:
- Vitesse (performances brutes)
- Évolutivité (clustering, maître / esclave, partitionnement)
- Fiabilité (intégrité des données)
- Conformité ACID
- Colonnes JSON
Heureusement, j’avais une certaine expérience en la matière. Il y a plusieurs années, mon ami et brillant technologue Tracy Lee m’a engagé pour enseigner à Hewlett-Packard Enterprise un cours d’une journée complète sur les considérations de conception entre MySQL et Postgres alors qu’ils envisageaient de migrer leur infrastructure technique vers Postgres. La plupart des choses dont j’ai parlé ce jour-là sont les mêmes aujourd’hui.
Pour cette application, les nuances d’une base de données relationnelle objet (Postgres) par rapport à une base de données relationnelle pure (MySQL) n’auront pas autant d’importance. Dans la base de données relationnelle objet, je serais en mesure d’utiliser des fonctionnalités telles que l’héritage de table et la surcharge de fonctions, mais je n’ai pas de cas d’utilisation spécifique ici pour mon application.
Face à la vitesse, il est vrai que les versions modernes de MySQL ont comblé l’écart avec Postgres en termes de performances brutes. Auparavant, le moteur MyISAM (plus ancien) de MySQL avait un avantage en termes de lectures par rapport à Postgres, et parce que mon application est lourde en lecture, j’aurais penché dans cette direction. Cependant, le moteur InnoDB dans MySQL (qui autorise les transactions, les contraintes clés et d’autres fonctionnalités importantes pour moi) est plus équilibré et n’a pas l’avantage de lire.
Une autre considération du côté vitesse est les allocations de mémoire. Postgres fait tourner un processus enfant pour chaque connexion, pesant jusqu’à 10 Mo chacun. Le modèle de thread par connexion de MySQL a une taille de pile par défaut de 256 Ko par thread sur les plates-formes 64 bits. Pas une différence significative pour mon application, mais mérite d’être considérée.
Pour l’évolutivité, Postgres a été conçu pour évoluer hors de la boîte – même au détriment de la vitesse brute dans certains cas. De ce fait, Postgres à grande échelle est gérable par une équipe beaucoup plus petite et ne nécessite pas d’aide extérieure de services tels que Percona, qui ont construit des industries artisanales entières à partir de la complexité de la boxe avec MySQL en matière de réplication et de clustering. Cela signifie que vous pouvez généralement simplement «jeter plus de matériel» sur Postgres, ce qui convient parfaitement à mon infrastructure cloud.
En termes de fiabilité, les deux bases de données disposent de systèmes robustes pour gérer l’intégrité des données. Lorsque vous optez pour MySQL, le moteur de cluster MySQL utilise la réplication synchrone via un processus de validation en deux phases pour essentiellement «garantir» que les données sont écrites sur plusieurs nœuds. Ce mécanisme asynchrone est ce que les gens veulent dire quand ils disent «réplication MySQL». Postgres utilise une forme de réplication synchrone (qu’ils appellent «2-safe»). Essentiellement, deux instances de base de données s’exécutent simultanément et la base de données maître est synchronisée avec une base de données esclave. À moins que les deux bases de données ne tombent en panne simultanément, il est peu probable que des données soient perdues. Il convient toutefois de noter que la réplication synchrone signifie que chaque écriture attend la confirmation de la réception des instances maître et esclave.
Conformité ACID (atomicité, cohérence, isolement, durabilité pour atténuer la perte de données en cas de panne de courant ou autre interruption inattendue) était une pomme de discorde auparavant avec MySQL contre PostgreSQL. Cependant, avec le choix InnoDB plus moderne pour MySQL, ils sont tous les deux à peu près les mêmes de nos jours. Toutes les instances Postgres sont conformes à ACID, pour ce qu’elles valent.
Pour JSON, Postgres utilise TOAST, qui est un stockage dédié de «shadow table». Votre objet JSON est extrait en mémoire si (et seulement si) la ligne et la colonne sont sélectionnées. Cela économise de la mémoire cache et est compressible en tant qu’objet TOAST. Postgres prend en charge les types géométriques / SIG, les adresses réseau, JSONB qui peut être indexé (y compris les paires valeur-clé avec HSTORE), la prise en charge XML native, l’UUID natif, les horodatages sensibles au fuseau horaire «out of the box», mais vous permet également d’ajouter les vôtres types, si nécessaire. MySQL a ajouté la compression de page transparente à partir de Fusion-io, une société de stockage SSD appartenant à Sandisk, qui appartient elle-même à Western Digital. Cette fonctionnalité permet de prolonger la durée de vie des SSD, mais comme j’utilise une infrastructure cloud redondante et conteneurisée par rapport au bare metal, ce n’est pas une considération majeure pour moi.
Au final, j’ai opté pour Postgres comme base de données et Sequelize v5 comme ORM à l’intérieur de Node.js.
Conteneurisation
Comme j’avais structuré les points de terminaison individuels autour de modèles dans la base de données, j’ai pu les séparer facilement en tant que microservice. J’ai utilisé Docker Compose sur mon monorepo pour construire mes conteneurs individuels et Kubernetes pour orchestrer le déploiement. Kubernetes Ingress, Services et Deployments fonctionnent ensemble pour servir les composants frontaux et principaux. Ma base de données, cependant, vit entièrement sur Google Cloud Platform en tant que RDS (Postgres).
J’ai utilisé le modèle de conception de sidecar pour stocker en toute sécurité mes variables d’environnement et mes informations d’identification Cloud SQL. Cela me permet de définir un secret Kubernetes, puis de charger le secret dans un volume monté avec mon conteneur. Le conteneur atteint ensuite le volume pour extraire des éléments comme les noms d’utilisateur et les mots de passe. J’ai également établi un utilisateur proxy Google Container Engine à l’aide des rôles Google Cloud IAM, et je charge latéralement une copie de l’image du conteneur GCE-proxy à partir du registre Google Cloud. Cela me permet d’accéder à la base de données à partir de «localhost» sur tous les conteneurs principaux, simplifiant les migrations ou les changements ultérieurs.
spec:
volumes:
- name: my-secrets-volume
secret:
secretName: connectionfox-cloudsql-instance-credentials
containers:
- name: connectionfox-api-news
image: gcr.io/connectionfox-[redacted]/connectionfox-api-news:latest
ports:
- name: api-news
containerPort: 3002
protocol: TCP
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: connectionfox-cloudsql-db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: connectionfox-cloudsql-db-credentials
key: password
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: connectionfox-cloudsql-db-credentials
key: dbname
- name: connectionfox-cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.17
command: ["/cloud_sql_proxy",
"-instances=connectionfox-[redacted]:us-west2:connectionfox-db=tcp:5432",
"-credential_file=/secrets/connectionfox-cloudsql/credentials.json"]
volumeMounts:
- name: my-secrets-volume
mountPath: /secrets/connectionfox-cloudsql
readOnly: true
Infrastructure cloud
J’ai de l’expérience dans le déploiement de produits à grande échelle sur Azure, Amazon Web Services (AWS) et Google Cloud Platform (GCP), ainsi que sur une collection de serveurs Digital Ocean en dehors des «trois grands». La plupart des choses sont assez égales entre Microsoft, Amazon et Google, donc pour moi, cela se résumait à choisir celui que je connaissais le mieux en raison des contraintes de temps.
La base de données est un service GCP RDS, GCE exécute les conteneurs, Kubernetes orchestre les déploiements et la surveillance de l’API GCP gère les rapports sur les erreurs d’API. C’est un déploiement assez simple mais robuste, et est entièrement géré à partir de la ligne de commande, ce qui m’évite beaucoup de maux de tête. Cela me permet également d’écrire des scripts pour créer, redéployer et autrement gérer des services.
Front-end et infrastructure d’interface utilisateur mobile
Lors du choix entre Angular et React, la principale considération pour moi est souvent de savoir si mon application a ou non un composant mobile. Dans ce cas, je choisis souvent React car React Native peut partager beaucoup de code et de fonctionnalités. Cependant, comme je dispose de peu de temps pour réparer bugs, je ne peux pas créer autant de bugs. Cela signifie que TypeScript était le choix pour moi plutôt que Javascript, et Angular a été construit avec TypeScript à l’esprit dès le départ.
Je ne mets pas à jour une grande quantité d’éléments DOM dans cette application. Si j’étais, cependant, je pencherais pour le DOM virtuel de React contre la méthode d’Angular pour accéder au vrai DOM, uniquement en fonction des performances.
La structure du projet était beaucoup plus facile à échafauder en angulaire en raison de l’excellente CLI angulaire et Nrwl’S NX (notez qu’il s’agit de liens séparés). Bien que NX prenne également en charge React, je ne l’ai pas encore utilisé dans ce contexte. La structure familière «HTML, CSS, Component» d’Angular m’a permis de commencer très rapidement le prototypage et j’ai pu parcourir plus d’une douzaine de versions de chaque page rapidement avec mes utilisateurs.
En termes de tests, Jasmine d’Angular (pour une lecture humaine) et Karma (pour des tests multi-navigateurs et multi-plateformes) convenaient très bien à mon cas d’utilisation. Jest (avec Enzyme) aurait été fonctionnellement très similaire.
En fin de compte, en raison de la parité entre les deux, j’ai choisi Angular pour son adhérence «prête à l’emploi» aux paradigmes de structure et de design. React a tendance à être un langage facile à maîtriser pour un débutant, ce qui est à la fois bon et mauvais. Angular nécessite une compréhension beaucoup plus nuancée du processus de programmation et de développement, ce qui signifie que moins de personnes utilisent globalement la technologie, mais ceux qui ont tendance à fournir de meilleurs exemples de techniques «appropriées» sur Stack Overflow, et al.
En ce qui concerne l’aspect «natif» du mobile, j’ai choisi de rendre Connection Fox très réactif via HTML5 et Bootstrap CSS, ce qui signifie que j’ai utilisé les classes hidden-sm et hidden-md-up (cela est particulièrement visible sur la connexion page) pour masquer les colonnes et déplacer les éléments en fonction de la taille de l’écran et de la plate-forme. Si j’avais plus de temps, ou si j’avais utilisé React, j’aurais peut-être opté pour une application native.