Prérequis et bonnes pratiques pour les applications

Prérequis pour les applications sur Atlas

Ce document explique les prérequis et les bonnes pratiques pour les applications déployées sur la plateforme Atlas, ainsi que les antipatterns à éviter.

Applications Cloud Native

La plateforme Atlas est conçue exclusivement pour héberger des applications cloud native. Ces applications sont spécifiquement développées pour tirer parti des avantages du cloud computing et des environnements conteneurisés comme Kubernetes.

Caractéristiques des applications cloud native

Les applications cloud native présentent généralement les caractéristiques suivantes :

  • Conteneurisées : Empaquetées dans des conteneurs Docker pour garantir la cohérence entre les environnements
  • Orchestrées dynamiquement : Gérées par Kubernetes pour optimiser l'utilisation des ressources
  • Orientées microservices : Composées de services faiblement couplés et indépendamment déployables
  • Résilientes : Capables de gérer les pannes sans impact majeur sur la disponibilité
  • Scalables : Pouvant s'adapter automatiquement à la charge
  • Observable : Fournissant des métriques, des logs et des traces pour le monitoring
  • Automatisées : Déployées via des pipelines CI/CD

Méthodologie Twelve-Factor App

Pour garantir que vos applications sont véritablement cloud native, nous recommandons fortement de suivre la méthodologie Twelve-Factor App, qui définit un ensemble de bonnes pratiques pour le développement d'applications modernes.

Antipatterns et mauvaises pratiques

Voici une liste d'antipatterns et de mauvaises pratiques à éviter lors du développement d'applications pour la plateforme Atlas, ainsi que des alternatives recommandées.

1. Stockage de fichiers dans le système de fichiers local

Problème : Les conteneurs sont éphémères par nature. Toutes les données stockées dans le système de fichiers local d'un conteneur sont perdues lorsque le conteneur est redémarré ou recréé. De plus, si plusieurs instances de l'application sont exécutées, chacune aura son propre système de fichiers isolé.

Conséquences : - Perte de données lors des redémarrages ou des mises à jour - Incohérences entre les instances de l'application - Impossibilité de scaler horizontalement

Alternatives : - Utiliser des buckets S3 pour le stockage persistant des fichiers - Stocker les données structurées dans une base de données - Pour les fichiers temporaires, utiliser la mémoire ou un volume temporaire (emptyDir) avec la conscience que ces données sont éphémères

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Mauvaise pratique
const fs = require('fs');
fs.writeFileSync('/app/data/user-uploads/file.txt', data);

// Bonne pratique
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3Client = new S3Client({ endpoint: process.env.S3_ENDPOINT });
await s3Client.send(new PutObjectCommand({
  Bucket: process.env.BUCKET_NAME,
  Key: 'user-uploads/file.txt',
  Body: data
}));

2. Supposition d'instance unique (singleton)

Problème : Supposer qu'il n'y a qu'une seule instance de l'application en cours d'exécution est une erreur courante. Dans un environnement cloud, les applications sont souvent exécutées avec plusieurs réplicas pour la haute disponibilité et la scalabilité.

Conséquences : - Problèmes de concurrence et de verrouillage - Incohérences de données - Impossibilité de scaler horizontalement - Points uniques de défaillance

Alternatives : - Concevoir l'application pour être sans état (stateless) - Utiliser des mécanismes de verrouillage distribué pour les opérations critiques - Stocker l'état partagé dans des services externes (bases de données, caches distribués) - Implémenter des mécanismes de synchronisation si nécessaire

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Mauvaise pratique
let globalCounter = 0;
app.post('/increment', (req, res) => {
  globalCounter++;
  res.json({ counter: globalCounter });
});

// Bonne pratique
app.post('/increment', async (req, res) => {
  const result = await db.collection('counters').findOneAndUpdate(
    { _id: 'main-counter' },
    { $inc: { value: 1 } },
    { returnDocument: 'after', upsert: true }
  );
  res.json({ counter: result.value });
});

3. Migrations de base de données au démarrage de l'application

Problème : Exécuter des migrations de base de données au démarrage de l'application peut causer des problèmes dans un environnement avec plusieurs instances, où plusieurs migrations pourraient s'exécuter simultanément.

Conséquences : - Verrouillage de la base de données - Migrations concurrentes causant des erreurs - Temps de démarrage prolongés - Échecs de déploiement si la migration échoue

Alternatives : - Exécuter les migrations comme une tâche séparée avant le déploiement de l'application - Utiliser des outils de migration avec support de verrouillage - Implémenter un mécanisme de leader election pour que seule une instance exécute la migration - Concevoir des migrations idempotentes et réversibles

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Mauvaise pratique: Migration dans le conteneur d'application
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:latest
        command: ["sh", "-c", "npm run migrate && npm start"]

# Bonne pratique: Migration comme Job séparé
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  template:
    spec:
      containers:
      - name: migration
        image: my-app:latest
        command: ["npm", "run", "migrate"]
      restartPolicy: Never
  backoffLimit: 3

4. Dépendance à l'adresse IP ou au nom d'hôte

Problème : Coder en dur des adresses IP ou des noms d'hôte spécifiques dans l'application limite sa portabilité et sa flexibilité dans un environnement cloud où les adresses peuvent changer.

Conséquences : - Fragilité lors des redéploiements - Difficultés à migrer entre environnements - Problèmes lors du scaling

Alternatives : - Utiliser des variables d'environnement pour toutes les configurations d'adresse - Utiliser la découverte de services fournie par Kubernetes - Implémenter des mécanismes de retry et de circuit breaker pour la résilience

