diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..44fa25304b --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,22 @@ +# ๐Ÿ™ Version control +.git +.gitignore + +# ๐Ÿ Python +__pycache__ +*.pyc +*.pyo +venv/ +.venv/ + +# ๐Ÿ” Secrets (NEVER include!) +.env +*.pem +secrets/ + +# ๐Ÿ“ Documentation +*.md +docs/ + +# ๐Ÿงช Tests (if not needed in container) +tests/ \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..60e104ebef --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.13-slim + +WORKDIR /app + +RUN useradd --create-home --shell /bin/bash appuser + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..da3754a546 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,103 @@ +# DevOps Info Service + +## Overview + +Web service that reports system information and health status. Provides API endpoints for service info, hostname, platform, uptime, and request details. + +## Prerequisites + +- Python 3.11+ +- pip + +## Installation + +```bash +python -m venv venv +venv\Scripts\activate +pip install -r requirements.txt +``` + +## Running + +```bash +python app.py +``` + +Default: `http://0.0.0.0:5000` + +**Custom config:** + +```bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## API Endpoints + +### `GET /` + +Returns service and system information. + +```bash +curl http://localhost:5000/ +``` + +### `GET /health` + +Health check endpoint. + +```bash +curl http://localhost:5000/health +``` + +## Configuration + +| Variable | Default | Description | +| -------- | --------- | ------------ | +| `HOST` | `0.0.0.0` | Host address | +| `PORT` | `5000` | Port number | +| `DEBUG` | `False` | Debug mode | + +## Docker + +### Build the image + +```bash +docker build -t roma3213/info_service:1.0 . +``` + +### Run a container + +```bash +docker run -p 5000:5000 roma3213/info_service:1.0 +``` + +With custom port: + +```bash +docker run -p 5000:5000 roma3213/info_service:1.0 +``` + +### Pull from Docker Hub + +```bash +docker pull roma3213/info_service:1.0 +docker run -p 5000:5000 roma3213/info_service:1.0 +``` + +## Project Structure + +``` +app_python/ +โ”œโ”€โ”€ app.py # Main app +โ”œโ”€โ”€ config.py # Config +โ”œโ”€โ”€ routes/ # API routes +โ”œโ”€โ”€ services/ # Business logic +โ”œโ”€โ”€ tests/ +โ”œโ”€โ”€ docs/ # Lab docs, screenshots +โ”œโ”€โ”€ requirements.txt +โ”œโ”€โ”€ Dockerfile # Container image +โ”œโ”€โ”€ .dockerignore +โ”œโ”€โ”€ .gitignore +โ””โ”€โ”€ README.md +``` diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..4f29723835 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,42 @@ +import os +import logging +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import JSONResponse +from routes.system_info import router as system_info_router +from config import HOST, PORT, DEBUG +import uvicorn + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI() + +app.include_router(system_info_router) + +@app.exception_handler(404) +async def not_found(request: Request, exc: HTTPException): + return JSONResponse( + status_code=404, + content={ + "error": "Not Found", + "message": "Endpoint does not exist" + } + ) + +@app.exception_handler(Exception) +async def internal_error(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred" + } + ) + +if __name__ == "__main__": + logger.info(f"Application started on {HOST}:{PORT}") + uvicorn.run(app, host=HOST, port=PORT) + diff --git a/app_python/config.py b/app_python/config.py new file mode 100644 index 0000000000..6cb4e62bcd --- /dev/null +++ b/app_python/config.py @@ -0,0 +1,5 @@ +import os + +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' \ No newline at end of file diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..62bf8c4e7a --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,67 @@ +# Lab 1 Submission + +## Framework Selection + +**FastAPI** - Modern, fast, has auto-docs and async support. + +| Criteria | FastAPI | Flask | Django | +| --------- | --------- | ---------- | ------ | +| Speed | Very Fast | Fast | Medium | +| Auto Docs | Yes | No | No | +| Async | Yes | Limited | Yes | +| Size | Small | Very Small | Large | + +**Why not Flask?** Flask is simpler but FastAPI has better async and auto-docs. + +**Why not Django?** Too big for this simple service. + +## Best Practices Applied + +1. **Code Organization** - Separated into routes/, services/, config.py +2. **Error Handling** - 404 and 500 handlers +3. **Logging** - Basic logging setup +4. **Environment Variables** - Config via env vars +5. **Clear Function Names** - get_system_info(), get_uptime(), etc. + +## API Documentation + +### `GET /` + +Returns service info, system info, runtime, request details, and endpoints list. + +```bash +curl http://localhost:5000/ +``` + +### `GET /health` + +Returns health status and uptime. + +```bash +curl http://localhost:5000/health +``` + +### Error Responses + +```bash +curl http://localhost:5000/something +# {"error": "Not Found", "message": "Endpoint does not exist"} +``` + +## Testing Evidence + +Screenshots in `docs/screenshots/`: + +- `01-main-endpoint.png` +- `02-health-check.png` +- `03-formatted-output.png` + +## Challenges & Solutions + +1. **Function name conflict** โ€” Named the route handler `get_system_info()` which conflicted with the imported function from `services.system_info`. When calling `get_system_info()` inside the route handler, Python was calling the route handler itself instead of the imported function, causing recursion errors. Fixed by importing the entire module as `import services.system_info as system_info_service` and accessing functions via `system_info_service.get_system_info()`. + +2. **Timezone method call error** โ€” Used `timezone.utc.tzname()` without arguments, but `tzname()` method requires a datetime object as parameter. This caused `TypeError: timezone.tzname() takes exactly one argument (0 given)`. Fixed by calling `tzname()` on a datetime object: `datetime.now(timezone.utc).tzname()`. + +## GitHub Community + +\*Starring repositories helps with discovery and bookmarking โ€” it signals project quality to the community and encourages maintainers. Following developers builds professional connections and keeps you informed about relevant projects and industry trends. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..1af6c7542f --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,177 @@ +# Lab 02 โ€” Docker Containerization + +## Docker Best Practices Applied + +### 1. Non-root user + +```dockerfile +RUN useradd --create-home --shell /bin/bash appuser +USER appuser +``` + +**Why:** Running as root means container escape = root on host. Non-root user limits damage if compromised. + +### 2. Specific base image version + +```dockerfile +FROM python:3.13-slim +``` + +**Why:** `python:latest` can change unexpectedly. Pinning `3.13-slim` ensures reproducible builds. Slim is ~150 MB vs ~1 GB for full image. + +### 3. Layer ordering (dependencies before code) + +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +``` + +**Why:** Dependencies change rarely, code changes often. This order caches dependency layer, making rebuilds fast (seconds vs minutes). + +### 4. .dockerignore + +**Why:** Excludes `venv/`, `.git/`, docs from build context. Faster builds, prevents secrets from entering image. + +### 5. --no-cache-dir for pip + +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Why:** No reinstalls in Docker, cache just wastes space. Reduces image size. + +--- + +## Image Information & Decisions + +**Base image:** `python:3.13-slim` โ€” Debian-based minimal Python image. Chosen over Alpine (musl libc compatibility issues). Good balance of size (~150 MB) and compatibility. + +**Final image size:** ~170-200 MB (check with `docker images roma3213/info_service:1.0`) + +**Layer structure:** + +1. Base image (python:3.13-slim) +2. Create user +3. Set working directory +4. Copy requirements.txt +5. Install dependencies (cached separately) +6. Copy application code +7. Switch to non-root user + +--- + +## Build & Run Process + +### Build + +```bash +cd app_python +docker build -t roma3213/info_service:1.0 . +``` + +**Terminal output:** + +``` +[+] Building 31.3s (11/11) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.2s + => => transferring dockerfile: 281B 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 2.0s + => [internal] load .dockerignore 0.1s + => => transferring context: 289B 0.0s + => [1/6] FROM docker.io/library/python:3.13-slim@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f 10.1s + => => resolve docker.io/library/python:3.13-slim@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f 0.1s + => => sha256:03af238a5946948d06e8485bb27b05831c5d13f0b3781a01fe347aaf847c2400 1.29MB / 1.29MB 0.8s + => => sha256:0c8d55a45c0dc58de60579b9cc5b708de9e7957f4591fc7de941b67c7e245da0 29.78MB / 29.78MB 8.8s + => => sha256:f1cadbd7abd229d3d8c50b4aa381724025f6bfe89783a8d2bfd6fa751a75946b 252B / 252B 1.0s + => => sha256:686599c79c8709aa5d9f1abf19c75b1760ae0a0ea0335206fe1db9a8793e09f6 11.80MB / 11.80MB 6.2s + => => extracting sha256:0c8d55a45c0dc58de60579b9cc5b708de9e7957f4591fc7de941b67c7e245da0 0.7s + => => extracting sha256:03af238a5946948d06e8485bb27b05831c5d13f0b3781a01fe347aaf847c2400 0.1s + => => extracting sha256:686599c79c8709aa5d9f1abf19c75b1760ae0a0ea0335206fe1db9a8793e09f6 0.4s + => => extracting sha256:f1cadbd7abd229d3d8c50b4aa381724025f6bfe89783a8d2bfd6fa751a75946b 0.0s + => [internal] load build context 0.1s + => => transferring context: 4.89kB 0.0s + => [2/6] WORKDIR /app 0.3s + => [3/6] RUN useradd --create-home --shell /bin/bash appuser 1.0s + => [4/6] COPY requirements.txt . 0.1s + => [5/6] RUN pip install --no-cache-dir -r requirements.txt 13.1s + => [6/6] COPY . . 0.1s + => exporting to image 3.8s + => => exporting layers 2.4s + => => exporting manifest sha256:b218227291f74e8761d0df79e23c22fae99f0311107901b5e76cb89c72a1a55e 0.1s + => => exporting config sha256:f2ef2715b0f7271a2011dcc48f2375f243776b5a3330bbdfaba7f34b8ebc3b7d 0.1s + => => exporting attestation manifest sha256:9c55cad9161240ac44ab4ca9a11105878271afcfe5245d94f27ef097c0effa65 0.1s + => => exporting manifest list sha256:9fb3c79f5a1e50a7a91bb55d089095581954ffc37b3236163b3b76b037bf8ab5 0.1s + => => naming to docker.io/library/info_service:1.0 0.0s + => => unpacking to docker.io/library/info_service:1.0 0.9s +``` + +### Run + +```bash +docker run -d -p 5000:5000 roma3213/info_service:1.0 +``` + +**Check container status:** +```bash +docker ps +``` + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +91128e9d6039 roma3213/info_service:1.0 "python app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp epic_elbakyan +``` + +### Testing + +```bash +curl http://localhost:5000/ +``` + +**Output:** +```json +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"FastAPI"},"system":{"hostname":"91128e9d6039","platform":"Linux","platform_version":"Linux-5.15.167.4-microsoft-standard-WSL2-x86_64-with-glibc2.41","architecture":"x86_64","cpu_count":12,"python_version":"3.13.12"},"runtime":{"uptime_seconds":186,"uptime_human":"0 hours, 3 minutes","current_time":"2026-02-13T11:01:43.559091+00:00","timezone":"UTC"},"request":{"client_ip":"172.17.0.1","user_agent":"curl/8.8.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} +``` + +```bash +curl http://localhost:5000/health +``` + +**Output:** +```json +{"status":"healthy","timestamp":"2026-02-13T11:02:11.626478+00:00","uptime_seconds":214} +``` + +### Docker Hub + +```bash +docker tag roma3213/info_service:1.0 roma3213/info_service:1.0 +docker login +docker push roma3213/info_service:1.0 +``` + +**Docker Hub repository URL:** https://hub.docker.com/r/roma3213/info_service + +--- + +## Technical Analysis + +**Why layer order works:** Code changes frequently, dependencies don't. By copying requirements first, dependency layer is cached. Changing code only rebuilds from `COPY . .` onwards. + +**If order changed:** `COPY . .` before `pip install` would invalidate cache on every code change โ†’ slow rebuilds. + +**Security:** + +- Non-root user prevents privilege escalation +- Slim image = smaller attack surface +- `.dockerignore` prevents secrets in image + +**How .dockerignore helps:** Excludes `venv/`, `.git/` from build context โ†’ faster builds, smaller context. + +--- + +## Challenges & Solutions + +**Port mapping:** Forgot `-p 5000:5000` flag initially โ†’ app wasn't accessible. Always specify port mapping explicitly. + +**Docker Hub tagging:** Must use full name `roma3213/info_service:1.0`, not just `info_service:1.0`, otherwise Docker looks in official repo. 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..6d0a41efdd 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..88f95b7b67 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..0cca35f0bd 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..9abb353041 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 \ No newline at end of file diff --git a/app_python/routes/system_info.py b/app_python/routes/system_info.py new file mode 100644 index 0000000000..fc26d42cfe --- /dev/null +++ b/app_python/routes/system_info.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Request +import logging +from datetime import datetime, timezone +import services.system_info as system_info_service + +router = APIRouter() + +@router.get("/") +async def get_system_info(request: Request): + return { + "service": system_info_service.get_service_info(), + "system": system_info_service.get_system_info(), + "runtime": system_info_service.get_runtime_info(), + "request": system_info_service.get_request_info(request), + "endpoints": system_info_service.get_endpoints_info() + } + +@router.get("/health") +async def health(): + return { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": system_info_service.get_uptime()['seconds'] + } \ No newline at end of file diff --git a/app_python/services/system_info.py b/app_python/services/system_info.py new file mode 100644 index 0000000000..6465ae545f --- /dev/null +++ b/app_python/services/system_info.py @@ -0,0 +1,68 @@ +from fastapi import Request +import os +import platform +import socket +from datetime import datetime, timezone + +start_time = datetime.now(timezone.utc) + +def get_system_info(): + hostname = socket.gethostname() + platform_name = platform.system() + platform_version = platform.platform() + architecture = platform.machine() + cpu_count = os.cpu_count() + python_version = platform.python_version() + + return { + "hostname": hostname, + "platform": platform_name, + "platform_version": platform_version, + "architecture": architecture, + "cpu_count": cpu_count, + "python_version": python_version + } + +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_runtime_info(): + uptime_info = get_uptime() + + return { + "uptime_seconds": uptime_info['seconds'], + "uptime_human": uptime_info['human'], + 'current_time': datetime.now(timezone.utc).isoformat(), + 'timezone': datetime.now(timezone.utc).tzname() + } + +def get_service_info(): + return { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "FastAPI" + } + +def get_request_info(request: Request): + return { + "client_ip": request.client.host, + "user_agent": request.headers.get('user-agent'), + "method": request.method, + "path": request.url.path + } + +def get_endpoints_info(): + return [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..d4839a6b14 --- /dev/null +++ b/app_python/tests/__init__.py @@ -0,0 +1 @@ +# Tests package