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
83 changes: 83 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Python CI

on:
[push, pull_request]

permissions:
contents: read

jobs:

test:
name: Lint & Tests
runs-on: ubuntu-latest
timeout-minutes: 10

strategy:
fail-fast: true

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt
pip install pytest ruff

- name: Lint
run: ruff check .

- name: Run tests
run: pytest

- name: Setup Snyk
uses: snyk/actions/setup@master

- name: Run Snyk
run: snyk test --file=app_python/requirements.txt
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}


docker:
name: Build & Push Docker
needs: test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set version (CalVer)
id: version
run: |
echo "VERSION=$(date +'%Y.%m')" >> $GITHUB_OUTPUT

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./app_python
file: ./app_python/Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/app_python:2026.02
${{ secrets.DOCKERHUB_USERNAME }}/app_python:latest
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
test
test

# Ansible
*.retry
.vault_pass
ansible/inventory/*.pyc
__pycache__/
10 changes: 10 additions & 0 deletions ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[defaults]
inventory = inventory/hosts.ini
roles_path = roles
host_key_checking = False
retry_files_enabled = False

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

### Ansible version: 2.12.5
### Target VM OS: Ubuntu 24.04 LTS
### Role structure:
```text
ansible/
├── inventory/
│ └── hosts.ini
├── roles/
│ ├── common/
│ ├── docker/
│ └── app_deploy/
├── playbooks/
│ ├── site.yml
│ ├── provision.yml
│ └── deploy.yml
├── group_vars/
│ └── all.yml
├── ansible.cfg
└── docs/
└── LAB05.md
```

### Why roles:
- They allow you to separate tasks by functionality.
- They improve code reuse.
- They are easy to maintain and test independently.

# 2. Roles Documentation
### 2.1 common
- Purpose: Basic system setup (apt update, package installation, time settings).
- Variables: list of packages defaults/main.yml.
- Handlers: no.
- Dependencies: no.

### 2.2 docker
- Purpose: Install and run Docker, add a user to the docker group.
- Variables: Docker version, username.
- Handlers: restart docker service.
- Dependencies: common.
### 2.3 app_deploy
- Purpose: deploying a Python container.
- Variables: Docker Hub username, password, application name, port, container name, image tag.
- Handlers: restart the container if necessary.
- Dependencies: docker.

# 3. Idempotency Demonstration

### First run of playbook deploy.yml:
![First run](ansible/docs/screenshots/img.png)

### Second run of playbook deploy.yml:
![Second run](ansible/docs/screenshots/img_1.png)

### Analysis:

- `ok` — tasks where no changes were made (e.g., a port is already open).
- `changed` — tasks that updated the state (pull, run, restart the container).
The roles are idempotent by design, but starting a container always calls "changed" because we're deleting the old one and creating a new one.

# 4. Ansible Vault Usage

### Vault file: `group_vars/all.yml`
Content:
```yml
dockerhub_username: th1ef
dockerhub_password: my_password
app_name: devops-info-service
docker_image: "{{ dockerhub_username }}/{{ app_name }}"
docker_image_tag: latest
app_port: 5000
app_container_name: "{{ app_name }}"
```

# 5. Deployment Verification
### Container condition:
![Container condition](ansible/docs/screenshots/img_2.png)

### Health check:
![Health check](ansible/docs/screenshots/img_3.png)


### 6. Key Decisions

- **Why roles instead of monolithic playbooks?** \
They allow you to structure tasks and are easier to maintain and test.
- **How do roles improve reusability?** \
You can use a single role across different VMs and projects without duplicating code.
- **What makes a task idempotent?** \
The module checks the current state and makes changes only when necessary (state: present, state: started).
- **How do handlers improve efficiency?** \
They are executed only when the state changes, preventing unnecessary service restarts.
- **Why Ansible Vault?** \
To securely store sensitive data (passwords, tokens) in the repository.

### 7. Challenges
- Errors when logging into Docker Hub without a collection.
- Incorrect SSH key permissions in the container.
- Pull errors if the image wasn't on Docker Hub — resolved by uploading the image using your own account.
Binary file added ansible/docs/screenshots/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ansible/docs/screenshots/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ansible/docs/screenshots/img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ansible/docs/screenshots/img_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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]
vm ansible_host=93.77.177.133 ansible_user=ubuntu
8 changes: 8 additions & 0 deletions ansible/playbooks/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
- name: Deploy Python application
hosts: webservers
become: yes
vars_files:
- /ansible/group_vars/all.yml
roles:
- app_deploy
8 changes: 8 additions & 0 deletions ansible/playbooks/provision.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
- name: Provision web servers
hosts: webservers
become: yes

roles:
- common
- docker
8 changes: 8 additions & 0 deletions ansible/roles/app_deploy/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
app_name: devops-app
docker_image: "{{ dockerhub_username }}/{{ app_name }}"
docker_image_tag: latest
app_port: 5000
app_container_name: "{{ app_name }}"
dockerhub_username: your-username
dockerhub_password: your-access-token
6 changes: 6 additions & 0 deletions ansible/roles/app_deploy/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: restart app container
shell: |
docker stop "{{ app_container_name }}" || true
docker rm "{{ app_container_name }}" || true
docker run -d --name "{{ app_container_name }}" -p {{ app_port }}:5000 --restart unless-stopped "{{ docker_image }}:{{ docker_image_tag }}"
34 changes: 34 additions & 0 deletions ansible/roles/app_deploy/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
- name: Login to Docker Hub
shell: echo "{{ dockerhub_password }}" | docker login -u "{{ dockerhub_username }}" --password-stdin
no_log: true

- name: Pull Docker image
shell: docker pull "{{ docker_image }}:{{ docker_image_tag }}"

- name: Stop old container if running
shell: docker stop "{{ app_container_name }}" || true

- name: Remove old container if exists
shell: docker rm "{{ app_container_name }}" || true

- name: Run new container
shell: >
docker run -d
--name "{{ app_container_name }}"
-p {{ app_port }}:5000
--restart unless-stopped
"{{ docker_image }}:{{ docker_image_tag }}"

- name: Wait for app port to be available
wait_for:
host: 127.0.0.1
port: "{{ app_port }}"
delay: 5
timeout: 30

- name: Verify application health
uri:
url: "http://127.0.0.1:{{ app_port }}/health"
return_content: yes
status_code: 200
7 changes: 7 additions & 0 deletions ansible/roles/common/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
common_packages:
- python3-pip
- curl
- git
- vim
- htop
11 changes: 11 additions & 0 deletions ansible/roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600

- name: Install essential packages
apt:
name: "{{ common_packages }}"
state: present
install_recommends: no
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 @@
---
docker_user: ubuntu
5 changes: 5 additions & 0 deletions ansible/roles/docker/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
- name: restart docker
service:
name: docker
state: restarted
31 changes: 31 additions & 0 deletions ansible/roles/docker/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present

- name: Add Docker repository
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present

- name: Install Docker packages
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
state: present
update_cache: yes

- name: Ensure Docker service is running
service:
name: docker
state: started
enabled: yes

- name: Add user to docker group
user:
name: "{{ docker_user }}"
groups: docker
append: yes
8 changes: 8 additions & 0 deletions app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.venv
venv
__pycache__
.git
.gitignore
.env
*.pyc
.idea
16 changes: 16 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.13-slim

WORKDIR /app

RUN adduser --disabled-password --gecos "" appuser

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN chown -R appuser:appuser /app

USER appuser

CMD ["python", "app.py"]
Loading