Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[defaults]
inventory = inventory/hosts.ini
roles_path = roles
host_key_checking = False
remote_user = vboxuser
retry_files_enabled = False

[privilege_escalation]
become = True
become_method = sudo
become_user = root
261 changes: 261 additions & 0 deletions ansible/docs/LAB05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# Task 2

### "First" run
(Not really the first, I tore the environment down manually).

```text
timur@timur-ficus:~/proj/DevOps-Core-Course/ansible$ ansible-playbook playbooks/provision.yml

PLAY [Provision web servers] *******************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
[WARNING]: Host 'devops-vm' is using the discovered Python interpreter at '/usr/bin/python3.12', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [devops-vm]

TASK [common : Update apt cache] ***************************************************************************************************************
ok: [devops-vm]

TASK [common : Install common packages] ********************************************************************************************************
changed: [devops-vm]

TASK [common : Set timezone to UTC] ************************************************************************************************************
ok: [devops-vm]

TASK [docker : Add Docker repo] ****************************************************************************************************************
changed: [devops-vm]

TASK [docker : update apt cache] ***************************************************************************************************************
changed: [devops-vm]

TASK [docker : Install Docker] *****************************************************************************************************************
changed: [devops-vm]

TASK [docker : Add ubuntu user to docker group] ************************************************************************************************
ok: [devops-vm]

RUNNING HANDLER [docker : restart docker] ******************************************************************************************************
changed: [devops-vm]

PLAY RECAP *************************************************************************************************************************************
devops-vm : ok=9 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

```

### The next run

```text
timur@timur-ficus:~/proj/DevOps-Core-Course/ansible$ ansible-playbook playbooks/provision.yml

PLAY [Provision web servers] *******************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
[WARNING]: Host 'devops-vm' is using the discovered Python interpreter at '/usr/bin/python3.12', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [devops-vm]

TASK [common : Update apt cache] ***************************************************************************************************************
ok: [devops-vm]

TASK [common : Install common packages] ********************************************************************************************************
ok: [devops-vm]

TASK [common : Set timezone to UTC] ************************************************************************************************************
ok: [devops-vm]

TASK [docker : Add Docker repo] ****************************************************************************************************************
ok: [devops-vm]

TASK [docker : update apt cache] ***************************************************************************************************************
skipping: [devops-vm]

TASK [docker : Install Docker] *****************************************************************************************************************
ok: [devops-vm]

TASK [docker : Add ubuntu user to docker group] ************************************************************************************************
ok: [devops-vm]

PLAY RECAP *************************************************************************************************************************************
devops-vm : ok=7 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
```

After this run, nothing changed. Even `apt update` was skipped because it is triggered by changes in `Add Docker repo`.

### Analysis: what changed the first time

Everything related to `apt` and `docker` because the packages were not installed.

### Why nothing changed the second time

This is why we need Ansible: it checks that the system is in the desired state before doing anything (providing
idempotency). The system was already in the desired state because Ansible was run just before.


# Task 3

### Terminal output

```text
timur@timur-ficus:~/proj/DevOps-Core-Course/ansible$ ansible-playbook playbooks/deploy.yml --ask-vault-pass
Vault password:

PLAY [Deploy application] **********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
[WARNING]: Host 'devops-vm' is using the discovered Python interpreter at '/usr/bin/python3.12', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [devops-vm]

TASK [app_deploy : load vault] *****************************************************************************************************************
ok: [devops-vm]

TASK [app_deploy : DockerHub Login] ************************************************************************************************************
changed: [devops-vm]

TASK [app_deploy : Pull image] *****************************************************************************************************************
changed: [devops-vm]

TASK [app_deploy : Stop and remove running container] ******************************************************************************************
ok: [devops-vm]

TASK [app_deploy : Run container] **************************************************************************************************************
changed: [devops-vm]

TASK [app_deploy : Wait for port] **************************************************************************************************************
ok: [devops-vm]

TASK [app_deploy : Healthcheck] ****************************************************************************************************************
ok: [devops-vm]

PLAY RECAP *************************************************************************************************************************************
devops-vm : ok=8 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```

### `docker ps` output

```text
vboxuser@devops-vm:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0cdb6ce0a0f5 timurusmanov/devops-infoservice:latest "gunicorn -b 0.0.0.0…" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp devops-infoservice
```

