Skip to content
Open

Lab05 #4533

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

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

env:
REGISTRY: docker.io
IMAGE_NAME: nadiaa02/devops-python-app
PYTHON_VERSION: '3.11'

jobs:
test:
name: Lint & Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: 'app_python/requirements*.txt'

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

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

- name: Lint with flake8
working-directory: ./app_python
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Run tests with coverage
working-directory: ./app_python
run: |
pytest tests/ -v --cov=. --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./app_python/coverage.xml
flags: python
name: python-coverage
token: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true

security:
name: Security Scan
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run Snyk vulnerability scan
uses: snyk/actions/python@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=app_python/requirements.txt --severity-threshold=high
continue-on-error: true

docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: [test, security]
if: github.event_name == 'push' && github.ref == 'refs/heads/lab03'

steps:
- name: Checkout code
uses: actions/checkout@v4

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

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

- name: Generate version
id: version
run: |
VERSION=$(date -u +'%Y.%m.%d-%H%M')
MONTH=$(date -u +'%Y.%m')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "month=$MONTH" >> $GITHUB_OUTPUT

- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./app_python
file: ./app_python/Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.month }}
${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
22 changes: 21 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
test
test
# Terraform
*.tfstate
*.tfstate.*
.terraform/
terraform.tfvars
*.tfvars
.terraform.lock.hcl

# Pulumi
pulumi/venv/
Pulumi.*.yaml

# Credentials
key.json
*.pem

# Ansible
*.retry
.vault_pass
__pycache__/
12 changes: 12 additions & 0 deletions ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[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
vault_password_file = .vault_pass
149 changes: 149 additions & 0 deletions ansible/docs/LAB05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Lab 05 — Ansible Fundamentals

## 1. Architecture Overview

- **Ansible version:** 2.20.5 (core)
- **Target VM:** Ubuntu 24.04 LTS (93.77.181.6, from Lab 4 Pulumi)
- **Control node:** macOS (local machine)

### Role Structure:
ansible/
├── inventory/hosts.ini # Static inventory with VM IP
├── roles/
│ ├── common/ # System packages
│ ├── docker/ # Docker installation
│ └── app_deploy/ # Application deployment
├── playbooks/
│ ├── provision.yml # Runs common + docker roles
│ └── deploy.yml # Runs app_deploy role
├── group_vars/all.yml # Encrypted credentials (Vault)
└── ansible.cfg # Ansible configuration

**Why roles instead of monolithic playbooks?**
Roles separate concerns — each role does one thing. This makes code reusable, testable, and easy to maintain. A monolithic playbook becomes hard to read and impossible to reuse across projects.

---

## 2. Roles Documentation

### common role
- **Purpose:** Updates apt cache and installs essential system packages
- **Variables:** `common_packages` — list of packages to install (python3-pip, curl, git, vim, htop, etc.)
- **Handlers:** None
- **Dependencies:** None

### docker role
- **Purpose:** Installs Docker CE on the target VM, ensures service is running, adds user to docker group
- **Variables:** `docker_user` — user to add to docker group (default: ubuntu)
- **Handlers:** `restart docker` — restarts Docker service when triggered by package installation
- **Dependencies:** common (apt cache must be updated)

### app_deploy role
- **Purpose:** Logs into Docker Hub, pulls image, stops old container, runs new container, verifies health
- **Variables:**
- `app_port: 5000`
- `app_restart_policy: unless-stopped`
- `dockerhub_username, dockerhub_password` — from Vault
- `docker_image, docker_image_tag, app_container_name` — from Vault
- **Handlers:** `restart app` — restarts application container
- **Dependencies:** docker role must be applied first

---

## 3. Idempotency Demonstration

### First run output:
TASK [common : Update apt cache] .............. changed
TASK [common : Install common packages] ....... changed
TASK [docker : Add Docker GPG key] ............ changed
TASK [docker : Add Docker repository] ......... changed
TASK [docker : Install Docker packages] ....... changed
TASK [docker : Add user to docker group] ...... changed
TASK [docker : Install python3-docker] ........ changed
RUNNING HANDLER [docker : restart docker] ..... changed
PLAY RECAP
lab04-vm: ok=12 changed=8 unreachable=0 failed=0

### Second run output:
TASK [common : Update apt cache] .............. ok
TASK [common : Install common packages] ....... ok
TASK [docker : Add Docker GPG key] ............ ok
TASK [docker : Add Docker repository] ......... ok
TASK [docker : Install Docker packages] ....... ok
TASK [docker : Add user to docker group] ...... ok
TASK [docker : Install python3-docker] ........ ok
PLAY RECAP
lab04-vm: ok=11 changed=0 unreachable=0 failed=0

**Analysis:** First run made 8 changes — installed packages, added GPG key, added repository, added user to group, restarted Docker. Second run showed 0 changes because all desired states were already achieved. Tasks are idempotent because Ansible modules (apt, apt_key, apt_repository, user, service) check current state before acting.

---

## 4. Ansible Vault Usage

Credentials stored in `group_vars/all.yml`, encrypted with Ansible Vault:
$ANSIBLE_VAULT;1.1;AES256
62633836313162393136646664616231633635383338...

- Vault password stored in `.vault_pass` (added to .gitignore)
- File encrypted with `ansible-vault encrypt`
- Viewed with `ansible-vault view group_vars/all.yml`
- Edited with `ansible-vault edit group_vars/all.yml`

**Why Ansible Vault is important:** Credentials must never be stored in plaintext in version control. Vault encrypts secrets so they can be safely committed to Git while remaining inaccessible without the vault password.

---

## 5. Deployment Verification

### deploy.yml run output:
TASK [app_deploy : Log in to Docker Hub] ...... changed
TASK [app_deploy : Pull Docker image] ......... changed
TASK [app_deploy : Stop existing container] ... changed
TASK [app_deploy : Remove existing container] . changed
TASK [app_deploy : Run application container] . changed
TASK [app_deploy : Wait for application] ...... ok
TASK [app_deploy : Verify application health] . ok
TASK [app_deploy : Show health check result] .. ok
msg: "App is running, status: 200"
PLAY RECAP
lab04-vm: ok=9 changed=5 unreachable=0 failed=0

### Container status (docker ps):
CONTAINER ID IMAGE COMMAND
5a4b4b29701f nadiaa02/lab02-python-app:latest "python app.py"
STATUS: Up PORTS: 0.0.0.0:5000->5000/tcp NAMES: devops-app

### Health check:
```bash
$ curl http://93.77.181.6:5000/
{"service":{"name":"devops-info-service","version":"1.0.0"},"runtime":{"uptime_human":"0 hours, 1 minute"},...}

HTTP Status: 200 OK
```

---

## 6. Key Decisions

**Why use roles instead of plain playbooks?**
Roles enforce separation of concerns and make code reusable. The docker role can be used in any project that needs Docker, without copying tasks. Plain playbooks become monolithic and hard to maintain.

**How do roles improve reusability?**
Each role is self-contained with its own tasks, handlers, and defaults. Any playbook can include a role with one line. Roles can also be shared via Ansible Galaxy.

**What makes a task idempotent?**
A task is idempotent when it checks current state before acting. Ansible modules like `apt`, `service`, and `user` do this automatically — they only make changes when the current state differs from the desired state.

**How do handlers improve efficiency?**
Handlers only run when notified, and only once per play even if notified multiple times. This prevents unnecessary service restarts — Docker is only restarted if packages actually changed.

**Why is Ansible Vault necessary?**
Plaintext credentials in Git are a critical security risk. Vault encrypts secrets at rest so they can be version-controlled safely. The vault password is kept separate and never committed.

---

## 7. Challenges

- Ansible 2.20 has a bug where group_vars vault variables are not resolved in task args — worked around by defining vars directly in playbook
- Docker GPG key deprecation warning in apt_key module — cosmetic only, does not affect functionality
17 changes: 17 additions & 0 deletions ansible/group_vars/all.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
$ANSIBLE_VAULT;1.1;AES256
66633034636538343337646632643063383135383963363564656665386533363866626537393030
6333613861343830343061373764313732393634643336640a353763636532623432373163323462
63343735303366323664663830666164633934653732373562353238326363333232303930383135
3231386465323164380a313534663865613062623266303261353963383936333338383362626665
32316166656463636636383032643335366334396136663430643162386432383736386261326463
65323836616566633935393166633437333433336637393137363661656335633131353638613062
63633866653732643937303634666566393930373136373861633935383962376464303264663237
31613364666362323839396430623136653936663066333562373533383162383134366139353331
37333463353530346365363062303662363536316161353366653230633764393561613238323661
65653736363437323738373739303535616538623665323935323664656165653330636563386465
63653332303934656466396434373166643236313465306136386139386665316130646231663735
38323533653034623864336438623763303135313536663461343836356161613339353435326263
36383035613364636236616438303035616662613238616331363837663333316361613637636234
38376665396163663239316432373933666238363464316232626166646165303661643435343366
38626466313437616434653663386261393937333435306137356133623161363031626630376533
61313666343631643433
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]
lab04-vm ansible_host=93.77.181.6 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/lab04_key

[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
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
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
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 @@
---
app_port: 5000
app_restart_policy: unless-stopped
app_env_vars: {}
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
community.docker.docker_container:
name: "{{ app_container_name }}"
state: started
restart: yes
Loading