Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Blue-Green Deployment i Zero-Downtime - sztuka bezprzerwowej zmiany warty

Mistrzu strategii! Senator Cicero powiada: "Imperium nigdy nie śpi, a drogi muszą być zawsze otwarte." Tak jak rzymskie legiony zmieniały wartę bez pozostawiania murów bez obrony, tak nowoczesne aplikacje muszą być aktualizowane bez przerw w działaniu. Czas poznać zaawansowane strategie wdrażania!

Blue-Green Deployment to technika, w której utrzymujemy dwa identyczne środowiska produkcyjne (blue i green). W danym momencie tylko jedno obsługuje ruch, a drugie jest gotowe na nową wersję. Przełączenie następuje natychmiastowo - bez przestojów.

Koncepcja Blue-Green Deployment

Wyobraź sobie dwie bramy do miasta Imperium. Jedna (Blue) jest aktualnie otwarta i obsługuje podróżnych. Druga (Green) jest przygotowywana z nową wersją fortyfikacji. Gdy Green jest gotowe, strażnicy przekierowują ruch - zero przestojów!

1// src/deployment/blue-green.config.ts
2interface DeploymentSlot {
3  name: 'blue' | 'green';
4  port: number;
5  version: string;
6  status: 'active' | 'standby' | 'deploying';
7  healthCheckUrl: string;
8  startedAt: Date;
9}
10
11interface BlueGreenConfig {
12  activeSlot: 'blue' | 'green';
13  slots: {
14    blue: DeploymentSlot;
15    green: DeploymentSlot;
16  };
17  loadBalancerUrl: string;
18  healthCheckInterval: number; // ms
19  healthCheckRetries: number;
20  rollbackTimeout: number;    // ms
21}
22
23const deploymentConfig: BlueGreenConfig = {
24  activeSlot: 'blue',
25  slots: {
26    blue: {
27      name: 'blue',
28      port: 3001,
29      version: '2.3.0',
30      status: 'active',
31      healthCheckUrl: 'http://blue.imperium.internal:3001/health',
32      startedAt: new Date(),
33    },
34    green: {
35      name: 'green',
36      port: 3002,
37      version: '2.4.0',
38      status: 'standby',
39      healthCheckUrl: 'http://green.imperium.internal:3002/health',
40      startedAt: new Date(),
41    },
42  },
43  loadBalancerUrl: 'http://lb.imperium.internal',
44  healthCheckInterval: 5000,
45  healthCheckRetries: 3,
46  rollbackTimeout: 30000,
47};

Rolling Updates

Rolling updates to inna strategia - stopniowe zastępowanie starych instancji nowymi, jedna po drugiej, jak zmiana warty centurionów. Dzięki temu zawsze część instancji obsługuje ruch.

1// src/deployment/rolling-update.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4interface Instance {
5  id: string;
6  version: string;
7  status: 'running' | 'updating' | 'ready' | 'failed';
8  port: number;
9}
10
11@Injectable()
12export class RollingUpdateService {
13  private readonly logger = new Logger(RollingUpdateService.name);
14
15  async performRollingUpdate(
16    instances: Instance[],
17    newVersion: string,
18    maxUnavailable: number = 1,
19  ): Promise<void> {
20    this.logger.log(
21      `Rozpoczynam rolling update do wersji ${newVersion}`
22    );
23    this.logger.log(
24      `Instancje: ${instances.length}, max niedostepnych: ${maxUnavailable}`
25    );
26
27    // Aktualizuj instancje partiami
28    for (let i = 0; i < instances.length; i += maxUnavailable) {
29      const batch = instances.slice(i, i + maxUnavailable);
30
31      for (const instance of batch) {
32        instance.status = 'updating';
33        this.logger.log(`Aktualizuje instancje ${instance.id}...`);
34
35        // 1. Wylacz instancje z load balancera
36        await this.removeFromLoadBalancer(instance);
37
38        // 2. Poczekaj az biezace requesty sie zakoncza
39        await this.drainConnections(instance);
40
41        // 3. Zaktualizuj do nowej wersji
42        await this.deployNewVersion(instance, newVersion);
43
44        // 4. Sprawdz health check
45        const healthy = await this.waitForHealthy(instance);
46
47        if (healthy) {
48          instance.status = 'running';
49          instance.version = newVersion;
50          await this.addToLoadBalancer(instance);
51          this.logger.log(`Instancja ${instance.id} zaktualizowana`);
52        } else {
53          instance.status = 'failed';
54          this.logger.error(
55            `Instancja ${instance.id} - rollback!`
56          );
57          await this.rollback(instance);
58        }
59      }
60    }
61  }
62
63  private async removeFromLoadBalancer(instance: Instance) {
64    // Usun instancje z puli load balancera
65  }
66
67  private async drainConnections(instance: Instance) {
68    // Poczekaj az aktywne polaczenia sie zakoncza (graceful)
69  }
70
71  private async deployNewVersion(
72    instance: Instance, version: string
73  ) {
74    // Wdrozenie nowej wersji na instancji
75  }
76
77  private async waitForHealthy(instance: Instance): Promise<boolean> {
78    const maxRetries = 10;
79    for (let i = 0; i < maxRetries; i++) {
80      try {
81        // Sprawdz endpoint /health
82        const healthy = true; // await fetch health check
83        if (healthy) return true;
84      } catch {
85        await new Promise(r => setTimeout(r, 2000));
86      }
87    }
88    return false;
89  }
90
91  private async addToLoadBalancer(instance: Instance) {
92    // Dodaj instancje z powrotem do puli
93  }
94
95  private async rollback(instance: Instance) {
96    // Przywroc poprzednia wersje
97  }
98}

