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..11c5028380 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,21 @@ -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__/ \ 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..3af8c6dd38 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,33 @@ +# Используем конкретную версию Python (slim — меньше размер, чем full) +FROM python:3.13-slim + +# Переменные окружения для Python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Создаём 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..31d23d7852 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,145 @@ +""" +DevOps Info Service +Main application module +""" + +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +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__) + + +def get_system_info(): + """Collect system information.""" + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.platform(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + } + + +def get_uptime(): + """Calculate application uptime.""" + 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, + } + + +@app.route("/") +def index(): + """Main endpoint - service and system information.""" + 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"}, + ], + } + + logger.info(f"Request: {request.method} {request.path}") + return jsonify(response), 200 + + +@app.route("/health") +def health(): + """Health check endpoint.""" + 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.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, + ) + + +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..78180a1ad1 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..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/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 +} +