Automating AWS EC2 + VPC + Internet Gateway + Nginx with Ansible

Ajay Avatar
Automating AWS EC2 + VPC + Internet Gateway + Nginx with Ansible

Automating AWS EC2 + VPC + Internet Gateway + Nginx with Ansible

In this tutorial, we will automate AWS EC2 and Nginx with Ansible, including VPC creation and deploying a website automatically.


Introduction & Why This Matters

Teams often treat infrastructure and application deployment as separate one-off tasks. That creates friction, configuration drift, and frequent “it works on my machine” problems. Using Ansible to manage both the infrastructure layer (VPC, subnets, security posture, route tables) and the application layer (installing packages, copying files, starting services) gives you repeatability and auditability.

This guide is not just code paste,it explains the networking fundamentals that trip up many engineers: for example, you can give an EC2 instance a “public IP”, but unless the subnet the instance sits in is routed to an Internet Gateway (IGW), that public IP is effectively unreachable from the internet. We’ll show a robust Ansible playbook that creates the VPC, subnet, security group, attaches an IGW and route table, launches the instance, writes a dynamic inventory, and then runs a second playbook to configure Nginx and deploy a static site.

 

Prerequisites (and why they matter)

  • AWS account with IAM programmatic access (Access Key & Secret): Ansible uses AWS API calls (boto3) to create resources; these credentials must have permissions to create VPCs, subnets, EC2s, IGWs, route tables, security groups, and to describe resources.
  • Ansible installed — playbooks are executed by the Ansible CLI from your workstation (or CI runner).
  • Python libraries boto3 and botocore — required by Ansible’s AWS collection modules.
  • A key pair (.pem) — used by SSH to connect from your workstation to the EC2 instance for the deployment playbook.
  • Knowledge: YAML, SSH basics, and AWS Console navigation — helps with troubleshooting.

Operational tip: Test your AWS credentials before running playbooks:

aws sts get-caller-identity

This verifies the credentials used by Ansible (if you rely on environment variables or an AWS profile).

 

Architecture & Networking Concepts 

VPC vs Subnet vs Route Table vs IGW

VPC (Virtual Private Cloud) is your virtual network. It defines a large CIDR (for example 10.10.0.0/16) which contains smaller subnets. A subnet is a contiguous block within the VPC (for example 10.10.0.0/24).

Internet Gateway (IGW) is an AWS-managed gateway that provides a route between your VPC and the internet. The IGW must be attached to the VPC. Then your subnet’s route table must have a rule pointing 0.0.0.0/0 → igw-xxxxx. If you omit the IGW or omit the route, instances in that subnet cannot be reached from the internet despite having a public IP.

Public IP vs Private IP

When launching an instance you can assign a public IPv4 address. Two things are required to reach it from your laptop:

  1. Instance has a public IP (either auto-assigned or an Elastic IP).
  2. Subnet route table routes internet-bound traffic to an IGW.

Security Group vs Network ACL

Security Groups are stateful per-instance virtual firewalls. They must allow inbound port 22 for SSH and port 80 for HTTP. NACLs are stateless and apply to the subnet. If you haven’t modified NACLs, default NACLs allow all; usually you don’t need to touch them for simple setups.

Why IGW is essential (concise): An EC2’s public IP is routable on the internet only when its subnet’s route table sends outbound traffic through an IGW. Without IGW the instance cannot send/receive internet packets, security groups do not affect whether the internet route exists.
Official Documentation : Amazon VPC Documentation

Variables — ec2_vars.yml

region: us-east-1
instance_type: t2.micro
ami: ami-020cba7c55df1f615   # Ubuntu 20.04 LTS (example for us-east-1)
key_name: Ansible-1
vpc_name: ansible-vpc-test
cidr_block: "10.10.0.0/16"
cidr: "10.10.0.0/24"


Explanation:

  • region: the AWS region to create resources in.
  • ami: machine image ID. Pick the correct AMI for your region/os.
  • key_name: the key pair you created in the AWS console or via CLI — the .pem must be available locally for Ansible to use SSH.
  • cidr_block and cidr: private address space (do NOT use 0.0.0.0/0 — AWS rejects it). Use RFC1918 private ranges.

 

Infrastructure Playbook — infra.yml (full code)

--- # Creating AWS Infrastructure with Ansible

