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.
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.
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=highDie 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.
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.
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'],
};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
- /dataSicherheits-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'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 }}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}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.jsonMonitoring 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 1Langfristiges 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.