diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..52c65fe81d --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,9 @@ +.git +*.md +docs/ +bin/ +dist/ +tmp/ +.idea/ +.vscode/ +.DS_Store \ No newline at end of file diff --git a/app_go/.gitignore b/app_go/.gitignore new file mode 100644 index 0000000000..d6c75d54e3 --- /dev/null +++ b/app_go/.gitignore @@ -0,0 +1,18 @@ +# Go binaries / build output +devops-info-service +*.exe +*.out +*.test +bin/ +dist/ + +# IDE/editor +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log \ No newline at end of file diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..6dd906a964 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,14 @@ +# 🔨 Stage 1: Builder +FROM golang:1.22-alpine AS builder +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o myapp + +# 🚀 Stage 2: Runtime +FROM gcr.io/distroless/static-debian12:nonroot +WORKDIR /app +COPY --from=builder /app/myapp . +EXPOSE 8080 +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..e53cc9a7e6 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,43 @@ +# devops-info-service (Go) + +## Overview +`devops-info-service` is a lightweight HTTP service written in Go. It returns: +- service metadata (name, version, description, framework), +- system information (hostname, OS/platform, architecture, CPU count, Go version), +- runtime information (uptime, current UTC time), +- request information (client IP, user-agent, method, path), +- a list of available endpoints. + +This is useful for DevOps labs and basic observability: quick environment inspection and health checks. + +--- + +## Prerequisites +- **Go:** 1.22+ (recommended) +- No external dependencies (standard library only) + +--- + +## Installation +```bash +cd app_go +go mod tidy +``` + +## Running the Application +```bash +go run . +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration + +The application is configured using environment variables. + +| Variable | Default | Description | Example | +|---------|---------|-------------|---------| +| `HOST` | `0.0.0.0` | Host interface to bind the server to | `0.0.0.0` | +| `PORT` | `8080` | Port the server listens on | `8080` | \ 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..39785012a7 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,5 @@ +### Why Go? +- **Compiled binary**: produces a single executable (useful for multi-stage Docker builds). +- **Fast startup and low overhead**: good for microservices. +- **Standard library is enough**: `net/http` covers routing and HTTP server without external frameworks. +- **Great DevOps fit**: simple deployment, small runtime requirements. diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..52920f06bc --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,217 @@ +# Lab 1 (Bonus) — DevOps Info Service in Go + +## 1. Language / Framework Selection + +### Choice +I implemented the bonus service in **Go** using the standard library **net/http** package. + +### Why Go? +- **Compiled binary**: produces a single executable (useful for multi-stage Docker builds). +- **Fast startup and low overhead**: good for microservices. +- **Standard library is enough**: `net/http` covers routing and HTTP server without external frameworks. +- **Great DevOps fit**: simple deployment, small runtime requirements. + +### Comparison with Alternatives + +| Criteria | Go (net/http) (chosen) | Rust | Java (Spring Boot) | C# (ASP.NET Core) | +|---------|--------------------------|------|---------------------|-------------------| +| Build artifact | Single binary | Single binary | JVM app + deps | .NET app + deps | +| Startup time | Fast | Fast | Usually slower | Medium | +| Runtime deps | None | None | JVM required | .NET runtime | +| HTTP stack | stdlib | frameworks (Axum/Actix) | Spring ecosystem | ASP.NET stack | +| Complexity | Low | Medium–high | Medium | Medium | +| Best fit for this lab | Excellent | Good | Overkill | Good | + +--- + +## 2. Best Practices Applied + +### 2.1 Clean Code Organization +- Clear data models (`ServiceInfo`, `Service`, `System`, `RuntimeInfo`, `RequestInfo`, `Endpoint`). +- Helper functions for concerns separation: + - `runtimeInfo()`, `requestInfo()`, `uptime()`, `isoUTCNow()`, `clientIP()`, `writeJSON()`. + +### 2.2 Configuration via Environment Variables +The service is configurable via environment variables: +- `HOST` (default `0.0.0.0`) +- `PORT` (default `8080`) +- `DEBUG` (default `false`) + +Implementation uses a simple helper: +```go +func getenv(key, def string) string { + v := os.Getenv(key) + if v == "" { + return def + } + return v +} +``` + +### 2.3 Logging Middleware +Request logging is implemented as middleware: +```go +func withLogging(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + logger.Printf("%s %s (%s) from %s in %s", + r.Method, r.URL.Path, r.Proto, r.RemoteAddr, time.Since(start)) + }) + } +} +``` + +### 2.4 Error Handling +#### 404 Not Found +Unknown endpoints return a consistent JSON error: +```json +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` +This is implemented via a wrapper that enforces valid paths: +```go +func withNotFound(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" && r.URL.Path != "/health" { + writeJSON(w, http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "Endpoint does not exist", + }) + return + } + next.ServeHTTP(w, r) + }) +} +``` +#### 500 Internal Server Error (panic recovery) +A recover middleware prevents crashes and returns a safe JSON response: +```go +func withRecover(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + logger.Printf("panic recovered: %v", rec) + writeJSON(w, http.StatusInternalServerError, ErrorResponse{ + Error: "Internal Server Error", + Message: "An unexpected error occurred", + }) + } + }() + next.ServeHTTP(w, r) + }) + } +} +``` +### 2.5 Production-Friendly HTTP Server Settings +The service uses `http.Server` with timeouts: +```go +srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 5 * time.Second, +} +``` +## 3. API Documentation +### 3.1 GET / — Service and System Information +**Description**: Returns service metadata, system info, runtime info, request info, and available endpoints. + +**Request**: +```bash +curl -i http://127.0.0.1:8080/ +``` +**Response (200 OK) example**: +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go net/http" + }, + "system": { + "hostname": "DESKTOP-KUN1CI4", + "platform": "windows", + "platform_version": "unknown", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.25.6" + }, + "runtime": { + "uptime_seconds": 6, + "uptime_human": "0 hours, 0 minutes", + "current_time": "2026-01-25T17:17:32.248Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "::1", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + { + "path": "/", + "method": "GET", + "description": "Service information" + }, + { + "path": "/health", + "method": "GET", + "description": "Health check" + } + ] +} +``` +### 3.2 GET /health — Health Check +**Description**: Description: Simple health endpoint used for monitoring and probes. + +**Request**: +```bash +curl -i http://127.0.0.1:8080/health +``` +**Response (200 OK) example**: +```json +{ + "status": "healthy", + "timestamp": "2026-01-25T17:19:02.582Z", + "uptime_seconds": 96 +} +``` +### 3.3 404 Behavior +**Request**: +```bash +curl -i http://127.0.0.1:8080/does-not-exist +``` +**Response (404 Not Found)**: +```json +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` + +## 4. Build & Run Instructions +### 4.1 Run locally (no build) +```bash +go run main.go +``` +### 4.2 Build binary +```bash +go build -o devops-info-service main.go +``` +Run: +```bash +./devops-info-service +``` +### 4.3 Environment variables examples +```bash +HOST=127.0.0.1 PORT=3000 ./devops-info-service +DEBUG=true PORT=8081 ./devops-info-service +``` +## 5. Challenges & Solutions +I don't know how `go` works. diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..ee98527851 --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,215 @@ +# Multi-stage build strategy +## Stage 1 - Builder +**Purpose:** compile the Go application using a full Go toolchain image. + +**Key points:** +- Uses `golang:1.22-alpine` to keep the builder stage smaller than Debian-based images. +- Copies `go.mod` first and runs `go mod download` to maximize Docker layer caching. +- Builds a Linux binary with `CGO_ENABLED=0` (static binary), which allows a minimal runtime image. + +**Dockerfile snippet:** +```dockerfile +FROM golang:1.22-alpine AS builder +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o myapp +``` + +## Stage 2 - Runtime +**Purpose:** run only the compiled binary in a minimal image with a non-root user. + +**Key points:** +- Uses `gcr.io/distroless/static-debian12:nonroot`. +- Distroless images contain only what is required to run the app (no package manager, no shell), reducing image size and attack surface. +- Runs as a **non-root** user (provided by the `:nonroot` tag). + +**Dockerfile snippet:** +```dockerfile +FROM gcr.io/distroless/static-debian12:nonroot +WORKDIR /app +COPY --from=builder /app/myapp . +EXPOSE 8080 +CMD ["./myapp"] +``` + +# Size comparison with analysis (builder vs final image) +## Builder +```bash +docker images newspec/app_go:builder +REPOSITORY TAG IMAGE ID CREATED SIZE +newspec/app_go builder 21b01f8c6103 37 minutes ago 305MB +``` +## Final +```bash +docker images newspec/app_go:1.0 +REPOSITORY TAG IMAGE ID CREATED SIZE +newspec/app_go 1.0 a944205e6030 59 minutes ago 9.32MB +``` +## Analysis +The builder image contains the Go toolchain and build dependencies, so it is significantly larger. +The final image contains only the static binary, which is much smaller and safer. + +# Why multi-stage builds matter for compiled languages + +Compiled languages (like Go, Rust, Java with native images, etc.) typically require a **heavy build environment**: compilers, linkers, SDKs, package managers, and temporary build artifacts. If you ship that same environment as your runtime container, the final image becomes unnecessarily large and less secure. + +Multi-stage builds solve this by separating concerns: + +### 1) Smaller final images (faster pull & deploy) +- The **builder stage** includes the full toolchain (large). +- The **runtime stage** contains only the compiled output (usually just a single binary). +This dramatically reduces image size, which improves: +- CI/CD speed (less time to push/pull) +- Startup speed (faster distribution in clusters) +- Bandwidth/storage usage + +### 2) Better security (smaller attack surface) +The runtime image no longer contains: +- compilers (go, gcc, build tools) +- package managers +- shells and utilities (especially with distroless/scratch) + +Fewer components, so fewer potential vulnerabilities (CVEs) and fewer tools available to an attacker if the container is compromised. + +### 3) Cleaner separation of build vs runtime +Multi-stage builds enforce a clear boundary: +- build dependencies exist only where needed (builder stage) +- runtime image stays minimal and focused on execution + +This makes the container easier to reason about and maintain. + +### 4) Reproducible and cache-friendly builds +When the Dockerfile copies dependency descriptors first (e.g., `go.mod`/`go.sum`) and downloads dependencies before copying source code: +- Docker can reuse cached layers if dependencies are unchanged +- rebuilds after code changes are significantly faster + +### 5) Enables ultra-minimal runtime images +Compiled apps can often run in very small base images: +- `distroless` (secure and minimal) +- `scratch` (almost empty) + +This is usually impossible for interpreted languages without bundling an interpreter runtime. + +**Summary:** For compiled languages, multi-stage builds provide the best of both worlds — a full build environment when you need it, and a minimal secure runtime image when you deploy. + +# Terminal output showing build process +## Builder +```bash +docker build --target builder -t newspec/app_go:builder . +[+] Building 2.4s (12/12) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 349B 0.0s + => [internal] load metadata for docker.io/library/golang:1.22-alpine 2.1s + => [auth] library/golang:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.1s + => => transferring context: 105B 0.0s + => [builder 1/6] FROM docker.io/library/golang:1.22-alpine@sha256:1699c10032ca2582ec89a24a1312d986a3f094aed3d5c1147b19880afe40e052 0.0s + => [internal] load build context 0.0s + => => transferring context: 146B 0.0s + => CACHED [builder 2/6] WORKDIR /app 0.0s + => CACHED [builder 3/6] COPY go.mod ./ 0.0s + => CACHED [builder 4/6] RUN go mod download 0.0s + => CACHED [builder 5/6] COPY . . 0.0s + => CACHED [builder 6/6] RUN CGO_ENABLED=0 go build -o myapp 0.0s + => exporting to image 0.0s + => => exporting layers 0.0s + => => writing image sha256:21b01f8c61038149b9130afe7881765d625b2eb6622b6b46f42682d26b10ae2b 0.0s + => => naming to docker.io/newspec/app_go:builder 0.0s + +View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/wvw3g1yzoqu1uput2mz8me7zx +``` + +## Final +```bash +docker build -t newspec/app_go:1.0 . +[+] Building 1.5s (15/15) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 349B 0.0s + => [internal] load metadata for gcr.io/distroless/static-debian12:nonroot 1.0s + => [internal] load metadata for docker.io/library/golang:1.22-alpine 0.7s + => [internal] load .dockerignore 0.0s + => => transferring context: 105B 0.0s + => [builder 1/6] FROM docker.io/library/golang:1.22-alpine@sha256:1699c10032ca2582ec89a24a1312d986a3f094aed3d5c1147b19880afe40e052 0.0s + => [stage-1 1/3] FROM gcr.io/distroless/static-debian12:nonroot@sha256:cba10d7abd3e203428e86f5b2d7fd5eb7d8987c387864ae4996cf97191b33764 0.0s + => [internal] load build context 0.0s + => => transferring context: 146B 0.0s + => CACHED [builder 2/6] WORKDIR /app 0.0s + => CACHED [builder 3/6] COPY go.mod ./ 0.0s + => CACHED [builder 4/6] RUN go mod download 0.0s + => CACHED [builder 5/6] COPY . . 0.0s + => CACHED [builder 6/6] RUN CGO_ENABLED=0 go build -o myapp 0.0s + => CACHED [stage-1 2/3] WORKDIR /app 0.0s + => CACHED [stage-1 3/3] COPY --from=builder /app/myapp . 0.0s + => exporting to image 0.1s + => => exporting layers 0.0s + => => writing image sha256:c9ff1572d8a13240f00ef7d66683264e0fbf4fa77c12790dc3f3428972819321 0.0s + => => naming to docker.io/newspec/app_go:1.0 0.0s + +View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/nvdyhylzo1hzpemy23lt42ll1 +``` +# Technical explanation of each stage's purpose +### Stage 1 — Builder (Compile Environment) +**Goal:** Produce a Linux executable from Go source code in a controlled build environment. + +**Why this stage exists:** +- Go compilation requires the Go toolchain (compiler, linker) which is large and should not be shipped in the final runtime image. +- The builder image provides everything needed to compile the application. + +**What happens technically:** +1. **Set working directory** + - `WORKDIR /app` defines where source code and build steps run inside the container. + +2. **Copy dependency definition first** + - `COPY go.mod ./` is done before copying the whole source tree. + - This allows Docker to cache the dependency download layer. + - Even if code changes, dependencies may not, so rebuilds are faster. + +3. **Download modules** + - `RUN go mod download` fetches required modules. + - In this project there are no external module dependencies, so Go prints: + `go: no module dependencies to download` + - The step is still good practice and keeps the Dockerfile consistent for future changes. + +4. **Copy application source code** + - `COPY . .` brings in the Go source files. + - This layer changes most often, so it comes after dependency caching steps. + +5. **Compile a static binary** + - `RUN CGO_ENABLED=0 go build -o myapp` + - `CGO_ENABLED=0` disables C bindings so the binary is statically linked. + - A static binary does not require libc or other runtime shared libraries, enabling minimal runtime images. + +**Output of the stage:** a compiled executable (`/app/myapp`). + +--- + +### Stage 2 — Runtime (Execution Environment) +**Goal:** Run only the compiled binary in a minimal and secure container image. + +**Why this stage exists:** +- The runtime stage should not contain compilers, source code, or build tools. +- A smaller runtime image reduces attack surface and improves deployment speed. + +**What happens technically:** +1. **Choose a minimal base image** + - `FROM gcr.io/distroless/static-debian12:nonroot` + - Distroless images contain only the minimum required runtime files. + - The `:nonroot` variant runs as a non-root user by default. + +2. **Set working directory** + - `WORKDIR /app` provides a predictable location for the binary. + +3. **Copy only the build artifact** + - `COPY --from=builder /app/myapp .` + - This copies only the compiled binary from the builder stage. + - No source code, no Go toolchain, no dependency caches are included. + +4. **Run the application** + - `CMD ["./myapp"]` starts the service. + - The application reads `HOST` and `PORT` environment variables: + - defaults: `HOST=0.0.0.0`, `PORT=8080` + - When running the container, port mapping must match the internal listening port (e.g., `-p 8000:8080`). + +**Output of the stage:** a minimal runtime container that executes the Go binary as a non-root user. \ No newline at end of file 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..c0f1b2ed5c 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..aed7a37918 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..ec011296fc Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..43fa976d01 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.22 \ No newline at end of file diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..1e5f3f1abe --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,277 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "strings" + "time" +) + +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime RuntimeInfo `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +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"` +} + +type RuntimeInfo struct { + UptimeSeconds int `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 ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` +} + +var startTime = time.Now().UTC() + +func main() { + host := getenv("HOST", "0.0.0.0") + port := getenv("PORT", "8080") + debug := strings.ToLower(getenv("DEBUG", "false")) == "true" + + logger := log.New(os.Stdout, "", log.LstdFlags) + if debug { + logger.SetFlags(log.LstdFlags | log.Lshortfile) + } + + mux := http.NewServeMux() + + // endpoints + mux.HandleFunc("/", mainHandler) + mux.HandleFunc("/health", healthHandler) + + // wrap with middleware: recover + logging + 404 + handler := withRecover(logger)(withLogging(logger)(withNotFound(mux))) + + addr := fmt.Sprintf("%s:%s", host, port) + logger.Printf("Application starting on http://%s\n", addr) + + // http.Server allows timeouts (good practice) + srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 5 * time.Second, + } + + if err := srv.ListenAndServe(); err != nil { + logger.Fatalf("server error: %v", err) + } +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + // will be caught by notFound wrapper, but this is extra safety + writeJSON(w, http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "Endpoint does not exist", + }) + return + } + + info := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go net/http", + }, + System: System{ + Hostname: hostname(), + Platform: runtime.GOOS, + PlatformVersion: platformVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: runtimeInfo(), + Request: requestInfo(r), + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + writeJSON(w, http.StatusOK, info) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := uptime() + resp := map[string]any{ + "status": "healthy", + "timestamp": isoUTCNow(), + "uptime_seconds": uptimeSeconds, + } + writeJSON(w, http.StatusOK, resp) +} + +func runtimeInfo() RuntimeInfo { + secs, human := uptime() + return RuntimeInfo{ + UptimeSeconds: secs, + UptimeHuman: human, + CurrentTime: isoUTCNow(), + Timezone: "UTC", + } +} + +func requestInfo(r *http.Request) RequestInfo { + ip := clientIP(r) + ua := r.Header.Get("User-Agent") + return RequestInfo{ + ClientIP: ip, + UserAgent: ua, + Method: r.Method, + Path: r.URL.Path, + } +} + +func uptime() (int, string) { + delta := time.Since(startTime) + seconds := int(delta.Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} + +func isoUTCNow() string { + // "2026-01-07T14:30:00.000Z" + return time.Now().UTC().Format("2006-01-02T15:04:05.000Z") +} + +func hostname() string { + h, err := os.Hostname() + if err != nil { + return "unknown" + } + return h +} + +func platformVersion() string { + // Best effort for Linux: /etc/os-release PRETTY_NAME (e.g., "Ubuntu 24.04.1 LTS") + if runtime.GOOS != "linux" { + return "unknown" + } + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return "unknown" + } + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "PRETTY_NAME=") { + val := strings.TrimPrefix(line, "PRETTY_NAME=") + val = strings.Trim(val, `"`) + if val != "" { + return val + } + } + } + return "unknown" +} + +func clientIP(r *http.Request) string { + // If behind proxy, you might consider X-Forwarded-For, but for lab keep it simple. + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil && host != "" { + return host + } + // fallback: may already be just an IP + return r.RemoteAddr +} + +func writeJSON(w http.ResponseWriter, statusCode int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + _ = json.NewEncoder(w).Encode(payload) +} + +func getenv(key, def string) string { + v := os.Getenv(key) + if v == "" { + return def + } + return v +} + +/* ---------------- Middleware (Best Practices) ---------------- */ + +func withLogging(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + logger.Printf("%s %s (%s) from %s in %s", + r.Method, r.URL.Path, r.Proto, r.RemoteAddr, time.Since(start)) + }) + } +} + +func withRecover(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + logger.Printf("panic recovered: %v", rec) + writeJSON(w, http.StatusInternalServerError, ErrorResponse{ + Error: "Internal Server Error", + Message: "An unexpected error occurred", + }) + } + }() + next.ServeHTTP(w, r) + }) + } +} + +func withNotFound(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Use ServeMux; if it doesn't match, it still calls handler with pattern "/" + // So we enforce our own 404 for unknown endpoints. + if r.URL.Path != "/" && r.URL.Path != "/health" { + writeJSON(w, http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "Endpoint does not exist", + }) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..44fa25304b --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,22 @@ +# 🐙 Version control +.git +.gitignore + +# 🐍 Python +__pycache__ +*.pyc +*.pyo +venv/ +.venv/ + +# 🔐 Secrets (NEVER include!) +.env +*.pem +secrets/ + +# 📝 Documentation +*.md +docs/ + +# 🧪 Tests (if not needed in container) +tests/ \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..0364cfd9a7 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . + +EXPOSE 8000 + +USER appuser + +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..2475567ab9 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,66 @@ +# devops-info-service + +## Overview +`devops-info-service` is a lightweight HTTP service built with **FastAPI** that returns comprehensive runtime and system information. It exposes: +- service metadata (name, version, description, framework), +- system details (hostname, OS/platform, architecture, CPU count, Python version), +- runtime data (uptime, current UTC time), +- request details (client IP, user-agent, method, path), +- a list of available endpoints. + +## Prerequisites +- **Python:** 3.10+ (recommended 3.11+) +- **Dependencies:** listed in `requirements.txt` + +## Installation + +``` +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Running the Application +``` +python app.py +# Or with custom config +PORT=8080 python app.py +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration + +The application is configured using environment variables. + +| Variable | Default | Description | Example | +|---------|---------|-------------|-------------| +| `HOST` | `0.0.0.0` | Host interface to bind the server to | `127.0.0.1` | +| `PORT` | `8000` | Port the server listens on | `8080` | + +# Docker + +## Building the image locally +Command pattern: +```bash +docker build -t : +``` + +## Running a container +Command pattern: +```bash +docker run --rm -p : : +``` + +## Pulling from Docker Hub +Command pattern: +```bash +docker pull /: +``` +Then run: +```bash +docker run --rm -p : /: +``` + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..8cd20e0680 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,162 @@ +""" +DevOps Info Service +Main application module +""" +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +import uvicorn +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException + +app = FastAPI() + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 8000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Application start time +start_time = datetime.now() + + +def get_service_info(): + """Get information about service.""" + logger.debug('Getting info about the service.') + return { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "FastAPI", + } + + +def get_system_info(): + """Get information about system.""" + logger.debug('Getting info about the system.') + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + } + + +def get_uptime(): + """Get uptime.""" + logger.debug('Getting uptime.') + delta = datetime.now() - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"} + + +def get_runtime_info(): + """Get information about runtime.""" + logger.debug('Getting runtime info.') + uptime = get_uptime() + uptime_seconds, uptime_human = uptime["seconds"], uptime["human"] + current_time = datetime.now(timezone.utc) + + return { + "uptime_seconds": uptime_seconds, + "uptime_human": uptime_human, + "current_time": current_time, + "timezone": "UTC", + } + + +def get_request_info(request: Request): + """Get information about request.""" + logger.debug('Getting info about request.') + return { + "client_ip": request.client.host, + "user_agent": request.headers.get("user-agent"), + "method": request.method, + "path": request.url.path, + } + + +def get_endpoints(): + """Get all existing ednpoints.""" + logger.debug('Getting list of all endpoints.') + return [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ] + + +@app.get("/", status_code=status.HTTP_200_OK) +async def root(request: Request): + """Main endpoint - service and system information.""" + logger.debug(f'Request: {request.method} {request.url.path}') + return { + "service": get_service_info(), + "system": get_system_info(), + "runtime": get_runtime_info(), + "request": get_request_info(request), + "endpoints": get_endpoints(), + } + + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health(request: Request): + """Endpoint to check health.""" + logger.debug(f'Request: {request.method} {request.url.path}') + return { + "status": "healthy", + "timestamp": datetime.now(timezone.utc), + "uptime_seconds": get_uptime()["seconds"], + } + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """Exception 404 (Not found) that endpoint does not exists.""" + if exc.status_code == 404: + return JSONResponse( + status_code=404, + content={ + "error": "Not Found", + "message": "Endpoint does not exist", + }, + ) + return JSONResponse( + status_code=exc.status_code, + content={ + "error": "HTTP Error", + "message": exc.detail if exc.detail else "Request failed", + }, + ) + + +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception): + """Exception 500 (Internal Server Error) - For any unhandled errors.""" + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }, + ) + + +if __name__ == "__main__": + # The entry point + logger.info('Application starting...') + + uvicorn.run("app:app", host=HOST, port=PORT, reload=True) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..1d43f6863d --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,341 @@ +# 1. Framework selection. +## Choice +I have chose FastAPI because I have had an experience with it and have no experience with other frameworks. + +## Comparison with Alternatives + +| Criteria | FastAPI (chosen) | Flask | Django (DRF) | +|---------|-----------------|-------|--------------| +| Primary use | APIs / microservices | Lightweight web apps & APIs | Full-stack apps & large APIs | +| Performance model | ASGI (async-ready) | WSGI (sync by default) | WSGI/ASGI (heavier stack) | +| Built-in API docs | Yes (Swagger/OpenAPI) | No (manual/add-ons) | Yes (via DRF) | +| Validation / typing | Strong (type hints + Pydantic) | Manual or extensions | Strong (serializers) | +| Boilerplate | Low | Very low | Higher | +| Learning curve | Low–medium | Low | Medium–high | +| Best fit for this lab | Excellent | Good | Overkill | + +--- + +# 2. Best Practices Applied +## Clean Code Organization + +### 1) Clear Function Names +The code uses descriptive, intention-revealing function names that clearly communicate what each block returns: + +```python +def get_service_info(): + """Get information about service.""" + ... + +def get_system_info(): + """Get information about system.""" + ... + +def get_runtime_info(): + """Get information about runtime.""" + ... + +def get_request_info(request: Request): + """Get information about request.""" + ... +``` +**Why it matters**: Clear naming improves readability, reduces the need for extra comments, and makes the code easier to maintain and extend. + +### 2) Proper imports grouping +Imports are organized by category (standard library first, then third-party libraries), which is the common Python convention: +```python +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +import uvicorn +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException +``` +**Why it matters**: Grouped imports make dependencies easier to understand at a glance, help keep the file structured, and align with typical linting rules. + +### 3) Comments only where needed +Instead of excessive inline comments, the code relies on clear names and short docstrings: +```python +""" +DevOps Info Service +Main application module +""" + +def get_uptime(): + """Get uptime.""" + ... +``` +**Why it matters**: Too many comments can become outdated. Minimal documentation plus clean naming keeps the codebase readable and accurate. + +### 4) Follow PEP 8 +The implementation follows common PEP 8 practices: +- consistent indentation and spacing, +- snake_case for variables and function names, +- configuration/constants placed near the top of the module (HOST, PORT, DEBUG), +- readable multi-line formatting for long calls: +```python +""" +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +``` +**Why it matters**: PEP 8 improves consistency, supports teamwork, and makes the code compatible with linters/formatters such as `flake8`, `ruff`, and `black`. + +## Error Handling +The service implements centralized error handling using FastAPI/Starlette exception handlers. This ensures that errors are returned in a consistent JSON format and that clients receive meaningful messages instead of raw stack traces. + +### HTTP errors (e.g., 404 Not Found) +A dedicated handler processes HTTP-related exceptions and customizes the response for missing endpoints. + +```python +from starlette.exceptions import HTTPException + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + if exc.status_code == 404: + return JSONResponse( + status_code=404, + content={ + "error": "Not Found", + "message": "Endpoint does not exist", + }, + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error": "HTTP Error", + "message": exc.detail if exc.detail else "Request failed", + }, + ) +``` +**Why it matters**: +- Provides a clear and user-friendly message for invalid routes. +- Keeps error responses consistent across the API. +- Avoids exposing internal implementation details to the client. + +### Unhandled exceptions (500 Internal Server Error) +A global handler catches any unexpected exceptions and returns a safe, standardized response. + +```python +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }, + ) +``` +**Why it matters**: +- Prevents server crashes from unhandled errors. +- Ensures clients always receive valid JSON (important for automation/scripts). +- Helps keep production behavior predictable while preserving the option to log the exception internally. + +## 3. Logging +The service includes basic logging configuration to improve observability and simplify debugging. Logs are useful both during development (troubleshooting requests and behavior) and in production (monitoring, incident investigation). + +### Logging setup +A global logging configuration is defined at startup with a consistent log format: +```python +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) +``` +**Why it matters**: +- Provides timestamps and log levels for easier troubleshooting. +- A consistent format makes logs easier to parse in log aggregators (e.g., ELK, Loki). +- Centralized config avoids inconsistent logging across modules. + +### Startup logging +The application logs an informational message when it starts: +```python +if __name__ == "__main__": + logger.info("Application starting...") + uvicorn.run("app:app", host=HOST, port=PORT, reload=True) +``` +**Why it matters**: +- Confirms that the service started successfully. +- Helps identify restarts and uptime issues. + +### Request logging (debug level) +Each endpoint logs basic request information (method and path): +```python +@app.get("/", status_code=status.HTTP_200_OK) +async def root(request: Request): + logger.debug(f"Request: {request.method} {request.url.path}") + ... +``` +**Why it matters**: +- Helps trace API usage during development. +- Useful for debugging routing problems and unexpected client behavior. + +## 4. Dependencies (requirements.txt) +The project keeps dependencies minimal and focused on what is required to run a FastAPI service in production. +### requirements.txt +```txt +fastapi==0.122.0 +uvicorn[standard]==0.38.0 +``` +**Why it matters**: +- Faster builds & simpler setup: fewer packages mean faster installation and fewer moving parts. +- Lower risk of conflicts: minimal dependencies reduce version incompatibilities and “dependency hell”. +- Better security posture: fewer third-party libraries reduce the overall attack surface. +- More predictable deployments: only installing what the service truly needs improves reproducibility across environments (local, CI, Docker, VM). + +## 5. Git Ignore (.gitignore) + +A `.gitignore` file is used to prevent committing temporary, machine-specific, or sensitive files into the repository. + +### Recommended `.gitignore` +```gitignore +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` +**Why it matters**: +- Keeps the repository clean: avoids committing generated files (`__pycache__`, build outputs, logs). +- Improves portability: prevents OS- and IDE-specific files from polluting the project and causing noisy diffs. +- Protects secrets: ensures configuration files like `.env` (which may contain API keys or credentials) are not accidentally pushed. +- Reduces merge conflicts: fewer irrelevant files tracked by Git means fewer conflicts between contributors. + +# 3. API Documentation +The service exposes two endpoints: the main information endpoint and a health check endpoint. +## Request/response examples +### GET `/` — Service and System Information +**Description:** +Returns comprehensive metadata about the service, system, runtime, request details, and available endpoints. +**Request example:** +```bash +curl -i http://127.0.0.1:8000/ +``` +**Response example (200 OK):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "FastAPI" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.13.1" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` +### GET /health — Health Check +**Description:** +Returns a simple status response to confirm the service is running.**Request example:** +```bash +curl -i http://127.0.0.1:8000/health +``` +**Response example (200 OK):** +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` +## Testing commands +### Basic tests +```bash +curl http://127.0.0.1:8000/ +curl http://127.0.0.1:8000/health +``` +### Test 404 handling (unknown endpoint) +```bash +curl -i http://127.0.0.1:8000/does-not-exist +``` +Expected response (404): +```json +{ + "error": "Not Found", + "message": "Endpoint does not exist" +} +``` + +# 4. Testing Evidence +Check screenshots. + +# 5. Challenges & Solutions +I have no problems in this lab. + +# GitHub Community +**Why Stars Matter:** + +**Discovery & Bookmarking:** +- Stars help you bookmark interesting projects for later reference +- Star count indicates project popularity and community trust +- Starred repos appear in your GitHub profile, showing your interests + +**Open Source Signal:** +- Stars encourage maintainers (shows appreciation) +- High star count attracts more contributors +- Helps projects gain visibility in GitHub search and recommendations + +**Professional Context:** +- Shows you follow best practices and quality projects +- Indicates awareness of industry tools and trends + +**Why Following Matters:** + +**Networking:** +- See what other developers are working on +- Discover new projects through their activity +- Build professional connections beyond the classroom + +**Learning:** +- Learn from others' code and commits +- See how experienced developers solve problems +- Get inspiration for your own projects + +**Collaboration:** +- Stay updated on classmates' work +- Easier to find team members for future projects +- Build a supportive learning community + +**Career Growth:** +- Follow thought leaders in your technology stack +- See trending projects in real-time +- Build visibility in the developer community \ 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..977e742f10 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,243 @@ +# Docker Best Practices Applied + +## 1. Non-root user +**What I did:** +Created a dedicated user (`appuser`) and ran the container as that user. + +**Why it matters**: +Running as root in a container increases the impact of a container escape or a vulnerable dependency. A non-root user reduces privileges and limits potential damage. + +**Dockerfile snippet:** +```dockerfile +RUN useradd --create-home --shell /bin/bash appuser +USER appuser +``` + +## 2. Layer caching +**What I did:** +Copied `requirements.txt` first and installed dependencies before copying the rest of the source code. + +**Why it matters**: +Docker caches layers. Dependencies change less frequently than application code, so separating them allows rebuilds to reuse the cached dependency layer and rebuild faster. + +**Dockerfile snippet:** +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +``` + +## 3. .dockerignore +**What I did:** +Added .dockerignore to exclude local artifacts such as venvs, caches, and IDE folders. + +**Why it matters**: +Docker sends the build context to the daemon. Excluding unnecessary files makes builds faster, reduces image bloat, and prevents leaking local secrets/files into the build. + +**.dockerignore snippet:** +```.dockerignore +# 🐙 Version control +.git +.gitignore + +# 🐍 Python +__pycache__ +*.pyc +*.pyo +venv/ +.venv/ + +# 🔐 Secrets (NEVER include!) +.env +*.pem +secrets/ + +# 📝 Documentation +*.md +docs/ + +# 🧪 Tests (if not needed in container) +tests/ +``` + +## 4. Minimal base image +**What I did:** +Used `python:3.12-slim`. + +**Why it matters**: +Smaller images generally mean fewer packages, fewer vulnerabilities, less bandwidth and faster pulls/deployments. + +**dockerfile snippet:** +```dockerfile +FROM python:3.12-slim +``` + +# Image Information & Decisions +## Base image chosen and justification (why this specific version?) +**Base image**: +`python:3.12-slim` + +**Justification**: +- Python 3.12 matches the project runtime requirements. +- slim variant keeps image smaller and reduces OS packages. +- Official Python images are widely used and well maintained. + +## Final image size and my assessment +**Image size**: +`195MB` + +**My assessment**: +Further optimization is possible (multi-stage build, wheels caching, removing build deps), but for this lab the size is acceptable. + +## Layer structure explanation +1. Base image layer (`python:3.12-slim`) +2. User creation layer +3. `WORKDIR` and `requirements.txt` copy layer +4. pip install dependencies layer (largest and most valuable for caching) +5. Application source copy layer +6. EXPOSE, USER, and CMD metadata layers + +## Optimization choices +- Used dependency-first copying to maximize caching. +- Used `--no-cache-dir` with pip to reduce layer size. +- Used slim base image to reduce OS footprint. +- Added `.dockerignore` to reduce build context. + +# Build & Run Process +## Complete terminal output from build process +```bash + docker build -t python_app:1.0 . +[+] Building 5.4s (12/12) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 277B 0.0s + => [internal] load metadata for docker.io/library/python:3.12-slim 3.7s + => [auth] library/python:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 289B 0.0s + => [1/6] FROM docker.io/library/python:3.12-slim@sha256:5e2dbd4bbdd9c0e67412aea9463906f74a22c60f89e 0.0s + => [internal] load build context 0.1s + => => transferring context: 62.28kB 0.1s + => CACHED [2/6] RUN useradd --create-home --shell /bin/bash appuser 0.0s + => CACHED [3/6] WORKDIR /app 0.0s + => CACHED [4/6] COPY requirements.txt . 0.0s + => CACHED [5/6] RUN pip install --no-cache-dir -r requirements.txt 0.0s + => [6/6] COPY app.py . 0.8s + => exporting to image 0.4s + => => exporting layers 0.3s + => => writing image sha256:a3d1dd41a468a1bb53d02edd846964c240eb160f49fd28e9f6ad90fc15677c52 0.0s + => => naming to docker.io/library/python_app:1.0 0.0s + +View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/djiu836gkk9g7syuakivz5ynt +``` + +## Terminal output showing container running +```bash + docker run --rm -p 8000:8000 python_app:1.0 +2026-01-31 18:29:56,977 - __main__ - INFO - Application starting... +INFO: Will watch for changes in these directories: ['/app'] +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +INFO: Started reloader process [1] using WatchFiles +INFO: Started server process [8] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +## Terminal output from testing endpoints +```bash +curl http://localhost:8000/ + + StatusCode : 200 +StatusDescription : OK +Content : {"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps cours + e info service","framework":"FastAPI"},"system":{"hostname":"3a77a23940f7","platform": + "Linux","platform_version":"... +RawContent : HTTP/1.1 200 OK + Content-Length: 755 + Content-Type: application/json + Date: Sat, 31 Jan 2026 18:31:18 GMT + Server: uvicorn + +Forms : {} +Headers : {[Content-Length, 755], [Content-Type, application/json], [Date, Sat, 31 Jan 2026 18:3 + 1:18 GMT], [Server, uvicorn]} Images : {} InputFields : {} Links : {} +ParsedHtml : mshtml.HTMLDocumentClass +RawContentLength : 755 +``` + +```bash +curl http://localhost:8000/health + + +StatusCode : 200 +StatusDescription : OK +Content : {"status":"healthy","timestamp":"2026-01-31T18:31:26.924513+00:00","uptime_seconds":89 + } +RawContent : HTTP/1.1 200 OK + Content-Length: 87 + Content-Type: application/json + Date: Sat, 31 Jan 2026 18:31:26 GMT + Server: uvicorn + + {"status":"healthy","timestamp":"2026-01-31T18:31:26.924513+00:00","uptime_... +Forms : {} +Headers : {[Content-Length, 87], [Content-Type, application/json], [Date, Sat, 31 Jan 2026 18:31 + :26 GMT], [Server, uvicorn]} +Images : {} +InputFields : {} +Links : {} +ParsedHtml : mshtml.HTMLDocumentClass +RawContentLength : 87 +``` + +## Terminal output showing successful push: +```bash +docker push newspec/python_app:1.0 + +The push refers to repository [docker.io/newspec/python_app] +8422fdf98022: Pushed +56a7b3684a2c: Pushed +410b7369101c: Pushed +4e7298e95b69: Pushed +b68196304589: Pushed +343fbb74dfa7: Pushed +cfdc6d123592: Pushed +ff565e4de379: Pushed +e50a58335e13: Pushed +1.0: digest: sha256:9084f1513bc5af085a268ee9e8b165af82f7224e442da0790cf81f07b67ab10e size: 2203 + +``` + +## Docker Hub repository URL +`https://hub.docker.com/repository/docker/newspec/python_app` + +## My tagging strategy +`:1.0`(major/minor) + +# Technical analysis +## Why does your Dockerfile work the way it does? +- The image is based on a minimal Python runtime. +- Dependencies are installed before application code to leverage Docker layer caching. +- The app runs as a non-root user for better security. + +## What would happen if you changed the layer order? +If the Dockerfile copied the entire project (`COPY . .`) before installing requirements, then any code change would invalidate the cache for the dependency install layer. That would force `pip install` to run again on every rebuild, making builds much slower. + +## What security considerations did you implement? +- Non-root execution reduces privilege. +- Smaller base image reduces attack surface. +- `.dockerignore` prevents accidentally shipping local files (including potential secrets) into the image. + +## How does .dockerignore improve your build? +- Reduces build context size (faster build, less IO). +- Avoids copying local venvs and cache files into the image. +- Prevents leaking IDE configs, git history, logs, and other irrelevant files. + +## Challenges & Solutions +# 1. “I cannot open my application” after `docker run` +**Fix**: Published the port with `-p 8000:8000` + +# What I Learned +- Containers need explicit port publishing to be accessible from the host. +- Layer ordering dramatically affects build speed due to caching. +- Running as non-root is a simple but important security improvement. +- .dockerignore is crucial to keep images clean and builds fast. \ No newline at end of file diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..2b5ad4b17f 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..321da88450 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..bd76d10670 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..94a41a1968 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.122.0 +uvicorn[standard]==0.38.0 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2