37 Containerisierung einer Angular-Anwendung

37.1 Einführung in die Containerisierung

Die Containerisierung hat sich als Standard für die Bereitstellung moderner Webanwendungen etabliert. Container bieten eine konsistente Umgebung für die Ausführung von Anwendungen, unabhängig von der zugrunde liegenden Infrastruktur. Für Angular-Anwendungen bietet die Containerisierung mehrere Vorteile:

In diesem Kapitel werden wir eine Angular-Anwendung in einen Docker-Container verpacken und anschließend ein Helm-Chart erstellen, um die Bereitstellung in Kubernetes-Umgebungen zu vereinfachen.

37.2 Containerisierung mit Docker

37.2.1 Vorbereitung der Angular-Anwendung

Bevor wir mit der Containerisierung beginnen, stellen wir sicher, dass unsere Angular-Anwendung produktionsreif ist. Moderne Angular-Versionen bringen verbesserte Build-Optimierungen mit, die wir nutzen werden.

# Sicherstellen, dass Angular CLI in der aktuellen Version installiert ist
npm install -g @angular/cli

# Produktion-Build der Anwendung erstellen
ng build --configuration production

Der Produktions-Build erzeugt eine optimierte Version der Anwendung im dist/-Verzeichnis.

37.2.2 Multi-Stage Dockerfile erstellen

Ein effizientes Docker-Image für Angular-Anwendungen verwendet typischerweise den Multi-Stage-Build-Ansatz. Damit trennen wir den Build-Prozess von der Laufzeitumgebung und erhalten ein schlankeres finales Image.

Erstellen Sie eine Datei namens Dockerfile im Wurzelverzeichnis Ihres Projekts:

# Stage 1: Build der Angular-Anwendung
FROM node:20-alpine AS build

WORKDIR /app

# Abhängigkeiten kopieren und installieren
COPY package.json package-lock.json ./
RUN npm ci

# Quellcode kopieren
COPY . .

# Build der Anwendung
RUN npm run build -- --configuration production

# Stage 2: Bereitstellung mit NGINX
FROM nginx:1.25-alpine

# NGINX-Konfiguration anpassen
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Build-Artefakte in NGINX-Verzeichnis kopieren
COPY --from=build /app/dist/[app-name]/browser /usr/share/nginx/html

# Container-Metadaten
LABEL maintainer="Ihr Name <email@example.com>"
LABEL version="1.0.0"
LABEL description="Angular-Anwendung"

# Health-Check für den Container
HEALTHCHECK --interval=30s --timeout=3s CMD wget -q --spider http://localhost/ || exit 1

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Ersetzen Sie [app-name] mit dem Namen Ihres Projekts. Dieser Pfad sollte dem Ausgabeverzeichnis Ihres Angular-Builds entsprechen.

37.2.3 NGINX-Konfiguration

Für eine Single-Page-Anwendung (SPA) wie Angular benötigen wir eine angepasste NGINX-Konfiguration. Erstellen Sie eine nginx.conf-Datei im Wurzelverzeichnis:

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Kompression aktivieren
    gzip on;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Cache-Kontrolle für statische Assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000";
    }

    # Hauptregel für das Angular-Router-Handling
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Fehlerseiten
    error_page 404 /index.html;
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

Diese Konfiguration leitet alle Anfragen an index.html weiter, was für das Client-seitige Routing von Angular notwendig ist.

37.2.4 Docker-Image bauen und testen

Jetzt können wir das Docker-Image bauen und lokal testen:

# Image bauen
docker build -t angular-app:1.0.0 .

# Container starten
docker run -d -p 8080:80 --name angular-app angular-app:1.0.0

# Überprüfen, ob der Container läuft
docker ps

Die Anwendung sollte nun unter http://localhost:8080 verfügbar sein.

37.2.5 Sicherheitsüberlegungen

Bei der Containerisierung einer Angular-Anwendung sollten einige Sicherheitsaspekte beachtet werden:

Um einen nicht-Root-Benutzer zu verwenden, können Sie das Dockerfile wie folgt anpassen:

