28 CI/CD für NestJS-Anwendungen

Stellen Sie sich vor, Sie entwickeln eine NestJS-Anwendung und jedes Mal, wenn Sie eine Änderung vornehmen, müssen Sie manuell Tests ausführen, die Anwendung bauen, Docker-Images erstellen und diese in verschiedene Umgebungen deployen. Was als einfacher Workflow beginnt, wird schnell zu einem zeitraubenden und fehleranfälligen Prozess. Hier kommt CI/CD ins Spiel - wie ein automatisierter Assistent, der diese repetitiven Aufgaben zuverlässig und konsistent für Sie übernimmt.

CI/CD steht für Continuous Integration und Continuous Deployment (oder Delivery). Denken Sie an CI/CD wie an ein Fließband in einer modernen Fabrik: Jede Änderung an Ihrem Code durchläuft automatisch verschiedene Stationen - von der Qualitätskontrolle über die Verpackung bis hin zur Auslieferung. Für NestJS-Anwendungen bedeutet dies, dass jeder Commit in Ihrem Repository eine Kette von automatisierten Prozessen auslöst, die sicherstellen, dass Ihr Code funktioniert, sicher ist und erfolgreich in die Produktion gelangt.

28.1 CI/CD-Strategie für NestJS

Eine durchdachte CI/CD-Strategie für NestJS-Anwendungen berücksichtigt die spezifischen Charakteristika des Frameworks. NestJS bringt TypeScript-Compilation, umfangreiche Dependency Injection und modulare Architektur mit sich - all diese Aspekte beeinflussen, wie Sie Ihre Pipeline gestalten sollten.

Die Grundphilosophie einer effektiven CI/CD-Strategie lässt sich in drei Kernprinzipien zusammenfassen: Geschwindigkeit, Zuverlässigkeit und Sicherheit. Geschwindigkeit bedeutet, dass Entwickler schnelles Feedback erhalten, wenn sie Code ändern. Zuverlässigkeit stellt sicher, dass nur funktionierender Code in die Produktion gelangt. Sicherheit garantiert, dass keine Schwachstellen oder sensiblen Daten durch den Deployment-Prozess eingeschleust werden.

Für NestJS-Anwendungen beginnt eine solche Strategie mit der Erkenntnis, dass TypeScript sowohl Vorteile als auch spezielle Anforderungen mit sich bringt. Der Compilation-Schritt muss in die Pipeline integriert werden, und Type-Checking sollte als separater, schneller Schritt erfolgen, um frühes Feedback zu ermöglichen. Die modulare Natur von NestJS ermöglicht es außerdem, Tests gezielt für geänderte Module auszuführen, was die Pipeline-Geschwindigkeit erheblich verbessern kann.

Ein wichtiger strategischer Aspekt ist die Entscheidung zwischen verschiedenen Deployment-Modellen. Monolithische NestJS-Anwendungen haben andere Anforderungen als Microservice-Architekturen. Bei Monolithen können Sie den gesamten Build- und Test-Prozess in einer Pipeline abwickeln. Bei Microservices hingegen benötigen Sie orchestrierte Pipelines, die Abhängigkeiten zwischen Services berücksichtigen und möglicherweise verschiedene Versionen parallel deployen können.

28.2 Pipeline-Design und Best Practices

Das Design einer CI/CD-Pipeline ist wie die Architektur eines Gebäudes - die Grundstruktur muss solide sein, bevor Sie Details hinzufügen können. Eine gut durchdachte Pipeline für NestJS-Anwendungen folgt einem bewährten Muster, das sich in mehrere Stufen gliedert.

Die erste Stufe ist die Code-Qualitätsprüfung. Hier werden Linting, Type-Checking und Code-Formatierung ausgeführt. Diese Schritte sind bewusst schnell gehalten, damit Entwickler innerhalb weniger Minuten Feedback erhalten. Ein typischer Qualitätsprüfungs-Job könnte folgendermaßen aussehen:

# .github/workflows/quality-check.yml
name: Code Quality Check

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main, develop]

