diff --git a/.gitignore b/.gitignore index 30d74d2584..748114b1fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -test \ No newline at end of file +test +all.yml diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..dd2f3d3134 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = False + +[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..8961459365 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,147 @@ +# Lab 5 — Ansible Fundamentals + +## 1. Architecture Overview + +### Ansible version used + +```bash +ansible --version +ansible [core 2.20.1] +``` + +### Target VM OS and version + +**Ubuntu 22.04 LTS** + +### Role structure explanation + +1. `common` role configures base system for server +2. `docker` role installs and configures Docker +3. `app_deploy` role deploys the actual application + +`common` role -> `docker` role -> `app_deploy` role + +### Why roles instead of monolithic playbooks? + +Using roles instead of monolithic playbooks ensures better readability as this provides clear separation of concerns and it's easier to debug and update isolated components. + +## 2. Roles Documentation + +### `common` role: + +**Purpose:** Configures base system for server +**Variables:** Common packages and timezone +**Handlers:** No handlers +**Dependencies:** No dependencies + +### `docker` role: + +**Purpose:** Installs and configures Docker +**Variables:** Dokcer version, Docker Compose version, Docker users, Docker repository URL, Docker GPG key URL +**Handlers:** Docker restart +**Dependencies:** Depends on `common` role + +### `deploy` role: + +**Purpose:** Deploys the actual application +**Variables:** Application settings, Docker settings, environment variables, health check and vault variables +**Handlers:** App container restart and reload +**Dependencies:** Depends on `docker` role + +## 3. Idempotency Demonstration + +### Terminal output from FIRST provision.yml run + +![](screenshots/first-provision.jpg) + +### Terminal output from SECOND provision.yml run + +![](screenshots/second-provision.jpg) + +### Analysis: What changed first time? What didn't change second time? + +Most of the tasks were changed first time. Packets, Docker, container weren't on the server. Then second time only 1 task was changed because I set `cache_valid_time: 3600` for cache update. Ansible checks system state and doesn't do unnecessary actions. If everything is set properly, nothing changes. + +### Explanation: What makes your roles idempotent? + +I used `state: present' that ensures packages are intalled. + +## 4. Ansible Vault Usage + +### How you store credentials securely + +I use **Ansible Vault** to encrypt sensitive data. + +### Vault password management strategy + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +### Example of encrypted file (show it's encrypted!) + +![](screenshots/encrypted-data.jpg) + +### Why Ansible Vault is important + +Ansible Vault is important because passwords are encrypted and it's safe in case of pushing file to git. + +## 5. Deployment Verification + +### Terminal output from deploy.yml run + +![](screenshots/deploy.jpg) + +### Container status: docker ps output + +![](screenshots/docker-ps.jpg) + +### Health check verification: curl outputs + +```bash +curl http://62.84.120.249:5000/health | jq +{ + "status": "healthy", + "timestamp": "2026-02-26T20:42:46.002Z", + "uptime_seconds": 10337 +} +``` + +## 6. Key Decisions + +### Why use roles instead of plain playbooks? + +- **Organization** - Roles group related tasks, variables, and handlers together +- **Readability** - Playbooks become clean and simple (just list roles) +- **Reusability** - Same role can be used in multiple playbooks +- **Maintainability** - Easier to update and debug isolated components + +### How do roles improve reusability? + +- **Parameterization** - Variables make roles adaptable to different environments +- **Encapsulation** - All dependencies are contained within the role +- **Sharing** - Roles can be shared via Ansible Galaxy +- **Composability** - Mix and match roles for different server types + +### What makes a task idempotent? + +- **State checking** - Modules check current state before making changes +- **Declarative syntax** - Describe the desired state, not how to achieve it +- **Conditionals** - Tasks run only when needed (e.g., when: container_info.exists) +- **No "latest"** - Using state: present instead of state: latest +- **Idempotent modules** - Ansible modules are designed to be idempotent + +### How do handlers improve efficiency? + +- **Run-once** - Execute only once, even if notified by multiple tasks +- **Conditional execution** - Run only when changes actually occur +- **Order control** - Execute at the end of the play, not during +- **Resource savings** - Prevent unnecessary restarts (e.g., restart Docker once, not multiple times) + +### Why is Ansible Vault necessary? + +- **Security** - Encrypts sensitive data (passwords, tokens, keys) +- **Version control safe** - Can commit encrypted files to git +- **Compliance** - Meets security standards and audit requirements +- **Team collaboration** - Share code without sharing secrets +- **Multi-environment** - Different passwords for dev/staging/production diff --git a/ansible/docs/image.png b/ansible/docs/image.png new file mode 100644 index 0000000000..894cf50711 Binary files /dev/null and b/ansible/docs/image.png differ diff --git a/ansible/docs/screenshots/deploy.jpg b/ansible/docs/screenshots/deploy.jpg new file mode 100644 index 0000000000..48b8752756 Binary files /dev/null and b/ansible/docs/screenshots/deploy.jpg differ diff --git a/ansible/docs/screenshots/docker-ps.jpg b/ansible/docs/screenshots/docker-ps.jpg new file mode 100644 index 0000000000..c07096b0b8 Binary files /dev/null and b/ansible/docs/screenshots/docker-ps.jpg differ diff --git a/ansible/docs/screenshots/encrypted-data.jpg b/ansible/docs/screenshots/encrypted-data.jpg new file mode 100644 index 0000000000..3c7489a05c Binary files /dev/null and b/ansible/docs/screenshots/encrypted-data.jpg differ diff --git a/ansible/docs/screenshots/first-provision.jpg b/ansible/docs/screenshots/first-provision.jpg new file mode 100644 index 0000000000..e3bc8ab6ac Binary files /dev/null and b/ansible/docs/screenshots/first-provision.jpg differ diff --git a/ansible/docs/screenshots/second-provision.jpg b/ansible/docs/screenshots/second-provision.jpg new file mode 100644 index 0000000000..2076050688 Binary files /dev/null and b/ansible/docs/screenshots/second-provision.jpg differ diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..165cf011d6 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,2 @@ +[webservers] +vm ansible_host=62.84.120.249 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yc-key diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..fac6006ca2 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,45 @@ +- name: Deploy Python containerized application + hosts: webservers + become: yes + gather_facts: yes + + vars_files: + - ../group_vars/all.yml + + roles: + - name: app_deploy + vars: + app_environment: + ENVIRONMENT: production + LOG_LEVEL: info + APP_VERSION: "{{ docker_image_tag }}" + + pre_tasks: + - name: Verify vault variables are loaded + debug: + msg: "Vault variables loaded successfully: {{ dockerhub_username is defined}}" + when: dockerhub_username is defined + + post_tasks: + - name: Get running containers + command: docker ps + register: docker_ps + changed_when: false + + - name: Show running containers + debug: + var: docker_ps.stdout_lines + + - name: Test application main endpoint + uri: + url: "http://{{ ansible_default_ipv4.address | default('localhost') }}:{{ app_host_port }}/" + method: GET + status_code: 200 + register: main_endpoint + ignore_errors: yes + + - name: Show application endpoints status + debug: + msg: + - "Main endpoint: {{ main_endpoint.status | default('FAILED') }}" + - "Health endpoint: {{ health_result.status | default('unknown') }}" diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..176326f8e0 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,7 @@ +- name: Provision web servers + hosts: webservers + become: yes + + roles: + - common + - docker diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..5a110d938e --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,14 @@ +app_name: fastapi-lab-app +app_port: 5000 +app_host_port: 5000 +app_container_name: "{{ app_name }}" +docker_image_tag: latest +restart_policy: unless-stopped + +app_environment: + ENVIRONMENT: development + LOG_LEVEL: debug + +health_check_retries: 30 +health_check_delay: 2 +health_endpoint: /health diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..56fcc81a45 --- /dev/null +++ b/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,12 @@ +- name: restart app container + docker_container: + name: "{{ app_container_name }}" + state: started + restart: yes + become: yes + +- name: reload app container + docker_container: + name: "{{ app_container_name }}" + restart: yes + become: yes diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..9e2651b7b9 --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,71 @@ +- name: Login to Docker Hub + docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + become: yes + no_log: true + +- name: Pull Docker image + docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_image_tag }}" + source: pull + force_source: yes + become: yes + register: pull_result + +- name: Get container info + docker_container_info: + name: "{{ app_container_name }}" + register: container_info + +- name: Stop existing container + docker_container: + name: "{{ app_container_name }}" + state: absent + become: yes + when: container_info.exists + +- name: Run application container + docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ restart_policy }}" + ports: + - "{{ app_host_port }}:{{ app_port }}" + env: "{{ app_environment }}" + become: yes + register: run_result + notify: restart app container + +- name: Wait for application to start + wait_for: + port: "{{ app_host_port }}" + host: "localhost" + delay: 5 + timeout: 60 + state: started + become: no + +- name: Check health endpoint + uri: + url: "http://localhost:{{ app_host_port }}{{ health_endpoint }}" + method: GET + status_code: 200 + timeout: 10 + register: health_result + retries: 5 + delay: 3 + until: health_result.status == 200 + become: no + +- name: Display application info + debug: + msg: + - "Application deployed successfully!" + - "Container: {{ app_container_name }}" + - "Image: {{ docker_image }}:{{ docker_image_tag }}" + - "Port: {{ app_host_port }} -> {{ app_port }}" + - "Health check: {{ health_result.status }}" + - "Container status: {{ run_result.container.State.Status | default('running') }}" diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..7c2fe4728e --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,10 @@ +common_packages: + - python3-pip + - python3-venv + - curl + - wget + - git + - vim + - htop + +timezone: "UTC" diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..7e7efbf771 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,23 @@ +- name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + become: yes + +- name: Install common packages + apt: + name: "{{ common_packages }}" + state: present + become: yes + +- name: Set timezone + timezone: + name: "{{ timezone }}" + become: yes + when: timezone is defined + +- name: Upgrade pip + pip: + name: pip + state: latest + become: yes diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..9cc896f797 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,2 @@ +docker_users: + - ubuntu diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..4dbfe45fab --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +- name: restart docker + systemd: + name: docker + state: restarted + become: yes diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..20f86d18d5 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,37 @@ +- name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + become: yes + +- name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + become: yes + +- name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + become: yes + notify: restart docker + +- name: Ensure Docker service is running + systemd: + name: docker + state: started + enabled: yes + become: yes + +- name: Add user to docker group + user: + name: "{{ item }}" + group: docker + append: yes + loop: "{{ docker_users }}" + become: yes + diff --git a/cloud-terraform/main.tf b/cloud-terraform/main.tf index 71afee97b8..298e55d878 100644 --- a/cloud-terraform/main.tf +++ b/cloud-terraform/main.tf @@ -9,6 +9,7 @@ terraform { provider "yandex" { zone = var.zone + folder_id = var.folder_id } data "http" "myip" { diff --git a/cloud-terraform/variables.tf b/cloud-terraform/variables.tf index 795f360348..e572809ed3 100644 --- a/cloud-terraform/variables.tf +++ b/cloud-terraform/variables.tf @@ -1,6 +1,10 @@ variable "zone" { type = string - default = "ru-sentral-b" + default = "ru-central1-b" +} + +variable "folder_id" { + type = string } variable "vm_name" {