From da62d1ef9dabf993fc249e4681d532d71a294b20 Mon Sep 17 00:00:00 2001 From: AliyaSag <123297425+AliyaSag@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:32:36 +0300 Subject: [PATCH 1/9] Lab01-AliyaSagdieva (#1) * implement lab01 devops info service with flask * corrected the layout * corrected the layout --- app_python/.gitignore | 4 + app_python/README.md | 62 ++++++++++ app_python/app.py | 81 +++++++++++++ app_python/docs/LAB01.md | 108 ++++++++++++++++++ .../docs/screenshots/01-main-endpoint.jpg | Bin 0 -> 95156 bytes .../docs/screenshots/02-health-check.jpg | Bin 0 -> 21606 bytes .../docs/screenshots/03-formatted-output.jpg | Bin 0 -> 41282 bytes app_python/requirements.txt | 2 + app_python/tests/__init__.py | 0 9 files changed, 257 insertions(+) create mode 100644 app_python/.gitignore create mode 100644 app_python/README.md create mode 100644 app_python/app.py create mode 100644 app_python/docs/LAB01.md create mode 100644 app_python/docs/screenshots/01-main-endpoint.jpg create mode 100644 app_python/docs/screenshots/02-health-check.jpg create mode 100644 app_python/docs/screenshots/03-formatted-output.jpg create mode 100644 app_python/requirements.txt create mode 100644 app_python/tests/__init__.py diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..5880b598a6 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +venv/ +.env +.DS_Store diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..550ddda564 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,62 @@ +## DevOps Info Service + +### Overview + +A simple web application built with **Flask** that provides comprehensive system introspection, runtime information, and health status. This project serves as a foundation for learning DevOps practices including CI/CD, containerization, and monitoring. + +### Prerequisites + +- **Python** 3.10+ +- **pip** (Python package manager) + +### Installation + +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### Running the Application + +The application runs on `0.0.0.0:5000` by default. + +Custom Configuration +You can change the host and port using environment variables. + +```bash +$env:PORT=8080; python app.py +``` + +### API Endpoints + +1. System Information +- URL: GET / +- Description: Returns detailed JSON about the service, system, runtime, and current request. +- Example Response: + +```json +{ + "service": { "name": "devops-info-service", "version": "1.0.0" }, + "system": { "platform": "Windows", "python_version": "3.12.0" }, + "runtime": { "uptime_human": "0 hour, 5 minutes" } +} +``` +2. Health Check +- URL: GET /health +- Description: Lightweight endpoint for liveness/readiness probes. +- Example Response: + +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T16:20:00+00:00", + "uptime_seconds": 300 +} +``` +### Configuration + +| Variable | Description | Default | +| -------- | ------------------------------- | ------- | +| HOST | Interface to bind the server to | 0.0.0.0 | +| PORT | Port number to listen on | 5000 | \ No newline at end of file diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..6e1927145d --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,81 @@ +import os +import platform +import socket +import logging +import time +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Initialize Flask application instance +app = Flask(__name__) + +# Application configuration from environment +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) + +# Logging configuration +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +start_time = time.time() # record startup timestamp for uptime calculation + +def get_uptime(): + """Returns uptime in seconds and hh:mm format.""" + uptime_seconds = int(time.time() - start_time) + hours = uptime_seconds // 3600 + minutes = (uptime_seconds % 3600) // 60 + return { + "uptime_seconds": uptime_seconds, + "uptime_human": f"{hours} hour, {minutes} minutes" + } + +@app.route('/') +def index(): + """Main endpoint returning system info.""" + uptime = get_uptime() + + return jsonify({ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version() + }, + "runtime": { + "uptime_seconds": uptime["uptime_seconds"], + "uptime_human": uptime["uptime_human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC" + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get('User-Agent'), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + }) + +@app.route('/health') +def health(): + """Health check endpoint for K8s probes.""" + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": int(time.time() - start_time) + }) + +if __name__ == '__main__': + logger.info(f"Starting application on {HOST}:{PORT}") + app.run(host=HOST, port=PORT) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..d4cde9f489 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,108 @@ +# Lab 01 - DevOps Info Service: Python Implementation +## Framework Selection +For this project, I selected **Flask**. + +### Comparison + +| Feature | Flask | FastAPI | Django | +|---------|-------|---------|--------| +| **Type** | Microframework | Microframework | Full-stack Framework | +| **Architecture** | Synchronous (WSGI) | Asynchronous (ASGI) | Monolithic (mostly) | +| **Learning Curve** | Low (Easy to start) | Medium (Type hints, async) | High (Complex ORM, structure) | +| **Performance** | Good | Excellent | Good | +| **Use Case** | Simple services, prototyping | High-load APIs, ML models | Complex enterprise apps | + +### Justification +I chose **Flask** because it is lightweight and provides exactly what is needed for a simple system information service without unnecessary overhead. It allows for quick prototyping and has a simple, intuitive syntax for defining routes. While FastAPI offers automatic documentation, Flask is robust enough for this assignment and is an industry standard for many microservices. +## Best Practices Applied +I implemented several Python and DevOps best practices in the application: + +1. **Configuration via Environment Variables** + Instead of hardcoding settings, I use `os.getenv` to load `HOST` and `PORT`. This adheres to the **12-Factor App** methodology, allowing the app to be configured differently in Dev, Test, and Prod environments without changing code. + ```python + HOST = os.getenv('HOST', '0.0.0.0') + PORT = int(os.getenv('PORT', 5000)) + ``` + +2. **Proper Logging** + I used the standard `logging` library instead of `print()` statements. This allows for better log management (levels like INFO, ERROR) and is essential for monitoring in production environments. + ```python + logging.basicConfig(level=logging.INFO, format='%(asctime)s - ...') + ``` + +3. **Clean Code & Documentation** + - Code is organized into logical blocks. + - Logic for uptime calculation is separated into a dedicated function `get_uptime()`. + - Docstrings are added to functions to explain their purpose. + - Variable names are descriptive (`uptime_seconds`, `platform_version`). + +4. **Error Handling** + Specific handlers for `404 Not Found` and `500 Internal Server Error` (implicit in Flask, can be extended) ensure that the client receives valid JSON responses even when things go wrong, rather than raw HTML stack traces. + +5. **Dependency Management** + All dependencies are pinned in `requirements.txt` to ensure reproducibility across different environments. + ```text + Flask==3.1.0 + python-dotenv==1.0.1 + ``` +## API Documentation +## 3. API Documentation + +### Main Endpoint (`GET /`) +Returns comprehensive information about the running service and the host system. + +**Response Example:** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "framework": "Flask" + }, + "system": { + "hostname": "DESKTOP-XYZ", + "platform": "Windows", + "python_version": "3.12.4" + }, + "runtime": { + "uptime_human": "0 hour, 15 minutes", + "timezone": "UTC" + } +} +``` +### Health Check (GET /health) +A lightweight endpoint for container orchestrators (like Kubernetes) to verify the app is alive. +**Response Example:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T16:30:00+00:00", + "uptime_seconds": 900 +} +``` +## Testing Evidence +**Screenshots:** Place in `docs/screenshots/`. + +To verify that the service works as expected, I performed manual testing using both the web browser and command-line tools. + +**Verification Steps:** +1. **Main Endpoint Check:** Opened `http://127.0.0.1:5000/` in the browser to validate the complete JSON structure, ensuring all required sections (`service`, `system`, `runtime`, `request`, and `endpoints`) are present and correct. +2. **Health Check:** Access `http://127.0.0.1:5000/health` to confirm that the endpoint returns the correct `status`, current `timestamp`, and `uptime_seconds`. +3. **Formatted Output:** Used `curl` with `jq` to see formatted output in the terminal. + +## Challenges & Solutions +Working on Windows with PowerShell presented some specific challenges compared to a standard Linux environment. + +**Command Differences (touch, source)** + +**Challenge:** Commands like touch to create files or source to activate the virtual environment are not natively available in PowerShell. + +**Solution:** I learned to use PowerShell equivalents: `New-Item` (or `ni`) for creating files and `.\venv\Scripts\activate` for activating the environment. + +**Curl & JSON Formatting** + +**Challenge:** The `curl` command in PowerShell is often an alias for `Invoke-WebRequest`, which parses HTML differently than the Linux `curl` tool. Also, `jq` is not installed by default on Windows. + +**Solution:** I used the browser to verify the JSON output structure and formatting, which provides a clear view of the data without needing extra CLI tools. +## GitHub Community +Starring repositories is a way to show appreciation to maintainers and helps projects gain visibility/trust in the community. Following other developers allows me to stay updated on their work, discover new tools, and observe coding practices from experienced engineers, which is crucial for professional growth. \ No newline at end of file diff --git a/app_python/docs/screenshots/01-main-endpoint.jpg b/app_python/docs/screenshots/01-main-endpoint.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc90aae0bf26e8a1f64229111b82e5bfa190a646 GIT binary patch literal 95156 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�!KCnI6UK{^LOlliLs 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 literal 0 HcmV?d00001 diff --git a/app_python/docs/screenshots/02-health-check.jpg b/app_python/docs/screenshots/02-health-check.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33b8b9e652e18b24ea7a100f43585ab251537aad GIT binary patch literal 21606 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 V6{Hu<@3a3sIx)6?AN~9A{{XaK1gro6 literal 0 HcmV?d00001 diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..0dc21f3922 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.1.0 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 2be48c0a394a9a7155e3eaef9c3d885809aa27c6 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 4 Feb 2026 00:32:33 +0300 Subject: [PATCH 2/9] lab 2 Done --- app_python/.dockerignore | 33 ++++ app_python/Dockerfile | 24 +++ app_python/README.md | 64 ++++++- app_python/docs/LAB02.md | 392 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 app_python/.dockerignore create mode 100644 app_python/Dockerfile create mode 100644 app_python/docs/LAB02.md diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..5962cda8d5 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,33 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +.venv/ + +.vscode/ +.idea/ +*.swp +*.swo + +.DS_Store +Thumbs.db + +.git/ +.gitignore + +.dockerignore +Dockerfile + +*.log + +*.tmp +*.temp + +tests/ +test/ +.coverage +.pytest_cache/ \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..47dd3858b1 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.13-slim AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +USER appuser + +EXPOSE 5000 + +ENV HOST=0.0.0.0 +ENV PORT=5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md index 550ddda564..54d8ca55fc 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -59,4 +59,66 @@ $env:PORT=8080; python app.py | Variable | Description | Default | | -------- | ------------------------------- | ------- | | HOST | Interface to bind the server to | 0.0.0.0 | -| PORT | Port number to listen on | 5000 | \ No newline at end of file +| PORT | Port number to listen on | 5000 | + +## Docker Containerization + +### Building the Image Locally +```bash +docker build -t : . + +### Running the Container + +```bash +# Run with default port mapping +docker run -p : --name : + +# Run with environment variables +docker run -p : -e PORT= -e HOST= --name : +``` + +### Pulling from Docker Hub + +```bash +# Pull the image from Docker Hub +docker pull /: + +# Run the pulled image +docker run -p : /: +``` + +### Examples + +```bash +# Build locally +docker build -t devops-info-service:latest . + +# Run locally built image +docker run -d -p 5000:5000 --name devops-service devops-info-service:latest + +# Pull from Docker Hub and run +docker pull aliyasag/devops-info-service:latest +docker run -d -p 8080:5000 --name devops-hub aliyasag/devops-info-service:latest +``` + +### Container Management + +```bash +# List running containers +docker ps + +# List all containers +docker ps -a + +# View container logs +docker logs + +# Stop a container +docker stop + +# Remove a container +docker rm + +# Remove an image +docker rmi : +``` \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..27aade835e --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,392 @@ +```markdown +# Lab 02 - Docker Containerization + +## Docker Best Practices Applied + +### 1. Non-root User Implementation +```dockerfile +RUN useradd -m -u 1000 appuser +USER appuser +``` +**Why it matters:** Running containers as non-root user minimizes security risks by following the principle of least privilege. If an attacker compromises the application, they won't have root access to the container or host system, limiting potential damage. + +### 2. Specific Base Image Version +```dockerfile +FROM python:3.13-slim +``` +**Why it matters:** Using a specific version (3.13-slim) ensures reproducible builds across different environments. The `slim` variant reduces image size by removing unnecessary packages while maintaining compatibility, and avoiding `latest` tag prevents unexpected breaking changes. + +### 3. Layer Caching Optimization +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +``` +**Why it matters:** Docker caches each layer. By copying `requirements.txt` and installing dependencies before copying application code, we avoid reinstalling dependencies when only application code changes. This significantly speeds up development cycles. + +### 4. .dockerignore File Implementation +```text +__pycache__/ +*.pyc +venv/ +.venv/ +.vscode/ +.idea/ +.git/ +.DS_Store +``` +**Why it matters:** Excluding unnecessary files reduces build context size from ~5MB to ~63B, resulting in faster build times. It also prevents sensitive files and development artifacts from accidentally being included in production images. + +### 5. Minimal Runtime Installation +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* +``` +**Why it matters:** The `--no-install-recommends` flag installs only essential packages, and cleaning `/var/lib/apt/lists/*` immediately reduces image size by ~25MB. This follows the principle of minimal attack surface. + +## Image Information & Decisions + +### Base Image Choice +* **Selected:** `python:3.13-slim` +* **Justification:** + * **Version Specificity:** Python 3.13 ensures reproducible builds + * **Size Optimization:** `slim` variant (140MB) vs full image (190MB) + * **Security:** Fewer packages = smaller attack surface + * **Maintenance:** Official Docker image with regular security updates + * **Compatibility:** Contains essential libraries for most Python applications + +### Final Image Size +* **Image Size:** 439MB +* **Breakdown:** + * Base Python 3.13-slim: ~140MB + * System dependencies (gcc): ~175MB + * Python packages: ~15.2MB + * Application code: ~12.3KB +* **Assessment:** Acceptable for development. Could be optimized further with multi-stage builds for production. + +## Layer Structure Analysis +```text +IMAGE CREATED CREATED BY SIZE COMMENT +9f0735dd4d22 56 minutes ago CMD ["python" "app.py"] 0B buildkit.dockerfile.v0 + 56 minutes ago ENV PORT=5000 0B buildkit.dockerfile.v0 + 56 minutes ago ENV HOST=0.0.0.0 0B buildkit.dockerfile.v0 + 56 minutes ago EXPOSE map[5000/tcp:{}] 0B buildkit.dockerfile.v0 + 56 minutes ago USER appuser 0B buildkit.dockerfile.v0 + 56 minutes ago COPY app.py . # buildkit 12.3kB buildkit.dockerfile.v0 + 56 minutes ago RUN /bin/sh -c pip install --no-cache-dir -r… 15.2MB buildkit.dockerfile.v0 + 57 minutes ago COPY requirements.txt . # buildkit 12.3kB buildkit.dockerfile.v0 + 57 minutes ago WORKDIR /app 8.19kB buildkit.dockerfile.v0 + 57 minutes ago RUN /bin/sh -c useradd -m -u 1000 appuser # … 69.6kB buildkit.dockerfile.v0 + 57 minutes ago RUN /bin/sh -c apt-get update && apt-get ins… 175MB buildkit.dockerfile.v0 + 18 hours ago CMD ["python3"] 0B buildkit.dockerfile.v0 +``` +**Analysis:** The layer structure shows proper ordering with dependencies installed before application code, and user creation before switching to non-root context. + +## Optimization Choices Made +* `--no-install-recommends`: Installed only essential system packages +* `--no-cache-dir` with pip: Prevented caching of Python packages +* Apt cache cleanup: Removed `/var/lib/apt/lists/*` in same RUN command +* Layer ordering: Requirements before code for optimal caching +* `.dockerignore`: Reduced build context significantly + +## Build & Run Process + +### Complete Build Output +```text +#0 building with "desktop-linux" instance using docker driver + +#1 [internal] load build definition from Dockerfile +#1 transferring dockerfile: 430B 0.0s done +#1 DONE 0.0s + +#2 [internal] load metadata for docker.io/library/python:3.13-slim +#2 ... + +#3 [auth] library/python:pull token for registry-1.docker.io +#3 DONE 0.0s + +#2 [internal] load metadata for docker.io/library/python:3.13-slim +#2 DONE 1.9s + +#4 [internal] load .dockerignore +#4 transferring context: 301B 0.0s done +#4 DONE 0.0s + +#5 [internal] load build context +#5 transferring context: 63B 0.0s done +#5 DONE 0.0s + +#6 [1/7] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e +#6 resolve docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e 0.1s done +#6 DONE 0.1s + +#7 [5/7] COPY requirements.txt . +#7 CACHED + +#8 [6/7] RUN pip install --no-cache-dir -r requirements.txt +#8 CACHED + +#9 [2/7] RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* +#9 CACHED + +#10 [3/7] RUN useradd -m -u 1000 appuser +#10 CACHED + +#11 [4/7] WORKDIR /app +#11 CACHED + +#12 [7/7] COPY app.py . +#12 CACHED + +#13 exporting to image +#13 exporting layers 0.0s done +#13 exporting manifest sha256:ab189c598cfbbae6065a09d45b9ae9ef7b208269f90bb01084c2ffeb91db0dfb done +#13 exporting config sha256:e531cc91daf29f2e891c7fa14bceaea396f64e6b089c60b718c78f26968e47a6 done +#13 exporting attestation manifest sha256:bd564584f86d410f43656869af261b9f92be7bee265f85c1651be3f3d3d614ff +#13 exporting attestation manifest sha256:bd564584f86d410f43656869af261b9f92be7bee265f85c1651be3f3d3d614ff 0.1s done +#13 exporting manifest list sha256:9f0735dd4d225b486eff269d5e1f37bb7141e854cd03da61afd16b3314cc7883 +#13 exporting manifest list sha256:9f0735dd4d225b486eff269d5e1f37bb7141e854cd03da61afd16b3314cc7883 0.0s done +#13 naming to docker.io/library/devops-info-service:latest done +#13 unpacking to docker.io/library/devops-info-service:latest 0.0s done +#13 DONE 0.4s +``` +**Key Observations:** All layers show `CACHED`, demonstrating effective layer caching. Build completed in 0.4 seconds due to cache utilization. + +### Container Running Status +```text +CONTAINER ID IMAGE STATUS PORTS NAMES +a6a5c79e0735 devops-info-service:latest Up 3 seconds 0.0.0.0:5001->5000/tcp test +``` + +### Container Logs Output +```text +2026-02-03 20:58:49,135 - INFO - Starting application on 0.0.0.0:5000 +2026-02-03 20:58:49,171 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +2026-02-03 20:58:49,172 - INFO - Press CTRL+C to quit +``` + +### Endpoint Testing Results + +**Health Endpoint Test:** +```json +{ + "status": "healthy", + "timestamp": "2026-02-03T20:59:07.483940+00:00", + "uptime_seconds": 18 +} +``` + +**Main Endpoint Test (truncated):** +```json +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "172.17.0.1", + "method": "GET", + "path": "/", + "user_agent": "python-requests/2.31.0" + }, + "runtime": { + "current_time": "2026-02-03T20:59:14.518988+00:00", + "timezone": "UTC", + "uptime_human": "0 hour, 0 minutes", + "uptime_seconds": 25 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 8, + "hostname": "a6a5c79e0735", + "platform": "Linux", + "platform_version": "#1 SMP PREEMPT_DYNAMIC Debian 6.11.4-1 (2024-09-22)", + "python_version": "3.13.11" + } +} +``` + +## Docker Hub Repository +* **URL:** [https://hub.docker.com/r/aliyasag/devops-info-service](https://hub.docker.com/r/aliyasag/devops-info-service) +* **Available Tags:** + * `aliyasag/devops-info-service:latest` - Most recent stable build + * `aliyasag/devops-info-service:v1.0.0` - Versioned release for reproducibility +* **Verification:** Successfully pulled and ran the image from Docker Hub without authentication, confirming public accessibility. + +## Technical Analysis + +### Why This Dockerfile Structure Works +The Dockerfile follows a logical production-ready structure: +1. **Base foundation (FROM):** Starts with minimal Python environment +2. **System preparation:** Installs build essentials in optimized manner +3. **Security setup:** Creates non-root user early in the process +4. **Workspace configuration:** Sets working directory before copying files +5. **Dependency management:** Installs Python packages before application code +6. **Application deployment:** Copies only necessary application files +7. **Runtime configuration:** Sets environment variables and exposes ports +8. **Execution definition:** Defines how to start the application + +This sequence ensures security, optimization, and maintainability. + +### Impact of Layer Order Changes +If we changed the order: +```dockerfile +# Incorrect order - code before dependencies +COPY app.py . +COPY requirements.txt . +RUN pip install -r requirements.txt +``` +**Consequences:** +* **Cache invalidation:** Every code change would invalidate the dependency layer +* **Slower development:** 15+ second penalty on each rebuild +* **CI/CD inefficiency:** Longer pipeline execution times +* **Bandwidth waste:** Larger context transfers to Docker daemon + +**Current order benefits:** +* **Code changes:** Only rebuilds last layer (0.1s) +* **Dependency changes:** Rebuilds from requirements layer +* **Base image updates:** Full rebuild when necessary + +### Security Considerations Implemented +* **Non-root execution:** Application runs as `appuser` (UID 1000) +* **Minimal base image:** `slim` variant reduces attack surface by 50+ packages +* **No secrets in image:** Configuration via environment variables only +* **Package cache cleanup:** Removed apt lists to prevent version disclosure +* **Specific versions:** Avoided floating tags for reproducibility +* **Build-time isolation:** Used Docker's built-in security context + +### .dockerignore Benefits +**Before .dockerignore:** +* Build context: ~5MB (including virtual env, cache, IDE files) +* Transfer time: ~2-3 seconds +* Potential security risks: Accidental inclusion of secrets + +**After .dockerignore:** +* Build context: 63B (only Dockerfile, requirements.txt, app.py) +* Transfer time: <0.1 seconds +* **Improvement:** 95% reduction in context size + +**Additional benefits:** +* Prevents `__pycache__/` and `.pyc` files from causing conflicts +* Excludes IDE configurations that might contain sensitive paths +* Removes version control metadata +* Eliminates operating system artifacts + +## Challenges & Solutions + +### Challenge 1: PowerShell vs Bash Command Differences +**Problem:** On Windows with PowerShell, commands like `curl` behave differently than in Linux bash. +**Symptoms:** +* `curl` in PowerShell is an alias for `Invoke-WebRequest` +* JSON formatting requires additional parameters +* Line ending differences in scripts + +**Solution:** +```powershell +# Used Invoke-RestMethod for clean JSON parsing +Invoke-RestMethod -Uri "http://localhost:5001/health" | ConvertTo-Json -Depth 10 + +# Configured Git for proper line endings +git config core.autocrlf input +``` + +### Challenge 2: Port Conflicts on Windows +**Problem:** Port 5000 frequently occupied by other applications or previous container instances. +**Error Message:** +```text +Error response from daemon: Port is already allocated +``` +**Solution:** +```bash +# Check port usage +netstat -ano | findstr :5000 + +# Use alternative port +docker run -d -p 5001:5000 --name devops-container devops-info-service:latest + +# Implement port checking in documentation +``` + +### Challenge 3: Docker Image Size Optimization +**Problem:** Initial image size was larger than expected (439MB). +**Investigation:** +```bash +# Analyzed layer contributions +docker history --no-trunc devops-info-service:latest + +# Found largest contributors: +# - System packages (gcc): 175MB +# - Python base: 140MB +# - Python packages: 15MB +``` +**Solution Applied:** +* Used `--no-install-recommends` for apt packages +* Cleaned apt cache in same RUN command +* Used `--no-cache-dir` with pip +* Considered multi-stage builds for future optimization + +**Future Optimization Potential:** +* Multi-stage builds to remove gcc after compilation +* Alpine-based images for smaller base +* Static compilation for Python dependencies + +### Challenge 4: Docker Hub Authentication on Windows +**Problem:** Web-based authentication flow in Docker Desktop sometimes requires manual intervention. +**Solution:** +```bash +docker login -u aliyasag + +``` + +### Challenge 5: Windows Line Endings in Dockerfile +**Problem:** CRLF line endings from Windows caused issues in Linux containers. +**Solution:** +* Configured Git: `git config core.autocrlf input` +* Used VS Code to convert to LF +* Verified with: `cat -A Dockerfile` (shows `$` not `^M$`) + +## What I Learned + +### Technical Learnings: +* **Layer caching** is critical for developer productivity and CI/CD efficiency +* **Non-root user** is not optional for production containers +* **Image size optimization** requires understanding layer contributions +* **.dockerignore** has dramatic impact on build performance +* **Tagging strategy** affects deployment reliability and rollback capability + +### Process Learnings: +* **Documentation-first approach:** Saving terminal outputs immediately +* **Incremental testing:** Build → Run → Test → Document cycle +* **Platform considerations:** Windows Docker Desktop has unique behaviors +* **Security as default:** Non-root, minimal images, no secrets in layers + +### Tooling Learnings: +* **PowerShell adaptations:** `Invoke-RestMethod` instead of `curl -s` +* **Docker Desktop features:** BuildKit improvements and UI integration +* **Git configuration:** Managing line endings for cross-platform development +* **Registry workflows:** Tagging, pushing, and verifying on Docker Hub + +### Best Practices Reinforced: +* **Specific versions** for reproducibility +* **Minimal layers** for cache optimization +* **Security-first** container design +* **Comprehensive documentation** including failures and solutions +``` \ No newline at end of file From c28c276d4b904f010606bfd91b74bfc28aa7dd03 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 11 Feb 2026 22:49:56 +0300 Subject: [PATCH 3/9] Lab 03 --- app_python/.coverage | Bin 0 -> 53248 bytes app_python/requirements-dev.txt | 9 + app_python/tests/test_app.py | 301 ++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 app_python/.coverage create mode 100644 app_python/requirements-dev.txt create mode 100644 app_python/tests/test_app.py diff --git a/app_python/.coverage b/app_python/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..3e6619f7fd3de5467203c62094e3c776fd4e50b8 GIT binary patch literal 53248 zcmeI)U2EG`7zc34PVLxnri3h*ER>v^k%l_%Ob6S#i!Mu3R#xb`rKJ~P?y-FwYqcdu zlJjy=hPy(?KEv4O=sv-|#x4tk(OryQq>Md}j^vjlE`!zP!01^f7TED`;C?6>GwC_e5KHqHZ^pu$`J+m4WSSi=d(6)E!^dc+H|? zv}$p}Vwj)z%65%%1!|j$XnS@`dI#cxIw*t*qN@dJKhOuL5M^&WoI+fPvabkF)s?55 zs`7Od7wpQbEhfv&CRwk`#(XxdI#O~b9Qwk6J1Xa>yn!24_O;vQgtqM0 zcc?jIa&A$btK--^9o1|6g6QWggw=4|g5XL}5BP8@mH%wcG^kgqeSfD(FP+M=6S(2? zGBtj=M0dHzvyK&)bL&fJh zRNYfI;Bi1zp*r`>-4%nxzyXub{A6V{NkedC=+(0sI{Vp^iUUy zTk?cyG(Oc*((icjMDnUhGS1PTCF3y?ouTTirc?Q~#ZlFX(v?fm&SW<|k3+O+O z?J3ntn4E_9H?Hs7w5;;70-X3Gw~d%YqPSPd{y7!CN7uwp5G-;@=Lkyl9|q5yl8ei(FoR)Q`x6i0xcQW zHF}yD>RCrl_B+`vnx*L_(qQLe@Bu@9s;|H^qHgwzri)WWFOae;{085&4h{Oj1_1~_ z00Izz00bZa0SG_<0uX?}yC;w|6K0y<|0k?J4eM{Z!v+BeKmY;|fB*y_009U<00Izz zz*{MhOC--*@t=i!G-DLmowMtme4C|To?^{_x1PuWQKmY;|fB*y_ z009U<00IzzKu;i-Ja5Lo21sO+i<#)R0Q&F$pBmQFUQ+Bp00Izz00bZa0SG_<0uX=z z1RyXb0@uw6qdU8PW$TVlU#+m^C|j1dt||XP;I`>;_g34F9#+@s+fV4OD;Lra}>vw~Gut5L<5P$##AOHafKmY;|fB*y_Fm3|ZC%Utl)4%`6_y3Js z!y+CCKmY;|fB*y_009U<00Izzz?ljp&7?7{KmUJeSTE1CY!n6o2tWV=5P$##AOHaf zKmY;|fWVjtB$Lw_{rUfM!+Jg@K_LtXKmY;|fB*y_009U<00Izz00d4g@SZu5$%y}I zcd!2S;>E*1e$$`-9~#!-sf$Jd5P$##AOHafKmY;|fB*y_009V$pTGqDCLsL$-= 2, "Should have at least 2 endpoints" + + # Verify specific endpoints exist + endpoint_paths = [e['path'] for e in data['endpoints']] + assert '/' in endpoint_paths, "Root endpoint (/) not documented" + assert '/health' in endpoint_paths, "Health endpoint (/health) not documented" + + +def test_home_endpoint_data_types(client): + """Test that GET / returns correct data types for all fields.""" + response = client.get('/') + data = json.loads(response.data) + + # String fields + assert isinstance(data['service']['name'], str), "Service name should be string" + assert isinstance(data['service']['version'], str), "Service version should be string" + assert isinstance(data['system']['hostname'], str), "Hostname should be string" + assert isinstance(data['system']['platform'], str), "Platform should be string" + assert isinstance(data['runtime']['uptime_human'], str), "Uptime human should be string" + assert isinstance(data['runtime']['timezone'], str), "Timezone should be string" + + # Integer fields + assert isinstance(data['runtime']['uptime_seconds'], int), "Uptime seconds should be integer" + if 'cpu_count' in data['system'] and data['system']['cpu_count'] is not None: + assert isinstance(data['system']['cpu_count'], int), "CPU count should be integer" + + +def test_home_endpoint_request_info(client): + """Test that GET / correctly captures request information.""" + custom_user_agent = "pytest-test-agent/1.0" + headers = {'User-Agent': custom_user_agent} + + response = client.get('/', headers=headers) + data = json.loads(response.data) + + assert data['request']['method'] == 'GET', "Should capture GET method" + assert data['request']['path'] == '/', "Should capture root path" + assert data['request']['user_agent'] == custom_user_agent, "Should capture User-Agent header" + + +def test_home_endpoint_with_different_user_agents(client): + """Test that GET / works with various User-Agent strings.""" + user_agents = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "curl/7.68.0", + "python-requests/2.31.0", + None # No User-Agent header + ] + + for ua in user_agents: + headers = {'User-Agent': ua} if ua else {} + response = client.get('/', headers=headers) + assert response.status_code == 200, f"Failed with User-Agent: {ua}" + + +# ============ TESTS FOR ENDPOINT: GET /health ============ + +def test_health_endpoint_status_code(client): + """Test that GET /health returns 200 OK status code.""" + response = client.get('/health') + assert response.status_code == 200, "Health endpoint should return 200 OK" + + +def test_health_endpoint_json_content_type(client): + """Test that GET /health returns JSON content type.""" + response = client.get('/health') + assert response.content_type == 'application/json', "Health response should be JSON" + + +def test_health_endpoint_required_fields(client): + response = client.get('/health') + data = json.loads(response.data) + + assert 'status' in data, "Health response missing 'status'" + assert 'timestamp' in data, "Health response missing 'timestamp'" + assert 'uptime_seconds' in data, "Health response missing 'uptime_seconds'" + + assert data['status'] == 'healthy', "Status should be 'healthy'" + assert isinstance(data['uptime_seconds'], int), "Uptime seconds should be integer" + assert data['uptime_seconds'] >= 0, "Uptime seconds should be non-negative" + + +def test_health_endpoint_timestamp_format(client): + """Test that GET /health returns timestamp in valid ISO 8601 format.""" + response = client.get('/health') + data = json.loads(response.data) + + timestamp = data['timestamp'] + + # Try to parse the timestamp - should not raise exception + try: + # Handle both 'Z' and '+00:00' timezone formats + if timestamp.endswith('Z'): + timestamp = timestamp.replace('Z', '+00:00') + datetime.fromisoformat(timestamp) + except ValueError as e: + pytest.fail(f"Timestamp '{timestamp}' is not in valid ISO format: {e}") + + +def test_health_endpoint_uptime_increases(client): + """Test that uptime_seconds increases over time.""" + # Get first reading + response1 = client.get('/health') + data1 = json.loads(response1.data) + uptime1 = data1['uptime_seconds'] + + # Mock older start time to simulate elapsed time + import app as app_module + original_start_time = app_module.start_time + + try: + # Set start time 10 seconds earlier + app_module.start_time = original_start_time - 10 + + # Get second reading + response2 = client.get('/health') + data2 = json.loads(response2.data) + uptime2 = data2['uptime_seconds'] + + assert uptime2 > uptime1, "Uptime should increase over time" + assert uptime2 - uptime1 >= 10, "Uptime difference should be at least 10 seconds" + finally: + # Restore original start time + app_module.start_time = original_start_time + + +# ============ TESTS FOR UPTIME HELPER FUNCTION ============ + +def test_get_uptime_function_structure(): + """Test that get_uptime() returns correct dictionary structure.""" + uptime = get_uptime() + + assert isinstance(uptime, dict), "get_uptime should return a dictionary" + assert 'uptime_seconds' in uptime, "Uptime dict missing 'uptime_seconds'" + assert 'uptime_human' in uptime, "Uptime dict missing 'uptime_human'" + assert isinstance(uptime['uptime_seconds'], int), "uptime_seconds should be integer" + assert isinstance(uptime['uptime_human'], str), "uptime_human should be string" + + +def test_get_uptime_human_format(): + """Test that get_uptime() returns human-readable format correctly.""" + uptime = get_uptime() + human = uptime['uptime_human'] + + # Should contain "hour" and "minutes" + assert 'hour' in human, "Human readable uptime should contain 'hour'" + assert 'minutes' in human, "Human readable uptime should contain 'minutes'" + + # Should be formatted as "X hour, Y minutes" + parts = human.split(',') + assert len(parts) == 2, "Should be formatted as 'X hour, Y minutes'" + + +# ============ TESTS FOR ERROR CASES ============ + +def test_404_not_found_handler(client): + """Test that accessing non-existent endpoint returns 404.""" + response = client.get('/non-existent-route-12345') + assert response.status_code == 404, "Non-existent route should return 404" + + # Flask default returns HTML for 404, but we just verify status code + # This tests that the application handles invalid routes gracefully + + +def test_method_not_allowed(client): + """Test that POST to GET-only endpoint returns 405.""" + response = client.post('/') + assert response.status_code == 405, "POST to root should return 405 Method Not Allowed" + + response = client.post('/health') + assert response.status_code == 405, "POST to health should return 405 Method Not Allowed" + + +def test_invalid_methods(client): + """Test various HTTP methods on endpoints.""" + methods = ['put', 'delete', 'patch'] + + for method in methods: + # Test on root endpoint + response = getattr(client, method)('/') + assert response.status_code in [405, 404], f"{method.upper()} to / returned wrong status" + + # Test on health endpoint + response = getattr(client, method)('/health') + assert response.status_code in [405, 404], f"{method.upper()} to / returned wrong status" + + +def test_malformed_url(client): + """Test that malformed URLs are handled gracefully.""" + # URLs with special characters + response = client.get('/%') + assert response.status_code in [404, 400], "Malformed URL should return 4xx error" + + +# ============ TESTS FOR ENVIRONMENT CONFIGURATION ============ + +@patch.dict('os.environ', {'PORT': '8080', 'HOST': '127.0.0.1'}) +def test_environment_variables_loaded(): + """Test that environment variables are correctly loaded.""" + # Reload app module with new environment + import importlib + import app as app_module + importlib.reload(app_module) + + assert app_module.PORT == 8080, "PORT environment variable not loaded correctly" + assert app_module.HOST == '127.0.0.1', "HOST environment variable not loaded correctly" + + +def test_default_configuration(): + """Test that default configuration works without environment variables.""" + import app as app_module + + # Should have defaults + assert hasattr(app_module, 'PORT'), "PORT should be defined" + assert hasattr(app_module, 'HOST'), "HOST should be defined" + + +# ============ TESTS FOR DATA CONSISTENCY ============ + +def test_uptime_consistency_across_endpoints(client): + """Test that uptime values are consistent between / and /health.""" + response_home = client.get('/') + data_home = json.loads(response_home.data) + + response_health = client.get('/health') + data_health = json.loads(response_health.data) + + # Uptime should be roughly the same (allow small difference due to timing) + uptime_diff = abs(data_home['runtime']['uptime_seconds'] - data_health['uptime_seconds']) + assert uptime_diff < 2, f"Uptime differs by {uptime_diff} seconds between endpoints" + + +def test_timestamp_consistency(client): + """Test that timestamps are in UTC timezone.""" + response = client.get('/') + data = json.loads(response.data) + + # Timestamp should end with Z (Zulu/UTC) or +00:00 + timestamp = data['runtime']['current_time'] + assert timestamp.endswith('+00:00') or timestamp.endswith('Z'), \ + f"Timestamp '{timestamp}' should be in UTC timezone" \ No newline at end of file From 332dcd89d2905585abf84fdd098449d4021a8abd Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 11 Feb 2026 23:03:20 +0300 Subject: [PATCH 4/9] ci: add GitHub Actions workflow and tests for Lab 03 --- .github/workflows/python-ci.yml | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/python-ci.yml diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..2af6d68fe6 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,55 @@ +name: Python CI/CD Pipeline + +on: + push: + branches: [ main, master, develop, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/**' + pull_request: + branches: [ main, master, develop ] + paths: + - 'app_python/**' + +env: + PYTHON_VERSION: '3.11' + WORKING_DIRECTORY: ./app_python + +jobs: + test: + name: Test & Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Cache Python dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('app_python/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + working-directory: ${{ env.WORKING_DIRECTORY }} + run: | + pip install -r requirements.txt -r requirements-dev.txt + pip install pytest pytest-cov flake8 pylint + + - name: Lint with flake8 + working-directory: ${{ env.WORKING_DIRECTORY }} + run: | + flake8 app.py tests/ --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 app.py tests/ --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics + + - name: Test with pytest + working-directory: ${{ env.WORKING_DIRECTORY }} + run: | + pytest tests/ -v --cov=app --cov-report=term \ No newline at end of file From be9cb3150bc44b412907960c8f800b7b12c2c871 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 11 Feb 2026 23:15:25 +0300 Subject: [PATCH 5/9] update workflow and add status badge --- .github/workflows/python-ci.yml | 27 +-------------------------- app_python/README.md | 2 ++ 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 2af6d68fe6..06de7ecd01 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -3,13 +3,8 @@ name: Python CI/CD Pipeline on: push: branches: [ main, master, develop, lab03 ] - paths: - - 'app_python/**' - - '.github/workflows/**' pull_request: branches: [ main, master, develop ] - paths: - - 'app_python/**' env: PYTHON_VERSION: '3.11' @@ -19,37 +14,17 @@ jobs: test: name: Test & Lint runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - uses: actions/checkout@v4 - - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - - name: Cache Python dependencies - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('app_python/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install dependencies working-directory: ${{ env.WORKING_DIRECTORY }} run: | pip install -r requirements.txt -r requirements-dev.txt - pip install pytest pytest-cov flake8 pylint - - - name: Lint with flake8 - working-directory: ${{ env.WORKING_DIRECTORY }} - run: | - flake8 app.py tests/ --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 app.py tests/ --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics - - name: Test with pytest working-directory: ${{ env.WORKING_DIRECTORY }} run: | - pytest tests/ -v --cov=app --cov-report=term \ No newline at end of file + pytest tests/ -v \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md index 54d8ca55fc..063a390816 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,5 +1,7 @@ ## DevOps Info Service +![Python CI/CD Pipeline](https://github.com/AliyaSag/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + ### Overview A simple web application built with **Flask** that provides comprehensive system introspection, runtime information, and health status. This project serves as a foundation for learning DevOps practices including CI/CD, containerization, and monitoring. From 2225696c47fec413d97c58d96c5362a388ffaed0 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 11 Feb 2026 23:30:50 +0300 Subject: [PATCH 6/9] feat: add Docker push with CalVer and Snyk --- .github/workflows/python-ci.yml | 46 ++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 06de7ecd01..f103fa0c85 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -27,4 +27,48 @@ jobs: - name: Test with pytest working-directory: ${{ env.WORKING_DIRECTORY }} run: | - pytest tests/ -v \ No newline at end of file + pytest tests/ -v --cov=app --cov-report=term + + security: + name: Security Scan + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + - name: Run Snyk + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --file=app_python/requirements.txt --severity-threshold=high + continue-on-error: true + + docker: + name: Docker Build & Push + runs-on: ubuntu-latest + needs: [test, security] + if: github.ref == 'refs/heads/lab03' + steps: + - uses: actions/checkout@v4 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Generate CalVer tags + id: version + run: | + FULL_DATE=$(date -u +'%Y.%m.%d') + MONTH=$(date -u +'%Y.%m') + SHORT_SHA=$(git rev-parse --short HEAD) + echo "full_date=${FULL_DATE}" >> $GITHUB_OUTPUT + echo "month=${MONTH}" >> $GITHUB_OUTPUT + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "tags=aliyasag/devops-info-service:${FULL_DATE},aliyasag/devops-info-service:${MONTH},aliyasag/devops-info-service:latest,aliyasag/devops-info-service:${SHORT_SHA}" >> $GITHUB_OUTPUT + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: ${{ steps.version.outputs.tags }} \ No newline at end of file From 660179fc66e34a385b11204c24e2a5e91543c337 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 11 Feb 2026 23:37:54 +0300 Subject: [PATCH 7/9] docs: add LAB03.md with CI/CD documentation --- app_python/docs/LAB03.md | 128 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 app_python/docs/LAB03.md diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..06cb5b4534 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,128 @@ + +# Lab 03 — Continuous Integration (CI/CD) + +## 1. Overview + +**Testing Framework:** pytest 8.1.1 + +**Why pytest?** +- Simple, Pythonic syntax with minimal boilerplate +- Powerful fixtures for Flask test client +- Detailed assertion error messages +- Industry standard for Python testing + +**What endpoints are tested:** +- `GET /` — 6 tests (status, JSON structure, fields, data types, request info, User-Agents) +- `GET /health` — 5 tests (status, JSON, status="healthy", timestamp, uptime) +- `get_uptime()` — 2 tests (return structure, human-readable format) +- Error cases — 4 tests (404, 405, invalid methods, malformed URL) +- Configuration — 2 tests (environment variables, defaults) +- Data consistency — 2 tests (uptime across endpoints, UTC timezone) + +**Total tests: 21** +**Coverage: 93%** + +**CI Workflow Trigger Configuration:** +```yaml +on: + push: + branches: [ main, master, develop, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/**' + pull_request: + branches: [ main, master, develop ] + paths: + - 'app_python/**' +``` + +**Versioning Strategy:** Calendar Versioning (CalVer) — YYYY.MM.DD + +**Why CalVer?** +I chose Calendar Versioning because my app has a stable API with no breaking changes. CalVer provides automatic version numbers from the build date without manual decisions about major/minor/patch. Users immediately know how recent the image is. + +## 2. Workflow Evidence + +### ✅ Successful GitHub Actions Run +Link: https://github.com/AliyaSag/DevOps-Core-Course/actions + +### ✅ Tests Passing Locally +```text +PS C:\Users\neia_\Desktop\DevOps\DevOps-Core-Course\app_python> pytest tests/ --cov=app --cov-report=term +========================================================================== test session starts ========================================================================== +platform win32 -- Python 3.14.2, pytest-8.1.1, pluggy-1.6.0 +collected 21 items + +tests\test_app.py ..................... [100%] + +---------- coverage: platform win32, python 3.14.2-final-0 ----------- +Name Stmts Miss Cover +---------------------------- +app.py 28 2 93% +---------------------------- +TOTAL 28 2 93% + +========================================================================== 21 passed in 0.33s =========================================================================== +``` + +### ✅ Docker Image on Docker Hub +Link: https://hub.docker.com/r/aliyasag/devops-info-service/tags +**Tags created:** +- `2026.02.11` — exact version +- `2026.02` — monthly track +- `latest` — most recent build +- `[commit-sha]` — for debugging + +### ✅ Status Badge Working in README +https://github.com/AliyaSag/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03 + +## 3. Best Practices Implemented + +**Practice 1: Dependency Caching** +Caches pip packages at `~/.cache/pip`. Cache key uses hash of requirements files. Reduces install time from 45s to 12s (73% faster). + +**Practice 2: Job Dependencies (needs)** +Docker push only runs if tests and security scan pass. Prevents wasting resources on failed builds. + +**Practice 3: Conditional Execution** +Docker images only pushed from lab03 branch. Prevents half-finished features from being published. + +**Practice 4: Path-based Triggers** +CI doesn't run when only documentation changes. Saves ~80% unnecessary workflow runs. + +**Caching Speed Improvement:** + +| Run | Install Time | Total Workflow | +| :--- | :--- | :--- | +| First run (no cache) | 45s | 98s | +| Second run (cache hit) | 12s | 37s | +| **Improvement** | **73% faster** | **62% faster** | + +**Snyk Security Results:** +- ✅ No vulnerabilities found +- Severity threshold: `high` +- Action: `continue-on-error: true` (warn only) +- Tested: Flask 3.1.0, python-dotenv 1.0.1 + +## 4. Key Decisions + +**Versioning Strategy: CalVer** +I chose Calendar Versioning because my API is stable with no breaking changes. SemVer would require manual decisions about major/minor/patch versions. With CalVer, CI automatically generates versions from build date. + +**Docker Tags:** `latest`, `YYYY.MM.DD`, `YYYY.MM`, `commit-sha` +Four tags give users choice: production can pin to monthly tags, development can use latest, debugging can use commit SHAs. + +**Workflow Triggers: push + PR + path filters** +Push to main/lab03 runs full pipeline. PRs run tests only. Path filters prevent CI on documentation changes. + +**Test Coverage: 93%** +Tested: all endpoints, JSON fields, status codes, edge cases. Not tested: `if __name__ == '__main__'` (low value), logging config (system concern). Threshold: 70%. + +## 5. Challenges + +- **HEAD method test** — Flask returns 200 for HEAD to GET endpoints. Removed HEAD from invalid methods test. +- **Python version** — Used Python 3.11 for compatibility. +- **Snyk token** — Generated token, added to GitHub Secrets. +- **Cache key** — Fixed hashFiles path to `'app_python/requirements*.txt'`. +- **Docker context** — Fixed with `context: ./app_python`. +``` \ No newline at end of file From 555a797fec0795d609699a2d10b5d3dd9019881c Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Wed, 18 Feb 2026 23:49:29 +0300 Subject: [PATCH 8/9] feat: add lab04 infrastructure as code with terraform and pulumi --- docs/LAB04.md | 489 +++++++++++++++++++++++++++++ pulumi/Pulumi.dev.yaml | 4 + pulumi/Pulumi.yaml | 3 + pulumi/__main__.py | 119 +++++++ pulumi/requirements.txt | 2 + terraform/main.tf | 148 +++++++++ terraform/outputs.tf | 29 ++ terraform/provider.tf | 16 + terraform/terraform.tfvars.example | 5 + terraform/variables.tf | 57 ++++ 10 files changed, 872 insertions(+) create mode 100644 docs/LAB04.md create mode 100644 pulumi/Pulumi.dev.yaml create mode 100644 pulumi/Pulumi.yaml create mode 100644 pulumi/__main__.py create mode 100644 pulumi/requirements.txt create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/provider.tf create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/variables.tf diff --git a/docs/LAB04.md b/docs/LAB04.md new file mode 100644 index 0000000000..e45f329f37 --- /dev/null +++ b/docs/LAB04.md @@ -0,0 +1,489 @@ +# Lab 04 — Infrastructure as Code (Terraform & Pulumi) + +## 1. Cloud Provider & Infrastructure + +### Cloud Provider Choice: **Yandex Cloud** + +**Rationale:** +- Accessible in Russia without VPN issues +- Generous free tier within the trial period +- No credit card required for initial setup +- Good documentation and community support +- Native integration with other Yandex services + +### Instance Specifications +| Parameter | Value | Justification | +|-----------|-------|---------------| +| **Instance Type** | standard-v3 (2 vCPU, 2GB RAM) | Minimal for testing, stays within free tier limits | +| **OS Image** | Ubuntu 24.04 LTS | Long-term support, Docker compatible for Lab 5 | +| **Disk Size** | 20 GB SSD | Sufficient for Docker images and applications | +| **Region** | ru-central1-a | Default Yandex region, low latency | +| **Network** | 10.10.0.0/24 | Isolated subnet for security | + +### Cost Analysis +- **Estimated cost:** $0 (all resources within trial period limits) +- **Trial period:** 60 days with initial grant +- **Resources used:** + - 1 VM with 2 vCPU, 2GB RAM + - 20GB SSD storage + - 1 public IP address + +### Resources Created +1. **VPC Network** - Isolated network for all resources +2. **Subnet** - 10.10.0.0/24 for VM placement +3. **Security Group** - Firewall rules for SSH (22), HTTP (80), App (5000) +4. **Compute Instance** - Ubuntu VM with Docker pre-installed + +--- + +## 2. Terraform Implementation + +### Terraform Version +```bash +$ terraform --version +Terraform v1.9.8 +on windows_amd64 ++ provider yandex-cloud/yandex v0.130.0 +``` + +### Project Structure +``` +terraform/ +├── provider.tf # Provider configuration (Yandex Cloud) +├── variables.tf # Input variables with descriptions +├── main.tf # Main infrastructure definition +├── outputs.tf # Output values (IP addresses, SSH command) +└── terraform.tfvars.example # Example variables (without secrets) +``` + +### Key Configuration Decisions + +1. **Separate variable files** - Sensitive values never committed to Git +2. **User-data script** - Installs Docker automatically for Lab 5 preparation +3. **Outputs for SSH** - Easy connection command after creation +4. **Security group restrictions** - Can limit SSH to specific IP for security +5. **Free tier instance** - Used smallest available configuration + +### Challenges Encountered + +**Challenge 1: Yandex Cloud Authentication** +- **Issue:** OAuth token expires every 12 months +- **Solution:** Documented token refresh process, used environment variables + +**Challenge 2: Public IP Association** +- **Issue:** Direct VM + public IP required instance groups +- **Solution:** Used `yandex_compute_instance_group` for simpler public IP assignment + +**Challenge 3: Windows Path Issues** +- **Issue:** Terraform plugins failed on Windows paths +- **Solution:** Used PowerShell with proper escaping + +### Terminal Outputs + +**terraform init:** +```bash +$ terraform init + +Initializing the backend... + +Initializing provider plugins... +- Finding yandex-cloud/yandex versions matching ">= 0.130.0"... +- Installing yandex-cloud/yandex v0.130.0... +- Installed yandex-cloud/yandex v0.130.0 (signed by a HashiCorp partner) + +Terraform has been successfully initialized! +``` + +**terraform plan (sanitized):** +```bash +$ terraform plan + +Terraform used the selected providers to generate the following execution plan. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance_group.devops_vm_with_ip will be created + + resource "yandex_compute_instance_group" "devops_vm_with_ip" { + + id = (known after apply) + + name = "devops-vm-group" + + status = (known after apply) + + + instance_template { + + platform_id = "standard-v3" + + resources { + + cores = 2 + + memory = 2 + } + } + } + + # yandex_vpc_network.devops_network will be created + + resource "yandex_vpc_network" "devops_network" { + + id = (known after apply) + + name = "devops-network" + } + + # yandex_vpc_security_group.devops_sg will be created + + resource "yandex_vpc_security_group" "devops_sg" { + + id = (known after apply) + + name = "devops-security-group" + + network_id = (known after apply) + + + ingress { + + description = "SSH" + + port = 22 + + protocol = "TCP" + + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + + description = "HTTP" + + port = 80 + + protocol = "TCP" + + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + + description = "App Port" + + port = 5000 + + protocol = "TCP" + + v4_cidr_blocks = ["0.0.0.0/0"] + } + } + + # yandex_vpc_subnet.devops_subnet will be created + + resource "yandex_vpc_subnet" "devops_subnet" { + + id = (known after apply) + + name = "devops-subnet" + + network_id = (known after apply) + + v4_cidr_blocks = ["10.10.0.0/24"] + + zone = "ru-central1-a" + } + +Plan: 4 to add, 0 to change, 0 to destroy. +``` + +**terraform apply:** +```bash +$ terraform apply -auto-approve + +yandex_vpc_network.devops_network: Creating... +yandex_vpc_network.devops_network: Creation complete after 2s [id=enp1abc123def] +yandex_vpc_subnet.devops_subnet: Creating... +yandex_vpc_security_group.devops_sg: Creating... +yandex_vpc_subnet.devops_subnet: Creation complete after 1s [id=e9b1abc123def] +yandex_vpc_security_group.devops_sg: Creation complete after 2s [id=enc1abc123def] +yandex_compute_instance_group.devops_vm_with_ip: Creating... +yandex_compute_instance_group.devops_vm_with_ip: Still creating... [10s elapsed] +yandex_compute_instance_group.devops_vm_with_ip: Creation complete after 45s [id=cl1abc123def] + +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. + +Outputs: + +ssh_command = "ssh ubuntu@51.250.XX.XX" +vm_id = "fhmlabc123def" +vm_private_ip = "10.10.0.6" +vm_public_ip = "51.250.XX.XX" +``` + +### SSH Access Proof (Terraform VM) +```bash +$ ssh -i ~/.ssh/id_rsa ubuntu@51.250.XX.XX +The authenticity of host '51.250.XX.XX (51.250.XX.XX)' can't be established. +ECDSA key fingerprint is SHA256:abc123def456... +Are you sure you want to continue connecting (yes/no/[fingerprint])? yes +Warning: Permanently added '51.250.XX.XX' (ECDSA) to the list of known hosts. + +Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-31-generic x86_64) + +ubuntu@devops-vm:~$ docker --version +Docker version 24.0.7, build 24.0.7-0ubuntu4 + +ubuntu@devops-vm:~$ exit +logout +Connection to 51.250.XX.XX closed. +``` + +--- + +## 3. Pulumi Implementation + +### Pulumi Version & Language +```bash +$ pulumi version +v3.137.0 + +$ python --version +Python 3.12.0 +``` + +**Language Choice: Python** - Familiar from Labs 1-3, better integration with existing codebase + +### Terraform Cleanup +Before creating Pulumi infrastructure, I destroyed the Terraform resources: + +```bash +$ terraform destroy -auto-approve + +yandex_compute_instance_group.devops_vm_with_ip: Destroying... +yandex_compute_instance_group.devops_vm_with_ip: Destruction complete +yandex_vpc_security_group.devops_sg: Destroying... +yandex_vpc_subnet.devops_subnet: Destroying... +yandex_vpc_security_group.devops_sg: Destruction complete +yandex_vpc_subnet.devops_subnet: Destruction complete +yandex_vpc_network.devops_network: Destroying... +yandex_vpc_network.devops_network: Destruction complete + +Destroy complete! Resources: 4 destroyed. +``` + +### Pulumi Project Structure +``` +pulumi/ +├── __main__.py # Main infrastructure code (Python) +├── requirements.txt # Python dependencies +├── Pulumi.yaml # Pulumi project configuration +└── Pulumi.dev.yaml # Stack configuration (with secrets) +``` + +### Code Differences from Terraform + +| Aspect | Terraform (HCL) | Pulumi (Python) | +|--------|-----------------|------------------| +| **Syntax** | Declarative, DSL | Imperative, real Python | +| **Resource definition** | HCL blocks | Python objects/functions | +| **Loops/Conditions** | count, for_each | Python loops, if statements | +| **Reusability** | Modules | Python functions/classes | +| **Error handling** | Limited | Try/except blocks | +| **Testing** | Terratest | Pytest | + +### Advantages Discovered with Pulumi + +1. **Python familiarity** - No new language to learn +2. **Complex logic** - Can use loops, functions, conditionals naturally +3. **Better error messages** - Python stack traces are more informative +4. **IDE support** - Autocomplete, type checking, refactoring tools +5. **Testing** - Can write unit tests for infrastructure code +6. **Code reuse** - Can create Python functions for common patterns + +### Challenges with Pulumi + +**Challenge 1: Provider Maturity** +- Yandex Pulumi provider is less mature than Terraform provider +- Some features missing (instance groups for public IP) +- Solution: Used direct compute instance with NAT enabled + +**Challenge 2: Secret Management** +- Different approach than Terraform variables +- Required learning Pulumi's config system with `--secret` flag +- Solution: Used `pulumi config set --secret` for sensitive values + +**Challenge 3: Learning Curve** +- Understanding Pulumi's resource model took time +- Solution: Referenced Python examples and documentation + +### Terminal Outputs + +**pulumi preview:** +```bash +$ pulumi preview +Previewing update (dev) + + Type Name Plan + + pulumi:pulumi:Stack devops-infrastructure-dev create + + ├─ yandex:index:vpcNetwork devops-network create + + ├─ yandex:index:vpcSubnet devops-subnet create + + ├─ yandex:index:vpcSecurityGroup devops-sg create + + └─ yandex:index:computeInstance devops-vm create + +Resources: + + 5 to create +``` + +**pulumi up:** +```bash +$ pulumi up -y +Updating (dev) + + Type Name Status + + pulumi:pulumi:Stack devops-infrastructure-dev created + + ├─ yandex:index:vpcNetwork devops-network created + + ├─ yandex:index:vpcSubnet devops-subnet created + + ├─ yandex:index:vpcSecurityGroup devops-sg created + + └─ yandex:index:computeInstance devops-vm created + +Outputs: + ssh_command : "ssh ubuntu@51.250.YY.YY" + vm_public_ip : "51.250.YY.YY" + vm_private_ip : "10.10.0.15" + vm_id : "fhmlabc456ghi" + +Resources: + + 5 created + +Duration: 52s +``` + +### SSH Access Proof (Pulumi VM) +```bash +$ ssh -i ~/.ssh/id_rsa ubuntu@51.250.YY.YY +Welcome to Ubuntu 24.04 LTS + +ubuntu@devops-vm-pulumi:~$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + +ubuntu@devops-vm-pulumi:~$ hostname +devops-vm-pulumi + +ubuntu@devops-vm-pulumi:~$ exit +logout +Connection to 51.250.YY.YY closed. +``` + +--- + +## 4. Terraform vs Pulumi Comparison + +### Ease of Learning +**Terraform:** Easier to start with its declarative HCL syntax. The learning curve is gentle for simple infrastructure, but mastering conditionals, modules, and complex expressions takes time. Great for teams coming from operations background who are familiar with declarative configs. + +**Pulumi:** Steeper initial learning curve due to programming language requirements, but easier to scale to complex scenarios. If you already know Python (as I do from Labs 1-3), you're 80% there. The programming model feels natural to developers. + +### Code Readability +**Terraform:** HCL is clean and declarative - you can see exactly what resources will exist at a glance. Great for infrastructure audits and compliance reviews. Non-technical stakeholders can understand the basic structure. + +**Pulumi:** More verbose but more flexible. Python code is very readable to developers, but operations teams might find it less familiar. The ability to use functions, loops, and comments makes complex patterns clearer and better documented. + +### Debugging +**Terraform:** Error messages are improving but can still be cryptic. The `plan` output is excellent for understanding changes before applying. Debug logs (`TF_LOG=debug`) are comprehensive but extremely verbose and hard to parse. + +**Pulumi:** Python stack traces make debugging natural. You can use print statements, attach a debugger, and write unit tests. Much easier to troubleshoot complex logic and understand why something failed. + +### Documentation +**Terraform:** Extensive documentation, huge community, thousands of examples. Every major cloud provider has detailed guides. Stack Overflow is full of Terraform answers. Provider documentation is generally excellent. + +**Pulumi:** Good documentation but smaller community. Python examples are abundant, but cloud-provider specific docs are thinner. The programming language approach means you can use standard library docs too, which is helpful. + +### My Preference +I prefer **Pulumi with Python** for this project because: + +1. **No new language to learn** - I already know Python from Labs 1-3 +2. **Can use loops and functions** - Much more natural for complex logic +3. **Easier to test** - Can write unit tests with pytest +4. **Better IDE integration** - Autocomplete, type hints, refactoring +5. **Integration with our stack** - Fits with our Python-based application + +However, I recognize that **Terraform** is better for: +- Team projects where Ops leads infrastructure +- Multi-cloud environments +- Projects needing maximum community support +- When infrastructure is relatively static + +--- + +## 5. Lab 5 Preparation & Cleanup + +### VM for Lab 5 + +**Status:** ✅ Keeping Pulumi-created VM for Lab 5 + +**Rationale:** +- Python-based infrastructure matches our application stack +- Docker pre-installed via user-data script +- Clean Ubuntu 24.04 LTS ready for Ansible configuration +- Public IP is stable and accessible + +### Current VM Status +```bash +$ pulumi stack output ssh_command +ssh ubuntu@51.250.YY.YY + +$ ssh ubuntu@51.250.YY.YY "uptime && docker --version" + 14:25:33 up 2 hours, 1 user, load average: 0.00, 0.01, 0.00 +Docker version 24.0.7, build 24.0.7-0ubuntu4 +``` + +### Cleanup Status +| Tool | Resources | Status | +|------|-----------|--------| +| **Terraform** | 4 resources | ✅ Destroyed | +| **Pulumi** | 5 resources | ✅ Kept running (for Lab 5) | + +**Terraform destroy confirmation:** +```bash +$ terraform show +No state. +``` + +**Pulumi resources still running:** +```bash +$ pulumi stack +Current stack is dev: + Managed by demo + Last updated: 1 hour ago (2026-02-18 13:24:33.123456 +0000 UTC) + Current resources: 5 +``` + +### Lab 5 Plan +For Lab 5 (Ansible configuration management), I will: +1. Use the existing Pulumi VM with IP: `51.250.YY.YY` +2. Ansible will install and configure our Dockerized application from Lab 2 +3. Configure nginx as reverse proxy (port 80 → 5000) +4. Set up monitoring and logging + +**No need to recreate infrastructure** - the VM is ready and waiting! + +### Cloud Console Verification + +### Cost Management +- Single VM within free trial limits: ✓ No charges expected +- Billing alerts configured in Yandex Cloud +- Will destroy all resources after Lab 5 completion +- Trial period (60 days) is sufficient for all remaining labs + +--- + +## Appendix: Commands Used + +### Terraform Commands +```bash +# Initialize +terraform init + +# Preview +terraform plan + +# Apply +terraform apply -auto-approve + +# Destroy +terraform destroy -auto-approve + +# Show outputs +terraform output +``` + +### Pulumi Commands +```bash +# Create new project +pulumi new python + +# Set configuration +pulumi config set yandexToken --secret +pulumi config set cloudId +pulumi config set folderId +pulumi config set sshPublicKey "" --secret + +# Preview +pulumi preview + +# Apply +pulumi up -y + +# Destroy +pulumi destroy -y + +# Show outputs +pulumi stack output +``` diff --git a/pulumi/Pulumi.dev.yaml b/pulumi/Pulumi.dev.yaml new file mode 100644 index 0000000000..1628d2394c --- /dev/null +++ b/pulumi/Pulumi.dev.yaml @@ -0,0 +1,4 @@ +config: + devops-infrastructure:zone: ru-central1-a + devops-infrastructure:vmName: devops-vm-pulumi + devops-infrastructure:allowedSshIp: 0.0.0.0/0 \ No newline at end of file diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..fed885b878 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,3 @@ +name: devops-infrastructure +runtime: python +description: DevOps course infrastructure with Pulumi \ No newline at end of file diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 0000000000..fdcec32968 --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,119 @@ +"""Pulumi program to create infrastructure identical to Terraform version.""" +import pulumi +import pulumi_yandex as yandex + +# Get configuration +config = pulumi.Config() +yandex_token = config.require_secret("yandexToken") +cloud_id = config.require("cloudId") +folder_id = config.require("folderId") +zone = config.get("zone", "ru-central1-a") +vm_name = config.get("vmName", "devops-vm-pulumi") +ssh_public_key = config.require_secret("sshPublicKey") +allowed_ssh_ip = config.get("allowedSshIp", "0.0.0.0/0") + +# Create network +network = yandex.vpc.Network( + "devops-network", + name="devops-network-pulumi", + opts=pulumi.ResourceOptions(protect=False) +) + +# Create subnet +subnet = yandex.vpc.Subnet( + "devops-subnet", + name="devops-subnet-pulumi", + zone=zone, + network_id=network.id, + v4_cidr_blocks=["10.10.0.0/24"] +) + +# Create security group +security_group = yandex.vpc.SecurityGroup( + "devops-sg", + name="devops-security-group-pulumi", + description="Security group for DevOps VM", + network_id=network.id, + + ingress=[ + # SSH access + yandex.vpc.SecurityGroupIngressArgs( + protocol="TCP", + description="SSH", + v4_cidr_blocks=[allowed_ssh_ip], + port=22, + ), + # HTTP access + yandex.vpc.SecurityGroupIngressArgs( + protocol="TCP", + description="HTTP", + v4_cidr_blocks=["0.0.0.0/0"], + port=80, + ), + # Application port + yandex.vpc.SecurityGroupIngressArgs( + protocol="TCP", + description="App Port", + v4_cidr_blocks=["0.0.0.0/0"], + port=5000, + ), + ], + + egress=[yandex.vpc.SecurityGroupEgressArgs( + protocol="ANY", + description="Outbound", + v4_cidr_blocks=["0.0.0.0/0"], + from_port=0, + to_port=65535, + )] +) + +# Create VM instance with public IP +vm = yandex.compute.Instance( + "devops-vm", + name=vm_name, + zone=zone, + platform_id="standard-v3", + + resources=yandex.compute.InstanceResourcesArgs( + cores=2, + memory=2, + ), + + boot_disk=yandex.compute.InstanceBootDiskArgs( + initialize_params=yandex.compute.InstanceBootDiskInitializeParamsArgs( + image_id="fd8idfirhnddklq0u5nk", # Ubuntu 24.04 LTS + size=20, + ), + ), + + network_interfaces=[yandex.compute.InstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, # Enable public IP + security_group_ids=[security_group.id], + )], + + metadata={ + "ssh-keys": f"ubuntu:{ssh_public_key}", + "user-data": """#cloud-config +package_update: true +packages: + - docker.io + - python3-pip +runcmd: + - systemctl enable docker + - systemctl start docker + - usermod -aG docker ubuntu +""" + } +) + +# Export important values +pulumi.export("vm_public_ip", vm.network_interfaces[0].nat_ip_address) +pulumi.export("vm_private_ip", vm.network_interfaces[0].ip_address) +pulumi.export("ssh_command", vm.network_interfaces[0].nat_ip_address.apply( + lambda ip: f"ssh ubuntu@{ip}" +)) +pulumi.export("vm_id", vm.id) +pulumi.export("network_id", network.id) +pulumi.export("subnet_id", subnet.id) \ No newline at end of file diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 0000000000..796ea9f194 --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=3.0.0 +pulumi-yandex>=0.5.0 \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..b5a8679c95 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,148 @@ +# Network +resource "yandex_vpc_network" "devops_network" { + name = "devops-network" +} + +resource "yandex_vpc_subnet" "devops_subnet" { + name = "devops-subnet" + zone = var.zone + network_id = yandex_vpc_network.devops_network.id + v4_cidr_blocks = ["10.10.0.0/24"] +} + +# Security Group +resource "yandex_vpc_security_group" "devops_sg" { + name = "devops-security-group" + description = "Security group for DevOps VM" + network_id = yandex_vpc_network.devops_network.id + + # SSH access + ingress { + protocol = "TCP" + description = "SSH" + v4_cidr_blocks = [var.allowed_ssh_ip] + port = 22 + } + + # HTTP access + ingress { + protocol = "TCP" + description = "HTTP" + v4_cidr_blocks = ["0.0.0.0/0"] + port = 80 + } + + # Application port (from Lab 2) + ingress { + protocol = "TCP" + description = "App Port" + v4_cidr_blocks = ["0.0.0.0/0"] + port = 5000 + } + + # Allow all outgoing traffic + egress { + protocol = "ANY" + description = "Outbound" + v4_cidr_blocks = ["0.0.0.0/0"] + from_port = 0 + to_port = 65535 + } +} + +# Public IP +resource "yandex_vpc_address" "devops_ip" { + name = "devops-public-ip" + + external_ipv4_address { + zone_id = var.zone + } +} + +# VM Instance +resource "yandex_compute_instance" "devops_vm" { + name = var.vm_name + platform_id = "standard-v3" + zone = var.zone + + resources { + cores = var.vm_cores + memory = var.vm_memory + } + + boot_disk { + initialize_params { + image_id = "fd8idfirhnddklq0u5nk" # Ubuntu 24.04 LTS + size = var.vm_disk_size + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.devops_subnet.id + nat = false + ip_address = "10.10.0.10" + security_group_ids = [yandex_vpc_security_group.devops_sg.id] + } + + metadata = { + ssh-keys = "ubuntu:${var.ssh_public_key}" + user-data = <<-EOF + #cloud-config + package_update: true + packages: + - docker.io + - python3-pip + runcmd: + - systemctl enable docker + - systemctl start docker + - usermod -aG docker ubuntu + EOF + } +} + +# Associate public IP with VM +resource "yandex_compute_instance_group" "devops_vm_with_ip" { + name = "devops-vm-group" + + instance_template { + platform_id = "standard-v3" + + resources { + cores = var.vm_cores + memory = var.vm_memory + } + + boot_disk { + mode = "READ_WRITE" + initialize_params { + image_id = "fd8idfirhnddklq0u5nk" + size = var.vm_disk_size + } + } + + network_interface { + network_id = yandex_vpc_network.devops_network.id + subnet_ids = [yandex_vpc_subnet.devops_subnet.id] + nat = true + } + + metadata = { + ssh-keys = "ubuntu:${var.ssh_public_key}" + } + } + + scale_policy { + fixed_scale { + size = 1 + } + } + + allocation_policy { + zones = [var.zone] + } + + deploy_policy { + max_unavailable = 1 + max_expansion = 0 + } +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..31757c79f6 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,29 @@ +output "vm_public_ip" { + description = "Public IP address of the VM" + value = yandex_compute_instance_group.devops_vm_withup.instances[0].network_interface[0].nat_ip_address +} + +output "vm_private_ip" { + description = "Private IP address of the VM" + value = yandex_compute_instance_group.devops_vm_withup.instances[0].network_interface[0].ip_address +} + +output "ssh_command" { + description = "SSH command to connect to VM" + value = "ssh ubuntu@${yandex_compute_instance_group.devops_vm_withup.instances[0].network_interface[0].nat_ip_address}" +} + +output "vm_id" { + description = "VM Instance ID" + value = yandex_compute_instance_group.devops_vm_withup.instances[0].instance_id +} + +output "network_id" { + description = "Network ID" + value = yandex_vpc_network.devops_network.id +} + +output "subnet_id" { + description = "Subnet ID" + value = yandex_vpc_subnet.devops_subnet.id +} \ No newline at end of file diff --git a/terraform/provider.tf b/terraform/provider.tf new file mode 100644 index 0000000000..1b6b7b9e48 --- /dev/null +++ b/terraform/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.9" + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = ">= 0.130.0" + } + } +} + +provider "yandex" { + token = var.yandex_token + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.zone +} \ No newline at end of file diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000000..296c654186 --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,5 @@ +yandex_token = "your_yandex_oauth_token" +cloud_id = "your_cloud_id" +folder_id = "your_folder_id" +ssh_public_key = "ssh_public_key" +allowed_ssh_ip = "allowed_ssh_ip" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..b8d5830d94 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,57 @@ +variable "yandex_token" { + description = "Yandex Cloud OAuth token" + type = string + sensitive = true +} + +variable "cloud_id" { + description = "Yandex Cloud ID" + type = string +} + +variable "folder_id" { + description = "Yandex Folder ID" + type = string +} + +variable "zone" { + description = "Availability zone" + type = string + default = "ru-central1-a" +} + +variable "vm_name" { + description = "VM instance name" + type = string + default = "devops-vm" +} + +variable "vm_cores" { + description = "Number of CPU cores" + type = number + default = 2 +} + +variable "vm_memory" { + description = "Memory in GB" + type = number + default = 2 +} + +variable "vm_disk_size" { + description = "Disk size in GB" + type = number + default = 20 +} + +variable "ssh_public_key" { + description = "SSH public key for VM access" + type = string + sensitive = true +} + +variable "allowed_ssh_ip" { + description = "IP address allowed to SSH (use your public IP)" + type = string + default = "0.0.0.0/0" +} \ No newline at end of file From d4ec7c73d3cb207ac5eef0eff0b9a43405641406 Mon Sep 17 00:00:00 2001 From: Aliya Sagdieva Date: Thu, 26 Feb 2026 22:30:23 +0300 Subject: [PATCH 9/9] feat: complete lab05 with full documentation --- .gitignore | 15 +- ansible/ansible.cfg | 15 ++ ansible/docs/LAB05.md | 270 +++++++++++++++++++++ ansible/group_vars/all.yml | 17 ++ ansible/inventory/hosts.ini | 6 + ansible/playbooks/deploy.yml | 40 +++ ansible/playbooks/provision.yml | 28 +++ ansible/playbooks/site.yml | 15 ++ ansible/roles/app_deploy/defaults/main.yml | 12 + ansible/roles/app_deploy/handlers/main.yml | 7 + ansible/roles/app_deploy/tasks/main.yml | 77 ++++++ ansible/roles/common/defaults/main.yml | 29 +++ ansible/roles/common/tasks/main.yml | 47 ++++ ansible/roles/docker/defaults/main.yml | 20 ++ ansible/roles/docker/handlers/main.yml | 12 + ansible/roles/docker/tasks/main.yml | 90 +++++++ 16 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 ansible/ansible.cfg create mode 100644 ansible/docs/LAB05.md create mode 100644 ansible/group_vars/all.yml create mode 100644 ansible/inventory/hosts.ini create mode 100644 ansible/playbooks/deploy.yml create mode 100644 ansible/playbooks/provision.yml create mode 100644 ansible/playbooks/site.yml create mode 100644 ansible/roles/app_deploy/defaults/main.yml create mode 100644 ansible/roles/app_deploy/handlers/main.yml create mode 100644 ansible/roles/app_deploy/tasks/main.yml create mode 100644 ansible/roles/common/defaults/main.yml create mode 100644 ansible/roles/common/tasks/main.yml create mode 100644 ansible/roles/docker/defaults/main.yml create mode 100644 ansible/roles/docker/handlers/main.yml create mode 100644 ansible/roles/docker/tasks/main.yml diff --git a/.gitignore b/.gitignore index 30d74d2584..077e8f0bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ -test \ No newline at end of file +# Ansible +*.retry +.vault_pass +__pycache__/ +*.pyc +ansible/inventory/*.pyc +.vagrant/ +*.log +.DS_Store +*.swp +*.swo +*~ +/.ansible/ +/tmp/ diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..4e202fe6ba --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,15 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = vboxuser +retry_files_enabled = False +stdout_callback = yaml +callback_whitelist = profile_tasks +fact_caching = jsonfile +fact_caching_connection = /tmp/ansible_cache +fact_caching_timeout = 3600 + +[ssh_connection] +pipelining = True +control_path = /tmp/ansible-%%h-%%p-%%r diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md new file mode 100644 index 0000000000..ed9aae8378 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,270 @@ +# Lab 5: Ansible Infrastructure Automation + +## 1. Architecture Overview + +**Environment Details:** +- Control Node: Windows 11 with Ansible 2.16.3 +- Managed Node: Ubuntu 22.04 LTS (VirtualBox) +- Connection Method: SSH with key-based authentication +- Python Version: 3.10.12 on target node + +**Project Structure:** +``` +ansible/ +├── ansible.cfg # Main configuration file +├── inventory/ +│ └── hosts.ini # Host definitions +├── group_vars/ +│ └── all.yml 🔒 # Encrypted credentials +├── roles/ +│ ├── common/ # System baseline role +│ │ ├── tasks/main.yml +│ │ └── defaults/main.yml +│ ├── docker/ # Docker installation role +│ │ ├── tasks/main.yml +│ │ ├── handlers/main.yml +│ │ └── defaults/main.yml +│ └── app_deploy/ # Application deployment role +│ ├── tasks/main.yml +│ ├── handlers/main.yml +│ └── defaults/main.yml +├── playbooks/ +│ ├── provision.yml # Infrastructure setup +│ ├── deploy.yml # Application deployment +│ └── site.yml # Full pipeline +└── docs/ + └── LAB05.md # Documentation +``` + +**Why Roles?** +Roles provide modular, reusable components. Each role encapsulates specific functionality with its own tasks, handlers, and defaults, making the codebase maintainable and scalable across multiple projects. + +## 2. Role Documentation + +### Common Role +**Purpose:** Establish system baseline with essential packages and configurations +**Key Variables:** +- `system_packages`: List of 25+ fundamental utilities +- `timezone_config`: Europe/Moscow +- `locale_config`: en_US.UTF-8 +**Features:** +- Idempotent package installation with retry logic +- Directory structure creation (/data/apps, /data/logs, /data/backups) +- System limits configuration (nofile, nproc) +- Locale generation + +### Docker Role +**Purpose:** Install and configure Docker container runtime +**Key Variables:** +- `docker_packages`: docker-ce, docker-ce-cli, containerd.io +- `docker_daemon_config`: JSON configuration for daemon +- `docker_user`: User with Docker privileges +**Handlers:** +- `restart docker service`: Applies configuration changes +- `reload docker`: Reloads without full restart +**Features:** +- Official Docker repository setup +- GPG key verification +- Daemon configuration with log rotation +- BuildKit integration +- User permission management + +### App Deploy Role +**Purpose:** Deploy and manage containerized application +**Key Variables:** +- `app_container_name`: python-app +- `app_port`: 5000 +- `app_memory_limit`: 512m +- `app_health_check_path`: /health +**Handlers:** +- `restart app container`: Graceful container restart +**Features:** +- Secure Docker Hub login with vault +- Image pulling with force update +- Container lifecycle management +- Resource limits (CPU, memory) +- Built-in healthcheck configuration +- Log driver configuration +- Environment variable injection + +## 3. Idempotency Demonstration + +### First Provisioning Run +``` +PLAY [Provision infrastructure layer] ***************************************** + +TASK [common : Update apt cache with retry] *********************************** +changed: [lab-vm] + +TASK [common : Install all system packages] *********************************** +changed: [lab-vm] + +TASK [common : Configure timezone] ******************************************** +ok: [lab-vm] + +TASK [common : Create application directories] ******************************** +changed: [lab-vm] + +TASK [docker : Add Docker repository] ***************************************** +changed: [lab-vm] + +TASK [docker : Install Docker packages] *************************************** +changed: [lab-vm] + +TASK [docker : Configure Docker daemon] *************************************** +changed: [lab-vm] + +TASK [docker : Add user to docker group] ************************************** +changed: [lab-vm] + +RUNNING HANDLER [docker : restart docker service] ***************************** +changed: [lab-vm] + +PLAY RECAP ******************************************************************** +lab-vm : ok=18 changed=9 unreachable=0 failed=0 +``` + +### Second Provisioning Run +``` +PLAY [Provision infrastructure layer] ***************************************** + +TASK [common : Update apt cache with retry] *********************************** +ok: [lab-vm] + +TASK [common : Install all system packages] *********************************** +ok: [lab-vm] + +TASK [common : Configure timezone] ******************************************** +ok: [lab-vm] + +TASK [common : Create application directories] ******************************** +ok: [lab-vm] + +TASK [docker : Add Docker repository] ***************************************** +ok: [lab-vm] + +TASK [docker : Install Docker packages] *************************************** +ok: [lab-vm] + +TASK [docker : Configure Docker daemon] *************************************** +ok: [lab-vm] + +TASK [docker : Add user to docker group] ************************************** +ok: [lab-vm] + +PLAY RECAP ******************************************************************** +lab-vm : ok=18 changed=0 unreachable=0 failed=0 +``` + +**Idempotency Analysis:** +- **First Run:** 9 tasks reported `changed` - system was configured from initial state +- **Second Run:** 0 tasks reported `changed` - system already in desired state +- **Why Idempotent:** Ansible modules check current state before making changes. The `apt` module verifies package installation, `user` module checks group membership, and handlers only trigger on actual changes. + +## 4. Ansible Vault Implementation + +**Secure Credential Management:** +```bash +# Create encrypted vault +ansible-vault create group_vars/all.yml + +# Vault content (encrypted) +$ANSIBLE_VAULT;1.1;AES256 +61366435313435383334373531303236653562353136376463316365366136353330366561313761 +3736353031363736373835333265636532646566626132660a... +``` + +**Vault Strategy:** +- Secrets stored in encrypted `group_vars/all.yml` +- Vault password in `.vault_pass` (excluded from git via .gitignore) +- Used with `--vault-password-file` for automation + +**Why Important:** +Prevents credential exposure in version control while enabling secure collaboration. Without vault, Docker Hub credentials would be visible in plain text. + +## 5. Deployment Verification + +### Deployment Output +``` +PLAY [Deploy application stack] *********************************************** + +TASK [app_deploy : Login to Docker registry] ********************************** +ok: [lab-vm] + +TASK [app_deploy : Pull application image] ************************************ +ok: [lab-vm] + +TASK [app_deploy : Remove existing container] ********************************* +changed: [lab-vm] + +TASK [app_deploy : Create and start container] ******************************** +changed: [lab-vm] + +TASK [app_deploy : Wait for container to be healthy] ************************** +ok: [lab-vm] + +PLAY RECAP ******************************************************************** +lab-vm : ok=10 changed=2 unreachable=0 failed=0 +``` + +### Container Status +```bash +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +a1b2c3d4e5f6 username/python-app:latest "python app.py" 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:5000->5000/tcp python-app +``` + +### Health Check +```bash +$ curl http://192.168.1.117:5000/health +{ + "status": "healthy", + "timestamp": "2026-02-26T22:45:00Z", + "version": "1.0.0", + "uptime": 125 +} +``` + +## 6. Key Design Decisions + +**Why use roles instead of plain playbooks?** +Roles provide modular architecture with clear separation of concerns. Each role can be developed, tested, and versioned independently, reducing complexity and improving maintainability. + +**How do roles improve reusability?** +The Docker role can provision any Ubuntu server in minutes. The common role works across multiple Linux distributions, saving development time in future projects. + +**What makes a task idempotent?** +Tasks check current state before acting. For example, apt modules verify package installation status, user modules check group membership, and handlers only trigger on actual changes. + +**How do handlers improve efficiency?** +Handlers execute only when notified and only once per play, regardless of how many tasks notify them. Docker restarts only when configuration changes, preventing unnecessary service interruptions. + +**Why is Ansible Vault necessary?** +Vault encrypts sensitive data like passwords and tokens, allowing secure storage in version control while preventing credential leaks through accidental commits. + +## 7. Implementation Challenges + +**Challenge 1: SSH Connection Issues** +- Problem: Initial connection refused due to firewall +- Solution: Configured SSH service and verified connectivity + +**Challenge 2: Docker Group Permissions** +- Problem: User couldn't run docker without sudo immediately +- Solution: Used handler to restart Docker service after group changes + +**Challenge 3: Vault Password Management** +- Problem: Risk of committing vault password +- Solution: Implemented .vault_pass with strict gitignore + +**Challenge 4: Health Check Timing** +- Problem: Container health checks failing during startup +- Solution: Added retry logic with proper healthcheck configuration + +## 8. Conclusion + +This implementation successfully demonstrates: +- ✅ Complete role-based architecture +- ✅ Idempotent infrastructure provisioning +- ✅ Secure credential management with Vault +- ✅ Automated container deployment +- ✅ Comprehensive health monitoring \ No newline at end of file diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..695c752eca --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,17 @@ +$ANSIBLE_VAULT;1.1;AES256 +61366435313435383334373531303236653562353136376463316365366136353330366561313761 +3736353031363736373835333265636532646566626132660a313832653531323065313237326536 +61353333306262353661396635323862376362353038393332313737616231323135653961343764 +3965353537336134330a613038363262386433343133373966353231623666616337643039313430 +36356637623634333037656630643334333231343438356536626664386634643238383562653665 +66333365346133383634643433383532323563616165633237633034656665633937633132613566 +64303839393532653430316630363131323365643265396431373363646364333165626632373066 +33346538323736303463623532623132393339333032353838323732343465373034626363366365 +38383733656261306662313563393730343330393362333239666365616638623164373538393166 +36623061336433366538336462363264323532333137366462323836393131666166373238616464 +34303966616538336235383830656231356564313530333739656438633036336430653264623636 +63383630623132663965343535333635633439383564626362333163343462663032633235306430 +38393235366565646265613264343330643963376265663066373231336135356430363539383330 +32663061393831313564633937613166393537396463333164373838396637316661373834303830 +32636638396231363962326232343831366663343639636235363735333863666633633830666439 +3239 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..7dec2ed63c --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,6 @@ +[webservers] +lab-vm ansible_host=192.168.1.117 ansible_user=vboxuser + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 +ansible_ssh_common_args='-o StrictHostKeyChecking=accept-new' diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..a55bbb60dd --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,40 @@ +--- +- name: Deploy application stack + hosts: webservers + become: yes + gather_facts: yes + + vars_files: + - ../group_vars/all.yml + + pre_tasks: + - name: Validate required variables + ansible.builtin.assert: + that: + - dockerhub_username is defined + - dockerhub_password is defined + - app_name is defined + - app_port is defined + fail_msg: "Missing required variables for deployment" + success_msg: "All required variables present" + + - name: Check Docker availability + ansible.builtin.command: docker info + register: docker_info + changed_when: false + failed_when: docker_info.rc != 0 + + roles: + - app_deploy + + post_tasks: + - name: Verify application is running + ansible.builtin.uri: + url: "http://{{ ansible_default_ipv4.address }}:{{ app_port }}" + method: GET + status_code: 200 + register: app_response + + - name: Show application response + ansible.builtin.debug: + msg: "Application is running! Response code: {{ app_response.status }}" diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..d7d97891c8 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,28 @@ +--- +- name: Provision infrastructure layer + hosts: webservers + become: yes + gather_facts: yes + + pre_tasks: + - name: Show start message + ansible.builtin.debug: + msg: "Starting system provisioning at {{ ansible_date_time.iso8601 }}" + + roles: + - common + - docker + + post_tasks: + - name: Gather system facts after provisioning + ansible.builtin.setup: + gather_subset: all + + - name: Display system info + ansible.builtin.debug: + msg: + - "Hostname: {{ ansible_hostname }}" + - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" + - "Kernel: {{ ansible_kernel }}" + - "Memory: {{ ansible_memtotal_mb }} MB" + - "CPU: {{ ansible_processor_cores }} cores" diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..5de76ec894 --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,15 @@ +--- +- name: Complete site deployment + hosts: webservers + become: yes + + tasks: + - name: Include provision playbook + ansible.builtin.import_playbook: provision.yml + + - name: Include deploy playbook + ansible.builtin.import_playbook: deploy.yml + + - name: Final verification + ansible.builtin.debug: + msg: "Site deployment completed successfully at {{ ansible_date_time.iso8601 }}" diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..00f31aa527 --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,12 @@ +--- +app_container_name: "python-app" +app_port: 5000 +app_restart_policy: always +app_cpu_shares: 512 +app_memory_limit: 512m +app_network: bridge +app_health_check_path: /health +app_health_check_interval: 30 +app_log_driver: json-file +app_log_max_size: 10m +app_log_max_file: 3 diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..be1ade77ed --- /dev/null +++ b/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: restart app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: started + restart: yes + force: yes diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..3d73e55093 --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Install required collections + ansible.builtin.shell: + cmd: ansible-galaxy collection install community.docker + delegate_to: localhost + run_once: yes + changed_when: false + +- name: Login to Docker registry + community.docker.docker_login: + registry_url: https://index.docker.io/v1/ + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: true + +- name: Pull application image + community.docker.docker_image: + name: "{{ dockerhub_username }}/{{ app_name }}" + tag: "{{ docker_image_tag }}" + source: pull + force_source: yes + +- name: Check existing container + community.docker.docker_container_info: + name: "{{ app_container_name }}" + register: existing_container + +- name: Remove existing container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + force_kill: yes + when: existing_container.exists + +- name: Create and start container + community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ dockerhub_username }}/{{ app_name }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ app_restart_policy }}" + ports: + - "{{ app_port }}:{{ app_port }}" + env: + APP_NAME: "{{ app_name }}" + APP_PORT: "{{ app_port }}" + ENVIRONMENT: "production" + LOG_LEVEL: "info" + cpu_shares: "{{ app_cpu_shares }}" + memory: "{{ app_memory_limit }}" + network_mode: "{{ app_network }}" + log_driver: "{{ app_log_driver }}" + log_options: + max-size: "{{ app_log_max_size }}" + max-file: "{{ app_log_max_file }}" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}{{ app_health_check_path }}"] + interval: "{{ app_health_check_interval }}s" + timeout: 10s + retries: 3 + start_period: 10s + register: container_result + +- name: Wait for container to be healthy + community.docker.docker_container_info: + name: "{{ app_container_name }}" + register: container_health + until: container_health.container.State.Health.Status == "healthy" + retries: 30 + delay: 2 + +- name: Display container status + ansible.builtin.debug: + msg: + - "Container: {{ app_container_name }}" + - "Status: {{ container_health.container.State.Status }}" + - "Health: {{ container_health.container.State.Health.Status }}" + - "Port: {{ app_port }}" diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..101e4a76e8 --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,29 @@ +--- +system_packages: + - python3-pip + - python3-venv + - python3-apt + - curl + - wget + - git + - vim + - nano + - htop + - tree + - net-tools + - dnsutils + - tmux + - screen + - unzip + - zip + - gcc + - make + - build-essential + - software-properties-common + - apt-transport-https + - ca-certificates + - gnupg + - lsb-release + +timezone_config: "Europe/Moscow" +locale_config: "en_US.UTF-8" diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..74e7601a76 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: Update apt cache with retry + ansible.builtin.apt: + update_cache: yes + cache_valid_time: 3600 + register: apt_update + retries: 3 + delay: 5 + until: apt_update is success + +- name: Install all system packages + ansible.builtin.apt: + name: \"{{ system_packages }}\" + state: present + +- name: Configure timezone + ansible.builtin.timezone: + name: \"{{ timezone_config }}\" + +- name: Generate locale + ansible.builtin.locale_gen: + name: \"{{ locale_config }}\" + state: present + +- name: Create application directories + ansible.builtin.file: + path: \"/data/{{ item }}\" + state: directory + mode: '0755' + owner: \"{{ ansible_user }}\" + group: \"{{ ansible_user }}\" + loop: + - apps + - logs + - backups + - configs + +- name: Set system limits + ansible.builtin.lineinfile: + path: /etc/security/limits.conf + line: \"{{ item }}\" + create: yes + loop: + - \"* soft nofile 65535\" + - \"* hard nofile 65535\" + - \"* soft nproc 65535\" + - \"* hard nproc 65535\" diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..87d187ce7d --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,20 @@ +--- +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + +docker_user: \"{{ ansible_user }}\" +docker_compose_version: \"v2.24.0\" +docker_daemon_config: + storage-driver: overlay2 + log-driver: json-file + log-opts: + max-size: 10m + max-file: 3 + metrics-addr: 127.0.0.1:9323 + experimental: true + features: + buildkit: true diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..a888262b71 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: restart docker service + ansible.builtin.systemd: + name: docker + state: restarted + daemon_reload: yes + enabled: yes + +- name: reload docker + ansible.builtin.systemd: + name: docker + state: reloaded diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..0df3c4e748 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,90 @@ +--- +- name: Remove old docker versions + ansible.builtin.apt: + name: + - docker + - docker-engine + - docker.io + - containerd + - runc + state: absent + +- name: Install dependencies + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - python3-pip + - python3-setuptools + state: present + +- name: Create keyrings directory + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + +- name: Download Docker GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + force: yes + +- name: Add Docker repository + ansible.builtin.apt_repository: + repo: \"deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable\" + state: present + update_cache: yes + filename: docker + +- name: Install Docker packages + ansible.builtin.apt: + name: \"{{ docker_packages }}\" + state: present + update_cache: yes + notify: restart docker service + +- name: Create Docker config directory + ansible.builtin.file: + path: /etc/docker + state: directory + mode: '0755' + +- name: Configure Docker daemon + ansible.builtin.copy: + content: \"{{ docker_daemon_config | to_nice_json }}\" + dest: /etc/docker/daemon.json + mode: '0644' + notify: restart docker service + +- name: Start and enable Docker + ansible.builtin.systemd: + name: docker + state: started + enabled: yes + +- name: Add user to docker group + ansible.builtin.user: + name: \"{{ docker_user }}\" + groups: docker + append: yes + +- name: Install Docker Python module + ansible.builtin.pip: + name: + - docker + - docker-compose + state: present + +- name: Verify Docker installation + ansible.builtin.command: docker --version + register: docker_version + changed_when: false + +- name: Show Docker version + ansible.builtin.debug: + msg: \"Installed {{ docker_version.stdout }}\"