Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
FROM python:3.13-alpine
RUN addgroup -S infoservice
RUN adduser -S infoservice
RUN mkdir /app /venv
RUN chown infoservice:infoservice /venv /app
RUN adduser -D -H -h /app -u 2000 infoservice infoservice
RUN mkdir /app /venv /data
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"
Expand All @@ -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"]
2 changes: 1 addition & 1 deletion app_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>:<port>:5000 <docker image>
docker run <-it|-d> --name infoservice -p <127.0.0.1|0.0.0.0>:<port>:5000 -v <volume for data>:/data <docker image>
```

- Choose `-it` if you want the program to output logs to your terminal and you want to be able to terminate the
Expand Down
28 changes: 28 additions & 0 deletions app_python/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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())
Expand Down
8 changes: 8 additions & 0 deletions app_python/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh

if [ $(whoami) = root ]; then
chown infoservice:infoservice /data
exec su infoservice -s /docker-entrypoint.sh -- "$@"
fi

exec gunicorn "$@"
4 changes: 4 additions & 0 deletions app_python/tests/test_index.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import app
import re
import unittest.mock


"""
Expand Down Expand Up @@ -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):
Expand All @@ -112,3 +115,4 @@ def test_index():
validate_runtime(d["runtime"])
validate_service(d["service"])
validate_system(d["system"])
app.visit_counter = old
186 changes: 186 additions & 0 deletions k8s/CONFIGMAPS.md
Original file line number Diff line number Diff line change
@@ -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 <unset> 30s
```
4 changes: 4 additions & 0 deletions k8s/dinfochart/files/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"appname": "devops-infoservice",
"environment": "prod",
}
15 changes: 15 additions & 0 deletions k8s/dinfochart/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
29 changes: 13 additions & 16 deletions k8s/dinfochart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: data-volume
mountPath: /data
envFrom:
- configMapRef:
name: {{ include "dinfochart.fullname" . }}-env
volumes:
- name: app-secret
secret:
secretName: app-credentials
- name: app-config
configMap:
name: {{ include "dinfochart.fullname" . }}-config
- name: data-volume
persistentVolumeClaim:
claimName: {{ include "dinfochart.fullname" . }}-data
15 changes: 15 additions & 0 deletions k8s/dinfochart/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
5 changes: 5 additions & 0 deletions k8s/dinfochart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ readinessProbe:
secret:
username: "placeholder"
password: "placeholder"

persistence:
enabled: true
size: 100Mi
storageClass: "" # Use default
Loading
Loading