Health Check-based Deployment

Health checki to zwiadowcy Imperium - sprawdzają, czy nowa fortyfikacja jest gotowa do przyjęcia ruchu zanim otworzymy bramy.

1// src/health/deployment-health.controller.ts
2import { Controller, Get } from '@nestjs/common';
3import {
4  HealthCheck, HealthCheckService,
5  MongooseHealthIndicator, MemoryHealthIndicator,
6  DiskHealthIndicator,
7} from '@nestjs/terminus';
8
9@Controller('health')
10export class DeploymentHealthController {
11  constructor(
12    private health: HealthCheckService,
13    private mongoose: MongooseHealthIndicator,
14    private memory: MemoryHealthIndicator,
15    private disk: DiskHealthIndicator,
16  ) {}
17
18  // Liveness - czy aplikacja zyje?
19  @Get('live')
20  @HealthCheck()
21  checkLiveness() {
22    return this.health.check([
23      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
24    ]);
25  }
26
27  // Readiness - czy gotowa na ruch?
28  @Get('ready')
29  @HealthCheck()
30  checkReadiness() {
31    return this.health.check([
32      () => this.mongoose.pingCheck('mongodb'),
33      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
34      () => this.disk.checkStorage('disk', {
35        thresholdPercent: 0.9, path: '/',
36      }),
37    ]);
38  }
39
40  // Startup - czy aplikacja wystartowala poprawnie?
41  @Get('startup')
42  @HealthCheck()
43  checkStartup() {
44    return this.health.check([
45      () => this.mongoose.pingCheck('mongodb'),
46    ]);
47  }
48}

Migracje bazy danych - zero-downtime

Migracje bazy danych to najtrudniejszy element zero-downtime deployment. Stary i nowy kod muszą współistnieć z tą samą bazą danych.

1// src/database/migration-strategy.ts
2
3// ZASADA: Migracje musza byc KOMPATYBILNE WSTECZ
4// Stara wersja kodu musi dzialac z nowa baza danych
5
6// Przyklad: Zmiana nazwy kolumny
7// ZLE (powoduje downtime):
8// Krok 1: ALTER TABLE legions RENAME COLUMN name TO legion_name;
9// --> Stary kod szuka "name" i pada!
10
11// DOBRZE (zero-downtime, 3 kroki):
12interface MigrationStrategy {
13  // Wdrozenie 1: Dodaj nowa kolumne
14  step1_addColumn: string;
15  // Wdrozenie 2: Kod czyta z obu, pisze do obu
16  step2_dualWrite: string;
17  // Wdrozenie 3: Usun stara kolumne (po pelnej migracji)
18  step3_removeOldColumn: string;
19}
20
21const renameMigration: MigrationStrategy = {
22  step1_addColumn: `
23    // Migracja 1: Dodaj kolumne legion_name
24    db.collection('legions').updateMany(
25      {},
26      [{ $set: { legion_name: '$name' } }]
27    );
28    // Stary kod dalej czyta 'name' - dziala!
29  `,
30  step2_dualWrite: `
31    // Kod v2: Zapisuj do obu kolumn
32    async updateLegion(id: string, name: string) {
33      await this.model.updateOne(
34        { _id: id },
35        { $set: { name, legion_name: name } }
36      );
37    }
38    // Czytaj z nowej kolumny
39    async getLegion(id: string) {
40      const doc = await this.model.findById(id);
41      return doc.legion_name || doc.name; // Fallback
42    }
43  `,
44  step3_removeOldColumn: `
45    // Migracja 3: Usun stara kolumne (po pelnym wdrozeniu v2)
46    db.collection('legions').updateMany(
47      {},
48      { $unset: { name: '' } }
49    );
50  `,
51};

