← Back to list

How I Automated AWS Infrastructure With Ansible — A Beginner’s Honest Journey

Ansible playbooks, EC2 provisioning, passwordless SSH, and conditionals explained through real mistakes, real errors, and one very…

Henry Unaeze · 2026-06-16 11:39 · 0 claps · 8.5 min read
#ansible #passwordless #cloud-computing #ec2-instance #configuration-management
Open on Medium ↗
Wiki topics: BIZ · Business Strategy ☁️ · DevOps & Cloud

How I Automated AWS Infrastructure With Ansible — A Beginner’s Honest Journey

Ansible playbooks, EC2 provisioning, passwordless SSH, and conditionals explained through real mistakes, real errors, and one very stressful morning

How I Automated AWS Infrastructure With Ansible

How I Automated AWS Infrastructure With Ansible

If you read my first article, you know where this started. A bootcamp. A terminal. One command spinning up Apache Airflow from nothing. That moment of pure fascination sent me down a path I had no idea would consume me this completely.

Part 1 ended with a live Stripe payment app running on an AWS EC2 server. Real IP address. Real internet. I did it all by hand — SSH’d in, installed packages one by one, typed every command myself, made every mistake personally.

It worked. I celebrated.Then I sat back and thought about something uncomfortable. That was one server. What happens when there are ten?

The Honest Problem With What I Did Before

Here is the exact process I followed in Part 1 to get my app running on EC2:

1. SSH into the server manually
2. Install Node.js manually
3. Clone the project manually
4. Create the .env file manually
5. Run npm install manually
6. Start PM2 manually
7. Open port 3000 in AWS manually

Seven steps. One server. About forty minutes the first time — longer counting all the errors.

Now multiply that by fifty servers. By a hundred. By a team of engineers who all do it slightly differently and introduce inconsistencies nobody notices until something breaks in production at 2am. That is not infrastructure management. That is organised chaos.

Ansible exists because organised chaos does not scale.

What Ansible Is — Without the Jargon

Ansible lets you write your intentions once and apply them everywhere.

Instead of logging into each server and repeating the same commands, you write a playbook — a simple instruction file — and Ansible carries out those instructions across every server you point it at, simultaneously, consistently, without human error.

Before Ansible — what I did in Part 1:
Me → SSH → one server → type → hope → repeat

After Ansible:
Me → write instructions once
Ansible → all servers at the same time 

The person in me who got into DevOps specifically because I cannot stand doing the same thing twice finally felt like the tools were catching up with my laziness 😊.

The First Thing That Went Wrong — My Environment

Before a single playbook was written I lost a morning to a mistake that taught me something fundamental.

I started working in Git Bash — a Windows terminal that looks and feels like Linux but is not. Ansible does not run on Windows natively. Every command I typed either failed silently or threw confusing errors. I genuinely thought I had broken something.

The fix was switching to WSL — Windows Subsystem for Linux. A real Ubuntu environment living inside my Windows laptop.

Git Bash → looks like Linux, is not Linux → Ansible fails ❌
WSL Ubuntu → is actually Linux → Ansible works perfectly 

This connected directly to the confusion I documented in Part 1 — where I kept running commands on the wrong machine without realising it. The laptop terminal and the EC2 server terminal looked almost identical. I wasted time on both projects for the same underlying reason.

Knowing exactly where you are before you run anything is not a beginner tip. It is a professional discipline.

Protecting Secrets — Learning From My Worst Mistake

Part 1 had a moment I am still embarrassed about.

I committed my Stripe API keys inside a Dockerfile and pushed it to GitHub. GitHub’s secret scanner caught it and blocked the push. But the keys were already exposed in the commit history. I had to rotate them immediately, rewrite my entire Git history with git filter-repo, and start the repository from scratch.

My heart was genuinely racing.

Going into this project I was not going to make the same mistake with AWS credentials. Ansible has a built-in encryption tool called Ansible Vault that handles this properly:

ansible-vault create group_vars/all/pass.yml

The vault file stores credentials encrypted. Unreadable without a password. Never committed to version control.

vault.pass   → the password that unlocks the vault
pass.yml     → the vault holding AWS credentials, encrypted
.gitignore   → ensures neither file ever reaches GitHub

The pain of Part 1 made me do this right from the very start in Part 2. Sometimes the best teacher is the mistake that cost you an hour of panic.