- name: Creating an EC2 Instance with Ansible
  hosts: localhost
  vars_files:
    - ec2_vars.yml
  tasks:

    - name: Create a new VPC
      amazon.aws.ec2_vpc_net:
        name: "{{ vpc_name }}"
        cidr_block: "{{ cidr_block }}"
        region: "{{ region }}"
      register: vpc

    - name: Create a new Subnet
      amazon.aws.ec2_vpc_subnet:
        cidr: "{{ cidr }}"
        region: "{{ region }}"
        vpc_id: "{{ vpc.vpc.id }}"
        map_public: yes   # <--- Enable public IP assignment
      register: subnet

    - name: Create a Security Group
      amazon.aws.ec2_security_group:
        name: "{{ vpc_name }}-sg"
        description: "Security Group for Ansible test"
        vpc_id: "{{ vpc.vpc.id }}"
        region: "{{ region }}"
        rules:
          - proto: tcp
            ports:
              - 22
            cidr_ip: 0.0.0.0/0
            rule_desc: "allow SSH"
          - proto: tcp
            ports:
              - 80
            cidr_ip: 0.0.0.0/0
            rule_desc: "allow HTTP traffic"
      register: security_group

    - name: Launch an EC2 Instance
      amazon.aws.ec2_instance:
        name: "Test-Ansible"
        key_name: "{{ key_name }}"
        vpc_subnet_id: "{{ subnet.subnet.id }}"
        instance_type: "{{ instance_type }}"
        security_group: "{{ security_group.group_id }}"
        count: 1
        wait: yes
        aws_region: "{{ region }}"
        network:
          assign_public_ip: true
        image_id: "{{ ami }}"
      register: ec2

    - name: Create and attach Internet Gateway
      amazon.aws.ec2_vpc_igw:
        vpc_id: "{{ vpc.vpc.id }}"
        state: present
      register: igw

    - name: Update Route Table with IGW
      amazon.aws.ec2_vpc_route_table:
        vpc_id: "{{ vpc.vpc.id }}"
        routes:
          - dest: 0.0.0.0/0
            gateway_id: "{{ igw.gateway_id }}"
        state: present

    - name: Associate Subnet with Route Table
      amazon.aws.ec2_vpc_route_table:
        vpc_id: "{{ vpc.vpc.id }}"
        subnets:
          - "{{ subnet.subnet.id }}"
        routes:
          - dest: 0.0.0.0/0
            gateway_id: "{{ igw.gateway_id }}"
        state: present

    - name: Save new instance public IP into inventory
      copy:
        dest: ./newinventory.yml
        content: |
          all:
            hosts:
              webserver:
                ansible_host: {{ ec2.instances[0].public_ip_address }}
                ansible_user: ubuntu
                ansible_ssh_private_key_file: ./{{ key_name }}.pem
  

What to watch:

  • map_public: yes ensures subnet will auto-assign public IPv4 addresses to instances (so assign_public_ip works reliably).
  • Launch EC2 with assign_public_ip: true  often redundant with map_public but explicit is safer.
  • IGW created and then route table updated via the same playbook, we explicitly associate the subnet too (the subnets parameter under ec2_vpc_route_table).
  • We write newinventory.yaml so the second playbook can use the new host dynamically.
Common gotcha: Some Ansible AWS modules return different structures. Earlier you saw errors like 'dict object' has no attribute id — the correct attribute for the IGW in recent Ansible collections is igw.gateway_id. If you see an undefined attribute, inspect the registered variable (e.g., debug: var=igw) to find the correct key.

Deployment playbook — deploy.yml (full code)

---
- name: Install Nginx and Deploy Website on Ubuntu
  hosts: webserver
  become: yes
  tasks:
    - name: Update apt repository
      apt:
        update_cache: yes

    - name: Install OpenJDK (example additional package)
      apt:
        name: openjdk-11-jdk
        state: present

    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Start and Enable Nginx
      service:
        name: nginx
        state: started
        enabled: yes

    - name: Deploy Custom Website
      copy:
        src: index.html
        dest: /var/www/html/index.html
        owner: www-data
        group: www-data
        mode: '0644'

    - name: Restart Nginx
      service:
        name: nginx
        state: restarted
  

 

Website file — index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Test Ansible Website</title>
</head>
<body>
  <main>
    <h1>Website Deployed Automatically with Ansible by Instakhabri</h1>
    <p>This static page confirms Nginx is serving files on the EC2 instance.</p>
  </main>