jobs:
  quality-check:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install Dependencies
      run: npm ci
      
    # Schnelle Type-Checks zuerst für frühes Feedback
    - name: TypeScript Check
      run: npm run type-check
      
    - name: Lint Code
      run: npm run lint
      
    - name: Check Code Formatting
      run: npm run format:check
      
    # Dependency Vulnerability Scan
    - name: Audit Dependencies
      run: npm audit --audit-level=high

Die zweite Stufe umfasst das eigentliche Testen. Hier wird zwischen verschiedenen Testarten unterschieden, die parallel ausgeführt werden können, um Zeit zu sparen. Unit-Tests laufen schnell und geben sofortiges Feedback über die Funktionalität einzelner Module. Integration-Tests prüfen das Zusammenspiel zwischen verschiedenen Komponenten. End-to-End-Tests stellen sicher, dass die gesamte Anwendung wie erwartet funktioniert.

  test:
    runs-on: ubuntu-latest
    needs: quality-check
    
    strategy:
      matrix:
        test-type: [unit, integration, e2e]
        
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install Dependencies
      run: npm ci
      
    # Test-spezifische Services starten (nur für Integration/E2E)
    - name: Start Test Database
      if: matrix.test-type != 'unit'
      run: docker-compose -f docker-compose.test.yml up -d postgres redis
      
    - name: Run Tests
      run: npm run test:${{ matrix.test-type }}
      env:
        DATABASE_URL: postgresql://test:test@localhost:5432/testdb
        REDIS_URL: redis://localhost:6379
        
    - name: Upload Coverage Reports
      if: matrix.test-type == 'unit'
      uses: codecov/codecov-action@v3
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

Die dritte Stufe ist der Build-Prozess. Hier wird die TypeScript-Anwendung kompiliert und in ein Docker-Image verpackt. Der Build-Prozess sollte reproduzierbar sein, was bedeutet, dass derselbe Quellcode immer dasselbe Ergebnis produziert. Dies erreichen Sie durch das Pinning von Dependency-Versionen und die Verwendung deterministischer Build-Umgebungen.

  build:
    runs-on: ubuntu-latest
    needs: test
    
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}
      
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    - name: Login to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Extract Metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ghcr.io/${{ github.repository }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}
          
    - name: Build and Push Docker Image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        # Build-Args für reproduzierbare Builds
        build-args: |
          BUILD_VERSION=${{ github.sha }}
          BUILD_DATE=${{ steps.meta.outputs.date }}

Ein wichtiger Aspekt des Pipeline-Designs ist die Parallelisierung. Verschiedene Schritte, die nicht voneinander abhängen, sollten parallel ausgeführt werden. Beispielsweise können Sicherheits-Scans parallel zu Tests laufen, da beide unabhängig vom jeweils anderen sind.

28.3 Automatisierte Tests in CI/CD

Das Testen in CI/CD-Pipelines erfordert eine andere Herangehensweise als lokales Testen. Tests müssen zuverlässig, schnell und deterministisch sein. Ein Test, der manchmal fehlschlägt und manchmal erfolgreich ist (ein sogenannter “flaky test”), kann eine gesamte Pipeline blockieren und das Vertrauen der Entwickler in den Automatisierungsprozess untergraben.

28.3.1 Unit-Test-Integration

Unit-Tests bilden das Fundament Ihrer Test-Pyramide. Sie sollten schnell ausführbar sein und keine externen Abhängigkeiten haben. In NestJS bedeutet dies, dass Sie Services und Controller isoliert testen, indem Sie alle Dependencies mocken.

// user.service.spec.ts - Beispiel für isolierte Unit-Tests
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { getRepositoryToken } from '@nestjs/typeorm';

