diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..cfb650bdca --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,43 @@ +# Go artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.o +*.prof +*.trace +vendor/ +__pycache__/ + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Temporary files +*.tmp +*.log +tmp/ +logs/ diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..2a3e22eea0 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . + +# Stage 2: Minimal runtime +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..292daf65ab --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,97 @@ +# DevOps Info Service (Go) +## Overview +A Go-based web service designed to furnish details about itself and its operational environment. This compiled version of the service is optimized for containerization and multi-stage Docker builds, offering improved performance and smaller deployment footprints compared to interpreted languages. + +## Prerequisites +- Go 1.21 or higher +- Git (for dependency management) + +## Installation +1. Clone repository: + +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_go + +# Download dependencies +go mod download + +# Build the application +go build -o devops-info-service +``` + +## Running the Application +```bash +# Default configuration +./devops-info-service + +# With custom port +PORT=8080 ./devops-info-service + +# With custom port and host +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly without building +go run main.go +Building for Different Platforms +bash +# Build for current platform +go build -o devops-info-service +``` + +## API Endpoints +### GET / +Return comprehensive service and system information: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "my-laptop", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "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:12345", + "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 +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +## Configuration +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | \ 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..b42a88d6a8 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,18 @@ +# Language Justification +## My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. \ No newline at end of file diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..4b93018d26 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,293 @@ +# Lab 1 — DevOps Info Service: Web Application Development on Go +## Language Selection +### My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. + +## Best Practices Applied +### 1. Clean Code Organization +```go +// Clear imports grouping +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Descriptive function names +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} +``` +**Importance:** Clean organization with clear separation of concerns makes the code maintainable and testable. Following Go conventions (camelCase, exported/unexported identifiers) ensures consistency. + +### 2. Comprehensive Error Handling +```go +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} +``` +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```go +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + log.Printf("Starting DevOps Info Service on %s:%s", host, port) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), nil)) +} +``` +**Importance:** Logging provides visibility into application behavior and startup configuration. The standard log package is sufficient for this simple service, though larger applications might use structured logging libraries. + +### 4. Configuration via Environment Variables +```go +port := os.Getenv("PORT") +if port == "" { + port = "5000" +} +``` +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments without recompilation. + +### 5. Minimal Dependencies +```go +// go.mod - only Go standard library is used +module devops-info-service + +go 1.21 +``` +**Importance:** Using only the standard library eliminates dependency management overhead and reduces security vulnerabilities. The resulting binary is self-contained. + +### 6. Static Typing and Compile-Time Safety +```go +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} +``` +**Importance:** Static typing catches many errors at compile time rather than runtime, improving reliability. Struct tags provide clear mapping between Go structs and JSON output. + +## API Documentation +### Endpoint 1: GET / +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:8080/ +``` +**Response (example):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "ubuntu-dev", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "runtime": { + "uptime_seconds": 125, + "uptime_human": "0 hours, 2 minutes", + "current_time": "2026-01-27T10:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1:54321", + "user_agent": "curl/7.88.1", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` +### Endpoint 2: GET /health +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:8080/health +``` +**Response:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-27T10:30:00.000Z", + "uptime_seconds": 125 +} +``` +### Testing Commands +1. **Basic endpoint test:** + +```bash +curl http://localhost:8080/ +``` + +2. **Health check test:** +```bash +curl http://localhost:8080/health +``` +3. **Pretty-printed output:** +```bash +curl http://localhost:8080/ | jq . +``` +4. **Custom configuration:** +```bash +PORT=8080 ./devops-info-service +curl http://localhost:8080/health +``` + +5. **Error simulation:** +```bash +curl -v http://localhost:8080/nonexistent +# Should return 404 error +``` +## Build Process +### Compilation +```bash +# Initialize Go module +go mod init devops-info-service + +# Build standard binary +go build -o devops-info-service +``` +###Running +```bash +# Run the compiled binary +./devops-info-service + +# Run with custom configuration +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly (without building) +go run main.go +``` +## Testing Evidence +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + +## Challenges & Solutions +### Challenge 1: HTTP Handler Registration +**Problem:** Go's http.HandleFunc doesn't allow multiple registrations for the same path, unlike Flask's decorator pattern. + +**Solution:** Implemented a routing check within the main handler: +```go +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + // ... rest of handler +} +``` +### Challenge 2: Platform Version Detection +**Problem:** Go's standard library doesn't provide detailed OS version information like Python's platform.release(). + +**Solution:** Created a simple mapping function: +```go +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} +``` +### Challenge 3: Uptime Formatting +**Problem:** Converting seconds to human-readable format required manual calculation. + +**Solution:** Implemented a reusable function: +```go +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} +``` +### Challenge 4: JSON Serialization +**Problem:** Ensuring proper JSON field naming and null handling. + +**Solution:** Used struct tags and proper initialization: +```go +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + // ... other fields +} +``` + diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..9cec8ec40f --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,252 @@ +# Lab 2 Bonus — Multi-Stage Docker Build for Go Application + +## Multi-Stage Strategy + +### Stage 1: Builder +```dockerfile +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . +``` + +**Purpose:** Complete build environment containing: +- Go 1.21 compiler and standard library +- Git for dependency management +- All source code and build tools +- Temporary workspace for compilation + +### Stage 2: Runtime +```dockerfile +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] +``` + +**Purpose:** Absolute minimal runtime environment containing only: +- Statically compiled Go binary +- CA certificates for HTTPS/TLS support +- No operating system, shell, or package manager + +## Size Comparison + +### Image Size Analysis +| Component | Size | Contents | +|-----------|------|----------| +| **Builder Stage** | ~350MB | Full Go 1.21 SDK + Alpine + build tools | +| **Runtime Stage** | **7.16MB** | Static binary + CA certificates | +| **Size Reduction** | **~98%** | 350MB → 7.16MB | + +### Detailed Breakdown +- **Builder image:** Uses `golang:1.21-alpine` (~350MB with build tools) +- **Final image:** Uses `scratch` (0MB base) + binary + certificates +- **Binary size:** ~7.16MB (statically compiled Go application + CA certificate) + +## Why Multi-Stage Builds Matter for Compiled Languages + +### 1. Drastic Size Reduction +Compiled languages like Go have a fundamental advantage: they produce standalone binaries. Multi-stage builds leverage this by: +- **Separating concerns:** Build environment (large) vs runtime (minimal) +- **Eliminating build tools:** Compiler, linker, SDK removed from production +- **Removing dependencies:** Only the binary and absolute essentials remain + +### 2. Enhanced Security +```dockerfile +FROM scratch # No operating system, no shell, no package manager +``` + +**Security benefits:** +- **No shell access:** Cannot spawn shells even if compromised +- **Immutable runtime:** Binary cannot be modified without rebuilding +- **Principle of least privilege:** Only what's absolutely necessary + +### 3. Production Performance +- **Faster deployment:** Smaller images download quicker +- **Reduced storage:** Less disk space required across development/staging/production +- **Lower memory footprint:** Minimal OS overhead +- **Quick startup:** No initialization of unused services + +## Terminal Output + +### Build Process Output +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker build -t devops-info-service:go . +[+] Building 5.1s (14/14) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.00kB 0.0s + => [internal] load metadata for docker.io/library/golang:1.21-alpine 0.7s + => [internal] load .dockerignore 0.0s + => => transferring context: 359B 0.0s + => [builder 1/7] FROM docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => => resolve docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => [internal] load build context 0.0s + => => transferring context: 54B 0.0s + => CACHED [builder 2/7] RUN apk add --no-cache git ca-certificates 0.0s + => CACHED [builder 3/7] WORKDIR /app 0.0s + => CACHED [builder 4/7] COPY go.mod ./ 0.0s + => CACHED [builder 5/7] RUN go mod download 0.0s + => CACHED [builder 6/7] COPY . . 0.0s + => [builder 7/7] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-w -s -extldflags '-static'" -o devops-info-service . 3.9s + => [stage-1 1/2] COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 0.0s + => [stage-1 2/2] COPY --from=builder /app/devops-info-service /app/devops-info-service 0.0s + => exporting to image 0.4s + => => exporting layers 0.3s + => => exporting manifest sha256:4e1764a6a80bfc8666f97655b398a31766e6ef0b7e113b73651ca44601a369e5 0.0s + => => exporting config sha256:07f16274913e502fcb7339751611566f733555d340310768666604f24375006b 0.0s + => => exporting attestation manifest sha256:594b42605a1d485598b5ca39be853c1e154958e927a235027e0ca1b5fce2efa9 0.0s + => => exporting manifest list sha256:c5f945015fb0dfd3f762151c64c5393121944441d08e191e5a7533aadcf4f4eb 0.0s + => => naming to docker.io/library/devops-info-service:go 0.0s + => => unpacking to docker.io/library/devops-info-service:go +``` + +### Image Size Verification +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker images | grep devops-info-service +WARNING: This output is designed for human readability. For machine-readable output, please use --format. +devops-info-service:go c5f945015fb0 7.16MB 2.2MB U +devops-info-service:python 4b08b6e2f063 199MB 48.1MB +s3rap1s/devops-info-service:python ef074c1a118d 199MB 48.1MB + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker history devops-info-service:go +IMAGE CREATED CREATED BY SIZE COMMENT +c5f945015fb0 14 minutes ago CMD ["/app/devops-info-service"] 0B buildkit.dockerfile.v0 + 14 minutes ago ENV PORT=5000 0B buildkit.dockerfile.v0 + 14 minutes ago ENV HOST=0.0.0.0 0B buildkit.dockerfile.v0 + 14 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0 + 14 minutes ago COPY /app/devops-info-service /app/devops-in… 4.72MB buildkit.dockerfile.v0 + 14 minutes ago COPY /etc/ssl/certs/ca-certificates.crt /etc… 238kB buildkit.dockerfile.v0 +``` + +### Runtime Testing +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker run -d --name devops-go -p 5000:5000 devops-info-service:go +0d2cb36ff83b03fca8090248aa3a6fe1beba1e879617a8dd2e5a9c3a588e8c1c + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"Go"},"system":{"hostname":"0d2cb36ff83b","platform":"linux","platform_version":"Linux Kernel","architecture":"amd64","cpu_count":12,"go_version":"go1.21.13"},"runtime":{"uptime_seconds":17,"uptime_human":"0 hours, 0 minutes","current_time":"2026-01-31T20:20:36Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1:50750","user_agent":"curl/8.18.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T20:21:20Z","uptime_seconds":61} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker logs devops-go +2026/01/31 20:20:19 Starting DevOps Info Service on 0.0.0.0:5000 +2026/01/31 20:20:34 Health check from 172.17.0.1:50742 +2026/01/31 20:20:36 Request: GET / from 172.17.0.1:50750 +2026/01/31 20:21:13 404 Not Found: /helath +2026/01/31 20:21:20 Health check from 172.17.0.1:38286 +``` + +## Technical Explanation of Each Stage + +### Stage 1: Builder (`golang:1.21-alpine`) +**Purpose:** Provide complete compilation environment + +**Key operations:** +1. **Base setup:** Alpine Linux with Go 1.21 toolchain +2. **Dependencies:** Install git and CA certificates +3. **Module management:** Download Go dependencies with caching +4. **Compilation:** Build optimized static binary with: + - `CGO_ENABLED=0`: Disable CGO for pure Go static binary + - `-ldflags="-w -s"`: Strip debug symbols and DWARF tables + - `-extldflags '-static'`: Force static linking + - `-a`: Force rebuilding of packages + +**Output:** `/app/devops-info-service` (6.9MB static binary) + +### Stage 2: Runtime (`scratch`) +**Purpose:** Provide minimal production runtime + +**Key operations:** +1. **Base image:** `scratch` (empty filesystem) +2. **Binary copy:** Transfer compiled binary from builder +3. **Certificates:** Copy CA certificates for TLS/HTTPS support +4. **Configuration:** Set environment variables and expose port + +**Output:** Production-ready container image (7.16MB) + +## Security Benefits of Smaller Images + +### Specific Security Advantages +1. **No shell:** Cannot execute arbitrary commands or spawn shells +2. **Immutable filesystem:** Only the binary exists, cannot be modified +3. **Minimal CVE surface:** No packages = no vulnerabilities to patch +4. **Isolated execution:** Runs as PID 1 with no background services +5. **Resource limits:** Minimal memory/cpu usage reduces DoS impact + +## Why FROM scratch? Trade-offs and Decisions + +### Why `scratch` Was Chosen +```dockerfile +FROM scratch # Instead of alpine, distroless, or other minimal bases +``` + +**Advantages:** +1. **Absolute minimalism:** 0MB base, only binary + certs +2. **Maximum security:** No OS, no shell, no utilities +3. **Great for static binaries:** Go compiles to fully static executables + +### Trade-offs Considered +| Base Image | Size | Pros | Cons | Decision | +|------------|------|------|------|----------| +| **scratch** | 0MB | Max security, minimal size | No debugging tools, no shell | ✅ **Chosen** | +| **alpine** | 5.5MB | Shell for debugging, small | Larger, more attack surface | Rejected | +| **distroless** | 20MB | Secure | Much larger than scratch | Rejected | + +## Analysis of Size Reduction and Why It Matters + +### Why Size Reduction Matters +1. **Cost efficiency:** 98% reduction in storage and bandwidth costs +2. **Deployment speed:** Images deploy in seconds instead of minutes +3. **Developer productivity:** Faster CI/CD pipeline execution +4. **Environmental impact:** Less energy for storage and transfer +5. **Edge computing:** Suitable for resource-constrained environments + +## Challenges and Solutions + +### Challenge: Certificate Management with `scratch` +**Problem:** `scratch` has no CA certificates, breaking HTTPS calls from the application. + +**Solution:** Copy certificates from builder stage: +```dockerfile +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +``` + +## What I Learned + +1. **Multi-stage builds** are transformative for compiled languages, enabling near-zero runtime overhead +2. **Static compilation** is powerful but requires careful dependency management +3. **Security through minimalism** is achievable with `scratch` base images +4. **Trade-offs exist** between debuggability and security/minimalism 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..6acabfa3cf 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..f905599cf0 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..f4dd6737db 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..307ce0d1c5 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.21 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..7b0e355b6c --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Data structures +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `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 Runtime struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type Request 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 HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// Global variables +var startTime time.Time + +func init() { + startTime = time.Now() +} + +// Helper functions +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} + +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} + +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} + +func getCurrentTime() string { + return time.Now().UTC().Format(time.RFC3339) +} + +// HTTP handlers +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + + systemInfo := getSystemInfo() + uptimeSeconds, uptimeHuman := getUptime() + + response := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go", + }, + System: systemInfo, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: getCurrentTime(), + Timezone: "UTC", + }, + Request: Request{ + ClientIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := HealthResponse{ + Status: "healthy", + Timestamp: getCurrentTime(), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Health check from %s", r.RemoteAddr) +} + +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} + +// Main function +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + // Setup HTTP routes + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + // Start server + addr := fmt.Sprintf("%s:%s", host, port) + log.Printf("Starting DevOps Info Service on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..df2ec2cfb1 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.so +.Python +venv/ +env/ +.venv/ +.env +*.log + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Tests +tests/ +test*.py + +# Docker +Dockerfile* +docker-compose* +.dockerignore \ 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..3de1bb9253 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,56 @@ +# Dockerfile for Python Application with Virtual Environment +FROM python:3.13-slim AS builder + +# Installing system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Installing the working directory +WORKDIR /app + +# Creating a virtual environment +RUN python -m venv /opt/venv + +# Activating the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Copying requirements file +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Final stage: Copying the virtual environment and application code +FROM python:3.13-slim + +# Creating non-root user and group +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +# Setting the working directory +WORKDIR /app + +# Copying the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copying the application code +COPY . . + +# Setting permissions for the application directory +RUN chown -R appuser:appgroup /app && chmod -R 755 /app + +# Switching to non-root user +USER appuser + +# Setting the PATH to include the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Opening the port for the application +EXPOSE 5000 + +# Setting environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 +ENV PYTHONUNBUFFERED=1 + +# Running the application +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..eb7a9dd242 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,141 @@ +# DevOps Info Service + +## Overview +A Python-based web service designed to furnish details about itself and its operational environment. This service serves as a foundation for subsequent experiments in containerization, continuous integration and continuous deployment (CI/CD), monitoring, and deployment processes. + +## Prerequisites +- Python 3.11 or higher +- pip (Python Package manager) + +## Installation +1. Clone repository: +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_python + +# Create virtual environment +python3 -m venv venv +source venv/bin/activate # on Linux / macOs or .\venv\Scripts\Activate.ps1 on windows + +# Install dependencies +pip install -r requirements.txt +``` + +## Running the Application + +```bash +# Default configuration +python app.py + +# With custom port +PORT=8080 python app.py + +# With custom port and host +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## Docker + +This application is containerized and available as a Docker image. + +### Building the Image Locally + +```bash +docker build -t devops-info-service:latest . +``` + +### Running a Container + +```bash +# Run with default port mapping +docker run -d -p 5000:5000 devops-info-service:latest + +# Run with custom port +docker run -d -p 8080:5000 devops-info-service:latest + +# Run with environment variables +docker run -d -p 3000:3000 -e PORT=3000 -e HOST=0.0.0.0 devops-info-service:latest +``` + +### Pulling from Docker Hub + +```bash +# Pull the latest version +docker pull your-username/devops-info-service:latest + +# Run pulled image +docker run -d -p 5000:5000 your-username/devops-info-service:latest +``` + +### Environment Variables in Docker +When running in Docker, you can pass environment variables using the `-e` flag: + +```bash +docker run -d -p 5000:5000 \ + -e HOST=0.0.0.0 \ + -e PORT=5000 \ + -e DEBUG=false \ + devops-info-service:latest +``` + +## API Endpoints + +### `GET /` +Return comprehensive service and system information: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "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` + +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + + +## Configuration + +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | +| `DEBUG` | `false` | Enable debug mode | diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..1157cccc35 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,126 @@ +""" +DevOps Info Service +Main application module +""" +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Flask app initialization +app = Flask(__name__) + +# Configuration via environment variables +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +# Application startup time +START_TIME = datetime.now(timezone.utc) + +# Setting up logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def get_system_info(): + """Collecting information about the system. + + Returns: + dict: System configuration + """ + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'platform_version': platform.release(), + 'architecture': platform.machine(), + 'cpu_count': os.cpu_count() or 0, + 'python_version': platform.python_version() + } + +def get_uptime(): + """Calculating the running time of the application. + + Returns: + dict: Uptime in seconds and human-readable format + """ + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + 'seconds': seconds, + 'human': f"{hours} hours, {minutes} minutes" + } + +@app.route('/') +def index(): + """The main endpoint - Information about the service and the system.""" + client_ip = request.remote_addr + user_agent = request.headers.get('User-Agent', 'Unknown') + + # Forming a response + response = { + 'service': { + 'name': 'devops-info-service', + 'version': '1.0.0', + 'description': 'DevOps course info service', + 'framework': 'Flask' + }, + 'system': get_system_info(), + 'runtime': { + 'uptime_seconds': get_uptime()['seconds'], + 'uptime_human': get_uptime()['human'], + 'current_time': datetime.now(timezone.utc).isoformat(), + 'timezone': 'UTC' + }, + 'request': { + 'client_ip': client_ip, + 'user_agent': user_agent, + 'method': request.method, + 'path': request.path + }, + 'endpoints': [ + {'path': '/', 'method': 'GET', 'description': 'Service information'}, + {'path': '/health', 'method': 'GET', 'description': 'Health check'} + ] + } + logger.info(f"Request: {request.method} {request.path} from {client_ip}") + return jsonify(response) + +@app.route('/health') +def health(): + """Endpoint for health check.""" + response = { + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_uptime()['seconds'] + } + logger.debug(f"Health check: {response}") + return jsonify(response), 200 + +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 + +if __name__ == '__main__': + logger.info(f'Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})') + app.run(host=HOST, port=PORT, debug=DEBUG) \ No newline at end of file diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..90f920df57 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,271 @@ +# Lab 1 — DevOps Info Service: Web Application Development + +## Framework Selection + +### My Choice: Flask + +I selected **Flask** as the web framework for this DevOps Info Service. Here's why: + +**Comparison Table:** +| Criteria | Flask | FastAPI | Django | +|----------|-------|---------|--------| +| Learning Curve | Very low | Moderate | Steep | +| Development Speed | High | High | Medium | +| Built-in Features | Minimal | Moderate | Extensive | +| Auto-documentation | Requires extensions | Built-in (OpenAPI) | Requires extensions | +| Performance | Good | Excellent (async) | Good | +| Complexity | Low | Medium | High | +| **Choice for Lab 1** | **✓** | | | + +**Justification:** +Flask is a lightweight, minimalistic framework that perfectly suits our simple service with only two endpoints. For a DevOps monitoring tool foundation, we don't need the complexity of Django or the async capabilities of FastAPI yet. Flask allows rapid development with clean, understandable code, making it ideal for this educational project. Its simplicity aligns with the Unix philosophy of "do one thing well" - in this case, serve system information via HTTP. + +## Best Practices Applied + +### 1. Clean Code Organization +```python +# Clear imports grouping +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Descriptive function names with docstrings +def get_system_info(): + """Collecting information about the system. + Returns: + dict: System configuration + """ + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + # ... more fields + } +``` + +**Importance:** Clean organization makes code maintainable, readable, and easier to debug. Following PEP 8 ensures consistency across Python projects. + +### 2. Comprehensive Error Handling +```python +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 +``` + +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info(f'Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})') +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +``` + +**Importance:** Logging provides visibility into application behavior, helps with debugging in production, and allows monitoring of API usage patterns. + +### 4. Configuration via Environment Variables +```python +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +``` + +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments (development, testing, production) without code changes. + +### 5. Version-Pinned Dependencies +```txt +# Web framework +Flask==3.1.0 + +# Virtual environment for python +python-dotenv==1.0.1 +``` + +**Importance:** Pinning exact versions ensures consistent behavior across all deployments and prevents "works on my machine" issues due to dependency version mismatches. + +### 6. Git Ignore for Development Artifacts +```gitignore +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +**Importance:** Prevents accidental commits of generated files, virtual environments, and sensitive data, keeping the repository clean and focused on source code. + +## API Documentation + +### Endpoint 1: `GET /` + +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:5000/ +``` + +**Response (example):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "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"} + ] +} +``` + +### Endpoint 2: `GET /health` + +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:5000/health +``` + +**Response (example):** +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +### Testing Commands + +1. **Basic endpoint test:** + ```bash + curl http://localhost:5000/ + ``` + +2. **Health check test:** + ```bash + curl http://localhost:5000/health + ``` + +3. **Pretty-printed output:** + ```bash + curl http://localhost:5000/ | jq . + ``` + +4. **Custom configuration:** + ```bash + PORT=8080 python app.py + curl http://localhost:8080/health + ``` + +5. **Error simulation:** + ```bash + curl http://localhost:5000/nonexistent + # Should return 404 error + ``` + +## Testing Evidence + +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + + +## Challenges & Solutions + +### Challenge 1: Timezone-Aware Timestamps +**Problem:** `datetime.now()` without timezone creates naive datetime objects, which can cause issues with serialization and timezone calculations. + +**Solution:** Used `timezone.utc` consistently: +```python +from datetime import datetime, timezone +START_TIME = datetime.now(timezone.utc) +# ... +datetime.now(timezone.utc).isoformat() +``` + +### Challenge 2: Logging Configuration +**Problem:** Determining the appropriate log level and format for different types of messages. + +**Solution:** Configured logging with INFO level for normal operations, DEBUG for health checks, and WARNING/ERROR for error handlers: +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +logger.debug(f"Health check: {response}") +logger.warning(f"404 Not Found: {request.path}") +``` + +### Challenge 3: CPU Count Handling +**Problem:** `os.cpu_count()` can return None on some systems or when the count cannot be determined. + +**Solution:** Added a fallback value: +```python +'cpu_count': os.cpu_count() or 0 +``` + +## GitHub Community +### Why starring repositories matters in open source: +Starring repositories serves as both a bookmarking tool for personal reference and a public endorsement that helps projects gain visibility, attracting more contributors and showing appreciation to maintainers for their work. + +### How following developers helps in team projects and professional growth: +Following developers enables you to stay updated on their projects and insights, fostering collaboration and knowledge sharing that accelerates team productivity and your own skill development in the tech 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..aad9c97090 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,213 @@ +# Lab 2 — Containerization with Docker + +## Docker Best Practices Applied + +### 1. Multi-Stage Build +**Implementation:** +```dockerfile +FROM python:3.13-slim AS builder +# ... build stage with dependencies +FROM python:3.13-slim +COPY --from=builder /opt/venv /opt/venv +``` + +**Importance:** Separates the build environment from the runtime environment, reducing the final image size by excluding build tools and intermediate files. + +### 2. Non-Root User +**Implementation:** +```dockerfile +RUN groupadd -r appgroup && useradd -r -g appgroup appuser +USER appuser +``` + +**Importance:** Enhances security by following the principle of least privilege, minimizing potential damage if the container is compromised. + +### 3. Layer Caching Optimization +**Implementation:** +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +``` + +**Importance:** Docker caches layers. By copying requirements.txt first and installing dependencies, this layer is cached and only rebuilt when requirements change, speeding up subsequent builds. + +### 4. .dockerignore File +**Implementation:** Created a comprehensive `.dockerignore` file to exclude unnecessary files (development artifacts, IDE files, git, etc.). + +**Importance:** Reduces build context size, speeding up the build process and preventing sensitive or irrelevant files from being included. + +### 5. Specific Base Image Version +**Implementation:** `python:3.13-slim` (instead of `python:latest` or `python:3.13`) + +**Importance:** Ensures reproducible builds and avoids unexpected changes from base image updates. + +### 6. Clean Package Installation +**Implementation:** +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Importance:** The `--no-cache-dir` flag prevents pip from caching packages, reducing image size. + +### 7. System Dependency Cleanup +**Implementation:** +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* +``` + +**Importance:** Removes the package lists after installation, reducing image size and keeping the image clean. + +### 8. Virtual Environment Isolation +**Implementation:** +```dockerfile +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +``` + +**Importance:** Isolates application dependencies from the system Python, avoiding conflicts and making the environment reproducible. + +## Image Information & Decisions + +### Base Image Choice +**Selected:** `python:3.13-slim` + +**Justification:** +- **slim variant** provides a minimal Python runtime without unnesecary extra tools +- **Specific version (3.13)** ensures reproducibility and avoids breaking changes from future updates +- **Alternative considered:** `python:3.13-alpine` (about 45MB) was rejected due to potential compatibility issues with some Python packages that require compiled binaries. + +### Final Image Size +- **Final image size:** ~199MB + +**Assessment:** The image size is reasonable for a Python application. The multi-stage build helps keep it minimal by excluding build tools. Further reduction could be achieved by using Alpine, but at the cost of potential compatibility issues. + +### Layer Structure +The layer structure (from bottom to top): +1. **Base image layer:** `python:3.13-slim` +2. **System dependencies layer:** Installs gcc (builder stage) +3. **Python dependencies layer:** Creates virtual environment and installs packages (cached separately) +4. **Application code layer:** Copies the rest of the application +5. **Configuration layer:** Sets permissions, user, environment variables, and command + +### Optimization Choices Made +1. **Multi-stage build:** Separates build and runtime, removing build tools from final image. +2. **Dependency layer caching:** Requirements are installed in a separate layer that caches well. +3. **Cleanup:** Removal of apt lists and pip cache. +4. **Non-root user:** Added for security without significant overhead. +5. **Virtual environment:** Ensures dependency isolation and easier path management. + +## Build & Run Process + +### Complete Terminal Output from Build Process +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker build -t devops-info-service:python . +[+] Building 2.1s (17/17) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.41kB 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 1.9s + => [auth] library/python:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 321B 0.0s + => [internal] load build context 0.0s + => => transferring context: 63B 0.0s + => [builder 1/6] FROM docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => => resolve docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => CACHED [stage-1 2/6] RUN groupadd -r appgroup && useradd -r -g appgroup appuser 0.0s + => CACHED [stage-1 3/6] WORKDIR /app 0.0s + => CACHED [builder 2/6] RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* 0.0s + => CACHED [builder 3/6] WORKDIR /app 0.0s + => CACHED [builder 4/6] RUN python -m venv /opt/venv 0.0s + => CACHED [builder 5/6] COPY requirements.txt . 0.0s + => CACHED [builder 6/6] RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt 0.0s + => CACHED [stage-1 4/6] COPY --from=builder /opt/venv /opt/venv 0.0s + => CACHED [stage-1 5/6] COPY . . 0.0s + => CACHED [stage-1 6/6] RUN chown -R appuser:appgroup /app && chmod -R 755 /app 0.0s + => exporting to image 0.1s + => => exporting layers 0.0s + => => exporting manifest sha256:d9c4d5bbff6c71a63a4664b6176a7cf8d5738ea116827f910b356d290148a06f 0.0s + => => exporting config sha256:0cea5c6e8fea36e6da7112c67af628d9a5ecaca41edfd9f12b32a6ebf2f6c9b2 0.0s + => => exporting attestation manifest sha256:45c2bd60bc20c64827da237a0a245707051321a55ecbac6b03d1001102cc86d2 0.0s + => => exporting manifest list sha256:4b08b6e2f06333a4d7781a83bcebcdb3303c99ef310af40ae3e0e85e2a020d3e 0.0s + => => naming to docker.io/library/devops-info-service:python 0.0s + => => unpacking to docker.io/library/devops-info-service:python +``` + +### Terminal Output Showing Container Running +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker run -d --name devops-python -p 5000:5000 devops-info-service:python +234bff345b8f2c930681218fd9536b405c131b375a4d382a0b28a4f77d067b2c + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker logs devops-python +2026-01-31 18:45:04,632 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:5000 (debug=False) + * Serving Flask app 'app' + * Debug mode: off +2026-01-31 18:45:04,639 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +2026-01-31 18:45:04,639 - werkzeug - INFO - Press CTRL+C to quit +``` + +### Terminal Output from Testing Endpoints +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/ +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"172.17.0.1","method":"GET","path":"/","user_agent":"curl/8.18.0"},"runtime":{"current_time":"2026-01-31T18:45:48.101588+00:00","timezone":"UTC","uptime_human":"0 hours, 0 minutes","uptime_seconds":43},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":12,"hostname":"234bff345b8f","platform":"Linux","platform_version":"6.18.3-arch1-1","python_version":"3.13.11"}} + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T18:45:54.984116+00:00","uptime_seconds":50} +``` + +### Docker Hub Repository URL +**URL:** https://hub.docker.com/repository/docker/s3rap1s/devops-info-service/general + +## Technical Analysis + +### Why Does Your Dockerfile Work the Way It Does? +The Dockerfile uses a multi-stage build to separate concerns: +1. **Builder stage:** Installs system dependencies and Python packages in a virtual environment. +2. **Runtime stage:** Copies only the virtual environment and application code, then sets up a secure non-root user. + +This approach ensures that the final image contains only what's necessary to run the application, improving security and reducing size. + +### What Would Happen If You Changed the Layer Order? +If the layer order were changed, then every time any file in the project changes, the `COPY` layer would be invalidated, causing the `RUN` layer to also be invalidated (since Docker caches layers based on the previous layer's hash). This would result in a full reinstallation of dependencies on every code change, significantly slowing down builds. + +### What Security Considerations Did You Implement? +1. **Non-root user:** The application runs as a dedicated user with minimal privileges. +2. **Minimal base image:** The `slim` variant reduces attack surface. +3. **Virtual environment isolation:** Prevents dependency conflicts and limits access. +4. **No unnecessary services:** Only the Python application runs in the container. +5. **Cleanup of package lists:** Removes sensitive data and reduces image size. +6. **Explicit port exposure:** Only port 5000 is exposed. + +### How Does .dockerignore Improve Your Build? +The `.dockerignore` file excludes: +- Development artifacts (`.git`, `__pycache__`, `.venv`) +- IDE files (`.vscode`, `.idea`) +- Logs and temporary files +- Documentation and tests (not needed at runtime) + +This reduces the build context sent to the Docker daemon, resulting in: +- **Faster build** - smaller context to transfer +- **Smaller image sizes** - unnecessary files aren't included +- **Improved security** - sensitive files like secrets aren't accidentally included + +## Challenges & Solutions + +### Challenge: Port Configuration Inside Container +**Problem:** The application inside the container was binding to `localhost`, making it inaccessible from the host. + +**Solution:** Set the `HOST` environment variable to `0.0.0.0` in the Dockerfile to bind to all interfaces: +```dockerfile +ENV HOST=0.0.0.0 +``` + +## What I Learned + +1. **Layer caching** is crucial for efficient Docker builds +2. **Security** must be considered from the start +3. **`.dockerignore`** is as important as `.gitignore` for Docker projects, affecting both performance and security +4. **Reproducibility** requires pinning specific versions of base images and dependencies 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..c374142b86 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..ae97ae06f3 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..0c99336f5c 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..ae865aa112 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,5 @@ +# Web framework +Flask==3.1.0 + +# Virtual environment for python +python-dotenv==1.0.1 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..1fd4ec7ed4 --- /dev/null +++ b/app_python/tests/__init__.py @@ -0,0 +1 @@ +# Unit tests (Lab 3) \ No newline at end of file