# Automating Package Installation on Multiple Servers Using Ansible

If you are managing multiple Linux servers, installing packages manually on every server can become repetitive and time-consuming.

With **Ansible**, we can automate package installation across multiple servers using a simple playbook. In this example, we will:

*   Define a list of packages using variables
    
*   Install multiple packages using a loop
    
*   Use `apt` to install packages on Ubuntu/Debian servers
    
*   Display a message for every package being processed
    
*   Verify the installation
    

This is a beginner-friendly Ansible exercise and is useful for understanding **variables, loops, tasks, and package management**.

* * *

## 📌 What We Are Going to Build

Our Ansible playbook will install the following packages:

```text
tree
zip
unzip
wget
java
apache2
```

The basic structure will look like this:

```text
Ansible Controller
       |
       | SSH
       |
       +-------------------+
       |                   |
   Server 1             Server 2
   Ubuntu               Ubuntu
       |                   |
       +---- Install packages
```

Instead of logging into each server and running:

```bash
sudo apt install tree
sudo apt install zip
sudo apt install unzip
sudo apt install wget
sudo apt install apache2
```

we can let Ansible perform the work automatically.

* * *

# 1\. Prerequisites

Before starting, make sure you have:

*   Ansible installed on the control machine
    
*   One or more Ubuntu/Debian servers
    
*   SSH connectivity between the Ansible controller and managed servers
    
*   A user with `sudo` privileges
    

Check Ansible:

```bash
ansible --version
```

Test connectivity to your servers:

```bash
ansible servers -m ping
```

Expected output:

```text
SUCCESS
"ping": "pong"
```

* * *

# 2\. Ansible Inventory

First, create an inventory file.

For example:

```ini
[servers]
server1 ansible_host=SERVER_1_IP
server2 ansible_host=SERVER_2_IP

[servers:vars]
ansible_user=ubuntu
```

Replace the IP addresses with the private/public IP addresses of your servers as appropriate for your environment.

You can test the inventory with:

```bash
ansible-inventory --graph
```

* * *

# 3\. Create the Ansible Playbook

Create a file:

```bash
nano install-packages.yml
```

Add the following:

```yaml
---
- name: Install packages
  hosts: servers
  become: yes

  vars:
    packages:
      - tree
      - zip
      - unzip
      - wget
      - default-jre
      - apache2

  tasks:

    - name: Install packages
      apt:
        name: "{{ item }}"
        state: present
        update_cache: yes
      loop: "{{ packages }}"

    - name: Show installation
      debug:
        msg: "Installing {{ item }}"
      loop: "{{ packages }}"
```

> **Note:** Your handwritten notes appear to use `jv`; if you meant Java, `default-jre` is a suitable Ubuntu/Debian package name. You can also use `default-jdk` if you need the Java development tools.
> 
> ![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/c474d173-ae04-4696-83bb-85e7c5d4fb5a.jpg align="center")

* * *

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/8bac4574-6575-4986-8eab-257f9532a995.jpg align="center")

# 4\. Understanding the Playbook

Let's understand each section.

## Play Name

```yaml
- name: Install packages
```

This gives a name to our Ansible play.

It makes the playbook output easier to understand.

* * *

## Selecting the Servers

```yaml
hosts: servers
```

This tells Ansible to run the playbook against the group named `servers` in our inventory.

For example:

```ini
[servers]
server1
server2
```

Both servers will receive the tasks.

* * *

# 5\. Using Become

```yaml
become: yes
```

Package installation requires administrative privileges.

`become: yes` tells Ansible to use privilege escalation, normally through `sudo`.

It is similar to manually running:

```bash
sudo apt install tree
```

* * *

# 6\. Defining Packages Using Variables

This is one of the important parts of the exercise:

```yaml
vars:
  packages:
    - tree
    - zip
    - unzip
    - wget
    - default-jre
    - apache2
```

Here, `packages` is a variable containing a list of package names.

Instead of writing separate tasks for every package, we create one list.

For example:

```yaml
packages:
  - tree
  - zip
  - unzip
```

This makes the playbook easier to maintain.

If we want to install another package later, we simply add it:

```yaml
packages:
  - tree
  - zip
  - unzip
  - wget
  - default-jre
  - apache2
  - git
```

No additional installation task is required.

* * *

# 7\. Installing Packages Using a Loop

The main task is:

```yaml
- name: Install packages
  apt:
    name: "{{ item }}"
    state: present
    update_cache: yes
  loop: "{{ packages }}"
```

Here we use the Ansible `apt` module.

The important part is:

```yaml
loop: "{{ packages }}"
```

Ansible takes every value from the `packages` list and assigns it to:

```yaml
{{ item }}
```

