Ansible Project for Beginners: Provisioning & Configuration Management (Day 7)
This project demonstrates how to use Ansible for both cloud provisioning and configuration management. Based on a real-world technical interview assignment, this guide covers how to provision heterogeneous EC2 instances on AWS, configure secure passwordless SSH authentication, and use Ansible loops and conditionals to perform targeted administrative tasks (such as shutting down specific operating system families).
1. Architectural Overview & The Interview Assignment
In production environments, Ansible is frequently used in two distinct modes:
- Provisioning (Control Plane Automation): Interacting with cloud APIs (like AWS, Azure, or GCP) to create infrastructure.
- Configuration Management (Data Plane Automation): Connecting directly to servers via SSH to configure software, manage files, or execute system commands.
This project is structured around a three-task assignment designed to test your understanding of both modes, along with advanced Ansible concepts like loops, conditionals, and idempotency.
The Three Assignment Tasks
- Task 1: Provisioning: Create three EC2 instances on AWS using Ansible loops. Two instances must run Ubuntu, and one must run Amazon Linux.
- Task 2: Passwordless Authentication: Establish passwordless SSH authentication from your Ansible control node to all three newly created managed nodes.
- Task 3: Targeted Configuration Management: Write an Ansible playbook to automate the shutdown of only the Ubuntu instances, leaving the Amazon Linux instance running. This must be achieved using dynamic conditionals (
whenstatements) rather than manual inventory grouping.
2. Prerequisites & AWS IAM Setup
Before executing Ansible playbooks against AWS, you must configure authentication and install the necessary local dependencies.
Step 1: Create an IAM User on AWS
Ansible's AWS modules interact with the AWS API. You must create an Identity and Access Management (IAM) user with programmatic access:
- Navigate to the IAM Console on AWS.
- Create a user named
ansible-user. - Attach the
AmazonEC2FullAccesspolicy directly to this user (for production, follow the principle of least privilege). - Navigate to the Security Credentials tab of the user and select Create access key.
- Choose Application running outside AWS and download the generated Access Key ID and Secret Access Key.
Step 2: Install Local Dependencies
Your control node requires Python, the AWS SDK (boto3), and the Ansible Amazon AWS collection. Run the following commands on your control node:
# Install the Python AWS SDK
pip install boto3
# Install the Ansible AWS Collection
ansible-galaxy collection install amazon.aws
3. Securing Credentials with Ansible Vault
Hardcoding AWS access keys in plain text within playbooks is a severe security risk. Ansible Vault allows you to encrypt sensitive variables at rest.
Step 1: Create a Vault Password File
To avoid typing the vault password every time you run a playbook, store a secure key in a local file (and ensure this file is added to your .gitignore):
# Generate a base64 encoded password file
openssl rand -base64 2048 > .vault_pass.txt
Step 2: Create the Encrypted Variables File
Create an encrypted file named password.yaml to store your AWS credentials:
ansible-vault create password.yaml --vault-password-file .vault_pass.txt
Inside the encrypted file, define your AWS credentials:
ec2_access_key: "AKIAIOSFODNN7EXAMPLE"
ec2_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
To edit this file later, use the edit command:
ansible-vault edit password.yaml --vault-password-file .vault_pass.txt
4. Task 1: Provisioning EC2 Instances (The Idempotency Trap)
The Concept of Idempotency
A core principle of Ansible is idempotency. Mathematically, an operation is idempotent if applying it multiple times yields the same result as applying it once:
In shell scripting, running a creation command twice typically results in an error or duplicates the resource. Ansible, however, checks the current state of the target system and only applies changes if the current state does not match the desired state.
The Interview Trap
If you write a naive Ansible loop that iterates only over a list of AMI IDs to create three instances, Ansible's idempotency will cause the playbook to provision only two instances.
Because the two Ubuntu instances share the exact same AMI ID, security group, key pair, and subnet, Ansible evaluates the second iteration of the Ubuntu AMI and determines that an instance matching those exact specifications already exists. It will skip the task, assuming the desired state has already been reached.
The Solution: Multi-Variable Loops
To force Ansible to recognize them as distinct resources, you must pass unique metadata (such as distinct names or tags) for each instance. This is achieved by looping over a list of dictionaries containing both the AMI ID and a unique name.
5. Writing the Provisioning Playbook
Create a file named ec2_create.yaml. This playbook runs on localhost with a local connection because it communicates directly with the AWS API rather than SSHing into a target server.
---
- name: Provision Heterogeneous EC2 Instances
hosts: localhost
connection: local
gather_facts: false
vars_files:
- password.yaml
vars:
region: "ap-south-1" # Change to your nearest AWS region
key_name: "abi-aws-key" # Must match an existing EC2 Key Pair name in your AWS account
tasks:
- name: Provision EC2 Instances using Loops
amazon.aws.ec2_instance:
aws_access_key: "{{ ec2_access_key }}"
aws_secret_key: "{{ ec2_secret_key }}"
region: "{{ region }}"
key_name: "{{ key_name }}"
instance_type: t2.micro
image_id: "{{ item.image }}"
wait: true
network:
assign_public_ip: true
tags:
Name: "{{ item.name }}"
Environment: Development
loop:
- { name: "manage-node-1", image: "ami-007020fd9c84e18c7" } # Ubuntu AMI
- { name: "manage-node-2", image: "ami-007020fd9c84e18c7" } # Ubuntu AMI
- { name: "manage-node-3", image: "ami-013e83f579886ba14" } # Amazon Linux 2023 AMI
Run the Provisioning Playbook
Execute the playbook by passing the vault password file:
ansible-playbook ec2_create.yaml --vault-password-file .vault_pass.txt
Verify on your AWS EC2 Console that three instances (manage-node-1, manage-node-2, and manage-node-3) are successfully initializing.
6. Task 2: Setting Up Passwordless SSH Authentication
Once the instances are running, you must configure passwordless SSH access from your control node to the managed nodes. This is a prerequisite for configuration management.
Step 1: Locate Your Private Key
Ensure the private key (.pem file) corresponding to the key_name specified in your playbook is downloaded on your control node and has secure permissions:
chmod 400 ~/Downloads/abi-aws-key.pem
Step 2: Copy the Public Key to Managed Nodes
Use ssh-copy-id to install your public key onto the remote instances. Note that different Linux distributions use different default SSH usernames:
Run the following commands, replacing the IP addresses with the public IPs of your newly provisioned instances:
# Configure passwordless SSH for Amazon Linux (manage-node-3)
ssh-copy-id -f -i ~/Downloads/abi-aws-key.pem ec2-user@<PUBLIC_IP_NODE_3>
# Configure passwordless SSH for Ubuntu (manage-node-1)
ssh-copy-id -f -i ~/Downloads/abi-aws-key.pem ubuntu@<PUBLIC_IP_NODE_1>
# Configure passwordless SSH for Ubuntu (manage-node-2)
ssh-copy-id -f -i ~/Downloads/abi-aws-key.pem ubuntu@<PUBLIC_IP_NODE_2>
Step 3: Verify Passwordless Connection
Test the connection. You should be able to log in without explicitly passing the identity (-i) flag:
ssh ubuntu@<PUBLIC_IP_NODE_1>
exit
7. Task 3: Targeted Configuration Management (Conditionals & Facts)
The goal of Task 3 is to shut down only the Ubuntu instances. To make this dynamic and robust, we use Ansible Facts and Conditionals instead of static inventory groups.
Understanding Ansible Facts
When a playbook runs with gather_facts: true (the default setting), Ansible connects to the target host and queries a wide array of system metadata (IP addresses, OS family, memory, disk space, etc.).
Debugging Workflow: Finding the Correct Fact
If you are unsure of the exact variable name or structure of the gathered facts, you can print them using the ansible.builtin.debug module:
- name: Print All Gathered Facts
hosts: all
tasks:
- name: Debug Facts
ansible.builtin.debug:
var: ansible_facts
Running this debug task reveals that the operating system family is stored under the key os_family:
- For Ubuntu,
ansible_facts['os_family']is"Debian". - For Amazon Linux,
ansible_facts['os_family']is"RedHat".
8. Writing the Stop Playbook & Inventory
Step 1: Create the Inventory File
Create an inventory file named inventory.ini containing the connection details for your three managed nodes:
[all]
node1 ansible_host=<PUBLIC_IP_NODE_1> ansible_user=ubuntu
node2 ansible_host=<PUBLIC_IP_NODE_2> ansible_user=ubuntu
node3 ansible_host=<PUBLIC_IP_NODE_3> ansible_user=ec2-user
Step 2: Create the Stop Playbook (ec2_stop.yaml)
This playbook connects to all hosts, gathers their system facts, and conditionally executes a system shutdown command only if the host belongs to the Debian (Ubuntu) OS family.
---
- name: Conditional Instance Shutdown
hosts: all
become: true # Required for administrative privileges to run shutdown
gather_facts: true
tasks:
- name: Shut down Ubuntu instances only
ansible.builtin.command: /sbin/shutdown -h now
when: ansible_facts['os_family'] == "Debian"
Note: The
/sbin/shutdown -h nowcommand initiates an immediate system halt. Because this terminates the SSH connection, Ansible may report a "Failed to connect to the host via ssh" or "Connection closed" error for the stopped hosts. This is expected behavior when executing remote shutdown commands.
9. Complete Execution Flow & Verification
Step 1: Run the Stop Playbook
Execute the stop playbook against your inventory:
ansible-playbook -i inventory.ini ec2_stop.yaml
Step 2: Observe the Output
Observe the execution output in your terminal:
PLAY [Conditional Instance Shutdown] ***************************************************
TASK [Gathering Facts] *****************************************************************
ok: [node1]
ok: [node2]
ok: [node3]
TASK [Shut down Ubuntu instances only] *************************************************
changed: [node1]
changed: [node2]
skipping: [node3]
PLAY RECAP *****************************************************************************
node1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0
node2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0
node3 : ok=1 changed=0 unreachable=0 failed=0 skipped=1
node1&node2(Ubuntu): The conditionansible_facts['os_family'] == "Debian"evaluated totrue. The shutdown command was executed (changed=1).node3(Amazon Linux): The condition evaluated tofalsebecause its OS family isRedHat. The task was safely skipped (skipped=1).
Step 3: Verify on AWS Console
Navigate back to your AWS EC2 Console. You will see:
manage-node-1andmanage-node-2are in the Stopped state.manage-node-3remains in the Running state.
Summary and Key Takeaways
- Dual-Mode Ansible: Ansible serves as both a cloud provisioning tool (communicating locally with cloud APIs via Python libraries like
boto3) and a configuration management tool (communicating remotely over SSH). - Local Connection Mode: When provisioning cloud resources, set
hosts: localhostandconnection: localbecause there is no remote server to SSH into during the creation phase. - The Idempotency Principle: Ansible evaluates the desired state of a resource. If you attempt to create multiple identical resources in a loop, Ansible will skip subsequent iterations unless you introduce unique identifying attributes (such as distinct names or tags).
- Ansible Vault: Sensitive credentials like AWS access keys should always be encrypted using Ansible Vault and decrypted at runtime using a secure password file.
- Multi-Variable Loops: Use lists of dictionaries (
loop: - { name: x, image: y }) to pass multiple distinct parameters to a single task block. - Dynamic Conditionals: Use the
whenstatement combined with gathered facts (ansible_facts['os_family']) to write flexible, cross-platform playbooks that adapt dynamically to the target operating system. - Fact Gathering Debugging: Use the
ansible.builtin.debugmodule to dumpansible_factswhen you need to inspect the exact system variables available on your managed nodes.