diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..ac2a67a5d8 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,58 @@ +name: Python CI + +on: + push: + branches: [ master, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + pull_request: + paths: + - 'app_python/**' + +jobs: + test-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + pip install -r app_python/requirements.txt + pip install -r app_python/requirements-dev.txt + + - name: Lint + run: ruff check app_python + + - name: Run tests + run: pytest app_python/tests + + - name: Generate version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Login Docker + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Snyk Scan + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test --file=app_python/requirements.txt + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: ./app_python + push: true + tags: | + maksimmenshikh/devops-info-service:${{ env.VERSION }} + maksimmenshikh/devops-info-service:latest diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..58998276d6 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +venv/ +.venv/ +.git +.gitignore +README.md +docs/ +.env diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..ba1db0ed69 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..8573404bcc --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.13-slim + +RUN useradd -m appuser + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..e1613427b9 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,133 @@ +# DevOps Info Service + +A lightweight web service that provides detailed information about +itself and its runtime environment.\ + +------------------------------------------------------------------------ + +## Overview + +DevOps Info Service is a Python-based web application that exposes REST +API endpoints for retrieving: + +- Service metadata +- System information (OS, CPU, Python version, hostname) +- Runtime statistics (uptime, current time, timezone) +- Request details (client IP, HTTP method, user agent) + +The service is designed to be simple, extensible, and production-ready, +following best DevOps and software engineering practices. + +------------------------------------------------------------------------ + +## Prerequisites + +- Python **3.11+** +- pip +- Virtual environment support (`venv`) + +### Dependencies + +All dependencies are listed in `requirements.txt`: + +- Flask 3.1.0 + +------------------------------------------------------------------------ + +## Installation + +Create and activate a virtual environment, then install dependencies: + +``` bash +python -m venv venv +source venv/bin/activate # Linux / macOS +# venv\Scripts\activate # Windows +pip install -r requirements.txt +``` + +------------------------------------------------------------------------ + +## Running the Application + +Run the service using the default configuration: + +``` bash +python app.py +``` + +Run with custom configuration: + +``` bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +Once running, the service will be available at: + + http://localhost:5000 + +------------------------------------------------------------------------ + +## API Endpoints + +### GET / + +Returns detailed service, system, runtime, and request information. + +**Example:** + +``` bash +curl http://localhost:5000/ +``` + +------------------------------------------------------------------------ + +### GET /health + +Simple health-check endpoint used for monitoring and readiness probes. + +**Example:** + +``` bash +curl http://localhost:5000/health +``` + +------------------------------------------------------------------------ + +## Configuration + +The application is configured via environment variables: + + Variable Default Description + ---------- --------- ------------------------- + HOST 0.0.0.0 Server bind address + PORT 5000 Server listening port + DEBUG False Enable Flask debug mode + +**Example:** + +``` bash +HOST=127.0.0.1 PORT=8080 DEBUG=true python app.py +``` + +## Docker + +### Build Image + +docker build -t maksimmenshikh/devops-info-service . + +### Run Container + +docker run -p 5000:5000 maksimmenshikh/devops-info-service + +### Pull from Docker Hub + +docker pull maksimmenshikh/devops-info-service + +## Running Tests + +cd app_python +pip install -r requirements-dev.txt +pytest -v + +![CI](https://github.com/MMenshikh/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..e5155683e0 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,109 @@ +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Logging configuration +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application start time +START_TIME = datetime.now(timezone.utc) + + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + "seconds": seconds, + "human": f"{hours} hours, {minutes} minutes" + } + + +def get_system_info(): + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version() + } + + +@app.route("/") +def index(): + logger.info(f"Request: {request.method} {request.path}") + + uptime = get_uptime() + + return jsonify({ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": 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(): + uptime = get_uptime() + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime["seconds"] + }) + + +@app.errorhandler(404) +def not_found(error): + return jsonify({ + "error": "Not Found", + "message": "Endpoint does not exist" + }), 404 + + +@app.errorhandler(500) +def internal_error(error): + return jsonify({ + "error": "Internal Server Error", + "message": "An unexpected error occurred" + }), 500 + + +if __name__ == "__main__": + logger.info("Starting DevOps Info Service...") + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..c4bfd9e75a --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,229 @@ +# LAB01 --- DevOps Info Service + +## Framework Selection + +For this lab, **Flask 3.1.0** was selected as the web framework. + +### Why Flask? + +Flask is a lightweight, flexible, and beginner-friendly Python web +framework. It allows rapid development of REST APIs with minimal +boilerplate while still being powerful enough for production use. + +Key reasons for choosing Flask: - Simple and clean API - Minimal setup +and configuration - Large community and ecosystem - Easy integration +with Docker, CI/CD, and Kubernetes - Ideal for microservices and +DevOps-oriented projects + +### Comparison with Alternatives + + ------------------------------------------------------------------------------ + Framework Pros Cons Reason Not Chosen + ------------------- ---------------- ------------ ---------------------------- + Flask Simple, No built-in **Chosen** + lightweight, async, fewer + flexible built-ins + + FastAPI Async, auto More Overkill for current lab + OpenAPI docs, complex, + high performance async + complexity + + Django Full-featured, Heavy, Too heavy for microservice + ORM, admin panel complex, + monolithic + ------------------------------------------------------------------------------ + +Flask provides the perfect balance between simplicity and production +readiness for this lab. + +------------------------------------------------------------------------ + +## Best Practices Applied + +### 1. Clean Code Organization + +Functions were separated logically: - `get_system_info()` --- collects +system information - `get_uptime()` --- calculates service uptime - +Endpoint handlers contain minimal logic + +**Code Example:** + +``` python +def get_system_info(): + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version() + } +``` + +**Why Important:**\ +Improves readability, maintainability, and testability of the code. + +------------------------------------------------------------------------ + +### 2. Configuration via Environment Variables + +``` python +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" +``` + +**Why Important:**\ +Allows flexible configuration across different environments (local, +Docker, CI/CD, Kubernetes) without changing the code. + +------------------------------------------------------------------------ + +### 3. Logging + +``` python +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +``` + +**Why Important:**\ +Logging is critical for debugging, monitoring, and incident +investigation in production systems. + +------------------------------------------------------------------------ + +### 4. Error Handling + +``` python +@app.errorhandler(404) +def not_found(error): + return jsonify({ + "error": "Not Found", + "message": "Endpoint does not exist" + }), 404 +``` + +**Why Important:**\ +Provides predictable API behavior and improves client-side debugging. + +------------------------------------------------------------------------ + +## API Documentation + +### GET / + +Returns full service, system, runtime, and request information. + +**Request Example:** + +``` bash +curl http://localhost:5000/ +``` + +**Response Example:** + +``` json +{ + "service": {...}, + "system": {...}, + "runtime": {...}, + "request": {...}, + "endpoints": [...] +} +``` + +------------------------------------------------------------------------ + +### GET /health + +Health-check endpoint used for monitoring and readiness probes. + +**Request Example:** + +``` bash +curl http://localhost:5000/health +``` + +**Response Example:** + +``` json +{ + "status": "healthy", + "timestamp": "...", + "uptime_seconds": 120 +} +``` + +------------------------------------------------------------------------ + +## Testing Evidence + +The following screenshots demonstrate the correct functioning of the +application: + +- Main endpoint (`/`) returning full JSON output +- Health check endpoint (`/health`) returning service health +- Pretty-printed formatted JSON output + +Screenshots are available in: + + docs/screenshots/ + +------------------------------------------------------------------------ + +## Challenges & Solutions + +### Challenge 1 --- JSON Structure Consistency + +**Problem:**\ +Ensuring that the returned JSON exactly matches the required structure. + +**Solution:**\ +Careful comparison with the provided specification and manual testing +using browser and curl to validate field names and structure. + +------------------------------------------------------------------------ + +### Challenge 2 --- Uptime Calculation + +**Problem:**\ +Accurately calculating service uptime since application start. + +**Solution:**\ +Saved application start time using `datetime.now(timezone.utc)` and +calculated the difference on every request. + +------------------------------------------------------------------------ + +### Challenge 3 --- Cross-platform Compatibility + +**Problem:**\ +Ensuring the application works consistently on Windows, Linux, and +macOS. + +**Solution:**\ +Used only standard Python libraries (`platform`, `socket`, `os`) which +are fully cross-platform. + +------------------------------------------------------------------------ + +## GitHub Community + +Starring repositories helps support open-source developers and improves +project visibility, encouraging community contributions. + +Following developers and classmates helps build professional +connections, discover new tools and projects, and learn from others' +experience, which is essential for growth in software engineering and +DevOps. + +------------------------------------------------------------------------ + +## Conclusion + +This lab established a solid foundation for future DevOps tasks +including containerization, CI/CD pipelines, monitoring, and Kubernetes +deployments. The implemented service follows clean code principles, +production-ready practices, and proper documentation standards. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..995a9de44c --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,70 @@ +# LAB02 — Docker Containerization + +## Docker Best Practices Applied + +### Non-root User +Container runs as non-root user for better security and reduced attack surface. + +### Layer Caching +requirements.txt copied before application code to cache dependency installation layers. + +### Minimal Base Image +python:3.13-slim chosen for smaller image size and reduced vulnerabilities. + +### .dockerignore +Unnecessary files excluded from build context to reduce image size and speed up builds. + +--- + +## Image Information & Decisions + +Base Image: python:3.13-slim +Reason: lightweight, secure, production-friendly. + +Estimated Final Image Size: ~150-200MB depending on system. + +Layer Structure: +1. Base image +2. Dependencies installation +3. Application copy +4. Runtime execution + +--- + +## Build & Run Process + +### Build + +docker build -t devops-info-service . + +### Run + +docker run -p 5000:5000 devops-info-service + +### Test + +curl http://localhost:5000/ +curl http://localhost:5000/health + +Docker Hub URL: +https://hub.docker.com/r/maksimmenshikh/devops-info-service + +--- + +## Technical Analysis + +Layer order improves caching efficiency and reduces rebuild times. + +Running containers as non-root prevents privilege escalation risks. + +.dockerignore reduces build context size, speeding up builds. + +--- + +## Challenges & Solutions + +### Docker Engine Not Running +Solved by starting Docker Desktop. + +### Authorization Errors +Solved by using docker login before pulling or pushing images. diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..3939f0754c --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,93 @@ +# LAB03 --- CI/CD Pipeline with GitHub Actions + +## CI/CD Practices Applied + +### Automated Builds + +Docker image is automatically built on push and pull requests using +GitHub Actions. + +### Secure Secrets Management + +Sensitive data stored in GitHub Repository Secrets instead of hardcoding +credentials. + +### Container Security Scanning + +Snyk integrated into pipeline to scan Docker image for vulnerabilities. + +### Continuous Delivery + +Docker image automatically pushed to DockerHub after successful pipeline +execution. + +------------------------------------------------------------------------ + +## Pipeline Information & Decisions + +CI Platform: GitHub Actions\ +Container Registry: DockerHub\ +Security Scanner: Snyk + +Triggers: - push to main - pull requests + +Pipeline Stages: 1. Repository checkout 2. Docker image build 3. +Security scan with Snyk 4. DockerHub login 5. Image push to registry + +------------------------------------------------------------------------ + +## Workflow Execution Process + +### Automatic Trigger + +Pipeline runs when: + +- code is pushed +- pull request is created + +### Build Stage + +Docker image built using: + +docker build -t devops-info-service . + +### Security Scan + +Snyk scans Docker image for known vulnerabilities before publishing. + +### Push Stage + +Image pushed to: + +maksimmenshikh/devops-info-service + +------------------------------------------------------------------------ + +## Technical Analysis + +Automated CI reduces manual deployment steps and human error. + +Secrets stored in GitHub prevent credential exposure in repository code. + +Security scanning ensures vulnerabilities are detected early in +development. + +Automated publishing guarantees consistent container versions. + +------------------------------------------------------------------------ + +## Challenges & Solutions + +### Missing Secrets + +Pipeline failed due to missing repository secrets.\ +Solved by adding DOCKERHUB_TOKEN, DOCKERHUB_USERNAME and SNYK_TOKEN. + +### Docker Authentication Failure + +Fixed by generating DockerHub access token and configuring +docker/login-action. + +### Failed Security Scan + +Resolved by rebuilding image and ensuring dependencies were up to date. diff --git a/app_python/docs/LAB04.md b/app_python/docs/LAB04.md new file mode 100644 index 0000000000..e6f54084d2 --- /dev/null +++ b/app_python/docs/LAB04.md @@ -0,0 +1,177 @@ + +# LAB04 — Yandex Cloud Infrastructure with Pulumi + +## Goal +Create a simple infrastructure in Yandex Cloud using Pulumi (Python), including: + +- VPC network +- Subnet +- Security Group +- Virtual Machine with SSH access and application port + +## Prerequisites + +1. Yandex Cloud account. +2. Installed Pulumi and Python 3.10+. +3. Yandex Cloud CLI (`yc`). +4. Python virtual environment for the project: + +```bash +python -m venv venv +source venv/Scripts/activate +pip install --upgrade pip setuptools wheel +``` + +## 1. Service Account Setup + +1. Create a service account: + +```bash +yc iam service-account create --name lab04-sa +``` + +2. Create a service account key: + +```bash +yc iam key create --service-account-name lab04-sa --output authorized_key.json +``` + +3. Ensure the key contains `private_key` and `public_key`. + +4. Assign folder access to the service account via web console: **Folder → Access Management → Add binding → Role: Editor + Security Admin → Subject: lab04-sa**. + +## 2. Pulumi Project Setup + +1. Initialize the project: + +```bash +pulumi new python +``` + +- Name: `lab04` +- Stack: `dev` + +2. Install Yandex Pulumi provider: + +```bash +pip install pulumi_yandex +``` + +## 3. Project Structure + +``` +pulumi/ +├─ __main__.py +├─ venv/ +├─ Pulumi.yaml +└─ Pulumi.dev.yaml +``` + +## 4. Example `__main__.py` + +```python +import pulumi +from pulumi_yandex import Provider +from pulumi_yandex.vpc import Network, SecurityGroup, Subnet +from pulumi_yandex.compute import Instance, boot_disk, resources + +yc_provider = Provider("yc", + service_account_key_file="authorized_key.json", + cloud_id="your_cloud_id", + folder_id="b1gp20cgg1ivu6s502bu", + zone="ru-central1-a" +) + +network = Network("lab04-network", + name="lab04-network", + opts=pulumi.ResourceOptions(provider=yc_provider) +) + +sg = SecurityGroup("lab04-sg", + network_id=network.id, + ingress=[{ + "protocol": "TCP", + "port": 22, + "v4_cidr_blocks": ["0.0.0.0/0"] + }, { + "protocol": "TCP", + "port": 5000, + "v4_cidr_blocks": ["0.0.0.0/0"] + }], + egress=[{ + "protocol": "ANY", + "v4_cidr_blocks": ["0.0.0.0/0"] + }], + opts=pulumi.ResourceOptions(provider=yc_provider) +) + +subnet = Subnet("lab04-subnet", + network_id=network.id, + v4_cidr_blocks=["10.5.0.0/24"], + zone="ru-central1-a", + opts=pulumi.ResourceOptions(provider=yc_provider) +) + +vm = Instance("lab04-vm", + resources=resources.ResourcesArgs( + cores=2, + memory=2 + ), + boot_disk=boot_disk.BootDiskArgs( + initialize_params=boot_disk.InitializeParamsArgs( + image_id="fd8t1v7d1qsb9j3c2g4i", + size=20 + ) + ), + network_interfaces=[{ + "subnet_id": subnet.id, + "nat": True, + "security_group_ids": [sg.id] + }], + metadata={ + "ssh-keys": "ubuntu:" + open("id_rsa.pub").read() + }, + opts=pulumi.ResourceOptions(provider=yc_provider) +) + +pulumi.export("vm_ip", vm.network_interfaces[0].primary_v4_address) +``` + +## 5. Generate SSH Keys + +```bash +ssh-keygen -t rsa -b 2048 -f id_rsa +``` + +- `id_rsa` — private key +- `id_rsa.pub` — public key used in Pulumi + +## 6. Deploy Infrastructure + +```bash +source venv/Scripts/activate +winpty pulumi up +``` + +Confirm with `yes`. + +## 7. Verify + +```bash +ssh -i id_rsa ubuntu@ +``` + +Check that port 5000 is accessible. + +## 8. Cleanup + +```bash +pulumi destroy +pulumi stack rm dev +``` + +## 9. Summary + +- Created VPC network, subnet, security group, and VM. +- Configured service account roles: editor + security-admin. +- Managed infrastructure with Pulumi (Python). diff --git a/app_python/docs/screenshots/Lab01/Lab01-formatted-output.png b/app_python/docs/screenshots/Lab01/Lab01-formatted-output.png new file mode 100644 index 0000000000..316e8e79cf Binary files /dev/null and b/app_python/docs/screenshots/Lab01/Lab01-formatted-output.png differ diff --git a/app_python/docs/screenshots/Lab01/Lab01-health-check.png b/app_python/docs/screenshots/Lab01/Lab01-health-check.png new file mode 100644 index 0000000000..5714267326 Binary files /dev/null and b/app_python/docs/screenshots/Lab01/Lab01-health-check.png differ diff --git a/app_python/docs/screenshots/Lab01/Lab01-main-endpoint.png b/app_python/docs/screenshots/Lab01/Lab01-main-endpoint.png new file mode 100644 index 0000000000..185b6bd888 Binary files /dev/null and b/app_python/docs/screenshots/Lab01/Lab01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/Lab02/Lab02-succesfull-build.png b/app_python/docs/screenshots/Lab02/Lab02-succesfull-build.png new file mode 100644 index 0000000000..ff014a7f09 Binary files /dev/null and b/app_python/docs/screenshots/Lab02/Lab02-succesfull-build.png differ diff --git a/app_python/docs/screenshots/Lab02/Lab02-succesfull-localhost.png b/app_python/docs/screenshots/Lab02/Lab02-succesfull-localhost.png new file mode 100644 index 0000000000..ddf5b79a8c Binary files /dev/null and b/app_python/docs/screenshots/Lab02/Lab02-succesfull-localhost.png differ diff --git a/app_python/docs/screenshots/Lab02/Lab02-succesfull-run.png b/app_python/docs/screenshots/Lab02/Lab02-succesfull-run.png new file mode 100644 index 0000000000..59c03d46a5 Binary files /dev/null and b/app_python/docs/screenshots/Lab02/Lab02-succesfull-run.png differ diff --git a/app_python/docs/screenshots/Lab03/tests-local-passed.png b/app_python/docs/screenshots/Lab03/tests-local-passed.png new file mode 100644 index 0000000000..df8a49a77e Binary files /dev/null and b/app_python/docs/screenshots/Lab03/tests-local-passed.png differ diff --git a/app_python/docs/screenshots/Lab04/Pulumi_up_success.png b/app_python/docs/screenshots/Lab04/Pulumi_up_success.png new file mode 100644 index 0000000000..8ac96e64a1 Binary files /dev/null and b/app_python/docs/screenshots/Lab04/Pulumi_up_success.png differ diff --git a/app_python/docs/screenshots/Lab04/Terraform_destroy.png b/app_python/docs/screenshots/Lab04/Terraform_destroy.png new file mode 100644 index 0000000000..677fcaf056 Binary files /dev/null and b/app_python/docs/screenshots/Lab04/Terraform_destroy.png differ diff --git a/app_python/docs/screenshots/Lab04/Terraform_success.png b/app_python/docs/screenshots/Lab04/Terraform_success.png new file mode 100644 index 0000000000..a8ba19d894 Binary files /dev/null and b/app_python/docs/screenshots/Lab04/Terraform_success.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..eebbc87319 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.3.5 +pytest-cov==5.0.0 +ruff==0.9.0 diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..78180a1ad1 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 \ 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 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..a4fe2af9d7 --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,40 @@ +import pytest +from app import app + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +def test_index_ok(client): + response = client.get("/") + assert response.status_code == 200 + + data = response.get_json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + +def test_health_ok(client): + response = client.get("/health") + assert response.status_code == 200 + + data = response.get_json() + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + + +def test_404(client): + response = client.get("/unknown") + assert response.status_code == 404 + + data = response.get_json() + assert data["error"] == "Not Found" diff --git a/pulumi/.gitignore b/pulumi/.gitignore new file mode 100644 index 0000000000..a3807e5bdb --- /dev/null +++ b/pulumi/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..35378e7f2a --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,11 @@ +name: lab04 +description: lab04 devops +runtime: + name: python + options: + toolchain: pip + virtualenv: venv +config: + pulumi:tags: + value: + pulumi:template: python diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 0000000000..bd0ca381aa --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,47 @@ +import pulumi +from pulumi_yandex import ComputeInstance, VpcNetwork, VpcSubnet, VpcSecurityGroup +from pulumi_yandex import Provider + +yc_provider = Provider("yc", + service_account_key_file="authorized_key.json", + cloud_id="b1gu4hpr6n728hvsq2uu", + folder_id="b1gp20cgg1ivu6s502bu", + zone="ru-central1-a" + ) + +network = VpcNetwork( + "lab04-network", opts=pulumi.ResourceOptions(provider=yc_provider)) + +subnet = VpcSubnet("lab04-subnet", + network_id=network.id, + v4_cidr_blocks=["10.5.0.0/24"], + zone="ru-central1-a", + opts=pulumi.ResourceOptions(provider=yc_provider) + ) + +sg = VpcSecurityGroup("lab04-sg", + network_id=network.id, + ingresses=[ # <-- было ingress + {"protocol": "TCP", "description": "SSH", + "port": 22, "v4_cidr_blocks": ["0.0.0.0/0"]}, + {"protocol": "TCP", "description": "App", + "port": 5000, "v4_cidr_blocks": ["0.0.0.0/0"]} + ], + egresses=[ # <-- было egress + {"protocol": "ANY", "description": "Allow all outbound", + "v4_cidr_blocks": ["0.0.0.0/0"]} + ], + opts=pulumi.ResourceOptions(provider=yc_provider) + ) + +vm = ComputeInstance("lab04-vm", + platform_id="standard-v1", + resources={"cores": 2, "memory": 2}, + boot_disk={"initialize_params": { + "image_id": "fd87ce1b8tgh9b", "size": 20}}, + network_interfaces=[{"subnet_id": subnet.id, + "nat": True, "security_group_ids": [sg.id]}], + metadata={ + "ssh-keys": "ubuntu:YOUR_SSH_PUBLIC_KEY_CONTENT"}, + opts=pulumi.ResourceOptions(provider=yc_provider) + ) diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 0000000000..bc4e43087b --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1 @@ +pulumi>=3.0.0,<4.0.0 diff --git a/pulumi/setuptools-33.1.1.zip b/pulumi/setuptools-33.1.1.zip new file mode 100644 index 0000000000..9f4eb152ca Binary files /dev/null and b/pulumi/setuptools-33.1.1.zip differ diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..5d5912768e --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,5 @@ +*.tfstate +*.tfstate.* +.terraform/ +terraform.tfvars +authorized_key.json \ No newline at end of file diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..70d79eb192 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/yandex-cloud/yandex" { + version = "0.189.0" + hashes = [ + "h1:/kpsKwRq8DNLCDl7JkUdWkp5sMF0czjk8iEzSgBc9Uk=", + "zh:0c0a58871a70965b144db9d23911381bd9647c5ee5a299df10e05e5d687ff899", + "zh:135cd1abfc1a188ee27c72a9d74ab67cb93b6dcdee7264a00550fc4be32aa28d", + "zh:27541643161ab763b272dbb083f3f8d3ba263b75e9389f045f29af3d24718902", + "zh:4a84c32b2087a99c1cde58c122ef02770a4924f4594815bd9541474e6a07fd0b", + "zh:63a26ef01e94532b50007626f5fd9d3297fbfe479e0d8ca5dcfbc20e9e9963ab", + "zh:6ace1564b2d8b7d1564393b97cf1c7e53449df007f5e71ead0697fec7e6bbf0a", + "zh:8d650e980067c4a383f98dbdf4404ba55e56be2810eb5d0e2c1aa59512bdc1de", + "zh:976857feb3dc7f3e652cd80175fd3834b25870fe6bf9b8c8611d74585d2b5d74", + "zh:9afdfa3c67823df597d1680fc0589daef325f696294131c1f883dfcc086bc745", + "zh:c5a126adf22573c6307314f1809cf5ba6f925a6ff740adc2a3614d7ea54f4c0d", + "zh:cbc26b46b9efeb29d504bc95a00ac307934b3215f9683602d57880edbecbc0d0", + "zh:e6a5ec6ef7820906513972cc4f5851d1c57536c42953207e29bb80177710fd63", + "zh:f384ffe3f476ffb368232cc8d2880860461e3a73767354ef48a9ba084c78c452", + ] +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..9165314876 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,77 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } +} + +provider "yandex" { + service_account_key_file = "authorized_key.json" + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.zone +} + +resource "yandex_vpc_network" "network" { + name = "lab04-network" +} + +resource "yandex_vpc_subnet" "subnet" { + name = "lab04-subnet" + zone = var.zone + network_id = yandex_vpc_network.network.id + v4_cidr_blocks = ["10.5.0.0/24"] +} + +resource "yandex_vpc_security_group" "vm_sg" { + name = "lab04-sg" + network_id = yandex_vpc_network.network.id + + ingress { + protocol = "TCP" + description = "SSH" + port = 22 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "TCP" + description = "App" + port = 5000 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + egress { + protocol = "ANY" + description = "Allow all outbound" + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "yandex_compute_instance" "vm" { + name = var.vm_name + platform_id = var.platform_id + + resources { + cores = var.cores + memory = var.memory + } + + boot_disk { + initialize_params { + image_id = var.image_id + size = 20 + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet.id + nat = true + security_group_ids = [yandex_vpc_security_group.vm_sg.id] + } + + metadata = { + ssh-keys = "ubuntu:${file(var.ssh_public_key_path)}" + } +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..6d25d6979a --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "external_ip" { + value = yandex_compute_instance.vm.network_interface[0].nat_ip_address +} \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..7e6bc9fbf2 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,32 @@ +variable "cloud_id" {} +variable "folder_id" {} + +variable "zone" { + default = "ru-central1-a" +} + +variable "vm_name" { + default = "lab04-vm" +} + +variable "platform_id" { + default = "standard-v1" +} + +variable "cores" { + default = 2 +} + +variable "memory" { + default = 2 +} + +variable "image_id" { + description = "Ubuntu 22.04 LTS image ID" + type = string + default = "fd817i7o8012578061ra" +} + +variable "ssh_public_key_path" { + default = "~/.ssh/id_rsa.pub" +} \ No newline at end of file