Exemple :

1
2
3
4
5
// Mauvaise pratique
const dbClient = new DbClient('postgres://user:pass@db-server:5432/mydb');

// Bonne pratique
const dbClient = new DbClient(process.env.DATABASE_URL);

5. Sessions utilisateur stockées en mémoire

Problème : Stocker les sessions utilisateur en mémoire empêche le scaling horizontal et cause la perte des sessions lors des redémarrages.

Conséquences : - Déconnexions utilisateur lors des redéploiements - Impossibilité de répartir la charge entre plusieurs instances - Consommation excessive de mémoire

Alternatives : - Utiliser un store de sessions externe (Redis, MongoDB) - Implémenter des sessions sans état avec JWT - Utiliser des cookies chiffrés côté client

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Mauvaise pratique
const session = require('express-session');
app.use(session({
  secret: 'secret-key',
  resave: false,
  saveUninitialized: true
}));

// Bonne pratique
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redisClient = require('./redis-client');

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

6. Logs écrits dans des fichiers

Problème : Écrire des logs dans des fichiers locaux rend difficile leur collecte et leur analyse dans un environnement distribué.

Conséquences : - Difficulté à accéder aux logs après un crash - Impossibilité de centraliser les logs - Consommation d'espace disque dans le conteneur

Alternatives : - Écrire les logs sur la sortie standard (stdout/stderr) - Utiliser un format structuré (JSON) pour faciliter l'analyse - Laisser l'infrastructure de la plateforme gérer la collecte et l'agrégation des logs

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Mauvaise pratique
const fs = require('fs');
const logger = {
  info: (message) => fs.appendFileSync('/var/log/app.log', `INFO: ${message}\n`)
};

// Bonne pratique
const logger = {
  info: (message) => console.log(JSON.stringify({
    level: 'info',
    message,
    timestamp: new Date().toISOString()
  }))
};

7. Longues opérations de démarrage

Problème : Les applications qui prennent beaucoup de temps à démarrer ralentissent les déploiements et peuvent causer des problèmes avec les health checks de Kubernetes.

Conséquences : - Déploiements lents - Échecs de readiness probes - Temps d'arrêt prolongés lors des mises à jour

Alternatives : - Optimiser le temps de démarrage de l'application - Séparer les tâches longues en jobs distincts - Implémenter un démarrage progressif (lazy loading) - Utiliser des readiness probes appropriées

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Bonne pratique: Readiness probe avec délai initial
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:latest
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

8. Configuration en dur dans le code

Problème : Coder en dur la configuration dans l'application limite sa flexibilité et nécessite une recompilation pour chaque changement.

Conséquences : - Difficultés à adapter l'application à différents environnements - Nécessité de maintenir plusieurs versions du code - Risques de sécurité (secrets exposés)

Alternatives : - Utiliser des variables d'environnement pour la configuration - Implémenter un système de configuration externe - Séparer clairement le code de la configuration

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Mauvaise pratique
const config = {
  apiUrl: 'https://api.example.com',
  maxConnections: 100,
  timeout: 5000
};

// Bonne pratique
const config = {
  apiUrl: process.env.API_URL,
  maxConnections: parseInt(process.env.MAX_CONNECTIONS || '100', 10),
  timeout: parseInt(process.env.TIMEOUT || '5000', 10)
};

9. Absence de health checks

Problème : Sans health checks appropriés, Kubernetes ne peut pas déterminer si une application fonctionne correctement et quand elle est prête à recevoir du trafic.

Conséquences : - Trafic envoyé à des instances non fonctionnelles - Redémarrages inutiles ou manqués - Déploiements instables

Alternatives : - Implémenter des liveness probes pour détecter les blocages - Implémenter des readiness probes pour indiquer quand l'application est prête - Concevoir des health checks qui vérifient les dépendances critiques

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Exemple d'implémentation de health checks
app.get('/health/liveness', (req, res) => {
  // Vérification simple que l'application répond
  res.status(200).send('OK');
});

app.get('/health/readiness', async (req, res) => {
  try {
    // Vérifier les connexions aux dépendances
    await checkDatabaseConnection();
    await checkRedisConnection();
    res.status(200).send('Ready');
  } catch (error) {
    res.status(503).send('Not Ready');
  }
});

10. Gestion inappropriée des signaux de terminaison

Problème : Ne pas gérer correctement les signaux de terminaison (SIGTERM, SIGINT) peut entraîner une fermeture brutale de l'application et potentiellement une perte de données.

Conséquences : - Connexions interrompues - Transactions non terminées - Ressources non libérées - Perte de données

Alternatives : - Implémenter des gestionnaires de signaux pour une fermeture gracieuse - Fermer proprement les connexions aux bases de données et autres ressources - Terminer les requêtes en cours avant de s'arrêter

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Bonne pratique: Gestion gracieuse de la terminaison
const server = app.listen(process.env.PORT || 3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM signal received: closing HTTP server');

  // Arrêter d'accepter de nouvelles connexions
  server.close(() => {
    console.log('HTTP server closed');

    // Fermer les connexions à la base de données
    db.disconnect()
      .then(() => {
        console.log('Database connections closed');
        process.exit(0);
      })
      .catch((err) => {
        console.error('Error during shutdown', err);
        process.exit(1);
      });
  });
});

Liens utiles

Paramètres d’affichage

Choisissez un thème pour personnaliser l’apparence du site.