# Am Ende des Stage 2 (NGINX)
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chmod -R 755 /usr/share/nginx/html && \
    chown -R nginx:nginx /var/cache/nginx && \
    chown -R nginx:nginx /var/log/nginx && \
    chown -R nginx:nginx /etc/nginx/conf.d && \
    touch /var/run/nginx.pid && \
    chown -R nginx:nginx /var/run/nginx.pid

USER nginx

37.3 Kubernetes-Deployment mit Helm

37.3.1 Einführung in Helm

Helm ist ein Paketmanager für Kubernetes, der die Verwaltung von Kubernetes-Anwendungen vereinfacht. Mit Helm können wir unsere Angular-Anwendung als ein einziges Paket definieren, das alle benötigten Kubernetes-Ressourcen enthält.

37.3.2 Helm-Chart-Struktur erstellen

Erstellen Sie eine neue Helm-Chart-Struktur mit dem Helm-CLI:

# Helm-Chart erstellen
helm create angular-app

Dies erzeugt eine Basisstruktur für unser Helm-Chart:

37.3.3 Chart.yaml anpassen

Passen Sie die Chart.yaml-Datei an:

apiVersion: v2
name: angular-app
description: Helm-Chart für eine Angular-Anwendung
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
  - name: Ihr Name
    email: email@example.com

37.3.4 values.yaml konfigurieren

Die values.yaml-Datei enthält die Standardkonfigurationswerte für das Chart:

# Default-Werte für die Angular-Anwendung
replicaCount: 2

image:
  repository: angular-app
  tag: 1.0.0
  pullPolicy: IfNotPresent

nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: false
  name: ""

podSecurityContext: {}
securityContext: {}

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  className: "nginx"
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: angular-app.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: angular-app-tls
      hosts:
        - angular-app.example.com

resources:
  limits:
    cpu: 200m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

nodeSelector: {}
tolerations: []
affinity: {}

# Application-spezifische Konfiguration
config:
  apiUrl: "https://api.example.com"
  logLevel: "error"

37.3.5 Deployment-Template anpassen

Passen Sie das Deployment-Template (templates/deployment.yaml) an:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "angular-app.fullname" . }}
  labels:
    {{- include "angular-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "angular-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "angular-app.selectorLabels" . | nindent 8 }}
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - name: config
              mountPath: /usr/share/nginx/html/assets/config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: {{ include "angular-app.fullname" . }}-config

37.3.6 ConfigMap für runtime-Konfiguration

Damit wir die Angular-Anwendung ohne Neubuilds konfigurieren können, erstellen wir eine ConfigMap-Template (templates/configmap.yaml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "angular-app.fullname" . }}-config
  labels:
    {{- include "angular-app.labels" . | nindent 4 }}
data:
  config.json: |
    {
      "apiUrl": "{{ .Values.config.apiUrl }}",
      "logLevel": "{{ .Values.config.logLevel }}"
    }

37.3.7 Service und Ingress

Die Service- und Ingress-Definitionen (templates/service.yaml und templates/ingress.yaml) sollten bereits durch Helm erstellt worden sein. Überprüfen Sie diese und passen Sie sie bei Bedarf an.

37.3.8 Helm-Chart testen und bereitstellen

Vor der Produktionsbereitstellung sollten wir das Chart validieren:

# Helm-Chart-Syntax validieren
helm lint ./angular-app

# Template-Rendering testen
helm template ./angular-app

# Trockenlauf der Installation
helm install --dry-run --debug angular-release ./angular-app

Wenn alles in Ordnung ist, können wir das Chart in unserem Kubernetes-Cluster installieren:

# Helm-Chart installieren
helm install angular-release ./angular-app

# Status überprüfen
helm status angular-release

37.4 Konfiguration der Angular-Anwendung für Containerumgebungen

37.4.1 Runtime-Konfiguration

Moderne Angular-Versionen bieten verbesserte Möglichkeiten zur Runtime-Konfiguration. Wir können eine Konfigurationsdatei erstellen, die zur Laufzeit geladen wird, anstatt die Konfiguration in den Build einzubetten.

