diff --git a/.gitignore b/.gitignore index 30d74d2584..b3001116e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,17 @@ -test \ No newline at end of file +# Terraform +*.tfstate +*.tfstate.backup +.terraform/ +*.tfvars +authorized_key.json + +# Ansible +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ + +# Python +venv/ +*.pyc +*.pyo diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..0ddcbf1672 --- /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..166b60e444 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,324 @@ +# Lab 05 — Ansible Fundamentals + +## 1. Architecture Overview + +**Ansible version:** `ansible --version` _(fill in after install)_ +**Target VM OS:** Ubuntu 22.04 LTS (Yandex Cloud, zone ru-central1-a) +**Control node:** macOS (local machine) + +### Role Structure + +``` +ansible/ +├── inventory/ +│ └── hosts.ini # Static inventory with VM IP +├── roles/ +│ ├── common/ # Base system setup +│ │ ├── tasks/main.yml +│ │ └── defaults/main.yml +│ ├── docker/ # Docker CE installation +│ │ ├── tasks/main.yml +│ │ ├── handlers/main.yml +│ │ └── defaults/main.yml +│ └── app_deploy/ # Application deployment +│ ├── tasks/main.yml +│ ├── handlers/main.yml +│ └── defaults/main.yml +├── playbooks/ +│ ├── site.yml # Main playbook (runs all) +│ ├── provision.yml # System provisioning +│ └── deploy.yml # App deployment +├── group_vars/ +│ └── all.yml # Vault-encrypted secrets +├── ansible.cfg +└── docs/ + └── LAB05.md +``` + +**Why roles instead of monolithic playbooks?** +Roles separate concerns, making each unit independently reusable, testable, and maintainable. A monolithic playbook becomes unmanageable as complexity grows. + +--- + +## 2. Roles Documentation + +### Role: `common` + +**Purpose:** Baseline system setup applied to every server — updates apt cache and installs essential packages. + +**Variables (`defaults/main.yml`):** +| Variable | Default | Description | +|---|---|---| +| `common_packages` | list of tools | Packages to install | +| `common_timezone` | `Europe/Moscow` | System timezone | + +**Handlers:** None + +**Dependencies:** None + +--- + +### Role: `docker` + +**Purpose:** Installs Docker CE from the official Docker repository, ensures the service is running and the deploy user is in the `docker` group. + +**Variables (`defaults/main.yml`):** +| Variable | Default | Description | +|---|---|---| +| `docker_user` | `ubuntu` | User to add to docker group | +| `docker_packages` | `docker-ce, docker-ce-cli, containerd.io` | Docker packages | + +**Handlers:** +- `restart docker` — restarts the Docker service when package installation changes + +**Dependencies:** `common` role (apt cache must be fresh) + +--- + +### Role: `app_deploy` + +**Purpose:** Authenticates with Docker Hub, pulls the application image, stops/removes the old container, starts a new one, and verifies the health endpoint. + +**Variables (from Vault `group_vars/all.yml`):** +| Variable | Description | +|---|---| +| `dockerhub_username` | Docker Hub login | +| `dockerhub_password` | Docker Hub access token | +| `docker_image` | Full image name | +| `docker_image_tag` | Image tag (default: `latest`) | +| `app_port` | Exposed port (default: `5000`) | +| `app_container_name` | Container name | +| `app_restart_policy` | Restart policy (default: `unless-stopped`) | + +**Handlers:** +- `restart app container` — restarts the application container on config change + +**Dependencies:** `docker` role must run first + +--- + +## 3. Idempotency Demonstration + +### First run (`provision.yml`) + +``` +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [common : Update apt cache] *********************************************** +changed: [lab04-vm] + +TASK [common : Install common packages] **************************************** +changed: [lab04-vm] + +TASK [common : Set timezone] *************************************************** +changed: [lab04-vm] + +TASK [docker : Install prerequisite packages for Docker] *********************** +ok: [lab04-vm] + +TASK [docker : Create /etc/apt/keyrings directory] ***************************** +ok: [lab04-vm] + +TASK [docker : Add Docker GPG key] ********************************************* +changed: [lab04-vm] + +TASK [docker : Add Docker APT repository] ************************************** +changed: [lab04-vm] + +TASK [docker : Install Docker packages] **************************************** +changed: [lab04-vm] + +TASK [docker : Ensure Docker service is started and enabled] ******************* +ok: [lab04-vm] + +TASK [docker : Add user to docker group] *************************************** +changed: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible Docker modules] ************** +changed: [lab04-vm] + +RUNNING HANDLER [docker : restart docker] ************************************** +changed: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=14 changed=10 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Second run (`provision.yml`) + +``` +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [common : Update apt cache] *********************************************** +ok: [lab04-vm] + +TASK [common : Install common packages] **************************************** +ok: [lab04-vm] + +TASK [common : Set timezone] *************************************************** +ok: [lab04-vm] + +TASK [docker : Install prerequisite packages for Docker] *********************** +ok: [lab04-vm] + +TASK [docker : Create /etc/apt/keyrings directory] ***************************** +ok: [lab04-vm] + +TASK [docker : Add Docker GPG key] ********************************************* +ok: [lab04-vm] + +TASK [docker : Add Docker APT repository] ************************************** +ok: [lab04-vm] + +TASK [docker : Install Docker packages] **************************************** +ok: [lab04-vm] + +TASK [docker : Ensure Docker service is started and enabled] ******************* +ok: [lab04-vm] + +TASK [docker : Add user to docker group] *************************************** +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible Docker modules] ************** +ok: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=12 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Analysis + +| Task | First run | Second run | Why idempotent? | +|---|---|---|---| +| Update apt cache | `ok` (cache_valid_time) | `ok` | `cache_valid_time=3600` skips if fresh | +| Install common packages | `changed` | `ok` | `state: present` checks before acting | +| Add Docker GPG key | `changed` | `ok` | Key already exists | +| Add Docker repository | `changed` | `ok` | Repo already in sources.list | +| Install Docker packages | `changed` | `ok` | `state: present` — already installed | +| Docker service | `ok` | `ok` | Already started and enabled | +| Add user to docker group | `changed` | `ok` | User already in group | + +**What makes roles idempotent:** Using declarative state modules (`state: present`, `state: started`) instead of imperative commands. Ansible checks current state before making changes. + +--- + +## 4. Ansible Vault Usage + +### How secrets are stored + +All sensitive data lives in `group_vars/all.yml`, encrypted with Ansible Vault: + +```bash +ansible-vault create group_vars/all.yml +``` + +The file contains DockerHub credentials and app configuration. Once encrypted, it looks like: + +``` +$ANSIBLE_VAULT;1.1;AES256 +3338386234323834623866... +``` + +### Vault password management + +- Password stored locally in `.vault_pass` (600 permissions) +- `.vault_pass` is in `.gitignore` — **never committed** +- Encrypted `group_vars/all.yml` is safe to commit + +### Using the vault + +```bash +# Run with password prompt +ansible-playbook playbooks/deploy.yml --ask-vault-pass + +# Or configure in ansible.cfg (vault_password_file = .vault_pass) +ansible-playbook playbooks/deploy.yml +``` + +### Why Ansible Vault is necessary + +Storing plaintext credentials in Git is a critical security risk. Vault encrypts secrets at rest while keeping them in version control alongside the code that uses them. + +--- + +## 5. Deployment Verification + +### Deploy run output + +``` +PLAY [Deploy application] ****************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [app_deploy : Login to Docker Hub] **************************************** +changed: [lab04-vm] + +TASK [app_deploy : Pull Docker image] ****************************************** +changed: [lab04-vm] + +TASK [app_deploy : Stop and remove existing container (if any)] **************** +ok: [lab04-vm] + +TASK [app_deploy : Run application container] ********************************** +changed: [lab04-vm] + +TASK [app_deploy : Wait for application port to be available] ****************** +ok: [lab04-vm] + +TASK [app_deploy : Verify health endpoint] ************************************* +ok: [lab04-vm] + +TASK [app_deploy : Show health check result] *********************************** +ok: [lab04-vm] => { + "msg": "Health check passed: 200" +} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=8 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Container status + +```bash +$ ansible webservers -a "docker ps" --ask-vault-pass +lab04-vm | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +528c21f9eb52 samerdockerhup/devops-info-service:latest "python app.py" 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:5000->5000/tcp devops-info-service +``` + +![alt](./lab5image2.png) + +### Health check verification + +![alt](./lab5image.png) +--- + +## 6. Key Decisions + +**Why use roles instead of plain playbooks?** +Roles enforce a standard structure that makes automation code reusable across projects. Each role can be tested in isolation and shared via Ansible Galaxy, unlike code buried in a single monolithic playbook. + +**How do roles improve reusability?** +A role like `docker` can be applied to any server in any project without modification. Variables in `defaults/` provide sensible defaults that can be overridden per environment, making the same role work for dev, staging, and production. + +**What makes a task idempotent?** +Using state-based modules that check current state before acting. `apt: state=present` only installs if missing; `service: state=started` only starts if stopped. Running the same task 10 times produces the same result as running it once. + +**How do handlers improve efficiency?** +Handlers only run once at the end of a play, even if notified multiple times. This prevents restarting Docker after every single package install — it restarts once after all changes are complete. + +**Why is Ansible Vault necessary?** +Credentials in plaintext Git history are a permanent security liability. Even if deleted later, they remain in git log. Vault encrypts secrets so the same repository can be public without exposing credentials. + +--- + +## 7. Challenges + +_Fill in after completing the lab._ diff --git a/ansible/docs/lab5image.png b/ansible/docs/lab5image.png new file mode 100644 index 0000000000..4b12fb3394 Binary files /dev/null and b/ansible/docs/lab5image.png differ diff --git a/ansible/docs/lab5image2.png b/ansible/docs/lab5image2.png new file mode 100644 index 0000000000..53f268e0cb Binary files /dev/null and b/ansible/docs/lab5image2.png differ diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..d88d24ffaf --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,18 @@ +$ANSIBLE_VAULT;1.1;AES256 +38333233363833646637353662386135373330373164333164623063663233616237346664643163 +3237346130643162663261303661353431633961383838350a623832383666383337313863336431 +35626431313337636131626163356262386332323363613734363866623862666265346234313537 +6661326164646462300a396365636531616162626436383539393536393735326332366231623430 +64666530336265346636343138376634303765623263373935306430386635333438396262346266 +35353861663533323632623265663134353631343838346262343736353062383236343661666137 +32623133356364353265643863616638393533393536346531353066346132663661333861393361 +37623630656236393630613033613332356535613464666338353666366439303431633965306533 +39383064363763303062636265633464663733376437323664616636633964643265376364636537 +62366564616136376661333035326264626266383836393132656263333465393666646632383238 +39326137663065396432353466613430363537653938333762303338383031663538303562316331 +31313631363531336363653635663239616466346632653139373062393266626236316137656533 +34356232336433373063643863626231633265356362626539396232303636656163336631613035 +36633033646663376434333864393664613162313735376637663331653766323065306231383438 +35323132616435616135663532366436323336643338363161623161626565356561626639343366 +66396465326337663163383862633863303932663431316331306433396133373930666461333231 +38363934316166356139343662393436306262663936333461313261313836373038 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..3f78de555d --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +lab04-vm ansible_host=89.169.145.194 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..d8e871cf24 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,9 @@ +--- +- name: Deploy application + hosts: webservers + become: yes + vars_files: + - "{{ playbook_dir }}/../group_vars/all.yml" + + roles: + - app_deploy diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..f53efb0248 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- 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..139c08f693 --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,3 @@ +--- +- import_playbook: provision.yml +- import_playbook: deploy.yml diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..9ffffde25e --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,4 @@ +--- +app_port: 5000 +app_restart_policy: unless-stopped +app_env_vars: {} diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..73deea15ef --- /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: 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..cc6497548a --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Login to Docker Hub + community.docker.docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: true + +- name: Pull Docker image + community.docker.docker_image: + name: "{{ docker_image }}:{{ docker_image_tag }}" + source: pull + force_source: yes + +- name: Stop and remove existing container (if any) + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + force_kill: yes + +- 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 }}" + ports: + - "{{ app_port }}:{{ app_port }}" + env: "{{ app_env_vars }}" + +- name: Wait for application port to be available + wait_for: + host: localhost + port: "{{ app_port }}" + delay: 5 + timeout: 60 + +- name: Verify health endpoint + uri: + url: "http://localhost:{{ app_port }}/health" + method: GET + status_code: 200 + register: health_check + +- name: Show health check result + debug: + msg: "Health check passed: {{ health_check.status }}" diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..be9b66b29b --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,11 @@ +--- +common_packages: + - python3-pip + - curl + - git + - vim + - htop + - unzip + - wget + +common_timezone: "Europe/Moscow" diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..b0cc19708f --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + +- name: Install common packages + 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..ac1e236676 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,6 @@ +--- +docker_user: ubuntu +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..3627303e6b --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + service: + name: docker + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..2bf8d61ec4 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: Install prerequisite packages for Docker + apt: + name: + - ca-certificates + - curl + - gnupg + state: present + +- name: Create /etc/apt/keyrings directory + file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + +- name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Add Docker APT repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_facts['distribution_release'] }} stable" + state: present + filename: docker + +- name: Install Docker packages + apt: + name: "{{ docker_packages }}" + state: present + update_cache: yes + notify: restart docker + +- name: Ensure Docker service is started and enabled + service: + name: docker + state: started + enabled: yes + +- name: Add user to docker group + user: + name: "{{ docker_user }}" + groups: docker + append: yes + +- name: Install python3-docker for Ansible Docker modules + apt: + name: python3-docker + state: present