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
91 changes: 91 additions & 0 deletions .github/workflows/ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Ansible Deployment

on:
push:
branches: [ main ]
paths:
- 'ansible/**'
- '.github/workflows/ansible-deploy.yml'
pull_request:
branches: [ main ]
paths:
- 'ansible/**'

jobs:
lint:
name: Lint Ansible Code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install ansible-lint
run: pip install ansible-lint

- name: Run ansible-lint
run: |
cd ansible
ansible-lint playbooks/deploy.yml

deploy:
name: Deploy Application
needs: lint
runs-on: ubuntu-latest
if: github.event_name == 'push'

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

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Ansible
run: pip install ansible

- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts

- name: Create inventory
run: |
mkdir -p ansible/inventory
echo "[webservers]" > ansible/inventory/hosts.ini
echo "${{ secrets.VM_HOST }} ansible_user=${{ secrets.VM_USER }}" >> ansible/inventory/hosts.ini

- name: Decrypt vault password
run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass

- name: Run Ansible playbook
run: |
cd ansible
ansible-playbook playbooks/deploy.yml \
-i inventory/hosts.ini \
--vault-password-file /tmp/vault_pass

- name: Cleanup
if: always()
run: rm -f /tmp/vault_pass

verify:
name: Verify Deployment
needs: deploy
runs-on: ubuntu-latest
if: success()

steps:
- name: Check application
run: |
sleep 10
curl -f http://${{ secrets.VM_HOST }}:8000/health || exit 1
echo "Application verified successfully"
128 changes: 128 additions & 0 deletions ansible/docs/LAB06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Lab 6 — Advanced Ansible & CI/CD

## 1. Overview

This project automates application deployment using Ansible and GitHub Actions. I took a basic Ansible setup and added proper structure, safety features, and CI/CD automation.

What I Used:

- Ansible for automation
- Docker Compose for containers
- GitHub Actions for CI/CD
- Ubuntu servers for deployment

## 2. Blocks & Tags

I organized each role using blocks to group related tasks:

Common Role - Groups package tasks and user tasks separately
Docker Role - Separates installation from configuration

Tag Strategy:

- packages - Just install packages
- users - Just manage users
- docker_install - Only Docker installation
- docker_config - Only Docker setup
- web_app_wipe - Only cleanup operations

Example Usage:
```bash
ansible-playbook deploy.yml --tags docker_install

ansible-playbook deploy.yml --skip-tags common

ansible-playbook deploy.yml --list-tags
```

![](screenshots/tags.jpg)

![](screenshots/tag-docker.jpg)

## 3. Docker Compose Migration

I replaced the old docker run approach with Docker Compose templates.

Before: Manual container management with multiple tasks
After: Single declarative docker-compose.yml template

The template supports:

- Dynamic service names and ports
- Environment variables (including vault secrets)
- Health checks
- Restart policies

I also added proper role dependencies - the web_app role now automatically pulls in the docker role, so Docker is always installed first.

## 4. Wipe Logic

This was tricky - needed a way to completely remove the app, but make it really hard to do by accident.

The Solution: Double safety - requires BOTH a variable AND a tag:

```yaml
web_app_wipe: false

when: web_app_wipe | bool
tags: web_app_wipe
```

Test Scenarios:

- Normal deploy - wipe skipped (safe)
- Wipe only - -e "web_app_wipe=true" --tags web_app_wipe removes everything
- Clean reinstall - -e "web_app_wipe=true" wipes then deploys fresh
- Safety check - tag without variable = nothing happens

The wipe task removes containers, compose file, and app directory. Optional image/volume cleanup too.

![](screenshots/without-wipe.jpg)

![](screenshots/with-wipe.jpg)

## 5. CI/CD Pipeline

GitHub Actions automates everything on git push:

Workflow Steps:

- Lint - Runs ansible-lint to catch syntax errors
- Deploy - Sets up SSH, decrypts vault, runs playbook
- Verify - Checks health endpoint, confirms container is running

## 6. What I Learned

Blocks are great for:

-Grouping related tasks
- Applying conditions once
- Error handling with rescue/always

Tag + Variable combo is perfect for dangerous operations like wipe - prevents accidents but still allows automation.

Idempotency matters - Second run of the playbook shows "ok" not "changed". Docker Compose handles this automatically.

CI/CD secrets need careful handling - I create temp files and immediately delete them, even on failure.

## 7. Research Answers

Q: Rescue block failure?

A: Always block still runs, but playbook stops for that host.

Q: Nested blocks?

A: Yes, used them for package groups inside roles.

Q: Tag inheritance?

A: Tasks inherit parent block tags, can add their own.

Q: Variable + tag why both?

A: Double safety - tag for selective execution, variable for default-off behavior.

Q: Self-hosted vs GitHub runner?

A: Self-hosted is more secure (no SSH keys in GitHub), faster (same network), but needs maintenance.
Binary file added ansible/docs/screenshots/tag-docker.jpg
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/tags.jpg
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/with-wipe.jpg
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/without-wipe.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion ansible/inventory/hosts.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[webservers]
vm ansible_host=62.84.120.249 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yc-key
vm ansible_host=89.169.183.195 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yc-key
2 changes: 1 addition & 1 deletion ansible/playbooks/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- ../group_vars/all.yml

roles:
- name: app_deploy
- name: web_app
vars:
app_environment:
ENVIRONMENT: production
Expand Down
71 changes: 0 additions & 71 deletions ansible/roles/app_deploy/tasks/main.yml

This file was deleted.

84 changes: 64 additions & 20 deletions ansible/roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,67 @@
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
become: yes
- name: Common role tasks
block:
- name: Package installation block
block:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600

- name: Install common packages
apt:
name: "{{ common_packages }}"
state: present

- name: Upgrade pip
pip:
name: pip
state: latest

rescue:
- name: Handle apt cache update failure
debug:
msg: "Apt cache update failed, running fix..."

- name: Fix missing packages
apt:
name: apt
state: latest
update_cache: yes
when: ansible_os_family == "Debian"

tags:
- packages

- name: Install common packages
apt:
name: "{{ common_packages }}"
state: present
become: yes
- name: User creation block
block:
- name: Set timezone
timezone:
name: "{{ timezone }}"
when: timezone is defined

tags:
- users

- name: Set timezone
timezone:
name: "{{ timezone }}"
become: yes
when: timezone is defined
rescue:
- name: Common role failure handler
debug:
msg: "Common role execution failed"

- name: Log failure
copy:
content: "Common role failed at {{ ansible_date_time.iso8601 }}"
dest: /tmp/common_role_failed.log
mode: '0644'

- name: Upgrade pip
pip:
name: pip
state: latest
become: yes
always:
- name: Log completion
copy:
content: "Common role completed at {{ ansible_date_time.iso8601 }}"
dest: /tmp/common_role_completed.log
mode: '0644'
- debug:
msg: "Common role execution finished"

become: true
tags:
- common
Loading