diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..bd2ea48c64 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,8 @@ +# DevOps Info Service + +## Prerequisites +- Python 3.11+ + +## Installation +```bash +pip install -r requirements.txt diff --git "a/app_python/app \342\200\224 \320\272\320\276\320\277\320\270\321\217.py" "b/app_python/app \342\200\224 \320\272\320\276\320\277\320\270\321\217.py" new file mode 100644 index 0000000000..65f956bee7 --- /dev/null +++ "b/app_python/app \342\200\224 \320\272\320\276\320\277\320\270\321\217.py" @@ -0,0 +1,85 @@ +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)) +START_TIME = datetime.now(timezone.utc) + +def get_system_info(): + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'platform_version': platform.release(), + 'architecture': platform.machine(), + 'cpu_count': os.cpu_count(), + 'python_version': platform.python_version() + } + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours, remainder = divmod(seconds, 3600) + minutes, _ = divmod(remainder, 60) + return { + 'seconds': seconds, + 'human': f"{hours} hour{'s' if hours != 1 else ''}, {minutes} minute{'s' if minutes != 1 else ''}" + } + +@app.route('/', methods=['GET']) +def index(): + logger.info(f"Request: {request.method} {request.path} from {request.remote_addr}") + uptime = get_uptime() + return jsonify({ + "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 or 'unknown', + "user_agent": request.headers.get('User-Agent', 'unknown'), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + }) + +@app.route('/health', methods=['GET']) +def health(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_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=False) diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..65f956bee7 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,85 @@ +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)) +START_TIME = datetime.now(timezone.utc) + +def get_system_info(): + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'platform_version': platform.release(), + 'architecture': platform.machine(), + 'cpu_count': os.cpu_count(), + 'python_version': platform.python_version() + } + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours, remainder = divmod(seconds, 3600) + minutes, _ = divmod(remainder, 60) + return { + 'seconds': seconds, + 'human': f"{hours} hour{'s' if hours != 1 else ''}, {minutes} minute{'s' if minutes != 1 else ''}" + } + +@app.route('/', methods=['GET']) +def index(): + logger.info(f"Request: {request.method} {request.path} from {request.remote_addr}") + uptime = get_uptime() + return jsonify({ + "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 or 'unknown', + "user_agent": request.headers.get('User-Agent', 'unknown'), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + }) + +@app.route('/health', methods=['GET']) +def health(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_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=False) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..05e96ad286 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,33 @@ +# LAB01 - DevOps Info Service + +## Framework Selection +Flask 3.0.3 - lightweight framework suitable for simple APIs and microservices. + +## Best Practices Applied +1. Structured logging with timestamps +2. Error handling for 404/500 responses +3. Environment variables for configuration +4. PEP8 compliant code organization + +## API Documentation +GET / - Service and system information +GET /health - Health check endpoint + +text + +## Testing Evidence +![Main endpoint](screenshots/01-main-endpoint.png) +![Health check](screenshots/02-health-check.png) +![Terminal output](screenshots/03-formatted-output.png) + +## GitHub Community Engagement +- Starred: inno-devops-labs/DevOps-Core-Course +- Starred: simple-container-com/api +- Following: Cre-eD, marat-biriushev, pierrepicaud +- Following 3 classmates + +Stars increase project visibility. Following helps track best practices. + +## Challenges & Solutions +- Windows venv activation via direct python.exe path +- Client IP shows 127.0.0.1 for localhost correctly \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..47ac8bb8fa --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,127 @@ +## 1. Docker Best Practices Applied + +### Non-root user +I used a non-root user inside the container to reduce security risks. If the application is compromised it will not have root privileges inside the container. + +### Specific base image +I chose `python:3.13-slim` because it's the official python image with minimal size it makes the container smaller and faster to download. + +### Layer caching +I copied `requirements.txt` before the application code. This allows Docker to cache the dependencies layerr so when I change only my code, Docker doesn't need to reinstall dependencies. + +### .dockerignore file +This file prevents unnecessary files from being copied into the Docker image, which makes builds faster. + +## 2. Image Information & Decisions + +### Base image choice +**Image**: `python:3.13-slim` +**Why**: This is the official Python image that includes only essential packages. The slim version is much smaller than the full Python image. + +### Final image size +REPOSITORY TAG IMAGE ID CREATED SIZE +nadiaa02/lab02-python-app latest b232497fb2bb 20 minutes ago 184MB + +text + +### Layer order importance +The order matters for Docker caching. If I copy all files first and then install dependencies, every code change would cause Docker to reinstall all dependencies, which takes much longer. + +## 3. Build & Run Process + +### Docker build output +[+] Building 38.9s (12/12) FINISHED +=> [internal] load build definition from Dockerfile +=> => transferring dockerfile: 348B +=> [internal] load metadata for docker.io/library/python:3.13-slim +=> [1/7] FROM docker.io/library/python:3.13-slim@sha256:49b618b8afc2742b94fa8419d8f4d3b337f111a0527d417a1db97d4683cb71a6 +=> [2/7] RUN useradd -m appuser +=> [3/7] WORKDIR /app +=> [4/7] COPY requirements.txt . +=> [5/7] RUN pip install --no-cache-dir -r requirements.txt +=> [6/7] COPY . . +=> [7/7] RUN chown -R appuser:appuser /app +=> exporting to image +=> => naming to docker.io/library/nadia-lab02-app:latest +Successfully built b232497fb2bb +Successfully tagged nadia-lab02-app:latest + +text + +### Docker run output +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +bb4d98bd9722 nadia-lab02-app "python app.py" 12 seconds ago Up 12 seconds 0.0.0.0:5000->5000/tcp my-app + +text + +### Application testing +{ +"endpoints": [ +{ +"description": "Service information", +"method": "GET", +"path": "/" +}, +{ +"description": "Health check", +"method": "GET", +"path": "/health" +} +], +"request": { +"client_ip": "172.17.0.1", +"method": "GET", +"path": "/", +"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/25.12.0.0 Safari/537.36" +}, +"runtime": { +"current_time": "2026-02-05T10:35:17.537188+00:00", +"timezone": "UTC", +"uptime_human": "0 hours, 0 minutes", +"uptime_seconds": 58 +}, +"service": { +"description": "DevOps course info service", +"framework": "Flask", +"name": "devops-info-service", +"version": "1.0.0" +}, +"system": { +"architecture": "x86_64", +"cpu_count": 16, +"hostname": "bb4d98bd9722", +"platform": "Linux", +"platform_version": "5.15.167.4-microsoft-standard-WSL2", +"python_version": "3.13.12" +} +} + +text + +### Docker Hub repository +https://hub.docker.com/r/nadiaa02/lab02-python-app + +## 4. Technical Analysis + +### What happens if layer order changes? +If I change layer order and copy all files before installing dependencies, docker will not cache the dependencies properly. Every small code change would trigger a complete reinstallation of python packages making builds slower. + +### Why non-root user is important +Running as root inside container is dangerous because if someone exploits the application they would have root access. Using a non-root user limits potential damage. + +### How .dockerignore improves builds +The .dockerignore file tells Docker which files to skip when building the image. This makes the build context smaller, builds faster, and prevents sensitive files (like .env) from accidentally being included. + +## 5. Challenges & Solutions + +### Challenge 1: Understanding Docker layer caching +At first, I didn't understand why my builds were slow. I realized I was copying all files before installing dependencies. + +**Solution**: I reordered the Dockerfile to copy `requirements.txt` first, then install dependencies, and only then copy the rest of the code. + +### Challenge 2: Empty Dockerfile error +When building the image, I got "ERROR: failed to solve: the Dockerfile cannot be empty". + +**Solution**: I checked the Dockerfile and found it was empty. I recreated it with proper content using PowerShell's Out-File command. + +# \ No newline at end of file diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..9d7e876906 Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..550fdf19fa Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..5d441626c1 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..95fef4eb66 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.3 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2