describe('UserService', () => {
  let service: UserService;
  let repository: Repository<User>;

  // Mock Repository für isolierte Tests
  const mockRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    repository = module.get<Repository<User>>(getRepositoryToken(User));
  });

  afterEach(() => {
    // Mocks nach jedem Test zurücksetzen für saubere Isolation
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('should return an array of users', async () => {
      const expectedUsers = [
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
      ];

      // Mock-Verhalten definieren
      mockRepository.find.mockResolvedValue(expectedUsers);

      // Service-Methode testen
      const result = await service.findAll();

      // Erwartungen prüfen
      expect(result).toEqual(expectedUsers);
      expect(mockRepository.find).toHaveBeenCalledTimes(1);
    });
  });

  describe('create', () => {
    it('should create and save a new user', async () => {
      const createUserDto = { name: 'New User', email: 'new@example.com' };
      const savedUser = { id: 3, ...createUserDto };

      mockRepository.create.mockReturnValue(savedUser);
      mockRepository.save.mockResolvedValue(savedUser);

      const result = await service.create(createUserDto);

      expect(result).toEqual(savedUser);
      expect(mockRepository.create).toHaveBeenCalledWith(createUserDto);
      expect(mockRepository.save).toHaveBeenCalledWith(savedUser);
    });
  });
});

Für die CI/CD-Integration sollten Unit-Tests mit Coverage-Reporting konfiguriert werden:

