100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Infrastructure as Code — Terraform & Ansible
60 minintermediate

Practice — Multi-Tier LAMP Stack with 3 Ansible Roles and Molecule

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' | sort

Role 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: restarted

Role 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: 5

Role 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_analytics

Testing 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 0
Analogy🏏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.
Lesson 27 of 33
0% complete