For example, during execution:

```text
item = tree
item = zip
item = unzip
item = wget
item = default-jre
item = apache2
```

So Ansible effectively performs the task for each package.

* * *

# 8\. What Does `{{ item }}` Mean?

This is a very important Ansible concept.

Consider:

```yaml
loop:
  - tree
  - zip
  - unzip
```

When Ansible runs the loop:

```text
{{ item }}
```

represents the current value.

For example:

```text
First iteration  → item = tree
Second iteration → item = zip
Third iteration  → item = unzip
```

Therefore:

```yaml
name: "{{ item }}"
```

becomes:

```yaml
name: tree
```

then:

```yaml
name: zip
```

then:

```yaml
name: unzip
```

* * *

# 9\. Checking What Is Being Installed

The second task uses the `debug` module:

```yaml
- name: Show installation
  debug:
    msg: "Installing {{ item }}"
  loop: "{{ packages }}"
```

This doesn't install anything.

It simply displays a message for every item in the list.

For example:

```text
Installing tree
Installing zip
Installing unzip
Installing wget
Installing default-jre
Installing apache2
```

This is useful when learning loops and when debugging playbooks.

* * *

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/923c4385-348a-4942-a68e-573e32bdcf14.jpg align="center")

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/6959a883-53a2-4f93-a252-f47704607ce6.jpg align="center")

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/2043cd6f-4151-4617-8b04-3c74041ac3eb.jpg align="center")

# 10\. Run the Playbook

Now execute:

```bash
ANSIBLE_HOST_KEY_CHECKING=false ansible-playbook -i ../host.ini package-variable2.yml -v
```

You should see output similar to:

```text
PLAY [Install packages] ****************************************

TASK [Gathering Facts] *****************************************
ok: [server1]
ok: [server2]

TASK [Install packages] ***************************************
changed: [server1]
changed: [server2]

TASK [Show installation] **************************************
ok: [server1] => (item=tree)
ok: [server1] => (item=zip)
ok: [server1] => (item=unzip)

PLAY RECAP ****************************************************
server1 : ok=3 changed=1 failed=0
server2 : ok=3 changed=1 failed=0
```

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/f2065d99-4112-4d6b-af5e-e2a0b7591e48.png align="center")

The exact output will vary depending on whether the packages are already installed.

* * *

# 11\. Verify the Packages

After running the playbook, connect to one of your Ubuntu servers:

```bash
ssh ubuntu@SERVER_IP
```

Check `tree`:

```bash
tree --version
```

Check `wget`:

```bash
wget --version
```

Check Apache:

```bash
apache2 -v
```

Check Java:

```bash
java -version
```

Check ZIP:

```bash
zip -v
```

Check UNZIP:

```bash
unzip -v
```

* * *

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/861b0ba4-499e-4ac8-9594-197bc900f3d4.png align="center")

![](https://cdn.hashnode.com/uploads/covers/6a0ab1013104e2aff0249a0b/a08fbe03-6865-4f1f-aa46-4e4930e9823c.png align="center")

Run:

# Why Use Ansible for This?

Imagine you have **50 Ubuntu servers**.

Without Ansible, you might need to connect to every server and manually install packages.

With Ansible, you define the desired configuration once:

```yaml
packages:
  - tree
  - zip
  - unzip
  - wget
  - apache2
```

Then Ansible applies it to all servers:

```bash
ansible-playbook -i inventory install-packages.yml
```

This provides:

*   ⚡ Automation
    
*   🔄 Repeatability
    
*   📦 Consistent server configuration
    
*   ⏱️ Time savings
    
*   📈 Easy scaling
    
*   🛠️ Less manual work
    

* * *

### Lists

```yaml
packages:
  - tree
  - zip
  - unzip
```

A list can contain multiple values.

### Loops

```yaml
loop: "{{ packages }}"
```

Loops allow us to execute the same task for multiple items.

### `item`

```yaml
"{{ item }}"
```

`item` represents the current value during a loop.

### `apt` Module

```yaml
apt:
```

Used to manage packages on Debian/Ubuntu systems.

### `become`

```yaml
become: yes
```

Allows Ansible to execute tasks with elevated privileges.

### `debug`

```yaml
debug:
```

Useful for displaying information while running a playbook.

In this exercise, I learned how to automate **package installation on multiple Ubuntu servers using Ansible**.

Instead of manually installing each package on every server, I used an Ansible variable to store the package list and a loop to process each package.

The most important part of the playbook is:

```yaml
loop: "{{ packages }}"
```

combined with:

```yaml
name: "{{ item }}"
```

This simple concept is extremely useful when writing scalable Ansible playbooks.

* * *

* * *
