Ansible LAMP Stack Capstone

This is the flagship project of the course — the one you will feature in your portfolio and walk through in job interviews. You are going to automate the complete deployment of a LAMP stack (Linux, Apache/Nginx, MySQL/MariaDB, PHP) using everything you have learned: roles, templates, Vault, handlers, conditionals, loops, and error handling. The result is a production-grade, idempotent, version-controlled deployment pipeline that works with a single command.

Project Architecture

The LAMP stack deployment targets your three-node lab environment:

  • web01 and web02: Nginx as a reverse proxy, PHP-FPM for application processing, a simple PHP application
  • db01: MariaDB database server with an application database and user
  • All nodes: Security baseline hardening, monitoring agent, firewall rules

Project Directory Structure

lamp-deployment/
  site.yml                    # Master playbook
  ansible.cfg
  inventory/
    hosts.ini
    group_vars/
      all/
        vars.yml              # Global non-sensitive variables
        vault.yml             # Encrypted secrets
      webservers.yml          # Web tier variables
      databases.yml           # Database tier variables
    host_vars/
      web01.yml               # web01-specific overrides
  roles/
    common/                   # Base OS configuration (all nodes)
    nginx/                    # Nginx web server (web nodes)
    php/                      # PHP-FPM (web nodes)
    mariadb/                  # MariaDB (database nodes)
    firewall/                 # UFW firewall rules (all nodes)
    myapp/                    # Application deployment (web nodes)
  requirements.yml
  .vault_pass                 # In .gitignore
  README.md

Step 1: Define Variables and Secrets

In group_vars/all/vars.yml:

app_name: lampapp
app_user: www-data
app_directory: /var/www/lampapp
app_domain: lampapp.local

php_version: "8.1"
mariadb_version: "10.6"

db_name: "{{ app_name }}"
db_user: "{{ app_name }}_user"
db_password: "{{ vault_db_password }}"      # Points to vault variable
db_host: "{{ groups['databases'][0] }}"     # First database server's hostname

In group_vars/all/vault.yml (encrypted with ansible-vault):

vault_db_password: "{{ encrypted_string_here }}"
vault_db_root_password: "{{ encrypted_string_here }}"

Step 2: The common Role

The common role runs on every node and establishes a consistent baseline:

# roles/common/tasks/main.yml
- import_tasks: packages.yml
- import_tasks: users.yml
- import_tasks: security.yml
- import_tasks: timezone.yml
# roles/common/tasks/packages.yml
- name: Update apt cache and upgrade system packages
  apt:
    update_cache: true
    upgrade: safe
    cache_valid_time: 3600

- name: Install common utilities
  apt:
    name:
      - curl
      - vim
      - htop
      - git
      - unzip
      - python3-pip
    state: present

Step 3: The mariadb Role

# roles/mariadb/tasks/main.yml
- name: Install MariaDB
  apt:
    name:
      - mariadb-server
      - python3-pymysql
    state: present

- name: Start and enable MariaDB
  service:
    name: mariadb
    state: started
    enabled: true

- name: Set MariaDB root password
  mysql_user:
    name: root
    password: "{{ vault_db_root_password }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock
    state: present
  no_log: true    # Prevents password from appearing in logs

- name: Create application database
  mysql_db:
    name: "{{ db_name }}"
    state: present
    login_user: root
    login_password: "{{ vault_db_root_password }}"

- name: Create application database user
  mysql_user:
    name: "{{ db_user }}"
    password: "{{ db_password }}"
    priv: "{{ db_name }}.*:ALL"
    host: "{{ item }}"
    state: present
    login_user: root
    login_password: "{{ vault_db_root_password }}"
  loop:
    - localhost
    - "{{ hostvars[0]]['ansible_host'] }}"
    - "{{ hostvars[1]]['ansible_host'] }}"
  no_log: true

Step 4: The php Role

# roles/php/defaults/main.yml
php_version: "8.1"
php_packages:
  - "php{{ php_version }}-fpm"
  - "php{{ php_version }}-mysql"
  - "php{{ php_version }}-curl"
  - "php{{ php_version }}-json"
  - "php{{ php_version }}-mbstring"
php_fpm_pool_name: "{{ app_name }}"
php_fpm_listen: "/run/php/php{{ php_version }}-fpm-{{ app_name }}.sock"

Step 5: The myapp Role

# roles/myapp/tasks/main.yml
- name: Create application directory
  file:
    path: "{{ app_directory }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: '0755'

- name: Deploy application files
  copy:
    src: files/app/
    dest: "{{ app_directory }}/"
    owner: "{{ app_user }}"
    mode: '0644'
  notify: Restart PHP-FPM

- name: Deploy application configuration
  template:
    src: config.php.j2
    dest: "{{ app_directory }}/config.php"
    owner: "{{ app_user }}"
    mode: '0640'
  notify: Restart PHP-FPM

Step 6: The Master Playbook (site.yml)

---
- name: Apply common configuration to all servers
  hosts: all
  become: true
  roles:
    - common
    - firewall

- name: Configure database servers
  hosts: databases
  become: true
  roles:
    - mariadb

- name: Configure web servers
  hosts: webservers
  become: true
  roles:
    - php
    - nginx
    - myapp

- name: Verify deployment
  hosts: webservers
  tasks:
    - name: Verify application responds with HTTP 200
      uri:
        url: "http://{{ ansible_host }}/health"
        status_code: 200
      register: health_check

    - name: Confirm database connectivity
      uri:
        url: "http://{{ ansible_host }}/db-check"
        status_code: 200

    - name: Deployment summary
      debug:
        msg: "Deployment complete on {{ inventory_hostname }} — app is healthy"

Running the Deployment

# Full deployment
ansible-playbook -i inventory/ site.yml --vault-password-file .vault_pass

# Dry run first (always do this in production)
ansible-playbook -i inventory/ site.yml --vault-password-file .vault_pass --check --diff

# Deploy only database changes
ansible-playbook -i inventory/ site.yml --vault-password-file .vault_pass --tags mariadb

# Deploy only app code
ansible-playbook -i inventory/ site.yml --vault-password-file .vault_pass --tags myapp

# Limit to one host for testing
ansible-playbook -i inventory/ site.yml --vault-password-file .vault_pass --limit web01

Portfolio Documentation Requirements

Your project README should include:

  • Architecture diagram showing the three-node setup
  • Prerequisites list (Ansible version, Python dependencies, required collections)
  • Step-by-step deployment instructions
  • Variable reference table documenting every configurable parameter
  • Secrets management instructions (how to create and use the vault)
  • Idempotency verification instructions
  • A link to a screen recording of the deployment running end to end

Try This: Add Rolling Deployment

Modify the web tier play to use serial: 1 and max_fail_percentage: 0. This implements a rolling deployment — web servers are updated one at a time, and the deployment stops immediately if any server fails. This pattern ensures the application is never fully offline during deployment, simulating a real production rolling update strategy.

Summary

The LAMP stack capstone project applies every skill from the course in a coherent, production-realistic scenario. The project structure follows professional conventions with separated roles, encrypted secrets, group variables, and a master playbook that orchestrates the full deployment. The verification play confirms the application is healthy after deployment. Tag-based partial runs and the rolling deployment pattern demonstrate operational maturity. This project is the centrepiece of your Ansible portfolio.

Leave a Comment