diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..6db8c08313 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,86 @@ +name: Python CI - DevOps Info Service + +on: + push: + branches: [ "**" ] + tags: + - "v*.*.*" + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + + pull_request: + branches: [ "main" ] + paths: + - "app_python/**" + +jobs: + test: + name: Lint & Test + runs-on: ubuntu-latest + + defaults: + run: + working-directory: app_python + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: | + app_python/requirements.txt + app_python/requirements-dev.txt + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run linter (ruff) + run: | + pip install ruff + ruff check . + + - name: Run tests with coverage + run: | + pytest --cov=. --cov-report=term-missing --cov-fail-under=60 + + - name: Install Bandit + run: pip install bandit + + - name: Run Bandit security scan + run: bandit -r . -ll -s B101,B104 + + docker: + name: Build & Push Docker Image + needs: test + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract version + id: vars + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: ./app_python + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ steps.vars.outputs.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest diff --git a/README.md b/README.md index 371d51f456..c1d5964260 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +![CI](https://github.com/essence-666/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + + # DevOps Engineering: Core Practices [![Labs](https://img.shields.io/badge/Labs-18-blue)](#labs) diff --git a/app_go/.gitignore b/app_go/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..a5a3eb130a --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,24 @@ +# -------- Build stage -------- +FROM golang:1.25-alpine AS builder + +WORKDIR /app +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app + +# -------- Runtime stage -------- +FROM alpine:latest + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +WORKDIR /app +COPY --from=builder /app/app . + +RUN chown appuser:appgroup /app/app + +USER appuser + +EXPOSE 5000 +CMD ["./app"] diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..4b112918e9 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,58 @@ +# DevOps Info Service (Go) + +## Overview + +DevOps Info Service is a Go-based web application that provides detailed +information about the service itself, system environment, runtime status, and +incoming HTTP requests. + +The application is implemented in a single source file for simplicity. + +--- + +## Prerequisites + +- Go 1.22+ +- Docker (optional) + +--- + +## Running Locally + +```bash +go run main.go +```` + +### Custom Configuration + +```bash +PORT=8080 go run main.go +HOST=127.0.0.1 PORT=3000 go run main.go +``` + +--- + +## API Endpoints + +| Method | Path | Description | +| ------ | ------- | ------------------------------ | +| GET | / | Service and system information | +| GET | /health | Health check | + +--- + +## Configuration + +| Variable | Default | Description | +| -------- | ------- | ---------------- | +| HOST | 0.0.0.0 | Bind address | +| PORT | 5000 | Application port | + +--- + +## Docker Build (Multi-Stage) + +```bash +docker build -t devops-info-go . +docker run -p 5000:5000 devops-info-go +``` diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..46c89f33f2 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,180 @@ +# Lab 01 – DevOps Info Service (Go) + +## Framework Selection + +### Chosen Language and Framework: Go (net/http) + +The Go programming language with the standard `net/http` package was chosen +for this laboratory work. Go is widely used in DevOps and cloud-native +environments due to its performance, static compilation, simplicity, and +excellent support for concurrent workloads. + +Using the standard library avoids unnecessary dependencies and keeps the +service lightweight and predictable. + +### Comparison with Alternatives + +| Option | Pros | Cons | Reason Not Chosen | +|------|------|------|-------------------| +| Go (net/http) | Fast, static binary, no dependencies | Less abstraction | Chosen | +| Gin | Simple routing, middleware | External dependency | Not required | +| Echo | High performance | External dependency | Overkill | +| Python (FastAPI) | Rapid development, async | Interpreted, slower startup | Language diversity | + +--- + +## Best Practices Applied + +### 1. Minimal Dependency Usage + +Only the Go standard library is used. + +**Example:** +```go +import "net/http" +```` + +**Importance:** +Reduces attack surface, simplifies builds, and improves reliability. + +--- + +### 2. Single Responsibility Structure + +Although the application is implemented in a single file, logical separation +is maintained through clearly defined functions. + +**Example:** + +```go +func healthHandler(w http.ResponseWriter, r *http.Request) +``` + +**Importance:** +Keeps the code readable while remaining suitable for small services. + +--- + +### 3. Environment-Based Configuration + +Runtime configuration is handled via environment variables. + +**Example:** + +```go +port := os.Getenv("PORT") +``` + +**Importance:** +Allows flexible deployment across environments without code changes. + +--- + +### 4. Structured JSON Responses + +All responses are returned in structured JSON format. + +**Example:** + +```go +json.NewEncoder(w).Encode(response) +``` + +**Importance:** +Ensures API consistency and ease of integration. + +--- + +### 5. Health Check Endpoint + +A dedicated health check endpoint is implemented. + +**Example:** + +```go +http.HandleFunc("/health", healthHandler) +``` + +**Importance:** +Required for monitoring systems and container orchestration platforms. + +--- + +## API Documentation + +### GET / + +**Description:** +Returns service metadata, system information, runtime statistics, and request +details. + +**Example Response:** + +```json +{ + "service": { + "name": "devops-info-service", + "language": "go" + } +} +``` + +--- + +### GET /health + +**Description:** +Returns application health status. + +**Example Response:** + +```json +{ + "status": "healthy" +} +``` + +--- + +### Testing Commands + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +``` + +--- + +## Testing Evidence + +The following screenshots are provided: + +* Main endpoint (`/`) showing full JSON response +* Health check endpoint (`/health`) +* Pretty-printed JSON output in terminal + +--- + +## Challenges & Solutions + +### Problem 1: Balancing simplicity and structure + +**Issue:** +Splitting the application into multiple files was unnecessary for the scope of +the laboratory work. + +**Solution:** +Implemented all logic in a single file while preserving logical separation via +functions. + +--- + +### Problem 2: Container image size optimization + +**Issue:** +Default Go images produce relatively large containers. + +**Solution:** +Implemented a multi-stage Docker build to produce a minimal runtime image. + +```` diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..da39b9af3c --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,234 @@ +# LAB02 — Multi-Stage Docker Build (Go Application) + +## Overview + +This task demonstrates containerization of a compiled Go application using **multi-stage Docker builds**. +The goal is to separate the build environment from the runtime environment in order to reduce image size, improve security, and follow production best practices. + +--- + +## Multi-Stage Build Strategy + +The Dockerfile uses **two stages**: + +1. **Builder stage** — compiles the Go application +2. **Runtime stage** — runs only the compiled binary + +This approach ensures that the final image does **not** include compilers, SDKs, or build tools. + +--- + +## Dockerfile + +```dockerfile +# -------- Build stage -------- +FROM golang:1.25-alpine AS builder + +WORKDIR /app +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app + +# -------- Runtime stage -------- +FROM alpine:latest + +WORKDIR /app +COPY --from=builder /app/app . + +EXPOSE 5000 +CMD ["./app"] +``` + +--- + +## Stage-by-Stage Explanation + +### Build Stage (`builder`) + +```dockerfile +FROM golang:1.25-alpine AS builder +``` + +* Uses the official Go SDK image +* Required for compiling the application +* Includes Go compiler and build tools (large image) + +```dockerfile +COPY go.mod ./ +RUN go mod download +``` + +* Copies `go.mod` separately +* Allows Docker layer caching +* Dependencies are not re-downloaded unless `go.mod` changes + +```dockerfile +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app +``` + +* Produces a **static binary** +* Ensures compatibility with minimal runtime images +* No libc or system dependencies required + +--- + +### Runtime Stage + +```dockerfile +FROM alpine:latest +``` + +* Minimal Linux distribution +* Very small image size +* No compilers or SDKs included + +```dockerfile +COPY --from=builder /app/app . +``` + +* Copies **only the compiled binary** +* No source code or build artifacts included + +--- + +## Image Size Comparison + +```bash +docker build --target builder -t go-with-multi-stage . +docker build --target builder -t go-without-multi-stage . +``` + +| Image | Size | +| --------------------------------------- | ---------------- | +| Builder image (`golang:1.25-alpine`) | ~309 MB | +| Final runtime image (`alpine + binary`) | ~25.3 MB | + +![Comparison](./screenshots/compare_images.png) + +## Docker Hub + +The image was published to Docker Hub and is publicly accessible. + +**Repository:** + +``` +https://hub.docker.com/repository/docker/essence666/app_golang_lab_2/general +``` + +### Analysis + +* Image size reduced by **more than 95%** +* Smaller images: + + * Pull faster + * Use less disk space + * Reduce attack surface + +--- + +## Why Multi-Stage Builds Matter for Compiled Languages + +Without multi-stage builds: + +* Final image would include: + + * Go compiler + * Package manager + * Build cache +* Image size would be unnecessarily large +* Increased security risks + +With multi-stage builds: + +* Build tools are discarded +* Only the runtime artifact is shipped +* Clear separation of responsibilities + +--- + +## Security Considerations + +* Final image does **not** include: + + * Compiler + * Package manager + * Source code +* Smaller attack surface +* Fewer CVEs +* Easier vulnerability scanning + +--- + +## Why Not Use the Builder Image as Final? + +* Builder image is designed for development, not production +* Contains unnecessary tools +* Much larger size +* Increased attack surface + +--- + +## Can `FROM scratch` Be Used? + +In theory, yes — because the binary is statically compiled. + +However, `alpine` was chosen because: + +* Easier debugging +* Provides basic utilities +* Better balance between minimalism and usability + +--- + +## Build & Run Process + +### Build Image + +```bash +docker build -t go-info-service . +``` + +### Run Container + +```bash +docker run -p 5000:5000 go-info-service +``` + +### Test Application + +```bash +curl http://localhost:5000/health +``` + +--- + +## Challenges & Solutions + +### Challenge: Reducing final image size + +**Solution:** +Used multi-stage build and static compilation (`CGO_ENABLED=0`) to eliminate runtime dependencies. + +### Challenge: Dependency caching + +**Solution:** +Separated `go.mod` copy step to improve Docker layer caching. + +--- + +## What I Learned + +* How multi-stage builds dramatically reduce image size +* Why compiled languages benefit most from this approach +* How static compilation enables minimal runtime images +* How smaller images improve security and deployment speed + +--- + +## Conclusion + +Multi-stage Docker builds are essential for containerizing compiled applications in production. +They provide significant benefits in terms of **image size, security, and maintainability**, making them the recommended approach for Go, Rust, and similar languages. + diff --git a/app_go/docs/screenshots/01-main-endpoint.png b/app_go/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..ca291bf055 Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-check.png b/app_go/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..badf6f8d1c Binary files /dev/null and b/app_go/docs/screenshots/02-health-check.png differ diff --git a/app_go/docs/screenshots/03-formatted-output.png b/app_go/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..cd27b4ffac Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/docs/screenshots/compare_images.png b/app_go/docs/screenshots/compare_images.png new file mode 100644 index 0000000000..871bd926ab Binary files /dev/null and b/app_go/docs/screenshots/compare_images.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..c23731c7fa --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module api + +go 1.25.0 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..55a0f8316c --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "encoding/json" + "net/http" + "os" + "runtime" + "time" + "fmt" +) + +var startTime = time.Now() + +// Structs for JSON response +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Language string `json:"language"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type RuntimeInfo struct { + UptimeSeconds int64 `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type RequestInfo struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type MainResponse struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime RuntimeInfo `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int64 `json:"uptime_seconds"` +} + +// Helpers +func humanDuration(d time.Duration) string { + h := int(d.Hours()) + m := int(d.Minutes()) % 60 + s := int(d.Seconds()) % 60 + return fmt.Sprintf("%d hour(s), %d minute(s), %d second(s)", h, m, s) +} + +// Handlers +func rootHandler(w http.ResponseWriter, r *http.Request) { + now := time.Now() + uptime := now.Sub(startTime) + + resp := MainResponse{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Language: "Go", + }, + System: System{ + Hostname: getHostname(), + Platform: runtime.GOOS, + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: RuntimeInfo{ + UptimeSeconds: int64(uptime.Seconds()), + UptimeHuman: humanDuration(uptime), + CurrentTime: now.UTC().Format(time.RFC3339), + Timezone: "UTC", + }, + Request: RequestInfo{ + ClientIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + now := time.Now() + uptime := now.Sub(startTime) + + resp := HealthResponse{ + Status: "healthy", + Timestamp: now.UTC().Format(time.RFC3339), + UptimeSeconds: int64(uptime.Seconds()), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +// Utility +func getHostname() string { + name, err := os.Hostname() + if err != nil { + return "unknown" + } + return name +} + +// Main +func main() { + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + + http.HandleFunc("/", rootHandler) + http.HandleFunc("/health", healthHandler) + + fmt.Printf("Starting server at %s:%s...\n", host, port) + err := http.ListenAndServe(host+":"+port, nil) + if err != nil { + fmt.Printf("Server error: %v\n", err) + } +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..c592d7c77c --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,4 @@ +venv/ +__pycache__ +*.pyc +tests/ diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..d6d38fcdd6 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,15 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +.pytest_cache/ +.coverage diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..60ac7e68c3 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN addgroup --system appgroup \ + && adduser --system --ingroup appgroup appuser + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY core ./core +COPY api ./api +COPY services ./services +COPY app.py . + +RUN chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 5000 + +CMD ["python3", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..5325456cdc --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,124 @@ +# DevOps Info Service (FastAPI) + +## Overview + +DevOps Info Service is a FastAPI-based web application that provides detailed +information about the service itself, system environment, runtime status, and +incoming HTTP requests. + +--- + +## Prerequisites + +- Python 3.11+ +- pip + +--- + +## Installation + +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +```` + +--- + +## Running the Application + +```bash +python app.py +``` + +### Custom Configuration + +```bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +--- + +## API Endpoints + +| Method | Path | Description | +| ------ | ------- | ------------------------------ | +| GET | / | Service and system information | +| GET | /health | Health check | + +--- + +## Configuration + +| Variable | Default | Description | +| -------- | ------- | ---------------- | +| HOST | 0.0.0.0 | Bind address | +| PORT | 5000 | Application port | +| DEBUG | False | Debug mode | + +``` + + +Окей, добавляем ровно то, **что от тебя хотят по лабе**, без воды и с объяснениями. Ниже — **готовый Docker-раздел**, который ты просто **вставляешь в `README.md`** (обычно после `Running the Application`). + +--- + +## Docker + +This application can be run inside a Docker container. +The Docker image is built following Docker best practices: minimal base image, non-root user, optimized layer caching, and a clean build context. + +### Dockerfile Overview + +The Dockerfile is designed for production usage and includes the following decisions: + +* **Base image**: `python:3.13-slim` + Chosen for a balance between small image size and good compatibility with Python packages. + +* **Non-root user**: + The application runs as a dedicated non-root user to reduce security risks. + +* **Optimized layer caching**: + Dependencies are installed before copying application code, allowing Docker to reuse cached layers when only source code changes. + +* **Minimal file copy**: + Only required source files are copied into the image to keep it small and clean. + +* **`.dockerignore` usage**: + Excludes development artifacts, virtual environments, VCS files, and caches to reduce build context size and improve build performance. + +--- + +### Build the Docker Image + +```bash +docker build -t devops-info-service . +``` + +--- + +### Run the Container + +```bash +docker run -p 8000:8000 devops-info-service +``` + +The application will be available at: + +``` +http://localhost:8000 +``` + +--- + +### Environment Variables in Docker + +You can override configuration values using environment variables: + +```bash +docker run -p 8000:8000 \ + -e HOST=0.0.0.0 \ + -e PORT=8000 \ + devops-info-service +``` diff --git a/app_python/api/__init__.py b/app_python/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/api/routes/__init__.py b/app_python/api/routes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/api/routes/health.py b/app_python/api/routes/health.py new file mode 100644 index 0000000000..5800486d2c --- /dev/null +++ b/app_python/api/routes/health.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter +from datetime import datetime, timezone +from services.runtime import get_uptime + +router = APIRouter() + +@router.get("/health") +async def health(): + uptime_seconds, _ = get_uptime() + + return { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime_seconds, + } diff --git a/app_python/api/routes/root.py b/app_python/api/routes/root.py new file mode 100644 index 0000000000..d15a63c17a --- /dev/null +++ b/app_python/api/routes/root.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Request +from datetime import datetime, timezone + +from core.config import ( + SERVICE_NAME, + SERVICE_VERSION, + SERVICE_DESCRIPTION, + FRAMEWORK, +) +from services.system import get_system_info +from services.runtime import get_uptime + +router = APIRouter() + +@router.get("/") +async def root(request: Request): + uptime_seconds, uptime_human = get_uptime() + + return { + "service": { + "name": SERVICE_NAME, + "version": SERVICE_VERSION, + "description": SERVICE_DESCRIPTION, + "framework": FRAMEWORK, + }, + "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.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent"), + "method": request.method, + "path": request.url.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..46e591acce --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,36 @@ +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from core.config import SERVICE_NAME, SERVICE_VERSION, SERVICE_DESCRIPTION, HOST, PORT +from core.logging import setup_logging +from api.routes import root, health + +logger = setup_logging() + +app = FastAPI( + title=SERVICE_NAME, + version=SERVICE_VERSION, + description=SERVICE_DESCRIPTION, +) + +app.include_router(root.router) +app.include_router(health.router) + +@app.exception_handler(404) +async def not_found(request: Request, exc): + 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): + logger.exception("Unhandled exception") + return JSONResponse( + status_code=500, + content={"error": "Internal Server Error", "message": "An unexpected error occurred"}, + ) + +if __name__ == "__main__": + import uvicorn + uvicorn.run("app:app", host=HOST, port=PORT) diff --git a/app_python/core/__init__.py b/app_python/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/core/config.py b/app_python/core/config.py new file mode 100644 index 0000000000..314a683bce --- /dev/null +++ b/app_python/core/config.py @@ -0,0 +1,10 @@ +import os + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +SERVICE_NAME = "devops-info-service" +SERVICE_VERSION = "1.0.0" +SERVICE_DESCRIPTION = "DevOps course info service" +FRAMEWORK = "FastAPI" diff --git a/app_python/core/logging.py b/app_python/core/logging.py new file mode 100644 index 0000000000..444333d6c7 --- /dev/null +++ b/app_python/core/logging.py @@ -0,0 +1,9 @@ +import logging +from core.config import DEBUG + +def setup_logging(): + logging.basicConfig( + level=logging.DEBUG if DEBUG else logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + return logging.getLogger("devops-info-service") diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..0dad9a7af9 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,195 @@ +принял, делаем **сухо, академично, без воды**, чтобы ассистенту было **нечего резать**. +Ниже **чистый `.md` код**, копипастой. + +--- + +## 📄 `app_python/docs/LAB01.md` + +````md +# Lab 01 – DevOps Info Service + +## Framework Selection + +### Chosen Framework: FastAPI + +FastAPI was selected for this laboratory work due to its high performance, +native support for ASGI, automatic OpenAPI documentation, and clean modular +architecture support. These features align well with DevOps requirements such +as observability, scalability, and maintainability. + +FastAPI also allows easy extension of the application in future labs without +rewriting core components. + +### Comparison with Alternatives + +| Framework | Pros | Cons | Reason Not Chosen | +|---------|------|------|-------------------| +| FastAPI | High performance, async support, OpenAPI, type hints | Slightly higher learning curve | Chosen | +| Flask | Simple, lightweight | No async by default, manual validation | Limited scalability | +| Django | Full-featured, ORM, admin panel | Heavyweight, overkill for microservice | Excess complexity | + +--- + +## Best Practices Applied + +### 1. Modular Project Structure + +The application is split into logical modules: +- `core` – configuration and logging +- `api` – HTTP routes +- `services` – business logic + +**Example:** +```python +app.include_router(root.router) +app.include_router(health.router) +```` + +**Importance:** +Improves readability, scalability, and maintainability of the codebase. + +--- + +### 2. Environment-Based Configuration + +Configuration values are read from environment variables. + +**Example:** + +```python +PORT = int(os.getenv("PORT", 5000)) +``` + +**Importance:** +Allows easy configuration changes without modifying source code, following +the Twelve-Factor App methodology. + +--- + +### 3. Centralized Logging + +A unified logging setup is used across the application. + +**Example:** + +```python +logger = setup_logging() +``` + +**Importance:** +Simplifies debugging and is essential for production monitoring. + +--- + +### 4. Health Check Endpoint + +A dedicated `/health` endpoint is implemented. + +**Example:** + +```python +@router.get("/health") +async def health_check(): + return {"status": "healthy"} +``` + +**Importance:** +Required for container orchestration systems and service monitoring. + +--- + +### 5. Explicit Error Handling + +Custom handlers for 404 and 500 errors are defined. + +**Example:** + +```python +@app.exception_handler(404) +async def not_found(request: Request, exc): +``` + +**Importance:** +Provides consistent error responses and improves API reliability. + +--- + +## API Documentation + +### GET / + +**Description:** +Returns service metadata, system information, runtime statistics, and request +details. + +**Example Response:** + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0" + } +} +``` + +--- + +### GET /health + +**Description:** +Returns application health status. + +**Example Response:** + +```json +{ + "status": "healthy", + "uptime_seconds": 120 +} +``` + +--- + +### Testing Commands + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +``` + +--- + +## Testing Evidence + +The following screenshots are provided: + +* Main endpoint (`/`) showing full JSON response +* Health check endpoint (`/health`) +* Pretty-printed JSON output in terminal + +--- + +## Challenges & Solutions + +### Problem 1: ASGI application import error + +**Issue:** +Uvicorn could not import the application module when using a modular structure. + +**Solution:** +Corrected the `uvicorn.run()` module reference to match the entrypoint file. + +--- + +### Problem 2: Growing complexity of a single file + +**Issue:** +Maintaining all logic in a single file would not scale for future labs. + +**Solution:** +Refactored the application into multiple modules with clear responsibility +boundaries. + +```` + diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..2561891078 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,236 @@ +# LAB02 — Docker Containerization (Python FastAPI) + +## Overview + +In this lab, the Python FastAPI application from Lab 1 was containerized using Docker following production-ready best practices. The goal was to create a secure, optimized, and reproducible Docker image, publish it to Docker Hub, and document all technical decisions made during the process. + +--- + +## Docker Best Practices Applied + +### Non-root User + +The container runs the application as a non-root user instead of the default `root` user. + +**Why this matters:** + +* Containers are not a full security boundary +* Running as root increases the impact of a potential container breakout +* Follows the principle of least privilege + +```dockerfile +RUN addgroup --system appgroup \ + && adduser --system --ingroup appgroup appuser +... +USER appuser +``` + +--- + +### Specific Base Image Version + +The image uses a pinned base image: + +```dockerfile +FROM python:3.13-slim +``` + +**Why this matters:** + +* Guarantees reproducible builds +* Prevents unexpected breaking changes +* `slim` provides a good balance between size and compatibility + +--- + +### Optimized Layer Caching + +Dependencies are installed before application code is copied: + +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Why this matters:** + +* Docker caches layers +* Dependency installation is reused if only source code changes +* Significantly speeds up rebuilds + +--- + +### Minimal File Copy + +Only required application files are copied into the image: + +```dockerfile +COPY core ./core +COPY api ./api +COPY services ./services +COPY app.py . +``` + +**Why this matters:** + +* Smaller image size +* Reduced attack surface +* No unnecessary development files included + +--- + +### .dockerignore Usage + +A `.dockerignore` file is used to exclude unnecessary files from the build context. + +**Excluded files include:** + +* Python cache files (`__pycache__`, `*.pyc`) +* Virtual environments (`venv`, `.venv`) +* Git repository files +* IDE configuration files + +**Why this matters:** + +* Faster build times +* Smaller build context +* Prevents leaking development artifacts into the image + +--- + +## Image Information & Decisions + +### Base Image Choice + +* **Image:** `python:3.13-slim` +* Chosen for its small size and compatibility with Python dependencies +* Avoids issues commonly found with Alpine-based Python images + +### Final Image Size + +The final image size is relatively small compared to a full Python image and is suitable for production usage. + +Smaller images: + +* Pull faster +* Consume less disk space +* Reduce the number of potential vulnerabilities + +--- + +### Layer Structure Explanation + +1. Base image +2. Environment variables +3. Non-root user creation +4. Dependency installation +5. Application code copy +6. Switch to non-root user +7. Application startup + +This order maximizes cache efficiency and minimizes rebuild time. + +--- + +## Build & Run Process + +### Build Image + +```bash +docker build -t devops-info-service . +``` + +### Run Container + +```bash +docker run -p 8000:8000 devops-info-service +``` + +### Test Endpoints + +```bash +curl http://localhost:8000/health +``` + +Expected response: + +```json +{"status": "ok"} +``` + +--- + +## Docker Hub + +The image was published to Docker Hub and is publicly accessible. + +**Repository:** + +``` +https://hub.docker.com/repository/docker/essence666/app_python_lab_2/general +``` + +--- + +## Technical Analysis + +### Why the Dockerfile Works This Way + +* Dependency layers are cached +* Application runs as non-root +* Minimal runtime environment +* Clear separation between build and runtime concerns + +### What Happens If Layer Order Changes + +If application code is copied before installing dependencies: + +* Any code change invalidates dependency cache +* Dependencies are reinstalled on every build +* Build times increase significantly + +--- + +### Security Considerations + +* Application does not run as root +* Smaller image reduces attack surface +* No development tools included +* Environment variables used for configuration + +--- + +### How .dockerignore Improves the Build + +* Reduces build context size +* Prevents accidental inclusion of sensitive files +* Improves Docker build performance + +--- + +## Challenges & Solutions + +### Challenge: Docker layer cache invalidation + +**Solution:** +Copied `requirements.txt` separately before application code. + +### Challenge: Running as non-root + +**Solution:** +Created a dedicated system user and adjusted file permissions. + +--- + +## What I Learned + +* How Docker layer caching works in practice +* Why running containers as non-root is critical +* How to optimize Docker images for production +* How to document containerization decisions clearly + +--- + +## Conclusion + +This lab demonstrates a production-ready Docker setup for a Python FastAPI application. By applying best practices such as non-root execution, optimized layer caching, and minimal images, the resulting container is secure, efficient, and suitable for real-world deployment. diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..bfb829b744 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,243 @@ +# LAB03 – CI/CD, Testing & Security for FastAPI Application + +## Overview + +This project demonstrates implementation of a CI pipeline for a Python FastAPI application including: + +* Code linting +* Unit testing +* Static security analysis +* Dependency vulnerability scanning +* Docker image build validation + +The goal of this lab is to build a reliable and automated CI workflow following DevOps best practices. + +--- + +# Application Description + +The application is built using **FastAPI** and provides: + +* `GET /` — root endpoint +* `GET /health` — health check endpoint +* Error handling routes + +Testing is performed using: + +* `pytest` +* `fastapi.testclient` + +--- + +# Testing + +Unit tests are located in: + +``` +app_python/tests/ +``` + +Run locally: + +```bash +pytest +``` + +All tests must pass for CI to succeed. + +--- + +# Linting + +We use **Ruff** for linting. + +Run locally: + +```bash +ruff check . +``` + +Linting ensures: + +* PEP8 compliance +* Clean imports +* No unused variables +* No obvious code issues + +CI fails if linting errors are found. + +--- + +# Security Scanning + +## 1️Static Code Analysis – Bandit + +Instead of Snyk, **Bandit** was used. + +### Why not Snyk? + +Snyk requires an API token configured in repository secrets. Due to token validation issues in CI, it was not possible to fully automate Snyk integration. + +To maintain a fully automated pipeline, Bandit was selected. + +### Why Bandit? + +* Open-source +* No authentication required +* Designed for Python +* CI-friendly +* Maintained by OpenStack Security Team + +Bandit scans for: + +* Insecure function usage (`eval`, `exec`) +* Weak cryptography +* Unsafe subprocess calls +* Hardcoded credentials +* Insecure random usage + +Run locally: + +```bash +bandit -r app_python +``` + +CI fails if high severity issues are detected. + +--- + +## Dependency Vulnerability Scanning – pip-audit + +We use: + +```bash +pip-audit +``` + +It checks Python dependencies for known CVEs. + +If vulnerabilities are found, CI fails. + +--- + +# Docker + +The project includes a Dockerfile. + +Build locally: + +```bash +docker build -t fastapi-app . +``` + +Run: + +```bash +docker run -p 5000:5000 fastapi-app +``` + +The application will be available at: + +``` +http://localhost:5000 +``` + +--- + +# CI Pipeline + +GitHub Actions workflow: + +``` +.github/workflows/ci.yml +``` + +## CI Stages + +### Install dependencies + +```bash +pip install -r requirements.txt +pip install -r requirements-dev.txt +``` + +--- + +### Lint + +```bash +ruff check . +``` + +--- + +### Tests + +```bash +pytest +``` + +--- + +### Security Scan + +```bash +bandit -r app_python +pip-audit +``` + +--- + +### Docker Build + +```bash +docker build -t fastapi-app . +``` + +--- + +# CI Guarantees + +The pipeline ensures: + +* Code quality validation +* Test coverage enforcement +* Security issue detection +* Dependency vulnerability control +* Docker image build validation + +If any step fails — the pipeline fails. + +--- + +# Technologies Used + +* Python 3.x +* FastAPI +* Pytest +* Ruff +* Bandit +* pip-audit +* Docker +* GitHub Actions + +--- + +# DevOps Best Practices Applied + +* Automated testing +* Automated linting +* Automated security scanning +* Fail-fast CI strategy +* Reproducible builds +* Infrastructure-as-Code for CI + +--- + +# Conclusion + +This lab demonstrates implementation of a production-like CI pipeline for a Python web application. + +The project integrates testing, linting, security scanning, and container validation to ensure code reliability, security, and maintainability. + +[The docker images with release by tag 1.0.0 1.0.1 etc](https://hub.docker.com/repository/docker/essence666/devops-info-service/general) 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..021c3f56d0 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..98deb9b1f9 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..2986a23806 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..2e2512c7a6 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,6 @@ +pytest +pytest-cov +httpx +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +bandit diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..792449289f --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 diff --git a/app_python/services/__init__.py b/app_python/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/services/runtime.py b/app_python/services/runtime.py new file mode 100644 index 0000000000..8044186267 --- /dev/null +++ b/app_python/services/runtime.py @@ -0,0 +1,11 @@ +from datetime import datetime, timezone + +START_TIME = datetime.now(timezone.utc) + +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, f"{hours} hour(s), {minutes} minute(s)" diff --git a/app_python/services/system.py b/app_python/services/system.py new file mode 100644 index 0000000000..637b36c9dc --- /dev/null +++ b/app_python/services/system.py @@ -0,0 +1,13 @@ +import os +import socket +import platform + +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(), + } diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_errors.py b/app_python/tests/test_errors.py new file mode 100644 index 0000000000..65d1f7aad0 --- /dev/null +++ b/app_python/tests/test_errors.py @@ -0,0 +1,15 @@ +from fastapi.testclient import TestClient +from app import app + +client = TestClient(app) + + +def test_404_handler(): + response = client.get("/non-existing-endpoint") + + assert response.status_code == 404 + + data = response.json() + + assert data["error"] == "Not Found" + assert "message" in data diff --git a/app_python/tests/test_health.py b/app_python/tests/test_health.py new file mode 100644 index 0000000000..a9942d4ff5 --- /dev/null +++ b/app_python/tests/test_health.py @@ -0,0 +1,16 @@ +from fastapi.testclient import TestClient +from app import app + +client = TestClient(app) + + +def test_health_success(): + response = client.get("/health") + + assert response.status_code == 200 + + data = response.json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert isinstance(data["uptime_seconds"], int) diff --git a/app_python/tests/test_root.py b/app_python/tests/test_root.py new file mode 100644 index 0000000000..962f4381d9 --- /dev/null +++ b/app_python/tests/test_root.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient +from app import app + +client = TestClient(app) + + +def test_root_success(): + response = client.get("/") + + assert response.status_code == 200 + + data = response.json() + + # service block + assert "service" in data + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["version"] == "1.0.0" + assert data["service"]["framework"] == "FastAPI" + + # system block + assert "system" in data + assert "hostname" in data["system"] + assert "cpu_count" in data["system"] + + # runtime block + assert "runtime" in data + assert "uptime_seconds" in data["runtime"] + assert isinstance(data["runtime"]["uptime_seconds"], int) + + # request block + assert data["request"]["method"] == "GET" + assert data["request"]["path"] == "/" + + # endpoints list + assert isinstance(data["endpoints"], list) + assert any(e["path"] == "/health" for e in data["endpoints"])