Feature Flags

Feature flags to jak tajne rozkazy cesarza - pozwalają włączać i wyłączać funkcjonalności bez wdrażania nowego kodu.

1// src/features/feature-flag.service.ts
2import { Injectable } from '@nestjs/common';
3
4interface FeatureFlag {
5  name: string;
6  enabled: boolean;
7  rolloutPercentage: number; // 0-100
8  allowedUsers: string[];
9}
10
11@Injectable()
12export class FeatureFlagService {
13  private flags: Map<string, FeatureFlag> = new Map();
14
15  isEnabled(flagName: string, userId?: string): boolean {
16    const flag = this.flags.get(flagName);
17    if (!flag) return false;
18
19    // Flaga calkowicie wylaczona
20    if (!flag.enabled) return false;
21
22    // Uzytkownik na liscie dozwolonych (np. testerzy)
23    if (userId && flag.allowedUsers.includes(userId)) {
24      return true;
25    }
26
27    // Stopniowe wdrazanie (canary release)
28    if (flag.rolloutPercentage < 100) {
29      const hash = this.hashUserId(userId || 'anonymous');
30      return (hash % 100) < flag.rolloutPercentage;
31    }
32
33    return true;
34  }
35
36  private hashUserId(userId: string): number {
37    let hash = 0;
38    for (let i = 0; i < userId.length; i++) {
39      hash = ((hash << 5) - hash) + userId.charCodeAt(i);
40      hash |= 0;
41    }
42    return Math.abs(hash);
43  }
44}
45
46// Uzycie w kontrolerze
47@Controller('legions')
48class LegionsController {
49  constructor(private features: FeatureFlagService) {}
50
51  @Get()
52  async getLegions(@Req() req) {
53    if (this.features.isEnabled('new-ranking-system', req.user?.id)) {
54      return this.getNewRanking();
55    }
56    return this.getOldRanking();
57  }
58}

PM2 Cluster Mode

PM2 w trybie cluster to jak rozmieszczenie wielu garnizonów - wykorzystuje wszystkie rdzenie CPU i zapewnia automatyczny restart oraz zero-downtime reload.

1// ecosystem.config.js - konfiguracja PM2
2module.exports = {
3  apps: [{
4    name: 'imperium-api',
5    script: 'dist/main.js',
6    instances: 'max',       // Uzyj wszystkich rdzeni CPU
7    exec_mode: 'cluster',   // Tryb klastrowy
8    autorestart: true,
9    watch: false,
10    max_memory_restart: '500M',
11
12    // Zero-downtime reload
13    wait_ready: true,        // Czekaj na process.send('ready')
14    listen_timeout: 10000,   // Max czas na gotowość
15    kill_timeout: 5000,      // Czas na graceful shutdown
16
17    env_production: {
18      NODE_ENV: 'production',
19      PORT: 3000,
20    },
21  }],
22};
23
24// W main.ts - sygnalizacja gotowosci
25async function bootstrap() {
26  const app = await NestFactory.create(AppModule);
27  await app.listen(3000);
28
29  // Powiadom PM2 ze aplikacja jest gotowa
30  if (process.send) {
31    process.send('ready');
32  }
33}

Komendy PM2 dla zero-downtime:

1# Reload bez downtime (graceful)
2pm2 reload imperium-api
3
4# Status instancji
5pm2 status
6
7# Monitorowanie w czasie rzeczywistym
8pm2 monit

Blue-Green Deployment, rolling updates i feature flags to potężne narzędzia w arsenale każdego architekta Imperium. Dzięki nim Twoje wdrożenia będą tak płynne jak zmiana warty w najlepiej zorganizowanym legionie!

Przejdź do CodeWorlds