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
Prerequisites (and why each item is required)
Architecture & Networking Concepts (VPC, Subnets, IGW, Route Tables, Public IPs)
Variables (`ec2_vars.yml`) — explanation
Infrastructure playbook (`infra.yml`) — full code + line-by-line explanation
Deployment playbook (`deploy.yml`) — full code + explanation
Website file (`index.html`)
How to run (commands + expected console output)
Verification & Troubleshooting checklist (precise console checks)
Cost & cleanup
Security & production hardening
Appendix: module return examples & common errors
Conclusion
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
andbotocore
— 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:
- Instance has a public IP (either auto-assigned or an Elastic IP).
- 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.
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
andcidr
: 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 withmap_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 underec2_vpc_route_table
). - We write
newinventory.yaml
so the second playbook can use the new host dynamically.
'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.
- Deploy:
ansible-playbook -i newinventory.yaml deploy.yml
SSH connects, packages install, Nginx is started, and the index file is copied.
- 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?
- VPC Console → Route Tables → find the route table that contains
0.0.0.0/0 → igw-xxxxx
. - 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
- Verify public IP exists.
- Check route table → IGW association.
- Confirm Security Group allows inbound 22 from your IP.
- 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)
- Terminate EC2 instances.
- Release Elastic IPs (if any).
- Delete security groups you created (non-default).
- Detach and delete IGW.
- 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:
- Create a downloadable ZIP containing
infra.yml
,ec2_vars.yml
,deploy.yml
, andindex.html
. - Produce a Canva-ready visual (PNG/SVG) diagram of the architecture to insert into your WordPress post.
- 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