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: diff --git a/.gitignore b/.gitignore index d61b66f420..e050328db5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ -test -.env -key.json \ No newline at end of file +test +.env +key.json +# --- Ansible --- +*.retry +.vault_pass +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 new file mode 100644 index 0000000000..d680e5b46e --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,24 @@ +# Lab05 — Ansible + +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 new file mode 100644 index 0000000000..b6a8c1d248 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,14 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +# 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 + +[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..112d992ddc --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,450 @@ +# LAB05 — Ansible Fundamentals (Report) + +## 1. Architecture Overview + +### Control node +- OS: Windows 11 + WSL (Ubuntu) +- Ansible is executed inside WSL +- Ansible version: + +```bash +ansible --version +``` + +```text +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** +- SSH access: forwarded port (guest 22 -> host 2222) +- OS (queried via Ansible): + +```bash +ansible -i inventory/hosts.ini webservers -a "lsb_release -a" +``` + +```text +vagrant1 | CHANGED | rc=0 >> +Distributor ID: Ubuntu +Description: Ubuntu 22.04.5 LTS +Release: 22.04 +Codename: jammyNo LSB modules are available. +``` + +### 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 (Ansible) + +```text +ansible/ +├── ansible.cfg +├── inventory/ +│ └── hosts.ini +├── group_vars/ +│ └── webservers.yml # non-secret variables (public image) +├── playbooks/ +│ ├── provision.yml +│ └── deploy.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** +- Update apt cache +- Install common packages +- Set timezone (optional) + +**Variables** +- `common_packages` (list) +- `common_timezone` (string) +- `common_set_timezone` (bool) + +--- + +### Role: `docker` +**Purpose** +- Install Docker Engine from the official Docker APT repository. +- Enable and start Docker. +- Add SSH user to the `docker` group. +- Install `python3-docker` for Ansible Docker modules. + +**Key tasks** +- Add Docker GPG key + repo +- Install Docker packages +- Ensure `docker` service is enabled and running +- Add user to docker group +- Install Docker SDK for Python (`python3-docker`) + +**Variables** +- `docker_user` +- `docker_packages` + +--- + +### Role: `app_deploy` +**Purpose** +- Pull the application image. +- Run the container with a stable name, port mapping and restart policy. +- Wait for readiness and verify `/health` and `/`. + +**Key tasks** +- Optional `docker_login` (executed only if password is provided) +- `docker_image` pull +- `docker_container` start +- `wait_for` + HTTP checks + +**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` + +--- + +## 4. Idempotency Demonstration (Provisioning) + +### 4.1 First run + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/provision.yml +``` + +```text + +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 -i inventory/hosts.ini playbooks/provision.yml +``` + +```text +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.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. + +--- + +## 6. Deployment Verification + +### 6.1 Deploy run + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/deploy.yml +``` + +```text + +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 -i inventory/hosts.ini webservers -a "docker ps" +``` + +```text +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): + +```bash +ansible -i inventory/hosts.ini webservers -a "curl -i http://127.0.0.1:5000/health" +``` + +From the control node (WSL) via Windows host forwarding: + +```bash +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 20:05:00 GMT +Content-Type: application/json; charset=utf-8 +Content-Length: 675 +Connection: close + +{"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?** + 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 tasks. + +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, avoiding unnecessary restarts. + +5. **Why would Ansible Vault be needed?** + To store sensitive credentials safely in version control when secrets are required. + +--- + +## 8. Challenges (Optional) + +- 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`). 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/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/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..94c50c5212 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,19 @@ +# 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/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..af1b388e3f --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,10 @@ +--- +- name: Deploy application + hosts: webservers + become: true + + vars_files: + - ../group_vars/webservers.yml + + 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..934817c0c3 --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- 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 + when: dockerhub_password is defined and dockerhub_password | length > 0 + +- name: Pull application image + community.docker.docker_image: + name: "{{ (docker_image | default(dockerhub_username ~ '/' ~ app_name)) }}:{{ (docker_image_tag | default('latest')) }}" + 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 | default(dockerhub_username ~ '/' ~ app_name)) }}:{{ (docker_image_tag | default('latest')) }}" + 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..a2464dd655 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- 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 }}" + when: common_set_timezone | bool diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..7c7861e59f --- /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 (Vagrant boxes usually use "vagrant"). +docker_user: "{{ ansible_user | default('vagrant') }}" + +# 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 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 +