diff --git a/.gitignore b/.gitignore index 30d74d2584..c3a607003a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -test \ No newline at end of file +test +terraform/.terraform/providers/registry.terraform.io/hashicorp/aws/6.33.0/darwin_arm64/.terraform/ +*.tfstate +*.tfstate.* +.terraform.lock.hcl diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..ef181e178a --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,5 @@ +bin/ +*.log +__pycache__/ +.git +vendor/ diff --git a/app_go/.gitignore b/app_go/.gitignore new file mode 100644 index 0000000000..3d2508ceeb --- /dev/null +++ b/app_go/.gitignore @@ -0,0 +1,10 @@ +# Go build output +bin/ +*.out + +# IDE / Editor +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..47f2677eb1 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,30 @@ +## Multi-stage Dockerfile for Go app + +# Builder stage: use official Go image to compile a static Linux binary +FROM golang:1.22-alpine AS builder + +WORKDIR /src + +# Download dependencies separately to leverage caching +COPY go.mod ./ +RUN apk add --no-cache git \ + && go mod tidy + +# Copy source +COPY . . + +# Build a statically linked binary for Linux +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags "-s -w" -o /devops-info ./ + +# Final stage: tiny runtime image +FROM scratch + +# Copy binary from builder +COPY --from=builder /devops-info /devops-info + +# Expose port and use non-root numeric UID +EXPOSE 5002 +USER 1000 + +ENTRYPOINT ["/devops-info"] diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..f6125d649b --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,54 @@ +# DevOps Info Service (Go, Bonus) + +## Overview + +This directory contains a Go implementation of the **DevOps info service** with the same endpoints and JSON structure as the Python version: + +- `GET /` — service, system, runtime, request and endpoints information +- `GET /health` — simple health check for monitoring and Kubernetes probes + +## Prerequisites + +- Go 1.22+ installed + +## Build and Run + +```bash +cd app_go + +# Run directly +go run ./... + +# Or build a binary +mkdir -p bin +go build -o bin/devops-info-service-go ./... + +# Run the binary (default PORT=5002) +./bin/devops-info-service-go + +# Custom port +PORT=8080 ./bin/devops-info-service-go +``` + +## API Endpoints + +- `GET /` + - Returns JSON with: + - Service metadata (name, version, description, framework) + - System info (hostname, platform, architecture, CPU count, Go version) + - Runtime info (uptime in seconds and human readable, current time, timezone) + - Request info (client IP, user agent, method, path) + - List of available endpoints + +- `GET /health` + - Returns JSON with status, timestamp and uptime in seconds. + +## Configuration + +Configuration is done through environment variables: + +| Variable | Default | Description | +|----------|---------|------------------------------------| +| `PORT` | `5002` | TCP port for HTTP server | + +The server listens on `0.0.0.0:PORT` by default. diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..6102625301 --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,103 @@ +# LAB02 — Multi-stage Docker build for Go (English) + +Date: 2026-02-05 + +Goal: demonstrate a multi-stage Docker build for the Go application and explain decisions, trade-offs and measurements. + +## Multi-stage build strategy + +- **Builder stage (golang:1.22-alpine)** — compiles a statically linked binary with `CGO_ENABLED=0` and `-ldflags "-s -w"` to strip debugging symbols. +- **Runtime stage (scratch)** — copies only the resulting binary from the builder (`COPY --from=builder`) into a minimal image with no package manager or shell. + +Why this matters: +- The builder image contains compilers, source and toolchain (large). The final image contains only the binary, dramatically reducing size and attack surface. +- `COPY --from=builder` pulls artifacts from the named build stage into the final image. + +Dockerfile excerpts: + +```dockerfile +FROM golang:1.22-alpine AS builder +WORKDIR /src +COPY go.mod ./ +RUN apk add --no-cache git && go mod tidy +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o /devops-info ./ + +FROM scratch +COPY --from=builder /devops-info /devops-info +EXPOSE 5002 +USER 1000 +ENTRYPOINT ["/devops-info"] +``` + +Notes: +- `CGO_ENABLED=0` ensures a static binary that can run in `scratch` (no libc present). If the app uses C bindings, static build may fail and a different runtime base (e.g., distroless) may be required. +- We set `GOARCH=amd64` to produce an amd64 binary; adjust for your target architecture if needed (e.g., `arm64`). + +## Build & size measurements + +Commands executed locally (captured output below): + +```bash +docker build --platform=linux/amd64 -t devops-go:lab2 app_go +docker build --platform=linux/amd64 --target builder -t devops-go:builder app_go +docker images devops-go:lab2 --format "{{.Repository}}:{{.Tag}} {{.Size}}" +docker images devops-go:builder --format "{{.Repository}}:{{.Tag}} {{.Size}}" +``` + +Key terminal output (build & sizes): + +``` +... (build output omitted) ... +devops-go:lab2 2.16MB +devops-go:builder 103MB +``` + +Analysis: +- **Builder image (103MB)**: includes the Go toolchain and apk packages required to fetch dependencies and build the binary. +- **Final image (2.16MB)**: contains only the stripped static binary — well under the 20MB challenge target. + +Why you can't use the builder image as the final image: +- The builder contains compilers and package managers which increase image size and add additional attack surface. Keeping build tools out of production images reduces risk and distribution size. + +Security implications: +- Smaller images have fewer packages and fewer potential vulnerabilities. +- `scratch` has no shell; if an attacker gains code execution they have very limited tooling. +- Running as a non-root numeric user (`USER 1000`) reduces privilege even further. + +Trade-offs and notes: +- Using `scratch` gives the smallest possible image but also removes diagnostic tools; debugging requires reproducing the builder environment or adding temporary debug builds. +- If the binary depends on cgo or system libs, `scratch` may be unsuitable; use a minimal distro or distroless image. + +## Technical explanation of each stage + +- Builder stage: + - Installs `git` to allow `go mod tidy` to fetch modules. + - Copies `go.mod`, runs `go mod tidy` to populate `go.sum` and download dependencies (cached when `go.mod` hasn't changed). + - Copies source and builds a static binary with optimizations and stripped symbols. + +- Final stage: + - Uses `scratch` for minimal footprint. + - Copies only the binary. + - Exposes the application port and runs the binary directly. + +## Terminal outputs (selected) + +Build final image (abridged): + +``` +#10 [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o /devops-info ./ +#12 exporting to image +#12 naming to docker.io/library/devops-go:lab2 done +``` + +Image sizes: + +``` +devops-go:lab2 2.16MB +devops-go:builder 103MB +``` + +## Conclusion + +The multi-stage build achieved a very small runtime image (2.16MB) by compiling a static Go binary and copying only the artifact into a `scratch` image. This reduces distribution size and attack surface, and demonstrates the typical pattern to containerize compiled-language applications efficiently. diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..8e1c02997d --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service-go + +go 1.22 diff --git a/app_go/internal/config/config.go b/app_go/internal/config/config.go new file mode 100644 index 0000000000..6f4afc502a --- /dev/null +++ b/app_go/internal/config/config.go @@ -0,0 +1,20 @@ +package config + +import "os" + +// Config holds application configuration. +type Config struct { + Port string +} + +// FromEnv reads configuration from environment variables with sensible defaults. +func FromEnv() Config { + port := os.Getenv("PORT") + if port == "" { + port = "5002" + } + + return Config{ + Port: port, + } +} diff --git a/app_go/internal/info/handlers.go b/app_go/internal/info/handlers.go new file mode 100644 index 0000000000..724580fd72 --- /dev/null +++ b/app_go/internal/info/handlers.go @@ -0,0 +1,99 @@ +package info + +import ( + "encoding/json" + "log" + "net" + "net/http" + "os" + "runtime" + "time" + + "devops-info-service-go/internal/uptime" +) + +// MainHandler returns an http.HandlerFunc for the "/" endpoint. +func MainHandler(logger *log.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger.Printf("Handling main endpoint: method=%s path=%s", r.Method, r.URL.Path) + + uptimeSeconds, uptimeHuman := uptime.Get() + + hostname, err := os.Hostname() + if err != nil { + logger.Printf("error getting hostname: %v", err) + hostname = "unknown" + } + + now := time.Now().UTC() + + clientIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + clientIP = r.RemoteAddr + } + + info := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service (Go)", + Framework: "net/http", + }, + System: System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: runtime.Version(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: now.Format(time.RFC3339Nano), + Timezone: "UTC", + }, + Request: Request{ + ClientIP: clientIP, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: http.MethodGet, Description: "Service information"}, + {Path: "/health", Method: http.MethodGet, Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(info); err != nil { + logger.Printf("error encoding main response: %v", err) + } else { + logger.Printf("Successfully handled main endpoint for client_ip=%s", info.Request.ClientIP) + } + } +} + +// HealthHandler returns an http.HandlerFunc for the "/health" endpoint. +func HealthHandler(logger *log.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger.Printf("Handling health endpoint: method=%s path=%s", r.Method, r.URL.Path) + + uptimeSeconds, _ := uptime.Get() + + now := time.Now().UTC() + + health := Health{ + Status: "healthy", + Timestamp: now.Format(time.RFC3339Nano), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(health); err != nil { + logger.Printf("error encoding health response: %v", err) + } else { + logger.Printf("Successfully handled health endpoint, uptime_seconds=%d", health.UptimeSeconds) + } + } +} diff --git a/app_go/internal/info/models.go b/app_go/internal/info/models.go new file mode 100644 index 0000000000..d253127643 --- /dev/null +++ b/app_go/internal/info/models.go @@ -0,0 +1,52 @@ +package info + +// ServiceInfo represents the full JSON payload for the main endpoint. +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type Runtime struct { + UptimeSeconds int64 `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type Request struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type Health struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int64 `json:"uptime_seconds"` +} diff --git a/app_go/internal/server/server.go b/app_go/internal/server/server.go new file mode 100644 index 0000000000..42438daf95 --- /dev/null +++ b/app_go/internal/server/server.go @@ -0,0 +1,75 @@ +package server + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "devops-info-service-go/internal/info" +) + +// New creates an HTTP handler with registered routes and middleware. +func New(logger *log.Logger) http.Handler { + mux := http.NewServeMux() + + mux.Handle("/", info.MainHandler(logger)) + logger.Println("Registered route: GET /") + + mux.Handle("/health", info.HealthHandler(logger)) + logger.Println("Registered route: GET /health") + + // Wrap with middleware: recovery then logging. + var handler http.Handler = mux + handler = recoveryMiddleware(logger, handler) + handler = loggingMiddleware(logger, handler) + + return handler +} + +// loggingMiddleware logs basic request information and latency. +func loggingMiddleware(logger *log.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + + logger.Printf("Incoming request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) + + next.ServeHTTP(ww, r) + + duration := time.Since(start) + logger.Printf("%s %s %d %s", r.Method, r.URL.Path, ww.statusCode, duration) + }) +} + +// recoveryMiddleware recovers from panics and returns a JSON 500 error. +func recoveryMiddleware(logger *log.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + logger.Printf("panic recovered: %v", rec) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + + _ = json.NewEncoder(w).Encode(map[string]string{ + "error": "Internal Server Error", + "message": "Unexpected server error", + }) + } + }() + + next.ServeHTTP(w, r) + }) +} + +// responseWriter wraps http.ResponseWriter to capture status codes. +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (w *responseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/app_go/internal/uptime/uptime.go b/app_go/internal/uptime/uptime.go new file mode 100644 index 0000000000..194e485330 --- /dev/null +++ b/app_go/internal/uptime/uptime.go @@ -0,0 +1,18 @@ +package uptime + +import ( + "fmt" + "time" +) + +var startTime = time.Now() + +// Get returns uptime in seconds and human-readable format. +func Get() (seconds int64, human string) { + delta := time.Since(startTime) + seconds = int64(delta.Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + human = fmt.Sprintf("%d hours, %d minutes", hours, minutes) + return +} diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..8d47d64a57 --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "net/http" + "os" + + "devops-info-service-go/internal/config" + "devops-info-service-go/internal/server" +) + +func main() { + logger := log.New(os.Stdout, "devops-go ", log.LstdFlags|log.LUTC|log.Lshortfile) + + cfg := config.FromEnv() + logger.Printf("Loaded config: port=%s", cfg.Port) + + handler := server.New(logger) + + addr := ":" + cfg.Port + logger.Printf("Starting Go devops-info-service on %s", addr) + + if err := http.ListenAndServe(addr, handler); err != nil { + logger.Fatalf("server error: %v", err) + } +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..563cc2fd15 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +.pytest_cache/ +tests/ +docs/ +.git +.venv +venv/ +env/ +*.env +*.egg-info/ diff --git a/app_python/.env_example b/app_python/.env_example new file mode 100644 index 0000000000..01ef2c131d --- /dev/null +++ b/app_python/.env_example @@ -0,0 +1,8 @@ +# Example environment configuration for DevOps Info Service (Python) + +# Host and port configuration +HOST=0.0.0.0 +PORT=5002 + +# Enable debug mode only in development +DEBUG=false diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..3c9c67f86a --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,20 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# Virtual environments +venv/ +.env/ +.env + +# Logs +*.log + +# IDE / Editor +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..fc1f8c476f --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Create a non-root user and group +RUN groupadd --gid 1000 appgroup \ + && useradd --uid 1000 --gid appgroup --create-home --home-dir /home/appuser --shell /bin/bash appuser \ + && mkdir -p /app \ + && chown -R appuser:appgroup /app + +WORKDIR /app + +# Install Python dependencies first to leverage layer caching +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy only the application code +COPY app.py ./ + +# Ensure app directory ownership, switch to non-root user +RUN chown -R appuser:appgroup /app +USER appuser + +# Document the port the app uses +EXPOSE 5002 + +# Start the application +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..25a9880577 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,69 @@ +# DevOps Info Service (Lab 1) + +## Overview + +This project implements a simple **DevOps info service** written in Python using **Flask**. The service exposes HTTP endpoints that return detailed information about the application, the underlying system, and its runtime health. It is the base for later labs (Docker, CI/CD, monitoring, persistence, etc.). + +## Prerequisites + +- Python 3.11+ (recommended) +- pip (Python package manager) +- Optional: `virtualenv` or `venv` for isolated environments + +## Installation + +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +pip install -r requirements.txt +cp .env_example .env # create local env file from example +``` + +## Running the Application + +```bash +# Default configuration (HOST=0.0.0.0, PORT=5002, DEBUG=False) +python app.py + +# Custom configuration via environment variables +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 DEBUG=true python app.py +``` + +## API Endpoints + +- `GET /` – Service and system information + - Service metadata (name, version, description, framework) + - System info (hostname, platform, architecture, CPU count, Python version) + - Runtime info (uptime, current time, timezone) + - Request info (client IP, user agent, HTTP method, path) + - List of available endpoints + +- `GET /health` – Health check + - Returns basic health status, timestamp, and uptime in seconds + +## Configuration + +Configuration is done via environment variables: + +| Variable | Default | Description | +|---------|-------------|---------------------------------------| +| `HOST` | `0.0.0.0` | Address the Flask app listens on | +| `PORT` | `5002` | TCP port for HTTP server | +| `DEBUG` | `False` | Enable Flask debug mode if `true` | + +All configuration is read in `app.py` at startup, so restart the application after changing environment variables. + +## Docker + +How to use the containerized application (patterns): + +- **Build image (local):** `docker build -t : ` +- **Tag for Docker Hub:** `docker tag : /:` +- **Run container (local):** `docker run -p : --name ` +- **Pull from Docker Hub:** `docker pull /:` + +Notes: +- The container exposes port `5002` by default (see `app.py`). +- The image runs as a non-root user for improved security. + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..8fe89858ca --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,160 @@ +""" +DevOps Info Service +Main application module for Lab 1. +""" + +import logging +import os +import platform +import socket +from datetime import datetime, timezone +from typing import Any, Dict + +from flask import Flask, jsonify, request + + +app = Flask(__name__) + + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5002)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + + +# Application start time (for uptime calculation) +START_TIME = datetime.now(timezone.utc) + + +# Logging configuration +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) +logger.info("Application starting...") + + +def get_uptime() -> Dict[str, Any]: + """Return uptime in seconds and human-readable form.""" + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + "seconds": seconds, + "human": f"{hours} hours, {minutes} minutes", + } + + +def get_system_info() -> Dict[str, Any]: + """Collect system information.""" + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.platform(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count() or 1, + "python_version": platform.python_version(), + } + + +def get_request_info() -> Dict[str, Any]: + """Collect request information from the current Flask request.""" + client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) + user_agent = request.headers.get("User-Agent", "") + + return { + "client_ip": client_ip, + "user_agent": user_agent, + "method": request.method, + "path": request.path, + } + + +@app.route("/", methods=["GET"]) +def index(): + """Main endpoint - service and system information.""" + uptime = get_uptime() + system_info = get_system_info() + request_info = get_request_info() + + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": system_info, + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": request_info, + "endpoints": [ + { + "path": "/", + "method": "GET", + "description": "Service information", + }, + { + "path": "/health", + "method": "GET", + "description": "Health check", + }, + ], + } + + logger.info("Handled main / request") + return jsonify(response) + + +@app.route("/health", methods=["GET"]) +def health(): + """Health check endpoint.""" + uptime = get_uptime() + payload = { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime["seconds"], + } + logger.info("Health check OK") + return jsonify(payload), 200 + + +@app.errorhandler(404) +def not_found(error): + """Return JSON for 404 errors.""" + logger.warning("404 Not Found: %s %s", request.method, request.path) + return ( + jsonify( + { + "error": "Not Found", + "message": "Endpoint does not exist", + } + ), + 404, + ) + + +@app.errorhandler(500) +def internal_error(error): + """Return JSON for 500 errors.""" + logger.exception("500 Internal Server Error") + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + ), + 500, + ) + + +if __name__ == "__main__": + logger.info("Starting Flask development server on %s:%s", HOST, PORT) + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..9feb7536df --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,110 @@ +# LAB 1 — DevOps Info Service (Go Bonus) + +## 1. Language Choice (Go) + +For the compiled implementation I chose **Go**. It is well suited for small HTTP services because: + +- It produces small, static binaries that are easy to ship in Docker images. +- The standard library includes a solid `net/http` package, so no heavy frameworks are required. +- Compiles quickly and has good performance for concurrent workloads. + +## 2. Implementation Overview + +The Go service exposes the same endpoints and similar JSON structure as the Python/Flask version: + +- `GET /` — returns `service`, `system`, `runtime`, `request`, `endpoints`. +- `GET /health` — returns `status`, `timestamp`, `uptime_seconds`. + +The uptime is calculated from a `startTime` global set at application start. System information uses values from the Go runtime (OS, architecture, CPU count, Go version) and the OS hostname. + +## 3. Build and Run + +```bash +cd app_go + +# Run directly (development) +go run ./... + +# Build binary +mkdir -p bin +go build -o bin/devops-info-service-go ./... + +# Run with default configuration (PORT=5002) +./bin/devops-info-service-go + +# Custom port +PORT=8080 ./bin/devops-info-service-go +``` + +## 4. API Documentation + +### `GET /` + +Example (shortened): + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "framework": "net/http" + }, + "system": { + "platform": "linux", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.22.0" + }, + "runtime": { + "uptime_seconds": 120, + "uptime_human": "0 hours, 2 minutes" + }, + "request": { + "client_ip": "127.0.0.1:53412", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET"}, + {"path": "/health", "method": "GET"} + ] +} +``` + +### `GET /health` + +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T12:00:00.000000Z", + "uptime_seconds": 360 +} +``` + +### Testing Commands + +```bash +# Main endpoint +curl -s http://localhost:5002/ | jq + +# Health check +curl -s http://localhost:5002/health | jq +``` + +## 5. Binary Size Comparison + +After building both implementations in release mode (Python in a slim image and Go as a static binary) you can compare image or binary sizes: + +- Go binary `bin/devops-info-service-go` is typically much smaller than a full Python runtime + dependencies. +- This makes multi‑stage Docker builds efficient: first stage builds the Go binary, second stage copies only the small binary. + +## 6. Screenshots + +Screenshots for the Go version can be stored in `app_go/docs/screenshots/`: + +- `01-main-endpoint-go.png` — `/` response +- `02-health-check-go.png` — `/health` response + +## 7. GitHub Community + +Starring repositories is important in open source because it shows maintainers that their work is useful and increases the visibility of good projects for the wider community. Following developers (professors, TAs, and classmates) helps you discover new projects, learn from real-world commits, and build a professional network that makes teamwork and long‑term career growth easier. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..7f2ffb49d2 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,205 @@ +# LAB02 — Документация по Docker + +Дата: 2026-02-05 + +Цель: документировать решение по контейнеризации `app_python` и объяснить принятые решения. + +## 1. Docker Best Practices Applied + +- **Non-root user**: в `Dockerfile` создан пользователь `appuser` и используется инструкция `USER appuser`. + - Почему важно: запуск приложения не от root снижает риск эскалации привилегий в случае уязвимости. + - Фрагмент Dockerfile: + +# LAB02 — Docker documentation + +Date: 2026-02-05 + +Goal: document the containerization of `app_python` and explain implementation decisions. + +## 1. Docker Best Practices Applied + +- **Non-root user**: the `Dockerfile` creates a dedicated user `appuser` and switches to it with `USER appuser`. + - Why it matters: running the application as non-root reduces the risk of privilege escalation if the application is compromised. + - Dockerfile snippet: + +```dockerfile +RUN groupadd --gid 1000 appgroup \ + && useradd --uid 1000 --gid appgroup --create-home --home-dir /home/appuser --shell /bin/bash appuser +USER appuser +``` + +- **Layer caching (layer ordering)**: dependencies are copied and installed first (`COPY requirements.txt` + `RUN pip install ...`), then application code is copied. + - Why it matters: when code changes, dependency layers remain cached so builds are faster. + - Dockerfile snippet: + +```dockerfile +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py ./ +``` + +- **.dockerignore**: unnecessary files are excluded (`__pycache__`, `tests`, `docs`, `.git`, virtual environments, etc.). + - Why it matters: reduces build context size, speeds up context upload, and lowers the chance of accidentally including secrets. + - Example entries: `__pycache__/`, `*.pyc`, `tests/`, `docs/`, `.git`, `venv/`. + +- **Minimal files copied into the image**: only `requirements.txt` and `app.py` are copied. + - Why it matters: reduces image size and attack surface. + +- **Documented port**: `EXPOSE 5002` reflects the `PORT` used in `app.py`. + - Why it matters: clarifies how container port maps to host port. + +## 2. Image Information & Decisions + +- **Base image:** `python:3.13-slim` + - Rationale: a recent Python 3.13 base that balances completeness with size; the `-slim` variant reduces unnecessary packages while still being well-supported. + +- **Final image size:** locally the image is `208MB` for `sofiakulagina/devops-info:lab2`. + - Assessment: acceptable for a simple Flask app without compiled dependencies. Options to reduce size further include `python:3.13-alpine` or multi-stage builds, but alpine can require additional work for binary dependencies. + +- **Layer structure:** + 1. `FROM python:3.13-slim` + 2. create user and directories (`RUN groupadd && useradd ...`) + 3. `WORKDIR /app` + 4. `COPY requirements.txt` (dependencies layer) + 5. `RUN pip install ...` (installed packages) + 6. `COPY app.py` (application code) + 7. set ownership and switch to non-root user + 8. `EXPOSE` and `CMD` + + - Optimization rationale: place rarely-changed layers (dependencies) before frequently-changed layers (code). + +## 3. Build & Run Process + +Below is the full build and push output copied from the terminal: + +``` +[+] Building 13.7s (13/13) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 762B 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 2.2s + => [auth] library/python:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 127B 0.0s + => [1/7] FROM docker.io/library/python:3.13-slim@sha256:49b618b8afc2742b94f 7.5s + => => resolve docker.io/library/python:3.13-slim@sha256:49b618b8afc2742b94f 0.0s + => => sha256:4c4a8dac933699cea1f21584a1e5db68e248aadadfff93ddd7 251B / 251B 0.4s + => => sha256:14c37da83ac4440d59e5d2c0f06fb6ccd1c771929bd408 1.27MB / 1.27MB 1.0s + => => sha256:af94c6242df37e8cf3963ed59ccc0252e79a0554a8f1 11.73MB / 11.73MB 3.7s + => => sha256:3ea009573b472d108af9af31ec35a06fe3649084f661 30.14MB / 30.14MB 6.8s + => => extracting sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7 0.4s + => => extracting sha256:14c37da83ac4440d59e5d2c0f06fb6ccd1c771929bd4083c0a3 0.0s + => => extracting sha256:af94c6242df37e8cf3963ed59ccc0252e79a0554a8f18f4555d 0.2s + => => extracting sha256:4c4a8dac933699cea1f21584a1e5db68e248aadadfff93ddd73 0.0s + => [internal] load build context 0.0s + => => transferring context: 4.20kB 0.0s + => [2/7] RUN groupadd --gid 1000 appgroup && useradd --uid 1000 --gid a 0.5s + => [3/7] WORKDIR /app 0.0s + => [4/7] COPY requirements.txt ./ 0.0s + => [5/7] RUN pip install --no-cache-dir -r requirements.txt 3.0s + => [6/7] COPY app.py ./ 0.0s + => [7/7] RUN chown -R appuser:appgroup /app 0.1s + => exporting to image 0.3s + => => exporting layers 0.1s + => => exporting manifest sha256:c642c1c7aaee4610b39023ffcadbed270e6451b5c9f 0.0s + => => exporting config sha256:a9190d3d8c85ae31f27fa2ce1878878d83fce01a98442 0.0s + => => exporting attestation manifest sha256:5e06288fb0b759a0d0c0d51a3509877 0.1s + => => exporting manifest list sha256:2e664e7122e99b89753b34ffa33fad32cf91c2 0.0s + => => naming to docker.io/library/devops-info:lab2 0.0s + => => unpacking to docker.io/library/devops-info:lab2 0.0s +``` + +Authentication and push output: + +``` +Authenticating with existing credentials... [Username: sofiakulagina] + +i Info → To login with a different account, run 'docker logout' followed by 'docker login' + +Login Succeeded +The push refers to repository [docker.io/sofiakulagina/devops-info] +fb9638eac157: Pushed +4c4a8dac9336: Pushed +3ea009573b47: Pushed +334e78adf83a: Pushed +de87663f4206: Pushed +af94c6242df3: Pushed +37c0e012c728: Pushed +004840e29eb7: Pushed +4f4fb700ef54: Mounted from vulhub/langflow +de09dc1fb1f3: Pushed +14c37da83ac4: Pushed +lab2: digest: sha256:2e664e7122e99b89753b34ffa33fad32cf91c2843be398292fd1d2a97b167558 size: 856 +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 2189, + "digest": "sha256:c642c1c7aaee4610b39023ffcadbed270e6451b5c9f1019b3c3cc483d6c260df", + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 566, + "digest": "sha256:5e06288fb0b759a0d0c0d51a35098779dc0b0e8c68ff67b936c7e3f844a25006", + "platform": { + "architecture": "unknown", + "os": "unknown" + } + } + ] +} +``` + +Below is the output from running the container and testing endpoints locally: + +``` +de299d26c9701ccf05e8fc8221db02946e058a9a3c8b766ec3d7c9ad38784f05 +de299d26c970 sofiakulagina/devops-info:lab2 Up 1 second +2026-02-05 10:40:00,921 - __main__ - INFO - Application starting... +2026-02-05 10:40:00,921 - __main__ - INFO - Starting Flask development server on 0. +0.0.0.0:5002 * Serving Flask app 'app' + * Debug mode: off +2026-02-05 10:40:00,926 - werkzeug - INFO - WARNING: This is a development server. +Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5002 + * Running on http://172.17.0.2:5002 +2026-02-05 10:40:00,926 - werkzeug - INFO - Press CTRL+C to quit +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"192.168.65.1","method":"GET","path":"/","user_agent":"curl/8.7.1"},"runtime":{"current_time":"2026-02-05T10:40:01.742865+00:00","timezone":"UTC","uptime_human":"0 hours, 0 minutes","uptime_seconds":0},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"aarch64","cpu_count":10,"hostname":"de299d26c970","platform":"Linux","platform_version":"Linux-6.10.14-linuxkit-aarch64-with-glibc2.41","python_version":"3.13.12"}} +{"status":"healthy","timestamp":"2026-02-05T10:40:01.753235+00:00","uptime_seconds":0} +``` + +**Docker Hub repository URL:** https://hub.docker.com/r/sofiakulagina/devops-info + +## 4. Technical Analysis + +- **Why the Dockerfile works:** instruction ordering minimizes unnecessary layer rebuilds—dependencies are installed before copying frequently-changing application code. The user and permissions are created and fixed before switching to the non-root user. + +- **What happens if layer order changes:** copying the entire source first would invalidate the cached dependency layers on every code change, causing slower rebuilds. + +- **Security considerations:** running as non-root, limiting copied files, and excluding sensitive files via `.dockerignore`. + +- **How `.dockerignore` improves build:** reduces context size, lowers the risk of accidentally including sensitive or unnecessary files, and speeds up `docker build`. + +## 5. Challenges & Solutions + +- **Issue:** an earlier attempt to run `docker build` failed because the build context `app_python` was not found. + - **How I debugged:** confirmed the current working directory was the repository root and that the `app_python` folder exists; rerunning the command from the correct directory fixed the issue. + +- **Issue:** Flask prints a warning about using the development server. + - **Resolution/notes:** for production, use a WSGI server like `gunicorn` or `uvicorn`; for lab/testing, the development server is acceptable. + +## 6. Improvements to consider + +- Switch to a production-ready WSGI server (`gunicorn`) and update `CMD` accordingly. +- Consider multi-stage builds or `python:3.13-alpine` to reduce image size (account for compiling dependencies). +- Add CI to build and push images automatically on releases. + +--- + +Files: `Dockerfile` and `.dockerignore` are in `app_python`. diff --git a/app_python/docs/screenshots/endpoint_check.png b/app_python/docs/screenshots/endpoint_check.png new file mode 100644 index 0000000000..52c73f342a Binary files /dev/null and b/app_python/docs/screenshots/endpoint_check.png differ diff --git a/app_python/docs/screenshots/test.png b/app_python/docs/screenshots/test.png new file mode 100644 index 0000000000..67cfef477a Binary files /dev/null and b/app_python/docs/screenshots/test.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..22ac75b399 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..222f4ab610 --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,86 @@ +import os +import sys +import unittest + + +# Allow importing app_python/app.py as a module named "app" +TESTS_DIR = os.path.dirname(__file__) +APP_DIR = os.path.dirname(TESTS_DIR) +sys.path.insert(0, APP_DIR) + +from app import app as flask_app # noqa: E402 + + +class DevOpsInfoServiceTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + flask_app.testing = True + cls.client = flask_app.test_client() + + def test_root_endpoint_returns_expected_structure(self): + resp = self.client.get("/") + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.is_json) + + data = resp.get_json() + self.assertIsInstance(data, dict) + + # Top-level keys + for key in ("service", "system", "runtime", "request", "endpoints"): + self.assertIn(key, data) + + # Service + self.assertEqual(data["service"]["name"], "devops-info-service") + self.assertEqual(data["service"]["version"], "1.0.0") + self.assertEqual(data["service"]["framework"], "Flask") + + # System + self.assertIn("hostname", data["system"]) + self.assertIn("platform", data["system"]) + self.assertIn("platform_version", data["system"]) + self.assertIn("architecture", data["system"]) + self.assertIn("cpu_count", data["system"]) + self.assertIn("python_version", data["system"]) + + # Runtime + self.assertGreaterEqual(int(data["runtime"]["uptime_seconds"]), 0) + self.assertIsInstance(data["runtime"]["uptime_human"], str) + self.assertIsInstance(data["runtime"]["current_time"], str) + self.assertEqual(data["runtime"]["timezone"], "UTC") + + # Request + self.assertEqual(data["request"]["method"], "GET") + self.assertEqual(data["request"]["path"], "/") + self.assertIn("client_ip", data["request"]) + self.assertIn("user_agent", data["request"]) + + # Endpoints list + endpoints = data["endpoints"] + self.assertIsInstance(endpoints, list) + paths = {e.get("path") for e in endpoints if isinstance(e, dict)} + self.assertIn("/", paths) + self.assertIn("/health", paths) + + def test_health_endpoint_returns_expected_payload(self): + resp = self.client.get("/health") + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.is_json) + + data = resp.get_json() + self.assertEqual(data["status"], "healthy") + self.assertIsInstance(data["timestamp"], str) + self.assertGreaterEqual(int(data["uptime_seconds"]), 0) + + def test_not_found_returns_json_404(self): + resp = self.client.get("/does-not-exist") + self.assertEqual(resp.status_code, 404) + self.assertTrue(resp.is_json) + + data = resp.get_json() + self.assertEqual(data["error"], "Not Found") + self.assertIn("message", data) + + +if __name__ == "__main__": + unittest.main() + diff --git a/pulumi/.gitignore b/pulumi/.gitignore new file mode 100644 index 0000000000..a3807e5bdb --- /dev/null +++ b/pulumi/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/pulumi/Pulumi.dev.yaml b/pulumi/Pulumi.dev.yaml new file mode 100644 index 0000000000..1a38cefb17 --- /dev/null +++ b/pulumi/Pulumi.dev.yaml @@ -0,0 +1,2 @@ +config: + aws:region: us-east-1 diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..57c871f485 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,10 @@ +name: lab04 +description: lab04 +runtime: + name: python + options: + toolchain: poetry +config: + pulumi:tags: + value: + pulumi:template: aws-python diff --git a/pulumi/README.md b/pulumi/README.md new file mode 100644 index 0000000000..7b0b6d100b --- /dev/null +++ b/pulumi/README.md @@ -0,0 +1,92 @@ + # AWS Python S3 Bucket Pulumi Template + + A minimal Pulumi template for provisioning a single AWS S3 bucket using Python. + + ## Overview + + This template provisions an S3 bucket (`pulumi_aws.s3.BucketV2`) in your AWS account and exports its ID as an output. It’s an ideal starting point when: + - You want to learn Pulumi with AWS in Python. + - You need a barebones S3 bucket deployment to build upon. + - You prefer a minimal template without extra dependencies. + + ## Prerequisites + + - An AWS account with permissions to create S3 buckets. + - AWS credentials configured in your environment (for example via AWS CLI or environment variables). + - Python 3.6 or later installed. + - Pulumi CLI already installed and logged in. + + ## Getting Started + + 1. Generate a new project from this template: + ```bash + pulumi new aws-python + ``` + 2. Follow the prompts to set your project name and AWS region (default: `us-east-1`). + 3. Change into your project directory: + ```bash + cd + ``` + 4. Preview the planned changes: + ```bash + pulumi preview + ``` + 5. Deploy the stack: + ```bash + pulumi up + ``` + 6. Tear down when finished: + ```bash + pulumi destroy + ``` + + ## Project Layout + + After running `pulumi new`, your directory will look like: + ``` + ├── __main__.py # Entry point of the Pulumi program + ├── Pulumi.yaml # Project metadata and template configuration + ├── requirements.txt # Python dependencies + └── Pulumi..yaml # Stack-specific configuration (e.g., Pulumi.dev.yaml) + ``` + + ## Configuration + + This template defines the following config value: + + - `aws:region` (string) + The AWS region to deploy resources into. + Default: `us-east-1` + + View or update configuration with: + ```bash + pulumi config get aws:region + pulumi config set aws:region us-west-2 + ``` + + ## Outputs + + Once deployed, the stack exports: + + - `bucket_name` — the ID of the created S3 bucket. + + Retrieve outputs with: + ```bash + pulumi stack output bucket_name + ``` + + ## Next Steps + + - Customize `__main__.py` to add or configure additional resources. + - Explore the Pulumi AWS SDK: https://www.pulumi.com/registry/packages/aws/ + - Break your infrastructure into modules for better organization. + - Integrate into CI/CD pipelines for automated deployments. + + ## Help and Community + + If you have questions or need assistance: + - Pulumi Documentation: https://www.pulumi.com/docs/ + - Community Slack: https://slack.pulumi.com/ + - GitHub Issues: https://github.com/pulumi/pulumi/issues + + Contributions and feedback are always welcome! \ No newline at end of file diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 0000000000..faff050eda --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,61 @@ +import pulumi +import pulumi_aws as aws + +vpc = aws.ec2.get_vpc(default=True) + +subnets = aws.ec2.get_subnets(filters=[ + {"name": "vpc-id", "values": [vpc.id]} +]) + +sg = aws.ec2.SecurityGroup("lab-sg", + vpc_id=vpc.id, + ingress=[ + { + "protocol": "tcp", + "from_port": 22, + "to_port": 22, + "cidr_blocks": ["0.0.0.0/0"], + }, + { + "protocol": "tcp", + "from_port": 80, + "to_port": 80, + "cidr_blocks": ["0.0.0.0/0"], + }, + { + "protocol": "tcp", + "from_port": 5000, + "to_port": 5000, + "cidr_blocks": ["0.0.0.0/0"], + }, + ], + egress=[ + { + "protocol": "-1", + "from_port": 0, + "to_port": 0, + "cidr_blocks": ["0.0.0.0/0"], + } + ], +) + +ami = aws.ec2.get_ami( + most_recent=True, + owners=["099720109477"], + filters=[ + { + "name": "name", + "values": ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"], + } + ], +) + +instance = aws.ec2.Instance("lab-vm", + instance_type="t2.micro", + ami=ami.id, + subnet_id=subnets.ids[0], + vpc_security_group_ids=[sg.id], + key_name="vockey", +) + +pulumi.export("public_ip", instance.public_ip) \ No newline at end of file diff --git a/pulumi/poetry.lock b/pulumi/poetry.lock new file mode 100644 index 0000000000..6968f81115 --- /dev/null +++ b/pulumi/poetry.lock @@ -0,0 +1,382 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "arpeggio" +version = "2.0.3" +description = "Packrat parser interpreter" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Arpeggio-2.0.3-py2.py3-none-any.whl", hash = "sha256:9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f"}, + {file = "Arpeggio-2.0.3.tar.gz", hash = "sha256:9e85ad35cfc6c938676817c7ae9a1000a7c72a34c71db0c687136c460d12b85e"}, +] + +[package.extras] +dev = ["mike", "mkdocs", "twine", "wheel"] +test = ["coverage", "coveralls", "flake8", "pytest"] + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64"}, + {file = "debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642"}, + {file = "debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2"}, + {file = "debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893"}, + {file = "debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b"}, + {file = "debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344"}, + {file = "debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec"}, + {file = "debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb"}, + {file = "debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d"}, + {file = "debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b"}, + {file = "debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390"}, + {file = "debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3"}, + {file = "debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a"}, + {file = "debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf"}, + {file = "debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393"}, + {file = "debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7"}, + {file = "debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173"}, + {file = "debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad"}, + {file = "debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f"}, + {file = "debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be"}, + {file = "debugpy-1.8.20-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:b773eb026a043e4d9c76265742bc846f2f347da7e27edf7fe97716ea19d6bfc5"}, + {file = "debugpy-1.8.20-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:20d6e64ea177ab6732bffd3ce8fc6fb8879c60484ce14c3b3fe183b1761459ca"}, + {file = "debugpy-1.8.20-cp38-cp38-win32.whl", hash = "sha256:0dfd9adb4b3c7005e9c33df430bcdd4e4ebba70be533e0066e3a34d210041b66"}, + {file = "debugpy-1.8.20-cp38-cp38-win_amd64.whl", hash = "sha256:60f89411a6c6afb89f18e72e9091c3dfbcfe3edc1066b2043a1f80a3bbb3e11f"}, + {file = "debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6"}, + {file = "debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5"}, + {file = "debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee"}, + {file = "debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8"}, + {file = "debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7"}, + {file = "debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33"}, +] + +[[package]] +name = "dill" +version = "0.4.1" +description = "serialize all of Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "grpcio" +version = "1.78.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5"}, + {file = "grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813"}, + {file = "grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de"}, + {file = "grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf"}, + {file = "grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6"}, + {file = "grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074"}, + {file = "grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856"}, + {file = "grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558"}, + {file = "grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97"}, + {file = "grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce"}, + {file = "grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68"}, + {file = "grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e"}, + {file = "grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b"}, + {file = "grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20"}, + {file = "grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670"}, + {file = "grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4"}, + {file = "grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e"}, + {file = "grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65"}, + {file = "grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c"}, + {file = "grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb"}, + {file = "grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5"}, + {file = "grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c"}, + {file = "grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525"}, + {file = "grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df"}, + {file = "grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5"}, +] + +[package.dependencies] +typing-extensions = ">=4.12,<5.0" + +[package.extras] +protobuf = ["grpcio-tools (>=1.78.0)"] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "parver" +version = "0.5" +description = "Parse and manipulate version numbers." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "parver-0.5-py3-none-any.whl", hash = "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2"}, + {file = "parver-0.5.tar.gz", hash = "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777"}, +] + +[package.dependencies] +arpeggio = ">=1.7" +attrs = ">=19.2" + +[package.extras] +docs = ["furo", "sphinx"] +docstest = ["doc8"] +pep8test = ["flake8", "pep8-naming"] +test = ["hypothesis", "pretend", "pytest"] + +[[package]] +name = "pip" +version = "26.0.1" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, + {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, + {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, + {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"}, + {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"}, + {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"}, + {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"}, + {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, +] + +[[package]] +name = "pulumi" +version = "3.224.0" +description = "Pulumi's Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pulumi-3.224.0-py3-none-any.whl", hash = "sha256:2045987052c39740dcdb13d8845868106aebdc91792aa25ef82b06797ebf0a8e"}, +] + +[package.dependencies] +debugpy = ">=1.8.7,<1.9.0" +dill = ">=0.4,<1.0" +grpcio = [ + {version = ">=1.68.1,<2", markers = "python_version < \"3.14\""}, + {version = ">=1.75.1,<2", markers = "python_version >= \"3.14\""}, +] +packaging = ">=26.0" +pip = ">=24.3.1" +protobuf = ">=3.20.3,<7" +pyyaml = ">=6.0,<7.0" +semver = ">=3.0,<4.0" + +[[package]] +name = "pulumi-aws" +version = "7.20.0" +description = "A Pulumi package for creating and managing Amazon Web Services (AWS) cloud resources." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pulumi_aws-7.20.0-py3-none-any.whl", hash = "sha256:ec366ca00041abbda7f95a70fec34c6a0b12b09bee40154075914972ed4d410b"}, + {file = "pulumi_aws-7.20.0.tar.gz", hash = "sha256:58e066d415dd72821fa8eb2f65d81a43d782e4a89783f7643b4371afe071763c"}, +] + +[package.dependencies] +parver = ">=0.2.1" +pulumi = ">=3.165.0,<4.0.0" +semver = ">=2.8.1" +typing-extensions = {version = ">=4.11,<5", markers = "python_version < \"3.11\""} + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "semver" +version = "3.0.4" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "805f979d8c510a2dd2bb4cd7514c09ce877930d1fe70c2a36c8177fbaca3ff7d" diff --git a/pulumi/pyproject.toml b/pulumi/pyproject.toml new file mode 100644 index 0000000000..386ae508ea --- /dev/null +++ b/pulumi/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "lab04" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool] +[tool.poetry] +package-mode = false +[tool.poetry.dependencies] +pulumi = ">=3.0.0,<4.0.0" +pulumi-aws = ">=7.0.0,<8.0.0" +python = "^3.10" diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..04be6d5fa4 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,24 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.33.0" + hashes = [ + "h1:wNrviem6bg9fq1bYvtGqH9QWO6iWbM1bBRLSqFJWqWM=", + "zh:207f3f9db05c11429a241b84deeecfbd4caa941792c2c49b09c8c85cd59474dd", + "zh:25c36ad1f4617aeb23f8cd18efc7856127db721f6cf3e2e474236af019ce9ad1", + "zh:2685af1f3eb9abfce3168777463eaaad9dba5687f9f84d8bb579cb878bcfa18b", + "zh:57e28457952cf43923533af0a9bb322164be5fc3d66c080b5c59ee81950e9ef6", + "zh:5b6cd074f9e3a8d91841e739d259fe11f181e69c4019e3321231b35c0dde08c8", + "zh:6e3251500cebf1effb9c68d49041268ea270f75b122b94d261af231a8ebfa981", + "zh:7eee56f52f4b94637793508f3e83f68855f5f884a77aed2bd2fe77480c89e33d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9e228c92db1b9e36a0f899d6ab7446e6b8cf3183112d4f1c1613d6827a0ed3d6", + "zh:b34a84475e91715352ed1119b21e51a81d8ad12e93c86d4e78cd2d315d02dcab", + "zh:cdcc05a423a78a9b2c4e2844c58ecbf2ce6a3117cab353fa05197782d6f76667", + "zh:d0f5f6b1399cfa1b64f3e824bee9e39ff15d5a540ff197e9bfc157fe354a8426", + "zh:d9525dbb53468dee6b8e6d15669d25957e9872bf1cd386231dff93c8c659f1d7", + "zh:ed37db2df08b961a7fc390164273e602767ca6922f57560daa9678a2e1315fd0", + "zh:f6adc66b86e12041a2d3739600e6a153a1f5752dd363db11469f6f4dbd090080", + ] +} diff --git a/terraform/docs/LAB04.md b/terraform/docs/LAB04.md new file mode 100644 index 0000000000..465105dc7c --- /dev/null +++ b/terraform/docs/LAB04.md @@ -0,0 +1,281 @@ +## 1. Cloud Provider & Infrastructure + +- **Cloud provider**: AWS + - Chosen because it is the default provider for the course, we have a 50 dollars limit from university accounts, and provides a simple way to launch a single EC2 VM in the default VPC. + +- **Instance type / size**: `t2.micro` + +- **Region / zone**: `us-east-1` + - Default region for the lab environment, widely supported with many Ubuntu AMIs, and commonly used in examples and documentation. + +- **Estimated cost**: + - One `t2.micro` instance plus a small amount of EBS and networking. This lab workload is expected to cost **$1.5** for the whole lab session for 4 hours. + +- **Resources created (Terraform / AWS)**: + - Data sources: + - `aws_vpc.default` – default VPC in `us-east-1`. + - `aws_subnets.default` – subnets in the default VPC. + - `aws_ami.ubuntu` – latest Ubuntu 22.04 AMD64 server AMI. + - Managed resources: + - `aws_security_group.vm_sg` – security group allowing: + - SSH (`tcp/22`) from `0.0.0.0/0` (lab only, not production‑safe). + - HTTP (`tcp/80`) from `0.0.0.0/0`. + - App port (`tcp/5000`) from `0.0.0.0/0`. + - `aws_instance.vm` – Ubuntu EC2 instance: + - Type `t2.micro`. + - Uses the Ubuntu AMI from `aws_ami.ubuntu`. + - Placed in the first subnet from `aws_subnets.default`. + - Associated with the `vm_sg` security group. + - Uses an existing EC2 key pair (`vockey`) for SSH. + - Output: + - `public_ip` – the instance’s public IP address. + +- **Resources created (Pulumi / AWS)**: + - Data lookups: + - `aws.ec2.get_vpc(default=True)` – default VPC. + - `aws.ec2.get_subnets(...)` – subnets in that VPC. + - `aws.ec2.get_ami(...)` – latest Ubuntu AMI. + - Managed resources: + - `aws.ec2.SecurityGroup("lab-sg", ...)` – same ingress/egress rules as Terraform. + - `aws.ec2.Instance("lab-vm", ...)` – same instance shape (`t2.micro`), subnet, AMI and key pair. + - `pulumi.export("public_ip", instance.public_ip)` – exports the VM public IP. + +--- + +## 2. Terraform Implementation + +- **Terraform version used**: + +```bash +$ terraform --version +Terraform v1.5.7 +on darwin_arm64 ++ provider registry.terraform.io/hashicorp/aws v6.33.0 +``` + +![Terraform version](./terraform_version.png) + +- **Project structure**: + - `terraform/main.tf` – provider, data sources, security group, EC2 instance. + - `terraform/outputs.tf` – output for `public_ip`. + - `terraform/vm_ssh.pem` – local private key file for SSH (not managed by Terraform, used only for `ssh -i`). + +- **Key configuration decisions**: + - **Provider & region**: `provider "aws" { region = "us-east-1" }` to match the course environment. + - **Networking**: Use the **default VPC and first subnet** via data sources instead of creating custom networking (simpler for a single‑VM lab). + - **AMI selection**: Use a data source that fetches the latest official Ubuntu AMI owned by Canonical. + - **Security group**: Open `22`, `80`, and `5000` to `0.0.0.0/0` for convenience in the lab; in production this would be much more restricted. + - **Key pair**: Use an existing AWS key pair (`vockey`) for the instance and a matching local `.pem` file for SSH. + +- **Challenges encountered**: + - Initial `terraform apply` failed with `InvalidKeyPair.NotFound: The key pair 'vm_ssh' does not exist`. This happened because the instance’s `key_name` referenced a key pair that had not been created in AWS. + - Fixed by aligning the configuration to use an existing key pair (`vockey`) and by ensuring that the local private key file had the correct permissions (`chmod 400 vm_ssh.pem`) when using SSH. + - Also iterated on security group rules (started with a more restricted IP on port 22, then opened to `0.0.0.0/0` for simplicity). + +- **Key terminal output – `terraform init`**: + +```bash +$ terraform init + +Initializing the backend... + +Initializing provider plugins... +- Reusing previous version of hashicorp/aws from the dependency lock file +- Using previously-installed hashicorp/aws v6.33.0 + +Terraform has been successfully initialized! +``` + + +- **Key terminal output – first `terraform apply` (failed due to missing key pair)**: + +![Terraform apply](./terraform_created_vm.png) + +```bash +$ terraform apply +... + # aws_instance.vm will be created + + resource "aws_instance" "vm" { + + instance_type = "t2.micro" + + key_name = "vm_ssh" + ... + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +Do you want to perform these actions? + Enter a value: yes + +aws_instance.vm: Creating... + +Error: creating EC2 Instance: ... api error InvalidKeyPair.NotFound: The key pair 'vm_ssh' does not exist +``` + +- **Key terminal output – successful `terraform apply`**: + +```bash +$ terraform apply +... + # aws_instance.vm will be created + + resource "aws_instance" "vm" { + + instance_type = "t2.micro" + + key_name = "vockey" + ... + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +Do you want to perform these actions? + Enter a value: yes + +aws_instance.vm: Creating... +aws_instance.vm: Creation complete after 17s [id=i-0cbc3d4b0af281d63] + +Apply complete! Resources: 1 added, 0 changed, 0 destroyed. + +Outputs: + +public_ip = "" +``` + +- **Key terminal output – `terraform plan` after initial apply (security group change)**: +![Terraform plan](./terraform_plan_1.png) +![Terraform plan](./terraform_plan_2.png) +```bash +$ terraform plan +... +Terraform will perform the following actions: + + # aws_security_group.vm_sg will be updated in-place + ~ resource "aws_security_group" "vm_sg" { + ~ ingress = [ + - ... previous rules ... + + ... updated rules for ports 22, 80, 5000 from 0.0.0.0/0 ... + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. +``` + +- **Key terminal output – SSH connection to Terraform VM**: +![Terraform plan](./login_ssh_terraform.png) +```bash +$ ssh -i vm_ssh.pem ubuntu@ +The authenticity of host ' ()' can't be established. +... +Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1046-aws x86_64) +... +ubuntu@ip-172-31-31-39:~$ exit +Connection to closed. +``` + + +![Terraform created VM](./terraform_created_vmm.png) +--- + +## 3. Pulumi Implementation + +- **Pulumi version and language**: + - Language: **Python**. + - Tooling: + - Pulumi Python SDK: `pulumi 3.224.0`. + - AWS provider SDK: `pulumi-aws 7.20.0`. + - Python version: `3.14.2` (via Poetry virtualenv). + - CLI used via `pulumi preview` / `pulumi up` in the `pulumi` directory. + +- **Project structure**: + - `pulumi/__main__.py` – main Pulumi program defining the stack, security group, and EC2 instance, and exporting `public_ip`. + - Pulumi configuration & state: + - Stack name: `lab04-dev`. + - Organization: `sofiakulagina-org`. + - Region: `us-east-1` (configured via Pulumi config). + +- **How the Pulumi code differs from Terraform**: + - Terraform uses **HCL** (a declarative DSL), while Pulumi uses **general-purpose Python code**. + - Data sources in Terraform (`data "aws_vpc"`, `data "aws_subnets"`, `data "aws_ami"`) become **Python function calls** (`aws.ec2.get_vpc`, `aws.ec2.get_subnets`, `aws.ec2.get_ami`). + - Resources are defined as **Python objects** (`aws.ec2.SecurityGroup(...)`, `aws.ec2.Instance(...)`) instead of HCL blocks. + - Outputs are exported via `pulumi.export("public_ip", instance.public_ip)` instead of an `output` block. + - Because it is Python, it can use normal control flow, loops, and functions to structure infrastructure logic. + +- **Advantages discovered with Pulumi**: + - **Single language**: You can stay in Python for both application and infrastructure code, which makes reuse and abstraction easier. + - **Rich IDE support**: Autocomplete, type hints, and refactoring tools from the Python ecosystem help catch mistakes early. + - **Familiar control flow**: Using regular `if` statements, lists, and functions is more natural than some of Terraform’s declarative constructs. + - **Consistent preview / up experience** similar to Terraform’s plan / apply, but with additional integration into the Pulumi web UI. + +- **Challenges encountered**: + - Hit the **same key pair error** as with Terraform: `InvalidKeyPair.NotFound: The key pair 'vm_ssh' does not exist`, because the Pulumi program also initially referenced a non‑existent key pair. + - Needed to align the Pulumi configuration with an actual AWS key pair and ensure the local `.pem` file permissions were correct (`chmod 400 vm_ssh.pem`). + - First `pulumi up` was intentionally cancelled (`Do you want to perform this update? no`) while reviewing the plan, then re-run to actually create resources. + +- **Key terminal output – `pulumi preview`**: +![pulumni previw](./pulumni_preview.png) +```bash +$ pulumi preview +Previewing update (dev) + + Type Name Plan + + pulumi:pulumi:Stack lab04-dev create + + ├─ aws:ec2:SecurityGroup lab-sg create + + └─ aws:ec2:Instance lab-vm create + +Outputs: + public_ip: [unknown] + +Resources: + + 3 to create +``` + +- **Key terminal output – `pulumi up`**: +![Pulumi up](./pulumni_up.png) + +```bash +$ pulumi up +Previewing update (dev) +... +Do you want to perform this update? yes +Updating (dev) + + Type Name Status + pulumi:pulumi:Stack lab04-dev + + └─ aws:ec2:Instance lab-vm created (16s) + +Outputs: + + public_ip: "" + +Resources: + + 1 created + 2 unchanged +``` + +- **Key terminal output – SSH connection to Pulumi VM**: +![Pulumi up](./pulumni_ssh_login.png) +```bash +$ ssh -i vm_ssh.pem ubuntu@ +Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1046-aws x86_64) +... +ubuntu@ip-172-31-20-68:~$ +``` +![Pulumi up](./pulumni_created_vm.png) +--- + +## 4. Terraform vs Pulumi Comparison + +- **Ease of learning**: + Terraform felt easier to learn initially because most introductory infrastructure‑as‑code material and examples use Terraform and its HCL syntax. The mental model of “write HCL → run `terraform plan` → run `terraform apply`” is very straightforward. Pulumi adds some extra concepts (stacks, organizations, SDKs, language runtimes), which is powerful but slightly more to absorb at the beginning. Overall, for a brand‑new IaC user, Terraform had a gentler learning curve in this lab. + +- **Code readability**: + For simple resources, Terraform’s HCL is very concise and easy to scan, especially when describing static infrastructure like one EC2 instance and one security group. As the configuration grows, being able to use Python in Pulumi may become more readable because you can refactor into functions, classes, and modules. In this lab, both were readable, but Terraform felt more compact while Pulumi felt more expressive. + +- **Debugging experience**: + Both tools surfaced clear AWS error messages (for example, the `InvalidKeyPair.NotFound` error). Terraform’s CLI output is very linear and easy to follow when something fails during `apply`. Pulumi’s output is similar but also ties into a web UI that can provide additional context about failed resources. In this exercise, debugging the key pair issue was about equally easy in both tools because the underlying AWS error was explicit. + +- **Documentation and examples**: + Terraform has a huge amount of documentation and community examples for AWS, which makes it easy to find snippets for common patterns (VPCs, security groups, EC2 instances, etc.). Pulumi also has good docs, but examples are spread between languages and sometimes require more navigation to find exactly the Python version of what you need. For this lab, Terraform’s docs felt slightly more direct and aligned with common AWS tutorials. + +- **Use cases / when to use which**: + Terraform is a great fit when teams want a **cloud‑agnostic, declarative DSL** with a massive ecosystem and when most infrastructure can be described as relatively static configuration. Pulumi shines when infrastructure needs to be **deeply integrated with application code**, benefit from reuse and abstraction in a general‑purpose language, or when engineers are already very comfortable in languages like Python or TypeScript. After this lab, Terraform feels ideal for straightforward infrastructure modules, while Pulumi feels attractive for larger, more programmatic environments where shared libraries and higher‑level abstractions are valuable. + +## 5. Lab5 Preparation & Cleanup + +i am keeping my vms bot terraform and pulumni created running. The status shown in picture higher. \ No newline at end of file diff --git a/terraform/docs/login_ssh_terraform.png b/terraform/docs/login_ssh_terraform.png new file mode 100644 index 0000000000..1c6429c0f2 Binary files /dev/null and b/terraform/docs/login_ssh_terraform.png differ diff --git a/terraform/docs/pulumni_created_vm.png b/terraform/docs/pulumni_created_vm.png new file mode 100644 index 0000000000..26110fb2e4 Binary files /dev/null and b/terraform/docs/pulumni_created_vm.png differ diff --git a/terraform/docs/pulumni_preview.png b/terraform/docs/pulumni_preview.png new file mode 100644 index 0000000000..37e7e08776 Binary files /dev/null and b/terraform/docs/pulumni_preview.png differ diff --git a/terraform/docs/pulumni_ssh_login.png b/terraform/docs/pulumni_ssh_login.png new file mode 100644 index 0000000000..6cf53b1a2f Binary files /dev/null and b/terraform/docs/pulumni_ssh_login.png differ diff --git a/terraform/docs/pulumni_up.png b/terraform/docs/pulumni_up.png new file mode 100644 index 0000000000..792e743df0 Binary files /dev/null and b/terraform/docs/pulumni_up.png differ diff --git a/terraform/docs/terraform_created_vm.png b/terraform/docs/terraform_created_vm.png new file mode 100644 index 0000000000..135b1e78bb Binary files /dev/null and b/terraform/docs/terraform_created_vm.png differ diff --git a/terraform/docs/terraform_created_vmm.png b/terraform/docs/terraform_created_vmm.png new file mode 100644 index 0000000000..0d84808dbb Binary files /dev/null and b/terraform/docs/terraform_created_vmm.png differ diff --git a/terraform/docs/terraform_plan_1.png b/terraform/docs/terraform_plan_1.png new file mode 100644 index 0000000000..1dacc72f9e Binary files /dev/null and b/terraform/docs/terraform_plan_1.png differ diff --git a/terraform/docs/terraform_plan_2.png b/terraform/docs/terraform_plan_2.png new file mode 100644 index 0000000000..392993f488 Binary files /dev/null and b/terraform/docs/terraform_plan_2.png differ diff --git a/terraform/docs/terraform_version.png b/terraform/docs/terraform_version.png new file mode 100644 index 0000000000..3f1d675579 Binary files /dev/null and b/terraform/docs/terraform_version.png differ diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..9b4deae158 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,71 @@ +provider "aws" { + region = "us-east-1" +} + +data "aws_vpc" "default" { + default = true +} + +data "aws_subnets" "default" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"] + } +} + +resource "aws_security_group" "vm_sg" { + name = "lab-sg" + vpc_id = data.aws_vpc.default.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 5000 + to_port = 5000 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_instance" "vm" { + ami = data.aws_ami.ubuntu.id + instance_type = "t2.micro" + + key_name = "vockey" + vpc_security_group_ids = [aws_security_group.vm_sg.id] + + subnet_id = data.aws_subnets.default.ids[0] + + tags = { + Name = "lab-vm" + } +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..d33b41b3a4 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = aws_instance.vm.public_ip +} \ No newline at end of file diff --git a/terraform/terraform.tfstate b/terraform/terraform.tfstate new file mode 100644 index 0000000000..0696820f85 --- /dev/null +++ b/terraform/terraform.tfstate @@ -0,0 +1,421 @@ +{ + "version": 4, + "terraform_version": "1.5.7", + "serial": 9, + "lineage": "a33b89e1-fc2e-7e10-f86b-8950af60d9df", + "outputs": { + "public_ip": { + "value": "54.160.197.99", + "type": "string" + } + }, + "resources": [ + { + "mode": "data", + "type": "aws_ami", + "name": "ubuntu", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-1::image/ami-04680790a315cd58d", + "block_device_mappings": [ + { + "device_name": "/dev/sda1", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "0", + "snapshot_id": "snap-0b2c426db6335afae", + "throughput": "0", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp2" + }, + "no_device": "", + "virtual_name": "" + }, + { + "device_name": "/dev/sdb", + "ebs": {}, + "no_device": "", + "virtual_name": "ephemeral0" + }, + { + "device_name": "/dev/sdc", + "ebs": {}, + "no_device": "", + "virtual_name": "ephemeral1" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-02-18T06:50:21.000Z", + "deprecation_time": "2028-02-18T06:50:21.000Z", + "description": "Canonical, Ubuntu, 22.04, amd64 jammy image", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "name", + "values": [ + "ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*" + ] + } + ], + "hypervisor": "xen", + "id": "ami-04680790a315cd58d", + "image_id": "ami-04680790a315cd58d", + "image_location": "amazon/ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20260218", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": true, + "name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20260218", + "name_regex": null, + "owner_id": "099720109477", + "owners": [ + "099720109477" + ], + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-1", + "root_device_name": "/dev/sda1", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0b2c426db6335afae", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "data", + "type": "aws_subnets", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "vpc-04a16760854a86a18" + ] + } + ], + "id": "us-east-1", + "ids": [ + "subnet-0d3aea8ba44b3b025", + "subnet-013e5fc5136781d65", + "subnet-0ed6021802d3db3b4", + "subnet-04be12f69279721aa", + "subnet-056d0abdc89185254", + "subnet-01ea093b9ea45e0c9" + ], + "region": "us-east-1", + "tags": null, + "timeouts": null + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "data", + "type": "aws_vpc", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-1:247022161270:vpc/vpc-04a16760854a86a18", + "cidr_block": "172.31.0.0/16", + "cidr_block_associations": [ + { + "association_id": "vpc-cidr-assoc-079cb0ac4a72e5157", + "cidr_block": "172.31.0.0/16", + "state": "associated" + } + ], + "default": true, + "dhcp_options_id": "dopt-07f036ccf0c4985a7", + "enable_dns_hostnames": true, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "filter": null, + "id": "vpc-04a16760854a86a18", + "instance_tenancy": "default", + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "main_route_table_id": "rtb-075eba5c119626e97", + "owner_id": "247022161270", + "region": "us-east-1", + "state": null, + "tags": {}, + "timeouts": null + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "managed", + "type": "aws_instance", + "name": "vm", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-04680790a315cd58d", + "arn": "arn:aws:ec2:us-east-1:247022161270:instance/i-0cbc3d4b0af281d63", + "associate_public_ip_address": true, + "availability_zone": "us-east-1b", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0cbc3d4b0af281d63", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "vockey", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0e74d1b48a8a1b061" + } + ], + "primary_network_interface_id": "eni-0e74d1b48a8a1b061", + "private_dns": "ip-172-31-31-39.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "172.31.31.39", + "public_dns": "ec2-54-160-197-99.compute-1.amazonaws.com", + "public_ip": "54.160.197.99", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/sda1", + "encrypted": false, + "iops": 100, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-0af129b4c56404977", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [ + "lab-sg" + ], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-0d3aea8ba44b3b025", + "tags": { + "Name": "lab-vm" + }, + "tags_all": { + "Name": "lab-vm" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-04417f33eae9b3dee" + ] + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.vm_sg", + "data.aws_ami.ubuntu", + "data.aws_subnets.default", + "data.aws_vpc.default" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "vm_sg", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-1:247022161270:security-group/sg-04417f33eae9b3dee", + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 0, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "-1", + "security_groups": [], + "self": false, + "to_port": 0 + } + ], + "id": "sg-04417f33eae9b3dee", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 22, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 22 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 5000, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 5000 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": null, + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 80 + } + ], + "name": "lab-sg", + "name_prefix": "", + "owner_id": "247022161270", + "region": "us-east-1", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-04a16760854a86a18" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "data.aws_vpc.default" + ] + } + ] + } + ], + "check_results": null +} diff --git a/terraform/terraform.tfstate.backup b/terraform/terraform.tfstate.backup new file mode 100644 index 0000000000..aa7f098ab4 --- /dev/null +++ b/terraform/terraform.tfstate.backup @@ -0,0 +1,420 @@ +{ + "version": 4, + "terraform_version": "1.5.7", + "serial": 7, + "lineage": "a33b89e1-fc2e-7e10-f86b-8950af60d9df", + "outputs": { + "public_ip": { + "value": "54.160.197.99", + "type": "string" + } + }, + "resources": [ + { + "mode": "data", + "type": "aws_ami", + "name": "ubuntu", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-1::image/ami-04680790a315cd58d", + "block_device_mappings": [ + { + "device_name": "/dev/sda1", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "0", + "snapshot_id": "snap-0b2c426db6335afae", + "throughput": "0", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp2" + }, + "no_device": "", + "virtual_name": "" + }, + { + "device_name": "/dev/sdb", + "ebs": {}, + "no_device": "", + "virtual_name": "ephemeral0" + }, + { + "device_name": "/dev/sdc", + "ebs": {}, + "no_device": "", + "virtual_name": "ephemeral1" + } + ], + "boot_mode": "uefi-preferred", + "creation_date": "2026-02-18T06:50:21.000Z", + "deprecation_time": "2028-02-18T06:50:21.000Z", + "description": "Canonical, Ubuntu, 22.04, amd64 jammy image", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "name", + "values": [ + "ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*" + ] + } + ], + "hypervisor": "xen", + "id": "ami-04680790a315cd58d", + "image_id": "ami-04680790a315cd58d", + "image_location": "amazon/ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20260218", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": true, + "name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20260218", + "name_regex": null, + "owner_id": "099720109477", + "owners": [ + "099720109477" + ], + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-1", + "root_device_name": "/dev/sda1", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0b2c426db6335afae", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "data", + "type": "aws_subnets", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "vpc-04a16760854a86a18" + ] + } + ], + "id": "us-east-1", + "ids": [ + "subnet-0d3aea8ba44b3b025", + "subnet-013e5fc5136781d65", + "subnet-0ed6021802d3db3b4", + "subnet-04be12f69279721aa", + "subnet-056d0abdc89185254", + "subnet-01ea093b9ea45e0c9" + ], + "region": "us-east-1", + "tags": null, + "timeouts": null + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "data", + "type": "aws_vpc", + "name": "default", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:ec2:us-east-1:247022161270:vpc/vpc-04a16760854a86a18", + "cidr_block": "172.31.0.0/16", + "cidr_block_associations": [ + { + "association_id": "vpc-cidr-assoc-079cb0ac4a72e5157", + "cidr_block": "172.31.0.0/16", + "state": "associated" + } + ], + "default": true, + "dhcp_options_id": "dopt-07f036ccf0c4985a7", + "enable_dns_hostnames": true, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "filter": null, + "id": "vpc-04a16760854a86a18", + "instance_tenancy": "default", + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "main_route_table_id": "rtb-075eba5c119626e97", + "owner_id": "247022161270", + "region": "us-east-1", + "state": null, + "tags": {}, + "timeouts": null + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "managed", + "type": "aws_instance", + "name": "vm", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 2, + "attributes": { + "ami": "ami-04680790a315cd58d", + "arn": "arn:aws:ec2:us-east-1:247022161270:instance/i-0cbc3d4b0af281d63", + "associate_public_ip_address": true, + "availability_zone": "us-east-1b", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0cbc3d4b0af281d63", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "vockey", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0e74d1b48a8a1b061" + } + ], + "primary_network_interface_id": "eni-0e74d1b48a8a1b061", + "private_dns": "ip-172-31-31-39.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "172.31.31.39", + "public_dns": "ec2-54-160-197-99.compute-1.amazonaws.com", + "public_ip": "54.160.197.99", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/sda1", + "encrypted": false, + "iops": 100, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-0af129b4c56404977", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [ + "lab-sg" + ], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-0d3aea8ba44b3b025", + "tags": { + "Name": "lab-vm" + }, + "tags_all": { + "Name": "lab-vm" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-04417f33eae9b3dee" + ] + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", + "dependencies": [ + "aws_security_group.vm_sg", + "data.aws_ami.ubuntu", + "data.aws_subnets.default" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_security_group", + "name": "vm_sg", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:ec2:us-east-1:247022161270:security-group/sg-04417f33eae9b3dee", + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 0, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "-1", + "security_groups": [], + "self": false, + "to_port": 0 + } + ], + "id": "sg-04417f33eae9b3dee", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 5000, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 5000 + }, + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 80 + }, + { + "cidr_blocks": [ + "46.8.236.179/32" + ], + "description": "", + "from_port": 22, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 22 + } + ], + "name": "lab-sg", + "name_prefix": "", + "owner_id": "247022161270", + "region": "us-east-1", + "revoke_rules_on_delete": false, + "tags": {}, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-04a16760854a86a18" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6OTAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=", + "dependencies": [ + "data.aws_vpc.default" + ] + } + ] + } + ], + "check_results": null +} diff --git a/terraform/vm_ssh.pem b/terraform/vm_ssh.pem new file mode 100644 index 0000000000..e0533b2c18 --- /dev/null +++ b/terraform/vm_ssh.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1YImr3y/fzAgJt3TOyj2ffzelz+Dru26a2X2r83SplYivqxe +lvXT4re4wNtppBZQ7Ahmjhz168dBpaFGXKpgxjxchFHeuXQDO8P+0wDB5a+5jWxV +TL0aV1+RytXKYwJvwIUjDQ/jhBCmHZKK7Ef/T5LF8IxHTqtRdVVcK1ORodx+UJ+M +XTW4e7lPjkhMLfJPOefFCC8qzp2Itzp6t/3WLfxRcUvW3bknvbGOi/j7l6frcW3Y +rciGKO9y0XIyJZyQ2bgZawg+UufjoN/thH+mK3TX7mipqUzFslxIfliov0NSpkEw +VsIXlgENKGf2D4p1BOu1xSZsIrxh3+VrDaSYpwIDAQABAoIBAQDH/QhVekk5To/f +OArisETq0BCDpbeoA/+JF9IQ7H7y1V3oMjbLumoFREfnRd5XF5wm6tt9JD1p45Nn +HIB2zhSluuaB/tnb5oIc2kJ3zI7SoV3xzVa88aiZzOgYEVdolGCu+UZStt5K1vly +W9CK8AmCtJ156zvjwW8ks5cI8ldIPzPL6t+UnZAhh27c7RyAog/GnJvA5W2n24RZ +9PNdFjpcfKA8I/oTl2+leRM867u/0YA2nacjag9MCVGsvQ78hGh0RVyKhOa2v6UQ +35YDF4mS/+9m2T9W6yIbfj62jzL9mE2fMgBAdyBUCs1dcVpv9gfk2N9VdUs72SbT +CEgrGjT5AoGBAP+iDpB6zGRZn29LLjUHmPaNqK96DqlGLqXr3LTMRnNBNOGN6js9 +T2H/S710usYJTv2PErnSQXGHEhsOzSnyP3kCFTIV8w04dEhNGMdtwtvuFqVGCSpj +E00iqtuHVY/WvoGvVokteUR8JdKa6Xa3Xb1EuYLr62xzWQZRYMYI2/07AoGBANXQ +nR8ZqrYK0xFGE07LcXg7B85Kpr6WrZSu8ddYDq5B/GaMPAD1HhCBI0CQ7nIpAkiV +MbCjHZyVHiMtXwTEH9clbM/asSihfMcmgLjKrYptovsE2aBxo57oj1dZ6hkzeEEo +wtzUo5hu2qjtEtahg+GN0w62OyItQOAGPedQ4YuFAoGBAL3vdQ7YjsEI7jNNEtoH ++RlIC8vFztrq8IILi4MyPC4qhtYIEb9OQIs5mtb7wkKea/hts6YpmMJ3b3Z3Ou0t +twd8Cq+aK2rpRUzrK7wRy7SJZH4MWYcK8i5f0b8nAOXM0uOcmZl3FiFSdO/Udh7l +A1s7kvK5w8RiuAS/tj2uG3Y9AoGAH25VSpcZUz1wHZ/xgqnBem0jDp5GuMmppqu4 +/XECZ3dnxSNkRrfBvadeKUWk579DiKv17ANufPAmyAEJcMfr7lKgFkYNuO1UTGpK +jd+tdyX6yW/DFiTeaqPBPpQHJ8E7MkjtrEzN58CUADUpicegfW290vHS64k/ei8w +tmpbXYkCgYEAnz5b+D9vUtxXs965N+l2Y7anDEaTVVYr9MTW9U6A8Ve89skgiR+G +E0YnBIuwcuDv6xXZCd2qwVcLcQKRf2gLkoXwZkB7pCvcH3uYu0JX+2s2rsNRMemG +tTQdGOiPx2YE6xalhvyjf3ytERxzk+AdfWSYiCQCGdyUNeJeZBhSius= +-----END RSA PRIVATE KEY----- \ No newline at end of file