What You'll Build
You will create three Ansible roles that together configure a multi-tier cricket analytics stack: 'cricket_common' (OS hardening, chrony, common packages — applied to all servers), 'cricket_nginx' (Nginx reverse proxy — applied to web tier), and 'cricket_postgresql_client' (PostgreSQL client libraries and connection configuration — applied to app tier). Each role has its own Molecule test scenario, and a site playbook applies all three roles to the correct host groups. This exercise mirrors the real-world role-based architecture used in production automation pipelines, where each component is independently testable, independently deployable, and independently versioned.
Analogy🏏Cricket
🏏 Think of it like cricket: You are designing the BCCI's standard cricket ground specification module — a parameterisable blueprint that can be instantiated for any venue in the country without redrawing it from scratch. The blueprint fixes the non-negotiables every certified ground shares: a boundary perimeter (the VPC and its CIDR), spectator zones with public access (public subnets with an Internet Gateway route), and restricted player-and-officials zones behind accreditation checks (private subnets routing through NAT). What varies per venue arrives as parameters: how many stands to build (var.azs and subnet counts), whether this is a full international stadium or a modest district ground (var.single_nat_gateway as the cost lever — one shared service corridor instead of one per stand), and the venue's name and signage (tags). Wankhede and a Ranchi district ground are instantiated from the same drawing with different inputs — and when the board later improves the blueprint (adds flow logs, tightens NACLs), every venue inherits the improvement on its next renovation (module version bump) instead of each ground hand-patching its own architecture. That is the entire economics of module authorship: design once, rigorously, then stamp out consistent grounds forever.
Project Structure
bash
#!/bin/bash
mkdir -p ~/cricket_roles
cd ~/cricket_roles
# Create all three roles
for role in cricket_common cricket_nginx cricket_postgresql_client; do
ansible-galaxy role init "roles/${role}"
done
# Create site playbook and inventory
touch site.yml requirements.yml ansible.cfg
mkdir -p inventory/group_vars
cat > inventory/hosts.yml << 'INV'
all:
children:
web:
hosts: { web-01: { ansible_host: 10.0.1.10 }, web-02: { ansible_host: 10.0.1.11 } }
app:
hosts: { app-01: { ansible_host: 10.0.2.10 }, app-02: { ansible_host: 10.0.2.11 } }
cricket_servers:
children:
web:
app:
vars:
ansible_user: ubuntu
ansible_python_interpreter: /usr/bin/python3
INV
echo 'Project created. Structure:'
find roles -name 'main.yml' | sortRole 1 — cricket_common
yaml
# roles/cricket_common/defaults/main.yml
---
common_packages:
- vim
- curl
- jq
- htop
- chrony
- unattended-upgrades
ntp_server: 169.254.169.123 # AWS Time Sync Service
sysctl_settings:
net.ipv4.tcp_syncookies: 1
net.ipv4.ip_forward: 0
vm.swappiness: 10
# roles/cricket_common/tasks/main.yml
---
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tags: [common, packages]
- name: Install common packages
ansible.builtin.package:
name: '{{ common_packages }}'
state: present
tags: [common, packages]
- name: Apply kernel parameters
ansible.posix.sysctl:
name: '{{ item.key }}'
value: '{{ item.value | string }}'
state: present
reload: true
loop: '{{ sysctl_settings | dict2items }}'
loop_control:
label: '{{ item.key }}'
tags: [common, sysctl]
- name: Configure chrony
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
mode: '0644'
notify: Restart chrony
tags: [common, ntp]
- name: Enable chrony
ansible.builtin.service:
name: chrony
enabled: true
state: started
tags: [common, ntp]
# roles/cricket_common/handlers/main.yml
---
- name: Restart chrony
ansible.builtin.service:
name: chrony
state: restartedRole 2 — cricket_nginx
yaml
# roles/cricket_nginx/defaults/main.yml
---
nginx_port: 80
nginx_upstream_port: 8080
nginx_server_name: '{{ inventory_hostname }}'
nginx_worker_processes: auto
nginx_access_log: /var/log/nginx/cricket-access.log
nginx_error_log: /var/log/nginx/cricket-error.log
# roles/cricket_nginx/meta/main.yml — depends on common
---
galaxy_info:
author: nagarajarao
min_ansible_version: '2.15'
dependencies:
- role: cricket_common # Always run common first
# roles/cricket_nginx/tasks/main.yml
---
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
tags: [nginx]
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
validate: 'nginx -t -c %s'
notify: Reload Nginx
tags: [nginx]
- name: Enable Nginx
ansible.builtin.service:
name: nginx
enabled: true
state: started
tags: [nginx]
# roles/cricket_nginx/handlers/main.yml
---
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
# roles/cricket_nginx/molecule/default/verify.yml — test assertions
---
- name: Verify cricket_nginx role
hosts: all
become: true
tasks:
- name: Assert Nginx is installed and running
ansible.builtin.service_facts:
- name: Verify nginx service state
ansible.builtin.assert:
that:
- ansible_facts.services['nginx.service'] is defined
- ansible_facts.services['nginx.service'].state == 'running'
fail_msg: 'Nginx must be running'
- name: Verify port 80 is listening
ansible.builtin.wait_for:
port: 80
timeout: 5Role 3 — cricket_postgresql_client and Site Playbook
yaml
# roles/cricket_postgresql_client/defaults/main.yml
---
postgresql_version: '15'
db_host: '{{ hostvars[groups["db"][0]]["ansible_host"] | default("localhost") }}'
db_port: 5432
db_name: cricket_analytics
db_user: cricket_app
db_config_path: /etc/cricket
# roles/cricket_postgresql_client/tasks/main.yml
---
- name: Install PostgreSQL client packages
ansible.builtin.package:
name:
- 'postgresql-client-{{ postgresql_version }}'
- python3-psycopg2
- libpq-dev
state: present
tags: [postgres, packages]
- name: Create database config directory
ansible.builtin.file:
path: '{{ db_config_path }}'
state: directory
mode: '0750'
owner: root
group: root
tags: [postgres]
- name: Deploy database connection configuration
ansible.builtin.template:
src: database.conf.j2
dest: '{{ db_config_path }}/database.conf'
mode: '0640'
owner: root
group: cricket-app
no_log: true # Config contains connection string
tags: [postgres]
# site.yml — Complete site playbook applying roles to host groups
---
- name: Configure all cricket servers (common baseline)
hosts: cricket_servers
become: true
roles:
- cricket_common # Applied to all servers first
- name: Configure web tier (Nginx)
hosts: web
become: true
roles:
- role: cricket_nginx
vars:
nginx_upstream_port: 8080
nginx_server_name: api.cricket-analytics.io
- name: Configure app tier (PostgreSQL client)
hosts: app
become: true
roles:
- role: cricket_postgresql_client
vars:
db_host: '{{ db_host_override | default(hostvars[groups["db"][0]].ansible_host) }}'
db_name: cricket_analyticsTesting with Molecule
bash
#!/bin/bash
cd ~/cricket_roles
# Install Molecule and dependencies
pip install molecule molecule-plugins[docker] ansible-lint
# Test each role independently
echo '=== Test cricket_common ==='
cd roles/cricket_common && molecule test && cd ../..
echo '=== Test cricket_nginx ==='
cd roles/cricket_nginx && molecule test && cd ../..
echo '=== Test cricket_postgresql_client ==='
cd roles/cricket_postgresql_client && molecule test && cd ../..
# Run site playbook in check mode against real inventory
echo '=== Site playbook check mode ==='
ansible-playbook \
-i inventory/hosts.yml \
site.yml \
--check --diff \
-v
# Verify idempotency — run site.yml twice
echo '=== Full deployment ==='
ansible-playbook -i inventory/hosts.yml site.yml
echo '=== Idempotency check ==='
ansible-playbook -i inventory/hosts.yml site.yml 2>&1 | \
grep 'PLAY RECAP' -A5
# All changed= must be 0Analogy🏏Cricket
🏏 Think of it like cricket: Running Molecule against each role before composing them in site.yml is certifying each specialist coaching programme individually before combining them into the full pre-season camp. The temptation at this stage is to skip straight to the integrated test — 'just run site.yml against a server and see if the stack works' — but that is auditioning three new coaches simultaneously in one chaotic session: when a player's technique comes out wrong, was it the fitness coach, the batting coach, or an interaction between their drills? Nobody can say, so debugging becomes archaeology. Molecule's per-role scenarios give each programme its own clean trainees and its own examiner (each role converges against fresh containers with its own verify assertions), so a failure indicts exactly one role, and — via the idempotency check — proves each programme individually converges before you ever stack them. Only then does composition testing mean anything: when the site playbook applies common, then nginx, then postgresql-client to one host and something breaks, you already know each role is sound in isolation, so the bug must live in the composition itself — a variable collision, an ordering assumption, a handler notified across roles. Certify the parts, then test the whole; every mature engineering discipline converges on the same sequence.
- Role dependencies in meta/main.yml enforce execution ordering — 'cricket_nginx' depends on 'cricket_common', so Ansible automatically runs common before nginx without the site playbook needing to specify the order.
- Each role has its own Molecule scenario — independent testing prevents a change to cricket_common from breaking cricket_nginx without the test catching it.
- The site playbook applies roles to host groups: 'cricket_common' to all servers, 'cricket_nginx' to web tier, 'cricket_postgresql_client' to app tier — each tier gets exactly the configuration it needs.
- Use 'no_log: true' on tasks that deploy files containing secrets (database connection strings with passwords) — this prevents the file contents from appearing in Ansible's verbose output or callback logs.
- The idempotency check (running the playbook twice and verifying changed=0) is the quality gate for the entire site playbook, not just individual roles — integration between roles can introduce non-idempotent behaviour.