diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..832cafdf6e --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,14 @@ +__pycache__/ +*.pyc +*.pyo + +.git/ +.gitignore + +venv/ +.venv/ + +.idea/ +.vscode/ + +tests/ diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..ba1db0ed69 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..a378457e96 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN useradd -m appuser +RUN chown -R appuser:appuser /app + +USER appuser + +COPY . . + +EXPOSE 8000 + +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..8852c10246 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,54 @@ +# DevOps Info Service + +A lightweight Python web application that provides system, runtime, and service information via HTTP endpoints. +Developed as part of **Lab 01 — Python Web Application** for a DevOps course. + +--- + +## 🚀 Features + +- Service metadata endpoint +- Detailed system information (OS, CPU, Python version) +- Runtime uptime tracking +- Health check endpoint for monitoring +- JSON responses suitable for automation and probes + +--- + +## 🛠 Tech Stack + +- Python 3.10+ +- Flask 3.1.0 + +--- + +## 📦 Installation + +```bash +python -m venv venv +source venv/bin/activate # Linux / macOS +pip install -r requirements.txt +``` +--- + +## Docker + +This application can be built and run using Docker. + +### Build the image locally + +Use the Docker build command to create an image from the Dockerfile: + +`docker build -t ` + +### Run the container + +Run the container with port mapping to access the service from the host machine: + +`docker run -p : ` + +### Pull the image from Docker Hub + +The image can be pulled from Docker Hub using: + +`docker pull /:` diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..ed27c90ac4 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,109 @@ +import os +import socket +import platform +import logging +from datetime import datetime, timezone + +from flask import Flask, jsonify, request + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +app = Flask(__name__) + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +START_TIME = datetime.now(timezone.utc) + + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"} + + +def get_system_info(): + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + } + + +@app.route("/", methods=["GET"]) +def index(): + logger.info(f"Request: {request.method} {request.path}") + uptime = get_uptime() + + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get("User-Agent"), + "method": request.method, + "path": request.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + + return jsonify(response) + + +@app.route("/health", methods=["GET"]) +def health(): + uptime = get_uptime() + return jsonify( + { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime["seconds"], + } + ) + + +@app.errorhandler(404) +def not_found(error): + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 + + +@app.errorhandler(500) +def internal_error(error): + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + ), + 500, + ) + + +if __name__ == "__main__": + logger.info("Application starting...") + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..c5622cfcab --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,192 @@ +# Lab 01 — Python Web Application + +## Framework Selection + +**Chosen framework: Flask** + +Flask was selected because it is lightweight, easy to understand, and well-suited for small production-ready services such as health checks, monitoring endpoints, and internal DevOps tools. It allows rapid development without unnecessary complexity. + +### Framework Comparison + +| Framework | Advantages | Disadvantages | Use Case | +| --------- | ----------------------------- | ---------------------------------- | ---------------------------- | +| **Flask** | Simple, lightweight, flexible | No built-in async, fewer built-ins | Small services, DevOps tools | +| **FastAPI** | Async support, OpenAPI docs | Slightly steeper learning curve | High-performance APIs | +| **Django** | Full-featured, ORM included | Overkill for small services | Large web applications | + +Flask provides the best balance between simplicity and production readiness for this laboratory work. + +--- + +## Best Practices Applied + +### 1. Clean Code Organization + +**What was done:** + +* Logical separation of configuration, helpers, routes, and error handlers +* Clear and descriptive function names +* Imports grouped according to PEP 8 + +**Code example:** + +```python +START_TIME = datetime.now(timezone.utc) + +def get_system_info(): + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'architecture': platform.machine(), + 'python_version': platform.python_version() + } +``` + +**Why it matters:** +Clean code improves readability, maintainability, and reduces the likelihood of bugs. + +--- + +### 2. Configuration via Environment Variables + +**What was done:** +Application host, port, and debug mode are configurable through environment variables. + +**Code example:** + +```python +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +``` + +**Why it matters:** +Environment-based configuration is essential for deploying applications across different environments (local, CI, containers, Kubernetes). + +--- + +### 3. Error Handling + +**What was done:** +Custom JSON responses for 404 and 500 errors. + +**Code example:** + +```python +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 +``` + +**Why it matters:** +Consistent error handling improves debugging, observability, and user experience. + +--- + +### 4. Logging + +**What was done:** +Structured logging is enabled for application startup and incoming requests. + +**Code example:** + +```python +logger.info('Application starting...') +logger.info(f"Request: {request.method} {request.path}") +``` + +**Why it matters:** +Logging is critical for monitoring, troubleshooting, and auditing in production systems. + +--- + +## API Documentation + +### GET / + +**Description:** Returns service metadata, system information, runtime details, and request context. + +**Request example:** + +```bash +curl http://localhost:5000/ +``` + +**Response example (shortened):** + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "hostname": "my-host", + "platform": "Linux" + } +} +``` + +--- + +### GET /health + +**Description:** Health check endpoint used for monitoring and probes. + +**Request example:** + +```bash +curl http://localhost:5000/health +``` + +**Response example:** + +```json +{ + "status": "healthy", + "uptime_seconds": 3600 +} +``` + +--- + +### Testing Commands + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +curl http://localhost:5000/ | jq +``` +![Testing](./screenshots/test.png) +--- + +## Testing Evidence + +The following evidence demonstrates correct application behavior: + +* Screenshot of `/` endpoint returning full JSON response +* Screenshot of `/health` endpoint returning healthy status +* Screenshot of formatted JSON output using `jq` +* Terminal output showing successful application startup + +All screenshots are stored in the `docs/screenshots/` directory. + +--- + +## Challenges & Solutions + +### Challenge: Timezone consistency + +**Problem:** Mixing local time and UTC could lead to inconsistent timestamps. + +**Solution:** +All timestamps are generated using `datetime.now(timezone.utc)` to ensure consistency and correctness. + +--- + +## GitHub Community + +Starring repositories helps support open-source contributors and increases the visibility of useful projects. Following developers allows engineers to learn best practices, stay updated with industry trends, and grow professionally through community collaboration. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..afa5039a82 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,110 @@ +# LAB02 — Dockerizing a Python Application + +## Docker Best Practices Applied + +### 🔹 Non-root user +**Practice:** The container runs the application as a non-root user. + +**Why it matters:** +Running containers as root increases security risks. If an attacker exploits the application, +they may gain elevated privileges inside the container. Using a non-root user limits the +potential impact of security vulnerabilities. + +**Implementation:** +``` +RUN useradd -m appuser +RUN chown -R appuser:appuser /app +USER appuser +``` + +### 🔹 Layer caching for dependencies +**Practice:** Dependencies are installed before copying application source code. + +**Why it matters:** +Docker caches layers during image builds. By copying `requirements.txt` and installing +dependencies first, Docker can reuse the cached layer when application code changes, +significantly speeding up rebuilds. + +**Implementation:** +``` +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +``` + +### 🔹 `.dockerignore` +**Practice:** A `.dockerignore` file is used to exclude unnecessary files. + +**Why it matters:** +Excluding files such as virtual environments, Git metadata, and cache files reduces +the build context size, speeds up the build process, and results in smaller images. + +### 🔹 Specific base image version +**Practice:** A specific Python image version is used instead of `latest`. + +**Why it matters:** +Pinning the base image version ensures reproducible builds and prevents unexpected +breaking changes caused by upstream updates. +--- +## Image Information & Decisions + +### Base image choice +The image is based on `python:3.12-slim`. + +This version was chosen because: +- It provides a stable and modern Python version +- The slim variant significantly reduces image size +- It avoids compatibility issues often encountered with Alpine images + +### Image size +The final image size is approximately 141MB. + +This size is reasonable for a Python service and reflects the use of a slim base image +and exclusion of unnecessary build artifacts. + +### Layer structure +The Dockerfile is structured to maximize cache efficiency: +1. Base image selection +2. Dependency installation +3. User creation and permission setup +4. Application source code copy +5. Application startup command + +--- +## Build & Run Process + +![build](screenshots/build.png) +![run](screenshots/run.png) +![hub](screenshots/hub.png) + +https://hub.docker.com/repository/docker/din19pg/python-service/general +--- + +## Technical Analysis + +### 🔹 Why does this Dockerfile work? +The Dockerfile correctly defines all required steps to build and run the application, +including dependency installation, security configuration, and runtime execution. +Each instruction builds upon the previous layer in a predictable and reproducible way. + +### 🔹 What if layer order changed? +If application source code were copied before installing dependencies, Docker would +invalidate the cache on every code change, resulting in slower rebuild times. + +### 🔹 Security considerations +Security was improved by running the application as a non-root user and minimizing +the attack surface using a slim base image. + +### 🔹 Role of `.dockerignore` +The `.dockerignore` file prevents unnecessary files from being sent to the Docker daemon, +which reduces build time and avoids leaking development artifacts into the image. + +--- +## Challenges & Solutions + +One challenge encountered was an incorrect build context path, which caused Docker +to fail with a "path not found" error. This was resolved by verifying the project +directory structure and running the build command from the correct location. + +Through this process, I gained a deeper understanding of Docker image layering, +build contexts, and container security best practices. + diff --git a/app_python/docs/screenshots/build.png b/app_python/docs/screenshots/build.png new file mode 100644 index 0000000000..d53bfcecd5 Binary files /dev/null and b/app_python/docs/screenshots/build.png differ diff --git a/app_python/docs/screenshots/hub.png b/app_python/docs/screenshots/hub.png new file mode 100644 index 0000000000..a7839d8765 Binary files /dev/null and b/app_python/docs/screenshots/hub.png differ diff --git a/app_python/docs/screenshots/run.png b/app_python/docs/screenshots/run.png new file mode 100644 index 0000000000..97ee7b9a06 Binary files /dev/null and b/app_python/docs/screenshots/run.png differ diff --git a/app_python/docs/screenshots/test.png b/app_python/docs/screenshots/test.png new file mode 100644 index 0000000000..7a04b3765b Binary files /dev/null and b/app_python/docs/screenshots/test.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..22ac75b399 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..795d55c4b0 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,224 @@ +# Lab 9 — Kubernetes Fundamentals + + +## 1. Architecture Overview + +This project deploys the **devops-info-service** application to a local Kubernetes cluster using **minikube**. + + +**Architecture Diagram**: +```text ++------------------+ +-----------------------------------+ +--------------------------------+ +| User / Browser | -----> | Service: devops-info-service | -----> | Deployment: devops-info-service | +| | | Type: NodePort | | Replicas: 5 | +| Request to app | | Port: 80 | | | +| | | NodePort: 30007 | | Pod 1 -> :5000 | ++------------------+ +-----------------------------------+ | Pod 2 -> :5000 | + | Pod 3 -> :5000 | + | Pod 4 -> :5000 | + | Pod 5 -> :5000 | + +---------------------------------+ +``` + +Resource configuration from the manifests: +- Requests: `100m CPU`, `128Mi memory` +- Limits: `200m CPU`, `256Mi memory` + +--- + +## 2. Manifest Files + +### `k8s/deployment.yml` +This file defines the Deployment for the Python application. + +Key configuration: +- image: `din19pg/python-service:latest` +- 5 replicas +- port `5000` +- liveness and readiness probes on `/health` +- resource requests and limits +- RollingUpdate strategy (`maxUnavailable: 1`, `maxSurge: 1`) + +### `k8s/service.yml` +This file defines the Service that exposes the Deployment. + +Key configuration: +- type: `NodePort` +- port `80` +- targetPort `5000` +- nodePort `30007` +- selector: `app=devops-info` + +--- + +## 3. Deployment Evidence + +### Cluster Setup +The local cluster was created with **minikube**. + +Commands used: +```bash + kubectl version --client + minikube version + minikube start + kubectl cluster-info + kubectl get nodes + kubectl get pods -A + kubectl get namespaces + kubectl get all -A +``` + +Result: +- minikube started successfully +- the node status was `Ready` +- Kubernetes system components were running + +![img.png](img/img0.png) + +![img/img1.png](img/img1.png) + +--- + +### Deployment +The Deployment was applied with: +```bash + kubectl apply -f k8s/deployment.yml +``` + +Verification commands: +```bash + kubectl get deployments + kubectl get pods + kubectl describe deployment devops-info-service +``` + +Result: +- Deployment created successfully +- Pods reached `Running` state +- health checks and resource settings were visible in `describe` + +![img/img.png](img/img.png) + +--- + +### Service +The Service was created with: +```bash + kubectl apply -f k8s/service.yml +``` + +Verification commands: +```bash + kubectl get services + kubectl describe service devops-info-service + kubectl get endpoints +``` + +Result: +- Service type was `NodePort` +- nodePort `30007` was assigned +- endpoints were linked to application Pods + +![img/img3.png](img/img3.png) + +--- + +## 4. Operations Performed + +### Deploy +```bash + kubectl apply -f k8s/deployment.yml + kubectl apply -f k8s/service.yml +``` + +### Scale +The Deployment was scaled and verified at **5 replicas**. + +Commands used: +```bash + kubectl apply -f k8s/deployment.yml + kubectl get pods -w +``` + +Result: +- 5 Pods were running successfully + +![img/img4.png](img/img5.png) + +### Rolling Update +A rolling update was performed by reapplying the Deployment manifest. + +Commands used: +```bash + kubectl apply -f k8s/deployment.yml + kubectl rollout status deployment/devops-info-service +``` + +Result: +- Pods were updated gradually +- rollout completed successfully +- the application remained available during the update + +![img/img6.png](img/img6.png) + +### Rollback +Rollback was demonstrated with: +```bash + kubectl rollout history deployment/devops-info-service + kubectl rollout undo deployment/devops-info-service + kubectl rollout status deployment/devops-info-service +``` + +Result: +- the previous revision was restored successfully + +![img/img7.png](img/img7.png) +--- + +## 5. Production Considerations + +### Health Checks +Liveness and readiness probes were configured on `/health`. This helps Kubernetes restart unhealthy containers and send traffic only to ready Pods. + +### Resource Limits +CPU and memory requests/limits were added to improve scheduling and prevent excessive resource usage. + +### Possible Production Improvements +For a real production environment, I would also add: +- Ingress instead of only NodePort +- TLS/HTTPS +- ConfigMaps and Secrets +- monitoring and logging +- Horizontal Pod Autoscaler + +--- + +## 6. Challenges and Solutions + +### Challenge 1: Watching rollout progress +During rollout and rollback, Kubernetes updated Pods gradually, so the process took time. + +**Solution:** +I used: +```bash + kubectl rollout status deployment/devops-info-service + kubectl get pods -w +``` + +### Challenge 2: Verifying the Service selector +It was necessary to confirm that the Service routed traffic to the correct Pods. + +**Solution:** +I used: +```bash + kubectl describe service devops-info-service + kubectl get endpoints +``` + +### What I Learned +In this lab, I learned: +- how to deploy applications with Kubernetes manifests +- how Deployments manage replicas and updates +- how Services expose Pods +- how readiness/liveness probes improve reliability +- how rollback works in Kubernetes diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000000..0b82547037 --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: devops-info-service + labels: + app: devops-info +spec: + replicas: 5 + + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + + selector: + matchLabels: + app: devops-info + + template: + metadata: + labels: + app: devops-info + + spec: + containers: + - name: devops-info-container + image: din19pg/python-service:latest + + ports: + - containerPort: 5000 + + env: + - name: PORT + value: "5000" + - name: DEBUG + value: "true" + + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "200m" + + livenessProbe: + httpGet: + path: /health + port: 5000 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 5000 + initialDelaySeconds: 5 + periodSeconds: 3 + failureThreshold: 3 diff --git a/k8s/img/img.png b/k8s/img/img.png new file mode 100644 index 0000000000..7d7704eb66 Binary files /dev/null and b/k8s/img/img.png differ diff --git a/k8s/img/img0.png b/k8s/img/img0.png new file mode 100644 index 0000000000..ef54d2fe7b Binary files /dev/null and b/k8s/img/img0.png differ diff --git a/k8s/img/img1.png b/k8s/img/img1.png new file mode 100644 index 0000000000..fb41b74b57 Binary files /dev/null and b/k8s/img/img1.png differ diff --git a/k8s/img/img2.png b/k8s/img/img2.png new file mode 100644 index 0000000000..f55643b76a Binary files /dev/null and b/k8s/img/img2.png differ diff --git a/k8s/img/img3.png b/k8s/img/img3.png new file mode 100644 index 0000000000..0c0b0769b0 Binary files /dev/null and b/k8s/img/img3.png differ diff --git a/k8s/img/img5.png b/k8s/img/img5.png new file mode 100644 index 0000000000..aa332773f3 Binary files /dev/null and b/k8s/img/img5.png differ diff --git a/k8s/img/img6.png b/k8s/img/img6.png new file mode 100644 index 0000000000..f55643b76a Binary files /dev/null and b/k8s/img/img6.png differ diff --git a/k8s/img/img7.png b/k8s/img/img7.png new file mode 100644 index 0000000000..7ac09ec6e2 Binary files /dev/null and b/k8s/img/img7.png differ diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000000..158bb34159 --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: devops-info-service +spec: + type: NodePort + + selector: + app: devops-info + + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + nodePort: 30007