ALEX C. GODWIN | Cloud & DevOps Engineer
INFRASTRUCTURE AUTOMATION PROJECT

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.

AnsibleLinuxConfiguration ManagementSSHJinja2NGINX

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.

MANAGED NODES2 Ubuntu Nodesnode1 + node2
IDEMPOTENCYchanged=0Both nodes on repeat run
HEALTH CHECKHTTP 200Both NGINX endpoints healthy
VERIFICATIONfailed=0Dedicated validation playbook

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.

CONTROL PLANE Ansible Controller ansible.cfg • inventory • group_vars
CONFIGURATION site.yml common role + nginx role
↓ SSH / privilege escalation
MANAGED NODE node1 Ubuntu • NGINX • /health
MANAGED NODE node2 Ubuntu • NGINX • /health
ASSURANCE verify.yml syntax • user • HTTP health • assertions

What I Automated

  • Centralized inventory for the two managed Ubuntu nodes.
  • Shared environment values through group_vars.
  • Common Linux administration packages.
  • Creation of the platformops user 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 platformops account existed.
  • The local /health endpoint returned HTTP 200.
  • The response body contained the expected healthy state.
- 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 ansible account and used privilege escalation for administrative tasks.
  • A separate platformops operations 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.