### Healthcheck

Healthcheck passed: Ansible printed `ok`.

# Task 4

## Architecture overview

## 1. Architecture Overview
### Ansible version used
```text
ansible [core 2.20.2]
config file = None
configured module search path = ['/home/timur/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.14/site-packages/ansible
ansible collection location = /home/timur/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.14.3 (main, Feb 13 2026, 15:31:44) [GCC 15.2.1 20260209] (/usr/bin/python)
jinja version = 3.1.6
pyyaml version = 6.0.3 (with libyaml v0.2.5)
```

### Target VM OS and version
Ubuntu 24.04 LTS

### Role structure diagram or explanation
3 roles:
1. `common` for setting up necessary system software and configuration
2. `docker` for installing Docker
3. `app_deploy` for pulling and running the application.

### Why roles instead of monolithic playbooks?
Roles act as reusable and invokable playbooks. Increases modularity.

## 2. Roles Documentation

### common
- **Purpose**: Configure the system and set up common software.
- **Variables**: `common_packages` contains a list of packages to install
- **Handlers**: none
- **Dependencies**: no other roles needed

### docker
- **Purpose**: Install the latest Docker version and configure the system
- **Variables**: `ubuntu_user` is the user to add to the `docker` group, in my case `vboxuser`
- **Handlers**: `restart docker` restarts the daemon
- **Dependencies**: no other roles required

### app\_deploy
- **Purpose**: Pull and run the app
- **Variables in the role**:
- `default_port: 5000` is the listened port inside the container
- `default_restart_policy: unless-stopped`
- `default_environment_variables: '{}'` is the env. variables passed to the app
- **Variables in the vault** `group_vars/all.yml`:
- `dockerhub_username` is my username on DockerHub
- `dockerhub_password` is my PAT (by principle of least privilege, it grants read-only access only to public images)
- `app_name: devops-infoservice` is the container and image name (without my username)
- `docker_image: "{{ dockerhub_username }}/{{ app_name }}"` is the full image name on DockerHub
- `docker_image_tag: latest`, as required by the lab
- `app_port: 5000` is the port exposed on the VM
- `app_container_name: "{{ app_name }}"` is the container name
- **Handlers**: `restart container` restarts the container
- **Dependencies**: requires the `docker` role to run before it

## 3. Idempotency Demonstration
### Terminal output from FIRST provision.yml run
See Task 2.
### Terminal output from SECOND provision.yml run
See Task 2.
### Analysis: What changed first time? What didn't change second time?
See Task 2.
### Explanation: What makes your roles idempotent?
I took care to specify the desired state of the machine instead of imperatively running the commands.
This way, Ansible can determine what it actually needs to run, and, of course, on the second run there will be nothing
to do.

## 4. Ansible Vault Usage
### How you store credentials securely
I store them in the `group_vars/all.yml` vault (encrypted with a password).
### Vault password management strategy
The password is stored securely inside my head ;)
So unless you apply a soldering iron directly to the head, you will not extract it.
### Example of encrypted file (show it's encrypted!)
For example:
```sh
head -n 2 group_vars/all.yml
```
Yields:
```text
$ANSIBLE_VAULT;1.1;AES256
35666430376135623761373732656538383137656466336266366239303435363364353462323136
```
So this file is an unreadable Ansible vault that uses AES256 symmetric encryption.

### Why Ansible Vault is important
Since I have to push the variable file to GitHub, I have to encrypt it so that nobody can see the credentials.

## 5. Deployment Verification
See Task 3.

## 6. Key Decisions
### Why use roles instead of plain playbooks?
See **Why roles instead of monolithic playbooks?** in **Architecture overview**.
### How do roles improve reusability?
When done correctly, a role acts like a module that does one specific task and does it well.
This allows engineers to paste the same role into new projects.

### What makes a task idempotent?
When the task specifies what the system should be like by the end of the task, it can be re-run multiple times, but
runs after the first one will see that the system is already in the desired state and do nothing.

### How do handlers improve efficiency?
Handlers only execute when a task has changed the system state, not always. That reduces overhead.

### Why is Ansible Vault necessary?
See **Why Ansible Vault is important** in **Ansible Vault Usage**.

