diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml
new file mode 100644
index 0000000000..f6b67b08ff
--- /dev/null
+++ b/.github/workflows/ansible-deploy.yml
@@ -0,0 +1,86 @@
+name: Ansible Deployment
+
+on:
+ push:
+ branches: [ master, lab06 ]
+ paths:
+ - 'ansible/**'
+ - '.github/workflows/ansible-deploy.yml'
+ pull_request:
+ branches: [ master, lab06 ]
+ paths:
+ - 'ansible/**'
+ - '.github/workflows/ansible-deploy.yml'
+ workflow_dispatch: # Позволяет запускать вручную
+
+jobs:
+ lint:
+ name: Ansible Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install Ansible and ansible-lint
+ run: |
+ pip install ansible ansible-lint
+
+ - name: Run ansible-lint
+ run: |
+ cd ansible
+ ansible-lint playbooks/*.yml roles/*/tasks/*.yml || true
+ continue-on-error: true
+
+ deploy:
+ name: Deploy Application
+ needs: lint
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install Ansible and dependencies
+ run: |
+ pip install ansible
+ ansible-galaxy collection install community.docker
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
+ chmod 600 ~/.ssh/id_rsa
+ ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts
+
+ - name: Test SSH connection
+ run: |
+ ssh -i ~/.ssh/id_rsa ${{ secrets.VM_USER }}@${{ secrets.VM_HOST }} "echo 'SSH connection successful'"
+
+ - name: Deploy with Ansible
+ env:
+ ANSIBLE_HOST_KEY_CHECKING: 'False'
+ run: |
+ cd ansible
+ echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
+ ansible-playbook playbooks/deploy.yml \
+ -i inventory/hosts.ini \
+ --vault-password-file /tmp/vault_pass
+ rm /tmp/vault_pass
+
+ - name: Verify deployment
+ run: |
+ sleep 10
+ curl -f http://${{ secrets.VM_HOST }}:5000/health || exit 1
+ echo "✅ Application is healthy!"
\ No newline at end of file
diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
new file mode 100644
index 0000000000..4bdf2edf8a
--- /dev/null
+++ b/.github/workflows/python-ci.yml
@@ -0,0 +1,86 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ - lab03
+ paths:
+ - 'app_python/**'
+ - '.github/workflows/python-ci.yml'
+ pull_request:
+ branches:
+ - master
+ paths:
+ - 'app_python/**'
+ - '.github/workflows/python-ci.yml'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up 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: Run linter
+ run: |
+ cd app_python
+ flake8 app.py
+
+ - name: Run tests
+ run: |
+ cd app_python
+ pytest -v
+
+ - name: Install Snyk CLI
+ run: |
+ npm install -g snyk
+
+ - name: Authenticate Snyk
+ run: |
+ snyk auth ${{ secrets.SYNK_TOKEN }}
+
+ - name: Run Snyk security scan
+ run: |
+ cd app_python
+ snyk test --severity-threshold=high
+
+
+
+
+ docker:
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Generate version
+ run: echo "VERSION=$(date +%Y.%m)" >> $GITHUB_ENV
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: app_python
+ push: true
+ tags: |
+ ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }}
+ ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest
diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml
new file mode 100644
index 0000000000..8770390c10
--- /dev/null
+++ b/.github/workflows/terraform-ci.yml
@@ -0,0 +1,124 @@
+name: Terraform CI/CD
+
+on:
+ push:
+ branches:
+ - master
+ - lab04
+ paths:
+ - 'terraform/**'
+ - '.github/workflows/terraform-ci.yml'
+ pull_request:
+ branches:
+ - master
+ - lab04
+ paths:
+ - 'terraform/**'
+ - '.github/workflows/terraform-ci.yml'
+
+jobs:
+ terraform-validation:
+ name: Terraform Validation
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: ./terraform
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v3
+ with:
+ terraform_version: latest
+
+ - name: Terraform Format Check
+ id: fmt
+ run: terraform fmt -check -recursive
+ continue-on-error: true
+
+ - name: Terraform Init
+ id: init
+ run: terraform init -backend=false
+
+ - name: Terraform Validate
+ id: validate
+ run: terraform validate -no-color
+
+ - name: Setup TFLint
+ uses: terraform-linters/setup-tflint@v4
+ with:
+ tflint_version: v0.61.0
+
+ - name: Show TFLint version
+ run: tflint --version
+
+ - name: Initialize TFLint
+ run: tflint --init
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Run TFLint
+ id: tflint
+ run: tflint --format compact --recursive
+ continue-on-error: true
+
+ - name: Comment PR with Results
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const output = `#### Terraform CI Results 🔍
+
+ | Check | Status |
+ |-------|--------|
+ | **Terraform Format** | \`${{ steps.fmt.outcome }}\` |
+ | **Terraform Init** | \`${{ steps.init.outcome }}\` |
+ | **Terraform Validate** | \`${{ steps.validate.outcome }}\` |
+ | **TFLint** | \`${{ steps.tflint.outcome }}\` |
+
+ 📋 Show Details
+
+ #### Terraform Format Check
+ ${{ steps.fmt.outcome == 'success' && '✅ All files are properly formatted' || '❌ Some files need formatting. Run: `terraform fmt -recursive`' }}
+
+ #### Terraform Init
+ ${{ steps.init.outcome == 'success' && '✅ Initialization successful' || '❌ Initialization failed' }}
+
+ #### Terraform Validate
+ ${{ steps.validate.outcome == 'success' && '✅ Configuration is valid' || '❌ Configuration has syntax errors' }}
+
+ #### TFLint
+ ${{ steps.tflint.outcome == 'success' && '✅ No linting issues found' || '⚠️ Linting issues detected (see logs)' }}
+
+
+
+ ---
+ *Pusher: @${{ github.actor }} | Workflow: \`${{ github.workflow }}\`*`;
+
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: output
+ });
+
+
+ - name: Fail workflow if critical validation fails
+ if: steps.init.outcome == 'failure' || steps.validate.outcome == 'failure'
+ run: |
+ echo "❌ Critical validation failed!"
+ echo "Init status: ${{ steps.init.outcome }}"
+ echo "Validate status: ${{ steps.validate.outcome }}"
+ exit 1
+
+ - name: Warning if format or lint fails
+ if: steps.fmt.outcome == 'failure' || steps.tflint.outcome == 'failure'
+ run: |
+ echo "⚠️ Non-critical checks failed (format or lint)"
+ echo "Format status: ${{ steps.fmt.outcome }}"
+ echo "TFLint status: ${{ steps.tflint.outcome }}"
+ echo "Please fix these issues, but workflow will not fail."
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 30d74d2584..30c3d2285a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,19 @@
-test
\ No newline at end of file
+test
+
+# Terraform
+terraform/.terraform/
+terraform/.terraform.lock.hcl
+terraform/terraform.tfstate
+terraform/terraform.tfstate.backup
+terraform/key.json
+terraform/*.tfvars
+
+# Pulumi
+.pulumi/
+venv/
+__pycache__/
+*.pyc
+.env
+key.json
+Pulumi.*.yaml
+!Pulumi.yaml
\ No newline at end of file
diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg
new file mode 100644
index 0000000000..0b4a898088
--- /dev/null
+++ b/ansible/ansible.cfg
@@ -0,0 +1,11 @@
+[defaults]
+inventory = inventory/hosts.ini
+roles_path = roles
+host_key_checking = False
+retry_files_enabled = False
+remote_user = ubuntu
+
+[privilege_escalation]
+become = True
+become_method = sudo
+become_user = root
diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md
new file mode 100644
index 0000000000..b5036926fd
--- /dev/null
+++ b/ansible/docs/LAB05.md
@@ -0,0 +1,519 @@
+# LAB05 — Ansible Fundamentals
+
+**Author:** Sofia Palkina
+**Date:** 2026-02-26
+**Course:** DevOps Core Course
+
+---
+
+## 1. Architecture Overview
+
+### Ansible Version
+
+
+**Version:** `ansible [core 2.20.3]`
+**Python:** `3.12.3`
+**Platform:** Ubuntu 24.04 LTS (WSL2)
+
+---
+
+### Target VM Configuration
+
+| Parameter | Value |
+|-----------|-------|
+| **Cloud Provider** | Yandex Cloud |
+| **Provisioning Tool** | Terraform |
+| **OS** | Ubuntu 22.04 LTS |
+| **Public IP** | `89.169.158.252` |
+| **Internal IP** | `192.168.10.30` |
+| **SSH User** | `ubuntu` |
+| **Connection** | SSH with key authentication |
+
+---
+
+### Role Structure
+
+```
+ansible/
+├── ansible.cfg # Ansible configuration
+├── inventory/
+│ ├── hosts.ini # Static inventory
+│ └── group_vars/
+│ └── all.yml # Encrypted variables (Vault)
+├── roles/
+│ ├── common/ # System provisioning
+│ │ ├── tasks/
+│ │ │ └── main.yml
+│ │ └── defaults/
+│ │ └── main.yml
+│ ├── docker/ # Docker installation
+│ │ ├── tasks/
+│ │ │ └── main.yml
+│ │ ├── handlers/
+│ │ │ └── main.yml
+│ │ └── defaults/
+│ │ └── main.yml
+│ └── app_deploy/ # Application deployment
+│ ├── tasks/
+│ │ └── main.yml
+│ ├── handlers/
+│ │ └── main.yml
+│ └── defaults/
+│ └── main.yml
+├── playbooks/
+│ ├── provision.yml # System provisioning playbook
+│ └── deploy.yml # Application deployment playbook
+└── docs/
+ ├── LAB05.md # This documentation
+ └── screenshots_l5/ # Screenshots
+```
+
+---
+
+### Why Roles Instead of Monolithic Playbooks?
+
+**Roles provide:**
+
+1. **Modularity** — Each role has a single responsibility
+ - `common`: System packages
+ - `docker`: Docker installation
+ - `app_deploy`: Application deployment
+
+2. **Reusability** — Roles can be shared across projects
+ - Docker role works on any Ubuntu server
+ - No code duplication
+
+3. **Maintainability** — Changes isolated to specific roles
+ - Update Docker version in one place
+ - Clear separation of concerns
+
+4. **Testability** — Test roles independently
+ - Verify Docker installation separately
+ - Gradual deployment
+---
+
+## 2. Roles Documentation
+
+### Role: `common`
+
+#### Purpose
+
+Performs basic system provisioning for Ubuntu servers:
+- Updates apt package cache
+- Installs essential system packages
+- Configures timezone (optional)
+
+This role prepares any Ubuntu server for further automation by ensuring common tools are available.
+
+#### Tasks Implementation
+
+**File:** `roles/common/tasks/main.yml`
+
+
+#### Variables
+
+**File:** `roles/common/defaults/main.yml`
+
+
+**Variable Explanation:**
+- `common_packages`: List of essential packages
+- Can be overridden in inventory or playbook
+
+#### Handlers
+
+None required — package installation doesn't need service restarts.
+
+#### Dependencies
+
+None — this is typically the first role executed.
+
+#### Idempotency
+
+- `apt: state=present` — installs only if missing
+- `update_cache` with `cache_valid_time` — caches for 1 hour
+- Multiple runs don't change system if packages exist
+
+---
+
+### Role: `docker`
+
+#### Purpose
+
+Installs and configures Docker Engine on Ubuntu:
+1. Adds Docker official GPG key
+2. Adds Docker APT repository
+3. Installs Docker CE packages
+4. Configures Docker service (enable, start)
+5. Adds user to `docker` group
+6. Installs Python Docker SDK for Ansible modules
+
+#### Tasks Implementation
+
+**File:** `roles/docker/tasks/main.yml`
+
+#### Variables
+
+**File:** `roles/docker/defaults/main.yml`
+
+**Variable Explanation:**
+- `docker_user`: User to add to docker group (enables non-root Docker commands)
+
+#### Handlers
+
+**File:** `roles/docker/handlers/main.yml`
+
+**Handler Explanation:**
+- Triggered when Docker repository is added
+- Ensures Docker service uses new configuration
+- Only runs when needed (efficiency!)
+
+#### Dependencies
+
+Should run after `common` role (requires `curl`, `apt-transport-https`).
+
+#### Idempotency
+
+- `apt_key: state=present` — adds key only if missing
+- `apt_repository: state=present` — adds repo only if missing
+- `service: state=started` — starts only if stopped
+- `user: append=yes` — adds to group without removing other groups
+
+---
+
+### Role: `app_deploy`
+
+#### Purpose
+
+Deploys containerized Python application from Docker Hub:
+1. Authenticates with Docker Hub (using Vault credentials)
+2. Pulls latest Docker image
+3. Stops and removes old container (if exists)
+4. Runs new container with proper configuration
+5. Verifies deployment health
+
+#### Tasks Implementation
+
+**File:** `roles/app_deploy/tasks/main.yml`
+
+#### Variables
+
+**Encrypted Variables** (from Vault):
+
+**File:** `inventory/group_vars/all.yml` (encrypted)
+
+```yaml
+---
+# Docker Hub credentials
+dockerhub_username: spalkkina
+dockerhub_password:
+
+# Application configuration
+app_name: devops-info-service
+docker_image: "{{ dockerhub_username }}/{{ app_name }}"
+docker_image_tag: "1.0"
+app_port: 5000
+app_port_2: 6000
+app_container_name: "{{ app_name }}"
+```
+
+**Default Variables:**
+
+**File:** `roles/app_deploy/defaults/main.yml`
+
+**Variable Explanation:**
+- `dockerhub_username/password`: Docker Hub authentication (from Vault)
+- `docker_image`: Full image name
+- `docker_image_tag`: Image version to deploy
+- `app_port`: Host port (external)
+- `app_port_2`: Container port
+- `docker_restart_policy`: `unless-stopped` (auto-restart on reboot)
+- `app_environment_vars`: Custom environment variables (empty by default)
+
+#### Handlers
+
+**File:** `roles/app_deploy/handlers/main.yml`
+
+**Handler Explanation:**
+- Restarts container when configuration changes
+- Used for config updates without redeployment
+
+#### Dependencies
+
+**Requires:**
+- `docker` role executed first
+- Docker daemon running
+- Python Docker SDK installed
+
+#### Security Considerations
+
+- `no_log: true` on Docker login (prevents credentials in logs)
+- Credentials stored in Ansible Vault
+- Vault password not committed to repository
+
+#### Idempotency
+
+- `docker_container: state=started` — starts only if not running
+- Image pull only downloads if newer version exists
+- Container recreated only if config changes
+
+---
+
+## 3. Idempotency Demonstration
+
+### Concept
+
+**Idempotency:** Running the same operation multiple times produces the same result.
+
+In Ansible: Re-running a playbook should only make changes if system state has drifted from desired state.
+
+---
+
+### First Run
+
+
+
+**Analysis:**
+- **9 tasks changed** (yellow)
+- System was in initial state
+- All packages installed
+- Docker service started
+- User added to group
+
+---
+
+### Second Run
+
+
+
+**Analysis:**
+- **0 tasks changed** (green "ok")
+- Desired state already achieved
+- No unnecessary operations
+- Idempotency
+
+---
+
+### What Changed First Time?
+
+| Task | Why Changed? |
+|------|--------------|
+| **Update apt cache** | Cache was outdated |
+| **Install packages** | Packages not installed |
+| **Add GPG key** | Key didn't exist |
+| **Add repository** | Repository not configured |
+| **Install Docker** | Docker not present |
+| **Start Docker service** | Service not running |
+| **Add user to group** | User not in docker group |
+| **Install Docker SDK** | Python package missing |
+
+---
+
+### Why Nothing Changed Second Time?
+
+Ansible **detected current state = desired state** for all resources:
+
+- Packages already installed (`state=present` satisfied)
+- Docker service already running (`state=started` satisfied)
+- User already in group (`groups: docker` satisfied)
+- Repository already exists
+
+**Key Insight:** Ansible compares current vs desired state **before** applying changes.
+
+---
+
+### What Makes Tasks Idempotent?
+
+**1. State-based modules:**
+```yaml
+apt:
+ name: docker-ce
+ state: present # ← Declarative, not imperative
+```
+
+**2. Service management:**
+```yaml
+service:
+ name: docker
+ state: started # ← Starts only if stopped
+```
+
+**3. User management:**
+```yaml
+user:
+ name: ubuntu
+ groups: docker
+ append: yes # ← Doesn't remove other groups
+```
+
+**4. Conditional operations:**
+```yaml
+apt:
+ update_cache: yes
+ cache_valid_time: 3600 # ← Skip if cache fresh
+```
+
+---
+
+## 4. Ansible Vault Usage
+
+### How Credentials Are Stored Securely
+
+**Encrypted file:**
+
+```
+inventory/group_vars/all.yml
+```
+
+**Created with:**
+
+```bash
+ansible-vault create inventory/group_vars/all.yml
+```
+
+**Encrypted content:**
+
+
+
+---
+
+### Vault Password Management Strategy
+
+**Password file:** `.vault_pass`
+
+```bash
+# Create password file (do ONCE)
+echo "your-secure-password" > .vault_pass
+chmod 600 .vault_pass
+```
+
+**Added to `.gitignore`:**
+
+```
+# Ansible
+.vault_pass
+*.retry
+__pycache__/
+```
+
+**Usage with playbooks:**
+
+**Option 1: Prompt for password**
+```bash
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+```
+
+---
+
+### Why Ansible Vault is Important
+
+1. **Prevents credential leaks** — No plaintext secrets in git
+2. **Safe version control** — Encrypted files can be committed
+3. **Team collaboration** — Share playbooks without exposing secrets
+4. **Compliance** — Meets security best practices
+5. **Audit trail** — Track changes to encrypted files
+
+---
+
+## 5. Deployment Verification
+
+### Deployment Execution
+
+
+
+
+### Container Status
+
+
+
+
+### Health Check and Main Endpoint Verification
+
+
+
+
+### Handler Execution
+
+Handlers were **not triggered** in this deployment because:
+- Container didn't exist before (no config change)
+- First deployment uses `state=started`
+
+**Handlers would trigger when:**
+- Updating image tag
+- Changing environment variables
+- Modifying port mappings
+
+---
+
+## 7. Key Decisions
+
+### Why use roles instead of plain playbooks?
+
+Roles provide **modular architecture** with clear **separation of concerns**. Each role has a single responsibility:
+- `common` → system packages
+- `docker` → Docker installation
+- `app_deploy` → application deployment
+
+
+### How do roles improve reusability?
+
+Roles **encapsulate logic and variables**, making them portable across projects:
+
+1. **Same role, different projects:**
+ - `docker` role works on any Ubuntu server
+ - No code duplication
+
+2. **Override variables per environment:**
+ ```yaml
+ # dev environment
+ docker_user: devuser
+
+ # prod environment
+ docker_user: ubuntu
+ ```
+
+3. **Share via Ansible Galaxy:**
+ - Publish roles for community
+ - Import roles from others
+
+4. **Version control roles independently:**
+ - Update docker role without touching app_deploy
+ - Clear change history
+
+---
+
+### What makes a task idempotent?
+
+A task is **idempotent** when:
+> Running it multiple times produces the same final state, regardless of initial state.
+
+
+### How do handlers improve efficiency?
+
+**Answer:**
+
+Handlers **execute only when notified** and **only once per playbook run**, preventing unnecessary service restarts:
+
+
+### Why is Ansible Vault necessary?
+
+Ansible Vault is **essential for secure credential (encrypted at rest) management**:
+
+---
+
+## 8. Challenges
+
+### Challenge 1: WSL2 File Permissions
+
+**Problem:**
+```
+[WARNING]: Ansible is being run in a world writable directory
+```
+
+**Cause:** Windows filesystem (`/mnt/c/`) has open permissions incompatible with Ansible security requirements.
+
+**Solution:** Copied project to WSL native filesystem:
+```bash
+cp -r /mnt/c/.../ansible ~/ansible-lab05
+```
+
+**Learning:** Always work in WSL native filesystem for Ansible projects.
diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md
new file mode 100644
index 0000000000..7d7f4a4104
--- /dev/null
+++ b/ansible/docs/LAB06.md
@@ -0,0 +1,295 @@
+# Lab 6: Advanced Ansible & CI/CD - Submission
+
+**Name:** Sofia Palkina
+**Date:** 2026-03-05
+
+## Task 1: Blocks & Tags
+
+
+### Tag Strategy
+
+**Available tags:**
+- `common` - entire common role
+- `packages` - package installation tasks
+- `config` - system configuration tasks
+- `docker` - entire docker role
+- `docker_install` - Docker installation only
+- `docker_config` - Docker configuration only
+
+### Testing Results
+
+**List all tags:**
+
+```bash
+ansible-playbook playbooks/provision.yml --list-tags --ask-vault-pass
+```
+![alt text]()
+
+
+**Selective execution with `--tags "packages"`:**
+
+![alt text]()
+
+**Result:** Only package installation tasks executed.
+
+**Selective execution with `--tags "docker_install"`:**
+
+![alt text]()
+
+
+**Result:** Only Docker installation tasks executed, configuration skipped.
+
+**Idempotency check:**
+
+First run: `changed=2`
+Second run: `changed=0` ✅
+
+
+
+### Research Questions
+
+**Q: What happens if rescue block also fails?**
+A: Ansible marks the task as failed and stops playbook execution (unless `ignore_errors: yes` is set). The `always` block still executes.
+
+**Q: Can you have nested blocks?**
+A: Yes, blocks can be nested inside other blocks for more complex error handling logic.
+
+**Q: How do tags inherit to tasks within blocks?**
+A: Tags applied to a block automatically apply to all tasks within that block, but tasks can also have their own additional tags.
+
+---
+
+## Task 2: Docker Compose
+
+### Role Rename
+
+**Action taken:**
+```bash
+mv roles/app_deploy roles/web_app
+```
+
+**Updated references in:**
+- `playbooks/deploy.yml`
+- All documentation
+
+### Docker Compose Template
+
+**File:** `roles/web_app/templates/docker-compose.yml.j2`
+
+```yaml
+services:
+ {{ app_name }}:
+ image: {{ docker_image }}:{{ docker_image_tag }}
+ container_name: {{ app_name }}
+ ports:
+ - "{{ app_port }}:{{ app_internal_port }}"
+ environment:
+ - APP_NAME={{ app_name }}
+ - ENVIRONMENT=production
+ restart: {{ restart_policy }}
+ networks:
+ - app_network
+
+networks:
+ app_network:
+ driver: bridge
+```
+
+### Role Dependencies
+
+**File:** `roles/web_app/meta/main.yml`
+
+```yaml
+---
+dependencies:
+ - role: docker
+ tags:
+ - docker
+```
+
+### Deployment Results
+
+**First deployment:**
+
+![alt text]()
+
+**Verification and Health check:**
+
+```bash
+ansible webservers -a "docker ps" --ask-vault-pass
+ansible webservers -a "curl -s http://localhost:5000/health" --ask-vault-pass
+```
+
+**Output:**
+
+
+
+### Idempotency Verification
+
+**Second deployment run:**
+
+![alt text]()
+
+**Result:** Idempotent - no unnecessary changes on repeated execution.
+
+### Research Questions
+
+**Q: What's the difference between `restart: always` and `restart: unless-stopped`?**
+A: `always` restarts container even after Docker daemon restarts. `unless-stopped` doesn't restart if container was manually stopped.
+
+**Q: How do Docker Compose networks differ from Docker bridge networks?**
+A: Compose creates isolated networks per project with automatic DNS resolution by service name. Bridge networks are shared across all containers.
+
+**Q: Can you reference Ansible Vault variables in the template?**
+A: Yes, Vault variables are decrypted before templating, so they can be used in Jinja2 templates.
+
+
+## Task 3: Wipe Logic
+
+### Testing Scenarios
+
+#### Scenario 1: Normal Deployment (wipe skipped)
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks skipped
+- ✅ Application deployed
+- ✅ Container running
+
+![alt text]()
+![alt text]()
+#### Scenario 2: Wipe Only
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --tags web_app_wipe \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks executed
+- ✅ Deployment tasks skipped
+- ✅ Container removed
+- ✅ Directory deleted
+
+![alt text]()
+![alt text]()
+
+#### Scenario 3: Clean Reinstall (wipe → deploy)
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks executed (old removed)
+- ✅ Deployment tasks executed (new installed)
+- ✅ Fresh container running
+
+![alt text]()
+
+#### Scenario 4: Safety Check (tag without variable)
+
+**Command 4a:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ --tags web_app_wipe \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks skipped (`when` condition blocks)
+- ✅ Application NOT removed
+- ✅ Double-gating protection works!
+
+![alt text]()
+
+**Command 4b:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --tags web_app_wipe
+```
+![alt text]()
+
+
+### Research Questions
+
+**Q: Why use both variable AND tag?**
+A: Double protection against accidental deletion. Tag allows wipe-only execution without deployment, variable requires explicit confirmation.
+
+**Q: What's the difference between `never` tag and this approach?**
+A: `never` tag requires explicit tag specification to run. Our approach adds additional variable check for extra safety.
+
+**Q: Why must wipe logic come BEFORE deployment in main.yml?**
+A: To support clean reinstallation scenario where we wipe old installation and deploy fresh in single playbook run.
+
+**Q: When would you want clean reinstallation vs. rolling update?**
+A: Clean reinstall for testing or state corruption issues. Rolling update for production with zero downtime.
+
+**Q: How would you extend this to wipe Docker images and volumes too?**
+A: Add tasks with `docker_image` module (state: absent) and `docker_volume` module to remove associated volumes.
+
+## Task 4: CI/CD
+
+### GitHub Secrets Configuration
+
+**Configured secrets:**
+- `ANSIBLE_VAULT_PASSWORD` - Vault decryption password
+- `SSH_PRIVATE_KEY` - SSH private key for VM access
+- `VM_HOST` - `89.169.158.252`
+- `VM_USER` - `ubuntu`
+
+### Workflow Results
+
+**Status Badge:**
+
+[](https://github.com/angel-palkina/DevOps-Core-Course/actions/workflows/ansible-deploy.yml)
+
+**Successful workflow run:**
+
+![alt text]()
+![alt text]()
+
+**Lint job output:**
+
+
+
+**Deploy job output:**
+
+
+
+
+**Verification output:**
+
+
+
+### Research Questions
+
+**Q: What are the security implications of storing SSH keys in GitHub Secrets?**
+A: GitHub Secrets are encrypted at rest and only exposed during workflow runs. Risks include GitHub account compromise. Mitigations: use 2FA, limit SSH key permissions, regular key rotation.
+
+**Q: How would you implement a staging → production deployment pipeline?**
+A: Create separate workflows for staging and production with different triggers (staging on push, production on release/tag). Use GitHub Environments with approval gates.
+
+**Q: What would you add to make rollbacks possible?**
+A: Tag Docker images with Git commit SHA, store image tags in Git, create rollback workflow accepting version parameter, use wipe logic before deploying previous version.
+
+**Q: How does self-hosted runner improve security compared to GitHub-hosted?**
+A: Direct infrastructure access (no SSH needed), controlled environment, secrets never leave infrastructure, faster for large files, compliance requirements.
+
+### Challenges & Solutions
+
+**Challenge:** Health check failing due to incorrect port mapping
+**Solution:** Fixed port mapping from `5000:6000` to `5000:5000` to match application configuration
+
+
diff --git a/ansible/docs/READMY.md b/ansible/docs/READMY.md
new file mode 100644
index 0000000000..d46947d65d
--- /dev/null
+++ b/ansible/docs/READMY.md
@@ -0,0 +1,19 @@
+# Ansible Automation
+
+[](https://github.com/angel-palkina/DevOps-Core-Course/actions/workflows/ansible-deploy.yml)
+
+## Lab 06 - Advanced Ansible & CI/CD
+
+Automated deployment with GitHub Actions.
+
+### Quick Start
+
+```bash
+# Deploy application
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+
+# Provision servers
+ansible-playbook playbooks/provision.yml --ask-vault-pass
+
+# Clean reinstall
+ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" --ask-vault-pass
\ No newline at end of file
diff --git a/ansible/docs/Screenshot 2026-03-05 191756.png b/ansible/docs/Screenshot 2026-03-05 191756.png
new file mode 100644
index 0000000000..95a308cbbb
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 191756.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 192337.png b/ansible/docs/Screenshot 2026-03-05 192337.png
new file mode 100644
index 0000000000..ad0089373d
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 192337.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 194004.png b/ansible/docs/Screenshot 2026-03-05 194004.png
new file mode 100644
index 0000000000..c19dd65877
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 194004.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 220630.png b/ansible/docs/Screenshot 2026-03-05 220630.png
new file mode 100644
index 0000000000..b83ac31998
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 220630.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 221207.png b/ansible/docs/Screenshot 2026-03-05 221207.png
new file mode 100644
index 0000000000..8293e88b58
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 221207.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222221-1.png b/ansible/docs/Screenshot 2026-03-05 222221-1.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222221-1.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222221.png b/ansible/docs/Screenshot 2026-03-05 222221.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222221.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222252.png b/ansible/docs/Screenshot 2026-03-05 222252.png
new file mode 100644
index 0000000000..3f369395f4
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222252.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222432.png b/ansible/docs/Screenshot 2026-03-05 222432.png
new file mode 100644
index 0000000000..bb901abbe2
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222432.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222742.png b/ansible/docs/Screenshot 2026-03-05 222742.png
new file mode 100644
index 0000000000..085764b918
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222742.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225003.png b/ansible/docs/Screenshot 2026-03-05 225003.png
new file mode 100644
index 0000000000..279c18f87c
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225003.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225036-1.png b/ansible/docs/Screenshot 2026-03-05 225036-1.png
new file mode 100644
index 0000000000..6b53d60841
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225036-1.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225036.png b/ansible/docs/Screenshot 2026-03-05 225036.png
new file mode 100644
index 0000000000..6b53d60841
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225036.png differ
diff --git a/ansible/docs/images/3.png b/ansible/docs/images/3.png
new file mode 100644
index 0000000000..5dc8c9893a
Binary files /dev/null and b/ansible/docs/images/3.png differ
diff --git a/ansible/docs/images/4.png b/ansible/docs/images/4.png
new file mode 100644
index 0000000000..e0b76459a6
Binary files /dev/null and b/ansible/docs/images/4.png differ
diff --git a/ansible/docs/images/5.png b/ansible/docs/images/5.png
new file mode 100644
index 0000000000..37090f3f62
Binary files /dev/null and b/ansible/docs/images/5.png differ
diff --git a/ansible/docs/images/7.png b/ansible/docs/images/7.png
new file mode 100644
index 0000000000..ca70ff0000
Binary files /dev/null and b/ansible/docs/images/7.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 163842.png b/ansible/docs/images/Screenshot 2026-02-26 163842.png
new file mode 100644
index 0000000000..16b664868b
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 163842.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 163855.png b/ansible/docs/images/Screenshot 2026-02-26 163855.png
new file mode 100644
index 0000000000..79862bcb8a
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 163855.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170120.png b/ansible/docs/images/Screenshot 2026-02-26 170120.png
new file mode 100644
index 0000000000..cddf5b5052
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170120.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170411.png b/ansible/docs/images/Screenshot 2026-02-26 170411.png
new file mode 100644
index 0000000000..94753dc4bd
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170411.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170552.png b/ansible/docs/images/Screenshot 2026-02-26 170552.png
new file mode 100644
index 0000000000..f0f3a958a2
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170552.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 192129.png b/ansible/docs/images/Screenshot 2026-02-26 192129.png
new file mode 100644
index 0000000000..476b0ebf4d
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 192129.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191610.png b/ansible/docs/images/Screenshot 2026-03-05 191610.png
new file mode 100644
index 0000000000..7b2ac65f3e
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191610.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191756.png b/ansible/docs/images/Screenshot 2026-03-05 191756.png
new file mode 100644
index 0000000000..95a308cbbb
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191756.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191832.png b/ansible/docs/images/Screenshot 2026-03-05 191832.png
new file mode 100644
index 0000000000..599e5e9808
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191832.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 192337.png b/ansible/docs/images/Screenshot 2026-03-05 192337.png
new file mode 100644
index 0000000000..ad0089373d
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 192337.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 194004.png b/ansible/docs/images/Screenshot 2026-03-05 194004.png
new file mode 100644
index 0000000000..c19dd65877
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 194004.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 220514.png b/ansible/docs/images/Screenshot 2026-03-05 220514.png
new file mode 100644
index 0000000000..35d03e6a18
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 220514.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 220630.png b/ansible/docs/images/Screenshot 2026-03-05 220630.png
new file mode 100644
index 0000000000..b83ac31998
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 220630.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 221207.png b/ansible/docs/images/Screenshot 2026-03-05 221207.png
new file mode 100644
index 0000000000..8293e88b58
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 221207.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222221.png b/ansible/docs/images/Screenshot 2026-03-05 222221.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222221.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222252.png b/ansible/docs/images/Screenshot 2026-03-05 222252.png
new file mode 100644
index 0000000000..3f369395f4
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222252.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222432.png b/ansible/docs/images/Screenshot 2026-03-05 222432.png
new file mode 100644
index 0000000000..bb901abbe2
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222432.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222742.png b/ansible/docs/images/Screenshot 2026-03-05 222742.png
new file mode 100644
index 0000000000..085764b918
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222742.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 225003.png b/ansible/docs/images/Screenshot 2026-03-05 225003.png
new file mode 100644
index 0000000000..279c18f87c
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 225003.png differ
diff --git a/ansible/docs/images/image.png b/ansible/docs/images/image.png
new file mode 100644
index 0000000000..4ff28c8da2
Binary files /dev/null and b/ansible/docs/images/image.png differ
diff --git a/ansible/docs/images/image1.png b/ansible/docs/images/image1.png
new file mode 100644
index 0000000000..0245c17860
Binary files /dev/null and b/ansible/docs/images/image1.png differ
diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml
new file mode 100644
index 0000000000..976fa46fd5
--- /dev/null
+++ b/ansible/inventory/group_vars/all.yml
@@ -0,0 +1,18 @@
+$ANSIBLE_VAULT;1.1;AES256
+38613830353565626539316439343866313634616337313962636431613362656132333130653439
+6662613561323563623332323234333764653832623833610a363462653964396435323763356465
+35623562376538323838346239336666666361356162363132356438663364326433323430343730
+3338393138373936370a343337363161643233333861623161336563303831363636353736373439
+32303430306636393765323337663537363530653737323164663334373338363366306532633739
+64643237623465646230306635366565616435663533373434393265663962633031666137396638
+66396266353364373836343939613036316632346235323839376433306630656566636439303063
+30336562636634303731393035393662326565383936393436613264666431393862333930376666
+61643665613238393631616435373839303334386135666465366433393630353562626238646437
+66636365653666323562356236313863373933376539663462636530366131613538643433393665
+33643233643964336131623664663334376362663531346333363865383763326439623031373431
+62323932363864303832343866636562393463636331653164366136393530353964363335656439
+66663331633532333237663237646263346364656364616137353936653363613836326166313631
+32306331636261313136373661643166336464633732343562333763616638373931363230633565
+39383966386138633464636638666165336635656166626433363765623732313161613861313063
+35366163613265356338353961643065343262363838343232613738656566653337666630646465
+6662
diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini
new file mode 100644
index 0000000000..19bbe814b3
--- /dev/null
+++ b/ansible/inventory/hosts.ini
@@ -0,0 +1,5 @@
+[webservers]
+terraform-vm ansible_host=89.169.158.252 ansible_user=ubuntu
+
+[webservers:vars]
+ansible_python_interpreter=/usr/bin/python3
diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml
new file mode 100644
index 0000000000..cb92900093
--- /dev/null
+++ b/ansible/playbooks/deploy.yml
@@ -0,0 +1,7 @@
+---
+- name: Deploy web application
+ hosts: webservers
+ become: yes
+
+ roles:
+ - web_app
\ No newline at end of file
diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml
new file mode 100644
index 0000000000..0fafc4162d
--- /dev/null
+++ b/ansible/playbooks/provision.yml
@@ -0,0 +1,5 @@
+- name: Provision web servers
+ hosts: webservers
+ roles:
+ - common
+ - docker
diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml
new file mode 100644
index 0000000000..96b9736524
--- /dev/null
+++ b/ansible/roles/common/defaults/main.yml
@@ -0,0 +1,6 @@
+common_packages:
+ - python3-pip
+ - curl
+ - git
+ - vim
+ - htop
diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml
new file mode 100644
index 0000000000..1dbc528ab5
--- /dev/null
+++ b/ansible/roles/common/tasks/main.yml
@@ -0,0 +1,43 @@
+---
+# Common role with blocks, tags, and error handling
+- name: Package installation block
+ block:
+ - name: Update apt cache
+ apt:
+ update_cache: yes
+ cache_valid_time: 3600
+
+ - name: Install common packages
+ apt:
+ name: "{{ common_packages }}"
+ state: present
+
+ rescue:
+ - name: Handle apt cache update failure
+ debug:
+ msg: "Apt cache update failed, attempting fix-missing"
+
+ - name: Fix apt cache
+ command: apt-get update --fix-missing
+ changed_when: false
+
+ - name: Retry installing common packages
+ apt:
+ name: "{{ common_packages }}"
+ state: present
+
+ become: yes
+ tags:
+ - common
+ - packages
+
+- name: System configuration block
+ block:
+ - name: Set timezone
+ community.general.timezone:
+ name: Europe/Moscow
+
+ become: yes
+ tags:
+ - common
+ - config
\ No newline at end of file
diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml
new file mode 100644
index 0000000000..372575fd86
--- /dev/null
+++ b/ansible/roles/docker/defaults/main.yml
@@ -0,0 +1 @@
+docker_user: ubuntu
diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml
new file mode 100644
index 0000000000..1907c4cd1c
--- /dev/null
+++ b/ansible/roles/docker/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: restart docker
+ service:
+ name: docker
+ state: restarted
diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml
new file mode 100644
index 0000000000..8053d71a3d
--- /dev/null
+++ b/ansible/roles/docker/tasks/main.yml
@@ -0,0 +1,95 @@
+---
+# Docker role with blocks, tags, and error handling
+- name: Docker installation block
+ block:
+ - name: Install required system packages
+ apt:
+ name:
+ - ca-certificates
+ - gnupg
+ - lsb-release
+ - apt-transport-https
+ state: present
+
+ - name: Add Docker GPG key
+ apt_key:
+ url: https://download.docker.com/linux/ubuntu/gpg
+ state: present
+
+ - name: Add Docker repository
+ apt_repository:
+ repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+ notify: restart docker
+
+ - name: Install Docker packages
+ apt:
+ name:
+ - docker-ce
+ - docker-ce-cli
+ - containerd.io
+ state: present
+ update_cache: yes
+
+ rescue:
+ - name: Handle GPG key failure
+ debug:
+ msg: "Docker GPG key addition failed, retrying after delay"
+
+ - name: Wait before retry
+ wait_for:
+ timeout: 10
+
+ - name: Retry adding Docker GPG key
+ apt_key:
+ url: https://download.docker.com/linux/ubuntu/gpg
+ state: present
+
+ - name: Retry adding Docker repository
+ apt_repository:
+ repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+
+ - name: Retry Docker installation
+ apt:
+ name:
+ - docker-ce
+ - docker-ce-cli
+ - containerd.io
+ state: present
+ update_cache: yes
+
+ always:
+ - name: Ensure Docker service is enabled
+ service:
+ name: docker
+ enabled: yes
+
+ become: yes
+ tags:
+ - docker
+ - docker_install
+
+- name: Docker configuration block
+ block:
+ - name: Ensure Docker is running
+ service:
+ name: docker
+ state: started
+ enabled: true
+
+ - name: Add user to docker group
+ user:
+ name: "{{ docker_user }}"
+ groups: docker
+ append: yes
+
+ - name: Install Python Docker SDK
+ pip:
+ name: docker
+ state: present
+
+ become: yes
+ tags:
+ - docker
+ - docker_config
\ No newline at end of file
diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml
new file mode 100644
index 0000000000..223feed0ac
--- /dev/null
+++ b/ansible/roles/web_app/defaults/main.yml
@@ -0,0 +1,15 @@
+---
+# Web application defaults
+app_name: devops-info-service
+docker_image: "spalkkina/{{ app_name }}"
+docker_image_tag: "1.0"
+app_port: 5000 # External port (host)
+app_internal_port: 5000 # Internal port (container) ← ИЗМЕНЕНО
+restart_policy: unless-stopped
+
+# Docker Compose configuration
+compose_project_dir: "/opt/{{ app_name }}"
+compose_file_name: docker-compose.yml
+
+# Wipe control (default: do not wipe)
+web_app_wipe: false
\ No newline at end of file
diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml
new file mode 100644
index 0000000000..c56af2d69b
--- /dev/null
+++ b/ansible/roles/web_app/handlers/main.yml
@@ -0,0 +1,11 @@
+- name: restart app container
+ docker_container:
+ name: "{{ app_container_name }}"
+ state: started
+ restart: yes
+ image: "{{ docker_image }}:{{ docker_image_tag }}"
+ restart_policy: "{{ docker_restart_policy }}"
+ ports:
+ - "{{ app_port }}:{{ app_port }}"
+ env: "{{ app_environment_vars }}"
+ become: yes
\ No newline at end of file
diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml
new file mode 100644
index 0000000000..4aef9e68ed
--- /dev/null
+++ b/ansible/roles/web_app/meta/main.yml
@@ -0,0 +1,6 @@
+---
+# Role dependencies
+dependencies:
+ - role: docker
+ tags:
+ - docker
\ No newline at end of file
diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml
new file mode 100644
index 0000000000..a579e83662
--- /dev/null
+++ b/ansible/roles/web_app/tasks/main.yml
@@ -0,0 +1,90 @@
+---
+# Wipe logic runs first (when explicitly requested)
+- name: Include wipe tasks
+ include_tasks: wipe.yml
+ tags:
+ - web_app_wipe
+
+# Web app deployment with Docker Compose and blocks/tags
+- name: Deploy application with Docker Compose
+ block:
+ - name: Create application directory
+ file:
+ path: "{{ compose_project_dir }}"
+ state: directory
+ mode: '0755'
+
+ - name: Template docker-compose.yml file
+ template:
+ src: docker-compose.yml.j2
+ dest: "{{ compose_project_dir }}/{{ compose_file_name }}"
+ mode: '0644'
+
+ # - name: Login to Docker Hub
+ # community.docker.docker_login:
+ # username: "{{ dockerhub_username }}"
+ # password: "{{ dockerhub_password }}"
+ # no_log: true
+
+ # - name: Pull Docker image
+ # community.docker.docker_image:
+ # name: "{{ docker_image }}:{{ docker_image_tag }}"
+ # source: pull
+ #
+ # - name: Check if standalone container exists (from Lab 05)
+ # command: "docker ps -aq -f name=^{{ app_name }}$"
+ # register: standalone_container
+ # changed_when: false
+ # failed_when: false
+
+ # - name: Stop existing standalone container (from Lab 05)
+ # community.docker.docker_container:
+ # name: "{{ app_name }}"
+ # state: absent
+ # when:
+ # - standalone_container.stdout is defined
+ # - standalone_container.stdout | length > 0
+
+ - name: Deploy with Docker Compose
+ community.docker.docker_compose_v2:
+ project_src: "{{ compose_project_dir }}"
+ state: present
+ pull: always
+ # pull: "policy"
+ # remove_orphans: yes
+ # recreate: "auto" # ← ИСПРАВЛЕНО: auto вместо smart
+
+ # - name: Wait for application to be ready
+ # wait_for:
+ # host: localhost
+ # port: "{{ app_port }}"
+ # delay: 5
+ # timeout: 60
+
+ # - name: Verify application health
+ # uri:
+ # url: "http://localhost:{{ app_port }}/health"
+ # status_code: 200
+ # register: health_check
+ # retries: 3
+ # delay: 5
+
+ rescue:
+ - name: Handle deployment failure
+ debug:
+ msg: "Deployment failed. Check logs with: docker compose -f {{ compose_project_dir }}/{{ compose_file_name }} logs"
+
+ - name: Show deployment status
+ command: "docker compose -f {{ compose_project_dir }}/{{ compose_file_name }} ps"
+ register: compose_status
+ ignore_errors: yes
+
+ - name: Display compose status
+ debug:
+ var: compose_status.stdout_lines
+
+ become: yes
+ tags:
+ - web_app
+ - app_deploy
+ - compose
\ No newline at end of file
diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml
new file mode 100644
index 0000000000..68d2e376c1
--- /dev/null
+++ b/ansible/roles/web_app/tasks/wipe.yml
@@ -0,0 +1,29 @@
+---
+# Wipe logic - removes deployed application
+# Double-gated: requires variable=true AND tag
+- name: Wipe web application
+ block:
+ - name: Stop and remove containers with Docker Compose
+ community.docker.docker_compose_v2:
+ project_src: "{{ compose_project_dir }}"
+ state: absent
+ ignore_errors: yes
+
+ - name: Remove docker-compose.yml file
+ file:
+ path: "{{ compose_project_dir }}/{{ compose_file_name }}"
+ state: absent
+
+ - name: Remove application directory
+ file:
+ path: "{{ compose_project_dir }}"
+ state: absent
+
+ - name: Log wipe completion
+ debug:
+ msg: "Application {{ app_name }} wiped successfully"
+
+ when: web_app_wipe | bool
+ become: yes
+ tags:
+ - web_app_wipe
\ No newline at end of file
diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2
new file mode 100644
index 0000000000..ff5ca51cdd
--- /dev/null
+++ b/ansible/roles/web_app/templates/docker-compose.yml.j2
@@ -0,0 +1,16 @@
+services:
+ {{ app_name }}:
+ image: {{ docker_image }}:{{ docker_image_tag }}
+ container_name: {{ app_name }}
+ ports:
+ - "{{ app_port }}:{{ app_internal_port }}"
+ environment:
+ - APP_NAME={{ app_name }}
+ - ENVIRONMENT=production
+ restart: {{ restart_policy }}
+ networks:
+ - app_network
+
+networks:
+ app_network:
+ driver: bridge
\ No newline at end of file
diff --git a/app_python/.dockerignore b/app_python/.dockerignore
new file mode 100644
index 0000000000..44fac1397a
--- /dev/null
+++ b/app_python/.dockerignore
@@ -0,0 +1,61 @@
+# Python cache
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+*.py[cod]
+*.so
+*.dll
+*.egg
+*.egg-info/
+dist/
+build/
+
+# Virtual Environment
+venv/
+.venv/
+env/
+.env/
+
+# Testing & Coverage
+.pytest_cache/
+.coverage
+.coverage.*
+htmlcov/
+coverage.xml
+*.cover
+tests/
+docs/
+
+# Version Control
+.git/
+.gitignore
+.gitattributes
+.dockerignore
+README.md
+LICENSE
+*.md
+
+# IDE
+.vscode/
+.idea/
+*.swp
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Docker
+Dockerfile
+docker-compose*.yml
+.dockerignore
+
+# Secrets (ОЧЕНЬ ВАЖНО!)
+*.key
+*.pem
+.env
+.env.*
\ No newline at end of file
diff --git a/app_python/.gitignore b/app_python/.gitignore
new file mode 100644
index 0000000000..606bceb0a2
--- /dev/null
+++ b/app_python/.gitignore
@@ -0,0 +1,51 @@
+# Python
+__pycache__/
+*.py[cod]
+*.pyo
+*.pyd
+*.so
+*.dll
+*.egg
+*.egg-info/
+dist/
+build/
+
+# Virtual Environment
+venv/
+.venv/
+env/
+.env/
+
+# Testing
+.pytest_cache/
+.coverage
+.coverage.*
+*.cover
+htmlcov/
+coverage.xml
+.cache/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Docker
+.dockerignore
+Dockerfile
+docker-compose*.yml
+
+# Secrets (никогда не коммитить!)
+*.key
+*.pem
+.env.local
+.env.production
\ No newline at end of file
diff --git a/app_python/Dockerfile b/app_python/Dockerfile
new file mode 100644
index 0000000000..e327658846
--- /dev/null
+++ b/app_python/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.13-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+RUN useradd -m appuser
+USER appuser
+
+EXPOSE 5000
+
+CMD ["python", "app.py"]
\ No newline at end of file
diff --git a/app_python/README.md b/app_python/README.md
new file mode 100644
index 0000000000..64b84f025a
--- /dev/null
+++ b/app_python/README.md
@@ -0,0 +1,242 @@
+# DevOps Info Service
+
+## Overview
+A web application that provides detailed information about itself and its runtime environment. This service will evolve throughout the DevOps course into a comprehensive monitoring tool.
+
+## Project Structure
+```text
+app_python/
+├── app.py # Main application
+├── requirements.txt # Dependencies
+├── requirements-dev.txt # Development dependencies (testing, linting)
+├── .gitignore
+├── README.md # This file
+├── tests/
+│ ├── __init__.py
+│ └── test_app.py
+└── docs/ # Documentation
+ ├── LAB01.md
+ ├── LAB02.md
+ ├── LAB03.md
+ ├── ...
+ └── screenshots/
+```
+
+## Prerequisites
+- Python 3.11 or higher
+- pip (Python package manager)
+
+## Installation
+
+1. Clone the repository:
+ ```bash
+ git clone
+ cd app_python
+ ```
+
+
+2. Create a virtual environment:
+
+ ```bash
+ python -m venv venv
+ ```
+
+3. Activate the virtual environment:
+- Linux/Mac:
+
+ ```bash
+ source venv/bin/activate
+ ```
+- Windows:
+
+ ```bash
+ venv\Scripts\activate
+ ```
+
+4. Install dependencies:
+
+ ```bash
+ pip install -r requirements.txt
+
+ # or for testing
+ pip install -r requirements-dev.txt
+ ```
+
+## Running the Application
+
+```bash
+python app.py #Default Configuration The service will start at: http://0.0.0.0:5000
+
+# Custom Configuration on Linux/Mac:
+PORT=8080 python app.py # Change port
+HOST=127.0.0.1 PORT=3000 python app.py # Change host and port
+DEBUG=true python app.py # Enable debug mode
+
+# Custom Configuration on Windows PowerShell:
+$env:HOST="127.0.0.1"; $env:PORT=8080; python app.py
+```
+
+## API Endpoints
+
+### GET /
+
+- Returns comprehensive service and system information (endpoints, request, runtime, system info, service info).
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+### GET /health
+
+- Health check endpoint for monitoring systems and Kubernetes probes.
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/health
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+## Configuration
+
+The application is configured through environment variables:
+
+|Variable | Default | Description |
+|----------|-------|---------|
+|HOST | 0.0.0.0 | Host interface to bind the server|
+|PORT | 5000 | Port number to listen on|
+|DEBUG | false | Debug mode (true/false)|
+
+## Testing
+
+This project uses pytest for unit testing. Pytest was chosen for its:
+
+- Simple, Pythonic syntax
+
+- Powerful fixture system
+
+- Excellent plugin ecosystem (coverage, mocking)
+
+- Clear assertion reporting
+
+### What's Tested
+
+1) `GET /`
+
+ ✓ Status code 200
+
+ ✓ JSON response format
+
+ ✓ All required sections present
+
+ ✓ Data types validation
+
+ ✓ Service info correctness
+
+2) `GET /health`
+
+ ✓ Status code 200
+
+ ✓ Status = "healthy"
+
+ ✓ Timestamp format (ISO 8601)
+
+ ✓ Uptime tracking
+3) Error Handling
+
+ ✓ 404 Not Found response
+
+ ✓ JSON error format
+
+ ✓ Method not allowed
+
+### Running Tests Locally
+
+```bash
+# Run all tests
+pytest tests/ -v
+
+# Run tests with coverage report
+pytest tests/ --cov=app --cov-report=term
+
+# Run tests with HTML coverage report
+pytest tests/ --cov=app --cov-report=html
+# Then open htmlcov/index.html in your browser
+
+# Run specific test file
+pytest tests/test_app.py -v
+
+# Run tests matching a name pattern
+pytest tests/ -k "health" -v
+
+```
+
+### Expected Output
+
+
+
+### Test Coverage
+Current test coverage: ~92%
+
+
+### CI/CD Status
+
+
+
+
+## Docker
+
+This application is containerized using Docker for consistent and portable deployment.
+
+- Build the image locally
+
+ ```sh
+ docker build -t spalkkina/devops-info-service:1.0 .
+ ```
+- Run the container locally
+ ```
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+ The app will be accessible at http://localhost:5000.
+- Pull from Docker Hub
+ ```
+ docker pull spalkkina/devops-info-service:1.0
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+- You can run specific versions by adjusting the tag
+ ```
+ docker run -p 5000:5000 spalkkina/devops-info-service:latest
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+## Future Development
+
+This service will evolve throughout the course:
+
+- Lab 8: Metrics endpoint for Prometheus
+
+- Lab 9: Kubernetes deployment
+
+- Lab 12: Persistence with file storage
+
+- Lab 13: Multi-environment deployment
+
+
diff --git a/app_python/app.py b/app_python/app.py
new file mode 100644
index 0000000000..a6f2a843a0
--- /dev/null
+++ b/app_python/app.py
@@ -0,0 +1,146 @@
+"""
+DevOps Info Service - Flask Application
+Main application module providing system and service information.
+"""
+
+import os
+import socket
+import platform
+import logging
+from datetime import datetime, timezone
+from flask import Flask, jsonify, request
+
+# ========== Flask App Initialization ==========
+app = Flask(__name__)
+
+# ========== Configuration ==========
+HOST = os.getenv('HOST', '0.0.0.0')
+PORT = int(os.getenv('PORT', 5000))
+DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
+
+# ========== Logging Setup ==========
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# ========== Application Start Time ==========
+START_TIME = datetime.now(timezone.utc)
+
+
+# ========== Helper Functions ==========
+def get_system_info():
+ """Collect system information."""
+ return {
+ 'hostname': socket.gethostname(),
+ 'platform': platform.system(),
+ 'platform_version': platform.platform(),
+ 'architecture': platform.machine(),
+ 'cpu_count': os.cpu_count(),
+ 'python_version': platform.python_version()
+ }
+
+
+def get_uptime():
+ """Calculate application uptime."""
+ delta = datetime.now(timezone.utc) - START_TIME
+ seconds = int(delta.total_seconds())
+ hours, remainder = divmod(seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ return {
+ 'seconds': seconds,
+ 'human': f"{hours} hours, {minutes} minutes"
+ }
+
+
+def get_runtime_info():
+ """Collect runtime information."""
+ uptime = get_uptime()
+ now = datetime.now(timezone.utc)
+ return {
+ 'uptime_seconds': uptime['seconds'],
+ 'uptime_human': uptime['human'],
+ 'current_time': now.isoformat(),
+ 'timezone': str(now.tzinfo)
+ }
+
+
+def get_request_info():
+ """Collect request information."""
+ return {
+ 'client_ip': request.remote_addr,
+ 'user_agent': request.headers.get('User-Agent', 'unknown'),
+ 'method': request.method,
+ 'path': request.path
+ }
+
+
+def get_service_info():
+ """Service metadata."""
+ return {
+ 'name': 'devops-info-service',
+ 'version': '1.0.0',
+ 'description': 'DevOps course info service',
+ 'framework': 'Flask'
+ }
+
+
+def get_endpoints_list():
+ """List available endpoints."""
+ return [
+ {'path': '/', 'method': 'GET', 'description': 'Service information'},
+ {'path': '/health', 'method': 'GET', 'description': 'Health check'}
+ ]
+
+
+# ========== Main Endpoint ==========
+@app.route('/')
+def index():
+ """Main endpoint - returns comprehensive service and system information."""
+ logger.info(f"Request to / from {request.remote_addr}")
+ response_data = {
+ 'service': get_service_info(),
+ 'system': get_system_info(),
+ 'runtime': get_runtime_info(),
+ 'request': get_request_info(),
+ 'endpoints': get_endpoints_list()
+ }
+ return jsonify(response_data)
+
+
+# ========== Health Check Endpoint ==========
+@app.route('/health')
+def health():
+ """Health check endpoint for monitoring."""
+ logger.info(f"Health check from {request.remote_addr}")
+ return jsonify({
+ 'status': 'healthy',
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'uptime_seconds': get_uptime()['seconds']
+ })
+
+
+# ========== Error Handlers ==========
+@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):
+ logger.error(f"Internal Server Error: {error}")
+ return jsonify({
+ 'error': 'Internal Server Error',
+ 'message': 'An unexpected error occurred'
+ }), 500
+
+
+# ========== Application Entry Point ==========
+if __name__ == '__main__':
+ logger.info(
+ f"Starting DevOps Info Service on {HOST}:{PORT} (DEBUG={DEBUG})")
+ 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..2f2111cfd9
--- /dev/null
+++ b/app_python/docs/LAB01.md
@@ -0,0 +1,151 @@
+# Lab 1 — DevOps Info Service: Implementation Report
+
+## Framework Selection
+
+I selected **Flask** as the web framework for this project, because Flask provides the optimal balance of simplicity, flexibility, and learning opportunity. Flask's minimal approach allows us to focus on DevOps practices while still creating a production-ready service.
+
+
+| Framework | Pros | Cons | Decision Reason |
+|-----------|------|------|----------------|
+| **Flask** | - Lightweight and minimal
- Perfect for microservices
- Excellent for educational purposes | - Less built-in features compared to Django
- Manual setup for async operations | Ideal for simple API services, easier for beginners learning DevOps concepts, sufficient for our monitoring service needs |
+| FastAPI | - Native async support
- Auto-generated OpenAPI documentation
- High performance
- Type hints integration | - Steeper learning curve for Python beginners
- More overhead for a simple application | Considered for modern features but Flask's simplicity better fits educational goals |
+| Django | - Comprehensive security features
- Excellent for full-stack applications | - Heavyweight
- Overkill for a simple API service
- Longer setup time | Too complex for this microservice; would add unnecessary complexity |
+
+## Best Practices Applied
+
+### 1. Clean Code Organization
+- **Modular Structure:** Separated concerns into distinct functions (`get_system_info`, `get_runtime_info`, etc.) for better understanding
+- **Descriptive Naming:** Functions clearly indicate their purpose
+- **Import Grouping:** Organized imports in logical groups (standard library, third-party)
+- **Minimal Comments:** Comments only where business logic needs explanation
+- **PEP 8 Compliance:** Followed Python style guide for indentation, line length, and naming conventions
+- **Comprehensive Error Handling:** Implemented error handlers for common HTTP status codes
+- **Structured Logging:** Timestamps help debug timing issues
+- **Environment-Based Configuration:** No hardcoded values in source code, easy configuration for different environments
+
+**Code Example:**
+```python
+def get_system_info():
+ """Collect comprehensive system information."""
+ return {
+ 'hostname': socket.gethostname(),
+ 'platform': platform.system(),
+ 'platform_version': platform.platform(),
+ 'architecture': platform.machine(),
+ 'cpu_count': os.cpu_count(),
+ 'python_version': platform.python_version()
+ }
+```
+
+## API Documentation
+
+### GET /
+
+- Returns comprehensive service and system information (endpoints, request, runtime, system info, service info).
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+### GET /health
+
+- Health check endpoint for monitoring systems and Kubernetes probes.
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/health
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+## Testing commands
+
+```bash
+python app.py #Default Configuration The service will start at: http://0.0.0.0:5000
+
+# Custom Configuration on Linux/Mac:
+PORT=8080 python app.py # Change port
+HOST=127.0.0.1 PORT=3000 python app.py # Change host and port
+DEBUG=true python app.py # Enable debug mode
+
+# Custom Configuration on Windows PowerShell:
+$env:HOST="127.0.0.1"; $env:PORT=8080; python app.py
+```
+
+## Terminal Output Examples
+
+Application Startup:
+
+```text
+2026-01-28 22:02:58,501 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:8813 (DEBUG=False)
+ * Serving Flask app 'app'
+ * Debug mode: off
+```
+
+Request Logging:
+
+```text
+2026-01-28 22:03:13,068 - __main__ - INFO - Request to / from 127.0.0.1
+2026-01-28 22:03:13,116 - werkzeug - INFO - 127.0.0.1 - - [28/Jan/2026 22:03:13] "GET / HTTP/1.1" 200 -
+```
+
+## Challenges & Solutions
+
+### Windows Port Access Restrictions
+
+**Problem:**
+When trying to bind to certain ports in Windows, received error: "An attempt was made to access a socket in a way forbidden by its access permissions". This occurred regardless of which port was used with the command $env:HOST="127.0.0.1"; $env:PORT=3000; python app.py.
+
+**Root Cause:**
+Windows has strict socket permissions, and ports below 1024 often require administrator privileges. Additionally, Windows Firewall or antivirus software can block socket creation.
+
+**Solution:**
+
+1. Use ports above 1024: Changed to port 5000 or 8080 which don't require admin rights
+2. Run PowerShell as Administrator: For ports that require elevated privileges
+
+## GitHub Community
+
+### Importance of Starring Repositories
+Starring repositories on GitHub is crucial in open source development for several reasons:
+
+- **Discovery & Visibility:** Stars help projects gain visibility in GitHub search and recommendations. More stars often indicate a trustworthy and useful project.
+
+- **Appreciation & Motivation:** Stars show appreciation to maintainers, encouraging them to continue development and support.
+
+- **Bookmarking:** Stars serve as personal bookmarks for interesting projects you might want to reference or contribute to later.
+
+- **Professional Profile:** Your starred repositories appear on your GitHub profile, showcasing your interests and engagement with the developer community.
+
+
+### Value of Following Developers
+Following classmates, professors, and industry professionals provides significant benefits:
+
+- **Learning Opportunities:** You can see how experienced developers solve problems, structure projects, and write code.
+
+- **Networking:** Building connections within the DevOps community can lead to collaboration opportunities, job prospects, and knowledge sharing.
+
+- **Project Discovery:** Following others helps you discover new tools, libraries, and best practices through their activity and starred repositories.
+
+- **Community Building:** In educational settings, following classmates creates a supportive learning environment where you can share knowledge and help each other.
+
+- **Career Growth:** Following industry leaders keeps you updated on trends and shows potential employers your active engagement in the field.
\ 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..9c3eb5ad4f
--- /dev/null
+++ b/app_python/docs/LAB02.md
@@ -0,0 +1,94 @@
+# Lab 2 — Docker Containerization Report
+
+## Docker Best Practices Applied
+
+- **Non-root user:**
+ `RUN useradd -m appuser` and `USER appuser` are used to avoid running the app as root, which improves security by following the principle of least privilege.This prevents container breakout attacks where an attacker could gain root access to the host system if a vulnerability is exploited
+
+- **Specific base image version:**
+ `FROM python:3.13-slim` ensures reproducibility and reduces image size by using a minimal Python image. Smaller attack surface, faster download/startup times, reduced storage costs.
+ ```
+ python:3.13-full: ~900MB
+ python:3.13-slim: ~195MB (79% reduction)
+ ```
+
+- **Minimal context and .dockerignore:**
+ The `.dockerignore` file excludes files unnecessary for the container (e.g., `.git/`, `__pycache__/`, `tests/`). This reduces build time and image size. Excluding ``.git/`` prevents source code history, API keys, or secrets from accidentally being baked into the image.
+
+- **Layer ordering for caching:**
+ `COPY requirements.txt .` and `RUN pip install ...` are placed before `COPY . .` so Docker caches dependencies installation if only the source code changes. In automated pipelines, proper layer caching can reduce build times from minutes to seconds, saving computational resources and speeding up deployments.
+
+- **Only necessary files are copied:**
+ The Dockerfile copies only files required to run the application. Smaller images transfer faster over networks — crucial for scaling across multiple nodes in production.
+
+- **`WORKDIR` instruction:** Setting `/app` as working directory improves organization. Ensures all subsequent commands (`COPY`, `RUN`, `CMD`) execute from /app rather than relying on relative paths which can break.
+
+- **No-cache pip install:** `--no-cache-dir` flag reduces image size by not storing pip cache. Pip's cache (typically 50-100MB) contains downloaded package wheels for faster reinstalls. This cache is useless in a production container but adds significant bloat.
+
+- **`EXPOSE` instruction:** Documents the port the application listens on (5000). Tools like Docker Compose and Kubernetes read `EXPOSE` to configure networking automatically.
+
+### Relevant Dockerfile snippets
+
+```dockerfile
+FROM python:3.13-slim
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY . .
+RUN useradd -m appuser
+USER appuser
+EXPOSE 5000
+CMD ["python", "app.py"]
+```
+
+## Image Information & Decisions
+
+- **Base image:** ``python:3.13-slim`` (Smaller, faster to load, fewer vulnerabilities).
+- Final image size: 195 MB
+- Compresed image size (in Dockerhub): 45.5 MB
+- Layer structure: Dependencies are installed before code is copied, which optimizes for build caching.
+ - Layer 1: python:3.13-slim base image (~195MB)
+ - Layer 2: WORKDIR /app
+ - Layer 3: COPY requirements.txt
+ - Layer 4: RUN pip install... (dependencies layer)
+ - Layer 5: COPY . . (application code)
+ - Layer 6: RUN useradd... (security layer)
+ - Layer 7: Metadata (EXPOSE, CMD)
+- Optimization choices: Kept the number of layers minimal, excluded unnecessary development files.
+
+## Build & Run Process
+
+- Build output
+ 
+- Run output
+ 
+- Sample endpoint test
+ 
+
+Docker Hub URL: https://hub.docker.com/r/spalkkina/devops-info-service
+
+## Technical Analysis
+- **Strategic Layer Design for Cache Efficiency:**
+
+ The Dockerfile follows a "least volatile → most volatile" layer ordering strategy. Dependencies (requirements.txt) change infrequently, while application code changes frequently. By isolating dependencies in early layers, we maximize Docker's build cache utilization. This is critical in CI/CD pipelines where 90% of builds involve only code changes.
+
+- **Layer order:**
+
+ Changing the order (e.g., copying all files before installing dependencies) would invalidate the cache when any source code changes, so `pip install` would run every build. If we moved COPY . . before RUN pip install, a 1-byte change in app.py would invalidate the pip install layer cache, adding 30+ seconds to every build
+- **Security:**
+
+ Non-root, minimal base image, no unnecessary files copied, Secrets Protection
+- **`.dockerignore` advantage:**
+
+ Prevents large/insecure/unneeded files from being included, which keeps the image smaller and more secure. Prevents credentials from being baked into the image. Reduces information available to attackers if image is compromised.
+
+
+
+## Challenges & Solutions
+
+**Problem:** Determining Actual Image Size. Initially confused about which size metric to report. Docker CLI shows uncompressed size, while Docker Hub shows compressed size.
+
+**Solution:** Learned to use multiple commands for comprehensive understanding
+
+**Learning:** The 195MB local image compresses to 45.5MB on Docker Hub due to layer deduplication and compression algorithms.
diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md
new file mode 100644
index 0000000000..a66a1198a5
--- /dev/null
+++ b/app_python/docs/LAB03.md
@@ -0,0 +1,148 @@
+# Lab 3 — Continuous Integration (CI/CD)
+
+## 1. Overview
+
+### Testing Framework: pytest
+I chose **pytest** for unit testing because:
+- **Simple syntax** — no boilerplate code, just `assert` statements
+- **Powerful fixtures** — easy to create test clients and shared resources
+- **Rich plugin ecosystem** — `pytest-cov` for coverage, integration with GitHub Actions
+- **Industry standard** — widely used in Python DevOps projects
+
+### Test Coverage
+| Endpoint | What is tested |
+|----------|----------------|
+| `GET /` | ✓ Status code 200
✓ JSON content type
✓ All 5 sections (service, system, runtime, request, endpoints)
✓ Service name, version, framework
✓ System fields presence and types
✓ Uptime format and validity
✓ Request info (method, path) |
+| `GET /health` | ✓ Status code 200
✓ Status = "healthy"
✓ Timestamp in ISO format
✓ Uptime seconds (positive integer) |
+| Error handling | ✓ 404 Not Found response structure
✓ Error message format |
+
+**Total tests:** 17 unit tests
+
+### CI Workflow Triggers
+The workflow runs on:
+- **Push** to `master` and `lab03` branches
+- **Pull request** targeting `master` branch
+
+**Why?**
+- Push triggers ensure every commit is tested
+- PR triggers catch issues before merging
+- Lab03 branch is included for active development
+
+### Versioning Strategy: Calendar Versioning (CalVer)
+I chose **CalVer** with format `YYYY.MM` (e.g., `2025.02`)
+
+**Rationale:**
+- This is a **service**, not a library — breaking changes don't require SemVer
+- Time-based versions are **easy to understand** and correlate with release dates
+- Perfect for **continuous deployment** — new version every month
+- Simple to implement in CI using `date` command
+
+**Docker tags:**
+- `spalkkina/devops-info-service:2026.02` — monthly version
+- `spalkkina/devops-info-service:latest` — most recent build
+
+---
+
+## 2. Workflow Evidence
+
+### Successful GitHub Actions Run
+🔗 [Link to workflow run](https://github.com/angel-palkina/DevOps-Core-Course/actions/runs/21921911777)
+
+### Tests Passing Locally
+
+
+### Docker Hub Image
+🔗 [Docker Hub repository](https://hub.docker.com/r/spalkkina/devops-info-service)
+
+| Tag | Size |
+|----------|-----------|
+| 2026.02 | 45.54 MB |
+| latest | 45.54 MB |
+
+### CI/CD Status
+
+
+
+## Best Practices Implemented
+- **Practice 1:** Dependency Caching
+
+ What: Caching pip packages using actions/setup-python@v5 built-in cache
+
+ Why: Speeds up workflow by ~104 seconds (no need to download packages every time)
+
+ Before: 2m 05s → After: 1m 01s
+
+- **Practice 2:** Job Dependencies (Fail Fast)
+
+ What: Docker job has needs: test — only runs if tests pass
+
+ Why: Prevents publishing broken images to Docker Hub
+
+- **Practice 3:** Conditional Execution
+
+ What: Docker job runs only on push, not on PRs
+
+ Why: Avoid pushing images from temporary PR branches
+
+- **Practice 4:** Security Scanning with Snyk
+
+ What: Integrated Snyk to scan dependencies for vulnerabilities
+
+ Why: Catch security issues before they reach production
+
+ Snyk Results: no vulnerable paths found.
+
+- **Practice 5:** Linting
+
+ What: flake8 runs on every commit
+
+ Why: Enforces code style consistency, catches syntax errors early
+
+## Key Decisions
+
+### Versioning Strategy: CalVer
+**Why CalVer?** This is a web service, not a shared library. Users don't need to know about breaking changes via version numbers — they just consume the latest API. CalVer clearly communicates when the image was built, which is more useful for operations teams.
+
+Alternative considered: SemVer — rejected because our app has no API consumers that pin versions.
+
+### Docker Tags
+What tags are created?
+
+- YYYY.MM (e.g., 2026.02) — monthly version
+
+- latest — points to the most recent build
+
+**Why two tags?**
+
+- latest is convenient for development and testing
+
+- Date-based tag provides a stable reference for production rollbacks
+
+### Workflow Triggers
+Why push + PR?
+
+- Push triggers ensure every commit is tested immediately
+
+- PR triggers prevent merging broken code into master
+
+Lab03 branch is included to test the workflow itself during development
+
+### Test Coverage
+What is tested?
+
+- All happy paths (200 OK responses)
+
+- Response structure and data types
+
+- Error handlers (404)
+
+- Helper functions
+
+What is NOT tested?
+
+- 500 error handler (requires mocking internal errors)
+
+- Actual hostname value (changes per environment)
+
+- IP address format (varies in CI)
+
diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png
new file mode 100644
index 0000000000..ac11fa5644
Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ
diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png
new file mode 100644
index 0000000000..cf67c921ae
Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ
diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png
new file mode 100644
index 0000000000..8f72401155
Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ
diff --git a/app_python/docs/screenshots/04 -terminal-build-output.png b/app_python/docs/screenshots/04 -terminal-build-output.png
new file mode 100644
index 0000000000..93764e5f0b
Binary files /dev/null and b/app_python/docs/screenshots/04 -terminal-build-output.png differ
diff --git a/app_python/docs/screenshots/05-run-app-output.png b/app_python/docs/screenshots/05-run-app-output.png
new file mode 100644
index 0000000000..baf13ebf1a
Binary files /dev/null and b/app_python/docs/screenshots/05-run-app-output.png differ
diff --git a/app_python/docs/screenshots/06-endpoint-test-with-docker.png b/app_python/docs/screenshots/06-endpoint-test-with-docker.png
new file mode 100644
index 0000000000..cb7be8e994
Binary files /dev/null and b/app_python/docs/screenshots/06-endpoint-test-with-docker.png differ
diff --git a/app_python/docs/screenshots/07-test-output.png b/app_python/docs/screenshots/07-test-output.png
new file mode 100644
index 0000000000..b2289cb27b
Binary files /dev/null and b/app_python/docs/screenshots/07-test-output.png differ
diff --git a/app_python/docs/screenshots/08-test-coverage-output.png b/app_python/docs/screenshots/08-test-coverage-output.png
new file mode 100644
index 0000000000..2f14bea072
Binary files /dev/null and b/app_python/docs/screenshots/08-test-coverage-output.png differ
diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt
new file mode 100644
index 0000000000..d3443530ee
--- /dev/null
+++ b/app_python/requirements-dev.txt
@@ -0,0 +1,10 @@
+Flask==2.3.3
+
+# Testing
+pytest==8.0.0
+pytest-cov==5.0.0
+
+# Linting
+pylint==3.0.3
+flake8==7.0.0
+black==24.2.0
\ No newline at end of file
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..044e6f9d17
--- /dev/null
+++ b/app_python/tests/test_app.py
@@ -0,0 +1,192 @@
+"""
+Unit tests for DevOps Info Service Flask application.
+"""
+
+import pytest
+import sys
+import os
+from datetime import datetime
+
+# Добавляем путь к app.py чтобы импортировать
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from app import app, get_system_info, get_uptime, get_service_info
+
+@pytest.fixture
+def client():
+ """Фикстура: создаём тестовый клиент Flask"""
+ app.config['TESTING'] = True
+ with app.test_client() as client:
+ yield client
+
+# ========== ТЕСТЫ ДЛЯ ЭНДПОИНТА / ==========
+
+def test_index_status_code(client):
+ """Тест 1: Проверяем, что главная страница возвращает 200 OK"""
+ response = client.get('/')
+ assert response.status_code == 200
+
+def test_index_is_json(client):
+ """Тест 2: Проверяем, что ответ — это JSON"""
+ response = client.get('/')
+ assert response.is_json
+
+def test_index_has_required_sections(client):
+ """Тест 3: Проверяем, что есть все основные секции"""
+ response = client.get('/')
+ 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_index_service_info(client):
+ """Тест 4: Проверяем информацию о сервисе"""
+ response = client.get('/')
+ service = response.get_json()['service']
+
+ assert service['name'] == 'devops-info-service'
+ assert service['version'] == '1.0.0'
+ assert service['framework'] == 'Flask'
+ assert isinstance(service['description'], str)
+
+def test_index_system_info_fields(client):
+ """Тест 5: Проверяем, что системная информация содержит все поля"""
+ response = client.get('/')
+ system = response.get_json()['system']
+
+ expected_fields = ['hostname', 'platform', 'platform_version',
+ 'architecture', 'cpu_count', 'python_version']
+
+ for field in expected_fields:
+ assert field in system
+
+ # Проверяем типы данных
+ assert isinstance(system['hostname'], str)
+ assert isinstance(system['cpu_count'], int)
+ assert isinstance(system['python_version'], str)
+
+def test_index_runtime_info(client):
+ """Тест 6: Проверяем информацию о времени работы"""
+ response = client.get('/')
+ runtime = response.get_json()['runtime']
+
+ assert 'uptime_seconds' in runtime
+ assert 'uptime_human' in runtime
+ assert 'current_time' in runtime
+ assert 'timezone' in runtime
+
+ # uptime_seconds должен быть положительным числом
+ assert runtime['uptime_seconds'] >= 0
+ assert isinstance(runtime['uptime_seconds'], int)
+
+def test_index_request_info(client):
+ """Тест 7: Проверяем информацию о запросе"""
+ response = client.get('/')
+ request_info = response.get_json()['request']
+
+ assert 'client_ip' in request_info
+ assert 'user_agent' in request_info
+ assert 'method' in request_info
+ assert 'path' in request_info
+
+ assert request_info['method'] == 'GET'
+ assert request_info['path'] == '/'
+
+def test_index_endpoints_list(client):
+ """Тест 8: Проверяем список эндпоинтов"""
+ response = client.get('/')
+ endpoints = response.get_json()['endpoints']
+
+ assert isinstance(endpoints, list)
+ assert len(endpoints) >= 2
+
+ # Проверяем, что есть / и /health
+ paths = [e['path'] for e in endpoints]
+ assert '/' in paths
+ assert '/health' in paths
+
+# ========== ТЕСТЫ ДЛЯ ЭНДПОИНТА /HEALTH ==========
+
+def test_health_status_code(client):
+ """Тест 9: Проверяем, что health endpoint доступен"""
+ response = client.get('/health')
+ assert response.status_code == 200
+
+def test_health_is_json(client):
+ """Тест 10: Проверяем, что health возвращает JSON"""
+ response = client.get('/health')
+ assert response.is_json
+
+def test_health_response_structure(client):
+ """Тест 11: Проверяем структуру ответа health"""
+ response = client.get('/health')
+ data = response.get_json()
+
+ assert 'status' in data
+ assert 'timestamp' in data
+ assert 'uptime_seconds' in data
+
+ assert data['status'] == 'healthy'
+ assert data['uptime_seconds'] >= 0
+
+def test_health_timestamp_format(client):
+ """Тест 12: Проверяем, что timestamp в правильном формате"""
+ response = client.get('/health')
+ timestamp = response.get_json()['timestamp']
+
+ # Пробуем распарсить ISO формат даты
+ try:
+ datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
+ is_valid = True
+ except ValueError:
+ is_valid = False
+
+ assert is_valid
+
+# ========== ТЕСТЫ ДЛЯ ОБРАБОТЧИКОВ ОШИБОК ==========
+
+def test_404_not_found(client):
+ """Тест 13: Проверяем, что несуществующий путь возвращает 404"""
+ response = client.get('/non-existent-path')
+ assert response.status_code == 404
+
+ data = response.get_json()
+ assert 'error' in data
+ assert 'message' in data
+ assert data['error'] == 'Not Found'
+
+def test_method_not_allowed(client):
+ """Тест 14: Проверяем POST на GET endpoint"""
+ response = client.post('/')
+ assert response.status_code in [405, 404] # 405 Method Not Allowed
+
+# ========== ТЕСТЫ ДЛЯ ХЕЛПЕР-ФУНКЦИЙ ==========
+
+def test_get_system_info_function():
+ """Тест 15: Проверяем функцию get_system_info"""
+ info = get_system_info()
+
+ assert isinstance(info, dict)
+ assert 'hostname' in info
+ assert 'platform' in info
+ assert 'cpu_count' in info
+ assert info['cpu_count'] > 0
+
+def test_get_uptime_function():
+ """Тест 16: Проверяем функцию get_uptime"""
+ uptime = get_uptime()
+
+ assert 'seconds' in uptime
+ assert 'human' in uptime
+ assert uptime['seconds'] >= 0
+ assert isinstance(uptime['human'], str)
+
+def test_get_service_info_function():
+ """Тест 17: Проверяем функцию get_service_info"""
+ info = get_service_info()
+
+ assert info['name'] == 'devops-info-service'
+ assert info['version'] == '1.0.0'
\ No newline at end of file
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..39ac3d6c97
--- /dev/null
+++ b/pulumi/Pulumi.yaml
@@ -0,0 +1,11 @@
+name: project
+description: A minimal Python Pulumi program
+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..2ba54cc26d
--- /dev/null
+++ b/pulumi/__main__.py
@@ -0,0 +1,89 @@
+"""Pulumi program for Yandex Cloud VM infrastructure"""
+
+import pulumi
+import pulumi_yandex as yandex
+import json
+
+# Конфигурация
+config = pulumi.Config()
+cloud_id = config.get("cloud_id") or "b1ghfahdukhmskkq1sh"
+folder_id = config.get("folder_id") or "b1goafhlbrpmfacul97b"
+zone = config.get("zone") or "ru-central1-a"
+
+# Читаем SSH ключ
+with open("C:/Users/sofia/.ssh/id_rsa.pub", "r") as f:
+ ssh_key = f.read().strip()
+
+# Cloud-init конфигурация для пользователя
+user_data = f"""#cloud-config
+users:
+ - name: ubuntu
+ groups: sudo
+ shell: /bin/bash
+ sudo: ['ALL=(ALL) NOPASSWD:ALL']
+ ssh-authorized-keys:
+ - {ssh_key}
+"""
+
+# Создаем VPC сеть
+network = yandex.VpcNetwork("network-1",
+ name="pulumi-network",
+ labels={
+ "environment": "dev",
+ "project": "devops-lab4",
+ "owner": "angel-palkina",
+ "managed_by": "pulumi"
+ }
+)
+
+# Создаем подсеть
+subnet = yandex.VpcSubnet("subnet-1",
+ name="pulumi-subnet",
+ zone=zone,
+ network_id=network.id,
+ v4_cidr_blocks=["192.168.20.0/24"],
+ labels={
+ "environment": "dev",
+ "project": "devops-lab4",
+ "owner": "angel-palkina",
+ "managed_by": "pulumi"
+ }
+)
+
+# Создаем виртуальную машину
+vm = yandex.ComputeInstance("vm-1",
+ name="pulumi-vm",
+ platform_id="standard-v2",
+ zone=zone,
+ resources=yandex.ComputeInstanceResourcesArgs(
+ cores=2,
+ memory=2,
+ ),
+ boot_disk=yandex.ComputeInstanceBootDiskArgs(
+ initialize_params=yandex.ComputeInstanceBootDiskInitializeParamsArgs(
+ image_id="fd8autg36kchufhej85b", # Ubuntu 22.04
+ size=10,
+ ),
+ ),
+ network_interfaces=[yandex.ComputeInstanceNetworkInterfaceArgs(
+ subnet_id=subnet.id,
+ nat=True,
+ )],
+ metadata={
+ "user-data": user_data
+ },
+ labels={
+ "environment": "dev",
+ "project": "devops-lab4",
+ "owner": "angel-palkina",
+ "managed_by": "pulumi"
+ }
+)
+
+# Outputs
+pulumi.export("vm_id", vm.id)
+pulumi.export("vm_name", vm.name)
+pulumi.export("vm_external_ip", vm.network_interfaces[0].nat_ip_address)
+pulumi.export("vm_internal_ip", vm.network_interfaces[0].ip_address)
+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..bc4e43087b
--- /dev/null
+++ b/pulumi/requirements.txt
@@ -0,0 +1 @@
+pulumi>=3.0.0,<4.0.0
diff --git a/terraform/.gitignore b/terraform/.gitignore
new file mode 100644
index 0000000000..d2e8cdec48
--- /dev/null
+++ b/terraform/.gitignore
@@ -0,0 +1,24 @@
+# Local .terraform directories
+.terraform/
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Sensitive files
+key.json
+*.tfvars
+*.tfvars.json
+
+# Lock file (опционально)
+.terraform.lock.hcl
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Override files
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
\ No newline at end of file
diff --git a/terraform/docs/LAB04-bonus.md b/terraform/docs/LAB04-bonus.md
new file mode 100644
index 0000000000..ce4d3854ef
--- /dev/null
+++ b/terraform/docs/LAB04-bonus.md
@@ -0,0 +1,122 @@
+# Lab 04 Bonus: IaC CI/CD Pipeline
+
+**Author:** Angel Palkina
+**Date:** 2026-02-19
+**Course:** DevOps Core Course
+
+
+## 🛠️ Tools & Technologies
+
+| Tool | Version | Purpose |
+|------|---------|---------|
+| **GitHub Actions** | - | CI/CD automation platform |
+| **Terraform** | 1.10.x | Infrastructure as Code |
+| **TFLint** | 0.61.0 | Terraform linter |
+| **tfsec** | 1.0.3 | Security scanner for Terraform |
+| **github-script** | v7 | PR automation |
+
+---
+
+## Workflow Implementation
+
+### File Structure
+
+```
+.github/workflows/
+├── python-ci.yml # Python application CI (improved with path filters)
+└── terraform-ci.yml # NEW: Terraform validation workflow
+```
+
+
+## Path Filter Configuration
+
+### Problem Statement
+
+**Before:** All workflows triggered on any file change
+- Python CI ran on Terraform changes
+- Wasted GitHub Actions minutes
+- Confusing workflow results
+
+**After:** Workflows trigger only on relevant files
+- Python CI → `app_python/**` only
+- Terraform CI → `terraform/**` only
+- Efficient resource usage
+
+
+## TFLint Configuration & Results
+
+### Local Testing Results
+
+**Commands executed:**
+
+```bash
+cd terraform
+
+# Format check
+terraform fmt -check -recursive
+# No output = all files formatted correctly
+
+# Initialize
+terraform init -backend=false
+# Success
+
+# Validate
+terraform validate
+# Success! The configuration is valid.
+
+# TFLint
+tflint --init
+# Installing plugins...
+
+tflint --recursive
+# No issues found!
+```
+
+**All checks passed locally before pushing to CI! **
+
+
+## Workflow Execution Examples
+
+### Example 1: Successful Validation (Push to branch)
+
+
+
+### Example 2: Pull Request with Comments
+
+**Trigger:** Create PR from `lab04-bonus` to `lab04`
+
+
+
+### Example 3: Format Error Detection
+
+**Scenario:** Intentionally break formatting
+
+**Workflow Result:**
+
+```
+✓ Terraform Format Check (⚠️ warning)
+ Error: main.tf needs formatting
+
+✓ Terraform Init (✅ success)
+✓ Terraform Validate (✅ success)
+✓ Run TFLint (✅ success)
+⚠️ Warning if format or lint fails
+ Format status: failure
+ Please fix these issues, but workflow will not fail.
+```
+
+**PR Comment:**
+
+```markdown
+| **Terraform Format** | `failure` |
+
+❌ Some files need formatting. Run: `terraform fmt -recursive`
+```
+
+**Workflow:** Completes with warning (doesn't fail)
+
+
+## 📚 References & Resources
+
+**Documentation:**
+- [GitHub Actions Documentation](https://docs.github.com/en/actions)
diff --git a/terraform/docs/LAB04.md b/terraform/docs/LAB04.md
new file mode 100644
index 0000000000..fdbb14418b
--- /dev/null
+++ b/terraform/docs/LAB04.md
@@ -0,0 +1,329 @@
+# Lab 04: Infrastructure as Code with Terraform and Pulumi
+
+**Author:** Sofia Palkina
+**Date:** 2026-02-19
+**Course:** DevOps Core Course
+
+
+## 🛠️ Tools Used
+
+| Tool | Version | Purpose |
+|------|---------|---------|
+| **Terraform** | v1.10.6 | Infrastructure as Code (HCL) |
+| **Pulumi** | v3.222.0 | Infrastructure as Code (Python) |
+| **Yandex Cloud CLI** | Latest | Cloud provider CLI |
+| **Python** | 3.12.10 | Pulumi runtime |
+| **Git** | Latest | Version control |
+
+
+## Cloud Provider & Infrastructure
+
+### Cloud Provider Selection
+
+**Provider:** Yandex Cloud
+
+**Rationale:**
+- ✅ **Free Tier Available** - 60-day trial with ₽4000 (~$50) free credits
+- ✅ **Russian Data Centers** - Low latency for local users
+- ✅ **Terraform Support** - Official provider available
+- ✅ **Pulumi Support** - Community provider available
+- ✅ **Good Documentation** - Comprehensive guides in Russian/English
+- ✅ **Educational Focus** - Suitable for learning IaC concepts
+
+### Instance Configuration
+
+**Instance Type:** `standard-v2`
+
+**Specifications:**
+- **vCPU:** 2 cores
+- **RAM:** 2 GB
+- **Disk:** 10 GB SSD
+- **OS:** Ubuntu 22.04 LTS (Jammy Jellyfish)
+- **Image ID:** `fd8autg36kchufhej85b`
+
+### Region & Zone Selection
+
+**Region:** `ru-central1` (Moscow)
+**Zone:** `ru-central1-a`
+
+### Cost Analysis
+
+| Resource | Specification | Monthly Cost | Lab Cost |
+|----------|--------------|--------------|----------|
+| **VM Instance** | 2 vCPU, 2GB RAM | ~₽600 (~$7) | ₽0 (Free Tier) |
+| **Public IP** | 1 static IP | ~₽150 (~$2) | ₽0 (Free Tier) |
+| **Disk Storage** | 10 GB SSD | ~₽50 (~$0.60) | ₽0 (Free Tier) |
+| **Network Traffic** | Minimal (<1GB) | ~₽10 (~$0.12) | ₽0 (Free Tier) |
+| **Total** | - | ~₽810 (~$10) | **₽0** |
+
+
+### Resources Created
+
+#### Terraform Infrastructure
+
+| Resource Type | Resource Name | ID | Purpose |
+|--------------|---------------|-----|---------|
+| **VPC Network** | `terraform-network` | `enp79hgfkbvn1shvdt0r` | Virtual network isolation |
+| **Subnet** | `terraform-subnet` | `e9bvuhc7rgc0pjm4njrq` | IP address range (192.168.10.0/24) |
+| **VM Instance** | `terraform-vm` | `fhmgbm8ggh1ktbvk2r31` | Ubuntu 22.04 compute instance |
+| **Public IP** | Auto-assigned | `158.160.148.30` | External access via NAT |
+
+**Total Resources:** 4 (including implicit NAT gateway)
+
+#### Pulumi Infrastructure
+
+| Resource Type | Resource Name | ID | Purpose |
+|--------------|---------------|-----|---------|
+| **VPC Network** | `pulumi-network` | `enpb06dvuvmq35tmgo6u` | Virtual network isolation |
+| **Subnet** | `pulumi-subnet` | `e9bt7r810el7oc3jjvem` | IP address range (192.168.20.0/24) |
+| **VM Instance** | `pulumi-vm` | `fhm8t59hlemrrv4miqt7` | Ubuntu 22.04 compute instance |
+| **Public IP** | Auto-assigned | `46.21.245.184` | External access via NAT |
+
+**Total Resources:** 4 (including implicit NAT gateway)
+
+**Resource Labels (Both Implementations):**
+```yaml
+environment: dev
+project: devops-lab4
+owner: angel-palkina
+managed_by: terraform/pulumi
+```
+
+---
+
+## Terraform Infrastructure
+
+### Project Structure Explanation
+
+```
+terraform/
+├── main.tf # Main infrastructure definition
+│ ├── Provider configuration (Yandex Cloud)
+│ ├── VPC Network resource
+│ ├── Subnet resource
+│ └── VM Instance resource
+│
+├── variables.tf # Input variable declarations
+│ ├── cloud_id (Yandex Cloud ID)
+│ ├── folder_id (Yandex Folder ID)
+│ └── zone (Deployment zone)
+│
+├── outputs.tf # Output value definitions
+│ ├── VM external IP
+│ ├── VM internal IP
+│ ├── Network ID
+│ └── Subnet ID
+│
+├── terraform.tfvars # Variable values (gitignored)
+├── key.json # Service account key (gitignored)
+├── cloud-init.yaml # VM initialization script
+├── .terraform/ # Provider plugins (gitignored)
+└── terraform.tfstate # State file (gitignored)
+```
+
+**Key Design Decisions:**
+
+1. **Separate Variable Files**
+ - `variables.tf` - declarations only
+ - `terraform.tfvars` - actual values
+ - **Benefit:** Reusability across environments
+
+2. **Cloud-Init Template**
+ - Separate YAML file for user-data
+ - SSH key injected via `templatefile()`
+ - **Benefit:** Clean separation of concerns
+
+3. **Resource Labeling**
+ - Consistent labels across all resources
+ - Includes: environment, project, owner, managed_by
+ - **Benefit:** Easy resource tracking and cost allocation
+
+4. **Output Values**
+ - Exported IPs and resource IDs
+ - Used for connecting/referencing resources
+ - **Benefit:** Easy integration with other tools
+
+### Installation
+```
+terraform version
+# Output: Terraform v1.10.6
+```
+
+### Deployment Process
+
+```bash
+
+# Validate configuration
+terraform validate
+
+# Plan changes
+terraform plan
+
+# Apply configuration
+terraform apply
+```
+
+
+### Terraform Results
+
+```
+Outputs:
+
+network_id = "enp79hgfkbvn1shvdt0r"
+subnet_id = "e9bvuhc7rgc0pjm4njrq"
+vm_external_ip = "158.160.148.30"
+vm_id = "fhmgbm8ggh1ktbvk2r31"
+vm_internal_ip = "192.168.10.8"
+vm_name = "terraform-vm"
+```
+
+**SSH Connection**
+
+```bash
+ssh ubuntu@158.160.148.30
+```
+
+
+**Terraform destroy output**
+
+## Pulumi Infrastructure
+
+### Installation
+
+Programming language chosen for Pulumi - Python
+
+pulumi version - v3.222.0
+
+
+### Deployment Process
+
+```powershell
+# Set environment variable for service account key
+$env:YC_SERVICE_ACCOUNT_KEY_FILE = "key.json"
+
+# Configure Pulumi
+pulumi config set yandex:cloud_id b1ghfahdukhmskkq1sh
+pulumi config set yandex:folder_id b1goafhlbrpmfacul97b
+pulumi config set yandex:zone ru-central1-a
+
+# Preview changes
+pulumi preview
+
+```
+
+```powershell
+# Deploy infrastructure
+pulumi up
+```
+
+
+### Pulumi Outputs
+
+```
+Outputs:
+ network_id : "enpb06dvuvmq35tmgo6u"
+ subnet_id : "e9bt7r810el7oc3jjvem"
+ vm_external_ip: "46.21.245.184"
+ vm_id : "fhm8t59hlemrrv4miqt7"
+ vm_internal_ip: "192.168.20.16"
+ vm_name : "pulumi-vm"
+
+```
+
+**SSH Connection Test:**
+
+```bash
+ssh ubuntu@46.21.245.184
+```
+
+
+## Comparison: Terraform vs Pulumi
+
+| Aspect | Terraform | Pulumi |
+|--------|-----------|--------|
+| **Language** | HCL (HashiCorp Configuration Language) | Python (also TypeScript, Go, C#) |
+| **Learning Curve** | Low - declarative DSL | Medium - requires programming knowledge |
+| **State Management** | Local or remote backend | Local or Pulumi Cloud |
+| **Type Safety** | Limited | Strong (with programming language) |
+| **Testing** | Limited native support | Full unit/integration testing support |
+| **Code Reusability** | Modules | Functions, classes, packages |
+| **Debugging** | Limited | Full IDE debugging support |
+| **Community** | Very large, mature | Growing rapidly |
+| **Deployment Time** | ~60s (3 resources) | ~54s (4 resources) |
+| **Resource Definition** | Declarative blocks | Object-oriented code |
+| **Documentation** | Extensive | Good, improving |
+
+
+
+### Terraform Advantages:
+- **Simpler syntax** - easier for beginners
+- **Declarative approach** - clear infrastructure definition
+- **Wide adoption** - industry standard
+- **Better documentation** - comprehensive resources
+
+### Pulumi Advantages:
+- **Real programming language** - Python/TypeScript/Go
+- **Better code reusability** - functions, classes, modules
+- **IDE support** - autocomplete, refactoring, debugging
+- **Testing capabilities** - unit tests, mocking
+- **Dynamic configuration** - loops, conditionals, etc.
+
+## When to Use What?
+
+**Use Terraform when:**
+- Team prefers declarative approach
+- Need maximum provider support
+- Working with existing Terraform modules
+- Simple infrastructure without complex logic
+
+**Use Pulumi when:**
+- Team has strong programming background
+- Need complex logic and abstractions
+- Want to write unit tests for infrastructure
+- Prefer using familiar programming languages
+
+
+## Challenges
+
+### 1. Python Version Compatibility
+**Problem:** Pulumi provider `pulumi-yandex` v0.13.0 requires `pkg_resources` which is deprecated in Python 3.14.
+
+**Solution:** Downgraded to Python 3.12.10 and installed older setuptools:
+```powershell
+pip install setuptools==69.0.0
+```
+
+### Lab 5 VM Decision
+
+**Strategy:** **Destroying both VMs. Will recreate cloud VM with Terraform**
+
+**Rationale:**
+- Practice infrastructure destruction (important IaC skill)
+- Leverages Lab 4 learning
+- Can modify configuration as needed for Lab 5
+
+### Post-Cleanup Verification
+
+**Check all resources are deleted:**
+
+```bash
+# List all VMs (should be empty)
+yc compute instance list
+
+# List all networks (should only show default)
+yc vpc network list
+
+# List all subnets (should only show defaults)
+yc vpc subnet list
+
+# Check remaining free tier credits
+yc billing account list
+```
+
+## 📚 References
+
+- [Terraform Documentation](https://www.terraform.io/docs)
+- [Pulumi Documentation](https://www.pulumi.com/docs/)
+- [Yandex Cloud Terraform Provider](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs)
+- [Yandex Cloud Pulumi Provider](https://www.pulumi.com/registry/packages/yandex/)
+
diff --git a/terraform/docs/images/image-1.png b/terraform/docs/images/image-1.png
new file mode 100644
index 0000000000..81f4480951
Binary files /dev/null and b/terraform/docs/images/image-1.png differ
diff --git a/terraform/docs/images/image-2.png b/terraform/docs/images/image-2.png
new file mode 100644
index 0000000000..0d532feefc
Binary files /dev/null and b/terraform/docs/images/image-2.png differ
diff --git a/terraform/docs/images/image-3.png b/terraform/docs/images/image-3.png
new file mode 100644
index 0000000000..e84dbd3f84
Binary files /dev/null and b/terraform/docs/images/image-3.png differ
diff --git a/terraform/docs/images/image-4.png b/terraform/docs/images/image-4.png
new file mode 100644
index 0000000000..453df3908a
Binary files /dev/null and b/terraform/docs/images/image-4.png differ
diff --git a/terraform/docs/images/image-5.png b/terraform/docs/images/image-5.png
new file mode 100644
index 0000000000..dfd20c1963
Binary files /dev/null and b/terraform/docs/images/image-5.png differ
diff --git a/terraform/docs/images/image-6.png b/terraform/docs/images/image-6.png
new file mode 100644
index 0000000000..447b9b073a
Binary files /dev/null and b/terraform/docs/images/image-6.png differ
diff --git a/terraform/docs/images/image-7.png b/terraform/docs/images/image-7.png
new file mode 100644
index 0000000000..b4aa9bbfc8
Binary files /dev/null and b/terraform/docs/images/image-7.png differ
diff --git a/terraform/docs/images/image.png b/terraform/docs/images/image.png
new file mode 100644
index 0000000000..ee80feeb74
Binary files /dev/null and b/terraform/docs/images/image.png differ
diff --git a/terraform/main.tf b/terraform/main.tf
new file mode 100644
index 0000000000..7e89ade5d3
--- /dev/null
+++ b/terraform/main.tf
@@ -0,0 +1,67 @@
+resource "yandex_compute_instance" "vm-1" {
+ name = var.vm_name
+ platform_id = "standard-v2"
+ zone = var.zone
+
+ labels = {
+ environment = var.environment
+ project = var.project
+ owner = var.owner
+ managed_by = "terraform"
+ }
+
+ resources {
+ cores = var.vm_cores
+ memory = var.vm_memory
+ }
+
+ boot_disk {
+ initialize_params {
+ image_id = var.vm_image_id
+ size = var.vm_disk_size
+ }
+ }
+
+ network_interface {
+ subnet_id = yandex_vpc_subnet.subnet-1.id
+ nat = true
+ }
+
+ metadata = {
+ user-data = <<-EOT
+ #cloud-config
+ users:
+ - name: ubuntu
+ groups: sudo
+ shell: /bin/bash
+ sudo: ['ALL=(ALL) NOPASSWD:ALL']
+ ssh-authorized-keys:
+ - ${file(pathexpand(var.ssh_public_key_path))}
+ EOT
+ }
+}
+
+resource "yandex_vpc_network" "network-1" {
+ name = var.network_name
+
+ labels = {
+ environment = var.environment
+ project = var.project
+ owner = var.owner
+ managed_by = "terraform"
+ }
+}
+
+resource "yandex_vpc_subnet" "subnet-1" {
+ name = var.subnet_name
+ zone = var.zone
+ network_id = yandex_vpc_network.network-1.id
+ v4_cidr_blocks = var.subnet_cidr
+
+ labels = {
+ environment = var.environment
+ project = var.project
+ owner = var.owner
+ managed_by = "terraform"
+ }
+}
\ No newline at end of file
diff --git a/terraform/outputs.tf b/terraform/outputs.tf
new file mode 100644
index 0000000000..0e48f77954
--- /dev/null
+++ b/terraform/outputs.tf
@@ -0,0 +1,14 @@
+output "vm_external_ip" {
+ description = "External IP address of the VM"
+ value = yandex_compute_instance.vm-1.network_interface[0].nat_ip_address
+}
+
+output "vm_internal_ip" {
+ description = "Internal IP address of the VM"
+ value = yandex_compute_instance.vm-1.network_interface[0].ip_address
+}
+
+output "vm_id" {
+ description = "ID of the VM"
+ value = yandex_compute_instance.vm-1.id
+}
\ No newline at end of file
diff --git a/terraform/provider.tf b/terraform/provider.tf
new file mode 100644
index 0000000000..2931f24f38
--- /dev/null
+++ b/terraform/provider.tf
@@ -0,0 +1,18 @@
+terraform {
+ required_providers {
+ yandex = {
+ source = "yandex-cloud/yandex"
+ version = "~> 0.100"
+ }
+ }
+ required_version = ">= 1.0"
+}
+
+provider "yandex" {
+ # Service account key file is only used locally
+ # In CI/CD, authentication is skipped (validation only)
+ service_account_key_file = fileexists("${path.module}/key.json") ? "${path.module}/key.json" : null
+ cloud_id = var.cloud_id
+ folder_id = var.folder_id
+ zone = var.zone
+}
\ No newline at end of file
diff --git a/terraform/variables.tf b/terraform/variables.tf
new file mode 100644
index 0000000000..9ec8c83130
--- /dev/null
+++ b/terraform/variables.tf
@@ -0,0 +1,89 @@
+variable "cloud_id" {
+ description = "Yandex Cloud ID"
+ type = string
+ default = "b1ghfahdukhmskkq1sh"
+}
+
+variable "folder_id" {
+ description = "Yandex Cloud Folder ID"
+ type = string
+ default = "b1goafhlbrpmfacul97b"
+}
+
+variable "zone" {
+ description = "Yandex Cloud Zone"
+ type = string
+ default = "ru-central1-a"
+}
+
+variable "vm_name" {
+ description = "Name of the virtual machine"
+ type = string
+ default = "terraform-vm"
+}
+
+variable "vm_cores" {
+ description = "Number of CPU cores"
+ type = number
+ default = 2
+}
+
+variable "vm_memory" {
+ description = "Amount of memory in GB"
+ type = number
+ default = 2
+}
+
+variable "vm_disk_size" {
+ description = "Boot disk size in GB"
+ type = number
+ default = 10
+}
+
+variable "vm_image_id" {
+ description = "OS image ID (Ubuntu 22.04)"
+ type = string
+ default = "fd8autg36kchufhej85b"
+}
+
+variable "ssh_public_key_path" {
+ description = "Path to SSH public key"
+ type = string
+ default = "~/.ssh/id_rsa.pub"
+}
+
+variable "network_name" {
+ description = "Name of the VPC network"
+ type = string
+ default = "network1"
+}
+
+variable "subnet_name" {
+ description = "Name of the subnet"
+ type = string
+ default = "subnet1"
+}
+
+variable "subnet_cidr" {
+ description = "CIDR block for subnet"
+ type = list(string)
+ default = ["192.168.10.0/24"]
+}
+
+variable "environment" {
+ description = "Environment name (dev, staging, prod)"
+ type = string
+ default = "dev"
+}
+
+variable "project" {
+ description = "Project name"
+ type = string
+ default = "devops-lab4"
+}
+
+variable "owner" {
+ description = "Owner of the resources"
+ type = string
+ default = "angel-palkina"
+}
\ No newline at end of file