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.
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 productionDer Produktions-Build erzeugt eine optimierte Version der Anwendung
im dist/-Verzeichnis.
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.
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.
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 psDie Anwendung sollte nun unter http://localhost:8080
verfügbar sein.
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 nginxHelm 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.
Erstellen Sie eine neue Helm-Chart-Struktur mit dem Helm-CLI:
# Helm-Chart erstellen
helm create angular-appDies erzeugt eine Basisstruktur für unser Helm-Chart:
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.comDie 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"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" . }}-configDamit 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 }}"
}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.
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-appWenn 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-releaseModerne 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
}
]
};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 },
];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-appsUm 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.