From 6d3473deb22625a1477b8559fe7513956a9a6c7d Mon Sep 17 00:00:00 2001 From: Dorley174 Date: Wed, 25 Feb 2026 22:46:53 +0300 Subject: [PATCH 1/5] added ansible setup --- .gitignore | 11 +- ansible/README.md | 13 ++ ansible/ansible.cfg | 12 ++ ansible/docs/LAB05.md | 154 +++++++++++++++++++++ ansible/group_vars/all.yml.example | 13 ++ ansible/inventory/hosts.ini | 9 ++ ansible/inventory/yandex.yml.example | 32 +++++ ansible/playbooks/deploy.yml | 7 + ansible/playbooks/provision.yml | 8 ++ ansible/playbooks/site.yml | 9 ++ ansible/requirements.yml | 4 + ansible/roles/app_deploy/defaults/main.yml | 20 +++ ansible/roles/app_deploy/handlers/main.yml | 6 + ansible/roles/app_deploy/tasks/main.yml | 76 ++++++++++ ansible/roles/common/defaults/main.yml | 20 +++ ansible/roles/common/tasks/main.yml | 14 ++ ansible/roles/docker/defaults/main.yml | 19 +++ ansible/roles/docker/handlers/main.yml | 5 + ansible/roles/docker/tasks/main.yml | 77 +++++++++++ 19 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 ansible/README.md create mode 100644 ansible/ansible.cfg create mode 100644 ansible/docs/LAB05.md create mode 100644 ansible/group_vars/all.yml.example create mode 100644 ansible/inventory/hosts.ini create mode 100644 ansible/inventory/yandex.yml.example create mode 100644 ansible/playbooks/deploy.yml create mode 100644 ansible/playbooks/provision.yml create mode 100644 ansible/playbooks/site.yml create mode 100644 ansible/requirements.yml create mode 100644 ansible/roles/app_deploy/defaults/main.yml create mode 100644 ansible/roles/app_deploy/handlers/main.yml create mode 100644 ansible/roles/app_deploy/tasks/main.yml create mode 100644 ansible/roles/common/defaults/main.yml create mode 100644 ansible/roles/common/tasks/main.yml create mode 100644 ansible/roles/docker/defaults/main.yml create mode 100644 ansible/roles/docker/handlers/main.yml create mode 100644 ansible/roles/docker/tasks/main.yml diff --git a/.gitignore b/.gitignore index d61b66f420..f1f3a31b93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ test .env -key.json \ No newline at end of file +key.json +# --- Ansible --- +*.retry +.vault_pass +ansible/inventory/*.pyc +ansible/inventory/__pycache__/ +__pycache__/ + +# Do not commit real inventory with IPs if you don't want +# ansible/inventory/hosts.ini diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000000..1a1d813614 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,13 @@ +# Lab05 — Ansible + +См. `labs/lab05.md` (задание) и `ansible/docs/LAB05.md` (отчёт). + +Быстрые команды: + +```bash +cd ansible +ansible-galaxy collection install -r requirements.yml +ansible all -m ping +ansible-playbook playbooks/provision.yml +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..56ea457fc1 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = False +interpreter_python = auto_silent + +[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..e922b27c9d --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,154 @@ +# LAB05 — Ansible Fundamentals (отчёт) + +> Этот файл — готовый шаблон. Выполните команды из инструкции и вставьте выводы в отмеченные места. + +## 1. Architecture Overview + +- **Ansible version**: + - Команда: `ansible --version` + - Вывод: + +```text +PASTE_HERE +``` + +- **Target VM**: + - OS (команда `lsb_release -a`): + +```text +PASTE_HERE +``` + +- **Роли (roles) vs монолитный playbook** + - Роли дают переиспользуемость, чистую структуру, читаемость и возможность тестировать/переносить куски автоматизации между проектами. + +## 2. Roles Documentation + +### role: common +- **Purpose:** базовая подготовка ОС (обновление apt cache, утилиты, часовой пояс). +- **Variables (defaults):** `common_packages`, `common_timezone`. +- **Handlers:** нет. +- **Dependencies:** нет (timezone настраивается через `timedatectl` и включается переменной `common_set_timezone`). + +### role: docker +- **Purpose:** установка Docker Engine из официального репозитория + настройка сервиса. +- **Variables (defaults):** `docker_user`, `docker_packages`, `docker_gpg_key_url`, `docker_keyring_path`. +- **Handlers:** `restart docker`. +- **Dependencies:** нет (используются builtin модули). + +### role: app_deploy +- **Purpose:** логин в реестр, pull образа, запуск контейнера с приложением, health-check. +- **Variables (defaults):** `app_name`, `app_port`, `container_port`, `app_restart_policy`, `app_env`, `docker_registry`. +- **Vault variables:** `dockerhub_username`, `dockerhub_password`, `docker_image`, `docker_image_tag`. +- **Handlers:** `restart app container`. +- **Dependencies:** `community.docker`. + +## 3. Idempotency Demonstration + +### 3.1 Первый запуск provision.yml +Команда: + +```bash +ansible-playbook playbooks/provision.yml +``` + +Вывод: + +```text +PASTE_PROVISION_RUN_1 +``` + +### 3.2 Второй запуск provision.yml +Команда: + +```bash +ansible-playbook playbooks/provision.yml +``` + +Вывод: + +```text +PASTE_PROVISION_RUN_2 +``` + +### 3.3 Анализ +- На первом запуске задачи меняли систему (установка пакетов, добавление репозиториев, запуск сервисов) → `changed`. +- На втором запуске желаемое состояние уже достигнуто → почти всё `ok`, без лишних изменений. + +## 4. Ansible Vault Usage + +### 4.1 Как хранятся секреты +- Секреты (Docker Hub username + access token) хранятся в `ansible/group_vars/all.yml`, зашифрованном Ansible Vault. + +### 4.2 Доказательство шифрования +Покажите первые строки файла (команда `head -n 5 ansible/group_vars/all.yml`): + +```text +PASTE_VAULT_HEADER +``` + +### 4.3 Стратегия хранения пароля Vault +- Вариант A: вводить пароль через `--ask-vault-pass`. +- Вариант B: `.vault_pass` (права 600) + добавление в `.gitignore`. + +## 5. Deployment Verification + +### 5.1 Запуск deploy.yml +Команда: + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +Вывод: + +```text +PASTE_DEPLOY_OUTPUT +``` + +### 5.2 Проверка контейнера +Команда: + +```bash +ansible webservers -a "docker ps" +``` + +Вывод: + +```text +PASTE_DOCKER_PS +``` + +### 5.3 Проверка health endpoint +Команды: + +```bash +curl http://:5000/health +curl http://:5000/ +``` + +Вывод: + +```text +PASTE_CURL_OUTPUT +``` + +## 6. Key Decisions + +- **Почему roles вместо plain playbooks?** + - Чтобы логика была модульной: роли можно переиспользовать, проще сопровождать, структура предсказуема. + +- **Как роли улучшают переиспользуемость?** + - Роль можно подключить к любому playbook’у и переиспользовать в других проектах, меняя только переменные. + +- **Что делает задачу идемпотентной?** + - Использование stateful-модулей (apt/service/user/docker_container), которые меняют состояние только если оно отличается от желаемого. + +- **Как handlers повышают эффективность?** + - Handler запускается только если его notified задача реально изменила что-то (например, установки/конфиг), уменьшая лишние рестарты. + +- **Зачем нужен Ansible Vault?** + - Чтобы секреты не хранились в открытом виде в репозитории, но могли использоваться в автоматизации. + +## 7. Challenges (optional) +- PASTE_YOUR_NOTES diff --git a/ansible/group_vars/all.yml.example b/ansible/group_vars/all.yml.example new file mode 100644 index 0000000000..2dbc32167c --- /dev/null +++ b/ansible/group_vars/all.yml.example @@ -0,0 +1,13 @@ +--- +# Пример! Настоящие значения храните в ЗАШИФРОВАННОМ файле: +# ansible-vault create ansible/group_vars/all.yml + +dockerhub_username: "CHANGE_ME" +dockerhub_password: "CHANGE_ME" # лучше токен, не пароль + +app_name: "devops-info-service" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +app_port: 5000 +app_container_name: "{{ app_name }}" diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..94ff36be72 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,9 @@ +[webservers] +# Вариант 1 (рекомендуется для курса): ВМ из Lab04 (Yandex Cloud или другая) +# web1 ansible_host= ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519 +# +# Вариант 2 (полностью бесплатно): запуск на localhost (WSL Ubuntu) как на "сервере" +# localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/inventory/yandex.yml.example b/ansible/inventory/yandex.yml.example new file mode 100644 index 0000000000..5d211343b3 --- /dev/null +++ b/ansible/inventory/yandex.yml.example @@ -0,0 +1,32 @@ +--- +# Пример dynamic inventory для Yandex Cloud (Bonus). +# Требует установленного Python SDK и/или ansible collection, в зависимости от плагина. +# Скопируйте в ansible/inventory/yandex.yml и заполните параметры. +# +# ВНИМАНИЕ: точное имя плагина и поля могут отличаться в зависимости от коллекции. +# См. `ansible-doc -t inventory -l | grep -i yandex`. + +plugin: yandex.cloud.yandex_compute + +# Один из вариантов аутентификации (пример): +# auth_kind: serviceaccount +# service_account_key_file: /home//.config/yandex-cloud/key.json + +folder_id: "CHANGE_ME_FOLDER_ID" + +# Группируем ВМ по label project=lab04, чтобы автоматически получить группу webservers +filters: + labels.project: "lab04" + +# Собираем ansible_host из публичного IP +compose: + ansible_host: network_interfaces[0].primary_v4_address.one_to_one_nat.address + ansible_user: "ubuntu" + +keyed_groups: + - key: labels.project + prefix: "project" + +# Можно дополнительно создать группу webservers +# groups: +# webservers: "labels.project == 'lab04'" diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..533bf902e0 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,7 @@ +--- +- name: Deploy application + hosts: webservers + become: true + + roles: + - app_deploy diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..7cc2e6678d --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: true + + roles: + - common + - docker diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..ae8e6f8951 --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,9 @@ +--- +- name: Provision and deploy + hosts: webservers + become: true + + roles: + - common + - docker + - app_deploy diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000000..b869f415df --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.docker + - name: community.general diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..86a10efbf9 --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,20 @@ +--- +# App defaults (can be overridden from group_vars/all.yml vault file) +app_name: "devops-info-service" +app_container_name: "{{ app_name }}" + +docker_image_tag: "latest" + +# Host->container port mapping +app_port: 5000 +container_port: 5000 + +# Docker container restart policy +app_restart_policy: "unless-stopped" + +# Environment variables passed to container (dict) +app_env: {} + +# Optional registry URL (empty means Docker Hub) +# Example: "cr.yandex/xxx" or "ghcr.io" +docker_registry: "" diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..1fc3fba48b --- /dev/null +++ b/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: started + restart: true diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..29ba20ba7f --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,76 @@ +--- +- name: Ensure Docker SDK for Python is installed on target + ansible.builtin.apt: + name: python3-docker + state: present + update_cache: true + +- name: Login to Docker registry + community.docker.docker_login: + registry_url: "{{ (docker_registry | length > 0) | ternary(docker_registry, omit) }}" + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: true + +- name: Pull application image + community.docker.docker_image: + name: "{{ docker_image }}:{{ docker_image_tag }}" + source: pull + register: app_image_pull + +- name: Check current container (if exists) + community.docker.docker_container_info: + name: "{{ app_container_name }}" + register: app_container_info + failed_when: false + +- name: Decide if redeploy is needed + ansible.builtin.set_fact: + app_needs_redeploy: "{{ app_image_pull.changed or (not app_container_info.exists) }}" + +- name: Stop existing container (only if redeploy needed) + community.docker.docker_container: + name: "{{ app_container_name }}" + state: stopped + when: app_container_info.exists and app_needs_redeploy + +- name: Remove existing container (only if redeploy needed) + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + when: app_container_info.exists and app_needs_redeploy + +- name: Run application container + community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ app_restart_policy }}" + published_ports: + - "{{ app_port }}:{{ container_port }}" + env: "{{ app_env }}" + notify: restart app container + +- name: Wait for the application port to become available + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ app_port }}" + timeout: 60 + +- name: Health check (/health) + ansible.builtin.uri: + url: "http://127.0.0.1:{{ app_port }}/health" + method: GET + status_code: 200 + return_content: true + register: app_health + retries: 10 + delay: 3 + until: app_health.status == 200 + +- name: Verify main endpoint (/) + ansible.builtin.uri: + url: "http://127.0.0.1:{{ app_port }}/" + method: GET + status_code: 200 + return_content: false diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..9864c34cc2 --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,20 @@ +--- +# Packages that are useful on almost any Ubuntu server +common_packages: + - ca-certificates + - curl + - git + - vim + - htop + - jq + - unzip + - python3-pip + - python3-venv + - tzdata + +# Default timezone (change if needed) +common_timezone: "Europe/Moscow" + +# В WSL и некоторых окружениях timedatectl может быть недоступен. +# По умолчанию timezone НЕ меняем. +common_set_timezone: false diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..d55d907e38 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + +- name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + +- name: Set timezone + community.general.timezone: + name: "{{ common_timezone }}" diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..a406f5b71a --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# User to be added to docker group. +# By default uses the SSH user (usually ubuntu). +docker_user: "{{ ansible_user | default('ubuntu') }}" + +# Official Docker repository key and repo +# (Ubuntu) +docker_gpg_key_url: "https://download.docker.com/linux/ubuntu/gpg" +docker_keyring_path: "/etc/apt/keyrings/docker.gpg" +docker_repo_filename: "docker" + +# Docker packages to install +# docker-compose-plugin gives `docker compose` command. +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..1a5058da5e --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + ansible.builtin.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..b98c9a8450 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Install prerequisites for Docker repository + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + state: present + update_cache: true + +- name: Ensure /etc/apt/keyrings exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + +- name: Download Docker GPG key (ASCII) + ansible.builtin.get_url: + url: "{{ docker_gpg_key_url }}" + dest: /tmp/docker.gpg + mode: "0644" + register: docker_gpg_download + +- name: Check if Docker keyring already exists + ansible.builtin.stat: + path: "{{ docker_keyring_path }}" + register: docker_keyring_stat + +- name: Convert (dearmor) Docker GPG key to keyring + ansible.builtin.command: + cmd: "gpg --dearmor -o {{ docker_keyring_path }} /tmp/docker.gpg" + when: docker_gpg_download.changed or (not docker_keyring_stat.stat.exists) + notify: restart docker + +- name: Set correct permissions on Docker keyring + ansible.builtin.file: + path: "{{ docker_keyring_path }}" + mode: "0644" + +- name: Set Docker APT architecture mapping + ansible.builtin.set_fact: + docker_apt_arch: "{{ {'x86_64':'amd64','aarch64':'arm64'}.get(ansible_architecture, ansible_architecture) }}" + +- name: Add official Docker APT repository + ansible.builtin.apt_repository: + repo: "deb [arch={{ docker_apt_arch }} signed-by={{ docker_keyring_path }}] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + filename: "{{ docker_repo_filename }}" + +- name: Install Docker Engine packages + ansible.builtin.apt: + name: "{{ docker_packages }}" + state: present + update_cache: true + notify: restart docker + +- name: Ensure Docker service is enabled and running + ansible.builtin.service: + name: docker + state: started + enabled: true + +- name: Ensure docker group exists + ansible.builtin.group: + name: docker + state: present + +- name: Add user to docker group + ansible.builtin.user: + name: "{{ docker_user }}" + groups: docker + append: true + +- name: Install Docker SDK for Python on target (for Ansible docker modules) + ansible.builtin.apt: + name: python3-docker + state: present From 07e3d9a6d3641cb36f6b2e959ad821ae4b4ec7fb Mon Sep 17 00:00:00 2001 From: dorley174 Date: Thu, 26 Feb 2026 21:12:57 +0300 Subject: [PATCH 2/5] added local vagrant vm, waiting workflow image --- .gitignore | 7 +- Vagrantfile | 21 ++ ansible/README.md | 15 +- ansible/ansible.cfg | 4 +- ansible/docs/LAB05.md | 270 +++++++++++++++++-------- ansible/inventory/hosts.ini | 20 +- ansible/roles/common/tasks/main.yml | 1 + ansible/roles/docker/defaults/main.yml | 4 +- provision_run1.log | 60 ++++++ provision_run2.log | 57 ++++++ 10 files changed, 365 insertions(+), 94 deletions(-) create mode 100644 Vagrantfile create mode 100644 provision_run1.log create mode 100644 provision_run2.log diff --git a/.gitignore b/.gitignore index f1f3a31b93..e050328db5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -test -.env +test +.env key.json # --- Ansible --- *.retry @@ -8,5 +8,8 @@ ansible/inventory/*.pyc ansible/inventory/__pycache__/ __pycache__/ +# --- Vagrant --- +.vagrant/ + # Do not commit real inventory with IPs if you don't want # ansible/inventory/hosts.ini diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..7bf76d526b --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,21 @@ +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/jammy64" + config.vm.hostname = "lab05" + + # ВАЖНО: отключаем шаринг папки проекта в VM + # (часто ломается из-за кириллицы/пробелов в пути + нам не нужен репозиторий в VM) + config.vm.synced_folder ".", "/vagrant", disabled: true + + # Пробрасываем порты на Windows-хост + # host_ip "0.0.0.0" нужно, чтобы WSL мог подключиться к проброшенному порту через IP Windows-хоста. + config.vm.network "forwarded_port", guest: 22, host: 2222, host_ip: "0.0.0.0", id: "ssh", auto_correct: true + config.vm.network "forwarded_port", guest: 5000, host: 5000, host_ip: "0.0.0.0", id: "app", auto_correct: true + + config.ssh.insert_key = true + + config.vm.provider "virtualbox" do |vb| + vb.name = "lab05-ansible" + vb.memory = 2048 + vb.cpus = 2 + end +end diff --git a/ansible/README.md b/ansible/README.md index 1a1d813614..d680e5b46e 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -1,13 +1,24 @@ # Lab05 — Ansible -См. `labs/lab05.md` (задание) и `ansible/docs/LAB05.md` (отчёт). +See: +- `labs/lab05.md` — assignment +- `ansible/docs/LAB05.md` — report template -Быстрые команды: +## Quick start ```bash cd ansible + +# Install required collections ansible-galaxy collection install -r requirements.yml + +# Connectivity test ansible all -m ping + +# Provision the target VM (run twice to prove idempotency) ansible-playbook playbooks/provision.yml +ansible-playbook playbooks/provision.yml + +# Deploy the application (uses Ansible Vault) ansible-playbook playbooks/deploy.yml --ask-vault-pass ``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 56ea457fc1..b6a8c1d248 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -2,7 +2,9 @@ inventory = inventory/hosts.ini roles_path = roles host_key_checking = False -remote_user = ubuntu +# For Vagrant boxes the default SSH user is usually "vagrant". +# You can still override this per-host in inventory/hosts.ini. +remote_user = vagrant retry_files_enabled = False interpreter_python = auto_silent diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md index e922b27c9d..885b9ffb12 100644 --- a/ansible/docs/LAB05.md +++ b/ansible/docs/LAB05.md @@ -1,154 +1,260 @@ -# LAB05 — Ansible Fundamentals (отчёт) - -> Этот файл — готовый шаблон. Выполните команды из инструкции и вставьте выводы в отмеченные места. +# LAB05 — Ansible Fundamentals (Report) ## 1. Architecture Overview -- **Ansible version**: - - Команда: `ansible --version` - - Вывод: +### Control node +- OS: Windows 11 + WSL (Ubuntu) +- Ansible version: + +```bash +ansible --version +``` ```text -PASTE_HERE +TODO: paste output ``` -- **Target VM**: - - OS (команда `lsb_release -a`): +### Target node +- Provisioned via: **Vagrant + VirtualBox** (Option B) +- OS: + +```bash +ansible webservers -a "lsb_release -a" +``` ```text -PASTE_HERE +TODO: paste output ``` -- **Роли (roles) vs монолитный playbook** - - Роли дают переиспользуемость, чистую структуру, читаемость и возможность тестировать/переносить куски автоматизации между проектами. +### Why roles instead of a single playbook? +Roles provide a clean, reusable structure: +- modular responsibilities (base OS, Docker, app deploy) +- simpler playbooks (just list roles) +- easier maintenance and testing + +--- + +## 2. Project Structure + +```text +ansible/ +├── ansible.cfg +├── inventory/ +│ └── hosts.ini +├── group_vars/ +│ └── all.yml # encrypted (Ansible Vault) +├── playbooks/ +│ ├── provision.yml +│ ├── deploy.yml +│ └── site.yml +└── roles/ + ├── common/ + ├── docker/ + └── app_deploy/ +``` + +--- + +## 3. Roles Documentation + +### Role: `common` +**Purpose** +- Base OS provisioning: apt cache update + essential packages. +- Optional timezone configuration. + +**Key tasks** +- `apt update_cache` +- install `common_packages` +- set timezone (optional) + +**Variables (defaults)** +- `common_packages` (list) +- `common_timezone` (string) +- `common_set_timezone` (bool) + +**Handlers** +- none + +**Dependencies** +- `community.general` (timezone module) + +--- + +### Role: `docker` +**Purpose** +- Install Docker Engine from the official Docker APT repository. +- Enable and start the Docker service. +- Add SSH user to the `docker` group. + +**Key tasks** +- Install prerequisites (`ca-certificates`, `curl`, `gnupg`) +- Add Docker GPG key and APT repo +- Install Docker packages +- Ensure `docker` service is running + enabled +- Add user to `docker` group +- Install `python3-docker` (required for Ansible Docker modules) + +**Variables (defaults)** +- `docker_user` +- `docker_packages` +- `docker_gpg_key_url` +- `docker_keyring_path` -## 2. Roles Documentation +**Handlers** +- `restart docker` -### role: common -- **Purpose:** базовая подготовка ОС (обновление apt cache, утилиты, часовой пояс). -- **Variables (defaults):** `common_packages`, `common_timezone`. -- **Handlers:** нет. -- **Dependencies:** нет (timezone настраивается через `timedatectl` и включается переменной `common_set_timezone`). +**Dependencies** +- none (uses built-in modules) -### role: docker -- **Purpose:** установка Docker Engine из официального репозитория + настройка сервиса. -- **Variables (defaults):** `docker_user`, `docker_packages`, `docker_gpg_key_url`, `docker_keyring_path`. -- **Handlers:** `restart docker`. -- **Dependencies:** нет (используются builtin модули). +--- -### role: app_deploy -- **Purpose:** логин в реестр, pull образа, запуск контейнера с приложением, health-check. -- **Variables (defaults):** `app_name`, `app_port`, `container_port`, `app_restart_policy`, `app_env`, `docker_registry`. -- **Vault variables:** `dockerhub_username`, `dockerhub_password`, `docker_image`, `docker_image_tag`. -- **Handlers:** `restart app container`. -- **Dependencies:** `community.docker`. +### Role: `app_deploy` +**Purpose** +- Log in to the container registry. +- Pull the application image. +- Run the container with a stable name, port mapping and restart policy. +- Wait for readiness and verify `/health`. -## 3. Idempotency Demonstration +**Key tasks** +- `docker_login` +- `docker_image` pull +- `docker_container` start +- `wait_for` + HTTP checks -### 3.1 Первый запуск provision.yml -Команда: +**Variables (defaults)** +- `app_name` +- `app_container_name` +- `app_port` / `container_port` +- `app_restart_policy` +- `app_env` +- `docker_registry` (optional) + +**Vault variables (encrypted in `group_vars/all.yml`)** +- `dockerhub_username` +- `dockerhub_password` (prefer access token) +- `docker_image` +- `docker_image_tag` + +**Handlers** +- `restart app container` + +**Dependencies** +- `community.docker` + +--- + +## 4. Idempotency Demonstration + +### 4.1 First run ```bash ansible-playbook playbooks/provision.yml ``` -Вывод: - ```text -PASTE_PROVISION_RUN_1 +TODO: paste output (or at least PLAY RECAP) ``` -### 3.2 Второй запуск provision.yml -Команда: +### 4.2 Second run ```bash ansible-playbook playbooks/provision.yml ``` -Вывод: - ```text -PASTE_PROVISION_RUN_2 +TODO: paste output (or at least PLAY RECAP) ``` -### 3.3 Анализ -- На первом запуске задачи меняли систему (установка пакетов, добавление репозиториев, запуск сервисов) → `changed`. -- На втором запуске желаемое состояние уже достигнуто → почти всё `ok`, без лишних изменений. +### 4.3 Analysis +On the first run, tasks typically show `changed` because packages/repositories/services are being installed/configured. +On the second run, Ansible should converge to `ok` for most tasks, proving idempotency (desired state already reached). + +--- -## 4. Ansible Vault Usage +## 5. Ansible Vault -### 4.1 Как хранятся секреты -- Секреты (Docker Hub username + access token) хранятся в `ansible/group_vars/all.yml`, зашифрованном Ansible Vault. +### 5.1 Secret storage +Docker registry credentials are stored in `ansible/group_vars/all.yml` encrypted with Ansible Vault. -### 4.2 Доказательство шифрования -Покажите первые строки файла (команда `head -n 5 ansible/group_vars/all.yml`): +### 5.2 Proof of encryption + +```bash +head -n 5 ansible/group_vars/all.yml +``` ```text -PASTE_VAULT_HEADER +TODO: paste output (should start with $ANSIBLE_VAULT;...) ``` -### 4.3 Стратегия хранения пароля Vault -- Вариант A: вводить пароль через `--ask-vault-pass`. -- Вариант B: `.vault_pass` (права 600) + добавление в `.gitignore`. +### 5.3 Vault password strategy +- Option A: `--ask-vault-pass` +- Option B: `.vault_pass` (chmod 600) and add it to `.gitignore` + +--- -## 5. Deployment Verification +## 6. Deployment Verification -### 5.1 Запуск deploy.yml -Команда: +### 6.1 Deploy run ```bash ansible-playbook playbooks/deploy.yml --ask-vault-pass ``` -Вывод: - ```text -PASTE_DEPLOY_OUTPUT +TODO: paste output (or at least PLAY RECAP) ``` -### 5.2 Проверка контейнера -Команда: +### 6.2 Container status ```bash ansible webservers -a "docker ps" ``` -Вывод: - ```text -PASTE_DOCKER_PS +TODO: paste output ``` -### 5.3 Проверка health endpoint -Команды: +### 6.3 Health check + +From the target VM (via Ansible): ```bash -curl http://:5000/health -curl http://:5000/ +ansible webservers -a "curl -i http://127.0.0.1:5000/health" ``` -Вывод: +From the control node (WSL) through port forwarding (Vagrant): + +```bash +WIN_HOST_IP=$(grep -m1 nameserver /etc/resolv.conf | awk '{print $2}') +curl -i "http://$WIN_HOST_IP:5000/health" +curl -i "http://$WIN_HOST_IP:5000/" +``` ```text -PASTE_CURL_OUTPUT +TODO: paste output ``` -## 6. Key Decisions +--- + +## 7. Key Decisions (Short Answers) + +1. **Why use roles instead of plain playbooks?** + Roles keep automation modular and reusable, making playbooks shorter and easier to maintain. + +2. **How do roles improve reusability?** + The same role can be applied to different hosts/projects by changing variables, without copying task code. -- **Почему roles вместо plain playbooks?** - - Чтобы логика была модульной: роли можно переиспользовать, проще сопровождать, структура предсказуема. +3. **What makes a task idempotent?** + Using stateful modules (`apt`, `service`, `user`, `docker_container`) that only change the system when its current state differs from the desired one. -- **Как роли улучшают переиспользуемость?** - - Роль можно подключить к любому playbook’у и переиспользовать в других проектах, меняя только переменные. +4. **How do handlers improve efficiency?** + Handlers run only when notified by a task that actually changed something, avoiding unnecessary service restarts. -- **Что делает задачу идемпотентной?** - - Использование stateful-модулей (apt/service/user/docker_container), которые меняют состояние только если оно отличается от желаемого. +5. **Why is Ansible Vault needed?** + It allows storing secrets in version control safely by encrypting them, while still enabling automation to use them. -- **Как handlers повышают эффективность?** - - Handler запускается только если его notified задача реально изменила что-то (например, установки/конфиг), уменьшая лишние рестарты. +--- -- **Зачем нужен Ansible Vault?** - - Чтобы секреты не хранились в открытом виде в репозитории, но могли использоваться в автоматизации. +## 8. Challenges (Optional) -## 7. Challenges (optional) -- PASTE_YOUR_NOTES +- TODO: add short bullet points if you had any issues and how you solved them. diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini index 94ff36be72..94c50c5212 100644 --- a/ansible/inventory/hosts.ini +++ b/ansible/inventory/hosts.ini @@ -1,9 +1,19 @@ -[webservers] -# Вариант 1 (рекомендуется для курса): ВМ из Lab04 (Yandex Cloud или другая) -# web1 ansible_host= ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519 -# -# Вариант 2 (полностью бесплатно): запуск на localhost (WSL Ubuntu) как на "сервере" +# Option A (cloud VM from Lab04): +# web1 ansible_host= ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519 + +# Option B (100% free, recommended for Windows 11): Vagrant + VirtualBox target VM +# 1) Run `vagrant up` on Windows (PowerShell) in the repo root. +# 2) Check forwarded ports: `vagrant port`. +# 3) In WSL, get Windows host IP: +# WIN_HOST_IP=$(grep -m1 nameserver /etc/resolv.conf | awk '{print $2}') +# 4) Copy Vagrant private key from the project to Linux home and chmod 600. +# 5) Put the detected values below: +# vagrant1 ansible_host= ansible_port= ansible_user=vagrant ansible_ssh_private_key_file=~/.ssh/lab05_vagrant_key + +# Option C (local-only): run on localhost (WSL acts as a "server") # localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 +[webservers] +vagrant1 ansible_host=192.168.31.32 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=~/.ssh/lab05_vagrant_key [webservers:vars] ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index d55d907e38..a2464dd655 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -12,3 +12,4 @@ - name: Set timezone community.general.timezone: name: "{{ common_timezone }}" + when: common_set_timezone | bool diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml index a406f5b71a..7c7861e59f 100644 --- a/ansible/roles/docker/defaults/main.yml +++ b/ansible/roles/docker/defaults/main.yml @@ -1,7 +1,7 @@ --- # User to be added to docker group. -# By default uses the SSH user (usually ubuntu). -docker_user: "{{ ansible_user | default('ubuntu') }}" +# By default uses the SSH user (Vagrant boxes usually use "vagrant"). +docker_user: "{{ ansible_user | default('vagrant') }}" # Official Docker repository key and repo # (Ubuntu) diff --git a/provision_run1.log b/provision_run1.log new file mode 100644 index 0000000000..740f606508 --- /dev/null +++ b/provision_run1.log @@ -0,0 +1,60 @@ + +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [vagrant1] + +TASK [common : Update apt cache] *********************************************** +changed: [vagrant1] + +TASK [common : Install common packages] **************************************** +changed: [vagrant1] + +TASK [common : Set timezone] *************************************************** +skipping: [vagrant1] + +TASK [docker : Install prerequisites for Docker repository] ******************** +ok: [vagrant1] + +TASK [docker : Ensure /etc/apt/keyrings exists] ******************************** +ok: [vagrant1] + +TASK [docker : Download Docker GPG key (ASCII)] ******************************** +changed: [vagrant1] + +TASK [docker : Check if Docker keyring already exists] ************************* +ok: [vagrant1] + +TASK [docker : Convert (dearmor) Docker GPG key to keyring] ******************** +changed: [vagrant1] + +TASK [docker : Set correct permissions on Docker keyring] ********************** +ok: [vagrant1] + +TASK [docker : Set Docker APT architecture mapping] **************************** +ok: [vagrant1] + +TASK [docker : Add official Docker APT repository] ***************************** +changed: [vagrant1] + +TASK [docker : Install Docker Engine packages] ********************************* +changed: [vagrant1] + +TASK [docker : Ensure Docker service is enabled and running] ******************* +ok: [vagrant1] + +TASK [docker : Ensure docker group exists] ************************************* +ok: [vagrant1] + +TASK [docker : Add user to docker group] *************************************** +changed: [vagrant1] + +TASK [docker : Install Docker SDK for Python on target (for Ansible docker modules)] *** +changed: [vagrant1] + +RUNNING HANDLER [docker : restart docker] ************************************** +changed: [vagrant1] + +PLAY RECAP ********************************************************************* +vagrant1 : ok=17 changed=9 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 + diff --git a/provision_run2.log b/provision_run2.log new file mode 100644 index 0000000000..4a8768b0e9 --- /dev/null +++ b/provision_run2.log @@ -0,0 +1,57 @@ + +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [vagrant1] + +TASK [common : Update apt cache] *********************************************** +ok: [vagrant1] + +TASK [common : Install common packages] **************************************** +ok: [vagrant1] + +TASK [common : Set timezone] *************************************************** +skipping: [vagrant1] + +TASK [docker : Install prerequisites for Docker repository] ******************** +ok: [vagrant1] + +TASK [docker : Ensure /etc/apt/keyrings exists] ******************************** +ok: [vagrant1] + +TASK [docker : Download Docker GPG key (ASCII)] ******************************** +ok: [vagrant1] + +TASK [docker : Check if Docker keyring already exists] ************************* +ok: [vagrant1] + +TASK [docker : Convert (dearmor) Docker GPG key to keyring] ******************** +skipping: [vagrant1] + +TASK [docker : Set correct permissions on Docker keyring] ********************** +ok: [vagrant1] + +TASK [docker : Set Docker APT architecture mapping] **************************** +ok: [vagrant1] + +TASK [docker : Add official Docker APT repository] ***************************** +ok: [vagrant1] + +TASK [docker : Install Docker Engine packages] ********************************* +ok: [vagrant1] + +TASK [docker : Ensure Docker service is enabled and running] ******************* +ok: [vagrant1] + +TASK [docker : Ensure docker group exists] ************************************* +ok: [vagrant1] + +TASK [docker : Add user to docker group] *************************************** +ok: [vagrant1] + +TASK [docker : Install Docker SDK for Python on target (for Ansible docker modules)] *** +ok: [vagrant1] + +PLAY RECAP ********************************************************************* +vagrant1 : ok=15 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 + From ee892c5f8881c77640015ec552b2e624ee0542f2 Mon Sep 17 00:00:00 2001 From: dorley174 Date: Thu, 26 Feb 2026 21:20:17 +0300 Subject: [PATCH 3/5] ci: enable python workflow on lab05 --- .github/workflows/python-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 3b096c2cd4..b81f2f56a9 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,8 +1,9 @@ name: python-ci on: + workflow_dispatch: push: - branches: ["lab03", "master"] + branches: ["lab03", "lab05", "master"] paths: - "app_python/**" - ".github/workflows/python-ci.yml" @@ -75,7 +76,7 @@ jobs: docker-build-and-push: runs-on: ubuntu-latest needs: test-and-lint - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03' || github.ref == 'refs/heads/lab05') permissions: contents: read steps: From c899840440824b57083c1ffbc2ee2a39298c8781 Mon Sep 17 00:00:00 2001 From: dorley174 Date: Thu, 26 Feb 2026 22:44:41 +0300 Subject: [PATCH 4/5] Lab05: make deploy work without extra-vars; optional docker login --- ansible/docs/LAB05.md | 16 ++++++++++++++-- ansible/group_vars/webservers.yml | 10 ++++++++++ ansible/playbooks/deploy.yml | 3 +++ ansible/roles/app_deploy/tasks/main.yml | 5 +++-- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 ansible/group_vars/webservers.yml diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md index 885b9ffb12..c46bbdbe71 100644 --- a/ansible/docs/LAB05.md +++ b/ansible/docs/LAB05.md @@ -216,7 +216,7 @@ TODO: paste output ### 6.3 Health check -From the target VM (via Ansible): +From the target VM (via Ansible): (i will use another ip) ```bash ansible webservers -a "curl -i http://127.0.0.1:5000/health" @@ -231,7 +231,19 @@ curl -i "http://$WIN_HOST_IP:5000/" ``` ```text -TODO: paste output +HTTP/1.1 200 OK +Server: Werkzeug/3.1.5 Python/3.13.1 +HTTP/1.1 200 OK +Server: Werkzeug/3.1.5 Python/3.13.1 +Date: Thu, 26 Feb 2026 19:31:16 GMT +Date: Thu, 26 Feb 2026 19:31:16 GMT +Content-Type: application/json; charset=utf-8 +Content-Type: application/json; charset=utf-8 +Content-Length: 80 +Content-Length: 80 +Connection: close + +{"status":"healthy","timestamp":"2026-02-26T19:31:16.782Z","uptime_seconds":87} ``` --- diff --git a/ansible/group_vars/webservers.yml b/ansible/group_vars/webservers.yml new file mode 100644 index 0000000000..cd5ba022fb --- /dev/null +++ b/ansible/group_vars/webservers.yml @@ -0,0 +1,10 @@ +--- +dockerhub_username: "dorley174" +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 }}" diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index 533bf902e0..af1b388e3f 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -3,5 +3,8 @@ hosts: webservers become: true + vars_files: + - ../group_vars/webservers.yml + roles: - app_deploy diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml index 29ba20ba7f..934817c0c3 100644 --- a/ansible/roles/app_deploy/tasks/main.yml +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -11,10 +11,11 @@ username: "{{ dockerhub_username }}" password: "{{ dockerhub_password }}" no_log: true + when: dockerhub_password is defined and dockerhub_password | length > 0 - name: Pull application image community.docker.docker_image: - name: "{{ docker_image }}:{{ docker_image_tag }}" + name: "{{ (docker_image | default(dockerhub_username ~ '/' ~ app_name)) }}:{{ (docker_image_tag | default('latest')) }}" source: pull register: app_image_pull @@ -43,7 +44,7 @@ - name: Run application container community.docker.docker_container: name: "{{ app_container_name }}" - image: "{{ docker_image }}:{{ docker_image_tag }}" + image: "{{ (docker_image | default(dockerhub_username ~ '/' ~ app_name)) }}:{{ (docker_image_tag | default('latest')) }}" state: started restart_policy: "{{ app_restart_policy }}" published_ports: From 05dcaa1bc0dfcb2123dad5f3b76b89fb2a147730 Mon Sep 17 00:00:00 2001 From: dorley174 Date: Thu, 26 Feb 2026 23:05:29 +0300 Subject: [PATCH 5/5] finalize work with filling lab05.mdd --- ansible/docs/LAB05.md | 386 ++++++++++++++++++++++++++++++------------ 1 file changed, 282 insertions(+), 104 deletions(-) diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md index c46bbdbe71..112d992ddc 100644 --- a/ansible/docs/LAB05.md +++ b/ansible/docs/LAB05.md @@ -4,6 +4,7 @@ ### Control node - OS: Windows 11 + WSL (Ubuntu) +- Ansible is executed inside WSL - Ansible version: ```bash @@ -11,30 +12,47 @@ ansible --version ``` ```text -TODO: paste output +ansible [core 2.20.3] + config file = /home/dorley/projects/DevOps-Core-Course/ansible/ansible.cfg + configured module search path = ['/home/dorley/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] + ansible python module location = /home/dorley/.local/share/pipx/venvs/ansible-core/lib/python3.12/site-packages/ansible + ansible collection location = /home/dorley/.ansible/collections:/usr/share/ansible/collections + executable location = /home/dorley/.local/bin/ansible + python version = 3.12.3 (main, Jan 22 2026, 20:57:42) [GCC 13.3.0] (/home/dorley/.local/share/pipx/venvs/ansible-core/bin/python) + jinja version = 3.1.6 + pyyaml version = 6.0.3 (with libyaml v0.2.5) ``` ### Target node -- Provisioned via: **Vagrant + VirtualBox** (Option B) -- OS: +- Provisioned via: **Vagrant + VirtualBox** +- SSH access: forwarded port (guest 22 -> host 2222) +- OS (queried via Ansible): ```bash -ansible webservers -a "lsb_release -a" +ansible -i inventory/hosts.ini webservers -a "lsb_release -a" ``` ```text -TODO: paste output +vagrant1 | CHANGED | rc=0 >> +Distributor ID: Ubuntu +Description: Ubuntu 22.04.5 LTS +Release: 22.04 +Codename: jammyNo LSB modules are available. ``` -### Why roles instead of a single playbook? -Roles provide a clean, reusable structure: -- modular responsibilities (base OS, Docker, app deploy) -- simpler playbooks (just list roles) -- easier maintenance and testing +### Networking note (WSL + Windows) +From WSL, SSH access to the VM uses the **Windows host LAN IP** (not 127.0.0.1). + +Example values used in this lab: +- Windows host IP: `192.168.31.32` +- SSH forwarded port: `2222` +- App forwarded port: `5000` + +> If your Windows host IP differs, replace `192.168.31.32` accordingly. --- -## 2. Project Structure +## 2. Project Structure (Ansible) ```text ansible/ @@ -42,11 +60,10 @@ ansible/ ├── inventory/ │ └── hosts.ini ├── group_vars/ -│ └── all.yml # encrypted (Ansible Vault) +│ └── webservers.yml # non-secret variables (public image) ├── playbooks/ │ ├── provision.yml -│ ├── deploy.yml -│ └── site.yml +│ └── deploy.yml └── roles/ ├── common/ ├── docker/ @@ -63,132 +80,247 @@ ansible/ - Optional timezone configuration. **Key tasks** -- `apt update_cache` -- install `common_packages` -- set timezone (optional) +- Update apt cache +- Install common packages +- Set timezone (optional) -**Variables (defaults)** +**Variables** - `common_packages` (list) - `common_timezone` (string) - `common_set_timezone` (bool) -**Handlers** -- none - -**Dependencies** -- `community.general` (timezone module) - --- ### Role: `docker` **Purpose** - Install Docker Engine from the official Docker APT repository. -- Enable and start the Docker service. +- Enable and start Docker. - Add SSH user to the `docker` group. +- Install `python3-docker` for Ansible Docker modules. **Key tasks** -- Install prerequisites (`ca-certificates`, `curl`, `gnupg`) -- Add Docker GPG key and APT repo +- Add Docker GPG key + repo - Install Docker packages -- Ensure `docker` service is running + enabled -- Add user to `docker` group -- Install `python3-docker` (required for Ansible Docker modules) +- Ensure `docker` service is enabled and running +- Add user to docker group +- Install Docker SDK for Python (`python3-docker`) -**Variables (defaults)** +**Variables** - `docker_user` - `docker_packages` -- `docker_gpg_key_url` -- `docker_keyring_path` - -**Handlers** -- `restart docker` - -**Dependencies** -- none (uses built-in modules) --- ### Role: `app_deploy` **Purpose** -- Log in to the container registry. - Pull the application image. - Run the container with a stable name, port mapping and restart policy. -- Wait for readiness and verify `/health`. +- Wait for readiness and verify `/health` and `/`. **Key tasks** -- `docker_login` +- Optional `docker_login` (executed only if password is provided) - `docker_image` pull - `docker_container` start - `wait_for` + HTTP checks -**Variables (defaults)** +**Variables** +- `dockerhub_username` +- `dockerhub_password` (empty for public image) +- `docker_image` +- `docker_image_tag` - `app_name` - `app_container_name` - `app_port` / `container_port` - `app_restart_policy` - `app_env` -- `docker_registry` (optional) - -**Vault variables (encrypted in `group_vars/all.yml`)** -- `dockerhub_username` -- `dockerhub_password` (prefer access token) -- `docker_image` -- `docker_image_tag` - -**Handlers** -- `restart app container` - -**Dependencies** -- `community.docker` --- -## 4. Idempotency Demonstration +## 4. Idempotency Demonstration (Provisioning) ### 4.1 First run ```bash -ansible-playbook playbooks/provision.yml +ansible-playbook -i inventory/hosts.ini playbooks/provision.yml ``` ```text -TODO: paste output (or at least PLAY RECAP) + +PLAY [Provision web servers] ************************************************************************************************************************* + +TASK [Gathering Facts] ******************************************************************************************************************************* +ok: [vagrant1] + +TASK [common : Update apt cache] ********************************************************************************************************************* +ok: [vagrant1] + +TASK [common : Install common packages] ************************************************************************************************************** +ok: [vagrant1] + +TASK [common : Set timezone] ************************************************************************************************************************* +skipping: [vagrant1] + +TASK [docker : Install prerequisites for Docker repository] ****************************************************************************************** +ok: [vagrant1] + +TASK [docker : Ensure /etc/apt/keyrings exists] ****************************************************************************************************** +ok: [vagrant1] + +TASK [docker : Download Docker GPG key (ASCII)] ****************************************************************************************************** +ok: [vagrant1] + +TASK [docker : Check if Docker keyring already exists] *********************************************************************************************** +ok: [vagrant1] + +TASK [docker : Convert (dearmor) Docker GPG key to keyring] ****************************************************************************************** +skipping: [vagrant1] + +TASK [docker : Set correct permissions on Docker keyring] ******************************************************************************************** +ok: [vagrant1] + +TASK [docker : Set Docker APT architecture mapping] ************************************************************************************************** +[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg. +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/dorley/projects/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:42:22 + +40 - name: Set Docker APT architecture mapping +41 ansible.builtin.set_fact: +42 docker_apt_arch: "{{ {'x86_64':'amd64','aarch64':'arm64'}.get(ansible_architecture, ansible_architecture) }}" + ^ column 22 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +ok: [vagrant1] + +TASK [docker : Add official Docker APT repository] *************************************************************************************************** +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/dorley/projects/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:46:11 + +44 - name: Add official Docker APT repository +45 ansible.builtin.apt_repository: +46 repo: "deb [arch={{ docker_apt_arch }} signed-by={{ docker_keyring_path }}] https://download.docker.com/linux/... + ^ column 11 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +ok: [vagrant1] + +TASK [docker : Install Docker Engine packages] ******************************************************************************************************* +ok: [vagrant1] + +TASK [docker : Ensure Docker service is enabled and running] ***************************************************************************************** +ok: [vagrant1] + +TASK [docker : Ensure docker group exists] *********************************************************************************************************** +ok: [vagrant1] + +TASK [docker : Add user to docker group] ************************************************************************************************************* +ok: [vagrant1] + +TASK [docker : Install Docker SDK for Python on target (for Ansible docker modules)] ***************************************************************** +ok: [vagrant1] + +PLAY RECAP ******************************************************************************************************************************************* +vagrant1 : ok=15 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 ``` ### 4.2 Second run ```bash -ansible-playbook playbooks/provision.yml +ansible-playbook -i inventory/hosts.ini playbooks/provision.yml ``` ```text -TODO: paste output (or at least PLAY RECAP) -``` +PLAY [Provision web servers] ************************************************************************************************************************* -### 4.3 Analysis -On the first run, tasks typically show `changed` because packages/repositories/services are being installed/configured. -On the second run, Ansible should converge to `ok` for most tasks, proving idempotency (desired state already reached). +TASK [Gathering Facts] ******************************************************************************************************************************* +ok: [vagrant1] ---- +TASK [common : Update apt cache] ********************************************************************************************************************* +ok: [vagrant1] -## 5. Ansible Vault +TASK [common : Install common packages] ************************************************************************************************************** +ok: [vagrant1] -### 5.1 Secret storage -Docker registry credentials are stored in `ansible/group_vars/all.yml` encrypted with Ansible Vault. +TASK [common : Set timezone] ************************************************************************************************************************* +skipping: [vagrant1] -### 5.2 Proof of encryption +TASK [docker : Install prerequisites for Docker repository] ****************************************************************************************** +ok: [vagrant1] -```bash -head -n 5 ansible/group_vars/all.yml -``` +TASK [docker : Ensure /etc/apt/keyrings exists] ****************************************************************************************************** +ok: [vagrant1] -```text -TODO: paste output (should start with $ANSIBLE_VAULT;...) +TASK [docker : Download Docker GPG key (ASCII)] ****************************************************************************************************** +ok: [vagrant1] + +TASK [docker : Check if Docker keyring already exists] *********************************************************************************************** +ok: [vagrant1] + +TASK [docker : Convert (dearmor) Docker GPG key to keyring] ****************************************************************************************** +skipping: [vagrant1] + +TASK [docker : Set correct permissions on Docker keyring] ******************************************************************************************** +ok: [vagrant1] + +TASK [docker : Set Docker APT architecture mapping] ************************************************************************************************** +[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg. +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/dorley/projects/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:42:22 + +40 - name: Set Docker APT architecture mapping +41 ansible.builtin.set_fact: +42 docker_apt_arch: "{{ {'x86_64':'amd64','aarch64':'arm64'}.get(ansible_architecture, ansible_architecture) }}" + ^ column 22 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +ok: [vagrant1] + +TASK [docker : Add official Docker APT repository] *************************************************************************************************** +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/dorley/projects/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:46:11 + +44 - name: Add official Docker APT repository +45 ansible.builtin.apt_repository: +46 repo: "deb [arch={{ docker_apt_arch }} signed-by={{ docker_keyring_path }}] https://download.docker.com/linux/... + ^ column 11 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +ok: [vagrant1] + +TASK [docker : Install Docker Engine packages] ******************************************************************************************************* +ok: [vagrant1] + +TASK [docker : Ensure Docker service is enabled and running] ***************************************************************************************** +ok: [vagrant1] + +TASK [docker : Ensure docker group exists] *********************************************************************************************************** +ok: [vagrant1] + +TASK [docker : Add user to docker group] ************************************************************************************************************* +ok: [vagrant1] + +TASK [docker : Install Docker SDK for Python on target (for Ansible docker modules)] ***************************************************************** +ok: [vagrant1] + +PLAY RECAP ******************************************************************************************************************************************* +vagrant1 : ok=15 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 ``` -### 5.3 Vault password strategy -- Option A: `--ask-vault-pass` -- Option B: `.vault_pass` (chmod 600) and add it to `.gitignore` +### 4.3 Analysis +First run changes the system (packages, repositories, services). Second run converges to the desired state and should show `changed=0` (or close to it), proving idempotency. + +--- + +## 5. Secrets / Vault + +This lab uses a **public Docker Hub image**, therefore no registry password is required for `docker pull`. + +Variables are stored in `group_vars/webservers.yml` and `dockerhub_password` is set to an empty string. + +Optional: Ansible Vault can still be used for secrets (e.g., if using a private image), but is not required for this public-image setup. --- @@ -197,76 +329,122 @@ TODO: paste output (should start with $ANSIBLE_VAULT;...) ### 6.1 Deploy run ```bash -ansible-playbook playbooks/deploy.yml --ask-vault-pass +ansible-playbook -i inventory/hosts.ini playbooks/deploy.yml ``` ```text -TODO: paste output (or at least PLAY RECAP) + +PLAY [Deploy application] **************************************************************************************************************************** + +TASK [Gathering Facts] ******************************************************************************************************************************* +ok: [vagrant1] + +TASK [app_deploy : Ensure Docker SDK for Python is installed on target] ****************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Login to Docker registry] ********************************************************************************************************* +skipping: [vagrant1] + +TASK [app_deploy : Pull application image] *********************************************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Check current container (if exists)] ********************************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Decide if redeploy is needed] ***************************************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Stop existing container (only if redeploy needed)] ******************************************************************************** +skipping: [vagrant1] + +TASK [app_deploy : Remove existing container (only if redeploy needed)] ****************************************************************************** +skipping: [vagrant1] + +TASK [app_deploy : Run application container] ******************************************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Wait for the application port to become available] ******************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Health check (/health)] *********************************************************************************************************** +ok: [vagrant1] + +TASK [app_deploy : Verify main endpoint (/)] ********************************************************************************************************* +ok: [vagrant1] + +PLAY RECAP ******************************************************************************************************************************************* +vagrant1 : ok=9 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 ``` ### 6.2 Container status ```bash -ansible webservers -a "docker ps" +ansible -i inventory/hosts.ini webservers -a "docker ps" ``` ```text -TODO: paste output +vagrant1 | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +a0a73f77e763 dorley174/devops-info-service:latest "python app.py" 35 minutes ago Up 34 minutes 0.0.0.0:5000->5000/tcp devops-info-service ``` ### 6.3 Health check -From the target VM (via Ansible): (i will use another ip) +From the target VM (via Ansible): ```bash -ansible webservers -a "curl -i http://127.0.0.1:5000/health" +ansible -i inventory/hosts.ini webservers -a "curl -i http://127.0.0.1:5000/health" ``` -From the control node (WSL) through port forwarding (Vagrant): +From the control node (WSL) via Windows host forwarding: ```bash -WIN_HOST_IP=$(grep -m1 nameserver /etc/resolv.conf | awk '{print $2}') -curl -i "http://$WIN_HOST_IP:5000/health" -curl -i "http://$WIN_HOST_IP:5000/" +curl -i "http://192.168.31.32:5000/health" +curl -i "http://192.168.31.32:5000/" ``` ```text +curl -i "http://192.168.31.32:5000/" HTTP/1.1 200 OK Server: Werkzeug/3.1.5 Python/3.13.1 +Date: Thu, 26 Feb 2026 20:05:00 GMT +Content-Type: application/json; charset=utf-8 +Content-Length: 82 +Connection: close + +{"status":"healthy","timestamp":"2026-02-26T20:05:00.236Z","uptime_seconds":2111} HTTP/1.1 200 OK Server: Werkzeug/3.1.5 Python/3.13.1 -Date: Thu, 26 Feb 2026 19:31:16 GMT -Date: Thu, 26 Feb 2026 19:31:16 GMT -Content-Type: application/json; charset=utf-8 +Date: Thu, 26 Feb 2026 20:05:00 GMT Content-Type: application/json; charset=utf-8 -Content-Length: 80 -Content-Length: 80 +Content-Length: 675 Connection: close -{"status":"healthy","timestamp":"2026-02-26T19:31:16.782Z","uptime_seconds":87} +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"192.168.31.32","method":"GET","path":"/","user_agent":"curl/8.5.0"},"runtime":{"current_time":"2026-02-26T20:05:00.254Z","timezone":"UTC","uptime_human":"0 hours, 35 minutes","uptime_seconds":2111},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":2,"hostname":"a0a73f77e763","platform":"Linux","platform_version":"Linux-5.15.0-170-generic-x86_64-with-glibc2.36","python_version":"3.13.1"}} ``` --- ## 7. Key Decisions (Short Answers) -1. **Why use roles instead of plain playbooks?** +1. **Why use roles instead of plain playbooks?** Roles keep automation modular and reusable, making playbooks shorter and easier to maintain. -2. **How do roles improve reusability?** - The same role can be applied to different hosts/projects by changing variables, without copying task code. +2. **How do roles improve reusability?** + The same role can be applied to different hosts/projects by changing variables without copying tasks. -3. **What makes a task idempotent?** - Using stateful modules (`apt`, `service`, `user`, `docker_container`) that only change the system when its current state differs from the desired one. +3. **What makes a task idempotent?** + Stateful modules that only change the system if the current state differs from desired state. -4. **How do handlers improve efficiency?** - Handlers run only when notified by a task that actually changed something, avoiding unnecessary service restarts. +4. **How do handlers improve efficiency?** + Handlers run only when notified, avoiding unnecessary restarts. -5. **Why is Ansible Vault needed?** - It allows storing secrets in version control safely by encrypting them, while still enabling automation to use them. +5. **Why would Ansible Vault be needed?** + To store sensitive credentials safely in version control when secrets are required. --- ## 8. Challenges (Optional) -- TODO: add short bullet points if you had any issues and how you solved them. +- VirtualBox/Vagrant leftovers caused VM name conflicts; fixed by removing stale VMs from VirtualBox. +- WSL could not access `127.0.0.1:2222` forwarded port; fixed by using Windows LAN IP (e.g., `192.168.31.32`).