Linux Infrastructure Automation with Ansible
Built and validated repeatable Linux configuration management across two Ubuntu nodes using Ansible roles, SSH-based administration, centralized inventory, templated NGINX configuration, independent verification and idempotent execution.
Project Overview
I built and validated a centralized Linux configuration-management workflow using Ansible. The automation applied a common operating-system baseline, managed users and filesystem paths, configured NGINX from version-controlled templates and independently verified the resulting system and service state.
The implementation was tested against two reproducible Ubuntu nodes in a containerized test environment. Docker provided the temporary Linux execution environment, while Ansible used SSH and standard Linux administration patterns to configure and verify the managed nodes.
Engineering Problem
Configuring Linux systems manually one host at a time creates inconsistency and makes repeated administration difficult to verify. The project needed a repeatable way to apply the same operating-system and service baseline across multiple nodes while keeping host configuration, environment values and service logic version-controlled.
- Centralize host targeting and SSH connection settings.
- Apply the same Linux baseline consistently across multiple nodes.
- Separate reusable operating-system and service responsibilities into roles.
- Keep environment-specific values outside role task logic.
- Manage NGINX configuration from version-controlled templates.
- Avoid unnecessary service reloads when configuration has not changed.
- Verify the resulting service state independently from configuration execution.
- Prove repeatability through an idempotent second run.
Solution Architecture
The Ansible controller loaded the development inventory and shared variables, connected to each Ubuntu node over SSH and used privilege escalation for administrative changes. The common and nginx roles converged both nodes toward the intended state. A separate verify.yml playbook then checked the resulting configuration and service health.
What I Automated
- Centralized inventory for the two managed Ubuntu nodes.
- Shared environment values through
group_vars. - Common Linux administration packages.
- Creation of the
platformopsuser and group. - Managed SSH directory and authorized-key configuration.
- Creation of required platform filesystem paths with defined ownership and permissions.
- NGINX package and service configuration.
- Jinja2-rendered NGINX and status-page configuration.
- Change-driven NGINX reload handling.
- Independent post-configuration verification through
verify.yml.
The repository separates inventory, variables, playbooks, roles, templates and verification instead of placing all automation logic in a single playbook:
cloud-platform-ansible/
├── ansible.cfg
├── docker-compose.yml
├── inventories/
│ └── dev/
│ ├── hosts.yml
│ └── group_vars/
│ └── all.yml
├── playbooks/
│ ├── site.yml
│ └── verify.yml
└── roles/
├── common/
│ └── tasks/main.yml
└── nginx/
├── tasks/main.yml
├── handlers/main.yml
└── templates/
├── default.conf.j2
└── index.html.j2 Configuration & Service Management
Inventory and Shared Variables
Host definitions were maintained in inventories/dev/hosts.yml, while environment and platform values were kept in group_vars. This prevented host-specific configuration from being duplicated throughout the role logic.
all:
children:
linux_servers:
hosts:
node1:
ansible_host: node1
node2:
ansible_host: node2
vars:
ansible_user: ansible
ansible_port: 22
ansible_python_interpreter: /usr/bin/python3 Common Linux Baseline
The common role handled shared operating-system configuration including administration packages, the operations account and managed filesystem paths.
environment_name: dev
platform_user: platformops
platform_group: platformops
platform_directories:
- /opt/platform
- /var/log/platform - name: Create managed platform directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ platform_user }}"
group: "{{ platform_group }}"
mode: "0755"
loop: "{{ platform_directories }}" NGINX Configuration
The nginx role managed package installation, templated configuration, service state and the health endpoint. Configuration changes notified a reload handler only when Ansible detected a change.
- name: Deploy managed NGINX configuration
ansible.builtin.template:
src: default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
notify: Reload NGINX
- name: Validate NGINX configuration
ansible.builtin.command:
cmd: nginx -t
changed_when: false The managed NGINX configuration exposed a deterministic local health endpoint:
location = /health {
default_type text/plain;
return 200 "healthy\n";
} Challenges & How I Addressed Them
A Successful Playbook Did Not Automatically Prove Service Health
A configuration run can complete without proving that the resulting application endpoint behaves as expected. I separated configuration convergence from operational verification by creating verify.yml. It independently checked NGINX syntax, the expected operations account, HTTP status and health-response content.
NGINX Reloads Needed Protection from Invalid Configuration
A templated configuration change should not be treated as successful if the generated NGINX syntax is invalid. I included nginx -t validation before handler processing so an invalid configuration causes the play to fail instead of proceeding as though the service change was healthy.
Service Actions Should Occur Only When Configuration Changes
Reloading NGINX on every automation run would create unnecessary service activity. The template task notifies the NGINX handler only when Ansible detects a configuration change, keeping service actions tied to actual configuration drift.
Repeat Runs Needed to Converge Without Reapplying the Same Changes
Repeatability was treated as a testable requirement rather than an assumption. After the nodes reached the desired state, I executed the configuration playbook again and confirmed changed=0 on both nodes.
Validation & Idempotency
The independent verification workflow checked four operational conditions on each node:
- NGINX configuration passed
nginx -t. - The expected
platformopsaccount existed. - The local
/healthendpoint returned HTTP200. - The response body contained the expected
healthystate.
- name: Verify HTTP health endpoint
ansible.builtin.uri:
url: http://127.0.0.1/health
method: GET
return_content: true
status_code: 200
register: health_result
- name: Assert application health
ansible.builtin.assert:
that:
- health_result.status == 200
- "'healthy' in health_result.content" I then reran the main configuration playbook after convergence:
node1 : ok=13 changed=0 unreachable=0 failed=0 skipped=1
node2 : ok=13 changed=0 unreachable=0 failed=0 skipped=1 The second run confirmed that resources already matching the declared configuration were not continuously modified.
Security Controls
- SSH public-key authentication was configured for Ansible connectivity.
- Password-based SSH authentication was disabled on the managed nodes.
- Direct root SSH login was disabled.
- Automation connected through a dedicated non-root
ansibleaccount and used privilege escalation for administrative tasks. - A separate
platformopsoperations account was created through configuration management. - Local SSH private-key material was excluded from Git tracking.
- Repository line-ending controls were used for Linux shell, YAML, template, Dockerfile and configuration content.
Verified Evidence
ANSIBLE CONNECTIVITY
node1 SUCCESS
node2 SUCCESS
REPEAT CONFIGURATION RUN
node1 changed=0 failed=0
node2 changed=0 failed=0
HTTP HEALTH VALIDATION
node1 200 healthy
node2 200 healthy
VERIFY PLAYBOOK
node1 ok=4 changed=0 failed=0
node2 ok=4 changed=0 failed=0
MANAGED HOST IDENTITY
node1 ansible-node1
node2 ansible-node2 The evidence confirms SSH connectivity, configuration convergence, NGINX health, expected host identity and idempotent repeat execution across both managed nodes.
Production Considerations
The project validates the configuration-management pattern in a reproducible Linux environment. For a larger production estate I would retain the same role-based model while extending the surrounding controls with:
- Cloud- or CMDB-backed dynamic inventory instead of static host definitions.
- Ansible Vault or an external secrets platform for automation-managed sensitive values.
- More narrowly scoped privilege-escalation permissions.
ansible-lint, syntax validation and automated role testing in CI.- Separate inventory groups and variables for development, staging and production.
- Centralized execution logging and change reporting for operational auditability.
Source Code & Evidence
The implementation is version-controlled in the public repository below, including inventory, variables, reusable roles, Jinja2 templates, configuration playbooks, the independent verification workflow and the reproducible environment definition.
View the Ansible Repository on GitHub →
Engineering Value
This project demonstrates practical Linux configuration management rather than simply executing Ansible commands. I separated host data from automation logic, organized configuration into reusable roles, managed Linux users and filesystem state, rendered service configuration from templates, controlled service actions with handlers and validated the final operating state independently.
It also demonstrates the operational discipline behind repeatable automation: secure SSH administration, configuration validation before service reload, measurable HTTP health checks and a second execution proving changed=0 across both managed nodes.