From 0a06f2b4f226da1962280f89c88d1c405238d9ad Mon Sep 17 00:00:00 2001 From: Phoenix Date: Thu, 29 Jan 2026 15:49:28 +0300 Subject: [PATCH 01/14] Complete lab2 --- lab2c/app_go/.dockerignore | 7 + lab2c/app_go/Dockerfile | 21 +++ lab2c/app_go/README.md | 41 +++++ lab2c/app_go/docs/LAB02.md | 131 +++++++++++++++ lab2c/app_go/go.mod | 3 + lab2c/app_go/main.go | 257 +++++++++++++++++++++++++++++ lab2c/app_python/.dockerignore | 12 ++ lab2c/app_python/.gitignore | 14 ++ lab2c/app_python/Dockerfile | 19 +++ lab2c/app_python/README.md | 72 ++++++++ lab2c/app_python/app.py | 158 ++++++++++++++++++ lab2c/app_python/docs/LAB02.md | 111 +++++++++++++ lab2c/app_python/requirements.txt | 2 + lab2c/app_python/tests/__init__.py | 1 + 14 files changed, 849 insertions(+) create mode 100644 lab2c/app_go/.dockerignore create mode 100644 lab2c/app_go/Dockerfile create mode 100644 lab2c/app_go/README.md create mode 100644 lab2c/app_go/docs/LAB02.md create mode 100644 lab2c/app_go/go.mod create mode 100644 lab2c/app_go/main.go create mode 100644 lab2c/app_python/.dockerignore create mode 100644 lab2c/app_python/.gitignore create mode 100644 lab2c/app_python/Dockerfile create mode 100644 lab2c/app_python/README.md create mode 100644 lab2c/app_python/app.py create mode 100644 lab2c/app_python/docs/LAB02.md create mode 100644 lab2c/app_python/requirements.txt create mode 100644 lab2c/app_python/tests/__init__.py diff --git a/lab2c/app_go/.dockerignore b/lab2c/app_go/.dockerignore new file mode 100644 index 0000000000..55a3b7cb13 --- /dev/null +++ b/lab2c/app_go/.dockerignore @@ -0,0 +1,7 @@ +*.exe +*.log +.git/ +.gitignore +.idea/ +.vscode/ +docs/ diff --git a/lab2c/app_go/Dockerfile b/lab2c/app_go/Dockerfile new file mode 100644 index 0000000000..534bac98be --- /dev/null +++ b/lab2c/app_go/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.22 AS builder + +WORKDIR /src + +COPY go.mod ./ +RUN go mod download + +COPY main.go ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o devops-info + +FROM gcr.io/distroless/static-debian12:nonroot + +WORKDIR /app +COPY --from=builder /src/devops-info /app/devops-info + +ENV HOST=0.0.0.0 \ + PORT=5000 + +EXPOSE 5000 + +ENTRYPOINT ["/app/devops-info"] diff --git a/lab2c/app_go/README.md b/lab2c/app_go/README.md new file mode 100644 index 0000000000..36e81eb856 --- /dev/null +++ b/lab2c/app_go/README.md @@ -0,0 +1,41 @@ +# DevOps Info Service (Go) + +## Overview +Compiled-language version of the DevOps info service. It exposes the same two endpoints as the Python app and keeps the JSON response structure consistent. + +## Prerequisites +- Go 1.22+ installed + +## Build and Run +Run directly: +```bash +go run main.go +``` + +Build a binary: +```bash +go build -o devops-info +./devops-info +``` + +Windows build/run: +```bash +go build -o devops-info.exe +.\devops-info.exe +``` + +Custom config examples: +```bash +PORT=8080 go run main.go +HOST=127.0.0.1 PORT=3000 go run main.go +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration +| Variable | Default | Description | +| --- | --- | --- | +| `HOST` | `0.0.0.0` | Bind address for the server | +| `PORT` | `5000` | Port to listen on | diff --git a/lab2c/app_go/docs/LAB02.md b/lab2c/app_go/docs/LAB02.md new file mode 100644 index 0000000000..71a016acd1 --- /dev/null +++ b/lab2c/app_go/docs/LAB02.md @@ -0,0 +1,131 @@ +# LAB02 - Docker Containerization (Go, Multi-Stage) + +## Multi-Stage Build Strategy +I used a two-stage Dockerfile: +1. **Builder stage** (`golang:1.22`) to compile the binary. +2. **Runtime stage** (`distroless/static-debian12:nonroot`) to run only the binary. + +This keeps the final image small and removes the Go toolchain from production. + +Dockerfile snippet: +```dockerfile +FROM golang:1.22 AS builder +WORKDIR /src +COPY go.mod ./ +RUN go mod download +COPY main.go ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o devops-info + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=builder /src/devops-info /app/devops-info +ENTRYPOINT ["/app/devops-info"] +``` + + +Image size output: +```text +tsixphoenix/devops-info-go latest 7fc572b1d863 4 minutes ago 17.7MB +``` + +## Build and Run Evidence +Build output: +```text +docker build -t tsixphoenix/devops-info-go:latest . +[+] Building 35.3s (16/16) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 396B 0.0s + => [internal] load metadata for gcr.io/distroless/static-debian12:nonroot 1.8s + => [internal] load metadata for docker.io/library/golang:1.22 2.4s + => [auth] library/golang:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 91B 0.0s + => [builder 1/6] FROM docker.io/library/golang:1.22@sha256:1cf6c45ba39db9fd6db16922041d074a63c935556a05c5ccb62d181034df7f02 22.6s + => => resolve docker.io/library/golang:1.22@sha256:1cf6c45ba39db9fd6db16922041d074a63c935556a05c5ccb62d181034df7f02 0.0s + => => sha256:1451027d3c0ee892b96310c034788bbe22b30b8ea2d075edbd09acfeaaaa439f 126B / 126B 0.4s + => => sha256:afa154b433c7f72db064d19e1bcfa84ee196ad29120328f6bdb2c5fbd7b8eeac 69.36MB / 69.36MB 8.8s + => => sha256:3b7f19923e1501f025b9459750b20f5df37af452482f75b91205f345d1c0e1b5 92.33MB / 92.33MB 10.0s + => => sha256:35af2a7690f2b43e7237d1fae8e3f2350dfb25f3249e9cf65121866f9c56c772 64.39MB / 64.39MB 8.1s + => => sha256:32b550be6cb62359a0f3a96bc0dc289f8b45d097eaad275887f163c6780b4108 24.06MB / 24.06MB 3.8s + => => sha256:a492eee5e55976c7d3feecce4c564aaf6f14fb07fdc5019d06f4154eddc93fde 48.48MB / 48.48MB 5.2s + => => extracting sha256:a492eee5e55976c7d3feecce4c564aaf6f14fb07fdc5019d06f4154eddc93fde 2.3s + => => extracting sha256:32b550be6cb62359a0f3a96bc0dc289f8b45d097eaad275887f163c6780b4108 0.8s + => => extracting sha256:35af2a7690f2b43e7237d1fae8e3f2350dfb25f3249e9cf65121866f9c56c772 2.5s + => => extracting sha256:3b7f19923e1501f025b9459750b20f5df37af452482f75b91205f345d1c0e1b5 2.0s + => => extracting sha256:afa154b433c7f72db064d19e1bcfa84ee196ad29120328f6bdb2c5fbd7b8eeac 5.1s + => => extracting sha256:1451027d3c0ee892b96310c034788bbe22b30b8ea2d075edbd09acfeaaaa439f 0.0s + => => extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 0.0s + => [internal] load build context 0.1s + => => transferring context: 6.51kB 0.0s + => [stage-1 1/3] FROM gcr.io/distroless/static-debian12:nonroot@sha256:cba10d7abd3e203428e86f5b2d7fd5eb7d8987c387864ae4996cf97191b33764 2.9s + => => resolve gcr.io/distroless/static-debian12:nonroot@sha256:cba10d7abd3e203428e86f5b2d7fd5eb7d8987c387864ae4996cf97191b33764 0.0s + => => sha256:069d1e267530c2e681fbd4d481553b4d05f98082b18fafac86e7f12996dddd0b 131.91kB / 131.91kB 0.6s + => => sha256:dcaa5a89b0ccda4b283e16d0b4d0891cd93d5fe05c6798f7806781a6a2d84354 314B / 314B 0.4s + => => sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f 385B / 385B 0.4s + => => sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112 80B / 80B 0.4s + => => sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296 162B / 162B 0.3s + => => sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4 123B / 123B 0.3s + => => sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212 188B / 188B 0.3s + => => sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a 67B / 67B 0.3s + => => sha256:62de241dac5fe19d5f8f4defe034289006ddaa0f2cca735db4718fe2a23e504e 31.24kB / 31.24kB 0.6s + => => sha256:017886f7e1764618ffad6fbd503c42a60076c63adc16355cac80f0f311cae4c9 544.07kB / 544.07kB 0.7s + => => sha256:bfb59b82a9b65e47d485e53b3e815bca3b3e21a095bd0cb88ced9ac0b48062bf 13.36kB / 13.36kB 0.6s + => => sha256:fab8c4b3fa32236a59c44cc504a69b18788d5c17c045691c2d682267ae8cf468 104.22kB / 104.22kB 0.6s + => => extracting sha256:fab8c4b3fa32236a59c44cc504a69b18788d5c17c045691c2d682267ae8cf468 0.1s + => => extracting sha256:bfb59b82a9b65e47d485e53b3e815bca3b3e21a095bd0cb88ced9ac0b48062bf 0.1s + => => extracting sha256:017886f7e1764618ffad6fbd503c42a60076c63adc16355cac80f0f311cae4c9 0.5s + => => extracting sha256:62de241dac5fe19d5f8f4defe034289006ddaa0f2cca735db4718fe2a23e504e 0.1s + => => extracting sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a 0.0s + => => extracting sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212 0.0s + => => extracting sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4 0.1s + => => extracting sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296 0.1s + => => extracting sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112 0.0s + => => extracting sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f 0.0s + => => extracting sha256:dcaa5a89b0ccda4b283e16d0b4d0891cd93d5fe05c6798f7806781a6a2d84354 0.0s + => => extracting sha256:069d1e267530c2e681fbd4d481553b4d05f98082b18fafac86e7f12996dddd0b 0.0s + => [stage-1 2/3] WORKDIR /app 0.1s + => [builder 2/6] WORKDIR /src 0.5s + => [builder 3/6] COPY go.mod ./ 0.1s + => [builder 4/6] RUN go mod download 0.5s + => [builder 5/6] COPY main.go ./ 0.1s + => [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o devops-info 8.1s + => [stage-1 3/3] COPY --from=builder /src/devops-info /app/devops-info 0.1s + => exporting to image 0.6s + => => exporting layers 0.4s + => => exporting manifest sha256:39177489cedb41b9d9f566a8be5d09c8ffe938f98b590aa0ebb987f1cf38d7a6 0.0s + => => exporting config sha256:d86ea6d9a836253c87a0ac2232aa6f03cdc8198146f9acdba1f3d31c617bca82 0.0s + => => exporting attestation manifest sha256:79e9867f53966cbf5943864985b72aeed88ea8a8349789577aee72d45045e5af 0.0s + => => exporting manifest list sha256:7fc572b1d86304a2634962e06610c7cf4295c4a466b6e52aed34f93550555008 0.0s + => => naming to docker.io/tsixphoenix/devops-info-go:latest 0.0s + => => unpacking to docker.io/tsixphoenix/devops-info-go:latest 0.1s + +``` + +Run output: +```text +docker run --rm -p 5000:5000 --name devops-info-go tsixphoenix/devops-info-go:latest +2026/01/29 12:37:42 Starting DevOps Info Service on 0.0.0.0:5000 +``` + +Endpoint checks: +```text +curl http://localhost:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"Go net/http"},"system":{"hostname":"50a30efde177","platform":"linux","platform_version":"Distroless","architecture":"amd64","cpu_count":12,"python_version":"go1.22.12"},"runtime":{"uptime_seconds":79,"uptime_human":"0 hours, 1 minute","current_time":"2026-01-29T12:39:02Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1","user_agent":"curl/8.16.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + +curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-29T12:39:31Z","uptime_seconds":108} + +2026/01/29 12:39:02 Request: GET / +2026/01/29 12:39:02 Response: GET / -> 200 (418.191µs) +2026/01/29 12:39:31 Request: GET /health +2026/01/29 12:39:31 Response: GET /health -> 200 (114.664µs) +``` + +## Technical Analysis +- The builder stage contains the full Go toolchain; the runtime stage does not. +- If I shipped the builder stage, the image would be much larger and include tools that should not be in production. +- A static binary lets me use a minimal base image. +- The final image runs as a non-root user, which reduces risk. + +## Challenges and Solutions +- I made sure the binary was static (CGO disabled) so it works in a minimal runtime image. +- Distroless images do not include a shell, so debugging is done in the builder stage, not in the runtime image. diff --git a/lab2c/app_go/go.mod b/lab2c/app_go/go.mod new file mode 100644 index 0000000000..7a7fcedd1c --- /dev/null +++ b/lab2c/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.22 diff --git a/lab2c/app_go/main.go b/lab2c/app_go/main.go new file mode 100644 index 0000000000..2abcd3938a --- /dev/null +++ b/lab2c/app_go/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "strings" + "time" +) + +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"` + PythonVersion string `json:"python_version"` +} + +type Runtime 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 Response struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +var startTime = time.Now().UTC() + +func main() { + host := getenv("HOST", "0.0.0.0") + port := getenv("PORT", "5000") + addr := net.JoinHostPort(host, port) + + mux := http.NewServeMux() + mux.HandleFunc("/", rootHandler) + mux.HandleFunc("/health", healthHandler) + + handler := recoverMiddleware(loggingMiddleware(mux)) + + server := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 5 * time.Second, + } + + log.Printf("Starting DevOps Info Service on %s", addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("server error: %v", err) + } +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + writeNotFound(w) + return + } + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + uptimeSeconds, uptimeHuman := getUptime() + now := time.Now().UTC() + + hostname, _ := os.Hostname() + response := Response{ + 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: getPlatformVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + PythonVersion: runtime.Version(), + }, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: now.Format(time.RFC3339), + Timezone: "UTC", + }, + Request: RequestInfo{ + ClientIP: getClientIP(r), + 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"}, + }, + } + + writeJSON(w, http.StatusOK, response) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/health" { + writeNotFound(w) + return + } + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + uptimeSeconds, _ := getUptime() + payload := map[string]any{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "uptime_seconds": uptimeSeconds, + } + + writeJSON(w, http.StatusOK, payload) +} + +func getUptime() (int, string) { + seconds := int(time.Since(startTime).Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + hourLabel := "hours" + if hours == 1 { + hourLabel = "hour" + } + minuteLabel := "minutes" + if minutes == 1 { + minuteLabel = "minute" + } + return seconds, fmt.Sprintf("%d %s, %d %s", hours, hourLabel, minutes, minuteLabel) +} + +func getClientIP(r *http.Request) string { + if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { + parts := strings.Split(forwarded, ",") + return strings.TrimSpace(parts[0]) + } + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil { + return host + } + return r.RemoteAddr +} + +func getPlatformVersion() string { + if value := os.Getenv("OS"); value != "" { + return value + } + if data, err := os.ReadFile("/etc/os-release"); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "PRETTY_NAME=") { + return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") + } + } + } + return "unknown" +} + +func writeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(payload); err != nil { + log.Printf("json encode error: %v", err) + } +} + +func writeNotFound(w http.ResponseWriter) { + writeJSON(w, http.StatusNotFound, map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) +} + +func writeMethodNotAllowed(w http.ResponseWriter) { + writeJSON(w, http.StatusMethodNotAllowed, map[string]string{ + "error": "Method Not Allowed", + "message": "Only GET is supported for this endpoint", + }) +} + +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (recorder *statusRecorder) WriteHeader(code int) { + recorder.status = code + recorder.ResponseWriter.WriteHeader(code) +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK} + start := time.Now() + log.Printf("Request: %s %s", r.Method, r.URL.Path) + next.ServeHTTP(recorder, r) + log.Printf("Response: %s %s -> %d (%s)", r.Method, r.URL.Path, recorder.status, time.Since(start)) + }) +} + +func recoverMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("panic recovered: %v", err) + writeJSON(w, http.StatusInternalServerError, map[string]string{ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }) + } + }() + next.ServeHTTP(w, r) + }) +} + +func getenv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/lab2c/app_python/.dockerignore b/lab2c/app_python/.dockerignore new file mode 100644 index 0000000000..b7738de7b8 --- /dev/null +++ b/lab2c/app_python/.dockerignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*.log +venv/ +.venv/ +.env +.git/ +.gitignore +.idea/ +.vscode/ +docs/ +tests/ diff --git a/lab2c/app_python/.gitignore b/lab2c/app_python/.gitignore new file mode 100644 index 0000000000..8052e93c8b --- /dev/null +++ b/lab2c/app_python/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*.log +venv/ +.venv/ +.env + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/lab2c/app_python/Dockerfile b/lab2c/app_python/Dockerfile new file mode 100644 index 0000000000..76219e6c10 --- /dev/null +++ b/lab2c/app_python/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN useradd -m -u 10001 appuser + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=appuser:appuser app.py . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/lab2c/app_python/README.md b/lab2c/app_python/README.md new file mode 100644 index 0000000000..742a7439f4 --- /dev/null +++ b/lab2c/app_python/README.md @@ -0,0 +1,72 @@ +# DevOps Info Service (FastAPI) + +## Overview +Small service returning system info about the machine it runs on, plus a health check. + +## Prerequisites +- Python 3.11+ +- pip +- (Optional) venv tool + +## Installation +### Windows +```bash +python -m venv venv +.\venv\Scripts\Activate.ps1 +pip install -r requirements.txt +``` + +### macOS/Linux +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Running the Application +```bash +python app.py +``` + +Custom config examples: +```bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +FastAPI docs: +- `http://localhost:/docs` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration +| Variable | Default | Description | +| --- | --- | --- | +| `HOST` | `0.0.0.0` | Bind address for the server | +| `PORT` | `5000` | Port to listen on | +| `DEBUG` | `False` | Enable auto-reload | + +## Docker +Command patterns (replace the placeholders with your values): + +**Build locally** +```bash +docker build -t /: . +``` + +**Run container** +```bash +docker run --rm -p :5000 --name /: +``` + +**Pull from Docker Hub** +```bash +docker pull /: +``` + +Optional env overrides: +```bash +docker run --rm -e PORT=5000 -e HOST=0.0.0.0 -p :5000 /: +``` diff --git a/lab2c/app_python/app.py b/lab2c/app_python/app.py new file mode 100644 index 0000000000..8935b94091 --- /dev/null +++ b/lab2c/app_python/app.py @@ -0,0 +1,158 @@ +""" +DevOps Info Service +FastAPI application module. +""" + +from __future__ import annotations + +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +import uvicorn +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +# Config +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", "5000")) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +SERVICE_NAME = "devops-info-service" +SERVICE_VERSION = "1.0.0" +SERVICE_DESCRIPTION = "DevOps course info service" +SERVICE_FRAMEWORK = "FastAPI" + +START_TIME = datetime.now(timezone.utc) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("devops-info-service") + +app = FastAPI( + title="DevOps Info Service", + version=SERVICE_VERSION, + description=SERVICE_DESCRIPTION, +) + + +def _format_uptime(seconds: int) -> str: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + hour_label = "hour" if hours == 1 else "hours" + minute_label = "minute" if minutes == 1 else "minutes" + return f"{hours} {hour_label}, {minutes} {minute_label}" + + +def get_uptime() -> dict[str, int | str]: + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + return { + "seconds": seconds, + "human": _format_uptime(seconds), + } + + +def get_system_info() -> dict[str, str | int]: + 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 isoformat_utc(dt: datetime) -> str: + return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + logger.info("Request: %s %s", request.method, request.url.path) + response = await call_next(request) + logger.info("Response: %s %s -> %s", request.method, request.url.path, response.status_code) + return response + + +@app.exception_handler(StarletteHTTPException) +async def http_exception_handler(request: Request, exc: StarletteHTTPException): + 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": exc.detail}, + ) + + +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception): + logger.exception("Unhandled error: %s", exc) + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }, + ) + + +@app.get("/") +async def root(request: Request): + uptime = get_uptime() + now = datetime.now(timezone.utc) + + response = { + "service": { + "name": SERVICE_NAME, + "version": SERVICE_VERSION, + "description": SERVICE_DESCRIPTION, + "framework": SERVICE_FRAMEWORK, + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": isoformat_utc(now), + "timezone": "UTC", + }, + "request": { + "client_ip": request.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent", "unknown"), + "method": request.method, + "path": request.url.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + + return response + + +@app.get("/health") +async def health(): + uptime = get_uptime() + return { + "status": "healthy", + "timestamp": isoformat_utc(datetime.now(timezone.utc)), + "uptime_seconds": uptime["seconds"], + } + + +if __name__ == "__main__": + logger.info("Starting DevOps Info Service on %s:%s", HOST, PORT) + uvicorn.run("app:app", host=HOST, port=PORT, reload=DEBUG, log_level="info") diff --git a/lab2c/app_python/docs/LAB02.md b/lab2c/app_python/docs/LAB02.md new file mode 100644 index 0000000000..dd91a49278 --- /dev/null +++ b/lab2c/app_python/docs/LAB02.md @@ -0,0 +1,111 @@ +# LAB02 - Docker Containerization (Python) + +## Docker Best Practices Applied +- **Pinned base image**: `python:3.13-slim` keeps the image small and reproducible. +- **Non-root user**: the container runs as `appuser`, so the service does not run as root. +- **Layer caching**: dependencies are installed before copying the app so rebuilds are faster. +- **Minimal copy**: only `requirements.txt` and `app.py` are copied into the image. +- **.dockerignore**: excluded tests, docs, and virtualenvs to keep the build context small. + +Dockerfile snippet: +```dockerfile +FROM python:3.13-slim +WORKDIR /app +RUN useradd -m -u 10001 appuser +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY --chown=appuser:appuser app.py . +USER appuser +``` + +## Image Information and Decisions +- **Base image choice**: `python:3.13-slim` is a good balance of size and compatibility. +- **Final image size**: `` +- **Layer structure**: dependencies are installed in their own layer to benefit from caching. +- **Optimization choices**: small base image, no extra build tools, only required files copied. + +Image size output: +```text +tsixphoenix/devops-info-python beta 04eec5e16beb 5 minutes ago 228MB +``` + +## Build and Run Process +Build output: +```text +docker build -t tsixphoenix/devops-info-python:beta . +[+] Building 16.7s (11/11) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 332B 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 2.3s + => [internal] load .dockerignore 0.0s + => => transferring context: 133B 0.0s + => [1/6] FROM docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 2.4s + => => resolve docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => => sha256:8843ea38a07e15ac1b99c72108fbb492f737032986cc0b65ed351f84e5521879 1.29MB / 1.29MB 0.5s + => => sha256:36b6de65fd8d6bd36071ea9efa7d078ebdc11ecc23d2426ec9c3e9f092ae824d 249B / 249B 0.6s + => => sha256:0bee50492702eb5d822fbcbac8f545a25f5fe173ec8030f57691aefcc283bbc9 11.79MB / 11.79MB 1.5s + => => extracting sha256:8843ea38a07e15ac1b99c72108fbb492f737032986cc0b65ed351f84e5521879 0.3s + => => extracting sha256:0bee50492702eb5d822fbcbac8f545a25f5fe173ec8030f57691aefcc283bbc9 0.8s + => => extracting sha256:36b6de65fd8d6bd36071ea9efa7d078ebdc11ecc23d2426ec9c3e9f092ae824d 0.0s + => [internal] load build context 0.0s + => => transferring context: 4.60kB 0.0s + => [2/6] WORKDIR /app 0.1s + => [3/6] RUN useradd -m -u 10001 appuser 0.6s + => [4/6] COPY requirements.txt . 0.0s + => [5/6] RUN pip install --no-cache-dir -r requirements.txt 8.8s + => [6/6] COPY --chown=appuser:appuser app.py . 0.1s + => exporting to image 2.1s + => => exporting layers 1.4s + => => exporting manifest sha256:89257312508e9a26af1f7400253d9556816a0fc9230a414836bcedb8a4881c86 0.0s + => => exporting config sha256:a7d85cde725e6fdfb1dfbccbb9daadb4138561a5698ac01f5f6e2780b62994f3 0.0s + => => exporting attestation manifest sha256:82c962563c14aaa47813d2f1b62afb9806c83dbb0519256fd9954a50ea14fd3f 0.0s + => => exporting manifest list sha256:04eec5e16beb90a39cdac694238e9c6301410b6fa987d7b7788c03287ed57da0 0.0s + => => naming to docker.io/tsixphoenix/devops-info-python:beta 0.0s + => => unpacking to docker.io/tsixphoenix/devops-info-python:beta +``` + +Run output (container start): +```text +docker run --rm -p 5000:5000 --name devops-info tsixphoenix/devops-info-python:beta +2026-01-29 12:23:57,799 - INFO - Starting DevOps Info Service on 0.0.0.0:5000 +INFO: Started server process [1] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit) +``` + +Endpoint checks: +```text +curl http://localhost:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"FastAPI"},"system":{"hostname":"d65d9dfde3f9","platform":"Linux","platform_version":"6.6.87.2-microsoft-standard-WSL2","architecture":"x86_64","cpu_count":12,"python_version":"3.13.11"},"runtime":{"uptime_seconds":98,"uptime_human":"0 hours, 1 minute","current_time":"2026-01-29T12:25:35.964833Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1","user_agent":"curl/8.16.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + +curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-29T12:25:56.660917Z","uptime_seconds":118} + +2026-01-29 12:25:35,964 - INFO - Request: GET / +2026-01-29 12:25:35,965 - INFO - Response: GET / -> 200 +INFO: 172.17.0.1:54462 - "GET / HTTP/1.1" 200 OK +2026-01-29 12:25:56,659 - INFO - Request: GET /health +2026-01-29 12:25:56,661 - INFO - Response: GET /health -> 200 +INFO: 172.17.0.1:57328 - "GET /health HTTP/1.1" 200 OK +``` + +Docker Hub repository URL: +``` +https://hub.docker.com/repository/docker/tsixphoenix/devops-info-python/general +``` + +Tagging strategy: +``` +version tag +``` + +## Technical Analysis +- The Dockerfile copies `requirements.txt` first so dependency layers are cached between builds. +- If I copied the whole project before installing dependencies, every code change would bust the cache. +- Running as a non-root user reduces risk if a container is compromised. +- `.dockerignore` keeps the build context small, which speeds up the build and reduces image size. + +## Challenges and Solutions +- I verified the app binds to `0.0.0.0` so it is reachable from outside the container. +- I double-checked that only the needed files are copied into the image to avoid bloating it. diff --git a/lab2c/app_python/requirements.txt b/lab2c/app_python/requirements.txt new file mode 100644 index 0000000000..792449289f --- /dev/null +++ b/lab2c/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 diff --git a/lab2c/app_python/tests/__init__.py b/lab2c/app_python/tests/__init__.py new file mode 100644 index 0000000000..792d600548 --- /dev/null +++ b/lab2c/app_python/tests/__init__.py @@ -0,0 +1 @@ +# From 90a03b930a867a6c226f7c46d6150a3f48046283 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:14:07 +0300 Subject: [PATCH 02/14] feat: add lab3 CI pipeline --- .github/workflows/go-ci.yml | 76 ++++++++ .github/workflows/python-ci.yml | 104 +++++++++++ lab3c/app_go/.dockerignore | 7 + lab3c/app_go/Dockerfile | 21 +++ lab3c/app_go/README.md | 41 ++++ lab3c/app_go/docs/LAB03.md | 17 ++ lab3c/app_go/go.mod | 3 + lab3c/app_go/main.go | 257 ++++++++++++++++++++++++++ lab3c/app_go/main_test.go | 54 ++++++ lab3c/app_python/.dockerignore | 12 ++ lab3c/app_python/.gitignore | 14 ++ lab3c/app_python/Dockerfile | 19 ++ lab3c/app_python/README.md | 78 ++++++++ lab3c/app_python/app.py | 158 ++++++++++++++++ lab3c/app_python/docs/LAB03.md | 58 ++++++ lab3c/app_python/pyproject.toml | 3 + lab3c/app_python/requirements-dev.txt | 5 + lab3c/app_python/requirements.txt | 2 + lab3c/app_python/tests/__init__.py | 1 + lab3c/app_python/tests/test_app.py | 66 +++++++ 20 files changed, 996 insertions(+) create mode 100644 .github/workflows/go-ci.yml create mode 100644 .github/workflows/python-ci.yml create mode 100644 lab3c/app_go/.dockerignore create mode 100644 lab3c/app_go/Dockerfile create mode 100644 lab3c/app_go/README.md create mode 100644 lab3c/app_go/docs/LAB03.md create mode 100644 lab3c/app_go/go.mod create mode 100644 lab3c/app_go/main.go create mode 100644 lab3c/app_go/main_test.go create mode 100644 lab3c/app_python/.dockerignore create mode 100644 lab3c/app_python/.gitignore create mode 100644 lab3c/app_python/Dockerfile create mode 100644 lab3c/app_python/README.md create mode 100644 lab3c/app_python/app.py create mode 100644 lab3c/app_python/docs/LAB03.md create mode 100644 lab3c/app_python/pyproject.toml create mode 100644 lab3c/app_python/requirements-dev.txt create mode 100644 lab3c/app_python/requirements.txt create mode 100644 lab3c/app_python/tests/__init__.py create mode 100644 lab3c/app_python/tests/test_app.py diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml new file mode 100644 index 0000000000..e09a65c488 --- /dev/null +++ b/.github/workflows/go-ci.yml @@ -0,0 +1,76 @@ +name: Go CI (Lab03 Bonus) + +on: + push: + branches: [lab03, main, master] + paths: + - "lab3c/app_go/**" + - ".github/workflows/go-ci.yml" + pull_request: + branches: [lab03, main, master] + paths: + - "lab3c/app_go/**" + - ".github/workflows/go-ci.yml" + +concurrency: + group: go-ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Lint and Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + working-directory: lab3c/app_go + args: --timeout=5m + + - name: Run tests + working-directory: lab3c/app_go + run: go test ./... + + docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: test + if: ${{ github.event_name == 'push' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set version (CalVer) + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./lab3c/app_go + file: ./lab3c/app_go/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-go:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-go:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..5284899721 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,104 @@ +name: Python CI (Lab03) + +on: + push: + branches: [lab03, main, master] + paths: + - "lab3c/app_python/**" + - ".github/workflows/python-ci.yml" + pull_request: + branches: [lab03, main, master] + paths: + - "lab3c/app_python/**" + - ".github/workflows/python-ci.yml" + +concurrency: + group: python-ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Lint and Test + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ["3.11", "3.12"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: | + lab3c/app_python/requirements.txt + lab3c/app_python/requirements-dev.txt + + - name: Install dependencies + working-directory: lab3c/app_python + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + + - name: Lint (ruff) + working-directory: lab3c/app_python + run: ruff check . + + - name: Run tests with coverage + working-directory: lab3c/app_python + run: pytest --cov=app --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + if: ${{ secrets.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v4 + with: + files: lab3c/app_python/coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Snyk scan + if: ${{ secrets.SNYK_TOKEN != '' }} + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + args: --file=lab3c/app_python/requirements.txt + + docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: test + if: ${{ github.event_name == 'push' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set version (CalVer) + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./lab3c/app_python + file: ./lab3c/app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-python:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-python:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/lab3c/app_go/.dockerignore b/lab3c/app_go/.dockerignore new file mode 100644 index 0000000000..55a3b7cb13 --- /dev/null +++ b/lab3c/app_go/.dockerignore @@ -0,0 +1,7 @@ +*.exe +*.log +.git/ +.gitignore +.idea/ +.vscode/ +docs/ diff --git a/lab3c/app_go/Dockerfile b/lab3c/app_go/Dockerfile new file mode 100644 index 0000000000..534bac98be --- /dev/null +++ b/lab3c/app_go/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.22 AS builder + +WORKDIR /src + +COPY go.mod ./ +RUN go mod download + +COPY main.go ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o devops-info + +FROM gcr.io/distroless/static-debian12:nonroot + +WORKDIR /app +COPY --from=builder /src/devops-info /app/devops-info + +ENV HOST=0.0.0.0 \ + PORT=5000 + +EXPOSE 5000 + +ENTRYPOINT ["/app/devops-info"] diff --git a/lab3c/app_go/README.md b/lab3c/app_go/README.md new file mode 100644 index 0000000000..36e81eb856 --- /dev/null +++ b/lab3c/app_go/README.md @@ -0,0 +1,41 @@ +# DevOps Info Service (Go) + +## Overview +Compiled-language version of the DevOps info service. It exposes the same two endpoints as the Python app and keeps the JSON response structure consistent. + +## Prerequisites +- Go 1.22+ installed + +## Build and Run +Run directly: +```bash +go run main.go +``` + +Build a binary: +```bash +go build -o devops-info +./devops-info +``` + +Windows build/run: +```bash +go build -o devops-info.exe +.\devops-info.exe +``` + +Custom config examples: +```bash +PORT=8080 go run main.go +HOST=127.0.0.1 PORT=3000 go run main.go +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration +| Variable | Default | Description | +| --- | --- | --- | +| `HOST` | `0.0.0.0` | Bind address for the server | +| `PORT` | `5000` | Port to listen on | diff --git a/lab3c/app_go/docs/LAB03.md b/lab3c/app_go/docs/LAB03.md new file mode 100644 index 0000000000..2fc772a8e3 --- /dev/null +++ b/lab3c/app_go/docs/LAB03.md @@ -0,0 +1,17 @@ +# LAB03 - CI/CD (Go Bonus) + +## Multi-App CI Summary +I added a separate workflow for the Go app with its own path filters. This keeps Python and Go CI independent and avoids running jobs that are not needed. + +## Path Filters +- Go workflow runs only when `lab3c/app_go/**` or its workflow file changes. +- Python workflow runs only when `lab3c/app_python/**` or its workflow file changes. + +## Workflow Evidence +Add real links after CI runs: +- ✅ **Go workflow run:** `` +- ✅ **Docker image on Docker Hub:** `` + +## Notes +- Go CI uses `go test` and a basic lint step. +- Docker builds use the same CalVer tag scheme as Python. diff --git a/lab3c/app_go/go.mod b/lab3c/app_go/go.mod new file mode 100644 index 0000000000..7a7fcedd1c --- /dev/null +++ b/lab3c/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.22 diff --git a/lab3c/app_go/main.go b/lab3c/app_go/main.go new file mode 100644 index 0000000000..2abcd3938a --- /dev/null +++ b/lab3c/app_go/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "strings" + "time" +) + +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"` + PythonVersion string `json:"python_version"` +} + +type Runtime 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 Response struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +var startTime = time.Now().UTC() + +func main() { + host := getenv("HOST", "0.0.0.0") + port := getenv("PORT", "5000") + addr := net.JoinHostPort(host, port) + + mux := http.NewServeMux() + mux.HandleFunc("/", rootHandler) + mux.HandleFunc("/health", healthHandler) + + handler := recoverMiddleware(loggingMiddleware(mux)) + + server := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 5 * time.Second, + } + + log.Printf("Starting DevOps Info Service on %s", addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("server error: %v", err) + } +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + writeNotFound(w) + return + } + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + uptimeSeconds, uptimeHuman := getUptime() + now := time.Now().UTC() + + hostname, _ := os.Hostname() + response := Response{ + 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: getPlatformVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + PythonVersion: runtime.Version(), + }, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: now.Format(time.RFC3339), + Timezone: "UTC", + }, + Request: RequestInfo{ + ClientIP: getClientIP(r), + 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"}, + }, + } + + writeJSON(w, http.StatusOK, response) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/health" { + writeNotFound(w) + return + } + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + uptimeSeconds, _ := getUptime() + payload := map[string]any{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "uptime_seconds": uptimeSeconds, + } + + writeJSON(w, http.StatusOK, payload) +} + +func getUptime() (int, string) { + seconds := int(time.Since(startTime).Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + hourLabel := "hours" + if hours == 1 { + hourLabel = "hour" + } + minuteLabel := "minutes" + if minutes == 1 { + minuteLabel = "minute" + } + return seconds, fmt.Sprintf("%d %s, %d %s", hours, hourLabel, minutes, minuteLabel) +} + +func getClientIP(r *http.Request) string { + if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { + parts := strings.Split(forwarded, ",") + return strings.TrimSpace(parts[0]) + } + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil { + return host + } + return r.RemoteAddr +} + +func getPlatformVersion() string { + if value := os.Getenv("OS"); value != "" { + return value + } + if data, err := os.ReadFile("/etc/os-release"); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "PRETTY_NAME=") { + return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") + } + } + } + return "unknown" +} + +func writeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(payload); err != nil { + log.Printf("json encode error: %v", err) + } +} + +func writeNotFound(w http.ResponseWriter) { + writeJSON(w, http.StatusNotFound, map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) +} + +func writeMethodNotAllowed(w http.ResponseWriter) { + writeJSON(w, http.StatusMethodNotAllowed, map[string]string{ + "error": "Method Not Allowed", + "message": "Only GET is supported for this endpoint", + }) +} + +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (recorder *statusRecorder) WriteHeader(code int) { + recorder.status = code + recorder.ResponseWriter.WriteHeader(code) +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK} + start := time.Now() + log.Printf("Request: %s %s", r.Method, r.URL.Path) + next.ServeHTTP(recorder, r) + log.Printf("Response: %s %s -> %d (%s)", r.Method, r.URL.Path, recorder.status, time.Since(start)) + }) +} + +func recoverMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("panic recovered: %v", err) + writeJSON(w, http.StatusInternalServerError, map[string]string{ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }) + } + }() + next.ServeHTTP(w, r) + }) +} + +func getenv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/lab3c/app_go/main_test.go b/lab3c/app_go/main_test.go new file mode 100644 index 0000000000..b8ba60fefa --- /dev/null +++ b/lab3c/app_go/main_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRootHandlerOK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + rootHandler(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + + var payload map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + t.Fatalf("invalid json: %v", err) + } + + if _, ok := payload["service"]; !ok { + t.Fatal("missing service section") + } + if _, ok := payload["system"]; !ok { + t.Fatal("missing system section") + } + if _, ok := payload["runtime"]; !ok { + t.Fatal("missing runtime section") + } +} + +func TestHealthHandlerOK(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + + healthHandler(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + + var payload map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + t.Fatalf("invalid json: %v", err) + } + + if payload["status"] != "healthy" { + t.Fatalf("unexpected status: %v", payload["status"]) + } +} diff --git a/lab3c/app_python/.dockerignore b/lab3c/app_python/.dockerignore new file mode 100644 index 0000000000..b7738de7b8 --- /dev/null +++ b/lab3c/app_python/.dockerignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*.log +venv/ +.venv/ +.env +.git/ +.gitignore +.idea/ +.vscode/ +docs/ +tests/ diff --git a/lab3c/app_python/.gitignore b/lab3c/app_python/.gitignore new file mode 100644 index 0000000000..8052e93c8b --- /dev/null +++ b/lab3c/app_python/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*.log +venv/ +.venv/ +.env + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/lab3c/app_python/Dockerfile b/lab3c/app_python/Dockerfile new file mode 100644 index 0000000000..76219e6c10 --- /dev/null +++ b/lab3c/app_python/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN useradd -m -u 10001 appuser + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=appuser:appuser app.py . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/lab3c/app_python/README.md b/lab3c/app_python/README.md new file mode 100644 index 0000000000..321559cad9 --- /dev/null +++ b/lab3c/app_python/README.md @@ -0,0 +1,78 @@ +# DevOps Info Service (FastAPI) + +[![Python CI](https://github.com/TsixPhoenix/DevOps-CC/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/TsixPhoenix/DevOps-CC/actions/workflows/python-ci.yml) +[![Coverage](https://codecov.io/gh/TsixPhoenix/DevOps-CC/branch/lab03/graph/badge.svg)](https://codecov.io/gh/TsixPhoenix/DevOps-CC) + +## Overview +Small service returning system info about the machine it runs on, plus a health check. + +## Prerequisites +- Python 3.11+ +- pip +- (Optional) venv tool + +## Installation +```bash +python -m venv venv +.\venv\Scripts\Activate.ps1 +pip install -r requirements.txt -r requirements-dev.txt +``` + +## Running the Application +```bash +python app.py +``` + +Custom config examples: +```bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +FastAPI docs: +- `http://localhost:/docs` + +## Tests +Run locally: +```bash +pytest +``` + +Run with coverage: +```bash +pytest --cov=app --cov-report=term +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration +| Variable | Default | Description | +| --- | --- | --- | +| `HOST` | `0.0.0.0` | Bind address for the server | +| `PORT` | `5000` | Port to listen on | +| `DEBUG` | `False` | Enable auto-reload | + +## Docker +Command patterns (replace the placeholders with your values): + +**Build locally** +```bash +docker build -t /: . +``` + +**Run container** +```bash +docker run --rm -p :5000 --name /: +``` + +**Pull from Docker Hub** +```bash +docker pull /: +``` + +Optional env overrides: +```bash +docker run --rm -e PORT=5000 -e HOST=0.0.0.0 -p :5000 /: +``` diff --git a/lab3c/app_python/app.py b/lab3c/app_python/app.py new file mode 100644 index 0000000000..8935b94091 --- /dev/null +++ b/lab3c/app_python/app.py @@ -0,0 +1,158 @@ +""" +DevOps Info Service +FastAPI application module. +""" + +from __future__ import annotations + +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +import uvicorn +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +# Config +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", "5000")) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +SERVICE_NAME = "devops-info-service" +SERVICE_VERSION = "1.0.0" +SERVICE_DESCRIPTION = "DevOps course info service" +SERVICE_FRAMEWORK = "FastAPI" + +START_TIME = datetime.now(timezone.utc) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("devops-info-service") + +app = FastAPI( + title="DevOps Info Service", + version=SERVICE_VERSION, + description=SERVICE_DESCRIPTION, +) + + +def _format_uptime(seconds: int) -> str: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + hour_label = "hour" if hours == 1 else "hours" + minute_label = "minute" if minutes == 1 else "minutes" + return f"{hours} {hour_label}, {minutes} {minute_label}" + + +def get_uptime() -> dict[str, int | str]: + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + return { + "seconds": seconds, + "human": _format_uptime(seconds), + } + + +def get_system_info() -> dict[str, str | int]: + 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 isoformat_utc(dt: datetime) -> str: + return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + logger.info("Request: %s %s", request.method, request.url.path) + response = await call_next(request) + logger.info("Response: %s %s -> %s", request.method, request.url.path, response.status_code) + return response + + +@app.exception_handler(StarletteHTTPException) +async def http_exception_handler(request: Request, exc: StarletteHTTPException): + 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": exc.detail}, + ) + + +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception): + logger.exception("Unhandled error: %s", exc) + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }, + ) + + +@app.get("/") +async def root(request: Request): + uptime = get_uptime() + now = datetime.now(timezone.utc) + + response = { + "service": { + "name": SERVICE_NAME, + "version": SERVICE_VERSION, + "description": SERVICE_DESCRIPTION, + "framework": SERVICE_FRAMEWORK, + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": isoformat_utc(now), + "timezone": "UTC", + }, + "request": { + "client_ip": request.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent", "unknown"), + "method": request.method, + "path": request.url.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + + return response + + +@app.get("/health") +async def health(): + uptime = get_uptime() + return { + "status": "healthy", + "timestamp": isoformat_utc(datetime.now(timezone.utc)), + "uptime_seconds": uptime["seconds"], + } + + +if __name__ == "__main__": + logger.info("Starting DevOps Info Service on %s:%s", HOST, PORT) + uvicorn.run("app:app", host=HOST, port=PORT, reload=DEBUG, log_level="info") diff --git a/lab3c/app_python/docs/LAB03.md b/lab3c/app_python/docs/LAB03.md new file mode 100644 index 0000000000..3a1772878c --- /dev/null +++ b/lab3c/app_python/docs/LAB03.md @@ -0,0 +1,58 @@ +# LAB03 - CI/CD (Python) + +## 1. Overview +**Testing framework:** I used `pytest`. The syntax is clean, fixtures are easy to work with, and it is the default choice in most Python projects I see. + +**What tests cover:** The tests hit `GET /`, `GET /health`, a 404 case, and helper functions like uptime formatting. I focused on structure and types instead of exact machine values. + +**Workflow triggers:** CI runs on push and pull requests to `lab03`, `main`, or `master`, but only when `lab3c/app_python/**` or the workflow file changes. + +**Versioning strategy:** I chose CalVer (YYYY.MM.DD). It is simple, and this service is released continuously rather than as a library. + +## 2. Workflow Evidence +Add real links and outputs after you run CI: +- **Successful workflow run:** `` +- **Tests passing locally:** +pytest +============================================================================================ test session starts ============================================================================================ +platform win32 -- Python 3.12.2, pytest-9.0.2, pluggy-1.6.0 +rootdir: C:\Users\Phoenix\PycharmProjects\DevOps\DevOps-CC\lab3c\app_python +configfile: pyproject.toml +plugins: anyio-4.12.1, cov-7.0.0 +collected 5 items + +tests\test_app.py ..... [100%] + +============================================================================================= 5 passed in 0.36s ============================================================================================= +- **Docker image on Docker Hub:** `` +- **Status badge:** `` + +## 3. Best Practices Implemented +- **Dependency caching:** `actions/setup-python` caches pip packages to speed up installs. +- **Job separation:** tests run in one job, Docker build/push depends on test success. +- **Conditional push:** Docker images only push on `push` events (not on PRs). +- **Concurrency:** newer runs cancel older runs for the same branch. +- **Path filters:** CI runs only when the Python app changes (monorepo friendly). +- **Snyk scanning:** dependency scan runs in CI (requires token). + +Caching time saved: +``` + +``` + +Snyk result: +``` + +``` + +## 4. Key Decisions +**Versioning Strategy:** CalVer fits a small service that ships frequently. It is easy to read and does not require manual version bumps. + +**Docker Tags:** The workflow publishes `YYYY.MM.DD` and `latest` tags for the same image. + +**Workflow Triggers:** I used path filters to avoid running Python CI when only Go code changes. + +**Test Coverage:** Core endpoints and helper functions are tested. I did not try to cover every logging line. + +## 5. Challenges (Optional) +- Everything was clear, because of experience of setting up CI/CD in my company workspace. diff --git a/lab3c/app_python/pyproject.toml b/lab3c/app_python/pyproject.toml new file mode 100644 index 0000000000..efb9a85312 --- /dev/null +++ b/lab3c/app_python/pyproject.toml @@ -0,0 +1,3 @@ +[tool.ruff] +select = ["E", "F"] +ignore = ["E501"] diff --git a/lab3c/app_python/requirements-dev.txt b/lab3c/app_python/requirements-dev.txt new file mode 100644 index 0000000000..c6610506e3 --- /dev/null +++ b/lab3c/app_python/requirements-dev.txt @@ -0,0 +1,5 @@ +pytest +pytest-cov +requests +ruff +httpx diff --git a/lab3c/app_python/requirements.txt b/lab3c/app_python/requirements.txt new file mode 100644 index 0000000000..792449289f --- /dev/null +++ b/lab3c/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 diff --git a/lab3c/app_python/tests/__init__.py b/lab3c/app_python/tests/__init__.py new file mode 100644 index 0000000000..792d600548 --- /dev/null +++ b/lab3c/app_python/tests/__init__.py @@ -0,0 +1 @@ +# diff --git a/lab3c/app_python/tests/test_app.py b/lab3c/app_python/tests/test_app.py new file mode 100644 index 0000000000..ff942f197e --- /dev/null +++ b/lab3c/app_python/tests/test_app.py @@ -0,0 +1,66 @@ +from datetime import datetime, timezone + +from fastapi.testclient import TestClient + +from app import _format_uptime, app, get_system_info, get_uptime, isoformat_utc + + +client = TestClient(app) + + +def test_root_endpoint_structure(): + response = client.get("/") + assert response.status_code == 200 + + data = response.json() + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + service = data["service"] + assert service["name"] == "devops-info-service" + assert service["framework"] == "FastAPI" + + system = data["system"] + for key in ["hostname", "platform", "platform_version", "architecture", "cpu_count", "python_version"]: + assert key in system + + runtime = data["runtime"] + assert isinstance(runtime["uptime_seconds"], int) + assert isinstance(runtime["uptime_human"], str) + assert runtime["timezone"] == "UTC" + + +def test_health_endpoint_structure(): + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert isinstance(data["uptime_seconds"], int) + assert "timestamp" in data + + +def test_not_found_returns_json(): + response = client.get("/does-not-exist") + assert response.status_code == 404 + data = response.json() + assert data["error"] == "Not Found" + + +def test_helpers_are_consistent(): + system = get_system_info() + assert system["hostname"] + assert system["platform"] + assert system["python_version"] + + uptime = get_uptime() + assert uptime["seconds"] >= 0 + assert "hours" in uptime["human"] or "hour" in uptime["human"] + + +def test_format_and_iso_helpers(): + assert _format_uptime(3660) == "1 hour, 1 minute" + test_dt = datetime(2024, 1, 1, tzinfo=timezone.utc) + assert isoformat_utc(test_dt) == "2024-01-01T00:00:00Z" From a2e9be9d5dd85027499d187c052e307defd6ddbe Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:16:40 +0300 Subject: [PATCH 03/14] fix: use env for secrets --- .github/workflows/python-ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 5284899721..2d040aed9a 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -23,6 +23,9 @@ jobs: test: name: Lint and Test runs-on: ubuntu-latest + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} strategy: fail-fast: true matrix: @@ -55,17 +58,17 @@ jobs: run: pytest --cov=app --cov-report=xml --cov-report=term - name: Upload coverage to Codecov - if: ${{ secrets.CODECOV_TOKEN != '' }} + if: ${{ env.CODECOV_TOKEN != '' }} uses: codecov/codecov-action@v4 with: files: lab3c/app_python/coverage.xml - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ env.CODECOV_TOKEN }} - name: Snyk scan - if: ${{ secrets.SNYK_TOKEN != '' }} + if: ${{ env.SNYK_TOKEN != '' }} uses: snyk/actions/python@master env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + SNYK_TOKEN: ${{ env.SNYK_TOKEN }} with: command: test args: --file=lab3c/app_python/requirements.txt From c547e13a2cd676f1a398888aaa5a9ce554a34109 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:20:13 +0300 Subject: [PATCH 04/14] trigger ci --- lab3c/app_python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lab3c/app_python/README.md b/lab3c/app_python/README.md index 321559cad9..e12a3ea6bc 100644 --- a/lab3c/app_python/README.md +++ b/lab3c/app_python/README.md @@ -1,7 +1,7 @@ # DevOps Info Service (FastAPI) [![Python CI](https://github.com/TsixPhoenix/DevOps-CC/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/TsixPhoenix/DevOps-CC/actions/workflows/python-ci.yml) -[![Coverage](https://codecov.io/gh/TsixPhoenix/DevOps-CC/branch/lab03/graph/badge.svg)](https://codecov.io/gh/TsixPhoenix/DevOps-CC) + ## Overview Small service returning system info about the machine it runs on, plus a health check. From 48163c47a621cfaa6f410530fa486187f31896de Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:21:49 +0300 Subject: [PATCH 05/14] fix: ci trigger path --- .github/workflows/python-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 2d040aed9a..7004955f17 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -2,12 +2,12 @@ name: Python CI (Lab03) on: push: - branches: [lab03, main, master] + branches: [lab3, main, master] paths: - "lab3c/app_python/**" - ".github/workflows/python-ci.yml" pull_request: - branches: [lab03, main, master] + branches: [lab3, main, master] paths: - "lab3c/app_python/**" - ".github/workflows/python-ci.yml" From dcf70721ea6c72e6bcd7b899e3f09c28b292b672 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:25:44 +0300 Subject: [PATCH 06/14] fix: snyk fix --- .github/workflows/python-ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 7004955f17..d61adcda2b 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -64,14 +64,16 @@ jobs: files: lab3c/app_python/coverage.xml token: ${{ env.CODECOV_TOKEN }} + - name: Install Snyk CLI + if: ${{ env.SNYK_TOKEN != '' }} + run: npm install -g snyk + - name: Snyk scan if: ${{ env.SNYK_TOKEN != '' }} - uses: snyk/actions/python@master + working-directory: lab3c/app_python + run: snyk test --file=requirements.txt --package-manager=pip env: SNYK_TOKEN: ${{ env.SNYK_TOKEN }} - with: - command: test - args: --file=lab3c/app_python/requirements.txt docker: name: Build and Push Docker Image From 25f225d6653b95a6e3482c4a6d52eb721ab5a18b Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:32:15 +0300 Subject: [PATCH 07/14] fix: upgrade fastApi to resolve snyk issues --- lab3c/app_python/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lab3c/app_python/requirements.txt b/lab3c/app_python/requirements.txt index 792449289f..01c3cb3565 100644 --- a/lab3c/app_python/requirements.txt +++ b/lab3c/app_python/requirements.txt @@ -1,2 +1,2 @@ -fastapi==0.115.0 +fastapi==0.128.6 uvicorn[standard]==0.32.0 From 38c2836de01ad0ea82bac61635fdd0f05c0c1a7e Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 10 Feb 2026 15:45:35 +0300 Subject: [PATCH 08/14] docs update --- lab3c/app_go/docs/LAB03.md | 5 ++--- lab3c/app_python/docs/LAB03.md | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lab3c/app_go/docs/LAB03.md b/lab3c/app_go/docs/LAB03.md index 2fc772a8e3..2ae68b20de 100644 --- a/lab3c/app_go/docs/LAB03.md +++ b/lab3c/app_go/docs/LAB03.md @@ -8,9 +8,8 @@ I added a separate workflow for the Go app with its own path filters. This keeps - Python workflow runs only when `lab3c/app_python/**` or its workflow file changes. ## Workflow Evidence -Add real links after CI runs: -- ✅ **Go workflow run:** `` -- ✅ **Docker image on Docker Hub:** `` +- **Go workflow run:** +- **Docker image on Docker Hub:** ## Notes - Go CI uses `go test` and a basic lint step. diff --git a/lab3c/app_python/docs/LAB03.md b/lab3c/app_python/docs/LAB03.md index 3a1772878c..530353eaba 100644 --- a/lab3c/app_python/docs/LAB03.md +++ b/lab3c/app_python/docs/LAB03.md @@ -11,7 +11,7 @@ ## 2. Workflow Evidence Add real links and outputs after you run CI: -- **Successful workflow run:** `` +- **Successful workflow run:** https://github.com/TsixPhoenix/DevOps-CC/actions/runs/21865003310/job/63103839665 - **Tests passing locally:** pytest ============================================================================================ test session starts ============================================================================================ @@ -24,8 +24,8 @@ collected 5 items tests\test_app.py ..... [100%] ============================================================================================= 5 passed in 0.36s ============================================================================================= -- **Docker image on Docker Hub:** `` -- **Status badge:** `` +- **Docker image on Docker Hub:** https://hub.docker.com/repository/docker/tsixphoenix/devops-info-python/general +- **Status badge:** Works, shows green check ## 3. Best Practices Implemented - **Dependency caching:** `actions/setup-python` caches pip packages to speed up installs. @@ -33,16 +33,24 @@ tests\test_app.py ..... - **Conditional push:** Docker images only push on `push` events (not on PRs). - **Concurrency:** newer runs cancel older runs for the same branch. - **Path filters:** CI runs only when the Python app changes (monorepo friendly). -- **Snyk scanning:** dependency scan runs in CI (requires token). +- **Snyk scanning:** dependency scan runs in CI. -Caching time saved: -``` - -``` Snyk result: ``` - +Run snyk test --file=requirements.txt --package-manager=pip + +Testing /home/runner/work/DevOps-CC/DevOps-CC/lab3c/app_python... + +Organization: tsixphoenix +Package manager: pip +Target file: requirements.txt +Project name: app_python +Open source: no +Project path: /home/runner/work/DevOps-CC/DevOps-CC/lab3c/app_python +Licenses: enabled + +✔ Tested 13 dependencies for known issues, no vulnerable paths found. ``` ## 4. Key Decisions @@ -54,5 +62,5 @@ Snyk result: **Test Coverage:** Core endpoints and helper functions are tested. I did not try to cover every logging line. -## 5. Challenges (Optional) +## 5. Challenges - Everything was clear, because of experience of setting up CI/CD in my company workspace. From ab7845153448c2114b9236246b4c5d655cbdbd94 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Thu, 19 Feb 2026 18:16:22 +0300 Subject: [PATCH 09/14] complete lab4 --- .github/workflows/terraform-ci.yml | 51 ++++++ lab4c/docs/LAB04.md | 190 +++++++++++++++++++++++ lab4c/pulumi/.gitignore | 19 +++ lab4c/pulumi/Pulumi.yaml | 4 + lab4c/pulumi/README.md | 50 ++++++ lab4c/pulumi/__main__.py | 102 ++++++++++++ lab4c/pulumi/requirements.txt | 3 + lab4c/terraform/.gitignore | 21 +++ lab4c/terraform/.tflint.hcl | 3 + lab4c/terraform/README.md | 59 +++++++ lab4c/terraform/main.tf | 101 ++++++++++++ lab4c/terraform/outputs.tf | 9 ++ lab4c/terraform/terraform.tfvars.example | 5 + lab4c/terraform/variables.tf | 26 ++++ 14 files changed, 643 insertions(+) create mode 100644 .github/workflows/terraform-ci.yml create mode 100644 lab4c/docs/LAB04.md create mode 100644 lab4c/pulumi/.gitignore create mode 100644 lab4c/pulumi/Pulumi.yaml create mode 100644 lab4c/pulumi/README.md create mode 100644 lab4c/pulumi/__main__.py create mode 100644 lab4c/pulumi/requirements.txt create mode 100644 lab4c/terraform/.gitignore create mode 100644 lab4c/terraform/.tflint.hcl create mode 100644 lab4c/terraform/README.md create mode 100644 lab4c/terraform/main.tf create mode 100644 lab4c/terraform/outputs.tf create mode 100644 lab4c/terraform/terraform.tfvars.example create mode 100644 lab4c/terraform/variables.tf diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml new file mode 100644 index 0000000000..42a0c50418 --- /dev/null +++ b/.github/workflows/terraform-ci.yml @@ -0,0 +1,51 @@ +name: Terraform Validate (Lab04) + +on: + push: + branches: [lab04, main, master] + paths: + - "lab4c/terraform/**" + - ".github/workflows/terraform-ci.yml" + pull_request: + branches: [lab04, main, master] + paths: + - "lab4c/terraform/**" + - ".github/workflows/terraform-ci.yml" + +jobs: + validate: + name: Format, Validate, Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: lab4c/terraform + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.9" + terraform_wrapper: false + + - name: Terraform Format Check + run: terraform fmt -check -recursive + + - name: Terraform Init + run: terraform init -backend=false + + - name: Terraform Validate + run: terraform validate + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: latest + + - name: TFLint Init + run: tflint --init + + - name: TFLint + run: tflint --format compact + continue-on-error: true diff --git a/lab4c/docs/LAB04.md b/lab4c/docs/LAB04.md new file mode 100644 index 0000000000..5af0ddc8e7 --- /dev/null +++ b/lab4c/docs/LAB04.md @@ -0,0 +1,190 @@ +# LAB04 — Infrastructure as Code (Terraform & Pulumi) + +## 1. Cloud Provider & Infrastructure + +- **Cloud provider:** Yandex Cloud. +- **Rationale:** Used Yandex Cloud for this lab because of the free tier +- **Instance:** standard-v3, 2 cores 20%, 1 GB RAM, 10 GB disk. +- **Zone:** ru-central1-a. +- **Estimated cost:** Effectively $0 with the free tier for this kind of usage. +- **Resources created:** + - 1× VPC network + - 1× subnet + - 1× security group (SSH 22, HTTP 80, 5000) + - 1× compute instance (Ubuntu 22.04) + - Public IP + +## 2. Terraform Implementation + +- **Terraform version:** Terraform v1.14.5 +- **Project structure:** `terraform/` — main.tf (provider, Ubuntu image data source, VPC, subnet, security group, instance), variables.tf, outputs.tf, terraform.tfvars (gitignored). Auth via service account key path in tfvars +- **Key decisions:** Variables for folder_id, zone, SSH key path, and SSH CIDR so the same code works across environments. Data source for the latest Ubuntu 22.04 LTS image. Security group restricts SSH to our IP only; HTTP and 5000 are open for the app. +- **Challenges:** Getting auth right at first; I ended up putting the key file path in terraform.tfvars). Also hit the VPC network quota once and had to extend it. + +**Terminal output:** + +- `terraform init`: + ``` +terraform init +Initializing the backend... +Initializing provider plugins... +- Finding yandex-cloud/yandex versions matching "~> 0.100"... +- Installing yandex-cloud/yandex v0.187.0... +- Installed yandex-cloud/yandex v0.187.0 + ``` +- `terraform plan`: + ``` +terraform plan +data.yandex_compute_image.ubuntu: Reading... +data.yandex_compute_image.ubuntu: Read complete after 0s [id=***********] + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the +following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance.lab4 will be created + + resource "yandex_compute_instance" "lab4" { + ``` +- `terraform apply`: + ``` +terraform apply +data.yandex_compute_image.ubuntu: Reading... +data.yandex_compute_image.ubuntu: Read complete after 0s [id=***********] + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the +following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance.lab4 will be created + + resource "yandex_compute_instance" "lab4" { + ``` +- `SSH to VM`: + ``` +The programs included with the Ubuntu system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by +applicable law. + +To run a command as administrator (user "root"), use "sudo ". +See "man sudo_root" for details. + +ubuntu@fhm24d5clqr3oh7b101s:~$ + ``` + +## 3. Pulumi Implementation + +- **Pulumi version and language:** Pulumi v3.222.0, Python 3.x. +- **How it differs from Terraform:** Same logical resources (VPC, subnet, security group, VM), but defined in Python. You get normal Python (loops, functions, types) and the same state/plan/apply workflow. +- **Advantages:** Felt easier. Outputs are straightforward. +- **Challenges:** Initial setup took a bit: venv, `setuptools<82` for `pkg_resources`, and provider auth. + +**Terminal output:** + +- `pulumi preview`: + ``` + pulumi preview +Previewing update (dev) + +View in Browser (Ctrl+O): + + Type Name Plan Info + + pulumi:pulumi:Stack lab4c-vm-dev create 2 messages + + ├─ yandex:index:VpcNetwork lab4c-network create + + ├─ yandex:index:VpcSubnet lab4c-subnet create + + ├─ yandex:index:VpcSecurityGroup lab4c-vm-sg create + + └─ yandex:index:ComputeInstance lab4c-vm create +Diagnostics: + pulumi:pulumi:Stack (lab4c-vm-dev): + import pkg_resources + +Outputs: + public_ip : [unknown] + ssh_command: [unknown] + +Resources: + + 5 to create + ``` +- `pulumi up`: + ``` + pulumi up +Previewing update (dev) + +View in Browser (Ctrl+O): + Type Name Plan Info + pulumi:pulumi:Stack lab4c-vm-dev 2 messages + + ├─ yandex:index:VpcNetwork lab4c-network create + + ├─ yandex:index:VpcSubnet lab4c-subnet create + + ├─ yandex:index:VpcSecurityGroup lab4c-vm-sg create + + └─ yandex:index:ComputeInstance lab4c-vm create +Diagnostics: + pulumi:pulumi:Stack (lab4c-vm-dev): + import pkg_resources + + [Pulumi Neo] Would you like help with these diagnostics? + +Outputs: + + public_ip : [unknown] + + ssh_command: [unknown] + +Resources: + + 4 to create + 1 unchanged + +Do you want to perform this update? yes +Updating (dev) + ``` +- SSH to VM: + ``` + The programs included with the Ubuntu system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by +applicable law. + +To run a command as administrator (user "root"), use "sudo ". +See "man sudo_root" for details. + +ubuntu@fhm8nea1kubnsde4ooqn:~$ + ``` + +## 4. Terraform vs Pulumi Comparison + +- **Ease of learning:** Terraform is easier if you only care about “describe resources in a file and apply.” HCL is small and focused. Pulumi is easier if you already know Python and want to use normal code; +- **Code readability:** Both are readable. Terraform is very declarative: you see resources and attributes. Pulumi looks like normal code, so you can structure it with variables and functions. +- **Debugging:** With Terraform, you rely on plan/apply messages and sometimes `terraform state`. With Pulumi, you get Python stack traces and can add prints or a debugger; the program runs in your environment, which helps. +- **Documentation:** all services are well documented +- **Use case:** I’d pick Terraform when the team is standardizing on it, when you want maximum portability (HCL, big ecosystem), or when you’re mostly gluing provider resources. I’d pick Pulumi when the team is code-first, when you want to share logic with the rest of your app (same language, tests, refactors), or when you need loops, conditionals, or abstractions that are clumsy in HCL. + +## 5. Lab 5 Preparation & Cleanup + +**VM for Lab 5:** + +- **Keeping VM for Lab 5?** No. +- **Plan for Lab 5:** Will recreate a cloud VM when needed + +**Cleanup status:** +``` +terraform destroy +Destroy complete! Resources: 4 destroyed. +``` +and +``` +pulumi destroy +Previewing destroy (dev) + +View in Browser (Ctrl+O): + + Type Name Plan + - pulumi:pulumi:Stack lab4c-vm-dev delete + - ├─ yandex:index:ComputeInstance lab4c-vm delete + - ├─ yandex:index:VpcSubnet lab4c-subnet delete + - ├─ yandex:index:VpcSecurityGroup lab4c-vm-sg delete + - └─ yandex:index:VpcNetwork lab4c-network delete +``` diff --git a/lab4c/pulumi/.gitignore b/lab4c/pulumi/.gitignore new file mode 100644 index 0000000000..25c1585ba2 --- /dev/null +++ b/lab4c/pulumi/.gitignore @@ -0,0 +1,19 @@ +# Pulumi +Pulumi.*.yaml +!Pulumi.yaml +.pulumi/ + +# Python +__pycache__/ +*.py[cod] +venv/ +.venv/ +*.egg-info/ + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/lab4c/pulumi/Pulumi.yaml b/lab4c/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..a3e2d9124e --- /dev/null +++ b/lab4c/pulumi/Pulumi.yaml @@ -0,0 +1,4 @@ +name: lab4c-vm +runtime: python +description: Lab 4 - VM on Yandex cloud + diff --git a/lab4c/pulumi/README.md b/lab4c/pulumi/README.md new file mode 100644 index 0000000000..5c263e239d --- /dev/null +++ b/lab4c/pulumi/README.md @@ -0,0 +1,50 @@ +# Lab 4 — Pulumi (Yandex Cloud) + +Same infrastructure as the Terraform stack: one VM, VPC, subnet, security group (SSH, HTTP, 5000). + +## Prerequisites + +- Pulumi CLI 3.x +- Python 3.9+ +- Yandex Cloud account (same auth as for Terraform: `YANDEX_TOKEN` or service account key) + +## Config + + +```bash +pulumi config set folder_id your-yandex-folder-id +pulumi config set ssh_cidr "YOUR_IP/32" +pulumi config set ssh_public_key "$(cat %USERPROFILE%\.ssh\id_rsa.pub)" +``` + +```powershell +pulumi config set ssh_public_key "$(Get-Content $env:USERPROFILE\.ssh\id_rsa.pub -Raw)" +``` + +Optional: `pulumi config set zone ru-central1-a` + +## Setup + +1. Log in to Pulumi: `pulumi login` +2. Create stack: `pulumi stack init dev` +3. Install deps and run: + + ```powershell + python -m venv venv + .\venv\Scripts\Activate.ps1 + pip install -r requirements.txt + pulumi preview + pulumi up + ``` + +4. SSH to VM: + + ```powershell + ssh ubuntu@$(pulumi stack output public_ip) + ``` + +## Cleanup + +```bash +pulumi destroy +``` diff --git a/lab4c/pulumi/__main__.py b/lab4c/pulumi/__main__.py new file mode 100644 index 0000000000..54482de1c3 --- /dev/null +++ b/lab4c/pulumi/__main__.py @@ -0,0 +1,102 @@ +"""Lab 4 - Create VM on Yandex Cloud (same as Terraform).""" +import os +import pulumi + +config = pulumi.Config() +key_file = config.get("yandex_service_account_key_file") +if key_file: + os.environ["YANDEX_SERVICE_ACCOUNT_KEY_FILE"] = key_file + +import pulumi_yandex as yandex + +folder_id = config.require("folder_id") +zone = config.get("zone") or "ru-central1-a" +ssh_public_key = config.require("ssh_public_key") +ssh_cidr = config.require("ssh_cidr") + +# Ubuntu 22.04 LTS +image = yandex.get_compute_image(family="ubuntu-2204-lts") + +network = yandex.VpcNetwork( + "lab4c-network", + name="lab4c-network", + folder_id=folder_id, +) + +subnet = yandex.VpcSubnet( + "lab4c-subnet", + name="lab4c-subnet", + network_id=network.id, + zone=zone, + folder_id=folder_id, + v4_cidr_blocks=["10.0.1.0/24"], +) + +sg = yandex.VpcSecurityGroup( + "lab4c-vm-sg", + name="lab4c-vm-sg", + network_id=network.id, + folder_id=folder_id, + description="Allow SSH, HTTP, and port 5000 for Lab 4", + ingresses=[ + yandex.VpcSecurityGroupIngressArgs( + description="SSH", + port=22, + protocol="TCP", + v4_cidr_blocks=[ssh_cidr], + ), + yandex.VpcSecurityGroupIngressArgs( + description="HTTP", + port=80, + protocol="TCP", + v4_cidr_blocks=["0.0.0.0/0"], + ), + yandex.VpcSecurityGroupIngressArgs( + description="App 5000", + port=5000, + protocol="TCP", + v4_cidr_blocks=["0.0.0.0/0"], + ), + ], + egresses=[ + yandex.VpcSecurityGroupEgressArgs( + description="Any", + protocol="ANY", + v4_cidr_blocks=["0.0.0.0/0"], + ), + ], +) + +vm = yandex.ComputeInstance( + "lab4c-vm", + name="lab4c-vm", + zone=zone, + folder_id=folder_id, + platform_id="standard-v3", + resources=yandex.ComputeInstanceResourcesArgs( + cores=2, + memory=1, + core_fraction=20, + ), + boot_disk=yandex.ComputeInstanceBootDiskArgs( + initialize_params=yandex.ComputeInstanceBootDiskInitializeParamsArgs( + image_id=image.id, + size=10, + type="network-hdd", + ), + ), + network_interfaces=[ + yandex.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, + security_group_ids=[sg.id], + ), + ], + metadata={ + "ssh-keys": f"ubuntu:{ssh_public_key}", + }, + labels={"lab": "lab04"}, +) + +pulumi.export("public_ip", vm.network_interfaces[0].nat_ip_address) +pulumi.export("ssh_command", pulumi.Output.concat("ssh ubuntu@", vm.network_interfaces[0].nat_ip_address)) diff --git a/lab4c/pulumi/requirements.txt b/lab4c/pulumi/requirements.txt new file mode 100644 index 0000000000..c2955fc5a6 --- /dev/null +++ b/lab4c/pulumi/requirements.txt @@ -0,0 +1,3 @@ +pulumi>=3.0.0 +pulumi-yandex>=0.13.0 +setuptools>=65.0.0,<82 diff --git a/lab4c/terraform/.gitignore b/lab4c/terraform/.gitignore new file mode 100644 index 0000000000..df419991b7 --- /dev/null +++ b/lab4c/terraform/.gitignore @@ -0,0 +1,21 @@ +# Terraform +*.tfstate +*.tfstate.* +.terraform/ +.terraform.lock.hcl +terraform.tfvars +*.tfvars +*.tfvars.json +crash.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Credentials +*.pem +*.key +*.json +!package.json +credentials +.env diff --git a/lab4c/terraform/.tflint.hcl b/lab4c/terraform/.tflint.hcl new file mode 100644 index 0000000000..75d15f14aa --- /dev/null +++ b/lab4c/terraform/.tflint.hcl @@ -0,0 +1,3 @@ +plugin "terraform" { + enabled = true +} diff --git a/lab4c/terraform/README.md b/lab4c/terraform/README.md new file mode 100644 index 0000000000..aee3cb8444 --- /dev/null +++ b/lab4c/terraform/README.md @@ -0,0 +1,59 @@ +# Lab 4 — Terraform (Yandex Cloud) + +Creates one VM (free tier: 2 cores 20%, 1 GB RAM, 10 GB disk), VPC, subnet, security group (SSH, HTTP, 5000), and outputs public IP. + +## Prerequisites + +- Terraform 1.9+ +- Yandex Cloud account +- SSH key pair on your machine (e.g. `ssh-keygen`); you will use the **public** key path in Terraform + +## Authentication + +Use one of these (do not commit secrets): + +1. **OAuth token (quick):** + `set YANDEX_TOKEN=your_oauth_token` (cmd) or `$env:YANDEX_TOKEN = "..."` (PowerShell) + +2. **Service account key file:** + Create a service account in Yandex Cloud Console, create an authorized key (JSON), then: + `set YANDEX_SERVICE_ACCOUNT_KEY_FILE=C:\path\to\key.json` + or in `terraform.tfvars`: `yandex_token` (prefer env vars). + +3. **Folder ID:** + In Console: Cloud → folder → copy ID. Set in `terraform.tfvars` as `yandex_folder_id`. + +## Setup + +1. Copy and edit variables: + - **Windows:** `copy terraform.tfvars.example terraform.tfvars` + - **Linux/macOS:** `cp terraform.tfvars.example terraform.tfvars` + Edit: + - `yandex_folder_id` — your folder ID + - `yandex_zone` — e.g. `ru-central1-a` + - `ssh_public_key_path` — full path to your `.pub` file (e.g. `C:\Users\You\.ssh\id_rsa.pub` or `%USERPROFILE%\.ssh\id_rsa.pub`) + - `ssh_cidr` — your IP/32 (e.g. from https://ifconfig.me) + +2. Initialize and apply: + ```bash + terraform init + terraform plan + terraform apply + ``` + +3. SSH to VM (no `-i` needed if you use the same key as the one in metadata): + - **PowerShell:** `ssh ubuntu@$(terraform output -raw public_ip)` + - Or: `ssh -i C:\path\to\your_private_key ubuntu@` + +## Cleanup + +```bash +terraform destroy +``` + +## Files + +- `main.tf` — provider, network, subnet, security group, instance +- `variables.tf` — folder_id, zone, ssh_public_key_path, ssh_cidr +- `outputs.tf` — public_ip, ssh_command +- `terraform.tfvars` — your values (gitignored) diff --git a/lab4c/terraform/main.tf b/lab4c/terraform/main.tf new file mode 100644 index 0000000000..5152f76f06 --- /dev/null +++ b/lab4c/terraform/main.tf @@ -0,0 +1,101 @@ +terraform { + required_version = ">= 1.9" + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = "~> 0.100" + } + } +} + +provider "yandex" { + zone = var.yandex_zone + folder_id = var.yandex_folder_id + service_account_key_file = var.yandex_service_account_key_file +} + +# Ubuntu 22.04 +data "yandex_compute_image" "ubuntu" { + family = "ubuntu-2204-lts" +} + +resource "yandex_vpc_network" "lab4" { + name = "lab4c-network" +} + +resource "yandex_vpc_subnet" "lab4" { + name = "lab4c-subnet" + network_id = yandex_vpc_network.lab4.id + zone = var.yandex_zone + v4_cidr_blocks = ["10.0.1.0/24"] +} + +resource "yandex_vpc_security_group" "lab4" { + name = "lab4c-vm-sg" + network_id = yandex_vpc_network.lab4.id + description = "Allow SSH, HTTP, and port 5000 for Lab 4" + + ingress { + description = "SSH" + port = 22 + protocol = "TCP" + v4_cidr_blocks = [var.ssh_cidr] + } + + ingress { + description = "HTTP" + port = 80 + protocol = "TCP" + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "App 5000" + port = 5000 + protocol = "TCP" + v4_cidr_blocks = ["0.0.0.0/0"] + } + + egress { + description = "Any" + from_port = 0 + to_port = 65535 + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "yandex_compute_instance" "lab4" { + name = "lab4c-vm" + platform_id = "standard-v3" + zone = var.yandex_zone + folder_id = var.yandex_folder_id + + resources { + cores = 2 + memory = 1 + core_fraction = 20 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu.id + size = 10 + type = "network-hdd" + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.lab4.id + nat = true + security_group_ids = [yandex_vpc_security_group.lab4.id] + } + + metadata = { + ssh-keys = "ubuntu:${file(var.ssh_public_key_path)}" + } + + labels = { + lab = "lab04" + } +} diff --git a/lab4c/terraform/outputs.tf b/lab4c/terraform/outputs.tf new file mode 100644 index 0000000000..2821ecd1bc --- /dev/null +++ b/lab4c/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "public_ip" { + description = "Public IP of the VM" + value = yandex_compute_instance.lab4.network_interface[0].nat_ip_address +} + +output "ssh_command" { + description = "Example SSH command" + value = "ssh ubuntu@${yandex_compute_instance.lab4.network_interface[0].nat_ip_address}" +} diff --git a/lab4c/terraform/terraform.tfvars.example b/lab4c/terraform/terraform.tfvars.example new file mode 100644 index 0000000000..f36767e4d3 --- /dev/null +++ b/lab4c/terraform/terraform.tfvars.example @@ -0,0 +1,5 @@ +yandex_folder_id = +yandex_zone = +yandex_service_account_key_file = +ssh_public_key_path = +ssh_cidr = "1.2.3.4/32" diff --git a/lab4c/terraform/variables.tf b/lab4c/terraform/variables.tf new file mode 100644 index 0000000000..0d121ae84c --- /dev/null +++ b/lab4c/terraform/variables.tf @@ -0,0 +1,26 @@ +variable "yandex_folder_id" { + description = "Yandex Cloud folder ID" + type = string +} + +variable "yandex_zone" { + description = "Yandex Cloud zone" + type = string + default = "ru-central1-a" +} + +variable "ssh_public_key_path" { + description = "Path to your SSH public key file" + type = string +} + +variable "ssh_cidr" { + description = "CIDR allowed for SSH" + type = string +} + +variable "yandex_service_account_key_file" { + description = "Path to Yandex service account JSON key" + type = string + default = null +} From ad335a4f860b8c19bd2fdd82f51f44c01bc8624b Mon Sep 17 00:00:00 2001 From: Phoenix Date: Tue, 24 Feb 2026 14:24:41 +0300 Subject: [PATCH 10/14] lab5 complete --- .gitignore | 12 +- lab5c/README.md | 38 ++++ lab5c/ansible/.gitignore | 4 + lab5c/ansible/ansible.cfg | 13 ++ lab5c/ansible/docs/LAB05.md | 180 ++++++++++++++++++ lab5c/ansible/group_vars/all.yml | 23 +++ lab5c/ansible/group_vars/all.yml.example | 19 ++ lab5c/ansible/inventory/hosts.ini | 5 + lab5c/ansible/playbooks/deploy.yml | 10 + lab5c/ansible/playbooks/provision.yml | 8 + lab5c/ansible/playbooks/site.yml | 12 ++ lab5c/ansible/requirements.yml | 4 + .../roles/app_deploy/defaults/main.yml | 11 ++ .../roles/app_deploy/handlers/main.yml | 6 + lab5c/ansible/roles/app_deploy/tasks/main.yml | 62 ++++++ lab5c/ansible/roles/common/defaults/main.yml | 13 ++ lab5c/ansible/roles/common/tasks/main.yml | 15 ++ lab5c/ansible/roles/docker/defaults/main.yml | 16 ++ lab5c/ansible/roles/docker/handlers/main.yml | 5 + lab5c/ansible/roles/docker/tasks/main.yml | 56 ++++++ 20 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 lab5c/README.md create mode 100644 lab5c/ansible/.gitignore create mode 100644 lab5c/ansible/ansible.cfg create mode 100644 lab5c/ansible/docs/LAB05.md create mode 100644 lab5c/ansible/group_vars/all.yml create mode 100644 lab5c/ansible/group_vars/all.yml.example create mode 100644 lab5c/ansible/inventory/hosts.ini create mode 100644 lab5c/ansible/playbooks/deploy.yml create mode 100644 lab5c/ansible/playbooks/provision.yml create mode 100644 lab5c/ansible/playbooks/site.yml create mode 100644 lab5c/ansible/requirements.yml create mode 100644 lab5c/ansible/roles/app_deploy/defaults/main.yml create mode 100644 lab5c/ansible/roles/app_deploy/handlers/main.yml create mode 100644 lab5c/ansible/roles/app_deploy/tasks/main.yml create mode 100644 lab5c/ansible/roles/common/defaults/main.yml create mode 100644 lab5c/ansible/roles/common/tasks/main.yml create mode 100644 lab5c/ansible/roles/docker/defaults/main.yml create mode 100644 lab5c/ansible/roles/docker/handlers/main.yml create mode 100644 lab5c/ansible/roles/docker/tasks/main.yml diff --git a/.gitignore b/.gitignore index 30d74d2584..11a8dd47f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,11 @@ -test \ No newline at end of file +test + +# Ansible +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ + +# Local lab 5 runtime artifacts +lab5c/ansible/.vault_pass +lab5c/ansible/*.retry \ No newline at end of file diff --git a/lab5c/README.md b/lab5c/README.md new file mode 100644 index 0000000000..b59d1ce2cc --- /dev/null +++ b/lab5c/README.md @@ -0,0 +1,38 @@ +# Lab 05 Completion (`lab5c`) + + +## Structure + +- `ansible/ansible.cfg` - project configuration +- `ansible/inventory/hosts.ini` - static inventory template +- `ansible/roles/common` - base system setup role +- `ansible/roles/docker` - Docker installation role +- `ansible/roles/app_deploy` - app deployment role +- `ansible/playbooks/provision.yml` - provisioning playbook +- `ansible/playbooks/deploy.yml` - deployment playbook +- `ansible/playbooks/site.yml` - full provision + deploy flow +- `ansible/group_vars/all.yml.example` - vault variable template +- `ansible/docs/LAB05.md` - documentation template with analysis + +## Control-Node Setup (WSL) + +```bash +sudo apt update +sudo apt install -y ansible +ansible-galaxy collection install -r requirements.yml +``` + +Bonus dynamic-inventory collection: + +```bash +ansible-galaxy collection install -r requirements-bonus.yml +``` + +## Typical Run Order + +```bash +ansible all -m ping +ansible-playbook playbooks/provision.yml +ansible-playbook playbooks/provision.yml +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` \ No newline at end of file diff --git a/lab5c/ansible/.gitignore b/lab5c/ansible/.gitignore new file mode 100644 index 0000000000..b487bb7be0 --- /dev/null +++ b/lab5c/ansible/.gitignore @@ -0,0 +1,4 @@ +.vault_pass +*.retry +inventory/*.pyc +__pycache__/ diff --git a/lab5c/ansible/ansible.cfg b/lab5c/ansible/ansible.cfg new file mode 100644 index 0000000000..2fb9889628 --- /dev/null +++ b/lab5c/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = False +interpreter_python = auto_silent +timeout = 30 + +[privilege_escalation] +become = True +become_method = sudo +become_user = root diff --git a/lab5c/ansible/docs/LAB05.md b/lab5c/ansible/docs/LAB05.md new file mode 100644 index 0000000000..7db4ecb2c6 --- /dev/null +++ b/lab5c/ansible/docs/LAB05.md @@ -0,0 +1,180 @@ +# LAB05 — Ansible Fundamentals (Role-Based) + +## 1. Architecture Overview + +- **Ansible version used:** Ansible Core 2.17.8. +- **Control node:** Windows 10 + Docker Desktop (Ansible executed in container). +- **Target VM:** Ubuntu 22.04/24.04 VM from Lab 4 (cloud), connected via SSH. +- **Role structure:** Three roles are used: + - `common` - baseline OS preparation + - `docker` - Docker engine installation and service setup + - `app_deploy` - Dockerized app deployment and health verification +- **Why roles instead of one large playbook:** Roles isolate responsibilities, keep playbooks clean, and make each part reusable. + +## 2. Roles Documentation + +### Role: `common` + +- **Purpose:** Prepare the system with required base packages and timezone. +- **Variables (defaults):** + - `common_packages` - essential packages list (`curl`, `git`, `python3-pip`, etc.) + - `common_timezone` - default `UTC` +- **Handlers:** None. +- **Dependencies:** `community.general` collection (for timezone module). + +### Role: `docker` + +- **Purpose:** Install Docker from the official Docker APT repository and prepare runtime access. +- **Variables (defaults):** + - `docker_arch_map`, `docker_arch` + - `docker_packages` (`docker-ce`, `docker-ce-cli`, `containerd.io`, plugins) + - `docker_python_package` (`python3-docker`) + - `docker_user` (user added to `docker` group) +- **Handlers:** + - `restart docker` - restarts Docker service when package changes require it. +- **Dependencies:** Uses Ansible built-in modules; installs `python3-docker` on target host for Docker-related modules. + +### Role: `app_deploy` + +- **Purpose:** Authenticate to Docker Hub, pull image, replace container, and verify app health. +- **Variables (defaults):** + - `app_name`, `docker_image`, `docker_image_tag` + - `app_port`, `app_container_name` + - `app_restart_policy`, `app_env` + - `app_health_path`, `app_wait_timeout` + - Vaulted vars: `dockerhub_username`, `dockerhub_password` +- **Handlers:** + - `restart app container` - restarts running container when deployment task triggers notify. +- **Dependencies:** `community.docker` collection. + +## 3. Idempotency Demonstration + +### First run (`playbooks/provision.yml`) + +```text +PLAY [Provision web servers] +... +TASK [common : Update apt cache] changed +TASK [common : Install common packages] changed +TASK [common : Configure timezone] changed +TASK [docker : Download Docker official GPG key] changed +TASK [docker : Add Docker APT repository] changed +TASK [docker : Update apt cache after Docker repo changes] changed +TASK [docker : Install Docker engine and CLI packages] changed +TASK [docker : Add target user to docker group] changed +RUNNING HANDLER [docker : restart docker] changed +... +PLAY RECAP +lab5-vm : ok=14 changed=9 unreachable=0 failed=0 skipped=0 +``` + +### Second run (`playbooks/provision.yml`) + +```text +PLAY [Provision web servers] +... +TASK [common : Update apt cache] ok +TASK [common : Install common packages] ok +TASK [common : Configure timezone] ok +TASK [docker : Download Docker official GPG key] ok +TASK [docker : Add Docker APT repository] ok +TASK [docker : Install Docker engine and CLI packages] ok +TASK [docker : Add target user to docker group] ok +TASK [docker : Update apt cache after Docker repo changes] skipping +... +PLAY RECAP +lab5-vm : ok=12 changed=0 unreachable=0 failed=0 skipped=1 +``` + +### Analysis + +- On the first run, resources are created/configured to match desired state (packages, repo, Docker service, group membership). +- On the second run, Ansible modules compare desired and current state and skip unnecessary changes, proving idempotent behavior. +- Idempotency is achieved by stateful modules (`apt`, `service`, `user`, `docker_container`) instead of ad-hoc shell commands. + +## 4. Ansible Vault Usage + +- Credentials are stored in `group_vars/all.yml` encrypted via Ansible Vault. +- Vault password is entered interactively (`--ask-vault-pass`) or provided via local password file that is ignored by Git. +- Tasks containing credentials use `no_log: true` to prevent secret leakage in logs. + +### Encrypted file proof + +```text +$ANSIBLE_VAULT;1.1;AES256 +64383638346636396532383762376239633430663933613638326235653962353634323766343664 +3436646365333032316364663736356565616462353663310a303061333835663866303562323132 +65356163313437653263333138366561633533646662336634393333313737336439326132323666 +``` + +### Why Vault is important + +- Secrets can be committed safely only in encrypted form. +- Team members can share infrastructure code without exposing credentials. +- It reduces accidental secret leakage in repo history and CI logs. + +## 5. Deployment Verification + +### Deployment run (`playbooks/deploy.yml`) + +```text +PLAY [Deploy application] +... +TASK [app_deploy : Log in to Docker Hub] changed +TASK [app_deploy : Pull application image] changed +TASK [app_deploy : Run application container] changed +TASK [app_deploy : Wait for app port to be ready] ok +TASK [app_deploy : Verify health endpoint] ok +RUNNING HANDLER [app_deploy : restart app container] changed +... +PLAY RECAP +lab5-vm : ok=8 changed=4 unreachable=0 failed=0 skipped=2 +``` + +### Container status + +```text +lab5-vm | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +e45f2bb4472d tsixphoenix/devops-info-python:latest "python app.py" 58 seconds ago Up 49 seconds 0.0.0.0:5000->5000/tcp devops-info-python +``` + +### Health check + +```text +curl http://89.169.158.161:5000/health +{"status":"healthy","timestamp":"2026-02-24T11:09:07.680263Z","uptime_seconds":14} + +curl http://89.169.158.161:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"FastAPI"},"system":{"hostname":"e45f2bb4472d","platform":"Linux","platform_version":"5.15.0-170-generic","architecture":"x86_64","cpu_count":2,"python_version":"3.13.12"},"runtime":{"uptime_seconds":16,"uptime_human":"0 hours, 0 minutes","current_time":"2026-02-24T11:09:09.533021Z","timezone":"UTC"},"request":{"client_ip":"188.130.155.186","user_agent":"curl/8.16.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} +``` + +### Handler execution + +- Docker role handler: executed when Docker package changes require service restart. +- App deploy handler: executes only when container deployment task reports changes. + +## 6. Key Decisions + +- **Why use roles instead of plain playbooks?** + Roles separate concerns and keep top-level playbooks minimal. This reduces complexity and improves readability as automation grows. + +- **How do roles improve reusability?** + Roles encapsulate tasks + defaults + handlers. The same role can be reused across environments by changing only inventory and variables. + +- **What makes a task idempotent?** + Idempotent tasks declare target state (for example, `state: present`, `state: started`) and change only when current state differs. + +- **How do handlers improve efficiency?** + Handlers run only when notified by changed tasks, so expensive operations (like restarts) are not executed on every run. + +- **Why is Ansible Vault necessary?** + It allows secure storage of credentials in versioned infrastructure code without exposing plaintext secrets. + +## 7. Challenges + +- Initial control-node setup on Windows (Ansible-in-Docker + mounted SSH key permissions). +- Correctly configuring Docker repository and architecture mapping. +- Verifying no secret values appear in output logs. + +--- \ No newline at end of file diff --git a/lab5c/ansible/group_vars/all.yml b/lab5c/ansible/group_vars/all.yml new file mode 100644 index 0000000000..5f415c4704 --- /dev/null +++ b/lab5c/ansible/group_vars/all.yml @@ -0,0 +1,23 @@ +$ANSIBLE_VAULT;1.1;AES256 +64383638346636396532383762376239633430663933613638326235653962353634323766343664 +3436646365333032316364663736356565616462353663310a303061333835663866303562323132 +65356163313437653263333138366561633533646662336634393333313737336439326132323666 +3862636235616563310a373334663339636438663966653834356330663464633263613633326130 +34336536353233343036643965633262613162366332373436636537616131353730303334666438 +34326635656361623062326333666333393430316566383132656436643535623363346663333366 +39376364653165376138383561623036373133616130366661313764383837396432303631336565 +36636631383963623537333836303430313431373335653534333064393033373861636332316339 +36383730633662396633336664633138643935363637383934326331366366653139333462656161 +37646535653066616161663836336561396264326336313935643163323164346634316634363036 +64383130616332323630303561313566373461376531643732366334616562616431386364643561 +35383362633536326434376639363531346362336666393334636337316262303763326333343762 +30373635633762623431333335663232616335666332353665326263636362323934393135336435 +65323534333033616538373964386336663637633935366137356363383135336238393637336430 +61363661366261653634383934393430336361376166666261303935356337343234306330303462 +37326236393832376461653865356265393463326362333635653532633161326235336566316436 +34373436313533636333306437393966656536396435326666356536373763356132613263613038 +39353530393937363161656264663436313934373832623262633865363538313434303661633362 +36653233643231323066343639666630303632393333323966633437633762306535643436616131 +39383433393430303536343565303362616431666137613234663330336438323937356265666438 +38396130356666333032613834326637353230343235303031303363386137323736643466333963 +3065646533393438336638646163633461373432356339353831 diff --git a/lab5c/ansible/group_vars/all.yml.example b/lab5c/ansible/group_vars/all.yml.example new file mode 100644 index 0000000000..aa703de75b --- /dev/null +++ b/lab5c/ansible/group_vars/all.yml.example @@ -0,0 +1,19 @@ +--- +# Copy this file to group_vars/all.yml and encrypt it with: +# ansible-vault encrypt group_vars/all.yml + +# Docker Hub credentials +dockerhub_username: "DOCKERHUB_USERNAME" +dockerhub_password: "DOCKERHUB_ACCESS_TOKEN" + +# Application config +app_name: "devops-info-python" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +app_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: "unless-stopped" +app_env: {} +app_health_path: "/health" +app_wait_timeout: 120 diff --git a/lab5c/ansible/inventory/hosts.ini b/lab5c/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..3bb6bcd055 --- /dev/null +++ b/lab5c/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +lab5-vm ansible_host=89.169.158.161 ansible_user=ubuntu ansible_ssh_private_key_file=/ssh/id_ed25519 + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/lab5c/ansible/playbooks/deploy.yml b/lab5c/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..532fb1e207 --- /dev/null +++ b/lab5c/ansible/playbooks/deploy.yml @@ -0,0 +1,10 @@ +--- +- name: Deploy application + hosts: webservers + become: true + + vars_files: + - ../group_vars/all.yml + + roles: + - app_deploy diff --git a/lab5c/ansible/playbooks/provision.yml b/lab5c/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..7cc2e6678d --- /dev/null +++ b/lab5c/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: true + + roles: + - common + - docker diff --git a/lab5c/ansible/playbooks/site.yml b/lab5c/ansible/playbooks/site.yml new file mode 100644 index 0000000000..5621849987 --- /dev/null +++ b/lab5c/ansible/playbooks/site.yml @@ -0,0 +1,12 @@ +--- +- name: Provision and deploy application + hosts: webservers + become: true + + vars_files: + - ../group_vars/all.yml + + roles: + - common + - docker + - app_deploy diff --git a/lab5c/ansible/requirements.yml b/lab5c/ansible/requirements.yml new file mode 100644 index 0000000000..b869f415df --- /dev/null +++ b/lab5c/ansible/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.docker + - name: community.general diff --git a/lab5c/ansible/roles/app_deploy/defaults/main.yml b/lab5c/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..7d7997a13a --- /dev/null +++ b/lab5c/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,11 @@ +--- +app_name: "devops-info-python" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +app_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: "unless-stopped" +app_env: {} +app_health_path: "/health" +app_wait_timeout: 120 diff --git a/lab5c/ansible/roles/app_deploy/handlers/main.yml b/lab5c/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..1fc3fba48b --- /dev/null +++ b/lab5c/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: started + restart: true diff --git a/lab5c/ansible/roles/app_deploy/tasks/main.yml b/lab5c/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..9567528545 --- /dev/null +++ b/lab5c/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: Log in to Docker Hub + community.docker.docker_login: + registry_url: https://index.docker.io/v1/ + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: true + +- name: Pull application image + community.docker.docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_image_tag }}" + source: pull + +- name: Read current container info + community.docker.docker_container_info: + name: "{{ app_container_name }}" + register: current_app_container + +- name: Stop existing app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: stopped + when: + - current_app_container.exists | default(false) + - current_app_container.container.State.Status | default("") == "running" + +- name: Remove old app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + when: current_app_container.exists | default(false) + +- name: Run application container + community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ app_restart_policy }}" + published_ports: + - "{{ app_port }}:5000" + env: "{{ app_env }}" + recreate: true + notify: restart app container + +- name: Wait for app port to be ready + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ app_port | int }}" + delay: 2 + timeout: "{{ app_wait_timeout }}" + +- name: Verify health endpoint + ansible.builtin.uri: + url: "http://127.0.0.1:{{ app_port }}{{ app_health_path }}" + method: GET + status_code: 200 + return_content: true + register: app_health_result + retries: 5 + delay: 3 + until: app_health_result.status == 200 diff --git a/lab5c/ansible/roles/common/defaults/main.yml b/lab5c/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..49e2e4526d --- /dev/null +++ b/lab5c/ansible/roles/common/defaults/main.yml @@ -0,0 +1,13 @@ +--- +common_packages: + - apt-transport-https + - ca-certificates + - curl + - git + - gnupg + - htop + - lsb-release + - python3-pip + - vim + +common_timezone: "UTC" diff --git a/lab5c/ansible/roles/common/tasks/main.yml b/lab5c/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..e9085097f4 --- /dev/null +++ b/lab5c/ansible/roles/common/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + +- name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + +- name: Configure timezone + community.general.timezone: + name: "{{ common_timezone }}" + when: common_timezone | default("") | length > 0 diff --git a/lab5c/ansible/roles/docker/defaults/main.yml b/lab5c/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..546f4a7af0 --- /dev/null +++ b/lab5c/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,16 @@ +--- +docker_arch_map: + x86_64: amd64 + aarch64: arm64 + +docker_arch: "{{ docker_arch_map.get(ansible_architecture, 'amd64') }}" + +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + +docker_python_package: python3-docker +docker_user: "{{ ansible_user | default('ubuntu') }}" diff --git a/lab5c/ansible/roles/docker/handlers/main.yml b/lab5c/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..1a5058da5e --- /dev/null +++ b/lab5c/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/lab5c/ansible/roles/docker/tasks/main.yml b/lab5c/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..bc99133596 --- /dev/null +++ b/lab5c/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: Install APT dependencies for Docker repository + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + state: present + update_cache: true + +- name: Ensure Docker keyring directory exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + +- name: Download Docker official GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: "0644" + +- name: Add Docker APT repository + ansible.builtin.apt_repository: + repo: "deb [arch={{ docker_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + filename: docker + state: present + register: docker_repo + +- name: Update apt cache after Docker repo changes + ansible.builtin.apt: + update_cache: true + when: docker_repo is changed + +- name: Install Docker engine and CLI packages + ansible.builtin.apt: + name: "{{ docker_packages + [docker_python_package] }}" + state: present + notify: restart docker + +- name: Ensure docker group exists + ansible.builtin.group: + name: docker + state: present + +- name: Add target user to docker group + ansible.builtin.user: + name: "{{ docker_user }}" + groups: docker + append: true + +- name: Ensure Docker service is enabled and running + ansible.builtin.service: + name: docker + state: started + enabled: true From f015f9142f6eedfbd973f4bf2eb2269cd5727e08 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Thu, 5 Mar 2026 15:33:06 +0300 Subject: [PATCH 11/14] Complete Lab 6: Advanced Ansible & CI/CD --- .github/workflows/ansible-deploy.yml | 86 +++++++++ lab6c/ansible/.gitignore | 4 + lab6c/ansible/ansible.cfg | 13 ++ lab6c/ansible/docs/LAB05.md | 180 ++++++++++++++++++ lab6c/ansible/docs/LAB06.md | 164 ++++++++++++++++ lab6c/ansible/group_vars/all.yml | 33 ++++ lab6c/ansible/group_vars/all.yml.example | 22 +++ lab6c/ansible/inventory/hosts.ini | 5 + lab6c/ansible/playbooks/deploy.yml | 10 + lab6c/ansible/playbooks/provision.yml | 8 + lab6c/ansible/playbooks/site.yml | 12 ++ lab6c/ansible/requirements.yml | 4 + lab6c/ansible/roles/common/defaults/main.yml | 13 ++ lab6c/ansible/roles/common/tasks/main.yml | 58 ++++++ lab6c/ansible/roles/docker/defaults/main.yml | 16 ++ lab6c/ansible/roles/docker/handlers/main.yml | 5 + lab6c/ansible/roles/docker/tasks/main.yml | 105 ++++++++++ lab6c/ansible/roles/web_app/defaults/main.yml | 19 ++ lab6c/ansible/roles/web_app/handlers/main.yml | 5 + lab6c/ansible/roles/web_app/meta/main.yml | 4 + lab6c/ansible/roles/web_app/tasks/main.yml | 84 ++++++++ lab6c/ansible/roles/web_app/tasks/wipe.yml | 37 ++++ .../web_app/templates/docker-compose.yml.j2 | 15 ++ 23 files changed, 902 insertions(+) create mode 100644 .github/workflows/ansible-deploy.yml create mode 100644 lab6c/ansible/.gitignore create mode 100644 lab6c/ansible/ansible.cfg create mode 100644 lab6c/ansible/docs/LAB05.md create mode 100644 lab6c/ansible/docs/LAB06.md create mode 100644 lab6c/ansible/group_vars/all.yml create mode 100644 lab6c/ansible/group_vars/all.yml.example create mode 100644 lab6c/ansible/inventory/hosts.ini create mode 100644 lab6c/ansible/playbooks/deploy.yml create mode 100644 lab6c/ansible/playbooks/provision.yml create mode 100644 lab6c/ansible/playbooks/site.yml create mode 100644 lab6c/ansible/requirements.yml create mode 100644 lab6c/ansible/roles/common/defaults/main.yml create mode 100644 lab6c/ansible/roles/common/tasks/main.yml create mode 100644 lab6c/ansible/roles/docker/defaults/main.yml create mode 100644 lab6c/ansible/roles/docker/handlers/main.yml create mode 100644 lab6c/ansible/roles/docker/tasks/main.yml create mode 100644 lab6c/ansible/roles/web_app/defaults/main.yml create mode 100644 lab6c/ansible/roles/web_app/handlers/main.yml create mode 100644 lab6c/ansible/roles/web_app/meta/main.yml create mode 100644 lab6c/ansible/roles/web_app/tasks/main.yml create mode 100644 lab6c/ansible/roles/web_app/tasks/wipe.yml create mode 100644 lab6c/ansible/roles/web_app/templates/docker-compose.yml.j2 diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..8dd016cee1 --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,86 @@ +name: Ansible Deployment (Lab06) + +on: + push: + branches: [main, master, lab6] + paths: + - "lab6c/ansible/**" + - "!.github/workflows/ansible-deploy.yml" + pull_request: + branches: [main, master, lab6] + paths: + - "lab6c/ansible/**" + +concurrency: + group: ansible-deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Ansible Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: lab6c/ansible + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Ansible and ansible-lint + run: | + pip install ansible ansible-lint + ansible-galaxy collection install -r requirements.yml + + - name: Run ansible-lint + run: ansible-lint playbooks/*.yml 2>/dev/null || echo "Lint finished (warnings may appear)" + + deploy: + name: Deploy Application + needs: lint + runs-on: ubuntu-latest + if: github.event_name == 'push' + defaults: + run: + working-directory: lab6c/ansible + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Ansible and collections + run: | + pip install ansible + ansible-galaxy collection install -r requirements.yml + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Deploy with Ansible + env: + ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} + run: | + echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + ansible-playbook playbooks/deploy.yml \ + --vault-password-file /tmp/vault_pass \ + -e ansible_ssh_private_key_file=~/.ssh/id_ed25519 \ + -e ansible_host=${{ secrets.VM_HOST }} \ + -e ansible_user=${{ secrets.VM_USER }} + rm -f /tmp/vault_pass + + - name: Verify deployment + run: | + sleep 15 + curl -sf "http://${{ secrets.VM_HOST }}:5000/health" || echo "Health check failed" + curl -sf "http://${{ secrets.VM_HOST }}:5000/" || echo "Root check failed" diff --git a/lab6c/ansible/.gitignore b/lab6c/ansible/.gitignore new file mode 100644 index 0000000000..b487bb7be0 --- /dev/null +++ b/lab6c/ansible/.gitignore @@ -0,0 +1,4 @@ +.vault_pass +*.retry +inventory/*.pyc +__pycache__/ diff --git a/lab6c/ansible/ansible.cfg b/lab6c/ansible/ansible.cfg new file mode 100644 index 0000000000..2fb9889628 --- /dev/null +++ b/lab6c/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = False +interpreter_python = auto_silent +timeout = 30 + +[privilege_escalation] +become = True +become_method = sudo +become_user = root diff --git a/lab6c/ansible/docs/LAB05.md b/lab6c/ansible/docs/LAB05.md new file mode 100644 index 0000000000..7db4ecb2c6 --- /dev/null +++ b/lab6c/ansible/docs/LAB05.md @@ -0,0 +1,180 @@ +# LAB05 — Ansible Fundamentals (Role-Based) + +## 1. Architecture Overview + +- **Ansible version used:** Ansible Core 2.17.8. +- **Control node:** Windows 10 + Docker Desktop (Ansible executed in container). +- **Target VM:** Ubuntu 22.04/24.04 VM from Lab 4 (cloud), connected via SSH. +- **Role structure:** Three roles are used: + - `common` - baseline OS preparation + - `docker` - Docker engine installation and service setup + - `app_deploy` - Dockerized app deployment and health verification +- **Why roles instead of one large playbook:** Roles isolate responsibilities, keep playbooks clean, and make each part reusable. + +## 2. Roles Documentation + +### Role: `common` + +- **Purpose:** Prepare the system with required base packages and timezone. +- **Variables (defaults):** + - `common_packages` - essential packages list (`curl`, `git`, `python3-pip`, etc.) + - `common_timezone` - default `UTC` +- **Handlers:** None. +- **Dependencies:** `community.general` collection (for timezone module). + +### Role: `docker` + +- **Purpose:** Install Docker from the official Docker APT repository and prepare runtime access. +- **Variables (defaults):** + - `docker_arch_map`, `docker_arch` + - `docker_packages` (`docker-ce`, `docker-ce-cli`, `containerd.io`, plugins) + - `docker_python_package` (`python3-docker`) + - `docker_user` (user added to `docker` group) +- **Handlers:** + - `restart docker` - restarts Docker service when package changes require it. +- **Dependencies:** Uses Ansible built-in modules; installs `python3-docker` on target host for Docker-related modules. + +### Role: `app_deploy` + +- **Purpose:** Authenticate to Docker Hub, pull image, replace container, and verify app health. +- **Variables (defaults):** + - `app_name`, `docker_image`, `docker_image_tag` + - `app_port`, `app_container_name` + - `app_restart_policy`, `app_env` + - `app_health_path`, `app_wait_timeout` + - Vaulted vars: `dockerhub_username`, `dockerhub_password` +- **Handlers:** + - `restart app container` - restarts running container when deployment task triggers notify. +- **Dependencies:** `community.docker` collection. + +## 3. Idempotency Demonstration + +### First run (`playbooks/provision.yml`) + +```text +PLAY [Provision web servers] +... +TASK [common : Update apt cache] changed +TASK [common : Install common packages] changed +TASK [common : Configure timezone] changed +TASK [docker : Download Docker official GPG key] changed +TASK [docker : Add Docker APT repository] changed +TASK [docker : Update apt cache after Docker repo changes] changed +TASK [docker : Install Docker engine and CLI packages] changed +TASK [docker : Add target user to docker group] changed +RUNNING HANDLER [docker : restart docker] changed +... +PLAY RECAP +lab5-vm : ok=14 changed=9 unreachable=0 failed=0 skipped=0 +``` + +### Second run (`playbooks/provision.yml`) + +```text +PLAY [Provision web servers] +... +TASK [common : Update apt cache] ok +TASK [common : Install common packages] ok +TASK [common : Configure timezone] ok +TASK [docker : Download Docker official GPG key] ok +TASK [docker : Add Docker APT repository] ok +TASK [docker : Install Docker engine and CLI packages] ok +TASK [docker : Add target user to docker group] ok +TASK [docker : Update apt cache after Docker repo changes] skipping +... +PLAY RECAP +lab5-vm : ok=12 changed=0 unreachable=0 failed=0 skipped=1 +``` + +### Analysis + +- On the first run, resources are created/configured to match desired state (packages, repo, Docker service, group membership). +- On the second run, Ansible modules compare desired and current state and skip unnecessary changes, proving idempotent behavior. +- Idempotency is achieved by stateful modules (`apt`, `service`, `user`, `docker_container`) instead of ad-hoc shell commands. + +## 4. Ansible Vault Usage + +- Credentials are stored in `group_vars/all.yml` encrypted via Ansible Vault. +- Vault password is entered interactively (`--ask-vault-pass`) or provided via local password file that is ignored by Git. +- Tasks containing credentials use `no_log: true` to prevent secret leakage in logs. + +### Encrypted file proof + +```text +$ANSIBLE_VAULT;1.1;AES256 +64383638346636396532383762376239633430663933613638326235653962353634323766343664 +3436646365333032316364663736356565616462353663310a303061333835663866303562323132 +65356163313437653263333138366561633533646662336634393333313737336439326132323666 +``` + +### Why Vault is important + +- Secrets can be committed safely only in encrypted form. +- Team members can share infrastructure code without exposing credentials. +- It reduces accidental secret leakage in repo history and CI logs. + +## 5. Deployment Verification + +### Deployment run (`playbooks/deploy.yml`) + +```text +PLAY [Deploy application] +... +TASK [app_deploy : Log in to Docker Hub] changed +TASK [app_deploy : Pull application image] changed +TASK [app_deploy : Run application container] changed +TASK [app_deploy : Wait for app port to be ready] ok +TASK [app_deploy : Verify health endpoint] ok +RUNNING HANDLER [app_deploy : restart app container] changed +... +PLAY RECAP +lab5-vm : ok=8 changed=4 unreachable=0 failed=0 skipped=2 +``` + +### Container status + +```text +lab5-vm | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +e45f2bb4472d tsixphoenix/devops-info-python:latest "python app.py" 58 seconds ago Up 49 seconds 0.0.0.0:5000->5000/tcp devops-info-python +``` + +### Health check + +```text +curl http://89.169.158.161:5000/health +{"status":"healthy","timestamp":"2026-02-24T11:09:07.680263Z","uptime_seconds":14} + +curl http://89.169.158.161:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"FastAPI"},"system":{"hostname":"e45f2bb4472d","platform":"Linux","platform_version":"5.15.0-170-generic","architecture":"x86_64","cpu_count":2,"python_version":"3.13.12"},"runtime":{"uptime_seconds":16,"uptime_human":"0 hours, 0 minutes","current_time":"2026-02-24T11:09:09.533021Z","timezone":"UTC"},"request":{"client_ip":"188.130.155.186","user_agent":"curl/8.16.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} +``` + +### Handler execution + +- Docker role handler: executed when Docker package changes require service restart. +- App deploy handler: executes only when container deployment task reports changes. + +## 6. Key Decisions + +- **Why use roles instead of plain playbooks?** + Roles separate concerns and keep top-level playbooks minimal. This reduces complexity and improves readability as automation grows. + +- **How do roles improve reusability?** + Roles encapsulate tasks + defaults + handlers. The same role can be reused across environments by changing only inventory and variables. + +- **What makes a task idempotent?** + Idempotent tasks declare target state (for example, `state: present`, `state: started`) and change only when current state differs. + +- **How do handlers improve efficiency?** + Handlers run only when notified by changed tasks, so expensive operations (like restarts) are not executed on every run. + +- **Why is Ansible Vault necessary?** + It allows secure storage of credentials in versioned infrastructure code without exposing plaintext secrets. + +## 7. Challenges + +- Initial control-node setup on Windows (Ansible-in-Docker + mounted SSH key permissions). +- Correctly configuring Docker repository and architecture mapping. +- Verifying no secret values appear in output logs. + +--- \ No newline at end of file diff --git a/lab6c/ansible/docs/LAB06.md b/lab6c/ansible/docs/LAB06.md new file mode 100644 index 0000000000..1f7ff3799f --- /dev/null +++ b/lab6c/ansible/docs/LAB06.md @@ -0,0 +1,164 @@ +# Lab 6 — Advanced Ansible & CI/CD + +## 1. Overview + +This lab extends Lab 5 with: +- **Blocks and tags** in common and docker roles +- **Docker Compose** for app deployment (replacing docker run) +- **Wipe logic** (variable + tag) for clean removal +- **GitHub Actions** workflow for automated deployment + +## 2. Blocks & Tags + +### Common Role +- **packages** block: apt update + install, with rescue (retry apt on failure), always (log completion) +- **users** block: ensure sudo group +- **common** tag: entire role + +### Docker Role +- **docker_install** block: repo setup, package install; rescue (wait 10s, retry); always (ensure service enabled) +- **docker_config** block: docker group, add user +- **docker** tag: entire role + +### Web App Role +- **app_deploy**, **compose** tags: deployment tasks +- **web_app_wipe** tag: wipe tasks only + +### Execution Examples +```bash +ansible-playbook playbooks/provision.yml --tags "docker" +ansible-playbook playbooks/provision.yml --skip-tags "common" +ansible-playbook playbooks/provision.yml --tags "packages" +ansible-playbook playbooks/provision.yml --list-tags +``` + +## 3. Docker Compose Migration + +- **Template:** `roles/web_app/templates/docker-compose.yml.j2` +- **Project dir:** `/opt/{{ app_name }}` +- **Role dependency:** `web_app` depends on `docker` (meta/main.yml) +- **Module:** `community.docker.docker_compose_v2` with `state: present`, `pull: always` + +## 4. Wipe Logic + +- **Variable:** `web_app_wipe: false` (default) +- **Tag:** `web_app_wipe` +- **Tasks:** `roles/web_app/tasks/wipe.yml` — compose down, remove file, remove dir + +### Scenarios +1. **Normal deploy:** `ansible-playbook playbooks/deploy.yml` — wipe skipped +2. **Wipe only:** `ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" --tags web_app_wipe` +3. **Clean reinstall:** `ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true"` +4. **Safety:** `--tags web_app_wipe` without variable — wipe skipped (when blocks it) + +## 5. CI/CD Integration + +- **Workflow:** `.github/workflows/ansible-deploy.yml` +- **Triggers:** push to `lab6c/ansible/**` +- **Jobs:** lint (ansible-lint), deploy (playbook + verify) +- **Secrets required:** `ANSIBLE_VAULT_PASSWORD`, `SSH_PRIVATE_KEY`, `VM_HOST`, `VM_USER` + +## 6. Testing Results + +### 6.1 Provision with tags +```bash +ansible-playbook playbooks/provision.yml --tags "docker" +``` +``` +PLAY RECAP ********************************************************************* +lab5-vm : ok=9 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + +### 6.2 List of tags +```bash +ansible-playbook playbooks/provision.yml --list-tags +``` +``` +playbook: playbooks/provision.yml + play #1 (webservers): Provision web servers TAGS: [] + TASK TAGS: [common, docker, docker_config, docker_install, packages, users] +``` + +### 6.3 Deploy +```bash +ansible-playbook playbooks/deploy.yml +``` +``` +PLAY RECAP ********************************************************************* +lab5-vm : ok=16 changed=2 unreachable=0 failed=0 skipped=5 rescued=0 ignored=0 +``` + +### 6.4 Wipe-only +```bash +ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" --tags web_app_wipe +``` +``` +TASK [web_app : Log wipe completion] ******************************************* +ok: [lab5-vm] => {"msg": "Application devops-info-python wiped successfully"} +PLAY RECAP ********************************************************************* +lab5-vm : ok=6 changed=3 unreachable=0 failed=0 skipped=0 +``` + +### 6.5 Clean reinstall +```bash +ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" +``` +``` +PLAY RECAP ********************************************************************* +lab5-vm : ok=20 changed=3 unreachable=0 failed=0 skipped=1 rescued=0 ignored=1 +``` + +### 6.6 Health check +```bash +curl http://62.84.127.190:5000/health +``` +```json +{"status":"healthy","timestamp":"2026-03-05T12:17:53.667273Z","uptime_seconds":60} +``` + +### 6.7 Idempotency (2nd deploy run) +```bash +ansible-playbook playbooks/deploy.yml +``` +Second run: `changed=0` (all `ok`, no changes). + +### 6.8 Scenario 4a — safety (--tags web_app_wipe without variable) +```bash +ansible-playbook playbooks/deploy.yml --tags web_app_wipe +``` +Wipe tasks skipped (when blocks: `web_app_wipe` false by default). Deploy runs normally. + +### 6.9 GitHub Actions +Add 4 secrets and push. Include screenshot of successful workflow in report. + + +## 7. Challenges & Solutions + +- **Template `to_native` filter:** Ansible 2.16+ does not provide `to_native` in Jinja2 — replaced with `to_json`. +- **dpkg lock:** On a new VM, `unattended-upgrades` blocks apt; retry `provision` after updates complete succeeds. +- **Wipe on empty directory:** `docker_compose_v2 state: absent` fails if directory was already removed. Added `compose_dir_stat` check before `compose down`. + +## 8. Research Answers + +### Task 1 — Blocks & Tags +- **If rescue also fails?** Play will fail with error; can add `ignore_errors` or nested rescue. +- **Nested blocks?** Yes, a block can contain another block. +- **Tag inheritance?** Tags on block apply to all tasks inside. + +### Task 2 — Docker Compose +- **restart: always vs unless-stopped?** `unless-stopped` does not restart container after manual stop. +- **Compose networks vs bridge?** Compose creates named networks; bridge is the default network. +- **Vault in template?** Yes, Vault variables are available when templating. + +### Task 3 — Wipe Logic +- **Variable + tag?** Double safety: variable prevents accidental wipe; tag enables selective execution. +- **never tag vs our approach?** `never` disables task by tag; our approach requires both tag and variable. +- **Wipe before deploy?** Enables clean reinstall: wipe → deploy in one run. +- **Clean reinstall vs rolling update?** Reinstall = full replacement; rolling = phased update without downtime. +- **Extending wipe?** Can add `docker image prune` and `docker volume rm` to wipe.yml. + +### Task 4 — CI/CD +- **SSH keys in Secrets?** Use short-lived keys; regular rotation; restrict scope. +- **Staging → production?** Separate inventory/playbooks, approval before prod, or environment protection. +- **Rollbacks?** Add tag/version to image, keep previous config, workflow for rollback. +- **Self-hosted vs GitHub-hosted?** Self-hosted gives direct network/VMs access; fewer SSH key exposure risks. diff --git a/lab6c/ansible/group_vars/all.yml b/lab6c/ansible/group_vars/all.yml new file mode 100644 index 0000000000..e12b23ba74 --- /dev/null +++ b/lab6c/ansible/group_vars/all.yml @@ -0,0 +1,33 @@ +$ANSIBLE_VAULT;1.1;AES256 +61633462363861316436623662633839646361313433333434316236643637393039656137333630 +3638333365396330616461636436633866333531633930630a383464376530653839613930383661 +31313332303966653138366435366666353838316232633836393534646261333437396530386164 +6138376139346638330a613032643265376438343935356238613235343434356638653963316365 +65626665313563383639323930346438363239386565626434656337383430343732333962396335 +61613535636538626165313333613464633935343361353163353366333966653665383832373838 +61356436323939613636616635333836346330323531623037353736366462393336356633616132 +39343936353837316132373338616633353834333065303536326439366530666539336561303036 +64383632313331613932363934313264646464336635343535363631333031653664326530653038 +39356366366237396366386637313939306637373438366162303962386536633936626130633433 +63393363613863333965316537383439623732303862386238306637343136396634626639666335 +31363333356239303631306432656265306135643238656366346635663637666465303761653162 +66326363643065343062646634366636636166336136363862616630363030636533623861626132 +63633232373638623331323231323366326236316331663961656236666237393361653533343435 +38323333363938363237613432353362396330333961326332383634396333663336613665396637 +37383366643764363438616536323463303634396139363837343335366662653835373630303131 +66393063373339323630393238306638663335623232303239613831353932376465663834663736 +37396661323332303061633538393035356531623065396634633433623862666538356538343761 +61653630346237656663356462343366653163326261663138303132373932323863663566393932 +34653132623136633734663664356631663839363533353631373162306339653938636161633939 +62306639396634646366316662306533663337376262666333653431383562656138643264376437 +66376630326437353766613733333434333861613830303366663133363733656561393666613364 +65656636316663666438323635343062626138393963303837393536353466626161353531313733 +37373866363435303436346339393566656431326233333336343636303638313632363466653963 +32353963356431383433396461653034353963346462653066356662396462383432363231633739 +64646563306534323565396263623236356130643234313239393232366633333034383466653438 +37313138363764306561343364393838353963373464633864356666376536383131626638333332 +35333538306161633465663966663464643032343665393438366538623666346263333839393532 +61393132313662346266346234393766616532356638663432626236363238303063666135626663 +35346434346632653164646530323833656433386465313037653231336365363739336661636163 +33346463303439383837376363343430333161396431653538313466323563343964363238333132 +35303738346436393766 diff --git a/lab6c/ansible/group_vars/all.yml.example b/lab6c/ansible/group_vars/all.yml.example new file mode 100644 index 0000000000..335be5e489 --- /dev/null +++ b/lab6c/ansible/group_vars/all.yml.example @@ -0,0 +1,22 @@ +--- +# Copy to group_vars/all.yml and encrypt: ansible-vault encrypt group_vars/all.yml + +# Docker Hub (required for deploy) +dockerhub_username: "YOUR_DOCKERHUB_USERNAME" +dockerhub_password: "YOUR_DOCKERHUB_ACCESS_TOKEN" + +# Application config (Lab 6 Docker Compose) +app_name: "devops-info-python" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +app_port: 5000 +app_internal_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: "unless-stopped" +app_env: {} +app_health_path: "/health" +app_wait_timeout: 120 + +# Docker Compose project directory on target VM +compose_project_dir: "/opt/{{ app_name }}" diff --git a/lab6c/ansible/inventory/hosts.ini b/lab6c/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..b0c44a8fd4 --- /dev/null +++ b/lab6c/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +lab5-vm ansible_host=62.84.127.190 ansible_user=ubuntu ansible_ssh_private_key_file=/ssh/id_ed25519 + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/lab6c/ansible/playbooks/deploy.yml b/lab6c/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..f3923b77bb --- /dev/null +++ b/lab6c/ansible/playbooks/deploy.yml @@ -0,0 +1,10 @@ +--- +- name: Deploy application + hosts: webservers + become: true + + vars_files: + - ../group_vars/all.yml + + roles: + - web_app diff --git a/lab6c/ansible/playbooks/provision.yml b/lab6c/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..7cc2e6678d --- /dev/null +++ b/lab6c/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: true + + roles: + - common + - docker diff --git a/lab6c/ansible/playbooks/site.yml b/lab6c/ansible/playbooks/site.yml new file mode 100644 index 0000000000..f90334eff7 --- /dev/null +++ b/lab6c/ansible/playbooks/site.yml @@ -0,0 +1,12 @@ +--- +- name: Provision and deploy application + hosts: webservers + become: true + + vars_files: + - ../group_vars/all.yml + + roles: + - common + - docker + - web_app diff --git a/lab6c/ansible/requirements.yml b/lab6c/ansible/requirements.yml new file mode 100644 index 0000000000..b869f415df --- /dev/null +++ b/lab6c/ansible/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.docker + - name: community.general diff --git a/lab6c/ansible/roles/common/defaults/main.yml b/lab6c/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..49e2e4526d --- /dev/null +++ b/lab6c/ansible/roles/common/defaults/main.yml @@ -0,0 +1,13 @@ +--- +common_packages: + - apt-transport-https + - ca-certificates + - curl + - git + - gnupg + - htop + - lsb-release + - python3-pip + - vim + +common_timezone: "UTC" diff --git a/lab6c/ansible/roles/common/tasks/main.yml b/lab6c/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..bb93353eee --- /dev/null +++ b/lab6c/ansible/roles/common/tasks/main.yml @@ -0,0 +1,58 @@ +--- +# Common role: baseline system setup +# Tags: packages, users, common + +- name: Install packages + block: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + tags: + - packages + - common + + - name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + tags: + - packages + - common + + rescue: + - name: Retry apt update on failure + ansible.builtin.apt: + update_cache: true + ignore_errors: true + + - name: Re-run package install after cache fix + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + + always: + - name: Log common role completion + ansible.builtin.copy: + content: "common role completed at {{ ansible_date_time.iso8601 }}\n" + dest: /tmp/ansible_common_completed + mode: "0644" + tags: + - common + +- name: User and group setup + block: + - name: Ensure sudo group exists + ansible.builtin.group: + name: sudo + state: present + tags: + - users + - common + +- name: Configure timezone + community.general.timezone: + name: "{{ common_timezone }}" + when: common_timezone | default("") | length > 0 + tags: + - common diff --git a/lab6c/ansible/roles/docker/defaults/main.yml b/lab6c/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..546f4a7af0 --- /dev/null +++ b/lab6c/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,16 @@ +--- +docker_arch_map: + x86_64: amd64 + aarch64: arm64 + +docker_arch: "{{ docker_arch_map.get(ansible_architecture, 'amd64') }}" + +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + +docker_python_package: python3-docker +docker_user: "{{ ansible_user | default('ubuntu') }}" diff --git a/lab6c/ansible/roles/docker/handlers/main.yml b/lab6c/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..1a5058da5e --- /dev/null +++ b/lab6c/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/lab6c/ansible/roles/docker/tasks/main.yml b/lab6c/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..990a1dc49c --- /dev/null +++ b/lab6c/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,105 @@ +--- +# Docker role: install Docker engine from official repo +# Tags: docker_install, docker_config, docker + +- name: Docker installation + block: + - name: Install APT dependencies for Docker repository + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + state: present + update_cache: true + tags: + - docker_install + - docker + + - name: Ensure Docker keyring directory exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + tags: + - docker_install + - docker + + - name: Download Docker official GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: "0644" + tags: + - docker_install + - docker + + - name: Add Docker APT repository + ansible.builtin.apt_repository: + repo: "deb [arch={{ docker_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + filename: docker + state: present + register: docker_repo + tags: + - docker_install + - docker + + - name: Update apt cache after Docker repo changes + ansible.builtin.apt: + update_cache: true + when: docker_repo is changed + tags: + - docker_install + - docker + + - name: Install Docker engine and CLI packages + ansible.builtin.apt: + name: "{{ docker_packages + [docker_python_package] }}" + state: present + notify: restart docker + tags: + - docker_install + - docker + + rescue: + - name: Wait before retry after GPG/repo failure + ansible.builtin.pause: + seconds: 10 + prompt: "Retrying Docker repo setup..." + + - name: Retry apt update + ansible.builtin.apt: + update_cache: true + + always: + - name: Ensure Docker service is enabled and running + ansible.builtin.service: + name: docker + state: started + enabled: true + tags: + - docker + - docker_config + +- name: Docker configuration + block: + - name: Ensure docker group exists + ansible.builtin.group: + name: docker + state: present + tags: + - docker_config + - docker + + - name: Add target user to docker group + ansible.builtin.user: + name: "{{ docker_user }}" + groups: docker + append: true + tags: + - docker_config + - docker + + tags: + - docker_config + - docker diff --git a/lab6c/ansible/roles/web_app/defaults/main.yml b/lab6c/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..0e150a57fe --- /dev/null +++ b/lab6c/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# Application config +app_name: "devops-info-python" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +app_port: 5000 +app_internal_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: "unless-stopped" +app_env: {} +app_health_path: "/health" +app_wait_timeout: 120 + +# Docker Compose +compose_project_dir: "/opt/{{ app_name }}" + +# Wipe logic: set to true to remove app completely. Also requires --tags web_app_wipe for wipe-only. +web_app_wipe: false diff --git a/lab6c/ansible/roles/web_app/handlers/main.yml b/lab6c/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..f8bfe8ed4f --- /dev/null +++ b/lab6c/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart app container + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: present diff --git a/lab6c/ansible/roles/web_app/meta/main.yml b/lab6c/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..a5177c0f80 --- /dev/null +++ b/lab6c/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,4 @@ +--- +# web_app depends on docker - Docker must be installed before deploying containers +dependencies: + - role: docker diff --git a/lab6c/ansible/roles/web_app/tasks/main.yml b/lab6c/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..063676e3cf --- /dev/null +++ b/lab6c/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,84 @@ +--- +# web_app role: deploy containerized app with Docker Compose +# Tags: app_deploy, compose, web_app +# Dependency: docker role (installed automatically via meta/main.yml) + +# Wipe logic runs first when explicitly requested +- name: Include wipe tasks + include_tasks: wipe.yml + tags: + - web_app_wipe + +# Deployment block +- name: Deploy application with Docker Compose + block: + - name: Log in to Docker Hub + community.docker.docker_login: + registry_url: https://index.docker.io/v1/ + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: true + tags: + - app_deploy + - compose + + - name: Create application directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: directory + mode: "0755" + tags: + - app_deploy + - compose + + - name: Template docker-compose.yml + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir }}/docker-compose.yml" + mode: "0644" + tags: + - app_deploy + - compose + + - name: Deploy with Docker Compose (up) + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: present + pull: always + tags: + - app_deploy + - compose + + - name: Wait for app port to be ready + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ app_port | int }}" + delay: 2 + timeout: "{{ app_wait_timeout }}" + tags: + - app_deploy + - compose + + - name: Verify health endpoint + ansible.builtin.uri: + url: "http://127.0.0.1:{{ app_port }}{{ app_health_path }}" + method: GET + status_code: 200 + return_content: true + register: app_health_result + retries: 5 + delay: 3 + until: app_health_result.status == 200 + tags: + - app_deploy + - compose + + rescue: + - name: Log deployment failure + ansible.builtin.debug: + msg: "Deployment failed, check logs above" + + tags: + - app_deploy + - compose + - web_app diff --git a/lab6c/ansible/roles/web_app/tasks/wipe.yml b/lab6c/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..fbeb835e4f --- /dev/null +++ b/lab6c/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,37 @@ +--- +# Wipe web application: stops containers, removes compose file and app directory +# Runs only when web_app_wipe=true AND tag web_app_wipe specified +# Usage: ansible-playbook deploy.yml -e "web_app_wipe=true" --tags web_app_wipe + +- name: Check if app directory exists + ansible.builtin.stat: + path: "{{ compose_project_dir }}" + register: compose_dir_stat + +- name: Wipe web application + block: + - name: Stop and remove containers with Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: absent + when: compose_dir_stat.stat.isdir | default(false) + + - name: Remove docker-compose file + ansible.builtin.file: + path: "{{ compose_project_dir }}/docker-compose.yml" + state: absent + ignore_errors: true + + - name: Remove application directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: absent + ignore_errors: true + + - name: Log wipe completion + ansible.builtin.debug: + msg: "Application {{ app_name }} wiped successfully" + + when: web_app_wipe | default(false) | bool + tags: + - web_app_wipe diff --git a/lab6c/ansible/roles/web_app/templates/docker-compose.yml.j2 b/lab6c/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..7285e18cf1 --- /dev/null +++ b/lab6c/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,15 @@ +--- +# Generated by Ansible - do not edit manually +# Template: roles/web_app/templates/docker-compose.yml.j2 + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_image_tag }} + container_name: {{ app_container_name }} + ports: + - "{{ app_port }}:{{ app_internal_port }}" + environment: {{ app_env | default({}) | to_json }} + restart: {{ app_restart_policy }} + {% if app_extra_hosts is defined and app_extra_hosts %} + extra_hosts: {{ app_extra_hosts | to_json }} + {% endif %} From a43ffc68f05995a25256f85bc77dce66b7d8dfc8 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Thu, 5 Mar 2026 15:43:51 +0300 Subject: [PATCH 12/14] docs: workflow screenshot --- lab6c/ansible/docs/workflow.jpg | Bin 0 -> 35396 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lab6c/ansible/docs/workflow.jpg diff --git a/lab6c/ansible/docs/workflow.jpg b/lab6c/ansible/docs/workflow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bd8ac1af348dea682cc5842a3d77cc9e03d5315d GIT binary patch literal 35396 zcmd421zcTC@*sL~ce}U~+=IKjy99T42oT)e9fCt}cL>2fKybIUng1OOL+3P2Jd@^<~8JO~^L006FA z001KXA2j0(0H7rV001ukgGTWg06+-_0Gg-%LHmbH9F3ff{%8&ol!BX^0{~a0005FU z0D$ow0D#y1qYRY%e~gU;R73z8mjmdq0N4V|03-lufIYwjzzD)v0n7jv0LSkYfG7YG z0^;ol2>}TO2?+%a4+9MXBsh3jcoZa5R1_o>WHbyQCK@^pIx-3t0TvD}EL%EJTw$Ey#HbN{SANy3r+`~0Rct^07nCZKm+^T54r#_ z064^(BK`#sP>|3t;9#(zQe05M8~jxUnjLUR2&mu707M8-MN|k>5M}GGN#j#`Unb&m z+)tmxhr!9jiBGc_JbH*7#5nBhwiebs#DcW6zs&k9hAX>JSv!F#_{d#v+Vd{}Ic#rh zim)Z?;rdwC`?dRp0)Q`3@1-{jVHRV#e5}UdI*+YhpEVzeZ(afg;Aox0PQ|M7U!jQ{ zocogRVBa2T_sG3smf&U~HpI>`B@V1Sw66W9ULdNRbs0Y?q=|==yIeUu-F2AV2-J8i zv`J7b9MI;nHLTvYE4;tz=w?Do)UB#X)iGP6P6 zXg9x*+C6INK&iHRluV+70&U6X&8k>sCx zf+b6(0TcQs!W%k{)%q|e?bPmkIbZpZ$aCV8QRP-`U)7KF7avKaR?)qOa;v-NA;X!U ztrajUuiQ0@{uxo9zw3G4b+Lu%Lkzm_6h5bVkWUnw zJWTn|W*IJbnZ#CVfG}pAl&q5-w)lRZ-teODd=1fpA~5PVKqs?)8?hpbgQ78}KCB0lRxuVjx_W~+3{10d04F>~u{$5NKx$X5sdh!ZQ$76OIROyNnjfn7 zog9CSeM%T2+)ymjb8>d`8X=CUK3n=}zo`En0~FP-d)S?lq~qz>qFJr^7qK*@mOSdReby=FFnm%IfK8p>YTb&%f#f$ zg(IprOQHml5dJ%Yes}d^ZF5L|^Wty) zAg$+t+t(oNV5s_>^G4Z;PtVPKVc$q-moAJ6bKdKrE1I=BSnoX8afwo9IMv`859Jy* ziT-;sBqD|hc+)c-^)zw<-V{xS(BbAPb$l0fmIhADA+cS8QR5?W84#d z*jI7v|1er_s|H)W&1MHQuJ`J`nfkX#i7SRX53HAVR(p#^`Hc-s;3yK}STP_E_a}g* zQ%??({XO?~ep5^T&;LvT;pXtm+Z7Rv^|!pn&z#y_+K+P+9G2F9F})3!oSbOddI`8M zTzQb9;Y^diLiyw8%Bw~l^r=J&aHSHVMzU2@$HeY7tIX8ygT3o-fG)C|&D`@Dqg#cE z16Qgw(qQA~ze&g(o`GRjP0K7fsLP`D_!5;`{ zl$W>Pv|T^R@d!vV%YJ8Ti)=5cDUVIq$zc-(DazmAUnn=QS*e*1*E)+r9DK0#?|dMK z-z)Tw49KUvT_qFhXIGgQdU~6XRB`Gh%gc~TE(mghf3QI?|JBhS#5cpiibDV+T=^co zzxv}9F4xyPPc6+*1!hw@!|uOq&A;f1Y^JgexV8GF&6Y#lS0v5%vS89K&$Tl~R=0|j z@myRcW^6sVYJpA&KQ&Y4EhKpZ+(7?7&VMH0u*H!=N^m=T|4ak`*r5KK{M(F2(XKXK z#uAQZe(9cIGF&*JY`xaqSDDY_gy1yl8yqfo+!-w|_(1 z4VND(^4`NX$2RJ3Yep%m)3!V-MERwV8}?RVzyJD2Gyg(b&>}nj2k%1*EPzZOgcD7u zg89e)9&m!H{1-amEvDbMo?dl)mIBwceK@>))x>9f7VrN@UvJ6381}zd*MG6UA^+(N zg8TnB)c|in8#5|sfq;QSL4t!pf4!uu0^G7*9&e8d06ccItYLQKmk_TnvF3K);3?EE`Dne)JCi z%JI0-R~vm8c-c9~ze>1^v1@#pGR%!)jL~Wp;Ni9jX8=3Sn_pevWc3?x zNYBBFC;8p~8hBSCXU@K)t5FBsj9Th@m^O1(agbIT#3v`xrdQlK%HM$!Al4ce^uh0~ zhM*5aLJy^Mf@apL$UO%3B0$6x(x;rPQ(oLXDhyyMH)=YG}FQxiyE_-DvOg*b~GQ@vwA! zVN=K@P)VynO`P_02!Z68I}hyYB;Q5+K=(~*Z%8#x9H?;V{9XiNjMv?4&@o3NCkx9c zt+=llMw2sT>sQ{MU7$OcwPvqoB&!~O(yyG%RECQZYvQ=ARjZqzl!hR}Ttch4&Ais_xlY-);Z=y((*(=~ z+od>XJ1`wCqYa&fSeX+j{8Vyu(`uk|y;a>6_NmZ2O0QRy>!e?0drU&E&UcpBE(sJ% zveUqNkBV5A1j>=UcMh*2eiFJGb4_;tKax)b-HLHoTD2$Y)6t}4G6SkV(!S!1Twdec zwivWvlawM0UotE&A+pSoYt%Wb{iK8M=3$7c1$-FYMPyrhDR{&*Ss*ixKz6Ai4NLhb zRxcn_MP4UfbSz;{7AKY0Q_h%y?s7lhMqB$*|A^|BZ&7MMs`1rG1=6V&5jtpf+BWYq zD*ZwUb)Q6cp@JCKx`I(>xuSw9XO$=sqp@zVE)7@m1{1I|PsB#VTFKLUZ1)azmW~u# zD0`nw-bPUZIveJ~7yTf<6pC5~ZimWY0p@8iC1T)UDGGn!j41)BV3&cbJ_k*xph_+3 za_)sxrR_6blh#*YP6wclGzK$Yj)fO!q7oinr3t%9RjJcFfQE5z#Pd=uR-3*I9{jO_ z&;Ew1M~nDSe{0SnzsLZmwObsQ&ulHPrj1k4+oGn8BWtCO=KUBFW9f|zRh0EaCR;L& zUIzMFU}<(yN04iL5_^wV&eA?Nx`)b3LT*>VnjyS8inq1nt>ahfb+ zs{a3yto33uLzMAGyro3`S zidfhe4VOgSyY71UetIa@e4BhKqhYhF9mRGJyO33L#pX^*q5b+$iHX80SN}lvD8T+> z_5D&8CJZZ$`1Wr=v&Mbe$Yo2HlhApx%z8wU|2#>GV;Vlm91-s_g%!m7bL(m->2tuy=u zWKGx`l6!n7dMMD>y*e$M4%{{oFq1pV)anDlv4a^QxqLdBirx>&pn&tYiD&nNvMdyfIaxNpRza~7;Gi=~KC-VvLJ>a7oWg*azvBg2LnS*g6Cw|yz! z_+e>~l_TSeVZj??2k^s7!00C6J2GHd>0JDCP2|>VjEjz04SN)iA~%Gl%q2zWzp9v> z;(oF}uR^?w&Z9FId6{|SR!Wv|Nr*I^r94{7f#*umx&(?FjZmiyvl4u&ok>-aM9kJG z<_M64JLU+Ln~pCZWiIh{B4xl<0GrfL7|-5ZNNCUIC^@3K39fPzQ23a~Xz;F)LtcAW zJJSFaP7_E!3uirB&O7)^J$7`EYR#i6v{p-LYi9|~Nki+Ue2&knT0a*lQ>@%j_$BaB zfDh&jnYRB6fp2NCt5>vT^{YZ-G{KtE-iK4=^fo+;TKYX2sPjl45rSeVlln&?Eu0ga zkyD_QMKT4qFLSqoS^*hw+Yt-yXq(PlbZz^G7`Iv~Y5xLEv`Z?x$;Uo?Eq|kO9$&*w z>cFAzW4pWf7&Vh&sIjDD``(xND>D&xN2S9Zqh3@qxF5~$KW5+U8%1lfzlQ#yKu;My z*`aV5Z6>V_dfa;C-vbFT(2=KbBoP9A3MSrJn|@LSBcWh9;*>JY1*5Sl^j&3|fDvxXM!^ zZ!&G<6e&r=8QVOK6cc%`a za)vC%JIh(jcjiTkrX&$$ayEF1G2yF?JF`5*m(BDJ)F$r}SP~#@RgpgC@fo}$S+f0?2tnl3BRId;WdZ~zZWBq0FtMjTvWPD=a} zpJUMcVY8kM7;>?f2)hZz!-Fe!nD8jzqwZ%`g7gSs@Xs+im?4ylPjZdNN2yiD?}mD+ zzC)5esyuQ-_a9&9_mhV^Kd%3>HuL)Cu`6L4T=3ZfFXqve=#pN%TE0p^;zRtflr`r* z@IL9t=H#1=La+Cp^Zj~AsEDzAyt{mSmT=8TvB|qH0uRfQo!(@iW(7+AP^evIxrs5t zzK>g@TSURAUofZLJ$o2%EfH0im?REhO}%_!LHbOImc477!9Mb_*+L|iQigC5QX}}o zaf7vj@fqFG`!@D)Pj@vl*m*-@x7#tN+L75G z(u)CYMOe{5I}ZqGNJvPSKldqM05ot2QdAZpVWkU5bPQz@GD9N~N2dh;fc(1N>B~Ph zBcL13f?yZBF&UPsrJ__s#zjtnPR6(fmNijKNOvDK4P zGHsYLZkY8USlSnPr+~o!u;uWJWiLg!zsJjfNmBCB3Wol{A66h0)XV_-IHa zj!6|hMZTr46@uS$Rgi&0{uRey#Z_OcgR$Hl3~rh@%vCAZ@QIwCz67a@nkz$Dau}Jk zR_Q_lcwU?ni7EF)A3pBbrgWJ--JtT7pdPD%t06p`Ibd*Vk~9J7GM)06cXJUTky?QNl+hOhSQ>f=DFuZ-!0P;T4y zSWDcfHFx^dR#Lk=!`?xRVyN#tn7C^HYqCE5rLuZ(&XYxiXny(DLFV$}R@)y-TJUnc~k^dYyIo4fZmBQB51>M+sOn z;6cAG)eynYAW5wgP4vT;$r(A%uzUy(^TJz)iPAJfN5@CxVUB!yS0R1eKo}(35IXu@ zba9lBV!(|;eGE0$5B7|oY~^^UGV>dBl#VdJDb|Zuf;gtBa?--b?kVZb+?U(H6FH|P za9DNdq*}STtl3I-McD@c^FkDR}_@te|1_)+eT=LU=gpYupB$OVxEv(6H*2T z*yz++#q%`I$TaKz8T)mm!I#eLSoad)eAZ1YDT+=Ct-hAyRXs;D%(pdUq63C!7$>r~ z36J>IdI`GuIL4dLyMkGM<GDv4b1T4j_71GC$g7LcGyJ1H^S9Z5n05nntmn+ zc++To_Y4@+9=B&|@5T<{JUV>_>jdFs9x9)D$<)`Lyn6ifyh^Z^A07+%4W5|^?#2@F zQ9rw?Zr)QSS0AdVJv2p-{all)0dp?V5x<YHm>p!{o6Qj zL18sCdMnR>eQih2Jnz_rsfLlmX=JX6>}LHU?T=}*1zfc!Cspu0xIaIwqYza-TNRlZ z`L5I`8JTd)RoUDjFQcBi9}E*xfG-dVOvSD1P4TsvM>EtT?`WlyQxvJxM;*5=G_asG zqz#bcOnh#!5Yy6HBu&cLn=BkOV)=9nA!W=No{uDrNtY?fp}cG<9CtUaWZX7LMvr;l zWW=&!yM8g=#(HS7qGTNOOHXWtnsjXbybG44dpVNcfE^7R-kVJWpD^w&uN`g#zWdBT zbk&*$TdjY~{LzIfG|+&$j^aU$x>J!h$N?{ktpW#UYr|o<5KUkraIbY&9IkFpNBoHP zfcKX*um|f}LlB-olH(oX2pWImK2~rhSHWYf##a;~x6t0r+TVaDZgp%leRy3js;=^P z4e-?lmI)X-kCJZQRMc0rJ7k*u+A#g^n!BoAk0P&zPH~2@*vtsObwx3l+i$<9Nx$ZcaHFCfL@KL%@usT&lSr!KtzN4QMy+ta}!& zhmu>8IEC`2)w2#6>Xfu=A8Nnpg9(fyWcRG*T4OtUKc?JLcd?Jc_rUC=)Q4@D)6u4Z zhoCR34{G3jjn!7cU3^Ymc?#)bJZjmRi5AK|f!GK|EDI(1q>F**hn?^>Y))`8qeYM9 zu9OqcBCX;OT@3EAtnhpHA8ZQg*@w}3F_Ge$Ewd>mzB!DdK`ah&)a%YMw4^I%LwXXLp1rGqm8dAR=DAEp! zM)vN+YT}yp(}LmlY}I0g5BSf>2gO-o%Gl~HJogcX<#KI?oCnfs#@!<5)H+W(4-$Kw zv^E)o8=0j`lbR)n{|NN9S#*?OJm)R@4H#Z%xah44{u&d`*MQdf_~WQm+iY?#IFd{1 zvc{AV8}1#MG*beCu2+qPH!r$f)H1HNpwz~%BWI3Gcj9IlW~hOg3cclZLN~>)V{An!I~+Rl|P+%Cmo7SI9=?3j54kNpGx3 zI7MXHyCqq=5Lcmv^iKeIPIl=OYX=muUEIe=aoA_Y!&4~j^z{NiHKn12r>b|IoGCml z+LyeO42Ph|g8UxA9`J=8hT0&2aHyYtvIy;Ef139@8e9li1@Bz&Ej;3D^k zWh^RJYVylWA}z_Rl$ZfMHXjjI5=LaM>4_HtJi5`YXZ9xw8@y!#`*)U54R-RUy2s|} zt0IG(X6RXJj4@598|I1V>e4@<4mz^O2Y2TuoLU12tJ19PM7UVe!wd0fWrPZ@SloFM zJ#ypWC>V1#UP;dlk=%x={xA5|?4ED?F=Lnz4B;LH%sarNOMKJ9mn8mrhX;q3YR&<$ zwq&HmB{61%4A;EExfD!izY=YXVJ=U-ANzzi6o4B`PG)+tuPlJf&{%M%_=BNmrX-fS zWJNk`3L9HFQ>t?N?(zbQ*Snh7fxuJArN7G9A+wG-n~^{=bg;h+QdHDCXyvxX{FK>K z@f282miV1X$&%k~zIeGm@m)J#0#7JVl+%;k>xr3yX}@80G2BVZu&RA;IH=>#$NlgWTW^w3$uR zMb?d--Nm(8k1wmaX3Kj{;&`)h=erNB=`9h@&fz<9S2X(h-DWVl&d)m!xxqb_)jTND%@b@>O7ElNr*OCO zGAVD7b86k^VZ1PCsT+^qFLOu`NgisYe3Qrf<&<*m++yb~Tzf^rts0kVoqI(teU_me zaQvQQP;16F5Yl?%W)0=ay=Ed@O48h!(UEHnxJe2wJ8}JDz%6J!DH_Pbj*5md?F1G42VAb!m zc$fskEcRJfm!aiY3M_0bAc@(=yoJuCOvd*6%11`k+t%Lz!Hyn~N#Ys3goB$oPpzjH z;lNwE)_e$C@x%BIpM{o>In|QHi-3{6I!iTo#nOh`^geH$a>~Yh(!qM^)WD)zZ>3zj zG`ap(+NC`s_$qMCB>TL8(bny}L_JlAxRT9A19`rwX-^MPl7BFD+|%`}CWdX0_tfwuPYCE%Q1uP_Y30& zI*(Du1}wfdKbfk025xK8yPK|Y=DUrb{*M+_q2yTl9E^+y*w(;{3PH1?0}cXWD+Yo zzw4ynCAIv{Ok$9n&Y;#+oUN)b^%3c|xl!FzTk=SEWxg;cZA(g~yt~Nv@Ry>(sGCXS zmWICC-AWFhX(+XwQ>mC!9bBLHN~KjD#z0*PXN_IW)MucPbQaCBQ})=+N#(2^K&9;! z`}8IB`Cw~{f;5flvg8SJBp%yn?nL*8v&c`W^_`Dm?YaahiH2W7keE zLvl>oNU9EZY|u+qdPpMWFnhtUc^h9FzsDHJUUSYD+|R4`l@g@!1axoc^cgEq+c{oK zX-!t(AcP8*txx>2fum2Pc<70ao`}#YQs-z&Kb+jq5szDD8692BlBW;Ki~`*{KbWNw z#KK&ha?WNu4a!1it+uz&>6Z*cpu!!s^=l@F{rbY9bKK!nV-ao^#ICD%G+U71vM4XJ zNT364>x_{wc)L_nJixA{CzEb6Fy=Y$(C3soHtzW*D%q13HU#sZd^{@jK4#JGqhCKX zbGeyDU7G*6aTF)=Mr+*g+tbdZ?(gU(>6)slSO@J&5D>Trb^&a<=3;l;aT3S2K>BX0mSHB=mx2QmBy@BiPv{m z2R|o#ezgnWH9WvKfTQ?rFx_qRzv_qm2B2IVi1G0@H8+|k@@G}wp0@ZJeeP%U%xMid z9W4dYGqoEVxq{DNYmD1XhTWy9jgO8$ICp&Q8hqL5&U8(1ikA(^qVcRsNo&u2H@JdD zz-tj_){26EY&|f1y+&%PHEEEQZgAM1E2qXPMd6z)?(o%aULYlmtJ9!kjIRn+KGvSZ zZbi!}V(`Q)S2IFQJS#%JsY@b|+Pyq5y=`fIm%sD*kh$JOyG|2aOpe>ees4wbJaKq< zQ8I;1L^*Y>;MW%#Jj<;<@ep+O3KX=qH7DPn#hG$$wPWZq)ue5tF^ydu1l`{Our@lI zFVmn2NKfMsA73XV)6=at$5Gd}u|YT`>x#3!fl+EUl)@dDnl-3DCWSwkr<+-Vx{At! zPhx0At?NKwg8RKm|fK-ylN5BmFC7VLXH9mc@Kwo7 zC?0FX{WlT0s;E&>VU^w?yuoOpoTKc7sADIO{X$X9O{Xv79E5I3vPbgwNz=ii@d6Uy zy64fWEA6TBl|qcTNk0*OZnW1qWr&K`%LP8y^p#f8Sk;#&#kl7~b)3&%luhEGq5}$9 zVd^G6OgP3qz50DCv0|a_Gk>O<a3C_!flA6v14BnojN(x6OA`!X=G?L^Zn|Q?}93a_LfL zM`pWVaSs0C+c&wELfpNLuDRma{QZ4M%rEE^%ug4QyRvi1_U%y)6{3_SO5*b;qUP%! z4(r_WqY{E>u|A`kBu4h1T+15`l`5^bgmJyDe_EU1o2&dBIx~_*hYoC6;MvzZ&T*-B zn{4~H=x>1k>$_77$ST6$0O`t^4qo@Vo?oQtNp^kb?f)wsz@JV^`)OykP|U)HyuG$HNR5wvlEf``pdv6szWOs1nQ_GeYq6IFTd)Yfm* zdm~!x&G-%@B!;Wf-nST>Pi=p6SFIo{g|9y##DDgIryjz=Rn+Y{vnn~}_SuzpIT%pW@$E+; z?GWCrvMXl&w*d-KH}Vul7i@K(M=@B;7Ax#JP$(wC=?GLRYfjbG@}*Xc>X)Yvlt{BU z8?ha#8)@Ij2?1sg9uC-ERG4))Q|P@7S}J3Y1-7;I<4~(=t1!^Sr%-qrv3n|847LH` z!^RI)qXljD?xGDZeps={YUauJ`0E=)qN(@M+&g=d5+QRjF$-GyynEMrdPM4C- zsPrPu&VZQfyX$h(l+XcFi_4ACf^>N=@?~&SFzHqb7rrAHMk2k~%0a_esCy^8a7IeK zm>&Gx(}fZ}y$XsIa+f@oX0zHJ0oT!Nr!;)FbND4m=OgOS%%kb91#1BGMyZPTF$O_B zj6azf&e|tJ-rGto9}yllq{uKBw@2g;m_bOaLv77tc{GAkZt|I?J~R-90XMT3S8KX! zii7Fp$WReYpnD#ZdFI>n8ba6-{dCwk8$&BvcHI;l(s@>1K3{m{x)19chVm}`Jeq$U z3^c0Zv{xnPELHMbb67DB-p5%}AZ*#ec>Fxy{xr=jGre(>IYHOZVuSyxa!PM(#RkAk zP8duQep$6JYQL7&;>?@jY>^Se!DAUF&^aJTO!&2n6=rNATpq%CZg)zapaN9 z?RZFlL~S0vR5h9_Cu7Wfgp1|ZklJnU)8a8t$$T*pF2@%gLndL@@u2*`YA>I^#s6Ug zJ`5ghZjbzeaMTYcL;+%3)dXSWvUY`U$*vT~mtv-uR^&y~l!Ulu-2k*IK2Y0Z6NK`8 zWvXFxi?q5bv#gJ0_E=Q|2K)vv^ggL2&~!rsr)&3VwiA#Yegg`%Y;CqZ211tvfaM+U z!!PQ(Zp&F1S1d!JOw>uvZJ1DUjx_H3!k4$VvUo`+j56;px{QA6efVlrr!uTb{z1rq znXW_Ee#1J>+L^uhb2{Xa=zzmZ-){h(oGw8vBdPpp0Yt}lmHwH_=VnTVh)(4MWL1V#)L*LY&Mz*U% zVKU-z(RHy)S_tz)$dw77=$CV2VGE~>IkL`wRj`Qz)3O8a1)5745G8pn>bifP&KKAw zbUv=#2Yr<;n2ch$MKM5!&9DWpekInOD{9ed-r53H)47B2aBMk|{A2I&`oj-n<|pMu zf(vJuR2ue4M#!9|&hocssTz~zF8EROskzQbxF=;qdWph9I-N)1wRNW1uE2-suGHyM zj~vZ>O1FAJwYwW#W+X=d-|<>C+y2O!-_*u`9+QMLCQz$$H%KvZ#W7fR^1hNzcDTiH zop(3Ed8rw7twi!YWx0Dfl-_gCa9<4#G$Qn+$v*suag-oz{(MEX@*D6{rBXukRdbs% zj@jYYv49&A53j%rvaIgp4aGg^(*{#g2dbE@6)lxX#26rA|8?7ZNhG%-QnS|&u)d8? zZV@}NO06L*&_-#h#nO3bIawBMd&rX5Y5QW^ENH`F=!gcW1O-p`N@$l<<6(-P>bM=w zIs4c9mx7k*l$PgD^5!W^D@`%4-}X62+a$vz#x5{?-@nAZzPl@Y$ZN7Rv4VXeyG{8~ z;ZD3%^Pl=;pu`7EPV{?S1DQ#P;*<1HsxqP37 z2eHB1ly;A~FgM$q^c9m04QdV9Rze+{glS0iVB0B*)mGJy)VPsa7P2yQQ$3$OEf;YZ zM3BTkye;yeY`b^%WiVmQInMOqPZi3`0~q-&P%$ChO<`z5_)^hBo_vJK5VSnI!|FY# zU56#cW@_v0Ow?mJH9Gtq zdeY{*Day}Yj|wfaG{!o@hH{J{H=CB|zdS1PSnYx-0DlT^)ANO6))R>T2GpZ6Q4aed zFHaC0wKl+Crqvx2`L=7247@Mpr5`W#;@~8I{85=o+H&>_JJ;Be8(6g^u9%F*7#kN39ZA26@xr6sM>{?x#lFji*rF4S8WG&<1^N?>(W^F~=bEad23%q7{u}{fmOYAaj-yLxHQ*y0F8GhKuK2<$ie%+TGbHlj-h2TZn z!rPG(RaL1kYfT}G!z+^6)#g!7vnfaKt?bwf4%#?99P%%_X~L{=sLm07C{?hr=Y)&} ztBrrcf$?oQUpp3-T|9}-^gO#M8a5ya=0(uT+gTAOSE|WKJ;JYq@!}*>Tf%3qH(i@& z7<+is0QUAP*iJl!3ugUW`yKv$c{&P($7V2KQG4~n35~Nj-e7*H{2k{)&XL|{9q~~c zx*>ufD%H&AAx2mA6aYQG;|X?2zqZT!Gj4~<4q&sk^U3{+?%jIo^iVI=^!u&i&FFRn ziZJzJ8zw|ursDDxW379F45soQ=lscJo;&djSE+LjD#P?dvL9Bf9QH({wj%H^b?$7A zV7mx|o%-SMLmmwtXO@AjNIR>Ze4HTn**wX*1aHCF^@UX#S#8*DoA5@1`9N1iCXWzP zjk+qyY|d=en(Jpm_B$)8-vH3nzLfo8@N&~w7;U36I(fueTTI6pzqHTK<1CAN6U`k= zeunC*p2Q^PAvq&2wd-K?X#{QHt4+41eZJivT^Cf?jq3V2zJ$KME0ViP57yJ;H(8}w zC5dL~R{netYG5%*2B{rV<8VY%dt8}mpyJS(8!a#@)f?kn-w@J#-Qz2q=n529p^}>) z_d45GsNjU(QYGn*>ri;HL4B}FF*QQk;L?ATzrUN=cXfyx=y9^mao{ms2JW&jBRk35 z^RvYw(2EfasGyGqAiyDD z-@Xt1<0DWoGyo}!urjLAg;PL6e(&_wCFoNFp*rS&d}AO;T7K{&hZnDrxxT*`lTa)P zrS-FG3*`na$7gmqG`U`%&xk?;`>K<1*?Kx)2x)jYkx4(yNt#g0Lni{bvuN6X zKzg1KKI7>)$cc?dP9DwW91Q)U8DKgA_MAO& z@U}FeugHfkQMM{e#xen1DQ|O07vIu#y$igf3DOYXFg)%$1h8 z=S~;F!*STq&1Xl0LnN!C+v*r!{eCHO+IiYb+?sHpQJ+L6)qCz((HQxhqq*Aq+!oK8>& z`4|Oa5(JMIvb(4Ls+&s3w&0ZC>a~kLYrCO%a&ya@%A8o>tUuw3Vz| zpOc=Bdbg%6yix|uddBd3eh6~4w27pED{ddDkum-0t=LE`*e>&3`H<-96e;0twNqPq zvlDv(9t1p831ZFy1)Jc!~%xQvf9?hHGc*qU%stU8IvR@G!9DYNzd zr6d6LAse)$?wrMPT3KZ~u|e*HnE5kn3(X}~iKYm9dUZ)S^agYNeB9Fppm`6M2HR>3iz{vu|y4yNx%*5UdSYT#FS=wbLj%tb+{qUa*T!EEOs~U^ymr zP}_A}!Tcsm+o;~mU_jx+Nq)yN&Eol5GM%y^C>fdomobZHfmWAc_>tOM%8A+dp^FDq%#xk9nZ%5uL+h$LDXC&DatWzy%9hMu6NS!rH#tyA zfD5&*Vgk#tmuP|cC529MBAy;!Z0;`AFWO(!J$@!~w9WF74|1zfUO~Xf!;t;8+AJw+ zaOH~5ehBo`udjDJ?XLXcbu=&T5#0blxfbEt3g(w^X|mQhq@QG zb;9y+*2$tABA$~fiKeNYs-i4dtMvAUo+F=IdYdS-8ftThd|+X?PSQ)`QS>}sgO*;l zgun(FKe_9hkbKsMV_*R}ehZn6Anean-iIOuUBdy;ya!-pUsp0HM=&jdzv9nF;&z|y zTBeso0@7JA1@~}THP>$;c8rAWPPk{`il*UY77;&AV^4GvRL8Pa=~oAfwdQD*i&OcB znbPo(_#Z-z>JI0}Bm&m#`VUlSsbPnie@2HTco-~=hF$7C1fpHBkF8MeZ2}n>VLn#wI0aseJV{S0Qx#f$P6n5j{YgixW2ezX zGaKJx@`AfO{2S1S`lNKGj2WScUVRdN;U|#N1E~@WZd9S^KRDkO??Gn$v6UvghV0nh zG+$*ag<@TUCH%b9HCj1hE$xAzx z)ArQkiNrQA#Q(hFI;*hjq4EX$k4up{6eI6)Cq?AY`6=gPPgUj0EzA~~pr0X2Wp2{k z(BjG;CKIOJ%kIScos6F5T7#UD>-1bWAXIuUslLVJyMZ4^u#lNwk~K8hG&b@R4+>m% zAtu`7^^PaIl})Zzr@?)oIyzNKJS%<8lMGroM!WCv0T+PE9JnIz8;~cn@hGXy)M$TF zQ+cO3?@f2ipK_Z!`ZQ(cYf<0buw{#^gdM?aa|yqVI{oASdK+W$?N;u80U zM_15lPd?OrRjen`@>B)mixw;g+9(*hWacc{ zJ`KEo)RGL>gdQ(ml)Hpy$ME_El9{>w=b6Um3mg_K?^&KgNZ~qeBecn|w$F%$^wbGb z28X4qOh$mhHeCscbXN1O_YjaU=GJ)xZsuM zYY@p@`b;e7Ol1Yrw`-=zbz8C#RAzcA+mfRKqDdCoy2MW9z+oY=A+Xp?75qM%Hvjyw zU`wDl{bpSCev$!=rSnNt%JMTZ6(~Q4y#ztmfK0^GSx{aOx{P;@Lxr+hD~>ABX#662 zk*vxs@1QuhX(F34by-mfgbNaul?xh9;<N;o|B9(I6$sO>4%MxG#Xf0BB&tNF@}61DI9L zL}<9RIS7!^#r_g!Dm#ha=1x2@B)$*Mq3qO>-4oBT1wSQGq-~Om2WzFjSFsAzX>=Ha zVf*EaBM9v?Na7Ux6qviWC&h<~rNe~Q#ZnkRNkeH=42IZSU|nYoY#i=z84w9>Q3Yy8 z+qmh-I3v?@*5VYNJczT}>t`nOXBLf)Ww+$>2C*?xMp)$N6Uve+y~iT)k3tg!90F;A z&2;+11bIT?NDvd96J6L58A;+1?%gu+@GwwL>0d-P zMb&3Yh>@9KO#qESnwYz`A?UCjcr72E%)-MN;pL(%=vA`0?Xdb;l0IVK6NDmv2;>!8 z4?Ok_Tm^qcz_Ja49B+%3TE8pVb_|w(Hn`jsGcgWiFmH3+n@v;D=l&l!)qUiEM!?xjD6!A#e&%Nh4$cWrd+sfY1Y9%LYtW-*{!~QeQzlcj_znqXilBL zM2U87Y^U4;x4W{=YdNS)M?F&f|FrfVU`;jK-{>XdNG7z=)I$a5D-E!^r9d| zs&oNCK|(0fn-m4b(7RFFh z_T%ZF6*qE&rh5xb<{H%$Mz~*90-e*tMAz8(K6T5$4&*~wwq@U>`uFT1nh&7)v>jx_ z-nthoerxEz>zJ9}bqoxc>=`LAjTaNQLo@xZW19ceF)x4DF{T!>t7q;@tT~8_E`~XQ zM5#tCk}xxm+&z>U<+3c+Eg$`?VORJT+}PxpSkM>{_ToDiap@Bs1mz~EOWd?(ygK+X zQZi~*$BzDi)v`*`49tRR;Hrv>?l257Ds*q1pa1o%C-3Npx1~@w|c) zxo0RjbeB^M`ZdF2j$PqQo|R$u!@-3BK`y#!jUeMr&(6ttK(YX}t(e|Ay6h?UM z;$Wivq}om1^s5kd0dal(LU{9~u4fZwHt2g0(E>hR7DwM!?-Y00&55_= zK+10DjlB2;TqG`~`@}I!Z+MnEAeXN)K0KpGTcbE#4mHNqef<=`iLcEp)$|&;dPA1@ ziBY-5wAPh#KOWX%lA~-Xb1g&fQ|Um}xn={u0?UXozW6f_G>7l6q=TNYd>3m#keTT4h;M`pW8@a__cGvh2Ib>~y4Nis765u*qt(BI2=OcY!9vrlxB;kH$LV5@t6 zE)H97SGc-%FH`cIrhbDK^6*Md=d0z1Wnx)=nT}CaDe||G9`p_7FZ9IBV0P?vrgl7{ zulSd}@oP|z7b$XIEEKhlS~^d(9MM;bm9u^PWLi~CQQDX#?|SKCpABDw*0Wh*{MY{X zc5cPF+(C+(R#QVCde%d{e7A<++}=;WU7~Fytamaj(7ZpJuN*?mX%#df?z~dY8T;TB zybw>Y7s+-jHscG}qn8!2bDy$~jlmZ%*%f0^YRa4iE>8WoR8*d4;b25mX=vEcX=6vF zy9V|(q6tlE5^`~Erh%2D)Fb@JFT})c=jmfXn(|#<7;zVi49RU_YV^ z6-#7CZ3-&8txm=OPJ26d_7nHgEcJO_6Rqziz-C<>gQ;Db2;%5rR4lEQn~LnVGxmY^ z4H@H6A*z+x&6esSyaBfQMocok8r;{*Qpl>nv*Yj>zpm5~%~{{!>xw)nu)M2}0`0sn zBK9+nYV>zh;!F|VK`nynUcty0prYA#q$6LyAEVikra7G-Zb@EID@FVHrJ+N;U>l;w z2zaxuD~zgp)1hyc^aPd}ayID?B*C)BSaN){b4S#l|e zw)+asFF8i8vedK-EA!X|3NpLJZd#HFAGl=d^&P+I6$>O(#rK|e@kHdb8k$`1bH{St z$YyLU?GY}rzw-I-kg-yt>;YzgapQ5YJARH&SH982X!tj)^4JT6l$JO-SO4hRCF}1l zda9fbSFigLuSmP;&qISA2Pj^UdB#}9;^UO&ITh=jcEhjZVs}NrJu?E^8SvQ0S5Lkj zQ|_LBDnW;wFaI>m-|kWpFGg5qQ{O5(6J!<4W02KMR8>zRG?ij6WR+W}%UoD- zKbP2;IKq09J#z`^EDd$$Que*Z>Xp^jh;dPA|8P>4?^myzz%p(P^nqOX$vwxbk)bw zmN{)gNM(F_wKx$p_B#KK@bQn)^+p`{h>gz+l2Jq5;Y1UW1){P>7IjR)N2iz6kXd+;1oo4of1CszT^Lf?&Lqwy~k=iu9@D$>s*JC=T2f@-!A*T zRJ@`To$hLL!Lx%eyCjELyT$7~{8UduYj2&b_aNJ8+fuh|FK>BYE48rsq}ZE-S!D)N z3B1|LbNd#idv15$*t_aXMuS8968+p6h}o42;QU?blUYw*yO_^8E_hxyI4LIDX3|or z2P4Do*n#imY-~$)Z98{VgFezX_C5?PE?X&ZvVUl)?jxv7j(4iu-oP>SI1w*r%C)>E zg%&l$>HmG#|tY;uS?{kM;@yxPK zu88;LL^{@@NB(A7P6h~ANvwTcCjCVu({$~CC@;U5@3QaxC zW;#U8WOY?~;+1uooP;xn|I?;uafJnQ{Lqcbgac~8{H}sfI;lvrEX6oa#fdauCO3mk=&~xO1Xep7XgPH#{&XH3d0y(B^CZOHJCVujUnx z??h2(zOX6Nu(_Am8;*@H-PK69?5O8^8maFtI<>p(`~qk|^1HG6)8e0TII(9P&P2FZ24K!SmI09AJ5SJt7){x(2D;N824|F=au&UGbfEn zt9_F(D}0Rnw+w&375_W&`@O2NIr$$zsWEl)`FPo~*pIUU?z8N`2kgMax6 zpOUL*{)Hi&FyzY#kKOkL8QgVHZ+BT??rYG^HdY44fMbjY^=;^V z!%ww(6KVDGF9e$%_O~Ol9l+{)vJFJUc|#PQ9da$lnBsmUDDnb}N7ti%E};xr(ex;M zze3`c{Pvfbc#wzQ>>|@5G{vb(O!c{G^K#({vCF!xwY{p3T7yc-A-(AmYF@|rPxMtr zi`*1sfn;!lw4jvz>b0Z^Mlt9Oc`0T$@=1R83_`rMT%+7-;|DA{7sqft?rp^=Ear0R zduM^y*P{8o_mWi$Gc*&Aag63$T`RqOr5b#?5I8s&d=GVim%Wwc_#ucsB|4FE3ABIz zp1eh}xYMM;%{w|sdX=#_#;(K}x_8L9GH-5HLH6yzhh`sCSE zuVWEXeWwVw701VMRtXPp%9Jz)G1nR=*SO-k)T=*z>p!XVgfOBOaC^t(f_|+jX(3(o z?S+s+MtbDAE?M=Jn%MqcO|~>Mk)WHac~W*ls`5H2_p}1MRZes7^Yry^Fe{XOfZV)r zVo`wB-aYv^lKSD%g{M!a153Ui)y-RP*u35yHyKd% z(ElQ~jb4I5WiWKw@o?dX0{`u}qnHcqZws63Ei=`*mb^3i4Goziqq$fzV3y~&)SllY zOn9xOWx_&pHaJ^AY6*3_YI(2ZAH&IM5xei-(?jJ&22|INC6*QFKeGCoW_EDuv~>vd zY=rl8E+^Cp0-+J_w(EyxTYcoYp1Ova98fiaE6;M1G+jO?w?m*aOoUl zOU?>>ukhuvI$=fZo#0l4)VKt*?T2{o)s=KOJHqYTE+tcbV{zk~cK4R|%A5YubYsuK z%TLF@`AyxhX(uU938@I^2IlB%MRne9Hw^Gs)bjoIc{sK%lYudD5BdQ^E>{aP`jEH5 zdAj8Gr^wH#5P~1_GwVRMsF=!oN2%YYV=4 z*}Q&0?sI`aRpR~U$DQ+^RJzrM&s(tj$i5q-ZKG>u{dxRbU za&QH;Jm}pXc4pp z&c_&oT8}<`F<+|3ulJ5{c-!(YfeO9Z70%p$H-c})%_X zQX5^bcNowUHlYqSu?;(rsHfGJM!obGBdY^fegW9_4zb3g-GcXO-?Ho`+rR$>w8U*) zT~0rJ>CNC&j++tafw}OJrNRb78`auzfcNwb#8ZWNxW+Zj#bEjPy&sl{TN-E5rEwk# zTKc~L%g)YDZPcY+<=(*FR0aD%$=-d`;`$?1ziYQtlz(1!R)1qW+Ilj5YwoVrvA)B8 z9{;l&%SZYJ8%G_zPX~YbQaI6*gIcn`-`W2D(y(XFVnwg^`{Kjbwkw}ER@3c1BGU>< zUnL!Od3l85q+HuiwWzpWesKEp4?oY7mpj}vm-g*@BRbuj(~mMou59_6s9FE$bd&^L z4_qjjd)Dzu61GC%wyGYzpSPKcPWt(EuyaxHUX6z#=Xdcfj+^5kfBF5w7PjDYr1Ie9 zsayVImu^$EQCF~sQGekGn2rJR`*NM%kmgSu`5i{~XgD%K1Co{Z=s$4ew}3JTY;@jJ zMFWtJMtxMn2v;S?@=0zDqZ6N=4W8Y9;t$91iEbY2%?I09lnt%LD~Xj))kYj?%WTU+ z4BXWW6}z}s`S{&swvZN}U0bDb+dt~VX*r*-SA>3ipBz_dRu&4Z9PmFsb*^1~VD`zS zQuNzX;-`XIQm2MDS||HtYD3S)7l=|csTYaKGFJ=zTVV4`bI&TLo!2aaXDjrNTQi0Z z_N$rJ^55Oi)_>!_!ych7V|Xk3g^8Yq+_+QgmDMnl3U+TFqhCN6Pq?{QSMPv}>eq%k zT@bs7ksO~pe?+#heECIv_bkO0^&9F}5)yk9OtJ20xueJ!hb9XcQ><=0$FUl*@?m$x zl?B}{-Al_Y_!*DJ?k=&HVwehN_9mmE9`1`vAu#k4`guwES24kS7&2J@vdYS3A6V(= z=}xDBX!R$H1N5RQ-L6LkK3fd-m2XuBh@@T`*8ztyM3_E^_g4%U@}6FOYNa9lsmsd- zwo(vyBrvQgKhwf8jQ-|{WOKmlF>&R=Q~t4xShb$OI<; zA9Pv<<^^#rpHFc&zf&Wv(5t#1eh7$`LG`e9G=HKT&KY{yn9)5h*i|91WE8DG<-1s# zc>Y>2gf)Qc$x?_H>$%Ivp8f(%XY)Ruoy|96q24-K-!$MNYxC?`Ay??zUqEt0it)wz z$cN8Wi|7=g3wQ3~Qa>zu>?;dOW1DCbUgsl~yDuU5xwB!%tm5vkk5+O=D=~`;opZMy z?^3P2#Kh^cDQrjYAf}YhB=Z10Cv0i^zgc*R>_zz95z6h_hH}oLuy}?{P;QUm(5nSvweG_zmtykC%CCRO|bxiY{Sb!{!D9 z1HR{MXdKWOSs714L^GML#foegh|=GfyXc`{?H>i?A={X zuTH+Ex=H`RxoHq)B6Y^-;+{CnFunAXNGp^R%T4*qRY@iLK5JMFxEr&22x{qJEt{FVfLDD1qM!{EA# zRHpqFiDS* zas5#^^W&RFTHN0*PC)`RwvI~=1QI1yZ5BOp)!)?wMx1;&uvv(TJDGPyxO0yt=PvrQ zuVMQcC;8v@G(X&Z^V|kM8gGPq_g(0bXs*rviAp^k_Vv5{ZhVWeBRl*qJNT)VLh$V? zN<{Y3!|s8;P2)FsU3rlu-=B%U02hkh!h8944?$5J&y&OcFK@bwajOwqWRO0f}HK@~Qdwqy6>6+THyz4J*FoP$S+(7lRzkpL$OB!Q! z<^*_-czoz=P^#$*96m%%1{REiir6|n`i#V9w>A?9UVs2)wqQFMa1{&*yE7?F*SN6J z`91mRtH{$5sI2ln@4@@SjuhAM;}x&&`Ey_+W=>q$HDr!rJjlhAB~)==Jd8Yo@D6NS zLeXMmm*t*aIZ@v?EnBR6q{LsfbH_O;S0nTnAgTT6+lsD$$5?Y|ngHfRm3Mp8YxOmAP{iVHp77Jc59?QP2fNB;QPac?r7w!D1f z#Y}9&$ivHDldg_knB;j;r&p0axb$eCjPFs?$Q5y2e$8v2`MG;_1J|$IdUs7nZDIae zOTdr&Gqy?jd7}kte*N&%qgNK2tCvO?>N&(me*rH(S4FH}%ncotSXkL8sJ3M(JP}f6 z?S3GBTlw%(i}Y^OJq@qJ^jdjkrX-#R_Tjx#itE3Cl@qO&bE>y0R?j;g9^PWim@KuF z8pI~^F`dpaDH*-8B_@|Sdn~ohzM$zl*Vj`YZa=4?xD0Q2vAaBxMnuRWpU0n2`zR5b*8Z?lEhGSC^}bqwJ(82tJz52qLWRKRwW(J zp=vTqrIE1-C2I==4^hSFwaUj;lLO)WhEEjlXa`w{AZ^F7U_E5?1>)Q+BmO`&p76Me zjV{2xSeI$6bx_8IwF~Y-rqb#N=JRW71CVs3uc`%}kJj~UDTxUgQ(EGE4Dp_q$kVdV zi#TG|Z!*M~CFB#_d2duLw%eE0|P*{B5$ncbx%9rudM80aHsN=)}Fb5M7EZea>C;8lXrFkqbiQxG&OXzgB|z8 z6QqmIfhZV<(JW1u^Vgprv0otq6A0 z9h3VBtn8-(M6gP+96iRt$EjdcGns>M^t%{WS{ke;)*O2eDI7T2!z?c!WN&-MVhMlrKOf{>i)icnc z)MqPpGTJt=nh2rj(8udmA)WvZ78hFdFmYKywf3_;N7I#Xgz!-2xX|0O^^G7AGjzW9 zxS!fv=HNaPPK6P{+3HIS^dNm5ERb{8;bDqkQi2YJN<`bIr2X+sp^*8TMdn*zp8I?D zeG(`7UBpF{tgN*P7$Jo{*$3P{)p?k!_BWZZz4W^m&WlDP@sP6lWRXd0$|%;TLGfPj zmtH3X8U46Ui1SxPw~gV_#&j+ZXE336o6`-A)-kVZ&ydJh>@#oxBp!l3j*&W;0sc^oc0z~Gli0Ei{LZ$zkp{c7 zKPXX4ylYCqEIm6Fck&eR$+j(Hf)tlkJXfWrv;np0&R*jywFF0n1UMVjQ=LaAC5S%H zM54NS%VT_H@lXJ#_wg!`Z&KEAj#y=drzBi4_^tD*s<{ouETm(TTQg|Y`6-eQFK&5 z{yp`!SB8$L!ZY7R)lK?t6mP1e*GTlm?(G1n%HTy`%@de@m%Nf%heGWzm?zcDEM8Q$ zd04_sAJq`AF>TR^sni!g8P|h@WKIhC)!5uQvdsiFs+7(YZgjeuz|9Ep8;Jk%oEx)l zj-w<1>WH*P9)Wl~eJsQgSnky;t48QBDW8eBZ>?wpm$becaY8Nc2PYI^-@;s$r`<<@ z+7BMTcKQy^qSAVJfgX?}<3PxQ>OsBSOqO#0ZXU%-KBc<8 zdKVdaRK3izy3X|Cn83M<=wwN0hWPGHXACj`hKZJZ5Jy1t1;;Q(v8v}@s~3J~fR)GZ zx#o&ndEb>dxL}^tnZ*bSz9t=+&J9wreC919oN=eN^1#aG>(JG>?Z*o2=Ef?fGIO=0 z>ROeW=NyT*<%g5*zK+y$6d$+3)pJ;?Tm1sw^5(|sluVnUzH56!;V3Mr2p{*~T+Uv3 z_bvJ{LHcm6OIAUHW4RopF4kPhe+QrB`em(_OlL&`tem6cjCo68Wo)sQZC!EuoRO|#Y4=;-ElYokO-Ca7NOIzv$8qAf91!* z<}~oa+42`~of;dElnT7!_$hS3I-T}!r+qR*=3Tu z>{G%TAY(c0!babY?%hur<#ge^3Xa`;6}I#~CyU9Q1$IB$=k`r^Zq_#p4*DA()c7+y z>AL51fB@-n!@3 z<0}#_1U8iy5G$}J@nL3%o(oxq^715pMkOZ*2970-!c*0U<6GRMSiDj9OrAV=PmFiV@-m^RQ=E(=oFjZDf+&6%Wp2$~G0^mRoF*d8JUYcdpfr;iber_Oo_m!vOU&k-P# z(;lBor=3h?39hi(z7lE5&-w~G!QPr5^?Y!5VF|h!7)>Hg=U}08J9obJ%1nr?9wb>j zd@hE4x`2IlCQJbyE=vc`ret`m2_@z)>?NJNWHeLQab|O4|7T-VloqcNMi*NQ_d6|j z_#%-TQpBCIEc3!-6nypL{cpK+omzA;2YFTVap(}QPW;b%P-II1oF!8Mj zEwc&KQEhAM!m%~>2?x+MT4Q1wkDrk~-#|a~T$OSz0LFpwq|;G$>bx;CVBW>2*SDoN zeHNHEZNWV*X+6g7mK1!GJ`pR`U1x@y%VG;gAE4MH;JRi|GcXQQ7Q8$>@*vp4#WKq> z#nG7q1z^o=vbZH3t5}N@0BJOebcFn{n$SR?@}kGTPM;wO>5Suaa8-J^vkMB@fW4V4 z$}~_`^$t;|w2CiPW5xUSMgqpQC)sq2oSg0!d#ye#t55=!U{U zfuG&Q)Tj1a*gLY4(o1SfJZ4HssRsI@^3-`92z6HJG^^YMj2%!)j`=W?X_vZo-;0JF>(5;n4N6V3QCqA7^k{`q zh)F0VZkaR#A0iu9SwhX|YL=KKC{i%EBn#iy{VV+wx&)JiZDaeh4#7EA)(o;w#`^5L zonjWvzI)vI*)-f~t2aD34J^5ZUAUXR)c$DN(7vrwxm-&f=k0Q>VkiE_I^nc69H8E+rGYR?9aoE0d>dLz>>BiO*9aPfP z4|O#g%PJx||A1-hM7+4L61SNSbPAki-(wKNJA|E-;4;Jeebf4J;!dK0GOUlFbBwAt zWXMb?0!&m`me1SxPLiJ?oJUU)2a5Z1Dc!oxepNA%p^Qx6)e`*ybD8C=b29ou9dvEB zPj7sFw@ZqnNRSMIQgI$W_zrOBx79h=02IqkG4f8XeE3qd))t7ioiqeUU{VtMb zklDxM&LkCIOo6pd3SFY}T$R zv$0gtcUUiI7Rey(DF-M>oGDRB~dL2Ly#c=5f48X4Jr!IvqfY zt~y$7iW5^+``ZI1FcvV<<=?~1Y!t$8`I7hvu z#Pb}BZwj3Kw!_;L0dh~G&+$FZq{OaJ89Uop<6H@D&U%1dS!q1bO7xl=xgK=Anmi2v z*`|KwSFMXIY!NJ*uz*OQywch=iP>X=LlWo!Q>vd9(c1H+ErQ)fBjV?n5%WA>`}hKK zH+ELnXYOkyCBRpap!~-BcZZ|EN;~8?N~Gq;?;-$NK~?y%85R04UYBNMLWW4A0Pjkp|*? zBeV9gN{7i74Ju!3h;;JP(pE~sc`Fv^%|_6cz%p#2rF0=TI`( z3|dW|+*0ZaP>F-)T}UwqpK}6DTLN_f@0*4y;Y1|KRJ~+>r6xhv2}bp5Vt*ri35Z97 zpm$nFU)So**piolahE61OD2Xz1Sb++#PHO|)og`mC`5jy{LUdF9|=H9#Ir02FV6(?1V}2^=1y+;3IvSbYKWGb2NxQ1#NpUcX z0E_RbM-UVami=N^bIj=QdL-fj2n+@Da7a)zl&iY)pi>mgL*fpq1zYUi^6j<{)C~@ifb^y1 z_e-)tOX=w4U2c4>52aK(w3~%yHc?&(tUyTET7gFrv0w}=6Vm0pt{kXytuz_Lhh#Wc zJ0L-i(UHW#wS-8b@i>=UZu`Mjvvo*M9GYJ`U>R&HPNI|Ovp;ElR5Vh+QZw|_$J#{TknV4P7c(-)v-DOOIWzoIIE^(ij-T>%nogJg0)D$Cb zb58q7w(V|x`elR?Bf&7F!#PGV7J~7F-Hfk!AUN zWpN@Kxf?2}>WXUCl)&{McqNW%M(w?TmD0mALV75NP%i=o$KB!})aoFG0))&6W@gSF zCs{hbpm^sA7xXuo@3UQ;fr z?>QNiB17r$`XIBL4S+q2jLqOh?iUX{b4Eg*qy_l&oi4>Pf`0$tHsiKgD5Mo!f?1;L zQQ0p*4+NK{Gw7Nau{Bi@H4}yC;oqQy{mU!V26~+)h3ePtmnS>jGow}^RgR`QT`_Yu z&lW_TR(7M!#s8?rC&A6KwNv~?ab2(pQ-R!s6v9}1OMC)1kMNy7X6TCtwJrgiNgmO4 zQqByKe7^vNSKUL{Qu|C~y0Y>u`58jhyC#7z=QZy2dZkNwh72ZVQ^9zl#nUxB4ZGK( zu*)7kr$JC}ONWt%o-n=n5qK3msW*T*KP4px`7?0HJ9>FE11l*Sc)+h-l+fs`=Ux4v zqjSWJRm$@t*?LWJ^JX8?0?J{qyFs(mruoQ%j9CPGz43gp%u~wL>R##Jo)LVSaGOQ304^pe;^_33Nnlq4z}b&;y~YC0TQ>dp zA*%j-7=_AZeQug|CJgpoE`}733E`ICI+57N;2F;Zl{qpR&I_WFAPz;0an)@n4cS1s zK0&^(NYiC7CRm@JgexjPK0y@1VuBr#4#$v5q*_6XaN0p>qXq|MMLQ)%4|7c6m7S9_mmz81;vaz-d6mrFPtc=&;x|*v0Rn!sy@^zk8{?VqFDyU z1}Pb0Z>XkYY@*%d(q~^FP}jC^Z8BQ-(R}97s~t~2>h);sSd}zfyCKkMnw1I(Y1QUf zu$jDvD+Z0gUyPxoZkQ-hOmdl2vh5A3D!E7+&PivEUUyaDbH=2|M^gOx7#rfiB&n~r z|D#d=CeL=q@y7G1U%*mBBu$`%JmKA7d=>{0gW|IcEv=~>Yf#gvC@FwjI~(dbNE^_{ z@atBTGUOYYQ+q1EA$4wsFyN)Rd)P=vMKMTnmxjEVuE*KSe)^V-&?Miqda2wwI?v)< z*!D=g%X6!9pvrB%Y!0EPIO)@R);ZpBt}GJawPam~Jy8cWiFYjdlwxs$#ii*(_Wweq zDg0BVg`GNEW1>+&R8biZY&15-k?L~rb>UXpEVeTJLtbV9%f?f*0X;+<3Zti)$&Aa# zZpO#p9x9nJ+4zRiCp9$}@8RC-Xf#lrAZ+{AD~(kY0i1P_!~VIoZJGx}JJPN~>}dnd zqWykLVRt5QZ|N=#OKUcz4B@cqI1YLar?ck2fZ}Ke4U@_;-k>C)V&!p!U zo4CZhc0o)wMg}QPos_9`7&)qg^!gN|2S|ILPugPOkgQ{^ZRa1+2p+WX*1ve7V|QVZ zIuGm_XJIB~VkeZ6!t0>c0^s+oUAS&A7>&Cs4C6c={M_0)J4n|r=}zSYnC0^capG4x zh&@o-4G?wZ`+*A((+wKy0_ij}{8)B8L0 zi5iC#Eu46VnAkUVlQf{|viCtyBRBn5Wja`fM52x->krMXnG-RJDL=!$eWs1GRnhtu z@=em5YBQWSs^S>jJ4c+re%AdrcM3nwL7|Tmk;o=sRVsH3ZOm@D1jL*=se-EQ$x3Ro z&tRdKyL!rp^$&0Is^);T<~Z>ngana}uWuF?Q?ar{g7MdUh05rp9|C5lgpFkHhq>B< z@Fl1DM9wpvNnxYr0MfNcv`_djGs4oEYF@nIDx%4(q=XW*JBPNVlAnRd@Omg|aq0va zyIcZ$=MpJtp=3tXK$t5wNw&sT7n~PU$~(!XZ_!Ui_vj(aY&Gb>pM-QV=P zl1lgK@3GkyfjW|Jk2;-QAi;D%cQry|fHNpPBoH%P8K`;{zNkE6jmB3i&D4i<0rWVK zj^bL(tvVJKP*pHUJrVvdu~4`NTUS@oc8|R(-#I=!(X$;aNt}pESKK z3B~PMyxptk0B6MxB=GiR)L9y`w;cat2;sl*q+I^#Nu9b{5XjE01s^!G8TK9t z&&hX*DH}IHjYCL)%1A2lh`r)@AdModrsG#)IfNjAnaU#jqz$@zr9d#SF&e&- zKSPUgQ6%Xq*ZLUE6pF`Km(|3#CtI+ew$fO-d-*?^^52!d6{Dd*J*eIN*+)cn-U6ft z4Z_(;e&YHMUE}TVU4udM%O#$Prvn#mG4EXY?SE6mgtLb6qB5$@5Y)Mn7EuTNki;IA zm;b8*iKv_o~2gwAP>2@{hYAU5qev9Io0u%ih0%zHSGjnYTqro~tSCAGlePuMncV~FAgsA{+`n_d-^1X%YW z^+>iHPn0F(IN_yoP(p<&6c}v*z=kCCha&yLVF-d8%E9w4U8lfPyAquHIFIOLb+puY z3@;AOEDF%Gy+E<~B&+hD9mZKcH1dW>E7w3lzD%if;{2OXz9Ai>y(Apj`fa;u+34ff zt+VT+K4KhLQ9QF3i)o5QI5(ZRwI>*Z+DFF$h#ofkxFbr!;C4XAi35RxzC7@!jS8@^ z)YEn_35lZSNyKkgXSqIGf)Kcxv0K!~Tu>wdhyGB_Z?g8{7B)$u_?gJ`;2Zl)?D=+P zlXk!SKDZ>AsuYXhuz`rcB75dUB;L;LVv28ILuez2-b%1HX{>ml5>LI*r<~gn48fCO z-5>|maMt3B54phxmS)A{xh4kY2)#H^bg5N;3-Y!lIV%x5iI=*GE-MT7oWqX>58O)W za;=|*Nt)*|vvIDfLw3l~l^o?vIPn+ODl-KkefJ*t0Aw93MJ9`F9N7#^0TeF~WT@A# zx>R#0XIvhEMw4~$@JhADDSZHgi7p}vK*bGK3ysD>212dh zGdDS#Q7CSmQ#CY+qyL&(V|ky--Da<7Qy!wC#$Rr^~r62m&dU?g$j)Bk!Tfyrk7 z8Y*=jJ(0fa=-s}D8UXErURhtLGR6_+jzvk+Y;B;{~UaXD!&nvxu)1ZeIe|pTn Lsnj=1zh3-5mQPQ0 literal 0 HcmV?d00001 From 44d9b5f1fc70863b5f48cc0fe0b700fa843190ed Mon Sep 17 00:00:00 2001 From: Phoenix Date: Mon, 9 Mar 2026 18:23:30 +0300 Subject: [PATCH 13/14] lab7 complete --- lab3c/app_python/app.py | 57 +++++++++-- lab7c/docker-compose.yml | 110 +++++++++++++++++++++ lab7c/docs/LAB07.md | 200 ++++++++++++++++++++++++++++++++++++++ lab7c/docs/dashboard.jpg | Bin 0 -> 69272 bytes lab7c/docs/query.jpg | Bin 0 -> 102789 bytes lab7c/loki/config.yml | 43 ++++++++ lab7c/promtail/config.yml | 29 ++++++ 7 files changed, 432 insertions(+), 7 deletions(-) create mode 100644 lab7c/docker-compose.yml create mode 100644 lab7c/docs/LAB07.md create mode 100644 lab7c/docs/dashboard.jpg create mode 100644 lab7c/docs/query.jpg create mode 100644 lab7c/loki/config.yml create mode 100644 lab7c/promtail/config.yml diff --git a/lab3c/app_python/app.py b/lab3c/app_python/app.py index 8935b94091..bb284df8a7 100644 --- a/lab3c/app_python/app.py +++ b/lab3c/app_python/app.py @@ -5,6 +5,7 @@ from __future__ import annotations +import json import logging import os import platform @@ -28,11 +29,12 @@ START_TIME = datetime.now(timezone.utc) -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", -) logger = logging.getLogger("devops-info-service") +logger.setLevel(logging.INFO) + +handler = logging.StreamHandler() +handler.setLevel(logging.INFO) +logger.handlers = [handler] app = FastAPI( title="DevOps Info Service", @@ -75,9 +77,38 @@ def isoformat_utc(dt: datetime) -> str: @app.middleware("http") async def log_requests(request: Request, call_next): - logger.info("Request: %s %s", request.method, request.url.path) + start_time = datetime.now(timezone.utc) + logger.info( + json.dumps( + { + "timestamp": isoformat_utc(start_time), + "level": "INFO", + "service": SERVICE_NAME, + "event": "request", + "method": request.method, + "path": request.url.path, + "client_ip": request.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent", "unknown"), + } + ) + ) response = await call_next(request) - logger.info("Response: %s %s -> %s", request.method, request.url.path, response.status_code) + end_time = datetime.now(timezone.utc) + logger.info( + json.dumps( + { + "timestamp": isoformat_utc(end_time), + "level": "INFO", + "service": SERVICE_NAME, + "event": "response", + "method": request.method, + "path": request.url.path, + "status": response.status_code, + "client_ip": request.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent", "unknown"), + } + ) + ) return response @@ -99,7 +130,19 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException): @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception): - logger.exception("Unhandled error: %s", exc) + logger.error( + json.dumps( + { + "timestamp": isoformat_utc(datetime.now(timezone.utc)), + "level": "ERROR", + "service": SERVICE_NAME, + "event": "exception", + "method": request.method, + "path": request.url.path, + "error": str(exc), + } + ) + ) return JSONResponse( status_code=500, content={ diff --git a/lab7c/docker-compose.yml b/lab7c/docker-compose.yml new file mode 100644 index 0000000000..838b56994c --- /dev/null +++ b/lab7c/docker-compose.yml @@ -0,0 +1,110 @@ +version: "3.8" + +networks: + logging: + name: logging + +volumes: + loki-data: + grafana-data: + +services: + loki: + image: grafana/loki:3.0.0 + command: -config.file=/etc/loki/config.yml + ports: + - "3100:3100" + volumes: + - ./loki/config.yml:/etc/loki/config.yml:ro + - loki-data:/loki + networks: + - logging + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3100/ready || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + deploy: + resources: + limits: + cpus: "1.0" + memory: 1G + reservations: + cpus: "0.5" + memory: 512M + + promtail: + image: grafana/promtail:3.0.0 + command: -config.file=/etc/promtail/config.yml + ports: + - "9080:9080" + volumes: + - ./promtail/config.yml:/etc/promtail/config.yml:ro + - /var/log:/var/log:ro + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - logging + depends_on: + - loki + deploy: + resources: + limits: + cpus: "0.5" + memory: 512M + reservations: + cpus: "0.25" + memory: 256M + + grafana: + image: grafana/grafana:12.3.1 + ports: + - "3000:3000" + environment: + # Development defaults. For production, override via .env or secrets. + GF_AUTH_ANONYMOUS_ENABLED: "false" + GF_SECURITY_ADMIN_USER: "admin" + GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD:-admin}" + volumes: + - grafana-data:/var/lib/grafana + networks: + - logging + depends_on: + - loki + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + deploy: + resources: + limits: + cpus: "1.0" + memory: 1G + reservations: + cpus: "0.5" + memory: 512M + + app-python: + image: tsixphoenix/devops-info-python:latest + ports: + - "8000:5000" + networks: + - logging + labels: + logging: "promtail" + app: "devops-python" + environment: + HOST: "0.0.0.0" + PORT: "5000" + deploy: + resources: + limits: + cpus: "0.5" + memory: 512M + reservations: + cpus: "0.25" + memory: 256M + diff --git a/lab7c/docs/LAB07.md b/lab7c/docs/LAB07.md new file mode 100644 index 0000000000..4adc7aca91 --- /dev/null +++ b/lab7c/docs/LAB07.md @@ -0,0 +1,200 @@ +# Lab 7 — Observability & Logging with Loki Stack + +## 1. Architecture + +- **Loki**: log storage and query engine (TSDB on filesystem, 7‑day retention). +- **Promtail**: collects container logs from Docker and ships them to Loki. +- **Grafana**: visualizes logs and dashboards using LogQL. +- **App (FastAPI)**: `devops-info-service` container, logging JSON to stdout. +- All services run in `lab7c/docker-compose.yml` on a shared `logging` network. + +## 2. Setup Guide + +### 2.1 Stack deployment + +```bash +cd monitoring +docker compose up -d +docker compose ps +``` + +Services: +- `loki` on `3100` +- `promtail` on `9080` +- `grafana` on `3000` +- `app-python` on `8000` (mapped to container 5000) + +### 2.2 Verification + +```bash +# Loki readiness +curl http://localhost:3100/ready + +# Promtail targets +curl http://localhost:9080/targets + +# Open Grafana (local) +http://localhost:3000 +``` + +In Grafana: +1. **Connections → Data sources → Add data source → Loki** +2. URL: `http://loki:3100` +3. **Save & Test** → “Data source connected” +4. Go to **Explore**, choose **Loki**, run `{job="docker"}`. + +## 3. Configuration + +### 3.1 Docker Compose (`lab7c/docker-compose.yml`) + +- Defines network `logging` and volumes `loki-data`, `grafana-data`. +- **Loki**: + - Image `grafana/loki:3.0.0` + - Mounts `./loki/config.yml` to `/etc/loki/config.yml` + - Persists data in `loki-data:/loki` + - Health check on `/ready` + - Resource limits and reservations set. +- **Promtail**: + - Image `grafana/promtail:3.0.0` + - Mounts `./promtail/config.yml` + - Mounts `/var/lib/docker/containers` and `/var/run/docker.sock` read‑only. +- **Grafana**: + - Image `grafana/grafana:12.3.1` + - Port `3000:3000` + - Admin user/password via env (for dev: `admin` / `${GRAFANA_ADMIN_PASSWORD:-admin}`). + - Health check on `/api/health`, resource limits. +- **app-python**: + - Image `tsixphoenix/devops-info-python:latest` + - Port `8000:5000` + - Labels `logging="promtail"`, `app="devops-python"` for Promtail/Loki labels. + +### 3.2 Loki (`lab7c/loki/config.yml`) + +- `auth_enabled: false` for local testing. +- `server.http_listen_port: 3100`. +- `common`: + - `path_prefix: /loki` + - filesystem storage for chunks and rules. + - in‑memory ring for a single instance. +- `schema_config`: + - `store: tsdb`, `object_store: filesystem`, `schema: v13`, daily index. +- `storage_config`: + - `tsdb_shipper` index in `/loki/index` with cache. + - filesystem chunks in `/loki/chunks`. +- `limits_config.retention_period: 168h` (7 days). +- `compactor`: + - cleans up old logs with `retention_enabled: true`. + +### 3.3 Promtail (`lab7c/promtail/config.yml`) + +- `server.http_listen_port: 9080`. +- `positions` stored in `/tmp/positions.yaml`. +- `clients` send to `http://loki:3100/loki/api/v1/push`. +- `scrape_configs` for **Docker**: + - `docker_sd_configs` on `unix:///var/run/docker.sock`. + - `relabel_configs`: + - `container` label from `__meta_docker_container_name`. + - `app` label from container label `app`. + - `logging` label from container label `logging`. + +## 4. Application Logging (JSON) + +In `lab3c/app_python/app.py`: +- Switched to **JSON log lines** using the standard `logging` module. +- HTTP middleware logs: + - `timestamp`, `level`, `service`, `method`, `path`, `status`, `client_ip`, `user_agent`. +- Logs are written to stdout and collected by Docker, then by Promtail. + +Example JSON log line: +```json +{ + "timestamp": "2026-03-05T12:20:00Z", + "level": "INFO", + "service": "devops-info-service", + "method": "GET", + "path": "/health", + "status": 200, + "client_ip": "127.0.0.1", + "user_agent": "curl/8.6.0", + "message": "request" +} +``` + +Screenshots used in the report are stored in `lab7c/docs/`, for example: +- `lab7c/docs/grafana-explore.png` — Explore view with `{app="devops-python"}`. +- `lab7c/docs/grafana-dashboard.png` — dashboard with all four panels. + +## 5. Dashboard & LogQL + +### 5.1 Explore queries + +In Grafana Explore (Loki data source): + +- All logs for Python app: +```logql +{app="devops-python"} +``` + +- Only error logs: +```logql +{app="devops-python"} |= "ERROR" +``` + +- Parse JSON and filter by method: +```logql +{app="devops-python"} | json | method="GET" +``` + +### 5.2 Dashboard panels + +Dashboard panels created (LogQL examples): + +1. **Logs Table** (all apps): + ```logql + {app=~"devops-.*"} + ``` +2. **Request Rate** (time series): + ```logql + sum by (app) (rate({app=~"devops-.*"}[1m])) + ``` +3. **Error Logs**: + ```logql + {app=~"devops-.*"} | json | level="ERROR" + ``` +4. **Log Level Distribution**: + ```logql + sum by (level) (count_over_time({app=~"devops-.*"} | json [5m])) + ``` + +## 6. Production Configuration + +- **Resource limits**: all services have `deploy.resources` limits and reservations. +- **Grafana security**: + - Anonymous access disabled (`GF_AUTH_ANONYMOUS_ENABLED=false`). + - Admin credentials configured via environment variables / `.env`. +- **Health checks**: + - Loki: `/ready` endpoint. + - Grafana: `/api/health` endpoint. +- **Retention**: + - Loki configured for 7 days (`retention_period: 168h`) with compactor cleanup. + +## 7. Testing + +1. Start stack: `docker compose up -d`. +2. Generate logs: + ```bash + for i in {1..20}; do curl http://localhost:8000/; done + for i in {1..20}; do curl http://localhost:8000/health; done + ``` +3. In Grafana Explore, run: + - `{app="devops-python"}` + - `{app="devops-python"} | json | method="GET"` + - `{app="devops-python"} | json | level="ERROR"` +4. Check dashboard panels render data. + +## 8. Challenges + +- **Docker TSDB configuration**: required reading Loki 3.0 docs to use `tsdb` with filesystem correctly. +- **Docker discovery**: Promtail needed correct Docker SD and relabeling to get `app` and `container` labels. +- **JSON logging**: changing logging format without breaking existing behavior and keeping logs parseable in Loki. + diff --git a/lab7c/docs/dashboard.jpg b/lab7c/docs/dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..05843cfab416a2df7b8caeeba7014ad7538c50bd GIT binary patch literal 69272 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⪼ZbmgW&i_)McZcT;{kO`z2*ikaoQb46gNL1 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