## 7. Challenges (Optional)
### Issues encountered and solutions
- Running `apt update` only if the docker repo has been newly added. Tried handlers, but it turned out they only run
after all tasks have run (which really should have been clarified better in the lecture, IMHO).

18 changes: 18 additions & 0 deletions ansible/group_vars/all.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$ANSIBLE_VAULT;1.1;AES256
35666430376135623761373732656538383137656466336266366239303435363364353462323136
3765323833663966376435333161643165363562656364390a663564353162643738303335646261
32613533636439346534623965376632363761656532666462613536613235633965626138353866
3636316635316430320a633562383464383065626561333064633661616364663534653531636533
31326237313365373739663031353664613036656230323239613165663932656364326630353135
35306339626335616238623561353030323063363531383331646464323066626230616336616436
39386238633763616530396361336563663366383831643431663562316331323834663336613932
61643833383139396265353330643237356335343266363261333765363735633731346565646437
65653262333733623732623962326434613334383131393461333533356136303836356264383461
33303533653533633038383038643330393763356334353465373464373166366665346366643432
32646233323362653635313966633636633139393165353339653535666136613039666336646161
34366137623666333632386231643238366265373064346564316637393935643063313831343861
36303064383063363630386566393539333664386665386162376238313134666538303261363332
34303537656335376338363533646630653862656164313939366263663662383136373932663665
30393463643534663434663538663934303666316534333932346164343866636161383763666139
38393365653832383536613535666534383634353536353233653537653762626133376361613065
36383937376137623539626363363866336566643235393565633463326665396664
2 changes: 2 additions & 0 deletions ansible/inventory/hosts.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[webservers]
devops-vm ansible_host=127.0.0.1 ansible_port=10022 ansible_user=vboxuser
7 changes: 7 additions & 0 deletions ansible/playbooks/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
- name: Deploy application
hosts: webservers
become: yes

roles:
- app_deploy
7 changes: 7 additions & 0 deletions ansible/playbooks/provision.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- name: Provision web servers
hosts: webservers
become: yes

roles:
- common
- docker
Empty file added ansible/playbooks/site.yml
Empty file.
4 changes: 4 additions & 0 deletions ansible/roles/app_deploy/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
default_port: 5000
default_restart_policy: unless-stopped
default_environment_variables: '{}'
10 changes: 10 additions & 0 deletions ansible/roles/app_deploy/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
- name: restart container
community.docker.docker_container:
name: '{{ app_container_name }}'
image: '{{ docker_image }}:{{ docker_image_tag }}'
published_ports:
- '{{ app_port }}:{{ default_port }}'
env: "{{ default_environment_variables }}"
restart_policy: '{{ default_restart_policy }}'
state: restarted
38 changes: 38 additions & 0 deletions ansible/roles/app_deploy/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
- name: load vault
ansible.builtin.include_vars: '../group_vars/all.yml'

- name: DockerHub Login
community.docker.docker_login:
username: '{{ dockerhub_username }}'
password: '{{ dockerhub_password }}'
no_log: true

- name: Pull image
community.docker.docker_image_pull:
name: '{{ docker_image }}'
tag: '{{ docker_image_tag }}'

- name: Stop and remove running container
community.docker.docker_container:
name: '{{ app_container_name }}'
state: absent

- name: Run container
community.docker.docker_container:
name: '{{ app_container_name }}'
image: '{{ docker_image }}:{{ docker_image_tag }}'
published_ports:
- '{{ app_port }}:{{ default_port }}'
env: "{{ default_environment_variables }}"
restart_policy: '{{ default_restart_policy }}'
state: started

- name: Wait for port
ansible.builtin.wait_for:
port: '{{ app_port }}'

- name: Healthcheck
ansible.builtin.uri:
url: 'http://localhost:{{ app_port }}/health'
status_code: 200
8 changes: 8 additions & 0 deletions ansible/roles/common/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
common_packages:
- python3-pip
- curl
- git
- vim
- htop
- gpg
15 changes: 15 additions & 0 deletions ansible/roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
- 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 to UTC
community.general.timezone:
name: Etc/UTC
hwclock: UTC
2 changes: 2 additions & 0 deletions ansible/roles/docker/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
ubuntu_user: vboxuser
Loading
Loading