diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..5e03dd9bd9 --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,76 @@ +name: Ansible Deployment + +on: + push: + branches: [ master, lab06 ] + paths: + - 'ansible/**' + - '.github/workflows/ansible-deploy.yml' + + pull_request: + branches: [ main, master ] + paths: + - 'ansible/**' + +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 dependencies + run: | + pip install ansible ansible-lint + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/*.yml + continue-on-error: true + + + deploy: + name: Deploy Application + needs: lint + runs-on: ubuntu-latest + + steps: + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Ansible + 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: Deploy with Ansible + 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 diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..24d01d9f21 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,82 @@ +name: Python CI & Docker Build + +on: + push: + branches: + - master + - lab03 + pull_request: + branches: + - master + + +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.SNYK_TOKEN }} + + - name: Run Snyk security scan + run: | + cd app_python + snyk test --severity-threshold=high + + + + + docker: + needs: test + runs-on: ubuntu-latest + # if: github.ref == 'refs/heads/master' + 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: | + mrdebuff/devops-info-service:${{ env.VERSION }} + mrdebuff/devops-info-service:latest diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml new file mode 100644 index 0000000000..25b56da25d --- /dev/null +++ b/.github/workflows/terraform-ci.yml @@ -0,0 +1,46 @@ +name: Terraform CI Validation + +on: + pull_request: + paths: + - 'terraform/**' + - '.github/workflows/terraform-ci.yml' + +jobs: + terraform-validate: + name: Terraform Validation + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: terraform + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.14.5 + + - name: Terraform Format Check + run: terraform fmt -check -recursive + + - name: Terraform Init + run: terraform init -backend=false + + - name: Terraform Validate + run: terraform validate + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v3 + with: + tflint_version: latest + + - name: Initialize TFLint + run: tflint --init + + - name: Run TFLint + run: tflint --format compact diff --git a/.gitignore b/.gitignore index 30d74d2584..a18ba139d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,24 @@ -test \ No newline at end of file +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Test cache +.pytest_cache + +# Ansible +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ + +#env +monitoring/.env \ No newline at end of file diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000000..e5e80cd903 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1 @@ +[![Ansible Deployment](https://github.com/MrDeBuFF/DevOps-Core-Course/actions/workflows/ansible-deploy.yml/badge.svg)](https://github.com/your-username/your-repo/actions/workflows/ansible-deploy.yml) \ 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..ce8695aace --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,298 @@ +# LAB05 — Ansible Fundamentals + +## 1. Architecture Overview + +### Ansible Version + +![](screenshots_l5/p0.png) + +### Target VM + +- Cloud provider: `Yandex Cloud` +- Provisioning tool: `Pulumi` +- OS: + +![](screenshots_l5/p0-1.png) + +- Public IP: `93.77.185.211` + +### Role Structure Diagram + +``` +ansible/ +├── ansible.cfg +├── docs +│ ├── LAB05.md +│ └── screenshots_l5/... +├── inventory +│ ├── group_vars +│ │ └── all.yml +│ └── hosts.ini +├── playbooks +│ ├── deploy.yml +│ └── provision.yml +└── roles + ├── app_deploy + │ ├── defaults + │ │ └── main.yml + │ ├── handlers + │ │ └── main.yml + │ └── tasks + │ └── main.yml + ├── common + │ ├── defaults + │ │ └── main.yml + │ └── tasks + │ └── main.yml + └── docker + ├── defaults + │ └── main.yml + ├── handlers + │ └── main.yml + └── tasks + └── main.yml +``` + +### Why Roles Instead of Monolithic Playbooks + +Roles: +- provide modularity +- separate responsibilities +- allow reuse across projects +- follow Ansible best practices +- improve readability and maintainability + +A monolithic playbook becomes hard to maintain, test, and reuse. + +## 2. Roles Documentation + +### Role: `common` + +### Purpose + +Performs basic system provisioning: +- updates apt cache +- installs essential packages +- configures timezone + +This role prepares any Ubuntu server for further automation. + +### Variables (defaults/main.yml) + +```yaml +common_packages: + - python3-pip + - curl + - git + - vim + - htop +``` + +### Handlers + +None required in this role. + +### Dependencies + +No explicit dependencies, but typically executed before other roles. + +### Role: `docker` + +### Purpose + +Installs and configures Docker Engine: +- adds Docker GPG key +- adds official Docker repository +- installs docker-ce packages +- enables and starts Docker service +- adds user to docker group +- installs Python Docker SDK + +### Variables (defaults/main.yml) + +```yaml +docker_user: ubuntu +``` + +### Handlers (handlers/main.yml) + +```yaml +- name: restart docker + service: + name: docker + state: restarted +``` + +Triggered when repository configuration changes. + +### Dependencies + +Executed after common role (system packages must exist). + +### Role: `app_deploy` + +### Purpose + +Deploys containerized Python application: +- logs into Docker Hub +- pulls image +- runs container +- configures port mapping +- sets restart policy + +### Variables + +From Vault (inventory/group_vars/all.yml): + +```yaml +dockerhub_username: +dockerhub_password: + +app_name: devops-info-service +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: latest +app_port: 5000 +app_container_name: "{{ app_name }}" +``` + +Defaults: + +```yaml +restart_policy: unless-stopped + +app_port_2: 6000 # My port in the container is 6000 instead of 5000 because I use macOS and 5000 is already in use by system services + +app_environment_vars: {} +``` + +### Handlers (handlers/main.yml): + +```yaml +- 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 +``` + +Container restart handler. + +### Dependencies + +Requires: +- Docker role executed first +- Docker daemon running + +## 3. Idempotency Demonstration + +![](screenshots_l5/p1.png) + +### First Run + +![](screenshots_l5/p2.png) + +### Second Run + +![](screenshots_l5/p3.png) + +### Analysis + +First run: +- system state was not configured +- packages and services were installed + +Second run: +- desired state already achieved +- Ansible detected no drift + +### What Makes It Idempotent? +- `apt: state=present` +- `service: state=started` +- `user: append=yes` +- declarative configuration instead of shell commands + +Ansible modules compare current state vs desired state before applying changes. + +## 4. Ansible Vault Usage + +### How you store credentials securely + +Sensitive data stored in: + +``` +inventory/group_vars/all.yml +``` + +Created with: + +```bash +ansible-vault create inventory/group_vars/all.yml +``` + +### Vault password management strategy + +- Password stored locally in `.vault_pass` +- `.vault_pass` added to `.gitignore` +- Not committed to repository +- Used via: + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +### Example of encrypted file + +``` +$ANSIBLE_VAULT;1.1;AES256 +3839326463386438306632346632663166... +``` + +### Why Ansible Vault is important + +- prevents credential leaks +- safe for version control +- protects Docker Hub access tokens +- aligns with security best practices + +## 5. Deployment Verification + +### Terminal output from deploy.yml run + +![](screenshots_l5/p4.png) + +### Container status and Health check verification + +```bash +ansible webservers -a "docker ps" --ask-vault-pass + +curl http://93.77.185.211:5000/health + +curl http://93.77.185.211:5000/ +``` + +![](screenshots_l5/p5.png) + +## 6. Key Decisions + +### Why use roles instead of plain playbooks? + +Roles provide modular architecture and separation of concerns. This improves maintainability and follows industry best practices. + +### How do roles improve reusability? + +Roles encapsulate logic and variables. They can be reused across different environments and projects without modification. + +### What makes a task idempotent? +A task is idempotent when running it multiple times results in the same final state. Ansible achieves this using state-based modules like `apt`, `service`, and `docker_container`. + +### How do handlers improve efficiency? +Handlers execute only when notified. This prevents unnecessary service restarts and reduces downtime. + +### Why is Ansible Vault necessary? +It securely stores sensitive credentials such as Docker Hub tokens. Without Vault, secrets could be exposed in version control. diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..78871dfc9d --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,335 @@ +# Lab 6: Advanced Ansible & CI/CD - Submission + +**Name:** Amir Bairamov +**Date:** 2026-03-05 +**Lab Points:** 10 + +--- + +# Overview + +In this lab I implemented an advanced **Ansible automation workflow** and integrated it with **CI/CD using GitHub Actions**. + +The main goal was to improve the existing infrastructure by introducing: + +- Ansible **Blocks** +- **Tag-based execution** +- Migration from **single container deployment to Docker Compose** +- **Safe wipe logic** for removing deployed applications +- **CI/CD pipeline** for automatic deployment +- **Automated linting and verification** + +### Technologies Used + +- Ansible +- Docker +- Docker Compose +- GitHub Actions +- Ansible Vault +- SSH +- Linux (Ubuntu 22.04) + +The final result is a fully automated deployment pipeline where: + +``` +Code Push → GitHub Actions → ansible-lint → ansible-playbook → Docker Compose → Running Application +``` + + +--- + +# Task 1: Blocks & Tags (2 pts) + +## Blocks Implementation + +Blocks were used to group logically related tasks and provide better error handling. + +Example from **web_app role**: + +```yaml +- name: Deploy application with Docker Compose + block: + + - name: Create app directory + file: + path: "/opt/{{ app_name }}" + state: directory + mode: '0755' + + - name: Template docker-compose + template: + src: docker-compose.yml.j2 + dest: "/opt/{{ app_name }}/docker-compose.yml" + + - name: Deploy container + community.docker.docker_compose_v2: + project_src: "/opt/{{ app_name }}" + state: present + pull: always + + rescue: + + - name: Deployment failed + debug: + msg: "Deployment failed" + + tags: + - app_deploy + - compose +``` + +Benefits of using blocks: +- Logical grouping of tasks +- Easier error handling +- Cleaner role structure +- Better debugging + +### Tag Strategy + +Tags allow executing only specific parts of the playbook. + +Implemented tags: + +| Tag | Purpose | +| ---------- | ---------------------------- | +| docker | Install and configure Docker | +| app_deploy | Deploy application | +| compose | Docker Compose related tasks | +| wipe | Remove application | + +Example: List all tags + +``` +ansible-playbook playbooks/deploy.yml --list-tags +``` + +![](screenshots_l6/t1_p4.png) + +Example: Run different tags + +![](screenshots_l6/t1_p1.png) + +![](screenshots_l6/t1_p2.png) + +![](screenshots_l6/t1_p3.png) + +## Task 2: Docker Compose Migration (3 pts) + +Originally the application was deployed using direct docker_container module. + +### Before (Single Container Deployment) + +Example: + +``` +community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + ports: + - "{{ app_port }}:{{ app_port_2 }}" +``` + +Limitations: +- Hard to scale +- Difficult multi-container support +- Harder configuration management + +### After (Docker Compose Deployment) + +The deployment was migrated to Docker Compose. + +Template File + +``` +roles/web_app/templates/docker-compose.yml.j2 +``` + +Example template: + +``` +version: "3.8" + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }} + + ports: + - "{{ app_port }}:{{ app_internal_port }}" + +{% if app_environment_vars %} + environment: +{% for key, value in app_environment_vars.items() %} + {{ key }}: "{{ value }}" +{% endfor %} +{% endif %} + + restart: unless-stopped +``` + +Advantages +- Easier multi-container architecture +- Better environment management +- Standard Docker deployment method +- Easier scaling + +### Deployment Evidence and Verification + +![](screenshots_l6/t2_p1.png) + +![](screenshots_l6/t2_p2.png) + +![](screenshots_l6/t2_p3.png) + +![](screenshots_l6/t2_p4.png) + +### Task 3: Wipe Logic (1 pt) + +A wipe mechanism was implemented to safely remove the deployed application. + +Purpose: +- Remove containers +- Remove application directory +- Clean deployment state + +### Implementation + +``` +--- + +- name: Wipe web application + block: + + - name: Stop containers + community.docker.docker_compose_v2: + project_src: "/opt/{{ app_name }}" + state: absent + ignore_errors: yes + + - name: Remove application directory + file: + path: "/opt/{{ app_name }}" + state: absent + + - name: Log wipe completion + debug: + msg: "Application {{ app_name }} wiped" + + when: web_app_wipe | bool + + tags: + - web_app_wipe +``` + +### Test Scenarios + +Scenario 1: + +![](screenshots_l6/t3_s1_p1.png) +![](screenshots_l6/t3_s1_p2.png) + +Scenario 2: + +![](screenshots_l6/t3_s2.png) + +Scenario 3: + +![](screenshots_l6/t3_s3.png) + +Scenario 4: + +![](screenshots_l6/t3_s4.png) + +## Task 4: CI/CD Integration (3 pts) + +CI/CD pipeline was implemented using GitHub Actions. + +Workflow file: + +``` +.github/workflows/ansible-deploy.yml +``` + +Workflow Steps +- Checkout repository +- Setup Python +- Install Ansible +- Run ansible-lint +- Setup SSH connection +- Run ansible-playbook +- Verify deployment with curl + +### GitHub Secrets Configuration + +Secrets used: + +| Secret | Purpose | +| ---------------------- | ------------- | +| ANSIBLE_VAULT_PASSWORD | decrypt vault | +| SSH_PRIVATE_KEY | connect to VM | +| VM_HOST | server IP | +| VM_USER | SSH username | + +### Successful Workflow Run + +![](screenshots_l6/t4_p1.png) + +![](screenshots_l6/t4_p2.png) + +![](screenshots_l6/t4_p3.png) + +## Challenges & Solutions + +### Challenge 1 — Docker Compose environment mapping error +Error: +``` +services.devops-app.environment must be a mapping +``` + +Solution: Corrected the Jinja template loop to generate proper YAML mapping. + + +### Challenge 2 — Port mismatch + +The application internally listens on port 6000, while external access was configured for 8000. + +Solution: Corrected the Docker Compose port mapping. + +## Research Answers +1. Security implications of storing SSH keys in GitHub Secrets + +GitHub Secrets are encrypted and hidden from logs, but risks remain: +- Compromised workflows could leak credentials +- Repository write access could allow malicious workflow changes +- Secrets are exposed to runner environment + +Best practices: +- Use deploy keys +- Limit repository permissions +- Rotate keys regularly +- Prefer short-lived tokens where possible + +3. Implementing Rollbacks + +Rollbacks can be implemented using Docker image versioning. + +Example: + +``` +app:v1 +app:v2 +app:v3 +``` + +If deployment fails, the playbook can redeploy the previous version. +Ansible can store the previous version tag and redeploy it. + +4. Self-hosted runner security benefits + +Self-hosted runners improve security because: +- Deployment happens inside the organization infrastructure +- Secrets never leave the internal network +- Firewall restrictions can be applied +- Infrastructure access can be tightly controlled + +However they require more maintenance. \ No newline at end of file diff --git a/ansible/docs/screenshots_l5/p0-1.png b/ansible/docs/screenshots_l5/p0-1.png new file mode 100644 index 0000000000..b545d64ea3 Binary files /dev/null and b/ansible/docs/screenshots_l5/p0-1.png differ diff --git a/ansible/docs/screenshots_l5/p0.png b/ansible/docs/screenshots_l5/p0.png new file mode 100644 index 0000000000..7a91d1351b Binary files /dev/null and b/ansible/docs/screenshots_l5/p0.png differ diff --git a/ansible/docs/screenshots_l5/p1.png b/ansible/docs/screenshots_l5/p1.png new file mode 100644 index 0000000000..2a18f12693 Binary files /dev/null and b/ansible/docs/screenshots_l5/p1.png differ diff --git a/ansible/docs/screenshots_l5/p2.png b/ansible/docs/screenshots_l5/p2.png new file mode 100644 index 0000000000..5bb4f782cd Binary files /dev/null and b/ansible/docs/screenshots_l5/p2.png differ diff --git a/ansible/docs/screenshots_l5/p3.png b/ansible/docs/screenshots_l5/p3.png new file mode 100644 index 0000000000..5263d62054 Binary files /dev/null and b/ansible/docs/screenshots_l5/p3.png differ diff --git a/ansible/docs/screenshots_l5/p4.png b/ansible/docs/screenshots_l5/p4.png new file mode 100644 index 0000000000..151ed295fe Binary files /dev/null and b/ansible/docs/screenshots_l5/p4.png differ diff --git a/ansible/docs/screenshots_l5/p5.png b/ansible/docs/screenshots_l5/p5.png new file mode 100644 index 0000000000..a1fe76b3d5 Binary files /dev/null and b/ansible/docs/screenshots_l5/p5.png differ diff --git a/ansible/docs/screenshots_l6/t1_p1.png b/ansible/docs/screenshots_l6/t1_p1.png new file mode 100644 index 0000000000..64445f2c86 Binary files /dev/null and b/ansible/docs/screenshots_l6/t1_p1.png differ diff --git a/ansible/docs/screenshots_l6/t1_p2.png b/ansible/docs/screenshots_l6/t1_p2.png new file mode 100644 index 0000000000..9b1df38f9e Binary files /dev/null and b/ansible/docs/screenshots_l6/t1_p2.png differ diff --git a/ansible/docs/screenshots_l6/t1_p3.png b/ansible/docs/screenshots_l6/t1_p3.png new file mode 100644 index 0000000000..de30384355 Binary files /dev/null and b/ansible/docs/screenshots_l6/t1_p3.png differ diff --git a/ansible/docs/screenshots_l6/t1_p4.png b/ansible/docs/screenshots_l6/t1_p4.png new file mode 100644 index 0000000000..c164e2519e Binary files /dev/null and b/ansible/docs/screenshots_l6/t1_p4.png differ diff --git a/ansible/docs/screenshots_l6/t2_p1.png b/ansible/docs/screenshots_l6/t2_p1.png new file mode 100644 index 0000000000..ff8dcb1480 Binary files /dev/null and b/ansible/docs/screenshots_l6/t2_p1.png differ diff --git a/ansible/docs/screenshots_l6/t2_p2.png b/ansible/docs/screenshots_l6/t2_p2.png new file mode 100644 index 0000000000..fc06b6058f Binary files /dev/null and b/ansible/docs/screenshots_l6/t2_p2.png differ diff --git a/ansible/docs/screenshots_l6/t2_p3.png b/ansible/docs/screenshots_l6/t2_p3.png new file mode 100644 index 0000000000..8abbadd9ab Binary files /dev/null and b/ansible/docs/screenshots_l6/t2_p3.png differ diff --git a/ansible/docs/screenshots_l6/t2_p4.png b/ansible/docs/screenshots_l6/t2_p4.png new file mode 100644 index 0000000000..4d2c401a1f Binary files /dev/null and b/ansible/docs/screenshots_l6/t2_p4.png differ diff --git a/ansible/docs/screenshots_l6/t3_s1_p1.png b/ansible/docs/screenshots_l6/t3_s1_p1.png new file mode 100644 index 0000000000..97d8de3c38 Binary files /dev/null and b/ansible/docs/screenshots_l6/t3_s1_p1.png differ diff --git a/ansible/docs/screenshots_l6/t3_s1_p2.png b/ansible/docs/screenshots_l6/t3_s1_p2.png new file mode 100644 index 0000000000..e7fdb531fd Binary files /dev/null and b/ansible/docs/screenshots_l6/t3_s1_p2.png differ diff --git a/ansible/docs/screenshots_l6/t3_s2.png b/ansible/docs/screenshots_l6/t3_s2.png new file mode 100644 index 0000000000..348dcb7bf2 Binary files /dev/null and b/ansible/docs/screenshots_l6/t3_s2.png differ diff --git a/ansible/docs/screenshots_l6/t3_s3.png b/ansible/docs/screenshots_l6/t3_s3.png new file mode 100644 index 0000000000..74ed6e642d Binary files /dev/null and b/ansible/docs/screenshots_l6/t3_s3.png differ diff --git a/ansible/docs/screenshots_l6/t3_s4.png b/ansible/docs/screenshots_l6/t3_s4.png new file mode 100644 index 0000000000..bd6530e9b3 Binary files /dev/null and b/ansible/docs/screenshots_l6/t3_s4.png differ diff --git a/ansible/docs/screenshots_l6/t4_p1.png b/ansible/docs/screenshots_l6/t4_p1.png new file mode 100644 index 0000000000..d08cfe3128 Binary files /dev/null and b/ansible/docs/screenshots_l6/t4_p1.png differ diff --git a/ansible/docs/screenshots_l6/t4_p2.png b/ansible/docs/screenshots_l6/t4_p2.png new file mode 100644 index 0000000000..ecac22604e Binary files /dev/null and b/ansible/docs/screenshots_l6/t4_p2.png differ diff --git a/ansible/docs/screenshots_l6/t4_p3.png b/ansible/docs/screenshots_l6/t4_p3.png new file mode 100644 index 0000000000..00f1c7967a Binary files /dev/null and b/ansible/docs/screenshots_l6/t4_p3.png differ diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml new file mode 100644 index 0000000000..4b52268e70 --- /dev/null +++ b/ansible/inventory/group_vars/all.yml @@ -0,0 +1,20 @@ +$ANSIBLE_VAULT;1.1;AES256 +36656634633731663739616439623938376566323062306462303564313536393838393535386337 +3432376364666233396431356665306563633033323463630a643830366132653237626635323033 +32623230323966346263326663373962313938353062313331313033643634333362646332356338 +3637656436653339320a343134363538343433313861303430303637343038313366616665363235 +32383264663835333134663038656337393231373636626466376238653965363738336134636432 +31633031616638663464396235343633336135613762326633333631633361333039656634636530 +34613965383163316632333134326439653765323262356530386533356330366635336436656661 +64643066373132316234316234656432646336653530363833396239396535643930643836626566 +65363338343331306131333539663836653061386261366437363638373737373737326139313533 +34663961303134636335336333373930636635646264303630363166623539633932623363656233 +65386136623865633963653332303364653633323862366234646134346339373536343062613662 +64373934393139656435306531376432346331623731613762393437363733306562633939383664 +32323833313731663362616438663437326430313536653033383864393932343636396135616239 +37356163323864356565363863333838656462313261646363623734343464633062393632393962 +36663034616566653965393735333162346535623431623134663031313164303135383638616231 +30333435306364393235303938383534306133353761353530613563633335346163303336333365 +65373466383663613061616434613532303963353638353162313532396362323965396339316365 +36333865313139613632626131663366383838373866613664393530313634333038373036343731 +633365626233616637313661323065373862 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..d54ced864b --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +lab4-vm ansible_host=93.77.185.211 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..d1f9f3a668 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,4 @@ +- name: Deploy application + hosts: webservers + roles: + - web_app 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..0f8ba1d48a --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,45 @@ +--- + +- name: Package management + 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: Fix apt cache if update failed + command: apt-get update --fix-missing + + always: + + - name: Log packages block completion + copy: + content: "Packages block finished" + dest: /tmp/common_packages.log + + become: true + + tags: + - packages + - common + +- name: User configuration + block: + + - name: Set timezone + community.general.timezone: + name: Europe/Moscow + + become: true + + tags: + - users + - common \ 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..097f306ce3 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,76 @@ +- name: Install Docker + block: + + - name: Install required system packages + apt: + name: + - ca-certificates + - gnupg + - lsb-release + 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 + + - name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + + rescue: + + - name: Wait before retry + pause: + seconds: 10 + + - name: Retry apt update + command: apt-get update + + tags: + - docker + - docker_install + + become: true + +- name: Configure Docker + block: + + - name: Ensure Docker 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 + + always: + + - name: Ensure docker service enabled + service: + name: docker + state: started + enabled: true + + become: true + + 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..b3407c263e --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,7 @@ +restart_policy: unless-stopped + +app_port_2: 6000 # My port in the container is 6000 instead of 5000 because I use macOS and 5000 is already in use by system services + +app_environment_vars: {} + +web_app_wipe: false diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..7597802696 --- /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_internal_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..fc95875336 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: 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..d31d37a218 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,36 @@ +--- + +- name: Include wipe tasks + include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Deploy application with Docker Compose + block: + + - name: Create app directory + file: + path: "/opt/{{ app_name }}" + state: directory + mode: '0755' + + - name: Template docker-compose + template: + src: docker-compose.yml.j2 + dest: "/opt/{{ app_name }}/docker-compose.yml" + + - name: Deploy container + community.docker.docker_compose_v2: + project_src: "/opt/{{ app_name }}" + state: present + pull: always + + rescue: + + - name: Deployment failed + debug: + msg: "Deployment failed" + + tags: + - 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..f9af05b492 --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,24 @@ +--- + +- name: Wipe web application + block: + + - name: Stop containers + community.docker.docker_compose_v2: + project_src: "/opt/{{ app_name }}" + state: absent + ignore_errors: yes + + - name: Remove application directory + file: + path: "/opt/{{ app_name }}" + state: absent + + - name: Log wipe completion + debug: + msg: "Application {{ app_name }} wiped" + + when: web_app_wipe | bool + + 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..925f24a402 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,18 @@ +version: "3.8" + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }} + + ports: + - "{{ app_port }}:{{ app_internal_port }}" + +{% if app_environment_vars %} + environment: +{% for key, value in app_environment_vars.items() %} + {{ key }}: "{{ value }}" +{% endfor %} +{% endif %} + + restart: unless-stopped \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..f923110b94 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.pyc +*.pyo + +# Virtual environments +venv/ +.venv/ + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# OS files +.DS_Store + +# Docs (не нужны для runtime) +docs/ +README.md +tests/ diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..7f02fa00e8 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,15 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Test cache +.pytest_cache \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..26063cd59c --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,37 @@ +# Используем конкретную версию Python (slim — меньше размер, чем full) +FROM python:3.13-slim + +# Переменные окружения для Python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Устанавливаем curl +RUN apt-get update && apt-get install -y curl \ + && rm -rf /var/lib/apt/lists/* + +# Создаём non-root пользователя +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Рабочая директория +WORKDIR /app + +# Копируем ТОЛЬКО зависимости сначала (важно для layer caching) +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY app.py . + +# Меняем владельца файлов +RUN chown -R appuser:appuser /app + +# Переключаемся на non-root пользователя +USER appuser + +# Документируем порт +EXPOSE 6000 + +# Команда запуска +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..2b4da14bff --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,187 @@ +# DevOps Info Service + +![CI](https://github.com/MrDeBuFF/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +A lightweight web service built with Flask that provides detailed system information and health status monitoring. + +## 📋 Overview + +DevOps Info Service is a Python-based web application that exposes two main endpoints: +- **GET /** - Comprehensive service and system information +- **GET /health** - Health check endpoint for monitoring and probes + +The service is designed to be configurable, production-ready, and follows Python best practices. + +## 🚀 Quick Start + +### 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 +``` + +### Running the Application + +- **Default Configuration:** + + +```bash +python app.py +``` +The service will start at: http://0.0.0.0:6000 + +- **Custom Configuration:** + + +```bash +# Change port +PORT=8080 python app.py + +# Change host and port +HOST=127.0.0.1 PORT=3000 python app.py + +# Enable debug mode +DEBUG=true python app.py +``` + +## 🌐 API Endpoints + +### GET / + +Returns comprehensive service and system information. + +**Request:** + + +```bash +curl http://localhost:6000/ +``` + +### GET /health + +Health check endpoint for monitoring systems and Kubernetes probes. + +**Request:** + +```bash +curl http://localhost:6000/health +``` + +**Status Codes:** + +- 200 OK: Service is healthy +- 5xx: Service is unhealthy (implemented in future labs) + +### ⚙️ Configuration + +The application is configured through environment variables: + +|Variable | Default | Description | +|----------|-------|---------| +|HOST | 0.0.0.0 | Host interface to bind the server| +|PORT | 6000 | Port number to listen on| +|DEBUG | false | Debug mode (true/false)| + + +## Docker + +### Build image locally + +```bash +docker build -t devops-info-service:1.0 . +``` + +### Run container + +```bash +docker run -p 6000:6000 devops-info-service:1.0 +``` + +### Push image to Docker Hub + +```bash +# Login +docker login + +# Tag image +docker tag devops-info-service:1.0 mrdebuff/devops-info-service:1.0 + +# Push +docker push mrdebuff/devops-info-service:1.0 +``` + +### Pull image from Docker Hub + +```bash +# Pull image +docker pull mrdebuff/devops-info-service:1.0 + +# Run container +docker run -p 6000:6000 mrdebuff/devops-info-service:1.0 +``` + +## Testing + +This project uses `pytest` for unit testing. + +### Install dependencies + +```bash +pip install -r requirements.txt +pip install -r requirements-dev.txt +``` + +### Run tests + +```bash +pytest -v +``` + +Example output: + +![](docs/screenshots/07-pytest.png) + +### Code Quality + +Linting is performed using `flake8`: + +```bash +flake8 app.py +``` + +### Security Scanning + +Dependency vulnerabilities are checked using `Snyk` during CI pipeline execution. + +Only high and critical severity vulnerabilities fail the build. \ No newline at end of file diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..21bc136ba1 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,243 @@ +""" +DevOps Info Service +Main application module +""" + +import os +import socket +import platform +import logging +import time +from datetime import datetime, timezone + +from flask import Flask, jsonify, request, g + +from prometheus_client import ( + Counter, + Histogram, + Gauge, + generate_latest, + CONTENT_TYPE_LATEST +) + +app = Flask(__name__) + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 6000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application start time +START_TIME = datetime.now(timezone.utc) + +# Logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# Prometheus Metrics +http_requests_total = Counter( + "http_requests_total", + "Total HTTP requests", + ["method", "endpoint", "status"] +) + +http_request_duration_seconds = Histogram( + "http_request_duration_seconds", + "HTTP request duration seconds", + ["method", "endpoint"] +) + +http_requests_in_progress = Gauge( + "http_requests_in_progress", + "HTTP requests currently being processed" +) + +endpoint_calls = Counter( + "devops_info_endpoint_calls", + "Endpoint calls", + ["endpoint"] +) + +system_info_duration = Histogram( + "devops_info_system_collection_seconds", + "System info collection time" +) + + +# Request instrumentation +@app.before_request +def before_request(): + g.start_time = time.time() + http_requests_in_progress.inc() + + +@app.after_request +def after_request(response): + duration = time.time() - g.start_time + + endpoint = request.path + + http_requests_total.labels( + method=request.method, + endpoint=endpoint, + status=response.status_code + ).inc() + + http_request_duration_seconds.labels( + method=request.method, + endpoint=endpoint + ).observe(duration) + + http_requests_in_progress.dec() + + return response + + +# Helper functions +def get_system_info(): + """Collect system information.""" + + start = time.time() + + info = { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.platform(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + } + + system_info_duration.observe(time.time() - start) + + return info + + +def get_uptime(): + """Calculate application uptime.""" + uptime = (datetime.now(timezone.utc) - START_TIME).total_seconds() + hours = int(uptime // 3600) + minutes = int((uptime % 3600) // 60) + + return { + "seconds": int(uptime), + "human": f"{hours} hour, {minutes} minutes" + } + + +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, + } + + +# Routes +@app.route("/") +def index(): + endpoint_calls.labels(endpoint="/").inc() + + uptime = get_uptime() + + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": datetime.now( + timezone.utc).isoformat().replace("+00:00", "") + + "Z", + "timezone": "UTC", + }, + "request": get_request_info(), + "endpoints": [ + {"path": "/", "method": "GET", + "description": "Service information"}, + {"path": "/health", "method": "GET", + "description": "Health check"}, + {"path": "/metrics", "method": "GET", + "description": "Prometheus metrics"}, + ], + } + + logger.info(f"Request: {request.method} {request.path}") + + return jsonify(response), 200 + + +@app.route("/health") +def health(): + endpoint_calls.labels(endpoint="/health").inc() + + uptime = get_uptime() + + response = { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", + "") + "Z", + "uptime_seconds": uptime["seconds"], + } + + logger.debug(f'Health check: {response["status"]}') + + return jsonify(response), 200 + + +@app.route("/metrics") +def metrics(): + return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST} + + +# Errors + +@app.errorhandler(404) +def not_found(error): + + logger.warning(f"Not found: {request.method} {request.path}") + + return ( + jsonify( + { + "error": "Not Found", + "message": "The requested endpoint does not exist", + "available_endpoints": ["/", "/health", "/metrics"], + } + ), + 404, + ) + + +@app.errorhandler(500) +def internal_error(error): + + logger.error(f"Internal server error: {str(error)}") + + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + ), + 500, + ) + + +if __name__ == "__main__": + logger.info("Starting DevOps Info Service v1.0.0") + logger.info(f"Server running at http://{HOST}:{PORT}") + + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..74695720ff --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,286 @@ +# Lab 1: DevOps Info Service - Report + +## Framework Selection + +Choice – Flask + +### Framework Comparison: + +| Criterion | Flask | FastAPI | Django | +|-----------|-------|---------|--------| +| **Learning Curve** | Low | Medium | High | +| **Performance** | High | Very High | Medium | +| **Built-in Features** | Minimal | Modern API features | Full-stack | +| **Auto-documentation** | Manual | OpenAPI/Swagger | Manual | +| **Async Support** | Limited | Native | Limited | +| **Project Size** | ~150KB | ~1.2MB | ~8MB+ | +| **Use Case** | Microservices, APIs | Modern APIs, Microservices | Monoliths, CMS | + +**Decision Justification:** For Lab 1's requirements (simple info service with 2 endpoints), Flask provides the perfect balance of simplicity, control, and maintainability. It allows us to focus on the DevOps aspects rather than framework intricacies. + +## Implemented Best Practices + +### 1. Clean Code Organization + +**Code Example:** +```python +def get_system_info(): + """Collect system information.""" + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + # ... other fields + } + +@app.route("/") +def index(): + """Main endpoint - service and system information.""" + # Clear request handling logic +``` + +Benefits: +- Clear separation of concerns with dedicated functions for better understanding +- Logical grouping of imports +- Comprehensive docstrings for all functions and endpoints +- Consistent naming conventions (PEP 8 compliant) + + + +### 2. Comprehensive Error Handling + +Implemented error handlers for common HTTP status codes: + +```python +@app.errorhandler(404) +def not_found(error): + """Handle 404 errors.""" + logger.warning(f"Not found: {request.method} {request.path}") + return ( + jsonify({ + "error": "Not Found", + "message": "The requested endpoint does not exist", + "available_endpoints": ["/", "/health"], + }), + 404, + ) + +@app.errorhandler(500) +def internal_error(error): + """Handle 500 errors.""" + logger.error(f"Internal server error: {str(error)}") + return ( + jsonify({ + "error": "Internal Server Error", + "message": "An unexpected error occurred", + }), + 500, + ) +``` + +Benefits: + +- Consistent error responses +- Helpful error messages for API consumers +- Proper logging of all errors + +### 3. Structured Logging + +Configured logging with appropriate levels and format: + +```python +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +``` + +Benefits: + +- Timestamps help debug timing issues +- Different levels for filtering +- Production monitoring systems read these logs + + +### 4. Environment-Based Configuration + +All configuration externalized to environment variables: + +```python +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" +``` + +Benefits: + +- No hardcoded values in source code +- Easy configuration for different environments +- Secure handling of sensitive data (for future features) +- Twelve-factor app compliance + +### 5. Type Consistency and ISO 8601 Formatting + +```python +# Consistent time formatting in UTC +datetime.now(timezone.utc).isoformat().replace("+00:00", "") + "Z" + +# Human-readable uptime formatting +f"{hours} hour, {minutes} minutes" +``` + +Benefits: + +- Consistent time formatting +- Human-readable uptime formatting + +## API Documentation + +### Endpoint 1: GET / + +Purpose: Retrieve comprehensive service and system information. + +Response Structure: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "ubuntu-server", + "platform": "Linux", + "platform_version": "Linux-6.8.0-31-generic-x86_64-with-glibc2.39", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.12.3" + }, + "runtime": { + "uptime_seconds": 120, + "uptime_human": "0 hour, 2 minutes", + "current_time": "2024-10-15T10:30:45.123456Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### Endpoint 2: GET /health + +Purpose: Health check for monitoring and Kubernetes probes. + +Response Structure: + +```json +{ + "status": "healthy", + "timestamp": "2024-10-15T10:30:45.123456Z", + "uptime_seconds": 120 +} +``` + +### Testing Commands: + +```bash +# Get service information +curl http://localhost:6000/ + +# Health check +curl http://localhost:6000/health + +# Formatted JSON output +curl http://localhost:6000/ | python3 -m json.tool +``` + +## Testing Evidence + + +See the screenshots directory `app_python/docs/screenshots/` for visual proof: + +- 01-main-endpoint.png - Complete JSON response from GET / +- 02-health-check.png - Health check endpoint response +- 03-formatted-output.png - Pretty-printed JSON output +Terminal Output Examples: + +```bash +$ curl http://localhost:6000/ | python3 -m json.tool + +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.7.1" + }, + "runtime": { + "current_time": "2026-01-27T16:58:50.775183Z", + "timezone": "UTC", + "uptime_human": "0 hour, 0 minutes", + "uptime_seconds": 4 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "arm64", + "cpu_count": 8, + "hostname": "MacBook-Air-Mr-DeBuFF.local", + "platform": "Darwin", + "platform_version": "macOS-26.2-arm64-arm-64bit-Mach-O", + "python_version": "3.13.2" + } +} +``` + +## Challenges and Solutions + +### Challenge 1: Accurate Uptime Calculation + +Problem: Needed to calculate application uptime in both seconds and human-readable format. + +Solution: Created a dedicated function that calculates the difference between current time and application start time: + +```python +def get_uptime(): + """Calculate application uptime.""" + uptime = (datetime.now(timezone.utc) - START_TIME).total_seconds() + hours = int(uptime // 3600) + minutes = int((uptime % 3600) // 60) + return {"seconds": int(uptime), "human": f"{hours} hour, {minutes} minutes"} +``` + +## GitHub Community + +1. **The Value of Starring Repositories in Open Source:** + +They act as quality signals that help developers discover reliable and well-maintained projects. When you star a repository, you're not just bookmarking it for personal reference—you're contributing to its visibility and credibility. + +2. **The Importance of Following Developers in Team Projects:** + +By following professors and TAs, you gain insights into professional development practices and stay updated on industry trends. Following classmates fosters collaboration and peer learning, allowing you to see different approaches to problem-solving and stay connected on course projects. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..d16afd0573 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,106 @@ +# Lab 2: Docker Containerization - Report + +## 1. Docker Best Practices Applied + +### Non-root user + +The container is started by the unprivileged user `appuser`. This reduces the security risks in case the container is compromised. + +```dockerfile +RUN groupadd -r appuser && useradd -r -g appuser appuser + +USER appuser +``` + +### Layer caching + +The file `requirements.txt` it is copied and installed before the application code. This allows Docker to use the cache if the dependencies have not changed. + +```dockerfile +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt +``` + +### .dockerignore + +File `.dockerignore` eliminates the virtual environment, `git` files, and `IDE` configurations, reducing the size of the build context and speeding up image assembly. + +## 2. Image Information & Decisions + +**Base image**: `python:3.13-slim` — official image, minimum size, current Python version + +**Final image size**: `42,6 MB` — I think it's really cool result + +**Layer structure explanation**: Dependencies are installed before code is copied, which optimizes for build caching. + +**Optimization choices**: +- slim image +- no-cache pip install +- excluding unnecessary files via `.dockerignore` + +## 3. Build & Run Process + +### Build process + +```bash +docker build -t devops-info-service:1.0 . +``` + +![](./screenshots/04-docker-build.png) + +### Container running + +```bash +docker run -p 6000:6000 devops-info-service:1.0 +``` + +![](./screenshots/05-container-running.png) + +### Testing endpoints + +```bash +curl http://localhost:6000/ + +curl http://localhost:6000/health +``` + +![](./screenshots/06-testing-endpoints.png) + +### Docker Hub repository + +**URL** — https://hub.docker.com/r/mrdebuff/devops-info-service + +## 4. Technical Analysis + +- *Why does your Dockerfile work the way it does?* + +The Dockerfile is designed for efficiency and security. +Dependencies are installed before copying application code to take advantage of Docker layer caching. A specific `slim` Python image ensures a consistent and lightweight runtime. The application runs as a `non-root` user, following container security best practices. + +- *What would happen if you changed the layer order?* + +If application code were copied before installing dependencies, Docker cache would be invalidated on every code change, causing slower rebuilds. Changing the order of user creation could also lead to permission issues during build or runtime. + +- *What security considerations did you implement?* + +The container runs as a `non-root` user to reduce security risks. A minimal base image is used to decrease the attack surface, and only necessary files are included in the final image. Dependency cache is disabled to avoid unnecessary artifacts. + +- *How does .dockerignore improve your build?* + +The `.dockerignore` file reduces the build context by excluding unnecessary files like virtual environments and `git` metadata. This speeds up builds, reduces image size, and prevents accidental inclusion of development artifacts. + +## 5. Challenges & Solutions + +**Issues**: Error with the port — Flask was listening to localhost + +**Solution**: Using HOST=0.0.0.0 allowed accepting connections from the container. + +### What I Learned +- Dockerfile basics + +- РHow to work with Docker Hub + +- The importance of layer order + +- Container security \ No newline at end of file diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..386c206f6d --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,163 @@ +# Lab 3: Continuous Integration (CI/CD) + +## 1. Overview + +### Testing Framework + +I chose **pytest** as the testing framework because: + +- It provides clean and minimal syntax +- Supports fixtures and modular test design +- Is widely adopted in modern Python projects +- Integrates easily with CI pipelines + +### Test Coverage + +The following endpoints are covered: + +- `GET /` + - Verifies status code (200) + - Checks required JSON fields + - Validates response structure + +- `GET /health` + - Verifies status code (200) + - Validates health status and uptime fields + +- `404` error handling + - Verifies correct error response + - Checks available endpoints list + +Tests focus on API contract validation rather than environment-specific values. + +### CI Workflow Trigger Configuration + +The workflow runs on: + +- `push` to `master` +- `push` to `lab03` +- `pull_request` targeting `master` + +Docker image build and push occur only when: + +- Event is `push` + +This ensures: +- All changes are tested +- Only stable code is published + +### Versioning Strategy + +I selected **Calendar Versioning (CalVer)**: `YYYY.MM` + +Example: `2026.02` + +Reasoning: +- This project is a service, not a library +- Releases are continuous +- No need for strict semantic versioning +- Date-based tagging reflects deployment timeline + +Docker tags created: + +- `mrdebuff/devops-info-service:YYYY.MM` +- `mrdebuff/devops-info-service:latest`\ + +## 2. Workflow Evidence + +### ✅ Successful Workflow Run + +GitHub Actions: +https://github.com/MrDeBuFF/DevOps-Core-Course/actions/runs/21914946597 + +### ✅ Tests Passing Locally + +![](screenshots/07-pytest.png) + +### ✅ Docker Image Published + +Docker Hub: +https://hub.docker.com/r/mrdebuff/devops-info-service/tags + +### ✅ Status Badge + +Status badge is visible in README.md and reflects current workflow state. + +![CI](https://github.com/MrDeBuFF/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +## 3. Best Practices Implemented + +### Fail Fast +Docker build depends on successful completion of test job (`needs: test`). + +Prevents publishing broken images. + +### Dependency Caching +Enabled pip caching via: + +``` +actions/setup-python cache: pip +``` + +Result: +- First run: 1.5 minutes +- Cached run: ~50 seconds + +Significant performance improvement. + +### Conditional Docker Push +Docker image is built and pushed only on: + +``` +push to master +``` + +Prevents publishing development builds. + +### Security Scanning (Snyk) +Integrated Snyk CLI into CI pipeline. + +- Scans dependencies from `requirements.txt` +- Fails build on high or critical vulnerabilities + +No high severity vulnerabilities found in Flask 3.1.0. + +### Secrets Management + +Secrets are stored in GitHub secrets and are not committed to the repository. + +## 4. Key Decisions + +### Versioning Strategy +CalVer was chosen because this is a continuously deployed service. Date-based tagging simplifies release tracking and avoids manual version bumping. + +### Docker Tags +CI creates: +- `YYYY.MM` +- `latest` + +Ensures both fixed and rolling version references. + +### Workflow Triggers +- All branches are tested. +- Only pushes publishes Docker images. +- Pull requests are validated before merge. + +### Test Coverage +Tests cover: +- All public endpoints +- Status codes +- JSON structure +- Error handling (404) + +Not covered: +- Internal helper functions +- Platform-specific values (hostname, CPU count) + +Focus is on API contract validation. + +## 5. Challenges + +- Snyk Docker-based action failed due to isolated container environment. +- Resolved by switching to Snyk CLI installation inside workflow. +- Required specifying correct working directory for dependency scanning. 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..37bd9a977f 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..07904486bd 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..ea1c4ad018 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/04-docker-build.png b/app_python/docs/screenshots/04-docker-build.png new file mode 100644 index 0000000000..9225e164e2 Binary files /dev/null and b/app_python/docs/screenshots/04-docker-build.png differ diff --git a/app_python/docs/screenshots/05-container-running.png b/app_python/docs/screenshots/05-container-running.png new file mode 100644 index 0000000000..4dae35fea2 Binary files /dev/null and b/app_python/docs/screenshots/05-container-running.png differ diff --git a/app_python/docs/screenshots/06-testing-endpoints.png b/app_python/docs/screenshots/06-testing-endpoints.png new file mode 100644 index 0000000000..e0061d43a2 Binary files /dev/null and b/app_python/docs/screenshots/06-testing-endpoints.png differ diff --git a/app_python/docs/screenshots/07-pytest.png b/app_python/docs/screenshots/07-pytest.png new file mode 100644 index 0000000000..8d6ad90438 Binary files /dev/null and b/app_python/docs/screenshots/07-pytest.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..1504689633 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest==8.0.0 +flake8==7.0.0 diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..f6309a6723 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.1.0 +prometheus-client==0.23.1 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..450b468a2b --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,57 @@ +import pytest +from app import app + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + +def test_index_success(client): + response = client.get("/") + + assert response.status_code == 200 + + data = response.get_json() + + # Проверяем верхний уровень + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + # Проверяем service + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["framework"] == "Flask" + + # Проверяем system (НЕ значения, а наличие) + assert "hostname" in data["system"] + assert "cpu_count" in data["system"] + + # Проверяем runtime + assert isinstance(data["runtime"]["uptime_seconds"], int) + + +def test_health_success(client): + response = client.get("/health") + + assert response.status_code == 200 + + data = response.get_json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert isinstance(data["uptime_seconds"], int) + + +def test_not_found(client): + response = client.get("/does-not-exist") + + assert response.status_code == 404 + + data = response.get_json() + + assert data["error"] == "Not Found" + assert "/health" in data["available_endpoints"] diff --git a/docs/LAB04.md b/docs/LAB04.md new file mode 100644 index 0000000000..7ddd6327b8 --- /dev/null +++ b/docs/LAB04.md @@ -0,0 +1,220 @@ +# LAB 04 — Infrastructure as Code (Terraform & Pulumi) + +# 1. Cloud Provider & Infrastructure + +## Cloud Provider Chosen + +Yandex Cloud was selected as the cloud provider. + +### Rationale + +* Free tier suitable for educational purposes +* Simple IAM and service account integration +* Lightweight infrastructure sufficient for lab requirements +* Good compatibility with both Terraform and Pulumi + + +## Instance Type / Size + +* 2 vCPU +* 1 GB RAM +* Core fraction: 20% +* Ubuntu 22.04 LTS image + +### Why This Size? + +This configuration is sufficient to: + +* Run a basic Linux VM +* Install Docker +* Run a simple Flask application +* Stay within free tier limits + +## Region / Zone + +* Region: ru-central1 +* Zone: ru-central1-a + +### Why This Region? + +* Default and most stable region +* Supported by free-tier eligible resources +* Minimal latency for testing purposes + +## Total Cost + +$0 — all resources were created within free tier limits. + +## Resources Created + +### Networking + +* VPC Network +* Subnet +* Security Group +* Security Group Rules (SSH, HTTP, App Port, Egress) + +### Compute + +* Virtual Machine instance +* Boot disk (10 GB) +* Public IP via NAT + +# 2. Terraform Implementation + +## Terraform Version Used + +Terraform v1.14.5 (latest stable at time of implementation) + +## Project Structure + +``` +terraform/ +├── main.tf +├── variables.tf +├── outputs.tf +├── provider.tf +└── .gitignore +``` + +Explanation: + +* provider.tf — Cloud provider configuration +* variables.tf — Input variables (folder_id, cloud_id, ssh key path) +* main.tf — Network, Security Group, VM definitions +* outputs.tf — VM public IP output + +## Key Configuration Decisions + +* Used service account key file for authentication +* Separated networking and compute resources +* Opened only required ports (22, 80, 5000) +* Used Ubuntu 22.04 LTS image family +* Enabled NAT for public access + +## Challenges Encountered + +* Understanding how SSH public key injection works +* Configuring service account credentials properly +* Debugging Security Group rule syntax +* Handling environment variables for authentication + +## Key Command Outputs + +### terraform init + +``` +Terraform has been successfully initialized! +``` + +### terraform plan (sanitized) + +![](screenshots_l4/ter-plan-p1.png) +![](screenshots_l4/ter-plan-p2.png) +![](screenshots_l4/ter-plan-p3.png) + +### terraform apply + +![](screenshots_l4/ter-apply.png) + +### SSH Connection + +![](screenshots_l4/ter-ssh.png) + +# 3. Pulumi Implementation + +## Pulumi Version & Language + +* Pulumi v3.221.0 +* Language: Python + +## How Code Differs from Terraform + +* Uses general-purpose programming language (Python) +* Resources created via classes instead of HCL blocks +* Security Group rules required separate resource definitions +* Native programming logic available (loops, variables, functions) + +## Advantages Discovered + +* Full power of Python +* Easier reuse of logic +* Familiar syntax for developers +* Better integration with application code + +## Challenges Encountered + +* Version compatibility issues (Python 3.13) +* Missing Python dependencies (pkg_resources) +* Differences in Security Group rule implementation +* Less documentation/examples compared to Terraform + +## Key Command Outputs + +### pulumi preview + +![](screenshots_l4/pul-plan.png) + +### pulumi up + +![](screenshots_l4/pul-up.png) + +### SSH Connection + +![](screenshots_l4/pul-ssh.png) + +# 4. Terraform vs Pulumi Comparison + +## Ease of Learning + +Terraform was easier initially because of simpler declarative syntax and better documentation. Pulumi required understanding provider-specific quirks and Python dependency management. + +--- + +## Code Readability + +Terraform is cleaner and more readable for pure infrastructure definitions. Pulumi is more flexible but slightly more verbose. + +--- + +## Debugging + +Terraform was easier to debug due to clearer error messages and larger community support. Pulumi debugging required analyzing Python stack traces. + +--- + +## Documentation + +Terraform has better documentation and more real-world examples. Pulumi documentation for Yandex Cloud is more limited. + +--- + +## Use Case + +Terraform is ideal for pure infrastructure management and team environments. +Pulumi is preferable when infrastructure must tightly integrate with application logic. + +--- + +# 5. Lab 5 Preparation & Cleanup + +## VM for Lab 5 + +Yes — VM will be kept for Lab 5. + +Selected VM: Pulumi-created VM. + +Reason: Cleaner final implementation and better understanding of configuration. + +--- + +## Cleanup Status + +VM is still running and accessible via SSH. + +Verification: +``` +ssh ubuntu@93.77.185.211 +``` + +![](screenshots_l4/l4-cloud.png) diff --git a/docs/screenshots_l4/l4-cloud.png b/docs/screenshots_l4/l4-cloud.png new file mode 100644 index 0000000000..70f507c21c Binary files /dev/null and b/docs/screenshots_l4/l4-cloud.png differ diff --git a/docs/screenshots_l4/pul-plan.png b/docs/screenshots_l4/pul-plan.png new file mode 100644 index 0000000000..68181300f4 Binary files /dev/null and b/docs/screenshots_l4/pul-plan.png differ diff --git a/docs/screenshots_l4/pul-ssh.png b/docs/screenshots_l4/pul-ssh.png new file mode 100644 index 0000000000..4ba6758f41 Binary files /dev/null and b/docs/screenshots_l4/pul-ssh.png differ diff --git a/docs/screenshots_l4/pul-up.png b/docs/screenshots_l4/pul-up.png new file mode 100644 index 0000000000..dcd7724191 Binary files /dev/null and b/docs/screenshots_l4/pul-up.png differ diff --git a/docs/screenshots_l4/ter-apply.png b/docs/screenshots_l4/ter-apply.png new file mode 100644 index 0000000000..5a83723867 Binary files /dev/null and b/docs/screenshots_l4/ter-apply.png differ diff --git a/docs/screenshots_l4/ter-plan-p1.png b/docs/screenshots_l4/ter-plan-p1.png new file mode 100644 index 0000000000..a82fa358ec Binary files /dev/null and b/docs/screenshots_l4/ter-plan-p1.png differ diff --git a/docs/screenshots_l4/ter-plan-p2.png b/docs/screenshots_l4/ter-plan-p2.png new file mode 100644 index 0000000000..25e3179d2f Binary files /dev/null and b/docs/screenshots_l4/ter-plan-p2.png differ diff --git a/docs/screenshots_l4/ter-plan-p3.png b/docs/screenshots_l4/ter-plan-p3.png new file mode 100644 index 0000000000..f03ba30402 Binary files /dev/null and b/docs/screenshots_l4/ter-plan-p3.png differ diff --git a/docs/screenshots_l4/ter-ssh.png b/docs/screenshots_l4/ter-ssh.png new file mode 100644 index 0000000000..22b1be20ef Binary files /dev/null and b/docs/screenshots_l4/ter-ssh.png differ diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..a278f7910a --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,338 @@ +# Lab 9 — Kubernetes Fundamentals + +## 1. Architecture Overview + +### Deployment Architecture + +The application is deployed to a local Kubernetes cluster using **kind**. The architecture consists of: + +* **Deployment**: Manages application Pods +* **Pods**: 3–5 replicas of the Python application container +* **Service (NodePort)**: Exposes the application outside the cluster + +### Components + +* **Pods**: + Each Pod runs a container based on the image: + + ``` + mrdebuff/devops-info-service:latest + ``` + + The container runs a Python application on port **6000**. + +* **Deployment**: + + * Initially configured with **3 replicas** + * Scaled to **5 replicas** during testing + * Uses **RollingUpdate strategy** for zero-downtime deployments + +* **Service**: + + * Type: `NodePort` + * Exposes the app on port **30080** + * Routes traffic to Pods on port **6000** + +### Networking Flow + +``` +User → localhost:8080 (port-forward) + ↓ +Kubernetes Service (NodePort) + ↓ +Pods (via label selector) + ↓ +Container (Python app on port 6000) +``` + +### Resource Allocation Strategy + +Each container has defined resource constraints: + +* **Requests**: + + * CPU: 100m + * Memory: 128Mi +* **Limits**: + + * CPU: 200m + * Memory: 256Mi + +This ensures: + +* Proper scheduling by Kubernetes +* Prevention of resource starvation +* Cluster stability + +--- + +## 2. Manifest Files + +### deployment.yml + +Defines the desired state of the application. + +**Key configurations:** + +* `replicas: 3` (scaled to 5 later) +* Rolling update strategy: + + ```yaml + maxSurge: 1 + maxUnavailable: 0 + ``` +* Container: + + * Image: `mrdebuff/devops-info-service:latest` + * Port: `6000` + +**Health Checks:** + +* **Liveness Probe** (`/health`) +* **Readiness Probe** (`/health`) + +**Why:** + +* Ensures automatic restart if app crashes +* Prevents traffic routing to unready Pods + +**Resources:** + +* Requests and limits defined for production-like behavior + +--- + +### service.yml + +Exposes the application. + +**Configuration:** + +* Type: `NodePort` +* Port mapping: + + * Service port: `80` + * Target port: `6000` + * NodePort: `30080` + +**Why NodePort:** + +* Suitable for local development +* No cloud load balancer required + +--- + +## 3. Deployment Evidence + +### Cluster State + +```bash +kubectl get all +``` + +![](screenshots_l9/sc0_get_all.png) + + +--- + +### Pods and Services + +```bash +kubectl get pods,svc -o wide +``` + +![](screenshots_l9/sc0_get_detailed_view.png) + +--- + +### Deployment Description + +```bash +kubectl describe deployment devops-info-service +``` + +Shows: + +* Replica count +* Rolling update strategy +* Resource configuration + +![](screenshots_l9/sc0_describe_deployment.png) + +--- + +### Application Access + +```bash +kubectl port-forward service/devops-info-service 8080:80 +``` + +![](screenshots_l9/sc3_service.png) + + +```bash +curl http://localhost:8080 +``` + +![](screenshots_l9/sc4_localhost.png) + +--- + +## 4. Operations Performed + +### Deployment + +```bash +kubectl apply -f k8s/deployment.yml +kubectl apply -f k8s/service.yml +``` + +![](screenshots_l9/sc2_deployment.png) + +![](screenshots_l9/sc3_service.png) + +--- + +### Scaling + +Scaled deployment to 5 replicas: + +```bash +kubectl scale deployment/devops-info-service --replicas=5 +``` + +![](screenshots_l9/sc5_scaling.png) + +--- + +### Rolling Update + +Updated deployment (e.g., environment variable change): + +```bash +kubectl apply -f k8s/deployment.yml +``` + +Monitor rollout: + +```bash +kubectl rollout status deployment/devops-info-service +kubectl get pods -w +``` + +![](screenshots_l9/sc6_p1_rolling_update.png) + +![](screenshots_l9/sc6_p2_rolling_update.png) + +Observed: + +* New Pods created +* Old Pods terminated gradually +* No downtime + +--- + +### Rollback + +```bash +kubectl rollout history deployment/devops-info-service +kubectl rollout undo deployment/devops-info-service +``` + +![](screenshots_l9/sc7_rollback.png) + +--- + +### Service Access + +```bash +kubectl port-forward service/devops-info-service 8080:80 +``` + +Application доступна по: + +``` +http://localhost:8080 +``` + +![](screenshots_l9/sc4_localhost.png) + +--- + +## 5. Production Considerations + +### Health Checks + +Implemented: + +* **Liveness Probe** → detects crashes and restarts container +* **Readiness Probe** → ensures traffic goes only to ready Pods + +Why: + +* Improves reliability +* Prevents serving broken instances + +--- + +### Resource Limits + +Set to: + +* Prevent one container from consuming all resources +* Enable Kubernetes scheduling decisions + +--- + +### Improvements for Production + +* Use **Ingress** instead of NodePort +* Add **Horizontal Pod Autoscaler (HPA)** +* Use **ConfigMaps and Secrets** +* Implement **CI/CD pipeline** +* Use **image versioning (not latest)** + +--- + +### Monitoring & Observability + +Recommended tools: + +* Prometheus + Grafana (metrics) +* Loki / ELK stack (logs) +* Kubernetes events monitoring + +--- + +## 6. Challenges & Solutions + +### Issue 1: Wrong container port + +**Problem:** +Initial mismatch between Dockerfile (6000) and Kubernetes config. + +**Solution:** +Updated: + +```yaml +containerPort: 6000 +targetPort: 6000 +``` + +### Debugging Tools Used + +```bash +kubectl describe pod +kubectl logs +kubectl get events +``` + +--- + +### Key Learnings + +* Kubernetes is **declarative** (desired state vs actual state) +* Deployments manage Pods automatically +* Services provide stable networking +* Health checks are critical for reliability +* Rolling updates ensure zero downtime diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000000..42b3b3b79d --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: devops-info-service + labels: + app: devops-info +spec: + replicas: 5 + selector: + matchLabels: + app: devops-info + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: devops-info + spec: + containers: + - name: devops-info-container + image: mrdebuff/devops-info-service:latest + ports: + - containerPort: 6000 + env: + - name: VERSION + value: "v2" + + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "200m" + memory: "256Mi" + + livenessProbe: + httpGet: + path: /health + port: 6000 + initialDelaySeconds: 10 + periodSeconds: 5 + + readinessProbe: + httpGet: + path: /health + port: 6000 + initialDelaySeconds: 5 + periodSeconds: 3 \ No newline at end of file diff --git a/k8s/screenshots_l9/sc0_describe_deployment.png b/k8s/screenshots_l9/sc0_describe_deployment.png new file mode 100644 index 0000000000..ac1de877e8 Binary files /dev/null and b/k8s/screenshots_l9/sc0_describe_deployment.png differ diff --git a/k8s/screenshots_l9/sc0_get_all.png b/k8s/screenshots_l9/sc0_get_all.png new file mode 100644 index 0000000000..2d59d370e3 Binary files /dev/null and b/k8s/screenshots_l9/sc0_get_all.png differ diff --git a/k8s/screenshots_l9/sc0_get_detailed_view.png b/k8s/screenshots_l9/sc0_get_detailed_view.png new file mode 100644 index 0000000000..7552081466 Binary files /dev/null and b/k8s/screenshots_l9/sc0_get_detailed_view.png differ diff --git a/k8s/screenshots_l9/sc1_cluster.png b/k8s/screenshots_l9/sc1_cluster.png new file mode 100644 index 0000000000..3062357d0d Binary files /dev/null and b/k8s/screenshots_l9/sc1_cluster.png differ diff --git a/k8s/screenshots_l9/sc2_deployment.png b/k8s/screenshots_l9/sc2_deployment.png new file mode 100644 index 0000000000..b58d9c6672 Binary files /dev/null and b/k8s/screenshots_l9/sc2_deployment.png differ diff --git a/k8s/screenshots_l9/sc3_service.png b/k8s/screenshots_l9/sc3_service.png new file mode 100644 index 0000000000..ff55b96b3b Binary files /dev/null and b/k8s/screenshots_l9/sc3_service.png differ diff --git a/k8s/screenshots_l9/sc4_localhost.png b/k8s/screenshots_l9/sc4_localhost.png new file mode 100644 index 0000000000..311a1256f1 Binary files /dev/null and b/k8s/screenshots_l9/sc4_localhost.png differ diff --git a/k8s/screenshots_l9/sc5_scaling.png b/k8s/screenshots_l9/sc5_scaling.png new file mode 100644 index 0000000000..585a1cd1ef Binary files /dev/null and b/k8s/screenshots_l9/sc5_scaling.png differ diff --git a/k8s/screenshots_l9/sc6_p1_rolling_update.png b/k8s/screenshots_l9/sc6_p1_rolling_update.png new file mode 100644 index 0000000000..e02a28cd3d Binary files /dev/null and b/k8s/screenshots_l9/sc6_p1_rolling_update.png differ diff --git a/k8s/screenshots_l9/sc6_p2_rolling_update.png b/k8s/screenshots_l9/sc6_p2_rolling_update.png new file mode 100644 index 0000000000..3396fc5dc0 Binary files /dev/null and b/k8s/screenshots_l9/sc6_p2_rolling_update.png differ diff --git a/k8s/screenshots_l9/sc7_rollback.png b/k8s/screenshots_l9/sc7_rollback.png new file mode 100644 index 0000000000..402bb3c61f Binary files /dev/null and b/k8s/screenshots_l9/sc7_rollback.png differ diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000000..f1769028a0 --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: devops-info-service +spec: + type: NodePort + selector: + app: devops-info + ports: + - protocol: TCP + port: 80 + targetPort: 6000 + nodePort: 30080 \ No newline at end of file diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 0000000000..a8ef8344aa --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,132 @@ +services: + + prometheus: + image: prom/prometheus:v3.9.0 + ports: + - "9090:9090" + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention.time=5d' + - '--storage.tsdb.retention.size=5GB' + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + networks: + - logging + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + cpus: '1.0' + memory: 1G + + loki: + image: grafana/loki:3.0.0 + command: -config.file=/etc/loki/config.yml + ports: + - "3100:3100" + volumes: + - ./loki/config.yml:/etc/loki/config.yml + - loki-data:/loki + networks: + - logging + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3100/ready || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + deploy: + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + + promtail: + image: grafana/promtail:3.0.0 + command: -config.file=/etc/promtail/config.yml + volumes: + - ./promtail/config.yml:/etc/promtail/config.yml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + networks: + - logging + depends_on: + - loki + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + grafana: + image: grafana/grafana:12.3.1 + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + networks: + - logging + environment: + - GF_AUTH_ANONYMOUS_ENABLED=false + - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} + depends_on: + - prometheus + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/api/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + app-python: + build: ../app_python + ports: + - "8000:6000" + networks: + - logging + labels: + logging: "promtail" + app: "devops-python" + depends_on: + - promtail + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.25' + memory: 128M + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:6000/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + logging: + +volumes: + loki-data: + grafana-data: + prometheus-data: \ No newline at end of file diff --git a/monitoring/docs/LAB07.md b/monitoring/docs/LAB07.md new file mode 100644 index 0000000000..709d91176a --- /dev/null +++ b/monitoring/docs/LAB07.md @@ -0,0 +1,442 @@ +# Lab 7 — Observability & Logging with Loki Stack + +## Architecture + +The logging architecture consists of four main components: + +* **Application (Flask)** — generates structured logs +* **Promtail** — collects logs from Docker containers +* **Loki** — stores logs and indexes metadata labels +* **Grafana** — visualizes logs and dashboards + +``` ++--------------------+ +| Python Flask App | +| (Docker container) | ++---------+----------+ + | + | logs (stdout) + v ++--------------------+ +| Promtail | +| (Log collector) | ++---------+----------+ + | + | pushes logs + v ++--------------------+ +| Loki | +| (Log storage TSDB) | ++---------+----------+ + | + | queries + v ++--------------------+ +| Grafana | +| Dashboards & UI | ++--------------------+ +``` + +Promtail automatically discovers containers using the **Docker API** and attaches metadata labels such as container name and application label before sending logs to Loki. + +--- + +## Setup Guide + +### 1. Project Structure + +``` +project/ +│ +├── app_python/ +│ ├── app.py +│ ├── Dockerfile +│ └── requirements.txt +│ +└── monitoring/ + ├── docker-compose.yml + ├── .env + ├── loki/ + │ └── config.yml + ├── promtail/ + │ └── config.yml + └── docs/ + ├── screenshots_l7/ + └── LAB07.md +``` + +--- + +### 2. Build and start the stack + +From the `monitoring` directory: + +```bash +docker compose up -d --build +``` + +Verify containers: + +```bash +docker compose ps +``` + +![](./screenshots_l7/sc_1.png) + +--- + +### 3. Verify Loki + +``` +curl http://localhost:3100/ready +``` + +Expected response: + +``` +ready +``` + +![](./screenshots_l7/sc_2.png) + +--- + +### 4. Verify Promtail + +``` +curl http://localhost:9080/targets +``` + +--- + +### 5. Access Grafana + +``` +http://localhost:3000 +``` + +Login using credentials from the `.env` file. + +![](./screenshots_l7/sc_12.png) + +Add **Loki data source**: + +``` +URL: http://loki:3100 +``` + +![](./screenshots_l7/sc_3.png) + +Then open **Explore → Loki** to start querying logs. + +![](./screenshots_l7/sc_4.png) + +![](./screenshots_l7/sc_5.png) + +--- + +## Configuration + +### Loki Configuration + +Loki is configured to use the **TSDB storage engine**, which improves query performance and reduces memory usage. + +Example configuration snippet: + +```yaml +auth_enabled: false + +server: + http_listen_port: 3100 + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 +``` + +Key points: + +* **TSDB storage** improves query performance +* **Filesystem object store** used for a single-node setup +* **Schema v13** recommended for Loki 3.0+ + +Retention is configured to keep logs for **7 days**. + +--- + +### Promtail Configuration + +Promtail collects logs from Docker containers using **Docker service discovery**. + +Example snippet: + +```yaml +scrape_configs: + - job_name: docker + + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + + relabel_configs: + + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' +``` + +Promtail extracts metadata labels such as: + +* container name +* application label +* job identifier + +These labels allow Loki to efficiently index logs and enable powerful queries in Grafana. + +--- + +## Application Logging + +The Flask application was modified to use **structured JSON logging**. + +The library **python-json-logger** was used to format logs. + +Example logging configuration: + +```python +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) +``` + +Example log output: + +```json +{ + "asctime": "2026-03-12T20:10:01Z", + "levelname": "INFO", + "message": "request_received", + "method": "GET", + "path": "/" +} +``` + +Structured logs allow Loki to parse JSON fields and filter them using **LogQL queries**. + +--- + +## Dashboard + +A Grafana dashboard was created to visualize logs and log-derived metrics. + +The dashboard includes four panels. + +![Dashboard](screenshots_l7/sc_10.png) + +--- + +## 1. Logs Panel + +Displays recent logs from all applications. + +LogQL query: + +``` +{app=~"devops-.*"} +``` + +--- + +## 2. Request Rate + +Displays the number of log entries per second by application. + +LogQL query: + +``` +sum by (app) (rate({app=~"devops-.*"}[1m])) +``` + +Visualization: **Time Series** + +--- + +## 3. Error Logs + +Shows only error-level logs. + +LogQL query: + +``` +{app=~"devops-.*"} | json | level="error" +``` + +--- + +## 4. Log Level Distribution + +Shows the number of logs by level. + +LogQL query: + +``` +sum by (level) ( + count_over_time({app=~"devops-.*"} | json [5m]) +) +``` + +Visualization: **Pie Chart** + +--- + +## Production Configuration + +Several production best practices were implemented. + +### Resource limits + +To prevent containers from consuming excessive resources: + +```yaml +deploy: + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M +``` + +--- + +### Security + +Grafana anonymous authentication was disabled. + +Environment variables were stored in `.env` instead of the compose file. + +Example: + +``` +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=strongpassword +``` + +The `.env` file is excluded from version control using `.gitignore`. + +--- + +### Health checks + +Health checks ensure services are operational. + +Example (Grafana): + +```yaml +healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3100/ready || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s +``` + +--- + +## Testing + +### Generate logs + +``` +for i in {1..20}; do curl http://localhost:8000/; done + +for i in {1..20}; do curl http://localhost:8000/health; done +``` + +![Dashboard](screenshots_l7/sc_6.png) + +![](screenshots_l7/sc_7.png) + + + +--- + +### Query logs in Grafana + +Example LogQL queries: + +All logs: + +``` +{job="docker"} +``` + +Logs from Python app: + +``` +{app="devops-python"} +``` + +Only errors: + +``` +{app="devops-python"} |= "ERROR" +``` + +![](screenshots_l7/sc_8.png) + +Parse JSON logs: + +``` +{app="devops-python"} | json +``` + +--- + +## Challenges + +### Promtail logs rejected by Loki + +Error encountered: + +``` +error at least one label pair is required per stream +``` + +Cause: + +Promtail was sending logs without labels. + +Solution: + +Added a static label in `promtail/config.yml`: + +```yaml +- source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'job' + replacement: 'docker' +``` + +This ensured every log stream contained at least one label. + +--- + +# Conclusion + +The Loki logging stack successfully aggregated logs from containerized applications and visualized them in Grafana dashboards. + +Key benefits of this approach include: + +* centralized logging +* efficient label-based log indexing +* powerful LogQL querying +* integration with containerized environments + +This setup demonstrates modern observability practices used in cloud-native infrastructure. diff --git a/monitoring/docs/LAB08.md b/monitoring/docs/LAB08.md new file mode 100644 index 0000000000..4e5d9fc2ae --- /dev/null +++ b/monitoring/docs/LAB08.md @@ -0,0 +1,370 @@ +# Lab 8 — Metrics & Monitoring with Prometheus + +## 1. Architecture + +The monitoring architecture consists of the following components: + +- Python application exposing metrics +- Prometheus scraping metrics +- Grafana visualizing metrics +- Loki + Promtail collecting logs + +### Architecture Diagram + +``` +Application → Prometheus → Grafana +Application → Promtail → Loki → Grafana +``` + +### Components + +| Component | Purpose | +|--------|--------| +| Application | Exposes `/metrics` endpoint | +| Prometheus | Scrapes and stores metrics | +| Grafana | Dashboards and visualization | +| Loki | Log aggregation | +| Promtail | Log shipping agent | + +### Data Flow + +1. Application exposes metrics at `/metrics` +2. Prometheus scrapes metrics every 15 seconds +3. Metrics stored in Prometheus TSDB +4. Grafana queries Prometheus using PromQL +5. Dashboards visualize system health and performance + +## 2. Application Instrumentation + +The Python application was instrumented using the `prometheus-client` library. + +### Added Metrics + +| Metric | Type | Purpose | +|------|------|------| +| `http_requests_total` | Counter | Total number of HTTP requests | +| `http_request_duration_seconds` | Histogram | Request latency | +| `http_requests_in_progress` | Gauge | Active requests | +| `app_info` | Gauge | Application metadata | + +### Example Metrics Endpoint + +Application exposes metrics via: + +``` +/metrics +``` + +Example output: + +``` +#HELP http_requests_total Total HTTP requests +#TYPE http_requests_total counter +http_requests_total{method="GET",endpoint="/"} 15 +``` + +### Screenshot + +![](screenshots_l8/t1_p1.png) + +## 3. Prometheus Configuration + +Prometheus is configured to scrape multiple services. + +### Scrape Interval + +``` +scrape_interval: 15s +evaluation_interval: 15s +``` + +### Scrape Targets + +| Target | Endpoint | +|------|------| +| Prometheus | localhost:9090 | +| Application | app-python:6000 | +| Grafana | grafana:3000 | +| Loki | loki:3100 | + +Configuration example: + +``` +- job_name: 'app' +metrics_path: /metrics +static_configs: + - targets: ['app-python:6000'] +``` + +### Retention + +Prometheus storage configured via command flags: + +``` +--storage.tsdb.retention.time=5d +--storage.tsdb.retention.size=5GB +``` + +### Targets Page + +![](screenshots_l8/t2_p1.png) + +![](screenshots_l8/t2_p2.png) + +## 4. Dashboard Walkthrough + +A custom Grafana dashboard was created with multiple panels. + +### Panel Overview + +1. **Request Rate** (Graph) + - Query: `sum(rate(http_requests_total[5m])) by (endpoint)` + - Shows requests/sec per endpoint + +2. **Error Rate** (Graph) + - Query: `sum(rate(http_requests_total{status=~"5.."}[5m]))` + - Shows 5xx errors/sec + +3. **Request Duration p95** (Graph) + - Query: `histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))` + - Shows 95th percentile latency + +4. **Request Duration Heatmap** (Heatmap) + - Query: `rate(http_request_duration_seconds_bucket[5m])` + - Visualizes latency distribution + +5. **Active Requests** (Gauge/Graph) + - Query: `http_requests_in_progress` + - Shows concurrent requests + +6. **Status Code Distribution** (Pie Chart) + - Query: `sum by (status) (rate(http_requests_total[5m]))` + - Shows 2xx vs 4xx vs 5xx + +7. **Uptime** (Stat) + - Query: `up{job="app"}` + - Shows if service is up (1) or down (0) + +### Dashboard Screenshots + +![](screenshots_l8/t3_p1.png) + +![](screenshots_l8/t3_p2.png) + +Also imported dashboard: + +![](screenshots_l8/t3_p3.png) + + +--- + +## 5. PromQL Examples + +Below are example PromQL queries used during monitoring. + +### (1) Requests Per Second + +``` +rate(http_requests_total[1m]) +``` + +Shows request throughput. + +--- + +### (2) Total Requests + +``` +http_requests_total +``` + +Total number of processed requests. + +--- + +### (3) 95th Percentile Latency + +``` +histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) +``` + +Shows high-percentile latency. + +--- + +### (4) Error Rate + +``` +rate(http_requests_total{status=~"5.."}[1m]) +``` + +Tracks server errors. + +--- + +### (6) Active Requests + +``` +http_requests_in_progress +``` + +Shows currently processing requests. + +--- + +### Example Query Result + +![](screenshots_l8/t2_p2.png) + +## 6. Production Setup + +Several production practices were implemented. + +### Health Checks + +Each service has a healthcheck configured. + +Example: + +``` +healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:6000/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 +``` + +### Resource Limits + +Containers have CPU and memory limits: + +``` +deploy: + resources: + limits: + cpus: '1.0' + memory: 1G +``` + +### Persistence + +Persistent volumes ensure data durability. + +``` +volumes: + loki-data: + grafana-data: + prometheus-data: +``` + +- Check that dashboards exist in Grafana. + + ![](screenshots_l8/t4_p1.png) + +- Check `docker compose ps` and then `down` containers + + ![](screenshots_l8/t4_p2.png) + +- `up` containers and check `docker compose ps` + + ![](screenshots_l8/t4_p3.png) + +- Check that dashboards exist in Grafana (check time on all screenshots) + + ![](screenshots_l8/t4_p4.png) + +## 7. Testing Results + +The monitoring system was validated with several checks. + +### Services Status + +``` +docker compose ps +``` + +All services are healthy. + +![](screenshots_l8/t4_p5.png) + +### Prometheus Targets + +All scrape targets are operational. + +![](screenshots_l8/t2_p1.png) + + +--- + +## 8. Metrics vs Logs (Comparison with Lab 7) + +| Feature | Metrics | Logs | +|------|------|------| +| Purpose | Aggregated monitoring | Detailed events | +| Storage | Time-series | Text | +| Query Language | PromQL | LogQL | +| Use Case | Alerts, dashboards | Debugging | + +### When to Use Metrics + +- performance monitoring +- error rate tracking +- system health dashboards + +### When to Use Logs + +- debugging application errors +- investigating incidents +- tracing user activity + +## 9. Challenges & Solutions + +### Issue 1 — Prometheus container failing + +**Problem** + +Prometheus failed to start due to invalid configuration. + +**Cause** + +Retention settings were placed inside `prometheus.yml`. + +**Solution** + +Moved retention settings to container command flags. + +### Issue 2 — Application healthcheck unhealthy + +**Problem** + +Docker reported the application as unhealthy. + +**Cause** + +Healthcheck was using the wrong port (`8000` instead of container port `6000`). + +**Solution** + +Updated healthcheck endpoint: + +``` +curl http://localhost:6000/health +``` + +### Issue 3 — curl not available in container + +**Problem** + +Healthcheck command failed. + +**Cause** + +`curl` was not installed in the base image. + +**Solution** + +Installed curl in Dockerfile: + +``` +apt-get update && apt-get install -y curl +``` diff --git a/monitoring/docs/dashboard.json b/monitoring/docs/dashboard.json new file mode 100644 index 0000000000..e765496c45 --- /dev/null +++ b/monitoring/docs/dashboard.json @@ -0,0 +1,596 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Shows requests/sec per endpoint", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "editorMode": "code", + "expr": "sum(rate(http_requests_total[5m])) by (endpoint)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "5xx errors/sec", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Error Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Shows 95th percentile latency", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Duration p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Visualizes latency distribution", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "rate(http_request_duration_seconds_bucket[5m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Duration Heatmap", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Shows concurrent requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "http_requests_in_progress", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Active Requests", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Shows 2xx vs 4xx vs 5xx", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "sum by (status) (rate(http_requests_total[5m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Status Code Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "efga470ffos8wa" + }, + "description": "Shows if service is up (1) or down (0)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 6, + "y": 24 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "up{job=\"app\"}", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Uptime", + "type": "stat" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "New dashboard", + "uid": "adznzqk", + "version": 8 +} \ No newline at end of file diff --git a/monitoring/docs/screenshots_l7/sc_1.png b/monitoring/docs/screenshots_l7/sc_1.png new file mode 100644 index 0000000000..a1bcb7298a Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_1.png differ diff --git a/monitoring/docs/screenshots_l7/sc_10.png b/monitoring/docs/screenshots_l7/sc_10.png new file mode 100644 index 0000000000..f9fff5a065 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_10.png differ diff --git a/monitoring/docs/screenshots_l7/sc_11.png b/monitoring/docs/screenshots_l7/sc_11.png new file mode 100644 index 0000000000..d9e7f3f180 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_11.png differ diff --git a/monitoring/docs/screenshots_l7/sc_12.png b/monitoring/docs/screenshots_l7/sc_12.png new file mode 100644 index 0000000000..4a30b98e9a Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_12.png differ diff --git a/monitoring/docs/screenshots_l7/sc_2.png b/monitoring/docs/screenshots_l7/sc_2.png new file mode 100644 index 0000000000..e3901b8023 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_2.png differ diff --git a/monitoring/docs/screenshots_l7/sc_3.png b/monitoring/docs/screenshots_l7/sc_3.png new file mode 100644 index 0000000000..7972429eab Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_3.png differ diff --git a/monitoring/docs/screenshots_l7/sc_4.png b/monitoring/docs/screenshots_l7/sc_4.png new file mode 100644 index 0000000000..3d64d78c95 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_4.png differ diff --git a/monitoring/docs/screenshots_l7/sc_5.png b/monitoring/docs/screenshots_l7/sc_5.png new file mode 100644 index 0000000000..a5dce16911 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_5.png differ diff --git a/monitoring/docs/screenshots_l7/sc_6.png b/monitoring/docs/screenshots_l7/sc_6.png new file mode 100644 index 0000000000..ef357c085d Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_6.png differ diff --git a/monitoring/docs/screenshots_l7/sc_7.png b/monitoring/docs/screenshots_l7/sc_7.png new file mode 100644 index 0000000000..f66b58393a Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_7.png differ diff --git a/monitoring/docs/screenshots_l7/sc_8.png b/monitoring/docs/screenshots_l7/sc_8.png new file mode 100644 index 0000000000..fb5946c1b7 Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_8.png differ diff --git a/monitoring/docs/screenshots_l7/sc_9.png b/monitoring/docs/screenshots_l7/sc_9.png new file mode 100644 index 0000000000..d0e22b007d Binary files /dev/null and b/monitoring/docs/screenshots_l7/sc_9.png differ diff --git a/monitoring/docs/screenshots_l8/t1_p1.png b/monitoring/docs/screenshots_l8/t1_p1.png new file mode 100644 index 0000000000..7579319240 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t1_p1.png differ diff --git a/monitoring/docs/screenshots_l8/t2_p1.png b/monitoring/docs/screenshots_l8/t2_p1.png new file mode 100644 index 0000000000..9bbac5a671 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t2_p1.png differ diff --git a/monitoring/docs/screenshots_l8/t2_p2.png b/monitoring/docs/screenshots_l8/t2_p2.png new file mode 100644 index 0000000000..d3cbf3a819 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t2_p2.png differ diff --git a/monitoring/docs/screenshots_l8/t3_p1.png b/monitoring/docs/screenshots_l8/t3_p1.png new file mode 100644 index 0000000000..24be55f3c0 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t3_p1.png differ diff --git a/monitoring/docs/screenshots_l8/t3_p2.png b/monitoring/docs/screenshots_l8/t3_p2.png new file mode 100644 index 0000000000..18daa662d2 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t3_p2.png differ diff --git a/monitoring/docs/screenshots_l8/t3_p3.png b/monitoring/docs/screenshots_l8/t3_p3.png new file mode 100644 index 0000000000..e1841c1abe Binary files /dev/null and b/monitoring/docs/screenshots_l8/t3_p3.png differ diff --git a/monitoring/docs/screenshots_l8/t4_p1.png b/monitoring/docs/screenshots_l8/t4_p1.png new file mode 100644 index 0000000000..0e58d7ba81 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t4_p1.png differ diff --git a/monitoring/docs/screenshots_l8/t4_p2.png b/monitoring/docs/screenshots_l8/t4_p2.png new file mode 100644 index 0000000000..5563c797c4 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t4_p2.png differ diff --git a/monitoring/docs/screenshots_l8/t4_p3.png b/monitoring/docs/screenshots_l8/t4_p3.png new file mode 100644 index 0000000000..4ffb151a00 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t4_p3.png differ diff --git a/monitoring/docs/screenshots_l8/t4_p4.png b/monitoring/docs/screenshots_l8/t4_p4.png new file mode 100644 index 0000000000..895a62cad8 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t4_p4.png differ diff --git a/monitoring/docs/screenshots_l8/t4_p5.png b/monitoring/docs/screenshots_l8/t4_p5.png new file mode 100644 index 0000000000..392aae5ae9 Binary files /dev/null and b/monitoring/docs/screenshots_l8/t4_p5.png differ diff --git a/monitoring/loki/config.yml b/monitoring/loki/config.yml new file mode 100644 index 0000000000..8c11e8088e --- /dev/null +++ b/monitoring/loki/config.yml @@ -0,0 +1,36 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + log_level: info + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +limits_config: + retention_period: 168h + +ingester: + chunk_idle_period: 3m + chunk_retain_period: 1m + max_chunk_age: 1h + + diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 0000000000..043e5731a2 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,24 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'app' + metrics_path: /metrics + static_configs: + - targets: ['app-python:6000'] + + - job_name: 'loki' + metrics_path: /metrics + static_configs: + - targets: ['loki:3100'] + + - job_name: 'grafana' + metrics_path: /metrics + static_configs: + - targets: ['grafana:3000'] \ No newline at end of file diff --git a/monitoring/promtail/config.yml b/monitoring/promtail/config.yml new file mode 100644 index 0000000000..77720802e4 --- /dev/null +++ b/monitoring/promtail/config.yml @@ -0,0 +1,39 @@ +server: + http_listen_port: 9080 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + + relabel_configs: + + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + + - source_labels: ['__meta_docker_container_label_app'] + regex: '(.*)' + target_label: 'app' + + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'app' + replacement: 'devops-python' + + - source_labels: ['__meta_docker_container_image_name'] + regex: '(.*):.*' + target_label: 'image' + + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'job' + replacement: 'docker' \ No newline at end of file diff --git a/pulumi/.gitignore b/pulumi/.gitignore new file mode 100644 index 0000000000..0e85f9e4ce --- /dev/null +++ b/pulumi/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +venv/ +*.log \ No newline at end of file diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..a8e4064b4d --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,11 @@ +name: lab4-pulumi +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..67386708d0 --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,100 @@ +import pulumi +import pulumi_yandex as yc +from pathlib import Path + +config = pulumi.Config() + +network = yc.VpcNetwork( + "lab4-network" +) + +subnet = yc.VpcSubnet( + "lab4-subnet", + network_id=network.id, + zone="ru-central1-a", + v4_cidr_blocks=["10.0.0.0/24"], +) + +sg = yc.VpcSecurityGroup( + "lab4-sg", + network_id=network.id, +) + + +# SSH +yc.VpcSecurityGroupRule( + "ssh-ingress", + security_group_binding=sg.id, + direction="ingress", + protocol="TCP", + port=22, + v4_cidr_blocks=["0.0.0.0/0"], +) + +# HTTP +yc.VpcSecurityGroupRule( + "http-ingress", + security_group_binding=sg.id, + direction="ingress", + protocol="TCP", + port=80, + v4_cidr_blocks=["0.0.0.0/0"], +) + +# Flask / App +yc.VpcSecurityGroupRule( + "flask-ingress", + security_group_binding=sg.id, + direction="ingress", + protocol="TCP", + port=5000, + v4_cidr_blocks=["0.0.0.0/0"], +) + +# Egress (всё наружу) +yc.VpcSecurityGroupRule( + "all-egress", + security_group_binding=sg.id, + direction="egress", + protocol="ANY", + v4_cidr_blocks=["0.0.0.0/0"], +) + +image = yc.get_compute_image( + family="ubuntu-2204-lts" +) + +ssh_key_path = Path.home() / ".ssh" / "id_ed25519.pub" +ssh_key = ssh_key_path.read_text().strip() + +vm = yc.ComputeInstance( + "lab4-vm", + zone="ru-central1-a", + platform_id="standard-v2", + resources=yc.ComputeInstanceResourcesArgs( + cores=2, + memory=1, + core_fraction=20, + ), + boot_disk=yc.ComputeInstanceBootDiskArgs( + initialize_params=yc.ComputeInstanceBootDiskInitializeParamsArgs( + image_id=image.id, + size=10, + ) + ), + network_interfaces=[ + yc.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, + security_group_ids=[sg.id], + ) + ], + metadata={ + "ssh-keys": f"ubuntu:{ssh_key}", + }, +) + +pulumi.export( + "public_ip", + vm.network_interfaces[0].nat_ip_address +) diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 0000000000..ded3a4e88d --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=3.0.0,<4.0.0 +pulumi-yandex \ No newline at end of file diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..1ba737a471 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,15 @@ +# Terraform +*.tfstate +*.tfstate.* +.terraform/ +terraform.tfvars +*.tfvars +.terraform.lock.hcl + +# Credentials +*.json +*.key +*.pem + +# OS +.DS_Store diff --git a/terraform/.tflint.hcl b/terraform/.tflint.hcl new file mode 100644 index 0000000000..75d15f14aa --- /dev/null +++ b/terraform/.tflint.hcl @@ -0,0 +1,3 @@ +plugin "terraform" { + enabled = true +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..61668e8f6d --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,71 @@ +resource "yandex_vpc_network" "net" { + name = "lab4-network" +} + +resource "yandex_vpc_subnet" "subnet" { + name = "lab4-subnet" + zone = var.zone + network_id = yandex_vpc_network.net.id + v4_cidr_blocks = ["10.0.0.0/24"] +} + +resource "yandex_vpc_security_group" "sg" { + name = "lab4-sg" + network_id = yandex_vpc_network.net.id + + ingress { + protocol = "TCP" + port = 22 + v4_cidr_blocks = ["192.145.30.13/32"] + } + + ingress { + protocol = "TCP" + port = 80 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "TCP" + port = 5000 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + egress { + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +data "yandex_compute_image" "ubuntu" { + family = "ubuntu-2204-lts" +} + +resource "yandex_compute_instance" "vm" { + name = "lab4-vm" + platform_id = "standard-v2" + + resources { + cores = 2 + memory = 1 + core_fraction = 20 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu.id + size = 10 + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet.id + security_group_ids = [yandex_vpc_security_group.sg.id] + nat = true + } + + metadata = { + ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key)}" + } +} + diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..22be0e815a --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,4 @@ +output "public_ip" { + description = "Public IPv4 address of the VM" + value = yandex_compute_instance.vm.network_interface[0].nat_ip_address +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000000..186a702a57 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.14.5" + + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = "~> 0.100" + } + } +} + +provider "yandex" { + zone = var.zone + folder_id = var.folder_id +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..0f7d63be42 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,22 @@ +variable "zone" { + description = "Availability zone for resources" + type = string + default = "ru-central1-a" +} + +variable "folder_id" { + description = "Yandex Cloud folder ID" + type = string +} + +variable "ssh_user" { + description = "Linux user for SSH access" + type = string + default = "ubuntu" +} + +variable "ssh_public_key" { + description = "Path to SSH public key" + type = string +} +