// package.json - Test-Scripts für CI/CD
{
  "scripts": {
    "test:unit": "jest --config=jest.unit.config.js",
    "test:unit:watch": "jest --config=jest.unit.config.js --watch",
    "test:unit:coverage": "jest --config=jest.unit.config.js --coverage",
    "test:unit:ci": "jest --config=jest.unit.config.js --coverage --ci --watchAll=false --passWithNoTests"
  }
}
// jest.unit.config.js - Konfiguration für Unit-Tests
module.exports = {
  displayName: 'Unit Tests',
  testMatch: ['**/*.spec.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.spec.ts',
    '!src/**/*.e2e-spec.ts',
    '!src/main.ts',
    '!src/**/*.module.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  coverageReporters: ['text', 'lcov', 'html'],
  setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};

28.3.2 E2E-Test-Automation

End-to-End-Tests sind wie ein vollständiger Probelauf Ihrer Anwendung. Sie starten die komplette NestJS-Anwendung, verbinden sich mit echten (Test-)Datenbanken und simulieren echte Benutzerinteraktionen. Diese Tests sind wertvoller für das Vertrauen in Ihre Anwendung, dauern aber länger und sind komplexer zu warten.

// app.e2e-spec.ts - Beispiel für umfassende E2E-Tests
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { DataSource } from 'typeorm';

describe('UserController (e2e)', () => {
  let app: INestApplication;
  let dataSource: DataSource;

  beforeAll(async () => {
    // Komplette Anwendung für E2E-Tests starten
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    
    // Globale Pipes und Middleware wie in der echten Anwendung konfigurieren
    app.useGlobalPipes(new ValidationPipe({ transform: true }));
    
    await app.init();

    // Datenbankverbindung für Cleanup zwischen Tests
    dataSource = app.get(DataSource);
  });

  afterAll(async () => {
    // Aufräumen nach allen Tests
    await dataSource.destroy();
    await app.close();
  });

  beforeEach(async () => {
    // Datenbank zwischen Tests zurücksetzen für saubere Isolation
    await dataSource.synchronize(true);
    
    // Basis-Testdaten einfügen
    await dataSource.query(`
      INSERT INTO users (name, email) VALUES 
      ('Test User', 'test@example.com'),
      ('Admin User', 'admin@example.com')
    `);
  });

  describe('/users (GET)', () => {
    it('should return all users', () => {
      return request(app.getHttpServer())
        .get('/users')
        .expect(200)
        .expect((res) => {
          expect(res.body).toHaveLength(2);
          expect(res.body[0]).toHaveProperty('name', 'Test User');
          expect(res.body[1]).toHaveProperty('name', 'Admin User');
        });
    });

    it('should support pagination', () => {
      return request(app.getHttpServer())
        .get('/users?page=1&limit=1')
        .expect(200)
        .expect((res) => {
          expect(res.body.data).toHaveLength(1);
          expect(res.body.pagination).toMatchObject({
            page: 1,
            limit: 1,
            total: 2,
          });
        });
    });
  });

  describe('/users (POST)', () => {
    it('should create a new user', () => {
      const newUser = {
        name: 'New User',
        email: 'new@example.com',
      };

      return request(app.getHttpServer())
        .post('/users')
        .send(newUser)
        .expect(201)
        .expect((res) => {
          expect(res.body).toMatchObject(newUser);
          expect(res.body).toHaveProperty('id');
        });
    });

    it('should reject invalid email format', () => {
      const invalidUser = {
        name: 'Invalid User',
        email: 'not-an-email',
      };

      return request(app.getHttpServer())
        .post('/users')
        .send(invalidUser)
        .expect(400)
        .expect((res) => {
          expect(res.body.message).toContain('email must be an email');
        });
    });
  });

  describe('Authentication flows', () => {
    it('should complete full login/logout cycle', async () => {
      // Benutzer registrieren
      const userData = {
        name: 'Auth User',
        email: 'auth@example.com',
        password: 'securePassword123',
      };

      await request(app.getHttpServer())
        .post('/auth/register')
        .send(userData)
        .expect(201);

      // Anmelden und Token erhalten
      const loginResponse = await request(app.getHttpServer())
        .post('/auth/login')
        .send({
          email: userData.email,
          password: userData.password,
        })
        .expect(200);

      const token = loginResponse.body.accessToken;
      expect(token).toBeDefined();

      // Geschützten Endpunkt mit Token aufrufen
      await request(app.getHttpServer())
        .get('/users/profile')
        .set('Authorization', `Bearer ${token}`)
        .expect(200)
        .expect((res) => {
          expect(res.body.email).toBe(userData.email);
        });

      // Token invalidieren (Logout)
      await request(app.getHttpServer())
        .post('/auth/logout')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      // Verifizieren, dass Token nicht mehr funktioniert
      await request(app.getHttpServer())
        .get('/users/profile')
        .set('Authorization', `Bearer ${token}`)
        .expect(401);
    });
  });
});

Für die CI/CD-Integration benötigen E2E-Tests eine Test-Datenbank und möglicherweise andere Services:

# docker-compose.test.yml - Test-Services für E2E-Tests
version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5432:5432"
    tmpfs:
      # In-Memory-Dateisystem für schnellere Tests
      - /var/lib/postgresql/data
      
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    tmpfs:
      # In-Memory-Dateisystem für schnellere Tests  
      - /data

28.3.3 Security Scanning

Sicherheits-Scans sind ein kritischer Bestandteil moderner CI/CD-Pipelines. Sie identifizieren bekannte Schwachstellen in Dependencies, analysieren Code auf potentielle Sicherheitsprobleme und prüfen Docker-Images auf Vulnerabilities.

  security-scan:
    runs-on: ubuntu-latest
    needs: quality-check
    
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    # Dependency Vulnerability Scanning
    - name: Install Dependencies
      run: npm ci
      
    - name: Audit Dependencies with npm
      run: npm audit --audit-level=moderate
      
    - name: Run Snyk Security Test
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      with:
        args: --severity-threshold=high
        
    # Static Code Analysis für Sicherheitsprobleme
    - name: Run CodeQL Analysis
      uses: github/codeql-action/init@v3
      with:
        languages: javascript
        
    - name: Autobuild
      uses: github/codeql-action/autobuild@v3
      
    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      
    # Docker Image Security Scanning
    - name: Build Docker Image for Scanning
      run: docker build -t security-scan:latest .
      
    - name: Run Trivy Security Scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'security-scan:latest'
        format: 'sarif'
        output: 'trivy-results.sarif'
        
    - name: Upload Trivy Results to GitHub Security Tab
      uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: 'trivy-results.sarif'

28.4 Database Migrations in CI/CD

Datenbank-Migrationen in CI/CD-Pipelines erfordern besondere Aufmerksamkeit, da sie irreversible Änderungen an produktiven Daten vornehmen können. Der Schlüssel liegt in einer durchdachten Strategie, die sowohl Sicherheit als auch Verfügbarkeit gewährleistet.

Eine bewährte Herangehensweise ist die Trennung von Schema-Migrationen und Daten-Migrationen. Schema-Migrationen ändern die Struktur der Datenbank (neue Tabellen, Spalten, Indizes), während Daten-Migrationen bestehende Daten transformieren oder migrieren.

// migration/1699123456789-AddUserPreferences.ts
import { MigrationInterface, QueryRunner, Table, Column } from 'typeorm';

export class AddUserPreferences1699123456789 implements MigrationInterface {
  name = 'AddUserPreferences1699123456789';

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Neue Tabelle für Benutzereinstellungen hinzufügen
    await queryRunner.createTable(
      new Table({
        name: 'user_preferences',
        columns: [
          {
            name: 'id',
            type: 'uuid',
            isPrimary: true,
            generationStrategy: 'uuid',
            default: 'uuid_generate_v4()',
          },
          {
            name: 'user_id',
            type: 'uuid',
            isNullable: false,
          },
          {
            name: 'theme',
            type: 'varchar',
            length: '20',
            default: "'light'",
          },
          {
            name: 'language',
            type: 'varchar',
            length: '5',
            default: "'en'",
          },
          {
            name: 'notifications_enabled',
            type: 'boolean',
            default: true,
          },
          {
            name: 'created_at',
            type: 'timestamp',
            default: 'CURRENT_TIMESTAMP',
          },
          {
            name: 'updated_at',
            type: 'timestamp',
            default: 'CURRENT_TIMESTAMP',
          },
        ],
        foreignKeys: [
          {
            columnNames: ['user_id'],
            referencedTableName: 'users',
            referencedColumnNames: ['id'],
            onDelete: 'CASCADE',
          },
        ],
        indices: [
          {
            name: 'IDX_USER_PREFERENCES_USER_ID',
            columnNames: ['user_id'],
            isUnique: true,
          },
        ],
      }),
      true, // ifNotExists
    );

    // Standardeinstellungen für bestehende Benutzer erstellen
    await queryRunner.query(`
      INSERT INTO user_preferences (user_id, theme, language, notifications_enabled)
      SELECT id, 'light', 'en', true 
      FROM users 
      WHERE id NOT IN (SELECT user_id FROM user_preferences)
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    // Rückgängigmachen der Migration
    await queryRunner.dropTable('user_preferences');
  }
}

In der CI/CD-Pipeline sollten Migrationen in einem separaten, kontrollierten Schritt ausgeführt werden:

  database-migration:
    runs-on: ubuntu-latest
    needs: [test, security-scan]
    if: github.ref == 'refs/heads/main'
    
    environment: 
      name: staging
      
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install Dependencies
      run: npm ci
      
    # Datenbank-Backup vor Migration erstellen
    - name: Create Database Backup
      run: |
        npm run db:backup -- --environment=staging
      env:
        DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
        
    # Migration-Pläne validieren (Dry-Run)
    - name: Validate Migration Plan
      run: |
        npm run migration:show
        npm run migration:dry-run
      env:
        DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
        
    # Migrationen ausführen
    - name: Run Database Migrations
      run: npm run migration:run
      env:
        DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
        
    # Datenbankintegrität prüfen
    - name: Verify Database Integrity
      run: npm run db:verify
      env:
        DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}

28.5 Environment Management

Effektives Environment Management ist entscheidend für zuverlässige CI/CD-Pipelines. Verschiedene Umgebungen (Development, Staging, Production) haben unterschiedliche Konfigurationen, aber derselbe Code sollte in allen Umgebungen laufen können.

Ein bewährter Ansatz ist die Verwendung von Environment-spezifischen Konfigurationsdateien kombiniert mit Umgebungsvariablen für sensible Daten:

// config/configuration.ts - Environment-basierte Konfiguration
export default () => ({
  environment: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,
  
  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USERNAME || 'postgres',
    password: process.env.DATABASE_PASSWORD,
    database: process.env.DATABASE_NAME || 'nestjs_app',
    ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
    synchronize: process.env.NODE_ENV === 'development',
    logging: process.env.NODE_ENV === 'development',
  },
  
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
    password: process.env.REDIS_PASSWORD,
    db: parseInt(process.env.REDIS_DB, 10) || 0,
  },
  
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '24h',
  },
  
  email: {
    host: process.env.EMAIL_HOST,
    port: parseInt(process.env.EMAIL_PORT, 10) || 587,
    username: process.env.EMAIL_USERNAME,
    password: process.env.EMAIL_PASSWORD,
    from: process.env.EMAIL_FROM || 'noreply@example.com',
  },
  
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
  },
});

In der CI/CD-Pipeline sollten Environment-spezifische Secrets sicher verwaltet werden:

  deploy-staging:
    runs-on: ubuntu-latest
    needs: [build, database-migration]
    environment: staging
    
    steps:
    - name: Deploy to Staging
      run: |
        kubectl set image deployment/nestjs-app \
          nestjs-app=${{ needs.build.outputs.image-tag }}
      env:
        KUBECONFIG_DATA: ${{ secrets.STAGING_KUBECONFIG }}
        
    - name: Update Configuration
      run: |
        kubectl create configmap nestjs-config \
          --from-literal=NODE_ENV=staging \
          --from-literal=LOG_LEVEL=debug \
          --from-literal=API_VERSION=v1 \
          --dry-run=client -o yaml | kubectl apply -f -
          
        kubectl create secret generic nestjs-secrets \
          --from-literal=DATABASE_PASSWORD="${{ secrets.STAGING_DB_PASSWORD }}" \
          --from-literal=JWT_SECRET="${{ secrets.STAGING_JWT_SECRET }}" \
          --from-literal=REDIS_PASSWORD="${{ secrets.STAGING_REDIS_PASSWORD }}" \
          --dry-run=client -o yaml | kubectl apply -f -
          
    - name: Wait for Deployment
      run: kubectl rollout status deployment/nestjs-app --timeout=300s
      
    - name: Run Smoke Tests
      run: |
        STAGING_URL="https://staging-api.example.com"
        curl -f ${STAGING_URL}/health || exit 1
        npm run test:smoke -- --baseUrl=${STAGING_URL}

28.6 Container Registry Integration

Container Registries sind wie Bibliotheken für Docker-Images. Sie speichern verschiedene Versionen Ihrer containerisierten Anwendung und ermöglichen es verschiedenen Umgebungen, spezifische Versionen abzurufen. Die Integration mit Container Registries sollte sicher, effizient und gut organisiert sein.

  publish-container:
    runs-on: ubuntu-latest
    needs: [test, security-scan]
    
    outputs:
      image-url: ${{ steps.image.outputs.url }}
      image-digest: ${{ steps.build.outputs.digest }}
      
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    # Multi-Registry Support für Redundanz
    - name: Login to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        registry: docker.io
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
        
    - name: Extract Metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: |
          ghcr.io/${{ github.repository }}
          docker.io/${{ github.repository }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha,prefix={{branch}}-,format=short
          type=raw,value=latest,enable={{is_default_branch}}
          
    - name: Build and Push Multi-Platform Image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        
        # Build-Argumente für Traceability
        build-args: |
          BUILD_VERSION=${{ github.sha }}
          BUILD_DATE=${{ steps.meta.outputs.date }}
          BUILD_BRANCH=${{ github.ref_name }}
          
    # Image-Signierung für Supply Chain Security
    - name: Install Cosign
      uses: sigstore/cosign-installer@v3
      
    - name: Sign Container Image
      run: |
        cosign sign --yes ${{ steps.build.outputs.digest }}
      env:
        COSIGN_EXPERIMENTAL: 1
        
    # Vulnerability Scanning des finalen Images
    - name: Scan Final Image
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ steps.build.outputs.digest }}
        format: 'json'
        output: 'vulnerability-report.json'
        
    - name: Upload Vulnerability Report
      uses: actions/upload-artifact@v3
      with:
        name: vulnerability-report
        path: vulnerability-report.json

28.7 Monitoring und Rollback-Strategien

Monitoring und Rollback-Fähigkeiten sind wie Sicherheitsgurte für Ihre Deployments. Sie erkennen Probleme frühzeitig und ermöglichen es, schnell zu einer funktionierenden Version zurückzukehren, falls etwas schief geht.

Effektives Deployment-Monitoring beginnt bereits während des Deployment-Prozesses:

  deploy-production:
    runs-on: ubuntu-latest
    needs: [deploy-staging, integration-tests]
    environment: production
    
    steps:
    - name: Deploy with Blue-Green Strategy
      run: |
        # Neue Version in "Green" Environment deployen
        kubectl set image deployment/nestjs-app-green \
          nestjs-app=${{ needs.build.outputs.image-tag }}
          
        # Warten bis neue Version bereit ist
        kubectl rollout status deployment/nestjs-app-green --timeout=300s
        
    - name: Health Check New Deployment
      run: |
        # Erweiterte Gesundheitsprüfungen
        GREEN_URL="https://green-api.example.com"
        
        # Basis-Gesundheitsprüfung
        curl -f ${GREEN_URL}/health || exit 1
        
        # Abhängigkeiten prüfen
        curl -f ${GREEN_URL}/health/database || exit 1
        curl -f ${GREEN_URL}/health/redis || exit 1
        
        # Performance-Baseline etablieren
        npm run test:performance -- --baseUrl=${GREEN_URL}
        
    - name: Run Canary Tests
      run: |
        # Schrittweise Traffic-Weiterleitung an neue Version
        kubectl patch service nestjs-service -p '{"spec":{"selector":{"version":"green"}}}'
        
        # Überwachung der Key-Metriken für 5 Minuten
        npm run monitor:canary -- --duration=300 --threshold-error-rate=1%
        
    - name: Complete Blue-Green Switch
      if: success()
      run: |
        # Vollständige Umschaltung auf neue Version
        kubectl patch service nestjs-service -p '{"spec":{"selector":{"version":"green"}}}'
        
        # Alte Version als Rollback-Option behalten (für 1 Stunde)
        kubectl scale deployment/nestjs-app-blue --replicas=1
        
    - name: Rollback on Failure
      if: failure()
      run: |
        echo "Deployment failed, initiating rollback..."
        
        # Sofortige Rückkehr zur stabilen Version
        kubectl patch service nestjs-service -p '{"spec":{"selector":{"version":"blue"}}}'
        
        # Fehlgeschlagene Deployment-Artefakte aufräumen
        kubectl scale deployment/nestjs-app-green --replicas=0
        
        # Incident-Benachrichtigung senden
        curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
          -H 'Content-type: application/json' \
          --data '{"text":"🚨 Production deployment failed and rolled back to previous version"}'
        
        exit 1

Langfristiges Monitoring sollte automatisierte Alerts und Dashboards umfassen:

// monitoring.service.ts - Custom Metrics für Deployment-Monitoring
import { Injectable } from '@nestjs/common';
import { Counter, Histogram, Gauge, register } from 'prom-client';

@Injectable()
export class MonitoringService {
  private deploymentCounter = new Counter({
    name: 'deployment_total',
    help: 'Total number of deployments',
    labelNames: ['version', 'environment', 'status'],
  });

  private responseTimeHistogram = new Histogram({
    name: 'http_request_duration_seconds',
    help: 'HTTP request duration in seconds',
    labelNames: ['method', 'route', 'status'],
    buckets: [0.1, 0.5, 1, 2, 5, 10],
  });

  private activeConnectionsGauge = new Gauge({
    name: 'active_connections',
    help: 'Number of active connections',
  });

  private healthCheckGauge = new Gauge({
    name: 'health_check_status',
    help: 'Health check status (1 = healthy, 0 = unhealthy)',
    labelNames: ['check_name'],
  });

  recordDeployment(version: string, environment: string, status: 'success' | 'failure') {
    this.deploymentCounter.inc({ version, environment, status });
  }

  recordResponseTime(method: string, route: string, status: number, duration: number) {
    this.responseTimeHistogram.observe({ method, route, status: status.toString() }, duration);
  }

  updateActiveConnections(count: number) {
    this.activeConnectionsGauge.set(count);
  }

  updateHealthCheck(checkName: string, isHealthy: boolean) {
    this.healthCheckGauge.set({ check_name: checkName }, isHealthy ? 1 : 0);
  }

  getMetrics() {
    return register.metrics();
  }
}

Ein automatisierter Rollback-Mechanismus kann auf Basis von Metriken entscheiden:

# .github/workflows/auto-rollback.yml
name: Automated Rollback Monitor

on:
  schedule:
    # Überprüfung alle 5 Minuten nach einem Deployment
    - cron: '*/5 * * * *'
  workflow_dispatch:

jobs:
  monitor-and-rollback:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Check Production Health
      id: health-check
      run: |
        # Sammle Metriken von verschiedenen Quellen
        ERROR_RATE=$(curl -s https://api.example.com/metrics | grep error_rate | awk '{print $2}')
        RESPONSE_TIME=$(curl -s https://api.example.com/metrics | grep avg_response_time | awk '{print $2}')
        ACTIVE_CONNECTIONS=$(curl -s https://api.example.com/metrics | grep active_connections | awk '{print $2}')
        
        # Definiere Schwellenwerte
        MAX_ERROR_RATE=5.0
        MAX_RESPONSE_TIME=2000
        MIN_CONNECTIONS=1
        
        # Überprüfe kritische Metriken
        if (( $(echo "$ERROR_RATE > $MAX_ERROR_RATE" | bc -l) )); then
          echo "High error rate detected: $ERROR_RATE%"
          echo "rollback=true" >> $GITHUB_OUTPUT
        elif (( $(echo "$RESPONSE_TIME > $MAX_RESPONSE_TIME" | bc -l) )); then
          echo "High response time detected: ${RESPONSE_TIME}ms"
          echo "rollback=true" >> $GITHUB_OUTPUT
        elif (( $(echo "$ACTIVE_CONNECTIONS < $MIN_CONNECTIONS" | bc -l) )); then
          echo "Low connection count detected: $ACTIVE_CONNECTIONS"
          echo "rollback=true" >> $GITHUB_OUTPUT
        else
          echo "All metrics within acceptable ranges"
          echo "rollback=false" >> $GITHUB_OUTPUT
        fi
        
    - name: Execute Emergency Rollback
      if: steps.health-check.outputs.rollback == 'true'
      run: |
        echo "🚨 Executing emergency rollback due to degraded performance"
        
        # Rollback zur vorherigen stabilen Version
        kubectl rollout undo deployment/nestjs-app
        
        # Warten auf Rollback-Completion
        kubectl rollout status deployment/nestjs-app --timeout=300s
        
        # Benachrichtigungen senden
        curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
          -H 'Content-type: application/json' \
          --data '{"text":"🔄 Emergency rollback executed due to performance degradation"}'
          
        # Incident-Tracking erstellen
        curl -X POST https://api.pagerduty.com/incidents \
          -H "Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}" \
          -H "Content-Type: application/json" \
          -d '{
            "incident": {
              "type": "incident",
              "title": "Automated Rollback Triggered",
              "service": {
                "id": "${{ secrets.PAGERDUTY_SERVICE_ID }}",
                "type": "service_reference"
              },
              "urgency": "high"
            }
          }'

Diese umfassende CI/CD-Strategie für NestJS-Anwendungen stellt sicher, dass jede Code-Änderung durch eine rigorose Pipeline von Qualitätsprüfungen, Tests und Sicherheitskontrollen läuft, bevor sie Benutzer erreicht. Gleichzeitig bietet sie die Flexibilität und Zuverlässigkeit, die moderne Entwicklungsteams benötigen, um schnell und sicher zu iterieren. Denken Sie daran: Eine gute CI/CD-Pipeline ist wie ein zuverlässiger Teamkollege - sie arbeitet rund um die Uhr daran, Ihre Anwendung sicher und aktuell zu halten.