Project 1 — Three Cloud Servers With One Command

What I Was Replacing

In Part 1, creating one server meant navigating the AWS Console — clicking through forms, selecting options, waiting. Creating three would mean doing that three times.

With Ansible, this is the entire process:

---
- hosts: localhost
  connection: local

  tasks:
    - name: Create EC2 instances
      amazon.aws.ec2_instance:
        name: "{{ item.name }}"
        key_name: "ansible_ec2"
        instance_type: t2.micro
        security_group: "default"
        region: "us-east-1"
        aws_access_key: "{{ aws_access_key }}"
        aws_secret_key: "{{ aws_secret_key }}"
        network:
          assign_public_ip: true
        image_id: "{{ item.image_id }}"
      loop:
        - { name: "ansible_ec2_1", image_id: "ami-0521cb2d60cfbb1a6" }
        - { name: "ansible_ec2_2", image_id: "ami-0b6d9d3d33ba97d99" }
        - { name: "ansible_ec2_3", image_id: "ami-0b6d9d3d33ba97d99" }

The loop at the bottom is where the magic is. Ansible reads each item in that list and creates a server for each one — same configuration, applied consistently, no repetition on my end.

Running it:

ansible-playbook PLAYBOOKS/ec2_create.yaml \
  --vault-password-file vault.pass \
  -e @group_vars/all/pass.yml

Three servers. Seconds. No clicking.

Compare that to the manual process I used in Part 1 to set up just one server — and the power of what just happened starts to sink in.

The Mistake I Made Here

I used two different AMI IDs without thinking about the consequences:

ami-0521cb2d60cfbb1a6 → Amazon Linux → connects as: ec2-user
ami-0b6d9d3d33ba97d99 → Ubuntu       → connects as: ubuntu

Different operating systems use different default usernames. When I tried to SSH into all three servers the same way, one refused. I spent time debugging something that was entirely self-inflicted.

Consistency in infrastructure is not a preference. It is a time-saving discipline. Pick one OS and stick with it across your servers unless there is a specific technical reason not to.

Project 2 — Passwordless SSH: Retiring the .pem File

The Connection to Part 1

Every time I connected to my EC2 server in Part 1, the command looked like this:

ssh -i ~/first_server.pem ubuntu@3.80.241.110

Carrying a .pem file. Specifying it every time. Manual. Error-prone.

For one server that is manageable. For Ansible handling multiple servers simultaneously it is impossible. Ansible cannot pause mid-execution and wait for a human to locate a key file.

Passwordless SSH is not a convenience feature. It is the foundation that makes automation possible at all.

Generating the Key Pair

ssh-keygen -t ed25519

This created two mathematically linked files:

~/.ssh/id_ed25519      → private key — stays on my laptop, always
~/.ssh/id_ed25519.pub  → public key — gets copied to every server I manage

The relationship between them is straightforward. The private key signs. The public key verifies. A server that holds your public key will recognise your private key instantly — no password, no .pem file, no human involvement required.

ED25519 is the modern standard. Faster and more secure than the older RSA format. Always use it for new keys.

Copying the Public Key to Each Server

ssh-copy-id -f "-o IdentityFile ~/ansible_ec2.pem" ubuntu@SERVER-IP

This uses the .pem file one final time to gain access and copy the public key into the server's authorized_keys file. After this single step the .pem is retired permanently.

The Result

# What connecting looked like in Part 1
ssh -i ~/first_server.pem ubuntu@3.80.241.110

# What it looks like now
ssh ubuntu@SERVER-IP ✅

Every Part 1 problem with the .pem file — Windows path errors, chmod failures, SSH permission warnings — disappeared completely. Storing the key pair in ~/.ssh/ with proper Linux permissions meant everything worked the way it was supposed to

Project 3 — Teaching Ansible to Think

The Task

Shut down only the Ubuntu instances. Leave everything else running. Do it automatically, without manually checking each server.

Why This Matters

In Part 1 everything was uniform. One server. One OS. One configuration. The real world is never that clean.

Production infrastructure runs different operating systems for different purposes. A playbook that blindly executes the same task on every server regardless of what it finds is not automation — it is a script with good PR. Real automation adapts to what it encounters.

How Ansible Learns About Your Servers

Before executing any task Ansible runs a step called gather_facts. It connects to each server and collects information:

