diff --git a/cloudPractice/ansible/inventory.ini b/cloudPractice/ansible/inventory.ini new file mode 100644 index 0000000..272afc1 --- /dev/null +++ b/cloudPractice/ansible/inventory.ini @@ -0,0 +1,8 @@ +[app_servers] +vm1 ansible_host= ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa + +[db_servers] +vm2 ansible_host= ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa + +[all:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/cloudPractice/ansible/playbook.yml b/cloudPractice/ansible/playbook.yml new file mode 100644 index 0000000..86e43e2 --- /dev/null +++ b/cloudPractice/ansible/playbook.yml @@ -0,0 +1,20 @@ +--- +- name: Install Docker on all VMs + hosts: all + become: true + roles: + - docker + +- name: Deploy MySQL on DB server + hosts: db_servers + become: true + roles: + - db + +- name: Deploy Todo App on App server + hosts: app_servers + become: true + vars: + db_host: "{{ hostvars[groups['db_servers'][0]]['ansible_host'] }}" + roles: + - app diff --git a/cloudPractice/ansible/roles/app/files/docker-compose.yml b/cloudPractice/ansible/roles/app/files/docker-compose.yml new file mode 100644 index 0000000..6747e59 --- /dev/null +++ b/cloudPractice/ansible/roles/app/files/docker-compose.yml @@ -0,0 +1,12 @@ +services: + app: + build: ./app + container_name: todo_app + restart: unless-stopped + ports: + - "5000:5000" + environment: + DB_HOST: "{{ db_host }}" + DB_USER: todo_user + DB_PASSWORD: todo_pass + DB_NAME: todo_db diff --git a/cloudPractice/ansible/roles/app/tasks/main.yml b/cloudPractice/ansible/roles/app/tasks/main.yml new file mode 100644 index 0000000..9fec95f --- /dev/null +++ b/cloudPractice/ansible/roles/app/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Create app directory on App server + file: + path: /opt/todo/app/templates + state: directory + owner: ubuntu + group: ubuntu + recurse: true + +- name: Copy App docker-compose + copy: + src: docker-compose.yml + dest: /opt/todo/docker-compose.yml + owner: ubuntu + group: ubuntu + +- name: Copy application files + copy: + src: "{{ item }}" + dest: /opt/todo/app/ + owner: ubuntu + group: ubuntu + loop: + - app.py + - requirements.txt + - Dockerfile + +- name: Copy HTML template + copy: + src: index.html + dest: /opt/todo/app/templates/index.html + owner: ubuntu + group: ubuntu + +- name: Start Todo App with docker compose + community.docker.docker_compose_v2: + project_src: /opt/todo + state: present + build: always diff --git a/cloudPractice/ansible/roles/db/files/docker-compose.yml b/cloudPractice/ansible/roles/db/files/docker-compose.yml new file mode 100644 index 0000000..31314ae --- /dev/null +++ b/cloudPractice/ansible/roles/db/files/docker-compose.yml @@ -0,0 +1,18 @@ +services: + db: + image: mysql:8.0 + container_name: todo_db + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root_pass + MYSQL_DATABASE: todo_db + MYSQL_USER: todo_user + MYSQL_PASSWORD: todo_pass + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + db_data: diff --git a/cloudPractice/ansible/roles/db/files/init.sql b/cloudPractice/ansible/roles/db/files/init.sql new file mode 100644 index 0000000..2ab0946 --- /dev/null +++ b/cloudPractice/ansible/roles/db/files/init.sql @@ -0,0 +1,8 @@ +CREATE DATABASE IF NOT EXISTS todo_db; +USE todo_db; + +CREATE TABLE IF NOT EXISTS todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + done TINYINT(1) NOT NULL DEFAULT 0 +); diff --git a/cloudPractice/ansible/roles/db/tasks/main.yml b/cloudPractice/ansible/roles/db/tasks/main.yml new file mode 100644 index 0000000..a2aec61 --- /dev/null +++ b/cloudPractice/ansible/roles/db/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- name: Create app directory on DB server + file: + path: /opt/todo/db + state: directory + owner: ubuntu + group: ubuntu + +- name: Copy DB docker-compose + copy: + src: docker-compose.yml + dest: /opt/todo/docker-compose.yml + owner: ubuntu + group: ubuntu + +- name: Copy DB init script + copy: + src: init.sql + dest: /opt/todo/db/init.sql + owner: ubuntu + group: ubuntu + +- name: Start MySQL with docker compose + community.docker.docker_compose_v2: + project_src: /opt/todo + state: present diff --git a/cloudPractice/ansible/roles/docker/tasks/main.yml b/cloudPractice/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..429e749 --- /dev/null +++ b/cloudPractice/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Install required packages + apt: + name: + - ca-certificates + - curl + - gnupg + state: present + update_cache: true + +- 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 [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + +- name: Install Docker CE and docker-compose-plugin + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + state: present + update_cache: true + +- name: Start and enable Docker + systemd: + name: docker + state: started + enabled: true + +- name: Add ubuntu user to docker group + user: + name: ubuntu + groups: docker + append: true diff --git a/cloudPractice/app/Dockerfile b/cloudPractice/app/Dockerfile new file mode 100644 index 0000000..ecfdfcc --- /dev/null +++ b/cloudPractice/app/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-alpine3.19 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["python", "app.py"] diff --git a/cloudPractice/app/app.py b/cloudPractice/app/app.py new file mode 100644 index 0000000..b05a927 --- /dev/null +++ b/cloudPractice/app/app.py @@ -0,0 +1,74 @@ +import os +import time + +import mysql.connector +from flask import Flask, redirect, render_template, request + +app = Flask(__name__) + +DB_CONFIG = { + "host": os.getenv("DB_HOST", "db"), + "port": int(os.getenv("DB_PORT", "3306")), + "user": os.getenv("DB_USER", "todo_user"), + "password": os.getenv("DB_PASSWORD", "todo_pass"), + "database": os.getenv("DB_NAME", "todo_db"), +} + + +def get_db(): + for _ in range(10): + try: + return mysql.connector.connect(**DB_CONFIG) + except mysql.connector.Error: + time.sleep(3) + raise RuntimeError("Cannot connect to MySQL") + + +@app.route("/") +def index(): + db = get_db() + cursor = db.cursor() + cursor.execute("SELECT id, title, done FROM todos ORDER BY id DESC") + todos = cursor.fetchall() + cursor.close() + db.close() + return render_template("index.html", todos=todos) + + +@app.route("/add", methods=["POST"]) +def add(): + title = request.form.get("title", "").strip() + if title: + db = get_db() + cursor = db.cursor() + cursor.execute("INSERT INTO todos (title, done) VALUES (%s, 0)", (title,)) + db.commit() + cursor.close() + db.close() + return redirect("/") + + +@app.route("/toggle/") +def toggle(todo_id): + db = get_db() + cursor = db.cursor() + cursor.execute("UPDATE todos SET done = NOT done WHERE id = %s", (todo_id,)) + db.commit() + cursor.close() + db.close() + return redirect("/") + + +@app.route("/delete/") +def delete(todo_id): + db = get_db() + cursor = db.cursor() + cursor.execute("DELETE FROM todos WHERE id = %s", (todo_id,)) + db.commit() + cursor.close() + db.close() + return redirect("/") + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/cloudPractice/app/requirements.txt b/cloudPractice/app/requirements.txt new file mode 100644 index 0000000..a7564e3 --- /dev/null +++ b/cloudPractice/app/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.3 +mysql-connector-python==8.4.0 diff --git a/cloudPractice/app/templates/index.html b/cloudPractice/app/templates/index.html new file mode 100644 index 0000000..fb97d0a --- /dev/null +++ b/cloudPractice/app/templates/index.html @@ -0,0 +1,49 @@ + + + + + + Todo — Кулаков Родион + + + +
+

Список задач

+
+ + +
+ +
+ + diff --git a/cloudPractice/db/init.sql b/cloudPractice/db/init.sql new file mode 100644 index 0000000..acd3666 --- /dev/null +++ b/cloudPractice/db/init.sql @@ -0,0 +1,8 @@ +CREATE DATABASE IF NOT EXISTS todo_db; +USE todo_db; + +CREATE TABLE IF NOT EXISTS todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + done TINYINT(1) NOT NULL DEFAULT 0 +); diff --git a/cloudPractice/docker-compose.yml b/cloudPractice/docker-compose.yml new file mode 100644 index 0000000..fb98b32 --- /dev/null +++ b/cloudPractice/docker-compose.yml @@ -0,0 +1,34 @@ +services: + db: + image: mysql:8.0 + container_name: todo_db + environment: + MYSQL_ROOT_PASSWORD: root_pass + MYSQL_DATABASE: todo_db + MYSQL_USER: todo_user + MYSQL_PASSWORD: todo_pass + volumes: + - db_data:/var/lib/mysql + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-utodo_user", "-ptodo_pass"] + interval: 5s + timeout: 5s + retries: 10 + + app: + build: ./app + container_name: todo_app + ports: + - "5000:5000" + environment: + DB_HOST: db + DB_USER: todo_user + DB_PASSWORD: todo_pass + DB_NAME: todo_db + depends_on: + db: + condition: service_healthy + +volumes: + db_data: diff --git a/cloudPractice/imgs/screen1.png b/cloudPractice/imgs/screen1.png new file mode 100644 index 0000000..c697971 Binary files /dev/null and b/cloudPractice/imgs/screen1.png differ diff --git a/cloudPractice/imgs/screen2.png b/cloudPractice/imgs/screen2.png new file mode 100644 index 0000000..5442a93 Binary files /dev/null and b/cloudPractice/imgs/screen2.png differ diff --git a/cloudPractice/imgs/screen3.png b/cloudPractice/imgs/screen3.png new file mode 100644 index 0000000..f821a9a Binary files /dev/null and b/cloudPractice/imgs/screen3.png differ diff --git a/cloudPractice/report.md b/cloudPractice/report.md new file mode 100644 index 0000000..95776da --- /dev/null +++ b/cloudPractice/report.md @@ -0,0 +1,133 @@ +# Отчёт по практической работе «Облако» + +**Студент:** Кулаков Родион Андреевич +**Дата рождения:** 13.08.2003 +**Дата выполнения:** 2026-05-23 + +--- + +## Цель работы + +Развернуть веб-приложение (Todo-список) с базой данных MySQL на двух виртуальных машинах с использованием инструмента автоматизации Ansible. + +В качестве альтернативы облачным ВМ (согласовано с преподавателем) использованы два Docker-контейнера на локальной машине. + +--- + +## Архитектура решения + +``` +[VM1 — Приложение] [VM2 — База данных] + todo_app todo_db + Flask (порт 5000) <──────> MySQL (порт 3306) + Python 3.12 Alpine mysql:8.0 +``` + +### Стек технологий + +| Компонент | Технология | +|-----------|-----------| +| Веб-приложение | Python 3.12, Flask 3.0.3 | +| База данных | MySQL 8.0 | +| Контейнеризация | Docker, Docker Compose | +| Автоматизация развёртывания | Ansible (playbook + roles) | + +--- + +## Реализованные компоненты + +### 1. Веб-приложение (`app/app.py`) + +Flask-приложение с четырьмя маршрутами: +- `GET /` — вывод списка задач из MySQL +- `POST /add` — добавление новой задачи +- `POST /toggle/` — отметить задачу как выполненную / невыполненную +- `POST /delete/` — удаление задачи + +Конфигурация через переменные окружения (для переносимости между средами): + +```python +DB_CONFIG = { + "host": os.getenv("DB_HOST", "db"), + "user": os.getenv("DB_USER", "todo_user"), + "password": os.getenv("DB_PASSWORD", "todo_pass"), + "database": os.getenv("DB_NAME", "todo_db"), +} +``` + +### 2. База данных (`db/init.sql`) + +```sql +CREATE TABLE IF NOT EXISTS todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + done TINYINT(1) NOT NULL DEFAULT 0 +); +``` + +### 3. Ansible-роли для автоматического развёртывания + +Структура ролей: +- **docker** — установка Docker CE на Ubuntu 22.04 +- **db** — развёртывание MySQL-контейнера +- **app** — сборка и запуск Flask-приложения + +Сценарий запуска (`playbook.yml`): +```yaml +- name: Install Docker on all VMs + hosts: all + roles: [docker] + +- name: Deploy MySQL on DB server + hosts: db_servers + roles: [db] + +- name: Deploy Todo App on App server + hosts: app_servers + roles: [app] +``` + +--- + +## Результаты выполнения + +### Скриншот 1 — Запущенные контейнеры + +Команда `docker ps` подтверждает, что оба контейнера работают: +- `todo_db` (mysql:8.0) — статус **healthy** +- `todo_app` — статус **Up**, порт 5000 проброшен на хост + +![docker ps — оба контейнера запущены](imgs/screen1.png) + +### Скриншот 2 — Работающее веб-приложение + +Браузер открыт на `http://localhost:5000`. Приложение отображает список задач, добавленных в MySQL. Доступны кнопки «Готово» и «Удалить» для каждой задачи. + +![Todo-приложение в браузере](imgs/screen2.png) + +### Скриншот 3 — Структура Ansible-проекта + +Дерево файлов показывает организацию ролей Ansible: + +``` +ansible/ +├── inventory.ini +├── playbook.yml +└── roles/ + ├── app/ + │ ├── files/ (docker-compose.yml, app.py, Dockerfile, ...) + │ └── tasks/ (main.yml) + ├── db/ + │ ├── files/ (docker-compose.yml, init.sql) + │ └── tasks/ (main.yml) + └── docker/ + └── tasks/ (main.yml) +``` + +![Структура Ansible-ролей](imgs/screen3.png) + +--- + +## Вывод + +В ходе работы было создано и развёрнуто Todo-приложение на двух изолированных «машинах» (Docker-контейнерах). Автоматизация развёртывания реализована через Ansible с тремя ролями: установка Docker, запуск базы данных, запуск приложения. Приложение успешно подключается к MySQL и обеспечивает полный CRUD для управления задачами.