</body>
</html>
  

How to run  exact commands and what you should see

Provision infra:

ansible-playbook infra.yml

Plays that create VPC, subnet, SG, EC2, IGW, and route table. At the end a file newinventory.yaml is created with the host IP.

 

infra.yml

  1. Deploy:
    ansible-playbook -i newinventory.yaml deploy.yml

    SSH connects, packages install, Nginx is started, and the index file is copied.

 

  1. Open browser:
    http://<public-ip-from-newinventory.yaml>

    You should see the “Website Deployed Automatically with Ansible ” page.

If SSH times out: run the manual SSH test:

ssh -i ./Ansible-1.pem ubuntu@<public-ip>

Then check these in the console (see troubleshooting section).

 

Verification & Troubleshooting — step-by-step checks

1. Does the instance have a public IP?

EC2 Console → Instances → check the Public IPv4 column. If empty, instance has no public IP — either assign an Elastic IP or relaunch with auto-assign enabled.

2. Is the subnet associated with the route table that has IGW?

  1. VPC Console → Route Tables → find the route table that contains 0.0.0.0/0 → igw-xxxxx.
  2. Open its Subnet Associations tab and ensure your subnet ID is listed. If not, explicitly associate the subnet.

3. Security Group inbound rules

EC2 Console → Security Groups → confirm inbound rules allow:

  • SSH: TCP 22 from your IP (or 0.0.0.0/0 for testing)
  • HTTP: TCP 80 from 0.0.0.0/0

4. Local environment

Corporate networks sometimes block outbound SSH. Try from a home network, phone hotspot, or use AWS Session Manager / EC2 Instance Connect if permitted.

5. Instance logs

If SSH connects but Nginx fails, check system logs:

sudo journalctl -u nginx -n 200
sudo tail -n 200 /var/log/cloud-init-output.log

6. Common error: ssh: connect to host x.x.x.x port 22: Connection timed out

  1. Verify public IP exists.
  2. Check route table → IGW association.
  3. Confirm Security Group allows inbound 22 from your IP.
  4. Confirm subnet auto-assign public IP (map_public) or that instance has assign_public_ip true.

Cost awareness & cleanup

IGW itself is free. What costs money:

  • EC2 instances (charged per hour, t2.micro is Free Tier eligible).
  • EBS volumes (charged per GB-month).
  • Elastic IPs (free when attached to a running instance, charged when unused).
  • NAT Gateways and Data Transfer — can be costly.

Cleanup checklist (manual)

  1. Terminate EC2 instances.
  2. Release Elastic IPs (if any).
  3. Delete security groups you created (non-default).
  4. Detach and delete IGW.
  5. Delete subnets and VPC.

You can write an Ansible cleanup playbook to delete these resources, but manual deletion is safe if you’re still experimenting.

Security & recommended hardening

  • Avoid 0.0.0.0/0 for SSH in production — restrict SSH to your office/home IP via CIDR.
  • Use IAM roles for EC2 and least-privilege IAM for the user running Ansible.
  • Consider using AWS SSM Session Manager instead of opening SSH to the world.
  • Automate monitoring: CloudWatch alarms for unused instances or high cost services.

Appendix: Ansible module returns & common troubleshooting examples

When you register: igw, run a debug to inspect the structure:

- debug:
    var: igw

Typical structure (example):

igw:
  gateway_id: igw-0abcd1234ef
  state: present

Use the correct key — older examples show igw.id, newer collections return igw.gateway_id. Always inspect registered variables if you see attribute errors.

Conclusion

This guide walked through everything you need to provision a VPC, public subnet, security group, IGW, route table and EC2 instance and then deploy a website using Ansible. The two-playbook separation (infrastructure vs application) is a small investment that pays off in repeatability and safety. You now understand the why behind IGW & routing: without attaching an IGW and associating your subnet’s route table, a public IP is meaningless.

If you want, I can next:

  1. Create a downloadable ZIP containing infra.yml, ec2_vars.yml, deploy.yml, and index.html.
  2. Produce a Canva-ready visual (PNG/SVG) diagram of the architecture to insert into your WordPress post.
  3. Generate a condensed one-page cheat sheet you can print.

Tell me which of the above you’d like and I’ll prepare it next.

Leave a Reply

Your email address will not be published. Required fields are marked *