diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..00c956b3db --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,51 @@ +# VCS +.git +.gitignore + +# IDE / editors +.vscode/ +.idea/ +*.swp + +# OS junk +.DS_Store +Thumbs.db + +# Docs (not needed for build/run) +docs/ +*.md +README* +LICENSE* + +# Secrets / env files (NEVER ship) +.env +.env.* + +# Logs / reports +*.log +coverage* +*.out +*.prof +*.trace + +# Go build artifacts +bin/ +dist/ +build/ +out/ +*.exe +*.dll +*.so +*.dylib +*.a +*.o + +# Go test cache / tooling caches +**/*_test.go +.golangci.yml +.golangci-lint* +.gotools/ +.tmp/ +tmp/ + + diff --git a/app_go/.gitignore b/app_go/.gitignore new file mode 100644 index 0000000000..bab2935820 --- /dev/null +++ b/app_go/.gitignore @@ -0,0 +1,8 @@ +# Go build outputs +/devops-info-service +*.exe +*.out + +# Go tooling +/bin/ +/dist/ diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..49c26e775d --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,27 @@ +# ---------- Stage 1: Build ---------- +# Use the full Go toolchain image only for compilation, which will take place in the /app directory +FROM golang:1.25.5-bookworm AS builder +WORKDIR /app + +# Copy and install module metadata to use Docker layer caching (if go.sum appears later, this will speed up rebuilding). +COPY go.mod ./ +RUN go mod download + +# Copy the rest of the source code (excess code is filtered out by .dockerignore) and build the binary +COPY . . +RUN CGO_ENABLED=0 go build -o myapp . + +# ---------- Stage 2: Runtime ---------- +# Define the runtime environment (only necessary for running the binary). +FROM alpine:3.18 + +# Create an unprivileged user (does not run as root). +RUN adduser -D appuser + +# Create an application directory inside the runtime container and copy the binary from the compilation environment into it (assigning the file to the appuser user) +WORKDIR /app +COPY --from=builder --chown=appuser:appuser /app/myapp . +USER appuser + +EXPOSE 5000 +CMD ["./myapp"] \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..d193cba049 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,246 @@ +# DevOps Info Service (Lab 01) — Go version + +Small Go web app for DevOps labs (extra points). + +Provides: +- `GET /` — service/system/runtime/request info + list of endpoints +- `GET /health` — simple health check endpoint (for monitoring / K8s probes) + +Configuration is done via environment variables: `HOST`, `PORT`, `DEBUG`. + +--- + +## Overview + +This service returns diagnostic information about: +- service metadata (name/version/description/framework) +- host system (hostname/platform/arch/CPU/Go version) +- runtime (uptime + current UTC time) +- current request (client IP, user-agent, method, path) +- available API endpoints (kept as a registry in the app and returned sorted) + +--- + +## Prerequisites + +- Go **1.20+** (any modern Go should work) +- No third-party dependencies (standard library only) + +--- + +## Quick Start (go run) +If `go.mod` is missing, you can create it: + +```bash +go mod init devops-info-service +``` +From the directory containing `main.go`: + +Default (0.0.0.0:5000): +```bash +go run . +# or: go run main.go +``` + +Custom port: +```bash +PORT=8080 go run . +``` + +Custom host + port: +```bash +HOST=127.0.0.1 PORT=3000 go run . +``` + +Enable debug-style logging: +```bash +DEBUG=true go run . +``` + +--- + +## Build (Binary) + +1. Initialize modules (if needed): +```bash +go mod init devops-info-service +go mod tidy +``` +2. Build a local binary: +```bash +go build -o devops-info-service . +``` +3. Run the binary: +```bash +./devops-info-service +# or with config: +HOST=127.0.0.1 PORT=3000 DEBUG=true ./devops-info-service +``` +--- + +## API Endpoints + +### `GET /` + +Returns full service + runtime info. + +Example: +```bash +curl -s http://127.0.0.1:5000/ | jq '{service, system, runtime, request, endpoints}' +``` + +Response structure: +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go net/http" + }, + "system": { + "hostname": "SerggAidd", + "platform": "linux", + "platform_version": "6.18.5-arch1-1", + "architecture": "amd64", + "cpu_count": 24, + "go_version": "go1.25.5 X:nodwarf5" + }, + "runtime": { + "uptime_seconds": 3, + "uptime_human": "0 hour, 0 minutes", + "current_time": "2026-01-23T19:08:17Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/8.18.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + { + "method": "GET", + "path": "/", + "description": "Root endpoint: returns service metadata and diagnostic information." + }, + { + "method": "GET", + "path": "/health", + "description": "Health check endpoint for monitoring and Kubernetes probes." + } + ] +} +``` + +> Note: JSON object key ordering is not guaranteed by the HTTP/JSON standard. +> Use `python -m json.tool` or `jq` (like in example) only for pretty printing. + +--- + +### `GET /health` + +Health endpoint for monitoring / Kubernetes probes. + +Example: +```bash +curl -s http://127.0.0.1:5000/health | python -m json.tool +``` + +Response: +```json +{ + "status": "healthy", + "timestamp": "2026-01-23T19:08:22Z", + "uptime_seconds": 8 +} +``` + +Always returns HTTP **200** when the service is running. + +--- + +## Error Handling + +- Unknown routes return JSON 404: + +Example: +```bash +curl -s http://127.0.0.1:5000/does-not-exist | python -m json.tool +``` + +Response: +```json +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` + +- Internal errors return JSON 500 (panic is recovered by middleware): + +Example (can be tested by uncommenting the code block with the corresponding endpoint): +```bash +curl -s http://127.0.0.1:5000/crash | python -m json.tool +``` + +Response: +```json +{ + "error": "Internal Server Error", + "message": "An unexpected error occurred" +} +``` + +--- + +## Logging + +The app logs to **stdout**, which is the recommended approach for Docker/Kubernetes. + +Logged events: +- request method/path/client IP/user-agent +- final HTTP status code and request latency +- recovered panics (500) with a short error message + +--- + +## Configuration + +| Variable | Default | Description | +|---------:|-----------|-------------| +| `HOST` | `0.0.0.0` | Bind address | +| `PORT` | `5000` | Listen port | +| `DEBUG` | `False` | `true` enables more verbose log flags | + +## Docker +Below are the basic commands for building and running an application in a container. + +### Local image build +```bash +cd app_go +docker build -t app-go:1.0 . +``` + +### Starting a container +The container listens on port 5000 internally, so we forward it to the host port (in the case bellow 8080) +```bash +docker run --rm -p 8080:5000 app-go:1.0 +``` +Example of running with variables: +```bash +docker run --rm -p 8080:5000 -e PORT=5000 -e DEBUG=false app-go:1.0 +``` +After that you can check endpoints in browser or in defferent terminal: +```bash +curl http://localhost:8080/ +curl http://localhost:8080/health +``` + +### Docker Hub (pull/run) +The image of this container publlished in Docker Hub. Image can be download with the following method: +```bash +docker pull sergey173/app-go:1.0.0 +docker run --rm -p 8080:5000 sergey173/app-go:1.0.0 +``` +Docker Hub repository URL: https://hub.docker.com/repository/docker/sergey173/app-go \ No newline at end of file diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..ff2519d273 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,28 @@ +# Language justification + +## Why Go +Go was selected as the implementation language because it offers a strong trade-off for a small, container-oriented HTTP JSON service: +- **Fast implementation**: minimal boilerplate, straightforward concurrency model, and a simple standard toolchain. +- **Standard library coverage**: `net/http`, `encoding/json`, `os`, `runtime`, and `time` are sufficient to implement routing, JSON serialization, environment-based configuration, and uptime without external dependencies. +- **Deployment simplicity**: produces a single static-like executable (depending on build settings), integrates cleanly with Docker/Kubernetes, and favors stdout logging by default. +- **Low operational overhead**: fast startup time and modest memory footprint, which is well-suited for health checks and probe endpoints. +- **Portability**: cross-compilation is first-class and enables easy builds for common targets (e.g., Linux/amd64) from a single development environment. + +## Contrast with other compiled languages + +### Go vs Rust +Rust provides stronger compile-time guarantees around memory safety, but typically increases development complexity (ownership model, lifetimes) and build friction for small services. For this lab-scale HTTP JSON service, Go achieves the required functionality faster while remaining reliable and maintainable. + +### Go vs Java +Java commonly implies a JVM/JRE runtime plus build tooling (Maven/Gradle), which increases packaging complexity and container footprint relative to a single Go binary. For a small service intended for probes/monitoring, Go keeps runtime requirements and deployment steps minimal. + +### Go vs C/C++ +C/C++ can produce small binaries, but requires manual memory management and often more complex build configuration. Go reduces the likelihood of memory-related defects and simplifies maintenance while still providing compiled performance and simple distribution. + +## Trade-offs +- Go provides fewer compile-time memory safety guarantees than Rust. +- Implementing routing/middleware without a third-party framework can require more manual code (though the standard library remains sufficient for the scope of this service). + +## Summary +Go was chosen to minimize dependencies and operational complexity while delivering a compact, portable HTTP JSON service suitable for containerized environments. Compared to Rust, Java, and C/C++, Go reduces implementation and deployment overhead for this specific lab task, with trade-offs that are acceptable given the service’s limited scope and reliance on the standard library. + diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..94ef578079 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,294 @@ +# LAB01 — DevOps Info Service (Go) + +This document describes the Go implementation of **DevOps Info Service** for Lab 01. +The service is a small HTTP JSON API that exposes system/runtime/request metadata and a health check endpoint. + +--- + +## Framework Selection + +### Choice: Go standard library (`net/http`) + +This implementation uses only the Go standard library: +- `net/http` — HTTP server +- `encoding/json` — JSON encoding +- `os`, `runtime`, `time` — configuration + system/runtime metadata +- a minimal custom router + middleware for logging and panic recovery + +**Rationale** +- Minimal dependencies (easy to review and reproduce). +- Predictable behavior and portability (single compiled binary). +- Container-friendly defaults (stdout logging, fast startup). + +### Comparison table with alternatives + +| Option | Pros | Cons | Decision | +|---|---|---|---| +| **Standard library (`net/http`)** | Zero deps, small binary, portable | Routing/middleware is manual | **Selected** (fits Lab 01 scope) | +| Gin | Fast, popular, good DX | External dependency, more abstraction | Not required for 2–3 routes | +| Echo | Middleware-rich, ergonomic | External dependency | Not required for Lab 01 | +| chi | Lightweight router | External dependency | Chose zero-deps approach | +| Gorilla/mux | Mature ecosystem | Heavier router, extra dep | Not needed for exact matches | + +--- + +## Best Practices Applied + +Below is a list of practices applied in the implementation, with short code excerpts and the reason each matters. + +### 1) Configuration via environment variables +**Example** +```go +host := os.Getenv("HOST") +if host == "" { host = "0.0.0.0" } + +port := os.Getenv("PORT") +if port == "" { port = "5000" } + +debug := strings.ToLower(os.Getenv("DEBUG")) == "true" +``` + +**Why it matters** +- Matches common container/Kubernetes configuration patterns. +- Allows the same binary to run in different environments without code changes. + +### 2) Consistent JSON errors (404 / 500) +**Examples** +```go +writeJSON(w, http.StatusNotFound, ErrorResponse{ + Error: "Not Found", Message: "Endpoint does not exist", +}) +``` + +```go +defer func() { + if rec := recover(); rec != nil { + writeJSON(w, http.StatusInternalServerError, ErrorResponse{ + Error: "Internal Server Error", + Message: "An unexpected error occurred", + }) + } +}() +``` + +**Why it matters** +- Ensures clients always receive machine-readable error payloads. +- Prevents unexpected crashes from stopping the service. + +### 3) Request logging to stdout +**Example** +```go +log.Printf("Request %s %s from %s UA=%s -> %d (%s)", + r.Method, r.URL.Path, clientIP(r), r.UserAgent(), sw.status, lat) +``` + +**Why it matters** +- Stdout logging is the standard in Docker/Kubernetes. +- Useful for validating health probes and debugging locally. + +### 4) Proxy-aware client IP extraction +**Example** +```go +xff := r.Header.Get("X-Forwarded-For") +if xff != "" { + return strings.TrimSpace(strings.Split(xff, ",")[0]) +} +``` + +**Why it matters** +- Preserves real client IP when the service is behind an ingress/reverse proxy. + +--- + +## API Documentation + +### `GET /` +Returns service metadata, system/runtime details, request metadata, and the list of available endpoints. + +**Request** +```bash +curl -s http://127.0.0.1:5000/ +``` + +**Response** +See *Testing Evidence* below. + +### `GET /health` +Health endpoint for monitoring / Kubernetes probes. Returns HTTP **200** when the service is running. + +**Request** +```bash +curl -s http://127.0.0.1:5000/health +``` + +**Response (example)** +```json +{ + "status": "healthy", + "timestamp": "2026-01-23T19:08:22Z", + "uptime_seconds": 8 +} +``` + +--- + +## Testing Commands + +Run the service: +```bash +HOST=0.0.0.0 PORT=5000 DEBUG=false go run main.go +``` + +Test endpoints: +```bash +curl -s http://127.0.0.1:5000/ +curl -s http://127.0.0.1:5000/health +curl -s http://127.0.0.1:5000/does-not-exist +# You also can use command bellow of uncomment crash endpoint in code +# curl -s http://127.0.0.1:5000/crash +``` + +Pretty-print JSON: +```bash +curl -s http://127.0.0.1:5000/ | python -m json.tool +``` + +--- + +## Testing Evidence + +### Screenshots showing endpoints work + +Required screenshots are stored in `docs/screenshots/`: + +1) **Main endpoint showing complete JSON** +- `docs/screenshots/01_root_complete_json.png` + +![GET / — complete JSON](./screenshots/LAB01/01_root_complete_json.png) + + +2) **Health check response** +- `docs/screenshots/02_health_check.png` + +![GET /health — health probe](./screenshots/LAB01/02_health_check.png) + +3) **Formatted/pretty-printed output** +- `docs/screenshots/03_pretty_print_command.png` + +![Pretty-print example](./screenshots/LAB01/03_pretty_print_command.png) + + +### Terminal output + +```text +$ curl -s http://127.0.0.1:5000/ | python -m json.tool +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go net/http" + }, + "system": { + "hostname": "SerggAidd", + "platform": "linux", + "platform_version": "6.18.5-arch1-1", + "architecture": "amd64", + "cpu_count": 24, + "go_version": "go1.25.5 X:nodwarf5" + }, + "runtime": { + "uptime_seconds": 3, + "uptime_human": "0 hour, 0 minutes", + "current_time": "2026-01-23T19:08:17Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/8.18.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + { + "method": "GET", + "path": "/", + "description": "Root endpoint: returns service metadata and diagnostic information." + }, + { + "method": "GET", + "path": "/health", + "description": "Health check endpoint for monitoring and Kubernetes probes." + } + ] +} +``` + +```text +$ curl -s http://127.0.0.1:5000/health | python -m json.tool +{ + "status": "healthy", + "timestamp": "2026-01-23T19:08:22Z", + "uptime_seconds": 8 +} +``` + +```text +$ curl -s http://127.0.0.1:5000/does-not-exist | python -m json.tool +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` + +```text +$ curl -s http://127.0.0.1:5000/crash | python -m json.tool +{ + "error": "Internal Server Error", + "message": "An unexpected error occurred" +} +``` + +--- + +## Challenges & Solutions + +### 1) Endpoint discovery without a framework +**Problem:** `net/http` does not provide a route registry similar to Flask. +**Solution:** Implemented a minimal router that stores routes and exposes a sorted `endpoints` list for the root response. + +### 2) Correct client IP behind proxies +**Problem:** `RemoteAddr` can reflect only the proxy address. +**Solution:** Prefer `X-Forwarded-For` (first value) and fall back to `RemoteAddr` parsing. + +### 3) Handling internal failures without process exit +**Problem:** A panic would terminate the process by default. +**Solution:** Added panic recovery middleware that converts panics into a JSON 500 response. + +### 4) Readable evidence output +**Problem:** JSON key ordering is not guaranteed by the standard. +**Solution:** Evidence uses pretty-printing tools; the endpoints list is sorted by `(path, method)` for deterministic output. + +--- + +## Compare binary size to Python +To compare the sizes of application binaries, the following commands were executed: + +- Go application (8591356 bytes): +```bash +go mod tidy +go build -o devops-info-service . +stat -c '%n %s bytes' devops-info-service +``` +![Binary size of go app](./screenshots/LAB01/04_go_binary_size.png) + +- Python application (13959512 bytes): +```bash +pip install pyinstaller +pyinstaller --onefile app.py +stat -c '%n %s bytes' dist/app +``` +![Binary size of python app](./screenshots/LAB01/05_python_binary_size.png) + +### Summary +According to measurements, the Go binary (8.19 MiB) is noticeably smaller than a Python onefile via PyInstaller (13.31 MiB) - a difference of about 5.12 MiB (around 38.5%). This is because Go builds a single native executable with runtime and dependencies, while PyInstaller in `--onefile` mode also packages the Python interpreter and a set of libraries, resulting in a larger final artifact. This gives Go an advantage in terms of size and portability for containers and fast deployments. \ No newline at end of file diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..6025f931b5 --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,65 @@ +# LAB02 — Docker Containerization (app_go) + +## Multi-stage build strategy +A multi-stage build strategy was used to separate the process into two independent stages: build and run. The main goal is to obtain a minimal production image containing only what is needed to run the application, without development tools. + +The strategy is based on the following principles: +1. Isolation of the build from the runtime + - The Go application is compiled in a separate "builder" stage, where the compiler, system utilities, and (in the future) module cache are available. + - The final "runtime" stage does not contain the source code, compiler, or downloaded build dependencies. + +2. Minimization of the final image + - Only the finished binary file is transferred to the runtime image, while the entire build environment is automatically discarded and not included in the final image. + - This directly reduces the image size and speeds up pull/push operations. + +3. Security by default + - The container runs as an unprivileged user to reduce the impact of an application compromise. + - Fewer files and tools within the runtime reduce the attack surface. + +4. Ready for scaling and faster rebuilds + - The Dockerfile structure enables efficient dependency caching by separately copying go.mod (and go.sum, when available), so that code changes don't require repeated, cumbersome steps. + +**The result of this strategy:** a large "builder" image is used only as a temporary environment, while the final "runtime" image is lightweight, reproducible, and more secure, which is the core value of a multi-stage approach for compiled languages. + +## Terminal output showing build process and image sizes +- Complete terminal output from build process: ![Complete terminal output from build process](./screenshots/LAB02/01_complit_build.png) +- Terminal output showing container running: ![Terminal output showing container running](./screenshots/LAB02/02_container_running.png) +- Terminal output from testing endpoints (curl/httpie): ![Terminal output from testing endpoints with curl](./screenshots/LAB02/03_endpoint_check.png) +- Building only the builder stage and output the dimensions of both images:![Building only the builder stage and output the dimensions of both images](./screenshots/LAB02/04_size_comparison.png) + +## Size comparison (builder vs final image) +A comparison of Docker image sizes shows that the final `app-go:1.0` runtime image is 15.7 MB, while the `app-go:builder` image is 898 MB. This means the final image is approximately 57.2 times smaller, representing a size reduction of approximately 98.25% (a savings of approximately 882 MB). This is achieved through a multi-stage approach, resulting in a runtime image that does not contain a compiler, source code, or build dependencies, which will contribute to faster pull and push operations and a reduced attack surface. + +## The importance of multi-stage builds for compiled languages +1. Drastically reduces the final image size + - Without multi-stage, you could accidentally "slip" your entire toolchain (hundreds of megabytes or gigabytes) into production. + - Multi-stage leaves only the binary in the final image, meaning less traffic and faster pull, push, and deploy operations. + +2. Reduced attack surface (security) + - The fewer components in a container, the fewer potential vulnerabilities. + - A runtime image does not include a package manager, compiler, or unnecessary utilities that could make it easier for an attacker to establish or develop an attack. + +3. Cleaner and more "production-like" environment + - The final image adheres to the principle of "only what's needed to run." + - This simplifies maintenance, updates, and analysis of what's actually in production. + +4. Better reproducibility and control + - The build is performed in a fixed environment (builder image), and the run is performed in a minimal environment, which reduces the risk of "works on my machine" and makes the build more predictable. + + +## Technical explanation of each stage's purpose: +This Dockerfile is split into two stages — builder and runtime — to separate compilation from execution and keep the final image minimal. + +1. Builder stage (builder): + - Uses the base image `golang:1.25.5-bookworm`, which includes the full Go toolchain required to compile the application. + - Sets `/app` as the working directory. + - Copies only `go.mod` and runs `go mod download`. This is done for layer caching: when dependencies are added later (and `go.sum` appears), Docker can reuse the cached dependency-download layer as long as `go.mod`/`go.sum` remain unchanged. + - Copies the rest of the source code (`COPY . .`) and compiles the project. + - Builds with `CGO_ENABLED=0`, which disables CGO and typically produces a statically linked binary. This is convenient because it reduces runtime dependencies and allows using a smaller runtime image. + - Output of this stage is a single executable binary: `myapp`. +2. Runtime stage (runtime): + - Uses a lightweight base image `alpine:3.18`. + - Creates an unprivileged user appuser, so the container does not run as root (security hardening). + - Sets `/app` as the working directory and copies only the compiled binary from the builder stage using `COPY --from=builder ...`. + - Applies `--chown=appuser:appuser` during copy, so the binary is owned by the unprivileged user without needing an extra chown layer. + - The final image contains no source code, no Go compiler, no module cache, and no build tools - only the minimal OS and the application binary. diff --git a/app_go/docs/screenshots/LAB01/01_root_complete_json.png b/app_go/docs/screenshots/LAB01/01_root_complete_json.png new file mode 100644 index 0000000000..5e2fb64f16 Binary files /dev/null and b/app_go/docs/screenshots/LAB01/01_root_complete_json.png differ diff --git a/app_go/docs/screenshots/LAB01/02_health_check.png b/app_go/docs/screenshots/LAB01/02_health_check.png new file mode 100644 index 0000000000..722e052290 Binary files /dev/null and b/app_go/docs/screenshots/LAB01/02_health_check.png differ diff --git a/app_go/docs/screenshots/LAB01/03_pretty_print_command.png b/app_go/docs/screenshots/LAB01/03_pretty_print_command.png new file mode 100644 index 0000000000..7269db29c5 Binary files /dev/null and b/app_go/docs/screenshots/LAB01/03_pretty_print_command.png differ diff --git a/app_go/docs/screenshots/LAB01/04_go_binary_size.png b/app_go/docs/screenshots/LAB01/04_go_binary_size.png new file mode 100644 index 0000000000..3c05be8cd8 Binary files /dev/null and b/app_go/docs/screenshots/LAB01/04_go_binary_size.png differ diff --git a/app_go/docs/screenshots/LAB01/05_python_binary_size.png b/app_go/docs/screenshots/LAB01/05_python_binary_size.png new file mode 100644 index 0000000000..54feec58b4 Binary files /dev/null and b/app_go/docs/screenshots/LAB01/05_python_binary_size.png differ diff --git a/app_go/docs/screenshots/LAB02/01_complit_build.png b/app_go/docs/screenshots/LAB02/01_complit_build.png new file mode 100644 index 0000000000..754bc37fac Binary files /dev/null and b/app_go/docs/screenshots/LAB02/01_complit_build.png differ diff --git a/app_go/docs/screenshots/LAB02/02_container_running.png b/app_go/docs/screenshots/LAB02/02_container_running.png new file mode 100644 index 0000000000..0e08928d5b Binary files /dev/null and b/app_go/docs/screenshots/LAB02/02_container_running.png differ diff --git a/app_go/docs/screenshots/LAB02/03_endpoint_check.png b/app_go/docs/screenshots/LAB02/03_endpoint_check.png new file mode 100644 index 0000000000..8c85d85d82 Binary files /dev/null and b/app_go/docs/screenshots/LAB02/03_endpoint_check.png differ diff --git a/app_go/docs/screenshots/LAB02/04_size_comparison.png b/app_go/docs/screenshots/LAB02/04_size_comparison.png new file mode 100644 index 0000000000..e879ffd01a Binary files /dev/null and b/app_go/docs/screenshots/LAB02/04_size_comparison.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..4f3ceac16b --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.25.5 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..6b4a4712ed --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,356 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "sort" + "strings" + "time" +) + +// Service describes metadata about the running service. +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +// System contains basic host and runtime details. +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +// RuntimeInfo reports uptime and current timestamp. +type RuntimeInfo struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +// RequestInfo captures request metadata. +type RequestInfo struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +// Endpoint represents a single API route. +type Endpoint struct { + Method string `json:"method"` + Path string `json:"path"` + Description string `json:"description"` +} + +// RootResponse is the response schema for the root endpoint. +type RootResponse struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime RuntimeInfo `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +// HealthResponse is the response schema for /health. +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// ErrorResponse is a JSON error payload for non-200 responses. +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` +} + +// routeKey uniquely identifies a route by method and path. +type routeKey struct { + Method string + Path string +} + +// route binds a handler with route metadata. +type route struct { + Method string + Path string + Description string + Handler http.HandlerFunc +} + +// router is a tiny HTTP router for exact method+path matches. +type router struct { + routes map[routeKey]route + endpoints []Endpoint +} + +// newRouter initializes an empty router instance. +func newRouter() *router { + return &router{ + routes: make(map[routeKey]route), + endpoints: make([]Endpoint, 0), + } +} + +// Handle registers a handler for an exact HTTP method and path. +func (rt *router) Handle(method, path, description string, h http.HandlerFunc) { + key := routeKey{Method: method, Path: path} + rt.routes[key] = route{ + Method: method, + Path: path, + Description: description, + Handler: h, + } + + rt.endpoints = append(rt.endpoints, Endpoint{ + Method: method, + Path: path, + Description: description, + }) +} + +// Endpoints returns a sorted copy of the registered endpoints list. +func (rt *router) Endpoints() []Endpoint { + out := make([]Endpoint, len(rt.endpoints)) + copy(out, rt.endpoints) + sort.Slice(out, func(i, j int) bool { + if out[i].Path == out[j].Path { + return out[i].Method < out[j].Method + } + return out[i].Path < out[j].Path + }) + return out +} + +// ServeHTTP dispatches the request to a registered route or returns a 404 JSON error. +// Note: method mismatch is treated as "not found" in this simplified router. +func (rt *router) ServeHTTP(w http.ResponseWriter, r *http.Request) { + key := routeKey{Method: r.Method, Path: r.URL.Path} + if rr, ok := rt.routes[key]; ok { + rr.Handler(w, r) + return + } + + writeJSON(w, http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "Endpoint does not exist", + }) +} + +var ( + // startTime is captured once at startup and used to compute uptime. + startTime = time.Now().UTC() + + // service contains static service metadata returned by the root endpoint. + service = Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go net/http", + } +) + +// systemInfo collects basic system/runtime information. +func systemInfo() System { + hostname, _ := os.Hostname() + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: linuxKernelRelease(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} + +// runtimeInfo computes uptime and generates a UTC timestamp in ISO 8601 format. +func runtimeInfo() RuntimeInfo { + now := time.Now().UTC() + uptime := now.Sub(startTime) + + seconds := int(uptime.Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return RuntimeInfo{ + UptimeSeconds: seconds, + UptimeHuman: fmt.Sprintf("%d hour, %d minutes", hours, minutes), + CurrentTime: now.Format("2006-01-02T15:04:05Z"), + Timezone: "UTC", + } +} + +// requestInfo extracts request metadata for the JSON response payload. +func requestInfo(r *http.Request) RequestInfo { + return RequestInfo{ + ClientIP: clientIP(r), + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + } +} + +// clientIP returns the best-effort client IP address. +// If behind a proxy, the first X-Forwarded-For value is preferred. +func clientIP(r *http.Request) string { + xff := r.Header.Get("X-Forwarded-For") + if xff != "" { + parts := strings.Split(xff, ",") + if len(parts) > 0 { + return strings.TrimSpace(parts[0]) + } + } + + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil && host != "" { + return host + } + return r.RemoteAddr +} + +// linuxKernelRelease reads Linux kernel release from /proc as a best-effort value. +// On non-Linux platforms (or if the file is missing), it returns an empty string. +func linuxKernelRelease() string { + if runtime.GOOS != "linux" { + return "" + } + b, err := os.ReadFile("/proc/sys/kernel/osrelease") + if err != nil { + return "" + } + return strings.TrimSpace(string(b)) +} + +// writeJSON writes a JSON response with the given HTTP status code. +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +type statusWriter struct { + http.ResponseWriter + status int +} + +// WriteHeader captures the status code for logging. +func (sw *statusWriter) WriteHeader(code int) { + sw.status = code + sw.ResponseWriter.WriteHeader(code) +} + +// loggingMiddleware logs request metadata and the final status code. +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(sw, r) + + lat := time.Since(start) + log.Printf("Request %s %s from %s UA=%s -> %d (%s)", + r.Method, + r.URL.Path, + clientIP(r), + r.UserAgent(), + sw.status, + lat, + ) + }) +} + +// recoverMiddleware converts panics into a JSON 500 response. +func recoverMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + log.Printf("ERROR 500 Internal Server Error: %v", rec) + writeJSON(w, http.StatusInternalServerError, ErrorResponse{ + Error: "Internal Server Error", + Message: "An unexpected error occurred", + }) + } + }() + next.ServeHTTP(w, r) + }) +} + +// rootHandler returns the service diagnostic payload. +func rootHandler(rt *router) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + payload := RootResponse{ + Service: service, + System: systemInfo(), + Runtime: runtimeInfo(), + Request: requestInfo(r), + Endpoints: rt.Endpoints(), + } + writeJSON(w, http.StatusOK, payload) + } +} + +// healthHandler returns a minimal health probe response (HTTP 200 on success). +func healthHandler(w http.ResponseWriter, r *http.Request) { + rt := runtimeInfo() + writeJSON(w, http.StatusOK, HealthResponse{ + Status: "healthy", + Timestamp: rt.CurrentTime, + UptimeSeconds: rt.UptimeSeconds, + }) +} + +// crashHandler intentionally panics to verify 500 error handling. +// func crashHandler(w http.ResponseWriter, r *http.Request) { +// panic("crash test") +// } + +func main() { + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + debug := strings.ToLower(os.Getenv("DEBUG")) == "true" + + if debug { + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + log.Println("DEBUG enabled") + } else { + log.SetFlags(log.LstdFlags) + } + + rt := newRouter() + rt.Handle(http.MethodGet, "/", "Root endpoint: returns service metadata and diagnostic information.", rootHandler(rt)) + rt.Handle(http.MethodGet, "/health", "Health check endpoint for monitoring and Kubernetes probes.", healthHandler) + // rt.Handle(http.MethodGet, "/crash", "Intentional error to test 500 handler.", crashHandler) + + + handler := recoverMiddleware(loggingMiddleware(rt)) + + addr := net.JoinHostPort(host, port) + log.Printf("Listening on http://%s", addr) + + srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 5 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..ab349d5c37 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,45 @@ +# VCS +.git +.gitignore + +# Python bytecode / cache +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Packaging / build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ + +# Logs +*.log +.coverage +htmlcov/ + +# IDEs / editors +.vscode/ +.idea/ +*.swp + +# Docs and misc (optional, but keeps build context small) +docs/ +*.md + +# OS junk +.DS_Store +Thumbs.db + +# Environment files / secrets (IMPORTANT) +.env +.env.* diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..e6d498e35d --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,40 @@ +# Python bytecode / cache +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Packaging / build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ +*.spec + +# Logs +*.log + +# Test / tooling cache +.pytest_cache/ +.coverage +htmlcov/ +.mypy_cache/ +.ruff_cache/ + +# IDEs / editors +.vscode/ +.idea/ +*.swp + +# OS junk +.DS_Store +Thumbs.db + +# Environment files +.env +.env.* \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..846451649d --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,29 @@ +# Fixed version of Python, slim variant (lightweight, smaller attack surface) +FROM python:3.13-slim + +# Containerized Python behavior settings (no __pycache__/*.pyc, unbuffered output) +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Create an unprivileged user (safer, less attacker power) +RUN useradd --create-home --uid 1001 --shell /usr/sbin/nologin appuser + +# All further actions will be performed from the /app directory. +WORKDIR /app + +# Copying the requirements.txt file into the container, installing the dependencies described in it, and deleting the cache +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copying the app.py executable into the container +COPY app.py . + +# Granting ownership rights to the /app directory and all files in it to the user appuser +RUN chown -R appuser:appuser /app +USER appuser + +# Defining the container port on which the application will run +EXPOSE 5000 + +# Define startup command +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..2254a66068 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,224 @@ +# DevOps Info Service (Lab 01) + +Small Flask web app for DevOps labs. + +Provides: +- `GET /` — service/system/runtime/request info + list of endpoints +- `GET /health` — simple health check endpoint (for monitoring / K8s probes) + +Configuration is done via environment variables: `HOST`, `PORT`, `DEBUG`. + +--- + +## Overview + +This service returns diagnostic information about: +- service metadata (name/version/description/framework) +- host system (hostname/platform/arch/CPU/python) +- runtime (uptime + current UTC time) +- current request (client IP, user-agent, method, path) +- available API endpoints (generated from Flask URL map) + +--- + +## Prerequisites + +- Python **3.11+** (Flask 3.x) +- pip / venv + +--- + +## Installation + +```bash +cd app_python + +python -m venv venv +source venv/bin/activate + +pip install -r requirements.txt +``` + +--- + +## Running the Application + +Default (0.0.0.0:5000): +```bash +python app.py +``` + +Custom port: +```bash +PORT=8080 python app.py +``` + +Custom host + port: +```bash +HOST=127.0.0.1 PORT=3000 python app.py +``` + +Enable debug-level logging (and Flask debug mode): +```bash +DEBUG=true python app.py +``` + +--- + +## API Endpoints + +### `GET /` + +Returns full service + runtime info. + +Example: +```bash +curl -s http://127.0.0.1:5000/ | jq '{service, system, runtime, request, endpoints}' +``` + +Response structure: +```json +{ + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 24, + "hostname": "SerggAidd", + "platform": "Linux", + "platform_version": "6.18.5-arch1-1", + "python_version": "3.14.2" + }, + "runtime": { + "current_time": "2026-01-23T18:16:16Z", + "timezone": "UTC", + "uptime_human": "0 hour, 0 minutes", + "uptime_seconds": 4 + }, + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.18.0" + }, + "endpoints": [ + { + "description": "Root endpoint: returns service metadata and diagnostic information.", + "method": "GET", + "path": "/" + }, + { + "description": "Health check endpoint for monitoring and Kubernetes probes.", + "method": "GET", + "path": "/health" + } + ] +} +``` + +> Note: JSON object key ordering is not guaranteed by the HTTP/JSON standard. +> Use `python -m json.tool` or `jq` (like in example) only for pretty printing. + +### `GET /health` + +Health endpoint for monitoring / Kubernetes probes. + +Example: +```bash +curl -s http://127.0.0.1:5000/health | python -m json.tool +``` + +Response: +```json +{ + "status": "healthy", + "timestamp": "2026-01-23T21:25:39Z", + "uptime_seconds": 43 +} +``` + +Always returns HTTP **200** when service is running. + +--- + +## Error Handling + +- Unknown routes return JSON 404: + +Example: +```bash +curl -i http://127.0.0.1:5000/does-not-exist +``` + +Response: +```json +{"error":"Not Found","message":"Endpoint does not exist"} +``` + +- Internal errors return JSON 500 (test endpoint is intentionally NOT included by default). + +Example: +```bash +curl -i http://127.0.0.1:5000/crash +``` +Response: +```json +{"error":"Internal Server Error","message":"An unexpected error occurred"} +``` + +--- + +## Logging + +The app logs to **stdout**, which is the recommended approach for Docker/Kubernetes. + +Logged events: +- request metadata before handling (`@app.before_request`) +- response status code after handling (`@app.after_request`) +- custom 404/500 handlers + +--- + +## Configuration + +| Variable | Default | Description | +|---------:|-----------|-------------| +| `HOST` | `0.0.0.0` | Bind address | +| `PORT` | `5000` | Listen port | +| `DEBUG` | `False` | `true` enables Flask debug mode and DEBUG logging | + +--- +## Docker +Below are the basic commands for building and running an application in a container. + +### Local image build +```bash +cd app_python +docker build -t app_python:1.0 . +``` + +### Starting a container +The container listens on port 5000 internally, so we forward it to the host port (in the case bellow 8080) +```bash +docker run --rm -p 8080:5000 app_python:1.0 +``` +Example of running with variables: +```bash +docker run --rm -p 8080:5000 -e PORT=5000 -e DEBUG=false app_python:1.0 +``` +After that you can check endpoints in browser or in defferent terminal: +```bash +curl http://localhost:8080/ +curl http://localhost:8080/health +``` + +### Docker Hub (pull/run) +The image of this container publlished in Docker Hub. Image can be download with the following method: +```bash +docker pull sergey173/app_python:1.0.0 +docker run --rm -p 8080:5000 sergey173/app_python:1.0.0 +``` diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..1a152e338a --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,205 @@ +""" +DevOps Info Service + +Small Flask web app for DevOps labs. +Provides basic system/runtime/request information and a health check endpoint. +Configured via environment variables (HOST, PORT, DEBUG). +""" + +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +from flask import Flask, jsonify, request + +# Flask application instance +app = Flask(__name__) + +# Runtime configuration (can be overridden via environment variables) +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", "5000")) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Timestamp captured at startup (used to calculate uptime) +START_TIME = datetime.now(timezone.utc) + +SERVICE = { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + } + +# Logging configuration (stdout; suitable for Docker/Kubernetes) +logging.basicConfig( + level=logging.DEBUG if DEBUG else logging.INFO, + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", +) + + +# General functions of application +def system_info(): + """Return basic host and Python runtime information.""" + + 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 runtime_info(): + """Return uptime and current UTC timestamp for the running application.""" + + current_time = datetime.now(timezone.utc) + delta = current_time - START_TIME + timestamp = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + "uptime_seconds": seconds, + "uptime_human": f"{hours} hour, {minutes} minutes", + "current_time": timestamp, + "timezone": "UTC", + } + + +def request_info(): + """ + Extract request metadata. + + If the app is behind a reverse proxy, the client IP may be passed via + X-Forwarded-For header (first IP in the list). Fallback to remote_addr. + """ + + xff = request.headers.get("X-Forwarded-For", "") + client_ip = xff.split(",")[0].strip() if xff else request.remote_addr + + return { + "client_ip": client_ip, + "user_agent": request.headers.get("User-Agent"), + "method": request.method, + "path": request.path, + } + + +def endpoints_info(): + """ + Build an API endpoints list dynamically from Flask URL map. + Description is taken from the first line of each handler's docstring. + """ + + endpoints = [] + for rule in app.url_map.iter_rules(): + if rule.endpoint == "static": + continue + + view_func = app.view_functions.get(rule.endpoint) + doc = getattr(view_func, "__doc__", None) if view_func else None + desc = (doc or "").strip().splitlines()[0] if doc else "No description" + + methods = sorted((rule.methods or set()) - {"HEAD", "OPTIONS"}) + for m in methods: + endpoints.append({ + "method": m, + "path": rule.rule, + "description": desc, + }) + + endpoints.sort(key=lambda e: (e["path"], e["method"])) + return endpoints + + +# General endpoints +@app.get("/") +def index(): + """Root endpoint: returns service metadata and diagnostic information.""" + + payload = { + "service": SERVICE, + "system": system_info(), + "runtime": runtime_info(), + "request": request_info(), + "endpoints": endpoints_info(), + } + return jsonify(payload) + + +@app.get("/health") +def health(): + """Health check endpoint for monitoring and Kubernetes probes.""" + rt = runtime_info() + payload = { + "status": "healthy", + "timestamp": rt["current_time"], + "uptime_seconds": rt["uptime_seconds"], + } + return jsonify(payload) + + +# Test-only endpoint to trigger HTTP 500 (uncomment to verify error handler) +# @app.get("/crash") +# def crash(): +# """Intentional error to test 500 handler.""" +# 1 / 0 + + +# Error Handlers +@app.errorhandler(404) +def not_found(error): + """Return JSON error for unknown endpoints.""" + + app.logger.warning("404 Not Found: %s %s", request.method, request.path) + return jsonify({ + "error": "Not Found", + "message": "Endpoint does not exist", + }), 404 + + +@app.errorhandler(500) +def internal_error(error): + """Return JSON error for unhandled server exceptions.""" + + app.logger.exception("500 Internal Server Error") + return jsonify({ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }), 500 + + +# Logging endpoints +@app.before_request +def log_requests(): + """Log basic request metadata before handling.""" + + app.logger.info( + "Request %s %s from %s UA=%s", + request.method, + request.path, + request.headers.get("X-Forwarded-For", request.remote_addr), + request.headers.get("User-Agent"), + ) + + +@app.after_request +def log_response(response): + """Log response status code after handling.""" + + app.logger.info( + "Response %s %s -> %s", + request.method, + request.path, + response.status_code, + ) + + return response + + +if __name__ == "__main__": + 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..6bf344aaff --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,358 @@ +# LAB01 — DevOps Info Service (Python) + +This document describes the **Python/Flask** implementation of **DevOps Info Service** for Lab 01. +The service is a small HTTP JSON API that exposes system/runtime/request metadata and a health check endpoint. + +--- + +## Framework Selection + +### Choice: Flask + +This implementation uses **Flask** as a lightweight WSGI web framework to build a small JSON API with minimal overhead. + +**Rationale** +- **Small scope fit:** Flask is well-suited for 2–3 endpoints without extra abstractions. +- **Simple request/response model:** easy access to request metadata (`request.method`, `request.path`, headers). +- **Built-in routing + hooks:** `@app.get`, `@app.before_request`, `@app.after_request`, and error handlers reduce boilerplate. +- **Container-friendly logging:** logs can be emitted to stdout and captured by Docker/Kubernetes. + +### Comparison table with alternatives + +| Option | Pros | Cons | Decision | +|---|---|---|---| +| **Flask** | Minimal API, easy routing, simple hooks | Not async-first | **Selected** (best fit for small lab service) | +| FastAPI | Automatic OpenAPI, type hints, async support | More dependencies, more setup | Not needed for Lab 01 | +| Django | Full-featured framework | Heavy for a tiny JSON service | Overkill | + +--- + +## Best Practices Applied + +Below is a list of practices applied in the implementation, with short code excerpts and why each matters. + +### 1) Configuration via environment variables +**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** +- Matches Docker/Kubernetes configuration conventions. +- The same code runs in different environments without changes. + +### 2) Consistent JSON error responses (404 / 500) +**Examples** +```python +@app.errorhandler(404) +def not_found(error): + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 +``` + +```python +@app.errorhandler(500) +def internal_error(error): + return jsonify({"error": "Internal Server Error", "message": "An unexpected error occurred"}), 500 +``` + +**Why it matters** +- Clients always receive machine-readable responses. +- Prevents HTML error pages, which are inconvenient for API consumers. + +### 3) Request/response logging to stdout +**Examples** +```python +@app.before_request +def log_requests(): + app.logger.info("Request %s %s ...", request.method, request.path) +``` + +```python +@app.after_request +def log_response(response): + app.logger.info("Response %s %s -> %s", request.method, request.path, response.status_code) + return response +``` + +**Why it matters** +- Stdout logging is the standard for containers. +- Helps verify probes and debug behavior without extra tooling. + +### 4) Proxy-aware client IP extraction +**Example** +```python +xff = request.headers.get("X-Forwarded-For", "") +client_ip = xff.split(",")[0].strip() if xff else request.remote_addr +``` + +**Why it matters** +- Preserves the real client IP when the service runs behind an ingress/reverse proxy. + +--- + +## API Documentation + +### `GET /` +Returns service metadata, system/runtime details, request metadata, and a list of available endpoints. + +**Request** +```bash +curl -s http://127.0.0.1:5000/ +``` + +**Response (schema)** +```json +{ + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 24, + "hostname": "SerggAidd", + "platform": "Linux", + "platform_version": "6.18.5-arch1-1", + "python_version": "3.14.2" + }, + "runtime": { + "current_time": "2026-01-23T18:16:16Z", + "timezone": "UTC", + "uptime_human": "0 hour, 0 minutes", + "uptime_seconds": 4 + }, + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.18.0" + }, + "endpoints": [ + { + "description": "Root endpoint: returns service metadata and diagnostic information.", + "method": "GET", + "path": "/" + }, + { + "description": "Health check endpoint for monitoring and Kubernetes probes.", + "method": "GET", + "path": "/health" + } + ] +} +``` + +### `GET /health` +Health endpoint for monitoring / Kubernetes probes. Returns HTTP **200** when the service is running. + +**Request** +```bash +curl -s http://127.0.0.1:5000/health +``` + +**Response (example)** +```json +{ + "status": "healthy", + "timestamp": "2026-01-23T19:08:22Z", + "uptime_seconds": 8 +} +``` + +--- + +### Error handling + +**404 Not Found** (unknown routes) +```bash +curl -s http://127.0.0.1:5000/does-not-exist +``` + +**Response** +```json +{"error":"Not Found","message":"Endpoint does not exist"} +``` + +**500 Internal Server Error** (unhandled exceptions) +- A test endpoint can be temporarily enabled by uncommenting the `/crash` handler in the code. + +```bash +curl -s http://127.0.0.1:5000/crash +``` + +**Response** +```json +{"error":"Internal Server Error","message":"An unexpected error occurred"} +``` + +> Note: JSON object key ordering is not guaranteed. Use `python -m json.tool` or `jq` only for pretty-printing. + +--- + +## Testing Commands + +### Setup and run + +Create venv and install dependencies: +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +Run the service: +```bash +HOST=0.0.0.0 PORT=5000 DEBUG=false python app.py +``` + +### Endpoint checks + +```bash +curl -s http://127.0.0.1:5000/ +curl -s http://127.0.0.1:5000/health +curl -s http://127.0.0.1:5000/does-not-exist +# Optional (if /crash is enabled): +# curl -s http://127.0.0.1:5000/crash +``` + +Pretty-print JSON: +```bash +curl -s http://127.0.0.1:5000/ | python -m json.tool +``` + +--- + +## Testing Evidence + +### Screenshots showing endpoints work + +Required screenshots should be stored in `docs/screenshots/`: + +1) **Main endpoint showing complete JSON** +- `docs/screenshots/01_root_complete_json.png` +![GET / — complete JSON](./screenshots/LAB01/01_root_complete_json.png) + +2) **Health check response** +- `docs/screenshots/02_health_check.png` +![GET /health — health probe](./screenshots/LAB01/02_health_check.png) + +3) **Formatted/pretty-printed output** +- `docs/screenshots/03_pretty_print_command.png` +![Pretty-print example](./screenshots/LAB01/03_pretty_print_command.png) + + +### Terminal output + +Include terminal output demonstrating: +```text +curl -s http://127.0.0.1:5000/ | jq '{service, system, runtime, request, endpoints}' +{ + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 24, + "hostname": "SerggAidd", + "platform": "Linux", + "platform_version": "6.18.5-arch1-1", + "python_version": "3.14.2" + }, + "runtime": { + "current_time": "2026-01-23T21:25:29Z", + "timezone": "UTC", + "uptime_human": "0 hour, 0 minutes", + "uptime_seconds": 32 + }, + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.18.0" + }, + "endpoints": [ + { + "description": "Root endpoint: returns service metadata and diagnostic information.", + "method": "GET", + "path": "/" + }, + { + "description": "Health check endpoint for monitoring and Kubernetes probes.", + "method": "GET", + "path": "/health" + } + ] +} +``` + +```text +curl -s http://127.0.0.1:5000/health | python -m json.tool +{ + "status": "healthy", + "timestamp": "2026-01-23T21:25:39Z", + "uptime_seconds": 43 +} +``` + +```text +curl -s http://127.0.0.1:5000/does-not-exist | python -m json.tool +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` + +```text +curl -s http://127.0.0.1:5000/crash | python -m json.tool +{ + "error": "Internal Server Error", + "message": "An unexpected error occurred" +} +``` + +--- + +## Challenges & Solutions + +### 1) Deterministic endpoint list ordering +**Problem:** Flask’s URL map iteration order is not guaranteed to match a desired display order. +**Solution:** Collected endpoint entries and sorted by `(path, method)` before returning. + +### 2) Correct client IP behind reverse proxies +**Problem:** `request.remote_addr` may show only the proxy address. +**Solution:** Prefer the first value from `X-Forwarded-For` and fall back to `remote_addr`. + +### 3) Consistent 500 responses for exceptions +**Problem:** Unhandled exceptions can result in default HTML error pages. +**Solution:** Added a `500` error handler returning a JSON payload. A test-only `/crash` endpoint can be enabled to demonstrate this behavior during validation. + +--- +## GitHub Community Engagement +Starring repositories is a lightweight way to bookmark useful projects and also signals community interest, which improves discovery and encourages maintainers. Following developers and classmates helps track relevant updates, learn from real code activity, and makes collaboration easier by keeping your team’s work visible in one place. + +### My Stars: +- Star the course repository +![Course repository star](./screenshots/LAB01/04_star_for_course.png) +- Star simple-container-com/api +![Simple-container-com/api repository star](./screenshots/LAB01/05_star_for_simple-container-com.png) + +### My Follows: +- Following to Dmitriy Creed (Professor) +![Follow to Professor](./screenshots/LAB01/06_prof_follow.png) +- Following to Du Tham Lieu (TA) +![Follow to TA](./screenshots/LAB01/07_ta1_follow.png) +- Following to Marat Biriushev (TA) +![Follow to TA](./screenshots/LAB01/08_ta2_follow.png) +- Following to Alexander Rozanov (classmate) +![Follow to CM](./screenshots/LAB01/09_cm1_follow.png) +- Following to Ilvina Akhmetzyanova (classmate) +![Follow to CM](./screenshots/LAB01/10_cm2_follow.png) +- Following to Klimentii Chistyakov (classmate) +![Follow to CM](./screenshots/LAB01/11_cm3_follow.png) \ 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..31e6fa3a8b --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,145 @@ +# LAB02 — Docker Containerization (app_python) + +This document describes the **Python/Flask** implementation of **DevOps Info Service** for Lab 01. +The service is a small HTTP JSON API that exposes system/runtime/request metadata and a health check endpoint. + +--- + +## Best Practices Applied +### 1) Base image pin and minimal base +**Example** +```dockerfile +FROM python:3.13-slim +``` + +**Why it matters** +- "Pinning" the version makes the build reproducible: you'll get the same environment today and in a month. +- `-slim` is significantly smaller than the "full" image, then faster pull/push, smaller attack surface. + +### 2) Non-root runtime +**Example** +```dockerfile +RUN useradd --create-home --uid 1001 --shell /usr/sbin/nologin appuser +... +USER appuser +``` + +**Why it matters** +- If a process in a container is compromised, the attacker will not gain root privileges. +- Important for Kubernetes/PodSecurity (root containers are often prohibited by policy) + +### 3) Correct layer order (layer caching) +**Example** +First, `requirements.txt` is copied and dependencies are installed, and only then the code is copied: +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . +``` + +**Why it matters** +- Docker caches layers. +- If only `app.py` changes, the build runs faster because the dependency layer is reused. + +### 4) Installing dependencies is done with `--no-cache-dir` +**Example** +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Why it matters** +- The cache increases the final image size and is not required at runtime. +- Faster delivery due to a smaller file size. +- Smaller attack surface. + +### 5) `.dockerignore` file +**Example** + +Added `.dockerignore`.. + +**Why it matters** +- The build context is smaller, meaning faster builds. +- The risk of leaking sensitive files (e.g., .env) is reduced if they are accidentally stored in the directory. + +## Image Information & Decisions +### Base image +`python:3.13-slim` was selected: +- Sufficiently "complete" for most Python web apps; +- Significantly smaller than the full `python:3.13`; +- Compatible with most wheel packages and the glibc environment (unlike Alpine/musl, which sometimes have build/dependency compatibility issues). + +### Final image size and assessment +**Final image size:** 129 MB. + +**Assessment:** For a Python web application based on python:3.13-slim, this is a reasonable size because the image includes not only my code but also the Python interpreter and system libraries. This size ensures fast pull/push and a smaller attack surface compared to a "full" python:3.13 image (around 1 GB). + +### Layer structure +1. `FROM python:3.13-slim` — minimal base +2. `ENV ...` — configure Python behavior in the container. +3. `RUN useradd ...` — create an unprivileged user +4. `WORKDIR /app` — working directory. +5. `COPY requirements.txt` — depends on (cached layer) +6. `RUN pip install ...` — install dependencies +7. `COPY app.py .` — copy application code. +8. `RUN chown -R appuser:appuser /app` — directory and file permissions +9. `USER appuser` — run as an unprivileged user. +10. `EXPOSE 5000` — document the port +11. `CMD["python", "app.py"]` — start command + +### Optimization Choices: +>This section almost completely replicates the Best Practices guidelines, so key patterns will be described here + +- The minimum required for the application to work is used (a slim image is used, only the necessary files are copied, the cache is cleared) +- The correct layer order eliminates unnecessary actions (no reinstallation of dependencies, etc.) + + + +## Build & Run Process + +- Complete terminal output from build process: ![Complete terminal output from build process](./screenshots/LAB02/01_complit_build.png) +- Terminal output showing container running: ![Terminal output showing container running](./screenshots/LAB02/02_container_running.png) +- Terminal output from testing endpoints (curl/httpie): ![Terminal output from testing endpoints with curl](./screenshots/LAB02/03_endpoint_check.png) +- Docker Hub repository URL: https://hub.docker.com/repository/docker/sergey173/app_python + + + + +## Technical Analysis +1. **Why does your Dockerfile work the way it does?** + + **Answer:** Because it pins the runtime environment (base image + dependencies), uses efficient layer caching, and runs the app as a non-root user, which makes the container reproducible, faster to rebuild, and safer to run. + +2. **What would happen if you changed the layer order?** + + **Answer:** This is a complex question, depending on what you're changing. I'll give a couple of examples. + - If I copied the application code before installing dependencies, any code change would invalidate the cache and force pip to reinstall dependencies on every build, making rebuilds much slower (bad for CI/CD). + - If I switched to a non-root user too early, I could run into file permission issues (e.g., the app might not be able to write files under /app if ownership/permissions were not set correctly). + - If I moved ENV instructions to the very end, the app would still work, but it could reduce caching efficiency depending on what changes, and the Dockerfile would be less structured. ENV variables are meant to define runtime behavior early and clearly. + +3. **What security considerations did you implement?** + + **Answer:** + - Run as non-root user (USER appuser) to reduce impact if the application is compromised. + - Use a minimal base image (python:3.13-slim) to reduce attack surface and vulnerability exposure compared to full images. + - Avoid pip cache in the final image (--no-cache-dir) to keep the image smaller and reduce unnecessary files. + - Use .dockerignore to avoid accidentally shipping development artifacts or secrets into the build context/image. + +4. **How does `.dockerignore` improve your build?** + + **Answer:** This file reduces build context and the risk of using unnecessary files and secrets during the build. +--- + +## Challenges & Solutions + +### 1) BuildKit/buildx on Arch +**Problem:** I encountered a problem where `DOCKER_BUILDKIT=1` didn't work due to a buildx call/break. + +**Solution:** Install `docker-buildx` or build using the legacy builder (without `--check`). + +### 2) Unexpected requests in logs +**Problem:** When launching a containerized application, regular requests to a non-existent endpoint appeared in the logs. + +**Solution:** Using diagnostics (command `sudo lsof -nP -iTCP:8080 -sTCP:ESTABLISHED`) it was revealed that the problem was not in the container, but in the cache of the browser being used. + + diff --git a/app_python/docs/screenshots/LAB01/01_root_complete_json.png b/app_python/docs/screenshots/LAB01/01_root_complete_json.png new file mode 100644 index 0000000000..0fdb582e1d Binary files /dev/null and b/app_python/docs/screenshots/LAB01/01_root_complete_json.png differ diff --git a/app_python/docs/screenshots/LAB01/02_health_check.png b/app_python/docs/screenshots/LAB01/02_health_check.png new file mode 100644 index 0000000000..92a1b3c4ef Binary files /dev/null and b/app_python/docs/screenshots/LAB01/02_health_check.png differ diff --git a/app_python/docs/screenshots/LAB01/03_pretty_print_command.png b/app_python/docs/screenshots/LAB01/03_pretty_print_command.png new file mode 100644 index 0000000000..341f696aa1 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/03_pretty_print_command.png differ diff --git a/app_python/docs/screenshots/LAB01/04_star_for_course.png b/app_python/docs/screenshots/LAB01/04_star_for_course.png new file mode 100644 index 0000000000..39126d7366 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/04_star_for_course.png differ diff --git a/app_python/docs/screenshots/LAB01/05_star_for_simple-container-com.png b/app_python/docs/screenshots/LAB01/05_star_for_simple-container-com.png new file mode 100644 index 0000000000..4292b6a506 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/05_star_for_simple-container-com.png differ diff --git a/app_python/docs/screenshots/LAB01/06_prof_follow.png b/app_python/docs/screenshots/LAB01/06_prof_follow.png new file mode 100644 index 0000000000..d6aedbde4c Binary files /dev/null and b/app_python/docs/screenshots/LAB01/06_prof_follow.png differ diff --git a/app_python/docs/screenshots/LAB01/07_ta1_follow.png b/app_python/docs/screenshots/LAB01/07_ta1_follow.png new file mode 100644 index 0000000000..58d8181edc Binary files /dev/null and b/app_python/docs/screenshots/LAB01/07_ta1_follow.png differ diff --git a/app_python/docs/screenshots/LAB01/08_ta2_follow.png b/app_python/docs/screenshots/LAB01/08_ta2_follow.png new file mode 100644 index 0000000000..dde8ddba47 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/08_ta2_follow.png differ diff --git a/app_python/docs/screenshots/LAB01/09_cm1_follow.png b/app_python/docs/screenshots/LAB01/09_cm1_follow.png new file mode 100644 index 0000000000..a9048265d4 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/09_cm1_follow.png differ diff --git a/app_python/docs/screenshots/LAB01/10_cm2_follow.png b/app_python/docs/screenshots/LAB01/10_cm2_follow.png new file mode 100644 index 0000000000..36ab874f38 Binary files /dev/null and b/app_python/docs/screenshots/LAB01/10_cm2_follow.png differ diff --git a/app_python/docs/screenshots/LAB01/11_cm3_follow.png b/app_python/docs/screenshots/LAB01/11_cm3_follow.png new file mode 100644 index 0000000000..a1e0951b2f Binary files /dev/null and b/app_python/docs/screenshots/LAB01/11_cm3_follow.png differ diff --git a/app_python/docs/screenshots/LAB02/01_complit_build.png b/app_python/docs/screenshots/LAB02/01_complit_build.png new file mode 100644 index 0000000000..92b33686b2 Binary files /dev/null and b/app_python/docs/screenshots/LAB02/01_complit_build.png differ diff --git a/app_python/docs/screenshots/LAB02/02_container_running.png b/app_python/docs/screenshots/LAB02/02_container_running.png new file mode 100644 index 0000000000..ccafa6815a Binary files /dev/null and b/app_python/docs/screenshots/LAB02/02_container_running.png differ diff --git a/app_python/docs/screenshots/LAB02/03_endpoint_check.png b/app_python/docs/screenshots/LAB02/03_endpoint_check.png new file mode 100644 index 0000000000..01a82015c6 Binary files /dev/null and b/app_python/docs/screenshots/LAB02/03_endpoint_check.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..5fc39647c3 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,15 @@ +blinker==1.9.0 +certifi==2026.1.4 +charset-normalizer==3.4.4 +click==8.3.1 +DateTime==6.0 +Flask==3.1.2 +idna==3.11 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +pytz==2025.2 +requests==2.32.5 +urllib3==2.6.3 +Werkzeug==3.1.5 +zope.interface==8.2 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2