Add Apache status monitoring to Helm chart
Publish PHP Apache image / Build, test and push (push) Successful in 1m10s

This commit is contained in:
2026-09-10 20:49:26 +02:00
parent 0396053c47
commit 5acc3b0154
11 changed files with 260 additions and 7 deletions
+20 -5
View File
@@ -13,7 +13,7 @@ upgrade also upgrades the runtime image.
Add and refresh the Gitea chart repository with:
```sh
helm repo add brunner https://code.brunner.ninja/api/packages/feedc0de/helm
helm repo add brunner https://brunner.ninja/charts
helm repo update brunner
```
@@ -40,6 +40,7 @@ helm upgrade --install example-site brunner/php-homepage \
--namespace default \
--values /path/to/example-site/values.yaml \
--take-ownership \
--force-conflicts \
--wait
```
@@ -55,7 +56,21 @@ With `persistence.existingClaim` empty, the chart creates an RWX PVC using
uninstall does not discard site content. Set `persistence.retain: false` only
when deleting the release should also delete its dynamically created claim.
Ingress, TLS, probes, scheduling, storage size, and the image tag are all
configurable through `values.yaml`. TCP probes are used by default because
individual websites may redirect `/`, require authentication, or select their
own HTTP status behavior.
Ingress, TLS, probes, scheduling, storage size, Apache's `ServerAdmin`, and the
image tag are all configurable through `values.yaml`. TCP startup, readiness,
and liveness probes are enabled by default because individual websites may
redirect `/`, require authentication, or select their own HTTP status behavior.
## Apache metrics
The image provides an extended `mod_status` endpoint on pod-local loopback port
8080. It is deliberately not exposed directly by the Service because extended
status can contain current client and request details.
Set `metrics.enabled` to add an Apache exporter sidecar and expose its sanitized
Prometheus metrics on the Service. `metrics.serviceMonitor.enabled` and
`metrics.prometheusRule.enabled` render the corresponding Prometheus Operator
resources when their CRDs exist. The rules cover exporter scrape failures,
Apache status failures, and worker saturation. No Prometheus namespace, release
label, or Alertmanager is hard-coded; selection and alert routing remain the
responsibility of the cluster's Prometheus installation.
@@ -36,10 +36,18 @@ spec:
- name: {{ include "php-homepage.fullname" . }}
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: APACHE_SERVER_ADMIN
value: {{ .Values.apache.serverAdmin | quote }}
- name: APACHE_STATUS_PORT
value: {{ .Values.apache.statusPort | quote }}
ports:
- name: http
containerPort: 80
protocol: TCP
- name: apache-status
containerPort: {{ .Values.apache.statusPort }}
protocol: TCP
{{- if .Values.probes.startup.enabled }}
startupProbe:
tcpSocket:
@@ -73,6 +81,36 @@ spec:
- name: webroot
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}
{{- if .Values.metrics.enabled }}
- name: apache-exporter
image: "{{ .Values.metrics.image.repository }}:{{ .Values.metrics.image.tag }}"
imagePullPolicy: {{ .Values.metrics.image.pullPolicy }}
args:
- "--scrape_uri=http://127.0.0.1:{{ .Values.apache.statusPort }}/server-status?auto"
- "--web.listen-address=:{{ .Values.metrics.port }}"
ports:
- name: metrics
containerPort: {{ .Values.metrics.port }}
protocol: TCP
readinessProbe:
httpGet:
path: /metrics
port: metrics
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /metrics
port: metrics
periodSeconds: 30
timeoutSeconds: 2
failureThreshold: 3
{{- with .Values.metrics.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if .Values.persistence.enabled }}
volumes:
- name: webroot
@@ -0,0 +1,49 @@
{{- if and .Values.metrics.enabled .Values.metrics.prometheusRule.enabled (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1/PrometheusRule") }}
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: {{ include "php-homepage.fullname" . }}
labels:
{{- include "php-homepage.labels" . | nindent 4 }}
{{- with .Values.metrics.prometheusRule.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.metrics.prometheusRule.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
groups:
- name: {{ include "php-homepage.fullname" . }}.apache
rules:
{{- if .Values.metrics.prometheusRule.exporterDown.enabled }}
- alert: ApacheExporterDown
expr: up{namespace={{ .Release.Namespace | quote }},service={{ include "php-homepage.fullname" . | quote }},endpoint="metrics"} == 0
for: {{ .Values.metrics.prometheusRule.exporterDown.for }}
labels:
severity: {{ .Values.metrics.prometheusRule.exporterDown.severity | quote }}
annotations:
summary: {{ printf "Apache exporter for %s is unavailable" (include "php-homepage.fullname" .) | quote }}
description: {{ printf "Prometheus has not been able to scrape the Apache exporter in namespace %s." .Release.Namespace | quote }}
{{- end }}
{{- if .Values.metrics.prometheusRule.apacheDown.enabled }}
- alert: ApacheDown
expr: apache_up{namespace={{ .Release.Namespace | quote }},service={{ include "php-homepage.fullname" . | quote }}} == 0
for: {{ .Values.metrics.prometheusRule.apacheDown.for }}
labels:
severity: {{ .Values.metrics.prometheusRule.apacheDown.severity | quote }}
annotations:
summary: {{ printf "Apache for %s is unavailable" (include "php-homepage.fullname" .) | quote }}
description: {{ printf "The exporter cannot read Apache server-status in namespace %s." .Release.Namespace | quote }}
{{- end }}
{{- if .Values.metrics.prometheusRule.workerSaturation.enabled }}
- alert: ApacheWorkerSaturation
expr: (sum(apache_workers{namespace={{ .Release.Namespace | quote }},service={{ include "php-homepage.fullname" . | quote }},state="busy"}) / clamp_min(sum(apache_workers{namespace={{ .Release.Namespace | quote }},service={{ include "php-homepage.fullname" . | quote }}}), 1)) > {{ .Values.metrics.prometheusRule.workerSaturation.threshold }}
for: {{ .Values.metrics.prometheusRule.workerSaturation.for }}
labels:
severity: {{ .Values.metrics.prometheusRule.workerSaturation.severity | quote }}
annotations:
summary: {{ printf "Apache workers for %s are nearly exhausted" (include "php-homepage.fullname" .) | quote }}
description: {{ printf "The ratio of busy to available Apache workers has exceeded the configured threshold (%v) for the configured duration." .Values.metrics.prometheusRule.workerSaturation.threshold | quote }}
{{- end }}
{{- end }}
+6
View File
@@ -11,5 +11,11 @@ spec:
port: {{ .Values.service.port }}
protocol: TCP
targetPort: http
{{- if .Values.metrics.enabled }}
- name: metrics
port: {{ .Values.metrics.port }}
protocol: TCP
targetPort: metrics
{{- end }}
selector:
{{- include "php-homepage.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,24 @@
{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1/ServiceMonitor") }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "php-homepage.fullname" . }}
labels:
{{- include "php-homepage.labels" . | nindent 4 }}
{{- with .Values.metrics.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.metrics.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "php-homepage.selectorLabels" . | nindent 6 }}
endpoints:
- port: metrics
path: /metrics
interval: {{ .Values.metrics.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
{{- end }}
+42 -1
View File
@@ -1,10 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["image", "persistence", "service", "ingress"],
"required": ["apache", "image", "metrics", "persistence", "service", "ingress"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"fullnameOverride": { "type": "string" },
"apache": {
"type": "object",
"required": ["serverAdmin", "statusPort"],
"properties": {
"serverAdmin": { "type": "string", "minLength": 3 },
"statusPort": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
},
"image": {
"type": "object",
"required": ["repository", "tag", "pullPolicy"],
@@ -14,6 +22,39 @@
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"metrics": {
"type": "object",
"required": ["enabled", "image", "port", "serviceMonitor", "prometheusRule"],
"properties": {
"enabled": { "type": "boolean" },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"image": {
"type": "object",
"required": ["repository", "tag", "pullPolicy"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"serviceMonitor": {
"type": "object",
"required": ["enabled", "interval", "scrapeTimeout"],
"properties": {
"enabled": { "type": "boolean" },
"interval": { "type": "string", "minLength": 2 },
"scrapeTimeout": { "type": "string", "minLength": 2 }
}
},
"prometheusRule": {
"type": "object",
"required": ["enabled"],
"properties": {
"enabled": { "type": "boolean" }
}
}
}
},
"persistence": {
"type": "object",
"required": ["enabled", "existingClaim", "mountPath"],
+36
View File
@@ -16,6 +16,10 @@ fullnameOverride: ""
deploymentAnnotations: {}
podAnnotations: {}
apache:
serverAdmin: webmaster@localhost
statusPort: 8080
strategy:
type: RollingUpdate
rollingUpdate:
@@ -26,6 +30,38 @@ service:
type: ClusterIP
port: 80
metrics:
enabled: false
image:
repository: quay.io/lusitaniae/apache-exporter
tag: v1.1.1
pullPolicy: IfNotPresent
port: 9117
resources: {}
serviceMonitor:
enabled: false
interval: 30s
scrapeTimeout: 10s
labels: {}
annotations: {}
prometheusRule:
enabled: false
labels: {}
annotations: {}
exporterDown:
enabled: true
for: 5m
severity: warning
apacheDown:
enabled: true
for: 5m
severity: critical
workerSaturation:
enabled: true
threshold: 0.9
for: 10m
severity: warning
persistence:
enabled: true
existingClaim: ""