ansible_distribution         → "Ubuntu" or "Amazon"
ansible_os_family            → "Debian" or "RedHat"
ansible_distribution_version → "22.04"
ansible_hostname             → the server's name

Think of it as Ansible conducting a brief interview with each server before deciding what to do next.

The Conditional

when: ansible_distribution == "Ubuntu"

One line. It tells Ansible: run this task only when the server identifies itself as Ubuntu. On everything else — skip it entirely and move on.

The Playbook

---
- hosts: all
  become: true

  tasks:
    - name: Shutdown Ubuntu instances only
      ansible.builtin.command: /sbin/shutdown -h now
      when: ansible_distribution == "Ubuntu"

What Actually Happened:

Ansible connects to all 3 servers
        ↓
Gathers facts from each
        ↓
ansible_ec2_1 → Amazon Linux → condition false → skipped
ansible_ec2_2 → Ubuntu       → condition true  → shutdown
ansible_ec2_3 → Ubuntu       → condition true  → shutdown

One playbook. Intelligent decisions. Zero manual checking.

The Idea That Connected Everything — Idempotency

There is a concept in Ansible that I kept running into without having a name for it.

Run a playbook once — three servers are created. Run it again — nothing changes, servers already exist. Run it a hundred times — same result every time.

That is idempotency. You describe the state you want. Ansible ensures that state exists. If it already exists, Ansible does nothing. If it does not, Ansible creates it.

In Part 1 I ran npm install manually every time something broke. The result depended entirely on what was already installed. It was unpredictable and fragile.

Ansible is the opposite of that. It is built around predictability. The same playbook produces the same infrastructure regardless of when you run it or how many times. That reliability is what makes automation trustworthy enough to use in production.

The Setup Change That Made Everything Easier

Small thing worth mentioning.

In Part 1 I edited every file using nano — typing blind in the terminal with no visual feedback and no syntax highlighting. For a YAML-heavy tool like Ansible, where indentation errors break everything, this was painful.

Midway through this project I discovered I could type code . in my WSL terminal and VS Code would open pointing directly at my Linux files:

cd ~/ansible_project1406
code .

Suddenly I could see all my files, click to open them, and edit YAML with syntax highlighting that caught indentation problems before I even ran anything. It sounds minor. It saved hours.

Looking Back Across Both Projects

The distance between where I started and where I am now is clearer when laid out directly:

Part 1:
One server set up manually
.pem file carried everywhere
Secrets committed to GitHub by accident
Packages installed by hand
Everything done once, by me, slowly

Part 2:
Three servers created with one command
Passwordless SSH — no .pem needed
Secrets encrypted in Ansible Vault from day one
Configuration applied automatically across all servers
Ansible handles the repetition

Every mistake from Part 1 shaped a decision in Part 2. The exposed keys made me use Vault immediately. The .pem path errors made me set up passwordless SSH properly. The confusion between environments made me move everything into WSL before writing a single line.

That is what learning in public does. It forces you to be honest about what went wrong. And being honest about what went wrong is the fastest way to get better.

What Comes Next

✅ Part 1 — Deploy Node.js app to AWS EC2
✅ Part 2 — Automate infrastructure with Ansible
⬜ Part 3 — Terraform IaC
⬜ Part 4 — Docker/conatinarization 
⬜ Part 5 — Kubernetes
 And more ....

Credits

Course: DevOps Zero to Hero by Abhishek Veeramalla — YouTube

Tools used: Ansible · AWS EC2 · WSL2 · Ubuntu · VS Code · Ansible Vault

Henry Unaeze — DevOps Engineer in training 📍 Lincoln, England LinkedIn | GitHub: github.com/HenryUnaeze/devops-project-2026


메타데이터
post_id
da6ce9e16d93
slug
how-i-automated-aws-infrastructure-with-ansible-a-beginners-honest-journey-da6ce9e16d93
url
https://medium.com/@henryunamad/how-i-automated-aws-infrastructure-with-ansible-a-beginners-honest-journey-da6ce9e16d93
canonical_url
https://medium.com/@henryunamad/how-i-automated-aws-infrastructure-with-ansible-a-beginners-honest-journey-da6ce9e16d93
author_url
https://medium.com/@henryunamad
status
ok
fetched_at
2026-06-22 17:31:34