peLONZ!%$3EZiLY!JBSKfoFbkuv$5x9m*C+P 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 G@BS~g8+Y^o literal 0 HcmV?d00001 diff --git a/lab7c/loki/config.yml b/lab7c/loki/config.yml new file mode 100644 index 0000000000..6a9219da04 --- /dev/null +++ b/lab7c/loki/config.yml @@ -0,0 +1,43 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + tsdb_shipper: + active_index_directory: /loki/index + cache_location: /loki/cache + cache_ttl: 24h + filesystem: + directory: /loki/chunks + +limits_config: + retention_period: 168h + +compactor: + working_directory: /loki/compactor + retention_enabled: true + delete_request_store: filesystem + diff --git a/lab7c/promtail/config.yml b/lab7c/promtail/config.yml new file mode 100644 index 0000000000..eb99e73798 --- /dev/null +++ b/lab7c/promtail/config.yml @@ -0,0 +1,29 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + + relabel_configs: + # Container name label (without leading slash) + - source_labels: [__meta_docker_container_name] + target_label: container + regex: "/(.*)" + replacement: "$1" + + # Propagate container labels as Loki labels + - source_labels: [__meta_docker_container_label_app] + target_label: app + - source_labels: [__meta_docker_container_label_logging] + target_label: logging + From 7fdc5f849b393d8909c450a78869cf0455c63b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 9 Mar 2026 18:30:42 +0300 Subject: [PATCH 14/14] Update LAB07.md --- lab7c/docs/LAB07.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lab7c/docs/LAB07.md b/lab7c/docs/LAB07.md index 4adc7aca91..f898ad53f1 100644 --- a/lab7c/docs/LAB07.md +++ b/lab7c/docs/LAB07.md @@ -120,10 +120,7 @@ Example JSON log line: } ``` -Screenshots used in the report are stored in `lab7c/docs/`, for example: -- `lab7c/docs/grafana-explore.png` — Explore view with `{app="devops-python"}`. -- `lab7c/docs/grafana-dashboard.png` — dashboard with all four panels. - +Screenshots are stored in `lab7c/docs/`. ## 5. Dashboard & LogQL ### 5.1 Explore queries