Ansible Course
Mini Project — End-to-End Ansible Automation
In this lesson
This mini project is the capstone of the entire course. It ties together every concept from all three sections — inventory, playbooks, roles, variables, Vault, templates, handlers, error handling, tags, Galaxy, CI/CD, performance tuning, and security hardening — into a single cohesive project that provisions, configures, and deploys a real application stack. You will build it yourself, step by step, using the course material as your reference. The result is a production-grade Ansible project you can use as a template for real work.
Project Overview
What you are building:
A three-tier web application stack — an Nginx reverse proxy, a Python/Django
application server, and a PostgreSQL database — fully automated from bare server
to running application, with a GitHub Actions CI/CD pipeline that lints, tests,
and deploys on every push to main.
Project requirements
Four roles: common, nginx, postgresql, app_deploy — each independently testable with Molecule
All secrets encrypted with Ansible Vault — no plaintext credentials anywhere in the repository
Separate staging and production inventories — same playbooks, different variable values
Symlink release management with automated rollback via block/rescue
GitHub Actions pipeline: lint → Molecule → deploy staging → manual gate → deploy production
ansible.cfg with pipelining enabled, forks ≥ 20, fact caching, and profile_tasks
Project Structure
ansible-miniproject/
├── ansible.cfg # pipelining, forks, fact caching
├── site.yml # provision: all roles, all tiers
├── deploy.yml # deploy only: app_deploy role
├── smoke_tests.yml # post-deploy health verification
├── requirements.yml # Galaxy deps (geerlingguy.certbot etc.)
│
├── inventory/
│ ├── staging/
│ │ ├── hosts.ini
│ │ └── group_vars/
│ │ ├── all.yml # env: staging
│ │ ├── webservers.yml
│ │ ├── appservers.yml
│ │ └── databases.yml
│ └── production/
│ ├── hosts.ini
│ └── group_vars/
│ ├── all.yml # env: production
│ ├── webservers.yml
│ ├── appservers.yml
│ └── databases.yml
│
├── roles/
│ ├── common/ # base packages, timezone, NTP, motd
│ ├── nginx/ # reverse proxy + SSL
│ ├── postgresql/ # database server
│ └── app_deploy/ # symlink release deployment
│
├── vars/
│ └── secrets.yml # vault-encrypted (all credentials)
│
├── .github/
│ └── workflows/
│ └── ansible.yml # full CI/CD pipeline
│
├── .ansible-lint # lint configuration
├── .vault_pass # NOT committed (.gitignore)
├── .gitignore # includes .vault_pass, *.retry
└── README.md
Core Configuration Files
ansible.cfg
[defaults]
inventory = ./inventory/staging
remote_user = ansible
forks = 20
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 3600
callbacks_enabled = timer, profile_tasks
stdout_callback = yaml
[ssh_connection]
pipelining = True
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
site.yml — full provisioning
---
- name: Apply common baseline to all hosts
hosts: all
become: true
roles:
- role: common
tags: [common, always]
- name: Configure web tier
hosts: webservers
become: true
roles:
- role: nginx
tags: [nginx, config]
- name: Configure database tier
hosts: databases
become: true
roles:
- role: postgresql
tags: [postgresql, config]
- name: Deploy application
hosts: appservers
become: true
roles:
- role: app_deploy
tags: [deploy]
deploy.yml — deployment only (symlink + rollback)
---
- name: Deploy application release
hosts: appservers
become: true
serial: 1
max_fail_percentage: 0
vars_files:
- vars/secrets.yml
pre_tasks:
- name: Assert version is provided
ansible.builtin.assert:
that: app_version is defined and app_version | length > 0
fail_msg: "Pass -e app_version=X.Y.Z"
tasks:
- name: Deploy with automatic rollback
block:
- name: Create release directory
ansible.builtin.file:
path: "{{ deploy_dir }}/releases/{{ app_version }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
- name: Extract release archive
ansible.builtin.unarchive:
src: "dist/app-{{ app_version }}.tar.gz"
dest: "{{ deploy_dir }}/releases/{{ app_version }}"
owner: "{{ app_user }}"
- name: Link shared config
ansible.builtin.file:
src: "{{ deploy_dir }}/shared/config"
dest: "{{ deploy_dir }}/releases/{{ app_version }}/config"
state: link
- name: Run database migrations
ansible.builtin.command:
cmd: python manage.py migrate --no-input
chdir: "{{ deploy_dir }}/releases/{{ app_version }}"
become_user: "{{ app_user }}"
changed_when: true
- name: Update current symlink
ansible.builtin.file:
src: "{{ deploy_dir }}/releases/{{ app_version }}"
dest: "{{ deploy_dir }}/current"
state: link
force: true
notify: app_deploy | Restart application
- name: Flush handlers before health check
ansible.builtin.meta: flush_handlers
- name: Health check
ansible.builtin.uri:
url: "http://{{ ansible_default_ipv4.address }}:8000/health"
status_code: 200
retries: 10
delay: 6
until: health.status == 200
register: health
rescue:
- name: Rollback to previous release
ansible.builtin.shell: |
prev=$(ls -1dt {{ deploy_dir }}/releases/*/ | sed -n '2p')
ln -sfn "$prev" {{ deploy_dir }}/current
notify: app_deploy | Restart application
- name: Fail after rollback
ansible.builtin.fail:
msg: "Deployment of {{ app_version }} FAILED — rolled back."
always:
- name: Prune old releases
ansible.builtin.shell: |
ls -1dt {{ deploy_dir }}/releases/*/ | tail -n +6 | xargs rm -rf
changed_when: false
Secrets and Variables
vars/secrets.yml — before encryption
---
# ansible-vault encrypt vars/secrets.yml before committing
vault_db_password: "change-me-in-production"
vault_app_secret_key: "django-secret-key-here"
vault_registry_password: "registry-token-here"
# Encrypt before first commit
ansible-vault encrypt vars/secrets.yml
# Edit later
ansible-vault edit vars/secrets.yml
inventory/staging/group_vars/databases.yml
---
postgresql_version: "15"
postgresql_port: 5432
postgresql_max_connections: 50
postgresql_databases:
- name: appdb_staging
owner: appuser
postgresql_users:
- name: appuser
password: "{{ vault_db_password }}"
db: appdb_staging
CI/CD Pipeline
The pipeline enforces all four gates — lint passes before Molecule runs, Molecule passes before staging deploys, staging passes before production is available for approval.
# .github/workflows/ansible.yml
name: Ansible CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ansible ansible-lint yamllint
- run: ansible-galaxy install -r requirements.yml
- run: yamllint .
- run: ansible-lint
- run: ansible-playbook site.yml --syntax-check -i inventory/staging/
molecule:
runs-on: ubuntu-latest
needs: validate
strategy:
matrix:
role: [common, nginx, postgresql, app_deploy]
steps:
- uses: actions/checkout@v4
- run: pip install ansible molecule molecule-plugins[docker]
- run: cd roles/${{ matrix.role }} && molecule test
deploy-staging:
runs-on: ubuntu-latest
needs: molecule
if: github.event_name == 'push'
environment: staging
steps:
- uses: actions/checkout@v4
- run: pip install ansible
- run: ansible-galaxy install -r requirements.yml
- run: echo "${{ secrets.VAULT_PASSWORD }}" > .vault_pass
- run: echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
- run: |
ansible-playbook site.yml -i inventory/staging/ \
--vault-password-file .vault_pass --check --diff
ansible-playbook site.yml -i inventory/staging/ \
--vault-password-file .vault_pass
ansible-playbook smoke_tests.yml -i inventory/staging/ \
--vault-password-file .vault_pass
- if: always()
run: rm -f .vault_pass ~/.ssh/id_ed25519
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment: production
steps:
- uses: actions/checkout@v4
- run: pip install ansible
- run: ansible-galaxy install -r requirements.yml
- run: echo "${{ secrets.VAULT_PASSWORD_PROD }}" > .vault_pass
- run: echo "${{ secrets.SSH_KEY_PROD }}" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
- run: |
ansible-playbook site.yml -i inventory/production/ \
--vault-password-file .vault_pass
ansible-playbook smoke_tests.yml -i inventory/production/ \
--vault-password-file .vault_pass
- if: always()
run: rm -f .vault_pass ~/.ssh/id_ed25519
Step-by-Step Build Sequence
Follow this order. Each step is testable before moving to the next — never write all files at once and debug everything together.
Initialise project structure
Create directory layout, ansible.cfg, .gitignore, .ansible-lint. Run ansible-galaxy role init for each of the four roles.
Write and test the common role
Base packages, timezone, NTP. Run molecule test in roles/common/. Green Molecule before moving on.
Write and test the nginx role
Install, configure from template, manage vhosts. All configurable options in defaults/. Handler prefixed nginx |. Molecule green.
Write and test the postgresql role
Install, configure, create database and user. no_log: true on user task. Tasks split across install.yml, configure.yml, databases.yml. Molecule green.
Write the app_deploy role
Symlink release pattern with block/rescue rollback, health check, old release pruning. This is the core of deploy.yml.
Build the inventory and variable files
Staging and production inventories. All secrets in vars/secrets.yml — encrypt with Vault. Verify with ansible-inventory --graph.
Run site.yml against staging — first full run
Run with --check --diff first. Review output. Apply. Confirm all tasks are idempotent on the second run (changed=0).
Write the GitHub Actions pipeline
Store vault password and SSH key as repository secrets. Add the workflow file. Push to a branch and confirm all four jobs run and pass.
Test rollback
Deploy a version with a deliberately broken health endpoint. Confirm the rescue block fires, the previous release symlink is restored, and the application comes back healthy.
Completion Checklist
Use this checklist to verify your project meets every requirement before considering it complete. Every item maps to a lesson from this course.
Course Complete
You have completed the Dataplexa Ansible Course
Across 40 lessons you have gone from Ansible fundamentals to production-grade automation — writing idempotent playbooks, building reusable roles, managing secrets securely, deploying applications with automated rollback, hardening servers, managing Docker and Kubernetes, and shipping automation through a full CI/CD pipeline. The mini project in this lesson is proof of that capability. Keep it as a living template — every real project you build from here will reuse these patterns.
40
Lessons completed
3
Sections mastered
1
Production-ready project