Skip to main content
All Projects

DMI Internship

Production-Grade Deployment — Terraform + Ansible Roles (Azure)

April 2026Repository
TerraformAnsibleAzureIaCDevOpsNginxNode.jsMySQLPM2Linux

Deployed the EpicBook full-stack web application on Azure using Terraform for infrastructure and a three-role Ansible architecture — common (system baseline and SSH hardening), nginx (reverse proxy configuration via Jinja2 template), and epicbook (MySQL provisioning, Node.js, PM2, schema seeding, and application deployment).

Production-Grade Deployment — Terraform + Ansible Roles (Azure)

Overview

This project moves from a flat Ansible playbook to a production-grade role architecture — the pattern real teams use when configuration management needs to be modular, reusable, and maintainable across environments. Terraform provisions the Azure infrastructure, and three Ansible roles handle the full application stack in a clean, ordered sequence: system baseline first, then web server configuration, then application deployment.

The application itself is not a static site. EpicBook is a full-stack Node.js web application backed by MySQL, served behind an Nginx reverse proxy — a more realistic deployment target than a flat HTML directory.

Problem

Flat Ansible playbooks work for simple deployments. They become unmanageable when the deployment involves multiple concerns — system hardening, web server configuration, application runtime, database provisioning, and process management — each with its own tasks, templates, and handlers. The challenge was to structure the automation so that each concern is isolated, each role is independently testable, and the whole deployment is idempotent: safe to re-run without unintended side effects.

Architecture

Terraform provisions the full Azure environment from code:

  • Resource Grouprg-epicbook-prod
  • Virtual Network10.0.0.0/16 with Subnet 10.0.1.0/24
  • NSG — inbound TCP 22 and TCP 80 only
  • Static Public IP + NIC — associated to the VM
  • Ubuntu 22.04 VM (Standard_B2ats_v2) — key-based SSH, password authentication disabled
  • Two Terraform outputspublic_ip and admin_user, consumed directly by Ansible inventory

Ansible runs three roles in sequence against the provisioned host:

Role: common
  apt update + dist-upgrade
  Install baseline packages (git, curl, unzip, python3-pip)
  Disable root SSH login   → handler: restart ssh
  Disable password auth    → handler: restart ssh

Role: nginx
  Install Nginx
  Deploy epicbook.conf from Jinja2 template → reverse proxy port 80 to app_port
  Enable site (symlink sites-available → sites-enabled)
  Remove default Nginx site
  Ensure Nginx started and enabled → handler: reload nginx

Role: epicbook
  Install MySQL, ensure started and enabled
  Install Node.js 20 via NodeSource
  Install PM2 globally
  Clone https://github.com/pravinmishraaws/theepicbook to app_dest
  Set www-data ownership recursively
  npm install dependencies
  Create MySQL database, import schema, import seed data (guarded with creates:)
  Deploy config.json from Jinja2 template
  Start application with PM2 → handler: reload nginx

Traffic flow:  Browser → Nginx :80 → proxy_pass → Node.js/PM2 :8080

Group Variables

All role-shared configuration lives in group_vars/web.yml:

app_repo:  https://github.com/pravinmishraaws/theepicbook.git
app_dest:  /var/www/epicbook
app_user:  www-data
app_port:  8080
db_name:   bookstore
db_user:   root

Variables are referenced across roles via Jinja2 ({{ app_dest }}, {{ app_port }}), keeping the roles DRY — changing the deployment path or port requires editing one file, not hunting through tasks.

Technologies Used

  • Terraform — Infrastructure as Code, Azure provider ~> 3.100
  • Ansible — Role-based configuration management and deployment automation
  • Azure — Resource Group, VNet, NSG, Public IP, NIC, Ubuntu 22.04 VM
  • Nginx — Reverse proxy: Jinja2-templated site config, port 80 → app_port
  • Node.js 20 + PM2 — Application runtime installed via NodeSource; PM2 manages the process
  • MySQL — Database provisioned, schema applied, seed data imported via Ansible
  • GitHub Actions — CI: Terraform format check and Ansible syntax check on every push

Key Engineering Decisions

  • Role boundaries are enforcedcommon handles OS concerns, nginx handles web server concerns, epicbook handles application concerns. No task crosses a role boundary.
  • Handlers are role-scoped — each role defines only the handlers it owns. The nginx role reloads Nginx; the common role restarts SSH. Neither role reaches into the other.
  • Idempotency for database operations — schema and seed imports are guarded with creates: /tmp/epicbook_schema_loaded. The import runs once and never again, regardless of how many times the playbook is re-run.
  • Jinja2 templates for config injectionepicbook.conf.j2 renders app_port from group_vars into the Nginx site config. config.json.j2 renders database connection details. Environment-specific values never appear as literals in tasks.
  • SSH hardening in common — disabling root login and password authentication is a baseline control, not optional. It runs on every host before any application layer is touched.
  • Outputs expose what Ansible needs — Terraform outputs both public_ip and admin_user, making the handoff from infrastructure to configuration explicit and auditable.

Results

Terraform provisioned the full Azure environment in a single terraform apply. Ansible ran all three roles without failures. The EpicBook application loaded correctly in the browser via http://<public_ip>.

A second playbook run confirmed idempotency — tasks that had nothing to change returned ok or unchanged, with no unintended modifications to the running system.

Key Learnings

The step from a flat playbook to roles is not cosmetic. Roles enforce a discipline that flat playbooks allow you to avoid: each concern must be defined in isolation, handlers must be scoped correctly, and shared configuration must be centralised in group variables rather than duplicated across tasks.

Idempotency matters more than it appears on first run. A deployment that works once is useful. A deployment that can be re-run at any time — to verify state, apply a change, or recover from drift — is the foundation of reliable infrastructure operations. The creates: guards on database imports are a small detail with a significant operational consequence: they make the playbook safe to run against a live system without destroying data.