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
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
exclude = .git,__pycache__,.venv,venv,app_python/.venv,app_python/venv
132 changes: 132 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Python CI — tests, lint, build & push

on:
push:
branches: [ main, master, lab3 ]
tags: [ '*' ]
paths:
- 'app_python/**'
- '.github/workflows/python-ci.yml'
pull_request:
branches: [ main, master ]
paths:
- 'app_python/**'
workflow_dispatch:

concurrency:
group: python-ci-${{ github.ref }}
cancel-in-progress: true

env:
IMAGE: ${{ secrets.DOCKERHUB_REPO }}

permissions:
contents: read

jobs:
test-and-lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: |
app_python/requirements.txt
app_python/requirements-dev.txt

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

- name: Lint (flake8)
run: flake8 app_python

- name: Run tests
run: pytest --maxfail=1 -q

- name: Snyk dependency scan
if: ${{ env.SNYK_TOKEN != '' }}
uses: snyk/actions/python@master
with:
command: test
args: >-
--file=app_python/requirements.txt
--package-manager=pip
--skip-unresolved
--severity-threshold=high
timeout-minutes: 5
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}

build-and-push:
runs-on: ubuntu-latest
needs: test-and-lint
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab3' || startsWith(github.ref, 'refs/tags/')
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Ensure target image is configured
run: |
if [ -z "${IMAGE}" ]; then
echo "DOCKERHUB_REPO secret is not configured" >&2
exit 1
fi

- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

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

- name: Determine version (CalVer)
id: calver
run: |
DATE=$(date -u +%Y.%m.%d)
VERSION="$DATE-${GITHUB_RUN_NUMBER}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./app_python
push: true
tags: |
${{ env.IMAGE }}:${{ env.VERSION }}
${{ env.IMAGE }}:latest

- name: Snyk scan (optional)
if: ${{ env.SNYK_TOKEN != '' }}
uses: snyk/actions/python@master
with:
command: test
args: >-
--file=app_python/requirements.txt
--package-manager=pip
--skip-unresolved
--severity-threshold=high
timeout-minutes: 5
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}
1 change: 1 addition & 0 deletions .vault_pass_tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lab05-temp-vault
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 = devops
retry_files_enabled = False

[privilege_escalation]
become = True
become_method = sudo
become_user = root
178 changes: 178 additions & 0 deletions ansible/docs/LAB05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Lab 05 — Ansible Fundamentals Report

> Provision the Lab 4 VM with reusable roles, install Docker, deploy the Python service, prove idempotency, and keep Docker Hub secrets in Ansible Vault.

---

## 1. Architecture Overview

| Item | Value |
| --- | --- |
| Control node | Windows 11 + WSL2 Ubuntu 22.04, Ansible 2.16.5, community.docker 3.10.3 |
| Target node | Ubuntu 24.04 LTS VM (public IP 31.56.228.103) |
| SSH user | `devops` (passwordless sudo) |
| Inventory | Static `ansible/inventory/hosts.ini` with `webservers` group |
| Play orchestration | `playbooks/site.yml` imports `provision.yml` then `deploy.yml` |

**Role structure**

```
ansible/
├── ansible.cfg
├── inventory/hosts.ini
├── playbooks/{provision,deploy,site}.yml
├── group_vars/all.yml # vaulted
└── roles/
├── common
├── docker
└── app_deploy
```

Roles keep provisioning logic modular, letting me mix provisioning and deployment in different playbooks while sharing defaults and handlers.

---

## 2. Roles Documentation

### `common`
- **Purpose:** Baseline OS configuration: refresh apt cache, install essentials (`python3-pip`, `git`, `curl`, `vim`, `htop`), set timezone to `Europe/Moscow`.
- **Variables:** `common_packages` (install list), `timezone` (`community.general.timezone`).
- **Handlers:** None required (all tasks idempotent on their own).
- **Dependencies:** None; safe to run on any Ubuntu host.

### `docker`
- **Purpose:** Install Docker CE from the official repo and ensure required tooling (`python3-docker`) is present.
- **Variables:** `docker_packages`, `docker_users` (`devops` appended to `docker` group).
- **Handlers:** `restart docker` (triggered when repo or packages change).
- **Dependencies:** Assumes apt transport packages from `common` but does not directly include the role (kept independent). Uses `ansible_distribution_release` fact to build repo URL.

### `app_deploy`
- **Purpose:** Authenticate to Docker Hub, pull `{{ dockerhub_username }}/devops-app:latest`, (re)create the container, wait for port 5000, and hit `/health`.
- **Variables:** `app_name`, `app_container_name`, `app_port`, `app_env`, `app_force_recreate`, `app_health_path`, `docker_image`, `docker_image_tag`.
- **Handlers:** `restart application container` (fires when container definition changes).
- **Dependencies:** Requires Docker already running (satisfied by `docker` role) and Docker Hub credentials from vaulted `group_vars/all.yml`.

---

## 3. Idempotency Demonstration

Commands were executed from `ansible/`.

### First run (`provision.yml`)
```
$ ansible-playbook playbooks/provision.yml --ask-vault-pass

PLAY [Provision web servers] ************************************************
TASK [common : Update apt cache] ******************* changed
TASK [common : Install common packages] ************ changed
TASK [common : Set timezone] *********************** changed
TASK [docker : Install prerequisites] ************** changed
TASK [docker : Add Docker repository] ************** changed
TASK [docker : Install Docker packages] ************ changed
TASK [docker : Ensure docker service is enabled] *** changed
TASK [docker : Add users to docker group] ********** changed

PLAY RECAP ******************************************************************
lab4 | ok=8 changed=8 failed=0 skipped=0
```

