From fc62e33b79896fb903a45e3cd2c93a1ebce0768d Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:10:56 +0300 Subject: [PATCH 1/9] Add visit_count counter and GET /visits endpoint to app.py --- app_python/app.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app_python/app.py b/app_python/app.py index 14d3caa4c0..fb691bb42d 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -3,6 +3,7 @@ Main application module """ import json +from threading import Lock from flask import Flask, Response, jsonify, request from datetime import datetime, timezone import logging @@ -12,9 +13,30 @@ from prometheus_client import Counter, Histogram, Gauge, generate_latest +class VisitCounter: + lock: Lock + visit_count: int + FILENAME: str = '/data/visits' + + def __init__(self): + try: + with open(VisitCounter.FILENAME) as f: + self.visit_count = int(f.readline()) + except FileNotFoundError, ValueError: + self.visit_count = 0 + self.lock = Lock() + + def inc(self) -> None: + with self.lock: + self.visit_count = self.visit_count + 1 + with open(VisitCounter.FILENAME, 'w') as f: + _ = f.write(str(self.visit_count)) + + HOST = os.getenv('HOST', '0.0.0.0') PORT = int(os.getenv('PORT', 5000)) DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +visit_counter = VisitCounter() class PrometheusStats: @@ -128,6 +150,7 @@ def get_http_extra_info(): @prometheus.http_requests_in_progress.track_inprogress() def index(): """Main endpoint - service and system information.""" + visit_counter.inc() logger.debug(f'Request: {request.method} {request.path}', extra=get_http_extra_info()) with prometheus.system_info_duration_seconds.time(): response = { @@ -167,6 +190,11 @@ def metrics(): return Response(response=generate_latest(), status=200, content_type='text/plain') +@app.route('/visits') +def get_visits(): + return jsonify({'visits': visit_counter.visit_count}) + + @app.errorhandler(404) def notfound_handler(e): logger.info('A 404 Not Found error occured', extra=get_http_extra_info()) From 52aad9b16e592841aa978dffdf7897aab466cb3d Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:11:38 +0300 Subject: [PATCH 2/9] Create /data directory in dockerfile --- app_python/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_python/Dockerfile b/app_python/Dockerfile index 7b96447f2e..2edd71ff97 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.13-alpine RUN addgroup -S infoservice RUN adduser -S infoservice -RUN mkdir /app /venv +RUN mkdir /app /venv /data RUN chown infoservice:infoservice /venv /app WORKDIR /app USER infoservice From 1abb68ca947a8e88c14951b4833c3568cc809c4d Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:14:09 +0300 Subject: [PATCH 3/9] Add volume to monitoring/docker-compose.yml --- monitoring/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index 00a5f1cc63..f893cbd947 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -86,6 +86,7 @@ services: ports: - 9090:9090 volumes: + - app-data:/data:rw - ${PWD}/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus:rw command: @@ -107,3 +108,4 @@ volumes: loki-data: grafana-data: prometheus-data: + app-data: From a4ce7cedf8d7acdbc2e3cd33791ec0b1a8f15914 Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:20:30 +0300 Subject: [PATCH 4/9] Fix app.py --- app_python/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_python/app.py b/app_python/app.py index fb691bb42d..1eb4db77ab 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -22,7 +22,7 @@ def __init__(self): try: with open(VisitCounter.FILENAME) as f: self.visit_count = int(f.readline()) - except FileNotFoundError, ValueError: + except (FileNotFoundError, ValueError): self.visit_count = 0 self.lock = Lock() From 5a5643d98f3fb4376ab6f049a329610922d33a7a Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:49:38 +0300 Subject: [PATCH 5/9] Fix infoservice docker image --- app_python/Dockerfile | 9 ++++----- app_python/docker-entrypoint.sh | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100755 app_python/docker-entrypoint.sh diff --git a/app_python/Dockerfile b/app_python/Dockerfile index 2edd71ff97..a70562329b 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -1,10 +1,9 @@ FROM python:3.13-alpine -RUN addgroup -S infoservice -RUN adduser -S infoservice +RUN adduser -D -H -h /app -u 2000 infoservice infoservice RUN mkdir /app /venv /data -RUN chown infoservice:infoservice /venv /app +RUN chown infoservice:infoservice /venv /app /data WORKDIR /app -USER infoservice +COPY docker-entrypoint.sh / RUN python -m venv /venv ENV PATH="/venv/bin:$PATH" ENV VIRTUAL_ENV="/venv" @@ -14,5 +13,5 @@ RUN pip install -r requirements.txt COPY --chown=infoservice:infoservice app.py . EXPOSE 5000 -ENTRYPOINT ["gunicorn"] +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["-b", "0.0.0.0:5000", "-e", "DEBUG=true", "app:app"] diff --git a/app_python/docker-entrypoint.sh b/app_python/docker-entrypoint.sh new file mode 100755 index 0000000000..d8a3e28be6 --- /dev/null +++ b/app_python/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if [ $(whoami) = root ]; then + chown infoservice:infoservice /data + exec su infoservice -s /docker-entrypoint.sh -- "$@" +fi + +exec gunicorn "$@" From f4a4aad6430961b11d92f849574fcde97e1754dd Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 21:52:52 +0300 Subject: [PATCH 6/9] Update README --- app_python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_python/README.md b/app_python/README.md index bbd3be3d1c..342ffaa49f 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -98,7 +98,7 @@ In the next section, refer to the image as `timurusmanov/devops-infoservice`. Here is a template for the command you need to execute: ```bash -docker run <-it|-d> --name infoservice -p <127.0.0.1|0.0.0.0>::5000 +docker run <-it|-d> --name infoservice -p <127.0.0.1|0.0.0.0>::5000 -v :/data ``` - Choose `-it` if you want the program to output logs to your terminal and you want to be able to terminate the From caa78ccdc9e1126ec35f6ad9e807bd90874e70b8 Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 22:05:24 +0300 Subject: [PATCH 7/9] Add config mounts and env to deployment --- k8s/dinfochart/files/config.json | 4 ++++ k8s/dinfochart/templates/configmap.yaml | 15 ++++++++++++ k8s/dinfochart/templates/deployment.yaml | 29 +++++++++++------------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 k8s/dinfochart/files/config.json create mode 100644 k8s/dinfochart/templates/configmap.yaml diff --git a/k8s/dinfochart/files/config.json b/k8s/dinfochart/files/config.json new file mode 100644 index 0000000000..46c93034e9 --- /dev/null +++ b/k8s/dinfochart/files/config.json @@ -0,0 +1,4 @@ +{ + "appname": "devops-infoservice", + "environment": "prod", +} diff --git a/k8s/dinfochart/templates/configmap.yaml b/k8s/dinfochart/templates/configmap.yaml new file mode 100644 index 0000000000..c78361e0a0 --- /dev/null +++ b/k8s/dinfochart/templates/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dinfochart.fullname" . }}-config +data: + config.json: |- +{{ .Files.Get "files/config.json" | indent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dinfochart.fullname" . }}-env +data: + APP_ENV: {{ .Values.environment | quote }} + LOG_LEVEL: {{ .Values.logLevel | quote }} diff --git a/k8s/dinfochart/templates/deployment.yaml b/k8s/dinfochart/templates/deployment.yaml index 328aa59b6e..1545a214ee 100644 --- a/k8s/dinfochart/templates/deployment.yaml +++ b/k8s/dinfochart/templates/deployment.yaml @@ -43,22 +43,19 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} - env: - - name: username - valueFrom: - secretKeyRef: - name: app-credentials - key: username - - name: password - valueFrom: - secretKeyRef: - name: app-credentials - key: password volumeMounts: - - name: app-secret - mountPath: "/etc/app-secret" + - name: app-config + mountPath: /config readOnly: true + - name: app-data + mountPath: /data + envFrom: + - configMapRef: + name: {{ include "dinfochart.fullname" . }}-env volumes: - - name: app-secret - secret: - secretName: app-credentials + - name: app-config + configMap: + name: devops-info-config + - name: app-data + persistentVolumeClaim: + claimName: devops-info-data From 5dce54a02f304b74c0c5e51731f714494823fe61 Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 22:57:50 +0300 Subject: [PATCH 8/9] Finish lab12 --- k8s/CONFIGMAPS.md | 186 +++++++++++++++++++++++ k8s/dinfochart/templates/deployment.yaml | 8 +- k8s/dinfochart/templates/pvc.yaml | 15 ++ k8s/dinfochart/values.yaml | 5 + 4 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 k8s/CONFIGMAPS.md create mode 100644 k8s/dinfochart/templates/pvc.yaml diff --git a/k8s/CONFIGMAPS.md b/k8s/CONFIGMAPS.md new file mode 100644 index 0000000000..540e652887 --- /dev/null +++ b/k8s/CONFIGMAPS.md @@ -0,0 +1,186 @@ +## Application Changes +### Description of visits counter implementation +I created a class VisitCounter, instances of which read from `/data/visits` upon creation and support incrementing. +When incrementing, the object locks a `Lock`, increments the attribute, and saves the value to `/data/visits`. + +The program creates one instance of `VisitCounter` and uses it in `GET /` (to increment) and `GET /visits` (to get the +current number of visits). + +### New endpoint documentation +`GET /visits` returns an object of the format +```json +{ + "visits": 123 +} +``` + +### Local testing evidence with Docker +I ran the new image with +```bash +docker run -it --rm -p 5000:5000 -e DEBUG=true -v ./data:/data timurusmanov/devops-infoservice:latest +``` + +Even after reloading, the number of visits does not reset: + +```bash +cat data/visits +``` +```text +5 +``` + +## ConfigMap Implementation +### ConfigMap template structure +Apart from the common fields, it just defines `data:`. + +### `config.json` content +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dinfochart.fullname" . }}-config +data: + config.json: |- +{{ .Files.Get "files/config.json" | indent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dinfochart.fullname" . }}-env +data: + APP_ENV: {{ .Values.environment | quote }} + LOG_LEVEL: {{ .Values.logLevel | quote }} +``` + +### How ConfigMap is mounted as file +It is done with volume mounts in `deployment.yaml`: + +```yaml +volumeMounts: + - name: app-config + mountPath: /config + readOnly: true +``` +... +```yaml +volumes: + - name: app-config + configMap: + name: {{ include "dinfochart.fullname" . }}-config +``` + +### How ConfigMap provides environment variables +In the `envFrom` section, using `configMapRef`: +```yaml +envFrom: + - configMapRef: + name: {{ include "dinfochart.fullname" . }}-env +``` + +### Verification outputs +```bash +kubectl get pods +``` +```text +NAME READY STATUS RESTARTS AGE +dinfochart-54c9dd4c9-crjtl 1/1 Running 0 10m +``` + +```text +kubectl exec dinfochart-54c9dd4c9-crjtl -it -- sh +/app # +/app # env +KUBERNETES_PORT=tcp://10.96.0.1:443 +KUBERNETES_SERVICE_PORT=443 +LOG_LEVEL= +DINFOCHART_PORT_80_TCP=tcp://10.109.215.47:80 +HOSTNAME=dinfochart-54c9dd4c9-crjtl +SHLVL=1 +HOME=/root +GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305 +PYTHON_SHA256=2a84cd31dd8d8ea8aaff75de66fc1b4b0127dd5799aa50a64ae9a313885b4593 +DINFOCHART_SERVICE_HOST=10.109.215.47 +TERM=xterm +KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 +PATH=/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +KUBERNETES_PORT_443_TCP_PORT=443 +DINFOCHART_SERVICE_PORT=80 +KUBERNETES_PORT_443_TCP_PROTO=tcp +DINFOCHART_PORT=tcp://10.109.215.47:80 +PYTHON_VERSION=3.13.12 +DINFOCHART_PORT_80_TCP_ADDR=10.109.215.47 +KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 +KUBERNETES_SERVICE_PORT_HTTPS=443 +DINFOCHART_PORT_80_TCP_PORT=80 +VIRTUAL_ENV=/venv +APP_ENV= +DINFOCHART_PORT_80_TCP_PROTO=tcp +KUBERNETES_SERVICE_HOST=10.96.0.1 +PWD=/app +/app # +/app # cat /config/config.json +{ + "appname": "devops-infoservice", + "environment": "prod", +} +``` + +## Persistent Volume +### PVC configuration explanation + + +### Access modes and storage class discussion +### Volume mount configuration +### Counter value before pod deletion +```bash +kubectl port-forward pods/dinfochart-667547c76b-n8m8j 8080:5000& +curl localhost:8080 +curl localhost:8080 +curl localhost:8080 +curl localhost:8080 +``` +```bash +curl localhost:8080/visits +``` +```text +4 +``` +### Pod deletion command +```bash +kubectl delete pod dinfochart-667547c76b-n8m8j +kubectl get pods +``` +```text +NAME READY STATUS RESTARTS AGE +dinfochart-667547c76b-p6w2c 1/1 Running 0 16s +``` +(The pod restarted) +### Counter value after new pod starts +```bash +curl localhost:8080/visits +``` +```text +4 +``` + +## ConfigMap vs Secret +### When to use ConfigMap +ConfigMap is for providing non-sensitive configuration details. + +### When to use Secret +`Secret` is for providing parameters whose value should not be seen by anybody. + +## Required Screenshots/Outputs: + +```bash +kubectl get configmap,pvc +``` +```text +NAME DATA AGE +configmap/dinfochart-config 1 30s +configmap/dinfochart-env 2 30s +configmap/kube-root-ca.crt 1 36m + +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE +persistentvolumeclaim/dinfochart-data Bound pvc-11af931d-5ee4-4c4e-8364-cee981dc3183 100Mi RWO standard 30s +``` diff --git a/k8s/dinfochart/templates/deployment.yaml b/k8s/dinfochart/templates/deployment.yaml index 1545a214ee..f058aa033a 100644 --- a/k8s/dinfochart/templates/deployment.yaml +++ b/k8s/dinfochart/templates/deployment.yaml @@ -47,7 +47,7 @@ spec: - name: app-config mountPath: /config readOnly: true - - name: app-data + - name: data-volume mountPath: /data envFrom: - configMapRef: @@ -55,7 +55,7 @@ spec: volumes: - name: app-config configMap: - name: devops-info-config - - name: app-data + name: {{ include "dinfochart.fullname" . }}-config + - name: data-volume persistentVolumeClaim: - claimName: devops-info-data + claimName: {{ include "dinfochart.fullname" . }}-data diff --git a/k8s/dinfochart/templates/pvc.yaml b/k8s/dinfochart/templates/pvc.yaml new file mode 100644 index 0000000000..57ae29a022 --- /dev/null +++ b/k8s/dinfochart/templates/pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "dinfochart.fullname" . }}-data + labels: + {{- include "dinfochart.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} diff --git a/k8s/dinfochart/values.yaml b/k8s/dinfochart/values.yaml index 329839cebd..9bf1e1b26b 100644 --- a/k8s/dinfochart/values.yaml +++ b/k8s/dinfochart/values.yaml @@ -34,3 +34,8 @@ readinessProbe: secret: username: "placeholder" password: "placeholder" + +persistence: + enabled: true + size: 100Mi + storageClass: "" # Use default From ded1ad84a9ad30052c731b725108dc931fb17e76 Mon Sep 17 00:00:00 2001 From: Timur Usmanov Date: Thu, 16 Apr 2026 23:06:58 +0300 Subject: [PATCH 9/9] Mock VisitCounter in tests --- app_python/tests/test_index.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app_python/tests/test_index.py b/app_python/tests/test_index.py index 2e57e022c1..67aded1975 100644 --- a/app_python/tests/test_index.py +++ b/app_python/tests/test_index.py @@ -1,5 +1,6 @@ import app import re +import unittest.mock """ @@ -98,6 +99,8 @@ def validate_system(sysinfo): def test_index(): + old = app.visit_counter + app.visit_counter = unittest.mock.MagicMock(app.visit_counter) app.app.testing = True with app.app.test_client() as client: for _ in range(3): @@ -112,3 +115,4 @@ def test_index(): validate_runtime(d["runtime"]) validate_service(d["service"]) validate_system(d["system"]) + app.visit_counter = old