Erstellen Sie zuerst einen Konfigurationsservice (src/app/services/config.service.ts):

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, tap, throwError } from 'rxjs';

export interface AppConfig {
  apiUrl: string;
  logLevel: string;
}

@Injectable({
  providedIn: 'root'
})
export class ConfigService {
  private config: AppConfig | null = null;
  private readonly CONFIG_URL = 'assets/config/config.json';

  constructor(private http: HttpClient) {}

  loadConfig(): Observable<AppConfig> {
    return this.http.get<AppConfig>(this.CONFIG_URL).pipe(
      tap(config => {
        this.config = config;
        console.log('Config loaded', config);
      }),
      catchError(error => {
        console.error('Could not load configuration', error);
        return throwError(() => new Error('Could not load configuration'));
      })
    );
  }

  getConfig(): AppConfig {
    if (!this.config) {
      throw new Error('Config not loaded');
    }
    return this.config;
  }
}

Dann aktualisieren Sie die app.config.ts für APP_INITIALIZER:

import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

import { routes } from './app.routes';
import { ConfigService } from './services/config.service';

export function initializeApp(configService: ConfigService) {
  return () => configService.loadConfig().toPromise();
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [ConfigService],
      multi: true
    }
  ]
};

37.4.2 Health-Checks

Für bessere Überwachung in Kubernetes implementieren wir einen Health-Check-Endpunkt:

// src/app/health/health.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-health',
  template: '{"status":"UP"}',
})
export class HealthComponent {
  constructor() {}
}

Fügen Sie diese Route zu Ihren Angular-Routen hinzu:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HealthComponent } from './health/health.component';

export const routes: Routes = [
  // Andere Routen
  { path: 'health', component: HealthComponent },
];

37.5 CI/CD-Integration

37.5.1 GitHub Actions Workflow

Hier ist ein Beispiel für einen GitHub Actions Workflow, der das Docker-Image baut und das Helm-Chart bereitstellt:

name: Build and Deploy

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

env:
  IMAGE_NAME: angular-app
  REGISTRY: ghcr.io
  CHART_PATH: ./angular-app

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run tests
      run: npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
      
    - name: Build Angular app
      run: npm run build -- --configuration production
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Login to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract Docker metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Helm
      uses: azure/setup-helm@v3
      
    - name: Set up kubeconfig
      uses: azure/k8s-set-context@v3
      with:
        kubeconfig: ${{ secrets.KUBECONFIG }}
        
    - name: Deploy with Helm
      run: |
        helm upgrade --install angular-release ${{ env.CHART_PATH }} \
        --set image.repository=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} \
        --set image.tag=${GITHUB_SHA::8} \
        --namespace angular-apps

37.6 Überwachung und Logging

37.6.1 Prometheus Metriken

Um Prometheus-Metriken zu Ihrer Angular-Anwendung hinzuzufügen, können Sie eine einfache Metrik-Endpunkt implementieren:

// src/app/metrics/metrics.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-metrics',
  template: `# HELP angular_app_info Information about the Angular application
# TYPE angular_app_info gauge
angular_app_info{version="{{ version }}"} 1

# HELP angular_app_http_requests_total Total number of HTTP requests
# TYPE angular_app_http_requests_total counter
angular_app_http_requests_total{status="success"} {{ successRequests }}
angular_app_http_requests_total{status="error"} {{ errorRequests }}
`,
})
export class MetricsComponent implements OnInit {
  version = '1.0.0';
  successRequests = 0;
  errorRequests = 0;

  constructor() {}

  ngOnInit(): void {
    // Diese Werte würden normalerweise aus einem Service kommen
    this.successRequests = parseInt(localStorage.getItem('successRequests') || '0');
    this.errorRequests = parseInt(localStorage.getItem('errorRequests') || '0');
  }
}

Fügen Sie diese Route zu Ihren Angular-Routen hinzu und konfigurieren Sie die Service-Annotation in Kubernetes, um Prometheus-Scraping zu ermöglichen.

37.7 Best Practices