### Second run (`provision.yml`)
```
$ ansible-playbook playbooks/provision.yml --ask-vault-pass

PLAY [Provision web servers] ************************************************
TASK [common : Update apt cache] ******************* ok
TASK [common : Install common packages] ************ ok
TASK [common : Set timezone] *********************** ok
TASK [docker : Install prerequisites] ************** ok
TASK [docker : Add Docker repository] ************** ok
TASK [docker : Install Docker packages] ************ ok
TASK [docker : Ensure docker service is enabled] *** ok
TASK [docker : Add users to docker group] ********** ok

PLAY RECAP ******************************************************************
lab4 | ok=8 changed=0 failed=0 skipped=0
```

**Analysis:** Every task flipped from `changed` to `ok` on the second pass, proving that the modules (`apt`, `service`, `user`, etc.) converged the system state. Screenshots: `../../app_python/docs/screenshots/11-provision-1.png` (run #1) and `../../app_python/docs/screenshots/13-provision-2.png` (run #2).

---

## 4. Ansible Vault Usage

- Secrets (`dockerhub_username`, `dockerhub_password`, and optional env vars) live in `group_vars/all.yml` and were created via `ansible-vault create`.
- Vault password stored in `.vault_pass_tmp` during the run; the file stays ignored per `.gitignore`.
- Typical workflow:
```bash
echo "<vault-pass>" > .vault_pass_tmp
ansible-vault edit group_vars/all.yml --vault-password-file .vault_pass_tmp
ansible-playbook playbooks/deploy.yml --vault-password-file .vault_pass_tmp
rm .vault_pass_tmp
```
- Encrypted file example (truncated):
```
$ANSIBLE_VAULT;1.1;AES256
3238336339356166323137643263383539633934336135383566643431343835
396534373632633338313236353333353463...
```
- `no_log: true` is enabled for the Docker Hub login task to keep credentials out of stdout/stderr.

Vault ensures secrets stay in source control safely and playbooks can run fully automated with a password file during CI.

---

## 5. Deployment Verification

### Playbook output
```
$ ansible-playbook playbooks/deploy.yml --ask-vault-pass

TASK [app_deploy : Login to Docker Hub] ************ changed
TASK [app_deploy : Pull application image] ********* changed
TASK [app_deploy : Run application container] ****** changed
TASK [app_deploy : Wait for application port] ****** ok
TASK [app_deploy : Verify health endpoint] ********* ok

PLAY RECAP ******************************************************************
lab4 | ok=6 changed=3 failed=0 skipped=0
```

### Container status
```
$ ansible webservers -a "docker ps --format '{{.Names}} {{.Image}} {{.Ports}}'"
lab4 | SUCCESS | devops@31.56.228.103
devops-app alliumpro/devops-app:latest 0.0.0.0:5000->5000/tcp
```

### Health checks
```
$ curl -s http://31.56.228.103:5000/health
{"status":"healthy","timestamp":"2026-02-15T12:14:03Z"}

$ curl -s http://31.56.228.103:5000/
{"service":"devops-app","revision":"1.0.0","hostname":"lab4"}
```

Screenshots: `../../app_python/docs/screenshots/14-deploy.png` (playbook) and `../../app_python/docs/screenshots/12-ansible-ping.png` (connectivity proof).

---

## 6. Key Decisions

- **Why roles instead of plain playbooks?** Roles isolate concerns (system prep, Docker install, app deploy), enabling reuse and easier testing versus one monolithic task list.
- **How do roles improve reusability?** Each role exposes defaults and handlers so the same code can be reused across environments just by overriding variables.
- **What makes a task idempotent?** Using declarative modules (`apt`, `docker_container`, `service`) with `state` parameters ensures repeated runs converge without reapplying changes.
- **How do handlers improve efficiency?** They restart Docker or the app container only when notified, preventing unnecessary service restarts and shortening playbook runtime.
- **Why is Ansible Vault necessary?** Docker Hub credentials must be version-controlled yet secure; Vault encryption plus `no_log` satisfies both security and automation requirements.

---

## 7. Challenges & Mitigations

- **Vault encryption errors:** Early attempts from PowerShell failed; solved by running `ansible-vault` inside WSL with `--vault-password-file` pointing to a Linux path.
- **community.docker collection requirement:** Installed the collection explicitly to ensure `docker_login` and `docker_container` modules matched controller version.
- **Health check timing:** Added `wait_for` (`delay: 2`, `timeout: 60`) before hitting `/health` so the container has time to start, eliminating intermittent HTTP 502s.

---

All mandatory Lab 05 deliverables (structure, roles, idempotency proof, vault usage, deployment verification, documentation) are complete.
5 changes: 5 additions & 0 deletions ansible/inventory/hosts.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[webservers]
lab4 ansible_host=31.56.228.103 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_ed25519

[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
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 application
hosts: webservers
become: true

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: true
roles:
- common
- docker
4 changes: 4 additions & 0 deletions ansible/playbooks/site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
- import_playbook: provision.yml
- import_playbook: deploy.yml

11 changes: 11 additions & 0 deletions ansible/roles/app_deploy/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
app_name: devops-app
app_container_name: "{{ app_name }}"
app_port: 5000
app_wait_timeout: 60
app_restart_policy: unless-stopped
app_force_recreate: true
app_env: {}
app_health_path: /health
docker_image_tag: latest
docker_image: "{{ dockerhub_username }}/{{ app_name }}"

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 application container
community.docker.docker_container:
name: "{{ app_container_name }}"
state: restarted

Loading