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
8 changes: 8 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# DevOps Info Service

## Prerequisites
- Python 3.11+

## Installation
```bash
pip install -r requirements.txt
85 changes: 85 additions & 0 deletions app_python/app — копия.py
Original file line number Diff line number Diff line change
@@ -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)
85 changes: 85 additions & 0 deletions app_python/app.py
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 33 additions & 0 deletions app_python/docs/LAB01.md
Original file line number Diff line number Diff line change
@@ -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
127 changes: 127 additions & 0 deletions app_python/docs/LAB02.md
Original file line number Diff line number Diff line change
@@ -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.

#
Binary file added app_python/docs/screenshots/01-main-endpoint.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app_python/docs/screenshots/02-health-check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app_python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask==3.0.3
Empty file added app_python/tests/__init__.py
Empty file.